Skip to content

Add optional names to GeoSpace layers with get_layer(name) accessor - #339

Open
Tejasv-Singh wants to merge 1 commit into
mesa:mainfrom
Tejasv-Singh:feat/named-layers
Open

Add optional names to GeoSpace layers with get_layer(name) accessor#339
Tejasv-Singh wants to merge 1 commit into
mesa:mainfrom
Tejasv-Singh:feat/named-layers

Conversation

@Tejasv-Singh

Copy link
Copy Markdown
Contributor

Description

Closes #324.

Adds optional name-based registration and retrieval of layers in GeoSpace, so multi-layer models no longer need index-based workarounds to reach a specific layer.

Usage

space = GeoSpace(crs="epsg:4326")
space.add_layer(elevation_layer, name="elevation")
space.add_layer(landuse_layer, name="landuse")

layer = space.get_layer("elevation")

Previously the only way to reach a specific layer was by position, which is what the GIS examples do today:

@property
def raster_layer(self):
    return self.layers[0]

That pattern appears in UrbanGrowth, Rainfall, and Population.

What changed

  • Optional name: str | None = None parameter added to:
    • RasterBase.__init__, RasterLayer.__init__, ImageLayer.__init__
    • RasterLayer.from_file, ImageLayer.from_file
    • GeoSpace.add_layer(layer, name=None)
  • New GeoSpace.get_layer(name), which returns the registered layer or raises KeyError listing the available names.
  • Names are preserved across GeoSpace.to_crs(), in both the inplace=True and inplace=False paths.
  • Internal GeoSpace._name_for_layer(layer) reverse lookup, used by the upcoming raster portrayal work.

Why

GeoSpace.layers is a plain list with no name-based lookup, so any model with more than one layer has to track indices or hand-write an accessor property. The three GIS examples above each do this independently.

This also unblocks a name-keyed raster portrayal API in a follow-up PR.

Backward compatibility

  • GeoSpace.layers still returns a plain list[ImageLayer | RasterLayer | gpd.GeoDataFrame], so indexing, len(), and iteration are unchanged.
  • space.add_layer(layer) with no name behaves exactly as before and registers nothing.
  • Every new parameter is appended to its signature with a None default, so positional calls are unaffected.
  • No existing test was modified. 81 existing tests pass unchanged.

Design notes

GeoDataFrame layers are never mutated. The registry lives on GeoSpace._layer_names. Vector layers are raw GeoDataFrame objects, and stamping .name on one would be routed by pandas NDFrame.__setattr__ to self["name"] = value, silently overwriting a column named "name". That column is common in GIS data, so only RasterBase instances get the attribute set.

Registration is explicit. Only layers added with an explicit name= enter the registry. An auto-derived name would collide for two files sharing a basename, turning a previously working add_layer call into a ValueError.

from_file stays subclass-safe. The name is assigned after construction rather than forwarded into cls(...). Forwarding it would break any RasterLayer subclass that overrides __init__ with the existing signature and no **kwargs.

Validation happens before mutation. Duplicate-name and duplicate-layer checks run before the in-place CRS conversion, so a rejected add_layer leaves the caller's layer untouched rather than reprojecting it on the way to raising.

Reverse lookup is type-guarded. _name_for_layer falls back to getattr(layer, "name", None), but a GeoDataFrame with a "name" column returns a Series from that attribute access, so the result is only used when it is a str.

to_crs(inplace=False) builds new layer objects, so names are mapped across to the new instances rather than copied by reference.

Tests

12 new tests in tests/test_named_layers.py, covering the add/get round trip, unknown and duplicate names, preserved list semantics, name survival through both to_crs paths, a GeoDataFrame carrying a "name" column, a RasterLayer subclass with the pre-existing __init__ signature loading through from_file, and rejection of the same layer object under a second name.

Suite: 92 passed, 1 skipped (the skip is pre-existing and unrelated).

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

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: Team

Run ID: 6ba87834-5cf5-4058-aa1b-d8b8cc51e84e

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

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.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.28571% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.18%. Comparing base (070f9b7) to head (cdbca89).

Files with missing lines Patch % Lines
mesa_geo/geospace.py 96.42% 0 Missing and 1 partial ⚠️
mesa_geo/raster_layers.py 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #339      +/-   ##
==========================================
+ Coverage   78.32%   79.18%   +0.86%     
==========================================
  Files          10       10              
  Lines        1024     1052      +28     
  Branches      168      176       +8     
==========================================
+ Hits          802      833      +31     
+ Misses        181      180       -1     
+ Partials       41       39       -2     

☔ View full report in Codecov by Harness.
📢 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.

@Tejasv-Singh

Copy link
Copy Markdown
Contributor Author

Both failing checks are pre-existing on main. Two upstream releases landed at once.

build (ubuntu, 3.11) : mesa dropped Python 3.11 at 3.4.0, so on 3.11 pip can only
resolve mesa 3.3.1, which still calls solara.v.TabsItems. ipyvuetify 3.0.0 targets
Vuetify 3, which removed that component. mesa main already migrated to
solara.v.Window/WindowItem, which is why 3.12, 3.13 and 3.14 all pass.

Test GIS examples : affine 3.0.1 added a PendingDeprecationWarning on *, and
rasterio.transform.from_bounds still uses it internally, with the examples suite
running warnings-as-errors. The split confirms it: the four vector-only examples pass,
the three raster ones fail, which is exactly "calls from_bounds".

Neither is reachable from this PR, which only touches geospace.py, raster_layers.py
and adds tests/test_named_layers.py. My change shows up in the second traceback only
because RasterBase.__init__ has always called _update_transform().

Happy to open follow-ups: dropping 3.11 from the CI matrix to match mesa's floor, and
migrating mesa-geo's own two * uses (raster_layers.py:211 and :770) to @, which
clears ~200 warnings from the suite.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GeoSpace.layers is a plain list with no named lookup, making multi-layer models ergonomically painful

1 participant