Mesa 3.1.4__py3-none-any.whl → 3.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. mesa/__init__.py +3 -1
  2. mesa/agent.py +26 -9
  3. mesa/batchrunner.py +6 -3
  4. mesa/discrete_space/__init__.py +50 -0
  5. mesa/{experimental/cell_space → discrete_space}/cell.py +29 -10
  6. mesa/{experimental/cell_space → discrete_space}/cell_agent.py +1 -1
  7. mesa/{experimental/cell_space → discrete_space}/cell_collection.py +3 -3
  8. mesa/{experimental/cell_space → discrete_space}/discrete_space.py +65 -3
  9. mesa/{experimental/cell_space → discrete_space}/grid.py +2 -2
  10. mesa/{experimental/cell_space → discrete_space}/network.py +22 -2
  11. mesa/{experimental/cell_space → discrete_space}/property_layer.py +1 -10
  12. mesa/{experimental/cell_space → discrete_space}/voronoi.py +2 -2
  13. mesa/examples/README.md +10 -5
  14. mesa/examples/__init__.py +2 -0
  15. mesa/examples/advanced/alliance_formation/Readme.md +50 -0
  16. mesa/examples/advanced/alliance_formation/__init__ .py +0 -0
  17. mesa/examples/advanced/alliance_formation/agents.py +20 -0
  18. mesa/examples/advanced/alliance_formation/app.py +71 -0
  19. mesa/examples/advanced/alliance_formation/model.py +184 -0
  20. mesa/examples/advanced/epstein_civil_violence/agents.py +1 -1
  21. mesa/examples/advanced/epstein_civil_violence/model.py +1 -1
  22. mesa/examples/advanced/pd_grid/Readme.md +4 -6
  23. mesa/examples/advanced/pd_grid/agents.py +1 -1
  24. mesa/examples/advanced/pd_grid/model.py +1 -1
  25. mesa/examples/advanced/sugarscape_g1mt/Readme.md +4 -5
  26. mesa/examples/advanced/sugarscape_g1mt/agents.py +1 -1
  27. mesa/examples/advanced/sugarscape_g1mt/model.py +2 -2
  28. mesa/examples/advanced/wolf_sheep/Readme.md +2 -17
  29. mesa/examples/advanced/wolf_sheep/agents.py +1 -1
  30. mesa/examples/advanced/wolf_sheep/app.py +2 -1
  31. mesa/examples/advanced/wolf_sheep/model.py +1 -1
  32. mesa/examples/basic/boid_flockers/Readme.md +6 -1
  33. mesa/examples/basic/boid_flockers/agents.py +1 -0
  34. mesa/examples/basic/boid_flockers/app.py +17 -2
  35. mesa/examples/basic/boid_flockers/model.py +12 -0
  36. mesa/examples/basic/boltzmann_wealth_model/Readme.md +2 -12
  37. mesa/examples/basic/boltzmann_wealth_model/agents.py +6 -11
  38. mesa/examples/basic/boltzmann_wealth_model/app.py +2 -2
  39. mesa/examples/basic/boltzmann_wealth_model/model.py +7 -11
  40. mesa/examples/basic/conways_game_of_life/Readme.md +1 -9
  41. mesa/examples/basic/conways_game_of_life/agents.py +13 -5
  42. mesa/examples/basic/conways_game_of_life/model.py +10 -7
  43. mesa/examples/basic/schelling/Readme.md +0 -8
  44. mesa/examples/basic/schelling/agents.py +13 -8
  45. mesa/examples/basic/schelling/model.py +6 -9
  46. mesa/examples/basic/virus_on_network/Readme.md +0 -4
  47. mesa/examples/basic/virus_on_network/agents.py +13 -17
  48. mesa/examples/basic/virus_on_network/model.py +20 -24
  49. mesa/experimental/__init__.py +2 -2
  50. mesa/experimental/cell_space/__init__.py +18 -8
  51. mesa/experimental/meta_agents/__init__.py +25 -0
  52. mesa/experimental/meta_agents/meta_agent.py +387 -0
  53. mesa/model.py +3 -3
  54. mesa/space.py +1 -12
  55. mesa/visualization/__init__.py +2 -0
  56. mesa/visualization/command_console.py +482 -0
  57. mesa/visualization/components/altair_components.py +276 -16
  58. mesa/visualization/mpl_space_drawing.py +17 -9
  59. mesa/visualization/solara_viz.py +150 -21
  60. {mesa-3.1.4.dist-info → mesa-3.2.0.dist-info}/METADATA +12 -8
  61. mesa-3.2.0.dist-info/RECORD +105 -0
  62. mesa-3.1.4.dist-info/RECORD +0 -96
  63. {mesa-3.1.4.dist-info → mesa-3.2.0.dist-info}/WHEEL +0 -0
  64. {mesa-3.1.4.dist-info → mesa-3.2.0.dist-info}/licenses/LICENSE +0 -0
  65. {mesa-3.1.4.dist-info → mesa-3.2.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,482 @@
1
+ """A command console interface for interactive Python code execution in the browser.
2
+
3
+ This module provides a set of classes and functions to create an interactive Python console
4
+ that can be embedded in a web browser. It supports command history, multi-line code blocks,
5
+ and special commands for console management.
6
+
7
+ Notes:
8
+ - The console supports multi-line code blocks with proper indentation
9
+ - Output is captured and displayed with appropriate formatting
10
+ - Error messages are displayed in red with distinct styling
11
+ - The console maintains a history of commands and their outputs
12
+ """
13
+
14
+ import code
15
+ import io
16
+ import sys
17
+ from collections.abc import Callable
18
+ from dataclasses import dataclass
19
+
20
+ import solara
21
+ from solara.components.input import use_change
22
+
23
+ from mesa.visualization.utils import force_update
24
+
25
+
26
+ @dataclass
27
+ class ConsoleEntry:
28
+ """A class to store command console entries.
29
+
30
+ Attributes:
31
+ command (str): The command entered
32
+ output (str): The output of the command
33
+ is_error (bool): Whether the entry represents an error
34
+ is_continuation (bool): Whether the entry is a continuation of previous command
35
+ """
36
+
37
+ command: str
38
+ output: str = ""
39
+ is_error: bool = False
40
+ is_continuation: bool = False
41
+
42
+ def __repr__(self):
43
+ """Return a string representation of the ConsoleEntry."""
44
+ return f"ConsoleEntry({self.command}, {self.output}, {self.is_error}, {self.is_continuation})"
45
+
46
+
47
+ class CaptureOutput:
48
+ """A context manager for capturing stdout and stderr output.
49
+
50
+ This class provides a way to capture output that would normally be printed
51
+ to stdout and stderr during the execution of code within its context.
52
+ """
53
+
54
+ def __init__(self):
55
+ """Initialize the CaptureOutput context manager with empty string buffers."""
56
+ self.stdout = io.StringIO()
57
+ self.stderr = io.StringIO()
58
+ self._old_stdout = None
59
+ self._old_stderr = None
60
+
61
+ def __enter__(self):
62
+ """Set up the context manager by redirecting stdout and stderr.
63
+
64
+ Returns:
65
+ self: The context manager instance
66
+ """
67
+ self._old_stdout = sys.stdout
68
+ self._old_stderr = sys.stderr
69
+ sys.stdout = self.stdout
70
+ sys.stderr = self.stderr
71
+ return self
72
+
73
+ def __exit__(self, exc_type, exc_val, exc_tb):
74
+ """Restore the original stdout and stderr when exiting the context."""
75
+ sys.stdout = self._old_stdout
76
+ sys.stderr = self._old_stderr
77
+
78
+ def get_output(self):
79
+ """Retrieve and clear the captured output.
80
+
81
+ Returns:
82
+ tuple: A pair of strings (stdout_output, stderr_output)
83
+ """
84
+ output = self.stdout.getvalue()
85
+ error = self.stderr.getvalue()
86
+ self.stdout.seek(0)
87
+ self.stdout.truncate(0)
88
+ self.stderr.seek(0)
89
+ self.stderr.truncate(0)
90
+ return output, error
91
+
92
+
93
+ class InteractiveConsole(code.InteractiveConsole):
94
+ """A custom interactive Python console with output capturing capabilities.
95
+
96
+ This class extends code.InteractiveConsole to provide output capturing functionality
97
+ when executing Python code interactively.
98
+
99
+ Args:
100
+ locals_dict (dict, optional): Dictionary of local variables. Defaults to None.
101
+ """
102
+
103
+ def __init__(self, locals_dict=None):
104
+ """Initialize the InteractiveConsole with the provided locals dictionary."""
105
+ super().__init__(locals=locals_dict or {})
106
+ self.capturer = CaptureOutput()
107
+
108
+ def push(self, line):
109
+ """Push a line to the command interpreter and execute it.
110
+
111
+ This method captures the output of the executed command and returns both
112
+ the 'more' flag and the captured output.
113
+
114
+ Args:
115
+ line (str): The line of code to be executed.
116
+
117
+ Returns:
118
+ tuple: A tuple containing:
119
+ - more (bool): Flag indicating if more input is needed
120
+ - str: The captured output from executing the command
121
+ """
122
+ with self.capturer:
123
+ more = super().push(line)
124
+ return more, self.capturer.get_output()
125
+
126
+
127
+ class ConsoleManager:
128
+ """A console manager for executing Python code interactively.
129
+
130
+ This class provides functionality to execute Python code in an interactive console environment,
131
+ maintain command history, and handle multi-line code blocks.
132
+
133
+ Attributes:
134
+ locals_dict (dict): Dictionary containing local variables available to the console
135
+ console (InteractiveConsole): Python's interactive console instance
136
+ buffer (list): Buffer for storing multi-line code blocks
137
+ history (list[ConsoleEntry]): List of console entries containing commands and their outputs
138
+ Special Commands:
139
+ 1. `history` : Shows the command history
140
+ 2. `cls` : Clears the console screen
141
+ 3. `tips` : Shows available console commands and usage tips
142
+ Example:
143
+ >>> console = ConsoleManager(model=my_model)
144
+ >>> console.execute_code("print('hello world')", set_input_callback)
145
+ """
146
+
147
+ def __init__(self, model=None, additional_imports=None):
148
+ """Initialize the console manager with the provided model and imports."""
149
+ # Create locals dictionary with model and imports
150
+ locals_dict = {}
151
+ if model is not None:
152
+ locals_dict["model"] = model
153
+ if additional_imports:
154
+ locals_dict.update(additional_imports)
155
+
156
+ self.locals_dict = locals_dict
157
+ self.console = InteractiveConsole(locals_dict)
158
+ self.buffer = []
159
+ self.history: list[ConsoleEntry] = []
160
+ self.history_index = -1
161
+ self.current_input = ""
162
+
163
+ def execute_code(
164
+ self, code_line: str, set_input_text: Callable[[str], None]
165
+ ) -> None:
166
+ """Execute the provided code line and update the console history."""
167
+ # Custom Commands
168
+ # A. History
169
+ if code_line == "history":
170
+ # Get the current history except the custom commands
171
+ cur_his = [
172
+ (
173
+ f"Command: {entry.command}, \nOutput: {entry.output if entry.output else None}\n"
174
+ )
175
+ for entry in self.history
176
+ if entry.command != "[history]"
177
+ and entry.command != "[tips]"
178
+ and entry.command != ""
179
+ ]
180
+
181
+ self.history.append(
182
+ ConsoleEntry(
183
+ command="[history]",
184
+ output="\n".join(cur_his) if cur_his else "No history",
185
+ is_error=False,
186
+ is_continuation=False,
187
+ )
188
+ )
189
+
190
+ set_input_text("")
191
+ return
192
+
193
+ # B. Clear
194
+ if code_line == "cls":
195
+ self.clear_console()
196
+ set_input_text("")
197
+ return
198
+
199
+ # C. Tips
200
+ if code_line == "tips":
201
+ self.history.append(
202
+ ConsoleEntry(
203
+ command="[tips]",
204
+ output="Available Console Commands:\n1. Press Enter to execute a command\n2. Type 'cls' to clear the console screen (it doesn't delete past variables and functions)\n3. Type 'history' to view previous commands\n4. Press Enter on an empty line to complete a multiline block\n5. Use proper indentation for multiline blocks\n6. The console will show '..: ' for continuation lines",
205
+ is_error=False,
206
+ is_continuation=False,
207
+ )
208
+ )
209
+
210
+ set_input_text("")
211
+ return
212
+
213
+ # Handle empty lines
214
+ if not code_line.strip():
215
+ # If we have a buffer, complete the block
216
+ if self.buffer:
217
+ full_code = "\n".join(self.buffer)
218
+ more, (output, error) = self.console.push("")
219
+
220
+ # Remove the redundant commands from the history
221
+ for _ in range(len(self.buffer) - 1):
222
+ self.history.pop()
223
+
224
+ # Completing a multi-line block
225
+ if self.history:
226
+ self.history[-1].command = full_code
227
+ self.history[-1].output = error if error else output
228
+ self.history[-1].is_error = bool(error)
229
+ self.history[-1].is_continuation = False
230
+ self.buffer = []
231
+ else:
232
+ # Empty line with no buffer - just add a blank entry
233
+ self.history.append(ConsoleEntry(command=""))
234
+ set_input_text("")
235
+ return
236
+
237
+ # Execute the line
238
+ more, (output, error) = self.console.push(code_line)
239
+
240
+ # Force update to display any changes to the model
241
+ force_update()
242
+
243
+ # If this is the start of a multi-line block
244
+ if more:
245
+ self.buffer.append(code_line)
246
+ self.history.append(
247
+ ConsoleEntry(
248
+ command=code_line,
249
+ output="", # Don't show partial output for incomplete blocks
250
+ is_error=False,
251
+ is_continuation=True,
252
+ )
253
+ )
254
+ else:
255
+ # Single complete command
256
+ if not self.buffer:
257
+ # Normal single-line command
258
+ self.history.append(
259
+ ConsoleEntry(
260
+ command=code_line,
261
+ output=error if error else output,
262
+ is_error=bool(error),
263
+ is_continuation=False,
264
+ )
265
+ )
266
+ else:
267
+ # Remove the redundant commands from the history
268
+ for _ in range(len(self.buffer) - 1):
269
+ self.history.pop()
270
+
271
+ # Completing a multi-line block
272
+ self.buffer.append(code_line)
273
+ full_code = "\n".join(self.buffer)
274
+ if self.history:
275
+ self.history[-1].command = full_code
276
+ self.history[-1].output = error if error else output
277
+ self.history[-1].is_error = bool(error)
278
+ self.history[-1].is_continuation = False
279
+ self.buffer = []
280
+
281
+ set_input_text("")
282
+
283
+ def clear_console(self) -> None:
284
+ """Clear the console history and reset the console state."""
285
+ self.history.clear()
286
+ self.buffer.clear()
287
+ self.history_index = -1
288
+ self.current_input = ""
289
+ # Reset the console while maintaining the locals dictionary
290
+ self.console = InteractiveConsole(self.locals_dict)
291
+
292
+ def get_entries(self) -> list[ConsoleEntry]:
293
+ """Get the list of console entries."""
294
+ return self.history
295
+
296
+ def prev_command(
297
+ self, current_text: str, set_input_text: Callable[[str], None]
298
+ ) -> None:
299
+ """Navigate to previous command in history."""
300
+ if not self.history:
301
+ return
302
+
303
+ # Save the current input
304
+ if self.history_index == -1:
305
+ self.current_input = current_text
306
+
307
+ # Move up in history
308
+ if self.history_index == -1:
309
+ self.history_index = len(self.history) - 1
310
+ elif self.history_index > 0:
311
+ self.history_index -= 1
312
+
313
+ # Set text to the historical command
314
+ if 0 <= self.history_index < len(self.history):
315
+ set_input_text(self.history[self.history_index].command)
316
+
317
+ def next_command(self, set_input_text: Callable[[str], None]) -> None:
318
+ """Navigate to next command in history."""
319
+ if self.history_index == -1:
320
+ return # Not in history navigation mode
321
+
322
+ # Move down in history
323
+ self.history_index += 1
324
+
325
+ # If we've moved past the end of history, restore the saved input
326
+ if self.history_index >= len(self.history):
327
+ self.history_index = -1
328
+ set_input_text(self.current_input)
329
+ else:
330
+ set_input_text(self.history[self.history_index].command)
331
+
332
+
333
+ def format_command_html(entry):
334
+ """Format the command part of a console entry as HTML."""
335
+ prompt = "..: " if entry.is_continuation else ">>> "
336
+ prompt_color = "#6c757d" if entry.is_continuation else "#2196F3"
337
+
338
+ # Handle multi-line commands with proper formatting
339
+ command_lines = entry.command.split("\n")
340
+ formatted_lines = []
341
+
342
+ for i, line in enumerate(command_lines):
343
+ line_prompt = prompt if i == 0 else "..: "
344
+ line_prompt_color = prompt_color if i == 0 else "#6c757d"
345
+ formatted_lines.append(
346
+ f'<span style="color: {line_prompt_color};">{line_prompt}</span><span>{line.removeprefix(">>> ").removeprefix("..: ")}</span>'
347
+ )
348
+
349
+ command_html = "<br>".join(formatted_lines)
350
+
351
+ return f"""
352
+ <div style="margin: 0px 0 0 0;">
353
+ <div style="background-color: #f5f5f5; padding: 6px 8px; border-radius: 4px; font-family: 'Consolas', monospace; font-size: 0.9em;">
354
+ {command_html}
355
+ </div>
356
+ """
357
+
358
+
359
+ def format_output_html(entry):
360
+ """Format the output part of a console entry as HTML."""
361
+ if not entry.output:
362
+ return "</div>"
363
+
364
+ escaped_output = (
365
+ entry.output.replace("&", "&amp;")
366
+ .replace("<", "&lt;")
367
+ .replace(">", "&gt;")
368
+ .replace(" ", "&nbsp;")
369
+ .replace("\n", "<br>")
370
+ )
371
+
372
+ return f"""
373
+ <div style="background-color: #ffffff; padding: 6px 8px; border-left: 3px solid {"#ff3860" if entry.is_error else "#2196F3"}; margin-top: 2px; font-family: 'Consolas', monospace; font-size: 0.9em; {"color: #ff3860;" if entry.is_error else ""}">
374
+ {escaped_output}
375
+ </div>
376
+ </div>
377
+ """
378
+
379
+
380
+ @solara.component
381
+ def ConsoleInput(on_submit, on_up, on_down):
382
+ """A solara component for handling console input."""
383
+ input_text, set_input_text = solara.use_state("")
384
+
385
+ def handle_submit(*ignore_args):
386
+ on_submit(input_text, set_input_text)
387
+
388
+ def handle_up(*ignore_args):
389
+ on_up(input_text, set_input_text)
390
+
391
+ def handle_down(*ignore_args):
392
+ on_down(set_input_text)
393
+
394
+ input_elem = solara.v.TextField(
395
+ v_model=input_text,
396
+ on_v_model=set_input_text,
397
+ flat=True,
398
+ hide_details=True,
399
+ dense=True,
400
+ height="auto",
401
+ background_color="transparent",
402
+ style_="font-family: monospace; border: none; box-shadow: none; padding: 0; margin: 0; background-color: transparent; color: #000; flex-grow: 1;",
403
+ placeholder="",
404
+ solo=False,
405
+ filled=False,
406
+ outlined=False,
407
+ id="console-input",
408
+ attributes={
409
+ "spellcheck": "false",
410
+ "autocomplete": "off",
411
+ },
412
+ )
413
+
414
+ # Bind key events with the input element
415
+ use_change(input_elem, handle_submit, update_events=["keypress.enter"])
416
+ use_change(input_elem, handle_up, update_events=["keyup.38"]) # 38 -> Up arrow
417
+ use_change(
418
+ input_elem, handle_down, update_events=["keydown.40"]
419
+ ) # 40 -> Down arrow
420
+
421
+ return input_elem
422
+
423
+
424
+ @solara.component
425
+ def CommandConsole(model=None, additional_imports=None):
426
+ """A solara component for executing Python code interactively in the browser."""
427
+ # Initialize state for the console manager
428
+ console_ref = solara.use_ref(None)
429
+ if console_ref.current is None:
430
+ console_ref.current = ConsoleManager(
431
+ model=model, additional_imports=additional_imports
432
+ )
433
+
434
+ # State to trigger re-renders
435
+ refresh, set_refresh = solara.use_state(0)
436
+
437
+ def handle_code_execution(code, set_input_text):
438
+ console_ref.current.execute_code(code, set_input_text)
439
+ set_refresh(refresh + 1)
440
+
441
+ def handle_up(current_text, set_input_text):
442
+ console_ref.current.prev_command(current_text, set_input_text)
443
+ set_refresh(refresh + 1)
444
+
445
+ def handle_down(set_input_text):
446
+ console_ref.current.next_command(set_input_text)
447
+ set_refresh(refresh + 1)
448
+
449
+ with solara.Column(
450
+ style={
451
+ "height": "300px",
452
+ "overflow-y": "auto",
453
+ "gap": "0px",
454
+ "box-shadow": "inset 0 0 10px rgba(0,0,0,0.1)",
455
+ "border": "3px solid #e0e0e0",
456
+ "border-radius": "6px",
457
+ "padding": "8px",
458
+ }
459
+ ):
460
+ console_entries = console_ref.current.get_entries()
461
+
462
+ # Display history entries with auto-scrolling
463
+ with solara.v.ScrollYTransition(group=True):
464
+ for entry in console_entries:
465
+ with solara.Div():
466
+ command_html = format_command_html(entry)
467
+ output_html = format_output_html(entry)
468
+ solara.Markdown(command_html + output_html)
469
+
470
+ # Input row that adapts to content above it
471
+ with solara.Row(
472
+ style={"align-items": "center", "margin": "0", "width": "94.5%"}
473
+ ):
474
+ solara.Text(">>> ", style={"color": "#0066cc"})
475
+ ConsoleInput(
476
+ on_submit=handle_code_execution, on_up=handle_up, on_down=handle_down
477
+ )
478
+
479
+ solara.Markdown(
480
+ "*Type 'tips' for usage instructions.*",
481
+ style="font-size: 0.8em; color: #666;",
482
+ )