Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 30 additions & 1 deletion .github/workflows/full-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
- name: Install Arbor
if: startsWith(matrix.os, 'ubuntu')
run: |
python -m pip install arbor==0.9.0 libNeuroML morphio
python -m pip install arbor==0.10.0 libNeuroML morphio
- name: Install NESTML
if: startsWith(matrix.os, 'ubuntu')
run: |
Expand Down Expand Up @@ -102,6 +102,35 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.test-name }}
COVERALLS_PARALLEL: true
arbor-versions:
# Check that the Arbor backend works across the supported version range
name: Arbor ${{ matrix.arbor-version }} (Ubuntu, Python 3.12)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arbor-version: ["0.10.0", "0.12.2"]
steps:
- uses: actions/checkout@v6
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install Arbor ${{ matrix.arbor-version }} and dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest scipy
python -m pip install "arbor==${{ matrix.arbor-version }}" libNeuroML morphio
- name: Install PyNN itself
run: |
python -m pip install -e ".[test]"
- name: Test that the Arbor backend imports (builds the catalogue)
run: |
python -c "import arbor; print('arbor', arbor.__version__)"
python -c "import pyNN.arbor"
- name: Run Arbor unit and scenario tests
run: |
pytest -v test/unittests/test_arbor.py test/system/scenarios -k arbor
coveralls:
name: Indicate completion to coveralls.io
needs: test
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ $(NEST_STAMP): $(NEST_VENV_STAMP) $(NEST_SRC_UNPACKED)
$(CURDIR)/pyNN/nest/extensions
cd $(NEST_BUILD_DIR)/pynn_extensions && make install
$(NEST_PIP) install \
"neuron>=9.0.0" nrnutils "arbor==0.9.0" \
"neuron>=9.0.0" nrnutils "arbor==0.10.0" \
brian2 libNeuroML scipy matplotlib Cheetah3 h5py Jinja2 \
pytest pytest-xdist pytest-cov flake8 morphio nestml
$(NEST_PIP) install -e .
Expand Down
84 changes: 84 additions & 0 deletions pyNN/arbor/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Compatibility shims for supporting a range of Arbor versions from a single codebase.

