diff --git a/arbor/common_types_io.cpp b/arbor/common_types_io.cpp index 81cbc05d35cce691bfdbefd00fe3797a59fd1539..9ade3cd0410eaef322fd73cd22b44c5a706b59c8 100644 --- a/arbor/common_types_io.cpp +++ b/arbor/common_types_io.cpp @@ -8,6 +8,8 @@ ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, lid_selection_policy pol switch (policy) { case lid_selection_policy::round_robin: return o << "round_robin"; + case lid_selection_policy::round_robin_halt: + return o << "round_robin_halt"; case lid_selection_policy::assert_univalent: return o << "univalent"; } diff --git a/arbor/include/arbor/common_types.hpp b/arbor/include/arbor/common_types.hpp index 7db5bcde5af2b840cfd8c8e87d0964bf09a15aaf..4cf6a41d6db6be7daf060e58a65c24c9f0d0f925 100644 --- a/arbor/include/arbor/common_types.hpp +++ b/arbor/include/arbor/common_types.hpp @@ -70,6 +70,7 @@ struct lid_range { enum class lid_selection_policy { round_robin, + round_robin_halt, assert_univalent // throw if the range of possible lids is wider than 1 }; diff --git a/arbor/label_resolution.cpp b/arbor/label_resolution.cpp index 43c9a688f2031acf80a52b5a4db4a121f84262e4..8d5ddadbb7fe283ec0bddd8f8a6f37a711a49ad6 100644 --- a/arbor/label_resolution.cpp +++ b/arbor/label_resolution.cpp @@ -78,7 +78,6 @@ lid_hopefully label_resolution_map::range_set::at(unsigned idx) const { // Offset into the range containing idx. const auto& range_part = part.at(ridx); auto offset = idx - range_part.first; - return start + offset; } @@ -126,6 +125,19 @@ lid_hopefully round_robin_state::update(const label_resolution_map::range_set& r return lid; } +cell_lid_type round_robin_state::get() { + return state; +} + +lid_hopefully round_robin_halt_state::update(const label_resolution_map::range_set& range_set) { + auto lid = range_set.at(state); + return lid; +} + +cell_lid_type round_robin_halt_state::get() { + return state; +} + lid_hopefully assert_univalent_state::update(const label_resolution_map::range_set& range_set) { if (range_set.size() != 1) { return util::unexpected("range is not univalent"); @@ -134,11 +146,29 @@ lid_hopefully assert_univalent_state::update(const label_resolution_map::range_s return range_set.at(0); } +cell_lid_type assert_univalent_state::get() { + return 0; +} + // resolver methods resolver::state_variant resolver::construct_state(lid_selection_policy pol) { switch (pol) { case lid_selection_policy::round_robin: return round_robin_state(); + case lid_selection_policy::round_robin_halt: + return round_robin_halt_state(); + case lid_selection_policy::assert_univalent: + return assert_univalent_state(); + default: return assert_univalent_state(); + } +} + +resolver::state_variant resolver::construct_state(lid_selection_policy pol, cell_lid_type state) { + switch (pol) { + case lid_selection_policy::round_robin: + return round_robin_state(state); + case lid_selection_policy::round_robin_halt: + return round_robin_halt_state(state); case lid_selection_policy::assert_univalent: return assert_univalent_state(); default: return assert_univalent_state(); @@ -151,15 +181,24 @@ cell_lid_type resolver::resolve(const cell_global_label_type& iden) { } const auto& range_set = label_map_->at(iden.gid, iden.label.tag); - // Construct state if if doesn't exist - if (!state_map_[iden.gid][iden.label.tag].count(iden.label.policy)) { - state_map_[iden.gid][iden.label.tag][iden.label.policy] = construct_state(iden.label.policy); - } - + // Policy round_robin_halt: use previous state of round_robin policy, if existent + if (iden.label.policy == lid_selection_policy::round_robin_halt + && state_map_[iden.gid][iden.label.tag].count(lid_selection_policy::round_robin)) { + cell_lid_type prev_state_rr = std::visit([range_set](auto& state) { return state.get(); }, state_map_[iden.gid][iden.label.tag][lid_selection_policy::round_robin]); + state_map_[iden.gid][iden.label.tag][lid_selection_policy::round_robin_halt] = construct_state(lid_selection_policy::round_robin_halt, prev_state_rr); + } + + // Construct state if it doesn't exist + if (!state_map_[iden.gid][iden.label.tag].count(iden.label.policy)) { + state_map_[iden.gid][iden.label.tag][iden.label.policy] = construct_state(iden.label.policy); + } + + // Update state auto lid = std::visit([range_set](auto& state) { return state.update(range_set); }, state_map_[iden.gid][iden.label.tag][iden.label.policy]); if (!lid) { throw arb::bad_connection_label(iden.gid, iden.label.tag, lid.error()); } + return lid.value(); } diff --git a/arbor/label_resolution.hpp b/arbor/label_resolution.hpp index 7cd69a5bd8cbc8908cb9293fb3bf6d9b2ad17ea4..c015524e4a1f80bc609597902d89d69e2e3976d6 100644 --- a/arbor/label_resolution.hpp +++ b/arbor/label_resolution.hpp @@ -88,13 +88,23 @@ private: }; struct ARB_ARBOR_API round_robin_state { - cell_size_type state = 0; + cell_lid_type state = 0; round_robin_state() : state(0) {}; round_robin_state(cell_lid_type state) : state(state) {}; + cell_lid_type get(); + lid_hopefully update(const label_resolution_map::range_set& range); +}; + +struct ARB_ARBOR_API round_robin_halt_state { + cell_lid_type state = 0; + round_robin_halt_state() : state(0) {}; + round_robin_halt_state(cell_lid_type state) : state(state) {}; + cell_lid_type get(); lid_hopefully update(const label_resolution_map::range_set& range); }; struct ARB_ARBOR_API assert_univalent_state { + cell_lid_type get(); lid_hopefully update(const label_resolution_map::range_set& range); }; @@ -105,9 +115,10 @@ struct ARB_ARBOR_API resolver { cell_lid_type resolve(const cell_global_label_type& iden); private: - using state_variant = std::variant<round_robin_state, assert_univalent_state>; + using state_variant = std::variant<round_robin_state, round_robin_halt_state, assert_univalent_state>; state_variant construct_state(lid_selection_policy pol); + state_variant construct_state(lid_selection_policy pol, cell_lid_type state); const label_resolution_map* label_map_; std::unordered_map<cell_gid_type, std::unordered_map<cell_tag_type, std::unordered_map <lid_selection_policy, state_variant>>> state_map_; diff --git a/doc/cpp/cell.rst b/doc/cpp/cell.rst index 56e34c7c4be32a767845c64011de1d52ad01ca7f..6a8450e2e323006d7448cac098ee397fff8db492 100644 --- a/doc/cpp/cell.rst +++ b/doc/cpp/cell.rst @@ -54,6 +54,10 @@ cells and members of cell-local collections. Iterate over the items of the group in a round-robin fashion. + .. cpp:enumerator:: round_robin_halt + + Halts at the current item of the group until the round_robin policy is called (again). + .. cpp:enumerator:: assert_univalent Assert that ony one item is available in the group. Throws an exception if the assertion diff --git a/doc/python/cell.rst b/doc/python/cell.rst index 7f19f603edc97b61e66ca42c2ee769f6fdaabb06..6ea77ad989268895b108d95f310e815faeaa4c68 100644 --- a/doc/python/cell.rst +++ b/doc/python/cell.rst @@ -18,6 +18,10 @@ The types defined below are used as identifiers for cells and members of cell-lo Iterate over the items of the group in a round-robin fashion. + .. attribute:: round_robin_halt + + Halts at the current item of the group until the round_robin policy is called (again). + .. attribute:: univalent Assert that only one item is available in the group. Throws an exception if the assertion diff --git a/mechanisms/CMakeLists.txt b/mechanisms/CMakeLists.txt index a219475fac58e26ffe6eda487ad67bd256bbd507..976304bc63295afa065417b66e33a6a2168d612b 100644 --- a/mechanisms/CMakeLists.txt +++ b/mechanisms/CMakeLists.txt @@ -27,7 +27,7 @@ make_catalogue( NAME default SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/default" OUTPUT "CAT_DEFAULT_SOURCES" - MOD exp2syn expsyn expsyn_stdp hh kamt kdrmt nax nernst pas gj + MOD exp2syn expsyn expsyn_curr expsyn_stdp hh kamt kdrmt nax nernst pas gj CXX PREFIX "${PROJECT_SOURCE_DIR}/mechanisms" CXX_FLAGS_TARGET "${ARB_CXX_FLAGS_TARGET_FULL}" diff --git a/mechanisms/default/expsyn_curr.mod b/mechanisms/default/expsyn_curr.mod new file mode 100644 index 0000000000000000000000000000000000000000..35e9cae5e8f80162fe2b7fe6eaa6d84b4c029453 --- /dev/null +++ b/mechanisms/default/expsyn_curr.mod @@ -0,0 +1,47 @@ +: Exponential current-based synapse + +NEURON { + POINT_PROCESS expsyn_curr + RANGE w, tau, R_mem + NONSPECIFIC_CURRENT I +} + +UNITS { + (ms) = (milliseconds) + (mV) = (millivolt) + (MOhm) = (megaohm) +} + +PARAMETER { + R_mem = 10.0 (MOhm) : membrane resistance + tau = 5.0 (ms) : synaptic time constant + w = 4.20075 (mV) : weight +} + +STATE { + g (mV) : instantaneous synaptic conductance +} + +INITIAL { + g = 0 +} + +BREAKPOINT { + :SOLVE state METHOD cnexp + SOLVE state METHOD sparse : to match with expsyn_curr_calcium_plasticity + + I = -g / R_mem +} + +DERIVATIVE state { + : Exponential decay of postsynaptic potential + g' = -g / tau +} + +NET_RECEIVE(weight) { + if (weight >= 0) { + : Start of postsynaptic potential + g = g + w + } +} + diff --git a/python/identifiers.cpp b/python/identifiers.cpp index 71e583a6242e9a2f7f2562c347f1cb868cfbacbf..d34dd332d285a5e731b0c45842c88e4ef393c8ae 100644 --- a/python/identifiers.cpp +++ b/python/identifiers.cpp @@ -18,7 +18,9 @@ void register_identifiers(py::module& m) { py::enum_<arb::lid_selection_policy>(m, "selection_policy", "Enumeration used to identify a selection policy, used by the model for selecting one of possibly multiple locations on the cell associated with a labeled item.") .value("round_robin", arb::lid_selection_policy::round_robin, - "iterate round-robin over all possible locations.") + "Iterate round-robin over all possible locations.") + .value("round_robin_halt", arb::lid_selection_policy::round_robin_halt, + "Halts at the current location until the round_robin policy is called (again).") .value("univalent", arb::lid_selection_policy::assert_univalent, "Assert that there is only one possible location associated with a labeled item on the cell. The model throws an exception if the assertion fails."); diff --git a/python/test/fixtures.py b/python/test/fixtures.py index a0d90a56b842783d4c7d53924280a02e1c0a0774..f046865679a9ee9b802453acc4c9177f4188b214 100644 --- a/python/test/fixtures.py +++ b/python/test/fixtures.py @@ -140,34 +140,10 @@ class empty_recipe(arbor.recipe): """ pass - -@_fixture -def cable_cell(): - # (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm - tree = arbor.segment_tree() - tree.append( - arbor.mnpos, - arbor.mpoint(-3, 0, 0, 3), - arbor.mpoint(3, 0, 0, 3), - tag=1, - ) - - # (2) Define the soma and its midpoint - labels = arbor.label_dict({'soma': '(tag 1)', - 'midpoint': '(location 0 0.5)'}) - - # (3) Create cell and set properties - decor = arbor.decor() - decor.set_property(Vm=-40) - decor.paint('"soma"', arbor.density('hh')) - decor.place('"midpoint"', arbor.iclamp( 10, 2, 0.8), "iclamp") - decor.place('"midpoint"', arbor.spike_detector(-10), "detector") - return arbor.cable_cell(tree, labels, decor) - @_fixture class art_spiker_recipe(arbor.recipe): """ - Recipe fixture with 3 artificial spiking cells. + Recipe fixture with 3 artificial spiking cells and one cable cell. """ def __init__(self): super().__init__() @@ -201,15 +177,52 @@ class art_spiker_recipe(arbor.recipe): else: return [arbor.cable_probe_membrane_voltage('"midpoint"')] - @cable_cell - def _cable_cell(self, cable_cell): - return cable_cell + def _cable_cell_elements(self): + # (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm + tree = arbor.segment_tree() + tree.append( + arbor.mnpos, + arbor.mpoint(-3, 0, 0, 3), + arbor.mpoint(3, 0, 0, 3), + tag=1, + ) + + # (2) Define the soma and its midpoint + labels = arbor.label_dict({'soma': '(tag 1)', + 'midpoint': '(location 0 0.5)'}) + + # (3) Create cell and set properties + decor = arbor.decor() + decor.set_property(Vm=-40) + decor.paint('"soma"', arbor.density('hh')) + decor.place('"midpoint"', arbor.iclamp( 10, 2, 0.8), "iclamp") + decor.place('"midpoint"', arbor.spike_detector(-10), "detector") + + # return tuple of tree, labels, and decor for creating a cable cell (can still be modified before calling arbor.cable_cell()) + return tree, labels, decor def cell_description(self, gid): if gid < 3: return arbor.spike_source_cell("src", arbor.explicit_schedule(self.trains[gid])) else: - return self._cable_cell() + tree, labels, decor = self._cable_cell_elements() + return arbor.cable_cell(tree, labels, decor) + +@_fixture +def sum_weight_hh_spike(): + """ + Fixture returning connection weight for 'expsyn_stdp' mechanism which is just enough to evoke an immediate spike + at t=1ms in the 'hh' neuron in 'art_spiker_recipe' + """ + return 0.4 + +@_fixture +def sum_weight_hh_spike_2(): + """ + Fixture returning connection weight for 'expsyn_stdp' mechanism which is just enough to evoke an immediate spike + at t=1.8ms in the 'hh' neuron in 'art_spiker_recipe' + """ + return 0.36 @_fixture @context diff --git a/python/test/unit/test_multiple_connections.py b/python/test/unit/test_multiple_connections.py new file mode 100644 index 0000000000000000000000000000000000000000..3f065e83955c7da06516e7ceb73092dd4fc81905 --- /dev/null +++ b/python/test/unit/test_multiple_connections.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# +# test_multiple_connections.py + +import unittest +import types +import numpy as np + +import arbor as arb +from .. import fixtures + +""" +tests for multiple connections onto the same postsynaptic label and for one connection that has the same net impact as the multiple-connection paradigm, +thereby testing the selection policies 'round_robin', 'round_robin_halt', and 'univalent' + +NOTE: In principle, a plasticity (STDP) mechanism is employed here to test if a selected connection uses the correct instance of the mechanism. + Thus, the scenario in Test #1 is intentionally "a wrong one", as opposed to the scenario in Test #2. In Test #1, one presynaptic neuron effectively connects _via one synapse_ to two postsynaptic neurons, + and the spike at t=0.8ms in presynaptic neuron 0 will enhance potentiation in both the first and the second synapse mechanism. In Test #2, this is prevented by the 'round_robin_halt' policy, whereby the + potentiation in the second synapse mechanism is only enhanced by spikes of presynaptic neuron 1. +""" + +class TestMultipleConnections(unittest.TestCase): + + # Constructor (overridden) + def __init__(self, args): + super(TestMultipleConnections, self).__init__(args) + + self.runtime = 2 # ms + self.dt = 0.01 # ms + + # Method creating a new mechanism for a synapse with STDP + def create_syn_mechanism(self, scale_contrib = 1): + # create new synapse mechanism + syn_mechanism = arb.mechanism("expsyn_stdp") + + # set pre- and postsynaptic contributions for STDP + syn_mechanism.set("Apre", 0.01 * scale_contrib) + syn_mechanism.set("Apost", -0.01 * scale_contrib) + + # set minimal decay time + syn_mechanism.set("tau", self.dt) + + return syn_mechanism + + # Method that does the final evaluation for all tests + def evaluate_outcome(self, sim, handle_mem): + # membrane potential should temporarily be above the spiking threshold at around 1.0 ms (only testing this if the current node keeps the data, cf. GitHub issue #1892) + if len(sim.samples(handle_mem)) > 0: + data_mem, _ = sim.samples(handle_mem)[0] + #print(data_mem[(data_mem[:, 0] >= 1.0), 1]) + self.assertGreater(data_mem[(np.round(data_mem[:, 0], 2) == 1.02), 1], -10) + self.assertLess(data_mem[(np.round(data_mem[:, 0], 2) == 1.05), 1], -10) + + # neuron 3 should spike at around 1.0 ms, when the added input from all connections will cause threshold crossing + spike_times = sim.spikes()["time"] + spike_gids = sim.spikes()["source"]["gid"] + #print(list(zip(*[spike_times, spike_gids]))) + self.assertGreater(sum(spike_gids == 3), 0) + self.assertAlmostEqual(spike_times[(spike_gids == 3)][0], 1.00, delta=0.04) + + # Method that does additional evaluation for Test #1 + def evaluate_additional_outcome_1(self, sim, handle_mem): + # order of spiking neurons (also cf. 'test_spikes.py') + spike_gids = sim.spikes()["source"]["gid"] + self.assertEqual([2, 1, 0, 3, 3], spike_gids.tolist()) + + # neuron 3 should spike again at around 1.8 ms, when the added input from all connections will cause threshold crossing + spike_times = sim.spikes()["time"] + self.assertAlmostEqual(spike_times[(spike_gids == 3)][1], 1.80, delta=0.04) + + # Method that does additional evaluation for Test #2 and Test #3 + def evaluate_additional_outcome_2_3(self, sim, handle_mem): + # order of spiking neurons (also cf. 'test_spikes.py') + spike_gids = sim.spikes()["source"]["gid"] + self.assertEqual([2, 1, 0, 3], spike_gids.tolist()) + + # Method that runs the main part of Test #1 and Test #2 + def rr_main(self, context, art_spiker_recipe, weight, weight2): + # define new method 'cell_description()' and overwrite the original one in the 'art_spiker_recipe' object + create_syn_mechanism = self.create_syn_mechanism + def cell_description(self, gid): + # spike source neuron + if gid < 3: + return arb.spike_source_cell("spike_source", arb.explicit_schedule(self.trains[gid])) + + # spike-receiving cable neuron + elif gid == 3: + tree, labels, decor = self._cable_cell_elements() + + scale_stdp = 0.5 # use only half of the original magnitude for STDP because two connections will come into play + + decor.place('"midpoint"', arb.synapse(create_syn_mechanism(scale_stdp)), "postsyn_target") # place synapse for input from one presynaptic neuron at the center of the soma + decor.place('"midpoint"', arb.synapse(create_syn_mechanism(scale_stdp)), "postsyn_target") # place synapse for input from another presynaptic neuron at the center of the soma + # (using the same label as above!) + return arb.cable_cell(tree, labels, decor) + art_spiker_recipe.cell_description = types.MethodType(cell_description, art_spiker_recipe) + + # read connections from recipe for testing + connections_from_recipe = art_spiker_recipe.connections_on(3) + + # connection #1 from neuron 0 to 3 + self.assertEqual(connections_from_recipe[0].dest.label, "postsyn_target") + self.assertAlmostEqual(connections_from_recipe[0].weight, weight) + self.assertAlmostEqual(connections_from_recipe[0].delay, 0.2) + + # connection #2 from neuron 0 to 3 + self.assertEqual(connections_from_recipe[1].dest.label, "postsyn_target") + self.assertAlmostEqual(connections_from_recipe[1].weight, weight) + self.assertAlmostEqual(connections_from_recipe[1].delay, 0.2) + + # connection #1 from neuron 1 to 3 + self.assertEqual(connections_from_recipe[2].dest.label, "postsyn_target") + self.assertAlmostEqual(connections_from_recipe[2].weight, weight2) + self.assertAlmostEqual(connections_from_recipe[2].delay, 1.4) + + # connection #2 from neuron 1 to 3 + self.assertEqual(connections_from_recipe[3].dest.label, "postsyn_target") + self.assertAlmostEqual(connections_from_recipe[3].weight, weight2) + self.assertAlmostEqual(connections_from_recipe[3].delay, 1.4) + + # construct domain_decomposition and simulation object + dd = arb.partition_load_balance(art_spiker_recipe, context) + sim = arb.simulation(art_spiker_recipe, dd, context) + sim.record(arb.spike_recording.all) + + # create schedule and handle to record the membrane potential of neuron 3 + reg_sched = arb.regular_schedule(0, self.dt, self.runtime) + handle_mem = sim.sample((3, 0), reg_sched) + + # run the simulation + sim.run(self.runtime, self.dt) + + return sim, handle_mem + + # Test #1 (for 'round_robin') + @fixtures.context + @fixtures.art_spiker_recipe + @fixtures.sum_weight_hh_spike + @fixtures.sum_weight_hh_spike_2 + def test_multiple_connections_rr_no_halt(self, context, art_spiker_recipe, sum_weight_hh_spike, sum_weight_hh_spike_2): + weight = sum_weight_hh_spike/2 # connection strength which is, summed over two connections, just enough to evoke an immediate spike at t=1ms + weight2 = 0.97*sum_weight_hh_spike_2/2 # connection strength which is, summed over two connections, just NOT enough to evoke an immediate spike at t=1.8ms + + # define new method 'connections_on()' and overwrite the original one in the 'art_spiker_recipe' object + def connections_on(self, gid): + # incoming to neurons 0--2 + if gid < 3: + return [] + + # incoming to neuron 3 + elif gid == 3: + source_label_0 = arb.cell_global_label(0, "spike_source") # referring to the "spike_source" label of neuron 0 + source_label_1 = arb.cell_global_label(1, "spike_source") # referring to the "spike_source" label of neuron 1 + + target_label_rr = arb.cell_local_label("postsyn_target", arb.selection_policy.round_robin) # referring to the current item in the "postsyn_target" label group of neuron 3, moving to the next item afterwards + + conn_0_3_n1 = arb.connection(source_label_0, target_label_rr, weight, 0.2) # first connection from neuron 0 to 3 + conn_0_3_n2 = arb.connection(source_label_0, target_label_rr, weight, 0.2) # second connection from neuron 0 to 3 + # NOTE: this is not connecting to the same target label item as 'conn_0_3_n1' because 'round_robin' has been used before! + conn_1_3_n1 = arb.connection(source_label_1, target_label_rr, weight2, 1.4) # first connection from neuron 1 to 3 + conn_1_3_n2 = arb.connection(source_label_1, target_label_rr, weight2, 1.4) # second connection from neuron 1 to 3 + # NOTE: this is not connecting to the same target label item as 'conn_1_3_n1' because 'round_robin' has been used before! + + return [conn_0_3_n1, conn_0_3_n2, conn_1_3_n1, conn_1_3_n2] + art_spiker_recipe.connections_on = types.MethodType(connections_on, art_spiker_recipe) + + # run the main part of this test + sim, handle_mem = self.rr_main(context, art_spiker_recipe, weight, weight2) + + # evaluate the outcome + self.evaluate_outcome(sim, handle_mem) + self.evaluate_additional_outcome_1(sim, handle_mem) + + # Test #2 (for the combination of 'round_robin_halt' and 'round_robin') + @fixtures.context + @fixtures.art_spiker_recipe + @fixtures.sum_weight_hh_spike + @fixtures.sum_weight_hh_spike_2 + def test_multiple_connections_rr_halt(self, context, art_spiker_recipe, sum_weight_hh_spike, sum_weight_hh_spike_2): + weight = sum_weight_hh_spike/2 # connection strength which is, summed over two connections, just enough to evoke an immediate spike at t=1ms + weight2 = 0.97*sum_weight_hh_spike_2/2 # connection strength which is, summed over two connections, just NOT enough to evoke an immediate spike at t=1.8ms + + # define new method 'connections_on()' and overwrite the original one in the 'art_spiker_recipe' object + def connections_on(self, gid): + # incoming to neurons 0--2 + if gid < 3: + return [] + + # incoming to neuron 3 + elif gid == 3: + source_label_0 = arb.cell_global_label(0, "spike_source") # referring to the "spike_source" label of neuron 0 + source_label_1 = arb.cell_global_label(1, "spike_source") # referring to the "spike_source" label of neuron 1 + + target_label_rr_halt = arb.cell_local_label("postsyn_target", arb.selection_policy.round_robin_halt) # referring to the current item in the "postsyn_target" label group of neuron 3 + target_label_rr = arb.cell_local_label("postsyn_target", arb.selection_policy.round_robin) # referring to the current item in the "postsyn_target" label group of neuron 3, moving to the next item afterwards + + conn_0_3_n1 = arb.connection(source_label_0, target_label_rr_halt, weight, 0.2) # first connection from neuron 0 to 3 + conn_0_3_n2 = arb.connection(source_label_0, target_label_rr, weight, 0.2) # second connection from neuron 0 to 3 + conn_1_3_n1 = arb.connection(source_label_1, target_label_rr_halt, weight2, 1.4) # first connection from neuron 1 to 3 + conn_1_3_n2 = arb.connection(source_label_1, target_label_rr, weight2, 1.4) # second connection from neuron 1 to 3 + + return [conn_0_3_n1, conn_0_3_n2, conn_1_3_n1, conn_1_3_n2] + art_spiker_recipe.connections_on = types.MethodType(connections_on, art_spiker_recipe) + + # run the main part of this test + sim, handle_mem = self.rr_main(context, art_spiker_recipe, weight, weight2) + + # evaluate the outcome + self.evaluate_outcome(sim, handle_mem) + self.evaluate_additional_outcome_2_3(sim, handle_mem) + + # Test #3 (for 'univalent') + @fixtures.context + @fixtures.art_spiker_recipe + @fixtures.sum_weight_hh_spike + @fixtures.sum_weight_hh_spike_2 + def test_multiple_connections_uni(self, context, art_spiker_recipe, sum_weight_hh_spike, sum_weight_hh_spike_2): + weight = sum_weight_hh_spike # connection strength which is just enough to evoke an immediate spike at t=1ms (equaling the sum of two connections in Test #2) + weight2 = 0.97*sum_weight_hh_spike_2 # connection strength which is just NOT enough to evoke an immediate spike at t=1.8ms (equaling the sum of two connections in Test #2) + + # define new method 'connections_on()' and overwrite the original one in the 'art_spiker_recipe' object + def connections_on(self, gid): + # incoming to neurons 0--2 + if gid < 3: + return [] + + # incoming to neuron 3 + elif gid == 3: + source_label_0 = arb.cell_global_label(0, "spike_source") # referring to the "spike_source" label of neuron 0 + source_label_1 = arb.cell_global_label(1, "spike_source") # referring to the "spike_source" label of neuron 1 + + target_label_uni_n1 = arb.cell_local_label("postsyn_target_1", arb.selection_policy.univalent) # referring to an only item in the "postsyn_target_1" label group of neuron 3 + target_label_uni_n2 = arb.cell_local_label("postsyn_target_2", arb.selection_policy.univalent) # referring to an only item in the "postsyn_target_2" label group of neuron 3 + + conn_0_3 = arb.connection(source_label_0, target_label_uni_n1, weight, 0.2) # connection from neuron 0 to 3 + conn_1_3 = arb.connection(source_label_1, target_label_uni_n2, weight2, 1.4) # connection from neuron 1 to 3 + + return [conn_0_3, conn_1_3] + art_spiker_recipe.connections_on = types.MethodType(connections_on, art_spiker_recipe) + + # define new method 'cell_description()' and overwrite the original one in the 'art_spiker_recipe' object + create_syn_mechanism = self.create_syn_mechanism + def cell_description(self, gid): + # spike source neuron + if gid < 3: + return arb.spike_source_cell("spike_source", arb.explicit_schedule(self.trains[gid])) + + # spike-receiving cable neuron + elif gid == 3: + tree, labels, decor = self._cable_cell_elements() + + decor.place('"midpoint"', arb.synapse(create_syn_mechanism()), "postsyn_target_1") # place synapse for input from one presynaptic neuron at the center of the soma + decor.place('"midpoint"', arb.synapse(create_syn_mechanism()), "postsyn_target_2") # place synapse for input from another presynaptic neuron at the center of the soma + # (using another label as above!) + + return arb.cable_cell(tree, labels, decor) + art_spiker_recipe.cell_description = types.MethodType(cell_description, art_spiker_recipe) + + # read connections from recipe for testing + connections_from_recipe = art_spiker_recipe.connections_on(3) + + # connection from neuron 0 to 3 + self.assertEqual(connections_from_recipe[0].dest.label, "postsyn_target_1") + self.assertAlmostEqual(connections_from_recipe[0].weight, weight) + self.assertAlmostEqual(connections_from_recipe[0].delay, 0.2) + + # connection from neuron 1 to 3 + self.assertEqual(connections_from_recipe[1].dest.label, "postsyn_target_2") + self.assertAlmostEqual(connections_from_recipe[1].weight, weight2) + self.assertAlmostEqual(connections_from_recipe[1].delay, 1.4) + + # construct domain_decomposition and simulation object + dd = arb.partition_load_balance(art_spiker_recipe, context) + sim = arb.simulation(art_spiker_recipe, dd, context) + sim.record(arb.spike_recording.all) + + # create schedule and handle to record the membrane potential of neuron 3 + reg_sched = arb.regular_schedule(0, self.dt, self.runtime) + handle_mem = sim.sample((3, 0), reg_sched) + + # run the simulation + sim.run(self.runtime, self.dt) + + # evaluate the outcome + self.evaluate_outcome(sim, handle_mem) + self.evaluate_additional_outcome_2_3(sim, handle_mem) +