Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a193c37
add typed membership backend foundation with invariant tests.
falloficaruss May 14, 2026
64139cb
add typed membership backend foundation with invariant tests.
falloficaruss May 14, 2026
9bd4f53
Merge pull request #2 from mesa/main
falloficaruss May 14, 2026
4bd8a11
add typed membership backend foundation with invariant tests.
falloficaruss May 16, 2026
cdc8aa7
Merge pull request #3 from mesa/main
falloficaruss May 16, 2026
48e747a
normalize id-based removals, generalize relation keys and add perf TO…
falloficaruss May 17, 2026
6e4c4bc
update docstring
falloficaruss May 20, 2026
3249d25
Merge pull request #4 from mesa/main
falloficaruss May 23, 2026
a1edb53
add warehouse model to test the membership backend
falloficaruss May 26, 2026
054297a
wire membership backend into alliance formation model
falloficaruss May 29, 2026
5f0df61
Merge pull request #6 from falloficaruss/alliance-formation-model
falloficaruss Jun 15, 2026
b233e3e
Merge pull request #5 from falloficaruss/test-backend
falloficaruss Jun 15, 2026
64e08aa
Merge pull request #1 from falloficaruss/backend
falloficaruss Jun 15, 2026
311e731
Add explicit entity indexing and fix warehouse robot workflow
falloficaruss Jun 15, 2026
174dd14
Merge pull request #7 from falloficaruss/meta-agent-identity-registry
falloficaruss Jun 17, 2026
afca612
Merge branch 'mesa:main' into main
falloficaruss Jun 17, 2026
874a269
Merge branch 'mesa:main' into main
falloficaruss Jun 25, 2026
a98de20
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2026
1aa5b7b
Merge branch 'mesa:main' into main
falloficaruss Jun 29, 2026
414f055
remove entity indexing
falloficaruss Jun 29, 2026
674293f
Merge branch 'mesa:main' into main
falloficaruss Jul 1, 2026
a5c6f43
add typed membership backend foundation with invariant tests.
falloficaruss May 14, 2026
786654c
add typed membership backend foundation with invariant tests.
falloficaruss May 14, 2026
45c00f7
normalize id-based removals, generalize relation keys and add perf TO…
falloficaruss May 17, 2026
47fb48b
initial facade implementation
falloficaruss Jul 5, 2026
c777208
replace facade naming with meta-agents API alias
falloficaruss Jul 6, 2026
9b6a828
ruff
falloficaruss Jul 6, 2026
9913e09
update examples
falloficaruss Jul 10, 2026
651860b
remove one breaking facade reference from the warehouse model
falloficaruss Jul 10, 2026
38b5a03
Merge branch 'mesa:main' into meta-agents-api
falloficaruss Jul 10, 2026
bd178c5
rewrite meta_agent.py
falloficaruss Jul 20, 2026
baa9bcd
Merge pull request #12 from falloficaruss/meta-agents-api
falloficaruss Jul 24, 2026
b2bf0e6
Merge branch 'mesa:main' into meta-agents
falloficaruss Jul 24, 2026
99c43b9
remove facade
falloficaruss Jul 24, 2026
3aeae59
Merge branch 'mesa:main' into meta-agents-api
falloficaruss Jul 24, 2026
08a3704
Merge pull request #14 from falloficaruss/meta-agents-examples
falloficaruss Jul 24, 2026
0fdfd1e
Merge pull request #16 from falloficaruss/meta-agents-api
falloficaruss Jul 24, 2026
0e7d201
Merge branch 'mesa:main' into rewrite
falloficaruss Jul 24, 2026
0cef4d6
Merge pull request #17 from falloficaruss/rewrite
falloficaruss Jul 24, 2026
74a9f36
Merge branch 'mesa:main' into meta-agents
falloficaruss Jul 29, 2026
f80aa0f
Merge branch 'mesa:main' into meta-agents
falloficaruss Jul 31, 2026
905b677
remove warehouse model from the mesa repo
falloficaruss Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The `/benchmarks` directory contains tools for benchmarking the Mesa library performance on the included example models. This allows to track and compare model initialisation time and runtime between different Mesa versions.

