diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d750066e..3cfee223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,13 +43,15 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt + pip install pytest pip install . - - name: Lint with flake8 - run: | - pip install flake8 - flake8 . --count --exit-zero --show-source --max-line-length=127 --statistics - name: Run tests run: | + pytest . pynml -h ./test-ghactions.sh + - name: Lint with flake8 + run: | + pip install flake8 + flake8 . --count --exit-zero --show-source --max-line-length=127 --statistics diff --git a/pyneuroml/__init__.py b/pyneuroml/__init__.py index 316cfd5b..e722bbf3 100644 --- a/pyneuroml/__init__.py +++ b/pyneuroml/__init__.py @@ -7,9 +7,9 @@ # Define a logger for the package logging.basicConfig(format="pyNeuroML >>> %(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.WARN) logger = logging.getLogger(__name__) -logger.setLevel(logging.WARN) +logger.setLevel(logging.INFO) ch = logging.StreamHandler() -ch.setLevel(logging.DEBUG) +ch.setLevel(logging.INFO) formatter = logging.Formatter('pyNeuroML >>> %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) diff --git a/pyneuroml/pynml.py b/pyneuroml/pynml.py index 3dd2750e..18511d55 100644 --- a/pyneuroml/pynml.py +++ b/pyneuroml/pynml.py @@ -13,6 +13,7 @@ from __future__ import print_function from __future__ import unicode_literals import os +import shutil import sys import subprocess import math @@ -25,6 +26,7 @@ from lxml import etree import pprint import logging +import tempfile try: import typing @@ -33,6 +35,7 @@ import matplotlib import lems.model.model as lems_model +from lems.model.fundamental import Include from lems.parser.LEMS import LEMSFileParser from pyneuroml import __version__ @@ -51,6 +54,7 @@ lems_model_with_units = None logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) def parse_arguments(): @@ -317,6 +321,129 @@ def get_lems_model_with_units(): return lems_model_with_units +def extract_lems_definition_files(path=None): + # type: (typing.Union[str, None, tempfile.TemporaryDirectory[typing.Any]]) -> str + """Extract the NeuroML2 LEMS definition files to a directory and return its path. + + This function can be used by other LEMS related functions that need to + include the NeuroML2 LEMS definitions. + + If a path is provided, the folder is created relative to the current + working directory. + + If no path is provided, for repeated usage for example, the files are + extracted to a temporary directory using Python's + `tempfile.mkdtemp + `__ function. + + Note: in both cases, it is the user's responsibility to remove the created + directory when it is no longer required, for example using. the + `shutil.rmtree()` Python function. + + :param path: path of directory relative to current working directory to extract to, or None + :type path: str or None + :returns: directory path + """ + jar_path = get_path_to_jnml_jar() + logger.debug("Loading standard NeuroML2 dimension/unit definitions from %s" % jar_path) + jar = zipfile.ZipFile(jar_path, 'r') + namelist = [x for x in jar.namelist() if ".xml" in x and "NeuroML2CoreTypes" in x] + logger.debug("NeuroML LEMS definition files in jar are: {}".format(namelist)) + + # If a string is provided, ensure that it is relative to cwd + if path and isinstance(path, str) and len(path) > 0: + path = "./" + path + try: + os.makedirs(path) + except FileExistsError: + logger.warn("{} already exists. Any NeuroML LEMS files in it will be overwritten".format(path)) + except OSError as err: + logger.critical(err) + sys.exit(-1) + else: + path = tempfile.mkdtemp() + + logger.debug("Created directory: " + path) + jar.extractall(path, namelist) + path = path + "/NeuroML2CoreTypes/" + logger.info("NeuroML LEMS definition files extracted to: {}".format(path)) + return path + + +def list_exposures(nml_doc_fn, substring=""): + # type: (str, str) -> typing.Union[typing.Dict[lems_model.component.Component, typing.List[lems_model.component.Exposure]], None] + """List exposures in a NeuroML model document file. + + This wraps around `lems.model.list_exposures` to list the exposures in a + NeuroML2 model. The only difference between the two is that the + `lems.model.list_exposures` function is not aware of the NeuroML2 component + types (since it's for any LEMS models in general), but this one is. + + :param nml_doc_fn: NeuroML2 file to list exposures for + :type nml_doc: str + :param substring: substring to match for in component names + :type substring: str + :returns: dictionary of components and their exposures. + + The returned dictionary is of the form: + + .. + { + "component": ["exp1", "exp2"] + } + + """ + return get_standalone_lems_model(nml_doc_fn).list_exposures(substring) + + +def list_recording_paths_for_exposures(nml_doc_fn, substring="", target=""): + # type: (str, str, str) -> typing.List[str] + """List the recording path strings for exposures. + + This wraps around `lems.model.list_recording_paths` to list the recording + paths in the given NeuroML2 model. The only difference between the two is + that the `lems.model.list_recording_paths` function is not aware of the + NeuroML2 component types (since it's for any LEMS models in general), but + this one is. + + :param nml_doc_fn: NeuroML2 file to list recording paths for + :type nml_doc: str + :param substring: substring to match component ids against + :type substring: str + :returns: list of recording paths + + """ + return get_standalone_lems_model(nml_doc_fn).list_recording_paths_for_exposures(substring, target) + + +def get_standalone_lems_model(nml_doc_fn): + # type: (str) -> lems_model.Model + """Get the complete, expanded LEMS model. + + This function takes a NeuroML2 file, includes all the NeuroML2 LEMS + definitions in it and generates the complete, standalone LEMS model. + + :param nml_doc_fn: name of NeuroML file to expand + :type nml_doc_fn: str + :returns: complete LEMS model + """ + new_lems_model = lems_model.Model(include_includes=True, + fail_on_missing_includes=True) + if logger.level < logging.INFO: + new_lems_model.debug = True + else: + new_lems_model.debug = False + neuroml2_defs_dir = extract_lems_definition_files() + filelist = os.listdir(neuroml2_defs_dir) + # Remove the temporary directory + for nml_lems_f in filelist: + new_lems_model.include_file(neuroml2_defs_dir + nml_lems_f, + [neuroml2_defs_dir]) + new_lems_model.include_file(nml_doc_fn, [""]) + shutil.rmtree(neuroml2_defs_dir[:-1 * len("NeuroML2CoreTypes/")]) + return new_lems_model + + def split_nml2_quantity(nml2_quantity): # type: (str) -> typing.Tuple[float, str] """Split a NeuroML 2 quantity into its magnitude and units @@ -340,7 +467,7 @@ def split_nml2_quantity(nml2_quantity): def get_value_in_si(nml2_quantity): - # type: (str) -> float + # type: (str) -> typing.Union[float, None] """Get value of a NeuroML2 quantity in SI units :param nml2_quantity: NeuroML2 quantity to convert diff --git a/setup.py b/setup.py index 71b8ca99..8928f1f4 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ long_description_content_type="text/markdown", install_requires=[ 'argparse', - 'pylems>=0.5.0', + 'pylems>=0.5.7', 'airspeed>=0.5.5', 'libNeuroML>=0.2.52', 'neuromllite>=0.2.2', diff --git a/tests/HH_example_cell.nml b/tests/HH_example_cell.nml new file mode 100644 index 00000000..85c43d4f --- /dev/null +++ b/tests/HH_example_cell.nml @@ -0,0 +1,28 @@ + + HH cell + + + + + A single compartment HH cell + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/HH_example_k_channel.nml b/tests/HH_example_k_channel.nml new file mode 100644 index 00000000..3c342a78 --- /dev/null +++ b/tests/HH_example_k_channel.nml @@ -0,0 +1,11 @@ + + k channel for HH neuron + + Potassium channel for HH cell + + n gate for k channel + + + + + diff --git a/tests/HH_example_leak_channel.nml b/tests/HH_example_leak_channel.nml new file mode 100644 index 00000000..1bd4f7c7 --- /dev/null +++ b/tests/HH_example_leak_channel.nml @@ -0,0 +1,6 @@ + + leak channel for HH neuron + + Leak conductance + + diff --git a/tests/HH_example_na_channel.nml b/tests/HH_example_na_channel.nml new file mode 100644 index 00000000..92124fcc --- /dev/null +++ b/tests/HH_example_na_channel.nml @@ -0,0 +1,16 @@ + + Na channel for HH neuron + + Sodium channel for HH cell + + m gate for na channel + + + + + h gate for na channel + + + + + diff --git a/tests/HH_example_net.nml b/tests/HH_example_net.nml new file mode 100644 index 00000000..836b7012 --- /dev/null +++ b/tests/HH_example_net.nml @@ -0,0 +1,22 @@ + + HH cell network + + + Simple pulse generator + + + + A population for our cell + + + A populationList for our cell + + + + + + + + + + diff --git a/tests/izhikevich_test_file.nml b/tests/izhikevich_test_file.nml new file mode 100644 index 00000000..f9df37b2 --- /dev/null +++ b/tests/izhikevich_test_file.nml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/test_pynml.py b/tests/test_pynml.py new file mode 100644 index 00000000..51fb98db --- /dev/null +++ b/tests/test_pynml.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +Unit tests for pynml.py + +File: test/test_pynml.py + +Copyright 2021 NeuroML contributors +Author: Ankur Sinha +""" + +import unittest +import os +import shutil +import logging + +from pyneuroml.pynml import (extract_lems_definition_files, list_exposures, + list_recording_paths_for_exposures) + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +class TestJarUtils(unittest.TestCase): + + """Test jNeuroML jar related functions""" + + def test_lems_def_files_extraction(self): + """Test extraction of NeuroML2 LEMS files from jar.""" + filelist = ["Cells.xml", + "Channels.xml", + "Inputs.xml", + "Networks.xml", + "NeuroML2CoreTypes.xml", + "NeuroMLCoreCompTypes.xml", + "NeuroMLCoreDimensions.xml", + "PyNN.xml", + "Simulation.xml", + "Synapses.xml"] + + extraction_dir = extract_lems_definition_files() + newfilelist = os.listdir(extraction_dir) + shutil.rmtree(extraction_dir[:-1 * len("NeuroML2CoreTypes/")]) + assert(sorted(filelist) == sorted(newfilelist)) + + +class TestHelperUtils(unittest.TestCase): + + """Test helper utilities.""" + + def test_exposure_listing(self): + """Test listing of exposures in NeuroML documents.""" + exps = list_exposures("tests/izhikevich_test_file.nml", "iz") + ctypes = {} + for key, val in exps.items(): + ctypes[key.type] = val + + self.assertTrue("izhikevich2007Cell" in ctypes.keys()) + expnames = [] + for exp in ctypes["izhikevich2007Cell"]: + expnames.append(exp.name) + # https://docs.neuroml.org/Userdocs/Schemas/Cells.html#schema-izhikevich2007cell + self.assertTrue("u" in expnames) + self.assertTrue("v" in expnames) + self.assertTrue("iMemb" in expnames) + self.assertTrue("iSyn" in expnames) + + def test_exposure_listing_2(self): + """Test listing of exposures in NeuroML documents.""" + os.chdir("tests/") + exps = list_exposures("HH_example_net.nml") + print(exps) + os.chdir("../") + + def test_recording_path_listing(self): + """Test listing of recording paths in NeuroML documents.""" + paths = list_recording_paths_for_exposures("tests/izhikevich_test_file.nml", + "", "IzhNet") + print("\n".join(paths)) + # self.assertTrue("izh2007RS0/u" in paths) + # self.assertTrue("izh2007RS0/v" in paths) + + def test_recording_path_listing_2(self): + """Test listing of recording paths in NeuroML documents.""" + os.chdir("tests/") + paths = list_recording_paths_for_exposures("HH_example_net.nml", + "hh_cell", "single_hh_cell_network") + print("\n".join(paths)) + os.chdir("../") + + +if __name__ == "__main__": + unittest.main()