Rotation and Symmetry

Module: cutoop.rotation.

You can add CUTOOP_STRICT_MODE=1 to environment to enable a strict value assertion before calculating rotation error.

SymLabel

classSymLabel​(any: bool, x: Literal['none', 'any', 'half', 'quarter'], y: Literal['none', 'any', 'half', 'quarter'], z: Literal['none', 'any', 'half', 'quarter'])

Bases: object

Symmetry labels for real-world objects.

Axis rotation details:

  • any : arbitrary rotation around this axis is ok
  • half : rotate 180 degrees along this axis (central symmetry)
  • quarter : rotate 90 degrees along this axis (like square)
>>> from cutoop.rotation import SymLabel
>>> sym = SymLabel(any=False, x='any', y='none', z='none')
>>> str(sym)
'x-cone'
>>> sym = SymLabel(any=False, x='any', y='any', z='none') # two any produces 'any'
>>> str(sym)
'any'
>>> sym = SymLabel(any=False, x='half', y='half', z='half')
>>> str(sym)
'box'
propertyany: bool

Whether arbitrary rotation is allowed

propertyx: Literal['none', 'any', 'half', 'quarter']

axis rotation for x

propertyy: Literal['none', 'any', 'half', 'quarter']

axis rotation for y

propertyz: Literal['none', 'any', 'half', 'quarter']

axis rotation for z

method__str__​(): str

For human readability, rotations are divided into the following types (names):

  • any : arbitrary rotation is ok;
  • cube : the same symmetry as a cube;
  • box : the same symmetry as a box (flipping along x, y, and z axis);
  • none : no symmetry is provided;
  • {x,y,z}-flip : flip along a single axis;
  • {x,y,z}-square-tube : the same symmetry as a square tube alone the axis;
  • {x,y,z}-square-pyramid : the same symmetry as a pyramid alone the axis;
  • {x,y,z}-cylinder : the same symmetry as a cylinder the axis;
  • {x,y,z}-cone : the same symmetry as a cone the axis.
methodfrom_str​(s: str): SymLabel

Construct symmetry from string.

Note

See also STANDARD_SYMMETRY .

methodget_only​(tag: Literal['any', 'half', 'quarter'])

Get the only axis marked with the tag. If multiple or none is find, return None .

Rotation Manipulation

functionrot_canonical_sym​(rA_3x3: ndarray, rB_3x3: ndarray, sym: SymLabel, split = 100, return_theta = False): ndarray | tuple[ndarray, float]

Find the optimal rotation rot that minimize theta(rA, rB @ rot) .

Parameters:

  • rA_3x3 – often the ground truth rotation.
  • rB_3x3 – often the predicted rotation.
  • sym – symmetry label.
  • split – see rots_of_sym() .
  • return_theta – if enabled, a tuple of the rot and its theta will both be returned.

Returns: the optimal rot

functionrots_of_sym​(sym: SymLabel, split = 20): ndarray

Get a list of rotation group corresponding to the sym label.

Parameters: split – Set the snap of rotation to 2 * pi / split for continuous symmetry.

Returns: ndarray of shape ?, 3, 3 containing a set of rotation matrix.

Rotation Error Computation

functionrot_diff_axis​(rA_3x3: ndarray, rB_Nx3x3: ndarray, axis: ndarray): ndarray

compute the difference angle where rotation aroud axis is ignored.

Parameters: axis – One of ax .

functionrot_diff_sym​(rA_3x3: ndarray, rB_3x3: ndarray, sym: SymLabel): float

compute the difference angle (rotation error) with regard of symmetry.

This function use analytic method to calculate the difference angle, which is more accurate than rot_canonical_sym() .

Returns: the difference angle.

functionrot_diff_theta​(rA_3x3: ndarray, rB_Nx3x3: ndarray): ndarray

Compute the difference angle of one rotation with a series of rotations.

Note that since arccos gets quite steep around 0, the computational loss is somehow inevitable (around 1e-4).

Returns: theta (unit: radius) array of length N.

functionrot_diff_theta_pointwise​(rA_Nx3x3: ndarray, rB_Nx3x3: ndarray): ndarray

compute the difference angle of two sequences of rotations pointwisely.

Returns: theta (unit: radius) array of length N

Prelude Rotations

objectSTANDARD_SYMMETRY = {'any': SymLabel(any=True, x='any', y='any', z='any'), 'box': SymLabel(any=False, x='half', y='half', z='half'), 'cube': SymLabel(any=False, x='quarter', y='quarter', z='quarter'), 'none': SymLabel(any=False, x='none', y='none', z='none'), 'x-cone': SymLabel(any=False, x='any', y='none', z='none'), 'x-cylinder': SymLabel(any=False, x='any', y='half', z='half'), 'x-flip': SymLabel(any=False, x='half', y='none', z='none'), 'x-square-pyramid': SymLabel(any=False, x='quarter', y='none', z='none'), 'x-square-tube': SymLabel(any=False, x='quarter', y='half', z='half'), 'y-cone': SymLabel(any=False, x='none', y='any', z='none'), 'y-cylinder': SymLabel(any=False, x='half', y='any', z='half'), 'y-flip': SymLabel(any=False, x='none', y='half', z='none'), 'y-square-pyramid': SymLabel(any=False, x='none', y='quarter', z='none'), 'y-square-tube': SymLabel(any=False, x='half', y='quarter', z='half'), 'z-cone': SymLabel(any=False, x='none', y='none', z='any'), 'z-cylinder': SymLabel(any=False, x='half', y='half', z='any'), 'z-flip': SymLabel(any=False, x='none', y='none', z='half'), 'z-square-pyramid': SymLabel(any=False, x='none', y='none', z='quarter'), 'z-square-tube': SymLabel(any=False, x='half', y='half', z='quarter')}

