Skip to content

Fix network visualization bug: Replace array indexing with dictionary lookup#3045

Merged
quaquel merged 5 commits intomesa:mainfrom
codebyNJ:fix-network-visualization-bug
Jan 17, 2026
Merged

Fix network visualization bug: Replace array indexing with dictionary lookup#3045
quaquel merged 5 commits intomesa:mainfrom
codebyNJ:fix-network-visualization-bug

Conversation

@codebyNJ
Copy link
Copy Markdown
Contributor

@codebyNJ codebyNJ commented Dec 30, 2025

Fixes #3023. @Sahil-Chhoker This is a new PR I have raised a cleaner version.

Problem: Code used node IDs as array indices instead of dictionary keys, causing IndexError that was silently suppressed.

Solution: Replace array indexing with dictionary lookup with proper error handling.

Before: Nodes [0,1,5,10,15] → 0/5 agents visible (all lost)
After: Nodes [0,1,5,10,15] → 5/5 agents visible

Tests: 6 comprehensive unit tests added covering:

  • Non-contiguous node IDs
  • Missing node positions with warnings
  • Large and negative node IDs
  • Edge cases (empty, single node, duplicates)

Files changed:

  • mesa/visualization/space_renderer.py - Fix implementation
  • tests/visualization/test_space_renderer.py - Unit tests

To Reproduce

import networkx as nx
from mesa import Agent, Model
from mesa.space import NetworkGrid
from mesa.visualization import SpaceRenderer
import numpy as np
import contextlib

# Create network with non-contiguous node IDs
G = nx.Graph()
G.add_nodes_from([0, 1, 5, 10, 15])
G.add_edges_from([(0, 1), (1, 5), (5, 10), (10, 15)])

model = Model()
space = NetworkGrid(G)
model.grid = space

for node_id in [0, 1, 5, 10, 15]:
    agent = Agent(model)
    space.place_agent(agent, node_id)

renderer = SpaceRenderer(model)

# Demonstrate the actual bug - what old code did
print("BEFORE FIX (old code with array indexing):")

pos_array = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6], [0.7, 0.8], [0.9, 1.0]])
node_ids = np.array([[0], [1], [5], [10], [15]], dtype=float)

mapped_positions_old = []
for node_id in node_ids[:, 0].astype(int):
    with contextlib.suppress(IndexError):  # This is what the old code did!
        pos = pos_array[node_id]
        mapped_positions_old.append(pos)

print(f"Agents mapped: {len(mapped_positions_old)}/5")
print(f"Agents silently dropped: {5 - len(mapped_positions_old)}/5")
print(f"Which nodes were dropped: {[int(n) for n in node_ids[:, 0] if int(n) >= len(pos_array)]}")

# Demonstrate the fix - what new code does
print("\nAFTER FIX (new code with dictionary lookup):")

pos_dict = {
    0: np.array([0.1, 0.2]),
    1: np.array([0.3, 0.4]),
    5: np.array([0.5, 0.6]),
    10: np.array([0.7, 0.8]),
    15: np.array([0.9, 1.0])
}

mapped_positions_new = []
for node_id in node_ids[:, 0].astype(int):
    if node_id in pos_dict:  # Dictionary lookup - works for any node ID
        mapped_positions_new.append(pos_dict[node_id])

print(f"Agents mapped: {len(mapped_positions_new)}/5")
print(f"All agents visible: {len(mapped_positions_new) == 5}")

Explaination

Networks with non-contiguous node IDs (like [0, 1, 5, 10, 15] ) caused the old code to silently fail when using array indexing. By replacing array indexing with dictionary lookup, ensures all agents are properly visualized regardless of node ID values.

@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.9% [-0.1%, +1.9%] 🔵 -0.0% [-0.1%, +0.1%]
BoltzmannWealth large 🔵 +1.3% [-0.8%, +4.5%] 🔵 -3.3% [-7.1%, +0.5%]
Schelling small 🔵 +0.1% [-0.9%, +1.0%] 🔵 -0.1% [-0.6%, +0.3%]
Schelling large 🔵 +1.7% [+0.6%, +3.5%] 🔵 -0.1% [-0.9%, +0.7%]
WolfSheep small 🔵 -0.4% [-1.3%, +0.3%] 🔵 -0.6% [-0.8%, -0.4%]
WolfSheep large 🔵 -1.7% [-5.4%, +0.9%] 🔵 -0.2% [-1.0%, +0.8%]
BoidFlockers small 🔵 +0.8% [+0.5%, +1.1%] 🔵 +1.8% [+1.6%, +2.0%]
BoidFlockers large 🔵 +0.0% [-0.4%, +0.5%] 🔵 +0.7% [+0.3%, +1.0%]

