Skip to content

Commit

Permalink
Merge pull request #266 from robertvi/development
Browse files Browse the repository at this point in the history
added -validate-sbml option
  • Loading branch information
pgleeson authored Nov 15, 2023
2 parents f2649ca + c358b2f commit 6c023c3
Show file tree
Hide file tree
Showing 12 changed files with 514 additions and 2 deletions.
42 changes: 42 additions & 0 deletions examples/test_data/valid_doc.sbml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" level="3" version="1">
<model substanceUnits="mole" timeUnits="second" extentUnits="mole">
<listOfUnitDefinitions>
<unitDefinition id="per_second">
<listOfUnits>
<unit kind="second" exponent="-1" scale="0" multiplier="1"/>
</listOfUnits>
</unitDefinition>
</listOfUnitDefinitions>
<listOfCompartments>
<compartment id="c1" spatialDimensions="3" size="1" units="litre" constant="true"/>
</listOfCompartments>
<listOfSpecies>
<species id="S1" compartment="c1" initialAmount="5" substanceUnits="mole" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
<species id="S2" compartment="c1" initialAmount="0" substanceUnits="mole" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
</listOfSpecies>
<listOfParameters>
<parameter id="k" value="1" units="per_second" constant="true"/>
</listOfParameters>
<listOfReactions>
<reaction id="r1" reversible="false" fast="false">
<listOfReactants>
<speciesReference species="S1" constant="true"/>
</listOfReactants>
<listOfProducts>
<speciesReference species="S2" constant="true"/>
</listOfProducts>
<kineticLaw>
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<times/>
<ci> k </ci>
<ci> S1 </ci>
<ci> c1 </ci>
</apply>
</math>
</kineticLaw>
</reaction>
</listOfReactions>
</model>
</sbml>
47 changes: 45 additions & 2 deletions pyneuroml/pynml.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ def parse_arguments():
"input_files",
type=str,
nargs="*",
metavar="<LEMS/NeuroML 2 file(s)>",
help="LEMS/NeuroML 2 file(s) to process",
metavar="<LEMS/NeuroML 2/SBML file(s)>",
help="LEMS/NeuroML 2/SBML file(s) to process",
)

mut_exc_opts_grp = parser.add_argument_group(
Expand Down Expand Up @@ -363,6 +363,16 @@ 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), unit consistency failure generates a warning"),
)
mut_exc_opts.add_argument(
"-validate-sbml-units",
action="store_true",
help=("Validate SBML file(s), unit consistency failure generates an error"),
)

return parser.parse_args()

Expand Down Expand Up @@ -2139,6 +2149,39 @@ 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 or args.validate_sbml_units:
try:
from pyneuroml.sbml import validate_sbml_files
except Exception:
logger.critical("Unable to import pyneuroml.sbml")
sys.exit(UNKNOWN_ERR)

if not len(args.input_files) >= 1:
logger.critical("No input files specified")
sys.exit(ARGUMENT_ERR)

if args.validate_sbml_units:
# A failed unit consistency check generates an error
strict_units = True
else:
# A failed unit consistency check generates only a warning
strict_units = False

try:
result = validate_sbml_files(args.input_files, strict_units)
except Exception as e:
logger.critical(f"validate_sbml_files failed with {str(e)}")
sys.exit(UNKNOWN_ERR)

if result:
# All files validated ok (with possible warnings but no errors)
sys.exit(0)

# Errors of some kind were found in one or more files
logger.error(f"one or more SBML files failed to validate")
sys.exit(UNKNOWN_ERR)

# These do not use the shared option where files are supplied
# They require the file name to be specified after
# TODO: handle these better
Expand Down
94 changes: 94 additions & 0 deletions pyneuroml/sbml/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
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 errno
import libsbml
from libsbml import SBMLReader
from typing import List


def validate_sbml_files(input_files: List[str], strict_units: bool = False) -> bool:
"""
validate each input file using libsbml.SBMLDocument.checkConsistency
input_files is a list of one or more filepaths
strict_units converts unit consistency warnings into errors
"""

if not len(input_files) >= 1:
raise ValueError("No input files specified")

all_valid = True

for file_name in input_files:
# These checks are already implemented by SBMLReader
# But could just be logged along with the other error types rather than causing exceptions
if not os.path.isfile(file_name):
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), file_name)

if not os.access(file_name, os.R_OK):
raise IOError(f"Could not read SBML file {file_name}")

try:
reader = SBMLReader()
doc = reader.readSBML(file_name)
except Exception:
# usually errors are logged within the doc object rather than being thrown
raise IOError(f"SBMLReader failed trying to open the file {file_name}")

# Always check for unit consistency
doc.setConsistencyChecks(libsbml.LIBSBML_CAT_UNITS_CONSISTENCY, True)
doc.checkConsistency()

# Get errors/warnings arising from the file reading or consistency checking calls above
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)
elif (
(strict_units is True)
# For error code definitions see
# https://github.com/sbmlteam/libsbml/blob/fee56c943ea39b9ac1f8491cac2fc9b3665e368f/src/sbml/SBMLError.h#L528
# and sbml.level-3.version-2.core.release-2.pdf page 159
and (error.getErrorId() >= 10500)
and (error.getErrorId() <= 10599)
):
# Treat unit consistency warnings as errors
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:
all_valid = False
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)

