mesa icon indicating copy to clipboard operation
mesa copied to clipboard

Minimal Bundled SVG Agent Icons (refs #2857)

Open Mani212005 opened this issue 3 weeks ago • 14 comments

Minimal Bundled SVG Agent Icons

Summary

This PR introduces a minimal internal SVG agent icon library and helper module. It is intentionally small and focused on packaging + access + baseline performance, with no frontend integration yet. That will follow in a separate PR after reviewer feedback.

Changes

  • mesa/visualization/icons.py:
    • list_icons() – returns available icon basenames.
    • get_icon_svg(name) – returns raw SVG text (supports mesa:<name>).
  • 3 SVG icons under mesa/visualization/icons/ (using fill="currentColor" for recoloring). Current icons: neutral_face, smiley, sad_update.
  • mesa/visualization/icons/README.md – brief style/naming rules.
  • tests/test_icons.py – listing and retrieval tests.
  • pyproject.toml – added [tool.hatch.build] include patterns to package SVG assets (Hatch/hatchling is used; no MANIFEST.in).
  • benchmarks/icons_benchmark.py – dev-only Python benchmark script (not included in package builds).

No existing visualization behavior is changed.

Rationale

Split the work:

  1. Asset + access layer (this PR) – low risk, easy to review and test.
  2. Frontend integration + performance comparison (next PR). This sequencing allows maintainers to validate packaging + API before adopting icon rendering.

Performance (Python benchmark)

Environment:

  • Python: 3.13.x
  • Packages: cairosvg (version), Pillow (version)

Method: python benchmarks/icons_benchmark.py --icon neutral_face --n N --frames 120
Measures cold SVG read, SVG→raster conversion, and per-frame composition time (Pillow alpha compositing).

Results (neutral_face.svg):

Agents (N) svg_read_sec svg_convert_sec avg_frame_ms p95_frame_ms
100 0.000 0.013 0.367 0.393
500 0.000 0.014 1.956 2.001
1000 0.000 0.011 3.883 4.129

Interpretation:

  • Cold read negligible.
  • One-time SVG→raster conversion fast (~13 ms).
  • Composition overhead for 100 agents is far below frame budget (<16 ms for 60 FPS).
  • Larger-N measurements will be added, and browser DOM/SVG vs Canvas will be benchmarked in the follow-up after minimal integration.

Verification

  • pytest -q passes locally.
  • Wheel build includes the SVG assets:
    • unzip -l dist/Mesa-*.whl | grep visualization/icons
  • Runtime check:
    python -c "from mesa.visualization import icons; print(icons.list_icons())" returns expected names.
  • Benchmark script runs successfully.

Follow-Up Plan (next PR)

  1. Add portrayal support: {"icon": "<name>", "color": "#RRGGBB"}.
  2. Implement Python-only Solara integration (inline SVG) and compare with cached raster composition.
  3. Benchmark browser frame times for N = 100 / 500 / 1000 agents (avg & p95).
  4. Consider metadata and/or sprite optimization if needed.

Notes

  • Icons are simple originals under the repository’s Apache 2.0 license.
  • Kept intentionally minimal: no metadata/gallery yet.
  • Benchmark script is dev-only and excluded from builds.

Refs #2857

Mani212005 avatar Nov 29 '25 14:11 Mani212005

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.2% [-0.7%, +1.1%] 🔵 -0.4% [-0.5%, -0.2%]
BoltzmannWealth large 🔵 -1.0% [-2.3%, +0.2%] 🔵 +1.0% [-1.8%, +3.4%]
Schelling small 🔵 +0.2% [-0.1%, +0.5%] 🔵 +1.3% [+0.8%, +1.8%]
Schelling large 🔵 -0.0% [-0.5%, +0.4%] 🔵 +1.4% [-0.1%, +2.7%]
WolfSheep small 🔵 +0.9% [+0.6%, +1.1%] 🔵 +0.5% [+0.3%, +0.8%]
WolfSheep large 🔵 +0.1% [-1.1%, +1.3%] 🔵 -1.0% [-1.9%, -0.2%]
BoidFlockers small 🔵 -1.6% [-2.4%, -0.8%] 🔵 -0.1% [-0.3%, +0.1%]
BoidFlockers large 🔵 -1.6% [-2.3%, -0.9%] 🔵 -0.9% [-1.3%, -0.6%]

github-actions[bot] avatar Nov 29 '25 14:11 github-actions[bot]

Thanks for the PR, especially with the different agent count benchmarks.

Could you compare the SVG drawing to the drawing of the default marker?

EwoutH avatar Nov 29 '25 15:11 EwoutH

Sure, I would be happy to do it.

Mani212005 avatar Nov 29 '25 15:11 Mani212005

Comparison: Default Marker vs Rasterized SVG (Pillow composition)

Setup:

  • Canvas: 800×600
  • Per-agent size: 32×32
  • Mode “marker”: ImageDraw.ellipse (filled circle)
  • Mode “icon”: pre-rasterized SVG (cairosvg once), per-agent alpha_composite
  • Environment: Python 3.13 (macOS), local machine

Results:

N agents Mode Avg frame time (ms) P95 frame time (ms) Notes
100 marker 0.084 0.104
100 icon 0.380 0.443 SVG convert ~0.014 s
500 marker 0.370 0.420
500 icon 1.906 1.994 SVG convert ~0.011 s
1000 marker 0.731 0.769
1000 icon 3.814 3.940 SVG convert ~0.012 s

Observations:

  • The default marker is ~4–5× faster than rasterized SVG composition at N=100, and both scale roughly linearly with N.
  • Rasterized SVG composition remains modest in absolute terms (~3.8 ms at N=1000 on this machine), but is consistently more expensive than drawing the primitive marker.
  • The one-time SVG → PNG conversion cost is small (~11–14 ms) and amortized across frames.

Next steps:

  • I’ll include a browser-side benchmark in the follow-up PR comparing: Should I do it ?
    • Canvas primitive markers vs cached icon blits (ImageBitmap/drawImage)
    • Inline SVG rendering
  • That should provide a more direct comparison for the web-based visualization path.

@EwoutH let me know if you’d like additional sizes (e.g., N=2000) or alternative marker shapes/colors for completeness.

Mani212005 avatar Nov 29 '25 16:11 Mani212005

Thanks. Can you give a screenshot how the benchmark looks?

EwoutH avatar Nov 29 '25 18:11 EwoutH

Yes, doing it

Mani212005 avatar Nov 30 '25 05:11 Mani212005

Screenshot 2025-11-30 at 6 58 43 PM Screenshot 2025-11-30 at 7 01 03 PM Screenshot 2025-11-30 at 7 02 14 PM

Sorry for the delay. Can I now build the Frontend for this ?

Mani212005 avatar Nov 30 '25 13:11 Mani212005

Thanks.

Can you explain how this integrates with our Solara visualisation and render backends? Or haven't you implemented that yet?

EwoutH avatar Nov 30 '25 19:11 EwoutH

Thanks! I haven’t implemented the Solara integration yet in this PR. Here’s my plan for the follow-up :

Integration plan (Solara + render backends)

  • Solara (ComponentsView)

    • Add a per-agent (or per-layer) render option to select:
      • default (ellipse marker)
      • icon:<name> (bundled SVG)
      • custom (user-supplied)
    • Canvas backend: cache rasterized icons (ImageBitmap) once, then draw per agent via drawImage. Avoid any per-frame SVG parsing.
    • Inline-SVG backend: reference the icon as <use> or <image> (data URL). Prefer Canvas for large N to reduce DOM overhead.
  • Backend considerations

    • Canvas 2D: icon caching + batched draws; scale via drawImage. Color variants via pre-tinted bitmaps or simple composition (phase 2).
    • Inline SVG: fine for small agent counts; we’ll document the trade-off vs Canvas for larger N.
    • Fallback: if an icon is missing/not loaded, fall back to the current default marker.
  • API surface

    • Introduce an optional rendering config read by ComponentsView, e.g.:
      render_style = {"type": "icon", "name": "smiley", "size": 32}
      
    • Defaults unchanged so existing models render exactly as today unless users opt in.
  • Performance

    • Add a browser-side benchmark page comparing:
      • Canvas primitive markers
      • Canvas cached icon blits (drawImage)
      • Inline SVG (<use>)
    • Complement the Python results in this PR; preload/cache icons to avoid runtime stalls.

What do you think about this ?

Mani212005 avatar Nov 30 '25 20:11 Mani212005

Observations:

  • The default marker is ~4–5× faster than rasterized SVG composition at N=100, and both scale roughly linearly with N.
  • Rasterized SVG composition remains modest in absolute terms (~3.8 ms at N=1000 on this machine), but is consistently more expensive than drawing the primitive marker.
  • The one-time SVG → PNG conversion cost is small (~11–14 ms) and amortized across frames.

Assuming these numbers and observations transfer to our Solara visualization, can we do anything to optimize it? 4-5x overhead sounds like quite a lot.

EwoutH avatar Nov 30 '25 20:11 EwoutH

Yes, for sure. I think we can definitely optimize to guarantee performance, immediate win is caching. We should ensure we only rasterize the SVG to a bitmap once , rather than per frame.

Mani212005 avatar Nov 30 '25 20:11 Mani212005

I have optimized it changes :

  • altair_backend.py:
    • Preserve original portrayal objects (“portrayals”) so SpaceRenderer can enrich with icon metadata.
    • Suppress warnings for icon/icon_size keys.
    • Render icons when arguments include icon_rasters (data URLs) by layering mark_image with mark_point.
  • Benchmarks:
    • backend_space_benchmark.py: single-run timing of per-frame render
    • backend_space_benchmark_matrix.py: batch runs producing Markdown tables for multiple N and modes

Benchmark Results (120 frames)

Backend Mode N Avg ms P95 ms Icon Overhead
Altair marker 100 15.02 15.45 baseline
Altair icon 100 15.10 15.56 +0.5%
Altair marker 500 22.77 22.98 baseline
Altair icon 500 22.72 23.32 -0.2%
Altair marker 1000 31.49 32.08 baseline
Altair icon 1000 31.97 32.66 +1.5%
Altair marker 2000 50.96 51.47 baseline
Altair icon 2000 51.57 52.17 +1.2%

Benchmark Results (60 frames)

Backend Mode N Avg ms P95 ms Icon Overhead
Altair marker 100 15.35 15.68 baseline
Altair icon 100 15.35 15.79 0%
Altair marker 500 22.48 22.97 baseline
Altair icon 500 23.16 23.34 +3.0%
Altair marker 1000 31.72 32.24 baseline
Altair icon 1000 32.07 32.71 +1.1%

Average icon overhead ~0.9–1.5% across test matrix (well below target of <50%).

Mani212005 avatar Dec 02 '25 16:12 Mani212005

That’s quite amazing! Thanks for the detailed benchmarks.

If you want you can move forward with Solara integration. Try to keep the implementation minimal / code complexity low. You can do that in a new PR if you want.

EwoutH avatar Dec 02 '25 18:12 EwoutH

Thankyou, would be happy to do that

Mani212005 avatar Dec 04 '25 02:12 Mani212005