@quaquel quaquel added the bug Release notes label label Dec 30, 2025
@EwoutH EwoutH requested a review from Sahil-Chhoker January 9, 2026 10:17
@Sahil-Chhoker
Copy link
Copy Markdown
Collaborator

@codebyNJ Thanks for the PR. After reviewing the problem again, it appears we have two distinct issues: the Node ID mapping error (which you identified) and the layout synchronization race condition (identified by @codebreaker32).

While this PR fixes the mapping logic, we also need the graceful failure handling from @codebreaker32's approach to prevent crashes during frame delays. I’d like you two to collaborate on a hybrid solution in this PR. Ideally, we should combine the accuracy of dictionary lookups with NaN masking for missing nodes. Please update the tests to cover both scenarios efficiently.

@codebyNJ codebyNJ force-pushed the fix-network-visualization-bug branch from 79e3d2b to 2cee4db Compare January 9, 2026 15:24
@codebyNJ codebyNJ force-pushed the fix-network-visualization-bug branch from d88afd3 to 23adae6 Compare January 9, 2026 16:04
@codebyNJ
Copy link
Copy Markdown
Contributor Author

codebyNJ commented Jan 9, 2026

@Sahil-Chhoker

Hybrid fix combining approaches from #3045 and #3065

This PR addresses two distinct issues:

Non-contiguous node ID mapping - Using dictionary lookup instead of array indexing
Race condition resilience - Using NaN masking for missing nodes (from @codebreaker32's approach in #3065)
Missing agents are now hidden (NaN) rather than placed at origin, which is safer and prevents visual artifacts.

The below is the benchmarks after the hybrid fix
image

@tpike3
Copy link
Copy Markdown
Member

tpike3 commented Jan 10, 2026

@Sahil-Chhoker I think the hybrid suggestion and collaboration is genius!

I will throw out a thought that anyone can feel free to dismiss, but in the hybrid vein, instead of a for loop (Line 171), what if you vectorized the operation (the mask approach of #3065)? I think this should further improve performance.

@codebyNJ
Copy link
Copy Markdown
Contributor Author

@tpike3 I tried out your idea locally and also benchmarked by implementing vectorization using np.unique() with boolean masking.

If you want me to make the change I am ready to make it.

Vectorization Results

The vectorized approach (using np.unique() with boolean masking) is consistently faster:

Scenario Loop Vectorized Improvement
Small networks (10 nodes) 112 µs 59 µs -48%
Non-contiguous nodes 175 µs 62 µs -65%
Large networks 787 µs 748 µs -5%
Race conditions 153 µs 31 µs -80%
image

@Sahil-Chhoker
Copy link
Copy Markdown
Collaborator

@codebyNJ thanks for benchmarking both approaches, can you implement the vectorized approach if it doesn't introduce the discrete node_id problem again.

@codebyNJ
Copy link
Copy Markdown
Contributor Author

@Sahil-Chhoker I have updated with vectorization technique. It does not introduce discrete node_id issue again.

Copy link
Copy Markdown
Collaborator

@Sahil-Chhoker Sahil-Chhoker left a comment

Choose a reason for hiding this comment

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

Thanks @codebyNJ for this PR, this is approved from my side but it can take a look from @quaquel.

Comment on lines +173 to +182
# Warn if significant nodes missing (likely layout issue, not race condition)
if missing_nodes and len(missing_nodes) > len(pos_dict) / 10:
sample = missing_nodes[: min(5, len(missing_nodes))]
warnings.warn(
f"Many nodes {sample}{'...' if len(missing_nodes) > 5 else ''} not found "
f"in position layout ({len(missing_nodes)}/{len(node_ids)} agents). "
f"This may indicate the network layout needs to be updated or regenerated.",
UserWarning,
stacklevel=2,
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did you run into such a problem while testing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't encounter this during testing of Issue #3023 specifically, but I added it as defensive handling since we're now using dictionary lookup. Without the warning, missing nodes would silently become NaN (invisible) with no indication to the user.

If you think this warning is unnecessary, I can remove it and just let missing nodes silently become NaN. Let me know your preference!

@quaquel quaquel merged commit fb75535 into mesa:main Jan 17, 2026
14 checks passed
quaquel pushed a commit to quaquel/mesa that referenced this pull request Jan 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Network visualization silently drops agents on non-contiguous nodes due to IndexError suppression

4 participants