Skip to content

Commit

Permalink
python: raise exceptions for Math API usage errors.
Browse files Browse the repository at this point in the history
Originally those were assertions that were kept even in release builds,
which meant that calling math.angle() on non-normalized vectors aborted
the whole Python interpreted. Not great. But then the assertions were
made debug-only, which means invalid usage from Python (where the
bindings are usually only built as Release) now silently gives back a
wrong result, which is perhaps even worse.

Because the Python overhead is already massive due to all string lookup
and such, doing one more check in the implementations isn't really going
to slow down anything. Thus I'm mirroring all (debug-only) Magnum
assertions on the Python side, turning them into exceptions. With proper
messages as well, because those are extremely useful.
  • Loading branch information
mosra committed Dec 5, 2023
1 parent daa7862 commit 7a35f40
Show file tree
Hide file tree
Showing 5 changed files with 480 additions and 49 deletions.
140 changes: 140 additions & 0 deletions doc/python/magnum.math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,146 @@
:py:`mat.translation` is a read-write property accessing the fourth column
of the matrix. Similarly for the :ref:`Matrix3` class.

.. py:function:: magnum.Matrix2x2.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix2x2d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3x3.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3x3d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4x4.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4x4d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal

.. py:function:: magnum.Matrix3.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix3d.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix4.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix4d.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix3.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix3d.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix4.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix4d.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix3.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix3d.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix4.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix4d.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix3.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3d.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4d.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3d.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4d.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation
.. py:function:: magnum.Matrix3d.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation
.. py:function:: magnum.Matrix4.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation
.. py:function:: magnum.Matrix4d.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation

.. py:function:: magnum.math.half_angle(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.half_angle(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp_shortest_path(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp_shortest_path(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp_shortest_path(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp_shortest_path(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.Quaternion.rotation(angle: magnum.Rad, normalized_axis: magnum.Vector3)
:raise ValueError: If :p:`normalized_axis` is not normalized
.. py:function:: magnum.Quaterniond.rotation(angle: magnum.Rad, normalized_axis: magnum.Vector3d)
:raise ValueError: If :p:`normalized_axis` is not normalized
.. py:function:: magnum.Quaternion.from_matrix
:raise ValueError: If :p:`matrix` is not a rotation
.. py:function:: magnum.Quaterniond.from_matrix
:raise ValueError: If :p:`matrix` is not a rotation
.. py:function:: magnum.Quaternion.angle
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.angle
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaternion.axis
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.axis
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaternion.inverted_normalized
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.inverted_normalized
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaternion.transform_vector_normalized
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.transform_vector_normalized
:raise ValueError: If the quaternion is not normalized

.. py:function:: magnum.math.angle(normalized_a: magnum.Vector2, normalized_b: magnum.Vector2)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector2d, normalized_b: magnum.Vector2d)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector3, normalized_b: magnum.Vector3)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector3d, normalized_b: magnum.Vector3d)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector4, normalized_b: magnum.Vector4)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector4d, normalized_b: magnum.Vector4d)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.Vector2.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector2d.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector3.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector3d.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector4.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector4d.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized

.. For pickling, because py::pickle() doesn't seem to have a way to set the
__setstate__ / __getstate__ docs directly FFS
Expand Down
90 changes: 71 additions & 19 deletions src/python/magnum/math.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,27 +302,60 @@ template<class T> void quaternion(py::module_& m, py::class_<T>& c) {
.def("dot", static_cast<typename T::Type(*)(const T&, const T&)>(&Math::dot),
"Dot product between two quaternions")
.def("half_angle", [](const T& normalizedA, const T& normalizedB) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
/** @todo switch back to angle() once it's reintroduced with the
correct output again */
return Radd(Math::halfAngle(normalizedA, normalizedB));
}, "Angle between normalized quaternions", py::arg("normalized_a"), py::arg("normalized_b"))
.def("lerp", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::lerp),
"Linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("lerp_shortest_path", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::lerpShortestPath),
"Linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::slerp),
"Spherical linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp_shortest_path", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::slerpShortestPath),
"Spherical linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
;
.def("lerp", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::lerp(normalizedA, normalizedB, t);
}, "Linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("lerp_shortest_path", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::lerpShortestPath(normalizedA, normalizedB, t);
}, "Linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::slerp(normalizedA, normalizedB, t);
}, "Spherical linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp_shortest_path", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::slerpShortestPath(normalizedA, normalizedB, t);
}, "Spherical linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"));

c
/* Constructors */
.def_static("rotation", [](Radd angle, const Math::Vector3<typename T::Type>& axis) {
return T::rotation(Math::Rad<typename T::Type>(angle), axis);
.def_static("rotation", [](Radd angle, const Math::Vector3<typename T::Type>& normalizedAxis) {
if(!normalizedAxis.isNormalized()) {
PyErr_Format(PyExc_ValueError, "axis %S is not normalized", py::cast(normalizedAxis).ptr());
throw py::error_already_set{};
}
return T::rotation(Math::Rad<typename T::Type>(angle), normalizedAxis);
}, "Rotation quaternion", py::arg("angle"), py::arg("normalized_axis"))
.def_static("from_matrix", &T::fromMatrix,
"Create a quaternion from rotation matrix", py::arg("matrix"))
.def_static("from_matrix", [](const Math::Matrix3x3<typename T::Type>& matrix) {
/* Same as the check in fromMatrix() */
if(std::abs(matrix.determinant() - typename T::Type(1)) >= typename T::Type(3)*Math::TypeTraits<typename T::Type>::epsilon()) {
PyErr_Format(PyExc_ValueError, "the matrix is not a rotation:\n%S", py::cast(matrix).ptr());
throw py::error_already_set{};
}
return T::fromMatrix(matrix);
}, "Create a quaternion from rotation matrix", py::arg("matrix"))
.def_static("zero_init", []() {
return T{Math::ZeroInit};
}, "Construct a zero-initialized quaternion")
Expand Down Expand Up @@ -385,10 +418,19 @@ template<class T> void quaternion(py::module_& m, py::class_<T>& c) {
.def("is_normalized", &T::isNormalized,
"Whether the quaternion is normalized")
.def("angle", [](const T& self) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return Radd(self.angle());
}, "Rotation angle of a unit quaternion")
.def("axis", &T::axis,
"Rotation axis of a unit quaternion")
.def("axis", [](const T& self) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.axis();
}, "Rotation axis of a unit quaternion")
.def("to_matrix", &T::toMatrix,
"Convert to a rotation matrix")
.def("dot", &T::dot,
Expand All @@ -401,12 +443,22 @@ template<class T> void quaternion(py::module_& m, py::class_<T>& c) {
"Conjugated quaternion")
.def("inverted", &T::inverted,
"Inverted quaternion")
.def("inverted_normalized", &T::invertedNormalized,
"Inverted normalized quaternion")
.def("inverted_normalized", [](const T& self) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.invertedNormalized();
}, "Inverted normalized quaternion")
.def("transform_vector", &T::transformVector,
"Rotate a vector with a quaternion", py::arg("vector"))
.def("transform_vector_normalized", &T::transformVectorNormalized,
"Rotate a vector with a normalized quaternion", py::arg("vector"))
.def("transform_vector_normalized", [](const T& self, const Math::Vector3<typename T::Type>& vector) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.transformVectorNormalized(vector);
}, "Rotate a vector with a normalized quaternion", py::arg("vector"))

/* Properties */
.def_property("vector",
Expand Down
Loading

0 comments on commit 7a35f40

Please sign in to comment.