diff --git a/pyneuroml/pynml.py b/pyneuroml/pynml.py index df1f45bb..08e60c61 100644 --- a/pyneuroml/pynml.py +++ b/pyneuroml/pynml.py @@ -345,6 +345,11 @@ def parse_arguments(): action="store_true", help=("(Via jNeuroML) Validate NeuroML file(s) against the\n" "v1.8.1 Schema"), ) + mut_exc_opts.add_argument( + "-validate-sbml", + action="store_true", + help=("Validate SBML file(s)"), + ) return parser.parse_args() @@ -2111,6 +2116,14 @@ def evaluate_arguments(args): post_args = "" exit_on_fail = True + # Deal with the SBML validation option which doesn't call run_jneuroml + if args.validate_sbml: + from pyneuroml.sbml import validate_sbml_files + + validate_sbml_files(" ".join(args.input_files)) + + return True + # These do not use the shared option where files are supplied # They require the file name to be specified after # TODO: handle these better diff --git a/pyneuroml/sbml/__init__.py b/pyneuroml/sbml/__init__.py new file mode 100644 index 00000000..615bbfa0 --- /dev/null +++ b/pyneuroml/sbml/__init__.py @@ -0,0 +1,64 @@ +""" +use libsbml.SBMLDocument.checkConsistency to check validaity of an SBML document +based on https://github.com/combine-org/combine-notebooks/blob/main/src/combine_notebooks/validation/validation_sbml.py +""" + +import os +import libsbml +from libsbml import SBMLReader +from typing import List + + +def validate_sbml_files(input_files: str, units_consistency: bool = False): + """ + validate each input file using libsbml.SBMLDocument.checkConsistency + input_files is a space separated list of one or more filepaths + """ + + for file_name in input_files.split(): + if not os.path.isfile(file_name): + raise OSError(("Could not find SBML file %s" % file_name)) + + try: + reader = SBMLReader() + doc = reader.readSBML(file_name) + except Exception: + raise OSError(("SBMLReader failed to load the file %s" % file_name)) + + # set the unit checking, similar for the other settings + doc.setConsistencyChecks( + libsbml.LIBSBML_CAT_UNITS_CONSISTENCY, units_consistency + ) + doc.checkConsistency() + # get errors/warnings + n_errors: int = doc.getNumErrors() + errors: List[libsbml.SBMLError] = list() + warnings: List[libsbml.SBMLError] = list() + for k in range(n_errors): + error: libsbml.SBMLError = doc.getError(k) + severity = error.getSeverity() + if (severity == libsbml.LIBSBML_SEV_ERROR) or ( + severity == libsbml.LIBSBML_SEV_FATAL + ): + errors.append(error) + else: + warnings.append(error) + # print results + print("-" * 80) + print(f"{'validation error(s)':<25}: {len(errors)}") + print(f"{'validation warning(s)':<25}: {len(warnings)}") + if len(errors) > 0: + print("--- errors ---") + for kerr in enumerate(errors): + print(f"E{kerr}: {error.getCategoryAsString()} L{error.getLine()}") + print( + f"[{error.getSeverityAsString()}] {error.getShortMessage()} | {error.getMessage()}" + ) + if len(warnings) > 0: + print("--- warnings ---") + for kwarn in enumerate(warnings): + print(f"E{kwarn}: {error.getCategoryAsString()} L{error.getLine()}") + print( + f"[{error.getSeverityAsString()}] {error.getShortMessage()} | {error.getMessage()}" + ) + print("-" * 80) diff --git a/setup.cfg b/setup.cfg index 6c9339ef..4e672195 100644 --- a/setup.cfg +++ b/setup.cfg @@ -101,6 +101,9 @@ plotly = nsg = pynsgr +sbml = + python-libsbml + all = pyNeuroML[neuron] pyNeuroML[brian] @@ -113,6 +116,7 @@ all = pyNeuroML[vispy] pyNeuroML[plotly] pyNeuroML[nsg] + pyNeuroML[sbml] dev = pyNeuroML[all] diff --git a/tests/sbml/test_data/test_doc.sbml b/tests/sbml/test_data/test_doc.sbml new file mode 100644 index 00000000..cd05db81 --- /dev/null +++ b/tests/sbml/test_data/test_doc.sbml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + k + S1 + c1 + + + + + + + \ No newline at end of file diff --git a/tests/sbml/test_sbml.py b/tests/sbml/test_sbml.py new file mode 100755 index 00000000..297e9c53 --- /dev/null +++ b/tests/sbml/test_sbml.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from pyneuroml import sbml + +fname = "tests/sbml/test_data/test_doc.sbml" +doc = sbml.validate_sbml_files(fname)