diff --git a/examples/sugarscape/README.md b/examples/sugarscape/README.md new file mode 100644 index 000000000..202afda81 --- /dev/null +++ b/examples/sugarscape/README.md @@ -0,0 +1,262 @@ +# Sugarscape: Drive-Based Agent Architecture + +## What This Model Does + +This project implements three versions of Epstein & Axtell's Sugarscape with Traders model, each with the same intended agent behaviour but a different internal architecture: + +1. **Original** - Mesa's built-in Sugarscape G1MT. Agents follow a fixed action sequence (move -> eat -> trade) every tick with no internal decision-making about what they should prioritise. + +2. **Monolith** - Adds four internal drives (survive, gather sugar, gather spice, seek trade) that change how agents move and whether they trade or not. It is named "Monolith" as all the drive logic is crammed into `step()` and `move()` using if/elif chains. + +3. **Behavioural** - The same four drives, refactored into separate 'Behaviour' classes. Each behaviour defines its own urgency scoring and action sequence. The agent's `step()` selected the highest-scoring behaviour and delegates to it. + +The point of this model is not to just build a better Sugarscape. It is to show, with data, that how you structure agent decision-making can change the results emerging from the model. It is also to show that currently Mesa has no clean way to express drive-based architectures, and that adding proper abstraction (like the Behaviour class pattern) makes models easier to extend; adding a new drive just involves adding a new class, not involving edits across multiple methods(). + +## Why Sugarscape + +EwoutH explicitly cited Sugarscape as a pain point for the Behavioural Framework project, saying: + +> "All needs get stuffed into one big step method, illustrating how badly Mesa lacks goals, tasks, or behaviour trees." + +Sugarscape is also a harder test case than most examples. The bilateral MRS-based trading system (Epstein & Axtell, 1996) creates complex agent interactions: agents compute marginal rates of substitution, negotiate prices as the geometric mean of their MRS values, and trade only when both parties benefit. + +In a Complexity Explorer session on the Sugarscape model (that I stumbled across trying to research the model on Youtube), co-creator Rob Axtell explained that decentralised bilateral trade introduces structural friction; because agents can only trade with immediate neighbours rather than seeking optimal partners globally, trade volume is suppressed below what classical supply-and-demand theory predicts. The price converges roughly correctly, but the quantity traded is lower than equilibrium. Axtell framed this bottom-up approach though as a more realistic model than the top-down "auctioneer computes market-clearing price" approach. Real markets work through local adaptive agents, not based off of central planners. However, the friction is a direct consequence of agents having no strategic trade-seeking behaviour. + +This makes Sugarscape ideal for testing whether drive-based agents can reduce that friction by actively seeking for complementary trade partners, rather than passively stumbling into them. + +Sugarscape's two independent resources create drive conflicts that simpler models cannot. In Wolf-sheep, there was one prey type, making the only decision possible to eat or not to eat. In Sugarscape, an agent can be desperate for sugar and comfortable on spice simultaneously, and it has two ways to address this deficit: gather the resource directly, or trade surplus spice for it. This is what makes a four-drive system meaningful. + +This complexity is also what makes the architectural problem visible. With four drives, two resource types, and two acquisition methods, the if / elif chain in a monolithic `step()` grows quickly. Each new drive requires changes across `step()`, `move()`, and `trade_with_neighbors()`. This calls for an implementation of proper behavioural abstraction, to allow for scaling and easy implementation of desires for a complex problem. + +## What Mesa Features it Uses + +- **OrthogonalVonNeumannGrid** - 50x50 grid with Von Neumann neighbourhood (four cardinal directions) +- **PropertyLayer** - Sugar and spice distributions loaded from 'sugar-map.txt', with per-tick regrowth capped at initial max values. +- **CellAgent** - Agents bound to grid cells. Movement is handled by reassigning `self.cell`. +- **shuffle_do()** - Randomised agent activation order each tick. The original and monolith use two passes (`"step"` then `"trade_with_neighbours"`). The behavioural version uses a single pass with trading handled inside each behaviour's `act()` method. +- **DataCollector** - Tracks trader count, trade volume, geometric mean price, and drive distribution each tick +- **SolaraViz** - Interactive browser-based visualisation with agents colour-coded by active drive +- **create_agents()** - batch agent creation in model.py +- **AgentSet / model.agents** - Agent collection used in DataCollector lambdas and `shuffle_do()`for randomised activation +- **get_neighborhood()** - Vision-based cell and agent scanning. Used in every drive's cell selection and in trade partner dcisions +- **self.random / self.rng** - Mesa's built in reproducible random number generation that enables the 100-seed batch comparison with identical initial conditions across all three model versions + +## The Three Versions + +### Original (Mesa's built-in) + +Every agent executes the same fixed sequence every tick: + +```python +def step(self): + self.move() + self.eat() + self.maybe_die() +``` + +Then the model runs a separate trading pass. The agent never decides what to do; not considering whether moving or trading is more urgent. Movement always uses the same Cobb-Douglas welfare function regardless of which resource the agent actually needs. + +This isn't just a simple reflex agent though, since it already has internal state, and a utility function. However, it is a short-sighted single-step optimiser with no action selection, no drive prioritisation and no ability to skip or reorder actions + +### Monolith (drives crammed into step()) + +The monolith adds four drives by computing urgency values at the top of 'step()' and branching into different behaviours. This works but it creates some problems: + +- **`step()` becomes a long if/elif chain** where every branch ends with the same `eat()` -> `maybe_die()` -> `return` pattern. +- **`move()` branches on a string mode parameter**, with four different cell-scoring strategies inside one method. +- **`trade_with_neighbors()` checks `self.active_drive`**, a flag set during `step()` in a completely different execution phase. The coupling is implicit. +- **Adding a new drive** means adding a branch in `step()` AND in `move()` AND potentially in `trade_with_neighbors()`. + +This is the pattern that EwoutH described, with all needs being stuffed into one big step method. The monolith acts as a problem model, showing the issues caused by the current structure when looking to extend the original model. This is used to compare against the next refactored model. + +### Behavioural (drives as separate classes) + +Each drive becomes a 'Behaviour' subclass with three responsibilities: `score(agent)` returns urgency, `choose_cell(agent)` selects the target cell, and `act(agent)`executes the full action sequence including movement, eating, and trading. + +The Trader class has no `move()` method. Movement is fully owned by each behaviour's `choose_cell()`. The agent's `step()` reduces to: + +```python +def step(self): + self.prices = [] + self.trade_partners = [] + best = max(self.behaviours, key=lambda b: b.score(self)) + self.active_drive = best.name + best.act(self) +``` + +Adding a new drive means you only have to write one new class and append it to `self.behaviours`. No changes are needed for the Trader class, model, or any other behaviour. This allows for easy expansion of the model to add any drives. The decoupled Behaviour classes could also serve as leaf nodes in a behaviour tree or as actions within a goal-based planning system, since each is self-contained with its own scoring, cell selection, and action sequence. The flat max-score selection mechanism would need to be replaced with a hierarchical selector or planner, but the building blocks it selects between are ready to be reused. + +## The Drives + +Each drive computes a score based on the agent's current state. The highest-scoring +drive wins and controls the agent's behaviour for that tick. + +**Survive** — Fires when either resource is critically low. The agent moves greedily +toward whichever resource is most urgent and skips trading entirely. + +**Gather Sugar / Gather Spice** — Activates when one resource has fewer ticks of +reserves than the other. The agent targets cells rich in the needed resource, using +welfare as a tiebreaker between equal options. Trades opportunistically if neighbours +are nearby. + +**Seek Trade** — Activates when both resources are comfortable but imbalanced. The +agent moves toward cells near complementary traders — agents who have surplus of +what it needs. This is the drive that directly addresses Axtell's friction. + +**Default** — Fallback when resources are roughly balanced. Uses the original +welfare-maximising movement from Epstein & Axtell. + +### Drive Distribution over time + +- **Steps 0–60:** Gather Sugar and Gather Spice dominate as agents start with random endowments and immediately prioritise whichever resource they lack. Survive spikes briefly as some agents hit critical levels. There is a rapid die-off during this phase. +- **Steps 60–120:** Gather drives drop as agents die or stabilise, with Survive falling to near zero. +- **Steps 120+:** Seek Trade becomes the most common drive. Surviving agents are comfortable enough that their primary activity is rebalancing through trade. Gather drives persist at low levels as agents occasionally top up a resource. + +This emergent shift — from resource desperation to trade-seeking — falls naturally out of the scoring functions without any explicit phase logic. + +![Drive distribution over time](images/drive_distribution.png) + +## Results + +The batch_run.py class is used to compare the final results of each model with reliability through repetition, taking the results and the standard deviations of each. + +100-seed batch comparison, 500 steps per run, 200 initial agents. + +| Metric | Original | Monolith | Behavioural | +|---|---|---|---| +| Survival rate | 0.310 ± 0.033 | 0.314 ± 0.032 | 0.291 ± 0.030 | +| Final alive | 61.9 ± 6.7 | 62.8 ± 6.5 | 58.1 ± 6.0 | +| Total trades | 8,541 ± 888 | 9,046 ± 988 | 13,036 ± 1,502 | +| Final price | 1.039 ± 0.239 | 0.989 ± 0.107 | 0.996 ± 0.162 | + +### Key findings + +**Survival is comparable across all three versions.** The standard deviations heavily overlap, showing the drive-based agents survive at the same rate as the reactive agents. Adding these drives neither hurts nor helps survival in a statistically significant way. + +**The behavioural version generates 53% more trades.** 13,036 vs 8,541, with non-overlapping standard deviations. This is the most important finding; the seek trade drive actively moves agents toward complementary partners rather than relying on random encounters. This directly reduces the bilateral trade friction described by Axtell; agents no longer just trade with whoever they happen to be near, they instead move toward agents who have what they need. + +**Price convergence is tighter.** The behavioural version reaches a final price of 0.996 (closer to the theoretical equilibrium of 1.0) with lower variance than the original 1.039 ± 0.239. More trade volume means more price discovery, connecting to Axtell's point that the decentralised bottom-up view can approximate market-clearing prices. Drive-based agents get there faster because they trade more. + +**The monolith and behavioural versions implement the same drives but produce different trade volumes.** The monolith generates 9,046 trades, whereas the behavioural version generates 13,036. The difference is because of execution timing. + +### Execution timing matters + +The monolith and behavioural versions have the same drives, the same scoring functions, and the same cell selection logic, however they produce these very different trade volumes. The difference is entirely structural. + +In the monolith, the model runs two separate passes: all agents move (`shuffle_do("step")`), then all agents trade (`shuffle_do("trade_with_neighbors")`). Everyone is in their final position before any trading begins. + +In the behavioural version, each agent moves and trades within their own `step()` before the next agent acts. Each trade changes both agents' resource levels, which can create new trade opportunities for agents who act later in the same tick. This cascading effect is impossible in the separated-pass approahc, where all movement is finished before any trading begins. + +This was unintended. The 53% trade increase is a consequence that fell out of the architectural decision about when trading happens relative to movement. This is the kind of effect that the Behavioural Framework needs to make explicit and controllable. + +## What I learned + +**The original agents are smarter than they look.** My initial instinct was to frame the original as a simple reflex agent, and position my extension to adding goals. I quickly realised that would be wrong though, as the original uses a Cobb-Douglas welfare function to evaluate outcomes and select the best available cell - it is a utility based optimiser, not a reflex agent. What it lacks is not goals but temporal depth and action selection: the ability to choose between moving and trading, to prioritise one resource over another, or to pursue a strategy across multiple ticks. + +**Execution timing is an architectural decision with emergent consequences.** I did not set out to increase trade volume. The 53% increase was a side effect of moving trading from a separate model pass into the agent's behaviour. This finding reinforces the case for the Behavioural Framework: when Mesa forces you to structure agent actions in a particular way (separate activation passes), it implicitly constrains what can emerge from the model. A framework that gives modellers explicit control over action sequencing would prevent these accidental effects, or let modellers create them intentionally. + +**Building the monolith first made the refactor's value obvious.** If i had gone straight to the behavioural version, the clean architecture would look like over-engineering. Building the monolith first and experiencing the pain of adding any drives to an already-branching `step()` method made the case for separation strong. + +**Decoupling movement from the Trader completed the separation.** The earlier behavioural version still had a shared `move()` method with string-flag branching, which was the same pattern used in the monolith model, just being called from a nicer place. Moving cell selection into each behaviour's `choose_cell()` completed the decoupling. The Trader class no longer knows how any drive makes movement decisions. + +**Learning Solara changed how i debug models.** Being able to watch agents switch drives in real time (colour coded on the grid) caught issues that batch runs couldn't do. I saw that agents would get stuck in survive mode in areas with plenty of resources, with almost no agents getting being put in seek_trade mode, pointing towards a threshold problem that i was able to test and change. I learnt that solara visualisation was not just for presenting and understanding, but it's also a very useful development tool. + +![Spatial view at step 540](images/spatial_view.png) + +*Agent colours: black = survive, green = gather sugar, orange = gather spice, purple = seek trade, blue = default.* + +**The batch comparison forced me to think about what to measure.** Deciding on survival rate, trade volume, and price as the comparison metrics meant understanding what each model version was actually supposed to change. + +**Working with Mesa's internals gave me a stronger sense of what the Behavioural Framework needs to provide.** I hit the same architectural wall twice across two models (Wolf-Sheep and Sugarscape), adding drives that need to control movement, action selection, and trading, but Mesa only gives you `step()` and `shuffle_do()`. The Behaviour class pattern I built is a workaround for what should be a first-class Mesa abstraction. Building the model myself means I understand what the framework needs to replace and why. + +## What Was Hard + +**Mesa's API is changing rapidly.** The installed version's PropertyLayer, DataCollector, and Model constructor signatures all differ from what the documentation and built-in examples show. The built-in Sugarscape G1MT example itself has bugs (the `max()` crashes on empty welfare lists, the KeyError in the datacollector). This was the most frustrating part in the building process, spending time debugging the API mismatches rather than building the model. + +**The seek trade cell scoring is expensive.** For each candidate cell, the agent scans all traders within vision of that cell to count complementary partners. With 200 agents and vision up to 5, this is a lot of nested iteration. It works for 50x50 grids but would not scale well. A Behavioural Framework implementation might use spatial indexing or cached neighbour lookups. + +**Getting the three versions to produce a fair comparison was tricky** The original uses Mesa's Scenario class; the monolith and behavioural use plain constructor parameters. The original and monolith separate movement and trading into two activation passes, whereas the behavioural model interleaves them. These structural differences affect outcomes in ways that are hard to control for. In the batch comparison, I used the same seeds, same grid size, same initial parameters, and same step count, but the execution order difference remains a confounding factor, and is in itself a finding. + +**Drive thresholds and principled justification** +The values for `CRITICAL_THRESHOLD`, `COMFORTABLE_THRESHOLD`, and `IMBALANCE_RATIO` are currently hand-tuned. A sensitivity analysis varying these thresholds and measuring their effect on survival and trade volume would strengthen the results. This is something I would add given more time + +## Future Directions + +The drive system built here is the simplest useful architecture; each tick you score all drives, pick the highest, and then execute. This is enough to demonstrate the problem and produce measurable emergent differences, but it is not the end of the road. + +**Goal-based agents with multi-step plans.** The current drives are reactive, re-evaluating every tick. A goal-based agent would commit to a plan ("move to the spice hills over the next 5 ticks") and would only re-evaluate when the goal is achieved or an interrupt fires. This adds temporal depth, making the agent's current action, with the agent's current action depending on decisions made several ticks ago. Sugarscape is well-suited for this because the landscape has resource-hills, so an agent could form a plan to reach one rather than taking a cell-by-cell greedy approach. + +**Behaviour trees.** Currently, the `max(score)` selection in the behavioural model is flat, with every drive competing equally. A behaviour tree would add hierarchical structure: a top-level selector could choose between "survival subtree" and "economic subtree" where the economic subtree has its own selector between gathering and trading. This would make complex decision logic composable and inspectable, rather than relying on numeric score comparisons. + + +**Adaptive trade behaviour** Trade behaviour could also become drive dependent, meaning a desperate agent could accept worse trading terms, or a comfortable agent could hold out for better terms. A drive's `act()` could pass modified parameters to the trade logic, but this has not yet been implemented. + +These directions map onto a progression of agent architectures from Russell & Norvig: reactive -> drive-based (this model) -> goal-based -> utility-based -> learning. Each state adds a capability that Mesa currently cannot express natively. The Behavioural Framework's job is to make all of them possible without forcing modellers to reinvent the abstractions each time. + +## How to Run + +### Requirements + +``` +pip install mesa[rec] numpy pandas +``` + +### Visualisation (Solara) + +```bash +# Original +cd models/sugarscape/original +solara run original_app.py + +# Monolith +cd models/sugarscape/monolith +solara run monolith_app.py + +# Behavioural +cd models/sugarscape/behavioural +solara run behavioural_app.py +``` + +### Batch comparison + +```bash +cd models/sugarscape +python batch_run.py +``` + +Runs all three models across 100 seeds (500 steps each). Outputs a side-by-side comparison table and saves raw data to `batch_comparison.csv`. + +### Individual runs + +```bash +cd models/sugarscape/monolith +python monolith_run.py +``` + +## File Structure + +``` +sugarscape/ +├── batch_run.py # 100-seed comparison across all three models +├── sugar-map.txt # Landscape data (shared) +├── original/ +│ └── original_app.py # Solara viz for Mesa's built-in model +├── monolith/ +│ ├── monolith_agents.py # Drives crammed into step() and move() +│ ├── monolith_model.py # Model with drive distribution tracking +│ ├── monolith_run.py # Single-run output +│ └── monolith_app.py # Solara viz with drive-coloured agents +└── behavioural/ + ├── behavioural_agents.py # Drives as Behaviour classes with choose_cell() + ├── behavioural_model.py # Model with single-pass execution + └── behavioural_app.py # Solara viz with drive-coloured agents +``` + + + + + + + + + diff --git a/examples/sugarscape/batch_comparison.csv b/examples/sugarscape/batch_comparison.csv new file mode 100644 index 000000000..0e8f8c09c --- /dev/null +++ b/examples/sugarscape/batch_comparison.csv @@ -0,0 +1,300 @@ +seed,alive,survival_rate,total_trade,final_price,model +0,68,0.34,8793,0.9968960176749561,Original +1,68,0.34,10258,1.0479302550986085,Original +2,58,0.29,7761,0.9490187644038589,Original +3,61,0.305,9250,1.031981595036089,Original +4,55,0.275,7714,0.8941194422422978,Original +5,60,0.3,8104,1.0783531197484848,Original +6,52,0.26,7786,1.1354921023751945,Original +7,52,0.26,7803,0.7962773014395208,Original +8,64,0.32,8159,0.9886379368811291,Original +9,68,0.34,11213,1.7665399049525006,Original +10,68,0.34,8440,1.1554785025863772,Original +11,72,0.36,10556,1.0299416144548226,Original +12,52,0.26,7968,1.0646729065609117,Original +13,75,0.375,9782,1.1476970577583916,Original +14,58,0.29,8504,1.1822436963517402,Original +15,58,0.29,9060,0.9920556612209368,Original +16,59,0.295,7444,1.0821487313343414,Original +17,71,0.355,9714,0.9815177081690335,Original +18,71,0.355,9047,1.2252015289250808,Original +19,59,0.295,8937,1.0669405323003458,Original +20,59,0.295,7274,0.8591547100748557,Original +21,75,0.375,9067,0.984055853197206,Original +22,62,0.31,8510,1.4765647795624934,Original +23,62,0.31,8571,0.8235509311045087,Original +24,67,0.335,8885,1.0085578878910688,Original +25,60,0.3,7591,1.008845214082267,Original +26,54,0.27,9263,1.0137364605045742,Original +27,60,0.3,7009,0.9885260362490371,Original +28,48,0.24,6970,0.9036900903459764,Original +29,58,0.29,8731,1.007273365316077,Original +30,60,0.3,9278,0.9716441607622864,Original +32,70,0.35,9307,0.9419258727313354,Original +33,68,0.34,8773,1.7841227145448062,Original +34,63,0.315,9852,1.185798869921736,Original +35,61,0.305,7783,1.7891035869959018,Original +36,53,0.265,7511,1.3860671939014544,Original +37,60,0.3,8506,0.908997520444898,Original +38,65,0.325,9804,0.9991582413992073,Original +39,54,0.27,8246,1.0222224142341831,Original +40,77,0.385,9551,1.079151169477917,Original +41,60,0.3,8698,0.8210056320371152,Original +42,57,0.285,7537,0.7021068250219378,Original +43,64,0.32,7916,1.0197820946671743,Original +44,64,0.32,8587,0.9799110015645961,Original +45,73,0.365,9284,0.9756564188669977,Original +46,56,0.28,8217,0.9370442513274369,Original +47,65,0.325,8524,0.9016724641332722,Original +48,65,0.325,9230,1.0358346560431058,Original +49,64,0.32,8110,0.9905236192661481,Original +50,64,0.32,8906,1.0459914105103212,Original +51,62,0.31,9505,0.6770147468514934,Original +52,57,0.285,7952,0.8580837614873061,Original +53,58,0.29,7120,1.14587867104899,Original +54,50,0.25,7876,0.934429517915274,Original +55,65,0.325,8502,0.9879874194097944,Original +56,57,0.285,9172,0.9332701581017753,Original +57,56,0.28,7844,0.9900737613674419,Original +58,68,0.34,8276,0.7860637067351822,Original +59,67,0.335,9356,0.8070004362665836,Original +60,72,0.36,10429,1.0433486271788528,Original +61,60,0.3,9173,0.8650579479619126,Original +62,67,0.335,8950,0.38544314324604295,Original +63,76,0.38,9241,1.7607394373443759,Original +64,70,0.35,9788,1.671829251018696,Original +65,69,0.345,8275,0.9977008454975489,Original +66,70,0.35,9559,0.9970726549272186,Original +67,62,0.31,7721,1.0087338979056144,Original +68,64,0.32,7719,1.0106601697564366,Original +69,50,0.25,6869,1.057672603199883,Original +70,55,0.275,8933,0.9099826224397752,Original +71,67,0.335,8734,0.9874994365045487,Original +72,59,0.295,8629,0.9785532395530245,Original +73,65,0.325,8886,1.0620681422792355,Original +74,57,0.285,7678,0.9826010110935696,Original +75,47,0.235,8876,0.9989981307292902,Original +76,73,0.365,7575,1.0158923128133392,Original +77,76,0.38,8621,1.0083910648667085,Original +78,59,0.295,7748,1.0235507432557092,Original +79,59,0.295,8023,1.0261116180328642,Original +80,58,0.29,8140,0.9748924290212994,Original +81,63,0.315,7252,1.0001176445303182,Original +82,66,0.33,9087,0.9347853497948033,Original +83,61,0.305,7982,0.7968172078652912,Original +84,65,0.325,7912,1.053091957516723,Original +85,55,0.275,8635,1.2329744853216142,Original +86,62,0.31,9393,1.064547964651261,Original +87,66,0.33,8982,1.0020192209542365,Original +88,57,0.285,8596,1.0167149279371748,Original +89,69,0.345,9523,0.9098412507547369,Original +90,61,0.305,8931,0.8812279793474324,Original +91,54,0.27,7391,0.9968021901527426,Original +92,63,0.315,10011,0.6265554543226537,Original +93,61,0.305,8600,1.013224087801643,Original +94,56,0.28,8191,0.9964053911971182,Original +95,51,0.255,6993,0.9144709161821593,Original +96,63,0.315,8849,1.0217269267121007,Original +97,57,0.285,6736,1.899738678580259,Original +98,53,0.265,7063,1.5362650792655013,Original +99,58,0.29,8492,0.9070490903216882,Original +0,66,0.33,9667,1.0930666617257918,Monolith +1,70,0.35,9223,1.0118754315014025,Monolith +2,55,0.275,7738,0.9747477575466407,Monolith +3,64,0.32,9215,0.8555677314746782,Monolith +4,55,0.275,7904,0.9934264939589231,Monolith +5,57,0.285,7921,1.001115136080175,Monolith +6,57,0.285,9595,1.1464479470940052,Monolith +7,54,0.27,8194,1.0118803319035021,Monolith +8,63,0.315,8860,1.015917158921784,Monolith +9,66,0.33,10415,1.0105394785183368,Monolith +10,72,0.36,9063,1.0167107991444206,Monolith +11,71,0.355,11077,0.9887781384195578,Monolith +12,51,0.255,9400,0.9673189224654843,Monolith +13,72,0.36,10913,1.0700723032230028,Monolith +14,61,0.305,9199,1.1237394608539109,Monolith +15,62,0.31,9449,0.9933190703543069,Monolith +16,62,0.31,8547,1.0778234403452092,Monolith +17,70,0.35,10127,1.0306571056124307,Monolith +18,68,0.34,11075,1.1017326778406484,Monolith +19,63,0.315,9455,0.9781139009075875,Monolith +20,54,0.27,8521,1.003553134976041,Monolith +21,81,0.405,9587,0.9568080842334562,Monolith +22,67,0.335,9318,1.1843121283489972,Monolith +23,63,0.315,8314,0.7783036404818595,Monolith +24,64,0.32,10718,0.9176845372405074,Monolith +25,61,0.305,9226,0.8676258886186313,Monolith +26,58,0.29,8497,1.0917478425587372,Monolith +27,63,0.315,8173,1.0313976960552316,Monolith +28,48,0.24,7424,0.6112850310890915,Monolith +29,58,0.29,8453,1.017017228697876,Monolith +30,54,0.27,7832,1.0055268059679106,Monolith +31,69,0.345,8133,0.995887864451889,Monolith +32,72,0.36,11149,1.0664540768544934,Monolith +33,74,0.37,10677,0.8552587488773139,Monolith +34,62,0.31,8368,1.0781320203740858,Monolith +35,59,0.295,8490,1.0929456032680605,Monolith +36,54,0.27,7849,0.8569911908861556,Monolith +37,58,0.29,8762,0.9592222081562676,Monolith +38,61,0.305,10275,1.0068749368187215,Monolith +39,59,0.295,9322,0.8655038661453677,Monolith +40,78,0.39,11041,1.0656070815707566,Monolith +41,66,0.33,9768,1.0321065979125035,Monolith +42,61,0.305,9282,0.9227660816084289,Monolith +43,66,0.33,9352,1.0313013939820137,Monolith +44,70,0.35,10540,0.9754796303069839,Monolith +45,72,0.36,8971,0.9015374733277901,Monolith +46,56,0.28,8921,0.9785119074517936,Monolith +47,67,0.335,7984,0.8041784774834326,Monolith +48,64,0.32,8407,0.940885988837605,Monolith +49,67,0.335,8355,1.0017025021425043,Monolith +50,70,0.35,9264,0.9756698682829712,Monolith +51,63,0.315,8198,0.9875647322856793,Monolith +52,58,0.29,7945,1.0067080858219537,Monolith +53,60,0.3,7788,1.080818470560248,Monolith +54,51,0.255,7712,0.8795068045813121,Monolith +55,65,0.325,9431,1.2640965654773366,Monolith +56,65,0.325,9964,0.9760843989365329,Monolith +57,56,0.28,7568,0.9954978527000999,Monolith +58,69,0.345,8991,0.9385643734493428,Monolith +59,67,0.335,10566,1.010712750993899,Monolith +60,71,0.355,9258,0.9524253910493656,Monolith +61,59,0.295,8820,0.9535229736316182,Monolith +62,70,0.35,11201,0.7520614070956884,Monolith +63,69,0.345,8477,0.9919351215521912,Monolith +64,69,0.345,11142,0.957512287288733,Monolith +65,67,0.335,9065,0.9690745157281507,Monolith +66,69,0.345,10281,1.116813500895752,Monolith +67,59,0.295,8859,0.9914253537569488,Monolith +68,66,0.33,7996,0.8422745780487187,Monolith +69,53,0.265,7536,0.9998421032235636,Monolith +70,60,0.3,8353,0.9118415448363455,Monolith +71,65,0.325,8528,0.9202858765173644,Monolith +72,60,0.3,9529,0.9830325034277794,Monolith +73,66,0.33,8998,0.9565344566912269,Monolith +74,54,0.27,7913,1.1693380369659352,Monolith +75,53,0.265,9189,0.9332687565482516,Monolith +76,73,0.365,9452,1.1540108035280006,Monolith +77,78,0.39,9528,0.978365822662685,Monolith +78,56,0.28,8906,0.9318309384133167,Monolith +79,58,0.29,9266,1.0213764154990108,Monolith +80,56,0.28,7823,0.8813680279201757,Monolith +81,60,0.3,7917,1.008889695858537,Monolith +82,63,0.315,8738,1.009454977108908,Monolith +83,62,0.31,8367,1.0635945416692045,Monolith +84,61,0.305,8707,1.0087034013324054,Monolith +85,60,0.3,9051,0.9961102426178189,Monolith +86,64,0.32,9880,1.1820951840181106,Monolith +87,65,0.325,10004,1.1355976994933874,Monolith +88,61,0.305,8849,0.9796476084557914,Monolith +89,66,0.33,9932,0.7854588055602904,Monolith +90,60,0.3,9439,0.9015899521059886,Monolith +91,58,0.29,7864,1.101109500567651,Monolith +92,68,0.34,9918,1.2754343137469069,Monolith +93,61,0.305,9907,0.9044544211112594,Monolith +94,59,0.295,9794,0.6571729140068069,Monolith +95,52,0.26,7418,1.0732482861947468,Monolith +96,65,0.325,9266,0.9451762558150546,Monolith +97,58,0.29,7743,1.0639289487893613,Monolith +98,60,0.3,7566,0.9033375514687939,Monolith +99,54,0.27,7986,1.0430052928596814,Monolith +0,59,0.295,12354,1.1966123145368113,Behavioural +1,58,0.29,14083,0.902832791564973,Behavioural +2,53,0.265,10072,0.8769521892409083,Behavioural +3,57,0.285,12695,1.0279721718910777,Behavioural +4,53,0.265,11385,0.9679698679747772,Behavioural +5,52,0.26,10910,0.9441161698803218,Behavioural +6,51,0.255,13633,1.0151475891086117,Behavioural +7,53,0.265,13725,0.9399419982880108,Behavioural +8,62,0.31,12046,0.8298919626710328,Behavioural +9,61,0.305,15905,1.1406566041573074,Behavioural +10,69,0.345,13483,0.9064960637953964,Behavioural +11,66,0.33,14775,1.1614421039009202,Behavioural +12,49,0.245,13403,1.156631864712214,Behavioural +13,66,0.33,13886,1.0061829743624149,Behavioural +14,54,0.27,14661,1.0522579507626255,Behavioural +15,58,0.29,14688,0.5847391052006236,Behavioural +16,57,0.285,14441,1.2505506553188503,Behavioural +17,68,0.34,15085,0.9218062954238309,Behavioural +18,60,0.3,13798,0.997919988220892,Behavioural +19,54,0.27,12198,1.0829729613895125,Behavioural +20,55,0.275,12625,1.4791825802549774,Behavioural +21,74,0.37,14804,1.1213115628877455,Behavioural +22,59,0.295,13458,1.0984093409985811,Behavioural +23,54,0.27,11861,1.085240542777366,Behavioural +24,62,0.31,12862,0.756692558554022,Behavioural +25,60,0.3,14150,0.7267460622713846,Behavioural +26,58,0.29,14253,1.3030685299478748,Behavioural +27,58,0.29,12405,0.9897346168099722,Behavioural +28,46,0.23,9551,1.1396047329719625,Behavioural +29,54,0.27,11547,0.9867298706362267,Behavioural +30,54,0.27,13050,0.939126266680695,Behavioural +31,65,0.325,13705,0.9443746630796703,Behavioural +32,65,0.325,16003,1.3041237443789122,Behavioural +33,66,0.33,14473,0.915605761509301,Behavioural +34,64,0.32,12958,1.0060333465008338,Behavioural +35,51,0.255,12697,0.8578023995921702,Behavioural +36,48,0.24,11125,0.7756369469180613,Behavioural +37,54,0.27,11886,0.9224954243679202,Behavioural +38,57,0.285,12018,0.9564090186281519,Behavioural +39,50,0.25,12178,1.120512093603715,Behavioural +40,70,0.35,13946,0.9645433009220845,Behavioural +41,57,0.285,13007,1.1573275786361665,Behavioural +42,49,0.245,13455,1.0270992904785412,Behavioural +43,61,0.305,13146,0.9057565809352167,Behavioural +44,67,0.335,15454,0.9877333175302828,Behavioural +45,71,0.355,13102,1.1478414481390504,Behavioural +46,50,0.25,11435,0.8302177607209948,Behavioural +47,61,0.305,11590,0.9837048657563287,Behavioural +48,62,0.31,14070,0.7607631663998087,Behavioural +49,59,0.295,13549,1.0545440711210494,Behavioural +50,63,0.315,15284,1.0315203309699998,Behavioural +51,62,0.31,11623,1.5693244215004152,Behavioural +52,54,0.27,11245,1.0801718647379377,Behavioural +53,53,0.265,10152,1.183839715404108,Behavioural +54,50,0.25,11497,0.8658704834616354,Behavioural +55,61,0.305,13618,0.9820631941257116,Behavioural +56,54,0.27,14563,1.0531331200390905,Behavioural +57,53,0.265,11183,0.9974340423415267,Behavioural +58,62,0.31,14353,0.9450629617784179,Behavioural +59,59,0.295,12298,0.9592587120565728,Behavioural +60,66,0.33,15074,0.8319510710177179,Behavioural +61,58,0.29,13359,0.7860197256097189,Behavioural +62,66,0.33,16852,0.7909188287442599,Behavioural +63,66,0.33,13447,1.276722771696934,Behavioural +64,68,0.34,16569,1.1453160118891386,Behavioural +65,60,0.3,13154,0.9292672331701548,Behavioural +66,63,0.315,14129,0.9538195295358919,Behavioural +67,60,0.3,12462,1.0014690833943898,Behavioural +68,63,0.315,12898,0.7910395760465704,Behavioural +69,51,0.255,9877,1.2128507143955605,Behavioural +70,54,0.27,10772,0.9182550471877026,Behavioural +71,64,0.32,13725,0.8624590058422261,Behavioural +72,57,0.285,15140,0.9662581468262992,Behavioural +73,61,0.305,12464,1.136728566510001,Behavioural +74,52,0.26,9703,1.1941854906656622,Behavioural +75,51,0.255,12508,0.9177393383053934,Behavioural +76,60,0.3,12569,0.8255320880179655,Behavioural +77,74,0.37,12597,1.3416165118693801,Behavioural +78,59,0.295,12967,0.893131423560075,Behavioural +79,51,0.255,12223,0.9519020002380217,Behavioural +80,51,0.255,11131,0.6747998284950989,Behavioural +81,58,0.29,12177,0.9051699443354485,Behavioural +82,58,0.29,14576,0.9896800025234048,Behavioural +83,59,0.295,13754,0.9295970893941553,Behavioural +84,53,0.265,14716,1.1120190798279368,Behavioural +85,54,0.27,13542,0.9688192375673054,Behavioural +86,56,0.28,12580,1.0721217357470685,Behavioural +87,61,0.305,13974,0.9966401127308392,Behavioural +88,53,0.265,12274,0.9034812427633309,Behavioural +89,63,0.315,13862,0.9443128157064087,Behavioural +90,60,0.3,13578,1.045823992249312,Behavioural +91,52,0.26,12355,0.9766971901060502,Behavioural +92,60,0.3,14541,0.8467494188736774,Behavioural +93,57,0.285,13854,1.0103636002959786,Behavioural +94,51,0.255,12468,0.797577023440408,Behavioural +95,51,0.255,10775,1.0947383423136503,Behavioural +96,59,0.295,13115,0.8610513303614028,Behavioural +97,54,0.27,11618,1.1561694138439726,Behavioural +98,53,0.265,10675,0.977210999915846,Behavioural +99,52,0.26,12144,0.6942667880881335,Behavioural diff --git a/examples/sugarscape/batch_run.py b/examples/sugarscape/batch_run.py new file mode 100644 index 000000000..24ca31034 --- /dev/null +++ b/examples/sugarscape/batch_run.py @@ -0,0 +1,142 @@ +""" +Batch comparison: Original vs Monolith vs Behavioural Sugarscape. + +Runs all three models across the same seeds and compares: +- Survival rate +- Trade volume +- Final price +- Drive distribution (monolith and behavioural only) + +Outputs summary statistics and saves raw data to CSV. +""" + +import sys +from pathlib import Path + +# Add subfolders to path +sys.path.insert(0, str(Path(__file__).parent / "monolith")) +sys.path.insert(0, str(Path(__file__).parent / "behavioural")) + +import pandas as pd +from behavioural_model import SugarscapeBehavioural +from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt +from monolith_model import SugarscapeMonolith + +# ---- Configuration ---- +NUM_SEEDS = 100 +STEPS = 500 +INITIAL_POPULATION = 200 + + +def run_model(model_class, seed): + """Run a single model and return key metrics.""" + try: + model = model_class(rng=seed) + model.run_model(step_count=STEPS) + except (ValueError, KeyError): + # Original Mesa model has bugs with empty grids / datacollector + return None + + data = model.datacollector.get_model_vars_dataframe() + + alive = len(model.agents) + survival_rate = alive / INITIAL_POPULATION + + total_trade = data["Trade Volume"].sum() + + prices = data["Price"] + valid_prices = prices[prices > 0] + final_price = valid_prices.iloc[-1] if len(valid_prices) > 0 else -1 + + return { + "seed": seed, + "alive": alive, + "survival_rate": survival_rate, + "total_trade": total_trade, + "final_price": final_price, + } + + +def run_batch(model_class, name): + print(f"\nRunning {name}...") + results = [] + for i, seed in enumerate(range(NUM_SEEDS)): + if (i + 1) % 10 == 0: + print(f" {i + 1}/{NUM_SEEDS}") + result = run_model(model_class, seed) + if result is not None: + results.append(result) + print(f" Completed: {len(results)}/{NUM_SEEDS} succeeded") + df = pd.DataFrame(results) + df["model"] = name + return df + + +def print_summary(df, name): + """Print summary statistics for a model's batch results.""" + print(f"\n{'=' * 50}") + print(f" {name} ({NUM_SEEDS} runs, {STEPS} steps)") + print(f"{'=' * 50}") + print( + f" Survival rate: {df['survival_rate'].mean():.3f} " + f"± {df['survival_rate'].std():.3f}" + ) + print(f" Final alive: {df['alive'].mean():.1f} ± {df['alive'].std():.1f}") + print( + f" Total trades: {df['total_trade'].mean():.1f} " + f"± {df['total_trade'].std():.1f}" + ) + valid = df[df["final_price"] > 0]["final_price"] + if len(valid) > 0: + print(f" Final price: {valid.mean():.3f} ± {valid.std():.3f}") + else: + print(" Final price: N/A") + + +def main(): + print(f"Batch comparison: {NUM_SEEDS} seeds, {STEPS} steps each") + print("Models: Original, Monolith, Behavioural") + + # Run all three + original_df = run_batch(SugarscapeG1mt, "Original") + monolith_df = run_batch(SugarscapeMonolith, "Monolith") + behavioural_df = run_batch(SugarscapeBehavioural, "Behavioural") + + # Print summaries + print_summary(original_df, "Original") + print_summary(monolith_df, "Monolith") + print_summary(behavioural_df, "Behavioural") + + # Side-by-side comparison + print(f"\n{'=' * 60}") + print(" Side-by-Side Comparison") + print(f"{'=' * 60}") + print(f"{'Metric':<20} {'Original':>12} {'Monolith':>12} {'Behavioural':>12}") + print(f"{'-' * 56}") + print( + f"{'Survival rate':<20} " + f"{original_df['survival_rate'].mean():>11.3f} " + f"{monolith_df['survival_rate'].mean():>12.3f} " + f"{behavioural_df['survival_rate'].mean():>12.3f}" + ) + print( + f"{'Final alive':<20} " + f"{original_df['alive'].mean():>11.1f} " + f"{monolith_df['alive'].mean():>12.1f} " + f"{behavioural_df['alive'].mean():>12.1f}" + ) + print( + f"{'Total trades':<20} " + f"{original_df['total_trade'].mean():>11.1f} " + f"{monolith_df['total_trade'].mean():>12.1f} " + f"{behavioural_df['total_trade'].mean():>12.1f}" + ) + + # Save raw data + all_data = pd.concat([original_df, monolith_df, behavioural_df]) + all_data.to_csv("batch_comparison.csv", index=False) + print("\nRaw data saved to batch_comparison.csv") + + +if __name__ == "__main__": + main() diff --git a/examples/sugarscape/behavioural/behavioural_agents.py b/examples/sugarscape/behavioural/behavioural_agents.py new file mode 100644 index 000000000..d0457b1d2 --- /dev/null +++ b/examples/sugarscape/behavioural/behavioural_agents.py @@ -0,0 +1,679 @@ +import math + +from mesa.discrete_space import CellAgent + +# Thresholds (module-level constants) +CRITICAL_THRESHOLD = 3 +COMFORTABLE_THRESHOLD = 7 +IMBALANCE_RATIO = 1.5 + + +# Helper function +def get_distance(cell_1, cell_2): + """ + Calculate the Euclidean distance between two positions. + + Used in Behaviour.choose_cell() for tiebreaking. + """ + x1, y1 = cell_1.coordinate + x2, y2 = cell_2.coordinate + dx = x1 - x2 + dy = y1 - y2 + return math.sqrt(dx**2 + dy**2) + + +###################################################################### +# # +# BEHAVIOUR CLASSES # +# # +###################################################################### + + +class Behaviour: + """ + Base class for agent drives. + + Each behaviour defines: + - name: string identifier used for data collection + - score(): how urgent this behaviour is for the given agent + - choose_cell(): which cell the agent should move to + - act(): the full action sequence when this behaviour is selected + + Adding a new drive means subclassing Behaviour and appending + it to the agent's self.behaviours list. No changes needed + to the Trader class itself. + """ + + name = "default" + + def score(self, agent): + raise NotImplementedError + + def choose_cell(self, agent): + raise NotImplementedError + + def act(self, agent): + raise NotImplementedError + + +class SurviveBehaviour(Behaviour): + """ + Emergency drive: fires when either resource is critically low. + + Overrides normal behaviour — agent moves greedily toward the + most urgent resource and skips trading entirely. + """ + + name = "survive" + + def score(self, agent): + sugar_ticks = agent.sugar / agent.metabolism_sugar + spice_ticks = agent.spice / agent.metabolism_spice + # Score is positive only when below critical threshold + return max(0, CRITICAL_THRESHOLD - min(sugar_ticks, spice_ticks)) + + def choose_cell(self, agent): + """ + Pure greedy movement toward the critical resource. + + No welfare tiebreaker — when survival is at stake, + the agent takes the richest cell for whichever + resource is most urgent. + """ + sugar_ticks = agent.sugar / agent.metabolism_sugar + spice_ticks = agent.spice / agent.metabolism_spice + urgent = "sugar" if sugar_ticks < spice_ticks else "spice" + + neighboring_cells = [ + cell + for cell in agent.cell.get_neighborhood(agent.vision, include_center=True) + if cell.is_empty + ] + + if not neighboring_cells: + return agent.cell + + # Score by the urgent resource only + if urgent == "sugar": + max_val = max(cell.sugar for cell in neighboring_cells) + candidates = [cell for cell in neighboring_cells if cell.sugar == max_val] + else: + max_val = max(cell.spice for cell in neighboring_cells) + candidates = [cell for cell in neighboring_cells if cell.spice == max_val] + + # Tiebreak: closest cell + min_dist = min(get_distance(agent.cell, cell) for cell in candidates) + final_candidates = [ + cell + for cell in candidates + if math.isclose(get_distance(agent.cell, cell), min_dist, rel_tol=1e-02) + ] + + return agent.random.choice(final_candidates) + + def act(self, agent): + agent.cell = self.choose_cell(agent) + agent.eat() + # No trading — survival is all that matters + agent.maybe_die() + + +class GatherSugarBehaviour(Behaviour): + """ + Resource drive: agent needs sugar more than spice. + + Two-pass cell selection: first filter to cells with the + maximum sugar, then rank those by welfare as tiebreaker. + Trades opportunistically if neighbours are available. + """ + + name = "gather_sugar" + + def score(self, agent): + sugar_ticks = agent.sugar / agent.metabolism_sugar + spice_ticks = agent.spice / agent.metabolism_spice + # Score is the deficit between spice ticks and sugar ticks + return max(0, spice_ticks - sugar_ticks) + + def choose_cell(self, agent): + """ + Two-pass: filter to cells with the most sugar, + then rank those by welfare as tiebreaker. + + Tiebreaking chain: max sugar -> highest welfare -> closest -> random. + """ + neighboring_cells = [ + cell + for cell in agent.cell.get_neighborhood(agent.vision, include_center=True) + if cell.is_empty + ] + + if not neighboring_cells: + return agent.cell + + # First pass: filter to cells with the most sugar + max_sugar = max(cell.sugar for cell in neighboring_cells) + sugar_cells = [cell for cell in neighboring_cells if cell.sugar == max_sugar] + + # Second pass: rank by welfare as tiebreaker + welfares = [ + agent.calculate_welfare( + agent.sugar + cell.sugar, + agent.spice + cell.spice, + ) + for cell in sugar_cells + ] + + max_welfare = max(welfares) + candidates = [ + cell + for cell, w in zip(sugar_cells, welfares) + if math.isclose(w, max_welfare) + ] + + # Tiebreak: closest cell + min_dist = min(get_distance(agent.cell, cell) for cell in candidates) + final_candidates = [ + cell + for cell in candidates + if math.isclose(get_distance(agent.cell, cell), min_dist, rel_tol=1e-02) + ] + + return agent.random.choice(final_candidates) + + def act(self, agent): + agent.cell = self.choose_cell(agent) + agent.eat() + agent.maybe_die() + if agent.cell is not None and agent.model.enable_trade: + agent.trade_with_neighbors() + + +class GatherSpiceBehaviour(Behaviour): + """ + Resource drive: agent needs spice more than sugar. + + Two-pass cell selection: first filter to cells with the + maximum spice, then rank those by welfare as tiebreaker. + Trades opportunistically if neighbours are available. + """ + + name = "gather_spice" + + def score(self, agent): + sugar_ticks = agent.sugar / agent.metabolism_sugar + spice_ticks = agent.spice / agent.metabolism_spice + # Score is the deficit between sugar ticks and spice ticks + return max(0, sugar_ticks - spice_ticks) + + def choose_cell(self, agent): + """ + Two-pass: filter to cells with the most spice, + then rank those by welfare as tiebreaker. + + Tiebreaking chain: max spice -> highest welfare -> closest -> random. + """ + neighboring_cells = [ + cell + for cell in agent.cell.get_neighborhood(agent.vision, include_center=True) + if cell.is_empty + ] + + if not neighboring_cells: + return agent.cell + + # First pass: filter to cells with the most spice + max_spice = max(cell.spice for cell in neighboring_cells) + spice_cells = [cell for cell in neighboring_cells if cell.spice == max_spice] + + # Second pass: rank by welfare as tiebreaker + welfares = [ + agent.calculate_welfare( + agent.sugar + cell.sugar, + agent.spice + cell.spice, + ) + for cell in spice_cells + ] + + max_welfare = max(welfares) + candidates = [ + cell + for cell, w in zip(spice_cells, welfares) + if math.isclose(w, max_welfare) + ] + + # Tiebreak: closest cell + min_dist = min(get_distance(agent.cell, cell) for cell in candidates) + final_candidates = [ + cell + for cell in candidates + if math.isclose(get_distance(agent.cell, cell), min_dist, rel_tol=1e-02) + ] + + return agent.random.choice(final_candidates) + + def act(self, agent): + agent.cell = self.choose_cell(agent) + agent.eat() + agent.maybe_die() + if agent.cell is not None and agent.model.enable_trade: + agent.trade_with_neighbors() + + +class SeekTradeBehaviour(Behaviour): + """ + Trade drive: agent is comfortable but has an imbalanced surplus. + + Only activates when both resources are above the comfortable + threshold. Scores cells by base welfare weighted by the count + of complementary traders reachable from that cell. + + This is the drive that directly addresses Axtell's friction — + agents actively move toward beneficial trade partners rather + than passively stumbling into them. + """ + + name = "seek_trade" + + def score(self, agent): + sugar_ticks = agent.sugar / agent.metabolism_sugar + spice_ticks = agent.spice / agent.metabolism_spice + # Only scores positive when both resources are comfortable + if sugar_ticks > COMFORTABLE_THRESHOLD and spice_ticks > COMFORTABLE_THRESHOLD: + return max(sugar_ticks / spice_ticks, spice_ticks / sugar_ticks) + return 0 + + def choose_cell(self, agent): + """ + Score cells by base welfare multiplied by count of + complementary traders reachable from that cell. + + A cell near 2 good trade partners scores 3x a cell + near none. This is proportional, not arbitrary. + """ + neighboring_cells = [ + cell + for cell in agent.cell.get_neighborhood(agent.vision, include_center=True) + if cell.is_empty + ] + + if not neighboring_cells: + return agent.cell + + # Determine what we need based on imbalance + sugar_ticks = agent.sugar / agent.metabolism_sugar + spice_ticks = agent.spice / agent.metabolism_spice + need_sugar = spice_ticks > sugar_ticks + need_spice = sugar_ticks > spice_ticks + + scores = [] + for cell in neighboring_cells: + base = agent.calculate_welfare( + agent.sugar + cell.sugar, + agent.spice + cell.spice, + ) + # Count traders reachable from this cell who have + # a complementary surplus + complementary_count = 0 + for other in cell.get_neighborhood(radius=agent.vision).agents: + if not isinstance(other, Trader) or other is agent: + continue + other_sugar_t = other.sugar / other.metabolism_sugar + other_spice_t = other.spice / other.metabolism_spice + # I need sugar and they have more sugar than spice + if (need_sugar and other_sugar_t > other_spice_t) or ( + need_spice and other_spice_t > other_sugar_t + ): + complementary_count += 1 + + # Proportional bonus: each complementary trader + # multiplies the base welfare score + scores.append(base * (1 + complementary_count)) + + # Select best cell + max_score = max(scores) + candidates = [ + cell + for cell, score in zip(neighboring_cells, scores) + if math.isclose(score, max_score) + ] + + # Tiebreak: closest cell + min_dist = min(get_distance(agent.cell, cell) for cell in candidates) + final_candidates = [ + cell + for cell in candidates + if math.isclose(get_distance(agent.cell, cell), min_dist, rel_tol=1e-02) + ] + + return agent.random.choice(final_candidates) + + def act(self, agent): + agent.cell = self.choose_cell(agent) + agent.eat() + agent.maybe_die() + if agent.cell is not None and agent.model.enable_trade: + agent.trade_with_neighbors() + + +class DefaultBehaviour(Behaviour): + """ + Fallback drive: resources are roughly balanced. + + Uses the original welfare-maximising movement from + Epstein & Axtell. Always scores just above zero so + it loses to any real drive. + """ + + name = "default" + + def score(self, agent): + return 0.01 + + def choose_cell(self, agent): + """ + Original welfare-maximising cell selection. + + Identical to the movement logic in Mesa's built-in + Sugarscape — pick the cell that maximises Cobb-Douglas + welfare, tiebreak by distance, then random. + """ + neighboring_cells = [ + cell + for cell in agent.cell.get_neighborhood(agent.vision, include_center=True) + if cell.is_empty + ] + + if not neighboring_cells: + return agent.cell + + welfares = [ + agent.calculate_welfare( + agent.sugar + cell.sugar, + agent.spice + cell.spice, + ) + for cell in neighboring_cells + ] + + max_welfare = max(welfares) + candidates = [ + cell + for cell, w in zip(neighboring_cells, welfares) + if math.isclose(w, max_welfare) + ] + + # Tiebreak: closest cell + min_dist = min(get_distance(agent.cell, cell) for cell in candidates) + final_candidates = [ + cell + for cell in candidates + if math.isclose(get_distance(agent.cell, cell), min_dist, rel_tol=1e-02) + ] + + return agent.random.choice(final_candidates) + + def act(self, agent): + agent.cell = self.choose_cell(agent) + agent.eat() + agent.maybe_die() + if agent.cell is not None and agent.model.enable_trade: + agent.trade_with_neighbors() + + +###################################################################### +# # +# TRADER AGENT # +# # +###################################################################### + + +class Trader(CellAgent): + """ + Trader agent with fully decoupled, behaviour-driven actions. + + Each tick, the behaviour with the highest score is selected. + That behaviour controls the full action sequence: cell selection, + eating, trading. The Trader class itself has no move() method — + movement is owned entirely by the behaviours. + + Adding a new drive means writing one new Behaviour subclass + and appending it to self.behaviours. No changes to the Trader + class, model, or any other behaviour. + """ + + def __init__( + self, + model, + cell, + sugar=0, + spice=0, + metabolism_sugar=0, + metabolism_spice=0, + vision=0, + ): + super().__init__(model) + self.cell = cell + self.sugar = sugar + self.spice = spice + self.metabolism_sugar = metabolism_sugar + self.metabolism_spice = metabolism_spice + self.vision = vision + self.prices = [] + self.trade_partners = [] + self.active_drive = None + + # Behaviours — extend by appending new Behaviour subclasses + self.behaviours = [ + SurviveBehaviour(), + GatherSugarBehaviour(), + GatherSpiceBehaviour(), + SeekTradeBehaviour(), + DefaultBehaviour(), + ] + + ###################################################################### + # # + # TRADE HELPERS # + # # + ###################################################################### + + def get_trader(self, cell): + """Helper function used in self.trade_with_neighbors()""" + for agent in cell.agents: + if isinstance(agent, Trader): + return agent + + def calculate_welfare(self, sugar, spice): + """ + Cobb-Douglas welfare function. + + From Growing Artificial Societies p. 97. + Used in Behaviour.choose_cell() for cell scoring + and in trade() for evaluating whether a trade + improves both agents' welfare. + """ + m_total = self.metabolism_sugar + self.metabolism_spice + return sugar ** (self.metabolism_sugar / m_total) * spice ** ( + self.metabolism_spice / m_total + ) + + def is_starved(self): + """Helper function for self.maybe_die()""" + return (self.sugar <= 0) or (self.spice <= 0) + + def calculate_MRS(self, sugar, spice): + """ + Marginal Rate of Substitution. + + From Growing Artificial Societies p. 101. + Determines what the trader needs and can give up. + """ + return (spice / self.metabolism_spice) / (sugar / self.metabolism_sugar) + + def calculate_sell_spice_amount(self, price): + """ + Helper function for self.maybe_sell_spice(). + + Determines the quantities of sugar and spice to exchange + at a given price. + """ + if price >= 1: + sugar = 1 + spice = int(price) + else: + sugar = int(1 / price) + spice = 1 + return sugar, spice + + def sell_spice(self, other, sugar, spice): + """ + Execute a spice-for-sugar exchange between two traders. + + Used in self.maybe_sell_spice(). + """ + self.sugar += sugar + other.sugar -= sugar + self.spice -= spice + other.spice += spice + + def maybe_sell_spice(self, other, price, welfare_self, welfare_other): + """ + Evaluate and potentially execute a spice sale. + + Checks two criteria before trading: + 1. Both agents must be better off after the trade + 2. MRS crossing condition must not be violated + """ + sugar_exchanged, spice_exchanged = self.calculate_sell_spice_amount(price) + + # Assess hypothetical post-trade amounts + self_sugar = self.sugar + sugar_exchanged + other_sugar = other.sugar - sugar_exchanged + self_spice = self.spice - spice_exchanged + other_spice = other.spice + spice_exchanged + + # Ensure neither agent runs out of either resource + if ( + (self_sugar <= 0) + or (other_sugar <= 0) + or (self_spice <= 0) + or (other_spice <= 0) + ): + return False + + # Trade criteria #1 — are both agents better off? + both_agents_better_off = ( + welfare_self < self.calculate_welfare(self_sugar, self_spice) + ) and (welfare_other < other.calculate_welfare(other_sugar, other_spice)) + + # Trade criteria #2 — MRS crossing condition + mrs_not_crossing = self.calculate_MRS( + self_sugar, self_spice + ) > other.calculate_MRS(other_sugar, other_spice) + + if not (both_agents_better_off and mrs_not_crossing): + return False + + # Criteria met, execute trade + self.sell_spice(other, sugar_exchanged, spice_exchanged) + return True + + def trade(self, other): + """ + Bilateral trade between self and other. + + Computes MRS for both agents, determines price as the + geometric mean, and executes the trade if beneficial. + Recurses until no further beneficial trades are possible. + """ + assert self.sugar > 0 + assert self.spice > 0 + assert other.sugar > 0 + assert other.spice > 0 + + # Calculate marginal rate of substitution (p. 101) + mrs_self = self.calculate_MRS(self.sugar, self.spice) + mrs_other = other.calculate_MRS(other.sugar, other.spice) + + # Calculate each agent's welfare + welfare_self = self.calculate_welfare(self.sugar, self.spice) + welfare_other = other.calculate_welfare(other.sugar, other.spice) + + if math.isclose(mrs_self, mrs_other): + return + + # Price is geometric mean of both MRS values + price = math.sqrt(mrs_self * mrs_other) + + if mrs_self > mrs_other: + # Self is a sugar buyer, spice seller + sold = self.maybe_sell_spice(other, price, welfare_self, welfare_other) + else: + # Self is a spice buyer, sugar seller + sold = other.maybe_sell_spice(self, price, welfare_other, welfare_self) + + if not sold: + return + + # Capture data + self.prices.append(price) + self.trade_partners.append(other.unique_id) + + # Continue trading until no further benefit + self.trade(other) + + ###################################################################### + # # + # MAIN AGENT FUNCTIONS # + # # + ###################################################################### + + def eat(self): + """Harvest resources from current cell and pay metabolism costs.""" + self.sugar += self.cell.sugar + self.cell.sugar = 0 + self.sugar -= self.metabolism_sugar + + self.spice += self.cell.spice + self.cell.spice = 0 + self.spice -= self.metabolism_spice + + def maybe_die(self): + """Remove trader if either sugar or spice is exhausted.""" + if self.is_starved(): + self.remove() + + def step(self): + """ + Behaviour-driven step. + + The highest-scoring behaviour determines the full action + sequence for this tick — cell selection, eating, and trading + are all controlled by the selected behaviour's act() method. + + Compare to the monolith version where this logic is spread + across step(), move(), and trade_with_neighbors() via string + flags and if/elif chains. + """ + self.prices = [] + self.trade_partners = [] + + # Select the most urgent behaviour + best = max(self.behaviours, key=lambda b: b.score(self)) + self.active_drive = best.name + + # Behaviour controls the full action sequence + best.act(self) + + def trade_with_neighbors(self): + """ + Trade with all traders within vision. + + Called by the active behaviour's act() method — not by the + model. This means the behaviour controls whether trading + happens at all (survive skips it) and when it happens + relative to movement and eating. + """ + for a in self.cell.get_neighborhood(radius=self.vision).agents: + if isinstance(a, Trader): + self.trade(a) diff --git a/examples/sugarscape/behavioural/behavioural_app.py b/examples/sugarscape/behavioural/behavioural_app.py new file mode 100644 index 000000000..993f36019 --- /dev/null +++ b/examples/sugarscape/behavioural/behavioural_app.py @@ -0,0 +1,97 @@ +from behavioural_model import SugarscapeBehavioural +from mesa.visualization import Slider, SolaraViz, SpaceRenderer, make_plot_component +from mesa.visualization.components import AgentPortrayalStyle, PropertyLayerStyle + + +def agent_portrayal(agent): + color_map = { + "survive": "black", + "gather_sugar": "green", + "gather_spice": "orange", + "seek_trade": "purple", + "default": "blue", + } + + return AgentPortrayalStyle( + x=agent.cell.coordinate[0], + y=agent.cell.coordinate[1], + color=color_map.get(agent.active_drive, "gray"), + marker="o", + size=10, + zorder=1, + ) + + +def property_layer_portrayal(layer): + if layer == "sugar": + return PropertyLayerStyle( + color="blue", alpha=0.8, colorbar=True, vmin=0, vmax=10 + ) + return PropertyLayerStyle(color="red", alpha=0.8, colorbar=True, vmin=0, vmax=10) + + +def post_process(chart): + chart = chart.properties(width=400, height=400) + return chart + + +model_params = { + "rng": { + "type": "InputText", + "value": 42, + "label": "Random Seed", + }, + "width": 50, + "height": 50, + # Population parameters + "initial_population": Slider( + "Initial Population", value=200, min=50, max=500, step=10 + ), + # Agent endowment parameters + "endowment_min": Slider("Min Initial Endowment", value=25, min=5, max=30, step=1), + "endowment_max": Slider("Max Initial Endowment", value=50, min=30, max=100, step=1), + # Metabolism parameters + "metabolism_min": Slider("Min Metabolism", value=1, min=1, max=3, step=1), + "metabolism_max": Slider("Max Metabolism", value=5, min=3, max=8, step=1), + # Vision parameters + "vision_min": Slider("Min Vision", value=1, min=1, max=3, step=1), + "vision_max": Slider("Max Vision", value=5, min=3, max=8, step=1), + # Trade parameter + "enable_trade": {"type": "Checkbox", "value": True, "label": "Enable Trading"}, +} + +model = SugarscapeBehavioural() + +# Here, the renderer uses the Altair backend, while the plot components +# use the Matplotlib backend. +# Both can be mixed and matched to enhance the visuals of your model. +renderer = ( + SpaceRenderer(model, backend="altair") + .setup_agents(agent_portrayal) + .setup_propertylayer(property_layer_portrayal) +) +# Specifically, avoid drawing the grid to hide the grid lines. +renderer.draw_agents() +renderer.draw_propertylayer() + +renderer.post_process = post_process + +# Note: It is advised to switch the pages after pausing the model +# on the Solara dashboard. +page = SolaraViz( + model, + renderer, + components=[ + make_plot_component("#Traders", page=1), + make_plot_component("Price", page=1), + make_plot_component("Survive", page=2), + make_plot_component("Gather Sugar", page=2), + make_plot_component("Gather Spice", page=2), + make_plot_component("Seek Trade", page=2), + make_plot_component("Default", page=2), + ], + model_params=model_params, + name="Sugarscape {G1, M, T}", + play_interval=150, +) +page # noqa diff --git a/examples/sugarscape/behavioural/behavioural_model.py b/examples/sugarscape/behavioural/behavioural_model.py new file mode 100644 index 000000000..92293f725 --- /dev/null +++ b/examples/sugarscape/behavioural/behavioural_model.py @@ -0,0 +1,116 @@ +from pathlib import Path + +import mesa +import numpy as np + +# Import from LOCAL agents file, not Mesa's built-in +from behavioural_agents import Trader +from mesa.discrete_space import OrthogonalVonNeumannGrid +from mesa.discrete_space.property_layer import PropertyLayer + +# Helper Functions + + +def flatten(list_of_lists): + return [item for sublist in list_of_lists for item in sublist] + + +def geometric_mean(list_of_prices): + if len(list_of_prices) == 0: + return -1 + return np.exp(np.log(list_of_prices).mean()) + + +def drive_count(model, drive_name): + """Count how many agents have a given active drive.""" + return sum(1 for a in model.agents if a.active_drive == drive_name) + + +class SugarscapeBehavioural(mesa.Model): + def __init__( + self, + width=50, + height=50, + initial_population=200, + endowment_min=25, + endowment_max=50, + metabolism_min=1, + metabolism_max=5, + vision_min=1, + vision_max=5, + enable_trade=True, + rng=None, + ): + super().__init__(rng=rng) + self.width = width + self.height = height + self.enable_trade = enable_trade + self.running = True + + # Initiate grid + self.grid = OrthogonalVonNeumannGrid( + (self.width, self.height), torus=False, random=self.random + ) + + # Read in landscape file + self.sugar_distribution = np.genfromtxt(Path(__file__).parent / "sugar-map.txt") + self.spice_distribution = np.flip(self.sugar_distribution, 1) + + sugar_layer = PropertyLayer("sugar", (self.width, self.height)) + sugar_layer.data[:] = self.sugar_distribution + self.grid.add_property_layer(sugar_layer) + + spice_layer = PropertyLayer("spice", (self.width, self.height)) + spice_layer.data[:] = self.spice_distribution + self.grid.add_property_layer(spice_layer) + + # DataCollector — original reporters plus drive distribution + self.datacollector = mesa.DataCollector( + model_reporters={ + "#Traders": lambda m: len(m.agents), + "Trade Volume": lambda m: sum(len(a.trade_partners) for a in m.agents), + "Price": lambda m: geometric_mean( + flatten([a.prices for a in m.agents]) + ), + # Drive distribution — this is the new data + "Survive": lambda m: drive_count(m, "survive"), + "Gather Sugar": lambda m: drive_count(m, "gather_sugar"), + "Gather Spice": lambda m: drive_count(m, "gather_spice"), + "Seek Trade": lambda m: drive_count(m, "seek_trade"), + "Default": lambda m: drive_count(m, "default"), + }, + ) + + # Create trader agents + n = initial_population + Trader.create_agents( + self, + n, + self.random.choices(self.grid.all_cells.cells, k=n), + sugar=self.rng.integers(endowment_min, endowment_max, (n,), endpoint=True), + spice=self.rng.integers(endowment_min, endowment_max, (n,), endpoint=True), + metabolism_sugar=self.rng.integers( + metabolism_min, metabolism_max, (n,), endpoint=True + ), + metabolism_spice=self.rng.integers( + metabolism_min, metabolism_max, (n,), endpoint=True + ), + vision=self.rng.integers(vision_min, vision_max, (n,), endpoint=True), + ) + + def step(self): + # Regrow sugar and spice (1 unit per tick, up to max) + self.grid.sugar.data[:] = np.minimum( + self.grid.sugar.data + 1, self.sugar_distribution + ) + self.grid.spice.data[:] = np.minimum( + self.grid.spice.data + 1, self.spice_distribution + ) + + # Step trader agents (Trading is handled by each behaviour's act() method now) + self.agents.shuffle_do("step") + self.datacollector.collect(self) + + def run_model(self, step_count=1000): + for _ in range(step_count): + self.step() diff --git a/examples/sugarscape/behavioural/sugar-map.txt b/examples/sugarscape/behavioural/sugar-map.txt new file mode 100644 index 000000000..3a1ed84bd --- /dev/null +++ b/examples/sugarscape/behavioural/sugar-map.txt @@ -0,0 +1,50 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 +0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 +0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 +1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 +1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 +1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 +1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 +1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 +1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 +1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 +1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 +2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 +2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/examples/sugarscape/images/drive_distribution.png b/examples/sugarscape/images/drive_distribution.png new file mode 100644 index 000000000..7f703063e Binary files /dev/null and b/examples/sugarscape/images/drive_distribution.png differ diff --git a/examples/sugarscape/images/spatial_view.png b/examples/sugarscape/images/spatial_view.png new file mode 100644 index 000000000..e3f4868db Binary files /dev/null and b/examples/sugarscape/images/spatial_view.png differ diff --git a/examples/sugarscape/monolith/monolith_agents.py b/examples/sugarscape/monolith/monolith_agents.py new file mode 100644 index 000000000..d0083969a --- /dev/null +++ b/examples/sugarscape/monolith/monolith_agents.py @@ -0,0 +1,444 @@ +import math + +from mesa.discrete_space import CellAgent + +# Thresholds (module level constants) +CRITICAL_THRESHOLD = 3 +COMFORTABLE_THRESHOLD = 7 +IMBALANCE_RATIO = 1.5 + + +# Helper function +def get_distance(cell_1, cell_2): + """ + Calculate the Euclidean distance between two positions + + used in trade.move() + """ + + x1, y1 = cell_1.coordinate + x2, y2 = cell_2.coordinate + dx = x1 - x2 + dy = y1 - y2 + return math.sqrt(dx**2 + dy**2) + + +class Trader(CellAgent): + """ + Trader: + - has a metabolism of sugar and spice + - harvest and trade sugar and spice to survive + """ + + def __init__( + self, + model, + cell, + sugar=0, + spice=0, + metabolism_sugar=0, + metabolism_spice=0, + vision=0, + ): + super().__init__(model) + self.cell = cell + self.sugar = sugar + self.spice = spice + self.metabolism_sugar = metabolism_sugar + self.metabolism_spice = metabolism_spice + self.vision = vision + self.prices = [] + self.trade_partners = [] + self.active_drive = None + + def get_trader(self, cell): + """ + helper function used in self.trade_with_neighbors() + """ + + for agent in cell.agents: + if isinstance(agent, Trader): + return agent + + def calculate_welfare(self, sugar, spice): + """ + helper function + + part 2 self.move() + self.trade() + """ + + # calculate total resources + m_total = self.metabolism_sugar + self.metabolism_spice + # Cobb-Douglas functional form; starting on p. 97 + # on Growing Artificial Societies + return sugar ** (self.metabolism_sugar / m_total) * spice ** ( + self.metabolism_spice / m_total + ) + + def is_starved(self): + """ + Helper function for self.maybe_die() + """ + + return (self.sugar <= 0) or (self.spice <= 0) + + def calculate_MRS(self, sugar, spice): + """ + Helper function for + - self.trade() + - self.maybe_self_spice() + + Determines what trader agent needs and can give up + """ + + return (spice / self.metabolism_spice) / (sugar / self.metabolism_sugar) + + def calculate_sell_spice_amount(self, price): + """ + helper function for self.maybe_sell_spice() which is called from + self.trade() + """ + + if price >= 1: + sugar = 1 + spice = int(price) + else: + sugar = int(1 / price) + spice = 1 + return sugar, spice + + def sell_spice(self, other, sugar, spice): + """ + used in self.maybe_sell_spice() + + exchanges sugar and spice between traders + """ + + self.sugar += sugar + other.sugar -= sugar + self.spice -= spice + other.spice += spice + + def maybe_sell_spice(self, other, price, welfare_self, welfare_other): + """ + helper function for self.trade() + """ + + sugar_exchanged, spice_exchanged = self.calculate_sell_spice_amount(price) + + # Assess new sugar and spice amount - what if change did occur + self_sugar = self.sugar + sugar_exchanged + other_sugar = other.sugar - sugar_exchanged + self_spice = self.spice - spice_exchanged + other_spice = other.spice + spice_exchanged + + # double check to ensure agents have resources + + if ( + (self_sugar <= 0) + or (other_sugar <= 0) + or (self_spice <= 0) + or (other_spice <= 0) + ): + return False + + # trade criteria #1 - are both agents better off? + both_agents_better_off = ( + welfare_self < self.calculate_welfare(self_sugar, self_spice) + ) and (welfare_other < other.calculate_welfare(other_sugar, other_spice)) + + # trade criteria #2 is their mrs crossing with potential trade + mrs_not_crossing = self.calculate_MRS( + self_sugar, self_spice + ) > other.calculate_MRS(other_sugar, other_spice) + + if not (both_agents_better_off and mrs_not_crossing): + return False + + # criteria met, execute trade + self.sell_spice(other, sugar_exchanged, spice_exchanged) + + return True + + def trade(self, other): + """ + helper function used in trade_with_neighbors() + + other is a trader agent object + """ + + # sanity check to verify code is working as expected + assert self.sugar > 0 + assert self.spice > 0 + assert other.sugar > 0 + assert other.spice > 0 + + # calculate marginal rate of substitution in Growing Artificial Societies p. 101 + mrs_self = self.calculate_MRS(self.sugar, self.spice) + mrs_other = other.calculate_MRS(other.sugar, other.spice) + + # calculate each agents welfare + welfare_self = self.calculate_welfare(self.sugar, self.spice) + welfare_other = other.calculate_welfare(other.sugar, other.spice) + + if math.isclose(mrs_self, mrs_other): + return + + # calculate price + price = math.sqrt(mrs_self * mrs_other) + + if mrs_self > mrs_other: + # self is a sugar buyer, spice seller + sold = self.maybe_sell_spice(other, price, welfare_self, welfare_other) + # no trade - criteria not met + if not sold: + return + else: + # self is a spice buyer, sugar seller + sold = other.maybe_sell_spice(self, price, welfare_other, welfare_self) + # no trade - criteria not met + if not sold: + return + + # Capture data + self.prices.append(price) + self.trade_partners.append(other.unique_id) + + # continue trading + self.trade(other) + + ###################################################################### + # # + # MAIN TRADE FUNCTIONS # + # # + ###################################################################### + + def move(self, mode="default", urgent_resource=None): + """ + - Movement logic now branches on mode. + - This is where the bulky step method creates issues and gets ugly - + - Every drive needs different cell scoring, all stuffed into one method + """ + + # 1. identify all possible moves + + neighboring_cells = [ + cell + for cell in self.cell.get_neighborhood(self.vision, include_center=True) + if cell.is_empty + ] + + if not neighboring_cells: + # all neighboring cells are occupied + return + + # 2. Score cells differently depending on mode + + if mode == "survive": + # Pure greedy: only care about the critical resource. + if urgent_resource == "sugar": + scores = [cell.sugar for cell in neighboring_cells] + else: + scores = [cell.spice for cell in neighboring_cells] + + elif mode == "gather_sugar": + # Two-pass: first filter to cells with the most sugar, + # then rank those by welfare as the tiebreaker. + max_sugar = max(cell.sugar for cell in neighboring_cells) + neighboring_cells = [ + cell for cell in neighboring_cells if cell.sugar == max_sugar + ] + scores = [ + self.calculate_welfare( + self.sugar + cell.sugar, + self.spice + cell.spice, + ) + for cell in neighboring_cells + ] + + elif mode == "gather_spice": + # Same two-pass logic used in gather sugar. + max_spice = max(cell.spice for cell in neighboring_cells) + neighboring_cells = [ + cell for cell in neighboring_cells if cell.spice == max_spice + ] + scores = [ + self.calculate_welfare( + self.sugar + cell.sugar, + self.spice + cell.spice, + ) + for cell in neighboring_cells + ] + + elif mode == "seek_trade": + """ + - Agent is comfortable but imbalanced + - it wants to find traders who have the opposite imbalance + - Score each cell by: base welfare + count of complementary + traders reachable from that cell within vision. + """ + sugar_ticks = self.sugar / self.metabolism_sugar + spice_ticks = self.spice / self.metabolism_spice + need_sugar = spice_ticks > sugar_ticks + need_spice = sugar_ticks > spice_ticks + + scores = [] + for cell in neighboring_cells: + base = self.calculate_welfare( + self.sugar + cell.sugar, + self.spice + cell.spice, + ) + # Count traders reachable from this cell who have complementary surplus + complementary_count = 0 + for agent in cell.get_neighborhood(radius=self.vision).agents: + if not isinstance(agent, Trader) or agent is self: + continue + other_sugar_t = agent.sugar / agent.metabolism_sugar + other_spice_t = agent.spice / agent.metabolism_spice + # I need sugar and and they have more sugar than spice + if (need_sugar and other_sugar_t > other_spice_t) or ( + need_spice and other_spice_t > other_sugar_t + ): + complementary_count += 1 + + # Each complementary trader adds the base welfare as bonus + # So a cell near 2 good partners scores 3x a cell near none + # Makes it proportional + scores.append(base * (1 + complementary_count)) + + else: + # Default: original welfare-maximising behaviour + scores = [ + self.calculate_welfare( + self.sugar + cell.sugar, + self.spice + cell.spice, + ) + for cell in neighboring_cells + ] + + # 3. Select best cell + + max_score = max(scores) + candidates = [ + cell + for cell, score in zip(neighboring_cells, scores) + if math.isclose(score, max_score) + ] + + min_dist = min(get_distance(self.cell, cell) for cell in candidates) + + final_candidates = [ + cell + for cell in candidates + if math.isclose(get_distance(self.cell, cell), min_dist, rel_tol=1e-02) + ] + + # random choice tiebreaker + self.cell = self.random.choice(final_candidates) + + def eat(self): + self.sugar += self.cell.sugar + self.cell.sugar = 0 + self.sugar -= self.metabolism_sugar + + self.spice += self.cell.spice + self.cell.spice = 0 + self.spice -= self.metabolism_spice + + def maybe_die(self): + """ + Function to remove Traders who have consumed all their sugar or spice + """ + + if self.is_starved(): + self.remove() + + def step(self): + """ + MONOLITH STEP: All drive logic crammed into one method. + + Working, but look at what happens: + - Adding a new drive means adding another elif branch here AND in move() + - Changing how one drive affects trading means editing deep in this method + - The interaction between urgency calculation and movement is implicit + - Duplicated patterns across branches (eat + maybe_die in every path) + """ + + self.prices = [] + self.trade_partners = [] + + # Compute urgency (ticks of reserves remaining) + sugar_ticks = self.sugar / self.metabolism_sugar + spice_ticks = self.spice / self.metabolism_spice + + # SURVIVAL: either resource is critically low + if sugar_ticks < CRITICAL_THRESHOLD or spice_ticks < CRITICAL_THRESHOLD: + self.active_drive = "survive" + if sugar_ticks < spice_ticks: + self.move(mode="survive", urgent_resource="sugar") + else: + self.move(mode="survive", urgent_resource="spice") + self.eat() + # No trading - survival is all that matters + self.maybe_die() + return + + # SEEK TRADE: both resources comfortable, but imbalanced + if ( + sugar_ticks > COMFORTABLE_THRESHOLD + and spice_ticks > COMFORTABLE_THRESHOLD + and ( + sugar_ticks / spice_ticks > IMBALANCE_RATIO + or spice_ticks / sugar_ticks > IMBALANCE_RATIO + ) + ): + self.active_drive = "seek_trade" + self.move(mode="seek_trade") + self.eat() + self.maybe_die() + return + + # GATHER SUGAR: sugar is the more urgent resource + if sugar_ticks < spice_ticks: + self.active_drive = "gather_sugar" + self.move(mode="gather_sugar") + self.eat() + self.maybe_die() + return + + # GATHER SPICE: spice is the more urgent resource + if spice_ticks < sugar_ticks: + self.active_drive = "gather_spice" + self.move(mode="gather_spice") + self.eat() + self.maybe_die() + return + + # BALANCED: resources roughly equal, so use default movement + self.active_drive = "default" + self.move(mode="default") + self.eat() + self.maybe_die() + return + + def trade_with_neighbors(self): + """ + Function for trader agents to decide who to trade with + Now drive aware: survival drive skips trading entirely. + This check is awkward here though since the drive was determined in step(), + we're coupling two methods via instance state set in a completely different place. + + Three steps when trading. + 1- identify neighbors who can trade + 2- trade (2 sessions) + 3- collect data + """ + # iterate through traders in neighboring cells and trade + if self.active_drive == "survive": + return + + for a in self.cell.get_neighborhood(radius=self.vision).agents: + self.trade(a) + + return diff --git a/examples/sugarscape/monolith/monolith_app.py b/examples/sugarscape/monolith/monolith_app.py new file mode 100644 index 000000000..54d85ab9f --- /dev/null +++ b/examples/sugarscape/monolith/monolith_app.py @@ -0,0 +1,97 @@ +from mesa.visualization import Slider, SolaraViz, SpaceRenderer, make_plot_component +from mesa.visualization.components import AgentPortrayalStyle, PropertyLayerStyle +from monolith_model import SugarscapeMonolith + + +def agent_portrayal(agent): + color_map = { + "survive": "black", + "gather_sugar": "green", + "gather_spice": "orange", + "seek_trade": "purple", + "default": "blue", + } + + return AgentPortrayalStyle( + x=agent.cell.coordinate[0], + y=agent.cell.coordinate[1], + color=color_map.get(agent.active_drive, "gray"), + marker="o", + size=10, + zorder=1, + ) + + +def property_layer_portrayal(layer): + if layer == "sugar": + return PropertyLayerStyle( + color="blue", alpha=0.8, colorbar=True, vmin=0, vmax=10 + ) + return PropertyLayerStyle(color="red", alpha=0.8, colorbar=True, vmin=0, vmax=10) + + +def post_process(chart): + chart = chart.properties(width=400, height=400) + return chart + + +model_params = { + "rng": { + "type": "InputText", + "value": 42, + "label": "Random Seed", + }, + "width": 50, + "height": 50, + # Population parameters + "initial_population": Slider( + "Initial Population", value=200, min=50, max=500, step=10 + ), + # Agent endowment parameters + "endowment_min": Slider("Min Initial Endowment", value=25, min=5, max=30, step=1), + "endowment_max": Slider("Max Initial Endowment", value=50, min=30, max=100, step=1), + # Metabolism parameters + "metabolism_min": Slider("Min Metabolism", value=1, min=1, max=3, step=1), + "metabolism_max": Slider("Max Metabolism", value=5, min=3, max=8, step=1), + # Vision parameters + "vision_min": Slider("Min Vision", value=1, min=1, max=3, step=1), + "vision_max": Slider("Max Vision", value=5, min=3, max=8, step=1), + # Trade parameter + "enable_trade": {"type": "Checkbox", "value": True, "label": "Enable Trading"}, +} + +model = SugarscapeMonolith() + +# Here, the renderer uses the Altair backend, while the plot components +# use the Matplotlib backend. +# Both can be mixed and matched to enhance the visuals of your model. +renderer = ( + SpaceRenderer(model, backend="altair") + .setup_agents(agent_portrayal) + .setup_propertylayer(property_layer_portrayal) +) +# Specifically, avoid drawing the grid to hide the grid lines. +renderer.draw_agents() +renderer.draw_propertylayer() + +renderer.post_process = post_process + +# Note: It is advised to switch the pages after pausing the model +# on the Solara dashboard. +page = SolaraViz( + model, + renderer, + components=[ + make_plot_component("#Traders", page=1), + make_plot_component("Price", page=1), + make_plot_component("Survive", page=2), + make_plot_component("Gather Sugar", page=2), + make_plot_component("Gather Spice", page=2), + make_plot_component("Seek Trade", page=2), + make_plot_component("Default", page=2), + ], + model_params=model_params, + name="Sugarscape {G1, M, T}", + play_interval=150, +) +page # noqa diff --git a/examples/sugarscape/monolith/monolith_model.py b/examples/sugarscape/monolith/monolith_model.py new file mode 100644 index 000000000..59c7ad7b7 --- /dev/null +++ b/examples/sugarscape/monolith/monolith_model.py @@ -0,0 +1,118 @@ +from pathlib import Path + +import mesa +import numpy as np +from mesa.discrete_space import OrthogonalVonNeumannGrid +from mesa.discrete_space.property_layer import PropertyLayer + +# Import from LOCAL agents file, not Mesa's built-in +from monolith_agents import Trader + +# Helper Functions + + +def flatten(list_of_lists): + return [item for sublist in list_of_lists for item in sublist] + + +def geometric_mean(list_of_prices): + if len(list_of_prices) == 0: + return -1 + return np.exp(np.log(list_of_prices).mean()) + + +def drive_count(model, drive_name): + """Count how many agents have a given active drive.""" + return sum(1 for a in model.agents if a.active_drive == drive_name) + + +class SugarscapeMonolith(mesa.Model): + def __init__( + self, + width=50, + height=50, + initial_population=200, + endowment_min=25, + endowment_max=50, + metabolism_min=1, + metabolism_max=5, + vision_min=1, + vision_max=5, + enable_trade=True, + rng=None, + ): + super().__init__(rng=rng) + self.width = width + self.height = height + self.enable_trade = enable_trade + self.running = True + + # Initiate grid + self.grid = OrthogonalVonNeumannGrid( + (self.width, self.height), torus=False, random=self.random + ) + + # Read in landscape file + self.sugar_distribution = np.genfromtxt(Path(__file__).parent / "sugar-map.txt") + self.spice_distribution = np.flip(self.sugar_distribution, 1) + + sugar_layer = PropertyLayer("sugar", (self.width, self.height)) + sugar_layer.data[:] = self.sugar_distribution + self.grid.add_property_layer(sugar_layer) + + spice_layer = PropertyLayer("spice", (self.width, self.height)) + spice_layer.data[:] = self.spice_distribution + self.grid.add_property_layer(spice_layer) + + # DataCollector — original reporters plus drive distribution + self.datacollector = mesa.DataCollector( + model_reporters={ + "#Traders": lambda m: len(m.agents), + "Trade Volume": lambda m: sum(len(a.trade_partners) for a in m.agents), + "Price": lambda m: geometric_mean( + flatten([a.prices for a in m.agents]) + ), + # Drive distribution — this is the new data + "Survive": lambda m: drive_count(m, "survive"), + "Gather Sugar": lambda m: drive_count(m, "gather_sugar"), + "Gather Spice": lambda m: drive_count(m, "gather_spice"), + "Seek Trade": lambda m: drive_count(m, "seek_trade"), + "Default": lambda m: drive_count(m, "default"), + }, + ) + + # Create trader agents + n = initial_population + Trader.create_agents( + self, + n, + self.random.choices(self.grid.all_cells.cells, k=n), + sugar=self.rng.integers(endowment_min, endowment_max, (n,), endpoint=True), + spice=self.rng.integers(endowment_min, endowment_max, (n,), endpoint=True), + metabolism_sugar=self.rng.integers( + metabolism_min, metabolism_max, (n,), endpoint=True + ), + metabolism_spice=self.rng.integers( + metabolism_min, metabolism_max, (n,), endpoint=True + ), + vision=self.rng.integers(vision_min, vision_max, (n,), endpoint=True), + ) + + def step(self): + # Regrow sugar and spice (1 unit per tick, up to max) + self.grid.sugar.data[:] = np.minimum( + self.grid.sugar.data + 1, self.sugar_distribution + ) + self.grid.spice.data[:] = np.minimum( + self.grid.spice.data + 1, self.spice_distribution + ) + + # Step trader agents + self.agents.shuffle_do("step") + if self.enable_trade: + self.agents.shuffle_do("trade_with_neighbors") + self.datacollector.collect(self) + + def run_model(self, step_count=1000): + for _ in range(step_count): + self.step() diff --git a/examples/sugarscape/monolith/monolith_results.csv b/examples/sugarscape/monolith/monolith_results.csv new file mode 100644 index 000000000..cae22a8d7 --- /dev/null +++ b/examples/sugarscape/monolith/monolith_results.csv @@ -0,0 +1,501 @@ +,#Traders,Trade Volume,Price,Survive,Gather Sugar,Gather Spice,Seek Trade,Default +0,200,980,0.9695541886122512,0,50,37,111,2 +1,200,202,0.8639668299211828,0,65,63,51,21 +2,200,136,0.8903786973264998,0,61,75,37,27 +3,200,66,0.8724249577783588,0,62,88,32,18 +4,200,74,0.9236989424216557,2,69,83,27,19 +5,200,59,1.0671883607131032,6,76,72,30,16 +6,198,95,0.8800237682184607,10,69,69,36,14 +7,197,109,0.8016710161435471,15,59,63,40,20 +8,194,124,0.8168905579554807,20,59,62,37,16 +9,189,98,0.7483547097949468,25,62,59,28,15 +10,185,102,0.9352204649863316,26,61,60,24,14 +11,176,101,0.9516176799894672,21,57,59,22,17 +12,170,78,0.913617999363404,25,58,53,21,13 +13,165,116,0.8053383071355519,24,52,52,19,18 +14,159,69,0.901990193723107,27,59,46,17,10 +15,153,88,0.8753340364847928,22,54,47,15,15 +16,148,60,0.6937999879797999,23,52,46,12,15 +17,143,68,0.8397475280651951,24,38,51,13,17 +18,139,68,0.7567335899981324,21,41,50,10,17 +19,137,44,0.9690487386710579,20,34,55,13,15 +20,131,48,0.7790037312315296,23,36,47,13,12 +21,125,81,0.8681279020668465,18,35,43,13,16 +22,121,51,0.8290560718218017,12,30,53,14,12 +23,119,51,1.0403817510846234,14,34,46,11,14 +24,118,48,0.7334586716870022,15,30,45,13,15 +25,115,59,0.6983973781091087,10,34,44,14,13 +26,114,39,0.8364808754386297,11,26,46,13,18 +27,112,43,0.8892431085982195,11,24,47,13,17 +28,112,38,0.9292468453037278,14,23,45,12,18 +29,111,37,0.9113140246884127,14,25,43,10,19 +30,110,42,0.8904774675991058,15,26,40,9,20 +31,110,28,0.8436086003126929,18,28,37,8,19 +32,108,33,0.8324326125003965,17,32,36,8,15 +33,106,39,0.7904130692055252,15,30,41,7,13 +34,102,37,0.700145070819974,11,28,35,9,19 +35,100,30,0.8026334029016838,10,31,36,10,13 +36,100,43,0.8406810120211833,11,21,39,9,20 +37,98,23,0.9126118231741752,9,25,43,7,14 +38,96,30,0.9637727640196889,7,34,33,7,15 +39,95,21,0.8950458659712212,6,37,27,6,19 +40,94,25,0.9970824789412472,8,29,34,6,17 +41,93,33,1.0360710804187203,7,29,32,7,18 +42,93,21,0.9417333841876583,8,34,28,5,18 +43,92,23,0.9258898640063702,7,33,34,6,12 +44,92,44,0.7337384907455909,7,37,30,7,11 +45,91,56,0.9348569179570407,5,38,21,8,19 +46,91,24,0.9453728382736597,7,38,25,3,18 +47,90,12,1.010511315577021,6,35,27,3,19 +48,88,22,0.9185017525836001,4,35,27,3,19 +49,88,18,0.9953846404811862,6,32,27,3,20 +50,87,11,0.9734565134181481,5,37,26,3,16 +51,87,16,0.9930129112918878,6,38,25,4,14 +52,86,30,1.1465277693658373,5,31,29,6,15 +53,86,23,1.0565454567624577,5,37,22,6,16 +54,85,16,1.0004427217835787,4,40,22,4,15 +55,85,17,0.9264057689491512,6,37,27,4,11 +56,84,24,1.1699626015157538,5,31,31,5,12 +57,84,15,1.0759001929436194,6,33,26,5,14 +58,84,30,0.9417084790802316,6,36,24,4,14 +59,83,21,1.0331964592797094,8,29,36,2,8 +60,83,15,1.0682614420821137,9,30,31,3,10 +61,83,21,1.0955423340198873,9,33,28,3,10 +62,82,18,0.955800087602722,8,38,23,3,10 +63,82,31,0.8944593025982064,8,29,27,3,15 +64,82,22,0.9805803700469735,9,24,29,4,16 +65,82,18,0.9100089530248903,9,30,24,4,15 +66,81,13,0.9769860480015199,8,31,22,4,16 +67,79,25,0.9197634049084813,6,33,22,5,13 +68,77,16,0.9606059410458869,4,29,26,4,14 +69,77,21,0.990064841779009,4,31,29,4,9 +70,76,18,1.0689302371341267,3,28,26,4,15 +71,76,23,0.9969190676323836,3,32,25,4,12 +72,75,17,1.0615896198495338,2,35,20,3,15 +73,75,15,1.0109180591015114,2,38,20,3,12 +74,74,16,1.0396856881172623,2,38,20,4,10 +75,74,13,1.0583848708686883,2,36,22,3,11 +76,74,15,1.125284133306914,2,32,22,4,14 +77,74,7,1.15013480701751,2,32,21,4,15 +78,74,20,1.1812736895477884,2,36,22,5,9 +79,73,12,1.1441736734613073,2,37,17,4,13 +80,73,9,1.0972472625734064,2,33,22,5,11 +81,73,25,1.0597844834325911,2,24,27,6,14 +82,72,12,1.0056067871328054,1,34,20,4,13 +83,72,5,0.9437174303224183,1,31,24,4,12 +84,72,17,0.8310230822089045,1,25,31,4,11 +85,72,9,0.8778523745824447,2,29,25,4,12 +86,72,10,1.0251410255532623,2,27,24,5,14 +87,72,10,1.1238672543462584,2,26,25,5,14 +88,72,19,0.856241595938209,2,30,21,5,14 +89,72,10,0.9695492515446638,2,33,23,4,10 +90,72,13,0.9625432164437978,2,31,20,5,14 +91,72,9,0.9622553866840715,2,35,23,4,8 +92,72,12,1.0365368140852624,2,34,24,5,7 +93,72,9,0.9119042245348006,3,35,22,4,8 +94,71,12,1.090397146857565,2,27,25,4,13 +95,71,10,0.9017399790752213,3,28,28,5,7 +96,71,8,0.9600744721397457,3,23,31,5,9 +97,71,21,0.9676066268451651,3,18,26,6,18 +98,71,6,1.0464587657046729,3,28,26,4,10 +99,71,11,0.8072224189939955,3,29,27,5,7 +100,70,5,0.9544956053455158,2,25,28,4,11 +101,70,20,0.8426100268897777,2,18,33,7,10 +102,70,7,0.7658902983427561,2,25,29,5,9 +103,70,20,0.7209581322956978,2,23,32,5,8 +104,70,5,0.7893739756844307,2,23,30,7,8 +105,70,24,0.8339137380907214,3,20,23,7,17 +106,70,13,0.9525882091852954,2,24,29,6,9 +107,70,17,0.823744218887374,2,26,29,6,7 +108,70,6,1.0247203584255333,2,25,33,5,5 +109,69,14,0.8494732831081573,2,20,27,5,15 +110,68,11,0.9013628112534621,1,21,26,5,15 +111,68,6,0.7241610154098937,1,22,30,5,10 +112,68,9,0.9595829158000053,2,23,27,5,11 +113,68,12,0.9855906424588278,2,23,29,5,9 +114,68,18,0.710496548361153,2,23,29,6,8 +115,68,6,0.999079932100387,2,22,27,7,10 +116,68,17,0.7936531294110976,2,24,23,7,12 +117,68,9,0.9618203974707863,2,20,26,6,14 +118,68,9,0.8270065308714547,2,20,29,6,11 +119,68,7,0.870849889248114,2,17,33,5,11 +120,68,7,0.9221389971616064,3,16,33,5,11 +121,68,9,0.6657616917991879,3,15,32,4,14 +122,68,12,0.8307139014614177,3,17,29,5,14 +123,68,10,0.9153475982774731,3,17,32,4,12 +124,68,9,0.7705375317948944,3,14,34,4,13 +125,67,8,0.9632589219888806,2,12,38,4,11 +126,67,9,0.7445632265505246,2,20,32,4,9 +127,67,9,0.8700344636340293,2,15,38,4,8 +128,67,5,0.9588056277999073,2,13,33,4,15 +129,67,9,0.805793481114504,2,13,36,4,12 +130,67,10,0.8668831420157401,2,17,32,5,11 +131,67,4,1.0349132025883996,2,11,36,5,13 +132,67,17,0.49831921542227836,2,18,33,5,9 +133,67,21,0.7917259843293558,2,13,36,4,12 +134,67,9,0.9313380199592485,2,13,34,4,14 +135,66,12,0.7405495484770029,1,13,34,4,14 +136,66,8,0.769370864075394,1,15,33,4,13 +137,66,4,0.8560050995355535,1,17,30,5,13 +138,66,5,0.8511511490085676,1,19,32,4,10 +139,66,3,0.7926302067722184,1,18,34,4,9 +140,66,14,0.9081095300521955,1,18,32,4,11 +141,66,5,0.62920816881518,1,12,37,3,13 +142,66,8,0.6194636068660634,1,16,34,3,12 +143,66,4,0.6541965251995474,1,17,31,4,13 +144,66,7,0.9573924344034084,1,18,33,4,10 +145,66,11,0.6893962664581128,1,16,33,4,12 +146,66,8,0.6792427760068345,1,10,39,4,12 +147,66,9,0.81522723437702,2,14,31,5,14 +148,66,10,0.9278945713523697,2,10,37,4,13 +149,66,18,0.7491177681853298,1,10,42,4,9 +150,66,9,0.7606942197625474,1,11,39,4,11 +151,66,9,0.896937066010175,2,10,34,4,16 +152,66,19,0.8222707399147418,2,13,29,4,18 +153,66,10,0.8287520846414914,2,17,30,4,13 +154,66,11,0.7282624428362736,4,16,32,4,10 +155,66,19,0.808442873641553,4,16,30,4,12 +156,66,10,0.836792400289995,4,14,29,4,15 +157,66,16,0.4814582125673673,4,19,30,4,9 +158,66,23,1.4485021063691697,5,18,27,4,12 +159,66,23,0.9218204067598711,4,18,29,5,10 +160,66,17,0.8959930715310808,4,14,29,5,14 +161,66,11,0.8747902073882081,4,18,27,3,14 +162,66,8,0.7103079768859455,4,16,29,3,14 +163,66,7,0.7325701852539763,4,14,35,3,10 +164,66,7,0.6988265049781311,4,17,36,3,6 +165,65,11,0.7648671303105258,3,15,36,3,8 +166,65,8,0.7036176632725029,3,14,37,3,8 +167,65,12,0.7841446763555965,3,12,38,3,9 +168,65,14,0.7686040458459801,4,11,40,4,6 +169,65,19,0.7090401968590396,4,9,34,4,14 +170,65,12,0.7283092279458229,4,10,38,4,9 +171,65,14,0.8113008886758315,4,10,36,4,11 +172,65,9,0.7621912299194998,4,9,39,4,9 +173,65,13,0.8040460038720617,4,11,35,4,11 +174,64,21,1.2990437927809262,3,7,36,4,14 +175,64,13,0.919653224509448,3,8,38,6,9 +176,64,26,0.7875838391420792,3,15,25,6,15 +177,64,16,0.8172498517112435,3,17,27,4,13 +178,64,15,0.8419230587138152,3,16,29,5,11 +179,64,10,0.9501040995545881,3,16,28,5,12 +180,64,12,0.8327647174186803,3,21,22,5,13 +181,64,5,0.94086151186703,3,21,23,4,13 +182,64,12,0.7852826229699338,3,18,27,3,13 +183,64,14,0.911329820594796,3,19,27,6,9 +184,64,14,0.8589198284696988,3,18,31,5,7 +185,64,11,0.9234743587650266,3,18,27,5,11 +186,64,9,0.8804049919409992,3,20,27,4,10 +187,64,9,0.8820877219984765,3,17,30,4,10 +188,64,9,1.0031968965757236,3,17,31,4,9 +189,63,9,0.8794598708179843,2,21,24,5,11 +190,63,12,0.9230472761388089,2,13,28,5,15 +191,63,8,0.8662464038347542,2,11,29,5,16 +192,63,9,0.9673915912309863,2,9,34,5,13 +193,63,35,0.4896668231795937,2,13,29,5,14 +194,63,11,0.8393899680757015,2,12,27,6,16 +195,63,12,0.7632372757729251,2,12,28,6,15 +196,63,15,0.8487283197345016,2,13,30,7,11 +197,62,9,0.8986614645735637,1,10,31,7,13 +198,62,10,0.7953797929866174,1,12,30,6,13 +199,62,13,0.8338941006156718,1,15,28,6,12 +200,62,11,0.7368617429640485,1,11,30,6,14 +201,62,11,0.8282814266689984,1,14,32,6,9 +202,62,10,0.779507905387954,1,14,31,6,10 +203,62,11,0.800306130039198,1,16,32,6,7 +204,62,7,0.8272603401450684,1,12,32,6,11 +205,62,6,0.8405828768984811,1,16,26,6,13 +206,62,11,0.878118521617146,1,15,28,5,13 +207,62,10,0.8257501649070785,1,14,33,5,9 +208,62,13,0.8820966857770429,1,14,35,6,6 +209,62,10,0.8389719428788389,1,18,26,5,12 +210,62,14,0.8786031668183314,1,14,31,5,11 +211,62,36,0.5355086822069347,1,10,37,4,10 +212,62,13,0.8555372713106622,1,16,33,5,7 +213,62,14,0.8625525388487923,1,19,28,5,9 +214,62,14,0.7869565461265272,1,15,34,5,7 +215,62,16,0.8827494907838586,1,18,31,5,7 +216,62,17,0.8784619373872595,1,23,27,5,6 +217,62,11,0.8465366299522167,1,21,21,4,15 +218,62,14,0.8471650584842898,1,21,29,4,7 +219,62,10,0.8930039542322964,1,21,28,5,7 +220,62,11,0.9146905774684606,1,19,30,4,8 +221,62,9,0.8090291308620765,1,22,24,3,12 +222,62,6,0.9363349239641912,1,22,30,3,6 +223,62,8,0.9268290092265694,1,24,29,3,5 +224,62,8,0.8766614479687924,1,28,23,3,7 +225,62,14,0.9715747256034886,1,26,24,3,8 +226,62,4,0.8615861413840707,1,22,30,3,6 +227,62,6,0.9624585358765149,1,21,26,4,10 +228,62,9,0.8741993047176727,1,23,25,4,9 +229,62,7,0.9526591144369985,1,21,29,3,8 +230,62,12,0.9915884745555297,1,22,31,3,5 +231,62,14,0.887052888479994,1,24,29,3,5 +232,62,11,0.9743612891731043,1,22,30,3,6 +233,62,12,1.011365025929918,1,26,26,3,6 +234,62,9,0.9871674930662923,1,24,28,3,6 +235,62,10,0.9950948005714982,1,30,20,3,8 +236,62,7,1.0427228873366376,1,27,22,3,9 +237,62,9,0.9699777228149434,1,29,21,3,8 +238,62,9,0.9674283436837733,1,29,22,3,7 +239,62,8,1.0232032273171907,1,28,23,3,7 +240,62,6,1.0370172097829133,1,23,24,3,11 +241,62,8,1.0266699060757747,1,27,27,3,4 +242,62,12,0.9755626230200536,1,23,29,3,6 +243,62,15,1.0096178255964274,1,23,25,3,10 +244,62,5,0.9940753013645087,1,24,23,3,11 +245,62,10,0.9987755963592061,1,25,22,3,11 +246,62,8,0.9606233505653716,1,23,24,3,11 +247,62,7,1.0341085164882073,1,24,27,3,7 +248,62,17,0.9447682388770011,1,28,16,3,14 +249,62,6,0.9364102535850114,1,20,21,3,17 +250,62,13,0.9127285653991675,1,22,28,3,8 +251,62,15,0.9089516315724506,1,20,32,3,6 +252,62,8,1.003775060514536,1,22,31,3,5 +253,62,9,1.0017690577228744,1,23,30,3,5 +254,61,7,0.9945849274607702,0,17,32,3,9 +255,61,8,0.9216699120046453,0,19,29,3,10 +256,61,15,0.8600316697276849,0,24,27,4,6 +257,61,10,0.8680290369479794,0,13,30,4,14 +258,61,9,0.8903154262746493,0,9,36,4,12 +259,61,12,0.8633645829798327,0,18,34,4,5 +260,61,11,0.8860026839943976,0,13,32,5,11 +261,61,11,0.9388004161450699,0,11,37,5,8 +262,61,8,0.925263107368708,0,13,31,5,12 +263,61,8,0.9843876703843601,0,21,26,5,9 +264,61,9,0.9355523221842672,0,18,28,5,10 +265,61,10,0.9524707318765169,0,17,27,6,11 +266,61,10,0.8359724360701554,0,19,25,6,11 +267,61,9,0.9903577115813577,0,19,21,6,15 +268,61,11,0.8793011306997427,0,17,26,6,12 +269,61,11,1.0305451621198927,0,21,23,6,11 +270,61,16,0.8967922525373024,0,22,22,6,11 +271,61,7,0.9665940220526099,0,18,28,6,9 +272,61,10,0.9476404702583112,0,20,28,6,7 +273,61,14,0.967339525464172,0,19,31,6,5 +274,61,9,0.98316756657059,0,15,33,6,7 +275,61,7,0.9923392745176816,0,16,29,6,10 +276,61,9,0.943812205231249,0,22,24,6,9 +277,61,7,0.9540023185355769,0,23,25,6,7 +278,61,9,0.9419539795332544,0,20,29,6,6 +279,61,13,0.9279089589291961,0,17,30,6,8 +280,61,13,0.9129794793317664,0,17,27,6,11 +281,61,10,0.9371717510661002,0,22,19,6,14 +282,61,8,1.0113824009207932,0,24,22,6,9 +283,61,9,1.0166093959999385,0,21,26,6,8 +284,61,8,0.8987861243298982,0,22,23,6,10 +285,61,11,0.9881752897537552,0,20,20,6,15 +286,61,10,0.8385566531590442,0,17,25,6,13 +287,61,13,0.9070942404302241,0,25,19,6,11 +288,61,11,1.0036982270172303,0,22,26,5,8 +289,61,5,0.9622856459432798,0,18,26,5,12 +290,61,8,0.9301471481736553,0,16,28,6,11 +291,61,11,0.9444762446556758,0,22,20,6,13 +292,61,5,0.9287665325505658,0,24,22,7,8 +293,61,6,0.9165508961250693,0,22,28,6,5 +294,61,9,0.9758707345340716,0,17,29,6,9 +295,61,8,0.7251632759681735,0,19,23,6,13 +296,61,5,0.9645542128990069,0,14,26,6,15 +297,61,6,0.7948225603237324,0,16,29,6,10 +298,61,5,0.945171719484478,0,16,25,6,14 +299,61,5,0.8225676232299487,0,18,26,6,11 +300,61,12,0.9722412732859372,0,19,23,6,13 +301,61,6,0.8726170932045697,0,12,27,5,17 +302,61,16,1.0608215235205443,0,13,28,6,14 +303,61,10,0.8008041124254885,0,17,28,5,11 +304,61,6,0.900056090131828,0,21,28,5,7 +305,61,10,0.8789278937222162,0,17,32,5,7 +306,61,9,0.8248386486839302,0,13,31,6,11 +307,61,11,0.8614893918907546,0,20,26,6,9 +308,61,19,0.8631122555602768,0,16,29,5,11 +309,61,16,0.9177283746378742,0,22,25,4,10 +310,61,6,0.9126041870554631,0,23,24,4,10 +311,61,11,0.7833888180855646,0,20,24,4,13 +312,61,6,0.8084175659259422,0,17,28,4,12 +313,61,13,0.883810183374337,0,19,26,4,12 +314,61,19,0.871215581994595,0,22,25,4,10 +315,61,11,0.8827275394834054,0,18,28,4,11 +316,61,8,0.8614125447562312,0,18,24,4,15 +317,61,6,0.9780422005862063,0,20,25,3,13 +318,61,29,0.9401298644862323,0,22,27,3,9 +319,61,8,0.8252336700054268,0,28,21,4,8 +320,61,11,0.8273433365598741,0,24,23,4,10 +321,61,9,0.9599365215888263,0,21,27,4,9 +322,61,82,0.5719791609870449,0,28,24,4,5 +323,61,16,0.8923468113622102,0,17,30,3,11 +324,61,9,0.9637729405578259,0,19,24,4,14 +325,61,11,0.9516848675440348,0,26,22,4,9 +326,61,9,0.9792407344004045,0,25,25,4,7 +327,61,8,0.8718802715538081,0,23,23,4,11 +328,61,9,0.9227195663955019,0,25,21,4,11 +329,61,7,1.0400501791471437,0,22,27,3,9 +330,61,6,1.0193261303097576,0,24,25,3,9 +331,61,4,0.9972988954055292,0,23,27,3,8 +332,61,5,1.0588095964817092,0,22,27,3,9 +333,61,5,1.055520440365357,0,25,28,3,5 +334,61,2,0.972996665394705,0,26,25,3,7 +335,61,4,0.9974612158997964,0,24,26,3,8 +336,61,4,0.995986628305449,0,24,27,3,7 +337,61,7,0.9880100649005091,0,28,25,3,5 +338,61,13,1.0053615263856528,0,25,30,3,3 +339,61,9,0.9965460085831526,0,26,25,4,6 +340,61,7,1.0145287083986576,0,26,24,4,7 +341,61,6,0.9565890247305524,0,26,24,4,7 +342,61,4,0.9734810796737373,0,28,21,4,8 +343,61,7,0.9962042839193316,0,26,27,4,4 +344,61,3,1.014735934865319,0,25,27,4,5 +345,61,3,0.9681767411076033,0,22,26,5,8 +346,61,3,0.9042063191710837,0,22,26,6,7 +347,61,3,1.0177110351757852,0,22,27,6,6 +348,61,6,0.8956835879344096,0,22,24,7,8 +349,61,11,1.0736699413759228,0,24,28,7,2 +350,61,8,0.988156032334147,0,28,26,5,2 +351,61,8,0.8996863127887201,0,25,25,5,6 +352,61,8,0.9841416537567217,0,25,27,4,5 +353,61,25,0.40580931951739524,0,25,24,4,8 +354,61,4,0.9880399747498579,0,27,27,5,2 +355,61,4,0.9970079876570839,0,23,25,5,8 +356,61,18,0.5640824926853587,0,22,23,5,11 +357,61,6,1.0765602679187736,0,23,24,6,8 +358,61,3,1.044875623000993,0,22,28,6,5 +359,61,34,0.7982240469999682,0,21,25,6,9 +360,61,3,0.9813333384277663,0,18,28,5,10 +361,61,17,0.7164897494906433,0,22,27,6,6 +362,61,65,0.4980497495820414,0,23,26,7,5 +363,61,40,0.8133346787652489,0,17,26,8,10 +364,61,8,0.918600965978649,0,20,25,9,7 +365,61,2,1.0513729304695991,0,24,25,8,4 +366,61,7,0.9487206252502843,0,19,24,8,10 +367,61,9,0.7914407929754183,0,18,21,9,13 +368,61,5,0.9310823227153566,0,15,23,9,14 +369,61,16,0.9692165985343546,0,15,26,9,11 +370,61,5,0.8466147343843301,0,15,24,9,13 +371,61,5,0.9089733994513092,0,18,25,8,10 +372,61,6,0.7767578263832117,0,16,22,9,14 +373,61,11,0.6382973720943086,0,12,30,8,11 +374,61,8,0.8104933157281452,0,13,27,8,13 +375,61,8,0.8340245083902931,0,18,27,8,8 +376,61,8,0.8484438249631698,0,14,30,8,9 +377,61,11,0.8637497633873318,0,16,29,8,8 +378,61,18,0.5139188548461124,0,15,27,8,11 +379,61,13,0.8993534198804022,0,12,30,8,11 +380,61,7,0.8856113484282652,0,13,29,8,11 +381,61,10,0.8754302419383468,0,19,24,8,10 +382,61,11,0.8040148247718321,0,12,29,8,12 +383,61,11,0.8480788629222907,0,12,31,8,10 +384,61,8,0.8150301365175987,0,13,31,8,9 +385,61,10,0.832285164904269,0,17,27,8,9 +386,61,10,0.9067249111573285,0,13,26,9,13 +387,61,8,0.8561613792587798,0,13,25,9,14 +388,61,7,0.8637524542518447,0,14,23,9,15 +389,61,6,0.6791976018905226,0,14,25,9,13 +390,61,6,0.784937864456383,0,12,26,9,14 +391,61,10,0.7978124994826148,0,14,28,9,10 +392,61,7,0.7882681054935003,0,14,27,9,11 +393,61,6,0.832361075619152,0,13,34,9,5 +394,61,9,0.770892886224269,0,15,28,9,9 +395,61,15,0.8163106616307632,0,12,30,9,10 +396,61,7,0.7948146882554168,0,11,32,9,9 +397,61,10,0.7858282434618337,0,16,27,9,9 +398,61,8,0.7471778719559247,0,16,29,8,8 +399,61,9,0.7165960555760541,0,17,26,8,10 +400,61,13,0.8099833443246319,0,20,26,8,7 +401,61,11,0.8680903392108623,0,10,30,9,12 +402,61,11,0.783985594151362,0,17,27,9,8 +403,61,12,0.8357298567831717,0,16,29,8,8 +404,61,13,0.8710174634421118,0,15,30,8,8 +405,61,14,0.9236366450056672,0,18,25,7,11 +406,61,16,0.7681082334021295,0,18,30,7,6 +407,61,18,0.8430303719862942,0,20,29,6,6 +408,61,15,0.8490583965880014,0,12,34,5,10 +409,61,15,0.8993107538990918,0,14,32,5,10 +410,61,18,0.888224418612042,0,16,31,5,9 +411,61,20,0.9375762745556058,0,14,35,5,7 +412,61,17,0.8439584977531759,0,16,34,5,6 +413,61,22,0.895029172476974,0,16,29,5,11 +414,61,12,0.8776040907301939,0,21,28,5,7 +415,61,17,0.9084641921930403,0,22,27,5,7 +416,61,18,0.9466943331232511,0,24,20,5,12 +417,61,16,0.9749985742860695,0,24,22,5,10 +418,61,15,0.7547962704169277,0,27,21,5,8 +419,61,10,0.9411662087230916,0,24,24,5,8 +420,61,10,0.8391523670565645,0,25,23,5,8 +421,61,16,0.8864227515672282,0,26,23,5,7 +422,61,8,0.9009141489394181,0,28,21,5,7 +423,61,11,0.8588048815314532,0,25,23,5,8 +424,61,7,0.8791220673184573,0,22,20,5,14 +425,61,9,0.8750652697464587,0,25,22,5,9 +426,61,9,0.9830257256281756,0,25,20,5,11 +427,61,6,0.9489291933524885,0,24,23,5,9 +428,61,10,1.0332695926302717,0,23,26,5,7 +429,61,21,0.9456221858391988,0,23,25,5,8 +430,61,20,0.6835634609850209,0,19,27,5,10 +431,61,11,0.9564332845104432,0,17,30,5,9 +432,61,12,1.0211825167299642,0,18,29,5,9 +433,61,10,0.9576259944640786,0,16,26,5,14 +434,61,20,1.004146851859322,0,17,30,5,9 +435,61,7,1.016834792565927,0,18,29,5,9 +436,61,13,1.0603130658458462,0,18,28,5,10 +437,61,15,1.0655825016203329,0,19,27,5,10 +438,61,59,0.5185886456942828,0,23,23,5,10 +439,61,8,0.9782986995680271,0,25,22,6,8 +440,61,8,0.9979365608203781,0,25,26,6,4 +441,61,16,0.950354256977675,0,21,28,6,6 +442,61,5,1.0444811133517158,0,23,22,6,10 +443,61,6,0.9460387540761029,0,22,28,6,5 +444,61,12,0.9804544350015174,0,19,30,6,6 +445,61,11,0.9630715310999961,0,22,24,6,9 +446,61,15,0.8883591049719766,0,20,31,6,4 +447,61,10,0.9354585793140578,0,16,33,6,6 +448,61,15,0.8378019252597224,0,22,29,6,4 +449,61,10,0.9529013464581185,0,16,30,6,9 +450,61,20,0.8167670939835241,0,19,29,6,7 +451,61,9,0.868312754640112,0,16,30,6,9 +452,61,10,0.8115772899801695,0,14,23,6,18 +453,61,11,0.8142766472403962,0,14,30,7,10 +454,61,5,0.9081309792612784,0,14,31,7,9 +455,61,9,0.9240430440831584,0,11,32,6,12 +456,61,23,0.8095926700955256,0,16,25,6,14 +457,61,10,0.8512303278177996,0,15,31,6,9 +458,61,15,0.8901479369522551,0,13,32,5,11 +459,61,14,0.9193326117586044,0,13,34,5,9 +460,61,11,0.914100591557483,0,15,36,6,4 +461,61,18,0.9623370879299301,0,12,32,7,10 +462,61,12,0.9165402561244533,0,15,30,6,10 +463,61,9,0.9774891401218955,0,22,29,5,5 +464,61,10,0.9709223766575259,0,25,22,6,8 +465,61,16,0.4138110086187985,0,25,19,6,11 +466,61,5,0.8280819308664669,0,22,22,6,11 +467,61,5,0.8510050299460804,0,22,22,6,11 +468,61,12,0.9628252985111002,0,24,23,6,8 +469,61,14,0.983193298867409,0,23,26,6,6 +470,61,46,0.3683519894296203,0,23,21,6,11 +471,61,8,0.8465562113756667,0,20,24,6,11 +472,61,10,1.0222427281476385,0,23,22,7,9 +473,61,10,1.0443333682621163,0,24,20,7,10 +474,61,19,0.8965896256878927,1,24,23,7,6 +475,61,11,0.6763986239617107,1,20,26,6,8 +476,61,9,0.7354260543303232,1,18,27,6,9 +477,61,9,0.8225355232484135,1,14,29,6,11 +478,61,5,0.9498310639334263,1,13,29,6,12 +479,61,13,0.6915148118326456,1,18,31,6,5 +480,61,14,0.41389195624676217,1,14,29,6,11 +481,61,10,0.6951596814999899,0,12,29,6,14 +482,61,16,0.573071006940218,0,12,36,5,8 +483,61,12,0.9477602212033371,0,15,35,5,6 +484,61,9,0.8930395941671068,0,12,40,5,4 +485,61,15,0.8960754363109645,0,14,38,5,4 +486,61,23,0.8657897717709959,0,10,36,5,10 +487,61,12,0.8838276708256834,0,14,38,5,4 +488,61,18,0.8385000563747435,0,18,29,6,8 +489,61,12,0.856914418295363,0,15,28,6,12 +490,61,14,0.8731997283588189,0,16,31,6,8 +491,61,12,0.8516962276232792,0,16,32,6,7 +492,61,11,0.8268134694862844,0,15,29,6,11 +493,61,15,0.8289216429975359,0,13,30,6,12 +494,61,89,0.48387456005975965,0,16,35,5,5 +495,61,12,0.8301890722523574,0,14,35,6,6 +496,61,11,0.8020044491933569,0,15,33,6,7 +497,61,13,0.8982338517703471,0,15,33,7,6 +498,61,12,0.8081601393838407,0,14,29,7,11 +499,61,10,0.9227660816084289,0,16,28,6,11 diff --git a/examples/sugarscape/monolith/sugar-map.txt b/examples/sugarscape/monolith/sugar-map.txt new file mode 100644 index 000000000..3a1ed84bd --- /dev/null +++ b/examples/sugarscape/monolith/sugar-map.txt @@ -0,0 +1,50 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 +0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 +0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 +1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 +1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 +1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 +1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 +1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 +1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 +1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 +1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 +2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 +2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/examples/sugarscape/original/original_app.py b/examples/sugarscape/original/original_app.py new file mode 100644 index 000000000..e6e20fc10 --- /dev/null +++ b/examples/sugarscape/original/original_app.py @@ -0,0 +1,84 @@ +from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt +from mesa.visualization import Slider, SolaraViz, SpaceRenderer, make_plot_component +from mesa.visualization.components import AgentPortrayalStyle, PropertyLayerStyle + + +def agent_portrayal(agent): + return AgentPortrayalStyle( + x=agent.cell.coordinate[0], + y=agent.cell.coordinate[1], + color="red", + marker="o", + size=10, + zorder=1, + ) + + +def property_layer_portrayal(layer): + if layer == "sugar": + return PropertyLayerStyle( + color="blue", alpha=0.8, colorbar=True, vmin=0, vmax=10 + ) + return PropertyLayerStyle(color="red", alpha=0.8, colorbar=True, vmin=0, vmax=10) + + +def post_process(chart): + chart = chart.properties(width=400, height=400) + return chart + + +model_params = { + "rng": { + "type": "InputText", + "value": 42, + "label": "Random Seed", + }, + "width": 50, + "height": 50, + # Population parameters + "initial_population": Slider( + "Initial Population", value=200, min=50, max=500, step=10 + ), + # Agent endowment parameters + "endowment_min": Slider("Min Initial Endowment", value=25, min=5, max=30, step=1), + "endowment_max": Slider("Max Initial Endowment", value=50, min=30, max=100, step=1), + # Metabolism parameters + "metabolism_min": Slider("Min Metabolism", value=1, min=1, max=3, step=1), + "metabolism_max": Slider("Max Metabolism", value=5, min=3, max=8, step=1), + # Vision parameters + "vision_min": Slider("Min Vision", value=1, min=1, max=3, step=1), + "vision_max": Slider("Max Vision", value=5, min=3, max=8, step=1), + # Trade parameter + "enable_trade": {"type": "Checkbox", "value": True, "label": "Enable Trading"}, +} + +model = SugarscapeG1mt() + +# Here, the renderer uses the Altair backend, while the plot components +# use the Matplotlib backend. +# Both can be mixed and matched to enhance the visuals of your model. +renderer = ( + SpaceRenderer(model, backend="altair") + .setup_agents(agent_portrayal) + .setup_propertylayer(property_layer_portrayal) +) +# Specifically, avoid drawing the grid to hide the grid lines. +renderer.draw_agents() +renderer.draw_propertylayer() + +renderer.post_process = post_process + +# Note: It is advised to switch the pages after pausing the model +# on the Solara dashboard. +page = SolaraViz( + model, + renderer, + components=[ + make_plot_component("#Traders", page=1), + make_plot_component("Price", page=1), + ], + model_params=model_params, + name="Sugarscape {G1, M, T}", + play_interval=150, +) +page # noqa diff --git a/examples/sugarscape/original/sugar-map.txt b/examples/sugarscape/original/sugar-map.txt new file mode 100644 index 000000000..3a1ed84bd --- /dev/null +++ b/examples/sugarscape/original/sugar-map.txt @@ -0,0 +1,50 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 +0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 +0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 +0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 +1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 +1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 +1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 +1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 +1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 +1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 +1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 +1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 +2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 +2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file