diff --git a/lmorpho/CMakeLists.txt b/lmorpho/CMakeLists.txt index 1bb074d891494098c73d140e4b63538e98364cc6..126b93f8f280d888ddb4575a48dcc7a37d7ea316 100644 --- a/lmorpho/CMakeLists.txt +++ b/lmorpho/CMakeLists.txt @@ -1,4 +1,4 @@ -add_executable(lmorpho lmorpho.cpp lsystem.cpp lsys_models.cpp morphology.cpp morphio.cpp) +add_executable(lmorpho lmorpho.cpp lsystem.cpp lsys_models.cpp morphio.cpp) target_link_libraries(lmorpho LINK_PUBLIC nestmc) target_link_libraries(lmorpho LINK_PUBLIC ${EXTERNAL_LIBRARIES}) diff --git a/lmorpho/lsystem.cpp b/lmorpho/lsystem.cpp index 4cf824f9994833f56232ff6fa0df358ab5b040c6..f88ba290190d247ae2eb3e614f5b55f1ff7c936b 100644 --- a/lmorpho/lsystem.cpp +++ b/lmorpho/lsystem.cpp @@ -5,12 +5,16 @@ #include <vector> #include <math.hpp> +#include <morphology.hpp> #include "lsystem.hpp" -#include "morphology.hpp" using namespace nest::mc::math; +using nest::mc::section_geometry; +using nest::mc::section_point; +using nest::mc::morphology; + // L-system implementation. // Random distribution used in the specification of L-system parameters. @@ -263,9 +267,9 @@ grow_result grow(section_tip tip, const lsys_sampler& S, Gen &g) { } } -morphology generate_morphology(const lsys_param& P, lsys_generator &g) { +nest::mc::morphology generate_morphology(const lsys_param& P, lsys_generator &g) { constexpr quaternion xaxis = {0, 1, 0, 0}; - morphology morph; + nest::mc::morphology morph; lsys_sampler S(P); double soma_radius = 0.5*S.diam_soma(g); diff --git a/lmorpho/lsystem.hpp b/lmorpho/lsystem.hpp index f3ce4deb3ca578df970153800da40c4cdd436acc..6568e279d5e859fece0a30e047e1e8d7be566f1a 100644 --- a/lmorpho/lsystem.hpp +++ b/lmorpho/lsystem.hpp @@ -2,13 +2,13 @@ #include <random> -#include "morphology.hpp" +#include <morphology.hpp> struct lsys_param; using lsys_generator = std::minstd_rand; -morphology generate_morphology(const lsys_param& P, lsys_generator& g); +nest::mc::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. diff --git a/lmorpho/morphio.cpp b/lmorpho/morphio.cpp index e6259be54a8d68b12f13f069b337057836c29f76..4c50fb6b77178e3466ae2937dd40b13b18eaaccd 100644 --- a/lmorpho/morphio.cpp +++ b/lmorpho/morphio.cpp @@ -4,13 +4,14 @@ #include <string> #include <vector> +#include <morphology.hpp> #include <swcio.hpp> #include "morphio.hpp" using nest::mc::io::swc_record; -std::vector<swc_record> as_swc(const morphology& morph); +std::vector<swc_record> as_swc(const nest::mc::morphology& morph); // printf wrappers. @@ -78,7 +79,8 @@ void multi_file::open(unsigned n) { using nest::mc::io::swc_record; -std::vector<swc_record> as_swc(const morphology& morph) { +// TODO: Move this functionality to nestmc library. +std::vector<swc_record> as_swc(const nest::mc::morphology& morph) { using kind = swc_record::kind; std::map<int, int> parent_end_id; std::vector<swc_record> swc; @@ -127,7 +129,7 @@ std::vector<swc_record> as_swc(const morphology& morph) { // SWC emitter implementation. -void swc_emitter::operator()(unsigned index, const morphology& m) { +void swc_emitter::operator()(unsigned index, const nest::mc::morphology& m) { file_.open(index); auto& stream = file_.stream(); @@ -138,7 +140,7 @@ void swc_emitter::operator()(unsigned index, const morphology& m) { // pvector emitter implementation. -std::vector<int> as_pvector(const morphology& morph, unsigned offset) { +std::vector<int> as_pvector(const nest::mc::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 @@ -168,7 +170,7 @@ std::vector<int> as_pvector(const morphology& morph, unsigned offset) { return pvec; } -void pvector_emitter::operator()(unsigned index, const morphology& m) { +void pvector_emitter::operator()(unsigned index, const nest::mc::morphology& m) { auto pvec = as_pvector(m, offset_); if (coalesce_) offset_ += pvec.size(); diff --git a/lmorpho/morphio.hpp b/lmorpho/morphio.hpp index 37ec56ae69e06caed6b9bb96078a94f41f20df8a..87a80db2f85488d3c9196fa6b4320c3860827658 100644 --- a/lmorpho/morphio.hpp +++ b/lmorpho/morphio.hpp @@ -4,7 +4,7 @@ #include <iostream> #include <vector> -#include "morphology.hpp" +#include <morphology.hpp> // Manage access to a single file, std::cout, or an indexed // sequence of files. @@ -59,7 +59,7 @@ public: swc_emitter(swc_emitter&&) = default; // write `index`th morphology as SWC. - void operator()(unsigned index, const morphology& m); + void operator()(unsigned index, const nest::mc::morphology& m); void close() { file_.close(); } ~swc_emitter() { close(); } @@ -85,7 +85,7 @@ public: pvector_emitter(pvector_emitter&&) = default; // write pvector for `index`th morphology. - void operator()(unsigned index, const morphology& m); + void operator()(unsigned index, const nest::mc::morphology& m); void close() { file_.close(); } ~pvector_emitter() { close(); } diff --git a/lmorpho/morphology.cpp b/lmorpho/morphology.cpp deleted file mode 100644 index c06c76d873a41c4a97f4f9be8f28101d897c0edc..0000000000000000000000000000000000000000 --- a/lmorpho/morphology.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include <cmath> -#include <vector> - -#include <math.hpp> - -#include "morphology.hpp" - -using nest::mc::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; -} diff --git a/lmorpho/morphology.hpp b/lmorpho/morphology.hpp deleted file mode 100644 index d1adaa6fdf6a3c0966cb827a0f4894d0db8f3b51..0000000000000000000000000000000000000000 --- a/lmorpho/morphology.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -// Representation of 3-d embedded cable morphology. - -#include <vector> - -struct section_point { - double x, y, z, r; // [µm], r is radius. -}; - -struct section_geometry { - unsigned id; - unsigned parent_id; - bool terminal; - std::vector<section_point> points; - double length; // µm - - // re-discretize the section into ceil(length/dx) segments. - void segment(double dx); -}; - -struct morphology { - section_point soma; // origin + spherical radius - std::vector<section_geometry> sections; - - // re-discretize all sections. - void segment(double dx) { - for (auto& s: sections) s.segment(dx); - } -}; - diff --git a/miniapp/miniapp_recipes.cpp b/miniapp/miniapp_recipes.cpp index 742951dff6df83cb13c2f13af6e2d1ace2ae97eb..62d4b48cd1411e30b4589a1e8cab19c5f82f58f5 100644 --- a/miniapp/miniapp_recipes.cpp +++ b/miniapp/miniapp_recipes.cpp @@ -4,6 +4,7 @@ #include <utility> #include <cell.hpp> +#include <morphology.hpp> #include <util/debug.hpp> #include "miniapp_recipes.hpp" @@ -29,9 +30,9 @@ cell make_basic_cell( // add dendrite of length 200 um and diameter 1 um with passive channel std::vector<mc::cable_segment*> dendrites; - dendrites.push_back(cell.add_cable(0, mc::segmentKind::dendrite, 0.5, 0.5, 200)); - dendrites.push_back(cell.add_cable(1, mc::segmentKind::dendrite, 0.5, 0.25,100)); - dendrites.push_back(cell.add_cable(1, mc::segmentKind::dendrite, 0.5, 0.25,100)); + dendrites.push_back(cell.add_cable(0, section_kind::dendrite, 0.5, 0.5, 200)); + dendrites.push_back(cell.add_cable(1, section_kind::dendrite, 0.5, 0.25,100)); + dendrites.push_back(cell.add_cable(1, section_kind::dendrite, 0.5, 0.25,100)); for (auto d : dendrites) { d->add_mechanism(mc::pas_parameters()); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ca4032a3a9b4d161de99d409a90c177da40760b9..b3faee92ef7df76c17935e6806ea7eb8dd2006cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,7 @@ set(HEADERS set(BASE_SOURCES common_types_io.cpp cell.cpp + morphology.cpp parameter_list.cpp profiling/profiler.cpp swcio.cpp diff --git a/src/cell.cpp b/src/cell.cpp index 1828ff83ca6ad2dd6ce7900a18ce4061082c9c22..04766c84a4f80e8b20ca539fee1f0effc08beb20 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -1,4 +1,5 @@ #include <cell.hpp> +#include <morphology.hpp> #include <tree.hpp> #include <util/debug.hpp> @@ -235,5 +236,49 @@ bool cell_basic_equality(cell const& lhs, cell const& rhs) return true; } +// Construct cell from flat morphology specification. + +cell make_cell(const morphology& morph, bool compartments_from_discretization) { + using point3d = cell::point_type; + cell newcell; + + if (!morph) { + return newcell; + } + + EXPECTS(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 std::invalid_argument("no support for complex somata"); + break; + default: ; + } + + std::vector<cell::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)); + } + + auto cable = newcell.add_cable(section.parent_id, kind, radii, points); + if (compartments_from_discretization) { + cable->as_cable()->set_compartments(radii.size()-1); + } + } + + return newcell; +} + } // namespace mc } // namespace nest diff --git a/src/cell.hpp b/src/cell.hpp index 4b9052f2c2698ee6dd66192b98f5cd3c268d18ff..0c2bdd13a095e724062a430ad8ca24a8fdba1c7e 100644 --- a/src/cell.hpp +++ b/src/cell.hpp @@ -7,6 +7,7 @@ #include <common_types.hpp> #include <cell_tree.hpp> +#include <morphology.hpp> #include <segment.hpp> #include <stimulus.hpp> #include <util/debug.hpp> @@ -245,5 +246,11 @@ cable_segment* cell::add_cable(cell::index_type parent, Args&&... args) 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. +cell make_cell(const morphology&, bool compartments_from_discretization=false); + } // namespace mc } // namespace nest diff --git a/src/morphology.cpp b/src/morphology.cpp new file mode 100644 index 0000000000000000000000000000000000000000..740d488386ea6ea407d43c2dd96d7ab26fedd512 --- /dev/null +++ b/src/morphology.cpp @@ -0,0 +1,135 @@ +#include <cmath> +#include <vector> + +#include <math.hpp> + +#include <morphology.hpp> + +namespace nest { +namespace mc { + +using ::nest::mc::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"); + } + sections[section.parent_id].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(true, nsection); + + 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) { + auto parent_index = parent_id-1; + terminal[parent_index] = false; + } + } + + 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 mc +} // namespace nest diff --git a/src/morphology.hpp b/src/morphology.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e57278f28ed89db6249c8ca5b231df71f3f41d65 --- /dev/null +++ b/src/morphology.hpp @@ -0,0 +1,75 @@ +#pragma once + +// Representation of 3-d embedded cable morphology, independent of other +// cell information. + +#include <stdexcept> +#include <vector> + +namespace nest { +namespace mc { + +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; + + // 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(); } + + // 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 mc +} // namespace nest diff --git a/src/segment.hpp b/src/segment.hpp index dc518170485c03b4616462e18132f32782026eb6..53a486c2742b06375687a0eea22dc969dbfc3b71 100644 --- a/src/segment.hpp +++ b/src/segment.hpp @@ -7,6 +7,7 @@ #include <common_types.hpp> #include <compartment.hpp> #include <math.hpp> +#include <morphology.hpp> #include <parameter_list.hpp> #include <point.hpp> #include <util/make_unique.hpp> @@ -21,13 +22,6 @@ struct segment_properties { T cm = 0.01; // capacitance [F/m^2] : 10 nF/mm^2 = 0.01 F/m^2 }; -enum class segmentKind { - soma, - dendrite, - axon, - none -}; - // forward declarations of segment specializations class soma_segment; class cable_segment; @@ -42,23 +36,23 @@ public: // (Yet more motivation for a separate morphology description class!) virtual std::unique_ptr<segment> clone() const = 0; - segmentKind kind() const { + section_kind kind() const { return kind_; } bool is_soma() const { - return kind_==segmentKind::soma; + return kind_==section_kind::soma; } bool is_dendrite() const { - return kind_==segmentKind::dendrite; + return kind_==section_kind::dendrite; } bool is_axon() const { - return kind_==segmentKind::axon; + return kind_==section_kind::axon; } virtual size_type num_compartments() const = 0; @@ -152,7 +146,7 @@ public: } protected: - segmentKind kind_; + section_kind kind_; std::vector<parameter_list> mechanisms_; }; @@ -165,7 +159,7 @@ public: placeholder_segment() { - kind_ = segmentKind::none; + kind_ = section_kind::none; } std::unique_ptr<segment> clone() const override { @@ -209,7 +203,7 @@ public: soma_segment(value_type r): radius_{r} { - kind_ = segmentKind::soma; + kind_ = section_kind::soma; mechanisms_.push_back(membrane_parameters()); } @@ -281,12 +275,12 @@ public: // constructors for a cable with no location information cable_segment( - segmentKind k, + section_kind k, std::vector<value_type> r, std::vector<value_type> lens ) { kind_ = k; - assert(k==segmentKind::dendrite || k==segmentKind::axon); + assert(k==section_kind::dendrite || k==section_kind::axon); radii_ = std::move(r); lengths_ = std::move(lens); @@ -296,7 +290,7 @@ public: } cable_segment( - segmentKind k, + section_kind k, value_type r1, value_type r2, value_type len @@ -307,12 +301,12 @@ public: // constructor that lets the user describe the cable as a // seriew of radii and locations cable_segment( - segmentKind k, + section_kind k, std::vector<value_type> r, std::vector<point_type> p ) { kind_ = k; - assert(k==segmentKind::dendrite || k==segmentKind::axon); + assert(k==section_kind::dendrite || k==section_kind::axon); radii_ = std::move(r); locations_ = std::move(p); @@ -326,7 +320,7 @@ public: // of just the end points of the cable // i.e. describing the cable as a single frustrum cable_segment( - segmentKind k, + section_kind k, value_type r1, value_type r2, point_type const& p1, @@ -463,7 +457,7 @@ using segment_ptr = std::unique_ptr<segment>; /// Helper for constructing segments in a segment_ptr unique pointer wrapper. /// Forwards the supplied arguments to construct a segment of type SegmentType. -/// e.g. auto my_cable = make_segment<cable>(segmentKind::dendrite, ... ); +/// e.g. auto my_cable = make_segment<cable>(section_kind::dendrite, ... ); template <typename SegmentType, typename... Args> segment_ptr make_segment(Args&&... args) { return segment_ptr(new SegmentType(std::forward<Args>(args)...)); diff --git a/src/swcio.cpp b/src/swcio.cpp index 1c5ee0bb0fb1345d627e68f6596cde8459145035..023bbf2880c44f97cee788356c616fc4525d7302 100644 --- a/src/swcio.cpp +++ b/src/swcio.cpp @@ -6,6 +6,8 @@ #include <unordered_set> #include <algorithms.hpp> +#include <cell.hpp> +#include <morphology.hpp> #include <point.hpp> #include <swcio.hpp> #include <util/debug.hpp> @@ -14,314 +16,143 @@ namespace nest { namespace mc { namespace io { -// // swc_record implementation -// -void swc_record::renumber(id_type new_id, std::map<id_type, id_type>& idmap) -{ - auto old_id = id_; - id_ = new_id; - // Obtain parent_id from the map - auto new_parent_id = idmap.find(parent_id_); - if (new_parent_id != idmap.end()) { - parent_id_ = new_parent_id->second; - } - check_consistency(); - idmap.insert(std::make_pair(old_id, new_id)); -} +// 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); -void swc_record::check_consistency() const -{ - // Check record type as well; enum's do not offer complete type safety, - // since you can cast anything that fits to its underlying type - if (static_cast<int>(type_) < 0 || - static_cast<int>(type_) > static_cast<int>(kind::custom)) { - throw std::invalid_argument("unknown record type"); + if (static_cast<int>(r.type) < 0 || static_cast<int>(r.type) > max_type) { + return "unknown record type"; } - if (id_ < 0) { - throw std::invalid_argument("negative ids not allowed"); + if (r.id < 0) { + return "negative ids not allowed"; } - if (parent_id_ < -1) { - throw std::invalid_argument("parent_id < -1 not allowed"); + if (r.parent_id < -1) { + return "parent_id < -1 not allowed"; } - if (parent_id_ >= id_) { - throw std::invalid_argument("parent_id >= id is not allowed"); + if (r.parent_id >= r.id) { + return "parent_id >= id is not allowed"; } - if (r_ < 0) { - throw std::invalid_argument("negative radii are not allowed"); + if (r.r < 0) { + return "negative radii are not allowed"; } -} -std::istream& operator>>(std::istream& is, swc_record& record) -{ - swc_parser parser; - parser.parse_record(is, record); - return is; + return nullptr; } -std::ostream& operator<<(std::ostream& os, const swc_record& record) -{ - // output in one-based indexing - os << record.id_+1 << " " - << static_cast<int>(record.type_) << " " - << std::setprecision(7) << record.x_ << " " - << std::setprecision(7) << record.y_ << " " - << std::setprecision(7) << record.z_ << " " - << std::setprecision(7) << record.r_ << " " - << ((record.parent_id_ == -1) ? record.parent_id_ : record.parent_id_+1); - - return os; -} - - -// -// Utility functions -// - -std::string::size_type find_first_non_whitespace(const std::string& str) -{ - return str.find_first_not_of(" \f\n\r\t\v"); -} - -bool starts_with(const std::string& str, const std::string& prefix) -{ - // ignore leading whitespace - auto pos = find_first_non_whitespace(str); - if (pos == std::string::npos) { - return false; - } - - return str.find(prefix, pos) == pos; -} - -bool is_space(const std::string& str) -{ - return find_first_non_whitespace(str) == std::string::npos; +bool swc_record::is_consistent() const { + return swc_record_error(*this)==nullptr; } -void check_parse_status(const std::istream& is, const swc_parser& parser) -{ - if (is.fail()) { - // If we try to read past the eof; fail bit will also be set - throw swc_parse_error("could not parse value", parser.lineno()); +void swc_record::assert_consistent() const { + const char* error = swc_record_error(*this); + if (error) { + throw swc_error(error); } } -nest::mc::segmentKind convert_kind(const swc_record::kind& kind) -{ - switch (kind) { - case swc_record::kind::soma: - return segmentKind::soma; - case swc_record::kind::dendrite: - return segmentKind::dendrite; - case swc_record::kind::axon: - return segmentKind::axon; - default: - throw swc_parse_error("no known conversion for swc record type", 0); +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); + if (is) { + // Convert to zero-based, leaving parent_id as-is if -1 + --r.id; + if (r.parent_id>=0) { + --r.parent_id; + } + record = r; + return true; } + return false; } - -template<typename T> -T parse_value_strict(std::istream& is, const swc_parser& parser) -{ - T val; - check_parse_status(is >> val, parser); - - // everything's fine - return val; +bool is_comment(const std::string& line) { + auto pos = line.find_first_not_of(" \f\n\r\t\v"); + return pos==std::string::npos || line[pos]=='#'; } -// specialize parsing for record types -template<> -swc_record::kind parse_value_strict(std::istream& is, const swc_parser& parser) -{ - swc_record::id_type val; - check_parse_status(is >> val, parser); - - // Let swc_record's constructor check for the type validity - return static_cast<swc_record::kind>(val); -} +std::istream& operator>>(std::istream& is, swc_record& record) { + std::string line; -// -// swc_parser implementation -// - -std::istream& swc_parser::parse_record(std::istream& is, swc_record& record) -{ - while (!is.eof() && !is.bad()) { - // consume empty and comment lines first - std::getline(is, linebuff_); - - ++lineno_; - if (!is_space(linebuff_) && - !starts_with(linebuff_, comment_prefix_)) { + while (is) { + std::getline(is, line); + if (!is) { break; } - } - if (is.bad()) { - // let the caller check for such events - return is; - } - - if (is.eof() && - (is_space(linebuff_) || starts_with(linebuff_, comment_prefix_))) { - // last line is either empty or a comment; don't parse anything - return is; - } - - if (is.fail()) { - throw swc_parse_error("too long line detected", lineno_); - } + if (is_comment(line)) { + continue; + } - std::istringstream line(linebuff_); - try { - record = parse_record(line); - } catch (std::invalid_argument& e) { - // Rethrow as a parse error - throw swc_parse_error(e.what(), lineno_); + bool ok = parse_record(line, record); + if (!ok) { + is.setstate(std::ios::failbit); + } + break; } - return is; } -swc_record swc_parser::parse_record(std::istringstream& is) -{ - auto id = parse_value_strict<int>(is, *this); - auto type = parse_value_strict<swc_record::kind>(is, *this); - auto x = parse_value_strict<swc_record::coord_type>(is, *this); - auto y = parse_value_strict<swc_record::coord_type>(is, *this); - auto z = parse_value_strict<swc_record::coord_type>(is, *this); - auto r = parse_value_strict<swc_record::coord_type>(is, *this); - auto parent_id = parse_value_strict<swc_record::id_type>(is, *this); - - // Convert to zero-based, leaving parent_id as-is if -1 - if (parent_id != -1) { - parent_id--; - } +std::ostream& operator<<(std::ostream& os, const swc_record& record) { + // output in one-based indexing + os << record.id+1 << " " + << static_cast<int>(record.type) << " " + << std::setprecision(7) << record.x << " " + << std::setprecision(7) << record.y << " " + << std::setprecision(7) << record.z << " " + << std::setprecision(7) << record.r << " " + << ((record.parent_id == -1) ? record.parent_id : record.parent_id+1); - return swc_record(type, id-1, x, y, z, r, parent_id); + return os; } +std::vector<swc_record> parse_swc_file(std::istream& is) { + std::vector<swc_record> records; + std::size_t line_number = 1; + std::string line; -swc_record_range_clean::swc_record_range_clean(std::istream& is) -{ - std::unordered_set<swc_record::id_type> ids; - - std::size_t num_trees = 0; - swc_record::id_type last_id = -1; - bool needsort = false; - - swc_record curr_record; - for (auto r : swc_get_records<swc_io_raw>(is)) { - if (r.parent() == -1 && ++num_trees > 1) { - // only a single tree is allowed - break; - } - - auto inserted = ids.insert(r.id()); - if (inserted.second) { - // not a duplicate; insert record - records_.push_back(r); - if (!needsort && r.id() < last_id) { - needsort = true; + try { + while (is) { + std::getline(is, line); + if (is_comment(line)) { + continue; } - last_id = r.id(); - } - } - - if (needsort) { - std::sort(records_.begin(), records_.end()); - } - - // Renumber records if necessary - std::map<swc_record::id_type, swc_record::id_type> idmap; - swc_record::id_type next_id = 0; - for (auto& r : records_) { - if (r.id() != next_id) { - r.renumber(next_id, idmap); + swc_record record; + bool ok = parse_record(line, record); + if (!ok) { + is.setstate(std::ios::failbit); + } + else { + record.assert_consistent(); + records.push_back(std::move(record)); + } + ++line_number; } - - ++next_id; } - - // Reject if branches are not contiguously numbered - std::vector<swc_record::id_type> parent_list = { 0 }; - for (std::size_t i = 1; i < records_.size(); ++i) { - parent_list.push_back(records_[i].parent()); + catch (swc_error& e) { + e.line_number = line_number; // rethrow with saved line number + throw e; } - if (!nest::mc::algorithms::has_contiguous_compartments(parent_list)) { - throw swc_parse_error("branches are not contiguously numbered", 0); - } -} - -cell swc_read_cell(std::istream& is) -{ - using namespace nest::mc; - - cell newcell; - std::vector<swc_record::id_type> parent_index; - std::vector<swc_record> swc_records; - for (const auto& r : swc_get_records<swc_io_clean>(is)) { - swc_records.push_back(r); - parent_index.push_back(r.parent()); - } - - if (parent_index.empty()) { - return newcell; - } - - // The parent of soma must be 0, while in SWC files is -1 - parent_index[0] = 0; - auto branch_index = algorithms::branches(parent_index); - auto new_parent_index = algorithms::make_parent_index(parent_index, - branch_index); - - // sanity check - EXPECTS(new_parent_index.size() == branch_index.size() - 1); - - // Add the soma first; then the segments - newcell.add_soma(swc_records[0].radius(), swc_records[0].coord()); - for (std::size_t i = 1; i < new_parent_index.size(); ++i) { - auto b_start = std::next(swc_records.begin(), branch_index[i]); - auto b_end = std::next(swc_records.begin(), branch_index[i+1]); - - std::vector<swc_record::coord_type> radii; - std::vector<nest::mc::point<swc_record::coord_type>> points; - if (new_parent_index[i] != 0) { - // include the parent of current record if not branching from soma - auto p = parent_index[branch_index[i]]; - radii.push_back(swc_records[p].radius()); - points.push_back(swc_records[p].coord()); - } - - // extract the radii and the points - std::for_each(b_start, b_end, - [&radii](const swc_record& r) { - radii.push_back(r.radius()); - }); - - std::for_each(b_start, b_end, - [&points](const swc_record& r) { - points.push_back(r.coord()); - }); - - // add the new cable - newcell.add_cable(new_parent_index[i], - convert_kind(b_start->type()), radii, points); + if (!is.eof()) { + // parse error, so throw exception + throw swc_error("SWC parse error", line_number); } - return newcell; + swc_canonicalize_sequence(records); + return records; } } // namespace io diff --git a/src/swcio.hpp b/src/swcio.hpp index 6530cba35190049d58be20dcebdcf9d81570053e..019b26ff384f6ac627cef07576054a09004d234b 100644 --- a/src/swcio.hpp +++ b/src/swcio.hpp @@ -3,11 +3,15 @@ #include <exception> #include <iostream> #include <iterator> +#include <map> #include <string> +#include <unordered_set> #include <vector> -#include "cell.hpp" -#include "point.hpp" +#include <algorithms.hpp> +#include <morphology.hpp> +#include <point.hpp> +#include <util/debug.hpp> namespace nest { namespace mc { @@ -30,384 +34,213 @@ public: custom }; + kind type = kind::undefined; // 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 + // swc records assume zero-based indexing; root's parent remains -1 swc_record(swc_record::kind type, 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) - { - check_consistency(); - } - - swc_record() - : type_(swc_record::kind::undefined) - , id_(0) - , x_(0) - , y_(0) - , z_(0) - , r_(0) - , parent_id_(-1) - { } + int parent_id): + type(type), id(id), x(x), y(y), z(z), r(r), parent_id(parent_id) + {} + swc_record() = default; swc_record(const swc_record& other) = default; swc_record& operator=(const swc_record& other) = default; - bool strict_equals(const swc_record& other) const - { - return id_ == other.id_ && - x_ == other.x_ && - y_ == other.y_ && - z_ == other.z_ && - r_ == other.r_ && - parent_id_ == other.parent_id_; - } - - // Equality and comparison operators - friend bool operator==(const swc_record& lhs, - const swc_record& rhs) - { - return lhs.id_ == rhs.id_; - } - - friend bool operator<(const swc_record& lhs, - const swc_record& rhs) - { - return lhs.id_ < rhs.id_; - } - - friend bool operator<=(const swc_record& lhs, - const swc_record& rhs) - { - return (lhs < rhs) || (lhs == rhs); + bool operator==(const swc_record& other) const { + return id == other.id && + x == other.x && + y == other.y && + z == other.z && + r == other.r && + parent_id == other.parent_id; } - friend bool operator!=(const swc_record& lhs, - const swc_record& rhs) - { + friend bool operator!=(const swc_record& lhs, const swc_record& rhs) { return !(lhs == rhs); } - friend bool operator>(const swc_record& lhs, - const swc_record& rhs) - { - return !(lhs < rhs) && (lhs != rhs); - } - - friend bool operator>=(const swc_record& lhs, - const swc_record& rhs) - { - return !(lhs < rhs); - } - friend std::ostream& operator<<(std::ostream& os, const swc_record& record); - kind type() const - { - return type_; - } - - id_type id() const - { - return id_; - } - - id_type parent() const - { - return parent_id_; + coord_type diameter() const { + return 2*r; } - coord_type x() const - { - return x_; + nest::mc::point<coord_type> coord() const { + return nest::mc::point<coord_type>(x, y, z); } - coord_type y() const - { - return y_; + nest::mc::section_point as_section_point() const { + return nest::mc::section_point{x, y, z, r}; } - coord_type z() const - { - return z_; - } + // validity checks + bool is_consistent() const; + void assert_consistent() const; // throw swc_error if inconsistent. +}; - coord_type radius() const - { - return r_; - } - coord_type diameter() const - { - return 2*r_; - } +class swc_error: public std::runtime_error { +public: + explicit swc_error(const char* msg, std::size_t lineno = 0): + std::runtime_error(msg), line_number(lineno) + {} - nest::mc::point<coord_type> coord() const - { - return nest::mc::point<coord_type>(x_, y_, z_); - } + explicit swc_error(const std::string& msg, std::size_t lineno = 0): + std::runtime_error(msg), line_number(lineno) + {} - void renumber(id_type new_id, std::map<id_type, id_type>& idmap); + std::size_t line_number; +}; -private: - void check_consistency() const; +// Parse one record, skipping comments and blank lines. +std::istream& operator>>(std::istream& is, swc_record& record); - kind type_; // record type - id_type id_; // record id - coord_type x_, y_, z_; // record coordinates - coord_type r_; // record radius - id_type parent_id_; // record parent's id -}; +// Parse and canonicalize an EOF-terminated sequence of records. +// Throw on parsing failure. +std::vector<swc_record> parse_swc_file(std::istream& is); +// Convert a canonical (see below) sequence of SWC records to a morphology object. +template <typename RandomAccessSequence> +morphology swc_as_morphology(const RandomAccessSequence& swc_records) { + morphology morph; -class swc_parse_error : public std::runtime_error -{ -public: - explicit swc_parse_error(const char* msg, std::size_t lineno) - : std::runtime_error(msg) - , lineno_(lineno) - { } - - explicit swc_parse_error(const std::string& msg, std::size_t lineno) - : std::runtime_error(msg) - , lineno_(lineno) - { } - - std::size_t lineno() const - { - return lineno_; + std::vector<swc_record::id_type> swc_parent_index; + for (const auto& r: swc_records) { + swc_parent_index.push_back(r.parent_id); } -private: - std::size_t lineno_; -}; - -class swc_parser { -public: - swc_parser(const std::string& delim, std::string comment_prefix) - : delim_(delim) - , comment_prefix_(comment_prefix) - , lineno_(0) - { } - - swc_parser() - : delim_(" ") - , comment_prefix_("#") - , lineno_(0) - { } - - std::size_t lineno() const - { - return lineno_; + if (swc_parent_index.empty()) { + return morph; } - std::istream& parse_record(std::istream& is, swc_record& record); + // 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::make_parent_index(swc_parent_index, branch_index); -private: - // Read the record from a string stream; will be treated like a single line - swc_record parse_record(std::istringstream& is); + // sanity check + EXPECTS(parent_branch_index.size() == branch_index.size() - 1); - std::string delim_; - std::string comment_prefix_; - std::string linebuff_; - std::size_t lineno_; -}; + // Add the soma first; then the segments + const auto& soma = swc_records[0]; + morph.soma = { soma.x, soma.y, soma.z, soma.r }; + auto n_branches = parent_branch_index.size(); + for (std::size_t i = 1; i < n_branches; ++i) { + auto b_start = std::next(swc_records.begin(), branch_index[i]); + auto b_end = std::next(swc_records.begin(), branch_index[i+1]); -std::istream& operator>>(std::istream& is, swc_record& record); + section_geometry section; + section.id = i; + section.parent_id = parent_branch_index[i]; -class swc_record_stream_iterator : - public std::iterator<std::forward_iterator_tag, swc_record> { -public: - struct eof_tag { }; - - swc_record_stream_iterator(std::istream& is) - : is_(is) - , eof_(false) - { - is_.clear(); - is_.seekg(std::ios_base::beg); - read_next_record(); - } + if (section.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]]]; - swc_record_stream_iterator(std::istream& is, eof_tag) - : is_(is) - , eof_(true) - { } - - swc_record_stream_iterator(const swc_record_stream_iterator& other) - : is_(other.is_) - , parser_(other.parser_) - , curr_record_(other.curr_record_) - , eof_(other.eof_) - { } - - swc_record_stream_iterator& operator++() - { - if (eof_) { - throw std::out_of_range("attempt to read past eof"); + section_point point{parent_record.x, parent_record.y, parent_record.z, parent_record.r}; + section.points.push_back(point); } - read_next_record(); - return *this; - } - - swc_record_stream_iterator operator++(int) - { - swc_record_stream_iterator ret(*this); - operator++(); - return ret; - } - - value_type operator*() const - { - if (eof_) { - throw std::out_of_range("attempt to read past eof"); + for (auto b = b_start; b!=b_end; ++b) { + section_point point{b->x, b->y, b->z, b->r}; + section.points.push_back(point); + + switch (b->type) { + case swc_record::kind::axon: + section.kind = section_kind::axon; + break; + case swc_record::kind::dendrite: + case swc_record::kind::apical_dendrite: + section.kind = section_kind::dendrite; + break; + case swc_record::kind::soma: + section.kind = section_kind::soma; + break; + default: ; // stick with what we have + } } - return curr_record_; + morph.sections.push_back(section); } - bool operator==(const swc_record_stream_iterator& other) const - { - if (eof_ && other.eof_) { - return true; - } else { - return curr_record_.strict_equals(other.curr_record_); - } - } + EXPECTS(morph.check_valid()); + return morph; +} - bool operator!=(const swc_record_stream_iterator& other) const - { - return !(*this == other); - } +// Given a 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 +// are ordered with repect to parent indices. +template <typename RandomAccessSequence> +void swc_canonicalize_sequence(RandomAccessSequence& swc_records) { + std::unordered_set<swc_record::id_type> ids; - friend std::ostream& operator<<(std::ostream& os, - const swc_record_stream_iterator& iter) - { - os << "{ is_.tellg(): " << iter.is_.tellg() << ", " - << "curr_record_: " << iter.curr_record_ << ", " - << "eof_: " << iter.eof_ << "}"; + std::size_t num_trees = 0; + swc_record::id_type last_id = -1; + bool needsort = false; - return os; - } + for (const auto& r: swc_records) { + r.assert_consistent(); -private: - void read_next_record() - { - parser_.parse_record(is_, curr_record_); - if (is_.eof()) { - eof_ = true; + if (r.parent_id == -1 && ++num_trees > 1) { + // only a single tree is allowed + throw swc_error("multiple trees found in SWC record sequence"); + } + if (ids.count(r.id)) { + throw swc_error("records with duplicated ids in SWC record sequence"); } - } - - std::istream& is_; - swc_parser parser_; - swc_record curr_record_; - - // indicator of eof; we need a way to define an end() iterator without - // seeking to the end of file - bool eof_; -}; - -class swc_record_range_raw { -public: - using value_type = swc_record; - using reference = value_type&; - using const_reference = const value_type&; - using iterator = swc_record_stream_iterator; - using const_iterator = const swc_record_stream_iterator; - - swc_record_range_raw(std::istream& is) - : is_(is) - { } - - iterator begin() const - { - return swc_record_stream_iterator(is_); - } + if (!needsort && r.id < last_id) { + needsort = true; + } - iterator end() const - { - iterator::eof_tag eof; - return swc_record_stream_iterator(is_, eof); + last_id = r.id; + ids.insert(r.id); } - bool empty() const - { - return begin() == end(); + if (needsort) { + std::sort(std::begin(swc_records), std::end(swc_records), + [](const swc_record& a, const swc_record& b) { return a.id<b.id; }); } -private: - std::istream& is_; -}; - -// -// Reads records from an input stream until an eof is encountered and returns a -// cleaned sequence of swc records. -// -// For more information check here: -// https://github.com/eth-cscs/cell_algorithms/wiki/SWC-file-parsing -// - -class swc_record_range_clean { -public: - using value_type = swc_record; - using reference = value_type&; - using const_referene = const value_type&; - using iterator = std::vector<swc_record>::iterator; - using const_iterator = std::vector<swc_record>::const_iterator; - - swc_record_range_clean(std::istream& is); + // Renumber records if necessary + std::map<swc_record::id_type, swc_record::id_type> idmap; + swc_record::id_type next_id = 0; + for (auto& r: swc_records) { + if (r.id != next_id) { + auto old_id = r.id; + r.id = next_id; - iterator begin() - { - return records_.begin(); - } + auto new_parent_id = idmap.find(r.parent_id); + if (new_parent_id != idmap.end()) { + r.parent_id = new_parent_id->second; + } - iterator end() - { - return records_.end(); + r.assert_consistent(); + idmap.insert(std::make_pair(old_id, next_id)); + } + ++next_id; } - std::size_t size() - { - return records_.size(); + // Reject if branches are not contiguously numbered + std::vector<swc_record::id_type> parent_list = { 0 }; + for (std::size_t i = 1; i < swc_records.size(); ++i) { + parent_list.push_back(swc_records[i].parent_id); } - bool empty() const - { - return records_.empty(); + if (!nest::mc::algorithms::has_contiguous_compartments(parent_list)) { + throw swc_error("branches are not contiguously numbered", 0); } - -private: - std::vector<swc_record> records_; -}; - -struct swc_io_raw -{ - using record_range_type = swc_record_range_raw; -}; - -struct swc_io_clean -{ - using record_range_type = swc_record_range_clean; -}; - -template<typename T = swc_io_clean> -typename T::record_range_type swc_get_records(std::istream& is) -{ - return typename T::record_range_type(is); } -cell swc_read_cell(std::istream& is); - } // namespace io } // namespace mc } // namespace nest diff --git a/tests/test_common_cells.hpp b/tests/test_common_cells.hpp index 43601ee9956e87ec74c39df990ece68ec6bf6261..e5f1a49bc40147822ad9e22e18c26bb87d63ecbe 100644 --- a/tests/test_common_cells.hpp +++ b/tests/test_common_cells.hpp @@ -63,7 +63,7 @@ inline cell make_cell_ball_and_stick(bool with_stim = true) { auto soma = c.add_soma(12.6157/2.0); soma->add_mechanism(hh_parameters()); - c.add_cable(0, segmentKind::dendrite, 1.0/2, 1.0/2, 200.0); + c.add_cable(0, section_kind::dendrite, 1.0/2, 1.0/2, 200.0); for (auto& seg: c.segments()) { seg->mechanism("membrane").set("r_L", 100); @@ -108,7 +108,7 @@ inline cell make_cell_ball_and_taper(bool with_stim = true) { auto soma = c.add_soma(12.6157/2.0); soma->add_mechanism(hh_parameters()); - c.add_cable(0, segmentKind::dendrite, 1.0/2, 0.4/2, 200.0); + c.add_cable(0, section_kind::dendrite, 1.0/2, 0.4/2, 200.0); for (auto& seg: c.segments()) { seg->mechanism("membrane").set("r_L", 100); @@ -162,7 +162,7 @@ inline cell make_cell_ball_and_squiggle(bool with_stim = true) { }; auto dendrite = - make_segment<cable_segment>(segmentKind::dendrite, radii, points); + make_segment<cable_segment>(section_kind::dendrite, radii, points); c.add_cable(0, std::move(dendrite)); for (auto& seg: c.segments()) { @@ -209,9 +209,9 @@ inline cell make_cell_ball_and_3stick(bool with_stim = true) { auto soma = c.add_soma(12.6157/2.0); soma->add_mechanism(hh_parameters()); - c.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 100); - c.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100); - c.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100); + c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100); + c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100); + c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100); for (auto& seg: c.segments()) { seg->mechanism("membrane").set("r_L", 100); @@ -260,7 +260,7 @@ inline cell make_cell_simple_cable(bool with_stim = true) { cell c; c.add_soma(0); - c.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 1000); + c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 1000); double r_L = 100; double c_m = 0.01; diff --git a/tests/unit/test_cell.cpp b/tests/unit/test_cell.cpp index 143128fd43865df24daca89f9ea4a632e7181c7d..e08c0650b62e3e20383d683bb6cac56ea3307325 100644 --- a/tests/unit/test_cell.cpp +++ b/tests/unit/test_cell.cpp @@ -54,7 +54,7 @@ TEST(cell_type, add_segment) auto seg = make_segment<cable_segment>( - segmentKind::dendrite, + section_kind::dendrite, cable_radius, cable_radius, cable_length ); c.add_cable(0, std::move(seg)); @@ -75,7 +75,7 @@ TEST(cell_type, add_segment) c.add_cable( 0, - segmentKind::dendrite, cable_radius, cable_radius, cable_length + section_kind::dendrite, cable_radius, cable_radius, cable_length ); EXPECT_EQ(c.num_segments(), 2u); @@ -92,7 +92,7 @@ TEST(cell_type, add_segment) c.add_cable( 0, - segmentKind::dendrite, + section_kind::dendrite, std::vector<double>{cable_radius, cable_radius, cable_radius, cable_radius}, std::vector<double>{cable_length, cable_length, cable_length} ); @@ -108,7 +108,7 @@ TEST(cell_type, multiple_cables) // generate a cylindrical cable segment of length 1/pi and radius 1 // volume = 1 // area = 2 - auto seg = [](segmentKind k) { + auto seg = [](section_kind k) { return make_segment<cable_segment>( k, 1.0, 1.0, 1./math::pi<double>() ); }; @@ -132,10 +132,10 @@ TEST(cell_type, multiple_cables) c.add_soma(soma_radius, {0,0,1}); // hook the dendrite and axons - c.add_cable(0, seg(segmentKind::dendrite)); - c.add_cable(0, seg(segmentKind::axon)); - c.add_cable(1, seg(segmentKind::dendrite)); - c.add_cable(1, seg(segmentKind::dendrite)); + c.add_cable(0, seg(section_kind::dendrite)); + c.add_cable(0, seg(section_kind::axon)); + c.add_cable(1, seg(section_kind::dendrite)); + c.add_cable(1, seg(section_kind::dendrite)); EXPECT_EQ(c.num_segments(), 5u); // each of the 5 segments has volume 1 by design @@ -173,9 +173,9 @@ TEST(cell_type, clone) cell c; c.add_soma(2.1); - c.add_cable(0, segmentKind::dendrite, 0.3, 0.2, 10); + c.add_cable(0, section_kind::dendrite, 0.3, 0.2, 10); c.segment(1)->set_compartments(3); - c.add_cable(1, segmentKind::dendrite, 0.2, 0.15, 20); + c.add_cable(1, section_kind::dendrite, 0.2, 0.15, 20); c.segment(2)->set_compartments(5); parameter_list exp_default("expsyn"); @@ -208,7 +208,7 @@ TEST(cell_type, clone) // check clone is independent - c.add_cable(2, segmentKind::dendrite, 0.15, 0.1, 20); + c.add_cable(2, section_kind::dendrite, 0.15, 0.1, 20); EXPECT_NE(c.num_segments(), d.num_segments()); d.detectors()[0].threshold = 13.0; diff --git a/tests/unit/test_fvm_multi.cpp b/tests/unit/test_fvm_multi.cpp index 8c08b29470c1c81d4a3351a3b7d770dedd360572..7eb0606487239d2372a699bf23b95855dec7ebd5 100644 --- a/tests/unit/test_fvm_multi.cpp +++ b/tests/unit/test_fvm_multi.cpp @@ -253,9 +253,9 @@ TEST(fvm_multi, mechanism_indexes) soma->add_mechanism(hh_parameters()); // add dendrite of length 200 um and diameter 1 um with passive channel - c.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 100); - c.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100); - c.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100); + c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100); + c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100); + c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100); auto& segs = c.segments(); segs[1]->add_mechanism(pas_parameters()); diff --git a/tests/unit/test_probe.cpp b/tests/unit/test_probe.cpp index 7ebbefc7adfd0c81de952479cc94d8d2f03d2e88..8027fc2fef77e3dfb194a9312889452066db534c 100644 --- a/tests/unit/test_probe.cpp +++ b/tests/unit/test_probe.cpp @@ -43,7 +43,7 @@ TEST(probe, fvm_multicell) // ball-and-stick model morphology bs.add_soma(12.6157/2.0); - bs.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 200); + bs.add_cable(0, section_kind::dendrite, 0.5, 0.5, 200); bs.soma()->set_compartments(5); segment_location loc0{0, 0}; diff --git a/tests/unit/test_segment.cpp b/tests/unit/test_segment.cpp index d2c52959020945e022f2606933133ea51b6be50f..97f0adc98544bb0a5239ccc0075c76cef69c79bd 100644 --- a/tests/unit/test_segment.cpp +++ b/tests/unit/test_segment.cpp @@ -14,7 +14,7 @@ TEST(segments, soma) EXPECT_EQ(s->volume(), pi<double>()*4./3.); EXPECT_EQ(s->area(), pi<double>()*4.); - EXPECT_EQ(s->kind(), segmentKind::soma); + EXPECT_EQ(s->kind(), section_kind::soma); } { @@ -22,7 +22,7 @@ TEST(segments, soma) EXPECT_EQ(s->volume(), pi<double>()*4./3.); EXPECT_EQ(s->area(), pi<double>()*4.); - EXPECT_EQ(s->kind(), segmentKind::soma); + EXPECT_EQ(s->kind(), section_kind::soma); } } @@ -38,25 +38,25 @@ TEST(segments, cable) // single cylindrical frustrum { - auto s = make_segment<cable_segment>(segmentKind::dendrite, radius, radius, length); + auto s = make_segment<cable_segment>(section_kind::dendrite, radius, radius, length); EXPECT_EQ(s->volume(), 1.0); EXPECT_EQ(s->area(), 2.0); - EXPECT_EQ(s->kind(), segmentKind::dendrite); + EXPECT_EQ(s->kind(), section_kind::dendrite); } // cable made up of three identical cylindrical frustrums { auto s = make_segment<cable_segment>( - segmentKind::axon, + section_kind::axon, std::vector<double>{radius, radius, radius, radius}, std::vector<double>{length, length, length} ); EXPECT_EQ(s->volume(), 3.0); EXPECT_EQ(s->area(), 6.0); - EXPECT_EQ(s->kind(), segmentKind::axon); + EXPECT_EQ(s->kind(), section_kind::axon); } } @@ -70,14 +70,14 @@ TEST(segments, cable_positions) { auto s = make_segment<cable_segment>( - segmentKind::dendrite, + section_kind::dendrite, 1, 2, point<double>(0,0,0), point<double>(0,1,0) ); EXPECT_EQ(s->volume(), math::volume_frustrum(1., 1., 2.)); EXPECT_EQ(s->area(), math::area_frustrum (1., 1., 2.)); - EXPECT_EQ(s->kind(), segmentKind::dendrite); + EXPECT_EQ(s->kind(), section_kind::dendrite); } // cable made up of three frustrums @@ -85,13 +85,13 @@ TEST(segments, cable_positions) { auto s = make_segment<cable_segment>( - segmentKind::axon, + section_kind::axon, std::vector<double>{1, 1.5, 2}, std::vector<point<double>>{ {0,0,0}, {0,0.5,0}, {0,1,0} } ); EXPECT_EQ(s->volume(), math::volume_frustrum(1., 1., 2.)); EXPECT_EQ(s->area(), math::area_frustrum(1., 1., 2.)); - EXPECT_EQ(s->kind(), segmentKind::axon); + EXPECT_EQ(s->kind(), section_kind::axon); } } diff --git a/tests/unit/test_swcio.cpp b/tests/unit/test_swcio.cpp index 99fd5750c09fd3aacd8eb59724ae6c81463e91df..9917bbfe12bfd2f1e11561f667607ae57cb872fa 100644 --- a/tests/unit/test_swcio.cpp +++ b/tests/unit/test_swcio.cpp @@ -1,6 +1,7 @@ #include <array> #include <exception> #include <iostream> +#include <iterator> #include <fstream> #include <numeric> #include <type_traits> @@ -16,17 +17,19 @@ # define DATADIR "../data" #endif +using namespace nest::mc; + // SWC tests -void expect_record_equals(const nest::mc::io::swc_record& expected, - const nest::mc::io::swc_record& actual) +void expect_record_equals(const io::swc_record& expected, + const io::swc_record& actual) { - EXPECT_EQ(expected.id(), actual.id()); - EXPECT_EQ(expected.type(), actual.type()); - EXPECT_FLOAT_EQ(expected.x(), actual.x()); - EXPECT_FLOAT_EQ(expected.y(), actual.y()); - EXPECT_FLOAT_EQ(expected.z(), actual.z()); - EXPECT_FLOAT_EQ(expected.radius(), actual.radius()); - EXPECT_EQ(expected.parent(), actual.parent()); + EXPECT_EQ(expected.id, actual.id); + EXPECT_EQ(expected.type, actual.type); + EXPECT_FLOAT_EQ(expected.x, actual.x); + EXPECT_FLOAT_EQ(expected.y, actual.y); + EXPECT_FLOAT_EQ(expected.z, actual.z); + EXPECT_FLOAT_EQ(expected.r, actual.r); + EXPECT_EQ(expected.parent_id, actual.parent_id); } TEST(swc_record, construction) @@ -36,56 +39,57 @@ TEST(swc_record, construction) { // force an invalid type swc_record::kind invalid_type = static_cast<swc_record::kind>(100); - EXPECT_THROW(swc_record record(invalid_type, 7, 1., 1., 1., 1., 5), - std::invalid_argument); + EXPECT_THROW(swc_record(invalid_type, 7, 1., 1., 1., 1., 5).assert_consistent(), + swc_error); } { // invalid id - EXPECT_THROW(swc_record record( - swc_record::kind::custom, -3, 1., 1., 1., 1., 5), - std::invalid_argument); + EXPECT_THROW(swc_record( + swc_record::kind::custom, -3, 1., 1., 1., 1., 5).assert_consistent(), + swc_error); } { // invalid parent id - EXPECT_THROW(swc_record record( - swc_record::kind::custom, 0, 1., 1., 1., 1., -5), - std::invalid_argument); + EXPECT_THROW(swc_record( + swc_record::kind::custom, 0, 1., 1., 1., 1., -5).assert_consistent(), + swc_error); } { // invalid radius - EXPECT_THROW(swc_record record( - swc_record::kind::custom, 0, 1., 1., 1., -1., -1), - std::invalid_argument); + EXPECT_THROW(swc_record( + swc_record::kind::custom, 0, 1., 1., 1., -1., -1).assert_consistent(), + swc_error); } { // parent_id > id - EXPECT_THROW(swc_record record( - swc_record::kind::custom, 0, 1., 1., 1., 1., 2), - std::invalid_argument); + EXPECT_THROW(swc_record( + swc_record::kind::custom, 0, 1., 1., 1., 1., 2).assert_consistent(), + swc_error); } { // parent_id == id - EXPECT_THROW(swc_record record( - swc_record::kind::custom, 0, 1., 1., 1., 1., 0), - std::invalid_argument); + EXPECT_THROW(swc_record( + swc_record::kind::custom, 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); - EXPECT_EQ(record.id(), 0); - EXPECT_EQ(record.type(), swc_record::kind::custom); - EXPECT_EQ(record.x(), 1.); - EXPECT_EQ(record.y(), 1.); - EXPECT_EQ(record.z(), 1.); - EXPECT_EQ(record.radius(), 1.); + EXPECT_TRUE(record.is_consistent()); + EXPECT_EQ(record.id, 0); + EXPECT_EQ(record.type, swc_record::kind::custom); + EXPECT_EQ(record.x, 1.); + EXPECT_EQ(record.y, 1.); + EXPECT_EQ(record.z, 1.); + EXPECT_EQ(record.r, 1.); EXPECT_EQ(record.diameter(), 2*1.); - EXPECT_EQ(record.parent(), -1); + EXPECT_EQ(record.parent_id, -1); } { @@ -96,47 +100,51 @@ TEST(swc_record, construction) } } -TEST(swc_record, comparison) + +TEST(swc_parser, invalid_input_istream) { using namespace nest::mc::io; { - // check comparison operators - swc_record record0(swc_record::kind::custom, 0, 1., 1., 1., 1., -1); - swc_record record1(swc_record::kind::custom, 0, 2., 3., 4., 5., -1); - swc_record record2(swc_record::kind::custom, 1, 2., 3., 4., 5., -1); - EXPECT_EQ(record0, record1); - EXPECT_LT(record0, record2); - EXPECT_GT(record2, record1); + // check incomplete lines; missing parent + std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830\n"); + swc_record record; + is >> record; + EXPECT_TRUE(is.fail()); } + { + // Check non-parsable values + std::istringstream is( + "1a 1 14.566132 34.873772 7.857000 0.717830 -1\n"); + swc_record record; + is >> record; + EXPECT_TRUE(is.fail()); + } } -TEST(swc_parser, invalid_input) +TEST(swc_parser, invalid_input_parse) { using namespace nest::mc::io; { // check incomplete lines; missing parent std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830\n"); - swc_record record; - EXPECT_THROW(is >> record, swc_parse_error); + EXPECT_THROW(parse_swc_file(is), swc_error); } { // Check non-parsable values std::istringstream is( "1a 1 14.566132 34.873772 7.857000 0.717830 -1\n"); - swc_record record; - EXPECT_THROW(is >> record, swc_parse_error); + 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"); - swc_record record; - EXPECT_THROW(is >> record, swc_parse_error); + EXPECT_THROW(parse_swc_file(is), swc_error); } { @@ -152,20 +160,10 @@ TEST(swc_parser, invalid_input) is << "3 1 14.566132 34.873772 7.857000 0.717830 1\n"; is << "4 1 14.566132 34.873772 7.857000 0.717830 2\n"; - std::vector<swc_record> records; - try { - for (auto c : swc_get_records<swc_io_clean>(is)) { - records.push_back(std::move(c)); - } - - FAIL() << "expected swc_parse_error, none was thrown\n"; - } catch (const swc_parse_error& e) { - SUCCEED(); - } + EXPECT_THROW(parse_swc_file(is), swc_error); } } - TEST(swc_parser, valid_input) { using namespace nest::mc::io; @@ -174,7 +172,11 @@ TEST(swc_parser, valid_input) // check empty file; no record may be parsed swc_record record, record_orig; std::istringstream is(""); - EXPECT_NO_THROW(is >> record); + is >> record; + + EXPECT_TRUE(is.eof()); + EXPECT_TRUE(is.fail()); + EXPECT_FALSE(is.bad()); expect_record_equals(record_orig, record); } @@ -183,15 +185,23 @@ TEST(swc_parser, valid_input) // no record may be parsed swc_record record, record_orig; std::istringstream is("#comment\n#comment"); - EXPECT_NO_THROW(is >> record); + is >> record; + + EXPECT_TRUE(is.eof()); + EXPECT_TRUE(is.fail()); + EXPECT_FALSE(is.bad()); expect_record_equals(record_orig, record); } { // check comment not starting at first character swc_record record, record_orig; - std::istringstream is(" #comment"); - EXPECT_NO_THROW(is >> record); + std::istringstream is(" #comment\n"); + is >> record; + + EXPECT_TRUE(is.eof()); + EXPECT_TRUE(is.fail()); + EXPECT_FALSE(is.bad()); expect_record_equals(record_orig, record); } @@ -203,11 +213,12 @@ TEST(swc_parser, valid_input) is << " \t\n"; is << "1 1 14.566132 34.873772 7.857000 0.717830 -1\n"; - EXPECT_NO_THROW(is >> record); + is >> record; + EXPECT_TRUE(is); + swc_record record_expected( swc_record::kind::soma, 0, 14.566132, 34.873772, 7.857000, 0.717830, -1); - expect_record_equals(record_expected, record); } @@ -219,39 +230,44 @@ TEST(swc_parser, valid_input) is << "\r\n"; is << "1 1 14.566132 34.873772 7.857000 0.717830 -1\r\n"; - EXPECT_NO_THROW(is >> record); + is >> record; + EXPECT_TRUE(is); + swc_record record_expected( swc_record::kind::soma, 0, 14.566132, 34.873772, 7.857000, 0.717830, -1); - expect_record_equals(record_expected, record); } { // check old-style mac eol; these eol are treated as simple whitespace - // characters, so in the following case no parse error shall be thrown - // and no record shall be read - swc_record record, record_expected; + // characters, so should look line a long comment. + swc_record record; std::stringstream is; is << "#comment\r"; is << "1 1 14.566132 34.873772 7.857000 0.717830 -1\r"; - EXPECT_NO_THROW(is >> record); - expect_record_equals(record_expected, record); + is >> record; + EXPECT_TRUE(is.eof()); + EXPECT_TRUE(is.fail()); + EXPECT_FALSE(is.bad()); } { // check last line case (no newline at the end) std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830 -1"); swc_record record; - EXPECT_NO_THROW(is >> record); - EXPECT_EQ(0, record.id()); // zero-based indexing - EXPECT_EQ(swc_record::kind::soma, record.type()); - EXPECT_FLOAT_EQ(14.566132, record.x()); - EXPECT_FLOAT_EQ(34.873772, record.y()); - EXPECT_FLOAT_EQ( 7.857000, record.z()); - EXPECT_FLOAT_EQ( 0.717830, record.radius()); - EXPECT_FLOAT_EQ( -1, record.parent()); + is >> record; + EXPECT_TRUE(is.eof()); + 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_FLOAT_EQ(14.566132, record.x); + EXPECT_FLOAT_EQ(34.873772, record.y); + EXPECT_FLOAT_EQ( 7.857000, record.z); + EXPECT_FLOAT_EQ( 0.717830, record.r); + EXPECT_FLOAT_EQ( -1, record.parent_id); } { @@ -271,18 +287,22 @@ TEST(swc_parser, valid_input) swc_input << "# this is a final comment\n"; + using swc_iter = std::istream_iterator<swc_record>; + swc_iter end; + std::size_t nr_records = 0; - for (auto record : swc_get_records<swc_io_raw>(swc_input)) { + for (swc_iter i = swc_iter(swc_input); i!=end; ++i) { ASSERT_LT(nr_records, records_orig.size()); - expect_record_equals(records_orig[nr_records], record); + expect_record_equals(records_orig[nr_records], *i); ++nr_records; } + EXPECT_EQ(2u, nr_records); } } TEST(swc_parser, from_allen_db) { - using namespace nest::mc; + using namespace nest::mc::io; std::string datadir{DATADIR}; auto fname = datadir + "/example.swc"; @@ -293,13 +313,10 @@ TEST(swc_parser, from_allen_db) } // load the record records into a std::vector - std::vector<io::swc_record> nodes; - for (auto node : io::swc_get_records<io::swc_io_raw>(fid)) { - nodes.push_back(std::move(node)); - } + std::vector<swc_record> nodes = parse_swc_file(fid); // verify that the correct number of nodes was read - EXPECT_EQ(nodes.size(), 1058u); + EXPECT_EQ(1058u, nodes.size()); } TEST(swc_parser, input_cleaning) @@ -314,7 +331,7 @@ TEST(swc_parser, input_cleaning) is << "2 2 14.566132 34.873772 7.857000 0.717830 1\n"; is << "2 2 14.566132 34.873772 7.857000 0.717830 1\n"; - EXPECT_EQ(2u, swc_get_records<swc_io_clean>(is).size()); + EXPECT_THROW(parse_swc_file(is), swc_error); } { @@ -325,8 +342,7 @@ TEST(swc_parser, input_cleaning) is << "3 1 14.566132 34.873772 7.857000 0.717830 -1\n"; is << "4 2 14.566132 34.873772 7.857000 0.717830 1\n"; - auto records = swc_get_records<swc_io_clean>(is); - EXPECT_EQ(2u, records.size()); + EXPECT_THROW(parse_swc_file(is), swc_error); } { @@ -339,14 +355,12 @@ TEST(swc_parser, input_cleaning) std::array<swc_record::id_type, 4> expected_id_list = {{ 0, 1, 2, 3 }}; - auto expected_id = expected_id_list.cbegin(); - for (auto c : swc_get_records<swc_io_clean>(is)) { - EXPECT_EQ(*expected_id, c.id()); - ++expected_id; - } + auto records = parse_swc_file(is); + ASSERT_EQ(expected_id_list.size(), records.size()); - // Check that we have read through the whole input - EXPECT_EQ(expected_id_list.end(), expected_id); + for (unsigned i = 0; i< expected_id_list.size(); ++i) { + EXPECT_EQ(expected_id_list[i], records[i].id); + } } { @@ -364,22 +378,16 @@ TEST(swc_parser, input_cleaning) std::array<swc_record::id_type, 6> expected_parent_list = {{ -1, 0, 1, 1, 0, 4 }}; - auto expected_id = expected_id_list.cbegin(); - auto expected_parent = expected_parent_list.cbegin(); - for (auto c : swc_get_records<swc_io_clean>(is)) { - EXPECT_EQ(*expected_id, c.id()); - EXPECT_EQ(*expected_parent, c.parent()); - ++expected_id; - ++expected_parent; + auto records = parse_swc_file(is); + ASSERT_EQ(expected_id_list.size(), records.size()); + for (unsigned i = 0; i< expected_id_list.size(); ++i) { + EXPECT_EQ(expected_id_list[i], records[i].id); + EXPECT_EQ(expected_parent_list[i], records[i].parent_id); } - - // Check that we have read through the whole input - EXPECT_EQ(expected_id_list.end(), expected_id); - EXPECT_EQ(expected_parent_list.end(), expected_parent); } } -TEST(swc_record_ranges, raw) +TEST(swc_parser, raw) { using namespace nest::mc::io; @@ -391,50 +399,11 @@ TEST(swc_record_ranges, raw) is << "3 2 14.566132 34.873772 7.857000 0.717830 1\n"; is << "4 2 14.566132 34.873772 7.857000 0.717830 1\n"; - std::vector<swc_record> records; - for (auto c : swc_get_records<swc_io_raw>(is)) { - records.push_back(c); - } + using swc_iter = std::istream_iterator<swc_record>; + std::vector<swc_record> records{swc_iter(is), swc_iter()}; EXPECT_EQ(4u, records.size()); - - bool entered = false; - auto citer = records.begin(); - for (auto c : swc_get_records<swc_io_raw>(is)) { - expect_record_equals(c, *citer++); - entered = true; - } - - EXPECT_TRUE(entered); - } - - { - // Check out of bounds reads - std::stringstream is; - is << "1 1 14.566132 34.873772 7.857000 0.717830 -1\n"; - - auto ibegin = swc_get_records<swc_io_raw>(is).begin(); - - EXPECT_NO_THROW(++ibegin); - EXPECT_THROW(*ibegin, std::out_of_range); - - } - - { - // Check iterator increments - std::stringstream is; - is << "1 1 14.566132 34.873772 7.857000 0.717830 -1\n"; - - auto iter = swc_get_records<swc_io_raw>(is).begin(); - auto iend = swc_get_records<swc_io_raw>(is).end(); - - swc_record c; - EXPECT_NO_THROW(c = *iter++); - EXPECT_EQ(-1, c.parent()); - EXPECT_EQ(iend, iter); - - // Try to read past eof - EXPECT_THROW(*iter, std::out_of_range); + EXPECT_EQ(3, records.back().id); } { @@ -445,105 +414,108 @@ TEST(swc_record_ranges, raw) is << "3 10 14.566132 34.873772 7.857000 0.717830 1\n"; is << "4 2 14.566132 34.873772 7.857000 0.717830 1\n"; - std::vector<swc_record> records; try { - for (auto c : swc_get_records<swc_io_raw>(is)) { - records.push_back(c); - } - + parse_swc_file(is); ADD_FAILURE() << "expected an exception\n"; - } catch (const swc_parse_error& e) { - EXPECT_EQ(3u, e.lineno()); + } + catch (const swc_error& e) { + EXPECT_EQ(3u, e.line_number); } } { // Test empty range std::stringstream is(""); - EXPECT_TRUE(swc_get_records<swc_io_raw>(is).empty()); - EXPECT_TRUE(swc_get_records<swc_io_clean>(is).empty()); + using swc_iter = std::istream_iterator<swc_record>; + std::vector<swc_record> records{swc_iter(is), swc_iter()}; + + EXPECT_TRUE(records.empty()); } } -TEST(swc_io, cell_construction) -{ +TEST(swc_io, cell_construction) { using namespace nest::mc; - { - // - // 0 - // | - // 1 - // | - // 2 - // / \. - // 3 4 - // \. - // 5 - // - - std::stringstream is; - is << "1 1 0 0 0 2.1 -1\n"; - is << "2 3 0.1 1.2 1.2 1.3 1\n"; - is << "3 3 1.0 2.0 2.2 1.1 2\n"; - is << "4 3 1.5 3.3 1.3 2.2 3\n"; - is << "5 3 2.5 5.3 2.5 0.7 3\n"; - is << "6 3 3.5 2.3 3.7 3.4 5\n"; - - using point_type = point<double>; - std::vector<point_type> points = { - { 0.0, 0.0, 0.0 }, - { 0.1, 1.2, 1.2 }, - { 1.0, 2.0, 2.2 }, - { 1.5, 3.3, 1.3 }, - { 2.5, 5.3, 2.5 }, - { 3.5, 2.3, 3.7 }, - }; - - cell cell = io::swc_read_cell(is); - EXPECT_TRUE(cell.has_soma()); - EXPECT_EQ(4u, cell.num_segments()); - - EXPECT_EQ(norm(points[1]-points[2]), cell.cable(1)->length()); - EXPECT_EQ(norm(points[2]-points[3]), cell.cable(2)->length()); - EXPECT_EQ(norm(points[2]-points[4]) + norm(points[4]-points[5]), - cell.cable(3)->length()); - - - // Check each segment separately - EXPECT_TRUE(cell.segment(0)->is_soma()); - EXPECT_EQ(2.1, cell.soma()->radius()); - EXPECT_EQ(point_type(0, 0, 0), cell.soma()->center()); - - for (auto i = 1u; i < cell.num_segments(); ++i) { - EXPECT_TRUE(cell.segment(i)->is_dendrite()); - } - - EXPECT_EQ(1u, cell.cable(1)->num_sub_segments()); - EXPECT_EQ(1u, cell.cable(2)->num_sub_segments()); - EXPECT_EQ(2u, cell.cable(3)->num_sub_segments()); - - - // Check the radii - EXPECT_EQ(1.3, cell.cable(1)->radius(0)); - EXPECT_EQ(1.1, cell.cable(1)->radius(1)); - - EXPECT_EQ(1.1, cell.cable(2)->radius(0)); - EXPECT_EQ(2.2, cell.cable(2)->radius(1)); - - EXPECT_EQ(1.1, cell.cable(3)->radius(0)); - EXPECT_EQ(3.4, cell.cable(3)->radius(1)); - - auto len_ratio = norm(points[2]-points[4]) / cell.cable(3)->length(); - EXPECT_NEAR(.7, cell.cable(3)->radius(len_ratio), 1e-15); - - // Double-check radii at joins are equal - EXPECT_EQ(cell.cable(1)->radius(1), - cell.cable(2)->radius(0)); - - EXPECT_EQ(cell.cable(1)->radius(1), - cell.cable(3)->radius(0)); - } + // + // 0 + // | + // 1 + // | + // 2 + // / \. + // 3 4 + // \. + // 5 + // + + std::stringstream is; + is << "1 1 0 0 0 2.1 -1\n"; + is << "2 3 0.1 1.2 1.2 1.3 1\n"; + is << "3 3 1.0 2.0 2.2 1.1 2\n"; + is << "4 3 1.5 3.3 1.3 2.2 3\n"; + is << "5 3 2.5 5.3 2.5 0.7 3\n"; + is << "6 3 3.5 2.3 3.7 3.4 5\n"; + + using point_type = point<double>; + std::vector<point_type> points = { + { 0.0, 0.0, 0.0 }, + { 0.1, 1.2, 1.2 }, + { 1.0, 2.0, 2.2 }, + { 1.5, 3.3, 1.3 }, + { 2.5, 5.3, 2.5 }, + { 3.5, 2.3, 3.7 }, + }; + + // swc -> morphology + auto morph = io::swc_as_morphology(io::parse_swc_file(is)); + + cell cell = make_cell(morph, true); + EXPECT_TRUE(cell.has_soma()); + EXPECT_EQ(4u, cell.num_segments()); + + EXPECT_EQ(norm(points[1]-points[2]), cell.cable(1)->length()); + EXPECT_EQ(norm(points[2]-points[3]), cell.cable(2)->length()); + EXPECT_EQ(norm(points[2]-points[4]) + norm(points[4]-points[5]), + cell.cable(3)->length()); + + + // Check each segment separately + EXPECT_TRUE(cell.segment(0)->is_soma()); + EXPECT_EQ(2.1, cell.soma()->radius()); + EXPECT_EQ(point_type(0, 0, 0), cell.soma()->center()); + + for (auto i = 1u; i < cell.num_segments(); ++i) { + EXPECT_TRUE(cell.segment(i)->is_dendrite()); + } + + EXPECT_EQ(1u, cell.cable(1)->num_sub_segments()); + EXPECT_EQ(1u, cell.cable(2)->num_sub_segments()); + EXPECT_EQ(2u, cell.cable(3)->num_sub_segments()); + + // We asked to use the same discretization as in the SWC, so check number of compartments too. + EXPECT_EQ(1u, cell.cable(1)->num_compartments()); + EXPECT_EQ(1u, cell.cable(2)->num_compartments()); + EXPECT_EQ(2u, cell.cable(3)->num_compartments()); + + // Check the radii + EXPECT_EQ(1.3, cell.cable(1)->radius(0)); + EXPECT_EQ(1.1, cell.cable(1)->radius(1)); + + EXPECT_EQ(1.1, cell.cable(2)->radius(0)); + EXPECT_EQ(2.2, cell.cable(2)->radius(1)); + + EXPECT_EQ(1.1, cell.cable(3)->radius(0)); + EXPECT_EQ(3.4, cell.cable(3)->radius(1)); + + auto len_ratio = norm(points[2]-points[4]) / cell.cable(3)->length(); + EXPECT_NEAR(.7, cell.cable(3)->radius(len_ratio), 1e-15); + + // Double-check radii at joins are equal + EXPECT_EQ(cell.cable(1)->radius(1), + cell.cable(2)->radius(0)); + + EXPECT_EQ(cell.cable(1)->radius(1), + cell.cable(3)->radius(0)); } // check that simple ball and stick model with one dendrite attached to a soma @@ -560,16 +532,16 @@ TEST(swc_parser, from_file_ball_and_stick) } // read the file into a cell object - auto cell = nest::mc::io::swc_read_cell(fid); + auto bas_cell = make_cell(io::swc_as_morphology(io::parse_swc_file(fid))); // verify that the correct number of nodes was read - EXPECT_EQ(cell.num_segments(), 2u); - EXPECT_EQ(cell.num_compartments(), 2u); + EXPECT_EQ(2u, bas_cell.num_segments()); + EXPECT_EQ(2u, bas_cell.num_compartments()); // make an equivalent cell via C++ interface - nest::mc::cell local_cell; + cell local_cell; local_cell.add_soma(6.30785); - local_cell.add_cable(0, nest::mc::segmentKind::dendrite, 0.5, 0.5, 200); + local_cell.add_cable(0, section_kind::dendrite, 0.5, 0.5, 200); - EXPECT_TRUE(nest::mc::cell_basic_equality(local_cell, cell)); + EXPECT_TRUE(cell_basic_equality(local_cell, bas_cell)); }