TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
268 / 306
87.6%
Branches:
236 / 253
93.3%
Functions:
60 / 61
98.4%

python/src/pipeline.cpp
Line Branch Exec Source
1 #include "bindings.h"
2 #include "common.h"
3 #include "objects.h"
4 #include "tables.h"
5
6 #include "terraingen/config/build.h"
7 #include "terraingen/config/generate.h"
8 #include "terraingen/config/json.h"
9 #include "terraingen/config/validate.h"
10 #include "terraingen/pipeline/body_mesh.h"
11 #include "terraingen/pipeline/features.h"
12 #include "terraingen/pipeline/height_mesh.h"
13 #include "terraingen/pipeline/terrain.h"
14 #include "terraingen/pipeline/tiles.h"
15
16 #include <nanobind/operators.h>
17
18 #include <filesystem>
19 #include <optional>
20 #include <stdexcept>
21 #include <string>
22 #include <utility>
23 #include <vector>
24
25 namespace terraingen::python {
26
27 namespace {
28
29 using OptionalPath = std::optional<std::filesystem::path>;
30
31 43 std::filesystem::path Directory(const OptionalPath& directory) {
32
1/1
✓ Branch 2 taken 43 times.
43 return directory.value_or(std::filesystem::path{});
33 }
34
35 using PointArray = nb::ndarray<const double, nb::ndim<2>, nb::device::cpu, nb::c_contig>;
36
37 /// `EvaluatePoints` over the rows of an (n, 2) array, as a float32 array.
38 2 nb::object EvaluatePointArray(const PyTerrain& terrain, const PointArray& points, double spacing,
39 unsigned threads) {
40
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 if (points.shape(1) != 2) {
41
1/1
✓ Branch 2 taken 1 time.
1 throw std::invalid_argument("Points must be an array of shape (n, 2)");
42 }
43
1/1
✓ Branch 2 taken 1 time.
1 std::vector<Vec2d> positions(points.shape(0));
44
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
4 for (std::size_t i = 0; i < positions.size(); ++i) {
45 3 positions[i] = {points.data()[2 * i], points.data()[2 * i + 1]};
46 }
47 1 std::vector<float> heights;
48 {
49 1 nb::gil_scoped_release release;
50
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
1 heights = EvaluatePoints(terrain.Get(), positions, spacing, threads);
51 1 }
52
1/1
✓ Branch 3 taken 1 time.
2 return Column(std::move(heights));
53 1 }
54
55 /// @brief `text` parsed by Python's json module.
56
1/1
✓ Branch 1 taken 22 times.
22 nb::object FromJson(const std::string& text) {
57
2/2
✓ Branch 1 taken 22 times.
✓ Branch 4 taken 22 times.
22 return nb::module_::import_("json").attr("loads")(text);
58 }
59
60 /// @brief `grid`, or else the grid of `terrain`'s source.
61 /// @throws std::invalid_argument when there is neither.
62 186 RasterGrid GridOr(const Terrain& terrain, const std::optional<RasterGrid>& grid) {
63
2/2
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 166 times.
186 if (grid) {
64 20 return *grid;
65 }
66
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 164 times.
166 if (!terrain.source) {
67
1/1
✓ Branch 2 taken 2 times.
2 throw std::invalid_argument("A terrain without a source needs a grid");
68 }
69 164 return terrain.source->Grid();
70 }
71
72 /// @brief The list format a file's extension asks for: JSON for .json, CSV otherwise.
73 6 ListFormat FormatOf(const std::filesystem::path& path) {
74
4/4
✓ Branch 1 taken 6 times.
✓ Branch 4 taken 6 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 4 times.
6 return path.extension() == ".json" ? ListFormat::Json : ListFormat::Csv;
75 }
76
77 44 void BindTerrainClass(nb::module_& m) {
78 ✗ nb::class_<PyTerrain>(m, "Terrain",
79 "A terrain: heights from `source` (the datum, 0 m, when None), plus "
80 "each of `layers` in order.")
81 44 .def(
82 "__init__",
83 202 [](PyTerrain* self, std::shared_ptr<Source> source,
84 std::vector<std::shared_ptr<Layer>> layers, const Surface& surface) {
85 158 new (self) PyTerrain{std::move(source), std::move(layers), surface};
86 158 },
87
2/2
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
132 "source"_a = nb::none(), "layers"_a = std::vector<std::shared_ptr<Layer>>{},
88
1/1
✓ Branch 2 taken 44 times.
88 "surface"_a = Surface{}, "`surface` must be the one the layers were built for.")
89 44 .def_rw("surface", &PyTerrain::surface)
90 44 .def_rw("source", &PyTerrain::source)
91 .def_rw("layers", &PyTerrain::layers);
92 44 }
93
94 44 void BindFeatures(nb::module_& m) {
95 88 nb::class_<CraterFeatures>(m, "CraterFeatures", "Craters and their attributes, in step.")
96 44 .def_ro("craters", &CraterFeatures::craters, "The craters, as a CraterList.")
97 44 .def_prop_ro(
98 29 "table", [](const CraterFeatures& f) { return CraterTable(f.craters, f.attributes); },
99 "The craters as NumPy columns 'x', 'y', 'diameter', 'age_gyr', 'id' and 'manual' (1 "
100 "for manual craters), in the surface's coordinates, and their attributes "
101 "'floor_depth' and 'rim_height' (m), 'depth_ratio', 'freshness' and 'complex'.")
102 .def(
103 "write",
104 47 [](const CraterFeatures& f, const std::filesystem::path& path) {
105 3 WriteCraterList(f.craters, path, FormatOf(path), f.attributes);
106 3 },
107
1/1
✓ Branch 1 taken 44 times.
88 "path"_a, "Writes the craters as CSV, or JSON for a .json path.");
108 44 nb::class_<BoulderFeatures>(m, "BoulderFeatures",
109 "Boulders, and those of them placed as their own 3D models.")
110 44 .def_ro("boulders", &BoulderFeatures::boulders, "The boulders, as Boulder objects.")
111 44 .def_ro("models", &BoulderFeatures::models,
112 "The boulder models, with their shapes and placements.")
113 44 .def_prop_ro(
114 20 "table", [](const BoulderFeatures& f) { return BoulderTable(f.boulders); },
115 "The boulders as NumPy columns 'x', 'y', 'ground' (NaN when unknown), 'diameter', "
116 "'height', 'burial', 'yaw', 'lean', 'id' and 'model' (1 for boulder models).")
117 44 .def(
118 "write_list",
119 ✗ [](const BoulderFeatures& f, const std::filesystem::path& path) {
120 3 WriteBoulderList(f.boulders, path, FormatOf(path));
121 3 },
122
1/1
✓ Branch 1 taken 44 times.
44 "path"_a, "Writes the boulders as CSV, or JSON for a .json path.")
123 .def(
124 "write_models",
125 47 [](const BoulderFeatures& f, const std::filesystem::path& path, double creaseAngle,
126 bool instanced) {
127
1/1
✓ Branch 2 taken 3 times.
3 WriteBoulderModels(f.models, path, creaseAngle, {}, {}, instanced);
128 3 },
129
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
176 "path"_a, "crease_angle"_a = 60.0, "instanced"_a = true,
130 "Writes the boulder models as a glTF scene (.glb or .gltf), OBJ (.obj) or STL (.stl): "
131 "each shared shape once, drawn per boulder, or with `instanced` false every boulder as "
132 "its own placed mesh.");
133
1/1
✓ Branch 1 taken 44 times.
44 m.def(
134 "placed_craters",
135 ✗ [](const PyTerrain& terrain, std::optional<RasterGrid> grid, double spacing) {
136
1/1
✓ Branch 1 taken 32 times.
32 const Terrain built = terrain.Get();
137
2/2
✓ Branch 1 taken 31 times.
✓ Branch 4 taken 31 times.
63 return PlacedCraters(built, GridOr(built, grid), spacing);
138 32 },
139
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
176 "terrain"_a, "grid"_a = nb::none(), "spacing"_a = 0.0,
140 ✗ nb::call_guard<nb::gil_scoped_release>(),
141 "The craters the terrain's crater and secondary crater layers apply over `grid` (by "
142 "default its source's) at `spacing` metres (0 for the grid's), in layer order.");
143
1/1
✓ Branch 1 taken 44 times.
44 m.def(
144 "placed_boulders",
145 ✗ [](const PyTerrain& terrain, std::optional<RasterGrid> grid, double spacing) {
146
1/1
✓ Branch 1 taken 25 times.
25 const Terrain built = terrain.Get();
147
2/2
✓ Branch 1 taken 25 times.
✓ Branch 4 taken 25 times.
50 return PlacedBoulders(built, GridOr(built, grid), spacing);
148 25 },
149
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
176 "terrain"_a, "grid"_a = nb::none(), "spacing"_a = 0.0,
150 44 nb::call_guard<nb::gil_scoped_release>(),
151 "The boulders the terrain's boulder layers place over `grid` (by default its source's) at "
152 "`spacing` metres (0 for the grid's), and the boulder models among them.");
153 44 }
154
155 44 void BindEvaluation(nb::module_& m) {
156
1/1
✓ Branch 1 taken 44 times.
44 m.def(
157 "evaluate",
158 ✗ [](const PyTerrain& terrain, double spacing, unsigned threads,
159 std::optional<RasterGrid> grid, std::optional<Backend> backend) {
160
1/1
✓ Branch 1 taken 129 times.
129 const Terrain built = terrain.Get();
161
1/1
✓ Branch 1 taken 128 times.
130 return Evaluate(built, GridOr(built, grid),
162
1/1
✓ Branch 2 taken 127 times.
515 EvaluationOptions{spacing, threads, backend});
163 129 },
164
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
220 "terrain"_a, "spacing"_a = 0.0, "threads"_a = 0, "grid"_a = nb::none(),
165
1/1
✓ Branch 1 taken 44 times.
88 "backend"_a = nb::none(), nb::call_guard<nb::gil_scoped_release>(),
166 "Evaluates `terrain` over `grid` (by default its source's) at a sample spacing of "
167 "`spacing` metres (0 for the grid's), on up to `threads` threads (0 for every hardware "
168 "thread; the result does not depend on it), on `backend` (None for default_backend()). "
169 "On Vulkan the heights match the CPU to within float rounding.");
170
1/1
✓ Branch 1 taken 44 times.
44 m.def(
171 "evaluate_at",
172 ✗ [](const PyTerrain& terrain, double x, double y, double spacing) {
173
2/2
✓ Branch 1 taken 4015 times.
✓ Branch 4 taken 4015 times.
4015 return EvaluateAt(terrain.Get(), x, y, spacing);
174 },
175
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 "terrain"_a, "x"_a, "y"_a, "spacing"_a, nb::call_guard<nb::gil_scoped_release>(),
176 "The terrain's height at (x, y) sampled every `spacing` metres; equal to the cell centred "
177 "there of `evaluate`.");
178
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("evaluate_points", &EvaluatePointArray, "terrain"_a, "points"_a, "spacing"_a,
179
1/1
✓ Branch 1 taken 44 times.
88 "threads"_a = 0,
180 "The terrain's heights at `points`, an array of shape (n, 2) in surface coordinates, "
181 "sampled every `spacing` metres, as a float32 array; each equals `evaluate_at` there. "
182 "Batches of points are much faster than one call per point.");
183
3/3
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
✓ Branch 8 taken 44 times.
88 m.def("height_mesh", &HeightMesh, "heights"_a, "surface"_a = Surface{},
184
1/1
✓ Branch 1 taken 44 times.
88 "site"_a = Vec2d{0.0, 0.0},
185 "The heights as a mesh, a vertex per cell with data, in the frame boulder models are "
186 "written in: world coordinates on a plane, east-north-up metres of `site` on a sphere.");
187
1/1
✓ Branch 1 taken 44 times.
44 m.def(
188 "body_mesh",
189 ✗ [](const PyTerrain& terrain, std::size_t cellsPerFace, double spacing, unsigned threads) {
190
2/2
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 4 times.
5 return BodyMesh(terrain.Get(), cellsPerFace, spacing, threads);
191 },
192
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
176 "terrain"_a, "cells_per_face"_a, "spacing"_a = 0.0, "threads"_a = 0,
193 ✗ nb::call_guard<nb::gil_scoped_release>(),
194 "A mesh of the whole body a terrain on a sphere or ellipsoid lies on: six cube faces of "
195 "`cells_per_face` squared quads, each vertex at the radius plus the terrain's height "
196 "there.");
197
1/1
✓ Branch 1 taken 44 times.
44 m.def(
198 "body_icq",
199 ✗ [](const PyTerrain& terrain, std::size_t q, double spacing, unsigned threads) {
200
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
1 return BodyIcq(terrain.Get(), q, spacing, threads);
201 },
202
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
176 "terrain"_a, "q"_a, "spacing"_a = 0.0, "threads"_a = 0,
203 44 nb::call_guard<nb::gil_scoped_release>(),
204 "The whole body a terrain on a sphere or ellipsoid lies on as an ICQ shape model with "
205 "`q` intervals per face edge, each label at the radius plus the terrain's height there.");
206 44 }
207
208 44 void BindTileKey(nb::module_& m) {
209 ✗ nb::class_<TileKey>(m, "TileKey", "Tile (x, y) of a tile scheme's level `level`.")
210 44 .def(
211 "__init__",
212 ✗ [](TileKey* self, int level, std::int64_t x, std::int64_t y) {
213 34 new (self) TileKey{level, x, y};
214 34 },
215
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 "level"_a, "x"_a, "y"_a)
216 44 .def_rw("level", &TileKey::level)
217 44 .def_rw("x", &TileKey::x)
218 44 .def_rw("y", &TileKey::y)
219
1/1
✓ Branch 2 taken 44 times.
44 .def(nb::self == nb::self)
220 44 .def("__repr__",
221 47 [](const TileKey& k) {
222
6/6
✓ Branch 1 taken 3 times.
✓ Branch 5 taken 3 times.
✓ Branch 8 taken 3 times.
✓ Branch 11 taken 3 times.
✓ Branch 14 taken 3 times.
✓ Branch 17 taken 3 times.
6 return "TileKey(" + std::to_string(k.level) + ", " + std::to_string(k.x) + ", " +
223
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
9 std::to_string(k.y) + ")";
224 })
225 88 .def_prop_ro("parent", &TileScheme::Parent)
226 44 .def_prop_ro("children", [](const TileKey& key) {
227
1/1
✓ Branch 1 taken 1 time.
1 const auto c = TileScheme::Children(key);
228
1/1
✓ Branch 3 taken 1 time.
2 return std::vector<TileKey>(c.begin(), c.end());
229 });
230 44 }
231
232 44 void BindTileScheme(nb::module_& m) {
233 ✗ nb::class_<TileScheme>(
234 m, "TileScheme",
235 "A quadtree of north-up tiles: level 0 has tiles `root_size` across from `origin` (the "
236 "top-left corner of tile (0, 0)), each level halves them, and every tile has `cells` x "
237 "`cells` samples.")
238 44 .def(
239 "__init__",
240 ✗ [](TileScheme* self, const Vec2d& origin, double rootSize, std::size_t cells) {
241 2 new (self) TileScheme{origin, rootSize, cells};
242 2 self->Validate();
243 1 },
244
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
132 "origin"_a, "root_size"_a, "cells"_a = 256)
245
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 .def_static("covering", &TileScheme::Covering, "grid"_a, "cells"_a = 256,
246 "A scheme one of whose levels has the cells of `grid`.")
247
1/1
✓ Branch 1 taken 44 times.
132 .def_static("whole_body", &TileScheme::WholeBody, "cells"_a = 256,
248 "A longitude-latitude pyramid over a whole sphere.")
249
1/1
✓ Branch 1 taken 44 times.
44 .def_static("body_tiles", &TileScheme::BodyTiles, "level"_a,
250 "Every tile of a level of a whole-body scheme.")
251
1/1
✓ Branch 1 taken 44 times.
88 .def_static("wrap_body_tile", &TileScheme::WrapBodyTile, "key"_a,
252 "The whole-body tile a neighbour across the antimeridian is.")
253 44 .def_ro("origin", &TileScheme::origin)
254 44 .def_ro("root_size", &TileScheme::rootSize)
255 44 .def_ro("cells", &TileScheme::cells)
256
1/1
✓ Branch 1 taken 44 times.
44 .def("tile_size", &TileScheme::TileSize, "level"_a)
257
1/1
✓ Branch 1 taken 44 times.
44 .def("spacing", &TileScheme::Spacing, "level"_a)
258
1/1
✓ Branch 1 taken 44 times.
44 .def("level_for", &TileScheme::LevelFor, "spacing"_a,
259 "The coarsest level whose spacing is at most `spacing`.")
260
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 .def("tile_at", &TileScheme::TileAt, "x"_a, "y"_a, "level"_a)
261
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 .def("tiles_covering", &TileScheme::TilesCovering, "bounds"_a, "level"_a)
262
1/1
✓ Branch 1 taken 44 times.
88 .def("grid", &TileScheme::Grid, "key"_a, "The tile's grid of samples.");
263 44 }
264
265 44 void BindTiles(nb::module_& m) {
266 44 BindTileKey(m);
267 44 BindTileScheme(m);
268
1/1
✓ Branch 1 taken 44 times.
44 m.def(
269 "evaluate_tile",
270 ✗ [](const PyTerrain& terrain, const TileScheme& scheme, const TileKey& key,
271
2/2
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
5 unsigned threads) { return EvaluateTile(terrain.Get(), scheme, key, threads); },
272
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
132 "terrain"_a, "scheme"_a, "key"_a, "threads"_a = 1, nb::call_guard<nb::gil_scoped_release>(),
273 "One tile of `scheme` at its level's spacing, equal to the same cells of any larger grid.");
274
1/1
✓ Branch 1 taken 44 times.
44 m.def(
275 "evaluate_tiles",
276 ✗ [](const PyTerrain& terrain, const TileScheme& scheme, const std::vector<TileKey>& keys,
277
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
3 unsigned threads) { return EvaluateTiles(terrain.Get(), scheme, keys, threads); },
278
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
132 "terrain"_a, "scheme"_a, "keys"_a, "threads"_a = 0,
279 44 nb::call_guard<nb::gil_scoped_release>(),
280 "`evaluate_tile` for each of `keys`, split over threads.");
281 ✗ nb::class_<TileCache>(
282 m, "TileCache",
283 "Tiles of one terrain and scheme, evaluated on demand and kept up to about "
284 "`max_bytes`, least recently used first out; thread-safe. The budget is "
285 "split over 16 shards that each keep at least one tile, so a budget "
286 "smaller than 16 tiles is exceeded.")
287 44 .def(
288 "__init__",
289 ✗ [](TileCache* self, const PyTerrain& terrain, const TileScheme& scheme,
290
2/4
✓ Branch 2 taken 4 times.
✓ Branch 5 taken 4 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
4 std::size_t maxBytes) { new (self) TileCache(terrain.Get(), scheme, maxBytes); },
291
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
132 "terrain"_a, "scheme"_a, "max_bytes"_a = std::size_t{1} << 30)
292 44 .def(
293 "get",
294
2/2
✓ Branch 1 taken 589 times.
✓ Branch 5 taken 589 times.
589 [](TileCache& cache, const TileKey& key) { return Raster<float>(*cache.Get(key)); },
295
1/1
✓ Branch 1 taken 44 times.
44 "key"_a, nb::call_guard<nb::gil_scoped_release>(),
296 "The tile at `key`, evaluated if it is not held.")
297 44 .def_prop_ro("size", &TileCache::Size, "Bytes held.")
298 44 .def_prop_ro("max_bytes", &TileCache::MaxBytes)
299 88 .def_prop_ro("evictions", &TileCache::Evictions, "Tiles evicted so far.");
300 44 }
301
302 /// @brief Registers ConfigError, a ValueError with the JSON `path` of the offending value.
303 44 void BindConfigError(nb::module_& m) {
304 // Named within the extension module, as nanobind names its own types, so its stubs list it.
305
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
88 const std::string name = nb::cast<std::string>(m.attr("__name__")) + ".ConfigError";
306 static nb::object configError =
307
1/1
✓ Branch 1 taken 44 times.
44 nb::steal(PyErr_NewExceptionWithDoc(name.c_str(),
308 "An invalid configuration; `path` is the JSON path of "
309 "the offending value, empty for the whole document.",
310
2/6
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 44 times.
✗ Branch 4 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
88 PyExc_ValueError, nullptr));
311
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.attr("ConfigError") = configError;
312
1/1
✓ Branch 2 taken 44 times.
44 nb::register_exception_translator([](const std::exception_ptr& error, void*) {
313 try {
314 144 std::rethrow_exception(error);
315
2/2
✓ Branch 0 taken 62 times.
✓ Branch 1 taken 10 times.
72 } catch (const ConfigError& config) {
316
1/1
✓ Branch 2 taken 10 times.
10 nb::object instance = configError(config.what());
317
2/2
✓ Branch 2 taken 10 times.
✓ Branch 5 taken 10 times.
20 instance.attr("path") = config.Path();
318
1/1
✓ Branch 1 taken 10 times.
20 PyErr_SetObject(configError.ptr(), instance.ptr());
319 10 }
320 10 });
321 44 }
322
323 44 void BindConfigIo(nb::class_<TerrainConfig>& config) {
324 ✗ config.def(nb::init<>(), "The default configuration.")
325
1/1
✓ Branch 1 taken 44 times.
44 .def_static("from_json", &ParseConfig, "text"_a,
326 "Parses a configuration from JSON text. Raises ConfigError if it does not fit "
327 "the format.")
328 44 .def_static(
329 "from_dict",
330
1/1
✓ Branch 1 taken 61 times.
61 [](nb::handle data) {
331 61 const nb::object json = nb::module_::import_("json");
332
3/3
✓ Branch 1 taken 61 times.
✓ Branch 4 taken 61 times.
✓ Branch 8 taken 58 times.
244 return ParseConfig(nb::cast<std::string>(json.attr("dumps")(data)));
333 },
334
1/1
✓ Branch 1 taken 44 times.
44 "data"_a, "Parses a configuration from a dict of JSON values.")
335
1/1
✓ Branch 1 taken 44 times.
44 .def_static("load", &LoadConfig, "path"_a,
336 "Reads the configuration in the JSON file at `path`.")
337
1/1
✓ Branch 1 taken 44 times.
176 .def("to_json", &ConfigToJson, "indent"_a = 2,
338 "The configuration as JSON with every value written out.")
339 .def(
340
2/2
✓ Branch 2 taken 17 times.
✓ Branch 5 taken 17 times.
61 "to_dict", [](const TerrainConfig& c) { return FromJson(ConfigToJson(c, -1)); },
341 "The configuration as a dict of JSON values, every value written out.");
342 44 }
343
344 44 void BindConfigQueries(nb::class_<TerrainConfig>& config) {
345 config
346 44 .def(
347 "validate",
348 ✗ [](const TerrainConfig& c, const OptionalPath& directory) {
349
2/2
✓ Branch 1 taken 19 times.
✓ Branch 4 taken 13 times.
25 ValidateConfig(c, Directory(directory));
350 13 },
351
1/1
✓ Branch 1 taken 44 times.
88 "directory"_a = nb::none(),
352 "Checks everything parsing cannot (grid limits, raster files, model parameters, "
353 "expected crater counts); relative input paths resolve against `directory`. Raises "
354 "ConfigError naming the first invalid value.")
355 55 .def("__eq__", [](const TerrainConfig& a, const TerrainConfig& b) { return a == b; })
356 44 .def(
357 "body",
358 ✗ [](const TerrainConfig& c, const OptionalPath& directory) {
359
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
1 return BuildSurface(c.body, Directory(directory));
360 },
361
1/1
✓ Branch 1 taken 44 times.
88 "directory"_a = nb::none(),
362 "The Surface the configuration's terrain lies on; a shape model's path resolves "
363 "against `directory`.")
364 44 .def(
365 "base_grid",
366 ✗ [](const TerrainConfig& c, const OptionalPath& directory) -> std::optional<RasterGrid> {
367
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 if (!c.base) {
368 1 return std::nullopt;
369 }
370
0/2
✗ Branch 1 not taken.
✗ Branch 5 not taken.
✗ return DescribeSource(*c.base, Directory(directory),
371
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ BuildSurface(c.body, Directory(directory)));
372 },
373
1/1
✓ Branch 1 taken 44 times.
88 "directory"_a = nb::none(),
374 "The grid the configuration's base covers, reading only a raster's header; None "
375 "without a base.")
376 44 .def(
377 "output_grids",
378 49 [](const TerrainConfig& c, const OptionalPath& directory) {
379
2/2
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
5 return ValidateConfig(c, Directory(directory)).outputGrids;
380 },
381
1/1
✓ Branch 1 taken 44 times.
88 "directory"_a = nb::none(),
382 "The grid each output covers, in order (None for a mesh), after validating the "
383 "configuration.")
384 44 .def("__repr__", [](const TerrainConfig& c) {
385
4/4
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
✓ Branch 8 taken 1 time.
✓ Branch 11 taken 1 time.
2 return "Config(" + std::to_string(c.layers.size()) + " layers, " +
386
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
3 std::to_string(c.outputs.size()) + " outputs)";
387 });
388 44 }
389
390 44 void BindConfig(nb::module_& m) {
391
1/1
✓ Branch 1 taken 44 times.
44 BindConfigError(m);
392 nb::class_<TerrainConfig> config(m, "Config",
393 "A terrain configuration: a body, optionally a base, layers, "
394 "outputs and optionally an albedo, as the JSON configs of the "
395 44 "terraingen command describe.");
396
1/1
✓ Branch 1 taken 44 times.
44 BindConfigIo(config);
397
1/1
✓ Branch 1 taken 44 times.
44 BindConfigQueries(config);
398
1/1
✓ Branch 1 taken 44 times.
44 m.def(
399
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
4 "config_catalog", [] { return FromJson(ConfigCatalog(-1)); },
400 "Every configuration type with its defaults, as a dict: the lists 'body', 'base', "
401 "'layers' and 'outputs', the 'albedo', and the 'models' (densities, ages, "
402 "degradation, profiles, boulder sizes, region shapes and albedo sources and layers).");
403
1/1
✓ Branch 1 taken 44 times.
44 m.def(
404
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
1 "config_schema", [] { return FromJson(ConfigSchema(-1)); },
405 "The structure of a configuration, for editors, as a dict: 'root' (the configuration "
406 "object) and 'definitions' (each struct and choice of types by id, referred to as "
407 "{'kind': 'ref', 'ref': id}). Every field has a 'name', 'description', 'unit', 'schema' "
408 "and 'default'; schema kinds are number, integer, boolean, string, enum (with 'choices' "
409 "and their 'descriptions'), optional, array, object and choice (with 'alternatives').");
410 44 }
411
412 /// @brief `grid`, or else the first grid `terrain` generated.
413 /// @throws std::invalid_argument when there is neither.
414 8 RasterGrid GeneratedGridOr(const GeneratedTerrain& terrain, const std::optional<RasterGrid>& grid) {
415
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 5 times.
8 if (grid) {
416 3 return *grid;
417 }
418
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
5 if (terrain.grids.empty()) {
419
0/1
✗ Branch 2 not taken.
✗ throw std::invalid_argument("The terrain generated no grid; give one");
420 }
421 5 return RasterGrid::FromRaster(terrain.grids.front().heights);
422 }
423
424 44 void BindGeneratedTerrain(nb::module_& m) {
425 88 nb::class_<GeneratedGrid>(m, "GeneratedGrid",
426 "A terrain's heights, and its albedo bands, over one grid.")
427 44 .def_prop_ro(
428 8 "heights", [](GeneratedGrid& g) -> Raster<float>& { return g.heights; },
429 nb::rv_policy::reference_internal, "Heights in metres over the grid.")
430 44 .def_prop_ro(
431 "albedo",
432 46 [](nb::handle self) {
433 2 nb::list bands;
434
2/2
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 2 times.
3 for (Raster<float>& band : nb::cast<GeneratedGrid&>(self).albedo) {
435
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
2 bands.append(nb::cast(&band, nb::rv_policy::reference_internal, self));
436 }
437 2 return bands;
438 ✗ },
439 "Albedo bands, empty unless an albedo output covers the grid.")
440 44 .def_prop_ro(
441 "materials",
442 44 [](nb::handle self) {
443 ✗ nb::list bands;
444
0/2
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ for (Raster<float>& band : nb::cast<GeneratedGrid&>(self).materials) {
445
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ bands.append(nb::cast(&band, nb::rv_policy::reference_internal, self));
446 }
447 ✗ return bands;
448 ✗ },
449 "The share of each MaterialClass, one raster per class; empty unless a materials or "
450 "photometry output covers the grid.")
451 .def_prop_ro(
452 45 "grid", [](const GeneratedGrid& g) { return RasterGrid::FromRaster(g.heights); },
453 "The grid the heights cover.");
454 nb::class_<GeneratedTerrain> terrain(m, "GeneratedTerrain",
455 "A generated terrain: the heights and albedo over each "
456 "grid its outputs cover, and the craters and boulders it "
457 44 "places on request.");
458 terrain.def_prop_ro(
459 "grids",
460 57 [](nb::handle self) {
461 13 nb::list grids;
462
2/2
✓ Branch 4 taken 13 times.
✓ Branch 5 taken 13 times.
26 for (GeneratedGrid& grid : nb::cast<GeneratedTerrain&>(self).grids) {
463
2/2
✓ Branch 2 taken 13 times.
✓ Branch 5 taken 13 times.
26 grids.append(nb::cast(&grid, nb::rv_policy::reference_internal, self));
464 }
465 13 return grids;
466 ✗ },
467 "The GeneratedGrid of each distinct grid a heightmap, albedo or terrain mesh output "
468 "covers, in output order.");
469 terrain
470 44 .def(
471 "placed_craters",
472 ✗ [](const GeneratedTerrain& t, std::optional<RasterGrid> grid, double spacing) {
473
2/2
✓ Branch 1 taken 6 times.
✓ Branch 4 taken 6 times.
6 return PlacedCraters(t.terrain, GeneratedGridOr(t, grid), spacing);
474 },
475
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "grid"_a = nb::none(), "spacing"_a = 0.0,
476 "The craters the terrain's layers apply over `grid` (by default its first generated "
477 "grid) at `spacing` metres (0 for the grid's).")
478 44 .def(
479 "placed_boulders",
480 46 [](const GeneratedTerrain& t, std::optional<RasterGrid> grid, double spacing) {
481
2/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
2 return PlacedBoulders(t.terrain, GeneratedGridOr(t, grid), spacing);
482 },
483
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "grid"_a = nb::none(), "spacing"_a = 0.0,
484 "The boulders the terrain's layers place over `grid` (by default its first generated "
485 "grid) at `spacing` metres (0 for the grid's), and the models among them.")
486 44 .def_ro("seconds", &GeneratedTerrain::seconds,
487 "Wall-clock time the heights and albedo took to evaluate, in seconds.")
488 44 .def_ro("backend", &GeneratedTerrain::backend,
489 "Where the heights were evaluated: CPU or a GPU backend.")
490 88 .def("summary", &Summary, "One line describing the terrain: its size and time.");
491 44 }
492
493 44 void BindRun(nb::module_& m) {
494
1/1
✓ Branch 1 taken 44 times.
44 m.def(
495 "generate_terrain",
496 ✗ [](const TerrainConfig& config, const OptionalPath& directory, unsigned threads,
497 std::optional<Backend> backend) {
498
2/2
✓ Branch 1 taken 12 times.
✓ Branch 4 taken 12 times.
12 return GenerateTerrain(config, Directory(directory), threads, backend);
499 },
500
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
220 "config"_a, "directory"_a = nb::none(), "threads"_a = 0, "backend"_a = nb::none(),
501 44 nb::call_guard<nb::gil_scoped_release>(),
502 "Generates the terrain `config` describes over each grid its outputs cover, on up to "
503 "`threads` threads (0 for every hardware thread; the result does not depend on it), "
504 "evaluating the heights on `backend` (None for default_backend()). "
505 "Relative input "
506 "paths resolve against `directory`. Raises ConfigError for an invalid configuration.");
507
1/1
✓ Branch 1 taken 44 times.
44 m.def(
508 "build_terrain",
509 ✗ [](const TerrainConfig& config, const OptionalPath& directory) {
510
3/3
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
✓ Branch 7 taken 5 times.
5 return FromTerrain(BuildTerrain(config, Directory(directory)));
511 },
512
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "config"_a, "directory"_a = nb::none(),
513 "The Terrain a configuration describes, as library objects to evaluate, tile, query at "
514 "points or change; relative input paths resolve against `directory`.");
515
1/1
✓ Branch 1 taken 44 times.
44 m.def(
516 "build_albedo",
517 ✗ [](const TerrainConfig& config, const PyTerrain& terrain, const OptionalPath& directory) {
518
4/4
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
✓ Branch 7 taken 1 time.
✓ Branch 10 taken 1 time.
1 return FromAlbedo(BuildAlbedo(config, terrain.Get(), Directory(directory)));
519 },
520
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
132 "config"_a, "terrain"_a, "directory"_a = nb::none(),
521 "The Albedo a configuration describes for `terrain`, built from the same configuration "
522 "by `build_terrain`.");
523
1/1
✓ Branch 1 taken 44 times.
44 m.def(
524 "write_outputs",
525 ✗ [](const TerrainConfig& config, const GeneratedTerrain& terrain,
526 const std::filesystem::path& directory) {
527 2 std::vector<std::string> paths;
528
3/3
✓ Branch 1 taken 2 times.
✓ Branch 7 taken 5 times.
✓ Branch 8 taken 2 times.
7 for (const auto& path : WriteOutputs(config, terrain, directory)) {
529
2/2
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
5 paths.push_back(path.string());
530 2 }
531 2 return paths;
532 ✗ },
533
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 "config"_a, "terrain"_a, "directory"_a, nb::call_guard<nb::gil_scoped_release>(),
534 "Writes `config`'s outputs for `terrain`, resolving relative paths against `directory`, "
535 "and returns the paths written.");
536 44 }
537
538 } // namespace
539
540 44 void BindPipeline(nb::module_& m) {
541 44 BindTerrainClass(m);
542 44 BindFeatures(m);
543 44 BindEvaluation(m);
544 44 BindTiles(m);
545 44 BindConfig(m);
546 44 BindGeneratedTerrain(m);
547 44 BindRun(m);
548 44 }
549
550 } // namespace terraingen::python
551