**Proposal Overview**
This is an issue to discuss a proposal to extend the [`s…cipy.spatial.transform`](https://scipy.github.io/devdocs/reference/spatial.transform.html) module. Currently, only 3-D rotations are supported under the module. What I would like to add is a `Translation` object which would apply 3-D translational offsets to points. This would be fairly basic on its own, but would become much more powerful by allowing composition with `Rotations`. Such a composition would be captured in a new `Transformation` object, where each `Transformation` represents a `Rotation` followed by a `Translation`. This would fully implement what are formally known as [proper rigid transformations](https://en.wikipedia.org/wiki/Rigid_transformation), or the 3-D [special Euclidean group SE(3)](https://en.wikipedia.org/wiki/Euclidean_group).
**Applications**
The end goal I am shooting for here is allowing for the representation of arbitrary _coordinate frames_ in 3 dimensions. Working with and converting between coordinate frames is fundamental functionality that is critical to the fields of robotics, aerospace, mechanical engineering, computer graphics, and anything that has to do with observing, modeling, analyzing, or controlling the physical world in 3-D space. I think that this is a natural extension to the `scipy.spatial.transform` module that would be broadly useful to the scientific community over many domains.
This page gives a good introduction to the problem and some canonical representations: http://motion.cs.illinois.edu/RoboticSystems/CoordinateTransformations.html
![ros_tf](https://github.com/scipy/scipy/assets/14363975/8fe99015-c7bc-44fe-a4ca-ab7121362475)
**Proposed Object Structure**
Proposed object structure in the `scipy.spatial.transform` module:
```
- _BaseTransformation: Abstract base class to define types of transformations
- Rotation: Current Rotation class, now subclassed
- Translation: New Translation subclass
- Transformation: A proper rigid transformation, defined as a rotation followed by a translation.
Formed from the composition of one or more _BaseTransformation objects.
```
All `_BaseTransformation` objects would be composable with each other using the `__mul__`operator, can compose with itself with the `__pow__` operator, has an identity operation that can be generated with an `identity()` constructor, is invertible with an `inv()` method, and can act on 3-vectors with an `apply()` method. Each object would be able to hold multiple transformations, and we would follow the same broadcasting rules as currently followed by the `Rotations` class.
**Proposed API**
```python
import numpy as np
from scipy.spatial.transform import Rotation as R, Translation as t, Transformation as T
### Translation ###
## Basic operations
t1 = t.from_vector([x1, y1, z1]) # Form a Translation that represents an offset of [x1, y1, z1] from the origin
t1.as_vector() # Returns np.array([x1, y1, z1])
t1.inv() # Returns a Translation that represents an offset of [-x1, -y1, -z1] from the origin
t1.apply([a, b, c]) # Returns np.array([a + x1, b + y1, c + z1])
## Other constructors
t0 = t.identity() # Same as t.from_vector([0, 0, 0])
## Composition with __mul__ and __pow__
t2 = t.from_vector([x2, y2, z2])
t1 * t2 # Same as t.from_vector([x1 + x2, y1 + y2, z1 + z2])
t1 ** n # Same as t.from_vector([n * x1, n * y1, n * z1])
### Transformation ###
## Basic operations
r1 = R.from_rotvec([u1, v1, w1])
T1 = T.from_transformations([r1, t1]) # A Transformation defined by rotation, followed by translation. TODO: ordering?
T1.as_basetransformations() # Returns (r1, t1)
T1 = t1 * r1 # Same as T.from_transformations([r1, t1])
T1.inv() # Returns a transformation equivalent to r1.inv() * t1.inv()
T1.apply([a, b, c]) # Same as t1.apply(r1.apply([a, b, c]))
## Attributes
T1.rotation # Returns r1
T1.translation # Returns t1
T1 == T1.translation * T1.rotation # Always holds true (though TODO we need to think about how to make comparisons)
## Composition rules
# Every composition of an arbitrary number of rotations and translations is equivalent to a single rotation followed
# by a single translation. A Transformation object will store only that simplified pair, and not the chain of
# component translations that generated it
# When composing multiple transformations, the following relationships hold. This is enough to simplify any chain
# of transformations
T1.apply([a, b, c]) = (t1 * r1).apply([a, b, c]) = t1.apply(r1.apply([a, b, c])) # Same as above
r1 * t1 = t.from_vector(r1.apply(t1.as_vector())) * r1 # So in most cases, r1 * t1 != t1 * r1
r2 * (t1 * r1) = (r2 * t1) * (r2 * r1)
t2 * (t1 * r1) = (t2 * t1) * r1
X * r2 * r1 * Y = X * (r2 * r1) * Y # Follows from above, not a base axiom
X * t2 * t1 * Y = X * (t2 * t1) * Y # Follows from above, not a base axiom
## Other constructors
T.identity() # Same as t.identity() * R.identity()
T1_copy = T.from_transformations(T1) # Can take in Translation objects, and single inputs are ok if not array_like
T2 = t1 * t2 * r1 * T1 * r2 # Can chain together Translations, Rotations, and Transformations.
T2.apply([x, y, z]) # Same as t1.apply(t2.apply(r1.apply(T1.apply(r2.apply([x, y, z])))))
## Composition with __mul__ and __pow__
T1 * T1 # Same as t1 * r1 * t1 * r1
T1 ** 2 # Same as T1 * T1
T1 ** -2 # Same as T1.inv() * T1.inv()
T1 ** n # Same as t1 ** (n % 1) * r1 ** (n % 1) * (t1 * r1) * ... * (t1 * r1) # TODO: does non-integer n make sense?
## Change of reference frames
T1_wrt_T2 = T1 * T2.inv() # The T1 coordinate frame expressed in reference frame T2
## Other methods
T1.rotate_about([x2, y2, z2], r2) # Same as (r2 * t2) * t2.inv() * T1
# The first argument could be points or a Translation object
T1.rotate_about([x2, y2, z2], R.from_rotvec(theta*np.array([u2, v2, w2]))) # Rotate angle theta about a line uvw
T1.rotate_about(T1.translation, r2) # A pure rotation of T1 about its local coordinate origin
### New top-level methods in scipy.spatial.transform ###
TranslationLerp(times, translations) # Analagous to scipy.spatial.transform.Slerp(times, rotations), TODO: better name?
TranslationSpline(times, translations) # Analagous to scipy.spatial.transform.RotationSpline(times, rotations)
TransformationLerp(times, transformations) # Composition of TranslationLerp and Slerp, TODO: better name?
TransformationSpline(times, transformations) # Composition of TranslationSpline and RotationSpline
```
**Out Of Scope Enhancements**
For this proposal, the following would be excellent future additions to the `scipy.spatial.transform` module, but are out of scope of this effort. However, the modularity of the class structure introduced here lends itself to easy extension of the latter 3 items. Scale, shear, and reflection transformations would be their own `_BaseTransformation` subclasses, and would also be able to be composed with all other `_BaseTransformations` to form a `Transformation` object.
- 2-D transformations (we have something like this in `scipy.ndimage.affine_transform` but that's for pixel grids specifically)
- N-D transformations for N > 3
- Scale transformations
- Shear transformations
- Reflections
Related to https://github.com/scipy/scipy/issues/17753, which proposes general affine transformations (I think a good future direction, where this issue would be the first step).
**Implementations in Other Libraries**
I found the following python libraries which implement this sort of functionality. I am having trouble finding the original discussion around adding the `Rotation` class to scipy, but I would expect the arguments to incorporate it to apply to the proposed extensions here as well. I think there is quite a bit of value in having this in scipy as a reference implementation, for not relying on additional package imports for this functionality, and for putting this functionality under the umbrella of a larger developer community.
- [pytransform3d](https://dfki-ric.github.io/pytransform3d/index.html) (highly recommended for people looking for more functionality)
- [rigid-body-motion](https://rigid-body-motion.readthedocs.io/en/latest/)
- [transforms3d](http://matthew-brett.github.io/transforms3d/)
**Feedback Wanted**
I am interested in implementing this, [having done some work](https://github.com/scipy/scipy/pulls?q=is%3Apr+author%3Ascottshambaugh+rotation) on the `Rotation` class, but have not started writing code yet. I'm hoping to solicit feedback on the proposal before that - most importantly whether this should be done at all at, and then scope, naming, API changes would all be very welcome feedback. I'm also interested in thoughts on how best to break up this work - one large PR, or multiple smaller ones? A summary of this proposal [has been sent to the mailing list](https://mail.python.org/archives/list/scipy-dev@python.org/thread/IXY2PJJRN2GMXVIPHE5XEHSV3MPEMTG5/#IXY2PJJRN2GMXVIPHE5XEHSV3MPEMTG5). (And pinging @nmayorov specifically)