Skip to content

Commit 36414b6

Browse files
authored
Merge pull request #42 from renbytes/dev-toxicBerryRigorous
Experiment - Toxic Berry Simulation
2 parents b031e6e + 36eb98c commit 36414b6

15 files changed

Lines changed: 686 additions & 521 deletions

File tree

Makefile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,12 @@ setup:
5656

5757
## run: Start Celery workers and run a full experiment.
5858
run:
59-
@echo "👷 Starting $(WORKERS) Celery workers in the background..."
60-
@docker compose up -d worker --scale worker=$(WORKERS)
61-
@echo "▶️ Running experiment from: $(FILE)"
59+
@echo "👷 Starting services and $(WORKERS) Celery workers..."
60+
@docker compose up -d app worker --scale worker=$(WORKERS)
61+
@echo "⏳ Waiting 5 seconds for services to initialize..."
62+
@sleep 5
63+
@echo "▶️ Submitting experiment from: $(FILE)"
64+
# This command will now succeed because the 'app' container is running.
6265
@docker compose exec app poetry run agentsim run-experiment $(FILE)
6366

6467
## run-local: Run a single, local simulation for quick testing and debugging.

agent-sim/src/agent_sim/infrastructure/tasks/celery_app.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,19 @@
2222
)
2323

2424
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
25-
app = Celery("agent_sim", include=["agent_sim.infrastructure.tasks.simulation_tasks"])
25+
26+
# --- MODIFIED: Explicitly list all modules containing tasks ---
27+
app = Celery(
28+
"agent_sim",
29+
include=[
30+
"agent_sim.infrastructure.tasks.simulation_tasks",
31+
# This ensures Celery is aware of the simulation's run module,
32+
# which helps resolve import paths when the task is executed.
33+
"simulations.berry_sim.run",
34+
"simulations.schelling_sim.run",
35+
],
36+
)
37+
# --- END MODIFICATION ---
2638

2739
app.conf.worker_pool_restarts = True
2840
app.conf.worker_pool = "prefork"

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ services:
5151
- .env
5252
ports:
5353
- "5432:5432"
54+
command: postgres -c 'max_connections=201'
5455
healthcheck:
55-
# CORRECTED: Was misspelled as ${POSTGRAES_USER}
5656
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
5757
interval: 10s
5858
timeout: 5s

