Skip to content

feat: add drive-based Sugarscape with three-version architectural com…#457

Open
TomRodger wants to merge 7 commits intomesa:mainfrom
TomRodger:add-sugarscape-drives
Open

feat: add drive-based Sugarscape with three-version architectural com…#457
TomRodger wants to merge 7 commits intomesa:mainfrom
TomRodger:add-sugarscape-drives

Conversation

@TomRodger
Copy link
Copy Markdown

@TomRodger TomRodger commented Mar 31, 2026


Context & motivation

I built this model while exploring Mesa's Behavioural Framework project for GSoC 2026. I chose to work on Sugarscape as EwoutH mentioned it explicitly as a pain point, saying "all needs get stuffed into one big step method". I wanted to test that claim directly by building the monolithic version first, so I could experience the pain of building with the current architecture, before refactoring it in another second model. Going this path with three-version comparison was done to make the value of the refactor clear and real rather than just theoretical. The seek trade drive was motivated by Rob Axtell's observation that decentralised bilateral trade suppresses volume below equilibrium; I wanted to test whether drive-based agents could reduce that friction by actively seeking for complementary partners. Sugarscape got me genuinely interested in ABM, seeing how something resembling market dynamics emerges from agents just following simple bilateral trade rules felt highly impressive to me, and it made me want to understand and learn how it worked from the inside.

What I learned

The biggest surprise was that execution timing is an architectural decision with real emergent consequences. I didn't initially set my goal on increasing trade volume, however, there was a 53% increase that came as a result of moving trading inside each agent's step() rather than running a separate model pass. I only understood why after the fact, and it changed how I think about what the Behavioural Framework actually needs to control.

Building the monolith first taught me more about Mesa's limitations than reading the codebase would have. Feeling the pain of adding drives to an already-branching step() myself made it clear that proper abstraction is a needed large improvement to Mesa's current architecture.

I also learned that Solara is a development tool, not just for presentation and understanding. Watching the agents switch drives on the colour-coded map and the plots helped me find a threshold bug that 100 batch runs never showed. That changed my approach for debugging and testing Mesa models going forward.

Learning repo

🔗 My learning repo: https://github.com/TomRodger/GSoC-learning-space
🔗 Relevant model(s): https://github.com/TomRodger/GSoC-learning-space/tree/main/models/sugarscape

Readiness checks

  • This PR addresses an agreed-upon problem (linked issue or discussion with maintainer approval), or is a small/trivial fix: see Discussion #2927 (GSoC 2026 ideas, Behavioural Framework) where EwoutH explicitly cited Sugarscape as a pain point
  • I have read the contributing guide and deprecation policy
  • I have performed a self-review: I reviewed my own PR as if I were a reviewer and left comments on anything that needs explanation
  • Another GSoC contributor has reviewed this PR: @
  • Tests pass locally: Example runs without errors locally across all three versions
  • Code is formatted (ruff check . --fix): Ran ruff check . --fix — 20 issues auto-fixed. 2 remaining trailing whitespace warnings in docstring comments in monolith_agents.py were fixed manually.
  • If applicable: documentation, examples, and/or migration guide are updated

Copy link
Copy Markdown
Author

@TomRodger TomRodger left a comment

Choose a reason for hiding this comment

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

Self-review: reviewed my own PR and left inline comments on what I believe were my key architectural decisions, known limitations, and intentional choices.

return max(sugar_ticks / spice_ticks, spice_ticks / sugar_ticks)
return 0

def choose_cell(self, agent):
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This has an O(n^2) runtime. For each candidate cell we scan all traders within vision of that cell to count complementary partners. Works for 50 x 50 grids with 200 agents, but would not scale well to large populations. A production implementation might use spatial indexing or cached neighbour lookups.

if self.is_starved():
self.remove()

def step(self):
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Trading happens inside each behaviour's act() rather than in a separate model pass. This is what produced the 53% trade increase which is documented in the README. This change is the strongest finding to support the case for the Behavioural Framework project.; Mesa's execution model implicitly constrains emergent outcomes right now in the original Sugarscape G1MT architecture.

import sys
from pathlib import Path

# Add subfolders to path
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sys.path is needed to be used here because batch_run.py imports from two subdirectories (monolith/ and behavioural/). The inserts must come before the imports. The #Noqa: E402 comments are used to prevent ruff's import-order warning.

try:
model = model_class(rng=seed)
model.run_model(step_count=STEPS)
except (ValueError, KeyError):
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The original Mesa Sugarscape G1MT has bugs, crashing with KeyError in the DataCollector when the grid empties. This try/except catches those crashes so they don't abort the batch run. The failed runs are excluded from the results rather than counted as zeros (In testing there was only ever a couple failed runs), which is why the original model has slightly fewer valid results than 100 in some runs.

if self.is_starved():
self.remove()

def step(self):
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This method is intentionally verbose to demonstrate the architectural problem. Every drive branch ends with the same eat() -> maybe_die() -> return pattern. Adding a new drive requires duplicating this pattern and adding another elif branch here AND in move() as well. This is the pain point that the refactored behavioural version solves.

# #
######################################################################

def move(self, mode="default", urgent_resource=None):
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The string-flag branching here (mode="survive", mode="gather_sugar" etc.) is the same coupling problem as in step(), but just one level down. Each drive needs different cell scoring logic, but there's no clean way to express that without branching on a string passed from step(). The behavioural version instead moves this logic into each Behaviour's choose_cell() instead.

3- collect data
"""
# iterate through traders in neighboring cells and trade
if self.active_drive == "survive":
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is the most problematic coupling since active_drive is set in step(), but trade_with_neighbors() is called by the model in a separate pass, after all agents have stepped. The method has to check a flag set in a completely different execution phase to know whether to skip trading. In the behavioural version this coupling disappears because trading is handled inside each behaviour's act().

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.

1 participant