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
ate/urdf/kinematics.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Joint parameter estimation for URDF generation.
|
|
3
|
+
|
|
4
|
+
This module handles Phase 3 of the pipeline:
|
|
5
|
+
1. Fit kinematic parameters to point cloud trajectories
|
|
6
|
+
2. Estimate joint axes, pivots, and limits
|
|
7
|
+
3. Generate kinematics.json output
|
|
8
|
+
|
|
9
|
+
Implements the core kinematic optimization described in the research.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Dict, List, Optional, Tuple, Set
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from dataclasses import dataclass, asdict
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import numpy as np
|
|
21
|
+
NUMPY_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
NUMPY_AVAILABLE = False
|
|
24
|
+
np = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KinematicsError(Exception):
|
|
28
|
+
"""Error during kinematic estimation."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class JointParameters:
|
|
34
|
+
"""Estimated parameters for a single joint."""
|
|
35
|
+
name: str
|
|
36
|
+
parent_link: str
|
|
37
|
+
child_link: str
|
|
38
|
+
joint_type: str # revolute, prismatic, fixed
|
|
39
|
+
axis: List[float] # [x, y, z] unit vector
|
|
40
|
+
origin: List[float] # [x, y, z] position
|
|
41
|
+
limits: Dict[str, float] # lower, upper (radians or meters)
|
|
42
|
+
confidence: float # Estimation confidence [0, 1]
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> Dict:
|
|
45
|
+
return {
|
|
46
|
+
"name": self.name,
|
|
47
|
+
"parent_link": self.parent_link,
|
|
48
|
+
"child_link": self.child_link,
|
|
49
|
+
"type": self.joint_type,
|
|
50
|
+
"axis": self.axis,
|
|
51
|
+
"origin": self.origin,
|
|
52
|
+
"limits": self.limits,
|
|
53
|
+
"confidence": self.confidence,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_per_frame_clouds(
|
|
58
|
+
session: "ScanSession",
|
|
59
|
+
masks: Dict[str, Dict[int, "np.ndarray"]],
|
|
60
|
+
depth_maps: Dict[int, "np.ndarray"],
|
|
61
|
+
) -> Dict[str, List["np.ndarray"]]:
|
|
62
|
+
"""
|
|
63
|
+
Load per-frame point clouds for each link.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
session: ScanSession
|
|
67
|
+
masks: Per-frame segmentation masks
|
|
68
|
+
depth_maps: Per-frame depth maps
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dict mapping link_name -> list of point clouds per frame
|
|
72
|
+
"""
|
|
73
|
+
import cv2
|
|
74
|
+
from .lifting import lift_to_3d
|
|
75
|
+
from .scale import estimate_intrinsics_from_resolution
|
|
76
|
+
|
|
77
|
+
# Get intrinsics
|
|
78
|
+
w, h = session.metadata.resolution
|
|
79
|
+
intrinsics = estimate_intrinsics_from_resolution(w, h)
|
|
80
|
+
|
|
81
|
+
# Open video
|
|
82
|
+
cap = cv2.VideoCapture(str(session.video_path))
|
|
83
|
+
if not cap.isOpened():
|
|
84
|
+
raise KinematicsError(f"Could not open video: {session.video_path}")
|
|
85
|
+
|
|
86
|
+
frame_indices = sorted(depth_maps.keys())
|
|
87
|
+
link_clouds = {name: [] for name in masks.keys()}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
for frame_idx in frame_indices:
|
|
91
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
|
92
|
+
ret, frame = cap.read()
|
|
93
|
+
if not ret:
|
|
94
|
+
for name in masks.keys():
|
|
95
|
+
link_clouds[name].append(None)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
99
|
+
depth = depth_maps[frame_idx]
|
|
100
|
+
|
|
101
|
+
for link_name in masks.keys():
|
|
102
|
+
if frame_idx in masks[link_name]:
|
|
103
|
+
mask = masks[link_name][frame_idx]
|
|
104
|
+
points, _ = lift_to_3d(rgb, depth, mask, intrinsics)
|
|
105
|
+
link_clouds[link_name].append(points if len(points) > 0 else None)
|
|
106
|
+
else:
|
|
107
|
+
link_clouds[link_name].append(None)
|
|
108
|
+
|
|
109
|
+
finally:
|
|
110
|
+
cap.release()
|
|
111
|
+
|
|
112
|
+
return link_clouds
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def estimate_joint_parameters(
|
|
116
|
+
parent_traj: "LinkTrajectory",
|
|
117
|
+
child_traj: "LinkTrajectory",
|
|
118
|
+
joint_name: str,
|
|
119
|
+
joint_type_hint: Optional[str] = None,
|
|
120
|
+
) -> JointParameters:
|
|
121
|
+
"""
|
|
122
|
+
Estimate joint parameters between parent and child links.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
parent_traj: Parent link trajectory
|
|
126
|
+
child_traj: Child link trajectory
|
|
127
|
+
joint_name: Name for this joint
|
|
128
|
+
joint_type_hint: Optional hint for joint type
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
JointParameters with estimated values
|
|
132
|
+
"""
|
|
133
|
+
from .motion_analysis import (
|
|
134
|
+
compute_relative_motion,
|
|
135
|
+
estimate_revolute_axis,
|
|
136
|
+
estimate_prismatic_axis,
|
|
137
|
+
estimate_joint_limits,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Compute relative motion
|
|
141
|
+
rel_motion = compute_relative_motion(parent_traj, child_traj)
|
|
142
|
+
|
|
143
|
+
# Classify joint type
|
|
144
|
+
if joint_type_hint:
|
|
145
|
+
joint_type = joint_type_hint
|
|
146
|
+
confidence = 0.9 # User-provided hint
|
|
147
|
+
else:
|
|
148
|
+
joint_type, confidence = rel_motion.analyze_joint_type()
|
|
149
|
+
|
|
150
|
+
# Estimate axis and origin
|
|
151
|
+
if joint_type == "revolute":
|
|
152
|
+
axis, pivot, axis_conf = estimate_revolute_axis(rel_motion)
|
|
153
|
+
confidence = min(confidence, axis_conf)
|
|
154
|
+
origin = pivot.tolist()
|
|
155
|
+
elif joint_type == "prismatic":
|
|
156
|
+
axis, axis_conf = estimate_prismatic_axis(rel_motion)
|
|
157
|
+
confidence = min(confidence, axis_conf)
|
|
158
|
+
# Origin at parent centroid
|
|
159
|
+
origin = parent_traj.centroids[0].tolist()
|
|
160
|
+
else: # fixed
|
|
161
|
+
axis = [0.0, 0.0, 1.0]
|
|
162
|
+
origin = child_traj.centroids[0].tolist()
|
|
163
|
+
|
|
164
|
+
# Normalize axis
|
|
165
|
+
axis_array = np.array(axis)
|
|
166
|
+
axis_array = axis_array / (np.linalg.norm(axis_array) + 1e-8)
|
|
167
|
+
axis = axis_array.tolist()
|
|
168
|
+
|
|
169
|
+
# Estimate limits
|
|
170
|
+
if joint_type != "fixed":
|
|
171
|
+
lower, upper = estimate_joint_limits(
|
|
172
|
+
rel_motion, joint_type, np.array(axis), np.array(origin)
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
lower, upper = 0.0, 0.0
|
|
176
|
+
|
|
177
|
+
return JointParameters(
|
|
178
|
+
name=joint_name,
|
|
179
|
+
parent_link=parent_traj.name,
|
|
180
|
+
child_link=child_traj.name,
|
|
181
|
+
joint_type=joint_type,
|
|
182
|
+
axis=axis,
|
|
183
|
+
origin=origin,
|
|
184
|
+
limits={"lower": lower, "upper": upper},
|
|
185
|
+
confidence=confidence,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_kinematic_optimization(
|
|
190
|
+
session: "ScanSession",
|
|
191
|
+
masks: Dict[str, Dict[int, "np.ndarray"]],
|
|
192
|
+
depth_maps: Dict[int, "np.ndarray"],
|
|
193
|
+
joint_type_hints: Optional[Dict[str, str]] = None,
|
|
194
|
+
progress_callback: Optional[callable] = None,
|
|
195
|
+
) -> List[JointParameters]:
|
|
196
|
+
"""
|
|
197
|
+
Run kinematic optimization to estimate joint parameters.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
session: ScanSession
|
|
201
|
+
masks: Per-frame segmentation masks
|
|
202
|
+
depth_maps: Per-frame depth maps
|
|
203
|
+
joint_type_hints: Optional dict of link_name -> joint_type
|
|
204
|
+
progress_callback: Optional progress callback
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of JointParameters
|
|
208
|
+
"""
|
|
209
|
+
from .motion_analysis import extract_trajectories
|
|
210
|
+
from .topology import build_kinematic_tree, infer_joint_names
|
|
211
|
+
|
|
212
|
+
logger.info("Starting kinematic optimization...")
|
|
213
|
+
|
|
214
|
+
# Load per-frame clouds
|
|
215
|
+
logger.info("Loading per-frame point clouds...")
|
|
216
|
+
frame_indices = sorted(depth_maps.keys())
|
|
217
|
+
link_clouds = load_per_frame_clouds(session, masks, depth_maps)
|
|
218
|
+
|
|
219
|
+
# Extract trajectories
|
|
220
|
+
logger.info("Extracting link trajectories...")
|
|
221
|
+
trajectories = extract_trajectories(link_clouds, frame_indices)
|
|
222
|
+
|
|
223
|
+
if len(trajectories) < 2:
|
|
224
|
+
raise KinematicsError(
|
|
225
|
+
f"Need at least 2 links for kinematics, got {len(trajectories)}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Get fixed link hints from session
|
|
229
|
+
fixed_hints = {
|
|
230
|
+
link.name for link in session.links if link.is_fixed
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Build kinematic tree
|
|
234
|
+
logger.info("Building kinematic tree...")
|
|
235
|
+
tree = build_kinematic_tree(trajectories, fixed_hints)
|
|
236
|
+
|
|
237
|
+
# Get joint names
|
|
238
|
+
joint_names = infer_joint_names(tree)
|
|
239
|
+
|
|
240
|
+
# Estimate parameters for each joint
|
|
241
|
+
logger.info("Estimating joint parameters...")
|
|
242
|
+
joints = []
|
|
243
|
+
total = len(joint_names)
|
|
244
|
+
|
|
245
|
+
for i, ((parent, child), joint_name) in enumerate(joint_names.items()):
|
|
246
|
+
parent_traj = trajectories[parent]
|
|
247
|
+
child_traj = trajectories[child]
|
|
248
|
+
|
|
249
|
+
# Get type hint if available
|
|
250
|
+
type_hint = None
|
|
251
|
+
if joint_type_hints and child in joint_type_hints:
|
|
252
|
+
type_hint = joint_type_hints[child]
|
|
253
|
+
|
|
254
|
+
params = estimate_joint_parameters(
|
|
255
|
+
parent_traj, child_traj, joint_name, type_hint
|
|
256
|
+
)
|
|
257
|
+
joints.append(params)
|
|
258
|
+
|
|
259
|
+
logger.info(
|
|
260
|
+
f" {joint_name}: {params.joint_type} "
|
|
261
|
+
f"(conf={params.confidence:.2f}, axis={params.axis})"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if progress_callback:
|
|
265
|
+
progress_callback(i + 1, total)
|
|
266
|
+
|
|
267
|
+
# Update session
|
|
268
|
+
from .scan_session import JointInfo
|
|
269
|
+
session.joints = [
|
|
270
|
+
JointInfo(
|
|
271
|
+
name=j.name,
|
|
272
|
+
parent_link=j.parent_link,
|
|
273
|
+
child_link=j.child_link,
|
|
274
|
+
joint_type=j.joint_type,
|
|
275
|
+
axis=j.axis,
|
|
276
|
+
origin=j.origin,
|
|
277
|
+
limits=j.limits,
|
|
278
|
+
)
|
|
279
|
+
for j in joints
|
|
280
|
+
]
|
|
281
|
+
session.save_kinematics()
|
|
282
|
+
session.metadata.optimize_complete = True
|
|
283
|
+
session.save_metadata()
|
|
284
|
+
|
|
285
|
+
logger.info(f"Kinematic optimization complete: {len(joints)} joints")
|
|
286
|
+
return joints
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def save_kinematics_json(
|
|
290
|
+
output_path: Path,
|
|
291
|
+
links: List[str],
|
|
292
|
+
joints: List[JointParameters],
|
|
293
|
+
tree: "KinematicTree",
|
|
294
|
+
) -> None:
|
|
295
|
+
"""
|
|
296
|
+
Save kinematics to JSON file.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
output_path: Path to output file
|
|
300
|
+
links: List of link names
|
|
301
|
+
joints: List of JointParameters
|
|
302
|
+
tree: KinematicTree structure
|
|
303
|
+
"""
|
|
304
|
+
import json
|
|
305
|
+
|
|
306
|
+
data = {
|
|
307
|
+
"links": [
|
|
308
|
+
{
|
|
309
|
+
"name": name,
|
|
310
|
+
"parent": tree.get_parent(name),
|
|
311
|
+
}
|
|
312
|
+
for name in tree.get_ordered_links()
|
|
313
|
+
],
|
|
314
|
+
"joints": [j.to_dict() for j in joints],
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
with open(output_path, 'w') as f:
|
|
318
|
+
json.dump(data, f, indent=2)
|
|
319
|
+
|
|
320
|
+
logger.info(f"Saved kinematics to {output_path}")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
__all__ = [
|
|
324
|
+
"KinematicsError",
|
|
325
|
+
"JointParameters",
|
|
326
|
+
"load_per_frame_clouds",
|
|
327
|
+
"estimate_joint_parameters",
|
|
328
|
+
"run_kinematic_optimization",
|
|
329
|
+
"save_kinematics_json",
|
|
330
|
+
]
|