spatial-reasoner 0.1.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.
@@ -0,0 +1,94 @@
1
+ """Public API for the Spatial Reasoner Python implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ from .bbox_sector import BBoxSector, BBoxSectorFlags
8
+ from .reasoner import SpatialReasoner
9
+ from .spatial_basics import (
10
+ MotionState,
11
+ NearbySchema,
12
+ ObjectCause,
13
+ ObjectConfidence,
14
+ ObjectHandling,
15
+ ObjectShape,
16
+ SectorSchema,
17
+ SpatialAdjustment,
18
+ SpatialAtribute,
19
+ SpatialExistence,
20
+ SpatialPredicateCategories,
21
+ defaultAdjustment,
22
+ )
23
+ from .spatial_inference import SpatialInference
24
+ from .spatial_object import SpatialObject
25
+ from .spatial_predicate import (
26
+ PredicateTerm,
27
+ SpatialPredicate,
28
+ SpatialTerms,
29
+ adjacency,
30
+ assembly,
31
+ comparability,
32
+ connectivity,
33
+ contacts,
34
+ directionality,
35
+ geography,
36
+ orientations,
37
+ proximity,
38
+ sectors,
39
+ similarity,
40
+ topology,
41
+ visibility,
42
+ )
43
+ from .spatial_relation import SpatialRelation
44
+ from .spatial_taxonomy import SpatialObjectConcept, SpatialTaxonomy, TaxonomyParser
45
+ from .vector2 import Vector2
46
+ from .vector3 import Vector3
47
+
48
+ try:
49
+ __version__ = version("spatial-reasoner")
50
+ except PackageNotFoundError: # pragma: no cover - direct source-tree import
51
+ __version__ = "0.1.0"
52
+
53
+ __all__ = [
54
+ "BBoxSector",
55
+ "BBoxSectorFlags",
56
+ "MotionState",
57
+ "NearbySchema",
58
+ "ObjectCause",
59
+ "ObjectConfidence",
60
+ "ObjectHandling",
61
+ "ObjectShape",
62
+ "PredicateTerm",
63
+ "SectorSchema",
64
+ "SpatialAdjustment",
65
+ "SpatialAtribute",
66
+ "SpatialExistence",
67
+ "SpatialInference",
68
+ "SpatialObject",
69
+ "SpatialObjectConcept",
70
+ "SpatialPredicate",
71
+ "SpatialPredicateCategories",
72
+ "SpatialReasoner",
73
+ "SpatialRelation",
74
+ "SpatialTaxonomy",
75
+ "SpatialTerms",
76
+ "TaxonomyParser",
77
+ "Vector2",
78
+ "Vector3",
79
+ "__version__",
80
+ "adjacency",
81
+ "assembly",
82
+ "comparability",
83
+ "connectivity",
84
+ "contacts",
85
+ "defaultAdjustment",
86
+ "directionality",
87
+ "geography",
88
+ "orientations",
89
+ "proximity",
90
+ "sectors",
91
+ "similarity",
92
+ "topology",
93
+ "visibility",
94
+ ]
@@ -0,0 +1,351 @@
1
+ # src/BBoxSector.py
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import IntFlag
6
+ from typing import Any, ClassVar
7
+
8
+ from .spatial_predicate import SpatialPredicate
9
+
10
+
11
+ class BBoxSectorFlags(IntFlag):
12
+ none = 0 # no sector specified
13
+ i = 1 << 0 # i : inside, inner
14
+ a = 1 << 1 # a : ahead
15
+ b = 1 << 2 # b : behind
16
+ l = 1 << 3 # noqa: E741 # SRSwift-compatible short sector code for left
17
+ r = 1 << 4 # r : right
18
+ o = 1 << 5 # o : over
19
+ u = 1 << 6 # u : under
20
+
21
+ # Composite sectors
22
+ al = a | l
23
+ ar = a | r
24
+ bl = b | l
25
+ br = b | r
26
+ ao = a | o
27
+ au = a | u
28
+ bo = b | o
29
+ bu = b | u
30
+ lo = l | o
31
+ lu = l | u
32
+ ro = r | o
33
+ ru = r | u
34
+ alo = a | l | o
35
+ aro = a | r | o
36
+ blo = b | l | o
37
+ bro = b | r | o
38
+ alu = a | l | u
39
+ aru = a | r | u
40
+ blu = b | l | u
41
+ bru = b | r | u
42
+
43
+
44
+ class BBoxSector:
45
+ """
46
+ A mutable class that represents spatial sectors using bitmask flags.
47
+ Mimics Swift's OptionSet behavior.
48
+ """
49
+
50
+ # Predefined descriptions for composite and individual sectors
51
+ debug_descriptions: ClassVar[dict[BBoxSectorFlags, str]] = {
52
+ BBoxSectorFlags.i: "i",
53
+ BBoxSectorFlags.a: "a",
54
+ BBoxSectorFlags.b: "b",
55
+ BBoxSectorFlags.l: "l",
56
+ BBoxSectorFlags.r: "r",
57
+ BBoxSectorFlags.o: "o",
58
+ BBoxSectorFlags.u: "u",
59
+ BBoxSectorFlags.al: "al",
60
+ BBoxSectorFlags.ar: "ar",
61
+ BBoxSectorFlags.bl: "bl",
62
+ BBoxSectorFlags.br: "br",
63
+ BBoxSectorFlags.ao: "ao",
64
+ BBoxSectorFlags.au: "au",
65
+ BBoxSectorFlags.bo: "bo",
66
+ BBoxSectorFlags.bu: "bu",
67
+ BBoxSectorFlags.lo: "lo",
68
+ BBoxSectorFlags.lu: "lu",
69
+ BBoxSectorFlags.ro: "ro",
70
+ BBoxSectorFlags.ru: "ru",
71
+ BBoxSectorFlags.alo: "alo",
72
+ BBoxSectorFlags.aro: "aro",
73
+ BBoxSectorFlags.blo: "blo",
74
+ BBoxSectorFlags.bro: "bro",
75
+ BBoxSectorFlags.alu: "alu",
76
+ BBoxSectorFlags.aru: "aru",
77
+ BBoxSectorFlags.blu: "blu",
78
+ BBoxSectorFlags.bru: "bru",
79
+ }
80
+
81
+ # Define base flags (individual flags only)
82
+ base_flags: ClassVar[set[BBoxSectorFlags]] = {
83
+ BBoxSectorFlags.i,
84
+ BBoxSectorFlags.a,
85
+ BBoxSectorFlags.b,
86
+ BBoxSectorFlags.l,
87
+ BBoxSectorFlags.r,
88
+ BBoxSectorFlags.o,
89
+ BBoxSectorFlags.u,
90
+ }
91
+
92
+ def __init__(self, flags=BBoxSectorFlags.none):
93
+ """
94
+ Initialize a BBoxSector instance.
95
+
96
+ Args:
97
+ flags (BBoxSectorFlags, optional): Initial sector flags. Defaults to BBoxSectorFlags.none.
98
+ """
99
+ self.flags = flags
100
+
101
+ def insert(self, flag: BBoxSectorFlags):
102
+ """
103
+ Insert a sector flag.
104
+
105
+ Args:
106
+ flag (BBoxSectorFlags): The flag to insert.
107
+ """
108
+ self.flags |= flag
109
+
110
+ def remove(self, flag: BBoxSectorFlags):
111
+ """
112
+ Remove a sector flag.
113
+
114
+ Args:
115
+ flag (BBoxSectorFlags): The flag to remove.
116
+ """
117
+ self.flags &= ~flag
118
+
119
+ def contains_flag(self, flag: BBoxSectorFlags) -> bool:
120
+ """
121
+ Check if a sector flag is present.
122
+
123
+ Args:
124
+ flag (BBoxSectorFlags): The flag to check.
125
+
126
+ Returns:
127
+ bool: True if the flag is present, False otherwise.
128
+ """
129
+ return (self.flags & flag) == flag
130
+
131
+ def contains(self, flag: BBoxSectorFlags) -> bool:
132
+ """
133
+ Alias for contains_flag to maintain backward compatibility.
134
+
135
+ Args:
136
+ flag (BBoxSectorFlags): The flag to check.
137
+
138
+ Returns:
139
+ bool: True if the flag is present, False otherwise.
140
+ """
141
+ return self.contains_flag(flag)
142
+
143
+ def divergencies(self) -> int:
144
+ """
145
+ Calculate the amount of divergency from the inner zone in all 3 directions.
146
+
147
+ Returns:
148
+ int: 0 if the sector includes 'i' (inside), otherwise the number of set bits.
149
+ """
150
+ if self.contains_flag(BBoxSectorFlags.i):
151
+ return 0
152
+ return bin(self.flags.value).count("1")
153
+
154
+ def list_base_flags(self):
155
+ """
156
+ List only the base flags present in the sector.
157
+ """
158
+ return [
159
+ name
160
+ for name, member in BBoxSectorFlags.__members__.items()
161
+ if member in self.flags and name != "none" and member in BBoxSector.base_flags
162
+ ]
163
+
164
+ def __str__(self) -> str:
165
+ """
166
+ Provide a string representation for the sector.
167
+
168
+ Returns:
169
+ str: The descriptive string of the sector.
170
+ """
171
+ # Always list base flags to match test expectations
172
+ flags = self.list_base_flags()
173
+ if flags:
174
+ return "".join(flags)
175
+ elif self.flags == BBoxSectorFlags.none:
176
+ return "no sector"
177
+ else:
178
+ # If no base flags are set, but some composite flags are, list them
179
+ # This can happen if only composite flags are set without their base flags
180
+ composite_flags = [
181
+ name
182
+ for name, member in BBoxSectorFlags.__members__.items()
183
+ if member in self.flags and name != "none" and member not in BBoxSector.base_flags
184
+ ]
185
+ if composite_flags:
186
+ return "".join(composite_flags)
187
+ return "no sector"
188
+
189
+ def __eq__(self, other: Any) -> bool:
190
+ """
191
+ Check equality with another BBoxSector instance.
192
+
193
+ Args:
194
+ other (Any): The object to compare.
195
+
196
+ Returns:
197
+ bool: True if equal, False otherwise.
198
+ """
199
+ if isinstance(other, BBoxSector):
200
+ return self.flags == other.flags
201
+ return False
202
+
203
+ def __repr__(self) -> str:
204
+ """
205
+ Return the official string representation of the sector.
206
+
207
+ Returns:
208
+ str: The string representation.
209
+ """
210
+ return f"BBoxSector(flags={self.flags})"
211
+
212
+ def __hash__(self) -> int:
213
+ """
214
+ Make BBoxSector hashable (so you can use it in sets/dicts),
215
+ just like Swift’s Hashable conformance.
216
+ """
217
+ return hash(self.flags)
218
+
219
+ @classmethod
220
+ def named(cls, name: str) -> BBoxSector:
221
+ """
222
+ Look up a sector by its name (e.g. "alo", "b", "ru").
223
+ Returns BBoxSector.none if no match found.
224
+ """
225
+ for flag, desc in cls.debug_descriptions.items():
226
+ if desc == name:
227
+ return cls(flag)
228
+ return cls()
229
+
230
+ def __or__(self, other: Any) -> BBoxSector:
231
+ """
232
+ Define the behavior of the | operator.
233
+
234
+ Args:
235
+ other (BBoxSector or BBoxSectorFlags): The other sector or flag to combine.
236
+
237
+ Returns:
238
+ BBoxSector: A new BBoxSector instance with combined flags.
239
+ """
240
+ if isinstance(other, BBoxSector):
241
+ return BBoxSector(self.flags | other.flags)
242
+ elif isinstance(other, BBoxSectorFlags):
243
+ return BBoxSector(self.flags | other)
244
+ else:
245
+ return NotImplemented
246
+
247
+ def __ior__(self, other: Any) -> BBoxSector:
248
+ """
249
+ Define the behavior of the |= operator.
250
+
251
+ Args:
252
+ other (BBoxSector or BBoxSectorFlags): The other sector or flag to combine.
253
+
254
+ Returns:
255
+ BBoxSector: The updated BBoxSector instance.
256
+ """
257
+ if isinstance(other, BBoxSector):
258
+ self.flags |= other.flags
259
+ return self
260
+ elif isinstance(other, BBoxSectorFlags):
261
+ self.flags |= other
262
+ return self
263
+ else:
264
+ return NotImplemented
265
+
266
+ def __contains__(self, item: Any) -> bool:
267
+ """
268
+ Enable the 'in' operator to check for SpatialPredicate or BBoxSectorFlags.
269
+
270
+ Args:
271
+ item (Any): SpatialPredicate or BBoxSectorFlags to check.
272
+
273
+ Returns:
274
+ bool: True if the item is present, False otherwise.
275
+ """
276
+ if isinstance(item, SpatialPredicate):
277
+ # Map SpatialPredicate to BBoxSectorFlags
278
+ flag_map = {
279
+ SpatialPredicate.l: BBoxSectorFlags.l,
280
+ SpatialPredicate.r: BBoxSectorFlags.r,
281
+ SpatialPredicate.a: BBoxSectorFlags.a,
282
+ SpatialPredicate.b: BBoxSectorFlags.b,
283
+ SpatialPredicate.o: BBoxSectorFlags.o,
284
+ SpatialPredicate.u: BBoxSectorFlags.u,
285
+ SpatialPredicate.i: BBoxSectorFlags.i,
286
+ SpatialPredicate.al: BBoxSectorFlags.al,
287
+ SpatialPredicate.ar: BBoxSectorFlags.ar,
288
+ SpatialPredicate.bl: BBoxSectorFlags.bl,
289
+ SpatialPredicate.br: BBoxSectorFlags.br,
290
+ SpatialPredicate.ao: BBoxSectorFlags.ao,
291
+ SpatialPredicate.au: BBoxSectorFlags.au,
292
+ SpatialPredicate.bo: BBoxSectorFlags.bo,
293
+ SpatialPredicate.bu: BBoxSectorFlags.bu,
294
+ SpatialPredicate.lo: BBoxSectorFlags.lo,
295
+ SpatialPredicate.lu: BBoxSectorFlags.lu,
296
+ SpatialPredicate.ro: BBoxSectorFlags.ro,
297
+ SpatialPredicate.ru: BBoxSectorFlags.ru,
298
+ SpatialPredicate.alo: BBoxSectorFlags.alo,
299
+ SpatialPredicate.aro: BBoxSectorFlags.aro,
300
+ SpatialPredicate.blo: BBoxSectorFlags.blo,
301
+ SpatialPredicate.bro: BBoxSectorFlags.bro,
302
+ SpatialPredicate.alu: BBoxSectorFlags.alu,
303
+ SpatialPredicate.aru: BBoxSectorFlags.aru,
304
+ SpatialPredicate.blu: BBoxSectorFlags.blu,
305
+ SpatialPredicate.bru: BBoxSectorFlags.bru,
306
+ # Add more mappings as needed
307
+ }
308
+ flag = flag_map.get(item, None)
309
+ if flag is not None:
310
+ return self.contains_flag(flag)
311
+ elif isinstance(item, BBoxSectorFlags):
312
+ return self.contains_flag(item)
313
+ return False
314
+
315
+
316
+ # Example Usage
317
+
318
+ if __name__ == "__main__":
319
+ # Initialize with a predefined composite sector
320
+ sector = BBoxSector(BBoxSectorFlags.alo)
321
+ print(f"Sector: {sector}") # Output: Sector: a+l+o
322
+ print(f"Divergencies: {sector.divergencies()}") # Output: Divergencies: 3
323
+
324
+ # Create an empty sector and insert flags
325
+ combined_sector = BBoxSector()
326
+ combined_sector.insert(BBoxSectorFlags.a)
327
+ combined_sector.insert(BBoxSectorFlags.l)
328
+ combined_sector.insert(BBoxSectorFlags.o)
329
+ print(f"Combined Sector: {combined_sector}") # Output: Combined Sector: a+l+o
330
+ print(f"Divergencies: {combined_sector.divergencies()}") # Output: Divergencies: 3
331
+
332
+ # Initialize with the 'inside' sector
333
+ inner_sector = BBoxSector(BBoxSectorFlags.i)
334
+ print(f"Inner Sector: {inner_sector}") # Output: Inner Sector: i
335
+ print(f"Divergencies: {inner_sector.divergencies()}") # Output: Divergencies: 0
336
+
337
+ # Initialize with no sector
338
+ no_sector = BBoxSector()
339
+ print(f"No Sector: {no_sector}") # Output: No Sector: no sector
340
+ print(f"Divergencies: {no_sector.divergencies()}") # Output: Divergencies: 0
341
+
342
+ # Undefined composite sector example
343
+ undefined_sector = BBoxSector()
344
+ undefined_sector.insert(BBoxSectorFlags.a)
345
+ undefined_sector.insert(BBoxSectorFlags.b)
346
+ undefined_sector.insert(BBoxSectorFlags.l)
347
+ undefined_sector.insert(BBoxSectorFlags.r)
348
+ print(
349
+ f"Undefined Combined Sector: {undefined_sector}"
350
+ ) # Output: Undefined Combined Sector: a+b+l+r
351
+ print(f"Divergencies: {undefined_sector.divergencies()}") # Output: Divergencies: 4
@@ -0,0 +1,128 @@
1
+ """Optional OpenUSD scene export support.
2
+
3
+ Install ``spatial-reasoner[export]`` before importing this module.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ import random
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ try:
14
+ from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade, UsdUtils
15
+ except ModuleNotFoundError as exc: # pragma: no cover - depends on installation extras
16
+ raise ModuleNotFoundError(
17
+ "Scene export requires the optional dependency group; "
18
+ "install it with `pip install spatial-reasoner[export]`."
19
+ ) from exc
20
+
21
+ from .spatial_object import SpatialObject
22
+
23
+
24
+ class SceneExporter:
25
+ """Export spatial-object bounding boxes as USDZ scenes."""
26
+
27
+ def __init__(self, root_dir: str | Path):
28
+ """Create an exporter writing into ``root_dir``."""
29
+ self.root_dir = Path(root_dir)
30
+ self.root_dir.mkdir(parents=True, exist_ok=True)
31
+ self.usd_file_path: Path | None = None
32
+ self.temp_usd_path: Path | None = None
33
+ self.stage = None
34
+
35
+ def exportUSDZ(self, spatial_objects: list[SpatialObject], filename: str | Path) -> None:
36
+ """Export ``spatial_objects`` to a USDZ file.
37
+
38
+ The method name intentionally follows SRSwift's public API.
39
+ """
40
+ output_path = Path(filename)
41
+ if not output_path.is_absolute():
42
+ output_path = self.root_dir / output_path
43
+ output_path.parent.mkdir(parents=True, exist_ok=True)
44
+ output_path.unlink(missing_ok=True)
45
+ self.usd_file_path = output_path
46
+
47
+ with tempfile.NamedTemporaryFile(
48
+ prefix="spatial_reasoner_", suffix=".usd", dir=self.root_dir, delete=False
49
+ ) as temporary_file:
50
+ self.temp_usd_path = Path(temporary_file.name)
51
+ self.temp_usd_path.unlink(missing_ok=True)
52
+
53
+ try:
54
+ self.stage = Usd.Stage.CreateNew(str(self.temp_usd_path))
55
+ for obj in spatial_objects:
56
+ prim_path = self._create_obj_cube(obj)
57
+ self._create_bbox_cube(obj, prim_path)
58
+
59
+ self.stage.GetRootLayer().Save()
60
+ self.stage = None
61
+ created = UsdUtils.CreateNewUsdzPackage(
62
+ Sdf.AssetPath(str(self.temp_usd_path)), str(output_path)
63
+ )
64
+ if not created or not output_path.exists():
65
+ raise RuntimeError(f"OpenUSD could not create USDZ package: {output_path}")
66
+ finally:
67
+ self.stage = None
68
+ self.temp_usd_path.unlink(missing_ok=True)
69
+
70
+ def _create_obj_cube(self, spatial_object: SpatialObject) -> str:
71
+ """Add a spatial object and its material to the current USD stage."""
72
+ prim_path = f"/{spatial_object.id}"
73
+ xform = UsdGeom.Xform.Define(self.stage, prim_path)
74
+ xform.AddRotateYOp().Set(math.degrees(spatial_object.angle))
75
+ xform.AddTranslateOp().Set(
76
+ Gf.Vec3d(
77
+ spatial_object.position.x,
78
+ spatial_object.position.y,
79
+ spatial_object.position.z,
80
+ )
81
+ )
82
+ xform.AddScaleOp().Set(
83
+ Gf.Vec3f(spatial_object.width, spatial_object.height, spatial_object.depth)
84
+ )
85
+
86
+ cube = UsdGeom.Cube.Define(self.stage, f"{prim_path}/Cube")
87
+ material_path = f"{prim_path}/Material"
88
+ material = UsdShade.Material.Define(self.stage, material_path)
89
+ color = Gf.Vec3f(random.random(), random.random(), random.random())
90
+
91
+ shader = UsdShade.Shader.Define(self.stage, f"{material_path}/Shader")
92
+ shader.CreateIdAttr("UsdPreviewSurface")
93
+ shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(color)
94
+ shader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(
95
+ 1.0 - spatial_object.transparency
96
+ )
97
+ shader.CreateOutput("surface", Sdf.ValueTypeNames.Token)
98
+ material.CreateSurfaceOutput().ConnectToSource(shader.GetOutput("surface"))
99
+ UsdShade.MaterialBindingAPI(xform.GetPrim()).Bind(material)
100
+
101
+ prim = cube.GetPrim()
102
+ prim.CreateAttribute("user:transparency", Sdf.ValueTypeNames.Float).Set(
103
+ spatial_object.transparency
104
+ )
105
+ prim.CreateAttribute("user:color", Sdf.ValueTypeNames.Color3f).Set(color)
106
+ return prim_path
107
+
108
+ def _create_bbox_cube(
109
+ self,
110
+ obj: SpatialObject,
111
+ parent_path: str,
112
+ color: tuple[float, float, float] = (0.0, 0.0, 0.0),
113
+ ) -> None:
114
+ """Add the object's nearby-radius sphere below ``parent_path``."""
115
+ bbox_prim_path = f"{parent_path}/BBox"
116
+ UsdGeom.Xform.Define(self.stage, bbox_prim_path)
117
+ sphere_path = f"{bbox_prim_path}/NearbySphere"
118
+ sphere = UsdGeom.Sphere.Define(self.stage, sphere_path)
119
+ sphere.GetPrim().CreateAttribute("radius", Sdf.ValueTypeNames.Float).Set(obj.nearbyRadius())
120
+
121
+ material = UsdShade.Material.Define(self.stage, f"{sphere_path}/Material")
122
+ shader = UsdShade.Shader.Define(self.stage, f"{sphere_path}/Material/Shader")
123
+ shader.CreateIdAttr("UsdPreviewSurface")
124
+ shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(*color))
125
+ shader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(0.3)
126
+ shader.CreateOutput("surface", Sdf.ValueTypeNames.Token)
127
+ material.CreateSurfaceOutput().ConnectToSource(shader.GetOutput("surface"))
128
+ UsdShade.MaterialBindingAPI(sphere.GetPrim()).Bind(material)