mathrobo 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mathrobo-0.0.1/.gitignore +4 -0
- mathrobo-0.0.1/LICENSE +21 -0
- mathrobo-0.0.1/PKG-INFO +59 -0
- mathrobo-0.0.1/README.md +40 -0
- mathrobo-0.0.1/mathrobo/__init__.py +8 -0
- mathrobo-0.0.1/mathrobo/basic/__init__.py +7 -0
- mathrobo-0.0.1/mathrobo/basic/basic.py +133 -0
- mathrobo-0.0.1/mathrobo/basic/cm_vec.py +34 -0
- mathrobo-0.0.1/mathrobo/basic/factorial_vec.py +80 -0
- mathrobo-0.0.1/mathrobo/calculus/__init__.py +7 -0
- mathrobo-0.0.1/mathrobo/calculus/bspline.py +159 -0
- mathrobo-0.0.1/mathrobo/calculus/integral.py +84 -0
- mathrobo-0.0.1/mathrobo/calculus/numerical_grad.py +111 -0
- mathrobo-0.0.1/mathrobo/lie/__init__.py +8 -0
- mathrobo-0.0.1/mathrobo/lie/cmtm_abst.py +561 -0
- mathrobo-0.0.1/mathrobo/lie/lie_abst.py +141 -0
- mathrobo-0.0.1/mathrobo/lie/se3.py +673 -0
- mathrobo-0.0.1/mathrobo/lie/so2.py +98 -0
- mathrobo-0.0.1/mathrobo/lie/so3.py +538 -0
- mathrobo-0.0.1/mathrobo/transformation/__init__.py +5 -0
- mathrobo-0.0.1/mathrobo/transformation/rotation.py +45 -0
- mathrobo-0.0.1/pyproject.toml +30 -0
- mathrobo-0.0.1/requirements.txt +5 -0
- mathrobo-0.0.1/tests/__init__.py +0 -0
- mathrobo-0.0.1/tests/basic/__init__.py +0 -0
- mathrobo-0.0.1/tests/basic/test_basic.py +0 -0
- mathrobo-0.0.1/tests/basic/test_factorial_vec.py +33 -0
- mathrobo-0.0.1/tests/calculus/__init__.py +0 -0
- mathrobo-0.0.1/tests/calculus/test_bspline.py +58 -0
- mathrobo-0.0.1/tests/calculus/test_build_integrator.py +61 -0
- mathrobo-0.0.1/tests/calculus/test_numerical_grad.py +45 -0
- mathrobo-0.0.1/tests/lie/__init__.py +0 -0
- mathrobo-0.0.1/tests/lie/test_cmtm_se3.py +478 -0
- mathrobo-0.0.1/tests/lie/test_cmtm_so3.py +407 -0
- mathrobo-0.0.1/tests/lie/test_jax_consistency.py +91 -0
- mathrobo-0.0.1/tests/lie/test_se3.py +422 -0
- mathrobo-0.0.1/tests/lie/test_so2.py +8 -0
- mathrobo-0.0.1/tests/lie/test_so3.py +214 -0
- mathrobo-0.0.1/tests/lie/test_so3_jax_grad.py +24 -0
- mathrobo-0.0.1/uv.lock +833 -0
mathrobo-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Taiki Ishigaki
|
|
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.
|
mathrobo-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mathrobo
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: basic mathematical library for robotics reserach
|
|
5
|
+
Project-URL: Homepage, https://github.com/MathRobotics/MathRobo
|
|
6
|
+
Author-email: taiki-ishigaki <taiki000ishigaki@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: jax>=0.4.30
|
|
14
|
+
Requires-Dist: numpy>=2.0.2
|
|
15
|
+
Requires-Dist: pytest>=8.4.2
|
|
16
|
+
Requires-Dist: scipy>=1.13.1
|
|
17
|
+
Requires-Dist: sympy>=1.14.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Mathrobo
|
|
21
|
+
|
|
22
|
+
Mathrobo is a lightweight library designed to support mathematical optimization and computations related to robotics.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Clone the repository
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/MathRobotics/MathRobo.git
|
|
30
|
+
cd MathRobo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Install dependencies with uv
|
|
34
|
+
|
|
35
|
+
Sync the environment from `pyproject.toml` and `uv.lock`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv sync
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Install the package in editable mode
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
Refer to the examples in the `examples` folder, where you can find Jupyter notebooks and scripts demonstrating various use cases of the library.
|
|
49
|
+
|
|
50
|
+
## Running Tests
|
|
51
|
+
|
|
52
|
+
Run the test suite with uv:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv run pytest
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Contributing
|
|
59
|
+
Contributions are welcome! Feel free to report issues, suggest features, or submit pull requests.
|
mathrobo-0.0.1/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Mathrobo
|
|
2
|
+
|
|
3
|
+
Mathrobo is a lightweight library designed to support mathematical optimization and computations related to robotics.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Clone the repository
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/MathRobotics/MathRobo.git
|
|
11
|
+
cd MathRobo
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Install dependencies with uv
|
|
15
|
+
|
|
16
|
+
Sync the environment from `pyproject.toml` and `uv.lock`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv sync
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Install the package in editable mode
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
Refer to the examples in the `examples` folder, where you can find Jupyter notebooks and scripts demonstrating various use cases of the library.
|
|
30
|
+
|
|
31
|
+
## Running Tests
|
|
32
|
+
|
|
33
|
+
Run the test suite with uv:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv run pytest
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Contributing
|
|
40
|
+
Contributions are welcome! Feel free to report issues, suggest features, or submit pull requests.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3.9
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# 2024.06.23 Created by T.Ishigaki
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import sympy as sp
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
def lib_error_message():
|
|
12
|
+
ValueError("Unsupported library. Choose 'numpy', 'sympy' or 'jax'.")
|
|
13
|
+
|
|
14
|
+
def iszero(x):
|
|
15
|
+
tolerance = 1e-8 # 許容範囲
|
|
16
|
+
return math.isclose(x, 0, abs_tol=tolerance)
|
|
17
|
+
|
|
18
|
+
def sin(theta, LIB = 'numpy'):
|
|
19
|
+
if LIB == 'numpy':
|
|
20
|
+
return np.sin(theta)
|
|
21
|
+
elif LIB == 'sympy':
|
|
22
|
+
return sp.sin(theta)
|
|
23
|
+
elif LIB == 'jax':
|
|
24
|
+
return jnp.sin(theta)
|
|
25
|
+
else:
|
|
26
|
+
raise lib_error_message()
|
|
27
|
+
|
|
28
|
+
def cos(theta, LIB = 'numpy'):
|
|
29
|
+
if LIB == 'numpy':
|
|
30
|
+
return np.cos(theta)
|
|
31
|
+
elif LIB == 'sympy':
|
|
32
|
+
return sp.cos(theta)
|
|
33
|
+
elif LIB == 'jax':
|
|
34
|
+
return jnp.cos(theta)
|
|
35
|
+
else:
|
|
36
|
+
raise lib_error_message()
|
|
37
|
+
|
|
38
|
+
def zeros(shape, LIB = 'numpy'):
|
|
39
|
+
if LIB == 'numpy':
|
|
40
|
+
return np.zeros(shape)
|
|
41
|
+
elif LIB == 'sympy':
|
|
42
|
+
if type(shape) == int:
|
|
43
|
+
return sp.zeros(shape,1)
|
|
44
|
+
elif type(shape) == tuple and len(shape) == 2:
|
|
45
|
+
return sp.zeros(shape[0],shape[1])
|
|
46
|
+
elif LIB == 'jax':
|
|
47
|
+
return jnp.zeros(shape)
|
|
48
|
+
else:
|
|
49
|
+
raise lib_error_message()
|
|
50
|
+
|
|
51
|
+
def identity(size, LIB = 'numpy'):
|
|
52
|
+
if LIB == 'numpy':
|
|
53
|
+
return np.identity(size)
|
|
54
|
+
elif LIB == 'sympy':
|
|
55
|
+
m = sp.zeros(size,size)
|
|
56
|
+
for i in range(size):
|
|
57
|
+
m[i,i] = 1
|
|
58
|
+
return m
|
|
59
|
+
elif LIB == 'jax':
|
|
60
|
+
return jnp.identity(size)
|
|
61
|
+
else:
|
|
62
|
+
raise lib_error_message()
|
|
63
|
+
|
|
64
|
+
def norm(vec, LIB = 'numpy'):
|
|
65
|
+
if LIB == 'numpy':
|
|
66
|
+
return np.linalg.norm(vec)
|
|
67
|
+
elif LIB == 'sympy':
|
|
68
|
+
return sp.sqrt(vec.dot(vec))
|
|
69
|
+
elif LIB == 'jax':
|
|
70
|
+
return jnp.linalg.norm(vec)
|
|
71
|
+
else:
|
|
72
|
+
raise lib_error_message()
|
|
73
|
+
|
|
74
|
+
def isclose(a, b, LIB = 'numpy'):
|
|
75
|
+
if LIB == 'numpy':
|
|
76
|
+
return np.isclose(a, b)
|
|
77
|
+
elif LIB == 'sympy':
|
|
78
|
+
return sp.simplify(a - b) == 0
|
|
79
|
+
elif LIB == 'jax':
|
|
80
|
+
return jnp.isclose(a, b)
|
|
81
|
+
else:
|
|
82
|
+
raise lib_error_message()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def gq_integrate(func, a, b, digit = 5, LIB = 'numpy'):
|
|
86
|
+
if LIB == 'numpy':
|
|
87
|
+
integ = np.zeros((func(0).shape))
|
|
88
|
+
|
|
89
|
+
if digit == 3:
|
|
90
|
+
quad_x = np.array((-0.77459666924, 0, 0.77459666924))
|
|
91
|
+
quad_weight = np.array((0.55555555555, 0.88888888888, 0.55555555555))
|
|
92
|
+
elif digit == 4:
|
|
93
|
+
quad_x = np.array((-0.8611363116, -0.3399810436, 0.3399810436, 0.8611363116))
|
|
94
|
+
quad_weight = np.array((0.3478548451, 0.6521451549, 0.6521451549, 0.3478548451))
|
|
95
|
+
elif digit == 5:
|
|
96
|
+
quad_x = np.array((-0.9061798459, -0.5384693101, 0, 0.5384693101, 0.9061798459))
|
|
97
|
+
quad_weight = np.array((0.2369268851, 0.4786286705, 0.5688888889, 0.4786286705, 0.2369268851))
|
|
98
|
+
|
|
99
|
+
for i in range(digit):
|
|
100
|
+
w = quad_weight[i] * (b - a) * 0.5
|
|
101
|
+
x = quad_x[i] * (b - a) * 0.5 + (a + b) * 0.5
|
|
102
|
+
integ += w*func(x)
|
|
103
|
+
|
|
104
|
+
return integ
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError("This method for 'numpy'")
|
|
107
|
+
|
|
108
|
+
def jac_lie_wrt_scaler(lie, vec, a, dvec, LIB = 'numpy'):
|
|
109
|
+
m = lie.exp(vec, a, LIB)
|
|
110
|
+
integ_m = -lie.exp_integ_adj(vec, -a, LIB)
|
|
111
|
+
|
|
112
|
+
return m @ lie.hat(integ_m @ dvec, LIB)
|
|
113
|
+
|
|
114
|
+
def jac_adj_lie_wrt_scaler(lie, vec, a, dvec, LIB = 'numpy'):
|
|
115
|
+
m = lie.exp_adj(vec, a, LIB)
|
|
116
|
+
integ_m = -lie.exp_integ_adj(vec, -a, LIB)
|
|
117
|
+
|
|
118
|
+
return m @ lie.hat_adj(integ_m @ dvec, LIB)
|
|
119
|
+
|
|
120
|
+
def jac_lie_v_wrt_vector(lie, vec, a, v, LIB = 'numpy'):
|
|
121
|
+
m = lie.exp(vec, a, LIB)
|
|
122
|
+
integ_m = -lie.exp_integ_adj(vec, -a, LIB)
|
|
123
|
+
|
|
124
|
+
return m @ lie.hat_commute_adj(v, LIB) @ integ_m
|
|
125
|
+
|
|
126
|
+
def sympy_to_numpy(sp_mat):
|
|
127
|
+
return np.array(sp_mat).astype(np.float64)
|
|
128
|
+
|
|
129
|
+
def sympy_subs_mat(m, vec_str, vec_val):
|
|
130
|
+
for i in range(len(vec_str)):
|
|
131
|
+
m = m.subs([(vec_str[i], vec_val[i])])
|
|
132
|
+
|
|
133
|
+
return m
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
|
|
7
|
+
from .factorial_vec import *
|
|
8
|
+
|
|
9
|
+
class CMVector:
|
|
10
|
+
def __init__(self, vecs : Union[np.ndarray, jnp.ndarray]):
|
|
11
|
+
self._factorial_vector = FactorialVector(vecs)
|
|
12
|
+
self._n = self._factorial_vector._n
|
|
13
|
+
self._dim = self._factorial_vector._dim
|
|
14
|
+
self._len = self._factorial_vector._len
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def set_cmvecs(cm_vecs : Union[np.ndarray, jnp.ndarray]) -> 'CMVector':
|
|
18
|
+
factorial_vec = FactorialVector.set_ifac_vecs(cm_vecs)
|
|
19
|
+
return CMVector(factorial_vec.vecs())
|
|
20
|
+
|
|
21
|
+
def vecs(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
22
|
+
return self._factorial_vector.vecs()
|
|
23
|
+
|
|
24
|
+
def cm_vecs(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
25
|
+
return self._factorial_vector.ifac_vecs()
|
|
26
|
+
|
|
27
|
+
def vec(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
28
|
+
return self._factorial_vector.vec()
|
|
29
|
+
|
|
30
|
+
def cm_vec(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
31
|
+
return self._factorial_vector.ifac_vec()
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
return f"CMVector(n={self._n}, dim={self._dim}, len={self._len})\n{self.cm_vecs()}"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
|
|
7
|
+
class Factorial:
|
|
8
|
+
@classmethod
|
|
9
|
+
def mat(cls, n: int, dim: int) -> Union[np.ndarray, jnp.ndarray]:
|
|
10
|
+
length = n * dim
|
|
11
|
+
if isinstance(dim, int):
|
|
12
|
+
mat = np.eye(length)
|
|
13
|
+
else:
|
|
14
|
+
mat = jnp.eye(length)
|
|
15
|
+
|
|
16
|
+
for i in range(n):
|
|
17
|
+
mat[i*dim:(i+1)*dim, i*dim:(i+1)*dim] *= math.factorial(i)
|
|
18
|
+
return mat
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def mat_inv(cls, n: int, dim: int) -> Union[np.ndarray, jnp.ndarray]:
|
|
22
|
+
length = n * dim
|
|
23
|
+
if isinstance(dim, int):
|
|
24
|
+
mat = np.eye(length)
|
|
25
|
+
else:
|
|
26
|
+
mat = jnp.eye(length)
|
|
27
|
+
|
|
28
|
+
for i in range(n):
|
|
29
|
+
mat[i*dim:(i+1)*dim, i*dim:(i+1)*dim] /= math.factorial(i)
|
|
30
|
+
return mat
|
|
31
|
+
class FactorialVector:
|
|
32
|
+
def __init__(self, vecs : Union[np.ndarray, jnp.ndarray]):
|
|
33
|
+
self._n = vecs.shape[0]
|
|
34
|
+
self._dim = vecs.shape[1] if len(vecs.shape) > 1 else 1
|
|
35
|
+
self._len = vecs.flatten().shape[0]
|
|
36
|
+
self._vecs = vecs
|
|
37
|
+
self._factorial_mat = Factorial.mat(self._n, self._dim)
|
|
38
|
+
self._inverse_factorial_mat = Factorial.mat_inv(self._n, self._dim)
|
|
39
|
+
self._factorial_vecs = (self._factorial_mat @ vecs.flatten()).reshape(self._n, self._dim)
|
|
40
|
+
self._inverse_factorial_vecs = (self._inverse_factorial_mat @ vecs.flatten()).reshape(self._n, self._dim)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def set_fac_vecs(fac_vecs : Union[np.ndarray, jnp.ndarray]) -> 'FactorialVector':
|
|
44
|
+
n = fac_vecs.shape[0]
|
|
45
|
+
dim = fac_vecs.shape[1] if len(fac_vecs.shape) > 1 else 1
|
|
46
|
+
inverse_factorial_mat = Factorial.mat_inv(n, dim)
|
|
47
|
+
vecs = (inverse_factorial_mat @ fac_vecs.flatten()).reshape(n, dim)
|
|
48
|
+
return FactorialVector(vecs)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def set_ifac_vecs(ifac_vecs : Union[np.ndarray, jnp.ndarray]) -> 'FactorialVector':
|
|
52
|
+
n = ifac_vecs.shape[0]
|
|
53
|
+
dim = ifac_vecs.shape[1] if len(ifac_vecs.shape) > 1 else 1
|
|
54
|
+
factorial_mat = Factorial.mat(n, dim)
|
|
55
|
+
vecs = (factorial_mat @ ifac_vecs.flatten()).reshape(n, dim)
|
|
56
|
+
return FactorialVector(vecs)
|
|
57
|
+
|
|
58
|
+
def vecs(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
59
|
+
return self._vecs
|
|
60
|
+
|
|
61
|
+
def fac_vecs(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
62
|
+
return self._factorial_vecs
|
|
63
|
+
|
|
64
|
+
def ifac_vecs(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
65
|
+
return self._inverse_factorial_vecs
|
|
66
|
+
|
|
67
|
+
def vec(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
68
|
+
return self._vecs.flatten()
|
|
69
|
+
|
|
70
|
+
def fac_vec(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
71
|
+
return self._factorial_vecs.flatten()
|
|
72
|
+
|
|
73
|
+
def ifac_vec(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
74
|
+
return self._inverse_factorial_vecs.flatten()
|
|
75
|
+
|
|
76
|
+
def fac_mat(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
77
|
+
return self._factorial_mat
|
|
78
|
+
|
|
79
|
+
def ifac_mat(self) -> Union[np.ndarray, jnp.ndarray]:
|
|
80
|
+
return self._inverse_factorial_mat
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.interpolate import BSpline
|
|
3
|
+
|
|
4
|
+
# ----------------------------------------------------------------------
|
|
5
|
+
def build_bspline_model(knots, control, degree: int):
|
|
6
|
+
"""
|
|
7
|
+
Parameters
|
|
8
|
+
----------
|
|
9
|
+
knots : (m+1,) non-decreasing knot vector
|
|
10
|
+
control : (n, D) control-point matrix
|
|
11
|
+
degree : int spline order k
|
|
12
|
+
|
|
13
|
+
Returns
|
|
14
|
+
-------
|
|
15
|
+
curve(tq, order) -> (..., D)
|
|
16
|
+
jac (tq, order) -> (M*D, n*D)
|
|
17
|
+
"""
|
|
18
|
+
T = np.asarray(knots, dtype=np.float64)
|
|
19
|
+
P = np.asarray(control, dtype=np.float64)
|
|
20
|
+
n_ctrl, D = P.shape
|
|
21
|
+
k = degree
|
|
22
|
+
|
|
23
|
+
# --- dimension-wise splines ---------------------------------------
|
|
24
|
+
spl = [BSpline(T, P[:, d], k) for d in range(D)]
|
|
25
|
+
|
|
26
|
+
# cache derivatives up to order k
|
|
27
|
+
deriv_tbl = [[s.derivative(o) if o else s for o in range(k + 1)] for s in spl]
|
|
28
|
+
|
|
29
|
+
# basis splines for Jacobian
|
|
30
|
+
I = np.eye(n_ctrl)
|
|
31
|
+
bases = [BSpline(T, I[i], k, extrapolate=False) for i in range(n_ctrl)]
|
|
32
|
+
bases_der = [[b.derivative(o) if o else b for o in range(k + 1)]
|
|
33
|
+
for b in bases]
|
|
34
|
+
|
|
35
|
+
def curve(tq, order: int = 0):
|
|
36
|
+
if order < 0 or order > k:
|
|
37
|
+
raise ValueError(f"order must be 0-{k}")
|
|
38
|
+
tq = np.asarray(tq, dtype=np.float64)
|
|
39
|
+
return np.column_stack([tbl[order](tq) for tbl in deriv_tbl])
|
|
40
|
+
|
|
41
|
+
def jac(tq, order: int = 0):
|
|
42
|
+
if order < 0 or order > k:
|
|
43
|
+
raise ValueError(f"order must be 0-{k}")
|
|
44
|
+
tq = np.asarray(tq, dtype=np.float64).ravel()
|
|
45
|
+
M = tq.size
|
|
46
|
+
|
|
47
|
+
# derivative of basis (M, n_ctrl)
|
|
48
|
+
B = np.column_stack([bases_der[i][order](tq) for i in range(n_ctrl)])
|
|
49
|
+
return np.kron(B, np.eye(D)).reshape(M * D, n_ctrl * D)
|
|
50
|
+
|
|
51
|
+
return curve, jac
|
|
52
|
+
|
|
53
|
+
def bspline_curve(knots, control, degree, t, order=0):
|
|
54
|
+
"""
|
|
55
|
+
B-spline curve at evaluation points.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
knots : array_like, shape (m+1,)
|
|
60
|
+
Non-decreasing knot vector.
|
|
61
|
+
control : array_like, shape (n_ctrl, D)
|
|
62
|
+
Control point matrix.
|
|
63
|
+
degree : int
|
|
64
|
+
Spline degree.
|
|
65
|
+
t : array_like
|
|
66
|
+
Parameter values at which to evaluate.
|
|
67
|
+
order : int, optional
|
|
68
|
+
Derivative order (0 means curve value). Must be 0 <= order <= degree.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
curve_vals : ndarray, shape (len(t), D)
|
|
73
|
+
Evaluated curve or derivative.
|
|
74
|
+
"""
|
|
75
|
+
T = np.asarray(knots, dtype=np.float64, copy=False)
|
|
76
|
+
P = np.asarray(control, dtype=np.float64, copy=False)
|
|
77
|
+
_, D = P.shape
|
|
78
|
+
k = degree
|
|
79
|
+
|
|
80
|
+
if order < 0 or order > k:
|
|
81
|
+
raise ValueError(f"order must be between 0 and {k}")
|
|
82
|
+
|
|
83
|
+
t = np.asarray(t, dtype=np.float64, copy=False)
|
|
84
|
+
result = []
|
|
85
|
+
for d in range(D):
|
|
86
|
+
spline = BSpline(T, P[:, d], k, extrapolate=False)
|
|
87
|
+
if order > 0:
|
|
88
|
+
spline = spline.derivative(order)
|
|
89
|
+
result.append(spline(t))
|
|
90
|
+
return np.column_stack(result)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def bspline_jacobian(knots, n_ctrl, degree, t, order=0):
|
|
94
|
+
"""
|
|
95
|
+
Compute the Jacobian of the B-spline curve wrt control points.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
knots : array_like, shape (m+1,)
|
|
100
|
+
Non-decreasing knot vector.
|
|
101
|
+
control : array_like, shape (n_ctrl, D)
|
|
102
|
+
Control point matrix.
|
|
103
|
+
degree : int
|
|
104
|
+
Spline degree.
|
|
105
|
+
t : array_like
|
|
106
|
+
Parameter values at which to evaluate.
|
|
107
|
+
order : int, optional
|
|
108
|
+
Derivative order. Must be 0 <= order <= degree.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
J : ndarray, shape (len(t)*D, n_ctrl*D)
|
|
113
|
+
Jacobian matrix d(Curve)/d(control).
|
|
114
|
+
"""
|
|
115
|
+
T = np.asarray(knots, dtype=np.float64, copy=False)
|
|
116
|
+
k = degree
|
|
117
|
+
|
|
118
|
+
if order < 0 or order > k:
|
|
119
|
+
raise ValueError(f"order must be between 0 and {k}")
|
|
120
|
+
|
|
121
|
+
t = np.asarray(t, dtype=np.float64).ravel()
|
|
122
|
+
I = np.eye(n_ctrl)
|
|
123
|
+
bases = [BSpline(T, I[i], k, extrapolate=False) for i in range(n_ctrl)]
|
|
124
|
+
B = np.column_stack([
|
|
125
|
+
(bases[i].derivative(order) if order > 0 else bases[i])(t)
|
|
126
|
+
for i in range(n_ctrl)
|
|
127
|
+
]) # shape (len(t), n_ctrl)
|
|
128
|
+
|
|
129
|
+
J = B
|
|
130
|
+
return J
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ----------------------------------------------------------------------
|
|
135
|
+
def jac_numerical(curve, ctrl, knots, k, tq, h=1e-6, order=0):
|
|
136
|
+
"""
|
|
137
|
+
Finite-difference Jacobian for validation
|
|
138
|
+
curve(tq, order) : 0-k 階(k = degree)まで評価
|
|
139
|
+
ctrl : (n_ctrl, D) control-point matrix
|
|
140
|
+
knots : (m+1,) non-decreasing knot vector
|
|
141
|
+
k : int spline order k
|
|
142
|
+
tq : (M,) evaluation points
|
|
143
|
+
h : float finite-difference step size
|
|
144
|
+
order : int derivative order (0-k)
|
|
145
|
+
"""
|
|
146
|
+
n_ctrl, D = ctrl.shape
|
|
147
|
+
M = len(tq)
|
|
148
|
+
J_fd = np.empty((M * D, n_ctrl * D))
|
|
149
|
+
|
|
150
|
+
base = curve(tq, order).reshape(M * D)
|
|
151
|
+
|
|
152
|
+
for i in range(n_ctrl):
|
|
153
|
+
for d in range(D):
|
|
154
|
+
pert = ctrl.copy()
|
|
155
|
+
pert[i, d] += h
|
|
156
|
+
curve_p, _ = build_bspline_model(knots, pert, k)
|
|
157
|
+
col = (curve_p(tq, order).reshape(M * D) - base) / h
|
|
158
|
+
J_fd[:, i * D + d] = col
|
|
159
|
+
return J_fd
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
def build_integrator(dof, order, dt, method="euler"):
|
|
5
|
+
"""
|
|
6
|
+
Construct integration matrices for various numerical integration methods.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
dof : int
|
|
11
|
+
Degrees of freedom.
|
|
12
|
+
order : int
|
|
13
|
+
Order of the state (e.g., 2 for [q, dq], 3 for [q, dq, ddq]).
|
|
14
|
+
dt : float
|
|
15
|
+
Time step.
|
|
16
|
+
method : str
|
|
17
|
+
Integration method: "euler", "poly", "rk2", "rk4".
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
mat : ndarray of shape (dof * order, dof * order)
|
|
22
|
+
State transition matrix.
|
|
23
|
+
vec : ndarray of shape (dof * order, dof)
|
|
24
|
+
Control/integration vector matrix (for highest-order derivative input).
|
|
25
|
+
"""
|
|
26
|
+
dim = dof * order
|
|
27
|
+
A = np.eye(dim)
|
|
28
|
+
B = np.zeros((dim, dof))
|
|
29
|
+
|
|
30
|
+
if method == "euler":
|
|
31
|
+
# 1st-order approximation: x_{k+1} ≈ x_k + dt * dx_k
|
|
32
|
+
for i in range(order - 1):
|
|
33
|
+
A[i * dof:(i + 1) * dof, (i + 1) * dof:(i + 2) * dof] = dt * np.eye(dof)
|
|
34
|
+
B[(order - 1) * dof:] = dt * np.eye(dof)
|
|
35
|
+
|
|
36
|
+
elif method == "poly":
|
|
37
|
+
# Arbitrary-order Taylor expansion: dt^n / n!
|
|
38
|
+
for i in range(1, order):
|
|
39
|
+
for j in range(i):
|
|
40
|
+
power = i - j
|
|
41
|
+
coef = (dt ** power) / math.factorial(power)
|
|
42
|
+
A[j*dof:(j+1)*dof, i*dof:(i+1)*dof] = coef * np.eye(dof)
|
|
43
|
+
for i in range(order):
|
|
44
|
+
power = order - i
|
|
45
|
+
coef = (dt ** power) / math.factorial(power)
|
|
46
|
+
B[i*dof:(i+1)*dof] = coef * np.eye(dof)
|
|
47
|
+
|
|
48
|
+
elif method == "rk2":
|
|
49
|
+
# Midpoint method approximation (for 2nd-order accuracy)
|
|
50
|
+
# x(t+dt) ≈ x(t) + dt * f(x(t) + 0.5*dt*f(x))
|
|
51
|
+
for i in range(1, order):
|
|
52
|
+
for j in range(i):
|
|
53
|
+
coef = dt ** (i - j)
|
|
54
|
+
if i - j == 1:
|
|
55
|
+
coef += 0.5 * dt ** (i - j)
|
|
56
|
+
A[j*dof:(j+1)*dof, i*dof:(i+1)*dof] = coef * np.eye(dof)
|
|
57
|
+
|
|
58
|
+
for i in range(order):
|
|
59
|
+
coef = dt ** (order - i)
|
|
60
|
+
if order - i == 1:
|
|
61
|
+
coef += 0.5 * dt ** (order - i)
|
|
62
|
+
B[i*dof:(i+1)*dof] = coef * np.eye(dof)
|
|
63
|
+
|
|
64
|
+
elif method == "rk4":
|
|
65
|
+
# RK4 is not straightforwardly expressed in this matrix form,
|
|
66
|
+
# but we can approximate with higher-weighted Taylor expansion
|
|
67
|
+
coeffs = [1, 1/2, 1/6] # leading coefficients for 1st, 2nd, 3rd derivatives
|
|
68
|
+
for i in range(1, order):
|
|
69
|
+
for j in range(i):
|
|
70
|
+
power = i - j
|
|
71
|
+
if power <= len(coeffs):
|
|
72
|
+
coef = coeffs[power - 1] * dt ** power
|
|
73
|
+
A[j*dof:(j+1)*dof, i*dof:(i+1)*dof] = coef * np.eye(dof)
|
|
74
|
+
|
|
75
|
+
for i in range(order):
|
|
76
|
+
power = order - i
|
|
77
|
+
if power <= len(coeffs):
|
|
78
|
+
coef = coeffs[power - 1] * dt ** power
|
|
79
|
+
B[i*dof:(i+1)*dof] = coef * np.eye(dof)
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(f"Unsupported method: {method}")
|
|
83
|
+
|
|
84
|
+
return A, B
|