diff --git a/Dockerfile b/Dockerfile index 700c3d5..6b71507 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ # Base OS FROM python:3.9-slim-buster -ARG VERSION="0.1.16" +ARG VERSION="0.1.17" ARG SIMULATOR_VERSION="0.22.1" # metadata diff --git a/biosimulators_cobrapy/_version.py b/biosimulators_cobrapy/_version.py index b7e15fa..b04cffb 100644 --- a/biosimulators_cobrapy/_version.py +++ b/biosimulators_cobrapy/_version.py @@ -1 +1 @@ -__version__ = '0.1.16' +__version__ = '0.1.17' diff --git a/biosimulators_cobrapy/core.py b/biosimulators_cobrapy/core.py index 88c5d95..8501c1f 100644 --- a/biosimulators_cobrapy/core.py +++ b/biosimulators_cobrapy/core.py @@ -19,7 +19,6 @@ from biosimulators_utils.sedml.data_model import (Task, ModelLanguage, ModelAttributeChange, SteadyStateSimulation, # noqa: F401 Variable) from biosimulators_utils.sedml.exec import exec_sed_doc as base_exec_sed_doc -from biosimulators_utils.sedml.utils import apply_changes_to_xml_model from biosimulators_utils.simulator.utils import get_algorithm_substitution_policy from biosimulators_utils.utils.core import raise_errors_warnings from biosimulators_utils.warnings import warn, BioSimulatorsWarning @@ -30,7 +29,6 @@ import cobra.io import copy import os -import tempfile __all__ = [ 'exec_sedml_docs_in_combine_archive', @@ -139,40 +137,23 @@ 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) + # get model + cobra_model = preprocessed_task['model']['model'] + # modify model - raise_errors_warnings(validation.validate_model_change_types(task.model.changes, (ModelAttributeChange, )), - error_summary='Changes for model `{}` are not supported.'.format(task.model.id)) if task.model.changes: - model_etree = preprocessed_task['model']['etree'] - - model = copy.deepcopy(task.model) - for change in model.changes: - change.new_value = str(change.new_value) - - apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=None) - - model_file, model_filename = tempfile.mkstemp(suffix='.xml') - os.close(model_file) + raise_errors_warnings(validation.validate_model_change_types(task.model.changes, (ModelAttributeChange, )), + error_summary='Changes for model `{}` are not supported.'.format(task.model.id)) - model_etree.write(model_filename, - xml_declaration=True, - encoding="utf-8", - standalone=False, - pretty_print=False) - else: - model_filename = task.model.source - - # get the model - cobra_model = cobra.io.read_sbml_model(model_filename) - if task.model.changes: - os.remove(model_filename) + model_change_obj_attr_map = preprocessed_task['model']['model_change_obj_attr_map'] + for change in task.model.changes: + model_obj, attr_name = model_change_obj_attr_map[change.target] + new_value = float(change.new_value) + setattr(model_obj, attr_name, new_value) variable_xpath_sbml_id_map = preprocessed_task['model']['variable_xpath_sbml_id_map'] variable_xpath_sbml_fbc_id_map = preprocessed_task['model']['variable_xpath_sbml_fbc_id_map'] - # set solver - cobra_model.solver = preprocessed_task['simulation']['solver'] - # Load the simulation method specified by ``sim.algorithm`` method_props = preprocessed_task['simulation']['method_props'] method_kw_args = copy.copy(preprocessed_task['simulation']['method_kw_args']) @@ -241,6 +222,50 @@ def preprocess_sed_task(task, variables, config=None): model_etree = etree.parse(model.source) namespaces = get_namespaces_for_xml_doc(model_etree) + # Read the model + cobra_model = cobra.io.read_sbml_model(model.source) + + # preprocess model changes + model_change_sbml_id_map = validation.validate_target_xpaths( + model.changes, model_etree, attr='id') + model_change_obj_attr_map = {} + sbml_id_model_obj_map = {'R_' + reaction.id: reaction for reaction in cobra_model.reactions} + namespaces_list = namespaces.values() + invalid_changes = [] + for change in model.changes: + sbml_id = model_change_sbml_id_map[change.target] + model_obj = sbml_id_model_obj_map.get(sbml_id, None) + + attr_name = None + + if model_obj is not None: + _, sep, attr = change.target.partition('/@') + ns, _, attr = attr.partition(':') + if change.target_namespaces.get(ns, None) in namespaces_list: + if attr == 'lowerFluxBound': + attr_name = 'lower_bound' + elif attr == 'upperFluxBound': + attr_name = 'upper_bound' + + if attr_name: + model_change_obj_attr_map[change.target] = (model_obj, attr_name) + else: + invalid_changes.append(change.target) + + if invalid_changes: + valid_changes = [] + for reaction in cobra_model.reactions: + valid_changes.append( + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_{}']/@fbc:lowerFluxBound".format(reaction.id)) + valid_changes.append( + "/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_{}']/@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) + # preprocess variables variable_xpath_sbml_id_map = validation.validate_target_xpaths( variables, model_etree, attr='id') @@ -256,9 +281,6 @@ def preprocess_sed_task(task, variables, config=None): } ) - # Read the model - cobra_model = cobra.io.read_sbml_model(model.source) - # get the SBML-FBC id of the active objective active_objective_sbml_fbc_id = get_active_objective_sbml_fbc_id(model.source) @@ -306,8 +328,9 @@ def preprocess_sed_task(task, variables, config=None): # Return processed information about the task return { 'model': { - 'etree': model_etree, + 'model': cobra_model, 'active_objective_sbml_fbc_id': active_objective_sbml_fbc_id, + 'model_change_obj_attr_map': model_change_obj_attr_map, 'variable_xpath_sbml_id_map': variable_xpath_sbml_id_map, 'variable_xpath_sbml_fbc_id_map': variable_xpath_sbml_fbc_id_map, }, @@ -315,6 +338,5 @@ def preprocess_sed_task(task, variables, config=None): 'algorithm_kisao_id': exec_kisao_id, 'method_props': method_props, 'method_kw_args': method_kw_args, - 'solver': cobra_model.solver, } } diff --git a/requirements.txt b/requirements.txt index d9ae9ec..05ec12a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -biosimulators_utils[logging] >= 0.1.119 +biosimulators_utils[logging] >= 0.1.121 cobra kisao lxml diff --git a/tests/test_core_main.py b/tests/test_core_main.py index ee58da2..3807a1e 100644 --- a/tests/test_core_main.py +++ b/tests/test_core_main.py @@ -226,13 +226,24 @@ def test_exec_sed_task_with_changes(self): 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=10, + )) 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'].tolist(), 0.8739215069684301, rtol=1e-4, atol=1e-8) task.model.changes.append(sedml_data_model.ModelAttributeChange( - target="/sbml:sbml/sbml:model/sbml:listOfParameters/sbml:parameter[@id='R_EX_glc__D_e_lower_bound']/@value", + target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_EX_glc__D_e']/@fbc:lowerFluxBound", target_namespaces=self.NAMESPACES, new_value=-1, )) @@ -244,6 +255,26 @@ def test_exec_sed_task_with_changes(self): self.assertLess(results3['active_objective'].tolist(), results['active_objective'].tolist()) self.assertGreater(results3['active_objective'].tolist(), results2['active_objective'].tolist()) + task.model.changes = [ + sedml_data_model.ModelAttributeChange( + target="/sbml:sbml", + target_namespaces=self.NAMESPACES, + new_value=10, + ), + sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfParameters/sbml:parameter[@id='R_EX_glc__D_e_lower_bound']/@value", + target_namespaces=self.NAMESPACES, + new_value=10, + ), + sedml_data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfReactions/sbml:reaction[@id='R_EX_glc__D_e']/@fbc:id", + target_namespaces=self.NAMESPACES, + new_value=10, + ), + ] + with self.assertRaises(ValueError): + core.preprocess_sed_task(task, variables) + task.model.source = 'not a file' with self.assertRaises(FileNotFoundError): core.preprocess_sed_task(task, variables)