:copyright: Copyright 2006-2026 by the PyNN team, see AUTHORS.
:license: CeCILL, see LICENSE for details.
"""

import arbor
from arbor import units as U


# --- iclamp was renamed to i_clamp in Arbor 0.12 -----------------------------

_MECHANISM_RENAMES = {"iclamp": "i_clamp"}


def get_electrode_mechanism(name):
"""Return the Arbor current-source mechanism class for the given PyNN model
name, accounting for renames across Arbor versions (e.g. iclamp -> i_clamp)."""
for candidate in (name, _MECHANISM_RENAMES.get(name)):
if candidate is not None and hasattr(arbor, candidate):
return getattr(arbor, candidate)
raise AttributeError(f"Arbor has no current-source mechanism {name!r}")


# --- decor.place() dropped the label arg for current stimuli in Arbor 0.12 ----

def _current_stimulus_place_takes_label():
decor = arbor.decor()
clamp = get_electrode_mechanism("iclamp")(0.0 * U.ms, 0.0 * U.ms, 0.0 * U.nA)
try:
decor.place("(root)", clamp, "_probe")
return True
except TypeError:
return False


_CURRENT_PLACE_TAKES_LABEL = _current_stimulus_place_takes_label()


def place_current_source(decor, locset, mechanism, label):
"""Place a current-clamp stimulus on a decor. Arbor 0.12 dropped the label
argument from the current-stimulus ``place()`` overload that 0.10 accepted
(synapse/junction/detector placements keep their label in all versions)."""
if _CURRENT_PLACE_TAKES_LABEL:
decor.place(locset, mechanism, label)
else:
decor.place(locset, mechanism)


# --- cv_policy_max_extent gained units in Arbor 0.12 -------------------------

def _cv_policy_wants_units():
try:
arbor.cv_policy_max_extent(1.0 * U.um)
return True
except TypeError:
return False


_CV_POLICY_WANTS_UNITS = _cv_policy_wants_units()


def max_extent_policy(length_um):
"""Build a max-extent cv_policy from a length in micrometres, handling the
0.12 change that requires a unit-typed quantity."""
if _CV_POLICY_WANTS_UNITS:
return arbor.cv_policy_max_extent(length_um * U.um)
return arbor.cv_policy_max_extent(length_um)


# --- discretisation moved from decor to cable_cell in Arbor 0.11 -------------

_DECOR_HAS_DISCRETIZATION = hasattr(arbor.decor(), "discretization")


def make_cable_cell(tree, decor, labels, discretization):
"""Construct an ``arbor.cable_cell``, applying the discretisation cv_policy in
the way the installed Arbor version expects: as a ``decor`` method in 0.10, or
as a ``cable_cell`` constructor argument in 0.11+."""
if _DECOR_HAS_DISCRETIZATION:
decor.discretization(discretization)
return arbor.cable_cell(tree, decor, labels)
return arbor.cable_cell(tree, decor, labels, discretization=discretization)
52 changes: 35 additions & 17 deletions pyNN/arbor/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@
from collections import defaultdict
from lazyarray import larray
import arbor
from arbor import units as U
from neuroml import Point3DWithDiam
from . import _compat
from ..morphology import Morphology, NeuroMLMorphology, MorphIOMorphology, IonChannelDistribution
from ..models import BaseCellType
from ..parameters import ParameterSpace


# Units for the parameters of current-source mechanisms (e.g. iclamp).
ELECTRODE_PARAM_UNITS = {
"tstart": U.ms,
"duration": U.ms,
"current": U.nA,
}


def convert_point(p3d: Point3DWithDiam) -> arbor.mpoint:
return arbor.mpoint(p3d.x, p3d.y, p3d.z, p3d.diameter/2)

Expand Down Expand Up @@ -64,7 +74,7 @@ def _build_tree(self, i):
if segment.name not in self.labels:
self.labels[segment.name] = f"(segment {i})"
elif isinstance(std_morphology, MorphIOMorphology):
tree = arbor.load_swc_neuron(std_morphology.morphology_file, raw=True)
tree = arbor.load_swc_neuron(std_morphology.morphology_file).segment_tree
else:
raise ValueError("{} not supported as a neuron morphology".format(type(std_morphology)))

Expand All @@ -89,18 +99,22 @@ def _build_decor(self, i):
decor = arbor.decor()
# Set the default properties of the cell (this overrides the model defaults).
decor.set_property(
cm=self.parameters["cm"][i] * 0.01, # µF/cm² -> F/m²
rL=self.parameters["Ra"][i] * 1, # Ω·cm
Vm=self.initial_values["v"][i]
cm=self.parameters["cm"][i] * U.uF / U.cm2,
rL=self.parameters["Ra"][i] * U.Ohm * U.cm,
Vm=self.initial_values["v"][i] * U.mV
)
if not self.parameters["ionic_species"]._evaluated:
self.parameters["ionic_species"].evaluate(simplify=True)
for ion_name, ionic_species in self.parameters["ionic_species"].items():
assert ion_name == ionic_species.ion_name
decor.set_ion(ion_name,
int_con=ionic_species.internal_concentration,
ext_con=ionic_species.external_concentration,
rev_pot=ionic_species.reversal_potential) # method="nernst/na")
ion_kwargs = {}
if ionic_species.internal_concentration is not None:
ion_kwargs["int_con"] = ionic_species.internal_concentration * U.mM
if ionic_species.external_concentration is not None:
ion_kwargs["ext_con"] = ionic_species.external_concentration * U.mM
if ionic_species.reversal_potential is not None:
ion_kwargs["rev_pot"] = ionic_species.reversal_potential * U.mV
decor.set_ion(ion_name, **ion_kwargs) # method="nernst/na")
for native_name, region_params in mechanism_parameters.items():
for region, params in region_params.items():
if native_name == "hh":
Expand All @@ -122,20 +136,20 @@ def _build_decor(self, i):
# insert current sources
for current_source in self.current_sources[i]:
location_generator = current_source["location_generator"]
mechanism = getattr(arbor, current_source["model_name"])
mechanism = _compat.get_electrode_mechanism(current_source["model_name"])
for locset, label in location_generator.generate_locations(morph, label=f"{current_source['model_name']}_label"):
params = current_source["parameters"].evaluate(simplify=True)
params = current_source["parameters"].evaluate(simplify=True).as_dict()
params = {
name: value * ELECTRODE_PARAM_UNITS[name] if name in ELECTRODE_PARAM_UNITS else value
for name, value in params.items()
}
mech = mechanism(**params)
decor.place(locset, mech, label)
_compat.place_current_source(decor, locset, mech, label)

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

policy = arbor.cv_policy_max_extent(10.0)
# to do: allow user to specify this value and/or the policy more generally
decor.discretization(policy)

return decor

def set_initial_values(self, variable, initial_values):
Expand All @@ -158,10 +172,14 @@ def set_shape(self, value):
pse.parameter_space.shape = value

def __call__(self, i):
# The discretisation cv_policy is applied by _compat.make_cable_cell at
# cell-construction time (on the decor in Arbor 0.10, or as a cable_cell
# argument in 0.11+). to do: allow the user to specify this value/policy.
return {
"tree": self._build_tree(i),
"decor": self._build_decor(i),
"labels": arbor.label_dict(self.labels)
"labels": arbor.label_dict(self.labels),
"discretization": _compat.max_extent_policy(10.0)
}


Expand Down
7 changes: 5 additions & 2 deletions pyNN/arbor/morphology.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,12 @@ def generate_locations(self, morphology, label):
for location in self.labels:
if location == "soma":
# todo: proper location mapping
locsets.append(('"root"', f"{label}-{location}"))
# Use the resolved locset expression rather than a label reference
# ('"root"'): Arbor 0.12's label resolution does not resolve label
# references in probe locsets against the cell's label_dict.
locsets.append(('(root)', f"{label}-{location}"))
elif location == "dendrite":
locsets.append(('"mid-dend"', f"{label}-{location}"))
locsets.append(('(location 0 0.5)', f"{label}-{location}"))
elif isinstance(location, str):
locsets.append((
f'(on-components 0.5 (region "{location}"))',
Expand Down
16 changes: 14 additions & 2 deletions pyNN/arbor/populations.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ..standardmodels import StandardCellType
from ..parameters import ParameterSpace, simplify, Sequence
from . import simulator
from . import _compat
from .recording import Recorder


Expand Down Expand Up @@ -87,11 +88,22 @@ def arbor_cell_description(self, gid):
for key, value in params.items():
if isinstance(value, Sequence):
params[key] = value.value
schedule = self.celltype.arbor_schedule(**params)
schedule_units = getattr(self.celltype, "arbor_schedule_units", {})
schedule_params = {}
for key, value in params.items():
unit = schedule_units.get(key)
if unit is None or value is None:
schedule_params[key] = value
elif isinstance(value, (list, tuple, np.ndarray)):
schedule_params[key] = [float(v) * unit for v in value]
else:
schedule_params[key] = value * unit
schedule = self.celltype.arbor_schedule(**schedule_params)
return arbor.spike_source_cell("spike-source", schedule)
else:
args = self._arbor_cell_description[index]
return arbor.cable_cell(args["tree"], args["decor"], args["labels"])
return _compat.make_cable_cell(
args["tree"], args["decor"], args["labels"], args["discretization"])

def _create_cells(self):
# for now, we create all cells and store them in memory,
Expand Down
3 changes: 2 additions & 1 deletion pyNN/arbor/projections.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from itertools import repeat

import arbor
from arbor import units as U

from .. import common
from ..core import ezip
Expand Down Expand Up @@ -89,7 +90,7 @@ def arbor_connections(self, gid):
(self.pre[cg.presynaptic_index], source),
target,
cg.weight,
cg.delay
cg.delay * U.ms
)
)
else:
Expand Down
20 changes: 15 additions & 5 deletions pyNN/arbor/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections import defaultdict
import numpy as np
import arbor
from arbor import units as U

from .. import recording
from . import simulator
Expand Down Expand Up @@ -58,18 +59,24 @@ def _localize_variables(self, variables, locations):
return resolved_variables

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

def _get_arbor_probes(self, gid):
probes = []
probe_index = 0
for variable in self.recorded:
if variable.location is None:
pass
Expand All @@ -79,12 +86,15 @@ def _get_arbor_probes(self, gid):
if gid in [cell.gid for cell in self.recorded[variable]]:
if variable.name == "spikes":
continue
elif variable.name == "v":
probe = arbor.cable_probe_membrane_voltage(locset)
# Tag must match the one assigned in _set_arbor_sim (per-gid index).
tag = str(probe_index)
probe_index += 1
if variable.name == "v":
probe = arbor.cable_probe_membrane_voltage(locset, tag)
else:
mech_name, state_name = variable.name.split(".")
arbor_model = mech_name # to do: find_arbor_model(mech_name)
probe = arbor.cable_probe_density_state(locset, arbor_model, state_name)
probe = arbor.cable_probe_density_state(locset, arbor_model, state_name, tag)
probes.append(probe)
return probes

Expand Down
Loading
Loading