devagent-physical-engine 0.10.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.
Files changed (84) hide show
  1. devagent_physical_engine/__init__.py +44 -0
  2. devagent_physical_engine/agent/__init__.py +40 -0
  3. devagent_physical_engine/agent/compiler.py +285 -0
  4. devagent_physical_engine/agent/contracts.py +129 -0
  5. devagent_physical_engine/agent/coordinator.py +108 -0
  6. devagent_physical_engine/agent/critic.py +72 -0
  7. devagent_physical_engine/agent/evidence.py +34 -0
  8. devagent_physical_engine/agent/interpreter.py +179 -0
  9. devagent_physical_engine/agent/planner.py +105 -0
  10. devagent_physical_engine/agent/recovery.py +54 -0
  11. devagent_physical_engine/agent/routing.py +76 -0
  12. devagent_physical_engine/agent/runtime.py +270 -0
  13. devagent_physical_engine/agent/semantic.py +304 -0
  14. devagent_physical_engine/agent/structured.py +423 -0
  15. devagent_physical_engine/ai_cli.py +226 -0
  16. devagent_physical_engine/cli.py +392 -0
  17. devagent_physical_engine/doctor.py +20 -0
  18. devagent_physical_engine/engineering_agent.py +243 -0
  19. devagent_physical_engine/engineering_request.py +630 -0
  20. devagent_physical_engine/execution.py +90 -0
  21. devagent_physical_engine/models.py +143 -0
  22. devagent_physical_engine/operating_envelope.py +120 -0
  23. devagent_physical_engine/optimization/__init__.py +50 -0
  24. devagent_physical_engine/optimization/benchmark.py +122 -0
  25. devagent_physical_engine/optimization/candidates.py +198 -0
  26. devagent_physical_engine/optimization/contracts.py +235 -0
  27. devagent_physical_engine/optimization/evaluator.py +107 -0
  28. devagent_physical_engine/optimization/evidence.py +53 -0
  29. devagent_physical_engine/optimization/experience.py +105 -0
  30. devagent_physical_engine/optimization/measured.py +125 -0
  31. devagent_physical_engine/optimization/optimizer.py +215 -0
  32. devagent_physical_engine/optimization/orchestrator.py +155 -0
  33. devagent_physical_engine/physical_campaign.py +413 -0
  34. devagent_physical_engine/physical_evidence.py +214 -0
  35. devagent_physical_engine/physical_motion.py +196 -0
  36. devagent_physical_engine/planning.py +80 -0
  37. devagent_physical_engine/preexecution_contract.py +65 -0
  38. devagent_physical_engine/provider_adapters/__init__.py +22 -0
  39. devagent_physical_engine/provider_adapters/anthropic.py +112 -0
  40. devagent_physical_engine/provider_adapters/common.py +187 -0
  41. devagent_physical_engine/provider_adapters/factory.py +20 -0
  42. devagent_physical_engine/provider_adapters/gemini.py +126 -0
  43. devagent_physical_engine/provider_adapters/openai.py +95 -0
  44. devagent_physical_engine/provider_qualification.py +268 -0
  45. devagent_physical_engine/providers.py +94 -0
  46. devagent_physical_engine/qualification.py +44 -0
  47. devagent_physical_engine/qualification_cli.py +195 -0
  48. devagent_physical_engine/qualification_harness.py +917 -0
  49. devagent_physical_engine/robot_platform.py +411 -0
  50. devagent_physical_engine/robots.py +76 -0
  51. devagent_physical_engine/ros2/__init__.py +35 -0
  52. devagent_physical_engine/ros2/acceptance.py +324 -0
  53. devagent_physical_engine/ros2/commands.py +175 -0
  54. devagent_physical_engine/ros2/doctor.py +116 -0
  55. devagent_physical_engine/ros2/fk_probe.py +83 -0
  56. devagent_physical_engine/ros2/frame_alignment.py +61 -0
  57. devagent_physical_engine/ros2/gazebo_world.py +125 -0
  58. devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
  59. devagent_physical_engine/ros2/measured_motion.py +233 -0
  60. devagent_physical_engine/ros2/moveit_scene.py +121 -0
  61. devagent_physical_engine/ros2/preexecution.py +113 -0
  62. devagent_physical_engine/ros2/qualification.py +81 -0
  63. devagent_physical_engine/ros2/qualification_v10.py +252 -0
  64. devagent_physical_engine/ros2/scene_probe.py +219 -0
  65. devagent_physical_engine/ros2/state_validity_probe.py +125 -0
  66. devagent_physical_engine/ros2/tf_probe.py +51 -0
  67. devagent_physical_engine/ros2/trajectory.py +188 -0
  68. devagent_physical_engine/ros2/ur5e.py +59 -0
  69. devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
  70. devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
  71. devagent_physical_engine/setup_profile.py +356 -0
  72. devagent_physical_engine/simulation.py +32 -0
  73. devagent_physical_engine/simulation_platform.py +269 -0
  74. devagent_physical_engine/trajectory_qualification.py +201 -0
  75. devagent_physical_engine/twin.py +939 -0
  76. devagent_physical_engine/twin_builder.py +309 -0
  77. devagent_physical_engine/twin_materialization.py +404 -0
  78. devagent_physical_engine/verification.py +46 -0
  79. devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
  80. devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
  81. devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
  82. devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
  83. devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
  84. devagent_physical_engine-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from ..physical_motion import PhysicalMotionPlan
