rational-linkages 2.0.0__cp311-cp311-macosx_12_0_x86_64.whl → 2.2.3__cp311-cp311-macosx_12_0_x86_64.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,11 +1,14 @@
|
|
1
|
-
from .RationalMechanism import RationalMechanism
|
2
|
-
from .RationalCurve import RationalCurve
|
3
|
-
from .MiniBall import MiniBall
|
4
|
-
from .DualQuaternion import DualQuaternion
|
5
|
-
from .PointHomogeneous import PointOrbit
|
6
|
-
|
7
1
|
import numpy
|
8
|
-
|
2
|
+
|
3
|
+
from sympy import symbols, Poly
|
4
|
+
|
5
|
+
from .Linkage import LineSegment
|
6
|
+
from .DualQuaternion import DualQuaternion
|
7
|
+
from .NormalizedLine import NormalizedLine
|
8
|
+
from .PointHomogeneous import PointHomogeneous, PointOrbit
|
9
|
+
from .RationalBezier import RationalSoo
|
10
|
+
from .RationalCurve import RationalCurve
|
11
|
+
from .RationalMechanism import RationalMechanism
|
9
12
|
|
10
13
|
|
11
14
|
class CollisionAnalyser:
|
@@ -22,7 +25,7 @@ class CollisionAnalyser:
|
|
22
25
|
self.segments[segment.id] = segment
|
23
26
|
|
24
27
|
self.motions = self.get_motions()
|
25
|
-
self.bezier_splits = self.get_bezier_splits(
|
28
|
+
self.bezier_splits = self.get_bezier_splits(20)
|
26
29
|
|
27
30
|
def get_bezier_splits(self, min_splits: int = 0) -> list:
|
28
31
|
"""
|
@@ -44,11 +47,12 @@ class CollisionAnalyser:
|
|
44
47
|
|
45
48
|
relative_motions = branch0 + branch1[::-1]
|
46
49
|
|
47
|
-
t =
|
50
|
+
t = symbols('t')
|
48
51
|
|
49
52
|
motions = []
|
50
53
|
for motion in relative_motions:
|
51
|
-
motions.append(RationalCurve([
|
54
|
+
motions.append(RationalCurve([Poly(c, t, greedy=False)
|
55
|
+
for c in motion],
|
52
56
|
metric=self.metric))
|
53
57
|
return motions
|
54
58
|
|
@@ -63,11 +67,9 @@ class CollisionAnalyser:
|
|
63
67
|
"""
|
64
68
|
Get the orbit of a segment.
|
65
69
|
"""
|
66
|
-
import time
|
67
|
-
|
68
70
|
segment = self.segments[segment_id]
|
69
71
|
|
70
|
-
if segment.type == 'l'
|
72
|
+
if segment.type == 'l':
|
71
73
|
if segment.factorization_idx == 0:
|
72
74
|
split_idx = segment.idx - 1
|
73
75
|
p0_idx = 2 * segment.idx - 1
|
@@ -196,14 +198,6 @@ class CollisionAnalyser:
|
|
196
198
|
|
197
199
|
return it_collides
|
198
200
|
|
199
|
-
@staticmethod
|
200
|
-
def get_object_type(obj):
|
201
|
-
"""
|
202
|
-
Get the type of an object.
|
203
|
-
"""
|
204
|
-
if isinstance(obj, MiniBall):
|
205
|
-
return 'is_miniball'
|
206
|
-
|
207
201
|
@staticmethod
|
208
202
|
def check_two_miniballs(ball0, ball1):
|
209
203
|
"""
|
@@ -212,3 +206,311 @@ class CollisionAnalyser:
|
|
212
206
|
diff = ball0.center.coordinates - ball1.center.coordinates
|
213
207
|
center_dist_squared = numpy.dot(diff, diff)
|
214
208
|
return center_dist_squared < ball0.radius_squared + ball1.radius_squared
|
209
|
+
|
210
|
+
def get_split_and_point_indices(self, segment):
|
211
|
+
"""
|
212
|
+
Compute split index and point indices for a segment.
|
213
|
+
"""
|
214
|
+
if segment.type == 'l':
|
215
|
+
if segment.factorization_idx == 0:
|
216
|
+
split_idx = segment.idx - 1
|
217
|
+
p0_idx = 2 * segment.idx - 1
|
218
|
+
p1_idx = 2 * segment.idx
|
219
|
+
else:
|
220
|
+
split_idx = -1 * segment.idx
|
221
|
+
p0_idx = -2 * segment.idx - 1
|
222
|
+
p1_idx = -2 * segment.idx
|
223
|
+
else: # type == 'j'
|
224
|
+
if segment.factorization_idx == 0:
|
225
|
+
split_idx = segment.idx - 1
|
226
|
+
p0_idx = 2 * segment.idx
|
227
|
+
p1_idx = 2 * segment.idx + 1
|
228
|
+
else:
|
229
|
+
split_idx = -1 * segment.idx
|
230
|
+
p0_idx = -2 * segment.idx - 1
|
231
|
+
p1_idx = -2 * segment.idx - 2
|
232
|
+
return split_idx, p0_idx, p1_idx
|
233
|
+
|
234
|
+
def optimize_curved_link(self,
|
235
|
+
segment_id: str,
|
236
|
+
min_splits: int = 20,
|
237
|
+
curve_degree: int = 3):
|
238
|
+
"""
|
239
|
+
Optimize the curved link to avoid collisions.
|
240
|
+
"""
|
241
|
+
if segment_id.startswith('j'):
|
242
|
+
raise ValueError('Joints cannot be optimized as curved lines, only links.')
|
243
|
+
|
244
|
+
# get segment creation index
|
245
|
+
segment_id_num = None
|
246
|
+
for s_id, segment in enumerate(self.segments.values()):
|
247
|
+
if segment.id == segment_id:
|
248
|
+
segment_id_num = s_id
|
249
|
+
break
|
250
|
+
|
251
|
+
indices = list(range(len(self.mechanism.segments)))
|
252
|
+
|
253
|
+
# remove index of segment to optimize and the two neighboring segments
|
254
|
+
indices.remove(segment_id_num)
|
255
|
+
if segment_id_num != 0:
|
256
|
+
indices.remove(segment_id_num - 1)
|
257
|
+
else:
|
258
|
+
indices.remove(indices[-1]) # remove last if first segment is optimized
|
259
|
+
if segment_id_num != len(self.mechanism.segments) - 1:
|
260
|
+
indices.remove(segment_id_num + 1)
|
261
|
+
else:
|
262
|
+
indices.remove(indices[0]) # remove first if last segment is optimized
|
263
|
+
|
264
|
+
# remove odd indices which correspond to joints; keep also zero
|
265
|
+
indices_reduced = [idx for i, idx in enumerate(indices)
|
266
|
+
if idx % 2 == 0 or idx == 0]
|
267
|
+
|
268
|
+
bounding_balls = self.obtain_global_bounding_balls(segment_id_num,
|
269
|
+
indices_reduced,
|
270
|
+
min_splits)
|
271
|
+
|
272
|
+
dh, design_params, design_points = self.mechanism.get_design(
|
273
|
+
return_point_homogeneous=True,
|
274
|
+
update_design=True,
|
275
|
+
pretty_print=False)
|
276
|
+
|
277
|
+
joint_id = segment_id_num // 2
|
278
|
+
pt0 = design_points[joint_id - 1][1]
|
279
|
+
pt1 = design_points[joint_id][0]
|
280
|
+
|
281
|
+
link_cps = RationalSoo.control_points_between_two_points(pt0,
|
282
|
+
pt1,
|
283
|
+
degree=curve_degree)
|
284
|
+
init_control_points = link_cps[1:-1] # remove the first and last control points
|
285
|
+
|
286
|
+
new_cps = self.optimize_control_points(init_control_points,
|
287
|
+
bounding_balls)
|
288
|
+
new_cps.insert(0, pt0)
|
289
|
+
new_cps.append(pt1)
|
290
|
+
|
291
|
+
return RationalSoo(new_cps)
|
292
|
+
|
293
|
+
@staticmethod
|
294
|
+
def optimize_control_points(init_points: list[PointHomogeneous],
|
295
|
+
bounding_orbits: list[list[PointOrbit]]):
|
296
|
+
"""
|
297
|
+
Optimize the link control points to avoid collisions with the bounding orbits.
|
298
|
+
"""
|
299
|
+
try:
|
300
|
+
from scipy.optimize import minimize # lazy import
|
301
|
+
except ImportError:
|
302
|
+
raise RuntimeError("Scipy import failed. Check its installation.")
|
303
|
+
|
304
|
+
def flatten_cps(cps):
|
305
|
+
return numpy.array([cp.normalized_in_3d() for cp in cps]).flatten()
|
306
|
+
|
307
|
+
def unflatten_cps(cps_flat):
|
308
|
+
return [PointHomogeneous([1, cps_flat[i], cps_flat[i + 1], cps_flat[i + 2]])
|
309
|
+
for i in range(0, len(cps_flat), 3)]
|
310
|
+
|
311
|
+
flattened_orbits = []
|
312
|
+
for i in range(len(bounding_orbits)):
|
313
|
+
for j in range(len(bounding_orbits[i])):
|
314
|
+
flattened_orbits.extend(bounding_orbits[i][j][1:])
|
315
|
+
|
316
|
+
orbit_centers = [orbit.center.normalized_in_3d() for orbit in flattened_orbits]
|
317
|
+
orbit_radii = [orbit.radius for orbit in flattened_orbits]
|
318
|
+
|
319
|
+
init_cps = flatten_cps(init_points)
|
320
|
+
lambda_reg = 0.1
|
321
|
+
|
322
|
+
def loss(params):
|
323
|
+
cps = unflatten_cps(params)
|
324
|
+
margin = 0.01
|
325
|
+
penalty = 0.0
|
326
|
+
for cp in cps:
|
327
|
+
for i, orbit in enumerate(flattened_orbits):
|
328
|
+
dist = numpy.linalg.norm(cp.normalized_in_3d() - orbit_centers[i])
|
329
|
+
if dist < orbit_radii[i] + margin:
|
330
|
+
penalty += (orbit_radii[i] + margin - dist) ** 2
|
331
|
+
# Regularization: keep cps close to initial guess
|
332
|
+
penalty += lambda_reg * numpy.sum((params - init_cps) ** 2)
|
333
|
+
return penalty
|
334
|
+
|
335
|
+
res = minimize(loss, init_cps)
|
336
|
+
|
337
|
+
if not res.success:
|
338
|
+
raise RuntimeError(f'Optimization failed: {res.message}')
|
339
|
+
else:
|
340
|
+
new_control_points = unflatten_cps(res.x)
|
341
|
+
|
342
|
+
return new_control_points
|
343
|
+
|
344
|
+
def obtain_global_bounding_balls(self,
|
345
|
+
segment_id_number: int,
|
346
|
+
reduced_indices: list[int],
|
347
|
+
min_splits: int = 20):
|
348
|
+
"""
|
349
|
+
Obtain global covering balls for a segment to optimize it as a curved link.
|
350
|
+
|
351
|
+
:param segment_id_number: The index of the segment to optimize.
|
352
|
+
:param reduced_indices: Indices of segments to consider for bounding balls.
|
353
|
+
:param min_splits: Minimum number of splits for the bezier curves.
|
354
|
+
"""
|
355
|
+
|
356
|
+
t = symbols('t')
|
357
|
+
motions = []
|
358
|
+
for i, idx in enumerate(reduced_indices):
|
359
|
+
rel_motion = self.mechanism.relative_motion(segment_id_number, idx)
|
360
|
+
motions.append(RationalCurve([Poly(c, t, greedy=False)
|
361
|
+
for c in rel_motion],
|
362
|
+
metric=self.metric))
|
363
|
+
|
364
|
+
bezier_splits = [motion.split_in_beziers(min_splits) for motion in motions]
|
365
|
+
|
366
|
+
all_orbits = []
|
367
|
+
for i, segment_idx in enumerate(reduced_indices):
|
368
|
+
|
369
|
+
split_idx = i
|
370
|
+
p0_idx = segment_idx - 1
|
371
|
+
p1_idx = segment_idx
|
372
|
+
|
373
|
+
rel_bezier_splits = bezier_splits[split_idx]
|
374
|
+
|
375
|
+
p0 = self.mechanism_points[p0_idx]
|
376
|
+
p1 = self.mechanism_points[p1_idx]
|
377
|
+
|
378
|
+
orbits0 = [PointOrbit(*p0.get_point_orbit(acting_center=split.ball.center,
|
379
|
+
acting_radius=split.ball.radius_squared,
|
380
|
+
metric=self.metric),
|
381
|
+
t_interval=split.t_param_of_motion_curve)
|
382
|
+
for split in rel_bezier_splits]
|
383
|
+
orbits1 = [PointOrbit(*p1.get_point_orbit(acting_center=split.ball.center,
|
384
|
+
acting_radius=split.ball.radius_squared,
|
385
|
+
metric=self.metric),
|
386
|
+
t_interval=split.t_param_of_motion_curve)
|
387
|
+
for split in rel_bezier_splits]
|
388
|
+
|
389
|
+
all_orbits_of_a_link = []
|
390
|
+
for i in range(len(orbits0)):
|
391
|
+
orbits_for_t = [orbits0[i].t_interval, orbits0[i]]
|
392
|
+
dist = numpy.linalg.norm(orbits0[i].center.normalized_in_3d() - orbits1[
|
393
|
+
i].center.normalized_in_3d())
|
394
|
+
radius_sum = orbits0[i].radius + orbits1[i].radius
|
395
|
+
if dist > radius_sum:
|
396
|
+
add_balls = dist / radius_sum
|
397
|
+
num_steps = int(add_balls) * 2 + 1
|
398
|
+
|
399
|
+
# linear interpolation from smaller ball to bigger ball
|
400
|
+
radii = 0
|
401
|
+
radius_diff = orbits1[i].radius - orbits0[i].radius
|
402
|
+
center_diff = orbits1[i].center - orbits0[i].center
|
403
|
+
for j in range(1, num_steps):
|
404
|
+
new_radius = orbits0[i].radius + j * radius_diff / num_steps
|
405
|
+
radii += new_radius
|
406
|
+
new_center = orbits0[i].center + 2 * radii * center_diff / (
|
407
|
+
dist * 2)
|
408
|
+
orbits_for_t.append(PointOrbit(new_center, new_radius ** 2,
|
409
|
+
orbits0[i].t_interval))
|
410
|
+
orbits_for_t.append(orbits1[i])
|
411
|
+
all_orbits_of_a_link.append(orbits_for_t)
|
412
|
+
all_orbits.append(all_orbits_of_a_link)
|
413
|
+
|
414
|
+
return all_orbits
|
415
|
+
|
416
|
+
@staticmethod
|
417
|
+
def quantify_collision(segment0: LineSegment,
|
418
|
+
segment1: LineSegment,
|
419
|
+
t_val):
|
420
|
+
"""
|
421
|
+
Quantify the collision between two line segments.
|
422
|
+
"""
|
423
|
+
p00 = segment0.point0.evaluate(t_val)
|
424
|
+
p01 = segment0.point1.evaluate(t_val)
|
425
|
+
|
426
|
+
p10 = segment1.point0.evaluate(t_val)
|
427
|
+
p11 = segment1.point1.evaluate(t_val)
|
428
|
+
|
429
|
+
l0 = NormalizedLine.from_two_points(p00, p01)
|
430
|
+
l1 = NormalizedLine.from_two_points(p10, p11)
|
431
|
+
|
432
|
+
pts, dist, cos_alpha = l0.common_perpendicular_to_other_line(l1)
|
433
|
+
|
434
|
+
if numpy.isclose(dist, 0.0): # lines are intersecting
|
435
|
+
quantif = CollisionAnalyser.quatif_intersection_location(
|
436
|
+
PointHomogeneous.from_3d_point(pts[0]),
|
437
|
+
p00,
|
438
|
+
p01)
|
439
|
+
else: # lines are not intersecting, there is a distance
|
440
|
+
quantif_l0 = CollisionAnalyser.quatif_intersection_location(
|
441
|
+
PointHomogeneous.from_3d_point(pts[0]),
|
442
|
+
p00,
|
443
|
+
p01)
|
444
|
+
quantif_l1 = CollisionAnalyser.quatif_intersection_location(
|
445
|
+
PointHomogeneous.from_3d_point(pts[1]),
|
446
|
+
p10,
|
447
|
+
p11)
|
448
|
+
quantif_dist = CollisionAnalyser.map_to_exponential_decay(dist, k=10.0)
|
449
|
+
|
450
|
+
quantif = quantif_dist * (quantif_l0 + quantif_l1)
|
451
|
+
|
452
|
+
return quantif
|
453
|
+
|
454
|
+
@staticmethod
|
455
|
+
def quatif_intersection_location(interection_pt, segment_pt0, segment_pt1):
|
456
|
+
a = numpy.linalg.norm(
|
457
|
+
segment_pt0.normalized_in_3d() - interection_pt.normalized_in_3d())
|
458
|
+
b = numpy.linalg.norm(
|
459
|
+
segment_pt1.normalized_in_3d() - interection_pt.normalized_in_3d())
|
460
|
+
|
461
|
+
segment_lenght = numpy.linalg.norm(
|
462
|
+
segment_pt0.normalized_in_3d() - segment_pt1.normalized_in_3d())
|
463
|
+
if a + b > segment_lenght:
|
464
|
+
val = a + b - segment_lenght
|
465
|
+
quantif_val = CollisionAnalyser.map_to_exponential_decay(val, k=2.0)
|
466
|
+
else:
|
467
|
+
val = a if a < b else b
|
468
|
+
quantif_val = CollisionAnalyser.map_to_range_inside(val, segment_lenght, 2)
|
469
|
+
|
470
|
+
return quantif_val
|
471
|
+
|
472
|
+
@staticmethod
|
473
|
+
def map_to_exponential_decay(x, k=1.0):
|
474
|
+
"""
|
475
|
+
Maps a value x using an exponential decay function to the range [0, 1].
|
476
|
+
|
477
|
+
Maps a value x from the range [0, inf) to the interval [0, 1] using an
|
478
|
+
exponential decay function.
|
479
|
+
|
480
|
+
:param float x: The input value in the range [0, inf).
|
481
|
+
:param float k: The decay rate (must be positive). Default is 1.0.
|
482
|
+
|
483
|
+
:return: The mapped value in the range [0, 1].
|
484
|
+
:rtype: float
|
485
|
+
"""
|
486
|
+
if x < 0:
|
487
|
+
raise ValueError("x must be non-negative")
|
488
|
+
if k <= 0:
|
489
|
+
raise ValueError("k must be positive")
|
490
|
+
|
491
|
+
# Exponential decay formula
|
492
|
+
y = numpy.exp(-k * x)
|
493
|
+
return y
|
494
|
+
|
495
|
+
@staticmethod
|
496
|
+
def map_to_range_inside(x, x_max, weight=1.0):
|
497
|
+
"""
|
498
|
+
Maps a value x from the range [0, x_max] to a value y in the range [1, 2].
|
499
|
+
|
500
|
+
Function preserves the ratio of the input value relative to the maximum value.
|
501
|
+
|
502
|
+
:param float x: The input value in the range [0, x_max].
|
503
|
+
:param float x_max: The maximum value of the input range.
|
504
|
+
:param float weight: The weight to scale the output value. Default is 1.0.
|
505
|
+
|
506
|
+
:return: The mapped value in the range [1, 2].
|
507
|
+
:rtype: float
|
508
|
+
"""
|
509
|
+
if x < 0 or x > x_max:
|
510
|
+
raise ValueError("x must be in the range [0, x_max]")
|
511
|
+
if x_max <= 0:
|
512
|
+
raise ValueError("x_max must be greater than 0")
|
513
|
+
|
514
|
+
# linear mapping formula
|
515
|
+
y = 1 + (x / x_max) * weight
|
516
|
+
return y
|
@@ -1,8 +1,9 @@
|
|
1
|
-
from
|
2
|
-
from .NormalizedLine import NormalizedLine
|
1
|
+
from itertools import product
|
3
2
|
|
4
3
|
import numpy as np
|
5
|
-
|
4
|
+
|
5
|
+
from .NormalizedLine import NormalizedLine
|
6
|
+
from .RationalMechanism import RationalMechanism
|
6
7
|
|
7
8
|
|
8
9
|
class CollisionFreeOptimization:
|
@@ -29,7 +30,10 @@ class CollisionFreeOptimization:
|
|
29
30
|
parameters of the points, result of the optimization
|
30
31
|
:rtype: tuple
|
31
32
|
"""
|
32
|
-
|
33
|
+
try:
|
34
|
+
from scipy.optimize import minimize # lazy import
|
35
|
+
except ImportError:
|
36
|
+
raise RuntimeError("Scipy import failed. Check its installation.")
|
33
37
|
|
34
38
|
# get the axes represented as normalized lines
|
35
39
|
if len(self.mechanism.factorizations) == 1:
|
@@ -226,10 +226,13 @@ class DualQuaternion:
|
|
226
226
|
:return: DualQuaternion with rational elements
|
227
227
|
:rtype: DualQuaternion
|
228
228
|
"""
|
229
|
-
from sympy import Rational
|
229
|
+
from sympy import Rational
|
230
230
|
|
231
231
|
if study_parameters is not None:
|
232
|
-
rational_numbers = [
|
232
|
+
rational_numbers = [x if isinstance(x, Expr)
|
233
|
+
else Rational(*x) if isinstance(x, tuple)
|
234
|
+
else Rational(x)
|
235
|
+
for x in study_parameters]
|
233
236
|
else:
|
234
237
|
rational_numbers = [Rational(1), Rational(0), Rational(0), Rational(0),
|
235
238
|
Rational(0), Rational(0), Rational(0), Rational(0)]
|
@@ -3,7 +3,8 @@ from warnings import warn
|
|
3
3
|
|
4
4
|
import biquaternion_py
|
5
5
|
import numpy as np
|
6
|
-
|
6
|
+
|
7
|
+
from sympy import Symbol, Rational
|
7
8
|
|
8
9
|
from .DualQuaternion import DualQuaternion
|
9
10
|
from .MotionFactorization import MotionFactorization
|
@@ -45,7 +46,7 @@ class FactorizationProvider:
|
|
45
46
|
|
46
47
|
:warning: If the given curve has not only rational numbers as input.
|
47
48
|
"""
|
48
|
-
t =
|
49
|
+
t = Symbol("t")
|
49
50
|
|
50
51
|
if isinstance(curve, RationalCurve):
|
51
52
|
bi_quat = biquaternion_py.BiQuaternion(curve.extract_expressions())
|
@@ -58,7 +59,7 @@ class FactorizationProvider:
|
|
58
59
|
poly_coeffs = bi_poly.all_coeffs()
|
59
60
|
for i in range(len(poly_coeffs)):
|
60
61
|
for j in range(len(poly_coeffs[i].args)):
|
61
|
-
if not isinstance(poly_coeffs[i].args[j],
|
62
|
+
if not isinstance(poly_coeffs[i].args[j], Rational):
|
62
63
|
warn('The given curve has not only rational numbers as input. The factorization will be performed with floating point numbers, but may be instable.')
|
63
64
|
break
|
64
65
|
|
@@ -92,7 +93,7 @@ class FactorizationProvider:
|
|
92
93
|
'as input. The factorization will be performed with floating '
|
93
94
|
'point numbers, but may be instable.')
|
94
95
|
|
95
|
-
t =
|
96
|
+
t = Symbol("t")
|
96
97
|
|
97
98
|
bi_poly = t - biquaternion_py.BiQuaternion(factorization.dq_axes[0].array())
|
98
99
|
for i in range(1, factorization.number_of_factors):
|
@@ -147,7 +148,7 @@ class FactorizationProvider:
|
|
147
148
|
:return: The rotation axis of the factor.
|
148
149
|
:rtype: DualQuaternion
|
149
150
|
"""
|
150
|
-
t =
|
151
|
+
t = Symbol("t")
|
151
152
|
t_dq = DualQuaternion([t, 0, 0, 0, 0, 0, 0, 0])
|
152
153
|
|
153
154
|
factor_dq = DualQuaternion(factor.poly.coeffs)
|
rational_linkages/MiniBall.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
import numpy as np
|
2
|
-
from scipy.optimize import minimize
|
3
2
|
|
4
|
-
from .PointHomogeneous import PointHomogeneous
|
5
3
|
from .AffineMetric import AffineMetric
|
4
|
+
from .PointHomogeneous import PointHomogeneous
|
6
5
|
|
7
6
|
|
8
7
|
class MiniBall:
|
@@ -55,6 +54,14 @@ class MiniBall:
|
|
55
54
|
return PointHomogeneous(center), radius_squared
|
56
55
|
|
57
56
|
def get_ball_minimize(self):
|
57
|
+
"""
|
58
|
+
Find the smallest ball containing all given points using optimization
|
59
|
+
"""
|
60
|
+
try:
|
61
|
+
from scipy.optimize import minimize # lazy import
|
62
|
+
except ImportError:
|
63
|
+
raise RuntimeError("Scipy import failed. Check the package installation.")
|
64
|
+
|
58
65
|
def objective_function(x):
|
59
66
|
"""
|
60
67
|
Objective function to minimize the squared radius r^2 of the ball
|
@@ -1,12 +1,16 @@
|
|
1
|
-
import numpy as np
|
2
|
-
from scipy.optimize import minimize
|
3
1
|
from typing import Union
|
4
2
|
|
5
|
-
|
3
|
+
import numpy as np
|
4
|
+
|
6
5
|
from .AffineMetric import AffineMetric
|
6
|
+
from .DualQuaternion import DualQuaternion
|
7
7
|
from .PointHomogeneous import PointHomogeneous
|
8
8
|
from .RationalCurve import RationalCurve
|
9
9
|
|
10
|
+
try:
|
11
|
+
from scipy.optimize import minimize # lazy import, optional dependency
|
12
|
+
except ImportError:
|
13
|
+
minimize = None
|
10
14
|
|
11
15
|
### NOT YET in the documentation ### TODO: add to docs
|
12
16
|
|