Skip to content

Commit c5f36ba

Browse files
authored
Merge pull request #818 from apdavison/arbor-ci
Start testing pyNN.arbor
2 parents 22682d3 + c21ebb0 commit c5f36ba

File tree

9 files changed

+140
-10
lines changed

9 files changed

+140
-10
lines changed

.github/workflows/full-test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ jobs:
5050
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.8
5151
make
5252
make install
53+
- name: Install Arbor
54+
if: startsWith(matrix.os, 'ubuntu')
55+
run: |
56+
python -m pip install arbor==0.9.0 libNeuroML
5357
- name: Install PyNN itself
5458
run: |
55-
pip install -e ".[test]"
59+
python -m pip install -e ".[test]"
5660
- name: Test installation has worked (Ubuntu)
5761
# this is needed because the PyNN tests are just skipped if the simulator
5862
# fails to install, so we need to catch import failures separately
@@ -61,6 +65,7 @@ jobs:
6165
python -c "import pyNN.nest"
6266
python -c "import pyNN.neuron"
6367
python -c "import pyNN.brian2"
68+
python -c "import pyNN.arbor"
6469
- name: Test installation has worked (Windows)
6570
if: startsWith(matrix.os, 'windows')
6671
run: |

pyNN/arbor/cells.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ def _build_decor(self, i):
125125
location_generator = current_source["location_generator"]
126126
mechanism = getattr(arbor, current_source["model_name"])
127127
for locset, label in location_generator.generate_locations(morph, label=f"{current_source['model_name']}_label"):
128-
decor.place(locset, mechanism(**current_source["parameters"].evaluate()), label)
128+
params = current_source["parameters"].evaluate(simplify=True)
129+
mech = mechanism(**params)
130+
decor.place(locset, mech, label)
129131

130132
# add spike source
131133
decor.place('"root"', arbor.threshold_detector(-10), "detector")

pyNN/arbor/populations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ def _create_cells(self):
117117
[simulator.Cell(id) for id in id_range],
118118
dtype=simulator.Cell
119119
)
120+
for obj in self.all_cells:
121+
obj.parent = self
120122

121123
# for i, cell in enumerate(self.all_cells):
122124
# #for key, value in parameter_space.items():

pyNN/arbor/simulator.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,17 @@ def build_mechanisms():
3535
return mech_path
3636

3737

38-
class Cell(int):
38+
class Cell(int, common.IDMixin):
39+
local = True
3940

4041
def __init__(self, n):
4142
"""Create an ID object with numerical value `n`."""
42-
self.gid = n
43-
self.local = True
43+
int.__init__(n)
44+
common.IDMixin.__init__(self)
45+
46+
@property
47+
def gid(self):
48+
return int(self)
4449

4550

4651
class NetworkRecipe(arbor.recipe):

pyNN/arbor/standardmodels.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
from copy import deepcopy
1010

11+
import numpy as np
1112
import arbor
1213

1314
from ..standardmodels import cells, ion_channels, synapses, electrodes, receptors, build_translations
@@ -60,9 +61,15 @@ def inject_into(self, cells, location=None): # rename to `locations` ?
6061
if hasattr(cells, "parent"):
6162
cell_descr = cells.parent._arbor_cell_description.base_value
6263
index = cells.parent.id_to_index(cells.all_cells.astype(int))
63-
else:
64+
elif hasattr(cells, "_arbor_cell_description"):
6465
cell_descr = cells._arbor_cell_description.base_value
6566
index = cells.id_to_index(cells.all_cells.astype(int))
67+
else:
68+
assert isinstance(cells, (list, tuple))
69+
# we're assuming all cells have the same parent here
70+
cell_descr = cells[0].parent._arbor_cell_description.base_value
71+
index = np.array(cells, dtype=int)
72+
6673
self.parameter_space.shape = (1,)
6774
if location is None:
6875
raise NotImplementedError

