microecs.pool

pool.py - A pool of entities of the same type (same list of components). Basically a dynamic array with numpy

 1"""pool.py - A pool of entities of the same type (same list of components). Basically a dynamic array with numpy"""
 2import numpy as np
 3from .utils import Shape, logger
 4
 5# Note: if Pool gets new fields, add them here! Otherwise the user code may overwrite them.
 6_POOL_INTERNAL_ATTRS = {"size", "capacity", "fields", "shapes", "dtypes", "data", "fields_set"}
 7
 8class Pool:
 9    """
10    Pool is a dynamic array of entities data given a list of fields, shapes and dtypes (from traits).
11    Pool has no concept of entity ids.
12    """
13    INITIAL_CAPACITY = 100
14
15    def __init__(self, fields: list[str], shapes: list[Shape], dtypes: list[np.dtype]):
16        if not len(fields) == len(shapes) == len(dtypes):
17            raise ValueError(f"Lens not match: {len(fields)} - {len(shapes)} - {len(dtypes)}")
18        if (fields_set := set(fields)) & POOL_RESERVED_NAMES:
19            raise ValueError(f"One of {fields=} in {POOL_RESERVED_NAMES}")
20
21        self.fields = fields
22        self.shapes = shapes
23        self.dtypes = dtypes
24        self.fields_set = fields_set # useful for fast checking (e.g. in entity)
25
26        self.data: dict[str, np.ndarray] = {} # the actual data is stored in dict of dynamic arrrays, one per field
27        self.size = 0
28        self.capacity = Pool.INITIAL_CAPACITY
29        for _field, shape, dtype in zip(fields, shapes, dtypes):
30            self.data[_field] = np.empty(shape=(self.capacity, *shape), dtype=dtype)
31
32    def add_entity(self, entity_data: dict[str, np.ndarray]) -> int:
33        """Adds an entity to the pool. All the fields required by this pool must be provided in entity_data"""
34        if self.size == self.capacity:
35            self._realloc(self.capacity * 2)
36            logger.debug(f"Capacity extended from {self.capacity // 2} to {self.capacity}")
37
38        for _field, field_shape, field_dtype in zip(self.fields, self.shapes, self.dtypes):
39            new_item = entity_data[_field] # checked in World._get_entity_pool(entity).
40            if (dtp := new_item.dtype) != field_dtype or new_item.shape != field_shape:
41                raise ValueError(f"Field {_field}. Dtype: {dtp} {field_dtype=}. Shape: {new_item.shape} {field_shape=}")
42            self.data[_field][self.size] = new_item
43        self.size += 1
44        return self.size - 1
45
46    def remove_entity(self, entity_index: int):
47        """removes an entity given an index (NOT ID) inside this pool"""
48        if not 0 <= entity_index < self.size:
49            raise IndexError(f"OOB: {entity_index=}, {self.size=}")
50        for _field in self.fields:
51            self.data[_field][entity_index] = self.data[_field][self.size - 1]
52        self.size -= 1
53
54        if self.size < self.capacity / 4 and self.capacity > Pool.INITIAL_CAPACITY:
55            self._realloc(self.capacity // 2)
56
57    def pop_entity(self, entity_index: int) -> dict[str, np.ndarray]:
58        """pops an entity given an index (NOT ID) inside this pool and returns the data"""
59        res = {_field: self.data[_field][entity_index].copy() for _field in self.fields}
60        self.remove_entity(entity_index)
61        return res
62
63    def _realloc(self, new_capacity: int):
64        for _field, shape, dtype in zip(self.fields, self.shapes, self.dtypes):
65            old_data = self.data[_field]
66            self.data[_field] = np.empty(shape=(new_capacity, *shape), dtype=dtype)
67            self.data[_field][0:self.size] = old_data[0:self.size]
68        self.capacity = new_capacity
69
70    def __getattr__(self, name):
71        if (data := self.__dict__.get("data")) is not None and name in data:
72            return data[name][0: self.size]
73        raise AttributeError(name)
74
75    def __setattr__(self, name, value):
76        if (data := self.__dict__.get("data")) is not None and name in data:
77            raise ValueError(f"Cannot explicitly set anything to Pool. Use `pool.component[:] = ...` ({name=})")
78        super().__setattr__(name, value)
79
80    def __len__(self):
81        return self.size
82
83POOL_RESERVED_NAMES = _POOL_INTERNAL_ATTRS | {n for n in vars(Pool) if not n.startswith("__")}
class Pool:
 9class Pool:
10    """
11    Pool is a dynamic array of entities data given a list of fields, shapes and dtypes (from traits).
12    Pool has no concept of entity ids.
13    """
14    INITIAL_CAPACITY = 100
15
16    def __init__(self, fields: list[str], shapes: list[Shape], dtypes: list[np.dtype]):
17        if not len(fields) == len(shapes) == len(dtypes):
18            raise ValueError(f"Lens not match: {len(fields)} - {len(shapes)} - {len(dtypes)}")
19        if (fields_set := set(fields)) & POOL_RESERVED_NAMES:
20            raise ValueError(f"One of {fields=} in {POOL_RESERVED_NAMES}")
21
22        self.fields = fields
23        self.shapes = shapes
24        self.dtypes = dtypes
25        self.fields_set = fields_set # useful for fast checking (e.g. in entity)
26
27        self.data: dict[str, np.ndarray] = {} # the actual data is stored in dict of dynamic arrrays, one per field
28        self.size = 0
29        self.capacity = Pool.INITIAL_CAPACITY
30        for _field, shape, dtype in zip(fields, shapes, dtypes):
31            self.data[_field] = np.empty(shape=(self.capacity, *shape), dtype=dtype)
32
33    def add_entity(self, entity_data: dict[str, np.ndarray]) -> int:
34        """Adds an entity to the pool. All the fields required by this pool must be provided in entity_data"""
35        if self.size == self.capacity:
36            self._realloc(self.capacity * 2)
37            logger.debug(f"Capacity extended from {self.capacity // 2} to {self.capacity}")
38
39        for _field, field_shape, field_dtype in zip(self.fields, self.shapes, self.dtypes):
40            new_item = entity_data[_field] # checked in World._get_entity_pool(entity).
41            if (dtp := new_item.dtype) != field_dtype or new_item.shape != field_shape:
42                raise ValueError(f"Field {_field}. Dtype: {dtp} {field_dtype=}. Shape: {new_item.shape} {field_shape=}")
43            self.data[_field][self.size] = new_item
44        self.size += 1
45        return self.size - 1
46
47    def remove_entity(self, entity_index: int):
48        """removes an entity given an index (NOT ID) inside this pool"""
49        if not 0 <= entity_index < self.size:
50            raise IndexError(f"OOB: {entity_index=}, {self.size=}")
51        for _field in self.fields:
52            self.data[_field][entity_index] = self.data[_field][self.size - 1]
53        self.size -= 1
54
55        if self.size < self.capacity / 4 and self.capacity > Pool.INITIAL_CAPACITY:
56            self._realloc(self.capacity // 2)
57
58    def pop_entity(self, entity_index: int) -> dict[str, np.ndarray]:
59        """pops an entity given an index (NOT ID) inside this pool and returns the data"""
60        res = {_field: self.data[_field][entity_index].copy() for _field in self.fields}
61        self.remove_entity(entity_index)
62        return res
63
64    def _realloc(self, new_capacity: int):
65        for _field, shape, dtype in zip(self.fields, self.shapes, self.dtypes):
66            old_data = self.data[_field]
67            self.data[_field] = np.empty(shape=(new_capacity, *shape), dtype=dtype)
68            self.data[_field][0:self.size] = old_data[0:self.size]
69        self.capacity = new_capacity
70
71    def __getattr__(self, name):
72        if (data := self.__dict__.get("data")) is not None and name in data:
73            return data[name][0: self.size]
74        raise AttributeError(name)
75
76    def __setattr__(self, name, value):
77        if (data := self.__dict__.get("data")) is not None and name in data:
78            raise ValueError(f"Cannot explicitly set anything to Pool. Use `pool.component[:] = ...` ({name=})")
79        super().__setattr__(name, value)
80
81    def __len__(self):
82        return self.size

Pool is a dynamic array of entities data given a list of fields, shapes and dtypes (from traits). Pool has no concept of entity ids.

Pool( fields: list[str], shapes: list[tuple[int, ...]], dtypes: list[numpy.dtype])
16    def __init__(self, fields: list[str], shapes: list[Shape], dtypes: list[np.dtype]):
17        if not len(fields) == len(shapes) == len(dtypes):
18            raise ValueError(f"Lens not match: {len(fields)} - {len(shapes)} - {len(dtypes)}")
19        if (fields_set := set(fields)) & POOL_RESERVED_NAMES:
20            raise ValueError(f"One of {fields=} in {POOL_RESERVED_NAMES}")
21
22        self.fields = fields
23        self.shapes = shapes
24        self.dtypes = dtypes
25        self.fields_set = fields_set # useful for fast checking (e.g. in entity)
26
27        self.data: dict[str, np.ndarray] = {} # the actual data is stored in dict of dynamic arrrays, one per field
28        self.size = 0
29        self.capacity = Pool.INITIAL_CAPACITY
30        for _field, shape, dtype in zip(fields, shapes, dtypes):
31            self.data[_field] = np.empty(shape=(self.capacity, *shape), dtype=dtype)
INITIAL_CAPACITY = 100
fields
shapes
dtypes
fields_set
data: dict[str, numpy.ndarray]
size
capacity
def add_entity(self, entity_data: dict[str, numpy.ndarray]) -> int:
33    def add_entity(self, entity_data: dict[str, np.ndarray]) -> int:
34        """Adds an entity to the pool. All the fields required by this pool must be provided in entity_data"""
35        if self.size == self.capacity:
36            self._realloc(self.capacity * 2)
37            logger.debug(f"Capacity extended from {self.capacity // 2} to {self.capacity}")
38
39        for _field, field_shape, field_dtype in zip(self.fields, self.shapes, self.dtypes):
40            new_item = entity_data[_field] # checked in World._get_entity_pool(entity).
41            if (dtp := new_item.dtype) != field_dtype or new_item.shape != field_shape:
42                raise ValueError(f"Field {_field}. Dtype: {dtp} {field_dtype=}. Shape: {new_item.shape} {field_shape=}")
43            self.data[_field][self.size] = new_item
44        self.size += 1
45        return self.size - 1

Adds an entity to the pool. All the fields required by this pool must be provided in entity_data

def remove_entity(self, entity_index: int):
47    def remove_entity(self, entity_index: int):
48        """removes an entity given an index (NOT ID) inside this pool"""
49        if not 0 <= entity_index < self.size:
50            raise IndexError(f"OOB: {entity_index=}, {self.size=}")
51        for _field in self.fields:
52            self.data[_field][entity_index] = self.data[_field][self.size - 1]
53        self.size -= 1
54
55        if self.size < self.capacity / 4 and self.capacity > Pool.INITIAL_CAPACITY:
56            self._realloc(self.capacity // 2)

removes an entity given an index (NOT ID) inside this pool

def pop_entity(self, entity_index: int) -> dict[str, numpy.ndarray]:
58    def pop_entity(self, entity_index: int) -> dict[str, np.ndarray]:
59        """pops an entity given an index (NOT ID) inside this pool and returns the data"""
60        res = {_field: self.data[_field][entity_index].copy() for _field in self.fields}
61        self.remove_entity(entity_index)
62        return res

pops an entity given an index (NOT ID) inside this pool and returns the data

POOL_RESERVED_NAMES = {'shapes', 'add_entity', 'data', 'remove_entity', 'dtypes', 'size', '_realloc', 'INITIAL_CAPACITY', 'fields', 'capacity', 'pop_entity', 'fields_set'}