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.
Files changed (40) hide show
  1. mathrobo-0.0.1/.gitignore +4 -0
  2. mathrobo-0.0.1/LICENSE +21 -0
  3. mathrobo-0.0.1/PKG-INFO +59 -0
  4. mathrobo-0.0.1/README.md +40 -0
  5. mathrobo-0.0.1/mathrobo/__init__.py +8 -0
  6. mathrobo-0.0.1/mathrobo/basic/__init__.py +7 -0
  7. mathrobo-0.0.1/mathrobo/basic/basic.py +133 -0
  8. mathrobo-0.0.1/mathrobo/basic/cm_vec.py +34 -0
  9. mathrobo-0.0.1/mathrobo/basic/factorial_vec.py +80 -0
  10. mathrobo-0.0.1/mathrobo/calculus/__init__.py +7 -0
  11. mathrobo-0.0.1/mathrobo/calculus/bspline.py +159 -0
  12. mathrobo-0.0.1/mathrobo/calculus/integral.py +84 -0
  13. mathrobo-0.0.1/mathrobo/calculus/numerical_grad.py +111 -0
  14. mathrobo-0.0.1/mathrobo/lie/__init__.py +8 -0
  15. mathrobo-0.0.1/mathrobo/lie/cmtm_abst.py +561 -0
  16. mathrobo-0.0.1/mathrobo/lie/lie_abst.py +141 -0
  17. mathrobo-0.0.1/mathrobo/lie/se3.py +673 -0
  18. mathrobo-0.0.1/mathrobo/lie/so2.py +98 -0
  19. mathrobo-0.0.1/mathrobo/lie/so3.py +538 -0
  20. mathrobo-0.0.1/mathrobo/transformation/__init__.py +5 -0
  21. mathrobo-0.0.1/mathrobo/transformation/rotation.py +45 -0
  22. mathrobo-0.0.1/pyproject.toml +30 -0
  23. mathrobo-0.0.1/requirements.txt +5 -0
  24. mathrobo-0.0.1/tests/__init__.py +0 -0
  25. mathrobo-0.0.1/tests/basic/__init__.py +0 -0
  26. mathrobo-0.0.1/tests/basic/test_basic.py +0 -0
  27. mathrobo-0.0.1/tests/basic/test_factorial_vec.py +33 -0
  28. mathrobo-0.0.1/tests/calculus/__init__.py +0 -0
  29. mathrobo-0.0.1/tests/calculus/test_bspline.py +58 -0
  30. mathrobo-0.0.1/tests/calculus/test_build_integrator.py +61 -0
  31. mathrobo-0.0.1/tests/calculus/test_numerical_grad.py +45 -0
  32. mathrobo-0.0.1/tests/lie/__init__.py +0 -0
  33. mathrobo-0.0.1/tests/lie/test_cmtm_se3.py +478 -0
  34. mathrobo-0.0.1/tests/lie/test_cmtm_so3.py +407 -0
  35. mathrobo-0.0.1/tests/lie/test_jax_consistency.py +91 -0
  36. mathrobo-0.0.1/tests/lie/test_se3.py +422 -0
  37. mathrobo-0.0.1/tests/lie/test_so2.py +8 -0
  38. mathrobo-0.0.1/tests/lie/test_so3.py +214 -0
  39. mathrobo-0.0.1/tests/lie/test_so3_jax_grad.py +24 -0
  40. mathrobo-0.0.1/uv.lock +833 -0
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+
3
+ /mrenv
4
+ .python-version
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.
@@ -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.
@@ -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,8 @@
1
+ #!/usr/bin/env python3.9
2
+ # -*- coding: utf-8 -*-
3
+ # 2024.06.23 Created by T.Ishigaki
4
+
5
+ from .basic import *
6
+ from .lie import *
7
+ from .calculus import *
8
+ from .transformation import *
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3.9
2
+ # -*- coding: utf-8 -*-
3
+ # 2024.08.17 Created by T.Ishigaki
4
+
5
+ from .basic import *
6
+ from .factorial_vec import *
7
+ from .cm_vec import *
@@ -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,7 @@
1
+ #!/usr/bin/env python3.9
2
+ # -*- coding: utf-8 -*-
3
+ # 2025.05.08 Created by T.Ishigaki
4
+
5
+ from .integral import *
6
+ from .numerical_grad import *
7
+ from .bspline import *
@@ -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