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,404 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
import json
|
|
6
|
+
from math import asin, atan2, cos, isfinite, sin
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .twin import GeometryKind, Pose3D, TwinSpec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TwinMaterializationError(ValueError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class MaterializationIssue:
|
|
20
|
+
code: str
|
|
21
|
+
detail: str
|
|
22
|
+
entity_id: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class MaterializedObject:
|
|
27
|
+
entity_id: str
|
|
28
|
+
backend_id: str
|
|
29
|
+
kind: str
|
|
30
|
+
dimensions_m: tuple[float, ...]
|
|
31
|
+
position_m: tuple[float, float, float]
|
|
32
|
+
quaternion_xyzw: tuple[float, float, float, float]
|
|
33
|
+
static: bool
|
|
34
|
+
mass_kg: float | None = None
|
|
35
|
+
friction: float | None = None
|
|
36
|
+
restitution: float | None = None
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
return asdict(self)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class CanonicalTwinMaterialization:
|
|
44
|
+
"""One canonical geometry set consumed by both Gazebo and MoveIt."""
|
|
45
|
+
|
|
46
|
+
twin_id: str
|
|
47
|
+
twin_hash: str
|
|
48
|
+
robot_profile_key: str
|
|
49
|
+
planning_frame: str
|
|
50
|
+
objects: tuple[MaterializedObject, ...]
|
|
51
|
+
materializer_version: str = "canonical-twin-v1"
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"twin_id": self.twin_id,
|
|
56
|
+
"twin_hash": self.twin_hash,
|
|
57
|
+
"robot_profile_key": self.robot_profile_key,
|
|
58
|
+
"planning_frame": self.planning_frame,
|
|
59
|
+
"objects": [item.to_dict() for item in self.objects],
|
|
60
|
+
"materializer_version": self.materializer_version,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def fingerprint(self) -> str:
|
|
65
|
+
return _hash(self.to_dict())
|
|
66
|
+
|
|
67
|
+
def canonical_scene(self) -> dict[str, Any]:
|
|
68
|
+
return {
|
|
69
|
+
"planning_frame": self.planning_frame,
|
|
70
|
+
"objects": [
|
|
71
|
+
{
|
|
72
|
+
"id": item.backend_id,
|
|
73
|
+
"geometry_kind": item.kind,
|
|
74
|
+
"dimensions_m": [_canon_float(v) for v in item.dimensions_m],
|
|
75
|
+
"position_m": [_canon_float(v) for v in item.position_m],
|
|
76
|
+
"quaternion_xyzw": [_canon_float(v) for v in item.quaternion_xyzw],
|
|
77
|
+
}
|
|
78
|
+
for item in self.objects
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def scene_hash(self) -> str:
|
|
84
|
+
return _hash(self.canonical_scene())
|
|
85
|
+
|
|
86
|
+
def moveit_scene_payload(self) -> dict[str, Any]:
|
|
87
|
+
return {
|
|
88
|
+
"materialization_hash": self.fingerprint,
|
|
89
|
+
"twin_hash": self.twin_hash,
|
|
90
|
+
"planning_frame": self.planning_frame,
|
|
91
|
+
"scene_hash": self.scene_hash,
|
|
92
|
+
"objects": [
|
|
93
|
+
{
|
|
94
|
+
"id": item.backend_id,
|
|
95
|
+
"geometry_kind": item.kind,
|
|
96
|
+
"dimensions_m": list(item.dimensions_m),
|
|
97
|
+
"pose": {
|
|
98
|
+
"position_m": list(item.position_m),
|
|
99
|
+
"quaternion_xyzw": list(item.quaternion_xyzw),
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
for item in self.objects
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def to_sdf(self) -> str:
|
|
107
|
+
lines = [
|
|
108
|
+
"<sdf version='1.10'>",
|
|
109
|
+
" <world name='default'>",
|
|
110
|
+
" <gravity>0 0 -9.8</gravity>",
|
|
111
|
+
" <physics type='ode'><max_step_size>0.001</max_step_size><real_time_factor>1</real_time_factor><real_time_update_rate>1000</real_time_update_rate></physics>",
|
|
112
|
+
]
|
|
113
|
+
for item in self.objects:
|
|
114
|
+
x, y, z = item.position_m
|
|
115
|
+
roll, pitch, yaw = _rpy(item.quaternion_xyzw)
|
|
116
|
+
lines.extend([
|
|
117
|
+
f" <model name='{item.backend_id}'>",
|
|
118
|
+
f" <pose>{_f(x)} {_f(y)} {_f(z)} {_f(roll)} {_f(pitch)} {_f(yaw)}</pose>",
|
|
119
|
+
" <link name='body'>",
|
|
120
|
+
])
|
|
121
|
+
if not item.static:
|
|
122
|
+
if item.mass_kg is None:
|
|
123
|
+
raise TwinMaterializationError(f"dynamic_mass_missing:{item.entity_id}")
|
|
124
|
+
ixx, iyy, izz = _inertia(item, item.mass_kg)
|
|
125
|
+
lines.extend([
|
|
126
|
+
" <inertial>",
|
|
127
|
+
f" <mass>{_f(item.mass_kg)}</mass>",
|
|
128
|
+
" <inertia>",
|
|
129
|
+
f" <ixx>{_f(ixx)}</ixx><ixy>0</ixy><ixz>0</ixz>",
|
|
130
|
+
f" <iyy>{_f(iyy)}</iyy><iyz>0</iyz><izz>{_f(izz)}</izz>",
|
|
131
|
+
" </inertia>",
|
|
132
|
+
" </inertial>",
|
|
133
|
+
])
|
|
134
|
+
geometry = _sdf_geometry(item)
|
|
135
|
+
lines.extend([
|
|
136
|
+
" <collision name='collision'><geometry>",
|
|
137
|
+
*[" " + value for value in geometry],
|
|
138
|
+
" </geometry>",
|
|
139
|
+
])
|
|
140
|
+
if item.friction is not None:
|
|
141
|
+
lines.append(
|
|
142
|
+
" <surface><friction><ode>"
|
|
143
|
+
f"<mu>{_f(item.friction)}</mu><mu2>{_f(item.friction)}</mu2>"
|
|
144
|
+
"</ode></friction></surface>"
|
|
145
|
+
)
|
|
146
|
+
lines.extend([
|
|
147
|
+
" </collision>",
|
|
148
|
+
" <visual name='visual'><geometry>",
|
|
149
|
+
*[" " + value for value in geometry],
|
|
150
|
+
" </geometry></visual>",
|
|
151
|
+
" </link>",
|
|
152
|
+
f" <static>{str(item.static).lower()}</static>",
|
|
153
|
+
" </model>",
|
|
154
|
+
])
|
|
155
|
+
lines.extend([" </world>", "</sdf>"])
|
|
156
|
+
return "\n".join(lines) + "\n"
|
|
157
|
+
|
|
158
|
+
def write_sdf(self, path: Path | str) -> Path:
|
|
159
|
+
target = Path(path).expanduser().resolve()
|
|
160
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
temporary = target.with_suffix(target.suffix + ".tmp")
|
|
162
|
+
temporary.write_text(self.to_sdf(), encoding="utf-8")
|
|
163
|
+
temporary.replace(target)
|
|
164
|
+
return target
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True, slots=True)
|
|
168
|
+
class TwinMaterializationResult:
|
|
169
|
+
twin_hash: str
|
|
170
|
+
materialization: CanonicalTwinMaterialization | None
|
|
171
|
+
issues: tuple[MaterializationIssue, ...]
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def ready(self) -> bool:
|
|
175
|
+
return self.materialization is not None and not self.issues
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True)
|
|
179
|
+
class _Transform:
|
|
180
|
+
translation: tuple[float, float, float]
|
|
181
|
+
quaternion: tuple[float, float, float, float]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _canon_float(value: float) -> float:
|
|
185
|
+
value = float(value)
|
|
186
|
+
if abs(value) < 1e-12:
|
|
187
|
+
return 0.0
|
|
188
|
+
return float(format(value, ".12g"))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _hash(value: object) -> str:
|
|
192
|
+
text = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
193
|
+
return sha256(text.encode("utf-8")).hexdigest()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _qmul(first, second):
|
|
197
|
+
ax, ay, az, aw = first
|
|
198
|
+
bx, by, bz, bw = second
|
|
199
|
+
return (
|
|
200
|
+
aw * bx + ax * bw + ay * bz - az * by,
|
|
201
|
+
aw * by - ax * bz + ay * bw + az * bx,
|
|
202
|
+
aw * bz + ax * by - ay * bx + az * bw,
|
|
203
|
+
aw * bw - ax * bx - ay * by - az * bz,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _qconj(quaternion):
|
|
208
|
+
return (-quaternion[0], -quaternion[1], -quaternion[2], quaternion[3])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _qrot(quaternion, vector):
|
|
212
|
+
output = _qmul(_qmul(quaternion, (vector[0], vector[1], vector[2], 0.0)), _qconj(quaternion))
|
|
213
|
+
return output[:3]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _compose(first: _Transform, second: _Transform) -> _Transform:
|
|
217
|
+
rotated = _qrot(first.quaternion, second.translation)
|
|
218
|
+
return _Transform(
|
|
219
|
+
tuple(first.translation[index] + rotated[index] for index in range(3)),
|
|
220
|
+
_qmul(first.quaternion, second.quaternion),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _inverse(value: _Transform) -> _Transform:
|
|
225
|
+
quaternion = _qconj(value.quaternion)
|
|
226
|
+
return _Transform(_qrot(quaternion, tuple(-component for component in value.translation)), quaternion)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _pose(value: Pose3D) -> _Transform:
|
|
230
|
+
cr, sr = cos(value.roll_rad / 2), sin(value.roll_rad / 2)
|
|
231
|
+
cp, sp = cos(value.pitch_rad / 2), sin(value.pitch_rad / 2)
|
|
232
|
+
cy, sy = cos(value.yaw_rad / 2), sin(value.yaw_rad / 2)
|
|
233
|
+
quaternion = (
|
|
234
|
+
sr * cp * cy - cr * sp * sy,
|
|
235
|
+
cr * sp * cy + sr * cp * sy,
|
|
236
|
+
cr * cp * sy - sr * sp * cy,
|
|
237
|
+
cr * cp * cy + sr * sp * sy,
|
|
238
|
+
)
|
|
239
|
+
return _Transform((float(value.x_m), float(value.y_m), float(value.z_m)), quaternion)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _canonical_quaternion(quaternion):
|
|
243
|
+
canonical = tuple(_canon_float(value) for value in quaternion)
|
|
244
|
+
return tuple(-value for value in canonical) if canonical[3] < 0 else canonical
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _rpy(quaternion):
|
|
248
|
+
x, y, z, w = quaternion
|
|
249
|
+
return (
|
|
250
|
+
atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)),
|
|
251
|
+
asin(max(-1.0, min(1.0, 2 * (w * y - z * x)))),
|
|
252
|
+
atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _backend_id(entity_id: str) -> str:
|
|
257
|
+
slug = re.sub(r"[^A-Za-z0-9_]+", "_", entity_id).strip("_").lower()[:32] or "entity"
|
|
258
|
+
return f"devagent_{slug}_{sha256(entity_id.encode('utf-8')).hexdigest()[:10]}"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _numeric(value: object) -> float | None:
|
|
262
|
+
if value is None or isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
263
|
+
return None
|
|
264
|
+
output = float(value)
|
|
265
|
+
return output if isfinite(output) else None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _f(value: float) -> str:
|
|
269
|
+
return format(float(value), ".12g")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _sdf_geometry(item: MaterializedObject) -> list[str]:
|
|
273
|
+
if item.kind == GeometryKind.BOX.value:
|
|
274
|
+
x, y, z = item.dimensions_m
|
|
275
|
+
return [f"<box><size>{_f(x)} {_f(y)} {_f(z)}</size></box>"]
|
|
276
|
+
radius, height = item.dimensions_m
|
|
277
|
+
return [f"<cylinder><radius>{_f(radius)}</radius><length>{_f(height)}</length></cylinder>"]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _inertia(item: MaterializedObject, mass: float):
|
|
281
|
+
if item.kind == GeometryKind.BOX.value:
|
|
282
|
+
x, y, z = item.dimensions_m
|
|
283
|
+
return (
|
|
284
|
+
mass * (y * y + z * z) / 12,
|
|
285
|
+
mass * (x * x + z * z) / 12,
|
|
286
|
+
mass * (x * x + y * y) / 12,
|
|
287
|
+
)
|
|
288
|
+
radius, height = item.dimensions_m
|
|
289
|
+
transverse = mass * (3 * radius * radius + height * height) / 12
|
|
290
|
+
return transverse, transverse, mass * radius * radius / 2
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class TwinMaterializer:
|
|
294
|
+
"""Resolve the engineering Twin once for both physics and planning backends."""
|
|
295
|
+
|
|
296
|
+
def __init__(self, *, planning_frame: str = "base_link") -> None:
|
|
297
|
+
if not planning_frame.strip():
|
|
298
|
+
raise ValueError("planning_frame_required")
|
|
299
|
+
self.planning_frame = planning_frame
|
|
300
|
+
|
|
301
|
+
def compile(self, twin: TwinSpec) -> TwinMaterializationResult:
|
|
302
|
+
if twin.robot_base_pose is None:
|
|
303
|
+
return TwinMaterializationResult(
|
|
304
|
+
twin.fingerprint,
|
|
305
|
+
None,
|
|
306
|
+
(MaterializationIssue("robot_base_pose_missing", "Resolved robot base pose is required."),),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
issues: list[MaterializationIssue] = []
|
|
310
|
+
frame_specs = {frame.child_frame_id: frame for frame in twin.frames}
|
|
311
|
+
cache: dict[str, _Transform] = {
|
|
312
|
+
"world": _Transform((0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))
|
|
313
|
+
}
|
|
314
|
+
visiting: set[str] = set()
|
|
315
|
+
|
|
316
|
+
def resolve_frame(frame_id: str) -> _Transform:
|
|
317
|
+
if frame_id in cache:
|
|
318
|
+
return cache[frame_id]
|
|
319
|
+
if frame_id in visiting:
|
|
320
|
+
raise TwinMaterializationError(f"frame_cycle:{frame_id}")
|
|
321
|
+
spec = frame_specs.get(frame_id)
|
|
322
|
+
if spec is None:
|
|
323
|
+
raise TwinMaterializationError(f"unknown_frame:{frame_id}")
|
|
324
|
+
visiting.add(frame_id)
|
|
325
|
+
try:
|
|
326
|
+
output = _compose(resolve_frame(spec.parent_frame_id), _pose(spec.transform))
|
|
327
|
+
finally:
|
|
328
|
+
visiting.remove(frame_id)
|
|
329
|
+
cache[frame_id] = output
|
|
330
|
+
return output
|
|
331
|
+
|
|
332
|
+
def world_pose(pose: Pose3D) -> _Transform:
|
|
333
|
+
return _compose(resolve_frame(pose.frame_id), _pose(pose))
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
base_from_world = _inverse(world_pose(twin.robot_base_pose))
|
|
337
|
+
except TwinMaterializationError as exc:
|
|
338
|
+
return TwinMaterializationResult(
|
|
339
|
+
twin.fingerprint,
|
|
340
|
+
None,
|
|
341
|
+
(MaterializationIssue(str(exc).split(":", 1)[0], str(exc)),),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
objects: list[MaterializedObject] = []
|
|
345
|
+
for entity in twin.entities:
|
|
346
|
+
if not entity.enabled or not entity.geometry.collision_geometry:
|
|
347
|
+
continue
|
|
348
|
+
if entity.pose is None:
|
|
349
|
+
issues.append(MaterializationIssue("entity_pose_missing", "Collision entity has no pose.", entity.entity_id))
|
|
350
|
+
continue
|
|
351
|
+
if entity.geometry.kind not in {GeometryKind.BOX, GeometryKind.CYLINDER}:
|
|
352
|
+
issues.append(MaterializationIssue(
|
|
353
|
+
"geometry_not_materializable_v1",
|
|
354
|
+
"Only verified box/cylinder primitives are materialized; mesh/CAD requires asset qualification.",
|
|
355
|
+
entity.entity_id,
|
|
356
|
+
))
|
|
357
|
+
continue
|
|
358
|
+
try:
|
|
359
|
+
backend_pose = _compose(base_from_world, world_pose(entity.pose))
|
|
360
|
+
except TwinMaterializationError as exc:
|
|
361
|
+
issues.append(MaterializationIssue(str(exc).split(":", 1)[0], str(exc), entity.entity_id))
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
is_workpiece = entity.entity_id == twin.workpiece_entity_id
|
|
365
|
+
mass = _numeric(entity.physics.mass_kg.value)
|
|
366
|
+
friction = _numeric(entity.physics.friction_coefficient.value)
|
|
367
|
+
if is_workpiece and mass is None:
|
|
368
|
+
issues.append(MaterializationIssue("dynamic_workpiece_mass_missing", "Dynamic workpiece mass evidence is required.", entity.entity_id))
|
|
369
|
+
continue
|
|
370
|
+
if is_workpiece and friction is None:
|
|
371
|
+
issues.append(MaterializationIssue("dynamic_workpiece_friction_missing", "Dynamic workpiece friction evidence is required.", entity.entity_id))
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
dimensions = tuple(float(value) for value in entity.geometry.dimensions_m)
|
|
375
|
+
if any(not isfinite(value) or value <= 0 for value in dimensions):
|
|
376
|
+
issues.append(MaterializationIssue("invalid_geometry_dimensions", "Geometry dimensions must be finite and positive.", entity.entity_id))
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
objects.append(MaterializedObject(
|
|
380
|
+
entity_id=entity.entity_id,
|
|
381
|
+
backend_id=_backend_id(entity.entity_id),
|
|
382
|
+
kind=entity.geometry.kind.value,
|
|
383
|
+
dimensions_m=dimensions,
|
|
384
|
+
position_m=tuple(_canon_float(value) for value in backend_pose.translation),
|
|
385
|
+
quaternion_xyzw=_canonical_quaternion(backend_pose.quaternion),
|
|
386
|
+
static=not is_workpiece,
|
|
387
|
+
mass_kg=mass,
|
|
388
|
+
friction=friction,
|
|
389
|
+
restitution=_numeric(entity.physics.restitution.value),
|
|
390
|
+
))
|
|
391
|
+
|
|
392
|
+
if not objects:
|
|
393
|
+
issues.append(MaterializationIssue("collision_environment_empty", "At least one collision object must materialize."))
|
|
394
|
+
if issues:
|
|
395
|
+
return TwinMaterializationResult(twin.fingerprint, None, tuple(issues))
|
|
396
|
+
|
|
397
|
+
materialization = CanonicalTwinMaterialization(
|
|
398
|
+
twin_id=twin.twin_id,
|
|
399
|
+
twin_hash=twin.fingerprint,
|
|
400
|
+
robot_profile_key=twin.robot_profile_key,
|
|
401
|
+
planning_frame=self.planning_frame,
|
|
402
|
+
objects=tuple(sorted(objects, key=lambda item: item.backend_id)),
|
|
403
|
+
)
|
|
404
|
+
return TwinMaterializationResult(twin.fingerprint, materialization, ())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .models import Resource, TaskGraph, TaskNode, WorldState
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class VerificationIssue:
|
|
10
|
+
code: str
|
|
11
|
+
task_id: str | None = None
|
|
12
|
+
detail: str = ""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class VerificationResult:
|
|
17
|
+
passed: bool
|
|
18
|
+
issues: list[VerificationIssue] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeterministicVerifier:
|
|
22
|
+
def verify_graph(self, graph: TaskGraph, resources: list[Resource]) -> VerificationResult:
|
|
23
|
+
issues = [VerificationIssue(error) for error in graph.validate_structure()]
|
|
24
|
+
resource_map = {resource.resource_id: resource for resource in resources}
|
|
25
|
+
try:
|
|
26
|
+
ordered = graph.topological_order()
|
|
27
|
+
except ValueError as exc:
|
|
28
|
+
issues.append(VerificationIssue("invalid_task_graph", detail=str(exc)))
|
|
29
|
+
return VerificationResult(False, issues)
|
|
30
|
+
for task in ordered:
|
|
31
|
+
resource = resource_map.get(task.contract.resource_id)
|
|
32
|
+
if resource is None:
|
|
33
|
+
issues.append(VerificationIssue("unknown_resource", task.task_id, task.contract.resource_id))
|
|
34
|
+
elif not resource.supports(task.contract.action):
|
|
35
|
+
issues.append(VerificationIssue("unsupported_capability", task.task_id, task.contract.action.value))
|
|
36
|
+
return VerificationResult(not issues, issues)
|
|
37
|
+
|
|
38
|
+
def verify_runtime_preconditions(self, task: TaskNode, world: WorldState) -> VerificationResult:
|
|
39
|
+
missing = [item for item in task.contract.preconditions if item not in world.facts]
|
|
40
|
+
issues = [VerificationIssue("missing_precondition", task.task_id, item) for item in missing]
|
|
41
|
+
return VerificationResult(not issues, issues)
|
|
42
|
+
|
|
43
|
+
def verify_simulation_constraints(self, task: TaskNode, satisfied_constraints: set[str]) -> VerificationResult:
|
|
44
|
+
missing = [item for item in task.contract.constraints if item not in satisfied_constraints]
|
|
45
|
+
issues = [VerificationIssue("unsatisfied_constraint", task.task_id, item) for item in missing]
|
|
46
|
+
return VerificationResult(not issues, issues)
|