Skip to content

Commit f914bbe

Browse files
committed
Update Arbor backend to work with Arbor 0.10.0
Arbor 0.10.0 introduced breaking changes to its Python bindings that prevented the backend from importing/running. Update the backend and version pins accordingly: - Units at the user interface: physical quantities passed to decor.set_property/set_ion, threshold_detector, iclamp, the spike schedules and simulation.run must now be unit-typed (arbor.units). - Morphology loaders now return a bundle; use load_swc_neuron(f).segment_tree instead of the removed raw=True argument. - Probes now require a tag string and sampling is addressed by (gid, tag) instead of the removed cell_member index. Also fixes a latent frequency-unit bug in SpikeSourcePoisson: rate (Hz) was passed into Arbor's kHz frequency slot; it is now tagged with U.Hz, matching PyNN's Hz semantics and the NEURON/NEST backends.
1 parent 989f9df commit f914bbe

8 files changed

Lines changed: 62 additions & 20 deletions

File tree

.github/workflows/full-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
- name: Install Arbor
5959
if: startsWith(matrix.os, 'ubuntu')
6060
run: |
61-
python -m pip install arbor==0.9.0 libNeuroML morphio
61+
python -m pip install arbor==0.10.0 libNeuroML morphio
6262
- name: Install NESTML
6363
if: startsWith(matrix.os, 'ubuntu')
6464
run: |

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ $(NEST_STAMP): $(NEST_VENV_STAMP) $(NEST_SRC_UNPACKED)
9797
$(CURDIR)/pyNN/nest/extensions
9898
cd $(NEST_BUILD_DIR)/pynn_extensions && make install
9999
$(NEST_PIP) install \
100-
"neuron>=9.0.0" nrnutils "arbor==0.9.0" \
100+
"neuron>=9.0.0" nrnutils "arbor==0.10.0" \
101101
brian2 libNeuroML scipy matplotlib Cheetah3 h5py Jinja2 \
102102
pytest pytest-xdist pytest-cov flake8 morphio nestml
103103
$(NEST_PIP) install -e .

pyNN/arbor/cells.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22
from collections import defaultdict
33
from lazyarray import larray
44
import arbor
5+
from arbor import units as U
56
from neuroml import Point3DWithDiam
67
from ..morphology import Morphology, NeuroMLMorphology, MorphIOMorphology, IonChannelDistribution
78
from ..models import BaseCellType
89
from ..parameters import ParameterSpace
910

1011

12+
# Units for the parameters of current-source mechanisms (e.g. iclamp).
13+
ELECTRODE_PARAM_UNITS = {
14+
"tstart": U.ms,
15+
"duration": U.ms,
16+
"current": U.nA,
17+
}
18+
19+
1120
def convert_point(p3d: Point3DWithDiam) -> arbor.mpoint:
1221
return arbor.mpoint(p3d.x, p3d.y, p3d.z, p3d.diameter/2)
1322

@@ -64,7 +73,7 @@ def _build_tree(self, i):
6473
if segment.name not in self.labels:
6574
self.labels[segment.name] = f"(segment {i})"
6675
elif isinstance(std_morphology, MorphIOMorphology):
67-
tree = arbor.load_swc_neuron(std_morphology.morphology_file, raw=True)
76+
tree = arbor.load_swc_neuron(std_morphology.morphology_file).segment_tree
6877
else:
6978
raise ValueError("{} not supported as a neuron morphology".format(type(std_morphology)))
7079

