From 4045980034315ea1cc0b696a5c1446251cd9976e Mon Sep 17 00:00:00 2001 From: alex patrie Date: Tue, 3 Oct 2023 16:50:12 -0400 Subject: [PATCH 01/34] added higher level conversion function to exec method --- biosimulators_utils/combine/exec.py | 5 +++++ biosimulators_utils/config.py | 4 +++- requirements.optional.txt | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 7087f140..50991afa 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -7,6 +7,7 @@ """ +from biosimulators_simularium import generate_new_simularium_file from ..archive.io import ArchiveWriter from ..archive.utils import build_archive_from_paths from ..config import get_config, Config # noqa: F401 @@ -221,6 +222,10 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, log_level=log_level, indent=1, config=config) + if config.SPATIAL: + generate_new_simularium_file( + archive_rootpath=archive_tmp_dir + ) if config.COLLECT_COMBINE_ARCHIVE_RESULTS: results[content.location] = doc_results if config.LOG: diff --git a/biosimulators_utils/config.py b/biosimulators_utils/config.py index 3e530a9a..db5ab4c8 100644 --- a/biosimulators_utils/config.py +++ b/biosimulators_utils/config.py @@ -96,7 +96,8 @@ def __init__(self, BIOSIMULATIONS_API_AUTH_ENDPOINT=DEFAULT_BIOSIMULATIONS_API_AUTH_ENDPOINT, BIOSIMULATIONS_API_AUDIENCE=DEFAULT_BIOSIMULATIONS_API_AUDIENCE, VERBOSE=False, - DEBUG=False): + DEBUG=False, + SPATIAL=False): """ Args: OMEX_METADATA_INPUT_FORMAT (:obj:`OmexMetadataInputFormat`, optional): format to validate OMEX Metadata files against @@ -162,6 +163,7 @@ def __init__(self, self.BIOSIMULATIONS_API_AUDIENCE = BIOSIMULATIONS_API_AUDIENCE self.VERBOSE = VERBOSE self.DEBUG = DEBUG + self.SPATIAL = SPATIAL def get_config(): diff --git a/requirements.optional.txt b/requirements.optional.txt index d28f5af7..2f988db5 100644 --- a/requirements.optional.txt +++ b/requirements.optional.txt @@ -25,8 +25,8 @@ python_libsbml smoldyn >= 2.66 # simulariumio -[simularium] -simulariumio +# [simularium] +# biosimulators-simularium ################################# ## Visualization formats From 991892e8c4686b2b94c6f59d4fa14075c0d0f6b7 Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 15:31:58 -0400 Subject: [PATCH 02/34] feat: added spatial library for simularium smoldyn conversion --- biosimulators_utils/spatial/__init__.py | 0 biosimulators_utils/spatial/data_model.py | 783 ++++++++++++++++++++++ biosimulators_utils/spatial/io.py | 60 ++ biosimulators_utils/spatial/utils.py | 29 + 4 files changed, 872 insertions(+) create mode 100644 biosimulators_utils/spatial/__init__.py create mode 100644 biosimulators_utils/spatial/data_model.py create mode 100644 biosimulators_utils/spatial/io.py create mode 100644 biosimulators_utils/spatial/utils.py diff --git a/biosimulators_utils/spatial/__init__.py b/biosimulators_utils/spatial/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py new file mode 100644 index 00000000..5b593d92 --- /dev/null +++ b/biosimulators_utils/spatial/data_model.py @@ -0,0 +1,783 @@ +""" +:Author: Alexander Patrie +:Date: 2023-09-16 +:Copyright: 2023, UConn Health +:License: MIT + + +Using the Biosimulators side of Smoldyn to generate a modelout.txt Smoldyn file for a specified OMEX/COMBINE archive +which then is used to generate a .simularium file for the given simulation. That .simularium file is then stored along +with the log.yml and report.{FORMAT} relative to the simulation. Remember: each simulation, while not inherently +published, has the potential for publication based purely on the simulation's ability to provide a valid OMEX/COMBINE +archive. There exists (or should exist) an additional layer of abstraction to then validate and +verify the contents therein. +""" + + +# pragma: no cover +import os +from dataclasses import dataclass +from warnings import warn +from typing import Optional, Tuple, Dict, List, Union +from abc import ABC, abstractmethod +from smoldyn import Simulation as smoldynSim +import numpy as np +import pandas as pd +from simulariumio import ( + CameraData, + UnitData, + MetaData, + DisplayData, + DISPLAY_TYPE, + BinaryWriter, + JsonWriter, + TrajectoryConverter, + TrajectoryData, + AgentData +) +from simulariumio.smoldyn.smoldyn_data import InputFileData +from simulariumio.smoldyn import SmoldynConverter, SmoldynData +from simulariumio.filters import TranslateFilter +from biosimulators_utils.combine.data_model import CombineArchiveContent +from biosimulators_utils.combine.io import CombineArchiveReader, CombineArchiveWriter +from biosimulators_utils.archive.io import ArchiveReader, ArchiveWriter +from biosimulators_utils.model_lang.smoldyn.validation import validate_model + + +__all__ = [ + 'ModelValidation', + 'SpatialCombineArchive', + 'SmoldynCombineArchive', + 'BiosimulatorsDataConverter', + 'SmoldynDataConverter', +] + + +"""*Validation/Simulation*""" +@dataclass +class ModelValidation: + errors: List[List[str]] + warnings: List[str] + simulation: smoldynSim + config: List[str] + + def __init__(self, validation: Tuple[List[List[str]], List, Tuple[smoldynSim, List[str]]]): + self.errors = validation[0] + self.warnings = validation[1] + self.simulation = validation[2][0] + self.config = validation[2][1] + + + + +"""*Combine Archives*""" + +# TODO: Add more robust rezipping +class SpatialCombineArchive(ABC): + paths: Dict[str, str] + + def __init__(self, + rootpath: str, + simularium_filename=None): + """ABC Object for storing and setting/getting files pertaining to simularium file conversion. + + Args: + rootpath: root of the unzipped archive. Consider this your working dirpath. + simularium_filename:`Optional`: full path which to assign to the newly generated simularium file. + If using this value, it EXPECTS a full path. Defaults to `{name}_output_for_simularium`. + name: Commonplace name for the archive to be used if no `simularium_filename` is passed. Defaults to + `new_spatial_archive`. + """ + super().__init__() + self.rootpath = rootpath + if not simularium_filename: + simularium_filename = 'spatial_combine_archive' + self.simularium_filename = os.path.join(self.rootpath, simularium_filename) + self.__parse_rootpath() + self.paths = self.get_all_archive_filepaths() + + def __parse_rootpath(self): + """Private method for parsing whether `self.rootpath` is the path to a directory or single OMEX/COMBINE + zipped file. If .omex, then decompress the input path into an unzipped directory for working. + """ + if self.rootpath.endswith('.omex'): + self.unzip() + + def unzip(self, unzipped_output_location: str = None): + reader = ArchiveReader() + try: + if not unzipped_output_location: + unzipped_output_location = self.rootpath.replace( + '.omex', + '_UNZIPPED' + ) # TODO: make tempdir here instead + reader.run(self.rootpath, unzipped_output_location) + print('Omex successfully unzipped!...') + self.rootpath = unzipped_output_location + except Exception as e: + warn(f'Omex could not be unzipped because: {e}') + + def rezip(self, paths_to_write: Optional[List[str]] = None, destination: Optional[str] = None): + if '.omex' in self.rootpath: + writer = ArchiveWriter() + if not paths_to_write: + paths_to_write = list(self.get_all_archive_filepaths().values()) + print(f'HERE THEY ARE: {paths_to_write}') + if not destination: + destination = self.rootpath + writer.run(archive=paths_to_write, archive_filename=destination) + print(f'Omex successfully bundled with the following paths: {paths_to_write}!') + + def get_all_archive_filepaths(self) -> Dict[str, str]: + """Recursively read the contents of the directory found at `self.rootpath` and set their full paths. + + Returns: + `Dict[str, str]`: Dict of form {'path_root': full_path} + """ + paths = {} + if os.path.exists(self.rootpath): + for root, _, files in os.walk(self.rootpath): + paths['root'] = root + for f in files: + fp = os.path.join(root, f) + paths[f] = fp + return paths + + def get_manifest_filepath(self) -> Union[List[str], str]: + """Read SmoldynCombineArchive manifest files. Return all filepaths containing the word 'manifest'. + + Returns: + :obj:`str`: path if there is just one manifest file, otherwise `List[str]` of manifest filepaths. + """ + manifest = [] + for v in list(self.paths.values()): + if 'manifest' in v: + manifest.append(v) + self.paths['manifest'] = v + return list(set(manifest))[0] + + def read_manifest_contents(self): + """Reads the contents of the manifest file within `self.rootpath`. + Read the return value of `self.get_manifest_filepath()` as the input for `CombineArchiveReader.run(). + """ + manifest_fp = self.get_manifest_filepath() + reader = CombineArchiveReader() + return reader.read_manifest(filename=manifest_fp) + + @staticmethod + def generate_new_archive_content(fp: str) -> CombineArchiveContent: + """Factory for generating a new instance of `CombineArchiveContent` using just fp. + + Args: + fp: filepath of the content you wish to add to the combine archive. + + Returns: + `CombineArchiveContent` based on the passed `fp`. + """ + return CombineArchiveContent(fp) + + def add_file_to_manifest(self, contents_fp: str) -> None: + contents = self.read_manifest_contents() + new_content = self.generate_new_archive_content(contents_fp) + contents.append(new_content) + writer = CombineArchiveWriter() + try: + manifest_fp = self.get_manifest_filepath() + writer.write_manifest(contents=contents, filename=manifest_fp) + print('File added to archive manifest contents!') + except Exception as e: + print(e) + warn(f'The simularium file found at {contents_fp} could not be added to manifest.') + return + + def add_modelout_file_to_manifest(self, model_fp) -> None: + return self.add_file_to_manifest(model_fp) + + def add_simularium_file_to_manifest(self, simularium_fp: Optional[str] = None) -> None: + """Read the contents of the manifest file found at `self.rootpath`, create a new instance of + `CombineArchiveContent` using a set simularium_fp, append the new content to the original, + and re-write the archive to reflect the newly added content. + + Args: + simularium_fp:`Optional`: path to the newly generated simularium file. Defaults + to `self.simularium_filename`. + """ + try: + if not simularium_fp: + simularium_fp = self.simularium_filename + self.add_file_to_manifest(simularium_fp) + print('Simularium File added to archive manifest contents!') + except Exception: + raise IOError(f'The simularium file found at {simularium_fp} could not be added to manifest.') + + def verify_smoldyn_in_manifest(self) -> bool: + """Pass the return value of `self.get_manifest_filepath()` into a new instance of `CombineArchiveReader` + such that the string manifest object tuples are evaluated for the presence of `smoldyn`. + + Returns: + `bool`: Whether there exists a smoldyn model in the archive based on the archive's manifest. + """ + manifest_contents = [c.to_tuple() for c in self.read_manifest_contents()] + model_info = manifest_contents[0][1] + return 'smoldyn' in model_info + + @abstractmethod + def set_model_filepath(self, + model_default: str, + model_filename: Optional[str] = None) -> Union[str, None]: + """Recurse `self.rootpath` and search for your simulator's model file extension.""" + pass + + @abstractmethod + def set_model_output_filepath(self) -> None: + """Recursively read the directory at `self.rootpath` and standardize the model output filename to become + `self.model_output_filename`. + """ + pass + + @abstractmethod + def generate_model_validation_object(self) -> ModelValidation: + """Generate an instance of `ModelValidation` based on the output of `self.model_path` + with your simulator's primary validation method. + + Returns: + :obj:`ModelValidation` + """ + pass + + +class SmoldynCombineArchive(SpatialCombineArchive): + def __init__(self, + rootpath: str, + model_output_filename='modelout.txt', + simularium_filename='smoldyn_combine_archive'): + """Object for handling the output of Smoldyn simulation data. Implementation child of `SpatialCombineArchive`. + + Args: + rootpath: fp to the root of the archive 'working dir'. + model_output_filename: filename ONLY not filepath of the model file you are working with. Defaults to + `modelout.txt`. + simularium_filename: + """ + super().__init__(rootpath, simularium_filename) + self.set_model_filepath() + self.model_output_filename = os.path.join(self.rootpath, model_output_filename) + self.paths['model_output_file'] = self.model_output_filename + + def set_model_filepath(self, model_filename: Optional[str] = None, model_default='model.txt'): + """Recursively read the full paths of all files in `self.paths` and return the full path of the file + containing the term 'model.txt', which is the naming convention. + Implementation of ancestral abstract method. + + Args: + model_filename: `Optional[str]`: index by which to label a file in directory as the model file. + Defaults to `model_default`. + model_default: `str`: default model filename naming convention. Defaults to `'model.txt'` + """ + if not model_filename: + model_filename = os.path.join(self.rootpath, model_default) # default Smoldyn model name + for k in self.paths.keys(): + full_path = self.paths[k] + if model_filename in full_path: + # noinspection PyAttributeOutsideInit + self.model_path = model_filename + + def set_model_output_filepath(self) -> None: + """Recursively search the directory at `self.rootpath` for a smoldyn + modelout file (`.txt`) and standardize the model output filename to become + `self.model_output_filename`. Implementation of ancestral abstract method. + """ + for root, _, files in os.walk(self.rootpath): + for f in files: + if f.endswith('.txt') and 'model' not in f and os.path.exists(f): + f = os.path.join(root, f) + os.rename(f, self.model_output_filename) + + def generate_model_validation_object(self) -> ModelValidation: + """Generate an instance of `ModelValidation` based on the output of `self.model_path` + with `biosimulators-utils.model_lang.smoldyn.validate_model` method. + Implementation of ancestral abstract method. + + Returns: + :obj:`ModelValidation` + """ + validation_info = validate_model(self.model_path) + validation = ModelValidation(validation_info) + return validation + + + + +"""*Converters*""" + +class BiosimulatorsDataConverter(ABC): + has_smoldyn: bool + + def __init__(self, archive: SpatialCombineArchive): + """This class serves as the abstract interface for a simulator-specific implementation + of utilities through which the user may convert Biosimulators outputs to a valid simularium File. + + Args: + :obj:`archive`:(`SpatialCombineArchive`): instance of an archive to base conv and save on. + """ + self.archive = archive + self.has_smoldyn = self.archive.verify_smoldyn_in_manifest() + + # Factory Methods + @abstractmethod + def generate_output_data_object( + self, + file_data: InputFileData, + display_data: Optional[Dict[str, DisplayData]] = None, + spatial_units="nm", + temporal_units="ns", + ): + """Factory to generate a data object to fit the simulariumio.TrajectoryData interface. + """ + pass + + @abstractmethod + def translate_data_object(self, data_object, box_size, n_dim) -> TrajectoryData: + """Factory to create a mirrored negative image of a distribution and apply it to 3dimensions if + AND ONLY IF it contains all non-negative values. + """ + pass + + @abstractmethod + def generate_simularium_file( + self, + simularium_filename: str, + box_size: float, + translate: bool, + spatial_units="nm", + temporal_units="ns", + n_dim=3, + display_data: Optional[Dict[str, DisplayData]] = None, + ) -> None: + """Factory for a taking in new data_object, optionally translate it, convert to simularium, and save. + """ + pass + + @abstractmethod + def generate_converter(self, data: TrajectoryData): + """Factory for creating a new instance of a translator/filter converter based on the Simulator, + whose output you are attempting to visualize. + """ + pass + + @staticmethod + def generate_agent_data_object( + timestep: int, + total_steps: int, + n_agents: int, + box_size: float, + min_radius: int, + max_radius: int, + display_data_dict: Dict[str, DisplayData], + type_names: List[List[str]], + positions=None, + radii=None, + ) -> AgentData: + """Factory for a new instance of an `AgentData` object following the specifications of the simulation within the + relative combine archive. + + Returns: + `AgentData` instance. + """ + positions = positions or np.random.uniform(size=(total_steps, n_agents, 3)) * box_size - box_size * 0.5 + radii = (max_radius - min_radius) * np.random.uniform(size=(total_steps, n_agents)) + min_radius + return AgentData( + times=timestep * np.array(list(range(total_steps))), + n_agents=np.array(total_steps * [n_agents]), + viz_types=np.array(total_steps * [n_agents * [1000.0]]), # default viz type = 1000 + unique_ids=np.array(total_steps * [list(range(n_agents))]), + types=type_names, + positions=positions, + radii=radii, + display_data=display_data_dict + ) + + @staticmethod + def prepare_simularium_fp(**simularium_config) -> str: + """Generate a simularium dir and joined path if not using the init object. + + Kwargs: + (obj):`**simularium_config`: keys are 'simularium_dirpath' and 'simularium_fname' + + Returns: + (obj):`str`: complete simularium filepath + """ + dirpath = simularium_config.get('simularium_dirpath') + if not os.path.exists(dirpath): + os.mkdir(dirpath) + return os.path.join(dirpath, simularium_config.get('simularium_fname')) + + @staticmethod + def generate_metadata_object(box_size: np.ndarray[int], camera_data: CameraData) -> MetaData: + """Factory for a new instance of `simulariumio.MetaData` based on the input params of this method. + Currently, `ModelMetaData` is not supported as a param. + + Args: + box_size: ndarray containing the XYZ dims of the simulation bounding volume. Defaults to [100,100,100]. + camera_data: new `CameraData` instance to control visuals. + + Returns: + `MetaData` instance. + """ + return MetaData(box_size=box_size, camera_defaults=camera_data) + + @staticmethod + def generate_camera_data_object( + position: np.ndarray, + look_position: np.ndarray, + up_vector: np.ndarray + ) -> CameraData: + """Factory for a new instance of `simulariumio.CameraData` based on the input params. + Wraps the simulariumio object. + + Args: + position: 3D position of the camera itself Default: np.array([0.0, 0.0, 120.0]). + look_position: np.ndarray (shape = [3]) position the camera looks at Default: np.zeros(3). + up_vector: np.ndarray (shape = [3]) the vector that defines which direction is “up” in the + camera’s view Default: np.array([0.0, 1.0, 0.0]) + + Returns: + `CameraData` instance. + """ + return CameraData(position=position, look_at_position=look_position, up_vector=up_vector) + + @staticmethod + def generate_display_data_object( + name: str, + radius: float, + display_type=None, + obj_color: Optional[str] = None, + ) -> DisplayData: + """Factory for creating a new instance of `simularimio.DisplayData` based on the params. + + Args: + name: name of agent + radius: `float` + display_type: any one of the `simulariumio.DISPLAY_TYPE` properties. Defaults to None. + obj_color: `str`: hex color of the display agent. + + Returns: + `DisplayData` instance. + """ + return DisplayData( + name=name, + radius=radius, + display_type=display_type, + color=obj_color + ) + + @staticmethod + def generate_display_data_object_dict(agent_displays: List) -> Dict[str, DisplayData]: + """Factory to generate a display object dict. + + Args: + agent_displays: `List[AgentDisplayData]`: A list of `AgentDisplayData` instances which describe the + visualized agents. + + Returns: + `Dict[str, DisplayData]` + """ + displays = {} + for agent_display in agent_displays: + key = agent_display.name + display = DisplayData( + name=agent_display.name, + radius=agent_display.radius, + display_type=agent_display.display_type, + url=agent_display.url, + color=agent_display.color + ) + displays[key] = display + return displays + + def generate_input_file_data_object(self, model_output_file: Optional[str] = None) -> InputFileData: + """Factory that generates a new instance of `simulariumio.data_model.InputFileData` based on + `self.archive.model_output_filename` (which itself is derived from the model file) if no `model_output_file` + is passed. + + Args: + model_output_file(:obj:`str`): `Optional`: file on which to base the `InputFileData` instance. + Returns: + (:obj:`InputFileData`): simulariumio input file data object based on `self.archive.model_output_filename` + + """ + model_output_file = model_output_file or self.archive.model_output_filename + return InputFileData(model_output_file) + + # IO Methods + @staticmethod + def write_simularium_file( + data: Union[SmoldynData, TrajectoryData], + simularium_filename: str, + save_format: str, + validation=True + ) -> None: + """Takes in either a `SmoldynData` or `TrajectoryData` instance and saves a simularium file based on it + with the name of `simularium_filename`. If none is passed, the file will be saved in `self.archive.rootpath` + + Args: + data(:obj:`Union[SmoldynData, TrajectoryData]`): data object to save. + simularium_filename(:obj:`str`): `Optional`: name by which to save the new simularium file. If None is + passed, will default to `self.archive.rootpath/self.archive.simularium_filename`. + save_format(:obj:`str`): format which to write the `data`. Options include `json, binary`. + validation(:obj:`bool`): whether to call the wrapped method using `validate_ids=True`. Defaults + to `True`. + """ + save_format = save_format.lower() + if not os.path.exists(simularium_filename): + if 'binary' in save_format: + writer = BinaryWriter() + elif 'json' in save_format: + writer = JsonWriter() + else: + warn('You must provide a valid writer object.') + return + return writer.save(trajectory_data=data, output_path=simularium_filename, validate_ids=validation) + + def simularium_to_json(self, data: Union[SmoldynData, TrajectoryData], simularium_filename: str, v=True) -> None: + """Write the contents of the simularium stream to a JSON Simularium file. + + Args: + data: data to write. + simularium_filename: filepath at which to write the new simularium file. + v: whether to call the wrapped method with validate_ids=True. Defaults to `True`. + """ + return self.write_simularium_file( + data=data, + simularium_filename=simularium_filename, + save_format='json', + validation=v + ) + + def simularium_to_binary(self, data: Union[SmoldynData, TrajectoryData], simularium_filename: str, v=True) -> None: + """Write the contents of the simularium stream to a Binary Simularium file. + + Args: + data: data to write. + simularium_filename: filepath at which to write the new simularium file. + v: whether to call the wrapped method with validate_ids=True. Defaults to `True`. + """ + return self.write_simularium_file( + data=data, + simularium_filename=simularium_filename, + save_format='binary', + validation=v + ) + + +class SmoldynDataConverter(BiosimulatorsDataConverter): + def __init__(self, archive: SmoldynCombineArchive, generate_model_output: bool = True): + """General class for converting Smoldyn output (modelout.txt) to .simularium. Checks the passed archive object + directory for a `modelout.txt` file (standard Smoldyn naming convention) and runs the simulation by default if + not. At the time of construction, checks for the existence of a simulation `out.txt` file and runs + `self.generate_model_output_file()` if such a file does not exist, based on `self.archive`. To turn + this off, pass `generate_data` as `False`. + + Args: + archive (:obj:`SmoldynCombineArchive`): instance of a `SmoldynCombineArchive` object. + generate_model_output(`bool`): Automatically generates and standardizes the name of a + smoldyn model output file based on the `self.archive` parameter if True. Defaults to `True`. + """ + super().__init__(archive) + if generate_model_output: + self.generate_model_output_file() + + def generate_model_output_file(self, + model_output_filename: Optional[str] = None, + smoldyn_archive: Optional[SmoldynCombineArchive] = None) -> None: + """Generate a modelout file if one does not exist using the `ModelValidation` interface via + `.utils.generate_model_validation_object` method. If either parameter is not passed, the data will + be derived from `self.archive(:obj:`SmoldynCombineArchive`)`. + + Args: + model_output_filename(:obj:`str`): `Optional`: filename from which to run a smoldyn simulation + and generate an out.txt file. Defaults to `self.archive.model_output_filename`. + smoldyn_archive(:obj:`SmoldynCombineArchive`): `Optional`: instance of `SmoldynCombineArchive` from + which to base the simulation/model.txt from. Defaults to `self.archive`. + + Returns: + None + """ + model_output_filename = model_output_filename or self.archive.model_output_filename + archive = smoldyn_archive or self.archive + if not os.path.exists(model_output_filename): + validation = archive.generate_model_validation_object() + validation.simulation.runSim() + + # standardize the modelout filename + for root, _, files in os.walk(archive.rootpath): + for f in files: + if f.endswith('.txt') and 'model' not in f: + f = os.path.join(root, f) + os.rename(f, archive.model_output_filename) + + def read_model_output_dataframe(self) -> pd.DataFrame: + """Create a pandas dataframe from the contents of `self.archive.model_output_filename`. WARNING: this method + is currently experimental. + + Returns: + `pd.DataFrame`: a pandas dataframe with the columns: ['mol_name', 'x', 'y', 'z', 't'] + """ + warn('WARNING: This method is experimental and may not function properly.') + colnames = ['mol_name', 'x', 'y', 'z', 't'] + return pd.read_csv(self.archive.model_output_filename, sep=" ", header=None, skiprows=1, names=colnames) + + def write_model_output_dataframe_to_csv(self, save_fp: str) -> None: + """Write output dataframe to csv file. + + Args: + save_fp:`str`: path at which to save the csv-converted pandas df. + """ + df = self.read_model_output_dataframe() + return df.to_csv(save_fp) + + def generate_output_data_object( + self, + file_data: InputFileData, + display_data: Optional[Dict[str, DisplayData]] = None, + meta_data: Optional[MetaData] = None, + spatial_units="nm", + temporal_units="ns", + ) -> SmoldynData: + """Generate a new instance of `SmoldynData`. If passing `meta_data`, please create a new `MetaData` instance + using the `self.generate_metadata_object` interface of this same class. + + Args: + file_data: (:obj:`InputFileData`): `simulariumio.InputFileData` instance based on model output. + display_data: (:obj:`Dict[Dict[str, DisplayData]]`): `Optional`: if passing this parameter, please + use the `self.generate_display_object_dict` interface of this same class. + meta_data: (:obj:`Metadata`): new instance of `Metadata` object. If passing this parameter, please use the + `self.generate_metadata_object` interface method of this same class. + spatial_units: (:obj:`str`): spatial units by which to measure this simularium output. Defaults to `nm`. + temporal_units: (:obj:`str`): time units to base this simularium instance on. Defaults to `ns`. + + Returns: + :obj:`SmoldynData` + """ + return SmoldynData( + smoldyn_file=file_data, + spatial_units=UnitData(spatial_units), + time_units=UnitData(temporal_units), + display_data=display_data, + meta_data=meta_data + ) + + def generate_converter(self, data: SmoldynData) -> SmoldynConverter: + """Implementation of parent-level factory which exposes an object for translating `SmoldynData` instance. + + Args: + data(`SmoldynData`): Data to be translated. + + Returns: + `SmoldynConverter` instance based on the data. + """ + return SmoldynConverter(data) + + def translate_data_object( + self, + c: SmoldynConverter, + box_size: float, + n_dim=3, + translation_magnitude: Optional[Union[int, float]] = None + ) -> TrajectoryData: + """Translate the data object's data if the coordinates are all positive to center the data in the + simularium viewer. + + Args: + c: Instance of `SmoldynConverter` loaded with `SmoldynData`. + box_size: size of the simularium viewer box. + n_dim: n dimensions of the simulation output. Defaults to `3`. + translation_magnitude: magnitude by which to translate and filter. Defaults to `-box_size / 2`. + + Returns: + `TrajectoryData`: translated data object instance. + """ + translation_magnitude = translation_magnitude or -box_size / 2 + return c.filter_data([ + TranslateFilter( + translation_per_type={}, + default_translation=translation_magnitude * np.ones(n_dim) + ), + ]) + + def generate_simularium_file( + self, + box_size=1., + spatial_units="nm", + temporal_units="s", + n_dim=3, + io_format="binary", + translate=True, + overwrite=True, + validate_ids=True, + simularium_filename: Optional[str] = None, + display_data: Optional[Dict[str, DisplayData]] = None, + new_omex_filename: Optional[str] = None, + ) -> None: + """Generate a new simularium file based on `self.archive.rootpath`. If `self.archive.rootpath` is an `.omex` + file, the outputs will be re-bundled. + + Args: + box_size(:obj:`float`): `Optional`: size by which to scale the simulation stage. Defaults to `1.` + spatial_units(:obj:`str`): `Optional`: units by which to measure the spatial aspect + of the simulation. Defaults to `nm`. + temporal_units(:obj:`str`): `Optional`: units by which to measure the temporal aspect + of the simulation. Defaults to `s`. + n_dim(:obj:`int`): `Optional`: n dimensions of the simulation output. Defaults to `3`. + simularium_filename(:obj:`str`): `Optional`: filename by which to save the simularium output. Defaults + to `archive.simularium_filename`. + display_data(:obj:`Dict[str, DisplayData]`): `Optional`: Dictionary of DisplayData objects. + new_omex_filename(:obj:`str`): `Optional`: Filename by which to save the newly generate .omex IF and + only IF `self.archive.rootpath` is an `.omex` file. + io_format(:obj:`str`): format in which to write out the simularium file. Used as an input param to call + `super.write_simularium_file`. Options include `'binary'` and `'json'`. Capitals may be used in + this string. Defaults to `binary`. + translate(:obj:`bool`): Whether to translate the simulation mirror data. Defaults to `True`. + overwrite(:obj:`bool`): Whether to overwrite a simularium file of the same name as `simularium_filename` + if one already exists in the COMBINE archive. Defaults to `True`. + validate_ids(:obj:`bool`): Whether to call the write method using `validation=True`. Defaults to True. + """ + if not simularium_filename: + simularium_filename = self.archive.simularium_filename + + if os.path.exists(simularium_filename): + warn('That file already exists in this COMBINE archive.') + if not overwrite: + warn('Overwrite is turned off an thus a new file will not be generated.') + return + + input_file = self.generate_input_file_data_object() + data = self.generate_output_data_object( + file_data=input_file, + display_data=display_data, + spatial_units=spatial_units, + temporal_units=temporal_units + ) + + if translate: + c = self.generate_converter(data) + data = self.translate_data_object(c, box_size, n_dim, translation_magnitude=box_size) + + # write the simularium file + self.write_simularium_file( + data=data, + simularium_filename=simularium_filename, + save_format=io_format, + validation=validate_ids + ) + print('New Simularium file generated!!') + + # add new file to manifest + self.archive.add_simularium_file_to_manifest(simularium_fp=simularium_filename) + + # re-zip the archive if it was passed as an omex file + if '.omex' in self.archive.rootpath: + writer = ArchiveWriter() + paths = list(self.archive.get_all_archive_filepaths().values()) + writer.run(paths, self.archive.rootpath) + print('Omex bundled!') \ No newline at end of file diff --git a/biosimulators_utils/spatial/io.py b/biosimulators_utils/spatial/io.py new file mode 100644 index 00000000..99d6eee7 --- /dev/null +++ b/biosimulators_utils/spatial/io.py @@ -0,0 +1,60 @@ +"""Methods for writing and reading simularium files within COMBINE/OMEX archives. + +:Author: Alexander Patrie +:Date: 2023-09-16 +:Copyright: 2023, UConn Health +:License: MIT +""" + + +from typing import Optional +from biosimulators_utils.spatial.data_model import SmoldynCombineArchive, SmoldynDataConverter +from biosimulators_utils.spatial.utils import generate_model_validation_object + + +__all__ = [ + 'generate_new_simularium_file', +] + + +# pragma: no cover +def generate_new_simularium_file( + archive_rootpath: str, + simularium_filename: Optional[str] = None, + save_output_df: bool = False, + __fmt: str = 'smoldyn' + ) -> None: + """Generate a new `.simularium` file based on the `model.txt` within the passed-archive rootpath using the above + validation method. Raises a `ValueError` if there are errors present. + + Args: + archive_rootpath (:obj:`str`): Parent dirpath relative to the model.txt file. + simularium_filename (:obj:`str`): `Optional`: Desired save name for the simularium file to be saved + in the `archive_rootpath`. Defaults to `None`. + save_output_df (:obj:`bool`): Whether to save the modelout.txt contents as a pandas df in csv form. Defaults + to `False`. + __fmt (:obj:`str`): format by which to convert and save the simularium file. Currently, only 'smoldyn' is + supported. Defaults to `smoldyn`. + """ + + # verify smoldyn combine archive + if 'smoldyn' in __fmt.lower(): + archive = SmoldynCombineArchive(rootpath=archive_rootpath, simularium_filename=simularium_filename) + + # store and parse model data + model_validation = generate_model_validation_object(archive) + if model_validation.errors: + raise ValueError( + f'There are errors involving your model file:\n{model_validation.errors}\nPlease adjust your model file.' + ) + + # construct converter and save + converter = SmoldynDataConverter(archive) + + if save_output_df: + df = converter.read_model_output_dataframe() + csv_fp = archive.model_output_filename.replace('txt', 'csv') + df.to_csv(csv_fp) + return converter.generate_simularium_file(simularium_filename=simularium_filename) + else: + raise ValueError('The only currently available format is "smoldyn".') \ No newline at end of file diff --git a/biosimulators_utils/spatial/utils.py b/biosimulators_utils/spatial/utils.py new file mode 100644 index 00000000..548adcb4 --- /dev/null +++ b/biosimulators_utils/spatial/utils.py @@ -0,0 +1,29 @@ +"""Utility functions related to `spatial` library. + +:Author: Alexander Patrie +:Date: 2023-09-16 +:Copyright: 2023, UConn Health +:License: MIT +""" + + +from biosimulators_utils.spatial.data_model import SpatialCombineArchive, ModelValidation + + +def generate_model_validation_object(archive: SpatialCombineArchive) -> ModelValidation: + """ Generate an instance of `ModelValidation` based on the output of `archive.model_path` + with above `validate_model` method. + + Args: + archive: (:obj:`SpatialCombineArchive`): Instance of `SpatialCombineArchive` to generate model validation on. + + Returns: + :obj:`ModelValidation` + """ + validation_info = validate_model(archive.model_path) + validation = ModelValidation(validation_info) + return validation + + +def verify_simularium_in_archive(archive: SpatialCombineArchive) -> bool: + return '.simularium' in list(archive.paths.keys()) \ No newline at end of file From 19490d922416b194ef9e2dd9de037b2f879f8c3d Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 15:33:46 -0400 Subject: [PATCH 03/34] feat: updated optional requirements to reflect new spatial library --- requirements.optional.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/requirements.optional.txt b/requirements.optional.txt index 2f988db5..69b993c1 100644 --- a/requirements.optional.txt +++ b/requirements.optional.txt @@ -23,10 +23,11 @@ python_libsbml [smoldyn] smoldyn >= 2.66 -# simulariumio -# [simularium] -# biosimulators-simularium +[spatial] +smoldyn >= 2.66 +simulariumio +zarr ################################# ## Visualization formats @@ -41,4 +42,4 @@ docker >= 4.4 ################################# ## Console logging [logging] -capturer +capturer \ No newline at end of file From 4179522bf85db34830f1b5391bc8a4992edec653 Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 16:06:51 -0400 Subject: [PATCH 04/34] added immutable instance-metadata parameter --- biosimulators_utils/spatial/data_model.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index 5b593d92..f4991808 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -74,6 +74,8 @@ def __init__(self, validation: Tuple[List[List[str]], List, Tuple[smoldynSim, Li # TODO: Add more robust rezipping class SpatialCombineArchive(ABC): + __zipped_file_format: str + was_unzipped: bool paths: Dict[str, str] def __init__(self, @@ -96,19 +98,27 @@ def __init__(self, self.__parse_rootpath() self.paths = self.get_all_archive_filepaths() + @property + def __zipped_file_format(self) -> str: + return '.omex' + def __parse_rootpath(self): """Private method for parsing whether `self.rootpath` is the path to a directory or single OMEX/COMBINE - zipped file. If .omex, then decompress the input path into an unzipped directory for working. + zipped file. If .omex, then decompress the input path into an unzipped directory for working and sets + `self.was_unzipped` to `True` and `False` if not. """ - if self.rootpath.endswith('.omex'): + if self.rootpath.endswith(self.__zipped_file_format): self.unzip() + self.was_unzipped = True + else: + self.was_unzipped = False def unzip(self, unzipped_output_location: str = None): reader = ArchiveReader() try: if not unzipped_output_location: unzipped_output_location = self.rootpath.replace( - '.omex', + self.__zipped_file_format, '_UNZIPPED' ) # TODO: make tempdir here instead reader.run(self.rootpath, unzipped_output_location) @@ -118,7 +128,7 @@ def unzip(self, unzipped_output_location: str = None): warn(f'Omex could not be unzipped because: {e}') def rezip(self, paths_to_write: Optional[List[str]] = None, destination: Optional[str] = None): - if '.omex' in self.rootpath: + if self.__zipped_file_format in self.rootpath: writer = ArchiveWriter() if not paths_to_write: paths_to_write = list(self.get_all_archive_filepaths().values()) @@ -776,7 +786,7 @@ def generate_simularium_file( self.archive.add_simularium_file_to_manifest(simularium_fp=simularium_filename) # re-zip the archive if it was passed as an omex file - if '.omex' in self.archive.rootpath: + if self.archive.was_unzipped: writer = ArchiveWriter() paths = list(self.archive.get_all_archive_filepaths().values()) writer.run(paths, self.archive.rootpath) From aea663577286e86fbac80af205b447ba7163e6f0 Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 16:11:33 -0400 Subject: [PATCH 05/34] chore: added optional default io format to high level util function --- biosimulators_utils/spatial/io.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/biosimulators_utils/spatial/io.py b/biosimulators_utils/spatial/io.py index 99d6eee7..b5770363 100644 --- a/biosimulators_utils/spatial/io.py +++ b/biosimulators_utils/spatial/io.py @@ -22,6 +22,7 @@ def generate_new_simularium_file( archive_rootpath: str, simularium_filename: Optional[str] = None, save_output_df: bool = False, + io_format: str = 'json', __fmt: str = 'smoldyn' ) -> None: """Generate a new `.simularium` file based on the `model.txt` within the passed-archive rootpath using the above @@ -33,8 +34,10 @@ def generate_new_simularium_file( in the `archive_rootpath`. Defaults to `None`. save_output_df (:obj:`bool`): Whether to save the modelout.txt contents as a pandas df in csv form. Defaults to `False`. + io_format (:obj:`str`): format by which to save the simularium file. Options are `'binary'` and `'json'`. + defaults to `'json'`. __fmt (:obj:`str`): format by which to convert and save the simularium file. Currently, only 'smoldyn' is - supported. Defaults to `smoldyn`. + supported. Defaults to `'smoldyn'`. """ # verify smoldyn combine archive @@ -55,6 +58,9 @@ def generate_new_simularium_file( df = converter.read_model_output_dataframe() csv_fp = archive.model_output_filename.replace('txt', 'csv') df.to_csv(csv_fp) - return converter.generate_simularium_file(simularium_filename=simularium_filename) + return converter.generate_simularium_file( + simularium_filename=simularium_filename, + io_format=io_format + ) else: raise ValueError('The only currently available format is "smoldyn".') \ No newline at end of file From 02b8cf94bc8865fab2e73cf3d52c6398f94f0b9b Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 16:14:12 -0400 Subject: [PATCH 06/34] fix: finished falsy eval --- biosimulators_utils/spatial/data_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index f4991808..9791cad7 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -395,7 +395,7 @@ def generate_agent_data_object( `AgentData` instance. """ positions = positions or np.random.uniform(size=(total_steps, n_agents, 3)) * box_size - box_size * 0.5 - radii = (max_radius - min_radius) * np.random.uniform(size=(total_steps, n_agents)) + min_radius + radii = radii or (max_radius - min_radius) * np.random.uniform(size=(total_steps, n_agents)) + min_radius return AgentData( times=timestep * np.array(list(range(total_steps))), n_agents=np.array(total_steps * [n_agents]), From b9394710ac2b7c12a192feef0166e2503d512258 Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 16:47:53 -0400 Subject: [PATCH 07/34] chore: updated args and type annotations --- biosimulators_utils/spatial/data_model.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index 9791cad7..3af2c55f 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -274,7 +274,7 @@ def __init__(self, self.model_output_filename = os.path.join(self.rootpath, model_output_filename) self.paths['model_output_file'] = self.model_output_filename - def set_model_filepath(self, model_filename: Optional[str] = None, model_default='model.txt'): + def set_model_filepath(self, model_default='model.txt', model_filename: Optional[str] = None): """Recursively read the full paths of all files in `self.paths` and return the full path of the file containing the term 'model.txt', which is the naming convention. Implementation of ancestral abstract method. @@ -347,7 +347,10 @@ def generate_output_data_object( pass @abstractmethod - def translate_data_object(self, data_object, box_size, n_dim) -> TrajectoryData: + def translate_data_object(self, + data_object_converter: TrajectoryConverter, + box_size: Union[float, int], + n_dim: str = 3) -> TrajectoryData: """Factory to create a mirrored negative image of a distribution and apply it to 3dimensions if AND ONLY IF it contains all non-negative values. """ From 3a3946a7003cd65740e8c42708a35a14982c6c11 Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 17:00:21 -0400 Subject: [PATCH 08/34] chore: updated method signature --- biosimulators_utils/spatial/data_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index 3af2c55f..e6a9bd2d 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -693,8 +693,8 @@ def generate_converter(self, data: SmoldynData) -> SmoldynConverter: def translate_data_object( self, - c: SmoldynConverter, - box_size: float, + data_object_converter: SmoldynConverter, + box_size: Union[float, int], n_dim=3, translation_magnitude: Optional[Union[int, float]] = None ) -> TrajectoryData: @@ -702,7 +702,7 @@ def translate_data_object( simularium viewer. Args: - c: Instance of `SmoldynConverter` loaded with `SmoldynData`. + data_object_converter: Instance of `SmoldynConverter` loaded with `SmoldynData`. box_size: size of the simularium viewer box. n_dim: n dimensions of the simulation output. Defaults to `3`. translation_magnitude: magnitude by which to translate and filter. Defaults to `-box_size / 2`. From 0882d2ecb766709a31b31aa62453c4489e4853a6 Mon Sep 17 00:00:00 2001 From: alex patrie Date: Wed, 4 Oct 2023 17:20:08 -0400 Subject: [PATCH 09/34] fix: linting fixes --- biosimulators_utils/spatial/data_model.py | 13 +++++-------- biosimulators_utils/spatial/io.py | 3 ++- biosimulators_utils/spatial/utils.py | 3 ++- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index e6a9bd2d..5958fbaa 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -28,7 +28,6 @@ UnitData, MetaData, DisplayData, - DISPLAY_TYPE, BinaryWriter, JsonWriter, TrajectoryConverter, @@ -68,10 +67,9 @@ def __init__(self, validation: Tuple[List[List[str]], List, Tuple[smoldynSim, Li self.config = validation[2][1] +"""*Spatial Combine Archives*""" -"""*Combine Archives*""" - # TODO: Add more robust rezipping class SpatialCombineArchive(ABC): __zipped_file_format: str @@ -316,10 +314,9 @@ def generate_model_validation_object(self) -> ModelValidation: return validation - - """*Converters*""" + class BiosimulatorsDataConverter(ABC): has_smoldyn: bool @@ -697,7 +694,7 @@ def translate_data_object( box_size: Union[float, int], n_dim=3, translation_magnitude: Optional[Union[int, float]] = None - ) -> TrajectoryData: + ) -> TrajectoryData: """Translate the data object's data if the coordinates are all positive to center the data in the simularium viewer. @@ -711,7 +708,7 @@ def translate_data_object( `TrajectoryData`: translated data object instance. """ translation_magnitude = translation_magnitude or -box_size / 2 - return c.filter_data([ + return data_object_converter.filter_data([ TranslateFilter( translation_per_type={}, default_translation=translation_magnitude * np.ones(n_dim) @@ -793,4 +790,4 @@ def generate_simularium_file( writer = ArchiveWriter() paths = list(self.archive.get_all_archive_filepaths().values()) writer.run(paths, self.archive.rootpath) - print('Omex bundled!') \ No newline at end of file + print('Omex bundled!') diff --git a/biosimulators_utils/spatial/io.py b/biosimulators_utils/spatial/io.py index b5770363..5cd98fc8 100644 --- a/biosimulators_utils/spatial/io.py +++ b/biosimulators_utils/spatial/io.py @@ -63,4 +63,5 @@ def generate_new_simularium_file( io_format=io_format ) else: - raise ValueError('The only currently available format is "smoldyn".') \ No newline at end of file + raise ValueError('The only currently available format is "smoldyn".') + \ No newline at end of file diff --git a/biosimulators_utils/spatial/utils.py b/biosimulators_utils/spatial/utils.py index 548adcb4..8e9ff64c 100644 --- a/biosimulators_utils/spatial/utils.py +++ b/biosimulators_utils/spatial/utils.py @@ -7,6 +7,7 @@ """ +from biosimulators_utils.model_lang.smoldyn.validation import validate_model from biosimulators_utils.spatial.data_model import SpatialCombineArchive, ModelValidation @@ -26,4 +27,4 @@ def generate_model_validation_object(archive: SpatialCombineArchive) -> ModelVal def verify_simularium_in_archive(archive: SpatialCombineArchive) -> bool: - return '.simularium' in list(archive.paths.keys()) \ No newline at end of file + return '.simularium' in list(archive.paths.keys()) From 5d04a962110a4cb2bb7d36b5f4ef4a1442edc6b0 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 08:00:21 -0400 Subject: [PATCH 10/34] chore: formatted whitespace for ci --- biosimulators_utils/spatial/data_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index 5958fbaa..ce95c0c3 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -20,6 +20,7 @@ from warnings import warn from typing import Optional, Tuple, Dict, List, Union from abc import ABC, abstractmethod +# noinspection PyPackageRequirements from smoldyn import Simulation as smoldynSim import numpy as np import pandas as pd @@ -53,6 +54,8 @@ """*Validation/Simulation*""" + + @dataclass class ModelValidation: errors: List[List[str]] From 6d829417830414d83085bbfcbef14f62fd54fb25 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 08:04:48 -0400 Subject: [PATCH 11/34] chore: updated utils subdir docstrings --- biosimulators_utils/spatial/utils.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/biosimulators_utils/spatial/utils.py b/biosimulators_utils/spatial/utils.py index 8e9ff64c..4248e289 100644 --- a/biosimulators_utils/spatial/utils.py +++ b/biosimulators_utils/spatial/utils.py @@ -7,16 +7,18 @@ """ +from typing import Union from biosimulators_utils.model_lang.smoldyn.validation import validate_model -from biosimulators_utils.spatial.data_model import SpatialCombineArchive, ModelValidation +from biosimulators_utils.spatial.data_model import SpatialCombineArchive, SmoldynCombineArchive, ModelValidation -def generate_model_validation_object(archive: SpatialCombineArchive) -> ModelValidation: +def generate_model_validation_object(archive: Union[SpatialCombineArchive, SmoldynCombineArchive]) -> ModelValidation: """ Generate an instance of `ModelValidation` based on the output of `archive.model_path` with above `validate_model` method. Args: - archive: (:obj:`SpatialCombineArchive`): Instance of `SpatialCombineArchive` to generate model validation on. + archive (:obj:`Union[SpatialCombineArchive, SmoldynCombineArchive]`): Instance of `SpatialCombineArchive` + by which to generate model validation on. Returns: :obj:`ModelValidation` @@ -26,5 +28,13 @@ def generate_model_validation_object(archive: SpatialCombineArchive) -> ModelVal return validation -def verify_simularium_in_archive(archive: SpatialCombineArchive) -> bool: +def verify_simularium_in_archive(archive: Union[SpatialCombineArchive, SmoldynCombineArchive]) -> bool: + """Verify the presence of a simularium file within the passed `archive.paths` dict values. + + Args: + archive: archive to search for simularium file. + + Returns: + `bool`: Whether there exists a simularium file in the directory. + """ return '.simularium' in list(archive.paths.keys()) From 150be1fa095a72df0845d114e4247de923f4dd4e Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 13:21:48 -0400 Subject: [PATCH 12/34] chore: reformatting --- biosimulators_utils/combine/exec.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 50991afa..c2ae530e 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -7,7 +7,6 @@ """ -from biosimulators_simularium import generate_new_simularium_file from ..archive.io import ArchiveWriter from ..archive.utils import build_archive_from_paths from ..config import get_config, Config # noqa: F401 @@ -30,18 +29,24 @@ import os import tempfile import shutil -import types # noqa: F401 +from typing import Optional +from types import FunctionType # noqa: F401 + __all__ = [ 'exec_sedml_docs_in_archive', ] -def exec_sedml_docs_in_archive(sed_doc_executer, archive_filename, out_dir, apply_xml_model_changes=False, +# noinspection PyIncorrectDocstring +def exec_sedml_docs_in_archive(sed_doc_executer: FunctionType, + archive_filename: str, + out_dir: str, + apply_xml_model_changes=False, sed_doc_executer_supported_features=(Task, Report, DataSet, Plot2D, Curve, Plot3D, Surface), sed_doc_executer_logged_features=(Task, Report, DataSet, Plot2D, Curve, Plot3D, Surface), log_level=StandardOutputErrorCapturerLevel.c, - config=None): + config: Optional[Config] = None): """ Execute the SED-ML files in a COMBINE/OMEX archive (execute tasks and save outputs) Args: @@ -222,10 +227,6 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, log_level=log_level, indent=1, config=config) - if config.SPATIAL: - generate_new_simularium_file( - archive_rootpath=archive_tmp_dir - ) if config.COLLECT_COMBINE_ARCHIVE_RESULTS: results[content.location] = doc_results if config.LOG: From ac2c6863fee91639c9dc54d9befdd84c015332cb Mon Sep 17 00:00:00 2001 From: alex patrie Date: Thu, 5 Oct 2023 15:16:16 -0400 Subject: [PATCH 13/34] temp archive scan --- biosimulators_utils/combine/exec.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index c2ae530e..09422eca 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -194,6 +194,9 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, else: log = None + # get archive contents and check for spatial + archive_contents = archive.get_master_content() + # execute SED-ML files: execute tasks and save output exceptions = [] for i_content, content in enumerate(sedml_contents): From 321735190a3bd82bb71b84b6304d7ea91a3c67d4 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 15:38:03 -0400 Subject: [PATCH 14/34] feat: added spatial attributes to common config object and updated combine exec method --- biosimulators_utils/combine/data_model.py | 4 +++- biosimulators_utils/combine/exec.py | 8 ++++++++ biosimulators_utils/config.py | 10 +++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/biosimulators_utils/combine/data_model.py b/biosimulators_utils/combine/data_model.py index 1bbcfb88..b3420950 100644 --- a/biosimulators_utils/combine/data_model.py +++ b/biosimulators_utils/combine/data_model.py @@ -11,6 +11,8 @@ import abc import datetime # noqa: F401 import enum +from typing import List + __all__ = [ 'CombineArchiveBase', @@ -40,7 +42,7 @@ def __init__(self, contents=None): """ self.contents = contents or [] - def get_master_content(self): + def get_master_content(self) -> List: """ Get the master content of an archive Returns: diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 09422eca..b685de24 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -7,6 +7,7 @@ """ +from ..spatial.data_model import SmoldynCombineArchive, SmoldynDataConverter from ..archive.io import ArchiveWriter from ..archive.utils import build_archive_from_paths from ..config import get_config, Config # noqa: F401 @@ -196,6 +197,9 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # get archive contents and check for spatial archive_contents = archive.get_master_content() + for content in archive_contents: + if 'smoldyn'.lower() in content.location: + config.SPATIAL = True # execute SED-ML files: execute tasks and save output exceptions = [] @@ -248,6 +252,10 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, doc_log.duration = (datetime.datetime.now() - doc_start_time).total_seconds() doc_log.export() + # generate simularium file if spatial + if config.SPATIAL: + spatial_archive = SmoldynCombineArchive + print('') # handle smoldyn output/simularium conversion diff --git a/biosimulators_utils/config.py b/biosimulators_utils/config.py index db5ab4c8..21ac6f12 100644 --- a/biosimulators_utils/config.py +++ b/biosimulators_utils/config.py @@ -28,8 +28,10 @@ DEFAULT_BIOSIMULATIONS_API_ENDPOINT = 'https://api.biosimulations.org/' DEFAULT_BIOSIMULATIONS_API_AUTH_ENDPOINT = 'https://auth.biosimulations.org/oauth/token' DEFAULT_BIOSIMULATIONS_API_AUDIENCE = 'api.biosimulations.org' +DEFAULT_SUPPORTED_SPATIAL_SIMULATOR = 'smoldyn' +# noinspection PyPep8Naming,PyDefaultArgument class Config(object): """ Configuration @@ -97,7 +99,8 @@ def __init__(self, BIOSIMULATIONS_API_AUDIENCE=DEFAULT_BIOSIMULATIONS_API_AUDIENCE, VERBOSE=False, DEBUG=False, - SPATIAL=False): + SPATIAL=False, + SUPPORTED_SPATIAL_SIMULATOR=DEFAULT_SUPPORTED_SPATIAL_SIMULATOR): """ Args: OMEX_METADATA_INPUT_FORMAT (:obj:`OmexMetadataInputFormat`, optional): format to validate OMEX Metadata files against @@ -133,6 +136,8 @@ def __init__(self, BIOSIMULATIONS_API_AUDIENCE (:obj:`str`, optional): audience for the BioSimulations API VERBOSE (:obj:`bool`, optional): whether to display the detailed output of the execution of each task DEBUG (:obj:`bool`, optional): whether to raise exceptions rather than capturing them + SPATIAL (:obj:`bool`, optional): whether the simulation is spatial in nature and able to be simularium-ed + SUPPORTED_SPATIAL_SIMULATOR (:obj:`strl`, optional): spatial simulator that this config supports. """ self.OMEX_METADATA_INPUT_FORMAT = OMEX_METADATA_INPUT_FORMAT self.OMEX_METADATA_OUTPUT_FORMAT = OMEX_METADATA_OUTPUT_FORMAT @@ -164,6 +169,7 @@ def __init__(self, self.VERBOSE = VERBOSE self.DEBUG = DEBUG self.SPATIAL = SPATIAL + self.SUPPORTED_SPATIAL_SIMULATOR = SUPPORTED_SPATIAL_SIMULATOR def get_config(): @@ -218,6 +224,8 @@ def get_config(): BIOSIMULATIONS_API_AUDIENCE=os.environ.get('BIOSIMULATIONS_API_AUDIENCE', DEFAULT_BIOSIMULATIONS_API_AUDIENCE), VERBOSE=os.environ.get('VERBOSE', '1').lower() in ['1', 'true'], DEBUG=os.environ.get('DEBUG', '0').lower() in ['1', 'true'], + SPATIAL=os.environ.get('SPATIAL', '0').lower() in ['1', 'true'], + SUPPORTED_SPATIAL_SIMULATOR=os.environ.get('SUPPORTED_SPATIAL_SIMULATOR', DEFAULT_SUPPORTED_SPATIAL_SIMULATOR) ) From c006e790e92bb087292cec5b774d71180f9a655e Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 15:46:02 -0400 Subject: [PATCH 15/34] feat: implemented simularium converter logic in combine exec method --- biosimulators_utils/combine/exec.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index b685de24..8368e293 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -198,7 +198,7 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # get archive contents and check for spatial archive_contents = archive.get_master_content() for content in archive_contents: - if 'smoldyn'.lower() in content.location: + if config.SUPPORTED_SPATIAL_SIMULATOR.lower() in content.location: config.SPATIAL = True # execute SED-ML files: execute tasks and save output @@ -254,7 +254,23 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # generate simularium file if spatial if config.SPATIAL: - spatial_archive = SmoldynCombineArchive + simularium_filename = os.path.join(out_dir, 'output') + spatial_archive = SmoldynCombineArchive(rootpath=out_dir, simularium_filename=simularium_filename) + + # check if modelout file exists + if not os.path.exists(spatial_archive.model_path): + generate_model_output_file = True + else: + generate_model_output_file = False + + # construct converter + converter = SmoldynDataConverter( + archive=spatial_archive, + generate_model_output=generate_model_output_file + ) + + # generate simularium file + converter.generate_simularium_file(io_format='json') print('') From 96f04ce7150861ecdb5548f6ad1fbdfe19b0dbba Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 15:46:32 -0400 Subject: [PATCH 16/34] chore: removed unused comments --- biosimulators_utils/combine/exec.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 8368e293..7f397805 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -274,14 +274,6 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, print('') - # handle smoldyn output/simularium conversion - # arch = SmoldynCombineArchive(rootpath=archive_tmp_dir) - # if arch.verify_smoldyn_in_manifest(): - # converter = SmoldynDataConverter(arch) - # simularium_fp = os.path.join(arch.rootpath, 'simularium_output') - # arch.simularium_filename = simularium_fp - # converter.generate_simularium_file() - if config.BUNDLE_OUTPUTS: print('Bundling outputs ...') From ee726eba2159f739cd8c4b7fcc9569ca83b3580d Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 16:39:30 -0400 Subject: [PATCH 17/34] feat: implemented exec file and method in spatial library --- biosimulators_utils/combine/exec.py | 2 +- biosimulators_utils/spatial/exec.py | 50 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 biosimulators_utils/spatial/exec.py diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 7f397805..c455aa40 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -40,7 +40,7 @@ # noinspection PyIncorrectDocstring -def exec_sedml_docs_in_archive(sed_doc_executer: FunctionType, +def exec_sedml_docs_in_archive(sed_doc_executer, archive_filename: str, out_dir: str, apply_xml_model_changes=False, diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py new file mode 100644 index 00000000..7a6f9ae7 --- /dev/null +++ b/biosimulators_utils/spatial/exec.py @@ -0,0 +1,50 @@ +""" Exec methods for executing spatial tasks in SED-ML files in COMBINE/OMEX archives + +:Author: Alexander Patrie +:Date: 2023-09-16 +:Copyright: 2023, UConn Health +:License: MIT +""" + +# pragma: no cover +# import os +from typing import Optional, Tuple +from biosimulators_utils.log.data_model import CombineArchiveLog +from biosimulators_utils.report.data_model import SedDocumentResults +from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive +from biosimulators_utils.config import Config, get_config + + +try: + from smoldyn.biosimulators.combine import exec_sed_doc +except ImportError: + raise ImportError('The Smoldyn-Python package must be installed to use this feature.') + + +def exec_combine_archive(archive_filename: str, + out_dir: str, + config: Optional[Config] = None) -> Tuple[SedDocumentResults, CombineArchiveLog]: + """Execute the SED tasks defined in a COMBINE/OMEX archive whose contents are spatial/spatiotemporal in nature. + Library-level wrapper method which imports smoldyn.biosimulators.combine and + calls `biosimulators_utils.combine.exec_sedml_docs_in_combine_archive`. Also outputs a simularium file. + + Args: + archive_filename (:obj:`str`): path to the COMBINE/OMEX archive. Must be the zipped archive. + out_dir (:obj:`str`): path to store the outputs of the archive (unzipped). + config (:obj:`Config`): BioSimulators Utils common configuration. Here, you may define the `LOG` and `REPORTS` + `_PATH`. + + Returns: + :obj:`tuple`: + + * :obj:`SedDocumentResults`: results + * :obj:`CombineArchiveLog`: log + """ + config = config or get_config() + return exec_sedml_docs_in_archive( + exec_sed_doc, + archive_filename, + out_dir, + apply_xml_model_changes=False, + config=config + ) From 487a5c2d46da7efc39675f1fd7e1130ffb927369 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 16:40:46 -0400 Subject: [PATCH 18/34] minor reformat --- biosimulators_utils/spatial/exec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py index 7a6f9ae7..7720bbfe 100644 --- a/biosimulators_utils/spatial/exec.py +++ b/biosimulators_utils/spatial/exec.py @@ -48,3 +48,5 @@ def exec_combine_archive(archive_filename: str, apply_xml_model_changes=False, config=config ) + + From 11b6099b66aa3c1bac79f41e6c2e685ae90bacf3 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 16:55:06 -0400 Subject: [PATCH 19/34] chore: updated docstrings --- biosimulators_utils/combine/exec.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index c455aa40..74106993 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -48,7 +48,8 @@ def exec_sedml_docs_in_archive(sed_doc_executer, sed_doc_executer_logged_features=(Task, Report, DataSet, Plot2D, Curve, Plot3D, Surface), log_level=StandardOutputErrorCapturerLevel.c, config: Optional[Config] = None): - """ Execute the SED-ML files in a COMBINE/OMEX archive (execute tasks and save outputs) + """ Execute the SED-ML files in a COMBINE/OMEX archive (execute tasks and save outputs). If 'smoldyn' is detected + in the archive, a simularium file will automatically be generated. Args: sed_doc_executer (:obj:`types.FunctionType`): function to execute each SED document in the archive. From 08862ce27bec98aafad9646bbc2370646f66e8f5 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 16:55:33 -0400 Subject: [PATCH 20/34] feat: copied code from smoldyn.biosimulators methods --- biosimulators_utils/spatial/data_model.py | 163 ++++ biosimulators_utils/spatial/exec.py | 970 +++++++++++++++++++++- 2 files changed, 1103 insertions(+), 30 deletions(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index ce95c0c3..03159aa2 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -15,6 +15,8 @@ # pragma: no cover +import enum +import types # noqa: F401 import os from dataclasses import dataclass from warnings import warn @@ -42,6 +44,7 @@ from biosimulators_utils.combine.io import CombineArchiveReader, CombineArchiveWriter from biosimulators_utils.archive.io import ArchiveReader, ArchiveWriter from biosimulators_utils.model_lang.smoldyn.validation import validate_model +from biosimulators_utils.data_model import ValueType __all__ = [ @@ -50,6 +53,15 @@ 'SmoldynCombineArchive', 'BiosimulatorsDataConverter', 'SmoldynDataConverter', + 'Simulation', + 'SimulationInstruction', + 'SmoldynCommand', + 'SmoldynOutputFile', + 'SimulationChange', + 'SimulationChangeExecution', + 'AlgorithmParameterType', + 'KISAO_ALGORITHMS_MAP', + 'KISAO_ALGORITHM_PARAMETERS_MAP', ] @@ -794,3 +806,154 @@ def generate_simularium_file( paths = list(self.archive.get_all_archive_filepaths().values()) writer.run(paths, self.archive.rootpath) print('Omex bundled!') + + +"""*Copied Objects from smoldyn.biosimulators.data_model*""" + + +class Simulation(object): + """ Configuration of a simulation + + Attributes: + species (:obj:`list` of :obj:`str`): names of species + compartments (:obj:`list` of :obj:`str`): names of compartments + surfaces (:obj:`list` of :obj:`str`): names of surfaces + instructions (:obj:`list` of :obj:`SimulationInstruction`): instructions + """ + + def __init__(self, species=None, compartments=None, surfaces=None, instructions=None): + """ + Args: + species (:obj:`list` of :obj:`str`, optional): names of species + compartments (:obj:`list` of :obj:`str`, optional): names of compartments + surfaces (:obj:`list` of :obj:`str`, optional): names of surfaces + instructions (:obj:`list` of :obj:`SimulationInstruction`, optional): instructions + """ + self.species = species or [] + self.compartments = compartments or [] + self.surfaces = surfaces or [] + self.instructions = instructions or [] + + +class SimulationInstruction(object): + """ Configuration of a simulation + + Attributes: + macro (:obj:`str`): Smoldyn macro + arguments (:obj:`str`): arguments of the Smoldyn macro + id (:obj:`str`): unique id of the instruction + description (:obj:`str`): human-readable description of the instruction + comment (:obj:`str`): comment about the instruction + """ + + def __init__(self, macro, arguments, id=None, description=None, comment=None): + """ + Args: + macro (:obj:`str`): Smoldyn macro + arguments (:obj:`str`): arguments of the Smoldyn macro + id (:obj:`str`, optional): unique id of the instruction + description (:obj:`str`, optional): human-readable description of the instruction + comment (:obj:`str`, optional): comment about the instruction + """ + self.macro = macro + self.arguments = arguments + self.id = id + self.description = description + self.comment = comment + + def is_equal(self, other): + return (self.__class__ == other.__class__ + and self.macro == other.macro + and self.arguments == other.arguments + and self.id == other.id + and self.description == other.description + and self.comment == other.comment + ) + + +class SmoldynCommand(object): + ''' A Smoldyn command + + Attributes: + command (:obj:`str`): command (e.g., ``molcount``) + type (:obj:`str`): command type (e.g., ``E``) + ''' + + def __init__(self, command, type): + ''' + Args: + command (:obj:`str`): command (e.g., ``molcount``) + type (:obj:`str`): command type (e.g., ``E``) + ''' + self.command = command + self.type = type + + +class SmoldynOutputFile(object): + ''' A Smoldyn output file + + Attributes: + name (:obj:`str`): name + filename (:obj:`str`): path to the file + ''' + + def __init__(self, name, filename): + ''' + Args: + name (:obj:`str`): name + filename (:obj:`str`): path to the file + ''' + self.name = name + self.filename = filename + + +class SimulationChange(object): + ''' A change to a Smoldyn simulation + + Attributes: + command (:obj:`types.FunctionType`): function for generating a Smoldyn configuration command for a new value + execution (:obj:`SimulationChangeExecution`): operation when change should be executed + ''' + + def __init__(self, command, execution): + ''' + Args: + command (:obj:`types.FunctionType`): function for generating a Smoldyn configuration command for a new value + execution (:obj:`SimulationChangeExecution`): operation when change should be executed + ''' + self.command = command + self.execution = execution + + +class SimulationChangeExecution(str, enum.Enum): + """ Operation when a simulation change should be executed """ + preprocessing = 'preprocessing' + simulation = 'simulation' + + +class AlgorithmParameterType(str, enum.Enum): + ''' Type of algorithm parameter ''' + run_argument = 'run_argument' + instance_attribute = 'instance_attribute' + + +KISAO_ALGORITHMS_MAP = { + 'KISAO_0000057': { + 'name': 'Brownian diffusion Smoluchowski method', + } +} + +KISAO_ALGORITHM_PARAMETERS_MAP = { + 'KISAO_0000254': { + 'name': 'accuracy', + 'type': AlgorithmParameterType.run_argument, + 'data_type': ValueType.float, + 'default': 10., + }, + 'KISAO_0000488': { + 'name': 'setRandomSeed', + 'type': AlgorithmParameterType.instance_attribute, + 'data_type': ValueType.integer, + 'default': None, + }, +} diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py index 7720bbfe..c423f33e 100644 --- a/biosimulators_utils/spatial/exec.py +++ b/biosimulators_utils/spatial/exec.py @@ -1,52 +1,962 @@ """ Exec methods for executing spatial tasks in SED-ML files in COMBINE/OMEX archives -:Author: Alexander Patrie +:Author: Alexander Patrie / Jonathan Karr :Date: 2023-09-16 :Copyright: 2023, UConn Health :License: MIT """ -# pragma: no cover -# import os -from typing import Optional, Tuple -from biosimulators_utils.log.data_model import CombineArchiveLog -from biosimulators_utils.report.data_model import SedDocumentResults + +import functools +import os +import numpy +import pandas +import re +import tempfile +import types # noqa: F401 +from biosimulators_utils.spatial.data_model import ( + SmoldynCommand, + SmoldynOutputFile, + SimulationChange, + SimulationChangeExecution, + AlgorithmParameterType, + KISAO_ALGORITHMS_MAP, + KISAO_ALGORITHM_PARAMETERS_MAP +) +from smoldyn import smoldyn from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive -from biosimulators_utils.config import Config, get_config +from biosimulators_utils.config import get_config, Config # noqa: F401 +from biosimulators_utils.log.data_model import CombineArchiveLog, TaskLog, StandardOutputErrorCapturerLevel # noqa: F401 +from biosimulators_utils.viz.data_model import VizFormat # noqa: F401 +from biosimulators_utils.report.data_model import ReportFormat, VariableResults, SedDocumentResults # noqa: F401 +from biosimulators_utils.sedml import validation +from biosimulators_utils.sedml.data_model import (Task, ModelLanguage, ModelAttributeChange, # noqa: F401 + UniformTimeCourseSimulation, AlgorithmParameterChange, Variable, + Symbol) +from biosimulators_utils.sedml.exec import exec_sed_doc as base_exec_sed_doc +from biosimulators_utils.utils.core import validate_str_value, parse_value, raise_errors_warnings + + +__all__ = ['exec_sedml_docs_in_combine_archive', 'exec_sed_task', 'exec_sed_doc', 'preprocess_sed_task'] -try: - from smoldyn.biosimulators.combine import exec_sed_doc -except ImportError: - raise ImportError('The Smoldyn-Python package must be installed to use this feature.') +def exec_sedml_docs_in_combine_archive(archive_filename, out_dir, config=None): + ''' Execute the SED tasks defined in a COMBINE/OMEX archive and save the outputs + Args: + archive_filename (:obj:`str`): path to COMBINE/OMEX archive + out_dir (:obj:`str`): path to store the outputs of the archive -def exec_combine_archive(archive_filename: str, - out_dir: str, - config: Optional[Config] = None) -> Tuple[SedDocumentResults, CombineArchiveLog]: - """Execute the SED tasks defined in a COMBINE/OMEX archive whose contents are spatial/spatiotemporal in nature. - Library-level wrapper method which imports smoldyn.biosimulators.combine and - calls `biosimulators_utils.combine.exec_sedml_docs_in_combine_archive`. Also outputs a simularium file. + * CSV: directory in which to save outputs to files + ``{ out_dir }/{ relative-path-to-SED-ML-file-within-archive }/{ report.id }.csv`` + * HDF5: directory in which to save a single HDF5 file (``{ out_dir }/reports.h5``), + with reports at keys ``{ relative-path-to-SED-ML-file-within-archive }/{ report.id }`` within the HDF5 file - Args: - archive_filename (:obj:`str`): path to the COMBINE/OMEX archive. Must be the zipped archive. - out_dir (:obj:`str`): path to store the outputs of the archive (unzipped). - config (:obj:`Config`): BioSimulators Utils common configuration. Here, you may define the `LOG` and `REPORTS` - `_PATH`. + config (:obj:`Config`, optional): BioSimulators common configuration - Returns: - :obj:`tuple`: + Returns: + :obj:`tuple`: * :obj:`SedDocumentResults`: results * :obj:`CombineArchiveLog`: log + ''' + return exec_sedml_docs_in_archive(exec_sed_doc, archive_filename, out_dir, + apply_xml_model_changes=False, + config=config) + + +def exec_sed_doc(doc, working_dir, base_out_path, rel_out_path=None, + apply_xml_model_changes=True, + log=None, indent=0, pretty_print_modified_xml_models=False, + log_level=StandardOutputErrorCapturerLevel.c, config=None): + """ Execute the tasks specified in a SED document and generate the specified outputs + + Args: + doc (:obj:`SedDocument` or :obj:`str`): SED document or a path to SED-ML file which defines a SED document + working_dir (:obj:`str`): working directory of the SED document (path relative to which models are located) + + base_out_path (:obj:`str`): path to store the outputs + + * CSV: directory in which to save outputs to files + ``{base_out_path}/{rel_out_path}/{report.id}.csv`` + * HDF5: directory in which to save a single HDF5 file (``{base_out_path}/reports.h5``), + with reports at keys ``{rel_out_path}/{report.id}`` within the HDF5 file + + rel_out_path (:obj:`str`, optional): path relative to :obj:`base_out_path` to store the outputs + apply_xml_model_changes (:obj:`bool`, optional): if :obj:`True`, apply any model changes specified in the SED-ML file before + calling :obj:`task_executer`. + log (:obj:`SedDocumentLog`, optional): log of the document + indent (:obj:`int`, optional): degree to indent status messages + pretty_print_modified_xml_models (:obj:`bool`, optional): if :obj:`True`, pretty print modified XML models + log_level (:obj:`StandardOutputErrorCapturerLevel`, optional): level at which to log output + config (:obj:`Config`, optional): BioSimulators common configuration + simulator_config (:obj:`SimulatorConfig`, optional): tellurium configuration + + Returns: + :obj:`tuple`: + + * :obj:`ReportResults`: results of each report + * :obj:`SedDocumentLog`: log of the document + """ + return base_exec_sed_doc(exec_sed_task, doc, working_dir, base_out_path, + rel_out_path=rel_out_path, + apply_xml_model_changes=apply_xml_model_changes, + log=log, + indent=indent, + pretty_print_modified_xml_models=pretty_print_modified_xml_models, + log_level=log_level, + config=config) + + +def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None): + ''' Execute a task and save its results + + Args: + task (:obj:`Task`): task + variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + preprocessed_task (:obj:`dict`, optional): preprocessed information about the task, including possible + model changes and variables. This can be used to avoid repeatedly executing the same initialization + for repeated calls to this method. + log (:obj:`TaskLog`, optional): log for the task + config (:obj:`Config`, optional): BioSimulators common configuration + + Returns: + :obj:`tuple`: + + :obj:`VariableResults`: results of variables + :obj:`TaskLog`: log + ''' + if not config: + config = get_config() + + if config.LOG and not log: + log = TaskLog() + + sed_model_changes = task.model.changes + sed_simulation = task.simulation + + if preprocessed_task is None: + preprocessed_task = preprocess_sed_task(task, variables, config=config) + sed_model_changes = list(filter(lambda change: change.target in preprocessed_task['sed_smoldyn_simulation_change_map'], + sed_model_changes)) + + # read Smoldyn configuration + smoldyn_simulation = preprocessed_task['simulation'] + + # apply model changes to the Smoldyn configuration + sed_smoldyn_simulation_change_map = preprocessed_task['sed_smoldyn_simulation_change_map'] + for change in sed_model_changes: + smoldyn_change = sed_smoldyn_simulation_change_map.get(change.target, None) + if smoldyn_change is None or smoldyn_change.execution != SimulationChangeExecution.simulation: + raise NotImplementedError('Target `{}` can only be changed during simulation preprocessing.'.format(change.target)) + apply_change_to_smoldyn_simulation( + smoldyn_simulation, change, smoldyn_change) + + # get the Smoldyn representation of the SED uniform time course simulation + smoldyn_simulation_run_timecourse_args = get_smoldyn_run_timecourse_args(sed_simulation) + + # execute the simulation + smoldyn_run_args = dict( + **smoldyn_simulation_run_timecourse_args, + **preprocessed_task['simulation_run_alg_param_args'], + ) + smoldyn_simulation.run(**smoldyn_run_args, overwrite=True, display=False, quit_at_end=False) + + # get the result of each SED variable + variable_output_cmd_map = preprocessed_task['variable_output_cmd_map'] + smoldyn_output_files = preprocessed_task['output_files'] + variable_results = get_variable_results(sed_simulation.number_of_steps, variables, variable_output_cmd_map, smoldyn_output_files) + + # cleanup output files + for smoldyn_output_file in smoldyn_output_files.values(): + os.remove(smoldyn_output_file.filename) + + # log simulation + if config.LOG: + log.algorithm = sed_simulation.algorithm.kisao_id + log.simulator_details = { + 'class': 'smoldyn.Simulation', + 'instanceAttributes': preprocessed_task['simulation_attrs'], + 'method': 'run', + 'methodArguments': smoldyn_run_args, + } + + # return the values of the variables and log + return variable_results, log + + +def preprocess_sed_task(task, variables, config=None): + """ Preprocess a SED task, including its possible model changes and variables. This is useful for avoiding + repeatedly initializing tasks on repeated calls of :obj:`exec_sed_task`. + + Args: + task (:obj:`Task`): task + variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + config (:obj:`Config`, optional): BioSimulators common configuration + + Returns: + :obj:`dict`: preprocessed information about the task """ config = config or get_config() - return exec_sedml_docs_in_archive( - exec_sed_doc, - archive_filename, - out_dir, - apply_xml_model_changes=False, - config=config + + sed_model = task.model + sed_simulation = task.simulation + + if config.VALIDATE_SEDML: + raise_errors_warnings(validation.validate_task(task), + error_summary='Task `{}` is invalid.'.format(task.id)) + raise_errors_warnings(validation.validate_model_language(sed_model.language, ModelLanguage.Smoldyn), + error_summary='Language for model `{}` is not supported.'.format(sed_model.id)) + raise_errors_warnings(validation.validate_model_change_types(sed_model.changes, (ModelAttributeChange, )), + error_summary='Changes for model `{}` are not supported.'.format(sed_model.id)) + raise_errors_warnings(*validation.validate_model_changes(sed_model), + error_summary='Changes for model `{}` are invalid.'.format(sed_model.id)) + raise_errors_warnings(validation.validate_simulation_type(sed_simulation, (UniformTimeCourseSimulation, )), + error_summary='{} `{}` is not supported.'.format(sed_simulation.__class__.__name__, sed_simulation.id)) + raise_errors_warnings(*validation.validate_simulation(sed_simulation), + error_summary='Simulation `{}` is invalid.'.format(sed_simulation.id)) + raise_errors_warnings(*validation.validate_data_generator_variables(variables), + error_summary='Data generator variables for task `{}` are invalid.'.format(task.id)) + + if sed_simulation.algorithm.kisao_id not in KISAO_ALGORITHMS_MAP: + msg = 'Algorithm `{}` is not supported. The following algorithms are supported:{}'.format( + sed_simulation.algorithm.kisao_id, + ''.join('\n {}: {}'.format(kisao_id, alg_props['name']) for kisao_id, alg_props in KISAO_ALGORITHMS_MAP.items()) + ) + raise NotImplementedError(msg) + + # read Smoldyn configuration + simulation_configuration = read_smoldyn_simulation_configuration(sed_model.source) + normalize_smoldyn_simulation_configuration(simulation_configuration) + + # turn off Smoldyn's graphics + disable_smoldyn_graphics_in_simulation_configuration(simulation_configuration) + + # preprocess model changes + sed_smoldyn_preprocessing_change_map = {} + sed_smoldyn_simulation_change_map = {} + for change in sed_model.changes: + smoldyn_change = validate_model_change(change) + + if smoldyn_change.execution == SimulationChangeExecution.preprocessing: + sed_smoldyn_preprocessing_change_map[change] = smoldyn_change + else: + sed_smoldyn_simulation_change_map[change.target] = smoldyn_change + + # apply preprocessing-time changes + for change, smoldyn_change in sed_smoldyn_preprocessing_change_map.items(): + apply_change_to_smoldyn_simulation_configuration( + simulation_configuration, change, smoldyn_change) + + # write the modified Smoldyn configuration to a temporary file + fid, smoldyn_configuration_filename = tempfile.mkstemp(suffix='.txt') + os.close(fid) + write_smoldyn_simulation_configuration(simulation_configuration, smoldyn_configuration_filename) + + # initialize a simulation from the Smoldyn file + smoldyn_simulation = init_smoldyn_simulation_from_configuration_file(smoldyn_configuration_filename) + + # clean up temporary file + os.remove(smoldyn_configuration_filename) + + # apply the SED algorithm parameters to the Smoldyn simulation and to the arguments to its ``run`` method + smoldyn_simulation_attrs = {} + smoldyn_simulation_run_alg_param_args = {} + for sed_algorithm_parameter_change in sed_simulation.algorithm.changes: + val = get_smoldyn_instance_attr_or_run_algorithm_parameter_arg(sed_algorithm_parameter_change) + if val['type'] == AlgorithmParameterType.run_argument: + smoldyn_simulation_run_alg_param_args[val['name']] = val['value'] + else: + smoldyn_simulation_attrs[val['name']] = val['value'] + + # apply the SED algorithm parameters to the Smoldyn simulation and to the arguments to its ``run`` method + for attr_name, value in smoldyn_simulation_attrs.items(): + setter = getattr(smoldyn_simulation, attr_name) + setter(value) + + # validate SED variables + variable_output_cmd_map = validate_variables(variables) + + # Setup Smoldyn output files for the SED variables + smoldyn_configuration_dirname = os.path.dirname(smoldyn_configuration_filename) + smoldyn_output_files = add_smoldyn_output_files_for_sed_variables( + smoldyn_configuration_dirname, variables, variable_output_cmd_map, smoldyn_simulation) + + # return preprocessed information + return { + 'simulation': smoldyn_simulation, + 'simulation_attrs': smoldyn_simulation_attrs, + 'simulation_run_alg_param_args': smoldyn_simulation_run_alg_param_args, + 'sed_smoldyn_simulation_change_map': sed_smoldyn_simulation_change_map, + 'variable_output_cmd_map': variable_output_cmd_map, + 'output_files': smoldyn_output_files, + } + + +def init_smoldyn_simulation_from_configuration_file(filename): + ''' Initialize a simulation for a Smoldyn model from a file + + Args: + filename (:obj:`str`): path to model file + + Returns: + :obj:`smoldyn.Simulation`: simulation + ''' + if not os.path.isfile(filename): + raise FileNotFoundError('Model source `{}` is not a file.'.format(filename)) + + smoldyn_simulation = smoldyn.Simulation.fromFile(filename) + if not smoldyn_simulation.getSimPtr(): + error_code, error_msg = smoldyn.getError() + msg = 'Model source `{}` is not a valid Smoldyn file.\n\n {}: {}'.format( + filename, error_code.name[0].upper() + error_code.name[1:], error_msg.replace('\n', '\n ')) + raise ValueError(msg) + + return smoldyn_simulation + + +def read_smoldyn_simulation_configuration(filename): + ''' Read a configuration for a Smoldyn simulation + + Args: + filename (:obj:`str`): path to model file + + Returns: + :obj:`list` of :obj:`str`: simulation configuration + ''' + with open(filename, 'r') as file: + return [line.strip('\n') for line in file] + + +def write_smoldyn_simulation_configuration(configuration, filename): + ''' Write a configuration for Smoldyn simulation to a file + + Args: + configuration + filename (:obj:`str`): path to save configuration + ''' + with open(filename, 'w') as file: + for line in configuration: + file.write(line) + file.write('\n') + + +def normalize_smoldyn_simulation_configuration(configuration): + ''' Normalize a configuration for a Smoldyn simulation + + Args: + configuration (:obj:`list` of :obj:`str`): configuration for a Smoldyn simulation + ''' + # normalize spacing and comments + for i_line, line in enumerate(configuration): + if '#' in line: + cmd, comment = re.split('#+', line, maxsplit=1) + cmd = re.sub(' +', ' ', cmd).strip() + comment = comment.strip() + + if cmd: + if comment: + line = cmd + ' # ' + comment + else: + line = cmd + else: + if comment: + line = '# ' + comment + else: + line = '' + + else: + line = re.sub(' +', ' ', line).strip() + + configuration[i_line] = line + + # remove end_file and following lines + for i_line, line in enumerate(configuration): + if re.match(r'^end_file( |$)', line): + for i_line_remove in range(len(configuration) - i_line): + configuration.pop() + break + + # remove empty starting lines + for line in list(configuration): + if not line: + configuration.pop(0) + else: + break + + # remove empty ending lines + for line in reversed(configuration): + if not line: + configuration.pop() + else: + break + + +def disable_smoldyn_graphics_in_simulation_configuration(configuration): + ''' Turn off graphics in the configuration of a Smoldyn simulation + + Args: + configuration (:obj:`list` of :obj:`str`): simulation configuration + ''' + for i_line, line in enumerate(configuration): + if line.startswith('graphics '): + configuration[i_line] = re.sub(r'^graphics +[a-z_]+', 'graphics none', line) + + +def validate_model_change(sed_model_change): + ''' Validate a SED model attribute change to a configuration for a Smoldyn simulation + + ==================================================================== =================== + target newValue + ==================================================================== =================== + ``define {name}`` float + ``difc {species}`` float + ``difc {species}({state})`` float + ``difc_rule {species}({state})`` float + ``difm {species}`` float[] + ``difm {species}({state})`` float[] + ``difm_rule {species}({state})`` float[] + ``drift {species}`` float[] + ``drift {species}({state})`` float[] + ``drift_rule {species}({state})`` float[] + ``surface_drift {species}({state}) {surface} {panel-shape}`` float[] + ``surface_drift_rule {species}({state}) {surface} {panel-shape}`` float[] + ``killmol {species}({state})`` 0 + ``killmolprob {species}({state}) {prob}`` 0 + ``killmolincmpt {species}({state}) {compartment}`` 0 + ``killmolinsphere {species}({state}) {surface}`` 0 + ``killmoloutsidesystem {species}({state})`` 0 + ``fixmolcount {species}({state})`` integer + ``fixmolcountincmpt {species}({staet}) {compartment}`` integer + ``fixmolcountonsurf {species}({state}) {surface}`` integer + ==================================================================== =================== + + Args: + sed_model_change (:obj:`ModelAttributeChange`): SED model change + + Returns: + :obj:`SimulationChange`: Smoldyn representation of the model change + + Raises: + :obj:`NotImplementedError`: unsupported model change + ''' + # TODO: support additional types of model changes + + target_type, _, target = sed_model_change.target.strip().partition(' ') + target_type = target_type.strip() + target = re.sub(r' +', ' ', target).strip() + + if target_type in [ + 'killmol', 'killmolprob', 'killmolinsphere', 'killmolincmpt', 'killmoloutsidesystem', + ]: + # Examples: + # killmol red + def new_line_func(new_value): + return target_type + ' ' + target + execution = SimulationChangeExecution.simulation + + elif target_type in [ + 'fixmolcount', 'fixmolcountonsurf', 'fixmolcountincmpt', + ]: + # Examples: + # fixmolcount red + species_name, species_target_sep, target = target.partition(' ') + + def new_line_func(new_value): + return target_type + ' ' + species_name + ' ' + new_value + species_target_sep + target + + execution = SimulationChangeExecution.simulation + + elif target_type in [ + 'define', 'difc', 'difc_rule', 'difm', 'difm_rule', + 'drift', 'drift_rule', 'surface_drift', 'surface_drift_rule', + ]: + # Examples: + # define K_FWD 0.001 + # difc S 3 + # difm red 1 0 0 0 0 0 0 0 2 + def new_line_func(new_value): + return target_type + ' ' + target + ' ' + new_value + + execution = SimulationChangeExecution.preprocessing + + # elif target_type in [ + # 'mol', 'compartment_mol', 'surface_mol', + # ]: + # # Examples: + # # mol 5 red u + # # compartment_mol 500 S inside + # # surface_mol 100 E(front) membrane all all + # def new_line_func(new_value): + # return target_type + ' ' + new_value + ' ' + target + # + # execution = SimulationChangeExecution.preprocessing + + # elif target_type in [ + # 'dim', + # ]: + # # Examples: + # # dim 1 + # def new_line_func(new_value): + # return target_type + ' ' + new_value + # + # execution = SimulationChangeExecution.preprocessing + + # elif target_type in [ + # 'boundaries', 'low_wall', 'high_wall', + # ]: + # # Examples: + # # low_wall x -10 + # # high_wall y 10 + # def new_line_func(new_value): + # return target_type + ' ' + target + ' ' + new_value + # + # execution = SimulationChangeExecution.preprocessing + + else: + raise NotImplementedError('Target `{}` is not supported.'.format(sed_model_change.target)) + + return SimulationChange(new_line_func, execution) + + +def apply_change_to_smoldyn_simulation(smoldyn_simulation, sed_change, smoldyn_change): + ''' Apply a SED model attribute change to a Smoldyn simulation + + Args: + smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation + sed_change (:obj:`ModelAttributeChange`): SED model change + smoldyn_change (:obj:`SimulationChange`): Smoldyn representation of the model change + ''' + new_value = str(sed_change.new_value).strip() + new_line = smoldyn_change.command(new_value) + smoldyn_simulation.addCommand(new_line, 'b') + + +def apply_change_to_smoldyn_simulation_configuration(smoldyn_simulation_configuration, sed_change, smoldyn_change): + ''' Apply a SED model attribute change to a configuration for a Smoldyn simulation + + Args: + smoldyn_simulation_configuration (:obj:`list` of :obj:`str`): configuration for the Smoldyn simulation + sed_change (:obj:`ModelAttributeChange`): SED model change + smoldyn_change (:obj:`SimulationChange`): Smoldyn representation of the model change + ''' + new_value = str(sed_change.new_value).strip() + new_line = smoldyn_change.command(new_value) + smoldyn_simulation_configuration.insert(0, new_line) + + +def get_smoldyn_run_timecourse_args(sed_simulation): + ''' Get the Smoldyn representation of a SED uniform time course simulation + + Args: + sed_simulation (:obj:`UniformTimeCourseSimulation`): SED uniform time course simulation + + Returns: + :obj:`dict`: dictionary with keys ``start``, ``stop``, and ``dt`` that captures the Smoldyn + representation of the time course + + Raises: + :obj:`NotImplementedError`: unsupported timecourse + ''' + number_of_steps = ( + ( + sed_simulation.output_end_time - sed_simulation.initial_time + ) / ( + sed_simulation.output_end_time - sed_simulation.output_start_time + ) * ( + sed_simulation.number_of_steps + ) ) + if (number_of_steps - int(number_of_steps)) > 1e-8: + msg = ( + 'Simulations must specify an integer number of steps, not {}.' + '\n Initial time: {}' + '\n Output start time: {}' + '\n Output end time: {}' + '\n Number of steps (output start to end time): {}' + ).format( + number_of_steps, + sed_simulation.initial_time, + sed_simulation.output_start_time, + sed_simulation.output_end_time, + sed_simulation.number_of_steps, + ) + raise NotImplementedError(msg) + + dt = (sed_simulation.output_end_time - sed_simulation.output_start_time) / sed_simulation.number_of_steps + + return { + 'start': sed_simulation.initial_time, + 'stop': sed_simulation.output_end_time, + 'dt': dt, + } + + +def get_smoldyn_instance_attr_or_run_algorithm_parameter_arg(sed_algorithm_parameter_change): + ''' Get the Smoldyn representation of a SED uniform time course simulation + + Args: + sed_algorithm_parameter_change (:obj:`AlgorithmParameterChange`): SED change to a parameter of an algorithm + + Returns: + :obj:`dict`: dictionary with keys ``type``, ``name``, and ``value`` that captures the Smoldyn representation + of the algorithm parameter + + Raises: + :obj:`ValueError`: unsupported algorithm parameter value + :obj:`NotImplementedError`: unsupported algorithm parameter + ''' + parameter_props = KISAO_ALGORITHM_PARAMETERS_MAP.get(sed_algorithm_parameter_change.kisao_id, None) + if parameter_props: + if not validate_str_value(sed_algorithm_parameter_change.new_value, parameter_props['data_type']): + msg = '{} ({}) must be a {}, not `{}`.'.format( + parameter_props['name'], sed_algorithm_parameter_change.kisao_id, + parameter_props['data_type'].name, sed_algorithm_parameter_change.new_value, + ) + raise ValueError(msg) + new_value = parse_value(sed_algorithm_parameter_change.new_value, parameter_props['data_type']) + + return { + 'type': parameter_props['type'], + 'name': parameter_props['name'], + 'value': new_value, + } + + else: + msg = 'Algorithm parameter `{}` is not supported. The following parameters are supported:{}'.format( + sed_algorithm_parameter_change.kisao_id, + ''.join('\n {}: {}'.format(kisao_id, parameter_props['name']) + for kisao_id, parameter_props in KISAO_ALGORITHM_PARAMETERS_MAP.items()) + ) + raise NotImplementedError(msg) + + +def add_smoldyn_output_file(configuration_dirname, smoldyn_simulation): + ''' Add an output file to a Smoldyn simulation + + Args: + configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file for the simulation + smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation + + Returns: + :obj:`SmoldynOutputFile`: output file + ''' + fid, filename = tempfile.mkstemp(dir=configuration_dirname, suffix='.ssv') + os.close(fid) + name = os.path.relpath(filename, configuration_dirname) + smoldyn_simulation.setOutputFile(name, append=False) + smoldyn_simulation.setOutputPath('./') + return SmoldynOutputFile(name=name, filename=filename) + + +def add_commands_to_smoldyn_output_file(simulation, output_file, commands): + ''' Add commands to a Smoldyn output file + + Args: + smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation + smoldyn_output_file (:obj:`SmoldynOutputFile`): Smoldyn output file + commands (:obj:`list` of :obj`SmoldynCommand`): Smoldyn commands + ''' + for command in commands: + simulation.addCommand(command.command + ' ' + output_file.name, command.type) + + +def validate_variables(variables): + ''' Validate SED variables + + ============================================================================================================================================= =========================================================================================================================================== =========================================== + Smoldyn output file SED variable target Shape + ============================================================================================================================================= =========================================================================================================================================== =========================================== + ``molcount`` ``molcount {species}`` (numberOfSteps + 1,) + ``molcountspecies {species}({state})`` ``molcountspecies {species}({state})`` (numberOfSteps + 1,) + ``molcountspecieslist {species}({state})+`` ``molcountspecies {species}({state})`` (numberOfSteps + 1,) + ``molcountinbox {low-x} {hi-x}`` ``molcountinbox {species} {low-x} {hi-x}`` (numberOfSteps + 1,) + ``molcountinbox {low-x} {hi-x} {low-y} {hi-y}`` ``molcountinbox {species} {low-x} {hi-x} {low-y} {hi-y}`` (numberOfSteps + 1,) + ``molcountinbox {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z}`` ``molcountinbox {species} {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z}`` (numberOfSteps + 1,) + ``molcountincmpt {compartment}`` ``molcountincmpt {species} {compartment}`` (numberOfSteps + 1,) + ``molcountincmpts {compartment}+`` ``molcountincmpt {species} {compartment}`` (numberOfSteps + 1,) + ``molcountincmpt2 {compartment} {state}`` ``molcountincmpt2 {species} {compartment} {state}`` (numberOfSteps + 1,) + ``molcountonsurf {surface}`` ``molcountonsurf {species} {surface}`` (numberOfSteps + 1,) + ``molcountspace {species}({state}) {axis} {low} {hi} {bins} 0`` ``molcountspace {species}({state}) {axis} {low} {hi} {bins}`` (numberOfSteps + 1, bins) + ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi} 0`` ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi}`` (numberOfSteps + 1, bins) + ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi} {low} {hi} 0`` ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi} {low} {hi}`` (numberOfSteps + 1, bins) + ``molcountspace2d {species}({state}) z {low-x} {hi-x} {bins-x} {low-y} {hi-y} {bins-y} 0`` ``molcountspace2d {species}({state}) z {low-x} {hi-x} {bins-x} {low-y} {hi-y} {bins-y}`` (numberOfSteps + 1, bins-x, bins-y) + ``molcountspace2d {species}({state}) {axis} {low-1} {hi-1} {bins-1} {low-2} {hi-2} {bins-2} {low-3} {hi-3} 0`` ``molcountspace2d {species}({state}) {axis} {low-1} {hi-1} {bins-1} {low-2} {hi-2} {bins-3} {low-3} {hi-3}`` (numberOfSteps + 1, bins-1, bins-2) + ``molcountspaceradial {species}({state}) {center-x} {radius} {bins} 0`` ``molcountspaceradial {species}({state}) {center-x} {radius} {bins}`` (numberOfSteps + 1, bins) + ``molcountspaceradial {species}({state}) {center-x} {center-y} {radius} {bins} 0`` ``molcountspaceradial {species}({state}) {center-x} {center-y} {radius} {bins}`` (numberOfSteps + 1, bins) + ``molcountspaceradial {species}({state}) {center-x} {center-y} {center-z} {radius} {bins} 0`` ``molcountspaceradial {species}({state}) {center-x} {center-y} {center-z} {radius} {bins}`` (numberOfSteps + 1, bins) + ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {pole-x} {pole-y} {radius-min} {radius-max} {bins} 0`` ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {pole-x} {pole-y} {radius-min} {radius-max} {bins}`` (numberOfSteps + 1, bins) + ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {center-z} {pole-x} {pole-y} {pole-z} {radius-min} {radius-max} {bins} 0`` ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {center-z} {pole-x} {pole-y} {pole-z} {radius-min} {radius-max} {bins}`` (numberOfSteps + 1, bins) + ``radialdistribution {species-1}({state-1}) {species-2}({state-2}) {radius} {bins} 0`` ``radialdistribution {species-1}({state-1}) {species-2}({state-2}) {radius} {bins}`` (numberOfSteps + 1, bins) + ``radialdistribution2 {species-1}({state-1}) {species-2}({state-2}) {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z} {radius} {bins} 0`` ``radialdistribution2 {species-1}({state-1}) {species-2}({state-2}) {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z} {radius} {bins}`` (numberOfSteps + 1, bins) + ============================================================================================================================================= ========================================================================================================================================== =========================================== + + Args: + variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + + Returns: + :obj:`dict`: dictionary that maps variable targets and symbols to Smoldyn output commands + ''' + # TODO: support additional kinds of outputs + + variable_output_cmd_map = {} + + invalid_symbols = [] + invalid_targets = [] + + for variable in variables: + if variable.symbol: + if variable.symbol == Symbol.time.value: + output_command_args = 'molcount' + include_header = True + shape = None + results_slicer = functools.partial(results_key_slicer, key='time') + + else: + invalid_symbols.append('{}: {}'.format(variable.id, variable.symbol)) + output_command_args = None + include_header = None + + else: + output_command, _, output_args = re.sub(r' +', ' ', variable.target).partition(' ') + + if output_command in ['molcount', 'molcountinbox', 'molcountincmpt', 'molcountincmpt2', 'molcountonsurf']: + species_name, _, output_args = output_args.partition(' ') + output_command_args = output_command + ' ' + output_args + include_header = True + shape = None + results_slicer = functools.partial(results_key_slicer, key=species_name) + + elif output_command in ['molcountspecies']: + output_command_args = output_command + ' ' + output_args + include_header = False + shape = None + results_slicer = results_array_slicer + + elif output_command in ['molcountspace', 'molcountspaceradial', + 'molcountspacepolarangle', 'radialdistribution', 'radialdistribution2']: + output_command_args = output_command + ' ' + output_args + ' 0' + include_header = False + shape = None + results_slicer = results_matrix_slicer + + elif output_command in ['molcountspace2d']: + output_command_args = output_command + ' ' + output_args + ' 0' + include_header = False + output_args_list = output_args.split(' ') + if len(output_args_list) == 8: + shape = (int(output_args_list[-4]), int(output_args_list[-1])) + else: + shape = (int(output_args_list[-6]), int(output_args_list[-3])) + results_slicer = None + + else: + invalid_targets.append('{}: {}'.format(variable.id, variable.target)) + output_command_args = None + + if output_command_args is not None: + output_command_args = output_command_args.strip() + variable_output_cmd_map[(variable.target, variable.symbol)] = (output_command_args, include_header, shape, results_slicer) + + if invalid_symbols: + msg = '{} symbols cannot be recorded:\n {}\n\nThe following symbols can be recorded:\n {}'.format( + len(invalid_symbols), + '\n '.join(sorted(invalid_symbols)), + '\n '.join(sorted([Symbol.time.value])), + ) + raise ValueError(msg) + + if invalid_targets: + valid_target_output_commands = [ + 'molcount', + + 'molcount', 'molcountinbox', 'molcountincmpt', 'molcountincmpt2', 'molcountonsurf', + + 'molcountspace', 'molcountspaceradial', + 'molcountspacepolarangle', 'radialdistribution', 'radialdistribution2', + + 'molcountspace2d', + ] + + msg = '{} targets cannot be recorded:\n {}\n\nTargets are supported for the following output commands:\n {}'.format( + len(invalid_targets), + '\n '.join(sorted(invalid_targets)), + '\n '.join(sorted(set(valid_target_output_commands))), + ) + raise NotImplementedError(msg) + + return variable_output_cmd_map + + +def results_key_slicer(results, key): + """ Get the results for a key from a set of results + + Args: + results (:obj:`pandas.DataFrame`): set of results + + Returns: + :obj:`pandas.DataFrame`: results for a key + """ + return results.get(key, None) + + +def results_array_slicer(results): + """ Extract an array of results from a matrix of time and results + + Args: + results (:obj:`pandas.DataFrame`): matrix of time and results + + Returns: + :obj:`pandas.DataFrame`: results + """ + return results.iloc[:, 1] + + +def results_matrix_slicer(results): + """ Extract a matrix array of results from a matrix of time and results + + Args: + results (:obj:`pandas.DataFrame`): matrix of time and results + + Returns: + :obj:`pandas.DataFrame`: results + """ + return results.iloc[:, 1:] + + +def add_smoldyn_output_files_for_sed_variables(configuration_dirname, variables, variable_output_cmd_map, smoldyn_simulation): + ''' Add Smoldyn output files for capturing each SED variable + + Args: + configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file for the simulation + variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + variable_output_cmd_map (:obj:`dict`): dictionary that maps variable targets and symbols to Smoldyn output commands + smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation + + Returns: + :obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`: Smoldyn output files + ''' + smoldyn_output_files = {} + + output_cmds = set() + for variable in variables: + output_cmds.add(variable_output_cmd_map[(variable.target, variable.symbol)]) + + for command, include_header, _, _ in output_cmds: + add_smoldyn_output_file_for_output(configuration_dirname, smoldyn_simulation, + command, include_header, + smoldyn_output_files) + # return output files + return smoldyn_output_files + + +def add_smoldyn_output_file_for_output(configuration_dirname, smoldyn_simulation, + smoldyn_output_command, include_header, smoldyn_output_files): + ''' Add a Smoldyn output file for molecule counts + + Args: + configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file for the simulation + smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation + smoldyn_output_command (:obj:`str`): Smoldyn output command (e.g., ``molcount``) + include_header (:obj:`bool`): whether to include a header + smoldyn_output_files (:obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`): Smoldyn output files + ''' + smoldyn_output_files[smoldyn_output_command] = add_smoldyn_output_file(configuration_dirname, smoldyn_simulation) + + commands = [] + if include_header: + commands.append(SmoldynCommand('molcountheader', 'B')) + commands.append(SmoldynCommand(smoldyn_output_command, 'E')) + + add_commands_to_smoldyn_output_file( + smoldyn_simulation, + smoldyn_output_files[smoldyn_output_command], + commands, + ) + + +def get_variable_results(number_of_steps, variables, variable_output_cmd_map, smoldyn_output_files): + ''' Get the result of each SED variable + + Args: + number_of_steps (:obj:`int`): number of steps + variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + variable_output_cmd_map (:obj:`dict`): dictionary that maps variable targets and symbols to Smoldyn output commands + smoldyn_output_files (:obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`): Smoldyn output files + + Returns: + :obj:`VariableResults`: result of each SED variable + + Raises: + :obj:`ValueError`: unsupported results + ''' + smoldyn_results = {} + + missing_variables = [] + + variable_results = VariableResults() + for variable in variables: + output_command_args, _, shape, results_slicer = variable_output_cmd_map[(variable.target, variable.symbol)] + variable_result = get_smoldyn_output(output_command_args, True, shape, smoldyn_output_files, smoldyn_results) + if results_slicer: + variable_result = results_slicer(variable_result) + + if variable_result is None: + missing_variables.append('{}: {}: {}'.format(variable.id, 'target', variable.target)) + else: + if variable_result.ndim == 1: + variable_results[variable.id] = variable_result.to_numpy()[-(number_of_steps + 1):, ] + elif variable_result.ndim == 2: + variable_results[variable.id] = variable_result.to_numpy()[-(number_of_steps + 1):, :] + else: + variable_results[variable.id] = variable_result[-(number_of_steps + 1):, :, :] + + if missing_variables: + msg = '{} variables could not be recorded:\n {}'.format( + len(missing_variables), + '\n '.join(missing_variables), + ) + raise ValueError(msg) + + return variable_results + + +def get_smoldyn_output(smoldyn_output_command, has_header, three_d_shape, smoldyn_output_files, smoldyn_results): + ''' Get the simulated count of each molecule + + Args: + smoldyn_output_command (:obj:`str`): Smoldyn output command (e.g., ``molcount``) + has_header (:obj:`bool`): whether to include a header + three_d_shape (:obj:`tuple` of :obj:`int`): dimensions of the output + smoldyn_output_files (:obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`): Smoldyn output files + smoldyn_results (:obj:`dict`) + + Returns: + :obj:`pandas.DataFrame` or :obj:`numpy.ndarray`: results + ''' + smoldyn_output_command = smoldyn_output_command.strip() + if smoldyn_output_command not in smoldyn_results: + smoldyn_output_file = smoldyn_output_files[smoldyn_output_command] + if three_d_shape: + with open(smoldyn_output_file.filename, 'r') as file: + data_list = [] + + i_line = 0 + for line in file: + if i_line % (three_d_shape[1] + 1) == 0: + time_point_data = [] + else: + profile = [int(el) for el in line.split(' ')] + time_point_data.append(profile) + + if i_line % (three_d_shape[1] + 1) == three_d_shape[1]: + data_list.append(time_point_data) + + i_line += 1 + + smoldyn_results[smoldyn_output_command] = numpy.array(data_list).transpose((0, 2, 1)) + else: + smoldyn_results[smoldyn_output_command] = pandas.read_csv(smoldyn_output_file.filename, sep=' ') + return smoldyn_results[smoldyn_output_command] From 7a0291033df5e8717aaefae537e6d990f5d69e74 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Thu, 5 Oct 2023 17:43:19 -0400 Subject: [PATCH 21/34] update --- biosimulators_utils/spatial/minE.omex | Bin 0 -> 2785 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 biosimulators_utils/spatial/minE.omex diff --git a/biosimulators_utils/spatial/minE.omex b/biosimulators_utils/spatial/minE.omex new file mode 100644 index 0000000000000000000000000000000000000000..c2bdf085bd1a5a5fa8cd2a049273aea0b26bcfdb GIT binary patch literal 2785 zcmZ{mXHXMb8iqq}VL=2hMT#K3mmCfD%9tutAqt>tGj#2B`r6 z05br`*k6rr^ zpu0Dv!i~va_=W8dF{NICUR#}l8pvXGWG_e5z5Ldv3^5ff5x{97Ri7W3t&mdTGfd~g z2_bF`jz&t-ze+b4NLDT{8J5%6#CkDWwya59X#!)#B)fKBARG)|$8HT)?kLdDZs*2- z`rdlpFuoASx2Utf#L4I=4zZ>htGKwmJdMy)&)v;Y$T%P4RChwAdy1a9gnm*PWXAivV$rQso!Wu>UCdH}Ap;zf~UX78` zs42Phxke1CsjBB$5jh(#(bCy(FcdwXSBbWTjW?ZS|dLI6-}? z-yD}cu)qJk1N2Sykz6}vxp?c%E3ZEZ=~gp+PuT8ln5fmE*wfu^h)hFwHbf0-D{xQp zAr!mfQ5sy$x|NdqMHx1x+lu{?tBgeX!lg&q=~*ivArw_$PdO!vi0R@iMIA=nWE}5T zMSW;weUCGv6A(jRF$4s^PTG_%baV?G$hRw@K`*wrcCyoi73bGB-k5~Ne-^lIhO9`= z6q?Df55m60skY>^S`D=)d}W)mvkAGdq{_7FaQjY}XX*`O<&U?^r2I`pcY5-m_{e&i zzAu_Co@%PDQ?uiB_aFi3JvD`w22V}SVG2jJT37A63;6LX9K<%Bkt@t|iC-65LL@?l z`ZD)7W?Bbp-H+V@@`>cewOcUIhd`wuI9s-HUOkpTAevH@s59GAbqmyE-a65tZd&br z=M*RN@Tjffh;-U6u9K`&w@sGw?wjpPlWCrw63#*-tHayNFvGy`O2iAwSANLc=Ga>y zdA4`Tr(d3-*^&IA1FWW@S?pc~+Cj^T+<2#kO_uFTA&&SVDi*sm3o`di!7BEN)~)9z zB~hLn%DwPeL-R?|_i*@}j@qnQN9J{3rOO%Uf7l+fuZ3dj zfqK02VXkAHD0JZNd$dcCb{wbv;o36VGye!vglrN_FV6DLUZ+Q7KzUq=uh=}imXcJ= zm;A(fB#Y|APwtEPViX{Tb3oy8rcGP|5dN4~w)RV#Q^--+-pV&aUX}e|5`jGbftIcH zX1?2*w`t4leL>h?#FKVs<5cB(F;W`z&HDMr>8WzV)K9Kl-how~!q~{$@N3s9VYS=_1zkZ;25;4l7TV>}HDLd=Ah>|xm<9Sb z>OFUl^g5*i0JOvb0M-lY1$+BGgtI8+GO@8KZHUbg1tfE(93BP<{*llz?rbtM2WaHf4t1aFyqCSe!PTaNDC+q*2_i7N z%1ka6#tNVid70Z7*|R%s-R?;tf4V98i<@(fm!VYy1=lSlQpH3IP4MUj!NnAQ>f2CL za4ny!mlzV^DEh=1mL&y#8JJ-K{qC8ZR?v*^e|ONAD>-JCf_% zn;zqgJ`Xt|MJVDms^&`-rI5{(eSpwiH?d2pJHb&?_9_NsN=p4v^P20u$#Ro;hXrXh z$qanQrbra(8>7!@HhnQ3B{A3}#^XRSe$!#4?6rI>(0F3c{UgUr0Cp#2ibx`nwn?pT zr;nRL{YwymfiWT)@;kW-YX@wq&bp-JOF1p0Fj)9Px8B?grsI5IX2}w*njje|IcNP* z;AfX7GFwINE%47fePIYQ1%QQxiTh8BI^CHfw*hy-13AY`C1KJ7NXOX0(wM0uQS^kp zTe$mbOQi;4bSIc_@aEyqN}v0|I#ZgydWN{9dfB*lVK0+{4=Ch3KFDtYMOVM(gH{v! zvGy<71@^05lzB;NtY7T}K>m?x+8y~-&ju*GdzT32`Q^d|nc|{RWq5%(IX8-$wQ#iG zwC5i>bi~UjLNgh>>3jN&EI!QlE^)}^nfRt91RB6V(ml&cc=?J5}mhwt%u%A^!cCVUkZ_v_Uroleg#aC2M)r zbnD)Z3`}uP&=(%Wx(y#B$V!L_o}M7mXX{`3(v^$8$Etsi-+5zD?MQ>eUCN<}#VZ`3 zJ8tRSWfs8@YVbDD4s!J?^}w<_Vq z0)PF`F3dRrM626ghLA5s++_c5Fp*RA!z+bzN_)L6{U~?iTI^@0RX`&Nt8oxl$g;p1 z3}_DcM}o%4o_je_Db`Vrj{E8lRhEWrB2zcV@|~FV%S`+&#KE}x2?ao!xvZ0$qu;KU z>xF@|)m0`uLikVs0Ki{<l5T3;4r0~ZIpc77Gpyq; zXnIttpoY^B5E;3ZZV2&^n2qtTj}}N;kOqeDfMt2*jW(Hix{%T{>poC4oB>=YrOMCW zX{Y$9?KV9@KW8akszNAN`u)>U71w($Qh|usB4ZUUr*&bEQTxwF)uF>Kq^z zBhwFV8(5>2rA!(LicFNLrZgrabH~WRZtBqdNMD9o(t!^sK z^TZBeJy1-Y;&2|B-~r5v7CwJx^Q4RE%qqM7+vlEB)hX5YR8V6IN&w*h+u=n4fQusj zz5e5hzsG*Z>#z6!ijgl=^LN<(p8mb^|E2{l4E29M0BTG_`|FnKVpm Date: Thu, 5 Oct 2023 21:13:57 -0400 Subject: [PATCH 22/34] chore: updated docstrings --- biosimulators_utils/combine/exec.py | 58 ++++++++----- biosimulators_utils/spatial/exec.py | 124 +++++++++++++++++++++------- 2 files changed, 129 insertions(+), 53 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 74106993..19ecb6e9 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -30,7 +30,7 @@ import os import tempfile import shutil -from typing import Optional +from typing import Optional, Tuple from types import FunctionType # noqa: F401 @@ -40,14 +40,16 @@ # noinspection PyIncorrectDocstring -def exec_sedml_docs_in_archive(sed_doc_executer, - archive_filename: str, - out_dir: str, - apply_xml_model_changes=False, - sed_doc_executer_supported_features=(Task, Report, DataSet, Plot2D, Curve, Plot3D, Surface), - sed_doc_executer_logged_features=(Task, Report, DataSet, Plot2D, Curve, Plot3D, Surface), - log_level=StandardOutputErrorCapturerLevel.c, - config: Optional[Config] = None): +def exec_sedml_docs_in_archive( + sed_doc_executer, + archive_filename: str, + out_dir: str, + apply_xml_model_changes=False, + sed_doc_executer_supported_features=(Task, Report, DataSet, Plot2D, Curve, Plot3D, Surface), + sed_doc_executer_logged_features=(Task, Report, DataSet, Plot2D, Curve, Plot3D, Surface), + log_level=StandardOutputErrorCapturerLevel.c, + config: Optional[Config] = None + ) -> Tuple[SedDocumentResults, CombineArchiveLog]: """ Execute the SED-ML files in a COMBINE/OMEX archive (execute tasks and save outputs). If 'smoldyn' is detected in the archive, a simularium file will automatically be generated. @@ -61,8 +63,10 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, ''' Execute the tasks specified in a SED document and generate the specified outputs Args: - doc (:obj:`SedDocument` of :obj:`str`): SED document or a path to SED-ML file which defines a SED document - working_dir (:obj:`str`): working directory of the SED document (path relative to which models are located) + doc (:obj:`SedDocument` of :obj:`str`): SED document or a + path to SED-ML file which defines a SED document + working_dir (:obj:`str`): working directory of the + SED document (path relative to which models are located) out_path (:obj:`str`): path to store the outputs @@ -72,7 +76,8 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, with reports at keys ``{rel_out_path}/{report.id}`` within the HDF5 file rel_out_path (:obj:`str`, optional): path relative to :obj:`out_path` to store the outputs - apply_xml_model_changes (:obj:`bool`, optional): if :obj:`True`, apply any model changes specified in the SED-ML file + apply_xml_model_changes (:obj:`bool`, optional): if :obj:`True`, + apply any model changes specified in the SED-ML file log (:obj:`SedDocumentLog`, optional): execution status of document log_level (:obj:`StandardOutputErrorCapturerLevel`, optional): level at which to log output indent (:obj:`int`, optional): degree to indent status messages @@ -85,14 +90,17 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, * CSV: directory in which to save outputs to files ``{ out_dir }/{ relative-path-to-SED-ML-file-within-archive }/{ report.id }.csv`` * HDF5: directory in which to save a single HDF5 file (``{ out_dir }/reports.h5``), - with reports at keys ``{ relative-path-to-SED-ML-file-within-archive }/{ report.id }`` within the HDF5 file - - apply_xml_model_changes (:obj:`bool`): if :obj:`True`, apply any model changes specified in the SED-ML files before - calling :obj:`task_executer`. - sed_doc_executer_supported_features (:obj:`list` of :obj:`type`, optional): list of the types of elements that the - SED document executer supports. Default: tasks, reports, plots, data sets, curves, and surfaces. - sed_doc_executer_logged_features (:obj:`list` of :obj:`type`, optional): list of the types fo elements which that - the SED document executer logs. Default: tasks, reports, plots, data sets, curves, and surfaces. + with reports at keys ``{ relative-path-to-SED-ML-file-within-archive }/{ report.id }`` + within the HDF5 file + + apply_xml_model_changes (:obj:`bool`): if :obj:`True`, apply any model changes specified in the + SED-ML files before calling :obj:`task_executer`. + sed_doc_executer_supported_features (:obj:`list` of :obj:`type`, optional): list of the types + of elements that the SED document executer supports. Default: tasks, reports, plots, data sets, + curves, and surfaces. + sed_doc_executer_logged_features (:obj:`list` of :obj:`type`, optional): list of the types of elements + which that the SED document executer logs. Default: tasks, reports, + plots, data sets, curves, and surfaces. log_level (:obj:`StandardOutputErrorCapturerLevel`, optional): level at which to log output config (:obj:`Config`): configuration @@ -280,7 +288,10 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # bundle CSV files of reports into zip archive report_formats = config.REPORT_FORMATS - archive_paths = [os.path.join(out_dir, '**', '*.' + format.value) for format in report_formats if format != ReportFormat.h5] + archive_paths = [ + os.path.join(out_dir, '**', '*.' + format.value) + for format in report_formats if format != ReportFormat.h5 + ] archive = build_archive_from_paths(archive_paths, out_dir) if archive.files: ArchiveWriter().run(archive, os.path.join(out_dir, config.REPORTS_PATH)) @@ -299,7 +310,10 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, report_formats = config.REPORT_FORMATS viz_formats = config.VIZ_FORMATS path_patterns = ( - [os.path.join(out_dir, '**', '*.' + format.value) for format in report_formats if format != ReportFormat.h5] + [ + os.path.join(out_dir, '**', '*.' + format.value) + for format in report_formats if format != ReportFormat.h5 + ] + [os.path.join(out_dir, '**', '*.' + format.value) for format in viz_formats] ) for path_pattern in path_patterns: diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py index c423f33e..255a84e2 100644 --- a/biosimulators_utils/spatial/exec.py +++ b/biosimulators_utils/spatial/exec.py @@ -13,6 +13,7 @@ import pandas import re import tempfile +from typing import * import types # noqa: F401 from biosimulators_utils.spatial.data_model import ( SmoldynCommand, @@ -24,15 +25,21 @@ KISAO_ALGORITHM_PARAMETERS_MAP ) from smoldyn import smoldyn -from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive +from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive as base_exec_combine_archive from biosimulators_utils.config import get_config, Config # noqa: F401 -from biosimulators_utils.log.data_model import CombineArchiveLog, TaskLog, StandardOutputErrorCapturerLevel # noqa: F401 +from biosimulators_utils.log.data_model import (CombineArchiveLog, + TaskLog, + StandardOutputErrorCapturerLevel, + SedDocumentLog) # noqa: F401 from biosimulators_utils.viz.data_model import VizFormat # noqa: F401 -from biosimulators_utils.report.data_model import ReportFormat, VariableResults, SedDocumentResults # noqa: F401 +from biosimulators_utils.report.data_model import (ReportFormat, + ReportResults, + VariableResults, + SedDocumentResults) # noqa: F401 from biosimulators_utils.sedml import validation from biosimulators_utils.sedml.data_model import (Task, ModelLanguage, ModelAttributeChange, # noqa: F401 UniformTimeCourseSimulation, AlgorithmParameterChange, Variable, - Symbol) + Symbol, SedDocument) from biosimulators_utils.sedml.exec import exec_sed_doc as base_exec_sed_doc from biosimulators_utils.utils.core import validate_str_value, parse_value, raise_errors_warnings @@ -40,8 +47,12 @@ __all__ = ['exec_sedml_docs_in_combine_archive', 'exec_sed_task', 'exec_sed_doc', 'preprocess_sed_task'] -def exec_sedml_docs_in_combine_archive(archive_filename, out_dir, config=None): - ''' Execute the SED tasks defined in a COMBINE/OMEX archive and save the outputs +def exec_sedml_docs_in_combine_archive( + archive_filename: str, + out_dir: str, + config: Optional[Config] = None + ) -> Tuple[SedDocumentResults, CombineArchiveLog]: + """ Execute the SED tasks defined in a COMBINE/OMEX archive and save the outputs Args: archive_filename (:obj:`str`): path to COMBINE/OMEX archive @@ -59,16 +70,28 @@ def exec_sedml_docs_in_combine_archive(archive_filename, out_dir, config=None): * :obj:`SedDocumentResults`: results * :obj:`CombineArchiveLog`: log - ''' - return exec_sedml_docs_in_archive(exec_sed_doc, archive_filename, out_dir, - apply_xml_model_changes=False, - config=config) + """ + return base_exec_combine_archive( + exec_sed_doc, + archive_filename, + out_dir, + apply_xml_model_changes=False, + config=config + ) -def exec_sed_doc(doc, working_dir, base_out_path, rel_out_path=None, - apply_xml_model_changes=True, - log=None, indent=0, pretty_print_modified_xml_models=False, - log_level=StandardOutputErrorCapturerLevel.c, config=None): +def exec_sed_doc( + doc: Union[SedDocument, str], + working_dir: str, + base_out_path: str, + apply_xml_model_changes=True, + indent=0, + pretty_print_modified_xml_models=False, + log_level=StandardOutputErrorCapturerLevel.c, + log: Optional[SedDocumentLog]=None, + rel_out_path: Optional[str]=None, + config: Optional[Config]=None + ) -> Tuple[ReportResults, SedDocumentLog]: """ Execute the tasks specified in a SED document and generate the specified outputs Args: @@ -83,8 +106,8 @@ def exec_sed_doc(doc, working_dir, base_out_path, rel_out_path=None, with reports at keys ``{rel_out_path}/{report.id}`` within the HDF5 file rel_out_path (:obj:`str`, optional): path relative to :obj:`base_out_path` to store the outputs - apply_xml_model_changes (:obj:`bool`, optional): if :obj:`True`, apply any model changes specified in the SED-ML file before - calling :obj:`task_executer`. + apply_xml_model_changes (:obj:`bool`, optional): if :obj:`True`, apply any model changes specified in + the SED-ML file before calling :obj:`task_executer`. log (:obj:`SedDocumentLog`, optional): log of the document indent (:obj:`int`, optional): degree to indent status messages pretty_print_modified_xml_models (:obj:`bool`, optional): if :obj:`True`, pretty print modified XML models @@ -108,7 +131,14 @@ def exec_sed_doc(doc, working_dir, base_out_path, rel_out_path=None, config=config) -def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None): +# noinspection PyShadowingNames +def exec_sed_task( + task: Task, + variables: List[Variable], + preprocessed_task: Optional[Dict] = None, + log: Optional[TaskLog] = None, + config: Optional[Config] = None + ) -> Tuple[VariableResults, TaskLog]: ''' Execute a task and save its results Args: @@ -137,8 +167,10 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None if preprocessed_task is None: preprocessed_task = preprocess_sed_task(task, variables, config=config) - sed_model_changes = list(filter(lambda change: change.target in preprocessed_task['sed_smoldyn_simulation_change_map'], - sed_model_changes)) + sed_model_changes = list(filter( + lambda change: change.target in preprocessed_task['sed_smoldyn_simulation_change_map'], + sed_model_changes + )) # read Smoldyn configuration smoldyn_simulation = preprocessed_task['simulation'] @@ -148,7 +180,9 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None for change in sed_model_changes: smoldyn_change = sed_smoldyn_simulation_change_map.get(change.target, None) if smoldyn_change is None or smoldyn_change.execution != SimulationChangeExecution.simulation: - raise NotImplementedError('Target `{}` can only be changed during simulation preprocessing.'.format(change.target)) + raise NotImplementedError( + 'Target `{}` can only be changed during simulation preprocessing.'.format(change.target) + ) apply_change_to_smoldyn_simulation( smoldyn_simulation, change, smoldyn_change) @@ -165,7 +199,12 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None # get the result of each SED variable variable_output_cmd_map = preprocessed_task['variable_output_cmd_map'] smoldyn_output_files = preprocessed_task['output_files'] - variable_results = get_variable_results(sed_simulation.number_of_steps, variables, variable_output_cmd_map, smoldyn_output_files) + variable_results = get_variable_results( + sed_simulation.number_of_steps, + variables, + variable_output_cmd_map, + smoldyn_output_files + ) # cleanup output files for smoldyn_output_file in smoldyn_output_files.values(): @@ -207,12 +246,15 @@ def preprocess_sed_task(task, variables, config=None): error_summary='Task `{}` is invalid.'.format(task.id)) raise_errors_warnings(validation.validate_model_language(sed_model.language, ModelLanguage.Smoldyn), error_summary='Language for model `{}` is not supported.'.format(sed_model.id)) - raise_errors_warnings(validation.validate_model_change_types(sed_model.changes, (ModelAttributeChange, )), + raise_errors_warnings(validation.validate_model_change_types(sed_model.changes, + (ModelAttributeChange, )), error_summary='Changes for model `{}` are not supported.'.format(sed_model.id)) raise_errors_warnings(*validation.validate_model_changes(sed_model), error_summary='Changes for model `{}` are invalid.'.format(sed_model.id)) - raise_errors_warnings(validation.validate_simulation_type(sed_simulation, (UniformTimeCourseSimulation, )), - error_summary='{} `{}` is not supported.'.format(sed_simulation.__class__.__name__, sed_simulation.id)) + raise_errors_warnings(validation.validate_simulation_type(sed_simulation, + (UniformTimeCourseSimulation, )), + error_summary='{} `{}` is not supported.'.format(sed_simulation.__class__.__name__, + sed_simulation.id)) raise_errors_warnings(*validation.validate_simulation(sed_simulation), error_summary='Simulation `{}` is invalid.'.format(sed_simulation.id)) raise_errors_warnings(*validation.validate_data_generator_variables(variables), @@ -221,7 +263,8 @@ def preprocess_sed_task(task, variables, config=None): if sed_simulation.algorithm.kisao_id not in KISAO_ALGORITHMS_MAP: msg = 'Algorithm `{}` is not supported. The following algorithms are supported:{}'.format( sed_simulation.algorithm.kisao_id, - ''.join('\n {}: {}'.format(kisao_id, alg_props['name']) for kisao_id, alg_props in KISAO_ALGORITHMS_MAP.items()) + ''.join('\n {}: {}'.format(kisao_id, alg_props['name']) + for kisao_id, alg_props in KISAO_ALGORITHMS_MAP.items()) ) raise NotImplementedError(msg) @@ -635,7 +678,8 @@ def add_smoldyn_output_file(configuration_dirname, smoldyn_simulation): ''' Add an output file to a Smoldyn simulation Args: - configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file for the simulation + configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file + for the simulation smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation Returns: @@ -756,7 +800,12 @@ def validate_variables(variables): if output_command_args is not None: output_command_args = output_command_args.strip() - variable_output_cmd_map[(variable.target, variable.symbol)] = (output_command_args, include_header, shape, results_slicer) + variable_output_cmd_map[(variable.target, variable.symbol)] = ( + output_command_args, + include_header, + shape, + results_slicer + ) if invalid_symbols: msg = '{} symbols cannot be recorded:\n {}\n\nThe following symbols can be recorded:\n {}'.format( @@ -824,7 +873,12 @@ def results_matrix_slicer(results): return results.iloc[:, 1:] -def add_smoldyn_output_files_for_sed_variables(configuration_dirname, variables, variable_output_cmd_map, smoldyn_simulation): +def add_smoldyn_output_files_for_sed_variables( + configuration_dirname, + variables, + variable_output_cmd_map, + smoldyn_simulation + ) -> Dict[str, SmoldynOutputFile]: ''' Add Smoldyn output files for capturing each SED variable Args: @@ -855,7 +909,8 @@ def add_smoldyn_output_file_for_output(configuration_dirname, smoldyn_simulation ''' Add a Smoldyn output file for molecule counts Args: - configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file for the simulation + configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration + file for the simulation smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation smoldyn_output_command (:obj:`str`): Smoldyn output command (e.g., ``molcount``) include_header (:obj:`bool`): whether to include a header @@ -881,7 +936,8 @@ def get_variable_results(number_of_steps, variables, variable_output_cmd_map, sm Args: number_of_steps (:obj:`int`): number of steps variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - variable_output_cmd_map (:obj:`dict`): dictionary that maps variable targets and symbols to Smoldyn output commands + variable_output_cmd_map (:obj:`dict`): dictionary that maps variable targets and symbols to Smoldyn output + commands smoldyn_output_files (:obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`): Smoldyn output files Returns: @@ -897,7 +953,13 @@ def get_variable_results(number_of_steps, variables, variable_output_cmd_map, sm variable_results = VariableResults() for variable in variables: output_command_args, _, shape, results_slicer = variable_output_cmd_map[(variable.target, variable.symbol)] - variable_result = get_smoldyn_output(output_command_args, True, shape, smoldyn_output_files, smoldyn_results) + variable_result = get_smoldyn_output( + output_command_args, + True, + shape, + smoldyn_output_files, + smoldyn_results + ) if results_slicer: variable_result = results_slicer(variable_result) From 0d620e4795c09121d2c2f2460a966bc1d6cf382d Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 07:49:38 -0400 Subject: [PATCH 23/34] chore: changed setter scope of default support spatial simulator --- biosimulators_utils/config.py | 44 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/biosimulators_utils/config.py b/biosimulators_utils/config.py index 21ac6f12..92c21bc7 100644 --- a/biosimulators_utils/config.py +++ b/biosimulators_utils/config.py @@ -33,6 +33,8 @@ # noinspection PyPep8Naming,PyDefaultArgument class Config(object): + SUPPORTED_SPATIAL_SIMULATOR: str + """ Configuration Attributes: @@ -67,6 +69,8 @@ class Config(object): BIOSIMULATIONS_API_AUDIENCE (:obj:`str`): audience for the BioSimulations API VERBOSE (:obj:`bool`): whether to display the detailed output of the execution of each task DEBUG (:obj:`bool`): whether to raise exceptions rather than capturing them + SUPPORTED_SPATIAL_SIMULATOR (:obj:`strl`, optional): spatial simulator that this config supports. Currently + only `'smoldyn'` is supported. """ def __init__(self, @@ -99,23 +103,25 @@ def __init__(self, BIOSIMULATIONS_API_AUDIENCE=DEFAULT_BIOSIMULATIONS_API_AUDIENCE, VERBOSE=False, DEBUG=False, - SPATIAL=False, - SUPPORTED_SPATIAL_SIMULATOR=DEFAULT_SUPPORTED_SPATIAL_SIMULATOR): + SPATIAL=False): """ Args: - OMEX_METADATA_INPUT_FORMAT (:obj:`OmexMetadataInputFormat`, optional): format to validate OMEX Metadata files against + OMEX_METADATA_INPUT_FORMAT (:obj:`OmexMetadataInputFormat`, optional): format to validate + OMEX Metadata files against OMEX_METADATA_OUTPUT_FORMAT (:obj:`OmexMetadataOutputFormat`, optional): format to export OMEX Metadata files OMEX_METADATA_SCHEMA (:obj:`OmexMetadataSchema`, optional): schema to validate OMEX Metadata files against - VALIDATE_OMEX_MANIFESTS (:obj:`bool`, optional): whether to validate OMEX manifests during the execution of COMBINE/OMEX archives - VALIDATE_SEDML (:obj:`bool`, optional): whether to validate SED-ML files during the execution of COMBINE/OMEX archives - VALIDATE_SEDML_MODELS (:obj:`bool`, optional): whether to validate models referenced by SED-ML files during the execution + VALIDATE_OMEX_MANIFESTS (:obj:`bool`, optional): whether to validate OMEX manifests during the execution of COMBINE/OMEX archives + VALIDATE_SEDML (:obj:`bool`, optional): whether to validate SED-ML files during the execution of + COMBINE/OMEX archives + VALIDATE_SEDML_MODELS (:obj:`bool`, optional): whether to validate models referenced by SED-ML + files during the execution of COMBINE/OMEX archives VALIDATE_IMPORTED_MODEL_FILES (:obj:`bool`, optional): whether to validate files imported from models VALIDATE_OMEX_METADATA (:obj:`bool`, optional): whether to validate OMEX metadata (RDF files) during the execution of COMBINE/OMEX archives VALIDATE_IMAGES (:obj:`bool`, optional): whether to validate the images in COMBINE/OMEX archives during their execution VALIDATE_RESULTS (:obj:`bool`, optional): whether to validate the results of simulations following their execution - ALGORITHM_SUBSTITUTION_POLICY (:obj:`str`, optional): algorithm substition policy + ALGORITHM_SUBSTITUTION_POLICY (:obj:`str`, optional): algorithm substitution policy COLLECT_COMBINE_ARCHIVE_RESULTS (:obj:`bool`, optional): whether to assemble an in memory data structure with all of the simulation results of COMBINE/OMEX archives COLLECT_SED_DOCUMENT_RESULTS (:obj:`bool`, optional): whether to assemble an in memory data structure with all of the @@ -137,7 +143,6 @@ def __init__(self, VERBOSE (:obj:`bool`, optional): whether to display the detailed output of the execution of each task DEBUG (:obj:`bool`, optional): whether to raise exceptions rather than capturing them SPATIAL (:obj:`bool`, optional): whether the simulation is spatial in nature and able to be simularium-ed - SUPPORTED_SPATIAL_SIMULATOR (:obj:`strl`, optional): spatial simulator that this config supports. """ self.OMEX_METADATA_INPUT_FORMAT = OMEX_METADATA_INPUT_FORMAT self.OMEX_METADATA_OUTPUT_FORMAT = OMEX_METADATA_OUTPUT_FORMAT @@ -169,24 +174,33 @@ def __init__(self, self.VERBOSE = VERBOSE self.DEBUG = DEBUG self.SPATIAL = SPATIAL - self.SUPPORTED_SPATIAL_SIMULATOR = SUPPORTED_SPATIAL_SIMULATOR + self.SUPPORTED_SPATIAL_SIMULATOR = DEFAULT_SUPPORTED_SPATIAL_SIMULATOR + try: + assert self.SUPPORTED_SPATIAL_SIMULATOR == 'smoldyn' + except AssertionError: + raise ValueError( + """ + The only spatial simulator that is currently supported is 'smoldyn'. Please set the value of + SUPPORTED_SPATIAL_SIMULATOR to 'smoldyn' and try again. + """ + ) def get_config(): - """ Get the configuration + """ Factory for generating a new instance of `Config`. Returns: :obj:`Config`: configuration """ report_formats = os.environ.get('REPORT_FORMATS', 'h5').strip() if report_formats: - report_formats = [ReportFormat(format.strip().lower()) for format in report_formats.split(',')] + report_formats = [ReportFormat(f.strip().lower()) for f in report_formats.split(',')] else: report_formats = [] viz_formats = os.environ.get('VIZ_FORMATS', 'pdf').strip() if viz_formats: - viz_formats = [VizFormat(format.strip().lower()) for format in viz_formats.split(',')] + viz_formats = [VizFormat(f.strip().lower()) for f in viz_formats.split(',')] else: viz_formats = [] @@ -206,7 +220,8 @@ def get_config(): VALIDATE_RESULTS=os.environ.get('VALIDATE_RESULTS', '1').lower() in ['1', 'true'], ALGORITHM_SUBSTITUTION_POLICY=AlgorithmSubstitutionPolicy(os.environ.get( 'ALGORITHM_SUBSTITUTION_POLICY', DEFAULT_ALGORITHM_SUBSTITUTION_POLICY)), - COLLECT_COMBINE_ARCHIVE_RESULTS=os.environ.get('COLLECT_COMBINE_ARCHIVE_RESULTS', '0').lower() in ['1', 'true'], + COLLECT_COMBINE_ARCHIVE_RESULTS=os.environ.get('COLLECT_COMBINE_ARCHIVE_RESULTS', + '0').lower() in ['1', 'true'], COLLECT_SED_DOCUMENT_RESULTS=os.environ.get('COLLECT_SED_DOCUMENT_RESULTS', '0').lower() in ['1', 'true'], SAVE_PLOT_DATA=os.environ.get('SAVE_PLOT_DATA', '1').lower() in ['1', 'true'], REPORT_FORMATS=report_formats, @@ -220,7 +235,8 @@ def get_config(): LOG_PATH=os.environ.get('LOG_PATH', DEFAULT_LOG_PATH), BIOSIMULATORS_API_ENDPOINT=os.environ.get('BIOSIMULATORS_API_ENDPOINT', DEFAULT_BIOSIMULATORS_API_ENDPOINT), BIOSIMULATIONS_API_ENDPOINT=os.environ.get('BIOSIMULATIONS_API_ENDPOINT', DEFAULT_BIOSIMULATIONS_API_ENDPOINT), - BIOSIMULATIONS_API_AUTH_ENDPOINT=os.environ.get('BIOSIMULATIONS_API_AUTH_ENDPOINT', DEFAULT_BIOSIMULATIONS_API_AUTH_ENDPOINT), + BIOSIMULATIONS_API_AUTH_ENDPOINT=os.environ.get('BIOSIMULATIONS_API_AUTH_ENDPOINT', + DEFAULT_BIOSIMULATIONS_API_AUTH_ENDPOINT), BIOSIMULATIONS_API_AUDIENCE=os.environ.get('BIOSIMULATIONS_API_AUDIENCE', DEFAULT_BIOSIMULATIONS_API_AUDIENCE), VERBOSE=os.environ.get('VERBOSE', '1').lower() in ['1', 'true'], DEBUG=os.environ.get('DEBUG', '0').lower() in ['1', 'true'], From 14d9a2992ea29dcc4a7998a307a2dea1adcdfd2f Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 07:49:50 -0400 Subject: [PATCH 24/34] chore: minor reformat --- biosimulators_utils/spatial/data_model.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index 03159aa2..6b3ec1c5 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -99,9 +99,8 @@ def __init__(self, Args: rootpath: root of the unzipped archive. Consider this your working dirpath. simularium_filename:`Optional`: full path which to assign to the newly generated simularium file. - If using this value, it EXPECTS a full path. Defaults to `{name}_output_for_simularium`. - name: Commonplace name for the archive to be used if no `simularium_filename` is passed. Defaults to - `new_spatial_archive`. + If using this value, it EXPECTS a full path. Defaults to `{name}_output_for_simularium`. + """ super().__init__() self.rootpath = rootpath From e8d132ce766b14eb87f77946446adf85fe4b94bb Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 07:50:48 -0400 Subject: [PATCH 25/34] fix: removed unused constructor param from factory --- biosimulators_utils/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/biosimulators_utils/config.py b/biosimulators_utils/config.py index 92c21bc7..5985990a 100644 --- a/biosimulators_utils/config.py +++ b/biosimulators_utils/config.py @@ -240,8 +240,7 @@ def get_config(): BIOSIMULATIONS_API_AUDIENCE=os.environ.get('BIOSIMULATIONS_API_AUDIENCE', DEFAULT_BIOSIMULATIONS_API_AUDIENCE), VERBOSE=os.environ.get('VERBOSE', '1').lower() in ['1', 'true'], DEBUG=os.environ.get('DEBUG', '0').lower() in ['1', 'true'], - SPATIAL=os.environ.get('SPATIAL', '0').lower() in ['1', 'true'], - SUPPORTED_SPATIAL_SIMULATOR=os.environ.get('SUPPORTED_SPATIAL_SIMULATOR', DEFAULT_SUPPORTED_SPATIAL_SIMULATOR) + SPATIAL=os.environ.get('SPATIAL', '0').lower() in ['1', 'true'] ) From c914a0ee725e5e93ccf82f2054b695e16172f8e6 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 08:13:59 -0400 Subject: [PATCH 26/34] chore: reformatted docstrings and code --- biosimulators_utils/spatial/exec.py | 126 ++++++++++++++-------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py index 255a84e2..97b0d807 100644 --- a/biosimulators_utils/spatial/exec.py +++ b/biosimulators_utils/spatial/exec.py @@ -15,6 +15,8 @@ import tempfile from typing import * import types # noqa: F401 +import pandas as pd +from smoldyn import smoldyn from biosimulators_utils.spatial.data_model import ( SmoldynCommand, SmoldynOutputFile, @@ -24,22 +26,31 @@ KISAO_ALGORITHMS_MAP, KISAO_ALGORITHM_PARAMETERS_MAP ) -from smoldyn import smoldyn -from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive as base_exec_combine_archive -from biosimulators_utils.config import get_config, Config # noqa: F401 -from biosimulators_utils.log.data_model import (CombineArchiveLog, - TaskLog, - StandardOutputErrorCapturerLevel, - SedDocumentLog) # noqa: F401 +from biosimulators_utils.log.data_model import ( + CombineArchiveLog, + TaskLog, + StandardOutputErrorCapturerLevel, + SedDocumentLog +) # noqa: F401 from biosimulators_utils.viz.data_model import VizFormat # noqa: F401 -from biosimulators_utils.report.data_model import (ReportFormat, - ReportResults, - VariableResults, - SedDocumentResults) # noqa: F401 +from biosimulators_utils.report.data_model import ( + ReportFormat, # noqa: F401 + ReportResults, + VariableResults, + SedDocumentResults +) +from biosimulators_utils.sedml.data_model import ( + Task, ModelLanguage, + ModelAttributeChange, # noqa: F401 + UniformTimeCourseSimulation, + AlgorithmParameterChange, # noqa: F401 + Variable, + Symbol, + SedDocument +) from biosimulators_utils.sedml import validation -from biosimulators_utils.sedml.data_model import (Task, ModelLanguage, ModelAttributeChange, # noqa: F401 - UniformTimeCourseSimulation, AlgorithmParameterChange, Variable, - Symbol, SedDocument) +from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive as base_exec_combine_archive +from biosimulators_utils.config import get_config, Config # noqa: F401 from biosimulators_utils.sedml.exec import exec_sed_doc as base_exec_sed_doc from biosimulators_utils.utils.core import validate_str_value, parse_value, raise_errors_warnings @@ -88,9 +99,9 @@ def exec_sed_doc( indent=0, pretty_print_modified_xml_models=False, log_level=StandardOutputErrorCapturerLevel.c, - log: Optional[SedDocumentLog]=None, - rel_out_path: Optional[str]=None, - config: Optional[Config]=None + log: Optional[SedDocumentLog] = None, + rel_out_path: Optional[str] = None, + config: Optional[Config] = None ) -> Tuple[ReportResults, SedDocumentLog]: """ Execute the tasks specified in a SED document and generate the specified outputs @@ -352,7 +363,8 @@ def init_smoldyn_simulation_from_configuration_file(filename): if not smoldyn_simulation.getSimPtr(): error_code, error_msg = smoldyn.getError() msg = 'Model source `{}` is not a valid Smoldyn file.\n\n {}: {}'.format( - filename, error_code.name[0].upper() + error_code.name[1:], error_msg.replace('\n', '\n ')) + filename, + error_code.name[0].upper() + error_code.name[1:], error_msg.replace('\n', '\n ')) raise ValueError(msg) return smoldyn_simulation @@ -446,7 +458,7 @@ def disable_smoldyn_graphics_in_simulation_configuration(configuration): configuration[i_line] = re.sub(r'^graphics +[a-z_]+', 'graphics none', line) -def validate_model_change(sed_model_change): +def validate_model_change(sed_model_change: ModelAttributeChange) -> SimulationChange: ''' Validate a SED model attribute change to a configuration for a Smoldyn simulation ==================================================================== =================== @@ -705,42 +717,15 @@ def add_commands_to_smoldyn_output_file(simulation, output_file, commands): simulation.addCommand(command.command + ' ' + output_file.name, command.type) -def validate_variables(variables): - ''' Validate SED variables - - ============================================================================================================================================= =========================================================================================================================================== =========================================== - Smoldyn output file SED variable target Shape - ============================================================================================================================================= =========================================================================================================================================== =========================================== - ``molcount`` ``molcount {species}`` (numberOfSteps + 1,) - ``molcountspecies {species}({state})`` ``molcountspecies {species}({state})`` (numberOfSteps + 1,) - ``molcountspecieslist {species}({state})+`` ``molcountspecies {species}({state})`` (numberOfSteps + 1,) - ``molcountinbox {low-x} {hi-x}`` ``molcountinbox {species} {low-x} {hi-x}`` (numberOfSteps + 1,) - ``molcountinbox {low-x} {hi-x} {low-y} {hi-y}`` ``molcountinbox {species} {low-x} {hi-x} {low-y} {hi-y}`` (numberOfSteps + 1,) - ``molcountinbox {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z}`` ``molcountinbox {species} {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z}`` (numberOfSteps + 1,) - ``molcountincmpt {compartment}`` ``molcountincmpt {species} {compartment}`` (numberOfSteps + 1,) - ``molcountincmpts {compartment}+`` ``molcountincmpt {species} {compartment}`` (numberOfSteps + 1,) - ``molcountincmpt2 {compartment} {state}`` ``molcountincmpt2 {species} {compartment} {state}`` (numberOfSteps + 1,) - ``molcountonsurf {surface}`` ``molcountonsurf {species} {surface}`` (numberOfSteps + 1,) - ``molcountspace {species}({state}) {axis} {low} {hi} {bins} 0`` ``molcountspace {species}({state}) {axis} {low} {hi} {bins}`` (numberOfSteps + 1, bins) - ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi} 0`` ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi}`` (numberOfSteps + 1, bins) - ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi} {low} {hi} 0`` ``molcountspace {species}({state}) {axis} {low} {hi} {bins} {low} {hi} {low} {hi}`` (numberOfSteps + 1, bins) - ``molcountspace2d {species}({state}) z {low-x} {hi-x} {bins-x} {low-y} {hi-y} {bins-y} 0`` ``molcountspace2d {species}({state}) z {low-x} {hi-x} {bins-x} {low-y} {hi-y} {bins-y}`` (numberOfSteps + 1, bins-x, bins-y) - ``molcountspace2d {species}({state}) {axis} {low-1} {hi-1} {bins-1} {low-2} {hi-2} {bins-2} {low-3} {hi-3} 0`` ``molcountspace2d {species}({state}) {axis} {low-1} {hi-1} {bins-1} {low-2} {hi-2} {bins-3} {low-3} {hi-3}`` (numberOfSteps + 1, bins-1, bins-2) - ``molcountspaceradial {species}({state}) {center-x} {radius} {bins} 0`` ``molcountspaceradial {species}({state}) {center-x} {radius} {bins}`` (numberOfSteps + 1, bins) - ``molcountspaceradial {species}({state}) {center-x} {center-y} {radius} {bins} 0`` ``molcountspaceradial {species}({state}) {center-x} {center-y} {radius} {bins}`` (numberOfSteps + 1, bins) - ``molcountspaceradial {species}({state}) {center-x} {center-y} {center-z} {radius} {bins} 0`` ``molcountspaceradial {species}({state}) {center-x} {center-y} {center-z} {radius} {bins}`` (numberOfSteps + 1, bins) - ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {pole-x} {pole-y} {radius-min} {radius-max} {bins} 0`` ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {pole-x} {pole-y} {radius-min} {radius-max} {bins}`` (numberOfSteps + 1, bins) - ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {center-z} {pole-x} {pole-y} {pole-z} {radius-min} {radius-max} {bins} 0`` ``molcountspacepolarangle {species}({state}) {center-x} {center-y} {center-z} {pole-x} {pole-y} {pole-z} {radius-min} {radius-max} {bins}`` (numberOfSteps + 1, bins) - ``radialdistribution {species-1}({state-1}) {species-2}({state-2}) {radius} {bins} 0`` ``radialdistribution {species-1}({state-1}) {species-2}({state-2}) {radius} {bins}`` (numberOfSteps + 1, bins) - ``radialdistribution2 {species-1}({state-1}) {species-2}({state-2}) {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z} {radius} {bins} 0`` ``radialdistribution2 {species-1}({state-1}) {species-2}({state-2}) {low-x} {hi-x} {low-y} {hi-y} {low-z} {hi-z} {radius} {bins}`` (numberOfSteps + 1, bins) - ============================================================================================================================================= ========================================================================================================================================== =========================================== +def validate_variables(variables) -> Dict: + """ Generate a dictionary that maps variable targets and symbols to Smoldyn output commands. - Args: - variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + Args: + variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - Returns: - :obj:`dict`: dictionary that maps variable targets and symbols to Smoldyn output commands - ''' + Returns: + :obj:`dict`: dictionary that maps variable targets and symbols to Smoldyn output commands + """ # TODO: support additional kinds of outputs variable_output_cmd_map = {} @@ -827,11 +812,12 @@ def validate_variables(variables): 'molcountspace2d', ] - msg = '{} targets cannot be recorded:\n {}\n\nTargets are supported for the following output commands:\n {}'.format( - len(invalid_targets), - '\n '.join(sorted(invalid_targets)), - '\n '.join(sorted(set(valid_target_output_commands))), - ) + msg = '{} targets cannot be recorded:\n {}\n\nTargets are supported for the following output commands:\n {}'\ + .format( + len(invalid_targets), + '\n '.join(sorted(invalid_targets)), + '\n '.join(sorted(set(valid_target_output_commands))), + ) raise NotImplementedError(msg) return variable_output_cmd_map @@ -904,8 +890,13 @@ def add_smoldyn_output_files_for_sed_variables( return smoldyn_output_files -def add_smoldyn_output_file_for_output(configuration_dirname, smoldyn_simulation, - smoldyn_output_command, include_header, smoldyn_output_files): +def add_smoldyn_output_file_for_output( + configuration_dirname: str, + smoldyn_simulation: smoldyn.Simulation, + smoldyn_output_command: str, + include_header: bool, + smoldyn_output_files: Dict[str, SmoldynOutputFile] + ) -> None: ''' Add a Smoldyn output file for molecule counts Args: @@ -930,7 +921,12 @@ def add_smoldyn_output_file_for_output(configuration_dirname, smoldyn_simulation ) -def get_variable_results(number_of_steps, variables, variable_output_cmd_map, smoldyn_output_files): +def get_variable_results( + number_of_steps: int, + variables: List[Variable], + variable_output_cmd_map: Dict, + smoldyn_output_files: Dict[str, SmoldynOutputFile] + ) -> VariableResults: ''' Get the result of each SED variable Args: @@ -983,7 +979,13 @@ def get_variable_results(number_of_steps, variables, variable_output_cmd_map, sm return variable_results -def get_smoldyn_output(smoldyn_output_command, has_header, three_d_shape, smoldyn_output_files, smoldyn_results): +def get_smoldyn_output( + smoldyn_output_command: str, + has_header: bool, + three_d_shape: Tuple[int], + smoldyn_output_files: Dict[str, SmoldynOutputFile], + smoldyn_results: Dict + ) -> pd.DataFrame: ''' Get the simulated count of each molecule Args: From a0cef5de31d5c17b8aa9538bd2b5fec8a75f9dec Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 08:34:18 -0400 Subject: [PATCH 27/34] updated nomenclature and reformatting --- biosimulators_utils/spatial/exec.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py index 97b0d807..54f297dd 100644 --- a/biosimulators_utils/spatial/exec.py +++ b/biosimulators_utils/spatial/exec.py @@ -55,15 +55,16 @@ from biosimulators_utils.utils.core import validate_str_value, parse_value, raise_errors_warnings -__all__ = ['exec_sedml_docs_in_combine_archive', 'exec_sed_task', 'exec_sed_doc', 'preprocess_sed_task'] +__all__ = ['exec_combine_archive', 'exec_sed_task', 'exec_sed_doc', 'preprocess_sed_task'] -def exec_sedml_docs_in_combine_archive( +def exec_combine_archive( archive_filename: str, out_dir: str, config: Optional[Config] = None ) -> Tuple[SedDocumentResults, CombineArchiveLog]: - """ Execute the SED tasks defined in a COMBINE/OMEX archive and save the outputs + """ Execute the SED tasks defined in a COMBINE/OMEX archive whose contents relate to a Spatial simulation + and save the outputs. Args: archive_filename (:obj:`str`): path to COMBINE/OMEX archive @@ -124,7 +125,6 @@ def exec_sed_doc( pretty_print_modified_xml_models (:obj:`bool`, optional): if :obj:`True`, pretty print modified XML models log_level (:obj:`StandardOutputErrorCapturerLevel`, optional): level at which to log output config (:obj:`Config`, optional): BioSimulators common configuration - simulator_config (:obj:`SimulatorConfig`, optional): tellurium configuration Returns: :obj:`tuple`: @@ -132,14 +132,19 @@ def exec_sed_doc( * :obj:`ReportResults`: results of each report * :obj:`SedDocumentLog`: log of the document """ - return base_exec_sed_doc(exec_sed_task, doc, working_dir, base_out_path, - rel_out_path=rel_out_path, - apply_xml_model_changes=apply_xml_model_changes, - log=log, - indent=indent, - pretty_print_modified_xml_models=pretty_print_modified_xml_models, - log_level=log_level, - config=config) + return base_exec_sed_doc( + exec_sed_task, + doc, + working_dir, + base_out_path, + rel_out_path=rel_out_path, + apply_xml_model_changes=apply_xml_model_changes, + log=log, + indent=indent, + pretty_print_modified_xml_models=pretty_print_modified_xml_models, + log_level=log_level, + config=config + ) # noinspection PyShadowingNames From e287e0e3d033c072e898912ed6af514e044bcbce Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 08:34:38 -0400 Subject: [PATCH 28/34] chore: added test file to ignore to be deleted --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 921d07f7..2db72f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ docs/.buildinfo docs/.doctrees/ docs/_raw_sources/ docs/_sources/ + +# test files (to be deleted) +test_spatial_exec.py From 3ed7ec077a60c2548a29793b95c234a4a8bf56bf Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 08:39:24 -0400 Subject: [PATCH 29/34] chore: updated doc signature --- biosimulators_utils/spatial/data_model.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py index 6b3ec1c5..626baf8f 100644 --- a/biosimulators_utils/spatial/data_model.py +++ b/biosimulators_utils/spatial/data_model.py @@ -1,10 +1,15 @@ """ +Objects related to the execution of SED-ML docs within COMBINE/OMEX archives whose simulation/model params +are spatial in nature. Currently, this library's primary focus is the conversion of spatial output data to the +`.simularium` file format using the `simulariumio` API. The only simulator is currently supported by both +BioSimulators and Simularium is Smoldyn, and thus the only actual simulator that is available in this library +(for now). + :Author: Alexander Patrie :Date: 2023-09-16 :Copyright: 2023, UConn Health :License: MIT - Using the Biosimulators side of Smoldyn to generate a modelout.txt Smoldyn file for a specified OMEX/COMBINE archive which then is used to generate a .simularium file for the given simulation. That .simularium file is then stored along with the log.yml and report.{FORMAT} relative to the simulation. Remember: each simulation, while not inherently From 4dc4471d4b9f98c1b87acd909b41c21d2ec61a48 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 09:07:27 -0400 Subject: [PATCH 30/34] rename nomenclature --- biosimulators_utils/spatial/exec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py index 54f297dd..cd443a05 100644 --- a/biosimulators_utils/spatial/exec.py +++ b/biosimulators_utils/spatial/exec.py @@ -55,10 +55,10 @@ from biosimulators_utils.utils.core import validate_str_value, parse_value, raise_errors_warnings -__all__ = ['exec_combine_archive', 'exec_sed_task', 'exec_sed_doc', 'preprocess_sed_task'] +__all__ = ['exec_spatial_combine_archive', 'exec_sed_task', 'exec_sed_doc', 'preprocess_sed_task'] -def exec_combine_archive( +def exec_spatial_combine_archive( archive_filename: str, out_dir: str, config: Optional[Config] = None From ae703fbe70e181d60e05d4d5c42ef9661430fc1f Mon Sep 17 00:00:00 2001 From: alex patrie Date: Fri, 6 Oct 2023 15:00:32 -0400 Subject: [PATCH 31/34] minor import dep adjustment --- biosimulators_utils/combine/exec.py | 10 +- biosimulators_utils/config.py | 4 +- biosimulators_utils/spatial/__init__.py | 0 biosimulators_utils/spatial/data_model.py | 963 ------------------- biosimulators_utils/spatial/exec.py | 1031 --------------------- biosimulators_utils/spatial/io.py | 67 -- biosimulators_utils/spatial/minE.omex | Bin 2785 -> 0 bytes biosimulators_utils/spatial/utils.py | 40 - requirements.optional.txt | 5 +- requirements.txt | 1 + 10 files changed, 13 insertions(+), 2108 deletions(-) delete mode 100644 biosimulators_utils/spatial/__init__.py delete mode 100644 biosimulators_utils/spatial/data_model.py delete mode 100644 biosimulators_utils/spatial/exec.py delete mode 100644 biosimulators_utils/spatial/io.py delete mode 100644 biosimulators_utils/spatial/minE.omex delete mode 100644 biosimulators_utils/spatial/utils.py diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 19ecb6e9..2bb17f54 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -7,7 +7,6 @@ """ -from ..spatial.data_model import SmoldynCombineArchive, SmoldynDataConverter from ..archive.io import ArchiveWriter from ..archive.utils import build_archive_from_paths from ..config import get_config, Config # noqa: F401 @@ -32,6 +31,7 @@ import shutil from typing import Optional, Tuple from types import FunctionType # noqa: F401 +import importlib __all__ = [ @@ -139,6 +139,11 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # unpack archive and read metadata archive = CombineArchiveReader().run(archive_filename, archive_tmp_dir, config=config) + # check the manifest for a smoldyn model + for content in archive.contents: + if 'smoldyn' in content.location: + config.SPATIAL = True + # validate archive errors, warnings = validate(archive, archive_tmp_dir, config=config) if warnings: @@ -263,8 +268,9 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # generate simularium file if spatial if config.SPATIAL: + biosimularium = importlib.import_module('biosimulators_simularium') simularium_filename = os.path.join(out_dir, 'output') - spatial_archive = SmoldynCombineArchive(rootpath=out_dir, simularium_filename=simularium_filename) + spatial_archive = biosimularium.SmoldynCombineArchive(rootpath=out_dir, simularium_filename=simularium_filename) # check if modelout file exists if not os.path.exists(spatial_archive.model_path): diff --git a/biosimulators_utils/config.py b/biosimulators_utils/config.py index 5985990a..46fe47bb 100644 --- a/biosimulators_utils/config.py +++ b/biosimulators_utils/config.py @@ -174,9 +174,9 @@ def __init__(self, self.VERBOSE = VERBOSE self.DEBUG = DEBUG self.SPATIAL = SPATIAL - self.SUPPORTED_SPATIAL_SIMULATOR = DEFAULT_SUPPORTED_SPATIAL_SIMULATOR + self.__SUPPORTED_SPATIAL_SIMULATOR = DEFAULT_SUPPORTED_SPATIAL_SIMULATOR try: - assert self.SUPPORTED_SPATIAL_SIMULATOR == 'smoldyn' + assert self.__SUPPORTED_SPATIAL_SIMULATOR == 'smoldyn' except AssertionError: raise ValueError( """ diff --git a/biosimulators_utils/spatial/__init__.py b/biosimulators_utils/spatial/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/biosimulators_utils/spatial/data_model.py b/biosimulators_utils/spatial/data_model.py deleted file mode 100644 index 626baf8f..00000000 --- a/biosimulators_utils/spatial/data_model.py +++ /dev/null @@ -1,963 +0,0 @@ -""" -Objects related to the execution of SED-ML docs within COMBINE/OMEX archives whose simulation/model params -are spatial in nature. Currently, this library's primary focus is the conversion of spatial output data to the -`.simularium` file format using the `simulariumio` API. The only simulator is currently supported by both -BioSimulators and Simularium is Smoldyn, and thus the only actual simulator that is available in this library -(for now). - -:Author: Alexander Patrie -:Date: 2023-09-16 -:Copyright: 2023, UConn Health -:License: MIT - -Using the Biosimulators side of Smoldyn to generate a modelout.txt Smoldyn file for a specified OMEX/COMBINE archive -which then is used to generate a .simularium file for the given simulation. That .simularium file is then stored along -with the log.yml and report.{FORMAT} relative to the simulation. Remember: each simulation, while not inherently -published, has the potential for publication based purely on the simulation's ability to provide a valid OMEX/COMBINE -archive. There exists (or should exist) an additional layer of abstraction to then validate and -verify the contents therein. -""" - - -# pragma: no cover -import enum -import types # noqa: F401 -import os -from dataclasses import dataclass -from warnings import warn -from typing import Optional, Tuple, Dict, List, Union -from abc import ABC, abstractmethod -# noinspection PyPackageRequirements -from smoldyn import Simulation as smoldynSim -import numpy as np -import pandas as pd -from simulariumio import ( - CameraData, - UnitData, - MetaData, - DisplayData, - BinaryWriter, - JsonWriter, - TrajectoryConverter, - TrajectoryData, - AgentData -) -from simulariumio.smoldyn.smoldyn_data import InputFileData -from simulariumio.smoldyn import SmoldynConverter, SmoldynData -from simulariumio.filters import TranslateFilter -from biosimulators_utils.combine.data_model import CombineArchiveContent -from biosimulators_utils.combine.io import CombineArchiveReader, CombineArchiveWriter -from biosimulators_utils.archive.io import ArchiveReader, ArchiveWriter -from biosimulators_utils.model_lang.smoldyn.validation import validate_model -from biosimulators_utils.data_model import ValueType - - -__all__ = [ - 'ModelValidation', - 'SpatialCombineArchive', - 'SmoldynCombineArchive', - 'BiosimulatorsDataConverter', - 'SmoldynDataConverter', - 'Simulation', - 'SimulationInstruction', - 'SmoldynCommand', - 'SmoldynOutputFile', - 'SimulationChange', - 'SimulationChangeExecution', - 'AlgorithmParameterType', - 'KISAO_ALGORITHMS_MAP', - 'KISAO_ALGORITHM_PARAMETERS_MAP', -] - - -"""*Validation/Simulation*""" - - -@dataclass -class ModelValidation: - errors: List[List[str]] - warnings: List[str] - simulation: smoldynSim - config: List[str] - - def __init__(self, validation: Tuple[List[List[str]], List, Tuple[smoldynSim, List[str]]]): - self.errors = validation[0] - self.warnings = validation[1] - self.simulation = validation[2][0] - self.config = validation[2][1] - - -"""*Spatial Combine Archives*""" - - -# TODO: Add more robust rezipping -class SpatialCombineArchive(ABC): - __zipped_file_format: str - was_unzipped: bool - paths: Dict[str, str] - - def __init__(self, - rootpath: str, - simularium_filename=None): - """ABC Object for storing and setting/getting files pertaining to simularium file conversion. - - Args: - rootpath: root of the unzipped archive. Consider this your working dirpath. - simularium_filename:`Optional`: full path which to assign to the newly generated simularium file. - If using this value, it EXPECTS a full path. Defaults to `{name}_output_for_simularium`. - - """ - super().__init__() - self.rootpath = rootpath - if not simularium_filename: - simularium_filename = 'spatial_combine_archive' - self.simularium_filename = os.path.join(self.rootpath, simularium_filename) - self.__parse_rootpath() - self.paths = self.get_all_archive_filepaths() - - @property - def __zipped_file_format(self) -> str: - return '.omex' - - def __parse_rootpath(self): - """Private method for parsing whether `self.rootpath` is the path to a directory or single OMEX/COMBINE - zipped file. If .omex, then decompress the input path into an unzipped directory for working and sets - `self.was_unzipped` to `True` and `False` if not. - """ - if self.rootpath.endswith(self.__zipped_file_format): - self.unzip() - self.was_unzipped = True - else: - self.was_unzipped = False - - def unzip(self, unzipped_output_location: str = None): - reader = ArchiveReader() - try: - if not unzipped_output_location: - unzipped_output_location = self.rootpath.replace( - self.__zipped_file_format, - '_UNZIPPED' - ) # TODO: make tempdir here instead - reader.run(self.rootpath, unzipped_output_location) - print('Omex successfully unzipped!...') - self.rootpath = unzipped_output_location - except Exception as e: - warn(f'Omex could not be unzipped because: {e}') - - def rezip(self, paths_to_write: Optional[List[str]] = None, destination: Optional[str] = None): - if self.__zipped_file_format in self.rootpath: - writer = ArchiveWriter() - if not paths_to_write: - paths_to_write = list(self.get_all_archive_filepaths().values()) - print(f'HERE THEY ARE: {paths_to_write}') - if not destination: - destination = self.rootpath - writer.run(archive=paths_to_write, archive_filename=destination) - print(f'Omex successfully bundled with the following paths: {paths_to_write}!') - - def get_all_archive_filepaths(self) -> Dict[str, str]: - """Recursively read the contents of the directory found at `self.rootpath` and set their full paths. - - Returns: - `Dict[str, str]`: Dict of form {'path_root': full_path} - """ - paths = {} - if os.path.exists(self.rootpath): - for root, _, files in os.walk(self.rootpath): - paths['root'] = root - for f in files: - fp = os.path.join(root, f) - paths[f] = fp - return paths - - def get_manifest_filepath(self) -> Union[List[str], str]: - """Read SmoldynCombineArchive manifest files. Return all filepaths containing the word 'manifest'. - - Returns: - :obj:`str`: path if there is just one manifest file, otherwise `List[str]` of manifest filepaths. - """ - manifest = [] - for v in list(self.paths.values()): - if 'manifest' in v: - manifest.append(v) - self.paths['manifest'] = v - return list(set(manifest))[0] - - def read_manifest_contents(self): - """Reads the contents of the manifest file within `self.rootpath`. - Read the return value of `self.get_manifest_filepath()` as the input for `CombineArchiveReader.run(). - """ - manifest_fp = self.get_manifest_filepath() - reader = CombineArchiveReader() - return reader.read_manifest(filename=manifest_fp) - - @staticmethod - def generate_new_archive_content(fp: str) -> CombineArchiveContent: - """Factory for generating a new instance of `CombineArchiveContent` using just fp. - - Args: - fp: filepath of the content you wish to add to the combine archive. - - Returns: - `CombineArchiveContent` based on the passed `fp`. - """ - return CombineArchiveContent(fp) - - def add_file_to_manifest(self, contents_fp: str) -> None: - contents = self.read_manifest_contents() - new_content = self.generate_new_archive_content(contents_fp) - contents.append(new_content) - writer = CombineArchiveWriter() - try: - manifest_fp = self.get_manifest_filepath() - writer.write_manifest(contents=contents, filename=manifest_fp) - print('File added to archive manifest contents!') - except Exception as e: - print(e) - warn(f'The simularium file found at {contents_fp} could not be added to manifest.') - return - - def add_modelout_file_to_manifest(self, model_fp) -> None: - return self.add_file_to_manifest(model_fp) - - def add_simularium_file_to_manifest(self, simularium_fp: Optional[str] = None) -> None: - """Read the contents of the manifest file found at `self.rootpath`, create a new instance of - `CombineArchiveContent` using a set simularium_fp, append the new content to the original, - and re-write the archive to reflect the newly added content. - - Args: - simularium_fp:`Optional`: path to the newly generated simularium file. Defaults - to `self.simularium_filename`. - """ - try: - if not simularium_fp: - simularium_fp = self.simularium_filename - self.add_file_to_manifest(simularium_fp) - print('Simularium File added to archive manifest contents!') - except Exception: - raise IOError(f'The simularium file found at {simularium_fp} could not be added to manifest.') - - def verify_smoldyn_in_manifest(self) -> bool: - """Pass the return value of `self.get_manifest_filepath()` into a new instance of `CombineArchiveReader` - such that the string manifest object tuples are evaluated for the presence of `smoldyn`. - - Returns: - `bool`: Whether there exists a smoldyn model in the archive based on the archive's manifest. - """ - manifest_contents = [c.to_tuple() for c in self.read_manifest_contents()] - model_info = manifest_contents[0][1] - return 'smoldyn' in model_info - - @abstractmethod - def set_model_filepath(self, - model_default: str, - model_filename: Optional[str] = None) -> Union[str, None]: - """Recurse `self.rootpath` and search for your simulator's model file extension.""" - pass - - @abstractmethod - def set_model_output_filepath(self) -> None: - """Recursively read the directory at `self.rootpath` and standardize the model output filename to become - `self.model_output_filename`. - """ - pass - - @abstractmethod - def generate_model_validation_object(self) -> ModelValidation: - """Generate an instance of `ModelValidation` based on the output of `self.model_path` - with your simulator's primary validation method. - - Returns: - :obj:`ModelValidation` - """ - pass - - -class SmoldynCombineArchive(SpatialCombineArchive): - def __init__(self, - rootpath: str, - model_output_filename='modelout.txt', - simularium_filename='smoldyn_combine_archive'): - """Object for handling the output of Smoldyn simulation data. Implementation child of `SpatialCombineArchive`. - - Args: - rootpath: fp to the root of the archive 'working dir'. - model_output_filename: filename ONLY not filepath of the model file you are working with. Defaults to - `modelout.txt`. - simularium_filename: - """ - super().__init__(rootpath, simularium_filename) - self.set_model_filepath() - self.model_output_filename = os.path.join(self.rootpath, model_output_filename) - self.paths['model_output_file'] = self.model_output_filename - - def set_model_filepath(self, model_default='model.txt', model_filename: Optional[str] = None): - """Recursively read the full paths of all files in `self.paths` and return the full path of the file - containing the term 'model.txt', which is the naming convention. - Implementation of ancestral abstract method. - - Args: - model_filename: `Optional[str]`: index by which to label a file in directory as the model file. - Defaults to `model_default`. - model_default: `str`: default model filename naming convention. Defaults to `'model.txt'` - """ - if not model_filename: - model_filename = os.path.join(self.rootpath, model_default) # default Smoldyn model name - for k in self.paths.keys(): - full_path = self.paths[k] - if model_filename in full_path: - # noinspection PyAttributeOutsideInit - self.model_path = model_filename - - def set_model_output_filepath(self) -> None: - """Recursively search the directory at `self.rootpath` for a smoldyn - modelout file (`.txt`) and standardize the model output filename to become - `self.model_output_filename`. Implementation of ancestral abstract method. - """ - for root, _, files in os.walk(self.rootpath): - for f in files: - if f.endswith('.txt') and 'model' not in f and os.path.exists(f): - f = os.path.join(root, f) - os.rename(f, self.model_output_filename) - - def generate_model_validation_object(self) -> ModelValidation: - """Generate an instance of `ModelValidation` based on the output of `self.model_path` - with `biosimulators-utils.model_lang.smoldyn.validate_model` method. - Implementation of ancestral abstract method. - - Returns: - :obj:`ModelValidation` - """ - validation_info = validate_model(self.model_path) - validation = ModelValidation(validation_info) - return validation - - -"""*Converters*""" - - -class BiosimulatorsDataConverter(ABC): - has_smoldyn: bool - - def __init__(self, archive: SpatialCombineArchive): - """This class serves as the abstract interface for a simulator-specific implementation - of utilities through which the user may convert Biosimulators outputs to a valid simularium File. - - Args: - :obj:`archive`:(`SpatialCombineArchive`): instance of an archive to base conv and save on. - """ - self.archive = archive - self.has_smoldyn = self.archive.verify_smoldyn_in_manifest() - - # Factory Methods - @abstractmethod - def generate_output_data_object( - self, - file_data: InputFileData, - display_data: Optional[Dict[str, DisplayData]] = None, - spatial_units="nm", - temporal_units="ns", - ): - """Factory to generate a data object to fit the simulariumio.TrajectoryData interface. - """ - pass - - @abstractmethod - def translate_data_object(self, - data_object_converter: TrajectoryConverter, - box_size: Union[float, int], - n_dim: str = 3) -> TrajectoryData: - """Factory to create a mirrored negative image of a distribution and apply it to 3dimensions if - AND ONLY IF it contains all non-negative values. - """ - pass - - @abstractmethod - def generate_simularium_file( - self, - simularium_filename: str, - box_size: float, - translate: bool, - spatial_units="nm", - temporal_units="ns", - n_dim=3, - display_data: Optional[Dict[str, DisplayData]] = None, - ) -> None: - """Factory for a taking in new data_object, optionally translate it, convert to simularium, and save. - """ - pass - - @abstractmethod - def generate_converter(self, data: TrajectoryData): - """Factory for creating a new instance of a translator/filter converter based on the Simulator, - whose output you are attempting to visualize. - """ - pass - - @staticmethod - def generate_agent_data_object( - timestep: int, - total_steps: int, - n_agents: int, - box_size: float, - min_radius: int, - max_radius: int, - display_data_dict: Dict[str, DisplayData], - type_names: List[List[str]], - positions=None, - radii=None, - ) -> AgentData: - """Factory for a new instance of an `AgentData` object following the specifications of the simulation within the - relative combine archive. - - Returns: - `AgentData` instance. - """ - positions = positions or np.random.uniform(size=(total_steps, n_agents, 3)) * box_size - box_size * 0.5 - radii = radii or (max_radius - min_radius) * np.random.uniform(size=(total_steps, n_agents)) + min_radius - return AgentData( - times=timestep * np.array(list(range(total_steps))), - n_agents=np.array(total_steps * [n_agents]), - viz_types=np.array(total_steps * [n_agents * [1000.0]]), # default viz type = 1000 - unique_ids=np.array(total_steps * [list(range(n_agents))]), - types=type_names, - positions=positions, - radii=radii, - display_data=display_data_dict - ) - - @staticmethod - def prepare_simularium_fp(**simularium_config) -> str: - """Generate a simularium dir and joined path if not using the init object. - - Kwargs: - (obj):`**simularium_config`: keys are 'simularium_dirpath' and 'simularium_fname' - - Returns: - (obj):`str`: complete simularium filepath - """ - dirpath = simularium_config.get('simularium_dirpath') - if not os.path.exists(dirpath): - os.mkdir(dirpath) - return os.path.join(dirpath, simularium_config.get('simularium_fname')) - - @staticmethod - def generate_metadata_object(box_size: np.ndarray[int], camera_data: CameraData) -> MetaData: - """Factory for a new instance of `simulariumio.MetaData` based on the input params of this method. - Currently, `ModelMetaData` is not supported as a param. - - Args: - box_size: ndarray containing the XYZ dims of the simulation bounding volume. Defaults to [100,100,100]. - camera_data: new `CameraData` instance to control visuals. - - Returns: - `MetaData` instance. - """ - return MetaData(box_size=box_size, camera_defaults=camera_data) - - @staticmethod - def generate_camera_data_object( - position: np.ndarray, - look_position: np.ndarray, - up_vector: np.ndarray - ) -> CameraData: - """Factory for a new instance of `simulariumio.CameraData` based on the input params. - Wraps the simulariumio object. - - Args: - position: 3D position of the camera itself Default: np.array([0.0, 0.0, 120.0]). - look_position: np.ndarray (shape = [3]) position the camera looks at Default: np.zeros(3). - up_vector: np.ndarray (shape = [3]) the vector that defines which direction is “up” in the - camera’s view Default: np.array([0.0, 1.0, 0.0]) - - Returns: - `CameraData` instance. - """ - return CameraData(position=position, look_at_position=look_position, up_vector=up_vector) - - @staticmethod - def generate_display_data_object( - name: str, - radius: float, - display_type=None, - obj_color: Optional[str] = None, - ) -> DisplayData: - """Factory for creating a new instance of `simularimio.DisplayData` based on the params. - - Args: - name: name of agent - radius: `float` - display_type: any one of the `simulariumio.DISPLAY_TYPE` properties. Defaults to None. - obj_color: `str`: hex color of the display agent. - - Returns: - `DisplayData` instance. - """ - return DisplayData( - name=name, - radius=radius, - display_type=display_type, - color=obj_color - ) - - @staticmethod - def generate_display_data_object_dict(agent_displays: List) -> Dict[str, DisplayData]: - """Factory to generate a display object dict. - - Args: - agent_displays: `List[AgentDisplayData]`: A list of `AgentDisplayData` instances which describe the - visualized agents. - - Returns: - `Dict[str, DisplayData]` - """ - displays = {} - for agent_display in agent_displays: - key = agent_display.name - display = DisplayData( - name=agent_display.name, - radius=agent_display.radius, - display_type=agent_display.display_type, - url=agent_display.url, - color=agent_display.color - ) - displays[key] = display - return displays - - def generate_input_file_data_object(self, model_output_file: Optional[str] = None) -> InputFileData: - """Factory that generates a new instance of `simulariumio.data_model.InputFileData` based on - `self.archive.model_output_filename` (which itself is derived from the model file) if no `model_output_file` - is passed. - - Args: - model_output_file(:obj:`str`): `Optional`: file on which to base the `InputFileData` instance. - Returns: - (:obj:`InputFileData`): simulariumio input file data object based on `self.archive.model_output_filename` - - """ - model_output_file = model_output_file or self.archive.model_output_filename - return InputFileData(model_output_file) - - # IO Methods - @staticmethod - def write_simularium_file( - data: Union[SmoldynData, TrajectoryData], - simularium_filename: str, - save_format: str, - validation=True - ) -> None: - """Takes in either a `SmoldynData` or `TrajectoryData` instance and saves a simularium file based on it - with the name of `simularium_filename`. If none is passed, the file will be saved in `self.archive.rootpath` - - Args: - data(:obj:`Union[SmoldynData, TrajectoryData]`): data object to save. - simularium_filename(:obj:`str`): `Optional`: name by which to save the new simularium file. If None is - passed, will default to `self.archive.rootpath/self.archive.simularium_filename`. - save_format(:obj:`str`): format which to write the `data`. Options include `json, binary`. - validation(:obj:`bool`): whether to call the wrapped method using `validate_ids=True`. Defaults - to `True`. - """ - save_format = save_format.lower() - if not os.path.exists(simularium_filename): - if 'binary' in save_format: - writer = BinaryWriter() - elif 'json' in save_format: - writer = JsonWriter() - else: - warn('You must provide a valid writer object.') - return - return writer.save(trajectory_data=data, output_path=simularium_filename, validate_ids=validation) - - def simularium_to_json(self, data: Union[SmoldynData, TrajectoryData], simularium_filename: str, v=True) -> None: - """Write the contents of the simularium stream to a JSON Simularium file. - - Args: - data: data to write. - simularium_filename: filepath at which to write the new simularium file. - v: whether to call the wrapped method with validate_ids=True. Defaults to `True`. - """ - return self.write_simularium_file( - data=data, - simularium_filename=simularium_filename, - save_format='json', - validation=v - ) - - def simularium_to_binary(self, data: Union[SmoldynData, TrajectoryData], simularium_filename: str, v=True) -> None: - """Write the contents of the simularium stream to a Binary Simularium file. - - Args: - data: data to write. - simularium_filename: filepath at which to write the new simularium file. - v: whether to call the wrapped method with validate_ids=True. Defaults to `True`. - """ - return self.write_simularium_file( - data=data, - simularium_filename=simularium_filename, - save_format='binary', - validation=v - ) - - -class SmoldynDataConverter(BiosimulatorsDataConverter): - def __init__(self, archive: SmoldynCombineArchive, generate_model_output: bool = True): - """General class for converting Smoldyn output (modelout.txt) to .simularium. Checks the passed archive object - directory for a `modelout.txt` file (standard Smoldyn naming convention) and runs the simulation by default if - not. At the time of construction, checks for the existence of a simulation `out.txt` file and runs - `self.generate_model_output_file()` if such a file does not exist, based on `self.archive`. To turn - this off, pass `generate_data` as `False`. - - Args: - archive (:obj:`SmoldynCombineArchive`): instance of a `SmoldynCombineArchive` object. - generate_model_output(`bool`): Automatically generates and standardizes the name of a - smoldyn model output file based on the `self.archive` parameter if True. Defaults to `True`. - """ - super().__init__(archive) - if generate_model_output: - self.generate_model_output_file() - - def generate_model_output_file(self, - model_output_filename: Optional[str] = None, - smoldyn_archive: Optional[SmoldynCombineArchive] = None) -> None: - """Generate a modelout file if one does not exist using the `ModelValidation` interface via - `.utils.generate_model_validation_object` method. If either parameter is not passed, the data will - be derived from `self.archive(:obj:`SmoldynCombineArchive`)`. - - Args: - model_output_filename(:obj:`str`): `Optional`: filename from which to run a smoldyn simulation - and generate an out.txt file. Defaults to `self.archive.model_output_filename`. - smoldyn_archive(:obj:`SmoldynCombineArchive`): `Optional`: instance of `SmoldynCombineArchive` from - which to base the simulation/model.txt from. Defaults to `self.archive`. - - Returns: - None - """ - model_output_filename = model_output_filename or self.archive.model_output_filename - archive = smoldyn_archive or self.archive - if not os.path.exists(model_output_filename): - validation = archive.generate_model_validation_object() - validation.simulation.runSim() - - # standardize the modelout filename - for root, _, files in os.walk(archive.rootpath): - for f in files: - if f.endswith('.txt') and 'model' not in f: - f = os.path.join(root, f) - os.rename(f, archive.model_output_filename) - - def read_model_output_dataframe(self) -> pd.DataFrame: - """Create a pandas dataframe from the contents of `self.archive.model_output_filename`. WARNING: this method - is currently experimental. - - Returns: - `pd.DataFrame`: a pandas dataframe with the columns: ['mol_name', 'x', 'y', 'z', 't'] - """ - warn('WARNING: This method is experimental and may not function properly.') - colnames = ['mol_name', 'x', 'y', 'z', 't'] - return pd.read_csv(self.archive.model_output_filename, sep=" ", header=None, skiprows=1, names=colnames) - - def write_model_output_dataframe_to_csv(self, save_fp: str) -> None: - """Write output dataframe to csv file. - - Args: - save_fp:`str`: path at which to save the csv-converted pandas df. - """ - df = self.read_model_output_dataframe() - return df.to_csv(save_fp) - - def generate_output_data_object( - self, - file_data: InputFileData, - display_data: Optional[Dict[str, DisplayData]] = None, - meta_data: Optional[MetaData] = None, - spatial_units="nm", - temporal_units="ns", - ) -> SmoldynData: - """Generate a new instance of `SmoldynData`. If passing `meta_data`, please create a new `MetaData` instance - using the `self.generate_metadata_object` interface of this same class. - - Args: - file_data: (:obj:`InputFileData`): `simulariumio.InputFileData` instance based on model output. - display_data: (:obj:`Dict[Dict[str, DisplayData]]`): `Optional`: if passing this parameter, please - use the `self.generate_display_object_dict` interface of this same class. - meta_data: (:obj:`Metadata`): new instance of `Metadata` object. If passing this parameter, please use the - `self.generate_metadata_object` interface method of this same class. - spatial_units: (:obj:`str`): spatial units by which to measure this simularium output. Defaults to `nm`. - temporal_units: (:obj:`str`): time units to base this simularium instance on. Defaults to `ns`. - - Returns: - :obj:`SmoldynData` - """ - return SmoldynData( - smoldyn_file=file_data, - spatial_units=UnitData(spatial_units), - time_units=UnitData(temporal_units), - display_data=display_data, - meta_data=meta_data - ) - - def generate_converter(self, data: SmoldynData) -> SmoldynConverter: - """Implementation of parent-level factory which exposes an object for translating `SmoldynData` instance. - - Args: - data(`SmoldynData`): Data to be translated. - - Returns: - `SmoldynConverter` instance based on the data. - """ - return SmoldynConverter(data) - - def translate_data_object( - self, - data_object_converter: SmoldynConverter, - box_size: Union[float, int], - n_dim=3, - translation_magnitude: Optional[Union[int, float]] = None - ) -> TrajectoryData: - """Translate the data object's data if the coordinates are all positive to center the data in the - simularium viewer. - - Args: - data_object_converter: Instance of `SmoldynConverter` loaded with `SmoldynData`. - box_size: size of the simularium viewer box. - n_dim: n dimensions of the simulation output. Defaults to `3`. - translation_magnitude: magnitude by which to translate and filter. Defaults to `-box_size / 2`. - - Returns: - `TrajectoryData`: translated data object instance. - """ - translation_magnitude = translation_magnitude or -box_size / 2 - return data_object_converter.filter_data([ - TranslateFilter( - translation_per_type={}, - default_translation=translation_magnitude * np.ones(n_dim) - ), - ]) - - def generate_simularium_file( - self, - box_size=1., - spatial_units="nm", - temporal_units="s", - n_dim=3, - io_format="binary", - translate=True, - overwrite=True, - validate_ids=True, - simularium_filename: Optional[str] = None, - display_data: Optional[Dict[str, DisplayData]] = None, - new_omex_filename: Optional[str] = None, - ) -> None: - """Generate a new simularium file based on `self.archive.rootpath`. If `self.archive.rootpath` is an `.omex` - file, the outputs will be re-bundled. - - Args: - box_size(:obj:`float`): `Optional`: size by which to scale the simulation stage. Defaults to `1.` - spatial_units(:obj:`str`): `Optional`: units by which to measure the spatial aspect - of the simulation. Defaults to `nm`. - temporal_units(:obj:`str`): `Optional`: units by which to measure the temporal aspect - of the simulation. Defaults to `s`. - n_dim(:obj:`int`): `Optional`: n dimensions of the simulation output. Defaults to `3`. - simularium_filename(:obj:`str`): `Optional`: filename by which to save the simularium output. Defaults - to `archive.simularium_filename`. - display_data(:obj:`Dict[str, DisplayData]`): `Optional`: Dictionary of DisplayData objects. - new_omex_filename(:obj:`str`): `Optional`: Filename by which to save the newly generate .omex IF and - only IF `self.archive.rootpath` is an `.omex` file. - io_format(:obj:`str`): format in which to write out the simularium file. Used as an input param to call - `super.write_simularium_file`. Options include `'binary'` and `'json'`. Capitals may be used in - this string. Defaults to `binary`. - translate(:obj:`bool`): Whether to translate the simulation mirror data. Defaults to `True`. - overwrite(:obj:`bool`): Whether to overwrite a simularium file of the same name as `simularium_filename` - if one already exists in the COMBINE archive. Defaults to `True`. - validate_ids(:obj:`bool`): Whether to call the write method using `validation=True`. Defaults to True. - """ - if not simularium_filename: - simularium_filename = self.archive.simularium_filename - - if os.path.exists(simularium_filename): - warn('That file already exists in this COMBINE archive.') - if not overwrite: - warn('Overwrite is turned off an thus a new file will not be generated.') - return - - input_file = self.generate_input_file_data_object() - data = self.generate_output_data_object( - file_data=input_file, - display_data=display_data, - spatial_units=spatial_units, - temporal_units=temporal_units - ) - - if translate: - c = self.generate_converter(data) - data = self.translate_data_object(c, box_size, n_dim, translation_magnitude=box_size) - - # write the simularium file - self.write_simularium_file( - data=data, - simularium_filename=simularium_filename, - save_format=io_format, - validation=validate_ids - ) - print('New Simularium file generated!!') - - # add new file to manifest - self.archive.add_simularium_file_to_manifest(simularium_fp=simularium_filename) - - # re-zip the archive if it was passed as an omex file - if self.archive.was_unzipped: - writer = ArchiveWriter() - paths = list(self.archive.get_all_archive_filepaths().values()) - writer.run(paths, self.archive.rootpath) - print('Omex bundled!') - - -"""*Copied Objects from smoldyn.biosimulators.data_model*""" - - -class Simulation(object): - """ Configuration of a simulation - - Attributes: - species (:obj:`list` of :obj:`str`): names of species - compartments (:obj:`list` of :obj:`str`): names of compartments - surfaces (:obj:`list` of :obj:`str`): names of surfaces - instructions (:obj:`list` of :obj:`SimulationInstruction`): instructions - """ - - def __init__(self, species=None, compartments=None, surfaces=None, instructions=None): - """ - Args: - species (:obj:`list` of :obj:`str`, optional): names of species - compartments (:obj:`list` of :obj:`str`, optional): names of compartments - surfaces (:obj:`list` of :obj:`str`, optional): names of surfaces - instructions (:obj:`list` of :obj:`SimulationInstruction`, optional): instructions - """ - self.species = species or [] - self.compartments = compartments or [] - self.surfaces = surfaces or [] - self.instructions = instructions or [] - - -class SimulationInstruction(object): - """ Configuration of a simulation - - Attributes: - macro (:obj:`str`): Smoldyn macro - arguments (:obj:`str`): arguments of the Smoldyn macro - id (:obj:`str`): unique id of the instruction - description (:obj:`str`): human-readable description of the instruction - comment (:obj:`str`): comment about the instruction - """ - - def __init__(self, macro, arguments, id=None, description=None, comment=None): - """ - Args: - macro (:obj:`str`): Smoldyn macro - arguments (:obj:`str`): arguments of the Smoldyn macro - id (:obj:`str`, optional): unique id of the instruction - description (:obj:`str`, optional): human-readable description of the instruction - comment (:obj:`str`, optional): comment about the instruction - """ - self.macro = macro - self.arguments = arguments - self.id = id - self.description = description - self.comment = comment - - def is_equal(self, other): - return (self.__class__ == other.__class__ - and self.macro == other.macro - and self.arguments == other.arguments - and self.id == other.id - and self.description == other.description - and self.comment == other.comment - ) - - -class SmoldynCommand(object): - ''' A Smoldyn command - - Attributes: - command (:obj:`str`): command (e.g., ``molcount``) - type (:obj:`str`): command type (e.g., ``E``) - ''' - - def __init__(self, command, type): - ''' - Args: - command (:obj:`str`): command (e.g., ``molcount``) - type (:obj:`str`): command type (e.g., ``E``) - ''' - self.command = command - self.type = type - - -class SmoldynOutputFile(object): - ''' A Smoldyn output file - - Attributes: - name (:obj:`str`): name - filename (:obj:`str`): path to the file - ''' - - def __init__(self, name, filename): - ''' - Args: - name (:obj:`str`): name - filename (:obj:`str`): path to the file - ''' - self.name = name - self.filename = filename - - -class SimulationChange(object): - ''' A change to a Smoldyn simulation - - Attributes: - command (:obj:`types.FunctionType`): function for generating a Smoldyn configuration command for a new value - execution (:obj:`SimulationChangeExecution`): operation when change should be executed - ''' - - def __init__(self, command, execution): - ''' - Args: - command (:obj:`types.FunctionType`): function for generating a Smoldyn configuration command for a new value - execution (:obj:`SimulationChangeExecution`): operation when change should be executed - ''' - self.command = command - self.execution = execution - - -class SimulationChangeExecution(str, enum.Enum): - """ Operation when a simulation change should be executed """ - preprocessing = 'preprocessing' - simulation = 'simulation' - - -class AlgorithmParameterType(str, enum.Enum): - ''' Type of algorithm parameter ''' - run_argument = 'run_argument' - instance_attribute = 'instance_attribute' - - -KISAO_ALGORITHMS_MAP = { - 'KISAO_0000057': { - 'name': 'Brownian diffusion Smoluchowski method', - } -} - -KISAO_ALGORITHM_PARAMETERS_MAP = { - 'KISAO_0000254': { - 'name': 'accuracy', - 'type': AlgorithmParameterType.run_argument, - 'data_type': ValueType.float, - 'default': 10., - }, - 'KISAO_0000488': { - 'name': 'setRandomSeed', - 'type': AlgorithmParameterType.instance_attribute, - 'data_type': ValueType.integer, - 'default': None, - }, -} diff --git a/biosimulators_utils/spatial/exec.py b/biosimulators_utils/spatial/exec.py deleted file mode 100644 index cd443a05..00000000 --- a/biosimulators_utils/spatial/exec.py +++ /dev/null @@ -1,1031 +0,0 @@ -""" Exec methods for executing spatial tasks in SED-ML files in COMBINE/OMEX archives - -:Author: Alexander Patrie / Jonathan Karr -:Date: 2023-09-16 -:Copyright: 2023, UConn Health -:License: MIT -""" - - -import functools -import os -import numpy -import pandas -import re -import tempfile -from typing import * -import types # noqa: F401 -import pandas as pd -from smoldyn import smoldyn -from biosimulators_utils.spatial.data_model import ( - SmoldynCommand, - SmoldynOutputFile, - SimulationChange, - SimulationChangeExecution, - AlgorithmParameterType, - KISAO_ALGORITHMS_MAP, - KISAO_ALGORITHM_PARAMETERS_MAP -) -from biosimulators_utils.log.data_model import ( - CombineArchiveLog, - TaskLog, - StandardOutputErrorCapturerLevel, - SedDocumentLog -) # noqa: F401 -from biosimulators_utils.viz.data_model import VizFormat # noqa: F401 -from biosimulators_utils.report.data_model import ( - ReportFormat, # noqa: F401 - ReportResults, - VariableResults, - SedDocumentResults -) -from biosimulators_utils.sedml.data_model import ( - Task, ModelLanguage, - ModelAttributeChange, # noqa: F401 - UniformTimeCourseSimulation, - AlgorithmParameterChange, # noqa: F401 - Variable, - Symbol, - SedDocument -) -from biosimulators_utils.sedml import validation -from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive as base_exec_combine_archive -from biosimulators_utils.config import get_config, Config # noqa: F401 -from biosimulators_utils.sedml.exec import exec_sed_doc as base_exec_sed_doc -from biosimulators_utils.utils.core import validate_str_value, parse_value, raise_errors_warnings - - -__all__ = ['exec_spatial_combine_archive', 'exec_sed_task', 'exec_sed_doc', 'preprocess_sed_task'] - - -def exec_spatial_combine_archive( - archive_filename: str, - out_dir: str, - config: Optional[Config] = None - ) -> Tuple[SedDocumentResults, CombineArchiveLog]: - """ Execute the SED tasks defined in a COMBINE/OMEX archive whose contents relate to a Spatial simulation - and save the outputs. - - Args: - archive_filename (:obj:`str`): path to COMBINE/OMEX archive - out_dir (:obj:`str`): path to store the outputs of the archive - - * CSV: directory in which to save outputs to files - ``{ out_dir }/{ relative-path-to-SED-ML-file-within-archive }/{ report.id }.csv`` - * HDF5: directory in which to save a single HDF5 file (``{ out_dir }/reports.h5``), - with reports at keys ``{ relative-path-to-SED-ML-file-within-archive }/{ report.id }`` within the HDF5 file - - config (:obj:`Config`, optional): BioSimulators common configuration - - Returns: - :obj:`tuple`: - - * :obj:`SedDocumentResults`: results - * :obj:`CombineArchiveLog`: log - """ - return base_exec_combine_archive( - exec_sed_doc, - archive_filename, - out_dir, - apply_xml_model_changes=False, - config=config - ) - - -def exec_sed_doc( - doc: Union[SedDocument, str], - working_dir: str, - base_out_path: str, - apply_xml_model_changes=True, - indent=0, - pretty_print_modified_xml_models=False, - log_level=StandardOutputErrorCapturerLevel.c, - log: Optional[SedDocumentLog] = None, - rel_out_path: Optional[str] = None, - config: Optional[Config] = None - ) -> Tuple[ReportResults, SedDocumentLog]: - """ Execute the tasks specified in a SED document and generate the specified outputs - - Args: - doc (:obj:`SedDocument` or :obj:`str`): SED document or a path to SED-ML file which defines a SED document - working_dir (:obj:`str`): working directory of the SED document (path relative to which models are located) - - base_out_path (:obj:`str`): path to store the outputs - - * CSV: directory in which to save outputs to files - ``{base_out_path}/{rel_out_path}/{report.id}.csv`` - * HDF5: directory in which to save a single HDF5 file (``{base_out_path}/reports.h5``), - with reports at keys ``{rel_out_path}/{report.id}`` within the HDF5 file - - rel_out_path (:obj:`str`, optional): path relative to :obj:`base_out_path` to store the outputs - apply_xml_model_changes (:obj:`bool`, optional): if :obj:`True`, apply any model changes specified in - the SED-ML file before calling :obj:`task_executer`. - log (:obj:`SedDocumentLog`, optional): log of the document - indent (:obj:`int`, optional): degree to indent status messages - pretty_print_modified_xml_models (:obj:`bool`, optional): if :obj:`True`, pretty print modified XML models - log_level (:obj:`StandardOutputErrorCapturerLevel`, optional): level at which to log output - config (:obj:`Config`, optional): BioSimulators common configuration - - Returns: - :obj:`tuple`: - - * :obj:`ReportResults`: results of each report - * :obj:`SedDocumentLog`: log of the document - """ - return base_exec_sed_doc( - exec_sed_task, - doc, - working_dir, - base_out_path, - rel_out_path=rel_out_path, - apply_xml_model_changes=apply_xml_model_changes, - log=log, - indent=indent, - pretty_print_modified_xml_models=pretty_print_modified_xml_models, - log_level=log_level, - config=config - ) - - -# noinspection PyShadowingNames -def exec_sed_task( - task: Task, - variables: List[Variable], - preprocessed_task: Optional[Dict] = None, - log: Optional[TaskLog] = None, - config: Optional[Config] = None - ) -> Tuple[VariableResults, TaskLog]: - ''' Execute a task and save its results - - Args: - task (:obj:`Task`): task - variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - preprocessed_task (:obj:`dict`, optional): preprocessed information about the task, including possible - model changes and variables. This can be used to avoid repeatedly executing the same initialization - for repeated calls to this method. - log (:obj:`TaskLog`, optional): log for the task - config (:obj:`Config`, optional): BioSimulators common configuration - - Returns: - :obj:`tuple`: - - :obj:`VariableResults`: results of variables - :obj:`TaskLog`: log - ''' - if not config: - config = get_config() - - if config.LOG and not log: - log = TaskLog() - - sed_model_changes = task.model.changes - sed_simulation = task.simulation - - if preprocessed_task is None: - preprocessed_task = preprocess_sed_task(task, variables, config=config) - sed_model_changes = list(filter( - lambda change: change.target in preprocessed_task['sed_smoldyn_simulation_change_map'], - sed_model_changes - )) - - # read Smoldyn configuration - smoldyn_simulation = preprocessed_task['simulation'] - - # apply model changes to the Smoldyn configuration - sed_smoldyn_simulation_change_map = preprocessed_task['sed_smoldyn_simulation_change_map'] - for change in sed_model_changes: - smoldyn_change = sed_smoldyn_simulation_change_map.get(change.target, None) - if smoldyn_change is None or smoldyn_change.execution != SimulationChangeExecution.simulation: - raise NotImplementedError( - 'Target `{}` can only be changed during simulation preprocessing.'.format(change.target) - ) - apply_change_to_smoldyn_simulation( - smoldyn_simulation, change, smoldyn_change) - - # get the Smoldyn representation of the SED uniform time course simulation - smoldyn_simulation_run_timecourse_args = get_smoldyn_run_timecourse_args(sed_simulation) - - # execute the simulation - smoldyn_run_args = dict( - **smoldyn_simulation_run_timecourse_args, - **preprocessed_task['simulation_run_alg_param_args'], - ) - smoldyn_simulation.run(**smoldyn_run_args, overwrite=True, display=False, quit_at_end=False) - - # get the result of each SED variable - variable_output_cmd_map = preprocessed_task['variable_output_cmd_map'] - smoldyn_output_files = preprocessed_task['output_files'] - variable_results = get_variable_results( - sed_simulation.number_of_steps, - variables, - variable_output_cmd_map, - smoldyn_output_files - ) - - # cleanup output files - for smoldyn_output_file in smoldyn_output_files.values(): - os.remove(smoldyn_output_file.filename) - - # log simulation - if config.LOG: - log.algorithm = sed_simulation.algorithm.kisao_id - log.simulator_details = { - 'class': 'smoldyn.Simulation', - 'instanceAttributes': preprocessed_task['simulation_attrs'], - 'method': 'run', - 'methodArguments': smoldyn_run_args, - } - - # return the values of the variables and log - return variable_results, log - - -def preprocess_sed_task(task, variables, config=None): - """ Preprocess a SED task, including its possible model changes and variables. This is useful for avoiding - repeatedly initializing tasks on repeated calls of :obj:`exec_sed_task`. - - Args: - task (:obj:`Task`): task - variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - config (:obj:`Config`, optional): BioSimulators common configuration - - Returns: - :obj:`dict`: preprocessed information about the task - """ - config = config or get_config() - - sed_model = task.model - sed_simulation = task.simulation - - if config.VALIDATE_SEDML: - raise_errors_warnings(validation.validate_task(task), - error_summary='Task `{}` is invalid.'.format(task.id)) - raise_errors_warnings(validation.validate_model_language(sed_model.language, ModelLanguage.Smoldyn), - error_summary='Language for model `{}` is not supported.'.format(sed_model.id)) - raise_errors_warnings(validation.validate_model_change_types(sed_model.changes, - (ModelAttributeChange, )), - error_summary='Changes for model `{}` are not supported.'.format(sed_model.id)) - raise_errors_warnings(*validation.validate_model_changes(sed_model), - error_summary='Changes for model `{}` are invalid.'.format(sed_model.id)) - raise_errors_warnings(validation.validate_simulation_type(sed_simulation, - (UniformTimeCourseSimulation, )), - error_summary='{} `{}` is not supported.'.format(sed_simulation.__class__.__name__, - sed_simulation.id)) - raise_errors_warnings(*validation.validate_simulation(sed_simulation), - error_summary='Simulation `{}` is invalid.'.format(sed_simulation.id)) - raise_errors_warnings(*validation.validate_data_generator_variables(variables), - error_summary='Data generator variables for task `{}` are invalid.'.format(task.id)) - - if sed_simulation.algorithm.kisao_id not in KISAO_ALGORITHMS_MAP: - msg = 'Algorithm `{}` is not supported. The following algorithms are supported:{}'.format( - sed_simulation.algorithm.kisao_id, - ''.join('\n {}: {}'.format(kisao_id, alg_props['name']) - for kisao_id, alg_props in KISAO_ALGORITHMS_MAP.items()) - ) - raise NotImplementedError(msg) - - # read Smoldyn configuration - simulation_configuration = read_smoldyn_simulation_configuration(sed_model.source) - normalize_smoldyn_simulation_configuration(simulation_configuration) - - # turn off Smoldyn's graphics - disable_smoldyn_graphics_in_simulation_configuration(simulation_configuration) - - # preprocess model changes - sed_smoldyn_preprocessing_change_map = {} - sed_smoldyn_simulation_change_map = {} - for change in sed_model.changes: - smoldyn_change = validate_model_change(change) - - if smoldyn_change.execution == SimulationChangeExecution.preprocessing: - sed_smoldyn_preprocessing_change_map[change] = smoldyn_change - else: - sed_smoldyn_simulation_change_map[change.target] = smoldyn_change - - # apply preprocessing-time changes - for change, smoldyn_change in sed_smoldyn_preprocessing_change_map.items(): - apply_change_to_smoldyn_simulation_configuration( - simulation_configuration, change, smoldyn_change) - - # write the modified Smoldyn configuration to a temporary file - fid, smoldyn_configuration_filename = tempfile.mkstemp(suffix='.txt') - os.close(fid) - write_smoldyn_simulation_configuration(simulation_configuration, smoldyn_configuration_filename) - - # initialize a simulation from the Smoldyn file - smoldyn_simulation = init_smoldyn_simulation_from_configuration_file(smoldyn_configuration_filename) - - # clean up temporary file - os.remove(smoldyn_configuration_filename) - - # apply the SED algorithm parameters to the Smoldyn simulation and to the arguments to its ``run`` method - smoldyn_simulation_attrs = {} - smoldyn_simulation_run_alg_param_args = {} - for sed_algorithm_parameter_change in sed_simulation.algorithm.changes: - val = get_smoldyn_instance_attr_or_run_algorithm_parameter_arg(sed_algorithm_parameter_change) - if val['type'] == AlgorithmParameterType.run_argument: - smoldyn_simulation_run_alg_param_args[val['name']] = val['value'] - else: - smoldyn_simulation_attrs[val['name']] = val['value'] - - # apply the SED algorithm parameters to the Smoldyn simulation and to the arguments to its ``run`` method - for attr_name, value in smoldyn_simulation_attrs.items(): - setter = getattr(smoldyn_simulation, attr_name) - setter(value) - - # validate SED variables - variable_output_cmd_map = validate_variables(variables) - - # Setup Smoldyn output files for the SED variables - smoldyn_configuration_dirname = os.path.dirname(smoldyn_configuration_filename) - smoldyn_output_files = add_smoldyn_output_files_for_sed_variables( - smoldyn_configuration_dirname, variables, variable_output_cmd_map, smoldyn_simulation) - - # return preprocessed information - return { - 'simulation': smoldyn_simulation, - 'simulation_attrs': smoldyn_simulation_attrs, - 'simulation_run_alg_param_args': smoldyn_simulation_run_alg_param_args, - 'sed_smoldyn_simulation_change_map': sed_smoldyn_simulation_change_map, - 'variable_output_cmd_map': variable_output_cmd_map, - 'output_files': smoldyn_output_files, - } - - -def init_smoldyn_simulation_from_configuration_file(filename): - ''' Initialize a simulation for a Smoldyn model from a file - - Args: - filename (:obj:`str`): path to model file - - Returns: - :obj:`smoldyn.Simulation`: simulation - ''' - if not os.path.isfile(filename): - raise FileNotFoundError('Model source `{}` is not a file.'.format(filename)) - - smoldyn_simulation = smoldyn.Simulation.fromFile(filename) - if not smoldyn_simulation.getSimPtr(): - error_code, error_msg = smoldyn.getError() - msg = 'Model source `{}` is not a valid Smoldyn file.\n\n {}: {}'.format( - filename, - error_code.name[0].upper() + error_code.name[1:], error_msg.replace('\n', '\n ')) - raise ValueError(msg) - - return smoldyn_simulation - - -def read_smoldyn_simulation_configuration(filename): - ''' Read a configuration for a Smoldyn simulation - - Args: - filename (:obj:`str`): path to model file - - Returns: - :obj:`list` of :obj:`str`: simulation configuration - ''' - with open(filename, 'r') as file: - return [line.strip('\n') for line in file] - - -def write_smoldyn_simulation_configuration(configuration, filename): - ''' Write a configuration for Smoldyn simulation to a file - - Args: - configuration - filename (:obj:`str`): path to save configuration - ''' - with open(filename, 'w') as file: - for line in configuration: - file.write(line) - file.write('\n') - - -def normalize_smoldyn_simulation_configuration(configuration): - ''' Normalize a configuration for a Smoldyn simulation - - Args: - configuration (:obj:`list` of :obj:`str`): configuration for a Smoldyn simulation - ''' - # normalize spacing and comments - for i_line, line in enumerate(configuration): - if '#' in line: - cmd, comment = re.split('#+', line, maxsplit=1) - cmd = re.sub(' +', ' ', cmd).strip() - comment = comment.strip() - - if cmd: - if comment: - line = cmd + ' # ' + comment - else: - line = cmd - else: - if comment: - line = '# ' + comment - else: - line = '' - - else: - line = re.sub(' +', ' ', line).strip() - - configuration[i_line] = line - - # remove end_file and following lines - for i_line, line in enumerate(configuration): - if re.match(r'^end_file( |$)', line): - for i_line_remove in range(len(configuration) - i_line): - configuration.pop() - break - - # remove empty starting lines - for line in list(configuration): - if not line: - configuration.pop(0) - else: - break - - # remove empty ending lines - for line in reversed(configuration): - if not line: - configuration.pop() - else: - break - - -def disable_smoldyn_graphics_in_simulation_configuration(configuration): - ''' Turn off graphics in the configuration of a Smoldyn simulation - - Args: - configuration (:obj:`list` of :obj:`str`): simulation configuration - ''' - for i_line, line in enumerate(configuration): - if line.startswith('graphics '): - configuration[i_line] = re.sub(r'^graphics +[a-z_]+', 'graphics none', line) - - -def validate_model_change(sed_model_change: ModelAttributeChange) -> SimulationChange: - ''' Validate a SED model attribute change to a configuration for a Smoldyn simulation - - ==================================================================== =================== - target newValue - ==================================================================== =================== - ``define {name}`` float - ``difc {species}`` float - ``difc {species}({state})`` float - ``difc_rule {species}({state})`` float - ``difm {species}`` float[] - ``difm {species}({state})`` float[] - ``difm_rule {species}({state})`` float[] - ``drift {species}`` float[] - ``drift {species}({state})`` float[] - ``drift_rule {species}({state})`` float[] - ``surface_drift {species}({state}) {surface} {panel-shape}`` float[] - ``surface_drift_rule {species}({state}) {surface} {panel-shape}`` float[] - ``killmol {species}({state})`` 0 - ``killmolprob {species}({state}) {prob}`` 0 - ``killmolincmpt {species}({state}) {compartment}`` 0 - ``killmolinsphere {species}({state}) {surface}`` 0 - ``killmoloutsidesystem {species}({state})`` 0 - ``fixmolcount {species}({state})`` integer - ``fixmolcountincmpt {species}({staet}) {compartment}`` integer - ``fixmolcountonsurf {species}({state}) {surface}`` integer - ==================================================================== =================== - - Args: - sed_model_change (:obj:`ModelAttributeChange`): SED model change - - Returns: - :obj:`SimulationChange`: Smoldyn representation of the model change - - Raises: - :obj:`NotImplementedError`: unsupported model change - ''' - # TODO: support additional types of model changes - - target_type, _, target = sed_model_change.target.strip().partition(' ') - target_type = target_type.strip() - target = re.sub(r' +', ' ', target).strip() - - if target_type in [ - 'killmol', 'killmolprob', 'killmolinsphere', 'killmolincmpt', 'killmoloutsidesystem', - ]: - # Examples: - # killmol red - def new_line_func(new_value): - return target_type + ' ' + target - execution = SimulationChangeExecution.simulation - - elif target_type in [ - 'fixmolcount', 'fixmolcountonsurf', 'fixmolcountincmpt', - ]: - # Examples: - # fixmolcount red - species_name, species_target_sep, target = target.partition(' ') - - def new_line_func(new_value): - return target_type + ' ' + species_name + ' ' + new_value + species_target_sep + target - - execution = SimulationChangeExecution.simulation - - elif target_type in [ - 'define', 'difc', 'difc_rule', 'difm', 'difm_rule', - 'drift', 'drift_rule', 'surface_drift', 'surface_drift_rule', - ]: - # Examples: - # define K_FWD 0.001 - # difc S 3 - # difm red 1 0 0 0 0 0 0 0 2 - def new_line_func(new_value): - return target_type + ' ' + target + ' ' + new_value - - execution = SimulationChangeExecution.preprocessing - - # elif target_type in [ - # 'mol', 'compartment_mol', 'surface_mol', - # ]: - # # Examples: - # # mol 5 red u - # # compartment_mol 500 S inside - # # surface_mol 100 E(front) membrane all all - # def new_line_func(new_value): - # return target_type + ' ' + new_value + ' ' + target - # - # execution = SimulationChangeExecution.preprocessing - - # elif target_type in [ - # 'dim', - # ]: - # # Examples: - # # dim 1 - # def new_line_func(new_value): - # return target_type + ' ' + new_value - # - # execution = SimulationChangeExecution.preprocessing - - # elif target_type in [ - # 'boundaries', 'low_wall', 'high_wall', - # ]: - # # Examples: - # # low_wall x -10 - # # high_wall y 10 - # def new_line_func(new_value): - # return target_type + ' ' + target + ' ' + new_value - # - # execution = SimulationChangeExecution.preprocessing - - else: - raise NotImplementedError('Target `{}` is not supported.'.format(sed_model_change.target)) - - return SimulationChange(new_line_func, execution) - - -def apply_change_to_smoldyn_simulation(smoldyn_simulation, sed_change, smoldyn_change): - ''' Apply a SED model attribute change to a Smoldyn simulation - - Args: - smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation - sed_change (:obj:`ModelAttributeChange`): SED model change - smoldyn_change (:obj:`SimulationChange`): Smoldyn representation of the model change - ''' - new_value = str(sed_change.new_value).strip() - new_line = smoldyn_change.command(new_value) - smoldyn_simulation.addCommand(new_line, 'b') - - -def apply_change_to_smoldyn_simulation_configuration(smoldyn_simulation_configuration, sed_change, smoldyn_change): - ''' Apply a SED model attribute change to a configuration for a Smoldyn simulation - - Args: - smoldyn_simulation_configuration (:obj:`list` of :obj:`str`): configuration for the Smoldyn simulation - sed_change (:obj:`ModelAttributeChange`): SED model change - smoldyn_change (:obj:`SimulationChange`): Smoldyn representation of the model change - ''' - new_value = str(sed_change.new_value).strip() - new_line = smoldyn_change.command(new_value) - smoldyn_simulation_configuration.insert(0, new_line) - - -def get_smoldyn_run_timecourse_args(sed_simulation): - ''' Get the Smoldyn representation of a SED uniform time course simulation - - Args: - sed_simulation (:obj:`UniformTimeCourseSimulation`): SED uniform time course simulation - - Returns: - :obj:`dict`: dictionary with keys ``start``, ``stop``, and ``dt`` that captures the Smoldyn - representation of the time course - - Raises: - :obj:`NotImplementedError`: unsupported timecourse - ''' - number_of_steps = ( - ( - sed_simulation.output_end_time - sed_simulation.initial_time - ) / ( - sed_simulation.output_end_time - sed_simulation.output_start_time - ) * ( - sed_simulation.number_of_steps - ) - ) - if (number_of_steps - int(number_of_steps)) > 1e-8: - msg = ( - 'Simulations must specify an integer number of steps, not {}.' - '\n Initial time: {}' - '\n Output start time: {}' - '\n Output end time: {}' - '\n Number of steps (output start to end time): {}' - ).format( - number_of_steps, - sed_simulation.initial_time, - sed_simulation.output_start_time, - sed_simulation.output_end_time, - sed_simulation.number_of_steps, - ) - raise NotImplementedError(msg) - - dt = (sed_simulation.output_end_time - sed_simulation.output_start_time) / sed_simulation.number_of_steps - - return { - 'start': sed_simulation.initial_time, - 'stop': sed_simulation.output_end_time, - 'dt': dt, - } - - -def get_smoldyn_instance_attr_or_run_algorithm_parameter_arg(sed_algorithm_parameter_change): - ''' Get the Smoldyn representation of a SED uniform time course simulation - - Args: - sed_algorithm_parameter_change (:obj:`AlgorithmParameterChange`): SED change to a parameter of an algorithm - - Returns: - :obj:`dict`: dictionary with keys ``type``, ``name``, and ``value`` that captures the Smoldyn representation - of the algorithm parameter - - Raises: - :obj:`ValueError`: unsupported algorithm parameter value - :obj:`NotImplementedError`: unsupported algorithm parameter - ''' - parameter_props = KISAO_ALGORITHM_PARAMETERS_MAP.get(sed_algorithm_parameter_change.kisao_id, None) - if parameter_props: - if not validate_str_value(sed_algorithm_parameter_change.new_value, parameter_props['data_type']): - msg = '{} ({}) must be a {}, not `{}`.'.format( - parameter_props['name'], sed_algorithm_parameter_change.kisao_id, - parameter_props['data_type'].name, sed_algorithm_parameter_change.new_value, - ) - raise ValueError(msg) - new_value = parse_value(sed_algorithm_parameter_change.new_value, parameter_props['data_type']) - - return { - 'type': parameter_props['type'], - 'name': parameter_props['name'], - 'value': new_value, - } - - else: - msg = 'Algorithm parameter `{}` is not supported. The following parameters are supported:{}'.format( - sed_algorithm_parameter_change.kisao_id, - ''.join('\n {}: {}'.format(kisao_id, parameter_props['name']) - for kisao_id, parameter_props in KISAO_ALGORITHM_PARAMETERS_MAP.items()) - ) - raise NotImplementedError(msg) - - -def add_smoldyn_output_file(configuration_dirname, smoldyn_simulation): - ''' Add an output file to a Smoldyn simulation - - Args: - configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file - for the simulation - smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation - - Returns: - :obj:`SmoldynOutputFile`: output file - ''' - fid, filename = tempfile.mkstemp(dir=configuration_dirname, suffix='.ssv') - os.close(fid) - name = os.path.relpath(filename, configuration_dirname) - smoldyn_simulation.setOutputFile(name, append=False) - smoldyn_simulation.setOutputPath('./') - return SmoldynOutputFile(name=name, filename=filename) - - -def add_commands_to_smoldyn_output_file(simulation, output_file, commands): - ''' Add commands to a Smoldyn output file - - Args: - smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation - smoldyn_output_file (:obj:`SmoldynOutputFile`): Smoldyn output file - commands (:obj:`list` of :obj`SmoldynCommand`): Smoldyn commands - ''' - for command in commands: - simulation.addCommand(command.command + ' ' + output_file.name, command.type) - - -def validate_variables(variables) -> Dict: - """ Generate a dictionary that maps variable targets and symbols to Smoldyn output commands. - - Args: - variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - - Returns: - :obj:`dict`: dictionary that maps variable targets and symbols to Smoldyn output commands - """ - # TODO: support additional kinds of outputs - - variable_output_cmd_map = {} - - invalid_symbols = [] - invalid_targets = [] - - for variable in variables: - if variable.symbol: - if variable.symbol == Symbol.time.value: - output_command_args = 'molcount' - include_header = True - shape = None - results_slicer = functools.partial(results_key_slicer, key='time') - - else: - invalid_symbols.append('{}: {}'.format(variable.id, variable.symbol)) - output_command_args = None - include_header = None - - else: - output_command, _, output_args = re.sub(r' +', ' ', variable.target).partition(' ') - - if output_command in ['molcount', 'molcountinbox', 'molcountincmpt', 'molcountincmpt2', 'molcountonsurf']: - species_name, _, output_args = output_args.partition(' ') - output_command_args = output_command + ' ' + output_args - include_header = True - shape = None - results_slicer = functools.partial(results_key_slicer, key=species_name) - - elif output_command in ['molcountspecies']: - output_command_args = output_command + ' ' + output_args - include_header = False - shape = None - results_slicer = results_array_slicer - - elif output_command in ['molcountspace', 'molcountspaceradial', - 'molcountspacepolarangle', 'radialdistribution', 'radialdistribution2']: - output_command_args = output_command + ' ' + output_args + ' 0' - include_header = False - shape = None - results_slicer = results_matrix_slicer - - elif output_command in ['molcountspace2d']: - output_command_args = output_command + ' ' + output_args + ' 0' - include_header = False - output_args_list = output_args.split(' ') - if len(output_args_list) == 8: - shape = (int(output_args_list[-4]), int(output_args_list[-1])) - else: - shape = (int(output_args_list[-6]), int(output_args_list[-3])) - results_slicer = None - - else: - invalid_targets.append('{}: {}'.format(variable.id, variable.target)) - output_command_args = None - - if output_command_args is not None: - output_command_args = output_command_args.strip() - variable_output_cmd_map[(variable.target, variable.symbol)] = ( - output_command_args, - include_header, - shape, - results_slicer - ) - - if invalid_symbols: - msg = '{} symbols cannot be recorded:\n {}\n\nThe following symbols can be recorded:\n {}'.format( - len(invalid_symbols), - '\n '.join(sorted(invalid_symbols)), - '\n '.join(sorted([Symbol.time.value])), - ) - raise ValueError(msg) - - if invalid_targets: - valid_target_output_commands = [ - 'molcount', - - 'molcount', 'molcountinbox', 'molcountincmpt', 'molcountincmpt2', 'molcountonsurf', - - 'molcountspace', 'molcountspaceradial', - 'molcountspacepolarangle', 'radialdistribution', 'radialdistribution2', - - 'molcountspace2d', - ] - - msg = '{} targets cannot be recorded:\n {}\n\nTargets are supported for the following output commands:\n {}'\ - .format( - len(invalid_targets), - '\n '.join(sorted(invalid_targets)), - '\n '.join(sorted(set(valid_target_output_commands))), - ) - raise NotImplementedError(msg) - - return variable_output_cmd_map - - -def results_key_slicer(results, key): - """ Get the results for a key from a set of results - - Args: - results (:obj:`pandas.DataFrame`): set of results - - Returns: - :obj:`pandas.DataFrame`: results for a key - """ - return results.get(key, None) - - -def results_array_slicer(results): - """ Extract an array of results from a matrix of time and results - - Args: - results (:obj:`pandas.DataFrame`): matrix of time and results - - Returns: - :obj:`pandas.DataFrame`: results - """ - return results.iloc[:, 1] - - -def results_matrix_slicer(results): - """ Extract a matrix array of results from a matrix of time and results - - Args: - results (:obj:`pandas.DataFrame`): matrix of time and results - - Returns: - :obj:`pandas.DataFrame`: results - """ - return results.iloc[:, 1:] - - -def add_smoldyn_output_files_for_sed_variables( - configuration_dirname, - variables, - variable_output_cmd_map, - smoldyn_simulation - ) -> Dict[str, SmoldynOutputFile]: - ''' Add Smoldyn output files for capturing each SED variable - - Args: - configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration file for the simulation - variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - variable_output_cmd_map (:obj:`dict`): dictionary that maps variable targets and symbols to Smoldyn output commands - smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation - - Returns: - :obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`: Smoldyn output files - ''' - smoldyn_output_files = {} - - output_cmds = set() - for variable in variables: - output_cmds.add(variable_output_cmd_map[(variable.target, variable.symbol)]) - - for command, include_header, _, _ in output_cmds: - add_smoldyn_output_file_for_output(configuration_dirname, smoldyn_simulation, - command, include_header, - smoldyn_output_files) - # return output files - return smoldyn_output_files - - -def add_smoldyn_output_file_for_output( - configuration_dirname: str, - smoldyn_simulation: smoldyn.Simulation, - smoldyn_output_command: str, - include_header: bool, - smoldyn_output_files: Dict[str, SmoldynOutputFile] - ) -> None: - ''' Add a Smoldyn output file for molecule counts - - Args: - configuration_dirname (:obj:`str`): path to the parent directory of the Smoldyn configuration - file for the simulation - smoldyn_simulation (:obj:`smoldyn.Simulation`): Smoldyn simulation - smoldyn_output_command (:obj:`str`): Smoldyn output command (e.g., ``molcount``) - include_header (:obj:`bool`): whether to include a header - smoldyn_output_files (:obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`): Smoldyn output files - ''' - smoldyn_output_files[smoldyn_output_command] = add_smoldyn_output_file(configuration_dirname, smoldyn_simulation) - - commands = [] - if include_header: - commands.append(SmoldynCommand('molcountheader', 'B')) - commands.append(SmoldynCommand(smoldyn_output_command, 'E')) - - add_commands_to_smoldyn_output_file( - smoldyn_simulation, - smoldyn_output_files[smoldyn_output_command], - commands, - ) - - -def get_variable_results( - number_of_steps: int, - variables: List[Variable], - variable_output_cmd_map: Dict, - smoldyn_output_files: Dict[str, SmoldynOutputFile] - ) -> VariableResults: - ''' Get the result of each SED variable - - Args: - number_of_steps (:obj:`int`): number of steps - variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - variable_output_cmd_map (:obj:`dict`): dictionary that maps variable targets and symbols to Smoldyn output - commands - smoldyn_output_files (:obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`): Smoldyn output files - - Returns: - :obj:`VariableResults`: result of each SED variable - - Raises: - :obj:`ValueError`: unsupported results - ''' - smoldyn_results = {} - - missing_variables = [] - - variable_results = VariableResults() - for variable in variables: - output_command_args, _, shape, results_slicer = variable_output_cmd_map[(variable.target, variable.symbol)] - variable_result = get_smoldyn_output( - output_command_args, - True, - shape, - smoldyn_output_files, - smoldyn_results - ) - if results_slicer: - variable_result = results_slicer(variable_result) - - if variable_result is None: - missing_variables.append('{}: {}: {}'.format(variable.id, 'target', variable.target)) - else: - if variable_result.ndim == 1: - variable_results[variable.id] = variable_result.to_numpy()[-(number_of_steps + 1):, ] - elif variable_result.ndim == 2: - variable_results[variable.id] = variable_result.to_numpy()[-(number_of_steps + 1):, :] - else: - variable_results[variable.id] = variable_result[-(number_of_steps + 1):, :, :] - - if missing_variables: - msg = '{} variables could not be recorded:\n {}'.format( - len(missing_variables), - '\n '.join(missing_variables), - ) - raise ValueError(msg) - - return variable_results - - -def get_smoldyn_output( - smoldyn_output_command: str, - has_header: bool, - three_d_shape: Tuple[int], - smoldyn_output_files: Dict[str, SmoldynOutputFile], - smoldyn_results: Dict - ) -> pd.DataFrame: - ''' Get the simulated count of each molecule - - Args: - smoldyn_output_command (:obj:`str`): Smoldyn output command (e.g., ``molcount``) - has_header (:obj:`bool`): whether to include a header - three_d_shape (:obj:`tuple` of :obj:`int`): dimensions of the output - smoldyn_output_files (:obj:`dict` of :obj:`str` => :obj:`SmoldynOutputFile`): Smoldyn output files - smoldyn_results (:obj:`dict`) - - Returns: - :obj:`pandas.DataFrame` or :obj:`numpy.ndarray`: results - ''' - smoldyn_output_command = smoldyn_output_command.strip() - if smoldyn_output_command not in smoldyn_results: - smoldyn_output_file = smoldyn_output_files[smoldyn_output_command] - if three_d_shape: - with open(smoldyn_output_file.filename, 'r') as file: - data_list = [] - - i_line = 0 - for line in file: - if i_line % (three_d_shape[1] + 1) == 0: - time_point_data = [] - else: - profile = [int(el) for el in line.split(' ')] - time_point_data.append(profile) - - if i_line % (three_d_shape[1] + 1) == three_d_shape[1]: - data_list.append(time_point_data) - - i_line += 1 - - smoldyn_results[smoldyn_output_command] = numpy.array(data_list).transpose((0, 2, 1)) - - else: - smoldyn_results[smoldyn_output_command] = pandas.read_csv(smoldyn_output_file.filename, sep=' ') - - return smoldyn_results[smoldyn_output_command] diff --git a/biosimulators_utils/spatial/io.py b/biosimulators_utils/spatial/io.py deleted file mode 100644 index 5cd98fc8..00000000 --- a/biosimulators_utils/spatial/io.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Methods for writing and reading simularium files within COMBINE/OMEX archives. - -:Author: Alexander Patrie -:Date: 2023-09-16 -:Copyright: 2023, UConn Health -:License: MIT -""" - - -from typing import Optional -from biosimulators_utils.spatial.data_model import SmoldynCombineArchive, SmoldynDataConverter -from biosimulators_utils.spatial.utils import generate_model_validation_object - - -__all__ = [ - 'generate_new_simularium_file', -] - - -# pragma: no cover -def generate_new_simularium_file( - archive_rootpath: str, - simularium_filename: Optional[str] = None, - save_output_df: bool = False, - io_format: str = 'json', - __fmt: str = 'smoldyn' - ) -> None: - """Generate a new `.simularium` file based on the `model.txt` within the passed-archive rootpath using the above - validation method. Raises a `ValueError` if there are errors present. - - Args: - archive_rootpath (:obj:`str`): Parent dirpath relative to the model.txt file. - simularium_filename (:obj:`str`): `Optional`: Desired save name for the simularium file to be saved - in the `archive_rootpath`. Defaults to `None`. - save_output_df (:obj:`bool`): Whether to save the modelout.txt contents as a pandas df in csv form. Defaults - to `False`. - io_format (:obj:`str`): format by which to save the simularium file. Options are `'binary'` and `'json'`. - defaults to `'json'`. - __fmt (:obj:`str`): format by which to convert and save the simularium file. Currently, only 'smoldyn' is - supported. Defaults to `'smoldyn'`. - """ - - # verify smoldyn combine archive - if 'smoldyn' in __fmt.lower(): - archive = SmoldynCombineArchive(rootpath=archive_rootpath, simularium_filename=simularium_filename) - - # store and parse model data - model_validation = generate_model_validation_object(archive) - if model_validation.errors: - raise ValueError( - f'There are errors involving your model file:\n{model_validation.errors}\nPlease adjust your model file.' - ) - - # construct converter and save - converter = SmoldynDataConverter(archive) - - if save_output_df: - df = converter.read_model_output_dataframe() - csv_fp = archive.model_output_filename.replace('txt', 'csv') - df.to_csv(csv_fp) - return converter.generate_simularium_file( - simularium_filename=simularium_filename, - io_format=io_format - ) - else: - raise ValueError('The only currently available format is "smoldyn".') - \ No newline at end of file diff --git a/biosimulators_utils/spatial/minE.omex b/biosimulators_utils/spatial/minE.omex deleted file mode 100644 index c2bdf085bd1a5a5fa8cd2a049273aea0b26bcfdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2785 zcmZ{mXHXMb8iqq}VL=2hMT#K3mmCfD%9tutAqt>tGj#2B`r6 z05br`*k6rr^ zpu0Dv!i~va_=W8dF{NICUR#}l8pvXGWG_e5z5Ldv3^5ff5x{97Ri7W3t&mdTGfd~g z2_bF`jz&t-ze+b4NLDT{8J5%6#CkDWwya59X#!)#B)fKBARG)|$8HT)?kLdDZs*2- z`rdlpFuoASx2Utf#L4I=4zZ>htGKwmJdMy)&)v;Y$T%P4RChwAdy1a9gnm*PWXAivV$rQso!Wu>UCdH}Ap;zf~UX78` zs42Phxke1CsjBB$5jh(#(bCy(FcdwXSBbWTjW?ZS|dLI6-}? z-yD}cu)qJk1N2Sykz6}vxp?c%E3ZEZ=~gp+PuT8ln5fmE*wfu^h)hFwHbf0-D{xQp zAr!mfQ5sy$x|NdqMHx1x+lu{?tBgeX!lg&q=~*ivArw_$PdO!vi0R@iMIA=nWE}5T zMSW;weUCGv6A(jRF$4s^PTG_%baV?G$hRw@K`*wrcCyoi73bGB-k5~Ne-^lIhO9`= z6q?Df55m60skY>^S`D=)d}W)mvkAGdq{_7FaQjY}XX*`O<&U?^r2I`pcY5-m_{e&i zzAu_Co@%PDQ?uiB_aFi3JvD`w22V}SVG2jJT37A63;6LX9K<%Bkt@t|iC-65LL@?l z`ZD)7W?Bbp-H+V@@`>cewOcUIhd`wuI9s-HUOkpTAevH@s59GAbqmyE-a65tZd&br z=M*RN@Tjffh;-U6u9K`&w@sGw?wjpPlWCrw63#*-tHayNFvGy`O2iAwSANLc=Ga>y zdA4`Tr(d3-*^&IA1FWW@S?pc~+Cj^T+<2#kO_uFTA&&SVDi*sm3o`di!7BEN)~)9z zB~hLn%DwPeL-R?|_i*@}j@qnQN9J{3rOO%Uf7l+fuZ3dj zfqK02VXkAHD0JZNd$dcCb{wbv;o36VGye!vglrN_FV6DLUZ+Q7KzUq=uh=}imXcJ= zm;A(fB#Y|APwtEPViX{Tb3oy8rcGP|5dN4~w)RV#Q^--+-pV&aUX}e|5`jGbftIcH zX1?2*w`t4leL>h?#FKVs<5cB(F;W`z&HDMr>8WzV)K9Kl-how~!q~{$@N3s9VYS=_1zkZ;25;4l7TV>}HDLd=Ah>|xm<9Sb z>OFUl^g5*i0JOvb0M-lY1$+BGgtI8+GO@8KZHUbg1tfE(93BP<{*llz?rbtM2WaHf4t1aFyqCSe!PTaNDC+q*2_i7N z%1ka6#tNVid70Z7*|R%s-R?;tf4V98i<@(fm!VYy1=lSlQpH3IP4MUj!NnAQ>f2CL za4ny!mlzV^DEh=1mL&y#8JJ-K{qC8ZR?v*^e|ONAD>-JCf_% zn;zqgJ`Xt|MJVDms^&`-rI5{(eSpwiH?d2pJHb&?_9_NsN=p4v^P20u$#Ro;hXrXh z$qanQrbra(8>7!@HhnQ3B{A3}#^XRSe$!#4?6rI>(0F3c{UgUr0Cp#2ibx`nwn?pT zr;nRL{YwymfiWT)@;kW-YX@wq&bp-JOF1p0Fj)9Px8B?grsI5IX2}w*njje|IcNP* z;AfX7GFwINE%47fePIYQ1%QQxiTh8BI^CHfw*hy-13AY`C1KJ7NXOX0(wM0uQS^kp zTe$mbOQi;4bSIc_@aEyqN}v0|I#ZgydWN{9dfB*lVK0+{4=Ch3KFDtYMOVM(gH{v! zvGy<71@^05lzB;NtY7T}K>m?x+8y~-&ju*GdzT32`Q^d|nc|{RWq5%(IX8-$wQ#iG zwC5i>bi~UjLNgh>>3jN&EI!QlE^)}^nfRt91RB6V(ml&cc=?J5}mhwt%u%A^!cCVUkZ_v_Uroleg#aC2M)r zbnD)Z3`}uP&=(%Wx(y#B$V!L_o}M7mXX{`3(v^$8$Etsi-+5zD?MQ>eUCN<}#VZ`3 zJ8tRSWfs8@YVbDD4s!J?^}w<_Vq z0)PF`F3dRrM626ghLA5s++_c5Fp*RA!z+bzN_)L6{U~?iTI^@0RX`&Nt8oxl$g;p1 z3}_DcM}o%4o_je_Db`Vrj{E8lRhEWrB2zcV@|~FV%S`+&#KE}x2?ao!xvZ0$qu;KU z>xF@|)m0`uLikVs0Ki{<l5T3;4r0~ZIpc77Gpyq; zXnIttpoY^B5E;3ZZV2&^n2qtTj}}N;kOqeDfMt2*jW(Hix{%T{>poC4oB>=YrOMCW zX{Y$9?KV9@KW8akszNAN`u)>U71w($Qh|usB4ZUUr*&bEQTxwF)uF>Kq^z zBhwFV8(5>2rA!(LicFNLrZgrabH~WRZtBqdNMD9o(t!^sK z^TZBeJy1-Y;&2|B-~r5v7CwJx^Q4RE%qqM7+vlEB)hX5YR8V6IN&w*h+u=n4fQusj zz5e5hzsG*Z>#z6!ijgl=^LN<(p8mb^|E2{l4E29M0BTG_`|FnKVpm -:Date: 2023-09-16 -:Copyright: 2023, UConn Health -:License: MIT -""" - - -from typing import Union -from biosimulators_utils.model_lang.smoldyn.validation import validate_model -from biosimulators_utils.spatial.data_model import SpatialCombineArchive, SmoldynCombineArchive, ModelValidation - - -def generate_model_validation_object(archive: Union[SpatialCombineArchive, SmoldynCombineArchive]) -> ModelValidation: - """ Generate an instance of `ModelValidation` based on the output of `archive.model_path` - with above `validate_model` method. - - Args: - archive (:obj:`Union[SpatialCombineArchive, SmoldynCombineArchive]`): Instance of `SpatialCombineArchive` - by which to generate model validation on. - - Returns: - :obj:`ModelValidation` - """ - validation_info = validate_model(archive.model_path) - validation = ModelValidation(validation_info) - return validation - - -def verify_simularium_in_archive(archive: Union[SpatialCombineArchive, SmoldynCombineArchive]) -> bool: - """Verify the presence of a simularium file within the passed `archive.paths` dict values. - - Args: - archive: archive to search for simularium file. - - Returns: - `bool`: Whether there exists a simularium file in the directory. - """ - return '.simularium' in list(archive.paths.keys()) diff --git a/requirements.optional.txt b/requirements.optional.txt index 69b993c1..1c3d3fef 100644 --- a/requirements.optional.txt +++ b/requirements.optional.txt @@ -26,8 +26,7 @@ smoldyn >= 2.66 [spatial] smoldyn >= 2.66 -simulariumio -zarr +biosimulators_simularium ################################# ## Visualization formats @@ -42,4 +41,4 @@ docker >= 4.4 ################################# ## Console logging [logging] -capturer \ No newline at end of file +capturer diff --git a/requirements.txt b/requirements.txt index 5dd01685..9fac4547 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,3 +26,4 @@ simplejson termcolor uritools yamldown +importlib From 696e363477fa2ebf50a8272adc55a3a7b43e9a3d Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 15:16:06 -0400 Subject: [PATCH 32/34] chore: updated and reformatting --- biosimulators_utils/combine/exec.py | 24 +++++++++++++----------- requirements.txt | 1 - 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 2bb17f54..9a623a42 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -31,7 +31,6 @@ import shutil from typing import Optional, Tuple from types import FunctionType # noqa: F401 -import importlib __all__ = [ @@ -39,7 +38,7 @@ ] -# noinspection PyIncorrectDocstring +# noinspection PyIncorrectDocstring,PyRedundantParentheses def exec_sedml_docs_in_archive( sed_doc_executer, archive_filename: str, @@ -268,9 +267,12 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # generate simularium file if spatial if config.SPATIAL: - biosimularium = importlib.import_module('biosimulators_simularium') + import biosimulators_simularium as biosimularium simularium_filename = os.path.join(out_dir, 'output') - spatial_archive = biosimularium.SmoldynCombineArchive(rootpath=out_dir, simularium_filename=simularium_filename) + spatial_archive = biosimularium.SmoldynCombineArchive( + rootpath=out_dir, + simularium_filename=simularium_filename + ) # check if modelout file exists if not os.path.exists(spatial_archive.model_path): @@ -279,7 +281,7 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, generate_model_output_file = False # construct converter - converter = SmoldynDataConverter( + converter = biosimularium.SmoldynDataConverter( archive=spatial_archive, generate_model_output=generate_model_output_file ) @@ -295,8 +297,8 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # bundle CSV files of reports into zip archive report_formats = config.REPORT_FORMATS archive_paths = [ - os.path.join(out_dir, '**', '*.' + format.value) - for format in report_formats if format != ReportFormat.h5 + os.path.join(out_dir, '**', '*.' + f.value) + for f in report_formats if f != ReportFormat.h5 ] archive = build_archive_from_paths(archive_paths, out_dir) if archive.files: @@ -304,7 +306,7 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # bundle PDF files of plots into zip archive viz_formats = config.VIZ_FORMATS - archive_paths = [os.path.join(out_dir, '**', '*.' + format.value) for format in viz_formats] + archive_paths = [os.path.join(out_dir, '**', '*.' + f.value) for f in viz_formats] archive = build_archive_from_paths(archive_paths, out_dir) if archive.files: ArchiveWriter().run(archive, os.path.join(out_dir, config.PLOTS_PATH)) @@ -317,10 +319,10 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, viz_formats = config.VIZ_FORMATS path_patterns = ( [ - os.path.join(out_dir, '**', '*.' + format.value) - for format in report_formats if format != ReportFormat.h5 + os.path.join(out_dir, '**', '*.' + f.value) + for f in report_formats if format != ReportFormat.h5 ] - + [os.path.join(out_dir, '**', '*.' + format.value) for format in viz_formats] + + [os.path.join(out_dir, '**', '*.' + f.value) for f in viz_formats] ) for path_pattern in path_patterns: for path in glob.glob(path_pattern, recursive=True): diff --git a/requirements.txt b/requirements.txt index 9fac4547..5dd01685 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,3 @@ simplejson termcolor uritools yamldown -importlib From 3bf660f1ef3b607e71c127a9872face8daad8fc4 Mon Sep 17 00:00:00 2001 From: "spaceBearAmadeus (Alex)" Date: Fri, 6 Oct 2023 15:50:48 -0400 Subject: [PATCH 33/34] updated implementation --- biosimulators_utils/combine/exec.py | 15 +++++---------- biosimulators_utils/config.py | 4 ++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 9a623a42..93adfa46 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -138,11 +138,6 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # unpack archive and read metadata archive = CombineArchiveReader().run(archive_filename, archive_tmp_dir, config=config) - # check the manifest for a smoldyn model - for content in archive.contents: - if 'smoldyn' in content.location: - config.SPATIAL = True - # validate archive errors, warnings = validate(archive, archive_tmp_dir, config=config) if warnings: @@ -208,11 +203,11 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, else: log = None - # get archive contents and check for spatial - archive_contents = archive.get_master_content() - for content in archive_contents: - if config.SUPPORTED_SPATIAL_SIMULATOR.lower() in content.location: + # check the manifest for a smoldyn model + for content in archive.contents: + if 'smoldyn' in content.location: config.SPATIAL = True + print('There is spatial!') # execute SED-ML files: execute tasks and save output exceptions = [] @@ -275,7 +270,7 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, ) # check if modelout file exists - if not os.path.exists(spatial_archive.model_path): + if not os.path.exists(spatial_archive.model_output_filename): generate_model_output_file = True else: generate_model_output_file = False diff --git a/biosimulators_utils/config.py b/biosimulators_utils/config.py index 46fe47bb..5985990a 100644 --- a/biosimulators_utils/config.py +++ b/biosimulators_utils/config.py @@ -174,9 +174,9 @@ def __init__(self, self.VERBOSE = VERBOSE self.DEBUG = DEBUG self.SPATIAL = SPATIAL - self.__SUPPORTED_SPATIAL_SIMULATOR = DEFAULT_SUPPORTED_SPATIAL_SIMULATOR + self.SUPPORTED_SPATIAL_SIMULATOR = DEFAULT_SUPPORTED_SPATIAL_SIMULATOR try: - assert self.__SUPPORTED_SPATIAL_SIMULATOR == 'smoldyn' + assert self.SUPPORTED_SPATIAL_SIMULATOR == 'smoldyn' except AssertionError: raise ValueError( """ From 4a4c6a645f69728f527a088ad262519e9192474c Mon Sep 17 00:00:00 2001 From: alex patrie Date: Fri, 6 Oct 2023 16:29:07 -0400 Subject: [PATCH 34/34] chore: updated exec --- biosimulators_utils/combine/exec.py | 74 +++++++++++++++++------------ 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/biosimulators_utils/combine/exec.py b/biosimulators_utils/combine/exec.py index 93adfa46..69c00083 100644 --- a/biosimulators_utils/combine/exec.py +++ b/biosimulators_utils/combine/exec.py @@ -38,7 +38,6 @@ ] -# noinspection PyIncorrectDocstring,PyRedundantParentheses def exec_sedml_docs_in_archive( sed_doc_executer, archive_filename: str, @@ -203,12 +202,6 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, else: log = None - # check the manifest for a smoldyn model - for content in archive.contents: - if 'smoldyn' in content.location: - config.SPATIAL = True - print('There is spatial!') - # execute SED-ML files: execute tasks and save output exceptions = [] for i_content, content in enumerate(sedml_contents): @@ -246,6 +239,34 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, results[content.location] = doc_results if config.LOG: doc_log.status = Status.SUCCEEDED + + # check the manifest for a smoldyn model + for file_contents in archive.contents: + if 'smoldyn' in file_contents.location: + config.SPATIAL = True + print('There is spatial!') + + # generate simularium file if spatial + if config.SPATIAL: + import biosimulators_simularium as biosimularium + simularium_filename = os.path.join(out_dir, 'output') + spatial_archive = biosimularium.SmoldynCombineArchive( + rootpath=out_dir, + simularium_filename=simularium_filename + ) + # check if modelout file exists + if not os.path.exists(spatial_archive.model_output_filename): + generate_model_output_file = True + else: + generate_model_output_file = False + # construct converter + converter = biosimularium.SmoldynDataConverter( + archive=spatial_archive, + generate_model_output=generate_model_output_file + ) + # generate simularium file + converter.generate_simularium_file(io_format='json') + except Exception as exception: if config.DEBUG: raise @@ -260,30 +281,6 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, doc_log.duration = (datetime.datetime.now() - doc_start_time).total_seconds() doc_log.export() - # generate simularium file if spatial - if config.SPATIAL: - import biosimulators_simularium as biosimularium - simularium_filename = os.path.join(out_dir, 'output') - spatial_archive = biosimularium.SmoldynCombineArchive( - rootpath=out_dir, - simularium_filename=simularium_filename - ) - - # check if modelout file exists - if not os.path.exists(spatial_archive.model_output_filename): - generate_model_output_file = True - else: - generate_model_output_file = False - - # construct converter - converter = biosimularium.SmoldynDataConverter( - archive=spatial_archive, - generate_model_output=generate_model_output_file - ) - - # generate simularium file - converter.generate_simularium_file(io_format='json') - print('') if config.BUNDLE_OUTPUTS: @@ -306,6 +303,15 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, if archive.files: ArchiveWriter().run(archive, os.path.join(out_dir, config.PLOTS_PATH)) + # bundle Simularium file into zip archive + if config.SPATIAL: + simularium_format = ['simularium'] + archive_paths = [os.path.join(out_dir, '**', '*.' + f) for f in simularium_format] + archive = build_archive_from_paths(archive_paths, out_dir) + if archive.files: + ArchiveWriter().run(archive, os.path.join(out_dir, 'simularium.zip')) + + # cleanup temporary files print('Cleaning up ...') if not config.KEEP_INDIVIDUAL_OUTPUTS: @@ -373,3 +379,9 @@ def sed_doc_executer(doc, working_dir, base_out_path, rel_out_path=None, # return results and log return (results, log) + + + + + +