diff --git a/Dockerfile b/Dockerfile index 1afeb89..dfe54a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Base OS FROM python:3.9-slim-buster -ARG VERSION="0.1.12" +ARG VERSION="0.1.13" ARG SIMULATOR_VERSION=0.7.25 # metadata diff --git a/biosimulators_cbmpy/_version.py b/biosimulators_cbmpy/_version.py index e6d0c4f..377e1f6 100644 --- a/biosimulators_cbmpy/_version.py +++ b/biosimulators_cbmpy/_version.py @@ -1 +1 @@ -__version__ = '0.1.12' +__version__ = '0.1.13' diff --git a/biosimulators_cbmpy/core.py b/biosimulators_cbmpy/core.py index 41ba85c..bf92ba5 100644 --- a/biosimulators_cbmpy/core.py +++ b/biosimulators_cbmpy/core.py @@ -10,14 +10,15 @@ from .utils import (apply_algorithm_change_to_simulation_module_method_args, apply_variables_to_simulation_module_method_args, get_simulation_method_args, validate_variables, - get_results_of_variables, + get_results_paths_for_variables, get_results_of_variables, get_default_solver_module_function_args) from biosimulators_utils.combine.exec import exec_sedml_docs_in_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.model_lang.sbml.utils import get_package_namespace as get_sbml_package_namespace 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.data_model import (Task, ModelLanguage, SteadyStateSimulation, # noqa: F401 +from biosimulators_utils.sedml.data_model import (Task, ModelLanguage, ModelAttributeChange, SteadyStateSimulation, # noqa: F401 Variable) from biosimulators_utils.sedml import validation from biosimulators_utils.sedml.exec import exec_sed_doc as base_exec_sed_doc @@ -29,6 +30,7 @@ from kisao.utils import get_preferred_substitute_algorithm_by_ids from lxml import etree import cbmpy +import copy __all__ = [ 'exec_sedml_docs_in_combine_archive', @@ -112,7 +114,7 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None Args: task (:obj:`Task`): 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 @@ -130,12 +132,73 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None :obj:`NotImplementedError`: if the task is not of a supported type or involves an unsuported feature ''' 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) + model = task.model + + # Read the model + cbmpy_model = preprocessed_task['model']['model'] + + # modify model + if model.changes: + raise_errors_warnings(validation.validate_model_change_types(model.changes, (ModelAttributeChange,)), + error_summary='Changes for model `{}` are not supported.'.format(model.id)) + model_change_setter_map = preprocessed_task['model']['model_change_setter_map'] + for change in model.changes: + new_value = float(change.new_value) + model_change_setter_map[change.target](new_value) + + # Set up simulation function and its keyword arguments + variable_target_sbml_id_map = preprocessed_task['model']['variable_target_sbml_id_map'] + method_props = preprocessed_task['simulation']['method_props'] + simulation_method_args = copy.copy(preprocessed_task['simulation']['method_args']) + apply_variables_to_simulation_module_method_args(variable_target_sbml_id_map, method_props, variables, simulation_method_args) + + # Simulate the model + simulation_method = preprocessed_task['simulation']['method'] + solution = simulation_method(cbmpy_model, **simulation_method_args) + + # throw error if status isn't optimal + module_method_args = preprocessed_task['simulation']['module_method_args'] + method_props['raise_if_simulation_error'](module_method_args, solution) + + # get the result of each variable + variable_results = get_results_of_variables( + preprocessed_task['model']['variable_target_results_path_map'], + method_props, module_method_args['solver'], + variables, cbmpy_model, solution) + + # log action + if config.LOG: + log.algorithm = preprocessed_task['simulation']['algorithm_kisao_id'] + log.simulator_details = { + 'method': simulation_method.__module__ + '.' + simulation_method.__name__, + 'arguments': simulation_method_args, + } + + # return the result of each variable 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() + model = task.model sim = task.simulation @@ -144,7 +207,7 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None error_summary='Task `{}` is invalid.'.format(task.id)) raise_errors_warnings(validation.validate_model_language(model.language, ModelLanguage.SBML), error_summary='Language for model `{}` is not supported.'.format(model.id)) - raise_errors_warnings(validation.validate_model_change_types(model.changes, ()), + raise_errors_warnings(validation.validate_model_change_types(model.changes, (ModelAttributeChange,)), error_summary='Changes for model `{}` are not supported.'.format(model.id)) raise_errors_warnings(*validation.validate_model_changes(model), error_summary='Changes for model `{}` are invalid.'.format(model.id)) @@ -156,38 +219,78 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None error_summary='Data generator variables for task `{}` are invalid.'.format(task.id)) model_etree = etree.parse(model.source) - target_x_paths_ids = validation.validate_target_xpaths( + model_change_sbml_id_map = validation.validate_target_xpaths( + model.changes, model_etree, attr='id') + variable_target_sbml_id_map = validation.validate_target_xpaths( variables, model_etree, attr='id') namespaces = get_namespaces_for_xml_doc(model_etree) - target_x_paths_fbc_ids = validation.validate_target_xpaths( + sbml_fbc_prefix, sbml_fbc_uri = get_sbml_package_namespace('fbc', namespaces) + variable_target_sbml_fbc_id_map = validation.validate_target_xpaths( variables, model_etree, attr={ 'namespace': { - 'prefix': 'fbc', - 'uri': namespaces['fbc'], + 'prefix': sbml_fbc_prefix, + 'uri': sbml_fbc_uri, }, 'name': 'id', } ) # Read the model - model = cbmpy.CBRead.readSBML3FBC(model.source) + cbmpy_model = cbmpy.CBRead.readSBML3FBC(model.source) + + # preprocess model changes + model_change_setter_map = {} + sbml_id_reaction_map = {rxn.id: rxn for rxn in cbmpy_model.reactions} + invalid_changes = [] + for change in model.changes: + target = change.target + sbml_id = model_change_sbml_id_map[target] + reaction = sbml_id_reaction_map.get(sbml_id, None) + setter = None + if reaction: + attr = target.partition('/@')[2] + ns, _, attr = attr.partition(':') + ns = change.target_namespaces.get(ns, None) + + if ns == sbml_fbc_uri: + if attr == 'lowerFluxBound': + setter = reaction.setLowerBound + elif attr == 'upperFluxBound': + setter = reaction.setUpperBound + if setter: + model_change_setter_map[change.target] = setter + else: + invalid_changes.append(change.target) + + if invalid_changes: + valid_changes = [] + for reaction in cbmpy_model.reactions: + valid_changes.append( + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='{}']/@fbc:lowerFluxBound".format(reaction.id)) + valid_changes.append( + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='{}']/@fbc:upperFluxBound".format(reaction.id)) + + msg = 'The following changes are invalid:\n {}\n\nThe following targets are valid:\n {}'.format( + '\n '.join(sorted(invalid_changes)), + '\n '.join(sorted(valid_changes)), + ) + raise ValueError(msg) # Set up the algorithm specified by :obj:`task.simulation.algorithm.kisao_id` - algorithm_kisao_id = sim.algorithm.kisao_id alg_substitution_policy = get_algorithm_substitution_policy(config=config) exec_kisao_id = get_preferred_substitute_algorithm_by_ids( - algorithm_kisao_id, KISAO_ALGORITHMS_PARAMETERS_MAP.keys(), + sim.algorithm.kisao_id, KISAO_ALGORITHMS_PARAMETERS_MAP.keys(), substitution_policy=alg_substitution_policy) method_props = KISAO_ALGORITHMS_PARAMETERS_MAP[exec_kisao_id] # Set up the the parameters of the algorithm module_method_args = get_default_solver_module_function_args(exec_kisao_id) - if exec_kisao_id == algorithm_kisao_id: + if exec_kisao_id == sim.algorithm.kisao_id: for change in sim.algorithm.changes: try: - apply_algorithm_change_to_simulation_module_method_args(method_props, change, model, module_method_args) + apply_algorithm_change_to_simulation_module_method_args(method_props, change, cbmpy_model, module_method_args) except NotImplementedError as exception: if ( ALGORITHM_SUBSTITUTION_POLICY_LEVELS[alg_substitution_policy] @@ -209,50 +312,31 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None else: raise - # validate variables - validate_variables(method_props, variables) - - # set keyword arguments based on desired outputs - apply_variables_to_simulation_module_method_args(target_x_paths_ids, method_props, variables, module_method_args) + # validate selected solver is available if not module_method_args['solver']['module']: raise ModuleNotFoundError('{} solver is not available.'.format(module_method_args['solver']['name'])) # Setup simulation function and its keyword arguments simulation_method, simulation_method_args = get_simulation_method_args(method_props, module_method_args) - # Simulate the model - solution = simulation_method(model, **simulation_method_args) - - # throw error if status isn't optimal - method_props['raise_if_simulation_error'](module_method_args, solution) - - # get the result of each variable - variable_results = get_results_of_variables(target_x_paths_ids, target_x_paths_fbc_ids, - method_props, module_method_args['solver'], - variables, model, solution) - - # log action - if config.LOG: - log.algorithm = exec_kisao_id - log.simulator_details = { - 'method': simulation_method.__module__ + '.' + simulation_method.__name__, - 'arguments': simulation_method_args, + # validate and preprocess variables + validate_variables(cbmpy_model, method_props, variables, variable_target_sbml_id_map, variable_target_sbml_fbc_id_map, sbml_fbc_uri) + variable_target_results_path_map = get_results_paths_for_variables( + cbmpy_model, method_props, variables, variable_target_sbml_id_map, variable_target_sbml_fbc_id_map) + + # return preprocessed information + return { + 'model': { + 'model': cbmpy_model, + 'model_change_setter_map': model_change_setter_map, + 'variable_target_sbml_id_map': variable_target_sbml_id_map, + 'variable_target_results_path_map': variable_target_results_path_map, + }, + 'simulation': { + 'algorithm_kisao_id': exec_kisao_id, + 'method': simulation_method, + 'method_props': method_props, + 'method_args': simulation_method_args, + 'module_method_args': module_method_args, } - - # return the result of each variable 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:`object`: preprocessed information about the task - """ - pass + } diff --git a/biosimulators_cbmpy/data_model.py b/biosimulators_cbmpy/data_model.py index c0039c0..a94390f 100644 --- a/biosimulators_cbmpy/data_model.py +++ b/biosimulators_cbmpy/data_model.py @@ -8,7 +8,6 @@ """ from biosimulators_utils.data_model import ValueType -from numpy import nan import cbmpy # noqa: F401 import collections import numpy @@ -24,8 +23,8 @@ __all__ = [ 'SOLVERS', 'OPTIMIZATION_METHODS', - 'FBA_DEPENDENT_VARIABLE_TARGETS', - 'FVA_DEPENDENT_VARIABLE_TARGETS', + 'FBA_OUTPUT_VARIABLE_TARGETS', + 'FVA_OUTPUT_VARIABLE_TARGETS', 'KISAO_ALGORITHMS_PARAMETERS_MAP', ] @@ -69,57 +68,86 @@ 'exact', ]) -FBA_DEPENDENT_VARIABLE_TARGETS = [ +FBA_OUTPUT_VARIABLE_TARGETS = [ { 'description': 'objective value', 'target_type': 'objective', 'target': r'^/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective(\[.*?\])?(/@value)?$', - 'get_result': - lambda el_id, el_fbc_id, solution: - solution['objective_value'].get(el_fbc_id, nan), + 'get_target_results_paths': lambda model: + ( + [ + (None, objective.id, None, 'objective_value', objective.id) + for objective in model.objectives + ] + + [ + (None, objective.id, 'value', 'objective_value', objective.id) + for objective in model.objectives + ] + ), }, { 'description': 'reaction flux', 'target_type': 'reaction', 'target': r'^/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction(\[.*?\])?(/@flux)?$', - 'get_result': - lambda el_id, el_fbc_id, solution: - solution['reaction_flux'].get(el_id), + 'get_target_results_paths': lambda model: + ( + [ + (reaction.id, None, None, 'reaction_flux', reaction.id) + for reaction in model.reactions + ] + + [ + (reaction.id, None, 'flux', 'reaction_flux', reaction.id) + for reaction in model.reactions + ] + ), }, { 'description': 'reaction reduced cost', 'target_type': 'reaction', 'target': r'^/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction(\[.*?\])?/@reducedCost$', - 'get_result': - lambda el_id, el_fbc_id, solution: - solution['reaction_reduced_cost'][el_id], + 'get_target_results_paths': lambda model: + [ + (reaction.id, None, 'reducedCost', 'reaction_reduced_cost', reaction.id) + for reaction in model.reactions + ], }, { 'description': 'species shadow price', 'target_type': 'species', 'target': r'^/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species(\[.*?\])?(/@shadowPrice)?$', - 'get_result': - lambda el_id, el_fbc_id, solution: - solution['species_shadow_price'][el_id], + 'get_target_results_paths': lambda model: + ( + [ + (specie.id, None, None, 'species_shadow_price', specie.id) + for specie in model.species + ] + [ + (specie.id, None, 'shadowPrice', 'species_shadow_price', specie.id) + for specie in model.species + ] + ), }, ] -FVA_DEPENDENT_VARIABLE_TARGETS = [ +FVA_OUTPUT_VARIABLE_TARGETS = [ { 'description': 'minimum reaction flux', 'target_type': 'reaction', 'target': r'^/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction(\[.*?\])?/@minFlux?$', - 'get_result': - lambda el_id, el_fbc_id, solution: - solution['reaction_min_flux'][el_id], + 'get_target_results_paths': lambda model: + [ + (reaction.id, None, 'minFlux', 'reaction_min_flux', reaction.id) + for reaction in model.reactions + ], }, { 'description': 'maximum reaction flux', 'target_type': 'reaction', 'target': r'^/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction(\[.*?\])?/@maxFlux?$', - 'get_result': - lambda el_id, el_fbc_id, solution: - solution['reaction_max_flux'][el_id], + 'get_target_results_paths': lambda model: + [ + (reaction.id, None, 'maxFlux', 'reaction_max_flux', reaction.id) + for reaction in model.reactions + ], }, ] @@ -250,7 +278,7 @@ def get_fva_results(method_props, solver, model, solution): 'with_reduced_costs': True, 'return_lp_obj': True, }, - 'variables': FBA_DEPENDENT_VARIABLE_TARGETS, + 'variables': FBA_OUTPUT_VARIABLE_TARGETS, 'raise_if_simulation_error': raise_if_fba_simulation_error, 'get_results': get_fba_results, }), @@ -284,7 +312,7 @@ def get_fva_results(method_props, solver, model, solution): 'with_reduced_costs': True, 'return_lp_obj': True, }, - 'variables': FBA_DEPENDENT_VARIABLE_TARGETS, + 'variables': FBA_OUTPUT_VARIABLE_TARGETS, 'raise_if_simulation_error': raise_if_fba_simulation_error, 'get_results': get_fba_results, }), @@ -312,7 +340,7 @@ def get_fva_results(method_props, solver, model, solution): 'default_args': { 'return_lp_obj': True, }, - 'variables': FBA_DEPENDENT_VARIABLE_TARGETS, + 'variables': FBA_OUTPUT_VARIABLE_TARGETS, 'raise_if_simulation_error': lambda module_method_args, opt_solution: raise_if_fba_simulation_error(module_method_args, opt_solution[1]), @@ -343,7 +371,7 @@ def get_fva_results(method_props, solver, model, solution): }, 'default_args': { }, - 'variables': FVA_DEPENDENT_VARIABLE_TARGETS, + 'variables': FVA_OUTPUT_VARIABLE_TARGETS, 'raise_if_simulation_error': lambda module_method_args, solution: None, 'get_results': get_fva_results, }) diff --git a/biosimulators_cbmpy/utils.py b/biosimulators_cbmpy/utils.py index eb2aca1..c56a0b2 100644 --- a/biosimulators_cbmpy/utils.py +++ b/biosimulators_cbmpy/utils.py @@ -11,7 +11,6 @@ from biosimulators_utils.sedml.data_model import Variable # noqa: F401 from biosimulators_utils.utils.core import validate_str_value, parse_value import numpy -import re import types # noqa: F401 __all__ = [ @@ -111,22 +110,21 @@ def apply_algorithm_change_to_simulation_module_method_args(method_props, argume module_method_args['args'][parameter_props['arg_name']] = parsed_value -def apply_variables_to_simulation_module_method_args(target_x_paths_ids, method_props, variables, module_method_args): +def apply_variables_to_simulation_module_method_args(target_sbml_id_map, method_props, variables, solver_method_args): """ Encode the desired output variables into arguments to simulation methods Args: - target_x_paths_ids (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the + target_sbml_id_map (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the SBML id of the corresponding model object method_props (:obj:`dict`): properties of the simulation method variables (:obj:`list` of :obj:`Variable`): variables that should be recorded - module_method_args (:obj:`dict`): dictionary representing the desired simulation function, - its parent module, and the desired keyword arguments to the function + solver_method_args (:obj:`dict`): keyword arguments for the simulation method """ - if method_props['kisao_id'] == 'KISAO_0000526': + if method_props['function_suffix'] == 'FluxVariabilityAnalysis': selected_reactions = set() for variable in variables: - selected_reactions.add(target_x_paths_ids[variable.target]) - module_method_args['args']['selected_reactions'] = sorted(selected_reactions) + selected_reactions.add(target_sbml_id_map[variable.target]) + solver_method_args['selected_reactions'] = sorted(selected_reactions) def get_simulation_method_args(method_props, module_method_args): @@ -164,13 +162,24 @@ def get_simulation_method_args(method_props, module_method_args): return solver_method, solver_method_args -def validate_variables(method, variables): +def validate_variables(model, method, variables, target_sbml_id_map, target_sbml_fbc_id_map, sbml_fbc_uri): """ Validate the desired output variables of a simulation Args: + model (:obj:`cbmpy.CBModel.Model`): model method (:obj:`dict`): properties of desired simulation method variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + target_sbml_id_map (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the + SBML id of the corresponding model object + target_sbml_fbc_id_map (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the + SBML-FBC id of the corresponding model object + sbml_fbc_uri (:obj:`str`): URI for SBML FBC package """ + possible_target_results_path_map = set() + for variable_pattern in method['variables']: + for sbml_id, fbc_id, attr, _, _ in variable_pattern['get_target_results_paths'](model): + possible_target_results_path_map.add((sbml_id, fbc_id, attr)) + invalid_symbols = set() invalid_targets = set() for variable in variables: @@ -178,13 +187,18 @@ def validate_variables(method, variables): invalid_symbols.add(variable.symbol) else: - valid = False - for variable_pattern in method['variables']: - if re.match(variable_pattern['target'], variable.target): - valid = True - break - - if not valid: + valid = True + + target = variable.target + variable_target_id = target_sbml_id_map.get(target, None) + variable_target_fbc_id = target_sbml_fbc_id_map.get(target, None) + target_attr = target.partition('/@')[2] or None + if target_attr: + target_ns, _, target_attr = target_attr.rpartition(':') + if target_ns and variable.target_namespaces.get(target_ns, None) != sbml_fbc_uri: + valid = False + + if not valid or (variable_target_id, variable_target_fbc_id, target_attr) not in possible_target_results_path_map: invalid_targets.add(variable.target) if invalid_symbols: @@ -206,15 +220,44 @@ def validate_variables(method, variables): raise ValueError(msg) -def get_results_of_variables(target_x_paths_ids, target_x_paths_fbc_ids, method_props, solver, - variables, model, solution): - """ Get the results of the desired variables +def get_results_paths_for_variables(model, method_props, variables, target_sbml_id_map, target_sbml_fbc_id_map): + """ Get the path to results for the desired variables Args: - target_x_paths_ids (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the + model (:obj:`cbmpy.CBModel.Model`): model + method_props (:obj:`dict`): properties of desired simulation method + variables (:obj:`list` of :obj:`Variable`): variables that should be recorded + target_sbml_id_map (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the SBML id of the corresponding model object - target_x_paths_fbc_ids (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the + target_sbml_fbc_id_map (:obj:`dict` of :obj:`str` to :obj:`str`): dictionary that maps each XPath to the SBML-FBC id of the corresponding model object + + Returns: + :obj:`dict`: path to results of desired variables + """ + possible_target_results_path_map = {} + for variable_pattern in method_props['variables']: + for sbml_id, fbc_id, attr, result_type, result_name in variable_pattern['get_target_results_paths'](model): + possible_target_results_path_map[(sbml_id, fbc_id, attr)] = (result_type, result_name) + + target_results_path_map = {} + for variable in variables: + target = variable.target + variable_target_id = target_sbml_id_map[target] + variable_target_fbc_id = target_sbml_fbc_id_map[target] + target_attr = target.partition('/@')[2].rpartition(':')[2] or None + target_results_path_map[variable.target] = possible_target_results_path_map[( + variable_target_id, variable_target_fbc_id, target_attr)] + + return target_results_path_map + + +def get_results_of_variables(target_results_path_map, method_props, solver, + variables, model, solution): + """ Get the results of the desired variables + + Args: + target_results_path_map (:obj:`dict`): path to results of desired variables method_props (:obj:`dict`): properties of desired simulation method solver (:obj:`dict`): dictionary representing the desired simulation function, its parent module, and the desired keyword arguments to the function @@ -229,16 +272,8 @@ def get_results_of_variables(target_x_paths_ids, target_x_paths_fbc_ids, method_ variable_results = VariableResults() for variable in variables: - target = variable.target - for variable_pattern in method_props['variables']: - if re.match(variable_pattern['target'], target): - variable_target_id = target_x_paths_ids[target] - variable_target_fbc_id = target_x_paths_fbc_ids[target] - result = variable_pattern['get_result'](variable_target_id, - variable_target_fbc_id, all_values) - - break - + result_type, result_name = target_results_path_map[variable.target] + result = all_values[result_type].get(result_name, numpy.nan) variable_results[variable.id] = numpy.array(result) return variable_results diff --git a/requirements.txt b/requirements.txt index 08844a4..f0fbcfd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -biosimulators_utils[logging] >= 0.1.116 +biosimulators_utils[logging] >= 0.1.122 cbmpy <= 0.7.25 glpk kisao diff --git a/tests/test_core_main.py b/tests/test_core_main.py index e3e4f56..e995a2b 100644 --- a/tests/test_core_main.py +++ b/tests/test_core_main.py @@ -285,6 +285,89 @@ def test_exec_sed_task_successfully(self): for var_id, result in variable_results.items(): numpy.testing.assert_allclose(result, numpy.array(expected_results[var_id]), rtol=1e-4, atol=1e-8) + def test_exec_sed_task_with_changes(self): + task = sedml_data_model.Task( + model=sedml_data_model.Model( + source=os.path.join(os.path.dirname(__file__), 'fixtures', 'textbook.xml'), + language=sedml_data_model.ModelLanguage.SBML.value, + ), + simulation=sedml_data_model.SteadyStateSimulation( + algorithm=sedml_data_model.Algorithm( + kisao_id='KISAO_0000437', + changes=[ + sedml_data_model.AlgorithmParameterChange( + kisao_id='KISAO_0000553', + new_value='GLPK', + ), + ], + ), + ), + ) + + variables = [ + sedml_data_model.Variable( + id='active_objective', + target="/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']/@value", + target_namespaces=self.NAMESPACES, + task=task), + ] + + task.model.changes.append(sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_EX_glc__D_e']/@fbc:lowerFluxBound", + target_namespaces=self.NAMESPACES, + new_value=-1, + )) + task.model.changes.append(sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_EX_glc__D_e']/@fbc:upperFluxBound", + target_namespaces=self.NAMESPACES, + new_value=-1, + )) + preprocessed_task = core.preprocess_sed_task(task, variables) + + task.model.changes = [] + results, _ = core.exec_sed_task(task, variables, preprocessed_task=preprocessed_task) + numpy.testing.assert_allclose(results['active_objective'], 0.8739215069684301) + + task.model.changes = [ + sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_EX_glc__D_e']/@fbc:lowerFluxBound", + target_namespaces=self.NAMESPACES, + new_value=-1, + ), + ] + results2, _ = core.exec_sed_task(task, variables, preprocessed_task=preprocessed_task) + numpy.testing.assert_allclose(results2['active_objective'], 0.048384972296965874) + + task.model.changes = [ + sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_EX_glc__D_e']/@fbc:lowerFluxBound", + target_namespaces=self.NAMESPACES, + new_value='-1', + ), + ] + results2, _ = core.exec_sed_task(task, variables, preprocessed_task=preprocessed_task) + numpy.testing.assert_allclose(results2['active_objective'], 0.048384972296965874) + + task.model.changes = [ + sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_EX_glc__D_e']/@id", + target_namespaces=self.NAMESPACES, + new_value=-1, + ), + ] + with self.assertRaises(ValueError): + core.preprocess_sed_task(task, variables) + + task.model.changes = [ + sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='M_13dpg_c']", + target_namespaces=self.NAMESPACES, + new_value=-1, + ), + ] + with self.assertRaises(ValueError): + core.preprocess_sed_task(task, variables) + def test_exec_sed_task_error_handling_unsupported_algorithm(self): task = sedml_data_model.Task( model=sedml_data_model.Model( diff --git a/tests/test_utils.py b/tests/test_utils.py index eff235e..ed756f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ from biosimulators_cbmpy.utils import (apply_algorithm_change_to_simulation_module_method_args, apply_variables_to_simulation_module_method_args, get_simulation_method_args, - validate_variables, get_results_of_variables, + validate_variables, get_results_paths_for_variables, get_results_of_variables, get_default_solver_module_function_args) from biosimulators_utils.sedml.data_model import AlgorithmParameterChange, Variable from numpy import nan @@ -122,15 +122,15 @@ def test_apply_variables_to_simulation_module_method_args(self): # FVA module_method_args = {'args': {}} expected_module_method_args = {'args': {'selected_reactions': ['A', 'B', 'C']}} - apply_variables_to_simulation_module_method_args(target_x_paths_ids, method_props, variables, module_method_args) - self.assertEqual(module_method_args, expected_module_method_args) + apply_variables_to_simulation_module_method_args(target_x_paths_ids, method_props, variables, module_method_args['args']) + self.assertEqual(module_method_args['args'], expected_module_method_args['args']) # FBA method_props = KISAO_ALGORITHMS_PARAMETERS_MAP['KISAO_0000437'] module_method_args = {'args': {}} expected_module_method_args = {'args': {}} - apply_variables_to_simulation_module_method_args(target_x_paths_ids, method_props, variables, module_method_args) - self.assertEqual(module_method_args, expected_module_method_args) + apply_variables_to_simulation_module_method_args(target_x_paths_ids, method_props, variables, module_method_args['args']) + self.assertEqual(module_method_args['args'], expected_module_method_args['args']) def test_get_simulation_method_args(self): method_props = KISAO_ALGORITHMS_PARAMETERS_MAP['KISAO_0000437'] @@ -164,6 +164,46 @@ def test_get_simulation_method_args(self): def test_validate_variables(self): method_props = KISAO_ALGORITHMS_PARAMETERS_MAP['KISAO_0000437'] + + model = cbmpy.CBRead.readSBML3FBC(self.MODEL_FILENAME) + variable_target_sbml_id_map = { + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']/@value": None, + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:type='maximize']/@value": None, + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']": None, + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective/@value": None, + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']/@flux": 'R_ACALD', + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']/@reducedCost": 'R_ACALD', + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@metaid='R_ACALD']/@flux": 'R_ACALD', + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']": 'R_ACALD', + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction/@flux": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction": None, + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='M_13dpg_c']/@shadowPrice": 'M_13dpg_c', + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@metaid='M_13dpg_c']/@shadowPrice": 'M_13dpg_c', + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='M_13dpg_c']": 'M_13dpg_c', + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species/@shadowPrice": None, + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species": None, + } + variable_target_sbml_fbc_id_map = { + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']/@value": 'obj', + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:type='maximize']/@value": 'obj', + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']": 'obj', + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective/@value": None, + "/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']/@flux": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']/@reducedCost": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@metaid='R_ACALD']/@flux": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction/@flux": None, + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction": None, + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='M_13dpg_c']/@shadowPrice": None, + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@metaid='M_13dpg_c']/@shadowPrice": None, + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='M_13dpg_c']": None, + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species/@shadowPrice": None, + "/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species": None, + } + sbml_fbc_uri = self.NAMESPACES['fbc'] + variables = [ Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']/@value"), @@ -171,10 +211,6 @@ def test_validate_variables(self): target="/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:type='maximize']/@value"), Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']"), - Variable(target_namespaces=self.NAMESPACES, - target="/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective/@value"), - Variable(target_namespaces=self.NAMESPACES, - target="/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective"), Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']/@flux"), Variable(target_namespaces=self.NAMESPACES, @@ -183,35 +219,34 @@ def test_validate_variables(self): target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@metaid='R_ACALD']/@flux"), Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_ACALD']"), - Variable(target_namespaces=self.NAMESPACES, - target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction/@flux"), - Variable(target_namespaces=self.NAMESPACES, - target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction"), Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='M_13dpg_c']/@shadowPrice"), Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@metaid='M_13dpg_c']/@shadowPrice"), Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='M_13dpg_c']"), - Variable(target_namespaces=self.NAMESPACES, - target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species/@shadowPrice"), - Variable(target_namespaces=self.NAMESPACES, - target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species"), ] - validate_variables(method_props, variables) + validate_variables(model, method_props, variables, variable_target_sbml_id_map, variable_target_sbml_fbc_id_map, sbml_fbc_uri) variables = [ Variable(symbol='urn:sedml:symbol:time'), ] with self.assertRaises(NotImplementedError): - validate_variables(method_props, variables) + validate_variables(model, method_props, variables, variable_target_sbml_id_map, variable_target_sbml_fbc_id_map, sbml_fbc_uri) variables = [ Variable(target_namespaces=self.NAMESPACES, target="/sbml:sbml/sbml:model/sbml:listOfCompartments/sbml:compartment[@id='c']") ] with self.assertRaises(ValueError): - validate_variables(method_props, variables) + validate_variables(model, method_props, variables, variable_target_sbml_id_map, variable_target_sbml_fbc_id_map, sbml_fbc_uri) + + variables = [ + Variable(target_namespaces=self.NAMESPACES, + target="/sbml:sbml/sbml:model/sbml:listOfCompartments/sbml:compartment[@id='c']/@sbml:name") + ] + with self.assertRaises(ValueError): + validate_variables(model, method_props, variables, variable_target_sbml_id_map, variable_target_sbml_fbc_id_map, sbml_fbc_uri) def test_get_results_of_variables(self): method_props = KISAO_ALGORITHMS_PARAMETERS_MAP['KISAO_0000437'] @@ -260,8 +295,9 @@ def test_get_results_of_variables(self): getActiveObjective=lambda: mock.Mock(id='obj'), getObjFuncValue=lambda: 0.8739215069684909, getReactionValues=lambda: {'R_ACALD': 1.250555e-12}, + objectives=[mock.Mock(id='obj'), mock.Mock(id='inactive_obj')], species=[mock.Mock(id='M_13dpg_c'), mock.Mock(id='M_2pg_c')], - reactions=[mock.Mock(id='R_ACALD')], + reactions=[mock.Mock(id='R_ACALD'), mock.Mock(id='R_PGI'), mock.Mock(id='R_PPC')], ) # FBA, GLPK @@ -274,7 +310,8 @@ def test_get_results_of_variables(self): ) } solution = None - result = get_results_of_variables(target_to_id, target_to_fbc_id, method_props, solver, variables, model, solution) + target_results_path_map = get_results_paths_for_variables(model, method_props, variables, target_to_id, target_to_fbc_id) + result = get_results_of_variables(target_results_path_map, method_props, solver, variables, model, solution) self.assertEqual(set(result.keys()), set(var.id for var in variables)) numpy.testing.assert_allclose(result['obj'], numpy.array(0.8739215069684909)) numpy.testing.assert_allclose(result['inactive_obj'], numpy.array(nan)) @@ -299,7 +336,8 @@ def test_get_results_of_variables(self): ) } solution = None - result = get_results_of_variables(target_to_id, target_to_fbc_id, method_props, solver, variables, model, solution) + target_results_path_map = get_results_paths_for_variables(model, method_props, variables, target_to_id, target_to_fbc_id) + result = get_results_of_variables(target_results_path_map, method_props, solver, variables, model, solution) self.assertEqual(set(result.keys()), set(var.id for var in variables)) numpy.testing.assert_allclose(result['obj'], numpy.array(0.8739215069684909)) numpy.testing.assert_allclose(result['inactive_obj'], numpy.array(nan)) @@ -318,7 +356,8 @@ def test_get_results_of_variables(self): 'module': mock.Mock() } solution = (None, None) - result = get_results_of_variables(target_to_id, target_to_fbc_id, method_props, solver, variables, model, solution) + target_results_path_map = get_results_paths_for_variables(model, method_props, variables, target_to_id, target_to_fbc_id) + result = get_results_of_variables(target_results_path_map, method_props, solver, variables, model, solution) self.assertEqual(set(result.keys()), set(var.id for var in variables)) numpy.testing.assert_allclose(result['obj'], numpy.array(0.8739215069684909)) numpy.testing.assert_allclose(result['inactive_obj'], numpy.array(nan)) @@ -356,7 +395,8 @@ def test_get_results_of_variables(self): ), ['R_ACALD', 'R_PGI', 'R_PPC'] ) - result = get_results_of_variables(target_to_id, target_to_fbc_id, method_props, solver, variables, model, solution) + target_results_path_map = get_results_paths_for_variables(model, method_props, variables, target_to_id, target_to_fbc_id) + result = get_results_of_variables(target_results_path_map, method_props, solver, variables, model, solution) numpy.testing.assert_allclose(result['R_PGI_min_flux'], numpy.array(-15.)) numpy.testing.assert_allclose(result['R_PGI_max_flux'], numpy.array(25.))