foodforthought-cli 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +100 -0
- ate/behaviors/approach.py +399 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +855 -3995
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +39 -0
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +942 -0
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +187 -0
- ate/interfaces/base.py +273 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/sensors.py +247 -0
- ate/interfaces/types.py +371 -0
- ate/llm_proxy.py +239 -0
- ate/mcp_server.py +387 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +83 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +415 -0
- ate/recording/upload.py +304 -0
- ate/recording/visual.py +416 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +221 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +668 -0
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +3735 -0
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +441 -0
- ate/robot/introspection.py +330 -0
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/manager.py +270 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +281 -0
- ate/robot/registry.py +322 -0
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +675 -0
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +1048 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Motion analysis for joint discovery from point cloud trajectories.
|
|
3
|
+
|
|
4
|
+
This module provides tools for analyzing how links move over time:
|
|
5
|
+
- Trajectory extraction from per-frame point clouds
|
|
6
|
+
- Joint type classification (revolute vs prismatic)
|
|
7
|
+
- Motion correlation for parent-child relationship discovery
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, List, Optional, Tuple
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import numpy as np
|
|
18
|
+
from scipy.spatial.transform import Rotation
|
|
19
|
+
DEPS_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
DEPS_AVAILABLE = False
|
|
22
|
+
np = None
|
|
23
|
+
Rotation = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MotionAnalysisError(Exception):
|
|
27
|
+
"""Error during motion analysis."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class LinkTrajectory:
|
|
33
|
+
"""Trajectory of a single link over time."""
|
|
34
|
+
name: str
|
|
35
|
+
frame_indices: List[int]
|
|
36
|
+
centroids: "np.ndarray" # (T, 3) positions
|
|
37
|
+
orientations: Optional["np.ndarray"] = None # (T, 3, 3) rotation matrices
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def n_frames(self) -> int:
|
|
41
|
+
return len(self.frame_indices)
|
|
42
|
+
|
|
43
|
+
def get_velocity(self) -> "np.ndarray":
|
|
44
|
+
"""Compute velocity (change in position per frame)."""
|
|
45
|
+
if self.n_frames < 2:
|
|
46
|
+
return np.zeros((0, 3))
|
|
47
|
+
return np.diff(self.centroids, axis=0)
|
|
48
|
+
|
|
49
|
+
def get_total_displacement(self) -> float:
|
|
50
|
+
"""Get total path length traveled."""
|
|
51
|
+
velocity = self.get_velocity()
|
|
52
|
+
return np.sum(np.linalg.norm(velocity, axis=1))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class RelativeMotion:
|
|
57
|
+
"""Relative motion between two links."""
|
|
58
|
+
parent_name: str
|
|
59
|
+
child_name: str
|
|
60
|
+
frame_indices: List[int]
|
|
61
|
+
relative_positions: "np.ndarray" # (T, 3) child position in parent frame
|
|
62
|
+
relative_rotations: "np.ndarray" # (T, 3, 3) child rotation in parent frame
|
|
63
|
+
|
|
64
|
+
def analyze_joint_type(self) -> Tuple[str, float]:
|
|
65
|
+
"""
|
|
66
|
+
Classify joint type based on relative motion patterns.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Tuple of (joint_type, confidence)
|
|
70
|
+
joint_type: "revolute", "prismatic", or "fixed"
|
|
71
|
+
"""
|
|
72
|
+
n = len(self.frame_indices)
|
|
73
|
+
if n < 3:
|
|
74
|
+
return "fixed", 1.0
|
|
75
|
+
|
|
76
|
+
# Compute translation and rotation magnitudes
|
|
77
|
+
translations = np.linalg.norm(self.relative_positions - self.relative_positions[0], axis=1)
|
|
78
|
+
max_translation = np.max(translations)
|
|
79
|
+
|
|
80
|
+
# Compute rotation angles from rotation matrices
|
|
81
|
+
angles = []
|
|
82
|
+
for R in self.relative_rotations:
|
|
83
|
+
R0 = self.relative_rotations[0]
|
|
84
|
+
R_rel = R @ R0.T
|
|
85
|
+
angle = np.arccos(np.clip((np.trace(R_rel) - 1) / 2, -1, 1))
|
|
86
|
+
angles.append(angle)
|
|
87
|
+
angles = np.array(angles)
|
|
88
|
+
max_rotation = np.max(angles)
|
|
89
|
+
|
|
90
|
+
# Classification thresholds
|
|
91
|
+
TRANSLATION_THRESHOLD = 0.01 # 1cm
|
|
92
|
+
ROTATION_THRESHOLD = 0.05 # ~3 degrees
|
|
93
|
+
|
|
94
|
+
if max_translation < TRANSLATION_THRESHOLD and max_rotation < ROTATION_THRESHOLD:
|
|
95
|
+
return "fixed", 0.9
|
|
96
|
+
|
|
97
|
+
# Ratio of rotation to translation determines joint type
|
|
98
|
+
# Revolute: mostly rotation, minimal translation
|
|
99
|
+
# Prismatic: mostly translation, minimal rotation
|
|
100
|
+
if max_rotation > ROTATION_THRESHOLD and max_translation < 0.05:
|
|
101
|
+
return "revolute", 0.8
|
|
102
|
+
elif max_translation > TRANSLATION_THRESHOLD and max_rotation < 0.1:
|
|
103
|
+
return "prismatic", 0.8
|
|
104
|
+
else:
|
|
105
|
+
# Mixed motion - default to revolute as most common
|
|
106
|
+
return "revolute", 0.6
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def extract_trajectories(
|
|
110
|
+
link_clouds: Dict[str, List["np.ndarray"]],
|
|
111
|
+
frame_indices: List[int],
|
|
112
|
+
) -> Dict[str, LinkTrajectory]:
|
|
113
|
+
"""
|
|
114
|
+
Extract link trajectories from per-frame point clouds.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
link_clouds: Dict mapping link_name -> list of point clouds per frame
|
|
118
|
+
frame_indices: List of frame indices
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dict mapping link_name -> LinkTrajectory
|
|
122
|
+
"""
|
|
123
|
+
if not DEPS_AVAILABLE:
|
|
124
|
+
raise MotionAnalysisError("NumPy/SciPy not available")
|
|
125
|
+
|
|
126
|
+
trajectories = {}
|
|
127
|
+
|
|
128
|
+
for link_name, clouds in link_clouds.items():
|
|
129
|
+
if len(clouds) != len(frame_indices):
|
|
130
|
+
logger.warning(f"Mismatched cloud count for {link_name}: {len(clouds)} vs {len(frame_indices)}")
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Compute centroids
|
|
134
|
+
centroids = []
|
|
135
|
+
valid_indices = []
|
|
136
|
+
|
|
137
|
+
for i, cloud in enumerate(clouds):
|
|
138
|
+
if cloud is not None and len(cloud) > 0:
|
|
139
|
+
centroid = np.mean(cloud, axis=0)
|
|
140
|
+
centroids.append(centroid)
|
|
141
|
+
valid_indices.append(frame_indices[i])
|
|
142
|
+
|
|
143
|
+
if len(centroids) < 2:
|
|
144
|
+
logger.warning(f"Insufficient data for trajectory of '{link_name}'")
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
trajectories[link_name] = LinkTrajectory(
|
|
148
|
+
name=link_name,
|
|
149
|
+
frame_indices=valid_indices,
|
|
150
|
+
centroids=np.array(centroids),
|
|
151
|
+
orientations=None, # Could be computed with PCA if needed
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return trajectories
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def compute_relative_motion(
|
|
158
|
+
parent_traj: LinkTrajectory,
|
|
159
|
+
child_traj: LinkTrajectory,
|
|
160
|
+
) -> RelativeMotion:
|
|
161
|
+
"""
|
|
162
|
+
Compute relative motion of child link w.r.t. parent link.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
parent_traj: Parent link trajectory
|
|
166
|
+
child_traj: Child link trajectory
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
RelativeMotion describing child motion in parent frame
|
|
170
|
+
"""
|
|
171
|
+
if not DEPS_AVAILABLE:
|
|
172
|
+
raise MotionAnalysisError("NumPy not available")
|
|
173
|
+
|
|
174
|
+
# Find common frames
|
|
175
|
+
parent_frames = set(parent_traj.frame_indices)
|
|
176
|
+
child_frames = set(child_traj.frame_indices)
|
|
177
|
+
common_frames = sorted(parent_frames & child_frames)
|
|
178
|
+
|
|
179
|
+
if len(common_frames) < 2:
|
|
180
|
+
raise MotionAnalysisError(
|
|
181
|
+
f"Insufficient common frames between {parent_traj.name} and {child_traj.name}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Get positions at common frames
|
|
185
|
+
parent_pos = []
|
|
186
|
+
child_pos = []
|
|
187
|
+
|
|
188
|
+
for frame in common_frames:
|
|
189
|
+
p_idx = parent_traj.frame_indices.index(frame)
|
|
190
|
+
c_idx = child_traj.frame_indices.index(frame)
|
|
191
|
+
parent_pos.append(parent_traj.centroids[p_idx])
|
|
192
|
+
child_pos.append(child_traj.centroids[c_idx])
|
|
193
|
+
|
|
194
|
+
parent_pos = np.array(parent_pos)
|
|
195
|
+
child_pos = np.array(child_pos)
|
|
196
|
+
|
|
197
|
+
# Compute relative positions (child - parent)
|
|
198
|
+
relative_positions = child_pos - parent_pos
|
|
199
|
+
|
|
200
|
+
# For now, assume identity rotations (orientation estimation requires more data)
|
|
201
|
+
n = len(common_frames)
|
|
202
|
+
relative_rotations = np.tile(np.eye(3), (n, 1, 1))
|
|
203
|
+
|
|
204
|
+
return RelativeMotion(
|
|
205
|
+
parent_name=parent_traj.name,
|
|
206
|
+
child_name=child_traj.name,
|
|
207
|
+
frame_indices=common_frames,
|
|
208
|
+
relative_positions=relative_positions,
|
|
209
|
+
relative_rotations=relative_rotations,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def estimate_revolute_axis(
|
|
214
|
+
relative_motion: RelativeMotion,
|
|
215
|
+
) -> Tuple["np.ndarray", "np.ndarray", float]:
|
|
216
|
+
"""
|
|
217
|
+
Estimate revolute joint axis from relative motion.
|
|
218
|
+
|
|
219
|
+
Uses SVD to find the best-fit rotation axis that explains
|
|
220
|
+
the child link's motion relative to the parent.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
relative_motion: RelativeMotion between parent and child
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Tuple of (axis, pivot_point, confidence):
|
|
227
|
+
- axis: (3,) unit vector of rotation axis
|
|
228
|
+
- pivot_point: (3,) point on the rotation axis
|
|
229
|
+
- confidence: Quality of axis estimation (0-1)
|
|
230
|
+
"""
|
|
231
|
+
if not DEPS_AVAILABLE:
|
|
232
|
+
raise MotionAnalysisError("NumPy not available")
|
|
233
|
+
|
|
234
|
+
positions = relative_motion.relative_positions
|
|
235
|
+
n = len(positions)
|
|
236
|
+
|
|
237
|
+
if n < 3:
|
|
238
|
+
# Not enough data - return default Y axis
|
|
239
|
+
return np.array([0, 1, 0]), positions[0], 0.3
|
|
240
|
+
|
|
241
|
+
# Center the positions
|
|
242
|
+
mean_pos = np.mean(positions, axis=0)
|
|
243
|
+
centered = positions - mean_pos
|
|
244
|
+
|
|
245
|
+
# SVD to find principal components
|
|
246
|
+
U, S, Vt = np.linalg.svd(centered)
|
|
247
|
+
|
|
248
|
+
# For a revolute joint, motion is planar perpendicular to the axis
|
|
249
|
+
# The axis is the direction with minimum variance (last singular value)
|
|
250
|
+
axis = Vt[-1] # Direction with smallest variance
|
|
251
|
+
|
|
252
|
+
# Ensure consistent axis direction (positive Z preferred)
|
|
253
|
+
if axis[2] < 0:
|
|
254
|
+
axis = -axis
|
|
255
|
+
|
|
256
|
+
# Pivot point is at the mean position
|
|
257
|
+
pivot = mean_pos
|
|
258
|
+
|
|
259
|
+
# Confidence based on how planar the motion is
|
|
260
|
+
# Ratio of smallest to middle singular value
|
|
261
|
+
if S[1] > 1e-6:
|
|
262
|
+
planarity = 1 - (S[2] / S[1])
|
|
263
|
+
confidence = np.clip(planarity, 0.3, 1.0)
|
|
264
|
+
else:
|
|
265
|
+
confidence = 0.5
|
|
266
|
+
|
|
267
|
+
return axis.astype(np.float64), pivot.astype(np.float64), confidence
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def estimate_prismatic_axis(
|
|
271
|
+
relative_motion: RelativeMotion,
|
|
272
|
+
) -> Tuple["np.ndarray", float]:
|
|
273
|
+
"""
|
|
274
|
+
Estimate prismatic joint axis from relative motion.
|
|
275
|
+
|
|
276
|
+
Uses SVD to find the dominant direction of translation.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
relative_motion: RelativeMotion between parent and child
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Tuple of (axis, confidence):
|
|
283
|
+
- axis: (3,) unit vector of translation direction
|
|
284
|
+
- confidence: Quality of axis estimation (0-1)
|
|
285
|
+
"""
|
|
286
|
+
if not DEPS_AVAILABLE:
|
|
287
|
+
raise MotionAnalysisError("NumPy not available")
|
|
288
|
+
|
|
289
|
+
positions = relative_motion.relative_positions
|
|
290
|
+
n = len(positions)
|
|
291
|
+
|
|
292
|
+
if n < 2:
|
|
293
|
+
return np.array([0, 0, 1]), 0.3
|
|
294
|
+
|
|
295
|
+
# Compute displacement from first position
|
|
296
|
+
displacements = positions - positions[0]
|
|
297
|
+
|
|
298
|
+
# SVD to find principal direction
|
|
299
|
+
U, S, Vt = np.linalg.svd(displacements)
|
|
300
|
+
|
|
301
|
+
# Dominant translation direction is first singular vector
|
|
302
|
+
axis = Vt[0]
|
|
303
|
+
|
|
304
|
+
# Ensure consistent direction
|
|
305
|
+
if np.sum(displacements[-1] * axis) < 0:
|
|
306
|
+
axis = -axis
|
|
307
|
+
|
|
308
|
+
# Confidence based on how linear the motion is
|
|
309
|
+
if S[0] > 1e-6:
|
|
310
|
+
linearity = 1 - (S[1] / S[0]) if len(S) > 1 else 1.0
|
|
311
|
+
confidence = np.clip(linearity, 0.3, 1.0)
|
|
312
|
+
else:
|
|
313
|
+
confidence = 0.5
|
|
314
|
+
|
|
315
|
+
return axis.astype(np.float64), confidence
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def estimate_joint_limits(
|
|
319
|
+
relative_motion: RelativeMotion,
|
|
320
|
+
joint_type: str,
|
|
321
|
+
axis: "np.ndarray",
|
|
322
|
+
pivot: Optional["np.ndarray"] = None,
|
|
323
|
+
) -> Tuple[float, float]:
|
|
324
|
+
"""
|
|
325
|
+
Estimate joint limits from observed motion range.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
relative_motion: RelativeMotion data
|
|
329
|
+
joint_type: "revolute" or "prismatic"
|
|
330
|
+
axis: Joint axis
|
|
331
|
+
pivot: Pivot point (for revolute joints)
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Tuple of (lower_limit, upper_limit)
|
|
335
|
+
"""
|
|
336
|
+
if not DEPS_AVAILABLE:
|
|
337
|
+
raise MotionAnalysisError("NumPy not available")
|
|
338
|
+
|
|
339
|
+
positions = relative_motion.relative_positions
|
|
340
|
+
|
|
341
|
+
if joint_type == "revolute":
|
|
342
|
+
# Project positions onto plane perpendicular to axis
|
|
343
|
+
# Then measure angles
|
|
344
|
+
if pivot is None:
|
|
345
|
+
pivot = np.mean(positions, axis=0)
|
|
346
|
+
|
|
347
|
+
relative = positions - pivot
|
|
348
|
+
|
|
349
|
+
# Project onto plane
|
|
350
|
+
axis_component = np.outer(np.dot(relative, axis), axis)
|
|
351
|
+
in_plane = relative - axis_component
|
|
352
|
+
|
|
353
|
+
# Compute angles (relative to first position)
|
|
354
|
+
if len(in_plane) < 2:
|
|
355
|
+
return -np.pi, np.pi
|
|
356
|
+
|
|
357
|
+
ref = in_plane[0]
|
|
358
|
+
ref_norm = np.linalg.norm(ref)
|
|
359
|
+
if ref_norm < 1e-6:
|
|
360
|
+
return -np.pi, np.pi
|
|
361
|
+
|
|
362
|
+
ref = ref / ref_norm
|
|
363
|
+
angles = []
|
|
364
|
+
|
|
365
|
+
for pos in in_plane:
|
|
366
|
+
pos_norm = np.linalg.norm(pos)
|
|
367
|
+
if pos_norm < 1e-6:
|
|
368
|
+
angles.append(0)
|
|
369
|
+
continue
|
|
370
|
+
pos = pos / pos_norm
|
|
371
|
+
angle = np.arctan2(np.dot(np.cross(ref, pos), axis), np.dot(ref, pos))
|
|
372
|
+
angles.append(angle)
|
|
373
|
+
|
|
374
|
+
angles = np.array(angles)
|
|
375
|
+
|
|
376
|
+
# Add some margin to observed range
|
|
377
|
+
margin = 0.1 # ~6 degrees
|
|
378
|
+
return float(np.min(angles) - margin), float(np.max(angles) + margin)
|
|
379
|
+
|
|
380
|
+
else: # prismatic
|
|
381
|
+
# Project onto axis and measure displacement
|
|
382
|
+
displacements = np.dot(positions - positions[0], axis)
|
|
383
|
+
margin = 0.01 # 1cm
|
|
384
|
+
return float(np.min(displacements) - margin), float(np.max(displacements) + margin)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
__all__ = [
|
|
388
|
+
"MotionAnalysisError",
|
|
389
|
+
"LinkTrajectory",
|
|
390
|
+
"RelativeMotion",
|
|
391
|
+
"extract_trajectories",
|
|
392
|
+
"compute_relative_motion",
|
|
393
|
+
"estimate_revolute_axis",
|
|
394
|
+
"estimate_prismatic_axis",
|
|
395
|
+
"estimate_joint_limits",
|
|
396
|
+
]
|