Source code for modelly.data.recorders.object_datasets

"""Datasets read from Python objects: agents, the model, and hand-written table rows."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from modelly.core.agent import Agent
from modelly.core.agentset import AbstractAgentSet
from modelly.data.recorders.protocol import BaseDataSet

if TYPE_CHECKING:
    from modelly.core.model import Model
    from modelly.data.recorders.base import BaseDataRecorder
    from modelly.data.recorders.config import DatasetConfig
    from modelly.data.recorders.protocol import DataSet


[docs] class AgentDataSet[A: Agent](BaseDataSet): """Data set for agents data. Args: name: the name of the data set agents: the agents to collect data from fields: fields to collect use_dirty_flag: if True, enables manual dirty-flag caching optimization. Default is False (always recompute snapshot). """ def __init__( self, name: str, agents: AbstractAgentSet[A], *, use_dirty_flag: bool = False, fields: str | list[str] | None = None, ): """Init. of AgentDataSet.""" if fields is None: raise ValueError("please pass one or more fields to collect") elif isinstance(fields, str): fields = [fields] super().__init__(name, fields=["unique_id", *fields]) self.agents = agents # Dirty-flag behavior (opt-in only) self._use_dirty_flag: bool = use_dirty_flag self._is_dirty: bool = True self._cache: list[dict[str, Any]] | None = None @property def data(self) -> list[dict[str, Any]]: """Return the data of the dataset.""" self._check_closed() if (not self._use_dirty_flag) or self._is_dirty or self._cache is None: snapshot = [ dict(zip(self._attributes, self._collector(agent))) for agent in self.agents ] if not self._use_dirty_flag: return snapshot self._cache = snapshot self._is_dirty = False return self._cache
[docs] def set_dirty_flag(self) -> None: """Mark the dataset as dirty. Only meaningful if `use_dirty_flag=True`. The next access to `.data` will recompute the snapshot. """ self._check_closed() if self._use_dirty_flag: self._is_dirty = True
[docs] def close(self): """Close the data set.""" super().close() self.agents = None self._cache = None
[docs] class ModelDataSet[M: Model](BaseDataSet): """Data set for model data. Args: name: the name of the data set model: the model to collect data from fields: the fields to collect. """ def __init__(self, name, model: M, fields: str | list[str] | None = None): """Init of ModelDataSet.""" super().__init__(name, fields=fields) self.model = model @property def data(self) -> dict[str, Any]: """Return the data of the dataset.""" self._check_closed() values = self._collector(self.model) if len(self._attributes) == 1: return {self._attributes[0]: values} else: return dict(zip(self._attributes, values))
[docs] def close(self): """Close the data set.""" super().close() self.model = None
[docs] class TableDataSet: """A Table DataSet. Args: name: the name of the data set fields: string or list of strings specifying the columns fixme: this needs a closer look it now follows the datacollector, so you just add a row. """ def __init__(self, name, fields: str | list[str] | None = None): """Init.""" self.name = name if fields is None: raise ValueError("please pass one or more fields to collect") self.fields = fields if isinstance(fields, list) else [fields] self.rows = []
[docs] def add_row(self, row: dict[str, Any]): """Add a row to the table. Args: row: the row to add Raises: RuntimeError: if the dataset has been closed ValueError: if the row is empty ValueError: if the row is missing required fields ValueError: if the row contains unexpected fields """ if self.rows is None: raise RuntimeError(f"DataSet '{self.name}' has been closed") row_keys = set(row) fields_set = set(self.fields) # Value error if the row provided by the user is empty if not row: raise ValueError("row is empty") # If the row user passed miss the value for required field missing = fields_set - row_keys if missing: raise ValueError("row is missing fields") # If the row user passed includes the unexpected fields unexpected = row_keys - fields_set if unexpected: raise ValueError(f"Row contains unexpected fields: {unexpected}") # Adding the row to rows row_to_add = {k: row[k] for k in self.fields} self.rows.append(row_to_add)
@property def data(self) -> list[dict[str, Any]]: """Return the data of the dataset.""" if self.rows is None: raise RuntimeError(f"DataSet '{self.name}' has been closed") return self.rows
[docs] def close(self): """Close the data set.""" self.rows = None
[docs] def record( self, recorder: BaseDataRecorder, configuration: DatasetConfig | None = None ) -> DataSet: """Record the collected data.""" recorder.add_dataset(self, configuration=configuration) return self