diff --git a/doc/reference/stores.rst b/doc/reference/stores.rst index d24e83dc2..9052bfacc 100644 --- a/doc/reference/stores.rst +++ b/doc/reference/stores.rst @@ -290,7 +290,7 @@ molecule). All unique molecules will have the following named fields: 1. ``unique_index`` (:py:class:`int`): Unique identifier for each unique molecule When processes add new unique molecules, the helper function - :py:func:`ecoli.library.schema.create_unqiue_indices` is used to generate + :py:func:`ecoli.library.schema.create_unqiue_indexes` is used to generate unique indices for each molecule to be added. 2. ``_entryState`` (:py:attr:`numpy.int8`): 1 for active row, 0 for inactive row When unique molecules are deleted (e.g. RNA degradation), all of their data, diff --git a/ecoli/composites/ecoli_master.py b/ecoli/composites/ecoli_master.py index d19a0e677..c3e423bae 100644 --- a/ecoli/composites/ecoli_master.py +++ b/ecoli/composites/ecoli_master.py @@ -122,7 +122,8 @@ def initial_state(self, config: dict[str, Any]=None) -> dict[str, Any]: 1. ``config['initial_state']`` - 2. Load the JSON file at ``f'data/{config['initial_state_file]}.json'`` + 2. Load the JSON file at ``f'data/{config['initial_state_file]}.json'`` + using :py:func:`~ecoli.states.wcecoli_state.get_state_from_file`. 3. Generate initial state from simulation data object (see :py:meth:`~ecoli.library.sim_data.LoadSimData.generate_initial_state`) @@ -216,7 +217,7 @@ def generate_processes_and_steps(self, config: dict[str, Any] This method is called when :py:class:`~ecoli.composites.ecoli_master.Ecoli` is initialized and its return value is cached as the instance variable :py:data:`~ecoli.composites.ecoli_master.Ecoli.processes_and_steps`. This - allows the :py:class:`~ecoli.composites.ecoil_master.Ecoli.initial_state` + allows the :py:class:`~ecoli.composites.ecoli_master.Ecoli.initial_state` method to be run before calling :py:meth:`~vivarium.core.composer.Composer.generate` on this composer. @@ -231,8 +232,7 @@ def generate_processes_and_steps(self, config: dict[str, Any] be loaded from the pickled simulation data object using :py:meth:`~ecoli.library.sim_data.LoadSimData.get_config_by_name`, or the string ``"default"`` to indicate that the - :py:data:`~vivarium.core.process.Process.defaults` attribute of - the process should be used as its config. + ``defaults`` attribute of the process should be used as its config. * ``processes``: Mapping of all process names (:py:class:`str`) @@ -497,7 +497,7 @@ def generate_topology(self, config: dict[str, Any]) -> dict[str, tuple[str]]: Args: config: Uses the same ``config`` supplied to this composer in - :py:meth:`~ecoli.composites.ecoli_master.Ecoli.__init__` that + :py:class:`~ecoli.composites.ecoli_master.Ecoli` that was used to generate the processes and steps in :py:meth:`~ecoli.composites.ecoli_master.Ecoli.generate_processes_and_steps`. Important key-value pairs include: diff --git a/ecoli/experiments/ecoli_master_sim.py b/ecoli/experiments/ecoli_master_sim.py index 2e7b075f2..510020d1f 100644 --- a/ecoli/experiments/ecoli_master_sim.py +++ b/ecoli/experiments/ecoli_master_sim.py @@ -19,6 +19,7 @@ from typing import Optional, Dict, Any from vivarium.core.engine import Engine +from vivarium.core.process import Process from vivarium.core.serialize import deserialize_value, serialize_value from vivarium.library.dict_utils import deep_merge from vivarium.library.topology import assoc_path @@ -334,6 +335,22 @@ def __init__(self, config: dict[str, Any]): # in case multiple simulations are run with suffix_time = True. self.experiment_id_base = config['experiment_id'] self.config = config + self.ecoli = None + """vivarium.core.composer.Composite: Contains the fully instantiated + processes, steps, topologies, and flow necessary to run simulation. + Generated by + :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.build_ecoli` and + cleared when :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.run` + is called to potentially free up memory after division.""" + self.generated_initial_state = None + """dict: Fully populated initial state for simulation. Generated by + :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.build_ecoli` and + cleared when :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.run` + is called to potentially free up memory after division.""" + self.ecoli_experiment = None + """vivarium.core.engine.Engine: Engine that runs the simulation. + Instantiated by + :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.run`.""" # Unpack config using Descriptor protocol: # All of the entries in config are translated to properties @@ -370,15 +387,15 @@ def __set__(self, sim, value): @staticmethod def from_file(filepath=CONFIG_DIR_PATH + 'default.json') -> 'EcoliSim': - f"""Used to instantiate + """Used to instantiate :py:class:`~ecoli.experiments.ecoli_master_sim.EcoliSim` with a config loaded from the JSON at ``filepath`` by :py:class:`~ecoli.experiments.ecoli_master_sim.SimConfig`. Args: filepath: String filepath of JSON file with config options to - apply on top of the options laid out in - {EcoliSim.default_file_path} + apply on top of the options laid out in the default JSON + located at the default value for ``filepath``. """ config = SimConfig() config.update_from_json(filepath) @@ -398,11 +415,25 @@ def from_cli() -> 'EcoliSim': def _retrieve_processes(self, - processes, - add_processes, - exclude_processes, - swap_processes, - ): + processes: list[str], + add_processes: list[str], + exclude_processes: list[str], + swap_processes: dict[str, str], + ) -> dict[str, Process]: + """ + Retrieve process classes from :py:data:`~ecoli.processes.process_registry`. + + Args: + processes: Base list of process names to retrieve classes for + add_processes: Additional process names to retrieve classes for + exclude_processes: Process names to not retrieve classes for + swap_processes: Mapping of process names to the names of the + processes they should be swapped for. It is assumed that + the swapped processes share the same topologies. + + Returns: + Mapping of process names to process classes. + """ result = {} for process_name in list(processes.keys()) + list(add_processes): if process_name in exclude_processes: @@ -420,12 +451,31 @@ def _retrieve_processes(self, def _retrieve_topology(self, - topology, - processes, - swap_processes, - log_updates, - divide, - ): + topology: dict[str, dict[str, tuple[str]]], + processes: list[str], + swap_processes: dict[str, str], + log_updates: bool, + ) -> dict[str, dict[str, tuple[str]]]: + """ + Retrieves topologies for processes from + :py:data:`~ecoli.processes.registries.topology_registry`. + + Args: + topology: Mapping of process names to user-specified topologies. + Will be merged with topology from topology_registry, if exists. + processes: List of process names for which to retrive topologies. + swap_processes: Mapping of process names to the names of processes + to swap them for. It is assumed that swapped processes share + the same topologies. User-specified topologies in ``topology`` + under either process name are merged into the topology + retrieved from the topology registry for the topology to be + swapped out. + log_updates: Whether to emit process updates. Adds topology for + ``log_update`` port. + + Returns: + Mapping of process names to process topologies. + """ result = {} original_processes = {v: k for k, v in swap_processes.items()} for process in processes: @@ -456,7 +506,21 @@ def _retrieve_topology(self, return result - def _retrieve_process_configs(self, process_configs, processes): + def _retrieve_process_configs(self, + process_configs: dict[str, dict[str, Any]], + processes: list[str]) -> dict[str, Any]: + """ + Sets up process configs to be interpreted by + :py:meth:`~ecoli.composites.ecoli_master.Ecoli.generate_processes_and_steps`. + + Args: + process_configs: Mapping of process names to user-specified process + configuration dictionaries. + processes: List of process names to set up process config for. + + Returns: + Mapping of process names to process configs. + """ result = {} for process in processes: result[process] = process_configs.get(process) @@ -473,7 +537,8 @@ def build_ecoli(self): For all processes in ``config['processes']``: 1. Retrieves process class from - :py:data:`~ecoli.processes.process_topology` + :py:data:`~vivarium.core.registry.process_registry`, which is + populated in ``ecoli/processes/__init__.py``. 2. Retrieves process topology from :py:data:`~ecoli.processes.registries.topology_registry` and merge @@ -488,6 +553,13 @@ def build_ecoli(self): ``True``. Spatial environment config options are loaded from ``config['spatial_environment_config]``. See ``ecoli/composites/ecoli_configs/spatial.json`` for an example. + + .. note:: + When loading from a saved state with a file name of the format + ``vivecoli_t{save time}``, the simulation seed is automatically + set to ``config['seed'] + {save_time}`` to prevent + :py:func:`~ecoli.library.schema.create_unique_indexes` from + generating clashing indices. """ # build processes, topology, configs self.processes = self._retrieve_processes( @@ -495,7 +567,7 @@ def build_ecoli(self): self.swap_processes) self.topology = self._retrieve_topology( self.topology, self.processes, self.swap_processes, - self.log_updates, self.divide) + self.log_updates) self.process_configs = self._retrieve_process_configs( self.process_configs, self.processes) @@ -544,7 +616,11 @@ def build_ecoli(self): def save_states(self): """ Runs the simulation while saving the states of specific - timesteps to jsons. + timesteps to jsons. Automatically invoked by + :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.run` + if ``config['save'] == True``. State is saved as a JSON that + can be reloaded into a simulation as described in + :py:meth:`~ecoli.composites.ecoli_master.Ecoli.initial_state`. """ for time in self.save_times: if time > self.total_time: @@ -590,7 +666,12 @@ def save_states(self): def run(self): - """Create and run an EcoliSim experiment. Must run build_ecoli first!""" + """Create and run an EcoliSim experiment. + + .. WARNING:: + Run :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.build_ecoli` + before calling :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.run`! + """ metadata = self.get_metadata() # make the experiment emitter_config = {'type': self.emitter} @@ -652,7 +733,26 @@ def run(self): report_profiling(self.ecoli_experiment.stats) - def query(self, query=None): + def query(self, query: list[tuple[str]]=None): + """ + Query emitted data. + + Args: + query: List of tuple-style paths in the simulation state to + retrieve emitted values for. Returns all emitted data + if ``None``. + + Returns: + Dictionary of emitted data in one of two forms. + + * Raw data (if ``self.raw_output``): Data is keyed by time + (e.g. ``{0: {'data': ...}, 1: {'data': ...}, ...}``) + + * Timeseries: Data is reorganized to match the structure of the + simulation state. Leaf values in the returned dictionary are + lists of the simulation state value over time (e.g. + ``{'data': [..., ..., ...]}``). + """ # Retrieve queried data (all if not specified) if self.raw_output: return self.ecoli_experiment.emitter.get_data(query) @@ -660,15 +760,23 @@ def query(self, query=None): return self.ecoli_experiment.emitter.get_timeseries(query) - def merge(self, other): + def merge(self, other: 'EcoliSim'): """ Combine settings from this EcoliSim with another, overriding current settings with those from the other EcoliSim. + + Args: + other: Simulation with settings to override current simulation. """ deep_merge(self.config, other.config) - def get_metadata(self): + def get_metadata(self) -> dict[str, Any]: + """ + Compiles all simulation settings, git hash, and process list into a single + dictionary. Called by + :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.to_json_string`. + """ # create metadata of this experiment to be emitted, # namely the config of this EcoliSim object # with an additional key for the current git hash. @@ -684,11 +792,25 @@ def get_metadata(self): return metadata - def to_json_string(self): + def to_json_string(self) -> str: + """ + Serializes simulation setting dictionary along with git hash and + final list of process names. Called by + :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.export_json`. + + """ return str(serialize_value(self.get_metadata())) - def export_json(self, filename=CONFIG_DIR_PATH + "export.json"): + def export_json(self, filename: str=CONFIG_DIR_PATH + "export.json"): + """ + Saves current simulation settings along with git hash and final list + of process names as a JSON that can be reloaded using + :py:meth:`~ecoli.experiments.ecoli_master_sim.EcoliSim.from_file`. + + Args: + filename: Filepath and name for saved JSON (include ``.json``). + """ with open(filename, 'w') as f: f.write(self.to_json_string()) diff --git a/ecoli/library/schema.py b/ecoli/library/schema.py index f91e8998d..0b969d88a 100644 --- a/ecoli/library/schema.py +++ b/ecoli/library/schema.py @@ -331,7 +331,7 @@ class UniqueNumpyUpdater: signal to apply these updates is given by a special process ( :py:class:`ecoli.processes.unique_update.UniqueUpdate`) that is automatically added to the simulation by - :py:meth:`ecoli.composites.ecoli_master.Ecoli._generate_processes_and_steps` + :py:meth:`ecoli.composites.ecoli_master.Ecoli.generate_processes_and_steps` """ def __init__(self): """Sets up instance attributes to accumulate updates.