diff --git a/README.md b/README.md index 40033d01..f6d281f2 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ pip install git+https://github.com/biosimulators/Biosimulators_utils.git#biosimu ### Installation optional features +To use BioSimulators utils to validate CellML models, install BioSimulators utils with the `cellml` option: +``` +pip install biosimulators-utils[cellml] +``` + To use BioSimulators utils to validate NeuroML models, install BioSimulators utils with the `neuroml` option: ``` pip install biosimulators-utils[neuroml] diff --git a/biosimulators_utils/cellml/__init__.py b/biosimulators_utils/cellml/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/biosimulators_utils/cellml/validation.py b/biosimulators_utils/cellml/validation.py new file mode 100644 index 00000000..06120951 --- /dev/null +++ b/biosimulators_utils/cellml/validation.py @@ -0,0 +1,61 @@ +""" Utilities for validating CellML models + +:Author: Jonathan Karr +:Date: 2021-05-10 +:Copyright: 2021, Center for Reproducible Biomedical Modeling +:License: MIT +""" + +import libcellml +import os + + +def validate_model(filename, name=None): + """ Check that a model is valid + + Args: + filename (:obj:`str`): path to model + name (:obj:`str`, optional): name of model for use in error messages + + Returns: + :obj:`tuple`: + + * nested :obj:`list` of :obj:`str`: nested list of errors (e.g., required ids missing or ids not unique) + * nested :obj:`list` of :obj:`str`: nested list of errors (e.g., required ids missing or ids not unique) + """ + errors = [] + warnings = [] + + if not os.path.isfile(filename): + errors.append(['`{}` is not a file.'.format(filename)]) + return (errors, warnings) + + # read model + parser = libcellml.Parser() + with open(filename, 'r') as file: + model = parser.parseModel(file.read()) + + for i_error in range(parser.errorCount()): + error = parser.error(i_error) + errors.append([error.description()]) + + for i_warning in range(parser.warningCount()): + warning = parser.warning(i_warning) + warnings.append([warning.description()]) + + if errors: + return (errors, warnings) + + # validate model + validator = libcellml.Validator() + validator.validateModel(model) + + for i_error in range(validator.errorCount()): + error = validator.error(i_error) + errors.append([error.description()]) + + for i_warning in range(validator.warningCount()): + warning = validator.warning(i_warning) + warnings.append([warning.description()]) + + return (errors, warnings) diff --git a/biosimulators_utils/sedml/validation.py b/biosimulators_utils/sedml/validation.py index 08a9224d..926be02d 100644 --- a/biosimulators_utils/sedml/validation.py +++ b/biosimulators_utils/sedml/validation.py @@ -723,7 +723,10 @@ def validate_model_with_language(source, language, name=None): errors = [] warnings = [] - if language and re.match(ModelLanguagePattern.NeuroML, language): + if language and re.match(ModelLanguagePattern.CellML, language): + from ..cellml.validation import validate_model + + elif language and re.match(ModelLanguagePattern.NeuroML, language): from ..neuroml.validation import validate_model elif language and re.match(ModelLanguagePattern.SBML, language): diff --git a/docs-src/installation.rst b/docs-src/installation.rst index f2733794..e279dc03 100644 --- a/docs-src/installation.rst +++ b/docs-src/installation.rst @@ -37,6 +37,12 @@ After installing `Python `_ (>= 3.7) and `pip Installing the optional features -------------------------------- +To use BioSimulators utils to validate models encoded in CellML, install BioSimulators utils with the ``cellml`` option: + +.. code-block:: text + + pip install biosimulators-utils[cellml] + To use BioSimulators utils to validate models encoded in NeuroML, install BioSimulators utils with the ``neuroml`` option: .. code-block:: text diff --git a/requirements.optional.txt b/requirements.optional.txt index c6c43200..b1746f1f 100644 --- a/requirements.optional.txt +++ b/requirements.optional.txt @@ -1,5 +1,5 @@ -# [cellml] -# libcellml +[cellml] +libcellml [neuroml] libneuroml diff --git a/tests/cellml/test_cellml_validation.py b/tests/cellml/test_cellml_validation.py new file mode 100644 index 00000000..38bee7d0 --- /dev/null +++ b/tests/cellml/test_cellml_validation.py @@ -0,0 +1,25 @@ +from biosimulators_utils.cellml.validation import validate_model +from biosimulators_utils.utils.core import flatten_nested_list_of_strings +import os +import unittest + + +class CellMlValidationTestCase(unittest.TestCase): + FIXTURE_DIR = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'cellml') + + def test(self): + errors, warnings = validate_model(os.path.join(self.FIXTURE_DIR, 'level2.xml')) + self.assertEqual(errors, []) + self.assertEqual(warnings, []) + + errors, warnings = validate_model(os.path.join(self.FIXTURE_DIR, 'not_a_path.xml')) + self.assertIn("is not a file", flatten_nested_list_of_strings(errors)) + self.assertEqual(warnings, []) + + errors, warnings = validate_model(os.path.join(self.FIXTURE_DIR, 'invalid_cellml_2.0.xml')) + self.assertIn("Start tag expected", flatten_nested_list_of_strings(errors)) + self.assertEqual(warnings, []) + + errors, warnings = validate_model(os.path.join(self.FIXTURE_DIR, 'missing-attribute.xml')) + self.assertIn("does not have a valid name attribute", flatten_nested_list_of_strings(errors)) + self.assertEqual(warnings, []) diff --git a/tests/fixtures/cellml/invalid_cellml_2.0.xml b/tests/fixtures/cellml/invalid_cellml_2.0.xml new file mode 100644 index 00000000..ce7e18fd --- /dev/null +++ b/tests/fixtures/cellml/invalid_cellml_2.0.xml @@ -0,0 +1 @@ +This is not a CellML file. \ No newline at end of file diff --git a/tests/fixtures/cellml/level2.xml b/tests/fixtures/cellml/level2.xml new file mode 100644 index 00000000..a897b2c2 --- /dev/null +++ b/tests/fixtures/cellml/level2.xml @@ -0,0 +1,26 @@ + + + + + + + + + + time + cosine + + + + parameter + time + + + + + + + + + + diff --git a/tests/fixtures/cellml/missing-attribute.xml b/tests/fixtures/cellml/missing-attribute.xml new file mode 100644 index 00000000..bb08a022 --- /dev/null +++ b/tests/fixtures/cellml/missing-attribute.xml @@ -0,0 +1,26 @@ + + + + + + + + + + time + cosine + + + + parameter + time + + + + + + + + + + diff --git a/tests/fixtures/IM.channel.nml b/tests/fixtures/neuroml/IM.channel.nml similarity index 100% rename from tests/fixtures/IM.channel.nml rename to tests/fixtures/neuroml/IM.channel.nml diff --git a/tests/fixtures/invalid-model.nml b/tests/fixtures/neuroml/invalid-model.nml similarity index 100% rename from tests/fixtures/invalid-model.nml rename to tests/fixtures/neuroml/invalid-model.nml diff --git a/tests/neuroml/test_neuroml_validation.py b/tests/neuroml/test_neuroml_validation.py index 009a9522..765ea838 100644 --- a/tests/neuroml/test_neuroml_validation.py +++ b/tests/neuroml/test_neuroml_validation.py @@ -5,7 +5,7 @@ class NeuroMlValidationTestCase(unittest.TestCase): - FIXTURE_DIR = os.path.join(os.path.dirname(__file__), '..', 'fixtures') + FIXTURE_DIR = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'neuroml') def test(self): errors, warnings = validate_model(os.path.join(self.FIXTURE_DIR, 'IM.channel.nml')) diff --git a/tests/sedml/test_sedml_exec.py b/tests/sedml/test_sedml_exec.py index cf121595..7401cdf2 100644 --- a/tests/sedml/test_sedml_exec.py +++ b/tests/sedml/test_sedml_exec.py @@ -46,7 +46,7 @@ def test_successful(self): doc.models.append(data_model.Model( id='model2', source='https://models.edu/model1.xml', - language=data_model.ModelLanguage.CellML.value, + language=data_model.ModelLanguage.VCML.value, )) doc.simulations.append(data_model.SteadyStateSimulation( diff --git a/tests/sedml/test_sedml_validation.py b/tests/sedml/test_sedml_validation.py index f50ff362..bf1322f6 100644 --- a/tests/sedml/test_sedml_validation.py +++ b/tests/sedml/test_sedml_validation.py @@ -719,12 +719,24 @@ def test_validate_model_with_language(self): self.assertEqual(errors, []) self.assertIn('No validation is available for', flatten_nested_list_of_strings(warnings)) - filename = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'IM.channel.nml') + # CellML + filename = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'cellml', 'level2.xml') + errors, warnings = validation.validate_model_with_language(filename, data_model.ModelLanguage.CellML) + self.assertEqual(errors, []) + self.assertEqual(warnings, []) + + filename = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'cellml', 'missing-attribute.xml') + errors, warnings = validation.validate_model_with_language(filename, data_model.ModelLanguage.CellML) + self.assertNotEqual(errors, []) + self.assertEqual(warnings, []) + + # NeuroML + filename = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'neuroml', 'IM.channel.nml') errors, warnings = validation.validate_model_with_language(filename, data_model.ModelLanguage.NeuroML) self.assertEqual(errors, []) self.assertEqual(warnings, []) - filename = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'invalid-model.nml') + filename = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'neuroml', 'invalid-model.nml') errors, warnings = validation.validate_model_with_language(filename, data_model.ModelLanguage.NeuroML) self.assertIn("Not a valid NeuroML 2 doc", flatten_nested_list_of_strings(errors)) self.assertEqual(warnings, [])