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,125 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from hashlib import sha256
5
+ import json
6
+ from math import ceil
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ def _hash(payload: object) -> str:
12
+ text = json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False)
13
+ return sha256(text.encode("utf-8")).hexdigest()
14
+
15
+
16
+ def _sample_motion(motion: dict[str, Any], *, max_joint_step_rad: float, max_samples: int) -> list[tuple[float, ...]]:
17
+ points = motion["points"]
18
+ samples: list[tuple[float, ...]] = []
19
+ for index in range(len(points) - 1):
20
+ start = tuple(float(value) for value in points[index]["positions_rad"])
21
+ end = tuple(float(value) for value in points[index + 1]["positions_rad"])
22
+ if len(start) != len(end):
23
+ raise ValueError("trajectory_joint_shape_mismatch")
24
+ maximum = max(abs(first - second) for first, second in zip(start, end))
25
+ steps = max(1, int(ceil(maximum / max_joint_step_rad)))
26
+ for step in range(steps):
27
+ ratio = step / steps
28
+ sample = tuple(first + (second - first) * ratio for first, second in zip(start, end))
29
+ if not samples or sample != samples[-1]:
30
+ samples.append(sample)
31
+ if len(samples) > max_samples:
32
+ raise ValueError("preexecution_sample_limit_exceeded")
33
+ final = tuple(float(value) for value in points[-1]["positions_rad"])
34
+ if not samples or final != samples[-1]:
35
+ samples.append(final)
36
+ if len(samples) > max_samples:
37
+ raise ValueError("preexecution_sample_limit_exceeded")
38
+ return samples
39
+
40
+
41
+ def check(path: Path, *, timeout_s: float, max_joint_step_rad: float, max_samples: int, group_name: str) -> dict[str, Any]:
42
+ payload = json.loads(path.read_text(encoding="utf-8"))
43
+ motion = payload["motion"]
44
+ joint_names = tuple(str(value) for value in motion["joint_names"])
45
+ samples = _sample_motion(motion, max_joint_step_rad=max_joint_step_rad, max_samples=max_samples)
46
+
47
+ import rclpy
48
+ from moveit_msgs.srv import GetStateValidity
49
+
50
+ rclpy.init()
51
+ node = rclpy.create_node("devagent_state_validity_probe")
52
+ client = node.create_client(GetStateValidity, "/check_state_validity")
53
+ try:
54
+ if not client.wait_for_service(timeout_sec=timeout_s):
55
+ return {"success": False, "code": "state_validity_service_unavailable", "sample_count": 0}
56
+ checked = 0
57
+ for index, positions in enumerate(samples):
58
+ request = GetStateValidity.Request()
59
+ request.group_name = group_name
60
+ request.robot_state.is_diff = True
61
+ request.robot_state.joint_state.name = list(joint_names)
62
+ request.robot_state.joint_state.position = list(positions)
63
+ future = client.call_async(request)
64
+ rclpy.spin_until_future_complete(node, future, timeout_sec=timeout_s)
65
+ if not future.done() or future.result() is None:
66
+ return {"success": False, "code": "state_validity_timeout", "sample_count": checked, "first_invalid": index}
67
+ response = future.result()
68
+ checked += 1
69
+ if not response.valid:
70
+ evidence = {"index": index, "positions_rad": list(positions), "contact_count": len(response.contacts)}
71
+ return {
72
+ "success": False,
73
+ "code": "collision_or_invalid_state",
74
+ "sample_count": checked,
75
+ "first_invalid": evidence,
76
+ "group_name": group_name,
77
+ "max_joint_step_rad": max_joint_step_rad,
78
+ "evidence_hash": _hash(evidence),
79
+ }
80
+ evidence = {
81
+ "motion_hash": payload["motion_hash"],
82
+ "materialization_hash": payload["materialization_hash"],
83
+ "sample_count": checked,
84
+ "group_name": group_name,
85
+ "max_joint_step_rad": max_joint_step_rad,
86
+ }
87
+ return {
88
+ "success": True,
89
+ "code": "sampled_state_validity_pass",
90
+ "sample_count": checked,
91
+ "group_name": group_name,
92
+ "max_joint_step_rad": max_joint_step_rad,
93
+ "evidence_hash": _hash(evidence),
94
+ }
95
+ finally:
96
+ node.destroy_node()
97
+ rclpy.shutdown()
98
+
99
+
100
+ def main() -> int:
101
+ parser = argparse.ArgumentParser()
102
+ sub = parser.add_subparsers(dest="command", required=True)
103
+ command = sub.add_parser("check")
104
+ command.add_argument("--input", required=True)
105
+ command.add_argument("--timeout", type=float, default=10.0)
106
+ command.add_argument("--max-joint-step", type=float, default=0.03)
107
+ command.add_argument("--max-samples", type=int, default=4000)
108
+ command.add_argument("--group", default="ur_manipulator")
109
+ args = parser.parse_args()
110
+ try:
111
+ result = check(
112
+ Path(args.input).expanduser().resolve(),
113
+ timeout_s=float(args.timeout),
114
+ max_joint_step_rad=float(args.max_joint_step),
115
+ max_samples=int(args.max_samples),
116
+ group_name=str(args.group),
117
+ )
118
+ except Exception as exc:
119
+ result = {"success": False, "code": f"state_validity_probe_error:{type(exc).__name__}", "sample_count": 0}
120
+ print(json.dumps(result, sort_keys=True))
121
+ return 0 if result.get("success") else 2
122
+
123
+
124
+ if __name__ == "__main__":
125
+ raise SystemExit(main())
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+
6
+
7
+ def main() -> int:
8
+ parser = argparse.ArgumentParser()
9
+ parser.add_argument("--parent", required=True)
10
+ parser.add_argument("--child", required=True)
11
+ parser.add_argument("--timeout", type=float, default=10.0)
12
+ args = parser.parse_args()
13
+ payload: dict[str, object]
14
+ try:
15
+ import rclpy
16
+ from rclpy.duration import Duration
17
+ from rclpy.time import Time
18
+ from tf2_ros import Buffer, TransformListener
19
+
20
+ rclpy.init()
21
+ node = rclpy.create_node("devagent_tf_probe")
22
+ buffer = Buffer()
23
+ listener = TransformListener(buffer, node, spin_thread=True)
24
+ try:
25
+ transform = buffer.lookup_transform(
26
+ args.parent,
27
+ args.child,
28
+ Time(),
29
+ timeout=Duration(seconds=float(args.timeout)),
30
+ )
31
+ translation = transform.transform.translation
32
+ rotation = transform.transform.rotation
33
+ payload = {
34
+ "success": True,
35
+ "code": "tf_verified",
36
+ "parent_frame": args.parent,
37
+ "child_frame": args.child,
38
+ "translation_m": [translation.x, translation.y, translation.z],
39
+ "quaternion_xyzw": [rotation.x, rotation.y, rotation.z, rotation.w],
40
+ }
41
+ finally:
42
+ node.destroy_node()
43
+ rclpy.shutdown()
44
+ except Exception as exc:
45
+ payload = {"success": False, "code": f"tf_probe_error:{type(exc).__name__}"}
46
+ print(json.dumps(payload, sort_keys=True))
47
+ return 0 if payload.get("success") else 2
48
+
49
+
50
+ if __name__ == "__main__":
51
+ raise SystemExit(main())
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from math import isfinite
6
+ import re
7
+ from typing import Mapping
8
+
9
+ from ..physical_motion import JointTrajectoryPoint, PhysicalMotionPlan
10
+ from .commands import CommandResult, RosCommandRunner
11
+
12
+
13
+ class RosTrajectoryError(RuntimeError):
14
+ pass
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class JointStateSnapshot:
19
+ names: tuple[str, ...]
20
+ positions_rad: tuple[float, ...]
21
+
22
+ def __post_init__(self) -> None:
23
+ if not self.names or len(self.names) != len(self.positions_rad):
24
+ raise RosTrajectoryError("joint_state_shape_invalid")
25
+ if len(set(self.names)) != len(self.names):
26
+ raise RosTrajectoryError("joint_state_duplicate_name")
27
+ if any(not name.strip() for name in self.names):
28
+ raise RosTrajectoryError("joint_state_empty_name")
29
+ if any(not isfinite(value) for value in self.positions_rad):
30
+ raise RosTrajectoryError("joint_state_non_finite")
31
+
32
+ def ordered(self, joint_names: tuple[str, ...]) -> tuple[float, ...]:
33
+ values = dict(zip(self.names, self.positions_rad))
34
+ missing = [name for name in joint_names if name not in values]
35
+ if missing:
36
+ raise RosTrajectoryError(
37
+ "joint_state_missing:" + ",".join(sorted(missing))
38
+ )
39
+ return tuple(values[name] for name in joint_names)
40
+
41
+
42
+ def read_joint_state(
43
+ runner: RosCommandRunner,
44
+ *,
45
+ topic: str = "/joint_states",
46
+ timeout_s: float = 8.0,
47
+ ) -> JointStateSnapshot:
48
+ result = runner.run(
49
+ ["ros2", "topic", "echo", topic, "--once"],
50
+ timeout_s=timeout_s,
51
+ )
52
+ if not result.ok:
53
+ raise RosTrajectoryError(
54
+ "joint_state_snapshot_failed:"
55
+ + (result.stderr.strip()[-300:] or "ros2_topic_echo_failed")
56
+ )
57
+ try:
58
+ import yaml
59
+
60
+ payload = yaml.safe_load(result.stdout)
61
+ if not isinstance(payload, Mapping):
62
+ raise TypeError("joint state payload is not a mapping")
63
+ names_raw = payload.get("name")
64
+ positions_raw = payload.get("position")
65
+ if not isinstance(names_raw, list) or not isinstance(positions_raw, list):
66
+ raise TypeError("name/position fields are not arrays")
67
+ return JointStateSnapshot(
68
+ tuple(str(item) for item in names_raw),
69
+ tuple(float(item) for item in positions_raw),
70
+ )
71
+ except Exception as exc:
72
+ raise RosTrajectoryError("joint_state_snapshot_invalid") from exc
73
+
74
+
75
+ def _duration_payload(seconds: float) -> dict[str, int]:
76
+ whole = int(seconds)
77
+ nanos = int(round((seconds - whole) * 1_000_000_000))
78
+ if nanos >= 1_000_000_000:
79
+ whole += 1
80
+ nanos -= 1_000_000_000
81
+ return {"sec": whole, "nanosec": nanos}
82
+
83
+
84
+ def _point_payload(point: JointTrajectoryPoint) -> dict:
85
+ payload = {
86
+ "positions": list(point.positions_rad),
87
+ "time_from_start": _duration_payload(point.time_from_start_s),
88
+ }
89
+ if point.velocities_rad_s:
90
+ payload["velocities"] = list(point.velocities_rad_s)
91
+ if point.accelerations_rad_s2:
92
+ payload["accelerations"] = list(point.accelerations_rad_s2)
93
+ return payload
94
+
95
+
96
+ def follow_joint_trajectory_command(
97
+ motion: PhysicalMotionPlan,
98
+ *,
99
+ action_name: str = "/joint_trajectory_controller/follow_joint_trajectory",
100
+ ) -> tuple[str, ...]:
101
+ if not action_name.strip().startswith("/"):
102
+ raise RosTrajectoryError("trajectory_action_must_be_absolute")
103
+ points = motion.points[1:]
104
+ goal = {
105
+ "trajectory": {
106
+ "joint_names": list(motion.joint_names),
107
+ "points": [_point_payload(point) for point in points],
108
+ }
109
+ }
110
+ return (
111
+ "ros2",
112
+ "action",
113
+ "send_goal",
114
+ action_name,
115
+ "control_msgs/action/FollowJointTrajectory",
116
+ json.dumps(goal, separators=(",", ":"), allow_nan=False),
117
+ )
118
+
119
+
120
+ def single_point_motion(
121
+ *,
122
+ motion_id: str,
123
+ robot_profile_key: str,
124
+ joint_names: tuple[str, ...],
125
+ current_positions_rad: tuple[float, ...],
126
+ target_positions_rad: tuple[float, ...],
127
+ duration_s: float,
128
+ source_graph_hash: str,
129
+ twin_hash: str,
130
+ planner_id: str = "devagent-reset",
131
+ ) -> PhysicalMotionPlan:
132
+ if duration_s <= 0:
133
+ raise RosTrajectoryError("reset_duration_must_be_positive")
134
+ return PhysicalMotionPlan(
135
+ motion_id=motion_id,
136
+ robot_profile_key=robot_profile_key,
137
+ joint_names=joint_names,
138
+ points=(
139
+ JointTrajectoryPoint(current_positions_rad, 0.0),
140
+ JointTrajectoryPoint(target_positions_rad, duration_s),
141
+ ),
142
+ source_graph_hash=source_graph_hash,
143
+ planner_id=planner_id,
144
+ twin_hash=twin_hash,
145
+ constraints=("simulation_reset_only",),
146
+ )
147
+
148
+
149
+ def _confirm_successful_action_output(output: str) -> None:
150
+ normalized = output.lower()
151
+ if "goal rejected" in normalized:
152
+ raise RosTrajectoryError("trajectory_goal_rejected")
153
+ for failure in ("status: aborted", "status: canceled", "status: cancelled"):
154
+ if failure in normalized:
155
+ raise RosTrajectoryError("trajectory_action_not_succeeded")
156
+
157
+ error_codes = [int(value) for value in re.findall(r"error_code\s*[=:]\s*(-?\d+)", normalized)]
158
+ if any(code != 0 for code in error_codes):
159
+ raise RosTrajectoryError("trajectory_result_error_code")
160
+
161
+ success_markers = (
162
+ "status: succeeded",
163
+ "goal finished with status: succeeded",
164
+ "successful",
165
+ )
166
+ if error_codes and all(code == 0 for code in error_codes):
167
+ return
168
+ if any(marker in normalized for marker in success_markers):
169
+ return
170
+ raise RosTrajectoryError("trajectory_result_unconfirmed")
171
+
172
+
173
+ def execute_joint_trajectory(
174
+ runner: RosCommandRunner,
175
+ motion: PhysicalMotionPlan,
176
+ *,
177
+ action_name: str = "/joint_trajectory_controller/follow_joint_trajectory",
178
+ timeout_s: float | None = None,
179
+ ) -> CommandResult:
180
+ timeout = timeout_s or max(15.0, motion.planned_duration_s + 15.0)
181
+ result = runner.run(
182
+ follow_joint_trajectory_command(motion, action_name=action_name),
183
+ timeout_s=timeout,
184
+ )
185
+ if not result.ok:
186
+ raise RosTrajectoryError("trajectory_action_failed")
187
+ _confirm_successful_action_output(result.stdout + "\n" + result.stderr)
188
+ return result
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from .commands import RosCommandRunner, RunningProcess
7
+
8
+
9
+ UR5E = "ur5e"
10
+
11
+
12
+ def official_ur5e_moveit_command(*, world_file: Path | str | None = None) -> tuple[str, ...]:
13
+ command = [
14
+ "ros2",
15
+ "launch",
16
+ "ur_simulation_gz",
17
+ "ur_sim_moveit.launch.py",
18
+ "ur_type:=ur5e",
19
+ ]
20
+ if world_file is not None:
21
+ world = Path(world_file).expanduser().resolve()
22
+ if not world.is_file():
23
+ raise FileNotFoundError(f"world_file_not_found:{world}")
24
+ command.append(f"world_file:={world}")
25
+ return tuple(command)
26
+
27
+
28
+ def official_ur_motion_smoke_command() -> tuple[str, ...]:
29
+ return ("ros2", "run", "ur_robot_driver", "example_move.py")
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class UR5eSimulation:
34
+ runner: RosCommandRunner
35
+ process: RunningProcess
36
+ log_path: Path
37
+ command: tuple[str, ...]
38
+
39
+ @property
40
+ def running(self) -> bool:
41
+ return self.process.poll() is None
42
+
43
+ def stop(self, *, timeout_s: float = 10.0) -> None:
44
+ self.runner.stop(self.process, timeout_s=timeout_s)
45
+
46
+
47
+ class UR5eSimulationLauncher:
48
+ def __init__(self, runner: RosCommandRunner) -> None:
49
+ self.runner = runner
50
+
51
+ def launch(
52
+ self,
53
+ *,
54
+ log_path: Path,
55
+ world_file: Path | str | None = None,
56
+ ) -> UR5eSimulation:
57
+ command = official_ur5e_moveit_command(world_file=world_file)
58
+ process = self.runner.start(command, log_path=log_path)
59
+ return UR5eSimulation(self.runner, process, log_path, command)