8
+ from ..preexecution_contract import PreExecutionVerification
9
+ from ..simulation_platform import SimulationPlatformError
10
+ from ..twin_materialization import CanonicalTwinMaterialization
11
+ from .commands import RosCommandRunner
12
+
13
+
14
+ class MoveItPreExecutionVerifier:
15
+ """Fail-closed sampled collision verification for an exact motion hash.
16
+
17
+ This v0.10 verifier is deliberately *not* continuous collision checking. Its
18
+ receipt carries that limitation so adapter qualification cannot silently
19
+ upgrade it to commissioning-grade evidence.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ runner: RosCommandRunner,
25
+ *,
26
+ work_dir: Path | str = "~/.devagent/physical-simulation/preexecution",
27
+ timeout_s: float = 10.0,
28
+ max_joint_step_rad: float = 0.03,
29
+ max_samples: int = 4000,
30
+ planning_group: str = "ur_manipulator",
31
+ ) -> None:
32
+ if timeout_s <= 0 or max_joint_step_rad <= 0 or max_samples <= 1:
33
+ raise ValueError("preexecution_verifier_limits_invalid")
34
+ if not planning_group.strip():
35
+ raise ValueError("preexecution_planning_group_required")
36
+ self.runner = runner
37
+ self.work_dir = Path(work_dir).expanduser().resolve()
38
+ self.timeout_s = float(timeout_s)
39
+ self.max_joint_step_rad = float(max_joint_step_rad)
40
+ self.max_samples = int(max_samples)
41
+ self.planning_group = planning_group
42
+
43
+ def verify(
44
+ self,
45
+ *,
46
+ motion: PhysicalMotionPlan,
47
+ materialization: CanonicalTwinMaterialization,
48
+ ) -> PreExecutionVerification:
49
+ if motion.twin_hash != materialization.twin_hash:
50
+ raise SimulationPlatformError("preexecution_twin_hash_mismatch")
51
+ self.work_dir.mkdir(parents=True, exist_ok=True)
52
+ payload = {
53
+ "motion_hash": motion.fingerprint,
54
+ "twin_hash": motion.twin_hash,
55
+ "materialization_hash": materialization.fingerprint,
56
+ "motion": motion.to_dict(),
57
+ }
58
+ input_path = self.work_dir / f"{motion.fingerprint}.state-validity.json"
59
+ temporary = input_path.with_suffix(input_path.suffix + ".tmp")
60
+ temporary.write_text(
61
+ json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False),
62
+ encoding="utf-8",
63
+ )
64
+ temporary.replace(input_path)
65
+ helper = Path(__file__).with_name("state_validity_probe.py").resolve()
66
+ result = self.runner.run(
67
+ (
68
+ "python3",
69
+ str(helper),
70
+ "check",
71
+ "--input",
72
+ str(input_path),
73
+ "--timeout",
74
+ format(self.timeout_s, ".6g"),
75
+ "--max-joint-step",
76
+ format(self.max_joint_step_rad, ".8g"),
77
+ "--max-samples",
78
+ str(self.max_samples),
79
+ "--group",
80
+ self.planning_group,
81
+ ),
82
+ timeout_s=max(self.timeout_s * max(2, min(self.max_samples, 100)), self.timeout_s + 10.0),
83
+ )
84
+ try:
85
+ output = json.loads(result.stdout.strip().splitlines()[-1])
86
+ except (IndexError, json.JSONDecodeError) as exc:
87
+ raise SimulationPlatformError("preexecution_probe_output_invalid") from exc
88
+ if not isinstance(output, dict):
89
+ raise SimulationPlatformError("preexecution_probe_output_invalid")
90
+ verified = bool(output.get("success")) and result.ok
91
+ code = str(output.get("code") or "preexecution_probe_failed")
92
+ failure_codes = () if verified else (code,)
93
+ evidence: dict[str, Any] = {
94
+ key: output.get(key)
95
+ for key in (
96
+ "evidence_hash",
97
+ "first_invalid",
98
+ "group_name",
99
+ "max_joint_step_rad",
100
+ )
101
+ if key in output
102
+ }
103
+ return PreExecutionVerification(
104
+ verified=verified,
105
+ method="moveit_get_state_validity_discrete_v1",
106
+ motion_hash=motion.fingerprint,
107
+ twin_hash=motion.twin_hash,
108
+ materialization_hash=materialization.fingerprint,
109
+ continuous_collision_check=False,
110
+ sample_count=int(output.get("sample_count") or 0),
111
+ failure_codes=failure_codes,
112
+ evidence=evidence,
113
+ )
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from hashlib import sha256
4
+
5
+ from ..physical_motion import JointTrajectoryPoint, PhysicalMotionPlan
6
+ from ..trajectory_qualification import TrajectoryQualificationCase
7
+ from ..twin import TwinSpec
8
+ from .ur5e_adapter import UR5E_JOINTS
9
+
10
+
11
+ REFERENCE_GRAPH_HASH = sha256(
12
+ b"devagent-v0.9-ur5e-trajectory-runtime-reference-corpus"
13
+ ).hexdigest()
14
+ UR5E_REFERENCE_START = (0.0, -1.57, 0.0, -1.57, 0.0, 0.0)
15
+
16
+
17
+ def ur5e_trajectory_runtime_twin() -> TwinSpec:
18
+ """A robot-only identity twin used strictly for trajectory-runtime qualification.
19
+
20
+ It is intentionally not a Level-2/3 cell twin and must never be used to make
21
+ environment, collision, physics, or commissioning claims.
22
+ """
23
+
24
+ return TwinSpec(
25
+ twin_id="TWIN-UR5E-TRAJECTORY-RUNTIME-V1",
26
+ robot_profile_key="ur5e",
27
+ robot_base_pose=None,
28
+ entities=(),
29
+ source_entity_id="runtime_probe_source",
30
+ destination_entity_id="runtime_probe_destination",
31
+ workpiece_entity_id=None,
32
+ metadata={"qualification_scope": "trajectory_runtime_only"},
33
+ )
34
+
35
+
36
+ def _motion(
37
+ twin: TwinSpec,
38
+ *,
39
+ motion_id: str,
40
+ points: tuple[tuple[float, ...], ...],
41
+ ) -> PhysicalMotionPlan:
42
+ timeline = tuple(
43
+ JointTrajectoryPoint(positions, float(index) * 4.0)
44
+ for index, positions in enumerate(points)
45
+ )
46
+ return PhysicalMotionPlan(
47
+ motion_id=motion_id,
48
+ robot_profile_key="ur5e",
49
+ joint_names=UR5E_JOINTS,
50
+ points=timeline,
51
+ source_graph_hash=REFERENCE_GRAPH_HASH,
52
+ planner_id="devagent-reference-trajectory-v1",
53
+ twin_hash=twin.fingerprint,
54
+ constraints=("simulation_only", "trajectory_runtime_qualification"),
55
+ metadata={"customer_plan": False, "environment_qualification": False},
56
+ )
57
+
58
+
59
+ def ur5e_trajectory_runtime_cases(
60
+ twin: TwinSpec,
61
+ ) -> tuple[TrajectoryQualificationCase, ...]:
62
+ plus = (0.15, -1.45, 0.10, -1.45, 0.10, 0.10)
63
+ minus = (-0.15, -1.65, -0.10, -1.65, -0.10, -0.10)
64
+ motions = (
65
+ _motion(twin, motion_id="UR5E-RUNTIME-001", points=(UR5E_REFERENCE_START, plus)),
66
+ _motion(twin, motion_id="UR5E-RUNTIME-002", points=(UR5E_REFERENCE_START, minus)),
67
+ _motion(
68
+ twin,
69
+ motion_id="UR5E-RUNTIME-003",
70
+ points=(UR5E_REFERENCE_START, plus, minus, UR5E_REFERENCE_START),
71
+ ),
72
+ )
73
+ return tuple(
74
+ TrajectoryQualificationCase(
75
+ case_id=f"UR5E-TRAJECTORY-{index:03d}",
76
+ motion=motion,
77
+ reset_state={"initial_joint_positions_rad": list(UR5E_REFERENCE_START)},
78
+ max_tracking_error_rad=0.05,
79
+ )
80
+ for index, motion in enumerate(motions, start=1)
81
+ )
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from dataclasses import asdict, dataclass
5
+ from datetime import datetime, timezone
6
+ from hashlib import sha256
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ..physical_motion import JointTrajectoryPoint, PhysicalMotionPlan
12
+ from ..twin import (
13
+ ControllerModel,
14
+ EvidenceOrigin,
15
+ EvidenceValue,
16
+ GeometryKind,
17
+ GeometrySpec,
18
+ PhysicsSpec,
19
+ Pose3D,
20
+ ToolSpec,
21
+ TwinEntity,
22
+ TwinSpec,
23
+ )
24
+ from .commands import SubprocessRosRunner
25
+ from .ur5e_adapter import UR5E_JOINTS
26
+ from .ur5e_v10_adapter import UR5eCanonicalTwinAdapter
27
+
28
+
29
+ _REFERENCE_GRAPH_HASH = sha256(b"devagent-v0.10-canonical-twin-reference-corpus").hexdigest()
30
+ _REFERENCE_START = (0.0, -1.57, 0.0, -1.57, 0.0, 0.0)
31
+
32
+
33
+ def _pose(x: float, y: float, z: float, *, origin: EvidenceOrigin = EvidenceOrigin.MEASURED) -> Pose3D:
34
+ return Pose3D(x, y, z, origin=origin, frame_id="world")
35
+
36
+
37
+ def _box(x: float, y: float, z: float, *, origin: EvidenceOrigin = EvidenceOrigin.IMPORTED) -> GeometrySpec:
38
+ return GeometrySpec(GeometryKind.BOX, origin, dimensions_m=(x, y, z), collision_geometry=True)
39
+
40
+
41
+ def ur5e_canonical_reference_twin() -> TwinSpec:
42
+ """Level-3-like reference Twin for canonical environment/runtime qualification."""
43
+ source = TwinEntity("reference_source", "fixture", _pose(2.0, 0.0, 0.4), _box(1.0, 0.8, 0.8))
44
+ destination = TwinEntity("reference_destination", "fixture", _pose(2.0, 1.2, 0.4), _box(1.0, 0.8, 0.8))
45
+ workpiece = TwinEntity(
46
+ "reference_workpiece",
47
+ "workpiece",
48
+ _pose(2.0, 0.0, 0.825),
49
+ _box(0.20, 0.10, 0.05),
50
+ physics=PhysicsSpec(
51
+ mass_kg=EvidenceValue(1.0, EvidenceOrigin.MEASURED, source_ref="v0.10-reference"),
52
+ friction_coefficient=EvidenceValue(0.5, EvidenceOrigin.MEASURED, source_ref="v0.10-reference"),
53
+ restitution=EvidenceValue(0.0, EvidenceOrigin.MEASURED, source_ref="v0.10-reference"),
54
+ ),
55
+ )
56
+ return TwinSpec(
57
+ twin_id="TWIN-UR5E-CANONICAL-V10-REFERENCE",
58
+ robot_profile_key="ur5e",
59
+ robot_base_pose=_pose(0.0, 0.0, 0.0),
60
+ entities=(source, destination, workpiece),
61
+ source_entity_id=source.entity_id,
62
+ destination_entity_id=destination.entity_id,
63
+ workpiece_entity_id=workpiece.entity_id,
64
+ tool=ToolSpec(
65
+ tool_id="reference_tool",
66
+ tcp=_pose(0.0, 0.0, 0.0),
67
+ geometry=_box(0.01, 0.01, 0.01, origin=EvidenceOrigin.ROBOT_PROFILE),
68
+ max_payload_kg=EvidenceValue(5.0, EvidenceOrigin.ROBOT_PROFILE),
69
+ ),
70
+ controller=ControllerModel(speed_scale_pct=EvidenceValue(100.0, EvidenceOrigin.MEASURED)),
71
+ metadata={
72
+ "qualification_scope": "canonical_twin_materialization_runtime_only",
73
+ "customer_plan": False,
74
+ "site_qualification": False,
75
+ },
76
+ )
77
+
78
+
79
+ def _motion(twin: TwinSpec, *, motion_id: str, points: tuple[tuple[float, ...], ...]) -> PhysicalMotionPlan:
80
+ trajectory = tuple(
81
+ JointTrajectoryPoint(positions_rad=positions, time_from_start_s=float(index) * 4.0)
82
+ for index, positions in enumerate(points)
83
+ )
84
+ return PhysicalMotionPlan(
85
+ motion_id=motion_id,
86
+ robot_profile_key="ur5e",
87
+ joint_names=UR5E_JOINTS,
88
+ points=trajectory,
89
+ source_graph_hash=_REFERENCE_GRAPH_HASH,
90
+ planner_id="devagent-v10-reference-trajectory",
91
+ twin_hash=twin.fingerprint,
92
+ constraints=("simulation_only", "canonical_twin_scope_qualification"),
93
+ metadata={"customer_plan": False, "workpiece_manipulation": False},
94
+ )
95
+
96
+
97
+ def ur5e_canonical_reference_motions(twin: TwinSpec) -> tuple[PhysicalMotionPlan, ...]:
98
+ plus = (0.12, -1.47, 0.08, -1.47, 0.08, 0.08)
99
+ minus = (-0.12, -1.67, -0.08, -1.67, -0.08, -0.08)
100
+ return (
101
+ _motion(twin, motion_id="UR5E-V10-CANONICAL-001", points=(_REFERENCE_START, plus)),
102
+ _motion(twin, motion_id="UR5E-V10-CANONICAL-002", points=(_REFERENCE_START, minus)),
103
+ _motion(twin, motion_id="UR5E-V10-CANONICAL-003", points=(_REFERENCE_START, plus, minus, _REFERENCE_START)),
104
+ )
105
+
106
+
107
+ @dataclass(frozen=True, slots=True)
108
+ class CanonicalQualificationCaseResult:
109
+ motion_hash: str
110
+ success: bool
111
+ failure_code: str | None
112
+ metrics: dict[str, Any]
113
+ state: dict[str, Any]
114
+
115
+ def to_dict(self) -> dict[str, Any]:
116
+ return asdict(self)
117
+
118
+
119
+ @dataclass(frozen=True, slots=True)
120
+ class CanonicalQualificationReport:
121
+ run_id: str
122
+ scope: str
123
+ adapter_id: str
124
+ twin_hash: str
125
+ total_cases: int
126
+ passed_cases: int
127
+ promotion_candidate: bool
128
+ physical_qualification: bool
129
+ commissioning_qualification: bool
130
+ continuous_collision_check: bool
131
+ minimum_clearance_measured: bool
132
+ results: tuple[CanonicalQualificationCaseResult, ...]
133
+
134
+ def to_dict(self) -> dict[str, Any]:
135
+ return {
136
+ **asdict(self),
137
+ "real_execution_allowed": False,
138
+ "site_qualification": False,
139
+ "results": [item.to_dict() for item in self.results],
140
+ }
141
+
142
+
143
+ def run_ur5e_canonical_qualification(
144
+ runner,
145
+ *,
146
+ ros_setup: Path | str = "/opt/ros/jazzy/setup.bash",
147
+ log_dir: Path | str = "~/.devagent/v10-canonical-qualification",
148
+ startup_timeout_s: float = 120.0,
149
+ ) -> CanonicalQualificationReport:
150
+ twin = ur5e_canonical_reference_twin()
151
+ motions = ur5e_canonical_reference_motions(twin)
152
+ adapter = UR5eCanonicalTwinAdapter(
153
+ runner,
154
+ ros_setup=ros_setup,
155
+ log_dir=log_dir,
156
+ startup_timeout_s=startup_timeout_s,
157
+ )
158
+ results: list[CanonicalQualificationCaseResult] = []
159
+ try:
160
+ adapter.prepare(twin=twin)
161
+ for index, motion in enumerate(motions, start=1):
162
+ failure: str | None = None
163
+ try:
164
+ adapter.reset(
165
+ case_id=f"UR5E-V10-{index:03d}",
166
+ state={"initial_joint_positions_rad": list(_REFERENCE_START)},
167
+ )
168
+ metrics = adapter.execute_motion(motion=motion)
169
+ state = dict(adapter.capture_state())
170
+ preexecution = state.get("preexecution") or {}
171
+ environment_ok = all(
172
+ bool((state.get(key) or {}).get("verified"))
173
+ for key in ("frame_alignment", "gazebo_world", "moveit_scene")
174
+ )
175
+ measured_ok = all(
176
+ value is not None
177
+ for value in (
178
+ metrics.observed_duration_s,
179
+ metrics.measured_joint_travel_rad,
180
+ metrics.path_length_m,
181
+ metrics.final_tcp_error_m,
182
+ )
183
+ )
184
+ passed = metrics.success and environment_ok and measured_ok and bool(preexecution.get("verified"))
185
+ if not passed:
186
+ failure = "canonical_scope_evidence_incomplete"
187
+ except Exception as exc:
188
+ passed = False
189
+ metrics = None
190
+ state = {}
191
+ failure = f"canonical_scope_error:{type(exc).__name__}"
192
+
193
+ results.append(CanonicalQualificationCaseResult(
194
+ motion_hash=motion.fingerprint,
195
+ success=passed,
196
+ failure_code=failure,
197
+ metrics=metrics.to_dict() if metrics is not None else {},
198
+ state=state,
199
+ ))
200
+ finally:
201
+ adapter.stop()
202
+
203
+ passed_cases = sum(item.success for item in results)
204
+ promotion_candidate = bool(results) and passed_cases == len(results)
205
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
206
+ return CanonicalQualificationReport(
207
+ run_id=f"canonical-twin-{timestamp}",
208
+ scope="canonical_twin_runtime_experimental",
209
+ adapter_id=adapter.descriptor.adapter_id,
210
+ twin_hash=twin.fingerprint,
211
+ total_cases=len(results),
212
+ passed_cases=passed_cases,
213
+ promotion_candidate=promotion_candidate,
214
+ physical_qualification=False,
215
+ commissioning_qualification=False,
216
+ continuous_collision_check=False,
217
+ minimum_clearance_measured=False,
218
+ results=tuple(results),
219
+ )
220
+
221
+
222
+ def main(argv: list[str] | None = None) -> int:
223
+ parser = argparse.ArgumentParser(prog="python -m devagent_physical_engine.ros2.qualification_v10")
224
+ parser.add_argument("--ros-setup", default="/opt/ros/jazzy/setup.bash")
225
+ parser.add_argument("--workspace-setup", default=None)
226
+ parser.add_argument("--log-dir", default="~/.devagent/v10-canonical-qualification")
227
+ parser.add_argument("--report", default=None)
228
+ parser.add_argument("--startup-timeout", type=float, default=120.0)
229
+ args = parser.parse_args(argv)
230
+
231
+ runner = SubprocessRosRunner(ros_setup=args.ros_setup, workspace_setup=args.workspace_setup)
232
+ report = run_ur5e_canonical_qualification(
233
+ runner,
234
+ ros_setup=args.ros_setup,
235
+ log_dir=args.log_dir,
236
+ startup_timeout_s=args.startup_timeout,
237
+ )
238
+ payload = report.to_dict()
239
+ report_path = (
240
+ Path(args.report).expanduser().resolve()
241
+ if args.report
242
+ else Path(args.log_dir).expanduser().resolve() / "latest-canonical-twin.json"
243
+ )
244
+ report_path.parent.mkdir(parents=True, exist_ok=True)
245
+ report_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
246
+ print(json.dumps(payload, indent=2))
247
+ print(f"Canonical Twin qualification report: {report_path}")
248
+ return 0 if report.promotion_candidate else 7
249
+
250
+
251
+ if __name__ == "__main__":
252
+ raise SystemExit(main())
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from hashlib import sha256
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def _canon_float(value: float) -> float:
11
+ value = float(value)
12
+ if abs(value) < 1e-12:
13
+ return 0.0
14
+ return float(format(value, ".12g"))
15
+
16
+
17
+ def _hash(payload: object) -> str:
18
+ text = json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False)
19
+ return sha256(text.encode("utf-8")).hexdigest()
20
+
21
+
22
+ def _canonical_scene(planning_frame: str, objects: list[dict[str, Any]]) -> dict[str, Any]:
23
+ return {
24
+ "planning_frame": planning_frame,
25
+ "objects": [
26
+ {
27
+ "id": item["id"],
28
+ "geometry_kind": item["geometry_kind"],
29
+ "dimensions_m": [_canon_float(value) for value in item["dimensions_m"]],
30
+ "position_m": [_canon_float(value) for value in item["position_m"]],
31
+ "quaternion_xyzw": [_canon_float(value) for value in item["quaternion_xyzw"]],
32
+ }
33
+ for item in sorted(objects, key=lambda value: value["id"])
34
+ ],
35
+ }
36
+
37
+
38
+ def _qmul(first, second):
39
+ ax, ay, az, aw = first
40
+ bx, by, bz, bw = second
41
+ return (
42
+ aw * bx + ax * bw + ay * bz - az * by,
43
+ aw * by - ax * bz + ay * bw + az * bx,
44
+ aw * bz + ax * by - ay * bx + az * bw,
45
+ aw * bw - ax * bx - ay * by - az * bz,
46
+ )
47
+
48
+
49
+ def _qconj(quaternion):
50
+ return (-quaternion[0], -quaternion[1], -quaternion[2], quaternion[3])
51
+
52
+
53
+ def _qrot(quaternion, vector):
54
+ result = _qmul(_qmul(quaternion, (vector[0], vector[1], vector[2], 0.0)), _qconj(quaternion))
55
+ return result[:3]
56
+
57
+
58
+ def _combine_pose(parent, child):
59
+ parent_q = (parent.orientation.x, parent.orientation.y, parent.orientation.z, parent.orientation.w)
60
+ child_q = (child.orientation.x, child.orientation.y, child.orientation.z, child.orientation.w)
61
+ rotated = _qrot(parent_q, (child.position.x, child.position.y, child.position.z))
62
+ position = (
63
+ parent.position.x + rotated[0],
64
+ parent.position.y + rotated[1],
65
+ parent.position.z + rotated[2],
66
+ )
67
+ return position, _qmul(parent_q, child_q)
68
+
69
+
70
+ def _wait_future(node, future, timeout_s: float) -> bool:
71
+ import rclpy
72
+ rclpy.spin_until_future_complete(node, future, timeout_sec=float(timeout_s))
73
+ return future.done()
74
+
75
+
76
+ def apply_verify(path: Path, timeout_s: float) -> dict[str, Any]:
77
+ payload = json.loads(path.read_text(encoding="utf-8"))
78
+ planning_frame = str(payload["planning_frame"])
79
+ expected_hash = str(payload["scene_hash"])
80
+ expected_objects = list(payload["objects"])
81
+
82
+ import rclpy
83
+ from geometry_msgs.msg import Pose
84
+ from moveit_msgs.msg import CollisionObject, PlanningScene, PlanningSceneComponents
85
+ from moveit_msgs.srv import ApplyPlanningScene, GetPlanningScene
86
+ from shape_msgs.msg import SolidPrimitive
87
+
88
+ rclpy.init()
89
+ node = rclpy.create_node("devagent_scene_probe")
90
+ apply_client = node.create_client(ApplyPlanningScene, "/apply_planning_scene")
91
+ get_client = node.create_client(GetPlanningScene, "/get_planning_scene")
92
+ try:
93
+ if not apply_client.wait_for_service(timeout_sec=timeout_s):
94
+ return {"success": False, "code": "apply_planning_scene_unavailable"}
95
+ if not get_client.wait_for_service(timeout_sec=timeout_s):
96
+ return {"success": False, "code": "get_planning_scene_unavailable"}
97
+
98
+ scene = PlanningScene()
99
+ scene.is_diff = True
100
+ for item in expected_objects:
101
+ collision = CollisionObject()
102
+ collision.header.frame_id = planning_frame
103
+ collision.id = str(item["id"])
104
+ primitive = SolidPrimitive()
105
+ dimensions = [float(value) for value in item["dimensions_m"]]
106
+ kind = str(item["geometry_kind"])
107
+ if kind == "box":
108
+ primitive.type = SolidPrimitive.BOX
109
+ primitive.dimensions = dimensions
110
+ elif kind == "cylinder":
111
+ primitive.type = SolidPrimitive.CYLINDER
112
+ primitive.dimensions = [dimensions[1], dimensions[0]]
113
+ else:
114
+ return {"success": False, "code": f"unsupported_geometry:{kind}"}
115
+
116
+ pose_data = item["pose"]
117
+ position = pose_data["position_m"]
118
+ quaternion = pose_data["quaternion_xyzw"]
119
+ pose = Pose()
120
+ pose.position.x = float(position[0])
121
+ pose.position.y = float(position[1])
122
+ pose.position.z = float(position[2])
123
+ pose.orientation.x = float(quaternion[0])
124
+ pose.orientation.y = float(quaternion[1])
125
+ pose.orientation.z = float(quaternion[2])
126
+ pose.orientation.w = float(quaternion[3])
127
+ collision.pose = pose
128
+ identity = Pose()
129
+ identity.orientation.w = 1.0
130
+ collision.primitives = [primitive]
131
+ collision.primitive_poses = [identity]
132
+ collision.operation = CollisionObject.ADD
133
+ scene.world.collision_objects.append(collision)
134
+
135
+ apply_request = ApplyPlanningScene.Request()
136
+ apply_request.scene = scene
137
+ future = apply_client.call_async(apply_request)
138
+ if not _wait_future(node, future, timeout_s):
139
+ return {"success": False, "code": "apply_planning_scene_timeout"}
140
+ response = future.result()
141
+ if response is None or not response.success:
142
+ return {"success": False, "code": "apply_planning_scene_rejected"}
143
+
144
+ request = GetPlanningScene.Request()
145
+ request.components.components = PlanningSceneComponents.WORLD_OBJECT_GEOMETRY
146
+ future = get_client.call_async(request)
147
+ if not _wait_future(node, future, timeout_s):
148
+ return {"success": False, "code": "get_planning_scene_timeout"}
149
+ response = future.result()
150
+ if response is None:
151
+ return {"success": False, "code": "get_planning_scene_empty"}
152
+
153
+ expected_ids = {str(item["id"]) for item in expected_objects}
154
+ observed: list[dict[str, Any]] = []
155
+ for collision in response.scene.world.collision_objects:
156
+ if collision.id not in expected_ids:
157
+ continue
158
+ if collision.header.frame_id != planning_frame:
159
+ return {
160
+ "success": False,
161
+ "code": f"scene_planning_frame_mismatch:{collision.id}:{collision.header.frame_id}",
162
+ }
163
+ if len(collision.primitives) != 1 or len(collision.primitive_poses) != 1:
164
+ return {"success": False, "code": f"scene_shape_invalid:{collision.id}"}
165
+ primitive = collision.primitives[0]
166
+ primitive_pose = collision.primitive_poses[0]
167
+ position, quaternion = _combine_pose(collision.pose, primitive_pose)
168
+ if primitive.type == SolidPrimitive.BOX:
169
+ kind = "box"
170
+ dimensions = list(primitive.dimensions)
171
+ elif primitive.type == SolidPrimitive.CYLINDER:
172
+ kind = "cylinder"
173
+ dimensions = [
174
+ primitive.dimensions[SolidPrimitive.CYLINDER_RADIUS],
175
+ primitive.dimensions[SolidPrimitive.CYLINDER_HEIGHT],
176
+ ]
177
+ else:
178
+ return {"success": False, "code": f"scene_shape_unsupported:{collision.id}"}
179
+ observed.append({
180
+ "id": collision.id,
181
+ "geometry_kind": kind,
182
+ "dimensions_m": dimensions,
183
+ "position_m": list(position),
184
+ "quaternion_xyzw": list(quaternion),
185
+ })
186
+
187
+ observed_scene = _canonical_scene(planning_frame, observed)
188
+ observed_hash = _hash(observed_scene)
189
+ missing = sorted(expected_ids - {item["id"] for item in observed})
190
+ success = not missing and len(observed) == len(expected_objects) and observed_hash == expected_hash
191
+ return {
192
+ "success": success,
193
+ "code": "moveit_scene_readback_verified" if success else "moveit_scene_readback_mismatch",
194
+ "observed_scene_hash": observed_hash,
195
+ "observed_object_count": len(observed),
196
+ "missing_object_ids": missing,
197
+ }
198
+ finally:
199
+ node.destroy_node()
200
+ rclpy.shutdown()
201
+
202
+
203
+ def main() -> int:
204
+ parser = argparse.ArgumentParser()
205
+ sub = parser.add_subparsers(dest="command", required=True)
206
+ apply = sub.add_parser("apply-verify")
207
+ apply.add_argument("--input", required=True)
208
+ apply.add_argument("--timeout", type=float, default=20.0)
209
+ args = parser.parse_args()
210
+ try:
211
+ result = apply_verify(Path(args.input).expanduser().resolve(), float(args.timeout))
212
+ except Exception as exc:
213
+ result = {"success": False, "code": f"moveit_scene_probe_error:{type(exc).__name__}"}
214
+ print(json.dumps(result, sort_keys=True))
215
+ return 0 if result.get("success") else 2
216
+
217
+
218
+ if __name__ == "__main__":
219
+ raise SystemExit(main())