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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: microecs
3
- Version: 0.4.1
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 _Field obj
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 `_Field`) | 1.8 | **27× faster** |
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 (`_Field` or per-pool) run at 1–2 ns/entity — **27–52×
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 `_Field`: a view over the matching pools that behaves like one contiguous-like `(N, *e)`
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 `_Field` from one `world.query(...)` into an op on another.
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 _Field obj
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 `_Field`) | 1.8 | **27× faster** |
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 (`_Field` or per-pool) run at 1–2 ns/entity — **27–52×
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 `_Field`: a view over the matching pools that behaves like one contiguous-like `(N, *e)`
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 `_Field` from one `world.query(...)` into an op on another.
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 _Field(np.lib.mixins.NDArrayOperatorsMixin):
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, _Field):
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 _Field so each subpart is called independently.
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: _Field = fn(*pool_args, **kwargs)
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 _Field(results)
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], _Field), out
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, _Field):
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 _Field([part[key] for part in self.parts])
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 _Field(data[name] or [np.empty((0, *self._field_shapes[name]), self._field_dtypes[name])])
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.1
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 _Field obj
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 `_Field`) | 1.8 | **27× faster** |
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 (`_Field` or per-pool) run at 1–2 ns/entity — **27–52×
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 `_Field`: a view over the matching pools that behaves like one contiguous-like `(N, *e)`
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 `_Field` from one `world.query(...)` into an op on another.
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:
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  from setuptools import setup, find_packages
4
4
 
5
5
  NAME = "microecs"
6
- VERSION = "0.4.1"
6
+ VERSION = "0.4.2"
7
7
  DESCRIPTION = "MicroECS: Minimal Entity Component System (ECS) in python and numpy"
8
8
  URL = "https://gitlab.com/meehai/microecs"
9
9
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes