microecs 0.4.1__tar.gz → 0.4.2__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.
- {microecs-0.4.1 → microecs-0.4.2}/PKG-INFO +6 -6
- {microecs-0.4.1 → microecs-0.4.2}/README.md +5 -5
- {microecs-0.4.1 → microecs-0.4.2}/microecs/__init__.py +2 -2
- {microecs-0.4.1 → microecs-0.4.2}/microecs/query_result.py +11 -10
- {microecs-0.4.1 → microecs-0.4.2}/microecs.egg-info/PKG-INFO +6 -6
- {microecs-0.4.1 → microecs-0.4.2}/setup.py +1 -1
- {microecs-0.4.1 → microecs-0.4.2}/LICENSE.TXT +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs/component.py +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs/entity.py +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs/pool.py +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs/utils.py +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs/world.py +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs.egg-info/SOURCES.txt +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs.egg-info/dependency_links.txt +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs.egg-info/requires.txt +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/microecs.egg-info/top_level.txt +0 -0
- {microecs-0.4.1 → microecs-0.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: microecs
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: MicroECS: Minimal Entity Component System (ECS) in python and numpy
|
|
5
5
|
Home-page: https://gitlab.com/meehai/microecs
|
|
6
6
|
License: MIT
|
|
@@ -76,7 +76,7 @@ class MotionSystem:
|
|
|
76
76
|
def __call__(self, world: World):
|
|
77
77
|
qr = world.query(HasPosition, HasVelocity) # 'exclude' is optional
|
|
78
78
|
qr.position[:] = qr.position + qr.velocity * DT # writes back to all the underlying pools using numpy's rules
|
|
79
|
-
# Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the
|
|
79
|
+
# Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the Field obj
|
|
80
80
|
for pool in qr.pool_list:
|
|
81
81
|
pool.position[:] = pool.position + pool.velocity * DT
|
|
82
82
|
|
|
@@ -124,7 +124,7 @@ Benchmark: we run the same physics step `pos += vel*dt` over N=100k entities spl
|
|
|
124
124
|
| pattern | ns/entity | vs OOP-scalar |
|
|
125
125
|
|---|---:|---:|
|
|
126
126
|
| `micro-ecs-pool-vectorized` — `for pool: pool.f[:] = pool.f + …` | 0.9 | **52× faster** |
|
|
127
|
-
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `
|
|
127
|
+
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `Field`) | 1.8 | **27× faster** |
|
|
128
128
|
| **`oop-scalar`** — `for o: o.x += o.vx*dt` (python floats) | 48 | 1× (baseline) |
|
|
129
129
|
| `oop-numpy` — objects holding `(2,)` numpy arrays | 605 | 13× slower |
|
|
130
130
|
| `micro-ecs-zip-rows` — `for p, v in zip(qr.pos, qr.vel)` | 744 | 15× slower |
|
|
@@ -133,7 +133,7 @@ Benchmark: we run the same physics step `pos += vel*dt` over N=100k entities spl
|
|
|
133
133
|
|
|
134
134
|
Three things to take from it:
|
|
135
135
|
|
|
136
|
-
1. **Vectorized wins big.** Batched ops (`
|
|
136
|
+
1. **Vectorized wins big.** Batched ops (`Field` or per-pool) run at 1–2 ns/entity — **27–52×
|
|
137
137
|
faster** than the *fastest* OOP loop. Same for data-parallel branches: an `np.where` clamp or
|
|
138
138
|
bounce is ~34× faster than a per-entity `if`.
|
|
139
139
|
2. **Per-entity loops are a cliff, not a tie.** Every per-entity microecs path is **15–30× slower**
|
|
@@ -152,7 +152,7 @@ microecs ~15× — use them there. microecs is the right tool for **vectorizable
|
|
|
152
152
|
|
|
153
153
|
## How much are `Pool` and `QueryResult` numpy-like and corner cases
|
|
154
154
|
|
|
155
|
-
Given `qr=world.query(A, B)`, then `qr.position` returns a `
|
|
155
|
+
Given `qr=world.query(A, B)`, then `qr.position` returns a `Field`: a view over the matching pools that behaves like one contiguous-like `(N, *e)`
|
|
156
156
|
numpy array (e.g. `(N, 2)` for a `(2,)` field). It applies each op **per pool** and stitches result back.
|
|
157
157
|
|
|
158
158
|
That covers elementwise math and ufuncs (e.g. `np.where`, `np.linalg.norm(..., axis=1)` etc.), broadcasting (every operand shape numpy accepts, and it raises on the ones numpy rejects). See `test/unit/test_field_numpy_parity.py` for a whole set of operations comparing both.
|
|
@@ -169,7 +169,7 @@ Edge cases worth knowing:
|
|
|
169
169
|
`qr.f.numpy()` first. A reduction that collapses the entity axis is rejected when its length no
|
|
170
170
|
longer matches the pool's row count.
|
|
171
171
|
- **Operands must come from the same query.** Alignment is per-pool, not by flat index, so don't
|
|
172
|
-
mix a `
|
|
172
|
+
mix a `Field` from one `world.query(...)` into an op on another.
|
|
173
173
|
- **Reserved field names.** A field is read back as `qr.<field>` and `entity.<field>`, so it may not
|
|
174
174
|
collide with an attribute or method of `QueryResult` or `Entity` — `World(...)` rejects such a
|
|
175
175
|
component at construction instead of silently shadowing it. The reserved set (source of truth:
|
|
@@ -64,7 +64,7 @@ class MotionSystem:
|
|
|
64
64
|
def __call__(self, world: World):
|
|
65
65
|
qr = world.query(HasPosition, HasVelocity) # 'exclude' is optional
|
|
66
66
|
qr.position[:] = qr.position + qr.velocity * DT # writes back to all the underlying pools using numpy's rules
|
|
67
|
-
# Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the
|
|
67
|
+
# Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the Field obj
|
|
68
68
|
for pool in qr.pool_list:
|
|
69
69
|
pool.position[:] = pool.position + pool.velocity * DT
|
|
70
70
|
|
|
@@ -112,7 +112,7 @@ Benchmark: we run the same physics step `pos += vel*dt` over N=100k entities spl
|
|
|
112
112
|
| pattern | ns/entity | vs OOP-scalar |
|
|
113
113
|
|---|---:|---:|
|
|
114
114
|
| `micro-ecs-pool-vectorized` — `for pool: pool.f[:] = pool.f + …` | 0.9 | **52× faster** |
|
|
115
|
-
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `
|
|
115
|
+
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `Field`) | 1.8 | **27× faster** |
|
|
116
116
|
| **`oop-scalar`** — `for o: o.x += o.vx*dt` (python floats) | 48 | 1× (baseline) |
|
|
117
117
|
| `oop-numpy` — objects holding `(2,)` numpy arrays | 605 | 13× slower |
|
|
118
118
|
| `micro-ecs-zip-rows` — `for p, v in zip(qr.pos, qr.vel)` | 744 | 15× slower |
|
|
@@ -121,7 +121,7 @@ Benchmark: we run the same physics step `pos += vel*dt` over N=100k entities spl
|
|
|
121
121
|
|
|
122
122
|
Three things to take from it:
|
|
123
123
|
|
|
124
|
-
1. **Vectorized wins big.** Batched ops (`
|
|
124
|
+
1. **Vectorized wins big.** Batched ops (`Field` or per-pool) run at 1–2 ns/entity — **27–52×
|
|
125
125
|
faster** than the *fastest* OOP loop. Same for data-parallel branches: an `np.where` clamp or
|
|
126
126
|
bounce is ~34× faster than a per-entity `if`.
|
|
127
127
|
2. **Per-entity loops are a cliff, not a tie.** Every per-entity microecs path is **15–30× slower**
|
|
@@ -140,7 +140,7 @@ microecs ~15× — use them there. microecs is the right tool for **vectorizable
|
|
|
140
140
|
|
|
141
141
|
## How much are `Pool` and `QueryResult` numpy-like and corner cases
|
|
142
142
|
|
|
143
|
-
Given `qr=world.query(A, B)`, then `qr.position` returns a `
|
|
143
|
+
Given `qr=world.query(A, B)`, then `qr.position` returns a `Field`: a view over the matching pools that behaves like one contiguous-like `(N, *e)`
|
|
144
144
|
numpy array (e.g. `(N, 2)` for a `(2,)` field). It applies each op **per pool** and stitches result back.
|
|
145
145
|
|
|
146
146
|
That covers elementwise math and ufuncs (e.g. `np.where`, `np.linalg.norm(..., axis=1)` etc.), broadcasting (every operand shape numpy accepts, and it raises on the ones numpy rejects). See `test/unit/test_field_numpy_parity.py` for a whole set of operations comparing both.
|
|
@@ -157,7 +157,7 @@ Edge cases worth knowing:
|
|
|
157
157
|
`qr.f.numpy()` first. A reduction that collapses the entity axis is rejected when its length no
|
|
158
158
|
longer matches the pool's row count.
|
|
159
159
|
- **Operands must come from the same query.** Alignment is per-pool, not by flat index, so don't
|
|
160
|
-
mix a `
|
|
160
|
+
mix a `Field` from one `world.query(...)` into an op on another.
|
|
161
161
|
- **Reserved field names.** A field is read back as `qr.<field>` and `entity.<field>`, so it may not
|
|
162
162
|
collide with an attribute or method of `QueryResult` or `Entity` — `World(...)` rejects such a
|
|
163
163
|
component at construction instead of silently shadowing it. The reserved set (source of truth:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""init file"""
|
|
2
2
|
|
|
3
3
|
from .world import World
|
|
4
|
-
from .query_result import QueryResult
|
|
4
|
+
from .query_result import QueryResult, Field
|
|
5
5
|
from .pool import Pool
|
|
6
6
|
from .entity import Entity
|
|
7
7
|
from .component import Component
|
|
8
8
|
from .utils import EntityId
|
|
9
9
|
|
|
10
|
-
__all__ = ["World", "QueryResult", "Pool", "Component", "Entity", "EntityId"]
|
|
10
|
+
__all__ = ["World", "QueryResult", "Field", "Pool", "Component", "Entity", "EntityId"]
|
|
@@ -8,7 +8,8 @@ from .pool import Pool
|
|
|
8
8
|
# Note: if QueryResult gets new fields, add them here! Otherwise the user code may overwrite them e.g. qr._len=xxx
|
|
9
9
|
QUERY_RESULT_INTERNAL_ATTRS = {"pool_list", "entity_ids", "fields", "_field_shapes", "_field_dtypes", "_data", "_len"}
|
|
10
10
|
|
|
11
|
-
class
|
|
11
|
+
class Field(np.lib.mixins.NDArrayOperatorsMixin):
|
|
12
|
+
"""Field is a single field (column) from a QueryResult object obtained from world.query(...)"""
|
|
12
13
|
def __init__(self, parts: list[np.ndarray]):
|
|
13
14
|
self.parts = parts
|
|
14
15
|
self._lens = [len(p) for p in self.parts]
|
|
@@ -22,7 +23,7 @@ class _Field(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
22
23
|
return np.concatenate(self.parts) if len(self.parts) != 1 else self.parts[0]
|
|
23
24
|
|
|
24
25
|
def _chunk(self, x: T, i: int) -> T:
|
|
25
|
-
if isinstance(x,
|
|
26
|
+
if isinstance(x, Field):
|
|
26
27
|
return x.parts[i]
|
|
27
28
|
if isinstance(x, np.ndarray) and x.ndim == len(self.shape) and x.shape[0] == self.len:
|
|
28
29
|
return x[self._bounds[i]:self._bounds[i + 1]]
|
|
@@ -30,16 +31,16 @@ class _Field(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
30
31
|
|
|
31
32
|
def _apply_fn_on_parts(self, fn: Callable, op_args: list, **kwargs):
|
|
32
33
|
# op args can be 1 element (-qr.velocity), 2 elements (qr.position * 0.1), 3 elements (np.where(a, b, c)), etc.
|
|
33
|
-
# all of them must be chunked based on how many we have in this
|
|
34
|
+
# all of them must be chunked based on how many we have in this Field so each subpart is called independently.
|
|
34
35
|
results = []
|
|
35
36
|
for i, part in enumerate(self.parts):
|
|
36
37
|
pool_args = [self._chunk(x, i) for x in op_args]
|
|
37
|
-
part_result:
|
|
38
|
+
part_result: Field = fn(*pool_args, **kwargs)
|
|
38
39
|
# we expect f(arr(N, ...)) -> arr(N, ...) where N = number of items in the pool
|
|
39
40
|
# for e.g. np.linalg.norm(velocity, axis=1) should do (N, 2) -> (N, 1) so the first axis is preserved
|
|
40
41
|
assert len(part_result) == part.shape[0], f"Result: {part_result.shape} vs {part.shape}"
|
|
41
42
|
results.append(part_result)
|
|
42
|
-
return
|
|
43
|
+
return Field(results)
|
|
43
44
|
|
|
44
45
|
def __array_ufunc__(self, ufunc, method, *inputs, out=None, **kwargs):
|
|
45
46
|
"""wrapper for elementwise (python) primitives, e.g. qr.position[:] += 1"""
|
|
@@ -48,7 +49,7 @@ class _Field(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
48
49
|
if out is None:
|
|
49
50
|
return self._apply_fn_on_parts(ufunc, inputs, **kwargs)
|
|
50
51
|
|
|
51
|
-
assert len(out) == 1 and isinstance(out[0],
|
|
52
|
+
assert len(out) == 1 and isinstance(out[0], Field), out
|
|
52
53
|
for i in range(len(self.parts)):
|
|
53
54
|
pool_args = [self._chunk(x, i) for x in inputs]
|
|
54
55
|
ufunc(*pool_args, out=out[0].parts[i], **kwargs)
|
|
@@ -64,7 +65,7 @@ class _Field(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
64
65
|
not (isinstance(key, tuple) and key and key[0] == slice(None))):
|
|
65
66
|
raise TypeError("entity-axis assignment crosses pools; use [:] or [:, k] or [i][...]")
|
|
66
67
|
|
|
67
|
-
if isinstance(value,
|
|
68
|
+
if isinstance(value, Field):
|
|
68
69
|
for i, part in enumerate(self.parts):
|
|
69
70
|
part[key] = value.parts[i]
|
|
70
71
|
return
|
|
@@ -78,10 +79,10 @@ class _Field(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
78
79
|
|
|
79
80
|
def __getitem__(self, key):
|
|
80
81
|
if key is Ellipsis or (isinstance(key, tuple) and key and (key[0] is Ellipsis or key[0] == slice(None))):
|
|
81
|
-
return
|
|
82
|
+
return Field([part[key] for part in self.parts])
|
|
82
83
|
|
|
83
84
|
raise TypeError(("Only batch updates are supported, e.g. `qr.attr[:]=xxx` or `qr.attr[:, k]=xxx`. "
|
|
84
|
-
"Use .numpy() for a proper array. For entity-level ops use `world.get_entity(eid).attr=xxx"))
|
|
85
|
+
"Use .numpy() for a proper array. For entity-level ops use `world.get_entity(eid).attr=xxx`"))
|
|
85
86
|
|
|
86
87
|
def __iter__(self):
|
|
87
88
|
for part in self.parts:
|
|
@@ -108,7 +109,7 @@ class QueryResult:
|
|
|
108
109
|
def __getattr__(self, name):
|
|
109
110
|
if (data := self.__dict__.get("_data")) is not None and name in data:
|
|
110
111
|
# the 'or' part is in case no pools match the query and we want qr.position[:] += 1 still to work (noop)
|
|
111
|
-
return
|
|
112
|
+
return Field(data[name] or [np.empty((0, *self._field_shapes[name]), self._field_dtypes[name])])
|
|
112
113
|
raise AttributeError(f"'{name}' not part of {self.fields}")
|
|
113
114
|
|
|
114
115
|
def __setattr__(self, name, value):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: microecs
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: MicroECS: Minimal Entity Component System (ECS) in python and numpy
|
|
5
5
|
Home-page: https://gitlab.com/meehai/microecs
|
|
6
6
|
License: MIT
|
|
@@ -76,7 +76,7 @@ class MotionSystem:
|
|
|
76
76
|
def __call__(self, world: World):
|
|
77
77
|
qr = world.query(HasPosition, HasVelocity) # 'exclude' is optional
|
|
78
78
|
qr.position[:] = qr.position + qr.velocity * DT # writes back to all the underlying pools using numpy's rules
|
|
79
|
-
# Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the
|
|
79
|
+
# Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the Field obj
|
|
80
80
|
for pool in qr.pool_list:
|
|
81
81
|
pool.position[:] = pool.position + pool.velocity * DT
|
|
82
82
|
|
|
@@ -124,7 +124,7 @@ Benchmark: we run the same physics step `pos += vel*dt` over N=100k entities spl
|
|
|
124
124
|
| pattern | ns/entity | vs OOP-scalar |
|
|
125
125
|
|---|---:|---:|
|
|
126
126
|
| `micro-ecs-pool-vectorized` — `for pool: pool.f[:] = pool.f + …` | 0.9 | **52× faster** |
|
|
127
|
-
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `
|
|
127
|
+
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `Field`) | 1.8 | **27× faster** |
|
|
128
128
|
| **`oop-scalar`** — `for o: o.x += o.vx*dt` (python floats) | 48 | 1× (baseline) |
|
|
129
129
|
| `oop-numpy` — objects holding `(2,)` numpy arrays | 605 | 13× slower |
|
|
130
130
|
| `micro-ecs-zip-rows` — `for p, v in zip(qr.pos, qr.vel)` | 744 | 15× slower |
|
|
@@ -133,7 +133,7 @@ Benchmark: we run the same physics step `pos += vel*dt` over N=100k entities spl
|
|
|
133
133
|
|
|
134
134
|
Three things to take from it:
|
|
135
135
|
|
|
136
|
-
1. **Vectorized wins big.** Batched ops (`
|
|
136
|
+
1. **Vectorized wins big.** Batched ops (`Field` or per-pool) run at 1–2 ns/entity — **27–52×
|
|
137
137
|
faster** than the *fastest* OOP loop. Same for data-parallel branches: an `np.where` clamp or
|
|
138
138
|
bounce is ~34× faster than a per-entity `if`.
|
|
139
139
|
2. **Per-entity loops are a cliff, not a tie.** Every per-entity microecs path is **15–30× slower**
|
|
@@ -152,7 +152,7 @@ microecs ~15× — use them there. microecs is the right tool for **vectorizable
|
|
|
152
152
|
|
|
153
153
|
## How much are `Pool` and `QueryResult` numpy-like and corner cases
|
|
154
154
|
|
|
155
|
-
Given `qr=world.query(A, B)`, then `qr.position` returns a `
|
|
155
|
+
Given `qr=world.query(A, B)`, then `qr.position` returns a `Field`: a view over the matching pools that behaves like one contiguous-like `(N, *e)`
|
|
156
156
|
numpy array (e.g. `(N, 2)` for a `(2,)` field). It applies each op **per pool** and stitches result back.
|
|
157
157
|
|
|
158
158
|
That covers elementwise math and ufuncs (e.g. `np.where`, `np.linalg.norm(..., axis=1)` etc.), broadcasting (every operand shape numpy accepts, and it raises on the ones numpy rejects). See `test/unit/test_field_numpy_parity.py` for a whole set of operations comparing both.
|
|
@@ -169,7 +169,7 @@ Edge cases worth knowing:
|
|
|
169
169
|
`qr.f.numpy()` first. A reduction that collapses the entity axis is rejected when its length no
|
|
170
170
|
longer matches the pool's row count.
|
|
171
171
|
- **Operands must come from the same query.** Alignment is per-pool, not by flat index, so don't
|
|
172
|
-
mix a `
|
|
172
|
+
mix a `Field` from one `world.query(...)` into an op on another.
|
|
173
173
|
- **Reserved field names.** A field is read back as `qr.<field>` and `entity.<field>`, so it may not
|
|
174
174
|
collide with an attribute or method of `QueryResult` or `Entity` — `World(...)` rejects such a
|
|
175
175
|
component at construction instead of silently shadowing it. The reserved set (source of truth:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|