Skip to content

Migrate RasterLayer to mesa.discrete_space backend#307

Open
falloficarus22 wants to merge 9 commits intomesa:mainfrom
falloficarus22:remove-mesa-space
Open

Migrate RasterLayer to mesa.discrete_space backend#307
falloficarus22 wants to merge 9 commits intomesa:mainfrom
falloficarus22:remove-mesa-space

Conversation

@falloficarus22
Copy link
Contributor

@falloficarus22 falloficarus22 commented Feb 20, 2026

Summary

mesa-geo failed to import with current Mesa because mesa.space is no longer available in Mesa 4 development versions. This blocked runtime usage immediately (import-time failure).
This PR removes the legacy dependency and migrates RasterLayer internals to Mesa’s current cell-space architecture.

Bug / Issue

Closes #306

  • Expected:
    • import mesa_geo works with current Mesa.
    • Raster APIs (RasterLayer, neighborhood methods, visualization paths) continue to behave as before.
  • Actual:
    • raster_layers.py imported Coordinate and accept_tuple_argument from mesa.space.
    • importing mesa_geo raised ModuleNotFoundError.

Implementation

  • Removed legacy mesa.space dependency from raster_layers.py.
  • Migrated raster cell backend to mesa.discrete_space:
    • Cell now subclasses mesa.discrete_space.cell.Cell.
    • RasterLayer now initializes an internal OrthogonalMooreGrid and maps grid cells to existing self.cells[x][y] structure.
  • Reimplemented neighborhood retrieval on top of discrete-space neighborhood APIs and preserved existing moore / Von Neumann behavior.
  • Kept tuple-friendly cell-list behavior by providing a local accept_tuple_argument helper.
  • Kept backward-compatible raster default-attribute overwrite behavior while making naming deterministic.
  • Updated brittle test assertions in test_RasterLayer.py to avoid hardcoded runtime-dependent attribute names.
  • Fixed lint issue by removing unused unpacked variables in CRS transform code.

Testing

  • Importing mesa_geo is now successful.

Additional Notes

  • Existing warnings from third-party dependencies (e.g., traitlets/ipyleaflet deprecations, GeoJupyterViz deprecation warning) were unchanged by this PR.
  • Public raster-facing behavior was preserved where possible; internal architecture now aligns with Mesa’s modern discrete-space model.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced coordinate handling with improved type support for raster operations.
  • Bug Fixes

    • Improved raster layer grid synchronization to ensure cell positions remain consistent.
  • Refactor

    • Cell class now leverages updated base class for improved compatibility with grid-based operations.
    • RasterLayer internals modernized with grid-driven cell management.

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19100934-78ac-44e4-afb0-3b4bb7df0e13

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Cell class now inherits from MesaDiscreteCell instead of Agent. RasterLayer refactored to use OrthogonalMooreGrid for cell storage and management. Constructor signature changed, type aliases added, and tests updated for dynamic attribute naming.

Changes

Cohort / File(s) Summary
Cell class and type system
mesa_geo/raster_layers.py
Cell now inherits from MesaDiscreteCell instead of Agent. Constructor signature changed from __init__(self, model, pos=None, indices=None, *, rowcol=None, xy=None) to __init__(self, coordinate, position=None, capacity=None, random=None). Added type aliases Coordinate, FloatCoordinate, TypeVar F, and accept_tuple_argument decorator function.
RasterLayer grid infrastructure
mesa_geo/raster_layers.py
Added private _grid (OrthogonalMooreGrid) for cell storage and _default_attribute_name attribute. New methods _initialize_cells() and _sync_cell_xy() handle grid-based cell initialization and coordinate synchronization. Refactored get_neighborhood() to derive coordinates from grid neighborhoods.
Test updates
tests/test_RasterLayer.py
Updated test to support dynamic attribute naming. Retrieves actual generated attribute name from layer's attribute set instead of using hard-coded "val".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 hops through refactored code
No more from mesa.space we flee,
MesaDiscreteCell sets us free,
Grid-based cells in order true,
Coordinates sync old with new,
Our hoppy refactor complete! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Migrate RasterLayer to mesa.discrete_space backend' directly summarizes the main change in the PR, which involves migrating the RasterLayer implementation from using mesa.space to mesa.discrete_space.
Linked Issues check ✅ Passed The PR successfully addresses all coding objectives from issue #306: removed mesa.space imports, migrated Cell to subclass MesaDiscreteCell, replaced legacy symbols with local implementations, and preserved public API behavior.
Out of Scope Changes check ✅ Passed All changes are directly related to the migration objective. The Cell class refactoring, RasterLayer grid integration, helper function addition, and test updates all support the core goal of eliminating mesa.space dependency.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Feb 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.26%. Comparing base (65a3751) to head (87c2fe4).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #307      +/-   ##
==========================================
+ Coverage   77.67%   78.26%   +0.59%     
==========================================
  Files          10       10              
  Lines         954     1003      +49     
  Branches      146      159      +13     
