Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cacheable mesa #2194

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,6 @@ dmypy.json
# JS dependencies
mesa/visualization/templates/external/
mesa/visualization/templates/js/external/

# output results default dir
mesa/output_dir/
265 changes: 265 additions & 0 deletions mesa/cacheable_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import contextlib
import glob
import itertools
import os
from collections.abc import Callable
from pathlib import Path
from typing import Any

Check warning on line 7 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L1-L7

Added lines #L1 - L7 were not covered by tests

import pyarrow as pa
import pyarrow.parquet as pq

Check warning on line 10 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L9-L10

Added lines #L9 - L10 were not covered by tests

from mesa import Model

Check warning on line 12 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L12

Added line #L12 was not covered by tests

with contextlib.suppress(ImportError):
import pandas as pd

Check warning on line 15 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L15

Added line #L15 was not covered by tests


class CacheableModel:

Check warning on line 18 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L18

Added line #L18 was not covered by tests
"""Class that takes a model and writes its steps to a cache file or reads them from a cache file."""

def __init__(

Check warning on line 21 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L21

Added line #L21 was not covered by tests
self,
model: Model,
cache_file_path: str | Path,
total_steps: int,
condition_function=None,
) -> None:
"""Create a new caching wrapper around an existing mesa model instance.

Attributes:
model: mesa model
cache_file_path: cache file to write to or read from
cache_state: whether to replay by reading from the cache or simulate and write to the cache
cache_step_rate: only every n-th step is cached. If it is 1, every step is cached. If it is 2,
only every second step is cached and so on. Increasing 'cache_step_rate' will reduce cache size and
increase replay performance by skipping the steps inbetween every n-th step.
"""

self.model = model
self.cache_file_path = Path(cache_file_path)
self._total_steps = total_steps
self.step_count: int = 0
self.run_finished = False

Check warning on line 43 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L39-L43

Added lines #L39 - L43 were not covered by tests

# temporary dicts to be flushed
self.model_vars_cache = {}
self._agent_records = {}

Check warning on line 47 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L46-L47

Added lines #L46 - L47 were not covered by tests

self._cache_interval = 100

Check warning on line 49 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L49

Added line #L49 was not covered by tests

self._last_cached_step = 0 # inclusive since it is the bottom bound of slicing
self.condition_function = condition_function

Check warning on line 52 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L51-L52

Added lines #L51 - L52 were not covered by tests

def get_agent_vars_dataframe(self):

Check warning on line 54 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L54

Added line #L54 was not covered by tests
"""Create a pandas DataFrame from the agent variables.

The DataFrame has one column for each variable, with two additional
columns for tick and agent_id.
"""
# Check if self.agent_reporters dictionary is empty, if so raise warning
if not self.model.datacollector.agent_reporters:
raise UserWarning(

Check warning on line 62 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L62

Added line #L62 was not covered by tests
"No agent reporters have been defined in the DataCollector, returning empty DataFrame."
)

all_records = itertools.chain.from_iterable(

Check warning on line 66 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L66

Added line #L66 was not covered by tests
self.model.datacollector._agent_records.values()
)
rep_names = list(self.model.datacollector.agent_reporters)
print(f"{all_records=}")
print(f"{rep_names=}")

Check warning on line 71 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L69-L71

Added lines #L69 - L71 were not covered by tests

df = pd.DataFrame.from_records(

Check warning on line 73 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L73

Added line #L73 was not covered by tests
data=all_records,
columns=["Step", "AgentID", *rep_names],
index=["Step", "AgentID"],
)

sliced_df = df.loc[self._last_cached_step : self.model._steps]

Check warning on line 79 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L79

Added line #L79 was not covered by tests

return sliced_df

Check warning on line 81 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L81

Added line #L81 was not covered by tests

def get_model_vars_dataframe(self):

Check warning on line 83 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L83

Added line #L83 was not covered by tests
"""Create a pandas DataFrame from the model variables.

The DataFrame has one column for each model variable, and the index is
(implicitly) the model tick.
"""
# Check if self.model_reporters dictionary is empty, if so raise warning
if not self.model.datacollector.model_reporters:
raise UserWarning(

Check warning on line 91 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L91

Added line #L91 was not covered by tests
"No model reporters have been defined in the DataCollector, returning empty DataFrame."
)

return pd.DataFrame(self.model.datacollector.model_vars)[

Check warning on line 95 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L95

Added line #L95 was not covered by tests
self._last_cached_step : self.model._steps
]

def _save_to_parquet(self, model):

Check warning on line 99 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L99

