Skip to content

Commit 5d2a3b8

Browse files
authored
Create simulated_execution_adapter.py
1 parent b938f57 commit 5d2a3b8

1 file changed

Lines changed: 257 additions & 0 deletions

File tree

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
"""
2+
In-memory simulated execution adapter for IX-HapticSight.
3+
4+
This adapter is intentionally simple and deterministic. Its purpose is to:
5+
- exercise the execution adapter contract in tests
6+
- provide a backend-agnostic placeholder for local runtime integration
7+
- support replay and benchmark scaffolding before any ROS 2 or hardware bridge
8+
9+
It does not perform real motion planning or physics.
10+
It simulates execution state transitions in a conservative, inspectable way.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from dataclasses import dataclass, field
16+
from time import time
17+
from typing import Dict, Optional
18+
19+
from .execution_adapter import (
20+
BoundedExecutionRequest,
21+
ExecutionAdapter,
22+
ExecutionBackendCapabilities,
23+
ExecutionCommandKind,
24+
ExecutionResponse,
25+
ExecutionResultStatus,
26+
ExecutionUpdate,
27+
)
28+
29+
30+
@dataclass
31+
class _SessionExecutionState:
32+
session_id: str
33+
request_id: str
34+
backend_execution_id: str
35+
status: ExecutionResultStatus
36+
progress: float = 0.0
37+
reason_code: str = ""
38+
last_update_utc_s: float = field(default_factory=time)
39+
40+
41+
class SimulatedExecutionAdapter(ExecutionAdapter):
42+
"""
43+
Deterministic in-memory adapter that implements the ExecutionAdapter
44+
contract without talking to real hardware or middleware.
45+
"""
46+
47+
def __init__(
48+
self,
49+
*,
50+
backend_name: str = "simulated-execution",
51+
support_pose_targets: bool = True,
52+
support_plan_execution: bool = True,
53+
support_progress_updates: bool = True,
54+
) -> None:
55+
self._caps = ExecutionBackendCapabilities(
56+
backend_name=backend_name,
57+
supports_plan_execution=support_plan_execution,
58+
supports_pose_targets=support_pose_targets,
59+
supports_abort=True,
60+
supports_retreat=True,
61+
supports_safe_hold=True,
62+
supports_progress_updates=support_progress_updates,
63+
supports_collision_aware_execution=False,
64+
supports_velocity_scaling=True,
65+
supports_force_limited_execution=False,
66+
)
67+
self._sessions: Dict[str, _SessionExecutionState] = {}
68+
self._counter = 0
69+
70+
def capabilities(self) -> ExecutionBackendCapabilities:
71+
return self._caps
72+
73+
def submit(self, request: BoundedExecutionRequest) -> ExecutionResponse:
74+
request.validate()
75+
76+
if request.command_kind == ExecutionCommandKind.PLAN and not self._caps.supports_plan_execution:
77+
return self._reject(request, "plan_execution_not_supported")
78+
if request.command_kind == ExecutionCommandKind.POSE_TARGET and not self._caps.supports_pose_targets:
79+
return self._reject(request, "pose_target_not_supported")
80+
81+
self._counter += 1
82+
backend_execution_id = f"{self._caps.backend_name}:{self._counter}"
83+
84+
status = ExecutionResultStatus.ACCEPTED
85+
progress = 0.0
86+
87+
if request.command_kind == ExecutionCommandKind.RETREAT:
88+
status = ExecutionResultStatus.RETREATING
89+
elif request.command_kind == ExecutionCommandKind.SAFE_HOLD:
90+
status = ExecutionResultStatus.SAFE_HOLD
91+
progress = 1.0
92+
elif request.command_kind == ExecutionCommandKind.ABORT:
93+
status = ExecutionResultStatus.ABORTED
94+
progress = 1.0
95+
96+
self._sessions[request.session_id] = _SessionExecutionState(
97+
session_id=request.session_id,
98+
request_id=request.request_id,
99+
backend_execution_id=backend_execution_id,
100+
status=status,
101+
progress=progress,
102+
reason_code=request.reason_code,
103+
)
104+
105+
return ExecutionResponse(
106+
request_id=request.request_id,
107+
session_id=request.session_id,
108+
status=status,
109+
accepted=True,
110+
backend_name=self._caps.backend_name,
111+
reason_code=request.reason_code or "accepted",
112+
backend_execution_id=backend_execution_id,
113+
)
114+
115+
def current_update(self, *, session_id: str) -> Optional[ExecutionUpdate]:
116+
state = self._sessions.get(session_id)
117+
if state is None:
118+
return None
119+
return ExecutionUpdate(
120+
request_id=state.request_id,
121+
session_id=state.session_id,
122+
status=state.status,
123+
backend_name=self._caps.backend_name,
124+
progress=state.progress,
125+
reason_code=state.reason_code,
126+
backend_execution_id=state.backend_execution_id,
127+
created_at_utc_s=state.last_update_utc_s,
128+
)
129+
130+
def abort(self, *, session_id: str, reason_code: str = "") -> ExecutionResponse:
131+
state = self._sessions.get(session_id)
132+
if state is None:
133+
return ExecutionResponse(
134+
request_id="",
135+
session_id=session_id,
136+
status=ExecutionResultStatus.UNAVAILABLE,
137+
accepted=False,
138+
backend_name=self._caps.backend_name,
139+
reason_code=reason_code or "unknown_session",
140+
backend_execution_id=None,
141+
)
142+
143+
state.status = ExecutionResultStatus.ABORTED
144+
state.progress = 1.0
145+
state.reason_code = reason_code or "aborted"
146+
state.last_update_utc_s = time()
147+
148+
return ExecutionResponse(
149+
request_id=state.request_id,
150+
session_id=session_id,
151+
status=ExecutionResultStatus.ABORTED,
152+
accepted=True,
153+
backend_name=self._caps.backend_name,
154+
reason_code=state.reason_code,
155+
backend_execution_id=state.backend_execution_id,
156+
)
157+
158+
def safe_hold(self, *, session_id: str, reason_code: str = "") -> ExecutionResponse:
159+
state = self._sessions.get(session_id)
160+
if state is None:
161+
return ExecutionResponse(
162+
request_id="",
163+
session_id=session_id,
164+
status=ExecutionResultStatus.UNAVAILABLE,
165+
accepted=False,
166+
backend_name=self._caps.backend_name,
167+
reason_code=reason_code or "unknown_session",
168+
backend_execution_id=None,
169+
)
170+
171+
state.status = ExecutionResultStatus.SAFE_HOLD
172+
state.progress = 1.0
173+
state.reason_code = reason_code or "safe_hold"
174+
state.last_update_utc_s = time()
175+
176+
return ExecutionResponse(
177+
request_id=state.request_id,
178+
session_id=session_id,
179+
status=ExecutionResultStatus.SAFE_HOLD,
180+
accepted=True,
181+
backend_name=self._caps.backend_name,
182+
reason_code=state.reason_code,
183+
backend_execution_id=state.backend_execution_id,
184+
)
185+
186+
def advance(
187+
self,
188+
*,
189+
session_id: str,
190+
progress: float,
191+
complete: bool = False,
192+
fault: bool = False,
193+
reason_code: str = "",
194+
) -> ExecutionUpdate:
195+
"""
196+
Advance one simulated session for tests or benchmark scaffolding.
197+
198+
Rules:
199+
- progress is clamped to [0, 1]
200+
- fault=True forces FAULTED
201+
- complete=True forces COMPLETED
202+
- SAFE_HOLD and ABORTED remain terminal
203+
"""
204+
state = self._sessions.get(session_id)
205+
if state is None:
206+
raise KeyError(f"unknown session_id: {session_id}")
207+
208+
if state.status in {
209+
ExecutionResultStatus.ABORTED,
210+
ExecutionResultStatus.SAFE_HOLD,
211+
ExecutionResultStatus.FAULTED,
212+
ExecutionResultStatus.COMPLETED,
213+
}:
214+
return self.current_update(session_id=session_id) # type: ignore[return-value]
215+
216+
state.progress = max(0.0, min(1.0, float(progress)))
217+
state.last_update_utc_s = time()
218+
219+
if fault:
220+
state.status = ExecutionResultStatus.FAULTED
221+
state.reason_code = reason_code or "faulted"
222+
elif complete or state.progress >= 1.0:
223+
state.status = ExecutionResultStatus.COMPLETED
224+
state.progress = 1.0
225+
state.reason_code = reason_code or "completed"
226+
elif state.status == ExecutionResultStatus.ACCEPTED:
227+
state.status = ExecutionResultStatus.RUNNING
228+
state.reason_code = reason_code or "running"
229+
else:
230+
state.reason_code = reason_code or state.reason_code or "running"
231+
232+
return ExecutionUpdate(
233+
request_id=state.request_id,
234+
session_id=state.session_id,
235+
status=state.status,
236+
backend_name=self._caps.backend_name,
237+
progress=state.progress,
238+
reason_code=state.reason_code,
239+
backend_execution_id=state.backend_execution_id,
240+
created_at_utc_s=state.last_update_utc_s,
241+
)
242+
243+
def _reject(self, request: BoundedExecutionRequest, reason_code: str) -> ExecutionResponse:
244+
return ExecutionResponse(
245+
request_id=request.request_id,
246+
session_id=request.session_id,
247+
status=ExecutionResultStatus.REJECTED,
248+
accepted=False,
249+
backend_name=self._caps.backend_name,
250+
reason_code=reason_code,
251+
backend_execution_id=None,
252+
)
253+
254+
255+
__all__ = [
256+
"SimulatedExecutionAdapter",
257+
]

0 commit comments

Comments
 (0)