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 | 45 | for (Crater crater : craters) { | |
| 36 | 26 | crater.manual = true; | |
| 37 | 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 | ✗ | for (std::size_t i = 0; i < list.Size(); ++i) { | |
| 45 | ✗ | 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 | 12 | if (!table.contains(name)) { | |
| 54 | ✗ | return std::nullopt; | |
| 55 | } | ||
| 56 | return nb::cast<Column>(nb::module_::import_("numpy").attr("ascontiguousarray")( | ||
| 57 | 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 | 2 | const Column64 x = *TableColumn<Column64>(table, "x", "float64"); | |
| 64 | 2 | const Column64 y = *TableColumn<Column64>(table, "y", "float64"); | |
| 65 | 2 | const Column64 diameter = *TableColumn<Column64>(table, "diameter", "float64"); | |
| 66 | 2 | if (x.shape(0) != y.shape(0) || x.shape(0) != diameter.shape(0)) { | |
| 67 | ✗ | throw std::invalid_argument("Crater columns must have the same length"); | |
| 68 | } | ||
| 69 | 2 | const auto age = TableColumn<Column64>(table, "age_gyr", "float64"); | |
| 70 | 2 | const auto ids = TableColumn<ColumnU64>(table, "id", "uint64"); | |
| 71 | 2 | const auto manual = TableColumn<Column64>(table, "manual", "float64"); | |
| 72 | 2 | CraterList list; | |
| 73 | 2 | list.Reserve(x.shape(0)); | |
| 74 | 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 | 394 | crater.ageGyr = age ? age->data()[i] : 0.0; | |
| 77 | 394 | crater.manual = manual ? manual->data()[i] != 0.0 : true; | |
| 78 | 788 | crater.id = ids && ids->data()[i] != 0 | |
| 79 | 788 | ? ids->data()[i] | |
| 80 | ✗ | : ManualCraterId(crater.x, crater.y, crater.diameter); | |
| 81 | 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 | 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 | ✗ | return "Crater(" + std::to_string(c.x) + ", " + std::to_string(c.y) + ", " + | |
| 104 | ✗ | 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 | 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 | 7 | for (const Crater& crater : craters) { | |
| 125 | 4 | self->Append(crater); | |
| 126 | } | ||
| 127 | 3 | }, | |
| 128 | 44 | "craters"_a) | |
| 129 | 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 | 326 | if (i >= list.Size()) { | |
| 137 | 2 | throw nb::index_error(); | |
| 138 | } | ||
| 139 | 324 | return list.At(i); | |
| 140 | }, | ||
| 141 | 44 | "index"_a) | |
| 142 | 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 | 44 | "other"_a) | |
| 146 | .def_prop_ro( | ||
| 147 | 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 | 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 | 44 | m.def("validate_crater_spacing", &ValidateCraterSpacing, "spacing"_a, kValidateDoc); | |
| 162 | 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 | 44 | m.def("crater_takes_precedence", &CraterTakesPrecedence, "a"_a, "b"_a, | |
| 165 | 88 | "outrank_from"_a = std::numeric_limits<double>::infinity(), | |
| 166 | "Whether `a` wins over `b` when they conflict."); | ||
| 167 | 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 | 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 | 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 | 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 | 44 | m.def("crater_bands", &CraterBands, "min_diameter"_a, "max_diameter"_a, | |
| 207 | 88 | "cell_size_factor"_a = 16.0, | |
| 208 | "The power-of-two diameter bands craters are generated in, and their cell sizes."); | ||
| 209 | 44 | m.def("generate_craters", &GenerateCraters, "cells"_a, "region"_a, "density"_a, "ages"_a, | |
| 210 | 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 | 44 | m.def("min_diameter_for_count", &MinDiameterForCount, "density"_a, "area"_a, "count"_a, | |
| 214 | 44 | "min_diameter"_a, "max_diameter"_a, | |
| 215 | "The smallest diameter that keeps the expected count over `area` at `count`."); | ||
| 216 | 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 | 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 | 44 | .def("weight", &CraterBlend::Weight, "radii"_a, | |
| 237 | "The interior weight `radii` crater radii from the centre."); | ||
| 238 | 44 | m.def( | |
| 239 | "crater_blend_weight", | ||
| 240 | 46 | [](double radii, const CraterBlend& blend) { return blend.Weight(radii); }, "radii"_a, | |
| 241 | 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 | 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 | 44 | .def("includes", &CraterSelection::Includes, "crater"_a, "Whether `crater` is selected."); | |
| 259 | 44 | m.def("validate_crater_selection", &ValidateCraterSelection, "selection"_a, | |
| 260 | 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 | 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 | 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 | 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 | 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 | 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 | 44 | m.def("outline_of", &OutlineOf, "crater"_a, "irregularity"_a, | |
| 307 | "The outline `irregularity` gives `crater`."); | ||
| 308 | 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 | 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 | 44 | .def("weight", &RimSmoothing::Weight, "radii"_a, | |
| 325 | "The smoothing weight at `radii` crater radii from the centre."); | ||
| 326 | 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 | 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 | 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 | 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 | 44 | m.def("validate_crater_texture", &ValidateCraterTexture, "texture"_a, kValidateDoc); | |
| 361 | 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 | 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 | 44 | m.def("validate_crater_slope_response", &ValidateCraterSlopeResponse, "response"_a, | |
| 378 | kValidateDoc); | ||
| 379 | 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 | 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 | 44 | m.def("validate_crater_apply_options", &ValidateCraterApplyOptions, "options"_a, kValidateDoc); | |
| 402 | 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 | 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 | 18 | }, | |
| 420 | 132 | "raster"_a, "craters"_a, "profile"_a, "degradation"_a, "options"_a = CraterApplyOptions{}, | |
| 421 | 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 | 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 | 2 | const std::optional<Plane> plane = FitCraterDatum(crater, options, ground); | |
| 429 | 2 | if (!plane) { | |
| 430 | 1 | return std::nullopt; | |
| 431 | } | ||
| 432 | 1 | return std::tuple{plane->a, plane->b, plane->c}; | |
| 433 | }, | ||
| 434 | 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 | 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 | 9 | const CraterList craters = layer.Craters(region, spacing); | |
| 458 | 9 | std::vector<CraterAttributes> attributes; | |
| 459 | 2128 | for (std::size_t i = 0; i < craters.Size(); ++i) { | |
| 460 | 2119 | attributes.push_back(layer.Attributes(craters.At(i))); | |
| 461 | } | ||
| 462 | 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 | 44 | .def(nb::init<CraterModels, CraterLayerOptions>(), "models"_a, | |
| 472 | 176 | "options"_a = CraterLayerOptions{}) | |
| 473 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 44 | BindSelection(options, &SecondaryCraterOptions::selection, "min_primary_diameter", | |
| 532 | "max_primary_diameter"); | ||
| 533 | 44 | m.def("validate_secondary_crater_options", &ValidateSecondaryCraterOptions, "options"_a, | |
| 534 | kValidateDoc); | ||
| 535 | 44 | } | |
| 536 | |||
| 537 | 44 | void BindSecondaryLaws(nb::module_& m) { | |
| 538 | 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 | 44 | m.def("secondary_onset_range", &SecondaryOnsetRange, "diameter"_a, | |
| 542 | "How far from its centre (m) a primary throws its first secondaries."); | ||
| 543 | 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 | 44 | m.def("elliott_median_ray_length", &ElliottMedianRayLength, "diameter"_a, | |
| 547 | 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 | ✗ | new (self) SecondaryCraterLayer(std::move(primaries), | |
| 563 | 2 | {std::move(profile), std::move(degradation)}, | |
| 564 | 6 | std::move(options)); | |
| 565 | 6 | }, | |
| 566 | 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 | return CraterTable(layer.Secondaries(region, spacing)); | |
| 571 | }, | ||
| 572 | 132 | "region"_a, "spacing"_a = 0.0) | |
| 573 | 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 |