Skip to content

Commit

Permalink
Merge pull request #154 from biosimulators/sys-bio-model-fixes
Browse files Browse the repository at this point in the history
Sys bio model fixes
  • Loading branch information
CodeByDrescher authored Oct 4, 2024
2 parents 5ce1030 + 0f5be30 commit e0fe4eb
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 102 deletions.
5 changes: 4 additions & 1 deletion biosimulators_utils/sedml/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ def exec_task(task, variables, preprocessed_task=None, log=None, config=None, **
' ' * 2 * (indent + 2),
('\n' + ' ' * 2 * (indent + 2)).join(sorted('`' + output.id + '`' for output in doc.outputs)),
))
for i_task, task in enumerate(expected_tasks):
for i_task in range(0, len(expected_tasks)):
task = expected_tasks[i_task]
task_status = Status.QUEUED
task_exception = None
print('{}Executing task {}: `{}`'.format(' ' * 2 * indent, i_task + 1, task.id))

if config.LOG:
Expand Down
140 changes: 94 additions & 46 deletions biosimulators_utils/sedml/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:Copyright: 2020, Center for Reproducible Biomedical Modeling
:License: MIT
"""
import regex

from ..log.data_model import Status
from ..report.data_model import VariableResults, DataGeneratorResults # noqa: F401
Expand All @@ -13,7 +14,8 @@
from ..xml.utils import eval_xpath
from .data_model import (SedBase, SedIdGroupMixin, SedDocument, # noqa: F401
Model, ModelLanguagePattern, ModelChange, ModelAttributeChange, AddElementModelChange,
ReplaceElementModelChange, RemoveElementModelChange, ComputeModelChange, SetValueComputeModelChange,
ReplaceElementModelChange, RemoveElementModelChange, ComputeModelChange,
SetValueComputeModelChange,
OneStepSimulation, SteadyStateSimulation, UniformTimeCourseSimulation,
Task, RepeatedTask, Output, Report, Plot, Plot2D, Plot3D,
DataGenerator, Variable,
Expand Down Expand Up @@ -319,7 +321,8 @@ def resolve_model_and_apply_xml_changes(orig_model, sed_doc, working_dir,
try:
model_etree = etree.parse(model.source)
except Exception as exception:
raise ValueError('The model could not be parsed because the model is not a valid XML document: {}'.format(str(exception)))
raise ValueError('The model could not be parsed because the model is not a valid XML document: {}'.format(
str(exception)))

if model.changes:
# Change source here so that tasks point to actual source they can find.
Expand All @@ -334,7 +337,8 @@ def resolve_model_and_apply_xml_changes(orig_model, sed_doc, working_dir,
# write model to file
if save_to_file:
if temp_model_source is None:
modified_model_file, temp_model_source = tempfile.mkstemp(suffix='.xml', dir=os.path.dirname(model.source))
modified_model_file, temp_model_source = tempfile.mkstemp(suffix='.xml',
dir=os.path.dirname(model.source))
os.close(modified_model_file)
model.source = temp_model_source

Expand Down Expand Up @@ -450,7 +454,14 @@ def apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=Non

# First pass: Must-be-XML changes:
non_xml_changes = []
possible_changes = (AddElementModelChange, ReplaceElementModelChange,
RemoveElementModelChange, ModelAttributeChange, ComputeModelChange)
for change in model.changes:
if not isinstance(change, possible_changes):
error_msg = (f"Change {' ' + change.name if change.name else ''} "
f"of type {change.__class__.__name__} is not supported.")
raise NotImplementedError(error_msg)

if isinstance(change, AddElementModelChange):
parents = eval_xpath(model_etree, change.target, change.target_namespaces)

Expand All @@ -473,7 +484,8 @@ def apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=Non
raise ValueError('xpath {} must match a single object'.format(change.target))

try:
new_elements = etree.parse(io.StringIO('<root>' + change.new_elements + '</root>')).getroot().getchildren()
new_elements = etree.parse(
io.StringIO('<root>' + change.new_elements + '</root>')).getroot().getchildren()
except etree.XMLSyntaxError as exception:
raise ValueError('`{}` is invalid XML. {}'.format(change.new_elements, str(exception)))

Expand All @@ -496,41 +508,71 @@ def apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=Non
parent.remove(element)

elif isinstance(change, ModelAttributeChange):
obj_xpath, sep, attr = change.target.rpartition('/@')
if sep != '/@':
change.model = model
non_xml_changes.append(change)
continue
# get object to change
obj_xpath, sep, attr = change.target.rpartition('/@')
if sep != '/@':
raise NotImplementedError('target ' + change.target + ' cannot be changed by XML manipulation, as the target '
'is not an attribute of a model element')
objs = eval_xpath(model_etree, obj_xpath, change.target_namespaces)
if validate_unique_xml_targets and len(objs) != 1:
raise ValueError('xpath {} must match a single object'.format(obj_xpath))

ns_prefix, _, attr = attr.rpartition(':')
if ns_prefix:
ns = change.target_namespaces.get(ns_prefix, None)
if ns is None:
raise ValueError('No namespace is defined with prefix `{}`'.format(ns_prefix))
attr = '{{{}}}{}'.format(ns, attr)

# change value
for obj in objs:
obj.set(attr, change.new_value)
xpath_captures = regex.split(r"[\[|\]]", change.target)
if len(xpath_captures) != 3 or "@" not in xpath_captures[1] or xpath_captures[2] != "":
# Old method for ModelAttributeChange
# get object to change
obj_xpath, sep, attr = change.target.rpartition('/@')
if sep != '/@':
change.model = model
non_xml_changes.append(change)
continue

objs = eval_xpath(model_etree, obj_xpath, change.target_namespaces)
if validate_unique_xml_targets and len(objs) != 1:
raise ValueError('xpath {} must match a single object'.format(obj_xpath))

ns_prefix, _, attr = attr.rpartition(':')
if ns_prefix:
ns = change.target_namespaces.get(ns_prefix, None)
if ns is None:
raise ValueError(f'No namespace is defined with prefix `{ns_prefix}`')
attr = '{{{}}}{}'.format(ns, attr)

# change value
for obj in objs:
obj.set(attr, change.new_value)
else:
# New Method for ModelAttributeChange
xml_target_captures = regex.split(r"[\@|=]", xpath_captures[1])
xml_target_captures[2] = xml_target_captures[2][1:-1]
_, target_type, target_value = tuple(xml_target_captures)
xml_model_element = eval_xpath(model_etree, change.target, change.target_namespaces)
if validate_unique_xml_targets and len(xml_model_element) != 1:
raise ValueError(f'xpath {change.target} must match a single object')
xpath_tiers = [elem for elem in regex.split("/", xpath_captures[0]) if ":" in elem]
if len(xpath_tiers) == 0:
raise ValueError("Unexpected number of tokens in model element xpath")
element_type = regex.split(":", xpath_tiers[-1])

namespace_prefix, type_suffix = tuple(element_type)
if change.target_namespaces.get(namespace_prefix) is None:
raise ValueError(f'No namespace is defined with prefix `{namespace_prefix}`')
# change value
for attribute in xml_model_element:
if type_suffix == "species" and attribute.get("initialConcentration") is not None:
attribute.set("initialConcentration", change.new_value)
elif type_suffix == "compartment" and attribute.get("size") is not None:
attribute.set("size", change.new_value)
elif type_suffix == "parameter" and attribute.get("value") is not None:
attribute.set("value", change.new_value)
else:
change.model = model
non_xml_changes.append(change)
continue

elif isinstance(change, ComputeModelChange):
# get the values of model variables referenced by compute model changes
if variable_values is None:
model_etrees = {model.id: model_etree}
iter_variable_values = get_values_of_variable_model_xml_targets_of_model_change(change, sed_doc, model_etrees, working_dir)
iter_variable_values = \
get_values_of_variable_model_xml_targets_of_model_change(change, sed_doc, model_etrees, working_dir)
else:
iter_variable_values = variable_values

# calculate new value
new_value = calc_compute_model_change_new_value(change, variable_values=iter_variable_values, range_values=range_values)
new_value = calc_compute_model_change_new_value(change, variable_values=iter_variable_values,
range_values=range_values)
if new_value == int(new_value):
new_value = str(int(new_value))
else:
Expand Down Expand Up @@ -559,10 +601,6 @@ def apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=Non
for obj in objs:
obj.set(attr, new_value)

else:
raise NotImplementedError('Change{} of type {} is not supported.'.format(
' ' + change.name if change.name else '', change.__class__.__name__))

# Interlude: set up the preprocessed task, if there's a set_value_executor
preprocessed_task = None
if preprocessed_task_sub_executer:
Expand All @@ -577,17 +615,24 @@ def apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=Non
# Second pass: changes that need to be interpreter-based:
for change in non_xml_changes:
if isinstance(change, ModelAttributeChange):

if not set_value_executer:
raise NotImplementedError('target ' + change.target + ' cannot be changed by XML manipulation, as the target '
'is not an attribute of a model element')
xpath_captures = regex.split(r"[\[|\]]", change.target)
if len(xpath_captures) != 3 or "@" not in xpath_captures[1] or xpath_captures[2] != "":
raise NotImplementedError(
'target ' + change.target + ' cannot be changed by XML manipulation, as the target '
'is not an attribute of a model element')
else:
raise ValueError(f"SBML attribute to apply `{change.new_value}` to can not be figured out.")
else:
set_value_executer(change.model, change.target, None, change.new_value, preprocessed_task)

elif isinstance(change, ComputeModelChange):
obj_xpath, sep, attr = change.target.rpartition('/@')
if not set_value_executer:
raise NotImplementedError('target ' + change.target + ' cannot be changed by XML manipulation, as the target '
'is not an attribute of a model element')
raise NotImplementedError(
'target ' + change.target + ' cannot be changed by XML manipulation, as the target '
'is not an attribute of a model element')
set_value_executer(change.model, change.target, change.symbol, change.new_value, preprocessed_task)

return preprocessed_task
Expand Down Expand Up @@ -742,7 +787,8 @@ def calc_data_generator_results(data_generator, variable_results):
else:
for aggregate_func in AGGREGATE_MATH_FUNCTIONS:
if re.search(aggregate_func + r' *\(', data_generator.math):
msg = 'Evaluation of aggregate mathematical functions such as `{}` is not supported.'.format(aggregate_func)
msg = 'Evaluation of aggregate mathematical functions such as `{}` is not supported.'.format(
aggregate_func)
raise NotImplementedError(msg)

padded_var_shapes = []
Expand Down Expand Up @@ -830,7 +876,8 @@ def calc_data_generators_results(data_generators, variable_results, output, task

if vars_failed:
status = Status.FAILED
msg = 'Data generator {} cannot be calculated because its variables were not successfully produced.'.format(data_gen.id)
msg = 'Data generator {} cannot be calculated because its variables were not successfully produced.'.format(
data_gen.id)
exceptions.append(ValueError(msg))
result = None

Expand Down Expand Up @@ -1135,7 +1182,8 @@ def resolve_range(range, model_etrees=None):
if var.symbol:
raise NotImplementedError('Symbols are not supported for variables of functional ranges')
if model_etrees[var.model.id] is None:
raise NotImplementedError('Functional ranges that involve variables of non-XML-encoded models are not supported.')
raise NotImplementedError(
'Functional ranges that involve variables of non-XML-encoded models are not supported.')
workspace[var.id] = get_value_of_variable_model_xml_targets(var, model_etrees)

# calculate the values of the range
Expand Down Expand Up @@ -1247,11 +1295,11 @@ def does_model_language_use_xpath_variable_targets(language):
:obj:`bool`: :obj:`True`, if the model language is encoded in XML
"""
return (
re.match(ModelLanguagePattern.CellML, language)
or re.match(ModelLanguagePattern.CopasiML, language)
or re.match(ModelLanguagePattern.MorpheusML, language)
or re.match(ModelLanguagePattern.SBML, language)
or re.match(ModelLanguagePattern.VCML, language)
re.match(ModelLanguagePattern.CellML, language)
or re.match(ModelLanguagePattern.CopasiML, language)
or re.match(ModelLanguagePattern.MorpheusML, language)
or re.match(ModelLanguagePattern.SBML, language)
or re.match(ModelLanguagePattern.VCML, language)
)


Expand Down
6 changes: 3 additions & 3 deletions biosimulators_utils/sedml/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,11 +1179,11 @@ def validate_simulation_type(simulation, types):
errors = []

if not isinstance(simulation, types):
valid_types = "\n - ".join(type.__name__ for type in types)
errors.append([
'Simulation {} of type `{}` is not supported. Simulation must be an instance of one of the following:\n - {}'.format(
simulation.id, simulation.__class__.__name__, '\n - '.join(type.__name__ for type in types))
f'Simulation {simulation.id} of type `{simulation.__class__.__name__}` is not supported. '
f'Simulation must be an instance of one of the following:\n - {valid_types}'
])

return errors


Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/sbml-list-of-species.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
<species compartment="compartment" id="SpeciesToReplace" initialConcentration="0.0035491784" name="Sic1" sboTerm="SBO:0000245">
</species>
</listOfSpecies>
<listOfParameters>
<parameter constant="false" id="parameter_1" name="Fe2GutQUant" value="1E-7" units="second">
</parameter>
</listOfParameters>
</model>
</sbml>
Loading

0 comments on commit e0fe4eb

Please sign in to comment.