diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp index 5fbfafbe20e42f52e83b4ebc87adaa713d12bd43..7eb69f6397841b1309ceb8d8721484a763b5060a 100644 --- a/arbor/fvm_layout.cpp +++ b/arbor/fvm_layout.cpp @@ -82,8 +82,6 @@ std::vector<V> unique_union(const std::vector<V>& a, const std::vector<V>& b) { return u; } -} // anonymous namespace - // Convert mcable_map values to a piecewise function over an mcable. // The projection gives the map from the values in the mcable_map to the values in the piecewise function. template <typename T, typename U, typename Proj = get_value> @@ -124,8 +122,14 @@ pw_elements<U> pw_over_cable(const mcable_map<T>& mm, mcable cable, U dflt_value } return pw; } +} // anonymous namespace + + +// Building CV geometry +// -------------------- // Construct cv_geometry for cell from locset describing CV boundary points. + cv_geometry cv_geometry_from_ends(const cable_cell& cell, const locset& lset) { auto pop = [](auto& vec) { auto h = vec.back(); return vec.pop_back(), h; }; @@ -271,6 +275,7 @@ namespace impl { } // Merge CV geometry lists in-place. + cv_geometry& append(cv_geometry& geom, const cv_geometry& right) { using impl::tail; using impl::append_offset; @@ -311,6 +316,7 @@ cv_geometry& append(cv_geometry& geom, const cv_geometry& right) { } // Combine two fvm_cv_geometry groups in-place. + fvm_cv_discretization& append(fvm_cv_discretization& dczn, const fvm_cv_discretization& right) { append(dczn.geometry, right.geometry); @@ -326,6 +332,10 @@ fvm_cv_discretization& append(fvm_cv_discretization& dczn, const fvm_cv_discreti return dczn; } + +// FVM discretization +// ------------------ + fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt) { const auto& dflt = cell.default_parameters; fvm_cv_discretization D; @@ -446,8 +456,234 @@ fvm_cv_discretization fvm_cv_discretize(const std::vector<cable_cell>& cells, return combined; } +// Voltage interpolation +// --------------------- +// +// Interpolated voltages and axial current at a given site are determined +// from 'voltage references'. A voltage reference is a CV from which the +// membrane voltage is taken, and a location within that CV where the +// voltage is deemed to be accurate. +// +// A CV that includes no fork points has one reference location which is +// the centre of the CV (by branch length). Otherwise, every fork in a CV +// is regarded as being a reference location. +// +// Voltage references should comprise adjacent CVs, however should the site +// lie between fork points within the one CV, there is nothing to interpolate +// and the voltage references will all come from the one CV containing the +// site. + +struct voltage_reference { + fvm_index_type cv = -1; + mlocation loc; +}; + +struct voltage_reference_pair { + voltage_reference proximal; + voltage_reference distal; +}; + +// Collection of other locations that are coincident under projection. +std::vector<mlocation> coincident_locations(const morphology& m, mlocation x) { + std::vector<mlocation> result; + if (x.pos==0) { + msize_t parent_bid = m.branch_parent(x.branch); + if (parent_bid!=mnpos) { + result.push_back({parent_bid, 1}); + } + for (msize_t sibling_bid: m.branch_children(parent_bid)) { + if (sibling_bid!=x.branch) { + result.push_back({sibling_bid, 0}); + } + } + } + else if (x.pos==1) { + for (msize_t child_bid: m.branch_children(x.branch)) { + result.push_back({child_bid, 0}); + } + } + return result; +} + +// Test if location intersects (sorted) sequence of cables. +template <typename Seq> +bool cables_intersect_location(Seq&& cables, mlocation x) { + struct cmp_branch { + bool operator()(const mcable& c, msize_t bid) const { return c.branch<bid; } + bool operator()(msize_t bid, const mcable& c) const { return bid<c.branch; } + }; + + using std::begin; + using std::end; + auto eqr = std::equal_range(begin(cables), end(cables), x.branch, cmp_branch{}); + + return util::any_of(util::make_range(eqr), + [&x](const mcable& c) { return c.prox_pos<=x.pos && x.pos<=c.dist_pos; }); +} + +voltage_reference_pair fvm_voltage_reference_points(const morphology& morph, const cv_geometry& geom, fvm_size_type cell_idx, mlocation site) { + voltage_reference site_ref, parent_ref, child_ref; + bool check_parent = true, check_child = true; + msize_t bid = site.branch; + + // 'Simple' CVs contain no fork points, and are represented by a single cable. + auto cv_simple = [&geom](auto cv) { return geom.cables(cv).size()==1u; }; + + auto cv_midpoint = [&geom](auto cv) { + // Under assumption that CV is simple: + mcable c = geom.cables(cv).front(); + return mlocation{c.branch, (c.prox_pos+c.dist_pos)/2}; + }; + + auto cv_contains_fork = [&](auto cv, mlocation x) { + // CV contains fork if it intersects any location coincident with x + // other than x itselfv. + + if (cv_simple(cv)) return false; + auto locs = coincident_locations(morph, x); + + return util::any_of(locs, [&](mlocation y) { return cables_intersect_location(geom.cables(cv), y); }); + }; + + site_ref.cv = geom.location_cv(cell_idx, site, cv_prefer::cv_empty); + if (cv_simple(site_ref.cv)) { + site_ref.loc = cv_midpoint(site_ref.cv); + } + else if (cv_contains_fork(site_ref.cv, mlocation{bid, 0})) { + site_ref.loc = mlocation{bid, 0}; + check_parent = false; + } + else { + // CV not simple, and without head of branch as fork point, must contain + // tail of branch as a fork point. + arb_assert(cv_contains_fork(site_ref.cv, mlocation{bid, 1})); + + site_ref.loc = mlocation{bid, 1}; + check_child = false; + } + + if (check_parent) { + parent_ref.cv = geom.cv_parent[site_ref.cv]; + } + if (parent_ref.cv!=-1) { + parent_ref.loc = cv_simple(parent_ref.cv)? cv_midpoint(parent_ref.cv): mlocation{bid, 0}; + arb_assert(parent_ref.loc.branch==bid); + } + + if (check_child) { + for (auto child_cv: geom.children(site_ref.cv)) { + mcable child_prox_cable = geom.cables(child_cv).front(); + if (child_prox_cable.branch==bid) { + child_ref.cv = child_cv; + break; + } + } + } + if (child_ref.cv!=-1) { + child_ref.loc = cv_simple(child_ref.cv)? cv_midpoint(child_ref.cv): mlocation{bid, 1}; + arb_assert(child_ref.loc.branch==bid); + } + + // If both child and parent references are possible, pick based on distallity with respect + // to the site_ref location. + + if (child_ref.cv!=-1 && parent_ref.cv!=-1) { + if (site.pos<site_ref.loc.pos) child_ref.cv = -1; // i.e. use parent. + else parent_ref.cv = -1; // i.e. use child. + } + + voltage_reference_pair result; + if (child_ref.cv!=-1) { + result.proximal = site_ref; + result.distal = child_ref; + } + else if (parent_ref.cv!=-1) { + result.proximal = parent_ref; + result.distal = site_ref; + } + else { + result.proximal = site_ref; + result.distal = site_ref; + } + + return result; +} + +// Interpolate membrane voltage from reference points in adjacent CVs. + +fvm_voltage_interpolant fvm_interpolate_voltage(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site) { + auto& embedding = cell.embedding(); + fvm_voltage_interpolant vi; + + auto vrefs = fvm_voltage_reference_points(cell.morphology(), D.geometry, cell_idx, site); + vi.proximal_cv = vrefs.proximal.cv; + vi.distal_cv = vrefs.distal.cv; + + arb_assert(vrefs.proximal.loc.branch==site.branch); + arb_assert(vrefs.distal.loc.branch==site.branch); + + if (vrefs.proximal.cv==vrefs.distal.cv) { // (no interpolation) + vi.proximal_coef = 1.0; + vi.distal_coef = 0.0; + } + else { + msize_t bid = site.branch; + + arb_assert(vrefs.proximal.loc.pos<vrefs.distal.loc.pos); + mcable rr_span = mcable{bid, vrefs.proximal.loc.pos , vrefs.distal.loc.pos}; + double rr_resistance = embedding.integrate_ixa(rr_span, D.axial_resistivity[0].at(bid)); + + // Note: site is not necessarily distal to the most proximal reference point. + bool flip_rs = vrefs.proximal.loc.pos>site.pos; + mcable rs_span = flip_rs? mcable{bid, site.pos, vrefs.proximal.loc.pos} + : mcable{bid, vrefs.proximal.loc.pos, site.pos}; + + double rs_resistance = embedding.integrate_ixa(rs_span, D.axial_resistivity[0].at(bid)); + if (flip_rs) { + rs_resistance = -rs_resistance; + } + + double p = rs_resistance/rr_resistance; + vi.proximal_coef = 1-p; + vi.distal_coef = p; + } + return vi; +} + +// Axial current as linear combination of membrane voltages at reference points in adjacent CVs. + +fvm_voltage_interpolant fvm_axial_current(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site) { + auto& embedding = cell.embedding(); + fvm_voltage_interpolant vi; + + auto vrefs = fvm_voltage_reference_points(cell.morphology(), D.geometry, cell_idx, site); + vi.proximal_cv = vrefs.proximal.cv; + vi.distal_cv = vrefs.distal.cv; + + if (vi.proximal_cv==vi.distal_cv) { + vi.proximal_coef = 0; + vi.distal_coef = 0; + } + else { + msize_t bid = site.branch; + + arb_assert(vrefs.proximal.loc.pos<vrefs.distal.loc.pos); + mcable rr_span = mcable{bid, vrefs.proximal.loc.pos , vrefs.distal.loc.pos}; + double rr_conductance = 100/embedding.integrate_ixa(rr_span, D.axial_resistivity[cell_idx].at(bid)); // [µS] + + vi.proximal_coef = -rr_conductance; + vi.distal_coef = rr_conductance; + } + + return vi; +} + +// FVM mechanism data +// ------------------ + // CVs are absolute (taken from combined discretization) so do not need to be shifted. // Only target numbers need to be shifted. + fvm_mechanism_data& append(fvm_mechanism_data& left, const fvm_mechanism_data& right) { using impl::append_offset; @@ -514,6 +750,8 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties& return combined; } +// Construct FVM mechanism data for a single cell. + fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties& gprop, const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx) { diff --git a/arbor/fvm_layout.hpp b/arbor/fvm_layout.hpp index 434ba6f5e80363941fe43f9e064e8480e5a3529d..e3ade27957e539c0a595d0bd37b4b4e1f2c6052f 100644 --- a/arbor/fvm_layout.hpp +++ b/arbor/fvm_layout.hpp @@ -203,6 +203,24 @@ fvm_cv_discretization& append(fvm_cv_discretization&, const fvm_cv_discretizatio fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt); fvm_cv_discretization fvm_cv_discretize(const std::vector<cable_cell>& cells, const cable_cell_parameter_set& global_defaults, const arb::execution_context& ctx={}); + +// Interpolant data for voltage, axial current probes. +// +// The proximal CV will always be either the same as distal CV (trivial +// interpolation) or the parent of the distal CV. + +struct fvm_voltage_interpolant { + fvm_index_type proximal_cv, distal_cv; + fvm_value_type proximal_coef, distal_coef; +}; + +// Interpolated membrane voltage. +fvm_voltage_interpolant fvm_interpolate_voltage(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site); + +// Axial current as linear combiantion of voltages. +fvm_voltage_interpolant fvm_axial_current(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site); + + // Post-discretization data for point and density mechanism instantiation. struct fvm_mechanism_config { diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp index c09f25eef696e26127fb0da910b989f197796028..54403866cde46c114a53eb831ae9b7a00a601412 100644 --- a/test/unit/test_fvm_layout.cpp +++ b/test/unit/test_fvm_layout.cpp @@ -1,3 +1,4 @@ +#include <limits> #include <string> #include <vector> @@ -13,6 +14,7 @@ #include "io/sepval.hpp" #include "common.hpp" +#include "common_morphologies.hpp" #include "unit_test_catalogue.hpp" #include "../common_cells.hpp" @@ -835,3 +837,223 @@ TEST(fvm_layout, revpot) { ASSERT_EQ(1u, M.mechanisms.count(write_eb_ec.name())); EXPECT_EQ((std::vector<fvm_index_type>(1, soma1_index)), M.mechanisms.at(write_eb_ec.name()).cv); } + +TEST(fvm_layout, vinterp_cable) { + // On a simple cable, expect CVs used forinterpolation to change at + // the midpoints of interior CVs. Every site in the proximal CV should + // interpolate between that and the next; every site in the distal CV + // should interpolate between that and the parent. + + // Cable cell with just one branch, non-spherical root. + morphology morph(sample_tree({msample{0., 0., 0., 1.}, msample{10., 0., 0., 1.}}, {mnpos, 0u})); + cable_cell cell(morph); + + // CV midpoints at branch pos 0.1, 0.3, 0.5, 0.7, 0.9. + // Expect voltage reference locations to be CV modpoints. + cell.default_parameters.discretization = cv_policy_fixed_per_branch(5); + fvm_cv_discretization D = fvm_cv_discretize(cell, neuron_parameter_defaults); + + // Test locations, either side of CV midpoints plus extrema, CV boundaries. + double site_pos[] = { 0., 0.03, 0.11, 0.2, 0.28, 0.33, 0.4, 0.46, 0.55, 0.6, 0.75, 0.8, 0.83, 0.95, 1.}; + + for (auto pos: site_pos) { + mlocation site{0, pos}; + + fvm_index_type expected_distal; + if (pos<0.3) { + expected_distal = 1; + } + else if (pos<0.5) { + expected_distal = 2; + } + else if (pos<0.7) { + expected_distal = 3; + } + else { + expected_distal = 4; + } + fvm_index_type expected_proximal = expected_distal-1; + + fvm_voltage_interpolant I = fvm_interpolate_voltage(cell, D, 0, site); + + EXPECT_EQ(expected_proximal, I.proximal_cv); + EXPECT_EQ(expected_distal, I.distal_cv); + + // Cable has constant diameter, so interpolation coefficients should + // be simple linear functions of branch position. + + double prox_refpos = I.proximal_cv*0.2+0.1; + double dist_refpos = I.distal_cv*0.2+0.1; + + // (Tortuous fp manipulation along the way makes the error greater than 4 ulp). + const double relerr = 32*std::numeric_limits<double>::epsilon(); + + EXPECT_TRUE(testing::near_relative((dist_refpos-pos)/0.2, I.proximal_coef, relerr)); + EXPECT_TRUE(testing::near_relative((pos-prox_refpos)/0.2, I.distal_coef, relerr)); + } +} + +TEST(fvm_layout, vinterp_forked) { + // If a CV contains points at both ends of a branch, there will be + // no other adjacent CV on the same branch that we can use for + // interpolation. + + // Cable cell with three branchses; branches 0 has child branches 1 and 2. + morphology morph(sample_tree( + {{0., 0., 0., 1.}, {10., 0., 0., 1}, {10., 20., 0., 1}, {10., -20., 0., 1}}, + {mnpos, 0u, 1u, 1u})); + cable_cell cell(morph); + + // CV 0 contains branch 0 and the fork point; CV 1 and CV 2 have CV 0 as parent, + // and contain branches 1 and 2 respectively, excluding the fork point. + mlocation_list cv_ends{{1, 0.}, {2, 0.}}; + cell.default_parameters.discretization = cv_policy_explicit(cv_ends); + fvm_cv_discretization D = fvm_cv_discretize(cell, neuron_parameter_defaults); + + // Points in branch 0 should only get CV 0 for interpolation. + { + fvm_voltage_interpolant I = fvm_interpolate_voltage(cell, D, 0, mlocation{0, 0.3}); + EXPECT_EQ(0, I.proximal_cv); + EXPECT_EQ(0, I.distal_cv); + EXPECT_EQ(1, I.proximal_coef+I.distal_coef); + } + // Points in branches 1 and 2 should get CV 0 and CV 1 or 2 respectively. + { + fvm_voltage_interpolant I = fvm_interpolate_voltage(cell, D, 0, mlocation{1, 0}); + EXPECT_EQ(0, I.proximal_cv); + EXPECT_EQ(1., I.proximal_coef); + EXPECT_EQ(1, I.distal_cv); + EXPECT_EQ(0., I.distal_coef); + + // Past the midpoint, we're extrapolating. + I = fvm_interpolate_voltage(cell, D, 0, mlocation{1, 0.7}); + EXPECT_EQ(0, I.proximal_cv); + EXPECT_LT(I.proximal_coef, 0.); + EXPECT_EQ(1, I.distal_cv); + EXPECT_GT(I.distal_coef, 1.); + + I = fvm_interpolate_voltage(cell, D, 0, mlocation{2, 0}); + EXPECT_EQ(0, I.proximal_cv); + EXPECT_EQ(1., I.proximal_coef); + EXPECT_EQ(2, I.distal_cv); + EXPECT_EQ(0., I.distal_coef); + + I = fvm_interpolate_voltage(cell, D, 0, mlocation{2, 0.7}); + EXPECT_EQ(0, I.proximal_cv); + EXPECT_LT(I.proximal_coef, 0.); + EXPECT_EQ(2, I.distal_cv); + EXPECT_GT(I.distal_coef, 1.); + } +} + +TEST(fvm_layout, iinterp) { + // If we get two distinct interpolation points back, the coefficients + // should match the face-conductance. + + // 1. Vertex-delimited and vertex-centred discretizations. + using namespace common_morphology; + + std::vector<cable_cell> cells; + std::vector<std::string> label; + for (auto& p: test_morphologies) { + if (p.second.empty()) continue; + + cells.emplace_back(p.second); + cells.back().default_parameters.discretization = cv_policy_fixed_per_branch(3); + label.push_back(p.first+": forks-at-end"s); + + cells.emplace_back(p.second); + cells.back().default_parameters.discretization = cv_policy_fixed_per_branch(3, cv_policy_flag::interior_forks); + label.push_back(p.first+": interior-forks"s); + } + + fvm_cv_discretization D = fvm_cv_discretize(cells, neuron_parameter_defaults); + for (unsigned cell_idx = 0; cell_idx<cells.size(); ++cell_idx) { + SCOPED_TRACE(label[cell_idx]); + unsigned n_branch = D.geometry.n_branch(cell_idx); + for (msize_t bid = 0; bid<n_branch; ++bid) { + for (double pos: {0., 0.3, 0.4, 0.7, 1.}) { + mlocation x{bid, pos}; + SCOPED_TRACE(x); + + fvm_voltage_interpolant I = fvm_axial_current(cells[cell_idx], D, cell_idx, x); + + // With the given discretization policies, should only have no interpolation when + // the cell has only the once CV. + + if (D.geometry.cell_cvs(cell_idx).size()==1) { + EXPECT_EQ(I.proximal_cv, I.distal_cv); + EXPECT_EQ(D.geometry.cell_cvs(cell_idx).front(), I.proximal_cv); + } + else { + EXPECT_EQ(D.geometry.cv_parent.at(I.distal_cv), I.proximal_cv); + EXPECT_TRUE(I.proximal_cv>=D.geometry.cell_cv_interval(cell_idx).first); + + double fc = D.face_conductance.at(I.distal_cv); + EXPECT_DOUBLE_EQ(-fc, I.proximal_coef); + EXPECT_DOUBLE_EQ(+fc, I.distal_coef); + } + } + } + } + + // 2. Weird discretization: test points where the interpolated current has to be zero. + // Use the same cell/discretiazation as in vinterp_forked test: + + // Cable cell with three branchses; branches 0 has child branches 1 and 2. + morphology morph(sample_tree( + {{0., 0., 0., 1.}, {10., 0., 0., 1}, {10., 20., 0., 1}, {10., -20., 0., 1}}, + {mnpos, 0u, 1u, 1u})); + cable_cell cell(morph); + + // CV 0 contains branch 0 and the fork point; CV 1 and CV 2 have CV 0 as parent, + // and contain branches 1 and 2 respectively, excluding the fork point. + mlocation_list cv_ends{{1, 0.}, {2, 0.}}; + cell.default_parameters.discretization = cv_policy_explicit(cv_ends); + D = fvm_cv_discretize(cell, neuron_parameter_defaults); + + // Expect axial current interpolations on branches 1 and 2 to match CV 1 and 2 + // face-conductances; CV 0 contains the fork point, so there is nothing to + // interpolate from on branch 0. + + // Branch 0: + for (double pos: {0., 0.1, 0.8, 1.}) { + mlocation x{0, pos}; + SCOPED_TRACE(x); + + fvm_voltage_interpolant I = fvm_axial_current(cell, D, 0, x); + + EXPECT_EQ(0, I.proximal_cv); + EXPECT_EQ(0, I.distal_cv); + EXPECT_EQ(0., I.proximal_coef); + EXPECT_EQ(0., I.distal_coef); + } + + // Branch 1: + double fc1 = D.face_conductance[1]; + for (double pos: {0., 0.1, 0.8, 1.}) { + mlocation x{1, pos}; + SCOPED_TRACE(x); + + fvm_voltage_interpolant I = fvm_axial_current(cell, D, 0, x); + + EXPECT_EQ(0, I.proximal_cv); + EXPECT_EQ(1, I.distal_cv); + EXPECT_EQ(-fc1, I.proximal_coef); + EXPECT_EQ(+fc1, I.distal_coef); + } + + // Branch 2: + double fc2 = D.face_conductance[2]; + for (double pos: {0., 0.1, 0.8, 1.}) { + mlocation x{2, pos}; + SCOPED_TRACE(x); + + fvm_voltage_interpolant I = fvm_axial_current(cell, D, 0, x); + + EXPECT_EQ(0, I.proximal_cv); + EXPECT_EQ(2, I.distal_cv); + EXPECT_EQ(-fc2, I.proximal_coef); + EXPECT_EQ(+fc2, I.distal_coef); + } +}