MESA uses several example base models for benchmarking performance (BoltzmannWealth, Schelling, BoidFlockers, and WolfSheep) by calculating the initialization time and run time for each of these models. These example models can be found in the `/mesa/examples` directory.
MESA uses several example base models for benchmarking performance (BoltzmannWealth, Schelling, BoidFlockers, WolfSheep, and the meta-agents examples) by calculating the initialization time and run time for each of these models. These example models can be found in the `/mesa/examples` directory.

## Available Files

Expand Down Expand Up @@ -39,6 +39,8 @@ This will:
- Save results to a pickle file named `timings_X.pickle` (where X is an incremental number)
- Display a summary of results in the terminal

For meta-agents branch comparisons, run the benchmark once on the `meta-agents` baseline branch, switch to your feature branch, and run it again before comparing the two pickle files.


>**Noteworthy :** the pickle file created by the benchmark is not under git control. So you can run the benchmark on the master branch first, switch to your development branch, and run the benchmarks again.

Expand Down
32 changes: 32 additions & 0 deletions benchmarks/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
from mesa.examples import (
BoidFlockers,
BoltzmannWealth,
MultiLevelAllianceModel,
Schelling,
SugarscapeG1mt,
WarehouseModel,
WolfSheep,
)
from mesa.examples.advanced.alliance_formation.model import AllianceScenario
from mesa.examples.advanced.sugarscape_g1mt.model import SugarScapeScenario
from mesa.examples.advanced.warehouse.model import WarehouseScenario
from mesa.examples.advanced.wolf_sheep.model import WolfSheepScenario
from mesa.examples.basic.boid_flockers.model import BoidsScenario
from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannScenario
Expand Down Expand Up @@ -126,4 +130,32 @@
),
},
},
MultiLevelAllianceModel: {
"small": {
"replications": 25,
"iterations": 3,
"steps": 10,
"scenario": AllianceScenario(n=50, mean=0.5, std_dev=0.1, rng=42),
},
"large": {
"replications": 10,
"iterations": 3,
"steps": 20,
"scenario": AllianceScenario(n=120, mean=0.5, std_dev=0.1, rng=42),
},
},
WarehouseModel: {
"small": {
"replications": 25,
"iterations": 3,
"steps": 10,
"scenario": WarehouseScenario(rows=8, cols=8, height=2, rng=42),
},
"large": {
"replications": 10,
"iterations": 3,
"steps": 20,
"scenario": WarehouseScenario(rows=16, cols=16, height=2, rng=42),
},
},
}
3 changes: 1 addition & 2 deletions mesa/examples/advanced/alliance_formation/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ To provide a simple demonstration of this capability is an alliance formation mo

