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.
- devagent_physical_engine/__init__.py +44 -0
- devagent_physical_engine/agent/__init__.py +40 -0
- devagent_physical_engine/agent/compiler.py +285 -0
- devagent_physical_engine/agent/contracts.py +129 -0
- devagent_physical_engine/agent/coordinator.py +108 -0
- devagent_physical_engine/agent/critic.py +72 -0
- devagent_physical_engine/agent/evidence.py +34 -0
- devagent_physical_engine/agent/interpreter.py +179 -0
- devagent_physical_engine/agent/planner.py +105 -0
- devagent_physical_engine/agent/recovery.py +54 -0
- devagent_physical_engine/agent/routing.py +76 -0
- devagent_physical_engine/agent/runtime.py +270 -0
- devagent_physical_engine/agent/semantic.py +304 -0
- devagent_physical_engine/agent/structured.py +423 -0
- devagent_physical_engine/ai_cli.py +226 -0
- devagent_physical_engine/cli.py +392 -0
- devagent_physical_engine/doctor.py +20 -0
- devagent_physical_engine/engineering_agent.py +243 -0
- devagent_physical_engine/engineering_request.py +630 -0
- devagent_physical_engine/execution.py +90 -0
- devagent_physical_engine/models.py +143 -0
- devagent_physical_engine/operating_envelope.py +120 -0
- devagent_physical_engine/optimization/__init__.py +50 -0
- devagent_physical_engine/optimization/benchmark.py +122 -0
- devagent_physical_engine/optimization/candidates.py +198 -0
- devagent_physical_engine/optimization/contracts.py +235 -0
- devagent_physical_engine/optimization/evaluator.py +107 -0
- devagent_physical_engine/optimization/evidence.py +53 -0
- devagent_physical_engine/optimization/experience.py +105 -0
- devagent_physical_engine/optimization/measured.py +125 -0
- devagent_physical_engine/optimization/optimizer.py +215 -0
- devagent_physical_engine/optimization/orchestrator.py +155 -0
- devagent_physical_engine/physical_campaign.py +413 -0
- devagent_physical_engine/physical_evidence.py +214 -0
- devagent_physical_engine/physical_motion.py +196 -0
- devagent_physical_engine/planning.py +80 -0
- devagent_physical_engine/preexecution_contract.py +65 -0
- devagent_physical_engine/provider_adapters/__init__.py +22 -0
- devagent_physical_engine/provider_adapters/anthropic.py +112 -0
- devagent_physical_engine/provider_adapters/common.py +187 -0
- devagent_physical_engine/provider_adapters/factory.py +20 -0
- devagent_physical_engine/provider_adapters/gemini.py +126 -0
- devagent_physical_engine/provider_adapters/openai.py +95 -0
- devagent_physical_engine/provider_qualification.py +268 -0
- devagent_physical_engine/providers.py +94 -0
- devagent_physical_engine/qualification.py +44 -0
- devagent_physical_engine/qualification_cli.py +195 -0
- devagent_physical_engine/qualification_harness.py +917 -0
- devagent_physical_engine/robot_platform.py +411 -0
- devagent_physical_engine/robots.py +76 -0
- devagent_physical_engine/ros2/__init__.py +35 -0
- devagent_physical_engine/ros2/acceptance.py +324 -0
- devagent_physical_engine/ros2/commands.py +175 -0
- devagent_physical_engine/ros2/doctor.py +116 -0
- devagent_physical_engine/ros2/fk_probe.py +83 -0
- devagent_physical_engine/ros2/frame_alignment.py +61 -0
- devagent_physical_engine/ros2/gazebo_world.py +125 -0
- devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
- devagent_physical_engine/ros2/measured_motion.py +233 -0
- devagent_physical_engine/ros2/moveit_scene.py +121 -0
- devagent_physical_engine/ros2/preexecution.py +113 -0
- devagent_physical_engine/ros2/qualification.py +81 -0
- devagent_physical_engine/ros2/qualification_v10.py +252 -0
- devagent_physical_engine/ros2/scene_probe.py +219 -0
- devagent_physical_engine/ros2/state_validity_probe.py +125 -0
- devagent_physical_engine/ros2/tf_probe.py +51 -0
- devagent_physical_engine/ros2/trajectory.py +188 -0
- devagent_physical_engine/ros2/ur5e.py +59 -0
- devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
- devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
- devagent_physical_engine/setup_profile.py +356 -0
- devagent_physical_engine/simulation.py +32 -0
- devagent_physical_engine/simulation_platform.py +269 -0
- devagent_physical_engine/trajectory_qualification.py +201 -0
- devagent_physical_engine/twin.py +939 -0
- devagent_physical_engine/twin_builder.py +309 -0
- devagent_physical_engine/twin_materialization.py +404 -0
- devagent_physical_engine/verification.py +46 -0
- devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
- devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
- devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
- devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
- devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
- 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())
|