memory engine occlusion#83
Conversation
…ngine Implements the abstraction described in #8 so storage backends can be reused across engines. Adds StoreType, refines IStore/IMemoryEngine, and ships MemoryStore/SqliteStore backends. Wires the memory engine into the internal context so ctx.ictx.memory is available after gamms.create_context(). Extends IGraphEngine with add_polygon/get_polygon/get_polygons/remove_polygon which lazily back onto a polygon store from the memory engine. This is the foundation for the occlusion work in #82.
Polygons are extracted from OSM via extract_osm_polygons / populate_polygons_from_osm and registered with the graph engine's polygon store. Each polygon is treated as a vertical prism: the footprint is extruded between base and base+height, creating trapezoidal lateral faces. When height is missing the OSM tags are consulted (height, building:height, building:levels) and otherwise defaults to a two-storey building (~6 m) so the API stays usable without metadata. Adds the occlusion module with segment_blocked_by_polygon / segment_blocked_by_polygons that test 3D ray segments against the prism faces and the polygon top (relevant for drones flying over). Adds OCCLUDED_* SensorType entries and matching sensor variants (OccludedMapSensor, OccludedAgentSensor, OccludedAerialSensor, OccludedAerialAgentSensor) that filter their parent sensors' results by testing each visibility ray against the polygon store. Ground sensors use a configurable observer_height (eye level) for the ray origin, while aerial sensors use the agent's 3D position.
memory_engine_test.py covers MemoryStore CRUD, SQLite-backed store persistence (round-trip via load_store), error paths for duplicate names/keys, and the polygon store wired onto GraphEngine. occlusion_test.py covers the geometric primitives (low/high rays, top-face intersection, multi-polygon iteration, degenerate input) and end-to-end OCCLUDED_RANGE / OCCLUDED_AGENT_RANGE / OCCLUDED_AERIAL sensor behaviour against polygons registered with the graph engine.
Rewrites MemoryStore and SqliteStore against the schema-based IStore without leaking graph-specific concerns into the storage layer. Both graph backends keep their own optimizations and consume the store via a small generic extension API plus an honest backend handle. Polygons now live in the same engine-typed store as the graph and gain a bbox-indexed range query; occluded sensors use it to pre-filter polygon candidates. Sensor module split by behavioral group; occluded sensors collapse to one general class per axis with ARC/RANGE wired as factory presets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Realign stores, graphs, sensors with new IStore abstraction.
* Replace per-layer surfaces with single render surface + per-artist RGBA cache * Reuse graph cache across camera moves via offset blit and zoom stretch * Substitute short edges and skip sub-pixel edges and nodes in graph rendering * Clean up docstring formatting in default_drawers * Move cached artist rendering behind a RenderManager callback * Merge get_culling_bounds and world_to_screen_scale into get_viewport * Skip edge.linestring access for short or cached edges in graph rendering * Move constants to init * Revert graph render cache to lazy edge_line_points dict * Apply skip/short edge optimization to render_map_sensor * Avoid strip re-render and bilinear cost on zoom-out * Clarify cached artist handler docstring * render agent optimization * get_edges in a box * edge caching in waiting_simulation * edge finding optimizations * Fix projection updates on window resize * Remove will_draw from artist interfaces and use ArtistType for static cache routing * Replace per-artist pixel caches with per-layer surfaces * Guard render projection against zero screen size * Change layer cache artist_names from set to tuple * cache mismatch bug fix * Remove unused _static_layers from RenderManager * Clamp render dimensions on resize --------- Co-authored-by: Jinmin Lee <90895797+jinmin111@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mehul Sinha <mehulsinha73@gmail.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Mehul Sinha <72033137+mehulsinha73@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new structured MemoryEngine (in-memory + SQLite backends), adds occlusion-aware sensors and occlusion geometry helpers, and reworks the Pygame visualization pipeline (viewport exposure, static-layer caching, and obstacle rendering from OSM-derived faces).
Changes:
- Add a structured MemoryEngine API with MemoryStore/SqliteStore implementations and corresponding tests.
- Add occlusion geometry + occlusion-aware sensor variants, plus end-to-end occlusion tests.
- Rework visualization rendering/caching and add obstacle-face extraction + rendering utilities.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/occlusion_test.py | Adds geometry + end-to-end occluded sensor tests (currently assumes polygon-store APIs). |
| tests/memory_engine_test.py | Adds store contract tests for MemoryStore/SqliteStore + polygon-store tests (currently mismatched to implementation). |
| gamms/VisualizationEngine/render_manager.py | Adds cached-layer handler hook, viewport bounds helpers, and projection/bounds recalculation helpers. |
| gamms/VisualizationEngine/pygame_engine.py | Reworks rendering to a unified render surface with per-layer caching and exposes get_viewport(). |
| gamms/VisualizationEngine/no_engine.py | Adds get_viewport() stub returning None. |
| gamms/VisualizationEngine/default_drawers.py | Adds viewport-aware pixel-threshold skipping and obstacle rendering from obstacle faces. |
| gamms/VisualizationEngine/artist.py | Removes will-draw flag plumbing from Artist implementation. |
| gamms/VisualizationEngine/init.py | Adds visualization constants for caching and pixel thresholds. |
| gamms/typing/visualization_engine.py | Adds get_viewport() to the visualization engine interface; simplifies color typing. |
| gamms/typing/sensor_engine.py | Adds occlusion sensor enum values. |
| gamms/typing/memory_engine.py | Replaces legacy “artifact store” style interface with structured store/map API. |
| gamms/typing/graph_engine.py | Adds obstacle-face API to graph engine typing. |
| gamms/typing/artist.py | Removes will-draw methods from the artist typing interface. |
| gamms/typing/init.py | Re-exports new memory engine types and obstacle face type. |
| gamms/SensorEngine/sensors_occluded.py | Adds occlusion-aware sensor implementations (currently depends on polygon-store APIs). |
| gamms/SensorEngine/sensors_basic.py | Extracts basic ground sensors into a dedicated module. |
| gamms/SensorEngine/sensors_aerial.py | Extracts aerial sensors + quaternion helpers into a dedicated module. |
| gamms/SensorEngine/sensor_engine.py | Refactors factory to use extracted sensor modules and adds occluded sensor creation paths. |
| gamms/SensorEngine/occlusion.py | Adds prism occlusion geometry routines. |
| gamms/osm.py | Adds OSM obstacle face extraction helpers and obstacle-from-place/xml functions. |
| gamms/osm_constants.py | Adds obstacle tag filters, height estimates, and color maps. |
| gamms/MemoryEngine/store.py | Implements MemoryStore and SqliteStore with schema + CBOR-encoded complex fields. |
| gamms/MemoryEngine/memory_engine.py | Implements MemoryEngine store registry and lifecycle. |
| gamms/MemoryEngine/init.py | Exposes MemoryEngine public API. |
| gamms/internal_context.py | Makes internal engines optional and terminates them conditionally. |
| gamms/GraphEngine/graph_engine.py | Refactors graph storage onto MemoryEngine stores and adds obstacle-face storage/query. |
| gamms/init.py | Wires MemoryEngine into context creation via InternalContext. |
| examples/base/config.py | Changes example defaults (NO_VIS, different location). |
Comments suppressed due to low confidence (6)
tests/memory_engine_test.py:63
- This test expects
ValueErroron duplicate primary-key insert, butMemoryStore.insert_data/SqliteStore.insert_datacurrently raiseKeyErrorfor duplicate keys. Update the test expectation (or change the stores) to ensure a consistent error contract.
def test_duplicate_key_raises(self):
self.store.create_map('m', {'id': int, 'value': int}, 'id')
self.store.insert_data('m', {'id': 1, 'value': 10})
with self.assertRaises(ValueError):
self.store.insert_data('m', {'id': 1, 'value': 20})
tests/memory_engine_test.py:124
bulk_insert,count, andcreate_indexare used here but are not implemented onIStore,MemoryStore, orSqliteStorein this branch. This will fail at runtime. Either implement these methods (and add them to the typing interface) or remove/replace these tests with the APIs that actually exist.
def test_bulk_insert_and_count(self):
self.store.create_map('m', {'id': int, 'value': int}, 'id')
self.store.bulk_insert('m', [
{'id': 1, 'value': 1},
{'id': 2, 'value': 2},
{'id': 3, 'value': 3},
])
self.assertEqual(self.store.count('m'), 3)
self.assertEqual(set(self.store.query_keys('m')), {1, 2, 3})
def test_create_index(self):
self.store.create_map('m', {'id': int, 'x': float, 'y': float}, 'id')
self.store.create_index('m', ('x', 'y'))
tests/memory_engine_test.py:135
MemoryStoreTestcallsself.store.get_map('m'), butMemoryStoredoes not exposeget_mapin this branch (and it is not part ofIStore). This test will fail unless the method is added or the test is updated to assert via the publicIStoreAPIs.
def test_get_map_backend_handle(self):
self.store.create_map('m', {'id': int, 'name': str}, 'id')
self.store.insert_data('m', {'id': 1, 'name': 'foo'})
backing = self.store.get_map('m')
self.assertEqual(backing[1], {'id': 1, 'name': 'foo'})
tests/memory_engine_test.py:223
- This suite assumes a polygon API on
ctx.graph(add_polygon,get_polygons,get_polygon,remove_polygon), but the currentIGraphEngine/GraphEngineinterfaces in this branch only define obstacle-face operations. Unless polygon support is added back, these tests will fail withAttributeError.
def test_add_get_remove_polygon(self):
coords = [(0, 0), (10, 0), (10, 10), (0, 10)]
self.ctx.graph.add_polygon(0, coords)
self.assertEqual(list(self.ctx.graph.get_polygons()), [0])
record = self.ctx.graph.get_polygon(0)
self.assertEqual(record['id'], 0)
self.assertEqual(record['height'], 6.0)
self.assertEqual(record['category'], 'building')
self.ctx.graph.add_polygon(1, coords, height=12.5, category='foliage')
self.assertEqual(set(self.ctx.graph.get_polygons()), {0, 1})
self.assertEqual(self.ctx.graph.get_polygon(1)['height'], 12.5)
self.assertEqual(self.ctx.graph.get_polygon(1)['category'], 'foliage')
self.ctx.graph.remove_polygon(0)
self.assertEqual(list(self.ctx.graph.get_polygons()), [1])
tests/memory_engine_test.py:76
MemoryStore/SqliteStoreraiseIndexErrorwhen a map name doesn’t exist (via_require_map/_require_schema), but this test expectsKeyError. Update the test to match the current store contract (or change the stores to raiseKeyError) so missing-map behavior is consistent.
def test_unknown_map_raises(self):
with self.assertRaises(KeyError):
self.store.insert_data('nope', {'id': 1})
with self.assertRaises(KeyError):
self.store.get_data('nope', 1)
with self.assertRaises(KeyError):
list(self.store.query_keys('nope'))
tests/memory_engine_test.py:83
SqliteStore.delete_mapcurrently raisesIndexErrorwhen the map is missing, while this test expectsKeyError. Decide on a single exception type for “unknown map” across store backends and tests (and align the typing docs accordingly).
def test_delete_map(self):
self.store.create_map('m', {'id': int}, 'id')
self.store.delete_map('m')
self.assertNotIn('m', self.store.list_maps())
with self.assertRaises(KeyError):
self.store.delete_map('m')
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def load(self, path: str) -> IGraph: | ||
| """ | ||
| Loads a graph from a file. | ||
| """ | ||
| self._graph.load(path) | ||
| return self.graph |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| AGENT_ARC = 7 | ||
| AERIAL = 8 | ||
| AERIAL_AGENT = 9 | ||
| OCCLUDED_MAP = 10 |
There was a problem hiding this comment.
Only keep occluded_map occluded_agent occluded_aerial occluded_aerial_agent
| return faces | ||
|
|
||
|
|
||
| def _is_blocked( |
There was a problem hiding this comment.
Do not assume trapezoid. Do it for general quadrilateral
| super().sense(node_id) | ||
| current_node = self.ctx.graph.graph.get_node(node_id) | ||
| origin = self._origin(current_node) | ||
| faces = _faces_in_range(self.ctx, origin[0], origin[1], self.range) |
There was a problem hiding this comment.
This is a standard api call. Given initial output from map sensor, create a numpy array and do blocking checks on that. Plus, batch the faces based on how the graph engine iterator yields
| super().sense(node_id) | ||
| current_node = self.ctx.graph.graph.get_node(node_id) | ||
| origin = self._origin(current_node) | ||
| faces = _faces_in_range(self.ctx, origin[0], origin[1], self.range) |
There was a problem hiding this comment.
Do not enumerate all faces. Even if one face blocks, it is blocked
… individual file(s) in groups of commits so that a specific update gets its own commit
….com/GAMMSim/gamms into claude/memory-engine-occlusion-4T55m
No description provided.