microecs.query_result

query_result.py - A list of pools seen as a contiguous view. Implements array interface to look like numpy

 1"""query_result.py - A list of pools seen as a contiguous view. Implements array interface to look like numpy"""
 2import numpy as np
 3
 4from .utils import Shape, EntityId
 5from .pool import Pool
 6from .qr_field import QRField
 7
 8# Note: if QueryResult gets new fields, add them here! Otherwise the user code may overwrite them e.g. qr._data=xxx
 9_QR_INTERNAL_ATTRS = {"pool_list", "fields", "_field_shapes", "_field_dtypes", "_data",
10                      "_cache", "_entity_ids", "_len", "_pool_ids"}
11
12class _QRArray(np.ndarray):
13    """small shim array so we don't instantiate QRField which is more expensive (contiguous view for >=2 pools)"""
14    def numpy(self) -> np.ndarray:
15        """for compatibility with QRField.numpy()"""
16        return np.asarray(self)
17
18    @property
19    def parts(self) -> list[np.ndarray]:
20        """for compatibility with QRField.parts"""
21        return [np.asarray(self)]
22
23class QueryResult:
24    """A query result containing entities. Fields (e.g. qr.position) implement array interface to look like numpy"""
25    def __init__(self, pool_list: list[Pool], field_shapes: dict[str, Shape], field_dtypes: dict[str, np.dtype],
26                 pool_ids: dict[Pool, list[EntityId]]):
27        self.pool_list = pool_list
28        self.fields = list(field_shapes)
29        self._field_shapes = field_shapes
30        self._field_dtypes = field_dtypes
31        self._pool_ids = pool_ids
32
33        self._data: dict[str, list[np.ndarray]] = {f: [p.data[f][0:len(p)] for p in pool_list] for f in field_shapes}
34        self._cache: dict[str, QRField | _QRArray] = {}
35        self._entity_ids: np.ndarray | None = None
36        self._len: int | None = None
37
38    @property
39    def entity_ids(self) -> np.ndarray:
40        """The entity ids of this query result"""
41        if self._entity_ids is None:
42            self._entity_ids = np.array(sum((self._pool_ids[p] for p in self.pool_list), []), dtype="int64")
43        return self._entity_ids
44
45    def __getattr__(self, name):
46        data: dict[str, list[np.ndarray]]
47        # .get and not self._data: on an instance built without __init__ (copy/deepcopy/pickle.loads probe
48        # hasattr(__setstate__)), _data is MISSING -- and self._data would re-enter __getattr__ -> RecursionError
49        if name not in (data := self.__dict__.get("_data", {})):
50            raise AttributeError(f"'{name}' not part of {self.__dict__.get('fields')}")
51
52        if name not in self._cache:
53            if len(parts := data[name]) in (0, 1): # optimized path for a single pool -> return an actual np array
54                # the if/else part is in case no pools match the query so we create a (0, k) array for that field.
55                arr = parts[0] if parts else np.empty((0, *self._field_shapes[name]), self._field_dtypes[name])
56                self._cache[name] = arr.view(_QRArray)
57            else:
58                self._cache[name] = QRField(parts)
59
60        return self._cache[name]
61
62    def __setattr__(self, name, value):
63        if name in _QR_INTERNAL_ATTRS:
64            super().__setattr__(name, value)
65            return
66
67        # When is _data None and __setattr__ called? On any instance built without __init__ (copy/deepcopy/pickle.loads)
68        if name not in self.__dict__.get("_data", {}): # note: self._data bounces to getattr (recursion).
69            raise AttributeError(f"Attribute '{name}' not in query result fields: {self.__dict__.get('fields')}")
70
71        # (!46) Weird optimization trick with [:]: `qr.f += x`: the ufunc already wrote in place; `col[:] = col` skipped
72        if (col := getattr(self, name)) is value:
73            return
74
75        col[:] = value
76
77    def __iter__(self):
78        raise TypeError(("QueryResult is not iterable. Use `qr.field = ..` that applies to all items at once.\n"
79                         "Common pattern: `for e in world.query(..): e.attr = X` -> `qr=world.query(..); qr.attr = X`"))
80
81    def __len__(self):
82        if self._len is None:
83            self._len = sum(len(p) for p in self.pool_list)
84        return self._len
85
86    def __repr__(self):
87        return (f"[QueryResult]\n- Entities: {len(self.entity_ids)} (pools: {len(self.pool_list)})"
88                f"\n- Fields: {self.fields}"
89                f"\n- Shapes: {list(self._field_shapes.values())}\n- Dtypes: {list(self._field_dtypes.values())}")
90
91QUERY_RESULT_RESERVED_NAMES = _QR_INTERNAL_ATTRS | {n for n in vars(QueryResult) if not n.startswith("__")}
class QueryResult:
24class QueryResult:
25    """A query result containing entities. Fields (e.g. qr.position) implement array interface to look like numpy"""
26    def __init__(self, pool_list: list[Pool], field_shapes: dict[str, Shape], field_dtypes: dict[str, np.dtype],
27                 pool_ids: dict[Pool, list[EntityId]]):
28        self.pool_list = pool_list
29        self.fields = list(field_shapes)
30        self._field_shapes = field_shapes
31        self._field_dtypes = field_dtypes
32        self._pool_ids = pool_ids
33
34        self._data: dict[str, list[np.ndarray]] = {f: [p.data[f][0:len(p)] for p in pool_list] for f in field_shapes}
35        self._cache: dict[str, QRField | _QRArray] = {}
36        self._entity_ids: np.ndarray | None = None
37        self._len: int | None = None
38
39    @property
40    def entity_ids(self) -> np.ndarray:
41        """The entity ids of this query result"""
42        if self._entity_ids is None:
43            self._entity_ids = np.array(sum((self._pool_ids[p] for p in self.pool_list), []), dtype="int64")
44        return self._entity_ids
45
46    def __getattr__(self, name):
47        data: dict[str, list[np.ndarray]]
48        # .get and not self._data: on an instance built without __init__ (copy/deepcopy/pickle.loads probe
49        # hasattr(__setstate__)), _data is MISSING -- and self._data would re-enter __getattr__ -> RecursionError
50        if name not in (data := self.__dict__.get("_data", {})):
51            raise AttributeError(f"'{name}' not part of {self.__dict__.get('fields')}")
52
53        if name not in self._cache:
54            if len(parts := data[name]) in (0, 1): # optimized path for a single pool -> return an actual np array
55                # the if/else part is in case no pools match the query so we create a (0, k) array for that field.
56                arr = parts[0] if parts else np.empty((0, *self._field_shapes[name]), self._field_dtypes[name])
57                self._cache[name] = arr.view(_QRArray)
58            else:
59                self._cache[name] = QRField(parts)
60
61        return self._cache[name]
62
63    def __setattr__(self, name, value):
64        if name in _QR_INTERNAL_ATTRS:
65            super().__setattr__(name, value)
66            return
67
68        # When is _data None and __setattr__ called? On any instance built without __init__ (copy/deepcopy/pickle.loads)
69        if name not in self.__dict__.get("_data", {}): # note: self._data bounces to getattr (recursion).
70            raise AttributeError(f"Attribute '{name}' not in query result fields: {self.__dict__.get('fields')}")
71
72        # (!46) Weird optimization trick with [:]: `qr.f += x`: the ufunc already wrote in place; `col[:] = col` skipped
73        if (col := getattr(self, name)) is value:
74            return
75
76        col[:] = value
77
78    def __iter__(self):
79        raise TypeError(("QueryResult is not iterable. Use `qr.field = ..` that applies to all items at once.\n"
80                         "Common pattern: `for e in world.query(..): e.attr = X` -> `qr=world.query(..); qr.attr = X`"))
81
82    def __len__(self):
83        if self._len is None:
84            self._len = sum(len(p) for p in self.pool_list)
85        return self._len
86
87    def __repr__(self):
88        return (f"[QueryResult]\n- Entities: {len(self.entity_ids)} (pools: {len(self.pool_list)})"
89                f"\n- Fields: {self.fields}"
90                f"\n- Shapes: {list(self._field_shapes.values())}\n- Dtypes: {list(self._field_dtypes.values())}")

A query result containing entities. Fields (e.g. qr.position) implement array interface to look like numpy

QueryResult( pool_list: list[microecs.pool.Pool], field_shapes: dict[str, tuple[int, ...]], field_dtypes: dict[str, numpy.dtype], pool_ids: dict[microecs.pool.Pool, list[int]])
26    def __init__(self, pool_list: list[Pool], field_shapes: dict[str, Shape], field_dtypes: dict[str, np.dtype],
27                 pool_ids: dict[Pool, list[EntityId]]):
28        self.pool_list = pool_list
29        self.fields = list(field_shapes)
30        self._field_shapes = field_shapes
31        self._field_dtypes = field_dtypes
32        self._pool_ids = pool_ids
33
34        self._data: dict[str, list[np.ndarray]] = {f: [p.data[f][0:len(p)] for p in pool_list] for f in field_shapes}
35        self._cache: dict[str, QRField | _QRArray] = {}
36        self._entity_ids: np.ndarray | None = None
37        self._len: int | None = None
pool_list
fields
entity_ids: numpy.ndarray
39    @property
40    def entity_ids(self) -> np.ndarray:
41        """The entity ids of this query result"""
42        if self._entity_ids is None:
43            self._entity_ids = np.array(sum((self._pool_ids[p] for p in self.pool_list), []), dtype="int64")
44        return self._entity_ids

The entity ids of this query result

QUERY_RESULT_RESERVED_NAMES = {'_entity_ids', 'entity_ids', '_pool_ids', 'pool_list', '_data', '_field_shapes', '_cache', 'fields', '_field_dtypes', '_len'}