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 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,3 @@
1
+ from .mock import MockBackend
2
+
3
+ __all__ = ["MockBackend"]
@@ -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,10 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class RenderData:
8
+ task_id: str
9
+ status: str
10
+ image_urls: list[str] = field(default_factory=list)
@@ -0,0 +1,3 @@
1
+ from .camera import Camera
2
+
3
+ __all__ = ["Camera"]
@@ -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
+ qhsim
qhsim-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()