From 06cbb637ba2c1586056f936f185981e8cffc1fac Mon Sep 17 00:00:00 2001 From: Colin Frisch Date: Wed, 26 Mar 2025 22:39:51 +0800 Subject: [PATCH 1/4] first proposal for a memory module in mesa --- mesa/experimental/devs/memory.py | 107 +++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 mesa/experimental/devs/memory.py diff --git a/mesa/experimental/devs/memory.py b/mesa/experimental/devs/memory.py new file mode 100644 index 00000000000..a7973a553d1 --- /dev/null +++ b/mesa/experimental/devs/memory.py @@ -0,0 +1,107 @@ +"""An entry-recording functionality designed to be used as the memory of an agent. + +This module provides the foundational class needed for storing information in the memory of an agent. +Then objective is to implement a very simple and efficient system that can be used for any agent and +for any kind of information (an entry). The user defines the capacity of the memory chooses the format +of the entries, which allows for a greater flexibility. Key features: + +- Capacity-based memory, with a FIFO system +- Efficient storage and retrieval of entries +- Support for different entry_types of entries +- Possibility to send entries to other agents + +For now, the module contains only one main component: +- Memory: A class representing the memory of an agent + +""" + +# from experimental.devs.eventlist import EventList, SimulationEvent +import copy +import itertools +from collections import OrderedDict +from typing import Any + +from mesa.model import Model + + +class Memory: + """The memory of an Agent : it can store any kind of information based on a unique id of the form (agent_id, entry_id) to ensure the uniqueness of the entry. + + Attributes: + model (Model): The used model + agent_id (int): The id of the agent + capacity (int): The capacity of the memory + + Structure of one entry (example): + "entry_id" : { + "external_agent_id" : external_agent_id, # Not mandatory : represents the agent that sent the entry (if memory was received) + "entry_step" : 1, + "entry_type" : "position", + "entry_content" : [1,2] + }, + + """ + + def __init__(self, model: Model, agent_id: int, capacity: int): + """Initializes the agent memory.""" + self.model = model + self.capacity = capacity + self.agent_id = agent_id + self._ids = itertools.count() + + self.memory_storage = OrderedDict() + + def remember( + self, entry_content: Any, entry_type: Any, external_agent_id=None + ) -> tuple: + """Store an entry in the memory.""" + entry_id = (self.agent_id, next(self._ids)) + + # creation of a new entry in the memory + if entry_id not in self.memory_storage: + self.memory_storage[entry_id] = OrderedDict() + + self.memory_storage[entry_id]["entry_content"] = entry_content + self.memory_storage[entry_id]["entry_type"] = entry_type + self.memory_storage[entry_id]["entry_step"] = self.model.steps + + if external_agent_id is not None: + self.memory_storage[entry_id]["external_agent_id"] = external_agent_id + + # if the memory is longer than the capacity, we remove the oldest entry + if len(self.memory_storage) > self.capacity: + self.memory_storage.popitem(last=False) + + return entry_id + + def recall(self, entry_id): + """Recall a specific entry.""" + # Verification of the existence of the entry + if entry_id not in self.memory_storage: + return None + return self.memory_storage[entry_id] + + def get_by_type(self, entry_type: str) -> list: + """Returns all the ids of the entries of a specific entry_type.""" + entry_list = [ + entry_id + for entry_id, entry in self.memory_storage.items() + if entry["entry_type"] == entry_type + ] + return entry_list + + def forget(self, entry_id): + """Forget a specific entry.""" + if entry_id in self.memory_storage: + self.memory_storage.pop(entry_id) + + def tell_to(self, entry_id, external_agent): + """Send a precise memory to another agent by making a deep copy of the entry.""" + entry_copy = copy.deepcopy(self.memory_storage[entry_id]) + new_entry_id = external_agent.memory.remember( + entry_copy["entry_content"], + entry_copy["entry_type"], + external_agent_id=self.agent_id, + ) + + return new_entry_id From 34e991a975407f8c0e0f9d349d217ac7b58c4514 Mon Sep 17 00:00:00 2001 From: Colin Frisch Date: Tue, 1 Apr 2025 22:55:26 +0800 Subject: [PATCH 2/4] memory version 2 --- mesa/experimental/devs/memory.py | 107 ---------- mesa/experimental/memory/__init__.py | 30 +++ mesa/experimental/memory/memory.py | 286 +++++++++++++++++++++++++++ 3 files changed, 316 insertions(+), 107 deletions(-) delete mode 100644 mesa/experimental/devs/memory.py create mode 100644 mesa/experimental/memory/__init__.py create mode 100644 mesa/experimental/memory/memory.py diff --git a/mesa/experimental/devs/memory.py b/mesa/experimental/devs/memory.py deleted file mode 100644 index a7973a553d1..00000000000 --- a/mesa/experimental/devs/memory.py +++ /dev/null @@ -1,107 +0,0 @@ -"""An entry-recording functionality designed to be used as the memory of an agent. - -This module provides the foundational class needed for storing information in the memory of an agent. -Then objective is to implement a very simple and efficient system that can be used for any agent and -for any kind of information (an entry). The user defines the capacity of the memory chooses the format -of the entries, which allows for a greater flexibility. Key features: - -- Capacity-based memory, with a FIFO system -- Efficient storage and retrieval of entries -- Support for different entry_types of entries -- Possibility to send entries to other agents - -For now, the module contains only one main component: -- Memory: A class representing the memory of an agent - -""" - -# from experimental.devs.eventlist import EventList, SimulationEvent -import copy -import itertools -from collections import OrderedDict -from typing import Any - -from mesa.model import Model - - -class Memory: - """The memory of an Agent : it can store any kind of information based on a unique id of the form (agent_id, entry_id) to ensure the uniqueness of the entry. - - Attributes: - model (Model): The used model - agent_id (int): The id of the agent - capacity (int): The capacity of the memory - - Structure of one entry (example): - "entry_id" : { - "external_agent_id" : external_agent_id, # Not mandatory : represents the agent that sent the entry (if memory was received) - "entry_step" : 1, - "entry_type" : "position", - "entry_content" : [1,2] - }, - - """ - - def __init__(self, model: Model, agent_id: int, capacity: int): - """Initializes the agent memory.""" - self.model = model - self.capacity = capacity - self.agent_id = agent_id - self._ids = itertools.count() - - self.memory_storage = OrderedDict() - - def remember( - self, entry_content: Any, entry_type: Any, external_agent_id=None - ) -> tuple: - """Store an entry in the memory.""" - entry_id = (self.agent_id, next(self._ids)) - - # creation of a new entry in the memory - if entry_id not in self.memory_storage: - self.memory_storage[entry_id] = OrderedDict() - - self.memory_storage[entry_id]["entry_content"] = entry_content - self.memory_storage[entry_id]["entry_type"] = entry_type - self.memory_storage[entry_id]["entry_step"] = self.model.steps - - if external_agent_id is not None: - self.memory_storage[entry_id]["external_agent_id"] = external_agent_id - - # if the memory is longer than the capacity, we remove the oldest entry - if len(self.memory_storage) > self.capacity: - self.memory_storage.popitem(last=False) - - return entry_id - - def recall(self, entry_id): - """Recall a specific entry.""" - # Verification of the existence of the entry - if entry_id not in self.memory_storage: - return None - return self.memory_storage[entry_id] - - def get_by_type(self, entry_type: str) -> list: - """Returns all the ids of the entries of a specific entry_type.""" - entry_list = [ - entry_id - for entry_id, entry in self.memory_storage.items() - if entry["entry_type"] == entry_type - ] - return entry_list - - def forget(self, entry_id): - """Forget a specific entry.""" - if entry_id in self.memory_storage: - self.memory_storage.pop(entry_id) - - def tell_to(self, entry_id, external_agent): - """Send a precise memory to another agent by making a deep copy of the entry.""" - entry_copy = copy.deepcopy(self.memory_storage[entry_id]) - new_entry_id = external_agent.memory.remember( - entry_copy["entry_content"], - entry_copy["entry_type"], - external_agent_id=self.agent_id, - ) - - return new_entry_id diff --git a/mesa/experimental/memory/__init__.py b/mesa/experimental/memory/__init__.py new file mode 100644 index 00000000000..6c4e747910b --- /dev/null +++ b/mesa/experimental/memory/__init__.py @@ -0,0 +1,30 @@ +"""Core event management functionality for Mesa's memory system. + +This module provides the foundational data structures and classes needed for memory +entries recording in mesa. The Memory class is a manager between the ShortTermMemory +(dque data structure) and LongTermMemory (hash map). + +Key features: + +- Priority-based event ordering +- Weak references to prevent memory leaks from canceled events +- Efficient event insertion and removal using a heap queue +- Support for event cancellation without breaking the heap structure + +The module contains three main components: +- Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW) +- SimulationEvent: A class representing individual events with timing and execution details +- EventList: A heap-based priority queue managing the chronological ordering of events + +The implementation supports both pure discrete event simulation and hybrid approaches +combining agent-based modeling with event scheduling. +""" + +from .memory import LongTermMemory, Memory, MemoryEntry, ShortTermMemory + +__all__ = [ + "LongTermMemory", + "Memory", + "MemoryEntry", + "ShortTermMemory", +] diff --git a/mesa/experimental/memory/memory.py b/mesa/experimental/memory/memory.py new file mode 100644 index 00000000000..5ac31cb66fc --- /dev/null +++ b/mesa/experimental/memory/memory.py @@ -0,0 +1,286 @@ +"""An entry-recording functionality designed to be used as the memory of an agent. + +This module provides the foundational class needed for storing information in the memory of an agent. +Then objective is to implement a very simple and efficient system that can be used for any agent and +for any kind of information (an entry). The user defines the capacity of the memory chooses the format +of the entries, which allows for a greater flexibility. + +Features: + +- Capacity-based short-term memory, with a FIFO system. +- Efficient storage and retrieval of entries. +- Support for different entry types of entries. +- Possibility to send entries to other agents. + +For now, the module contains only one main component: +- Memory: A class representing the memory of an agent. +""" + +import copy +from collections import deque +from typing import Any + + +class MemoryEntry: + """Base class for all memory entries.""" + + def __init__( + self, + entry_content: str, + entry_step: int, + entry_type: str, + entry_metadata: dict | None = None, + ): + """Initialize a MemoryEntry with given content, step, type, and optional metadata.""" + self.entry_content = entry_content + self.entry_step = entry_step + self.entry_type = entry_type + self.entry_metadata = entry_metadata or {} + + def to_dict(self) -> dict: + """Convert memory entry to dictionary for serialization.""" + return { + "entry_content": self.entry_content, + "entry_step": self.entry_step, + "entry_type": self.entry_type, + "entry_metadata": self.entry_metadata, + } + + @classmethod + def from_dict(cls, data: dict) -> "MemoryEntry": + """Create memory entry from dictionary.""" + entry = cls( + entry_content=data["entry_content"], + entry_step=data["entry_step"], + entry_type=data["entry_type"], + entry_metadata=data["entry_metadata"], + ) + return entry + + +class ShortTermMemory: + """Short-term memory with limited capacity that follows recency principles. + + Implemented as a double-ended queue with O(1) add/remove operations. + """ + + def __init__(self, model, capacity: int = 10): + """Initialize ShortTermMemory with a model and capacity.""" + self.model = model + self.capacity = capacity + self.entries = deque(maxlen=capacity) + + def add( + self, + model, + entry_content: str | None = None, + entry_type: str = "general", + entry_metadata: dict | None = None, + entry=None, + ) -> MemoryEntry: + """Add a new entry to short-term memory.""" + if entry is not None: + self.entries.append(entry) + return entry + + entry_metadata = entry_metadata or {} + entry = MemoryEntry( + entry_step=model.step, + entry_content=entry_content, + entry_type=entry_type, + entry_metadata=entry_metadata, + ) + self.entries.append(entry) + return entry + + def get_recent(self, n: int = 10) -> list[MemoryEntry]: + """Get n most recent entries.""" + return list(self.entries)[-n:] + + def get_by_id(self, entry_id) -> MemoryEntry | None: + """Retrieve an entry by its ID.""" + entry_list = [entry for entry in self.entries if id(entry) == entry_id] + if entry_list: + return entry_list[0] + else: + return None + + def get_by_type(self, entry_type: str) -> list[MemoryEntry]: + """Get entries from a specific entry type.""" + entry_list = [entry for entry in self.entries if entry.entry_type == entry_type] + return entry_list + + def forget_last(self) -> bool: + """Remove the most recent entry from memory. Returns True if successful.""" + if self.entries: + self.entries.pop() + return True + else: + return False + + def forget_first(self) -> bool: + """Remove the oldest entry from memory. Returns True if successful.""" + if self.entries: + self.entries.popleft() + return True + else: + return False + + def forget(self, entry_id=None, entry: MemoryEntry = None) -> bool: + """Remove an entry from short-term memory.""" + if entry_id is not None: + entry_list = [entry for entry in self.entries if id(entry) == entry_id] + if entry_list: + entry = entry_list[0] + if isinstance(entry, MemoryEntry): + try: + self.entries.remove(entry) + return True + except ValueError: + return False + return False + + def clear(self): + """Remove all entries from memory.""" + self.entries.clear() + + +class LongTermMemory: + """Long-term memory with categorization and importance-based retrieval. + + Implemented using dictionaries for O(1) entry type access. + """ + + def __init__(self, model): + """Initialize LongTermMemory with a model.""" + self.model = model + self.entries: dict[int, MemoryEntry] = {} + + def add( + self, + model, + entry_content: str | None = None, + entry_type: str = "general", + entry_metadata: dict | None = None, + entry=None, + ) -> MemoryEntry: + """Add a new entry to long-term memory.""" + if entry is not None: + entry_id = id(entry) + self.entries[entry_id] = entry + return entry + + entry_metadata = entry_metadata or {} + entry = MemoryEntry( + entry_step=model.step, + entry_content=entry_content, + entry_type=entry_type, + entry_metadata=entry_metadata, + ) + entry_id = id(entry) + self.entries[entry_id] = entry + return entry + + def get_by_id(self, entry_id) -> MemoryEntry | None: + """Retrieve an entry by its ID.""" + if entry_id in self.entries: + return self.entries[entry_id] + return None + + def get_by_type(self, entry_type: str) -> list[MemoryEntry]: + """Get entries from a specific entry type.""" + entry_list = [ + entry for entry in self.entries.values() if entry.entry_type == entry_type + ] + return entry_list + + def forget(self, entry_id=None, entry: MemoryEntry = None) -> bool: + """Remove an entry from long-term memory.""" + if entry: + entry_id = id(entry) + if entry_id is None or entry_id not in self.entries: + return False + del self.entries[entry_id] + return True + + +class Memory: + """Main memory manager combining short-term and long-term memory. + + Provides consolidation, search (by type for now) and memory transfer (communicate) functionality. + """ + + def __init__(self, agent, model, stm_capacity: int = 10): + """Initialize Memory with an agent, model, and short-term memory capacity.""" + self.model = model + self.agent = agent + self.short_term = ShortTermMemory(model=self.model, capacity=stm_capacity) + self.long_term = LongTermMemory(model=self.model) + + def remember_short_term( + self, + model, + entry_content: Any, + entry_type: str = "general", + entry_metadata: dict | None = None, + ) -> MemoryEntry: + """Add an entry to short-term memory. Returns the MemoryEntry object created.""" + return self.short_term.add(model, entry_content, entry_type, entry_metadata) + + def remember_long_term( + self, + model, + entry_content: Any, + entry_type: str = "general", + entry_metadata: dict | None = None, + ) -> MemoryEntry: + """Add an entry directly to long-term memory.""" + return self.long_term.add(model, entry_content, entry_type, entry_metadata) + + def consolidate(self, entry: MemoryEntry) -> MemoryEntry: + """Transfer an entry from short-term to long-term memory. + + Returns the MemoryEntry object transferred. + """ + entry = self.long_term.add(entry=entry, model=self.model) + self.short_term.forget(entry_id=id(entry)) + return entry + + def get_by_type( + self, + entry_type: str, + include_short_term: bool = True, + include_long_term: bool = True, + limit: int = 10, + ) -> list[MemoryEntry]: + """Get a list of entries of the same entry type.""" + results: list[MemoryEntry] = [] + if include_short_term: + short_results = self.short_term.get_by_type(entry_type) + if short_results is not None: + if isinstance(short_results, list): + results.extend(short_results) + else: + results.append(short_results) + if include_long_term: + long_results = self.long_term.get_by_type(entry_type) + if long_results is not None: + if isinstance(long_results, list): + results.extend(long_results) + else: + results.append(long_results) + if not results: + return [] + return results[:limit] if limit is not None else results + + def communicate(self, entry, external_agent): + """Send a memory entry to another agent by making a deep copy of the entry.""" + entry_copy = copy.deepcopy(entry) + entry_copy.entry_metadata["external_id"] = self.agent.unique_id + new_entry = external_agent.memory.remember_short_term( + model=self.model, + entry_content=entry_copy.entry_content, + entry_type=entry_copy.entry_type, + entry_metadata=entry_copy.entry_metadata, + ) + return new_entry From 24e8428ea1116424940573fab290a515f53504d8 Mon Sep 17 00:00:00 2001 From: colinfrisch Date: Wed, 2 Apr 2025 07:47:36 +0800 Subject: [PATCH 3/4] Update __init__.py --- mesa/experimental/memory/__init__.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mesa/experimental/memory/__init__.py b/mesa/experimental/memory/__init__.py index 6c4e747910b..f3508e2b839 100644 --- a/mesa/experimental/memory/__init__.py +++ b/mesa/experimental/memory/__init__.py @@ -4,20 +4,11 @@ entries recording in mesa. The Memory class is a manager between the ShortTermMemory (dque data structure) and LongTermMemory (hash map). -Key features: - -- Priority-based event ordering -- Weak references to prevent memory leaks from canceled events -- Efficient event insertion and removal using a heap queue -- Support for event cancellation without breaking the heap structure - The module contains three main components: - Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW) - SimulationEvent: A class representing individual events with timing and execution details - EventList: A heap-based priority queue managing the chronological ordering of events -The implementation supports both pure discrete event simulation and hybrid approaches -combining agent-based modeling with event scheduling. """ from .memory import LongTermMemory, Memory, MemoryEntry, ShortTermMemory From 8982579b1b73f3a30e8681bbdc8bb06c1d85a46c Mon Sep 17 00:00:00 2001 From: colinfrisch Date: Wed, 2 Apr 2025 07:49:55 +0800 Subject: [PATCH 4/4] Update __init__.py --- mesa/experimental/memory/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/memory/__init__.py b/mesa/experimental/memory/__init__.py index f3508e2b839..2fd3dc16a21 100644 --- a/mesa/experimental/memory/__init__.py +++ b/mesa/experimental/memory/__init__.py @@ -4,11 +4,11 @@ entries recording in mesa. The Memory class is a manager between the ShortTermMemory (dque data structure) and LongTermMemory (hash map). -The module contains three main components: -- Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW) -- SimulationEvent: A class representing individual events with timing and execution details -- EventList: A heap-based priority queue managing the chronological ordering of events +The module now contains four main component: +- Memory: The operating class for managing ShortTermMemory and LongTermMemory +- ShortTermMemory more memory-efficient and reactive (efficient store and pop functionality) +- LongTermMemory : more computational-efficient (efficient navigation) """ from .memory import LongTermMemory, Memory, MemoryEntry, ShortTermMemory