diff --git a/.gitignore b/.gitignore index 036ba49182d024c4de5daf45f4026bf7184de43d..4b1dccf5bf06dd3dfcdfda2c86255f5167315b43 100644 --- a/.gitignore +++ b/.gitignore @@ -66,7 +66,7 @@ mechanisms/gpu/*.hpp mechanisms/gpu/*.cu # build path -build* +build/* commit.msg @@ -89,3 +89,11 @@ dist # generated image files by Python examples python/example/*.svg python/example/*.csv + +# made by direnv +.direnv +.envrc + +# build/compile caches +_skbuild +.ccls-cache diff --git a/arbor/arbexcept.cpp b/arbor/arbexcept.cpp index 74dca037f3b2e655bf65c2517c0ee4a2e81c65fd..21ccedc18d8c3421701fe56b42bf656a118ce1fb 100644 --- a/arbor/arbexcept.cpp +++ b/arbor/arbexcept.cpp @@ -37,6 +37,11 @@ bad_cell_description::bad_cell_description(cell_kind kind, cell_gid_type gid): kind(kind) {} +invalid_mechanism_kind::invalid_mechanism_kind(arb_mechanism_kind kind): + arbor_exception(pprintf("Invalid mechanism kind: {})", kind)), + kind(kind) +{} + bad_connection_source_gid::bad_connection_source_gid(cell_gid_type gid, cell_gid_type src_gid, cell_size_type num_cells): arbor_exception(pprintf("Model building error on cell {}: connection source gid {} is out of range: there are only {} cells in the model, in the range [{}:{}].", gid, src_gid, num_cells, 0, num_cells-1)), gid(gid), src_gid(src_gid), num_cells(num_cells) diff --git a/arbor/backends/gpu/shared_state.cpp b/arbor/backends/gpu/shared_state.cpp index d6c66065a54b7064f346b6c366a6194858e83d4f..ab6ae51665e8e0dd8140a1cffde8b2d2bf212fe1 100644 --- a/arbor/backends/gpu/shared_state.cpp +++ b/arbor/backends/gpu/shared_state.cpp @@ -216,24 +216,6 @@ shared_state::shared_state( add_scalar(temperature_degC.size(), temperature_degC.data(), -273.15); } -void shared_state::set_parameter(mechanism& m, const std::string& key, const std::vector<arb_value_type>& values) { - if (values.size()!=m.ppack_.width) throw arbor_internal_error("mechanism parameter size mismatch"); - const auto& store = storage.at(m.mechanism_id()); - - arb_value_type* data = nullptr; - for (arb_size_type i = 0; i<m.mech_.n_parameters; ++i) { - if (key==m.mech_.parameters[i].name) { - data = store.parameters_[i]; - break; - } - } - - if (!data) throw arbor_internal_error(util::pprintf("no such mechanism parameter '{}'", key)); - - if (!m.ppack_.width) return; - memory::copy(memory::make_const_view(values), memory::device_view<arb_value_type>(data, m.ppack_.width)); -} - void shared_state::update_prng_state(mechanism& m) { if (!m.mech_.n_random_variables) return; auto const mech_id = m.mechanism_id(); @@ -252,7 +234,11 @@ const arb_value_type* shared_state::mechanism_state_data(const mechanism& m, con return nullptr; } -void shared_state::instantiate(mechanism& m, unsigned id, const mechanism_overrides& overrides, const mechanism_layout& pos_data) { +void shared_state::instantiate(mechanism& m, + unsigned id, + const mechanism_overrides& overrides, + const mechanism_layout& pos_data, + const std::vector<std::pair<std::string, std::vector<arb_value_type>>>& params) { assert(m.iface_.backend == arb_backend_kind_gpu); using util::make_range; using util::make_span; @@ -313,19 +299,28 @@ void shared_state::instantiate(mechanism& m, unsigned id, const mechanism_overri { // Allocate bulk storage std::size_t count = (m.mech_.n_state_vars + m.mech_.n_parameters + 1)*width_padded + m.mech_.n_globals; - store.data_ = array(count, NAN); + store.data_ = array(count, NAN); chunk_writer writer(store.data_.data(), width_padded); // First sub-array of data_ is used for weight_ m.ppack_.weight = writer.append_with_padding(pos_data.weight, 0); - // Set fields + // Set parameters to either default or explicit setting for (auto idx: make_span(m.mech_.n_parameters)) { - store.parameters_[idx] = writer.fill(m.mech_.parameters[idx].default_value); + const auto& param = m.mech_.parameters[idx]; + const auto& it = std::find_if(params.begin(), params.end(), + [&](const auto& k) { return k.first == param.name; }); + if (it != params.end()) { + if (it->second.size() != width) throw arbor_internal_error("mechanism field size mismatch"); + store.parameters_[idx] = writer.append_with_padding(it->second, param.default_value); + } + else { + store.parameters_[idx] = writer.fill(param.default_value); + } } + // Make STATE var the default for (auto idx: make_span(m.mech_.n_state_vars)) { store.state_vars_[idx] = writer.fill(m.mech_.state_vars[idx].default_value); } - // Assign global scalar parameters. NB: Last chunk, since it breaks the width striding. for (auto idx: make_span(m.mech_.n_globals)) store.globals_[idx] = m.mech_.globals[idx].default_value; for (auto& [k, v]: overrides.globals) { diff --git a/arbor/backends/gpu/shared_state.hpp b/arbor/backends/gpu/shared_state.hpp index c09cdd12989275aaf53659ee8652d0f955919411..e08de3aece8b56e5c959f548f03c1aac885bb496 100644 --- a/arbor/backends/gpu/shared_state.hpp +++ b/arbor/backends/gpu/shared_state.hpp @@ -177,9 +177,11 @@ struct ARB_ARBOR_API shared_state { ); // Setup a mechanism and tie its backing store to this object - void instantiate(arb::mechanism&, unsigned, const mechanism_overrides&, const mechanism_layout&); - - void set_parameter(mechanism&, const std::string&, const std::vector<arb_value_type>&); + void instantiate(mechanism&, + unsigned, + const mechanism_overrides&, + const mechanism_layout&, + const std::vector<std::pair<std::string, std::vector<arb_value_type>>>&); void update_prng_state(mechanism&); diff --git a/arbor/backends/multicore/shared_state.cpp b/arbor/backends/multicore/shared_state.cpp index e7c2e8f8effcae528331ed5620cb137837c191d4..1ae389365cbe006603f225a9daf7e29a8f9c90bb 100644 --- a/arbor/backends/multicore/shared_state.cpp +++ b/arbor/backends/multicore/shared_state.cpp @@ -437,27 +437,6 @@ std::size_t extend_width(const arb::mechanism& mech, std::size_t width) { } } // anonymous namespace -void shared_state::set_parameter(mechanism& m, const std::string& key, const std::vector<arb_value_type>& values) { - if (values.size()!=m.ppack_.width) throw arbor_internal_error("mechanism field size mismatch"); - - bool found = false; - arb_value_type* data = nullptr; - for (arb_size_type i = 0; i<m.mech_.n_parameters; ++i) { - if (key==m.mech_.parameters[i].name) { - data = m.ppack_.parameters[i]; - found = true; - break; - } - } - - if (!found) throw arbor_internal_error(util::pprintf("no such parameter '{}'", key)); - - if (!m.ppack_.width) return; - - auto width_padded = extend_width<arb_value_type>(m, m.ppack_.width); - copy_extend(values, util::range_n(data, width_padded), values.back()); -} - const arb_value_type* shared_state::mechanism_state_data(const mechanism& m, const std::string& key) { for (arb_size_type i = 0; i<m.mech_.n_state_vars; ++i) { if (key==m.mech_.state_vars[i].name) { @@ -508,7 +487,11 @@ void shared_state::update_prng_state(mechanism& m) { // * For indices in the padded tail of node_index_, set index to last valid CV index. // * For indices in the padded tail of ion index maps, set index to last valid ion index. -void shared_state::instantiate(arb::mechanism& m, unsigned id, const mechanism_overrides& overrides, const mechanism_layout& pos_data) { +void shared_state::instantiate(arb::mechanism& m, + unsigned id, + const mechanism_overrides& overrides, + const mechanism_layout& pos_data, + const std::vector<std::pair<std::string, std::vector<arb_value_type>>>& params) { // Mechanism indices and data require: // * an alignment that is a multiple of the mechansim data_alignment(); // * a size which is a multiple of partition_width() for SIMD access. @@ -518,9 +501,14 @@ void shared_state::instantiate(arb::mechanism& m, unsigned id, const mechanism_o util::padded_allocator<> pad(m.data_alignment()); + if (storage.find(id) != storage.end()) { + throw arbor_internal_error("Duplicate mechanism id in MC shared state."); + } + auto& store = storage[id]; + auto width = pos_data.cv.size(); // Assign non-owning views onto shared state: m.ppack_ = {0}; - m.ppack_.width = pos_data.cv.size(); + m.ppack_.width = width; m.ppack_.mechanism_id = id; m.ppack_.vec_ci = cv_to_cell.data(); m.ppack_.vec_di = cv_to_intdom.data(); @@ -537,9 +525,6 @@ void shared_state::instantiate(arb::mechanism& m, unsigned id, const mechanism_o bool mult_in_place = !pos_data.multiplicity.empty(); bool peer_indices = !pos_data.peer_cv.empty(); - if (storage.find(id) != storage.end()) throw arb::arbor_internal_error("Duplicate mech id in shared state"); - auto& store = storage[id]; - // store indices for random number generation store.gid_ = pos_data.gid; store.idx_ = pos_data.idx; @@ -585,10 +570,20 @@ void shared_state::instantiate(arb::mechanism& m, unsigned id, const mechanism_o // First sub-array of data_ is used for weight_ m.ppack_.weight = writer.append(pos_data.weight, 0); - // Set fields + // Set parameters: either default, or explicit override for (auto idx: make_span(m.mech_.n_parameters)) { - m.ppack_.parameters[idx] = writer.fill(m.mech_.parameters[idx].default_value); + const auto& param = m.mech_.parameters[idx]; + const auto& it = std::find_if(params.begin(), params.end(), + [&](const auto& k) { return k.first == param.name; }); + if (it != params.end()) { + if (it->second.size() != width) throw arbor_internal_error("mechanism field size mismatch"); + m.ppack_.parameters[idx] = writer.append(it->second, param.default_value); + } + else { + m.ppack_.parameters[idx] = writer.fill(param.default_value); + } } + // Set initial state values for (auto idx: make_span(m.mech_.n_state_vars)) { m.ppack_.state_vars[idx] = writer.fill(m.mech_.state_vars[idx].default_value); } diff --git a/arbor/backends/multicore/shared_state.hpp b/arbor/backends/multicore/shared_state.hpp index 7d306ce7cf68ab36190fdd888b589129df0a127b..dda56f51eef8381e60d3849c40a27d945415963a 100644 --- a/arbor/backends/multicore/shared_state.hpp +++ b/arbor/backends/multicore/shared_state.hpp @@ -185,9 +185,11 @@ struct ARB_ARBOR_API shared_state { arb_seed_type cbprng_seed_ = 0u ); - void instantiate(mechanism&, unsigned, const mechanism_overrides&, const mechanism_layout&); - - void set_parameter(mechanism&, const std::string&, const std::vector<arb_value_type>&); + void instantiate(mechanism&, + unsigned, + const mechanism_overrides&, + const mechanism_layout&, + const std::vector<std::pair<std::string, std::vector<arb_value_type>>>&); void update_prng_state(mechanism&); diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp index 8b7d03823f13e0006d7ec6ade1682f8612efc24a..34c4f39b77e9d896064f6a3fb01778a005d719cb 100644 --- a/arbor/cable_cell.cpp +++ b/arbor/cable_cell.cpp @@ -55,6 +55,9 @@ std::string show(const paintable& item) { else if constexpr (std::is_same_v<density, T>) { os << "density:" << p.mech.name(); } + else if constexpr (std::is_same_v<voltage_process, T>) { + os << "voltage-process:" << p.mech.name(); + } }, item); return os.str(); @@ -136,6 +139,10 @@ struct cable_cell_impl { return region_map.get<T>(); } + mcable_map<voltage_process>& get_region_map(const voltage_process& v) { + return region_map.get<voltage_process>()[v.mech.name()]; + } + mcable_map<std::pair<density, iexpr_map>> & get_region_map(const density &desc) { return region_map.get<density>()[desc.mech.name()]; diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp index 94a8e1071f2e8c4f3eacd5089975aea4a3d2fb35..7862419dcce192a8dc31763abf61a979361c23cc 100644 --- a/arbor/fvm_layout.cpp +++ b/arbor/fvm_layout.cpp @@ -81,12 +81,13 @@ std::vector<V> unique_union(const std::vector<V>& a, const std::vector<V>& b) { } return u; } + } // anonymous namespace // Building CV geometry // -------------------- - +// // CV geometry cv_geometry::cv_geometry(const cable_cell& cell, const locset& ls): @@ -794,6 +795,16 @@ ARB_ARBOR_API std::unordered_map<cell_gid_type, std::vector<fvm_gap_junction>> f return gj_conns; } +std::unordered_map<std::string, fvm_mechanism_config> +make_voltage_mechanism_config(const cable_cell_global_properties& gprop, + const region_assignment<voltage_process> assignments, + const mechanism_catalogue& catalogue, + iexpr_ptr unit_scale, + unsigned cell_idx, + const fvm_cv_discretization& D, + const concrete_embedding& embedding, + const mprovider& provider); + fvm_mechanism_data fvm_build_mechanism_data( const cable_cell_global_properties& gprop, const cable_cell& cell, @@ -821,6 +832,47 @@ ARB_ARBOR_API fvm_mechanism_data fvm_build_mechanism_data( return combined; } + +// Verify mechanism ion usage, parameter values. +void verify_mechanism(const cable_cell_global_properties& gprop, + const fvm_cv_discretization& D, + const mechanism_info& info, + const mechanism_desc& desc) { + const auto& global_ions = gprop.ion_species; + + for (const auto& pv: desc.values()) { + if (!info.parameters.count(pv.first)) { + throw no_such_parameter(desc.name(), pv.first); + } + if (!info.parameters.at(pv.first).valid(pv.second)) { + throw invalid_parameter_value(desc.name(), pv.first, pv.second); + } + } + + for (const auto& [ion, dep]: info.ions) { + if (!global_ions.count(ion)) { + throw cable_cell_error( + "mechanism "+desc.name()+" uses ion "+ion+ " which is missing in global properties"); + } + + if (dep.verify_ion_charge) { + if (dep.expected_ion_charge!=global_ions.at(ion)) { + throw cable_cell_error( + "mechanism "+desc.name()+" uses ion "+ion+ " expecting a different valence"); + } + } + + if (dep.write_reversal_potential && (dep.write_concentration_int || dep.write_concentration_ext)) { + throw cable_cell_error("mechanism "+desc.name()+" writes both reversal potential and concentration"); + } + + auto is_diffusive = D.diffusive_ions.count(ion); + if (dep.access_concentration_diff && !is_diffusive) { + throw illegal_diffusive_mechanism(desc.name(), ion); + } + } +} + // Construct FVM mechanism data for a single cell. fvm_mechanism_data fvm_build_mechanism_data( @@ -834,8 +886,9 @@ fvm_mechanism_data fvm_build_mechanism_data( using index_type = arb_index_type; using value_type = arb_value_type; - const mechanism_catalogue& catalogue = gprop.catalogue; + const auto& catalogue = gprop.catalogue; const auto& embedding = cell.embedding(); + const auto& provider = cell.provider(); const auto& global_dflt = gprop.default_parameters; const auto& dflt = cell.default_parameters(); @@ -847,43 +900,6 @@ fvm_mechanism_data fvm_build_mechanism_data( const auto& assignments = cell.region_assignments(); - // Verify mechanism ion usage, parameter values. - auto verify_mechanism = [&gprop, &D](const mechanism_info& info, const mechanism_desc& desc) { - const auto& global_ions = gprop.ion_species; - - for (const auto& pv: desc.values()) { - if (!info.parameters.count(pv.first)) { - throw no_such_parameter(desc.name(), pv.first); - } - if (!info.parameters.at(pv.first).valid(pv.second)) { - throw invalid_parameter_value(desc.name(), pv.first, pv.second); - } - } - - for (const auto& [ion, dep]: info.ions) { - if (!global_ions.count(ion)) { - throw cable_cell_error( - "mechanism "+desc.name()+" uses ion "+ion+ " which is missing in global properties"); - } - - if (dep.verify_ion_charge) { - if (dep.expected_ion_charge!=global_ions.at(ion)) { - throw cable_cell_error( - "mechanism "+desc.name()+" uses ion "+ion+ " expecting a different valence"); - } - } - - if (dep.write_reversal_potential && (dep.write_concentration_int || dep.write_concentration_ext)) { - throw cable_cell_error("mechanism "+desc.name()+" writes both reversal potential and concentration"); - } - - auto is_diffusive = D.diffusive_ions.count(ion); - if (dep.access_concentration_diff && !is_diffusive) { - throw illegal_diffusive_mechanism(desc.name(), ion); - } - } - }; - // Track ion usage of mechanisms so that ions are only instantiated where required. std::unordered_map<std::string, std::vector<index_type>> ion_support; @@ -905,8 +921,22 @@ fvm_mechanism_data fvm_build_mechanism_data( std::unordered_map<std::string, mcable_map<double>> init_iconc_mask; std::unordered_map<std::string, mcable_map<double>> init_econc_mask; - iexpr_ptr unit_scale = thingify(iexpr::scalar(1.0), cell.provider()); - + iexpr_ptr unit_scale = thingify(iexpr::scalar(1.0), provider); + + // Voltage mechanisms + + { + auto assigns = assignments.get<voltage_process>(); + auto configs = make_voltage_mechanism_config(gprop, + assigns, + catalogue, + unit_scale, + cell_idx, + D, + embedding, + provider); + M.mechanisms.insert(configs.begin(), configs.end()); + } // Density mechanisms: for (const auto& [name, cables]: assignments.get<density>()) { mechanism_info info = catalogue[name]; @@ -941,7 +971,7 @@ fvm_mechanism_data fvm_build_mechanism_data( const auto& mech = density_iexpr.first.mech; const auto& scale_expr = density_iexpr.second; - verify_mechanism(info, mech); + verify_mechanism(gprop, D, info, mech); const auto& set_params = mech.values(); support.insert(cable, 1.); @@ -969,7 +999,7 @@ fvm_mechanism_data fvm_build_mechanism_data( c.branch, pw_over_cable( param_maps[i], c, 0., [&](const mcable &c, const auto& x) { - return x.first * x.second->eval(cell.provider(), c); + return x.first * x.second->eval(provider, c); })); } } @@ -1061,7 +1091,7 @@ fvm_mechanism_data fvm_build_mechanism_data( std::size_t offset = 0; for (const placed<synapse>& pm: entry.second) { const auto& mech = pm.item.mech; - verify_mechanism(info, mech); + verify_mechanism(gprop, D, info, mech); synapse_instance in{}; @@ -1210,7 +1240,7 @@ fvm_mechanism_data fvm_build_mechanism_data( for (const placed<junction>& pm: placements) { const auto& mech = pm.item.mech; - verify_mechanism(info, mech); + verify_mechanism(gprop, D, info, mech); const auto& set_params = mech.values(); junction_desc per_lid; @@ -1369,12 +1399,12 @@ fvm_mechanism_data fvm_build_mechanism_data( } if (auto di = D.diffusive_ions.find(ion); di != D.diffusive_ions.end()) { - config.is_diffusive = true; - config.face_diffusivity = di->second.face_diffusivity; + config.is_diffusive = true; + config.face_diffusivity = di->second.face_diffusivity; } - config.econc_written = write_xo.count(ion); - config.iconc_written = write_xi.count(ion); + config.econc_written = write_xo.count(ion); + config.iconc_written = write_xi.count(ion); if (!config.cv.empty()) M.ions[ion] = std::move(config); } @@ -1391,7 +1421,7 @@ fvm_mechanism_data fvm_build_mechanism_data( throw cable_cell_error("expected reversal potential mechanism for ion " +ion +", got "+ revpot.name() +" which has " +arb_mechanism_kind_str(info.kind)); } - verify_mechanism(info, revpot); + verify_mechanism(gprop, D, info, revpot); revpot_specified.insert(ion); bool writes_this_revpot = false; @@ -1464,4 +1494,100 @@ fvm_mechanism_data fvm_build_mechanism_data( return M; } +std::unordered_map<std::string, fvm_mechanism_config> +make_voltage_mechanism_config(const cable_cell_global_properties& gprop, + const region_assignment<voltage_process> assignments, + const mechanism_catalogue& catalogue, + iexpr_ptr unit_scale, + unsigned cell_idx, + const fvm_cv_discretization& D, + const concrete_embedding& embedding, + const mprovider& provider) { + std::unordered_map<std::string, fvm_mechanism_config> result; + std::unordered_set<mcable> voltage_support; + for (const auto& [name, cables]: assignments) { + auto info = catalogue[name]; + if (info.kind != arb_mechanism_kind_voltage) { + throw cable_cell_error("expected voltage mechanism, got " +name +" which has " +arb_mechanism_kind_str(info.kind)); + } + + fvm_mechanism_config config; + config.kind = arb_mechanism_kind_voltage; + + std::vector<std::string> param_names; + assign(param_names, util::keys(info.parameters)); + sort(param_names); + + std::vector<double> param_dflt; + std::size_t n_param = param_names.size(); + param_dflt.reserve(n_param); + config.param_values.reserve(n_param); + + for (std::size_t i = 0; i<n_param; ++i) { + const auto& p = param_names[i]; + config.param_values.emplace_back(p, std::vector<arb_value_type>{}); + param_dflt.push_back(info.parameters.at(p).default_value); + } + + mcable_map<double> support; + std::vector<mcable_map<std::pair<double, iexpr_ptr>>> param_maps; + + param_maps.resize(n_param); + + for (const auto& [cable, density_iexpr]: cables) { + const auto& mech = density_iexpr.mech; + + verify_mechanism(gprop, D, info, mech); + const auto& set_params = mech.values(); + + support.insert(cable, 1.); + for (std::size_t i = 0; i<n_param; ++i) { + double value = value_by_key(set_params, param_names[i]).value_or(param_dflt[i]); + param_maps[i].insert(cable, {value, unit_scale}); + } + } + + std::vector<double> param_on_cv(n_param); + + for (auto cv: D.geometry.cell_cvs(cell_idx)) { + double area = 0; + util::fill(param_on_cv, 0.); + + for (mcable c: D.geometry.cables(cv)) { + double area_on_cable = embedding.integrate_area(c.branch, pw_over_cable(support, c, 0.)); + if (!area_on_cable) continue; + area += area_on_cable; + for (std::size_t i = 0; i<n_param; ++i) { + auto map = param_maps[i]; + auto fun = [&](const auto& c, const auto& x) { + return x.first * x.second->eval(provider, c); + }; + auto pw = pw_over_cable(map, c, 0., fun); + param_on_cv[i] += embedding.integrate_area(c.branch, pw); + } + } + + if (area>0) { + config.cv.push_back(cv); + config.norm_area.push_back(area/D.cv_area[cv]); + + double oo_area = 1./area; + for (auto i: count_along(param_on_cv)) { + config.param_values[i].second.push_back(param_on_cv[i]*oo_area); + } + } + } + + for (const auto& [cable, _]: support) { + if (voltage_support.count(cable)) { + throw cable_cell_error("Multiple voltage processes on a single cable"); + } + voltage_support.insert(cable); + } + if (!config.cv.empty()) result.emplace(name, std::move(config)); + } + return result; +} + + } // namespace arb diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp index 09902f5c27701e6da3b0d6cf8fe674431763a80a..b3d09765e2c343655d1eb7ba32ff237bd12c856a 100644 --- a/arbor/fvm_lowered_cell_impl.hpp +++ b/arbor/fvm_lowered_cell_impl.hpp @@ -98,6 +98,7 @@ private: value_type tmin_ = 0; std::vector<mechanism_ptr> mechanisms_; // excludes reversal potential calculators. std::vector<mechanism_ptr> revpot_mechanisms_; + std::vector<mechanism_ptr> voltage_mechanisms_; // Optional non-physical voltage check threshold std::optional<double> check_voltage_mV_; @@ -166,6 +167,10 @@ void fvm_lowered_cell_impl<Backend>::reset() { state_->reset(); set_tmin(0); + for (auto& m: voltage_mechanisms_) { + m->initialize(); + } + for (auto& m: revpot_mechanisms_) { m->initialize(); } @@ -188,6 +193,10 @@ void fvm_lowered_cell_impl<Backend>::reset() { m->initialize(); } + for (auto& m: voltage_mechanisms_) { + m->initialize(); + } + // NOTE: Threshold watcher reset must come after the voltage values are set, // as voltage is implicitly read by watcher to set initial state. threshold_watcher_.reset(state_->voltage); @@ -286,21 +295,28 @@ fvm_integration_result fvm_lowered_cell_impl<Backend>::integrate( state_->integrate_diffusion(); PL(); - // Integrate mechanism state. - + // Integrate mechanism state for density for (auto& m: mechanisms_) { state_->update_prng_state(*m); m->update_state(); } // Update ion concentrations. - PE(advance:integrate:ionupdate); update_ion_state(); PL(); - // Update time and test for spike threshold crossings. + // voltage mechs run now; after the cable_solver, but before the + // threshold test + for (auto& m: voltage_mechanisms_) { + m->update_current(); + } + for (auto& m: voltage_mechanisms_) { + state_->update_prng_state(*m); + m->update_state(); + } + // Update time and test for spike threshold crossings. PE(advance:integrate:threshold); threshold_watcher_.test(&state_->time_since_spike); PL(); @@ -545,10 +561,7 @@ fvm_initialization_data fvm_lowered_cell_impl<Backend>::initialize( std::unordered_map<std::string, mechanism*> mechptr_by_name; unsigned mech_id = 0; - for (auto& m: mech_data.mechanisms) { - auto& name = m.first; - auto& config = m.second; - + for (const auto& [name, config]: mech_data.mechanisms) { mechanism_layout layout; layout.cv = config.cv; layout.multiplicity = config.multiplicity; @@ -599,6 +612,7 @@ fvm_initialization_data fvm_lowered_cell_impl<Backend>::initialize( layout.weight[i] = config.local_weight[i] * 1000/D.cv_area[cv]; } break; + case arb_mechanism_kind_voltage: case arb_mechanism_kind_density: // Current density contributions from mechanism are already in [A/m²]. @@ -617,19 +631,28 @@ fvm_initialization_data fvm_lowered_cell_impl<Backend>::initialize( break; } - auto minst = mech_instance(name); - state_->instantiate(*minst.mech, mech_id++, minst.overrides, layout); - mechptr_by_name[name] = minst.mech.get(); - - for (auto& pv: config.param_values) { - state_->set_parameter(*minst.mech, pv.first, pv.second); - } + auto [mech, over] = mech_instance(name); + state_->instantiate(*mech, mech_id, over, layout, config.param_values); + mechptr_by_name[name] = mech.get(); + ++mech_id; - if (config.kind==arb_mechanism_kind_reversal_potential) { - revpot_mechanisms_.emplace_back(minst.mech.release()); - } - else { - mechanisms_.emplace_back(minst.mech.release()); + switch (config.kind) { + case arb_mechanism_kind_gap_junction: + case arb_mechanism_kind_point: + case arb_mechanism_kind_density: { + mechanisms_.emplace_back(mech.release()); + break; + } + case arb_mechanism_kind_reversal_potential: { + revpot_mechanisms_.emplace_back(mech.release()); + break; + } + case arb_mechanism_kind_voltage: { + voltage_mechanisms_.emplace_back(mech.release()); + break; + } + default:; + throw invalid_mechanism_kind(config.kind); } } diff --git a/arbor/include/arbor/arbexcept.hpp b/arbor/include/arbor/arbexcept.hpp index deb578e43cbb7b5db61b95877488acae4864cffb..efa1255d0a6e95abb2595629b715a1e02776d42b 100644 --- a/arbor/include/arbor/arbexcept.hpp +++ b/arbor/include/arbor/arbexcept.hpp @@ -6,6 +6,7 @@ #include <arbor/common_types.hpp> #include <arbor/export.hpp> +#include <arbor/mechanism_abi.h> // Arbor-specific exception hierarchy. @@ -41,6 +42,11 @@ struct ARB_SYMBOL_VISIBLE bad_cell_probe: arbor_exception { cell_kind kind; }; +struct ARB_SYMBOL_VISIBLE invalid_mechanism_kind: arbor_exception { + invalid_mechanism_kind(arb_mechanism_kind); + arb_mechanism_kind kind; +}; + struct ARB_SYMBOL_VISIBLE bad_cell_description: arbor_exception { bad_cell_description(cell_kind kind, cell_gid_type gid); cell_gid_type gid; diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp index cb0570bc1a05cf2f728eba8b8dbda29cc9986dc7..221c967d149d8bd1b15067d0f34acd16e920e90f 100644 --- a/arbor/include/arbor/cable_cell.hpp +++ b/arbor/include/arbor/cable_cell.hpp @@ -213,7 +213,7 @@ using region_assignment = std::conditional_t< std::conditional_t< std::is_same<T, init_int_concentration>::value || std::is_same<T, init_ext_concentration>::value || std::is_same<T, init_reversal_potential>::value || - std::is_same<T, ion_diffusivity>::value, + std::is_same<T, ion_diffusivity>::value || std::is_same<T, voltage_process>::value, std::unordered_map<std::string, mcable_map<T>>, std::conditional_t<std::is_same_v<T, density>, std::unordered_map<std::string, mcable_map<std::pair<T, iexpr_map>>>, @@ -238,7 +238,7 @@ using location_assignment = mlocation_map<T>>; using cable_cell_region_map = static_typed_map<region_assignment, - density, init_membrane_potential, axial_resistivity, + density, voltage_process, init_membrane_potential, axial_resistivity, temperature_K, membrane_capacitance, init_int_concentration, ion_diffusivity, init_ext_concentration, init_reversal_potential>; diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp index b4307d0a843b040d4e6abac56b4c752c149cfd95..42748a57a3f4e35231431ebd2a006e0decff2013 100644 --- a/arbor/include/arbor/cable_cell_param.hpp +++ b/arbor/include/arbor/cable_cell_param.hpp @@ -227,6 +227,16 @@ struct ARB_SYMBOL_VISIBLE density { } }; +struct ARB_SYMBOL_VISIBLE voltage_process { + mechanism_desc mech; + explicit voltage_process(mechanism_desc m): mech(std::move(m)) {} + voltage_process(mechanism_desc m, const std::unordered_map<std::string, double>& params): mech(std::move(m)) { + for (const auto& [param, value]: params) { + mech.set(param, value); + } + } +}; + struct ARB_SYMBOL_VISIBLE ion_reversal_potential_method { std::string ion; mechanism_desc method; @@ -255,6 +265,7 @@ using paintable = init_ext_concentration, init_reversal_potential, density, + voltage_process, scaled_mechanism<density>>; using placeable = diff --git a/arbor/include/arbor/lif_cell.hpp b/arbor/include/arbor/lif_cell.hpp index 17ab8d8eb4067492711d9a74e0e98816d2a7ffbb..52fb6eff4e116b67b86165b38f9e6a72dd5c2a06 100644 --- a/arbor/include/arbor/lif_cell.hpp +++ b/arbor/include/arbor/lif_cell.hpp @@ -20,7 +20,7 @@ struct ARB_SYMBOL_VISIBLE lif_cell { double t_ref = 2; // Refractory period [ms]. lif_cell() = delete; - lif_cell(cell_tag_type source, cell_tag_type target): source(std::move(source)), target(std::move(target)) {} + lif_cell(cell_tag_type source, cell_tag_type target): source(std::move(source)), target(std::move(target)) {} }; // LIF probe metadata, to be passed to sampler callbacks. Intentionally left blank. diff --git a/arbor/include/arbor/mechanism_abi.h b/arbor/include/arbor/mechanism_abi.h index a5050ed84c32994c7c2d57a83c403abe9a4df703..bebe1aaad38b51bca727bf4aaa23a828b9ae4d01 100644 --- a/arbor/include/arbor/mechanism_abi.h +++ b/arbor/include/arbor/mechanism_abi.h @@ -18,8 +18,6 @@ extern "C" { #define ARB_NO_ALIAS restrict #endif - - // Version #define ARB_MECH_ABI_VERSION_MAJOR 0 #define ARB_MECH_ABI_VERSION_MINOR 3 @@ -35,6 +33,7 @@ typedef uint32_t arb_mechanism_kind; #define arb_mechanism_kind_density 2 #define arb_mechanism_kind_reversal_potential 3 #define arb_mechanism_kind_gap_junction 4 +#define arb_mechanism_kind_voltage 5 typedef uint32_t arb_backend_kind; #define arb_backend_kind_nil 0 @@ -46,6 +45,8 @@ inline const char* arb_mechanism_kind_str(const arb_mechanism_kind& mech) { case arb_mechanism_kind_density: return "density mechanism kind"; case arb_mechanism_kind_point: return "point mechanism kind"; case arb_mechanism_kind_reversal_potential: return "reversal potential mechanism kind"; + case arb_mechanism_kind_gap_junction: return "gap junction mechanism kind"; + case arb_mechanism_kind_voltage: return "voltage mechanism kind"; default: return "unknown mechanism kind"; } } diff --git a/arbor/include/arbor/simd/simd.hpp b/arbor/include/arbor/simd/simd.hpp index 966c472f2e50e734c43e9ae3278a724c43168331..3c3ba9e7f95321b06d768530d6e10abf415e3a25 100644 --- a/arbor/include/arbor/simd/simd.hpp +++ b/arbor/include/arbor/simd/simd.hpp @@ -206,6 +206,12 @@ namespace detail { return *this; } + template <typename Other> + indirect_indexed_expression& operator*=(const Other& s) { + compound_indexed_mul(s, p, index, width, constraint); + return *this; + } + template <typename Other> indirect_indexed_expression& operator-=(const Other& s) { compound_indexed_add(neg(s), p, index, width, constraint); diff --git a/arborio/cableio.cpp b/arborio/cableio.cpp index 8be039c645c83c64183d3b945069b75099cd1570..0afbad2317cf5bde241a96c04a6bba3cf9217372 100644 --- a/arborio/cableio.cpp +++ b/arborio/cableio.cpp @@ -90,6 +90,9 @@ s_expr mksexp(const synapse& j) { s_expr mksexp(const density& j) { return slist("density"_symbol, mksexp(j.mech)); } +s_expr mksexp(const voltage_process& j) { + return slist("voltage-process"_symbol, mksexp(j.mech)); +} s_expr mksexp(const iexpr& j) { std::stringstream s; s << j; @@ -674,6 +677,7 @@ eval_map named_evals{ {"junction", make_call<arb::mechanism_desc>(make_wrapped_mechanism<junction>, "'junction' with 1 argumnet (m: mechanism)")}, {"synapse", make_call<arb::mechanism_desc>(make_wrapped_mechanism<synapse>, "'synapse' with 1 argumnet (m: mechanism)")}, {"density", make_call<arb::mechanism_desc>(make_wrapped_mechanism<density>, "'density' with 1 argumnet (m: mechanism)")}, + {"voltage-process", make_call<arb::mechanism_desc>(make_wrapped_mechanism<voltage_process>, "'voltage-process' with 1 argumnet (m: mechanism)")}, {"scaled-mechanism", make_scaled_mechanism_call<arb::density>("'scaled_mechanism' with a density argument, and 0 or more parameter scaling expressions" "(d:density (param:string val:iexpr))")}, {"place", make_call<locset, i_clamp, std::string>(make_place, "'place' with 3 arguments (ls:locset c:current-clamp name:string)")}, diff --git a/doc/fileformat/nmodl.rst b/doc/fileformat/nmodl.rst index a125cc82d8d393ce7e48993ecd5aae95224bd7b7..f8015e40f41acee4216da1a77d3d40e47ca7e920 100644 --- a/doc/fileformat/nmodl.rst +++ b/doc/fileformat/nmodl.rst @@ -159,7 +159,6 @@ Arbor-specific features of a gap-junction connection as well as the local site. The peer membrane potential is made available through the ``v_peer`` variable while the local membrane potential is available through ``v``, as usual. - * Arbor offers a number of additional unary math functions which may offer improved performance compared to hand-rolled solutions (especially with the vectorized and GPU backends). All of the following functions take a single argument `x` and return a @@ -175,6 +174,46 @@ Arbor-specific features signum(x) sign of argument :math:`\begin{align*} +1 & ~~ \text{if} ~x \gt 0, \\ -1 & ~~ \text{if} ~x \lt 0, \\ 0 & ~~ \text{otherwise}. \end{align*}` exprelr(x) guarded exponential :math:`x e^{1-x}` ================== ===================================== ========= + +Voltage Processes +----------------- + +Some cases require direct manipulation of the membrane voltage ``v``; which is +normally prohibited and for good reason so. For these limited application, +however, we offer mechanisms that are similar to ``density`` mechanism, but are +tagged with ``VOLTAGE_PROCESS`` where normally ``SUFFIX`` would be used. + +This is both a very sharp tool and a somewhat experimental feature. Depending on +our experience, it might be changed or removed. Using a ``VOLTAGE_PROCESS``, +voltage clamping and limiting can be implemented, c.f. relevant examples in the +``default`` catalogue. Example: limiting membrane voltage from above and below + +.. code:: none + + NEURON { + VOLTAGE_PROCESS v_limit + GLOBAL v_low, v_high + } + + PARAMETER { + v_high = 20 (mV) + v_low = -70 (mV) + } + + BREAKPOINT { + v = max(min(v, v_high), v_low) + } + +As of the current implementation, we note the following details and constraints + +* only the ``INITIAL`` and ``BREAKPOINT`` procedures are called. +* no ``WRITE`` access to ionic quantities is allowed. +* only one ``VOLTAGE_PROCESS`` maybe present on a single location, adding more + results in an exception. +* the ``BREAKPOINT`` callback will execute _after_ the cable solver. A + consequence of this is that if the initial membrane potential :math:`V_0` is + unequal to that of a potentially applied voltage clamp :math:`V_c`, the first + timestep will observe :math:`V_0`. .. _format-sde: diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index b341b25736cca5247a63fc58ba1d291a1a52bfb7..e97d41acab3e53459a55cca65ca466e1f325757c 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -14,4 +14,5 @@ add_subdirectory(probe-demo) add_subdirectory(plasticity) add_subdirectory(lfp) add_subdirectory(diffusion) +add_subdirectory(v_clamp) add_subdirectory(ornstein_uhlenbeck) diff --git a/example/probe-demo/probe-demo.cpp b/example/probe-demo/probe-demo.cpp index d7f21181b8fb8ac45a76676cd447ff7de4216ae0..388832d7613707293c12bcfad72bf852a123c315 100644 --- a/example/probe-demo/probe-demo.cpp +++ b/example/probe-demo/probe-demo.cpp @@ -199,14 +199,14 @@ void vector_sampler(arb::probe_metadata pm, std::size_t n, const arb::sample_rec for (unsigned j = 0; j<n_entities; ++j) { std::cout << samples[i].time << ", "; - if (cables_ptr) { - arb::mcable where = (*cables_ptr)[j]; - std::cout << where.prox_pos << ", " << where.dist_pos << ", "; - } else { - arb::mlocation loc = (*point_info_ptr)[j].loc; - std::cout << loc.pos << ", "; - } - std::cout << lo[j] << '\n'; + if (cables_ptr) { + arb::mcable where = (*cables_ptr)[j]; + std::cout << where.prox_pos << ", " << where.dist_pos << ", "; + } else { + arb::mlocation loc = (*point_info_ptr)[j].loc; + std::cout << loc.pos << ", "; + } + std::cout << lo[j] << '\n'; } } } diff --git a/example/v_clamp/CMakeLists.txt b/example/v_clamp/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..43de3438c3407bc7d14331ff52878e345751c69d --- /dev/null +++ b/example/v_clamp/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(voltage-clamp EXCLUDE_FROM_ALL v-clamp.cpp) +add_dependencies(examples voltage-clamp) +target_link_libraries(voltage-clamp PRIVATE arbor arborio arborenv arbor-sup ext-tinyopt) diff --git a/example/v_clamp/README.md b/example/v_clamp/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d1a69fb92cb864d0657906825f8995175ad720c8 --- /dev/null +++ b/example/v_clamp/README.md @@ -0,0 +1,38 @@ +# Voltage Clamp Example. + +Modified example of single neuron with morphology described by an SWC file, +a voltage clamp is added to certain regions. + +A cell is constructed from a supplied morphology with H–H channels +on the soma and passive channels on the dendrites. A simple exponential +synapse is added at the end of the last dendrite in the morphology, +and is triggered at time t = 1 ms. + +The simulation outputs a trace of the soma membrane voltage in a simple CSV +format. + +## Features + +The example demonstrates the use of: + +* Generating a morphology from an SWC file. +* Using a morphology to construct a cable cell. +* Injecting an artificial spike event into the simulation. +* Adding a voltage probe to a cell and running a sampler on the simulation. + +## Running the example + +By default, `single-cell` will simulate a 'ball-and-stick' neuron for 20 ms, +with a maxium dt of 0.025 ms and samples taken every 0.1 ms. The default +synaptic weight is 0.01 µS. + +### Command line options + +| Option | Effect | +|-----------------------|--------| +| -m, --morphology FILE | Load the morphology from FILE in SWC format | +| -d, --dt TIME | Set the maximum integration time step [ms] | +| -t, --t-end TIME | Set the simulation duration [ms] | +| -w, --weight WEIGHT | Set the synaptic weight [µS] | +| -u, --voltage VALUE | Clamp soma to voltage [mV] | + diff --git a/example/v_clamp/v-clamp.cpp b/example/v_clamp/v-clamp.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2d00a481121929fd66520668fbd59460e8adb7fb --- /dev/null +++ b/example/v_clamp/v-clamp.cpp @@ -0,0 +1,167 @@ +#include <any> +#include <fstream> +#include <iomanip> +#include <iostream> +#include <stdexcept> +#include <string> +#include <vector> + +#include <arborio/label_parse.hpp> + +#include <arbor/load_balance.hpp> +#include <arbor/cable_cell.hpp> +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/segment_tree.hpp> +#include <arbor/simulation.hpp> +#include <arbor/simple_sampler.hpp> + +#include <arborio/swcio.hpp> + +#include <tinyopt/tinyopt.h> + +using namespace arborio::literals; + +struct options { + std::string swc_file; + double t_end = 20; + double dt = 0.025; + float syn_weight = 0.01; + std::optional<double> voltage; + arb::cv_policy policy = arb::default_cv_policy(); +}; + +options parse_options(int argc, char** argv); +arb::morphology default_morphology(); +arb::morphology read_swc(const std::string& path); + +struct single_recipe: public arb::recipe { + explicit single_recipe(arb::morphology m, arb::cv_policy pol): morpho(std::move(m)) { + gprop.default_parameters = arb::neuron_parameter_defaults; + gprop.default_parameters.discretization = pol; + } + + arb::cell_size_type num_cells() const override { return 1; } + + std::vector<arb::probe_info> get_probes(arb::cell_gid_type) const override { + arb::mlocation mid_soma = {0, 0.5}; + arb::cable_probe_membrane_voltage probe = {mid_soma}; + + // Probe info consists of a probe address and an optional tag, for use + // by the sampler callback. + + return {arb::probe_info{probe, 0}}; + } + + arb::cell_kind get_cell_kind(arb::cell_gid_type) const override { + return arb::cell_kind::cable; + } + + std::any get_global_properties(arb::cell_kind) const override { + return gprop; + } + + arb::util::unique_any get_cell_description(arb::cell_gid_type) const override { + arb::label_dict dict; + using arb::reg::tagged; + dict.set("soma", tagged(1)); + dict.set("dend", join(tagged(3), tagged(4), tagged(42))); + + auto decor = arb::decor{} + // Add HH mechanism to soma, passive channels to dendrites. + .paint("soma"_lab, arb::density("hh")) + .paint("dend"_lab, arb::density("pas")) + // Add synapse to last branch. + .place(arb::mlocation{ morpho.num_branches()-1, 1. }, arb::synapse("exp2syn"), "synapse"); + if (voltage_clamp) decor.paint("soma"_lab, arb::voltage_process("v_clamp")); + + return arb::cable_cell(morpho, decor, dict); + } + + std::optional<double> voltage_clamp; + arb::morphology morpho; + arb::cable_cell_global_properties gprop; +}; + +int main(int argc, char** argv) { + try { + options opt = parse_options(argc, argv); + single_recipe R(opt.swc_file.empty()? default_morphology(): read_swc(opt.swc_file), opt.policy); + R.voltage_clamp = opt.voltage; + arb::simulation sim(R); + + // Attach a sampler to the probe described in the recipe, sampling every 0.1 ms. + + arb::trace_vector<double> traces; + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.1), arb::make_simple_sampler(traces)); + + // Trigger the single synapse (target is gid 0, index 0) at t = 1 ms with + // the given weight. + + arb::spike_event spike = {0, 1., opt.syn_weight}; + arb::cell_spike_events cell_spikes = {0, {spike}}; + sim.inject_events({cell_spikes}); + + sim.run(opt.t_end, opt.dt); + + for (auto entry: traces.at(0)) { + std::cout << entry.t << ", " << entry.v << "\n"; + } + } + catch (std::exception& e) { + std::cerr << "caught exception: " << e.what() << "\n"; + return 2; + } +} + +options parse_options(int argc, char** argv) { + using namespace to; + options opt; + + char** arg = argv+1; + while (*arg) { + if (auto dt = parse<double>(arg, "-d", "--dt")) { + opt.dt = dt.value(); + } + else if (auto t_end = parse<double>(arg, "-t", "--t-end")) { + opt.t_end = t_end.value(); + } + else if (auto weight = parse<float>(arg, "-w", "--weight")) { + opt.syn_weight = weight.value(); + } + else if (auto voltage = parse<double>(arg, "-u", "--voltage")) { + opt.voltage = voltage.value(); + } + else if (auto swc = parse<std::string>(arg, "-m", "--morphology")) { + opt.swc_file = swc.value(); + } + else if (auto nseg = parse<unsigned>(arg, "-n", "--cv-per-branch")) { + opt.policy = arb::cv_policy_fixed_per_branch(nseg.value()); + } + else { + usage(argv[0], "[-m|--morphology SWCFILE] [-d|--dt TIME] [-t|--t-end TIME] [-w|--weight WEIGHT] [-n|--cv-per-branch N]"); + std::exit(1); + } + } + return opt; +} + +// If no SWC file is given, the default morphology consists +// of a soma of radius 6.3 µm and a single unbranched dendrite +// of length 200 µm and radius decreasing linearly from 0.5 µm +// to 0.2 µm. + +arb::morphology default_morphology() { + arb::segment_tree tree; + + tree.append(arb::mnpos, { -6.3, 0.0, 0.0, 6.3}, { 6.3, 0.0, 0.0, 6.3}, 1); + tree.append( 0, { 6.3, 0.0, 0.0, 0.5}, {206.3, 0.0, 0.0, 0.2}, 3); + + return arb::morphology(tree); +} + +arb::morphology read_swc(const std::string& path) { + std::ifstream f(path); + if (!f) throw std::runtime_error("unable to open SWC file: "+path); + + return arborio::load_swc_arbor(arborio::parse_swc(f)); +} diff --git a/mechanisms/CMakeLists.txt b/mechanisms/CMakeLists.txt index 56c0e20836ca8c8ccbd3542067518925567e53d9..320fd85236a2d2ca89df94c9a8accda2eb601480 100644 --- a/mechanisms/CMakeLists.txt +++ b/mechanisms/CMakeLists.txt @@ -17,7 +17,7 @@ make_catalogue( make_catalogue( NAME default - MOD exp2syn expsyn expsyn_curr expsyn_stdp hh kamt kdrmt nax nernst pas gj decay inject + MOD exp2syn expsyn expsyn_curr expsyn_stdp hh kamt kdrmt nax nernst pas gj decay inject v_clamp v_limit VERBOSE ${ARB_CAT_VERBOSE} ADD_DEPS ON) diff --git a/mechanisms/default/v_clamp.mod b/mechanisms/default/v_clamp.mod new file mode 100644 index 0000000000000000000000000000000000000000..33e3eb245f6bf03009f6f02b6604cec9d24feedf --- /dev/null +++ b/mechanisms/default/v_clamp.mod @@ -0,0 +1,12 @@ +NEURON { + VOLTAGE_PROCESS v_clamp + GLOBAL v0 +} + +PARAMETER { + v0 = -60 (mV) +} + +BREAKPOINT { + v = v0 +} diff --git a/mechanisms/default/v_limit.mod b/mechanisms/default/v_limit.mod new file mode 100644 index 0000000000000000000000000000000000000000..276f625f7c2cea61e58fdb115e0749a150a34f88 --- /dev/null +++ b/mechanisms/default/v_limit.mod @@ -0,0 +1,13 @@ +NEURON { + VOLTAGE_PROCESS v_limit + GLOBAL v_low, v_high +} + +PARAMETER { + v_high = 20 (mV) + v_low = -70 (mV) +} + +BREAKPOINT { + v = max(min(v, v_high), v_low) +} diff --git a/modcc/blocks.cpp b/modcc/blocks.cpp index 2bc80734030ec6b4e4f2434e0bc582f07e62b493..70005455bb8a7bc35c340027539a4069f0902f3a 100644 --- a/modcc/blocks.cpp +++ b/modcc/blocks.cpp @@ -36,7 +36,15 @@ ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, IonDep const& I) { } ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, moduleKind const& k) { - return os << (k==moduleKind::density ? "density" : "point process"); + switch (k) { + case moduleKind::density: os << "density"; break; + case moduleKind::voltage: os << "voltage"; break; + case moduleKind::point: os << "point"; break; + case moduleKind::revpot: os << "revpot"; break; + case moduleKind::junction: os << "junction"; break; + default: os << "???"; break; + } + return os; } ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, NeuronBlock const& N) { diff --git a/modcc/identifier.hpp b/modcc/identifier.hpp index 47c835f0f977a4c7201ec6039779e28ae09fc79a..1da953023f45151a3391c8a18e4e99c685c88b38 100644 --- a/modcc/identifier.hpp +++ b/modcc/identifier.hpp @@ -5,7 +5,7 @@ #include <stdexcept> enum class moduleKind { - point, density, junction, revpot + point, density, junction, revpot, voltage, }; /// indicate how a variable is accessed diff --git a/modcc/mechanism.hpp b/modcc/mechanism.hpp deleted file mode 100644 index 8151816504cf82e429ce02ed66e424d24ca2c948..0000000000000000000000000000000000000000 --- a/modcc/mechanism.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include "lib/vector/include/Vector.h" - -/* - Abstract base class for all mechanisms - - This works well for the standard interface that is exported by all mechanisms, - i.e. compute_currents(), etc. The overhead of using virtual dispatch - for such functions is negligable compared to the cost of the operations themselves. - However, the friction between compile time and run time dispatch has to be considered - carefully: - - we want to dispatch on template parameters like vector width, target - hardware etc. Maybe these could be template parameters to the base - class? - - how to expose mechanism functionality, e.g. for computing a mechanism- - specific quantity for visualization? -*/ - -class Mechanism { -public: - // typedefs for storage - using value_type = double; - using array = memory::HostVector<value_type>; - using view_type = memory::HostView<value_type>; - - Mechanism(std::string const& name) - : name_(name) - {} - - //virtual void state() = 0; - //virtual void jacobi() = 0; - virtual void current() = 0; - virtual void init() = 0; - - std::string const& name() { - return name_; - } - -protected: - std::string name_; -}; - diff --git a/modcc/module.cpp b/modcc/module.cpp index 6b76116267a4f47c19f8fc27b67b01aa08fc5796..dcde114f668634d4e23bf43b32e738dd78770ef3 100644 --- a/modcc/module.cpp +++ b/modcc/module.cpp @@ -604,6 +604,9 @@ bool Module::semantic() { post_events_api.first->body(post_events_api.second->body()->clone()); } + // check voltage mechanisms before rev pot ... otherwise we are in trouble + check_voltage_mechanism(); + // Are we writing an ionic reversal potential? If so, change the moduleKind to // `revpot` and assert that the mechanism is 'pure': it has no state variables; // it contributes to no currents, ionic or otherwise; it isn't a point mechanism; @@ -651,12 +654,13 @@ void Module::add_variables_to_symbols() { return symbols_[token.spelling] = make_symbol<WhiteNoise>(token.location, token.spelling); }; - sourceKind current_kind = kind_==moduleKind::density? sourceKind::current_density: sourceKind::current; - sourceKind conductance_kind = kind_==moduleKind::density? sourceKind::conductivity: sourceKind::conductance; + auto current_kind = kind_ == moduleKind::density ? sourceKind::current_density : sourceKind::current; + auto conductance_kind = kind_ == moduleKind::density ? sourceKind::conductivity : sourceKind::conductance; + auto v_access = kind_ == moduleKind::voltage ? accessKind::write : accessKind::read; create_indexed_variable("current_", current_kind, accessKind::write, "", Location()); create_indexed_variable("conductivity_", conductance_kind, accessKind::write, "", Location()); - create_indexed_variable("v", sourceKind::voltage, accessKind::read, "", Location()); + create_indexed_variable("v", sourceKind::voltage, v_access, "", Location()); create_indexed_variable("v_peer", sourceKind::peer_voltage, accessKind::read, "", Location()); create_indexed_variable("dt", sourceKind::dt, accessKind::read, "", Location()); @@ -1010,3 +1014,20 @@ void Module::check_revpot_mechanism() { kind_ = moduleKind::revpot; } + +void Module::check_voltage_mechanism() { + if (kind_ != moduleKind::voltage) return; + + auto impure = neuron_block_.has_nonspecific_current(); + for (const auto& ion: neuron_block_.ions) { + impure |= ion.writes_concentration_int(); + impure |= ion.writes_concentration_ext(); + impure |= ion.writes_current(); + impure |= ion.writes_rev_potential(); + } + + if (impure) { + error("Voltage mechanisms may not write ionic quantities.."); + return; + } +} diff --git a/modcc/module.hpp b/modcc/module.hpp index b75897d5ff1fd83b7906d591d151e486050b9f53..81facaa19ed8ca027053bc3d53fb6c8997e7ee3f 100644 --- a/modcc/module.hpp +++ b/modcc/module.hpp @@ -150,6 +150,9 @@ private: return s == symbols_.end() ? false : s->second->kind() == kind; } + // Check requirements for membrane potential manipulation. + void check_voltage_mechanism(); + // Check requirements for reversal potential setters. void check_revpot_mechanism(); diff --git a/modcc/parser.cpp b/modcc/parser.cpp index cc86179f5f63fc912c8d4ac3bdc09b28e70a6f98..dfd0bf4d9186fb845abdbc21229c620edbcccb3e 100644 --- a/modcc/parser.cpp +++ b/modcc/parser.cpp @@ -236,17 +236,19 @@ void Parser::parse_neuron_block() { case tok::suffix: case tok::point_process: + case tok::voltage_process: case tok::junction_process: neuron_block.kind = (token_.type == tok::suffix) ? moduleKind::density : + (token_.type == tok::voltage_process) ? moduleKind::voltage : (token_.type == tok::point_process) ? moduleKind::point : moduleKind::junction; - // set the modul kind + // set the module kind module_->kind(neuron_block.kind); get_token(); // consume SUFFIX / POINT_PROCESS // assert that a valid name for the Neuron has been specified if (token_.type != tok::identifier) { - error(pprintf("invalid name for SUFFIX, found '%'", token_.spelling)); + error(pprintf("invalid name for mechanism, found '%'", token_.spelling)); return; } neuron_block.name = token_.spelling; diff --git a/modcc/printer/cprinter.cpp b/modcc/printer/cprinter.cpp index 432c1187bf0967c1ff68ea868654a77007b7d0ca..708ad24fe124eba3893870cf2d2922d24c214e03 100644 --- a/modcc/printer/cprinter.cpp +++ b/modcc/printer/cprinter.cpp @@ -232,7 +232,8 @@ ARB_LIBMODCC_API std::string emit_cpp_source(const Module& module_, const printe auto emit_body = [&](APIMethod *p, bool add=false) { auto flags = ApiFlags{} .additive(add) - .point(moduleKind::point == module_.kind()); + .point(moduleKind::point == module_.kind()) + .voltage(moduleKind::voltage == module_.kind()); if (with_simd) { emit_simd_api_body(out, p, vars.scalars, flags); } else { @@ -536,19 +537,26 @@ void emit_api_body(std::ostream& out, APIMethod* method, const ApiFlags& flags) for (auto& sym: indexed_vars) { auto d = decode_indexed_variable(sym->external_variable()); + auto write_voltage = sym->external_variable()->data_source() == sourceKind::voltage + && flags.can_write_voltage; out << "arb_value_type " << cprint(sym) << " = "; - if (sym->is_read() || (sym->is_write() && d.additive)) { + if (sym->is_read() || (sym->is_write() && d.additive) || write_voltage) { out << scaled(d.scale) << deref(d) << ";\n"; } else { out << "0;\n"; } } + out << cprint(body); for (auto& sym: indexed_vars) { if (!sym->external_variable()->is_write()) continue; auto d = decode_indexed_variable(sym->external_variable()); + auto write_voltage = sym->external_variable()->data_source() == sourceKind::voltage + && flags.can_write_voltage; + if (write_voltage) d.readonly = false; + bool use_weight = d.always_use_weight || !flags.is_point; if (d.readonly) throw compiler_exception("Cannot assign to read-only external state: "+sym->to_string()); std::string @@ -564,6 +572,10 @@ void emit_api_body(std::ostream& out, APIMethod* method, const ApiFlags& flags) "{0} = fma({1}{2}, {3}, {0});\n", var, scale, weight, name); } + else if (write_voltage) { + // SAFETY: we only ever allow *one* V-PROCESS per CV, so this is OK. + out << fmt::format("{0} = {1};\n", var, name); + } else if (d.accumulate) { out << fmt::format("{} = fma({}{}, {}, {});\n", var, scale, weight, name, var); @@ -705,11 +717,15 @@ void SimdPrinter::visit(BlockExpression* block) { EXITM(out_, "block"); } -void emit_simd_state_read(std::ostream& out, LocalVariable* local, simd_expr_constraint constraint) { +void emit_simd_state_read(std::ostream& out, LocalVariable* local, simd_expr_constraint constraint, const ApiFlags& flags) { ENTER(out); out << "simd_value " << local->name(); - if (local->is_read() || (local->is_write() && decode_indexed_variable(local->external_variable()).additive)) { + auto write_voltage = local->external_variable()->data_source() == sourceKind::voltage + && flags.can_write_voltage; + auto is_additive = local->is_write() && decode_indexed_variable(local->external_variable()).additive; + + if (local->is_read() || is_additive || write_voltage) { auto d = decode_indexed_variable(local->external_variable()); if (d.scalar()) { out << " = simd_cast<simd_value>(" << pp_var_pfx << d.data_var @@ -760,9 +776,12 @@ void emit_simd_state_update(std::ostream& out, const ApiFlags& flags) { if (!external->is_write()) return; ENTER(out); - auto d = decode_indexed_variable(external); + if (external->data_source() == sourceKind::voltage && flags.can_write_voltage) { + d.readonly = false; + } + if (d.readonly) { throw compiler_exception("Cannot assign to read-only external state: "+external->to_string()); } @@ -779,6 +798,9 @@ void emit_simd_state_update(std::ostream& out, ss << "S::mul(" << name << ", simd_cast<simd_value>(" << as_c_double(1/d.scale) << "))"; scaled = ss.str(); } + auto write_voltage = external->data_source() == sourceKind::voltage + && flags.can_write_voltage; + if (write_voltage) d.readonly = false; std::string weight = (d.always_use_weight || !flags.is_point) ? "w_" : "simd_cast<simd_value>(1.0)"; @@ -807,6 +829,34 @@ void emit_simd_state_update(std::ostream& out, data, index, weight, scaled); } } + else if (write_voltage) { + /* For voltage processes we *assign* to the potential field. + ** SAFETY: only one V-PROCESS per CV allowed + */ + if (d.index_var_kind == index_kind::node) { + if (constraint == simd_expr_constraint::contiguous) { + out << fmt::format("indirect({} + {}, simd_width_) = {};\n", + data, node, name); + } + else { + // We need this instead of simple assignment! + out << fmt::format("{{\n" + " simd_value t_{}0_ = simd_cast<simd_value>(0.0);\n" + " assign(t_{}0_, indirect({}, simd_cast<simd_index>({}), simd_width_, constraint_category_));\n" + " {} -= t_{}0_;\n" + " indirect({}, simd_cast<simd_index>({}), simd_width_, constraint_category_) += S::mul({}, {});\n" + "}}\n", + name, + name, data, node, + scaled, name, + data, node, weight, scaled); + } + } + else { + out << fmt::format("indirect({}, {}, simd_width_, index_constraint::none) = {};\n", + data, index, name); + } + } else if (d.accumulate) { if (d.index_var_kind == index_kind::node) { std::string tempvar = "t_" + external->name(); @@ -903,7 +953,7 @@ void emit_simd_body_for_loop( emit_simd_index_initialize(out, indices, constraint); for (auto& sym: indexed_vars) { - emit_simd_state_read(out, sym, constraint); + emit_simd_state_read(out, sym, constraint, flags); } simdprint printer(body, scalars); @@ -988,7 +1038,7 @@ void emit_simd_api_body(std::ostream& out, APIMethod* method, else { // We may nonetheless need to read a global scalar indexed variable. for (auto& sym: scalar_indexed_vars) { - emit_simd_state_read(out, sym, simd_expr_constraint::other); + emit_simd_state_read(out, sym, simd_expr_constraint::other, flags); } out << fmt::format("for (arb_size_type i_ = 0; i_ < {}width; i_ += simd_width_) {{\n", diff --git a/modcc/printer/cprinter.hpp b/modcc/printer/cprinter.hpp index b53c318bd9ac73f16fd1299fe55486ec7136d5da..010d1398b00474ffca8732fbf723d0a34a3f81b4 100644 --- a/modcc/printer/cprinter.hpp +++ b/modcc/printer/cprinter.hpp @@ -51,15 +51,17 @@ struct ApiFlags { bool ppack_iface=true; bool use_additive=false; bool is_point=false; + bool can_write_voltage=false; ApiFlags& loop(bool v) { cv_loop = v; return *this; } ApiFlags& iface(bool v) { ppack_iface = v; return *this; } ApiFlags& additive(bool v) { use_additive = v; return *this; } ApiFlags& point(bool v) { is_point = v; return *this; } + ApiFlags& voltage(bool v) { can_write_voltage = v; return *this; } }; -const ApiFlags net_recv_flags = {false, false, true, false}; -const ApiFlags post_evt_flags = {false, false, false, false}; +const ApiFlags net_recv_flags = {false, false, true}; // No CV loop, no PPACK, use additive +const ApiFlags post_evt_flags = {false, false}; // No CV loop, no PPACK class ARB_LIBMODCC_API SimdPrinter: public Visitor { public: diff --git a/modcc/printer/gpuprinter.cpp b/modcc/printer/gpuprinter.cpp index 7b326fea93b61d110efd547c89c3e52d1ccd376d..2150bf0291101802ecbb157851eb39bb5408da27 100644 --- a/modcc/printer/gpuprinter.cpp +++ b/modcc/printer/gpuprinter.cpp @@ -29,7 +29,7 @@ static std::string scaled(double coeff) { void emit_api_body_cu(std::ostream& out, APIMethod* method, const ApiFlags&); -void emit_state_read_cu(std::ostream& out, LocalVariable* local); +void emit_state_read_cu(std::ostream& out, LocalVariable* local, const ApiFlags&); void emit_state_update_cu(std::ostream& out, Symbol* from, IndexedVariable* external, const ApiFlags&); const char* index_id(Symbol *s); @@ -200,7 +200,7 @@ ARB_LIBMODCC_API std::string emit_gpu_cu_source(const Module& module_, const pri << "void " << e->name() << "(arb_mechanism_ppack params_) {\n" << indent << "int n_ = params_.width;\n" << "int tid_ = threadIdx.x + blockDim.x*blockIdx.x;\n"; - emit_api_body_cu(out, e, ApiFlags{}.point(is_point_proc).additive(additive)); + emit_api_body_cu(out, e, ApiFlags{}.point(is_point_proc).additive(additive).voltage(moduleKind::voltage == module_.kind())); out << popindent << "}\n\n"; } }; @@ -411,7 +411,7 @@ void emit_api_body_cu(std::ostream& out, APIMethod* e, const ApiFlags& flags) { } for (auto& sym: indexed_vars) { - emit_state_read_cu(out, sym); + emit_state_read_cu(out, sym, flags); } out << cuprint(body); @@ -438,10 +438,12 @@ namespace { }; } -void emit_state_read_cu(std::ostream& out, LocalVariable* local) { +void emit_state_read_cu(std::ostream& out, LocalVariable* local, const ApiFlags& flags) { + auto write_voltage = local->external_variable()->data_source() == sourceKind::voltage + && flags.can_write_voltage; out << "arb_value_type " << cuprint(local) << " = "; auto d = decode_indexed_variable(local->external_variable()); - if (local->is_read() || (local->is_write() && d.additive)) { + if (local->is_read() || (local->is_write() && d.additive) || write_voltage) { if (d.scale != 1) { out << as_c_double(d.scale) << "*"; } @@ -453,10 +455,15 @@ void emit_state_read_cu(std::ostream& out, LocalVariable* local) { } -void emit_state_update_cu(std::ostream& out, Symbol* from, - IndexedVariable* external, const ApiFlags& flags) { +void emit_state_update_cu(std::ostream& out, + Symbol* from, + IndexedVariable* external, + const ApiFlags& flags) { if (!external->is_write()) return; auto d = decode_indexed_variable(external); + auto write_voltage = external->data_source() == sourceKind::voltage + && flags.can_write_voltage; + if (write_voltage) d.readonly = false; if (d.readonly) { throw compiler_exception("Cannot assign to read-only external state: "+external->to_string()); } @@ -466,8 +473,8 @@ void emit_state_update_cu(std::ostream& out, Symbol* from, auto data = pp_var_pfx + d.data_var; auto index = index_i_name(d.outer_index_var()); auto var = deref(d); - std::string weight = (d.always_use_weight || !flags.is_point) ? pp_var_pfx + "weight[tid_]" : "1.0"; - weight = scale + weight; + auto use_weight = d.always_use_weight || !flags.is_point; + std::string weight = scale + (use_weight ? pp_var_pfx + "weight[tid_]" : "1.0"); if (d.additive && flags.use_additive) { out << name << " -= " << var << ";\n"; @@ -478,6 +485,15 @@ void emit_state_update_cu(std::ostream& out, Symbol* from, out << var << " = fma(" << weight << ", " << name << ", " << var << ");\n"; } } + else if (write_voltage) { + /* SAFETY: + ** - Only one V-PROCESS per CV + ** - these can never be point mechs + ** - they run separatly from density/point mechs + */ + out << name << " -= " << var << ";\n" + << var << " = fma(" << weight << ", " << name << ", " << var << ");\n"; + } else if (d.accumulate) { if (flags.is_point) { out << "::arb::gpu::reduce_by_key(" << weight << "*" << name << ',' << data << ", " << index << ", lane_mask_);\n"; diff --git a/modcc/printer/printerutil.hpp b/modcc/printer/printerutil.hpp index f0a7ff8ed748d8314cfb3356d01a220b4d68ff26..7fc8cfa34cfde32fb71348cb03ffa9b30cdb6b14 100644 --- a/modcc/printer/printerutil.hpp +++ b/modcc/printer/printerutil.hpp @@ -60,6 +60,7 @@ struct namespace_declaration_close { inline const char* module_kind_str(const Module& m) { switch (m.kind()) { case moduleKind::density: return "arb_mechanism_kind_density"; + case moduleKind::voltage: return "arb_mechanism_kind_voltage"; case moduleKind::point: return "arb_mechanism_kind_point"; case moduleKind::revpot: return "arb_mechanism_kind_reversal_potential"; case moduleKind::junction: return "arb_mechanism_kind_gap_junction"; diff --git a/modcc/token.cpp b/modcc/token.cpp index e8b70a5ae3e7c0afc18e78a9c8e998f03f323a7c..34081c48db414460ce4ec819270c1b9a1afa0d66 100644 --- a/modcc/token.cpp +++ b/modcc/token.cpp @@ -41,6 +41,7 @@ static Keyword keywords[] = { {"UNITSOFF", tok::unitsoff}, {"UNITSON", tok::unitson}, {"SUFFIX", tok::suffix}, + {"VOLTAGE_PROCESS", tok::voltage_process}, {"NONSPECIFIC_CURRENT", tok::nonspecific_current}, {"USEION", tok::useion}, {"READ", tok::read}, @@ -132,6 +133,7 @@ static TokenString token_strings[] = { {"UNITSOFF", tok::unitsoff}, {"UNITSON", tok::unitson}, {"SUFFIX", tok::suffix}, + {"VOLTAGE_PROCESS", tok::voltage_process}, {"NONSPECIFIC_CURRENT", tok::nonspecific_current}, {"USEION", tok::useion}, {"READ", tok::read}, diff --git a/modcc/token.hpp b/modcc/token.hpp index c3e6d3168539c4e65524b087ae1d3ba26f7af9b9..109d38a0624fc826350adb5928b9cb56a94d6def 100644 --- a/modcc/token.hpp +++ b/modcc/token.hpp @@ -64,7 +64,7 @@ enum class tok { range, local, conserve, compartment, solve, method, steadystate, threadsafe, global, - point_process, junction_process, + point_process, junction_process, voltage_process, from, to, // prefix binary operators diff --git a/python/cells.cpp b/python/cells.cpp index b3d542c437df837eeb245cb480b1899dab018a95..bc881d3ae81fbd724f79fd6a6ed57bfa451e168c 100644 --- a/python/cells.cpp +++ b/python/cells.cpp @@ -466,8 +466,6 @@ void register_cells(pybind11::module& m) { .def(pybind11::init([](const std::string& i, double v) -> arb::ion_diffusivity { return {i, v}; })) .def("__repr__", [](const arb::ion_diffusivity& d){return "D" + d.ion + "=" + std::to_string(d.value);}); - // arb::density - pybind11::class_<arb::density> density(m, "density", "For painting a density mechanism on a region."); density .def(pybind11::init([](const std::string& name) {return arb::density(name);})) @@ -478,6 +476,16 @@ void register_cells(pybind11::module& m) { .def("__repr__", [](const arb::density& d){return "<arbor.density " + mechanism_desc_str(d.mech) + ">";}) .def("__str__", [](const arb::density& d){return "<arbor.density " + mechanism_desc_str(d.mech) + ">";}); + pybind11::class_<arb::voltage_process> voltage_process(m, "voltage_process", "For painting a voltage_process mechanism on a region."); + voltage_process + .def(pybind11::init([](const std::string& name) {return arb::voltage_process(name);})) + .def(pybind11::init([](arb::mechanism_desc mech) {return arb::voltage_process(mech);})) + .def(pybind11::init([](const std::string& name, const std::unordered_map<std::string, double>& params) {return arb::voltage_process(name, params);})) + .def(pybind11::init([](arb::mechanism_desc mech, const std::unordered_map<std::string, double>& params) {return arb::voltage_process(mech, params);})) + .def_readonly("mech", &arb::voltage_process::mech, "The underlying mechanism.") + .def("__repr__", [](const arb::voltage_process& d){return "<arbor.voltage_process " + mechanism_desc_str(d.mech) + ">";}) + .def("__str__", [](const arb::voltage_process& d){return "<arbor.voltage_process " + mechanism_desc_str(d.mech) + ">";}); + // arb::scaled_mechanism<arb::density> pybind11::class_<arb::scaled_mechanism<arb::density>> scaled_mechanism( @@ -828,6 +836,12 @@ void register_cells(pybind11::module& m) { }, "region"_a, "mechanism"_a, "Associate a density mechanism with a region.") + .def("paint", + [](arb::decor& dec, const char* region, const arb::voltage_process& mechanism) { + return dec.paint(arborio::parse_region_expression(region).unwrap(), mechanism); + }, + "region"_a, "mechanism"_a, + "Associate a voltage process mechanism with a region.") .def("paint", [](arb::decor& dec, const char* region, const arb::scaled_mechanism<arb::density>& mechanism) { dec.paint(arborio::parse_region_expression(region).unwrap(), mechanism); diff --git a/python/example/v-clamp.py b/python/example/v-clamp.py new file mode 100755 index 0000000000000000000000000000000000000000..274691777b4489c7c462badaedd6adb84bdb1b9d --- /dev/null +++ b/python/example/v-clamp.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# This script is included in documentation. Adapt line numbers if touched. + +import arbor +import pandas # You may have to pip install these. +import seaborn # You may have to pip install these. + +# (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 and set up a decor object + +decor = ( + arbor.decor() + .set_property(Vm=-40) + .paint('"soma"', arbor.density("hh")) + .paint('"soma"', arbor.voltage_process("v_clamp/v0=-42")) + .place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp") + .place('"midpoint"', arbor.threshold_detector(-10), "detector") +) + +# (4) Create cell and the single cell model based on it +cell = arbor.cable_cell(tree, decor, labels) + +# (5) Make single cell model. +m = arbor.single_cell_model(cell) + +# (6) Attach voltage probe sampling at 10 kHz (every 0.1 ms). +m.probe("voltage", '"midpoint"', frequency=10) + +# (7) Run simulation for 30 ms of simulated activity. +m.run(tfinal=30) + +# (8) Print spike times. +if len(m.spikes) > 0: + print("{} spikes:".format(len(m.spikes))) + for s in m.spikes: + print("{:3.3f}".format(s)) +else: + print("no spikes") + +# (9) Plot the recorded voltages over time. +print("Plotting results ...") +seaborn.set_theme() # Apply some styling to the plot +df = pandas.DataFrame({"t/ms": m.traces[0].time, "U/mV": m.traces[0].value}) +seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", ci=None).savefig( + "single_cell_model_result.svg" +) + +# (10) Optionally, you can store your results for later processing. +df.to_csv("single_cell_model_result.csv", float_format="%g") diff --git a/scripts/run_cpp_examples.sh b/scripts/run_cpp_examples.sh index 5af6591ce71cee0722a280d1f0b2b84881940ea7..f332e60b574e166344fe7e5cfd1de67d11364339 100755 --- a/scripts/run_cpp_examples.sh +++ b/scripts/run_cpp_examples.sh @@ -26,7 +26,7 @@ check () { fi } -for ex in bench brunel gap_junctions generators lfp ring single-cell "probe-demo v" plasticity ou +for ex in bench brunel gap_junctions generators lfp ring single-cell "probe-demo v" plasticity ou voltage-clamp do echo " - $ex" dir=`echo $ex | tr ' ' '_'` diff --git a/scripts/run_python_examples.sh b/scripts/run_python_examples.sh index 4277d7b71f8a63a91aa1be7b8d2fa40def3ad6cc..3bdc5a7d4c19e4efca9f2ac1a40bfda1b07b21b2 100755 --- a/scripts/run_python_examples.sh +++ b/scripts/run_python_examples.sh @@ -31,3 +31,4 @@ $PREFIX python python/example/network_ring_gpu.py # by default, gpu_id=None $PREFIX python python/example/network_two_cells_gap_junctions.py $PREFIX python python/example/diffusion.py $PREFIX python python/example/plasticity.py +$PREFIX python python/example/v-clamp.py diff --git a/test/unit-modcc/mod_files/test_v_process.mod b/test/unit-modcc/mod_files/test_v_process.mod new file mode 100644 index 0000000000000000000000000000000000000000..a770c55bbf3f64f4c4d6efd6ae10f06497350aaf --- /dev/null +++ b/test/unit-modcc/mod_files/test_v_process.mod @@ -0,0 +1,13 @@ +NEURON { + VOLTAGE_PROCESS vmech +} + +INITIAL { c = 0 } + +STATE { c } + +PARAMETER { + tau_c = 150 (ms) +} + +BREAKPOINT { v = 42 } diff --git a/test/unit-modcc/test_module.cpp b/test/unit-modcc/test_module.cpp index 67e4b32f100ad8a23993ba971834ed9c0ea31325..daf0d1b8504b1f963c3cf494f811d5a515b122d7 100644 --- a/test/unit-modcc/test_module.cpp +++ b/test/unit-modcc/test_module.cpp @@ -123,6 +123,15 @@ TEST(Module, read_write_ion) { EXPECT_TRUE(m.semantic()); } +TEST(Module, v_process) { + Module m(io::read_all(DATADIR "/mod_files/test_v_process.mod"), "test_v_process.mod"); + EXPECT_NE(m.buffer().size(), 0u); + + Parser p(m, false); + EXPECT_TRUE(p.parse()); + EXPECT_TRUE(m.semantic()); +} + // Regression test in #1893 we found that the solver segfaults when handed a // naked comparison statement. TEST(Module, solver_bug_1893) { diff --git a/test/unit-modcc/test_parser.cpp b/test/unit-modcc/test_parser.cpp index c666a3b49890623daf7d9bafaefcbe04e22ff9be..a5800ce7e32d62d981b5256945a865107f619bfe 100644 --- a/test/unit-modcc/test_parser.cpp +++ b/test/unit-modcc/test_parser.cpp @@ -79,6 +79,16 @@ TEST(Parser, full_file) { EXPECT_EQ(p.status(), lexerStatus::happy); } +TEST(Parser, v_proc) { + Module m(io::read_all(DATADIR "/mod_files/test_v_process.mod"), "v_process.mod"); + if (m.buffer().size() == 0) { + std::cout << "skipping Parser.full_file test because unable to open input file" << std::endl; + return; + } + Parser p(m); + EXPECT_EQ(p.status(), lexerStatus::happy); +} + TEST(Parser, procedure) { std::vector<const char*> calls = { "PROCEDURE foo(x, y) {\n" diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 36892930db35fba373fdad991e068e76721febe3..f42737eb5467dcb635e6bcbcbacb3f155eb63ee1 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -145,6 +145,7 @@ set(unit_sources test_unique_any.cpp test_vector.cpp test_version.cpp + test_v_clamp.cpp # unit test driver test.cpp diff --git a/test/unit/test_abi.cpp b/test/unit/test_abi.cpp index 1c6c68b59ecc2d9c17d8440165f6b283b1d017c5..7c700ac90a09500803919e3b88163a89164e856d 100644 --- a/test/unit/test_abi.cpp +++ b/test/unit/test_abi.cpp @@ -59,7 +59,7 @@ TEST(abi, multicore_initialisation) { layout.weight.assign(ncv, 1.); for (arb_size_type i = 0; i<ncv; ++i) layout.cv.push_back(i); - EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout)); + EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout, {})); { ASSERT_EQ(globals.size(), mech.mech_.n_globals); @@ -137,7 +137,7 @@ TEST(abi, multicore_null) { layout.weight.assign(ncv, 1.); for (arb_size_type i = 0; i<ncv; ++i) layout.cv.push_back(i); - EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout)); + EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout, {})); } #ifdef ARB_GPU_ENABLED @@ -202,7 +202,7 @@ TEST(abi, gpu_initialisation) { layout.weight.assign(ncv, 1.); for (arb_size_type i = 0; i<ncv; ++i) layout.cv.push_back(i); - EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout)); + EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout, {})); { ASSERT_EQ(globals.size(), mech.mech_.n_globals); @@ -279,7 +279,7 @@ TEST(abi, gpu_null) { layout.weight.assign(ncv, 1.); for (arb_size_type i = 0; i<ncv; ++i) layout.cv.push_back(i); - EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout)); + EXPECT_NO_THROW(shared_state.instantiate(mech, 42, {}, layout, {})); } diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp index a11be575dd7ad71ebdc6d6f7369e7f172f56715e..351504fce1ad2871953893c1343a9de1177a9755 100644 --- a/test/unit/test_fvm_lowered.cpp +++ b/test/unit/test_fvm_lowered.cpp @@ -608,8 +608,8 @@ TEST(fvm_lowered, ionic_concentrations) { ncell, ncell, 0, cv_to_intdom, cv_to_intdom, vinit, temp, diam, src_to_spike, read_cai_mech->data_alignment()); shared_state->add_ion("ca", 2, ion_config); - shared_state->instantiate(*read_cai_mech, 0, overrides, layout); - shared_state->instantiate(*write_cai_mech, 1, overrides, layout); + shared_state->instantiate(*read_cai_mech, 0, overrides, layout, {}); + shared_state->instantiate(*write_cai_mech, 1, overrides, layout, {}); shared_state->reset(); diff --git a/test/unit/test_kinetic_linear.cpp b/test/unit/test_kinetic_linear.cpp index e61d59a9cea9cfa04a616551396db0a6ae7d9ec5..bccdf44e2834dae58c755c4bfc621606fe5b61de 100644 --- a/test/unit/test_kinetic_linear.cpp +++ b/test/unit/test_kinetic_linear.cpp @@ -56,7 +56,7 @@ void run_test(std::string mech_name, layout.cv.push_back(i); } - shared_state->instantiate(*test, 0, overrides, layout); + shared_state->instantiate(*test, 0, overrides, layout, {}); shared_state->reset(); test->initialize(); diff --git a/test/unit/test_mech_temp_diam.cpp b/test/unit/test_mech_temp_diam.cpp index d3adf7ad0c3cc16b798922c68a39792a00ba89dd..dbaf1b0a124e3a2807c3fa512f6c56d13038c656 100644 --- a/test/unit/test_mech_temp_diam.cpp +++ b/test/unit/test_mech_temp_diam.cpp @@ -46,7 +46,7 @@ void run_celsius_test() { layout.cv.push_back(i); } - shared_state->instantiate(*celsius_test, 0, overrides, layout); + shared_state->instantiate(*celsius_test, 0, overrides, layout, {}); shared_state->reset(); // expect 0 value in state 'c' after init: @@ -96,7 +96,7 @@ void run_diam_test() { ncell, ncell, 0, cv_to_intdom, cv_to_intdom, vinit, temp, diam, src_to_spike, celsius_test->data_alignment()); - shared_state->instantiate(*celsius_test, 0, overrides, layout); + shared_state->instantiate(*celsius_test, 0, overrides, layout, {}); shared_state->reset(); // expect 0 value in state 'd' after init: diff --git a/test/unit/test_s_expr.cpp b/test/unit/test_s_expr.cpp index 98fe3e7ce81d1acf5fe4878a4ea9271d03b1d3a8..f1e9237833353ba3b38117a20db08d07205a47f1 100644 --- a/test/unit/test_s_expr.cpp +++ b/test/unit/test_s_expr.cpp @@ -597,6 +597,9 @@ std::ostream& operator<<(std::ostream& o, const synapse& p) { std::ostream& operator<<(std::ostream& o, const density& p) { return o << "(density " << p.mech << ')'; } +std::ostream& operator<<(std::ostream& o, const voltage_process& p) { + return o << "(voltage-process " << p.mech << ')'; +} template <typename TaggedMech> std::ostream& operator<<(std::ostream& o, const scaled_mechanism<TaggedMech>& p) { o << "(scaled-mechanism " << p.t_mech; @@ -718,6 +721,7 @@ TEST(decor_literals, round_tripping) { "(ion-external-concentration \"h\" -50.1)", "(ion-reversal-potential \"na\" 30)"}; auto paint_literals = { + "(voltage-process (mechanism \"hh\"))", "(density (mechanism \"hh\"))", "(density (mechanism \"pas\" (\"g\" 0.02)))", "(scaled-mechanism (density (mechanism \"pas\" (\"g\" 0.02))))", diff --git a/test/unit/test_segment_tree.cpp b/test/unit/test_segment_tree.cpp index 571f332f001353191c766704e277bd362de74590..fb05e9abe5247a4314178e810d06d29288078d5d 100644 --- a/test/unit/test_segment_tree.cpp +++ b/test/unit/test_segment_tree.cpp @@ -358,7 +358,7 @@ TEST(segment_tree, tag_roots) { ** \ ** 2 - 3 ** \ - ** 5 + ** 5 */ { arb::segment_tree tree; diff --git a/test/unit/test_synapses.cpp b/test/unit/test_synapses.cpp index 3fb0d713f2f217558752a5423b5a569a327c2a4f..d13aa718b00583b850ef74967fd67dd7f85c97d3 100644 --- a/test/unit/test_synapses.cpp +++ b/test/unit/test_synapses.cpp @@ -107,8 +107,8 @@ TEST(synapses, syn_basic_state) { std::vector<index_type> syn_mult(num_syn, 1); std::vector<value_type> syn_weight(num_syn, 1.0); - state.instantiate(*expsyn, 0, {}, {syn_cv, {}, syn_weight, syn_mult}); - state.instantiate(*exp2syn, 1, {}, {syn_cv, {}, syn_weight, syn_mult}); + state.instantiate(*expsyn, 0, {}, {syn_cv, {}, syn_weight, syn_mult}, {}); + state.instantiate(*exp2syn, 1, {}, {syn_cv, {}, syn_weight, syn_mult}, {}); // Parameters initialized to default values? diff --git a/test/unit/test_v_clamp.cpp b/test/unit/test_v_clamp.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a2948c7cd1f045e02c1f62f356d3f74a884a9eb5 --- /dev/null +++ b/test/unit/test_v_clamp.cpp @@ -0,0 +1,276 @@ +#include <gtest/gtest.h> + +#include "common.hpp" + +#include <arbor/arbexcept.hpp> +#include <arbor/cable_cell.hpp> +#include <arbor/domain_decomposition.hpp> +#include <arbor/load_balance.hpp> +#include <arbor/recipe.hpp> +#include <arbor/schedule.hpp> +#include <arbor/simulation.hpp> + +#include <arborio/label_parse.hpp> +using namespace arborio::literals; + +struct recipe: public arb::recipe { + recipe(bool clamp, bool limit): limit{limit}, clamp{clamp} { + gprop.default_parameters = arb::neuron_parameter_defaults; + gprop.default_parameters.discretization = arb::cv_policy_max_extent(1.0); + } + + arb::cell_size_type num_cells() const override { return 1; } + arb::cell_kind get_cell_kind(arb::cell_gid_type gid) const override { return arb::cell_kind::cable; } + std::vector<arb::cell_connection> connections_on(arb::cell_gid_type gid) const override { return {}; } + arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override { + auto tree = arb::segment_tree{}; + auto p = arb::mnpos; + p = tree.append(p, {0, 0, 0, 1 }, {0, 0, 1, 1 }, 1); // soma 0-1 + p = tree.append(p, {0, 0, 1, 0.1}, {0, 0, 4, 0.1}, 2); // dend 1-4 + auto decor = arb::decor{} + .paint("(tag 1)"_reg, arb::density("hh")) + .paint("(tag 2)"_reg, arb::density("pas")) + .place("(location 0 0.125)"_ls, arb::synapse("expsyn"), "tgt"); + + if (clamp) decor.paint("(tag 1)"_reg, arb::voltage_process("v_clamp/v0=-42")); + if (limit) decor.paint("(tag 1)"_reg, arb::voltage_process("v_limit/v_high=0,v_low=-60")); + return arb::cable_cell(arb::morphology{tree}, decor); + } + std::vector<arb::probe_info> get_probes(arb::cell_gid_type gid) const override { + return { arb::cable_probe_membrane_voltage{"(location 0 0.125)"_ls}, // soma center: 0.25/2 + arb::cable_probe_membrane_voltage{"(location 0 0.625)"_ls}}; // dend center: 0.75/2 + 0.25 + } + std::vector<arb::event_generator> event_generators(arb::cell_gid_type) const override { + return {arb::regular_generator({"tgt"}, 5.0, 0.2, 0.05)}; + } + std::any get_global_properties(arb::cell_kind) const override { return gprop; } + + arb::cable_cell_global_properties gprop; + bool limit = false; + bool clamp = false; +}; + +struct Um_type { + constexpr static double delta = 1e-6; + + double t; + double u; + + friend std::ostream& operator<<(std::ostream& os, const Um_type& um) { + os << "{ " << um.t << ", " << um.u << " }"; + return os; + } + + friend bool operator==(const Um_type& lhs, const Um_type& rhs) { + return (std::abs(lhs.t - rhs.t) <= delta) + && (std::abs(lhs.u - rhs.u) <= delta); + } +}; + +using um_s_type = std::vector<Um_type>; + +TEST(v_process, clamp) { + auto u_soma = um_s_type{}; + auto u_dend = um_s_type{}; + auto fun = [&u_soma, &u_dend](arb::probe_metadata pm, + std::size_t n, + const arb::sample_record* samples) { + for (std::size_t ix = 0ul; ix < n; ++ix) { + const auto& [t, v] = samples[ix]; + double u = *arb::util::any_cast<const double*>(v); + if (pm.id.index == 0) { + u_soma.push_back({t, u}); + } + else if (pm.id.index == 1) { + u_dend.push_back({t, u}); + } + else { + throw std::runtime_error{"Unexpected probe id"}; + } + } + }; + auto sim = arb::simulation(recipe{true, false}); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05), fun); + sim.run(1.0, 0.005); + + um_s_type exp_soma{{ 0, -65 }, + { 0.05, -42 }, + { 0.095, -42 }, + { 0.145, -42 }, + { 0.2, -42 }, + { 0.25, -42 }, + { 0.3, -42 }, + { 0.35, -42 }, + { 0.4, -42 }, + { 0.45, -42 }, + { 0.5, -42 }, + { 0.55, -42 }, + { 0.6, -42 }, + { 0.65, -42 }, + { 0.7, -42 }, + { 0.75, -42 }, + { 0.8, -42 }, + { 0.85, -42 }, + { 0.9, -42 }, + { 0.95, -42 },}; + um_s_type exp_dend{{ 0, -65 }, + { 0.05, -42.1167152 }, + { 0.095, -42.0917893 }, + { 0.145, -42.0464582 }, + { 0.2, -41.9766685 }, + { 0.25, -0.124773816 }, + { 0.3, -0.0708536802 }, + { 0.35, -0.0526604489 }, + { 0.4, -0.043518448 }, + { 0.45, -0.0380607556 }, + { 0.5, -0.0344769712 }, + { 0.55, -0.0319770579 }, + { 0.6, -0.0301575597 }, + { 0.65, -0.0287897554 }, + { 0.7, -0.0277342043 }, + { 0.75, -0.0269013197 }, + { 0.8, -0.0262312483 }, + { 0.85, -0.0256827719 }, + { 0.9, -0.0252268065 }, + { 0.95, -0.0248424087 }}; + ASSERT_TRUE(testing::seq_eq(u_soma, exp_soma)); + ASSERT_TRUE(testing::seq_eq(u_dend, exp_dend)); +} + +TEST(v_process, limit) { + auto u_soma = um_s_type{}; + auto u_dend = um_s_type{}; + auto fun = [&u_soma, &u_dend](arb::probe_metadata pm, + std::size_t n, + const arb::sample_record* samples) { + for (std::size_t ix = 0ul; ix < n; ++ix) { + const auto& [t, v] = samples[ix]; + double u = *arb::util::any_cast<const double*>(v); + if (pm.id.index == 0) { + u_soma.push_back({t, u}); + } + else if (pm.id.index == 1) { + u_dend.push_back({t, u}); + } + else { + throw std::runtime_error{"Unexpected probe id"}; + } + } + }; + auto sim = arb::simulation(recipe{false, true}); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05), fun); + sim.run(1.0, 0.005); + + um_s_type exp_soma{{ 0, -65 }, + { 0.05, -60 }, + { 0.095, -60 }, + { 0.145, -60 }, + { 0.2, -60 }, + { 0.25, -0.000425679283 }, + { 0.3, 0 }, + { 0.35, 0 }, + { 0.4, 0 }, + { 0.45, 0 }, + { 0.5, 0 }, + { 0.55, 0 }, + { 0.6, 0 }, + { 0.65, 0 }, + { 0.7, 0 }, + { 0.75, 0 }, + { 0.8, 0 }, + { 0.85, 0 }, + { 0.9, 0 }, + { 0.95, 0 }}; + um_s_type exp_dend{{ 0, -65 }, + { 0.05, -60.032677 }, + { 0.095, -60.0308171 }, + { 0.145, -60.028791 }, + { 0.2, -60.0266703 }, + { 0.25, -0.0178456839 }, + { 0.3, -0.0168966758 }, + { 0.35, -0.0162935748 }, + { 0.4, -0.0158862702 }, + { 0.45, -0.0156348508 }, + { 0.5, -0.0155055671 }, + { 0.55, -0.0154673288 }, + { 0.6, -0.0154936751 }, + { 0.65, -0.0155635334 }, + { 0.7, -0.0156609133 }, + { 0.75, -0.015774124 }, + { 0.8, -0.0158948866 }, + { 0.85, -0.0160175185 }, + { 0.9, -0.0161382535 }, + { 0.95, -0.0162547064 },}; + ASSERT_TRUE(testing::seq_eq(u_soma, exp_soma)); + ASSERT_TRUE(testing::seq_eq(u_dend, exp_dend)); +} + +TEST(v_process, clamp_fine) { + auto u_soma = um_s_type{}; + auto u_dend = um_s_type{}; + auto fun = [&u_soma, &u_dend](arb::probe_metadata pm, + std::size_t n, + const arb::sample_record* samples) { + for (std::size_t ix = 0ul; ix < n; ++ix) { + const auto& [t, v] = samples[ix]; + double u = *arb::util::any_cast<const double*>(v); + if (pm.id.index == 0) { + u_soma.push_back({t, u}); + } + else if (pm.id.index == 1) { + u_dend.push_back({t, u}); + } + else { + throw std::runtime_error{"Unexpected probe id"}; + } + } + }; + auto rec = recipe{true, false}; + rec.gprop.default_parameters.discretization = arb::cv_policy_max_extent(0.5); + auto sim = arb::simulation(rec); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05), fun); + sim.run(1.0, 0.005); + + um_s_type exp_soma{{ 0, -65 }, + { 0.05, -42 }, + { 0.095, -42 }, + { 0.145, -42 }, + { 0.2, -42 }, + { 0.25, -42 }, + { 0.3, -42 }, + { 0.35, -42 }, + { 0.4, -42 }, + { 0.45, -42 }, + { 0.5, -42 }, + { 0.55, -42 }, + { 0.6, -42 }, + { 0.65, -42 }, + { 0.7, -42 }, + { 0.75, -42 }, + { 0.8, -42 }, + { 0.85, -42 }, + { 0.9, -42 }, + { 0.95, -42 },}; + um_s_type exp_dend{{ 0, -65 }, + { 0.05, -42.1164544 }, + { 0.095, -42.0915289 }, + { 0.145, -42.0461939 }, + { 0.2, -41.9764002 }, + { 0.25, -0.124099713 }, + { 0.3, -0.0701885048 }, + { 0.35, -0.0519983288 }, + { 0.4, -0.0428578593 }, + { 0.45, -0.0374010627 }, + { 0.5, -0.0338178467 }, + { 0.55, -0.0313183138 }, + { 0.6, -0.0294990809 }, + { 0.65, -0.0281314686 }, + { 0.7, -0.0270760611 }, + { 0.75, -0.0262432877 }, + { 0.8, -0.025573305 }, + { 0.85, -0.0250249014 }, + { 0.9, -0.0245689972 }, + { 0.95, -0.0241846519 },}; + ASSERT_TRUE(testing::seq_eq(u_soma, exp_soma)); + ASSERT_TRUE(testing::seq_eq(u_dend, exp_dend)); +}