diff --git a/examples/adaptive_risk_agents/README.md b/examples/adaptive_risk_agents/README.md new file mode 100644 index 00000000..a368e370 --- /dev/null +++ b/examples/adaptive_risk_agents/README.md @@ -0,0 +1,83 @@ +# Adaptive Risk Agents + +This example demonstrates agents that **adapt their risk-taking behavior +based on past experiences**, implemented using only core Mesa primitives. + +The model is intentionally simple in structure but rich in behavior, making it +useful as a diagnostic example for understanding how adaptive decision-making +is currently modeled in Mesa. + + + +## Motivation + +Many real-world agents do not follow fixed rules. +Instead, they: + +- make decisions under uncertainty, +- remember past outcomes, +- adapt future behavior based on experience. + +In Mesa today, modeling this kind of adaptive behavior often results in +a large amount of logic being concentrated inside `agent.step()`, combining +multiple concerns in a single execution phase. + +This example exists to **make that structure explicit**, not to abstract it away. + + + +## Model Overview + +- Each agent chooses between: + - a **safe action** (low or zero payoff, no risk), + - a **risky action** (stochastic payoff). +- Agents track recent outcomes of risky actions in a short memory window. +- If recent outcomes are negative, agents become more risk-averse. +- If outcomes are positive, agents increase their risk preference. + +All behavior is implemented using plain Python and Mesa’s public APIs. + + + +## Observations From This Example + +This model intentionally does **not** introduce new abstractions +(tasks, goals, states, schedulers, etc.). + +Instead, it highlights several patterns that commonly arise when modeling +adaptive behavior in Mesa today: + +- Decision-making, action execution, memory updates, and learning logic + are handled within a single `step()` method. +- There is no explicit separation between decision phases. +- Actions are instantaneous, with no notion of duration or interruption. +- As behaviors grow richer, agent logic can become deeply nested and harder + to maintain. + +These observations may be useful input for ongoing discussions around: + +- Behavioral frameworks +- Tasks and continuous states +- Richer agent decision abstractions + + + +## Mesa Version & API Alignment + +This example is written to align with the **Mesa 4 design direction**: + +- Uses `AgentSet` and `shuffle_do` +- Avoids deprecated schedulers +- Avoids `DataCollector` +- Uses keyword-only arguments for public APIs +- Relies on `model.random` for reproducibility + +No experimental or private APIs are used. + + + +## Running the Example + +From the Mesa repository root: + +python -m mesa.examples.adaptive_risk_agents.run diff --git a/examples/adaptive_risk_agents/__init__.py b/examples/adaptive_risk_agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/adaptive_risk_agents/agents.py b/examples/adaptive_risk_agents/agents.py new file mode 100644 index 00000000..defc6be5 --- /dev/null +++ b/examples/adaptive_risk_agents/agents.py @@ -0,0 +1,86 @@ +"""Adaptive Risk Agent. + +An agent that chooses between safe and risky actions and adapts its +risk preference based on past outcomes. + +This example intentionally keeps all decision logic inside `step()` +to highlight current limitations in Mesa's behavior modeling. +""" + +from __future__ import annotations + +from collections import deque + +from mesa import Agent + + +class AdaptiveRiskAgent(Agent): + """An agent that adapts its risk-taking behavior over time. + + Attributes + ---------- + risk_preference : float + Probability (0-1) of choosing a risky action. + memory : deque[int] + Recent outcomes of risky actions (+1 reward, -1 loss). + """ + + def __init__( + self, + model, + *, + initial_risk_preference: float = 0.5, + memory_size: int = 10, + ) -> None: + super().__init__(model) + self.risk_preference = initial_risk_preference + self.memory: deque[int] = deque(maxlen=memory_size) + + def choose_action(self) -> str: + """Choose between a safe or risky action.""" + if self.model.random.random() < self.risk_preference: + return "risky" + return "safe" + + def risky_action(self) -> int: + """Perform a risky action. + + Returns + ------- + int + Outcome of the action (+1 for reward, -1 for loss). + """ + return 1 if self.model.random.random() < 0.5 else -1 + + def safe_action(self) -> int: + """Perform a safe action.Returns ------- int Guaranteed neutral outcome.""" + return 0 + + def update_risk_preference(self) -> None: + """Update risk preference based on recent memory.""" + if not self.memory: + return + + avg_outcome = sum(self.memory) / len(self.memory) + + if avg_outcome < 0: + self.risk_preference = max(0.0, self.risk_preference - 0.05) + else: + self.risk_preference = min(1.0, self.risk_preference + 0.05) + + def step(self) -> None: + """Execute one decision step. + + NOTE: + This method intentionally mixes decision-making, action execution, + memory updates, and learning to demonstrate how behavioral + complexity accumulates in current Mesa models. + """ + action = self.choose_action() + + if action == "risky": + outcome = self.risky_action() + self.memory.append(outcome) + self.update_risk_preference() + else: + self.safe_action() diff --git a/examples/adaptive_risk_agents/model.py b/examples/adaptive_risk_agents/model.py new file mode 100644 index 00000000..3cb2c969 --- /dev/null +++ b/examples/adaptive_risk_agents/model.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from mesa import Model + +from examples.adaptive_risk_agents.agents import AdaptiveRiskAgent + + +class AdaptiveRiskModel(Model): + """A simple model running adaptive risk-taking agents.""" + + def __init__(self, n_agents: int = 50, *, seed: int | None = None) -> None: + super().__init__(seed=seed) + + # Create agents — Mesa will register them automatically + for _ in range(n_agents): + AdaptiveRiskAgent(self) + + def step(self) -> None: + """Advance the model by one step.""" + self.agents.shuffle_do("step") diff --git a/examples/adaptive_risk_agents/run.py b/examples/adaptive_risk_agents/run.py new file mode 100644 index 00000000..895d0599 --- /dev/null +++ b/examples/adaptive_risk_agents/run.py @@ -0,0 +1,38 @@ +"""Run script for the Adaptive Risk Agents example. + +This script runs the model for a fixed number of steps and prints +aggregate statistics to illustrate how agent behavior evolves over time. + +Intentionally simple: +- No DataCollector +- No batch_run +- No visualization +""" + +from __future__ import annotations + +from examples.adaptive_risk_agents.model import AdaptiveRiskModel + + +def run_model(*, n_agents: int = 50, steps: int = 100, seed: int | None = None) -> None: + """Run the AdaptiveRiskModel and print summary statistics.""" + model = AdaptiveRiskModel(n_agents=n_agents, seed=seed) + + for step in range(steps): + model.step() + + total_risk = 0.0 + count = 0 + + for agent in model.agents: + total_risk += agent.risk_preference + count += 1 + + avg_risk = total_risk / count if count > 0 else 0.0 + + if step % 10 == 0: + print(f"Step {step:3d} | Average risk preference: {avg_risk:.3f}") + + +if __name__ == "__main__": + run_model() diff --git a/examples/adaptive_risk_agents/tests/test_agent_smoke.py b/examples/adaptive_risk_agents/tests/test_agent_smoke.py new file mode 100644 index 00000000..ffdb3d5b --- /dev/null +++ b/examples/adaptive_risk_agents/tests/test_agent_smoke.py @@ -0,0 +1,11 @@ +from examples.adaptive_risk_agents.model import AdaptiveRiskModel + + +def test_agent_methods_execute(): + model = AdaptiveRiskModel(n_agents=1, seed=1) + agent = next(iter(model.agents)) + + action = agent.choose_action() + assert action in {"safe", "risky"} + + agent.step() # should not crash diff --git a/examples/adaptive_risk_agents/tests/test_smoke.py b/examples/adaptive_risk_agents/tests/test_smoke.py new file mode 100644 index 00000000..6aefa6ec --- /dev/null +++ b/examples/adaptive_risk_agents/tests/test_smoke.py @@ -0,0 +1,18 @@ +"""Smoke tests for the Adaptive Risk Agents example. + +These tests only verify that the example runs without crashing. +They intentionally avoid checking model outcomes or behavior. +""" + +from examples.adaptive_risk_agents.model import AdaptiveRiskModel + + +def test_model_initializes(): + model = AdaptiveRiskModel(n_agents=10, seed=42) + assert model is not None + + +def test_model_steps_without_error(): + model = AdaptiveRiskModel(n_agents=10, seed=42) + for _ in range(5): + model.step()