# ----------------------------------------------------------------------------
# SymForce - Copyright 2022, Skydio, Inc.
# This source code is under the Apache 2.0 license found in the LICENSE file.
# ----------------------------------------------------------------------------
import unittest
import numpy as np
import symforce.symbolic as sf
from symforce.ops import LieGroupOps
from symforce.ops import StorageOps
from symforce.ops.interfaces.lie_group import LieGroup
from .group_ops_test_mixin import GroupOpsTestMixin
[docs]class LieGroupOpsTestMixin(GroupOpsTestMixin):
"""
Test helper for the LieGroupOps concept. Inherit a test case from this.
"""
# Small number to avoid singularities
EPSILON = 1e-8
# Are retract and local_coordinates defined in terms of the group ops?
MANIFOLD_IS_DEFINED_IN_TERMS_OF_GROUP_OPS = True
[docs] def test_lie_group_ops(self) -> None:
"""
Tests:
- tangent_dim
- from_tangent
- to_tangent
- retract
- local_coordinates
"""
# Create an identity and non-identity element
element = self.element()
identity = LieGroupOps.identity(element)
# Check manifold dimension
dim = LieGroupOps.tangent_dim(element)
self.assertEqual(dim, LieGroupOps.tangent_dim(identity))
self.assertGreater(dim, 0)
# Manifold dimension must be less than or equal to storage dim
self.assertLessEqual(dim, LieGroupOps.storage_dim(identity))
# Construct from a tangent space perturbation around identity
perturbation = list(np.random.normal(scale=0.1, size=(dim,)))
value = LieGroupOps.from_tangent(element, perturbation, epsilon=self.EPSILON)
self.assertEqual(type(value), self.element_type())
# Map back to the tangent space
recovered_perturbation = LieGroupOps.to_tangent(value, epsilon=self.EPSILON)
self.assertEqual(type(recovered_perturbation), list)
# Assert we are close (near epsilon) to the original
self.assertStorageNear(perturbation, recovered_perturbation, places=6)
# Element from zero tangent vector is identity
identity_actual = LieGroupOps.from_tangent(element, [0] * dim, epsilon=self.EPSILON)
self.assertStorageNear(identity, identity_actual, places=7)
# Tangent vector of identity element is zero
tangent_zero_actual = LieGroupOps.to_tangent(identity, epsilon=self.EPSILON)
self.assertStorageNear(tangent_zero_actual, sf.M.zeros(dim, 1), places=7)
# Test zero retraction
element_actual = LieGroupOps.retract(element, [0] * dim, epsilon=self.EPSILON)
self.assertStorageNear(element_actual, element, places=7)
# Test that it recovers the original perturbation
retracted_element = LieGroupOps.retract(element, perturbation, epsilon=self.EPSILON)
perturbation_recovered = LieGroupOps.local_coordinates(
element, retracted_element, epsilon=self.EPSILON
)
self.assertStorageNear(perturbation, perturbation_recovered, places=6)
# Test an identity local coordinates
self.assertStorageNear(
LieGroupOps.local_coordinates(element, element, epsilon=self.EPSILON),
sf.M.zeros(dim, 1),
places=7,
)
[docs] def test_manifold_ops_match_group_ops_definitions(self) -> None:
"""
Tests:
- retract(a, vec) = compose(a, from_tangent(vec))
- local_coordinates(a, b) = to_tangent(between(a, b))
"""
if not self.MANIFOLD_IS_DEFINED_IN_TERMS_OF_GROUP_OPS:
raise unittest.SkipTest(
"This object does not satisfy the constraints this test is evaluating"
)
# Create a non-identity element and a perturbation
element = self.element()
dim = LieGroupOps.tangent_dim(element)
perturbation = list(np.random.normal(scale=0.1, size=(dim,)))
value = LieGroupOps.from_tangent(element, perturbation, epsilon=self.EPSILON)
# Test retraction behaves as expected (compose and from_tangent)
retracted_element = LieGroupOps.retract(element, perturbation, epsilon=self.EPSILON)
self.assertStorageNear(retracted_element, LieGroupOps.compose(element, value), places=7)
# Test local_coordinates behaves as expected (between and to_tangent)
retracted_element = LieGroupOps.retract(element, perturbation, epsilon=self.EPSILON)
perturbation_recovered = LieGroupOps.local_coordinates(
element, retracted_element, epsilon=self.EPSILON
)
diff_element = LieGroupOps.between(element, retracted_element)
self.assertStorageNear(
LieGroupOps.to_tangent(diff_element, epsilon=self.EPSILON),
perturbation_recovered,
places=7,
)
[docs] def test_jacobian(self) -> None:
symbolic_element = StorageOps.symbolic(self.element(), "e")
symbolic_perturbation = sf.M(LieGroupOps.tangent_dim(symbolic_element), 1).symbolic("p")
symbolic_retracted_element = LieGroupOps.retract(
symbolic_element, symbolic_perturbation.to_flat_list()
)
LieGroupOps.jacobian(symbolic_retracted_element, symbolic_perturbation, tangent_space=True)
LieGroupOps.jacobian(symbolic_retracted_element, symbolic_perturbation, tangent_space=False)
if isinstance(self.element(), (LieGroup, sf.Matrix)):
# These classes should also have a .jacobian instance method
# TODO(aaron): Should there be a separate test mixin for these classes?
symbolic_retracted_element.jacobian(symbolic_perturbation, tangent_space=True)
symbolic_retracted_element.jacobian(symbolic_perturbation, tangent_space=False)
[docs] def test_storage_D_tangent(self) -> None:
element = self.element()
# TODO(nathan): We have to convert to a sf.Matrix for scalars
# and elements without a hardcoded storage_D_tangent function
storage_D_tangent = sf.M(LieGroupOps.storage_D_tangent(element))
# Check that the jacobian is the correct dimension
storage_dim = StorageOps.storage_dim(element)
tangent_dim = LieGroupOps.tangent_dim(element)
self.assertEqual(storage_D_tangent.shape, (storage_dim, tangent_dim))
# Check that the jacobian is close to a numerical approximation
xi = sf.Matrix(tangent_dim, 1).symbolic("xi")
element_perturbed = LieGroupOps.retract(element, xi.to_flat_list())
element_perturbed_storage = StorageOps.to_storage(element_perturbed)
storage_D_tangent_approx = sf.M(element_perturbed_storage).jacobian(xi)
storage_D_tangent_approx = storage_D_tangent_approx.subs(xi, self.EPSILON * xi.one())
self.assertStorageNear(storage_D_tangent, storage_D_tangent_approx)
[docs] def test_tangent_D_storage(self) -> None:
element = self.element()
# TODO(nathan): We have to convert to a sf.Matrix for scalars
tangent_D_storage = sf.M(LieGroupOps.tangent_D_storage(element))
# Check that the jacobian is the correct dimension
storage_dim = StorageOps.storage_dim(element)
tangent_dim = LieGroupOps.tangent_dim(element)
self.assertEqual(tangent_D_storage.shape, (tangent_dim, storage_dim))
# Check that the jacobian is close to a numerical approximation
xi = sf.Matrix(storage_dim, 1).symbolic("xi")
storage_perturbed = sf.M(LieGroupOps.to_storage(element)) + xi
element_perturbed = LieGroupOps.from_storage(element, storage_perturbed.to_flat_list())
element_perturbed_tangent = sf.M(
LieGroupOps.local_coordinates(element, element_perturbed, self.EPSILON)
)
tangent_D_storage_approx = element_perturbed_tangent.jacobian(xi)
tangent_D_storage_approx = tangent_D_storage_approx.subs(xi, xi.zero())
self.assertStorageNear(tangent_D_storage, tangent_D_storage_approx)