Repository: dfki-ric/pytransform3d (language: python)

Issue:
Increase tolerance threshold for matrix check
Hi, I would like to use your library for my project but I get a value error in the line below.

I am pretty sure that my matrix is correct as it represents the extrinsics of a camera.
When I increase the a-tol value `np.allclose`, it works just fine

![image](https://user-images.githubusercontent.com/6517465/85123514-b0086e80-b228-11ea-965d-e94d51de9868.png)


So my suggestion would be to:
1. change the raise to a warning so that one can keep working without hacking around in the library
2. consider pulling up an a-tol flag to every call of the method. However, this would be a pretty large rafactoring.
3. Alternatively, one could try to find a context manager that switches the precision of `np.allclose`, however, I was not able to find this.

https://github.com/rock-learning/pytransform3d/blob/f4c5c01132df31c0c81962ee3857e670ff026699/pytransform3d/rotations.py#L251

I am happy to hear back on suggestions. Apart from that, I think the library is pretty nice to work with and goes well with http://matthew-brett.github.io/transforms3d/




Discussion / hints:
I added a possible fix here:

https://github.com/theRealSuperMario/pytransform3d/tree/numerical_precision

I could make a PR if desired
Hi,

thanks for the feedback. I would need more information.

* I assume the rotation matrix was not computed in pytransform3d. How did you obtain it?
* Can you tell me the exact values to reproduce the error?
* The second check (for the determinant) was introduced only recently. Did you have problems with that?

I agree that raising an error every time something is slightly incorrect is not the most convenient behavior. Numpy has a global function to define error handling: [numpy.seterr](https://numpy.org/doc/stable/reference/generated/numpy.seterr.html). Maybe pytransform3d should have something similar. In addition, I could raise the error threshold a bit, but I'd like to see the exact numbers first.

> I think the library is pretty nice to work with

Good to hear that!
1. It is from a public dataset (human36M)
2. 
```
0:array([-9.15361835e-01,  4.01808328e-01,  2.57475872e-02, -3.46050839e+02])
1:array([ 5.15480570e-02,  1.80374088e-01, -9.82246499e-01,  5.46980731e+02])
2:array([-3.99318925e-01, -8.97783496e-01, -1.85819250e-01,  5.47448099e+03])
```
Only the [:3,:3] slice is rotation

3. The determinant was not a problem

4. np.seterr does not allow me to suppress the ValueError, does it?
OK, there is the simple solution in #56. I noticed that assert_rotation_matrix uses another threshold for the test (1e-6). I'm OK with that then. Does it work for you?
1e-6 worked for me.

However, I still strongly argue that a functionality to just go over the value error should be implemented. 
I have a different matrix case where I calculate the rotation from matrix from a bunch of cross products and the absolute error is very large. However, it still works sufficiently well for my problem, but the ValueError prevents me from using the library.
Can we merge #56 ? Or do we need more checks?
Just did this. The issue remains open though. We still need a way to deactivate errors.
I don't like global variables and I would like to keep the default behavior of rasing errors for numerically unstable rotation matrices. I implemented something similar to your suggestion (2). See #57. Does it work for you?
Unfortunately not.

I had a look and the entry points for my code are through `check_transform`, which gets called many times within the library

`transform_manager.TransformManager.add_transform`
https://github.com/rock-learning/pytransform3d/blob/b53c8c1e24b88deae1d7c90cafb394da8d26e155/pytransform3d/transform_manager.py#L67

For instance, here the `check_transform` function still calls `check_matrix` without `strict_check`.

When fixing this, you actually have to pass through the `strict_check` flag through many layers, which is kind of ugly. I think there has to be a better way.
I implemented this here : https://github.com/theRealSuperMario/pytransform3d/tree/deactivate_checks

One option would be to put the functions in `transformations.py` into a class (make them methods of that class) and the class takes the `strict_checks` flag during initialization. 



> When fixing this, you actually have to pass through the `strict_check` flag through many layers, which is kind of ugly. I think there has to be a better way.

I did that now.

>  I implemented this here : https://github.com/theRealSuperMario/pytransform3d/tree/deactivate_checks

If you want to be able to override class-wide `strict_check` in each method call, can you make a pull request based on #57? It would need more documentation (docstring updates) and testing though.

> One option would be to put the functions in `transformations.py` into a class (make them methods of that class) and the class takes the `strict_checks` flag during initialization.

That would change the interface significantly and wouldn't be backward compatible.
I think it is fine as it is now. Thanks for helping out.

Fix patch:
diff --git a/README.md b/README.md
index d69ea4206..a89ade869 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,32 @@ Take a look at the
 [installation instructions](https://dfki-ric.github.io/pytransform3d/install.html)
 in the documentation for more details.
 
+## Gallery
+
+The following plots and visualizations have been generated with pytransform3d.
+The code for most examples can be found in
+[the documentation](https://dfki-ric.github.io/pytransform3d/_auto_examples/index.html).
+
+Left: [Nao robot](https://www.softbankrobotics.com/emea/en/nao) with URDF
+from [Bullet3](https://github.com/bulletphysics/bullet3).
+Right: [Kuka iiwa](https://www.kuka.com/en-de/products/robot-systems/industrial-robots/lbr-iiwa).
+The animation is based on pytransform3d's visualization interface to
+[Open3D](http://www.open3d.org/).
+
+<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/animation_nao.gif" height=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/animation_kuka.gif" height=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/animation_dynamics.gif" height=200px/>
+
+Visualizations based on [Open3D](http://www.open3d.org/).
+
+<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/photogrammetry.png" height=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/kuka_trajectories.png" height=200px/>
+
+Various plots based on Matplotlib.
+
+<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/example_plot_box.png" width=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/cylinders.png" width=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/paper/plot_urdf.png" width=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/transform_manager_mesh.png" width=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/accelerate_cylinder.png" width=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/example_plot_screw.png" width=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/rotations_axis_angle.png" width=200px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/concatenate_uncertain_transforms.png" width=200px/>
+
+Transformation editor based on Qt.
+
+<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/paper/app_transformation_editor.png" height=300px/>
+
 ## Example
 
 This is just one simple example. You can find more examples in the subfolder
@@ -66,11 +92,11 @@ from pytransform3d import transformations as pt
 from pytransform3d.transform_manager import TransformManager
 
 
-random_state = np.random.RandomState(0)
+rng = np.random.default_rng(0)
 
 ee2robot = pt.transform_from_pq(
     np.hstack((np.array([0.4, -0.3, 0.5]),
-               pr.random_quaternion(random_state))))
+               pr.random_quaternion(rng))))
 cam2robot = pt.transform_from_pq(
     np.hstack((np.array([0.0, 0.0, 0.8]), pr.q_id)))
 object2cam = pt.transform_from(
@@ -93,32 +119,6 @@ plt.show()
 
 ![output](https://dfki-ric.github.io/pytransform3d/_images/plot_transform_manager.png)
 
-## Gallery
-
-The following plots and visualizations have been generated with pytransform3d.
-The code for most examples can be found in
-[the documentation](https://dfki-ric.github.io/pytransform3d/_auto_examples/index.html).
-
-Left: [Nao robot](https://www.softbankrobotics.com/emea/en/nao) with URDF
-from [Bullet3](https://github.com/bulletphysics/bullet3).
-Right: [Kuka iiwa](https://www.kuka.com/en-de/products/robot-systems/industrial-robots/lbr-iiwa).
-The animation is based on pytransform3d's visualization interface to
-[Open3D](http://www.open3d.org/).
-
-<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/animation_nao.gif" height=400px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/animation_kuka.gif" height=400px/>
-
-Visualizations based on [Open3D](http://www.open3d.org/).
-
-<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/photogrammetry.png" height=300px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/kuka_trajectories.png" height=300px/>
-
-Various plots based on Matplotlib.
-
-<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/example_plot_box.png" width=300px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/cylinders.png" width=300px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/paper/plot_urdf.png" width=300px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/transform_manager_mesh.png" width=300px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/accelerate_cylinder.png" width=300px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/example_plot_screw.png" width=300px/><img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/doc/source/_static/rotations_axis_angle.png" width=300px/>
-
-Transformation editor based on Qt.
-
-<img src="https://raw.githubusercontent.com/dfki-ric/pytransform3d/master/paper/app_transformation_editor.png" height=300px/>
-
 ## Documentation
 
 The API documentation can be found
diff --git a/doc/source/_static/accelerate_cylinder.png b/doc/source/_static/accelerate_cylinder.png
index ff11c8c70..04e21cd3f 100644
Binary files a/doc/source/_static/accelerate_cylinder.png and b/doc/source/_static/accelerate_cylinder.png differ
diff --git a/doc/source/_static/animation_dynamics.gif b/doc/source/_static/animation_dynamics.gif
new file mode 100644
index 000000000..0a1908716
Binary files /dev/null and b/doc/source/_static/animation_dynamics.gif differ
diff --git a/doc/source/_static/concatenate_uncertain_transforms.png b/doc/source/_static/concatenate_uncertain_transforms.png
new file mode 100644
index 000000000..4b0c94e84
Binary files /dev/null and b/doc/source/_static/concatenate_uncertain_transforms.png differ
diff --git a/doc/source/_static/cylinders.png b/doc/source/_static/cylinders.png
index 383be9763..2ecee2250 100644
Binary files a/doc/source/_static/cylinders.png and b/doc/source/_static/cylinders.png differ
diff --git a/doc/source/_static/example_plot_screw.png b/doc/source/_static/example_plot_screw.png
index 665cff84e..78783484c 100644
Binary files a/doc/source/_static/example_plot_screw.png and b/doc/source/_static/example_plot_screw.png differ
diff --git a/doc/source/_static/frame.png b/doc/source/_static/frame.png
new file mode 100644
index 000000000..11270a8b3
Binary files /dev/null and b/doc/source/_static/frame.png differ
diff --git a/doc/source/_static/frame.svg b/doc/source/_static/frame.svg
new file mode 100644
index 000000000..98c80be57
--- /dev/null
+++ b/doc/source/_static/frame.svg
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="12.293253mm"
+   height="18.07617mm"
+   viewBox="0 0 12.293253 18.07617"
+   version="1.1"
+   id="svg1592"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="frame.svg"
+   inkscape:export-filename="/home/afabisch/Projekte/pytransform3d/doc/source/_static/frame.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1594"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="18.358698"
+     inkscape:cx="4.7661331"
+     inkscape:cy="55.232674"
+     inkscape:window-width="1920"
+     inkscape:window-height="1051"
+     inkscape:window-x="1920"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs1589">
+    <marker
+       inkscape:stockid="Arrow2Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow2Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path840"
+         style="fill:#0000ff;fill-opacity:1;fill-rule:evenodd;stroke:#0000ff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow2Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow2Lend-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path840-6"
+         style="fill:#00f200;fill-opacity:1;fill-rule:evenodd;stroke:#00f200;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow2Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow2Lend-3-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path840-6-3"
+         style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:#ff0000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-87.25316,-47.202288)">
+    <g
+       id="g11962"
+       transform="translate(46.186747,11.259867)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path817"
+         d="M 42.333331,46.968756 V 36.385422"
+         style="fill:none;stroke:#0000ff;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Lend)" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path817-7"
+         d="M 42.33333,46.968755 H 52.916665"
+         style="fill:none;stroke:#00f200;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Lend-3)" />
+      <path
+         sodipodi:nodetypes="cc"
+         inkscape:connector-curvature="0"
+         id="path817-7-5"
+         d="m 42.333332,46.968755 1.322916,6.614583"
+         style="fill:none;stroke:#ff0000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Lend-3-5)" />
+    </g>
+    <ellipse
+       style="fill:#505050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316"
+       cx="88.625076"
+       cy="58.273239"
+       rx="1.3569278"
+       ry="1.356928" />
+  </g>
+</svg>
diff --git a/doc/source/_static/position.png b/doc/source/_static/position.png
new file mode 100644
index 000000000..da882a834
Binary files /dev/null and b/doc/source/_static/position.png differ
diff --git a/doc/source/_static/position.svg b/doc/source/_static/position.svg
new file mode 100644
index 000000000..b390b7d52
--- /dev/null
+++ b/doc/source/_static/position.svg
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="10.264915mm"
+   height="11.203955mm"
+   viewBox="0 0 10.264915 11.203955"
+   version="1.1"
+   id="svg1802"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="position.svg"
+   inkscape:export-filename="/home/afabisch/Projekte/pytransform3d/doc/source/_static/position.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1804"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     inkscape:zoom="3.24539"
+     inkscape:cx="13.711757"
+     inkscape:cy="47.451924"
+     inkscape:window-width="1920"
+     inkscape:window-height="1015"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <defs
+     id="defs1799">
+    <marker
+       style="overflow:visible"
+       id="marker3286"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lend"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path2868" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow2Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow2Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path840"
+         style="fill:#0000ff;fill-opacity:1;fill-rule:evenodd;stroke:#0000ff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-117.96802,-78.332327)">
+    <ellipse
+       style="fill:#505050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316"
+       cx="126.87601"
+       cy="79.689255"
+       rx="1.3569278"
+       ry="1.356928" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path817"
+       d="m 118.06678,89.44795 7.82324,-8.746931"
+       style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker3286)"
+       sodipodi:nodetypes="cc" />
+  </g>
+</svg>
diff --git a/doc/source/_static/rotation.png b/doc/source/_static/rotation.png
new file mode 100644
index 000000000..6b8a4e157
Binary files /dev/null and b/doc/source/_static/rotation.png differ
diff --git a/doc/source/_static/rotation.svg b/doc/source/_static/rotation.svg
new file mode 100644
index 000000000..dfa7c2c8c
--- /dev/null
+++ b/doc/source/_static/rotation.svg
@@ -0,0 +1,305 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="19.192951mm"
+   height="21.681004mm"
+   viewBox="0 0 19.192951 21.681004"
+   version="1.1"
+   id="svg878"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="rotation.svg"
+   inkscape:export-filename="/home/afabisch/Projekte/pytransform3d/doc/source/_static/rotation.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview880"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     inkscape:zoom="2.2948373"
+     inkscape:cx="22.441678"
+     inkscape:cy="72.772044"
+     inkscape:window-width="1920"
+     inkscape:window-height="1015"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     showguides="true"
+     inkscape:guide-bbox="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid3756" />
+  </sodipodi:namedview>
+  <defs
+     id="defs875">
+    <marker
+       style="overflow:visible"
+       id="marker3286"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-6"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-62"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-0"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-93" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-67"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-3"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-3-6"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-6-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-3-6-9"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-6-2-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-3-6-9-7"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-6-2-2-5" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-91.560515,-71.29566)">
+    <g
+       id="g1586">
+      <ellipse
+         style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-3"
+         cx="105.26973"
+         cy="87.926491"
+         rx="1.3569278"
+         ry="1.356928" />
+      <ellipse
+         style="fill:#000001;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-0-3"
+         cx="94.208435"
+         cy="83.197372"
+         ry="1.356928"
+         rx="1.3569278" />
+      <ellipse
+         style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3"
+         cx="99.539749"
+         cy="88.192825"
+         rx="1.3569278"
+         ry="1.356928" />
+      <ellipse
+         style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-3-6"
+         cx="102.3406"
+         cy="91.299347"
+         rx="1.3569278"
+         ry="1.356928" />
+      <ellipse
+         style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-3-2"
+         cx="107.78191"
+         cy="91.276962"
+         rx="1.3569278"
+         ry="1.356928" />
+    </g>
+    <g
+       id="g1586-9"
+       transform="rotate(-66.512982,94.211032,83.214418)">
+      <ellipse
+         style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-3-1"
+         cx="105.26973"
+         cy="87.926491"
+         rx="1.3569278"
+         ry="1.356928" />
+      <ellipse
+         style="fill:#000001;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-0-3-2"
+         cx="94.208435"
+         cy="83.197372"
+         ry="1.356928"
+         rx="1.3569278" />
+      <ellipse
+         style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-7"
+         cx="99.539749"
+         cy="88.192825"
+         rx="1.3569278"
+         ry="1.356928" />
+      <ellipse
+         style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-3-6-0"
+         cx="102.3406"
+         cy="91.299347"
+         rx="1.3569278"
+         ry="1.356928" />
+      <ellipse
+         style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path3316-3-3-2-9"
+         cx="107.78191"
+         cy="91.276962"
+         rx="1.3569278"
+         ry="1.356928" />
+    </g>
+    <g
+       id="g3150-7"
+       transform="rotate(25.443823,63.279943,100.69788)"
+       style="stroke:#bf0000;stroke-opacity:1">
+      <path
+         style="fill:none;stroke:#010000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker3286-3)"
+         d="M 89.671108,67.698533 88.530306,66.139939"
+         id="path2716-5"
+         sodipodi:nodetypes="cc" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 90.631293,73.88053 c 1.020329,-3.488277 -0.08366,-5.598115 -2.100987,-7.740591"
+         id="path1078-53"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <g
+       id="g3150-7-6"
+       transform="rotate(25.443823,57.398058,108.38323)"
+       style="stroke:#bf0000;stroke-opacity:1">
+      <path
+         style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 90.62503,73.694089 C 92.733224,68.51324 90.173949,63.936735 87.562474,61.41488"
+         id="path1078-53-8"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <g
+       id="g3150-7-6-0"
+       transform="rotate(2.9834975,-168.99546,355.73784)"
+       style="stroke:#000000;stroke-opacity:1">
+      <path
+         style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 90.89582,74.129629 c 0.973995,-3.006061 1.338132,-8.032777 -3.010145,-12.934968"
+         id="path1078-53-8-3"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <g
+       id="g3150-7-6-0-9"
+       transform="rotate(2.9834975,-236.52358,413.76178)"
+       style="stroke:#bf0000;stroke-opacity:1">
+      <path
+         style="fill:none;stroke:#010000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 90.607695,73.888914 C 92.764988,69.484812 93.649,63.229015 88.898012,56.409878"
+         id="path1078-53-8-3-2"
+         sodipodi:nodetypes="cc" />
+    </g>
+  </g>
+</svg>
diff --git a/doc/source/_static/rotations.png b/doc/source/_static/rotations.png
index 065ff8306..a9dc45333 100644
Binary files a/doc/source/_static/rotations.png and b/doc/source/_static/rotations.png differ
diff --git a/doc/source/_static/rotations.svg b/doc/source/_static/rotations.svg
index db420090a..2417d8c2b 100644
--- a/doc/source/_static/rotations.svg
+++ b/doc/source/_static/rotations.svg
@@ -18,6 +18,20 @@
    xmlns:dc="http://purl.org/dc/elements/1.1/">
   <defs
      id="defs4">
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart"
+       refX="0.0"
+       refY="0.0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="scale(0.8) translate(12.5,0)"
+         style="fill-rule:evenodd;fill:context-stroke;stroke:context-stroke;stroke-width:1.0pt"
+         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+         id="path17458" />
+    </marker>
     <marker
        inkscape:stockid="Arrow1Lend"
        orient="auto"
@@ -83,6 +97,219 @@
          style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
          transform="matrix(-0.8,0,0,-0.8,-10,0)" />
     </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible">
+      <path
+         id="path3927-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-9"
+       style="overflow:visible">
+      <path
+         id="path3927-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-55"
+       style="overflow:visible">
+      <path
+         id="path3927-4"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-6"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path17458-5" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-55-6"
+       style="overflow:visible">
+      <path
+         id="path3927-4-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-6-7"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path17458-5-4" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-55-6-5"
+       style="overflow:visible">
+      <path
+         id="path3927-4-9-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-4"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path17458-7" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-55-4"
+       style="overflow:visible">
+      <path
+         id="path3927-4-4"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-6-7-7"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path17458-5-4-8" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-55-6-5-6"
+       style="overflow:visible">
+      <path
+         id="path3927-4-9-2-8"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-4"
+       style="overflow:visible">
+      <path
+         id="path3927-6-3"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-6-4"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path17458-5-9" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-55-6-2"
+       style="overflow:visible">
+      <path
+         id="path3927-4-9-0"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-8"
+       style="overflow:visible">
+      <path
+         id="path3927-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="marker19736"
+       style="overflow:visible">
+      <path
+         id="path19734"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
   </defs>
   <sodipodi:namedview
      id="base"
@@ -91,9 +318,9 @@
      borderopacity="1.0"
      inkscape:pageopacity="0.0"
      inkscape:pageshadow="2"
-     inkscape:zoom="1.4142136"
-     inkscape:cx="239.70919"
-     inkscape:cy="205.06096"
+     inkscape:zoom="0.7071068"
+     inkscape:cx="794.08089"
+     inkscape:cy="132.93607"
      inkscape:document-units="px"
      inkscape:current-layer="layer1"
      showgrid="false"
@@ -116,7 +343,6 @@
         <dc:format>image/svg+xml</dc:format>
         <dc:type
            rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title />
       </cc:Work>
     </rdf:RDF>
   </metadata>
@@ -125,188 +351,147 @@
      inkscape:groupmode="layer"
      id="layer1"
      transform="translate(0,-652.36218)">
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
-       id="rect3148"
-       width="136.4716"
-       height="74.246208"
-       x="218.5284"
-       y="957.36218" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="268.76895"
-       y="998.7713"
-       id="text2985"><tspan
-         sodipodi:role="line"
-         id="tspan2987"
-         x="268.76895"
-         y="998.7713"
-         style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:'Serif Bold Italic'">R</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="227.26895"
-       y="1019.2714"
-       id="text2989"><tspan
-         sodipodi:role="line"
-         id="tspan2991"
-         x="227.26895"
-         y="1019.2714"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">rotation matrix</tspan></text>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
-       id="rect3142"
-       width="121.62237"
-       height="86.974136"
-       x="30.325901"
-       y="810.73267" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="66.476562"
-       y="857.44226"
-       id="text2985-8"><tspan
-         sodipodi:role="line"
-         id="tspan2987-5"
-         x="66.476562"
-         y="857.44226"
-         style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">θω</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="48.371094"
-       y="878.66882"
-       id="text2989-2"><tspan
-         sodipodi:role="line"
-         id="tspan2991-0"
-         x="48.371094"
-         y="878.66882"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">axis-angle</tspan></text>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.815228"
-       id="rect3144"
-       width="152.73506"
-       height="75.660431"
-       x="410"
-       y="957.36218" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="436.27301"
-       y="995.4386"
-       id="text2985-8-4"><tspan
-         sodipodi:role="line"
-         id="tspan2987-5-7"
-         x="436.27301"
-         y="995.4386"
-         style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">(α,β,γ)</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="438.90192"
-       y="1025.5109"
-       id="text2989-2-4"><tspan
-         sodipodi:role="line"
-         id="tspan2991-0-5"
-         x="438.90192"
-         y="1025.5109"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">Euler angles</tspan></text>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
-       id="rect3146"
-       width="108.89445"
-       height="76.367538"
-       x="231.85132"
-       y="736.48651" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="275.47803"
-       y="775.39703"
-       id="text2985-4"><tspan
-         sodipodi:role="line"
-         id="tspan2987-8"
-         x="275.47803"
-         y="775.39703"
-         style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">q</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="243.87256"
-       y="800.6236"
-       id="text2989-9"><tspan
-         sodipodi:role="line"
-         id="tspan2991-9"
-         x="243.87256"
-         y="800.6236"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">quaternion</tspan></text>
+    <g
+       id="g1080"
+       transform="translate(-56.616744,-196.86047)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
+         id="rect3148-1"
+         width="136.4716"
+         height="74.246208"
+         x="178.54446"
+         y="934.14423" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="228.785"
+         y="975.55328"
+         id="text2985-2"><tspan
+           sodipodi:role="line"
+           id="tspan2987-7"
+           x="228.785"
+           y="975.55328"
+           style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:'Serif Bold Italic'">R</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="187.285"
+         y="996.05341"
+         id="text2989-0"><tspan
+           sodipodi:role="line"
+           id="tspan2991-93"
+           x="187.285"
+           y="996.05341"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">rotation matrix</tspan></text>
+    </g>
+    <g
+       id="g16984"
+       transform="translate(226.12381,62.951498)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.815228"
+         id="rect3144-3"
+         width="152.73506"
+         height="75.660431"
+         x="58.689861"
+         y="898.08179" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="84.962929"
+         y="936.1582"
+         id="text2985-8-4-2"><tspan
+           sodipodi:role="line"
+           id="tspan2987-5-7-0"
+           x="84.962929"
+           y="936.1582"
+           style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">(α,β,γ)</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="87.591835"
+         y="966.23047"
+         id="text2989-2-4-6"><tspan
+           sodipodi:role="line"
+           id="tspan2991-0-5-1"
+           x="87.591835"
+           y="966.23047"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">Euler angles</tspan></text>
+    </g>
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend)"
-       d="m 304.68333,827.70323 0,115.25841"
-       id="path3150"
-       inkscape:connector-curvature="0" />
+       style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:12, 12;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 610,894.36218 H 10 -10"
+       id="path28502"
+       sodipodi:nodetypes="ccc" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.774597;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:9.29514, 9.29514;stroke-dashoffset:0"
+       id="rect28606-7"
+       width="30"
+       height="30"
+       x="169"
+       y="879.36218" />
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend)"
-       d="m 264.68333,945.79007 0,-116.67262"
-       id="path4728"
-       inkscape:connector-curvature="0" />
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-8)"
+       d="M 192.39342,819.78588 V 945.04434"
+       id="path3150-2"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.805502;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:9.666, 9.666;stroke-dashoffset:0"
+       id="rect28606"
+       width="32.441654"
+       height="30"
+       x="111.55834"
+       y="874.36218" />
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
-       d="m 355,1007.3622 h 65"
-       id="path4912"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-8)"
+       d="M 138.39342,947.87274 V 821.2001"
+       id="path4728-6"
        inkscape:connector-curvature="0"
        sodipodi:nodetypes="cc" />
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
-       d="M 425,982.36218 H 355"
-       id="path5096"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart);marker-end:url(#Arrow1Lend-55)"
+       d="M 390.55786,956.67358 V 832.00094"
+       id="path4728-7"
        inkscape:connector-curvature="0"
        sodipodi:nodetypes="cc" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.894428;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:10.7331, 10.7331;stroke-dashoffset:0"
+       id="rect28606-1"
+       width="40"
+       height="30"
+       x="249"
+       y="879.36218" />
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend)"
-       d="M 231.14423,947.20428 161.14066,881.44335"
-       id="path5280"
-       inkscape:connector-curvature="0" />
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-6);marker-end:url(#Arrow1Lend-55-6)"
+       d="m 300.47874,956.87216 -60,-134.67264"
+       id="path4728-7-3"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+    <rect
+       style="fill:#ffffff;stroke-width:1.69933;stroke-linejoin:round"
+       id="rect1351-5-8"
+       width="18.261255"
+       height="26.877016"
+       x="259.83057"
+       y="877.93402" />
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend)"
-       d="m 136.31223,904.61492 72.832,68.58936"
-       id="path5464"
-       inkscape:connector-curvature="0" />
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-6-4);marker-end:url(#Arrow1Lend-55-6-2)"
+       d="M 214.96156,947.3247 356.11478,830.0782"
+       id="path4728-7-3-6"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend)"
-       d="M 88.06602,805.6616 222.41631,776.67023"
-       id="path5648"
-       inkscape:connector-curvature="0" />
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-6-7);marker-end:url(#Arrow1Lend-55-6-5)"
+       d="m 524.55786,904.67358 -72,-72.67264"
+       id="path4728-7-3-5"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
     <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend)"
-       d="m 226.90159,807.19714 -65.76093,16.26345"
-       id="path5832"
-       inkscape:connector-curvature="0" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="728.31073"
-       y="577.03131"
-       id="text6016"
-       transform="rotate(43.92535)"><tspan
-         sodipodi:role="line"
-         id="tspan6018"
-         x="728.31073"
-         y="577.03131"
-         style="font-size:14px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">exponential map</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="730.76025"
-       y="503.71512"
-       id="text6016-5"
-       transform="rotate(43.92535)"><tspan
-         sodipodi:role="line"
-         id="tspan6018-1"
-         x="730.76025"
-         y="503.71512"
-         style="font-size:14px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">logarithmic map</tspan></text>
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-6-7);marker-end:url(#Arrow1Lend-5-4)"
+       d="m 250.24405,781.60342 h 91.00003"
+       id="path5648-2-1"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
     <text
        xml:space="preserve"
        style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
@@ -318,88 +503,250 @@
          x="7.1923866"
          y="703.03064"
          style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'Sans Bold'">pytransform3d.rotations</tspan></text>
+    <g
+       id="g9860"
+       transform="translate(-3.906387,-76.283055)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
+         id="rect3146-2"
+         width="108.89445"
+         height="76.367538"
+         x="342.94678"
+         y="824.18915" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="386.57349"
+         y="863.09967"
+         id="text2985-4-8"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-9"
+           x="386.57349"
+           y="863.09967"
+           style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">q</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="354.96802"
+         y="888.32623"
+         id="text2989-9-7"><tspan
+           sodipodi:role="line"
+           id="tspan2991-9-3"
+           x="354.96802"
+           y="888.32623"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">quaternion</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="388.53827"
+         y="854.88654"
+         id="text2985-4-4-6"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-1-1"
+           x="388.53827"
+           y="854.88654"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
+    </g>
+    <g
+       id="g1013"
+       transform="translate(14.190818,17.389975)" />
+    <g
+       id="g1013-6"
+       transform="translate(54.541674,20.173102)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
+         id="rect3142-3-0"
+         width="121.62237"
+         height="86.974136"
+         x="61.518288"
+         y="933.43713" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="97.668922"
+         y="980.14673"
+         id="text2985-8-67-6"><tspan
+           sodipodi:role="line"
+           id="tspan2987-5-5-2"
+           x="97.668922"
+           y="980.14673"
+           style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">θω</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="79.563454"
+         y="1001.3733"
+         id="text2989-2-3-6"><tspan
+           sodipodi:role="line"
+           id="tspan2991-0-56-1"
+           x="79.563454"
+           y="1001.3733"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">axis-angle</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="122.92387"
+         y="971.95294"
+         id="text2985-4-4-2-2-8"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-1-0-9-7"
+           x="122.92387"
+           y="971.95294"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
+    </g>
+    <g
+       id="g9927"
+       transform="translate(81.744206,-36.576642)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.727599"
+         id="rect3142-5-2"
+         width="80.000008"
+         height="69.999954"
+         x="408.30566"
+         y="787.77496" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="435.15851"
+         y="825.51245"
+         id="text2985-8-6-9"><tspan
+           sodipodi:role="line"
+           id="tspan2987-5-2-3"
+           x="435.15851"
+           y="825.51245"
+           style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">R</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="429.05304"
+         y="846.73907"
+         id="text2989-2-9-1"><tspan
+           sodipodi:role="line"
+           id="tspan2991-0-1-9"
+           x="429.05304"
+           y="846.73907"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">rotor</tspan></text>
+    </g>
+    <g
+       id="g16917"
+       transform="translate(52.178534,47.504385)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.04771"
+         id="rect3142-5-3-5"
+         width="96.685944"
+         height="120.09381"
+         x="422.59015"
+         y="858.18744" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="458.73029"
+         y="889.92493"
+         id="text2985-8-6-6-0"><tspan
+           sodipodi:role="line"
+           id="tspan2987-5-2-7-3"
+           x="458.73029"
+           y="889.92493"
+           style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">ψ</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="471.18463"
+         y="920.57562"
+         id="text2989-2-9-5-6"><tspan
+           sodipodi:role="line"
+           id="tspan2991-0-1-3-1"
+           x="471.18463"
+           y="920.57562"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman';text-align:center;text-anchor:middle">modified</tspan><tspan
+           sodipodi:role="line"
+           x="471.18463"
+           y="945.83832"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman';text-align:center;text-anchor:middle"
+           id="tspan3572-0">Rodrigues</tspan><tspan
+           sodipodi:role="line"
+           x="471.18463"
+           y="970.83832"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman';text-align:center;text-anchor:middle"
+           id="tspan3574-6">parameters</tspan></text>
+    </g>
+    <g
+       id="g16990"
+       transform="translate(-7.114597,-35.582235)">
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="462.168"
+         y="827.13379"
+         id="text2985-8-6-9-4"><tspan
+           sodipodi:role="line"
+           id="tspan2987-5-2-3-7"
+           x="462.168"
+           y="827.13379"
+           style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">=</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;font-size:12px;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="463.4256"
+         y="833.16809"
+         id="text2985-8-6-9-4-8"><tspan
+           sodipodi:role="line"
+           id="tspan2987-5-2-3-7-4"
+           x="463.4256"
+           y="833.16809"
+           style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">in 3D</tspan></text>
+    </g>
     <text
        xml:space="preserve"
        style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="277.44278"
-       y="767.1839"
-       id="text2985-4-4"><tspan
+       x="-950.86066"
+       y="127.13091"
+       id="text2989-0-6"
+       transform="rotate(-90)"><tspan
          sodipodi:role="line"
-         id="tspan2987-8-1"
-         x="277.44278"
-         y="767.1839"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
+         id="tspan2991-93-4"
+         x="-950.86066"
+         y="127.13091"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">exponential map</tspan></text>
     <text
        xml:space="preserve"
        style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="91.731499"
-       y="849.24847"
-       id="text2985-4-4-2"><tspan
+       x="-386.97549"
+       y="863.84692"
+       id="text2989-0-6-0"
+       transform="rotate(-38.639272)"><tspan
          sodipodi:role="line"
-         id="tspan2987-8-1-0"
-         x="91.731499"
-         y="849.24847"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.727599"
-       id="rect3142-5"
-       width="80.000008"
-       height="69.999954"
-       x="344.74576"
-       y="740.48651" />
+         id="tspan2991-93-4-4"
+         x="-386.97549"
+         y="863.84692"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">exp./log. map</tspan></text>
     <text
        xml:space="preserve"
        style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="371.5986"
-       y="778.224"
-       id="text2985-8-6"><tspan
+       x="-949.31812"
+       y="180.11841"
+       id="text2989-0-6-9"
+       transform="rotate(-90)"><tspan
          sodipodi:role="line"
-         id="tspan2987-5-2"
-         x="371.5986"
-         y="778.224"
-         style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:serif;-inkscape-font-specification:'Serif Italic'">R</tspan></text>
+         id="tspan2991-93-4-5"
+         x="-949.31812"
+         y="180.11841"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">logarithmic map</tspan></text>
     <text
        xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="365.49313"
-       y="799.45062"
-       id="text2989-2-9"><tspan
+       style="font-size:16px;line-height:1.25;font-family:sans-serif"
+       x="58.30204"
+       y="990.0816"
+       id="text31521"><tspan
          sodipodi:role="line"
-         id="tspan2991-0-1"
-         x="365.49313"
-         y="799.45062"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">rotor</tspan></text>
-    <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-9)"
-       d="M 381.23938,819.0112 350,952.36218"
-       id="path5464-6"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cc" />
-    <rect
-       style="fill:#ffffff;stroke-width:0.777728;stroke-linejoin:round"
-       id="rect1351"
-       width="9.6257839"
-       height="10.680155"
-       x="363.42297"
-       y="867.15308" />
-    <rect
-       style="fill:#ffffff;stroke-width:0.777728;stroke-linejoin:round"
-       id="rect1351-5"
-       width="9.6257839"
-       height="10.680155"
-       x="366.84915"
-       y="851.82172" />
-    <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-9-3)"
-       d="M 325.23938,821.0112 434.47876,952.36217"
-       id="path5464-6-7"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cc" />
-    <path
-       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-9-3-3)"
-       d="M 449.699,950.55085 340.45962,819.19988"
-       id="path5464-6-7-7"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cc" />
+         x="58.30204"
+         y="990.0816"
+         id="tspan31523"
+         style="text-align:center;text-anchor:middle">minimal</tspan><tspan
+         sodipodi:role="line"
+         x="58.30204"
+         y="1010.0816"
+         id="tspan32799"
+         style="text-align:center;text-anchor:middle">representations</tspan></text>
   </g>
 </svg>
diff --git a/doc/source/_static/rotations_axis_angle.png b/doc/source/_static/rotations_axis_angle.png
index 3a46faf0c..85f95b208 100644
Binary files a/doc/source/_static/rotations_axis_angle.png and b/doc/source/_static/rotations_axis_angle.png differ
diff --git a/doc/source/_static/transform_graph.png b/doc/source/_static/transform_graph.png
new file mode 100644
index 000000000..33772d662
Binary files /dev/null and b/doc/source/_static/transform_graph.png differ
diff --git a/doc/source/_static/transform_graph.svg b/doc/source/_static/transform_graph.svg
new file mode 100644
index 000000000..bb9347113
--- /dev/null
+++ b/doc/source/_static/transform_graph.svg
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="115.74403mm"
+   height="107.4909mm"
+   viewBox="0 0 115.74403 107.4909"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="transform_graph.svg"
+   inkscape:export-filename="/home/afabisch/Projekte/pytransform3d/doc/source/_static/transform_graph.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     inkscape:zoom="0.81134749"
+     inkscape:cx="57.928323"
+     inkscape:cy="225.55071"
+     inkscape:window-width="1419"
+     inkscape:window-height="849"
+     inkscape:window-x="2118"
+     inkscape:window-y="135"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="layer1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <defs
+     id="defs2">
+    <marker
+       style="overflow:visible"
+       id="marker10986"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path10984" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker10896"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path10894" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker9323"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path9321" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker8706"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path8704" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path5966" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lend"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path5969" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lend-5"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path5969-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lend-5-6"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path5969-3-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker10986-1"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path10984-2" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-41.143081,-24.393627)">
+    <circle
+       style="fill:none;fill-opacity:1;stroke:#008900;stroke-width:0.960685;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path846"
+       cx="51.143082"
+       cy="68.232841"
+       r="9.5196581" />
+    <circle
+       style="fill:none;fill-opacity:1;stroke:#008900;stroke-width:0.960685;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path846-3"
+       cx="111.33987"
+       cy="34.393627"
+       r="9.5196581" />
+    <circle
+       style="fill:none;fill-opacity:1;stroke:#008900;stroke-width:0.960685;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path846-6"
+       cx="141.23146"
+       cy="74.011368"
+       r="9.5196581" />
+    <circle
+       style="fill:none;fill-opacity:1;stroke:#008900;stroke-width:0.960685;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path846-7"
+       cx="146.88712"
+       cy="121.88452"
+       r="9.5196581" />
+    <path
+       style="fill:none;stroke:#008900;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart)"
+       d="m 105.79256,41.99216 c 0,0 -5.01363,11.787155 -13.370702,17.437594 -14.754714,9.976053 -31.730229,9.836571 -31.730229,9.836571"
+       id="path5713"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;stroke:#008900;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker8706)"
+       d="m 115.50405,43.136936 c 9.92048,20.053382 17.8365,6.703842 22.91394,21.270123"
+       id="path5713-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;stroke:#008900;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker9323)"
+       d="m 140.7325,83.716512 c 0.95401,16.802188 6.57775,13.832481 5.68797,28.400358"
+       id="path5713-5-9"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;stroke:#890000;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1.6, 1.6;stroke-dashoffset:0;stroke-opacity:1;marker-start:url(#marker10986)"
+       d="M 137.84509,120.49961 C 105.65494,114.56623 59.817011,118.71162 51.240459,78.031709"
+       id="path10529"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;stroke:#890000;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker10986-1)"
+       d="m 149.57301,112.23249 c 2.01065,-10.13144 -2.0392,-11.81414 -4.04051,-20.091587 -1.66882,-6.902251 0.56057,-10.724858 -1.74817,-22.23918 -4.20619,-20.977419 -13.74776,-15.059531 -18.7968,-21.215672 -5.31966,-6.486096 -2.18991,-12.505485 -14.5216,-12.532578 -13.161078,-0.02891 -8.84951,10.638392 -18.724055,17.95803 -10.652203,7.896087 -22.924836,8.529344 -31.371642,10.527103"
+       id="path10529-7"
+       sodipodi:nodetypes="csssssc" />
+  </g>
+</svg>
diff --git a/doc/source/_static/transform_manager_mesh.png b/doc/source/_static/transform_manager_mesh.png
index 1ae35e0af..57a071228 100644
Binary files a/doc/source/_static/transform_manager_mesh.png and b/doc/source/_static/transform_manager_mesh.png differ
diff --git a/doc/source/_static/transformations.png b/doc/source/_static/transformations.png
index 8bf60634f..236fd939d 100644
Binary files a/doc/source/_static/transformations.png and b/doc/source/_static/transformations.png differ
diff --git a/doc/source/_static/transformations.svg b/doc/source/_static/transformations.svg
index ebf4c216e..db67bfd59 100644
--- a/doc/source/_static/transformations.svg
+++ b/doc/source/_static/transformations.svg
@@ -1,23 +1,37 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="592.18671"
-   height="491.49185"
+   width="619.42944"
+   height="491.43866"
    id="svg2"
    version="1.1"
-   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
    sodipodi:docname="transformations.svg"
    inkscape:export-filename="/home/afabisch/Projekte/pytransform3d/doc/source/_static/transformations.png"
    inkscape:export-xdpi="300"
-   inkscape:export-ydpi="300">
+   inkscape:export-ydpi="300"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
   <defs
      id="defs4">
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818" />
+    </marker>
     <marker
        inkscape:stockid="Arrow1Lend"
        orient="auto"
@@ -396,485 +410,901 @@
          style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
          transform="matrix(-0.8,0,0,-0.8,-10,0)" />
     </marker>
-  </defs>
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="1.4142136"
-     inkscape:cx="385.16682"
-     inkscape:cy="260.67349"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1015"
-     inkscape:window-x="0"
-     inkscape:window-y="0"
-     inkscape:window-maximized="1"
-     fit-margin-right="0.8"
-     fit-margin-top="0"
-     fit-margin-left="0"
-     fit-margin-bottom="0"
-     inkscape:document-rotation="0">
-    <inkscape:grid
-       type="xygrid"
-       id="grid1029"
-       originx="-3.8291054"
-       originy="-4.5498845" />
-  </sodipodi:namedview>
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(-3.8291054,-558.04626)">
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.18933"
-       id="rect3148"
-       width="193.04015"
-       height="74.246208"
-       x="337.80026"
-       y="903.70392" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="374.0408"
-       y="943.11304"
-       id="text2985"><tspan
-         sodipodi:role="line"
-         id="tspan2987"
-         x="374.0408"
-         y="943.11304"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">T = </tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="346.5408"
-       y="965.61304"
-       id="text2989"><tspan
-         sodipodi:role="line"
-         id="tspan2991"
-         x="346.5408"
-         y="965.61304"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">transformation matrix</tspan></text>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
-       id="rect3142"
-       width="121.62237"
-       height="86.974136"
-       x="203.96466"
-       y="717.46423" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="218.59564"
-       y="774.08673"
-       id="text2989-2"><tspan
-         sodipodi:role="line"
-         id="tspan2991-0"
-         x="218.59564"
-         y="774.08673"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">exponential</tspan><tspan
-         sodipodi:role="line"
-         x="218.59564"
-         y="799.08673"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'"
-         id="tspan913">coordinates</tspan></text>
-    <g
-       id="g903"
-       transform="rotate(31.927998,188.19775,1110.317)">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-0"
+       style="overflow:visible">
       <path
-         inkscape:connector-curvature="0"
-         id="path5280"
-         d="M 225.14423,829.20428 155.14066,763.44335"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)" />
+         id="path3927-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="marker13664"
+       style="overflow:visible">
       <path
-         inkscape:connector-curvature="0"
-         id="path5464"
-         d="m 130.31223,786.61492 72.832,68.58936"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)" />
-      <text
-         transform="rotate(43.92535)"
-         id="text6016"
-         y="496.20474"
-         x="642.13025"
-         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         xml:space="preserve"><tspan
-           style="font-size:14px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'"
-           y="496.20474"
-           x="642.13025"
-           id="tspan6018"
-           sodipodi:role="line">exponential map</tspan></text>
-      <text
-         transform="rotate(43.92535)"
-         id="text6016-5"
-         y="422.88867"
-         x="644.57977"
-         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         xml:space="preserve"><tspan
-           style="font-size:14px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'"
-           y="422.88867"
-           x="644.57977"
-           id="tspan6018-1"
-           sodipodi:role="line">logarithmic map</tspan></text>
-    </g>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="1.1923866"
-       y="587.03064"
-       id="text6041"><tspan
-         sodipodi:role="line"
-         id="tspan6043"
-         x="1.1923866"
-         y="587.03064"
-         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'Sans Bold'">pytransform3d.transformations</tspan></text>
-    <g
-       id="g1615"
-       transform="translate(15.244816,-107.79656)">
-      <rect
-         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.19794"
-         id="rect3146"
-         width="156.2706"
-         height="76.367538"
-         x="18.329353"
-         y="795.34039" />
-      <text
-         xml:space="preserve"
-         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         x="49.956062"
-         y="834.25092"
-         id="text2985-4"><tspan
-           sodipodi:role="line"
-           id="tspan2987-8"
-           x="49.956062"
-           y="834.25092"
-           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif"><tspan
-   style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
-   id="tspan1076">q</tspan>, <tspan
-   style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
-   id="tspan1078">s</tspan>, h​</tspan></text>
-      <text
-         xml:space="preserve"
-         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         x="30.350594"
-         y="863.47748"
-         id="text2989-9"><tspan
-           sodipodi:role="line"
-           id="tspan2991-9"
-           x="30.350594"
-           y="863.47748"
-           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">screw parameters</tspan></text>
-      <text
-         xml:space="preserve"
-         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         x="89.920815"
-         y="826.03778"
-         id="text2985-4-4"><tspan
-           sodipodi:role="line"
-           id="tspan2987-8-1"
-           x="89.920815"
-           y="826.03778"
-           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
-    </g>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="240.16693"
-       y="752.44568"
-       id="text2985-4-5"><tspan
-         sodipodi:role="line"
-         id="tspan2987-8-6"
-         x="240.16693"
-         y="752.44568"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif"><tspan
-   style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"
-   id="tspan933">S</tspan>θ</tspan></text>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.19794"
-       id="rect3146-6"
-       width="156.2706"
-       height="76.367538"
-       x="203.36816"
-       y="604.7262" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="220.02429"
-       y="643.63672"
-       id="text2985-4-0"><tspan
-         sodipodi:role="line"
-         id="tspan2987-8-10"
-         x="220.02429"
-         y="643.63672"
-         style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-size:40px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'">S<tspan
-   style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
-   id="tspan980"> = [  ]</tspan></tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="245.3894"
-       y="672.86328"
-       id="text2989-9-2"><tspan
-         sodipodi:role="line"
-         id="tspan2991-9-5"
-         x="245.3894"
-         y="672.86328"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">screw axis</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="319.86264"
-       y="628.56677"
-       id="text988"><tspan
-         sodipodi:role="line"
-         id="tspan986"
-         x="319.86264"
-         y="628.56677"
-         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:21.3333px;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Bold';text-align:center;text-anchor:middle">ω</tspan><tspan
-         sodipodi:role="line"
-         x="319.86264"
-         y="649.90009"
-         id="tspan990"
-         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:21.3333px;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Bold';text-align:center;text-anchor:middle">v</tspan></text>
-    <g
-       id="g1175"
-       transform="rotate(142.64184,200.78526,663.27853)">
+         id="path13662"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0"
+       style="overflow:visible">
       <path
-         sodipodi:nodetypes="cc"
          inkscape:connector-curvature="0"
-         id="path5832-4"
-         d="m 236.14867,637.37029 h -55"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4)" />
+         id="path3927-2-4-7-62-4"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
       <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-4"
-         d="m 187.00814,661.12983 h 55"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9)" />
-    </g>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="369.49997"
-       y="741.86218"
-       id="text974" />
-    <g
-       id="g903-8"
-       transform="rotate(-122.18227,358.67987,739.44568)">
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2"
+       style="overflow:visible">
       <path
          inkscape:connector-curvature="0"
-         id="path5280-0"
-         d="M 225.14423,829.20428 155.14066,763.44335"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-6)" />
+         id="path3927-2-4-7-62-4-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-1"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
       <path
-         inkscape:connector-curvature="0"
-         id="path5464-8"
-         d="m 130.31223,786.61492 72.832,68.58936"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-6)" />
-      <text
-         transform="rotate(43.92535)"
-         id="text6016-1"
-         y="500.858"
-         x="649.22577"
-         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         xml:space="preserve"><tspan
-           style="font-size:14px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'"
-           y="500.858"
-           x="649.22577"
-           id="tspan6018-0"
-           sodipodi:role="line">exponential map</tspan></text>
-      <text
-         transform="rotate(43.92535)"
-         id="text6016-5-2"
-         y="416.68433"
-         x="635.11908"
-         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         xml:space="preserve"><tspan
-           style="font-size:14px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'"
-           y="416.68433"
-           x="635.11908"
-           id="tspan6018-1-2"
-           sodipodi:role="line">logarithmic map</tspan></text>
-    </g>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.19794"
-       id="rect3146-6-3-9"
-       width="156.2706"
-       height="76.367538"
-       x="409.36816"
-       y="724.7262" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="452.52429"
-       y="762.63672"
-       id="text2985-4-0-6-7"><tspan
-         sodipodi:role="line"
-         id="tspan2987-8-10-7-5"
-         x="452.52429"
-         y="762.63672"
-         style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-size:40px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"><tspan
-           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
-           id="tspan970-6">[<tspan
-   style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"
-   id="tspan1461">S</tspan>]θ</tspan></tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="419.8894"
-       y="793.36328"
-       id="text2989-9-2-3-4"><tspan
-         sodipodi:role="line"
-         id="tspan2991-9-5-5-6"
-         x="419.8894"
-         y="793.36328"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">matrix logarithm</tspan></text>
-    <g
-       id="g1171"
-       transform="rotate(-90,154.18518,506.90859)">
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-4"
+       style="overflow:visible">
       <path
-         sodipodi:nodetypes="cc"
          inkscape:connector-curvature="0"
-         id="path5832-4-9"
-         d="M -17.55862,650.36217 H -72.55861"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-97)" />
+         id="path3927-2-4-7-62-4-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
       <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-4-1"
-         d="M -74.99999,672.36218 H -20"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-3)" />
-    </g>
-    <g
-       transform="translate(173.93147,1.2234681)"
-       id="g1175-8">
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0"
+       style="overflow:visible">
       <path
-         sodipodi:nodetypes="cc"
          inkscape:connector-curvature="0"
-         id="path5832-4-5"
-         d="m 236.14867,637.37029 h -55"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-7)" />
+         id="path3927-2-4-7-62-4-9-5"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-8"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-3"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-9-3-8"
+       style="overflow:visible">
       <path
-         sodipodi:nodetypes="cc"
          inkscape:connector-curvature="0"
-         id="path5832-4-4-3"
-         d="m 178.70729,659.3703 h 55"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-6)" />
+         id="path3927-2-9-7-0"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-8-1"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-3-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-9-3-8-6"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-9-7-0-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-8-1-9"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-3-9-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-9-3-8-6-3"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-9-7-0-2-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-8-2"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-3-94"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-9-3-8-3"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-9-7-0-5"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-8-2-1"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-3-94-4"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-9-3-8-3-6"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-9-7-0-5-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6-8"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4-9" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1-2"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2-8"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6-86"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4-8" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1-3"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2-83"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6-86-3"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4-8-8" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1-3-0"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2-83-4"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6-86-3-9"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4-8-8-0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1-3-0-6"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2-83-4-8"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6-3"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4-3" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1-7"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2-3"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-65"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-2" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-6"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-5"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-9"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-6" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-0"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-4"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6-8-0"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4-9-4" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1-2-8"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2-8-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="Arrow1Lstart-0-2-5-6-8-2"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Lstart"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(0.8,0,0,0.8,10,0)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path13818-4-2-2-4-9-47" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-4-97-36-0-0-2-0-9-1-2-9"
+       style="overflow:visible">
+      <path
+         inkscape:connector-curvature="0"
+         id="path3927-2-4-7-62-4-9-5-0-2-8-3"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="230.49999"
+     inkscape:cy="190.99999"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1051"
+     inkscape:window-x="1920"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     fit-margin-right="0.8"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-bottom="0"
+     inkscape:document-rotation="0"
+     inkscape:pagecheckerboard="0">
+    <inkscape:grid
+       type="xygrid"
+       id="grid1029"
+       originx="-3.8291054"
+       originy="-4.5498891" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-3.8291054,-558.04626)">
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+       x="1.1923866"
+       y="587.03064"
+       id="text6041"><tspan
+         sodipodi:role="line"
+         id="tspan6043"
+         x="1.1923866"
+         y="587.03064"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'Sans Bold'">pytransform3d.transformations</tspan></text>
+    <g
+       id="g1615-1"
+       transform="translate(439.51921,59.067729)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.19794"
+         id="rect3146-0"
+         width="156.2706"
+         height="76.367538"
+         x="18.329353"
+         y="795.34039" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="49.956062"
+         y="834.25092"
+         id="text2985-4-51"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-108"
+           x="49.956062"
+           y="834.25092"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif"><tspan
+   style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
+   id="tspan1076-5">q</tspan>, <tspan
+   style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
+   id="tspan1078-0">s</tspan>, h​</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="30.350594"
+         y="863.47748"
+         id="text2989-9-6"><tspan
+           sodipodi:role="line"
+           id="tspan2991-9-4"
+           x="30.350594"
+           y="863.47748"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">screw parameters</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="89.920815"
+         y="826.03778"
+         id="text2985-4-4-6"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-1-2"
+           x="89.920815"
+           y="826.03778"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
     </g>
     <g
-       transform="translate(140.93147,101.72347)"
-       id="g1175-1">
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-55"
-         d="m 272.14867,653.37029 h -91"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-94)" />
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-4-4"
-         d="m 178.70729,675.3703 h 91"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-5)" />
+       id="g12190"
+       transform="translate(-73.05894,111.36876)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none"
+         id="rect3142-5"
+         width="121.62237"
+         height="86.974136"
+         x="138.1794"
+         y="851.14203" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="152.81038"
+         y="907.76453"
+         id="text2989-2-8"><tspan
+           sodipodi:role="line"
+           id="tspan2991-0-6"
+           x="152.81038"
+           y="907.76453"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">exponential</tspan><tspan
+           sodipodi:role="line"
+           x="152.81038"
+           y="932.76453"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'"
+           id="tspan913-2">coordinates</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="174.38167"
+         y="886.12347"
+         id="text2985-4-5-8"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-6-4"
+           x="174.38167"
+           y="886.12347"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif"><tspan
+   style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"
+   id="tspan933-7">S</tspan>θ</tspan></text>
     </g>
     <g
-       id="g1171-2"
-       transform="rotate(-90,273.68519,386.4086)">
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-9-2"
-         d="M -17.55862,650.36217 H -72.55861"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-97-3)" />
+       id="g12890"
+       transform="translate(-98.94151,8.24212)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.19794"
+         id="rect3146-6-0"
+         width="156.2706"
+         height="76.367538"
+         x="375.4137"
+         y="958.26868" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="392.06982"
+         y="997.1792"
+         id="text2985-4-0-3"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-10-4"
+           x="392.06982"
+           y="997.1792"
+           style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-size:40px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'">S<tspan
+   style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
+   id="tspan980-0"> = [  ]</tspan></tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="417.43494"
+         y="1026.4058"
+         id="text2989-9-2-39"><tspan
+           sodipodi:role="line"
+           id="tspan2991-9-5-1"
+           x="417.43494"
+           y="1026.4058"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">screw axis</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="491.90814"
+         y="982.10925"
+         id="text988-9"><tspan
+           sodipodi:role="line"
+           id="tspan986-6"
+           x="491.90814"
+           y="982.10925"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:21.3333px;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Bold';text-align:center;text-anchor:middle">ω</tspan><tspan
+           sodipodi:role="line"
+           x="491.90814"
+           y="1003.4426"
+           id="tspan990-9"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:21.3333px;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Bold';text-align:center;text-anchor:middle">v</tspan></text>
+    </g>
+    <g
+       id="g23156-7"
+       transform="translate(204,178)">
       <path
          sodipodi:nodetypes="cc"
          inkscape:connector-curvature="0"
-         id="path5832-4-4-1-7"
-         d="M -74.99999,672.36218 H -20"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-3-5)" />
+         id="path5832-4-9-29-7-8-6-2-8-3-7-9"
+         d="m 74.872552,826.00408 h -94.99999"
+         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-5-6-86-3-9);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-9-1-3-0-6)" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="17.128622"
+         y="863.59247"
+         id="text2985-4-5-2-6-0"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-6-0-8-3"
+           x="17.128622"
+           y="863.59247"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">θ</tspan></text>
     </g>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.30591"
-       id="rect3148-3"
-       width="203.64674"
-       height="84.852806"
-       x="21.030783"
-       y="964.6853" />
     <text
        xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="76.342392"
-       y="1007.1653"
-       id="text2985-6"><tspan
-         sodipodi:role="line"
-         id="tspan2987-7"
-         x="76.342392"
-         y="1007.1653"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">(<tspan
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+       x="369.49997"
+       y="741.86218"
+       id="text974" />
+    <g
+       id="g12381"
+       transform="translate(-334.38003,-188.09318)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.19794"
+         id="rect3146-6-3-9-2"
+         width="156.2706"
+         height="76.367538"
+         x="362.68579"
+         y="980.89612" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="405.84192"
+         y="1018.8066"
+         id="text2985-4-0-6-7-4"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-10-7-5-0"
+           x="405.84192"
+           y="1018.8066"
+           style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-size:40px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"><tspan
+             style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
+             id="tspan970-6-6">[<tspan
+   style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"
+   id="tspan1461-2">S</tspan>]θ</tspan></tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="373.20703"
+         y="1049.5332"
+         id="text2989-9-2-3-4-9"><tspan
+           sodipodi:role="line"
+           id="tspan2991-9-5-5-6-9"
+           x="373.20703"
+           y="1049.5332"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">matrix logarithm</tspan></text>
+    </g>
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path5832-4-9-29-7-8-6-2-8"
+       d="m 110.21297,869.60383 v 94.99999"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-5-6);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-9-1)" />
+    <g
+       id="g11734"
+       transform="translate(-33.461968,-113.70422)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.30591"
+         id="rect3148-3-8"
+         width="203.64674"
+         height="84.852806"
+         x="286.67181"
+         y="812.60468" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="341.9834"
+         y="855.08472"
+         id="text2985-6-1"><tspan
+           sodipodi:role="line"
+           id="tspan2987-7-2"
+           x="341.9834"
+           y="855.08472"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">(<tspan
    style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
-   id="tspan1118">p</tspan>, <tspan
+   id="tspan1118-8">p</tspan>, <tspan
    style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
-   id="tspan1116">q</tspan>)</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="29.77132"
-       y="1039.2009"
-       id="text2989-5"><tspan
-         sodipodi:role="line"
-         id="tspan2991-3"
-         x="29.77132"
-         y="1039.2009"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">position and quaternion</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="133.62192"
-       y="995.27039"
-       id="text2985-4-4-5"><tspan
-         sodipodi:role="line"
-         id="tspan2987-8-1-6"
-         x="133.62192"
-         y="995.27039"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
-    <g
-       transform="rotate(-16.531901,1367.7886,557.06666)"
-       id="g1175-8-7">
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-5-0"
-         d="m 263.87816,636.55406 h -110"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-7-2)" />
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-4-3-9"
-         d="m 153.87816,659.55406 h 110"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-6-1)" />
+   id="tspan1116-9">q</tspan>)</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="295.41235"
+         y="887.12036"
+         id="text2989-5-3"><tspan
+           sodipodi:role="line"
+           id="tspan2991-3-68"
+           x="295.41235"
+           y="887.12036"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">position and quaternion</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="399.26294"
+         y="843.18976"
+         id="text2985-4-4-5-0"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-1-6-2"
+           x="399.26294"
+           y="843.18976"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif">^</tspan></text>
     </g>
     <g
-       id="g1962"
-       transform="translate(0,-22)">
+       id="g1962-3"
+       transform="translate(-133.16658,183.41705)">
       <rect
          style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.19794"
-         id="rect3146-6-3"
+         id="rect3146-6-3-3"
          width="156.2706"
          height="76.367538"
          x="409.63876"
@@ -884,32 +1314,32 @@
          style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
          x="408.29489"
          y="664.50427"
-         id="text2985-4-0-6"><tspan
+         id="text2985-4-0-6-8"><tspan
            sodipodi:role="line"
-           id="tspan2987-8-10-7"
+           id="tspan2987-8-10-7-0"
            x="408.29489"
            y="664.50427"
            style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-size:40px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"><tspan
              style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
-             id="tspan970">[<tspan
+             id="tspan970-5">[<tspan
    style="font-style:oblique;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium Oblique'"
-   id="tspan1111">S</tspan>]=</tspan></tspan></text>
+   id="tspan1111-6">S</tspan>]=</tspan></tspan></text>
       <text
          xml:space="preserve"
          style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         x="420.16"
+         x="436.16"
          y="695.73083"
-         id="text2989-9-2-3"><tspan
+         id="text2989-9-2-3-6"><tspan
            sodipodi:role="line"
-           id="tspan2991-9-5-5"
-           x="420.16"
+           id="tspan2991-9-5-5-4"
+           x="436.16"
            y="695.73083"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">screw matrix</tspan></text>
       <g
-         id="g1330"
+         id="g1330-0"
          transform="matrix(0.53065276,0,0,0.53065276,162.67269,259.05422)">
         <text
-           id="text1114"
+           id="text1114-0"
            y="720.32416"
            x="630.44916"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
@@ -917,29 +1347,29 @@
              style="text-align:center;text-anchor:middle"
              y="720.32416"
              x="630.44916"
-             id="tspan1112"
+             id="tspan1112-4"
              sodipodi:role="line">0</tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1121"
+             id="tspan1121-6"
              y="738.99084"
              x="630.44916"
              sodipodi:role="line">ω<tspan
-   id="tspan1131"
+   id="tspan1131-2"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">3</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1119"
+             id="tspan1119-6"
              y="757.65753"
              x="630.44916"
              sodipodi:role="line">-ω<tspan
-   id="tspan1135"
+   id="tspan1135-7"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">2</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1125"
+             id="tspan1125-5"
              y="776.32428"
              x="630.44916"
              sodipodi:role="line">0</tspan></text>
         <text
-           id="text1114-3"
+           id="text1114-3-6"
            y="720.32385"
            x="664.53442"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
@@ -947,29 +1377,29 @@
              style="text-align:center;text-anchor:middle"
              y="720.32385"
              x="664.53442"
-             id="tspan1112-6"
+             id="tspan1112-6-9"
              sodipodi:role="line">-ω<tspan
-   id="tspan1127-7"
+   id="tspan1127-7-8"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">3</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1121-5"
+             id="tspan1121-5-7"
              y="738.99054"
              x="664.53442"
              sodipodi:role="line">0</tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1119-1"
+             id="tspan1119-1-2"
              y="757.65723"
              x="664.53442"
              sodipodi:role="line">ω<tspan
-   id="tspan1137-7"
+   id="tspan1137-7-8"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">1</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1125-9"
+             id="tspan1125-9-29"
              y="776.32397"
              x="664.53442"
              sodipodi:role="line">0</tspan></text>
         <text
-           id="text1114-3-3"
+           id="text1114-3-3-9"
            y="720.32385"
            x="698.78564"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
@@ -977,29 +1407,29 @@
              style="text-align:center;text-anchor:middle"
              y="720.32385"
              x="698.78564"
-             id="tspan1112-6-6"
+             id="tspan1112-6-6-6"
              sodipodi:role="line">ω<tspan
-   id="tspan1129-5-6"
+   id="tspan1129-5-6-0"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">2</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1121-5-6"
+             id="tspan1121-5-6-2"
              y="738.99054"
              x="698.78564"
              sodipodi:role="line">-ω<tspan
-   id="tspan1133-2-1"
+   id="tspan1133-2-1-7"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">1</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1119-1-7"
+             id="tspan1119-1-7-6"
              y="757.65723"
              x="698.78564"
              sodipodi:role="line">0</tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1125-9-0"
+             id="tspan1125-9-0-1"
              y="776.32397"
              x="698.78564"
              sodipodi:role="line">0</tspan></text>
         <text
-           id="text2985-4-0-6-9"
+           id="text2985-4-0-6-9-3"
            y="761.92218"
            x="594.60004"
            style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
@@ -1007,10 +1437,10 @@
              style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:74.6667px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
              y="761.92218"
              x="594.60004"
-             id="tspan2987-8-10-7-3"
+             id="tspan2987-8-10-7-3-2"
              sodipodi:role="line">[     ]</tspan></text>
         <text
-           id="text1114-3-2"
+           id="text1114-3-2-1"
            y="720.32385"
            x="728.72064"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
@@ -1018,225 +1448,276 @@
              style="text-align:center;text-anchor:middle"
              y="720.32385"
              x="728.72064"
-             id="tspan1112-6-3"
+             id="tspan1112-6-3-5"
              sodipodi:role="line">v<tspan
-   id="tspan1143-3-9"
+   id="tspan1143-3-9-9"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">1</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1121-5-2"
+             id="tspan1121-5-2-9"
              y="738.99054"
              x="728.72064"
              sodipodi:role="line">v<tspan
-   id="tspan1139-9-89"
+   id="tspan1139-9-89-1"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">2</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1119-1-73"
+             id="tspan1119-1-73-4"
              y="757.65723"
              x="728.72064"
              sodipodi:role="line">v<tspan
-   id="tspan1141-0-1"
+   id="tspan1141-0-1-9"
    style="font-size:65%;text-align:center;baseline-shift:sub;text-anchor:middle">3</tspan></tspan><tspan
              style="text-align:center;text-anchor:middle"
-             id="tspan1125-9-2"
+             id="tspan1125-9-2-1"
              y="776.32397"
              x="728.72064"
              sodipodi:role="line">0</tspan></text>
       </g>
     </g>
     <g
-       id="g1330-4"
-       transform="matrix(0.53065276,0,0,0.53065276,107.39322,535.03435)">
+       id="g2868"
+       transform="translate(-4.9983751,-53.961647)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.18933"
+         id="rect3148-0"
+         width="193.04015"
+         height="74.246208"
+         x="45.309032"
+         y="675.92316" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="81.549576"
+         y="715.33228"
+         id="text2985-48"><tspan
+           sodipodi:role="line"
+           id="tspan2987-88"
+           x="81.549576"
+           y="715.33228"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif"><tspan
+   style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
+   id="tspan4086">T</tspan> = </tspan></text>
       <text
-         id="text2985-4-0-6-9-6"
-         y="763.33557"
-         x="625.22253"
+         xml:space="preserve"
          style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-         xml:space="preserve"><tspan
-           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:74.6667px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
+         x="54.049572"
+         y="737.83228"
+         id="text2989-97"><tspan
+           sodipodi:role="line"
+           id="tspan2991-7"
+           x="54.049572"
+           y="737.83228"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">transformation matrix</tspan></text>
+      <g
+         id="g1330-4-6"
+         transform="matrix(0.53065276,0,0,0.53065276,-185.098,307.25359)">
+        <text
+           id="text2985-4-0-6-9-6-4"
            y="763.33557"
            x="625.22253"
-           id="tspan2987-8-10-7-3-9"
-           sodipodi:role="line">[   ]</tspan></text>
-      <text
-         id="text1114-3-1-4"
-         y="775.64752"
-         x="713.07532"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
-         xml:space="preserve"><tspan
-           style="text-align:center;text-anchor:middle;stroke-width:1"
-           id="tspan1125-9-6-4"
+           style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+           xml:space="preserve"><tspan
+             style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:74.6667px;line-height:1.25;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Medium'"
+             y="763.33557"
+             x="625.22253"
+             id="tspan2987-8-10-7-3-9-3"
+             sodipodi:role="line">[   ]</tspan></text>
+        <text
+           id="text1114-3-1-4-0"
            y="775.64752"
            x="713.07532"
-           sodipodi:role="line">1</tspan></text>
-      <text
-         id="text1114-3-1-3"
-         y="775.64838"
-         x="683.3949"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
-         xml:space="preserve"><tspan
-           style="text-align:center;text-anchor:middle;stroke-width:1"
-           id="tspan1125-9-6-0"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
+           xml:space="preserve"><tspan
+             style="text-align:center;text-anchor:middle;stroke-width:1"
+             id="tspan1125-9-6-4-3"
+             y="775.64752"
+             x="713.07532"
+             sodipodi:role="line">1</tspan></text>
+        <text
+           id="text1114-3-1-3-0"
            y="775.64838"
            x="683.3949"
-           sodipodi:role="line">0</tspan></text>
-      <text
-         id="text1114-3-1-3-8"
-         y="775.64874"
-         x="669.02576"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
-         xml:space="preserve"><tspan
-           style="text-align:center;text-anchor:middle;stroke-width:1"
-           id="tspan1125-9-6-0-4"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
+           xml:space="preserve"><tspan
+             style="text-align:center;text-anchor:middle;stroke-width:1"
+             id="tspan1125-9-6-0-92"
+             y="775.64838"
+             x="683.3949"
+             sodipodi:role="line">0</tspan></text>
+        <text
+           id="text1114-3-1-3-8-5"
            y="775.64874"
            x="669.02576"
-           sodipodi:role="line">0</tspan></text>
-      <text
-         id="text1114-3-1-3-4"
-         y="775.6488"
-         x="654.42102"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
-         xml:space="preserve"><tspan
-           style="text-align:center;text-anchor:middle;stroke-width:1"
-           id="tspan1125-9-6-0-9"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
+           xml:space="preserve"><tspan
+             style="text-align:center;text-anchor:middle;stroke-width:1"
+             id="tspan1125-9-6-0-4-4"
+             y="775.64874"
+             x="669.02576"
+             sodipodi:role="line">0</tspan></text>
+        <text
+           id="text1114-3-1-3-4-0"
            y="775.6488"
            x="654.42102"
-           sodipodi:role="line">0</tspan></text>
-      <text
-         xml:space="preserve"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:35.1768px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.88447"
-         x="650.40729"
-         y="750.21918"
-         id="text1540"><tspan
-           sodipodi:role="line"
-           id="tspan1538"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
+           xml:space="preserve"><tspan
+             style="text-align:center;text-anchor:middle;stroke-width:1"
+             id="tspan1125-9-6-0-9-5"
+             y="775.6488"
+             x="654.42102"
+             sodipodi:role="line">0</tspan></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:35.1768px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.88447"
            x="650.40729"
            y="750.21918"
-           style="font-size:55.2778px;stroke-width:1.88447">R</tspan></text>
-      <text
-         id="text1114-3-1-4-2"
-         y="749.26532"
-         x="715.19537"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
-         xml:space="preserve"><tspan
-           style="font-size:55.2778px;text-align:center;text-anchor:middle;stroke-width:1"
-           id="tspan1125-9-6-4-0"
+           id="text1540-9"><tspan
+             sodipodi:role="line"
+             id="tspan1538-4"
+             x="650.40729"
+             y="750.21918"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:55.2778px;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Bold';stroke-width:1.88447">R</tspan></text>
+        <text
+           id="text1114-3-1-4-2-6"
            y="749.26532"
            x="715.19537"
-           sodipodi:role="line">t</tspan></text>
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1"
+           xml:space="preserve"><tspan
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:55.2778px;font-family:'CMU Bright';-inkscape-font-specification:'CMU Bright Bold';text-align:center;text-anchor:middle;stroke-width:1"
+             id="tspan1125-9-6-4-0-9"
+             y="749.26532"
+             x="715.19537"
+             sodipodi:role="line">t</tspan></text>
+      </g>
     </g>
-    <rect
-       style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.10677"
-       id="rect3148-3-3"
-       width="146.27301"
-       height="84.852806"
-       x="71.02063"
-       y="817.43591" />
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="93.701157"
-       y="859.91602"
-       id="text2985-6-6"><tspan
-         sodipodi:role="line"
-         id="tspan2987-7-7"
-         x="93.701157"
-         y="859.91602"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif"><tspan
+    <g
+       id="g11635"
+       transform="translate(108.50095,-60.637697)">
+      <rect
+         style="opacity:0.3;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:1.10677"
+         id="rect3148-3-3-2"
+         width="146.27301"
+         height="84.852806"
+         x="367.68457"
+         y="668.35492" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="390.36505"
+         y="710.83502"
+         id="text2985-6-6-2"><tspan
+           sodipodi:role="line"
+           id="tspan2987-7-7-4"
+           x="390.36505"
+           y="710.83502"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif"><tspan
    style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
-   id="tspan1118-5">p +</tspan> ε<tspan
+   id="tspan1118-5-7">p +</tspan> ε<tspan
    style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Serif;-inkscape-font-specification:'Serif Bold'"
-   id="tspan1116-3">q</tspan></tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="81.130081"
-       y="891.95166"
-       id="text2989-5-5"><tspan
-         sodipodi:role="line"
-         id="tspan2991-3-6"
-         x="81.130081"
-         y="891.95166"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">dual quaternion</tspan></text>
-    <g
-       id="g1171-6"
-       transform="rotate(-90,184.81788,702.12126)">
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-9-29"
-         d="M -17.55862,650.36217 H -72.55861"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-97-36)" />
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-4-1-1"
-         d="M -74.99999,672.36218 H -20"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-3-53)" />
+   id="tspan1116-3-7">q</tspan></tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="377.79401"
+         y="742.87067"
+         id="text2989-5-5-5"><tspan
+           sodipodi:role="line"
+           id="tspan2991-3-6-4"
+           x="377.79401"
+           y="742.87067"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:1.25;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman'">dual quaternion</tspan></text>
     </g>
     <g
-       transform="rotate(19.390559,-518.08796,980.65501)"
-       id="g1175-8-7-3">
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-5-0-5"
-         d="m 263.87816,636.55406 h -110"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-7-2-3)" />
+       id="g23156"
+       transform="translate(204,20)">
       <path
          sodipodi:nodetypes="cc"
          inkscape:connector-curvature="0"
-         id="path5832-4-4-3-9-6"
-         d="m 153.87816,659.55406 h 110"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-6-1-7)" />
+         id="path5832-4-9-29-7-8-6-2-8-3-7"
+         d="m 74.872552,826.00408 h -94.99999"
+         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-5-6-86-3);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-9-1-3-0)" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="17.128622"
+         y="863.59247"
+         id="text2985-4-5-2-6"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-6-0-8"
+           x="17.128622"
+           y="863.59247"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">θ</tspan></text>
     </g>
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path5832-4-9-29-7-8"
+       d="M 240,643.49638 H 466"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-1);marker-end:url(#Arrow1Lend-4-97-36-0-0-4)" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path5832-4-9-29-7-8-6"
+       d="M 424.62046,730.4675 480,673.49638"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0)" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path5832-4-9-29-7-8-6-1"
+       d="m 429.49699,986.35548 55.37954,-56.97112"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-9);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-0)" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path5832-4-9-29-7-8-6-2"
+       d="m 230,673.49638 54.62046,56.97112"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-5);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-9)" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path5832-4-9-29-7-8-6-2-8-8"
+       d="m 142.21297,697.60383 v 94.99999"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-5-6-8);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-9-1-2)" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path5832-4-9-29-7-8-6-2-8-8-9"
+       d="m 350.49072,885.48126 v 88.99999"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-5-6-8-2);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-9-1-2-9)" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart);marker-end:url(#Arrow1Lend)"
+       d="m 68.38623,984.94157 c 0,0 -53.729194,-67.08505 -52.934534,-151.08775 C 16.210786,753.61062 48.3872,696.4638 48.3872,696.4638"
+       id="path17272"
+       sodipodi:nodetypes="csc" />
     <g
-       id="g1171-6-8"
-       transform="rotate(-90,113.06315,630.83467)">
+       id="g25101">
       <path
          sodipodi:nodetypes="cc"
          inkscape:connector-curvature="0"
-         id="path5832-4-9-29-7"
-         d="M -17.55862,650.36217 H -72.55861"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-97-36-0)" />
-      <path
-         sodipodi:nodetypes="cc"
-         inkscape:connector-curvature="0"
-         id="path5832-4-4-1-1-9"
-         d="M -74.99999,672.36218 H -20"
-         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-4-9-3-53-6)" />
+         id="path5832-4-9-29-7-8-6-2-8-8-0"
+         d="M 540.63768,696.03082 V 853.03081"
+         style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow1Lstart-0-2-5-6-8-0);marker-end:url(#Arrow1Lend-4-97-36-0-0-2-0-9-1-2-8)" />
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+         x="548.27344"
+         y="787.1048"
+         id="text2985-4-5-2-6-8"><tspan
+           sodipodi:role="line"
+           id="tspan2987-8-6-0-8-6"
+           x="548.27344"
+           y="787.1048"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">θ</tspan></text>
     </g>
     <text
        xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="102.20217"
-       y="801.94458"
-       id="text2985-4-5-2"><tspan
-         sodipodi:role="line"
-         id="tspan2987-8-6-0"
-         x="102.20217"
-         y="801.94458"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">θ</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="267.25903"
-       y="713.05792"
-       id="text2985-4-5-2-3"><tspan
-         sodipodi:role="line"
-         id="tspan2987-8-6-0-7"
-         x="267.25903"
-         y="713.05792"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">θ</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
-       x="507.19553"
-       y="717.33142"
-       id="text2985-4-5-2-3-5"><tspan
+       style="font-size:16px;line-height:1.25;font-family:sans-serif"
+       x="-792.4671"
+       y="126.75281"
+       id="text30558"
+       transform="rotate(-90)"><tspan
          sodipodi:role="line"
-         id="tspan2987-8-6-0-7-9"
-         x="507.19553"
-         y="717.33142"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:1.25;font-family:Serif;-inkscape-font-specification:Serif">θ</tspan></text>
+         id="tspan30556"
+         x="-792.4671"
+         y="126.75281">exp./log. map</tspan></text>
   </g>
 </svg>
diff --git a/doc/source/_static/translation.png b/doc/source/_static/translation.png
new file mode 100644
index 000000000..3fe626b7b
Binary files /dev/null and b/doc/source/_static/translation.png differ
diff --git a/doc/source/_static/translation.svg b/doc/source/_static/translation.svg
new file mode 100644
index 000000000..afaa11dc5
--- /dev/null
+++ b/doc/source/_static/translation.svg
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="19.192951mm"
+   height="21.681004mm"
+   viewBox="0 0 19.192951 21.681004"
+   version="1.1"
+   id="svg878"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="translation.svg"
+   inkscape:export-filename="/home/afabisch/Projekte/pytransform3d/doc/source/_static/translation.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview880"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     inkscape:zoom="4.5896745"
+     inkscape:cx="42.704553"
+     inkscape:cy="39.000587"
+     inkscape:window-width="1419"
+     inkscape:window-height="849"
+     inkscape:window-x="436"
+     inkscape:window-y="156"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="layer1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <defs
+     id="defs875">
+    <marker
+       style="overflow:visible"
+       id="marker3286"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-6"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-62"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker3286-0"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow2Lend"
+       inkscape:isstock="true">
+      <path
+         transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
+         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:0.625;stroke-linejoin:round"
+         id="path2886-93" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-91.560515,-71.29566)">
+    <ellipse
+       style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316"
+       cx="102.72945"
+       cy="72.652588"
+       rx="1.3569278"
+       ry="1.356928" />
+    <ellipse
+       style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316-3"
+       cx="93.250778"
+       cy="83.085403"
+       rx="1.3569278"
+       ry="1.356928" />
+    <ellipse
+       style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316-5"
+       cx="109.39654"
+       cy="74.472092"
+       rx="1.3569278"
+       ry="1.356928" />
+    <ellipse
+       style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316-3-3"
+       cx="99.91787"
+       cy="84.904907"
+       rx="1.3569278"
+       ry="1.356928" />
+    <ellipse
+       style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316-1"
+       cx="109.35013"
+       cy="81.18692"
+       rx="1.3569278"
+       ry="1.356928" />
+    <ellipse
+       style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316-3-2"
+       cx="99.871452"
+       cy="91.619736"
+       rx="1.3569278"
+       ry="1.356928" />
+    <ellipse
+       style="fill:#50cfcd;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316-6"
+       cx="102.39611"
+       cy="78.742401"
+       rx="1.3569278"
+       ry="1.356928" />
+    <ellipse
+       style="fill:#e85050;fill-opacity:1;stroke:none;stroke-width:0.286144;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3316-3-0"
+       cx="92.917442"
+       cy="89.175217"
+       rx="1.3569278"
+       ry="1.356928" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path817-6"
+       d="m 100.52413,91.100955 7.82324,-8.74694"
+       style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker3286-0)"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 89.489083,87.349719 105.58051,69.358339"
+       id="path1340" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 89.582223,96.581918 112.00826,71.507974"
+       id="path1340-2" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 89.099184,93.52354 111.52522,68.449596"
+       id="path1340-2-1" />
+  </g>
+</svg>
diff --git a/doc/source/api.rst b/doc/source/api.rst
index 9cd1fbaae..de1a0ec73 100644
--- a/doc/source/api.rst
+++ b/doc/source/api.rst
@@ -34,6 +34,7 @@ Input Validation Functions
    ~pytransform3d.rotations.check_quaternion
    ~pytransform3d.rotations.check_quaternions
    ~pytransform3d.rotations.check_rotor
+   ~pytransform3d.rotations.check_mrp
 
 Conversions to Rotation Matrix
 ------------------------------
@@ -80,11 +81,12 @@ Conversions to Quaternion
    ~pytransform3d.rotations.quaternion_from_axis_angle
    ~pytransform3d.rotations.quaternion_from_compact_axis_angle
    ~pytransform3d.rotations.quaternion_from_euler
+   ~pytransform3d.rotations.quaternion_from_mrp
    ~pytransform3d.rotations.quaternion_xyzw_from_wxyz
    ~pytransform3d.rotations.quaternion_wxyz_from_xyzw
 
 Conversions to Rotor
--------------------------
+--------------------
 
 .. autosummary::
    :toctree: _apidoc/
@@ -93,6 +95,15 @@ Conversions to Rotor
    ~pytransform3d.rotations.rotor_from_two_directions
    ~pytransform3d.rotations.rotor_from_plane_angle
 
+Conversions to Modified Rodrigues Parameters
+--------------------------------------------
+
+.. autosummary::
+   :toctree: _apidoc/
+   :template: function.rst
+
+   ~pytransform3d.rotations.mrp_from_quaternion
+
 Quaternion and Axis-Angle Operations
 ------------------------------------
 
@@ -171,6 +182,10 @@ Utility Functions
    ~pytransform3d.rotations.random_compact_axis_angle
    ~pytransform3d.rotations.random_quaternion
    ~pytransform3d.rotations.cross_product_matrix
+   ~pytransform3d.rotations.left_jacobian_SO3
+   ~pytransform3d.rotations.left_jacobian_SO3_series
+   ~pytransform3d.rotations.left_jacobian_SO3_inv
+   ~pytransform3d.rotations.left_jacobian_SO3_inv_series
 
 Deprecated Functions
 --------------------
@@ -198,7 +213,12 @@ Utility Functions
 
    ~pytransform3d.transformations.random_transform
    ~pytransform3d.transformations.random_screw_axis
+   ~pytransform3d.transformations.random_exponential_coordinates
    ~pytransform3d.transformations.norm_exponential_coordinates
+   ~pytransform3d.transformations.left_jacobian_SE3
+   ~pytransform3d.transformations.left_jacobian_SE3_series
+   ~pytransform3d.transformations.left_jacobian_SE3_inv
+   ~pytransform3d.transformations.left_jacobian_SE3_inv_series
 
 Input Validation Functions
 --------------------------
@@ -416,6 +436,26 @@ Testing
    ~pytransform3d.trajectories.mirror_screw_axis_direction
 
 
+:mod:`pytransform3d.uncertainty`
+================================
+
+.. automodule:: pytransform3d.uncertainty
+    :no-members:
+    :no-inherited-members:
+
+.. autosummary::
+   :toctree: _apidoc/
+   :template: function.rst
+
+   ~pytransform3d.uncertainty.estimate_gaussian_transform_from_samples
+   ~pytransform3d.uncertainty.invert_uncertain_transform
+   ~pytransform3d.uncertainty.concat_globally_uncertain_transforms
+   ~pytransform3d.uncertainty.pose_fusion
+   ~pytransform3d.uncertainty.to_ellipsoid
+   ~pytransform3d.uncertainty.to_projected_ellipsoid
+   ~pytransform3d.uncertainty.plot_projected_ellipsoid
+
+
 :mod:`pytransform3d.coordinates`
 =================================
 
@@ -527,6 +567,7 @@ Testing
    ~pytransform3d.plot_utils.plot_length_variable
    ~pytransform3d.plot_utils.plot_box
    ~pytransform3d.plot_utils.plot_sphere
+   ~pytransform3d.plot_utils.plot_spheres
    ~pytransform3d.plot_utils.plot_cylinder
    ~pytransform3d.plot_utils.plot_mesh
    ~pytransform3d.plot_utils.plot_ellipsoid
diff --git a/doc/source/euler_angles.rst b/doc/source/euler_angles.rst
index 4b17914a5..4659d06a3 100644
--- a/doc/source/euler_angles.rst
+++ b/doc/source/euler_angles.rst
@@ -51,22 +51,35 @@ Euler angles rotate about three basis vectors with by the angles
 :math:`\alpha`, :math:`\beta`, and :math:`\gamma`. If we want to find the
 Euler angles that correspond to one rotation matrix :math:`R`, there is an
 infinite number of solutions because we can always add or subtract
-:math:`2\pi` to one of the angles and get the same result. For this
-reason the proper Euler angles are typically restricted to
+:math:`2\pi` to one of the angles and get the same result. In addition,
+for proper Euler angles
 
 .. math::
 
-    -\pi \leq \alpha \leq \pi,\\
-    0 \leq \beta \leq \pi,\\
-    -\pi \leq \gamma \leq \pi
+    R(\alpha, \beta, \gamma) = R(\alpha + \pi, -\beta, \gamma - \pi).
+
+For Cardan angles
+
+.. math::
+
+    R(\alpha, \beta, \gamma) = R(\alpha + \pi, \pi - \beta, \gamma - \pi).
+
+For this reason the proper Euler angles are typically restricted to
+
+.. math::
+
+    -\pi \leq \alpha < \pi, \qquad 0 \leq \beta \leq \pi, \qquad -\pi \leq \gamma < \pi
 
 and Cardan angles are usually restricted to
 
 .. math::
 
-    -\pi \leq \alpha \leq \pi,\\
-    -\frac{\pi}{2} \leq \beta \leq \frac{\pi}{2},\\
-    -\pi \leq \gamma \leq \pi
+    -\pi \leq \alpha < \pi, \qquad -\frac{\pi}{2} \leq \beta \leq \frac{\pi}{2}, \qquad -\pi \leq \gamma < \pi
+
+to make these representations unique.
+
+An alternative convention limits the range of :math:`\alpha` and :math:`\beta` to
+:math:`\left[0, 2 \pi\right)`.
 
 -----------
 Gimbal Lock
@@ -162,3 +175,11 @@ API: Euler Angles from Rotation Matrix / Quaternion
    ~pytransform3d.rotations.extrinsic_euler_zyx_from_active_matrix
    ~pytransform3d.rotations.intrinsic_euler_zxy_from_active_matrix
    ~pytransform3d.rotations.extrinsic_euler_zxy_from_active_matrix
+
+----------
+References
+----------
+
+1. Malcolm D. Shuster: A Survery of Attitude Representations.
+   In: The Journal of Astronautical Sciences, Vol. 41, No.4, 1993,
+   pp. 475-476, http://malcolmdshuster.com/Pub_1993h_J_Repsurv_scan.pdf
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 1929fc1c1..6c7f00248 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -44,49 +44,36 @@ pytransform3d
 
         <div class="col-md-8">
 
-pytransform3d covers the following groups of transformations.
 This documentation explains how you can work with pytransform3d and with
 3D transformations in general.
 
-+-------+--------------------+-----------------------------------------+
-| Group | Description        | Representations                         |
-+=======+====================+=========================================+
-| SO(3) | 3D rotations       | unit quaternion, rotation matrix,       |
-|       |                    | axis-angle, Euler angles, rotor         |
-+-------+--------------------+-----------------------------------------+
-| SE(3) | 3D transformations | transformation matrix, translation      |
-|       | (rotation and      | and unit quaternion, exponential        |
-|       | translation)       | coordinates, logarithm of               |
-|       |                    | transformation, unit dual quaternion    |
-+-------+--------------------+-----------------------------------------+
-
 
 -----
 Scope
 -----
 
-The library focuses on readability and debugging, not on computational
+pytransform3d focuses on readability and debugging, not on computational
 efficiency. If you want to have an efficient implementation of some function
 from the library you can easily extract the relevant code and implement it
 more efficiently in a language of your choice.
 
 The library integrates well with the
 `scientific Python ecosystem <https://scipy-lectures.org/>`_
-with its core libraries Numpy, Scipy and Matplotlib.
-We rely on `Numpy <https://numpy.org/>`_ for linear algebra and on
+with its core libraries NumPy, SciPy and Matplotlib.
+We rely on `NumPy <https://numpy.org/>`_ for linear algebra and on
 `Matplotlib <https://matplotlib.org/>`_ to offer plotting functionalities.
-`Scipy <https://scipy.org/scipylib/index.html>`_ is used if you want to
+`SciPy <https://scipy.org/>`_ is used if you want to
 automatically compute transformations from a graph of transformations.
 
 pytransform3d offers...
 
-* operations for most common representations of rotation (orientation) and
-  translation (position)
+* operations for most common representations of rotation / orientation and
+  translation / position
 * conversions between those representations
-* clear documentation of transformation conventions
+* clear documentation of conventions
 * tight coupling with matplotlib to quickly visualize (or animate)
   transformations
-* the TransformManager which manages complex chains of transformations
+* the TransformManager which organizes complex chains of transformations
 * the TransformEditor which allows to modify transformations graphically
 * the UrdfTransformManager which is able to load transformations from
   `URDF <https://wiki.ros.org/urdf>`_ files
diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst
index 26da004da..9f3d8f1de 100644
--- a/doc/source/introduction.rst
+++ b/doc/source/introduction.rst
@@ -3,31 +3,49 @@ Introduction to 3D Rigid Transformations
 ========================================
 
 ------
-Frames
+Basics
 ------
 
-The basis of this introduction are (coordinate reference) frames in 3D
-Euclidean space. A frame is defined by an origin and 3 orthogonal basis
-vectors and it is attached to a rigid body.
-The pose (its position and orientation) of a rigid body (i.e., of its frame)
-is always expressed with respect to another frame.
+.. list-table::
+   :widths: 15 35 15 35
+
+   * - .. image:: _static/position.png
+     - **Position** of a rigid body in 3D Euclidean space is expressed as a 3D
+       vector.
+     - .. image:: _static/translation.png
+     - **Translation** is a displacement, in which points move along parallel
+       lines by the same distance.
+   * - .. image:: _static/frame.png
+     - **Orientation** of a rigid body in 3D Euclidean space is defined by a
+       set of 3 orthogonal basis vectors.
+     - .. image:: _static/rotation.png
+     -  **Rotation** is a displacement, in which points move about a rotation
+        axis through the origin of the reference frame (fixed point) along a
+        circle by the same angle.
+   * - .. image:: _static/position.png
+           :width: 30%
+           :align: center
+       .. image:: _static/frame.png
+           :width: 30%
+           :align: center
+     - **Pose** is a combination of position and orientation.
+     - .. image:: _static/translation.png
+           :width: 30%
+           :align: center
+       .. image:: _static/rotation.png
+           :width: 30%
+           :align: center
+     - A (proper) **rigid transformation** is a combination of translation and
+       rotation.
 
 ------
-Basics
+Frames
 ------
 
-**Position** of a rigid body in 3D Euclidean space is expressed as a 3D
-vector.
-
-**Orientation** of a rigid body in 3D Euclidean space is defined by a set
-of 3 orthogonal basis vectors.
-
-**Translation** is a displacement, in which points move along parallel lines
-by the same distance.
-
-**Rotation** is a displacement, in which points move about a rotation axis
-through the origin of the reference frame (fixed point) along a circle by the
-same angle.
+A (coordinate reference) frame in 3D Euclidean space is defined by an origin
+(position) and 3 orthogonal basis vectors (orientation) and it is attached to
+a rigid body. The pose (position and orientation) of a rigid body (i.e., of
+its frame) is always expressed with respect to another frame.
 
 --------------
 Frame Notation
@@ -80,52 +98,57 @@ All representations are stored in NumPy arrays, of which the corresponding
 shape is shown in this table. You will find more details on these
 representations on the following pages.
 
-+----------------------------------------+---------------------+----------+-------------+
-| Representation and Mathematical Symbol | NumPy Array Shape   | Rotation | Translation |
-+========================================+=====================+==========+=============+
-| Rotation matrix                        | (3, 3)              | X        |             |
-| :math:`\pmb{R}`                        |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Compact axis-angle                     | (3,)                | X        |             |
-| :math:`\pmb{\omega}`                   |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Axis-angle                             | (4,)                | X        |             |
-| :math:`(\hat{\pmb{\omega}}, \theta)`   |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Logarithm of rotation                  | (3, 3)              | X        |             |
-| :math:`\left[\pmb{\omega}\right]`      |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Quaternion                             | (4,)                | X        |             |
-| :math:`\pmb{q}`                        |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Rotor                                  | (4,)                | X        |             |
-| :math:`R`                              |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Euler angles                           | (3,)                | X        |             |
-| :math:`(\alpha, \beta, \gamma)`        |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Transformation matrix                  | (4, 4)              | X        | X           |
-| :math:`\pmb{T}`                        |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Exponential coordinates                | (6,)                | X        | X           |
-| :math:`\mathcal{S}\theta`              |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Logarithm of transformation            | (4, 4)              | X        | X           |
-| :math:`\left[\mathcal{S}\right]\theta` |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Position and quaternion                | (7,)                | X        | X           |
-| :math:`(\pmb{p}, \pmb{q})`             |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
-| Dual quaternion                        | (8,)                | X        | X           |
-| :math:`\pmb{p} + \epsilon\pmb{q}`      |                     |          |             |
-+----------------------------------------+---------------------+----------+-------------+
++----------------------------------------+---------------------+------------------+---------------+
+|                                        |                     | Rigid Transformation - SE(3)     |
++                                        |                     +------------------+---------------+
+| Representation and Mathematical Symbol | NumPy Array Shape   | Rotation - SO(3) | Translation   |
++========================================+=====================+==================+===============+
+| Rotation matrix                        | (3, 3)              | X                |               |
+| :math:`\pmb{R}`                        |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Compact axis-angle                     | (3,)                | X                |               |
+| :math:`\pmb{\omega}`                   |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Axis-angle                             | (4,)                | X                |               |
+| :math:`(\hat{\pmb{\omega}}, \theta)`   |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Logarithm of rotation                  | (3, 3)              | X                |               |
+| :math:`\left[\pmb{\omega}\right]`      |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Quaternion                             | (4,)                | X                |               |
+| :math:`\pmb{q}`                        |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Rotor                                  | (4,)                | X                |               |
+| :math:`R`                              |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Euler angles                           | (3,)                | X                |               |
+| :math:`(\alpha, \beta, \gamma)`        |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Modified Rodrigues parameters          | (3,)                | X                |               |
+| :math:`\psi`                           |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Transformation matrix                  | (4, 4)              | X                | X             |
+| :math:`\pmb{T}`                        |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Exponential coordinates                | (6,)                | X                | X             |
+| :math:`\mathcal{S}\theta`              |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Logarithm of transformation            | (4, 4)              | X                | X             |
+| :math:`\left[\mathcal{S}\right]\theta` |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Position and quaternion                | (7,)                | X                | X             |
+| :math:`(\pmb{p}, \pmb{q})`             |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
+| Dual quaternion                        | (8,)                | X                | X             |
+| :math:`\pmb{p} + \epsilon\pmb{q}`      |                     |                  |               |
++----------------------------------------+---------------------+------------------+---------------+
 
 ----------
 References
 ----------
 
-* Waldron, K., Schmiedeler, J. (2008). Kinematics. In: Siciliano, B., Khatib,
-  O. (eds) Springer Handbook of Robotics. Springer, Berlin, Heidelberg.
-  https://doi.org/10.1007/978-3-540-30301-5_2
-* Representing Robot Pose: The good, the bad, and the ugly (slides): http://static.squarespace.com/static/523c5c56e4b0abc2df5e163e/t/53957839e4b05045ad65021d/1402304569659/Workshop+-+Rotations_v102.key.pdf
-* Representing Robot Pose: The good, the bad, and the ugly (blog): http://paulfurgale.info/news/2014/6/9/representing-robot-pose-the-good-the-bad-and-the-ugly
+1. Waldron, K., Schmiedeler, J. (2008). Kinematics. In: Siciliano, B., Khatib,
+   O. (eds) Springer Handbook of Robotics. Springer, Berlin, Heidelberg.
+   https://doi.org/10.1007/978-3-540-30301-5_2
+2. Representing Robot Pose: The good, the bad, and the ugly (slides): http://static.squarespace.com/static/523c5c56e4b0abc2df5e163e/t/53957839e4b05045ad65021d/1402304569659/Workshop+-+Rotations_v102.key.pdf
+3. Representing Robot Pose: The good, the bad, and the ugly (blog): http://paulfurgale.info/news/2014/6/9/representing-robot-pose-the-good-the-bad-and-the-ugly
diff --git a/doc/source/rotations.rst b/doc/source/rotations.rst
index d5b4825ed..9ef41e776 100644
--- a/doc/source/rotations.rst
+++ b/doc/source/rotations.rst
@@ -49,6 +49,9 @@ is not implemented in pytransform3d then it is shown in brackets.
 | Euler angles                           | No            | No                 | No            | No            |
 | :math:`(\alpha, \beta, \gamma)`        |               |                    |               |               |
 +----------------------------------------+---------------+--------------------+---------------+---------------+
+| Modified Rodrigues parameters          | Negative      | No                 | No            | No            |
+| :math:`\psi`                           |               |                    |               |               |
++----------------------------------------+---------------+--------------------+---------------+---------------+
 
 
 ---------------
@@ -79,10 +82,18 @@ A more compact representation of these constraints is
 :math:`\boldsymbol R^T \boldsymbol R = \boldsymbol I
 \Leftrightarrow \boldsymbol R^T = \boldsymbol R^{-1}`.
 
-In addition, :math:`det(\boldsymbol R) = 1` because we use right-handed
-coordinate system   (:math:`det(\boldsymbol R) = -1` for left-handed
+In addition, :math:`\det(\boldsymbol R) = 1` because we use right-handed
+coordinate system   (:math:`\det(\boldsymbol R) = -1` for left-handed
 coordinate systems).
 
+Hence, the group :math:`SO(3)` is defined as
+
+.. math::
+
+    SO(3) = \{\boldsymbol{R} \in \mathbb{R}^{3 \times 3} |
+    \boldsymbol{R}\boldsymbol{R}^T = \boldsymbol{I},
+    \det(\boldsymbol{R}) = 1\}.
+
 pytransform3d uses a numpy array of shape (3, 3) to represent rotation
 matrices and typically we use the variable name R for a rotation matrix.
 
@@ -110,16 +121,13 @@ This means that we rotate a point :math:`_B\boldsymbol{p}` by
 
 This is called **linear map**.
 
+Note that using our index notation and conventions the second index of the
+rotation matrix and the left index of the point have to be the same. The
+rotation is applied incorrectly if this is not the case.
+
 We can see that *each column* of such a rotation matrix is a basis vector
 of frame :math:`A` with respect to frame :math:`B`.
-
 We can plot the basis vectors of an orientation to visualize it.
-
-.. note::
-
-    When plotting basis vectors it is a convention to use red for the x-axis,
-    green for the y-axis and blue for the z-axis (RGB for xyz).
-
 Here, we can see orientation represented by the rotation matrix
 
 .. math::
@@ -137,6 +145,11 @@ Here, we can see orientation represented by the rotation matrix
     from pytransform3d.rotations import plot_basis
     plot_basis()
 
+.. note::
+
+    When plotting basis vectors it is a convention to use red for the x-axis,
+    green for the y-axis and blue for the z-axis (RGB for xyz).
+
 We can easily chain multiple rotations: we can apply the rotation defined
 by :math:`\boldsymbol R_{AB}` after the rotation :math:`\boldsymbol R_{BC}`
 by applying the rotation
@@ -145,6 +158,9 @@ by applying the rotation
 
     \boldsymbol R_{AC} = \boldsymbol R_{AB} \boldsymbol R_{BC}.
 
+Note that again the indices have to align. Otherwise rotations are not applied
+in the correct order.
+
 .. warning::
 
     There are two different conventions on how to concatenate rotation
@@ -157,6 +173,12 @@ by applying the rotation
     which :math:`R_2` is applied. Note that this applies to both
     passive and active rotation matrices.
 
+The easiest way to construct rotation matrices is through rotations about the
+basis vectors with :func:`~pytransform3d.rotations.active_matrix_from_angle`.
+Multiple rotation matrices that were constructed like this can be concatenated.
+This will be done, for instance, to obtain rotation matrices from Euler angles
+(see :doc:`euler_angles`).
+
 **Pros**
 
 * It is easy to apply rotations on point vectors by matrix-vector
@@ -196,18 +218,18 @@ It is possible to write this in a more compact way as a rotation vector:
 
 .. math::
 
-    \boldsymbol{\omega} = \theta \hat{\boldsymbol{\omega}}
+    \boldsymbol{\omega} = \hat{\boldsymbol{\omega}} \theta \in \mathbb{R}^3
 
 pytransform3d uses a numpy array of shape (3,) for the compact axis-angle
 representation of a rotation and typically we use the variable name a.
 
 We can also refer to this representation as **exponential coordinates of
 rotation**. We can easily represent angular velocity as
-:math:`\dot{\theta} \hat{\boldsymbol{\omega}}`
+:math:`\hat{\boldsymbol{\omega}} \dot{\theta}`
 and angular acceleration as
-:math:`\ddot{\theta} \hat{\boldsymbol{\omega}}` so that we can easily do
+:math:`\hat{\boldsymbol{\omega}} \ddot{\theta}` so that we can easily do
 component-wise integration and differentiation with this representation.
-In addition, we can represent :math:`\theta \hat{\boldsymbol{\omega}}` by
+In addition, we can represent :math:`\hat{\boldsymbol{\omega}} \theta` by
 the cross-product matrix
 
 .. math::
@@ -242,7 +264,8 @@ the Lie group :math:`SO(3)`.
 * There might be discontinuities during interpolation as an angle of 0 and
   any multiple of :math:`2\pi` represent the same orientation. This has to
   be considered.
-* Concatenation involves conversion to another representation
+* Concatenation and transformation of vectors requires conversion to rotation
+  matrix or quaternion
 
 -----------
 Quaternions
@@ -255,6 +278,7 @@ and an vector / imaginary part
 .. math::
 
     \boldsymbol{q} = w + x \boldsymbol{i} + y \boldsymbol{j} + z \boldsymbol{k}
+    \in \mathbb{H}
 
 .. warning::
 
@@ -268,8 +292,8 @@ Read `this paper <https://arxiv.org/pdf/1801.07478.pdf>`_ for details about
 the two conventions and why Hamilton's convention should be used. Section VI A
 gives further useful hints to identify which convention is used.
 
-The unit quaternion space :math:`S^3` can be used to represent orientations.
-To do that, we use an encoding based on the rotation axis and angle.
+The unit quaternion space :math:`\mathbb{S}^3` can be used to represent
+orientations with an encoding based on the rotation axis and angle.
 A rotation quaternion is a four-dimensional unit vector (versor)
 :math:`\boldsymbol{\hat{q}}`.
 The following equation describes its relation to axis-axis notation.
@@ -284,6 +308,11 @@ The following equation describes its relation to axis-axis notation.
         \omega_y \sin \frac{\theta}{2}\\
         \omega_z \sin \frac{\theta}{2}\\
     \end{array} \right)
+    =
+    \left( \begin{array}{c}
+        \cos \frac{\theta}{2}\\
+        \hat{\boldsymbol{\omega}} \sin \frac{\theta}{2}\\
+    \end{array} \right)
 
 pytransform3d uses a numpy array of shape (4,) for quaternions and
 typically we use the variable name q.
@@ -296,8 +325,8 @@ typically we use the variable name q.
 
 .. warning::
 
-    The unit quaternions :math:`\boldsymbol{\hat{q}}` and
-    :math:`-\boldsymbol{\hat{q}}` represent exactly the same rotation.
+    The *antipodal* unit quaternions :math:`\boldsymbol{\hat{q}}` and
+    :math:`-\boldsymbol{\hat{q}}` represent the same rotation.
 
 **Pros**
 
@@ -409,12 +438,54 @@ quaternions on the one hand and rotors on the other hand.
 
 * The representation is not straightforward to interpret
 
+-----------------------------
+Modified Rodrigues Parameters
+-----------------------------
+
+Another minimal representation of rotation are modified Rodrigues parameters
+(MRP)
+
+.. math::
+
+    \psi = \tan \left(\frac{\theta}{4}\right) \hat{\boldsymbol{\omega}}
+
+This representation is similar to the compact axis-angle representation.
+However, the angle of rotation is converted to :math:`\tan(\frac{\theta}{4})`.
+Hence, there is an easy conversion from unit quaternions to MRP:
+
+.. math::
+
+    \psi = \frac{\left( \begin{array}{c} x\\ y\\ z\\ \end{array} \right)}{1 + w}.
+
+given some quaternion with a scalar :math:`w` and a vector
+:math:`\left(x, y, z \right)^T`.
+
+pytransform3d uses a numpy array of shape (3,) for modified Rodrigues
+parameters.
+
+.. warning::
+
+    MRPs have a singuarity at :math:`2 \pi` which we can avoid by ensuring the
+    angle of rotation does not exceed :math:`\pi`.
+
+**Pros**
+
+* Minimal representation
+
+**Cons**
+
+* The representation is not straightforward to interpret
+* Normalization of angle required to avoid singularity
+* Concatenation and transformation of vectors requires conversion to rotation
+  matrix or quaternion
+
 ----------
 References
 ----------
 
-* Why and How to Avoid the Flipped Quaternion Multiplication: https://arxiv.org/pdf/1801.07478.pdf
-* Kindr cheat sheet: https://docs.leggedrobotics.com/kindr/cheatsheet_latest.pdf
-* Let's remove Quaternions from every 3D Engine: https://marctenbosch.com/quaternions/
-* Applications of Geometric Algebra: http://geometry.mrao.cam.ac.uk/wp-content/uploads/2015/02/01ApplicationsI.pdf
-* Euler–Rodrigues formula variations, quaternion conjugation and intrinsic connections: https://doi.org/10.1016/j.mechmachtheory.2015.03.004
+1. Why and How to Avoid the Flipped Quaternion Multiplication: https://arxiv.org/pdf/1801.07478.pdf
+2. Kindr cheat sheet: https://docs.leggedrobotics.com/kindr/cheatsheet_latest.pdf
+3. Let's remove Quaternions from every 3D Engine: https://marctenbosch.com/quaternions/
+4. Applications of Geometric Algebra: http://geometry.mrao.cam.ac.uk/wp-content/uploads/2015/02/01ApplicationsI.pdf
+5. Euler–Rodrigues formula variations, quaternion conjugation and intrinsic connections: https://doi.org/10.1016/j.mechmachtheory.2015.03.004
+6. Terzakis, Lourakis, Alt-Boudaoud: Modified Rodrigues Parameters: An Efficient Representation of Orientation in 3D Vision and Graphics, https://link.springer.com/article/10.1007/s10851-017-0765-x
diff --git a/doc/source/transform_manager.rst b/doc/source/transform_manager.rst
index 4210848fb..97e8aa86b 100644
--- a/doc/source/transform_manager.rst
+++ b/doc/source/transform_manager.rst
@@ -6,16 +6,34 @@ It is sometimes very difficult to have an overview of all the transformations
 that are required to calculate another transformation. Suppose you have
 a robot with a camera that can observe the robot's end-effector and an object
 that we want to manipulate. We would like to know the position of the
-end-effector in the object's frame so that we can control it. The
-:class:`~pytransform3d.transform_manager.TransformManager` can handle this
-for you.
+end-effector in the object's frame so that we can control it.
+
+We can organize transformations in a graph like the one shown in the figure
+below. Each frame represents a vertex (node) and each transformation an edge
+(connection). Edges in these graphs are directed, but we can create an edge
+in the opposite direction by inverting the transformation. If we want to
+compute a new edge indicated by the dashed red arrow, we have to use known
+transformations, that is, we compute the shortest path through the undirected
+graph, concatenate transformations along the path, and invert transformations
+if required as indicated by the solid red arrow.
+
+.. figure:: _static/transform_graph.png
+    :width: 35%
+    :align: center
+
+    Example of a graph representing transformations that connect frames.
 
-.. plot:: ../../examples/plots/plot_transform_manager.py
-    :include-source:
+The :class:`~pytransform3d.transform_manager.TransformManager` does this for
+you. For the given example, this would be the corresponding code
+(see also: :ref:`sphx_glr__auto_examples_plots_plot_transform_manager.py`).
+
+.. literalinclude:: ../../examples/plots/plot_transform_manager.py
+   :language: python
+   :lines: 27-32
 
 We can also export the underlying graph structure as a PNG with
 
-.. code-block::
+.. code-block:: python
 
     tm.write_png(filename)
 
@@ -23,6 +41,7 @@ We can also export the underlying graph structure as a PNG with
     :width: 50%
     :align: center
 
+|
 
 A subclass of :class:`~pytransform3d.transform_manager.TransformManager` is
 :class:`~pytransform3d.urdf.UrdfTransformManager` which can load robot
@@ -30,6 +49,4 @@ definitions from `URDF <http://wiki.ros.org/urdf/Tutorials>`_ files.
 The same class can be used to display collision objects or visuals from URDF
 files. The library `trimesh <https://trimsh.org/>`_ will be used to load
 meshes. Here is a simple example with one visual that is used for two links:
-
-.. plot:: ../../examples/plots/plot_urdf_with_meshes.py
-    :include-source:
+:ref:`sphx_glr__auto_examples_plots_plot_urdf_with_meshes.py`.
diff --git a/doc/source/transformation_ambiguities.rst b/doc/source/transformation_ambiguities.rst
index b1200a146..b473c95e7 100644
--- a/doc/source/transformation_ambiguities.rst
+++ b/doc/source/transformation_ambiguities.rst
@@ -54,7 +54,10 @@ right-handed system and with the left hand in a left-handed system.
 .. raw:: html
 
     <table>
-    <tr><td>Right-handed</td><td>Left-handed</td></tr>
+    <tr>
+        <th style="text-align:center">Right-handed</th>
+        <th style="text-align:center">Left-handed</th>
+    </tr>
     <tr>
     <td>
 
@@ -156,7 +159,10 @@ frame.
 .. raw:: html
 
     <table>
-    <tr><td>Active</td><td>Passive</td></tr>
+    <tr>
+        <th style="text-align:center">Active</th>
+        <th style="text-align:center">Passive</th>
+    </tr>
     <tr>
     <td>
 
@@ -178,9 +184,9 @@ frame.
     ax.set_yticks(())
     ax.set_zticks(())
 
-    random_state = np.random.RandomState(42)
+    rng = np.random.default_rng(42)
     PA = np.ones((10, 4))
-    PA[:, :3] = 0.1 * random_state.randn(10, 3)
+    PA[:, :3] = 0.1 * rng.standard_normal(size=(10, 3))
     PA[:, 0] += 0.3
     PA[:, :3] += 0.3
 
@@ -234,9 +240,9 @@ frame.
     ax.set_yticks(())
     ax.set_zticks(())
 
-    random_state = np.random.RandomState(42)
+    rng = np.random.default_rng(42)
     PA = np.ones((10, 4))
-    PA[:, :3] = 0.1 * random_state.randn(10, 3)
+    PA[:, :3] = 0.1 * rng.standard_normal(size=(10, 3))
     PA[:, 0] += 0.3
     PA[:, :3] += 0.3
 
diff --git a/doc/source/transformation_modeling.rst b/doc/source/transformation_modeling.rst
index 4b9fad9c5..a7bc1a33e 100644
--- a/doc/source/transformation_modeling.rst
+++ b/doc/source/transformation_modeling.rst
@@ -10,6 +10,8 @@ and make to always concatenate them in the correct way.
    :alt: Three frames
    :align: center
 
+|
+
 When modeling transformations in mathematical equations we often sequence them
 from right to left (extrinsic convention).
 Here it makes most sense to give names like :math:`\boldsymbol T_{BA}` for
diff --git a/doc/source/transformations.rst b/doc/source/transformations.rst
index 00be8dd52..3ef842a81 100644
--- a/doc/source/transformations.rst
+++ b/doc/source/transformations.rst
@@ -2,8 +2,8 @@
 SE(3): 3D Transformations
 =========================
 
-The group of all rigid transformations in the 3D Cartesian space is :math:`SE(3)`
-(SE: special Euclidean group).
+The group of all proper rigid transformations (rototranslations) in the
+3D Cartesian space is :math:`SE(3)` (SE: special Euclidean group).
 Transformations consist of a rotation and a translation. Those can be
 represented in different ways just like rotations can be expressed
 in different ways.
@@ -67,11 +67,23 @@ the form
     0 & 0 & 0 & 1\\
     \end{matrix}
     \right)
+    \in SE(3).
 
 It is a partitioned matrix with a 3x3 rotation matrix :math:`\boldsymbol R`
 and a column vector :math:`\boldsymbol t` that represents the translation.
 It is also sometimes called the homogeneous representation of a transformation.
-All transformation matrices of this form generate the special Euclidean group :math:`SE(3)`.
+All transformation matrices of this form generate the special Euclidean group
+:math:`SE(3)`, that is,
+
+.. math::
+
+    SE(3) = \{ \boldsymbol{T} = \left(
+    \begin{array}{cc}
+    \boldsymbol{R} & \boldsymbol{t}\\
+    \boldsymbol{0} & 1
+    \end{array}
+    \right) \in \mathbb{R}^{4 \times 4}
+    | \boldsymbol{R} \in SO(3), \boldsymbol{t} \in \mathbb{R}^3 \}.
 
 pytransform3d uses a numpy array of shape (4, 4) to represent transformation
 matrices and typically we use the variable name A2B for a transformation
@@ -109,9 +121,7 @@ quaternion:
 
 .. math::
 
-    \left( \begin{array}{c}
-        x\\y\\z\\q_w\\q_x\\q_y\\q_z
-    \end{array} \right)
+    \left( x, y, z, q_w, q_x, q_y, q_z \right)^T
 
 This representation is more compact than a transformation matrix and is
 particularly useful if you want to represent a sequence of poses in
@@ -120,9 +130,9 @@ a 2D array.
 pytransform3d uses a numpy array of shape (7,) to represent position and
 quaternion and typically we use the variable name pq.
 
------------------------
-Exponential Coordinates
------------------------
+----------------
+Screw Parameters
+----------------
 
 .. plot:: ../../examples/plots/plot_screw.py
 
@@ -140,9 +150,13 @@ h to represent the parameters of a screw.
 
 .. image:: _static/screw_axis.png
    :alt: Screw axis
-   :width: 80%
+   :width: 50%
    :align: center
 
+----------
+Screw Axis
+----------
+
 A **screw axis** is typically represented by
 :math:`\mathcal{S} = \left[\begin{array}{c}\boldsymbol{\omega}\\\boldsymbol{v}\end{array}\right] \in \mathbb{R}^6`,
 where either
@@ -163,6 +177,10 @@ In case 1, we can compute the screw axis from screw parameters
 
 In case 2, :math:`h` is infinite and we directly translate along :math:`\hat{\boldsymbol{s}}`.
 
+-----------------------
+Exponential Coordinates
+-----------------------
+
 By multiplication with an additional parameter :math:`\theta` we can then
 define a complete transformation through its exponential coordinates
 :math:`\mathcal{S} \theta = \left[\begin{array}{c}\boldsymbol{\omega}\theta\\\boldsymbol{v}\theta\end{array}\right] \in \mathbb{R}^6`.
@@ -171,6 +189,17 @@ This is a minimal representation as it only needs 6 values.
 pytransform3d uses a numpy array of shape (6,) to represent a exponential
 coordinates of transformation and typically we use the variable name Stheta.
 
+.. warning::
+
+    Note that we use the screw theory definition of exponential coordinates
+    and :math:`so(3)` (see next section) used by Paden (1985), Lynch and Park
+    (2017), and Corke (2017). They separate the parameter :math:`\theta` from
+    the screw axis. Additionally, they use the first three components to encode
+    rotation and the last three components to encode translation. There is an
+    alternative definition used by Eade (2017) and Sola et al. (2018). They use
+    a different order of the 3D vector components and they do not separate
+    :math:`\theta` from the screw axis in their notation.
+
 ---------------------------
 Logarithm of Transformation
 ---------------------------
@@ -194,7 +223,7 @@ Alternatively, we can represent a screw axis :math:`\mathcal S` in a matrix
     0 & 0 & 0 & 0\\
     \end{matrix}
     \right)
-    \in \mathbb{R}^{4 \times 4}
+    \in se(3) \subset \mathbb{R}^{4 \times 4}
 
 that contains the cross-product matrix of its orientation part and its
 translation part. This is the **matrix representation of a screw axis** and
@@ -219,7 +248,7 @@ Twist
 We call spatial velocity (translation and rotation) **twist**. Similarly
 to the matrix logarithm, a twist :math:`\mathcal{V} = \mathcal{S} \dot{\theta}`
 is described by a screw axis :math:`S` and a scalar :math:`\dot{\theta}`
-and :math:`\left[\mathcal{V}\right] = \left[\mathcal{S}\right] \theta \in se(3)`
+and :math:`\left[\mathcal{V}\right] = \left[\mathcal{S}\right] \dot{\theta} \in se(3)`
 is the matrix representation of a twist.
 
 ----------------
@@ -263,14 +292,21 @@ component and the scalar 0, and rotation quaternions have the same ambiguity.
 References
 ----------
 
-Lynch, Park: Modern Robotics (Section 3.3); available at
-http://hades.mech.northwestern.edu/index.php/Modern_Robotics
-
-Wikipedia: Dual Quaternion; available at
-https://en.wikipedia.org/wiki/Dual_quaternion
-
-Yan-Bin Jia: Dual Quaternions; available at
-http://web.cs.iastate.edu/~cs577/handouts/dual-quaternion.pdf
-
-Ben Kenwright: A Beginners Guide to Dual-Quaternions; available at
-http://wscg.zcu.cz/WSCG2012/!_WSCG2012-Communications-1.pdf
+1. Lynch, Park: Modern Robotics (2017); available at
+   http://hades.mech.northwestern.edu/index.php/Modern_Robotics
+2. Bradley Evan Paden: Kinematics and Control of Robot Manipulators, PhD
+   thesis (1985); available at
+   https://digitalassets.lib.berkeley.edu/techreports/ucb/text/ERL-86-5.pdf
+3. Peter Corke: Robotics, Vision and Control, 2nd Edition (2017),
+   https://link.springer.com/book/10.1007/978-3-319-54413-7
+4. Ethan Eade: Lie Groups for 2D and 3D Transformations (2017); available at
+   https://ethaneade.com/lie.pdf
+5. Sola, Deray, Atchuthan: A micro Lie theory for state estimation in robotics
+   (2018), technical report; available at:
+   http://www.iri.upc.edu/files/scidoc/2089-A-micro-Lie-theory-for-state-estimation-in-robotics.pdf
+6. Wikipedia: Dual Quaternion; available at
+   https://en.wikipedia.org/wiki/Dual_quaternion
+7. Yan-Bin Jia: Dual Quaternions; available at
+   http://web.cs.iastate.edu/~cs577/handouts/dual-quaternion.pdf
+8. Ben Kenwright: A Beginners Guide to Dual-Quaternions; available at
+   http://wscg.zcu.cz/WSCG2012/!_WSCG2012-Communications-1.pdf
diff --git a/examples/animations/animate_angle_axis_interpolation.py b/examples/animations/animate_angle_axis_interpolation.py
index 6f48edfc5..95939c0f6 100644
--- a/examples/animations/animate_angle_axis_interpolation.py
+++ b/examples/animations/animate_angle_axis_interpolation.py
@@ -10,8 +10,6 @@
 results in a non-constant angular velocity. Usually it is a better idea to
 interpolate with slerp.
 """
-print(__doc__)
-
 import numpy as np
 import matplotlib.pyplot as plt
 from mpl_toolkits.mplot3d import axes3d
diff --git a/examples/animations/animate_quaternion_integration.py b/examples/animations/animate_quaternion_integration.py
index 39634e520..ce94397ca 100644
--- a/examples/animations/animate_quaternion_integration.py
+++ b/examples/animations/animate_quaternion_integration.py
@@ -5,9 +5,6 @@
 
 Integrate angular accelerations to a quaternion sequence and animate it.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from mpl_toolkits.mplot3d import axes3d
@@ -32,13 +29,13 @@ def update_lines(step, Q, rot):
 
 
 if __name__ == "__main__":
-    random_state = np.random.RandomState(3)
-    start = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(3)
+    start = pr.random_quaternion(rng)
     n_frames = 1000
     dt = 0.01
     angular_accelerations = np.empty((n_frames, 3))
     for i in range(n_frames):
-        angular_accelerations[i] = pr.random_compact_axis_angle(random_state)
+        angular_accelerations[i] = pr.random_compact_axis_angle(rng)
     # Integrate angular accelerations to velocities
     angular_velocities = np.vstack(
         (np.zeros((1, 3)), np.cumsum(angular_accelerations * dt, axis=0)))
diff --git a/examples/animations/animate_quaternion_interpolation.py b/examples/animations/animate_quaternion_interpolation.py
index 04fc6df05..2ea025b68 100644
--- a/examples/animations/animate_quaternion_interpolation.py
+++ b/examples/animations/animate_quaternion_interpolation.py
@@ -10,9 +10,6 @@
 non-constant angular velocity. Usually it is a better idea to interpolate with
 slerp.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from mpl_toolkits.mplot3d import axes3d
diff --git a/examples/animations/animate_rotation.py b/examples/animations/animate_rotation.py
index 122978413..45bc90677 100644
--- a/examples/animations/animate_rotation.py
+++ b/examples/animations/animate_rotation.py
@@ -5,9 +5,6 @@
 
 Animates a rotation about the x-axis.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from mpl_toolkits.mplot3d import axes3d
diff --git a/examples/animations/animate_trajectory.py b/examples/animations/animate_trajectory.py
index 5748e569d..1289f838c 100644
--- a/examples/animations/animate_trajectory.py
+++ b/examples/animations/animate_trajectory.py
@@ -5,9 +5,6 @@
 
 Animates a trajectory.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from mpl_toolkits.mplot3d import axes3d
diff --git a/examples/plots/plot_axis_angle.py b/examples/plots/plot_axis_angle.py
index 3201f894c..ffc77302f 100644
--- a/examples/plots/plot_axis_angle.py
+++ b/examples/plots/plot_axis_angle.py
@@ -7,9 +7,6 @@
 Here we see a frame that is rotated in multiple steps around a rotation
 axis.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.rotations import (random_axis_angle, matrix_from_axis_angle,
diff --git a/examples/plots/plot_axis_angle_from_two_vectors.py b/examples/plots/plot_axis_angle_from_two_vectors.py
index 596e45c12..7c7e4a01c 100644
--- a/examples/plots/plot_axis_angle_from_two_vectors.py
+++ b/examples/plots/plot_axis_angle_from_two_vectors.py
@@ -10,9 +10,6 @@
 and its corresponding frame after the rotation are represented by shorter
 lines.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.rotations import (
diff --git a/examples/plots/plot_bivector.py b/examples/plots/plot_bivector.py
index 51387fb6d..5f18594f6 100644
--- a/examples/plots/plot_bivector.py
+++ b/examples/plots/plot_bivector.py
@@ -12,9 +12,6 @@
 that can be extracted by rearranging the elements of the bivector and
 normalizing the vector.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 import pytransform3d.rotations as pr
diff --git a/examples/plots/plot_box.py b/examples/plots/plot_box.py
index 461028851..59c492e5d 100644
--- a/examples/plots/plot_box.py
+++ b/examples/plots/plot_box.py
@@ -3,9 +3,6 @@
 Plot Box
 ========
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.plot_utils import (plot_box, plot_length_variable,
diff --git a/examples/plots/plot_camera_3d.py b/examples/plots/plot_camera_3d.py
index 077843e43..3bd3785ad 100644
--- a/examples/plots/plot_camera_3d.py
+++ b/examples/plots/plot_camera_3d.py
@@ -7,9 +7,6 @@
 show the camera center, a virtual image plane at a desired distance to the
 camera center, and the top direction of the virtual image plane.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 import pytransform3d.camera as pc
diff --git a/examples/plots/plot_camera_projection.py b/examples/plots/plot_camera_projection.py
index f358fd007..f0455efb2 100644
--- a/examples/plots/plot_camera_projection.py
+++ b/examples/plots/plot_camera_projection.py
@@ -7,9 +7,6 @@
 coordinate system which will be projected on the sensor. From the coordinates
 on the sensor we can compute the corresponding pixels.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.transformations import plot_transform
diff --git a/examples/plots/plot_camera_trajectory.py b/examples/plots/plot_camera_trajectory.py
index f7969003f..694a50519 100644
--- a/examples/plots/plot_camera_trajectory.py
+++ b/examples/plots/plot_camera_trajectory.py
@@ -7,9 +7,6 @@
 estimated from odometry. This specific trajectory has been used to reconstruct
 a colored mesh from a depth camera and an RGB camera.
 """
-print(__doc__)
-
-
 import os
 import numpy as np
 import matplotlib.pyplot as plt
diff --git a/examples/plots/plot_camera_with_image.py b/examples/plots/plot_camera_with_image.py
index c24762d97..e0281aab3 100644
--- a/examples/plots/plot_camera_with_image.py
+++ b/examples/plots/plot_camera_with_image.py
@@ -8,9 +8,6 @@
 the camera in the world, we can easily compute the location of the points on
 the image.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.plot_utils import make_3d_axis
diff --git a/examples/plots/plot_compare_rotations.py b/examples/plots/plot_compare_rotations.py
index e4135efc2..99cb5c2e3 100644
--- a/examples/plots/plot_compare_rotations.py
+++ b/examples/plots/plot_compare_rotations.py
@@ -3,9 +3,6 @@
 Compare Various Definitions of Rotations
 ========================================
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d import rotations as pr
diff --git a/examples/plots/plot_concatenate_uncertain_transforms.py b/examples/plots/plot_concatenate_uncertain_transforms.py
new file mode 100644
index 000000000..af30d5fbc
--- /dev/null
+++ b/examples/plots/plot_concatenate_uncertain_transforms.py
@@ -0,0 +1,96 @@
+"""
+================================
+Concatenate Uncertain Transforms
+================================
+
+Each of the poses has an associated covariance that is considered.
+In this example, we assume that a robot is moving with constant velocity
+along the x-axis, however, there is noise in the orientation of the robot
+that accumulates and leads to different paths when sampling. Uncertainty
+accumulation leads to the so-called banana distribution, which does not seem
+Gaussian in Cartesian space, but it is Gaussian in exponential coordinate
+space of SO(3).
+
+This example adapted and modified to 3D from
+
+Barfoot, Furgale: Associating Uncertainty With Three-Dimensional Poses for Use
+in Estimation Problems, http://ncfrn.mcgill.ca/members/pubs/barfoot_tro14.pdf
+
+The banana distribution was analyzed in detail by
+
+Long, Wolfe, Mashner, Chirikjian: The Banana Distribution is Gaussian:
+A Localization Study with Exponential Coordinates,
+http://www.roboticsproceedings.org/rss08/p34.pdf
+"""
+import numpy as np
+import matplotlib.pyplot as plt
+import pytransform3d.transformations as pt
+import pytransform3d.trajectories as ptr
+import pytransform3d.uncertainty as pu
+import pytransform3d.plot_utils as ppu
+
+
+rng = np.random.default_rng(0)
+cov_pose_chol = np.diag([0, 0.02, 0.03, 0, 0, 0])
+cov_pose = np.dot(cov_pose_chol, cov_pose_chol.T)
+velocity_vector = np.array([0, 0, 0, 1.0, 0, 0])
+T_vel = pt.transform_from_exponential_coordinates(velocity_vector)
+n_steps = 100
+n_mc_samples = 1000
+n_skip_trajectories = 1  # plot every n-th trajectory
+
+T_est = np.eye(4)
+path = np.zeros((n_steps + 1, 6))
+path[0] = pt.exponential_coordinates_from_transform(T_est)
+cov_est = np.zeros((6, 6))
+for t in range(n_steps):
+    T_est, cov_est = pu.concat_globally_uncertain_transforms(
+        T_est, cov_est, T_vel, cov_pose)
+    path[t + 1] = pt.exponential_coordinates_from_transform(T_est)
+
+T = np.eye(4)
+mc_path = np.zeros((n_steps + 1, n_mc_samples, 4, 4))
+mc_path[0, :] = T
+for t in range(n_steps):
+    noise_samples = ptr.transforms_from_exponential_coordinates(
+        cov_pose_chol.dot(rng.standard_normal(size=(6, n_mc_samples))).T)
+    step_samples = ptr.concat_many_to_one(noise_samples, T_vel)
+    mc_path[t + 1] = np.einsum("nij,njk->nik", step_samples, mc_path[t])
+# Plot the random samples' trajectory lines (in a frame attached to the start)
+# same as mc_path[t, i, :3, :3].T.dot(mc_path[t, i, :3, 3]), but faster
+mc_path_vec = np.einsum(
+    "tinm,tin->tim", mc_path[:, :, :3, :3], mc_path[:, :, :3, 3])
+
+ax = ppu.make_3d_axis(100)
+
+for i in range(0, mc_path_vec.shape[1], n_skip_trajectories):
+    ax.plot(
+        mc_path_vec[:, i, 0], mc_path_vec[:, i, 1], mc_path_vec[:, i, 2],
+        lw=1, c="b", alpha=0.05)
+ax.scatter(
+    mc_path_vec[-1, :, 0], mc_path_vec[-1, :, 1], mc_path_vec[-1, :, 2],
+    s=3, c="b")
+
+ptr.plot_trajectory(ax, ptr.pqs_from_transforms(
+    ptr.transforms_from_exponential_coordinates(path)), s=5.0, lw=3)
+
+pu.plot_projected_ellipsoid(
+    ax, T_est, cov_est, wireframe=False, alpha=0.3, color="g", factor=3.0)
+pu.plot_projected_ellipsoid(
+    ax, T_est, cov_est, wireframe=True, alpha=0.5, color="g", factor=3.0)
+
+mean_mc = np.mean(mc_path_vec[-1, :], axis=0)
+cov_mc = np.cov(mc_path_vec[-1, :], rowvar=False)
+
+ellipsoid2origin, radii = pu.to_ellipsoid(mean_mc, cov_mc)
+ppu.plot_ellipsoid(
+    ax, 3.0 * radii, ellipsoid2origin, wireframe=False, alpha=0.3, color="r")
+ppu.plot_ellipsoid(
+    ax, 3.0 * radii, ellipsoid2origin, wireframe=True, alpha=0.5, color="r")
+
+plt.xlim((-5, 105))
+plt.ylim((-50, 50))
+plt.xlabel("x")
+plt.ylabel("y")
+ax.view_init(elev=70, azim=-90)
+plt.show()
diff --git a/examples/plots/plot_convention_rotation_global_local.py b/examples/plots/plot_convention_rotation_global_local.py
index 7c8cc9b33..28d827cd5 100644
--- a/examples/plots/plot_convention_rotation_global_local.py
+++ b/examples/plots/plot_convention_rotation_global_local.py
@@ -12,14 +12,14 @@
 from pytransform3d.rotations import (
     passive_matrix_from_angle, active_matrix_from_angle, plot_basis)
 
-plt.figure(figsize=(8, 8))
-plt.subplots_adjust(
-    left=0, right=1, bottom=0.05, top=0.95, wspace=0, hspace=0.3)
-axes = [plt.subplot(4, 3, 1 + i, projection="3d") for i in range(12)]
-for i in range(len(axes)):
-    plt.setp(axes[i], xlim=(-1, 1), ylim=(-1, 1), zlim=(-1, 1),
+# %%
+# Passive Extrinsic Rotations
+# ---------------------------
+plt.figure(figsize=(8, 3))
+axes = [plt.subplot(1, 3, 1 + i, projection="3d") for i in range(3)]
+for ax in axes:
+    plt.setp(ax, xlim=(-1, 1), ylim=(-1, 1), zlim=(-1, 1),
              xlabel="X", ylabel="Y", zlabel="Z")
-    axes[i].view_init(elev=20, azim=60)
 
 Rx45 = passive_matrix_from_angle(0, np.deg2rad(45))
 Rz45 = passive_matrix_from_angle(2, np.deg2rad(45))
@@ -30,27 +30,64 @@
 plot_basis(ax=axes[1], R=Rx45)
 axes[2].set_title("$R_z(45^{\circ}) R_x(45^{\circ})$", y=0.95)
 plot_basis(ax=axes[2], R=Rz45.dot(Rx45))
-axes[3].set_title("Passive Intrinsic Rotations", y=0.95)
-plot_basis(ax=axes[3], R=np.eye(3))
-axes[4].set_title("$R_x(45^{\circ})$", y=0.95)
-plot_basis(ax=axes[4], R=Rx45)
-axes[5].set_title("$R_x(45^{\circ}) R_{z'}(45^{\circ})$", y=0.95)
-plot_basis(ax=axes[5], R=Rx45.dot(Rz45))
+
+plt.tight_layout()
+
+# %%
+# Passive Intrinsic Rotations
+# ---------------------------
+plt.figure(figsize=(8, 3))
+axes = [plt.subplot(1, 3, 1 + i, projection="3d") for i in range(3)]
+for ax in axes:
+    plt.setp(ax, xlim=(-1, 1), ylim=(-1, 1), zlim=(-1, 1),
+             xlabel="X", ylabel="Y", zlabel="Z")
+
+axes[0].set_title("Passive Intrinsic Rotations", y=0.95)
+plot_basis(ax=axes[0], R=np.eye(3))
+axes[1].set_title("$R_x(45^{\circ})$", y=0.95)
+plot_basis(ax=axes[1], R=Rx45)
+axes[2].set_title("$R_x(45^{\circ}) R_{z'}(45^{\circ})$", y=0.95)
+plot_basis(ax=axes[2], R=Rx45.dot(Rz45))
+
+plt.tight_layout()
+
+# %%
+# Active Extrinsic Rotations
+# --------------------------
+plt.figure(figsize=(8, 3))
+axes = [plt.subplot(1, 3, 1 + i, projection="3d") for i in range(3)]
+for ax in axes:
+    plt.setp(ax, xlim=(-1, 1), ylim=(-1, 1), zlim=(-1, 1),
+             xlabel="X", ylabel="Y", zlabel="Z")
 
 Rx45 = active_matrix_from_angle(0, np.deg2rad(45))
 Rz45 = active_matrix_from_angle(2, np.deg2rad(45))
 
-axes[6].set_title("Active Extrinsic Rotations", y=0.95)
-plot_basis(ax=axes[6], R=np.eye(3))
-axes[7].set_title("$R_x(45^{\circ})$", y=0.95)
-plot_basis(ax=axes[7], R=Rx45)
-axes[8].set_title("$R_z(45^{\circ}) R_x(45^{\circ})$", y=0.95)
-plot_basis(ax=axes[8], R=Rz45.dot(Rx45))
-axes[9].set_title("Active Intrinsic Rotations", y=0.95)
-plot_basis(ax=axes[9], R=np.eye(3))
-axes[10].set_title("$R_x(45^{\circ})$", y=0.95)
-plot_basis(ax=axes[10], R=Rx45)
-axes[11].set_title("$R_x(45^{\circ}) R_{z'}(45^{\circ})$", y=0.95)
-plot_basis(ax=axes[11], R=Rx45.dot(Rz45))
+axes[0].set_title("Active Extrinsic Rotations", y=0.95)
+plot_basis(ax=axes[0], R=np.eye(3))
+axes[1].set_title("$R_x(45^{\circ})$", y=0.95)
+plot_basis(ax=axes[1], R=Rx45)
+axes[2].set_title("$R_z(45^{\circ}) R_x(45^{\circ})$", y=0.95)
+plot_basis(ax=axes[2], R=Rz45.dot(Rx45))
+
+plt.tight_layout()
+
+# %%
+# Active Intrinsic Rotations
+# --------------------------
+plt.figure(figsize=(8, 3))
+axes = [plt.subplot(1, 3, 1 + i, projection="3d") for i in range(3)]
+for ax in axes:
+    plt.setp(ax, xlim=(-1, 1), ylim=(-1, 1), zlim=(-1, 1),
+             xlabel="X", ylabel="Y", zlabel="Z")
+
+axes[0].set_title("Active Intrinsic Rotations", y=0.95)
+plot_basis(ax=axes[0], R=np.eye(3))
+axes[1].set_title("$R_x(45^{\circ})$", y=0.95)
+plot_basis(ax=axes[1], R=Rx45)
+axes[2].set_title("$R_x(45^{\circ}) R_{z'}(45^{\circ})$", y=0.95)
+plot_basis(ax=axes[2], R=Rx45.dot(Rz45))
+
+plt.tight_layout()
 
 plt.show()
diff --git a/examples/plots/plot_cylinder.py b/examples/plots/plot_cylinder.py
index d3047b6cc..b5b2a08ef 100644
--- a/examples/plots/plot_cylinder.py
+++ b/examples/plots/plot_cylinder.py
@@ -5,9 +5,6 @@
 
 Plots surfaces of transformed cylindrical shells.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.transformations import transform_from, plot_transform
@@ -15,10 +12,10 @@
 from pytransform3d.plot_utils import plot_cylinder, remove_frame
 
 
-random_state = np.random.RandomState(42)
+rng = np.random.default_rng(44)
 A2B = transform_from(
-    R=matrix_from_axis_angle(random_axis_angle(random_state)),
-    p=random_state.randn(3))
+    R=matrix_from_axis_angle(random_axis_angle(rng)),
+    p=rng.standard_normal(size=3))
 ax = plot_cylinder(length=1.0, radius=0.3, thickness=0.1,
                    wireframe=False, alpha=0.2)
 plot_transform(ax=ax, A2B=np.eye(4), s=0.3, lw=3)
diff --git a/examples/plots/plot_dual_quaternion_interpolation.py b/examples/plots/plot_dual_quaternion_interpolation.py
index fb5ee1cbb..32c70dce2 100644
--- a/examples/plots/plot_dual_quaternion_interpolation.py
+++ b/examples/plots/plot_dual_quaternion_interpolation.py
@@ -10,9 +10,6 @@
 dual quaternions. The true screw motion from pose 1 to pose 2 is shown by
 a thick, transparent black line in the background of the ScLERP interpolation.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 import pytransform3d.transformations as pt
@@ -20,9 +17,9 @@
 import pytransform3d.plot_utils as ppu
 
 
-random_state = np.random.RandomState(21)
-pose1 = pt.random_transform(random_state)
-pose2 = pt.random_transform(random_state)
+rng = np.random.default_rng(25)
+pose1 = pt.random_transform(rng)
+pose2 = pt.random_transform(rng)
 dq1 = pt.dual_quaternion_from_transform(pose1)
 dq2 = -pt.dual_quaternion_from_transform(pose2)
 Stheta1 = pt.exponential_coordinates_from_transform(pose1)
diff --git a/examples/plots/plot_euler_angles.py b/examples/plots/plot_euler_angles.py
index e4f11b46a..246b9544c 100644
--- a/examples/plots/plot_euler_angles.py
+++ b/examples/plots/plot_euler_angles.py
@@ -6,9 +6,6 @@
 Any rotation can be represented by three consecutive rotations about three
 basis vectors. Here we use the extrinsic xyz convention.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d import rotations as pr
diff --git a/examples/plots/plot_frames.py b/examples/plots/plot_frames.py
index 33b932418..833312974 100644
--- a/examples/plots/plot_frames.py
+++ b/examples/plots/plot_frames.py
@@ -7,9 +7,6 @@
 We will add several transforms to the manager and plot all frames in
 two reference frames ('world' and 'A').
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.plot_utils import make_3d_axis
@@ -17,11 +14,11 @@
 from pytransform3d.transform_manager import TransformManager
 
 
-random_state = np.random.RandomState(0)
-A2world = random_transform(random_state)
-B2world = random_transform(random_state)
-A2C = random_transform(random_state)
-D2B = random_transform(random_state)
+rng = np.random.default_rng(5)
+A2world = random_transform(rng)
+B2world = random_transform(rng)
+A2C = random_transform(rng)
+D2B = random_transform(rng)
 
 tm = TransformManager()
 tm.add_transform("A", "world", A2world)
@@ -31,7 +28,7 @@
 
 plt.figure(figsize=(10, 5))
 
-ax = make_3d_axis(3, 121)
+ax = make_3d_axis(2, 121)
 ax = tm.plot_frames_in("world", ax=ax, alpha=0.6)
 ax.view_init(30, 20)
 
diff --git a/examples/plots/plot_invert_uncertain_transform.py b/examples/plots/plot_invert_uncertain_transform.py
new file mode 100644
index 000000000..cba2796c4
--- /dev/null
+++ b/examples/plots/plot_invert_uncertain_transform.py
@@ -0,0 +1,64 @@
+"""
+==========================
+Invert Uncertain Transform
+==========================
+
+We sample from the original transform distribution and from the inverse
+distribution. Samples are then projected to all 2D planes and plotted.
+The color indicates the pose distribution. Green is the original
+distribution and red is the inverse.
+"""
+import numpy as np
+import matplotlib.pyplot as plt
+import pytransform3d.uncertainty as pu
+import pytransform3d.rotations as pr
+import pytransform3d.transformations as pt
+import pytransform3d.trajectories as ptr
+
+
+n_mc_samples = 1000
+rng = np.random.default_rng(1)
+alpha = 2.0
+
+A2B = pt.transform_from(
+    R=pr.matrix_from_euler([1.5, 0.5, 1.3], 0, 1, 2, True),
+    p=[10.0, -7.0, -5.0]
+)
+variances = alpha * np.array([0.1, 0.2, 0.1, 2.0, 1.0, 1.0])
+std_devs = np.sqrt(variances)
+correlations = np.array([
+    [0.5, 0, 0, 0, 0, 0],
+    [0.1, 0.5, 0, 0, 0, 0],
+    [-0.3, 0.1, 0.5, 0, 0, 0],
+    [-0.1, 0.2, 0.3, 0.5, 0, 0],
+    [-0.3, -0.1, -0.2, 0.3, 0.5, 0],
+    [-0.1, 0.1, 0.2, 0.1, 0.3, 0.5]
+])
+correlations += correlations.T
+cov_A2B = correlations * np.outer(std_devs, std_devs)
+x_A2B = pt.exponential_coordinates_from_transform(A2B)
+samples_A2B = ptr.exponential_coordinates_from_transforms(
+    [pt.random_transform(rng, A2B, cov_A2B) for _ in range(n_mc_samples)])
+
+B2A, cov_B2A = pu.invert_uncertain_transform(A2B, cov_A2B)
+x_B2A = pt.exponential_coordinates_from_transform(B2A)
+samples_B2A = ptr.exponential_coordinates_from_transforms(
+    [pt.random_transform(rng, B2A, cov_B2A) for _ in range(n_mc_samples)])
+
+_, axes = plt.subplots(nrows=6, ncols=6, squeeze=True, figsize=(10, 10))
+for i in range(6):
+    for j in range(6):
+        if i == j:
+            continue
+
+        indices = np.array([i, j])
+        ax = axes[i][j]
+
+        ax.scatter(
+            samples_A2B[:, i], samples_A2B[:, j], color="g", s=1, alpha=0.3)
+        ax.scatter(
+            samples_B2A[:, i], samples_B2A[:, j], color="r", s=1, alpha=0.3)
+
+        ax.scatter(0, 0, color="k")
+
+plt.show()
diff --git a/examples/plots/plot_matrix_from_two_vectors.py b/examples/plots/plot_matrix_from_two_vectors.py
index c2a966918..28d52ff2f 100644
--- a/examples/plots/plot_matrix_from_two_vectors.py
+++ b/examples/plots/plot_matrix_from_two_vectors.py
@@ -8,9 +8,6 @@
 normalized vector rejection of b on a, and the z-axis is the cross product of
 the other basis vectors.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.rotations import (
@@ -18,9 +15,9 @@
 from pytransform3d.plot_utils import plot_vector
 
 
-random_state = np.random.RandomState(1)
-a = random_vector(random_state, 3) * 0.3
-b = random_vector(random_state, 3) * 0.3
+rng = np.random.default_rng(1)
+a = random_vector(rng, 3) * 0.3
+b = random_vector(rng, 3) * 0.3
 R = matrix_from_two_vectors(a, b)
 
 ax = plot_vector(direction=a, color="r")
diff --git a/examples/plots/plot_mesh.py b/examples/plots/plot_mesh.py
index 052c7930d..55007422d 100644
--- a/examples/plots/plot_mesh.py
+++ b/examples/plots/plot_mesh.py
@@ -7,9 +7,6 @@
 run from within the main folder because it uses a
 hard-coded path to the STL file.
 """
-print(__doc__)
-
-
 import os
 import numpy as np
 import matplotlib.pyplot as plt
diff --git a/examples/plots/plot_pose_fusion.py b/examples/plots/plot_pose_fusion.py
new file mode 100644
index 000000000..a24a22973
--- /dev/null
+++ b/examples/plots/plot_pose_fusion.py
@@ -0,0 +1,139 @@
+"""
+============
+Fuse 3 Poses
+============
+
+Each of the poses is has an associated covariance that is considered during
+the fusion. Each of the plots shows a projection of the distributions in
+exponential coordinate space to two dimensions. Red, green, and blue ellipses
+represent the uncertain poses that will be fused. A black ellipse indicates
+the fused pose's distribution.
+
+This example is from
+
+Barfoot, Furgale: Associating Uncertainty With Three-Dimensional Poses for Use
+in Estimation Problems, http://ncfrn.mcgill.ca/members/pubs/barfoot_tro14.pdf
+"""
+import numpy as np
+import matplotlib.pyplot as plt
+import pytransform3d.uncertainty as pu
+import pytransform3d.transformations as pt
+
+
+def to_ellipse(cov, factor=1.0):
+    """Compute error ellipse.
+
+    An error ellipse shows equiprobable points of a 2D Gaussian distribution.
+
+    Parameters
+    ----------
+    cov : array-like, shape (2, 2)
+        Covariance of the Gaussian distribution.
+
+    factor : float, optional (default: 1)
+        One means standard deviation.
+
+    Returns
+    -------
+    angle : float
+        Rotation angle of the ellipse.
+
+    width : float
+        Width of the ellipse (semi axis, not diameter).
+
+    height : float
+        Height of the ellipse (semi axis, not diameter).
+    """
+    from scipy import linalg
+    vals, vecs = linalg.eigh(cov)
+    order = vals.argsort()[::-1]
+    vals, vecs = vals[order], vecs[:, order]
+    angle = np.arctan2(*vecs[:, 0][::-1])
+    width, height = factor * np.sqrt(vals)
+    return angle, width, height
+
+
+def plot_error_ellipse(ax, mean, cov, color=None, alpha=0.25,
+                       factors=np.linspace(0.25, 2.0, 8)):
+    """Plot error ellipse of MVN.
+
+    Parameters
+    ----------
+    ax : axis
+        Matplotlib axis.
+
+    mean : array-like, shape (2,)
+        Mean of the Gaussian distribution.
+
+    cov : array-like, shape (2, 2)
+        Covariance of the Gaussian distribution.
+
+    color : str, optional (default: None)
+        Color in which the ellipse should be plotted
+
+    alpha : float, optional (default: 0.25)
+        Alpha value for ellipse
+
+    factors : array, optional (default: np.linspace(0.25, 2.0, 8))
+        Multiples of the standard deviations that should be plotted.
+    """
+    from matplotlib.patches import Ellipse
+    for factor in factors:
+        angle, width, height = to_ellipse(cov, factor)
+        ell = Ellipse(xy=mean, width=2.0 * width, height=2.0 * height,
+                      angle=np.degrees(angle))
+        ell.set_alpha(alpha)
+        if color is not None:
+            ell.set_color(color)
+        ax.add_artist(ell)
+
+
+x_true = np.array([1.0, 0.0, 0.0, 0.0, 0.0, np.pi / 6.0])
+T_true = pt.transform_from_exponential_coordinates(x_true)
+alpha = 5.0
+cov1 = alpha * np.diag([0.1, 0.2, 0.1, 2.0, 1.0, 1.0])
+cov2 = alpha * np.diag([0.1, 0.1, 0.2, 1.0, 3.0, 1.0])
+cov3 = alpha * np.diag([0.2, 0.1, 0.1, 1.0, 1.0, 5.0])
+
+rng = np.random.default_rng(0)
+
+T1 = np.dot(pt.transform_from_exponential_coordinates(
+    pt.random_exponential_coordinates(rng=rng, cov=cov1)), T_true)
+T2 = np.dot(pt.transform_from_exponential_coordinates(
+    pt.random_exponential_coordinates(rng=rng, cov=cov2)), T_true)
+T3 = np.dot(pt.transform_from_exponential_coordinates(
+    pt.random_exponential_coordinates(rng=rng, cov=cov3)), T_true)
+
+x1 = pt.exponential_coordinates_from_transform(T1)
+x2 = pt.exponential_coordinates_from_transform(T2)
+x3 = pt.exponential_coordinates_from_transform(T3)
+
+T_est, cov_est, V = pu.pose_fusion([T1, T2, T3], [cov1, cov2, cov3])
+x_est = pt.exponential_coordinates_from_transform(T_est)
+
+_, axes = plt.subplots(
+    nrows=6, ncols=6, sharex=True, sharey=True, squeeze=True, figsize=(10, 10))
+factors = [1.0]
+for i in range(6):
+    for j in range(6):
+        if i == j:
+            continue
+
+        indices = np.array([i, j])
+        ax = axes[i][j]
+
+        for x, cov, color in zip([x1, x2, x3], [cov1, cov2, cov3], "rgb"):
+            plot_error_ellipse(
+                ax, x[indices], cov[indices][:, indices],
+                color=color, alpha=0.4, factors=factors)
+
+        plot_error_ellipse(
+            ax, x_est[indices], cov_est[indices][:, indices],
+            color="k", alpha=1, factors=factors)
+
+        ax.scatter(x_true[i], x_true[j])
+
+        ax.set_xlim((-10, 10))
+        ax.set_ylim((-10, 10))
+
+plt.show()
diff --git a/examples/plots/plot_pose_trajectory.py b/examples/plots/plot_pose_trajectory.py
index 919cf86d7..88892d185 100644
--- a/examples/plots/plot_pose_trajectory.py
+++ b/examples/plots/plot_pose_trajectory.py
@@ -5,9 +5,6 @@
 
 Plotting pose trajectories with pytransform3d is easy.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.batch_rotations import quaternion_slerp_batch
diff --git a/examples/plots/plot_random_geometries.py b/examples/plots/plot_random_geometries.py
index 5a694ae2e..60e34ff2c 100644
--- a/examples/plots/plot_random_geometries.py
+++ b/examples/plots/plot_random_geometries.py
@@ -5,9 +5,6 @@
 
 Plotting of several geometric shapes is directly supported by the library.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.plot_utils import (
@@ -17,43 +14,43 @@
     random_transform, plot_transform, translate_transform)
 
 
-random_state = np.random.RandomState(2823)
+rng = np.random.default_rng(2834)
 ax = make_3d_axis(2)
 
-box2origin = random_transform(random_state)
-size = random_state.rand(3) * 3
+box2origin = random_transform(rng)
+size = rng.random(size=3) * 3
 plot_transform(ax=ax, A2B=box2origin, s=0.3)
 plot_box(ax=ax, A2B=box2origin, size=size, color="b", alpha=0.5,
          wireframe=False)
 
-p = random_state.randn(3)
-radius = float(random_state.rand())
+p = rng.standard_normal(size=3)
+radius = float(rng.random())
 plot_transform(ax=ax, A2B=translate_transform(np.eye(4), p), s=0.3)
 plot_sphere(ax=ax, p=p, radius=radius, color="y", alpha=0.5, wireframe=False)
 
-cylinder2origin = random_transform(random_state)
-length = float(random_state.rand()) * 5
-radius = float(random_state.rand())
+cylinder2origin = random_transform(rng)
+length = float(rng.random()) * 5
+radius = float(rng.random())
 plot_transform(ax=ax, A2B=cylinder2origin, s=0.3)
 plot_cylinder(ax=ax, A2B=cylinder2origin, length=length, radius=radius,
               color="g", alpha=0.5, wireframe=False)
 
-ellipsoid2origin = random_transform(random_state)
-radii = random_state.rand(3) * 3
+ellipsoid2origin = random_transform(rng)
+radii = rng.random(size=3) * 3
 plot_transform(ax=ax, A2B=ellipsoid2origin, s=0.3)
 plot_ellipsoid(ax=ax, A2B=ellipsoid2origin, radii=radii, color="m", alpha=0.5,
                wireframe=False)
 
-capsule2origin = random_transform(random_state)
-height = float(random_state.rand()) * 2
-radius = float(random_state.rand())
+capsule2origin = random_transform(rng)
+height = float(rng.random()) * 2
+radius = float(rng.random())
 plot_transform(ax=ax, A2B=capsule2origin, s=0.3)
 plot_capsule(ax=ax, A2B=capsule2origin, height=height, radius=radius,
              color="r", alpha=0.5, wireframe=False)
 
-cone2origin = random_transform(random_state)
-height = float(random_state.rand()) * 5
-radius = float(random_state.rand())
+cone2origin = random_transform(rng)
+height = float(rng.random()) * 5
+radius = float(rng.random())
 plot_transform(ax=ax, A2B=cone2origin, s=0.3)
 plot_cone(ax=ax, A2B=cone2origin, height=height, radius=radius, color="c",
           alpha=0.5, wireframe=False)
diff --git a/examples/plots/plot_robot.py b/examples/plots/plot_robot.py
index a80e91e9d..cbcf9d851 100644
--- a/examples/plots/plot_robot.py
+++ b/examples/plots/plot_robot.py
@@ -5,9 +5,6 @@
 
 We see a 6-DOF robot arm with visuals.
 """
-print(__doc__)
-
-
 import os
 import numpy as np
 import matplotlib.pyplot as plt
diff --git a/examples/plots/plot_rotate_cylinder.py b/examples/plots/plot_rotate_cylinder.py
index 1d9e9a1e7..28c73809f 100644
--- a/examples/plots/plot_rotate_cylinder.py
+++ b/examples/plots/plot_rotate_cylinder.py
@@ -6,9 +6,6 @@
 In this example, we apply a constant torque (tau) to a cylinder at its
 center of gravity and plot it at several steps during the acceleration.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.rotations import matrix_from_compact_axis_angle
@@ -22,7 +19,6 @@ def inertia_of_cylinder(mass, length, radius):
     return np.eye(3) * np.array([I_xx, I_yy, I_zz])
 
 
-random_state = np.random.RandomState(42)
 A2B = np.eye(4)
 
 length = 1.0
diff --git a/examples/plots/plot_sample_rotations.py b/examples/plots/plot_sample_rotations.py
new file mode 100644
index 000000000..10d9b7bb2
--- /dev/null
+++ b/examples/plots/plot_sample_rotations.py
@@ -0,0 +1,41 @@
+"""
+=================================
+Compare Rotation Sampling Methods
+=================================
+
+There are different ways of sampling rotations. We draw random samples of
+rotations, convert them to rotation matrices, and apply these to the basis
+vector (1, 0, 0) to obtain points on the unit sphere.
+"""
+import numpy as np
+import matplotlib.pyplot as plt
+import pytransform3d.rotations as pr
+import pytransform3d.batch_rotations as pbr
+import pytransform3d.plot_utils as ppu
+
+
+rng = np.random.default_rng(1223532)
+n_samples = 2000
+v = np.array([1.0, 0.0, 0.0])
+
+plt.figure(figsize=(9, 5))
+
+ax1 = plt.subplot(121, projection="3d")
+ax1.set_title("Sampled rotation vectors (axis-angle)")
+ppu.plot_sphere(ax1, radius=1, n_steps=100)
+rotations = np.vstack([pr.random_compact_axis_angle(rng)
+                       for _ in range(n_samples)])
+R = pbr.matrices_from_compact_axis_angles(rotations)
+v_R = np.einsum("nij,j->ni", R, v)
+ax1.scatter(v_R[:, 0], v_R[:, 1], v_R[:, 2])
+
+ax2 = plt.subplot(122, projection="3d")
+ax2.set_title("Sampled quaternions")
+ppu.plot_sphere(ax2, radius=1, n_steps=100)
+rotations = np.vstack([pr.random_quaternion(rng) for _ in range(n_samples)])
+R = pbr.matrices_from_quaternions(rotations)
+v_R = np.einsum("nij,j->ni", R, v)
+ax2.scatter(v_R[:, 0], v_R[:, 1], v_R[:, 2])
+
+plt.tight_layout()
+plt.show()
diff --git a/examples/plots/plot_sample_transforms.py b/examples/plots/plot_sample_transforms.py
new file mode 100644
index 000000000..529af109d
--- /dev/null
+++ b/examples/plots/plot_sample_transforms.py
@@ -0,0 +1,19 @@
+"""
+=================
+Sample Transforms
+=================
+"""
+import numpy as np
+import matplotlib.pyplot as plt
+import pytransform3d.transformations as pt
+
+
+mean = pt.transform_from(R=np.eye(3), p=np.array([0.0, 0.0, 0.5]))
+cov = np.diag([0.001, 0.001, 0.5, 0.001, 0.001, 0.001])
+rng = np.random.default_rng(0)
+ax = None
+poses = np.array([
+    pt.random_transform(rng=rng, mean=mean, cov=cov) for _ in range(1000)])
+for pose in poses:
+    ax = pt.plot_transform(ax=ax, A2B=pose, s=0.3)
+plt.show()
diff --git a/examples/plots/plot_screw.py b/examples/plots/plot_screw.py
index 54ef5a507..cf9f69269 100644
--- a/examples/plots/plot_screw.py
+++ b/examples/plots/plot_screw.py
@@ -15,9 +15,6 @@
 arrow shows the direction of the screw axis. The spiral path represents
 a displacement of length theta along the screw axis.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.rotations import active_matrix_from_extrinsic_roll_pitch_yaw
diff --git a/examples/plots/plot_sphere.py b/examples/plots/plot_sphere.py
index f2050ae9f..63f0a5cbb 100644
--- a/examples/plots/plot_sphere.py
+++ b/examples/plots/plot_sphere.py
@@ -3,18 +3,14 @@
 Plot Sphere
 ===========
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.transformations import plot_transform
 from pytransform3d.plot_utils import plot_sphere, remove_frame
 
 
-random_state = np.random.RandomState(42)
 ax = plot_sphere(
-    radius=0.5, wireframe=False, alpha=0.1, color="k", n_steps=20, ax_s=0.5)
+    radius=0.5, wireframe=False, alpha=0.1, color="k", ax_s=0.5)
 plot_sphere(ax=ax, radius=0.5, wireframe=True)
 plot_transform(ax=ax, A2B=np.eye(4), s=0.3, lw=3)
 remove_frame(ax)
diff --git a/examples/plots/plot_spheres.py b/examples/plots/plot_spheres.py
new file mode 100644
index 000000000..3dbeffa32
--- /dev/null
+++ b/examples/plots/plot_spheres.py
@@ -0,0 +1,38 @@
+"""
+=====================
+Plot Multiple Spheres
+=====================
+
+Benchmarks plotting of multiple spheres at once and compares it to plotting
+each sphere individually.
+"""
+import numpy as np
+import matplotlib.pyplot as plt
+from pytransform3d.plot_utils import plot_sphere, plot_spheres
+import time
+
+
+n_spheres = 50
+random_state = np.random.default_rng(0)
+P = 2 * random_state.random((n_spheres, 3)) - 1
+radii = random_state.random(n_spheres) / 2
+colors = random_state.random((n_spheres, 3))
+alphas = random_state.random(n_spheres)
+
+start = time.time()
+plot_spheres(p=P, radius=radii, color=colors, alpha=alphas, wireframe=False)
+end = time.time()
+time_multi = end - start
+
+start = time.time()
+for p, radius, color, alpha in zip(P, radii, colors, alphas):
+    plot_sphere(p=p, radius=radius, color=color, alpha=alpha, wireframe=False)
+end = time.time()
+time_single = end - start
+
+speedup = time_single / time_multi
+
+print("n_spheres", "single", "\t", "multi", "\t", "speedup", sep="\t")
+print(n_spheres, "", time_single, time_multi, speedup, sep="\t")
+
+plt.show()
diff --git a/examples/plots/plot_spherical_grid.py b/examples/plots/plot_spherical_grid.py
index 122afd3c1..e9d080eae 100644
--- a/examples/plots/plot_spherical_grid.py
+++ b/examples/plots/plot_spherical_grid.py
@@ -17,7 +17,8 @@
 thetas, phis = np.meshgrid(np.linspace(0, np.pi, 11),
                            np.linspace(-np.pi, np.pi, 21))
 rhos = np.ones_like(thetas)
-spherical_grid = np.column_stack((rhos.reshape(-1), thetas.reshape(-1), phis.reshape(-1)))
+spherical_grid = np.column_stack((
+    rhos.reshape(-1), thetas.reshape(-1), phis.reshape(-1)))
 cartesian_grid = pc.cartesian_from_spherical(spherical_grid)
 
 ax = make_3d_axis(ax_s=1, unit="m", n_ticks=6)
diff --git a/examples/plots/plot_straight_line_path.py b/examples/plots/plot_straight_line_path.py
index f65fb8213..8e06a332a 100644
--- a/examples/plots/plot_straight_line_path.py
+++ b/examples/plots/plot_straight_line_path.py
@@ -7,9 +7,6 @@
 coordinates. This is a demonstration of batch conversion from exponential
 coordinates to transformation matrices.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d.plot_utils import Trajectory, make_3d_axis, remove_frame
diff --git a/examples/plots/plot_transform.py b/examples/plots/plot_transform.py
index 877261761..efbcabc26 100644
--- a/examples/plots/plot_transform.py
+++ b/examples/plots/plot_transform.py
@@ -6,9 +6,6 @@
 We can display transformations by plotting the basis vectors of the
 corresponding coordinate frame.
 """
-print(__doc__)
-
-
 import matplotlib.pyplot as plt
 from pytransform3d.transformations import plot_transform
 from pytransform3d.plot_utils import make_3d_axis
diff --git a/examples/plots/plot_transform_concatenation.py b/examples/plots/plot_transform_concatenation.py
index 173fe7f95..9ae13a141 100644
--- a/examples/plots/plot_transform_concatenation.py
+++ b/examples/plots/plot_transform_concatenation.py
@@ -7,9 +7,6 @@
 the transform C2B and B2A. We can construct a transform C2A to extract the
 position of p in frame A.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 import pytransform3d.rotations as pyrot
diff --git a/examples/plots/plot_transform_manager.py b/examples/plots/plot_transform_manager.py
index 7c959ca0d..a252b9509 100644
--- a/examples/plots/plot_transform_manager.py
+++ b/examples/plots/plot_transform_manager.py
@@ -6,9 +6,6 @@
 In this example, we will use the TransformManager to infer a transformation
 automatically.
 """
-print(__doc__)
-
-
 import numpy as np
 import matplotlib.pyplot as plt
 from pytransform3d import rotations as pr
@@ -16,11 +13,11 @@
 from pytransform3d.transform_manager import TransformManager
 
 
-random_state = np.random.RandomState(0)
+rng = np.random.default_rng(1)
 
 ee2robot = pt.transform_from_pq(
     np.hstack((np.array([0.4, -0.3, 0.5]),
-               pr.random_quaternion(random_state))))
+               pr.random_quaternion(rng))))
 cam2robot = pt.transform_from_pq(
     np.hstack((np.array([0.0, 0.0, 0.8]), pr.q_id)))
 object2cam = pt.transform_from(
diff --git a/examples/plots/plot_urdf.py b/examples/plots/plot_urdf.py
index e52a7d50f..2c5886677 100644
--- a/examples/plots/plot_urdf.py
+++ b/examples/plots/plot_urdf.py
@@ -10,6 +10,10 @@
 from pytransform3d.urdf import UrdfTransformManager
 
 
+# %%
+# URDF
+# ----
+# URDFs have to be provided as strings. These can be loaded from files.
 COMPI_URDF = """
 <?xml version="1.0"?>
   <robot name="compi">
@@ -74,10 +78,22 @@
 
 tm = UrdfTransformManager()
 tm.load_urdf(COMPI_URDF)
+
+# %%
+# Set Joints
+# ----------
+# In this example, we define joint angles manually. Joints are identified by
+# their string names extracted from the URDF.
 joint_names = ["joint%d" % i for i in range(1, 7)]
 joint_angles = [0, 0.5, 0.5, 0, 0.5, 0]
 for name, angle in zip(joint_names, joint_angles):
     tm.set_joint(name, angle)
+
+# %%
+# Visualization
+# -------------
+# We use matplotlib to visualize the link frames of the URDF and connections
+# between these.
 ax = tm.plot_frames_in("compi", whitelist=["link%d" % d for d in range(1, 7)],
                        s=0.05, show_name=True)
 ax = tm.plot_connections_in("compi", ax=ax)
diff --git a/examples/plots/plot_vector.py b/examples/plots/plot_vector.py
index b89861d8b..ef8aa09be 100644
--- a/examples/plots/plot_vector.py
+++ b/examples/plots/plot_vector.py
@@ -2,8 +2,6 @@
 ===========
 Plot Vector
 ===========
-
-Does this need more documentation?
 """
 import numpy as np
 import matplotlib.pyplot as plt
diff --git a/examples/visualizations/vis_add_remove_artist.py b/examples/visualizations/vis_add_remove_artist.py
index 35f9db0ba..543046714 100644
--- a/examples/visualizations/vis_add_remove_artist.py
+++ b/examples/visualizations/vis_add_remove_artist.py
@@ -5,9 +5,6 @@
 
 A demonstration of how previously added artists can be removed.
 """
-print(__doc__)
-
-
 import os
 from pytransform3d.urdf import UrdfTransformManager
 import pytransform3d.visualizer as pv
diff --git a/examples/visualizations/vis_basis.py b/examples/visualizations/vis_basis.py
index 67078767f..95f385d56 100644
--- a/examples/visualizations/vis_basis.py
+++ b/examples/visualizations/vis_basis.py
@@ -3,9 +3,6 @@
 Visualize Coordinate Frame
 ==========================
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
diff --git a/examples/visualizations/vis_box.py b/examples/visualizations/vis_box.py
index 7dc0baa06..4e13af951 100644
--- a/examples/visualizations/vis_box.py
+++ b/examples/visualizations/vis_box.py
@@ -3,19 +3,16 @@
 Plot Box
 ========
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 from pytransform3d.transformations import transform_from
 from pytransform3d.rotations import random_axis_angle, matrix_from_axis_angle
 
 
-random_state = np.random.RandomState(42)
+rng = np.random.default_rng(42)
 fig = pv.figure()
 A2B = transform_from(
-    R=matrix_from_axis_angle(random_axis_angle(random_state)),
+    R=matrix_from_axis_angle(random_axis_angle(rng)),
     p=np.zeros(3))
 fig.plot_box(size=[0.2, 0.5, 1], A2B=A2B)
 fig.plot_transform(A2B=A2B)
diff --git a/examples/visualizations/vis_camera_3d.py b/examples/visualizations/vis_camera_3d.py
index c82eb710a..50774b0b4 100644
--- a/examples/visualizations/vis_camera_3d.py
+++ b/examples/visualizations/vis_camera_3d.py
@@ -7,9 +7,6 @@
 show the camera center, a virtual image plane at a desired distance to the
 camera center, and the top direction of the virtual image plane.
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.transformations as pt
 import pytransform3d.visualizer as pv
diff --git a/examples/visualizations/vis_capsule.py b/examples/visualizations/vis_capsule.py
index dce246603..5e785bf97 100644
--- a/examples/visualizations/vis_capsule.py
+++ b/examples/visualizations/vis_capsule.py
@@ -3,9 +3,6 @@
 Visualize Capsule
 =================
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
diff --git a/examples/visualizations/vis_cone.py b/examples/visualizations/vis_cone.py
index 09fb11257..6f8c97643 100644
--- a/examples/visualizations/vis_cone.py
+++ b/examples/visualizations/vis_cone.py
@@ -3,9 +3,6 @@
 Visualize Cone
 ==============
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
diff --git a/examples/visualizations/vis_cylinder.py b/examples/visualizations/vis_cylinder.py
index f4bc7e233..edcb62bd1 100644
--- a/examples/visualizations/vis_cylinder.py
+++ b/examples/visualizations/vis_cylinder.py
@@ -5,9 +5,6 @@
 
 Plots transformed cylinders.
 """
-print(__doc__)
-
-
 import numpy as np
 from pytransform3d.transformations import transform_from
 from pytransform3d.rotations import random_axis_angle, matrix_from_axis_angle
@@ -15,10 +12,10 @@
 
 
 fig = pv.figure()
-random_state = np.random.RandomState(42)
+rng = np.random.default_rng(42)
 A2B = transform_from(
-    R=matrix_from_axis_angle(random_axis_angle(random_state)),
-    p=random_state.randn(3))
+    R=matrix_from_axis_angle(random_axis_angle(rng)),
+    p=rng.standard_normal(size=3))
 fig.plot_cylinder(length=1.0, radius=0.3)
 fig.plot_transform(A2B=np.eye(4))
 fig.plot_cylinder(length=1.0, radius=0.3, A2B=A2B)
diff --git a/examples/visualizations/vis_ee_wrench.py b/examples/visualizations/vis_ee_wrench.py
index a4b03f079..844d775d0 100644
--- a/examples/visualizations/vis_ee_wrench.py
+++ b/examples/visualizations/vis_ee_wrench.py
@@ -3,7 +3,7 @@
 Visualize Wrench
 ================
 
-We see a 6-DOF robot arm and we assume that we have a force/torque sensor
+We see a 6-DOF robot arm, and we assume that we have a force/torque sensor
 that measures the force of a spherical mass (gray sphere) at the tool center
 point (TCP). We can draw the screw representation of the wrench in the TCP
 frame as a force along a screw axis from the red sphere to the green sphere.
@@ -16,9 +16,6 @@
 direction to the displaced configuration in which the instantaneous
 wrench would move the base.
 """
-print(__doc__)
-
-
 import os
 import numpy as np
 from pytransform3d.urdf import UrdfTransformManager
diff --git a/examples/visualizations/vis_ellipsoid.py b/examples/visualizations/vis_ellipsoid.py
index bc20e9697..0a10a2b38 100644
--- a/examples/visualizations/vis_ellipsoid.py
+++ b/examples/visualizations/vis_ellipsoid.py
@@ -3,9 +3,6 @@
 Visualize Ellipsoid
 ===================
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
diff --git a/examples/visualizations/vis_mesh.py b/examples/visualizations/vis_mesh.py
index f287e45cf..0dfd9f6b4 100644
--- a/examples/visualizations/vis_mesh.py
+++ b/examples/visualizations/vis_mesh.py
@@ -8,9 +8,6 @@
 hard-coded path to the STL file. Press 'H' to print the viewer's
 help message to stdout.
 """
-print(__doc__)
-
-
 import os
 import numpy as np
 from pytransform3d import visualizer as pv
diff --git a/examples/visualizations/vis_moving_basis.py b/examples/visualizations/vis_moving_basis.py
index f6f2e5005..615273787 100644
--- a/examples/visualizations/vis_moving_basis.py
+++ b/examples/visualizations/vis_moving_basis.py
@@ -5,9 +5,6 @@
 
 Animates a rotation about the x-axis.
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 from pytransform3d import rotations as pr
diff --git a/examples/visualizations/vis_moving_cylinder_with_wrench.py b/examples/visualizations/vis_moving_cylinder_with_wrench.py
index 7837614a9..d0233c89d 100644
--- a/examples/visualizations/vis_moving_cylinder_with_wrench.py
+++ b/examples/visualizations/vis_moving_cylinder_with_wrench.py
@@ -7,9 +7,6 @@
 acceleration to twist and exponential coordinates of transformation
 to finally compute the new pose of the cylinder.
 """
-print(__doc__)
-
-
 import numpy as np
 from pytransform3d.transformations import (
     transform_from_exponential_coordinates)
diff --git a/examples/visualizations/vis_moving_line.py b/examples/visualizations/vis_moving_line.py
index 1f012734c..6b5cc9aae 100644
--- a/examples/visualizations/vis_moving_line.py
+++ b/examples/visualizations/vis_moving_line.py
@@ -5,9 +5,6 @@
 
 Animates a line.
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
diff --git a/examples/visualizations/vis_moving_robot.py b/examples/visualizations/vis_moving_robot.py
index 8ef118881..c9d296d92 100644
--- a/examples/visualizations/vis_moving_robot.py
+++ b/examples/visualizations/vis_moving_robot.py
@@ -5,9 +5,6 @@
 
 In this example we animate a 6-DOF robot arm with cylindrical visuals.
 """
-print(__doc__)
-
-
 import os
 import numpy as np
 from pytransform3d.urdf import UrdfTransformManager
diff --git a/examples/visualizations/vis_moving_trajectory.py b/examples/visualizations/vis_moving_trajectory.py
index c1b8e9157..4af3b0059 100644
--- a/examples/visualizations/vis_moving_trajectory.py
+++ b/examples/visualizations/vis_moving_trajectory.py
@@ -5,9 +5,6 @@
 
 Animates a trajectory.
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 from pytransform3d.rotations import passive_matrix_from_angle, R_id
diff --git a/examples/visualizations/vis_moving_urdf_with_meshes.py b/examples/visualizations/vis_moving_urdf_with_meshes.py
index 56d90f553..bee31e533 100644
--- a/examples/visualizations/vis_moving_urdf_with_meshes.py
+++ b/examples/visualizations/vis_moving_urdf_with_meshes.py
@@ -7,9 +7,6 @@
 This example must be run from within the examples folder or the main
 folder because it uses a hard-coded path to the URDF file and the meshes.
 """
-print(__doc__)
-
-
 import os
 import numpy as np
 from pytransform3d.urdf import UrdfTransformManager
diff --git a/examples/visualizations/vis_plane.py b/examples/visualizations/vis_plane.py
index 38bc488d6..1fc46f66f 100644
--- a/examples/visualizations/vis_plane.py
+++ b/examples/visualizations/vis_plane.py
@@ -6,9 +6,6 @@
 Visualizes one plane in Hesse normal form and one plane defined by point and
 normal.
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.rotations as pr
 import pytransform3d.visualizer as pv
@@ -16,11 +13,11 @@
 
 fig = pv.figure()
 fig.plot_transform(np.eye(4))
-random_state = np.random.RandomState(8853)
-fig.plot_plane(pr.norm_vector(random_state.randn(3)), random_state.randn(),
+rng = np.random.default_rng(8853)
+fig.plot_plane(pr.norm_vector(rng.standard_normal(3)), rng.standard_normal(),
                c=(1, 0.5, 0))
-fig.plot_plane(pr.norm_vector(random_state.randn(3)),
-               point_in_plane=random_state.randn(3), c=(0, 1, 1))
+fig.plot_plane(pr.norm_vector(rng.standard_normal(3)),
+               point_in_plane=rng.standard_normal(3), c=(0, 1, 1))
 if "__file__" in globals():
     fig.show()
 else:
diff --git a/examples/visualizations/vis_scatter.py b/examples/visualizations/vis_scatter.py
index f4afcfe8c..00bafb2f0 100644
--- a/examples/visualizations/vis_scatter.py
+++ b/examples/visualizations/vis_scatter.py
@@ -5,16 +5,13 @@
 
 Visualizes a point collection.
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
 
 fig = pv.figure()
-random_state = np.random.RandomState(41)
-P = random_state.randn(100, 3)
+rng = np.random.default_rng(41)
+P = rng.standard_normal(size=(100, 3))
 colors = np.empty((100, 3))
 for d in range(colors.shape[1]):
     colors[:, d] = np.linspace(0, 1, len(colors))
diff --git a/examples/visualizations/vis_sphere.py b/examples/visualizations/vis_sphere.py
index 815f224b0..efa8a0088 100644
--- a/examples/visualizations/vis_sphere.py
+++ b/examples/visualizations/vis_sphere.py
@@ -3,9 +3,6 @@
 Visualize Sphere
 ================
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
diff --git a/examples/visualizations/vis_urdf.py b/examples/visualizations/vis_urdf.py
index b0f457035..2cbaf4873 100644
--- a/examples/visualizations/vis_urdf.py
+++ b/examples/visualizations/vis_urdf.py
@@ -6,9 +6,6 @@
 This example shows how to load a URDF description of a robot, set some joint
 angles and display relevant frames.
 """
-print(__doc__)
-
-
 from pytransform3d.urdf import UrdfTransformManager
 import pytransform3d.visualizer as pv
 
diff --git a/examples/visualizations/vis_urdf_with_meshes.py b/examples/visualizations/vis_urdf_with_meshes.py
index 63c4238ee..e68e286b0 100644
--- a/examples/visualizations/vis_urdf_with_meshes.py
+++ b/examples/visualizations/vis_urdf_with_meshes.py
@@ -7,9 +7,6 @@
 run from within the examples folder or the main folder because it uses a
 hard-coded path to the URDF file and the meshes.
 """
-print(__doc__)
-
-
 import os
 from pytransform3d.urdf import UrdfTransformManager
 import pytransform3d.visualizer as pv
diff --git a/examples/visualizations/vis_vector.py b/examples/visualizations/vis_vector.py
index 19cf88af3..f644cacbe 100644
--- a/examples/visualizations/vis_vector.py
+++ b/examples/visualizations/vis_vector.py
@@ -3,9 +3,6 @@
 Visualize Vector
 ================
 """
-print(__doc__)
-
-
 import numpy as np
 import pytransform3d.visualizer as pv
 
diff --git a/pytransform3d/__init__.py b/pytransform3d/__init__.py
index 9eabf0483..be66674d0 100644
--- a/pytransform3d/__init__.py
+++ b/pytransform3d/__init__.py
@@ -1,4 +1,4 @@
 """3D transformations for Python."""
 
 
-__version__ = "2.3.1"
+__version__ = "3.0.0"
diff --git a/pytransform3d/camera.py b/pytransform3d/camera.py
index 1801ab8a2..1c6b9f8d7 100644
--- a/pytransform3d/camera.py
+++ b/pytransform3d/camera.py
@@ -276,7 +276,7 @@ def plot_camera(ax=None, M=None, cam2world=None, virtual_image_distance=1.0,
     sensor_corners_in_cam[:, 0] -= M[0, 2]
     sensor_corners_in_cam[:, 1] -= M[1, 2]
     sensor_corners_in_world = transform(
-        cam2world, vectors_to_points(sensor_corners_in_cam))[:, :3]
+        cam2world, vectors_to_points(sensor_corners_in_cam), strict_check=strict_check)[:, :3]
     virtual_image_corners = (
         virtual_image_distance / focal_length *
         (sensor_corners_in_world - camera_center_in_world[np.newaxis]) +
diff --git a/pytransform3d/coordinates.py b/pytransform3d/coordinates.py
index fbd997fa1..4fe4b7863 100644
--- a/pytransform3d/coordinates.py
+++ b/pytransform3d/coordinates.py
@@ -16,7 +16,7 @@ def cartesian_from_cylindrical(p):
     q : array, shape (..., 3)
         Cartesian coordinates (x, y, z)
     """
-    p = np.asarray(p)
+    p = np.asarray(p, dtype=float)
     q = np.empty_like(p)
     q[..., 0] = p[..., 0] * np.cos(p[..., 1])
     q[..., 1] = p[..., 0] * np.sin(p[..., 1])
@@ -38,7 +38,7 @@ def cartesian_from_spherical(p):
     q : array, shape (..., 3)
         Cartesian coordinates (x, y, z)
     """
-    p = np.asarray(p)
+    p = np.asarray(p, dtype=float)
     q = np.empty_like(p)
     r_sin_theta = p[..., 0] * np.sin(p[..., 1])
     q[..., 0] = np.cos(p[..., 2]) * r_sin_theta
@@ -61,7 +61,7 @@ def cylindrical_from_cartesian(p):
         Cylindrical coordinates: axial / radial distance (rho >= 0), azimuth
         (-pi >= phi >= pi), and axial coordinate / height (z)
     """
-    p = np.asarray(p)
+    p = np.asarray(p, dtype=float)
     q = np.empty_like(p)
     q[..., 0] = np.linalg.norm(p[..., :2], axis=-1)
     q[..., 1] = np.arctan2(p[..., 1], p[..., 0])
@@ -84,7 +84,7 @@ def cylindrical_from_spherical(p):
         Cylindrical coordinates: axial / radial distance (rho), azimuth
         (phi), and axial coordinate / height (z)
     """
-    p = np.asarray(p)
+    p = np.asarray(p, dtype=float)
     q = np.empty_like(p)
     q[..., 0] = p[..., 0] * np.sin(p[..., 1])
     q[..., 1] = p[..., 2]
@@ -106,7 +106,7 @@ def spherical_from_cartesian(p):
         Spherical coordinates: radial distance (rho >= 0), inclination /
         elevation (0 <= theta <= pi), and azimuth (-pi <= phi <= pi)
     """
-    p = np.asarray(p)
+    p = np.asarray(p, dtype=float)
     q = np.empty_like(p)
     q[..., 0] = np.linalg.norm(p, axis=-1)
     q[..., 1] = np.arctan2(np.linalg.norm(p[..., :2], axis=-1), p[..., 2])
@@ -129,7 +129,7 @@ def spherical_from_cylindrical(p):
         Spherical coordinates: radial distance (rho), inclination /
         elevation (theta), and azimuth (phi)
     """
-    p = np.asarray(p)
+    p = np.asarray(p, dtype=float)
     q = np.empty_like(p)
     q[..., 0] = np.linalg.norm(p[..., (0, 2)], axis=-1)
     q[..., 1] = np.arctan2(p[..., 0], p[..., 2])
diff --git a/pytransform3d/plot_utils/__init__.py b/pytransform3d/plot_utils/__init__.py
index a7e9fa80e..a6db9a4d3 100644
--- a/pytransform3d/plot_utils/__init__.py
+++ b/pytransform3d/plot_utils/__init__.py
@@ -5,15 +5,16 @@
     from ._artists import Arrow3D, Frame, LabeledFrame, Trajectory
     from ._layout import make_3d_axis, remove_frame
     from ._plot_functions import (
-        plot_box, plot_sphere, plot_cylinder, plot_mesh, plot_ellipsoid,
-        plot_capsule, plot_cone, plot_vector, plot_length_variable)
+        plot_box, plot_sphere, plot_spheres, plot_cylinder, plot_mesh,
+        plot_ellipsoid, plot_capsule, plot_cone, plot_vector,
+        plot_length_variable)
 
     __all__ = [
         "Arrow3D", "Frame", "LabeledFrame", "Trajectory",
         "make_3d_axis", "remove_frame",
-        "plot_box", "plot_sphere", "plot_cylinder", "plot_mesh",
-        "plot_ellipsoid", "plot_capsule", "plot_cone", "plot_vector",
-        "plot_length_variable"
+        "plot_box", "plot_sphere", "plot_spheres", "plot_cylinder",
+        "plot_mesh", "plot_ellipsoid", "plot_capsule", "plot_cone",
+        "plot_vector", "plot_length_variable"
     ]
 except ImportError:
     warnings.warn("Matplotlib is not installed, visualization is not "
diff --git a/pytransform3d/plot_utils/_layout.py b/pytransform3d/plot_utils/_layout.py
index d63e35b0d..77049a885 100644
--- a/pytransform3d/plot_utils/_layout.py
+++ b/pytransform3d/plot_utils/_layout.py
@@ -51,9 +51,15 @@ def make_3d_axis(ax_s, pos=111, unit=None, n_ticks=5):
     ax.yaxis.set_major_locator(MaxNLocator(n_ticks))
     ax.zaxis.set_major_locator(MaxNLocator(n_ticks))
 
-    ax.w_xaxis.pane.set_color("white")
-    ax.w_yaxis.pane.set_color("white")
-    ax.w_zaxis.pane.set_color("white")
+    try:
+        ax.xaxis.pane.set_color("white")
+        ax.yaxis.pane.set_color("white")
+        ax.zaxis.pane.set_color("white")
+    except AttributeError:  # pragma: no cover
+        # fallback for older versions of matplotlib, deprecated since v3.1
+        ax.w_xaxis.pane.set_color("white")
+        ax.w_yaxis.pane.set_color("white")
+        ax.w_zaxis.pane.set_color("white")
 
     return ax
 
diff --git a/pytransform3d/plot_utils/_plot_functions.py b/pytransform3d/plot_utils/_plot_functions.py
index 32cadc301..171fe860c 100644
--- a/pytransform3d/plot_utils/_plot_functions.py
+++ b/pytransform3d/plot_utils/_plot_functions.py
@@ -137,13 +137,75 @@ def plot_sphere(ax=None, radius=1.0, p=np.zeros(3), ax_s=1, wireframe=True,
 
     if wireframe:
         ax.plot_wireframe(
-            x, y, z, rstride=10, cstride=10, color=color, alpha=alpha)
+            x, y, z, rstride=2, cstride=2, color=color, alpha=alpha)
     else:
         ax.plot_surface(x, y, z, color=color, alpha=alpha, linewidth=0)
 
     return ax
 
 
+def plot_spheres(ax=None, radius=np.ones(1), p=np.zeros((1, 3)), ax_s=1,
+                 wireframe=True, n_steps=20, alpha=np.ones(1),
+                 color=np.zeros((1, 3))):
+    """Plot multiple spheres.
+
+    Parameters
+    ----------
+    ax : Matplotlib 3d axis, optional (default: None)
+        If the axis is None, a new 3d axis will be created
+
+    radius : array-like, shape (n_spheres,), optional (default: 1)
+        Radius of the sphere(s)
+
+    p : array-like, shape (n_spheres, 3), optional (default: [0, 0, 0])
+        Center of the sphere(s)
+
+    ax_s : float, optional (default: 1)
+        Scaling of the new matplotlib 3d axis
+
+    wireframe : bool, optional (default: True)
+        Plot wireframe of sphere(s) and surface otherwise
+
+    n_steps : int, optional (default: 20)
+        Number of discrete steps plotted in each dimension
+
+    alpha : array-like, shape (n_spheres,), optional (default: 1)
+        Alpha value of the sphere(s) that will be plotted
+
+    color : array-like, shape (n_spheres, 3), optional (default: black)
+        Color in which the sphere(s) should be plotted
+
+    Returns
+    -------
+    ax : Matplotlib 3d axis
+        New or old axis
+    """
+    if ax is None:
+        ax = make_3d_axis(ax_s)
+
+    radius = np.asarray(radius)
+    p = np.asarray(p)
+
+    phi, theta = np.mgrid[0.0:np.pi:n_steps * 1j, 0.0:2.0 * np.pi:n_steps * 1j]
+    sin_phi = np.sin(phi)
+    verts = (radius[..., np.newaxis, np.newaxis, np.newaxis]
+             * np.array([sin_phi * np.cos(theta), sin_phi * np.sin(theta),
+                         np.cos(phi)])[np.newaxis, ...]
+             + p[..., np.newaxis, np.newaxis])
+    colors = np.resize(color, (len(verts), 3))
+    alphas = np.resize(alpha, len(verts))
+
+    for verts_i, color_i, alpha_i in zip(verts, colors, alphas):
+        if wireframe:
+            ax.plot_wireframe(
+                *verts_i, rstride=2, cstride=2, color=color_i, alpha=alpha_i)
+        else:
+            ax.plot_surface(*verts_i, color=color_i, alpha=alpha_i,
+                            linewidth=0)
+
+    return ax
+
+
 def plot_cylinder(ax=None, length=1.0, radius=1.0, thickness=0.0,
                   A2B=np.eye(4), ax_s=1, wireframe=True, n_steps=100,
                   alpha=1.0, color="k"):
@@ -383,7 +445,7 @@ def plot_ellipsoid(ax=None, radii=np.ones(3), A2B=np.eye(4), ax_s=1,
 
     if wireframe:
         ax.plot_wireframe(
-            x, y, z, rstride=10, cstride=10, color=color, alpha=alpha)
+            x, y, z, rstride=2, cstride=2, color=color, alpha=alpha)
     else:
         ax.plot_surface(x, y, z, color=color, alpha=alpha, linewidth=0)
 
@@ -451,7 +513,7 @@ def plot_capsule(ax=None, A2B=np.eye(4), height=1.0, radius=1.0,
 
     if wireframe:
         ax.plot_wireframe(
-            x, y, z, rstride=10, cstride=10, color=color, alpha=alpha)
+            x, y, z, rstride=2, cstride=2, color=color, alpha=alpha)
     else:
         ax.plot_surface(x, y, z, color=color, alpha=alpha, linewidth=0)
 
diff --git a/pytransform3d/plot_utils/_plot_functions.pyi b/pytransform3d/plot_utils/_plot_functions.pyi
index b0d59cc4b..7390caba1 100644
--- a/pytransform3d/plot_utils/_plot_functions.pyi
+++ b/pytransform3d/plot_utils/_plot_functions.pyi
@@ -16,6 +16,13 @@ def plot_sphere(
         color: str = ...) -> Axes3D: ...
 
 
+def plot_spheres(
+        ax: Union[None, Axes3D] = ..., radius: npt.ArrayLike = ...,
+        p: npt.ArrayLike = ..., ax_s: float = ..., wireframe: bool = ...,
+        n_steps: float = ..., alpha: npt.ArrayLike = ...,
+        color: npt.ArrayLike = ...) -> Axes3D: ...
+
+
 def plot_cylinder(
         ax: Union[None, Axes3D] = ..., length: float = ...,
         radius: float = ..., thickness: float = ..., A2B: npt.ArrayLike = ...,
diff --git a/pytransform3d/rotations/__init__.py b/pytransform3d/rotations/__init__.py
index 1386d190f..59dc7bc49 100644
--- a/pytransform3d/rotations/__init__.py
+++ b/pytransform3d/rotations/__init__.py
@@ -13,7 +13,7 @@
     random_quaternion,
     check_skew_symmetric_matrix, check_matrix, check_quaternion,
     check_quaternions, check_axis_angle, check_compact_axis_angle,
-    check_rotor)
+    check_rotor, check_mrp)
 from ._conversions import (
     quaternion_from_axis_angle, quaternion_from_compact_axis_angle,
     quaternion_from_matrix, quaternion_wxyz_from_xyzw,
@@ -78,7 +78,9 @@
     euler_from_matrix,
     euler_from_quaternion,
     quaternion_from_angle,
-    cross_product_matrix)
+    cross_product_matrix,
+    mrp_from_quaternion,
+    quaternion_from_mrp)
 from ._quaternion_operations import (
     quaternion_integrate, quaternion_gradient, concatenate_quaternions, q_conj,
     q_prod_vector, quaternion_diff, quaternion_dist, quaternion_from_euler)
@@ -92,6 +94,9 @@
     wedge, geometric_product, rotor_apply, rotor_reverse, concatenate_rotors,
     rotor_from_plane_angle, rotor_from_two_directions, matrix_from_rotor,
     plane_normal_from_bivector)
+from ._jacobians import (
+    left_jacobian_SO3, left_jacobian_SO3_series, left_jacobian_SO3_inv,
+    left_jacobian_SO3_inv_series)
 
 __all__ = [
     "eps",
@@ -125,6 +130,7 @@
     "check_axis_angle",
     "check_rotor",
     "check_compact_axis_angle",
+    "check_mrp",
     "quaternion_from_axis_angle",
     "quaternion_from_compact_axis_angle",
     "quaternion_from_matrix",
@@ -199,6 +205,8 @@
     "quaternion_from_angle",
     "quaternion_from_euler",
     "cross_product_matrix",
+    "mrp_from_quaternion",
+    "quaternion_from_mrp",
     "quaternion_integrate",
     "quaternion_gradient",
     "concatenate_quaternions",
@@ -229,5 +237,9 @@
     "plot_bivector",
     "rotor_slerp",
     "plane_normal_from_bivector",
-    "plane_basis_from_normal"
+    "plane_basis_from_normal",
+    "left_jacobian_SO3",
+    "left_jacobian_SO3_series",
+    "left_jacobian_SO3_inv",
+    "left_jacobian_SO3_inv_series"
 ]
diff --git a/pytransform3d/rotations/_conversions.py b/pytransform3d/rotations/_conversions.py
index 95e733521..cf014db3d 100644
--- a/pytransform3d/rotations/_conversions.py
+++ b/pytransform3d/rotations/_conversions.py
@@ -4,7 +4,7 @@
 import numpy as np
 from ._utils import (
     check_matrix, check_quaternion, check_axis_angle, check_compact_axis_angle,
-    norm_angle, norm_vector, norm_axis_angle,
+    check_mrp, norm_angle, norm_vector, norm_axis_angle,
     perpendicular_to_vector, perpendicular_to_vectors, vector_projection)
 from ._constants import unitx, unity, unitz, eps
 
@@ -17,10 +17,18 @@ def cross_product_matrix(v):
     .. math::
 
         \boldsymbol{V} \boldsymbol{w} = \boldsymbol{v} \times
-        \boldsymbol{w}
+        \boldsymbol{w}.
 
-    It is a skew-symmetric (antisymmetric) matrix, i.e.
-    :math:`-\boldsymbol{V} = \boldsymbol{V}^T`.
+    It is a skew-symmetric (antisymmetric) matrix, i.e.,
+    :math:`-\boldsymbol{V} = \boldsymbol{V}^T`. Its elements are
+
+    .. math::
+
+        \boldsymbol{V} = \left(\begin{array}{ccc}
+        0 & -v_3 & v_2\\
+        v_3 & 0 & -v_1\\
+        -v_2 & v_1 & 0
+        \end{array}\right).
 
     Parameters
     ----------
@@ -86,10 +94,25 @@ def matrix_from_two_vectors(a, b):
 
 
 def matrix_from_axis_angle(a):
-    """Compute rotation matrix from axis-angle.
+    r"""Compute rotation matrix from axis-angle.
 
     This is called exponential map or Rodrigues' formula.
 
+    .. math::
+
+        \boldsymbol{R}(\hat{\boldsymbol{\omega}}, \theta)
+        =
+        Exp(\hat{\boldsymbol{\omega}} \theta)
+        =
+        \cos{\theta} \boldsymbol{I}
+        + \sin{\theta} \left[\hat{\boldsymbol{\omega}}\right]
+        + (1 - \cos{\theta})
+        \hat{\boldsymbol{\omega}}\hat{\boldsymbol{\omega}}^T
+        =
+        \boldsymbol{I}
+        + \sin{\theta} \left[\hat{\boldsymbol{\omega}}\right]
+        + (1 - \cos{\theta}) \left[\hat{\boldsymbol{\omega}}\right]^2
+
     This typically results in an active rotation matrix.
 
     Parameters
@@ -130,10 +153,23 @@ def matrix_from_axis_angle(a):
 
 
 def matrix_from_compact_axis_angle(a):
-    """Compute rotation matrix from compact axis-angle.
+    r"""Compute rotation matrix from compact axis-angle.
 
     This is called exponential map or Rodrigues' formula.
 
+    .. math::
+
+        Exp(\hat{\boldsymbol{\omega}} \theta)
+        =
+        \cos{\theta} \boldsymbol{I}
+        + \sin{\theta} \left[\hat{\boldsymbol{\omega}}\right]
+        + (1 - \cos{\theta})
+        \hat{\boldsymbol{\omega}}\hat{\boldsymbol{\omega}}^T
+        =
+        \boldsymbol{I}
+        + \sin{\theta} \left[\hat{\boldsymbol{\omega}}\right]
+        + (1 - \cos{\theta}) \left[\hat{\boldsymbol{\omega}}\right]^2
+
     This typically results in an active rotation matrix.
 
     Parameters
@@ -227,7 +263,43 @@ def passive_matrix_from_angle(basis, angle):
 
 
 def active_matrix_from_angle(basis, angle):
-    """Compute active rotation matrix from rotation about basis vector.
+    r"""Compute active rotation matrix from rotation about basis vector.
+
+    With the angle :math:`\alpha` and :math:`s = \sin{\alpha}, c=\cos{\alpha}`,
+    we construct rotation matrices about the basis vectors as follows:
+
+    .. math::
+
+        \boldsymbol{R}_x(\alpha) =
+        \left(
+        \begin{array}{ccc}
+        1 & 0 & 0\\
+        0 & c & -s\\
+        0 & s & c
+        \end{array}
+        \right)
+
+    .. math::
+
+        \boldsymbol{R}_y(\alpha) =
+        \left(
+        \begin{array}{ccc}
+        c & 0 & s\\
+        0 & 1 & 0\\
+        -s & 0 & c
+        \end{array}
+        \right)
+
+    .. math::
+
+        \boldsymbol{R}_z(\alpha) =
+        \left(
+        \begin{array}{ccc}
+        c & -s & 0\\
+        s & c & 0\\
+        0 & 0 & 1
+        \end{array}
+        \right)
 
     Parameters
     ----------
@@ -1822,7 +1894,7 @@ def compact_axis_angle(a):
     return a[:3] * a[3]
 
 
-def compact_axis_angle_from_matrix(R):
+def compact_axis_angle_from_matrix(R, check=True):
     """Compute compact axis-angle from rotation matrix.
 
     This operation is called logarithmic map. Note that there are two possible
@@ -1835,13 +1907,16 @@ def compact_axis_angle_from_matrix(R):
     R : array-like, shape (3, 3)
         Rotation matrix
 
+    check : bool, optional (default: True)
+        Check if rotation matrix is valid
+
     Returns
     -------
     a : array-like, shape (3,)
         Axis of rotation and rotation angle: angle * (x, y, z). The angle is
         constrained to [0, pi].
     """
-    a = axis_angle_from_matrix(R)
+    a = axis_angle_from_matrix(R, check=check)
     return compact_axis_angle(a)
 
 
@@ -2057,3 +2132,43 @@ def quaternion_from_extrinsic_euler_xyz(e):
         "quaternion_from_euler", DeprecationWarning)
     R = active_matrix_from_extrinsic_euler_xyz(e)
     return quaternion_from_matrix(R)
+
+
+def quaternion_from_mrp(mrp):
+    """Compute quaternion from modified Rodrigues parameters.
+
+    Parameters
+    ----------
+    mrp : array-like, shape (3,)
+        Modified Rodrigues parameters.
+
+    Returns
+    -------
+    q : array, shape (4,)
+        Unit quaternion to represent rotation: (w, x, y, z)
+    """
+    mrp = check_mrp(mrp)
+    dot_product_p1 = np.dot(mrp, mrp) + 1.0
+    q = np.empty(4, dtype=float)
+    q[0] = (2.0 - dot_product_p1) / dot_product_p1
+    q[1:] = 2.0 * mrp / dot_product_p1
+    return q
+
+
+def mrp_from_quaternion(q):
+    """Compute modified Rodrigues parameters from quaternion.
+
+    Parameters
+    ----------
+    q : array-like, shape (4,)
+        Unit quaternion to represent rotation: (w, x, y, z)
+
+    Returns
+    -------
+    mrp : array, shape (3,)
+        Modified Rodrigues parameters.
+    """
+    q = check_quaternion(q)
+    if q[0] < 0.0:
+        q = -q
+    return q[1:] / (1.0 + q[0])
diff --git a/pytransform3d/rotations/_conversions.pyi b/pytransform3d/rotations/_conversions.pyi
index 3d53b1b64..5c2aa4bb8 100644
--- a/pytransform3d/rotations/_conversions.pyi
+++ b/pytransform3d/rotations/_conversions.pyi
@@ -204,7 +204,7 @@ def axis_angle_from_two_directions(a: npt.ArrayLike, b: npt.ArrayLike) -> np.nda
 def compact_axis_angle(a: npt.ArrayLike) -> np.ndarray: ...
 
 
-def compact_axis_angle_from_matrix(R: npt.ArrayLike) -> np.ndarray: ...
+def compact_axis_angle_from_matrix(R: npt.ArrayLike, check: bool = ...) -> np.ndarray: ...
 
 
 def compact_axis_angle_from_quaternion(q: npt.ArrayLike) -> np.ndarray: ...
@@ -229,3 +229,9 @@ def quaternion_wxyz_from_xyzw(q_xyzw: npt.ArrayLike) -> np.ndarray: ...
 
 
 def quaternion_from_extrinsic_euler_xyz(e: npt.ArrayLike) -> np.ndarray: ...
+
+
+def quaternion_from_mrp(mrp: npt.ArrayLike) -> np.ndarray: ...
+
+
+def mrp_from_quaternion(q: npt.ArrayLike) -> np.ndarray: ...
diff --git a/pytransform3d/rotations/_jacobians.py b/pytransform3d/rotations/_jacobians.py
new file mode 100644
index 000000000..d71d89638
--- /dev/null
+++ b/pytransform3d/rotations/_jacobians.py
@@ -0,0 +1,130 @@
+import math
+import numpy as np
+from ._conversions import cross_product_matrix
+
+
+def left_jacobian_SO3(omega):
+    r"""Left Jacobian of SO(3) at theta (angle of rotation).
+
+    .. math::
+
+        \boldsymbol{J}(\theta)
+        =
+        \frac{\sin{\theta}}{\theta} \boldsymbol{I}
+        + \left(\frac{1 - \cos{\theta}}{\theta}\right)
+        \left[\hat{\boldsymbol{\omega}}\right]
+        + \left(1 - \frac{\sin{\theta}}{\theta} \right)
+        \hat{\boldsymbol{\omega}} \hat{\boldsymbol{\omega}}^T
+
+    Parameters
+    ----------
+    omega : array-like, shape (3,)
+        Compact axis-angle representation.
+
+    Returns
+    -------
+    J : array, shape (3, 3)
+        Left Jacobian of SO(3).
+    """
+    omega = np.asarray(omega)
+    theta = np.linalg.norm(omega)
+    if theta < np.finfo(float).eps:
+        return left_jacobian_SO3_series(omega, 10)
+    omega_unit = omega / theta
+    omega_matrix = cross_product_matrix(omega_unit)
+    return (
+        np.eye(3)
+        + (1.0 - math.cos(theta)) / theta * omega_matrix
+        + (1.0 - math.sin(theta) / theta) * np.dot(omega_matrix, omega_matrix)
+    )
+
+
+def left_jacobian_SO3_series(omega, n_terms):
+    """Left Jacobian of SO(3) at theta from Taylor series.
+
+    Parameters
+    ----------
+    omega : array-like, shape (3,)
+        Compact axis-angle representation.
+
+    n_terms : int
+        Number of terms to include in the series.
+
+    Returns
+    -------
+    J : array, shape (3, 3)
+        Left Jacobian of SO(3).
+    """
+    omega = np.asarray(omega)
+    J = np.eye(3)
+    pxn = np.eye(3)
+    px = cross_product_matrix(omega)
+    for n in range(n_terms):
+        pxn = np.dot(pxn, px) / (n + 2)
+        J += pxn
+    return J
+
+
+def left_jacobian_SO3_inv(omega):
+    r"""Inverse left Jacobian of SO(3) at theta (angle of rotation).
+
+    .. math::
+
+        \boldsymbol{J}^{-1}(\theta)
+        =
+        \frac{\theta}{2 \tan{\frac{\theta}{2}}} \boldsymbol{I}
+        - \frac{\theta}{2} \left[\hat{\boldsymbol{\omega}}\right]
+        + \left(1 - \frac{\theta}{2 \tan{\frac{\theta}{2}}}\right)
+        \hat{\boldsymbol{\omega}} \hat{\boldsymbol{\omega}}^T
+
+    Parameters
+    ----------
+    omega : array-like, shape (3,)
+        Compact axis-angle representation.
+
+    Returns
+    -------
+    J_inv : array, shape (3, 3)
+        Inverse left Jacobian of SO(3).
+    """
+    omega = np.asarray(omega)
+    theta = np.linalg.norm(omega)
+    if theta < np.finfo(float).eps:
+        return left_jacobian_SO3_inv_series(omega, 10)
+    omega_unit = omega / theta
+    omega_matrix = cross_product_matrix(omega_unit)
+    return (
+        np.eye(3)
+        - 0.5 * omega_matrix * theta
+        + (1.0 - 0.5 * theta / np.tan(theta / 2.0)) * np.dot(
+            omega_matrix, omega_matrix)
+    )
+
+
+def left_jacobian_SO3_inv_series(omega, n_terms):
+    """Inverse left Jacobian of SO(3) at theta from Taylor series.
+
+    Parameters
+    ----------
+    omega : array-like, shape (3,)
+        Compact axis-angle representation.
+
+    n_terms : int
+        Number of terms to include in the series.
+
+    Returns
+    -------
+    J_inv : array, shape (3, 3)
+        Inverse left Jacobian of SO(3).
+    """
+    from scipy.special import bernoulli
+
+    omega = np.asarray(omega)
+    J_inv = np.eye(3)
+    pxn = np.eye(3)
+    px = cross_product_matrix(omega)
+    b = bernoulli(n_terms + 1)
+    for n in range(n_terms):
+        pxn = np.dot(pxn, px / (n + 1))
+        J_inv += b[n + 1] * pxn
+    return J_inv
diff --git a/pytransform3d/rotations/_jacobians.pyi b/pytransform3d/rotations/_jacobians.pyi
new file mode 100644
index 000000000..25449c4fd
--- /dev/null
+++ b/pytransform3d/rotations/_jacobians.pyi
@@ -0,0 +1,14 @@
+import numpy as np
+import numpy.typing as npt
+
+
+def left_jacobian_SO3(omega: npt.ArrayLike) -> np.ndarray: ...
+
+
+def left_jacobian_SO3_series(omega: npt.ArrayLike, n_terms: int) -> np.ndarray: ...
+
+
+def left_jacobian_SO3_inv(omega: npt.ArrayLike) -> np.ndarray: ...
+
+
+def left_jacobian_SO3_inv_series(omega: npt.ArrayLike, n_terms: int) -> np.ndarray: ...
diff --git a/pytransform3d/rotations/_utils.py b/pytransform3d/rotations/_utils.py
index 5440f7148..0d6f1c9f3 100644
--- a/pytransform3d/rotations/_utils.py
+++ b/pytransform3d/rotations/_utils.py
@@ -258,7 +258,7 @@ def plane_basis_from_normal(plane_normal):
     return x_axis, y_axis
 
 
-def random_vector(rng=np.random.RandomState(0), n=3, random_state=None):
+def random_vector(rng=np.random.default_rng(0), n=3):
     r"""Generate an nd vector with normally distributed components.
 
     Each component will be sampled from :math:`\mathcal{N}(\mu=0, \sigma=1)`.
@@ -276,15 +276,10 @@ def random_vector(rng=np.random.RandomState(0), n=3, random_state=None):
     v : array, shape (n,)
         Random vector
     """
-    if random_state is not None:  # pragma: no cover
-        warnings.warn(
-            "The random_state argument is deprecated. Use rng.",
-            DeprecationWarning)
-        rng = random_state
-    return rng.standard_normal(n)
+    return rng.standard_normal(size=n)
 
 
-def random_axis_angle(rng=np.random.RandomState(0), random_state=None):
+def random_axis_angle(rng=np.random.default_rng(0)):
     r"""Generate random axis-angle.
 
     The angle will be sampled uniformly from the interval :math:`[0, \pi)`
@@ -302,23 +297,18 @@ def random_axis_angle(rng=np.random.RandomState(0), random_state=None):
     a : array, shape (4,)
         Axis of rotation and rotation angle: (x, y, z, angle)
     """
-    if random_state is not None:  # pragma: no cover
-        warnings.warn(
-            "The random_state argument is deprecated. Use rng.",
-            DeprecationWarning)
-        rng = random_state
     angle = np.pi * rng.random()
     a = np.array([0, 0, 0, angle])
-    a[:3] = norm_vector(rng.standard_normal(3))
+    a[:3] = norm_vector(rng.standard_normal(size=3))
     return a
 
 
-def random_compact_axis_angle(rng=np.random.RandomState(0), random_state=None):
+def random_compact_axis_angle(rng=np.random.default_rng(0)):
     r"""Generate random compact axis-angle.
 
     The angle will be sampled uniformly from the interval :math:`[0, \pi)`
     and each component of the rotation axis will be sampled from
-    :math:`\mathcal{N}(\mu=0, \sigma=1)` and than the axis will be normalized
+    :math:`\mathcal{N}(\mu=0, \sigma=1)` and then the axis will be normalized
     to length 1.
 
     Parameters
@@ -331,16 +321,11 @@ def random_compact_axis_angle(rng=np.random.RandomState(0), random_state=None):
     a : array, shape (3,)
         Axis of rotation and rotation angle: angle * (x, y, z)
     """
-    if random_state is not None:  # pragma: no cover
-        warnings.warn(
-            "The random_state argument is deprecated. Use rng.",
-            DeprecationWarning)
-        rng = random_state
     a = random_axis_angle(rng)
     return a[:3] * a[3]
 
 
-def random_quaternion(rng=np.random.RandomState(0), random_state=None):
+def random_quaternion(rng=np.random.default_rng(0)):
     """Generate random quaternion.
 
     Parameters
@@ -353,12 +338,7 @@ def random_quaternion(rng=np.random.RandomState(0), random_state=None):
     q : array, shape (4,)
         Unit quaternion to represent rotation: (w, x, y, z)
     """
-    if random_state is not None:  # pragma: no cover
-        warnings.warn(
-            "The random_state argument is deprecated. Use rng.",
-            DeprecationWarning)
-        rng = random_state
-    return norm_vector(rng.standard_normal(4))
+    return norm_vector(rng.standard_normal(size=4))
 
 
 def check_skew_symmetric_matrix(V, tolerance=1e-6, strict_check=True):
@@ -406,10 +386,20 @@ def check_skew_symmetric_matrix(V, tolerance=1e-6, strict_check=True):
 
 
 def check_matrix(R, tolerance=1e-6, strict_check=True):
-    """Input validation of a rotation matrix.
+    r"""Input validation of a rotation matrix.
 
     We check whether R multiplied by its inverse is approximately the identity
-    matrix, and whether the determinant is positive.
+    matrix
+
+    .. math::
+
+        \boldsymbol{R}\boldsymbol{R}^T = \boldsymbol{I}
+
+    and whether the determinant is positive
+
+    .. math::
+
+        det(\boldsymbol{R}) > 0
 
     Parameters
     ----------
@@ -593,3 +583,29 @@ def check_rotor(rotor):
         raise ValueError("Expected rotor with shape (4,), got "
                          "array-like object with shape %s" % (rotor.shape,))
     return norm_vector(rotor)
+
+
+def check_mrp(mrp):
+    """Input validation of modified Rodrigues parameters.
+
+    Parameters
+    ----------
+    mrp : array-like, shape (3,)
+        Modified Rodrigues parameters.
+
+    Returns
+    -------
+    mrp : array, shape (3,)
+        Validated modified Rodrigues parameters.
+
+    Raises
+    ------
+    ValueError
+        If input is invalid
+    """
+    mrp = np.asarray(mrp)
+    if mrp.ndim != 1 or mrp.shape[0] != 3:
+        raise ValueError(
+            "Expected modified Rodrigues parameters with shape (3,), got "
+            "array-like object with shape %s" % (mrp.shape,))
+    return mrp
diff --git a/pytransform3d/rotations/_utils.pyi b/pytransform3d/rotations/_utils.pyi
index 604c7d3a9..f4dfb8923 100644
--- a/pytransform3d/rotations/_utils.pyi
+++ b/pytransform3d/rotations/_utils.pyi
@@ -34,16 +34,17 @@ def plane_basis_from_normal(
         plane_normal: npt.ArrayLike) -> Tuple[np.ndarray, np.ndarray]: ...
 
 
-def random_vector(rng: np.random.Generator = ..., n: int = ..., random_state: None = ...) -> np.ndarray: ...
 
+def random_vector(rng: np.random.Generator = ..., n: int = ...) -> np.ndarray: ...
 
-def random_axis_angle(rng: np.random.Generator = ..., random_state: None = ...) -> np.ndarray: ...
 
+def random_axis_angle(rng: np.random.Generator = ...) -> np.ndarray: ...
 
-def random_compact_axis_angle(rng: np.random.Generator = ..., random_state: None = ...) -> np.ndarray: ...
 
+def random_compact_axis_angle(rng: np.random.Generator = ...) -> np.ndarray: ...
 
-def random_quaternion(rng: np.random.Generator = ..., random_state: None = ...) -> np.ndarray: ...
+
+def random_quaternion(rng: np.random.Generator = ...) -> np.ndarray: ...
 
 
 def check_skew_symmetric_matrix(V: npt.ArrayLike, tolerance: float = ..., strict_check: bool = ...) -> np.ndarray: ...
@@ -65,3 +66,6 @@ def check_quaternions(Q: npt.ArrayLike, unit: bool = ...) -> np.ndarray: ...
 
 
 def check_rotor(a: npt.ArrayLike) -> np.ndarray: ...
+
+
+def check_mrp(mrp: npt.ArrayLike) -> np.ndarray: ...
diff --git a/pytransform3d/transformations/__init__.py b/pytransform3d/transformations/__init__.py
index 0f7ca5ace..d8547d8d6 100644
--- a/pytransform3d/transformations/__init__.py
+++ b/pytransform3d/transformations/__init__.py
@@ -31,11 +31,15 @@
 from ._dual_quaternion_operations import (
     dq_q_conj, dq_conj, concatenate_dual_quaternions, dual_quaternion_sclerp,
     dual_quaternion_power, dq_prod_vector)
-from ._random import random_transform, random_screw_axis
+from ._random import (
+    random_transform, random_screw_axis, random_exponential_coordinates)
 from ._plot import plot_transform, plot_screw
 from ._testing import (
     assert_transform, assert_screw_parameters_equal,
     assert_unit_dual_quaternion_equal, assert_unit_dual_quaternion)
+from ._jacobians import (
+    left_jacobian_SE3, left_jacobian_SE3_series, left_jacobian_SE3_inv,
+    left_jacobian_SE3_inv_series)
 
 
 __all__ = [
@@ -63,10 +67,12 @@
     "invert_transform", "scale_transform", "concat",
     "vector_to_point", "vectors_to_points", "vector_to_direction",
     "vectors_to_directions", "transform",
-    "random_transform", "random_screw_axis",
+    "random_transform", "random_screw_axis", "random_exponential_coordinates",
     "dq_q_conj", "dq_conj", "concatenate_dual_quaternions",
     "dual_quaternion_sclerp", "dual_quaternion_power", "dq_prod_vector",
     "plot_transform", "plot_screw",
     "assert_transform", "assert_screw_parameters_equal",
-    "assert_unit_dual_quaternion_equal", "assert_unit_dual_quaternion"
+    "assert_unit_dual_quaternion_equal", "assert_unit_dual_quaternion",
+    "left_jacobian_SE3", "left_jacobian_SE3_series", "left_jacobian_SE3_inv",
+    "left_jacobian_SE3_inv_series"
 ]
diff --git a/pytransform3d/transformations/_conversions.py b/pytransform3d/transformations/_conversions.py
index 7e9db008f..d02d97545 100644
--- a/pytransform3d/transformations/_conversions.py
+++ b/pytransform3d/transformations/_conversions.py
@@ -1,18 +1,28 @@
 """Conversions between transform representations."""
-import math
 import numpy as np
 from ._utils import (check_transform, check_pq, check_screw_axis,
                      check_screw_parameters, check_exponential_coordinates,
                      check_screw_matrix, check_transform_log,
                      check_dual_quaternion)
 from ..rotations import (
-    matrix_from_quaternion, quaternion_from_matrix, axis_angle_from_matrix,
-    matrix_from_axis_angle, cross_product_matrix, q_conj,
-    concatenate_quaternions, axis_angle_from_quaternion, norm_angle, eps)
+    matrix_from_quaternion, quaternion_from_matrix,
+    compact_axis_angle_from_matrix, matrix_from_compact_axis_angle,
+    cross_product_matrix, q_conj, concatenate_quaternions,
+    axis_angle_from_quaternion, norm_angle, eps)
+from ..rotations import left_jacobian_SO3, left_jacobian_SO3_inv
 
 
 def transform_from(R, p, strict_check=True):
-    """Make transformation from rotation matrix and translation.
+    r"""Make transformation from rotation matrix and translation.
+
+    .. math::
+
+        \boldsymbol{T}_{BA} = \left(
+        \begin{array}{cc}
+        \boldsymbol{R} & \boldsymbol{p}\\
+        \boldsymbol{0} & 1
+        \end{array}
+        \right) \in SE(3)
 
     Parameters
     ----------
@@ -310,7 +320,23 @@ def exponential_coordinates_from_screw_axis(screw_axis, theta):
 
 
 def exponential_coordinates_from_transform_log(transform_log, check=True):
-    """Compute exponential coordinates from logarithm of transformation.
+    r"""Compute exponential coordinates from logarithm of transformation.
+
+    Extracts the vector :math:`\mathcal{S} \theta =
+    (\boldsymbol{\omega}, \boldsymbol{v}) \theta \in \mathbb{R}^6` from the
+    matrix
+
+    .. math::
+
+        \left(
+        \begin{array}{cccc}
+        0 & -\omega_3 & \omega_2 & v_1\\
+        \omega_3 & 0 & -\omega_1 & v_2\\
+        -\omega_2 & \omega_1 & 0 & v_3\\
+        0 & 0 & 0 & 0
+        \end{array}
+        \right)
+        \theta = \left[ \mathcal{S} \right] \theta \in so(3).
 
     Parameters
     ----------
@@ -341,10 +367,52 @@ def exponential_coordinates_from_transform_log(transform_log, check=True):
 
 
 def exponential_coordinates_from_transform(A2B, strict_check=True, check=True):
-    """Compute exponential coordinates from transformation matrix.
+    r"""Compute exponential coordinates from transformation matrix.
 
     Logarithmic map.
 
+    .. math::
+
+        Log: \boldsymbol{T} \in SE(3)
+        \rightarrow \mathcal{S} \theta \in \mathbb{R}^6
+
+    .. math::
+
+        Log(\boldsymbol{T}) =
+        Log\left(
+        \begin{array}{cc}
+        \boldsymbol{R} & \boldsymbol{p}\\
+        \boldsymbol{0} & 1
+        \end{array}
+        \right)
+        =
+        \left(
+        \begin{array}{c}
+        Log\boldsymbol{R}\\
+        \boldsymbol{G}^{-1}(\theta) \boldsymbol{p}
+        \end{array}
+        \right)
+        =
+        \left(
+        \begin{array}{c}
+        \boldsymbol{\omega}\\
+        \boldsymbol{v}
+        \end{array}
+        \right)
+        \theta
+        =
+        \mathcal{S}\theta,
+
+    where
+
+    .. math::
+
+        \boldsymbol{G}^{-1}(\theta)
+        = \boldsymbol{I} \frac{1}{\theta}
+        + \frac{1}{2} [\boldsymbol{\omega}]
+        + (\frac{1}{\theta} - \frac{1}{2 \tan \frac{\theta}{2}})
+        [\boldsymbol{\omega}]^2.
+
     Parameters
     ----------
     A2B : array-like, shape (4, 4)
@@ -376,21 +444,15 @@ def exponential_coordinates_from_transform(A2B, strict_check=True, check=True):
     if np.linalg.norm(np.eye(3) - R) < np.finfo(float).eps:
         return np.r_[0.0, 0.0, 0.0, p]
 
-    omega_theta = axis_angle_from_matrix(R, check=check)
-    omega_unit = omega_theta[:3]
-    theta = omega_theta[3]
+    omega_theta = compact_axis_angle_from_matrix(R, check=check)
+    theta = np.linalg.norm(omega_theta)
 
     if theta == 0:
         return np.r_[0.0, 0.0, 0.0, p]
 
-    omega_unit_matrix = cross_product_matrix(omega_unit)
-
-    G_inv = (np.eye(3) / theta - 0.5 * omega_unit_matrix
-             + (1.0 / theta - 0.5 / np.tan(theta / 2.0))
-             * np.dot(omega_unit_matrix, omega_unit_matrix))
-    v = G_inv.dot(p)
+    v = np.dot(left_jacobian_SO3_inv(omega_theta), p)
 
-    return np.hstack((omega_unit, v)) * theta
+    return np.hstack((omega_theta, v))
 
 
 def screw_matrix_from_screw_axis(screw_axis):
@@ -447,7 +509,24 @@ def screw_matrix_from_transform_log(transform_log):
 
 
 def transform_log_from_exponential_coordinates(Stheta):
-    """Compute matrix logarithm of transformation from exponential coordinates.
+    r"""Compute matrix logarithm of transformation from exponential coordinates.
+
+    Builds the matrix
+
+    .. math::
+
+        \left(
+        \begin{array}{cccc}
+        0 & -\omega_3 & \omega_2 & v_1\\
+        \omega_3 & 0 & -\omega_1 & v_2\\
+        -\omega_2 & \omega_1 & 0 & v_3\\
+        0 & 0 & 0 & 0
+        \end{array}
+        \right) \theta
+        = \left[ \mathcal{S} \right] \theta \in so(3)
+
+    from the vector :math:`\mathcal{S} \theta = (\boldsymbol{\omega},
+    \boldsymbol{v}) \theta \in \mathbb{R}^6`.
 
     Parameters
     ----------
@@ -496,7 +575,51 @@ def transform_log_from_screw_matrix(screw_matrix, theta):
 
 
 def transform_log_from_transform(A2B, strict_check=True):
-    """Compute matrix logarithm of transformation from transformation.
+    r"""Compute matrix logarithm of transformation from transformation.
+
+    Logarithmic map.
+
+    .. math::
+
+        \log: \boldsymbol{T} \in SE(3)
+        \rightarrow \left[ \mathcal{S} \right] \theta \in se(3)
+
+    .. math::
+
+        \log(\boldsymbol{T}) =
+        \log\left(
+        \begin{array}{cc}
+        \boldsymbol{R} & \boldsymbol{p}\\
+        \boldsymbol{0} & 1
+        \end{array}
+        \right)
+        =
+        \left(
+        \begin{array}{cc}
+        \log\boldsymbol{R} & \boldsymbol{G}^{-1}(\theta) \boldsymbol{p}\\
+        \boldsymbol{0} & 0
+        \end{array}
+        \right)
+        =
+        \left(
+        \begin{array}{cc}
+        \hat{\boldsymbol{\omega}}\theta
+        & \boldsymbol{G}^{-1}(\theta) \boldsymbol{p}\\
+        \boldsymbol{0} & 0
+        \end{array}
+        \right)
+        =
+        \left[\mathcal{S}\right]\theta,
+
+    where
+
+    .. math::
+
+        \boldsymbol{G}^{-1}(\theta)
+        = \boldsymbol{I} \frac{1}{\theta}
+        + \frac{1}{2} [\boldsymbol{\omega}]
+        + (\frac{1}{\theta} - \frac{1}{2 \tan \frac{\theta}{2}})
+        [\boldsymbol{\omega}]^2.
 
     Parameters
     ----------
@@ -524,32 +647,55 @@ def transform_log_from_transform(A2B, strict_check=True):
         transform_log[:3, 3] = p
         return transform_log
 
-    omega_theta = axis_angle_from_matrix(R)
-    omega_unit = omega_theta[:3]
-    theta = omega_theta[3]
+    omega_theta = compact_axis_angle_from_matrix(R)
+    theta = np.linalg.norm(omega_theta)
 
     if theta == 0:
         return transform_log
 
-    omega_unit_matrix = cross_product_matrix(omega_unit)
+    J_inv = left_jacobian_SO3_inv(omega_theta)
+    v = np.dot(J_inv, p)
 
-    G_inv = (np.eye(3) / theta - 0.5 * omega_unit_matrix
-             + (1.0 / theta - 0.5 / np.tan(theta / 2.0))
-             * np.dot(omega_unit_matrix, omega_unit_matrix))
-    v = G_inv.dot(p)
-
-    transform_log[:3, :3] = omega_unit_matrix
+    transform_log[:3, :3] = cross_product_matrix(omega_theta)
     transform_log[:3, 3] = v
-    transform_log *= theta
 
     return transform_log
 
 
 def transform_from_exponential_coordinates(Stheta, check=True):
-    """Compute transformation matrix from exponential coordinates.
+    r"""Compute transformation matrix from exponential coordinates.
 
     Exponential map.
 
+    .. math::
+
+        Exp: \mathcal{S} \theta \in \mathbb{R}^6
+        \rightarrow \boldsymbol{T} \in SE(3)
+
+    .. math::
+
+        Exp(\mathcal{S}\theta) =
+        Exp\left(\left(\begin{array}{c}
+        \boldsymbol{\omega}\\
+        \boldsymbol{v}
+        \end{array}\right)\theta\right)
+        =
+        \exp(\left[\mathcal{S}\right] \theta)
+        =
+        \left(\begin{array}{cc}
+        Exp(\boldsymbol{\omega}) & \boldsymbol{G}(\theta)\boldsymbol{v}\\
+        \boldsymbol{0} & 1
+        \end{array}\right),
+
+    where
+
+    .. math::
+
+        \boldsymbol{G}(\theta)
+        = \boldsymbol{I}\theta
+        + (1 - \cos \theta) [\boldsymbol{\omega}]
+        + (\theta - \sin \theta) [\boldsymbol{\omega}]^2.
+
     Parameters
     ----------
     Stheta : array-like, shape (6,)
@@ -576,23 +722,46 @@ def transform_from_exponential_coordinates(Stheta, check=True):
     if theta == 0.0:  # only translation
         return translate_transform(np.eye(4), Stheta[3:], check=check)
 
-    screw_axis = Stheta / theta
-    omega_unit = screw_axis[:3]
-    v = screw_axis[3:]
+    omega = Stheta[:3]
+    v = Stheta[3:]
 
     A2B = np.eye(4)
-    A2B[:3, :3] = matrix_from_axis_angle(np.r_[omega_unit, theta])
-    omega_matrix = cross_product_matrix(omega_unit)
-    A2B[:3, 3] = np.dot(
-        np.eye(3) * theta
-        + (1.0 - math.cos(theta)) * omega_matrix
-        + (theta - math.sin(theta)) * np.dot(omega_matrix, omega_matrix),
-        v)
+    A2B[:3, :3] = matrix_from_compact_axis_angle(omega)
+    J = left_jacobian_SO3(omega)
+    A2B[:3, 3] = np.dot(J, v)
     return A2B
 
 
 def transform_from_transform_log(transform_log):
-    """Compute transformation from matrix logarithm of transformation.
+    r"""Compute transformation from matrix logarithm of transformation.
+
+    Exponential map.
+
+    .. math::
+
+        \exp: \left[ \mathcal{S} \right] \theta \in se(3)
+        \rightarrow \boldsymbol{T} \in SE(3)
+
+    .. math::
+
+        \exp([\mathcal{S}]\theta) =
+        \exp\left(\left(\begin{array}{cc}
+        \left[\boldsymbol{\omega}\right] & \boldsymbol{v}\\
+        \boldsymbol{0} & 0
+        \end{array}\right)\theta\right) =
+        \left(\begin{array}{cc}
+        Exp(\boldsymbol{\omega}) & \boldsymbol{G}(\theta)\boldsymbol{v}\\
+        \boldsymbol{0} & 1
+        \end{array}\right),
+
+    where
+
+    .. math::
+
+        \boldsymbol{G}(\theta)
+        = \boldsymbol{I}\theta
+        + (1 - \cos \theta) [\boldsymbol{\omega}]
+        + (\theta - \sin \theta) [\boldsymbol{\omega}]^2.
 
     Parameters
     ----------
@@ -614,17 +783,10 @@ def transform_from_transform_log(transform_log):
     if theta == 0.0:  # only translation
         return translate_transform(np.eye(4), v)
 
-    omega_unit = omega_theta / theta
-    v = v / theta
-
     A2B = np.eye(4)
-    A2B[:3, :3] = matrix_from_axis_angle(np.r_[omega_unit, theta])
-    omega_unit_matrix = transform_log[:3, :3] / theta
-    G = (np.eye(3) * theta
-         + (1.0 - math.cos(theta)) * omega_unit_matrix
-         + (theta - math.sin(theta)) * np.dot(omega_unit_matrix,
-                                              omega_unit_matrix))
-    A2B[:3, 3] = np.dot(G, v)
+    A2B[:3, :3] = matrix_from_compact_axis_angle(omega_theta)
+    J = left_jacobian_SO3(omega_theta)
+    A2B[:3, 3] = np.dot(J, v)
     return A2B
 
 
diff --git a/pytransform3d/transformations/_jacobians.py b/pytransform3d/transformations/_jacobians.py
new file mode 100644
index 000000000..d5f5e7042
--- /dev/null
+++ b/pytransform3d/transformations/_jacobians.py
@@ -0,0 +1,207 @@
+import math
+import numpy as np
+from ..rotations import (
+    cross_product_matrix, left_jacobian_SO3, left_jacobian_SO3_inv)
+from ._conversions import screw_axis_from_exponential_coordinates
+from ._utils import check_exponential_coordinates
+
+
+def left_jacobian_SE3(Stheta):
+    r"""Left Jacobian of SE(3).
+
+    .. math::
+
+        \boldsymbol{\mathcal{J}}
+        =
+        \left(
+        \begin{array}{cc}
+        \boldsymbol{J} & \boldsymbol{0}\\
+        \boldsymbol{Q} & \boldsymbol{J}
+        \end{array}
+        \right),
+
+    where :math:`\boldsymbol{J}` is the left Jacobian of SO(3) and
+    :math:`\boldsymbol{Q}` is given by Barfoot and Furgale (see reference
+    below).
+
+    Parameters
+    ----------
+    Stheta : array-like, shape (6,)
+        Exponential coordinates of transformation:
+        S * theta = (omega_1, omega_2, omega_3, v_1, v_2, v_3) * theta,
+        where S is the screw axis, the first 3 components are related to
+        rotation and the last 3 components are related to translation.
+        Theta is the rotation angle and h * theta the translation.
+
+    Returns
+    -------
+    J : array, shape (6, 6)
+        Jacobian of SE(3).
+
+    References
+    ----------
+    Barfoot, Furgale: Associating Uncertainty With Three-Dimensional Poses for
+    Use in Estimation Problems,
+    http://ncfrn.mcgill.ca/members/pubs/barfoot_tro14.pdf
+    """
+    Stheta = check_exponential_coordinates(Stheta)
+
+    _, theta = screw_axis_from_exponential_coordinates(Stheta)
+    if theta < np.finfo(float).eps:
+        return left_jacobian_SE3_series(Stheta, 10)
+
+    phi = Stheta[:3]
+    J = left_jacobian_SO3(phi)
+    return np.block([
+        [J, np.zeros((3, 3))],
+        [_Q(Stheta), J]
+    ])
+
+
+def left_jacobian_SE3_series(Stheta, n_terms):
+    """Left Jacobian of SE(3) at theta from Taylor series.
+
+    Parameters
+    ----------
+    Stheta : array-like, shape (6,)
+        Exponential coordinates of transformation:
+        S * theta = (omega_1, omega_2, omega_3, v_1, v_2, v_3) * theta,
+        where S is the screw axis, the first 3 components are related to
+        rotation and the last 3 components are related to translation.
+        Theta is the rotation angle and h * theta the translation.
+
+    n_terms : int
+        Number of terms to include in the series.
+
+    Returns
+    -------
+    J : array, shape (3, 3)
+        Left Jacobian of SE(3).
+    """
+    Stheta = check_exponential_coordinates(Stheta)
+    J = np.eye(6)
+    pxn = np.eye(6)
+    px = _curlyhat(Stheta)
+    for n in range(n_terms):
+        pxn = np.dot(pxn, px) / (n + 2)
+        J += pxn
+    return J
+
+
+def left_jacobian_SE3_inv(Stheta):
+    r"""Left inverse Jacobian of SE(3).
+
+    .. math::
+
+        \boldsymbol{\mathcal{J}}^{-1}
+        =
+        \left(
+        \begin{array}{cc}
+        \boldsymbol{J}^{-1} & \boldsymbol{0}\\
+        -\boldsymbol{J}^{-1}\boldsymbol{Q}\boldsymbol{J}^{-1} &
+        \boldsymbol{J}^{-1}
+        \end{array}
+        \right),
+
+    where :math:`\boldsymbol{J}` is the left Jacobian of SO(3) and
+    :math:`\boldsymbol{Q}` is given by Barfoot and Furgale (see reference
+    below).
+
+    Parameters
+    ----------
+    Stheta : array-like, shape (6,)
+        Exponential coordinates of transformation:
+        S * theta = (omega_1, omega_2, omega_3, v_1, v_2, v_3) * theta,
+        where S is the screw axis, the first 3 components are related to
+        rotation and the last 3 components are related to translation.
+        Theta is the rotation angle and h * theta the translation.
+
+    Returns
+    -------
+    J_inv : array, shape (6, 6)
+        Inverse Jacobian of SE(3).
+    """
+    Stheta = check_exponential_coordinates(Stheta)
+
+    _, theta = screw_axis_from_exponential_coordinates(Stheta)
+    if theta < np.finfo(float).eps:
+        return left_jacobian_SE3_inv_series(Stheta, 10)
+
+    phi = Stheta[:3]
+    J_inv = left_jacobian_SO3_inv(phi)
+    return np.block([
+        [J_inv, np.zeros((3, 3))],
+        [-np.dot(J_inv, np.dot(_Q(Stheta), J_inv)), J_inv]
+    ])
+
+
+def _Q(Stheta):
+    rho = Stheta[3:]
+    phi = Stheta[:3]
+    ph = np.linalg.norm(phi)
+
+    px = cross_product_matrix(phi)
+    rx = cross_product_matrix(rho)
+
+    ph2 = ph * ph
+    ph3 = ph2 * ph
+    ph4 = ph3 * ph
+    ph5 = ph4 * ph
+
+    cph = math.cos(ph)
+    sph = math.sin(ph)
+
+    t1 = 0.5 * rx
+    t2 = (ph - sph) / ph3 * (np.dot(px, rx) + np.dot(rx, px)
+                             + np.dot(px, np.dot(rx, px)))
+    m3 = (1.0 - 0.5 * ph * ph - cph) / ph4
+    t3 = -m3 * (np.dot(px, np.dot(px, rx)) + np.dot(rx, np.dot(px, px))
+                - 3 * np.dot(px, np.dot(rx, px)))
+    m4 = 0.5 * (m3 - 3.0 * (ph - sph - ph3 / 6.0) / ph5)
+    t4 = -m4 * (np.dot(px, np.dot(rx, np.dot(px, px)))
+                + np.dot(px, np.dot(px, np.dot(rx, px))))
+
+    Q = t1 + t2 + t3 + t4
+
+    return Q
+
+
+def left_jacobian_SE3_inv_series(Stheta, n_terms):
+    """Left inverse Jacobian of SE(3) at theta from Taylor series.
+
+    Parameters
+    ----------
+    Stheta : array-like, shape (6,)
+        Exponential coordinates of transformation:
+        S * theta = (omega_1, omega_2, omega_3, v_1, v_2, v_3) * theta,
+        where S is the screw axis, the first 3 components are related to
+        rotation and the last 3 components are related to translation.
+        Theta is the rotation angle and h * theta the translation.
+
+    n_terms : int
+        Number of terms to include in the series.
+
+    Returns
+    -------
+    J_inv : array, shape (3, 3)
+        Inverse left Jacobian of SE(3).
+    """
+    from scipy.special import bernoulli
+
+    Stheta = check_exponential_coordinates(Stheta)
+    J_inv = np.eye(6)
+    pxn = np.eye(6)
+    px = _curlyhat(Stheta)
+    b = bernoulli(n_terms + 1)
+    for n in range(n_terms):
+        pxn = np.dot(pxn, px / (n + 1))
+        J_inv += b[n + 1] * pxn
+    return J_inv
+
+
+def _curlyhat(Stheta):
+    omega_matrix = cross_product_matrix(Stheta[:3])
+    return np.block([
+        [omega_matrix, np.zeros((3, 3))],
+        [cross_product_matrix(Stheta[3:]), omega_matrix]
+    ])
diff --git a/pytransform3d/transformations/_jacobians.pyi b/pytransform3d/transformations/_jacobians.pyi
new file mode 100644
index 000000000..26985aab8
--- /dev/null
+++ b/pytransform3d/transformations/_jacobians.pyi
@@ -0,0 +1,14 @@
+import numpy as np
+import numpy.typing as npt
+
+
+def left_jacobian_SE3(Stheta: npt.ArrayLike) -> np.ndarray: ...
+
+
+def left_jacobian_SE3_series(Stheta: npt.ArrayLike, n_terms: int) -> np.ndarray: ...
+
+
+def left_jacobian_SE3_inv(Stheta: npt.ArrayLike) -> np.ndarray: ...
+
+
+def left_jacobian_SE3_inv_series(Stheta: npt.ArrayLike, n_terms: int) -> np.ndarray: ...
diff --git a/pytransform3d/transformations/_random.py b/pytransform3d/transformations/_random.py
index f7acfa0ca..e4ce503cc 100644
--- a/pytransform3d/transformations/_random.py
+++ b/pytransform3d/transformations/_random.py
@@ -1,39 +1,47 @@
 """Random transform generation."""
-import warnings
 import numpy as np
-from ..rotations import (
-    random_quaternion, random_vector, matrix_from_quaternion, norm_vector)
-from ._conversions import transform_from
+from ..rotations import norm_vector
+from ._utils import check_transform
+from ._conversions import transform_from_exponential_coordinates
 
 
-def random_transform(rng=np.random.RandomState(0), random_state=None):
+def random_transform(
+        rng=np.random.default_rng(0), mean=np.eye(4), cov=np.eye(6)):
     r"""Generate random transform.
 
-    Each component of the translation will be sampled from
-    :math:`\mathcal{N}(\mu=0, \sigma=1)`.
+    Generate :math:`\Delta \boldsymbol{T}_{B_{i+1}{B_i}}
+    \boldsymbol{T}_{{B_i}A}`, with :math:`\Delta \boldsymbol{T}_{B_{i+1}{B_i}}
+    = Exp(S \theta)` and :math:`\mathcal{S}\theta \sim
+    \mathcal{N}(\boldsymbol{0}_6, \boldsymbol{\Sigma}_{6 \times 6})`.
+    The mean :math:`\boldsymbol{T}_{{B_i}A}` and the covariance
+    :math:`\boldsymbol{\Sigma}_{6 \times 6}` are parameters of the function.
+
+    Note that uncertainty is defined in the global frame B, not in the
+    body frame A.
 
     Parameters
     ----------
     rng : np.random.Generator, optional (default: random seed 0)
         Random number generator
 
+    mean : array-like, shape (4, 4), optional (default: I)
+        Mean transform as homogeneous transformation matrix.
+
+    cov : array-like, shape (6, 6), optional (default: I)
+        Covariance of noise in exponential coordinate space.
+
     Returns
     -------
     A2B : array-like, shape (4, 4)
         Random transform from frame A to frame B
     """
-    if random_state is not None:  # pragma: no cover
-        warnings.warn(
-            "The random_state argument is deprecated. Use rng.",
-            DeprecationWarning)
-        rng = random_state
-    q = random_quaternion(rng)
-    R = matrix_from_quaternion(q)
-    p = random_vector(rng, n=3)
-    return transform_from(R=R, p=p)
-
-
-def random_screw_axis(rng=np.random.RandomState(0), random_state=None):
+    mean = check_transform(mean)
+    Stheta = random_exponential_coordinates(rng=rng, cov=cov)
+    delta = transform_from_exponential_coordinates(Stheta)
+    return np.dot(delta, mean)
+
+
+def random_screw_axis(rng=np.random.default_rng(0)):
     r"""Generate random screw axis.
 
     Each component of v will be sampled from a standard normal distribution
@@ -53,11 +61,36 @@ def random_screw_axis(rng=np.random.RandomState(0), random_state=None):
         where the first 3 components are related to rotation and the last 3
         components are related to translation.
     """
-    if random_state is not None:  # pragma: no cover
-        warnings.warn(
-            "The random_state argument is deprecated. Use rng.",
-            DeprecationWarning)
-        rng = random_state
-    omega = norm_vector(rng.standard_normal(3))
-    v = rng.standard_normal(3)
+    omega = norm_vector(rng.standard_normal(size=3))
+    v = rng.standard_normal(size=3)
     return np.hstack((omega, v))
+
+
+def random_exponential_coordinates(
+        rng=np.random.default_rng(0), cov=np.eye(6)):
+    r"""Generate random exponential coordinates.
+
+    Each component of Stheta will be sampled from a standard normal
+    distribution :math:`\mathcal{N}(\boldsymbol{0}_6,
+    \boldsymbol{\Sigma}_{6 \times 6})`.
+
+    Parameters
+    ----------
+    rng : np.random.Generator, optional (default: random seed 0)
+        Random number generator
+
+    cov : array-like, shape (6, 6), optional (default: I)
+        Covariance of normal distribution.
+
+    Returns
+    -------
+    Stheta : array, shape (6,)
+        Exponential coordinates of transformation:
+        S * theta = (omega_1, omega_2, omega_3, v_1, v_2, v_3) * theta,
+        where the first 3 components are related to rotation and the last 3
+        components are related to translation. Theta is the rotation angle
+        and h * theta the translation. Theta should be >= 0. Negative rotations
+        will be represented by a negative screw axis instead. This is relevant
+        if you want to recover theta from exponential coordinates.
+    """
+    return rng.multivariate_normal(mean=np.zeros(6), cov=cov)
diff --git a/pytransform3d/transformations/_random.pyi b/pytransform3d/transformations/_random.pyi
index 8de71cfa8..79b5c2756 100644
--- a/pytransform3d/transformations/_random.pyi
+++ b/pytransform3d/transformations/_random.pyi
@@ -1,7 +1,18 @@
 import numpy as np
+import numpy.typing as npt
 
 
-def random_transform(rng: np.random.Generator = ..., random_state: None = ...) -> np.ndarray: ...
+def random_transform(
+        rng: np.random.Generator = ...,
+        mean: npt.ArrayLike = ...,
+        cov: npt.ArrayLike = ...
+) -> np.ndarray: ...
 
 
-def random_screw_axis(rng: np.random.Generator = ..., random_state: None = ...) -> np.ndarray: ...
+def random_screw_axis(rng: np.random.Generator = ...) -> np.ndarray: ...
+
+
+def random_exponential_coordinates(
+        rng: np.random.Generator = ...,
+        cov: npt.ArrayLike = ...
+) -> np.ndarray: ...
diff --git a/pytransform3d/transformations/_transform_operations.py b/pytransform3d/transformations/_transform_operations.py
index f91deccae..8517e34a9 100644
--- a/pytransform3d/transformations/_transform_operations.py
+++ b/pytransform3d/transformations/_transform_operations.py
@@ -23,7 +23,7 @@ def invert_transform(A2B, strict_check=True, check=True):
 
     Returns
     -------
-    B2A : array-like, shape (4, 4)
+    B2A : array, shape (4, 4)
         Transform from frame B to frame A
     """
     if check:
@@ -116,10 +116,14 @@ def vectors_to_directions(V):
 
 
 def concat(A2B, B2C, strict_check=True, check=True):
-    """Concatenate transformations.
+    r"""Concatenate transformations.
 
     We use the extrinsic convention, which means that B2C is left-multiplied
-    to A2B.
+    to A2B. In mathematical notation:
+
+    .. math::
+
+        \boldsymbol{T}_{CB} \boldsymbol{T}_{BA} = \boldsymbol{T}_{CA}.
 
     Parameters
     ----------
diff --git a/pytransform3d/transformations/_utils.py b/pytransform3d/transformations/_utils.py
index 84368259f..75d707fdf 100644
--- a/pytransform3d/transformations/_utils.py
+++ b/pytransform3d/transformations/_utils.py
@@ -242,7 +242,7 @@ def check_screw_matrix(screw_matrix, tolerance=1e-6, strict_check=True):
         0 & 0 & 0 & 0\\\\
         \\end{matrix}
         \\right)
-        \\in \\mathbb{R}^{4 \\times 4}
+        \\in se(3) \\subset \\mathbb{R}^{4 \\times 4}
 
     Parameters
     ----------
@@ -302,8 +302,8 @@ def check_transform_log(transform_log, tolerance=1e-6, strict_check=True):
     """Input validation for logarithm of transformation.
 
     The logarithm of a transformation :math:`\\left[\\mathcal{S}\\right]\\theta
-    \\in \\mathbb{R}^{4 \\times 4}` are the product of a screw matrix and a
-    scalar :math:`\\theta`.
+    \\in se(3) \\subset \\mathbb{R}^{4 \\times 4}` are the product of a screw
+    matrix and a scalar :math:`\\theta`.
 
     Parameters
     ----------
@@ -352,12 +352,12 @@ def check_dual_quaternion(dq, unit=True):
 
     .. math::
 
-        \\sigma = p + \\epsilon q,
+        \\boldsymbol{\\sigma} = \\boldsymbol{p} + \\epsilon \\boldsymbol{q},
 
-    where :math:`p` and :math:`q` are both quaternions and :math:`\\epsilon`
-    is the dual unit with :math:`\\epsilon^2 = 0`. The first quaternion is
-    also called the real part and the second quaternion is called the dual
-    part.
+    where :math:`\\boldsymbol{p}` and :math:`\\boldsymbol{q}` are both
+    quaternions and :math:`\\epsilon` is the dual unit with
+    :math:`\\epsilon^2 = 0`. The first quaternion is also called the real part
+    and the second quaternion is called the dual part.
 
     Parameters
     ----------
diff --git a/pytransform3d/uncertainty.py b/pytransform3d/uncertainty.py
new file mode 100644
index 000000000..c5f9ce261
--- /dev/null
+++ b/pytransform3d/uncertainty.py
@@ -0,0 +1,425 @@
+"""Operations related to uncertain transformations."""
+import numpy as np
+from .transformations import (
+    invert_transform, transform_from, concat, adjoint_from_transform,
+    left_jacobian_SE3_inv, transform_from_exponential_coordinates,
+    exponential_coordinates_from_transform)
+from .trajectories import (exponential_coordinates_from_transforms,
+                           transforms_from_exponential_coordinates,
+                           concat_many_to_one)
+
+
+def estimate_gaussian_transform_from_samples(samples):
+    """Estimate Gaussian distribution over transformations from samples.
+
+    Uses iterative approximation of mean described by Eade (2017) and computes
+    covariance in exponential coordinate space (using an unbiased estimator).
+
+    Parameters
+    ----------
+    samples : array-like, shape (n_samples, 4, 4)
+        Sampled transformations represented by homogeneous matrices.
+
+    Returns
+    -------
+    mean : array, shape (4, 4)
+        Mean as homogeneous transformation matrix.
+
+    cov : array, shape (6, 6)
+        Covariance of distribution in exponential coordinate space.
+
+    References
+    ----------
+    Eade: Lie Groups for 2D and 3D Transformations (2017),
+    https://ethaneade.com/lie.pdf
+    """
+    assert len(samples) > 0
+    mean = samples[0]
+    for _ in range(20):
+        mean_inv = invert_transform(mean)
+        mean_diffs = exponential_coordinates_from_transforms(
+            concat_many_to_one(samples, mean_inv))
+        avg_mean_diff = np.mean(mean_diffs, axis=0)
+        mean = np.dot(
+            transform_from_exponential_coordinates(avg_mean_diff), mean)
+
+    cov = np.cov(mean_diffs, rowvar=False, bias=True)
+    return mean, cov
+
+
+def invert_uncertain_transform(mean, cov):
+    r"""Invert uncertain transform.
+
+    For the mean :math:`\boldsymbol{T}_{BA}`, the inverse is simply
+    :math:`\boldsymbol{T}_{BA}^{-1} = \boldsymbol{T}_{AB}`.
+
+    For the covariance, we need the adjoint of the inverse transformation
+    :math:`\left[Ad_{\boldsymbol{T}_{BA}^{-1}}\right]`:
+
+    .. math::
+
+        \boldsymbol{\Sigma}_{\boldsymbol{T}_{AB}}
+        =
+        \left[Ad_{\boldsymbol{T}_{BA}^{-1}}\right]
+        \boldsymbol{\Sigma}_{\boldsymbol{T}_{BA}}
+        \left[Ad_{\boldsymbol{T}_{BA}^{-1}}\right]^T
+
+    Parameters
+    ----------
+    mean : array-like, shape (4, 4)
+        Mean of transform from frame A to frame B.
+
+    cov : array, shape (6, 6)
+        Covariance of transform from frame A to frame B in exponential
+        coordinate space.
+
+    Returns
+    -------
+    mean_inv : array, shape (4, 4)
+        Mean of transform from frame B to frame A.
+
+    cov_inv : array, shape (6, 6)
+        Covariance of transform from frame B to frame A in exponential
+        coordinate space.
+
+    References
+    ----------
+    Mangelson, Ghaffari, Vasudevan, Eustice: Characterizing the Uncertainty of
+    Jointly Distributed Poses in the Lie Algebra,
+    https://arxiv.org/pdf/1906.07795.pdf
+    """
+    mean_inv = invert_transform(mean)
+    ad_inv = adjoint_from_transform(mean_inv)
+    cov_inv = np.dot(ad_inv, np.dot(cov, ad_inv.T))
+    return mean_inv, cov_inv
+
+
+def concat_globally_uncertain_transforms(mean_A2B, cov_A2B, mean_B2C, cov_B2C):
+    r"""Concatenate two independent globally uncertain transformations.
+
+    We assume that the two distributions are independent.
+
+    Each of the two transformations is globally uncertain (not in the local /
+    body frame), that is, samples are generated through
+
+    .. math::
+
+        \boldsymbol{T} = Exp(\boldsymbol{\xi}) \overline{\boldsymbol{T}},
+
+    where :math:`\boldsymbol{T} \in SE(3)` is a sampled transformation matrix,
+    :math:`\overline{\boldsymbol{T}} \in SE(3)` is the mean transformation,
+    and :math:`\boldsymbol{\xi} \in \mathbb{R}^6` are exponential coordinates
+    of transformations and are distributed according to a Gaussian
+    distribution with zero mean and covariance :math:`\boldsymbol{\Sigma} \in
+    \mathbb{R}^{6 \times 6}`, that is, :math:`\boldsymbol{\xi} \sim
+    \mathcal{N}(\boldsymbol{0}, \boldsymbol{\Sigma})`.
+
+    The concatenation order is the same as in
+    :func:`~pytransform3d.transformations.concat`, that is, the transformation
+    B2C is left-multiplied to A2B.
+
+    This version of Barfoot and Furgale approximates the covariance up to
+    4th-order terms. Note that it is still an approximation of the covariance
+    after concatenation of the two transforms.
+
+    Parameters
+    ----------
+    mean_A2B : array, shape (4, 4)
+        Mean of transform from A to B.
+
+    cov_A2B : array, shape (6, 6)
+        Covariance of transform from A to B.
+
+    mean_B2C : array, shape (4, 4)
+        Mean of transform from B to C.
+
+    cov_B2C : array, shape (6, 6)
+        Covariance of transform from B to C.
+
+    Returns
+    -------
+    mean_A2C : array, shape (4, 4)
+        Mean of new pose.
+
+    cov_A2C : array, shape (6, 6)
+        Covariance of new pose.
+
+    References
+    ----------
+    Barfoot, Furgale: Associating Uncertainty With Three-Dimensional Poses for
+    Use in Estimation Problems,
+    http://ncfrn.mcgill.ca/members/pubs/barfoot_tro14.pdf
+    """
+    mean_A2C = concat(mean_A2B, mean_B2C)
+
+    ad_B2C = adjoint_from_transform(mean_B2C)
+    cov_A2B_in_C = np.dot(ad_B2C, np.dot(cov_A2B, ad_B2C.T))
+    second_order_terms = cov_B2C + cov_A2B_in_C
+
+    cov_A2C = second_order_terms + _compound_cov_fourth_order_terms(
+        cov_B2C, cov_A2B_in_C)
+
+    return mean_A2C, cov_A2C
+
+
+def _compound_cov_fourth_order_terms(cov1, cov2_prime):
+    cov1 = _swap_cov(cov1)
+    cov2_prime = _swap_cov(cov2_prime)
+
+    cov1_11 = cov1[:3, :3]
+    cov1_22 = cov1[3:, 3:]
+    cov1_12 = cov1[:3, 3:]
+
+    cov2_11 = cov2_prime[:3, :3]
+    cov2_22 = cov2_prime[3:, 3:]
+    cov2_12 = cov2_prime[:3, 3:]
+
+    A1 = np.block([
+        [_covop1(cov1_22), _covop1(cov1_12 + cov1_12.T)],
+        [np.zeros((3, 3)), _covop1(cov1_22)]
+    ])
+    A2 = np.block([
+        [_covop1(cov2_22), _covop1(cov2_12 + cov2_12.T)],
+        [np.zeros((3, 3)), _covop1(cov2_22)]
+    ])
+    B_11 = (_covop2(cov1_22, cov2_11) + _covop2(cov1_12.T, cov2_12)
+            + _covop2(cov1_12, cov2_12.T) + _covop2(cov1_11, cov2_22))
+    B_12 = _covop2(cov1_22, cov2_12.T) + _covop2(cov1_12.T, cov2_22)
+    B_22 = _covop2(cov1_22, cov2_22)
+    B = np.block([
+        [B_11, B_12],
+        [B_12.T, B_22]
+    ])
+
+    return _swap_cov(
+        (np.dot(A1, cov2_prime) + np.dot(cov2_prime, A1.T)
+         + np.dot(A2, cov1) + np.dot(cov1, A2.T)) / 12.0
+        + B / 4.0
+    )
+
+
+def _swap_cov(cov):
+    return np.block([
+        [cov[3:, 3:], cov[3:, :3]],
+        [cov[:3, 3:], cov[:3, :3]]
+    ])
+
+
+def _covop1(A):
+    return -np.trace(A) * np.eye(len(A)) + A
+
+
+def _covop2(A, B):
+    return np.dot(_covop1(A), _covop1(B)) + _covop1(np.dot(B, A))
+
+
+def pose_fusion(means, covs):
+    """Fuse Gaussian distributions of multiple poses.
+
+    Parameters
+    ----------
+    means : array-like, shape (n_poses, 4, 4)
+        Homogeneous transformation matrices.
+
+    covs : array-like, shape (n_poses, 6, 6)
+        Covariances of pose distributions in exponential coordinate space.
+
+    Returns
+    -------
+    mean : array, shape (4, 4)
+        Fused pose mean.
+
+    cov : array, shape (6, 6)
+        Fused pose covariance.
+
+    V : float
+        Error of optimization objective.
+
+    References
+    ----------
+    Barfoot, Furgale: Associating Uncertainty With Three-Dimensional Poses for
+    Use in Estimation Problems,
+    http://ncfrn.mcgill.ca/members/pubs/barfoot_tro14.pdf
+    """
+    n_poses = len(means)
+    covs_inv = [np.linalg.inv(cov) for cov in covs]
+
+    mean = np.eye(4)
+    LHS = np.empty((6, 6))
+    RHS = np.empty(6)
+    for _ in range(20):
+        LHS[:, :] = 0.0
+        RHS[:] = 0.0
+        for k in range(n_poses):
+            x_ik = exponential_coordinates_from_transform(
+                np.dot(mean, invert_transform(means[k])))
+            J_inv = left_jacobian_SE3_inv(x_ik)
+            J_invT_S = np.dot(J_inv.T, covs_inv[k])
+            LHS += np.dot(J_invT_S, J_inv)
+            RHS += np.dot(J_invT_S, x_ik)
+        x_i = np.linalg.solve(-LHS, RHS)
+        mean = np.dot(transform_from_exponential_coordinates(x_i), mean)
+
+    cov = np.linalg.inv(LHS)
+
+    V = 0.0
+    for k in range(n_poses):
+        x_ik = exponential_coordinates_from_transform(
+            np.dot(mean, invert_transform(means[k])))
+        V += 0.5 * np.dot(x_ik, np.dot(covs_inv[k], x_ik))
+    return mean, cov, V
+
+
+def to_ellipsoid(mean, cov):
+    """Compute error ellipsoid.
+
+    An error ellipsoid indicates the equiprobable surface. The resulting
+    ellipsoid includes one standard deviation of the data along each main
+    axis, which covers approximately 68.27% of the data. Multiplying the
+    radii with factors > 1 will increase the coverage. The usual factors
+    for Gaussian distributions apply:
+
+    * 1 - 68.27%
+    * 1.65 - 90%
+    * 1.96 - 95%
+    * 2 - 95.45%
+    * 2.58 - 99%
+    * 3 - 99.73%
+
+    Parameters
+    ----------
+    mean : array-like, shape (3,)
+        Mean of distribution.
+
+    cov : array-like, shape (3, 3)
+        Covariance of distribution.
+
+    Returns
+    -------
+    ellipsoid2origin : array, shape (4, 4)
+        Ellipsoid frame in world frame. Note that there are multiple solutions
+        possible for the orientation because an ellipsoid is symmetric.
+        A body-fixed rotation around a main axis by 180 degree results in the
+        same ellipsoid.
+
+    radii : array, shape (3,)
+        Radii of ellipsoid, coinciding with standard deviations along the
+        three axes of the ellipsoid. These are sorted in ascending order.
+    """
+    from scipy import linalg
+    radii, R = linalg.eigh(cov)
+    if np.linalg.det(R) < 0:  # undo reflection (exploit symmetry)
+        R *= -1
+    ellipsoid2origin = transform_from(R=R, p=mean)
+    return ellipsoid2origin, np.sqrt(np.abs(radii))
+
+
+def to_projected_ellipsoid(mean, cov, factor=1.96, n_steps=20):
+    """Compute projected error ellipsoid.
+
+    An error ellipsoid shows equiprobable points. This is a projection of a
+    Gaussian distribution in exponential coordinate space to 3D.
+
+    Parameters
+    ----------
+    mean : array-like, shape (4, 4)
+        Mean of pose distribution.
+
+    cov : array-like, shape (6, 6)
+        Covariance of pose distribution in exponential coordinate space.
+
+    factor : float, optional (default: 1.96)
+        Multiple of the standard deviations that should be plotted.
+
+    n_steps : int, optional (default: 20)
+        Number of discrete steps plotted in each dimension.
+
+    Returns
+    -------
+    x : array, shape (n_steps, n_steps)
+        Coordinates on x-axis of grid on projected ellipsoid.
+
+    y : array, shape (n_steps, n_steps)
+        Coordinates on y-axis of grid on projected ellipsoid.
+
+    z : array, shape (n_steps, n_steps)
+        Coordinates on z-axis of grid on projected ellipsoid.
+    """
+    from scipy import linalg
+    vals, vecs = linalg.eigh(cov)
+    order = vals.argsort()[::-1]
+    vals, vecs = vals[order], vecs[:, order]
+
+    radii = factor * np.sqrt(vals)
+
+    # Grid on ellipsoid in exponential coordinate space
+    radius_x, radius_y, radius_z = radii[:3]
+    phi, theta = np.mgrid[0.0:np.pi:n_steps * 1j, 0.0:2.0 * np.pi:n_steps * 1j]
+    x = radius_x * np.sin(phi) * np.cos(theta)
+    y = radius_y * np.sin(phi) * np.sin(theta)
+    z = radius_z * np.cos(phi)
+    P = np.column_stack((x.reshape(-1), y.reshape(-1), z.reshape(-1)))
+    P = np.dot(P, vecs[:, :3].T)
+
+    # Grid in Cartesian space
+    T_diff = transforms_from_exponential_coordinates(P)
+    # same as T_diff[m, :3, :3].T.dot(T_diff[m, :3, 3]) for each m
+    P = np.einsum("ikj,ik->ij", T_diff[:, :3, :3], T_diff[:, :3, 3])
+    P = (np.dot(P, mean[:3, :3].T) + mean[np.newaxis, :3, 3]).T
+
+    shape = x.shape
+    x = P[0].reshape(*shape)
+    y = P[1].reshape(*shape)
+    z = P[2].reshape(*shape)
+
+    return x, y, z
+
+
+def plot_projected_ellipsoid(
+        ax, mean, cov, factor=1.96, wireframe=True, n_steps=20, color=None,
+        alpha=1.0):  # pragma: no cover
+    """Plots projected equiprobable ellipsoid in 3D.
+
+    An error ellipsoid shows equiprobable points. This is a projection of a
+    Gaussian distribution in exponential coordinate space to 3D.
+
+    Parameters
+    ----------
+    ax : axis
+        Matplotlib axis.
+
+    mean : array-like, shape (4, 4)
+        Mean pose.
+
+    cov : array-like, shape (6, 6)
+        Covariance in exponential coordinate space.
+
+    factor : float, optional (default: 1.96)
+        Multiple of the standard deviations that should be plotted.
+
+    wireframe : bool, optional (default: True)
+        Plot wireframe of ellipsoid and surface otherwise.
+
+    n_steps : int, optional (default: 20)
+        Number of discrete steps plotted in each dimension.
+
+    color : str, optional (default: None)
+        Color in which the equiprobably lines should be plotted.
+
+    alpha : float, optional (default: 1.0)
+        Alpha value for lines.
+
+    Returns
+    -------
+    ax : axis
+        Matplotlib axis.
+    """
+    x, y, z = to_projected_ellipsoid(mean, cov, factor, n_steps)
+
+    if wireframe:
+        ax.plot_wireframe(
+            x, y, z, rstride=2, cstride=2, color=color, alpha=alpha)
+    else:
+        ax.plot_surface(x, y, z, color=color, alpha=alpha, linewidth=0)
+
+    return ax
diff --git a/pytransform3d/uncertainty.pyi b/pytransform3d/uncertainty.pyi
new file mode 100644
index 000000000..c753c00fb
--- /dev/null
+++ b/pytransform3d/uncertainty.pyi
@@ -0,0 +1,39 @@
+import numpy as np
+import numpy.typing as npt
+from typing import Tuple, Union
+from mpl_toolkits.mplot3d import Axes3D
+
+
+def estimate_gaussian_transform_from_samples(
+        samples: npt.ArrayLike) -> Tuple[np.ndarray, np.ndarray]: ...
+
+
+def invert_uncertain_transform(
+        mean: npt.ArrayLike,
+        cov: npt.ArrayLike) -> Tuple[np.ndarray, np.ndarray]: ...
+
+
+def concat_globally_uncertain_transforms(
+        mean_A2B: npt.ArrayLike, cov_A2B: npt.ArrayLike,
+        mean_B2C: npt.ArrayLike, cov_B2C: npt.ArrayLike) -> np.ndarray: ...
+
+
+def pose_fusion(means: npt.ArrayLike, covs: npt.ArrayLike) -> np.ndarray: ...
+
+
+def to_ellipsoid(mean: npt.ArrayLike,
+                 cov: npt.ArrayLike) -> Tuple[np.ndarray, np.ndarray]: ...
+
+
+def to_projected_ellipsoid(
+        mean: npt.ArrayLike, cov: npt.ArrayLike,
+        factor: float = ..., n_steps: int = ...
+) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: ...
+
+
+def plot_projected_ellipsoid(
+    ax: Union[None, Axes3D],
+    mean: npt.ArrayLike, cov: npt.ArrayLike,
+    factor: float = ..., wireframe: bool = ..., n_steps: int = ...,
+    color: str = ..., alpha: float = ...
+) -> Axes3D: ...
diff --git a/pytransform3d/visualizer/_figure.py b/pytransform3d/visualizer/_figure.py
index c5502896d..bc7d70d7e 100644
--- a/pytransform3d/visualizer/_figure.py
+++ b/pytransform3d/visualizer/_figure.py
@@ -505,7 +505,8 @@ def plot_capsule(self, height=1, radius=1, A2B=np.eye(4), resolution=20,
                      c=None):
         """Plot capsule.
 
-        A capsule is the volume covered by a sphere moving along a line segment.
+        A capsule is the volume covered by a sphere moving along a line
+        segment.
 
         Parameters
         ----------


Test patch:
diff --git a/pytransform3d/test/test_batch_rotations.py b/pytransform3d/test/test_batch_rotations.py
index 00a343316..2b1c68edb 100644
--- a/pytransform3d/test/test_batch_rotations.py
+++ b/pytransform3d/test/test_batch_rotations.py
@@ -6,31 +6,31 @@
 
 
 def test_norm_vectors_0dims():
-    random_state = np.random.RandomState(8380)
-    V = random_state.randn(3)
+    rng = np.random.default_rng(8380)
+    V = rng.standard_normal(size=3)
     V_unit = pbr.norm_vectors(V)
     assert pytest.approx(np.linalg.norm(V_unit)) == 1.0
 
 
 def test_norm_vectors_1dim():
-    random_state = np.random.RandomState(8381)
-    V = random_state.randn(100, 3)
+    rng = np.random.default_rng(8381)
+    V = rng.standard_normal(size=(100, 3))
     V_unit = pbr.norm_vectors(V)
     assert_array_almost_equal(
         np.linalg.norm(V_unit, axis=1), np.ones(len(V)))
 
 
 def test_norm_vectors_1dim_output_variable():
-    random_state = np.random.RandomState(8381)
-    V = random_state.randn(100, 3)
+    rng = np.random.default_rng(8381)
+    V = rng.standard_normal(size=(100, 3))
     pbr.norm_vectors(V, out=V)
     assert_array_almost_equal(
         np.linalg.norm(V, axis=1), np.ones(len(V)))
 
 
 def test_norm_vectors_3dims():
-    random_state = np.random.RandomState(8382)
-    V = random_state.randn(8, 2, 8, 3)
+    rng = np.random.default_rng(8382)
+    V = rng.standard_normal(size=(8, 2, 8, 3))
     V_unit = pbr.norm_vectors(V)
     assert_array_almost_equal(
         np.linalg.norm(V_unit, axis=-1), np.ones(V_unit.shape[:-1]))
@@ -43,27 +43,27 @@ def test_norm_vectors_zero():
 
 
 def test_angles_between_vectors_0dims():
-    random_state = np.random.RandomState(228)
-    A = random_state.randn(3)
-    B = random_state.randn(3)
+    rng = np.random.default_rng(228)
+    A = rng.standard_normal(size=3)
+    B = rng.standard_normal(size=3)
     angles = pbr.angles_between_vectors(A, B)
     angles2 = pr.angle_between_vectors(A, B)
     assert_array_almost_equal(angles, angles2)
 
 
 def test_angles_between_vectors_1dim():
-    random_state = np.random.RandomState(229)
-    A = random_state.randn(100, 3)
-    B = random_state.randn(100, 3)
+    rng = np.random.default_rng(229)
+    A = rng.standard_normal(size=(100, 3))
+    B = rng.standard_normal(size=(100, 3))
     angles = pbr.angles_between_vectors(A, B)
     angles2 = [pr.angle_between_vectors(a, b) for a, b in zip(A, B)]
     assert_array_almost_equal(angles, angles2)
 
 
 def test_angles_between_vectors_3dims():
-    random_state = np.random.RandomState(230)
-    A = random_state.randn(2, 4, 3, 4)
-    B = random_state.randn(2, 4, 3, 4)
+    rng = np.random.default_rng(230)
+    A = rng.standard_normal(size=(2, 4, 3, 4))
+    B = rng.standard_normal(size=(2, 4, 3, 4))
     angles = pbr.angles_between_vectors(A, B).ravel()
     angles2 = [pr.angle_between_vectors(a, b)
                for a, b in zip(A.reshape(-1, 4), B.reshape(-1, 4))]
@@ -83,8 +83,8 @@ def test_active_matrices_from_angles_1dim():
 
 
 def test_active_matrices_from_angles_3dims():
-    random_state = np.random.RandomState(8383)
-    angles = random_state.randn(2, 3, 4)
+    rng = np.random.default_rng(8383)
+    angles = rng.standard_normal(size=(2, 3, 4))
     Rs = pbr.active_matrices_from_angles(2, angles)
     Rs = Rs.reshape(-1, 3, 3)
     Rs2 = [pr.active_matrix_from_angle(2, angle)
@@ -93,8 +93,8 @@ def test_active_matrices_from_angles_3dims():
 
 
 def test_active_matrices_from_angles_3dims_output_variable():
-    random_state = np.random.RandomState(8384)
-    angles = random_state.randn(2, 3, 4)
+    rng = np.random.default_rng(8384)
+    angles = rng.standard_normal(size=(2, 3, 4))
     Rs = np.empty((2, 3, 4, 3, 3))
     pbr.active_matrices_from_angles(2, angles, out=Rs)
     Rs = Rs.reshape(-1, 3, 3)
@@ -104,16 +104,16 @@ def test_active_matrices_from_angles_3dims_output_variable():
 
 
 def test_active_matrices_from_intrinsic_euler_angles_0dims():
-    random_state = np.random.RandomState(8383)
-    e = random_state.randn(3)
+    rng = np.random.default_rng(8383)
+    e = rng.standard_normal(size=3)
     R = pbr.active_matrices_from_intrinsic_euler_angles(2, 1, 0, e)
     R2 = pr.active_matrix_from_intrinsic_euler_zyx(e)
     assert_array_almost_equal(R, R2)
 
 
 def test_active_matrices_from_intrinsic_euler_angles_1dim():
-    random_state = np.random.RandomState(8384)
-    e = random_state.randn(10, 3)
+    rng = np.random.default_rng(8384)
+    e = rng.standard_normal(size=(10, 3))
     Rs = pbr.active_matrices_from_intrinsic_euler_angles(2, 1, 0, e)
     for i in range(len(e)):
         Ri = pr.active_matrix_from_intrinsic_euler_zyx(e[i])
@@ -121,8 +121,8 @@ def test_active_matrices_from_intrinsic_euler_angles_1dim():
 
 
 def test_active_matrices_from_intrinsic_euler_angles_1dim_output_variables():
-    random_state = np.random.RandomState(8384)
-    e = random_state.randn(10, 3)
+    rng = np.random.default_rng(8384)
+    e = rng.standard_normal(size=(10, 3))
     Rs = np.empty((10, 3, 3))
     pbr.active_matrices_from_intrinsic_euler_angles(2, 1, 0, e, out=Rs)
     for i in range(len(e)):
@@ -131,8 +131,8 @@ def test_active_matrices_from_intrinsic_euler_angles_1dim_output_variables():
 
 
 def test_active_matrices_from_intrinsic_euler_angles_3dims():
-    random_state = np.random.RandomState(8385)
-    e = random_state.randn(2, 3, 4, 3)
+    rng = np.random.default_rng(8385)
+    e = rng.standard_normal(size=(2, 3, 4, 3))
     Rs = pbr.active_matrices_from_intrinsic_euler_angles(
         2, 1, 0, e).reshape(-1, 3, 3)
     e = e.reshape(-1, 3)
@@ -142,16 +142,16 @@ def test_active_matrices_from_intrinsic_euler_angles_3dims():
 
 
 def test_active_matrices_from_extrinsic_euler_angles_0dims():
-    random_state = np.random.RandomState(8383)
-    e = random_state.randn(3)
+    rng = np.random.default_rng(8383)
+    e = rng.standard_normal(size=3)
     R = pbr.active_matrices_from_extrinsic_euler_angles(2, 1, 0, e)
     R2 = pr.active_matrix_from_extrinsic_euler_zyx(e)
     assert_array_almost_equal(R, R2)
 
 
 def test_active_matrices_from_extrinsic_euler_angles_1dim():
-    random_state = np.random.RandomState(8384)
-    e = random_state.randn(10, 3)
+    rng = np.random.default_rng(8384)
+    e = rng.standard_normal(size=(10, 3))
     Rs = pbr.active_matrices_from_extrinsic_euler_angles(2, 1, 0, e)
     for i in range(len(e)):
         Ri = pr.active_matrix_from_extrinsic_euler_zyx(e[i])
@@ -159,8 +159,8 @@ def test_active_matrices_from_extrinsic_euler_angles_1dim():
 
 
 def test_active_matrices_from_extrinsic_euler_angles_3dim():
-    random_state = np.random.RandomState(8385)
-    e = random_state.randn(2, 3, 4, 3)
+    rng = np.random.default_rng(8385)
+    e = rng.standard_normal(size=(2, 3, 4, 3))
     Rs = pbr.active_matrices_from_extrinsic_euler_angles(
         2, 1, 0, e).reshape(-1, 3, 3)
     e = e.reshape(-1, 3)
@@ -170,8 +170,8 @@ def test_active_matrices_from_extrinsic_euler_angles_3dim():
 
 
 def test_active_matrices_from_extrinsic_euler_angles_1dim_output_variable():
-    random_state = np.random.RandomState(8385)
-    e = random_state.randn(10, 3)
+    rng = np.random.default_rng(8385)
+    e = rng.standard_normal(size=(10, 3))
     Rs = np.empty((10, 3, 3))
     pbr.active_matrices_from_extrinsic_euler_angles(2, 1, 0, e, out=Rs)
     for i in range(len(e)):
@@ -180,15 +180,15 @@ def test_active_matrices_from_extrinsic_euler_angles_1dim_output_variable():
 
 
 def test_cross_product_matrix():
-    random_state = np.random.RandomState(3820)
-    v = random_state.randn(3)
+    rng = np.random.default_rng(3820)
+    v = rng.standard_normal(size=3)
     assert_array_almost_equal(
         pbr.cross_product_matrices(v), pr.cross_product_matrix(v))
 
 
 def test_cross_product_matrices():
-    random_state = np.random.RandomState(3820)
-    V = random_state.randn(2, 2, 3, 3)
+    rng = np.random.default_rng(3820)
+    V = rng.standard_normal(size=(2, 2, 3, 3))
     V_cpm = pbr.cross_product_matrices(V)
     V_cpm = V_cpm.reshape(-1, 3, 3)
     V_cpm2 = [pr.cross_product_matrix(v) for v in V.reshape(-1, 3)]
@@ -196,16 +196,16 @@ def test_cross_product_matrices():
 
 
 def test_matrices_from_quaternions():
-    random_state = np.random.RandomState(83)
+    rng = np.random.default_rng(83)
     for _ in range(5):
-        q = pr.random_quaternion(random_state)
+        q = pr.random_quaternion(rng)
         R = pbr.matrices_from_quaternions(
             q[np.newaxis], normalize_quaternions=False)[0]
         q2 = pr.quaternion_from_matrix(R)
         pr.assert_quaternion_equal(q, q2)
 
     for _ in range(5):
-        q = random_state.randn(4)
+        q = rng.standard_normal(size=4)
         R = pbr.matrices_from_quaternions(
             q[np.newaxis], normalize_quaternions=True)[0]
         q2 = pr.quaternion_from_matrix(R)
@@ -213,9 +213,9 @@ def test_matrices_from_quaternions():
 
 
 def test_quaternions_from_matrices():
-    random_state = np.random.RandomState(84)
+    rng = np.random.default_rng(84)
     for _ in range(5):
-        q = pr.random_quaternion(random_state)
+        q = pr.random_quaternion(rng)
         R = pr.matrix_from_quaternion(q)
         q2 = pbr.quaternions_from_matrices(R[np.newaxis])[0]
         pr.assert_quaternion_equal(q, q2)
@@ -240,9 +240,9 @@ def test_quaternions_from_matrices():
 
 
 def test_quaternions_from_matrices_no_batch():
-    random_state = np.random.RandomState(85)
+    rng = np.random.default_rng(85)
     for _ in range(5):
-        q = pr.random_quaternion(random_state)
+        q = pr.random_quaternion(rng)
         R = pr.matrix_from_quaternion(q)
         q2 = pbr.quaternions_from_matrices(R)
         pr.assert_quaternion_equal(q, q2)
@@ -267,9 +267,9 @@ def test_quaternions_from_matrices_no_batch():
 
 
 def test_quaternions_from_matrices_4d():
-    random_state = np.random.RandomState(84)
+    rng = np.random.default_rng(84)
     for _ in range(5):
-        q = pr.random_quaternion(random_state)
+        q = pr.random_quaternion(rng)
         R = pr.matrix_from_quaternion(q)
         q2 = pbr.quaternions_from_matrices([[R, R], [R, R]])
         pr.assert_quaternion_equal(q, q2[0, 0])
@@ -279,10 +279,10 @@ def test_quaternions_from_matrices_4d():
 
 
 def test_axis_angles_from_matrices_0dims():
-    random_state = np.random.RandomState(84)
-    A = random_state.randn(3)
+    rng = np.random.default_rng(84)
+    A = rng.standard_normal(size=3)
     A /= np.linalg.norm(A, axis=-1)[..., np.newaxis]
-    A *= random_state.rand() * np.pi
+    A *= rng.random() * np.pi
 
     Rs = pbr.matrices_from_compact_axis_angles(A)
     A2 = pbr.axis_angles_from_matrices(Rs)
@@ -291,10 +291,10 @@ def test_axis_angles_from_matrices_0dims():
 
 
 def test_axis_angles_from_matrices():
-    random_state = np.random.RandomState(84)
-    A = random_state.randn(2, 3, 3)
+    rng = np.random.default_rng(84)
+    A = rng.standard_normal(size=(2, 3, 3))
     A /= np.linalg.norm(A, axis=-1)[..., np.newaxis]
-    A *= random_state.rand(2, 3, 1) * np.pi
+    A *= rng.random(size=(2, 3, 1)) * np.pi
     A[0, 0, :] = 0.0
 
     Rs = pbr.matrices_from_compact_axis_angles(A)
@@ -304,10 +304,10 @@ def test_axis_angles_from_matrices():
 
 
 def test_axis_angles_from_matrices_output_variable():
-    random_state = np.random.RandomState(84)
-    A = random_state.randn(2, 3, 3)
+    rng = np.random.default_rng(84)
+    A = rng.standard_normal(size=(2, 3, 3))
     A /= np.linalg.norm(A, axis=-1)[..., np.newaxis]
-    A *= random_state.rand(2, 3, 1) * np.pi
+    A *= rng.random(size=(2, 3, 1)) * np.pi
     A[0, 0, :] = 0.0
 
     Rs = np.empty((2, 3, 3, 3))
@@ -319,16 +319,16 @@ def test_axis_angles_from_matrices_output_variable():
 
 
 def test_quaternion_slerp_batch_zero_angle():
-    random_state = np.random.RandomState(228)
-    q = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(228)
+    q = pr.random_quaternion(rng)
     Q = pbr.quaternion_slerp_batch(q, q, [0.5])
     pr.assert_quaternion_equal(q, Q[0])
 
 
 def test_quaternion_slerp_batch():
-    random_state = np.random.RandomState(229)
-    q_start = pr.random_quaternion(random_state)
-    q_end = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(229)
+    q_start = pr.random_quaternion(rng)
+    q_end = pr.random_quaternion(rng)
     t = np.linspace(0, 1, 101)
     Q = pbr.quaternion_slerp_batch(q_start, q_end, t)
     for i in range(len(t)):
@@ -338,8 +338,8 @@ def test_quaternion_slerp_batch():
 
 def test_quaternion_slerp_batch_sign_ambiguity():
     n_steps = 10
-    random_state = np.random.RandomState(2323)
-    q1 = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(2323)
+    q1 = pr.random_quaternion(rng)
     a1 = pr.axis_angle_from_quaternion(q1)
     a2 = np.r_[a1[:3], a1[3] * 1.1]
     q2 = pr.quaternion_from_axis_angle(a2)
@@ -399,9 +399,9 @@ def test_batch_concatenate_quaternions_mismatch():
 
 
 def test_batch_concatenate_quaternions_1d():
-    random_state = np.random.RandomState(230)
-    q1 = pr.random_quaternion(random_state)
-    q2 = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(230)
+    q1 = pr.random_quaternion(rng)
+    q2 = pr.random_quaternion(rng)
     q12 = np.empty(4)
     pbr.batch_concatenate_quaternions(q1, q2, out=q12)
     assert_array_almost_equal(
@@ -409,14 +409,14 @@ def test_batch_concatenate_quaternions_1d():
 
 
 def test_batch_q_conj_1d():
-    random_state = np.random.RandomState(230)
-    q = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(230)
+    q = pr.random_quaternion(rng)
     assert_array_almost_equal(pr.q_conj(q), pbr.batch_q_conj(q))
 
 
 def test_batch_concatenate_q_conj():
-    random_state = np.random.RandomState(231)
-    Q = np.array([pr.random_quaternion(random_state)
+    rng = np.random.default_rng(231)
+    Q = np.array([pr.random_quaternion(rng)
                   for _ in range(10)])
     Q = Q.reshape(2, 5, 4)
 
@@ -441,8 +441,8 @@ def test_batch_convert_quaternion_conventions():
     pbr.batch_quaternion_wxyz_from_xyzw(q_xyzw, out=q_wxyz2)
     assert_array_almost_equal(q_wxyz, q_wxyz2)
 
-    random_state = np.random.RandomState(42)
-    q_wxyz_random = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(42)
+    q_wxyz_random = pr.random_quaternion(rng)
     q_xyzw_random = pbr.batch_quaternion_xyzw_from_wxyz(q_wxyz_random)
     assert_array_almost_equal(q_xyzw_random[:3], q_wxyz_random[1:])
     assert q_xyzw_random[3] == q_wxyz_random[0]
@@ -451,11 +451,11 @@ def test_batch_convert_quaternion_conventions():
 
 
 def test_smooth_quaternion_trajectory():
-    random_state = np.random.RandomState(232)
-    q_start = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(232)
+    q_start = pr.random_quaternion(rng)
     if q_start[1] < 0.0:
         q_start *= -1.0
-    q_goal = pr.random_quaternion(random_state)
+    q_goal = pr.random_quaternion(rng)
     n_steps = 101
     Q = np.empty((n_steps, 4))
     for i, t in enumerate(np.linspace(0, 1, n_steps)):
@@ -468,11 +468,11 @@ def test_smooth_quaternion_trajectory():
 
 
 def test_smooth_quaternion_trajectory_start_component_negative():
-    random_state = np.random.RandomState(232)
+    rng = np.random.default_rng(232)
 
     for index in range(4):
         component = "wxyz"[index]
-        q = pr.random_quaternion(random_state)
+        q = pr.random_quaternion(rng)
         if q[index] > 0.0:
             q *= -1.0
         q_corrected = pbr.smooth_quaternion_trajectory(
diff --git a/pytransform3d/test/test_coordinates.py b/pytransform3d/test/test_coordinates.py
index bdba01d88..f3b423516 100644
--- a/pytransform3d/test/test_coordinates.py
+++ b/pytransform3d/test/test_coordinates.py
@@ -14,11 +14,11 @@ def test_cartesian_from_cylindrical_edge_cases():
 
 
 def test_convert_cartesian_cylindrical():
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for i in range(1000):
-        rho = random_state.randn()
-        phi = random_state.rand() * 4 * np.pi - 2 * np.pi
-        z = random_state.randn()
+        rho = rng.standard_normal()
+        phi = rng.random() * 4 * np.pi - 2 * np.pi
+        z = rng.standard_normal()
         p = np.array([rho, phi, z])
         q = pc.cartesian_from_cylindrical(p)
         r = pc.cylindrical_from_cartesian(q)
@@ -39,11 +39,11 @@ def test_cartesian_from_spherical_edge_cases():
 
 
 def test_convert_cartesian_spherical():
-    random_state = np.random.RandomState(1)
+    rng = np.random.default_rng(1)
     for i in range(1000):
-        rho = random_state.randn()
-        theta = random_state.rand() * 4 * np.pi - 2 * np.pi
-        phi = random_state.rand() * 4 * np.pi - 2 * np.pi
+        rho = rng.standard_normal()
+        theta = rng.random() * 4 * np.pi - 2 * np.pi
+        phi = rng.random() * 4 * np.pi - 2 * np.pi
         p = np.array([rho, theta, phi])
         q = pc.cartesian_from_spherical(p)
         r = pc.spherical_from_cartesian(q)
@@ -65,13 +65,40 @@ def test_cylindrical_from_spherical_edge_cases():
 
 
 def test_convert_cylindrical_spherical():
-    random_state = np.random.RandomState(2)
+    rng = np.random.default_rng(2)
     for i in range(1000):
-        rho = random_state.randn()
-        theta = random_state.rand() * 4 * np.pi - 2 * np.pi
-        phi = random_state.rand() * 4 * np.pi - 2 * np.pi
+        rho = rng.standard_normal()
+        theta = rng.random() * 4 * np.pi - 2 * np.pi
+        phi = rng.random() * 4 * np.pi - 2 * np.pi
         p = np.array([rho, theta, phi])
         q = pc.cylindrical_from_spherical(p)
         r = pc.spherical_from_cylindrical(q)
         s = pc.cylindrical_from_spherical(r)
         assert_array_almost_equal(q, s)
+
+
+def test_integer_inputs():
+    assert_array_almost_equal(
+        pc.spherical_from_cylindrical([1, 0, 0]),
+        pc.spherical_from_cylindrical([1.0, 0.0, 0.0]),
+    )
+    assert_array_almost_equal(
+        pc.spherical_from_cartesian([1, 0, 0]),
+        pc.spherical_from_cartesian([1.0, 0.0, 0.0]),
+    )
+    assert_array_almost_equal(
+        pc.cylindrical_from_cartesian([0, 1, 0]),
+        pc.cylindrical_from_cartesian([0.0, 1.0, 0.0]),
+    )
+    assert_array_almost_equal(
+        pc.cartesian_from_cylindrical([1, 1, 0]),
+        pc.cartesian_from_cylindrical([1.0, 1.0, 0.0]),
+    )
+    assert_array_almost_equal(
+        pc.cartesian_from_spherical([1, 1, 0]),
+        pc.cartesian_from_spherical([1.0, 1.0, 0.0]),
+    )
+    assert_array_almost_equal(
+        pc.cylindrical_from_spherical([1, 1, 0]),
+        pc.cylindrical_from_spherical([1.0, 1.0, 0.0]),
+    )
diff --git a/pytransform3d/test/test_plot_utils.py b/pytransform3d/test/test_plot_utils.py
index bc5cae6ff..bfe70e190 100644
--- a/pytransform3d/test/test_plot_utils.py
+++ b/pytransform3d/test/test_plot_utils.py
@@ -6,8 +6,8 @@
     pytest.skip("matplotlib is required for these tests")
 from pytransform3d.plot_utils import (
     make_3d_axis, remove_frame, Frame, LabeledFrame, Trajectory,
-    plot_box, plot_sphere, plot_cylinder, plot_mesh, plot_ellipsoid,
-    plot_capsule, plot_cone, plot_vector, plot_length_variable)
+    plot_box, plot_sphere, plot_spheres, plot_cylinder, plot_mesh,
+    plot_ellipsoid, plot_capsule, plot_cone, plot_vector, plot_length_variable)
 from numpy.testing import assert_array_almost_equal
 
 
@@ -70,7 +70,8 @@ def test_frame():
 def test_frame_no_indicator():
     ax = make_3d_axis(1.0)
     try:
-        frame = Frame(np.eye(4), label="Frame", s=0.1, draw_label_indicator=False)
+        frame = Frame(
+            np.eye(4), label="Frame", s=0.1, draw_label_indicator=False)
         frame.add_frame(ax)
         assert len(ax.lines) == 3  # 3 axes and omit black line to text
         assert len(ax.texts) == 1  # label
@@ -96,7 +97,7 @@ def test_trajectory():
             np.array([np.eye(4), np.eye(4)]), s=0.1, n_frames=2)
         trajectory.add_trajectory(ax)
         assert len(ax.lines) == 7  # 2 * 3 axes + connection line
-        #assert_equal(len(ax.artists), 1)  # arrow is not an artist anymore
+        # assert_equal(len(ax.artists), 1)  # arrow is not an artist anymore
     finally:
         ax.remove()
 
@@ -137,6 +138,24 @@ def test_plot_sphere_wireframe():
         ax.remove()
 
 
+def test_plot_spheres():
+    ax = make_3d_axis(1.0)
+    try:
+        plot_spheres(ax, wireframe=False)
+        assert len(ax.collections) == 1
+    finally:
+        ax.remove()
+
+
+def test_plot_spheres_wireframe():
+    ax = make_3d_axis(1.0)
+    try:
+        plot_spheres(ax, wireframe=True)
+        assert len(ax.collections) == 1
+    finally:
+        ax.remove()
+
+
 def test_plot_cylinder():
     ax = make_3d_axis(1.0)
     try:
@@ -239,7 +258,7 @@ def test_plot_vector():
     ax = make_3d_axis(1.0)
     try:
         plot_vector(ax)
-        #assert_equal(len(ax.artists), 1)  # arrow is not an artist anymore
+        # assert_equal(len(ax.artists), 1)  # arrow is not an artist anymore
     finally:
         ax.remove()
 
diff --git a/pytransform3d/test/test_rotations.py b/pytransform3d/test/test_rotations.py
index bf999a433..8b091e64d 100644
--- a/pytransform3d/test/test_rotations.py
+++ b/pytransform3d/test/test_rotations.py
@@ -7,9 +7,9 @@
 
 def test_norm_vector():
     """Test normalization of vectors."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for n in range(1, 6):
-        v = pr.random_vector(random_state, n)
+        v = pr.random_vector(rng, n)
         u = pr.norm_vector(v)
         assert pytest.approx(np.linalg.norm(u)) == 1
 
@@ -22,8 +22,8 @@ def test_norm_zero_vector():
 
 def test_norm_angle():
     """Test normalization of angle."""
-    random_state = np.random.RandomState(0)
-    a_norm = random_state.uniform(-np.pi, np.pi, size=(100,))
+    rng = np.random.default_rng(0)
+    a_norm = rng.uniform(-np.pi, np.pi, size=(100,))
     for b in np.linspace(-10.0 * np.pi, 10.0 * np.pi, 11):
         a = a_norm + b
         assert_array_almost_equal(pr.norm_angle(a), a_norm)
@@ -46,10 +46,10 @@ def test_norm_axis_angle():
     n = pr.norm_axis_angle(a)
     assert_array_almost_equal(a, n)
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        a = pr.random_axis_angle(random_state)
-        angle = random_state.uniform(-20.0, -10.0 + 2.0 * np.pi)
+        a = pr.random_axis_angle(rng)
+        angle = rng.uniform(-20.0, -10.0 + 2.0 * np.pi)
         a[3] = angle
         n = pr.norm_axis_angle(a)
         for angle_offset in np.arange(0.0, 10.1 * np.pi, 2.0 * np.pi):
@@ -72,11 +72,11 @@ def test_norm_compact_axis_angle():
     n = pr.norm_compact_axis_angle(a)
     assert_array_almost_equal(a, n)
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        a = pr.random_compact_axis_angle(random_state)
+        a = pr.random_compact_axis_angle(rng)
         axis = a / np.linalg.norm(a)
-        angle = random_state.uniform(-20.0, -10.0 + 2.0 * np.pi)
+        angle = rng.uniform(-20.0, -10.0 + 2.0 * np.pi)
         a = axis * angle
         n = pr.norm_compact_axis_angle(a)
         for angle_offset in np.arange(0.0, 10.1 * np.pi, 2.0 * np.pi):
@@ -87,9 +87,9 @@ def test_norm_compact_axis_angle():
 
 def test_perpendicular_to_vectors():
     """Test function to compute perpendicular to vectors."""
-    random_state = np.random.RandomState(0)
-    a = pr.norm_vector(pr.random_vector(random_state))
-    a1 = pr.norm_vector(pr.random_vector(random_state))
+    rng = np.random.default_rng(0)
+    a = pr.norm_vector(pr.random_vector(rng))
+    a1 = pr.norm_vector(pr.random_vector(rng))
     b = pr.norm_vector(pr.perpendicular_to_vectors(a, a1))
     c = pr.norm_vector(pr.perpendicular_to_vectors(a, b))
     assert pytest.approx(pr.angle_between_vectors(a, b)) == np.pi / 2.0
@@ -107,9 +107,9 @@ def test_perpendicular_to_vector():
         pr.unity, pr.perpendicular_to_vector(pr.unity))) == np.pi / 2.0
     assert pytest.approx(pr.angle_between_vectors(
         pr.unitz, pr.perpendicular_to_vector(pr.unitz))) == np.pi / 2.0
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        a = pr.norm_vector(pr.random_vector(random_state))
+        a = pr.norm_vector(pr.random_vector(rng))
         assert pytest.approx(pr.angle_between_vectors(
             a, pr.perpendicular_to_vector(a))) == np.pi / 2.0
         b = a - np.array([a[0], 0.0, 0.0])
@@ -165,9 +165,9 @@ def test_angle_to_zero_vector_is_nan():
 
 def test_vector_projection_on_zero_vector():
     """Test projection on zero vector."""
-    random_state = np.random.RandomState(23)
+    rng = np.random.default_rng(23)
     for _ in range(5):
-        a = pr.random_vector(random_state, 3)
+        a = pr.random_vector(rng, 3)
         a_on_b = pr.vector_projection(a, np.zeros(3))
         assert_array_almost_equal(a_on_b, np.zeros(3))
 
@@ -177,15 +177,18 @@ def test_vector_projection():
     a = np.ones(3)
     a_on_unitx = pr.vector_projection(a, pr.unitx)
     assert_array_almost_equal(a_on_unitx, pr.unitx)
-    assert pytest.approx(pr.angle_between_vectors(a_on_unitx, pr.unitx)) == 0.0
+    assert pytest.approx(
+        pr.angle_between_vectors(a_on_unitx, pr.unitx)) == 0.0
 
     a2_on_unitx = pr.vector_projection(2 * a, pr.unitx)
     assert_array_almost_equal(a2_on_unitx, 2 * pr.unitx)
-    assert pytest.approx(pr.angle_between_vectors(a2_on_unitx, pr.unitx)) == 0.0
+    assert pytest.approx(
+        pr.angle_between_vectors(a2_on_unitx, pr.unitx)) == 0.0
 
     a_on_unity = pr.vector_projection(a, pr.unity)
     assert_array_almost_equal(a_on_unity, pr.unity)
-    assert pytest.approx(pr.angle_between_vectors(a_on_unity, pr.unity)) == 0.0
+    assert pytest.approx(
+        pr.angle_between_vectors(a_on_unity, pr.unity)) == 0.0
 
     minus_a_on_unity = pr.vector_projection(-a, pr.unity)
     assert_array_almost_equal(minus_a_on_unity, -pr.unity)
@@ -194,7 +197,8 @@ def test_vector_projection():
 
     a_on_unitz = pr.vector_projection(a, pr.unitz)
     assert_array_almost_equal(a_on_unitz, pr.unitz)
-    assert pytest.approx(pr.angle_between_vectors(a_on_unitz, pr.unitz)) == 0.0
+    assert pytest.approx(
+        pr.angle_between_vectors(a_on_unitz, pr.unitz)) == 0.0
 
     unitz_on_a = pr.vector_projection(pr.unitz, a)
     assert_array_almost_equal(unitz_on_a, np.ones(3) / 3.0)
@@ -218,7 +222,9 @@ def test_check_skew_symmetric_matrix():
         pr.check_skew_symmetric_matrix(np.zeros((4, 3)))
     V = np.zeros((3, 3))
     V[0, 0] = 0.001
-    with pytest.raises(ValueError, match="Expected skew-symmetric matrix, but it failed the test"):
+    with pytest.raises(
+            ValueError,
+            match="Expected skew-symmetric matrix, but it failed the test"):
         pr.check_skew_symmetric_matrix(V)
     with warnings.catch_warnings(record=True) as w:
         pr.check_skew_symmetric_matrix(V, strict_check=False)
@@ -229,10 +235,10 @@ def test_check_skew_symmetric_matrix():
 
 def test_cross_product_matrix():
     """Test cross-product matrix."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        v = pr.random_vector(random_state)
-        w = pr.random_vector(random_state)
+        v = pr.random_vector(rng)
+        w = pr.random_vector(rng)
         V = pr.cross_product_matrix(v)
         pr.check_skew_symmetric_matrix(V)
         r1 = np.cross(v, w)
@@ -288,10 +294,10 @@ def test_check_axis_angle():
     assert type(a) == np.ndarray
     assert a.dtype == np.float64
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     a = np.empty(4)
-    a[:3] = pr.random_vector(random_state, 3)
-    a[3] = random_state.randn() * 4.0 * np.pi
+    a[:3] = pr.random_vector(rng, 3)
+    a[3] = rng.standard_normal() * 4.0 * np.pi
     a2 = pr.check_axis_angle(a)
     pr.assert_axis_angle_equal(a, a2)
     assert pytest.approx(np.linalg.norm(a2[:3])) == 1.0
@@ -314,9 +320,9 @@ def test_check_compact_axis_angle():
     assert type(a) == np.ndarray
     assert a.dtype == np.float64
 
-    random_state = np.random.RandomState(0)
-    a = pr.norm_vector(pr.random_vector(random_state, 3))
-    a *= np.pi + random_state.randn() * 4.0 * np.pi
+    rng = np.random.default_rng(0)
+    a = pr.norm_vector(pr.random_vector(rng, 3))
+    a *= np.pi + rng.standard_normal() * 4.0 * np.pi
     a2 = pr.check_compact_axis_angle(a)
     pr.assert_compact_axis_angle_equal(a, a2)
     assert np.pi > np.linalg.norm(a2) > 0
@@ -337,15 +343,15 @@ def test_check_quaternion():
     assert type(q) == np.ndarray
     assert q.dtype == np.float64
 
-    random_state = np.random.RandomState(0)
-    q = random_state.randn(4)
+    rng = np.random.default_rng(0)
+    q = rng.standard_normal(4)
     q = pr.check_quaternion(q)
     assert pytest.approx(np.linalg.norm(q)) == 1.0
 
     with pytest.raises(ValueError, match="Expected quaternion with shape"):
-         pr.check_quaternion(np.zeros(3))
+        pr.check_quaternion(np.zeros(3))
     with pytest.raises(ValueError, match="Expected quaternion with shape"):
-         pr.check_quaternion(np.zeros((3, 3)))
+        pr.check_quaternion(np.zeros((3, 3)))
 
     q = np.array([0.0, 1.2, 0.0, 0.0])
     q2 = pr.check_quaternion(q, unit=False)
@@ -372,9 +378,11 @@ def test_check_quaternions():
     for i in range(len(Q)):
         assert pytest.approx(np.linalg.norm(Q[i])) == 1
 
-    with pytest.raises(ValueError, match="Expected quaternion array with shape"):
+    with pytest.raises(
+            ValueError, match="Expected quaternion array with shape"):
         pr.check_quaternions(np.zeros(4))
-    with pytest.raises(ValueError, match="Expected quaternion array with shape"):
+    with pytest.raises(
+            ValueError, match="Expected quaternion array with shape"):
         pr.check_quaternions(np.zeros((3, 3)))
 
     Q = np.array([[0.0, 1.2, 0.0, 0.0]])
@@ -385,7 +393,7 @@ def test_check_quaternions():
 def test_passive_matrix_from_angle():
     """Sanity checks for rotation around basis vectors."""
     with pytest.raises(ValueError, match="Basis must be in"):
-         pr.passive_matrix_from_angle(-1, 0)
+        pr.passive_matrix_from_angle(-1, 0)
     with pytest.raises(ValueError, match="Basis must be in"):
         pr.passive_matrix_from_angle(3, 0)
 
@@ -408,14 +416,14 @@ def test_passive_matrix_from_angle():
 def test_active_matrix_from_angle():
     """Sanity checks for rotation around basis vectors."""
     with pytest.raises(ValueError, match="Basis must be in"):
-         pr.active_matrix_from_angle(-1, 0)
+        pr.active_matrix_from_angle(-1, 0)
     with pytest.raises(ValueError, match="Basis must be in"):
         pr.active_matrix_from_angle(3, 0)
 
-    random_state = np.random.RandomState(21)
+    rng = np.random.default_rng(21)
     for i in range(20):
-        basis = random_state.randint(0, 3)
-        angle = 2.0 * np.pi * random_state.rand() - np.pi
+        basis = rng.integers(0, 3)
+        angle = 2.0 * np.pi * rng.random() - np.pi
         R_passive = pr.passive_matrix_from_angle(basis, angle)
         R_active = pr.active_matrix_from_angle(basis, angle)
         assert_array_almost_equal(R_active, R_passive.T)
@@ -587,9 +595,9 @@ def test_active_matrix_from_extrinsic_euler_zyz():
 
 def test_active_matrix_from_intrinsic_zyx():
     """Test conversion from intrinsic zyx Euler angles."""
-    random_state = np.random.RandomState(844)
+    rng = np.random.default_rng(844)
     for _ in range(5):
-        euler_zyx = ((random_state.rand(3) - 0.5) *
+        euler_zyx = ((rng.random(3) - 0.5) *
                      np.array([np.pi, 0.5 * np.pi, np.pi]))
         s = np.sin(euler_zyx)
         c = np.cos(euler_zyx)
@@ -628,9 +636,9 @@ def test_active_matrix_from_intrinsic_zyx():
 
 def test_active_matrix_from_extrinsic_zyx():
     """Test conversion from extrinsic zyx Euler angles."""
-    random_state = np.random.RandomState(844)
+    rng = np.random.default_rng(844)
     for _ in range(5):
-        euler_zyx = ((random_state.rand(3) - 0.5)
+        euler_zyx = ((rng.random(3) - 0.5)
                      * np.array([np.pi, 0.5 * np.pi, np.pi]))
 
         # Normal case, we can reconstruct original angles
@@ -666,9 +674,9 @@ def test_active_matrix_from_extrinsic_zyx():
 def _test_conversion_matrix_euler(
         matrix_from_euler, euler_from_matrix, proper_euler):
     """Test conversions between Euler angles and rotation matrix."""
-    random_state = np.random.RandomState(844)
+    rng = np.random.default_rng(844)
     for _ in range(5):
-        euler = ((random_state.rand(3) - 0.5)
+        euler = ((rng.random(3) - 0.5)
                  * np.array([np.pi, 0.5 * np.pi, np.pi]))
         if proper_euler:
             euler[1] += 0.5 * np.pi
@@ -869,7 +877,7 @@ def test_from_quaternion():
             match="Axis index k \\(3\\) must be in \\[0, 1, 2\\]"):
         pr.euler_from_quaternion(pr.q_id, 2, 0, 3, True)
 
-    random_state = np.random.RandomState(32)
+    rng = np.random.default_rng(32)
 
     euler_axes = [
         [0, 2, 0],
@@ -945,7 +953,7 @@ def test_from_quaternion():
             inv_fun = inverse_functions[fi]
             fi += 1
             for _ in range(5):
-                e = random_state.rand(3)
+                e = rng.random(3)
                 e[0] = 2.0 * np.pi * e[0] - np.pi
                 e[1] = np.pi * e[1]
                 e[2] = 2.0 * np.pi * e[2] - np.pi
@@ -1010,9 +1018,9 @@ def test_conversions_matrix_axis_angle():
     a2 = pr.axis_angle_from_matrix(R)
     pr.assert_axis_angle_equal(a2, a)
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(50):
-        a = pr.random_axis_angle(random_state)
+        a = pr.random_axis_angle(rng)
         R = pr.matrix_from_axis_angle(a)
         pr.assert_rotation_matrix(R)
 
@@ -1080,9 +1088,9 @@ def test_conversions_matrix_compact_axis_angle():
     a2 = pr.compact_axis_angle_from_matrix(R)
     pr.assert_compact_axis_angle_equal(a2, a)
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(50):
-        a = pr.random_compact_axis_angle(random_state)
+        a = pr.random_compact_axis_angle(rng)
         R = pr.matrix_from_compact_axis_angle(a)
         pr.assert_rotation_matrix(R)
 
@@ -1180,9 +1188,9 @@ def test_conversions_matrix_quaternion():
     a = pr.axis_angle_from_matrix(R)
     assert_array_almost_equal(a, np.array([1, 0, 0, 0]))
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        q = pr.random_quaternion(random_state)
+        q = pr.random_quaternion(rng)
         R = pr.matrix_from_quaternion(q)
         pr.assert_rotation_matrix(R)
 
@@ -1242,9 +1250,9 @@ def test_quaternion_from_matrix_180():
 
 def test_quaternion_from_matrix_180_not_axis_aligned():
     """Test for bug in rotation by 180 degrees around arbitrary axes."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for i in range(10):
-        a = pr.random_axis_angle(random_state)
+        a = pr.random_axis_angle(rng)
         a[3] = np.pi
         q = pr.quaternion_from_axis_angle(a)
         R = pr.matrix_from_axis_angle(a)
@@ -1254,9 +1262,9 @@ def test_quaternion_from_matrix_180_not_axis_aligned():
 
 def test_quaternion_from_extrinsic_euler_xyz():
     """Test quaternion_from_extrinsic_euler_xyz."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for i in range(10):
-        e = random_state.uniform(-100, 100, [3])
+        e = rng.uniform(-100, 100, [3])
         q = pr.quaternion_from_extrinsic_euler_xyz(e)
         R_from_q = pr.matrix_from_quaternion(q)
         R_from_e = pr.active_matrix_from_extrinsic_euler_xyz(e)
@@ -1271,9 +1279,9 @@ def test_conversions_axis_angle_quaternion():
     q2 = pr.quaternion_from_axis_angle(a)
     assert_array_almost_equal(q2, q)
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        a = pr.random_axis_angle(random_state)
+        a = pr.random_axis_angle(rng)
         q = pr.quaternion_from_axis_angle(a)
 
         a2 = pr.axis_angle_from_quaternion(q)
@@ -1310,14 +1318,16 @@ def test_axis_angle_from_two_direction_vectors():
     a = pr.axis_angle_from_two_directions(d3, d4)
     pr.assert_axis_angle_equal(a, np.array([0, 0, 1, 0.5 * np.pi]))
 
-    random_state = np.random.RandomState(323)
+    rng = np.random.default_rng(323)
     for i in range(5):
-        R = pr.matrix_from_axis_angle(pr.random_axis_angle(random_state))
-        v1 = pr.random_vector(random_state, 3)
+        R = pr.matrix_from_axis_angle(pr.random_axis_angle(rng))
+        v1 = pr.random_vector(rng, 3)
         v2 = R.dot(v1)
         a = pr.axis_angle_from_two_directions(v1, v2)
-        assert pytest.approx(pr.angle_between_vectors(v1, a[:3])) == 0.5 * np.pi
-        assert pytest.approx(pr.angle_between_vectors(v2, a[:3])) == 0.5 * np.pi
+        assert pytest.approx(
+            pr.angle_between_vectors(v1, a[:3])) == 0.5 * np.pi
+        assert pytest.approx(
+            pr.angle_between_vectors(v2, a[:3])) == 0.5 * np.pi
         assert_array_almost_equal(v2, pr.matrix_from_axis_angle(a).dot(v1))
 
 
@@ -1327,9 +1337,9 @@ def test_axis_angle_from_compact_axis_angle():
     a = pr.axis_angle_from_compact_axis_angle(ca)
     assert_array_almost_equal(a, np.array([1.0, 0.0, 0.0, 0.0]))
 
-    random_state = np.random.RandomState(1)
+    rng = np.random.default_rng(1)
     for _ in range(5):
-        ca = pr.random_compact_axis_angle(random_state)
+        ca = pr.random_compact_axis_angle(rng)
         a = pr.axis_angle_from_compact_axis_angle(ca)
         assert pytest.approx(np.linalg.norm(ca)) == a[3]
         assert_array_almost_equal(ca[:3] / np.linalg.norm(ca), a[:3])
@@ -1341,9 +1351,9 @@ def test_compact_axis_angle():
     ca = pr.compact_axis_angle(a)
     assert_array_almost_equal(ca, np.zeros(3))
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        a = pr.random_axis_angle(random_state)
+        a = pr.random_axis_angle(rng)
         ca = pr.compact_axis_angle(a)
         assert_array_almost_equal(pr.norm_vector(ca), a[:3])
         assert pytest.approx(np.linalg.norm(ca)) == a[3]
@@ -1357,9 +1367,9 @@ def test_conversions_compact_axis_angle_quaternion():
     q2 = pr.quaternion_from_compact_axis_angle(a)
     assert_array_almost_equal(q2, q)
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        a = pr.random_compact_axis_angle(random_state)
+        a = pr.random_compact_axis_angle(rng)
         q = pr.quaternion_from_compact_axis_angle(a)
 
         a2 = pr.compact_axis_angle_from_quaternion(q)
@@ -1372,9 +1382,9 @@ def test_conversions_compact_axis_angle_quaternion():
 def test_interpolate_axis_angle():
     """Test interpolation between two axis-angle rotations with slerp."""
     n_steps = 10
-    random_state = np.random.RandomState(1)
-    a1 = pr.random_axis_angle(random_state)
-    a2 = pr.random_axis_angle(random_state)
+    rng = np.random.default_rng(1)
+    a1 = pr.random_axis_angle(rng)
+    a2 = pr.random_axis_angle(rng)
 
     traj = [pr.axis_angle_slerp(a1, a2, t) for t in np.linspace(0, 1, n_steps)]
 
@@ -1396,8 +1406,8 @@ def test_interpolate_same_axis_angle():
     See issue #45.
     """
     n_steps = 3
-    random_state = np.random.RandomState(42)
-    a = pr.random_axis_angle(random_state)
+    rng = np.random.default_rng(42)
+    a = pr.random_axis_angle(rng)
     traj = [pr.axis_angle_slerp(a, a, t) for t in np.linspace(0, 1, n_steps)]
     assert len(traj) == n_steps
     assert_array_almost_equal(traj[0], a)
@@ -1408,8 +1418,8 @@ def test_interpolate_same_axis_angle():
 def test_interpolate_almost_same_axis_angle():
     """Test interpolation between almost the same axis-angle rotation."""
     n_steps = 3
-    random_state = np.random.RandomState(42)
-    a1 = pr.random_axis_angle(random_state)
+    rng = np.random.default_rng(42)
+    a1 = pr.random_axis_angle(rng)
     a2 = np.copy(a1)
     a2[-1] += np.finfo("float").eps
     traj = [pr.axis_angle_slerp(a1, a2, t) for t in np.linspace(0, 1, n_steps)]
@@ -1422,9 +1432,9 @@ def test_interpolate_almost_same_axis_angle():
 def test_interpolate_quaternion():
     """Test interpolation between two quaternions with slerp."""
     n_steps = 10
-    random_state = np.random.RandomState(0)
-    a1 = pr.random_axis_angle(random_state)
-    a2 = pr.random_axis_angle(random_state)
+    rng = np.random.default_rng(0)
+    a1 = pr.random_axis_angle(rng)
+    a2 = pr.random_axis_angle(rng)
     q1 = pr.quaternion_from_axis_angle(a1)
     q2 = pr.quaternion_from_axis_angle(a2)
 
@@ -1440,8 +1450,8 @@ def test_interpolate_quaternion():
 def test_interpolate_quaternion_shortest_path():
     """Test SLERP between similar quternions with opposite sign."""
     n_steps = 10
-    random_state = np.random.RandomState(2323)
-    q1 = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(2323)
+    q1 = pr.random_quaternion(rng)
     a1 = pr.axis_angle_from_quaternion(q1)
     a2 = np.r_[a1[:3], a1[3] * 1.1]
     q2 = pr.quaternion_from_axis_angle(a2)
@@ -1479,8 +1489,8 @@ def test_interpolate_same_quaternion():
     See issue #45.
     """
     n_steps = 3
-    random_state = np.random.RandomState(42)
-    a = pr.random_axis_angle(random_state)
+    rng = np.random.default_rng(42)
+    a = pr.random_axis_angle(rng)
     q = pr.quaternion_from_axis_angle(a)
     traj = [pr.quaternion_slerp(q, q, t) for t in np.linspace(0, 1, n_steps)]
     assert len(traj) == n_steps
@@ -1491,8 +1501,8 @@ def test_interpolate_same_quaternion():
 
 def test_interpolate_shortest_path_same_quaternion():
     """Test interpolate along shortest path with same quaternion."""
-    random_state = np.random.RandomState(8353)
-    q = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(8353)
+    q = pr.random_quaternion(rng)
     q_interpolated = pr.quaternion_slerp(q, q, 0.5, shortest_path=True)
     assert_array_almost_equal(q, q_interpolated)
 
@@ -1517,8 +1527,8 @@ def test_quaternion_conventions():
     q_wxyz2 = pr.quaternion_wxyz_from_xyzw(q_xyzw)
     assert_array_equal(q_wxyz, q_wxyz2)
 
-    random_state = np.random.RandomState(42)
-    q_wxyz_random = pr.random_quaternion(random_state)
+    rng = np.random.default_rng(42)
+    q_wxyz_random = pr.random_quaternion(rng)
     q_xyzw_random = pr.quaternion_xyzw_from_wxyz(q_wxyz_random)
     assert_array_equal(q_xyzw_random[:3], q_wxyz_random[1:])
     assert q_xyzw_random[3] == q_wxyz_random[0]
@@ -1535,10 +1545,10 @@ def test_concatenate_quaternions():
     q12 = pr.concatenate_quaternions(q1, q2)
     assert_array_almost_equal(q12, np.array([0, 0, 0, 1]))
 
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        q1 = pr.quaternion_from_axis_angle(pr.random_axis_angle(random_state))
-        q2 = pr.quaternion_from_axis_angle(pr.random_axis_angle(random_state))
+        q1 = pr.quaternion_from_axis_angle(pr.random_axis_angle(rng))
+        q2 = pr.quaternion_from_axis_angle(pr.random_axis_angle(rng))
 
         R1 = pr.matrix_from_quaternion(q1)
         R2 = pr.matrix_from_quaternion(q2)
@@ -1560,11 +1570,11 @@ def test_quaternion_hamilton():
 
 def test_quaternion_rotation():
     """Test quaternion rotation."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        q = pr.quaternion_from_axis_angle(pr.random_axis_angle(random_state))
+        q = pr.quaternion_from_axis_angle(pr.random_axis_angle(rng))
         R = pr.matrix_from_quaternion(q)
-        v = pr.random_vector(random_state)
+        v = pr.random_vector(rng)
         vR = np.dot(R, v)
         vq = pr.q_prod_vector(q, v)
         assert_array_almost_equal(vR, vq)
@@ -1572,10 +1582,10 @@ def test_quaternion_rotation():
 
 def test_quaternion_conjugate():
     """Test quaternion conjugate."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        q = pr.random_quaternion(random_state)
-        v = pr.random_vector(random_state)
+        q = pr.random_quaternion(rng)
+        v = pr.random_vector(rng)
         vq = pr.q_prod_vector(q, v)
         vq2 = pr.concatenate_quaternions(pr.concatenate_quaternions(
             q, np.hstack(([0], v))), pr.q_conj(q))[1:]
@@ -1594,10 +1604,10 @@ def test_quaternion_gradient_integration():
     """Test integration of quaternion gradients."""
     n_steps = 21
     dt = 0.1
-    random_state = np.random.RandomState(3)
+    rng = np.random.default_rng(3)
     for _ in range(5):
-        q1 = pr.random_quaternion(random_state)
-        q2 = pr.random_quaternion(random_state)
+        q1 = pr.random_quaternion(rng)
+        q2 = pr.random_quaternion(rng)
         Q = np.vstack([pr.quaternion_slerp(q1, q2, t)
                        for t in np.linspace(0, 1, n_steps)])
         angular_velocities = pr.quaternion_gradient(Q, dt)
@@ -1607,10 +1617,10 @@ def test_quaternion_gradient_integration():
 
 def test_quaternion_rotation_consistent_with_multiplication():
     """Test if quaternion rotation and multiplication are Hamiltonian."""
-    random_state = np.random.RandomState(1)
+    rng = np.random.default_rng(1)
     for _ in range(5):
-        v = pr.random_vector(random_state)
-        q = pr.random_quaternion(random_state)
+        v = pr.random_vector(rng)
+        q = pr.random_quaternion(rng)
         v_im = np.hstack(((0.0,), v))
         qv_mult = pr.concatenate_quaternions(
             q, pr.concatenate_quaternions(v_im, pr.q_conj(q)))[1:]
@@ -1620,11 +1630,11 @@ def test_quaternion_rotation_consistent_with_multiplication():
 
 def test_quaternion_dist():
     """Test angular metric of quaternions."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
 
     for _ in range(5):
-        q1 = pr.quaternion_from_axis_angle(pr.random_axis_angle(random_state))
-        q2 = pr.quaternion_from_axis_angle(pr.random_axis_angle(random_state))
+        q1 = pr.quaternion_from_axis_angle(pr.random_axis_angle(rng))
+        q2 = pr.quaternion_from_axis_angle(pr.random_axis_angle(rng))
         q1_to_q1 = pr.quaternion_dist(q1, q1)
         assert pytest.approx(q1_to_q1) == 0.0
         q2_to_q2 = pr.quaternion_dist(q2, q2)
@@ -1637,10 +1647,10 @@ def test_quaternion_dist():
 
 def test_quaternion_dist_for_identical_rotations():
     """Test angular metric of quaternions q and -q."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
 
     for _ in range(5):
-        q = pr.quaternion_from_axis_angle(pr.random_axis_angle(random_state))
+        q = pr.quaternion_from_axis_angle(pr.random_axis_angle(rng))
         assert_array_almost_equal(pr.matrix_from_quaternion(q),
                                   pr.matrix_from_quaternion(-q))
         assert pr.quaternion_dist(q, -q) == 0.0
@@ -1648,23 +1658,23 @@ def test_quaternion_dist_for_identical_rotations():
 
 def test_quaternion_dist_for_almost_identical_rotations():
     """Test angular metric of quaternions q and ca. -q."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
 
     for _ in range(5):
-        a = pr.random_axis_angle(random_state)
+        a = pr.random_axis_angle(rng)
         q1 = pr.quaternion_from_axis_angle(a)
-        r = 1e-4 * random_state.randn(4)
+        r = 1e-4 * rng.standard_normal(4)
         q2 = -pr.quaternion_from_axis_angle(a + r)
         assert pytest.approx(pr.quaternion_dist(q1, q2), abs=1e-3) == 0.0
 
 
 def test_quaternion_diff():
     """Test difference of quaternions."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
 
     for _ in range(5):
-        q1 = pr.random_quaternion(random_state)
-        q2 = pr.random_quaternion(random_state)
+        q1 = pr.random_quaternion(rng)
+        q2 = pr.random_quaternion(rng)
         a_diff = pr.quaternion_diff(q1, q2)          # q1 - q2
         q_diff = pr.quaternion_from_axis_angle(a_diff)
         q3 = pr.concatenate_quaternions(q_diff, q2)  # q1 - q2 + q2
@@ -1692,9 +1702,9 @@ def test_check_matrix_threshold():
 
 def test_assert_rotation_matrix_behaves_like_check_matrix():
     """Test of both checks for rotation matrix validity behave similar."""
-    random_state = np.random.RandomState(2345)
+    rng = np.random.default_rng(2345)
     for _ in range(5):
-        a = pr.random_axis_angle(random_state)
+        a = pr.random_axis_angle(rng)
         R = pr.matrix_from_axis_angle(a)
         original_value = R[2, 2]
         for error in [0, 1e-8, 1e-7, 1e-5, 1e-4, 1]:
@@ -1744,10 +1754,10 @@ def test_matrix_from_two_vectors():
     R = pr.matrix_from_two_vectors(pr.unitx, pr.unity)
     assert_array_almost_equal(R, np.eye(3))
 
-    random_state = np.random.RandomState(28)
+    rng = np.random.default_rng(28)
     for _ in range(5):
-        a = pr.random_vector(random_state, 3)
-        b = pr.random_vector(random_state, 3)
+        a = pr.random_vector(rng, 3)
+        b = pr.random_vector(rng, 3)
         R = pr.matrix_from_two_vectors(a, b)
         pr.assert_rotation_matrix(R)
         assert_array_almost_equal(pr.norm_vector(a), R[:, 0])
@@ -1791,18 +1801,18 @@ def test_check_rotor():
 
 
 def test_outer():
-    random_state = np.random.RandomState(82)
+    rng = np.random.default_rng(82)
     for _ in range(5):
-        a = random_state.randn(3)
+        a = rng.standard_normal(3)
         Zero = pr.wedge(a, a)
         assert_array_almost_equal(Zero, np.zeros(3))
 
-        b = random_state.randn(3)
+        b = rng.standard_normal(3)
         A = pr.wedge(a, b)
         B = pr.wedge(b, a)
         assert_array_almost_equal(A, -B)
 
-        c = random_state.randn(3)
+        c = rng.standard_normal(3)
         assert_array_almost_equal(
             pr.wedge(a, (b + c)),
             A + pr.wedge(a, c)
@@ -1810,19 +1820,19 @@ def test_outer():
 
 
 def test_plane_normal_from_bivector():
-    random_state = np.random.RandomState(82)
+    rng = np.random.default_rng(82)
     for _ in range(5):
-        a = random_state.randn(3)
-        b = random_state.randn(3)
+        a = rng.standard_normal(3)
+        b = rng.standard_normal(3)
         B = pr.wedge(a, b)
         n = pr.plane_normal_from_bivector(B)
         assert_array_almost_equal(n, pr.norm_vector(np.cross(a, b)))
 
 
 def test_geometric_product():
-    random_state = np.random.RandomState(83)
+    rng = np.random.default_rng(83)
     for _ in range(5):
-        a = random_state.randn(3)
+        a = rng.standard_normal(3)
         a2a = pr.geometric_product(a, 2 * a)
         assert_array_almost_equal(a2a, np.array([np.dot(a, 2 * a), 0, 0, 0]))
 
@@ -1832,10 +1842,10 @@ def test_geometric_product():
 
 
 def test_geometric_product_creates_rotor_that_rotates_by_double_angle():
-    random_state = np.random.RandomState(83)
+    rng = np.random.default_rng(83)
     for _ in range(5):
-        a_unit = pr.norm_vector(random_state.randn(3))
-        b_unit = pr.norm_vector(random_state.randn(3))
+        a_unit = pr.norm_vector(rng.standard_normal(3))
+        b_unit = pr.norm_vector(rng.standard_normal(3))
         # a geometric product of two unit vectors is a rotor
         ab = pr.geometric_product(a_unit, b_unit)
         assert pytest.approx(np.linalg.norm(ab)) == 1.0
@@ -1868,10 +1878,10 @@ def test_rotor_from_two_directions_special_cases():
 
 
 def test_rotor_from_two_directions():
-    random_state = np.random.RandomState(84)
+    rng = np.random.default_rng(84)
     for _ in range(5):
-        a = random_state.randn(3)
-        b = random_state.randn(3)
+        a = rng.standard_normal(3)
+        b = rng.standard_normal(3)
         rotor = pr.rotor_from_two_directions(a, b)
         b2 = pr.rotor_apply(rotor, a)
         assert_array_almost_equal(pr.norm_vector(b), pr.norm_vector(b2))
@@ -1879,11 +1889,11 @@ def test_rotor_from_two_directions():
 
 
 def test_rotor_concatenation():
-    random_state = np.random.RandomState(85)
+    rng = np.random.default_rng(85)
     for _ in range(5):
-        a = random_state.randn(3)
-        b = random_state.randn(3)
-        c = random_state.randn(3)
+        a = rng.standard_normal(3)
+        b = rng.standard_normal(3)
+        c = rng.standard_normal(3)
         rotor_ab = pr.rotor_from_two_directions(a, b)
         rotor_bc = pr.rotor_from_two_directions(b, c)
         rotor_ac = pr.rotor_from_two_directions(a, c)
@@ -1893,10 +1903,10 @@ def test_rotor_concatenation():
 
 
 def test_rotor_times_reverse():
-    random_state = np.random.RandomState(85)
+    rng = np.random.default_rng(85)
     for _ in range(5):
-        a = random_state.randn(3)
-        b = random_state.randn(3)
+        a = rng.standard_normal(3)
+        b = rng.standard_normal(3)
         rotor = pr.rotor_from_two_directions(a, b)
         rotor_reverse = pr.rotor_reverse(rotor)
         result = pr.concatenate_rotors(rotor, rotor_reverse)
@@ -1904,18 +1914,18 @@ def test_rotor_times_reverse():
 
 
 def test_rotor_slerp():
-    random_state = np.random.RandomState(86)
+    rng = np.random.default_rng(86)
     for _ in range(5):
-        a_unit = pr.norm_vector(random_state.randn(3))
-        b_unit = pr.norm_vector(random_state.randn(3))
+        a_unit = pr.norm_vector(rng.standard_normal(3))
+        b_unit = pr.norm_vector(rng.standard_normal(3))
         rotor1 = pr.rotor_from_two_directions(a_unit, b_unit)
 
         axis = pr.norm_vector(np.cross(a_unit, b_unit))
         angle = pr.angle_between_vectors(a_unit, b_unit)
         q1 = pr.quaternion_from_axis_angle(np.r_[axis, angle])
 
-        c_unit = pr.norm_vector(random_state.randn(3))
-        d_unit = pr.norm_vector(random_state.randn(3))
+        c_unit = pr.norm_vector(rng.standard_normal(3))
+        d_unit = pr.norm_vector(rng.standard_normal(3))
         rotor2 = pr.rotor_from_two_directions(c_unit, d_unit)
 
         axis = pr.norm_vector(np.cross(c_unit, d_unit))
@@ -1925,7 +1935,7 @@ def test_rotor_slerp():
         rotor_025 = pr.rotor_slerp(rotor1, rotor2, 0.25)
         q_025 = pr.quaternion_slerp(q1, q2, 0.25)
 
-        e = random_state.randn(3)
+        e = rng.standard_normal(3)
         assert_array_almost_equal(
             pr.rotor_apply(rotor_025, e),
             pr.q_prod_vector(q_025, e))
@@ -1933,23 +1943,23 @@ def test_rotor_slerp():
         rotor_075 = pr.rotor_slerp(rotor1, rotor2, 0.25)
         q_075 = pr.quaternion_slerp(q1, q2, 0.25)
 
-        e = random_state.randn(3)
+        e = rng.standard_normal(3)
         assert_array_almost_equal(
             pr.rotor_apply(rotor_075, e),
             pr.q_prod_vector(q_075, e))
 
 
 def test_rotor_from_plane_angle():
-    random_state = np.random.RandomState(87)
+    rng = np.random.default_rng(87)
     for _ in range(5):
-        a = random_state.randn(3)
-        b = random_state.randn(3)
+        a = rng.standard_normal(3)
+        b = rng.standard_normal(3)
         B = pr.wedge(a, b)
-        angle = 2 * np.pi * random_state.rand()
+        angle = 2 * np.pi * rng.random()
         axis = np.cross(a, b)
         rotor = pr.rotor_from_plane_angle(B, angle)
         q = pr.quaternion_from_axis_angle(np.r_[axis, angle])
-        v = random_state.randn(3)
+        v = rng.standard_normal(3)
         assert_array_almost_equal(
             pr.rotor_apply(rotor, v),
             pr.q_prod_vector(q, v)
@@ -1957,12 +1967,12 @@ def test_rotor_from_plane_angle():
 
 
 def test_matrix_from_rotor():
-    random_state = np.random.RandomState(88)
+    rng = np.random.default_rng(88)
     for _ in range(5):
-        a = random_state.randn(3)
-        b = random_state.randn(3)
+        a = rng.standard_normal(3)
+        b = rng.standard_normal(3)
         B = pr.wedge(a, b)
-        angle = 2 * np.pi * random_state.rand()
+        angle = 2 * np.pi * rng.random()
         axis = np.cross(a, b)
         rotor = pr.rotor_from_plane_angle(B, angle)
         R_rotor = pr.matrix_from_rotor(rotor)
@@ -1972,10 +1982,10 @@ def test_matrix_from_rotor():
 
 
 def test_negative_rotor():
-    random_state = np.random.RandomState(89)
+    rng = np.random.default_rng(89)
     for _ in range(5):
-        a = random_state.randn(3)
-        b = random_state.randn(3)
+        a = rng.standard_normal(3)
+        b = rng.standard_normal(3)
         rotor = pr.rotor_from_two_directions(a, b)
         neg_rotor = pr.norm_vector(-rotor)
         a2 = pr.rotor_apply(rotor, a)
@@ -1996,18 +2006,18 @@ def test_plane_basis_from_normal():
     R = np.column_stack((x, y, pr.unitz))
     pr.assert_rotation_matrix(R)
 
-    random_state = np.random.RandomState(25)
+    rng = np.random.default_rng(25)
     for _ in range(5):
-        normal = pr.norm_vector(random_state.randn(3))
+        normal = pr.norm_vector(rng.standard_normal(3))
         x, y = pr.plane_basis_from_normal(normal)
         R = np.column_stack((x, y, normal))
         pr.assert_rotation_matrix(R)
 
 
 def test_pick_closest_quaternion():
-    random_state = np.random.RandomState(483)
+    rng = np.random.default_rng(483)
     for _ in range(10):
-        q = pr.random_quaternion(random_state)
+        q = pr.random_quaternion(rng)
         assert_array_almost_equal(pr.pick_closest_quaternion(q, q), q)
         assert_array_almost_equal(pr.pick_closest_quaternion(-q, q), q)
 
@@ -2044,10 +2054,10 @@ def test_quaternion_from_angle():
     with pytest.raises(ValueError, match="Basis must be in"):
         pr.quaternion_from_angle(3, 0)
 
-    random_state = np.random.RandomState(22)
+    rng = np.random.default_rng(22)
     for i in range(20):
-        basis = random_state.randint(0, 3)
-        angle = 2.0 * np.pi * random_state.rand() - np.pi
+        basis = rng.integers(0, 3)
+        angle = 2.0 * np.pi * rng.random() - np.pi
         R = pr.active_matrix_from_angle(basis, angle)
         q = pr.quaternion_from_angle(basis, angle)
         Rq = pr.matrix_from_quaternion(q)
@@ -2095,11 +2105,11 @@ def test_quaternion_from_euler():
         [2, 1, 0],
         [2, 0, 1]
     ]
-    random_state = np.random.RandomState(83)
+    rng = np.random.default_rng(83)
     for ea in euler_axes:
         for extrinsic in [False, True]:
             for i in range(5):
-                e = random_state.rand(3)
+                e = rng.random(3)
                 e[0] = 2.0 * np.pi * e[0] - np.pi
                 e[1] = np.pi * e[1]
                 e[2] = 2.0 * np.pi * e[2] - np.pi
@@ -2108,16 +2118,19 @@ def test_quaternion_from_euler():
                 if proper_euler:
                     e[1] -= np.pi / 2.0
 
-                q = pr.quaternion_from_euler(e, ea[0], ea[1], ea[2], extrinsic)
-                e2 = pr.euler_from_quaternion(q, ea[0], ea[1], ea[2], extrinsic)
-                q2 = pr.quaternion_from_euler(e2, ea[0], ea[1], ea[2], extrinsic)
+                q = pr.quaternion_from_euler(
+                    e, ea[0], ea[1], ea[2], extrinsic)
+                e2 = pr.euler_from_quaternion(
+                    q, ea[0], ea[1], ea[2], extrinsic)
+                q2 = pr.quaternion_from_euler(
+                    e2, ea[0], ea[1], ea[2], extrinsic)
 
                 pr.assert_quaternion_equal(q, q2)
 
 
 def test_general_matrix_euler_conversions():
     """General conversion algorithms between matrix and Euler angles."""
-    random_state = np.random.RandomState(22)
+    rng = np.random.default_rng(22)
 
     euler_axes = [
         [0, 2, 0],
@@ -2136,7 +2149,7 @@ def test_general_matrix_euler_conversions():
     for ea in euler_axes:
         for extrinsic in [False, True]:
             for i in range(5):
-                e = random_state.rand(3)
+                e = rng.random(3)
                 e[0] = 2.0 * np.pi * e[0] - np.pi
                 e[1] = np.pi * e[1]
                 e[2] = 2.0 * np.pi * e[2] - np.pi
@@ -2145,15 +2158,70 @@ def test_general_matrix_euler_conversions():
                 if proper_euler:
                     e[1] -= np.pi / 2.0
 
-                q = pr.quaternion_from_euler(e, ea[0], ea[1], ea[2], extrinsic)
+                q = pr.quaternion_from_euler(
+                    e, ea[0], ea[1], ea[2], extrinsic)
                 R = pr.matrix_from_euler(e, ea[0], ea[1], ea[2], extrinsic)
                 q_R = pr.quaternion_from_matrix(R)
                 pr.assert_quaternion_equal(
                     q, q_R, err_msg=f"axes: {ea}, extrinsic: {extrinsic}")
 
                 e_R = pr.euler_from_matrix(R, ea[0], ea[1], ea[2], extrinsic)
-                e_q = pr.euler_from_quaternion(q, ea[0], ea[1], ea[2], extrinsic)
+                e_q = pr.euler_from_quaternion(
+                    q, ea[0], ea[1], ea[2], extrinsic)
 
-                R_R = pr.matrix_from_euler(e_R, ea[0], ea[1], ea[2], extrinsic)
-                R_q = pr.matrix_from_euler(e_q, ea[0], ea[1], ea[2], extrinsic)
+                R_R = pr.matrix_from_euler(
+                    e_R, ea[0], ea[1], ea[2], extrinsic)
+                R_q = pr.matrix_from_euler(
+                    e_q, ea[0], ea[1], ea[2], extrinsic)
                 assert_array_almost_equal(R_R, R_q)
+
+
+def test_check_mrp():
+    with pytest.raises(
+            ValueError,
+            match="Expected modified Rodrigues parameters with shape"):
+        pr.check_mrp([])
+    with pytest.raises(
+            ValueError,
+            match="Expected modified Rodrigues parameters with shape"):
+        pr.check_mrp(np.zeros((3, 4)))
+
+
+def test_mrp_quat_conversions():
+    rng = np.random.default_rng(22)
+
+    for _ in range(5):
+        q = pr.random_quaternion(rng)
+        mrp = pr.mrp_from_quaternion(q)
+        q2 = pr.quaternion_from_mrp(mrp)
+        pr.assert_quaternion_equal(q, q2)
+
+
+def test_jacobian_so3():
+    omega = np.zeros(3)
+
+    J = pr.left_jacobian_SO3(omega)
+    J_series = pr.left_jacobian_SO3_series(omega, 20)
+    assert_array_almost_equal(J, J_series)
+
+    J_inv = pr.left_jacobian_SO3_inv(omega)
+    J_inv_series = pr.left_jacobian_SO3_inv_series(omega, 20)
+    assert_array_almost_equal(J_inv, J_inv_series)
+
+    J_inv_J = np.dot(J_inv, J)
+    assert_array_almost_equal(J_inv_J, np.eye(3))
+
+    rng = np.random.default_rng(0)
+    for _ in range(5):
+        omega = pr.random_compact_axis_angle(rng)
+
+        J = pr.left_jacobian_SO3(omega)
+        J_series = pr.left_jacobian_SO3_series(omega, 20)
+        assert_array_almost_equal(J, J_series)
+
+        J_inv = pr.left_jacobian_SO3_inv(omega)
+        J_inv_series = pr.left_jacobian_SO3_inv_series(omega, 20)
+        assert_array_almost_equal(J_inv, J_inv_series)
+
+        J_inv_J = np.dot(J_inv, J)
+        assert_array_almost_equal(J_inv_J, np.eye(3))
diff --git a/pytransform3d/test/test_trajectories.py b/pytransform3d/test/test_trajectories.py
index 93a3929ca..cb6e9ad95 100644
--- a/pytransform3d/test/test_trajectories.py
+++ b/pytransform3d/test/test_trajectories.py
@@ -21,71 +21,71 @@
 
 
 def test_invert_transforms_0dims():
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
     B2A = invert_transform(A2B)
     assert_array_almost_equal(B2A, invert_transforms(A2B))
 
 
 def test_invert_transforms_1dims():
-    random_state = np.random.RandomState(1)
+    rng = np.random.default_rng(1)
     A2Bs = np.empty((3, 4, 4))
     B2As = np.empty((3, 4, 4))
     for i in range(len(A2Bs)):
-        A2Bs[i] = random_transform(random_state)
+        A2Bs[i] = random_transform(rng)
         B2As[i] = invert_transform(A2Bs[i])
     assert_array_almost_equal(B2As, invert_transforms(A2Bs))
 
 
 def test_invert_transforms_2dims():
-    random_state = np.random.RandomState(1)
+    rng = np.random.default_rng(1)
     A2Bs = np.empty((9, 4, 4))
     B2As = np.empty((9, 4, 4))
     for i in range(len(A2Bs)):
-        A2Bs[i] = random_transform(random_state)
+        A2Bs[i] = random_transform(rng)
         B2As[i] = invert_transform(A2Bs[i])
     assert_array_almost_equal(
         B2As.reshape(3, 3, 4, 4), invert_transforms(A2Bs.reshape(3, 3, 4, 4)))
 
 
 def test_concat_one_to_many():
-    random_state = np.random.RandomState(482)
-    A2B = random_transform(random_state)
-    B2C = random_transform(random_state)
+    rng = np.random.default_rng(482)
+    A2B = random_transform(rng)
+    B2C = random_transform(rng)
     A2C = concat(A2B, B2C)
     assert_array_almost_equal(A2C, concat_one_to_many(A2B, [B2C])[0])
 
-    B2Cs = [random_transform(random_state) for _ in range(5)]
+    B2Cs = [random_transform(rng) for _ in range(5)]
     A2Cs = [concat(A2B, B2C) for B2C in B2Cs]
     assert_array_almost_equal(A2Cs, concat_one_to_many(A2B, B2Cs))
 
 
 def test_concat_many_to_one():
-    random_state = np.random.RandomState(482)
-    A2B = random_transform(random_state)
-    B2C = random_transform(random_state)
+    rng = np.random.default_rng(482)
+    A2B = random_transform(rng)
+    B2C = random_transform(rng)
     A2C = concat(A2B, B2C)
     assert_array_almost_equal(A2C, concat_many_to_one([A2B], B2C)[0])
 
-    A2Bs = [random_transform(random_state) for _ in range(5)]
+    A2Bs = [random_transform(rng) for _ in range(5)]
     A2Cs = [concat(A2B, B2C) for A2B in A2Bs]
     assert_array_almost_equal(A2Cs, concat_many_to_one(A2Bs, B2C))
 
 
 def test_transforms_from_pqs_0dims():
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     pq = np.empty(7)
-    pq[:3] = random_state.randn(3)
-    pq[3:] = random_quaternion(random_state)
+    pq[:3] = rng.standard_normal(size=3)
+    pq[3:] = random_quaternion(rng)
     A2B = transforms_from_pqs(pq, False)
     assert_array_almost_equal(A2B, transform_from_pq(pq))
 
 
 def test_transforms_from_pqs_1dim():
     P = np.empty((10, 7))
-    random_state = np.random.RandomState(0)
-    P[:, :3] = random_state.randn(len(P), 3)
-    P[:, 3:] = norm_vectors(random_state.randn(len(P), 4))
+    rng = np.random.default_rng(0)
+    P[:, :3] = rng.standard_normal(size=(len(P), 3))
+    P[:, 3:] = norm_vectors(rng.standard_normal(size=(len(P), 4)))
 
     H = transforms_from_pqs(P)
     P2 = pqs_from_transforms(H)
@@ -99,8 +99,8 @@ def test_transforms_from_pqs_1dim():
 
 
 def test_transforms_from_pqs_4dims():
-    random_state = np.random.RandomState(0)
-    P = random_state.randn(2, 3, 4, 5, 7)
+    rng = np.random.default_rng(0)
+    P = rng.standard_normal(size=(2, 3, 4, 5, 7))
     P[..., 3:] = norm_vectors(P[..., 3:])
 
     H = transforms_from_pqs(P)
@@ -141,9 +141,9 @@ def test_transforms_from_exponential_coordinates():
     A2B2 = transforms_from_exponential_coordinates([[Stheta], [Stheta]])[0, 0]
     assert_array_almost_equal(A2B, A2B2)
 
-    random_state = np.random.RandomState(53)
+    rng = np.random.default_rng(53)
     for _ in range(5):
-        A2B = random_transform(random_state)
+        A2B = random_transform(rng)
         Stheta = exponential_coordinates_from_transform(A2B)
         A2B2 = transforms_from_exponential_coordinates(Stheta[np.newaxis])[0]
         assert_array_almost_equal(A2B, A2B2)
@@ -155,8 +155,8 @@ def test_transforms_from_exponential_coordinates():
 
 
 def test_exponential_coordinates_from_transforms_0dims():
-    random_state = np.random.RandomState(842)
-    Sthetas = random_state.randn(6)
+    rng = np.random.default_rng(842)
+    Sthetas = rng.standard_normal(size=6)
     H = transforms_from_exponential_coordinates(Sthetas)
     Sthetas2 = exponential_coordinates_from_transforms(H)
     H2 = transforms_from_exponential_coordinates(Sthetas2)
@@ -164,8 +164,8 @@ def test_exponential_coordinates_from_transforms_0dims():
 
 
 def test_exponential_coordinates_from_transforms_2dims():
-    random_state = np.random.RandomState(843)
-    Sthetas = random_state.randn(4, 4, 6)
+    rng = np.random.default_rng(843)
+    Sthetas = rng.standard_normal(size=(4, 4, 6))
     H = transforms_from_exponential_coordinates(Sthetas)
     Sthetas2 = exponential_coordinates_from_transforms(H)
     H2 = transforms_from_exponential_coordinates(Sthetas2)
@@ -173,8 +173,8 @@ def test_exponential_coordinates_from_transforms_2dims():
 
 
 def test_dual_quaternions_from_pqs_0dims():
-    random_state = np.random.RandomState(844)
-    pq = random_state.randn(7)
+    rng = np.random.default_rng(844)
+    pq = rng.standard_normal(size=7)
     pq[3:] /= np.linalg.norm(pq[3:], axis=-1)[..., np.newaxis]
     dq = dual_quaternions_from_pqs(pq)
     pq2 = pqs_from_dual_quaternions(dq)
@@ -183,8 +183,8 @@ def test_dual_quaternions_from_pqs_0dims():
 
 
 def test_dual_quaternions_from_pqs_1dim():
-    random_state = np.random.RandomState(845)
-    pqs = random_state.randn(20, 7)
+    rng = np.random.default_rng(845)
+    pqs = rng.standard_normal(size=(20, 7))
     pqs[..., 3:] /= np.linalg.norm(pqs[..., 3:], axis=-1)[..., np.newaxis]
     dqs = dual_quaternions_from_pqs(pqs)
     pqs2 = pqs_from_dual_quaternions(dqs)
@@ -194,8 +194,8 @@ def test_dual_quaternions_from_pqs_1dim():
 
 
 def test_dual_quaternions_from_pqs_2dims():
-    random_state = np.random.RandomState(846)
-    pqs = random_state.randn(5, 5, 7)
+    rng = np.random.default_rng(846)
+    pqs = rng.standard_normal(size=(5, 5, 7))
     pqs[..., 3:] /= np.linalg.norm(pqs[..., 3:], axis=-1)[..., np.newaxis]
     dqs = dual_quaternions_from_pqs(pqs)
     pqs2 = pqs_from_dual_quaternions(dqs)
@@ -205,8 +205,8 @@ def test_dual_quaternions_from_pqs_2dims():
 
 
 def test_batch_concatenate_dual_quaternions_0dims():
-    random_state = np.random.RandomState(847)
-    pqs = random_state.randn(2, 7)
+    rng = np.random.default_rng(847)
+    pqs = rng.standard_normal(size=(2, 7))
     pqs[..., 3:] /= np.linalg.norm(pqs[..., 3:], axis=-1)[..., np.newaxis]
     dqs = dual_quaternions_from_pqs(pqs)
     dq1 = dqs[0]
@@ -218,8 +218,8 @@ def test_batch_concatenate_dual_quaternions_0dims():
 
 
 def test_batch_concatenate_dual_quaternions_2dims():
-    random_state = np.random.RandomState(848)
-    pqs = random_state.randn(2, 2, 2, 7)
+    rng = np.random.default_rng(848)
+    pqs = rng.standard_normal(size=(2, 2, 2, 7))
     pqs[..., 3:] /= np.linalg.norm(pqs[..., 3:], axis=-1)[..., np.newaxis]
     dqs = dual_quaternions_from_pqs(pqs)
     dqs1 = dqs[0]
@@ -235,22 +235,22 @@ def test_batch_concatenate_dual_quaternions_2dims():
 
 
 def test_batch_dual_quaternion_vector_product_0dims():
-    random_state = np.random.RandomState(849)
-    pq = random_state.randn(7)
+    rng = np.random.default_rng(849)
+    pq = rng.standard_normal(size=7)
     pq[3:] /= np.linalg.norm(pq[3:], axis=-1)[..., np.newaxis]
     dq = dual_quaternions_from_pqs(pq)
-    v = random_state.randn(3)
+    v = rng.standard_normal(size=3)
 
     assert_array_almost_equal(
         batch_dq_prod_vector(dq, v), dq_prod_vector(dq, v))
 
 
 def test_batch_dual_quaternion_vector_product_2dims():
-    random_state = np.random.RandomState(850)
-    pqs = random_state.randn(3, 4, 7)
+    rng = np.random.default_rng(850)
+    pqs = rng.standard_normal(size=(3, 4, 7))
     pqs[..., 3:] /= np.linalg.norm(pqs[..., 3:], axis=-1)[..., np.newaxis]
     dqs = dual_quaternions_from_pqs(pqs)
-    V = random_state.randn(3, 4, 3)
+    V = rng.standard_normal(size=(3, 4, 3))
 
     V_transformed = batch_dq_prod_vector(dqs, V)
     for v_t, dq, v in zip(V_transformed.reshape(-1, 3), dqs.reshape(-1, 8),
@@ -259,8 +259,8 @@ def test_batch_dual_quaternion_vector_product_2dims():
 
 
 def test_batch_conversions_dual_quaternions_transforms_0dims():
-    random_state = np.random.RandomState(851)
-    pq = random_state.randn(7)
+    rng = np.random.default_rng(851)
+    pq = rng.standard_normal(size=7)
     pq[3:] /= np.linalg.norm(pq[3:], axis=-1)[..., np.newaxis]
     dq = dual_quaternions_from_pqs(pq)
 
@@ -272,8 +272,8 @@ def test_batch_conversions_dual_quaternions_transforms_0dims():
 
 
 def test_batch_conversions_dual_quaternions_transforms_3dims():
-    random_state = np.random.RandomState(852)
-    pqs = random_state.randn(3, 4, 5, 7)
+    rng = np.random.default_rng(852)
+    pqs = rng.standard_normal(size=(3, 4, 5, 7))
     pqs[..., 3:] /= np.linalg.norm(pqs[..., 3:], axis=-1)[..., np.newaxis]
     dqs = dual_quaternions_from_pqs(pqs)
 
diff --git a/pytransform3d/test/test_transform_manager.py b/pytransform3d/test/test_transform_manager.py
index 9e8cf754b..ff7780eba 100644
--- a/pytransform3d/test/test_transform_manager.py
+++ b/pytransform3d/test/test_transform_manager.py
@@ -16,8 +16,8 @@
 
 def test_request_added_transform():
     """Request an added transform from the transform manager."""
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
 
     tm = TransformManager()
     tm.add_transform("A", "B", A2B)
@@ -27,8 +27,8 @@ def test_request_added_transform():
 
 def test_request_inverse_transform():
     """Request an inverse transform from the transform manager."""
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
 
     tm = TransformManager()
     tm.add_transform("A", "B", A2B)
@@ -51,9 +51,9 @@ def test_has_frame():
 
 def test_transform_not_added():
     """Test request for transforms that have not been added."""
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
-    C2D = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
+    C2D = random_transform(rng)
 
     tm = TransformManager()
     tm.add_transform("A", "B", A2B)
@@ -69,10 +69,10 @@ def test_transform_not_added():
 
 def test_request_concatenated_transform():
     """Request a concatenated transform from the transform manager."""
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
-    B2C = random_transform(random_state)
-    F2A = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
+    B2C = random_transform(rng)
+    F2A = random_transform(rng)
 
     tm = TransformManager()
     tm.add_transform("A", "B", A2B)
@@ -93,9 +93,9 @@ def test_request_concatenated_transform():
 
 def test_update_transform():
     """Update an existing transform."""
-    random_state = np.random.RandomState(0)
-    A2B1 = random_transform(random_state)
-    A2B2 = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B1 = random_transform(rng)
+    A2B2 = random_transform(rng)
 
     tm = TransformManager()
     tm.add_transform("A", "B", A2B1)
@@ -110,8 +110,8 @@ def test_update_transform():
 
 def test_pickle():
     """Test if a transform manager can be pickled."""
-    random_state = np.random.RandomState(1)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(1)
+    A2B = random_transform(rng)
     tm = TransformManager()
     tm.add_transform("A", "B", A2B)
 
@@ -131,8 +131,8 @@ def test_pickle():
 
 def test_whitelist():
     """Test correct handling of whitelists for plotting."""
-    random_state = np.random.RandomState(2)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(2)
+    A2B = random_transform(rng)
     tm = TransformManager()
     tm.add_transform("A", "B", A2B)
 
@@ -146,33 +146,33 @@ def test_whitelist():
 
 def test_check_consistency():
     """Test correct detection of inconsistent graphs."""
-    random_state = np.random.RandomState(2)
+    rng = np.random.default_rng(2)
 
     tm = TransformManager()
 
-    A2B = random_transform(random_state)
+    A2B = random_transform(rng)
     tm.add_transform("A", "B", A2B)
-    B2A = random_transform(random_state)
+    B2A = random_transform(rng)
     tm.add_transform("B", "A", B2A)
     assert not tm.check_consistency()
 
     tm = TransformManager()
 
-    A2B = random_transform(random_state)
+    A2B = random_transform(rng)
     tm.add_transform("A", "B", A2B)
     assert tm.check_consistency()
 
-    C2D = random_transform(random_state)
+    C2D = random_transform(rng)
     tm.add_transform("C", "D", C2D)
     assert tm.check_consistency()
 
-    B2C = random_transform(random_state)
+    B2C = random_transform(rng)
     tm.add_transform("B", "C", B2C)
     assert tm.check_consistency()
 
     A2D_over_path = tm.get_transform("A", "D")
 
-    A2D = random_transform(random_state)
+    A2D = random_transform(rng)
     tm.add_transform("A", "D", A2D)
     assert not tm.check_consistency()
 
@@ -195,11 +195,11 @@ def test_connected_components():
 
 def test_png_export():
     """Test if the graph can be exported to PNG."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
 
     ee2robot = transform_from_pq(
         np.hstack((np.array([0.4, -0.3, 0.5]),
-                   random_quaternion(random_state))))
+                   random_quaternion(rng))))
     cam2robot = transform_from_pq(
         np.hstack((np.array([0.0, 0.0, 0.8]), q_id)))
     object2cam = transform_from(
@@ -288,13 +288,13 @@ def test_remove_transform():
 
 
 def test_from_to_dict():
-    random_state = np.random.RandomState(2323)
+    rng = np.random.default_rng(2323)
     tm = TransformManager()
-    A2B = random_transform(random_state)
+    A2B = random_transform(rng)
     tm.add_transform("A", "B", A2B)
-    B2C = random_transform(random_state)
+    B2C = random_transform(rng)
     tm.add_transform("B", "C", B2C)
-    C2D = random_transform(random_state)
+    C2D = random_transform(rng)
     tm.add_transform("C", "D", C2D)
 
     tm_dict = tm.to_dict()
diff --git a/pytransform3d/test/test_transformations.py b/pytransform3d/test/test_transformations.py
index efbd0613d..75546cd13 100644
--- a/pytransform3d/test/test_transformations.py
+++ b/pytransform3d/test/test_transformations.py
@@ -2,6 +2,7 @@
 import numpy as np
 import pytest
 
+import pytransform3d.transformations as pt
 from pytransform3d.transformations import (
     random_transform, transform_from, translate_transform, rotate_transform,
     invert_transform, vector_to_point, vectors_to_points, vector_to_direction,
@@ -56,8 +57,8 @@ def test_check_transform():
     with pytest.raises(ValueError, match="homogeneous transformation matrix"):
         check_transform(A2B)
 
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
     A2B2 = check_transform(A2B)
     assert_array_almost_equal(A2B, A2B2)
 
@@ -97,10 +98,10 @@ def test_check_pq():
 
 def test_invert_transform():
     """Test inversion of transformations."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        R = matrix_from_axis_angle(random_axis_angle(random_state))
-        p = random_vector(random_state)
+        R = matrix_from_axis_angle(random_axis_angle(rng))
+        p = random_vector(rng)
         A2B = transform_from(R, p)
         B2A = invert_transform(A2B)
         A2B2 = np.linalg.inv(B2A)
@@ -109,9 +110,9 @@ def test_invert_transform():
 
 def test_invert_transform_without_check():
     """Test inversion of transformations."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        A2B = random_state.randn(4, 4)
+        A2B = rng.standard_normal(size=(4, 4))
         A2B = A2B + A2B.T
         B2A = invert_transform(A2B, check=False)
         A2B2 = np.linalg.inv(B2A)
@@ -124,9 +125,9 @@ def test_vector_to_point():
     pA = vector_to_point(v)
     assert_array_almost_equal(pA, [1, 2, 3, 1])
 
-    random_state = np.random.RandomState(0)
-    R = matrix_from_axis_angle(random_axis_angle(random_state))
-    p = random_vector(random_state)
+    rng = np.random.default_rng(0)
+    R = matrix_from_axis_angle(random_axis_angle(rng))
+    p = random_vector(rng)
     A2B = transform_from(R, p)
     assert_transform(A2B)
     _ = transform(A2B, pA)
@@ -138,8 +139,8 @@ def test_vectors_to_points():
     PA = vectors_to_points(V)
     assert_array_almost_equal(PA, [[1, 2, 3, 1], [2, 3, 4, 1]])
 
-    random_state = np.random.RandomState(0)
-    V = random_state.randn(10, 3)
+    rng = np.random.default_rng(0)
+    V = rng.standard_normal(size=(10, 3))
     for i, p in enumerate(vectors_to_points(V)):
         assert_array_almost_equal(p, vector_to_point(V[i]))
 
@@ -150,9 +151,9 @@ def test_vector_to_direction():
     dA = vector_to_direction(v)
     assert_array_almost_equal(dA, [1, 2, 3, 0])
 
-    random_state = np.random.RandomState(0)
-    R = matrix_from_axis_angle(random_axis_angle(random_state))
-    p = random_vector(random_state)
+    rng = np.random.default_rng(0)
+    R = matrix_from_axis_angle(random_axis_angle(rng))
+    p = random_vector(rng)
     A2B = transform_from(R, p)
     assert_transform(A2B)
     _ = transform(A2B, dA)
@@ -164,20 +165,20 @@ def test_vectors_to_directions():
     DA = vectors_to_directions(V)
     assert_array_almost_equal(DA, [[1, 2, 3, 0], [2, 3, 4, 0]])
 
-    random_state = np.random.RandomState(0)
-    V = random_state.randn(10, 3)
+    rng = np.random.default_rng(0)
+    V = rng.standard_normal(size=(10, 3))
     for i, d in enumerate(vectors_to_directions(V)):
         assert_array_almost_equal(d, vector_to_direction(V[i]))
 
 
 def test_concat():
     """Test concatenation of transforms."""
-    random_state = np.random.RandomState(0)
+    rng = np.random.default_rng(0)
     for _ in range(5):
-        A2B = random_transform(random_state)
+        A2B = random_transform(rng)
         assert_transform(A2B)
 
-        B2C = random_transform(random_state)
+        B2C = random_transform(rng)
         assert_transform(B2C)
 
         A2C = concat(A2B, B2C)
@@ -201,8 +202,8 @@ def test_transform():
     PA = np.array([[1, 2, 3, 1],
                    [2, 3, 4, 1]])
 
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
 
     PB = transform(A2B, PA)
     p0B = transform(A2B, PA[0])
@@ -217,8 +218,8 @@ def test_transform():
 
 def test_scale_transform():
     """Test scaling of transforms."""
-    random_state = np.random.RandomState(0)
-    A2B = random_transform(random_state)
+    rng = np.random.default_rng(0)
+    A2B = random_transform(rng)
 
     A2B_scaled1 = scale_transform(A2B, s_xt=0.5, s_yt=0.5, s_zt=0.5)
     A2B_scaled2 = scale_transform(A2B, s_t=0.5)
@@ -280,13 +281,13 @@ def test_norm_exponential_coordinates():
     assert_array_almost_equal(
         Stheta_only_translation, Stheta_only_translation2)
 
-    random_state = np.random.RandomState(381)
+    rng = np.random.default_rng(381)
 
     # 180 degree rotation ambiguity
     for _ in range(10):
-        q = random_state.randn(3)
-        s = norm_vector(random_state.randn(3))
-        h = random_state.randn()
+        q = rng.standard_normal(size=3)
+        s = norm_vector(rng.standard_normal(size=3))
+        h = rng.standard_normal()
         Stheta = screw_axis_from_screw_parameters(q, s, h) * np.pi
         Stheta2 = screw_axis_from_screw_parameters(q, -s, -h) * np.pi
         assert_array_almost_equal(
@@ -296,9 +297,9 @@ def test_norm_exponential_coordinates():
                                   norm_exponential_coordinates(Stheta2))
 
     for _ in range(10):
-        Stheta = random_state.randn(6)
+        Stheta = rng.standard_normal(size=6)
         # ensure that theta is not within [-pi, pi]
-        Stheta[random_state.randint(0, 3)] += np.pi + random_state.rand()
+        Stheta[rng.integers(0, 3)] += np.pi + rng.random()
         Stheta_norm = norm_exponential_coordinates(Stheta)
         assert not np.all(Stheta == Stheta_norm)
 
@@ -336,9 +337,9 @@ def test_check_screw_parameters():
 
 
 def test_check_screw_axis():
-    random_state = np.random.RandomState(73)
-    omega = norm_vector(random_vector(random_state, 3))
-    v = random_vector(random_state, 3)
+    rng = np.random.default_rng(73)
+    omega = norm_vector(random_vector(rng, 3))
+    v = random_vector(rng, 3)
 
     with pytest.raises(ValueError, match="Expected 3D vector with shape"):
         check_screw_axis(np.r_[0, 1, v])
@@ -435,17 +436,17 @@ def test_check_transform_log():
 
 
 def test_random_screw_axis():
-    random_state = np.random.RandomState(893)
+    rng = np.random.default_rng(893)
     for _ in range(5):
-        S = random_screw_axis(random_state)
+        S = random_screw_axis(rng)
         check_screw_axis(S)
 
 
 def test_conversions_between_screw_axis_and_parameters():
-    random_state = np.random.RandomState(98)
+    rng = np.random.default_rng(98)
 
-    q = random_vector(random_state, 3)
-    s_axis = norm_vector(random_vector(random_state, 3))
+    q = random_vector(rng, 3)
+    s_axis = norm_vector(random_vector(rng, 3))
     h = np.inf
     screw_axis = screw_axis_from_screw_parameters(q, s_axis, h)
     assert_array_almost_equal(screw_axis, np.r_[0, 0, 0, s_axis])
@@ -456,10 +457,10 @@ def test_conversions_between_screw_axis_and_parameters():
     assert_array_almost_equal(h, h2)
 
     for _ in range(10):
-        s_axis = norm_vector(random_vector(random_state, 3))
+        s_axis = norm_vector(random_vector(rng, 3))
         # q has to be orthogonal to s_axis so that we reconstruct it exactly
         q = perpendicular_to_vector(s_axis)
-        h = random_state.rand() + 0.5
+        h = rng.random() + 0.5
 
         screw_axis = screw_axis_from_screw_parameters(q, s_axis, h)
         q2, s_axis2, h2 = screw_parameters_from_screw_axis(screw_axis)
@@ -488,9 +489,9 @@ def test_conversions_between_exponential_coordinates_and_transform():
     A2B2 = transform_from_exponential_coordinates(Stheta)
     assert_array_almost_equal(A2B, A2B2)
 
-    random_state = np.random.RandomState(52)
+    rng = np.random.default_rng(52)
     for _ in range(5):
-        A2B = random_transform(random_state)
+        A2B = random_transform(rng)
         Stheta = exponential_coordinates_from_transform(A2B)
         A2B2 = transform_from_exponential_coordinates(Stheta)
         assert_array_almost_equal(A2B, A2B2)
@@ -510,10 +511,10 @@ def test_conversions_between_screw_axis_and_exponential_coordinates():
     assert_array_almost_equal(S, S2)
     assert pytest.approx(theta) == theta2
 
-    random_state = np.random.RandomState(33)
+    rng = np.random.default_rng(33)
     for _ in range(5):
-        S = random_screw_axis(random_state)
-        theta = random_state.rand()
+        S = random_screw_axis(rng)
+        theta = rng.random()
         Stheta = exponential_coordinates_from_screw_axis(S, theta)
         S2, theta2 = screw_axis_from_exponential_coordinates(Stheta)
         assert_array_almost_equal(S, S2)
@@ -521,18 +522,18 @@ def test_conversions_between_screw_axis_and_exponential_coordinates():
 
 
 def test_conversions_between_exponential_coordinates_and_transform_log():
-    random_state = np.random.RandomState(22)
+    rng = np.random.default_rng(22)
     for _ in range(5):
-        Stheta = random_state.randn(6)
+        Stheta = rng.standard_normal(size=6)
         transform_log = transform_log_from_exponential_coordinates(Stheta)
         Stheta2 = exponential_coordinates_from_transform_log(transform_log)
         assert_array_almost_equal(Stheta, Stheta2)
 
 
 def test_conversions_between_screw_matrix_and_screw_axis():
-    random_state = np.random.RandomState(83)
+    rng = np.random.default_rng(83)
     for _ in range(5):
-        S = random_screw_axis(random_state)
+        S = random_screw_axis(rng)
         S_mat = screw_matrix_from_screw_axis(S)
         S2 = screw_axis_from_screw_matrix(S_mat)
         assert_array_almost_equal(S, S2)
@@ -556,10 +557,10 @@ def test_conversions_between_screw_matrix_and_transform_log():
     assert_array_almost_equal(S_mat, S_mat2)
     assert pytest.approx(theta) == theta2
 
-    random_state = np.random.RandomState(65)
+    rng = np.random.default_rng(65)
     for _ in range(5):
-        S = random_screw_axis(random_state)
-        theta = float(np.random.rand())
+        S = random_screw_axis(rng)
+        theta = float(np.random.random())
         S_mat = screw_matrix_from_screw_axis(S)
         transform_log = transform_log_from_screw_matrix(S_mat, theta)
         S_mat2, theta2 = screw_matrix_from_transform_log(transform_log)
@@ -574,25 +575,25 @@ def test_conversions_between_transform_and_transform_log():
     A2B2 = transform_from_transform_log(transform_log)
     assert_array_almost_equal(A2B, A2B2)
 
-    random_state = np.random.RandomState(84)
-    A2B = transform_from(np.eye(3), p=random_vector(random_state, 3))
+    rng = np.random.default_rng(84)
+    A2B = transform_from(np.eye(3), p=random_vector(rng, 3))
     transform_log = transform_log_from_transform(A2B)
     A2B2 = transform_from_transform_log(transform_log)
     assert_array_almost_equal(A2B, A2B2)
 
     for _ in range(5):
-        A2B = random_transform(random_state)
+        A2B = random_transform(rng)
         transform_log = transform_log_from_transform(A2B)
         A2B2 = transform_from_transform_log(transform_log)
         assert_array_almost_equal(A2B, A2B2)
 
 
 def test_adjoint_of_transformation():
-    random_state = np.random.RandomState(94)
+    rng = np.random.default_rng(94)
     for _ in range(5):
-        A2B = random_transform(random_state)
-        theta_dot = 3.0 * float(random_state.rand())
-        S = random_screw_axis(random_state)
+        A2B = random_transform(rng)
+        theta_dot = 3.0 * float(rng.random())
+        S = random_screw_axis(rng)
 
         V_A = S * theta_dot
 
@@ -633,26 +634,26 @@ def test_normalize_dual_quaternion():
     assert_unit_dual_quaternion(dq_norm)
     assert_array_almost_equal([1, 0, 0, 0, 0, 0, 0, 0], dq_norm)
 
-    random_state = np.random.RandomState(999)
+    rng = np.random.default_rng(999)
     for _ in range(5):
-        A2B = random_transform(random_state)
-        dq = random_state.randn() * dual_quaternion_from_transform(A2B)
+        A2B = random_transform(rng)
+        dq = rng.standard_normal() * dual_quaternion_from_transform(A2B)
         dq_norm = check_dual_quaternion(dq)
         assert_unit_dual_quaternion(dq_norm)
 
 
 def test_conversions_between_dual_quternion_and_transform():
-    random_state = np.random.RandomState(1000)
+    rng = np.random.default_rng(1000)
     for _ in range(5):
-        A2B = random_transform(random_state)
+        A2B = random_transform(rng)
         dq = dual_quaternion_from_transform(A2B)
         A2B2 = transform_from_dual_quaternion(dq)
         assert_array_almost_equal(A2B, A2B2)
         dq2 = dual_quaternion_from_transform(A2B2)
         assert_unit_dual_quaternion_equal(dq, dq2)
     for _ in range(5):
-        p = random_vector(random_state, 3)
-        q = random_quaternion(random_state)
+        p = random_vector(rng, 3)
+        q = random_quaternion(rng)
         dq = dual_quaternion_from_pq(np.hstack((p, q)))
         A2B = transform_from_dual_quaternion(dq)
         dq2 = dual_quaternion_from_transform(A2B)
@@ -662,9 +663,9 @@ def test_conversions_between_dual_quternion_and_transform():
 
 
 def test_conversions_between_dual_quternion_and_pq():
-    random_state = np.random.RandomState(1000)
+    rng = np.random.default_rng(1000)
     for _ in range(5):
-        pq = random_vector(random_state, 7)
+        pq = random_vector(rng, 7)
         pq[3:] /= np.linalg.norm(pq[3:])
         dq = dual_quaternion_from_pq(pq)
         pq2 = pq_from_dual_quaternion(dq)
@@ -674,10 +675,10 @@ def test_conversions_between_dual_quternion_and_pq():
 
 
 def test_dual_quaternion_concatenation():
-    random_state = np.random.RandomState(1000)
+    rng = np.random.default_rng(1000)
     for _ in range(5):
-        A2B = random_transform(random_state)
-        B2C = random_transform(random_state)
+        A2B = random_transform(rng)
+        B2C = random_transform(rng)
         A2C = concat(A2B, B2C)
         dq1 = dual_quaternion_from_transform(A2B)
         dq2 = dual_quaternion_from_transform(B2C)
@@ -687,10 +688,10 @@ def test_dual_quaternion_concatenation():
 
 
 def test_dual_quaternion_applied_to_point():
-    random_state = np.random.RandomState(1000)
+    rng = np.random.default_rng(1000)
     for _ in range(5):
-        p_A = random_vector(random_state, 3)
-        A2B = random_transform(random_state)
+        p_A = random_vector(rng, 3)
+        A2B = random_transform(rng)
         dq = dual_quaternion_from_transform(A2B)
         p_B = dq_prod_vector(dq, p_A)
         assert_array_almost_equal(
@@ -733,8 +734,8 @@ def test_screw_parameters_from_dual_quaternion():
     assert np.isinf(h)
     assert pytest.approx(theta) == np.linalg.norm(np.array([1.2, 1.3, 1.4]))
 
-    random_state = np.random.RandomState(1001)
-    quat = random_quaternion(random_state)
+    rng = np.random.default_rng(1001)
+    quat = random_quaternion(rng)
     a = axis_angle_from_quaternion(quat)
     dq = dual_quaternion_from_pq(
         np.r_[0, 0, 0, quat])
@@ -745,7 +746,7 @@ def test_screw_parameters_from_dual_quaternion():
     assert_array_almost_equal(theta, a[3])
 
     for _ in range(5):
-        A2B = random_transform(random_state)
+        A2B = random_transform(rng)
         dq = dual_quaternion_from_transform(A2B)
         Stheta = exponential_coordinates_from_transform(A2B)
         S, theta = screw_axis_from_exponential_coordinates(Stheta)
@@ -782,9 +783,9 @@ def test_dual_quaternion_from_screw_parameters():
         axis_angle_from_quaternion(pq[3:]),
         norm_axis_angle(np.r_[s_axis, theta]))
 
-    random_state = np.random.RandomState(1001)
+    rng = np.random.default_rng(1001)
     for _ in range(5):
-        A2B = random_transform(random_state)
+        A2B = random_transform(rng)
         Stheta = exponential_coordinates_from_transform(A2B)
         S, theta = screw_axis_from_exponential_coordinates(Stheta)
         q, s_axis, h = screw_parameters_from_screw_axis(S)
@@ -796,17 +797,17 @@ def test_dual_quaternion_from_screw_parameters():
 
 
 def test_dual_quaternion_sclerp_same_dual_quaternions():
-    random_state = np.random.RandomState(19)
-    pose = random_transform(random_state)
+    rng = np.random.default_rng(19)
+    pose = random_transform(rng)
     dq = dual_quaternion_from_transform(pose)
     dq2 = dual_quaternion_sclerp(dq, dq, 0.5)
     assert_array_almost_equal(dq, dq2)
 
 
 def test_dual_quaternion_sclerp():
-    random_state = np.random.RandomState(22)
-    pose1 = random_transform(random_state)
-    pose2 = random_transform(random_state)
+    rng = np.random.default_rng(22)
+    pose1 = random_transform(rng)
+    pose2 = random_transform(rng)
     dq1 = dual_quaternion_from_transform(pose1)
     dq2 = dual_quaternion_from_transform(pose2)
 
@@ -836,8 +837,8 @@ def test_dual_quaternion_sclerp():
 
 def test_dual_quaternion_sclerp_sign_ambiguity():
     n_steps = 10
-    random_state = np.random.RandomState(2323)
-    T1 = random_transform(random_state)
+    rng = np.random.default_rng(2323)
+    T1 = random_transform(rng)
     dq1 = dual_quaternion_from_transform(T1)
     dq2 = np.copy(dq1)
 
@@ -910,3 +911,33 @@ def test_adjoint_from_transform_without_check():
     assert_array_almost_equal(adjoint[3:, 3:], np.ones((3, 3)))
     assert_array_almost_equal(adjoint[3:, :3], np.zeros((3, 3)))
     assert_array_almost_equal(adjoint[:3, 3:], np.zeros((3, 3)))
+
+
+def test_jacobian_se3():
+    Stheta = np.zeros(6)
+
+    J = pt.left_jacobian_SE3(Stheta)
+    J_series = pt.left_jacobian_SE3_series(Stheta, 20)
+    assert_array_almost_equal(J, J_series)
+
+    J_inv = pt.left_jacobian_SE3_inv(Stheta)
+    J_inv_serias = pt.left_jacobian_SE3_inv_series(Stheta, 20)
+    assert_array_almost_equal(J_inv, J_inv_serias)
+
+    J_inv_J = np.dot(J_inv, J)
+    assert_array_almost_equal(J_inv_J, np.eye(6))
+
+    rng = np.random.default_rng(0)
+    for _ in range(5):
+        Stheta = pt.random_exponential_coordinates(rng)
+
+        J = pt.left_jacobian_SE3(Stheta)
+        J_series = pt.left_jacobian_SE3_series(Stheta, 20)
+        assert_array_almost_equal(J, J_series)
+
+        J_inv = pt.left_jacobian_SE3_inv(Stheta)
+        J_inv_serias = pt.left_jacobian_SE3_inv_series(Stheta, 20)
+        assert_array_almost_equal(J_inv, J_inv_serias)
+
+        J_inv_J = np.dot(J_inv, J)
+        assert_array_almost_equal(J_inv_J, np.eye(6))
diff --git a/pytransform3d/test/test_uncertainty.py b/pytransform3d/test/test_uncertainty.py
new file mode 100644
index 000000000..86ae5f691
--- /dev/null
+++ b/pytransform3d/test/test_uncertainty.py
@@ -0,0 +1,171 @@
+import numpy as np
+import pytransform3d.rotations as pr
+import pytransform3d.transformations as pt
+import pytransform3d.uncertainty as pu
+from numpy.testing import assert_array_almost_equal
+import pytest
+
+
+def test_same_fuse_poses():
+    mean1 = np.array([
+        [0.8573, -0.2854, 0.4285, 3.5368],
+        [-0.1113, 0.7098, 0.6956, -3.5165],
+        [-0.5026, -0.6440, 0.5767, -0.9112],
+        [0.0, 0.0, 0.0, 1.0000]
+    ])
+    mean1[:3, :3] = pr.norm_matrix(mean1[:3, :3])
+    mean2 = np.array([
+        [0.5441, -0.6105, 0.5755, -1.0935],
+        [0.8276, 0.5032, -0.2487, 5.5992],
+        [-0.1377, 0.6116, 0.7791, 0.2690],
+        [0.0, 0.0, 0.0, 1.0000]
+    ])
+    mean2[:3, :3] = pr.norm_matrix(mean2[:3, :3])
+    mean3 = np.array([
+        [-0.0211, -0.7869, 0.6167, -3.0968],
+        [-0.2293, 0.6042, 0.7631, 2.0868],
+        [-0.9731, -0.1254, -0.1932, 2.0239],
+        [0.0, 0.0, 0.0, 1.0000]
+    ])
+    mean3[:3, :3] = pr.norm_matrix(mean3[:3, :3])
+    alpha = 5.0
+    cov1 = alpha * np.diag([0.1, 0.2, 0.1, 2.0, 1.0, 1.0])
+    cov2 = alpha * np.diag([0.1, 0.1, 0.2, 1.0, 3.0, 1.0])
+    cov3 = alpha * np.diag([0.2, 0.1, 0.1, 1.0, 1.0, 5.0])
+    means = [mean1, mean2, mean3]
+    covs = [cov1, cov2, cov3]
+    mean_est, cov_est, V = pu.pose_fusion(means, covs)
+    mean_exp = np.array([
+        [0.2967, -0.7157, 0.6323, -1.4887],
+        [0.5338, 0.6733, 0.5116, 0.9935],
+        [-0.7918, 0.1857, 0.5818, -2.7035],
+        [0.0, 0.0, 0.0, 1.0000]
+    ])
+    cov_exp = np.array([
+        [0.14907707, -0.01935277, -0.0107348,
+         -0.02442925, -0.09843835, 0.0054134],
+        [-0.01935277, 0.14648459, 0.02055571,
+         0.11121064, 0.06272014, -0.08553834],
+        [-0.0107348, 0.02055571, 0.15260209,
+         -0.07451066, 0.06531188, -0.01890897],
+        [-0.02442925, 0.11121064, -0.07451066,
+         2.10256906, 0.13695598, -0.29705468],
+        [-0.09843835, 0.06272014, 0.06531188,
+         0.13695598, 2.29286157, -0.58004],
+        [0.0054134, -0.08553834, -0.01890897,
+         -0.29705468, -0.58004, 2.34528443]])
+    assert_array_almost_equal(mean_exp, mean_est, decimal=4)
+    assert_array_almost_equal(cov_exp, cov_est)
+    assert pytest.approx(V, abs=1e-4) == 4.6537
+
+
+def test_invert_pose():
+    rng = np.random.default_rng(2)
+
+    for _ in range(5):
+        T = pt.random_transform(rng)
+        cov = np.diag(rng.random((6, 6)))
+
+        T_inv, cov_inv = pu.invert_uncertain_transform(T, cov)
+
+        T2, cov2 = pu.invert_uncertain_transform(T_inv, cov_inv)
+
+        assert_array_almost_equal(T, T2)
+        assert_array_almost_equal(cov, cov2)
+
+
+def test_sample_estimate_gaussian():
+    rng = np.random.default_rng(2000)
+    mean = pt.transform_from(R=np.eye(3), p=np.array([0.0, 0.0, 0.5]))
+    cov = np.diag([0.001, 0.001, 0.5, 0.001, 0.001, 0.001])
+    samples = np.array([pt.random_transform(rng, mean, cov)
+                        for _ in range(1000)])
+    mean_est, cov_est = pu.estimate_gaussian_transform_from_samples(samples)
+    assert_array_almost_equal(mean, mean_est, decimal=2)
+    assert_array_almost_equal(cov, cov_est, decimal=2)
+
+
+def test_concat_globally_uncertain_transforms():
+    cov_pose_chol = np.diag([0, 0, 0.03, 0, 0, 0])
+    cov_pose = np.dot(cov_pose_chol, cov_pose_chol.T)
+    velocity_vector = np.array([0, 0, 0, 1.0, 0, 0])
+    T_vel = pt.transform_from_exponential_coordinates(velocity_vector)
+    n_steps = 100
+
+    T_est = np.eye(4)
+    cov_est = np.zeros((6, 6))
+    for t in range(n_steps):
+        T_est, cov_est = pu.concat_globally_uncertain_transforms(
+            T_est, cov_est, T_vel, cov_pose)
+    assert_array_almost_equal(
+        T_est, np.array([
+            [1, 0, 0, n_steps],
+            [0, 1, 0, 0],
+            [0, 0, 1, 0],
+            [0, 0, 0, 1]
+        ]))
+    # achievable with second-order terms
+    assert cov_est[2, 2] > 0
+    assert cov_est[4, 4] > 0
+    assert cov_est[2, 4] != 0
+    assert cov_est[4, 2] != 0
+    # achievable only with fourth-order terms
+    assert cov_est[3, 3] > 0
+    assert cov_est[4, 2] != 0
+    assert cov_est[2, 4] != 0
+
+
+def test_to_ellipsoid():
+    mean = np.array([0.1, 0.2, 0.3])
+    cov = np.array([
+        [25.0, 0.0, 0.0],
+        [0.0, 4.0, 0.0],
+        [0.0, 0.0, 9.0]
+    ])
+    ellipsoid2origin, radii = pu.to_ellipsoid(mean, cov)
+    assert_array_almost_equal(ellipsoid2origin[:3, 3], mean)
+    assert_array_almost_equal(
+        ellipsoid2origin[:3, :3],
+        np.array([
+            [0, 0, 1],
+            [1, 0, 0],
+            [0, 1, 0]
+        ])
+    )
+    assert_array_almost_equal(radii, np.array([2.0, 3.0, 5.0]))
+
+    rng = np.random.default_rng(28)
+    for _ in range(5):
+        R = pr.matrix_from_axis_angle(pr.random_axis_angle(rng))
+        std_devs = np.array([1.0, 2.0, 3.0])
+        cov = np.dot(R, np.dot(np.diag(std_devs ** 2), R.T))
+        ellipsoid2origin, radii = pu.to_ellipsoid(mean, cov)
+        assert_array_almost_equal(ellipsoid2origin[:3, 3], mean)
+        # multiple solutions for the rotation matrix are possible because of
+        # ellipsoid symmetries, hence, we only check the absolute values
+        assert_array_almost_equal(np.abs(ellipsoid2origin[:3, :3]), np.abs(R))
+        assert_array_almost_equal(radii, std_devs)
+
+
+def test_projected_ellipsoid():
+    mean = np.eye(4)
+    cov = np.eye(6)
+    x, y, z = pu.to_projected_ellipsoid(mean, cov, factor=1.0, n_steps=20)
+    P = np.column_stack((x.reshape(-1), y.reshape(-1), z.reshape(-1)))
+    r = np.linalg.norm(P, axis=1)
+    assert_array_almost_equal(r, np.ones_like(3))
+
+    pos_mean = np.array([0.5, -5.3, 10.5])
+    mean = pt.transform_from(R=np.eye(3), p=pos_mean)
+    x, y, z = pu.to_projected_ellipsoid(mean, cov, factor=1.0, n_steps=20)
+    P = np.column_stack((x.reshape(-1), y.reshape(-1), z.reshape(-1)))
+    r = np.linalg.norm(P - pos_mean[np.newaxis], axis=1)
+    assert_array_almost_equal(r, np.ones_like(3))
+
+    mean = np.eye(4)
+    cov = np.diag([1, 1, 1, 4, 1, 1])
+    x, y, z = pu.to_projected_ellipsoid(mean, cov, factor=1.0, n_steps=20)
+    P = np.column_stack((x.reshape(-1), y.reshape(-1), z.reshape(-1)))
+    r = np.linalg.norm(P, axis=1)
+    assert np.all(1.0 <= r)
+    assert np.all(r <= 2.0)