From 0120b3f76895efb71f38043638be2f78f31c4ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 2 Feb 2022 15:34:51 +0100 Subject: [PATCH] python: add remaining vector/scalar, exp and other math functions. Except for binomial coefficient, there the asserts are hard to replicate and would need a change on Magnum side. --- doc/python/magnum.math.rst | 4 + doc/python/pages/changelog.rst | 2 + src/python/magnum/math.cpp | 83 +++++++++++++++-- src/python/magnum/math.vector.h | 47 ++++++++++ src/python/magnum/math.vectorfloat.cpp | 29 ++++++ src/python/magnum/math.vectorintegral.cpp | 7 +- src/python/magnum/test/test_math.py | 104 ++++++++++++++++++++++ 7 files changed, 266 insertions(+), 10 deletions(-) diff --git a/doc/python/magnum.math.rst b/doc/python/magnum.math.rst index 060712e..7848709 100644 --- a/doc/python/magnum.math.rst +++ b/doc/python/magnum.math.rst @@ -195,6 +195,10 @@ - :dox:`Math::Matrix3::from()` / :dox:`Math::Matrix4::from()` are named :ref:`Matrix3.from_()` / :ref:`Matrix4.from_()` because :py:`from` is a Python keyword and thus can't be used as a name. + - :dox:`Math::isInf()` and :dox:`Math::isNan()` are named + :ref:`math.isinf() ` and + :ref:`math.isnan() ` for consistency with the + Python :ref:`math` module - :cpp:`Math::gather()` and :cpp:`Math::scatter()` operations are implemented as real swizzles: diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index f033329..394d32e 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -44,6 +44,8 @@ Changelog - Exposed newly added off-center variants of :ref:`Matrix4.orthographic_projection()` and :ref:`Matrix3.projection()` +- Exposed remaining vector/scalar, exponential and other functions in the + :ref:`math ` library - Exposed :ref:`gl.Context` and its platform-specific subclasses for EGL, WGL and GLX - Exposed :ref:`gl.Renderer.set_blend_function()`, diff --git a/src/python/magnum/math.cpp b/src/python/magnum/math.cpp index bf5305f..486f55f 100644 --- a/src/python/magnum/math.cpp +++ b/src/python/magnum/math.cpp @@ -89,7 +89,7 @@ const Py_ssize_t MatrixStridesDouble[][2]{ namespace { -template void angle(py::class_& c) { +template void angle(py::module_& m, py::class_& c) { /* Missing APIs: @@ -153,9 +153,28 @@ template void angle(py::class_& c) { }, "Ratio of two values") .def("__repr__", repr, "Object representation"); + + /* Overloads of scalar functions */ + m + .def("isinf", static_cast(Math::isInf), "If given number is a positive or negative infinity") + .def("isnan", static_cast(Math::isNan), "If given number is a NaN") + .def("min", static_cast(Math::min), "Minimum", py::arg("value"), py::arg("min")) + .def("max", static_cast(Math::max), "Maximum", py::arg("value"), py::arg("min")) + .def("minmax", static_cast(*)(T, T)>(Math::minmax), "Minimum and maximum of two values") + .def("clamp", static_cast(Math::clamp), "Clamp value", py::arg("value"), py::arg("min"), py::arg("max")) + .def("sign", Math::sign, "Sign") + .def("abs", static_cast(Math::abs), "Absolute value") + .def("floor", static_cast(Math::floor), "Nearest not larger integer") + .def("round", static_cast(Math::round), "Round value to nearest integer") + .def("ceil", static_cast(Math::ceil), "Nearest not smaller integer") + .def("fmod", static_cast(Math::fmod), "Floating point division remainder") + .def("lerp", static_cast(Math::lerp), "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("lerp", static_cast(Math::lerp), "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("lerp_inverted", static_cast(Math::lerpInverted), "Inverse linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("lerp")) + .def("select", static_cast(Math::select), "Constant interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")); } -template void boolVector(py::class_& c) { +template void boolVector(py::module_& m, py::class_& c) { c /* Constructors */ .def_static("zero_init", []() { @@ -230,6 +249,9 @@ template void boolVector(py::class_& c) { char lenDocstring[] = "Vector size. Returns _."; lenDocstring[sizeof(lenDocstring) - 3] = '0' + T::Size; c.def_static("__len__", []() { return int(T::Size); }, lenDocstring); + + m + .def("lerp", Math::lerp, "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")); } template void convertible(py::class_& c) { @@ -418,8 +440,8 @@ void math(py::module_& root, py::module_& m) { py::class_ rad{root, "Rad", "Radians"}; deg.def(py::init(), "Conversion from radians"); rad.def(py::init(), "Conversion from degrees"); - angle(deg); - angle(rad); + angle(m, deg); + angle(m, rad); /* Cyclic convertibility, so can't do that in angle() */ py::implicitly_convertible(); @@ -429,9 +451,9 @@ void math(py::module_& root, py::module_& m) { py::class_> boolVector2{root, "BoolVector2", "Two-component bool vector"}; py::class_> boolVector3{root, "BoolVector3", "Three-component bool vector"}; py::class_> boolVector4{root, "BoolVector4", "Four-component bool vector"}; - boolVector(boolVector2); - boolVector(boolVector3); - boolVector(boolVector4); + boolVector(m, boolVector2); + boolVector(m, boolVector3); + boolVector(m, boolVector4); /* Constants. Putting them into math like Python does and as doubles, since Python doesn't really differentiate between 32bit and 64bit floats */ @@ -448,6 +470,12 @@ void math(py::module_& root, py::module_& m) { /* Functions */ m + .def("div", [](Long x, Long y) { return Math::div(x, y); }, "Integer division with remainder", py::arg("x"), py::arg("y")) + /** @todo binomialCoefficient(), asserts are hard to replicate (have an + internal variant returning an Optional?) */ + .def("popcount", static_cast(Math::popcount), "Count of bits set in a number") + + /* Trigonometry */ .def("sin", [](Radd angle) { return Math::sin(angle); }, "Sine") .def("cos", [](Radd angle) { return Math::cos(angle); }, "Cosine") .def("sincos", [](Radd angle) { @@ -456,7 +484,46 @@ void math(py::module_& root, py::module_& m) { .def("tan", [](Radd angle) { return Math::tan(angle); }, "Tangent") .def("asin", [](Double angle) { return Math::asin(angle); }, "Arc sine") .def("acos", [](Double angle) { return Math::acos(angle); }, "Arc cosine") - .def("atan", [](Double angle) { return Math::atan(angle); }, "Arc tangent"); + .def("atan", [](Double angle) { return Math::atan(angle); }, "Arc tangent") + + /* Scalar/vector functions, scalar versions. Vector versions defined + for each vector variant below; angle versions defined above. */ + .def("isinf", static_cast(Math::isInf), "If given number is a positive or negative infinity") + .def("isnan", static_cast(Math::isNan), "If given number is a NaN") + .def("min", static_cast(Math::min), "Minimum", py::arg("value"), py::arg("min")) + .def("min", static_cast(Math::min), "Minimum", py::arg("value"), py::arg("min")) + .def("max", static_cast(Math::max), "Maximum", py::arg("value"), py::arg("min")) + .def("max", static_cast(Math::max), "Maximum", py::arg("value"), py::arg("min")) + .def("minmax", static_cast(*)(Long, Long)>(Math::minmax), "Minimum and maximum of two values") + .def("minmax", static_cast(*)(Double, Double)>(Math::minmax), "Minimum and maximum of two values") + .def("clamp", static_cast(Math::clamp), "Clamp value", py::arg("value"), py::arg("min"), py::arg("max")) + .def("clamp", static_cast(Math::clamp), "Clamp value", py::arg("value"), py::arg("min"), py::arg("max")) + .def("sign", Math::sign, "Sign") + .def("sign", Math::sign, "Sign") + .def("abs", static_cast(Math::abs), "Absolute value") + .def("abs", static_cast(Math::abs), "Absolute value") + .def("floor", static_cast(Math::floor), "Nearest not larger integer") + .def("round", static_cast(Math::round), "Round value to nearest integer") + .def("ceil", static_cast(Math::ceil), "Nearest not smaller integer") + .def("fmod", static_cast(Math::fmod), "Floating point division remainder") + .def("lerp", static_cast(Math::lerp), "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("lerp", static_cast(Math::lerp), "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("lerp", static_cast(Math::lerp), "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("lerp", static_cast(Math::lerp), "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("lerp_inverted", static_cast(Math::lerpInverted), "Inverse linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("lerp")) + .def("select", static_cast(Math::select), "Constant interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("select", static_cast(Math::select), "Constant interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("fma", static_cast(Math::fma), "Fused multiply-add") + + /* Exponential and power. These are not defined for angles as they + require the type to be unitless. */ + .def("log", static_cast(Math::log), "Integral algorithm", py::arg("base"), py::arg("number")) + .def("log2", static_cast(Math::log2), "Base-2 integral algorithm") + .def("log", static_cast(Math::log), "Natural algorithm") + .def("exp", static_cast(Math::exp), "Natural exponential") + .def("pow", static_cast(Math::pow), "Power") + .def("sqrt", static_cast(Math::sqrt), "Square root") + .def("sqrt_inverted", static_cast(Math::sqrtInverted), "Square root"); /* These are needed for the quaternion, so register them before. Double versions are called from inside these. */ diff --git a/src/python/magnum/math.vector.h b/src/python/magnum/math.vector.h index a04db61..ad879f2 100644 --- a/src/python/magnum/math.vector.h +++ b/src/python/magnum/math.vector.h @@ -198,6 +198,42 @@ template void vector(py::module_& m, py::class_& c) { */ m + /* Lambdas in order to convert from the generic Vector */ + .def("min", [](const T& value, const T& min) { + return T{Math::min(value, min)}; + }, "Minimum", py::arg("value"), py::arg("min")) + .def("min", [](const T& value, typename T::Type min) { + return T{Math::min(value, min)}; + }, "Minimum", py::arg("value"), py::arg("min")) + .def("max", [](const T& value, const T& max) { + return T{Math::max(value, max)}; + }, "Maximum", py::arg("value"), py::arg("max")) + .def("max", [](const T& value, typename T::Type max) { + return T{Math::max(value, max)}; + }, "Maximum", py::arg("value"), py::arg("max")) + .def("minmax", [](const T& a, const T& b) { + return std::pair{Math::minmax(a, b)}; + }, "Minimum and maximum of two values") + .def("clamp", [](const T& a, const T& min, const T& max) { + return T{Math::clamp(a, min, max)}; + }, "Clamp value", py::arg("value"), py::arg("min"), py::arg("max")) + .def("clamp", [](const T& a, typename T::Type min, typename T::Type max) { + return T{Math::clamp(a, min, max)}; + }, "Clamp value", py::arg("value"), py::arg("min"), py::arg("max")) + .def("lerp", [](const T& a, const T& b, Double t) { + return T{Math::lerp(a, b, t)}; + }, "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + /* The BoolVector overload has to be before the bool to match first */ + .def("lerp", [](const T& a, const T& b, Math::BoolVector t) { + return T{Math::lerp(a, b, t)}; + }, "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("lerp", [](const T& a, const T& b, bool t) { + return T{Math::lerp(a, b, t)}; + }, "Linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("select", [](const T& a, const T& b, Double t) { + return T{Math::select(a, b, t)}; + }, "Constant interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("dot", [](const T& a, const T& b) { return Math::dot(a, b); }, "Dot product of two vectors"); @@ -338,6 +374,17 @@ template void vector(py::module_& m, py::class_& c) { c.def_static("__len__", []() { return int(T::Size); }, lenDocstring); } +/* Things common for vectors of all sizes and types */ +template void vectorSigned(py::module_& m, py::class_&) { + m + .def("sign", [](const T& a) { + return T{Math::sign(a)}; + }, "Sign") + .def("abs", [](const T& a) { + return T{Math::abs(a)}; + }, "Absolute value"); +} + template void vector2(py::class_>& c) { py::implicitly_convertible&, Math::Vector2>(); diff --git a/src/python/magnum/math.vectorfloat.cpp b/src/python/magnum/math.vectorfloat.cpp index ad4d7be..53b4d3e 100644 --- a/src/python/magnum/math.vectorfloat.cpp +++ b/src/python/magnum/math.vectorfloat.cpp @@ -31,6 +31,32 @@ namespace { template void vectorFloat(py::module_& m, py::class_& c) { m + /* Lambdas in order to convert to/from the generic Vector */ + .def("isinf", [](const T& a) { + return Math::isInf(a); + }, "If given number is a positive or negative infinity") + .def("isnan", [](const T& a) { + return Math::isNan(a); + }, "If given number is a NaN") + .def("floor", [](const T& a) { + return T{Math::floor(a)}; + }, "Nearest not larger integer") + .def("round", [](const T& a) { + return T{Math::round(a)}; + }, "Round value to nearest integer") + .def("ceil", [](const T& a) { + return T{Math::ceil(a)}; + }, "Nearest not smaller integer") + .def("fmod", [](const T& a, const T& b) { + return T{Math::fmod(a, b)}; + }, "Floating point division remainder") + .def("lerp_inverted", [](const T& a, const T& b, const T& t) { + return T{Math::lerpInverted(a, b, t)}; + }, "Inverse linear interpolation of two values", py::arg("a"), py::arg("b"), py::arg("t")) + .def("fma", [](const T& a, const T& b, const T& c) { + return T{Math::fma(a, b, c)}; + }, "Fused multiply-add") + .def("angle", [](const T& a, const T& b) { return Radd(Math::angle(a, b)); }, "Angle between normalized vectors", py::arg("normalized_a"), py::arg("normalized_b")); @@ -60,6 +86,7 @@ template void vectorsFloat(py::module_& m, py::class_> everyVector(vector2_); everyVectorSigned(vector2_); vector>(m, vector2_); + vectorSigned>(m, vector2_); vectorFloat>(m, vector2_); vector2(vector2_); vector2Signed(vector2_); @@ -69,12 +96,14 @@ template void vectorsFloat(py::module_& m, py::class_> everyVector(vector3_); everyVectorSigned(vector3_); vector>(m, vector3_); + vectorSigned>(m, vector3_); vectorFloat>(m, vector3_); vector3(vector3_); everyVector(vector4_); everyVectorSigned(vector4_); vector>(m, vector4_); + vectorSigned>(m, vector4_); vectorFloat>(m, vector4_); vector4(vector4_); } diff --git a/src/python/magnum/math.vectorintegral.cpp b/src/python/magnum/math.vectorintegral.cpp index c1990fd..35f4eb7 100644 --- a/src/python/magnum/math.vectorintegral.cpp +++ b/src/python/magnum/math.vectorintegral.cpp @@ -98,10 +98,13 @@ template void vectorsIntegral(py::module_& m, py::class_(vector4_); } -template void vectorsIntegralSigned(py::class_>& vector2_, py::class_>& vector3_, py::class_>& vector4_) { +template void vectorsIntegralSigned(py::module_& m, py::class_>& vector2_, py::class_>& vector3_, py::class_>& vector4_) { everyVectorSigned(vector2_); everyVectorSigned(vector3_); everyVectorSigned(vector4_); + vectorSigned(m, vector2_); + vectorSigned(m, vector3_); + vectorSigned(m, vector4_); vector2Signed(vector2_); } @@ -138,7 +141,7 @@ void mathVectorIntegral(py::module_& root, py::module_& m) { /* Now register the generic from-list constructors and everything else */ vectorsIntegral(m, vector2i, vector3i, vector4i); - vectorsIntegralSigned(vector2i, vector3i, vector4i); + vectorsIntegralSigned(m, vector2i, vector3i, vector4i); vectorsIntegral(m, vector2ui, vector3ui, vector4ui); } diff --git a/src/python/magnum/test/test_math.py b/src/python/magnum/test/test_math.py index 74c398f..a96f764 100644 --- a/src/python/magnum/test/test_math.py +++ b/src/python/magnum/test/test_math.py @@ -110,6 +110,10 @@ class Constants(unittest.TestCase): class Functions(unittest.TestCase): def test(self): + self.assertEqual(math.div(16, 5), (3, 1)) + self.assertEqual(math.popcount(0xb5d194), 12) + + def test_trigonometry(self): self.assertAlmostEqual(math.sin(Deg(45.0)), 0.7071067811865475) self.assertAlmostEqual(Deg(math.asin(0.7071067811865475)), Deg(45.0)) @@ -117,6 +121,106 @@ class Functions(unittest.TestCase): self.assertAlmostEqual(sincos[0], 1.0) self.assertAlmostEqual(sincos[1], 0.0) + def test_scalar(self): + self.assertFalse(math.isinf(math.nan)) + self.assertFalse(math.isnan(math.inf)) + self.assertTrue(math.isinf(math.inf)) + self.assertTrue(math.isnan(math.nan)) + + self.assertEqual(math.min(15.0, 3.0), 3.0) + self.assertEqual(math.max(15.0, 3.0), 15.0) + self.assertEqual(math.minmax(15.0, 3.0), (3.0, 15.0)) + self.assertEqual(math.clamp(0.5, -1.0, 5.0), 0.5) + + self.assertEqual(math.sign(-15.0), -1.0) + self.assertEqual(math.abs(-15.0), 15.0) + + self.assertEqual(math.floor(15.3), 15.0) + self.assertEqual(math.ceil(15.3), 16.0) + self.assertEqual(math.round(15.3), 15.0) + self.assertEqual(math.round(15.7), 16.0) + self.assertAlmostEqual(math.fmod(5.1, 3.0), 2.1) + + self.assertEqual(math.lerp(2.0, 5.0, 0.5), 3.5) + self.assertEqual(math.lerp(2.0, 5.0, False), 2.0) + self.assertEqual(math.lerp(2, 5, 0.5), 3) + self.assertEqual(math.lerp(2, 5, True), 5) + self.assertEqual(math.lerp_inverted(2.0, 5.0, 3.5), 0.5) + self.assertEqual(math.select(2.0, 5.0, 0.6), 2.0) + self.assertEqual(math.select(2, 5, 1.0), 5) + + self.assertEqual(math.fma(2.0, 3.0, 0.75), 6.75) + + def test_scalar_angle(self): + self.assertFalse(math.isinf(Deg(math.nan))) + self.assertFalse(math.isnan(Rad(math.inf))) + self.assertTrue(math.isinf(Deg(math.inf))) + self.assertTrue(math.isnan(Rad(math.nan))) + + self.assertEqual(math.min(Deg(15.0), Deg(3.0)), Deg(3.0)) + self.assertEqual(math.max(Rad(15.0), Rad(3.0)), Rad(15.0)) + self.assertEqual(math.minmax(Deg(15.0), Deg(3.0)), (Deg(3.0), Deg(15.0))) + self.assertEqual(math.clamp(Rad(0.5), Rad(-1.0), Rad(5.0)), Rad(0.5)) + + self.assertEqual(math.sign(Deg(-15.0)), Deg(-1.0)) + self.assertEqual(math.abs(Rad(-15.0)), Rad(15.0)) + + self.assertEqual(math.floor(Deg(15.3)), Deg(15.0)) + self.assertEqual(math.ceil(Rad(15.3)), Rad(16.0)) + self.assertEqual(math.round(Deg(15.3)), Deg(15.0)) + self.assertEqual(math.round(Rad(15.7)), Rad(16.0)) + self.assertAlmostEqual(math.fmod(Deg(5.1), Deg(3.0)), Deg(2.1)) + + self.assertEqual(math.lerp(Deg(2.0), Deg(5.0), 0.5), Deg(3.5)) + self.assertEqual(math.lerp(Rad(2.0), Rad(5.0), False), Rad(2.0)) + self.assertEqual(math.lerp_inverted(Deg(2.0), Deg(5.0), Deg(3.5)), 0.5) + self.assertEqual(math.select(Rad(2.0), Rad(5.0), 0.6), Rad(2.0)) + + def test_vector(self): + self.assertEqual(math.isinf((math.inf, math.nan)), BoolVector2(0b01)) + self.assertEqual(math.isnan((math.inf, math.nan)), BoolVector2(0b10)) + + self.assertEqual(math.min((15.0, 0.5), (3.0, 1.0)), (3.0, 0.5)) + self.assertEqual(math.min((15.0, 0.5), 3.0), (3.0, 0.5)) + + self.assertEqual(math.max((15.0, 0.5), (3.0, 1.0)), (15.0, 1.0)) + self.assertEqual(math.max((15.0, 0.5), 3.0), (15.0, 3.0)) + + self.assertEqual(math.minmax((15.0, 0.5), (3.0, 1.0)), ((3.0, 0.5), (15.0, 1.0))) + + self.assertEqual(math.clamp((0.5, 3.5), (-1.0, 1.0), (5.0, 2.0)), (0.5, 2.0)) + self.assertEqual(math.clamp((0.5, 3.5), -1.0, 1.0), (0.5, 1.0)) + + self.assertEqual(math.sign((-15.0, 15.0)), (-1.0, 1.0)) + self.assertEqual(math.abs((-15.0, 15.0)), (15.0, 15.0)) + + self.assertEqual(math.floor((15.3, 15.6)), (15.0, 15.0)) + self.assertEqual(math.ceil((15.3, 15.6)), (16.0, 16.0)) + self.assertEqual(math.round((15.3, 15.6)), (15.0, 16.0)) + self.assertEqual(math.fmod((5.1, 1.5), (3.0, 1.0)), (2.1, 0.5)) + + self.assertEqual(math.lerp((2.0, 1.0), (5.0, 2.0), 0.5), (3.5, 1.5)) + self.assertEqual(math.lerp((2.0, 1.0), (5.0, 2.0), BoolVector2(0b01)), (5.0, 1.0)) + self.assertEqual(math.lerp((2.0, 1.0), (5.0, 2.0), False), (2.0, 1.0)) + self.assertEqual(math.lerp((2, 1), (5, 2), 0.5), (3, 1)) + self.assertEqual(math.lerp((2, 1), (5, 2), BoolVector2(0b01)), (5, 1)) + self.assertEqual(math.lerp((2, 1), (5, 2), True), (5, 2)) + self.assertEqual(math.lerp(BoolVector4(0b1001), BoolVector4(0b0110), BoolVector4(0b0101)), BoolVector4(0b1100)) + self.assertEqual(math.lerp_inverted((2.0, 1.0), (5.0, 2.0), (3.5, 1.5)), (0.5, 0.5)) + self.assertEqual(math.select((2.0, 1.0), (5.0, 2.0), 0.6), (2.0, 1.0)) + self.assertEqual(math.select((2, 1), (5, 2), 1.0), (5, 2)) + + self.assertEqual(math.fma((2.0, 1.0), (3.0, 2.0), (0.75, 0.1)), (6.75, 2.1)) + + def test_exponential(self): + self.assertEqual(math.log(2, 256), 8) + self.assertEqual(math.log2(256), 8) + self.assertAlmostEqual(math.log(2.0), 0.69314718) + self.assertAlmostEqual(math.exp(0.69314718), 2.0) + self.assertAlmostEqual(math.pow(2.0, 0.5), 1.414213562) + self.assertAlmostEqual(math.sqrt(2.0), 1.414213562) + self.assertAlmostEqual(math.sqrt_inverted(2.0), 1/1.414213562) + class Vector(unittest.TestCase): def test_init(self): a = Vector4i()