Added line #L99 was not covered by tests
"""Save the current cache of data to a Parquet file and clear the cache."""
model_df = self.get_model_vars_dataframe()
agent_df = self.get_agent_vars_dataframe()
padding = len(str(self._total_steps)) - 1

Check warning on line 103 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L101-L103

Added lines #L101 - L103 were not covered by tests

# ceiling function
model_file = f"{self.cache_file_path}/model_data_{-(self.model._steps // -self._cache_interval):0{padding}}.parquet"
agent_file = f"{self.cache_file_path}/agent_data_{-(self.model._steps // -self._cache_interval):0{padding}}.parquet"

Check warning on line 107 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L106-L107

Added lines #L106 - L107 were not covered by tests

self.cache_file_path.mkdir(parents=True, exist_ok=True)

Check warning on line 109 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L109

Added line #L109 was not covered by tests

absolute_path = os.path.abspath(model_file)

Check warning on line 111 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L111

Added line #L111 was not covered by tests
if os.path.exists(absolute_path):
raise FileExistsError(

Check warning on line 113 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L113

Added line #L113 was not covered by tests
f"A directory with the name {model_file} already exists."
)
if os.path.exists(model_file):
raise FileExistsError(

Check warning on line 117 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L117

Added line #L117 was not covered by tests
f"A directory with the name {model_file} already exists."
)
if os.path.exists(agent_file):
raise FileExistsError(

Check warning on line 121 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L121

Added line #L121 was not covered by tests
f"A directory with the name {agent_file} already exists."
)

if not model_df.empty:
model_table = pa.Table.from_pandas(model_df)
pq.write_table(model_table, model_file)

Check warning on line 127 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L126-L127

Added lines #L126 - L127 were not covered by tests

if not agent_df.empty:
agent_table = pa.Table.from_pandas(agent_df)
pq.write_table(agent_table, agent_file)

Check warning on line 131 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L130-L131

Added lines #L130 - L131 were not covered by tests

def cache(self):

Check warning on line 133 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L133

Added line #L133 was not covered by tests
"""Custom collect method to extend the original collect behavior."""
# Implement your custom logic here
# For example, let's say we want to write collected data to a cache file every `cache_step_rate` steps
if (
self.model._steps % self._cache_interval == 0
or self.model._steps == self._total_steps
):
self._save_to_parquet(self.model)
self._last_cached_step = self.model._steps

Check warning on line 142 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L141-L142

Added lines #L141 - L142 were not covered by tests

if self.condition_function and self.save_special_results(
self.condition_function
):
pass

Check warning on line 147 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L147

Added line #L147 was not covered by tests

def save_special_results(self, condition_function: Callable[[dict], bool]):
model_vars = self.model.datacollector.model_vars
self.cache_file_path.mkdir(parents=True, exist_ok=True)

Check warning on line 151 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L149-L151

Added lines #L149 - L151 were not covered by tests

current_step = self.model._steps
special_results_file = f"{self.cache_file_path}/special_results.parquet"

Check warning on line 154 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L153-L154

Added lines #L153 - L154 were not covered by tests
if condition_function(model_vars):
step_data = {key: [value[-1]] for key, value in model_vars.items()}
step_data["Step"] = current_step
special_results_df = pd.DataFrame(step_data)

Check warning on line 158 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L157-L158

Added lines #L157 - L158 were not covered by tests

# Append the current step data to the Parquet file
if os.path.exists(special_results_file):
existing_data = pq.read_table(special_results_file).to_pandas()
combined_data = pd.concat(

Check warning on line 163 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L162-L163

Added lines #L162 - L163 were not covered by tests
[existing_data, special_results_df], ignore_index=True
)
special_results_table = pa.Table.from_pandas(combined_data)

Check warning on line 166 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L166

Added line #L166 was not covered by tests
else:
special_results_table = pa.Table.from_pandas(special_results_df)

Check warning on line 168 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L168

Added line #L168 was not covered by tests

pq.write_table(special_results_table, special_results_file)

Check warning on line 170 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L170

Added line #L170 was not covered by tests

print(

Check warning on line 172 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L172

Added line #L172 was not covered by tests
f"Condition met. Appended special results for step {current_step} to {special_results_file}"
)
else:
print(f"Condition not met at step {current_step}. No data to save.")

Check warning on line 176 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L176

Added line #L176 was not covered by tests

def read_model_data(self):

Check warning on line 178 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L178

Added line #L178 was not covered by tests
"""Read and combine all model data Parquet files into a single DataFrame."""
model_files = glob.glob(f"{self.cache_file_path}/model_data_*.parquet")
model_dfs = []

