devagent-physical-engine 0.12.2__tar.gz → 0.12.3__tar.gz
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-0.12.2/src/devagent_physical_engine.egg-info → devagent_physical_engine-0.12.3}/PKG-INFO +4 -2
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/README.md +3 -1
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/pyproject.toml +1 -1
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/__init__.py +1 -1
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/moveit_scene.py +114 -6
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/scene_probe.py +94 -24
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3/src/devagent_physical_engine.egg-info}/PKG-INFO +4 -2
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine.egg-info/SOURCES.txt +1 -0
- devagent_physical_engine-0.12.3/tests/test_moveit_scene_v0123.py +213 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/NOTICE +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/setup.cfg +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/__init__.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/compiler.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/contracts.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/coordinator.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/critic.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/evidence.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/interpreter.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/planner.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/recovery.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/routing.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/runtime.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/semantic.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/agent/structured.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ai_cli.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/cli.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/continuous_clearance.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/doctor.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/engineering_agent.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/engineering_request.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/execution.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/models.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/operating_envelope.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/__init__.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/benchmark.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/candidates.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/contracts.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/evaluator.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/evidence.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/experience.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/measured.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/optimizer.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/optimization/orchestrator.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/physical_campaign.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/physical_evidence.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/physical_motion.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/planning.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/preexecution_contract.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/provider_adapters/__init__.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/provider_adapters/anthropic.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/provider_adapters/common.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/provider_adapters/factory.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/provider_adapters/gemini.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/provider_adapters/openai.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/provider_qualification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/providers.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/qualification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/qualification_cli.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/qualification_harness.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/reference_workcell.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/robot_platform.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/robots.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/__init__.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/acceptance.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/commands.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/continuous_clearance_probe.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/doctor.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/fk_pose.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/fk_probe.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/frame_alignment.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/gazebo_cli.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/gazebo_pose.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/gazebo_world.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/joint_state_recorder.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/measured_motion.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/moveit_joint_plan_probe.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/moveit_task_planner.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/preexecution.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/qualification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/qualification_v10.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/qualification_v11.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/qualification_v12.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/state_validity_probe.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/task_adapter.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/task_visualization.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/tf_probe.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/tool_scene.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/tool_scene_probe.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/trajectory.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/ur5e.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/ur5e_adapter.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/ur5e_v10_adapter.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/workpiece_follow.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/workpiece_gazebo_follower.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/workpiece_scene.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/ros2/workpiece_scene_probe.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/setup_profile.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/sim_real_qualification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/simulation.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/simulation_platform.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/tool_collision.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/trajectory_qualification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/twin.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/twin_builder.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/twin_materialization.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/verification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine/workpiece_lifecycle.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine.egg-info/dependency_links.txt +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine.egg-info/entry_points.txt +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine.egg-info/requires.txt +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/src/devagent_physical_engine.egg-info/top_level.txt +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_agent_core.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_agent_semantic_hardening.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_ai_cli.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_ai_cli_v07.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_continuous_clearance_v011.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_core.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_engineering_request_v07.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_gazebo_cli_v0122.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_interpreter_profiles_v08.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_live_provider_qualification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_measured_motion_v10.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_natural_language_agent_v07.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_openai_strict_schema_v0113.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_optimization_v05.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_physical_campaign_v09.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_physical_evidence_v09.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_physical_motion_v09.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_planning_fallback_v012.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_preexecution_contract_v10.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_provider_and_qualification.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_qualification_cli_lifecycle_v06.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_qualification_harness_v06.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_readme_pypi.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_real_provider_adapters.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_reference_workcell_v012.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_robot_platform_v08.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_ros2_production_receipts_v011.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_ros2_simulation.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_ros_trajectory_v09.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_setup_profile.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_sim_real_qualification_v011.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_simulation_platform_v08.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_tool_collision_v011.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_trajectory_qualification_v09.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_twin_materialization_v10.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_twin_v08.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_ur5e_adapter_scopes_v09.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_ur5e_v10_adapter_scopes.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_v052_stabilization.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_v08_catalog_mapping_regression.py +0 -0
- {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.3}/tests/test_workpiece_lifecycle_v011.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devagent-physical-engine
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.3
|
|
4
4
|
Summary: Verification-first agentic engineering, simulation, and evidence runtime for robotics and industrial automation.
|
|
5
5
|
Author: Tom Ha
|
|
6
6
|
License: All Rights Reserved
|
|
@@ -193,6 +193,8 @@ v0.12.1 hardens the initial ROS joint-state snapshot by parsing `ros2 topic echo
|
|
|
193
193
|
|
|
194
194
|
v0.12.2 additionally hardens Gazebo model-pose read-back across Harmonic CLI renderings. One shared parser now accepts both pipe-delimited and whitespace-delimited finite XYZ/RPY vectors and is used by initial world verification and dynamic workpiece pose read-back, so task simulation does not depend on a single human-readable `gz model --pose` delimiter style.
|
|
195
195
|
|
|
196
|
+
v0.12.3 hardens MoveIt PlanningScene read-back when MoveIt serializes world collision objects in its own planning/root frame. A different observed frame is never trusted by label alone: DevAgent first proves the observed frame and canonical Twin frame are identity-equivalent through TF, then re-reads the scene and canonicalizes it back to the Twin planning frame before enforcing the exact geometry hash. Non-identity frames and geometry drift remain fail-closed.
|
|
197
|
+
|
|
196
198
|
The packaged reference workcell is **not customer-site geometry**. Arbitrary plant layouts require an imported or user-declared physical Twin with trusted poses, collision geometry, tool/TCP information, and workpiece properties. DevAgent does not invent those site facts from a natural-language prompt.
|
|
197
199
|
|
|
198
200
|
The v0.12 implementation remains evidence-gated: source/CI success does not prove the ROS target campaign has passed on a particular workstation. Use the executable qualification below before treating the task-specific path as qualified on that target.
|
|
@@ -420,7 +422,7 @@ Detailed engineering documentation lives in the repository:
|
|
|
420
422
|
|
|
421
423
|
## Project status
|
|
422
424
|
|
|
423
|
-
**v0.12.
|
|
425
|
+
**v0.12.3 — Beta software / evidence-gated task-specific physical simulation.**
|
|
424
426
|
|
|
425
427
|
The software and release pipeline are production-oriented, but physical readiness is scope-specific and evidence-driven. The UR5e task-specific reference-workcell path must pass the executable v0.12 ROS/Gazebo/MoveIt campaign on the target workstation before that scope is considered simulation-qualified there. Real robot execution remains locked. Functional-safety certification and site qualification are not claimed.
|
|
426
428
|
|
|
@@ -152,6 +152,8 @@ v0.12.1 hardens the initial ROS joint-state snapshot by parsing `ros2 topic echo
|
|
|
152
152
|
|
|
153
153
|
v0.12.2 additionally hardens Gazebo model-pose read-back across Harmonic CLI renderings. One shared parser now accepts both pipe-delimited and whitespace-delimited finite XYZ/RPY vectors and is used by initial world verification and dynamic workpiece pose read-back, so task simulation does not depend on a single human-readable `gz model --pose` delimiter style.
|
|
154
154
|
|
|
155
|
+
v0.12.3 hardens MoveIt PlanningScene read-back when MoveIt serializes world collision objects in its own planning/root frame. A different observed frame is never trusted by label alone: DevAgent first proves the observed frame and canonical Twin frame are identity-equivalent through TF, then re-reads the scene and canonicalizes it back to the Twin planning frame before enforcing the exact geometry hash. Non-identity frames and geometry drift remain fail-closed.
|
|
156
|
+
|
|
155
157
|
The packaged reference workcell is **not customer-site geometry**. Arbitrary plant layouts require an imported or user-declared physical Twin with trusted poses, collision geometry, tool/TCP information, and workpiece properties. DevAgent does not invent those site facts from a natural-language prompt.
|
|
156
158
|
|
|
157
159
|
The v0.12 implementation remains evidence-gated: source/CI success does not prove the ROS target campaign has passed on a particular workstation. Use the executable qualification below before treating the task-specific path as qualified on that target.
|
|
@@ -379,7 +381,7 @@ Detailed engineering documentation lives in the repository:
|
|
|
379
381
|
|
|
380
382
|
## Project status
|
|
381
383
|
|
|
382
|
-
**v0.12.
|
|
384
|
+
**v0.12.3 — Beta software / evidence-gated task-specific physical simulation.**
|
|
383
385
|
|
|
384
386
|
The software and release pipeline are production-oriented, but physical readiness is scope-specific and evidence-driven. The UR5e task-specific reference-workcell path must pass the executable v0.12 ROS/Gazebo/MoveIt campaign on the target workstation before that scope is considered simulation-qualified there. Real robot execution remains locked. Functional-safety certification and site qualification are not claimed.
|
|
385
387
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devagent-physical-engine"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.3"
|
|
8
8
|
description = "Verification-first agentic engineering, simulation, and evidence runtime for robotics and industrial automation."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import asdict, dataclass
|
|
4
4
|
import json
|
|
5
|
+
from math import isfinite
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from ..simulation_platform import SimulationPlatformError
|
|
9
10
|
from ..twin_materialization import CanonicalTwinMaterialization
|
|
10
11
|
from .commands import RosCommandRunner
|
|
12
|
+
from .frame_alignment import FrameAlignmentReceipt, FrameAlignmentVerifier
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@dataclass(frozen=True, slots=True)
|
|
@@ -21,6 +23,8 @@ class MoveItSceneReceipt:
|
|
|
21
23
|
planning_frame: str
|
|
22
24
|
expected_object_count: int
|
|
23
25
|
observed_object_count: int | None
|
|
26
|
+
observed_planning_frames: tuple[str, ...] = ()
|
|
27
|
+
equivalent_planning_frames: tuple[str, ...] = ()
|
|
24
28
|
|
|
25
29
|
def __post_init__(self) -> None:
|
|
26
30
|
if not self.code.strip():
|
|
@@ -37,11 +41,44 @@ class MoveItSceneReceipt:
|
|
|
37
41
|
raise ValueError("moveit_scene_receipt_object_count_invalid")
|
|
38
42
|
if self.observed_object_count is not None and self.observed_object_count < 0:
|
|
39
43
|
raise ValueError("moveit_scene_receipt_object_count_invalid")
|
|
44
|
+
if any(not value.strip() for value in self.observed_planning_frames):
|
|
45
|
+
raise ValueError("moveit_scene_receipt_observed_frame_invalid")
|
|
46
|
+
if any(not value.strip() for value in self.equivalent_planning_frames):
|
|
47
|
+
raise ValueError("moveit_scene_receipt_equivalent_frame_invalid")
|
|
40
48
|
|
|
41
49
|
def to_dict(self) -> dict[str, Any]:
|
|
42
50
|
return asdict(self)
|
|
43
51
|
|
|
44
52
|
|
|
53
|
+
def _equivalent_frames_from_receipt(
|
|
54
|
+
planning_frame: str,
|
|
55
|
+
frame_alignment: FrameAlignmentReceipt | None,
|
|
56
|
+
) -> tuple[str, ...]:
|
|
57
|
+
"""Return only frames already proven identity-equivalent to the canonical frame."""
|
|
58
|
+
|
|
59
|
+
if frame_alignment is None:
|
|
60
|
+
return ()
|
|
61
|
+
if (
|
|
62
|
+
not frame_alignment.verified
|
|
63
|
+
or not isfinite(frame_alignment.translation_error_m)
|
|
64
|
+
or not isfinite(frame_alignment.rotation_error_rad)
|
|
65
|
+
or frame_alignment.translation_error_m < 0.0
|
|
66
|
+
or frame_alignment.rotation_error_rad < 0.0
|
|
67
|
+
):
|
|
68
|
+
raise SimulationPlatformError("moveit_scene_frame_receipt_invalid")
|
|
69
|
+
|
|
70
|
+
parent = frame_alignment.parent_frame.strip()
|
|
71
|
+
child = frame_alignment.child_frame.strip()
|
|
72
|
+
expected = planning_frame.strip()
|
|
73
|
+
if not parent or not child or not expected:
|
|
74
|
+
raise SimulationPlatformError("moveit_scene_frame_receipt_invalid")
|
|
75
|
+
if child == expected:
|
|
76
|
+
return () if parent == expected else (parent,)
|
|
77
|
+
if parent == expected:
|
|
78
|
+
return () if child == expected else (child,)
|
|
79
|
+
raise SimulationPlatformError("moveit_scene_frame_receipt_unbound")
|
|
80
|
+
|
|
81
|
+
|
|
45
82
|
class MoveItSceneBridge:
|
|
46
83
|
"""Apply canonical Twin collision geometry to MoveIt and read it back."""
|
|
47
84
|
|
|
@@ -58,15 +95,19 @@ class MoveItSceneBridge:
|
|
|
58
95
|
self.work_dir = Path(work_dir).expanduser().resolve()
|
|
59
96
|
self.timeout_s = float(timeout_s)
|
|
60
97
|
|
|
61
|
-
def
|
|
62
|
-
self,
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
def _run_probe(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
materialization: CanonicalTwinMaterialization,
|
|
102
|
+
equivalent_frames: tuple[str, ...],
|
|
103
|
+
) -> tuple[dict[str, Any], bool]:
|
|
104
|
+
payload = materialization.moveit_scene_payload()
|
|
105
|
+
payload["equivalent_planning_frames"] = list(equivalent_frames)
|
|
65
106
|
payload_path = self.work_dir / f"{materialization.fingerprint}.moveit-scene.json"
|
|
66
107
|
temporary = payload_path.with_suffix(payload_path.suffix + ".tmp")
|
|
67
108
|
temporary.write_text(
|
|
68
109
|
json.dumps(
|
|
69
|
-
|
|
110
|
+
payload,
|
|
70
111
|
sort_keys=True,
|
|
71
112
|
separators=(",", ":"),
|
|
72
113
|
allow_nan=False,
|
|
@@ -93,8 +134,73 @@ class MoveItSceneBridge:
|
|
|
93
134
|
raise SimulationPlatformError("moveit_scene_probe_output_invalid") from exc
|
|
94
135
|
if not isinstance(output, dict):
|
|
95
136
|
raise SimulationPlatformError("moveit_scene_probe_output_invalid")
|
|
137
|
+
return output, result.ok
|
|
138
|
+
|
|
139
|
+
def apply_and_verify(
|
|
140
|
+
self,
|
|
141
|
+
materialization: CanonicalTwinMaterialization,
|
|
142
|
+
*,
|
|
143
|
+
frame_alignment: FrameAlignmentReceipt | None = None,
|
|
144
|
+
) -> MoveItSceneReceipt:
|
|
145
|
+
self.work_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
equivalent_frames = _equivalent_frames_from_receipt(
|
|
147
|
+
materialization.planning_frame,
|
|
148
|
+
frame_alignment,
|
|
149
|
+
)
|
|
150
|
+
output, command_ok = self._run_probe(
|
|
151
|
+
materialization=materialization,
|
|
152
|
+
equivalent_frames=equivalent_frames,
|
|
153
|
+
)
|
|
96
154
|
code = str(output.get("code") or "moveit_scene_probe_failed")
|
|
97
|
-
|
|
155
|
+
|
|
156
|
+
# MoveIt serializes world collision objects in its own planning/root frame.
|
|
157
|
+
# If that label differs from the canonical Twin frame, do not trust it by
|
|
158
|
+
# convention. Prove the observed frame is identity-equivalent via TF, then
|
|
159
|
+
# repeat the read-back with that exact frame admitted for canonical hashing.
|
|
160
|
+
if (
|
|
161
|
+
frame_alignment is None
|
|
162
|
+
and not equivalent_frames
|
|
163
|
+
and not bool(output.get("success"))
|
|
164
|
+
and code.startswith("scene_planning_frame_mismatch:")
|
|
165
|
+
):
|
|
166
|
+
observed_frames = tuple(
|
|
167
|
+
sorted(
|
|
168
|
+
{
|
|
169
|
+
str(value).strip()
|
|
170
|
+
for value in output.get("observed_planning_frames") or ()
|
|
171
|
+
if str(value).strip()
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
if len(observed_frames) != 1:
|
|
176
|
+
raise SimulationPlatformError(
|
|
177
|
+
"moveit_scene_observed_frame_ambiguous"
|
|
178
|
+
)
|
|
179
|
+
observed_frame = observed_frames[0]
|
|
180
|
+
receipt = FrameAlignmentVerifier(self.runner).verify_identity(
|
|
181
|
+
parent_frame=observed_frame,
|
|
182
|
+
child_frame=materialization.planning_frame,
|
|
183
|
+
)
|
|
184
|
+
equivalent_frames = _equivalent_frames_from_receipt(
|
|
185
|
+
materialization.planning_frame,
|
|
186
|
+
receipt,
|
|
187
|
+
)
|
|
188
|
+
output, command_ok = self._run_probe(
|
|
189
|
+
materialization=materialization,
|
|
190
|
+
equivalent_frames=equivalent_frames,
|
|
191
|
+
)
|
|
192
|
+
code = str(output.get("code") or "moveit_scene_probe_failed")
|
|
193
|
+
|
|
194
|
+
verified = bool(output.get("success")) and command_ok
|
|
195
|
+
observed_frames = tuple(
|
|
196
|
+
sorted(
|
|
197
|
+
{
|
|
198
|
+
str(value).strip()
|
|
199
|
+
for value in output.get("observed_planning_frames") or ()
|
|
200
|
+
if str(value).strip()
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
)
|
|
98
204
|
receipt = MoveItSceneReceipt(
|
|
99
205
|
verified=verified,
|
|
100
206
|
code=code,
|
|
@@ -113,6 +219,8 @@ class MoveItSceneBridge:
|
|
|
113
219
|
if output.get("observed_object_count") is not None
|
|
114
220
|
else None
|
|
115
221
|
),
|
|
222
|
+
observed_planning_frames=observed_frames,
|
|
223
|
+
equivalent_planning_frames=equivalent_frames,
|
|
116
224
|
)
|
|
117
225
|
if not receipt.verified:
|
|
118
226
|
raise SimulationPlatformError(f"moveit_scene_verification_failed:{receipt.code}")
|
|
@@ -35,6 +35,71 @@ def _canonical_scene(planning_frame: str, objects: list[dict[str, Any]]) -> dict
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _verify_observed_scene(
|
|
39
|
+
*,
|
|
40
|
+
planning_frame: str,
|
|
41
|
+
expected_hash: str,
|
|
42
|
+
expected_objects: list[dict[str, Any]],
|
|
43
|
+
observed_objects: list[dict[str, Any]],
|
|
44
|
+
equivalent_planning_frames: tuple[str, ...] = (),
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Verify read-back without confusing an identity-equivalent frame label for drift.
|
|
47
|
+
|
|
48
|
+
``equivalent_planning_frames`` is not discovered here. It must be supplied by the
|
|
49
|
+
caller from an independently verified identity transform receipt. Geometry is
|
|
50
|
+
always canonicalized back to ``planning_frame`` before hashing.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
planning_frame = planning_frame.strip()
|
|
54
|
+
if not planning_frame:
|
|
55
|
+
raise ValueError("scene_planning_frame_required")
|
|
56
|
+
equivalent = tuple(value.strip() for value in equivalent_planning_frames)
|
|
57
|
+
if any(not value for value in equivalent):
|
|
58
|
+
raise ValueError("scene_equivalent_planning_frame_invalid")
|
|
59
|
+
accepted_frames = {planning_frame, *equivalent}
|
|
60
|
+
expected_ids = {str(item["id"]) for item in expected_objects}
|
|
61
|
+
relevant = [item for item in observed_objects if str(item["id"]) in expected_ids]
|
|
62
|
+
observed_frames = sorted({str(item.get("frame_id") or "").strip() for item in relevant})
|
|
63
|
+
|
|
64
|
+
for item in relevant:
|
|
65
|
+
frame_id = str(item.get("frame_id") or "").strip()
|
|
66
|
+
if frame_id not in accepted_frames:
|
|
67
|
+
return {
|
|
68
|
+
"success": False,
|
|
69
|
+
"code": f"scene_planning_frame_mismatch:{item['id']}:{frame_id or '<empty>'}",
|
|
70
|
+
"observed_object_count": len(relevant),
|
|
71
|
+
"observed_planning_frames": observed_frames,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
canonical_objects = [
|
|
75
|
+
{
|
|
76
|
+
"id": item["id"],
|
|
77
|
+
"geometry_kind": item["geometry_kind"],
|
|
78
|
+
"dimensions_m": item["dimensions_m"],
|
|
79
|
+
"position_m": item["position_m"],
|
|
80
|
+
"quaternion_xyzw": item["quaternion_xyzw"],
|
|
81
|
+
}
|
|
82
|
+
for item in relevant
|
|
83
|
+
]
|
|
84
|
+
observed_scene = _canonical_scene(planning_frame, canonical_objects)
|
|
85
|
+
observed_hash = _hash(observed_scene)
|
|
86
|
+
observed_ids = {str(item["id"]) for item in relevant}
|
|
87
|
+
missing = sorted(expected_ids - observed_ids)
|
|
88
|
+
success = (
|
|
89
|
+
not missing
|
|
90
|
+
and len(relevant) == len(expected_objects)
|
|
91
|
+
and observed_hash == expected_hash
|
|
92
|
+
)
|
|
93
|
+
return {
|
|
94
|
+
"success": success,
|
|
95
|
+
"code": "moveit_scene_readback_verified" if success else "moveit_scene_readback_mismatch",
|
|
96
|
+
"observed_scene_hash": observed_hash,
|
|
97
|
+
"observed_object_count": len(relevant),
|
|
98
|
+
"observed_planning_frames": observed_frames,
|
|
99
|
+
"missing_object_ids": missing,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
38
103
|
def _qmul(first, second):
|
|
39
104
|
ax, ay, az, aw = first
|
|
40
105
|
bx, by, bz, bw = second
|
|
@@ -78,6 +143,9 @@ def apply_verify(path: Path, timeout_s: float) -> dict[str, Any]:
|
|
|
78
143
|
planning_frame = str(payload["planning_frame"])
|
|
79
144
|
expected_hash = str(payload["scene_hash"])
|
|
80
145
|
expected_objects = list(payload["objects"])
|
|
146
|
+
equivalent_planning_frames = tuple(
|
|
147
|
+
str(value) for value in payload.get("equivalent_planning_frames") or ()
|
|
148
|
+
)
|
|
81
149
|
|
|
82
150
|
import rclpy
|
|
83
151
|
from geometry_msgs.msg import Pose
|
|
@@ -155,13 +223,12 @@ def apply_verify(path: Path, timeout_s: float) -> dict[str, Any]:
|
|
|
155
223
|
for collision in response.scene.world.collision_objects:
|
|
156
224
|
if collision.id not in expected_ids:
|
|
157
225
|
continue
|
|
158
|
-
if collision.
|
|
226
|
+
if len(collision.primitives) != 1 or len(collision.primitive_poses) != 1:
|
|
159
227
|
return {
|
|
160
228
|
"success": False,
|
|
161
|
-
"code": f"
|
|
229
|
+
"code": f"scene_shape_invalid:{collision.id}",
|
|
230
|
+
"observed_planning_frames": [collision.header.frame_id],
|
|
162
231
|
}
|
|
163
|
-
if len(collision.primitives) != 1 or len(collision.primitive_poses) != 1:
|
|
164
|
-
return {"success": False, "code": f"scene_shape_invalid:{collision.id}"}
|
|
165
232
|
primitive = collision.primitives[0]
|
|
166
233
|
primitive_pose = collision.primitive_poses[0]
|
|
167
234
|
position, quaternion = _combine_pose(collision.pose, primitive_pose)
|
|
@@ -175,26 +242,29 @@ def apply_verify(path: Path, timeout_s: float) -> dict[str, Any]:
|
|
|
175
242
|
primitive.dimensions[SolidPrimitive.CYLINDER_HEIGHT],
|
|
176
243
|
]
|
|
177
244
|
else:
|
|
178
|
-
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
245
|
+
return {
|
|
246
|
+
"success": False,
|
|
247
|
+
"code": f"scene_shape_unsupported:{collision.id}",
|
|
248
|
+
"observed_planning_frames": [collision.header.frame_id],
|
|
249
|
+
}
|
|
250
|
+
observed.append(
|
|
251
|
+
{
|
|
252
|
+
"id": collision.id,
|
|
253
|
+
"frame_id": collision.header.frame_id,
|
|
254
|
+
"geometry_kind": kind,
|
|
255
|
+
"dimensions_m": dimensions,
|
|
256
|
+
"position_m": list(position),
|
|
257
|
+
"quaternion_xyzw": list(quaternion),
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return _verify_observed_scene(
|
|
262
|
+
planning_frame=planning_frame,
|
|
263
|
+
expected_hash=expected_hash,
|
|
264
|
+
expected_objects=expected_objects,
|
|
265
|
+
observed_objects=observed,
|
|
266
|
+
equivalent_planning_frames=equivalent_planning_frames,
|
|
267
|
+
)
|
|
198
268
|
finally:
|
|
199
269
|
node.destroy_node()
|
|
200
270
|
rclpy.shutdown()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devagent-physical-engine
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.3
|
|
4
4
|
Summary: Verification-first agentic engineering, simulation, and evidence runtime for robotics and industrial automation.
|
|
5
5
|
Author: Tom Ha
|
|
6
6
|
License: All Rights Reserved
|
|
@@ -193,6 +193,8 @@ v0.12.1 hardens the initial ROS joint-state snapshot by parsing `ros2 topic echo
|
|
|
193
193
|
|
|
194
194
|
v0.12.2 additionally hardens Gazebo model-pose read-back across Harmonic CLI renderings. One shared parser now accepts both pipe-delimited and whitespace-delimited finite XYZ/RPY vectors and is used by initial world verification and dynamic workpiece pose read-back, so task simulation does not depend on a single human-readable `gz model --pose` delimiter style.
|
|
195
195
|
|
|
196
|
+
v0.12.3 hardens MoveIt PlanningScene read-back when MoveIt serializes world collision objects in its own planning/root frame. A different observed frame is never trusted by label alone: DevAgent first proves the observed frame and canonical Twin frame are identity-equivalent through TF, then re-reads the scene and canonicalizes it back to the Twin planning frame before enforcing the exact geometry hash. Non-identity frames and geometry drift remain fail-closed.
|
|
197
|
+
|
|
196
198
|
The packaged reference workcell is **not customer-site geometry**. Arbitrary plant layouts require an imported or user-declared physical Twin with trusted poses, collision geometry, tool/TCP information, and workpiece properties. DevAgent does not invent those site facts from a natural-language prompt.
|
|
197
199
|
|
|
198
200
|
The v0.12 implementation remains evidence-gated: source/CI success does not prove the ROS target campaign has passed on a particular workstation. Use the executable qualification below before treating the task-specific path as qualified on that target.
|
|
@@ -420,7 +422,7 @@ Detailed engineering documentation lives in the repository:
|
|
|
420
422
|
|
|
421
423
|
## Project status
|
|
422
424
|
|
|
423
|
-
**v0.12.
|
|
425
|
+
**v0.12.3 — Beta software / evidence-gated task-specific physical simulation.**
|
|
424
426
|
|
|
425
427
|
The software and release pipeline are production-oriented, but physical readiness is scope-specific and evidence-driven. The UR5e task-specific reference-workcell path must pass the executable v0.12 ROS/Gazebo/MoveIt campaign on the target workstation before that scope is considered simulation-qualified there. Real robot execution remains locked. Functional-safety certification and site qualification are not claimed.
|
|
426
428
|
|
|
@@ -117,6 +117,7 @@ tests/test_gazebo_cli_v0122.py
|
|
|
117
117
|
tests/test_interpreter_profiles_v08.py
|
|
118
118
|
tests/test_live_provider_qualification.py
|
|
119
119
|
tests/test_measured_motion_v10.py
|
|
120
|
+
tests/test_moveit_scene_v0123.py
|
|
120
121
|
tests/test_natural_language_agent_v07.py
|
|
121
122
|
tests/test_openai_strict_schema_v0113.py
|
|
122
123
|
tests/test_optimization_v05.py
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from tempfile import TemporaryDirectory
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from devagent_physical_engine.ros2.commands import CommandResult
|
|
9
|
+
from devagent_physical_engine.ros2.frame_alignment import FrameAlignmentReceipt
|
|
10
|
+
from devagent_physical_engine.ros2.moveit_scene import (
|
|
11
|
+
MoveItSceneBridge,
|
|
12
|
+
_equivalent_frames_from_receipt,
|
|
13
|
+
)
|
|
14
|
+
from devagent_physical_engine.ros2.scene_probe import (
|
|
15
|
+
_canonical_scene,
|
|
16
|
+
_hash,
|
|
17
|
+
_verify_observed_scene,
|
|
18
|
+
)
|
|
19
|
+
from devagent_physical_engine.simulation_platform import SimulationPlatformError
|
|
20
|
+
from devagent_physical_engine.twin_materialization import (
|
|
21
|
+
CanonicalTwinMaterialization,
|
|
22
|
+
MaterializedObject,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _expected_objects():
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
"id": "devagent_box",
|
|
30
|
+
"geometry_kind": "box",
|
|
31
|
+
"dimensions_m": [0.1, 0.08, 0.06],
|
|
32
|
+
"position_m": [0.4, -0.2, 0.3],
|
|
33
|
+
"quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _expected_hash() -> str:
|
|
39
|
+
return _hash(_canonical_scene("base_link", _expected_objects()))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _observed(*, frame_id="world", dimensions=None):
|
|
43
|
+
item = dict(_expected_objects()[0])
|
|
44
|
+
item["frame_id"] = frame_id
|
|
45
|
+
if dimensions is not None:
|
|
46
|
+
item["dimensions_m"] = list(dimensions)
|
|
47
|
+
return [item]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _materialization() -> CanonicalTwinMaterialization:
|
|
51
|
+
return CanonicalTwinMaterialization(
|
|
52
|
+
twin_id="TWIN-v0123-test",
|
|
53
|
+
twin_hash="a" * 64,
|
|
54
|
+
robot_profile_key="ur5e",
|
|
55
|
+
planning_frame="base_link",
|
|
56
|
+
objects=(
|
|
57
|
+
MaterializedObject(
|
|
58
|
+
entity_id="BOX_101",
|
|
59
|
+
backend_id="devagent_box",
|
|
60
|
+
kind="box",
|
|
61
|
+
dimensions_m=(0.1, 0.08, 0.06),
|
|
62
|
+
position_m=(0.4, -0.2, 0.3),
|
|
63
|
+
quaternion_xyzw=(0.0, 0.0, 0.0, 1.0),
|
|
64
|
+
static=True,
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class EquivalentPlanningFrameTests(unittest.TestCase):
|
|
71
|
+
def test_identity_equivalent_world_frame_is_canonicalized_before_hash(self):
|
|
72
|
+
result = _verify_observed_scene(
|
|
73
|
+
planning_frame="base_link",
|
|
74
|
+
expected_hash=_expected_hash(),
|
|
75
|
+
expected_objects=_expected_objects(),
|
|
76
|
+
observed_objects=_observed(frame_id="world"),
|
|
77
|
+
equivalent_planning_frames=("world",),
|
|
78
|
+
)
|
|
79
|
+
self.assertTrue(result["success"])
|
|
80
|
+
self.assertEqual(result["observed_scene_hash"], _expected_hash())
|
|
81
|
+
self.assertEqual(result["observed_planning_frames"], ["world"])
|
|
82
|
+
|
|
83
|
+
def test_unproved_world_frame_stays_fail_closed(self):
|
|
84
|
+
result = _verify_observed_scene(
|
|
85
|
+
planning_frame="base_link",
|
|
86
|
+
expected_hash=_expected_hash(),
|
|
87
|
+
expected_objects=_expected_objects(),
|
|
88
|
+
observed_objects=_observed(frame_id="world"),
|
|
89
|
+
)
|
|
90
|
+
self.assertFalse(result["success"])
|
|
91
|
+
self.assertEqual(
|
|
92
|
+
result["code"],
|
|
93
|
+
"scene_planning_frame_mismatch:devagent_box:world",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def test_equivalent_frame_does_not_bypass_geometry_hash(self):
|
|
97
|
+
result = _verify_observed_scene(
|
|
98
|
+
planning_frame="base_link",
|
|
99
|
+
expected_hash=_expected_hash(),
|
|
100
|
+
expected_objects=_expected_objects(),
|
|
101
|
+
observed_objects=_observed(
|
|
102
|
+
frame_id="world",
|
|
103
|
+
dimensions=(0.2, 0.08, 0.06),
|
|
104
|
+
),
|
|
105
|
+
equivalent_planning_frames=("world",),
|
|
106
|
+
)
|
|
107
|
+
self.assertFalse(result["success"])
|
|
108
|
+
self.assertEqual(result["code"], "moveit_scene_readback_mismatch")
|
|
109
|
+
self.assertNotEqual(result["observed_scene_hash"], _expected_hash())
|
|
110
|
+
|
|
111
|
+
def test_only_bound_verified_receipt_can_add_equivalent_frame(self):
|
|
112
|
+
receipt = FrameAlignmentReceipt(
|
|
113
|
+
True,
|
|
114
|
+
"frame_alignment_verified",
|
|
115
|
+
"world",
|
|
116
|
+
"base_link",
|
|
117
|
+
0.0,
|
|
118
|
+
0.0,
|
|
119
|
+
)
|
|
120
|
+
self.assertEqual(
|
|
121
|
+
_equivalent_frames_from_receipt("base_link", receipt),
|
|
122
|
+
("world",),
|
|
123
|
+
)
|
|
124
|
+
with self.assertRaisesRegex(
|
|
125
|
+
SimulationPlatformError,
|
|
126
|
+
"moveit_scene_frame_receipt_unbound",
|
|
127
|
+
):
|
|
128
|
+
_equivalent_frames_from_receipt(
|
|
129
|
+
"base_link",
|
|
130
|
+
FrameAlignmentReceipt(
|
|
131
|
+
True,
|
|
132
|
+
"frame_alignment_verified",
|
|
133
|
+
"map",
|
|
134
|
+
"odom",
|
|
135
|
+
0.0,
|
|
136
|
+
0.0,
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _RetryRunner:
|
|
142
|
+
def __init__(self, expected_scene_hash: str):
|
|
143
|
+
self.expected_scene_hash = expected_scene_hash
|
|
144
|
+
self.scene_calls = 0
|
|
145
|
+
self.tf_calls = 0
|
|
146
|
+
self.payloads: list[dict[str, object]] = []
|
|
147
|
+
|
|
148
|
+
def run(self, command, *, timeout_s):
|
|
149
|
+
command = tuple(command)
|
|
150
|
+
helper = Path(command[1]).name if len(command) > 1 else ""
|
|
151
|
+
if helper == "scene_probe.py":
|
|
152
|
+
self.scene_calls += 1
|
|
153
|
+
input_path = Path(command[command.index("--input") + 1])
|
|
154
|
+
payload = json.loads(input_path.read_text(encoding="utf-8"))
|
|
155
|
+
self.payloads.append(payload)
|
|
156
|
+
if self.scene_calls == 1:
|
|
157
|
+
output = {
|
|
158
|
+
"success": False,
|
|
159
|
+
"code": "scene_planning_frame_mismatch:devagent_box:world",
|
|
160
|
+
"observed_object_count": 1,
|
|
161
|
+
"observed_planning_frames": ["world"],
|
|
162
|
+
}
|
|
163
|
+
return CommandResult(command, 2, json.dumps(output) + "\n", "", 0.01)
|
|
164
|
+
output = {
|
|
165
|
+
"success": True,
|
|
166
|
+
"code": "moveit_scene_readback_verified",
|
|
167
|
+
"observed_scene_hash": self.expected_scene_hash,
|
|
168
|
+
"observed_object_count": 1,
|
|
169
|
+
"observed_planning_frames": ["world"],
|
|
170
|
+
"missing_object_ids": [],
|
|
171
|
+
}
|
|
172
|
+
return CommandResult(command, 0, json.dumps(output) + "\n", "", 0.01)
|
|
173
|
+
if helper == "tf_probe.py":
|
|
174
|
+
self.tf_calls += 1
|
|
175
|
+
output = {
|
|
176
|
+
"success": True,
|
|
177
|
+
"code": "tf_lookup_ok",
|
|
178
|
+
"translation_m": [0.0, 0.0, 0.0],
|
|
179
|
+
"quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
|
180
|
+
}
|
|
181
|
+
return CommandResult(command, 0, json.dumps(output) + "\n", "", 0.01)
|
|
182
|
+
raise AssertionError(f"unexpected command: {command}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class MoveItSceneBridgeRetryTests(unittest.TestCase):
|
|
186
|
+
def test_bridge_retries_only_after_observed_frame_identity_proof(self):
|
|
187
|
+
materialization = _materialization()
|
|
188
|
+
runner = _RetryRunner(materialization.scene_hash)
|
|
189
|
+
with TemporaryDirectory() as tmp:
|
|
190
|
+
receipt = MoveItSceneBridge(
|
|
191
|
+
runner,
|
|
192
|
+
work_dir=tmp,
|
|
193
|
+
timeout_s=1.0,
|
|
194
|
+
).apply_and_verify(materialization)
|
|
195
|
+
|
|
196
|
+
self.assertTrue(receipt.verified)
|
|
197
|
+
self.assertEqual(runner.scene_calls, 2)
|
|
198
|
+
self.assertEqual(runner.tf_calls, 1)
|
|
199
|
+
self.assertEqual(
|
|
200
|
+
runner.payloads[0]["equivalent_planning_frames"],
|
|
201
|
+
[],
|
|
202
|
+
)
|
|
203
|
+
self.assertEqual(
|
|
204
|
+
runner.payloads[1]["equivalent_planning_frames"],
|
|
205
|
+
["world"],
|
|
206
|
+
)
|
|
207
|
+
self.assertEqual(receipt.observed_planning_frames, ("world",))
|
|
208
|
+
self.assertEqual(receipt.equivalent_planning_frames, ("world",))
|
|
209
|
+
self.assertEqual(receipt.observed_scene_hash, materialization.scene_hash)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|