@@ -89,18 +98,22 @@ def _build_decor(self, i):
8998
decor = arbor.decor()
9099
# Set the default properties of the cell (this overrides the model defaults).
91100
decor.set_property(
92-
cm=self.parameters["cm"][i] * 0.01, # µF/cm² -> F/m²
93-
rL=self.parameters["Ra"][i] * 1, # Ω·cm
94-
Vm=self.initial_values["v"][i]
101+
cm=self.parameters["cm"][i] * U.uF / U.cm2,
102+
rL=self.parameters["Ra"][i] * U.Ohm * U.cm,
103+
Vm=self.initial_values["v"][i] * U.mV
95104
)
96105
if not self.parameters["ionic_species"]._evaluated:
97106
self.parameters["ionic_species"].evaluate(simplify=True)
98107
for ion_name, ionic_species in self.parameters["ionic_species"].items():
99108
assert ion_name == ionic_species.ion_name
100-
decor.set_ion(ion_name,
101-
int_con=ionic_species.internal_concentration,
102-
ext_con=ionic_species.external_concentration,
103-
rev_pot=ionic_species.reversal_potential) # method="nernst/na")
109+
ion_kwargs = {}
110+
if ionic_species.internal_concentration is not None:
111+
ion_kwargs["int_con"] = ionic_species.internal_concentration * U.mM
112+
if ionic_species.external_concentration is not None:
113+
ion_kwargs["ext_con"] = ionic_species.external_concentration * U.mM
114+
if ionic_species.reversal_potential is not None:
115+
ion_kwargs["rev_pot"] = ionic_species.reversal_potential * U.mV
116+
decor.set_ion(ion_name, **ion_kwargs) # method="nernst/na")
104117
for native_name, region_params in mechanism_parameters.items():
105118
for region, params in region_params.items():
106119
if native_name == "hh":
@@ -124,12 +137,16 @@ def _build_decor(self, i):
124137
location_generator = current_source["location_generator"]
125138
mechanism = getattr(arbor, current_source["model_name"])
126139
for locset, label in location_generator.generate_locations(morph, label=f"{current_source['model_name']}_label"):
127-
params = current_source["parameters"].evaluate(simplify=True)
140+
params = current_source["parameters"].evaluate(simplify=True).as_dict()
141+
params = {
142+
name: value * ELECTRODE_PARAM_UNITS[name] if name in ELECTRODE_PARAM_UNITS else value
143+
for name, value in params.items()
144+
}
128145
mech = mechanism(**params)
129146
decor.place(locset, mech, label)
130147

131148
# add spike source
132-
decor.place('"root"', arbor.threshold_detector(-10), "detector")
149+
decor.place('"root"', arbor.threshold_detector(-10 * U.mV), "detector")
133150
# todo: allow user to choose location and threshold value
134151

135152
policy = arbor.cv_policy_max_extent(10.0)

pyNN/arbor/populations.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,17 @@ def arbor_cell_description(self, gid):
8787
for key, value in params.items():
8888
if isinstance(value, Sequence):
8989
params[key] = value.value
90-
schedule = self.celltype.arbor_schedule(**params)
90+
schedule_units = getattr(self.celltype, "arbor_schedule_units", {})
91+
schedule_params = {}
92+
for key, value in params.items():
93+
unit = schedule_units.get(key)
94+
if unit is None or value is None:
95+
schedule_params[key] = value
96+
elif isinstance(value, (list, tuple, np.ndarray)):
97+
schedule_params[key] = [float(v) * unit for v in value]
98+
else:
99+
schedule_params[key] = value * unit
100+
schedule = self.celltype.arbor_schedule(**schedule_params)
91101
return arbor.spike_source_cell("spike-source", schedule)
92102
else:
93103
args = self._arbor_cell_description[index]

pyNN/arbor/recording.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections import defaultdict
66
import numpy as np
77
import arbor
8+
from arbor import units as U
89

910
from .. import recording
1011
from . import simulator
@@ -58,18 +59,24 @@ def _localize_variables(self, variables, locations):
5859
return resolved_variables
5960