In this simulation n agents are created, who have two attributes (1) power and (2) preference. Each attribute is a number between 0 and 1 over a gaussian distribution. Agents then randomly select other agents and use the [bilateral shapley value](https://en.wikipedia.org/wiki/Shapley_value) to determine if they should form an alliance. If the expected utility support an alliances, the agent creates a meta-agent. Subsequent steps may add agents to the meta-agent, create new instances of similar hierarchy, or create a new hierarchy level where meta-agents form an alliance of meta-agents. In this visualization of this model a new meta-agent hierarchy will be a larger node and a new color.

In MetaAgents current configuration, agents being part of multiple meta-agents is not supported.
This example records memberships in the backend so agents can participate in multiple overlapping memberships.

If you would like to see an example of explicit meta-agent formation see the [warehouse model in the Mesa example's repository](https://github.com/mesa/mesa-examples/tree/main/examples/warehouse)

Expand Down Expand Up @@ -45,4 +45,3 @@ Then run the example:
solara run app.py

Open the displayed local URL in your browser.

19 changes: 17 additions & 2 deletions mesa/examples/advanced/alliance_formation/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import mesa
from mesa import Agent
from mesa.examples.advanced.alliance_formation.agents import AllianceAgent
from mesa.experimental.meta_agents.backend import MembershipBackend
from mesa.experimental.meta_agents.meta_agent import (
create_meta_agent,
find_combinations,
Expand Down Expand Up @@ -37,6 +38,7 @@ def __init__(self, scenario: AllianceScenario = AllianceScenario):
super().__init__(scenario=scenario)
self.network = nx.Graph() # Initialize the network
self.datacollector = mesa.DataCollector(model_reporters={"Network": "network"})
self.membership_backend = MembershipBackend()

# Create Agents
power = self.rng.normal(scenario.mean, scenario.std_dev, scenario.n)
Expand All @@ -60,6 +62,12 @@ def add_link(self, meta_agent, agents):
for agent in agents:
self.network.add_edge(meta_agent.unique_id, agent.unique_id)

def _record_alliance_membership(self, meta_agent, agents) -> None:
"""Mirror alliance membership into the backend."""
self.membership_backend.bulk_add(
[(agent, meta_agent, "member") for agent in agents]
)

def calculate_shapley_value(self, agents):
"""
Calculate the Shapley value of the two agents.
Expand Down Expand Up @@ -173,11 +181,17 @@ def step(self):
)

for alliance, attributes in combinations:
class_name = f"MetaAgentLevel{attributes[2]}"
alliance_members = tuple(
sorted(alliance, key=lambda agent: agent.unique_id)
)
alliance_signature = "_".join(
str(agent.unique_id) for agent in alliance_members
)
class_name = f"MetaAgentLevel{attributes[2]}_{alliance_signature}"
meta = create_meta_agent(
self,
class_name,
alliance,
alliance_members,
Agent,
meta_attributes={
"level": attributes[2],
Expand All @@ -194,3 +208,4 @@ def step(self):
level=meta.level,
)
self.add_link(meta, meta.agents)
self._record_alliance_membership(meta, meta.agents)
46 changes: 21 additions & 25 deletions mesa/experimental/meta_agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
"""This method is for dynamically creating new agents (meta-agents).

Meta-agents are defined as agents composed of existing agents.

Meta-agents are created dynamically with a pointer to the model, name of the meta-agent,,
iterable of agents to belong to the new meta-agents, any new functions for the meta-agent,
any new attributes for the meta-agent, whether to retain sub-agent functions,
whether to retain sub-agent attributes.

Examples of meta-agents:
- An autonomous car where the subagents are the wheels, sensors,
battery, computer etc. and the meta-agent is the car itself.
- A company where the subagents are employees, departments, buildings, etc.
- A city where the subagents are people, buildings, streets, etc.

Currently meta-agents are restricted to one parent agent for each subagent/
one meta-agent per subagent.

Goal is to assess usage and expand functionality.

"""

from .meta_agent import MetaAgent

__all__ = ["MetaAgent"]
"""Experimental meta-agent membership helpers."""

from .backend import MembershipBackend
from .meta_agent import (
MetaAgent,
create_meta_agent,
evaluate_combination,
find_combinations,
)
from .meta_agents_api import MembershipEdge, MembershipView, MetaAgents

__all__ = [
"MembershipBackend",
"MembershipEdge",
"MembershipView",
"MetaAgent",
"MetaAgents",
"create_meta_agent",
"evaluate_combination",
"find_combinations",
]
154 changes: 154 additions & 0 deletions mesa/experimental/meta_agents/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Backend foundation for typed overlapping meta-agent memberships.

- Canonical typed membership representation
- Safe update operations
- Invariant checks
- Shared by the public meta-agents API and compatibility helpers
"""

from __future__ import annotations

from collections import defaultdict
from collections.abc import Hashable, Iterable

RelationKey = Hashable
Triplet = tuple[Hashable, Hashable, RelationKey]


class MembershipBackend:
"""Canonical backend for typed overlapping memberships."""

def __init__(self) -> None:
"""Initialize empty triplet storage and bidirectional indexes."""
self._triplets: set[Triplet] = set()
self._by_agent: dict[Hashable, set[tuple[Hashable, RelationKey]]] = defaultdict(
set
)
self._by_group: dict[Hashable, set[tuple[Hashable, RelationKey]]] = defaultdict(
set
)

def _to_id(self, entity: Hashable) -> Hashable:
"""Normalize entity to canonical ID.

Uses ``mesa.agent.Agent.unique_id`` when available and finally the entity
as-is (for already hashable external IDs).
"""
return getattr(entity, "unique_id", entity)

def add_membership(
self, agent: Hashable, group: Hashable, relation: RelationKey
) -> None:
"""Add one typed membership edge if it does not already exist."""
agent_id = self._to_id(agent)
group_id = self._to_id(group)
triplet = (agent_id, group_id, relation)
if triplet in self._triplets:
return
self._triplets.add(triplet)
self._by_agent[agent_id].add((group_id, relation))
self._by_group[group_id].add((agent_id, relation))

def bulk_add(self, memberships: Iterable[Triplet]) -> None:
"""Add many typed membership edges."""
for agent, group, relation in memberships:
self.add_membership(agent, group, relation)

def remove_membership(
self, agent: Hashable, group: Hashable, relation: RelationKey
) -> None:
"""Remove one typed membership edge if present."""
agent_id = self._to_id(agent)
group_id = self._to_id(group)
triplet = (agent_id, group_id, relation)
if triplet not in self._triplets:
return
self._triplets.remove(triplet)

self._by_agent[agent_id].discard((group_id, relation))
if not self._by_agent[agent_id]:
del self._by_agent[agent_id]

self._by_group[group_id].discard((agent_id, relation))
if not self._by_group[group_id]:
del self._by_group[group_id]

def replace_relation(
self,
agent: Hashable,
group: Hashable,
old_relation: RelationKey,
new_relation: RelationKey,
) -> None:
"""Replace one relation label for one agent-group pair."""
self.remove_membership(agent, group, old_relation)
self.add_membership(agent, group, new_relation)

def remove_agent(self, agent: Hashable) -> None:
"""Remove an agent and all incident memberships."""
agent_id = self._to_id(agent)
# TODO(perf): Current removal is O(degree(agent)) with per-edge updates.
# Revisit with bulk/index-aware deletion once benchmark baselines are in place.
edges = list(self._by_agent.get(agent_id, set()))
for group_id, relation in edges:
self.remove_membership(agent_id, group_id, relation)

def remove_group(self, group: Hashable) -> None:
"""Remove a group and all incident memberships."""
group_id = self._to_id(group)
# TODO(perf): Current removal is O(degree(agent)) with per-edge updates.
# Revisit with bulk/index-aware deletion once benchmark baselines are in place.
edges = list(self._by_group.get(group_id, set()))
for agent_id, relation in edges:
self.remove_membership(agent_id, group_id, relation)

def groups_of(
self, agent: Hashable, relation: RelationKey | None = None
) -> set[Hashable]:
"""Return groups for an agent, optionally filtered by relation."""
agent_id = self._to_id(agent)
entries = self._by_agent.get(agent_id, set())
if relation is None:
return {group for group, _ in entries}
return {group for group, rel in entries if rel == relation}

def agents_of(
self, group: Hashable, relation: RelationKey | None = None
) -> set[Hashable]:
"""Return agents for a group, optionally filtered relation."""
group_id = self._to_id(group)
entries = self._by_group.get(group_id, set())
if relation is None:
return {agent for agent, _ in entries}
return {agent for agent, rel in entries if rel == relation}

def relations_between(self, agent: Hashable, group: Hashable) -> set[RelationKey]:
"""Return all relation types between one agent and one group."""
agent_id = self._to_id(agent)
group_id = self._to_id(group)
return {
relation
for linked_group, relation in self._by_agent.get(agent_id, set())
if linked_group == group_id
}

def as_triplets(self) -> set[Triplet]:
"""Return all memberships as canonical triplets."""
return set(self._triplets)

def assert_invariants(self) -> None:
"""Assert internal consistency between triplets and indexes."""
# Triplets must match both indexes.
for agent, group, relation in self._triplets:
assert (group, relation) in self._by_agent.get(agent, set())
assert (agent, relation) in self._by_group.get(group, set())

# Agent index must match triplets
for agent, edges in self._by_agent.items():
for group, relation in edges:
assert (agent, group, relation) in self._triplets

# Group index must match triplets
for group, edges in self._by_group.items():
for agent, relation in edges:
assert (agent, group, relation) in self._triplets
Loading
Loading