simulations/berry_sim/actions.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ def execute(
5252
def get_feature_vector(
5353
self, entity_id: str, sim_state: SimulationState, params: Dict[str, Any]
5454
) -> List[float]:
55-
# Padded the vector to have a length of 4 to match EatBerryAction.
56-
# The schema is now [is_move, is_eat_red, is_eat_blue, is_eat_yellow]
57-
return [1.0, 0.0, 0.0, 0.0]
55+
"""
56+
Generates the feature vector for a move action.
57+
Schema: [is_move, is_eat_red, is_eat_blue, is_eat_yellow, is_eat_orange]
58+
"""
59+
return [1.0, 0.0, 0.0, 0.0, 0.0]
5860

5961

6062
@action_registry.register
@@ -100,11 +102,15 @@ def execute(
100102
def get_feature_vector(
101103
self, entity_id: str, sim_state: SimulationState, params: Dict[str, Any]
102104
) -> List[float]:
105+
"""
106+
Generates the one-hot encoded feature vector for eating a specific berry.
107+
Schema: [is_move, is_eat_red, is_eat_blue, is_eat_yellow, is_eat_orange]
108+
"""
103109
berry_type = params.get("berry_type", "")
104-
# The schema is [is_move, is_eat_red, is_eat_blue, is_eat_yellow]
105110
return [
106111
0.0,
107112
1.0 if berry_type == "red" else 0.0,
108113
1.0 if berry_type == "blue" else 0.0,
109114
1.0 if berry_type == "yellow" else 0.0,
115+
1.0 if berry_type == "orange" else 0.0,
110116
]

simulations/berry_sim/components.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,31 @@ def validate(self, entity_id: str) -> Tuple[bool, List[str]]:
4444
return True, []
4545

4646

47+
class MetabolicBoostComponent(Component):
48+
"""Tracks the status of a temporary metabolic boost for an agent."""
49+
50+
def __init__(self, active: bool = False, ticks_remaining: int = 0):
51+
self.active = active
52+
self.ticks_remaining = ticks_remaining
53+
54+
def to_dict(self) -> Dict[str, Any]:
55+
return {"active": self.active, "ticks_remaining": self.ticks_remaining}
56+
57+
def validate(self, entity_id: str) -> Tuple[bool, List[str]]:
58+
return True, []
59+
60+
4761
class BerryComponent(Component):
4862
"""Represents a berry resource in the environment."""
4963

5064
def __init__(self, berry_type: str) -> None:
51-
self.berry_type = berry_type # "red", "blue", or "yellow"
65+
self.berry_type = berry_type # "red", "blue", "yellow", or "orange"
5266

5367
def to_dict(self) -> Dict[str, Any]:
5468
return {"berry_type": self.berry_type}
5569

5670
def validate(self, entity_id: str) -> Tuple[bool, List[str]]:
57-
if self.berry_type not in ["red", "blue", "yellow"]:
71+
if self.berry_type not in ["red", "blue", "yellow", "orange"]:
5872
return False, [f"Invalid berry type: {self.berry_type}"]
5973
return True, []
6074

@@ -77,3 +91,13 @@ def to_dict(self) -> Dict[str, Any]:
7791

7892
def validate(self, entity_id: str) -> Tuple[bool, List[str]]:
7993
return True, []
94+
95+
96+
class PurifierCrystalComponent(Component):
97+
"""A marker component for a purifier crystal tile."""
98+
99+
def to_dict(self) -> Dict[str, Any]:
100+
return {}
101+
102+
def validate(self, entity_id: str) -> Tuple[bool, List[str]]:
103+
return True, []

simulations/berry_sim/config/config.yml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ simulation:
66
steps: 1600
77
log_directory: "data/logs/berry_sim"
88
random_seed: 42
9-
# Default to disabled; the experiment file will enable it for the causal agent.
109
enable_causal_system: false
1110

1211
environment:
@@ -15,11 +14,14 @@ environment:
1514
width: 50
1615
height: 50
1716
spawning:
18-
red_rate: 1.0
19-
blue_rate: 0.75
20-
yellow_rate: 0.75
17+
red_rate: 0.20
18+
blue_rate: 0.15
19+
yellow_rate: 0.15
20+
orange_rate: 0.10 # For Experiment 3
21+
purifier_crystal_count: 7 # For Experiment 2
2122

2223
agent:
24+
vision_range: 7
2325
vitals:
2426
initial_health: 100.0
2527
# Dummy values since these schemas are required by the base config
@@ -48,20 +50,20 @@ learning:
4850
q_learning:
4951
alpha: 0.1
5052
gamma: 0.95
51-
initial_epsilon: 0.1
53+
initial_epsilon: 1.0
54+
epsilon_decay_rate: 0.995
55+
min_epsilon: 0.01
5256
memory:
5357
reflection_interval: 100
5458

5559
scenario_loader:
56-
# CORRECTED: Was pointing to the Schelling loader
5760
class: "simulations.berry_sim.loader.BerryScenarioLoader"
5861

5962
action_generator:
6063
class: "simulations.berry_sim.providers.BerryActionGenerator"
6164

6265
decision_selector:
63-
# Default to the simple heuristic selector for the baseline.
64-
class: "simulations.berry_sim.providers.BerryDecisionSelector"
66+
class: "simulations.berry_sim.providers.HeuristicDecisionSelector"
6567

6668
component_factory:
6769
class: "simulations.berry_sim.providers.BerryComponentFactory"
@@ -85,5 +87,5 @@ logging:
8587
rendering:
8688
enabled: true
8789
output_directory: "data/gif_renders/berry_sim"
88-
frames_per_second: 1
90+
frames_per_second: 15
8991
pixel_scale: 10

simulations/berry_sim/environment.py

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,44 @@ def __init__(self, width: int, height: int) -> None:
1616
self.height = height
1717
self.water_locations: Set[Tuple[int, int]] = set()
1818
self.rock_locations: Set[Tuple[int, int]] = set()
19+
self.crystal_locations: Set[Tuple[int, int]] = set()
1920
self.berry_locations: Dict[Tuple[int, int], str] = {}
2021
self.agent_positions: Dict[str, Tuple[int, int]] = {}
2122
self._grid_entities: Dict[Tuple[int, int], str] = {}
2223

2324
def is_occupied(self, position: Tuple[int, int]) -> bool:
24-
"""Check if a cell is occupied by a blocking object (agent, rock, water)."""
25+
"""Check if a cell is occupied by a blocking object."""
2526
return (
2627
position in self._grid_entities
2728
or position in self.water_locations
2829
or position in self.rock_locations
30+
or position in self.crystal_locations
2931
)
3032

3133
def get_random_empty_cell(self) -> Optional[Tuple[int, int]]:
32-
"""Finds a random unoccupied cell."""
33-
for _ in range(self.width * self.height): # Avoid infinite loops
34-
pos = (
35-
random.randint(0, self.width - 1),
36-
random.randint(0, self.height - 1),
37-
)
38-
if not self.is_occupied(pos) and pos not in self.berry_locations:
39-
return pos
40-
return None
34+
"""
35+
Finds a random unoccupied cell.
36+
37+
This implementation is deterministic in finding all empty cells and will
38+
not fail if only one empty cell remains.
39+
"""
40+
# Changed from a probabilistic search to a deterministic one.
41+
# This guarantees finding an empty cell if one exists.
42+
occupied_cells = (
43+
self.water_locations
44+
| self.rock_locations
45+
| self.crystal_locations
46+
| set(self.berry_locations.keys())
47+
| set(self.agent_positions.values())
48+
)
49+
50+
all_cells = set((x, y) for x in range(self.width) for y in range(self.height))
51+
empty_cells = list(all_cells - occupied_cells)
52+
53+
if not empty_cells:
54+
return None
55+
56+
return random.choice(empty_cells)
4157

4258
def get_berry_toxicity(
4359
self, berry_type: str, position: Tuple[int, int], tick: int
@@ -46,16 +62,19 @@ def get_berry_toxicity(
4662
if berry_type == "red":
4763
return 10.0
4864
elif berry_type == "blue":
49-
return (
50-
-20.0
51-
if self.is_near_feature(position, self.water_locations, 2)
52-
else 10.0
53-
)
65+
is_near_water = self.is_near_feature(position, self.water_locations, 2)
66+
is_near_crystal = self.is_near_feature(position, self.crystal_locations, 2)
67+
if is_near_crystal:
68+
return 10.0 # Crystals purify blue berries
69+
if tick >= 1000 and is_near_water:
70+
return -20.0 # Toxic after tick 1000 if near water
71+
return 10.0
5472
elif berry_type == "yellow":
55-
# Toxicity is random, but seeded by position and tick for reproducibility
56-
seed = hash((position, tick // 100)) # Stable toxicity for a period
73+
seed = hash((position, tick // 100))
5774
rng = random.Random(seed)
5875
return -20.0 if rng.random() < 0.5 else 10.0
76+
elif berry_type == "orange":
77+
return 5.0 # Small direct health boost, main effect is metabolic
5978
return 0.0
6079

6180
def is_near_feature(
@@ -73,6 +92,7 @@ def get_environmental_context(self, position: Tuple[int, int]) -> Dict[str, bool
7392
return {
7493
"near_water": self.is_near_feature(position, self.water_locations, 2),
7594
"near_rocks": self.is_near_feature(position, self.rock_locations, 2),
95+
"near_crystal": self.is_near_feature(position, self.crystal_locations, 2),
7696
}
7797

7898
# --- Interface Methods ---
@@ -123,18 +143,22 @@ def is_valid_position(self, position: Any) -> bool:
123143
return 0 <= position[0] < self.width and 0 <= position[1] < self.height
124144

125145
def get_entities_in_radius(self, center: Any, radius: int) -> List[Tuple[str, Any]]:
126-
return [] # Not needed for this simulation
146+
return []
127147

128148
def to_dict(self) -> Dict[str, Any]:
129149
return {
130150
"width": self.width,
131151
"height": self.height,
132152
"water_locations": [list(pos) for pos in self.water_locations],
133153
"rock_locations": [list(pos) for pos in self.rock_locations],
154+
"crystal_locations": [list(pos) for pos in self.crystal_locations],
134155
}
135156

136157
def restore_from_dict(self, data: Dict[str, Any]) -> None:
137158
self.width = data["width"]
138159
self.height = data["height"]
139-
self.water_locations = {tuple(pos) for pos in data["water_locations"]}
140-
self.rock_locations = {tuple(pos) for pos in data["rock_locations"]}
160+
self.water_locations = {tuple(pos) for pos in data.get("water_locations", [])}
161+
self.rock_locations = {tuple(pos) for pos in data.get("rock_locations", [])}
162+
self.crystal_locations = {
163+
tuple(pos) for pos in data.get("crystal_locations", [])
164+
}
Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# simulations/berry_sim/experiments/causal_ab_test.yml
22

3-
experiment_name: "Berry Sim - Causal Agent A/B Test"
3+
experiment_name: "Exp1 - Isolating Causal Reasoning"
44

55
base_config_path: "simulations/berry_sim/config/config.yml"
66

@@ -9,24 +9,40 @@ simulation_package: "simulations.berry_sim"
99
scenarios:
1010
- "simulations/berry_sim/scenarios/default.json"
1111

12-
# Run each variation 3 times for statistical significance
13-
runs_per_scenario: 3
12+
# Run each variation 50 times for statistical power
13+
runs_per_scenario: 50
1414

1515
variations:
16-
- name: "Baseline-Heuristic-Agent"
16+
- name: "GroupA-Heuristic-Baseline"
1717
overrides:
1818
simulation:
19-
# Explicitly disable the causal system for the baseline
2019
enable_causal_system: false
2120
decision_selector:
22-
# Use the simple, hard-coded heuristic policy
23-
class: "simulations.berry_sim.providers.BerryDecisionSelector"
21+
# Simple rule-based agent with perfect environmental knowledge
22+
class: "simulations.berry_sim.providers.HeuristicDecisionSelector"
2423

25-
- name: "Causal-QLearning-Agent"
24+
- name: "GroupB-Full-Causal-Model"
2625
overrides:
2726
simulation:
28-
# Enable the causal system for the advanced agent
27+
# The full cognitive model with causal reasoning enabled
2928
enable_causal_system: true
3029
decision_selector:
31-
# Use the new, intelligent policy that queries the neural network
30+
# Q-learning agent that uses the causal system's output
3231
class: "simulations.berry_sim.providers.QLearningDecisionSelector"
32+
33+
- name: "GroupC-Perception-Only-Control"
34+
overrides:
35+
simulation:
36+
# Causal reasoning is turned off for this control group
37+
enable_causal_system: false
38+
decision_selector:
39+
# Standard Q-learning agent without causal feedback
40+
class: "simulations.berry_sim.providers.QLearningDecisionSelector"
41+
42+
- name: "GroupD-Exploration-Matched-Heuristic"
43+
overrides:
44+
simulation:
45+
enable_causal_system: false
46+
decision_selector:
47+
# Rule-based agent with exploration to match the Q-learning agents
48+
class: "simulations.berry_sim.providers.ExplorationHeuristicDecisionSelector"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# simulations/berry_sim/experiments/validate_perception.yml
2+
3+
experiment_name: "Exp2 - Validating Agent Perception"
4+
5+
base_config_path: "simulations/berry_sim/config/config.yml"
6+
7+
simulation_package: "simulations.berry_sim"
8+
9+
scenarios:
10+
- "simulations/berry_sim/scenarios/default.json"
11+
12+
# Run only a few times for a quick check
13+
runs_per_scenario: 2
14+
15+
variations:
16+
- name: "QLearning-Agent-With-Vision"
17+
overrides:
18+
# We want to test the full cognitive model with perception
19+
simulation:
20+
enable_causal_system: true
21+
agent:
22+
# Let's make the simulation smaller and faster for this test
23+
foundational:
24+
num_agents: 25
25+
vision_range: 7 # Make sure vision is on
26+
environment:
27+
params:
28+
width: 25
29+
height: 25
30+
decision_selector:
31+
class: "simulations.berry_sim.providers.QLearningDecisionSelector"

0 commit comments

Comments
 (0)