All standard symmetries.

>>> for name, sym in cutoop.rotation.STANDARD_SYMMETRY.items():
...     assert str(sym) == name, f"name: {name}, sym: {repr(sym)}"
objectcube_group: list[ndarray]

All 24 rotations of a cube (Sym(4)).

The correctness of this implementation lies in:

  1. cube_flip_group is a normal subgroup of alternating group
  2. alternating group Alt(4) is a normal subgroup of Sym(4)

Thus we can construct Sym(4) by enumerating all cosets of Alt(4), which is construct by enumerating all cosets of cube_flip_group .

One can check validity of it by

>>> from cutoop.rotation import cube_group
>>> for i in range(24):
...     for j in range(24):
...         if i < j:
...             diff = cube_group[i] @ cube_group[j].T
...             assert np.linalg.norm(diff - np.eye(3)) > 0.1
objectcube_flip_group: list[ndarray]

All 4 rotations of a box, as 3x3 matrices. That is rotating 180 degrees around x y z, respectively (abelian group).

objectqtr = {'x': [array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]), array([[ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00], [ 0.00000000e+00, 2.22044605e-16, -1.00000000e+00], [ 0.00000000e+00, 1.00000000e+00, 2.22044605e-16]]), array([[ 1.0000000e+00, 0.0000000e+00, 0.0000000e+00], [ 0.0000000e+00, -1.0000000e+00, -1.2246468e-16], [ 0.0000000e+00, 1.2246468e-16, -1.0000000e+00]]), array([[ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00], [ 0.00000000e+00, 2.22044605e-16, 1.00000000e+00], [ 0.00000000e+00, -1.00000000e+00, 2.22044605e-16]])], 'y': [array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]), array([[ 2.22044605e-16, 0.00000000e+00, 1.00000000e+00], [ 0.00000000e+00, 1.00000000e+00, 0.00000000e+00], [-1.00000000e+00, 0.00000000e+00, 2.22044605e-16]]), array([[-1.0000000e+00, 0.0000000e+00, 1.2246468e-16], [ 0.0000000e+00, 1.0000000e+00, 0.0000000e+00], [-1.2246468e-16, 0.0000000e+00, -1.0000000e+00]]), array([[ 2.22044605e-16, 0.00000000e+00, -1.00000000e+00], [ 0.00000000e+00, 1.00000000e+00, 0.00000000e+00], [ 1.00000000e+00, 0.00000000e+00, 2.22044605e-16]])], 'z': [array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]), array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00], [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00], [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]), array([[-1.0000000e+00, -1.2246468e-16, 0.0000000e+00], [ 1.2246468e-16, -1.0000000e+00, 0.0000000e+00], [ 0.0000000e+00, 0.0000000e+00, 1.0000000e+00]]), array([[ 2.22044605e-16, 1.00000000e+00, 0.00000000e+00], [-1.00000000e+00, 2.22044605e-16, 0.00000000e+00], [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])]}

All transformations composed by 90-degree rotations along each axis.

objectax = {'x': array([1, 0, 0]), 'y': array([0, 1, 0]), 'z': array([0, 0, 1])}

Normalized axis vectors.

objectrot90 = {'x': array([[ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00], [ 0.00000000e+00, 2.22044605e-16, -1.00000000e+00], [ 0.00000000e+00, 1.00000000e+00, 2.22044605e-16]]), 'y': array([[ 2.22044605e-16, 0.00000000e+00, 1.00000000e+00], [ 0.00000000e+00, 1.00000000e+00, 0.00000000e+00], [-1.00000000e+00, 0.00000000e+00, 2.22044605e-16]]), 'z': array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00], [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00], [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])}

90-degree rotation along each axis.

objectrot180 = {'x': array([[ 1.0000000e+00, 0.0000000e+00, 0.0000000e+00], [ 0.0000000e+00, -1.0000000e+00, -1.2246468e-16], [ 0.0000000e+00, 1.2246468e-16, -1.0000000e+00]]), 'y': array([[-1.0000000e+00, 0.0000000e+00, 1.2246468e-16], [ 0.0000000e+00, 1.0000000e+00, 0.0000000e+00], [-1.2246468e-16, 0.0000000e+00, -1.0000000e+00]]), 'z': array([[-1.0000000e+00, -1.2246468e-16, 0.0000000e+00], [ 1.2246468e-16, -1.0000000e+00, 0.0000000e+00], [ 0.0000000e+00, 0.0000000e+00, 1.0000000e+00]])}

flipping along each axis