qhsim 0.1.0__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.
- qhsim-0.1.0/PKG-INFO +60 -0
- qhsim-0.1.0/README.md +52 -0
- qhsim-0.1.0/pyproject.toml +15 -0
- qhsim-0.1.0/qhsim/__init__.py +30 -0
- qhsim-0.1.0/qhsim/backends/__init__.py +3 -0
- qhsim-0.1.0/qhsim/backends/mock.py +55 -0
- qhsim-0.1.0/qhsim/core/__init__.py +14 -0
- qhsim-0.1.0/qhsim/core/api.py +116 -0
- qhsim-0.1.0/qhsim/core/math.py +45 -0
- qhsim-0.1.0/qhsim/core/types.py +10 -0
- qhsim-0.1.0/qhsim/sensors/__init__.py +3 -0
- qhsim-0.1.0/qhsim/sensors/camera.py +52 -0
- qhsim-0.1.0/qhsim.egg-info/PKG-INFO +60 -0
- qhsim-0.1.0/qhsim.egg-info/SOURCES.txt +16 -0
- qhsim-0.1.0/qhsim.egg-info/dependency_links.txt +1 -0
- qhsim-0.1.0/qhsim.egg-info/top_level.txt +1 -0
- qhsim-0.1.0/setup.cfg +4 -0
- qhsim-0.1.0/tests/test_qhsim.py +137 -0
qhsim-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qhsim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mock-backend Python SDK for simulation-carema.
|
|
5
|
+
Author: Qunhe
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# qhsim SDK
|
|
10
|
+
|
|
11
|
+
Mock-backend Python SDK for the simulation-carema v1 API.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install -e sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from qhsim import SimulationApp
|
|
23
|
+
from qhsim.core.api import World
|
|
24
|
+
from qhsim.core.math import Position, Target
|
|
25
|
+
from qhsim.sensors.camera import Camera
|
|
26
|
+
|
|
27
|
+
simulation_app = SimulationApp(user_id="3FO4KV8SHSLV")
|
|
28
|
+
|
|
29
|
+
world = World(simulation_app)
|
|
30
|
+
world.load_scene(design_id="3FO3BK4HCD6O")
|
|
31
|
+
|
|
32
|
+
camera = Camera(
|
|
33
|
+
name="camera",
|
|
34
|
+
position=Position(0.0, 0.0, 1600.0),
|
|
35
|
+
target=Target(0.0, 1938.0, 1300.0),
|
|
36
|
+
fov=80.0,
|
|
37
|
+
clarity=1,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
world.scene.add(camera)
|
|
41
|
+
|
|
42
|
+
camera.set_world_pose(
|
|
43
|
+
position=Position(1090.0, 0.0, 1600.0),
|
|
44
|
+
target=Target(100.0, 1938.0, 1300.0),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
render_data = world.render()
|
|
48
|
+
print(render_data.image_urls)
|
|
49
|
+
print(world.result[render_data.task_id])
|
|
50
|
+
print(camera.get_history_pose())
|
|
51
|
+
print(world.scene.get_object(object_name=camera.name))
|
|
52
|
+
|
|
53
|
+
simulation_app.close()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The default backend is in-process and deterministic. To swap it out, pass a backend object with `load_scene()` and `render()` methods:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
simulation_app = SimulationApp(config={"backend": custom_backend})
|
|
60
|
+
```
|
qhsim-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# qhsim SDK
|
|
2
|
+
|
|
3
|
+
Mock-backend Python SDK for the simulation-carema v1 API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install -e sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from qhsim import SimulationApp
|
|
15
|
+
from qhsim.core.api import World
|
|
16
|
+
from qhsim.core.math import Position, Target
|
|
17
|
+
from qhsim.sensors.camera import Camera
|
|
18
|
+
|
|
19
|
+
simulation_app = SimulationApp(user_id="3FO4KV8SHSLV")
|
|
20
|
+
|
|
21
|
+
world = World(simulation_app)
|
|
22
|
+
world.load_scene(design_id="3FO3BK4HCD6O")
|
|
23
|
+
|
|
24
|
+
camera = Camera(
|
|
25
|
+
name="camera",
|
|
26
|
+
position=Position(0.0, 0.0, 1600.0),
|
|
27
|
+
target=Target(0.0, 1938.0, 1300.0),
|
|
28
|
+
fov=80.0,
|
|
29
|
+
clarity=1,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
world.scene.add(camera)
|
|
33
|
+
|
|
34
|
+
camera.set_world_pose(
|
|
35
|
+
position=Position(1090.0, 0.0, 1600.0),
|
|
36
|
+
target=Target(100.0, 1938.0, 1300.0),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
render_data = world.render()
|
|
40
|
+
print(render_data.image_urls)
|
|
41
|
+
print(world.result[render_data.task_id])
|
|
42
|
+
print(camera.get_history_pose())
|
|
43
|
+
print(world.scene.get_object(object_name=camera.name))
|
|
44
|
+
|
|
45
|
+
simulation_app.close()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The default backend is in-process and deterministic. To swap it out, pass a backend object with `load_scene()` and `render()` methods:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
simulation_app = SimulationApp(config={"backend": custom_backend})
|
|
52
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qhsim"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Mock-backend Python SDK for simulation-carema."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Qunhe" }]
|
|
12
|
+
|
|
13
|
+
[tool.setuptools.packages.find]
|
|
14
|
+
where = ["."]
|
|
15
|
+
include = ["qhsim*"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .backends.mock import MockBackend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimulationApp:
|
|
9
|
+
"""SDK runtime entrypoint."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, user_id: str | None = None, config: dict[str, Any] | None = None) -> None:
|
|
12
|
+
self.user_id = user_id
|
|
13
|
+
self.config = dict(config or {})
|
|
14
|
+
self.backend = self.config.get("backend") or MockBackend()
|
|
15
|
+
self._running = True
|
|
16
|
+
self._update_count = 0
|
|
17
|
+
|
|
18
|
+
def update(self) -> None:
|
|
19
|
+
if not self._running:
|
|
20
|
+
raise RuntimeError("SimulationApp is closed")
|
|
21
|
+
self._update_count += 1
|
|
22
|
+
|
|
23
|
+
def close(self) -> None:
|
|
24
|
+
self._running = False
|
|
25
|
+
|
|
26
|
+
def is_running(self) -> bool:
|
|
27
|
+
return self._running
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ["SimulationApp"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MockBackend:
|
|
8
|
+
"""Deterministic in-process backend used by the SDK by default."""
|
|
9
|
+
|
|
10
|
+
def load_scene(self, design_id: str) -> dict[str, Any]:
|
|
11
|
+
normalized = design_id.strip()
|
|
12
|
+
if not normalized:
|
|
13
|
+
raise ValueError("design_id is empty")
|
|
14
|
+
|
|
15
|
+
digest = hashlib.sha1(normalized.encode("utf-8")).hexdigest()[:8]
|
|
16
|
+
return {
|
|
17
|
+
"design_id": normalized,
|
|
18
|
+
"objects": {
|
|
19
|
+
"camera_target": {
|
|
20
|
+
"id": f"{digest}-target",
|
|
21
|
+
"name": "camera_target",
|
|
22
|
+
"position": {"x": 0.0, "y": 1938.0, "z": 1300.0},
|
|
23
|
+
"bbox": {"x": 100.0, "y": 100.0, "z": 100.0},
|
|
24
|
+
},
|
|
25
|
+
"robot_base": {
|
|
26
|
+
"id": f"{digest}-robot-base",
|
|
27
|
+
"name": "robot_base",
|
|
28
|
+
"position": {"x": 0.0, "y": 0.0, "z": 0.0},
|
|
29
|
+
"bbox": {"x": 1200.0, "y": 1200.0, "z": 1600.0},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def render(
|
|
35
|
+
self,
|
|
36
|
+
design_id: str,
|
|
37
|
+
camera_history: list[dict[str, Any]],
|
|
38
|
+
camera_name: str | None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
normalized = design_id.strip()
|
|
41
|
+
if not normalized:
|
|
42
|
+
raise ValueError("design_id is empty")
|
|
43
|
+
if not camera_history:
|
|
44
|
+
raise ValueError("camera_history is empty")
|
|
45
|
+
|
|
46
|
+
raw = f"{normalized}:{camera_name or 'camera'}:{len(camera_history)}"
|
|
47
|
+
task_id = f"mock-render-{hashlib.sha1(raw.encode('utf-8')).hexdigest()[:12]}"
|
|
48
|
+
return {
|
|
49
|
+
"task_id": task_id,
|
|
50
|
+
"status": "completed",
|
|
51
|
+
"image_urls": [
|
|
52
|
+
f"mock://render/{task_id}/image-{index + 1}.png"
|
|
53
|
+
for index in range(len(camera_history))
|
|
54
|
+
],
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .api import ModelData, Scene, SimulationContext, World
|
|
2
|
+
from .math import BoundingBox, Position, Target
|
|
3
|
+
from .types import RenderData
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"BoundingBox",
|
|
7
|
+
"ModelData",
|
|
8
|
+
"Position",
|
|
9
|
+
"RenderData",
|
|
10
|
+
"Scene",
|
|
11
|
+
"SimulationContext",
|
|
12
|
+
"Target",
|
|
13
|
+
"World",
|
|
14
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from qhsim import SimulationApp
|
|
7
|
+
from qhsim.core.math import BoundingBox, Position
|
|
8
|
+
from qhsim.core.types import RenderData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ModelData:
|
|
13
|
+
id: str
|
|
14
|
+
name: str
|
|
15
|
+
position: Position
|
|
16
|
+
bbox: BoundingBox
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SimulationContext:
|
|
20
|
+
def __init__(self, app: SimulationApp | None = None) -> None:
|
|
21
|
+
self.app = app or SimulationApp()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Scene:
|
|
25
|
+
def __init__(self, design_id: str, objects: dict[str, object] | None = None) -> None:
|
|
26
|
+
self.design_id = design_id
|
|
27
|
+
self.objects: dict[str, object] = dict(objects or {})
|
|
28
|
+
|
|
29
|
+
def add(self, obj: object) -> None:
|
|
30
|
+
name = getattr(obj, "name", None)
|
|
31
|
+
if not name:
|
|
32
|
+
raise ValueError("scene object must have a non-empty name")
|
|
33
|
+
self.objects[str(name)] = obj
|
|
34
|
+
|
|
35
|
+
def get_objects(self) -> dict[str, object]:
|
|
36
|
+
return dict(self.objects)
|
|
37
|
+
|
|
38
|
+
def get_object(self, object_id: str | None = None, object_name: str | None = None) -> object:
|
|
39
|
+
if object_id is None and object_name is None:
|
|
40
|
+
raise ValueError("object_id or object_name is required")
|
|
41
|
+
|
|
42
|
+
if object_name is not None:
|
|
43
|
+
try:
|
|
44
|
+
return self.objects[object_name]
|
|
45
|
+
except KeyError as exc:
|
|
46
|
+
raise KeyError(f"object not found by name: {object_name}") from exc
|
|
47
|
+
|
|
48
|
+
for obj in self.objects.values():
|
|
49
|
+
if getattr(obj, "id", None) == object_id:
|
|
50
|
+
return obj
|
|
51
|
+
raise KeyError(f"object not found by id: {object_id}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class World:
|
|
55
|
+
def __init__(self, app: SimulationApp | None = None) -> None:
|
|
56
|
+
self.app = app or SimulationApp()
|
|
57
|
+
self.scene: Scene | None = None
|
|
58
|
+
self.result: dict[str, RenderData] = {}
|
|
59
|
+
|
|
60
|
+
def load_scene(self, design_id: str) -> Scene:
|
|
61
|
+
normalized = design_id.strip()
|
|
62
|
+
if not normalized:
|
|
63
|
+
raise ValueError("design_id is empty")
|
|
64
|
+
|
|
65
|
+
response = self.app.backend.load_scene(normalized)
|
|
66
|
+
objects = {
|
|
67
|
+
name: _model_from_payload(payload)
|
|
68
|
+
for name, payload in dict(response.get("objects", {})).items()
|
|
69
|
+
}
|
|
70
|
+
self.scene = Scene(str(response.get("design_id") or normalized), objects)
|
|
71
|
+
return self.scene
|
|
72
|
+
|
|
73
|
+
def render(self, camera: object | None = None) -> RenderData:
|
|
74
|
+
if self.scene is None:
|
|
75
|
+
raise RuntimeError("load_scene must be called before render")
|
|
76
|
+
|
|
77
|
+
resolved_camera = camera or self._first_camera()
|
|
78
|
+
if resolved_camera is None:
|
|
79
|
+
raise RuntimeError("render requires a camera in the scene")
|
|
80
|
+
|
|
81
|
+
to_render_history = getattr(resolved_camera, "to_render_history", None)
|
|
82
|
+
if not callable(to_render_history):
|
|
83
|
+
raise TypeError("render camera must provide to_render_history()")
|
|
84
|
+
|
|
85
|
+
camera_history = to_render_history()
|
|
86
|
+
if not camera_history:
|
|
87
|
+
raise ValueError("camera history is empty")
|
|
88
|
+
|
|
89
|
+
response = self.app.backend.render(
|
|
90
|
+
self.scene.design_id,
|
|
91
|
+
camera_history,
|
|
92
|
+
getattr(resolved_camera, "name", None),
|
|
93
|
+
)
|
|
94
|
+
render_data = RenderData(
|
|
95
|
+
task_id=str(response["task_id"]),
|
|
96
|
+
status=str(response.get("status", "unknown")),
|
|
97
|
+
image_urls=[str(url) for url in response.get("image_urls", [])],
|
|
98
|
+
)
|
|
99
|
+
self.result[render_data.task_id] = render_data
|
|
100
|
+
return render_data
|
|
101
|
+
|
|
102
|
+
def _first_camera(self) -> object | None:
|
|
103
|
+
for obj in self.scene.objects.values() if self.scene else []:
|
|
104
|
+
if callable(getattr(obj, "to_render_history", None)):
|
|
105
|
+
return obj
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _model_from_payload(payload: Any) -> ModelData:
|
|
110
|
+
data = dict(payload)
|
|
111
|
+
return ModelData(
|
|
112
|
+
id=str(data["id"]),
|
|
113
|
+
name=str(data["name"]),
|
|
114
|
+
position=Position.from_dict(dict(data["position"])),
|
|
115
|
+
bbox=BoundingBox.from_dict(dict(data["bbox"])),
|
|
116
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Position:
|
|
8
|
+
x: float
|
|
9
|
+
y: float
|
|
10
|
+
z: float
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_dict(cls, data: dict[str, object]) -> "Position":
|
|
14
|
+
return cls(float(data["x"]), float(data["y"]), float(data["z"]))
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> dict[str, float]:
|
|
17
|
+
return {"x": self.x, "y": self.y, "z": self.z}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Target:
|
|
22
|
+
x: float
|
|
23
|
+
y: float
|
|
24
|
+
z: float
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_dict(cls, data: dict[str, object]) -> "Target":
|
|
28
|
+
return cls(float(data["x"]), float(data["y"]), float(data["z"]))
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict[str, float]:
|
|
31
|
+
return {"x": self.x, "y": self.y, "z": self.z}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class BoundingBox:
|
|
36
|
+
x: float
|
|
37
|
+
y: float
|
|
38
|
+
z: float
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(cls, data: dict[str, object]) -> "BoundingBox":
|
|
42
|
+
return cls(float(data["x"]), float(data["y"]), float(data["z"]))
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, float]:
|
|
45
|
+
return {"x": self.x, "y": self.y, "z": self.z}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from qhsim.core.math import Position, Target
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Camera:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
position: Position,
|
|
12
|
+
target: Target,
|
|
13
|
+
name: str = "camera",
|
|
14
|
+
resolution: tuple[int, int] | None = None,
|
|
15
|
+
fov: float = 80.0,
|
|
16
|
+
clarity: int = 1,
|
|
17
|
+
) -> None:
|
|
18
|
+
self.id = f"camera-{uuid4().hex}"
|
|
19
|
+
self.name = name
|
|
20
|
+
self.position = position
|
|
21
|
+
self.target = target
|
|
22
|
+
self.resolution = resolution
|
|
23
|
+
self.fov = float(fov)
|
|
24
|
+
self.clarity = int(clarity)
|
|
25
|
+
self.history_pose: list[tuple[Position, Target]] = []
|
|
26
|
+
self._record_pose()
|
|
27
|
+
|
|
28
|
+
def set_world_pose(self, position: Position, target: Target) -> None:
|
|
29
|
+
self.position = position
|
|
30
|
+
self.target = target
|
|
31
|
+
self._record_pose()
|
|
32
|
+
|
|
33
|
+
def get_world_pose(self) -> tuple[Position, Target]:
|
|
34
|
+
return self.position, self.target
|
|
35
|
+
|
|
36
|
+
def get_history_pose(self) -> list[tuple[Position, Target]]:
|
|
37
|
+
return list(self.history_pose)
|
|
38
|
+
|
|
39
|
+
def to_render_history(self) -> list[dict[str, object]]:
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
"position": position.to_dict(),
|
|
43
|
+
"target": target.to_dict(),
|
|
44
|
+
"fov": self.fov,
|
|
45
|
+
"resolution": self.resolution,
|
|
46
|
+
"clarity": self.clarity,
|
|
47
|
+
}
|
|
48
|
+
for position, target in self.history_pose
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def _record_pose(self) -> None:
|
|
52
|
+
self.history_pose.append((self.position, self.target))
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qhsim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mock-backend Python SDK for simulation-carema.
|
|
5
|
+
Author: Qunhe
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# qhsim SDK
|
|
10
|
+
|
|
11
|
+
Mock-backend Python SDK for the simulation-carema v1 API.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install -e sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from qhsim import SimulationApp
|
|
23
|
+
from qhsim.core.api import World
|
|
24
|
+
from qhsim.core.math import Position, Target
|
|
25
|
+
from qhsim.sensors.camera import Camera
|
|
26
|
+
|
|
27
|
+
simulation_app = SimulationApp(user_id="3FO4KV8SHSLV")
|
|
28
|
+
|
|
29
|
+
world = World(simulation_app)
|
|
30
|
+
world.load_scene(design_id="3FO3BK4HCD6O")
|
|
31
|
+
|
|
32
|
+
camera = Camera(
|
|
33
|
+
name="camera",
|
|
34
|
+
position=Position(0.0, 0.0, 1600.0),
|
|
35
|
+
target=Target(0.0, 1938.0, 1300.0),
|
|
36
|
+
fov=80.0,
|
|
37
|
+
clarity=1,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
world.scene.add(camera)
|
|
41
|
+
|
|
42
|
+
camera.set_world_pose(
|
|
43
|
+
position=Position(1090.0, 0.0, 1600.0),
|
|
44
|
+
target=Target(100.0, 1938.0, 1300.0),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
render_data = world.render()
|
|
48
|
+
print(render_data.image_urls)
|
|
49
|
+
print(world.result[render_data.task_id])
|
|
50
|
+
print(camera.get_history_pose())
|
|
51
|
+
print(world.scene.get_object(object_name=camera.name))
|
|
52
|
+
|
|
53
|
+
simulation_app.close()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The default backend is in-process and deterministic. To swap it out, pass a backend object with `load_scene()` and `render()` methods:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
simulation_app = SimulationApp(config={"backend": custom_backend})
|
|
60
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
qhsim/__init__.py
|
|
4
|
+
qhsim.egg-info/PKG-INFO
|
|
5
|
+
qhsim.egg-info/SOURCES.txt
|
|
6
|
+
qhsim.egg-info/dependency_links.txt
|
|
7
|
+
qhsim.egg-info/top_level.txt
|
|
8
|
+
qhsim/backends/__init__.py
|
|
9
|
+
qhsim/backends/mock.py
|
|
10
|
+
qhsim/core/__init__.py
|
|
11
|
+
qhsim/core/api.py
|
|
12
|
+
qhsim/core/math.py
|
|
13
|
+
qhsim/core/types.py
|
|
14
|
+
qhsim/sensors/__init__.py
|
|
15
|
+
qhsim/sensors/camera.py
|
|
16
|
+
tests/test_qhsim.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qhsim
|
qhsim-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from qhsim import SimulationApp
|
|
4
|
+
from qhsim.core.api import ModelData, Scene, World
|
|
5
|
+
from qhsim.core.math import BoundingBox, Position, Target
|
|
6
|
+
from qhsim.core.types import RenderData
|
|
7
|
+
from qhsim.sensors.camera import Camera
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CustomBackend:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.loaded_design_id = None
|
|
13
|
+
self.render_payload = None
|
|
14
|
+
|
|
15
|
+
def load_scene(self, design_id):
|
|
16
|
+
self.loaded_design_id = design_id
|
|
17
|
+
return {
|
|
18
|
+
"design_id": design_id,
|
|
19
|
+
"objects": {
|
|
20
|
+
"mock_object": {
|
|
21
|
+
"id": "object-1",
|
|
22
|
+
"name": "mock_object",
|
|
23
|
+
"position": {"x": 1, "y": 2, "z": 3},
|
|
24
|
+
"bbox": {"x": 4, "y": 5, "z": 6},
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def render(self, design_id, camera_history, camera_name):
|
|
30
|
+
self.render_payload = {
|
|
31
|
+
"design_id": design_id,
|
|
32
|
+
"camera_history": camera_history,
|
|
33
|
+
"camera_name": camera_name,
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
"task_id": "task-custom",
|
|
37
|
+
"status": "completed",
|
|
38
|
+
"image_urls": ["mock://custom/image-1.png"],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class QhSimTest(unittest.TestCase):
|
|
43
|
+
def test_documented_imports(self):
|
|
44
|
+
self.assertIs(SimulationApp, SimulationApp)
|
|
45
|
+
self.assertIs(World, World)
|
|
46
|
+
self.assertIs(Scene, Scene)
|
|
47
|
+
self.assertIs(ModelData, ModelData)
|
|
48
|
+
self.assertIs(Position, Position)
|
|
49
|
+
self.assertIs(Target, Target)
|
|
50
|
+
self.assertIs(BoundingBox, BoundingBox)
|
|
51
|
+
self.assertIs(RenderData, RenderData)
|
|
52
|
+
self.assertIs(Camera, Camera)
|
|
53
|
+
|
|
54
|
+
def test_simulation_app_lifecycle(self):
|
|
55
|
+
app = SimulationApp(user_id="user-1")
|
|
56
|
+
self.assertTrue(app.is_running())
|
|
57
|
+
app.update()
|
|
58
|
+
app.close()
|
|
59
|
+
self.assertFalse(app.is_running())
|
|
60
|
+
with self.assertRaisesRegex(RuntimeError, "closed"):
|
|
61
|
+
app.update()
|
|
62
|
+
|
|
63
|
+
def test_load_scene_fills_objects(self):
|
|
64
|
+
backend = CustomBackend()
|
|
65
|
+
app = SimulationApp(config={"backend": backend})
|
|
66
|
+
world = World(app)
|
|
67
|
+
|
|
68
|
+
scene = world.load_scene("design-1")
|
|
69
|
+
|
|
70
|
+
self.assertEqual(backend.loaded_design_id, "design-1")
|
|
71
|
+
self.assertEqual(scene.design_id, "design-1")
|
|
72
|
+
obj = scene.get_object(object_name="mock_object")
|
|
73
|
+
self.assertIsInstance(obj, ModelData)
|
|
74
|
+
self.assertEqual(obj.id, "object-1")
|
|
75
|
+
|
|
76
|
+
def test_scene_get_object_by_id_and_name(self):
|
|
77
|
+
scene = Scene(
|
|
78
|
+
"design-1",
|
|
79
|
+
{
|
|
80
|
+
"box": ModelData(
|
|
81
|
+
id="box-id",
|
|
82
|
+
name="box",
|
|
83
|
+
position=Position(1, 2, 3),
|
|
84
|
+
bbox=BoundingBox(4, 5, 6),
|
|
85
|
+
)
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.assertEqual(scene.get_object(object_name="box").id, "box-id")
|
|
90
|
+
self.assertEqual(scene.get_object(object_id="box-id").name, "box")
|
|
91
|
+
with self.assertRaisesRegex(ValueError, "object_id or object_name"):
|
|
92
|
+
scene.get_object()
|
|
93
|
+
|
|
94
|
+
def test_camera_pose_history(self):
|
|
95
|
+
camera = Camera(Position(0, 0, 1), Target(0, 0, 0), name="camera")
|
|
96
|
+
camera.set_world_pose(Position(1, 2, 3), Target(4, 5, 6))
|
|
97
|
+
|
|
98
|
+
position, target = camera.get_world_pose()
|
|
99
|
+
self.assertEqual(position, Position(1, 2, 3))
|
|
100
|
+
self.assertEqual(target, Target(4, 5, 6))
|
|
101
|
+
self.assertEqual(len(camera.get_history_pose()), 2)
|
|
102
|
+
|
|
103
|
+
def test_render_uses_backend_and_stores_result(self):
|
|
104
|
+
backend = CustomBackend()
|
|
105
|
+
app = SimulationApp(config={"backend": backend})
|
|
106
|
+
world = World(app)
|
|
107
|
+
scene = world.load_scene("design-1")
|
|
108
|
+
camera = Camera(Position(0, 0, 1), Target(0, 0, 0), name="camera")
|
|
109
|
+
scene.add(camera)
|
|
110
|
+
|
|
111
|
+
render_data = world.render()
|
|
112
|
+
|
|
113
|
+
self.assertEqual(backend.render_payload["design_id"], "design-1")
|
|
114
|
+
self.assertEqual(backend.render_payload["camera_name"], "camera")
|
|
115
|
+
self.assertEqual(len(backend.render_payload["camera_history"]), 1)
|
|
116
|
+
self.assertEqual(render_data.task_id, "task-custom")
|
|
117
|
+
self.assertEqual(render_data.status, "completed")
|
|
118
|
+
self.assertEqual(render_data.image_urls, ["mock://custom/image-1.png"])
|
|
119
|
+
self.assertIs(world.result[render_data.task_id], render_data)
|
|
120
|
+
|
|
121
|
+
def test_render_requires_loaded_scene(self):
|
|
122
|
+
with self.assertRaisesRegex(RuntimeError, "load_scene"):
|
|
123
|
+
World().render(Camera(Position(0, 0, 1), Target(0, 0, 0)))
|
|
124
|
+
|
|
125
|
+
def test_render_requires_camera(self):
|
|
126
|
+
world = World()
|
|
127
|
+
world.load_scene("design-1")
|
|
128
|
+
with self.assertRaisesRegex(RuntimeError, "camera"):
|
|
129
|
+
world.render()
|
|
130
|
+
|
|
131
|
+
def test_empty_design_id_fails(self):
|
|
132
|
+
with self.assertRaisesRegex(ValueError, "design_id is empty"):
|
|
133
|
+
World().load_scene(" ")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
unittest.main()
|