Vectorized raster portrayal with a callable raster_portrayal API - #341
Vectorized raster portrayal with a callable raster_portrayal API#341Tejasv-Singh wants to merge 9 commits into
Conversation
Extract _RasterRenderer and _VectorRenderer from MapModule. Renderers accept resolved data (no model coupling), no delegation shims. Snapshot tests use decoded RGBA arrays instead of brittle base64 strings.
Convert matplotlib colour strings (tab:blue, C0, xkcd:sky blue) and RGBA tuples to CSS hex strings in _VectorRenderer for Leaflet compatibility. Map fillColor <-> fill_color across point markers and GeoJSON options, and pop marker_type before marker construction.
Document raster_portrayal parameter in make_geospace_component and MapModule.__init__ with PropertyLayerStyle field reference and examples.
…l dict and alpha Also defers colorbar support to a follow-up PR: the stacked-colorbar commit was dropped from this branch, and the pre-existing _COLORBAR_STATE warn-once block is removed rather than reinstated (warning on a field that defaults to True would warn every user who never asked for a colorbar -- mesa#3455). PropertyLayerStyle.colorbar is documented as ignored by the Leaflet renderer.
Resolve the colormap where the style is validated, so _band_rgba consumes a real Colormap and every accepted form (name, Colormap, list or tuple of colors) goes through one code path. Tuples used to fall past every isinstance branch into cmap(norm(data)); empty colormaps ([] or "") passed the truthiness test into the colour branch and died at to_rgba(None). Both now raise a TypeError naming the accepted forms, and the colormap check uses `is not None` to match the vmin/vmax convention. Guard vmin > vmax beside the all-NaN and non-finite guards, rather than letting matplotlib's "minvalue must be less than or equal to maxvalue" escape the render callback and blank the visualization. Warn once when the space has agents but no agent_portrayal was given, so the optional-portrayal default cannot silently draw zero agents. Carry marker alpha as opacity/fill_opacity instead of 8-digit hex, which the ipywidgets Color trait rejected before 8.1. The GeoJSON path keeps #rrggbbaa; point markers no longer round-trip colours through hex twice, so alpha is no longer quantized to 8 bits.
The flag is module-level by design, which makes it leak across tests exactly as _COLORBAR_STATE did. Without this fixture, test_warns_once_across_renders fails with `assert 0 == 1` whenever an earlier test in the class has already tripped the warning.
…ion path Forward raster_portrayal to GeoSpaceLeaflet only when it is set, so the default call is byte-identical to the pre-feature one. tests/test_geospace_component.py is restored to its base content: it is a backward-compatibility guard, and editing it to accept the new keyword silenced exactly what it exists to catch. Give _VectorRenderer an optional marker factory and have MapModule pass its own bound _get_marker. Moving _get_marker onto the renderer had made a MapModule subclass override dead code -- markers silently reverted to the default Circle with no error, the worst failure shape for user-supplied code. Default radius=5 only when the portrayal named no marker_type at all. An explicit marker_type="Circle" keeps ipyleaflet's own 1000 m default, as it did before; applying 5 there shrank such markers 200x with no diagnostic. An explicit radius is still honoured, which the base overwrote. Pass "none" and "transparent" through _css_color unchanged: Leaflet distinguishes fill:none from a transparent fill for hit-testing. This is scoped to the GeoJSON path, since ipyleaflet's Color trait rejects both keywords and markers must still resolve them to a colour. Add an EPSG:3857 source test asserting decoded pixel equality through a real warp. Every other raster test uses EPSG:4326, where to_crs is the identity, so the uint8-after-to_crs ordering had no coverage; this test fails if the cast is moved above to_crs, since ImageLayer.to_crs reprojects into a fresh float64 array and silently discards any earlier cast.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
All failing checks are red on main as well and come from dependency releases, not this PR: build (every lane): test_RasterWebTile.py::test_from_xyzservices fails because a recent xyzservices release added an apikey field to provider metadata (present by 2026.9.0, absent in 2024.9.0) that the test's hard-coded dict does not expect. build (3.11 only): test_geospace_component fails because 3.11 resolves mesa 3.3.1, whose ComponentsView calls solara.v.TabsItems, removed in ipyvuetify 3.0.0. Fixed in mesa 3.4+, which 3.12+ pull. Test GIS examples: affine 3.0.1 raises a PendingDeprecationWarning on *, and the examples run under -Werror. These are pre-existing on main, not introduced by this PR. |
Description
Adds a callable portrayal API for
RasterLayerbands and replaces the per-cell colormap loopin the Leaflet renderer with a vectorized one.
Stacked on #339. That branch does not exist on this repo, so this PR is opened against
mainand its diff currently includes #339's commits. Once #339 merges it reduces to
geospace_component.py,pyproject.tomlandtests/test_raster_portrayal.py.Usage
What changed
raster_portrayal, keyword-only onmake_geospace_component. Takes a barePropertyLayerStylefor every band, or a callable(layer_name, band_name) -> PropertyLayerStyle | None. Callable rather than dict becausecore deprecated dict portrayals for removal in Mesa 4.0. The renderer iterates the layer's
own bands and asks the callable about each, mirroring core's
property_layers.items()loop,which makes unknown-band errors impossible by construction.
_RasterRendererand_VectorRenderer,composed by
MapModule, so a laterGeoSpaceRendererextraction is a mechanical move. Purecode move,
render(model)output identical on both paths.colorandfillColorgo throughmatplotlib.colors.to_rgba. Core'sAgentPortrayalStyle.colordefaults to"tab:blue",which Leaflet cannot parse, so a portrayal written against core rendered wrong with no error.
matplotlibadded as an explicit dependency, since this importsmatplotlib.colorsandmatplotlib.colormapsdirectly rather than relying onmesa[rec].Backward compatibility
raster_portrayalis keyword-only, defaults toNone, and is forwarded toMapModuleonlywhen set, so the default call is byte-identical to before. The legacy
agent_portrayal-returns-RGBA path for raster cells is unchanged.tests/test_geospace_component.pyandtests/test_MapModule.pypass unmodified.MapModule._get_markerstays onMapModuleand is injected into_VectorRenderer, so asubclass override is still consulted after the split.
Two things do change, both fixes, both pinned by tests: with
marker_typeabsent and anexplicit
radiusgiven, the caller's radius was overwritten with5and is now respected;and the caller's portrayal dict was mutated, so a dict reused across renders lost its
marker_typekey after the first call.Notes for review
Reads go through
get_band(), not_data. Reading_dataavoids a copy and looks likethe obvious optimisation, but it is only a construction-time snapshot until #332 lands, so the
raster would render frozen at t=0 for any model mutating cells in
step(). It fails silentlyand only for models that change over time. There is a regression test and a comment on that
line.
Following from that, the read into the colormap step is still an O(cells) Python loop until
#332 merges, so the path is not vectorized end to end.
vmin/vmaxuseis not Nonerather than core's truthiness check, sovmin=0is respectedinstead of silently auto-ranging. Zero is the most common lower bound for raster data. The
one-line fix belongs upstream in core and should be filed separately.
PropertyLayerStyle.colorbaris ignored and documented as such. Support is a follow-up:ScalarMappablewithfig.colorbarcovers the drawing, but the figure cache keys onauto-derived
vmin/vmaxso it never hits for a mutating layer, and the widgets created perrender are never closed.
The deprecated
leaflet_viz.MapModuleis a divergent copy and is intentionally untouched.Tests
tests/test_raster_portrayal.py, covering pixel-exact output after the CRS transform, bandselection and z-order,
vmin=0, nan-aware auto-ranging, constant bands, NaN transparency, thelive-cell read, marker override delegation, portrayal dict reuse, colour normalization, and
type validation.
Suite: 157 passed, 1 skipped. The skip is pre-existing and unrelated.