diff --git a/doc/python/magnum.math.rst b/doc/python/magnum.math.rst index 803b20a..72f17d4 100644 --- a/doc/python/magnum.math.rst +++ b/doc/python/magnum.math.rst @@ -111,6 +111,70 @@ On the other hand, matrix and vector types are exposed in both the float and double variants. + `Implicit conversions; NumPy compatibility`_ + ============================================ + + All vector classes are implicitly convertible from a tuple of correct size + and type as well as any type implementing the buffer protocol, and these + can be also converted back to lists using list comprehensions. This makes + them fully compatible with `numpy.array`, so the following expressions are + completely valid: + + .. + >>> import numpy as np + + .. code:: pycon + + >>> Matrix4.translation(np.array([1.5, 0.7, 3.3])) + Matrix(1, 0, 0, 1.5, + 0, 1, 0, 0.7, + 0, 0, 1, 3.3, + 0, 0, 0, 1) + + .. code:: pycon + + >>> m = Matrix4.scaling((0.5, 0.5, 1.0)) + >>> np.array(m.diagonal()) + array([0.5, 0.5, 1. , 1. ]) + + For matrices it's a bit more complicated, since Magnum is using + column-major layout while numpy defaults to row-major (but can do + column-major as well). Matrices thus implement the buffer protocol for both + directions of the conversion to give numpy proper metadata and while they + are implicitly convertible from/to types implementing a buffer protocol, + they *are not* implicitly convertible from/to plain tuples like vectors + are. + + To simplify the implementation, Magnum matrices are convertible only from + 32-bit and 64-bit floating-point types (:py:`'f'` and :py:`'d'` numpy + ``dtype``). In the other direction, unless overriden using ``dtype`` or + ``order``, the created numpy array matches Magnum data type and layout: + + .. code:: pycon + + >>> a = Matrix3(np.array( + ... [[1.0, 2.0, 3.0], + ... [4.0, 5.0, 6.0], + ... [7.0, 8.0, 9.0]])) + >>> a[0] # first column + Vector(1, 4, 7) + + .. code:: pycon + + >>> b = np.array(Matrix3.rotation(Deg(45.0))) + >>> b.strides[0] # column-major storage + 4 + >>> b[0] # first column, 32-bit floats + array([ 0.70710677, -0.70710677, 0. ], dtype=float32) + + .. code:: pycon + + >>> c = np.array(Matrix3.rotation(Deg(45.0)), order='C', dtype='d') + >>> c.strides[0] # row-major storage (overriden) + 24 + >>> c[0] # first column, 64-bit floats (overriden) + array([ 0.70710677, -0.70710677, 0. ]) + `Major differences to the C++ API`_ =================================== diff --git a/src/python/magnum/math.matrix.h b/src/python/magnum/math.matrix.h index bc91a39..3db3324 100644 --- a/src/python/magnum/math.matrix.h +++ b/src/python/magnum/math.matrix.h @@ -27,9 +27,12 @@ #include #include +#include #include #include +#include "corrade/PybindExtras.h" + #include "magnum/math.h" namespace magnum { @@ -41,10 +44,42 @@ template struct VectorTraits<2, T> { typedef Math::Vector2 Type; }; template struct VectorTraits<3, T> { typedef Math::Vector3 Type; }; template struct VectorTraits<4, T> { typedef Math::Vector4 Type; }; -/* Called for both Matrix3x3 and Matrix3 in order to return a proper type, so - has to be separate */ +template void initFromBuffer(T& out, const py::buffer_info& info) { + for(std::size_t i = 0; i != T::Cols; ++i) + for(std::size_t j = 0; j != T::Rows; ++j) + out[i][j] = static_cast(*reinterpret_cast(static_cast(info.ptr) + i*info.strides[1] + j*info.strides[0])); +} + +/* Called for both Matrix3x3 and Matrix3 in order to return a proper type / + construct correctly from a numpy array, so has to be separate */ template void everyRectangularMatrix(py::class_& c) { + /* Matrix is implicitly convertible from a buffer, but not from tuples + because there it isn't clear if it's column-major or row-major. */ + py::implicitly_convertible(); + c + /* Buffer protocol, needed in order to make numpy treat the matric + correctly as column-major. Has to be defined *before* the from-tuple + constructor so it gets precedence for types that implement the + buffer protocol. */ + .def(py::init([](py::buffer buffer) { + py::buffer_info info = buffer.request(); + + if(info.ndim != 2) + throw py::buffer_error{Utility::formatString("expected 2 dimensions but got {}", info.ndim)}; + + if(info.shape[0] != T::Rows ||info.shape[1] != T::Cols) + throw py::buffer_error{Utility::formatString("expected {}x{} elements but got {}x{}", T::Cols, T::Rows, info.shape[1], info.shape[0])}; + + T out{Math::NoInit}; + + if(info.format == "f") initFromBuffer(out, info); + else if(info.format == "d") initFromBuffer(out, info); + else throw py::buffer_error{Utility::formatString("expected format f or d but got {}", info.format)}; + + return out; + }), "Construct from a buffer") + /* Operators */ .def(-py::self, "Negated matrix") .def(py::self += py::self, "Add and assign a matrix") @@ -94,6 +129,21 @@ template void rectangularMatrix(py::class_& c) { .def(py::init(), "Default constructor") .def(py::init(), "Construct a matrix with one value for all components") + /* Buffer protocol, needed in order to make numpy treat the matric + correctly as column-major. The constructor is defined in + everyRectangularMatrix(). */ + .def_buffer([](const T& self) -> py::buffer_info { + // TODO: ownership? + return py::buffer_info{ + const_cast(self.data()), + sizeof(typename T::Type), + py::format_descriptor::format(), + 2, + {T::Rows, T::Cols}, + {sizeof(typename T::Type), sizeof(typename T::Type)*T::Rows} + }; + }) + /* Comparison */ .def(py::self == py::self, "Equality comparison") .def(py::self != py::self, "Non-equality comparison") @@ -174,7 +224,16 @@ template void matrices( py::class_, Math::Matrix3x3>& matrix3, py::class_, Math::Matrix4x4>& matrix4 ) { - /* Two-column matrices */ + /* Two-column matrices. Buffer constructors need to be *before* tuple + constructors so numpy buffer protocol gets extracted correctly. */ + everyRectangularMatrix(matrix2x2); + everyRectangularMatrix(matrix2x3); + everyRectangularMatrix(matrix2x4); + rectangularMatrix(matrix2x2); + rectangularMatrix(matrix2x3); + rectangularMatrix(matrix2x4); + everyMatrix(matrix2x2); + matrix(matrix2x2); matrix2x2 .def(py::init&, const Math::Vector2&>(), "Construct from column vectors") @@ -244,16 +303,17 @@ template void matrices( .def("transposed", [](const Math::Matrix2x4& self) -> Math::Matrix4x2 { return self.transposed(); }, "Transposed matrix"); - everyRectangularMatrix(matrix2x2); - everyRectangularMatrix(matrix2x3); - everyRectangularMatrix(matrix2x4); - rectangularMatrix(matrix2x2); - rectangularMatrix(matrix2x3); - rectangularMatrix(matrix2x4); - everyMatrix(matrix2x2); - matrix(matrix2x2); - /* Three-column matrices */ + /* Three-column matrices. Buffer constructors need to be *before* tuple + constructors so numpy buffer protocol gets extracted correctly. */ + everyRectangularMatrix(matrix3x2); + everyRectangularMatrix(matrix3x3); + everyRectangularMatrix(matrix3x4); + rectangularMatrix(matrix3x2); + rectangularMatrix(matrix3x3); + rectangularMatrix(matrix3x4); + everyMatrix(matrix3x3); + matrix(matrix3x3); matrix3x2 .def(py::init&, const Math::Vector2&, const Math::Vector2&>(), "Construct from column vectors") @@ -329,16 +389,17 @@ template void matrices( .def("transposed", [](const Math::Matrix3x4& self) -> Math::Matrix4x3 { return self.transposed(); }, "Transposed matrix"); - everyRectangularMatrix(matrix3x2); - everyRectangularMatrix(matrix3x3); - everyRectangularMatrix(matrix3x4); - rectangularMatrix(matrix3x2); - rectangularMatrix(matrix3x3); - rectangularMatrix(matrix3x4); - everyMatrix(matrix3x3); - matrix(matrix3x3); - /* Four-column matrices */ + /* Four-column matrices. Buffer constructors need to be *before* tuple + constructors so numpy buffer protocol gets extracted correctly. */ + everyRectangularMatrix(matrix4x2); + everyRectangularMatrix(matrix4x3); + everyRectangularMatrix(matrix4x4); + rectangularMatrix(matrix4x2); + rectangularMatrix(matrix4x3); + rectangularMatrix(matrix4x4); + everyMatrix(matrix4x4); + matrix(matrix4x4); matrix4x2 .def(py::init&, const Math::Vector2&, const Math::Vector2&, const Math::Vector2&>(), "Construct from column vectors") @@ -420,18 +481,13 @@ template void matrices( .def("__matmul__", [](const Math::Matrix4x4& self, const Math::Matrix3x4& other) -> Math::Matrix3x4 { return self*other; }, "Multiply a matrix"); - everyRectangularMatrix(matrix4x2); - everyRectangularMatrix(matrix4x3); - everyRectangularMatrix(matrix4x4); - rectangularMatrix(matrix4x2); - rectangularMatrix(matrix4x3); - rectangularMatrix(matrix4x4); - everyMatrix(matrix4x4); - matrix(matrix4x4); - /* 3x3 transformation matrix */ - py::implicitly_convertible, Math::Matrix3>(); + /* 3x3 transformation matrix. Buffer constructors need to be *before* tuple + constructors so numpy buffer protocol gets extracted correctly. */ + py::implicitly_convertible, Math::Matrix3>(); + everyRectangularMatrix(matrix3); + everyMatrix(matrix3); matrix3 /* Constructors. The scaling() / rotation() are handled below as they conflict with member functions. */ @@ -531,12 +587,12 @@ template void matrices( return matrix3.attr("_srotation")(*args, **kwargs); } }); - everyRectangularMatrix(matrix3); - everyMatrix(matrix3); - /* 4x4 transformation matrix */ + /* 4x4 transformation matrix. Buffer constructors need to be *before* tuple + constructors so numpy buffer protocol gets extracted correctly. */ py::implicitly_convertible, Math::Matrix4>(); - + everyRectangularMatrix(matrix4); + everyMatrix(matrix4); matrix4 /* Constructors. The scaling() / rotation() are handled below as they conflict with member functions. */ @@ -661,8 +717,6 @@ template void matrices( return matrix4.attr("_srotation")(*args, **kwargs); } }); - everyRectangularMatrix(matrix4); - everyMatrix(matrix4); } } diff --git a/src/python/magnum/math.matrixdouble.cpp b/src/python/magnum/math.matrixdouble.cpp index cf8e955..173ce98 100644 --- a/src/python/magnum/math.matrixdouble.cpp +++ b/src/python/magnum/math.matrixdouble.cpp @@ -28,20 +28,20 @@ namespace magnum { void mathMatrixDouble(py::module& root) { - py::class_ matrix2x2d{root, "Matrix2x2d", "2x2 double matrix"}; - py::class_ matrix2x3d{root, "Matrix2x3d", "2x3 double matrix"}; - py::class_ matrix2x4d{root, "Matrix2x4d", "2x4 double matrix"}; + py::class_ matrix2x2d{root, "Matrix2x2d", "2x2 double matrix", py::buffer_protocol{}}; + py::class_ matrix2x3d{root, "Matrix2x3d", "2x3 double matrix", py::buffer_protocol{}}; + py::class_ matrix2x4d{root, "Matrix2x4d", "2x4 double matrix", py::buffer_protocol{}}; - py::class_ matrix3x2d{root, "Matrix3x2d", "3x2 double matrix"}; - py::class_ matrix3x3d{root, "Matrix3x3d", "3x3 double matrix"}; - py::class_ matrix3x4d{root, "Matrix3x4d", "3x4 double matrix"}; + py::class_ matrix3x2d{root, "Matrix3x2d", "3x2 double matrix", py::buffer_protocol{}}; + py::class_ matrix3x3d{root, "Matrix3x3d", "3x3 double matrix", py::buffer_protocol{}}; + py::class_ matrix3x4d{root, "Matrix3x4d", "3x4 double matrix", py::buffer_protocol{}}; - py::class_ matrix4x2d{root, "Matrix4x2d", "4x2 double matrix"}; - py::class_ matrix4x3d{root, "Matrix4x3d", "4x3 double matrix"}; - py::class_ matrix4x4d{root, "Matrix4x4d", "4x4 double matrix"}; + py::class_ matrix4x2d{root, "Matrix4x2d", "4x2 double matrix", py::buffer_protocol{}}; + py::class_ matrix4x3d{root, "Matrix4x3d", "4x3 double matrix", py::buffer_protocol{}}; + py::class_ matrix4x4d{root, "Matrix4x4d", "4x4 double matrix", py::buffer_protocol{}}; - py::class_ matrix3d{root, "Matrix3d", "2D double transformation matrix"}; - py::class_ matrix4d{root, "Matrix4d", "3D double transformation matrix"}; + py::class_ matrix3d{root, "Matrix3d", "2D double transformation matrix", py::buffer_protocol{}}; + py::class_ matrix4d{root, "Matrix4d", "3D double transformation matrix", py::buffer_protocol{}}; matrices( matrix2x2d, matrix2x3d, matrix2x4d, diff --git a/src/python/magnum/math.matrixfloat.cpp b/src/python/magnum/math.matrixfloat.cpp index 8a7069c..a453fbe 100644 --- a/src/python/magnum/math.matrixfloat.cpp +++ b/src/python/magnum/math.matrixfloat.cpp @@ -28,20 +28,20 @@ namespace magnum { void mathMatrixFloat(py::module& root) { - py::class_ matrix2x2{root, "Matrix2x2", "2x2 float matrix"}; - py::class_ matrix2x3{root, "Matrix2x3", "2x3 float matrix"}; - py::class_ matrix2x4{root, "Matrix2x4", "2x4 float matrix"}; + py::class_ matrix2x2{root, "Matrix2x2", "2x2 float matrix", py::buffer_protocol{}}; + py::class_ matrix2x3{root, "Matrix2x3", "2x3 float matrix", py::buffer_protocol{}}; + py::class_ matrix2x4{root, "Matrix2x4", "2x4 float matrix", py::buffer_protocol{}}; - py::class_ matrix3x2{root, "Matrix3x2", "3x2 float matrix"}; - py::class_ matrix3x3{root, "Matrix3x3", "3x3 float matrix"}; - py::class_ matrix3x4{root, "Matrix3x4", "3x4 float matrix"}; + py::class_ matrix3x2{root, "Matrix3x2", "3x2 float matrix", py::buffer_protocol{}}; + py::class_ matrix3x3{root, "Matrix3x3", "3x3 float matrix", py::buffer_protocol{}}; + py::class_ matrix3x4{root, "Matrix3x4", "3x4 float matrix", py::buffer_protocol{}}; - py::class_ matrix4x2{root, "Matrix4x2", "4x2 float matrix"}; - py::class_ matrix4x3{root, "Matrix4x3", "4x3 float matrix"}; - py::class_ matrix4x4{root, "Matrix4x4", "4x4 float matrix"}; + py::class_ matrix4x2{root, "Matrix4x2", "4x2 float matrix", py::buffer_protocol{}}; + py::class_ matrix4x3{root, "Matrix4x3", "4x3 float matrix", py::buffer_protocol{}}; + py::class_ matrix4x4{root, "Matrix4x4", "4x4 float matrix", py::buffer_protocol{}}; - py::class_ matrix3{root, "Matrix3", "2D float transformation matrix"}; - py::class_ matrix4{root, "Matrix4", "3D float transformation matrix"}; + py::class_ matrix3{root, "Matrix3", "2D float transformation matrix", py::buffer_protocol{}}; + py::class_ matrix4{root, "Matrix4", "3D float transformation matrix", py::buffer_protocol{}}; matrices( matrix2x2, matrix2x3, matrix2x4, diff --git a/src/python/magnum/math.vector.h b/src/python/magnum/math.vector.h index f4052d9..84d4ee0 100644 --- a/src/python/magnum/math.vector.h +++ b/src/python/magnum/math.vector.h @@ -26,22 +26,93 @@ */ #include +#include #include #include +#include "corrade/PybindExtras.h" + #include "magnum/math.h" namespace magnum { +template bool isTypeCompatible(const std::string&); +template<> inline bool isTypeCompatible(const std::string& format) { + return format == "f" || format == "d"; +} +template<> inline bool isTypeCompatible(const std::string& format) { + return format == "f" || format == "d"; +} +template<> inline bool isTypeCompatible(const std::string& format) { + return format == "i" || format == "l"; +} +template<> inline bool isTypeCompatible(const std::string& format) { + return format == "I" || format == "L"; +} + +template void initFromBuffer(T& out, const py::buffer_info& info) { + for(std::size_t i = 0; i != T::Size; ++i) + out[i] = static_cast(*reinterpret_cast(static_cast(info.ptr) + i*info.strides[0])); +} + +/* Floating-point init */ +template void initFromBuffer(T& out, const py::buffer_info& info, std::true_type, std::true_type) { + if(info.format == "f") initFromBuffer(out, info); + else if(info.format == "d") initFromBuffer(out, info); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +/* Signed integeral init */ +template void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::true_type) { + if(info.format == "i") initFromBuffer(out, info); + else if(info.format == "l") initFromBuffer(out, info); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +/* Unsigned integeral init */ +template void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::false_type) { + if(info.format == "I") initFromBuffer(out, info); + else if(info.format == "L") initFromBuffer(out, info); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + /* Things that have to be defined for both VectorN and Color so they construct / return a proper type */ template void everyVector(py::class_& c) { + /* Implicitly convertible from a buffer (which is a numpy array as well). + Without this implicit conversion from numpy arrays sometimes doesn't + work. */ + py::implicitly_convertible(); + c + /* Constructors */ .def_static("zero_init", []() { return T{Math::ZeroInit}; }, "Construct a zero vector") .def(py::init(), "Default constructor") + /* Buffer protocol. If not present, implicit conversion from numpy + arrays of non-default types somehow doesn't work. On the other hand + only the constructor is needed (and thus also no py::buffer_protocol() + specified for the class), converting vectors to numpy arrays is + doable using the simple iteration iterface. */ + .def(py::init([](py::buffer buffer) { + py::buffer_info info = buffer.request(); + + if(info.ndim != 1) + throw py::buffer_error{Utility::formatString("expected 1 dimension but got {}", info.ndim)}; + + if(info.shape[0] != T::Size) + throw py::buffer_error{Utility::formatString("expected {} elements but got {}", T::Size, info.shape[0])}; + + if(!isTypeCompatible(info.format)) + throw py::buffer_error{Utility::formatString("unexpected format {} for a {} vector", info.format, py::format_descriptor::format())}; + + T out{Math::NoInit}; + initFromBuffer(out, info, std::is_floating_point{}, std::is_signed{}); + return out; + }), "Construct from a buffer") + /* Operators */ .def(-py::self, "Negated vector") .def(py::self += py::self, "Add and assign a vector") diff --git a/src/python/magnum/test/test_math_numpy.py b/src/python/magnum/test/test_math_numpy.py new file mode 100644 index 0000000..a2d5707 --- /dev/null +++ b/src/python/magnum/test/test_math_numpy.py @@ -0,0 +1,338 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import unittest + +from magnum import * +from magnum import math + +import numpy as np + +class Vector(unittest.TestCase): + def test_from_numpy(self): + a = Vector3(np.array([1.0, 2.0, 3.0])) + self.assertEqual(a, Vector3(1.0, 2.0, 3.0)) + + def test_to_numpy(self): + a = np.array(Vector3(1.0, 2.0, 3.0)) + np.testing.assert_array_equal(a, np.array([1.0, 2.0, 3.0])) + + def test_from_numpy_implicit(self): + # This works even w/o buffer protocol + a = Vector4() + a.xyz = np.array([1.0, 2.0, 3.0]) + + b = Matrix4.translation(np.array([1.0, 2.0, 3.0])) + self.assertEqual(b._translation, Vector3(1.0, 2.0, 3.0)) + + def test_from_numpy_implicit_typed(self): + # But this doesn't, works only if buffer protocol is defined + a = Vector4() + a.xyz = np.array([1.0, 2.0, 3.0], dtype='float32') + + a = Matrix4.translation(np.array([1.0, 2.0, 3.0], dtype='float32')) + self.assertEqual(a._translation, Vector3(1.0, 2.0, 3.0)) + + def test_from_numpy_invalid_dimensions(self): + a = np.array([[1, 2], [3, 4]]) + self.assertEqual(a.ndim, 2) + + with self.assertRaisesRegex(BufferError, "expected 1 dimension but got 2"): + b = Vector3i(a) + + def test_from_numpy_invalid_size(self): + a = np.array([1.0, 2.0, 3.0]) + self.assertEqual(a.shape[0], 3) + + with self.assertRaisesRegex(BufferError, "expected 2 elements but got 3"): + b = Vector2(a) + + def test_type_from_numpy(self): + a = Vector3i(np.array([1, 2, -3], dtype='int32')) + self.assertEqual(a, Vector3i(1, 2, -3)) + + a = Vector2ui(np.array([1, 2], dtype='uint32')) + self.assertEqual(a, Vector2ui(1, 2)) + + a = Vector4i(np.array([1, 2, -3, 0], dtype='int64')) + self.assertEqual(a, Vector4i(1, 2, -3, 0)) + + a = Vector3ui(np.array([1, 2, 3333], dtype='uint64')) + self.assertEqual(a, Vector3i(1, 2, 3333)) + + a = Vector2d(np.array([1.0, 2.0], dtype='float32')) + self.assertEqual(a, Vector2d(1.0, 2.0)) + + def test_type_from_numpy_invalid_float(self): + a = np.array([1, 2, 3]) + self.assertEqual(a.dtype, 'int64') + + with self.assertRaisesRegex(BufferError, "unexpected format l for a f vector"): + b = Vector3(a) + + def test_type_from_numpy_invalid_signed(self): + a = np.array([1.0, 2.0, 3.0]) + self.assertEqual(a.dtype, 'float64') + + with self.assertRaisesRegex(BufferError, "unexpected format d for a i vector"): + b = Vector3i(a) + +class Matrix(unittest.TestCase): + def test_from_numpy(self): + a = Matrix2x3(np.array( + [[1.0, 2.0], + [4.0, 5.0], + [7.0, 8.0]])) + self.assertEqual(a, Matrix2x3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0))) + + a = Matrix3x3d(np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + self.assertEqual(a, Matrix3x3d( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + + a = Matrix4x2(np.array( + [[1.0, 2.0, 3.0, 4.0], + [5.0, 6.0, 7.0, 8.0]])) + self.assertEqual(a, Matrix4x2( + (1.0, 5.0), + (2.0, 6.0), + (3.0, 7.0), + (4.0, 8.0))) + + def test_to_numpy(self): + a = np.array(Matrix2x3d( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0))) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0], + [4.0, 5.0], + [7.0, 8.0]])) + + a = np.array(Matrix3x3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + + a = np.array(Matrix4x2( + (1.0, 5.0), + (2.0, 6.0), + (3.0, 7.0), + (4.0, 8.0))) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0, 4.0], + [5.0, 6.0, 7.0, 8.0]])) + + def test_from_numpy_invalid_dimensions(self): + a = np.array([1, 2, 3, 4]) + self.assertEqual(a.ndim, 1) + + with self.assertRaisesRegex(BufferError, "expected 2 dimensions but got 1"): + b = Matrix2x2(a) + + def test_from_numpy_invalid_size(self): + a = np.array([[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0]]) + self.assertEqual(a.shape[0], 2) + self.assertEqual(a.shape[1], 3) + + with self.assertRaisesRegex(BufferError, "expected 2x3 elements but got 3x2"): + b = Matrix2x3(a) + + def test_order_from_numpy(self): + a = np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]]) + self.assertEqual(a.strides[0], 24) + self.assertEqual(a.strides[1], 8) + self.assertEqual(Matrix3x3d(a), Matrix3x3d( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + + a = np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]], order='C') + self.assertEqual(a.strides[0], 24) + self.assertEqual(a.strides[1], 8) + self.assertEqual(Matrix3x3d(a), Matrix3x3d( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + + a = np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]], order='F') + self.assertEqual(a.strides[0], 8) + self.assertEqual(a.strides[1], 24) + self.assertEqual(Matrix3x3d(a), Matrix3x3d( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + + def test_order_to_numpy(self): + a = np.array(Matrix3x3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + self.assertEqual(a.strides[0], 4) + self.assertEqual(a.strides[1], 12) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + + a = np.array(Matrix3x3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0)), order='C') + self.assertEqual(a.strides[0], 12) + self.assertEqual(a.strides[1], 4) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + + a = np.array(Matrix3x3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0)), order='F') + self.assertEqual(a.strides[0], 4) + self.assertEqual(a.strides[1], 12) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + + def test_type_from_numpy(self): + a = np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]], dtype='f') + self.assertEqual(a.dtype, 'f') + self.assertEqual(a.itemsize, 4) + self.assertEqual(Matrix3x3d(a), Matrix3x3d( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + + a = np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]], dtype='d') + self.assertEqual(a.dtype, 'd') + self.assertEqual(a.itemsize, 8) + self.assertEqual(Matrix3x3(a), Matrix3x3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + + def test_type_from_numpy_invalid(self): + a = np.array([[1, 2], [3, 4]]) + self.assertEqual(a.dtype, 'int64') + + with self.assertRaisesRegex(BufferError, "expected format f or d but got l"): + b = Matrix2x2(a) + + def test_type_to_numpy(self): + a = np.array(Matrix3x3d( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0)), dtype='f') + self.assertEqual(a.dtype, 'f') + self.assertEqual(a.itemsize, 4) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + + a = np.array(Matrix3x3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0)), dtype='d') + self.assertEqual(a.dtype, 'd') + self.assertEqual(a.itemsize, 8) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + +class Matrix3_(unittest.TestCase): + def test_from_numpy(self): + a = Matrix3(np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + self.assertEqual(a, Matrix3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + + def test_to_numpy(self): + a = np.array(Matrix3( + (1.0, 4.0, 7.0), + (2.0, 5.0, 8.0), + (3.0, 6.0, 9.0))) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]])) + +class Matrix4_(unittest.TestCase): + def test_from_numpy(self): + a = Matrix4(np.array( + [[1.0, 2.0, 3.0, 4.0], + [5.0, 6.0, 7.0, 8.0], + [9.0, 10.0, 11.0, 12.0], + [13.0, 14.0, 15.0, 16.0]])) + self.assertEqual(a, Matrix4( + (1.0, 5.0, 9.0, 13.0), + (2.0, 6.0, 10.0, 14.0), + (3.0, 7.0, 11.0, 15.0), + (4.0, 8.0, 12.0, 16.0))) + + def test_to_numpy(self): + a = np.array(Matrix4( + (1.0, 5.0, 9.0, 13.0), + (2.0, 6.0, 10.0, 14.0), + (3.0, 7.0, 11.0, 15.0), + (4.0, 8.0, 12.0, 16.0))) + np.testing.assert_array_equal(a, np.array( + [[1.0, 2.0, 3.0, 4.0], + [5.0, 6.0, 7.0, 8.0], + [9.0, 10.0, 11.0, 12.0], + [13.0, 14.0, 15.0, 16.0]])) diff --git a/src/python/magnum/test/test_scenegraph_numpy.py b/src/python/magnum/test/test_scenegraph_numpy.py new file mode 100644 index 0000000..61e1d4d --- /dev/null +++ b/src/python/magnum/test/test_scenegraph_numpy.py @@ -0,0 +1,59 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import numpy as np +import unittest + +from magnum import * +from magnum import scenegraph +from magnum.scenegraph.matrix import Object3D, Scene3D + +class Object(unittest.TestCase): + def test_transformation(self): + scene = Scene3D() + + a = Object3D(scene) + + # like a.rotate_local(Deg(35.0), Vector3.x_axis()), but way uglier, + # another could be scipy.spatial.transform.Rotation but that's meh as + # well + a.transform_local(np.array( + [[1.0, 0.0, 0.0, 0.0], + [0.0, 0.819152, -0.573576, 0.0], + [0.0, 0.573576, 0.819152, 0.0], + [0.0, 0.0, 0.0, 1.0]])) + self.assertEqual(a.transformation, Matrix4.rotation_x(Deg(35.0))) + self.assertEqual(a.absolute_transformation(), Matrix4.rotation_x(Deg(35.0))) + + b = Object3D(a) + b.translate(np.array([3.0, 4.0, 5.0], dtype='float32')) + self.assertEqual(b.transformation, Matrix4.translation((3.0, 4.0, 5.0))) + self.assertEqual(b.absolute_transformation(), + Matrix4.rotation_x(Deg(35.0))@ + Matrix4.translation((3.0, 4.0, 5.0))) + + c = Object3D(scene) + self.assertEqual(c.transformation, Matrix4.identity_init()) + self.assertEqual(c.absolute_transformation(), Matrix4.identity_init())