diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp index a60bcfb2a2facd822240752a7cb242d0629dd62b..9a13e1a93ac17a32c69b9387a834653698f9e904 100644 --- a/arbor/cable_cell_param.cpp +++ b/arbor/cable_cell_param.cpp @@ -1,8 +1,13 @@ #include <cfloat> #include <cmath> +#include <numeric> +#include <vector> +#include <arbor/cable_cell.hpp> #include <arbor/cable_cell_param.hpp> +#include <arbor/morph/locset.hpp> +#include "morph/em_morphology.hpp" #include "util/maputil.hpp" namespace arb { @@ -64,4 +69,74 @@ cable_cell_local_parameter_set neuron_parameter_defaults = { 0.01 }; +// Discretization policy implementations: + +locset cv_policy_max_extent::cv_boundary_points(const cable_cell& cell) const { + const auto& emorph = *cell.morphology(); + const unsigned nbranch = emorph.num_branches(); + if (!nbranch || max_extent_<=0) return ls::nil(); + + std::vector<mlocation> points; + + unsigned bidx = 0; + if (flags_&cv_policy_flag::single_root_cv) { + points.push_back({0, 0.}); + points.push_back({0, 1.}); + bidx = 1; + } + + const double oomax_extent = 1./max_extent_; + while (bidx<nbranch) { + unsigned ncv = std::ceil(emorph.branch_length(bidx)*oomax_extent); + double ooncv = 1./ncv; + + if (flags_&cv_policy_flag::interior_forks) { + for (unsigned i = 0; i<ncv; ++i) { + points.push_back({bidx, (1+2*i)*ooncv/2}); + } + } + else { + for (unsigned i = 0; i<ncv; ++i) { + points.push_back({bidx, i*ooncv}); + } + points.push_back({bidx, 1.}); + } + ++bidx; + } + + return std::accumulate(points.begin(), points.end(), ls::nil(), [](auto& l, auto& p) { return join(l, ls::location(p)); }); +} + +locset cv_policy_fixed_per_branch::cv_boundary_points(const cable_cell& cell) const { + const unsigned nbranch = cell.morphology()->num_branches(); + if (!nbranch) return ls::nil(); + + std::vector<mlocation> points; + + unsigned bidx = 0; + if (flags_&cv_policy_flag::single_root_cv) { + points.push_back({0, 0.}); + points.push_back({0, 1.}); + bidx = 1; + } + + double ooncv = 1./cv_per_branch_; + while (bidx<nbranch) { + if (flags_&cv_policy_flag::interior_forks) { + for (unsigned i = 0; i<cv_per_branch_; ++i) { + points.push_back({bidx, (1+2*i)*ooncv/2}); + } + } + else { + for (unsigned i = 0; i<cv_per_branch_; ++i) { + points.push_back({bidx, i*ooncv}); + } + points.push_back({bidx, 1.}); + } + ++bidx; + } + + return std::accumulate(points.begin(), points.end(), ls::nil(), [](auto& l, auto& p) { return join(l, ls::location(p)); }); +} + } // namespace arb diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp index 8ef752abae28868b00d2feecb11ebd065d61e77e..0de0dff56e55a77b8136ae36c4b6ba15835dddcb 100644 --- a/arbor/include/arbor/cable_cell_param.hpp +++ b/arbor/include/arbor/cable_cell_param.hpp @@ -2,8 +2,10 @@ #include <arbor/arbexcept.hpp> #include <arbor/mechcat.hpp> +#include <arbor/morph/locset.hpp> #include <arbor/util/optional.hpp> +#include <memory> #include <unordered_map> #include <string> @@ -39,7 +41,6 @@ struct threshold_detector { // Tag type for dispatching cable_cell::place() calls that add gap junction sites. struct gap_junction_site {}; - // Mechanism description, viz. mechanism name and // (non-global) parameter settings. Used to assign // density and point mechanisms to segments and @@ -103,6 +104,134 @@ private: std::unordered_map<std::string, double> param_; }; +// FVM discretization policies/hints. +// +// CV polices, given a cable cell, provide a locset comprising +// CV boundary points to be used by the discretization. The +// discretization need not adopt the boundary points 100% faithfully; +// for example, it may elide empty CVs or perform other transformations +// for numeric fidelity or performance reasons. +// +// The cv_policy class is a value-like wrapper for actual +// policies that derive from `cv_policy_base`. At present, +// there are only three policies implemented, described below. +// The intent is to provide more sophisticated policies in the +// near future, specifically one based on the 'd-lambda' rule. +// +// cv_policy_explicit: +// Simply use the provided locset. +// +// cv_policy_fixed_per_branch: +// Use the same number of CVs for each branch. +// +// cv_policy_max_extent: +// Use as many CVs as required to ensure that no CV has +// a length longer than a given value. +// +// Except for the explicit policy, CV policies may also take various +// flags (implemented as bitwise orable enums) to modify their +// behaviour. In general, future CV policies may choose to ignore +// flag values, but should respect them if their semantics are +// relevant. +// +// cv_policy_flag::interior_forks: +// Position CVs so as to include fork points, as opposed +// to positioning them so that fork points are at the +// boundaries of CVs. +// +// cv_policy_flag::single_root_cv: +// Always treat the root branch as a single CV regardless. + +class cable_cell; + +struct cv_policy_base { + virtual locset cv_boundary_points(const cable_cell& cell) const = 0; + virtual std::unique_ptr<cv_policy_base> clone() const = 0; + virtual ~cv_policy_base() {} +}; + +using cv_policy_base_ptr = std::unique_ptr<cv_policy_base>; + +struct cv_policy { + cv_policy(const cv_policy_base& ref) { // implicit + policy_ptr = ref.clone(); + } + + cv_policy(cv_policy&&) = default; + + cv_policy(const cv_policy& other): + policy_ptr(other.policy_ptr->clone()) {} + + cv_policy& operator=(const cv_policy&) = default; + cv_policy& operator=(cv_policy&&) = default; + + locset cv_boundary_points(const cable_cell& cell) const { + return policy_ptr->cv_boundary_points(cell); + } + +private: + cv_policy_base_ptr policy_ptr; +}; + +// Common flags for CV policies; bitwise composable. +namespace cv_policy_flag { + using value = unsigned; + enum : unsigned { + none = 0, + interior_forks = 1<<0, + single_root_cv = 1<<1 + }; +} + +struct cv_policy_explicit: cv_policy_base { + explicit cv_policy_explicit(locset locs): locs_(std::move(locs)) {} + + cv_policy_base_ptr clone() const override { + return cv_policy_base_ptr(new cv_policy_explicit(*this)); + } + + locset cv_boundary_points(const cable_cell&) const override { + return locs_; + } + +private: + locset locs_; +}; + +struct cv_policy_max_extent: cv_policy_base { + explicit cv_policy_max_extent(double max_extent, cv_policy_flag::value flags = cv_policy_flag::none): + max_extent_(max_extent), flags_(flags) {} + + cv_policy_base_ptr clone() const override { + return cv_policy_base_ptr(new cv_policy_max_extent(*this)); + } + + locset cv_boundary_points(const cable_cell&) const override; + +private: + double max_extent_; + cv_policy_flag::value flags_; +}; + +struct cv_policy_fixed_per_branch: cv_policy_base { + explicit cv_policy_fixed_per_branch(unsigned cv_per_branch, cv_policy_flag::value flags = cv_policy_flag::none): + cv_per_branch_(cv_per_branch), flags_(flags) {} + + cv_policy_base_ptr clone() const override { + return cv_policy_base_ptr(new cv_policy_fixed_per_branch(*this)); + } + + locset cv_boundary_points(const cable_cell&) const override; + +private: + unsigned cv_per_branch_; + cv_policy_flag::value flags_; +}; + +inline cv_policy default_cv_policy() { + return cv_policy_fixed_per_branch(1); +} + // Cable cell ion and electrical defaults. // // Parameters can be overridden with `cable_cell_local_parameter_set` @@ -127,11 +256,18 @@ struct cable_cell_local_parameter_set { struct cable_cell_parameter_set: public cable_cell_local_parameter_set { std::unordered_map<std::string, mechanism_desc> reversal_potential_method; + cv_policy discretization = default_cv_policy(); // We'll need something like this until C++17, for sane initialization syntax. cable_cell_parameter_set() = default; - cable_cell_parameter_set(cable_cell_local_parameter_set p, std::unordered_map<std::string, mechanism_desc> m = {}): - cable_cell_local_parameter_set(std::move(p)), reversal_potential_method(std::move(m)) + cable_cell_parameter_set( + cable_cell_local_parameter_set p, + std::unordered_map<std::string, mechanism_desc> m = {}, + cv_policy d = default_cv_policy() + ): + cable_cell_local_parameter_set(std::move(p)), + reversal_potential_method(std::move(m)), + discretization(std::move(d)) {} }; diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp index 63e0e6923e1fc4ecf772cb6c7de0c53f43e1eb81..a9a64cbb663122577d07fe19ff076eb115efac26 100644 --- a/arbor/include/arbor/morph/locset.hpp +++ b/arbor/include/arbor/morph/locset.hpp @@ -75,6 +75,14 @@ public: return sum(sum(std::move(l), std::move(r)), std::move(args)...); } + // The union of two location sets. + friend locset join(locset, locset); + + template <typename ...Args> + friend locset join(locset l, locset r, Args... args) { + return join(join(std::move(l), std::move(r)), std::move(args)...); + } + private: struct interface { virtual ~interface() {} diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp index 16d41ec763479797434a39f533e6b29ec8afcc68..4e90d30a483cd03ebf53da76450622f02403cb64 100644 --- a/arbor/include/arbor/morph/morphology.hpp +++ b/arbor/include/arbor/morph/morphology.hpp @@ -23,6 +23,9 @@ public: morphology(sample_tree m); morphology(); + // Empty/default-constructed morphology? + bool empty() const; + // Whether the root of the morphology is spherical. bool spherical_root() const; diff --git a/arbor/morph/em_morphology.cpp b/arbor/morph/em_morphology.cpp index f00865e3c59259730e020fff7370d715451b0b5e..8e49c3a9d42401d95038d5fbbeea28edeb23237a 100644 --- a/arbor/morph/em_morphology.cpp +++ b/arbor/morph/em_morphology.cpp @@ -40,6 +40,9 @@ em_morphology::em_morphology(const morphology& m): auto idx = util::make_range(morph_.branch_indexes(i)); branch_lengths_.push_back(dist2root_[idx.back()]- dist2root_[idx.front()]); } + if (morph_.spherical_root()) { + branch_lengths_[0] = samples[0].loc.radius*2; + } // Cache the sample locations. // Iterate backwards over branches distal to root, so that the parent branch at @@ -132,13 +135,17 @@ mlocation_list em_morphology::cover(mlocation loc, bool include_loc) const { return L; } -mlocation em_morphology::canonicalize(mlocation loc) const { +void em_morphology::assert_valid_location(mlocation loc) const { if (!test_invariants(loc)) { throw morphology_error(util::pprintf("Invalid location {}", loc)); } if (loc.branch>=morph_.num_branches()) { throw morphology_error(util::pprintf("Location {} does not exist in morpology", loc)); } +} + +mlocation em_morphology::canonicalize(mlocation loc) const { + assert_valid_location(loc); // Test if location is at the start of a branch. if (loc.pos==0.) { diff --git a/arbor/morph/em_morphology.hpp b/arbor/morph/em_morphology.hpp index 1e47dd59b5dd4c32ec33fd1e5bfc7f11ba315063..1feb88cda374c17985a269856e2857bce91591d4 100644 --- a/arbor/morph/em_morphology.hpp +++ b/arbor/morph/em_morphology.hpp @@ -24,11 +24,24 @@ public: const morphology& morph() const; + // Convenience methods for morphology access + // that are forwarded directly to the morphology object: + + auto empty() const { return morph_.empty(); } + auto spherical_root() const { return morph_.spherical_root(); } + auto num_branches() const { return morph_.num_branches(); } + auto num_samples() const { return morph_.num_samples(); } + auto branch_parent(msize_t b) const { return morph_.branch_parent(b); } + auto branch_children(msize_t b) const { return morph_.branch_children(b); } + + // Access to computed and cached data: + mlocation_list terminals() const; mlocation root() const; mlocation sample2loc(msize_t sid) const; + void assert_valid_location(mlocation) const; mlocation canonicalize(mlocation) const; // Find all locations on the morphology that share the same canonoical @@ -37,6 +50,8 @@ public: mlocation_list cover(mlocation, bool include_loc=true) const; mlocation_list minset(const mlocation_list&) const; + + double branch_length(msize_t bid) const { return branch_lengths_.at(bid); } }; } // namespace arb diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp index 55c0752ccf366594db1c6fb211e91a4d85fe72f9..c21bb3d69ded5d2b146f525d1217ebcb99c3a50f 100644 --- a/arbor/morph/locset.cpp +++ b/arbor/morph/locset.cpp @@ -125,8 +125,8 @@ locset location(mlocation loc) { } mlocation_list thingify_(const location_& x, const em_morphology& m) { - // canonicalize will throw if the location is not present. - return {m.canonicalize(x.loc)}; + m.assert_valid_location(x.loc); + return {x.loc}; } std::ostream& operator<<(std::ostream& o, const location_& x) { diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp index ee78c8532ef3a2121aeed365ee0fe1b9cfb53999..34253041750bba913d66d0acf25bd4adc4401b94 100644 --- a/arbor/morph/morphology.cpp +++ b/arbor/morph/morphology.cpp @@ -79,7 +79,7 @@ std::vector<mbranch> branches_from_parent_index(const std::vector<msize_t>& pare } // Returns false if one of the root's children has the same tag as the root. -bool root_sample_has_same_tag_as_child(const sample_tree& st) { +bool root_sample_tag_differs_from_children(const sample_tree& st) { if (st.empty()) return false; auto& P = st.parents(); auto& S = st.samples(); @@ -128,7 +128,7 @@ morphology_impl::morphology_impl(sample_tree m, bool use_spherical_root): morphology_impl::morphology_impl(sample_tree m): samples_(std::move(m)), - spherical_root_(impl::root_sample_has_same_tag_as_child(samples_)) + spherical_root_(impl::root_sample_tag_differs_from_children(samples_)) { init(); } @@ -186,6 +186,10 @@ morphology::morphology(): morphology(sample_tree()) {} +bool morphology::empty() const { + return impl_->branches_.empty(); +} + // The parent branch of branch b. msize_t morphology::branch_parent(msize_t b) const { return impl_->branch_parents_[b]; diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 0cd9afad10ba24dd970ea26fb36726f9164a8bc2..910764d41e8830a88b1a3b6792dee921f518e82e 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -77,6 +77,7 @@ set(unit_sources test_cable_cell.cpp test_compartments.cpp test_counter.cpp + test_cv_policy.cpp test_cycle.cpp test_domain_decomposition.cpp test_dry_run_context.cpp diff --git a/test/unit/test_cv_policy.cpp b/test/unit/test_cv_policy.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a7a099682dcf54b38f57f660a5d87c343c590b43 --- /dev/null +++ b/test/unit/test_cv_policy.cpp @@ -0,0 +1,235 @@ +#include <iterator> +#include <numeric> +#include <utility> +#include <vector> + +#include <arbor/util/optional.hpp> +#include <arbor/cable_cell.hpp> +#include <arbor/cable_cell_param.hpp> +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/locset.hpp> + +#include "morph/em_morphology.hpp" +#include "util/filter.hpp" +#include "util/rangeutil.hpp" +#include "util/span.hpp" + +#include "common.hpp" +#include "unit_test_catalogue.hpp" +#include "../common_cells.hpp" + +using namespace arb; +using util::make_span; + +namespace { + std::vector<msample> make_samples(unsigned n) { + std::vector<msample> ms; + for (auto i: make_span(n)) ms.push_back({{0., 0., (double)i, 0.5}, 5}); + return ms; + } + + // Test morphologies for CV determination: + // Samples points have radius 0.5, giving an initial branch length of 1.0 + // for morphologies with spherical roots. + + const morphology m_empty; + + // spherical root, one branch + const morphology m_sph_b1{sample_tree(make_samples(1), {mnpos}), true}; + + // regular root, one branch + const morphology m_reg_b1{sample_tree(make_samples(2), {mnpos, 0u}), false}; + + // spherical root, six branches + const morphology m_sph_b6{sample_tree(make_samples(8), {mnpos, 0u, 1u, 0u, 3u, 4u, 4u, 4u}), true}; + + // regular root, six branches + const morphology m_reg_b6{sample_tree(make_samples(7), {mnpos, 0u, 1u, 1u, 2u, 2u, 2u}), false}; + + // regular root, six branches, mutiple top level branches. + const morphology m_mlt_b6{sample_tree(make_samples(7), {mnpos, 0u, 1u, 1u, 0u, 4u, 4u}), false}; + + template <typename... A> + locset as_locset(mlocation head, A... tail) { + return join(ls::location(head), ls::location(tail)...); + } + + template <typename Seq> + locset as_locset(const Seq& seq) { + using std::begin; + using std::end; + return std::accumulate(begin(seq), end(seq), ls::nil(), + [](locset ls, const mlocation& p) { return join(std::move(ls), ls::location(p)); }); + } +} + +TEST(cv_policy, explicit_policy) { + using L = mlocation; + locset lset = as_locset(L{0, 0}, L{0, 0.5}, L{0, 1.}, L{1, 0.5}, L{4, 0.2}); + + cv_policy pol = cv_policy_explicit(lset); + for (auto& m: {m_sph_b6, m_reg_b6, m_mlt_b6}) { + cable_cell cell = make_cable_cell(m); + + locset result = pol.cv_boundary_points(cell); + EXPECT_EQ(thingify(lset, *cell.morphology()), thingify(result, *cell.morphology())); + } +} + +TEST(cv_policy, empty_morphology) { + // Any policy applied to an empty morphology should give an empty locset, + // with the exception of cv_policy_explicit (this is still being debated). + + using namespace cv_policy_flag; + + cv_policy policies[] = { + cv_policy_fixed_per_branch(3), + cv_policy_fixed_per_branch(3, single_root_cv|interior_forks), + cv_policy_max_extent(0.234), + cv_policy_max_extent(0.234, single_root_cv|interior_forks) + }; + + cable_cell cell = make_cable_cell(m_empty); + auto empty_loclist = thingify(ls::nil(), *cell.morphology()); + + for (auto& pol: policies) { + EXPECT_EQ(empty_loclist, thingify(pol.cv_boundary_points(cell), *cell.morphology())); + } +} + +TEST(cv_policy, single_root_cv) { + // For policies that respect the single_root_cv flag, the boundary points should + // be the same as if the flag were not provided, except that the points on branch 0 + // should only ever be (0, 0) and (0, 1) if the morphology is not empty. + + using namespace cv_policy_flag; + + std::pair<cv_policy, cv_policy> policy_pairs[] = { + {cv_policy_fixed_per_branch(3), cv_policy_fixed_per_branch(3, single_root_cv)}, + {cv_policy_fixed_per_branch(3, interior_forks), cv_policy_fixed_per_branch(3, single_root_cv|interior_forks)}, + {cv_policy_max_extent(0.234), cv_policy_max_extent(0.234, single_root_cv)}, + {cv_policy_max_extent(0.234, interior_forks), cv_policy_max_extent(0.234, single_root_cv|interior_forks)} + }; + + for (auto& morph: {m_sph_b1, m_reg_b1, m_sph_b6, m_reg_b6, m_mlt_b6}) { + cable_cell cell = make_cable_cell(morph); + + for (auto& polpair: policy_pairs) { + mlocation_list p1 = thingify(polpair.first.cv_boundary_points(cell), *cell.morphology()); + mlocation_list p2 = thingify(polpair.second.cv_boundary_points(cell), *cell.morphology()); + + auto p1_no_b0 = util::filter(p1, [](mlocation l) { return l.branch>0; }); + mlocation_list expected = {{0,0}, {0,1}}; + expected.insert(expected.end(), p1_no_b0.begin(), p1_no_b0.end()); + + EXPECT_EQ(expected, p2); + } + } +} + +TEST(cv_policy, fixed_per_branch) { + using namespace cv_policy_flag; + using L = mlocation; + + // root branch only + for (auto& morph: {m_sph_b1, m_reg_b1}) { + cable_cell cell = make_cable_cell(morph); + { + // boundary fork points + cv_policy pol = cv_policy_fixed_per_branch(4); + locset points = pol.cv_boundary_points(cell); + locset expected = as_locset(L{0, 0}, L{0, 0.25}, L{0, 0.5}, L{0, 0.75}, L{0, 1}); + EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology())); + } + { + // interior fork points + cv_policy pol = cv_policy_fixed_per_branch(4, interior_forks); + locset points = pol.cv_boundary_points(cell); + locset expected = as_locset(L{0, 0.125}, L{0, 0.375}, L{0, 0.625}, L{0, 0.875}); + EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology())); + } + } + + // spherical root, six branches and multiple top level branches cases: + // expected points are the same. + for (auto& morph: {m_sph_b6, m_mlt_b6}) { + cable_cell cell = make_cable_cell(morph); + + { + // boundary fork points + cv_policy pol = cv_policy_fixed_per_branch(2); + locset points = pol.cv_boundary_points(cell); + locset expected = as_locset( + L{0, 0}, L{0, 0.5}, L{0,1}, L{1, 0}, L{1, 0.5}, L{1,1}, L{2, 0}, L{2, 0.5}, L{2,1}, + L{3, 0}, L{3, 0.5}, L{3,1}, L{4, 0}, L{4, 0.5}, L{4,1}, L{5, 0}, L{5, 0.5}, L{5,1} + ); + EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology())); + } + { + // interior fork points + cv_policy pol = cv_policy_fixed_per_branch(2, interior_forks); + locset points = pol.cv_boundary_points(cell); + locset expected = as_locset( + L{0, 0.25}, L{0, 0.75}, L{1, 0.25}, L{1, 0.75}, L{2, 0.25}, L{2, 0.75}, + L{3, 0.25}, L{3, 0.75}, L{4, 0.25}, L{4, 0.75}, L{5, 0.25}, L{5, 0.75} + ); + EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology())); + } + } +} + +TEST(cv_policy, max_extent) { + using namespace cv_policy_flag; + using L = mlocation; + + // root branch only + for (auto& morph: {m_sph_b1, m_reg_b1}) { + cable_cell cell = make_cable_cell(morph); + ASSERT_EQ(1.0, cell.morphology()->branch_length(0)); + + { + // extent of 0.25 should give exact fp calculation, giving + // 4 CVs on the root branch. + cv_policy pol = cv_policy_max_extent(0.25); + locset points = pol.cv_boundary_points(cell); + locset expected = as_locset(L{0, 0}, L{0, 0.25}, L{0, 0.5}, L{0, 0.75}, L{0, 1}); + EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology())); + } + { + cv_policy pol = cv_policy_max_extent(0.25, interior_forks); + locset points = pol.cv_boundary_points(cell); + locset expected = as_locset(L{0, 0.125}, L{0, 0.375}, L{0, 0.625}, L{0, 0.875}); + EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology())); + } + } + + // cell with varying branch lengths; extent not exact fraction. + { + cable_cell cell = make_cable_cell(m_mlt_b6); + ASSERT_EQ(1.0, cell.morphology()->branch_length(0)); + ASSERT_EQ(1.0, cell.morphology()->branch_length(1)); + ASSERT_EQ(2.0, cell.morphology()->branch_length(2)); + ASSERT_EQ(4.0, cell.morphology()->branch_length(3)); + ASSERT_EQ(1.0, cell.morphology()->branch_length(4)); + ASSERT_EQ(2.0, cell.morphology()->branch_length(5)); + + { + // max extent of 0.6 should give two CVs on branches of length 1, + // four CVs on branches of length 2, and seven CVs on the branch of length 4. + cv_policy pol = cv_policy_max_extent(0.6); + mlocation_list points = thingify(pol.cv_boundary_points(cell), *cell.morphology()); + + mlocation_list points_b012 = util::assign_from(util::filter(points, [](mlocation l) { return l.branch<3; })); + mlocation_list expected_b012 = { + {0, 0}, {0, 0.5}, {0, 1}, + {1, 0}, {1, 0.5}, {1, 1}, + {2, 0}, {2, 0.25}, {2, 0.5}, {2, 0.75}, {2, 1} + }; + EXPECT_EQ(expected_b012, points_b012); + + mlocation_list points_b3 = util::assign_from(util::filter(points, [](mlocation l) { return l.branch==3; })); + EXPECT_EQ(8u, points_b3.size()); + } + } +} + diff --git a/test/unit/test_em_morphology.cpp b/test/unit/test_em_morphology.cpp index 39528837f92c891ecc1f141e6ff71de20dcac907..36cbf05f5b1e0a686445c70db71cd47d3097f70a 100644 --- a/test/unit/test_em_morphology.cpp +++ b/test/unit/test_em_morphology.cpp @@ -54,6 +54,8 @@ TEST(em_morphology, cache) { EXPECT_EQ(em.root(), (loc{0,0})); EXPECT_EQ(em.terminals(), (arb::mlocation_list{{0,1}})); + + EXPECT_EQ(10., em.branch_length(0)); } // Eight samples @@ -91,6 +93,13 @@ TEST(em_morphology, cache) { EXPECT_EQ(em.root(), (loc{0,0})); EXPECT_EQ(em.terminals(), (arb::mlocation_list{{1,1}, {3,1}, {4,1}})); + + ASSERT_EQ(5u, em.morph().num_branches()); + EXPECT_EQ(20., em.branch_length(0)); + EXPECT_EQ(90., em.branch_length(1)); + EXPECT_EQ(90., em.branch_length(2)); + EXPECT_EQ(100., em.branch_length(3)); + EXPECT_EQ(200., em.branch_length(4)); } { // No Spherical root pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6}; @@ -119,6 +128,12 @@ TEST(em_morphology, cache) { EXPECT_EQ(em.root(), (loc{0,0})); EXPECT_EQ(em.terminals(), (arb::mlocation_list{{0,1}, {2,1}, {3,1}})); + + ASSERT_EQ(4u, em.morph().num_branches()); + EXPECT_EQ(100., em.branch_length(0)); + EXPECT_EQ(100., em.branch_length(1)); + EXPECT_EQ(100., em.branch_length(2)); + EXPECT_EQ(200., em.branch_length(3)); } } @@ -348,10 +363,10 @@ TEST(locset, thingify) { EXPECT_EQ(thingify(midb2, em), (ll{{2,0.5}})); EXPECT_EQ(thingify(midb1, em), (ll{{1,0.5}})); EXPECT_EQ(thingify(begb0, em), (ll{{0,0}})); - EXPECT_EQ(thingify(begb1, em), (ll{{0,1}})); - EXPECT_EQ(thingify(begb2, em), (ll{{0,1}})); - EXPECT_EQ(thingify(begb3, em), (ll{{2,1}})); - EXPECT_EQ(thingify(begb4, em), (ll{{2,1}})); + EXPECT_EQ(thingify(begb1, em), (ll{{1,0}})); + EXPECT_EQ(thingify(begb2, em), (ll{{2,0}})); + EXPECT_EQ(thingify(begb3, em), (ll{{3,0}})); + EXPECT_EQ(thingify(begb4, em), (ll{{4,0}})); } { arb::em_morphology em(arb::morphology(sm, false)); @@ -362,9 +377,9 @@ TEST(locset, thingify) { EXPECT_EQ(thingify(midb2, em), (ll{{2,0.5}})); EXPECT_EQ(thingify(midb1, em), (ll{{1,0.5}})); EXPECT_EQ(thingify(begb0, em), (ll{{0,0}})); - EXPECT_EQ(thingify(begb1, em), (ll{{0,0}})); - EXPECT_EQ(thingify(begb2, em), (ll{{1,1}})); - EXPECT_EQ(thingify(begb3, em), (ll{{1,1}})); + EXPECT_EQ(thingify(begb1, em), (ll{{1,0}})); + EXPECT_EQ(thingify(begb2, em), (ll{{2,0}})); + EXPECT_EQ(thingify(begb3, em), (ll{{3,0}})); // In the absence of a spherical root, there is no branch 4. EXPECT_THROW(thingify(begb4, em), arb::morphology_error); }