==========================================
+ Hits          741      785      +44     
- Misses        174      177       +3     
- Partials       39       41       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@falloficarus22
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
mesa_geo/raster_layers.py (3)

186-217: Cell now carries two position attributes: coordinate (inherited from MesaDiscreteCell) and _pos (legacy).

Both are initialized to the same (x, y) value, but nothing prevents them from diverging if someone modifies one without the other. The pos setter (line 224) only updates _pos, leaving the inherited coordinate stale. Consider either:

  • Making pos a read-only alias for coordinate, or
  • Documenting that coordinate is the canonical source and pos is deprecated.

This avoids a class of subtle bugs where code reads one but the other was modified.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mesa_geo/raster_layers.py` around lines 186 - 217, Cell currently maintains
both inherited attribute coordinate and legacy _pos and the pos setter only
updates _pos, allowing divergence; change pos to be a read-only alias of the
canonical coordinate (return self.coordinate) and remove the pos setter, or if
you must keep a setter, make it update both MesaDiscreteCell.coordinate and
self._pos in sync (and mark pos as deprecated in the docstring), updating any
use sites accordingly; modify the Cell class (pos property/getter/setter) to
implement one of these two approaches and add a short deprecation note
referencing Cell.pos, Cell._pos, and the inherited coordinate.

35-47: Missing functools.wraps on the wrapper function.

Without @functools.wraps(wrapped_function), the decorated functions (iter_cell_list_contents, get_cell_list_contents) lose their __name__, __doc__, and __module__ attributes, which hurts debuggability and introspection.

Proposed fix
+import functools
+
 def accept_tuple_argument(wrapped_function: F) -> F:
     """Allow passing a single coordinate tuple to list-based cell APIs."""
 
+    `@functools.wraps`(wrapped_function)
     def wrapper(grid_instance, positions, *args, **kwargs) -> Any:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mesa_geo/raster_layers.py` around lines 35 - 47, The decorator
accept_tuple_argument currently defines inner wrapper without preserving
metadata; update it to apply functools.wraps(wrapped_function) to wrapper so
decorated functions like iter_cell_list_contents and get_cell_list_contents
retain their __name__, __doc__, and __module__; import functools if missing and
place `@functools.wraps`(wrapped_function) directly above the wrapper definition
(keeping the existing logic that converts a single (x,y) tuple into a list).

471-480: Auto-generated attribute naming scheme is unconventional.

The progression "attribute_0""attribute_0_1""attribute_0_2" is a bit confusing because "attribute_0_1" looks like a hierarchical sub-attribute. A simpler monotonic scheme like "attribute_0", "attribute_1", "attribute_2" would be more intuitive and align with common conventions.

Suggested naming approach
-    _default_attribute_name: str
+    _default_attribute_counter: int

     ...
-    self._default_attribute_name = "attribute_0"
+    self._default_attribute_counter = 0

     ...
         def _default_attr_name() -> str:
