TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
352 / 373
94.4%
Branches:
208 / 265
78.5%
Functions:
35 / 38
92.1%

python/src/craters.cpp
Line Branch Exec Source
1 #include "bindings.h"
2 #include "common.h"
3 #include "tables.h"
4
5 #include "terraingen/layers/craters/crater.h"
6 #include "terraingen/layers/craters/crater_layer.h"
7 #include "terraingen/layers/craters/generation/population.h"
8 #include "terraingen/layers/craters/generation/spacing.h"
9 #include "terraingen/layers/craters/secondaries.h"
10 #include "terraingen/layers/craters/shaping/apply.h"
11 #include "terraingen/layers/craters/shaping/attributes.h"
12 #include "terraingen/layers/craters/shaping/datum.h"
13 #include "terraingen/layers/craters/shaping/irregularity.h"
14
15 #include <limits>
16 #include <nanobind/ndarray.h>
17
18 #include <cstdint>
19 #include <optional>
20 #include <stdexcept>
21 #include <string>
22 #include <tuple>
23 #include <vector>
24
25 namespace terraingen::python {
26
27 namespace {
28
29 using Column64 = nb::ndarray<const double, nb::ndim<1>, nb::device::cpu, nb::c_contig>;
30 using ColumnU64 = nb::ndarray<const std::uint64_t, nb::ndim<1>, nb::device::cpu, nb::c_contig>;
31
32 /// @brief Craters held as manual craters.
33 19 CraterList ManualCraters(const std::vector<Crater>& craters) {
34 19 CraterList list;
35
2/2
✓ Branch 5 taken 26 times.
✓ Branch 6 taken 19 times.
45 for (Crater crater : craters) {
36 26 crater.manual = true;
37
1/1
✓ Branch 1 taken 26 times.
26 list.Append(crater);
38 }
39 19 return list;
40 }
41
42 ✗ std::vector<Crater> CratersOf(const CraterList& list) {
43 ✗ std::vector<Crater> craters;
44
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ for (std::size_t i = 0; i < list.Size(); ++i) {
45
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ craters.push_back(list.At(i));
46 }
47 ✗ return craters;
48 }
49
50 /// @brief Column `name` of `table` as contiguous values of `T`, if the table has it.
51 template <typename Column>
52 12 std::optional<Column> TableColumn(const nb::dict& table, const char* name, const char* dtype) {
53
4/6
std::optional<nanobind::ndarray<double const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> > terraingen::python::(anonymous namespace)::TableColumn<nanobind::ndarray<double const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> >(nanobind::dict const&, char const*, char const*):
✓ Branch 1 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
std::optional<nanobind::ndarray<unsigned long const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> > terraingen::python::(anonymous namespace)::TableColumn<nanobind::ndarray<unsigned long const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> >(nanobind::dict const&, char const*, char const*):
✓ Branch 1 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
12 if (!table.contains(name)) {
54 ✗ return std::nullopt;
55 }
56 return nb::cast<Column>(nb::module_::import_("numpy").attr("ascontiguousarray")(
57
6/6
std::optional<nanobind::ndarray<double const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> > terraingen::python::(anonymous namespace)::TableColumn<nanobind::ndarray<double const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> >(nanobind::dict const&, char const*, char const*):
✓ Branch 1 taken 10 times.
✓ Branch 4 taken 10 times.
✓ Branch 7 taken 10 times.
std::optional<nanobind::ndarray<unsigned long const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> > terraingen::python::(anonymous namespace)::TableColumn<nanobind::ndarray<unsigned long const, nanobind::detail::shape<-1l>, nanobind::device::cpu, nanobind::c_contig> >(nanobind::dict const&, char const*, char const*):
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
48 table[name], nb::arg("dtype") = dtype));
58 }
59
60 /// @brief A crater list from NumPy columns 'x', 'y', 'diameter' and optionally 'age_gyr', 'id'
61 /// and 'manual'; IDs of 0 are hashed from position and size.
62 2 CraterList FromTable(const nb::dict& table) {
63
1/1
✓ Branch 1 taken 2 times.
2 const Column64 x = *TableColumn<Column64>(table, "x", "float64");
64
1/1
✓ Branch 1 taken 2 times.
2 const Column64 y = *TableColumn<Column64>(table, "y", "float64");
65
1/1
✓ Branch 1 taken 2 times.
2 const Column64 diameter = *TableColumn<Column64>(table, "diameter", "float64");
66
3/6
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 2 times.
2 if (x.shape(0) != y.shape(0) || x.shape(0) != diameter.shape(0)) {
67
0/1
✗ Branch 2 not taken.
✗ throw std::invalid_argument("Crater columns must have the same length");
68 }
69
1/1
✓ Branch 1 taken 2 times.
2 const auto age = TableColumn<Column64>(table, "age_gyr", "float64");
70
1/1
✓ Branch 1 taken 2 times.
2 const auto ids = TableColumn<ColumnU64>(table, "id", "uint64");
71
1/1
✓ Branch 1 taken 2 times.
2 const auto manual = TableColumn<Column64>(table, "manual", "float64");
72 2 CraterList list;
73
1/1
✓ Branch 2 taken 2 times.
2 list.Reserve(x.shape(0));
74
2/2
✓ Branch 1 taken 394 times.
✓ Branch 2 taken 2 times.
396 for (std::size_t i = 0; i < x.shape(0); ++i) {
75 394 Crater crater{x.data()[i], y.data()[i], diameter.data()[i]};
76
1/2
✓ Branch 1 taken 394 times.
✗ Branch 2 not taken.
394 crater.ageGyr = age ? age->data()[i] : 0.0;
77
1/2
✓ Branch 1 taken 394 times.
✗ Branch 2 not taken.
394 crater.manual = manual ? manual->data()[i] != 0.0 : true;
78
1/2
✓ Branch 3 taken 394 times.
✗ Branch 4 not taken.
788 crater.id = ids && ids->data()[i] != 0
79
1/2
✓ Branch 0 taken 394 times.
✗ Branch 1 not taken.
788 ? ids->data()[i]
80
0/1
✗ Branch 1 not taken.
✗ : ManualCraterId(crater.x, crater.y, crater.diameter);
81
1/1
✓ Branch 1 taken 394 times.
394 list.Append(crater);
82 }
83 4 return list;
84 2 }
85
86 44 void BindCrater(nb::module_& m) {
87 44 nb::class_<Crater>(m, "Crater",
88 "One crater: centre (m), rim-to-rim diameter (m), age (Gyr) and a stable "
89 "ID (0 for one hashed from its position and size).")
90 44 .def(
91 "__init__",
92 108 [](Crater* self, double x, double y, double diameter, double ageGyr, std::uint64_t id) {
93 64 new (self) Crater{x, y, diameter, ageGyr, id, true};
94 64 },
95
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.
176 "x"_a, "y"_a, "diameter"_a, "age_gyr"_a = 0.0, "id"_a = 0)
96 44 .def_rw("x", &Crater::x)
97 44 .def_rw("y", &Crater::y)
98 44 .def_rw("diameter", &Crater::diameter)
99 44 .def_rw("age_gyr", &Crater::ageGyr)
100 44 .def_rw("id", &Crater::id)
101 44 .def_rw("manual", &Crater::manual)
102 44 .def("__repr__", [](const Crater& c) {
103
0/7
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ Branch 10 not taken.
✗ Branch 13 not taken.
✗ Branch 16 not taken.
✗ Branch 19 not taken.
✗ return "Crater(" + std::to_string(c.x) + ", " + std::to_string(c.y) + ", " +
104
0/5
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ Branch 10 not taken.
✗ Branch 13 not taken.
✗ std::to_string(c.diameter) + ", age_gyr=" + std::to_string(c.ageGyr) + ")";
105 });
106 88 nb::class_<CraterAttributes>(m, "CraterAttributes", "A crater's shape as applied.")
107 44 .def_ro("floor_depth", &CraterAttributes::floorDepth)
108 44 .def_ro("rim_height", &CraterAttributes::rimHeight)
109 44 .def_ro("depth_ratio", &CraterAttributes::depthRatio)
110 44 .def_ro("freshness", &CraterAttributes::freshness)
111 .def_ro("complex", &CraterAttributes::complex);
112
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 m.def("compute_crater_attributes", &ComputeCraterAttributes, "crater"_a, "state"_a, "profile"_a,
113 "A crater's floor depth, rim height and ratios in `state`.");
114 44 }
115
116 44 void BindCraterList(nb::module_& m) {
117 44 nb::class_<CraterList>(m, "CraterList",
118 "Craters stored column by column, as generated and applied.")
119 44 .def(nb::init<>())
120 44 .def(
121 "__init__",
122 ✗ [](CraterList* self, const std::vector<Crater>& craters) {
123 3 new (self) CraterList{};
124
2/2
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 3 times.
7 for (const Crater& crater : craters) {
125
1/1
✓ Branch 1 taken 4 times.
4 self->Append(crater);
126 }
127 3 },
128
1/1
✓ Branch 1 taken 44 times.
44 "craters"_a)
129
1/1
✓ Branch 1 taken 44 times.
44 .def_static("from_table", &FromTable, "table"_a,
130 "From a dict of columns 'x', 'y', 'diameter' and optionally 'age_gyr', 'id' "
131 "and 'manual', such as `CraterFeatures.table`.")
132 44 .def("__len__", &CraterList::Size)
133 44 .def(
134 "__getitem__",
135 ✗ [](const CraterList& list, std::size_t i) {
136
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 324 times.
326 if (i >= list.Size()) {
137
1/1
✓ Branch 2 taken 2 times.
2 throw nb::index_error();
138 }
139 324 return list.At(i);
140 },
141
1/1
✓ Branch 1 taken 44 times.
44 "index"_a)
142
1/1
✓ Branch 1 taken 44 times.
44 .def("append", nb::overload_cast<const Crater&>(&CraterList::Append), "crater"_a)
143 44 .def(
144 1 "extend", [](CraterList& list, const CraterList& other) { list.Append(other); },
145
1/1
✓ Branch 1 taken 44 times.
44 "other"_a)
146 .def_prop_ro(
147
1/1
✓ Branch 3 taken 4 times.
48 "table", [](const CraterList& list) { return CraterTable(list); },
148 "The craters as NumPy columns.");
149 44 }
150
151 44 void BindSpacing(nb::module_& m) {
152 auto spacing = nb::class_<CraterSpacing>(
153 m, "CraterSpacing",
154 "Keeps craters of similar size apart: two conflict when their diameters differ by at most "
155 "`diameter_ratio` (at least 1) and their centres are closer than `separation` times the "
156 44 "larger diameter. A separation of 0 disables it.");
157
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterSpacing>(spacing);
158 44 spacing.def_rw("diameter_ratio", &CraterSpacing::diameterRatio)
159 44 .def_rw("separation", &CraterSpacing::separation)
160 .def_rw("outrank_from", &CraterSpacing::outrankFrom);
161
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_spacing", &ValidateCraterSpacing, "spacing"_a, kValidateDoc);
162
5/5
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
✓ Branch 8 taken 44 times.
✓ Branch 11 taken 44 times.
✓ Branch 14 taken 44 times.
88 m.def("craters_conflict", &CratersConflict, "a"_a, "b"_a, "spacing"_a, "surface"_a = Surface{},
163 "Whether two craters are too close under `spacing`.");
164
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("crater_takes_precedence", &CraterTakesPrecedence, "a"_a, "b"_a,
165
1/1
✓ Branch 2 taken 44 times.
88 "outrank_from"_a = std::numeric_limits<double>::infinity(),
166 "Whether `a` wins over `b` when they conflict.");
167
4/4
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
✓ Branch 8 taken 44 times.
✓ Branch 11 taken 44 times.
88 m.def("thin_by_spacing", &ThinBySpacing, "craters"_a, "spacing"_a, "surface"_a = Surface{},
168 "The craters without those that conflict with one that takes precedence (Matern II).");
169
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 m.def("spacing_reach", &SpacingReach, "diameter"_a, "max_diameter"_a, "spacing"_a,
170 "How far (m) a crater can conflict with another.");
171 44 }
172
173 44 void BindGenerationOptions(nb::module_& m) {
174 auto options = nb::class_<CraterGenerationOptions>(
175 m, "CraterGenerationOptions",
176 "Which craters are generated: diameters from `min_diameter` to `max_diameter` (m), kept "
177 44 "apart by `spacing`, at most `max_craters` (0 for no cap), plus `manual` ones.");
178
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterGenerationOptions>(options);
179 44 options.def_rw("min_diameter", &CraterGenerationOptions::dMin)
180 44 .def_rw("max_diameter", &CraterGenerationOptions::dMax)
181 44 .def_rw("spacing", &CraterGenerationOptions::spacing)
182 44 .def_rw("max_craters", &CraterGenerationOptions::maxCraters)
183 44 .def_rw("cell_size_factor", &CraterGenerationOptions::cellSizeFactor)
184 44 .def_rw("max_expected_craters", &CraterGenerationOptions::maxExpectedCraters)
185 44 .def_prop_rw(
186 ✗ "manual", [](const CraterGenerationOptions& o) { return CratersOf(o.manual); },
187 63 [](CraterGenerationOptions& o, const std::vector<Crater>& craters) {
188
1/1
✓ Branch 1 taken 19 times.
19 o.manual = ManualCraters(craters);
189 19 },
190 "Hand-placed craters, kept whatever the density.")
191 44 .def_rw("keep_share", &CraterGenerationOptions::keepShare,
192 "Callable giving the share of generated craters like a Crater kept, by a uniform "
193 "keyed to each ID; it is called from worker threads.")
194 .def_rw("completeness", &CraterGenerationOptions::completeness,
195 "Diameter (m) from which `manual` is a complete catalog at each point.");
196 44 }
197
198 44 void BindGeneration(nb::module_& m) {
199 44 BindGenerationOptions(m);
200 88 nb::class_<CraterBand>(m, "CraterBand",
201 "A diameter band [low, high) generated in cells of `cell_size`.")
202 44 .def_ro("exponent", &CraterBand::exponent)
203 44 .def_ro("low", &CraterBand::low)
204 44 .def_ro("high", &CraterBand::high)
205 .def_ro("cell_size", &CraterBand::cellSize);
206
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("crater_bands", &CraterBands, "min_diameter"_a, "max_diameter"_a,
207
1/1
✓ Branch 1 taken 44 times.
88 "cell_size_factor"_a = 16.0,
208 "The power-of-two diameter bands craters are generated in, and their cell sizes.");
209
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.
44 m.def("generate_craters", &GenerateCraters, "cells"_a, "region"_a, "density"_a, "ages"_a,
210
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 "seed"_a, "options"_a, nb::call_guard<nb::gil_scoped_release>(),
211 "The craters of `density` with centres in `region` (min_x, min_y, max_x, max_y), drawn "
212 "per cell so any region, however tiled, holds the same craters.");
213
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 m.def("min_diameter_for_count", &MinDiameterForCount, "density"_a, "area"_a, "count"_a,
214
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 "min_diameter"_a, "max_diameter"_a,
215 "The smallest diameter that keeps the expected count over `area` at `count`.");
216
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 m.def("manual_crater_id", &ManualCraterId, "x"_a, "y"_a, "diameter"_a,
217 "A stable ID for a hand-placed crater, hashed from its position and size.");
218 44 }
219
220 44 void BindApplyEnums(nb::module_& m) {
221 88 nb::enum_<CraterOrder>(m, "CraterOrder", "The order overlapping craters are applied in.")
222 44 .value("OLDEST_FIRST", CraterOrder::OldestFirst)
223 .value("LARGEST_FIRST", CraterOrder::LargestFirst);
224 88 nb::enum_<CraterDatum>(m, "CraterDatum", "The surface a crater's interior is cut relative to.")
225 44 .value("NONE", CraterDatum::None)
226 44 .value("PLANE", CraterDatum::Plane)
227 44 .value("MEAN_HEIGHT", CraterDatum::MeanHeight)
228 .value("CENTRE_HEIGHT", CraterDatum::CentreHeight);
229 auto blendClass = nb::class_<CraterBlend>(
230 m, "CraterBlend",
231 44 "Where a crater's replaced interior blends into the draped exterior, in radii.");
232
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterBlend>(blendClass);
233 44 blendClass.def_rw("start_radii", &CraterBlend::startRadii)
234 44 .def_rw("end_radii", &CraterBlend::endRadii)
235 44 .def_rw("shape", &CraterBlend::shape)
236
1/1
✓ Branch 1 taken 44 times.
44 .def("weight", &CraterBlend::Weight, "radii"_a,
237 "The interior weight `radii` crater radii from the centre.");
238
1/1
✓ Branch 1 taken 44 times.
44 m.def(
239 "crater_blend_weight",
240
1/1
✓ Branch 1 taken 44 times.
46 [](double radii, const CraterBlend& blend) { return blend.Weight(radii); }, "radii"_a,
241
1/1
✓ Branch 1 taken 44 times.
44 "blend"_a, "The interior weight `radii` crater radii from the centre.");
242 44 }
243
244 44 void BindSelectionClass(nb::module_& m) {
245 88 nb::enum_<RimRoughnessModel>(m, "RimRoughnessModel", "How irregular crater rims are drawn.")
246 44 .value("POWER_LAW", RimRoughnessModel::PowerLaw)
247 .value("DU_2024", RimRoughnessModel::Du2024);
248 auto selection = nb::class_<CraterSelection>(
249 m, "CraterSelection",
250 "Which craters a model applies to: diameters (m) and ages (Gyr) within ranges, and manual "
251 44 "craters only with `manual`.");
252
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterSelection>(selection);
253 44 selection.def_rw("min_diameter", &CraterSelection::minDiameter)
254 44 .def_rw("max_diameter", &CraterSelection::maxDiameter)
255 44 .def_rw("min_age_gyr", &CraterSelection::minAgeGyr)
256 44 .def_rw("max_age_gyr", &CraterSelection::maxAgeGyr)
257 44 .def_rw("manual", &CraterSelection::manual)
258
1/1
✓ Branch 1 taken 44 times.
44 .def("includes", &CraterSelection::Includes, "crater"_a, "Whether `crater` is selected.");
259
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_selection", &ValidateCraterSelection, "selection"_a,
260
1/1
✓ Branch 1 taken 44 times.
88 "what"_a = "Crater selection", "Raises ValueError naming the first invalid setting.");
261 44 }
262
263 44 void BindIrregularity(nb::module_& m) {
264 auto irregularity = nb::class_<CraterIrregularity>(
265 m, "CraterIrregularity",
266 "Irregular, elongated crater outlines for the craters it selects: rims roughened by "
267 44 "random harmonics and some craters stretched into ellipses.");
268
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterIrregularity>(irregularity);
269 44 irregularity.def_rw("enabled", &CraterIrregularity::enabled)
270 44 .def_rw("seed", &CraterIrregularity::seed)
271 .def_rw("selection", &CraterIrregularity::selection);
272
1/1
✓ Branch 1 taken 44 times.
44 BindSelection(irregularity, &CraterIrregularity::selection, "min_diameter", "max_diameter");
273 44 irregularity.def_rw("rough_share", &CraterIrregularity::roughShare)
274 44 .def_rw("model", &CraterIrregularity::model)
275 44 .def_rw("scatter", &CraterIrregularity::scatter)
276 44 .def_rw("max_deviation", &CraterIrregularity::maxDeviation)
277 44 .def_rw("roughness", &CraterIrregularity::roughness)
278 44 .def_rw("spectral_slope", &CraterIrregularity::spectralSlope)
279 44 .def_rw("min_harmonic", &CraterIrregularity::minHarmonic)
280 44 .def_rw("max_harmonic", &CraterIrregularity::maxHarmonic)
281 44 .def_rw("elongated_share", &CraterIrregularity::elongatedShare)
282 44 .def_rw("ellipticity", &CraterIrregularity::ellipticity)
283 44 .def_rw("heading", &CraterIrregularity::heading)
284 44 .def_rw("heading_spread", &CraterIrregularity::headingSpread)
285 44 .def_rw("marks_share", &CraterIrregularity::marksShare)
286 44 .def_rw("marks_amplitude", &CraterIrregularity::marksAmplitude)
287 44 .def_rw("marks_knots", &CraterIrregularity::marksKnots)
288 44 .def_rw("marks_inner_radii", &CraterIrregularity::marksInnerRadii)
289 44 .def_rw("marks_outer_radii", &CraterIrregularity::marksOuterRadii)
290 .def_rw("marks_fade_radii", &CraterIrregularity::marksFadeRadii);
291
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_irregularity", &ValidateCraterIrregularity, "irregularity"_a,
292 kValidateDoc);
293 44 }
294
295 44 void BindOutlines(nb::module_& m) {
296 44 nb::class_<CraterOutline>(m, "CraterOutline", "A crater's irregular, elongated outline.")
297 44 .def_ro("ellipticity", &CraterOutline::ellipticity)
298 44 .def_ro("heading", &CraterOutline::heading)
299
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
88 .def("scale", &CraterOutline::Scale, "dx"_a, "dy"_a,
300 "The outline's radius, relative to the round one, in direction (dx, dy).")
301 44 .def_ro("marks", &CraterOutline::marks)
302
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 .def("mark_shift", &CraterOutline::MarkShift, "dx"_a, "dy"_a, "radii"_a,
303 "The shift in radii the wall marks add at `radii` from the centre toward (dx, dy).")
304 44 .def_prop_ro("max_scale", &CraterOutline::MaxScale)
305 88 .def_prop_ro("round", &CraterOutline::Round);
306
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("outline_of", &OutlineOf, "crater"_a, "irregularity"_a,
307 "The outline `irregularity` gives `crater`.");
308
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("max_outline_scale", &MaxOutlineScale, "irregularity"_a,
309 "The largest outline scale `irregularity` can give, for reach.");
310 44 }
311
312 44 void BindApplyOptions(nb::module_& m) {
313 auto smoothing = nb::class_<RimSmoothing>(
314 m, "RimSmoothing",
315 "Rounded crater rims: near the rim the profile is blended toward itself smoothed across "
316 "distance by a Gaussian of `sigma_radii` radii, over the band from `inner_radii` to "
317 44 "`outer_radii`, fading along `shape`, at `strength`; 0 `sigma_radii` leaves rims sharp.");
318
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<RimSmoothing>(smoothing);
319 44 smoothing.def_rw("sigma_radii", &RimSmoothing::sigmaRadii)
320 44 .def_rw("inner_radii", &RimSmoothing::innerRadii)
321 44 .def_rw("outer_radii", &RimSmoothing::outerRadii)
322 44 .def_rw("shape", &RimSmoothing::shape)
323 44 .def_rw("strength", &RimSmoothing::strength)
324
1/1
✓ Branch 1 taken 44 times.
44 .def("weight", &RimSmoothing::Weight, "radii"_a,
325 "The smoothing weight at `radii` crater radii from the centre.");
326
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_rim_smoothing", &ValidateRimSmoothing, "smoothing"_a, kValidateDoc);
327 auto noise = nb::class_<CraterNoise>(
328 m, "CraterNoise",
329 "Fractal noise added to each crater, keyed to its ID: `amplitude` over the whole "
330 "footprint (fading out beyond the rim) at `wavelength_radii`, and `rim_amplitude` in the "
331 "band from `rim_inner_radii` to `rim_outer_radii` at `rim_wavelength_radii`; amplitudes "
332 44 "are shares of the diameter and 0 adds none.");
333
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterNoise>(noise);
334 44 noise.def_rw("amplitude", &CraterNoise::amplitude)
335 44 .def_rw("wavelength_radii", &CraterNoise::wavelengthRadii)
336 44 .def_rw("octaves", &CraterNoise::octaves)
337 44 .def_rw("gain", &CraterNoise::gain)
338 44 .def_rw("rim_amplitude", &CraterNoise::rimAmplitude)
339 44 .def_rw("rim_wavelength_radii", &CraterNoise::rimWavelengthRadii)
340 44 .def_rw("rim_inner_radii", &CraterNoise::rimInnerRadii)
341 44 .def_rw("rim_outer_radii", &CraterNoise::rimOuterRadii)
342 .def_rw("seed", &CraterNoise::seed);
343
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_noise", &ValidateCraterNoise, "noise"_a, kValidateDoc);
344 auto texture = nb::class_<CraterTexture>(
345 m, "CraterTexture",
346 "Texture keyed to each crater's ID: `wall_streaks` (gullies down the inner wall, as a "
347 "share of the diameter, `wall_streak_width_radii` wide), `ejecta` (hummocky lineated "
348 "ejecta as a share of McGetchin's thickness, lineations `ejecta_lineation_radii` wide) "
349 "and `mantling` (older relief buried under the ejecta, with a datum), out to "
350 44 "`ejecta_reach_radii`; gullies and ejecta fade out by `min_freshness`.");
351
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterTexture>(texture);
352 44 texture.def_rw("wall_streaks", &CraterTexture::wallStreaks)
353 44 .def_rw("wall_streak_width_radii", &CraterTexture::wallStreakWidthRadii)
354 44 .def_rw("ejecta", &CraterTexture::ejecta)
355 44 .def_rw("ejecta_lineation_radii", &CraterTexture::ejectaLineationRadii)
356 44 .def_rw("mantling", &CraterTexture::mantling)
357 44 .def_rw("ejecta_reach_radii", &CraterTexture::ejectaReachRadii)
358 44 .def_rw("min_freshness", &CraterTexture::minFreshness)
359 .def_rw("seed", &CraterTexture::seed);
360
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_texture", &ValidateCraterTexture, "texture"_a, kValidateDoc);
361
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("ejecta_thickness_share", &EjectaThicknessShare, "radii"_a, "reach_radii"_a,
362 "McGetchin's ejecta thickness at `radii` radii over that at the rim, tapered to "
363 "nothing at `reach_radii`.");
364 auto slope = nb::class_<CraterSlopeResponse>(
365 m, "CraterSlopeResponse",
366 "Craters made asymmetric along the slope of the ground beneath their layer: the deepest "
367 "point `floor_offset` diameters downslope (drawn per crater), `upslope_rim` of the raised "
368 44 "rim and ejecta kept upslope, complete from `full_slope` radians.");
369
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterSlopeResponse>(slope);
370 44 slope.def_rw("enabled", &CraterSlopeResponse::enabled)
371 44 .def_rw("seed", &CraterSlopeResponse::seed)
372 44 .def_rw("floor_offset", &CraterSlopeResponse::floorOffset)
373 44 .def_rw("upslope_rim", &CraterSlopeResponse::upslopeRim)
374 44 .def_rw("full_slope", &CraterSlopeResponse::fullSlope)
375 44 .def_rw("critical_gradient", &CraterSlopeResponse::criticalGradient)
376 .def_rw("max_wear_rate", &CraterSlopeResponse::maxWearRate);
377
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_slope_response", &ValidateCraterSlopeResponse, "response"_a,
378 kValidateDoc);
379
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 m.def("slope_wear_rate", &SlopeWearRate, "response"_a, "east"_a, "north"_a,
380 "How many times as fast a crater wears on ground rising `east` and `north` m/m.");
381 auto apply = nb::class_<CraterApplyOptions>(
382 m, "CraterApplyOptions",
383 "How craters are applied: the `datum` interiors are cut relative to (fit over "
384 "`datum_fit_radii` radii; with none, craters are added to the terrain and the settings "
385 "after it are unused), how much of the interior replaces the older relief "
386 "(`interior_replacement`, 0 to 1), the `blend` into the exterior, the `order` overlapping "
387 "craters are applied in, the resolution `fade`, rim `irregularity`, `rim_smoothing`, "
388 44 "`noise`, `texture` and `slope`.");
389
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterApplyOptions>(apply);
390 44 apply.def_rw("order", &CraterApplyOptions::order)
391 44 .def_rw("fade", &CraterApplyOptions::fade)
392 44 .def_rw("datum", &CraterApplyOptions::datum)
393 44 .def_rw("datum_fit_radii", &CraterApplyOptions::datumFitRadii)
394 44 .def_rw("interior_replacement", &CraterApplyOptions::interiorReplacement)
395 44 .def_rw("blend", &CraterApplyOptions::blend)
396 44 .def_rw("irregularity", &CraterApplyOptions::irregularity)
397 44 .def_rw("rim_smoothing", &CraterApplyOptions::rimSmoothing)
398 44 .def_rw("noise", &CraterApplyOptions::noise)
399 44 .def_rw("texture", &CraterApplyOptions::texture)
400 .def_rw("slope", &CraterApplyOptions::slope);
401
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_crater_apply_options", &ValidateCraterApplyOptions, "options"_a, kValidateDoc);
402
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("crater_apply_order", &CraterApplyOrder, "craters"_a, "order"_a,
403 "The indices of `craters` in the order they are applied.");
404 44 }
405
406 /// @brief A shared pointer to `model` that does not own it, for a call that cannot outlive it.
407 18 template <typename T> std::shared_ptr<const T> Borrowed(const T& model) {
408 18 return std::shared_ptr<const T>(std::shared_ptr<const T>{}, &model);
409 }
410
411 44 void BindApply(nb::module_& m) {
412
1/1
✓ Branch 1 taken 44 times.
44 m.def(
413 "apply_craters",
414 ✗ [](Raster<float>& raster, const CraterList& craters, const CraterProfile& profile,
415 const DegradationModel& degradation, const CraterApplyOptions& options, double spacing,
416 unsigned threads) {
417 9 const CraterShapeModels models{Borrowed(profile), Borrowed(degradation)};
418 9 ApplyCraters(raster, craters, models, options, {spacing, threads});
419
1/5
✓ Branch 3 taken 9 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
18 },
420
5/5
✓ Branch 15 taken 44 times.
✓ Branch 18 taken 44 times.
✓ Branch 21 taken 44 times.
✓ Branch 24 taken 44 times.
✓ Branch 27 taken 44 times.
132 "raster"_a, "craters"_a, "profile"_a, "degradation"_a, "options"_a = CraterApplyOptions{},
421
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "spacing"_a = 0.0, "threads"_a = 1, nb::call_guard<nb::gil_scoped_release>(),
422 "Applies `craters` to `raster` in place, shaped by `profile` and `degradation`. With a "
423 "datum, the ground is the raster as given.");
424
1/1
✓ Branch 1 taken 44 times.
44 m.def(
425 "fit_crater_datum",
426 ✗ [](const Crater& crater, const CraterApplyOptions& options,
427 HeightSampler ground) -> std::optional<std::tuple<double, double, double>> {
428
1/1
✓ Branch 1 taken 2 times.
2 const std::optional<Plane> plane = FitCraterDatum(crater, options, ground);
429
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 if (!plane) {
430 1 return std::nullopt;
431 }
432 1 return std::tuple{plane->a, plane->b, plane->c};
433 },
434
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 "crater"_a, "options"_a, "ground"_a,
435 "The datum `options` asks for under `crater`, fitted to `ground(x, y)`: (a, b, c) of the "
436 "plane a + b dx + c dy, or None without a datum or ground.");
437 44 }
438
439 44 void BindLayerOptions(nb::module_& m) {
440 auto options = nb::class_<CraterLayerOptions>(
441 m, "CraterLayerOptions",
442 "A crater layer's settings: whether to `generate` craters from the density, its `seed`, "
443 44 "the `generation` options and the `apply` options.");
444
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<CraterLayerOptions>(options);
445 44 options.def_rw("generate", &CraterLayerOptions::generate)
446 44 .def_rw("surface", &CraterLayerOptions::surface,
447 "What the craters lie on; on a sphere they are placed by longitude and latitude.")
448 44 .def_rw("seed", &CraterLayerOptions::seed)
449 44 .def_rw("generation", &CraterLayerOptions::generation)
450 44 .def_rw("apply", &CraterLayerOptions::apply)
451 44 .def_rw("density_map", &CraterLayerOptions::densityMap)
452 .def_rw("secondaries", &CraterLayerOptions::secondaries,
453 "The secondaries layers throw from these craters, taken out of a total map.");
454 44 }
455
456 9 nb::dict CraterLayerTable(const CraterLayer& layer, const Bounds2d& region, double spacing) {
457
1/1
✓ Branch 2 taken 9 times.
9 const CraterList craters = layer.Craters(region, spacing);
458 9 std::vector<CraterAttributes> attributes;
459
2/2
✓ Branch 1 taken 2119 times.
✓ Branch 2 taken 9 times.
2128 for (std::size_t i = 0; i < craters.Size(); ++i) {
460
3/3
✓ Branch 1 taken 2119 times.
✓ Branch 4 taken 2119 times.
✓ Branch 7 taken 2119 times.
2119 attributes.push_back(layer.Attributes(craters.At(i)));
461 }
462
1/1
✓ Branch 1 taken 9 times.
18 return CraterTable(craters, attributes);
463 9 }
464
465 44 void BindCraterLayer(nb::module_& m) {
466 44 BindLayerOptions(m);
467 ✗ nb::class_<CraterLayer, Layer>(
468 m, "CraterLayer",
469 "Craters drawn from `models.density` and shaped by its profile and degradation, plus any "
470 "manual craters, applied to the terrain.")
471
1/1
✓ Branch 1 taken 44 times.
44 .def(nb::init<CraterModels, CraterLayerOptions>(), "models"_a,
472
2/24
✓ Branch 8 taken 44 times.
✓ Branch 28 taken 44 times.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
✗ Branch 38 not taken.
✗ Branch 39 not taken.
✗ Branch 41 not taken.
✗ Branch 42 not taken.
✗ Branch 44 not taken.
✗ Branch 45 not taken.
✗ Branch 47 not taken.
✗ Branch 48 not taken.
✗ Branch 50 not taken.
✗ Branch 51 not taken.
✗ Branch 53 not taken.
✗ Branch 54 not taken.
✗ Branch 56 not taken.
✗ Branch 57 not taken.
✗ Branch 59 not taken.
✗ Branch 60 not taken.
✗ Branch 62 not taken.
✗ Branch 63 not taken.
✗ Branch 65 not taken.
✗ Branch 66 not taken.
176 "options"_a = CraterLayerOptions{})
473
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 .def("craters", &CraterLayerTable, "region"_a, "spacing"_a = 0.0,
474 "The craters over `region` (min_x, min_y, max_x, max_y) as NumPy columns, without "
475 "evaluating any heights.")
476
1/1
✓ Branch 1 taken 44 times.
44 .def("attributes", &CraterLayer::Attributes, "crater"_a)
477 44 .def_prop_ro("models", &CraterLayer::Models)
478 88 .def_prop_ro("options", &CraterLayer::Options);
479 44 }
480
481 44 void BindSecondaryOptions(nb::module_& m) {
482 auto layout = nb::class_<RayLayout>(m, "RayLayout",
483 "How many rays a crater has (`count`) and how wide "
484 "(`width_degrees`), drawn from its ID and "
485 44 "`seed`; layers with the same layout share the same rays.");
486
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<RayLayout>(layout);
487 44 layout.def_rw("seed", &RayLayout::seed)
488 44 .def_rw("count", &RayLayout::count)
489 .def_rw("width_degrees", &RayLayout::widthDegrees);
490
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_ray_layout", &ValidateRayLayout, "layout"_a, kValidateDoc);
491 88 nb::class_<RayDirection>(m, "RayDirection", "One ray: its azimuth and width.")
492 44 .def_ro("azimuth", &RayDirection::azimuth, "Radians counter-clockwise from east.")
493 44 .def_ro("sin_half_width", &RayDirection::sinHalfWidth)
494 .def_ro("key", &RayDirection::key);
495
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("ray_directions", &RayDirections, "crater"_a, "layout"_a,
496 "The rays of `crater` under `layout`.");
497 auto rays = nb::class_<SecondaryRays>(
498 m, "SecondaryRays",
499 "Secondaries gathered into their primary's rays (`share` of them) and clumped along each "
500 44 "ray (`clustering`, every `cluster_spacing` in log range); off unless `enabled`.");
501
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<SecondaryRays>(rays);
502 44 rays.def_rw("enabled", &SecondaryRays::enabled)
503 44 .def_rw("layout", &SecondaryRays::layout)
504 44 .def_rw("share", &SecondaryRays::share)
505 44 .def_rw("min_half_angle_degrees", &SecondaryRays::minHalfAngleDegrees)
506 44 .def_rw("clustering", &SecondaryRays::clustering)
507 .def_rw("cluster_spacing", &SecondaryRays::clusterSpacing);
508 auto shapes = nb::class_<SecondaryShapes>(
509 m, "SecondaryShapes",
510 "Secondaries stretched toward their primary: `elongated_share` of them, by `ellipticity` "
511 44 "near it, rounder with range.");
512
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<SecondaryShapes>(shapes);
513 44 shapes.def_rw("elongated_share", &SecondaryShapes::elongatedShare)
514 .def_rw("ellipticity", &SecondaryShapes::ellipticity);
515 auto options = nb::class_<SecondaryCraterOptions>(
516 m, "SecondaryCraterOptions",
517 "Secondary craters around a crater layer's primaries; `onset_count` (the number at "
518 "least `count_fraction` of the primary's diameter across, per primary-sized area at the "
519 44 "onset) must be given.");
520
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<SecondaryCraterOptions>(options);
521 44 options.def_rw("seed", &SecondaryCraterOptions::seed)
522 44 .def_rw("onset_count", &SecondaryCraterOptions::onsetCount)
523 44 .def_rw("count_fraction", &SecondaryCraterOptions::countFraction)
524 44 .def_rw("size_slope", &SecondaryCraterOptions::sizeSlope)
525 44 .def_rw("min_diameter", &SecondaryCraterOptions::minDiameter)
526 44 .def_rw("max_diameter", &SecondaryCraterOptions::maxDiameter)
527 44 .def_rw("outer_range", &SecondaryCraterOptions::outerRange)
528 44 .def_rw("rays", &SecondaryCraterOptions::rays)
529 44 .def_rw("shapes", &SecondaryCraterOptions::shapes)
530 .def_rw("selection", &SecondaryCraterOptions::selection);
531
1/1
✓ Branch 1 taken 44 times.
44 BindSelection(options, &SecondaryCraterOptions::selection, "min_primary_diameter",
532 "max_primary_diameter");
533
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_secondary_crater_options", &ValidateSecondaryCraterOptions, "options"_a,
534 kValidateDoc);
535 44 }
536
537 44 void BindSecondaryLaws(nb::module_& m) {
538
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("singer_max_secondary_diameter", &SingerMaxSecondaryDiameter, "diameter"_a, "range"_a,
539 "The largest secondary (m) at `range` metres from a primary `diameter` metres across "
540 "(Singer et al. 2020).");
541
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("secondary_onset_range", &SecondaryOnsetRange, "diameter"_a,
542 "How far from its centre (m) a primary throws its first secondaries.");
543
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("singer_range_for_size", &SingerRangeForSize, "diameter"_a, "size"_a,
544 "The range (m) beyond which a primary `diameter` metres across throws no secondary "
545 "`size` metres across (Singer et al. 2020); infinite up to 1 km.");
546
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("elliott_median_ray_length", &ElliottMedianRayLength, "diameter"_a,
547
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "coefficient"_a = 6.59, "exponent"_a = 1.27,
548 "The median ray length (m) of a crater `diameter` metres across (Elliott et al. 2018).");
549 44 }
550
551 44 void BindSecondaries(nb::module_& m) {
552 44 BindSecondaryOptions(m);
553 ✗ nb::class_<SecondaryCraterLayer, Layer>(
554 m, "SecondaryCraterLayer",
555 "Secondary craters around the craters of `primaries` (Singer et al. 2020 sizes, "
556 "Chapman 1968 fall-off), shaped by `profile` and `degradation`.")
557 44 .def(
558 "__init__",
559 ✗ [](SecondaryCraterLayer* self, std::shared_ptr<CraterLayer> primaries,
560 std::shared_ptr<CraterProfile> profile,
561 std::shared_ptr<DegradationModel> degradation, SecondaryCraterOptions options) {
562
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ new (self) SecondaryCraterLayer(std::move(primaries),
563 2 {std::move(profile), std::move(degradation)},
564
1/1
✓ Branch 4 taken 2 times.
6 std::move(options));
565
0/2
✗ Branch 18 not taken.
✗ Branch 19 not taken.
6 },
566
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 "primaries"_a, "profile"_a, "degradation"_a, "options"_a)
567 44 .def(
568 "secondaries",
569 ✗ [](const SecondaryCraterLayer& layer, const Bounds2d& region, double spacing) {
570
2/2
✓ Branch 2 taken 2 times.
✓ Branch 5 taken 2 times.
2 return CraterTable(layer.Secondaries(region, spacing));
571 },
572
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "region"_a, "spacing"_a = 0.0)
573
1/1
✓ Branch 1 taken 44 times.
88 .def("outer_range", &SecondaryCraterLayer::OuterRange, "diameter"_a);
574 44 BindSecondaryLaws(m);
575 44 }
576
577 } // namespace
578
579 44 void BindCraters(nb::module_& m) {
580 44 BindCrater(m);
581 44 BindCraterList(m);
582 44 BindSpacing(m);
583 44 BindGeneration(m);
584 44 BindApplyEnums(m);
585 44 BindSelectionClass(m);
586 44 BindIrregularity(m);
587 44 BindOutlines(m);
588 44 BindApplyOptions(m);
589 44 BindApply(m);
590 44 BindCraterLayer(m);
591 44 BindSecondaries(m);
592 44 }
593
594 } // namespace terraingen::python
595