rational-linkages 2.0.0__cp311-cp311-macosx_12_0_arm64.whl → 2.2.3__cp311-cp311-macosx_12_0_arm64.whl
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.
- rational_linkages/CollisionAnalyser.py +323 -21
- rational_linkages/CollisionFreeOptimization.py +8 -4
- rational_linkages/DualQuaternion.py +5 -2
- rational_linkages/ExudynAnalysis.py +2 -1
- rational_linkages/FactorizationProvider.py +6 -5
- rational_linkages/MiniBall.py +9 -2
- rational_linkages/MotionApproximation.py +7 -3
- rational_linkages/MotionDesigner.py +553 -540
- rational_linkages/MotionFactorization.py +6 -5
- rational_linkages/MotionInterpolation.py +7 -7
- rational_linkages/NormalizedLine.py +1 -1
- rational_linkages/NormalizedPlane.py +1 -1
- rational_linkages/Plotter.py +1 -1
- rational_linkages/PlotterMatplotlib.py +27 -13
- rational_linkages/PlotterPyqtgraph.py +596 -534
- rational_linkages/PointHomogeneous.py +6 -3
- rational_linkages/RationalBezier.py +64 -4
- rational_linkages/RationalCurve.py +13 -5
- rational_linkages/RationalDualQuaternion.py +5 -4
- rational_linkages/RationalMechanism.py +48 -33
- rational_linkages/SingularityAnalysis.py +4 -5
- rational_linkages/StaticMechanism.py +4 -5
- rational_linkages/__init__.py +3 -2
- rational_linkages/utils.py +60 -3
- rational_linkages/utils_rust.cpython-311-darwin.so +0 -0
- {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/METADATA +32 -18
- rational_linkages-2.2.3.dist-info/RECORD +40 -0
- rational_linkages-2.0.0.dist-info/RECORD +0 -40
- {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/WHEEL +0 -0
- {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/licenses/LICENSE +0 -0
- {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
import numpy as np
|
2
|
-
|
3
1
|
from typing import Optional, Sequence
|
2
|
+
|
3
|
+
import numpy as np
|
4
4
|
from sympy import Rational
|
5
5
|
|
6
6
|
from .TransfMatrix import TransfMatrix
|
@@ -332,7 +332,7 @@ class PointHomogeneous:
|
|
332
332
|
:return: evaluated point with float elements
|
333
333
|
:rtype: PointHomogeneous
|
334
334
|
"""
|
335
|
-
from sympy import Expr,
|
335
|
+
from sympy import Expr, Number, Symbol
|
336
336
|
|
337
337
|
t = Symbol("t")
|
338
338
|
|
@@ -386,6 +386,9 @@ class PointOrbit:
|
|
386
386
|
|
387
387
|
self.t_interval = t_interval
|
388
388
|
|
389
|
+
def __repr__(self):
|
390
|
+
return f"PointOrbit(center={self.center}, radius_squared={self.radius_squared}, t_interval={self.t_interval})"
|
391
|
+
|
389
392
|
@property
|
390
393
|
def radius(self):
|
391
394
|
if self._radius is None:
|
@@ -2,12 +2,11 @@ from copy import deepcopy
|
|
2
2
|
|
3
3
|
import numpy as np
|
4
4
|
import sympy as sp
|
5
|
-
from sympy.integrals.quadrature import gauss_legendre
|
6
5
|
|
6
|
+
from .DualQuaternion import DualQuaternion
|
7
7
|
from .MiniBall import MiniBall
|
8
8
|
from .PointHomogeneous import PointHomogeneous
|
9
9
|
from .RationalCurve import RationalCurve
|
10
|
-
from .DualQuaternion import DualQuaternion
|
11
10
|
|
12
11
|
|
13
12
|
class RationalBezier(RationalCurve):
|
@@ -93,7 +92,7 @@ class RationalBezier(RationalCurve):
|
|
93
92
|
"""
|
94
93
|
Get the numerical coefficients of the Bezier curve
|
95
94
|
"""
|
96
|
-
from scipy.special import comb #
|
95
|
+
from scipy.special import comb # lazy import
|
97
96
|
|
98
97
|
control_pts = np.array([point.array() for point in control_points])
|
99
98
|
degree = len(control_points) - 1
|
@@ -210,7 +209,7 @@ class BezierSegment:
|
|
210
209
|
|
211
210
|
@metric.setter
|
212
211
|
def metric(self, metric: "AffineMetric"):
|
213
|
-
from .AffineMetric import AffineMetric #
|
212
|
+
from .AffineMetric import AffineMetric # lazy import
|
214
213
|
|
215
214
|
if isinstance(metric, AffineMetric):
|
216
215
|
self._metric = metric
|
@@ -336,6 +335,52 @@ class RationalSoo(RationalCurve):
|
|
336
335
|
|
337
336
|
return [sp.Poly(gl_curve[i], t, greedy=False) for i in range(dim)]
|
338
337
|
|
338
|
+
@classmethod
|
339
|
+
def from_two_points(cls,
|
340
|
+
p0: PointHomogeneous,
|
341
|
+
p1: PointHomogeneous,
|
342
|
+
degree: int = 2) -> "RationalSoo":
|
343
|
+
"""
|
344
|
+
Create a RationalSoo curve from two points.
|
345
|
+
|
346
|
+
The other control points will be added based on the given degree.
|
347
|
+
|
348
|
+
:param PointHomogeneous p0: first point
|
349
|
+
:param PointHomogeneous p1: second point
|
350
|
+
:param int degree: degree of the curve (default is 2)
|
351
|
+
|
352
|
+
:return: the resulting Gauss-Legendre curve
|
353
|
+
:rtype: RationalSoo
|
354
|
+
"""
|
355
|
+
control_points = RationalSoo.control_points_between_two_points(p0, p1, degree)
|
356
|
+
return cls(control_points)
|
357
|
+
|
358
|
+
@staticmethod
|
359
|
+
def control_points_between_two_points(p0: PointHomogeneous,
|
360
|
+
p1: PointHomogeneous,
|
361
|
+
degree: int = 2) -> list[PointHomogeneous]:
|
362
|
+
"""
|
363
|
+
Generate control points for a Gauss-Legendre curve between two points.
|
364
|
+
|
365
|
+
:param PointHomogeneous p0: first point
|
366
|
+
:param PointHomogeneous p1: second point
|
367
|
+
:param int degree: degree of the curve (default is 2)
|
368
|
+
|
369
|
+
:return: list of control points
|
370
|
+
:rtype: list[PointHomogeneous]
|
371
|
+
"""
|
372
|
+
if degree < 2:
|
373
|
+
raise ValueError("Degree must be at least 2 for a Gauss-Legendre curve.")
|
374
|
+
|
375
|
+
control_points = [p0]
|
376
|
+
for i in range(degree - 1):
|
377
|
+
# create intermediate control points
|
378
|
+
control_points.append(p0.linear_interpolation(p1, (i + 1) / degree))
|
379
|
+
|
380
|
+
control_points.append(p1)
|
381
|
+
|
382
|
+
return control_points
|
383
|
+
|
339
384
|
@staticmethod
|
340
385
|
def lagrange_basis(tau, symbol, weights):
|
341
386
|
"""
|
@@ -359,3 +404,18 @@ class RationalSoo(RationalCurve):
|
|
359
404
|
basis.append(basis_j / weights[j])
|
360
405
|
|
361
406
|
return basis
|
407
|
+
|
408
|
+
def get_plot_data(self,
|
409
|
+
interval: tuple = (-1, 1),
|
410
|
+
steps: int = 50) -> tuple:
|
411
|
+
"""
|
412
|
+
Get the data to plot the curve in 3D.
|
413
|
+
"""
|
414
|
+
# perform superclass coordinates
|
415
|
+
x, y, z = super().get_plot_data(interval=interval)
|
416
|
+
|
417
|
+
points = [point.normalized_in_3d() for point in self.control_points]
|
418
|
+
|
419
|
+
x_cp, y_cp, z_cp = zip(*points)
|
420
|
+
|
421
|
+
return x, y, z, x_cp, y_cp, z_cp
|
@@ -1,13 +1,11 @@
|
|
1
1
|
from copy import deepcopy
|
2
2
|
from typing import Union
|
3
3
|
|
4
|
-
from scipy.integrate import quad
|
5
|
-
|
6
4
|
import numpy as np
|
7
5
|
import sympy as sp
|
8
6
|
|
9
|
-
from .PointHomogeneous import PointHomogeneous
|
10
7
|
from .DualQuaternion import DualQuaternion
|
8
|
+
from .PointHomogeneous import PointHomogeneous
|
11
9
|
from .Quaternion import Quaternion
|
12
10
|
|
13
11
|
MotionFactorization = "MotionFactorization"
|
@@ -137,7 +135,7 @@ class RationalCurve:
|
|
137
135
|
|
138
136
|
@metric.setter
|
139
137
|
def metric(self, metric: "AffineMetric"):
|
140
|
-
from .AffineMetric import AffineMetric #
|
138
|
+
from .AffineMetric import AffineMetric # lazy import
|
141
139
|
|
142
140
|
if isinstance(metric, AffineMetric):
|
143
141
|
self._metric = metric
|
@@ -574,7 +572,7 @@ class RationalCurve:
|
|
574
572
|
if not self.is_motion:
|
575
573
|
raise ValueError("Not a motion curve, cannot split into Bezier curves.")
|
576
574
|
|
577
|
-
from .RationalBezier import BezierSegment #
|
575
|
+
from .RationalBezier import BezierSegment # lazy import
|
578
576
|
|
579
577
|
curve = self.get_curve_in_pr12()
|
580
578
|
|
@@ -658,6 +656,11 @@ class RationalCurve:
|
|
658
656
|
:raises ValueError: if the interval values are identical
|
659
657
|
:raises ValueError: if the number of segments is less than 1
|
660
658
|
"""
|
659
|
+
try:
|
660
|
+
from scipy.integrate import quad # lazy import
|
661
|
+
except ImportError:
|
662
|
+
raise RuntimeError("Scipy import failed. Check the package installation.")
|
663
|
+
|
661
664
|
if interval[0] > interval[1]:
|
662
665
|
raise ValueError("The interval must be in the form [a, b] where a < b")
|
663
666
|
elif interval[0] == interval[1]:
|
@@ -709,6 +712,11 @@ class RationalCurve:
|
|
709
712
|
:return: t value that splits the curve into given segment length
|
710
713
|
:rtype: float
|
711
714
|
"""
|
715
|
+
try:
|
716
|
+
from scipy.integrate import quad # lazy import
|
717
|
+
except ImportError:
|
718
|
+
raise RuntimeError("Scipy import failed. Check the package installation.")
|
719
|
+
|
712
720
|
# initial lower and upper bounds
|
713
721
|
low = section_start
|
714
722
|
high = curve_interval[1] # start with the upper bound
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import numpy as np
|
2
|
-
|
2
|
+
|
3
|
+
from sympy import Rational
|
3
4
|
|
4
5
|
from .DualQuaternion import DualQuaternion
|
5
6
|
|
@@ -12,7 +13,7 @@ class RationalDualQuaternion(DualQuaternion):
|
|
12
13
|
"""
|
13
14
|
RationalDualQuaternion class representing a 8-dimensional dual quaternion.
|
14
15
|
"""
|
15
|
-
def __init__(self, study_parameters: list[
|
16
|
+
def __init__(self, study_parameters: list[Rational]):
|
16
17
|
"""
|
17
18
|
RationalDualQuaternion class
|
18
19
|
|
@@ -34,7 +35,7 @@ class RationalDualQuaternion(DualQuaternion):
|
|
34
35
|
"""
|
35
36
|
return f"{self.rational_numbers}"
|
36
37
|
|
37
|
-
def __getitem__(self, idx) ->
|
38
|
+
def __getitem__(self, idx) -> Rational:
|
38
39
|
"""
|
39
40
|
Get an element of DualQuaternion
|
40
41
|
|
@@ -50,6 +51,6 @@ class RationalDualQuaternion(DualQuaternion):
|
|
50
51
|
Get the array of the rational numbers
|
51
52
|
|
52
53
|
:return: Rational numbers
|
53
|
-
:rtype:
|
54
|
+
:rtype: sympy.Matrix
|
54
55
|
"""
|
55
56
|
return np.array(self.rational_numbers)
|
@@ -8,12 +8,12 @@ import numpy as np
|
|
8
8
|
import sympy as sp
|
9
9
|
|
10
10
|
from .DualQuaternion import DualQuaternion
|
11
|
+
from .Linkage import LineSegment
|
11
12
|
from .MotionFactorization import MotionFactorization
|
12
13
|
from .NormalizedLine import NormalizedLine
|
14
|
+
from .PointHomogeneous import PointHomogeneous
|
13
15
|
from .RationalCurve import RationalCurve
|
14
16
|
from .TransfMatrix import TransfMatrix
|
15
|
-
from .PointHomogeneous import PointHomogeneous
|
16
|
-
from .Linkage import LineSegment
|
17
17
|
|
18
18
|
|
19
19
|
class RationalMechanism(RationalCurve):
|
@@ -110,7 +110,7 @@ class RationalMechanism(RationalCurve):
|
|
110
110
|
This metric is used for collision detection.
|
111
111
|
"""
|
112
112
|
if self._metric is None:
|
113
|
-
from .AffineMetric import AffineMetric #
|
113
|
+
from .AffineMetric import AffineMetric # lazy import
|
114
114
|
mechanism_points = self.points_at_parameter(0,
|
115
115
|
inverted_part=True,
|
116
116
|
only_links=False)
|
@@ -310,18 +310,18 @@ class RationalMechanism(RationalCurve):
|
|
310
310
|
if onshape_print:
|
311
311
|
for i in range(self.num_joints):
|
312
312
|
print(f"link{i}: "
|
313
|
-
f"[{dh[i, 1]:.
|
314
|
-
f"{design_params[i, 0]:.
|
313
|
+
f"[{dh[i, 1]:.15f}, {dh[i, 2]:.15f}, {dh[i, 3]:.15f}], "
|
314
|
+
f"{design_params[i, 0]:.15f}, {design_params[i, 1]:.15f}")
|
315
315
|
pretty_print = False
|
316
316
|
|
317
317
|
if pretty_print:
|
318
318
|
for i in range(self.num_joints):
|
319
319
|
print("---")
|
320
|
-
print(f"Link {i}: d = {dh[i, 1]:.
|
321
|
-
f"a = {dh[i, 2]:.
|
322
|
-
f"alpha = {dh[i, 3]:.
|
323
|
-
print(f"cp_0 = {design_params[i, 0]:.
|
324
|
-
f"cp_1 = {design_params[i, 1]:.
|
320
|
+
print(f"Link {i}: d = {dh[i, 1]:.15f}, "
|
321
|
+
f"a = {dh[i, 2]:.15f}, "
|
322
|
+
f"alpha = {dh[i, 3]:.15f}")
|
323
|
+
print(f"cp_0 = {design_params[i, 0]:.15f}, "
|
324
|
+
f"cp_1 = {design_params[i, 1]:.15f}")
|
325
325
|
|
326
326
|
return dh, design_params, design_points
|
327
327
|
|
@@ -400,7 +400,7 @@ class RationalMechanism(RationalCurve):
|
|
400
400
|
:return: list of TransfMatrix objects
|
401
401
|
:rtype: list[TransfMatrix]
|
402
402
|
"""
|
403
|
-
from .TransfMatrix import TransfMatrix #
|
403
|
+
from .TransfMatrix import TransfMatrix # lazy import
|
404
404
|
|
405
405
|
screws = deepcopy(self.get_screw_axes())
|
406
406
|
|
@@ -942,7 +942,7 @@ class RationalMechanism(RationalCurve):
|
|
942
942
|
"""
|
943
943
|
Perform singularity check of the mechanism.
|
944
944
|
"""
|
945
|
-
from .SingularityAnalysis import SingularityAnalysis #
|
945
|
+
from .SingularityAnalysis import SingularityAnalysis # lazy import
|
946
946
|
|
947
947
|
sa = SingularityAnalysis()
|
948
948
|
return sa.check_singularity(self)
|
@@ -957,7 +957,7 @@ class RationalMechanism(RationalCurve):
|
|
957
957
|
result of the optimization
|
958
958
|
:rtype: list, list, float
|
959
959
|
"""
|
960
|
-
from .CollisionFreeOptimization import CollisionFreeOptimization #
|
960
|
+
from .CollisionFreeOptimization import CollisionFreeOptimization # lazy import
|
961
961
|
|
962
962
|
# get smallest polyline
|
963
963
|
pts, points_params, res = CollisionFreeOptimization(self).smallest_polyline()
|
@@ -1087,7 +1087,8 @@ class RationalMechanism(RationalCurve):
|
|
1087
1087
|
Calculate inverse kinematics for given pose. Returns the joint angle in radians.
|
1088
1088
|
|
1089
1089
|
:param Union[DualQuaternion, TransfMatrix] pose: pose of the mechanism
|
1090
|
-
:param str unit: unit of the joint angle, can be 'rad' or '
|
1090
|
+
:param str unit: unit of the joint angle, can be 'rad', 'deg', or 't' as
|
1091
|
+
t is the parameter of the motion curve. Default is 'rad'.
|
1091
1092
|
:param str method: numerically for 'gauss-newton' or 'algebraic'; 'algebraic'
|
1092
1093
|
requires the input pose to be "achievable" by the mechanism, i.e. the pose
|
1093
1094
|
must be on Study quadric and the mechanism must be able to reach it
|
@@ -1102,7 +1103,7 @@ class RationalMechanism(RationalCurve):
|
|
1102
1103
|
elif not isinstance(pose, DualQuaternion):
|
1103
1104
|
raise ValueError("pose must be either DualQuaternion or TransfMatrix")
|
1104
1105
|
|
1105
|
-
if unit
|
1106
|
+
if unit not in {'rad', 'deg', 't'}:
|
1106
1107
|
raise ValueError("unit must be deg or rad")
|
1107
1108
|
|
1108
1109
|
if method == 'algebraic':
|
@@ -1113,12 +1114,13 @@ class RationalMechanism(RationalCurve):
|
|
1113
1114
|
else:
|
1114
1115
|
raise ValueError("method must be either 'algebraic' or 'gauss-newton")
|
1115
1116
|
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
joint_angle =
|
1120
|
-
|
1121
|
-
|
1117
|
+
if unit == 't':
|
1118
|
+
return t
|
1119
|
+
else:
|
1120
|
+
joint_angle = self.factorizations[0].t_param_to_joint_angle(t)
|
1121
|
+
if unit == 'deg':
|
1122
|
+
joint_angle = np.rad2deg(joint_angle)
|
1123
|
+
return joint_angle
|
1122
1124
|
|
1123
1125
|
def _ik_gauss_newton(self,
|
1124
1126
|
goal_pose: DualQuaternion,
|
@@ -1152,6 +1154,7 @@ class RationalMechanism(RationalCurve):
|
|
1152
1154
|
if robust:
|
1153
1155
|
t_init_set = np.linspace(-1.0, 1.0, 30)
|
1154
1156
|
max_iterations = 50
|
1157
|
+
tol = 1e-15
|
1155
1158
|
|
1156
1159
|
for inv, curve in enumerate(curves):
|
1157
1160
|
if inv == 1:
|
@@ -1162,23 +1165,35 @@ class RationalMechanism(RationalCurve):
|
|
1162
1165
|
|
1163
1166
|
c_diff = [element.diff(t) for element in norm_curve]
|
1164
1167
|
|
1168
|
+
# numerical preparation of the derivatives
|
1169
|
+
c_diff_funcs = [sp.lambdify(t, expr, modules='numpy')
|
1170
|
+
for expr in c_diff]
|
1171
|
+
def c_diff_lambdified(x: float):
|
1172
|
+
return np.array([f(x) for f in c_diff_funcs])
|
1173
|
+
|
1174
|
+
curve_funcs = [sp.lambdify(t, expr, modules='numpy')
|
1175
|
+
for expr in curve.symbolic]
|
1176
|
+
def curve_lambdified(x: float):
|
1177
|
+
return np.array([f(x) for f in curve_funcs])
|
1178
|
+
|
1165
1179
|
for t_val in t_init_set:
|
1166
1180
|
step_size = 1.0
|
1167
1181
|
previous_error = float('inf')
|
1168
1182
|
|
1169
1183
|
for i in range(max_iterations):
|
1170
1184
|
|
1171
|
-
if
|
1172
|
-
|
1173
|
-
|
1185
|
+
# check if t_val is valid, i.e. must be in the range [-1, 1]
|
1186
|
+
if (t_val == sp.nan or np.isnan(t_val) or t_val > 10.0
|
1187
|
+
or t_val < -10.0):
|
1188
|
+
break
|
1174
1189
|
|
1175
1190
|
target_pose = pose.array()
|
1176
|
-
current_pose =
|
1177
|
-
c_diff_eval =
|
1178
|
-
for element in c_diff])
|
1191
|
+
current_pose = curve_lambdified(t_val)
|
1192
|
+
c_diff_eval = c_diff_lambdified(t_val)
|
1179
1193
|
|
1180
1194
|
# error to desired pose
|
1181
|
-
if (target_pose[0]
|
1195
|
+
if (np.isclose(target_pose[0], 0.0)
|
1196
|
+
or np.isclose(current_pose[0], 0.0)):
|
1182
1197
|
twist_to_desired = target_pose - current_pose
|
1183
1198
|
else:
|
1184
1199
|
twist_to_desired = (target_pose / target_pose[0]
|
@@ -1211,7 +1226,7 @@ class RationalMechanism(RationalCurve):
|
|
1211
1226
|
t_res = t_min[0]
|
1212
1227
|
|
1213
1228
|
if inversed_part:
|
1214
|
-
if t_res
|
1229
|
+
if np.isclose(t_res, 0.0):
|
1215
1230
|
t_res = np.finfo(np.float64).tiny
|
1216
1231
|
t_res = 1 / t_res
|
1217
1232
|
|
@@ -1473,8 +1488,8 @@ class RationalMechanism(RationalCurve):
|
|
1473
1488
|
self._segments = self._get_line_segments_of_linkage()
|
1474
1489
|
|
1475
1490
|
def relative_motion(self,
|
1476
|
-
static:
|
1477
|
-
moving:
|
1491
|
+
static: int,
|
1492
|
+
moving: int) -> DualQuaternion:
|
1478
1493
|
"""
|
1479
1494
|
Calculate the relative motion between given pair of links or joints.
|
1480
1495
|
|
@@ -1487,8 +1502,8 @@ class RationalMechanism(RationalCurve):
|
|
1487
1502
|
if static == moving:
|
1488
1503
|
raise ValueError("static and moving cannot be the same")
|
1489
1504
|
|
1490
|
-
motion_cycle = self._shortest_path(static
|
1491
|
-
|
1505
|
+
motion_cycle = self._shortest_path(static, moving)
|
1506
|
+
|
1492
1507
|
rel_motion = DualQuaternion()
|
1493
1508
|
for idx in motion_cycle:
|
1494
1509
|
rel_motion *= self.linear_motions_cycle[idx]
|
@@ -1,7 +1,7 @@
|
|
1
|
-
from
|
2
|
-
from .Linkage import LineSegment
|
1
|
+
from sympy import Matrix
|
3
2
|
|
4
|
-
import
|
3
|
+
from .Linkage import LineSegment
|
4
|
+
from .RationalMechanism import RationalMechanism
|
5
5
|
|
6
6
|
|
7
7
|
class SingularityAnalysis:
|
@@ -49,8 +49,7 @@ class SingularityAnalysis:
|
|
49
49
|
# normalization
|
50
50
|
|
51
51
|
|
52
|
-
|
53
|
-
jacobian = sympy.Matrix.zeros(6, len(algebraic_plucker_coords))
|
52
|
+
jacobian = Matrix.zeros(6, len(algebraic_plucker_coords))
|
54
53
|
for i, plucker_line in enumerate(algebraic_plucker_coords):
|
55
54
|
jacobian[:, i] = plucker_line.screw
|
56
55
|
|
@@ -1,15 +1,14 @@
|
|
1
|
-
from warnings import warn
|
2
1
|
from typing import Union
|
2
|
+
from warnings import warn
|
3
3
|
|
4
4
|
import numpy as np
|
5
5
|
|
6
|
-
from .RationalMechanism import RationalMechanism
|
7
|
-
from .MotionFactorization import MotionFactorization
|
8
6
|
from .DualQuaternion import DualQuaternion
|
7
|
+
from .MotionFactorization import MotionFactorization
|
9
8
|
from .NormalizedLine import NormalizedLine
|
10
|
-
from .TransfMatrix import TransfMatrix
|
11
9
|
from .PointHomogeneous import PointHomogeneous
|
12
|
-
|
10
|
+
from .RationalMechanism import RationalMechanism
|
11
|
+
from .TransfMatrix import TransfMatrix
|
13
12
|
from .utils import dq_algebraic2vector
|
14
13
|
|
15
14
|
|
rational_linkages/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# __init__.py
|
2
2
|
|
3
|
-
from importlib.metadata import
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
4
|
+
|
4
5
|
try:
|
5
6
|
__version__ = version("rational_linkages")
|
6
7
|
except PackageNotFoundError:
|
@@ -19,7 +20,7 @@ from .NormalizedPlane import NormalizedPlane
|
|
19
20
|
from .Plotter import Plotter
|
20
21
|
from .PointHomogeneous import PointHomogeneous
|
21
22
|
from .Quaternion import Quaternion
|
22
|
-
from .RationalBezier import
|
23
|
+
from .RationalBezier import BezierSegment, RationalBezier
|
23
24
|
from .RationalCurve import RationalCurve
|
24
25
|
from .RationalDualQuaternion import RationalDualQuaternion
|
25
26
|
from .RationalMechanism import RationalMechanism
|
rational_linkages/utils.py
CHANGED
@@ -13,7 +13,7 @@ def dq_algebraic2vector(ugly_expression: list) -> list:
|
|
13
13
|
:return: 8-vector representation of the algebraic equation
|
14
14
|
:rtype: list
|
15
15
|
"""
|
16
|
-
from sympy import
|
16
|
+
from sympy import expand, symbols # lazy import
|
17
17
|
i, j, k, epsilon = symbols('i j k epsilon')
|
18
18
|
|
19
19
|
expr = expand(ugly_expression)
|
@@ -41,7 +41,7 @@ def extract_coeffs(expr, var, deg: int, expand: bool = True):
|
|
41
41
|
:rtype: list
|
42
42
|
"""
|
43
43
|
if expand:
|
44
|
-
from sympy import expand #
|
44
|
+
from sympy import expand # lazy import
|
45
45
|
expr = expand(expr)
|
46
46
|
return [expr.coeff(var, i) for i in range(deg, -1, -1)]
|
47
47
|
|
@@ -97,10 +97,67 @@ def is_package_installed(package_name: str) -> bool:
|
|
97
97
|
"""
|
98
98
|
Check if a package is installed.
|
99
99
|
"""
|
100
|
-
from importlib.metadata import distribution
|
100
|
+
from importlib.metadata import distribution # lazy import
|
101
101
|
|
102
102
|
try:
|
103
103
|
distribution(package_name)
|
104
104
|
return True
|
105
105
|
except ImportError:
|
106
106
|
return False
|
107
|
+
|
108
|
+
|
109
|
+
def tr_from_dh_rationally(t_theta, di, ai, t_alpha):
|
110
|
+
"""
|
111
|
+
Create transformation matrix from DH parameters using Sympy in rational form.
|
112
|
+
|
113
|
+
The input shall be rational numbers, including the angles which are expected
|
114
|
+
to be parameters of tangent half-angle substitution, i.e., t_theta = tan(theta/2)
|
115
|
+
and t_alpha = tan(alpha/2).
|
116
|
+
|
117
|
+
:param sp.Rational t_theta: DH parameter theta in tangent half-angle form
|
118
|
+
:param sp.Rational di: DH parameter d, the offset along Z axis
|
119
|
+
:param sp.Rational ai: DH parameter a, the length along X axis
|
120
|
+
:param sp.Rational t_alpha: DH parameter alpha in tangent half-angle form
|
121
|
+
|
122
|
+
:return: 4x4 transformation matrix
|
123
|
+
:rtype: sp.Matrix
|
124
|
+
"""
|
125
|
+
from sympy import Matrix, eye, Expr # lazy import
|
126
|
+
|
127
|
+
if not all(isinstance(param, Expr) for param in [t_theta, di, ai, t_alpha]):
|
128
|
+
raise ValueError("All parameters must be of type sympy objects (Expr).")
|
129
|
+
|
130
|
+
s_th = 2*t_theta / (1 + t_theta**2)
|
131
|
+
c_th = (1 - t_theta**2) / (1 + t_theta**2)
|
132
|
+
s_al = 2*t_alpha / (1 + t_alpha**2)
|
133
|
+
c_al = (1 - t_alpha**2) / (1 + t_alpha**2)
|
134
|
+
|
135
|
+
mat = eye(4)
|
136
|
+
mat[1:4, 0] = Matrix([ai * c_th, ai * s_th, di])
|
137
|
+
mat[1, 1:4] = Matrix([[c_th, -s_th * c_al, s_th * s_al]])
|
138
|
+
mat[2, 1:4] = Matrix([[s_th, c_th * c_al, -c_th * s_al]])
|
139
|
+
mat[3, 1:4] = Matrix([[0, s_al, c_al]])
|
140
|
+
return mat
|
141
|
+
|
142
|
+
|
143
|
+
def normalized_line_rationally(point, direction):
|
144
|
+
"""
|
145
|
+
Create a normalized Plücker line from a point and a direction using Sympy.
|
146
|
+
|
147
|
+
The input shall be rational numbers, i.e. Sympy objects.
|
148
|
+
|
149
|
+
:param sp.Rational point:
|
150
|
+
:param sp.Rational direction:
|
151
|
+
|
152
|
+
:return: 6-vector representing the Plücker line
|
153
|
+
:rtype: sp.Matrix
|
154
|
+
"""
|
155
|
+
from sympy import Matrix, Expr # lazy import
|
156
|
+
|
157
|
+
if not all(isinstance(param, Expr) for param in point + direction):
|
158
|
+
raise ValueError("All parameters must be of type sympy objects (Expr).")
|
159
|
+
|
160
|
+
dir = Matrix(direction)
|
161
|
+
pt = Matrix(point)
|
162
|
+
mom = (-1 * dir).cross(pt)
|
163
|
+
return Matrix.vstack(dir, mom)
|
Binary file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: rational-linkages
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.2.3
|
4
4
|
Summary: Rational Linkages
|
5
5
|
Author-email: Daniel Huczala <daniel.huczala@uibk.ac.at>
|
6
6
|
License-Expression: GPL-3.0-or-later
|
@@ -14,15 +14,17 @@ Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
15
15
|
License-File: LICENSE
|
16
16
|
Requires-Dist: biquaternion-py>=1.2.0
|
17
|
-
Requires-Dist:
|
17
|
+
Requires-Dist: numpy>=1.10.0
|
18
18
|
Requires-Dist: sympy>=1.10.0
|
19
19
|
Requires-Dist: PyQt6>=6.2.0
|
20
20
|
Requires-Dist: pyqtgraph>=0.12.4
|
21
21
|
Requires-Dist: PyOpenGL>=3.0.0
|
22
|
+
Requires-Dist: matplotlib>=3.9.0; platform_system == "Windows" and platform_machine == "ARM64"
|
22
23
|
Provides-Extra: opt
|
23
24
|
Requires-Dist: ipython>=8.0.0; extra == "opt"
|
24
|
-
Requires-Dist:
|
25
|
+
Requires-Dist: scipy>=1.10.0; extra == "opt"
|
25
26
|
Requires-Dist: matplotlib>=3.9.0; extra == "opt"
|
27
|
+
Requires-Dist: gmpy2>=2.2.0; (platform_system != "Windows" and platform_machine != "ARM64") and extra == "opt"
|
26
28
|
Provides-Extra: exu
|
27
29
|
Requires-Dist: exudyn>=1.9.0; extra == "exu"
|
28
30
|
Requires-Dist: numpy-stl>=3.0.0; extra == "exu"
|
@@ -38,6 +40,7 @@ Requires-Dist: sphinx-hoverxref; extra == "doc"
|
|
38
40
|
Requires-Dist: gitpython; extra == "doc"
|
39
41
|
Provides-Extra: dev
|
40
42
|
Requires-Dist: build; extra == "dev"
|
43
|
+
Requires-Dist: cibuildwheel; extra == "dev"
|
41
44
|
Requires-Dist: coverage; extra == "dev"
|
42
45
|
Requires-Dist: pytest; extra == "dev"
|
43
46
|
Requires-Dist: flake8; extra == "dev"
|
@@ -112,17 +115,28 @@ Using pip:
|
|
112
115
|
|
113
116
|
<code>pip install rational-linkages</code>
|
114
117
|
|
115
|
-
or
|
118
|
+
or with optional dependencies:
|
116
119
|
|
117
|
-
<code>pip install rational-linkages[opt]</code>
|
120
|
+
<code>pip install rational-linkages[opt,exu]</code>
|
118
121
|
|
119
|
-
Mac users might need to use backslashes to escape the brackets, e.g.:
|
122
|
+
Mac/linux users might need to use backslashes to escape the brackets, e.g.:
|
120
123
|
|
121
|
-
<code>pip install rational-linkages\\[opt\\]</code>
|
124
|
+
<code>pip install rational-linkages\\[opt,exu\\]</code>
|
122
125
|
|
123
|
-
for installing also
|
124
|
-
|
125
|
-
|
126
|
+
for installing also **opt**ional dependencies (scipy - optimization problems solving, ipython - inline plotting,
|
127
|
+
matplotlib - alternative engine for 3D plotting, gmpy2 - optimized symbolic computations)
|
128
|
+
and **exu**dyn dependencies (exudyn - multibody simulations,
|
129
|
+
numpy-stl + ngsolve - work with meshes in exudyn).
|
130
|
+
|
131
|
+
On **Linux systems**, to run GUI interactive plotting,
|
132
|
+
some additional libraries are required for plotting with PyQt6. For example,
|
133
|
+
on Ubuntu, it can be installed as follows:
|
134
|
+
|
135
|
+
<code>sudo apt install libgl1-mesa-glx libxkbcommon-x11-0 libegl1 libdbus-1-3</code>
|
136
|
+
|
137
|
+
or on Ubuntu 24.04 and higher:
|
138
|
+
|
139
|
+
<code>sudo apt install libgl1 libxkbcommon-x11-0 libegl1 libdbus-1-3</code>
|
126
140
|
|
127
141
|
### Install from source
|
128
142
|
|
@@ -142,21 +156,21 @@ work with meshes in exudyn).
|
|
142
156
|
|
143
157
|
<code>pip install -e .[opt,dev,doc]</code> including the development and documentation dependencies.
|
144
158
|
|
145
|
-
Mac
|
159
|
+
Mac/linux users might need to use backslashes to escape the brackets, e.g.:
|
146
160
|
|
147
161
|
<code>pip install -e .\\[opt\\]</code>
|
148
162
|
|
149
|
-
Additionally, on Linux systems, some additional libraries are required for plotting with PyQt6. For example,
|
150
|
-
on Ubuntu, it can be installed as follows:
|
151
163
|
|
152
|
-
<code>sudo apt install libgl1-mesa-glx libxkbcommon-x11-0 libegl1 libdbus-1-3</code>
|
153
164
|
|
154
|
-
|
165
|
+
To run the Rust functions, you need to install the [Rust toolchain](https://www.rust-lang.org) and
|
166
|
+
build the Rust code yourself. On top of that, on Windows, you need to install a
|
167
|
+
C++ build toolchain. In `Visual Studio Installer`, select:
|
155
168
|
|
156
|
-
|
169
|
+
* MSVC v143 - VS 2022 C++ x64/x86 build tools (latest)
|
170
|
+
* Windows 11 SDK
|
171
|
+
* C++ CMake tools for Windows
|
157
172
|
|
158
|
-
|
159
|
-
build the Rust code yourself, for example:
|
173
|
+
Then, navigate to the `rational_linkages/rust` folder and run:
|
160
174
|
|
161
175
|
<code>cargo build --release</code>
|
162
176
|
|