init commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +3 -0
- README.md +158 -12
- liegroups/.gitignore +6 -0
- liegroups/LICENSE +21 -0
- liegroups/README.md +36 -0
- liegroups/build/lib/liegroups/__init__.py +16 -0
- liegroups/build/lib/liegroups/_base.py +218 -0
- liegroups/build/lib/liegroups/numpy/__init__.py +9 -0
- liegroups/build/lib/liegroups/numpy/_base.py +172 -0
- liegroups/build/lib/liegroups/numpy/se2.py +206 -0
- liegroups/build/lib/liegroups/numpy/se3.py +354 -0
- liegroups/build/lib/liegroups/numpy/so2.py +161 -0
- liegroups/build/lib/liegroups/numpy/so3.py +430 -0
- liegroups/build/lib/liegroups/torch/__init__.py +9 -0
- liegroups/build/lib/liegroups/torch/_base.py +383 -0
- liegroups/build/lib/liegroups/torch/se2.py +160 -0
- liegroups/build/lib/liegroups/torch/se3.py +320 -0
- liegroups/build/lib/liegroups/torch/so2.py +157 -0
- liegroups/build/lib/liegroups/torch/so3.py +458 -0
- liegroups/build/lib/liegroups/torch/utils.py +49 -0
- liegroups/docs/Makefile +225 -0
- liegroups/docs/build/doctrees/environment.pickle +3 -0
- liegroups/docs/build/doctrees/index.doctree +0 -0
- liegroups/docs/build/doctrees/numpy.doctree +0 -0
- liegroups/docs/build/doctrees/torch.doctree +0 -0
- liegroups/docs/build/html/.buildinfo +4 -0
- liegroups/docs/build/html/_sources/index.rst.txt +14 -0
- liegroups/docs/build/html/_sources/numpy.rst.txt +53 -0
- liegroups/docs/build/html/_sources/torch.rst.txt +31 -0
- liegroups/docs/build/html/_static/ajax-loader.gif +0 -0
- liegroups/docs/build/html/_static/basic.css +764 -0
- liegroups/docs/build/html/_static/comment-bright.png +0 -0
- liegroups/docs/build/html/_static/comment-close.png +0 -0
- liegroups/docs/build/html/_static/comment.png +0 -0
- liegroups/docs/build/html/_static/css/badge_only.css +1 -0
- liegroups/docs/build/html/_static/css/theme.css +0 -0
- liegroups/docs/build/html/_static/doctools.js +314 -0
- liegroups/docs/build/html/_static/documentation_options.js +10 -0
- liegroups/docs/build/html/_static/down-pressed.png +0 -0
- liegroups/docs/build/html/_static/down.png +0 -0
- liegroups/docs/build/html/_static/file.png +0 -0
- liegroups/docs/build/html/_static/fonts/Inconsolata-Bold.ttf +0 -0
- liegroups/docs/build/html/_static/fonts/Inconsolata-Regular.ttf +0 -0
- liegroups/docs/build/html/_static/fonts/Inconsolata.ttf +0 -0
- liegroups/docs/build/html/_static/fonts/Lato-Bold.ttf +0 -0
- liegroups/docs/build/html/_static/fonts/Lato-Regular.ttf +0 -0
- liegroups/docs/build/html/_static/fonts/Lato/lato-bold.eot +0 -0
- liegroups/docs/build/html/_static/fonts/Lato/lato-bold.ttf +0 -0
- liegroups/docs/build/html/_static/fonts/Lato/lato-bold.woff +0 -0
- 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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+

|
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
|
|