Skip to content

Add support for the S1 (circle) manifold and its tangent space #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 15, 2025

Conversation

edxmorgan
Copy link
Contributor

Support for the 1-D circle manifold (S1) by:

  • Implementing S1 class with wrapping, multiplication, inverse, log/exp maps
  • Adding S1Tangent for tangent-space operations
  • Exporting S1 and S1Tangent in liecasadi/init.py
  • Extending generic operations to include S1 and S1Tangent
  • Adding unit tests for identity, multiplication, subtraction, log/exp consistency

@Giulero Giulero requested a review from Copilot June 25, 2025 08:05
@Giulero
Copy link
Collaborator

Giulero commented Jun 25, 2025

Thanks @edxmorgan for the PR! This is indeed a nice addition.
Sorry for the late response, I missed the notification.
Rewieving now!

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for the 1-D circle manifold (S1) and its tangent space, integrates it into the package API, and provides corresponding tests.

  • Introduces S1 and S1Tangent classes with wrapping, multiplication, inverse, and exp/log maps
  • Updates exports and generic operations to include S1 and S1Tangent
  • Adds unit tests for identity, multiplication, subtraction, and log/exp consistency

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tests/test_liecasadi.py Adds tests for S1 and S1Tangent functionality
src/liecasadi/s1.py Implements S1 manifold and S1Tangent operations
src/liecasadi/init.py Exports S1 and S1Tangent
README.md Updates documentation to include S1 support
Comments suppressed due to low confidence (2)

src/liecasadi/s1.py:24

  • Using a raw float for the identity angle may cause type mismatches with CasADi symbolic types; consider using cs.DM(0) or a CasADi scalar constructor to maintain consistency.
        return S1(0.0)

src/liecasadi/s1.py:78

  • [nitpick] For consistency with S1.as_angle(), consider renaming value() to as_value() or as_scalar() to clearly convey its purpose.
    def value(self) -> Scalar:

Comment on lines 153 to 160
# S1 objects
S1_angle = (np.random.rand() - 0.5) * 2 * np.pi
myS1 = S1(S1_angle)

# S1Tangent objects
S1_tangent_ang = (np.random.rand() - 0.5) * 2 * np.pi
myS1Tang = S1Tangent(S1_tangent_ang)

Copy link
Preview

Copilot AI Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Remove or utilize the unused variables S1_angle, myS1, S1_tangent_ang, and myS1Tang in the test file to eliminate noise and improve maintainability.

Suggested change
# S1 objects
S1_angle = (np.random.rand() - 0.5) * 2 * np.pi
myS1 = S1(S1_angle)
# S1Tangent objects
S1_tangent_ang = (np.random.rand() - 0.5) * 2 * np.pi
myS1Tang = S1Tangent(S1_tangent_ang)
# Removed unused variables S1_angle, myS1, S1_tangent_ang, and myS1Tang.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S1_angle, myS1, S1_tangent_ang, and myS1Tang unused variables removed

@edxmorgan
Copy link
Contributor Author

@Giulero review required. thanks

@Giulero
Copy link
Collaborator

Giulero commented Jul 7, 2025

Hi @edxmorgan ! Left a comment related to the function that wraps to pi - see the review. What do you think?

@edxmorgan
Copy link
Contributor Author

Hi @edxmorgan ! Left a comment related to the function that wraps to pi - see the review. What do you think?

@Giulero I'm sorry but i am unable to find this comment. can you please point it out with a quote reply

@Giulero
Copy link
Collaborator

Giulero commented Jul 7, 2025

No worries! I mean this comment #19 (comment)

@edxmorgan
Copy link
Contributor Author

edxmorgan commented Jul 7, 2025

okay . I see. I am able to partially read it like this. I think github has a bug (https://github.com/orgs/community/discussions/30638). The comment itself does not exist for me.
Screenshot 2025-07-07 at 1 07 52 PM.

@Giulero
Copy link
Collaborator

Giulero commented Jul 8, 2025

Uh I see! This is what i wrote, related to https://github.com/edxmorgan/liecasadi/blob/e02f79e6d090ced2e95bf72ae0d41540eee0e6c9/src/liecasadi/s1.py#L7-L9


I'm not sure this function is continuous and differentiable :/
A possible solution could be to use something like

def _wrap_to_pi(angle: Scalar) -> Scalar:
	s = cs.sin(angle)
	c = cs.cos(angle) 
	return cs.atan2(s, c)

which should be continuous.
The issue is when angle == 0.0, but maybe adding a small tolerance np.finfo().eps could solve this.
@edxmorgan what do you think?

@edxmorgan
Copy link
Contributor Author

edxmorgan commented Jul 8, 2025

Hi @Giulero, excellent catch!

atan2(cs.sin(θ), cs.cos(θ)) stays continuous on (−π, π] because sin²θ + cos²θ = 1, so the pair never reaches (0, 0). At θ = 0 the call is atan2(0, 1), which returns 0 cleanly, so an ε-guard isn’t necessary. The only nondifferentiable points are the inherent branch cuts at ±π.

I’ll replace the current implementation with this function. Thanks for spotting it! 🚀

Copy link
Collaborator

@Giulero Giulero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Thanks @edxmorgan :)
I think we can merge

from liecasadi.hints import Angle, Scalar


def _wrap_to_pi(angle: Scalar) -> Scalar:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this function is continuous and differentiable :/
A possible solution could be to use something like

def _wrap_to_pi(angle: Scalar) -> Scalar:
	s = cs.sin(angle)
	c = cs.cos(angle) 
	return cs.atan2(s, c)

which should be continuous.
The issue is when angle == 0.0, but maybe adding a small tolerance np.finfo().eps could solve this.
@edxmorgan what do you think?

cc @edxmorgan

assert float(rebuilt.as_angle()) == pytest.approx(float(cs.pi / 3))

tangent = a.log()
assert float(tangent.exp().as_angle()) == pytest.approx(float(cs.pi / 3))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line missing!

@@ -2,8 +2,8 @@
import numpy as np
import pytest
from scipy.spatial.transform import Rotation

from liecasadi import SE3, SO3, SE3Tangent, SO3Tangent
import casadi as cs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a new module, it could make sense to split the tests into multiple files. But we can do it in a different pr

Copy link
Contributor Author

@edxmorgan edxmorgan Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right! Splitting the tests into separate files will make it look organized. I’ll open a PR for that.

@Giulero Giulero merged commit d2f8574 into ami-iit:main Jul 15, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants