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 | 132290 | if (nb::isinstance<nb::float_>(value) || nb::isinstance<nb::int_>(value)) { | |
| 32 | 64 | sample.fill(nb::cast<float>(value)); | |
| 33 | 64 | return sample; | |
| 34 | } | ||
| 35 | 66113 | std::size_t band = 0; | |
| 36 | 132226 | for (nb::handle item : value) { | |
| 37 | 66113 | if (band == sample.size()) { | |
| 38 | ✗ | throw std::invalid_argument("An albedo value has at most three bands"); | |
| 39 | } | ||
| 40 | 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 | 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 | 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 | 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 | 132 | "spacing"_a = 1.0, "threads"_a = 1) | |
| 86 | 44 | .def_ro("spacing", &AlbedoContext::spacing) | |
| 87 | .def_ro("threads", &AlbedoContext::threads); | ||
| 88 | 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 | 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 | 3 | for (std::size_t band = 0; band < value.size() && band < sample.size(); ++band) { | |
| 107 | 1 | sample[band] = value[band]; | |
| 108 | } | ||
| 109 | 1 | layer.ApplyAt(sample, bands, x, y, context); | |
| 110 | 2 | return Bands(sample, bands); | |
| 111 | }, | ||
| 112 | 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 | 18 | new (self) SyntheticAlbedo(std::move(params), origin, surface); | |
| 135 | 18 | }, | |
| 136 | 132 | "value"_a = kDefaultSyntheticAlbedoValue, "value_noise"_a = std::vector<NoiseLayer>{}, | |
| 137 | 176 | "rgb"_a = false, "hue"_a = 0.0, "saturation"_a = 0.0, | |
| 138 | 88 | "hue_noise"_a = std::vector<NoiseLayer>{}, | |
| 139 | 132 | "saturation_noise"_a = std::vector<NoiseLayer>{}, "origin"_a = Vec2d{}, | |
| 140 | 88 | "surface"_a = Surface{}); | |
| 141 | 44 | m.def( | |
| 142 | "hsv_to_rgb", | ||
| 143 | ✗ | [](double hue, double saturation, double value) { | |
| 144 | 1 | const AlbedoSample rgb = HsvToRgb(hue, saturation, value); | |
| 145 | 2 | return std::tuple{rgb[0], rgb[1], rgb[2]}; | |
| 146 | }, | ||
| 147 | 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 | 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 | 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 | 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 | 44 | .def(nb::init<std::vector<Raster<float>>, RasterAlbedoOptions, Surface>(), "bands"_a, | |
| 173 | 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 | 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 | 44 | .def("myr", &RayRetention::Myr, "diameter"_a); | |
| 194 | 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 | 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 | 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 | 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 | 44 | .def("median_length", &CraterRays::MedianLength, "diameter"_a); | |
| 230 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 44 | BindKwargsInit<CraterAlbedoOptions>(options); | |
| 301 | 44 | options.def_rw("seed", &CraterAlbedoOptions::seed) | |
| 302 | .def_rw("selection", &CraterAlbedoOptions::selection); | ||
| 303 | 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 | 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 | 44 | .def(nb::init<std::shared_ptr<CraterLayer>, CraterAlbedoOptions>(), "craters"_a, | |
| 329 | 88 | "options"_a = CraterAlbedoOptions{}) | |
| 330 | 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 | 4 | if (terrain == nullptr) { | |
| 337 | 3 | return layer.ReflectanceChange(crater, x, y, spacing); | |
| 338 | } | ||
| 339 | 1 | const Terrain built = terrain->Get(); | |
| 340 | 1 | const TerrainGround ground(built, built.layers.size()); | |
| 341 | 1 | return layer.ReflectanceChange(crater, x, y, spacing, &ground); | |
| 342 | 1 | }, | |
| 343 | 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 | 44 | .def("impact", &CraterAlbedo::Impact, "crater"_a, | |
| 347 | "The impact `crater` came from, drawn from its ID.") | ||
| 348 | 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 | 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 | 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 | 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 | 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 | 88 | .def(nb::init<SplotchOptions, Surface>(), "options"_a = SplotchOptions{}, | |
| 412 | 88 | "surface"_a = Surface{}) | |
| 413 | 44 | .def( | |
| 414 | "count", | ||
| 415 | ✗ | [](const SplotchAlbedo& layer, const Bounds2d& region, double spacing) { | |
| 416 | 1 | return layer.Splotches(region, spacing).size(); | |
| 417 | }, | ||
| 418 | 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 | return layer.Swarm(crater, spacing).size(); | |
| 423 | }, | ||
| 424 | 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 | 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 | 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 | 132 | .def(nb::init<std::vector<BlastZone>, Surface>(), "zones"_a, "surface"_a = Surface{}) | |
| 451 | 44 | .def("change", &BlastZoneAlbedo::Change, "zone"_a, "x"_a, "y"_a, | |
| 452 | "The relative reflectance change `zone` gives (x, y).") | ||
| 453 | 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 | 44 | BindKwargsInit<OpticalMaturityOrigin>(origin); | |
| 464 | 44 | origin.def_rw("reflectance", &OpticalMaturityOrigin::reflectance) | |
| 465 | .def_rw("ratio", &OpticalMaturityOrigin::ratio); | ||
| 466 | 44 | m.def("optical_maturity", &OpticalMaturity, "r750"_a, "r950"_a, | |
| 467 | 88 | "origin"_a = OpticalMaturityOrigin{}, | |
| 468 | "Lucey et al.'s (2000) optical maturity of reflectances at 750 and 950 nm."); | ||
| 469 | 44 | m.def("reflectance_at_maturity", &ReflectanceAtMaturity, "reflectance"_a, "reference"_a, | |
| 470 | 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 | 132 | "source"_a, "layers"_a = std::vector<std::shared_ptr<AlbedoLayer>>{}, | |
| 484 | 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 | 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 | 3 | terrain ? std::optional<Terrain>(terrain->Get()) : std::nullopt; | |
| 493 | const std::optional<TerrainGround> ground = | ||
| 494 | 3 | built ? std::optional<TerrainGround>(std::in_place, *built, built->layers.size()) | |
| 495 | 3 | : std::nullopt; | |
| 496 | 3 | const AlbedoSample sample = EvaluateAlbedoAt( | |
| 497 | 6 | albedo.Get(), x, y, AlbedoContext{spacing, 1, ground ? &*ground : nullptr}); | |
| 498 | 6 | return Bands(sample, albedo.source ? albedo.source->Bands() : 1); | |
| 499 | 3 | }, | |
| 500 | 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 | 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 | 22 | terrain ? std::optional<Terrain>(terrain->Get()) : std::nullopt; | |
| 509 | const std::optional<TerrainGround> ground = | ||
| 510 | 22 | built ? std::optional<TerrainGround>(std::in_place, *built, built->layers.size()) | |
| 511 | 22 | : std::nullopt; | |
| 512 | 22 | return EvaluateAlbedo(albedo.Get(), grid, | |
| 513 | 44 | AlbedoContext{spacing, threads, ground ? &*ground : nullptr}); | |
| 514 | 22 | }, | |
| 515 | 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 | 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 | 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 | 2 | const Terrain built = terrain.Get(); | |
| 539 | 2 | std::vector<RockSource> sources; | |
| 540 | 3 | for (const std::size_t n : layers) { | |
| 541 | 2 | if (n >= built.layers.size()) { | |
| 542 | 2 | throw std::invalid_argument("The terrain has no layer " + | |
| 543 | 3 | std::to_string(n)); | |
| 544 | } | ||
| 545 | 1 | auto layer = std::dynamic_pointer_cast<const BoulderLayer>(built.layers[n]); | |
| 546 | 1 | if (!layer) { | |
| 547 | ✗ | throw std::invalid_argument("Layer " + std::to_string(n) + | |
| 548 | ✗ | " is not a boulder layer"); | |
| 549 | } | ||
| 550 | 2 | sources.push_back({std::move(layer), SharedGround(built, n)}); | |
| 551 | 1 | } | |
| 552 | 1 | new (self) RockAlbedo(options, std::move(sources)); | |
| 553 | 4 | }, | |
| 554 | 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 | return rocks.ShareAt(x, y, AlbedoContext{spacing}); | |
| 561 | }, | ||
| 562 | 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 | 44 | m.def( | |
| 565 | "rock_share", | ||
| 566 | ✗ | [](double resolved, double mean, double variance, double normal) { | |
| 567 | 1 | return RockShare(resolved, {mean, variance}, normal); | |
| 568 | }, | ||
| 569 | 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 | 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 | 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 | 88 | .def(nb::init<SlopeMaturityOptions, Surface>(), "options"_a = SlopeMaturityOptions{}, | |
| 590 | 88 | "surface"_a = Surface{}) | |
| 591 | 44 | .def( | |
| 592 | "slope_at", | ||
| 593 | ✗ | [](const SlopeAlbedo& slopes, double x, double y, const PyTerrain& terrain) { | |
| 594 | 1 | const Terrain built = terrain.Get(); | |
| 595 | 1 | const TerrainGround ground(built, built.layers.size()); | |
| 596 | 2 | return slopes.SlopeAt(x, y, ground); | |
| 597 | 1 | }, | |
| 598 | 44 | "x"_a, "y"_a, "terrain"_a, "The slope in degrees at (x, y) on `terrain`.") | |
| 599 | 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 |