return all_valid
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ plotly =
nsg =
pynsgr

sbml =
python-libsbml

all =
pyNeuroML[neuron]
pyNeuroML[brian]
Expand All @@ -113,6 +116,7 @@ all =
pyNeuroML[vispy]
pyNeuroML[plotly]
pyNeuroML[nsg]
pyNeuroML[sbml]

dev =
pyNeuroML[all]
Expand Down
7 changes: 7 additions & 0 deletions test-ghactions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ pynml LEMS_NML2_Ex9_FN.xml -spineml
pynml LEMS_NML2_Ex9_FN.xml -sbml


echo
echo "################################################"
echo "## Simple SBML validation example"

pynml -validate-sbml test_data/valid_doc.sbml
pynml -validate-sbml-units test_data/valid_doc.sbml


echo
echo "################################################"
Expand Down
42 changes: 42 additions & 0 deletions tests/sbml/test_data/inconsistent_units_doc.sbml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" level="3" version="1">
<model substanceUnits="mole" timeUnits="second" extentUnits="mole">
<listOfUnitDefinitions>
<unitDefinition id="per_second">
<listOfUnits>
<unit kind="second" exponent="-1" scale="0" multiplier="1"/>
</listOfUnits>
</unitDefinition>
</listOfUnitDefinitions>
<listOfCompartments>
<compartment id="c1" spatialDimensions="3" size="1" units="litre" constant="true"/>
</listOfCompartments>
<listOfSpecies>
<species id="S1" compartment="c1" initialAmount="5" substanceUnits="mole" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
<species id="S2" compartment="c1" initialAmount="0" substanceUnits="gram" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
</listOfSpecies>
<listOfParameters>
<parameter id="k" value="1" units="per_second" constant="true"/>
</listOfParameters>
<listOfReactions>
<reaction id="r1" reversible="false" fast="false">
<listOfReactants>
<speciesReference species="S1" constant="true"/>
</listOfReactants>
<listOfProducts>
<speciesReference species="S2" constant="true"/>
</listOfProducts>
<kineticLaw>
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<times/>
<ci> k </ci>
<ci> S1 </ci>
<ci> S2 </ci>
</apply>
</math>
</kineticLaw>
</reaction>
</listOfReactions>
</model>
</sbml>
2 changes: 2 additions & 0 deletions tests/sbml/test_data/invalid_doc00.sbml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
10 PRINT "HELLO"
20 GOTO 10
42 changes: 42 additions & 0 deletions tests/sbml/test_data/invalid_doc01.sbml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" level="3" version="1">
<model substanceUnits="molerat" timeUnits="second" extentUnits="mole">
<listOfUnitDefinitions>
<unitDefinition id="per_second">
<listOfUnits>
<unit kind="second" exponent="-1" scale="0" multiplier="1"/>
</listOfUnits>
</unitDefinition>
</listOfUnitDefinitions>
<listOfCompartments>
<compartment id="c1" spatialDimensions="3" size="1" units="litre" constant="true"/>
</listOfCompartments>
<listOfSpecies>
<species id="S1" compartment="c1" initialAmount="5" substanceUnits="mole" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
<species id="S2" compartment="c1" initialAmount="0" substanceUnits="mole" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
</listOfSpecies>
<listOfParameters>
<parameter id="k" value="1" units="per_second" constant="true"/>
</listOfParameters>
<listOfReactions>
<reaction id="r1" reversible="false" fast="false">
<listOfReactants>
<speciesReference species="S1" constant="true"/>
</listOfReactants>
<listOfProducts>
<speciesReference species="S2" constant="true"/>
</listOfProducts>
<kineticLaw>
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<times/>
<ci> k </ci>
<ci> S1 </ci>
<ci> c1 </ci>
</apply>
</math>
</kineticLaw>
</reaction>
</listOfReactions>
</model>
</sbml>
42 changes: 42 additions & 0 deletions tests/sbml/test_data/invalid_doc02.sbml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" level="3" version="1">
<model substanceUnits="mole" timeUnits="second" extentUnits="mole">
<listOfUnitDefinitions>
<unitDefinition id="per_second">
<listOfUnits>
<unit kind="second" exponent="-1" scale="0" multiplier="1"/>
</listOfUnits>
</unitDefinition>
</listOfUnitDefinitions>
<listOfCompartments>
<compartment id="c1" spatialDimensions="3" size="1" units="litre" constant="true"/>
</listOfCompartments>
<listOfSpecies>
<species id="S1" compartment="c1" initialAmount="5" substanceUnits="mole" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
<species id="S2" compartment="c1" initialAmount="0" substanceUnits="mole" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
</listOfSpecies>
<listOfParameters>
<parameter id="k" value="1" units="per_second" constant="true"/>
</listOfParameters>
<listOfReactions>
<reaction id="r1" reversible="false" fast="false">
<listOfReactants>
<speciesReference species="S1" constant="true"/>
</listOfReactants>
<listOfProducts>
<speciesReference species="S2" constant="true"/>
</listOfProducts>
<kineticLaw>
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<times/>
<ci> k </ci>
<ci> S1 </ci>
<ci> c1 </ci>
</math>
</math>
</kineticLaw>
</reaction>
</listOfReactions>
</model>
</sbml>
Loading

0 comments on commit 6c023c3

Please sign in to comment.