-            base = self._default_attribute_name
-            if base not in self._attributes:
-                return base
-            suffix = 1
-            candidate = f"{base}_{suffix}"
-            while candidate in self._attributes:
-                suffix += 1
-                candidate = f"{base}_{suffix}"
-            return candidate
+            while True:
+                candidate = f"attribute_{self._default_attribute_counter}"
+                self._default_attribute_counter += 1
+                if candidate not in self._attributes:
+                    return candidate
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mesa_geo/raster_layers.py` around lines 471 - 480, The current
_default_attr_name generates names like "attribute_0_1" because it appends
suffixes to the whole self._default_attribute_name; change it to derive a root
by stripping a trailing numeric suffix from self._default_attribute_name (e.g.
using a regex to get root and optional number) and then generate monotonic names
root_0, root_1, root_2… by starting suffix at 0 (or 1 if you prefer) and
incrementing until a candidate not in self._attributes is found; update the
_default_attr_name function to use self._default_attribute_name (root) and
self._attributes when building candidate names.
tests/test_RasterLayer.py (1)

87-93: generated_attr is misleading — the attribute name "val" is explicitly provided, not auto-generated.

Since attr_name="val" is passed on line 75, the attribute is always "val". Using next(iter(...)) to retrieve it obscures this and makes the test harder to read. The previous direct-access pattern (e.g., self.raster_layer.cells[0][1].val) was clearer here.

If the intent is to test auto-generated names, a separate test case with attr_name=None would be more appropriate (and you already have test_apply_raster_single_band_attr_name_none for that).

Simplification
     def test_apply_raster(self):
         raster_data = np.array([[[1, 2], [3, 4], [5, 6]]])
         self.raster_layer.apply_raster(raster_data, attr_name="val")
-        generated_attr = next(iter(self.raster_layer.attributes))
-        self.assertEqual(getattr(self.raster_layer.cells[0][1], generated_attr), 3)
-        self.assertEqual(self.raster_layer.attributes, {generated_attr})
+        self.assertEqual(self.raster_layer.cells[0][1].val, 3)
+        self.assertEqual(self.raster_layer.attributes, {"val"})

         self.raster_layer.apply_raster(raster_data, attr_name="elevation")
         self.assertEqual(self.raster_layer.cells[0][1].elevation, 3)
-        self.assertEqual(self.raster_layer.attributes, {generated_attr, "elevation"})
+        self.assertEqual(self.raster_layer.attributes, {"val", "elevation"})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_RasterLayer.py` around lines 87 - 93, The test uses a misleading
variable generated_attr to access an attribute that is explicitly created with
attr_name="val"; update the assertions in the block that calls
self.raster_layer.apply_raster(..., attr_name="val") to directly reference the
attribute name "val" (e.g., access self.raster_layer.cells[0][1].val and assert
self.raster_layer.attributes includes "val") instead of using next(iter(...))
and generated_attr; if you need to test auto-generated names keep or create a
separate test that calls RasterLayer.apply_raster with attr_name=None (you
already have test_apply_raster_single_band_attr_name_none) so this case remains
explicit and readable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@mesa_geo/raster_layers.py`:
- Around line 186-217: Cell currently maintains both inherited attribute
coordinate and legacy _pos and the pos setter only updates _pos, allowing
divergence; change pos to be a read-only alias of the canonical coordinate
(return self.coordinate) and remove the pos setter, or if you must keep a
setter, make it update both MesaDiscreteCell.coordinate and self._pos in sync
(and mark pos as deprecated in the docstring), updating any use sites
accordingly; modify the Cell class (pos property/getter/setter) to implement one
of these two approaches and add a short deprecation note referencing Cell.pos,
Cell._pos, and the inherited coordinate.
- Around line 35-47: The decorator accept_tuple_argument currently defines inner
wrapper without preserving metadata; update it to apply
functools.wraps(wrapped_function) to wrapper so decorated functions like
iter_cell_list_contents and get_cell_list_contents retain their __name__,
__doc__, and __module__; import functools if missing and place
`@functools.wraps`(wrapped_function) directly above the wrapper definition
(keeping the existing logic that converts a single (x,y) tuple into a list).
- Around line 471-480: The current _default_attr_name generates names like
"attribute_0_1" because it appends suffixes to the whole
self._default_attribute_name; change it to derive a root by stripping a trailing
numeric suffix from self._default_attribute_name (e.g. using a regex to get root
and optional number) and then generate monotonic names root_0, root_1, root_2…
by starting suffix at 0 (or 1 if you prefer) and incrementing until a candidate
not in self._attributes is found; update the _default_attr_name function to use
self._default_attribute_name (root) and self._attributes when building candidate
names.

In `@tests/test_RasterLayer.py`:
- Around line 87-93: The test uses a misleading variable generated_attr to
access an attribute that is explicitly created with attr_name="val"; update the
assertions in the block that calls self.raster_layer.apply_raster(...,
attr_name="val") to directly reference the attribute name "val" (e.g., access
self.raster_layer.cells[0][1].val and assert self.raster_layer.attributes
includes "val") instead of using next(iter(...)) and generated_attr; if you need
to test auto-generated names keep or create a separate test that calls
RasterLayer.apply_raster with attr_name=None (you already have
test_apply_raster_single_band_attr_name_none) so this case remains explicit and
readable.

@wang-boyu wang-boyu added the dependency Release notes label label Feb 25, 2026
@wang-boyu
Copy link
Member

Thanks for the fix.

Removing mesa.space is part of upcoming Mesa 4 release. Currently Mesa-Geo has mesa~=3.0 dependency so we should be safe for the time being.

Regarding this PR, do you mind narrowing it down to just fixing the mesa.space import break from #306? Now it includes refactor work on RasterLayer that is a major part of the upcoming GSoC project, and it is kept open for ideas and discussions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependency Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove mesa.space dependency from mesa-geo

2 participants