diff --git a/src/cell_group.hpp b/src/cell_group.hpp index e70d4b0bb5826b6ccde449f63a7fad6dab29c783..a52dc36d953bc35a557ad0688b9d94f7be46639c 100644 --- a/src/cell_group.hpp +++ b/src/cell_group.hpp @@ -9,20 +9,21 @@ #include <event_queue.hpp> #include <spike.hpp> #include <spike_source.hpp> +//#include <util/singleton.hpp> #include <profiling/profiler.hpp> namespace nest { namespace mc { -template <typename Cell> +template <typename LoweredCell> class cell_group { public: using index_type = cell_gid_type; - using cell_type = Cell; - using value_type = typename cell_type::value_type; - using size_type = typename cell_type::value_type; - using spike_detector_type = spike_detector<Cell>; + using lowered_cell_type = LoweredCell; + using value_type = typename lowered_cell_type::value_type; + using size_type = typename lowered_cell_type::value_type; + using spike_detector_type = spike_detector<lowered_cell_type>; using source_id_type = cell_member_type; using time_type = float; @@ -36,9 +37,13 @@ public: cell_group() = default; cell_group(cell_gid_type gid, const cell& c) : - gid_base_{gid}, cell_{c} + gid_base_{gid} { - initialize_cells(); + detector_handles_.resize(c.detectors().size()); + target_handles_.resize(c.synapses().size()); + probe_handles_.resize(c.probes().size()); + + cell_.initialize(util::singleton_view(c), detector_handles_, target_handles_, probe_handles_); // Create spike detectors and associate them with globally unique source ids, // as specified by cell gid and cell-local zero-based index. @@ -46,18 +51,19 @@ public: cell_gid_type source_gid = gid_base_; cell_lid_type source_lid = 0u; + unsigned i = 0; for (auto& d : c.detectors()) { cell_member_type source_id{source_gid, source_lid++}; spike_sources_.push_back({ - source_id, spike_detector_type(cell_, d.location, d.threshold, 0.f) + source_id, spike_detector_type(cell_, detector_handles_[i], d.threshold, 0.f) }); } } void reset() { remove_samplers(); - initialize_cells(); + cell_.reset(); for (auto& spike_source: spike_sources_) { spike_source.source.reset(cell_, 0.f); } @@ -70,11 +76,10 @@ public: PE("sampling"); while (auto m = sample_events_.pop_if_before(cell_time)) { - auto& sampler_spec = samplers_[m->sampler_index]; - EXPECTS((bool)sampler_spec.sampler); + auto& s = samplers_[m->sampler_index]; + EXPECTS((bool)s.sampler); + auto next = s.sampler(cell_.time(), cell_.probe(s.handle)); - index_type probe_index = sampler_spec.probe_id.index; - auto next = sampler_spec.sampler(cell_.time(), cell_.probe(probe_index)); if (next) { m->time = std::max(*next, cell_time); sample_events_.push(*m); @@ -105,12 +110,14 @@ public: // apply events if (next) { - cell_.apply_event(next.get()); + auto handle = target_handles_[next->target.index]; + cell_.deliver_event(handle, next->weight); // apply events that are due within some epsilon of the current // time step. This should be a parameter. e.g. with for variable // order time stepping, use the minimum possible time step size. while(auto e = events_.pop_if_before(cell_.time()+dt/10.)) { - cell_.apply_event(e.get()); + auto handle = target_handles_[e->target.index]; + cell_.deliver_event(handle, e->weight); } } PL(); @@ -128,9 +135,6 @@ public: const std::vector<spike<source_id_type, time_type>>& spikes() const { return spikes_; } - cell_type& cell() { return cell_; } - const cell_type& cell() const { return cell_; } - const std::vector<spike_source_type>& spike_sources() const { return spike_sources_; @@ -141,8 +145,9 @@ public: } void add_sampler(cell_member_type probe_id, sampler_function s, time_type start_time = 0) { + EXPECTS(probe_id.gid==gid_base_); auto sampler_index = uint32_t(samplers_.size()); - samplers_.push_back({probe_id, s}); + samplers_.push_back({probe_handles_[probe_id.index], s}); sample_events_.push({sampler_index, start_time}); } @@ -151,22 +156,21 @@ public: samplers_.clear(); } -private: - void initialize_cells() { - cell_.voltage()(memory::all) = -65.; - cell_.initialize(); + value_type probe(cell_member_type probe_id) const { + return cell_.probe(probe_handles_[probe_id.index]); } +private: /// gid of first cell in group cell_gid_type gid_base_; /// the lowered cell state (e.g. FVM) of the cell - cell_type cell_; + lowered_cell_type cell_; /// spike detectors attached to the cell std::vector<spike_source_type> spike_sources_; - //. spikes that are generated + /// spikes that are generated std::vector<spike<source_id_type, time_type>> spikes_; /// pending events to be delivered @@ -178,11 +182,18 @@ private: /// the global id of the first target (e.g. a synapse) in this group index_type first_target_gid_; - /// the global id of the first probe in this group - index_type first_probe_gid_; + /// handles for accessing lowered cell + using detector_handle = typename lowered_cell_type::detector_handle; + std::vector<detector_handle> detector_handles_; + + using target_handle = typename lowered_cell_type::target_handle; + std::vector<target_handle> target_handles_; + + using probe_handle = typename lowered_cell_type::probe_handle; + std::vector<probe_handle> probe_handles_; struct sampler_entry { - cell_member_type probe_id; + typename lowered_cell_type::probe_handle handle; sampler_function sampler; }; diff --git a/src/compartment.hpp b/src/compartment.hpp index da6bbcf57298593a60f7510822832060c7a89846..326563ad4261bd6d3e5ce0b516c3aa8808c893a9 100644 --- a/src/compartment.hpp +++ b/src/compartment.hpp @@ -3,7 +3,10 @@ #include <iterator> #include <utility> -#include "common_types.hpp" +#include <common_types.hpp> +#include <util/counter.hpp> +#include <util/span.hpp> +#include <util/transform.hpp> namespace nest { namespace mc { @@ -34,6 +37,7 @@ struct compartment { value_type length; }; + /// The simplest type of compartment iterator : /// - divide a segment into n compartments of equal length /// - assume that the radius varies linearly from one end of the segment @@ -161,6 +165,43 @@ public: real_type length_; }; +// (NB: auto type deduction and lambda in C++14 will simplify the following) + +template <typename size_type, typename real_type> +class compartment_maker { +public: + compartment_maker(size_type n, real_type length, real_type rL, real_type rR): + r0_{rL}, + dr_{(rR-rL)/n}, + dx_{length/n} + {} + + compartment operator()(size_type i) const { + return compartment(i, dx_, r0_+i*dr_, r0_+(i+1)*dr_); + } + +private: + real_type r0_; + real_type dr_; + real_type dx_; +}; + +template <typename size_type, typename real_type> +using compartment_iterator_bis = + util::transform_iterator<util::counter<size_type>, compartment_maker<size_type, real_type>>; + +template <typename size_type, typename real_type> +util::range<compartment_iterator_bis<size_type, real_type>> make_compartment_range( + size_type num_compartments, + real_type radius_L, + real_type radius_R, + real_type length) +{ + return util::transform_view( + util::span<size_type>(0, num_compartments), + compartment_maker<size_type, real_type>(num_compartments, length, radius_L, radius_R)); +} + } // namespace mc } // namespace nest diff --git a/src/fvm_cell.hpp b/src/fvm_cell.hpp index 526fd22a391b477d5af3c88a4535b4721c83cbfe..5335c8129a5963d8d288094116442c431279a2f7 100644 --- a/src/fvm_cell.hpp +++ b/src/fvm_cell.hpp @@ -1,6 +1,7 @@ #pragma once #include <algorithm> +#include <iterator> #include <map> #include <set> #include <string> @@ -14,27 +15,155 @@ #include <matrix.hpp> #include <mechanism.hpp> #include <mechanism_catalogue.hpp> +#include <profiling/profiler.hpp> #include <segment.hpp> #include <stimulus.hpp> #include <util.hpp> -#include <profiling/profiler.hpp> +#include <util/meta.hpp> #include <vector/include/Vector.hpp> +/* + * Lowered cell implementation based on finite volume method. + * + * TODO: Move following description of internal API + * to a better location or document. + * + * Lowered cells are managed by the `cell_group` class. + * A `cell_group` manages one lowered cell instance, which + * may in turn simulate one or more cells. + * + * The outward facing interface for `cell_group` is defined + * in terms of cell gids and member indices; the interface + * between `cell_group` and a lowered cell is described below. + * + * The motivation for the following interface is to + * 1. Provide more flexibility in lowered cell implementation + * 2. Save memory and duplicated index maps by associating + * externally visible objects with opaque handles. + * + * In the following, `lowered_cell` represents the lowered + * cell class, and `lowered` an instance thereof. + * + * `lowered_cell::detector_handle` + * Type of handles used by spike detectors to query + * membrane voltage. + * + * `lowered_cell::probe_handle` + * Type of handle used to query the value of a probe. + * + * `lowered_cell::target_handle` + * Type of handle used to identify the target of a + * postsynaptic spike event. + * + * `lowered_cell::value_type` + * Floating point type used for internal states + * and simulation time. + * + * `lowered_cell()` + * Default constructor; performs no cell-specific + * initialization. + * + * `lowered.initialize(const Cells& cells, ...)` + * Allocate and initalize data structures to simulate + * the cells described in the collection `cells`, + * where each item is of type `nest::mc::cell`. + * + * Remaining arguments consist of references to + * collections for storing: + * 1. Detector handles + * 2. Target handles + * 3. Probe handles + * + * Handles are written in the same order as they + * appear in the provided cell descriptions. + * + * `lowered.reset()` + * + * Resets state to initial conditiions and sets + * internal simulation time to 0. + * + * `lowered.advance(value_type dt)` + * + * Advanece simulation state by `dt` (value in + * milliseconds). For `fvm_cell` at least, + * this corresponds to one integration step. + * + * `lowered.deliver_event(target_handle target, value_type weight)` + * + * Update target-specifc state based on the arrival + * of a postsynaptic spike event with given weight. + * + * `lowered.detect_voltage(detector_handle)` + * + * Return membrane voltage at detector site as specified + * by handle. + * + * `lowered.probe(probe_handle)` + * + * Return value of corresponding probe. + * + * `lowered.resting_potential(value_type potential)` + * + * Set the steady-state membrane voltage used for the + * cell initial condition. (Defaults to -65 mV currently.) + */ + namespace nest { namespace mc { namespace fvm { -template <typename T, typename I> +template <typename Value, typename Index> class fvm_cell { public: - fvm_cell() = default; /// the real number type - using value_type = T; + using value_type = Value; + /// the integral index type - using size_type = I; + using size_type = Index; + + /// the container used for indexes + using index_type = memory::HostVector<size_type>; + + /// the container used for values + using vector_type = memory::HostVector<value_type>; + + /// API for cell_group (see above): + + using detector_handle = size_type; + using target_handle = std::pair<size_type, size_type>; + using probe_handle = std::pair<const vector_type fvm_cell::*, size_type>; + + void resting_potential(value_type potential_mV) { + resting_potential_ = potential_mV; + } + + template <typename Cells, typename Detectors, typename Targets, typename Probes> + void initialize( + const Cells& cells, // collection of nest::mc::cell descriptions + Detectors& detector_handles, // (write) where to store detector handles + Targets& target_handles, // (write) where to store target handles + Probes& probe_handles); // (write) where to store probe handles + + void reset(); + + void deliver_event(target_handle h, value_type weight) { + mechanisms_[synapse_base_+h.first]->net_receive(h.second, weight); + } + + value_type detector_voltage(detector_handle h) const { + return voltage_[h]; // detector_handle is just the compartment index + } + + value_type probe(probe_handle h) const { + return (this->*h.first)[h.second]; + } + + void advance(value_type dt); + + /// Following types and methods are public only for testing: /// the type used to store matrix information using matrix_type = matrix<value_type, size_type>; @@ -46,21 +175,14 @@ public: /// ion species storage using ion_type = mechanisms::ion<value_type, size_type>; - /// the container used for indexes - using index_type = memory::HostVector<size_type>; /// view into index container using index_view = typename index_type::view_type; using const_index_view = typename index_type::const_view_type; - /// the container used for values - using vector_type = memory::HostVector<value_type>; /// view into value container using vector_view = typename vector_type::view_type; using const_vector_view = typename vector_type::const_view_type; - /// constructor - fvm_cell(nest::mc::cell const& cell); - /// build the matrix for a given time step void setup_matrix(value_type dt); @@ -88,6 +210,10 @@ public: vector_view voltage() { return voltage_; } const_vector_view voltage() const { return voltage_; } + /// return the current in each CV + vector_view current() { return current_; } + const_vector_view current() const { return current_; } + std::size_t size() const { return matrix_.size(); } /// return reference to in iterable container of the mechanisms @@ -109,28 +235,6 @@ public: ion_type& ion_k() { return ions_[mechanisms::ionKind::k]; } ion_type const& ion_k() const { return ions_[mechanisms::ionKind::k]; } - /// make a time step - void advance(value_type dt); - - /// pass an event to the appropriate synapse and call net_receive - template <typename Time> - void apply_event(postsynaptic_spike_event<Time> e) { - mechanisms_[synapse_index_]->net_receive(e.target.index, e.weight); - } - - mechanism_type& synapses() { - return mechanisms_[synapse_index_]; - } - - /// set initial states - void initialize(); - - /// returns the compartment index of a segment location - int compartment_index(segment_location loc) const; - - /// returns voltage at a segment location - value_type voltage(segment_location loc) const; - /// flags if solution is physically realistic. /// here we define physically realistic as the voltage being within reasonable bounds. /// use a simple test of the voltage at the soma is reasonable, i.e. in the range @@ -140,51 +244,43 @@ public: return (v>-1000.) && (v<1000.); } - /// returns current at a segment location - value_type current(segment_location loc) const; - - value_type time() const { return t_; } - value_type probe(uint32_t i) const { - auto p = probes_[i]; - return (this->*p.first)[p.second]; - } - std::size_t num_probes() const { return probes_.size(); } private: - - /// current time + /// current time [ms] value_type t_ = value_type{0}; + /// resting potential (initial voltage condition) + value_type resting_potential_ = -65; + /// the linear system for implicit time stepping of cell state matrix_type matrix_; /// index for fast lookup of compartment index ranges of segments index_type segment_index_; - /// cv_areas_[i] is the surface area of CV i + /// cv_areas_[i] is the surface area of CV i [µm^2] vector_type cv_areas_; /// alpha_[i] is the following value at the CV face between /// CV i and its parent, required when constructing linear system /// face_alpha_[i] = area_face / (c_m * r_L * delta_x); - vector_type face_alpha_; + vector_type face_alpha_; // [µm·m^2/cm/s ≡ 10^5 µm^2/ms] - /// cv_capacitance_[i] is the capacitance of CV i per unit area (i.e. c_m) + /// cv_capacitance_[i] is the capacitance of CV i per unit area (i.e. c_m) [F/m^2] vector_type cv_capacitance_; - /// the average current over the surface of each CV + /// the average current density over the surface of each CV [mA/cm^2] /// current_ = i_m - i_e - /// so the total current over the surface of CV i is - /// current_[i] * cv_areas_ vector_type current_; - /// the potential in mV in each CV + /// the potential in each CV [mV] vector_type voltage_; - std::size_t synapse_index_; // synapses at the end of mechanisms_, from here + /// Where point mechanisms start in the mechanisms_ list. + std::size_t synapse_base_; /// the set of mechanisms present in the cell std::vector<mechanism_type> mechanisms_; @@ -197,7 +293,7 @@ private: std::vector<std::pair<const vector_type fvm_cell::*, uint32_t>> probes_; // mechanism factory - using mechanism_catalogue = nest::mc::mechanisms::catalogue<T, I>; + using mechanism_catalogue = nest::mc::mechanisms::catalogue<value_type, size_type>; }; //////////////////////////////////////////////////////////////////////////////// @@ -205,23 +301,35 @@ private: //////////////////////////////////////////////////////////////////////////////// template <typename T, typename I> -fvm_cell<T, I>::fvm_cell(nest::mc::cell const& cell) -: cv_areas_ {cell.num_compartments(), T(0)} -, face_alpha_ {cell.num_compartments(), T(0)} -, cv_capacitance_{cell.num_compartments(), T(0)} -, current_ {cell.num_compartments(), T(0)} -, voltage_ {cell.num_compartments(), T(0)} +template <typename Cells, typename Detectors, typename Targets, typename Probes> +void fvm_cell<T, I>::initialize( + const Cells& cells, + Detectors& detector_handles, + Targets& target_handles, + Probes& probe_handles) { + if (util::size(cells)!=1u) { + throw std::invalid_argument("fvm_cell accepts only one cell"); + } + + const nest::mc::cell& cell = *(std::begin(cells)); + size_type ncomp = cell.num_compartments(); + + // confirm write-parameters have enough room to store handles + EXPECTS(util::size(detector_handles)==cell.detectors().size()); + EXPECTS(util::size(target_handles)==cell.synapses().size()); + EXPECTS(util::size(probe_handles)==cell.probes().size()); + + // initialize storage + cv_areas_ = vector_type{ncomp, T{0}}; + face_alpha_ = vector_type{ncomp, T{0}}; + cv_capacitance_ = vector_type{ncomp, T{0}}; + current_ = vector_type{ncomp, T{0}}; + voltage_ = vector_type{ncomp, T{0}}; + using util::left; using util::right; - // TODO: potential code stink - // matrix_ is a member, but it is not initialized with the other members - // above because it requires the parent_index, which is calculated - // "on the fly" by cell.model(). - // cell.model() is quite expensive, and the information it calculates is - // used elsewhere, so we defer the intialization to inside the constructor - // body. const auto graph = cell.model(); matrix_ = matrix_type(graph.parent_index); @@ -313,8 +421,6 @@ fvm_cell<T, I>::fvm_cell(nest::mc::cell const& cell) // TODO : this works well for density mechanisms (e.g. ion channels), but // does it work for point processes (e.g. synapses)? for (auto& mech : mech_map) { - //auto& helper = nest::mc::mechanisms::get_mechanism_helper(mech.first); - // calculate the number of compartments that contain the mechanism auto num_comp = 0u; for (auto seg : mech.second) { @@ -338,22 +444,37 @@ fvm_cell<T, I>::fvm_cell(nest::mc::cell const& cell) // instantiate the mechanism mechanisms_.push_back( mechanism_catalogue::make(mech.first, voltage_, current_, compartment_index) - //helper->new_mechanism(voltage_, current_, compartment_index) ); } - synapse_index_ = mechanisms_.size(); + synapse_base_ = mechanisms_.size(); + + // Create the synapse mechanism implementations together with the target handles + std::vector<std::vector<cell_lid_type>> syn_mech_map; + std::map<std::string, std::size_t> syn_mech_indices; + auto target_hi = target_handles.begin(); - std::map<std::string, std::vector<cell_lid_type>> syn_map; for (const auto& syn : cell.synapses()) { - syn_map[syn.mechanism.name()].push_back(find_compartment_index(syn.location, graph)); + const auto& name = syn.mechanism.name(); + std::size_t index = 0; + if (syn_mech_indices.count(name)==0) { + index = syn_mech_map.size(); + syn_mech_indices[name] = index; + syn_mech_map.push_back(std::vector<cell_lid_type>{}); + } + else { + index = syn_mech_indices[name]; + } + + size_type comp = find_compartment_index(syn.location, graph); + *target_hi++ = target_handle{index, syn_mech_map[index].size()}; + syn_mech_map[index].push_back(comp); } - for (const auto& syni : syn_map) { + for (const auto& syni : syn_mech_indices) { const auto& mech_name = syni.first; - // auto& helper = nest::mc::mechanisms::get_mechanism_helper(mech_name); - index_type compartment_index(syni.second); + index_type compartment_index(syn_mech_map[syni.second]); auto mech = mechanism_catalogue::make(mech_name, voltage_, current_, compartment_index); mech->set_areas(cv_areas_); mechanisms_.push_back(std::move(mech)); @@ -417,24 +538,35 @@ fvm_cell<T, I>::fvm_cell(nest::mc::cell const& cell) } // record probe locations by index into corresponding state vector + auto probe_hi = probe_handles.begin(); for (auto probe : cell.probes()) { - uint32_t comp = find_compartment_index(probe.location, graph); + auto comp = find_compartment_index(probe.location, graph); switch (probe.kind) { - case probeKind::membrane_voltage: - probes_.push_back({&fvm_cell::voltage_, comp}); - break; - case probeKind::membrane_current: - probes_.push_back({&fvm_cell::current_, comp}); - break; - default: - throw std::logic_error("unrecognized probeKind"); + case probeKind::membrane_voltage: + *probe_hi = {&fvm_cell::voltage_, comp}; + break; + case probeKind::membrane_current: + *probe_hi = {&fvm_cell::current_, comp}; + break; + default: + throw std::logic_error("unrecognized probeKind"); } + ++probe_hi; } + + // detector handles are just their corresponding compartment indices + auto detector_hi = detector_handles.begin(); + for (auto detector : cell.detectors()) { + auto comp = find_compartment_index(detector.location, graph); + *detector_hi++ = comp; + } + + // initialise mechanism and voltage state + reset(); } template <typename T, typename I> -void fvm_cell<T, I>::setup_matrix(T dt) -{ +void fvm_cell<T, I>::setup_matrix(T dt) { using memory::all; // convenience accesors to matrix storage @@ -456,9 +588,9 @@ void fvm_cell<T, I>::setup_matrix(T dt) // . . . // l[i] . . d[i] // - //d(all) = 1.0; - d(all) = cv_areas_; - for(auto i=1u; i<d.size(); ++i) { + + d(all) = cv_areas_; // [µm^2] + for (auto i=1u; i<d.size(); ++i) { auto a = 1e5*dt * face_alpha_[i]; d[i] += a; @@ -471,50 +603,23 @@ void fvm_cell<T, I>::setup_matrix(T dt) // the RHS of the linear system is // V[i] - dt/cm*(im - ie) - auto factor = 10.*dt; + auto factor = 10.*dt; // units: 10·ms/(F/m^2)·(mA/cm^2) ≡ mV for(auto i=0u; i<d.size(); ++i) { - //rhs[i] = voltage_[i] - factor/cv_capacitance_[i]*current_[i]; rhs[i] = cv_areas_[i]*(voltage_[i] - factor/cv_capacitance_[i]*current_[i]); } } -template <typename T, typename I> -int fvm_cell<T, I>::compartment_index(segment_location loc) const -{ - EXPECTS(unsigned(loc.segment) < segment_index_.size()); - - const auto seg = loc.segment; - - auto first = segment_index_[seg]; - auto n = segment_index_[seg+1] - first; - auto index = std::floor(n*loc.position); - return index<n ? first+index : first+n-1; -} template <typename T, typename I> -T fvm_cell<T, I>::voltage(segment_location loc) const -{ - return voltage_[compartment_index(loc)]; -} - -template <typename T, typename I> -T fvm_cell<T, I>::current(segment_location loc) const -{ - return current_[compartment_index(loc)]; -} - -template <typename T, typename I> -void fvm_cell<T, I>::initialize() -{ +void fvm_cell<T, I>::reset() { + voltage_(memory::all) = resting_potential_; t_ = 0.; - - for(auto& m : mechanisms_) { + for (auto& m : mechanisms_) { m->nrn_init(); } } template <typename T, typename I> -void fvm_cell<T, I>::advance(T dt) -{ +void fvm_cell<T, I>::advance(T dt) { using memory::all; PE("current"); @@ -529,17 +634,18 @@ void fvm_cell<T, I>::advance(T dt) } // add current contributions from stimulii - for(auto& stim : stimulii_) { - auto ie = stim.second.amplitude(t_); + for (auto& stim : stimulii_) { + auto ie = stim.second.amplitude(t_); // [nA] auto loc = stim.first; - // the factor of 100 scales the injected current to 10^2.nA - current_[loc] -= 100.*ie/cv_areas_[loc]; + // note: current_ in [mA/cm^2], ie in [nA], cv_areas_ in [µm^2]. + // unit scale factor: [nA/µm^2]/[mA/cm^2] = 100 + current_[loc] -= 100*ie/cv_areas_[loc]; } PL(); - PE("matrix", "setup"); // solve the linear system + PE("matrix", "setup"); setup_matrix(dt); PL(); PE("solve"); matrix_.solve(); @@ -547,8 +653,8 @@ void fvm_cell<T, I>::advance(T dt) voltage_(all) = matrix_.rhs(); PL(); - PE("state"); // integrate state of gating variables etc. + PE("state"); for(auto& m : mechanisms_) { PE(m->name().c_str()); m->nrn_state(); @@ -563,4 +669,3 @@ void fvm_cell<T, I>::advance(T dt) } // namespace mc } // namespace nest - diff --git a/src/fvm_multicell.hpp b/src/fvm_multicell.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e7bc12bb3bcb05242438c118249daca3a31527df --- /dev/null +++ b/src/fvm_multicell.hpp @@ -0,0 +1,691 @@ +#pragma once + +#include <algorithm> +#include <iterator> +#include <map> +#include <set> +#include <string> +#include <vector> + +#include <algorithms.hpp> +#include <cell.hpp> +#include <event_queue.hpp> +#include <ion.hpp> +#include <math.hpp> +#include <matrix.hpp> +#include <mechanism.hpp> +#include <mechanism_catalogue.hpp> +#include <profiling/profiler.hpp> +#include <segment.hpp> +#include <stimulus.hpp> +#include <util.hpp> +#include <util/meta.hpp> + +#include <vector/include/Vector.hpp> + +namespace nest { +namespace mc { +namespace fvm { + +template <typename Value, typename Index> +class fvm_cell { +public: + fvm_cell() = default; + + /// the real number type + using value_type = Value; + + /// the integral index type + using size_type = Index; + + /// the container used for indexes + using index_type = memory::HostVector<size_type>; + + /// the container used for values + using vector_type = memory::HostVector<value_type>; + + /// API for cell_group (see above): + + using detector_handle = size_type; + using target_handle = std::pair<size_type, size_type>; + using probe_handle = std::pair<const vector_type fvm_cell::*, size_type>; + + void resting_potential(value_type potential_mV) { + resting_potential_ = potential_mV; + } + + template <typename Cells, typename Detectors, typename Targets, typename Probes> + void initialize( + const Cells& cells, // collection of nest::mc::cell descriptions + Detectors& detector_handles, // (write) where to store detector handles + Targets& target_handles, // (write) where to store target handles + Probes& probe_handles); // (write) where to store probe handles + + void reset(); + + void deliver_event(target_handle h, value_type weight) { + mechanisms_[synapse_base_+h.first]->net_receive(h.second, weight); + } + + value_type detector_voltage(detector_handle h) const { + return voltage_[h]; // detector_handle is just the compartment index + } + + value_type probe(probe_handle h) const { + return (this->*h.first)[h.second]; + } + + void advance(value_type dt); + + /// Following types and methods are public only for testing: + + /// the type used to store matrix information + using matrix_type = matrix<value_type, size_type>; + + /// mechanism type + using mechanism_type = + nest::mc::mechanisms::mechanism_ptr<value_type, size_type>; + + /// ion species storage + using ion_type = mechanisms::ion<value_type, size_type>; + + /// view into index container + using index_view = typename index_type::view_type; + using const_index_view = typename index_type::const_view_type; + + /// view into value container + using vector_view = typename vector_type::view_type; + using const_vector_view = typename vector_type::const_view_type; + + /// build the matrix for a given time step + void setup_matrix(value_type dt); + + /// which requires const_view in the vector library + const matrix_type& jacobian() { + return matrix_; + } + + /// return list of CV areas in : + /// um^2 + /// 1e-6.mm^2 + /// 1e-8.cm^2 + const_vector_view cv_areas() const { + return cv_areas_; + } + + /// return the capacitance of each CV surface + /// this is the total capacitance, not per unit area, + /// i.e. equivalent to sigma_i * c_m + const_vector_view cv_capacitance() const { + return cv_capacitance_; + } + + /// return the voltage in each CV + vector_view voltage() { return voltage_; } + const_vector_view voltage() const { return voltage_; } + + /// return the current in each CV + vector_view current() { return current_; } + const_vector_view current() const { return current_; } + + std::size_t size() const { return matrix_.size(); } + + /// return reference to in iterable container of the mechanisms + std::vector<mechanism_type>& mechanisms() { return mechanisms_; } + + /// return reference to list of ions + std::map<mechanisms::ionKind, ion_type>& ions() { return ions_; } + std::map<mechanisms::ionKind, ion_type> const& ions() const { return ions_; } + + /// return reference to sodium ion + ion_type& ion_na() { return ions_[mechanisms::ionKind::na]; } + ion_type const& ion_na() const { return ions_[mechanisms::ionKind::na]; } + + /// return reference to calcium ion + ion_type& ion_ca() { return ions_[mechanisms::ionKind::ca]; } + ion_type const& ion_ca() const { return ions_[mechanisms::ionKind::ca]; } + + /// return reference to pottasium ion + ion_type& ion_k() { return ions_[mechanisms::ionKind::k]; } + ion_type const& ion_k() const { return ions_[mechanisms::ionKind::k]; } + + /// flags if solution is physically realistic. + /// here we define physically realistic as the voltage being within reasonable bounds. + /// use a simple test of the voltage at the soma is reasonable, i.e. in the range + /// v_soma \in (-1000mv, 1000mv) + bool is_physical_solution() const { + auto v = voltage_[0]; + return (v>-1000.) && (v<1000.); + } + + value_type time() const { return t_; } + + std::size_t num_probes() const { return probes_.size(); } + +private: + /// current time [ms] + value_type t_ = value_type{0}; + + /// resting potential (initial voltage condition) + value_type resting_potential_ = -65; + + /// the linear system for implicit time stepping of cell state + matrix_type matrix_; + + /// index for fast lookup of compartment index ranges of segments + index_type segment_index_; + + /// cv_areas_[i] is the surface area of CV i [µm^2] + vector_type cv_areas_; + + /// alpha_[i] is the following value at the CV face between + /// CV i and its parent, required when constructing linear system + /// face_alpha_[i] = area_face / (c_m * r_L * delta_x); + vector_type face_alpha_; // [µm·m^2/cm/s ≡ 10^5 µm^2/ms] + + /// cv_capacitance_[i] is the capacitance of CV i per unit area (i.e. c_m) [F/m^2] + vector_type cv_capacitance_; + + /// the average current density over the surface of each CV [mA/cm^2] + /// current_ = i_m - i_e + vector_type current_; + + /// the potential in each CV [mV] + vector_type voltage_; + + /// Where point mechanisms start in the mechanisms_ list. + std::size_t synapse_base_; + + /// the set of mechanisms present in the cell + std::vector<mechanism_type> mechanisms_; + + /// the ion species + std::map<mechanisms::ionKind, ion_type> ions_; + + std::vector<std::pair<uint32_t, i_clamp>> stimulii_; + + std::vector<std::pair<const vector_type fvm_cell::*, uint32_t>> probes_; + + // mechanism factory + using mechanism_catalogue = nest::mc::mechanisms::catalogue<value_type, size_type>; + + // perform area and capacitance calculation on initialization + void compute_cv_area_capacitance( + std::pair<size_type, size_type> comps, const segment& seg, index_vector &parent); +}; + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////// Implementation //////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +template <typename T, typename I> +void fvm_cell<T, I>::compute_cv_area_capacitance( + std::pair<size_type, size_type> comps, + const segment& seg, + index_vector &parent) +{ + // precondition: group_parent_index[j] holds the correct value for + // j in [base_comp, base_comp+segment.num_compartments()]. + + if (auto soma = seg->as_soma()) { + // confirm assumption that there is one compartment in soma + if (comps.size()!=1) + throw std::logic_error("soma allocated more than one compartment"); + } + auto i = comps.first; + auto area = math::area_sphere(soma->radius()); + + cv_areas_[i] += area; + cv_capacitance_[i] += area * soma->mechanism("membrane").get("c_m").value; + } + else if (auto cable = s->as_cable()) { + // loop over each compartment in the cable + // each compartment has the face between two CVs at its centre + // the centers of the CVs are the end points of the compartment + // + // __________________________________ + // | ........ | .cvleft. | cv | + // | ........ L ........ C R + // |__________|__________|__________| + // + // The compartment has end points marked L and R (left and right). + // The left compartment is assumed to be closer to the soma + // (i.e. it follows the minimal degree ordering) + // The face is at the center, marked C. + // The full control volume to the left (marked with .) + + auto c_m = cable->mechanism("membrane").get("c_m").value; + auto r_L = cable->mechanism("membrane").get("r_L").value; + const auto& compartments = cable->compartments(); + + EXPECTS(util::size(compartments)==comps.second-comps.first); + + for (auto i: util::make_span(comps)) { + const auto c& = compartments[i-comps.first]; + auto j = parent[i]; + + auto radius_center = math::mean(c.radius); + auto area_face = math::area_circle(radius_center); + face_alpha_[i] = area_face / (c_m * r_L * c.length); + + auto halflen = c.length/2; + auto al = math::area_frustrum(halflen, left(c.radius), radius_center); + auto ar = math::area_frustrum(halflen, right(c.radius), radius_center); + + cv_areas_[j] += al; + cv_areas_[i] += ar; + cv_capacitance_[j] += al * c_m; + cv_capacitance_[i] += ar * c_m; + } + } + else { + throw std::domain_error("FVM lowering encountered unsuported segment type"); + } +} + + +template <typename T, typename I> +template <typename Cells, typename Detectors, typename Targets, typename Probes> +void fvm_cell<T, I>::initialize( + const Cells& cells, + Detectors& detector_handles, + Targets& target_handles, + Probes& probe_handles) +{ + using util::transform_view; + using util::make_partition; + using util::make_span; + using util::size; + + auto cell_num_compartments = + transform_view(cells, [](const cell& c) { return c.num_compartments(); }); + + std::vector<cell_lid_type> cell_comp_bounds; + auto cell_comp_part = make_partition(cell_comp_bounds, cell_num_compartments); + auto ncomp = cell_comp_part.bounds().second; + + index_vector group_parent_index{cell_comp_part.bounds().second, 0}; + + auto comps = cell_comp_part.begin(); + for (const auto& c: cells) { + auto parent_index = c.model().parent_index; + + for (auto j: make_span(*comps)) { + group_parent_index[j] = parent_index[j-comps->first]+comps->first; + } + + auto seg_num_compartments = + transform_view(c.segments(), [](const segment& s) { return s.num_compartments(); }); + + std::vector<cell_lid_type> seg_comp_bounds; + auto seg_comp_part = make_partition(seg_comp_bounds, seg_num_compartments, comps->first); + + auto seg_comps = seg_comp_part.begin(); + for (auto j=0; j<size(seg_comp_part); ++j) { + calculate_cv_area_capacitance( + } + + ++comps; + } + + + + + + const nest::mc::cell& cell = *(std::begin(cells)); + size_type ncomp = cell.num_compartments(); + + // confirm write-parameters have enough room to store handles + EXPECTS(util::size(detector_handles)==cell.detectors().size()); + EXPECTS(util::size(target_handles)==cell.synapses().size()); + EXPECTS(util::size(probe_handles)==cell.probes().size()); + + // initialize storage + cv_areas_ = vector_type{ncomp, T{0}}; + face_alpha_ = vector_type{ncomp, T{0}}; + cv_capacitance_ = vector_type{ncomp, T{0}}; + current_ = vector_type{ncomp, T{0}}; + voltage_ = vector_type{ncomp, T{0}}; + + using util::left; + using util::right; + + const auto graph = cell.model(); + matrix_ = matrix_type(graph.parent_index); + + auto parent_index = matrix_.p(); + segment_index_ = graph.segment_index; + + auto seg_idx = 0; + for(auto const& s : cell.segments()) { + if(auto soma = s->as_soma()) { + // assert the assumption that the soma is at 0 + if(seg_idx!=0) { + throw std::domain_error( + "FVM lowering encountered soma with non-zero index" + ); + } + auto area = math::area_sphere(soma->radius()); + cv_areas_[0] += area; + cv_capacitance_[0] += area * soma->mechanism("membrane").get("c_m").value; + } + else if(auto cable = s->as_cable()) { + // loop over each compartment in the cable + // each compartment has the face between two CVs at its centre + // the centers of the CVs are the end points of the compartment + // + // __________________________________ + // | ........ | .cvleft. | cv | + // | ........ L ........ C R + // |__________|__________|__________| + // + // The compartment has end points marked L and R (left and right). + // The left compartment is assumed to be closer to the soma + // (i.e. it follows the minimal degree ordering) + // The face is at the center, marked C. + // The full control volume to the left (marked with .) + auto c_m = cable->mechanism("membrane").get("c_m").value; + auto r_L = cable->mechanism("membrane").get("r_L").value; + for(auto c : cable->compartments()) { + auto i = segment_index_[seg_idx] + c.index; + auto j = parent_index[i]; + + auto radius_center = math::mean(c.radius); + auto area_face = math::area_circle( radius_center ); + face_alpha_[i] = area_face / (c_m * r_L * c.length); + + auto halflen = c.length/2; + + auto al = math::area_frustrum(halflen, left(c.radius), radius_center); + auto ar = math::area_frustrum(halflen, right(c.radius), radius_center); + cv_areas_[j] += al; + cv_areas_[i] += ar; + cv_capacitance_[j] += al * c_m; + cv_capacitance_[i] += ar * c_m; + } + } + else { + throw std::domain_error("FVM lowering encountered unsuported segment type"); + } + ++seg_idx; + } + + // normalize the capacitance by cv_area + for(auto i=0u; i<size(); ++i) { + cv_capacitance_[i] /= cv_areas_[i]; + } + + ///////////////////////////////////////////// + // create mechanisms + ///////////////////////////////////////////// + + // FIXME : candidate for a private member function + + // for each mechanism in the cell record the indexes of the segments that + // contain the mechanism + std::map<std::string, std::vector<unsigned>> mech_map; + + for (unsigned i=0; i<cell.num_segments(); ++i) { + for (const auto& mech : cell.segment(i)->mechanisms()) { + // FIXME : Membrane has to be a proper mechanism, + // because it is exposed via the public interface. + // This if statement is bad + if(mech.name() != "membrane") { + mech_map[mech.name()].push_back(i); + } + } + } + + // Create the mechanism implementations with the state for each mechanism + // instance. + // TODO : this works well for density mechanisms (e.g. ion channels), but + // does it work for point processes (e.g. synapses)? + for (auto& mech : mech_map) { + // calculate the number of compartments that contain the mechanism + auto num_comp = 0u; + for (auto seg : mech.second) { + num_comp += segment_index_[seg+1] - segment_index_[seg]; + } + + // build a vector of the indexes of the compartments that contain + // the mechanism + index_type compartment_index(num_comp); + auto pos = 0u; + for (auto seg : mech.second) { + auto seg_size = segment_index_[seg+1] - segment_index_[seg]; + std::iota( + compartment_index.data() + pos, + compartment_index.data() + pos + seg_size, + segment_index_[seg] + ); + pos += seg_size; + } + + // instantiate the mechanism + mechanisms_.push_back( + mechanism_catalogue::make(mech.first, voltage_, current_, compartment_index) + ); + } + + synapse_base_ = mechanisms_.size(); + + // Create the synapse mechanism implementations together with the target handles + std::vector<std::vector<cell_lid_type>> syn_mech_map; + std::map<std::string, std::size_t> syn_mech_indices; + auto target_hi = target_handles.begin(); + + for (const auto& syn : cell.synapses()) { + const auto& name = syn.mechanism.name(); + std::size_t index = 0; + if (syn_mech_indices.count(name)==0) { + index = syn_mech_map.size(); + syn_mech_indices[name] = index; + syn_mech_map.push_back(std::vector<cell_lid_type>{}); + } + else { + index = syn_mech_indices[name]; + } + + size_type comp = find_compartment_index(syn.location, graph); + *target_hi++ = target_handle{index, syn_mech_map[index].size()}; + syn_mech_map[index].push_back(comp); + } + + for (const auto& syni : syn_mech_indices) { + const auto& mech_name = syni.first; + + index_type compartment_index(syn_mech_map[syni.second]); + auto mech = mechanism_catalogue::make(mech_name, voltage_, current_, compartment_index); + mech->set_areas(cv_areas_); + mechanisms_.push_back(std::move(mech)); + } + + + ///////////////////////////////////////////// + // build the ion species + ///////////////////////////////////////////// + for(auto ion : mechanisms::ion_kinds()) { + // find the compartment indexes of all compartments that have a + // mechanism that depends on/influences ion + std::set<int> index_set; + for(auto& mech : mechanisms_) { + if(mech->uses_ion(ion)) { + for(auto idx : mech->node_index()) { + index_set.insert(idx); + } + } + } + std::vector<cell_lid_type> indexes(index_set.begin(), index_set.end()); + + // create the ion state + if(indexes.size()) { + ions_.emplace(ion, index_type(indexes)); + } + + // join the ion reference in each mechanism into the cell-wide ion state + for(auto& mech : mechanisms_) { + if(mech->uses_ion(ion)) { + mech->set_ion(ion, ions_[ion]); + } + } + } + + // FIXME: Hard code parameters for now. + // Take defaults for reversal potential of sodium and potassium from + // the default values in Neuron. + // Neuron's defaults are defined in the file + // nrn/src/nrnoc/membdef.h + auto all = memory::all; + + constexpr value_type DEF_vrest = -65.0; // same name as #define in Neuron + + ion_na().reversal_potential()(all) = 115+DEF_vrest; // mV + ion_na().internal_concentration()(all) = 10.0; // mM + ion_na().external_concentration()(all) = 140.0; // mM + + ion_k().reversal_potential()(all) = -12.0+DEF_vrest;// mV + ion_k().internal_concentration()(all) = 54.4; // mM + ion_k().external_concentration()(all) = 2.5; // mM + + ion_ca().reversal_potential()(all) = 12.5 * std::log(2.0/5e-5);// mV + ion_ca().internal_concentration()(all) = 5e-5; // mM + ion_ca().external_concentration()(all) = 2.0; // mM + + // add the stimulii + for(const auto& stim : cell.stimulii()) { + auto idx = find_compartment_index(stim.location, graph); + stimulii_.push_back( {idx, stim.clamp} ); + } + + // record probe locations by index into corresponding state vector + auto probe_hi = probe_handles.begin(); + for (auto probe : cell.probes()) { + auto comp = find_compartment_index(probe.location, graph); + switch (probe.kind) { + case probeKind::membrane_voltage: + *probe_hi = {&fvm_cell::voltage_, comp}; + break; + case probeKind::membrane_current: + *probe_hi = {&fvm_cell::current_, comp}; + break; + default: + throw std::logic_error("unrecognized probeKind"); + } + ++probe_hi; + } + + // detector handles are just their corresponding compartment indices + auto detector_hi = detector_handles.begin(); + for (auto detector : cell.detectors()) { + auto comp = find_compartment_index(detector.location, graph); + *detector_hi++ = comp; + } + + // initialise mechanism and voltage state + reset(); +} + +template <typename T, typename I> +void fvm_cell<T, I>::setup_matrix(T dt) { + using memory::all; + + // convenience accesors to matrix storage + auto l = matrix_.l(); + auto d = matrix_.d(); + auto u = matrix_.u(); + auto p = matrix_.p(); + auto rhs = matrix_.rhs(); + + // The matrix has the following layout in memory + // where j is the parent index of i, i.e. i<j + // + // d[i] is the diagonal entry at a_ii + // u[i] is the upper triangle entry at a_ji + // l[i] is the lower triangle entry at a_ij + // + // d[j] . . u[i] + // . . . + // . . . + // l[i] . . d[i] + // + + d(all) = cv_areas_; // [µm^2] + for (auto i=1u; i<d.size(); ++i) { + auto a = 1e5*dt * face_alpha_[i]; + + d[i] += a; + l[i] = -a; + u[i] = -a; + + // add contribution to the diagonal of parent + d[p[i]] += a; + } + + // the RHS of the linear system is + // V[i] - dt/cm*(im - ie) + auto factor = 10.*dt; // units: 10·ms/(F/m^2)·(mA/cm^2) ≡ mV + for(auto i=0u; i<d.size(); ++i) { + rhs[i] = cv_areas_[i]*(voltage_[i] - factor/cv_capacitance_[i]*current_[i]); + } +} + +template <typename T, typename I> +void fvm_cell<T, I>::reset() { + voltage_(memory::all) = resting_potential_; + t_ = 0.; + for (auto& m : mechanisms_) { + m->nrn_init(); + } +} + +template <typename T, typename I> +void fvm_cell<T, I>::advance(T dt) { + using memory::all; + + PE("current"); + current_(all) = 0.; + + // update currents from ion channels + for(auto& m : mechanisms_) { + PE(m->name().c_str()); + m->set_params(t_, dt); + m->nrn_current(); + PL(); + } + + // add current contributions from stimulii + for (auto& stim : stimulii_) { + auto ie = stim.second.amplitude(t_); // [nA] + auto loc = stim.first; + + // note: current_ in [mA/cm^2], ie in [nA], cv_areas_ in [µm^2]. + // unit scale factor: [nA/µm^2]/[mA/cm^2] = 100 + current_[loc] -= 100*ie/cv_areas_[loc]; + } + PL(); + + // solve the linear system + PE("matrix", "setup"); + setup_matrix(dt); + PL(); PE("solve"); + matrix_.solve(); + PL(); + voltage_(all) = matrix_.rhs(); + PL(); + + // integrate state of gating variables etc. + PE("state"); + for(auto& m : mechanisms_) { + PE(m->name().c_str()); + m->nrn_state(); + PL(); + } + PL(); + + t_ += dt; +} + +} // namespace fvm +} // namespace mc +} // namespace nest + diff --git a/src/spike_source.hpp b/src/spike_source.hpp index 0537b6800a19534d23aedd7de2edae3d022b53e6..fd043a3a6252410d89fd5d64c0d9d57816f14987 100644 --- a/src/spike_source.hpp +++ b/src/spike_source.hpp @@ -8,13 +8,12 @@ namespace mc { // spike detector for a lowered cell template <typename Cell> -class spike_detector -{ +class spike_detector { public: using cell_type = Cell; - spike_detector(const cell_type& cell, segment_location loc, double thresh, float t_init) : - location_(loc), + spike_detector(const cell_type& cell, typename Cell::detector_handle h, double thresh, float t_init) : + handle_(h), threshold_(thresh) { reset(cell, t_init); @@ -22,7 +21,7 @@ public: util::optional<float> test(const cell_type& cell, float t) { util::optional<float> result = util::nothing; - auto v = cell.voltage(location_); + auto v = cell.detector_voltage(handle_); // these if statements could be simplified, but I keep them like // this to clearly reflect the finite state machine @@ -50,22 +49,19 @@ public: bool is_spiking() const { return is_spiking_; } - segment_location location() const { return location_; } - float t() const { return previous_t_; } float v() const { return previous_v_; } void reset(const cell_type& cell, float t_init) { previous_t_ = t_init; - previous_v_ = cell.voltage(location_); + previous_v_ = cell.detector_voltage(handle_); is_spiking_ = previous_v_ >= threshold_; } private: - // parameters/data - segment_location location_; + typename cell_type::detector_handle handle_; double threshold_; // state diff --git a/src/stimulus.hpp b/src/stimulus.hpp index f7c7538cf398a7154588126620d97b7c054953a4..f3c587a412eb2aba5a0dc1c127c262b86ca018e7 100644 --- a/src/stimulus.hpp +++ b/src/stimulus.hpp @@ -45,9 +45,9 @@ class i_clamp { private: - value_type delay_ = 0; - value_type duration_ = 0; - value_type amplitude_ = 0; + value_type delay_ = 0; // [ms] + value_type duration_ = 0; // [ms] + value_type amplitude_ = 0; // [nA] }; } // namespace mc diff --git a/src/util/meta.hpp b/src/util/meta.hpp index 7b1fb0d6f228175377242b607a111457ed90b414..e949e251dc2f92101555cd7845931728c5b19f0b 100644 --- a/src/util/meta.hpp +++ b/src/util/meta.hpp @@ -3,6 +3,7 @@ /* Type utilities and convenience expressions. */ #include <iterator> +#include <cstddef> #include <type_traits> namespace nest { diff --git a/src/util/singleton.hpp b/src/util/singleton.hpp new file mode 100644 index 0000000000000000000000000000000000000000..c718eef3624af2088daa9d84abb00401ed1bd6d4 --- /dev/null +++ b/src/util/singleton.hpp @@ -0,0 +1,63 @@ +#pragma once + +/* + * Present a single object as a (non-owning) container with one + * element. + * + * (Will be subsumed by range/view code.) + */ + +#include <algorithm> + +namespace nest { +namespace mc { +namespace util { + +template <typename X> +struct singleton_adaptor { + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using value_type = X; + using reference = X&; + using const_reference = const X&; + using iterator = X*; + using const_iterator = const X*; + + X* xp; + singleton_adaptor(X& x): xp(&x) {} + + const X* cbegin() const { return xp; } + const X* begin() const { return xp; } + X* begin() { return xp; } + + const X* cend() const { return xp+1; } + const X* end() const { return xp+1; } + X* end() { return xp+1; } + + std::size_t size() const { return 1u; } + + bool empty() const { return false; } + + const X* front() const { return *xp; } + X* front() { return *xp; } + + const X* back() const { return *xp; } + X* back() { return *xp; } + + const X* operator[](difference_type) const { return *xp; } + X* operator[](difference_type) { return *xp; } + + void swap(singleton_adaptor& s) { std::swap(xp, s.xp); } + friend void swap(singleton_adaptor& r, singleton_adaptor& s) { + r.swap(s); + } +}; + +template <typename X> +singleton_adaptor<X> singleton_view(X& x) { + return singleton_adaptor<X>(x); +} + +} // namespace util +} // namespace mc +} // namespace nest diff --git a/tests/unit/test_compartments.cpp b/tests/unit/test_compartments.cpp index c167b16cce5585a43efc13fc9f136025b21f7811..176bf18177d2379d2c42d3fc31199f904f22bc52 100644 --- a/tests/unit/test_compartments.cpp +++ b/tests/unit/test_compartments.cpp @@ -131,3 +131,28 @@ TEST(compartments, compartment_range) EXPECT_EQ(rng.begin(), rng.end()); } } + +TEST(compartments, make_compartment_range) +{ + using namespace nest::mc; + auto rng = make_compartment_range(10, 1.0, 2.0, 10.); + + EXPECT_EQ((*rng.begin()).index, 0u); + EXPECT_EQ((*rng.end()).index, 10u); + EXPECT_NE(rng.begin(), rng.end()); + + unsigned count = 0; + for (auto c : rng) { + EXPECT_EQ(c.index, count); + auto er = 1.0 + double(count)/10.; + EXPECT_DOUBLE_EQ(left(c.radius), er); + EXPECT_DOUBLE_EQ(right(c.radius), er+0.1); + EXPECT_EQ(c.length, 1.0); + ++count; + } + EXPECT_EQ(count, 10u); + + // test case of zero length range + auto rng_empty = make_compartment_range(0, 1.0, 1.0, 0.); + EXPECT_EQ(rng_empty.begin(), rng_empty.end()); +} diff --git a/tests/unit/test_fvm.cpp b/tests/unit/test_fvm.cpp index c0ed438f20841d40a9f2653e38424aeeba4a64ea..ae7f0b5a3c15c01fc2c952c3ca84ff860bd10294 100644 --- a/tests/unit/test_fvm.cpp +++ b/tests/unit/test_fvm.cpp @@ -5,6 +5,7 @@ #include <common_types.hpp> #include <cell.hpp> #include <fvm_cell.hpp> +#include <util/range.hpp> #include "../test_util.hpp" @@ -42,7 +43,14 @@ TEST(fvm, cable) cell.segment(2)->set_compartments(4); using fvm_cell = fvm::fvm_cell<double, cell_lid_type>; - fvm_cell fvcell(cell); + + std::vector<fvm_cell::target_handle> targets; + std::vector<fvm_cell::detector_handle> detectors; + std::vector<fvm_cell::probe_handle> probes; + + fvm_cell fvcell; + fvcell.initialize(util::singleton_view(cell), detectors, targets, probes); + auto& J = fvcell.jacobian(); EXPECT_EQ(cell.num_compartments(), 9u); @@ -92,7 +100,13 @@ TEST(fvm, init) cell.segment(1)->set_compartments(10); using fvm_cell = fvm::fvm_cell<double, cell_lid_type>; - fvm_cell fvcell(cell); + std::vector<fvm_cell::target_handle> targets; + std::vector<fvm_cell::detector_handle> detectors; + std::vector<fvm_cell::probe_handle> probes; + + fvm_cell fvcell; + fvcell.initialize(util::singleton_view(cell), detectors, targets, probes); + auto& J = fvcell.jacobian(); EXPECT_EQ(J.size(), 11u); diff --git a/tests/unit/test_probe.cpp b/tests/unit/test_probe.cpp index 5af744d2cc5f7f9b8a403b991b22d260c9ec74d4..cf7562c94fbf9ddcf33e16308d418dbdddd0ebf7 100644 --- a/tests/unit/test_probe.cpp +++ b/tests/unit/test_probe.cpp @@ -1,8 +1,9 @@ #include "gtest.h" -#include "common_types.hpp" -#include "cell.hpp" -#include "fvm_cell.hpp" +#include <common_types.hpp> +#include <cell.hpp> +#include <fvm_cell.hpp> +#include <util/range.hpp> TEST(probe, instantiation) { @@ -49,31 +50,35 @@ TEST(probe, fvm_cell) segment_location loc1{1, 1}; segment_location loc2{1, 0.5}; - auto pv0 = bs.add_probe({loc0, probeKind::membrane_voltage}); - auto pv1 = bs.add_probe({loc1, probeKind::membrane_voltage}); - auto pi2 = bs.add_probe({loc2, probeKind::membrane_current}); + bs.add_probe({loc0, probeKind::membrane_voltage}); + bs.add_probe({loc1, probeKind::membrane_voltage}); + bs.add_probe({loc2, probeKind::membrane_current}); i_clamp stim(0, 100, 0.3); bs.add_stimulus({1, 1}, stim); - fvm::fvm_cell<double, cell_local_size_type> lcell(bs); - lcell.setup_matrix(0.01); - lcell.initialize(); + using fvm_cell = fvm::fvm_cell<double, cell_lid_type>; + std::vector<fvm_cell::target_handle> targets; + std::vector<fvm_cell::detector_handle> detectors; + std::vector<fvm_cell::probe_handle> probes{3}; - EXPECT_EQ(3u, lcell.num_probes()); + fvm_cell lcell; + lcell.initialize(util::singleton_view(bs), detectors, targets, probes); - // expect probe values and direct queries of voltage and current + // Know from implementation that probe_handle.second + // is a compartment index: expect probe values and + // direct queries of voltage and current // to be equal in fvm cell - EXPECT_EQ(lcell.voltage(loc0), lcell.probe(pv0)); - EXPECT_EQ(lcell.voltage(loc1), lcell.probe(pv1)); - EXPECT_EQ(lcell.current(loc2), lcell.probe(pi2)); + EXPECT_EQ(lcell.voltage()[probes[0].second], lcell.probe(probes[0])); + EXPECT_EQ(lcell.voltage()[probes[1].second], lcell.probe(probes[1])); + EXPECT_EQ(lcell.current()[probes[2].second], lcell.probe(probes[2])); lcell.advance(0.05); - EXPECT_EQ(lcell.voltage(loc0), lcell.probe(pv0)); - EXPECT_EQ(lcell.voltage(loc1), lcell.probe(pv1)); - EXPECT_EQ(lcell.current(loc2), lcell.probe(pi2)); + EXPECT_EQ(lcell.voltage()[probes[0].second], lcell.probe(probes[0])); + EXPECT_EQ(lcell.voltage()[probes[1].second], lcell.probe(probes[1])); + EXPECT_EQ(lcell.current()[probes[2].second], lcell.probe(probes[2])); } diff --git a/tests/unit/test_spikes.cpp b/tests/unit/test_spikes.cpp index 3b4862029208c1f6c5a657d83dd982f510852cd6..5546ebd30dd6021cf74d98618dd043ed0eea79ce 100644 --- a/tests/unit/test_spikes.cpp +++ b/tests/unit/test_spikes.cpp @@ -4,7 +4,8 @@ #include <spike_source.hpp> struct cell_proxy { - double voltage(nest::mc::segment_location loc) const { + using detector_handle = int; + double detector_voltage(detector_handle) const { return v; } @@ -15,16 +16,17 @@ TEST(spikes, spike_detector) { using namespace nest::mc; using detector_type = spike_detector<cell_proxy>; + using detector_handle = cell_proxy::detector_handle; + cell_proxy proxy; float threshold = 10.f; float t = 0.f; float dt = 1.f; - auto loc = segment_location(1, 0.1); + detector_handle handle{}; - auto detector = detector_type(proxy, loc, threshold, t); + auto detector = detector_type(proxy, handle, threshold, t); EXPECT_FALSE(detector.is_spiking()); - EXPECT_EQ(loc, detector.location()); EXPECT_EQ(proxy.v, detector.v()); EXPECT_EQ(t, detector.t()); @@ -35,7 +37,6 @@ TEST(spikes, spike_detector) EXPECT_FALSE(spike); EXPECT_FALSE(detector.is_spiking()); - EXPECT_EQ(loc, detector.location()); EXPECT_EQ(proxy.v, detector.v()); EXPECT_EQ(t, detector.t()); } @@ -49,7 +50,6 @@ TEST(spikes, spike_detector) EXPECT_EQ(spike.get(), 1.5); EXPECT_TRUE(detector.is_spiking()); - EXPECT_EQ(loc, detector.location()); EXPECT_EQ(proxy.v, detector.v()); EXPECT_EQ(t, detector.t()); } @@ -62,7 +62,6 @@ TEST(spikes, spike_detector) EXPECT_FALSE(spike); EXPECT_FALSE(detector.is_spiking()); - EXPECT_EQ(loc, detector.location()); EXPECT_EQ(proxy.v, detector.v()); EXPECT_EQ(t, detector.t()); } diff --git a/tests/validation/validate_ball_and_stick.cpp b/tests/validation/validate_ball_and_stick.cpp index 62d06b4dac85885f935dd88855223551cc33fcce..59e952de58de61ae32cdbfb721ac991ec2119f8b 100644 --- a/tests/validation/validate_ball_and_stick.cpp +++ b/tests/validation/validate_ball_and_stick.cpp @@ -4,6 +4,7 @@ #include <common_types.hpp> #include <cell.hpp> #include <fvm_cell.hpp> +#include <util/singleton.hpp> #include "gtest.h" #include "../test_util.hpp" @@ -82,6 +83,11 @@ TEST(ball_and_stick, neuron_baseline) } }; + using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>; + std::vector<fvm_cell::detector_handle> detectors(cell.detectors().size()); + std::vector<fvm_cell::target_handle> targets(cell.synapses().size()); + std::vector<fvm_cell::probe_handle> probes(cell.probes().size()); + std::vector<result> results; for(auto run_index=0u; run_index<cell_data.size(); ++run_index) { auto& run = cell_data[run_index]; @@ -90,13 +96,10 @@ TEST(ball_and_stick, neuron_baseline) std::vector<std::vector<double>> v(3); // make the lowered finite volume cell - fvm::fvm_cell<double, cell_local_size_type> model(cell); - auto graph = cell.model(); - // set initial conditions - using memory::all; - model.voltage()(all) = -65.; - model.initialize(); // have to do this _after_ initial conditions are set + fvm_cell model; + model.initialize(util::singleton_view(cell), detectors, targets, probes); + auto graph = cell.model(); // run the simulation auto soma_comp = nest::mc::find_compartment_index({0,0}, graph); @@ -234,6 +237,11 @@ TEST(ball_and_3stick, neuron_baseline) } }; + using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>; + std::vector<fvm_cell::detector_handle> detectors(cell.detectors().size()); + std::vector<fvm_cell::target_handle> targets(cell.synapses().size()); + std::vector<fvm_cell::probe_handle> probes(cell.probes().size()); + std::vector<result> results; auto start = testing::tic(); for(auto run_index=0u; run_index<cell_data.size(); ++run_index) { @@ -245,13 +253,11 @@ TEST(ball_and_3stick, neuron_baseline) std::vector<std::vector<double>> v(3); // make the lowered finite volume cell - fvm::fvm_cell<double, cell_local_size_type> model(cell); + fvm_cell model; + model.initialize(util::singleton_view(cell), detectors, targets, probes); auto graph = cell.model(); // set initial conditions - using memory::all; - model.voltage()(all) = -65.; - model.initialize(); // have to do this _after_ initial conditions are set // run the simulation auto soma_comp = nest::mc::find_compartment_index({0,0.}, graph); diff --git a/tests/validation/validate_soma.cpp b/tests/validation/validate_soma.cpp index 24a0c1c59f0db24bc6f9c7832909f9f52af7beb7..af4d32826acb12bcc522daf7aa3f84c80dcc6bb2 100644 --- a/tests/validation/validate_soma.cpp +++ b/tests/validation/validate_soma.cpp @@ -4,6 +4,7 @@ #include <common_types.hpp> #include <cell.hpp> #include <fvm_cell.hpp> +#include <util/singleton.hpp> #include "gtest.h" #include "../test_util.hpp" @@ -27,7 +28,13 @@ TEST(soma, neuron_baseline) cell.add_stimulus({0,0.5}, {10., 100., 0.1}); // make the lowered finite volume cell - fvm::fvm_cell<double, cell_local_size_type> model(cell); + using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>; + std::vector<fvm_cell::detector_handle> detectors(cell.detectors().size()); + std::vector<fvm_cell::target_handle> targets(cell.synapses().size()); + std::vector<fvm_cell::probe_handle> probes(cell.probes().size()); + + fvm_cell model; + model.initialize(util::singleton_view(cell), detectors, targets, probes); // load data from file auto cell_data = testing::g_validation_data.load("soma.json"); @@ -49,9 +56,7 @@ TEST(soma, neuron_baseline) double dt = run["dt"]; // set initial conditions - using memory::all; - model.voltage()(all) = -65.; - model.initialize(); // have to do this _after_ initial conditions are set + model.reset(); // run the simulation auto tfinal = 120.; // ms @@ -92,7 +97,13 @@ TEST(soma, convergence) cell.add_stimulus({0,0.5}, {10., 100., 0.1}); // make the lowered finite volume cell - fvm::fvm_cell<double, cell_local_size_type> model(cell); + using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>; + std::vector<fvm_cell::detector_handle> detectors(cell.detectors().size()); + std::vector<fvm_cell::target_handle> targets(cell.synapses().size()); + std::vector<fvm_cell::probe_handle> probes(cell.probes().size()); + + fvm_cell model; + model.initialize(util::singleton_view(cell), detectors, targets, probes); // generate baseline solution with small dt=0.0001 std::vector<double> baseline_spike_times; @@ -101,9 +112,7 @@ TEST(soma, convergence) std::vector<double> v; // set initial conditions - using memory::all; - model.voltage()(all) = -65.; - model.initialize(); // have to do this _after_ initial conditions are set + model.reset(); // run the simulation auto tfinal = 120.; // ms @@ -123,9 +132,7 @@ TEST(soma, convergence) std::vector<double> v; // set initial conditions - using memory::all; - model.voltage()(all) = -65.; - model.initialize(); // have to do this _after_ initial conditions are set + model.reset(); // run the simulation auto tfinal = 120.; // ms diff --git a/tests/validation/validate_synapses.cpp b/tests/validation/validate_synapses.cpp index 876b24eb55c9cad0e08c17cef9cdf16595f5bd90..9aa90abef6f6178a6285f9a1d5db2e9d1cb1d4f1 100644 --- a/tests/validation/validate_synapses.cpp +++ b/tests/validation/validate_synapses.cpp @@ -71,8 +71,11 @@ void run_neuron_baseline(const char* syn_type, const char* data_file) cell.add_synapse({1, 0.5}, syn_default); // add probes - auto probe_soma = cell.add_probe({{0,0}, probeKind::membrane_voltage}); - auto probe_dend = cell.add_probe({{1,0.5}, probeKind::membrane_voltage}); + auto probe_soma_idx = cell.add_probe({{0,0}, probeKind::membrane_voltage}); + auto probe_dend_idx = cell.add_probe({{1,0.5}, probeKind::membrane_voltage}); + + cell_member_type probe_soma{0u, probe_soma_idx}; + cell_member_type probe_dend{0u, probe_dend_idx}; // injected spike events postsynaptic_spike_event<float> synthetic_events[] = { @@ -111,15 +114,15 @@ void run_neuron_baseline(const char* syn_type, const char* data_file) group.enqueue_events(synthetic_events); // run the simulation - v[0].push_back(group.cell().probe(probe_soma)); - v[1].push_back(group.cell().probe(probe_dend)); + v[0].push_back(group.probe(probe_soma)); + v[1].push_back(group.probe(probe_dend)); double t = 0.; while(t < tfinal) { t += dt; group.advance(t, dt); // save voltage at soma and dendrite - v[0].push_back(group.cell().probe(probe_soma)); - v[1].push_back(group.cell().probe(probe_dend)); + v[0].push_back(group.probe(probe_soma)); + v[1].push_back(group.probe(probe_dend)); } results.push_back({num_compartments, dt, v, measurements});