foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.1__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.
Files changed (116) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +12 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/cli.py +855 -4551
  9. ate/client.py +90 -0
  10. ate/commands/__init__.py +168 -0
  11. ate/commands/auth.py +389 -0
  12. ate/commands/bridge.py +448 -0
  13. ate/commands/data.py +185 -0
  14. ate/commands/deps.py +111 -0
  15. ate/commands/generate.py +384 -0
  16. ate/commands/memory.py +907 -0
  17. ate/commands/parts.py +166 -0
  18. ate/commands/primitive.py +399 -0
  19. ate/commands/protocol.py +288 -0
  20. ate/commands/recording.py +524 -0
  21. ate/commands/repo.py +154 -0
  22. ate/commands/simulation.py +291 -0
  23. ate/commands/skill.py +303 -0
  24. ate/commands/skills.py +487 -0
  25. ate/commands/team.py +147 -0
  26. ate/commands/workflow.py +271 -0
  27. ate/detection/__init__.py +38 -0
  28. ate/detection/base.py +142 -0
  29. ate/detection/color_detector.py +402 -0
  30. ate/detection/trash_detector.py +322 -0
  31. ate/drivers/__init__.py +18 -6
  32. ate/drivers/ble_transport.py +405 -0
  33. ate/drivers/mechdog.py +360 -24
  34. ate/drivers/wifi_camera.py +477 -0
  35. ate/interfaces/__init__.py +16 -0
  36. ate/interfaces/base.py +2 -0
  37. ate/interfaces/sensors.py +247 -0
  38. ate/llm_proxy.py +239 -0
  39. ate/memory/__init__.py +35 -0
  40. ate/memory/cloud.py +244 -0
  41. ate/memory/context.py +269 -0
  42. ate/memory/embeddings.py +184 -0
  43. ate/memory/export.py +26 -0
  44. ate/memory/merge.py +146 -0
  45. ate/memory/migrate/__init__.py +34 -0
  46. ate/memory/migrate/base.py +89 -0
  47. ate/memory/migrate/pipeline.py +189 -0
  48. ate/memory/migrate/sources/__init__.py +13 -0
  49. ate/memory/migrate/sources/chroma.py +170 -0
  50. ate/memory/migrate/sources/pinecone.py +120 -0
  51. ate/memory/migrate/sources/qdrant.py +110 -0
  52. ate/memory/migrate/sources/weaviate.py +160 -0
  53. ate/memory/reranker.py +353 -0
  54. ate/memory/search.py +26 -0
  55. ate/memory/store.py +548 -0
  56. ate/recording/__init__.py +42 -3
  57. ate/recording/session.py +12 -2
  58. ate/recording/visual.py +416 -0
  59. ate/robot/__init__.py +142 -0
  60. ate/robot/agentic_servo.py +856 -0
  61. ate/robot/behaviors.py +493 -0
  62. ate/robot/ble_capture.py +1000 -0
  63. ate/robot/ble_enumerate.py +506 -0
  64. ate/robot/calibration.py +88 -3
  65. ate/robot/calibration_state.py +388 -0
  66. ate/robot/commands.py +143 -11
  67. ate/robot/direction_calibration.py +554 -0
  68. ate/robot/discovery.py +104 -2
  69. ate/robot/llm_system_id.py +654 -0
  70. ate/robot/locomotion_calibration.py +508 -0
  71. ate/robot/marker_generator.py +611 -0
  72. ate/robot/perception.py +502 -0
  73. ate/robot/primitives.py +614 -0
  74. ate/robot/profiles.py +6 -0
  75. ate/robot/registry.py +5 -2
  76. ate/robot/servo_mapper.py +1153 -0
  77. ate/robot/skill_upload.py +285 -3
  78. ate/robot/target_calibration.py +500 -0
  79. ate/robot/teach.py +515 -0
  80. ate/robot/types.py +242 -0
  81. ate/robot/visual_labeler.py +9 -0
  82. ate/robot/visual_servo_loop.py +494 -0
  83. ate/robot/visual_servoing.py +570 -0
  84. ate/robot/visual_system_id.py +906 -0
  85. ate/transports/__init__.py +121 -0
  86. ate/transports/base.py +394 -0
  87. ate/transports/ble.py +405 -0
  88. ate/transports/hybrid.py +444 -0
  89. ate/transports/serial.py +345 -0
  90. ate/urdf/__init__.py +30 -0
  91. ate/urdf/capture.py +582 -0
  92. ate/urdf/cloud.py +491 -0
  93. ate/urdf/collision.py +271 -0
  94. ate/urdf/commands.py +708 -0
  95. ate/urdf/depth.py +360 -0
  96. ate/urdf/inertial.py +312 -0
  97. ate/urdf/kinematics.py +330 -0
  98. ate/urdf/lifting.py +415 -0
  99. ate/urdf/meshing.py +300 -0
  100. ate/urdf/models/__init__.py +110 -0
  101. ate/urdf/models/depth_anything.py +253 -0
  102. ate/urdf/models/sam2.py +324 -0
  103. ate/urdf/motion_analysis.py +396 -0
  104. ate/urdf/pipeline.py +468 -0
  105. ate/urdf/scale.py +256 -0
  106. ate/urdf/scan_session.py +411 -0
  107. ate/urdf/segmentation.py +299 -0
  108. ate/urdf/synthesis.py +319 -0
  109. ate/urdf/topology.py +336 -0
  110. ate/urdf/validation.py +371 -0
  111. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.1.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/WHEEL +1 -1
  114. foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
  115. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.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
+ ]