diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt index 42a66fc7693b13594690bbcb0a4c97f2e7b0d257..a6a497a9d21c0bb9ab0ea718f4138e43e2115c11 100644 --- a/arbor/CMakeLists.txt +++ b/arbor/CMakeLists.txt @@ -15,6 +15,7 @@ set(arbor_sources cable_cell_param.cpp cell_group_factory.cpp common_types_io.cpp + cv_policy.cpp execution_context.cpp gpu_context.cpp event_binner.cpp diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp index 417229bb7e296fabe9b93f8ccc05b76fbf3d5508..b20a93a1b9067e3d0ef36106603380d8ff05f9a3 100644 --- a/arbor/cable_cell_param.cpp +++ b/arbor/cable_cell_param.cpp @@ -5,11 +5,8 @@ #include <arbor/cable_cell.hpp> #include <arbor/cable_cell_param.hpp> -#include <arbor/morph/locset.hpp> #include "util/maputil.hpp" -#include "util/rangeutil.hpp" -#include "util/span.hpp" namespace arb { @@ -71,109 +68,4 @@ cable_cell_parameter_set neuron_parameter_defaults = { }, }; -// Discretization policy implementations: - -locset cv_policy_max_extent::cv_boundary_points(const cable_cell& cell) const { - const unsigned nbranch = cell.morphology().num_branches(); - const auto& embed = cell.embedding(); - 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(embed.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; - } - - util::sort(points); - return points; -} - -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; - } - - util::sort(points); - return points; -} - -locset cv_policy_every_sample::cv_boundary_points(const cable_cell& cell) const { - const unsigned nbranch = cell.morphology().num_branches(); - if (!nbranch) return ls::nil(); - - bool single_root = cell.morphology().spherical_root() || (flags_&cv_policy_flag::single_root_cv); - - // Ignore interior_forks flag, but if single_root_cv is set, take sample indices only from branches 1+. - // Always include branch proximal points, so that forks are trivial. - - - if (single_root) { - std::vector<msize_t> samples; - for (unsigned i = 1; i<nbranch; ++i) { - util::append(samples, util::make_range(cell.morphology().branch_indexes(i))); - } - util::sort(samples); - samples.erase(std::unique(samples.begin(), samples.end()), samples.end()); - - return join( - ls::on_branches(0.), - ls::location(0, 1.), - std::accumulate(samples.begin(), samples.end(), ls::nil(), - [](auto&& l, auto&& r) { return sum(std::move(l), ls::sample(r)); })); - } - else { - auto samples = util::make_span(cell.morphology().num_samples()); - return join( - ls::on_branches(0.), - std::accumulate(samples.begin(), samples.end(), ls::nil(), - [](auto&& l, auto&& r) { return sum(std::move(l), ls::sample(r)); })); - } -} - } // namespace arb diff --git a/arbor/cv_policy.cpp b/arbor/cv_policy.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fa39de7d481c78f0eac865f6b763498636eb1b34 --- /dev/null +++ b/arbor/cv_policy.cpp @@ -0,0 +1,158 @@ +#include <utility> +#include <vector> + +#include <arbor/cable_cell.hpp> +#include <arbor/cv_policy.hpp> +#include <arbor/morph/locset.hpp> +#include <arbor/morph/region.hpp> + +#include "util/rangeutil.hpp" +#include "util/span.hpp" + +// Discretization policy implementations: + +namespace arb { + +static auto unique_sum = [](auto&&... lss) { + return ls::support(sum(std::forward<decltype(lss)>(lss)...)); +}; + +// Combinators: +// cv_policy_plus_ represents the result of operator+, +// cv_policy_bar_ represents the result of operator|. + +struct cv_policy_plus_: cv_policy_base { + cv_policy_plus_(const cv_policy& lhs, const cv_policy& rhs): + lhs_(lhs), rhs_(rhs) {} + + cv_policy_base_ptr clone() const override { + return cv_policy_base_ptr(new cv_policy_plus_(*this)); + } + + locset cv_boundary_points(const cable_cell& c) const override { + return unique_sum(lhs_.cv_boundary_points(c), rhs_.cv_boundary_points(c)); + } + + region domain() const override { return join(lhs_.domain(), rhs_.domain()); } + + cv_policy lhs_, rhs_; +}; + +cv_policy operator+(const cv_policy& lhs, const cv_policy& rhs) { + return cv_policy_plus_(lhs, rhs); +} + +struct cv_policy_bar_: cv_policy_base { + cv_policy_bar_(const cv_policy& lhs, const cv_policy& rhs): + lhs_(lhs), rhs_(rhs) {} + + cv_policy_base_ptr clone() const override { + return cv_policy_base_ptr(new cv_policy_bar_(*this)); + } + + locset cv_boundary_points(const cable_cell& c) const override { + return unique_sum(ls::restrict(lhs_.cv_boundary_points(c), complement(rhs_.domain())), rhs_.cv_boundary_points(c)); + } + + region domain() const override { return join(lhs_.domain(), rhs_.domain()); } + + cv_policy lhs_, rhs_; +}; + +cv_policy operator|(const cv_policy& lhs, const cv_policy& rhs) { + return cv_policy_bar_(lhs, rhs); +} + +// Public policy implementations: + +locset cv_policy_explicit::cv_boundary_points(const cable_cell& cell) const { + return + ls::support( + util::foldl( + [this](locset l, const auto& comp) { + return sum(std::move(l), ls::restrict(locs_, comp)); + }, + ls::boundary(domain_), + components(cell.morphology(), thingify(domain_, cell.provider())))); +} + +locset cv_policy_max_extent::cv_boundary_points(const cable_cell& cell) const { + const unsigned nbranch = cell.morphology().num_branches(); + const auto& embed = cell.embedding(); + if (!nbranch || max_extent_<=0) return ls::nil(); + + std::vector<mlocation> points; + double oomax_extent = 1./max_extent_; + auto comps = components(cell.morphology(), thingify(domain_, cell.provider())); + + for (auto& comp: comps) { + for (mcable c: comp) { + double cable_length = embed.integrate_length(c); + unsigned ncv = std::ceil(cable_length*oomax_extent); + double scale = (c.dist_pos-c.prox_pos)/ncv; + + if (flags_&cv_policy_flag::interior_forks) { + for (unsigned i = 0; i<ncv; ++i) { + points.push_back({c.branch, c.prox_pos+(1+2*i)*scale/2}); + } + } + else { + for (unsigned i = 0; i<ncv; ++i) { + points.push_back({c.branch, c.prox_pos+i*scale}); + } + points.push_back({c.branch, c.dist_pos}); + } + } + } + + util::sort(points); + return unique_sum(locset(std::move(points)), ls::cboundary(domain_)); +} + +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; + double ooncv = 1./cv_per_branch_; + auto comps = components(cell.morphology(), thingify(domain_, cell.provider())); + + for (auto& comp: comps) { + for (mcable c: comp) { + double scale = (c.dist_pos-c.prox_pos)*ooncv; + + if (flags_&cv_policy_flag::interior_forks) { + for (unsigned i = 0; i<cv_per_branch_; ++i) { + points.push_back({c.branch, c.prox_pos+(1+2*i)*scale/2}); + } + } + else { + for (unsigned i = 0; i<cv_per_branch_; ++i) { + points.push_back({c.branch, c.prox_pos+i*scale}); + } + points.push_back({c.branch, c.dist_pos}); + } + } + } + + util::sort(points); + return unique_sum(locset(std::move(points)), ls::cboundary(domain_)); +} + +locset cv_policy_every_sample::cv_boundary_points(const cable_cell& cell) const { + const unsigned nbranch = cell.morphology().num_branches(); + if (!nbranch) return ls::nil(); + + // Always include branch proximal points, so that forks are trivial. + + return + unique_sum(ls::cboundary(domain_), + ls::restrict( + util::foldl( + [](locset l, msize_t sidx) { return sum(std::move(l), ls::sample(sidx)); }, + ls::on_branches(0), + util::make_span(cell.morphology().num_samples())), + domain_)); +} + +} // namespace arb diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp index 2e041e7dfd4e823984fe297cb269d6352a06cd56..804ab131cfe3a2820ca28ddef47394f388955c3f 100644 --- a/arbor/include/arbor/cable_cell_param.hpp +++ b/arbor/include/arbor/cable_cell_param.hpp @@ -1,6 +1,7 @@ #pragma once #include <arbor/arbexcept.hpp> +#include <arbor/cv_policy.hpp> #include <arbor/mechcat.hpp> #include <arbor/morph/locset.hpp> #include <arbor/util/optional.hpp> @@ -144,151 +145,6 @@ struct ion_reversal_potential_method { mechanism_desc method; }; -// 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(const cv_policy& other): - policy_ptr(other.policy_ptr->clone()) {} - - cv_policy& operator=(const cv_policy& other) { - policy_ptr = other.policy_ptr->clone(); - return *this; - } - - cv_policy(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_; -}; - -struct cv_policy_every_sample: cv_policy_base { - explicit cv_policy_every_sample(cv_policy_flag::value flags = cv_policy_flag::none): - flags_(flags) {} - - cv_policy_base_ptr clone() const override { - return cv_policy_base_ptr(new cv_policy_every_sample(*this)); - } - - locset cv_boundary_points(const cable_cell&) const override; - -private: - 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 given as per-cell and global defaults via diff --git a/arbor/include/arbor/cv_policy.hpp b/arbor/include/arbor/cv_policy.hpp new file mode 100644 index 0000000000000000000000000000000000000000..f59fe418fc6a027daca96dbc3e975a2c4d66c45a --- /dev/null +++ b/arbor/include/arbor/cv_policy.hpp @@ -0,0 +1,206 @@ +#pragma once + +#include <memory> +#include <utility> + +#include <arbor/morph/region.hpp> +#include <arbor/morph/locset.hpp> + +// 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 a handful of 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_single: +// One CV for whole region. +// +// 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. +// +// The policies above can be restricted to apply only to a given region of a +// cell morphology. If a region is supplied, the CV policy is applied to the +// completion of each connected component of the morphology within the region, +// as if the subtree described by each component were itself a full cell +// morphology. The boundary points of these completed components are always +// included as boundary points provided by the policy. +// +// Except for the single and explicit policies, 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 objects can be combined. For two CV polices A & B, +// +// A + B: +// Use the CV boundary points from both A and B. +// +// A | B: +// Use the CV boundary points from A except for on the region where +// B is defined, where the boundary points from B are used. + +namespace arb { + +class cable_cell; + +struct cv_policy_base { + virtual locset cv_boundary_points(const cable_cell& cell) const = 0; + virtual region domain() 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(const cv_policy& other): + policy_ptr(other.policy_ptr->clone()) {} + + cv_policy& operator=(const cv_policy& other) { + policy_ptr = other.policy_ptr->clone(); + return *this; + } + + cv_policy(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); + } + + region domain() const { + return policy_ptr->domain(); + } + +private: + cv_policy_base_ptr policy_ptr; +}; + +cv_policy operator+(const cv_policy&, const cv_policy&); +cv_policy operator|(const cv_policy&, const cv_policy&); + + +// Common flags for CV policies; bitwise composable. +namespace cv_policy_flag { + using value = unsigned; + enum : unsigned { + none = 0, + interior_forks = 1<<0 + }; +} + +struct cv_policy_explicit: cv_policy_base { + explicit cv_policy_explicit(locset locs, region domain = reg::all()): + locs_(std::move(locs)), domain_(std::move(domain)) {} + + 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; + region domain() const override { return domain_; } + +private: + locset locs_; + region domain_; +}; + +struct cv_policy_single: cv_policy_base { + explicit cv_policy_single(region domain = reg::all()): + domain_(domain) {} + + cv_policy_base_ptr clone() const override { + return cv_policy_base_ptr(new cv_policy_single(*this)); + } + + locset cv_boundary_points(const cable_cell&) const override { + return ls::cboundary(domain_); + } + region domain() const override { return domain_; } + +private: + region domain_; +}; + +struct cv_policy_max_extent: cv_policy_base { + cv_policy_max_extent(double max_extent, region domain, cv_policy_flag::value flags = cv_policy_flag::none): + max_extent_(max_extent), domain_(std::move(domain)), flags_(flags) {} + + explicit cv_policy_max_extent(double max_extent, cv_policy_flag::value flags = cv_policy_flag::none): + max_extent_(max_extent), domain_(reg::all()), 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; + region domain() const override { return domain_; } + +private: + double max_extent_; + region domain_; + cv_policy_flag::value flags_; +}; + +struct cv_policy_fixed_per_branch: cv_policy_base { + cv_policy_fixed_per_branch(unsigned cv_per_branch, region domain, cv_policy_flag::value flags = cv_policy_flag::none): + cv_per_branch_(cv_per_branch), domain_(std::move(domain)), flags_(flags) {} + + 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), domain_(reg::all()), 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; + region domain() const override { return domain_; } + +private: + unsigned cv_per_branch_; + region domain_; + cv_policy_flag::value flags_; +}; + +struct cv_policy_every_sample: cv_policy_base { + explicit cv_policy_every_sample(region domain = reg::all()): + domain_(std::move(domain)) {} + + cv_policy_base_ptr clone() const override { + return cv_policy_base_ptr(new cv_policy_every_sample(*this)); + } + + locset cv_boundary_points(const cable_cell&) const override; + region domain() const override { return domain_; } + +private: + region domain_; + cv_policy_flag::value flags_; +}; + +inline cv_policy default_cv_policy() { + return cv_policy_fixed_per_branch(1); +} + +} // namespace arb diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp index a815b67f05c48294eaa7506e765aaa7f31e5f437..d6e3772b5970a83443530d70f2ba4c943eedd326 100644 --- a/arbor/include/arbor/morph/locset.hpp +++ b/arbor/include/arbor/morph/locset.hpp @@ -139,9 +139,16 @@ locset nil(); // Most distal points of a region. locset most_distal(region reg); -// Most proximal point of a region. +// Most proximal points of a region. locset most_proximal(region reg); +// Boundary points of a region. +locset boundary(region reg); + +// Completed boundary points of a region. +// (Boundary of completed components.) +locset cboundary(region reg); + // Returns all locations in a locset that are also in the region. locset restrict(locset ls, region reg); @@ -152,10 +159,14 @@ locset uniform(region reg, unsigned left, unsigned right, uint64_t seed); // Proportional location on every branch. locset on_branches(double pos); +// Support of a locset (x s.t. x in locset). +locset support(locset); + } // namespace ls // Union of two locsets. locset join(locset, locset); + // Multiset sum of two locsets. locset sum(locset, locset); diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp index 7c00efed00577a27f46ac025d416e62a43adb965..208c9926f6941ddd7d5cc3e1355921b2132b3d34 100644 --- a/arbor/include/arbor/morph/morphology.hpp +++ b/arbor/include/arbor/morph/morphology.hpp @@ -61,18 +61,6 @@ public: friend std::ostream& operator<<(std::ostream&, const morphology&); }; -// Morphology utility functions. - -// Find the set of locations in an mlocation_list for which there -// are no other locations that are more proximal in that list. -mlocation_list minset(const morphology&, const mlocation_list&); - -// Find the set of locations in an mlocation_list for which there -// are no other locations that are more distal in the list. -mlocation_list maxset(const morphology&, const mlocation_list&); - -mlocation canonical(const morphology&, mlocation); - // Represent a (possibly empty or disconnected) region on a morphology. // // Wraps an mcable_list, and satisfies the additional constraint that @@ -135,8 +123,50 @@ private: mcable_list cables_; }; +// Morphology utility functions. + +mlocation canonical(const morphology&, mlocation); + +// Find the set of locations in an mlocation_list for which there +// are no other locations that are more proximal in that list. +mlocation_list minset(const morphology&, const mlocation_list&); + +// Find the set of locations in an mlocation_list for which there +// are no other locations that are more distal in the list. +mlocation_list maxset(const morphology&, const mlocation_list&); + // Reduced representation of an extent, excluding zero-length cables // that are covered by more proximal or non-zero-length cables. mcable_list canonical(const morphology& m, const mextent& a); +// Determine the components of an extent. +// +// Let T be the topological tree described by a morphology and C be the +// cover, comprising the disjoint union of unit intervals, one per branch. +// +// Let π be the projection from C onto T. +// +// Locations in C are ordered by distality: (b1, x1) < (b2, x2) if branches b1 +// and b2 are the same and x1<x2, or else if b1 is a more proximal branch than +// b2. +// +// Locations in T are ordered by distality: given points a and b in C, +// π(a) < π(b) if a<b and π(a) is not equal to π(b). +// +// (NOTE: the notion of the cover may be extended in the future to include +// a 'most proximal point' (-1, 1) which projects to the root of the tree, +// and which is strictly more proximal than any other point in the cover.) +// +// Let two locations a,b in an extent X of C be directed-path-connected if +// there is an order-preserving map p: [0, 1] -> C such that π∘p is a +// path in T, with p(0) = a and p(1) = b. +// +// The components E_i of an extent X are subsets such that for all x and y +// in E_i, there exists a location a with both a, x and a, y +// directed-path-connected in X, and such that for all x in E_i and all y in +// E_j, with i not equal to j, x and y are not directed-path-connected in X. + +std::vector<mextent> components(const morphology& m, const mextent&); + + } // namespace arb diff --git a/arbor/include/arbor/morph/primitives.hpp b/arbor/include/arbor/morph/primitives.hpp index 7fc8e87b256ebd9c0e507d0f3cc979f0c764eab8..f325cc20e6baba6641bbd3f8415f79012ada1fbb 100644 --- a/arbor/include/arbor/morph/primitives.hpp +++ b/arbor/include/arbor/morph/primitives.hpp @@ -88,6 +88,7 @@ bool test_invariants(const mlocation_list&); mlocation_list sum(const mlocation_list&, const mlocation_list&); mlocation_list join(const mlocation_list&, const mlocation_list&); mlocation_list intersection(const mlocation_list&, const mlocation_list&); +mlocation_list support(mlocation_list); // Describe an unbranched cable in the morphology. // @@ -104,8 +105,12 @@ struct mcable { double prox_pos; // ∈ [0,1] double dist_pos; // ∈ [0,1] - friend mlocation prox_loc(const mcable&); - friend mlocation dist_loc(const mcable&); + friend mlocation prox_loc(const mcable& c) { + return {c.branch, c.prox_pos}; + } + friend mlocation dist_loc(const mcable& c) { + return {c.branch, c.dist_pos}; + } // branch ≠npos, and 0 ≤ prox_pos ≤ dist_pos ≤ 1 friend bool test_invariants(const mcable&); diff --git a/arbor/include/arbor/morph/region.hpp b/arbor/include/arbor/morph/region.hpp index 19fbcbaaa2ceef56aebb3cff151a62b413e78e98..5e60d945a372d1763ba28feda44709ceb4d68871 100644 --- a/arbor/include/arbor/morph/region.hpp +++ b/arbor/include/arbor/morph/region.hpp @@ -158,8 +158,9 @@ region z_dist_from_root_ge(double r); // Region with all segments in a cell. region all(); -// The extent of a region, i.e. including all fork cover points. -region super(region); +// Region including all covers of included fork points. +// (Pre-image of projection onto the topological tree.) +region complete(region); // Region associated with a name. region named(std::string); @@ -172,4 +173,10 @@ region join(region, region); // Intersection of two regions. region intersect(region, region); +// Closed complement of a region. +region complement(region); + +// (Closure of) set difference of two regions. +region difference(region a, region b); + } // namespace arb diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp index 0e4df9c32ed6dc358831e22656b541bae7f37101..393f9b025a35939fcd6ae1430b641e806eb5acfb 100644 --- a/arbor/morph/locset.cpp +++ b/arbor/morph/locset.cpp @@ -242,6 +242,80 @@ std::ostream& operator<<(std::ostream& o, const most_proximal_& x) { return o << "(proximal \"" << x.reg << "\")"; } +// Boundary points of a region. +// +// The boundary points of a region R are defined as the most proximal +// and most distal locations in the components of R. + +struct boundary_: locset_tag { + explicit boundary_(region reg): reg(std::move(reg)) {} + region reg; +}; + +locset boundary(region reg) { + return locset(boundary_(std::move(reg))); +}; + +mlocation_list thingify_(const boundary_& n, const mprovider& p) { + std::vector<mextent> comps = components(p.morphology(), thingify(n.reg, p)); + + mlocation_list L; + + for (const mextent& comp: comps) { + mlocation_list proximal_set, distal_set; + + for (const mcable& c: comp) { + proximal_set.push_back({c.branch, c.prox_pos}); + distal_set.push_back({c.branch, c.dist_pos}); + } + + L = sum(L, minset(p.morphology(), proximal_set)); + L = sum(L, maxset(p.morphology(), distal_set)); + } + return support(std::move(L)); +} + +std::ostream& operator<<(std::ostream& o, const boundary_& x) { + return o << "(boundary " << x.reg << ")"; +} + +// Completed boundary points of a region. +// +// The completed boundary is the boundary of the completion of +// each component. + +struct cboundary_: locset_tag { + explicit cboundary_(region reg): reg(std::move(reg)) {} + region reg; +}; + +locset cboundary(region reg) { + return locset(cboundary_(std::move(reg))); +}; + +mlocation_list thingify_(const cboundary_& n, const mprovider& p) { + std::vector<mextent> comps = components(p.morphology(), thingify(n.reg, p)); + + mlocation_list L; + + for (const mextent& comp: comps) { + mlocation_list proximal_set, distal_set; + + mextent ccomp = thingify(reg::complete(comp), p); + for (const mcable& c: ccomp.cables()) { + proximal_set.push_back({c.branch, c.prox_pos}); + distal_set.push_back({c.branch, c.dist_pos}); + } + + L = sum(L, minset(p.morphology(), proximal_set)); + L = sum(L, maxset(p.morphology(), distal_set)); + } + return support(std::move(L)); +} + +std::ostream& operator<<(std::ostream& o, const cboundary_& x) { + return o << "(cboundary " << x.reg << ")"; +} // Uniform locset. @@ -348,6 +422,25 @@ std::ostream& operator<<(std::ostream& o, const lsum& x) { return o << "(sum " << x.lhs << " " << x.rhs << ")"; } +// Support of point set. + +struct lsup_: locset_tag { + locset arg; + lsup_(locset arg): arg(std::move(arg)) {} +}; + +locset support(locset arg) { + return locset{lsup_{std::move(arg)}}; +} + +mlocation_list thingify_(const lsup_& P, const mprovider& p) { + return support(thingify(P.arg, p)); +}; + +std::ostream& operator<<(std::ostream& o, const lsup_& x) { + return o << "(support " << x.arg << ")"; +} + // Restrict a locset on to a region: returns all locations in the locset that // are also in the region. diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp index 4f131e6be09633d07e38309bcc06ff26dd7919e7..623eb7d9bc0d51c79fde9724796606f075c08dfc 100644 --- a/arbor/morph/morphology.cpp +++ b/arbor/morph/morphology.cpp @@ -447,5 +447,32 @@ mextent join(const mextent& a, const mextent& b) { return m; } +std::vector<mextent> components(const morphology& m, const mextent& ex) { + std::unordered_map<mlocation, unsigned> component_index; + std::vector<mcable_list> component_cables; + + for (mcable c: ex) { + mlocation head = canonical(m, prox_loc(c)); + + unsigned index; + if (component_index.count(head)) { + index = component_index.at(head); + } + else { + index = component_cables.size(); + component_cables.push_back({}); + } + + component_cables[index].push_back(c); + component_index[dist_loc(c)] = index; + } + + std::vector<mextent> components; + for (auto& cl: component_cables) { + components.emplace_back(std::move(cl)); + } + return components; +} + } // namespace arb diff --git a/arbor/morph/primitives.cpp b/arbor/morph/primitives.cpp index f98ddfcf5d0423b4f736f8fb64fce390d01372b9..40d9061b4ad430697f8236017c611095a9d265a7 100644 --- a/arbor/morph/primitives.cpp +++ b/arbor/morph/primitives.cpp @@ -7,6 +7,7 @@ #include "io/sepval.hpp" #include "util/span.hpp" #include "util/rangeutil.hpp" +#include "util/unique.hpp" namespace arb { @@ -125,16 +126,13 @@ mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs return L; } -bool test_invariants(const mcable& c) { - return (0.<=c.prox_pos && c.prox_pos<=c.dist_pos && c.dist_pos<=1.) && c.branch!=mnpos; -} - -mlocation prox_loc(const mcable& c) { - return {c.branch, c.prox_pos}; +mlocation_list support(mlocation_list L) { + util::unique_in_place(L); + return L; } -mlocation dist_loc(const mcable& c) { - return {c.branch, c.dist_pos}; +bool test_invariants(const mcable& c) { + return (0.<=c.prox_pos && c.prox_pos<=c.dist_pos && c.dist_pos<=1.) && c.branch!=mnpos; } bool test_invariants(const mcable_list& l) { diff --git a/arbor/morph/region.cpp b/arbor/morph/region.cpp index 90c30690c75d2f410f8a8961939bbcf71a2cf013..89bfce428cf09eaf90e274edbc60c544f99dafd2 100644 --- a/arbor/morph/region.cpp +++ b/arbor/morph/region.cpp @@ -558,7 +558,7 @@ struct super_ { region reg; }; -region super(region r) { +region complete(region r) { return region(super_{std::move(r)}); } @@ -617,7 +617,7 @@ mextent thingify_(const super_& r, const mprovider& p) { } std::ostream& operator<<(std::ostream& o, const super_& r) { - return o << "(super " << r.reg << ")"; + return o << "(complete " << r.reg << ")"; } @@ -654,11 +654,69 @@ std::ostream& operator<<(std::ostream& o, const reg_or& x) { return o << "(join " << x.lhs << " " << x.rhs << ")"; } + +// Complement of a region. + +struct reg_not: region_tag { + region r; + explicit reg_not(region r): r(std::move(r)) {} +}; + +mextent thingify_(const reg_not& P, const mprovider& p) { + auto nb = p.morphology().num_branches(); + mcable_list result; + + mextent rex = thingify(P.r, p); + auto rex_i = rex.begin(); + + for (auto i: util::make_span(nb)) { + while (rex_i!=rex.end() && rex_i->branch<i) ++rex_i; + + double x = 0; + while (rex_i!=rex.end() && rex_i->branch==i) { + double y = rex_i->prox_pos; + if (y>x) { + result.push_back(mcable{i, x, y}); + } + + x = rex_i->dist_pos; + ++rex_i; + } + + if (x<1) { + result.push_back(mcable{i, x, 1}); + } + } + + return mextent(result); +} + +std::ostream& operator<<(std::ostream& o, const reg_not& x) { + return o << "(complement " << x.r << ")"; +} + + +// Closed set difference of two regions. + +struct reg_minus: region_tag { + region lhs; + region rhs; + reg_minus(region lhs, region rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {} +}; + +mextent thingify_(const reg_minus& P, const mprovider& p) { + return thingify(intersect(std::move(P.lhs), complement(std::move(P.rhs))), p); +} + +std::ostream& operator<<(std::ostream& o, const reg_minus& x) { + return o << "(difference " << x.lhs << " " << x.rhs << ")"; +} + } // namespace reg -// The intersect and join operations in the arb:: namespace with region so that -// ADL allows for construction of expressions with regions without having -// to namespace qualify the intersect/join. +// The intersect, join, complement and difference operations are in the arb:: +// namespace with region so that ADL allows for construction of expressions +// with regions without having to namespace qualify these operations. region intersect(region l, region r) { return region{reg::reg_and(std::move(l), std::move(r))}; @@ -668,6 +726,14 @@ region join(region l, region r) { return region{reg::reg_or(std::move(l), std::move(r))}; } +region complement(region r) { + return region{reg::reg_not(std::move(r))}; +} + +region difference(region l, region r) { + return region{reg::reg_minus(std::move(l), std::move(r))}; +} + region::region() { *this = reg::nil(); } diff --git a/arbor/util/rangeutil.hpp b/arbor/util/rangeutil.hpp index 4e45d8c01e7c306c6cdb0bb497a44502a517b679..7e6a5fda30db450992cc4ed81f695fb4dd5dc9fa 100644 --- a/arbor/util/rangeutil.hpp +++ b/arbor/util/rangeutil.hpp @@ -399,6 +399,39 @@ range<Rev, Rev> reverse_view(Seq&& seq) { return range<Rev, Rev>(Rev(strict.right), Rev(strict.left)); } +// Left fold (accumulate) over sequence. +// +// Note that the order of arguments follows the application order; +// schematically: +// +// foldl f a [] = a +// foldl f a (b:bs) = foldl f (f a b) bs +// +// The binary operator f will be invoked once per element in the +// sequence in turn, with the running accumulator as the first +// argument. If the iterators for the sequence deference to a +// mutable lvalue, then mutation of the value in the input sequence +// is explicitly permitted. +// +// std::accumulate(begin, end, init, f) is equivalent to +// util::foldl(f, init, util::make_range(begin, end)). + +template <typename Seq, typename Acc, typename BinOp> +auto foldl(BinOp f, Acc a, Seq&& seq) { + using std::begin; + using std::end; + + auto b = begin(seq); + auto e = end(seq); + + while (b!=e) { + a = f(std::move(a), *b); + ++b; + } + return a; +} + + } // namespace util } // namespace arb diff --git a/doc/cpp_cable_cell.rst b/doc/cpp_cable_cell.rst index 02f13b3ed3335ae4a2d29ea032630f711ad7b22c..f60e322d0bd1267e8c5be8c1199a292b3b0e1677 100644 --- a/doc/cpp_cable_cell.rst +++ b/doc/cpp_cable_cell.rst @@ -141,6 +141,10 @@ value should be taken from the cell or global parameter set. Local areal capacitance of the cell membrane, in Farads per square metre. + .. cpp:member:: util::optional<cv_policy> discretization + + Method by which CV boundaries are determined when the cell is discretized. + See :ref:`cv-policies`. Default parameters for a cell are given by the :cpp:expr:`default_parameters` field in the :cpp:type:`cable_cell` object. This is a value of type :cpp:type:`cable_cell_parameter_set`, @@ -567,3 +571,117 @@ with which it is associated. * Metadata: ``std::vector<cable_probe_point_info>``. Target metadata for each associated target. + + +.. _cv-policies: + +Discretization and CV policies +------------------------------ + +For the purpose of simulation, cable cells are decomposed into discrete +subcomponents called *control volumes* (CVs), following the finite volume method +terminology. Each control volume comprises a connected subset of the +morphology. Each fork point in the morphology will be the responsibility of +a single CV, and as a special case a zero-volume CV can be used to represent +a single fork point in isolation. + +The CVs are uniquely determined by a set of *B* of ``mlocation`` boundary points. +For each non-terminal point *h* in *B*, there is a CV comprising the points +{*x*: *h* ≤ *x* and ¬∃ *y* ∈ *B* s.t *h* < *y* < *x*}, where < and ≤ refer to the +geometrical partial order of locations on the morphology. A fork point is +owned by a CV if and only if all of its corresponding representative locations +are in the CV. + +The set of boundary points used by the simulator is determined by a *CV policy*. +These are objects of type ``cv_policy``, which has the following +public methods: + +.. cpp:class:: cv_policy + + .. cpp:function:: locset cv_boundary_points(const cable_cell&) const + + Return a locset describing the boundary points for CVs on the given cell. + + .. cpp:function:: region domain() const + + Give the subset of a cell morphology on which this policy has been declared, + as a morphological ``region`` expression. + +Specific CV policy objects are created by functions described below (strictly +speaking, these are class constructors for classes are implicit converted to +``cv_policy`` objects). These all take a ``region`` parameter that restrict the +domain of applicability of that policy; this facility is useful for specifying +differing discretizations on different parts of a cell morphology. When a CV +policy is constrained in this manner, the boundary of the domain will always +constitute part of the CV boundary point set. + +CV policies can be combined with ``+`` and ``|`` operators. For two policies +*A* and *B*, *A* + *B* is a policy which gives boundary points from both *A* +and *B*, while *A* | *B* is a policy which gives all the boundary points from +*B* together with those from *A* which do not within the domain of *B*. +The domain of *A* + *B* and *A* | *B* is the union of the domains of *A* and +*B*. + +``cv_policy_single`` +^^^^^^^^^^^^^^^^^^^^ + +.. code:: + + cv_policy_single(region domain = reg::all()) + +Use one CV for the whole cell, or one for each connected component of the +supplied domain. + +``cv_policy_explicit`` +^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: + + cv_policy_explicit(locset locs, region domain = reg::all()) + +Use the points given by ``locs`` for CV boundaries, optionally restricted to the +supplied domain. + +``cv_policy_every_sample`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: + + cv_policy_every_sample(region domain = reg::all()) + +Use every sample point in the morpholgy definition as a CV boundary, optionally +restricted to the supplied domain. Each fork point in the domain is +represented by a trivial CV. + +``cv_policy_fixed_per_branch`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: + + cv_policy_fixed_per_branch(unsigned cv_per_branch, region domain, cv_policy_flag::value flags = cv_policy_flag::none); + + cv_policy_fixed_per_branch(unsigned cv_per_branch, cv_policy_flag::value flags = cv_policy_flag::none): + +For each branch in each connected component of the domain (or the whole cell, +if no domain is given), evenly distribute boundary points along the branch so +as to produce exactly ``cv_per_branch`` CVs. + +By default, CVs will terminate at branch ends. If the flag +``cv_policy_flag::interior_forks`` is given, fork points will be included in +non-trivial, branched CVs and CVs covering terminal points in the morphology +will be half-sized. + + +``cv_policy_max_extent`` +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: + + cv_policy_max_extent(double max_extent, region domain, cv_policy_flag::value flags = cv_policy_flag::none); + + cv_policy_max_extent(double max_extent, cv_policy_flag::value flags = cv_policy_flag::none): + +As for ``cv_policy_fixed_per_branch``, save that the number of CVs on any +given branch will be chosen to be the smallest number that ensures no +CV will have an extent on the branch longer than ``max_extent`` micrometres. + diff --git a/python/morph_parse.cpp b/python/morph_parse.cpp index 6c261029ced2b92d0715c0db9a5ed1bd37561a15..ba35b9a0c4a3bffc56a76be49745b28a0cdac8ab 100644 --- a/python/morph_parse.cpp +++ b/python/morph_parse.cpp @@ -196,7 +196,7 @@ std::unordered_multimap<std::string, evaluator> eval_map { {"proximal_interval", make_call<arb::locset>( [](arb::locset ls){return arb::reg::proximal_interval(std::move(ls), std::numeric_limits<double>::max());}, "'proximal_interval' with 1 argument: (start:locset)")}, - {"super", make_call<arb::region>(arb::reg::super, + {"complete", make_call<arb::region>(arb::reg::complete, "'super' with 1 argment: (reg:region)")}, {"radius_lt",make_call<arb::region, double>(arb::reg::radius_lt, "'radius_lt' with 2 arguments: (reg:region radius:real)")}, diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 966ff9fdba5219813029e9d171c1b946a3436bbb..c13aa40bc82116bf3c43122b6cc00068e6acad18 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -120,6 +120,7 @@ set(unit_sources test_merge_events.cpp test_merge_view.cpp test_morphology.cpp + test_morph_components.cpp test_morph_embedding.cpp test_morph_expr.cpp test_morph_place.cpp diff --git a/test/unit/common_morphologies.hpp b/test/unit/common_morphologies.hpp index a3b63bf4ac720e7b3c9d662faea0867de0f2899a..de00aac7e494db36fb21f6fadf3da04c810a4929 100644 --- a/test/unit/common_morphologies.hpp +++ b/test/unit/common_morphologies.hpp @@ -20,29 +20,20 @@ inline std::vector<arb::msample> make_morph_samples(unsigned n) { static const arb::morphology m_empty; -// spherical root, one branch -static const arb::morphology m_sph_b1{arb::sample_tree(make_morph_samples(1), {arb::mnpos}), true}; - -// regular root, one branch +// One branch. static const arb::morphology m_reg_b1{arb::sample_tree(make_morph_samples(2), {arb::mnpos, 0u}), false}; -// spherical root, six branches: -// branch 0 (spherical root) has child branches 1 and 2; branch 2 has child branches 3, 4 and 5. -static const arb::morphology m_sph_b6{arb::sample_tree(make_morph_samples(8), {arb::mnpos, 0u, 1u, 0u, 3u, 4u, 4u, 4u}), true}; - -// regular root, six branches +// Six branches: // branch 0 has child branches 1 and 2; branch 2 has child branches 3, 4 and 5. static const arb::morphology m_reg_b6{arb::sample_tree(make_morph_samples(7), {arb::mnpos, 0u, 1u, 1u, 2u, 2u, 2u}), false}; -// regular root, six branches, mutiple top level branches. +// Six branches, mutiple top level branches: // branch 0 has child branches 1 and 2; branch 3 has child branches 4 and 5. static const arb::morphology m_mlt_b6{arb::sample_tree(make_morph_samples(7), {arb::mnpos, 0u, 1u, 1u, 0u, 4u, 4u}), false}; static std::pair<const char*, arb::morphology> test_morphologies[] = { {"m_empty", m_empty}, - {"m_sph_b1", m_sph_b1}, {"m_reg_b1", m_reg_b1}, - {"m_sph_b6", m_sph_b6}, {"m_reg_b6", m_reg_b6}, {"m_mlt_b6", m_mlt_b6} }; diff --git a/test/unit/test_cv_geom.cpp b/test/unit/test_cv_geom.cpp index f71bb9e3d7ed5e270ec1f73142df25ab304a850d..a007a86b7277905b06a4b9ed8abae7c01f7302eb 100644 --- a/test/unit/test_cv_geom.cpp +++ b/test/unit/test_cv_geom.cpp @@ -127,8 +127,8 @@ TEST(cv_geom, trivial) { TEST(cv_geom, one_cv_per_branch) { using namespace common_morphology; - auto super = [] (const arb::morphology& m, arb::mcable c) { - return thingify(arb::reg::super(arb::region(c)), arb::mprovider(m)).cables(); + auto complete = [] (const arb::morphology& m, arb::mcable c) { + return thingify(arb::reg::complete(arb::region(c)), arb::mprovider(m)).cables(); }; for (auto& p: test_morphologies) { @@ -160,7 +160,7 @@ TEST(cv_geom, one_cv_per_branch) { EXPECT_TRUE(n_branch_child(c.branch)>1); } // Cables in trivial CV should be the same as those in the extent over the point. - EXPECT_TRUE(testing::seq_eq(super(m,c), cables)); + EXPECT_TRUE(testing::seq_eq(complete(m,c), cables)); } else { ASSERT_EQ(1u, cables.size()); @@ -171,7 +171,7 @@ TEST(cv_geom, one_cv_per_branch) { // Confirm parent CV is fork CV: if (i>0) { - auto fork_ext = super(m, {c.branch, 0}); + auto fork_ext = complete(m, {c.branch, 0}); mcable_list pcables = util::assign_from(geom.cables(geom.cv_parent[i])); ASSERT_TRUE(testing::cablelist_eq(fork_ext, pcables)); } @@ -311,8 +311,8 @@ TEST(cv_geom, location_cv) { return mextent(cl); }; - auto super = [] (const arb::morphology& m, arb::mcable c) { - return thingify(arb::reg::super(arb::region(c)), arb::mprovider(m)).cables(); + auto complete = [] (const arb::morphology& m, arb::mcable c) { + return thingify(arb::reg::complete(arb::region(c)), arb::mprovider(m)).cables(); }; // Two CVs per branch, plus trivial CV at forks. @@ -334,7 +334,7 @@ TEST(cv_geom, location_cv) { ASSERT_TRUE(cable0.prox_pos==cable0.dist_pos); mcable_list clist = util::assign_from(cables); - ASSERT_TRUE(testing::cablelist_eq(super(m, cable0), clist)); + ASSERT_TRUE(testing::cablelist_eq(complete(m, cable0), clist)); } } diff --git a/test/unit/test_cv_layout.cpp b/test/unit/test_cv_layout.cpp index af731d0f0503dd2541ef6ad8ec886e8ffa67b304..8502318d9e66b6b03a3a8f7255baee08b4c21625 100644 --- a/test/unit/test_cv_layout.cpp +++ b/test/unit/test_cv_layout.cpp @@ -47,6 +47,10 @@ TEST(cv_layout, trivial) { std::vector<cable_cell> cells; unsigned n_cv = 0; for (auto& p: test_morphologies) { + // Skip morpohologies with more than one root branch, becaue + // they are not 'connected', and will generate multiple CVs. + if (p.second.branch_children(mnpos).size()>1u) continue; + cells.emplace_back(p.second); n_cv += !p.second.empty(); // one cv per non-empty cell } diff --git a/test/unit/test_cv_policy.cpp b/test/unit/test_cv_policy.cpp index bfb9c43369264e15307c38b798bc57d8cf028b35..bba0fa1531736602163a966dfae4eb70365375ce 100644 --- a/test/unit/test_cv_policy.cpp +++ b/test/unit/test_cv_policy.cpp @@ -9,113 +9,107 @@ #include <arbor/morph/morphology.hpp> #include <arbor/morph/locset.hpp> #include <arbor/morph/mprovider.hpp> +#include <arbor/morph/region.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" +#include "common_morphologies.hpp" +#include "morph_pred.hpp" using namespace arb; using util::make_span; +using testing::locset_eq; +using testing::region_eq; +using testing::mlocationlist_eq; + +using namespace common_morphology; 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; + template <typename... A> + locset as_locset(mlocation head, A... tail) { + return join(locset(head), locset(tail)...); } +} - // Test morphologies for CV determination: - // Samples points have radius 0.5, giving an initial branch length of 1.0 - // for morphologies with spherical roots. +TEST(cv_policy, single) { + // In all instances, expect the boundary points to correspond to + // the extremal points of the completions of the components of the + // supplied region. - const morphology m_empty; + cable_cell cell(m_mlt_b6); + for (region reg: + {reg::all(), reg::branch(2), reg::cable(3, 0.25, 1.), + join(reg::cable(1, 0.75, 1), reg::branch(3), reg::cable(2, 0, 0.5)), + join(reg::cable(2, 0, 0.5), reg::branch(3), reg::cable(4, 0, 0.5))}) + { + locset expected = ls::cboundary(reg); + EXPECT_TRUE(locset_eq(cell.provider(), ls::cboundary(reg), cv_policy_single(reg).cv_boundary_points(cell))); + } - // spherical root, one branch - const morphology m_sph_b1{sample_tree(make_samples(1), {mnpos}), true}; + EXPECT_TRUE(locset_eq(cell.provider(), cv_policy_single().cv_boundary_points(cell), cv_policy_single(reg::all()).cv_boundary_points(cell))); +} - // regular root, one branch - const morphology m_reg_b1{sample_tree(make_samples(2), {mnpos, 0u}), false}; +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}); - // spherical root, six branches - const morphology m_sph_b6{sample_tree(make_samples(8), {mnpos, 0u, 1u, 0u, 3u, 4u, 4u, 4u}), true}; + cv_policy pol = cv_policy_explicit(lset); + for (auto& m: {m_reg_b6, m_mlt_b6}) { + cable_cell cell(m); - // regular root, six branches - const morphology m_reg_b6{sample_tree(make_samples(7), {mnpos, 0u, 1u, 1u, 2u, 2u, 2u}), false}; + locset result = pol.cv_boundary_points(cell); + locset expected = join(ls::boundary(reg::all()), lset); + EXPECT_TRUE(locset_eq(cell.provider(), expected, result)); + } - // 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}; + // With cables 1 and 2, expect to pick up (1, 0.5) from locset, + // and cable ends (1, 0), (1, 1), (2, 0), (2, 1), as the two + // cables constitute two components. - template <typename... A> - locset as_locset(mlocation head, A... tail) { - return join(locset(head), locset(tail)...); + region b12 = join(reg::branch(1), reg::branch(2)); + pol = cv_policy_explicit(lset, b12); + for (auto& m: {m_reg_b6, m_mlt_b6}) { + cable_cell cell(m); + + locset result = pol.cv_boundary_points(cell); + locset expected = as_locset(L{1, 0}, L{1, 0.5}, L{1, 1}, L{2, 0}, L{2, 1}); + EXPECT_TRUE(locset_eq(cell.provider(), expected, result)); } -} -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}); + // Taking the completion of the two cables, the boundary of the region + // will be (0, 1), (1, 1), (2, 1) for m_mlt_b6. - cv_policy pol = cv_policy_explicit(lset); - for (auto& m: {m_sph_b6, m_reg_b6, m_mlt_b6}) { + pol = cv_policy_explicit(lset, reg::complete(b12)); + for (auto& m: {m_mlt_b6}) { cable_cell cell(m); locset result = pol.cv_boundary_points(cell); - EXPECT_EQ(thingify(lset, cell.provider()), thingify(result, cell.provider())); + locset expected = as_locset(L{0, 1}, L{1, 0.5}, L{1, 1}, L{2, 1}); + EXPECT_TRUE(locset_eq(cell.provider(), expected, result)); } } 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). + // Any policy applied to an empty morphology should give an empty locset. 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_fixed_per_branch(3, interior_forks), cv_policy_max_extent(0.234), - cv_policy_max_extent(0.234, single_root_cv|interior_forks) + cv_policy_max_extent(0.234, interior_forks), + cv_policy_single(), + cv_policy_single(reg::all()), + cv_policy_explicit(ls::location(0, 0)) }; cable_cell cell(m_empty); - auto empty_loclist = thingify(ls::nil(), cell.provider()); for (auto& pol: policies) { - EXPECT_EQ(empty_loclist, thingify(pol.cv_boundary_points(cell), cell.provider())); - } -} - -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(morph); - - for (auto& polpair: policy_pairs) { - mlocation_list p1 = thingify(polpair.first.cv_boundary_points(cell), cell.provider()); - mlocation_list p2 = thingify(polpair.second.cv_boundary_points(cell), cell.provider()); - - 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); - } + EXPECT_TRUE(locset_eq(cell.provider(), ls::nil(), pol.cv_boundary_points(cell))); } } @@ -123,49 +117,83 @@ 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(morph); + // Root branch only: + { + cable_cell cell(m_reg_b1); { // 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.provider()), thingify(points, cell.provider())); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); } { // 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.provider()), thingify(points, cell.provider())); + locset expected = as_locset(L{0, 0}, L{0, 0.125}, L{0, 0.375}, L{0, 0.625}, L{0, 0.875}, L{0, 1}); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); } } - // 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(morph); + // Multiple top level branches: + // top level branches are 0 and 3, terminal branches are 1, 2, 4 and 5. + { + cable_cell cell(m_mlt_b6); { - // boundary fork points + // With 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.provider()), thingify(points, cell.provider())); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); } { - // interior fork points + // With 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} + L{0, 0}, L{0, 0.25}, L{0, 0.75}, + L{1, 0.25}, L{1, 0.75}, L{1, 1.0}, + L{2, 0.25}, L{2, 0.75}, L{2, 1.0}, + L{3, 0}, L{3, 0.25}, L{3, 0.75}, + L{4, 0.25}, L{4, 0.75}, L{4, 1.0}, + L{5, 0.25}, L{5, 0.75}, L{5, 1.0} ); - EXPECT_EQ(thingify(expected, cell.provider()), thingify(points, cell.provider())); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); + } + } + + // Restrict to an incomplete subtree (distal half of branch 0 and all of branch 2) + // in m_mlt_b6 morphology. + { + cable_cell cell(m_mlt_b6); + region reg = mcable_list{{0, 0.5, 1.}, {2, 0., 1.}}; + { + // With two per branch and fork points as boundaries, expect to see: + // (0, 0.5), (0, 0.75), (0, 1) on branch 0; + // (2, 0), (2, 0.5), (2, 1) on branch 2; + // (1, 0) on branch 1. + cv_policy pol = cv_policy_fixed_per_branch(2, reg); + locset expected = as_locset( + L{0, 0.5}, L{0, 0.75}, L{0, 1}, + L{1, 0}, + L{2, 0}, L{2, 0.5}, L{2, 1} + ); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); + } + { + // With two per branch and interior forks, expect to see: + // (0, 0.5), (0, 0.625), (0, 0.0875) on branch 0; + // (2, 0.25), (2, 0.75), (2, 1) on branch 2; + // (1, 0) on branch 1. + cv_policy pol = cv_policy_fixed_per_branch(2, reg, interior_forks); + locset expected = as_locset( + L{0, 0.5}, L{0, 0.625}, L{0, 0.875}, + L{1, 0}, + L{2, 0.25}, L{2, 0.75}, L{2, 1} + ); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); } } } @@ -174,28 +202,41 @@ 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(morph); + // Root branch only: + { + cable_cell cell(m_reg_b1); ASSERT_EQ(1.0, cell.embedding().branch_length(0)); { - // extent of 0.25 should give exact fp calculation, giving + // 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.provider()), thingify(points, cell.provider())); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); + } + { + // Same, but applied to cable (0, 0.25, 0.75) should give 2 Cvs. + cv_policy pol = cv_policy_max_extent(0.25, reg::cable(0, 0.25, 0.75)); + locset expected = as_locset(L{0, 0.25}, L{0, 0.5}, L{0, 0.75}); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); + } { + // Interior forks: 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.provider()), thingify(points, cell.provider())); + locset expected = as_locset(L{0, 0}, L{0, 0.125}, L{0, 0.375}, L{0, 0.625}, L{0, 0.875}, L{0, 1}); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); + } + { + // Interior forks but restricted to sub-cable. + cv_policy pol = cv_policy_max_extent(0.25, reg::cable(0, 0.25, 0.75), interior_forks); + locset expected = as_locset(L{0, 0.25}, L{0, 0.375}, L{0, 0.625}, L{0, 0.75}); + EXPECT_TRUE(locset_eq(cell.provider(), expected, pol.cv_boundary_points(cell))); + } } - // cell with varying branch lengths; extent not exact fraction. + // Cell with varying branch lengths; extent not exact fraction: { cable_cell cell(m_mlt_b6); ASSERT_EQ(1.0, cell.embedding().branch_length(0)); @@ -206,7 +247,7 @@ TEST(cv_policy, max_extent) { ASSERT_EQ(2.0, cell.embedding().branch_length(5)); { - // max extent of 0.6 should give two CVs on branches of length 1, + // 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.provider()); @@ -217,7 +258,7 @@ TEST(cv_policy, max_extent) { {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); + EXPECT_TRUE(mlocationlist_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()); @@ -245,31 +286,89 @@ TEST(cv_policy, every_sample) { { cable_cell cell(m); cv_policy pol = cv_policy_every_sample(); - mlocation_list points = thingify(pol.cv_boundary_points(cell), cell.provider()); - util::sort(points); mlocation_list expected = { - {0, 0}, {0, 0.25}, {0, 0.5}, {0, 0.75}, {0, 1.}, - {1, 0}, {1, 0.25}, {1, 0.5}, {1, 0.75}, {1, 1.}, - {2, 0}, {2, 0.25}, {2, 0.5}, {2, 0.75}, {2, 1.} + {0, 0}, {0, 0.25}, {0, 0.5}, {0, 0.75}, {0, 1}, + {1, 0}, {1, 0.25}, {1, 0.5}, {1, 0.75}, {1, 1}, + {2, 0}, {2, 0.25}, {2, 0.5}, {2, 0.75}, {2, 1} }; - EXPECT_EQ(expected, points); + EXPECT_TRUE(locset_eq(cell.provider(), locset(expected), pol.cv_boundary_points(cell))); } - - // Single root CV: + // Restricting to the two child branches (disconnected): { cable_cell cell(m); - cv_policy pol = cv_policy_every_sample(single_root_cv); - mlocation_list points = thingify(pol.cv_boundary_points(cell), cell.provider()); - util::sort(points); + region reg = join(reg::branch(1), reg::branch(2)); + cv_policy pol = cv_policy_every_sample(reg); + // Get samples from branches 1 and 2, plus boundary points from completions of each + // branch, viz. (0, 1), (2, 0), (1, 1) from branch 1 and (0, 1), (1, 0), (2, 1) from + // branch 2. mlocation_list expected = { - {0, 0}, {0, 1.}, - {1, 0}, {1, 0.25}, {1, 0.5}, {1, 0.75}, {1, 1.}, - {2, 0}, {2, 0.25}, {2, 0.5}, {2, 0.75}, {2, 1.} + {0, 1}, + {1, 0}, {1, 0.25}, {1, 0.5}, {1, 0.75}, {1, 1}, + {2, 0}, {2, 0.25}, {2, 0.5}, {2, 0.75}, {2, 1} }; - EXPECT_EQ(expected, points); + EXPECT_TRUE(locset_eq(cell.provider(), locset(expected), pol.cv_boundary_points(cell))); + } +} + +TEST(cv_policy, domain) { + using namespace cv_policy_flag; + + region reg1 = join(reg::branch(1), reg::cable(2, 0, 0.5)); + region reg2 = join(reg::branch(1), reg::cable(2, 0.5, 1), reg::cable(4, 0, 1)); + + cable_cell cell(m_mlt_b6); + + EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_single(reg1).domain())); + EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_fixed_per_branch(3, reg1).domain())); + EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_fixed_per_branch(3, reg1, interior_forks).domain())); + EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_max_extent(3, reg1).domain())); + EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_max_extent(3, reg1, interior_forks).domain())); + EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_every_sample(reg1).domain())); + + EXPECT_TRUE(region_eq(cell.provider(), join(reg1, reg2), (cv_policy_single(reg1)+cv_policy_single(reg2)).domain())); + EXPECT_TRUE(region_eq(cell.provider(), join(reg1, reg2), (cv_policy_single(reg1)|cv_policy_single(reg2)).domain())); +} + +TEST(cv_policy, combinators) { + auto unique_sum = [](auto... a) { return ls::support(sum(locset(a)...)); }; + + cable_cell cell(m_reg_b6); + auto eval_locset_eq = [&cell](const locset& a, const cv_policy& p) { + return locset_eq(cell.provider(), a, p.cv_boundary_points(cell)); + }; + + { + mlocation_list locs1{{0, 0.5}, {1, 0.25}, {2, 1}}; + mlocation_list locs2{{0, 0.75}, {1, 0.25}, {4, 0}}; + locset all_bdy = ls::boundary(reg::all()); + + ASSERT_TRUE(eval_locset_eq(unique_sum(all_bdy, locs1), cv_policy_explicit(locs1))); + ASSERT_TRUE(eval_locset_eq(unique_sum(all_bdy, locs2), cv_policy_explicit(locs2))); + + EXPECT_TRUE(eval_locset_eq(unique_sum(all_bdy, locs1, locs2), cv_policy_explicit(locs1)+cv_policy_explicit(locs2))); + EXPECT_TRUE(eval_locset_eq(unique_sum(all_bdy, locs2), cv_policy_explicit(locs1)|cv_policy_explicit(locs2))); + } + + { + region reg12 = join(reg::branch(1), reg::branch(2)); + region reg23 = join(reg::branch(2), reg::branch(3)); + + cv_policy pol12 = cv_policy_explicit(ls::on_branches(0.5), reg12); + cv_policy pol23 = cv_policy_explicit(ls::on_branches(0.75), reg23); + + using L = mlocation; + + ASSERT_TRUE(eval_locset_eq(unique_sum(ls::boundary(reg12), L{1, 0.5}, L{2, 0.5}), pol12)); + ASSERT_TRUE(eval_locset_eq(unique_sum(ls::boundary(reg23), L{2, 0.75}, L{3, 0.75}), pol23)); + + EXPECT_TRUE(eval_locset_eq(unique_sum(ls::boundary(reg12), ls::boundary(reg23), L{1, 0.5}, L{2, 0.5}, L{2, 0.75}, L{3, 0.75}), + pol12+pol23)); + + EXPECT_TRUE(eval_locset_eq(unique_sum(ls::boundary(reg12), ls::boundary(reg23), L{1, 0.5}, L{2, 0.75}, L{3, 0.75}), + pol12|pol23)); } } diff --git a/test/unit/test_morph_components.cpp b/test/unit/test_morph_components.cpp new file mode 100644 index 0000000000000000000000000000000000000000..32207ffc1b18de6a2ae32a3a69a627c35633e188 --- /dev/null +++ b/test/unit/test_morph_components.cpp @@ -0,0 +1,81 @@ +#include <cmath> +#include <vector> + +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/mprovider.hpp> + +#include "util/rangeutil.hpp" + +#include "../test/gtest.h" +#include "common_morphologies.hpp" + +using namespace arb; + +TEST(morph, subbranch_components) { + using namespace common_morphology; + + const auto& m = m_reg_b1; // simple cable + mextent ex(mcable_list{mcable{0, 0.125, 0.25}, mcable{0, 0.5, 0.625}, mcable{0, 0.625, 0.75}, mcable{0, 0.875, 1.}}); + auto comps = components(m, ex); + + ASSERT_EQ(3u, comps.size()); + ASSERT_FALSE(util::any_of(comps, [](const auto& x) { return x.empty(); })); + + util::sort_by(comps, [](const mextent& x) { return prox_loc(x.front()); }); + + EXPECT_EQ((mcable_list{mcable{0, 0.125, 0.25}}), comps[0].cables()); + EXPECT_EQ((mcable_list{mcable{0, 0.5, 0.75}}), comps[1].cables()); + EXPECT_EQ((mcable_list{mcable{0, 0.875, 1.}}), comps[2].cables()); +} + +TEST(morph, subtree_components) { + using namespace common_morphology; + + const auto& m = m_mlt_b6; // cell with two 'Y's (branches 0, 1, 2 and branches 3, 4, 5) meeting at root. + + // Component semantics has that initial segments of branches from a common fork are _not_ regarded + // as connected; a final segment of a branch and an initial segment of a child branch _are_ regarded + // as connected. + + std::pair<mcable_list, std::vector<mcable_list>> test_cases[] = + { + // Full cell gives two components (one for each 'Y'). + { + {mcable{0, 0, 1}, mcable{1, 0, 1}, mcable{2, 0, 1}, mcable{3, 0, 1}, mcable{4, 0, 1}, mcable{5, 0, 1}}, + { + {mcable{0, 0, 1}, mcable{1, 0, 1}, mcable{2, 0, 1}}, + {mcable{3, 0, 1}, mcable{4, 0, 1}, mcable{5, 0, 1}} + } + }, + + // Siblings are separated. + { + {mcable{1, 0, 1}, mcable{2, 0, 1}, mcable{4, 0, 1}, mcable{5, 0, 1}}, + { + {mcable{1, 0, 1}}, + {mcable{2, 0, 1}}, + {mcable{4, 0, 1}}, + {mcable{5, 0, 1}} + } + }, + + // Parent-child branches are connected if they include the fork point. + { + {mcable{0, 0.5, 1}, mcable{2, 0, 0.5}, mcable{3, 0.5, 1}, mcable{5, 0.5, 1}}, + { + {mcable{0, 0.5, 1}, mcable{2, 0, 0.5}}, + {mcable{3, 0.5, 1}}, + {mcable{5, 0.5, 1}} + } + }, + }; + + for (auto tc: test_cases) { + auto comps = components(m, mextent(tc.first)); + util::sort_by(comps, [](const mextent& x) { return prox_loc(x.front()); }); + + std::vector<mcable_list> result = util::assign_from(util::transform_view(comps, [](const auto& x) { return x.cables(); })); + EXPECT_EQ(tc.second, result); + } +} + diff --git a/test/unit/test_morph_expr.cpp b/test/unit/test_morph_expr.cpp index 08e52d1e919ebc46b3f66ab4c966c8837ce63b76..40307aa0b8630b3266ce70afc84c1edd195e3535 100644 --- a/test/unit/test_morph_expr.cpp +++ b/test/unit/test_morph_expr.cpp @@ -34,8 +34,10 @@ TEST(region, expr_repn) { auto t1 = reg::tagged(1); auto t2 = reg::tagged(2); auto t3 = reg::tagged(3); - auto s = reg::super(t3); + auto s = reg::complete(t3); auto all = reg::all(); + auto df = difference(t2, t1); + auto cm = complement(t3); EXPECT_EQ(to_string(c1), "(cable 1 0 1)"); EXPECT_EQ(to_string(c2), "(cable 4 0.125 0.5)"); @@ -43,12 +45,14 @@ TEST(region, expr_repn) { EXPECT_EQ(to_string(b1), "(cable 1 0 1)"); EXPECT_EQ(to_string(t1), "(tag 1)"); EXPECT_EQ(to_string(t2), "(tag 2)"); - EXPECT_EQ(to_string(s), "(super (tag 3))"); + EXPECT_EQ(to_string(s), "(complete (tag 3))"); EXPECT_EQ(to_string(intersect(c1, t2)), "(intersect (cable 1 0 1) (tag 2))"); EXPECT_EQ(to_string(join(c1, t2)), "(join (cable 1 0 1) (tag 2))"); EXPECT_EQ(to_string(join(t1, t2, t3)), "(join (join (tag 1) (tag 2)) (tag 3))"); EXPECT_EQ(to_string(intersect(t1, t2, t3)), "(intersect (intersect (tag 1) (tag 2)) (tag 3))"); EXPECT_EQ(to_string(all), "(all)"); + EXPECT_EQ(to_string(df), "(difference (tag 2) (tag 1))"); + EXPECT_EQ(to_string(cm), "(complement (tag 3))"); } TEST(region, invalid_mcable) { @@ -64,6 +68,7 @@ TEST(locset, expr_repn) { auto term = ls::terminal(); auto samp = ls::sample(42); auto loc = ls::location(2, 0.5); + auto bdy = ls::boundary(reg::tagged(1)); EXPECT_EQ(to_string(root), "(root)"); EXPECT_EQ(to_string(term), "(terminal)"); @@ -72,6 +77,7 @@ TEST(locset, expr_repn) { EXPECT_EQ(to_string(sum(root, term, samp, loc)), "(sum (sum (sum (root) (terminal)) (sample 42)) (location 2 0.5))"); EXPECT_EQ(to_string(samp), "(sample 42)"); EXPECT_EQ(to_string(loc), "(location 2 0.5)"); + EXPECT_EQ(to_string(bdy), "(boundary (tag 1))"); } TEST(locset, invalid_mlocation) { @@ -348,6 +354,10 @@ TEST(region, thingify_simple_morphologies) { auto t1 = reg::tagged(1); auto t2 = reg::tagged(2); auto all = reg::all(); + auto q1 = reg::cable(0, 0.0, 0.25); + auto q2 = reg::cable(0, 0.25, 0.5); + auto q3 = reg::cable(0, 0.5, 0.75); + auto q4 = reg::cable(0, 0.75, 1); // Concrete cable lists cl h1_{{0, 0.0, 0.5}}; @@ -372,6 +382,11 @@ TEST(region, thingify_simple_morphologies) { EXPECT_TRUE(region_eq(mp, join(h1, t2), cl{{0, 0, 0.5}, {0, 0.7, 1}})); EXPECT_TRUE(region_eq(mp, intersect(h2, t1), cl{{0, 0.5, 0.7}})); + EXPECT_TRUE(region_eq(mp, complement(join(q1, q3)), join(q2, q4))); + EXPECT_TRUE(region_eq(mp, complement(q2), join(q1, q3, q4))); + EXPECT_TRUE(region_eq(mp, difference(h1, q1), q2)); + EXPECT_TRUE(region_eq(mp, difference(h1, q3), h1)); + // Check round-trip of implicit region conversions. // (No fork points in cables, so extent should not including anyhing extra). EXPECT_EQ((mcable_list{{0, 0.3, 0.6}}), thingify(region(mcable{0, 0.3, 0.6}), mp).cables()); @@ -487,7 +502,7 @@ TEST(region, thingify_moderate_morphologies) { using reg::branch; using reg::all; using reg::cable; - using reg::super; + using reg::complete; auto soma = tagged(1); auto axon = tagged(2); @@ -517,12 +532,12 @@ TEST(region, thingify_moderate_morphologies) { // Test that intersection correctly generates zero-length cables at // parent-child interfaces. EXPECT_TRUE(region_eq(mp, intersect(apic, dend), cl{})); - EXPECT_TRUE(region_eq(mp, intersect(super(apic), super(dend)), cl{{1,1,1}, {2,0,0}, {3,0,0}})); + EXPECT_TRUE(region_eq(mp, intersect(complete(apic), complete(dend)), cl{{1,1,1}, {2,0,0}, {3,0,0}})); EXPECT_TRUE(region_eq(mp, intersect(apic, axon), cl{})); - EXPECT_TRUE(region_eq(mp, intersect(super(apic), axon), cl{{1,1,1}})); + EXPECT_TRUE(region_eq(mp, intersect(complete(apic), axon), cl{{1,1,1}})); EXPECT_TRUE(region_eq(mp, intersect(axon, dend), cl{})); - EXPECT_TRUE(region_eq(mp, intersect(super(axon), dend), cl{{0,0,0}, {3,0,0}})); - EXPECT_TRUE(region_eq(mp, intersect(super(dend), axon), cl{{1,0,0}, {1,1,1}})); + EXPECT_TRUE(region_eq(mp, intersect(complete(axon), dend), cl{{0,0,0}, {3,0,0}})); + EXPECT_TRUE(region_eq(mp, intersect(complete(dend), axon), cl{{1,0,0}, {1,1,1}})); // Test distal and proximal interavls auto start0_ = location(0, 0 ); @@ -771,7 +786,7 @@ TEST(region, thingify_complex_morphologies) { { mprovider mp(m); using reg::cable; - using reg::super; + using reg::complete; using ls::most_distal; using ls::most_proximal; @@ -797,8 +812,8 @@ TEST(region, thingify_complex_morphologies) { EXPECT_TRUE(mlocationlist_eq(thingify(most_proximal(reg_e_), mp), mlocation_list{{2,0}})); EXPECT_TRUE(mlocationlist_eq(thingify(most_proximal(reg_f_), mp), mlocation_list{{2,0}, {7,0}})); EXPECT_TRUE(mlocationlist_eq(thingify(most_proximal(reg_g_), mp), mlocation_list{{5,0}, {6,0}})); - EXPECT_TRUE(mlocationlist_eq(thingify(most_proximal(super(reg_f_)), mp), mlocation_list{{1,1}})); - EXPECT_TRUE(mlocationlist_eq(thingify(most_proximal(super(reg_g_)), mp), mlocation_list{{4,1}})); + EXPECT_TRUE(mlocationlist_eq(thingify(most_proximal(complete(reg_f_)), mp), mlocation_list{{1,1}})); + EXPECT_TRUE(mlocationlist_eq(thingify(most_proximal(complete(reg_g_)), mp), mlocation_list{{4,1}})); } } { diff --git a/test/unit/test_range.cpp b/test/unit/test_range.cpp index ed48a7923c35d618bef62f6e4cee2a3c99e0180b..ff1e22765734a64b0b5aaca867cd2ff0dd0259f9 100644 --- a/test/unit/test_range.cpp +++ b/test/unit/test_range.cpp @@ -735,3 +735,33 @@ TEST(range, reverse) { EXPECT_EQ("olleh"s, rev); } + +TEST(range, foldl) { + // Check invocation order: + int xs[] = {1, 2, 3, 4}; + int result = util::foldl( + [](int l, int r) { return 10*l+r; }, + 0, + xs); + + EXPECT_EQ(1234, result); + + // Check mutability permission: + result = util::foldl( + [](int l, int& r) { return ++r, 10*l+r; }, + 0, + xs); + EXPECT_EQ(2345, result); + EXPECT_EQ(5,xs[3]); + + // Check works with move-only values: + nocopy<int> ns[] = {1, 2, 3, 4}; + auto plus = [](nocopy<int> a, nocopy<int> b) { return nocopy<int>(10*a.value + b.value); }; + auto nc_result = util::foldl( + [&plus](auto a, auto& b) { return plus(std::move(a), std::move(b)); }, + nocopy<int>(0), + ns); + + EXPECT_EQ(1234, nc_result.value); + EXPECT_EQ(0, ns[3].value); +}