6061
def _set_arbor_sim(self, arbor_sim):
62+
# Since Arbor 0.10.0, probes are addressed by (gid, tag) rather than by
63+
# a positional cell_member index. The tag assigned here (a per-gid probe
64+
# counter, in the same iteration order as _get_arbor_probes) must match
65+
# the tag given to the corresponding probe there.
6166
self.handles = defaultdict(list)
6267
probe_indices = defaultdict(int)
6368
for variable in self.recorded:
6469
if variable.name != "spikes":
6570
for cell in self.recorded[variable]:
66-
probeset_id = arbor.cell_member(cell.gid, probe_indices[cell.gid])
71+
tag = str(probe_indices[cell.gid])
6772
probe_indices[cell.gid] += 1
68-
handle = arbor_sim.sample(probeset_id, arbor.regular_schedule(self.sampling_interval))
73+
handle = arbor_sim.sample(
74+
cell.gid, tag, arbor.regular_schedule(self.sampling_interval * U.ms))
6975
self.handles[variable].append(handle)
7076

7177
def _get_arbor_probes(self, gid):
7278
probes = []
79+
probe_index = 0
7380
for variable in self.recorded:
7481
if variable.location is None:
7582
pass
@@ -79,12 +86,15 @@ def _get_arbor_probes(self, gid):
7986
if gid in [cell.gid for cell in self.recorded[variable]]:
8087
if variable.name == "spikes":
8188
continue
82-
elif variable.name == "v":
83-
probe = arbor.cable_probe_membrane_voltage(locset)
89+
# Tag must match the one assigned in _set_arbor_sim (per-gid index).
90+
tag = str(probe_index)
91+
probe_index += 1
92+
if variable.name == "v":
93+
probe = arbor.cable_probe_membrane_voltage(locset, tag)
8494
else:
8595
mech_name, state_name = variable.name.split(".")
8696
arbor_model = mech_name # to do: find_arbor_model(mech_name)
87-
probe = arbor.cable_probe_density_state(locset, arbor_model, state_name)
97+
probe = arbor.cable_probe_density_state(locset, arbor_model, state_name, tag)
8898
probes.append(probe)
8999
return probes
90100

pyNN/arbor/simulator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
except ImportError:
1414
pass
1515
import arbor
16+
from arbor import units as U
1617
from .. import common
1718
from ..core import find
1819

@@ -196,7 +197,7 @@ def run(self, simtime):
196197
for recorder in self.recorders:
197198
recorder._set_arbor_sim(self.arbor_sim)
198199
self.t += simtime
199-
self.arbor_sim.run(self.t, self.dt)
200+
self.arbor_sim.run(self.t * U.ms, self.dt * U.ms)
200201
self.running = True
201202

202203
def run_until(self, tstop):

pyNN/arbor/standardmodels.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import numpy as np
1212
import arbor
13+
from arbor import units as U
1314

1415
from ..standardmodels import cells, ion_channels, synapses, electrodes, receptors, build_translations
1516
from ..parameters import ParameterSpace, IonicSpecies
@@ -32,6 +33,7 @@ class SpikeSourcePoisson(cells.SpikeSourcePoisson):
3233
# todo: manage "seed"
3334
arbor_cell_kind = arbor.cell_kind.spike_source
3435
arbor_schedule = arbor.poisson_schedule
36+
arbor_schedule_units = {"tstart": U.ms, "freq": U.Hz, "tstop": U.ms}
3537

3638

3739
class SpikeSourceArray(cells.SpikeSourceArray):
@@ -42,6 +44,8 @@ class SpikeSourceArray(cells.SpikeSourceArray):
4244
)
4345
arbor_cell_kind = arbor.cell_kind.spike_source
4446
arbor_schedule = arbor.explicit_schedule
47+
# Since Arbor 0.10.0, schedule parameters must be unit-typed.
48+
arbor_schedule_units = {"times": U.ms}
4549

4650

4751
class BaseCurrentSource(object):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ sonata = ["h5py"]
4242
morphologies = ["morphio"]
4343
neuron = ["neuron", "nrnutils"]
4444
brian2 = ["brian2"]
45-
arbor = ["arbor==0.9.0", "libNeuroML", "morphio"]
45+
arbor = ["arbor==0.10.0", "libNeuroML", "morphio"]
4646
spiNNaker = ["spyNNaker"]
4747
neuroml = ["libNeuroML"]
4848
nestml = ["nestml"]

0 commit comments

Comments
 (0)