python/src/util.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "bindings.h" | ||
| 2 | #include "common.h" | ||
| 3 | |||
| 4 | #include "terraingen/util/build_info.h" | ||
| 5 | #include "terraingen/util/cells.h" | ||
| 6 | #include "terraingen/util/log.h" | ||
| 7 | #include "terraingen/util/radial_shape.h" | ||
| 8 | #include "terraingen/util/special_functions.h" | ||
| 9 | #include "terraingen/util/surface.h" | ||
| 10 | #include "terraingen/util/version.h" | ||
| 11 | |||
| 12 | #include <memory> | ||
| 13 | #include <optional> | ||
| 14 | #include <string> | ||
| 15 | #include <tuple> | ||
| 16 | |||
| 17 | namespace terraingen::python { | ||
| 18 | |||
| 19 | namespace { | ||
| 20 | |||
| 21 | 44 | void BindVersion(nb::module_& m) { | |
| 22 | 44 | m.attr("__version__") = TERRAINGEN_FULL_VERSION_STRING; | |
| 23 | 44 | m.def( | |
| 24 | "version", | ||
| 25 | ✗ | [] { | |
| 26 | 1 | const Version v = GetVersion(); | |
| 27 | 2 | return nb::make_tuple(v.major, v.minor, v.patch); | |
| 28 | }, | ||
| 29 | "Library version as (major, minor, patch)."); | ||
| 30 | 44 | m.def("api_version", &GetApiVersion, "Library API version."); | |
| 31 | 44 | m.def( | |
| 32 | "build_info", | ||
| 33 | 45 | [] { | |
| 34 | 1 | const BuildInfo info = GetBuildInfo(); | |
| 35 | 1 | nb::dict d; | |
| 36 | 1 | d["git_sha"] = info.gitSha; | |
| 37 | 1 | d["git_dirty"] = info.gitDirty; | |
| 38 | 1 | d["build_date"] = info.buildDate; | |
| 39 | 2 | return d; | |
| 40 | 1 | }, | |
| 41 | "Build metadata of the compiled library: git_sha, git_dirty and build_date."); | ||
| 42 | 44 | } | |
| 43 | |||
| 44 | /// @brief A log sink that calls `callback(level, category, message)` with the GIL held. | ||
| 45 | 48 | LogSink PythonLogSink(nb::callable callback) { | |
| 46 | 48 | auto function = std::make_shared<nb::callable>(std::move(callback)); | |
| 47 | 96 | return [function](const LogRecord& record) { | |
| 48 | 2 | nb::gil_scoped_acquire gil; | |
| 49 | try { | ||
| 50 | 4 | (*function)(LogLevelName(record.level), record.category, record.message); | |
| 51 | ✗ | } catch (nb::python_error& error) { | |
| 52 | ✗ | error.discard_as_unraisable("TerrainGen log sink"); | |
| 53 | } | ||
| 54 | 98 | }; | |
| 55 | 48 | } | |
| 56 | |||
| 57 | 44 | void BindLog(nb::module_& m) { | |
| 58 | 44 | m.def( | |
| 59 | "set_log_level", | ||
| 60 | ✗ | [](std::string_view name) { | |
| 61 | 8 | const std::optional<LogLevel> level = ParseLogLevel(name); | |
| 62 | 8 | if (!level) { | |
| 63 | throw nb::value_error( | ||
| 64 | 1 | "Log level must be trace, debug, info, warning, error or off"); | |
| 65 | } | ||
| 66 | 7 | SetLogLevel(*level); | |
| 67 | 7 | }, | |
| 68 | 44 | "level"_a, | |
| 69 | "Sets the lowest level TerrainGen logs: 'trace', 'debug', 'info', 'warning' (the " | ||
| 70 | "default), 'error' or 'off'."); | ||
| 71 | 44 | m.def( | |
| 72 | 4 | "get_log_level", [] { return std::string(LogLevelName(GetLogLevel())); }, | |
| 73 | "The lowest level TerrainGen currently logs."); | ||
| 74 | 44 | m.def( | |
| 75 | "set_log_sink", | ||
| 76 | ✗ | [](std::optional<nb::callable> callback) { | |
| 77 | 91 | SetLogSink(callback ? PythonLogSink(std::move(*callback)) : nullptr); | |
| 78 | 91 | }, | |
| 79 | 88 | "callback"_a.none(), | |
| 80 | "Sends every log record to callback(level, category, message), or restores standard " | ||
| 81 | "error when callback is None. The package installs a sink that forwards to the logging " | ||
| 82 | "module on import. The callback runs with the GIL held, on whichever thread logged."); | ||
| 83 | 44 | } | |
| 84 | |||
| 85 | 44 | void BindSurfacePoints(nb::class_<Surface>& surface) { | |
| 86 | surface | ||
| 87 | 44 | .def("distance", &Surface::Distance, "a"_a, "b"_a, | |
| 88 | "The distance in metres between two points, great-circle on a sphere.") | ||
| 89 | 44 | .def("position", &Surface::Position, "point"_a, | |
| 90 | "The 3D position (m) of a point; on a sphere, body-fixed with z north.") | ||
| 91 | 44 | .def("offset", &Surface::Offset, "centre"_a, "point"_a, | |
| 92 | "`point` as (east, north) metres from `centre` (azimuthal equidistant on a sphere).") | ||
| 93 | 44 | .def("locate", &Surface::Locate, "centre"_a, "offset"_a, | |
| 94 | "The point (east, north) metres from `centre`.") | ||
| 95 | 44 | .def( | |
| 96 | "frame", | ||
| 97 | ✗ | [](const Surface& s, const Vec2d& point) { | |
| 98 | 1 | const LocalFrame frame = s.Frame(point); | |
| 99 | 2 | return std::tuple{frame.east, frame.north, frame.up}; | |
| 100 | }, | ||
| 101 | 44 | "point"_a, "The (east, north, up) unit vectors at a point.") | |
| 102 | 44 | .def("point_of", &Surface::PointOf, "direction"_a, | |
| 103 | "The surface point of a direction from the sphere's centre.") | ||
| 104 | 44 | .def("expand", &Surface::Expand, "region"_a, "metres"_a, | |
| 105 | "A box holding every point within `metres` of `region`.") | ||
| 106 | 44 | .def("contains", &Surface::Contains, "region"_a, "point"_a, | |
| 107 | "Whether `region` holds `point`, across the antimeridian and at the poles on a " | ||
| 108 | "sphere."); | ||
| 109 | 44 | } | |
| 110 | |||
| 111 | 44 | void BindRadialShape(nb::module_& m) { | |
| 112 | ✗ | nb::class_<RadialShape>(m, "RadialShape", | |
| 113 | "A star-shaped body's radius along each direction from its centre, on " | ||
| 114 | "a radial ICQ grid of labels (i, j, face) interpolated bilinearly.") | ||
| 115 | 88 | .def(nb::init<std::size_t, std::vector<double>>(), "q"_a, "radii"_a, | |
| 116 | "From 6 (q + 1)^2 label radii in metres, in ICQ file order (face, then j, then i).") | ||
| 117 | 44 | .def_static("sample", &RadialShape::Sample, "q"_a, "radius"_a, | |
| 118 | "From `radius(direction)` in metres, evaluated at every label.") | ||
| 119 | 44 | .def_static("label_direction", &RadialShape::LabelDirection, "q"_a, "i"_a, "j"_a, "face"_a, | |
| 120 | "The unit direction of label (i, j, face), face from 1 to 6.") | ||
| 121 | 44 | .def("label_index", &RadialShape::LabelIndex, "i"_a, "j"_a, "face"_a) | |
| 122 | 44 | .def("at", &RadialShape::At, "direction"_a, "The radius in metres along `direction`.") | |
| 123 | 44 | .def_prop_ro("q", &RadialShape::Q) | |
| 124 | 44 | .def_prop_ro("radii", &RadialShape::Radii) | |
| 125 | 44 | .def_prop_ro("min_radius", &RadialShape::MinRadius) | |
| 126 | 44 | .def_prop_ro("max_radius", &RadialShape::MaxRadius) | |
| 127 | 88 | .def_prop_ro("max_area_per_steradian", &RadialShape::MaxAreaPerSteradian) | |
| 128 | 46 | .def("__eq__", [](const RadialShape& a, const RadialShape& b) { return a == b; }); | |
| 129 | 44 | } | |
| 130 | |||
| 131 | 44 | void BindSurface(nb::module_& m) { | |
| 132 | nb::class_<Surface> surface(m, "Surface", | ||
| 133 | "What a terrain lies on: a plane, with coordinates in metres, or " | ||
| 134 | "a sphere, with coordinates in degrees of longitude and latitude " | ||
| 135 | 44 | "and lengths in metres."); | |
| 136 | ✗ | surface.def(nb::init<>(), "A plane.") | |
| 137 | 44 | .def_static("plane", &Surface::Plane) | |
| 138 | 44 | .def_static("sphere", &Surface::Sphere, "radius"_a, "A sphere of `radius` metres.") | |
| 139 | 44 | .def_static("ellipsoid", &Surface::Ellipsoid, "a"_a, "b"_a, "c"_a, | |
| 140 | "A triaxial ellipsoid with semi-axes `a`, `b` and `c` metres along x, y and z; " | ||
| 141 | "equal radii give a sphere.") | ||
| 142 | 44 | .def_static( | |
| 143 | "shape_model", | ||
| 144 | ✗ | [](const RadialShape& shape) { | |
| 145 | 1 | return Surface::ShapeModel(std::make_shared<const RadialShape>(shape)); | |
| 146 | }, | ||
| 147 | 44 | "shape"_a, "A star-shaped body whose radius by direction is `shape`.") | |
| 148 | 44 | .def_prop_ro( | |
| 149 | "shape", | ||
| 150 | ✗ | [](const Surface& s) -> std::optional<RadialShape> { | |
| 151 | 2 | return s.Shape() ? std::optional<RadialShape>(*s.Shape()) : std::nullopt; | |
| 152 | }, | ||
| 153 | "A copy of the body's shape model; None for a plane, sphere or ellipsoid.") | ||
| 154 | 44 | .def_prop_ro("is_sphere", &Surface::IsSphere) | |
| 155 | 44 | .def_prop_ro("min_radius", &Surface::MinRadius) | |
| 156 | 44 | .def_prop_ro("max_radius", &Surface::MaxRadius) | |
| 157 | 44 | .def_prop_ro("radii", &Surface::Radii, "The semi-axes (a, b, c) in metres.") | |
| 158 | 44 | .def("radius_at", &Surface::RadiusAt, "point"_a, | |
| 159 | "The distance in metres from the centre to the surface at (longitude, latitude).") | ||
| 160 | 44 | .def("area_per_steradian", &Surface::AreaPerSteradian, "point"_a, | |
| 161 | "The surface area per steradian of direction from the centre at a point, in m^2.") | ||
| 162 | 44 | .def_prop_ro("is_body", &Surface::IsBody) | |
| 163 | 44 | .def_prop_ro("radius", &Surface::Radius) | |
| 164 | 88 | .def_prop_ro("unit_length", &Surface::UnitLength, | |
| 165 | "Metres per coordinate unit: 1 on a plane, metres per degree along a " | ||
| 166 | "meridian on a sphere.") | ||
| 167 | 47 | .def("__eq__", [](const Surface& a, const Surface& b) { return a == b; }); | |
| 168 | 44 | BindSurfacePoints(surface); | |
| 169 | 44 | } | |
| 170 | |||
| 171 | 44 | void BindCellIndex(nb::module_& m) { | |
| 172 | ✗ | nb::class_<CellIndex>(m, "CellIndex", "Cell (i, j) of a surface's cells.") | |
| 173 | 44 | .def( | |
| 174 | "__init__", | ||
| 175 | 6 | [](CellIndex* self, std::int32_t i, std::int32_t j) { new (self) CellIndex{i, j}; }, | |
| 176 | 44 | "i"_a, "j"_a) | |
| 177 | 44 | .def_rw("i", &CellIndex::i) | |
| 178 | 44 | .def_rw("j", &CellIndex::j) | |
| 179 | 49 | .def("__eq__", [](const CellIndex& a, const CellIndex& b) { return a == b; }) | |
| 180 | 44 | .def("__repr__", [](const CellIndex& c) { | |
| 181 | ✗ | return "CellIndex(" + std::to_string(c.i) + ", " + std::to_string(c.j) + ")"; | |
| 182 | }); | ||
| 183 | 44 | } | |
| 184 | |||
| 185 | 44 | void BindCells(nb::module_& m) { | |
| 186 | 44 | BindCellIndex(m); | |
| 187 | 44 | nb::class_<SurfaceCells>(m, "SurfaceCells", | |
| 188 | "Cells that tile a surface, which crater and boulder generation draws " | ||
| 189 | "from one cell at a time.") | ||
| 190 | 44 | .def("cells_covering", &SurfaceCells::CellsCovering, "region"_a, "cell_size"_a, | |
| 191 | "The cells of `cell_size` metres that may hold points of `region`.") | ||
| 192 | 44 | .def("cell_area", &SurfaceCells::CellArea, "cell"_a, "cell_size"_a, | |
| 193 | "The cell's area in square metres.") | ||
| 194 | 88 | .def("sample_point", &SurfaceCells::SamplePoint, "u"_a, "v"_a, "cell"_a, "cell_size"_a, | |
| 195 | "The point at (u, v) in [0, 1)^2 of the cell, uniform in area."); | ||
| 196 | 88 | nb::class_<PlanarCells, SurfaceCells>(m, "PlanarCells", "Square cells of a plane.") | |
| 197 | 44 | .def(nb::init<>()); | |
| 198 | ✗ | nb::class_<CubeSphereCells, SurfaceCells>( | |
| 199 | m, "CubeSphereCells", | ||
| 200 | "Equiangular cube-sphere cells of a sphere of `radius` metres, of nearly equal area, or " | ||
| 201 | "of a body `Surface`; on an ellipsoid they are sized by its shortest semi-axis.") | ||
| 202 | 88 | .def(nb::init<double>(), "radius"_a) | |
| 203 | 88 | .def(nb::init<const Surface&>(), "surface"_a) | |
| 204 | 44 | .def( | |
| 205 | "cell_sampling", | ||
| 206 | ✗ | [](const CubeSphereCells& cells, const CellIndex& cell, double cellSize) { | |
| 207 | 3 | const SurfaceCells::Sampling sampling = cells.CellSampling(cell, cellSize); | |
| 208 | 6 | return nb::make_tuple(sampling.area, sampling.bound); | |
| 209 | }, | ||
| 210 | 44 | "cell"_a, "cell_size"_a, | |
| 211 | "(area, bound): draw counts over `area` m^2 and keep a point with chance " | ||
| 212 | "weight(point) / bound; bound 0 keeps every point.") | ||
| 213 | 44 | .def("weight", &CubeSphereCells::Weight, "point"_a, "The area per steradian at a point.") | |
| 214 | 44 | .def("cell_of", &CubeSphereCells::CellOf, "longitude"_a, "latitude"_a, "cell_size"_a, | |
| 215 | "The cell holding a point.") | ||
| 216 | 132 | .def("neighbours", &CubeSphereCells::Neighbours, "cell"_a, "cell_size"_a, "rings"_a = 1, | |
| 217 | "The cells within `rings` of `cell`, across face edges too.") | ||
| 218 | 44 | .def("cells_per_edge", &CubeSphereCells::CellsPerEdge, "cell_size"_a) | |
| 219 | 44 | .def_prop_ro("radius", &CubeSphereCells::Radius) | |
| 220 | 88 | .def_static("direction", &CubeSphereCells::Direction, "face"_a, "alpha"_a, "beta"_a, | |
| 221 | "The unit direction of equiangular coordinates (alpha, beta) on cube face " | ||
| 222 | "`face`."); | ||
| 223 | 44 | } | |
| 224 | |||
| 225 | } // namespace | ||
| 226 | |||
| 227 | 44 | void BindUtil(nb::module_& m) { | |
| 228 | 44 | BindVersion(m); | |
| 229 | 44 | BindLog(m); | |
| 230 | 44 | BindRadialShape(m); | |
| 231 | 44 | BindSurface(m); | |
| 232 | 44 | BindCells(m); | |
| 233 | 44 | m.def("exponential_integral", &ExponentialIntegral, "n"_a, "x"_a, | |
| 234 | "The generalized exponential integral E_n(x), as Golombek and Rapp's model uses."); | ||
| 235 | 44 | } | |
| 236 | |||
| 237 | } // namespace terraingen::python | ||
| 238 |