qitaoz commited on
Commit
26ce2a9
·
verified ·
1 Parent(s): 4f54ccd

init commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. README.md +158 -12
  3. liegroups/.gitignore +6 -0
  4. liegroups/LICENSE +21 -0
  5. liegroups/README.md +36 -0
  6. liegroups/build/lib/liegroups/__init__.py +16 -0
  7. liegroups/build/lib/liegroups/_base.py +218 -0
  8. liegroups/build/lib/liegroups/numpy/__init__.py +9 -0
  9. liegroups/build/lib/liegroups/numpy/_base.py +172 -0
  10. liegroups/build/lib/liegroups/numpy/se2.py +206 -0
  11. liegroups/build/lib/liegroups/numpy/se3.py +354 -0
  12. liegroups/build/lib/liegroups/numpy/so2.py +161 -0
  13. liegroups/build/lib/liegroups/numpy/so3.py +430 -0
  14. liegroups/build/lib/liegroups/torch/__init__.py +9 -0
  15. liegroups/build/lib/liegroups/torch/_base.py +383 -0
  16. liegroups/build/lib/liegroups/torch/se2.py +160 -0
  17. liegroups/build/lib/liegroups/torch/se3.py +320 -0
  18. liegroups/build/lib/liegroups/torch/so2.py +157 -0
  19. liegroups/build/lib/liegroups/torch/so3.py +458 -0
  20. liegroups/build/lib/liegroups/torch/utils.py +49 -0
  21. liegroups/docs/Makefile +225 -0
  22. liegroups/docs/build/doctrees/environment.pickle +3 -0
  23. liegroups/docs/build/doctrees/index.doctree +0 -0
  24. liegroups/docs/build/doctrees/numpy.doctree +0 -0
  25. liegroups/docs/build/doctrees/torch.doctree +0 -0
  26. liegroups/docs/build/html/.buildinfo +4 -0
  27. liegroups/docs/build/html/_sources/index.rst.txt +14 -0
  28. liegroups/docs/build/html/_sources/numpy.rst.txt +53 -0
  29. liegroups/docs/build/html/_sources/torch.rst.txt +31 -0
  30. liegroups/docs/build/html/_static/ajax-loader.gif +0 -0
  31. liegroups/docs/build/html/_static/basic.css +764 -0
  32. liegroups/docs/build/html/_static/comment-bright.png +0 -0
  33. liegroups/docs/build/html/_static/comment-close.png +0 -0
  34. liegroups/docs/build/html/_static/comment.png +0 -0
  35. liegroups/docs/build/html/_static/css/badge_only.css +1 -0
  36. liegroups/docs/build/html/_static/css/theme.css +0 -0
  37. liegroups/docs/build/html/_static/doctools.js +314 -0
  38. liegroups/docs/build/html/_static/documentation_options.js +10 -0
  39. liegroups/docs/build/html/_static/down-pressed.png +0 -0
  40. liegroups/docs/build/html/_static/down.png +0 -0
  41. liegroups/docs/build/html/_static/file.png +0 -0
  42. liegroups/docs/build/html/_static/fonts/Inconsolata-Bold.ttf +0 -0
  43. liegroups/docs/build/html/_static/fonts/Inconsolata-Regular.ttf +0 -0
  44. liegroups/docs/build/html/_static/fonts/Inconsolata.ttf +0 -0
  45. liegroups/docs/build/html/_static/fonts/Lato-Bold.ttf +0 -0
  46. liegroups/docs/build/html/_static/fonts/Lato-Regular.ttf +0 -0
  47. liegroups/docs/build/html/_static/fonts/Lato/lato-bold.eot +0 -0
  48. liegroups/docs/build/html/_static/fonts/Lato/lato-bold.ttf +0 -0
  49. liegroups/docs/build/html/_static/fonts/Lato/lato-bold.woff +0 -0
  50. liegroups/docs/build/html/_static/fonts/Lato/lato-bold.woff2 +0 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ simple-knn/build/lib.linux-x86_64-cpython-39/simple_knn/_C.cpython-39-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
