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.
Files changed (31) hide show
  1. rational_linkages/CollisionAnalyser.py +323 -21
  2. rational_linkages/CollisionFreeOptimization.py +8 -4
  3. rational_linkages/DualQuaternion.py +5 -2
  4. rational_linkages/ExudynAnalysis.py +2 -1
  5. rational_linkages/FactorizationProvider.py +6 -5
  6. rational_linkages/MiniBall.py +9 -2
  7. rational_linkages/MotionApproximation.py +7 -3
  8. rational_linkages/MotionDesigner.py +553 -540
  9. rational_linkages/MotionFactorization.py +6 -5
  10. rational_linkages/MotionInterpolation.py +7 -7
  11. rational_linkages/NormalizedLine.py +1 -1
  12. rational_linkages/NormalizedPlane.py +1 -1
  13. rational_linkages/Plotter.py +1 -1
  14. rational_linkages/PlotterMatplotlib.py +27 -13
  15. rational_linkages/PlotterPyqtgraph.py +596 -534
  16. rational_linkages/PointHomogeneous.py +6 -3
  17. rational_linkages/RationalBezier.py +64 -4
  18. rational_linkages/RationalCurve.py +13 -5
  19. rational_linkages/RationalDualQuaternion.py +5 -4
  20. rational_linkages/RationalMechanism.py +48 -33
  21. rational_linkages/SingularityAnalysis.py +4 -5
  22. rational_linkages/StaticMechanism.py +4 -5
  23. rational_linkages/__init__.py +3 -2
  24. rational_linkages/utils.py +60 -3
  25. rational_linkages/utils_rust.cpython-311-darwin.so +0 -0
  26. {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/METADATA +32 -18
  27. rational_linkages-2.2.3.dist-info/RECORD +40 -0
  28. rational_linkages-2.0.0.dist-info/RECORD +0 -40
  29. {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/WHEEL +0 -0
  30. {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/licenses/LICENSE +0 -0
  31. {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, Symbol, Number
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 # INNER IMPORT
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 # inner import
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 # inner import
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 # inner import
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
- import sympy as sp
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[sp.Rational]):
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) -> sp.Rational:
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: sp.Matrix
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 # inner import
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]:.6f}, {dh[i, 2]:.6f}, {dh[i, 3]:.6f}], "
314
- f"{design_params[i, 0]:.6f}, {design_params[i, 1]:.6f}")
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]:.6f}, "
321
- f"a = {dh[i, 2]:.6f}, "
322
- f"alpha = {dh[i, 3]:.6f}")
323
- print(f"cp_0 = {design_params[i, 0]:.6f}, "
324
- f"cp_1 = {design_params[i, 1]:.6f}")
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 # inner import
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 # inner import
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 # inner import
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 'deg'
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 != 'rad' and unit != 'deg':
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
- joint_angle = self.factorizations[0].t_param_to_joint_angle(t)
1117
-
1118
- if unit == 'deg':
1119
- joint_angle = np.rad2deg(joint_angle)
1120
-
1121
- return joint_angle
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 not robust:
1172
- if t_val == sp.nan or t_val > 1.0 or t_val < -1.0:
1173
- break
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 = curve.evaluate(t_val)
1177
- c_diff_eval = np.array([element.subs(t, t_val).evalf()
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] == 0. or current_pose[0] == 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 == 0.0:
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: LineSegment,
1477
- moving: LineSegment) -> DualQuaternion:
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.creation_index,
1491
- moving.creation_index)
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 .RationalMechanism import RationalMechanism
2
- from .Linkage import LineSegment
1
+ from sympy import Matrix
3
2
 
4
- import sympy
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
 
@@ -1,6 +1,7 @@
1
1
  # __init__.py
2
2
 
3
- from importlib.metadata import version, PackageNotFoundError
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 RationalBezier, BezierSegment
23
+ from .RationalBezier import BezierSegment, RationalBezier
23
24
  from .RationalCurve import RationalCurve
24
25
  from .RationalDualQuaternion import RationalDualQuaternion
25
26
  from .RationalMechanism import RationalMechanism
@@ -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 symbols, expand # inner 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 # inner import
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rational-linkages
3
- Version: 2.0.0
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: scipy>=1.10.0
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: gmpy2>=2.2.0; extra == "opt"
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 optional dependencies (ipython - inline plotting, gmpy2 - faster
124
- symbolic computations, exudyn - multibody simulations, numpy-stl -
125
- work with meshes in exudyn).
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 or Linux users might need to use backslashes to escape the brackets, e.g.:
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
- or on Ubuntu 24.04 and higher:
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
- <code>sudo apt install libgl1 libxkbcommon-x11-0 libegl1 libdbus-1-3</code>
169
+ * MSVC v143 - VS 2022 C++ x64/x86 build tools (latest)
170
+ * Windows 11 SDK
171
+ * C++ CMake tools for Windows
157
172
 
158
- To run the Rust functions, you need to install the [Rust toolchain](https://www.rust-lang.org) and
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