pyNN/common/populations.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class IDMixin(object):
4646
where p is a Population object.
4747
"""
4848
# Simulator ID classes should inherit both from the base type of the ID
49-
# (e.g., int or long) and from IDMixin.
49+
# (e.g., int) and from IDMixin.
5050

5151
def __getattr__(self, name):
5252
if name == "parent":
@@ -128,9 +128,12 @@ def _get_position(self):
128128
def local(self):
129129
return self.parent.is_local(self)
130130

131-
def inject(self, current_source):
131+
def inject(self, current_source, location=None):
132132
"""Inject current from a current source object into the cell."""
133-
current_source.inject_into([self])
133+
if location is None:
134+
current_source.inject_into([self])
135+
else:
136+
current_source.inject_into([self], location=location)
134137

135138
def get_initial_value(self, variable):
136139
"""Get the initial value of a state variable of the cell."""

pyNN/neuron/standardmodels/electrodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def inject_into(self, cells, location=None):
147147
pass
148148
else:
149149
raise TypeError("location must be a string or a LocationGenerator")
150-
morphology = cells.celltype.parameter_space["morphology"].base_value # todo: evaluate lazyarray
150+
morphology = id.celltype.parameter_space["morphology"].base_value # todo: evaluate lazyarray
151151
locations = location.generate_locations(morphology, label_prefix="dc_current_source", cell=id._cell)
152152
sections = []
153153
for loc in locations:

test/system/scenarios/fixtures.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
except ImportError:
2222
pass
2323

24+
try:
25+
import pyNN.arbor
26+
available_modules["arbor"] = pyNN.arbor
27+
except ImportError:
28+
pass
29+
2430

2531
class SimulatorNotAvailable:
2632

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import sys
2+
import numpy as np
3+
4+
try:
5+
from neuroml import Morphology, Segment, Point3DWithDiam as P
6+
have_neuroml = True
7+
except ImportError:
8+
have_neuroml = False
9+
10+
from pyNN.utility import init_logging
11+
from pyNN.morphology import NeuroMLMorphology
12+
from pyNN.parameters import IonicSpecies
13+
14+
import pytest
15+
16+
from .fixtures import run_with_simulators
17+
18+
19+
@run_with_simulators("arbor", "neuron")
20+
def test_scenario5(sim):
21+
"""
22+
Array of multi-compartment neurons, each injected with a different current.
23+
"""
24+
if not have_neuroml:
25+
pytest.skip("libNeuroML not available")
26+
27+
init_logging(logfile=None, debug=True)
28+
29+
sim.setup(timestep=0.01)
30+
31+
soma = Segment(proximal=P(x=18.8, y=0, z=0, diameter=18.8),
32+
distal=P(x=0, y=0, z=0, diameter=18.8),
33+
name="soma", id=0)
34+
dend = Segment(proximal=P(x=0, y=0, z=0, diameter=2),
35+
distal=P(x=-500, y=0, z=0, diameter=2),
36+
name="dendrite",
37+
parent=soma, id=1)
38+
39+
cell_class = sim.MultiCompartmentNeuron
40+
cell_class.label = "ExampleMultiCompartmentNeuron"
41+
cell_class.ion_channels = {'pas': sim.PassiveLeak, 'na': sim.NaChannel, 'kdr': sim.KdrChannel}
42+
43+
cell_type = cell_class(
44+
morphology=NeuroMLMorphology(Morphology(segments=(soma, dend))),
45+
cm=1.0, # mF / cm**2
46+
Ra=500.0, # ohm.cm
47+
ionic_species={
48+
"na": IonicSpecies("na", reversal_potential=50.0),
49+
"k": IonicSpecies("k", reversal_potential=-77.0)
50+
},
51+
pas={"conductance_density": sim.morphology.uniform('all', 0.0003),
52+
"e_rev":-54.3},
53+
na={"conductance_density": sim.morphology.uniform('soma', 0.120)},
54+
kdr={"conductance_density": sim.morphology.uniform('soma', 0.036)}
55+
)
56+
57+
neurons = sim.Population(5, cell_type, initial_values={'v': -60.0})
58+
59+
I = (0.04, 0.11, 0.13, 0.15, 0.18)
60+
currents = [sim.DCSource(start=50, stop=150, amplitude=amp)
61+
for amp in I]
62+
for j, (neuron, current) in enumerate(zip(neurons, currents)):
63+
if j % 2 == 0: # these should
64+
neuron.inject(current, location="soma") # be entirely
65+
else: # equivalent
66+
current.inject_into([neuron], location="soma")
67+
68+
neurons.record('spikes')
69+
70+
sim.run(200.0)
71+
72+
spiketrains = neurons.get_data().segments[0].spiketrains
73+
assert len(spiketrains) == 5
74+
assert len(spiketrains[0]) == 0 # first cell does not fire
75+
# expected values taken from the average of simulations with NEURON and Arbor
76+
expected_spike_times = [
77+
np.array([]),
78+
np.array([52.41]),
79+
np.array([52.15, 68.45, 84.73, 101.02, 117.31, 133.61, 149.9]),
80+
np.array([51.96, 67.14, 82.13, 97.11, 112.08, 127.06, 142.04]),
81+
np.array([51.75, 65.86, 79.7, 93.51, 107.33, 121.14, 134.96, 148.77])
82+
]
83+
spike_times = [np.array(st) for st in spiketrains[1:]]
84+
max_error = 0
85+
for a, b in zip(spike_times, expected_spike_times[1:]):
86+
if a.size == b.size:
87+
max_error += abs((a - b) / b).max()
88+
else:
89+
max_error += 1
90+
print("max error =", max_error)
91+
assert max_error < 0.005, max_error
92+
sim.end()
93+
if "pytest" not in sys.modules:
94+
return a, b, spike_times
95+
96+
97+
if __name__ == '__main__':
98+
from pyNN.utility import get_simulator
99+
sim, args = get_simulator()
100+
test_scenario5(sim)

0 commit comments

Comments
 (0)