TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
371 / 389
95.4%
Branches:
218 / 238
91.6%
Functions:
33 / 33
100.0%

python/src/albedo.cpp
Line Branch Exec Source
1 #include "bindings.h"
2 #include "common.h"
3 #include "objects.h"
4 #include "trampolines.h"
5
6 #include "terraingen/albedo/albedo.h"
7 #include "terraingen/albedo/blast_zones.h"
8 #include "terraingen/albedo/crater_albedo.h"
9 #include "terraingen/albedo/maturity.h"
10 #include "terraingen/albedo/raster_albedo.h"
11 #include "terraingen/albedo/rock_albedo.h"
12 #include "terraingen/albedo/slope_albedo.h"
13 #include "terraingen/albedo/splotches.h"
14 #include "terraingen/albedo/synthetic.h"
15 #include "terraingen/pipeline/terrain.h"
16
17 #include <optional>
18 #include <stdexcept>
19 #include <string>
20 #include <tuple>
21 #include <utility>
22 #include <vector>
23
24 namespace terraingen::python {
25
26 namespace {
27
28 /// @brief One albedo value from Python: a number for one band, or a sequence of up to three.
29 66177 AlbedoSample ToSample(nb::handle value) {
30 66177 AlbedoSample sample{};
31
5/6
✓ Branch 0 taken 66113 times.
✓ Branch 1 taken 64 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 66113 times.
✓ Branch 4 taken 64 times.
✓ Branch 5 taken 66113 times.
132290 if (nb::isinstance<nb::float_>(value) || nb::isinstance<nb::int_>(value)) {
32
1/1
✓ Branch 1 taken 64 times.
64 sample.fill(nb::cast<float>(value));
33 64 return sample;
34 }
35 66113 std::size_t band = 0;
36
5/5
✓ Branch 1 taken 66113 times.
✓ Branch 5 taken 66113 times.
✓ Branch 8 taken 132226 times.
✓ Branch 10 taken 66113 times.
✓ Branch 11 taken 66113 times.
132226 for (nb::handle item : value) {
37
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 66113 times.
66113 if (band == sample.size()) {
38
0/1
✗ Branch 2 not taken.
✗ throw std::invalid_argument("An albedo value has at most three bands");
39 }
40
1/1
✓ Branch 2 taken 66113 times.
66113 sample[band++] = nb::cast<float>(item);
41 66113 }
42 66113 return sample;
43 }
44
45 /// @brief The first `bands` values of `sample`.
46 66117 std::vector<float> Bands(const AlbedoSample& sample, std::size_t bands) {
47
1/1
✓ Branch 3 taken 66117 times.
132234 return std::vector<float>(sample.begin(), sample.begin() + static_cast<long>(bands));
48 }
49
50 /// @brief An albedo source written in Python: `bands()` (1 or 3) and `value_at(x, y, context)`, a
51 /// number or a sequence of `bands()` numbers.
52 class PyAlbedoSource : public AlbedoSource {
53 public:
54 NB_TRAMPOLINE(AlbedoSource);
55
56 1 std::size_t Bands() const override {
57
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
6 NB_OVERRIDE_PURE_NAME("bands", Bands);
58 }
59
60 64 AlbedoSample ValueAt(double x, double y, const AlbedoContext& context) const override {
61 64 return CallRequired(nb_trampoline, "value_at", &ToSample, x, y, context);
62 }
63 };
64
65 /// @brief An albedo layer written in Python: `apply_at(value, bands, x, y, context)` returns the
66 /// new value (a number or a sequence of `bands` numbers) from the one so far (a list).
67 class PyAlbedoLayer : public AlbedoLayer {
68 public:
69 NB_TRAMPOLINE(AlbedoLayer);
70
71 66113 void ApplyAt(AlbedoSample& value, std::size_t bands, double x, double y,
72 const AlbedoContext& context) const override {
73
2/2
✓ Branch 1 taken 66113 times.
✓ Branch 4 taken 66113 times.
66113 value = CallRequired(nb_trampoline, "apply_at", &ToSample, Bands(value, bands), bands, x, y,
74 context);
75 66113 }
76 };
77
78 44 void BindBases(nb::module_& m) {
79 ✗ nb::class_<AlbedoContext>(m, "AlbedoContext", "How an albedo is evaluated.")
80 44 .def(
81 "__init__",
82 46 [](AlbedoContext* self, double spacing, unsigned threads) {
83 2 new (self) AlbedoContext{spacing, threads};
84 2 },
85
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "spacing"_a = 1.0, "threads"_a = 1)
86 44 .def_ro("spacing", &AlbedoContext::spacing)
87 .def_ro("threads", &AlbedoContext::threads);
88
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_albedo_context", &ValidateAlbedoContext, "context"_a, kValidateDoc);
89 88 nb::class_<AlbedoSource, PyAlbedoSource>(
90 m, "AlbedoSource",
91 "Where an albedo starts. Subclass it and implement `bands()` (1 or 3) and `value_at(x, "
92 "y, context)` for a source of your own.")
93 44 .def(nb::init<>())
94 44 .def("bands", &AlbedoSource::Bands)
95
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
88 .def("value_at", &AlbedoSource::ValueAt, "x"_a, "y"_a, "context"_a);
96 44 nb::class_<AlbedoLayer, PyAlbedoLayer>(
97 m, "AlbedoLayer",
98 "A change to an albedo. Subclass it and implement `apply_at(value, bands, x, y, "
99 "context)` for a layer of your own.")
100 44 .def(nb::init<>())
101 .def(
102 "apply_at",
103 45 [](const AlbedoLayer& layer, const std::vector<float>& value, std::size_t bands,
104 double x, double y, const AlbedoContext& context) {
105 1 AlbedoSample sample{};
106
5/6
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 1 time.
3 for (std::size_t band = 0; band < value.size() && band < sample.size(); ++band) {
107 1 sample[band] = value[band];
108 }
109
1/1
✓ Branch 1 taken 1 time.
1 layer.ApplyAt(sample, bands, x, y, context);
110
1/1
✓ Branch 1 taken 1 time.
2 return Bands(sample, bands);
111 },
112
5/5
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
✓ Branch 13 taken 44 times.
88 "value"_a, "bands"_a, "x"_a, "y"_a, "context"_a);
113 44 }
114
115 44 void BindSynthetic(nb::module_& m) {
116 ✗ nb::class_<SyntheticAlbedo, AlbedoSource>(
117 m, "SyntheticAlbedo",
118 "A synthetic albedo: `value` (reflectance), varied by `value_noise` (relative), and with "
119 "`rgb` three bands from `hue` and `saturation` and their noise.")
120 .def(
121 "__init__",
122 62 [](SyntheticAlbedo* self, double value, std::vector<NoiseLayer> valueNoise, bool rgb,
123 double hue, double saturation, std::vector<NoiseLayer> hueNoise,
124 std::vector<NoiseLayer> saturationNoise, const Vec2d& origin,
125 const Surface& surface) {
126 18 SyntheticAlbedoParams params;
127 18 params.value = value;
128 18 params.valueNoise = std::move(valueNoise);
129 18 params.rgb = rgb;
130 18 params.hue = hue;
131 18 params.saturation = saturation;
132 18 params.hueNoise = std::move(hueNoise);
133 18 params.saturationNoise = std::move(saturationNoise);
134
1/3
✓ Branch 5 taken 18 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
18 new (self) SyntheticAlbedo(std::move(params), origin, surface);
135 18 },
136
2/2
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
132 "value"_a = kDefaultSyntheticAlbedoValue, "value_noise"_a = std::vector<NoiseLayer>{},
137
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
176 "rgb"_a = false, "hue"_a = 0.0, "saturation"_a = 0.0,
138
1/1
✓ Branch 2 taken 44 times.
88 "hue_noise"_a = std::vector<NoiseLayer>{},
139
2/2
✓ Branch 1 taken 44 times.
✓ Branch 5 taken 44 times.
132 "saturation_noise"_a = std::vector<NoiseLayer>{}, "origin"_a = Vec2d{},
140
1/1
✓ Branch 2 taken 44 times.
88 "surface"_a = Surface{});
141
1/1
✓ Branch 1 taken 44 times.
44 m.def(
142 "hsv_to_rgb",
143 ✗ [](double hue, double saturation, double value) {
144
1/1
✓ Branch 1 taken 1 time.
1 const AlbedoSample rgb = HsvToRgb(hue, saturation, value);
145 2 return std::tuple{rgb[0], rgb[1], rgb[2]};
146 },
147
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 "hue"_a, "saturation"_a, "value"_a, "A colour's (red, green, blue), hue in degrees.");
148 44 }
149
150 44 void BindRasterAlbedo(nb::module_& m) {
151 auto detail = nb::class_<AlbedoDetail>(
152 m, "AlbedoDetail",
153 "Sub-cell detail added to an albedo map: `octaves` of noise carrying the map's measured "
154 44 "roughness (or `hurst`) one octave down at a time, times `scale`.");
155
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<AlbedoDetail>(detail);
156 44 detail.def_rw("enabled", &AlbedoDetail::enabled)
157 44 .def_rw("seed", &AlbedoDetail::seed)
158 44 .def_rw("octaves", &AlbedoDetail::octaves)
159 44 .def_rw("scale", &AlbedoDetail::scale)
160 44 .def_rw("hurst", &AlbedoDetail::hurst)
161 .def_rw("fade", &AlbedoDetail::fade);
162
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_albedo_detail", &ValidateAlbedoDetail, "detail"_a, kValidateDoc);
163 auto options = nb::class_<RasterAlbedoOptions>(
164 44 m, "RasterAlbedoOptions", "How an albedo map is interpolated and given detail.");
165
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<RasterAlbedoOptions>(options);
166 44 options.def_rw("method", &RasterAlbedoOptions::method)
167 .def_rw("detail", &RasterAlbedoOptions::detail);
168 ✗ nb::class_<RasterAlbedo, AlbedoSource>(
169 m, "RasterAlbedo",
170 "An albedo map (1 or 3 bands, such as a normal albedo mosaic from orbit) interpolated to "
171 "the grid and given sub-cell detail; NaN outside it.")
172
1/1
✓ Branch 1 taken 44 times.
44 .def(nb::init<std::vector<Raster<float>>, RasterAlbedoOptions, Surface>(), "bands"_a,
173
2/2
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
132 "options"_a = RasterAlbedoOptions{}, "surface"_a = Surface{})
174 44 .def_prop_ro("hurst", &RasterAlbedo::Hurst, "The Hurst exponent the detail uses.")
175 88 .def_prop_ro("cell_roughness", &RasterAlbedo::CellRoughness,
176 "The roughness of the log brightness at one cell, as measured.");
177 44 }
178
179 44 void BindRetention(nb::module_& m) {
180 auto retention = nb::class_<RayRetention>(
181 m, "RayRetention",
182 "How long rays stay visible (Myr) by crater diameter (m): `small_myr` up to "
183 "`small_diameter`, `mid_myr` from `mid_diameter`, `large_myr` above `large_diameter`, "
184 44 "all times `scale`.");
185
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<RayRetention>(retention);
186 44 retention.def_rw("small_diameter", &RayRetention::smallDiameter)
187 44 .def_rw("small_myr", &RayRetention::smallMyr)
188 44 .def_rw("mid_diameter", &RayRetention::midDiameter)
189 44 .def_rw("mid_myr", &RayRetention::midMyr)
190 44 .def_rw("large_diameter", &RayRetention::largeDiameter)
191 44 .def_rw("large_myr", &RayRetention::largeMyr)
192 44 .def_rw("scale", &RayRetention::scale)
193
1/1
✓ Branch 1 taken 44 times.
44 .def("myr", &RayRetention::Myr, "diameter"_a);
194
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_ray_retention", &ValidateRayRetention, "retention"_a, kValidateDoc);
195 44 }
196
197 44 void BindRays(nb::module_& m) {
198 auto oblique = nb::class_<ObliqueImpacts>(
199 m, "ObliqueImpacts",
200 "Ray patterns of oblique impacts: below `asymmetric_degrees` the ejecta is asymmetric, "
201 "below `forbidden_degrees` an uprange zone has no rays, below `butterfly_degrees` rays "
202 44 "form a butterfly; impacts come from `heading` (degrees from north) or any direction.");
203
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<ObliqueImpacts>(oblique);
204 44 oblique.def_rw("enabled", &ObliqueImpacts::enabled)
205 44 .def_rw("heading", &ObliqueImpacts::heading)
206 44 .def_rw("asymmetric_degrees", &ObliqueImpacts::asymmetricDegrees)
207 44 .def_rw("forbidden_degrees", &ObliqueImpacts::forbiddenDegrees)
208 44 .def_rw("butterfly_degrees", &ObliqueImpacts::butterflyDegrees)
209 44 .def_rw("downrange_speed_ratio", &ObliqueImpacts::downrangeSpeedRatio)
210 .def_rw("zone_half_width_degrees", &ObliqueImpacts::zoneHalfWidthDegrees);
211
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_oblique_impacts", &ValidateObliqueImpacts, "oblique"_a, kValidateDoc);
212 auto rays = nb::class_<CraterRays>(
213 m, "CraterRays",
214 "Bright rays: how many (`count`), how long (Elliott et al. 2018: `length_coefficient` * "
215 "(D/2 in km)^`length_exponent` km, spread by `length_spread`), how wide, how bright "
216 44 "(`excess` maturity) and how patchy.");
217
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterRays>(rays);
218 44 rays.def_rw("enabled", &CraterRays::enabled)
219 44 .def_rw("count", &CraterRays::count)
220 44 .def_rw("length_coefficient", &CraterRays::lengthCoefficient)
221 44 .def_rw("length_exponent", &CraterRays::lengthExponent)
222 44 .def_rw("length_spread", &CraterRays::lengthSpread)
223 44 .def_rw("width_degrees", &CraterRays::widthDegrees)
224 44 .def_rw("excess", &CraterRays::excess)
225 44 .def_rw("patchiness", &CraterRays::patchiness)
226 44 .def_rw("patch_radii", &CraterRays::patchRadii)
227 44 .def_rw("oblique", &CraterRays::oblique)
228 44 .def_rw("layout", &CraterRays::layout)
229
1/1
✓ Branch 1 taken 44 times.
44 .def("median_length", &CraterRays::MedianLength, "diameter"_a);
230
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_rays", &ValidateCraterRays, "rays"_a, kValidateDoc);
231 44 }
232
233 44 void BindCraterAlbedoOptions(nb::module_& m) {
234 auto texture = nb::class_<CraterAlbedoTexture>(
235 m, "CraterAlbedoTexture",
236 "Texture of fresh craters' bright material: `streaks` breaking it into filaments "
237 "`streak_width_degrees` wide and `streak_length_radii` long, `lobes` (`lobe_count` of "
238 44 "them) wandering the ejecta edge, and `floor_share` dimming floors under walls.");
239
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterAlbedoTexture>(texture);
240 44 texture.def_rw("streaks", &CraterAlbedoTexture::streaks)
241 44 .def_rw("streak_width_degrees", &CraterAlbedoTexture::streakWidthDegrees)
242 44 .def_rw("streak_length_radii", &CraterAlbedoTexture::streakLengthRadii)
243 44 .def_rw("continuous_radii", &CraterAlbedoTexture::continuousRadii)
244 44 .def_rw("lobes", &CraterAlbedoTexture::lobes)
245 44 .def_rw("lobe_count", &CraterAlbedoTexture::lobeCount)
246 44 .def_rw("floor_share", &CraterAlbedoTexture::floorShare)
247 .def_rw("seed", &CraterAlbedoTexture::seed);
248
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_albedo_texture", &ValidateCraterAlbedoTexture, "texture"_a,
249 kValidateDoc);
250 auto zone = nb::class_<ReflectanceZone>(
251 m, "ReflectanceZone",
252 "A reflectance zone around new small craters: `share` of craters have it, it changes "
253 "reflectance by `change` and reaches `coefficient` * D^`exponent` m times `scale`, with "
254 44 "fingers pulling in by `raggedness` and an edge fading over `feather`.");
255
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<ReflectanceZone>(zone);
256 44 zone.def_rw("share", &ReflectanceZone::share)
257 44 .def_rw("change", &ReflectanceZone::change)
258 44 .def_rw("coefficient", &ReflectanceZone::coefficient)
259 44 .def_rw("exponent", &ReflectanceZone::exponent)
260 44 .def_rw("scale", &ReflectanceZone::scale)
261 44 .def_rw("raggedness", &ReflectanceZone::raggedness)
262 44 .def_rw("finger_width_degrees", &ReflectanceZone::fingerWidthDegrees)
263 44 .def_rw("feather", &ReflectanceZone::feather)
264 .def_rw("retention_myr", &ReflectanceZone::retentionMyr);
265
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_reflectance_zone", &ValidateReflectanceZone, "zone"_a, kValidateDoc);
266 auto zones = nb::class_<CraterReflectanceZones>(
267 m, "CraterReflectanceZones",
268 "The proximal and distal, high and low reflectance zones around new craters up to "
269 "`max_diameter` (Robinson et al. 2015, Speyerer et al. 2016); the proximal high zone only "
270 44 "where `excavation_ratio` * D exceeds `mature_depth`.");
271
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterReflectanceZones>(zones);
272 44 zones.def_rw("enabled", &CraterReflectanceZones::enabled)
273 44 .def_rw("max_diameter", &CraterReflectanceZones::maxDiameter)
274 44 .def_rw("mature_depth", &CraterReflectanceZones::matureDepth)
275 44 .def_rw("excavation_ratio", &CraterReflectanceZones::excavationRatio)
276 44 .def_rw("proximal_high", &CraterReflectanceZones::proximalHigh)
277 44 .def_rw("proximal_low", &CraterReflectanceZones::proximalLow)
278 44 .def_rw("distal_high", &CraterReflectanceZones::distalHigh)
279 44 .def_rw("distal_low", &CraterReflectanceZones::distalLow)
280 44 .def_rw("seed", &CraterReflectanceZones::seed)
281
1/1
✓ Branch 1 taken 44 times.
88 .def("max_reach", &CraterReflectanceZones::MaxReach, "diameter"_a);
282 auto shadows = nb::class_<ZoneShadows>(
283 m, "ZoneShadows",
284 "Shadows of rises in the distal zones: a jet from the rim that drops at most "
285 "`jet_angle_degrees`, full shadow `depth` m below it, sampled on `azimuths` rays of "
286 44 "`radial_samples`.");
287
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<ZoneShadows>(shadows);
288 44 shadows.def_rw("enabled", &ZoneShadows::enabled)
289 44 .def_rw("jet_angle_degrees", &ZoneShadows::jetAngleDegrees)
290 44 .def_rw("depth", &ZoneShadows::depth)
291 44 .def_rw("azimuths", &ZoneShadows::azimuths)
292 .def_rw("radial_samples", &ZoneShadows::radialSamples);
293 zones.def_rw("shadows", &CraterReflectanceZones::shadows);
294
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_reflectance_zones", &ValidateCraterReflectanceZones, "zones"_a,
295 kValidateDoc);
296 auto options = nb::class_<CraterAlbedoOptions>(
297 m, "CraterAlbedoOptions",
298 "Which craters get bright material (diameters, ages, `share`), how much brighter their "
299 44 "interiors and ejecta are in optical maturity, their rays, and how fast it all fades.");
300
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterAlbedoOptions>(options);
301 44 options.def_rw("seed", &CraterAlbedoOptions::seed)
302 .def_rw("selection", &CraterAlbedoOptions::selection);
303
1/1
✓ Branch 1 taken 44 times.
44 BindSelection(options, &CraterAlbedoOptions::selection, "min_diameter", "max_diameter");
304 44 options.def_rw("share", &CraterAlbedoOptions::share)
305 44 .def_rw("background_maturity", &CraterAlbedoOptions::backgroundMaturity)
306 44 .def_rw("max_maturity", &CraterAlbedoOptions::maxMaturity)
307 44 .def_rw("interior_excess", &CraterAlbedoOptions::interiorExcess)
308 44 .def_rw("ejecta_excess", &CraterAlbedoOptions::ejectaExcess)
309 44 .def_rw("ejecta_exponent", &CraterAlbedoOptions::ejectaExponent)
310 44 .def_rw("ejecta_reach_radii", &CraterAlbedoOptions::ejectaReachRadii)
311 44 .def_rw("rays", &CraterAlbedoOptions::rays)
312 44 .def_rw("texture", &CraterAlbedoOptions::texture)
313 44 .def_rw("zones", &CraterAlbedoOptions::zones)
314 44 .def_rw("retention", &CraterAlbedoOptions::retention)
315 44 .def_rw("fade_exponent", &CraterAlbedoOptions::fadeExponent)
316 44 .def_rw("origin_reflectance", &CraterAlbedoOptions::originReflectance)
317 .def_rw("fade", &CraterAlbedoOptions::fade);
318
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_albedo", &ValidateCraterAlbedo, "options"_a, kValidateDoc);
319 44 }
320
321 44 void BindCraterAlbedo(nb::module_& m) {
322 44 BindRetention(m);
323 44 BindRays(m);
324 44 BindCraterAlbedoOptions(m);
325 44 nb::class_<CraterAlbedo, AlbedoLayer>(m, "CraterAlbedo",
326 "Bright fresh craters and rays from the craters of "
327 "`craters`, fading with age.")
328
1/1
✓ Branch 1 taken 44 times.
44 .def(nb::init<std::shared_ptr<CraterLayer>, CraterAlbedoOptions>(), "craters"_a,
329
1/1
✓ Branch 15 taken 44 times.
88 "options"_a = CraterAlbedoOptions{})
330
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
88 .def("excess", &CraterAlbedo::Excess, "crater"_a, "x"_a, "y"_a, "spacing"_a,
331 "The excess optical maturity `crater` gives (x, y).")
332 44 .def(
333 "reflectance_change",
334 ✗ [](const CraterAlbedo& layer, const Crater& crater, double x, double y, double spacing,
335 const PyTerrain* terrain) {
336
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4 if (terrain == nullptr) {
337
1/1
✓ Branch 1 taken 3 times.
3 return layer.ReflectanceChange(crater, x, y, spacing);
338 }
339
1/1
✓ Branch 1 taken 1 time.
1 const Terrain built = terrain->Get();
340
1/1
✓ Branch 2 taken 1 time.
1 const TerrainGround ground(built, built.layers.size());
341
1/1
✓ Branch 1 taken 1 time.
1 return layer.ReflectanceChange(crater, x, y, spacing, &ground);
342 1 },
343
5/5
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
✓ Branch 13 taken 44 times.
132 "crater"_a, "x"_a, "y"_a, "spacing"_a, "terrain"_a = nb::none(),
344 "The relative reflectance change the zones of `crater` give (x, y), shadowed by "
345 "`terrain`'s heights when given.")
346
1/1
✓ Branch 1 taken 44 times.
44 .def("impact", &CraterAlbedo::Impact, "crater"_a,
347 "The impact `crater` came from, drawn from its ID.")
348
1/1
✓ Branch 1 taken 44 times.
88 .def("reach", &CraterAlbedo::Reach, "crater"_a,
349 "How far (m) from its centre `crater` can brighten the ground.");
350 88 nb::class_<CraterImpact>(m, "CraterImpact",
351 "An impact: its angle from the horizontal, downrange heading and "
352 "downrange-to-uprange ejection speed ratio.")
353 44 .def_ro("angle_degrees", &CraterImpact::angleDegrees)
354 44 .def_ro("heading", &CraterImpact::heading, "Radians counter-clockwise from east.")
355 .def_ro("speed_ratio", &CraterImpact::speedRatio);
356 44 }
357
358 44 void BindSplotches(nb::module_& m) {
359 auto swarms = nb::class_<SplotchSwarms>(
360 m, "SplotchSwarms",
361 "Swarms of splotches around new craters (Robinson et al. 2015), scaled by ejecta mass "
362 44 "(Housen and Holsapple 2011); off unless `enabled`.");
363
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<SplotchSwarms>(swarms);
364 44 swarms.def_rw("enabled", &SplotchSwarms::enabled)
365 44 .def_rw("crater_rate", &SplotchSwarms::craterRate)
366 44 .def_rw("crater_slope", &SplotchSwarms::craterSlope)
367 44 .def_rw("min_crater_diameter", &SplotchSwarms::minCraterDiameter)
368 44 .def_rw("max_crater_diameter", &SplotchSwarms::maxCraterDiameter)
369 44 .def_rw("reference_diameter", &SplotchSwarms::referenceDiameter)
370 44 .def_rw("reference_count", &SplotchSwarms::referenceCount)
371 44 .def_rw("mu", &SplotchSwarms::mu)
372 44 .def_rw("min_range", &SplotchSwarms::minRange)
373 44 .def_rw("max_range", &SplotchSwarms::maxRange)
374 44 .def_rw("splotch_diameter", &SplotchSwarms::splotchDiameter)
375 44 .def_rw("dark_share", &SplotchSwarms::darkShare)
376 44 .def_rw("bright_share", &SplotchSwarms::brightShare)
377 44 .def_rw("dark_change", &SplotchSwarms::darkChange)
378 44 .def_rw("bright_change", &SplotchSwarms::brightChange)
379 44 .def_rw("wedge_share", &SplotchSwarms::wedgeShare)
380 44 .def_rw("wedge_elongation", &SplotchSwarms::wedgeElongation)
381 44 .def_rw("craters", &SplotchSwarms::craters)
382
1/1
✓ Branch 1 taken 44 times.
88 .def("count", &SplotchSwarms::Count, "diameter"_a,
383 "Splotches a crater `diameter` m across throws, on average.");
384 auto options = nb::class_<SplotchOptions>(
385 m, "SplotchOptions",
386 "Splotches from secondary impacts (Speyerer et al. 2016): formed at `rate_coefficient` * "
387 "d^-`rate_exponent` per km^2 per year (d in km), lasting `lifetime_myr`, `min_diameter` to "
388 44 "`max_diameter` m across; `dark_share` dark, `bright_share` bright, the rest mixed.");
389
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<SplotchOptions>(options);
390 44 options.def_rw("seed", &SplotchOptions::seed)
391 44 .def_rw("rate_coefficient", &SplotchOptions::rateCoefficient)
392 44 .def_rw("rate_exponent", &SplotchOptions::rateExponent)
393 44 .def_rw("lifetime_myr", &SplotchOptions::lifetimeMyr)
394 44 .def_rw("min_diameter", &SplotchOptions::minDiameter)
395 44 .def_rw("max_diameter", &SplotchOptions::maxDiameter)
396 44 .def_rw("dark_share", &SplotchOptions::darkShare)
397 44 .def_rw("bright_share", &SplotchOptions::brightShare)
398 44 .def_rw("dark_change", &SplotchOptions::darkChange)
399 44 .def_rw("bright_change", &SplotchOptions::brightChange)
400 44 .def_rw("steep_bright_share", &SplotchOptions::steepBrightShare)
401 44 .def_rw("steep_slope_degrees", &SplotchOptions::steepSlopeDegrees)
402 44 .def_rw("softness", &SplotchOptions::softness)
403 44 .def_rw("raggedness", &SplotchOptions::raggedness)
404 44 .def_rw("fade", &SplotchOptions::fade)
405 44 .def_rw("swarms", &SplotchOptions::swarms)
406 44 .def("coefficient", &SplotchOptions::Coefficient,
407 "Splotches of at least 1 m per m^2 at any time.");
408
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_splotches", &ValidateSplotches, "options"_a, kValidateDoc);
409 ✗ nb::class_<SplotchAlbedo, AlbedoLayer>(
410 m, "SplotchAlbedo", "Dark and bright splotches over the surface, the youngest on top.")
411
1/1
✓ Branch 1 taken 44 times.
88 .def(nb::init<SplotchOptions, Surface>(), "options"_a = SplotchOptions{},
412
1/1
✓ Branch 2 taken 44 times.
88 "surface"_a = Surface{})
413 44 .def(
414 "count",
415 ✗ [](const SplotchAlbedo& layer, const Bounds2d& region, double spacing) {
416
1/1
✓ Branch 1 taken 1 time.
1 return layer.Splotches(region, spacing).size();
417 },
418
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 "region"_a, "spacing"_a, "How many splotches resolved at `spacing` m reach `region`.")
419 44 .def(
420 "swarm_size",
421 45 [](const SplotchAlbedo& layer, const Crater& crater, double spacing) {
422
1/1
✓ Branch 1 taken 1 time.
1 return layer.Swarm(crater, spacing).size();
423 },
424
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "crater"_a, "spacing"_a = 0.0, "How many splotches `crater`'s swarm holds.")
425 88 .def_prop_ro("options", &SplotchAlbedo::Options);
426 44 }
427
428 44 void BindBlastZones(nb::module_& m) {
429 auto zone = nb::class_<BlastZone>(
430 m, "BlastZone",
431 "The zone around a lander its engine scoured (Clegg et al. 2014): `area` m^2 brighter by "
432 "`change` on average, elongated along `heading_degrees`, in a halo reaching `halo_reach` "
433 44 "times as far, around a dark core of `core_radius` m.");
434
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<BlastZone>(zone);
435 44 zone.def_rw("x", &BlastZone::x)
436 44 .def_rw("y", &BlastZone::y)
437 44 .def_rw("area", &BlastZone::area)
438 44 .def_rw("elongation", &BlastZone::elongation)
439 44 .def_rw("heading_degrees", &BlastZone::headingDegrees)
440 44 .def_rw("change", &BlastZone::change)
441 44 .def_rw("halo_change", &BlastZone::haloChange)
442 44 .def_rw("halo_reach", &BlastZone::haloReach)
443 44 .def_rw("core_radius", &BlastZone::coreRadius)
444 44 .def_rw("core_change", &BlastZone::coreChange)
445 44 .def_rw("raggedness", &BlastZone::raggedness)
446 .def_rw("seed", &BlastZone::seed);
447
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_blast_zone", &ValidateBlastZone, "zone"_a, kValidateDoc);
448 ✗ nb::class_<BlastZoneAlbedo, AlbedoLayer>(m, "BlastZoneAlbedo",
449 "Blast zones around landers, brightening the albedo.")
450
2/2
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
132 .def(nb::init<std::vector<BlastZone>, Surface>(), "zones"_a, "surface"_a = Surface{})
451
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 .def("change", &BlastZoneAlbedo::Change, "zone"_a, "x"_a, "y"_a,
452 "The relative reflectance change `zone` gives (x, y).")
453
1/1
✓ Branch 1 taken 44 times.
44 .def_static("reach", &BlastZoneAlbedo::Reach, "zone"_a,
454 "How far (m) from its lander a zone changes the ground.")
455 88 .def_prop_ro("zones", &BlastZoneAlbedo::Zones);
456 44 }
457
458 44 void BindMaturity(nb::module_& m) {
459 auto origin = nb::class_<OpticalMaturityOrigin>(
460 m, "OpticalMaturityOrigin",
461 "The origin of Lucey et al.'s optical maturity: the reflectance at 750 nm and the 950/750 "
462 44 "ratio that fully immature material tends to.");
463
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<OpticalMaturityOrigin>(origin);
464 44 origin.def_rw("reflectance", &OpticalMaturityOrigin::reflectance)
465 .def_rw("ratio", &OpticalMaturityOrigin::ratio);
466
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("optical_maturity", &OpticalMaturity, "r750"_a, "r950"_a,
467
1/1
✓ Branch 1 taken 44 times.
88 "origin"_a = OpticalMaturityOrigin{},
468 "Lucey et al.'s (2000) optical maturity of reflectances at 750 and 950 nm.");
469
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("reflectance_at_maturity", &ReflectanceAtMaturity, "reflectance"_a, "reference"_a,
470
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "maturity"_a, "origin_reflectance"_a = OpticalMaturityOrigin{}.reflectance,
471 "The reflectance of material at `maturity`, from `reflectance` at the `reference` "
472 "maturity.");
473 44 }
474
475 44 void BindEvaluation(nb::module_& m) {
476 44 nb::class_<PyAlbedo>(m, "Albedo", "An albedo: `source`, changed by each of `layers` in order.")
477 44 .def(
478 "__init__",
479 67 [](PyAlbedo* self, std::shared_ptr<AlbedoSource> source,
480 std::vector<std::shared_ptr<AlbedoLayer>> layers, const Surface& surface) {
481 23 new (self) PyAlbedo{std::move(source), std::move(layers), surface};
482 23 },
483
2/2
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
132 "source"_a, "layers"_a = std::vector<std::shared_ptr<AlbedoLayer>>{},
484
1/1
✓ Branch 2 taken 44 times.
88 "surface"_a = Surface{})
485 44 .def_rw("surface", &PyAlbedo::surface)
486 44 .def_rw("source", &PyAlbedo::source)
487 .def_rw("layers", &PyAlbedo::layers);
488
1/1
✓ Branch 1 taken 44 times.
44 m.def(
489 "evaluate_albedo_at",
490 ✗ [](const PyAlbedo& albedo, double x, double y, double spacing, const PyTerrain* terrain) {
491 const std::optional<Terrain> built =
492
5/7
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
✓ Branch 3 taken 1 time.
✓ Branch 7 taken 1 time.
✓ Branch 8 taken 2 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3 terrain ? std::optional<Terrain>(terrain->Get()) : std::nullopt;
493 const std::optional<TerrainGround> ground =
494
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
3 built ? std::optional<TerrainGround>(std::in_place, *built, built->layers.size())
495
1/1
✓ Branch 2 taken 1 time.
3 : std::nullopt;
496
1/1
✓ Branch 1 taken 3 times.
3 const AlbedoSample sample = EvaluateAlbedoAt(
497
3/3
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✓ Branch 5 taken 3 times.
6 albedo.Get(), x, y, AlbedoContext{spacing, 1, ground ? &*ground : nullptr});
498
3/4
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 3 times.
✓ Branch 8 taken 3 times.
6 return Bands(sample, albedo.source ? albedo.source->Bands() : 1);
499 3 },
500
5/5
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
✓ Branch 13 taken 44 times.
132 "albedo"_a, "x"_a, "y"_a, "spacing"_a, "terrain"_a = nb::none(),
501 "The albedo's bands at (x, y) sampled every `spacing` metres, equal to the grid cell "
502 "centred there; layers shaped by the ground read `terrain`'s heights when given.");
503
1/1
✓ Branch 1 taken 44 times.
44 m.def(
504 "evaluate_albedo",
505 ✗ [](const PyAlbedo& albedo, const RasterGrid& grid, double spacing, unsigned threads,
506 const PyTerrain* terrain) {
507 const std::optional<Terrain> built =
508
5/7
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 21 times.
✓ Branch 3 taken 1 time.
✓ Branch 7 taken 1 time.
✓ Branch 8 taken 21 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
22 terrain ? std::optional<Terrain>(terrain->Get()) : std::nullopt;
509 const std::optional<TerrainGround> ground =
510
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 21 times.
22 built ? std::optional<TerrainGround>(std::in_place, *built, built->layers.size())
511
1/1
✓ Branch 2 taken 1 time.
22 : std::nullopt;
512 22 return EvaluateAlbedo(albedo.Get(), grid,
513
4/4
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 21 times.
✓ Branch 5 taken 22 times.
✓ Branch 8 taken 22 times.
44 AlbedoContext{spacing, threads, ground ? &*ground : nullptr});
514 22 },
515
5/5
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
✓ Branch 13 taken 44 times.
220 "albedo"_a, "grid"_a, "spacing"_a = 0.0, "threads"_a = 0, "terrain"_a = nb::none(),
516 44 nb::call_guard<nb::gil_scoped_release>(),
517 "The albedo's bands over `grid` (such as `RasterGrid.from_raster(heights)` or a source's "
518 "`grid()`), sampled every `spacing` metres (0 for the grid's), one Raster per band; layers "
519 "shaped by the ground read `terrain`'s heights when given.");
520 44 }
521
522 44 void BindRocks(nb::module_& m) {
523 auto options = nb::class_<RockAlbedoOptions>(
524 m, "RockAlbedoOptions",
525 "How rock brightens the albedo: `reflectance_ratio` times the regolith's (2.5 after "
526 44 "Marshal et al. 2023), and with `unresolved` a speckle of rock too small to resolve.");
527
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<RockAlbedoOptions>(options);
528 44 options.def_rw("seed", &RockAlbedoOptions::seed)
529 44 .def_rw("reflectance_ratio", &RockAlbedoOptions::reflectanceRatio)
530 .def_rw("unresolved", &RockAlbedoOptions::unresolved);
531
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_rock_albedo", &ValidateRockAlbedo, "options"_a, kValidateDoc);
532 ✗ nb::class_<RockAlbedo, AlbedoLayer>(m, "RockAlbedo",
533 "Rock of boulder layers brightening the albedo.")
534 44 .def(
535 "__init__",
536 ✗ [](RockAlbedo* self, RockAlbedoOptions options, const PyTerrain& terrain,
537 const std::vector<std::size_t>& layers) {
538
1/1
✓ Branch 1 taken 2 times.
2 const Terrain built = terrain.Get();
539 2 std::vector<RockSource> sources;
540
2/2
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 1 time.
3 for (const std::size_t n : layers) {
541
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 if (n >= built.layers.size()) {
542
1/1
✓ Branch 1 taken 1 time.
2 throw std::invalid_argument("The terrain has no layer " +
543
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
3 std::to_string(n));
544 }
545 1 auto layer = std::dynamic_pointer_cast<const BoulderLayer>(built.layers[n]);
546
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1 if (!layer) {
547
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ throw std::invalid_argument("Layer " + std::to_string(n) +
548
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ " is not a boulder layer");
549 }
550 2 sources.push_back({std::move(layer), SharedGround(built, n)});
551 1 }
552
1/3
✓ Branch 4 taken 1 time.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
1 new (self) RockAlbedo(options, std::move(sources));
553
2/4
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
4 },
554
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 "options"_a, "terrain"_a, "layers"_a,
555 "The rock of `terrain`'s boulder layers at indices `layers`, each read on the ground "
556 "beneath it.")
557 .def(
558 "share_at",
559 45 [](const RockAlbedo& rocks, double x, double y, double spacing) {
560
1/1
✓ Branch 1 taken 1 time.
1 return rocks.ShareAt(x, y, AlbedoContext{spacing});
561 },
562
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
132 "x"_a, "y"_a, "spacing"_a = 1.0,
563 "The share of the sample `spacing` metres across at (x, y) that rock covers.");
564
1/1
✓ Branch 1 taken 44 times.
44 m.def(
565 "rock_share",
566 ✗ [](double resolved, double mean, double variance, double normal) {
567
1/1
✓ Branch 1 taken 1 time.
1 return RockShare(resolved, {mean, variance}, normal);
568 },
569
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 "resolved"_a, "unresolved_mean"_a, "unresolved_variance"_a, "normal"_a,
570 "The share of a sample rock covers: `resolved` plus, of the rest, a log-normal share of "
571 "the given mean and variance at the standard normal draw `normal`, at most 1.");
572 44 }
573
574 44 void BindSlopes(nb::module_& m) {
575 auto options = nb::class_<SlopeMaturityOptions>(
576 m, "SlopeMaturityOptions",
577 "How steep ground stays immature: OMAT rises by `maturity_per_degree` beyond "
578 44 "`onset_degrees` (Ikeda et al. 2022), slopes read over `baseline` metres.");
579
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<SlopeMaturityOptions>(options);
580 44 options.def_rw("onset_degrees", &SlopeMaturityOptions::onsetDegrees)
581 44 .def_rw("maturity_per_degree", &SlopeMaturityOptions::maturityPerDegree)
582 44 .def_rw("background_maturity", &SlopeMaturityOptions::backgroundMaturity)
583 44 .def_rw("max_maturity", &SlopeMaturityOptions::maxMaturity)
584 44 .def_rw("baseline", &SlopeMaturityOptions::baseline)
585 .def_rw("origin_reflectance", &SlopeMaturityOptions::originReflectance);
586
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_slope_maturity", &ValidateSlopeMaturity, "options"_a, kValidateDoc);
587 ✗ nb::class_<SlopeAlbedo, AlbedoLayer>(m, "SlopeAlbedo",
588 "Steep ground brightened as fresher material.")
589
1/1
✓ Branch 1 taken 44 times.
88 .def(nb::init<SlopeMaturityOptions, Surface>(), "options"_a = SlopeMaturityOptions{},
590
1/1
✓ Branch 2 taken 44 times.
88 "surface"_a = Surface{})
591 44 .def(
592 "slope_at",
593 ✗ [](const SlopeAlbedo& slopes, double x, double y, const PyTerrain& terrain) {
594
1/1
✓ Branch 1 taken 1 time.
1 const Terrain built = terrain.Get();
595
1/1
✓ Branch 2 taken 1 time.
1 const TerrainGround ground(built, built.layers.size());
596
1/1
✓ Branch 1 taken 1 time.
2 return slopes.SlopeAt(x, y, ground);
597 1 },
598
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 "x"_a, "y"_a, "terrain"_a, "The slope in degrees at (x, y) on `terrain`.")
599
1/1
✓ Branch 1 taken 44 times.
88 .def("maturity_at", &SlopeAlbedo::MaturityAt, "slope_degrees"_a,
600 "The OMAT of ground of `slope_degrees`.");
601 44 }
602 } // namespace
603
604 44 void BindAlbedo(nb::module_& m) {
605 44 BindBases(m);
606 44 BindSynthetic(m);
607 44 BindRasterAlbedo(m);
608 44 BindCraterAlbedo(m);
609 44 BindSplotches(m);
610 44 BindBlastZones(m);
611 44 BindRocks(m);
612 44 BindSlopes(m);
613 44 BindMaturity(m);
614 44 BindEvaluation(m);
615 44 }
616
617 } // namespace terraingen::python
618