0
# Experimental Features
1
2
Mesa's experimental package contains cutting-edge features under active development. These modules provide advanced capabilities including discrete event simulation, reactive programming, enhanced continuous spaces, and modern visualization tools.
3
4
**⚠️ Warning:** Experimental features may have API changes between releases. Use in production with caution and pin specific Mesa versions.
5
6
## Imports
7
8
```python { .api }
9
# Main experimental package
10
from mesa import experimental
11
12
# Submodules
13
from mesa.experimental import devs
14
from mesa.experimental import mesa_signals
15
from mesa.experimental import meta_agents
16
from mesa.experimental import continuous_space
17
18
# Visualization (experimental)
19
from mesa.visualization import SolaraViz, JupyterViz
20
```
21
22
## Discrete Event Simulation (DEVS)
23
24
The DEVS (Discrete Event Simulation) framework enables event-driven modeling where system state changes occur at specific time points rather than fixed time steps.
25
26
### Core DEVS Classes
27
28
```python { .api }
29
from mesa.experimental.devs import DEVSModel, DEVSAgent, Event, EventScheduler
30
31
class Event:
32
"""
33
Discrete event with timing and payload information.
34
35
Events represent state changes that occur at specific simulation times,
36
enabling more efficient simulation of systems with sparse activity.
37
"""
38
39
def __init__(self, time: float, event_type: str, data: dict = None):
40
"""
41
Initialize an event.
42
43
Parameters:
44
time: When the event should occur (simulation time)
45
event_type: Type identifier for the event
46
data: Additional event payload data
47
"""
48
...
49
50
@property
51
def time(self) -> float:
52
"""Event occurrence time."""
53
...
54
55
@property
56
def event_type(self) -> str:
57
"""Event type identifier."""
58
...
59
60
@property
61
def data(self) -> dict:
62
"""Event data payload."""
63
...
64
65
class EventScheduler:
66
"""
67
Priority queue-based event scheduler for DEVS simulation.
68
69
Manages the event queue and provides efficient scheduling
70
and execution of discrete events.
71
"""
72
73
def __init__(self):
74
"""Initialize empty event scheduler."""
75
...
76
77
def schedule_event(self, event: Event):
78
"""
79
Schedule an event for future execution.
80
81
Parameters:
82
event: Event to schedule
83
"""
84
...
85
86
def schedule_in(self, delay: float, event_type: str, data: dict = None) -> Event:
87
"""
88
Schedule an event after a delay from current time.
89
90
Parameters:
91
delay: Time delay from current simulation time
92
event_type: Type of event to schedule
93
data: Event data payload
94
95
Returns:
96
Scheduled Event object
97
"""
98
...
99
100
def get_next_event(self) -> Event | None:
101
"""
102
Get and remove the next scheduled event.
103
104
Returns:
105
Next event to process, or None if queue is empty
106
"""
107
...
108
109
def peek_next_event(self) -> Event | None:
110
"""
111
Peek at next event without removing it.
112
113
Returns:
114
Next event or None if queue is empty
115
"""
116
...
117
118
@property
119
def current_time(self) -> float:
120
"""Current simulation time."""
121
...
122
123
class DEVSAgent(Agent):
124
"""
125
Agent class for discrete event simulation models.
126
127
Extends basic Agent with event handling capabilities and
128
lifecycle management for event-driven simulation.
129
"""
130
131
def __init__(self, model: 'DEVSModel'):
132
"""
133
Initialize DEVS agent.
134
135
Parameters:
136
model: The DEVS model containing this agent
137
"""
138
super().__init__(model)
139
...
140
141
def handle_event(self, event: Event):
142
"""
143
Process an incoming event.
144
145
This method should be overridden by subclasses to define
146
agent-specific event handling behavior.
147
148
Parameters:
149
event: Event to process
150
"""
151
...
152
153
def schedule_event(self, delay: float, event_type: str, data: dict = None) -> Event:
154
"""
155
Schedule an event for this agent.
156
157
Parameters:
158
delay: Time delay from current time
159
event_type: Type of event to schedule
160
data: Event data payload
161
162
Returns:
163
Scheduled Event object
164
"""
165
return self.model.scheduler.schedule_in(delay, event_type, data)
166
167
def on_activate(self):
168
"""Called when agent is activated in the simulation."""
169
...
170
171
def on_deactivate(self):
172
"""Called when agent is deactivated/removed."""
173
...
174
175
class DEVSModel(Model):
176
"""
177
Model class for discrete event simulation.
178
179
Manages event-driven simulation with precise timing and
180
efficient execution of sparse events.
181
"""
182
183
def __init__(self, **kwargs):
184
"""
185
Initialize DEVS model.
186
187
Parameters:
188
**kwargs: Model configuration parameters
189
"""
190
super().__init__(**kwargs)
191
self.scheduler = EventScheduler()
192
...
193
194
def step(self):
195
"""
196
Execute one simulation step (process next event).
197
198
In DEVS, a step processes the next scheduled event
199
rather than advancing by a fixed time increment.
200
"""
201
event = self.scheduler.get_next_event()
202
if event:
203
self._process_event(event)
204
else:
205
self.running = False # No more events
206
207
def _process_event(self, event: Event):
208
"""
209
Process a discrete event.
210
211
Parameters:
212
event: Event to process
213
"""
214
# Update simulation time
215
self.scheduler.current_time = event.time
216
217
# Handle model-level events
218
if hasattr(self, f'handle_{event.event_type}'):
219
handler = getattr(self, f'handle_{event.event_type}')
220
handler(event)
221
222
def run_until(self, end_time: float):
223
"""
224
Run simulation until specified time.
225
226
Parameters:
227
end_time: Simulation time to run until
228
"""
229
while (self.running and
230
self.scheduler.peek_next_event() and
231
self.scheduler.peek_next_event().time <= end_time):
232
self.step()
233
234
@property
235
def current_time(self) -> float:
236
"""Current simulation time."""
237
return self.scheduler.current_time
238
```
239
240
### DEVS Usage Example
241
242
```python { .api }
243
from mesa.experimental.devs import DEVSModel, DEVSAgent, Event
244
import random
245
246
class Customer(DEVSAgent):
247
"""Customer agent that generates service requests."""
248
249
def __init__(self, model, arrival_rate=1.0):
250
super().__init__(model)
251
self.arrival_rate = arrival_rate
252
self.service_time = 0
253
254
def on_activate(self):
255
"""Schedule first arrival."""
256
inter_arrival_time = random.expovariate(self.arrival_rate)
257
self.schedule_event(inter_arrival_time, "arrival", {"customer_id": self.unique_id})
258
259
def handle_event(self, event):
260
if event.event_type == "arrival":
261
# Process service request
262
self.model.handle_customer_arrival(self)
263
264
# Schedule next arrival
265
inter_arrival_time = random.expovariate(self.arrival_rate)
266
self.schedule_event(inter_arrival_time, "arrival", {"customer_id": self.unique_id})
267
268
elif event.event_type == "service_complete":
269
# Customer leaves system
270
self.service_time = event.data.get("service_duration", 0)
271
self.model.log_service_completion(self)
272
273
class Server(DEVSAgent):
274
"""Server agent that processes customer requests."""
275
276
def __init__(self, model, service_rate=2.0):
277
super().__init__(model)
278
self.service_rate = service_rate
279
self.busy = False
280
self.queue = []
281
self.customers_served = 0
282
283
def handle_event(self, event):
284
if event.event_type == "service_request":
285
customer = event.data["customer"]
286
287
if not self.busy:
288
# Start service immediately
289
self._start_service(customer)
290
else:
291
# Add to queue
292
self.queue.append(customer)
293
294
elif event.event_type == "service_complete":
295
self.busy = False
296
self.customers_served += 1
297
298
# Notify customer
299
customer = event.data["customer"]
300
customer_event = Event(
301
self.model.current_time,
302
"service_complete",
303
{"service_duration": event.data["service_duration"]}
304
)
305
customer.handle_event(customer_event)
306
307
# Serve next customer if any
308
if self.queue:
309
next_customer = self.queue.pop(0)
310
self._start_service(next_customer)
311
312
def _start_service(self, customer):
313
"""Start serving a customer."""
314
self.busy = True
315
service_duration = random.expovariate(self.service_rate)
316
317
self.schedule_event(service_duration, "service_complete", {
318
"customer": customer,
319
"service_duration": service_duration
320
})
321
322
class QueuingModel(DEVSModel):
323
"""Queueing system simulation using DEVS."""
324
325
def __init__(self, n_customers=5, n_servers=2, arrival_rate=1.0, service_rate=2.0):
326
super().__init__()
327
328
self.arrival_rate = arrival_rate
329
self.service_rate = service_rate
330
331
# Create servers
332
self.servers = [Server(self, service_rate) for _ in range(n_servers)]
333
for server in self.servers:
334
self.register_agent(server)
335
336
# Create customers
337
for i in range(n_customers):
338
customer = Customer(self, arrival_rate)
339
self.register_agent(customer)
340
customer.on_activate()
341
342
# Statistics tracking
343
self.total_arrivals = 0
344
self.total_completions = 0
345
self.service_times = []
346
347
self.running = True
348
349
def handle_customer_arrival(self, customer):
350
"""Handle customer arrival - assign to server."""
351
self.total_arrivals += 1
352
353
# Find server with shortest queue
354
best_server = min(self.servers, key=lambda s: len(s.queue) if s.busy else 0)
355
356
# Send service request to server
357
request_event = Event(
358
self.current_time,
359
"service_request",
360
{"customer": customer}
361
)
362
best_server.handle_event(request_event)
363
364
def log_service_completion(self, customer):
365
"""Log completed service."""
366
self.total_completions += 1
367
self.service_times.append(customer.service_time)
368
369
def get_statistics(self):
370
"""Get simulation statistics."""
371
return {
372
"total_arrivals": self.total_arrivals,
373
"total_completions": self.total_completions,
374
"avg_service_time": sum(self.service_times) / len(self.service_times) if self.service_times else 0,
375
"server_utilization": [s.customers_served / self.current_time * self.service_rate
376
for s in self.servers]
377
}
378
379
# Run DEVS simulation
380
model = QueuingModel(n_customers=10, n_servers=3, arrival_rate=0.8, service_rate=1.5)
381
model.run_until(100.0) # Run for 100 time units
382
383
stats = model.get_statistics()
384
print(f"Simulation completed at time {model.current_time}")
385
print(f"Arrivals: {stats['total_arrivals']}, Completions: {stats['total_completions']}")
386
print(f"Average service time: {stats['avg_service_time']:.2f}")
387
print(f"Server utilizations: {[f'{u:.2%}' for u in stats['server_utilization']]}")
388
```
389
390
## Reactive Programming (mesa_signals)
391
392
The mesa_signals module provides reactive programming capabilities with observable collections and signal-based communication.
393
394
```python { .api }
395
from mesa.experimental.mesa_signals import ObservableAgentSet, Signal, Observer
396
397
class Signal:
398
"""
399
Signal system for reactive communication between model components.
400
401
Enables decoupled communication through publisher-subscriber patterns
402
and reactive updates based on state changes.
403
"""
404
405
def __init__(self, name: str):
406
"""
407
Initialize signal.
408
409
Parameters:
410
name: Signal identifier
411
"""
412
...
413
414
def connect(self, handler: callable, sender=None):
415
"""
416
Connect a handler to this signal.
417
418
Parameters:
419
handler: Function to call when signal is emitted
420
sender: Optional specific sender to listen for
421
"""
422
...
423
424
def disconnect(self, handler: callable, sender=None):
425
"""
426
Disconnect a handler from this signal.
427
428
Parameters:
429
handler: Handler function to disconnect
430
sender: Optional specific sender
431
"""
432
...
433
434
def emit(self, sender=None, **kwargs):
435
"""
436
Emit the signal to all connected handlers.
437
438
Parameters:
439
sender: Object emitting the signal
440
**kwargs: Signal data payload
441
"""
442
...
443
444
class ObservableAgentSet(AgentSet):
445
"""
446
AgentSet with reactive capabilities and change notifications.
447
448
Extends standard AgentSet with signal emission for collection
449
changes, enabling reactive responses to agent additions/removals.
450
"""
451
452
def __init__(self, agents=None, random=None):
453
"""
454
Initialize observable agent set.
455
456
Parameters:
457
agents: Initial agents to include
458
random: Random number generator
459
"""
460
super().__init__(agents, random)
461
462
# Signals for collection changes
463
self.agent_added = Signal("agent_added")
464
self.agent_removed = Signal("agent_removed")
465
self.collection_changed = Signal("collection_changed")
466
467
def add(self, agent):
468
"""Add agent and emit signals."""
469
super().add(agent)
470
self.agent_added.emit(sender=self, agent=agent)
471
self.collection_changed.emit(sender=self, change_type="add", agent=agent)
472
473
def remove(self, agent):
474
"""Remove agent and emit signals."""
475
super().remove(agent)
476
self.agent_removed.emit(sender=self, agent=agent)
477
self.collection_changed.emit(sender=self, change_type="remove", agent=agent)
478
479
class Observer:
480
"""
481
Base class for reactive observers that respond to signals.
482
483
Provides infrastructure for objects that need to react to
484
changes in model state or agent collections.
485
"""
486
487
def __init__(self, name: str = None):
488
"""
489
Initialize observer.
490
491
Parameters:
492
name: Optional observer identifier
493
"""
494
self.name = name
495
self._connections = []
496
497
def observe(self, signal: Signal, handler: callable, sender=None):
498
"""
499
Start observing a signal.
500
501
Parameters:
502
signal: Signal to observe
503
handler: Method to call when signal emits
504
sender: Optional specific sender to observe
505
"""
506
signal.connect(handler, sender)
507
self._connections.append((signal, handler, sender))
508
509
def stop_observing(self, signal: Signal = None):
510
"""
511
Stop observing signals.
512
513
Parameters:
514
signal: Specific signal to stop observing (None for all)
515
"""
516
if signal:
517
connections_to_remove = [c for c in self._connections if c[0] == signal]
518
for conn in connections_to_remove:
519
signal.disconnect(conn[1], conn[2])
520
self._connections.remove(conn)
521
else:
522
# Disconnect all
523
for signal, handler, sender in self._connections:
524
signal.disconnect(handler, sender)
525
self._connections.clear()
526
```
527
528
### Reactive Programming Example
529
530
```python { .api }
531
from mesa import Agent, Model
532
from mesa.experimental.mesa_signals import ObservableAgentSet, Signal, Observer
533
534
class ReactiveAgent(Agent):
535
"""Agent that emits signals for state changes."""
536
537
def __init__(self, model, wealth=100):
538
super().__init__(model)
539
self.wealth = wealth
540
self._previous_wealth = wealth
541
542
# Signals for state changes
543
self.wealth_changed = Signal("wealth_changed")
544
self.became_wealthy = Signal("became_wealthy")
545
self.became_poor = Signal("became_poor")
546
547
def step(self):
548
# Simple wealth dynamics
549
change = self.random.randint(-10, 15)
550
self.wealth += change
551
self.wealth = max(0, self.wealth)
552
553
# Emit wealth change signal
554
if self.wealth != self._previous_wealth:
555
self.wealth_changed.emit(
556
sender=self,
557
old_wealth=self._previous_wealth,
558
new_wealth=self.wealth,
559
change=change
560
)
561
562
# Emit status change signals
563
if self._previous_wealth < 200 and self.wealth >= 200:
564
self.became_wealthy.emit(sender=self, wealth=self.wealth)
565
elif self._previous_wealth >= 50 and self.wealth < 50:
566
self.became_poor.emit(sender=self, wealth=self.wealth)
567
568
self._previous_wealth = self.wealth
569
570
class WealthTracker(Observer):
571
"""Observer that tracks wealth statistics."""
572
573
def __init__(self):
574
super().__init__("WealthTracker")
575
self.wealth_changes = []
576
self.wealthy_agents = set()
577
self.poor_agents = set()
578
579
def on_wealth_change(self, sender, old_wealth, new_wealth, change, **kwargs):
580
"""Handle wealth change events."""
581
self.wealth_changes.append({
582
'agent_id': sender.unique_id,
583
'old_wealth': old_wealth,
584
'new_wealth': new_wealth,
585
'change': change,
586
'timestamp': sender.model.steps
587
})
588
589
def on_became_wealthy(self, sender, wealth, **kwargs):
590
"""Handle agents becoming wealthy."""
591
self.wealthy_agents.add(sender.unique_id)
592
print(f"Agent {sender.unique_id} became wealthy with ${wealth}")
593
594
def on_became_poor(self, sender, wealth, **kwargs):
595
"""Handle agents becoming poor."""
596
self.poor_agents.add(sender.unique_id)
597
print(f"Agent {sender.unique_id} became poor with ${wealth}")
598
599
def get_statistics(self):
600
"""Get tracked statistics."""
601
return {
602
'total_changes': len(self.wealth_changes),
603
'wealthy_count': len(self.wealthy_agents),
604
'poor_count': len(self.poor_agents),
605
'avg_change': sum(c['change'] for c in self.wealth_changes) / len(self.wealth_changes)
606
if self.wealth_changes else 0
607
}
608
609
class ReactiveModel(Model):
610
"""Model using reactive programming patterns."""
611
612
def __init__(self, n_agents=50):
613
super().__init__()
614
615
# Use observable agent set
616
self._agents = ObservableAgentSet()
617
618
# Create wealth tracker
619
self.wealth_tracker = WealthTracker()
620
621
# Market signals
622
self.market_crash = Signal("market_crash")
623
self.market_boom = Signal("market_boom")
624
625
# Connect market event handlers
626
self.market_crash.connect(self.handle_market_crash)
627
self.market_boom.connect(self.handle_market_boom)
628
629
# Create agents and observe their signals
630
for i in range(n_agents):
631
agent = ReactiveAgent(self, wealth=self.random.randint(50, 200))
632
633
# Connect agent signals to tracker
634
self.wealth_tracker.observe(agent.wealth_changed, self.wealth_tracker.on_wealth_change)
635
self.wealth_tracker.observe(agent.became_wealthy, self.wealth_tracker.on_became_wealthy)
636
self.wealth_tracker.observe(agent.became_poor, self.wealth_tracker.on_became_poor)
637
638
# Observe agent collection changes
639
self._agents.agent_added.connect(self.on_agent_added)
640
self._agents.agent_removed.connect(self.on_agent_removed)
641
642
self.running = True
643
644
@property
645
def agents(self):
646
"""Override agents property to return observable set."""
647
return self._agents
648
649
def register_agent(self, agent):
650
"""Override to use observable agent set."""
651
self._agents.add(agent)
652
653
def deregister_agent(self, agent):
654
"""Override to use observable agent set."""
655
self._agents.discard(agent)
656
657
def on_agent_added(self, sender, agent, **kwargs):
658
"""Handle new agents being added."""
659
print(f"New agent {agent.unique_id} joined the simulation")
660
661
def on_agent_removed(self, sender, agent, **kwargs):
662
"""Handle agents being removed."""
663
print(f"Agent {agent.unique_id} left the simulation")
664
665
def handle_market_crash(self, sender, severity=0.5, **kwargs):
666
"""Handle market crash events."""
667
print(f"Market crash! Severity: {severity}")
668
for agent in self.agents:
669
loss = int(agent.wealth * severity)
670
agent.wealth = max(0, agent.wealth - loss)
671
672
def handle_market_boom(self, sender, multiplier=1.5, **kwargs):
673
"""Handle market boom events."""
674
print(f"Market boom! Multiplier: {multiplier}")
675
for agent in self.agents:
676
agent.wealth = int(agent.wealth * multiplier)
677
678
def step(self):
679
self.agents.shuffle_do("step")
680
681
# Random market events
682
if self.random.random() < 0.05: # 5% chance
683
if self.random.random() < 0.3: # 30% of events are crashes
684
self.market_crash.emit(sender=self, severity=self.random.uniform(0.2, 0.8))
685
else: # 70% are booms
686
self.market_boom.emit(sender=self, multiplier=self.random.uniform(1.1, 1.8))
687
688
# Run reactive simulation
689
model = ReactiveModel(n_agents=20)
690
for i in range(50):
691
model.step()
692
693
# Get wealth tracking statistics
694
stats = model.wealth_tracker.get_statistics()
695
print(f"\nWealth tracking results:")
696
print(f"Total wealth changes: {stats['total_changes']}")
697
print(f"Agents who became wealthy: {stats['wealthy_count']}")
698
print(f"Agents who became poor: {stats['poor_count']}")
699
print(f"Average wealth change: ${stats['avg_change']:.2f}")
700
```
701
702
## Enhanced Continuous Space
703
704
The experimental continuous space module provides improved continuous spatial modeling with enhanced performance and features.
705
706
```python { .api }
707
from mesa.experimental.continuous_space import ContinuousSpace3D, SpatialIndex
708
709
class ContinuousSpace3D:
710
"""
711
Three-dimensional continuous space with enhanced spatial queries.
712
713
Extends 2D continuous space to support 3D positioning and
714
spatial relationships with optimized neighbor searching.
715
"""
716
717
def __init__(self, x_max: float, y_max: float, z_max: float,
718
torus: bool = False, x_min: float = 0,
719
y_min: float = 0, z_min: float = 0):
720
"""
721
Initialize 3D continuous space.
722
723
Parameters:
724
x_max: Maximum x coordinate
725
y_max: Maximum y coordinate
726
z_max: Maximum z coordinate
727
torus: Whether space wraps around at boundaries
728
x_min: Minimum x coordinate
729
y_min: Minimum y coordinate
730
z_min: Minimum z coordinate
731
"""
732
...
733
734
def place_agent(self, agent, pos: tuple[float, float, float]):
735
"""
736
Place agent at 3D coordinates.
737
738
Parameters:
739
agent: Agent to place
740
pos: (x, y, z) coordinate tuple
741
"""
742
...
743
744
def move_agent(self, agent, pos: tuple[float, float, float]):
745
"""Move agent to new 3D coordinates."""
746
...
747
748
def get_neighbors(self, pos: tuple[float, float, float], radius: float,
749
include_center: bool = False) -> list[Agent]:
750
"""
751
Get agents within 3D radius of position.
752
753
Parameters:
754
pos: Center position (x, y, z)
755
radius: Search radius
756
include_center: Whether to include agents at exact center
757
"""
758
...
759
760
def get_distance_3d(self, pos_1: tuple[float, float, float],
761
pos_2: tuple[float, float, float]) -> float:
762
"""Get 3D Euclidean distance between positions."""
763
...
764
765
class SpatialIndex:
766
"""
767
Spatial index for efficient neighbor queries in continuous spaces.
768
769
Provides optimized spatial data structures for fast proximity
770
searches in large continuous spaces.
771
"""
772
773
def __init__(self, bounds: tuple[float, float, float, float]):
774
"""
775
Initialize spatial index.
776
777
Parameters:
778
bounds: (x_min, y_min, x_max, y_max) bounding rectangle
779
"""
780
...
781
782
def insert(self, agent, pos: tuple[float, float]):
783
"""Insert agent at position into spatial index."""
784
...
785
786
def remove(self, agent):
787
"""Remove agent from spatial index."""
788
...
789
790
def query_radius(self, pos: tuple[float, float], radius: float) -> list[Agent]:
791
"""Query agents within radius efficiently."""
792
...
793
794
def query_rectangle(self, bounds: tuple[float, float, float, float]) -> list[Agent]:
795
"""Query agents within rectangular bounds."""
796
...
797
```
798
799
## Meta Agents
800
801
Meta-agent functionality for hierarchical and composite agent systems.
802
803
```python { .api }
804
from mesa.experimental.meta_agents import MetaAgent, AgentGroup, HierarchicalAgent
805
806
class MetaAgent(Agent):
807
"""
808
Agent that can contain and manage other agents.
809
810
Enables hierarchical agent structures and composite behaviors
811
for complex multi-level agent-based models.
812
"""
813
814
def __init__(self, model, sub_agents=None):
815
"""
816
Initialize meta-agent.
817
818
Parameters:
819
model: The model instance
820
sub_agents: Initial sub-agents to contain
821
"""
822
super().__init__(model)
823
self.sub_agents = AgentSet(sub_agents or [])
824
...
825
826
def add_sub_agent(self, agent):
827
"""Add a sub-agent to this meta-agent."""
828
...
829
830
def remove_sub_agent(self, agent):
831
"""Remove a sub-agent from this meta-agent."""
832
...
833
834
def step(self):
835
"""Execute meta-agent behavior and coordinate sub-agents."""
836
# Meta-agent logic
837
self._execute_meta_behavior()
838
839
# Coordinate sub-agent actions
840
self.sub_agents.shuffle_do("step")
841
842
def _execute_meta_behavior(self):
843
"""Execute meta-level behavior - override in subclasses."""
844
pass
845
846
class AgentGroup(MetaAgent):
847
"""
848
Group of agents that act collectively.
849
850
Provides coordination mechanisms for groups of agents
851
to achieve collective goals and behaviors.
852
"""
853
854
def __init__(self, model, agents=None, coordination_strategy="consensus"):
855
"""
856
Initialize agent group.
857
858
Parameters:
859
model: Model instance
860
agents: Agents to include in group
861
coordination_strategy: How to coordinate group decisions
862
"""
863
super().__init__(model, agents)
864
self.coordination_strategy = coordination_strategy
865
self.group_goals = []
866
...
867
868
def add_group_goal(self, goal):
869
"""Add a collective goal for the group."""
870
...
871
872
def coordinate_action(self, action_type: str, **kwargs):
873
"""Coordinate a collective action among group members."""
874
if self.coordination_strategy == "consensus":
875
return self._consensus_coordination(action_type, **kwargs)
876
elif self.coordination_strategy == "leader":
877
return self._leader_coordination(action_type, **kwargs)
878
# Additional coordination strategies...
879
880
def _consensus_coordination(self, action_type: str, **kwargs):
881
"""Coordinate through consensus among group members."""
882
...
883
884
def _leader_coordination(self, action_type: str, **kwargs):
885
"""Coordinate through designated leader."""
886
...
887
```
888
889
## Visualization (Experimental)
890
891
Modern browser-based visualization using Solara.
892
893
```python { .api }
894
from mesa.visualization import SolaraViz, JupyterViz, make_space_altair
895
896
class SolaraViz:
897
"""
898
Browser-based visualization using Solara framework.
899
900
Provides interactive, modern web-based visualization for Mesa models
901
with real-time updates and user controls.
902
"""
903
904
def __init__(self,
905
model_cls,
906
model_params=None,
907
measures=None,
908
name="Mesa Model",
909
space_drawer=None,
910
agent_portrayal=None):
911
"""
912
Initialize Solara visualization.
913
914
Parameters:
915
model_cls: Mesa model class to visualize
916
model_params: Dictionary of model parameters with UI controls
917
measures: List of measures to plot over time
918
name: Display name for the model
919
space_drawer: Function to draw spatial elements
920
agent_portrayal: Function to define agent appearance
921
"""
922
...
923
924
def launch(self, port=8521):
925
"""
926
Launch the visualization server.
927
928
Parameters:
929
port: Port number for the web server
930
"""
931
...
932
933
class JupyterViz:
934
"""
935
Jupyter notebook visualization component.
936
937
Provides inline visualization widgets for Jupyter notebooks
938
with interactive controls and real-time model execution.
939
"""
940
941
def __init__(self,
942
model_cls,
943
model_params=None,
944
measures=None,
945
space_drawer=None,
946
agent_portrayal=None):
947
"""
948
Initialize Jupyter visualization.
949
950
Parameters:
951
model_cls: Mesa model class
952
model_params: Parameter controls
953
measures: Measures to display
954
space_drawer: Spatial visualization function
955
agent_portrayal: Agent appearance function
956
"""
957
...
958
959
def show(self):
960
"""Display the visualization widget in Jupyter."""
961
...
962
963
def make_space_altair(space_drawer):
964
"""
965
Create Altair-based space visualization.
966
967
Parameters:
968
space_drawer: Function that returns visualization data
969
970
Returns:
971
Altair chart specification for spatial visualization
972
"""
973
...
974
```
975
976
### Visualization Example
977
978
```python { .api }
979
from mesa import Agent, Model
980
from mesa.space import MultiGrid
981
from mesa.datacollection import DataCollector
982
from mesa.visualization import SolaraViz
983
import altair as alt
984
985
class VisualizationAgent(Agent):
986
def __init__(self, model, agent_type="normal"):
987
super().__init__(model)
988
self.agent_type = agent_type
989
self.energy = 100
990
self.size = self.random.uniform(0.5, 1.5)
991
992
def step(self):
993
# Simple movement
994
neighbors = self.model.grid.get_neighborhood(self.pos, moore=True)
995
new_pos = self.random.choice(neighbors)
996
self.model.grid.move_agent(self, new_pos)
997
998
# Energy dynamics
999
self.energy += self.random.randint(-5, 10)
1000
self.energy = max(0, min(200, self.energy))
1001
1002
class VisualizationModel(Model):
1003
def __init__(self, n_agents=50, width=20, height=20, agent_types=None):
1004
super().__init__()
1005
1006
self.grid = MultiGrid(width, height, torus=True)
1007
1008
self.datacollector = DataCollector(
1009
model_reporters={
1010
"Total Agents": lambda m: len(m.agents),
1011
"Average Energy": lambda m: sum(a.energy for a in m.agents) / len(m.agents),
1012
"High Energy Agents": lambda m: len([a for a in m.agents if a.energy > 150])
1013
}
1014
)
1015
1016
agent_types = agent_types or ["normal", "special", "rare"]
1017
1018
# Create agents
1019
for i in range(n_agents):
1020
agent_type = self.random.choice(agent_types)
1021
agent = VisualizationAgent(self, agent_type=agent_type)
1022
1023
# Place randomly
1024
x = self.random.randrange(width)
1025
y = self.random.randrange(height)
1026
self.grid.place_agent(agent, (x, y))
1027
1028
self.running = True
1029
1030
def step(self):
1031
self.datacollector.collect(self)
1032
self.agents.shuffle_do("step")
1033
1034
# Agent portrayal for visualization
1035
def agent_portrayal(agent):
1036
"""Define how agents appear in visualization."""
1037
size = agent.size * 20 # Scale for visibility
1038
1039
# Color by type and energy
1040
if agent.agent_type == "special":
1041
color = "blue"
1042
elif agent.agent_type == "rare":
1043
color = "red"
1044
else:
1045
color = "green"
1046
1047
# Adjust opacity by energy
1048
opacity = agent.energy / 200.0
1049
1050
return {
1051
"color": color,
1052
"size": size,
1053
"opacity": opacity,
1054
"shape": "circle"
1055
}
1056
1057
# Model parameters for UI controls
1058
model_params = {
1059
"n_agents": {
1060
"type": "SliderInt",
1061
"value": 50,
1062
"label": "Number of Agents",
1063
"min": 10,
1064
"max": 200,
1065
"step": 10
1066
},
1067
"width": {
1068
"type": "SliderInt",
1069
"value": 20,
1070
"label": "Grid Width",
1071
"min": 10,
1072
"max": 50
1073
},
1074
"height": {
1075
"type": "SliderInt",
1076
"value": 20,
1077
"label": "Grid Height",
1078
"min": 10,
1079
"max": 50
1080
}
1081
}
1082
1083
# Measures to plot
1084
measures = ["Total Agents", "Average Energy", "High Energy Agents"]
1085
1086
# Create and launch visualization
1087
viz = SolaraViz(
1088
model_cls=VisualizationModel,
1089
model_params=model_params,
1090
measures=measures,
1091
name="Agent Energy Dynamics",
1092
agent_portrayal=agent_portrayal
1093
)
1094
1095
# Launch in browser (uncomment to run)
1096
# viz.launch(port=8521)
1097
1098
# Or for Jupyter notebooks
1099
jupyter_viz = JupyterViz(
1100
model_cls=VisualizationModel,
1101
model_params=model_params,
1102
measures=measures,
1103
agent_portrayal=agent_portrayal
1104
)
1105
1106
# Display in notebook (uncomment to run)
1107
# jupyter_viz.show()
1108
```
1109
1110
## Migration and Compatibility
1111
1112
### Using Experimental Features Safely
1113
1114
```python { .api }
1115
# Pin Mesa version when using experimental features
1116
# requirements.txt:
1117
# Mesa==3.2.0 # Specific version for API stability
1118
1119
# Graceful fallbacks for missing experimental features
1120
try:
1121
from mesa.experimental.devs import DEVSModel
1122
HAS_DEVS = True
1123
except ImportError:
1124
HAS_DEVS = False
1125
DEVSModel = None
1126
1127
class AdaptiveModel(Model):
1128
"""Model that adapts to available experimental features."""
1129
1130
def __init__(self, use_devs=True, **kwargs):
1131
if use_devs and HAS_DEVS:
1132
# Use DEVS if available
1133
self._use_devs_features()
1134
else:
1135
# Fall back to standard Mesa
1136
super().__init__(**kwargs)
1137
1138
def _use_devs_features(self):
1139
"""Initialize DEVS-specific features."""
1140
# DEVS initialization code
1141
pass
1142
```
1143
1144
## Best Practices for Experimental Features
1145
1146
1. **Version Pinning**: Always pin specific Mesa versions when using experimental features
1147
2. **Fallback Strategies**: Implement graceful degradation when experimental features aren't available
1148
3. **Documentation**: Document which experimental features your model uses
1149
4. **Testing**: Thoroughly test models using experimental features across Mesa versions
1150
5. **Community Engagement**: Provide feedback on experimental features to help guide development
1151
1152
The experimental package represents the cutting edge of Mesa development. While these features provide powerful new capabilities, use them thoughtfully in production environments and stay engaged with the Mesa community for updates and best practices.