Browse Source

python: numpy compatibility for matrices and vectors.

pull/2/head
Vladimír Vondruš 7 years ago
parent
commit
82862b8bcd
  1. 64
      doc/python/magnum.math.rst
  2. 128
      src/python/magnum/math.matrix.h
  3. 22
      src/python/magnum/math.matrixdouble.cpp
  4. 22
      src/python/magnum/math.matrixfloat.cpp
  5. 71
      src/python/magnum/math.vector.h
  6. 338
      src/python/magnum/test/test_math_numpy.py
  7. 59
      src/python/magnum/test/test_scenegraph_numpy.py

64
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`_
===================================

128
src/python/magnum/math.matrix.h

@ -27,9 +27,12 @@
#include <pybind11/pybind11.h>
#include <pybind11/operators.h>
#include <Corrade/Utility/FormatStl.h>
#include <Magnum/Math/Matrix3.h>
#include <Magnum/Math/Matrix4.h>
#include "corrade/PybindExtras.h"
#include "magnum/math.h"
namespace magnum {
@ -41,10 +44,42 @@ template<class T> struct VectorTraits<2, T> { typedef Math::Vector2<T> Type; };
template<class T> struct VectorTraits<3, T> { typedef Math::Vector3<T> Type; };
template<class T> struct VectorTraits<4, T> { typedef Math::Vector4<T> Type; };
/* Called for both Matrix3x3 and Matrix3 in order to return a proper type, so
has to be separate */
template<class U, class T> 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<typename T::Type>(*reinterpret_cast<const U*>(static_cast<const char*>(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<class T, class ...Args> void everyRectangularMatrix(py::class_<T, Args...>& 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<py::buffer, T>();
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<Float>(out, info);
else if(info.format == "d") initFromBuffer<Double>(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<class T> void rectangularMatrix(py::class_<T>& c) {
.def(py::init(), "Default constructor")
.def(py::init<typename T::Type>(), "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<typename T::Type*>(self.data()),
sizeof(typename T::Type),
py::format_descriptor<typename T::Type>::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<class T> void matrices(
py::class_<Math::Matrix3<T>, Math::Matrix3x3<T>>& matrix3,
py::class_<Math::Matrix4<T>, Math::Matrix4x4<T>>& 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<T>&, const Math::Vector2<T>&>(),
"Construct from column vectors")
@ -244,16 +303,17 @@ template<class T> void matrices(
.def("transposed", [](const Math::Matrix2x4<T>& self) -> Math::Matrix4x2<T> {
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<T>&, const Math::Vector2<T>&, const Math::Vector2<T>&>(),
"Construct from column vectors")
@ -329,16 +389,17 @@ template<class T> void matrices(
.def("transposed", [](const Math::Matrix3x4<T>& self) -> Math::Matrix4x3<T> {
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<T>&, const Math::Vector2<T>&, const Math::Vector2<T>&, const Math::Vector2<T>&>(),
"Construct from column vectors")
@ -420,18 +481,13 @@ template<class T> void matrices(
.def("__matmul__", [](const Math::Matrix4x4<T>& self, const Math::Matrix3x4<T>& other) -> Math::Matrix3x4<T> {
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::Matrix3x3<T>, Math::Matrix3<T>>();
/* 3x3 transformation matrix. Buffer constructors need to be *before* tuple
constructors so numpy buffer protocol gets extracted correctly. */
py::implicitly_convertible<Math::Matrix3x3<T>, Math::Matrix3<T>>();
everyRectangularMatrix(matrix3);
everyMatrix(matrix3);
matrix3
/* Constructors. The scaling() / rotation() are handled below
as they conflict with member functions. */
@ -531,12 +587,12 @@ template<class T> 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::Matrix4x4<T>, Math::Matrix4<T>>();
everyRectangularMatrix(matrix4);
everyMatrix(matrix4);
matrix4
/* Constructors. The scaling() / rotation() are handled below
as they conflict with member functions. */
@ -661,8 +717,6 @@ template<class T> void matrices(
return matrix4.attr("_srotation")(*args, **kwargs);
}
});
everyRectangularMatrix(matrix4);
everyMatrix(matrix4);
}
}

22
src/python/magnum/math.matrixdouble.cpp

@ -28,20 +28,20 @@
namespace magnum {
void mathMatrixDouble(py::module& root) {
py::class_<Matrix2x2d> matrix2x2d{root, "Matrix2x2d", "2x2 double matrix"};
py::class_<Matrix2x3d> matrix2x3d{root, "Matrix2x3d", "2x3 double matrix"};
py::class_<Matrix2x4d> matrix2x4d{root, "Matrix2x4d", "2x4 double matrix"};
py::class_<Matrix2x2d> matrix2x2d{root, "Matrix2x2d", "2x2 double matrix", py::buffer_protocol{}};
py::class_<Matrix2x3d> matrix2x3d{root, "Matrix2x3d", "2x3 double matrix", py::buffer_protocol{}};
py::class_<Matrix2x4d> matrix2x4d{root, "Matrix2x4d", "2x4 double matrix", py::buffer_protocol{}};
py::class_<Matrix3x2d> matrix3x2d{root, "Matrix3x2d", "3x2 double matrix"};
py::class_<Matrix3x3d> matrix3x3d{root, "Matrix3x3d", "3x3 double matrix"};
py::class_<Matrix3x4d> matrix3x4d{root, "Matrix3x4d", "3x4 double matrix"};
py::class_<Matrix3x2d> matrix3x2d{root, "Matrix3x2d", "3x2 double matrix", py::buffer_protocol{}};
py::class_<Matrix3x3d> matrix3x3d{root, "Matrix3x3d", "3x3 double matrix", py::buffer_protocol{}};
py::class_<Matrix3x4d> matrix3x4d{root, "Matrix3x4d", "3x4 double matrix", py::buffer_protocol{}};
py::class_<Matrix4x2d> matrix4x2d{root, "Matrix4x2d", "4x2 double matrix"};
py::class_<Matrix4x3d> matrix4x3d{root, "Matrix4x3d", "4x3 double matrix"};
py::class_<Matrix4x4d> matrix4x4d{root, "Matrix4x4d", "4x4 double matrix"};
py::class_<Matrix4x2d> matrix4x2d{root, "Matrix4x2d", "4x2 double matrix", py::buffer_protocol{}};
py::class_<Matrix4x3d> matrix4x3d{root, "Matrix4x3d", "4x3 double matrix", py::buffer_protocol{}};
py::class_<Matrix4x4d> matrix4x4d{root, "Matrix4x4d", "4x4 double matrix", py::buffer_protocol{}};
py::class_<Matrix3d, Matrix3x3d> matrix3d{root, "Matrix3d", "2D double transformation matrix"};
py::class_<Matrix4d, Matrix4x4d> matrix4d{root, "Matrix4d", "3D double transformation matrix"};
py::class_<Matrix3d, Matrix3x3d> matrix3d{root, "Matrix3d", "2D double transformation matrix", py::buffer_protocol{}};
py::class_<Matrix4d, Matrix4x4d> matrix4d{root, "Matrix4d", "3D double transformation matrix", py::buffer_protocol{}};
matrices<Double>(
matrix2x2d, matrix2x3d, matrix2x4d,

22
src/python/magnum/math.matrixfloat.cpp

@ -28,20 +28,20 @@
namespace magnum {
void mathMatrixFloat(py::module& root) {
py::class_<Matrix2x2> matrix2x2{root, "Matrix2x2", "2x2 float matrix"};
py::class_<Matrix2x3> matrix2x3{root, "Matrix2x3", "2x3 float matrix"};
py::class_<Matrix2x4> matrix2x4{root, "Matrix2x4", "2x4 float matrix"};
py::class_<Matrix2x2> matrix2x2{root, "Matrix2x2", "2x2 float matrix", py::buffer_protocol{}};
py::class_<Matrix2x3> matrix2x3{root, "Matrix2x3", "2x3 float matrix", py::buffer_protocol{}};
py::class_<Matrix2x4> matrix2x4{root, "Matrix2x4", "2x4 float matrix", py::buffer_protocol{}};
py::class_<Matrix3x2> matrix3x2{root, "Matrix3x2", "3x2 float matrix"};
py::class_<Matrix3x3> matrix3x3{root, "Matrix3x3", "3x3 float matrix"};
py::class_<Matrix3x4> matrix3x4{root, "Matrix3x4", "3x4 float matrix"};
py::class_<Matrix3x2> matrix3x2{root, "Matrix3x2", "3x2 float matrix", py::buffer_protocol{}};
py::class_<Matrix3x3> matrix3x3{root, "Matrix3x3", "3x3 float matrix", py::buffer_protocol{}};
py::class_<Matrix3x4> matrix3x4{root, "Matrix3x4", "3x4 float matrix", py::buffer_protocol{}};
py::class_<Matrix4x2> matrix4x2{root, "Matrix4x2", "4x2 float matrix"};
py::class_<Matrix4x3> matrix4x3{root, "Matrix4x3", "4x3 float matrix"};
py::class_<Matrix4x4> matrix4x4{root, "Matrix4x4", "4x4 float matrix"};
py::class_<Matrix4x2> matrix4x2{root, "Matrix4x2", "4x2 float matrix", py::buffer_protocol{}};
py::class_<Matrix4x3> matrix4x3{root, "Matrix4x3", "4x3 float matrix", py::buffer_protocol{}};
py::class_<Matrix4x4> matrix4x4{root, "Matrix4x4", "4x4 float matrix", py::buffer_protocol{}};
py::class_<Matrix3, Matrix3x3> matrix3{root, "Matrix3", "2D float transformation matrix"};
py::class_<Matrix4, Matrix4x4> matrix4{root, "Matrix4", "3D float transformation matrix"};
py::class_<Matrix3, Matrix3x3> matrix3{root, "Matrix3", "2D float transformation matrix", py::buffer_protocol{}};
py::class_<Matrix4, Matrix4x4> matrix4{root, "Matrix4", "3D float transformation matrix", py::buffer_protocol{}};
matrices<Float>(
matrix2x2, matrix2x3, matrix2x4,

71
src/python/magnum/math.vector.h

@ -26,22 +26,93 @@
*/
#include <pybind11/operators.h>
#include <Corrade/Utility/FormatStl.h>
#include <Magnum/Math/Color.h>
#include <Magnum/Math/Vector4.h>
#include "corrade/PybindExtras.h"
#include "magnum/math.h"
namespace magnum {
template<class> bool isTypeCompatible(const std::string&);
template<> inline bool isTypeCompatible<Float>(const std::string& format) {
return format == "f" || format == "d";
}
template<> inline bool isTypeCompatible<Double>(const std::string& format) {
return format == "f" || format == "d";
}
template<> inline bool isTypeCompatible<Int>(const std::string& format) {
return format == "i" || format == "l";
}
template<> inline bool isTypeCompatible<UnsignedInt>(const std::string& format) {
return format == "I" || format == "L";
}
template<class U, class T> void initFromBuffer(T& out, const py::buffer_info& info) {
for(std::size_t i = 0; i != T::Size; ++i)
out[i] = static_cast<typename T::Type>(*reinterpret_cast<const U*>(static_cast<const char*>(info.ptr) + i*info.strides[0]));
}
/* Floating-point init */
template<class T> void initFromBuffer(T& out, const py::buffer_info& info, std::true_type, std::true_type) {
if(info.format == "f") initFromBuffer<Float>(out, info);
else if(info.format == "d") initFromBuffer<Double>(out, info);
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
}
/* Signed integeral init */
template<class T> void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::true_type) {
if(info.format == "i") initFromBuffer<Int>(out, info);
else if(info.format == "l") initFromBuffer<Long>(out, info);
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
}
/* Unsigned integeral init */
template<class T> void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::false_type) {
if(info.format == "I") initFromBuffer<UnsignedInt>(out, info);
else if(info.format == "L") initFromBuffer<UnsignedLong>(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<class T, class ...Args> void everyVector(py::class_<T, Args...>& 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<py::buffer, T>();
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<typename T::Type>(info.format))
throw py::buffer_error{Utility::formatString("unexpected format {} for a {} vector", info.format, py::format_descriptor<typename T::Type>::format())};
T out{Math::NoInit};
initFromBuffer(out, info, std::is_floating_point<typename T::Type>{}, std::is_signed<typename T::Type>{});
return out;
}), "Construct from a buffer")
/* Operators */
.def(-py::self, "Negated vector")
.def(py::self += py::self, "Add and assign a vector")

338
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š <mosra@centrum.cz>
#
# 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]]))

59
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š <mosra@centrum.cz>
#
# 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())
Loading…
Cancel
Save