Check warning on line 181 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L180-L181

Added lines #L180 - L181 were not covered by tests

for model_file in model_files:
table = pq.read_table(model_file)
df = table.to_pandas()
model_dfs.append(df)

Check warning on line 186 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L184-L186

Added lines #L184 - L186 were not covered by tests

if model_dfs:
model_df = pd.concat(model_dfs, ignore_index=True)
return model_df

Check warning on line 190 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L189-L190

Added lines #L189 - L190 were not covered by tests
else:
raise FileNotFoundError("No model data files found.")

Check warning on line 192 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L192

Added line #L192 was not covered by tests

def read_agent_data(self):

Check warning on line 194 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L194

Added line #L194 was not covered by tests
"""Read and combine all agent data Parquet files into a single DataFrame."""
agent_files = glob.glob(f"{self.cache_file_path}/agent_data_*.parquet")
agent_dfs = []

Check warning on line 197 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L196-L197

Added lines #L196 - L197 were not covered by tests

for agent_file in agent_files:
table = pq.read_table(agent_file)
df = table.to_pandas()
agent_dfs.append(df)

Check warning on line 202 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L200-L202

Added lines #L200 - L202 were not covered by tests

if agent_dfs:
agent_df = pd.concat(agent_dfs)
return agent_df

Check warning on line 206 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L205-L206

Added lines #L205 - L206 were not covered by tests
else:
raise FileNotFoundError("No agent data files found.")

Check warning on line 208 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L208

Added line #L208 was not covered by tests

def combine_dataframes(self):

Check warning on line 210 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L210

Added line #L210 was not covered by tests
"""Combine and return the model and agent DataFrames."""
try:
model_df = self.read_model_data()
agent_df = self.read_agent_data()

Check warning on line 214 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L212-L214

Added lines #L212 - L214 were not covered by tests

# Sort agent DataFrame by the multi-index (Step, AgentID) to ensure order
agent_df = agent_df.sort_index()

Check warning on line 217 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L217

Added line #L217 was not covered by tests

return model_df, agent_df
except FileNotFoundError as e:
print(e)
return None, None

Check warning on line 222 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L219-L222

Added lines #L219 - L222 were not covered by tests


# FOR GRID KIV IGNORE NOW
from mesa.agent import Agent

Check warning on line 226 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L226

Added line #L226 was not covered by tests


class AgentSerializer:
@staticmethod

Check warning on line 230 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L229-L230

Added lines #L229 - L230 were not covered by tests
def agent_to_dict(agent: Agent) -> dict[str, Any]:
"""Convert an Agent instance to a dictionary."""
return {

Check warning on line 233 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L233

Added line #L233 was not covered by tests
"unique_id": agent.unique_id,
"model": str(agent.model), # Convert model to a string or identifier
"pos": str(agent.pos)
if agent.pos
else None, # Convert position to a string or identifier
}

@staticmethod

Check warning on line 241 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L241

Added line #L241 was not covered by tests
def dict_to_agent(agent_dict: dict[str, Any], model: Any) -> Agent:
"""Convert a dictionary to an Agent instance."""
unique_id = agent_dict["unique_id"]
pos = agent_dict["pos"] if agent_dict["pos"] != "None" else None
agent = Agent(unique_id=unique_id, model=model)
agent.pos = pos
return agent

Check warning on line 248 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L244-L248

Added lines #L244 - L248 were not covered by tests

@staticmethod

Check warning on line 250 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L250

Added line #L250 was not covered by tests
def save_agents_to_parquet(agents: list[Agent], filename: str) -> None:
"""Save a list of agents to a Parquet file."""
agent_dicts = [AgentSerializer.agent_to_dict(agent) for agent in agents]
df = pd.DataFrame(agent_dicts)
df.to_parquet(filename)

Check warning on line 255 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L254-L255

Added lines #L254 - L255 were not covered by tests

@staticmethod

Check warning on line 257 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L257

Added line #L257 was not covered by tests
def load_agents_from_parquet(filename: str, model: Any) -> list[Agent]:
"""Load agents from a Parquet file."""
df = pd.read_parquet(filename)

Check warning on line 260 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L260

Added line #L260 was not covered by tests
agents = [
AgentSerializer.dict_to_agent(row.to_dict(), model)
for _, row in df.iterrows()
]
return agents

Check warning on line 265 in mesa/cacheable_model.py

View check run for this annotation

Codecov / codecov/patch

mesa/cacheable_model.py#L265

Added line #L265 was not covered by tests
Loading
Loading