Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sys bio model fixes #154

Merged
merged 7 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
# 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 @@

# 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 @@
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 @@
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")

Check warning on line 545 in biosimulators_utils/sedml/utils.py

View check run for this annotation

Codecov / codecov/patch

biosimulators_utils/sedml/utils.py#L545

Added line #L545 was not covered by tests
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}`')

Check warning on line 550 in biosimulators_utils/sedml/utils.py

View check run for this annotation

Codecov / codecov/patch

biosimulators_utils/sedml/utils.py#L550

Added line #L550 was not covered by tests
# 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

Check warning on line 562 in biosimulators_utils/sedml/utils.py

View check run for this annotation

Codecov / codecov/patch

biosimulators_utils/sedml/utils.py#L560-L562

Added lines #L560 - L562 were not covered by tests

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 @@
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 @@
# 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.")

Check warning on line 626 in biosimulators_utils/sedml/utils.py

View check run for this annotation

Codecov / codecov/patch

biosimulators_utils/sedml/utils.py#L626

Added line #L626 was not covered by tests
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 @@
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 @@

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 @@
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 @@
: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
Loading