37
+ simple-knn/build/temp.linux-x86_64-cpython-39/.ninja_deps filter=lfs diff=lfs merge=lfs -text
38
+ simple-knn/build/temp.linux-x86_64-cpython-39/simple_knn.o filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,158 @@
1
- ---
2
- title: SparseAGS
3
- emoji: 🐨
4
- colorFrom: indigo
5
- colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 5.7.1
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SparseAGS
2
+
3
+ This repository contains the official implementation for **Sparse-view Pose Estimation and Reconstruction via Analysis by Generative Synthesis**. The paper has been accepted to [NeurIPS 2024](https://neurips.cc/Conferences/2024).
4
+
5
+ ### [Project Page](https://qitaozhao.github.io/SparseAGS) | [arXiv (Coming Soon)](https://qitaozhao.github.io/SparseAGS)
6
+
7
+ ### News
8
+
9
+ - 2024.12.02: Initial code release.
10
+
11
+ ## Introduction
12
+
13
+ **tl;dr** Given a set of unposed input images, **SparseAGS** jointly infers the corresponding camera poses and underlying 3D, allowing high-fidelity 3D inference in the wild.
14
+
15
+ **Abstract.** Inferring the 3D structure underlying a set of multi-view images typically requires solving two co-dependent tasks -- accurate 3D reconstruction requires precise camera poses, and predicting camera poses relies on (implicitly or explicitly) modeling the underlying 3D. The classical framework of analysis by synthesis casts this inference as a joint optimization seeking to explain the observed pixels, and recent instantiations learn expressive 3D representations (e.g., Neural Fields) with gradient-descent-based pose refinement of initial pose estimates. However, given a sparse set of observed views, the observations may not provide sufficient direct evidence to obtain complete and accurate 3D. Moreover, large errors in pose estimation may not be easily corrected and can further degrade the inferred 3D. To allow robust 3D reconstruction and pose estimation in this challenging setup, we propose *SparseAGS*, a method that adapts this analysis-by-synthesis approach by: a) including novel-view-synthesis-based generative priors in conjunction with photometric objectives to improve the quality of the inferred 3D, and b) explicitly reasoning about outliers and using a discrete search with a continuous optimization-based strategy to correct them. We validate our framework across real-world and synthetic datasets in combination with several off-the-shelf pose estimation systems as initialization. We find that it significantly improves the base systems' pose accuracy while yielding high-quality 3D reconstructions that outperform the results from current multi-view reconstruction baselines.
16
+
17
+ ![teasert](assets/teaser.gif)
18
+
19
+ ## Install
20
+
21
+ 1. Clone SparseAGS:
22
+
23
+ ```bash
24
+ git clone --recursive https://github.com/QitaoZhao/SparseAGS.git
25
+ cd SparseAGS
26
+ # if you have already cloned sparseags:
27
+ # git submodule update --init --recursive
28
+ ```
29
+
30
+ 2. Create the environment and install packages:
31
+
32
+ ```bash
33
+ conda create -n sparseags python=3.9
34
+ conda activate sparseags
35
+
36
+ # enable nvcc
37
+ conda install -c conda-forge cudatoolkit-dev
38
+
39
+ ### torch
40
+ # CUDA 11.7
41
+ pip install torch==1.13.0+cu117 torchvision==0.14.0+cu117 torchaudio==0.13.0 --extra-index-url https://download.pytorch.org/whl/cu117
42
+
43
+ # CUDA 12.1
44
+ pip install torch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu121
45
+
46
+ pip install -r requirements.txt
47
+
48
+ ### pytorch3D
49
+ # CUDA 11.7
50
+ conda install https://anaconda.org/pytorch3d/pytorch3d/0.7.5/download/linux-64/pytorch3d-0.7.5-py39_cu117_pyt1130.tar.bz2
51
+
52
+ # CUDA 12.1
53
+ conda install https://anaconda.org/pytorch3d/pytorch3d/0.7.8/download/linux-64/pytorch3d-0.7.8-py39_cu121_pyt210.tar.bz2
54
+
55
+ # liegroups (minor modification to https://github.com/utiasSTARS/liegroups)
56
+ pip install ./liegroups
57
+
58
+ # simple-knn
59
+ pip install ./simple-knn
60
+
61
+ # a modified gaussian splatting that enables camera pose optimization
62
+ pip install ./diff-gaussian-rasterization-camera
63
+ ```
64
+
65
+ Tested on:
66
+
67
+ - Ubuntu 20.04 with torch 1.13 & CUDA 11.7 on an A5000 GPU.
68
+ - Springdale Linux 8.6 with torch 2.1.0 & CUDA 12.1 on an A5000 GPU.
69
+ - Red Hat Enterprise Linux 8.10 with torch 1.13 & CUDA 11.7 on a V100 GPU.
70
+
71
+ Note: Look at this [issue](https://github.com/graphdeco-inria/gaussian-splatting/issues/993) or try `sudo apt-get install libglm-dev` if you encounter `fatal error: glm/glm.hpp: No such file or directory` when doing `pip install ./diff-gaussian-rasterization-camera`.
72
+
73
+ 3. Download our 6-DoF Zero123 [checkpoint](https://drive.google.com/file/d/1JJ4wjaJ4IkUERRZYRrlNoQ-tXvftEYJD/view?usp=sharing) and place it in `SparseAGS/checkpoints`.
74
+
75
+ ```bash
76
+ mkdir checkpoints
77
+ cd checkpoints/
78
+ pip install gdown
79
+ gdown "https://drive.google.com/uc?id=1JJ4wjaJ4IkUERRZYRrlNoQ-tXvftEYJD"
80
+ cd ..
81
+ ```
82
+
83
+ ## Usage
84
+
85
+ (1) **Gradio Demo** (recommended, where you can upload your own images or use our preprocessed examples interactively):
86
+
87
+ ```bash
88
+ # first-time running may take a longer time
89
+ python gradio_app.py
90
+ ```
91
+
92
+ (2) Use command lines:
93
+
94
+ ```bash
95
+ ### preprocess
96
+ # background removal and recentering, save rgba at 256x256
97
+ python process.py data/name.jpg
98
+
99
+ # save at a larger resolution
100
+ python process.py data/name.jpg --size 512
101
+
102
+ # process all jpg images under a dir
103
+ python process.py data
104
+
105
+ ### sparse-view 3D reconstruction
106
+ # here we have some preprocessed examples in 'data/demo', with dust3r pose initialization
107
+ # the output will be saved in 'output/demo/{category}'
108
+ # valid category-num_views options are {[toy, 4], [butter, 6], [jordan, 8], [robot, 8], [eagle, 8]}
109
+
110
+ # run single 3D reconstruction (w/o outlier removal & correction)
111
+ python run.py --category jordan --num_views 8
112
+
113
+ # if you find the command above does not give you nice 3D, try enbaling loop-based outlier removal & correction (which takes more time)
114
+ python run.py --category jordan --num_views 8 --enable_loop
115
+ ```
116
+
117
+ Note: Actually, we include the `eagle` example to showcase how our full method works (we found in our experiments that dust3r gives one bad pose for this example). For other examples, you are supposed to see reasonable 3D with a single 3D reconstruction.
118
+
119
+ ## Tips
120
+
121
+ * The world & camera coordinate system is the same as OpenGL:
122
+ ```
123
+ World Camera
124
+
125
+ +y up target
126
+ | | /
127
+ | | /
128
+ |______+x |/______right
129
+ / /
130
+ / /
131
+ / /
132
+ +z forward
133
+
134
+ elevation: in (-90, 90), from +y to -y is (-90, 90)
135
+ azimuth: in (-180, 180), from +z to +x is (0, 90)
136
+ ```
137
+
138
+ ## Acknowledgments
139
+
140
+ from https://github.com/ashawkey/diff-gaussian-rasterization
141
+
142
+ This work is built on many amazing research works and open-source projects, thanks a lot to all the authors for sharing!
143
+
144
+ - [gaussian-splatting](https://github.com/graphdeco-inria/gaussian-splatting) and [diff-gaussian-rasterization](https://github.com/graphdeco-inria/diff-gaussian-rasterization)
145
+ - [threestudio](https://github.com/threestudio-project/threestudio)
146
+ - [nvdiffrast](https://github.com/NVlabs/nvdiffrast)
147
+ - [dearpygui](https://github.com/hoffstadt/DearPyGui)
148
+
149
+ ## Citation
150
+
151
+ ```
152
+ @inproceedings{zhao2024sparseags,
153
+ title={Sparse-view Pose Estimation and Reconstruction via Analysis by Generative Synthesis},
154
+ author={Qitao Zhao and Shubham Tulsiani},
155
+ booktitle={NeurIPS},
156
+ year={2024}
157
+ }
158
+ ```
liegroups/.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ __pycache__
3
+ .cache
4
+ *.egg-info
5
+ .vscode
6
+ *.pyc
liegroups/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Lee Clement
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
liegroups/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # liegroups
2
+ Python implementation of SO2, SE2, SO3, and SE3 matrix Lie groups using numpy or PyTorch.
3
+
4
+ [[Documentation]](docs/build/html/index.html)
5
+
6
+ ## Installation
7
+ To install, `cd` into the repository directory (the one with `setup.py`) and run:
8
+ ```bash
9
+ pip install .
10
+ ```
11
+ or
12
+ ```bash
13
+ pip install -e .
14
+ ```
15
+ The `-e` flag tells pip to install the package in-place, which lets you make changes to the code without having to reinstall every time. *Do not do this on shared workstations!*
16
+
17
+ ## Testing
18
+ Ensure you have `pytest` installed on your system, or install it using `conda install pytest` or `pip install pytest`. Then run `pytest` in the repository directory.
19
+
20
+ ## Usage
21
+ Numpy and torch implementations are accessible through the `liegroups.numpy` and `liegroups.torch` subpackages.
22
+ By default, the numpy implementation is available through the top-level package.
23
+
24
+ Access the numpy implementation using something like
25
+ ```python
26
+ from liegroups import SE3
27
+ ```
28
+ or
29
+ ```python
30
+ from liegroups.numpy import SO2
31
+ ```
32
+
33
+ Access the pytorch implementation using something like
34
+ ```python
35
+ from liegroups.torch import SE2
36
+ ```
liegroups/build/lib/liegroups/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Special Euclidean and Special Orthogonal Lie groups."""
2
+
3
+ from .numpy import SO2 as SO2
4
+ from .numpy import SE2 as SE2
5
+ from .numpy import SO3 as SO3
6
+ from .numpy import SE3 as SE3
7
+
8
+
9
+ try:
10
+ from . import numpy
11
+ from . import torch
12
+ except:
13
+ pass
14
+
15
+ __author__ = "Lee Clement"
16
+ __email__ = "lee.clement@robotics.utias.utoronto.ca"
liegroups/build/lib/liegroups/_base.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABCMeta, abstractmethod
2
+
3
+ # support for both python 2 and 3
4
+ from future.utils import with_metaclass
5
+
6
+
7
+ class LieGroupBase(with_metaclass(ABCMeta)):
8
+ """ Common abstract base class defining basic interface for Lie groups.
9
+ Does not depend on any specific linear algebra library.
10
+ """
11
+
12
+ def __init__(self):
13
+ pass
14
+
15
+ @property
16
+ @classmethod
17
+ @abstractmethod
18
+ def dim(cls):
19
+ """Dimension of the underlying representation."""
20
+ pass
21
+
22
+ @property
23
+ @classmethod
24
+ @abstractmethod
25
+ def dof(cls):
26
+ """Underlying degrees of freedom (i.e., dimension of the tangent space)."""
27
+ pass
28
+
29
+ @abstractmethod
30
+ def dot(self, other):
31
+ """Multiply another group element or one or more vectors on the left.
32
+ """
33
+ pass
34
+
35
+ @classmethod
36
+ @abstractmethod
37
+ def exp(cls, vec):
38
+ """Exponential map for the group.
39
+
40
+ Computes a transformation from a tangent vector.
41
+
42
+ This is the inverse operation to log.
43
+ """
44
+ pass
45
+
46
+ @classmethod
47
+ @abstractmethod
48
+ def identity(cls):
49
+ """Return the identity transformation."""
50
+ pass
51
+
52
+ @abstractmethod
53
+ def inv(self):
54
+ """Return the inverse transformation."""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def log(self):
59
+ """Logarithmic map for the group.
60
+
61
+ Computes a tangent vector from a transformation.
62
+
63
+ This is the inverse operation to exp.
64
+ """
65
+ pass
66
+
67
+ @abstractmethod
68
+ def normalize(self):
69
+ """Normalize the group element to ensure it is valid and
70
+ negate the effect of rounding errors.
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ def perturb(self, vec):
76
+ """Perturb the group element on the left by a vector in its local tangent space.
77
+ """
78
+ pass
79
+
80
+
81
+ class MatrixLieGroupBase(LieGroupBase):
82
+ """Common abstract base class defining basic interface for Matrix Lie Groups.
83
+ Does not depend on any specific linear algebra library.
84
+ """
85
+
86
+ def __repr__(self):
87
+ """Return a string representation of the transformation."""
88
+ return "<{}.{}>\n{}".format(self.__class__.__module__, self.__class__.__name__, self.as_matrix()).replace("\n", "\n| ")
89
+
90
+ @abstractmethod
91
+ def adjoint(self):
92
+ """Return the adjoint matrix of the transformation."""
93
+ pass
94
+
95
+ @abstractmethod
96
+ def as_matrix(self):
97
+ """Return the matrix representation of the transformation."""
98
+ pass
99
+
100
+ @classmethod
101
+ @abstractmethod
102
+ def from_matrix(cls, mat, normalize=False):
103
+ """Create a transformation from a matrix (safe, but slower)."""
104
+ pass
105
+
106
+ @classmethod
107
+ @abstractmethod
108
+ def inv_left_jacobian(cls, vec):
109
+ """Inverse of the left Jacobian for the group."""
110
+ pass
111
+
112
+ @classmethod
113
+ @abstractmethod
114
+ def is_valid_matrix(cls, mat):
115
+ """Check if a matrix is a valid transformation matrix."""
116
+ pass
117
+
118
+ @classmethod
119
+ @abstractmethod
120
+ def left_jacobian(cls, vec):
121
+ """Left Jacobian for the group."""
122
+ pass
123
+
124
+ @classmethod
125
+ @abstractmethod
126
+ def vee(cls, mat):
127
+ """vee operator as defined by Barfoot.
128
+
129
+ This is the inverse operation to wedge.
130
+ """
131
+ pass
132
+
133
+ @classmethod
134
+ @abstractmethod
135
+ def wedge(cls, vec):
136
+ """wedge operator as defined by Barfoot.
137
+
138
+ This is the inverse operation to vee.
139
+ """
140
+ pass
141
+
142
+
143
+ class SOMatrixBase(MatrixLieGroupBase):
144
+ """Common abstract base class for Special Orthogonal Matrix Lie Groups SO(N).
145
+ Does not depend on any specific linear algebra library.
146
+ """
147
+
148
+ def __init__(self, mat):
149
+ """Create a transformation from a rotation matrix (unsafe, but faster)."""
150
+ self.mat = mat
151
+ """Storage for the rotation matrix."""
152
+
153
+ def as_matrix(self):
154
+ """Return the matrix representation of the rotation."""
155
+ return self.mat
156
+
157
+ def perturb(self, phi):
158
+ """Perturb the rotation in-place on the left by a vector in its local tangent space.
159
+
160
+ .. math::
161
+ \\mathbf{C} \\gets \\exp(\\boldsymbol{\\phi}^\\wedge) \\mathbf{C}
162
+ """
163
+ self.mat = self.__class__.exp(phi).dot(self).mat
164
+
165
+
166
+ class SEMatrixBase(MatrixLieGroupBase):
167
+ """Common abstract base class for Special Euclidean Matrix Lie Groups SE(N).
168
+ Does not depend on any specific linear algebra library.
169
+ """
170
+
171
+ def __init__(self, rot, trans):
172
+ """Create a transformation from a translation and a rotation (unsafe, but faster)"""
173
+ self.rot = rot
174
+ """Storage for the rotation matrix."""
175
+ self.trans = trans
176
+ """Storage for the translation vector."""
177
+
178
+ @classmethod
179
+ @abstractmethod
180
+ def odot(cls, p, directional=False):
181
+ """odot operator as defined by Barfoot."""
182
+ pass
183
+
184
+ def perturb(self, xi):
185
+ """Perturb the transformation in-place on the left by a vector in its local tangent space.
186
+
187
+ .. math::
188
+ \\mathbf{T} \\gets \\exp(\\boldsymbol{\\xi}^\\wedge) \\mathbf{T}
189
+ """
190
+ perturbed = self.__class__.exp(xi).dot(self)
191
+ self.rot = perturbed.rot
192
+ self.trans = perturbed.trans
193
+
194
+ @property
195
+ @classmethod
196
+ @abstractmethod
197
+ def RotationType(cls):
198
+ """Rotation type."""
199
+ pass
200
+
201
+
202
+ class VectorLieGroupBase(LieGroupBase):
203
+ """Common abstract base class for Lie Groups with vector parametrizations
204
+ (complex, quaternions, dual quaternions). Does not depend on any
205
+ specific linear algebra library.
206
+ """
207
+
208
+ def __init__(self, data):
209
+ self.data = data
210
+
211
+ def __repr__(self):
212
+ """Return a string representation of the transformation."""
213
+ return "<{}.{}>\n{}".format(self.__class__.__module__, self.__class__.__name__, self.data).replace("\n", "\n| ")
214
+
215
+ @abstractmethod
216
+ def conjugate(self):
217
+ """Return the conjugate of the vector"""
218
+ pass
liegroups/build/lib/liegroups/numpy/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Numpy implementations of Special Euclidean and Special Orthogonal Lie groups."""
2
+
3
+ from .so2 import SO2Matrix as SO2
4
+ from .se2 import SE2Matrix as SE2
5
+ from .so3 import SO3Matrix as SO3
6
+ from .se3 import SE3Matrix as SE3
7
+
8
+ __author__ = "Lee Clement"
9
+ __email__ = "lee.clement@robotics.utias.utoronto.ca"
liegroups/build/lib/liegroups/numpy/_base.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from .. import _base
4
+
5
+
6
+ class SOMatrixBase(_base.SOMatrixBase):
7
+ """Implementation of methods common to SO(N) matrix lie groups using Numpy"""
8
+
9
+ def dot(self, other):
10
+ """Multiply another rotation or one or more vectors on the left.
11
+ """
12
+ if isinstance(other, self.__class__):
13
+ # Compound with another rotation
14
+ return self.__class__(np.dot(self.mat, other.mat))
15
+ else:
16
+ other = np.atleast_2d(other)
17
+
18
+ # Transform one or more 2-vectors or fail
19
+ if other.shape[1] == self.dim:
20
+ return np.squeeze(np.dot(self.mat, other.T).T)
21
+ else:
22
+ raise ValueError(
23
+ "Vector must have shape ({},) or (N,{})".format(self.dim, self.dim))
24
+
25
+ @classmethod
26
+ def identity(cls):
27
+ """Return the identity rotation."""
28
+ return cls(np.identity(cls.dim))
29
+
30
+ def inv(self):
31
+ """Return the inverse rotation:
32
+
33
+ .. math::
34
+ \\mathbf{C}^{-1} = \\mathbf{C}^T
35
+ """
36
+ return self.__class__(self.mat.T)
37
+
38
+ @classmethod
39
+ def from_matrix(cls, mat, normalize=False):
40
+ """Create a rotation from a matrix (safe, but slower).
41
+
42
+ Throws an error if mat is invalid and normalize=False.
43
+ If normalize=True invalid matrices will be normalized to be valid.
44
+ """
45
+ mat_is_valid = cls.is_valid_matrix(mat)
46
+
47
+ if mat_is_valid or normalize:
48
+ result = cls(mat)
49
+ if not mat_is_valid and normalize:
50
+ result.normalize()
51
+ else:
52
+ raise ValueError(
53
+ "Invalid rotation matrix. Use normalize=True to handle rounding errors.")
54
+
55
+ return result
56
+
57
+ @classmethod
58
+ def is_valid_matrix(cls, mat):
59
+ """Check if a matrix is a valid rotation matrix."""
60
+ return mat.shape == (cls.dim, cls.dim) and \
61
+ np.isclose(np.linalg.det(mat), 1.) and \
62
+ np.allclose(mat.T.dot(mat), np.identity(cls.dim))
63
+
64
+ def normalize(self):
65
+ """Normalize the rotation matrix to ensure it is valid and
66
+ negate the effect of rounding errors.
67
+ """
68
+ # The SVD is commonly written as a = U S V.H.
69
+ # The v returned by this function is V.H and u = U.
70
+ U, _, V = np.linalg.svd(self.mat, full_matrices=False)
71
+
72
+ S = np.identity(self.dim)
73
+ S[self.dim - 1, self.dim - 1] = np.linalg.det(U) * np.linalg.det(V)
74
+
75
+ self.mat = U.dot(S).dot(V)
76
+
77
+
78
+ class SEMatrixBase(_base.SEMatrixBase):
79
+ """Implementation of methods common to SE(N) matrix lie groups using Numpy"""
80
+
81
+ def as_matrix(self):
82
+ """Return the matrix representation of the rotation."""
83
+ R = self.rot.as_matrix()
84
+ t = np.reshape(self.trans, (self.dim - 1, 1))
85
+ bottom_row = np.append(np.zeros(self.dim - 1), 1.)
86
+ return np.vstack([np.hstack([R, t]),
87
+ bottom_row])
88
+
89
+ def dot(self, other):
90
+ """Multiply another rotation or one or more vectors on the left.
91
+ """
92
+ if isinstance(other, self.__class__):
93
+ # Compound with another transformation
94
+ return self.__class__(self.rot.dot(other.rot),
95
+ self.rot.dot(other.trans) + self.trans)
96
+ else:
97
+ other = np.atleast_2d(other)
98
+
99
+ if other.shape[1] == self.dim - 1:
100
+ # Transform one or more 2-vectors
101
+ return np.squeeze(self.rot.dot(other) + self.trans)
102
+ elif other.shape[1] == self.dim:
103
+ # Transform one or more 3-vectors
104
+ return np.squeeze(self.as_matrix().dot(other.T)).T
105
+ else:
106
+ raise ValueError("Vector must have shape ({},), ({},), (N,{}) or (N,{})".format(
107
+ self.dim - 1, self.dim, self.dim - 1, self.dim))
108
+
109
+ @classmethod
110
+ def from_matrix(cls, mat, normalize=False):
111
+ """Create a transformation from a matrix (safe, but slower).
112
+
113
+ Throws an error if mat is invalid and normalize=False.
114
+ If normalize=True invalid matrices will be normalized to be valid.
115
+ """
116
+ mat_is_valid = cls.is_valid_matrix(mat)
117
+
118
+ if mat_is_valid or normalize:
119
+ result = cls(
120
+ cls.RotationType(mat[0:cls.dim - 1, 0:cls.dim - 1]),
121
+ mat[0:cls.dim - 1, cls.dim - 1])
122
+ if not mat_is_valid and normalize:
123
+ result.normalize()
124
+ else:
125
+ raise ValueError(
126
+ "Invalid transformation matrix. Use normalize=True to handle rounding errors.")
127
+
128
+ return result
129
+
130
+ @classmethod
131
+ def identity(cls):
132
+ """Return the identity transformation."""
133
+ return cls.from_matrix(np.identity(cls.dim))
134
+
135
+ def inv(self):
136
+ """Return the inverse transformation:
137
+
138
+ .. math::
139
+ \\mathbf{T}^{-1} =
140
+ \\begin{bmatrix}
141
+ \\mathbf{C}^T & -\\mathbf{C}^T\\mathbf{r} \\\\
142
+ \\mathbf{0}^T & 1
143
+ \\end{bmatrix}
144
+ """
145
+ inv_rot = self.rot.inv()
146
+ inv_trans = -(inv_rot.dot(self.trans))
147
+ return self.__class__(inv_rot, inv_trans)
148
+
149
+ @classmethod
150
+ def is_valid_matrix(cls, mat):
151
+ """Check if a matrix is a valid transformation matrix."""
152
+ bottom_row = np.append(np.zeros(cls.dim - 1), 1.)
153
+
154
+ return mat.shape == (cls.dim, cls.dim) and \
155
+ np.array_equal(mat[cls.dim - 1, :], bottom_row) and \
156
+ cls.RotationType.is_valid_matrix(mat[0:cls.dim - 1, 0:cls.dim - 1])
157
+
158
+ def normalize(self):
159
+ """Normalize the transformation matrix to ensure it is valid and
160
+ negate the effect of rounding errors.
161
+ """
162
+ self.rot.normalize()
163
+
164
+
165
+ class VectorLieGroupBase(_base.VectorLieGroupBase):
166
+ """Implementation of methods common to vector-parametrized lie groups using Numpy"""
167
+
168
+ def normalize(self):
169
+ self.data = self.data / np.linalg.norm(self.data)
170
+
171
+ def conjugate(self):
172
+ return self.__class__(np.hstack([self.data[0], -self.data[1:]]))
liegroups/build/lib/liegroups/numpy/se2.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from . import _base
4
+ from .so2 import SO2Matrix
5
+
6
+
7
+ class SE2Matrix(_base.SEMatrixBase):
8
+ """Homogeneous transformation matrix in :math:`SE(2)` using active (alibi) transformations.
9
+
10
+ .. math::
11
+ SE(2) &= \\left\\{ \\mathbf{T}=
12
+ \\begin{bmatrix}
13
+ \\mathbf{C} & \\mathbf{r} \\\\
14
+ \\mathbf{0}^T & 1
15
+ \\end{bmatrix} \\in \\mathbb{R}^{3 \\times 3} ~\\middle|~ \\mathbf{C} \\in SO(2), \\mathbf{r} \\in \\mathbb{R}^2 \\right\\} \\\\
16
+ \\mathfrak{se}(2) &= \\left\\{ \\boldsymbol{\\Xi} =
17
+ \\boldsymbol{\\xi}^\\wedge \\in \\mathbb{R}^{3 \\times 3} ~\\middle|~
18
+ \\boldsymbol{\\xi}=
19
+ \\begin{bmatrix}
20
+ \\boldsymbol{\\rho} \\\\ \\phi
21
+ \\end{bmatrix} \\in \\mathbb{R}^3, \\boldsymbol{\\rho} \\in \\mathbb{R}^2, \\phi \\in \\mathbb{R} \\right\\}
22
+
23
+ :cvar ~liegroups.SE2.dim: Dimension of the rotation matrix.
24
+ :cvar ~liegroups.SE2.dof: Underlying degrees of freedom (i.e., dimension of the tangent space).
25
+ :ivar rot: Storage for the rotation matrix :math:`\\mathbf{C}`.
26
+ :ivar trans: Storage for the translation vector :math:`\\mathbf{r}`.
27
+ """
28
+ dim = 3
29
+ """Dimension of the transformation matrix."""
30
+ dof = 3
31
+ """Underlying degrees of freedom (i.e., dimension of the tangent space)."""
32
+ RotationType = SO2Matrix
33
+
34
+ def adjoint(self):
35
+ """Adjoint matrix of the transformation.
36
+
37
+ .. math::
38
+ \\text{Ad}(\\mathbf{T}) =
39
+ \\begin{bmatrix}
40
+ \\mathbf{C} & 1^\\wedge \\mathbf{r} \\\\
41
+ \\mathbf{0}^T & 1
42
+ \\end{bmatrix}
43
+ \\in \\mathbb{R}^{3 \\times 3}
44
+ """
45
+ rot_part = self.rot.as_matrix()
46
+ trans_part = np.array([self.trans[1], -self.trans[0]]).reshape((2, 1))
47
+ return np.vstack([np.hstack([rot_part, trans_part]),
48
+ [0, 0, 1]])
49
+
50
+ @classmethod
51
+ def exp(cls, xi):
52
+ """Exponential map for :math:`SE(2)`, which computes a transformation from a tangent vector:
53
+
54
+ .. math::
55
+ \\mathbf{T}(\\boldsymbol{\\xi}) =
56
+ \\exp(\\boldsymbol{\\xi}^\\wedge) =
57
+ \\begin{bmatrix}
58
+ \\exp(\\phi ^\\wedge) & \\mathbf{J} \\boldsymbol{\\rho} \\\\
59
+ \\mathbf{0} ^ T & 1
60
+ \\end{bmatrix}
61
+
62
+ This is the inverse operation to :meth:`~liegroups.SE2.log`.
63
+ """
64
+ if len(xi) != cls.dof:
65
+ raise ValueError("xi must have length {}".format(cls.dof))
66
+
67
+ rho = xi[0:2]
68
+ phi = xi[2]
69
+ return cls(cls.RotationType.exp(phi),
70
+ cls.RotationType.left_jacobian(phi).dot(rho))
71
+
72
+ @classmethod
73
+ def inv_left_jacobian(cls, xi):
74
+ """:math:`SE(2)` inverse left Jacobian.
75
+
76
+ .. math::
77
+ \\mathcal{J}^{-1}(\\boldsymbol{\\xi})
78
+ """
79
+ raise NotImplementedError
80
+
81
+ @classmethod
82
+ def left_jacobian(cls, xi):
83
+ """:math:`SE(2)` left Jacobian.
84
+
85
+ .. math::
86
+ \\mathcal{J}(\\boldsymbol{\\xi})
87
+ """
88
+ raise NotImplementedError
89
+
90
+ def log(self):
91
+ """Logarithmic map for :math:`SE(2)`, which computes a tangent vector from a transformation:
92
+
93
+ .. math::
94
+ \\boldsymbol{\\xi}(\\mathbf{T}) =
95
+ \\ln(\\mathbf{T})^\\vee =
96
+ \\begin{bmatrix}
97
+ \\mathbf{J} ^ {-1} \\mathbf{r} \\\\
98
+ \\ln(\\boldsymbol{C}) ^\\vee
99
+ \\end{bmatrix}
100
+
101
+ This is the inverse operation to :meth:`~liegroups.SE2.log`.
102
+ """
103
+ phi = self.rot.log()
104
+ rho = self.RotationType.inv_left_jacobian(phi).dot(self.trans)
105
+ return np.hstack([rho, phi])
106
+
107
+ @classmethod
108
+ def odot(cls, p, directional=False):
109
+ """:math:`SE(2)` odot operator as defined by Barfoot.
110
+
111
+ This is the Jacobian of a vector
112
+
113
+ .. math::
114
+ \\mathbf{p} =
115
+ \\begin{bmatrix}
116
+ sx \\\\ sy \\\\ sz \\\\ s
117
+ \\end{bmatrix} =
118
+ \\begin{bmatrix}
119
+ \\boldsymbol{\\epsilon} \\\\ \\eta
120
+ \\end{bmatrix}
121
+
122
+ with respect to a perturbation in the underlying parameters of :math:`\\mathbf{T}`.
123
+
124
+ If :math:`\\mathbf{p}` is given in Euclidean coordinates and directional=False, the missing scale value :math:`\\eta` is assumed to be 1 and the Jacobian is 2x3. If directional=True, :math:`\\eta` is assumed to be 0:
125
+
126
+ .. math::
127
+ \\mathbf{p}^\\odot =
128
+ \\begin{bmatrix}
129
+ \\eta \\mathbf{1} & 1^\\wedge \\boldsymbol{\\epsilon}
130
+ \\end{bmatrix}
131
+
132
+ If :math:`\\mathbf{p}` is given in Homogeneous coordinates, the Jacobian is 3x3:
133
+
134
+ .. math::
135
+ \\mathbf{p}^\\odot =
136
+ \\begin{bmatrix}
137
+ \\eta \\mathbf{1} & 1^\\wedge \\boldsymbol{\\epsilon} \\\\
138
+ \\mathbf{0}^T & 0
139
+ \\end{bmatrix}
140
+ """
141
+ p = np.atleast_2d(p)
142
+ result = np.zeros([p.shape[0], p.shape[1], cls.dof])
143
+
144
+ if p.shape[1] == cls.dim - 1:
145
+ # Assume scale parameter is 1 unless p is a direction
146
+ # vector, in which case the scale is 0
147
+ if not directional:
148
+ result[:, 0:2, 0:2] = np.eye(2)
149
+
150
+ result[:, 0:2, 2] = cls.RotationType.wedge(1).dot(p.T).T
151
+
152
+ elif p.shape[1] == cls.dim:
153
+ result[:, 0:2, 0:2] = p[:, 2] * np.eye(2)
154
+ result[:, 0:2, 2] = cls.RotationType.wedge(1).dot(p[:, 0:2].T).T
155
+
156
+ else:
157
+ raise ValueError("p must have shape ({},), ({},), (N,{}) or (N,{})".format(
158
+ cls.dim - 1, cls.dim, cls.dim - 1, cls.dim))
159
+
160
+ return np.squeeze(result)
161
+
162
+ @classmethod
163
+ def vee(cls, Xi):
164
+ """:math:`SE(2)` vee operator as defined by Barfoot.
165
+
166
+ .. math::
167
+ \\boldsymbol{\\xi} = \\boldsymbol{\\Xi} ^\\vee
168
+
169
+ This is the inverse operation to :meth:`~liegroups.SE2.wedge`.
170
+ """
171
+ if Xi.ndim < 3:
172
+ Xi = np.expand_dims(Xi, axis=0)
173
+
174
+ if Xi.shape[1:3] != (cls.dof, cls.dof):
175
+ raise ValueError("Xi must have shape ({},{}) or (N,{},{})".format(
176
+ cls.dof, cls.dof, cls.dof, cls.dof))
177
+
178
+ xi = np.empty([Xi.shape[0], cls.dof])
179
+ xi[:, 0:2] = Xi[:, 0:2, 2]
180
+ xi[:, 2] = cls.RotationType.vee(Xi[:, 0:2, 0:2])
181
+ return np.squeeze(xi)
182
+
183
+ @classmethod
184
+ def wedge(cls, xi):
185
+ """:math:`SE(2)` wedge operator as defined by Barfoot.
186
+
187
+ .. math::
188
+ \\boldsymbol{\\Xi} =
189
+ \\boldsymbol{\\xi} ^\\wedge =
190
+ \\begin{bmatrix}
191
+ \\phi ^\\wedge & \\boldsymbol{\\rho} \\\\
192
+ \\mathbf{0} ^ T & 0
193
+ \\end{bmatrix}
194
+
195
+ This is the inverse operation to :meth:`~liegroups.SE2.vee`.
196
+ """
197
+ xi = np.atleast_2d(xi)
198
+ if xi.shape[1] != cls.dof:
199
+ raise ValueError(
200
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
201
+
202
+ Xi = np.zeros([xi.shape[0], cls.dof, cls.dof])
203
+ Xi[:, 0:2, 0:2] = cls.RotationType.wedge(xi[:, 2])
204
+ Xi[:, 0:2, 2] = xi[:, 0:2]
205
+
206
+ return np.squeeze(Xi)
liegroups/build/lib/liegroups/numpy/se3.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from . import _base
4
+ from .so3 import SO3Matrix
5
+
6
+
7
+ class SE3Matrix(_base.SEMatrixBase):
8
+ """Homogeneous transformation matrix in :math:`SE(3)` using active (alibi) transformations.
9
+
10
+ .. math::
11
+ SE(3) &= \\left\\{ \\mathbf{T}=
12
+ \\begin{bmatrix}
13
+ \\mathbf{C} & \\mathbf{r} \\\\
14
+ \\mathbf{0}^T & 1
15
+ \\end{bmatrix} \\in \\mathbb{R}^{4 \\times 4} ~\\middle|~ \\mathbf{C} \\in SO(3), \\mathbf{r} \\in \\mathbb{R}^3 \\right\\} \\\\
16
+ \\mathfrak{se}(3) &= \\left\\{ \\boldsymbol{\\Xi} =
17
+ \\boldsymbol{\\xi}^\\wedge \\in \\mathbb{R}^{4 \\times 4} ~\\middle|~
18
+ \\boldsymbol{\\xi}=
19
+ \\begin{bmatrix}
20
+ \\boldsymbol{\\rho} \\\\ \\boldsymbol{\\phi}
21
+ \\end{bmatrix} \\in \\mathbb{R}^6, \\boldsymbol{\\rho} \\in \\mathbb{R}^3, \\boldsymbol{\\phi} \\in \\mathbb{R}^3 \\right\\}
22
+
23
+ :cvar ~liegroups.SE2.dim: Dimension of the rotation matrix.
24
+ :cvar ~liegroups.SE2.dof: Underlying degrees of freedom (i.e., dimension of the tangent space).
25
+ :ivar rot: Storage for the rotation matrix :math:`\\mathbf{C}`.
26
+ :ivar trans: Storage for the translation vector :math:`\\mathbf{r}`.
27
+ """
28
+ dim = 4
29
+ """Dimension of the transformation matrix."""
30
+ dof = 6
31
+ """Underlying degrees of freedom (i.e., dimension of the tangent space)."""
32
+ RotationType = SO3Matrix
33
+
34
+ def adjoint(self):
35
+ """Adjoint matrix of the transformation.
36
+
37
+ .. math::
38
+ \\text{Ad}(\\mathbf{T}) =
39
+ \\begin{bmatrix}
40
+ \\mathbf{C} & \\mathbf{r}^\\wedge\\mathbf{C} \\\\
41
+ \\mathbf{0} & \\mathbf{C}
42
+ \\end{bmatrix}
43
+ \\in \\mathbb{R}^{6 \\times 6}
44
+ """
45
+ rotmat = self.rot.as_matrix()
46
+ return np.vstack(
47
+ [np.hstack([rotmat,
48
+ self.RotationType.wedge(self.trans).dot(rotmat)]),
49
+ np.hstack([np.zeros((3, 3)), rotmat])]
50
+ )
51
+
52
+ @classmethod
53
+ def curlyvee(cls, Psi):
54
+ """:math:`SE(3)` curlyvee operator as defined by Barfoot.
55
+
56
+ .. math::
57
+ \\boldsymbol{\\xi} =
58
+ \\boldsymbol{\\Psi}^\\curlyvee
59
+
60
+ This is the inverse operation to :meth:`~liegroups.SE3.curlywedge`.
61
+ """
62
+ if Psi.ndim < 3:
63
+ Psi = np.expand_dims(Psi, axis=0)
64
+
65
+ if Psi.shape[1:3] != (cls.dof, cls.dof):
66
+ raise ValueError("Psi must have shape ({},{}) or (N,{},{})".format(
67
+ cls.dof, cls.dof, cls.dof, cls.dof))
68
+
69
+ xi = np.empty([Psi.shape[0], cls.dof])
70
+ xi[:, 0:3] = cls.RotationType.vee(Psi[:, 0:3, 3:6])
71
+ xi[:, 3:6] = cls.RotationType.vee(Psi[:, 0:3, 0:3])
72
+
73
+ return np.squeeze(xi)
74
+
75
+ @classmethod
76
+ def curlywedge(cls, xi):
77
+ """:math:`SE(3)` curlywedge operator as defined by Barfoot.
78
+
79
+ .. math::
80
+ \\boldsymbol{\\Psi} =
81
+ \\boldsymbol{\\xi}^\\curlywedge =
82
+ \\begin{bmatrix}
83
+ \\boldsymbol{\\phi}^\\wedge & \\boldsymbol{\\rho}^\\wedge \\\\
84
+ \\mathbf{0} & \\boldsymbol{\\phi}^\\wedge
85
+ \\end{bmatrix}
86
+
87
+ This is the inverse operation to :meth:`~liegroups.SE3.curlyvee`.
88
+ """
89
+ xi = np.atleast_2d(xi)
90
+ if xi.shape[1] != cls.dof:
91
+ raise ValueError(
92
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
93
+
94
+ Psi = np.zeros([xi.shape[0], cls.dof, cls.dof])
95
+ Psi[:, 0:3, 0:3] = cls.RotationType.wedge(xi[:, 3:6])
96
+ Psi[:, 0:3, 3:6] = cls.RotationType.wedge(xi[:, 0:3])
97
+ Psi[:, 3:6, 3:6] = Psi[:, 0:3, 0:3]
98
+
99
+ return np.squeeze(Psi)
100
+
101
+ @classmethod
102
+ def exp(cls, xi):
103
+ """Exponential map for :math:`SE(3)`, which computes a transformation from a tangent vector:
104
+
105
+ .. math::
106
+ \\mathbf{T}(\\boldsymbol{\\xi}) =
107
+ \\exp(\\boldsymbol{\\xi}^\\wedge) =
108
+ \\begin{bmatrix}
109
+ \\exp(\\boldsymbol{\\phi}^\\wedge) & \\mathbf{J} \\boldsymbol{\\rho} \\\\
110
+ \\mathbf{0} ^ T & 1
111
+ \\end{bmatrix}
112
+
113
+ This is the inverse operation to :meth:`~liegroups.SE3.log`.
114
+ """
115
+ if len(xi) != cls.dof:
116
+ raise ValueError("xi must have length {}".format(cls.dof))
117
+
118
+ rho = xi[0:3]
119
+ phi = xi[3:6]
120
+ return cls(cls.RotationType.exp(phi),
121
+ cls.RotationType.left_jacobian(phi).dot(rho))
122
+
123
+ @classmethod
124
+ def left_jacobian_Q_matrix(cls, xi):
125
+ """The :math:`\\mathbf{Q}` matrix used to compute :math:`\\mathcal{J}` in :meth:`~liegroups.SE3.left_jacobian` and :math:`\\mathcal{J}^{-1}` in :meth:`~liegroups.SE3.inv_left_jacobian`.
126
+
127
+ .. math::
128
+ \\mathbf{Q}(\\boldsymbol{\\xi}) =
129
+ \\frac{1}{2}\\boldsymbol{\\rho}^\\wedge &+
130
+ \\left( \\frac{\\phi - \\sin \\phi}{\\phi^3} \\right)
131
+ \\left(
132
+ \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\rho}^\\wedge +
133
+ \\boldsymbol{\\rho}^\\wedge \\boldsymbol{\\phi}^\\wedge +
134
+ \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\rho}^\\wedge \\boldsymbol{\\phi}^\\wedge
135
+ \\right) \\\\ &+
136
+ \\left( \\frac{\\phi^2 + 2 \\cos \\phi - 2}{2 \\phi^4} \\right)
137
+ \\left(
138
+ \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\rho}^\\wedge +
139
+ \\boldsymbol{\\rho}^\\wedge \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\phi}^\\wedge -
140
+ 3 \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\rho}^\\wedge \\boldsymbol{\\phi}^\\wedge
141
+ \\right) \\\\ &+
142
+ \\left( \\frac{2 \\phi - 3 \\sin \\phi + \\phi \\cos \\phi}{2 \\phi^5} \\right)
143
+ \\left(
144
+ \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\rho}^\\wedge \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\phi}^\\wedge +
145
+ \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\phi}^\\wedge \\boldsymbol{\\rho}^\\wedge \\boldsymbol{\\phi}^\\wedge
146
+ \\right)
147
+ """
148
+ if len(xi) != cls.dof:
149
+ raise ValueError("xi must have length {}".format(cls.dof))
150
+
151
+ rho = xi[0:3] # translation part
152
+ phi = xi[3:6] # rotation part
153
+
154
+ rx = cls.RotationType.wedge(rho)
155
+ px = cls.RotationType.wedge(phi)
156
+
157
+ ph = np.linalg.norm(phi)
158
+ ph2 = ph * ph
159
+ ph3 = ph2 * ph
160
+ ph4 = ph3 * ph
161
+ ph5 = ph4 * ph
162
+
163
+ cph = np.cos(ph)
164
+ sph = np.sin(ph)
165
+
166
+ m1 = 0.5
167
+ m2 = (ph - sph) / ph3
168
+ m3 = (0.5 * ph2 + cph - 1.) / ph4
169
+ m4 = (ph - 1.5 * sph + 0.5 * ph * cph) / ph5
170
+
171
+ t1 = rx
172
+ t2 = px.dot(rx) + rx.dot(px) + px.dot(rx).dot(px)
173
+ t3 = px.dot(px).dot(rx) + rx.dot(px).dot(px) - 3. * px.dot(rx).dot(px)
174
+ t4 = px.dot(rx).dot(px).dot(px) + px.dot(px).dot(rx).dot(px)
175
+
176
+ return m1 * t1 + m2 * t2 + m3 * t3 + m4 * t4
177
+
178
+ @classmethod
179
+ def inv_left_jacobian(cls, xi):
180
+ """:math:`SE(3)` inverse left Jacobian.
181
+
182
+ .. math::
183
+ \\mathcal{J}^{-1}(\\boldsymbol{\\xi}) =
184
+ \\begin{bmatrix}
185
+ \\mathbf{J}^{-1} & -\\mathbf{J}^{-1} \\mathbf{Q} \\mathbf{J}^{-1} \\\\
186
+ \\mathbf{0} & \\mathbf{J}^{-1}
187
+ \\end{bmatrix}
188
+
189
+ with :math:`\\mathbf{J}^{-1}` as in :meth:`liegroups.SO3.inv_left_jacobian` and :math:`\\mathbf{Q}` as in :meth:`~liegroups.SE3.left_jacobian_Q_matrix`.
190
+ """
191
+ rho = xi[0:3] # translation part
192
+ phi = xi[3:6] # rotation part
193
+
194
+ # Near |phi|==0, use first order Taylor expansion
195
+ if np.isclose(np.linalg.norm(phi), 0.):
196
+ return np.identity(cls.dof) - 0.5 * cls.curlywedge(xi)
197
+
198
+ so3_inv_jac = cls.RotationType.inv_left_jacobian(phi)
199
+ Q_mat = cls.left_jacobian_Q_matrix(xi)
200
+
201
+ jac = np.zeros([cls.dof, cls.dof])
202
+ jac[0:3, 0:3] = so3_inv_jac
203
+ jac[0:3, 3:6] = -so3_inv_jac.dot(Q_mat).dot(so3_inv_jac)
204
+ jac[3:6, 3:6] = so3_inv_jac
205
+
206
+ return jac
207
+
208
+ @classmethod
209
+ def left_jacobian(cls, xi):
210
+ """:math:`SE(3)` left Jacobian.
211
+
212
+ .. math::
213
+ \\mathcal{J}(\\boldsymbol{\\xi}) =
214
+ \\begin{bmatrix}
215
+ \\mathbf{J} & \\mathbf{Q} \\\\
216
+ \\mathbf{0} & \\mathbf{J}
217
+ \\end{bmatrix}
218
+
219
+ with :math:`\\mathbf{J}` as in :meth:`liegroups.SO3.left_jacobian` and :math:`\\mathbf{Q}` as in :meth:`~liegroups.SE3.left_jacobian_Q_matrix`.
220
+ """
221
+ rho = xi[0:3] # translation part
222
+ phi = xi[3:6] # rotation part
223
+
224
+ # Near |phi|==0, use first order Taylor expansion
225
+ if np.isclose(np.linalg.norm(phi), 0.):
226
+ return np.identity(cls.dof) + 0.5 * cls.curlywedge(xi)
227
+
228
+ so3_jac = cls.RotationType.left_jacobian(phi)
229
+ Q_mat = cls.left_jacobian_Q_matrix(xi)
230
+
231
+ jac = np.zeros([cls.dof, cls.dof])
232
+ jac[0:3, 0:3] = so3_jac
233
+ jac[0:3, 3:6] = Q_mat
234
+ jac[3:6, 3:6] = so3_jac
235
+
236
+ return jac
237
+
238
+ def log(self):
239
+ """Logarithmic map for :math:`SE(3)`, which computes a tangent vector from a transformation:
240
+
241
+ .. math::
242
+ \\boldsymbol{\\xi}(\\mathbf{T}) =
243
+ \\ln(\\mathbf{T})^\\vee =
244
+ \\begin{bmatrix}
245
+ \\mathbf{J} ^ {-1} \\mathbf{r} \\\\
246
+ \\ln(\\boldsymbol{C}) ^\\vee
247
+ \\end{bmatrix}
248
+
249
+ This is the inverse operation to :meth:`~liegroups.SE3.exp`.
250
+ """
251
+ phi = self.RotationType.log(self.rot)
252
+ rho = self.RotationType.inv_left_jacobian(phi).dot(self.trans)
253
+ return np.hstack([rho, phi])
254
+
255
+ @classmethod
256
+ def odot(cls, p, directional=False):
257
+ """:math:`SE(3)` odot operator as defined by Barfoot.
258
+
259
+ This is the Jacobian of a vector
260
+
261
+ .. math::
262
+ \\mathbf{p} =
263
+ \\begin{bmatrix}
264
+ sx \\\\ sy \\\\ sz \\\\ s
265
+ \\end{bmatrix} =
266
+ \\begin{bmatrix}
267
+ \\boldsymbol{\\epsilon} \\\\ \\eta
268
+ \\end{bmatrix}
269
+
270
+ with respect to a perturbation in the underlying parameters of :math:`\\mathbf{T}`.
271
+
272
+ If :math:`\\mathbf{p}` is given in Euclidean coordinates and directional=False, the missing scale value :math:`\\eta` is assumed to be 1 and the Jacobian is 3x6. If directional=True, :math:`\\eta` is assumed to be 0:
273
+
274
+ .. math::
275
+ \\mathbf{p}^\\odot =
276
+ \\begin{bmatrix}
277
+ \\eta \\mathbf{1} & -\\boldsymbol{\\epsilon}^\\wedge
278
+ \\end{bmatrix}
279
+
280
+ If :math:`\\mathbf{p}` is given in Homogeneous coordinates, the Jacobian is 4x6:
281
+
282
+ .. math::
283
+ \\mathbf{p}^\\odot =
284
+ \\begin{bmatrix}
285
+ \\eta \\mathbf{1} & -\\boldsymbol{\\epsilon}^\\wedge \\\\
286
+ \\mathbf{0}^T & \\mathbf{0}^T
287
+ \\end{bmatrix}
288
+ """
289
+ p = np.atleast_2d(p)
290
+ result = np.zeros([p.shape[0], p.shape[1], cls.dof])
291
+
292
+ if p.shape[1] == cls.dim - 1:
293
+ # Assume scale parameter is 1 unless p is a direction
294
+ # ptor, in which case the scale is 0
295
+ if not directional:
296
+ result[:, 0:3, 0:3] = np.eye(3)
297
+
298
+ result[:, 0:3, 3:6] = cls.RotationType.wedge(-p)
299
+
300
+ elif p.shape[1] == cls.dim:
301
+ # Broadcast magic
302
+ result[:, 0:3, 0:3] = p[:, 3][:, None, None] * np.eye(3)
303
+ result[:, 0:3, 3:6] = cls.RotationType.wedge(-p[:, 0:3])
304
+
305
+ else:
306
+ raise ValueError("p must have shape ({},), ({},), (N,{}) or (N,{})".format(
307
+ cls.dim - 1, cls.dim, cls.dim - 1, cls.dim))
308
+
309
+ return np.squeeze(result)
310
+
311
+ @classmethod
312
+ def vee(cls, Xi):
313
+ """:math:`SE(3)` vee operator as defined by Barfoot.
314
+
315
+ .. math::
316
+ \\boldsymbol{\\xi} = \\boldsymbol{\\Xi} ^\\vee
317
+
318
+ This is the inverse operation to :meth:`~liegroups.SE3.wedge`.
319
+ """
320
+ if Xi.ndim < 3:
321
+ Xi = np.expand_dims(Xi, axis=0)
322
+
323
+ if Xi.shape[1:3] != (cls.dim, cls.dim):
324
+ raise ValueError("Xi must have shape ({},{}) or (N,{},{})".format(
325
+ cls.dim, cls.dim, cls.dim, cls.dim))
326
+
327
+ xi = np.empty([Xi.shape[0], cls.dof])
328
+ xi[:, 0:3] = Xi[:, 0:3, 3]
329
+ xi[:, 3:6] = cls.RotationType.vee(Xi[:, 0:3, 0:3])
330
+ return np.squeeze(xi)
331
+
332
+ @classmethod
333
+ def wedge(cls, xi):
334
+ """:math:`SE(3)` wedge operator as defined by Barfoot.
335
+
336
+ .. math::
337
+ \\boldsymbol{\\Xi} =
338
+ \\boldsymbol{\\xi} ^\\wedge =
339
+ \\begin{bmatrix}
340
+ \\boldsymbol{\\phi} ^\\wedge & \\boldsymbol{\\rho} \\\\
341
+ \\mathbf{0} ^ T & 0
342
+ \\end{bmatrix}
343
+
344
+ This is the inverse operation to :meth:`~liegroups.SE2.vee`.
345
+ """
346
+ xi = np.atleast_2d(xi)
347
+ if xi.shape[1] != cls.dof:
348
+ raise ValueError(
349
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
350
+
351
+ Xi = np.zeros([xi.shape[0], cls.dim, cls.dim])
352
+ Xi[:, 0:3, 0:3] = cls.RotationType.wedge(xi[:, 3:6])
353
+ Xi[:, 0:3, 3] = xi[:, 0:3]
354
+ return np.squeeze(Xi)
liegroups/build/lib/liegroups/numpy/so2.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from . import _base
4
+
5
+
6
+ class SO2Matrix(_base.SOMatrixBase):
7
+ """Rotation matrix in :math:`SO(2)` using active (alibi) transformations.
8
+
9
+ .. math::
10
+ SO(2) &= \\left\\{ \\mathbf{C} \\in \\mathbb{R}^{2 \\times 2} ~\\middle|~ \\mathbf{C}\\mathbf{C}^T = \\mathbf{1}, \\det \\mathbf{C} = 1 \\right\\} \\\\
11
+ \\mathfrak{so}(2) &= \\left\\{ \\boldsymbol{\\Phi} = \\phi^\\wedge \\in \\mathbb{R}^{2 \\times 2} ~\\middle|~ \\phi \\in \\mathbb{R} \\right\\}
12
+
13
+ :cvar ~liegroups.SO2.dim: Dimension of the rotation matrix.
14
+ :cvar ~liegroups.SO2.dof: Underlying degrees of freedom (i.e., dimension of the tangent space).
15
+ :ivar mat: Storage for the rotation matrix :math:`\\mathbf{C}`.
16
+ """
17
+ dim = 2
18
+ """Dimension of the transformation matrix."""
19
+ dof = 1
20
+ """Underlying degrees of freedom (i.e., dimension of the tangent space)."""
21
+
22
+ def adjoint(self):
23
+ """Adjoint matrix of the transformation.
24
+
25
+ .. math::
26
+ \\text{Ad}(\\mathbf{C}) = 1
27
+ """
28
+ return 1.
29
+
30
+ @classmethod
31
+ def exp(cls, phi):
32
+ """Exponential map for :math:`SO(2)`, which computes a transformation from a tangent vector:
33
+
34
+ .. math::
35
+ \\mathbf{C}(\\phi) =
36
+ \\exp(\\phi^\\wedge) =
37
+ \\cos \\phi \\mathbf{1} + \\sin \\phi 1^\\wedge =
38
+ \\begin{bmatrix}
39
+ \\cos \\phi & -\\sin \\phi \\\\
40
+ \\sin \\phi & \\cos \\phi
41
+ \\end{bmatrix}
42
+
43
+ This is the inverse operation to :meth:`~liegroups.SO2.log`.
44
+ """
45
+ c = np.cos(phi)
46
+ s = np.sin(phi)
47
+
48
+ return cls(np.array([[c, -s],
49
+ [s, c]]))
50
+
51
+ @classmethod
52
+ def from_angle(cls, angle_in_radians):
53
+ """Form a rotation matrix given an angle in radians.
54
+
55
+ See :meth:`~liegroups.SO2.exp`
56
+ """
57
+ return cls.exp(angle_in_radians)
58
+
59
+ @classmethod
60
+ def inv_left_jacobian(cls, phi):
61
+ """:math:`SO(2)` inverse left Jacobian.
62
+
63
+ .. math::
64
+ \\mathbf{J}^{-1}(\\phi) =
65
+ \\begin{cases}
66
+ \\mathbf{1} - \\frac{1}{2} \\phi^\\wedge, & \\text{if } \\phi \\text{ is small} \\\\
67
+ \\frac{\\phi}{2} \\cot \\frac{\\phi}{2} \\mathbf{1} -
68
+ \\frac{\\phi}{2} 1^\\wedge, & \\text{otherwise}
69
+ \\end{cases}
70
+ """
71
+ # Near phi==0, use first order Taylor expansion
72
+ if np.isclose(phi, 0.):
73
+ return np.identity(cls.dim) - 0.5 * cls.wedge(phi)
74
+
75
+ half_angle = 0.5 * phi
76
+ cot_half_angle = 1. / np.tan(half_angle)
77
+ return half_angle * cot_half_angle * np.identity(cls.dim) - \
78
+ half_angle * cls.wedge(1.)
79
+
80
+ @classmethod
81
+ def left_jacobian(cls, phi):
82
+ """:math:`SO(2)` left Jacobian.
83
+
84
+ .. math::
85
+ \\mathbf{J}(\\phi) =
86
+ \\begin{cases}
87
+ \\mathbf{1} + \\frac{1}{2} \\phi^\\wedge, & \\text{if } \\phi \\text{ is small} \\\\
88
+ \\frac{\\sin \\phi}{\\phi} \\mathbf{1} -
89
+ \\frac{1 - \\cos \\phi}{\\phi} 1^\\wedge, & \\text{otherwise}
90
+ \\end{cases}
91
+ """
92
+ # Near phi==0, use first order Taylor expansion
93
+ if np.isclose(phi, 0.):
94
+ return np.identity(cls.dim) + 0.5 * cls.wedge(phi)
95
+
96
+ s = np.sin(phi)
97
+ c = np.cos(phi)
98
+
99
+ return (s / phi) * np.identity(cls.dim) + \
100
+ ((1 - c) / phi) * cls.wedge(1.)
101
+
102
+ def log(self):
103
+ """Logarithmic map for :math:`SO(2)`, which computes a tangent vector from a transformation:
104
+
105
+ .. math::
106
+ \\phi(\\mathbf{C}) =
107
+ \\ln(\\mathbf{C})^\\vee =
108
+ \\text{atan2}(C_{1,0}, C_{0,0})
109
+
110
+ This is the inverse operation to :meth:`~liegroups.SO2.exp`.
111
+ """
112
+ c = self.mat[0, 0]
113
+ s = self.mat[1, 0]
114
+ return np.arctan2(s, c)
115
+
116
+ def to_angle(self):
117
+ """Recover the rotation angle in radians from the rotation matrix.
118
+
119
+ See :meth:`~liegroups.SO2.log`
120
+ """
121
+ return self.log()
122
+
123
+ @classmethod
124
+ def vee(cls, Phi):
125
+ """:math:`SO(2)` vee operator as defined by Barfoot.
126
+
127
+ .. math::
128
+ \\phi = \\boldsymbol{\\Phi}^\\vee
129
+
130
+ This is the inverse operation to :meth:`~liegroups.SO2.wedge`.
131
+ """
132
+ if Phi.ndim < 3:
133
+ Phi = np.expand_dims(Phi, axis=0)
134
+
135
+ if Phi.shape[1:3] != (cls.dim, cls.dim):
136
+ raise ValueError(
137
+ "Phi must have shape ({},{}) or (N,{},{})".format(
138
+ cls.dim, cls.dim, cls.dim, cls.dim))
139
+
140
+ return np.squeeze(Phi[:, 1, 0])
141
+
142
+ @classmethod
143
+ def wedge(cls, phi):
144
+ """:math:`SO(2)` wedge operator as defined by Barfoot.
145
+
146
+ .. math::
147
+ \\boldsymbol{\\Phi} =
148
+ \\phi^\\wedge =
149
+ \\begin{bmatrix}
150
+ 0 & -\\phi \\\\
151
+ \\phi & 0
152
+ \\end{bmatrix}
153
+
154
+ This is the inverse operation to :meth:`~liegroups.SO2.vee`.
155
+ """
156
+ phi = np.atleast_1d(phi)
157
+
158
+ Phi = np.zeros([len(phi), cls.dim, cls.dim])
159
+ Phi[:, 0, 1] = -phi
160
+ Phi[:, 1, 0] = phi
161
+ return np.squeeze(Phi)
liegroups/build/lib/liegroups/numpy/so3.py ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from . import _base
4
+
5
+
6
+ class SO3Matrix(_base.SOMatrixBase):
7
+ """Rotation matrix in :math:`SO(3)` using active (alibi) transformations.
8
+
9
+ .. math::
10
+ SO(3) &= \\left\\{ \\mathbf{C} \\in \\mathbb{R}^{3 \\times 3} ~\\middle|~ \\mathbf{C}\\mathbf{C}^T = \\mathbf{1}, \\det \\mathbf{C} = 1 \\right\\} \\\\
11
+ \\mathfrak{so}(3) &= \\left\\{ \\boldsymbol{\\Phi} = \\boldsymbol{\\phi}^\\wedge \\in \\mathbb{R}^{3 \\times 3} ~\\middle|~ \\boldsymbol{\\phi} = \\phi \\mathbf{a} \\in \\mathbb{R}^3, \\phi = \\Vert \\boldsymbol{\\phi} \\Vert \\right\\}
12
+
13
+ :cvar ~liegroups.SO3.dim: Dimension of the rotation matrix.
14
+ :cvar ~liegroups.SO3.dof: Underlying degrees of freedom (i.e., dimension of the tangent space).
15
+ :ivar mat: Storage for the rotation matrix :math:`\\mathbf{C}`.
16
+ """
17
+ dim = 3
18
+ """Dimension of the transformation matrix."""
19
+ dof = 3
20
+ """Underlying degrees of freedom (i.e., dimension of the tangent space)."""
21
+
22
+ def adjoint(self):
23
+ """Adjoint matrix of the transformation.
24
+
25
+ .. math::
26
+ \\text{Ad}(\\mathbf{C}) = \\mathbf{C}
27
+ \\in \\mathbb{R}^{3 \\times 3}
28
+ """
29
+ return self.mat
30
+
31
+ @classmethod
32
+ def exp(cls, phi):
33
+ """Exponential map for :math:`SO(3)`, which computes a transformation from a tangent vector:
34
+
35
+ .. math::
36
+ \\mathbf{C}(\\boldsymbol{\\phi}) =
37
+ \\exp(\\boldsymbol{\\phi}^\\wedge) =
38
+ \\begin{cases}
39
+ \\mathbf{1} + \\boldsymbol{\\phi}^\\wedge, & \\text{if } \\phi \\text{ is small} \\\\
40
+ \\cos \\phi \\mathbf{1} +
41
+ (1 - \\cos \\phi) \\mathbf{a}\\mathbf{a}^T +
42
+ \\sin \\phi \\mathbf{a}^\\wedge, & \\text{otherwise}
43
+ \\end{cases}
44
+
45
+ This is the inverse operation to :meth:`~liegroups.SO3.log`.
46
+ """
47
+ if len(phi) != cls.dof:
48
+ raise ValueError("phi must have length 3")
49
+
50
+ angle = np.linalg.norm(phi)
51
+
52
+ # Near phi==0, use first order Taylor expansion
53
+ if np.isclose(angle, 0.):
54
+ return cls(np.identity(cls.dim) + cls.wedge(phi))
55
+
56
+ axis = phi / angle
57
+ s = np.sin(angle)
58
+ c = np.cos(angle)
59
+
60
+ return cls(c * np.identity(cls.dim) +
61
+ (1 - c) * np.outer(axis, axis) +
62
+ s * cls.wedge(axis))
63
+
64
+ @classmethod
65
+ def from_quaternion(cls, quat, ordering='wxyz'):
66
+ """Form a rotation matrix from a unit length quaternion.
67
+
68
+ Valid orderings are 'xyzw' and 'wxyz'.
69
+
70
+ .. math::
71
+ \\mathbf{C} =
72
+ \\begin{bmatrix}
73
+ 1 - 2 (y^2 + z^2) & 2 (xy - wz) & 2 (wy + xz) \\\\
74
+ 2 (wz + xy) & 1 - 2 (x^2 + z^2) & 2 (yz - wx) \\\\
75
+ 2 (xz - wy) & 2 (wx + yz) & 1 - 2 (x^2 + y^2)
76
+ \\end{bmatrix}
77
+ """
78
+ if not np.isclose(np.linalg.norm(quat), 1.):
79
+ raise ValueError("Quaternion must be unit length")
80
+
81
+ if ordering is 'xyzw':
82
+ qx, qy, qz, qw = quat
83
+ elif ordering is 'wxyz':
84
+ qw, qx, qy, qz = quat
85
+ else:
86
+ raise ValueError(
87
+ "Valid orderings are 'xyzw' and 'wxyz'. Got '{}'.".format(ordering))
88
+
89
+ # Form the matrix
90
+ qw2 = qw * qw
91
+ qx2 = qx * qx
92
+ qy2 = qy * qy
93
+ qz2 = qz * qz
94
+
95
+ R00 = 1. - 2. * (qy2 + qz2)
96
+ R01 = 2. * (qx * qy - qw * qz)
97
+ R02 = 2. * (qw * qy + qx * qz)
98
+
99
+ R10 = 2. * (qw * qz + qx * qy)
100
+ R11 = 1. - 2. * (qx2 + qz2)
101
+ R12 = 2. * (qy * qz - qw * qx)
102
+
103
+ R20 = 2. * (qx * qz - qw * qy)
104
+ R21 = 2. * (qw * qx + qy * qz)
105
+ R22 = 1. - 2. * (qx2 + qy2)
106
+
107
+ return cls(np.array([[R00, R01, R02],
108
+ [R10, R11, R12],
109
+ [R20, R21, R22]]))
110
+
111
+ @classmethod
112
+ def from_rpy(cls, roll, pitch, yaw):
113
+ """Form a rotation matrix from RPY Euler angles :math:`(\\alpha, \\beta, \\gamma)`.
114
+
115
+ .. math::
116
+ \\mathbf{C} = \\mathbf{C}_z(\\gamma) \\mathbf{C}_y(\\beta) \\mathbf{C}_x(\\alpha)
117
+ """
118
+ return cls.rotz(yaw).dot(cls.roty(pitch).dot(cls.rotx(roll)))
119
+
120
+ @classmethod
121
+ def inv_left_jacobian(cls, phi):
122
+ """:math:`SO(3)` inverse left Jacobian.
123
+
124
+ .. math::
125
+ \\mathbf{J}^{-1}(\\boldsymbol{\\phi}) =
126
+ \\begin{cases}
127
+ \\mathbf{1} - \\frac{1}{2} \\boldsymbol{\\phi}^\\wedge, & \\text{if } \\phi \\text{ is small} \\\\
128
+ \\frac{\\phi}{2} \\cot \\frac{\\phi}{2} \\mathbf{1} +
129
+ \\left( 1 - \\frac{\\phi}{2} \\cot \\frac{\\phi}{2} \\right) \\mathbf{a}\\mathbf{a}^T -
130
+ \\frac{\\phi}{2} \\mathbf{a}^\\wedge, & \\text{otherwise}
131
+ \\end{cases}
132
+ """
133
+ if len(phi) != cls.dof:
134
+ raise ValueError("phi must have length 3")
135
+
136
+ angle = np.linalg.norm(phi)
137
+
138
+ # Near phi==0, use first order Taylor expansion
139
+ if np.isclose(angle, 0.):
140
+ return np.identity(cls.dof) - 0.5 * cls.wedge(phi)
141
+
142
+ axis = phi / angle
143
+ half_angle = 0.5 * angle
144
+ cot_half_angle = 1. / np.tan(half_angle)
145
+
146
+ return half_angle * cot_half_angle * np.identity(cls.dof) + \
147
+ (1 - half_angle * cot_half_angle) * np.outer(axis, axis) - \
148
+ half_angle * cls.wedge(axis)
149
+
150
+ @classmethod
151
+ def left_jacobian(cls, phi):
152
+ """:math:`SO(3)` left Jacobian.
153
+
154
+ .. math::
155
+ \\mathbf{J}(\\boldsymbol{\\phi}) =
156
+ \\begin{cases}
157
+ \\mathbf{1} + \\frac{1}{2} \\boldsymbol{\\phi}^\\wedge, & \\text{if } \\phi \\text{ is small} \\\\
158
+ \\frac{\\sin \\phi}{\\phi} \\mathbf{1} +
159
+ \\left(1 - \\frac{\\sin \\phi}{\\phi} \\right) \\mathbf{a}\\mathbf{a}^T +
160
+ \\frac{1 - \\cos \\phi}{\\phi} \\mathbf{a}^\\wedge, & \\text{otherwise}
161
+ \\end{cases}
162
+ """
163
+ if len(phi) != cls.dof:
164
+ raise ValueError("phi must have length 3")
165
+
166
+ angle = np.linalg.norm(phi)
167
+
168
+ # Near |phi|==0, use first order Taylor expansion
169
+ if np.isclose(angle, 0.):
170
+ return np.identity(cls.dof) + 0.5 * cls.wedge(phi)
171
+
172
+ axis = phi / angle
173
+ s = np.sin(angle)
174
+ c = np.cos(angle)
175
+
176
+ return (s / angle) * np.identity(cls.dof) + \
177
+ (1 - s / angle) * np.outer(axis, axis) + \
178
+ ((1 - c) / angle) * cls.wedge(axis)
179
+
180
+ def log(self):
181
+ """Logarithmic map for :math:`SO(3)`, which computes a tangent vector from a transformation:
182
+
183
+ .. math::
184
+ \\phi &= \\frac{1}{2} \\left( \\mathrm{Tr}(\\mathbf{C}) - 1 \\right) \\\\
185
+ \\boldsymbol{\\phi}(\\mathbf{C}) &=
186
+ \\ln(\\mathbf{C})^\\vee =
187
+ \\begin{cases}
188
+ \\mathbf{1} - \\boldsymbol{\\phi}^\\wedge, & \\text{if } \\phi \\text{ is small} \\\\
189
+ \\left( \\frac{1}{2} \\frac{\\phi}{\\sin \\phi} \\left( \\mathbf{C} - \\mathbf{C}^T \\right) \\right)^\\vee, & \\text{otherwise}
190
+ \\end{cases}
191
+
192
+ This is the inverse operation to :meth:`~liegroups.SO3.log`.
193
+ """
194
+ # The cosine of the rotation angle is related to the trace of C
195
+ cos_angle = 0.5 * np.trace(self.mat) - 0.5
196
+ # Clip cos(angle) to its proper domain to avoid NaNs from rounding errors
197
+ cos_angle = np.clip(cos_angle, -1., 1.)
198
+ angle = np.arccos(cos_angle)
199
+
200
+ # If angle is close to zero, use first-order Taylor expansion
201
+ if np.isclose(angle, 0.):
202
+ return self.vee(self.mat - np.identity(3))
203
+
204
+ # Otherwise take the matrix logarithm and return the rotation vector
205
+ return self.vee((0.5 * angle / np.sin(angle)) * (self.mat - self.mat.T))
206
+
207
+ @classmethod
208
+ def rotx(cls, angle_in_radians):
209
+ """Form a rotation matrix given an angle in rad about the x-axis.
210
+
211
+ .. math::
212
+ \\mathbf{C}_x(\\phi) =
213
+ \\begin{bmatrix}
214
+ 1 & 0 & 0 \\\\
215
+ 0 & \\cos \\phi & -\\sin \\phi \\\\
216
+ 0 & \\sin \\phi & \\cos \\phi
217
+ \\end{bmatrix}
218
+ """
219
+ c = np.cos(angle_in_radians)
220
+ s = np.sin(angle_in_radians)
221
+
222
+ return cls(np.array([[1., 0., 0.],
223
+ [0., c, -s],
224
+ [0., s, c]]))
225
+
226
+ @classmethod
227
+ def roty(cls, angle_in_radians):
228
+ """Form a rotation matrix given an angle in rad about the y-axis.
229
+
230
+ .. math::
231
+ \\mathbf{C}_y(\\phi) =
232
+ \\begin{bmatrix}
233
+ \\cos \\phi & 0 & \\sin \\phi \\\\
234
+ 0 & 1 & 0 \\\\
235
+ \\sin \\phi & 0 & \\cos \\phi
236
+ \\end{bmatrix}
237
+ """
238
+ c = np.cos(angle_in_radians)
239
+ s = np.sin(angle_in_radians)
240
+
241
+ return cls(np.array([[c, 0., s],
242
+ [0., 1., 0.],
243
+ [-s, 0., c]]))
244
+
245
+ @classmethod
246
+ def rotz(cls, angle_in_radians):
247
+ """Form a rotation matrix given an angle in rad about the z-axis.
248
+
249
+ .. math::
250
+ \\mathbf{C}_z(\\phi) =
251
+ \\begin{bmatrix}
252
+ \\cos \\phi & -\\sin \\phi & 0 \\\\
253
+ \\sin \\phi & \\cos \\phi & 0 \\\\
254
+ 0 & 0 & 1
255
+ \\end{bmatrix}
256
+ """
257
+ c = np.cos(angle_in_radians)
258
+ s = np.sin(angle_in_radians)
259
+
260
+ return cls(np.array([[c, -s, 0.],
261
+ [s, c, 0.],
262
+ [0., 0., 1.]]))
263
+
264
+ def to_quaternion(self, ordering='wxyz'):
265
+ """Convert a rotation matrix to a unit length quaternion.
266
+
267
+ Valid orderings are 'xyzw' and 'wxyz'.
268
+ """
269
+ R = self.mat
270
+ qw = 0.5 * np.sqrt(1. + R[0, 0] + R[1, 1] + R[2, 2])
271
+
272
+ if np.isclose(qw, 0.):
273
+ if R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
274
+ d = 2. * np.sqrt(1. + R[0, 0] - R[1, 1] - R[2, 2])
275
+ qw = (R[2, 1] - R[1, 2]) / d
276
+ qx = 0.25 * d
277
+ qy = (R[1, 0] + R[0, 1]) / d
278
+ qz = (R[0, 2] + R[2, 0]) / d
279
+ elif R[1, 1] > R[2, 2]:
280
+ d = 2. * np.sqrt(1. + R[1, 1] - R[0, 0] - R[2, 2])
281
+ qw = (R[0, 2] - R[2, 0]) / d
282
+ qx = (R[1, 0] + R[0, 1]) / d
283
+ qy = 0.25 * d
284
+ qz = (R[2, 1] + R[1, 2]) / d
285
+ else:
286
+ d = 2. * np.sqrt(1. + R[2, 2] - R[0, 0] - R[1, 1])
287
+ qw = (R[1, 0] - R[0, 1]) / d
288
+ qx = (R[0, 2] + R[2, 0]) / d
289
+ qy = (R[2, 1] + R[1, 2]) / d
290
+ qz = 0.25 * d
291
+ else:
292
+ d = 4. * qw
293
+ qx = (R[2, 1] - R[1, 2]) / d
294
+ qy = (R[0, 2] - R[2, 0]) / d
295
+ qz = (R[1, 0] - R[0, 1]) / d
296
+
297
+ # Check ordering last
298
+ if ordering is 'xyzw':
299
+ quat = np.array([qx, qy, qz, qw])
300
+ elif ordering is 'wxyz':
301
+ quat = np.array([qw, qx, qy, qz])
302
+ else:
303
+ raise ValueError(
304
+ "Valid orderings are 'xyzw' and 'wxyz'. Got '{}'.".format(ordering))
305
+
306
+ return quat
307
+
308
+ def to_rpy(self):
309
+ """Convert a rotation matrix to RPY Euler angles :math:`(\\alpha, \\beta, \\gamma)`."""
310
+ pitch = np.arctan2(-self.mat[2, 0],
311
+ np.sqrt(self.mat[0, 0]**2 + self.mat[1, 0]**2))
312
+
313
+ if np.isclose(pitch, np.pi / 2.):
314
+ yaw = 0.
315
+ roll = np.arctan2(self.mat[0, 1], self.mat[1, 1])
316
+ elif np.isclose(pitch, -np.pi / 2.):
317
+ yaw = 0.
318
+ roll = -np.arctan2(self.mat[0, 1], self.mat[1, 1])
319
+ else:
320
+ sec_pitch = 1. / np.cos(pitch)
321
+ yaw = np.arctan2(self.mat[1, 0] * sec_pitch,
322
+ self.mat[0, 0] * sec_pitch)
323
+ roll = np.arctan2(self.mat[2, 1] * sec_pitch,
324
+ self.mat[2, 2] * sec_pitch)
325
+
326
+ return roll, pitch, yaw
327
+
328
+ @classmethod
329
+ def vee(cls, Phi):
330
+ """:math:`SO(3)` vee operator as defined by Barfoot.
331
+
332
+ .. math::
333
+ \\phi = \\boldsymbol{\\Phi}^\\vee
334
+
335
+ This is the inverse operation to :meth:`~liegroups.SO3.wedge`.
336
+ """
337
+ if Phi.ndim < 3:
338
+ Phi = np.expand_dims(Phi, axis=0)
339
+
340
+ if Phi.shape[1:3] != (cls.dim, cls.dim):
341
+ raise ValueError("Phi must have shape ({},{}) or (N,{},{})".format(
342
+ cls.dim, cls.dim, cls.dim, cls.dim))
343
+
344
+ phi = np.empty([Phi.shape[0], cls.dim])
345
+ phi[:, 0] = Phi[:, 2, 1]
346
+ phi[:, 1] = Phi[:, 0, 2]
347
+ phi[:, 2] = Phi[:, 1, 0]
348
+ return np.squeeze(phi)
349
+
350
+ @classmethod
351
+ def wedge(cls, phi):
352
+ """:math:`SO(3)` wedge operator as defined by Barfoot.
353
+
354
+ .. math::
355
+ \\boldsymbol{\\Phi} =
356
+ \\boldsymbol{\\phi}^\\wedge =
357
+ \\begin{bmatrix}
358
+ 0 & -\\phi_3 & \\phi_2 \\\\
359
+ \\phi_3 & 0 & -\\phi_1 \\\\
360
+ -\\phi_2 & \\phi_1 & 0
361
+ \\end{bmatrix}
362
+
363
+ This is the inverse operation to :meth:`~liegroups.SO3.vee`.
364
+ """
365
+ phi = np.atleast_2d(phi)
366
+ if phi.shape[1] != cls.dof:
367
+ raise ValueError(
368
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
369
+
370
+ Phi = np.zeros([phi.shape[0], cls.dim, cls.dim])
371
+ Phi[:, 0, 1] = -phi[:, 2]
372
+ Phi[:, 1, 0] = phi[:, 2]
373
+ Phi[:, 0, 2] = phi[:, 1]
374
+ Phi[:, 2, 0] = -phi[:, 1]
375
+ Phi[:, 1, 2] = -phi[:, 0]
376
+ Phi[:, 2, 1] = phi[:, 0]
377
+ return np.squeeze(Phi)
378
+
379
+
380
+ class SO3Quaternion(_base.VectorLieGroupBase):
381
+ """Rotation in SO(3) using unit-length quaternions (wxyz ordering)."""
382
+
383
+ dim = 4
384
+ dof = 3
385
+
386
+ def from_array(self, arr, ordering='wxyz'):
387
+ if ordering is 'xyzw':
388
+ self.data = arr[[3, 0, 1, 2]]
389
+ elif ordering is 'wxyz':
390
+ self.data = arr
391
+ else:
392
+ raise ValueError(
393
+ "Valid orderings are 'xyzw' and 'wxyz'. Got '{}'.".format(ordering))
394
+
395
+ def dot(self, other):
396
+ """Multiply another rotation or one or more vectors on the left.
397
+ """
398
+ if isinstance(other, self.__class__):
399
+ # Compound with another rotation
400
+ pv = p[1:]
401
+ qv = q[1:]
402
+
403
+ r = np.hstack([p[0]*q[0] - np.dot(pv, qv),
404
+ p[0]*qv + q[0]*pv + np.dot(skew(pv), qv)])
405
+ return 0
406
+ else:
407
+ other = np.atleast_2d(other)
408
+
409
+ # Transform one or more 2-vectors or fail
410
+ if other.shape[1] == self.dim:
411
+ return 0
412
+ else:
413
+ raise ValueError(
414
+ "Vector must have shape ({},) or (N,{})".format(self.dim, self.dim))
415
+
416
+ @classmethod
417
+ def identity(cls):
418
+ """Return the identity rotation."""
419
+ return cls(np.array([1, 0, 0, 0]))
420
+
421
+ def inv(self):
422
+ """Return the inverse rotation:
423
+
424
+ .. math::
425
+ \\mathbf{C}^{-1} = \\mathbf{C}^T
426
+ """
427
+ inv = self.conjugate()
428
+ inv.data = inv.data / np.dot(inv.data, inv.data)
429
+
430
+ return inv
liegroups/build/lib/liegroups/torch/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """PyTorch implementations of Special Euclidean and Special Orthogonal Lie groups."""
2
+
3
+ from .so2 import SO2Matrix as SO2
4
+ from .se2 import SE2Matrix as SE2
5
+ from .so3 import SO3Matrix as SO3
6
+ from .se3 import SE3Matrix as SE3
7
+
8
+ __author__ = "Lee Clement"
9
+ __email__ = "lee.clement@robotics.utias.utoronto.ca"
liegroups/build/lib/liegroups/torch/_base.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np # for matrix determinant and SVD
3
+
4
+ from .. import _base
5
+ from . import utils
6
+
7
+
8
+ class SOMatrixBase(_base.SOMatrixBase):
9
+ """Implementation of methods common to SO(N) matrix lie groups using PyTorch"""
10
+
11
+ def cpu(self):
12
+ """Return a copy with the underlying tensor on the CPU."""
13
+ return self.__class__(self.mat.cpu())
14
+
15
+ def cuda(self, device=None, non_blocking=False):
16
+ """Return a copy with the underlying tensor on the GPU."""
17
+ return self.__class__(self.mat.cuda(device=device, non_blocking=non_blocking))
18
+
19
+ def dot(self, other):
20
+ if isinstance(other, self.__class__):
21
+ # Compound with another rotation
22
+ return self.__class__(torch.matmul(self.mat, other.mat))
23
+ else:
24
+ if other.dim() < 2:
25
+ other = other.unsqueeze(dim=0) # vector --> matrix
26
+ if other.dim() < 3:
27
+ other = other.unsqueeze(dim=0) # matrix --> batch
28
+
29
+ if self.mat.dim() < 3:
30
+ mat = self.mat.unsqueeze(dim=0).expand(
31
+ other.shape[0], self.dim, self.dim) # matrix --> batch
32
+ else:
33
+ mat = self.mat
34
+ if other.shape[0] == 1:
35
+ other = other.expand(
36
+ mat.shape[0], other.shape[1], other.shape[2])
37
+
38
+ # Transform one or more vectors or fail
39
+ if other.shape[0] != mat.shape[0]:
40
+ raise ValueError("Expected vector-batch batch size of {}, got {}".format(
41
+ mat.shape[0], other.shape[0]))
42
+
43
+ if other.shape[2] == self.dim:
44
+ return torch.bmm(mat, other.transpose(2, 1)).transpose_(2, 1).squeeze_()
45
+ else:
46
+ raise ValueError(
47
+ "Vector or vector-batch must have shape ({},), (N,{}), or ({},N,{})".format(self.dim, self.dim, mat.shape[0], self.dim))
48
+
49
+ @classmethod
50
+ def from_matrix(cls, mat, normalize=False):
51
+
52
+ mat_is_valid = cls.is_valid_matrix(mat)
53
+
54
+ if mat_is_valid.all() or normalize:
55
+ result = cls(mat)
56
+
57
+ if normalize:
58
+ result.normalize(inds=mat_is_valid.logical_not().nonzero(as_tuple=False))
59
+
60
+ return result
61
+ else:
62
+ raise ValueError(
63
+ "Invalid rotation matrix. Use normalize=True to handle rounding errors.")
64
+
65
+ @classmethod
66
+ def from_numpy(cls, other, pin_memory=False):
67
+ """Create a torch-based copy of a numpy-based rotation."""
68
+ mat = torch.Tensor(other.mat)
69
+ if pin_memory:
70
+ mat = mat.pin_memory()
71
+
72
+ return cls(mat)
73
+
74
+ @classmethod
75
+ def identity(cls, batch_size=1, copy=False):
76
+ if copy:
77
+ mat = torch.eye(cls.dim).repeat(batch_size, 1, 1)
78
+ else:
79
+ mat = torch.eye(cls.dim).expand(
80
+ batch_size, cls.dim, cls.dim).squeeze()
81
+ return cls(mat)
82
+
83
+ def inv(self):
84
+ if self.mat.dim() < 3:
85
+ return self.__class__(self.mat.transpose(1, 0))
86
+ else:
87
+ return self.__class__(self.mat.transpose(2, 1))
88
+
89
+ def is_cuda(self):
90
+ """Returns true if the underlying tensor is a CUDA tensor"""
91
+ return self.mat.is_cuda
92
+
93
+ def is_pinned(self):
94
+ """Returns true if the underlying tensor resides in pinned memory"""
95
+ return self.mat.is_pinned()
96
+
97
+ @classmethod
98
+ def is_valid_matrix(cls, mat):
99
+ if mat.dim() < 3:
100
+ mat = mat.unsqueeze(dim=0)
101
+
102
+ # Check the shape
103
+ if mat.is_cuda:
104
+ shape_check = torch.cuda.BoolTensor(mat.shape[0]).fill_(False)
105
+ else:
106
+ shape_check = torch.BoolTensor(mat.shape[0]).fill_(False)
107
+
108
+ if mat.shape[1:3] != (cls.dim, cls.dim):
109
+ return shape_check
110
+ else:
111
+ shape_check.fill_(True)
112
+
113
+ # Determinants of each matrix in the batch should be 1
114
+ det_check = utils.isclose(mat.__class__(
115
+ np.linalg.det(mat.detach().cpu().numpy())), 1.)
116
+
117
+ # The transpose of each matrix in the batch should be its inverse
118
+ inv_check = utils.isclose(mat.transpose(2, 1).bmm(mat),
119
+ torch.eye(cls.dim, dtype=mat.dtype)).sum(dim=1).sum(dim=1) \
120
+ == cls.dim * cls.dim
121
+
122
+ return shape_check & det_check & inv_check
123
+
124
+ def _normalize_one(self, mat):
125
+ # U, S, V = torch.svd(A) returns the singular value
126
+ # decomposition of a real matrix A of size (n x m) such that A=USV′.
127
+ # Irrespective of the original strides, the returned matrix U will
128
+ # be transposed, i.e. with strides (1, n) instead of (n, 1).
129
+
130
+ # pytorch has native SVD function but not determinant...
131
+ # U, _, V = mat.squeeze().svd()
132
+ # S = torch.eye(self.dim)
133
+ # if U.is_cuda:
134
+ # S = S.cuda()
135
+ # S[self.dim - 1, self.dim - 1] = float(np.linalg.det(U.cpu().numpy()) *
136
+ # np.linalg.det(V.cpu().numpy()))
137
+ # mat_normalized = U.mm(S).mm(V.t_())
138
+
139
+ # pytorch SVD seems to be inaccurate, so just move to numpy immediately
140
+ mat_cpu = mat.detach().cpu().numpy().squeeze()
141
+ U, _, V = np.linalg.svd(mat_cpu, full_matrices=False)
142
+ S = np.eye(self.dim)
143
+ S[self.dim - 1, self.dim - 1] = np.linalg.det(U) * np.linalg.det(V)
144
+
145
+ mat_normalized = mat.__class__(U.dot(S).dot(V))
146
+
147
+ mat.copy_(mat_normalized)
148
+ return mat
149
+
150
+ def normalize(self, inds=None):
151
+ if self.mat.dim() < 3:
152
+ self._normalize_one(self.mat)
153
+ else:
154
+ if inds is None:
155
+ inds = range(self.mat.shape[0])
156
+
157
+ for batch_ind in inds:
158
+ # Slicing is a copy operation?
159
+ self.mat[batch_ind] = self._normalize_one(self.mat[batch_ind])
160
+
161
+ def pin_memory(self):
162
+ """Return a copy with the underlying tensor in pinned (page-locked) memory. Makes host-to-GPU copies faster.
163
+
164
+ See: http://pytorch.org/docs/master/notes/cuda.html?highlight=pinned
165
+ """
166
+ return self.__class__(self.mat.pin_memory())
167
+
168
+
169
+ class SEMatrixBase(_base.SEMatrixBase):
170
+ """Implementation of methods common to SE(N) matrix lie groups using PyTorch"""
171
+
172
+ def __init__(self, rot, trans):
173
+ super(SEMatrixBase, self).__init__(rot, trans)
174
+
175
+ def as_matrix(self):
176
+ R = self.rot.as_matrix()
177
+ if R.dim() < 3:
178
+ R = R.unsqueeze(dim=0)
179
+
180
+ if self.trans.dim() < 2:
181
+ t = self.trans.unsqueeze(dim=0)
182
+ else:
183
+ t = self.trans
184
+
185
+ t = t.unsqueeze(dim=2) # N x self.dim-1 x 1
186
+
187
+ bottom_row = self.trans.new_zeros(self.dim)
188
+ bottom_row[-1] = 1.
189
+ bottom_row = bottom_row.unsqueeze_(dim=0).unsqueeze_(
190
+ dim=1).expand(R.shape[0], 1, self.dim)
191
+
192
+ return torch.cat([torch.cat([R, t], dim=2),
193
+ bottom_row], dim=1).squeeze_()
194
+
195
+ def cpu(self):
196
+ """Return a copy with the underlying tensors on the CPU."""
197
+ return self.__class__(self.rot.cpu(), self.trans.cpu())
198
+
199
+ def cuda(self, device=None, non_blocking=False):
200
+ """Return a copy with the underlying tensors on the GPU."""
201
+ return self.__class__(self.rot.cuda(device=device, non_blocking=non_blocking),
202
+ self.trans.cuda(device=device, non_blocking=non_blocking))
203
+
204
+ def dot(self, other):
205
+ if isinstance(other, self.__class__):
206
+ if other.trans.dim() == 2:
207
+ # vectorbatch --> matrixbatch (NxD --> Nx1xD)
208
+ other_trans = other.trans.unsqueeze(dim=1)
209
+ else:
210
+ # vector --> matrix (D --> 1xD)
211
+ other_trans = other.trans.unsqueeze(dim=0)
212
+
213
+ # Compound with another transformation
214
+ return self.__class__(self.rot.dot(other.rot),
215
+ self.rot.dot(other_trans) + self.trans)
216
+ else:
217
+ if other.dim() < 2:
218
+ other = other.unsqueeze(dim=0) # vector --> matrix
219
+ if other.dim() < 3:
220
+ other = other.unsqueeze(dim=0) # matrix --> batch
221
+
222
+ # Got euclidean coordinates
223
+ if other.shape[2] == self.dim - 1:
224
+ rot = self.rot.as_matrix()
225
+ trans = self.trans
226
+
227
+ if trans.dim() < 2:
228
+ trans = trans.unsqueeze(dim=0) # vector --> vectorbatch
229
+ if trans.dim() < 3:
230
+ # vectorbatch --> matrixbatch
231
+ trans = trans.unsqueeze(dim=1)
232
+
233
+ if rot.dim() < 3:
234
+ # matrix --> batch
235
+ rot = rot.unsqueeze(dim=0).expand(
236
+ other.shape[0], rot.shape[0], rot.shape[1])
237
+ # matrix --> batch
238
+ trans = trans.expand(
239
+ other.shape[0], trans.shape[1], trans.shape[2])
240
+ elif other.shape[0] == 1:
241
+ other = other.expand(
242
+ rot.shape[0], other.shape[1], other.shape[2])
243
+
244
+ # Transform one or more vectors or fail
245
+ if other.shape[0] != rot.shape[0]:
246
+ raise ValueError(
247
+ "Expected vector-batch batch size of {}, got {}".format(rot.shape[0], other.shape[0]))
248
+
249
+ # rot * other + trans
250
+ return torch.baddbmm(trans.transpose(2, 1), rot,
251
+ other.transpose(2, 1)
252
+ ).transpose(2, 1).squeeze_()
253
+
254
+ # Got homogeneous coordinates
255
+ elif other.shape[2] == self.dim:
256
+ mat = self.as_matrix()
257
+
258
+ if mat.dim() < 3:
259
+ mat = mat.unsqueeze(dim=0).expand(
260
+ other.shape[0], self.dim, self.dim) # matrix --> batch
261
+ elif other.shape[0] == 1:
262
+ other = other.expand(
263
+ mat.shape[0], other.shape[1], other.shape[2])
264
+
265
+ if other.shape[0] != mat.shape[0]:
266
+ raise ValueError(
267
+ "Expected vector-batch batch size of {}, got {}".format(mat.shape[0], other.shape[0]))
268
+
269
+ return torch.bmm(mat, other.transpose(2, 1)
270
+ ).transpose(2, 1).squeeze_()
271
+
272
+ # Got wrong dimension
273
+ else:
274
+ if self.trans.dim() < 2:
275
+ batch_size = 1
276
+ else:
277
+ batch_size = self.trans.shape[0]
278
+
279
+ raise ValueError(
280
+ "Vector or vector-batch must have shape ({},), ({},), (N,{}), (N,{}), ({},N,{}), or ({},N,{})".format(self.dim - 1, self.dim, self.dim - 1, self.dim, batch_size, self.dim - 1, batch_size, self.dim))
281
+
282
+ @classmethod
283
+ def from_matrix(cls, mat, normalize=False):
284
+ if mat.dim() < 3:
285
+ mat = mat.unsqueeze(dim=0)
286
+
287
+ mat_is_valid = cls.is_valid_matrix(mat)
288
+
289
+ if mat_is_valid.all() or normalize:
290
+ rot = mat[:, 0:cls.dim - 1, 0:cls.dim - 1].squeeze()
291
+ trans = mat[:, 0:cls.dim - 1, cls.dim - 1].squeeze()
292
+ result = cls(cls.RotationType(rot), trans)
293
+
294
+ if normalize:
295
+ result.normalize(inds=mat_is_valid.logical_not().nonzero(as_tuple=False))
296
+
297
+ return result
298
+ else:
299
+ raise ValueError(
300
+ "Invalid transformation matrix. Use normalize=True to handle rounding errors.")
301
+
302
+ @classmethod
303
+ def from_numpy(cls, other, pin_memory=False):
304
+ """Create a torch-based copy of a numpy-based transformation."""
305
+ rot = cls.RotationType.from_numpy(other.rot, pin_memory)
306
+
307
+ trans = torch.Tensor(other.trans)
308
+ if pin_memory:
309
+ trans = torch.Tensor(other.trans).pin_memory
310
+
311
+ return cls(rot, trans)
312
+
313
+ @classmethod
314
+ def identity(cls, batch_size=1, copy=False):
315
+ if copy:
316
+ mat = torch.eye(cls.dim).repeat(batch_size, 1, 1)
317
+ else:
318
+ mat = torch.eye(cls.dim).expand(batch_size, cls.dim, cls.dim)
319
+
320
+ return cls.from_matrix(mat.squeeze_())
321
+
322
+ def inv(self):
323
+ if self.trans.dim() == 2:
324
+ # vectorbatch --> matrixbatch (NxD --> Nx1xD)
325
+ trans = self.trans.unsqueeze(dim=1)
326
+ else:
327
+ # vector --> matrix (D --> 1xD)
328
+ trans = self.trans.unsqueeze(dim=0)
329
+
330
+ inv_rot = self.rot.inv()
331
+ inv_trans = -(inv_rot.dot(trans))
332
+ return self.__class__(inv_rot, inv_trans)
333
+
334
+ def is_cuda(self):
335
+ """Returns true if the underlying tensors are CUDA tensors"""
336
+ return self.rot.is_cuda()
337
+
338
+ def is_pinned(self):
339
+ """Returns true if the underlying tensors reside in pinned memory"""
340
+ return self.rot.is_pinned()
341
+
342
+ @classmethod
343
+ def is_valid_matrix(cls, mat):
344
+ if mat.dim() < 3:
345
+ mat = mat.unsqueeze(dim=0)
346
+
347
+ # Check the shape
348
+ if mat.is_cuda:
349
+ shape_check = torch.cuda.BoolTensor(mat.shape[0]).fill_(False)
350
+ else:
351
+ shape_check = torch.BoolTensor(mat.shape[0]).fill_(False)
352
+
353
+ if mat.shape[1:3] != (cls.dim, cls.dim):
354
+ return shape_check
355
+ else:
356
+ shape_check.fill_(True)
357
+
358
+ # Bottom row should be [zeros, 1]
359
+ bottom_row = mat.new_zeros(cls.dim)
360
+ bottom_row[-1] = 1.
361
+ bottom_check = (mat[:, cls.dim - 1, :] == bottom_row.unsqueeze_(
362
+ dim=0).expand(mat.shape[0], cls.dim)).sum(dim=1) == cls.dim
363
+
364
+ # Check that the rotation part is valid
365
+ rot_check = cls.RotationType.is_valid_matrix(
366
+ mat[:, 0:cls.dim - 1, 0:cls.dim - 1])
367
+
368
+ return shape_check & bottom_check & rot_check
369
+
370
+ def normalize(self, inds=None):
371
+ self.rot.normalize(inds)
372
+
373
+ def pin_memory(self):
374
+ """Return a copy with the underlying tensor in pinned (page-locked) memory. Makes host-to-GPU copies faster.
375
+
376
+ See: http://pytorch.org/docs/master/notes/cuda.html?highlight=pinned
377
+ """
378
+ return self.__class__(self.rot.pin_memory(), self.trans.pin_memory())
379
+
380
+
381
+ class VectorLieGroupBase(_base.VectorLieGroupBase):
382
+ """Implementation of methods common to vector-parametrized lie groups using PyTorch"""
383
+ pass
liegroups/build/lib/liegroups/torch/se2.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from . import _base
4
+ from . import utils
5
+ from .so2 import SO2Matrix
6
+
7
+
8
+ class SE2Matrix(_base.SEMatrixBase):
9
+ """See :mod:`liegroups.SE2`"""
10
+ dim = 3
11
+ dof = 3
12
+ RotationType = SO2Matrix
13
+
14
+ def adjoint(self):
15
+ rot_part = self.rot.as_matrix()
16
+ if rot_part.dim() < 3:
17
+ rot_part = rot_part.unsqueeze(dim=0) # matrix --> batch
18
+
19
+ trans = self.trans
20
+ if trans.dim() < 2:
21
+ # vector --> vectorbatch
22
+ trans = trans.unsqueeze(dim=0)
23
+
24
+ trans_part = trans.new_empty(
25
+ trans.shape[0], trans.shape[1], 1)
26
+ trans_part[:, 0, 0] = trans[:, 1]
27
+ trans_part[:, 1, 0] = -trans[:, 0]
28
+
29
+ bottom_row = trans.new_zeros(self.dof)
30
+ bottom_row[-1] = 1.
31
+ bottom_row = bottom_row.unsqueeze_(dim=0).unsqueeze_(
32
+ dim=0).expand(trans.shape[0], 1, self.dof)
33
+
34
+ return torch.cat([torch.cat([rot_part, trans_part], dim=2),
35
+ bottom_row], dim=1).squeeze_()
36
+
37
+ @classmethod
38
+ def exp(cls, xi):
39
+ if xi.dim() < 2:
40
+ xi = xi.unsqueeze(dim=0)
41
+
42
+ if xi.shape[1] != cls.dof:
43
+ raise ValueError(
44
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
45
+
46
+ rho = xi[:, 0:2]
47
+ phi = xi[:, 2]
48
+
49
+ rot = cls.RotationType.exp(phi)
50
+ rot_jac = cls.RotationType.left_jacobian(phi)
51
+
52
+ if rot_jac.dim() < 3:
53
+ rot_jac.unsqueeze_(dim=0)
54
+ if rho.dim() < 3:
55
+ rho.unsqueeze_(dim=2)
56
+
57
+ trans = torch.bmm(rot_jac, rho).squeeze_()
58
+
59
+ return cls(rot, trans)
60
+
61
+ @classmethod
62
+ def inv_left_jacobian(cls, xi):
63
+ raise NotImplementedError
64
+
65
+ @classmethod
66
+ def left_jacobian(cls, xi):
67
+ raise NotImplementedError
68
+
69
+ def log(self):
70
+ phi = self.rot.log()
71
+ inv_rot_jac = self.RotationType.inv_left_jacobian(phi)
72
+
73
+ if self.trans.dim() < 2:
74
+ trans = self.trans.unsqueeze(dim=0)
75
+ else:
76
+ trans = self.trans
77
+
78
+ if phi.dim() < 1:
79
+ phi.unsqueeze_(dim=0)
80
+ phi.unsqueeze_(dim=1) # because phi is 1-dimensional for SE2
81
+
82
+ if inv_rot_jac.dim() < 3:
83
+ inv_rot_jac.unsqueeze_(dim=0)
84
+ if trans.dim() < 3:
85
+ trans = trans.unsqueeze(dim=2)
86
+
87
+ rho = torch.bmm(inv_rot_jac, trans).squeeze_()
88
+ if rho.dim() < 2:
89
+ rho.unsqueeze_(dim=0)
90
+
91
+ return torch.cat([rho, phi], dim=1).squeeze_()
92
+
93
+ @classmethod
94
+ def odot(cls, p, directional=False):
95
+ if p.dim() < 2:
96
+ p = p.unsqueeze(dim=0) # vector --> vectorbatch
97
+
98
+ result = p.__class__(p.shape[0], p.shape[1], cls.dof).zero_()
99
+
100
+ # Got euclidean coordinates
101
+ if p.shape[1] == cls.dim - 1:
102
+ # Assume scale parameter is 1 unless p is a direction
103
+ # vector, in which case the scale is 0
104
+ if not directional:
105
+ result[:, 0:2, 0:2] = torch.eye(
106
+ cls.RotationType.dim).unsqueeze_(dim=0).expand(
107
+ p.shape[0], cls.RotationType.dim, cls.RotationType.dim)
108
+
109
+ result[:, 0:2, 2] = torch.mm(
110
+ cls.RotationType.wedge(p.__class__([1.])),
111
+ p.transpose(1, 0)).transpose_(1, 0)
112
+
113
+ # Got homogeneous coordinates
114
+ elif p.shape[1] == cls.dim:
115
+ result[:, 0:2, 0:2] = \
116
+ p[:, 2].unsqueeze_(dim=1).unsqueeze_(dim=2) * \
117
+ torch.eye(
118
+ cls.RotationType.dim).unsqueeze_(dim=0).repeat(
119
+ p.shape[0], 1, 1)
120
+
121
+ result[:, 0:2, 2] = torch.mm(
122
+ cls.RotationType.wedge(p.__class__([1.])),
123
+ p[:, 0:2].transpose_(1, 0)).transpose_(1, 0)
124
+
125
+ # Got wrong dimension
126
+ else:
127
+ raise ValueError("p must have shape ({},), ({},), (N,{}) or (N,{})".format(
128
+ cls.dim - 1, cls.dim, cls.dim - 1, cls.dim))
129
+
130
+ return result.squeeze_()
131
+
132
+ @classmethod
133
+ def vee(cls, Xi):
134
+ if Xi.dim() < 3:
135
+ Xi = Xi.unsqueeze(dim=0)
136
+
137
+ if Xi.shape[1:3] != (cls.dim, cls.dim):
138
+ raise ValueError("Xi must have shape ({},{}) or (N,{},{})".format(
139
+ cls.dim, cls.dim, cls.dim, cls.dim))
140
+
141
+ xi = Xi.new_empty(Xi.shape[0], cls.dof)
142
+ xi[:, 0:2] = Xi[:, 0:2, 2]
143
+ xi[:, 2] = cls.RotationType.vee(Xi[:, 0:2, 0:2])
144
+
145
+ return xi.squeeze_()
146
+
147
+ @classmethod
148
+ def wedge(cls, xi):
149
+ if xi.dim() < 2:
150
+ xi = xi.unsqueeze(dim=0)
151
+
152
+ if xi.shape[1] != cls.dof:
153
+ raise ValueError(
154
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
155
+
156
+ Xi = xi.new_zeros(xi.shape[0], cls.dim, cls.dim)
157
+ Xi[:, 0:2, 0:2] = cls.RotationType.wedge(xi[:, 2])
158
+ Xi[:, 0:2, 2] = xi[:, 0:2]
159
+
160
+ return Xi.squeeze_()
liegroups/build/lib/liegroups/torch/se3.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from . import _base
4
+ from . import utils
5
+ from .so3 import SO3Matrix
6
+
7
+
8
+ class SE3Matrix(_base.SEMatrixBase):
9
+ """See :mod:`liegroups.SE3` """
10
+ dim = 4
11
+ dof = 6
12
+ RotationType = SO3Matrix
13
+
14
+ def adjoint(self):
15
+ rot = self.rot.as_matrix()
16
+ if rot.dim() < 3:
17
+ rot = rot.unsqueeze(dim=0) # matrix --> batch
18
+
19
+ trans = self.trans
20
+ if trans.dim() < 2:
21
+ # vector --> vectorbatch
22
+ trans = trans.unsqueeze(dim=0)
23
+
24
+ trans_wedge = self.RotationType.wedge(trans)
25
+ if trans_wedge.dim() < 3:
26
+ trans_wedge.unsqueeze_(dim=0) # matrix --> batch
27
+
28
+ trans_wedge_bmm_rot = torch.bmm(trans_wedge, rot)
29
+
30
+ zero_block = trans.new_empty(rot.shape).zero_()
31
+
32
+ return torch.cat([torch.cat([rot, trans_wedge_bmm_rot], dim=2),
33
+ torch.cat([zero_block, rot], dim=2)], dim=1
34
+ ).squeeze_()
35
+
36
+ @classmethod
37
+ def curlyvee(cls, Psi):
38
+ if Psi.dim() < 3:
39
+ Psi = Psi.unsqueeze(dim=0)
40
+
41
+ if Psi.shape[1:] != (cls.dof, cls.dof):
42
+ raise ValueError("Psi must have shape ({},{}) or (N,{},{})".format(
43
+ cls.dof, cls.dof, cls.dof, cls.dof))
44
+
45
+ xi = Psi.new_empty(Psi.shape[0], cls.dof)
46
+ xi[:, :3] = cls.RotationType.vee(Psi[:, :3, 3:])
47
+ xi[:, 3:] = cls.RotationType.vee(Psi[:, :3, :3])
48
+
49
+ return xi.squeeze_()
50
+
51
+ @classmethod
52
+ def curlywedge(cls, xi):
53
+ if xi.dim() < 2:
54
+ xi = xi.unsqueeze(dim=0)
55
+
56
+ if xi.shape[1] != cls.dof:
57
+ raise ValueError(
58
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
59
+
60
+ Psi = xi.new_empty(xi.shape[0], cls.dof, cls.dof).zero_()
61
+ Psi[:, :3, :3] = cls.RotationType.wedge(xi[:, 3:])
62
+ Psi[:, :3, 3:] = cls.RotationType.wedge(xi[:, :3])
63
+ Psi[:, 3:, 3:] = Psi[:, :3, :3]
64
+
65
+ return Psi.squeeze_()
66
+
67
+ @classmethod
68
+ def exp(cls, xi):
69
+ if xi.dim() < 2:
70
+ xi = xi.unsqueeze(dim=0)
71
+
72
+ if xi.shape[1] != cls.dof:
73
+ raise ValueError(
74
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
75
+
76
+ rho = xi[:, :3]
77
+ phi = xi[:, 3:]
78
+
79
+ rot = cls.RotationType.exp(phi)
80
+ rot_jac = cls.RotationType.left_jacobian(phi)
81
+
82
+ if rot_jac.dim() < 3:
83
+ rot_jac = rot_jac.unsqueeze(dim=0)
84
+ if rho.dim() < 3:
85
+ rho = rho.unsqueeze(dim=2)
86
+
87
+ trans = torch.bmm(rot_jac, rho).squeeze_()
88
+
89
+ return cls(rot, trans)
90
+
91
+ @classmethod
92
+ def left_jacobian_Q_matrix(cls, xi):
93
+ if xi.dim() < 2:
94
+ xi = xi.unsqueeze(dim=0)
95
+
96
+ if xi.shape[1] != cls.dof:
97
+ raise ValueError(
98
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
99
+
100
+ rho = xi[:, :3] # translation part
101
+ phi = xi[:, 3:] # rotation part
102
+
103
+ rx = cls.RotationType.wedge(rho)
104
+ if rx.dim() < 3:
105
+ rx.unsqueeze_(dim=0)
106
+
107
+ px = cls.RotationType.wedge(phi)
108
+ if px.dim() < 3:
109
+ px.unsqueeze_(dim=0)
110
+
111
+ ph = phi.norm(p=2, dim=1)
112
+ ph2 = ph * ph
113
+ ph3 = ph2 * ph
114
+ ph4 = ph3 * ph
115
+ ph5 = ph4 * ph
116
+
117
+ cph = ph.cos()
118
+ sph = ph.sin()
119
+
120
+ m1 = 0.5
121
+ m2 = (ph - sph) / ph3
122
+ m3 = (0.5 * ph2 + cph - 1.) / ph4
123
+ m4 = (ph - 1.5 * sph + 0.5 * ph * cph) / ph5
124
+
125
+ m2 = m2.unsqueeze_(dim=1).unsqueeze_(dim=2).expand_as(rx)
126
+ m3 = m3.unsqueeze_(dim=1).unsqueeze_(dim=2).expand_as(rx)
127
+ m4 = m4.unsqueeze_(dim=1).unsqueeze_(dim=2).expand_as(rx)
128
+
129
+ t1 = rx
130
+ t2 = px.bmm(rx) + rx.bmm(px) + px.bmm(rx).bmm(px)
131
+ t3 = px.bmm(px).bmm(rx) + rx.bmm(px).bmm(px) - 3. * px.bmm(rx).bmm(px)
132
+ t4 = px.bmm(rx).bmm(px).bmm(px) + px.bmm(px).bmm(rx).bmm(px)
133
+
134
+ Q = m1 * t1 + m2 * t2 + m3 * t3 + m4 * t4
135
+
136
+ return Q.squeeze_()
137
+
138
+ @classmethod
139
+ def inv_left_jacobian(cls, xi):
140
+ if xi.dim() < 2:
141
+ xi = xi.unsqueeze(dim=0)
142
+
143
+ if xi.shape[1] != cls.dof:
144
+ raise ValueError(
145
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
146
+
147
+ rho = xi[:, :3] # translation part
148
+ phi = xi[:, 3:] # rotation part
149
+
150
+ jac = phi.new_empty(phi.shape[0], cls.dof, cls.dof)
151
+ angle = phi.norm(p=2, dim=1)
152
+
153
+ # Near phi==0, use first order Taylor expansion
154
+ small_angle_mask = utils.isclose(angle, 0.)
155
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
156
+ if len(small_angle_inds) > 0:
157
+
158
+ # Create an identity matrix with a tensor type that matches the input
159
+ I = phi.new_empty(cls.dof, cls.dof)
160
+ torch.eye(cls.dof, out=I)
161
+
162
+ jac[small_angle_inds] = \
163
+ I.expand_as(jac[small_angle_inds]) - \
164
+ 0.5 * cls.curlywedge(xi[small_angle_inds])
165
+
166
+ # Otherwise...
167
+ large_angle_mask = small_angle_mask.logical_not()
168
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
169
+
170
+ if len(large_angle_inds) > 0:
171
+ so3_inv_jac = cls.RotationType.inv_left_jacobian(
172
+ phi[large_angle_inds])
173
+ if so3_inv_jac.dim() < 3:
174
+ so3_inv_jac.unsqueeze_(dim=0)
175
+
176
+ Q_mat = cls.left_jacobian_Q_matrix(xi[large_angle_inds])
177
+ if Q_mat.dim() < 3:
178
+ Q_mat.unsqueeze_(dim=0)
179
+
180
+ zero_block = phi.new_empty(Q_mat.shape).zero_()
181
+ inv_jac_Q_inv_jac = so3_inv_jac.bmm(Q_mat).bmm(so3_inv_jac)
182
+
183
+ jac[large_angle_inds] = torch.cat(
184
+ [torch.cat([so3_inv_jac, -inv_jac_Q_inv_jac], dim=2),
185
+ torch.cat([zero_block, so3_inv_jac], dim=2)], dim=1)
186
+
187
+ return jac.squeeze_()
188
+
189
+ @classmethod
190
+ def left_jacobian(cls, xi):
191
+ if xi.dim() < 2:
192
+ xi = xi.unsqueeze(dim=0)
193
+
194
+ if xi.shape[1] != cls.dof:
195
+ raise ValueError(
196
+ "xi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
197
+
198
+ rho = xi[:, :3] # translation part
199
+ phi = xi[:, 3:] # rotation part
200
+
201
+ jac = phi.new_empty(phi.shape[0], cls.dof, cls.dof)
202
+ angle = phi.norm(p=2, dim=1)
203
+
204
+ # Near phi==0, use first order Taylor expansion
205
+ small_angle_mask = utils.isclose(angle, 0.)
206
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
207
+ if len(small_angle_inds) > 0:
208
+ # Create an identity matrix with a tensor type that matches the input
209
+ I = phi.new_empty(cls.dof, cls.dof)
210
+ torch.eye(cls.dof, out=I)
211
+
212
+ jac[small_angle_inds] = \
213
+ I.expand_as(jac[small_angle_inds]) + \
214
+ 0.5 * cls.curlywedge(xi[small_angle_inds])
215
+
216
+ # Otherwise...
217
+ large_angle_mask = small_angle_mask.logical_not()
218
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
219
+
220
+ if len(large_angle_inds) > 0:
221
+ so3_jac = cls.RotationType.left_jacobian(phi[large_angle_inds])
222
+ if so3_jac.dim() < 3:
223
+ so3_jac.unsqueeze_(dim=0)
224
+
225
+ Q_mat = cls.left_jacobian_Q_matrix(xi[large_angle_inds])
226
+ if Q_mat.dim() < 3:
227
+ Q_mat.unsqueeze_(dim=0)
228
+
229
+ zero_block = phi.new_empty(Q_mat.shape).zero_()
230
+
231
+ jac[large_angle_inds] = torch.cat(
232
+ [torch.cat([so3_jac, Q_mat], dim=2),
233
+ torch.cat([zero_block, so3_jac], dim=2)], dim=1)
234
+
235
+ return jac.squeeze_()
236
+
237
+ def log(self):
238
+ phi = self.rot.log()
239
+ inv_rot_jac = self.RotationType.inv_left_jacobian(phi)
240
+
241
+ if self.trans.dim() < 2:
242
+ trans = self.trans.unsqueeze(dim=0)
243
+ else:
244
+ trans = self.trans
245
+
246
+ if inv_rot_jac.dim() < 3:
247
+ inv_rot_jac.unsqueeze_(dim=0)
248
+ if trans.dim() < 3:
249
+ trans = trans.unsqueeze(dim=2)
250
+
251
+ rho = torch.bmm(inv_rot_jac, trans).squeeze_()
252
+ if rho.dim() < 2:
253
+ rho.unsqueeze_(dim=0)
254
+ if phi.dim() < 2:
255
+ phi.unsqueeze_(dim=0)
256
+
257
+ return torch.cat([rho, phi], dim=1).squeeze_()
258
+
259
+ @classmethod
260
+ def odot(cls, p, directional=False):
261
+ if p.dim() < 2:
262
+ p = p.unsqueeze(dim=0) # vector --> vectorbatch
263
+
264
+ result = p.new_empty(p.shape[0], p.shape[1], cls.dof).zero_()
265
+
266
+ # Got euclidean coordinates
267
+ if p.shape[1] == cls.dim - 1:
268
+ # Assume scale parameter is 1 unless p is a direction
269
+ # vector, in which case the scale is 0
270
+ if not directional:
271
+ result[:, :3, :3] = torch.eye(3, dtype=p.dtype).unsqueeze_(dim=0).expand(
272
+ p.shape[0], 3, 3)
273
+
274
+ result[:, :3, 3:] = cls.RotationType.wedge(-p)
275
+
276
+ # Got homogeneous coordinates
277
+ elif p.shape[1] == cls.dim:
278
+ result[:, :3, :3] = \
279
+ p[:, 3].unsqueeze_(dim=1).unsqueeze_(dim=2) * \
280
+ torch.eye(3, dtype=p.dtype).unsqueeze_(dim=0).repeat(
281
+ p.shape[0], 1, 1)
282
+
283
+ result[:, :3, 3:] = cls.RotationType.wedge(-p[:, :3])
284
+
285
+ # Got wrong dimension
286
+ else:
287
+ raise ValueError("p must have shape ({},), ({},), (N,{}) or (N,{})".format(
288
+ cls.dim - 1, cls.dim, cls.dim - 1, cls.dim))
289
+
290
+ return result.squeeze_()
291
+
292
+ @classmethod
293
+ def vee(cls, Xi):
294
+ if Xi.dim() < 3:
295
+ Xi = Xi.unsqueeze(dim=0)
296
+
297
+ if Xi.shape[1:3] != (cls.dim, cls.dim):
298
+ raise ValueError("Xi must have shape ({},{}) or (N,{},{})".format(
299
+ cls.dim, cls.dim, cls.dim, cls.dim))
300
+
301
+ xi = Xi.new_empty(Xi.shape[0], cls.dof)
302
+ xi[:, :3] = Xi[:, :3, 3]
303
+ xi[:, 3:] = cls.RotationType.vee(Xi[:, :3, :3])
304
+
305
+ return xi.squeeze_()
306
+
307
+ @classmethod
308
+ def wedge(cls, xi):
309
+ if xi.dim() < 2:
310
+ xi = xi.unsqueeze(dim=0)
311
+
312
+ if xi.shape[1] != cls.dof:
313
+ raise ValueError(
314
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
315
+
316
+ Xi = xi.new_empty(xi.shape[0], cls.dim, cls.dim).zero_()
317
+ Xi[:, :3, :3] = cls.RotationType.wedge(xi[:, 3:])
318
+ Xi[:, :3, 3] = xi[:, :3]
319
+
320
+ return Xi.squeeze_()
liegroups/build/lib/liegroups/torch/so2.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from . import _base
4
+ from . import utils
5
+
6
+
7
+ class SO2Matrix(_base.SOMatrixBase):
8
+ """See :mod:`liegroups.SO2`"""
9
+ dim = 2
10
+ dof = 1
11
+
12
+ def adjoint(self):
13
+ if self.mat.dim() < 3:
14
+ return self.mat.__class__([1.])
15
+ else:
16
+ return self.mat.__class__(self.mat.shape[0]).fill_(1.)
17
+
18
+ @classmethod
19
+ def exp(cls, phi):
20
+ if phi.dim() < 1:
21
+ phi = phi.unsqueeze(dim=0)
22
+
23
+ s = phi.sin()
24
+ c = phi.cos()
25
+
26
+ mat = phi.__class__(phi.shape[0], cls.dim, cls.dim)
27
+ mat[:, 0, 0] = c
28
+ mat[:, 0, 1] = -s
29
+ mat[:, 1, 0] = s
30
+ mat[:, 1, 1] = c
31
+
32
+ return cls(mat.squeeze_())
33
+
34
+ @classmethod
35
+ def from_angle(cls, angle_in_radians):
36
+ """Form a rotation matrix given an angle in rad."""
37
+ return cls.exp(angle_in_radians)
38
+
39
+ @classmethod
40
+ def inv_left_jacobian(cls, phi):
41
+ """(see Barfoot/Eade)."""
42
+ if phi.dim() < 1:
43
+ phi = phi.unsqueeze(dim=0)
44
+
45
+ jac = phi.__class__(phi.shape[0], cls.dim, cls.dim)
46
+
47
+ # Near phi==0, use first order Taylor expansion
48
+ small_angle_mask = utils.isclose(phi, 0.)
49
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
50
+
51
+ if len(small_angle_inds) > 0:
52
+ jac[small_angle_inds] = torch.eye(cls.dim).expand(
53
+ len(small_angle_inds), cls.dim, cls.dim) \
54
+ - 0.5 * cls.wedge(phi[small_angle_inds])
55
+
56
+ # Otherwise...
57
+ large_angle_mask = small_angle_mask.logical_not()
58
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
59
+
60
+ if len(large_angle_inds) > 0:
61
+ angle = phi[large_angle_inds]
62
+ ha = 0.5 * angle # half angle
63
+ hacha = ha / ha.tan() # half angle * cot(half angle)
64
+
65
+ ha.unsqueeze_(dim=1).unsqueeze_(
66
+ dim=2).expand_as(jac[large_angle_inds])
67
+ hacha.unsqueeze_(dim=1).unsqueeze_(
68
+ dim=2).expand_as(jac[large_angle_inds])
69
+
70
+ A = hacha * \
71
+ torch.eye(cls.dim).unsqueeze_(
72
+ dim=0).expand_as(jac[large_angle_inds])
73
+ B = -ha * cls.wedge(phi.__class__([1.]))
74
+
75
+ jac[large_angle_inds] = A + B
76
+
77
+ return jac.squeeze_()
78
+
79
+ @classmethod
80
+ def left_jacobian(cls, phi):
81
+ """(see Barfoot/Eade)."""
82
+ if phi.dim() < 1:
83
+ phi = phi.unsqueeze(dim=0)
84
+
85
+ jac = phi.__class__(phi.shape[0], cls.dim, cls.dim)
86
+
87
+ # Near phi==0, use first order Taylor expansion
88
+ small_angle_mask = utils.isclose(phi, 0.)
89
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
90
+
91
+ if len(small_angle_inds) > 0:
92
+ jac[small_angle_inds] = torch.eye(cls.dim).expand(
93
+ len(small_angle_inds), cls.dim, cls.dim) \
94
+ + 0.5 * cls.wedge(phi[small_angle_inds])
95
+
96
+ # Otherwise...
97
+ large_angle_mask = small_angle_mask.logical_not()
98
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
99
+
100
+ if len(large_angle_inds) > 0:
101
+ angle = phi[large_angle_inds]
102
+ s = angle.sin()
103
+ c = angle.cos()
104
+
105
+ A = (s / angle).unsqueeze_(dim=1).unsqueeze_(
106
+ dim=2).expand_as(jac[large_angle_inds]) * \
107
+ torch.eye(cls.dim).unsqueeze_(dim=0).expand_as(
108
+ jac[large_angle_inds])
109
+ B = ((1. - c) / angle).unsqueeze_(dim=1).unsqueeze_(
110
+ dim=2).expand_as(jac[large_angle_inds]) * \
111
+ cls.wedge(phi.__class__([1.]))
112
+
113
+ jac[large_angle_inds] = A + B
114
+
115
+ return jac.squeeze_()
116
+
117
+ def log(self):
118
+ if self.mat.dim() < 3:
119
+ mat = self.mat.unsqueeze(dim=0)
120
+ else:
121
+ mat = self.mat
122
+
123
+ s = mat[:, 1, 0]
124
+ c = mat[:, 0, 0]
125
+
126
+ return torch.atan2(s, c).squeeze_()
127
+
128
+ def to_angle(self):
129
+ """Recover the rotation angle in rad from the rotation matrix."""
130
+ return self.log()
131
+
132
+ @classmethod
133
+ def vee(cls, Phi):
134
+ if Phi.dim() < 3:
135
+ Phi = Phi.unsqueeze(dim=0)
136
+
137
+ if Phi.shape[1:3] != (cls.dim, cls.dim):
138
+ raise ValueError(
139
+ "Phi must have shape ({},{}) or (N,{},{})".format(cls.dim, cls.dim, cls.dim, cls.dim))
140
+
141
+ return Phi[:, 1, 0].squeeze_()
142
+
143
+ @classmethod
144
+ def wedge(cls, phi):
145
+ if phi.dim() < 1:
146
+ phi = phi.unsqueeze(dim=0) # scalar --> vector
147
+ if phi.dim() < 2:
148
+ phi = phi.unsqueeze(dim=1) # vector --> matrix (N --> Nx1)
149
+
150
+ if phi.shape[1] != cls.dof:
151
+ raise ValueError(
152
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
153
+
154
+ Phi = phi.new_zeros(phi.shape[0], cls.dim, cls.dim)
155
+ Phi[:, 0, 1] = -phi[:, 0]
156
+ Phi[:, 1, 0] = phi[:, 0]
157
+ return Phi.squeeze_()
liegroups/build/lib/liegroups/torch/so3.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+
4
+ from . import _base
5
+ from . import utils
6
+
7
+
8
+ class SO3Matrix(_base.SOMatrixBase):
9
+ """See :mod:`liegroups.SO3`"""
10
+ dim = 3
11
+ dof = 3
12
+
13
+ def adjoint(self):
14
+ return self.mat
15
+
16
+ @classmethod
17
+ def exp(cls, phi):
18
+ if phi.dim() < 2:
19
+ phi = phi.unsqueeze(dim=0)
20
+
21
+ if phi.shape[1] != cls.dof:
22
+ raise ValueError(
23
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
24
+
25
+ mat = phi.new_empty(phi.shape[0], cls.dim, cls.dim)
26
+ angle = phi.norm(p=2, dim=1)
27
+
28
+ # Near phi==0, use first order Taylor expansion
29
+ small_angle_mask = utils.isclose(angle, 0.)
30
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
31
+
32
+ if len(small_angle_inds) > 0:
33
+ mat[small_angle_inds] = \
34
+ torch.eye(cls.dim, dtype=phi.dtype).expand_as(mat[small_angle_inds]) + \
35
+ cls.wedge(phi[small_angle_inds])
36
+
37
+ # Otherwise...
38
+ large_angle_mask = small_angle_mask.logical_not()
39
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
40
+
41
+ if len(large_angle_inds) > 0:
42
+ angle = angle[large_angle_inds]
43
+ axis = phi[large_angle_inds] / \
44
+ angle.unsqueeze(dim=1).expand(len(angle), cls.dim)
45
+ s = angle.sin().unsqueeze_(dim=1).unsqueeze_(
46
+ dim=2).expand_as(mat[large_angle_inds])
47
+ c = angle.cos().unsqueeze_(dim=1).unsqueeze_(
48
+ dim=2).expand_as(mat[large_angle_inds])
49
+
50
+ A = c * torch.eye(cls.dim, dtype=phi.dtype).unsqueeze_(dim=0).expand_as(
51
+ mat[large_angle_inds])
52
+ B = (1. - c) * utils.outer(axis, axis)
53
+ C = s * cls.wedge(axis)
54
+
55
+ mat[large_angle_inds] = A + B + C
56
+
57
+ return cls(mat.squeeze_())
58
+
59
+ @classmethod
60
+ def from_quaternion(cls, quat, ordering='wxyz'):
61
+ """Form a rotation matrix from a unit length quaternion.
62
+
63
+ Valid orderings are 'xyzw' and 'wxyz'.
64
+ """
65
+ if quat.dim() < 2:
66
+ quat = quat.unsqueeze(dim=0)
67
+
68
+ if not utils.allclose(quat.norm(p=2, dim=1), 1.):
69
+ raise ValueError("Quaternions must be unit length")
70
+
71
+ if ordering is 'xyzw':
72
+ qx = quat[:, 0]
73
+ qy = quat[:, 1]
74
+ qz = quat[:, 2]
75
+ qw = quat[:, 3]
76
+ elif ordering is 'wxyz':
77
+ qw = quat[:, 0]
78
+ qx = quat[:, 1]
79
+ qy = quat[:, 2]
80
+ qz = quat[:, 3]
81
+ else:
82
+ raise ValueError(
83
+ "Valid orderings are 'xyzw' and 'wxyz'. Got '{}'.".format(ordering))
84
+
85
+ # Form the matrix
86
+ mat = quat.new_empty(quat.shape[0], cls.dim, cls.dim)
87
+
88
+ qw2 = qw * qw
89
+ qx2 = qx * qx
90
+ qy2 = qy * qy
91
+ qz2 = qz * qz
92
+
93
+ mat[:, 0, 0] = 1. - 2. * (qy2 + qz2)
94
+ mat[:, 0, 1] = 2. * (qx * qy - qw * qz)
95
+ mat[:, 0, 2] = 2. * (qw * qy + qx * qz)
96
+
97
+ mat[:, 1, 0] = 2. * (qw * qz + qx * qy)
98
+ mat[:, 1, 1] = 1. - 2. * (qx2 + qz2)
99
+ mat[:, 1, 2] = 2. * (qy * qz - qw * qx)
100
+
101
+ mat[:, 2, 0] = 2. * (qx * qz - qw * qy)
102
+ mat[:, 2, 1] = 2. * (qw * qx + qy * qz)
103
+ mat[:, 2, 2] = 1. - 2. * (qx2 + qy2)
104
+
105
+ return cls(mat.squeeze_())
106
+
107
+ @classmethod
108
+ def from_rpy(cls, rpy):
109
+ """Form a rotation matrix from RPY Euler angles."""
110
+ if rpy.dim() < 2:
111
+ rpy = rpy.unsqueeze(dim=0)
112
+
113
+ roll = rpy[:, 0]
114
+ pitch = rpy[:, 1]
115
+ yaw = rpy[:, 2]
116
+ return cls.rotz(yaw).dot(cls.roty(pitch).dot(cls.rotx(roll)))
117
+
118
+ @classmethod
119
+ def inv_left_jacobian(cls, phi):
120
+ if phi.dim() < 2:
121
+ phi = phi.unsqueeze(dim=0)
122
+
123
+ if phi.shape[1] != cls.dof:
124
+ raise ValueError(
125
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
126
+
127
+ jac = phi.new_empty(phi.shape[0], cls.dof, cls.dof)
128
+ angle = phi.norm(p=2, dim=1)
129
+
130
+ # Near phi==0, use first order Taylor expansion
131
+ small_angle_mask = utils.isclose(angle, 0.)
132
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
133
+ if len(small_angle_inds) > 0:
134
+ jac[small_angle_inds] = \
135
+ torch.eye(cls.dof, dtype=phi.dtype).expand_as(jac[small_angle_inds]) - \
136
+ 0.5 * cls.wedge(phi[small_angle_inds])
137
+
138
+ # Otherwise...
139
+ large_angle_mask = small_angle_mask.logical_not()
140
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
141
+
142
+ if len(large_angle_inds) > 0:
143
+ angle = angle[large_angle_inds]
144
+ axis = phi[large_angle_inds] / \
145
+ angle.unsqueeze(dim=1).expand(len(angle), cls.dof)
146
+
147
+ ha = 0.5 * angle # half angle
148
+ hacha = ha / ha.tan() # half angle * cot(half angle)
149
+
150
+ ha.unsqueeze_(dim=1).unsqueeze_(
151
+ dim=2).expand_as(jac[large_angle_inds])
152
+ hacha.unsqueeze_(dim=1).unsqueeze_(
153
+ dim=2).expand_as(jac[large_angle_inds])
154
+
155
+ A = hacha * \
156
+ torch.eye(cls.dof, dtype=phi.dtype).unsqueeze_(
157
+ dim=0).expand_as(jac[large_angle_inds])
158
+ B = (1. - hacha) * utils.outer(axis, axis)
159
+ C = -ha * cls.wedge(axis)
160
+
161
+ jac[large_angle_inds] = A + B + C
162
+
163
+ return jac.squeeze_()
164
+
165
+ @classmethod
166
+ def left_jacobian(cls, phi):
167
+ if phi.dim() < 2:
168
+ phi = phi.unsqueeze(dim=0)
169
+
170
+ if phi.shape[1] != cls.dof:
171
+ raise ValueError(
172
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
173
+
174
+ jac = phi.new_empty(phi.shape[0], cls.dof, cls.dof)
175
+ angle = phi.norm(p=2, dim=1)
176
+
177
+ # Near phi==0, use first order Taylor expansion
178
+ small_angle_mask = utils.isclose(angle, 0.)
179
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
180
+ if len(small_angle_inds) > 0:
181
+ jac[small_angle_inds] = \
182
+ torch.eye(cls.dof, dtype=phi.dtype).expand_as(jac[small_angle_inds]) + \
183
+ 0.5 * cls.wedge(phi[small_angle_inds])
184
+
185
+ # Otherwise...
186
+ large_angle_mask = small_angle_mask.logical_not()
187
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
188
+
189
+ if len(large_angle_inds) > 0:
190
+ angle = angle[large_angle_inds]
191
+ axis = phi[large_angle_inds] / \
192
+ angle.unsqueeze(dim=1).expand(len(angle), cls.dof)
193
+ s = angle.sin()
194
+ c = angle.cos()
195
+
196
+ A = (s / angle).unsqueeze_(dim=1).unsqueeze_(
197
+ dim=2).expand_as(jac[large_angle_inds]) * \
198
+ torch.eye(cls.dof, dtype=phi.dtype).unsqueeze_(dim=0).expand_as(
199
+ jac[large_angle_inds])
200
+ B = (1. - s / angle).unsqueeze_(dim=1).unsqueeze_(
201
+ dim=2).expand_as(jac[large_angle_inds]) * \
202
+ utils.outer(axis, axis)
203
+ C = ((1. - c) / angle).unsqueeze_(dim=1).unsqueeze_(
204
+ dim=2).expand_as(jac[large_angle_inds]) * \
205
+ cls.wedge(axis.squeeze())
206
+
207
+ jac[large_angle_inds] = A + B + C
208
+
209
+ return jac.squeeze_()
210
+
211
+ def log(self):
212
+ if self.mat.dim() < 3:
213
+ mat = self.mat.unsqueeze(dim=0)
214
+ else:
215
+ mat = self.mat
216
+
217
+ phi = mat.new_empty(mat.shape[0], self.dof)
218
+
219
+ # The cosine of the rotation angle is related to the utils.trace of C
220
+ # Clamp to its proper domain to avoid NaNs from rounding errors
221
+ cos_angle = (0.5 * utils.trace(mat) - 0.5).clamp_(-1., 1.)
222
+ angle = cos_angle.acos()
223
+
224
+ # Near phi==0, use first order Taylor expansion
225
+ small_angle_mask = utils.isclose(angle, 0.)
226
+ small_angle_inds = small_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
227
+
228
+ if len(small_angle_inds) > 0:
229
+ phi[small_angle_inds, :] = \
230
+ self.vee(mat[small_angle_inds] -
231
+ torch.eye(self.dim, dtype=mat.dtype).expand_as(mat[small_angle_inds]))
232
+
233
+ # Otherwise...
234
+ large_angle_mask = small_angle_mask.logical_not()
235
+ large_angle_inds = large_angle_mask.nonzero(as_tuple=False).squeeze_(dim=1)
236
+
237
+ if len(large_angle_inds) > 0:
238
+ angle = angle[large_angle_inds]
239
+ sin_angle = angle.sin()
240
+ phi[large_angle_inds, :] = \
241
+ self.vee(
242
+ (0.5 * angle / sin_angle).unsqueeze_(dim=1).unsqueeze_(dim=1).expand_as(mat[large_angle_inds]) *
243
+ (mat[large_angle_inds] - mat[large_angle_inds].transpose(2, 1)))
244
+
245
+ return phi.squeeze_()
246
+
247
+ @classmethod
248
+ def rotx(cls, angle_in_radians):
249
+ """Form a rotation matrix given an angle in rad about the x-axis."""
250
+ s = angle_in_radians.sin()
251
+ c = angle_in_radians.cos()
252
+
253
+ mat = angle_in_radians.new_empty(
254
+ angle_in_radians.shape[0], cls.dim, cls.dim).zero_()
255
+ mat[:, 0, 0] = 1.
256
+ mat[:, 1, 1] = c
257
+ mat[:, 1, 2] = -s
258
+ mat[:, 2, 1] = s
259
+ mat[:, 2, 2] = c
260
+
261
+ return cls(mat.squeeze_())
262
+
263
+ @classmethod
264
+ def roty(cls, angle_in_radians):
265
+ """Form a rotation matrix given an angle in rad about the y-axis."""
266
+ s = angle_in_radians.sin()
267
+ c = angle_in_radians.cos()
268
+
269
+ mat = angle_in_radians.new_empty(
270
+ angle_in_radians.shape[0], cls.dim, cls.dim).zero_()
271
+ mat[:, 1, 1] = 1.
272
+ mat[:, 0, 0] = c
273
+ mat[:, 0, 2] = s
274
+ mat[:, 2, 0] = -s
275
+ mat[:, 2, 2] = c
276
+
277
+ return cls(mat.squeeze_())
278
+
279
+ @classmethod
280
+ def rotz(cls, angle_in_radians):
281
+ """Form a rotation matrix given an angle in rad about the z-axis."""
282
+ s = angle_in_radians.sin()
283
+ c = angle_in_radians.cos()
284
+
285
+ mat = angle_in_radians.new_empty(
286
+ angle_in_radians.shape[0], cls.dim, cls.dim).zero_()
287
+ mat[:, 2, 2] = 1.
288
+ mat[:, 0, 0] = c
289
+ mat[:, 0, 1] = -s
290
+ mat[:, 1, 0] = s
291
+ mat[:, 1, 1] = c
292
+
293
+ return cls(mat.squeeze_())
294
+
295
+ def to_quaternion(self, ordering='wxyz'):
296
+ """Convert a rotation matrix to a unit length quaternion.
297
+
298
+ Valid orderings are 'xyzw' and 'wxyz'.
299
+ """
300
+ if self.mat.dim() < 3:
301
+ R = self.mat.unsqueeze(dim=0)
302
+ else:
303
+ R = self.mat
304
+
305
+ qw = 0.5 * torch.sqrt(1. + R[:, 0, 0] + R[:, 1, 1] + R[:, 2, 2])
306
+ qx = qw.new_empty(qw.shape)
307
+ qy = qw.new_empty(qw.shape)
308
+ qz = qw.new_empty(qw.shape)
309
+
310
+ near_zero_mask = utils.isclose(qw, 0.)
311
+
312
+ if sum(near_zero_mask) > 0:
313
+ cond1_mask = near_zero_mask & \
314
+ (R[:, 0, 0] > R[:, 1, 1]).squeeze_() & \
315
+ (R[:, 0, 0] > R[:, 2, 2]).squeeze_()
316
+ cond1_inds = cond1_mask.nonzero(as_tuple=False).squeeze_(dim=1)
317
+
318
+ if len(cond1_inds) > 0:
319
+ R_cond1 = R[cond1_inds]
320
+ d = 2. * np.sqrt(1. + R_cond1[:, 0, 0] -
321
+ R_cond1[:, 1, 1] - R_cond1[:, 2, 2])
322
+ qw[cond1_inds] = (R_cond1[:, 2, 1] - R_cond1[:, 1, 2]) / d
323
+ qx[cond1_inds] = 0.25 * d
324
+ qy[cond1_inds] = (R_cond1[:, 1, 0] + R_cond1[:, 0, 1]) / d
325
+ qz[cond1_inds] = (R_cond1[:, 0, 2] + R_cond1[:, 2, 0]) / d
326
+
327
+ cond2_mask = near_zero_mask & (R[:, 1, 1] > R[:, 2, 2]).squeeze_()
328
+ cond2_inds = cond2_mask.nonzero(as_tuple=False).squeeze_(dim=1)
329
+
330
+ if len(cond2_inds) > 0:
331
+ R_cond2 = R[cond2_inds]
332
+ d = 2. * np.sqrt(1. + R_cond2[:, 1, 1] -
333
+ R_cond2[:, 0, 0] - R_cond2[:, 2, 2])
334
+ qw[cond2_inds] = (R_cond2[:, 0, 2] - R_cond2[:, 2, 0]) / d
335
+ qx[cond2_inds] = (R_cond2[:, 1, 0] + R_cond2[:, 0, 1]) / d
336
+ qy[cond2_inds] = 0.25 * d
337
+ qz[cond2_inds] = (R_cond2[:, 2, 1] + R_cond2[:, 1, 2]) / d
338
+
339
+ cond3_mask = near_zero_mask & cond1_mask.logical_not() & cond2_mask.logical_not()
340
+ cond3_inds = cond3_mask.nonzero(as_tuple=False).squeeze_(dim=1)
341
+
342
+ if len(cond3_inds) > 0:
343
+ R_cond3 = R[cond3_inds]
344
+ d = 2. * \
345
+ np.sqrt(1. + R_cond3[:, 2, 2] -
346
+ R_cond3[:, 0, 0] - R_cond3[:, 1, 1])
347
+ qw[cond3_inds] = (R_cond3[:, 1, 0] - R_cond3[:, 0, 1]) / d
348
+ qx[cond3_inds] = (R_cond3[:, 0, 2] + R_cond3[:, 2, 0]) / d
349
+ qy[cond3_inds] = (R_cond3[:, 2, 1] + R_cond3[:, 1, 2]) / d
350
+ qz[cond3_inds] = 0.25 * d
351
+
352
+ far_zero_mask = near_zero_mask.logical_not()
353
+ far_zero_inds = far_zero_mask.nonzero(as_tuple=False).squeeze_(dim=1)
354
+ if len(far_zero_inds) > 0:
355
+ R_fz = R[far_zero_inds]
356
+ d = 4. * qw[far_zero_inds]
357
+ qx[far_zero_inds] = (R_fz[:, 2, 1] - R_fz[:, 1, 2]) / d
358
+ qy[far_zero_inds] = (R_fz[:, 0, 2] - R_fz[:, 2, 0]) / d
359
+ qz[far_zero_inds] = (R_fz[:, 1, 0] - R_fz[:, 0, 1]) / d
360
+
361
+ # Check ordering last
362
+ if ordering is 'xyzw':
363
+ quat = torch.cat([qx.unsqueeze_(dim=1),
364
+ qy.unsqueeze_(dim=1),
365
+ qz.unsqueeze_(dim=1),
366
+ qw.unsqueeze_(dim=1)], dim=1).squeeze_()
367
+ elif ordering is 'wxyz':
368
+ quat = torch.cat([qw.unsqueeze_(dim=1),
369
+ qx.unsqueeze_(dim=1),
370
+ qy.unsqueeze_(dim=1),
371
+ qz.unsqueeze_(dim=1)], dim=1).squeeze_()
372
+ else:
373
+ raise ValueError(
374
+ "Valid orderings are 'xyzw' and 'wxyz'. Got '{}'.".format(ordering))
375
+
376
+ return quat
377
+
378
+ def to_rpy(self):
379
+ """Convert a rotation matrix to RPY Euler angles."""
380
+ if self.mat.dim() < 3:
381
+ mat = self.mat.unsqueeze(dim=0)
382
+ else:
383
+ mat = self.mat
384
+
385
+ pitch = torch.atan2(-mat[:, 2, 0],
386
+ torch.sqrt(mat[:, 0, 0]**2 + mat[:, 1, 0]**2))
387
+ yaw = pitch.new_empty(pitch.shape)
388
+ roll = pitch.new_empty(pitch.shape)
389
+
390
+ near_pi_over_two_mask = utils.isclose(pitch, np.pi / 2.)
391
+ near_pi_over_two_inds = near_pi_over_two_mask.nonzero(as_tuple=False).squeeze_(dim=1)
392
+
393
+ near_neg_pi_over_two_mask = utils.isclose(pitch, -np.pi / 2.)
394
+ near_neg_pi_over_two_inds = near_neg_pi_over_two_mask.nonzero(as_tuple=False).squeeze_(dim=1)
395
+
396
+ remainder_inds = (near_pi_over_two_mask |
397
+ near_neg_pi_over_two_mask).logical_not().nonzero(as_tuple=False).squeeze_(dim=1)
398
+
399
+ if len(near_pi_over_two_inds) > 0:
400
+ yaw[near_pi_over_two_inds] = 0.
401
+ roll[near_pi_over_two_inds] = torch.atan2(
402
+ mat[near_pi_over_two_inds, 0, 1],
403
+ mat[near_pi_over_two_inds, 1, 1])
404
+
405
+ if len(near_neg_pi_over_two_inds) > 0:
406
+ yaw[near_pi_over_two_inds] = 0.
407
+ roll[near_pi_over_two_inds] = -torch.atan2(
408
+ mat[near_pi_over_two_inds, 0, 1],
409
+ mat[near_pi_over_two_inds, 1, 1])
410
+
411
+ if len(remainder_inds) > 0:
412
+ sec_pitch = 1. / pitch[remainder_inds].cos()
413
+ remainder_mats = mat[remainder_inds]
414
+ yaw = torch.atan2(remainder_mats[:, 1, 0] * sec_pitch,
415
+ remainder_mats[:, 0, 0] * sec_pitch)
416
+ roll = torch.atan2(remainder_mats[:, 2, 1] * sec_pitch,
417
+ remainder_mats[:, 2, 2] * sec_pitch)
418
+
419
+ return torch.cat([roll.unsqueeze_(dim=1),
420
+ pitch.unsqueeze_(dim=1),
421
+ yaw.unsqueeze_(dim=1)], dim=1).squeeze_()
422
+
423
+ @classmethod
424
+ def vee(cls, Phi):
425
+ if Phi.dim() < 3:
426
+ Phi = Phi.unsqueeze(dim=0)
427
+
428
+ if Phi.shape[1:3] != (cls.dim, cls.dim):
429
+ raise ValueError("Phi must have shape ({},{}) or (N,{},{})".format(
430
+ cls.dim, cls.dim, cls.dim, cls.dim))
431
+
432
+ phi = Phi.new_empty(Phi.shape[0], cls.dim)
433
+ phi[:, 0] = Phi[:, 2, 1]
434
+ phi[:, 1] = Phi[:, 0, 2]
435
+ phi[:, 2] = Phi[:, 1, 0]
436
+ return phi.squeeze_()
437
+
438
+ @classmethod
439
+ def wedge(cls, phi):
440
+ if phi.dim() < 2:
441
+ phi = phi.unsqueeze(dim=0)
442
+
443
+ if phi.shape[1] != cls.dof:
444
+ raise ValueError(
445
+ "phi must have shape ({},) or (N,{})".format(cls.dof, cls.dof))
446
+
447
+ Phi = phi.new_empty(phi.shape[0], cls.dim, cls.dim).zero_()
448
+ Phi[:, 0, 1] = -phi[:, 2]
449
+ Phi[:, 1, 0] = phi[:, 2]
450
+ Phi[:, 0, 2] = phi[:, 1]
451
+ Phi[:, 2, 0] = -phi[:, 1]
452
+ Phi[:, 1, 2] = -phi[:, 0]
453
+ Phi[:, 2, 1] = phi[:, 0]
454
+ return Phi.squeeze_()
455
+
456
+
457
+ class SO3Quaternion(_base.VectorLieGroupBase):
458
+ pass
liegroups/build/lib/liegroups/torch/utils.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+
4
+ def allclose(mat1, mat2, tol=1e-6):
5
+ """Check if all elements of two tensors are close within some tolerance.
6
+
7
+ Either tensor can be replaced by a scalar.
8
+ """
9
+ return isclose(mat1, mat2, tol).all()
10
+
11
+
12
+ def isclose(mat1, mat2, tol=1e-6):
13
+ """Check element-wise if two tensors are close within some tolerance.
14
+
15
+ Either tensor can be replaced by a scalar.
16
+ """
17
+ return (mat1 - mat2).abs_().lt(tol)
18
+
19
+
20
+ def outer(vecs1, vecs2):
21
+ """Return the N x D x D outer products of a N x D batch of vectors,
22
+ or return the D x D outer product of two D-dimensional vectors.
23
+ """
24
+ # Default batch size is 1
25
+ if vecs1.dim() < 2:
26
+ vecs1 = vecs1.unsqueeze(dim=0)
27
+
28
+ if vecs2.dim() < 2:
29
+ vecs2 = vecs2.unsqueeze(dim=0)
30
+
31
+ if vecs1.shape[0] != vecs2.shape[0]:
32
+ raise ValueError("Got inconsistent batch sizes {} and {}".format(
33
+ vecs1.shape[0], vecs2.shape[0]))
34
+
35
+ return torch.bmm(vecs1.unsqueeze(dim=2),
36
+ vecs2.unsqueeze(dim=2).transpose(2, 1)).squeeze_()
37
+
38
+
39
+ def trace(mat):
40
+ """Return the N traces of a batch of N square matrices,
41
+ or return the trace of a square matrix."""
42
+ # Default batch size is 1
43
+ if mat.dim() < 3:
44
+ mat = mat.unsqueeze(dim=0)
45
+
46
+ # Element-wise multiply by identity and take the sum
47
+ tr = (torch.eye(mat.shape[1], dtype=mat.dtype) * mat).sum(dim=1).sum(dim=1)
48
+
49
+ return tr.view(mat.shape[0])
liegroups/docs/Makefile ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line.
5
+ SPHINXOPTS =
6
+ SPHINXBUILD = sphinx-build
7
+ PAPER =
8
+ BUILDDIR = build
9
+
10
+ # Internal variables.
11
+ PAPEROPT_a4 = -D latex_paper_size=a4
12
+ PAPEROPT_letter = -D latex_paper_size=letter
13
+ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
14
+ # the i18n builder cannot share the environment and doctrees with the others
15
+ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
16
+
17
+ .PHONY: help
18
+ help:
19
+ @echo "Please use \`make <target>' where <target> is one of"
20
+ @echo " html to make standalone HTML files"
21
+ @echo " dirhtml to make HTML files named index.html in directories"
22
+ @echo " singlehtml to make a single large HTML file"
23
+ @echo " pickle to make pickle files"
24
+ @echo " json to make JSON files"
25
+ @echo " htmlhelp to make HTML files and a HTML help project"
26
+ @echo " qthelp to make HTML files and a qthelp project"
27
+ @echo " applehelp to make an Apple Help Book"
28
+ @echo " devhelp to make HTML files and a Devhelp project"
29
+ @echo " epub to make an epub"
30
+ @echo " epub3 to make an epub3"
31
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
32
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
33
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
34
+ @echo " text to make text files"
35
+ @echo " man to make manual pages"
36
+ @echo " texinfo to make Texinfo files"
37
+ @echo " info to make Texinfo files and run them through makeinfo"
38
+ @echo " gettext to make PO message catalogs"
39
+ @echo " changes to make an overview of all changed/added/deprecated items"
40
+ @echo " xml to make Docutils-native XML files"
41
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
42
+ @echo " linkcheck to check all external links for integrity"
43
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
44
+ @echo " coverage to run coverage check of the documentation (if enabled)"
45
+ @echo " dummy to check syntax errors of document sources"
46
+
47
+ .PHONY: clean
48
+ clean:
49
+ rm -rf $(BUILDDIR)/*
50
+
51
+ .PHONY: html
52
+ html:
53
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54
+ @echo
55
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56
+
57
+ .PHONY: dirhtml
58
+ dirhtml:
59
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
60
+ @echo
61
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
62
+
63
+ .PHONY: singlehtml
64
+ singlehtml:
65
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66
+ @echo
67
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68
+
69
+ .PHONY: pickle
70
+ pickle:
71
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
72
+ @echo
73
+ @echo "Build finished; now you can process the pickle files."
74
+
75
+ .PHONY: json
76
+ json:
77
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
78
+ @echo
79
+ @echo "Build finished; now you can process the JSON files."
80
+
81
+ .PHONY: htmlhelp
82
+ htmlhelp:
83
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
84
+ @echo
85
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
86
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
87
+
88
+ .PHONY: qthelp
89
+ qthelp:
90
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
91
+ @echo
92
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
93
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
94
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/liegroups.qhcp"
95
+ @echo "To view the help file:"
96
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/liegroups.qhc"
97
+
98
+ .PHONY: applehelp
99
+ applehelp:
100
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
101
+ @echo
102
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
103
+ @echo "N.B. You won't be able to view it unless you put it in" \
104
+ "~/Library/Documentation/Help or install it in your application" \
105
+ "bundle."
106
+
107
+ .PHONY: devhelp
108
+ devhelp:
109
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
110
+ @echo
111
+ @echo "Build finished."
112
+ @echo "To view the help file:"
113
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/liegroups"
114
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/liegroups"
115
+ @echo "# devhelp"
116
+
117
+ .PHONY: epub
118
+ epub:
119
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
120
+ @echo
121
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
122
+
123
+ .PHONY: epub3
124
+ epub3:
125
+ $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
126
+ @echo
127
+ @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
128
+
129
+ .PHONY: latex
130
+ latex:
131
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
132
+ @echo
133
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
134
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
135
+ "(use \`make latexpdf' here to do that automatically)."
136
+
137
+ .PHONY: latexpdf
138
+ latexpdf:
139
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
140
+ @echo "Running LaTeX files through pdflatex..."
141
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
142
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
143
+
144
+ .PHONY: latexpdfja
145
+ latexpdfja:
146
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
147
+ @echo "Running LaTeX files through platex and dvipdfmx..."
148
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
149
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
150
+
151
+ .PHONY: text
152
+ text:
153
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
154
+ @echo
155
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
156
+
157
+ .PHONY: man
158
+ man:
159
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
160
+ @echo
161
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
162
+
163
+ .PHONY: texinfo
164
+ texinfo:
165
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
166
+ @echo
167
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
168
+ @echo "Run \`make' in that directory to run these through makeinfo" \
169
+ "(use \`make info' here to do that automatically)."
170
+
171
+ .PHONY: info
172
+ info:
173
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
174
+ @echo "Running Texinfo files through makeinfo..."
175
+ make -C $(BUILDDIR)/texinfo info
176
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
177
+
178
+ .PHONY: gettext
179
+ gettext:
180
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
181
+ @echo
182
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
183
+
184
+ .PHONY: changes
185
+ changes:
186
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
187
+ @echo
188
+ @echo "The overview file is in $(BUILDDIR)/changes."
189
+
190
+ .PHONY: linkcheck
191
+ linkcheck:
192
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
193
+ @echo
194
+ @echo "Link check complete; look for any errors in the above output " \
195
+ "or in $(BUILDDIR)/linkcheck/output.txt."
196
+
197
+ .PHONY: doctest
198
+ doctest:
199
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
200
+ @echo "Testing of doctests in the sources finished, look at the " \
201
+ "results in $(BUILDDIR)/doctest/output.txt."
202
+
203
+ .PHONY: coverage
204
+ coverage:
205
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
206
+ @echo "Testing of coverage in the sources finished, look at the " \
207
+ "results in $(BUILDDIR)/coverage/python.txt."
208
+
209
+ .PHONY: xml
210
+ xml:
211
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
212
+ @echo
213
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
214
+
215
+ .PHONY: pseudoxml
216
+ pseudoxml:
217
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
218
+ @echo
219
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
220
+
221
+ .PHONY: dummy
222
+ dummy:
223
+ $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
224
+ @echo
225
+ @echo "Build finished. Dummy builder generates no files."
liegroups/docs/build/doctrees/environment.pickle ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e2969896cea4747f8d6e1dffc55b54095e97c7056b44abf6111618064b2a8608
3
+ size 28411
liegroups/docs/build/doctrees/index.doctree ADDED
Binary file (3.11 kB). View file
 
liegroups/docs/build/doctrees/numpy.doctree ADDED
Binary file (192 kB). View file
 
liegroups/docs/build/doctrees/torch.doctree ADDED
Binary file (49.2 kB). View file
 
liegroups/docs/build/html/.buildinfo ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Sphinx build info version 1
2
+ # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
3
+ config: 17eb4b7752c8c4cbfb1c05ec79ac90ce
4
+ tags: 645f666f9bcd5a90fca523b33c5a78b7
liegroups/docs/build/html/_sources/index.rst.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. liegroups documentation master file, created by
2
+ sphinx-quickstart on Thu Nov 3 18:58:59 2016.
3
+ You can adapt this file completely to your liking, but it should at least
4
+ contain the root `toctree` directive.
5
+
6
+ =================
7
+ Lie Groups
8
+ =================
9
+
10
+ .. toctree::
11
+ :maxdepth: 2
12
+
13
+ numpy
14
+ torch
liegroups/docs/build/html/_sources/numpy.rst.txt ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ =================
2
+ liegroups
3
+ =================
4
+
5
+ The default implementation uses numpy as the backend linear algebra library.
6
+
7
+ SO(2)
8
+ -----
9
+ .. autoclass:: liegroups.SO2
10
+ :members:
11
+ :inherited-members:
12
+ :undoc-members:
13
+
14
+ .. autoclass:: liegroups.numpy.so2.SO2Matrix
15
+ :members:
16
+ :inherited-members:
17
+ :undoc-members:
18
+
19
+ SE(2)
20
+ -----
21
+ .. autoclass:: liegroups.SE2
22
+ :members:
23
+ :inherited-members:
24
+ :undoc-members:
25
+
26
+ .. autoclass:: liegroups.numpy.se2.SE2Matrix
27
+ :members:
28
+ :inherited-members:
29
+ :undoc-members:
30
+
31
+ SO(3)
32
+ -----
33
+ .. autoclass:: liegroups.SO3
34
+ :members:
35
+ :inherited-members:
36
+ :undoc-members:
37
+
38
+ .. autoclass:: liegroups.numpy.so3.SO3Matrix
39
+ :members:
40
+ :inherited-members:
41
+ :undoc-members:
42
+
43
+ SE(3)
44
+ -----
45
+ .. autoclass:: liegroups.SE3
46
+ :members:
47
+ :inherited-members:
48
+ :undoc-members:
49
+
50
+ .. autoclass:: liegroups.numpy.se3.SE3Matrix
51
+ :members:
52
+ :inherited-members:
53
+ :undoc-members:
liegroups/docs/build/html/_sources/torch.rst.txt ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ =================
2
+ liegroups.torch
3
+ =================
4
+
5
+ The PyTorch implementation uses torch.Tensor as the backend linear algebra library, which allows the user to on the GPU or CPU and integrate with other aspects of PyTorch.
6
+
7
+ This version provides sensible options for batching the transformations themselves, as well as anything they might operate on, and is generally agnostic to the specific Tensor type (e.g., given a torch.cuda.FloatTensor as input, the output will also be a torch.cuda.FloatTensor).
8
+
9
+ .. autoclass:: liegroups.torch.SO2
10
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
11
+
12
+ .. autoclass:: liegroups.torch.so2.SO2Matrix
13
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
14
+
15
+ .. autoclass:: liegroups.torch.SE2
16
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
17
+
18
+ .. autoclass:: liegroups.torch.se2.SE2Matrix
19
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
20
+
21
+ .. autoclass:: liegroups.torch.SO3
22
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
23
+
24
+ .. autoclass:: liegroups.torch.so3.SO3Matrix
25
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
26
+
27
+ .. autoclass:: liegroups.torch.SE3
28
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
29
+
30
+ .. autoclass:: liegroups.torch.se3.SE3Matrix
31
+ :members: cpu, cuda, from_numpy, is_cuda, is_pinned, pin_memory
liegroups/docs/build/html/_static/ajax-loader.gif ADDED
liegroups/docs/build/html/_static/basic.css ADDED
@@ -0,0 +1,764 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * basic.css
3
+ * ~~~~~~~~~
4
+ *
5
+ * Sphinx stylesheet -- basic theme.
6
+ *
7
+ * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
8
+ * :license: BSD, see LICENSE for details.
9
+ *
10
+ */
11
+
12
+ /* -- main layout ----------------------------------------------------------- */
13
+
14
+ div.clearer {
15
+ clear: both;
16
+ }
17
+
18
+ /* -- relbar ---------------------------------------------------------------- */
19
+
20
+ div.related {
21
+ width: 100%;
22
+ font-size: 90%;
23
+ }
24
+
25
+ div.related h3 {
26
+ display: none;
27
+ }
28
+
29
+ div.related ul {
30
+ margin: 0;
31
+ padding: 0 0 0 10px;
32
+ list-style: none;
33
+ }
34
+
35
+ div.related li {
36
+ display: inline;
37
+ }
38
+
39
+ div.related li.right {
40
+ float: right;
41
+ margin-right: 5px;
42
+ }
43
+
44
+ /* -- sidebar --------------------------------------------------------------- */
45
+
46
+ div.sphinxsidebarwrapper {
47
+ padding: 10px 5px 0 10px;
48
+ }
49
+
50
+ div.sphinxsidebar {
51
+ float: left;
52
+ width: 230px;
53
+ margin-left: -100%;
54
+ font-size: 90%;
55
+ word-wrap: break-word;
56
+ overflow-wrap : break-word;
57
+ }
58
+
59
+ div.sphinxsidebar ul {
60
+ list-style: none;
61
+ }
62
+
63
+ div.sphinxsidebar ul ul,
64
+ div.sphinxsidebar ul.want-points {
65
+ margin-left: 20px;
66
+ list-style: square;
67
+ }
68
+
69
+ div.sphinxsidebar ul ul {
70
+ margin-top: 0;
71
+ margin-bottom: 0;
72
+ }
73
+
74
+ div.sphinxsidebar form {
75
+ margin-top: 10px;
76
+ }
77
+
78
+ div.sphinxsidebar input {
79
+ border: 1px solid #98dbcc;
80
+ font-family: sans-serif;
81
+ font-size: 1em;
82
+ }
83
+
84
+ div.sphinxsidebar #searchbox form.search {
85
+ overflow: hidden;
86
+ }
87
+
88
+ div.sphinxsidebar #searchbox input[type="text"] {
89
+ float: left;
90
+ width: 80%;
91
+ padding: 0.25em;
92
+ box-sizing: border-box;
93
+ }
94
+
95
+ div.sphinxsidebar #searchbox input[type="submit"] {
96
+ float: left;
97
+ width: 20%;
98
+ border-left: none;
99
+ padding: 0.25em;
100
+ box-sizing: border-box;
101
+ }
102
+
103
+
104
+ img {
105
+ border: 0;
106
+ max-width: 100%;
107
+ }
108
+
109
+ /* -- search page ----------------------------------------------------------- */
110
+
111
+ ul.search {
112
+ margin: 10px 0 0 20px;
113
+ padding: 0;
114
+ }
115
+
116
+ ul.search li {
117
+ padding: 5px 0 5px 20px;
118
+ background-image: url(file.png);
119
+ background-repeat: no-repeat;
120
+ background-position: 0 7px;
121
+ }
122
+
123
+ ul.search li a {
124
+ font-weight: bold;
125
+ }
126
+
127
+ ul.search li div.context {
128
+ color: #888;
129
+ margin: 2px 0 0 30px;
130
+ text-align: left;
131
+ }
132
+
133
+ ul.keywordmatches li.goodmatch a {
134
+ font-weight: bold;
135
+ }
136
+
137
+ /* -- index page ------------------------------------------------------------ */
138
+
139
+ table.contentstable {
140
+ width: 90%;
141
+ margin-left: auto;
142
+ margin-right: auto;
143
+ }
144
+
145
+ table.contentstable p.biglink {
146
+ line-height: 150%;
147
+ }
148
+
149
+ a.biglink {
150
+ font-size: 1.3em;
151
+ }
152
+
153
+ span.linkdescr {
154
+ font-style: italic;
155
+ padding-top: 5px;
156
+ font-size: 90%;
157
+ }
158
+
159
+ /* -- general index --------------------------------------------------------- */
160
+
161
+ table.indextable {
162
+ width: 100%;
163
+ }
164
+
165
+ table.indextable td {
166
+ text-align: left;
167
+ vertical-align: top;
168
+ }
169
+
170
+ table.indextable ul {
171
+ margin-top: 0;
172
+ margin-bottom: 0;
173
+ list-style-type: none;
174
+ }
175
+
176
+ table.indextable > tbody > tr > td > ul {
177
+ padding-left: 0em;
178
+ }
179
+
180
+ table.indextable tr.pcap {
181
+ height: 10px;
182
+ }
183
+
184
+ table.indextable tr.cap {
185
+ margin-top: 10px;
186
+ background-color: #f2f2f2;
187
+ }
188
+
189
+ img.toggler {
190
+ margin-right: 3px;
191
+ margin-top: 3px;
192
+ cursor: pointer;
193
+ }
194
+
195
+ div.modindex-jumpbox {
196
+ border-top: 1px solid #ddd;
197
+ border-bottom: 1px solid #ddd;
198
+ margin: 1em 0 1em 0;
199
+ padding: 0.4em;
200
+ }
201
+
202
+ div.genindex-jumpbox {
203
+ border-top: 1px solid #ddd;
204
+ border-bottom: 1px solid #ddd;
205
+ margin: 1em 0 1em 0;
206
+ padding: 0.4em;
207
+ }
208
+
209
+ /* -- domain module index --------------------------------------------------- */
210
+
211
+ table.modindextable td {
212
+ padding: 2px;
213
+ border-collapse: collapse;
214
+ }
215
+
216
+ /* -- general body styles --------------------------------------------------- */
217
+
218
+ div.body {
219
+ min-width: 450px;
220
+ max-width: 800px;
221
+ }
222
+
223
+ div.body p, div.body dd, div.body li, div.body blockquote {
224
+ -moz-hyphens: auto;
225
+ -ms-hyphens: auto;
226
+ -webkit-hyphens: auto;
227
+ hyphens: auto;
228
+ }
229
+
230
+ a.headerlink {
231
+ visibility: hidden;
232
+ }
233
+
234
+ a.brackets:before,
235
+ span.brackets > a:before{
236
+ content: "[";
237
+ }
238
+
239
+ a.brackets:after,
240
+ span.brackets > a:after {
241
+ content: "]";
242
+ }
243
+
244
+ h1:hover > a.headerlink,
245
+ h2:hover > a.headerlink,
246
+ h3:hover > a.headerlink,
247
+ h4:hover > a.headerlink,
248
+ h5:hover > a.headerlink,
249
+ h6:hover > a.headerlink,
250
+ dt:hover > a.headerlink,
251
+ caption:hover > a.headerlink,
252
+ p.caption:hover > a.headerlink,
253
+ div.code-block-caption:hover > a.headerlink {
254
+ visibility: visible;
255
+ }
256
+
257
+ div.body p.caption {
258
+ text-align: inherit;
259
+ }
260
+
261
+ div.body td {
262
+ text-align: left;
263
+ }
264
+
265
+ .first {
266
+ margin-top: 0 !important;
267
+ }
268
+
269
+ p.rubric {
270
+ margin-top: 30px;
271
+ font-weight: bold;
272
+ }
273
+
274
+ img.align-left, .figure.align-left, object.align-left {
275
+ clear: left;
276
+ float: left;
277
+ margin-right: 1em;
278
+ }
279
+
280
+ img.align-right, .figure.align-right, object.align-right {
281
+ clear: right;
282
+ float: right;
283
+ margin-left: 1em;
284
+ }
285
+
286
+ img.align-center, .figure.align-center, object.align-center {
287
+ display: block;
288
+ margin-left: auto;
289
+ margin-right: auto;
290
+ }
291
+
292
+ img.align-default, .figure.align-default {
293
+ display: block;
294
+ margin-left: auto;
295
+ margin-right: auto;
296
+ }
297
+
298
+ .align-left {
299
+ text-align: left;
300
+ }
301
+
302
+ .align-center {
303
+ text-align: center;
304
+ }
305
+
306
+ .align-default {
307
+ text-align: center;
308
+ }
309
+
310
+ .align-right {
311
+ text-align: right;
312
+ }
313
+
314
+ /* -- sidebars -------------------------------------------------------------- */
315
+
316
+ div.sidebar {
317
+ margin: 0 0 0.5em 1em;
318
+ border: 1px solid #ddb;
319
+ padding: 7px 7px 0 7px;
320
+ background-color: #ffe;
321
+ width: 40%;
322
+ float: right;
323
+ }
324
+
325
+ p.sidebar-title {
326
+ font-weight: bold;
327
+ }
328
+
329
+ /* -- topics ---------------------------------------------------------------- */
330
+
331
+ div.topic {
332
+ border: 1px solid #ccc;
333
+ padding: 7px 7px 0 7px;
334
+ margin: 10px 0 10px 0;
335
+ }
336
+
337
+ p.topic-title {
338
+ font-size: 1.1em;
339
+ font-weight: bold;
340
+ margin-top: 10px;
341
+ }
342
+
343
+ /* -- admonitions ----------------------------------------------------------- */
344
+
345
+ div.admonition {
346
+ margin-top: 10px;
347
+ margin-bottom: 10px;
348
+ padding: 7px;
349
+ }
350
+
351
+ div.admonition dt {
352
+ font-weight: bold;
353
+ }
354
+
355
+ div.admonition dl {
356
+ margin-bottom: 0;
357
+ }
358
+
359
+ p.admonition-title {
360
+ margin: 0px 10px 5px 0px;
361
+ font-weight: bold;
362
+ }
363
+
364
+ div.body p.centered {
365
+ text-align: center;
366
+ margin-top: 25px;
367
+ }
368
+
369
+ /* -- tables ---------------------------------------------------------------- */
370
+
371
+ table.docutils {
372
+ border: 0;
373
+ border-collapse: collapse;
374
+ }
375
+
376
+ table.align-center {
377
+ margin-left: auto;
378
+ margin-right: auto;
379
+ }
380
+
381
+ table.align-default {
382
+ margin-left: auto;
383
+ margin-right: auto;
384
+ }
385
+
386
+ table caption span.caption-number {
387
+ font-style: italic;
388
+ }
389
+
390
+ table caption span.caption-text {
391
+ }
392
+
393
+ table.docutils td, table.docutils th {
394
+ padding: 1px 8px 1px 5px;
395
+ border-top: 0;
396
+ border-left: 0;
397
+ border-right: 0;
398
+ border-bottom: 1px solid #aaa;
399
+ }
400
+
401
+ table.footnote td, table.footnote th {
402
+ border: 0 !important;
403
+ }
404
+
405
+ th {
406
+ text-align: left;
407
+ padding-right: 5px;
408
+ }
409
+
410
+ table.citation {
411
+ border-left: solid 1px gray;
412
+ margin-left: 1px;
413
+ }
414
+
415
+ table.citation td {
416
+ border-bottom: none;
417
+ }
418
+
419
+ th > p:first-child,
420
+ td > p:first-child {
421
+ margin-top: 0px;
422
+ }
423
+
424
+ th > p:last-child,
425
+ td > p:last-child {
426
+ margin-bottom: 0px;
427
+ }
428
+
429
+ /* -- figures --------------------------------------------------------------- */
430
+
431
+ div.figure {
432
+ margin: 0.5em;
433
+ padding: 0.5em;
434
+ }
435
+
436
+ div.figure p.caption {
437
+ padding: 0.3em;
438
+ }
439
+
440
+ div.figure p.caption span.caption-number {
441
+ font-style: italic;
442
+ }
443
+
444
+ div.figure p.caption span.caption-text {
445
+ }
446
+
447
+ /* -- field list styles ----------------------------------------------------- */
448
+
449
+ table.field-list td, table.field-list th {
450
+ border: 0 !important;
451
+ }
452
+
453
+ .field-list ul {
454
+ margin: 0;
455
+ padding-left: 1em;
456
+ }
457
+
458
+ .field-list p {
459
+ margin: 0;
460
+ }
461
+
462
+ .field-name {
463
+ -moz-hyphens: manual;
464
+ -ms-hyphens: manual;
465
+ -webkit-hyphens: manual;
466
+ hyphens: manual;
467
+ }
468
+
469
+ /* -- hlist styles ---------------------------------------------------------- */
470
+
471
+ table.hlist td {
472
+ vertical-align: top;
473
+ }
474
+
475
+
476
+ /* -- other body styles ----------------------------------------------------- */
477
+
478
+ ol.arabic {
479
+ list-style: decimal;
480
+ }
481
+
482
+ ol.loweralpha {
483
+ list-style: lower-alpha;
484
+ }
485
+
486
+ ol.upperalpha {
487
+ list-style: upper-alpha;
488
+ }
489
+
490
+ ol.lowerroman {
491
+ list-style: lower-roman;
492
+ }
493
+
494
+ ol.upperroman {
495
+ list-style: upper-roman;
496
+ }
497
+
498
+ li > p:first-child {
499
+ margin-top: 0px;
500
+ }
501
+
502
+ li > p:last-child {
503
+ margin-bottom: 0px;
504
+ }
505
+
506
+ dl.footnote > dt,
507
+ dl.citation > dt {
508
+ float: left;
509
+ }
510
+
511
+ dl.footnote > dd,
512
+ dl.citation > dd {
513
+ margin-bottom: 0em;
514
+ }
515
+
516
+ dl.footnote > dd:after,
517
+ dl.citation > dd:after {
518
+ content: "";
519
+ clear: both;
520
+ }
521
+
522
+ dl.field-list {
523
+ display: grid;
524
+ grid-template-columns: fit-content(30%) auto;
525
+ }
526
+
527
+ dl.field-list > dt {
528
+ font-weight: bold;
529
+ word-break: break-word;
530
+ padding-left: 0.5em;
531
+ padding-right: 5px;
532
+ }
533
+
534
+ dl.field-list > dt:after {
535
+ content: ":";
536
+ }
537
+
538
+ dl.field-list > dd {
539
+ padding-left: 0.5em;
540
+ margin-top: 0em;
541
+ margin-left: 0em;
542
+ margin-bottom: 0em;
543
+ }
544
+
545
+ dl {
546
+ margin-bottom: 15px;
547
+ }
548
+
549
+ dd > p:first-child {
550
+ margin-top: 0px;
551
+ }
552
+
553
+ dd ul, dd table {
554
+ margin-bottom: 10px;
555
+ }
556
+
557
+ dd {
558
+ margin-top: 3px;
559
+ margin-bottom: 10px;
560
+ margin-left: 30px;
561
+ }
562
+
563
+ dt:target, span.highlighted {
564
+ background-color: #fbe54e;
565
+ }
566
+
567
+ rect.highlighted {
568
+ fill: #fbe54e;
569
+ }
570
+
571
+ dl.glossary dt {
572
+ font-weight: bold;
573
+ font-size: 1.1em;
574
+ }
575
+
576
+ .optional {
577
+ font-size: 1.3em;
578
+ }
579
+
580
+ .sig-paren {
581
+ font-size: larger;
582
+ }
583
+
584
+ .versionmodified {
585
+ font-style: italic;
586
+ }
587
+
588
+ .system-message {
589
+ background-color: #fda;
590
+ padding: 5px;
591
+ border: 3px solid red;
592
+ }
593
+
594
+ .footnote:target {
595
+ background-color: #ffa;
596
+ }
597
+
598
+ .line-block {
599
+ display: block;
600
+ margin-top: 1em;
601
+ margin-bottom: 1em;
602
+ }
603
+
604
+ .line-block .line-block {
605
+ margin-top: 0;
606
+ margin-bottom: 0;
607
+ margin-left: 1.5em;
608
+ }
609
+
610
+ .guilabel, .menuselection {
611
+ font-family: sans-serif;
612
+ }
613
+
614
+ .accelerator {
615
+ text-decoration: underline;
616
+ }
617
+
618
+ .classifier {
619
+ font-style: oblique;
620
+ }
621
+
622
+ .classifier:before {
623
+ font-style: normal;
624
+ margin: 0.5em;
625
+ content: ":";
626
+ }
627
+
628
+ abbr, acronym {
629
+ border-bottom: dotted 1px;
630
+ cursor: help;
631
+ }
632
+
633
+ /* -- code displays --------------------------------------------------------- */
634
+
635
+ pre {
636
+ overflow: auto;
637
+ overflow-y: hidden; /* fixes display issues on Chrome browsers */
638
+ }
639
+
640
+ span.pre {
641
+ -moz-hyphens: none;
642
+ -ms-hyphens: none;
643
+ -webkit-hyphens: none;
644
+ hyphens: none;
645
+ }
646
+
647
+ td.linenos pre {
648
+ padding: 5px 0px;
649
+ border: 0;
650
+ background-color: transparent;
651
+ color: #aaa;
652
+ }
653
+
654
+ table.highlighttable {
655
+ margin-left: 0.5em;
656
+ }
657
+
658
+ table.highlighttable td {
659
+ padding: 0 0.5em 0 0.5em;
660
+ }
661
+
662
+ div.code-block-caption {
663
+ padding: 2px 5px;
664
+ font-size: small;
665
+ }
666
+
667
+ div.code-block-caption code {
668
+ background-color: transparent;
669
+ }
670
+
671
+ div.code-block-caption + div > div.highlight > pre {
672
+ margin-top: 0;
673
+ }
674
+
675
+ div.code-block-caption span.caption-number {
676
+ padding: 0.1em 0.3em;
677
+ font-style: italic;
678
+ }
679
+
680
+ div.code-block-caption span.caption-text {
681
+ }
682
+
683
+ div.literal-block-wrapper {
684
+ padding: 1em 1em 0;
685
+ }
686
+
687
+ div.literal-block-wrapper div.highlight {
688
+ margin: 0;
689
+ }
690
+
691
+ code.descname {
692
+ background-color: transparent;
693
+ font-weight: bold;
694
+ font-size: 1.2em;
695
+ }
696
+
697
+ code.descclassname {
698
+ background-color: transparent;
699
+ }
700
+
701
+ code.xref, a code {
702
+ background-color: transparent;
703
+ font-weight: bold;
704
+ }
705
+
706
+ h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
707
+ background-color: transparent;
708
+ }
709
+
710
+ .viewcode-link {
711
+ float: right;
712
+ }
713
+
714
+ .viewcode-back {
715
+ float: right;
716
+ font-family: sans-serif;
717
+ }
718
+
719
+ div.viewcode-block:target {
720
+ margin: -1px -10px;
721
+ padding: 0 10px;
722
+ }
723
+
724
+ /* -- math display ---------------------------------------------------------- */
725
+
726
+ img.math {
727
+ vertical-align: middle;
728
+ }
729
+
730
+ div.body div.math p {
731
+ text-align: center;
732
+ }
733
+
734
+ span.eqno {
735
+ float: right;
736
+ }
737
+
738
+ span.eqno a.headerlink {
739
+ position: relative;
740
+ left: 0px;
741
+ z-index: 1;
742
+ }
743
+
744
+ div.math:hover a.headerlink {
745
+ visibility: visible;
746
+ }
747
+
748
+ /* -- printout stylesheet --------------------------------------------------- */
749
+
750
+ @media print {
751
+ div.document,
752
+ div.documentwrapper,
753
+ div.bodywrapper {
754
+ margin: 0 !important;
755
+ width: 100%;
756
+ }
757
+
758
+ div.sphinxsidebar,
759
+ div.related,
760
+ div.footer,
761
+ #top-link {
762
+ display: none;
763
+ }
764
+ }
liegroups/docs/build/html/_static/comment-bright.png ADDED
liegroups/docs/build/html/_static/comment-close.png ADDED
liegroups/docs/build/html/_static/comment.png ADDED
liegroups/docs/build/html/_static/css/badge_only.css ADDED
@@ -0,0 +1 @@
 
 
1
+ .fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}
liegroups/docs/build/html/_static/css/theme.css ADDED
The diff for this file is too large to render. See raw diff
 
liegroups/docs/build/html/_static/doctools.js ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * doctools.js
3
+ * ~~~~~~~~~~~
4
+ *
5
+ * Sphinx JavaScript utilities for all documentation.
6
+ *
7
+ * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
8
+ * :license: BSD, see LICENSE for details.
9
+ *
10
+ */
11
+
12
+ /**
13
+ * select a different prefix for underscore
14
+ */
15
+ $u = _.noConflict();
16
+
17
+ /**
18
+ * make the code below compatible with browsers without
19
+ * an installed firebug like debugger
20
+ if (!window.console || !console.firebug) {
21
+ var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
22
+ "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
23
+ "profile", "profileEnd"];
24
+ window.console = {};
25
+ for (var i = 0; i < names.length; ++i)
26
+ window.console[names[i]] = function() {};
27
+ }
28
+ */
29
+
30
+ /**
31
+ * small helper function to urldecode strings
32
+ */
33
+ jQuery.urldecode = function(x) {
34
+ return decodeURIComponent(x).replace(/\+/g, ' ');
35
+ };
36
+
37
+ /**
38
+ * small helper function to urlencode strings
39
+ */
40
+ jQuery.urlencode = encodeURIComponent;
41
+
42
+ /**
43
+ * This function returns the parsed url parameters of the
44
+ * current request. Multiple values per key are supported,
45
+ * it will always return arrays of strings for the value parts.
46
+ */
47
+ jQuery.getQueryParameters = function(s) {
48
+ if (typeof s === 'undefined')
49
+ s = document.location.search;
50
+ var parts = s.substr(s.indexOf('?') + 1).split('&');
51
+ var result = {};
52
+ for (var i = 0; i < parts.length; i++) {
53
+ var tmp = parts[i].split('=', 2);
54
+ var key = jQuery.urldecode(tmp[0]);
55
+ var value = jQuery.urldecode(tmp[1]);
56
+ if (key in result)
57
+ result[key].push(value);
58
+ else
59
+ result[key] = [value];
60
+ }
61
+ return result;
62
+ };
63
+
64
+ /**
65
+ * highlight a given string on a jquery object by wrapping it in
66
+ * span elements with the given class name.
67
+ */
68
+ jQuery.fn.highlightText = function(text, className) {
69
+ function highlight(node, addItems) {
70
+ if (node.nodeType === 3) {
71
+ var val = node.nodeValue;
72
+ var pos = val.toLowerCase().indexOf(text);
73
+ if (pos >= 0 &&
74
+ !jQuery(node.parentNode).hasClass(className) &&
75
+ !jQuery(node.parentNode).hasClass("nohighlight")) {
76
+ var span;
77
+ var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
78
+ if (isInSVG) {
79
+ span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
80
+ } else {
81
+ span = document.createElement("span");
82
+ span.className = className;
83
+ }
84
+ span.appendChild(document.createTextNode(val.substr(pos, text.length)));
85
+ node.parentNode.insertBefore(span, node.parentNode.insertBefore(
86
+ document.createTextNode(val.substr(pos + text.length)),
87
+ node.nextSibling));
88
+ node.nodeValue = val.substr(0, pos);
89
+ if (isInSVG) {
90
+ var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
91
+ var bbox = node.parentElement.getBBox();
92
+ rect.x.baseVal.value = bbox.x;
93
+ rect.y.baseVal.value = bbox.y;
94
+ rect.width.baseVal.value = bbox.width;
95
+ rect.height.baseVal.value = bbox.height;
96
+ rect.setAttribute('class', className);
97
+ addItems.push({
98
+ "parent": node.parentNode,
99
+ "target": rect});
100
+ }
101
+ }
102
+ }
103
+ else if (!jQuery(node).is("button, select, textarea")) {
104
+ jQuery.each(node.childNodes, function() {
105
+ highlight(this, addItems);
106
+ });
107
+ }
108
+ }
109
+ var addItems = [];
110
+ var result = this.each(function() {
111
+ highlight(this, addItems);
112
+ });
113
+ for (var i = 0; i < addItems.length; ++i) {
114
+ jQuery(addItems[i].parent).before(addItems[i].target);
115
+ }
116
+ return result;
117
+ };
118
+
119
+ /*
120
+ * backward compatibility for jQuery.browser
121
+ * This will be supported until firefox bug is fixed.
122
+ */
123
+ if (!jQuery.browser) {
124
+ jQuery.uaMatch = function(ua) {
125
+ ua = ua.toLowerCase();
126
+
127
+ var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
128
+ /(webkit)[ \/]([\w.]+)/.exec(ua) ||
129
+ /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
130
+ /(msie) ([\w.]+)/.exec(ua) ||
131
+ ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
132
+ [];
133
+
134
+ return {
135
+ browser: match[ 1 ] || "",
136
+ version: match[ 2 ] || "0"
137
+ };
138
+ };
139
+ jQuery.browser = {};
140
+ jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
141
+ }
142
+
143
+ /**
144
+ * Small JavaScript module for the documentation.
145
+ */
146
+ var Documentation = {
147
+
148
+ init : function() {
149
+ this.fixFirefoxAnchorBug();
150
+ this.highlightSearchWords();
151
+ this.initIndexTable();
152
+ if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
153
+ this.initOnKeyListeners();
154
+ }
155
+ },
156
+
157
+ /**
158
+ * i18n support
159
+ */
160
+ TRANSLATIONS : {},
161
+ PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
162
+ LOCALE : 'unknown',
163
+
164
+ // gettext and ngettext don't access this so that the functions
165
+ // can safely bound to a different name (_ = Documentation.gettext)
166
+ gettext : function(string) {
167
+ var translated = Documentation.TRANSLATIONS[string];
168
+ if (typeof translated === 'undefined')
169
+ return string;
170
+ return (typeof translated === 'string') ? translated : translated[0];
171
+ },
172
+
173
+ ngettext : function(singular, plural, n) {
174
+ var translated = Documentation.TRANSLATIONS[singular];
175
+ if (typeof translated === 'undefined')
176
+ return (n == 1) ? singular : plural;
177
+ return translated[Documentation.PLURALEXPR(n)];
178
+ },
179
+
180
+ addTranslations : function(catalog) {
181
+ for (var key in catalog.messages)
182
+ this.TRANSLATIONS[key] = catalog.messages[key];
183
+ this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
184
+ this.LOCALE = catalog.locale;
185
+ },
186
+
187
+ /**
188
+ * add context elements like header anchor links
189
+ */
190
+ addContextElements : function() {
191
+ $('div[id] > :header:first').each(function() {
192
+ $('<a class="headerlink">\u00B6</a>').
193
+ attr('href', '#' + this.id).
194
+ attr('title', _('Permalink to this headline')).
195
+ appendTo(this);
196
+ });
197
+ $('dt[id]').each(function() {
198
+ $('<a class="headerlink">\u00B6</a>').
199
+ attr('href', '#' + this.id).
200
+ attr('title', _('Permalink to this definition')).
201
+ appendTo(this);
202
+ });
203
+ },
204
+
205
+ /**
206
+ * workaround a firefox stupidity
207
+ * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
208
+ */
209
+ fixFirefoxAnchorBug : function() {
210
+ if (document.location.hash && $.browser.mozilla)
211
+ window.setTimeout(function() {
212
+ document.location.href += '';
213
+ }, 10);
214
+ },
215
+
216
+ /**
217
+ * highlight the search words provided in the url in the text
218
+ */
219
+ highlightSearchWords : function() {
220
+ var params = $.getQueryParameters();
221
+ var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
222
+ if (terms.length) {
223
+ var body = $('div.body');
224
+ if (!body.length) {
225
+ body = $('body');
226
+ }
227
+ window.setTimeout(function() {
228
+ $.each(terms, function() {
229
+ body.highlightText(this.toLowerCase(), 'highlighted');
230
+ });
231
+ }, 10);
232
+ $('<p class="highlight-link"><a href="javascript:Documentation.' +
233
+ 'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
234
+ .appendTo($('#searchbox'));
235
+ }
236
+ },
237
+
238
+ /**
239
+ * init the domain index toggle buttons
240
+ */
241
+ initIndexTable : function() {
242
+ var togglers = $('img.toggler').click(function() {
243
+ var src = $(this).attr('src');
244
+ var idnum = $(this).attr('id').substr(7);
245
+ $('tr.cg-' + idnum).toggle();
246
+ if (src.substr(-9) === 'minus.png')
247
+ $(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
248
+ else
249
+ $(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
250
+ }).css('display', '');
251
+ if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
252
+ togglers.click();
253
+ }
254
+ },
255
+
256
+ /**
257
+ * helper function to hide the search marks again
258
+ */
259
+ hideSearchWords : function() {
260
+ $('#searchbox .highlight-link').fadeOut(300);
261
+ $('span.highlighted').removeClass('highlighted');
262
+ },
263
+
264
+ /**
265
+ * make the url absolute
266
+ */
267
+ makeURL : function(relativeURL) {
268
+ return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
269
+ },
270
+
271
+ /**
272
+ * get the current relative url
273
+ */
274
+ getCurrentURL : function() {
275
+ var path = document.location.pathname;
276
+ var parts = path.split(/\//);
277
+ $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
278
+ if (this === '..')
279
+ parts.pop();
280
+ });
281
+ var url = parts.join('/');
282
+ return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
283
+ },
284
+
285
+ initOnKeyListeners: function() {
286
+ $(document).keyup(function(event) {
287
+ var activeElementType = document.activeElement.tagName;
288
+ // don't navigate when in search box or textarea
289
+ if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') {
290
+ switch (event.keyCode) {
291
+ case 37: // left
292
+ var prevHref = $('link[rel="prev"]').prop('href');
293
+ if (prevHref) {
294
+ window.location.href = prevHref;
295
+ return false;
296
+ }
297
+ case 39: // right
298
+ var nextHref = $('link[rel="next"]').prop('href');
299
+ if (nextHref) {
300
+ window.location.href = nextHref;
301
+ return false;
302
+ }
303
+ }
304
+ }
305
+ });
306
+ }
307
+ };
308
+
309
+ // quick alias for translations
310
+ _ = Documentation.gettext;
311
+
312
+ $(document).ready(function() {
313
+ Documentation.init();
314
+ });
liegroups/docs/build/html/_static/documentation_options.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ var DOCUMENTATION_OPTIONS = {
2
+ URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
3
+ VERSION: '1.1.0',
4
+ LANGUAGE: 'None',
5
+ COLLAPSE_INDEX: false,
6
+ FILE_SUFFIX: '.html',
7
+ HAS_SOURCE: true,
8
+ SOURCELINK_SUFFIX: '.txt',
9
+ NAVIGATION_WITH_KEYS: false
10
+ };
liegroups/docs/build/html/_static/down-pressed.png ADDED
liegroups/docs/build/html/_static/down.png ADDED
liegroups/docs/build/html/_static/file.png ADDED
liegroups/docs/build/html/_static/fonts/Inconsolata-Bold.ttf ADDED
Binary file (110 kB). View file
 
liegroups/docs/build/html/_static/fonts/Inconsolata-Regular.ttf ADDED
Binary file (97 kB). View file
 
liegroups/docs/build/html/_static/fonts/Inconsolata.ttf ADDED
Binary file (63.2 kB). View file
 
liegroups/docs/build/html/_static/fonts/Lato-Bold.ttf ADDED
Binary file (657 kB). View file
 
liegroups/docs/build/html/_static/fonts/Lato-Regular.ttf ADDED
Binary file (657 kB). View file
 
liegroups/docs/build/html/_static/fonts/Lato/lato-bold.eot ADDED
Binary file (256 kB). View file
 
liegroups/docs/build/html/_static/fonts/Lato/lato-bold.ttf ADDED
Binary file (601 kB). View file
 
liegroups/docs/build/html/_static/fonts/Lato/lato-bold.woff ADDED
Binary file (310 kB). View file
 
liegroups/docs/build/html/_static/fonts/Lato/lato-bold.woff2 ADDED
Binary file (185 kB). View file