diff --git a/CMakeLists.txt b/CMakeLists.txt index 83163769b4dfae29a3b0eea18ae5e314918029f8..94e856778a4ffd74f7c6c757f60f8a6f330b60aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -372,9 +372,6 @@ add_subdirectory(test) # self contained examples: add_subdirectory(example) -# lmorpho: -add_subdirectory(lmorpho) - # html: add_subdirectory(doc) diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt index 978d3a30514d3d189d0d51e5f0f944a0a4034fcf..e7d9740df549268667da5a7c4b21c5fcf6b9c12a 100644 --- a/arbor/CMakeLists.txt +++ b/arbor/CMakeLists.txt @@ -28,9 +28,11 @@ set(arbor_sources mechcat.cpp memory/cuda_wrappers.cpp memory/util.cpp + morph/morphology.cpp + morph/sample_tree.cpp + morph/primitives.cpp merge_events.cpp simulation.cpp - morphology.cpp partition_load_balance.cpp profile/clock.cpp profile/memory_meter.cpp diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp index bea58ad8b7e86b35ba31dfa9908e7e379e307063..50ca10c05c968e6200e51d7823b402c618797608 100644 --- a/arbor/cable_cell.cpp +++ b/arbor/cable_cell.cpp @@ -1,8 +1,9 @@ #include <arbor/cable_cell.hpp> +#include <arbor/morph/morphology.hpp> #include <arbor/segment.hpp> -#include <arbor/morphology.hpp> #include "util/rangeutil.hpp" +#include "util/span.hpp" namespace arb { @@ -163,43 +164,55 @@ value_type cable_cell::segment_mean_attenuation( return 2*std::sqrt(math::pi<double>*tau_per_um*frequency)*length_factor; // [1/µm] } - -// Construct cell from flat morphology specification. - cable_cell make_cable_cell(const morphology& morph, bool compartments_from_discretization) { using point3d = cable_cell::point_type; cable_cell newcell; - if (!morph) { + if (!morph.num_branches()) { return newcell; } - arb_assert(morph.check_valid()); - - // (not supporting soma-less cells yet) - newcell.add_soma(morph.soma.r, point3d(morph.soma.x, morph.soma.y, morph.soma.z)); - - for (const section_geometry& section: morph.sections) { - auto kind = section.kind; - switch (kind) { - case section_kind::none: // default to dendrite - kind = section_kind::dendrite; - break; - case section_kind::soma: - throw cable_cell_error("no support for complex somata"); - break; - default: ; + // Add the soma. + auto loc = morph.samples()[0].loc; // location of soma. + + // If there is no spherical root/soma use a zero-radius soma. + double srad = morph.spherical_root()? loc.radius: 0.; + newcell.add_soma(srad, point3d(loc.x, loc.y, loc.z)); + + auto& samples = morph.samples(); + for (auto i: util::make_span(1, morph.num_branches())) { + auto index = util::make_range(morph.branch_sample_span(i)); + + // find kind for the branch. Use the tag of the last sample in the branch. + int tag = samples[index.back()].tag; + section_kind kind; + switch (tag) { + case 1: // soma + throw cable_cell_error("No support for complex somata (yet)"); + case 2: // axon + kind = section_kind::axon; + case 3: // dendrite + case 4: // apical dendrite + default: // just take dendrite as default + kind = section_kind::dendrite; } std::vector<value_type> radii; std::vector<point3d> points; - for (const section_point& p: section.points) { - radii.push_back(p.r); - points.push_back(point3d(p.x, p.y, p.z)); + for (auto i: index) { + auto& s = samples[i]; + radii.push_back(s.loc.radius); + points.push_back(point3d(s.loc.x, s.loc.y, s.loc.z)); } - auto cable = newcell.add_cable(section.parent_id, kind, radii, points); + // Find the id of this branch's parent. + auto pid = morph.branch_parent(i); + // Adjust pid if a zero-radius soma was used. + if (!morph.spherical_root()) { + pid = pid==mnpos? 0: pid+1; + } + auto cable = newcell.add_cable(pid, kind, radii, points); if (compartments_from_discretization) { cable->as_cable()->set_compartments(radii.size()-1); } diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp index f790627a60bf5a515fe7c12dd9a5847c22e1775a..09baa5de0507dc6a8e28f51e3f59112892b45ba0 100644 --- a/arbor/include/arbor/cable_cell.hpp +++ b/arbor/include/arbor/cable_cell.hpp @@ -9,7 +9,7 @@ #include <arbor/common_types.hpp> #include <arbor/constants.hpp> #include <arbor/mechcat.hpp> -#include <arbor/morphology.hpp> +#include <arbor/morph/morphology.hpp> #include <arbor/segment.hpp> namespace arb { @@ -269,10 +269,10 @@ cable_segment* cable_cell::add_cable(cable_cell::index_type parent, Args&&... ar return segments_.back()->as_cable(); } -// Create a cell from a morphology specification. -// If compartments_from_discretization is true, set number of compartments in -// each segment to be the number of piecewise linear sections in the corresponding -// section of the morphologu. -cable_cell make_cable_cell(const morphology&, bool compartments_from_discretization=false); +// Create a cable cell from a morphology specification. +// If compartments_from_discretization is true, set number of compartments +// in each segment to be the number of piecewise linear sections in the +// corresponding section of the morphology. +cable_cell make_cable_cell(const morphology& morph, bool compartments_from_discretization); } // namespace arb diff --git a/arbor/include/arbor/morph/error.hpp b/arbor/include/arbor/morph/error.hpp new file mode 100644 index 0000000000000000000000000000000000000000..14c40be496cc6be0b8873072a3e18433876c3a4d --- /dev/null +++ b/arbor/include/arbor/morph/error.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include <stdexcept> + +#include <arbor/arbexcept.hpp> + +namespace arb { + +struct morphology_error: public arbor_exception { + morphology_error(const char* what): arbor_exception(what) {} + morphology_error(const std::string& what): arbor_exception(what) {} +}; + +} // namespace arb + diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp new file mode 100644 index 0000000000000000000000000000000000000000..4ad07955f68af5ea976bdb8d89ecdf74fbffa280 --- /dev/null +++ b/arbor/include/arbor/morph/morphology.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include <ostream> +#include <vector> + +#include <arbor/util/lexcmp_def.hpp> +#include <arbor/morph/primitives.hpp> +#include <arbor/morph/sample_tree.hpp> + +namespace arb { + +// An unbranched cable segment that has root, terminal or fork point at each end. +struct mbranch { + std::vector<msize_t> index; // sample index + msize_t parent_id = mnpos; // branch index + + mbranch() = default; + mbranch(std::vector<msize_t> idx, msize_t parent): + index(std::move(idx)), parent_id(parent) {} + + bool is_sphere() const { return size()==1u; } + msize_t size() const { return index.size(); } + bool has_parent() const { return parent_id!=mnpos;} + + friend bool operator==(const mbranch& l, const mbranch& r); + friend std::ostream& operator<<(std::ostream& o, const mbranch& b); +}; + +class morphology { + // The sample tree of sample points and their parent-child relationships. + sample_tree samples_; + + // Indicates whether the soma is a sphere. + bool spherical_root_ = false; + + // Branch state. + std::vector<mbranch> branches_; + std::vector<msize_t> branch_parents_; + std::vector<std::vector<msize_t>> branch_children_; + + using index_range = std::pair<const msize_t*, const msize_t*>; + + void init(); + +public: + morphology(sample_tree m, bool use_spherical_root); + morphology(sample_tree m); + + // Whether the root of the morphology is spherical. + bool spherical_root() const; + + // The number of branches in the morphology. + msize_t num_branches() const; + + // The parent sample of sample i. + const std::vector<msize_t>& sample_parents() const; + + // The parent branch of branch b. + msize_t branch_parent(msize_t b) const; + + // The child branches of branch b. + const std::vector<msize_t>& branch_children(msize_t b) const; + + // Range of indexes into the sample points in branch b. + index_range branch_sample_span(msize_t b) const; + + // Range of the samples in branch b. + const std::vector<msample>& samples() const; + + friend std::ostream& operator<<(std::ostream&, const morphology&); +}; + +} // namespace arb diff --git a/arbor/include/arbor/morph/primitives.hpp b/arbor/include/arbor/morph/primitives.hpp new file mode 100644 index 0000000000000000000000000000000000000000..42baa9b7f46c6d24d28df259fd9478cfb8daf60b --- /dev/null +++ b/arbor/include/arbor/morph/primitives.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include <algorithm> +#include <cstdlib> +#include <ostream> +#include <vector> + +#include <arbor/util/lexcmp_def.hpp> + +// +// Types used to identify concrete locations. +// + +namespace arb { + +using msize_t = std::uint32_t; +constexpr msize_t mnpos = msize_t(-1); + +// a morphology sample point: a 3D location and radius. +struct mpoint { + double x, y, z; // [µm] + double radius; // [μm] + + friend std::ostream& operator<<(std::ostream&, const mpoint&); +}; + +mpoint lerp(const mpoint& a, const mpoint& b, double u); +bool is_collocated(const mpoint& a, const mpoint& b); +double distance(const mpoint& a, const mpoint& b); + +// A morphology sample consists of a location and an integer tag. +// When loaded from an SWC file, the tag will correspond to the SWC label, +// which are standardised as follows: +// 1 - soma +// 2 - axon +// 3 - (basal) dendrite +// 4 - apical dendrite +// http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html +// However, any positive integer tag can be provided and labelled dynamically. + +struct msample { + mpoint loc; + int tag; + + friend std::ostream& operator<<(std::ostream&, const msample&); +}; + +bool is_collocated(const msample& a, const msample& b); +double distance(const msample& a, const msample& b); + +using point_prop = std::uint8_t; +enum point_prop_mask: point_prop { + point_prop_mask_none = 0, + point_prop_mask_root = 1, + point_prop_mask_fork = 2, + point_prop_mask_terminal = 4, + point_prop_mask_collocated = 8 +}; + +#define ARB_PROP(prop) \ +constexpr bool is_##prop(point_prop p) {\ + return p&point_prop_mask_##prop;\ +} \ +inline void set_##prop(point_prop& p) {\ + p |= point_prop_mask_##prop;\ +} \ +inline void unset_##prop(point_prop& p) {\ + p &= ~point_prop_mask_##prop;\ +} + +ARB_PROP(root) +ARB_PROP(fork) +ARB_PROP(terminal) +ARB_PROP(collocated) + +} // namespace arb diff --git a/arbor/include/arbor/morph/sample_tree.hpp b/arbor/include/arbor/morph/sample_tree.hpp new file mode 100644 index 0000000000000000000000000000000000000000..2ae690ebdc13cc9e95e440c763515de9e696c3a1 --- /dev/null +++ b/arbor/include/arbor/morph/sample_tree.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include <cassert> +#include <functional> +#include <vector> +#include <string> + +#include <arbor/swcio.hpp> +#include <arbor/morph/primitives.hpp> + +namespace arb { + +/// Morphology composed of samples. +class sample_tree { + std::vector<msample> samples_; + std::vector<msize_t> parents_; + std::vector<point_prop> props_; + +public: + sample_tree() = default; + sample_tree(std::vector<msample>, std::vector<msize_t>); + + // Reserve space for n samples. + void reserve(msize_t n); + + // The append functions return a handle to the last sample appended by the call. + + // Append a single sample. + msize_t append(msize_t p, const msample& s); // to sample p. + msize_t append(const msample& s); // to the last sample in the tree. + + // Append a sequence of samples. + msize_t append(msize_t p, const std::vector<msample>& slist); // to sample p. + msize_t append(const std::vector<msample>& slist); // to the last sample in the tree. + + // The number of samples in the tree. + msize_t size() const; + bool empty() const; + + // The samples in the tree. + const std::vector<msample>& samples() const; + + // The parent index of the samples. + const std::vector<msize_t>& parents() const; + + // The properties of the samples. + const std::vector<point_prop>& properties() const; + + friend std::ostream& operator<<(std::ostream&, const sample_tree&); +}; + +/// Build a sample tree from a sequence of swc records. +sample_tree swc_as_sample_tree(const std::vector<swc_record>& swc_records); + +} // namesapce arb + diff --git a/arbor/include/arbor/morphology.hpp b/arbor/include/arbor/morphology.hpp deleted file mode 100644 index 44ac1212d1f7313b5f68bcffa98cf74ff087e5d1..0000000000000000000000000000000000000000 --- a/arbor/include/arbor/morphology.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -// Representation of 3-d embedded cable morphology, independent of other -// cell information. - -#include <stdexcept> -#include <vector> - -namespace arb { - -struct section_point { - double x, y, z, r; // [µm], r is radius. -}; - -enum class section_kind { - soma, - dendrite, - axon, - none -}; - -struct section_geometry { - unsigned id = 0; // ids should be contigously numbered from 1 in the morphology. - unsigned parent_id = 0; - bool terminal = false; - std::vector<section_point> points; - double length = 0; // µm - section_kind kind = section_kind::none; - - section_geometry() = default; - section_geometry(unsigned id, unsigned parent_id, bool terminal, std::vector<section_point> points, double length, section_kind kind = section_kind::none): - id(id), parent_id(parent_id), terminal(terminal), points(std::move(points)), length(length), kind(kind) - {} - - // Re-discretize the section into ceil(length/dx) segments. - void segment(double dx); -}; - -struct morphology_error: public std::runtime_error { - morphology_error(const char* what): std::runtime_error(what) {} - morphology_error(const std::string& what): std::runtime_error(what) {} -}; - -struct morphology { - // origin + spherical radius; convention: r==0 => no soma - section_point soma = { 0, 0, 0, 0}; - - std::vector<section_geometry> sections; - - bool has_soma() const { - return soma.r!=0; - } - - bool empty() const { - return sections.empty() && !has_soma(); - } - - operator bool() const { return !empty(); } - - // Return number of sections plus soma - std::size_t components() const { - return has_soma()+sections.size(); - } - - // Check invariants: - // 1. sections[i].id = i+1 (id 0 corresponds to soma) - // 2. sections[i].parent_id < sections[i].id - // 3. sections[i].terminal iff !exists j s.t. sections[j].parent_id = sections[i].id - bool check_valid() const; - - // Throw morphology_error if invariants violated. - void assert_valid() const; - - // Re-discretize all sections. - void segment(double dx) { - for (auto& s: sections) s.segment(dx); - } - - // Add new section from sequence of section points. Return reference to new section. - section_geometry& add_section(std::vector<section_point> points, unsigned parent_id = 0, section_kind kind = section_kind::none); -}; - -} // namespace arb diff --git a/arbor/include/arbor/segment.hpp b/arbor/include/arbor/segment.hpp index ece282fbe0f122e2cf6c080201709950f0a9be55..10264eb525079b5ba51f78f3f19feee77bbb669d 100644 --- a/arbor/include/arbor/segment.hpp +++ b/arbor/include/arbor/segment.hpp @@ -13,13 +13,19 @@ #include <arbor/cable_cell_param.hpp> #include <arbor/common_types.hpp> #include <arbor/math.hpp> -#include <arbor/morphology.hpp> #include <arbor/mechinfo.hpp> #include <arbor/point.hpp> #include <arbor/util/optional.hpp> namespace arb { +enum class section_kind { + soma, + dendrite, + axon, + none +}; + // forward declarations of segment specializations class soma_segment; class cable_segment; diff --git a/arbor/include/arbor/swcio.hpp b/arbor/include/arbor/swcio.hpp index 50003bd1180db7023ba1a50000a67b5e0107907b..368a44653e85229fb39bd25308657fc745435365 100644 --- a/arbor/include/arbor/swcio.hpp +++ b/arbor/include/arbor/swcio.hpp @@ -10,7 +10,6 @@ #include <arbor/assert.hpp> #include <arbor/arbexcept.hpp> -#include <arbor/morphology.hpp> #include <arbor/point.hpp> namespace arb { @@ -24,34 +23,25 @@ struct swc_error: public arbor_exception { class swc_record { public: + static constexpr int undefined_tag = 0; + + using tag_type = int; using id_type = int; using coord_type = double; - // More on SWC files: http://research.mssm.edu/cnic/swc.html - enum class kind { - undefined = 0, - soma, - axon, - dendrite, - apical_dendrite, - fork_point, - end_point, - custom - }; - - kind type = kind::undefined; // record type + tag_type tag = undefined_tag; // record type id_type id = 0; // record id coord_type x = 0; // record coordinates coord_type y = 0; coord_type z = 0; coord_type r = 0; // record radius - id_type parent_id= -1; // record parent's id + id_type parent_id= -1; // record parent's id // swc records assume zero-based indexing; root's parent remains -1 - swc_record(swc_record::kind type, int id, + swc_record(int tag, int id, coord_type x, coord_type y, coord_type z, coord_type r, int parent_id): - type(type), id(id), x(x), y(y), z(z), r(r), parent_id(parent_id) + tag(tag), id(id), x(x), y(y), z(z), r(r), parent_id(parent_id) {} swc_record() = default; @@ -81,10 +71,6 @@ public: return arb::point<coord_type>(x, y, z); } - arb::section_point as_section_point() const { - return arb::section_point{x, y, z, r}; - } - // validity checks bool is_consistent() const; void assert_consistent() const; // throw swc_error if inconsistent. @@ -98,9 +84,6 @@ std::istream& operator>>(std::istream& is, swc_record& record); // Throw on parsing failure. std::vector<swc_record> parse_swc_file(std::istream& is); -// Convert a canonical (see below) vector of SWC records to a morphology object. -morphology swc_as_morphology(const std::vector<swc_record>& swc_records); - // Given a vector of random-access mutable sequence of `swc_record` describing // a single morphology, check for consistency and renumber records // so that ids are contiguous within branches, have no gaps, and diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp new file mode 100644 index 0000000000000000000000000000000000000000..23d38c1d2cb7448d863828f0ba83ee6124b42c33 --- /dev/null +++ b/arbor/morph/morphology.cpp @@ -0,0 +1,189 @@ +#include <cmath> +#include <iostream> +#include <unordered_map> +#include <utility> + +#include <arbor/math.hpp> +#include <arbor/morph/error.hpp> +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/sample_tree.hpp> +#include <arbor/morph/primitives.hpp> + +#include "algorithms.hpp" +#include "io/sepval.hpp" +#include "util/span.hpp" +#include "util/strprintf.hpp" + +namespace arb { + +namespace impl{ + +std::vector<mbranch> branches_from_parent_index(const std::vector<msize_t>& parents, + const std::vector<point_prop>& props, + bool spherical_root) +{ + using util::make_span; + + const char* errstr_single_sample_root = + "A morphology with only one sample must have a spherical root"; + const char* errstr_incomplete_cable = + "A branch must contain at least two samples"; + + if (parents.empty()) return {}; + + auto nsamp = parents.size(); + + // Enforce that a morphology with one sample has a spherical root. + if (!spherical_root && nsamp==1u) { + throw morphology_error(errstr_single_sample_root); + } + + std::vector<int> bids(nsamp); + int nbranches = spherical_root? 1: 0; + for (auto i: make_span(1, nsamp)) { + auto p = parents[i]; + bool first = is_root(props[p]) || is_fork(props[p]); + bids[i] = first? nbranches++: bids[p]; + } + + std::vector<mbranch> branches(nbranches); + for (auto i: make_span(nsamp)) { + auto p = parents[i]; + auto& branch = branches[bids[i]]; + if (!branch.size()) { + bool null_root = spherical_root? p==mnpos: p==mnpos||p==0; + branch.parent_id = null_root? mnpos: bids[p]; + + // Add the distal sample from the parent branch if the parent is not + // a spherical root or mnpos. + if (p!=mnpos && !(p==0 && spherical_root)) { + branch.index.push_back(p); + } + } + + branch.index.push_back(i); + } + + // Enforce that all cable branches that are potentially connected to a spherical + // root contain at least two samples. + if (spherical_root) { + for (auto i: make_span(1, nbranches)) { // skip the root. + if (branches[i].size()<2u) { + throw morphology_error(errstr_incomplete_cable); + } + } + } + + return branches; +} + +// 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) { + if (st.empty()) return false; + auto& P = st.parents(); + auto& S = st.samples(); + auto root_tag = S.front().tag; + for (auto i: util::make_span(1, st.size())) { + if (0u==P[i] && S[i].tag==root_tag) { + return false; + } + } + return true; +} + +} // namespace impl + +bool operator==(const mbranch& l, const mbranch& r) { + return l.parent_id==r.parent_id && l.index==r.index; +} + +std::ostream& operator<<(std::ostream& o, const mbranch& b) { + o <<"mbranch([" << io::csv(b.index) << "], "; + if (b.parent_id==mnpos) o << "none)"; + else o << b.parent_id << ")"; + return o; +} + +morphology::morphology(sample_tree m, bool use_spherical_root): + samples_(std::move(m)), + spherical_root_(use_spherical_root) +{ + init(); +} + +morphology::morphology(sample_tree m): + samples_(std::move(m)), + spherical_root_(impl::root_sample_has_same_tag_as_child(samples_)) +{ + init(); +} + +void morphology::init() { + using util::make_span; + using util::count_along; + + auto nsamp = samples_.size(); + + if (!nsamp) return; + + // Generate branches. + branches_ = impl::branches_from_parent_index(samples_.parents(), samples_.properties(), spherical_root_); + auto nbranch = branches_.size(); + + // Generate branch tree. + branch_children_.resize(nbranch); + branch_parents_.reserve(nbranch); + for (auto i: make_span(nbranch)) { + auto id = branches_[i].parent_id; + branch_parents_.push_back(id); + if (id!=mnpos) { + branch_children_[id].push_back(i); + } + } +} + +// The parent branch of branch b. +msize_t morphology::branch_parent(msize_t b) const { + return branch_parents_[b]; +} + +// The parent sample of sample i. +const std::vector<msize_t>& morphology::sample_parents() const { + return samples_.parents(); +} + +// The child branches of branch b. +const std::vector<msize_t>& morphology::branch_children(msize_t b) const { + return branch_children_[b]; +} + +// Whether the root of the morphology is spherical. +bool morphology::spherical_root() const { + return spherical_root_; +} + +morphology::index_range morphology::branch_sample_span(msize_t b) const { + const auto& idx = branches_[b].index; + return std::make_pair(idx.data(), idx.data()+idx.size()); +} + +const std::vector<msample>& morphology::samples() const { + return samples_.samples(); +} + +msize_t morphology::num_branches() const { + return branches_.size(); +} + +std::ostream& operator<<(std::ostream& o, const morphology& m) { + o << "morphology: " + << m.samples_.size() << " samples, " + << m.num_branches() << " branches."; + for (auto i: util::make_span(m.num_branches())) + o << "\n branch " << i << ": " << m.branches_[i]; + + return o; +} + +} // namespace arb + diff --git a/arbor/morph/primitives.cpp b/arbor/morph/primitives.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6decddbe1c93fa5c2ee35da12d59a9001fff77e8 --- /dev/null +++ b/arbor/morph/primitives.cpp @@ -0,0 +1,50 @@ +#include <ostream> + +#include <arbor/math.hpp> +#include <arbor/morph/primitives.hpp> + +#include "io/sepval.hpp" +#include "util/span.hpp" +#include "util/rangeutil.hpp" + +namespace arb { + +// interpolate between two points. +mpoint lerp(const mpoint& a, const mpoint& b, double u) { + return { math::lerp(a.x, b.x, u), + math::lerp(a.y, b.y, u), + math::lerp(a.z, b.z, u), + math::lerp(a.radius, b.radius, u) }; +} + +// test if two morphology sample points share the same location. +bool is_collocated(const mpoint& a, const mpoint& b) { + return a.x==b.x && a.y==b.y && a.z==b.z; +} + +// calculate the distance between two morphology sample points. +double distance(const mpoint& a, const mpoint& b) { + double dx = a.x - b.x; + double dy = a.y - b.y; + double dz = a.z - b.z; + + return std::sqrt(dx*dx + dy*dy * dz*dz); +} + +bool is_collocated(const msample& a, const msample& b) { + return is_collocated(a.loc, b.loc); +} + +double distance(const msample& a, const msample& b) { + return distance(a.loc, b.loc); +} + +std::ostream& operator<<(std::ostream& o, const mpoint& p) { + return o << "mpoint(" << p.x << "," << p.y << "," << p.z << "," << p.radius << ")"; +} + +std::ostream& operator<<(std::ostream& o, const msample& s) { + return o << "msample(" << s.loc << ", " << s.tag << ")"; +} + +} // namespace arb diff --git a/arbor/morph/sample_tree.cpp b/arbor/morph/sample_tree.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7a687e52ca9eb966470fbdd23f2c9d14ff010c93 --- /dev/null +++ b/arbor/morph/sample_tree.cpp @@ -0,0 +1,132 @@ +#include <cmath> +#include <vector> + +#include <arbor/math.hpp> + +#include <arbor/morph/error.hpp> +#include <arbor/morph/sample_tree.hpp> + +#include "algorithms.hpp" +#include "io/sepval.hpp" +#include "util/span.hpp" + +namespace arb { + +sample_tree::sample_tree(std::vector<msample> samples, std::vector<msize_t> parents) { + if (samples.size()!=parents.size()) { + throw std::runtime_error( + "The same number of samples and parent indices used to create a sample morphology"); + } + reserve(samples.size()); + for (auto i: util::make_span(samples.size())) { + append(parents[i], samples[i]); + } +} + +void sample_tree::reserve(msize_t n) { + samples_.reserve(n); + parents_.reserve(n); + props_.reserve(n); +} + +msize_t sample_tree::append(msize_t p, const msample& s) { + if (empty()) { + if (p!=mnpos) throw morphology_error("Parent id of root sample must be mnpos"); + } + else if (p>=size()) { + throw morphology_error("Parent id of a sample must be less than the sample id"); + } + auto id = size(); + + samples_.push_back(s); + parents_.push_back(p); + + // Set the point properties for the new point, and update those of its parent as needed. + point_prop prop = point_prop_mask_none; + if (!id) { + // if adding the first sample mark it as root + set_root(prop); + } + else { + // Mark the new node as terminal, and unset the parent sample's terminal bit. + set_terminal(prop); + const bool term_parent = is_terminal(props_[p]); // track if the parent was terminal. + unset_terminal(props_[p]); + + // Mark if the new sample is collocated with its parent. + if (is_collocated(s, samples_[p])) { + set_collocated(prop); + } + + // Set parent to be a fork if it was not a terminal point before the + // new sample was added (and if it isn't the root). + if (p && !term_parent) { + set_fork(props_[p]); + } + } + props_.push_back(prop); + + return id; +} + +msize_t sample_tree::append(const msample& s) { + std::cout << "decision : " << empty() << " " << (empty()? mnpos: size()-1) << "\n"; + return append(empty()? mnpos: size()-1, s); +} + +msize_t sample_tree::append(msize_t p, const std::vector<msample>& slist) { + if (!slist.size()) return size(); + + for (auto& s: slist) { + p = append(p, s); + } + + return p; +} + +msize_t sample_tree::append(const std::vector<msample>& slist) { + return append(empty()? mnpos: size()-1, slist); +} + +msize_t sample_tree::size() const { + return samples_.size(); +} + +bool sample_tree::empty() const { + return samples_.empty(); +} + +const std::vector<msample>& sample_tree::samples() const { + return samples_; +} + +const std::vector<msize_t>& sample_tree::parents() const { + return parents_; +} + +const std::vector<point_prop>& sample_tree::properties() const { + return props_; +} + +std::ostream& operator<<(std::ostream& o, const sample_tree& m) { + o << "sample_tree:" + << "\n " << m.size() << " samples" + << "\n samples [" << io::csv(m.samples_) << "]" + << "\n parents [" << io::csv(m.parents_) << "]"; + return o; +} + +sample_tree swc_as_sample_tree(const std::vector<swc_record>& swc_records) { + sample_tree m; + m.reserve(swc_records.size()); + + for (auto i: util::count_along(swc_records)) { + auto& r = swc_records[i]; + // The parent of soma must be mnpos, while in SWC files is -1 + msize_t p = i==0? mnpos: r.parent_id; + m.append(p, msample{{r.x, r.y, r.z, r.r}, r.tag}); + } + return m; +} + +} // namespace arb diff --git a/arbor/morphology.cpp b/arbor/morphology.cpp deleted file mode 100644 index 2243772b9cfe99f89460080cf9fd8e5b7664a349..0000000000000000000000000000000000000000 --- a/arbor/morphology.cpp +++ /dev/null @@ -1,135 +0,0 @@ -#include <cmath> -#include <vector> - -#include <arbor/morphology.hpp> -#include <arbor/math.hpp> - -namespace arb { - -using ::arb::math::lerp; - -static section_point lerp(const section_point& a, const section_point& b, double u) { - return { lerp(a.x, b.x, u), lerp(a.y, b.y, u), lerp(a.z, b.z, u), lerp(a.r, b.r, u) }; -} - -static double distance(const section_point& a, const section_point& b) { - double dx = b.x-a.x; - double dy = b.y-a.y; - double dz = b.z-a.z; - - return std::sqrt(dx*dx+dy*dy+dz*dz); -} - -void section_geometry::segment(double dx) { - unsigned npoint = points.size(); - if (dx<=0 || npoint<2) return; - - // Re-discretize into nseg segments (nseg+1 points). - unsigned nseg = static_cast<unsigned>(std::ceil(length/dx)); - - std::vector<section_point> sampled; - sampled.push_back(points.front()); - double sampled_length = 0; - - // [left, right) is the path-length interval for successive - // linear segments in the section. - double left = 0; - double right = left+distance(points[1], points[0]); - - // x is the next sample point (in path-length). - double x = length/nseg; - - // Scan segments for sample points. - unsigned i = 1; - for (;;) { - if (right>x) { - double u = (x-left)/(right-left); - sampled.push_back(lerp(points[i-1], points[i], u)); - unsigned k = sampled.size(); - sampled_length += distance(sampled[k-2], sampled[k-1]); - x = k*length/nseg; - } - else { - ++i; - if (i>=npoint) break; - - left = right; - right = left+distance(points[i-1], points[i]); - } - } - if (sampled.size()<=nseg) { - sampled.push_back(points.back()); - } - - points = std::move(sampled); - length = sampled_length; -} - -static double compute_length(const std::vector<section_point>& points) { - double length = 0; - std::size_t npoint = points.size(); - - for (std::size_t i =1; i<npoint; ++i) { - length += distance(points[i], points[i-1]); - } - - return length; -} - -section_geometry& morphology::add_section(std::vector<section_point> points, unsigned parent_id, section_kind kind) { - section_geometry section; - section.id = sections.size()+1; - section.parent_id = parent_id; - section.terminal = true; - section.points = std::move(points); - section.kind = kind; - section.length = compute_length(section.points); - - if (section.parent_id >= section.id) { - throw morphology_error("improper parent id for section"); - } - - if (section.parent_id>0) { - sections[section.parent_id-1].terminal = false; - } - sections.push_back(std::move(section)); - return sections.back(); -} - -static const char* morphology_invariant_violation(const morphology& m) { - std::size_t nsection = m.sections.size(); - std::vector<int> terminal(nsection, true); - - for (std::size_t i=0; i<nsection; ++i) { - auto id = m.sections[i].id; - auto parent_id = m.sections[i].parent_id; - - if (id!=i+1) return "section id does not correspond to index"; - if (parent_id>=id) return "section parent id not less than section id"; - if (parent_id>0) { - terminal[parent_id-1] = false; - } - if (parent_id==0 && !m.has_soma()) return "section has parent 0 but morphology has no (spherical) soma"; - } - - for (std::size_t i=0; i<nsection; ++i) { - if (terminal[i] && !m.sections[i].terminal) return "non-terminal section is marked terminal"; - if (!terminal[i] && m.sections[i].terminal) return "terminal section is marked non-terminal"; - } - - return nullptr; -} - -bool morphology::check_valid() const { - return morphology_invariant_violation(*this)==nullptr; -} - -void morphology::assert_valid() const { - auto error = morphology_invariant_violation(*this); - if (error) throw morphology_error(error); -} - - - - -} // namespace arb diff --git a/arbor/swcio.cpp b/arbor/swcio.cpp index ef18c38619eb3466ecff5d83302b31d1cf4449b7..8c0b14b5d4a075c4b318ec88b753069e7faeff9c 100644 --- a/arbor/swcio.cpp +++ b/arbor/swcio.cpp @@ -6,7 +6,6 @@ #include <unordered_set> #include <arbor/assert.hpp> -#include <arbor/morphology.hpp> #include <arbor/point.hpp> #include <arbor/swcio.hpp> @@ -19,10 +18,8 @@ namespace arb { // helper function: return error message if inconsistent, or nullptr if ok. const char* swc_record_error(const swc_record& r) { - constexpr int max_type = static_cast<int>(swc_record::kind::custom); - - if (static_cast<int>(r.type) < 0 || static_cast<int>(r.type) > max_type) { - return "unknown record type"; + if (r.tag<0) { + return "unknown record tag"; } if (r.id < 0) { @@ -60,9 +57,7 @@ static bool parse_record(const std::string& line, swc_record& record) { std::istringstream is(line); swc_record r; - int type_as_int; - is >> r.id >> type_as_int >> r.x >> r.y >> r.z >> r.r >> r.parent_id; - r.type = static_cast<swc_record::kind>(type_as_int); + is >> r.id >> r.tag >> r.x >> r.y >> r.z >> r.r >> r.parent_id; if (is) { // Convert to zero-based, leaving parent_id as-is if -1 --r.id; @@ -105,7 +100,7 @@ std::istream& operator>>(std::istream& is, swc_record& record) { std::ostream& operator<<(std::ostream& os, const swc_record& record) { // output in one-based indexing os << record.id+1 << " " - << static_cast<int>(record.type) << " " + << record.tag << " " << std::setprecision(7) << record.x << " " << std::setprecision(7) << record.y << " " << std::setprecision(7) << record.z << " " @@ -154,70 +149,6 @@ std::vector<swc_record> parse_swc_file(std::istream& is) { return records; } -morphology swc_as_morphology(const std::vector<swc_record>& swc_records) { - morphology morph; - - std::vector<swc_record::id_type> swc_parent_index; - for (const auto& r: swc_records) { - swc_parent_index.push_back(r.parent_id); - } - - if (swc_parent_index.empty()) { - return morph; - } - - // The parent of soma must be 0, while in SWC files is -1 - swc_parent_index[0] = 0; - auto branch_index = algorithms::branches(swc_parent_index); // partitions [0, #records] by branch. - auto parent_branch_index = algorithms::tree_reduce(swc_parent_index, branch_index); - - // sanity check - arb_assert(parent_branch_index.size() == branch_index.size() - 1); - - // Add the soma first; then the segments - const auto& soma = swc_records[0]; - morph.soma = { soma.x, soma.y, soma.z, soma.r }; - - for (auto i: util::make_span(1, parent_branch_index.size())) { - auto b_start = swc_records.begin() + branch_index[i]; - auto b_end = swc_records.begin() + branch_index[i+1]; - - unsigned parent_id = parent_branch_index[i]; - std::vector<section_point> points; - section_kind kind = section_kind::none; - - if (parent_id != 0) { - // include the parent of current record if not branching from soma - auto parent_record = swc_records[swc_parent_index[branch_index[i]]]; - - points.push_back(section_point{parent_record.x, parent_record.y, parent_record.z, parent_record.r}); - } - - for (auto b = b_start; b!=b_end; ++b) { - points.push_back(section_point{b->x, b->y, b->z, b->r}); - - switch (b->type) { - case swc_record::kind::axon: - kind = section_kind::axon; - break; - case swc_record::kind::dendrite: - case swc_record::kind::apical_dendrite: - kind = section_kind::dendrite; - break; - case swc_record::kind::soma: - kind = section_kind::soma; - break; - default: ; // stick with what we have - } - } - - morph.add_section(std::move(points), parent_id, kind); - } - - morph.assert_valid(); - return morph; -} - void swc_canonicalize(std::vector<swc_record>& swc_records) { std::unordered_set<swc_record::id_type> ids; diff --git a/arbor/util/range.hpp b/arbor/util/range.hpp index 154f8c35969b3a614c3987ace87717cc00ed09ef..9f175abf98e06e971e8280e28755bf6d8ffe41a7 100644 --- a/arbor/util/range.hpp +++ b/arbor/util/range.hpp @@ -28,11 +28,11 @@ #include <arbor/assert.hpp> -#include <util/counter.hpp> -#include <util/either.hpp> -#include <util/iterutil.hpp> -#include <util/meta.hpp> -#include <util/sentinel.hpp> +#include "util/either.hpp" +#include "util/counter.hpp" +#include "util/iterutil.hpp" +#include "util/meta.hpp" +#include "util/sentinel.hpp" namespace arb { namespace util { diff --git a/example/single/single.cpp b/example/single/single.cpp index 3855f9de4bbed6b618914773abdf67fd178ad232..df614950c3cf6b2060e48593dc71c92a283c8aca 100644 --- a/example/single/single.cpp +++ b/example/single/single.cpp @@ -7,7 +7,8 @@ #include <arbor/load_balance.hpp> #include <arbor/cable_cell.hpp> -#include <arbor/morphology.hpp> +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/sample_tree.hpp> #include <arbor/swcio.hpp> #include <arbor/simulation.hpp> #include <arbor/simple_sampler.hpp> @@ -26,7 +27,7 @@ arb::morphology default_morphology(); arb::morphology read_swc(const std::string& path); struct single_recipe: public arb::recipe { - explicit single_recipe(arb::morphology m): morpho(m) { + explicit single_recipe(arb::morphology m): morpho(std::move(m)) { gprop.default_parameters = arb::neuron_parameter_defaults; } @@ -53,7 +54,7 @@ struct single_recipe: public arb::recipe { } arb::util::unique_any get_cell_description(arb::cell_gid_type) const override { - arb::cable_cell c = make_cable_cell(morpho); + arb::cable_cell c = make_cable_cell(morpho, false); // Add HH mechanism to soma, passive channels to dendrites. // Discretize dendrites according to the NEURON d-lambda rule. @@ -70,9 +71,9 @@ struct single_recipe: public arb::recipe { seg->set_compartments(n); } - // Add synapse to last segment. + // Add synapse to last branch. - arb::cell_lid_type last_segment = morpho.components()-1; + arb::cell_lid_type last_segment = morpho.num_branches()-1; arb::segment_location end_last_segment = { last_segment, 1. }; c.add_synapse(end_last_segment, "exp2syn"); @@ -147,25 +148,19 @@ options parse_options(int argc, char** argv) { // to 0.2 µm. arb::morphology default_morphology() { - arb::morphology m; + arb::sample_tree samples; - // Points in a morphology are expressed as (x, y, z, r), - // with r being the radius. Units are µm. + auto p = samples.append(arb::msample{{ 0.0, 0.0, 0.0, 6.3}, 1}); + std::cout << "p is " << p << "\n"; + p = samples.append(p, arb::msample{{ 6.3, 0.0, 0.0, 0.5}, 3}); + p = samples.append(p, arb::msample{{206.3, 0.0, 0.0, 0.2}, 3}); - m.soma = {0., 0., 0., 6.3}; - - std::vector<arb::section_point> dendrite = { - {6.3, 0., 0., 0.5}, - {206.3, 0., 0., 0.2} - }; - m.add_section(dendrite, 0); - - return m; + return arb::morphology(std::move(samples)); } arb::morphology read_swc(const std::string& path) { std::ifstream f(path); if (!f) throw std::runtime_error("unable to open SWC file: "+path); - return arb::swc_as_morphology(arb::parse_swc_file(f)); + return arb::morphology(arb::swc_as_sample_tree(arb::parse_swc_file(f))); } diff --git a/lmorpho/CMakeLists.txt b/lmorpho/CMakeLists.txt deleted file mode 100644 index c198d458fd6135fe56c1e6fd2ed0e9a1e2572aa6..0000000000000000000000000000000000000000 --- a/lmorpho/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -add_executable(lmorpho lmorpho.cpp lsystem.cpp lsys_models.cpp morphio.cpp) - -target_link_libraries(lmorpho PRIVATE arbor arbor-sup) - -# TODO: resolve public headers -target_link_libraries(lmorpho PRIVATE arbor-private-headers) - -install(TARGETS lmorpho RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/lmorpho/README.md b/lmorpho/README.md deleted file mode 100644 index 561bbebd863f218a44fddb4affb65203ca9d0775..0000000000000000000000000000000000000000 --- a/lmorpho/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# `lmporho` — generate random neuron morphologies - -`lmorpho` implements an L-system geometry generator for the purpose of -generating a series of artificial neuron morphologies as described by a set of -stasticial parameters. - -## Method - -The basic algorithm used is that of [Burke (1992)][burke1992], with extensions -for embedding in 3-d taken from [Ascoli (2001)][ascoli2001]. - -A dendritic tree is represented by a piece-wise linear embedding of a rooted -tree into R³×Râº, describing the position in space and non-negative radius. -Morphologies are represented as a sequence of these trees together with a point -and radius describing a (spherical) soma. - -The generation process for a tree successively grows the terminal points by -some length ΔL each step, changing its orientation each step according to a -random distribution. After growing, there is a radius-dependent probability -that it will bifurcate or terminate. Parameters describing these various -probability distributions constitute the L-system model. - -The distributions used in [Ascoli (2001)][ascoli2001] can be constant (i.e. not -random), uniform over some interval, truncated normal, or a mixture -distribution of these. In the present version of the code, mixture -distributions are not supported. - -## Output - -Generated morphologies can be output either in -[SWC format](http://research.mssm.edu/cnic/swc.html), or as 'parent vectors', -which describe the topology of the associated tree. - -Entry _i_ of the (zero-indexed) parent vector gives the index of the proximal -unbranched section to which it connects. Somata have a parent index of -1. - -Morphology information can be written one each to separate files or -concatenated. If concatenated, the parent vector indices will be shifted as -required to maintain consistency. - -## Models - -Two L-system parameter sets are provided as 'built-in'; if neither model is -selected, a simple, non-biological default model is used. - -As mixture distributions are not yet supported in `lmorpho`, these models are -simplified approximations to those in the literature as described below. - -### Motor neuron model - -The 'motoneuron' model corresponds to the adult cat spinal alpha-motoneuron -model described in [Ascoli (2001)][ascoli2001], based in turn on the model of -[Burke (1992)][burke1992]. - -The model parameters in Ascoli are not complete; missing parameters were taken -from the corresponding ArborVitae model parameters in the same paper, and -somatic diameters were taken from [Cullheim (1987)][cullheim1987]. - -### Purkinke neuron model - -The 'purkinke' model corresponds to the guinea pig Purkinje cell model from -[Ascoli (2001)][ascoli2001], which was fit to data from [Rapp -(1994)][rapp1994]. Somatic diameters are a simple fit to the three measurements -in the original source. - -Some required parameters are not recorded in [Ascoli (2001)][ascoli2001]; -the correlation component `diam_child_a` and the `length_step` ΔL are -taken from the corresponding L-Neuron parameter description in the -[L-Neuron database](l-neuron-database). These should be verified by -fitting against the digitized morphologies as found in the database. - -Produced neurons from this model do not look especially realistic. - -### Other models - -There is not yet support for loading in arbitrary parameter sets, or for -translating or approximating the parameters that can be found in the -[L-Neuron database][l-neuron-database]. Support for parameter estimation -from a population of discretized morphologies would be useful. - -## References - -1. Ascoli, G. A., Krichmar, J. L., Scorcioni, R., Nasuto, S. J., Senft, S. L. - and Krichmar, G. L. (2001). Computer generation and quantitative morphometric - analysis of virtual neurons. _Anatomy and Embryology_ _204_(4), 283–301. - [DOI: 10.1007/s004290100201][ascoli2001] - -2. Burke, R. E., Marks, W. B. and Ulfhake, B. (1992). A parsimonious description - of motoneuron dendritic morphology using computer simulation. - _The Journal of Neuroscience_ _12_(6), 2403–2416. [online][burke1992] - -3. Cullheim, S., Fleshman, J. W., Glenn, L. L., and Burke, R. E. (1987). - Membrane area and dendritic structure in type-identified triceps surae alpha - motoneurons. _The Journal of Comparitive Neurology_ _255_(1), 68–81. - [DOI: 10.1002/cne.902550106][cullheim1987] - -4. Rapp, M., Segev, I. and Yaom, Y. (1994). Physiology, morphology and detailed - passive models of guinea-pig cerebellar Purkinje cells. - _The Journal of Physiology_, _474_, 101–118. - [DOI: 10.1113/jphysiol.1994.sp020006][rapp1994]. - - -[ascoli2001]: http://dx.doi.org/10.1007/s004290100201 - "Ascoli et al. (2001). Computer generation […] of virtual neurons." - -[burke1992]: http://www.jneurosci.org/content/12/6/2403 - "Burke et al. (1992). A parsimonious description of motoneuron dendritic morphology […]" - -[cullheim1987]: http://dx.doi.org/10.1002/cne.902550106 - "Cullheim et al. (1987). Membrane area and dendritic structure in […] alpha motoneurons." - -[rapp1994]: http://dx.doi.org/10.1113/jphysiol.1994.sp020006 - "Rapp et al. (1994). Physiology, morphology […] of guinea-pig cerebellar Purkinje cells." - -[l-neuron-database]: http://krasnow1.gmu.edu/cn3/L-Neuron/database/index.html diff --git a/lmorpho/lmorpho.cpp b/lmorpho/lmorpho.cpp deleted file mode 100644 index 56385a925e9e88cf2daec26646249eb0e2c1a5a8..0000000000000000000000000000000000000000 --- a/lmorpho/lmorpho.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include <algorithm> -#include <iostream> -#include <fstream> -#include <map> -#include <sstream> -#include <vector> - -#include <arbor/morphology.hpp> -#include <arbor/util/optional.hpp> -#include <sup/tinyopt.hpp> - -#include "morphio.hpp" -#include "lsystem.hpp" -#include "lsys_models.hpp" - -using arb::util::optional; -using arb::util::nullopt; -using arb::util::just; - -const char* usage_str = -"[OPTION]...\n" -"\n" -" -n, --count=N Number of morphologies to generate.\n" -" -m, --model=MODEL Use L-system MODEL for generation (see below).\n" -" -g, --segment=DX Segment model into compartments of max size DX µm.\n" -" --swc=FILE Output morphologies as SWC to FILE (see below).\n" -" --pvec=FILE Output 'parent vector' structural representation\n" -" to FILE.\n" -" -h, --help Emit this message and exit.\n" -"\n" -"Generate artificial neuron morphologies based on L-system descriptions.\n" -"\n" -"If a FILE argument contains a '%', then one file will be written for\n" -"each generated morphology, with the '%' replaced by the index of the\n" -"morphology, starting from zero. Output for each morphology will otherwise\n" -"be concatenated: SWC files will be headed by a comment line with the\n" -"index of the morphology; parent vectors will be merged into one long\n" -"vector. A FILE argument of '-' corresponds to standard output.\n" -"\n" -"Currently supported MODELs:\n" -" motoneuron Adult cat spinal alpha-motoneurons, based on models\n" -" and data in Burke 1992 and Ascoli 2001.\n" -" purkinje Guinea pig Purkinje cells, basd on models and data\n" -" from Rapp 1994 and Ascoli 2001.\n"; - -int main(int argc, char** argv) { - // options - int n_morph = 1; - optional<unsigned> rng_seed; - optional<std::string> swc_file; - optional<std::string> pvector_file; - double segment_dx = 0; - - std::pair<const char*, const lsys_param*> models[] = { - {"motoneuron", &alpha_motoneuron_lsys}, - {"purkinje", &purkinje_lsys} - }; - lsys_param P; - - try { - auto arg = argv+1; - while (*arg) { - if (auto o = to::parse_opt<int>(arg, 'n', "count")) { - n_morph = *o; - } - if (auto o = to::parse_opt<int>(arg, 's', "seed")) { - rng_seed = *o; - } - else if (auto o = to::parse_opt<std::string>(arg, 0, "swc")) { - swc_file = *o; - } - else if (auto o = to::parse_opt<double>(arg, 'g', "segment")) { - segment_dx = *o; - } - else if (auto o = to::parse_opt<std::string>(arg, 'p', "pvec")) { - pvector_file = *o; - } - else if (auto o = to::parse_opt<const lsys_param*>(arg, 'm', "model", to::keywords(models))) { - P = **o; - } - else if (to::parse_opt(arg, 'h', "help")) { - std::cout << "Usage: " << argv[0] << " " << usage_str; - return 0; - } - else { - throw to::parse_opt_error(*arg, "unrecognized option"); - } - } - - std::minstd_rand g; - if (rng_seed) g.seed(rng_seed.value()); - - auto emit_swc = swc_file? just(swc_emitter(*swc_file, n_morph)): nullopt; - auto emit_pvec = pvector_file? just(pvector_emitter(*pvector_file, n_morph)): nullopt; - - for (int i=0; i<n_morph; ++i) { - auto morph = generate_morphology(P, g); - morph.segment(segment_dx); - - if (emit_swc) (*emit_swc)(i, morph); - if (emit_pvec) (*emit_pvec)(i, morph); - } - } - catch (to::parse_opt_error& e) { - std::cerr << argv[0] << ": " << e.what() << "\n"; - std::cerr << "Try '" << argv[0] << " --help' for more information.\n"; - std::exit(2); - } - catch (std::exception& e) { - std::cerr << "caught exception: " << e.what() << "\n"; - std::exit(1); - } -} - diff --git a/lmorpho/lsys_models.cpp b/lmorpho/lsys_models.cpp deleted file mode 100644 index 41caf7e19bdb70606191f304343018c16aa92464..0000000000000000000000000000000000000000 --- a/lmorpho/lsys_models.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include <cmath> - -#include "lsystem.hpp" -#include "lsys_models.hpp" - -static constexpr double inf = INFINITY; - -// Predefined parameters for two classes of neurons. Numbers taken primarily -// from Ascoli et al. 2001, but some details (soma diameters for example) -// taken from earlier literature. -// -// Without mixture distribution support, these aren't entirely faithful to -// the original sources. -// -// Refer to `README.md` for references and more information. - -lsys_param make_alpha_motoneuron_lsys() { - lsys_param L; - - // Soma diameter [µm]. - L.diam_soma = { 47.0, 65.5, 57.6, 5.0 }; - - // Number of dendritic trees (rounded to nearest int). - L.n_tree = { 8.0, 16.0 }; - - // Initial dendrite diameter [µm]. (Dstem) - L.diam_initial = { 3.0, 12.0 }; - - // Dendrite step length [µm]. (ΔL) - L.length_step = { 25 }; - - // Initial roll (intrinsic rotation about x-axis) [degrees]. - L.roll_initial = { -180.0, 180.0 }; - - // Initial pitch (intrinsic rotation about y-axis) [degrees]. - L.pitch_initial = { 0.0, 180.0 }; - - // Tortuousness: roll within section over ΔL [degrees]. - L.roll_section = { -180.0, 180.0 }; - - // Tortuousness: pitch within section over ΔL [degrees]. - L.pitch_section = { -15.0, 15.0, 0.0, 5.0 }; - - // Taper rate: diameter decrease per unit length. - L.taper = { -1.25e-3 }; - - // Branching torsion: roll at branch point [degrees]. - L.roll_at_branch = { 0.0, 180.0 }; - - // Branch angle between siblings [degrees]. - L.branch_angle = { 1.0, 172.0, 45.0, 20.0 }; - - // Correlated child branch radius distribution parameters. - L.diam_child_a = -0.2087; - L.diam_child_r = { 0.2, 1.8, 0.8255, 0.2125 }; - - // Termination probability parameters [1/µm]. - L.pterm_k1 = 2.62e-2; - L.pterm_k2 = -2.955; - - // Bifurcation probability parameters [1/μm]. - L.pbranch_ov_k1 = 2.58e-5; - L.pbranch_ov_k2 = 2.219; - L.pbranch_nov_k1 = 2.34e-3; - L.pbranch_nov_k2 = 0.194; - - // Absolute maximum dendritic extent [µm]. - L.max_extent = 2000; - - return L; -} - -lsys_param alpha_motoneuron_lsys = make_alpha_motoneuron_lsys(); - -// Some parameters missing from the literature are taken from -// the `purkburk.prm` L-Neuron parameter file: -// http://krasnow1.gmu.edu/cn3/L-Neuron/database/purk/bu/purkburk.prm -// Note 'Pov' and 'Pterm' numbers are swapped in Ascoli fig 3B. - -lsys_param make_purkinje_lsys() { - lsys_param L; - - // Soma diameter [µm]. - // (Gaussian fit to 3 samples rom Rapp 1994); truncate to 2σ. - L.diam_soma = { 22.12, 27.68, 24.9, 1.39 }; - - // Number of dendritic trees (rounded to nearest int). - L.n_tree = { 1 }; - - // Initial dendrite diameter [µm]. (Dstem) - L.diam_initial = { 4.8, 7.6, 6.167, 1.069 }; - - // Dendrite step length [µm]. (ΔL) - L.length_step = { 2.3 }; // from `purkburk.prm` - - // Tortuousness: roll within section over ΔL [degrees]. - L.roll_section = { 0 }; - - // Tortuousness: pitch within section over ΔL [degrees]. - L.pitch_section = { -5, 5 }; // from `purkburk.prm` - - // Taper rate: diameter decrease per unit length. - L.taper = { -inf, inf, -0.010, 0.022 }; - - // Branching torsion: roll at branch point [degrees]. - L.roll_at_branch = { -inf, inf, 0.0, 6.5 }; // from `purkburj.prm` - - // Branch angle between siblings [degrees]. - L.branch_angle = { 1.0, 179.0, 71.0, 40.0 }; - - // Correlated child branch radius distribution parameters. - L.diam_child_a = 5.47078; // from `purkburk.prm` - L.diam_child_r = { 0, inf, 0.112, 0.035 }; - - // Termination probability parameters [1/µm]. - L.pterm_k1 = 0.1973; // from `purkburj.prm`; 0.4539 in Ascoli. - L.pterm_k2 = -1.1533; - - // Bifurcation probability parameters [1/μm]. - L.pbranch_ov_k1 = 0.1021; // from `purkburk.prm`; 0.0355 in Ascoli. - L.pbranch_ov_k2 = 0.7237; - L.pbranch_nov_k1 = 0.1021; // from `purkburk.prm`; 0.2349 in Ascoli. - L.pbranch_nov_k2 = -0.0116; - - // Absolute maximum dendritic extent [µm]. - L.max_extent = 20000; - - return L; -} - -lsys_param purkinje_lsys = make_purkinje_lsys(); diff --git a/lmorpho/lsys_models.hpp b/lmorpho/lsys_models.hpp deleted file mode 100644 index 96638e5ad0322cd830b4bf283a15fee152650592..0000000000000000000000000000000000000000 --- a/lmorpho/lsys_models.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "lsystem.hpp" - -// Predefined parameters for two classes of neurons. -// See lsys_models.cc for details. - -extern lsys_param alpha_motoneuron_lsys; -extern lsys_param purkinje_lsys; diff --git a/lmorpho/lsystem.cpp b/lmorpho/lsystem.cpp deleted file mode 100644 index ad3cf88ba0c359e5513dfe7fc00934982c71a8c2..0000000000000000000000000000000000000000 --- a/lmorpho/lsystem.cpp +++ /dev/null @@ -1,316 +0,0 @@ -#include <cmath> -#include <iostream> -#include <random> -#include <stack> -#include <vector> - -#include <arbor/morphology.hpp> -#include <arbor/math.hpp> - -#include "lsystem.hpp" - -using namespace arb::math; - -using arb::section_geometry; -using arb::section_point; -using arb::morphology; - -// L-system implementation. - -// Random distribution used in the specification of L-system parameters. -// It can be a constant, uniform over an interval, or truncated normal. -// Note that mixture distributions of the above should be, but isn't yet, -// implemented. - -struct lsys_distribution { - using result_type = double; - using param_type = lsys_distribution_param; - - lsys_distribution(const param_type& p): p_(p) { init(); } - - lsys_distribution(): p_() { init(); } - lsys_distribution(result_type x): p_(x) { init(); } - lsys_distribution(result_type lb, result_type ub): p_(lb, ub) { init(); } - lsys_distribution(result_type lb, result_type ub, result_type mu, result_type sigma): - p_(lb, ub, mu, sigma) - { - init(); - } - - param_type param() const { return p_; } - void param(const param_type& pbis) { p_=pbis; } - - result_type min() const { return p_.lb; } - result_type max() const { - return p_.kind==param_type::constant? min(): p_.ub; - } - - bool operator==(const lsys_distribution& y) const { - return p_==y.p_; - } - - bool operator!=(const lsys_distribution& y) const { - return p_!=y.p_; - } - - // omitting out ostream, istream methods - // (...) - - template <typename Gen> - result_type operator()(Gen& g, const param_type &p) const { - lsys_distribution dist(p); - return dist(g); - } - - template <typename Gen> - result_type operator()(Gen& g) const { - switch (p_.kind) { - case param_type::constant: - return p_.lb; - case param_type::uniform: - return uniform_dist(g); - case param_type::normal: - // TODO: replace with a robust truncated normal generator! - for (;;) { - result_type r = normal_dist(g); - if (r>=p_.lb && r<=p_.ub) return r; - } - } - return 0; - } - -private: - param_type p_; - mutable std::uniform_real_distribution<double> uniform_dist; - mutable std::normal_distribution<double> normal_dist; - - void init() { - switch (p_.kind) { - case param_type::uniform: - uniform_dist = std::uniform_real_distribution<double>(p_.lb, p_.ub); - break; - case param_type::normal: - normal_dist = std::normal_distribution<result_type>(p_.mu, p_.sigma); - break; - default: ; - } - } -}; - -class burke_correlated_r { - double a_; - lsys_distribution r_; - -public: - struct result_type { double rho1, rho2; }; - - burke_correlated_r(double a, lsys_distribution_param r): a_(a), r_(r) {} - - template <typename Gen> - result_type operator()(Gen& g) const { - double r1 = r_(g); - double r2 = r_(g); - return { r1+a_*r2, r2+a_*r1 }; - } -}; - -class burke_exp_test { - double k1_; - double k2_; - -public: - using result_type = int; - - burke_exp_test(double k1, double k2): k1_(k1), k2_(k2) {} - - template <typename Gen> - result_type operator()(double delta_l, double radius, Gen& g) const { - std::bernoulli_distribution B(delta_l*k1_*std::exp(k2_*radius*2.0)); - return B(g); - } -}; - -class burke_exp2_test { - double ak1_; - double ak2_; - double bk1_; - double bk2_; - -public: - using result_type = int; - - burke_exp2_test(double ak1, double ak2, double bk1, double bk2): - ak1_(ak1), ak2_(ak2), bk1_(bk1), bk2_(bk2) {} - - template <typename Gen> - result_type operator()(double delta_l, double radius, Gen& g) const { - double diam = 2.*radius; - std::bernoulli_distribution B( - delta_l*std::min(ak1_*std::exp(ak2_*diam), bk1_*std::exp(bk2_*diam))); - - return B(g); - } -}; - -// L-system parameter set with instantiated distributions. -struct lsys_sampler { - // Create distributions once from supplied parameter set. - explicit lsys_sampler(const lsys_param& P): - diam_soma(P.diam_soma), - n_tree(P.n_tree), - diam_initial(P.diam_initial), - length_step(P.length_step), - roll_initial(P.roll_initial), - pitch_initial(P.pitch_initial), - roll_section(P.roll_section), - pitch_section(P.pitch_section), - taper(P.taper), - roll_at_branch(P.roll_at_branch), - branch_angle(P.branch_angle), - diam_child(P.diam_child_a, P.diam_child_r), - term_test(P.pterm_k1, P.pterm_k2), - branch_test(P.pbranch_ov_k1, P.pbranch_ov_k2, - P.pbranch_nov_k1, P.pbranch_nov_k2), - max_extent(P.max_extent) - {} - - lsys_distribution diam_soma; - lsys_distribution n_tree; - lsys_distribution diam_initial; - lsys_distribution length_step; - lsys_distribution roll_initial; - lsys_distribution pitch_initial; - lsys_distribution roll_section; - lsys_distribution pitch_section; - lsys_distribution taper; - lsys_distribution roll_at_branch; - lsys_distribution branch_angle; - burke_correlated_r diam_child; - burke_exp_test term_test; - burke_exp2_test branch_test; - double max_extent; -}; - -struct section_tip { - section_point point = {0., 0., 0., 0.}; - quaternion rotation = {1., 0., 0., 0.}; - double somatic_distance = 0.; -}; - -struct grow_result { - std::vector<section_point> points; - std::vector<section_tip> children; - double length = 0.; -}; - -constexpr double deg_to_rad = 3.1415926535897932384626433832795/180.0; - -template <typename Gen> -grow_result grow(section_tip tip, const lsys_sampler& S, Gen &g) { - constexpr quaternion xaxis = {0, 1, 0, 0}; - static std::uniform_real_distribution<double> U; - - grow_result result; - std::vector<section_point>& points = result.points; - - points.push_back(tip.point); - for (;;) { - quaternion step = xaxis^tip.rotation; - double dl = S.length_step(g); - tip.point.x += dl*step.x; - tip.point.y += dl*step.y; - tip.point.z += dl*step.z; - tip.point.r += dl*0.5*S.taper(g); - tip.somatic_distance += dl; - result.length += dl; - - if (tip.point.r<0) tip.point.r = 0; - - double phi = S.roll_section(g); - double theta = S.pitch_section(g); - tip.rotation *= rotation_x(deg_to_rad*phi); - tip.rotation *= rotation_y(deg_to_rad*theta); - - points.push_back(tip.point); - - if (tip.point.r==0 || tip.somatic_distance>=S.max_extent) { - return result; - } - - if (S.branch_test(dl, tip.point.r, g)) { - auto r = S.diam_child(g); - // ignore branch if we get non-positive radii - if (r.rho1>0 && r.rho2>0) { - double branch_phi = S.roll_at_branch(g); - double branch_angle = S.branch_angle(g); - double branch_theta1 = U(g)*branch_angle; - double branch_theta2 = branch_theta1-branch_angle; - - tip.rotation *= rotation_x(deg_to_rad*branch_phi); - - section_tip t1 = tip; - t1.point.r = t1.point.r * r.rho1; - t1.rotation *= rotation_y(deg_to_rad*branch_theta1); - - section_tip t2 = tip; - t2.point.r = t2.point.r * r.rho2; - t2.rotation *= rotation_y(deg_to_rad*branch_theta2); - - result.children = {t1, t2}; - return result; - } - } - - if (S.term_test(dl, tip.point.r, g)) { - return result; - } - } -} - -arb::morphology generate_morphology(const lsys_param& P, lsys_generator &g) { - constexpr quaternion xaxis = {0, 1, 0, 0}; - arb::morphology morph; - - lsys_sampler S(P); - double soma_radius = 0.5*S.diam_soma(g); - morph.soma = {0, 0, 0, soma_radius}; - - struct section_start { - section_tip tip; - unsigned parent_id; - }; - std::stack<section_start> starts; - unsigned next_id = 1u; - - int n = (int)std::round(S.n_tree(g)); - for (int i=0; i<n; ++i) { - double phi = S.roll_initial(g); - double theta = S.pitch_initial(g); - double radius = 0.5*S.diam_initial(g); - - section_tip tip; - tip.rotation = rotation_x(deg_to_rad*phi)*rotation_y(deg_to_rad*theta); - tip.somatic_distance = 0.0; - - auto p = (soma_radius*xaxis)^tip.rotation; - tip.point = {p.x, p.y, p.z, radius}; - - starts.push({tip, 0u}); - } - - while (!starts.empty() && morph.sections.size()<P.max_sections) { - auto start = starts.top(); - starts.pop(); - - auto branch = grow(start.tip, S, g); - section_geometry section{next_id++, start.parent_id, branch.children.empty(), std::move(branch.points), branch.length}; - - for (auto child: branch.children) { - starts.push({child, section.id}); - } - morph.sections.push_back(std::move(section)); - } - - return morph; -} - diff --git a/lmorpho/lsystem.hpp b/lmorpho/lsystem.hpp deleted file mode 100644 index 55e5beda31a9b909a9db6aec58765d179e97c45e..0000000000000000000000000000000000000000 --- a/lmorpho/lsystem.hpp +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -#include <random> - -#include <arbor/morphology.hpp> - -struct lsys_param; - -using lsys_generator = std::minstd_rand; - -arb::morphology generate_morphology(const lsys_param& P, lsys_generator& g); - -// The distribution parameters used in the specification of the L-system parameters. -// The distribution can be a constant, uniform over an interval, or truncated normal. -// Note that mixture distributions of the above should be, but isn't yet, -// implemented. - -struct lsys_distribution_param { - using result_type = double; - enum lsys_kind { - constant, uniform, normal - }; - - lsys_kind kind; - result_type lb; // lower bound - result_type ub; // upper bound (used for only non-constant) - result_type mu; // mean (used only for truncated Gaussian) - result_type sigma; // s.d. (used only for truncated Gaussian) - - bool operator==(const lsys_distribution_param &p) const { - if (kind!=p.kind) return false; - switch (kind) { - case constant: - return lb == p.lb; - case uniform: - return lb == p.lb && ub == p.ub; - case normal: - return lb == p.lb && ub == p.ub && mu == p.mu && sigma == p.sigma; - } - return false; - } - - bool operator!=(const lsys_distribution_param &p) const { - return !(*this==p); - } - - // zero or one parameter => constant - lsys_distribution_param(): - kind(constant), lb(0.0) {} - - lsys_distribution_param(result_type x): - kind(constant), lb(x) {} - - // two paramter => uniform - lsys_distribution_param(result_type lb, result_type ub): - kind(uniform), lb(lb), ub(ub) {} - - // four paramter => truncated normal - lsys_distribution_param(result_type lb, result_type ub, result_type mu, result_type sigma): - kind(normal), lb(lb), ub(ub), mu(mu), sigma(sigma) {} -}; - - -// Parameters as referenced in Ascoli 2001 are given in parentheses below. -// Defaults below are chosen to be safe, boring and not biological. -struct lsys_param { - // Soma diameter [µm]. - lsys_distribution_param diam_soma = { 20 }; - - // Number of dendritic trees (rounded to nearest int). (Ntree) - lsys_distribution_param n_tree = { 1 }; - - // Initial dendrite diameter [µm]. (Dstem) - lsys_distribution_param diam_initial = { 2 }; - - // Dendrite step length [µm]. (ΔL) - lsys_distribution_param length_step = { 25 }; - - // Dendrites grow along x-axis after application of (instrinsic) rotations; - // roll (about x-axis) is applied first, followed by pitch (about y-axis). - - // Initial roll (intrinsic rotation about x-axis) [degrees]. (Taz) - lsys_distribution_param roll_initial = { 0 }; - - // Initial pitch (intrinsic rotation about y-axis) [degrees]. (Tel) - lsys_distribution_param pitch_initial = { 0 }; - - // Tortuousness: roll within section over ΔL [degrees]. (Eaz) - lsys_distribution_param roll_section = { 0 }; - - // Tortuousness: pitch within section over ΔL [degrees]. (Eel) - lsys_distribution_param pitch_section = { 0 }; - - // Taper rate: diameter decrease per unit length. (TPRB) - lsys_distribution_param taper = { 0 }; - - // Branching torsion: roll at branch point [degrees]. (Btor) - lsys_distribution_param roll_at_branch = { 0 }; - - // Branch angle between siblings [degrees]. (Bamp) - lsys_distribution_param branch_angle = { 45 }; - - // Child branch diameter ratios are typically anticorrelated. - // The Burke 1992 parameterization describes the two child ratios - // as d1 = r1 + a*r2 and d2 = r2 + a*r1 where a is a constant - // determined by the correlation, and r1 and r2 are drawn from - // a common distribution (`d_child_r` below). - double diam_child_a = 0; - lsys_distribution_param diam_child_r = { 0.5 }; - - // Bifurcation and termination probabilities per unit-length below are given by - // P = k1 * exp(k2*diameter), described by the paremeters k1 and k2 [1/µm]. - - // Termination probability parameters [1/µm]. (k1trm, k2trm) - double pterm_k1 = 0.05; - double pterm_k2 = -2; - - // Bifurcation probability is given by minimum of two probabilities, `pbranch_ov' and - // `pbranch_nov' below. - double pbranch_ov_k1 = 0.01; - double pbranch_ov_k2 = 0; - double pbranch_nov_k1 = 0.01; - double pbranch_nov_k2 = 0; - - // Absolute maximum dendritic extent [µm]. (Forces termination of algorithm) - double max_extent = 2000; - - // Absolute maximum number of unbranched sections. (Forces termination of algorithm) - unsigned max_sections = 10000; -}; - diff --git a/lmorpho/morphio.cpp b/lmorpho/morphio.cpp deleted file mode 100644 index e556f172c54e4c7d2654a7374e9b8ac970504009..0000000000000000000000000000000000000000 --- a/lmorpho/morphio.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include <fstream> -#include <iomanip> -#include <iterator> -#include <map> -#include <ostream> -#include <sstream> -#include <string> -#include <vector> - -#include <arbor/morphology.hpp> -#include <arbor/swcio.hpp> - -#include "morphio.hpp" - -using arb::swc_record; - -std::vector<swc_record> as_swc(const arb::morphology& morph); - -// Multi-file manager implementation. -multi_file::multi_file(const std::string& pattern, int digits) { - auto npos = std::string::npos; - - file_.exceptions(std::ofstream::failbit); - concat_ = (pattern.find("%")==npos); - use_stdout_ = pattern.empty() || pattern=="-"; - - if (!concat_) { - auto p = pattern.find("%"); - fmt_prefix_ = pattern.substr(0, p); - fmt_suffix_ = pattern.substr(p+1); - fmt_digits_ = digits; - } - else { - filename_ = pattern; - } -} - -void multi_file::open(unsigned n) { - if (use_stdout_ || (file_.is_open() && (concat_ || n==current_n_))) { - return; - } - - if (file_.is_open()) file_.close(); - - std::string fname; - if (concat_) { - fname = filename_; - } - else { - std::stringstream ss; - ss << fmt_prefix_ << std::setfill('0') << std::setw(fmt_digits_) << n << fmt_suffix_; - fname = ss.str(); - } - - file_.open(fname); - - current_n_ = n; -} - -static std::string short_cable_message(int id, unsigned sz) { - std::stringstream ss; - ss << "surprisingly short cable: id=" << id << ", size=" << sz; - return ss.str(); -} - -// SWC transform - -// TODO: Move this functionality to arbor library. -std::vector<swc_record> as_swc(const arb::morphology& morph) { - using kind = swc_record::kind; - std::map<int, int> parent_end_id; - std::vector<swc_record> swc; - - // soma - const auto &p = morph.soma; - int id = 0; - parent_end_id[0] = 0; - swc.emplace_back(kind::soma, id, p.x, p.y, p.z, p.r, -1); - - // dendrites: - for (auto& sec: morph.sections) { - int parent = parent_end_id[sec.parent_id]; - - const auto& points = sec.points; - auto n = points.size(); - if (n<2) { - throw std::runtime_error(short_cable_message(sec.id, n)); - } - - // Include first point only for dendrites segments attached to soma. - if (sec.parent_id==0) { - const auto& p = points[0]; - ++id; - swc.emplace_back(kind::fork_point, id, p.x, p.y, p.z, p.r, parent); - parent = id; - } - - // Interior points. - for (unsigned i = 1; i<n-1; ++i) { - const auto& p = points[i]; - ++id; - swc.emplace_back(kind::dendrite, id, p.x, p.y, p.z, p.r, parent); - parent = id; - } - - // Final point (fork or terminal). - const auto& p = points.back(); - ++id; - swc.emplace_back(sec.terminal? kind::end_point: kind::fork_point, id, p.x, p.y, p.z, p.r, parent); - parent_end_id[sec.id] = id; - } - - return swc; -} - -// SWC emitter implementation. - -void swc_emitter::operator()(unsigned index, const arb::morphology& m) { - file_.open(index); - auto& stream = file_.stream(); - - auto swc = as_swc(m); - stream << "# lmorpho generated morphology\n# index: " << index << "\n"; - std::copy(swc.begin(), swc.end(), std::ostream_iterator<swc_record>(stream, "\n")); -} - -// pvector emitter implementation. - -std::vector<int> as_pvector(const arb::morphology& morph, unsigned offset) { - std::map<int, unsigned> parent_index; // section id to segment index - std::vector<int> pvec; - unsigned index = offset; // starting segment index - - // soma - parent_index[0] = index; - pvec.push_back(-1); - ++index; - - // dendrites: - for (auto& sec: morph.sections) { - int parent = parent_index[sec.parent_id]; - - auto n = sec.points.size(); - if (n<2) { - throw std::runtime_error(short_cable_message(sec.id, n)); - } - - for (unsigned i = 1; i<n; ++i) { - pvec.push_back(parent); - parent = index++; - } - - parent_index[sec.id] = parent; - } - - return pvec; -} - -void pvector_emitter::operator()(unsigned index, const arb::morphology& m) { - auto pvec = as_pvector(m, offset_); - if (coalesce_) offset_ += pvec.size(); - - file_.open(index); - auto& stream = file_.stream(); - std::copy(pvec.begin(), pvec.end(), std::ostream_iterator<int>(stream, "\n")); -} - diff --git a/lmorpho/morphio.hpp b/lmorpho/morphio.hpp deleted file mode 100644 index 23a2854bead20fa2f796c4b1b340abcf9fd62dcc..0000000000000000000000000000000000000000 --- a/lmorpho/morphio.hpp +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -#include <fstream> -#include <iostream> -#include <vector> - -#include <arbor/morphology.hpp> - -// Manage access to a single file, std::cout, or an indexed -// sequence of files. - -class multi_file { -private: - std::ofstream file_; - bool concat_ = false; - bool use_stdout_ = false; - // use if not concat_: - std::string fmt_prefix_; - std::string fmt_suffix_; - int fmt_digits_ = 0; - // use if concat_: - std::string filename_; // use if concat_ - unsigned current_n_ = 0; - -public: - // If pattern is '-' or empty, represent std::cout. - // If pattern contains '%', use a different file for each index. - // Otherwise, use a single file given by pattern. - explicit multi_file(const std::string& pattern, int digits=0); - - multi_file(multi_file&&) = default; - - // Open file for index n, closing previously open file if different. - void open(unsigned n); - - // Close currently open file. - void close() { if (!use_stdout_ && file_.is_open()) file_.close(); } - - // True if writing to a single file or std::cout. - bool single_file() const { return concat_; } - - // Return std::cout or file stream as appropriate. - std::ostream& stream() { return use_stdout_? std::cout: file_; } -}; - -static constexpr unsigned digits(unsigned n) { - return n? 1+digits(n/10): 0; -} - -// Write a sequence of morphologies to one or more files -// as given my `pattern`. - -class swc_emitter { - multi_file file_; - -public: - // `pattern` is as for `multi_file`; number `n` optionally - // specifies the total number of morphologies, for better - // numeric formatting. - explicit swc_emitter(std::string pattern, unsigned n=0): - file_(pattern, digits(n)) {} - - swc_emitter(swc_emitter&&) = default; - - // write `index`th morphology as SWC. - void operator()(unsigned index, const arb::morphology& m); - - void close() { file_.close(); } - ~swc_emitter() { close(); } -}; - -// Write pvectors for a sequence of morphologies to one or more files -// as given my `pattern`. Coalesce the vectors if writing to a single -// file/stream. - -class pvector_emitter { - multi_file file_; - bool coalesce_ = false; - unsigned offset_ = 0; - -public: - // `pattern` is as for `multi_file`; number `n` optionally - // specifies the total number of morphologies, for better - // numeric formatting. - explicit pvector_emitter(std::string pattern, unsigned n=0): - file_(pattern, digits(n)), - coalesce_(file_.single_file()) {} - - pvector_emitter(pvector_emitter&&) = default; - - // write pvector for `index`th morphology. - void operator()(unsigned index, const arb::morphology& m); - - void close() { file_.close(); } - ~pvector_emitter() { close(); } -}; - diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 2fb9912b8db2bb04f4eb0af11e7ebb4271e390a1..1e042fbb902b89aa858127b349cbbd75a4208fa0 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -98,6 +98,7 @@ set(unit_sources test_multi_event_stream.cpp test_optional.cpp test_mechinfo.cpp + test_morphology.cpp test_padded.cpp test_partition.cpp test_partition_by_constraint.cpp diff --git a/test/unit/test_counter.cpp b/test/unit/test_counter.cpp index f58c67cefc6b7cbf5afdc0000ac6894705c23d96..83b6dc2eff602edd8087538bf9dd43ffb857cc06 100644 --- a/test/unit/test_counter.cpp +++ b/test/unit/test_counter.cpp @@ -3,7 +3,7 @@ #include <iterator> #include <type_traits> -#include <util/counter.hpp> +#include "util/counter.hpp" using namespace arb; diff --git a/test/unit/test_morphology.cpp b/test/unit/test_morphology.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f0af3a4a80e78121496013f73f6009e892f0e90b --- /dev/null +++ b/test/unit/test_morphology.cpp @@ -0,0 +1,561 @@ +/* + * Unit tests for sample_tree and morphology. + */ + +#include <fstream> +#include <cassert> +#include <cmath> +#include <stdexcept> +#include <string> +#include <vector> + +#include "../test/gtest.h" + +#include <arbor/math.hpp> +#include <arbor/morph/error.hpp> +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/sample_tree.hpp> +#include <arbor/cable_cell.hpp> + +#include "algorithms.hpp" +#include "io/sepval.hpp" +#include "util/range.hpp" +#include "util/span.hpp" +#include "util/strprintf.hpp" + +// Forward declare non-public functions that are used internally to build +// morphologies so that they can be tested. +namespace arb { namespace impl { + std::vector<mbranch> branches_from_parent_index(const std::vector<arb::msize_t>&, const std::vector<point_prop>&, bool); +}} + +template <typename T> +std::ostream& operator<<(std::ostream& o, const std::vector<T>& v) { + return o << "[" << arb::io::csv(v) << "]"; +} + +TEST(morphology, point_props) { + arb::point_prop p = arb::point_prop_mask_none; + + EXPECT_FALSE(arb::is_terminal(p)); + EXPECT_FALSE(arb::is_fork(p)); + EXPECT_FALSE(arb::is_root(p)); + EXPECT_FALSE(arb::is_collocated(p)); + + arb::set_root(p); + EXPECT_FALSE(arb::is_terminal(p)); + EXPECT_FALSE(arb::is_fork(p)); + EXPECT_TRUE(arb::is_root(p)); + EXPECT_FALSE(arb::is_collocated(p)); + + arb::set_terminal(p); + EXPECT_TRUE(arb::is_terminal(p)); + EXPECT_FALSE(arb::is_fork(p)); + EXPECT_TRUE(arb::is_root(p)); + EXPECT_FALSE(arb::is_collocated(p)); + + arb::unset_root(p); + EXPECT_TRUE(arb::is_terminal(p)); + EXPECT_FALSE(arb::is_fork(p)); + EXPECT_FALSE(arb::is_root(p)); + EXPECT_FALSE(arb::is_collocated(p)); + + arb::set_collocated(p); + EXPECT_TRUE(arb::is_terminal(p)); + EXPECT_FALSE(arb::is_fork(p)); + EXPECT_FALSE(arb::is_root(p)); + EXPECT_TRUE(arb::is_collocated(p)); + + arb::set_fork(p); + EXPECT_TRUE(arb::is_terminal(p)); + EXPECT_TRUE(arb::is_fork(p)); + EXPECT_FALSE(arb::is_root(p)); + EXPECT_TRUE(arb::is_collocated(p)); + + arb::unset_fork(p); + arb::unset_terminal(p); + arb::unset_collocated(p); + EXPECT_FALSE(arb::is_terminal(p)); + EXPECT_FALSE(arb::is_fork(p)); + EXPECT_FALSE(arb::is_root(p)); + EXPECT_FALSE(arb::is_collocated(p)); +} + +// TODO: test sample_tree marks properties correctly + +// Test internal function that parses a parent list, and marks +// each node as either root, sequential, fork or terminal. +TEST(sample_tree, properties) { + const auto npos = arb::mnpos; + using arb::sample_tree; + using pp = arb::point_prop; + using pvec = std::vector<arb::msize_t>; + + pp c = arb::point_prop_mask_collocated; + pp r = arb::point_prop_mask_root; + pp t = arb::point_prop_mask_terminal; + pp s = arb::point_prop_mask_none; + pp f = arb::point_prop_mask_fork; + pp tc = t+c; + pp sc = s+c; + pp fc = f+c; + + // make a sample tree from a parent vector with non-collocated points. + auto make_tree = [] (const pvec& parents) { + sample_tree st; + for (auto p: parents) st.append(p, {{0.,0.,double(st.size()),1.}, 1}); + return st; + }; + // make a sample tree from a parent vector with collocated points. + auto make_colloc_tree = [] (const pvec& parents) { + sample_tree st; + for (auto p: parents) st.append(p, {{0.,0.,0.,1.}, 1}); + return st; + }; + + { + EXPECT_EQ(make_tree({npos}).properties(), std::vector<pp>{r}); + EXPECT_EQ(make_colloc_tree({npos}).properties(), std::vector<pp>{r}); + } + + { + EXPECT_EQ(make_tree({npos,0}).properties(), (std::vector<pp>{r,t})); + EXPECT_EQ(make_colloc_tree({npos,0}).properties(), (std::vector<pp>{r,tc})); + } + + { + EXPECT_EQ(make_tree({npos,0,1,2}).properties(), (std::vector<pp>{r,s,s,t})); + EXPECT_EQ(make_colloc_tree({npos,0,1,2}).properties(), (std::vector<pp>{r,sc,sc,tc})); + } + + { + EXPECT_EQ(make_tree({npos,0,1,2,0,4,5}).properties(), (std::vector<pp>{r,s,s,t,s,s,t})); + EXPECT_EQ(make_colloc_tree({npos,0,1,2,0,4,5}).properties(), (std::vector<pp>{r,sc,sc,tc,sc,sc,tc})); + } + + { + EXPECT_EQ(make_tree({npos,0,1,2,3,2,4,4,7}).properties(), (std::vector<pp>{r,s,f,s,f,t,t,s,t})); + EXPECT_EQ(make_colloc_tree({npos,0,1,2,3,2,4,4,7}).properties(), (std::vector<pp>{r,sc,fc,sc,fc,tc,tc,sc,tc})); + } +} + +TEST(morphology, branches_from_parent_index) { + const auto npos = arb::mnpos; + using pvec = std::vector<arb::msize_t>; + using mb = arb::mbranch; + + // make a sample tree from a parent vector with non-collocated points. + auto make_tree = [] (const pvec& parents) { + arb::sample_tree st; + for (auto p: parents) st.append(p, {{0.,0.,double(st.size()),1.}, 1}); + return st; + }; + + { // single sample: can only be used to build a morphology with one spherical branch + pvec parents{npos}; + auto tree = make_tree(parents); + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), true); + EXPECT_EQ(1u, bc.size()); + EXPECT_EQ(mb({0}, npos), bc[0]); + + // A cable morphology can't be constructed from a single sample. + EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), false), + arb::morphology_error); + } + { + pvec parents = {npos, 0}; + auto tree = make_tree(parents); + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false); + EXPECT_EQ(1u, bc.size()); + EXPECT_EQ(mb({0,1}, npos), bc[0]); + + // A morphology can't be constructed with a spherical soma from two samples. + EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), true), + arb::morphology_error); + } + + { + pvec parents{npos, 0, 1}; + + // With cable soma: one cable with 3 samples. + auto tree = make_tree(parents); + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false); + EXPECT_EQ(1u, bc.size()); + EXPECT_EQ(mb({0,1,2},npos), bc[0]); + + // With spherical soma: one sphere and a 2-segment cable. + // The cable branch is attached to the sphere (i.e. the sphere is the parent branch). + auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true); + EXPECT_EQ(2u, bs.size()); + EXPECT_EQ(mb({0},npos), bs[0]); + EXPECT_EQ(mb({1,2},0), bs[1]); + } + + { + pvec parents{npos, 0, 0}; + auto tree = make_tree(parents); + + // A spherical root is not valid: each cable branch would have only one sample. + EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), true), arb::morphology_error); + + // Two cables, with two samples each, with the first sample in each being the root + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false); + EXPECT_EQ(2u, bc.size()); + EXPECT_EQ(mb({0,1},npos), bc[0]); + EXPECT_EQ(mb({0,2},npos), bc[1]); + } + + { + pvec parents{npos, 0, 1, 2}; + auto tree = make_tree(parents); + + // With cable soma: one cable with 4 samples. + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false); + EXPECT_EQ(1u, bc.size()); + EXPECT_EQ(mb({0,1,2,3},npos), bc[0]); + + // With spherical soma: one sphere and on 3-segment cable. + // The cable branch is attached to the sphere (i.e. the sphere is the parent branch). + auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true); + EXPECT_EQ(2u, bs.size()); + EXPECT_EQ(mb({0},npos), bs[0]); + EXPECT_EQ(mb({1,2,3},0), bs[1]); + } + + { + pvec parents{npos, 0, 1, 0}; + auto tree = make_tree(parents); + + // With cable soma: two cables with 3 and 2 samples respectively. + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false); + EXPECT_EQ(2u, bc.size()); + EXPECT_EQ(mb({0,1,2},npos), bc[0]); + EXPECT_EQ(mb({0,3},npos), bc[1]); + + // A spherical root is not valid: the second cable branch would have only one sample. + EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), true), arb::morphology_error); + } + + { + pvec parents{npos, 0, 1, 0, 3}; + auto tree = make_tree(parents); + + // With cable soma: two cables with 3 samples each [0,1,2] and [0,3,4] + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false); + EXPECT_EQ(2u, bc.size()); + EXPECT_EQ(mb({0,1,2},npos), bc[0]); + EXPECT_EQ(mb({0,3,4},npos), bc[1]); + + // With spherical soma: one sphere and 2 2-sample cables. + // each of the cable branches is attached to the sphere (i.e. the sphere is the parent branch). + auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true); + EXPECT_EQ(3u, bs.size()); + EXPECT_EQ(mb({0},npos), bs[0]); + EXPECT_EQ(mb({1,2},0), bs[1]); + EXPECT_EQ(mb({3,4},0), bs[2]); + } + + { + pvec parents{npos, 0, 1, 0, 3, 4, 4, 6}; + auto tree = make_tree(parents); + + // With cable soma: 4 cables: [0,1,2] [0,3,4] [4,5] [4,6,7] + auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false); + EXPECT_EQ(4u, bc.size()); + EXPECT_EQ(mb({0,1,2},npos), bc[0]); + EXPECT_EQ(mb({0,3,4},npos), bc[1]); + EXPECT_EQ(mb({4,5}, 1), bc[2]); + EXPECT_EQ(mb({4,6,7},1), bc[3]); + + // With spherical soma: 1 sphere and 4 cables: [1,2] [3,4] [4,5] [4,6,7] + auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true); + EXPECT_EQ(5u, bs.size()); + EXPECT_EQ(mb({0}, npos), bs[0]); + EXPECT_EQ(mb({1,2}, 0), bs[1]); + EXPECT_EQ(mb({3,4}, 0), bs[2]); + EXPECT_EQ(mb({4,5}, 2), bs[3]); + EXPECT_EQ(mb({4,6,7}, 2), bs[4]); + } +} + +// For different parent index vectors, attempt multiple valid and invalid sample sets. +TEST(morphology, construction) { + constexpr auto npos = arb::mnpos; + using arb::util::make_span; + using ms = arb::msample; + using pvec = std::vector<arb::msize_t>; + { + pvec p = {npos, 0}; + std::vector<ms> s = { + {{0.0, 0.0, 0.0, 1.0}, 1}, + {{0.0, 0.0, 1.0, 1.0}, 1} }; + + arb::sample_tree sm(s, p); + auto m = arb::morphology(sm); + + EXPECT_EQ(1u, m.num_branches()); + } + { + pvec p = {npos, 0, 1}; + { // 2-segment cable (1 seg soma, 1 seg dendrite) + std::vector<ms> s = { + {{0.0, 0.0, 0.0, 5.0}, 1}, + {{0.0, 0.0, 5.0, 1.0}, 1}, + {{0.0, 0.0, 8.0, 1.0}, 2} }; + + arb::sample_tree sm(s, p); + auto m = arb::morphology(sm); + + EXPECT_EQ(1u, m.num_branches()); + } + { // spherical soma and single-segment cable + std::vector<ms> s = { + {{0.0, 0.0, 0.0, 5.0}, 1}, + {{0.0, 0.0, 1.0, 1.0}, 2}, + {{0.0, 0.0, 8.0, 1.0}, 2} }; + + arb::sample_tree sm(s, p); + auto m = arb::morphology(sm); + + EXPECT_EQ(2u, m.num_branches()); + } + } + { + // 0 | + // 1 3 | + // 2 | + pvec p = {npos, 0, 1, 0}; + { + // two cables: 1x2 segments, 1x1 segment. + std::vector<ms> s = { + {{0.0, 0.0, 0.0, 5.0}, 1}, + {{0.0, 0.0, 5.0, 1.0}, 1}, + {{0.0, 0.0, 6.0, 1.0}, 2}, + {{0.0, 4.0, 0.0, 1.0}, 1}}; + + arb::sample_tree sm(s, p); + auto m = arb::morphology(sm); + + EXPECT_EQ(2u, m.num_branches()); + } + { + // error: spherical soma with a single point cable attached via sample 3 + std::vector<ms> s = { + {{0.0, 0.0, 0.0, 5.0}, 1}, + {{0.0, 0.0, 5.0, 1.0}, 2}, + {{0.0, 0.0, 8.0, 1.0}, 2}, + {{0.0, 5.0, 0.0, 1.0}, 2}}; + + arb::sample_tree sm(s, p); + EXPECT_THROW((arb::morphology(sm)), arb::morphology_error); + } + } + { + // 0 | + // 1 3 | + // 2 4 | + pvec p = {npos, 0, 1, 0, 3}; + { + // two cables: 1x2 segments, 1x1 segment. + std::vector<ms> s = { + {{0.0, 0.0, 0.0, 5.0}, 1}, + {{0.0, 0.0, 5.0, 1.0}, 2}, + {{0.0, 0.0, 8.0, 1.0}, 2}, + {{0.0, 5.0, 0.0, 1.0}, 2}, + {{0.0, 8.0, 0.0, 1.0}, 2}}; + + arb::sample_tree sm(s, p); + auto m = arb::morphology(sm); + + EXPECT_EQ(3u, m.num_branches()); + } + } +} + +// test that morphology generates branch child-parent structure correctly. +TEST(morphology, branches) { + using pvec = std::vector<arb::msize_t>; + using svec = std::vector<arb::msample>; + auto npos = arb::mnpos; + { + // 0 + pvec parents = {npos}; + svec samples = { + {{ 0,0,0,3}, 1}, + }; + arb::sample_tree sm(samples, parents); + arb::morphology m(sm); + + EXPECT_EQ(1u, m.num_branches()); + EXPECT_EQ(npos, m.branch_parent(0)); + EXPECT_EQ(pvec{}, m.branch_children(0)); + } + { + // 0 - 1 + pvec parents = {npos, 0}; + svec samples = { + {{ 0,0,0,3}, 1}, + {{ 10,0,0,3}, 1}, + }; + arb::sample_tree sm(samples, parents); + arb::morphology m(sm); + + EXPECT_EQ(1u, m.num_branches()); + EXPECT_EQ(npos, m.branch_parent(0)); + EXPECT_EQ(pvec{}, m.branch_children(0)); + } + { + // 0 - 1 - 2 + pvec parents = {npos, 0, 1}; + { + // All samples have same tag -> the morphology is a single unbranched cable. + svec samples = { + {{ 0,0,0,3}, 1}, + {{10,0,0,3}, 1}, + {{100,0,0,3}, 1}, + }; + arb::sample_tree sm(samples, parents); + arb::morphology m(sm); + + EXPECT_EQ(1u, m.num_branches()); + EXPECT_EQ(npos, m.branch_parent(0)); + EXPECT_EQ(pvec{}, m.branch_children(0)); + } + { + // First sample has unique tag -> spherical soma attached to a single-segment cable. + svec samples = { + {{ 0,0,0,10}, 1}, + {{ 10,0,0, 3}, 3}, + {{100,0,0, 3}, 3}, + }; + arb::sample_tree sm(samples, parents); + arb::morphology m(sm); + + EXPECT_EQ(2u, m.num_branches()); + EXPECT_EQ(npos, m.branch_parent(0)); + EXPECT_EQ(0u, m.branch_parent(1)); + EXPECT_EQ(pvec{1}, m.branch_children(0)); + EXPECT_EQ(pvec{}, m.branch_children(1)); + } + } + { + // 2 - 0 - 1 + pvec parents = {npos, 0, 0}; + + svec samples = { + {{ 0, 0,0, 5}, 3}, + {{ 10, 0,0, 5}, 3}, + {{ 0,10,0, 5}, 3}, + }; + arb::sample_tree sm(samples, parents); + arb::morphology m(sm); + + EXPECT_EQ(2u, m.num_branches()); + EXPECT_EQ(npos, m.branch_parent(0)); + EXPECT_EQ(npos, m.branch_parent(1)); + EXPECT_EQ(pvec{}, m.branch_children(0)); + EXPECT_EQ(pvec{}, m.branch_children(1)); + } + { + // 0 - 1 - 2 - 3 + pvec parents = {npos, 0, 1, 2}; + } + { + // 0 | + // / \ | + // 1 3 | + // / | + // 2 | + pvec parents = {npos, 0, 1, 0}; + } + { + // Eight samples + // + // 0 | + // / \ | + // 1 3 | + // / \ | + // 2 4 | + // / \ | + // 5 6 | + // \ | + // 7 | + pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6}; + { + svec samples = { + {{ 0, 0, 0, 10}, 1}, + {{ 10, 0, 0, 2}, 3}, + {{100, 0, 0, 2}, 3}, + {{ 0, 10, 0, 2}, 3}, + {{ 0,100, 0, 2}, 3}, + {{100,100, 0, 2}, 3}, + {{ 0,200, 0, 2}, 3}, + {{ 0,300, 0, 2}, 3}, + }; + arb::sample_tree sm(samples, parents); + arb::morphology m(sm); + + EXPECT_EQ(5u, m.num_branches()); + EXPECT_EQ(npos, m.branch_parent(0)); + EXPECT_EQ(0u, m.branch_parent(1)); + EXPECT_EQ(0u, m.branch_parent(2)); + EXPECT_EQ(2u, m.branch_parent(3)); + EXPECT_EQ(2u, m.branch_parent(4)); + EXPECT_EQ((pvec{1,2}), m.branch_children(0)); + EXPECT_EQ((pvec{}), m.branch_children(1)); + EXPECT_EQ((pvec{3,4}), m.branch_children(2)); + EXPECT_EQ((pvec{}), m.branch_children(3)); + EXPECT_EQ((pvec{}), m.branch_children(4)); + } + { + svec samples = { + {{ 0, 0, 0, 10}, 3}, + {{ 10, 0, 0, 2}, 3}, + {{100, 0, 0, 2}, 3}, + {{ 0, 10, 0, 2}, 3}, + {{ 0,100, 0, 2}, 3}, + {{100,100, 0, 2}, 3}, + {{ 0,200, 0, 2}, 3}, + {{ 0,300, 0, 2}, 3}, + }; + arb::sample_tree sm(samples, parents); + arb::morphology m(sm); + + EXPECT_EQ(4u, m.num_branches()); + EXPECT_EQ(npos, m.branch_parent(0)); + EXPECT_EQ(npos, m.branch_parent(1)); + EXPECT_EQ(1u, m.branch_parent(2)); + EXPECT_EQ(1u, m.branch_parent(3)); + EXPECT_EQ((pvec{}), m.branch_children(0)); + EXPECT_EQ((pvec{2,3}), m.branch_children(1)); + EXPECT_EQ((pvec{}), m.branch_children(2)); + EXPECT_EQ((pvec{}), m.branch_children(3)); + } + } +} + +TEST(morphology, swc) { + std::string datadir{DATADIR}; + auto fname = datadir + "/example.swc"; + std::ifstream fid(fname); + if (!fid.is_open()) { + std::cerr << "unable to open file " << fname << "... skipping test\n"; + return; + } + + // Load swc samples from file. + auto swc_samples = arb::parse_swc_file(fid); + + // Build a sample_tree from swc samples. + auto sm = arb::swc_as_sample_tree(swc_samples); + EXPECT_EQ(1058u, sm.size()); // file contains 195 samples + + // Test that the morphology contains the expected number of branches. + auto m = arb::morphology(sm); + EXPECT_EQ(31u, m.num_branches()); + + // Confirm that converting to a cable_cell generates the same number of branches. + auto c = arb::make_cable_cell(m, false); + EXPECT_EQ(31u, c.num_segments()); +} + diff --git a/test/unit/test_range.cpp b/test/unit/test_range.cpp index c8c0a82e4747bf7c17013de3dd7a269c6fdbae7d..b15c86abb54523ed7c38a262c822d1d2752c3fad 100644 --- a/test/unit/test_range.cpp +++ b/test/unit/test_range.cpp @@ -9,12 +9,12 @@ #include <type_traits> #include <unordered_map> -#include <util/counter.hpp> -#include <util/meta.hpp> -#include <util/range.hpp> -#include <util/rangeutil.hpp> -#include <util/sentinel.hpp> -#include <util/transform.hpp> +#include "util/counter.hpp" +#include "util/meta.hpp" +#include "util/range.hpp" +#include "util/rangeutil.hpp" +#include "util/sentinel.hpp" +#include "util/transform.hpp" #include "common.hpp" diff --git a/test/unit/test_swcio.cpp b/test/unit/test_swcio.cpp index 3a22d257cc3fdc29f310fc7d31f98e140d9ea7eb..c12fd36cc4c4e61c2e04438c354e3d1128867083 100644 --- a/test/unit/test_swcio.cpp +++ b/test/unit/test_swcio.cpp @@ -8,7 +8,6 @@ #include <vector> #include <arbor/cable_cell.hpp> -#include <arbor/morphology.hpp> #include <arbor/swcio.hpp> #include "../gtest.h" @@ -26,7 +25,7 @@ void expect_record_equals(const swc_record& expected, const swc_record& actual) { EXPECT_EQ(expected.id, actual.id); - EXPECT_EQ(expected.type, actual.type); + EXPECT_EQ(expected.tag, actual.tag); EXPECT_FLOAT_EQ(expected.x, actual.x); EXPECT_FLOAT_EQ(expected.y, actual.y); EXPECT_FLOAT_EQ(expected.z, actual.z); @@ -36,54 +35,44 @@ void expect_record_equals(const swc_record& expected, TEST(swc_record, construction) { - { - // force an invalid type - swc_record::kind invalid_type = static_cast<swc_record::kind>(100); - EXPECT_THROW(swc_record(invalid_type, 7, 1., 1., 1., 1., 5).assert_consistent(), - swc_error); - } + int soma_tag = 1; { // invalid id - EXPECT_THROW(swc_record( - swc_record::kind::custom, -3, 1., 1., 1., 1., 5).assert_consistent(), + EXPECT_THROW(swc_record(soma_tag, -3, 1., 1., 1., 1., 5).assert_consistent(), swc_error); } { // invalid parent id - EXPECT_THROW(swc_record( - swc_record::kind::custom, 0, 1., 1., 1., 1., -5).assert_consistent(), + EXPECT_THROW(swc_record(soma_tag, 0, 1., 1., 1., 1., -5).assert_consistent(), swc_error); } { // invalid radius - EXPECT_THROW(swc_record( - swc_record::kind::custom, 0, 1., 1., 1., -1., -1).assert_consistent(), + EXPECT_THROW(swc_record(soma_tag, 0, 1., 1., 1., -1., -1).assert_consistent(), swc_error); } { // parent_id > id - EXPECT_THROW(swc_record( - swc_record::kind::custom, 0, 1., 1., 1., 1., 2).assert_consistent(), + EXPECT_THROW(swc_record(soma_tag, 0, 1., 1., 1., 1., 2).assert_consistent(), swc_error); } { // parent_id == id - EXPECT_THROW(swc_record( - swc_record::kind::custom, 0, 1., 1., 1., 1., 0).assert_consistent(), + EXPECT_THROW(swc_record(soma_tag, 0, 1., 1., 1., 1., 0).assert_consistent(), swc_error); } { // check standard construction by value - swc_record record(swc_record::kind::custom, 0, 1., 1., 1., 1., -1); + swc_record record(soma_tag, 0, 1., 1., 1., 1., -1); EXPECT_TRUE(record.is_consistent()); EXPECT_EQ(record.id, 0); - EXPECT_EQ(record.type, swc_record::kind::custom); + EXPECT_EQ(record.tag, soma_tag); EXPECT_EQ(record.x, 1.); EXPECT_EQ(record.y, 1.); EXPECT_EQ(record.z, 1.); @@ -94,7 +83,7 @@ TEST(swc_record, construction) { // check copy constructor - swc_record record_orig(swc_record::kind::custom, 0, 1., 1., 1., 1., -1); + swc_record record_orig(soma_tag, 0, 1., 1., 1., 1., -1); swc_record record(record_orig); expect_record_equals(record_orig, record); } @@ -136,13 +125,6 @@ TEST(swc_parser, invalid_input_parse) EXPECT_THROW(parse_swc_file(is), swc_error); } - { - // Check invalid record type - std::istringstream is( - "1 10 14.566132 34.873772 7.857000 0.717830 -1\n"); - EXPECT_THROW(parse_swc_file(is), swc_error); - } - { // Non-contiguous numbering in branches is considered invalid // 1 @@ -210,8 +192,9 @@ TEST(swc_parser, valid_input) is >> record; EXPECT_TRUE(is); + int soma_tag = 1; swc_record record_expected( - swc_record::kind::soma, + soma_tag, 0, 14.566132, 34.873772, 7.857000, 0.717830, -1); expect_record_equals(record_expected, record); } @@ -227,8 +210,9 @@ TEST(swc_parser, valid_input) is >> record; EXPECT_TRUE(is); + int soma_tag = 1; swc_record record_expected( - swc_record::kind::soma, + soma_tag, 0, 14.566132, 34.873772, 7.857000, 0.717830, -1); expect_record_equals(record_expected, record); } @@ -256,7 +240,7 @@ TEST(swc_parser, valid_input) EXPECT_FALSE(is.fail()); EXPECT_FALSE(is.bad()); EXPECT_EQ(0, record.id); // zero-based indexing - EXPECT_EQ(swc_record::kind::soma, record.type); + EXPECT_EQ(1, record.tag); EXPECT_FLOAT_EQ(14.566132, record.x); EXPECT_FLOAT_EQ(34.873772, record.y); EXPECT_FLOAT_EQ( 7.857000, record.z); @@ -265,11 +249,12 @@ TEST(swc_parser, valid_input) } { + // check valid input with a series of records std::vector<swc_record> records_orig = { - swc_record(swc_record::kind::soma, 0, + swc_record(1, 0, 14.566132, 34.873772, 7.857000, 0.717830, -1), - swc_record(swc_record::kind::dendrite, 1, + swc_record(3, 1, 14.566132+1, 34.873772+1, 7.857000+1, 0.717830+1, -1) }; @@ -399,7 +384,7 @@ TEST(swc_parser, raw) std::stringstream is; is << "1 1 14.566132 34.873772 7.857000 0.717830 -1\n"; is << "2 2 14.566132 34.873772 7.857000 0.717830 1\n"; - is << "3 10 14.566132 34.873772 7.857000 0.717830 1\n"; + is << "-3 2 14.566132 34.873772 7.857000 0.717830 1\n"; // invalid sample identifier -3 is << "4 2 14.566132 34.873772 7.857000 0.717830 1\n"; try { @@ -421,6 +406,10 @@ TEST(swc_parser, raw) } } +/*** + * If you have proper unit tests for the steps from + * {sample_tree -> morphology -> cable_cell} + * then these tests can be simplified to check that {swc -> sample_tree} works TEST(swc_io, cell_construction) { using namespace arb; @@ -557,3 +546,4 @@ TEST(swc_parser, from_file_ball_and_stick) { expect_morph_eq(expected, bas_morph); } +*/