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

PR: list recordables #121

Merged
merged 19 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2eb3106
feat(pynml): add function to extract LEMS NeuroML2 definitions
sanjayankur31 Jul 20, 2021
b0c2bf9
ci: enable unit testing with pytest
sanjayankur31 Jul 20, 2021
30e9a0b
ci(flake8): move flake to run at end of CI cycle
sanjayankur31 Jul 20, 2021
ded6f11
chore(logging): set default level to INFO
sanjayankur31 Jul 20, 2021
8026caf
refactor(pynml): simplify function to only return str path of dir
sanjayankur31 Jul 21, 2021
fbe811b
refactor(pynml): improve LEMS definition file extraction function
sanjayankur31 Jul 21, 2021
7e85e40
feat(pynml): add functions to list exposures and list recording paths
sanjayankur31 Jul 21, 2021
0481af1
ci: add requirements file for development
sanjayankur31 Jul 21, 2021
0c40ff8
improvement(pynml): set the default logging level to INFO
sanjayankur31 Jul 21, 2021
a072bc9
improvement(tests): correct removal of temp dir
sanjayankur31 Jul 21, 2021
82931e8
improvement(tests): set default logging level for test
sanjayankur31 Jul 21, 2021
cc468ac
improvement(tests): add more assertions to test
sanjayankur31 Jul 22, 2021
c63c146
chore(tests): consolidate in one folder
sanjayankur31 Aug 4, 2021
4a300f5
chore: remove duplicate requirements file
sanjayankur31 Aug 4, 2021
e929f12
feat: update function name
sanjayankur31 Aug 10, 2021
056481b
test(pynml): update test for renamed function
sanjayankur31 Aug 10, 2021
cb28c66
feat: update functions to match improved pylems functions
sanjayankur31 Aug 16, 2021
5600d55
test: add detailed HH neuron example as test for exposure path listing
sanjayankur31 Aug 16, 2021
343f70e
chore(deps): bump pylems minimum version to 0.5.7
sanjayankur31 Aug 18, 2021
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
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions pyneuroml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
129 changes: 128 additions & 1 deletion pyneuroml/pynml.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from __future__ import print_function
from __future__ import unicode_literals
import os
import shutil
import sys
import subprocess
import math
Expand All @@ -25,6 +26,7 @@
from lxml import etree
import pprint
import logging
import tempfile

try:
import typing
Expand All @@ -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__
Expand All @@ -51,6 +54,7 @@
lems_model_with_units = None

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


def parse_arguments():
Expand Down Expand Up @@ -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
<https://docs.python.org/3/library/tempfile.html>`__ 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=None):
# 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(nml_doc_fn, substring):
# type: (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(substring)


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
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/izhikevich_test_file.nml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<neuroml xmlns="http://www.neuroml.org/schema/neuroml2" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.neuroml.org/schema/neuroml2 https://raw.github.com/NeuroML/NeuroML2/development/Schemas/NeuroML2/NeuroML_v2.2.xsd" id="IzhSingleNeuron">
<izhikevich2007Cell id="izh2007RS0" C="100pF" v0="-60mV" k="0.7nS_per_mV" vr="-60mV" vt="-40mV" vpeak="35mV" a="0.03per_ms" b="-2nS" c="-50.0mV" d="100pA"/>
<pulseGenerator id="pulseGen_0" delay="0ms" duration="1000ms" amplitude="0.07 nA"/>
<network id="IzhNet">
<population id="IzhPop0" component="izh2007RS0" size="1"/>
<explicitInput target="IzhPop0[0]" input="pulseGen_0"/>
</network>
</neuroml>

76 changes: 76 additions & 0 deletions tests/test_pynml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Unit tests for pynml.py

File: test/test_pynml.py

Copyright 2021 NeuroML contributors
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

import unittest
import os
import shutil
import logging

from pyneuroml.pynml import (extract_lems_definition_files, list_exposures,
list_recording_paths)


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_recording_path_listing(self):
"""Test listing of recording paths in NeuroML documents."""
paths = list_recording_paths("tests/izhikevich_test_file.nml", "iz")
sanjayankur31 marked this conversation as resolved.
Show resolved Hide resolved
self.assertTrue("izh2007RS0/u" in paths)
self.assertTrue("izh2007RS0/v" in paths)


if __name__ == "__main__":
unittest.main()