From 283e17aa335509df2cefdab6eccb8a616fc4a2a7 Mon Sep 17 00:00:00 2001 From: Jonathan Karr Date: Mon, 13 Apr 2020 22:32:54 -0400 Subject: [PATCH] editing repo to adhere to BioSimulations interface; simplifying code; integrating in BioSimulation-utils for parsing SED-ML and COMBINE files --- .gitignore | 5 + Biosimulations_bionetgen/__init__.py | 4 + Biosimulations_bionetgen/__main__.py | 50 ++++ Biosimulations_bionetgen/_version.py | 1 + Biosimulations_bionetgen/core.py | 230 +++++++++++++++++++ Dockerfile | 83 ++++--- LICENSE | 21 ++ MANIFEST.in | 8 + README.md | 60 +++++ assets/run-bionetgen.py | 191 --------------- properties.json | 203 ++++++++++++++++ requirements.optional.txt | 3 + requirements.txt | 3 + setup.cfg | 2 + setup.py | 46 ++++ {assets => tests/fixtures}/test.bngl | 0 tests/fixtures/test.omex | Bin 0 -> 3719 bytes assets/test.xml => tests/fixtures/test.sedml | 0 tests/requirements.txt | 2 + tests/test_all.py | 126 ++++++++++ 20 files changed, 816 insertions(+), 222 deletions(-) create mode 100644 .gitignore create mode 100644 Biosimulations_bionetgen/__init__.py create mode 100644 Biosimulations_bionetgen/__main__.py create mode 100644 Biosimulations_bionetgen/_version.py create mode 100644 Biosimulations_bionetgen/core.py create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md delete mode 100644 assets/run-bionetgen.py create mode 100644 properties.json create mode 100644 requirements.optional.txt create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py rename {assets => tests/fixtures}/test.bngl (100%) create mode 100644 tests/fixtures/test.omex rename assets/test.xml => tests/fixtures/test.sedml (100%) create mode 100644 tests/requirements.txt create mode 100644 tests/test_all.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c1c16e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +_pycache__/ +*.egg-info/ + +tests/results/ diff --git a/Biosimulations_bionetgen/__init__.py b/Biosimulations_bionetgen/__init__.py new file mode 100644 index 0000000..7b23813 --- /dev/null +++ b/Biosimulations_bionetgen/__init__.py @@ -0,0 +1,4 @@ +from ._version import __version__ # noqa: F401 +# :obj:`str`: version + +from .core import exec_combine_archive # noqa: F401 diff --git a/Biosimulations_bionetgen/__main__.py b/Biosimulations_bionetgen/__main__.py new file mode 100644 index 0000000..e6945c2 --- /dev/null +++ b/Biosimulations_bionetgen/__main__.py @@ -0,0 +1,50 @@ +""" BioSimulations-compliant command-line interface to the `BioNetGen `_ simulation program. + +:Author: Jonathan Karr +:Date: 2020-04-13 +:Copyright: 2020, Center for Reproducible Biomedical Modeling +:License: MIT +""" + +from .core import exec_combine_archive +import Biosimulations_bionetgen +import cement + + +class BaseController(cement.Controller): + """ Base controller for command line application """ + + class Meta: + label = 'base' + description = "BioSimulations-compliant command-line interface to the BioNetGen simulation program ." + help = "bionetgen" + arguments = [ + (['-i', '--archive'], dict(type=str, + required=True, + help='Path to OMEX file which contains one or more SED-ML-encoded simulation experiments')), + (['-o', '--out-dir'], dict(type=str, + default='.', + help='Directory to save outputs')), + (['-v', '--version'], dict(action='version', + version=Biosimulations_bionetgen.__version__)), + ] + + @cement.ex(hide=True) + def _default(self): + args = self.app.pargs + exec_combine_archive(args.archive, args.out_dir) + + +class App(cement.App): + """ Command line application """ + class Meta: + label = 'bionetgen' + base_controller = 'base' + handlers = [ + BaseController, + ] + + +def main(): + with App() as app: + app.run() diff --git a/Biosimulations_bionetgen/_version.py b/Biosimulations_bionetgen/_version.py new file mode 100644 index 0000000..e59b17b --- /dev/null +++ b/Biosimulations_bionetgen/_version.py @@ -0,0 +1 @@ +__version__ = '2.5.0' diff --git a/Biosimulations_bionetgen/core.py b/Biosimulations_bionetgen/core.py new file mode 100644 index 0000000..b049c3c --- /dev/null +++ b/Biosimulations_bionetgen/core.py @@ -0,0 +1,230 @@ +""" BioSimulations-compliant command-line interface to the `BioNetGen `_ simulation program. + +:Author: Ali Sinan Saglam +:Author: Jonathan Karr +:Date: 2020-04-13 +:Copyright: 2020, Center for Reproducible Biomedical Modeling +:License: MIT +""" + +from Biosimulations_utils.simulation.data_model import Simulation # noqa: F401 +import Biosimulations_utils.simulator.utils.exec_simulations_in_archive +import os +import pandas +import re +import subprocess +import tempfile + +__all__ = ['exec_combine_archive', 'BioNetGenSimulationRunner'] + + +def exec_combine_archive(archive_file, out_dir): + """ Execute the SED tasks defined in a COMBINE archive and save the outputs + + Args: + archive_file (:obj:`str`): path to COMBINE archive + out_dir (:obj:`str`): directory to store the outputs of the tasks + """ + Biosimulations_utils.simulator.utils.exec_simulations_in_archive(archive_file, BioNetGenSimulationRunner().run, out_dir) + + +class BioNetGenSimulationRunner(object): + def run(self, model_filename, model_sed_urn, simulation, working_dir, out_filename, out_format): + """ Execute a simulation and save its results + + Args: + model_filename (:obj:`str`): path to the model in BNGL format + model_sed_urn (:obj:`str`): SED URN for the format of the model (e.g., `urn:sedml:language:sbml`) + simulation (:obj:`Simulation`): simulation + working_dir (:obj:`str`): directory of the SED-ML file + out_filename (:obj:`str`): path to save the results of the simulation + out_format (:obj:`str`): format to save the results of the simulation (e.g., `csv`) + """ + + # read the model from the BNGL file + model_lines = self.read_model(model_filename, model_sed_urn) + + # modify the model according to `simulation` + modified_model_lines = self.modify_model(model_lines, simulation) + + # write the modified model lines to a BNGL file + modified_model_filename = tempfile.mkstemp(suffix='.bngl') + with open(modified_model_filename, "w") as file: + file.writelines(modified_model_lines) + + # simulate the modified model + subprocess.call(['BNG2.pl', modified_model_filename]) + + # put files into output path + gdat_results_filename = modified_model_filename.replace('.bngl', '.gdat') + self.convert_simulation_results(gdat_results_filename, out_filename, out_format) + + # cleanup temporary files + os.remove(modified_model_filename) + os.remove(gdat_results_filename) + + def read_model(self, model_filename, model_sed_urn): + """ Read a model from a file + + Args: + model_filename (:obj:`str`): path to the model in BNGL format + model_sed_urn (:obj:`str`): SED URN for the format of the model (e.g., `urn:sedml:language:sbml`) + + Returns: + :obj:`list` of :obj:`str`: model + + Raises: + :obj:`NotImplementedError`: if the model is not in BGNL format + """ + if model_sed_urn != 'urn:sedml:language:bngl': + raise NotImplementedError('Model format with SED URN {} is not supported'.format(model_sed_urn)) + + with open(model_filename, "r") as file: + line = file.readline() + model_lines = [] + # TODO: end model is not necessary, find other + # ways to check for the action block + while line.strip() != "end model": + model_lines.append(line) + line = file.readline() + model_lines.append(line) + return model_lines + + def modify_model(self, model_lines, simulation): + """ Modify a model according to a specified simulation experiment + + * Modify model parameters + * Set simulation time course + * Set simulation algorithm and algorithm parameters + + Args: + model_lines (:obj:`list` of :obj:`str`): model + simulation (:obj:`Simulation`): simulation + + Returns: + :obj:`list` of :obj:`str`: modified model + + Raises: + :obj:`NotImplementedError`: if the desired simulation algorithm is not supported + """ + + # use `setParameter` to add parameter changes to the model + for change in simulation.model_parameter_changes: + # TODO: properly apply parameter changes + model_lines += 'setParameter("{}", {})\n'.format(change.parameter.id, change.value) + + # get the initial time, end time, and the number of time points to record + t_start = simulation.start_time + t_end = simulation.end_time + n_steps = simulation.num_time_points + + # Get algorithm parameters and use them properly + alg_params = self.get_algorithm_parameters(simulation.parameters) + + # add the simulation to the model lines + assert simulation.algorithm, "Simulation must define an algorithm" + assert simulation.algorithm.kisao_term, "Simulation algorithm must include a KiSAO term" + assert simulation.algorithm.kisao_term.ontology == 'KISAO', "Simulation algorithm must include a KiSAO term" + + if simulation.algorithm.kisao_term.id == '0000019': + model_lines += "generate_network({overwrite=>1})\n" + simulate_line = 'simulate({' + 'method => "{}", t_start => {}, t_end => {}, n_steps => {}'.format( + "ode", t_start, t_end, n_steps) + for param, val in alg_params.items(): + simulate_line += ", {}=>{}".format(param, val) + simulate_line += '})\n' + model_lines += simulate_line + + elif simulation.algorithm.kisao_term.id == '0000029': + model_lines += "generate_network({overwrite=>1})\n" + simulate_line = 'simulate({' + 'method => "{}", t_start => {}, t_end => {}, n_steps => {}'.format( + "ssa", t_start, t_end, n_steps) + # TODO: support algorothm parameters + simulate_line += '})\n' + model_lines += simulate_line + + elif simulation.algorithm.kisao_term.id == '0000263': + simulate_line = 'simulate({' + 'method => "{}", t_start => {}, t_end => {}, n_steps => {}'.format( + "nfsim", t_start, t_end, n_steps) + # TODO: support algorothm parameters + simulate_line += '})\n' + model_lines += simulate_line + + else: + raise NotImplementedError("Algorithm with KiSAO id {} is not supported".format(simulation.algorithm.kisao_term.id)) + + # return the modified model + return model_lines + + def get_algorithm_parameters(self, parameter_changes): + """ Get the desired algorithm parameters for a simulation + + Args: + parameter_changes (:obj:`list` of :obj:`ParameterChange`): parameter changes + + Returns: + :obj:`dict`: dictionary that maps the ids of parameters to their values + + Raises: + :obj:`NotImplementedError`: if desired algorithm parameter is not supported + """ + bng_parameters = {} + for change in parameter_changes: + if change.parameter.kisao_term \ + and change.parameter.kisao_term.ontology == 'KISAO' \ + and change.parameter.kisao_term.id == '0000211': + # Relative tolerance + bng_parameters["rtol"] = change.value + + elif change.parameter.kisao_term \ + and change.parameter.kisao_term.ontology == 'KISAO' \ + and change.parameter.kisao_term.id == '0000209': + # Absolute tolerance + bng_parameters["atol"] = change.value + + else: + raise NotImplementedError("Parameter {} is not supported".format(change.parameter.id)) + + return bng_parameters + + def convert_simulation_results(self, gdat_filename, out_filename, out_format): + """ Convert simulation results from gdat to the desired output format + + Args: + gdat_filename (:obj:`str`): path to simulation results in gdat format + out_filename (:obj:`str`): path to save the results of the simulation + out_format (:obj:`str`): format to save the results of the simulation (e.g., `csv`) + + Raises: + :obj:`NotImplementedError`: if the desired output format is not supported + """ + data = self.read_simulation_results(gdat_filename) + if out_format == 'csv': + data.to_csv(out_filename) + elif out_format == 'tsv': + data.to_csv(out_filename, sep='\t') + else: + raise NotImplementedError('Unsupported output format {}'.format(out_format)) + + def read_simulation_results(self, gdat_filename): + """ Read the results of a simulation + + Args: + gdat_filename (:obj:`str`): path to simulation results in gdat format + + Returns: + :obj:`pandas.DataFrame`: simulation results + """ + with open(gdat_filename, "r") as file: + # Get column names from first line of file + line = file.readline() + names = re.split(r'\s+', (re.sub('#', '', line)).strip()) + + # Read results + results = pandas.read_table(file, sep=r'\s+', header=None, names=names) + + # Set the time as index + results = results.set_index("time") + + # Return results + return results diff --git a/Dockerfile b/Dockerfile index 47a3b49..991eb7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,57 @@ -# Author: Ali Sinan Saglam -# Date: 3/30/2020 -# Made for: BioNetGen docker image for BioSim +# BioSimulations-compliant Docker image for BioNetGen +# +# Build image: +# docker build \ +# --tag crbm/biosimulations_bionetgen:2.5.0 \ +# --tag crbm/biosimulations_bionetgen:latest \ +# . +# +# Run image: +# docker run \ +# --tty \ +# --rm \ +# --mount type=bind,source="$(pwd)"/tests/fixtures,target=/root/in,readonly \ +# --mount type=bind,source="$(pwd)"/tests/results,target=/root/out \ +# crbm/biosimulations_bionetgen:latest \ +# -i /root/in/test.omex \ +# -o /root/out +# +# Author: Ali Sinan Saglam +# Author: Jonathan Karr +# Date: 2020-04-13 -# FROM ubuntu:18.04 FROM continuumio/anaconda3 -# update -RUN apt-get update -# stuff we want -RUN apt-get install -y g++ vim perl cmake wget -# Get necessary libraries, SED-ML lib in particular -RUN pip install python-libsedml +# install requirements and BioNetGet +RUN apt-get update -y \ + && apt-get install --no-install-recommends -y \ + cmake \ + g++ \ + git \ + make \ + perl \ + vim \ + \ + && git clone https://github.com/RuleWorld/bionetgen /root/bionetgen \ + && cd /root/bionetgen \ + && git submodule init \ + && git submodule update \ + && cd /root/bionetgen/bng2 \ + && make \ + \ + && apt-get remove -y \ + cmake \ + g++ \ + git \ + make \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* +ENV PATH=${PATH}:/root/bionetgen/bng2 -# install bionetgen -WORKDIR /home/BNGDocker/ -RUN git clone https://github.com/RuleWorld/bionetgen -WORKDIR /home/BNGDocker/bionetgen/ -RUN git submodule init -RUN git submodule update -WORKDIR /home/BNGDocker/bionetgen/bng2/ -RUN make -WORKDIR /home/BNGDocker/simulation +# install BioSimulations-compliant command-line interface to BioNetGen +COPY . /root/Biosimulations_BioNetGen +RUN pip3 install /root/Biosimulations_BioNetGen -# Copy over run script -COPY assets/run-bionetgen.py /usr/local/bin/run-bionetgen -RUN chmod ugo+x /usr/local/bin/run-bionetgen - -# Temporary files for testing -COPY assets/test.bngl /home/BNGDocker/test.bngl -COPY assets/test.xml /home/BNGDocker/test.xml - -# Entry point setup -ENTRYPOINT ["run-bionetgen"] -# if we need to setup some defaults -# CMD [] +# setup entry point +ENTRYPOINT ["bionetgen"] +CMD [] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..612af68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Center for Reproducible Biomedical Modeling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5a9f907 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +# description +include README.rst + +# license +include LICENSE + +# requirements +include requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a95220 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Biosimulations_BioNetGen +Biosimulations-compliant command-line interface to the [BioNetGen](https://bionetgen.org/) simulation program. + +## Contents +* [Installation](#installation) +* [Usage](#usage) +* [License](#license) +* [Development team](#development-team) +* [Questions and comments](#questions-and-comments) + +## Installation + +### Install Python package +``` +pip install git+https://github.com/reproducible-biomedical-modeling/Biosimulations_BioNetGen +``` + +### Install Docker image +``` +docker pull crbm/biosimulations_bionetgen +``` + +## Local usage +``` +usage: bionetgen [-h] [-d] [-q] -i ARCHIVE [-o OUT_DIR] [-v] + +BioSimulations-compliant command-line interface to the BioNetGen simulation program . + +optional arguments: + -h, --help show this help message and exit + -d, --debug full application debug mode + -q, --quiet suppress all console output + -i ARCHIVE, --archive ARCHIVE + Path to OMEX file which contains one or more SED-ML- + encoded simulation experiments + -o OUT_DIR, --out-dir OUT_DIR + Directory to save outputs + -v, --version show program's version number and exit +``` + +## Usage through Docker container +``` +docker run \ + --tty \ + --rm \ + --mount type=bind,source="$(pwd)"/tests/fixtures,target=/root/in,readonly \ + --mount type=bind,source="$(pwd)"/tests/results,target=/root/out \ + crbm/biosimulations_bionetgen:latest \ + -i /root/in/BIOMD0000000297.omex \ + -o /root/out +``` + +## License +This package is released under the [MIT license](LICENSE). + +## Development team +This package was developed by [Ali Sinan Saglam](https://scholar.google.com/citations?user=7TM0eekAAAAJ&hl=en) in the Faeder Lab at the University of Pittsburgh, the [Karr Lab](https://www.karrlab.org) at the Icahn School of Medicine at Mount Sinai, and the [Center for Reproducible Biomedical Modeling](http://reproduciblebiomodels.org). + +## Questions and comments +Please contact [Ali Sinan Saglam](mailto:als251@pitt.edu) or the [Center for Reproducible Biomedical Modeling](mailto:info@reproduciblebiomodels.org) with any questions or comments. diff --git a/assets/run-bionetgen.py b/assets/run-bionetgen.py deleted file mode 100644 index 73d3ab6..0000000 --- a/assets/run-bionetgen.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/opt/conda/bin/python -import argparse, libsedml, subprocess, os, shutil, re -import pandas as pd - -class BNGRunner: - def __init__(self, adjusted_name="sedml_adjusted"): - self._parse_args() - self.adjusted_name = adjusted_name - self.adjusted_file = self.adjusted_name + ".bngl" - - def _parse_args(self): - self.parser = argparse.ArgumentParser() - self.parser.add_argument("--model", dest="model_file", help="Model BNGL or XML file") - self.parser.add_argument("--sim", dest="simulation_file", help="SED-ML file with simulation details") - self.parser.add_argument("--results", dest="output_path", help="Output path to write results to") - self.parser.add_argument("--bng_path", dest="bng_path", default="/home/BNGDocker/bionetgen/bng2/BNG2.pl", - help="Path to BNG2.pl") - self.args = self.parser.parse_args() - - def read_gdat(self, model_path): - with open(model_path, "r") as f: - # Get column names from first line of file - line = f.readline() - names = re.split('\s+',(re.sub('#','',line)).strip()) - gdat = pd.read_table(f,sep='\s+',header=None,names=names) - # Set the time as index - gdat = gdat.set_index("time") - return gdat - - def pass_results(self, input_name, output_path): - # load gdat - data = self.read_gdat(input_name + ".gdat") - print(data) - # save to csv - print(output_path, self.model_name) - data.to_csv(self.model_name +".csv") - - def get_bngl_lines(self, bngl_file): - with open(bngl_file, "r") as f: - line = f.readline() - bngl_lines = [] - # TODO: end model is not necessary, find other - # ways to check for the action block - while line.strip() != "end model": - bngl_lines.append(line) - line = f.readline() - bngl_lines.append(line) - return bngl_lines - - def get_time_values(self, sim_xml): - init = sim_xml.getOutputStartTime() - end = sim_xml.getOutputEndTime() - pts = sim_xml.getNumberOfPoints() - return (init, end, pts) - - def get_method_name(self, alg_xml): - # TODO: get it from KISAO ID - # 19 - CVODE/ODE - # 29 - SSA/Gillespie - # 263 - NFSim - kisao_ID = alg_xml.getKisaoID() - kisao_num = int(kisao_ID.split(":")[1]) - # check algorithm type - if kisao_num == 19: - # CVODE/ODE - method_name = "ode" - elif kisao_num == 29: - # SSA/Gillespie - method_name = "ssa" - elif kisao_num == 263: - # NFSim - method_name = "nfsim" - else: - print("this algorithm with KISAO ID {} is not supported".format(kisao_ID)) - return method_name - - def get_parameter_changes(self, model_xml): - list_of_changes = model_xml.getListOfChanges() - bngl_parameter_changes = [] - for num_change in range(list_of_changes.getNumChanges()): - # getting the change xml - change_xml = list_of_changes.get(num_change) - # name of the parameter - target = change_xml.getTarget() - # TODO: Fix this hacky mess - target = target.split("id=")[1].split("]")[0].replace("'","") - # value we are setting it to - value = float(change_xml.getNewValue()) - # append to the list of changes - bngl_parameter_changes.append( (target, value) ) - return bngl_parameter_changes - - def get_method_parameters(self, alg_xml): - method_parameters = [] - # Loop over all given parameters - num_params = alg_xml.getNumAlgorithmParameters() - for nparam in range(num_params): - param_xml = alg_xml.getAlgorithmParameter(nparam) - # get kisao ID - kisao_int = param_xml.getKisaoIDasInt() - param_val = param_xml.getValue() - if kisao_int == 211: - # Relative tolerance - method_parameters.append( ("rtol", param_val) ) - elif kisao_int == 209: - # Absolute tolerance - method_parameters.append( ("atol", param_val) ) - else: - print("Parameter of KISAO ID {} is not supported".format(param_xml.getKisaoID())) - return method_parameters - - def adjust_bngl(self, bngl_file, sedml_file): - # first read the BNGL file, only the model - bngl_lines = self.get_bngl_lines(bngl_file) - # now read SED-ML - sedml = libsedml.readSedMLFromFile(sedml_file) - # TODO: As the standard changes this block of code needs to be - # adapted. Currently assumes a single time series - sim_xml = sedml.getSimulation(0) - # get initial time, end time and number of points - init, end, pts = self.get_time_values(sim_xml) - # let's check the algorithm name we want to use - alg_xml = sim_xml.getAlgorithm() - method_name = self.get_method_name(alg_xml) - # Get algorithm parameters and use them properly - method_parameters = self.get_method_parameters(alg_xml) - - # TODO: Get simulation parameter changes and apply them - # properly - model_xml = sedml.getModel(0) - bngl_parameter_changes = self.get_parameter_changes(model_xml) - - # add setParameter command to the bngl - for change in bngl_parameter_changes: - t,v = change - bngl_lines += 'setParameter("{}",{})\n'.format(t,v) - - # now let's add the appropriate lines to the bngl - if method_name == "ode": - bngl_lines += "generate_network({overwrite=>1})\n" - simulate_line = 'simulate({' + 'method=>"{}",t_start=>{},t_end=>{},n_steps=>{}'.format(method_name, init, end, pts) - for mp in method_parameters: - param, val = mp - simulate_line += ",{}=>{}".format(param,val) - simulate_line += '})\n' - bngl_lines += simulate_line - elif method_name == "ssa": - bngl_lines += "generate_network({overwrite=>1})\n" - simulate_line = 'simulate({' + 'method=>"{}",t_start=>{},t_end=>{},n_steps=>{}'.format(method_name, init, end, pts) - # Add method parameters here if we support it in the future - simulate_line += '})\n' - bngl_lines += simulate_line - elif method_name == "nfsim": - simulate_line = 'simulate({' + 'method=>"{}",t_start=>{},t_end=>{},n_steps=>{}'.format(method_name, init, end, pts) - # Add method parameters here if we support it in the future - simulate_line += '})\n' - bngl_lines += simulate_line - # write adjusted bngl file - with open(self.adjusted_file, "w") as f: - f.writelines(bngl_lines) - return self.adjusted_file - - def run(self): - # for now testing purposes, we'll just write stuff out - print(self.args) - # get the simulation names in here - # TODO: Add checks to make sure this exists and a bngl file - model_file_name = os.path.basename(self.args.model_file) - model_name = model_file_name.replace(".bngl","") - self.model_name = model_name - sim_file_name = os.path.basename(self.args.simulation_file) - sim_name = sim_file_name.replace(".xml","") - # moving files - # TODO: determine a logical way to deal with these - shutil.copy(self.args.model_file, self.args.output_path) - shutil.copy(self.args.simulation_file, self.args.output_path) - os.chdir(self.args.output_path) - print(os.listdir()) - # adjusting the bngl file - adjusted_file = self.adjust_bngl(model_file_name, sim_file_name) - # running simulation - subprocess.call([self.args.bng_path, adjusted_file]) - print(os.listdir()) - # put files into output path - self.pass_results(self.adjusted_name, self.args.output_path) - # check if saved - print(os.listdir()) - -if __name__ == "__main__": - p = BNGRunner() - p.run() diff --git a/properties.json b/properties.json new file mode 100644 index 0000000..43a8b2b --- /dev/null +++ b/properties.json @@ -0,0 +1,203 @@ +{ + "id": "bionetgen", + "name": "BioNetGen", + "version": "2.5.0", + "description": "BioNetGen is an open-source software package for rule-based modeling of complex biochemical systems.", + "url": "https://bionetgen.org/", + "dockerHubImageId": "crbm/Biosimulations_bionetgen:2.5.0", + "format": { + "name": "Docker Image", + "version": "1.2", + "url": "https://github.com/moby/moby/blob/master/image/spec/v1.2.md" + }, + "authors": [ + { + "firstName": "Ali", + "middleName": "Sinan", + "lastName": "Saglam" + }, + { + "firstName": "Leonard", + "middleName": "A", + "lastName": "Harris" + }, + { + "firstName": "Justin", + "middleName": "S", + "lastName": "Hogg" + }, + { + "firstName": "José-Juan", + "lastName": "Tapi" + }, + { + "firstName": "John", + "middleName": "A P", + "lastName": "Sekar" + }, + { + "firstName": "Sanjana", + "lastName": "Gupta" + }, + { + "firstName": "Ilya", + "lastName": "Korsunsky" + }, + { + "firstName": "Arshi", + "lastName": "Arora" + }, + { + "firstName": "Dipak", + "lastName": "Barua" + }, + { + "firstName": "Robert", + "middleName": "P", + "lastName": "Sheehan" + }, + { + "firstName": "James", + "middleName": "R", + "lastName": "Faeder" + }, + { + "firstName": "Byron", + "lastName": "Goldstein" + }, + { + "firstName": "William", + "middleName": "S", + "lastName": "Hlavacek" + } + ], + "references": [ + { + "title": "BioNetGen 2.2: advances in rule-based modeling", + "authors": "Leonard A. Harris, Justin S. Hogg, José-Juan Tapia, John A. P. Sekar, Sanjana Gupta, Ilya Korsunsky, Arshi Arora, Dipak Barua, Robert P. Sheehan & James R. Faeder", + "journal": "Bioinformatics", + "volume": 32, + "issue": 21, + "pages": "3366-3368", + "year": 2016, + "doi": "10.1093/bioinformatics/btw469" + }, + { + "title": "Rule-based modeling of biochemical systems with BioNetGen", + "authors": "James R. Faeder, Michael L. Blinov & William S. Hlavacek", + "journal": "Methods in Molecular Biology", + "volume": 500, + "pages": "113-167", + "year": 2009, + "doi": "10.1007/978-1-59745-525-1_5" + }, + { + "title": "BioNetGen: software for rule-based modeling of signal transduction based on the interactions of molecular domains", + "authors": "Michael L. Blinov, James R. Faeder, Byron Goldstein & William S. Hlavacek", + "journal": "Bioinformatics", + "volume": 20, + "issue": 17, + "pages": "3289-3291", + "year": 2004, + "doi": "10.1093/bioinformatics/bth378" + } + ], + "license": "MIT", + "algorithms": [ + { + "id": "cvode", + "name": "CVODE", + "kisaoTerm": { + "ontology": "KISAO", + "id": "0000019", + "name": "CVODE", + "description": "The CVODE is a package written in C that solves ODE initial value problems, in real N-space, written as y'=f(t,y), y(t0)=y0. It is capable for stiff and non-stiff systems and uses two different linear multi-step methods, namely the Adam-Moulton method and the backward differentiation formula.", + "iri": "http://identifiers.org/biomodels.kisao/KISAO_0000019" + }, + "ontologyTerms": [ + { + "ontology": "KISAO", + "id": "0000433", + "name": "CVODE-like method", + "description": "Solves ODE initial value problems, in real N-space, written as y'=f(t,y), y(t0)=y0. It is capable for stiff and non-stiff systems and uses two different linear multi-step methods, namely the Adam-Moulton method and the backward differentiation formula.", + "iri": "http://identifiers.org/biomodels.kisao/KISAO_0000433" + } + ], + "modelingFrameworks": [ + { + "ontology": "SBO", + "id": "0000293", + "name": "non-spatial continuous framework", + "description": "Modelling approach where the quantities of participants are considered continuous, and represented by real values. The associated simulation methods make use of differential equations. The models do not take into account the distribution of the entities and describe only the temporal fluxes.", + "iri": "http://biomodels.net/SBO/SBO_0000293" + } + ], + "modelFormats": [ + { + "id": "SBML", + "name": "Systems Biology Markup Language", + "version": "L3V2", + "edamId": "format_2585", + "url": "http://sbml.org", + "specUrl": "http://identifiers.org/combine.specifications/sbml", + "mimeType": "application/sbml+xml", + "extension": "xml", + "sedUrn": "urn:sedml:language:sbml" + }, + { + "id": "BNGL", + "name": "BioNetGen Language", + "version": null, + "edamId": null, + "url": "https://bionetgen.org/", + "specUrl": "https://bionetgen.org/", + "mimeType": "text/plain", + "extension": "bngl", + "sedUrn": null + } + ], + "simulationFormats": [ + { + "id": "SED-ML", + "name": "Simulation Experiment Description Markup Language", + "version": "L1V3", + "edamId": "format_3685", + "url": "https://sed-ml.org", + "specUrl": "http://identifiers.org/combine.specifications/sed-ml", + "mimeType": "application/xml", + "extension": "sedml" + } + ], + "archiveFormats": [ + { + "id": "COMBINE", + "name": "COMBINE", + "version": "V1", + "edamId": "format_3686", + "url": "https://combinearchive.org/", + "specUrl": "http://identifiers.org/combine.specifications/omex", + "mimeType": "application/zip", + "extension": "omex" + } + ], + "references": [], + "parameters": [ + { + "id": "rel_tol", + "name": "Relative tolerance", + "type": "float", + "default": 0.000001, + "kisaoTerm": { + "ontology": "KISAO", + "id": "0000209", + "name": "relative tolerance", + "description": "This parameter is a numeric value specifying the desired relative tolerance the user wants to achieve. A smaller value means that the trajectory is calculated more accurately.", + "iri": "http://identifiers.org/biomodels.kisao/KISAO_0000209" + } + } + ] + } + ], + "created": "2020-04-13T12:00:00Z", + "updated": "2020-04-13T12:00:00Z" +} \ No newline at end of file diff --git a/requirements.optional.txt b/requirements.optional.txt new file mode 100644 index 0000000..d348438 --- /dev/null +++ b/requirements.optional.txt @@ -0,0 +1,3 @@ +[test-docker] +Biosimulations_utils[docker] +docker diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d771a52 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Biosimulations_format_utils +cement +pandas diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d08f61a --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +import setuptools +try: + import pkg_utils +except ImportError: + import subprocess + import sys + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "pkg_utils"]) + import pkg_utils +import os + +name = 'Biosimulations_bionetgen' +dirname = os.path.dirname(__file__) + +# get package metadata +md = pkg_utils.get_package_metadata(dirname, name) + +# install package +setuptools.setup( + name=name, + version=md.version, + description=("BioSimulations-compliant command-line interface to the BioNetGen simulation program ."), + long_description=md.long_description, + url="https://github.com/reproducible-biomedical-modeling/" + name, + download_url='https://github.com/reproducible-biomedical-modeling/' + name, + author='Center for Reproducible Biomedical Modeling', + author_email="info@reproduciblebiomodels.org", + license="MIT", + keywords='systems biology rule-based modeling network-free simulation', + packages=setuptools.find_packages(exclude=['tests', 'tests.*']), + install_requires=md.install_requires, + extras_require=md.extras_require, + tests_require=md.tests_require, + dependency_links=md.dependency_links, + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: MIT License', + 'Topic :: Scientific/Engineering :: Bio-Informatics', + ], + entry_points={ + 'console_scripts': [ + 'bionetgen = Biosimulations_bionetgen.__main__:main', + ], + }, +) diff --git a/assets/test.bngl b/tests/fixtures/test.bngl similarity index 100% rename from assets/test.bngl rename to tests/fixtures/test.bngl diff --git a/tests/fixtures/test.omex b/tests/fixtures/test.omex new file mode 100644 index 0000000000000000000000000000000000000000..b5dec00a01dcf0d27aaf485fe97ce399a0e60b38 GIT binary patch literal 3719 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 JPVbq3e*rR?STg_s literal 0 HcmV?d00001 diff --git a/assets/test.xml b/tests/fixtures/test.sedml similarity index 100% rename from assets/test.xml rename to tests/fixtures/test.sedml diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..d310cb7 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +capturer +numpy diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..63db2d5 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,126 @@ +""" Tests of the BioNetGen command-line interface and Docker image + +:Author: Jonathan Karr +:Date: 2020-04-07 +:Copyright: 2020, Center for Reproducible Biomedical Modeling +:License: MIT +""" + +from Biosimulations_bionetgen import __main__ +import Biosimulations_bionetgen +import capturer +import docker +import numpy +import os +import pandas +import shutil +import tempfile +import unittest + + +class CliTestCase(unittest.TestCase): + def setUp(self): + self.dirname = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.dirname) + + def test_help(self): + with self.assertRaises(SystemExit): + with __main__.App(argv=['--help']) as app: + app.run() + + def test_version(self): + with __main__.App(argv=['-v']) as app: + with capturer.CaptureOutput(merged=False, relay=False) as captured: + with self.assertRaises(SystemExit): + app.run() + self.assertIn(Biosimulations_bionetgen.__version__, captured.stdout.get_text()) + self.assertEqual(captured.stderr.get_text(), '') + + with __main__.App(argv=['--version']) as app: + with capturer.CaptureOutput(merged=False, relay=False) as captured: + with self.assertRaises(SystemExit): + app.run() + self.assertIn(Biosimulations_bionetgen.__version__, captured.stdout.get_text()) + self.assertEqual(captured.stderr.get_text(), '') + + def test_sim_short_arg_names(self): + archive_filename = 'tests/fixtures/test.omex' + with __main__.App(argv=['-i', archive_filename, '-o', self.dirname]) as app: + app.run() + self.assert_outputs_created(self.dirname) + + def test_sim_long_arg_names(self): + archive_filename = 'tests/fixtures/test.omex' + with __main__.App(argv=['--archive', archive_filename, '--out-dir', self.dirname]) as app: + app.run() + self.assert_outputs_created(self.dirname) + + @unittest.skipIf(os.getenv('CI', '0') in ['1', 'true'], 'Docker not setup in CI') + def test_build_docker_image(self): + docker_client = docker.from_env() + + # build image + image_repo = 'crbm/biosimulations_bionetgen' + image_tag = Biosimulations_bionetgen.__version__ + image, _ = docker_client.images.build( + path='.', + dockerfile='Dockerfile', + pull=True, + rm=True, + ) + image.tag(image_repo, tag='latest') + image.tag(image_repo, tag=image_tag) + + @unittest.skipIf(os.getenv('CI', '0') in ['1', 'true'], 'Docker not setup in CI') + def test_sim_with_docker_image(self): + docker_client = docker.from_env() + + # image config + image_repo = 'crbm/biosimulations_bionetgen' + image_tag = Biosimulations_bionetgen.__version__ + + # setup input and output directories + in_dir = os.path.join(self.dirname, 'in') + out_dir = os.path.join(self.dirname, 'out') + os.makedirs(in_dir) + os.makedirs(out_dir) + + # create intermediate directories so that the test runner will have permissions to cleanup the results generated by + # the docker image (running as root) + os.makedirs(os.path.join(out_dir, 'test')) + + # copy model and simulation to temporary directory which will be mounted into container + shutil.copyfile('tests/fixtures/test.omex', os.path.join(in_dir, 'test.omex')) + + # run image + docker_client.containers.run( + image_repo + ':' + image_tag, + volumes={ + in_dir: { + 'bind': '/root/in', + 'mode': 'ro', + }, + out_dir: { + 'bind': '/root/out', + 'mode': 'rw', + } + }, + command=['-i', '/root/in/test.omex', '-o', '/root/out'], + tty=True, + remove=True) + + self.assert_outputs_created(out_dir) + + def assert_outputs_created(self, dirname): + self.assertEqual(set(os.listdir(dirname)), set(['test'])) + self.assertEqual(set(os.listdir(os.path.join(dirname, 'test'))), set(['report.csv'])) + + results = pandas.read_csv(os.path.join(dirname, 'test', 'report.csv')) + + # test that the results have the correct time points + numpy.testing.assert_array_almost_equal(results['time'], numpy.linspace(0., 10., 101)) + + # TODO: test that the results have the correct row labels (observable ids) + # assert set(results.columns.to_list()) == set([var.id for var in model.variables] + ['time'])