From 7f78eeea86fb56292d9e307cf02f1732cd488843 Mon Sep 17 00:00:00 2001 From: Jonathan Karr Date: Fri, 3 Sep 2021 01:16:11 -0400 Subject: [PATCH] feat: separated simulation and pre-processing --- Dockerfile | 2 +- biosimulators_bionetgen/_version.py | 2 +- biosimulators_bionetgen/core.py | 90 +++++++++++++++++------------ biosimulators_bionetgen/utils.py | 77 ++++++++++++++++++------ 4 files changed, 115 insertions(+), 56 deletions(-) diff --git a/Dockerfile b/Dockerfile index 430263e..8b6944f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ FROM python:3.9-slim-buster -ARG VERSION="0.1.12" +ARG VERSION="0.1.13" ARG SIMULATOR_VERSION=2.6.0 # metadata diff --git a/biosimulators_bionetgen/_version.py b/biosimulators_bionetgen/_version.py index e6d0c4f..377e1f6 100644 --- a/biosimulators_bionetgen/_version.py +++ b/biosimulators_bionetgen/_version.py @@ -1 +1 @@ -__version__ = '0.1.12' +__version__ = '0.1.13' diff --git a/biosimulators_bionetgen/core.py b/biosimulators_bionetgen/core.py index 2740a49..dc877c6 100644 --- a/biosimulators_bionetgen/core.py +++ b/biosimulators_bionetgen/core.py @@ -8,7 +8,7 @@ """ from .io import read_task -from .utils import (exec_bionetgen_task, add_model_attribute_change_to_task, add_simulation_to_task, +from .utils import (exec_bionetgen_task, preprocess_model_attribute_change, add_model_attribute_change_to_task, add_simulation_to_task, get_variables_results_from_observable_results, add_variables_to_model) from .warnings import IgnoredBnglFileContentWarning from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive @@ -100,7 +100,7 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None Args: task (:obj:`Task`): SED task variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - preprocessed_task (:obj:`object`, optional): preprocessed information about the task, including possible + 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 @@ -136,12 +136,56 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None * :obj:`get_variables_results_from_observable_results` """ config = config or get_config() + if config.LOG and not log: log = TaskLog() if preprocessed_task is None: preprocessed_task = preprocess_sed_task(task, variables, config=config) + # read the model from the BNGL file + bionetgen_task = preprocessed_task['bionetgen_task'] + + # validate and apply the model attribute changes to the BioNetGen task + for change in task.model.changes: + add_model_attribute_change_to_task(bionetgen_task, change, preprocessed_task['model_changes'][change.target]) + + # apply the SED algorithm and its parameters to the BioNetGen task + alg_kisao_id = preprocessed_task['algorithm_kisao_id'] + + # execute the task + observable_results = exec_bionetgen_task(bionetgen_task) + + # get predicted values of the variables + variable_results = get_variables_results_from_observable_results(observable_results, variables) + for key in variable_results.keys(): + variable_results[key] = variable_results[key][-(task.simulation.number_of_points + 1):] + + # log action + if config.LOG: + log.algorithm = alg_kisao_id + log.simulator_details = { + 'actions': bionetgen_task.actions, + } + + # 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() + if config.VALIDATE_SEDML: raise_errors_warnings( validation.validate_task(task), @@ -174,8 +218,9 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None bionetgen_task.actions = [] # validate and apply the model attribute changes to the BioNetGen task + model_changes = {} for change in task.model.changes: - add_model_attribute_change_to_task(bionetgen_task, change) + model_changes[change.target] = preprocess_model_attribute_change(bionetgen_task, change) # add observables for the variables to the BioNetGen model add_variables_to_model(bionetgen_task.model, variables) @@ -183,38 +228,9 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None # apply the SED algorithm and its parameters to the BioNetGen task alg_kisao_id = add_simulation_to_task(bionetgen_task, task.simulation) - # execute the task - observable_results = exec_bionetgen_task(bionetgen_task) - - # get predicted values of the variables - variable_results = get_variables_results_from_observable_results(observable_results, variables) - for key in variable_results.keys(): - variable_results[key] = variable_results[key][-(task.simulation.number_of_points + 1):] - - # log action - if config.LOG: - log.algorithm = alg_kisao_id - log.simulator_details = { - 'actions': bionetgen_task.actions, - } - # 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 - preprocessed_task (:obj:`PreprocessedTask`, 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. - config (:obj:`Config`, optional): BioSimulators common configuration - - Returns: - :obj:`object`: preprocessed information about the task - """ - pass + return { + 'bionetgen_task': bionetgen_task, + 'model_changes': model_changes, + 'algorithm_kisao_id': alg_kisao_id, + } diff --git a/biosimulators_bionetgen/utils.py b/biosimulators_bionetgen/utils.py index 170a756..9598425 100644 --- a/biosimulators_bionetgen/utils.py +++ b/biosimulators_bionetgen/utils.py @@ -26,6 +26,7 @@ import tempfile __all__ = [ + 'preprocess_model_attribute_change', 'add_model_attribute_change_to_task', 'add_variables_to_model', 'add_simulation_to_task', @@ -34,8 +35,8 @@ ] -def add_model_attribute_change_to_task(task, change): - """ Encode SED model attribute changes into a BioNetGen task +def preprocess_model_attribute_change(task, change): + """ Process a model change * Compartment sizes: targets should follow the pattern ``compartments..size`` * Function expressions: targets should follow the pattern ``functions..expression`` @@ -46,11 +47,13 @@ def add_model_attribute_change_to_task(task, change): task (:obj:`Task`): BioNetGen task change (:obj:`ModelAttributeChange`): model attribute change + Returns: + :obj:`dict`: processed information about the model change + Raises: :obj:`ValueError`: if a target of a change is not valid """ target = change.target - new_value = change.new_value compartment_size_match = re.match(r'^compartments\.([^\.]+)(\.size)?$', target) if compartment_size_match: @@ -62,25 +65,32 @@ def add_model_attribute_change_to_task(task, change): for i_line, line in enumerate(block): match = re.match(pattern, line) if match: - block[i_line] = '{} {} {} {}'.format(obj_id, match.group(1), new_value, (match.group(3) or '').strip()).strip() comp_changed = True + return { + 'type': 'replace_line_in_block', + 'block': block, + 'i_line': i_line, + 'new_line': lambda new_value: '{} {} {} {}'.format(obj_id, match.group(1), new_value, (match.group(3) or '').strip()).strip(), + } if not comp_changed: raise ValueError(('The size of compartment `{}` cannot be changed ' 'because the model does not have a compartment with this id.').format(obj_id)) - return - parameter_values_match = re.match(r'^parameters\.([^\.]+)(\.value)?$', target) if parameter_values_match: - task.actions.append('setParameter("{}", {})'.format(parameter_values_match.group(1), new_value)) - return + return { + 'type': 'append_action', + 'action': lambda new_value: 'setParameter("{}", {})'.format(parameter_values_match.group(1), new_value), + } species_counts_match = re.match(r'^species\.([^\.]+)\((.*?)\)(\.initialCount)?$', target) if species_counts_match: - task.actions.append('setConcentration("{}({})", {})'.format( - species_counts_match.group(1), species_counts_match.group(2), new_value)) - return + return { + 'type': 'append_action', + 'action': lambda new_value: 'setConcentration("{}({})", {})'.format( + species_counts_match.group(1), species_counts_match.group(2), new_value), + } functions_expression_match = re.match(r'^functions\.([^\.\(\)]+)(\.expression)?$', target) if functions_expression_match: @@ -94,14 +104,17 @@ def add_model_attribute_change_to_task(task, change): match = re.match(pattern, line) if match: func_changed = True - block[i_line] = '{}({}) = {}'.format(obj_id, match.group(1), new_value) + return { + 'type': 'replace_line_in_block', + 'block': block, + 'i_line': i_line, + 'new_line': lambda new_value: '{}({}) = {}'.format(obj_id, match.group(1), new_value), + } if not func_changed: raise ValueError(('The expression of function `{}` cannot be changed ' 'because the model does not have a function with this id.').format(obj_id)) - return - function_args_expression_match = re.match(r'^functions\.([^\.]+)\((.*?)\)(\.expression)?$', target) if function_args_expression_match: obj_id = function_args_expression_match.group(1) @@ -115,14 +128,17 @@ def add_model_attribute_change_to_task(task, change): match = re.match(pattern, line) if match: func_changed = True - block[i_line] = '{}({}) = {}'.format(obj_id, obj_args, new_value) + return { + 'type': 'replace_line_in_block', + 'block': block, + 'i_line': i_line, + 'new_line': lambda new_value: '{}({}) = {}'.format(obj_id, obj_args, new_value), + } if not func_changed: raise ValueError(('The expression of function `{}` cannot be changed ' 'because the model does not have a function with this id.').format(obj_id)) - return - target_patterns = { 'compartment size': compartment_size_match, 'parameter value': parameter_values_match, @@ -135,6 +151,33 @@ def add_model_attribute_change_to_task(task, change): raise NotImplementedError(msg) +def add_model_attribute_change_to_task(task, change, preprocessed_change=None): + """ Encode SED model attribute changes into a BioNetGen task + + * Compartment sizes: targets should follow the pattern ``compartments..size`` + * Function expressions: targets should follow the pattern ``functions..expression`` + * Initial species counts: targets should follow the pattern ``species..count`` + * Parameter values: targets should follow the pattern ``parameters..value`` + + Args: + task (:obj:`Task`): BioNetGen task + change (:obj:`ModelAttributeChange`): model attribute change + preprocessed_change (:obj:`dict`): preprocessed information about the change + + Raises: + :obj:`ValueError`: if a target of a change is not valid + """ + if preprocessed_change is None: + preprocessed_change = preprocess_model_attribute_change(task, change) + + new_value = change.new_value + + if preprocessed_change['type'] == 'replace_line_in_block': + preprocessed_change['block'][preprocessed_change['i_line']] = preprocessed_change['new_line'](new_value) + else: + task.actions.append(preprocessed_change['action'](new_value)) + + def add_variables_to_model(model, variables): """ Encode SED variables into observables in a BioNetGen task