diff --git a/arbor/include/arbor/morph/label_dict.hpp b/arbor/include/arbor/morph/label_dict.hpp index f7c43ec6bbf9868d66dc17359352e22c1481cc9b..7d909ed0c735ecb3e0d8b8dd9d0737322bc3d640 100644 --- a/arbor/include/arbor/morph/label_dict.hpp +++ b/arbor/include/arbor/morph/label_dict.hpp @@ -24,7 +24,7 @@ public: // construct a label dict with SWC tags predefined label_dict& add_swc_tags(); - void import(const label_dict& other, const std::string& prefix = ""); + label_dict& extend(const label_dict& other, const std::string& prefix = ""); label_dict& set(const std::string& name, locset ls); label_dict& set(const std::string& name, region reg); diff --git a/arbor/morph/label_dict.cpp b/arbor/morph/label_dict.cpp index ba0c4d83cd905c87d8ea5603c901159b22854720..11ac3e5a18a6a4bc1332da17e00ab534b82f4d05 100644 --- a/arbor/morph/label_dict.cpp +++ b/arbor/morph/label_dict.cpp @@ -45,7 +45,7 @@ label_dict& label_dict::set(const std::string& name, arb::iexpr e) { return *this; } -void label_dict::import(const label_dict& other, const std::string& prefix) { +label_dict& label_dict::extend(const label_dict& other, const std::string& prefix) { for (const auto& entry: other.locsets()) { set(prefix+entry.first, entry.second); } @@ -55,6 +55,7 @@ void label_dict::import(const label_dict& other, const std::string& prefix) { for (const auto& entry: other.iexpressions()) { set(prefix+entry.first, entry.second); } + return *this; } std::optional<region> label_dict::region(const std::string& name) const { diff --git a/arborio/include/arborio/loaded_morphology.hpp b/arborio/include/arborio/loaded_morphology.hpp new file mode 100644 index 0000000000000000000000000000000000000000..84dbc41806db177c1860d8f718f36b763fd53fdc --- /dev/null +++ b/arborio/include/arborio/loaded_morphology.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include <variant> + +#include <arborio/export.hpp> + +#include <arbor/morph/label_dict.hpp> +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/segment_tree.hpp> + +namespace arborio { + +struct ARB_ARBORIO_API swc_metadata {}; + +struct ARB_ARBORIO_API asc_color { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; +}; + +struct ARB_ARBORIO_API asc_spine { + std::string name; + arb::mpoint location; +}; + +enum ARB_ARBORIO_API asc_marker { dot, circle, cross, none }; + +struct ARB_ARBORIO_API asc_marker_set { + asc_color color; + asc_marker marker = asc_marker::none; + std::string name; + std::vector<arb::mpoint> locations; +}; + +struct ARB_ARBORIO_API asc_metadata { + std::vector<asc_marker_set> markers; + std::vector<asc_spine> spines; +}; + +// Bundle some detailed metadata for neuroml ingestion. +struct ARB_SYMBOL_VISIBLE nml_metadata { + // Cell id, or empty if morphology was taken from a top-level <morphology> element. + std::optional<std::string> cell_id; + + // Morphology id. + std::string id; + + // One region expression for each segment id. + arb::label_dict segments; + + // One region expression for each name applied to one or more segments. + arb::label_dict named_segments; + + // One region expression for each segmentGroup id. + arb::label_dict groups; + + // Map from segmentGroup ids to their corresponding segment ids. + std::unordered_map<std::string, std::vector<unsigned long long>> group_segments; +}; + +// Interface for ingesting morphology data +struct ARB_ARBORIO_API loaded_morphology { + // Raw segment tree, identical to morphology. + arb::segment_tree segment_tree; + + // Morphology constructed from description. + arb::morphology morphology; + + // Regions and locsets defined in the description. + arb::label_dict labels; + + // Loader specific metadata + std::variant<swc_metadata, asc_metadata, nml_metadata> metadata; +}; + +} diff --git a/arborio/include/arborio/neurolucida.hpp b/arborio/include/arborio/neurolucida.hpp index 260459f883c37a252eba8d8d3746fb31d6407f6a..8019fcf592e8a00046c9e8dfa41ff54810dba781 100644 --- a/arborio/include/arborio/neurolucida.hpp +++ b/arborio/include/arborio/neurolucida.hpp @@ -6,8 +6,8 @@ #include <filesystem> #include <arbor/arbexcept.hpp> -#include <arbor/morph/label_dict.hpp> -#include <arbor/morph/morphology.hpp> + +#include <arborio/loaded_morphology.hpp> #include <arborio/export.hpp> namespace arborio { @@ -33,23 +33,10 @@ struct ARB_SYMBOL_VISIBLE asc_unsupported: asc_exception { std::string message; }; -struct asc_morphology { - // Raw segment tree from ASC, identical to morphology. - arb::segment_tree segment_tree; - - // Morphology constructed from asc description. - arb::morphology morphology; - - // Regions and locsets defined in the asc description. - arb::label_dict labels; -}; - // Perform the parsing of the input as a string. -ARB_ARBORIO_API asc_morphology parse_asc_string(const char* input); -ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input); +ARB_ARBORIO_API loaded_morphology parse_asc_string(const char* input); // Load asc morphology from file with name filename. -ARB_ARBORIO_API asc_morphology load_asc(const std::filesystem::path& filename); -ARB_ARBORIO_API arb::segment_tree load_asc_raw(const std::filesystem::path&filename); +ARB_ARBORIO_API loaded_morphology load_asc(const std::filesystem::path& filename); } // namespace arborio diff --git a/arborio/include/arborio/neuroml.hpp b/arborio/include/arborio/neuroml.hpp index 7d889581b4c948fd25fe141699a84e9790f97301..a17431fccb3a9e7cc2743583ee4a08146453b8ec 100644 --- a/arborio/include/arborio/neuroml.hpp +++ b/arborio/include/arborio/neuroml.hpp @@ -8,6 +8,7 @@ #include <unordered_map> #include <vector> +#include <arborio/loaded_morphology.hpp> #include <arbor/morph/label_dict.hpp> #include <arbor/morph/morphology.hpp> #include <arborio/export.hpp> @@ -69,29 +70,6 @@ struct ARB_SYMBOL_VISIBLE nml_cyclic_dependency: neuroml_exception { // Note: segment id values are interpreted as unsigned long long values; // parsing larger segment ids will throw an exception. -struct nml_morphology_data { - // Cell id, or empty if morphology was taken from a top-level <morphology> element. - std::optional<std::string> cell_id; - - // Morphology id. - std::string id; - - // Morphology constructed from a single NeuroML <morphology> element. - arb::morphology morphology; - - // One region expression for each segment id. - arb::label_dict segments; - - // One region expression for each name applied to one or more segments. - arb::label_dict named_segments; - - // One region expression for each segmentGroup id. - arb::label_dict groups; - - // Map from segmentGroup ids to their corresponding segment ids. - std::unordered_map<std::string, std::vector<unsigned long long>> group_segments; -}; - // Represent NeuroML data determined by provided string. struct ARB_ARBORIO_API neuroml_impl; @@ -126,8 +104,8 @@ struct ARB_ARBORIO_API neuroml { // Parse and retrieve top-level morphology or morphology associated with a cell. // Return nullopt if not found. - std::optional<nml_morphology_data> morphology(const std::string& morph_id, enum neuroml_options::values = neuroml_options::none) const; - std::optional<nml_morphology_data> cell_morphology(const std::string& cell_id, enum neuroml_options::values = neuroml_options::none) const; + std::optional<loaded_morphology> morphology(const std::string& morph_id, enum neuroml_options::values = neuroml_options::none) const; + std::optional<loaded_morphology> cell_morphology(const std::string& cell_id, enum neuroml_options::values = neuroml_options::none) const; ~neuroml(); diff --git a/arborio/include/arborio/swcio.hpp b/arborio/include/arborio/swcio.hpp index 38b59e0c5ae26fbb90595c744d2ee19de41e5f44..3d91e61f7a8eb1c678f19e28480b12072b1937bd 100644 --- a/arborio/include/arborio/swcio.hpp +++ b/arborio/include/arborio/swcio.hpp @@ -3,9 +3,11 @@ #include <iostream> #include <string> #include <vector> +#include <filesystem> #include <arbor/arbexcept.hpp> -#include <arbor/morph/morphology.hpp> + +#include <arborio/loaded_morphology.hpp> #include <arborio/export.hpp> namespace arborio { @@ -114,7 +116,6 @@ public: // conditions above are encountered. // // SWC records are returned in id order. - ARB_ARBORIO_API swc_data parse_swc(std::istream&); ARB_ARBORIO_API swc_data parse_swc(const std::string&); @@ -126,17 +127,15 @@ ARB_ARBORIO_API swc_data parse_swc(const std::string&); // one segment for each SWC record after the first: this record defines the tag // and distal point of the segment, while the proximal point is taken from the // parent record. - -ARB_ARBORIO_API arb::morphology load_swc_arbor(const swc_data& data); -ARB_ARBORIO_API arb::segment_tree load_swc_arbor_raw(const swc_data& data); +ARB_ARBORIO_API loaded_morphology load_swc_arbor(const swc_data& data); +ARB_ARBORIO_API loaded_morphology load_swc_arbor(const std::filesystem::path& fn); // As above, will convert a valid, ordered sequence of SWC records into a morphology // // Note that 'one-point soma' SWC files are supported here // // Complies inferred SWC rules from NEURON, explicitly listed in the docs. - -ARB_ARBORIO_API arb::morphology load_swc_neuron(const swc_data& data); -ARB_ARBORIO_API arb::segment_tree load_swc_neuron_raw(const swc_data& data); +ARB_ARBORIO_API loaded_morphology load_swc_neuron(const swc_data& data); +ARB_ARBORIO_API loaded_morphology load_swc_neuron(const std::filesystem::path& fn); } // namespace arborio diff --git a/arborio/neurolucida.cpp b/arborio/neurolucida.cpp index 12910e88a2c68a75905e008b201afd3e7c19bceb..96729037786df280d9b0640028e78abef8abd8b3 100644 --- a/arborio/neurolucida.cpp +++ b/arborio/neurolucida.cpp @@ -164,11 +164,6 @@ bool is_marker_symbol(const asc::token& t) { // Parse a color expression, which have been observed in the wild in two forms: // (Color Red) ; labeled // (Color RGB (152, 251, 152)) ; RGB literal -struct asc_color { - uint8_t r = 0; - uint8_t g = 0; - uint8_t b = 0; -}; [[maybe_unused]] std::ostream& operator<<(std::ostream& o, const asc_color& c) { @@ -307,16 +302,27 @@ parse_hopefully<arb::mpoint> parse_point(asc::lexer& L) { #define PARSE_POINT(L, X) if (auto rval__ = parse_point(L)) X=*rval__; else return FORWARD_PARSE_ERROR(rval__.error()); -parse_hopefully<arb::mpoint> parse_spine(asc::lexer& L) { +parse_hopefully<asc_spine> parse_spine(asc::lexer& L) { + // check and consume opening <( EXPECT_TOKEN(L, tok::lt); - auto& t = L.current(); - while (t.kind!=tok::gt && t.kind!=tok::error && t.kind!=tok::eof) { - L.next(); - } - //if (t.kind!=error && t.kind!=eof) + EXPECT_TOKEN(L, tok::lparen); + + arb::mpoint p; + PARSE_DOUBLE(L, p.x); + PARSE_DOUBLE(L, p.y); + PARSE_DOUBLE(L, p.z); + double diameter; + PARSE_DOUBLE(L, diameter); + p.radius = diameter/2.0; + // Now eat the label + std::string l = L.current().spelling; + L.next(); + + // check and consume closing )> + EXPECT_TOKEN(L, tok::rparen); EXPECT_TOKEN(L, tok::gt); - return arb::mpoint{}; + return asc_spine{l, p}; } #define PARSE_SPINE(L, X) if (auto rval__ = parse_spine(L)) X=std::move(*rval__); else return FORWARD_PARSE_ERROR(rval__.error()); @@ -342,30 +348,33 @@ parse_hopefully<std::string> parse_name(asc::lexer& L) { #define PARSE_NAME(L, X) {if (auto rval__ = parse_name(L)) X=*rval__; else return FORWARD_PARSE_ERROR(rval__.error());} -struct marker_set { - asc_color color; - std::string name; - std::vector<arb::mpoint> locations; -}; - [[maybe_unused]] -std::ostream& operator<<(std::ostream& o, const marker_set& ms) { +std::ostream& operator<<(std::ostream& o, const asc_marker_set& ms) { o << "(marker-set \"" << ms.name << "\" " << ms.color; for (auto& l: ms.locations) o << " " << l; return o << ")"; } -parse_hopefully<marker_set> parse_markers(asc::lexer& L) { +parse_hopefully<asc_marker_set> parse_markers(asc::lexer& L) { EXPECT_TOKEN(L, tok::lparen); - marker_set markers; + asc_marker_set markers; // parse marker kind keyword auto t = L.current(); if (!is_marker_symbol(t)) { return unexpected(PARSE_ERROR("expected a valid marker type", t.loc)); } + if (t.spelling == "Dot") { + markers.marker = asc_marker::dot; + } else if (t.spelling == "Cross") { + markers.marker = asc_marker::cross; + } else if (t.spelling == "Circle") { + markers.marker = asc_marker::circle; + } else { + // impossible + } L.next(); while (L.current().kind==tok::lparen) { auto n = L.peek(); @@ -392,6 +401,8 @@ parse_hopefully<marker_set> parse_markers(asc::lexer& L) { struct branch { std::vector<arb::mpoint> samples; std::vector<branch> children; + std::vector<asc_marker_set> markers; + std::vector<asc_spine> spines; }; std::size_t num_samples(const branch& b) { @@ -444,16 +455,15 @@ parse_hopefully<branch> parse_branch(asc::lexer& L) { } // A marker statement is always of the form ( MARKER_TYPE ...) else if (t.kind==tok::lparen && is_marker_symbol(p)) { - marker_set markers; + asc_marker_set markers; PARSE_MARKER(L, markers); - // Parse the markers, but don't record information about them. - // These could be grouped into locset by name and added to the label dictionary. + B.markers.push_back(markers); } // Spines are marked by a "less than", i.e. "<", symbol. else if (t.kind==tok::lt) { - arb::mpoint spine; + asc_spine spine; PARSE_SPINE(L, spine); - // parse the spine, but don't record the location. + B.spines.push_back(spine); } // Test for a symbol that indicates a terminal. else if (is_branch_end_symbol(t)) { @@ -610,10 +620,16 @@ parse_hopefully<sub_tree> parse_sub_tree(asc::lexer& L) { // Perform the parsing of the input as a string. -ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input) { +ARB_ARBORIO_API std::tuple<arb::segment_tree, + std::vector<asc_marker_set>, + std::vector<asc_spine>> +parse_asc_string_raw(const char* input) { asc::lexer lexer(input); std::vector<sub_tree> sub_trees; + std::vector<asc_marker_set> markers; + std::vector<asc_spine> spines; + // Iterate over high-level pseudo-s-expressions in the file. // This pass simply parses the contents of the file, to be interpretted @@ -695,6 +711,7 @@ ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input) { arb::mpoint soma_0, soma_1, soma_2; if (soma_count==1u) { const auto& st = sub_trees[soma_contours.front()]; + // NOTE No children allowed!? const auto& samples = st.root.samples; if (samples.size()==1u) { // The soma is described as a sphere with a single sample. @@ -709,10 +726,13 @@ ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input) { soma_0.radius = std::accumulate(samples.begin(), samples.end(), 0., [&soma_0](double a, auto& c) {return a+arb::distance(c, soma_0);}) / ns; } + soma_1 = {soma_0.x, soma_0.y-soma_0.radius, soma_0.z, soma_0.radius}; soma_2 = {soma_0.x, soma_0.y+soma_0.radius, soma_0.z, soma_0.radius}; stree.append(arb::mnpos, soma_0, soma_1, 1); stree.append(arb::mnpos, soma_0, soma_2, 1); + spines.insert(spines.end(), st.root.spines.begin(), st.root.spines.end()); + markers.insert(markers.end(), st.root.markers.begin(), st.root.markers.end()); } // Append the dend, axon and apical dendrites. @@ -742,6 +762,9 @@ ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input) { auto prox_sample = head.sample; if (!branch.samples.empty()) { // Skip empty branches, which are permitted + spines.insert(spines.end(), branch.spines.begin(), branch.spines.end()); + markers.insert(markers.end(), branch.markers.begin(), branch.markers.end()); + auto it = branch.samples.begin(); // Don't connect the first sample to the distal end of the parent // branch if the parent is the soma center. @@ -768,13 +791,13 @@ ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input) { } } - return stree; + return {stree, markers, spines}; } -ARB_ARBORIO_API asc_morphology parse_asc_string(const char* input) { +ARB_ARBORIO_API loaded_morphology parse_asc_string(const char* input) { // Parse segment tree - arb::segment_tree stree = parse_asc_string_raw(input); + const auto& [stree, markers, spines] = parse_asc_string_raw(input); // Construct the morphology. arb::morphology morphology(stree); @@ -786,7 +809,7 @@ ARB_ARBORIO_API asc_morphology parse_asc_string(const char* input) { labels.set("dend", arb::reg::tagged(3)); labels.set("apic", arb::reg::tagged(4)); - return {stree, std::move(morphology), std::move(labels)}; + return {stree, std::move(morphology), std::move(labels), asc_metadata{markers, spines}}; } @@ -809,17 +832,8 @@ inline std::string read_file(const std::filesystem::path& filename) { } -ARB_ARBORIO_API asc_morphology load_asc(const std::filesystem::path& filename) { +ARB_ARBORIO_API loaded_morphology load_asc(const std::filesystem::path& filename) { std::string fstr = read_file(filename); return parse_asc_string(fstr.c_str()); } - - -ARB_ARBORIO_API arb::segment_tree load_asc_raw(const std::filesystem::path& filename) { - std::string fstr = read_file(filename); - return parse_asc_string_raw(fstr.c_str()); -} - - } // namespace arborio - diff --git a/arborio/neuroml.cpp b/arborio/neuroml.cpp index 554b7a6db494c5f489554f28f6a8a13175bd49a0..fdc7a1914f96e083120ac7ae76957aefcdd4fe19 100644 --- a/arborio/neuroml.cpp +++ b/arborio/neuroml.cpp @@ -78,7 +78,7 @@ std::vector<std::string> neuroml::morphology_ids() const { return result; } -optional<nml_morphology_data> neuroml::morphology(const std::string& morph_id, enum neuroml_options::values options) const { +optional<loaded_morphology> neuroml::morphology(const std::string& morph_id, enum neuroml_options::values options) const { auto id = xpath_escape(morph_id); auto query = "//neuroml/morphology[@id=" + id + "]"; auto match = impl_->doc.select_node(query.data()).node(); @@ -86,13 +86,13 @@ optional<nml_morphology_data> neuroml::morphology(const std::string& morph_id, e return nml_parse_morphology_element(match, options); } -optional<nml_morphology_data> neuroml::cell_morphology(const std::string& cell_id, enum neuroml_options::values options) const { +optional<loaded_morphology> neuroml::cell_morphology(const std::string& cell_id, enum neuroml_options::values options) const { auto id = "//neuroml/cell[@id=" + xpath_escape(cell_id) + "]"; auto query = "(//neuroml/morphology[@id=string((" + id + "/@morphology)[1])] | " + id + "/morphology)[1]"; auto match = impl_->doc.select_node(query.data()).node(); if (match.empty()) return nullopt; - nml_morphology_data M = nml_parse_morphology_element(match, options); - M.cell_id = cell_id; + auto M = nml_parse_morphology_element(match, options); + std::get<nml_metadata>(M.metadata).cell_id = cell_id; return M; } diff --git a/arborio/nml_parse_morphology.cpp b/arborio/nml_parse_morphology.cpp index 8bcfcf1682a5edf96767f26959d4620c80a954a0..f6731c67ee36469ec2034737ae4dc2c3d9021ae0 100644 --- a/arborio/nml_parse_morphology.cpp +++ b/arborio/nml_parse_morphology.cpp @@ -20,7 +20,6 @@ #include "xml.hpp" using std::optional; -using arb::region; using arb::util::expected; using arb::util::unexpected; @@ -398,11 +397,13 @@ static arb::stitched_morphology construct_morphology(const neuroml_segment_tree& return arb::stitched_morphology(std::move(builder)); } -nml_morphology_data nml_parse_morphology_element(const xml_node& morph, - enum neuroml_options::values options) { +loaded_morphology nml_parse_morphology_element(const xml_node& morph, + enum neuroml_options::values options) { using namespace neuroml_options; - nml_morphology_data M; - M.id = get_attr<std::string>(morph, "id"); + loaded_morphology M; + M.metadata = nml_metadata{}; + auto& L = std::get<nml_metadata>(M.metadata); + L.id = get_attr<std::string>(morph, "id"); std::vector<neuroml_segment> segments; @@ -512,13 +513,14 @@ nml_morphology_data nml_parse_morphology_element(const xml_node& morph, groups.push_back(std::move(group)); } - M.group_segments = evaluate_segment_groups(std::move(groups), segtree); + L.group_segments = evaluate_segment_groups(std::move(groups), segtree); // Build morphology and label dictionaries: arb::stitched_morphology stitched = construct_morphology(segtree); M.morphology = stitched.morphology(); - M.segments = stitched.labels(); + M.segment_tree = M.morphology.to_segment_tree(); + L.segments = stitched.labels(); std::unordered_multimap<std::string, non_negative> name_to_ids; std::unordered_set<std::string> names; @@ -534,19 +536,22 @@ nml_morphology_data nml_parse_morphology_element(const xml_node& morph, arb::region r; auto ids = name_to_ids.equal_range(name); for (auto i = ids.first; i!=ids.second; ++i) { - r = join(std::move(r), M.segments.regions().at(std::to_string(i->second))); + r = join(std::move(r), L.segments.regions().at(std::to_string(i->second))); } - M.named_segments.set(name, std::move(r)); + L.named_segments.set(name, std::move(r)); } - for (const auto& [group_id, segment_ids]: M.group_segments) { + for (const auto& [group_id, segment_ids]: L.group_segments) { arb::region r; for (auto id: segment_ids) { - r = join(std::move(r), M.segments.regions().at(std::to_string(id))); + r = join(std::move(r), L.segments.regions().at(std::to_string(id))); } - M.groups.set(group_id, std::move(r)); + L.groups.set(group_id, std::move(r)); } + M.labels.extend(L.segments); + M.labels.extend(L.named_segments); + M.labels.extend(L.groups); return M; } diff --git a/arborio/nml_parse_morphology.hpp b/arborio/nml_parse_morphology.hpp index eb185604cd767b638133d62cc4dcce3dd691bf84..3d697a3cb2253e77b5f6a60bd800444bafecda23 100644 --- a/arborio/nml_parse_morphology.hpp +++ b/arborio/nml_parse_morphology.hpp @@ -1,11 +1,12 @@ #pragma once #include <arborio/neuroml.hpp> +#include <arborio/loaded_morphology.hpp> #include <pugixml.hpp> namespace arborio { -nml_morphology_data nml_parse_morphology_element(const pugi::xml_node& morph, enum neuroml_options::values); +loaded_morphology nml_parse_morphology_element(const pugi::xml_node& morph, enum neuroml_options::values); } // namespace arborio diff --git a/arborio/swcio.cpp b/arborio/swcio.cpp index f7ae9e79a8ff8dacd6f929a3825a449f25e01ab2..5cdfab8c224a85d735e8fc7cc336285dc471e678 100644 --- a/arborio/swcio.cpp +++ b/arborio/swcio.cpp @@ -6,11 +6,13 @@ #include <set> #include <string> #include <sstream> +#include <fstream> #include <unordered_map> #include <unordered_set> #include <vector> #include <arbor/morph/segment_tree.hpp> +#include <arbor/morph/label_dict.hpp> #include "arbor/morph/primitives.hpp" @@ -160,7 +162,7 @@ ARB_ARBORIO_API swc_data parse_swc(const std::string& text) { return parse_swc(is); } -ARB_ARBORIO_API arb::segment_tree load_swc_arbor_raw(const swc_data& data) { +arb::segment_tree load_swc_arbor_raw(const swc_data& data) { const auto& records = data.records(); if (records.empty()) return {}; @@ -205,7 +207,7 @@ ARB_ARBORIO_API arb::segment_tree load_swc_arbor_raw(const swc_data& data) { return tree; } -ARB_ARBORIO_API arb::segment_tree load_swc_neuron_raw(const swc_data& data) { +arb::segment_tree load_swc_neuron_raw(const swc_data& data) { constexpr int soma_tag = 1; const auto n_samples = data.records().size(); @@ -324,8 +326,29 @@ ARB_ARBORIO_API arb::segment_tree load_swc_neuron_raw(const swc_data& data) { return tree; } -ARB_ARBORIO_API arb::morphology load_swc_neuron(const swc_data& data) { return {load_swc_neuron_raw(data)}; } -ARB_ARBORIO_API arb::morphology load_swc_arbor(const swc_data& data) { return {load_swc_arbor_raw(data)}; } +ARB_ARBORIO_API loaded_morphology load_swc_neuron(const swc_data& data) { + auto raw = load_swc_neuron_raw(data); + arb::label_dict ld; ld.add_swc_tags(); + return {raw, {raw}, ld, swc_metadata{}}; +} + +ARB_ARBORIO_API loaded_morphology load_swc_arbor(const swc_data& data) { + auto raw = load_swc_arbor_raw(data); + arb::label_dict ld; ld.add_swc_tags(); + return {raw, {raw}, ld, swc_metadata{}}; +} + +ARB_ARBORIO_API loaded_morphology load_swc_arbor(const std::filesystem::path& path) { + std::ifstream fd(path); + if (!fd) throw arb::file_not_found_error("unable to open SWC file: "+path.string()); + return load_swc_arbor(parse_swc(fd)); +} + +ARB_ARBORIO_API loaded_morphology load_swc_neuron(const std::filesystem::path& path) { + std::ifstream fd(path); + if (!fd) throw arb::file_not_found_error("unable to open SWC file: "+path.string()); + return load_swc_neuron(parse_swc(fd)); +} } // namespace arborio diff --git a/doc/cpp/morphology.rst b/doc/cpp/morphology.rst index 178878e9304b3708801cbb60b4f8c2d6365394d9..cb353ccf76fd098e786bc487265f1ca6b8bfc072 100644 --- a/doc/cpp/morphology.rst +++ b/doc/cpp/morphology.rst @@ -475,7 +475,28 @@ The following classes and functions allow the user to inspect the CVs of a cell Supported morphology formats ============================ -Arbor supports morphologies described using the SWC file format and the NeuroML file format. +Arbor supports morphologies described using SWC, Neurolucida ASC, and the NeuroML file formats. +The ingestion of these formats is described below, but each returns a structure + + +.. cpp:class:: loaded_morphology + + .. cpp:member:: arb::segment_tree segment_tree + + Raw segment tree, identical to morphology. + + .. cpp:member:: arb::morphology morphology + + Morphology constructed from description. + + + .. cpp:member:: arb::label_dict labels + + Regions and locsets defined in the description. + + .. cpp:member:: std::variant<swc_metadata, asc_metadata, nml_metadata> metadata + + Loader specific metadata, see below in the individual sections. .. _cppswc: @@ -549,6 +570,17 @@ basic checks performed on them. The :cpp:type:`swc_data` object can then be used Returns a :cpp:type:`morphology` constructed according to NEURON's :ref:`SWC specifications <formatswc-neuron>`. +.. cpp:function:: morphology load_swc_arbor(const std::filesystem::path& data) + + Returns a :cpp:type:`morphology` constructed according to Arbor's + :ref:`SWC specifications <formatswc-arbor>`. + +.. cpp:function:: morphology load_swc_neuron(const std::filesystem::path& data) + + Returns a :cpp:type:`morphology` constructed according to NEURON's + :ref:`SWC specifications <formatswc-neuron>`. + + .. _cppasc: Neurolucida ASCII @@ -557,21 +589,40 @@ Neurolucida ASCII Arbor supports reading morphologies described using the :ref:`Neurolucida ASCII file format <formatasc>`. -The :cpp:func:`parse_asc()` function is used to parse the SWC file and generate a :cpp:type:`asc_morphology` object: -a simple struct with two members representing the morphology and a label dictionary with labeled -regions and locations. +The :cpp:func:`parse_asc()` function is used to parse the SWC file and generate a :cpp:type:`loaded_morphology` object: -.. cpp:class:: asc_morphology +.. cpp:function:: loaded_morphology load_asc(const std::filesystem::path& filename) - .. cpp:member:: arb::morphology morphology + Parse a Neurolucida ASCII file. + Throws an exception if there is an error parsing the file. - .. cpp:member:: arb::label_dict labels +.. cpp:class:: asc_metadata -.. cpp:function:: asc_morphology load_asc(const std::filesystem::path& filename) + .. cpp:member:: std::vector<asc_spine> spines - Parse a Neurolucida ASCII file. - Throws an exception if there is an error parsing the file. + A list of spines annotated in the ``.asc`` file. + + .. cpp:member:: std::vector<asc_marker_set> spines + + A list of marker set annotated in the ``.asc`` file. +.. cpp:class:: asc_spine + + .. cpp:member:: std::string name + + .. cpp:member:: arb::mpoint location + +.. cpp:class:: asc_marker_set + .. cpp:member:: asc_color color + + .. cpp:member:: asc_marker marker = asc_marker::none + + .. cpp:member:: std::string name + + .. cpp:member:: std::vector<arb::mpoint> locations + +where ``asc_marker`` is an enum of ``dot``, ``circle``, ``cross``, or ``none``, +and ``asc_color`` an RGB triple. .. _cppneuroml: @@ -614,12 +665,12 @@ namespace. Return the id of each top-level ``<morphology>`` element defined in the NeuroML document. - .. cpp:function:: std::optional<nml_morphology_data> morphology(const std::string&, enum neuroml_options::value = neuroml_options::none) const + .. cpp:function:: std::optional<loaded_morphology> morphology(const std::string&, enum neuroml_options::value = neuroml_options::none) const Return a representation of the top-level morphology with the supplied identifier, or ``std::nullopt`` if no such morphology could be found. - .. cpp:function:: std::optional<nml_morphology_data> cell_morphology(const std::string&, enum neuroml_options::value = neuroml_options::none) const + .. cpp:function:: std::optional<loaded_morphology> cell_morphology(const std::string&, enum neuroml_options::value = neuroml_options::none) const Return a representation of the morphology associated with the cell with the supplied identifier, or ``std::nullopt`` if the cell or its morphology could not be found. @@ -643,7 +694,7 @@ label dictionaries for regions corresponding to its segments and segment groups and id, and a map providing the explicit list of segments contained within each defined segment group. -.. cpp:class:: nml_morphology_data +.. cpp:class:: nml_metadata .. cpp:member:: std::optional<std::string> cell_id @@ -653,10 +704,6 @@ segment group. The id attribute of the morphology. - .. cpp:member:: arb::morphology morphology - - The corresponding Arbor morphology. - .. cpp:member:: arb::label_dict segments A label dictionary with a region entry for each segment, keyed by the segment id (as a string). diff --git a/doc/python/cable_cell.rst b/doc/python/cable_cell.rst index abb51d6acef0f4705f90caf8e7bf7877f84dcbcb..b78a753ebd1a2fd431edf447345c1d6e8106424b 100644 --- a/doc/python/cable_cell.rst +++ b/doc/python/cable_cell.rst @@ -37,13 +37,7 @@ Cable cells import arbor # Construct the morphology from an SWC file. - tree = arbor.load_swc_arbor('granule.swc') - morph = arbor.morphology(tree) - - # Define regions using standard SWC tags - labels = arbor.label_dict({'soma': '(tag 1)', - 'axon': '(tag 2)', - 'dend': '(join (tag 3) (tag 4))'}) + lmrf = arbor.load_swc_arbor('granule.swc') # Define decorations decor = arbor.decor() @@ -52,7 +46,7 @@ Cable cells decor.paint('"soma"', arbor.density('hh')) # Construct a cable cell. - cell = arbor.cable_cell(morph, decor, labels) + cell = arbor.cable_cell(lmrf.morphology, decor, lmrf.labels) .. method:: __init__(morphology, decorations, labels) diff --git a/doc/python/morphology.rst b/doc/python/morphology.rst index 77cca3c940c43b793d8f8346acbc540ffc5c24fb..6d1bfd9440d9707fa9953cdd42fb3fa52883b0b5 100644 --- a/doc/python/morphology.rst +++ b/doc/python/morphology.rst @@ -641,10 +641,9 @@ SWC NeuroML ------- -.. py:class:: neuroml_morph_data +.. py:class:: nml_metadata - A :class:`neuroml_morphology_data` object contains a representation of a morphology defined in - NeuroML. + A :class:`nml_metadata` object contains extra information specific to NeuroML. .. py:attribute:: cell_id :type: optional<str> @@ -661,11 +660,6 @@ NeuroML A map from each segment group id to its corresponding collection of segments. - .. py:attribute:: morphology - :type: morphology - - The morphology associated with the :class:`neuroml_morph_data` object. - .. py:method:: segments Returns a label dictionary with a region entry for each segment, keyed by the segment id (as a string). @@ -722,7 +716,7 @@ NeuroML :param str morph_id: ID of the top-level morphology. :param bool allow_spherical_root: Treat zero length root segments especially. - :rtype: optional(neuroml_morph_data) + :rtype: optional(loaded_morphology) .. py:method:: cell_morphology(cell_id, allow_spherical_root=false) @@ -731,29 +725,65 @@ NeuroML :param str morph_id: ID of the cell. :param bool allow_spherical_root: Treat zero length root segments especially. - :rtype: optional(neuroml_morph_data) + :rtype: optional(loaded_morphology) .. _pyasc: Neurolucida ----------- -.. py:class:: asc_morphology +.. py:enum:: asc_marker - The morphology and label dictionary meta-data loaded from a Neurolucida ASCII ``.asc`` file. + One of dot, circle, cross, or none. + +.. py:class:: asc_color + + RGB triple. + +.. py:class:: asc_spine + + Marked spine, comprising: + + .. py:attribute:: location + + ``mpoint`` of the spine. + + .. py:attribute:: name + + Associated name. + +.. py:class:: asc_marker_set - .. py:attribute:: morphology + Locations of interest, given as + + .. py:attribute:: locations + + List of ``mpoint``. + + .. py:attribute:: marker + + Associated ``asc_marker``. + + .. py:attribute:: color + + Associated ``asc_color``. + + .. py:attribute:: name + + Associated name. + +.. py:class:: asc_metadata + + The morphology and label dictionary meta-data loaded from a Neurolucida ASCII ``.asc`` file. - The cable cell morphology. + .. py:attribute:: markers - .. py:attribute:: segment_tree + List of marker sets in the input. - The raw segment tree. + .. py:attribute:: spines - .. py:attribute:: labels + List of spines in the input. - The labeled regions and locations extracted from the meta data. The four canonical regions are labeled - ``'soma'``, ``'axon'``, ``'dend'`` and ``'apic'``. .. py:function:: load_asc(filename) diff --git a/doc/scripts/gen-labels.py b/doc/scripts/gen-labels.py index 9ccf3e032a1ca379c3a16b1a45da72d4db9aa846..fe4751e866f85d7d1b09354506316899b6477f84 100644 --- a/doc/scripts/gen-labels.py +++ b/doc/scripts/gen-labels.py @@ -165,7 +165,8 @@ ysoma_morph3 = arbor.morphology(tree) fn = os.path.realpath( os.path.join(os.getcwd(), os.path.dirname(__file__), "../fileformat/example.swc") ) -swc_morph = arbor.load_swc_arbor(fn) +swc = arbor.load_swc_arbor(fn) +swc_morph = swc.morphology regions = { "empty": "(region-nil)", diff --git a/example/single/single.cpp b/example/single/single.cpp index d5590a5520f1cece44a2e1da81254f6bd348a6a0..200ad24a6ab1a222973843aef0d734864b623549 100644 --- a/example/single/single.cpp +++ b/example/single/single.cpp @@ -29,11 +29,11 @@ struct options { }; options parse_options(int argc, char** argv); -arb::morphology default_morphology(); -arb::morphology read_swc(const std::string& path); +arborio::loaded_morphology default_morphology(); struct single_recipe: public arb::recipe { - explicit single_recipe(arb::morphology m, arb::cv_policy pol): morpho(std::move(m)) { + explicit single_recipe(arborio::loaded_morphology m, arb::cv_policy pol): + morpho(std::move(m.morphology)) { gprop.default_parameters = arb::neuron_parameter_defaults; gprop.default_parameters.discretization = pol; } @@ -81,7 +81,8 @@ struct single_recipe: public arb::recipe { int main(int argc, char** argv) { try { options opt = parse_options(argc, argv); - single_recipe R(opt.swc_file.empty()? default_morphology(): read_swc(opt.swc_file), opt.policy); + single_recipe R(opt.swc_file.empty() ? default_morphology() : arborio::load_swc_arbor(opt.swc_file), + opt.policy); arb::simulation sim(R); @@ -143,18 +144,14 @@ options parse_options(int argc, char** argv) { // of length 200 µm and radius decreasing linearly from 0.5 µm // to 0.2 µm. -arb::morphology default_morphology() { +arborio::loaded_morphology default_morphology() { arb::segment_tree tree; tree.append(arb::mnpos, { -6.3, 0.0, 0.0, 6.3}, { 6.3, 0.0, 0.0, 6.3}, 1); tree.append( 0, { 6.3, 0.0, 0.0, 0.5}, {206.3, 0.0, 0.0, 0.2}, 3); - return arb::morphology(tree); -} - -arb::morphology read_swc(const std::string& path) { - std::ifstream f(path); - if (!f) throw std::runtime_error("unable to open SWC file: "+path); + auto labels = arb::label_dict{}; + labels.add_swc_tags(); - return arborio::load_swc_arbor(arborio::parse_swc(f)); + return {tree, {tree}, labels}; } diff --git a/example/v_clamp/v-clamp.cpp b/example/v_clamp/v-clamp.cpp index dbbc15170cf37fd4154db5c698513c2882d14a13..9068065cd6111535201e88b9d1f5f3d345640b14 100644 --- a/example/v_clamp/v-clamp.cpp +++ b/example/v_clamp/v-clamp.cpp @@ -30,11 +30,10 @@ struct options { }; options parse_options(int argc, char** argv); -arb::morphology default_morphology(); -arb::morphology read_swc(const std::string& path); +arborio::loaded_morphology default_morphology(); struct single_recipe: public arb::recipe { - explicit single_recipe(arb::morphology m, arb::cv_policy pol): morpho(std::move(m)) { + explicit single_recipe(arborio::loaded_morphology m, arb::cv_policy pol): morpho(std::move(m.morphology)) { gprop.default_parameters = arb::neuron_parameter_defaults; gprop.default_parameters.discretization = pol; } @@ -84,7 +83,7 @@ struct single_recipe: public arb::recipe { int main(int argc, char** argv) { try { options opt = parse_options(argc, argv); - single_recipe R(opt.swc_file.empty()? default_morphology(): read_swc(opt.swc_file), opt.policy); + single_recipe R(opt.swc_file.empty()? default_morphology(): arborio::load_swc_arbor(opt.swc_file), opt.policy); R.voltage_clamp = opt.voltage; arb::simulation sim(R); @@ -149,18 +148,13 @@ options parse_options(int argc, char** argv) { // of length 200 µm and radius decreasing linearly from 0.5 µm // to 0.2 µm. -arb::morphology default_morphology() { +arborio::loaded_morphology default_morphology() { arb::segment_tree tree; tree.append(arb::mnpos, { -6.3, 0.0, 0.0, 6.3}, { 6.3, 0.0, 0.0, 6.3}, 1); tree.append( 0, { 6.3, 0.0, 0.0, 0.5}, {206.3, 0.0, 0.0, 0.2}, 3); - return arb::morphology(tree); -} - -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 arborio::load_swc_arbor(arborio::parse_swc(f)); + auto labels = arb::label_dict{}; + labels.add_swc_tags(); + return {tree, {tree}, labels}; } diff --git a/python/cells.cpp b/python/cells.cpp index 93415fb6a8ebf1863458ccb0bc55c91426afe3e0..a28eb02a2891e9376bde63c914a1ce7cbd646166 100644 --- a/python/cells.cpp +++ b/python/cells.cpp @@ -305,6 +305,7 @@ void register_cells(py::module& m) { .def("__repr__", &lif_str) .def("__str__", &lif_str); + // arb::cv_policy wrappers cv_policy .def(py::init([](const std::string& expression) { return arborio::parse_cv_policy_expression(expression).unwrap(); }), "expression"_a, "A valid CV policy expression") diff --git a/python/example/gap_junctions.py b/python/example/gap_junctions.py index 67c028a661e8a3af3cf8cbc473463276eea93423..5c7af9b6acc19adff388054698f6a80a16f2e259 100644 --- a/python/example/gap_junctions.py +++ b/python/example/gap_junctions.py @@ -4,7 +4,6 @@ import arbor as A from arbor import units as U import pandas as pd import seaborn as sns -import matplotlib.pyplot as plt # Construct chains of cells linked with gap junctions, # Chains are connected by synapses. @@ -158,5 +157,6 @@ for gid in range(ncells): ) df = pd.concat(df_list, ignore_index=True) -sns.relplot(data=df, kind="line", x="t/ms", y="U/mV", hue="Cell", errorbar=None) -plt.show() +sns.relplot( + data=df, kind="line", x="t/ms", y="U/mV", hue="Cell", errorbar=None +).savefig("gap_junctions.svg") diff --git a/python/example/probe_lfpykit.py b/python/example/probe_lfpykit.py index d83b2732d2e2d48161ef7c6bf8e664328ea43f68..441153ed69a5f8ade5457986822e186ffb857bc0 100644 --- a/python/example/probe_lfpykit.py +++ b/python/example/probe_lfpykit.py @@ -61,8 +61,7 @@ else: print("Usage: single_cell_detailed.py [SWC file name]") sys.exit(1) -# define morphology (needed for ``A.place_pwlin`` and ``A.cable_cell`` below) -morphology = A.load_swc_arbor(filename) +morphology = A.load_swc_arbor(filename).morphology # define a location on morphology for current clamp clamp_location = A.location(4, 1 / 6) diff --git a/python/example/single_cell_allen.py b/python/example/single_cell_allen.py index 6daaf09f9131a20df9d098431b72914172fabd04..0b43daf8dd87b8b20a212dce127420e6532ce187 100644 --- a/python/example/single_cell_allen.py +++ b/python/example/single_cell_allen.py @@ -23,10 +23,10 @@ def load_allen_fit(fit): # cable parameters convenience class @dataclass class parameters: - cm: Optional[float] = None - tempK: Optional[float] = None - Vm: Optional[float] = None - rL: Optional[float] = None + cm: Optional[U.quantity] = None + temp: Optional[U.quantity] = None + Vm: Optional[U.quantity] = None + rL: Optional[U.quantity] = None param = defaultdict(parameters) mechs = defaultdict(dict) @@ -40,14 +40,13 @@ def load_allen_fit(fit): elif mech == "pas": # transform names and values if name == "cm": - # scaling factor NEURON -> Arbor - param[region].cm = value / 100.0 + param[region].cm = value * U.uF / U.cm2 elif name == "Ra": - param[region].rL = value + param[region].rL = value * U.Ohm * U.cm elif name == "Vm": - param[region].Vm = value + param[region].Vm = value * U.mV elif name == "celsius": - param[region].tempK = value + 273.15 + param[region].temp = value * U.Celsius else: raise Exception(f"Unknown key: {name}") continue @@ -59,9 +58,9 @@ def load_allen_fit(fit): mechs = [(r, m, vs) for (r, m), vs in mechs.items()] default = parameters( - tempK=float(fit["conditions"][0]["celsius"]) + 273.15, - Vm=float(fit["conditions"][0]["v_init"]), - rL=float(fit["passive"][0]["ra"]), + temp=float(fit["conditions"][0]["celsius"]) * U.Celsius, + Vm=float(fit["conditions"][0]["v_init"]) * U.mV, + rL=float(fit["passive"][0]["ra"]) * U.Ohm * U.cm, ) ions = [] @@ -71,14 +70,14 @@ def load_allen_fit(fit): if k == "section": continue ion = k[1:] - ions.append((region, ion, float(v))) + ions.append((region, ion, float(v) * U.mV)) return default, regs, ions, mechs, fit["fitting"][0]["junction_potential"] def make_cell(base, swc, fit): # (1) Load the swc file passed into this function - morphology = A.load_swc_neuron(base / swc) + morphology = A.load_swc_neuron(base / swc).morphology # (2) Label the region tags found in the swc with the names used in the parameter fit file. # In addition, label the midpoint of the soma. @@ -93,20 +92,20 @@ def make_cell(base, swc, fit): # (5) assign global electro-physiology parameters decor.set_property( - tempK=dflt.tempK * U.Kelvin, - Vm=dflt.Vm * U.mV, - cm=dflt.cm * U.F / U.m2, - rL=dflt.rL * U.Ohm * U.cm, + tempK=dflt.temp, + Vm=dflt.Vm, + cm=dflt.cm, + rL=dflt.rL, ) # (6) override regional electro-physiology parameters for region, vs in regions: decor.paint( f'"{region}"', - tempK=vs.tempK * U.Kelvin, - Vm=vs.Vm * U.Vm, - cm=vs.cm * U.F / U.m2, - rL=vs.rL * U.Ohm * U.cm, + tempK=vs.temp, + Vm=vs.Vm, + cm=vs.cm, + rL=vs.rL, ) # (7) set reversal potentials @@ -153,7 +152,7 @@ model.run(tfinal=1.4 * U.s, dt=5 * U.us) # (16) Load and scale reference reference = ( - 1e3 * pd.read_csv(here / "single_cell_allen_neuron_ref.csv")["U/mV"] + offset + 1e3 * pd.read_csv(here / "single_cell_allen_neuron_ref.csv")["U/mV"][:-1] + offset ) # (17) Plot diff --git a/python/example/single_cell_detailed.py b/python/example/single_cell_detailed.py index b58e2f924ac8e2f7fda811af511267e8b4fd3e6f..6b8155109f850e8afff9a3e45aaff55b3a330705 100755 --- a/python/example/single_cell_detailed.py +++ b/python/example/single_cell_detailed.py @@ -18,8 +18,7 @@ else: print("Usage: single_cell_detailed.py [SWC file name]") sys.exit(1) - -morph = A.load_swc_arbor(filename) +morph = A.load_swc_arbor(filename).morphology # (2) Create and populate the label dictionary. labels = A.label_dict( diff --git a/python/example/single_cell_detailed_recipe.py b/python/example/single_cell_detailed_recipe.py index 5d28457e98283c5530521e6a634662a29ac0aee3..e34c740f13e3fc6eaf6f421b12487d88f51a9ddc 100644 --- a/python/example/single_cell_detailed_recipe.py +++ b/python/example/single_cell_detailed_recipe.py @@ -15,10 +15,10 @@ if len(sys.argv) == 1: elif len(sys.argv) == 2: filename = Path(sys.argv[1]) else: - print("Usage: single_cell_detailed.py [SWC file name]") + print("Usage: single_cell_detailed_recipe.py [SWC file name]") sys.exit(1) -morph = A.load_swc_arbor(filename) +lmrf = A.load_swc_arbor(filename) # (2) Create and populate the label dictionary. labels = A.label_dict( @@ -39,7 +39,8 @@ labels = A.label_dict( # Add a label for the terminal locations in the "axon" region: "axon_terminal": '(restrict-to (locset "terminal") (region "axon"))', } -).add_swc_tags() # Add SWC pre-defined regions +) +labels.append(lmrf.labels) # (3) Create and populate the decor. decor = ( @@ -72,7 +73,7 @@ decor = ( # (4) Create the cell. -cell = A.cable_cell(morph, decor, labels) +cell = A.cable_cell(lmrf.morphology, decor, labels) # (5) Create a class that inherits from A.recipe diff --git a/python/example/single_cell_nml.py b/python/example/single_cell_nml.py index c6ea8169390b54ef6e61b1d495bc50cbd8ce7ce1..99f74e666aa68177f78244ec7543034c12b90605 100755 --- a/python/example/single_cell_nml.py +++ b/python/example/single_cell_nml.py @@ -4,14 +4,18 @@ from arbor import units as U import pandas as pd import seaborn as sns import sys +from pathlib import Path # Load a cell morphology from an nml file. # Example present here: morph.nml -if len(sys.argv) < 2: - print("No NeuroML file passed to the program") - sys.exit(0) - -filename = sys.argv[1] +if len(sys.argv) == 1: + print("No NML file passed to the program, using default.") + filename = Path(__file__).parent / "morph.nml" +elif len(sys.argv) == 2: + filename = Path(sys.argv[1]) +else: + print("Usage: single_cell_nml.py [NML file name]") + sys.exit(1) # Read the NeuroML morphology from the file. morpho_nml = A.neuroml(filename) @@ -22,12 +26,7 @@ morpho_data = morpho_nml.morphology("m1") # Get the morphology. morpho = morpho_data.morphology -# Get the region label dictionaries associated with the morphology. -morpho_segments = morpho_data.segments() -morpho_named = morpho_data.named_segments() -morpho_groups = morpho_data.groups() - -# Create new label dict with some locsets. +# Create new label dict with some locsets and add to it all the NeuroML dictionaries. labels = A.label_dict( { "stim_site": "(location 1 0.5)", # site for the stimulus, in the middle of branch 1. @@ -35,10 +34,7 @@ labels = A.label_dict( "root": "(root)", # the start of the soma in this morphology is at the root of the cell. } ) -# Add to it all the NeuroML dictionaries. -labels.append(morpho_segments) -labels.append(morpho_named) -labels.append(morpho_groups) +labels.append(morpho_data.labels) # Optional: print out the regions and locsets available in the label dictionary. print("Label dictionary regions: ", labels.regions, "\n") diff --git a/python/example/single_cell_swc.py b/python/example/single_cell_swc.py index 097c11b60be5f61fc95913571c0f013e4e5c2f66..6455850aed6f3dcb24f854025235aa292dcce13e 100755 --- a/python/example/single_cell_swc.py +++ b/python/example/single_cell_swc.py @@ -15,15 +15,21 @@ from arbor import units as U import pandas as pd import seaborn as sns import sys +from pathlib import Path # Load a cell morphology from an swc file. # Example present here: single_cell_detailed.swc -if len(sys.argv) < 2: - print("No SWC file passed to the program") - sys.exit(0) +# (1) Read the morphology from an SWC file. +if len(sys.argv) == 1: + print("No SWC file passed to the program, using default.") + filename = Path(__file__).parent / "single_cell_detailed.swc" +elif len(sys.argv) == 2: + filename = Path(sys.argv[1]) +else: + print("Usage: single_cell_swc.py [SWC file name]") + sys.exit(1) -filename = sys.argv[1] -morpho = A.load_swc_arbor(filename) +morpho = A.load_swc_arbor(filename).morphology # Define the regions and locsets in the model. labels = A.label_dict( diff --git a/python/identifiers.cpp b/python/identifiers.cpp index e58a8a035de0ce86c80c12eb34431326d83cc787..be12c7d345d952cf8eb23200630ea1c8f16be7fc 100644 --- a/python/identifiers.cpp +++ b/python/identifiers.cpp @@ -50,9 +50,14 @@ void register_identifiers(py::module& m) { "Construct a cell_local_label identifier with arguments:\n" " label: The identifier of a group of one or more items on a cell.\n" " policy: The policy for selecting one of possibly multiple items associated with the label.\n") - .def(py::init([](py::tuple t) { - if (py::len(t)!=2) throw std::runtime_error("tuple length != 2"); - return arb::cell_local_label_type{t[0].cast<arb::cell_tag_type>(), t[1].cast<arb::lid_selection_policy>()}; + .def(py::init([](const std::tuple<arb::cell_tag_type, arb::lid_selection_policy>& t) { + return arb::cell_local_label_type{std::get<arb::cell_tag_type>(t), std::get<arb::lid_selection_policy>(t)}; + }), + "Construct a cell_local_label identifier with tuple argument (label, policy):\n" + " label: The identifier of a group of one or more items on a cell.\n" + " policy: The policy for selecting one of possibly multiple items associated with the label.\n") + .def(py::init([](const std::pair<arb::cell_tag_type, arb::lid_selection_policy>& t) { + return arb::cell_local_label_type{std::get<arb::cell_tag_type>(t), std::get<arb::lid_selection_policy>(t)}; }), "Construct a cell_local_label identifier with tuple argument (label, policy):\n" " label: The identifier of a group of one or more items on a cell.\n" @@ -64,8 +69,10 @@ void register_identifiers(py::module& m) { .def("__str__", [](arb::cell_local_label_type m) {return pprintf("<arbor.cell_local_label: label {}, policy {}>", m.tag, m.policy);}) .def("__repr__",[](arb::cell_local_label_type m) {return pprintf("<arbor.cell_local_label: label {}, policy {}>", m.tag, m.policy);}); + py::implicitly_convertible<std::pair<arb::cell_tag_type, arb::lid_selection_policy>, arb::cell_local_label_type>(); + py::implicitly_convertible<std::tuple<arb::cell_tag_type, arb::lid_selection_policy>, arb::cell_local_label_type>(); py::implicitly_convertible<py::tuple, arb::cell_local_label_type>(); - py::implicitly_convertible<py::str, arb::cell_local_label_type>(); + py::implicitly_convertible<arb::cell_tag_type, arb::cell_local_label_type>(); py::class_<arb::cell_global_label_type> cell_global_label_type(m, "cell_global_label", "For global identification of an item.\n\n" @@ -89,13 +96,18 @@ void register_identifiers(py::module& m) { "Construct a cell_global_label identifier with arguments:\n" " gid: The global identifier of the cell.\n" " label: The cell_local_label representing the label and selection policy of an item on the cell.\n") - .def(py::init([](py::tuple t) { - if (py::len(t)!=2) throw std::runtime_error("tuple length != 2"); - return arb::cell_global_label_type{t[0].cast<arb::cell_gid_type>(), t[1].cast<arb::cell_local_label_type>()}; + .def(py::init([](const std::tuple<arb::cell_gid_type, arb::cell_local_label_type>& t) { + return arb::cell_global_label_type{std::get<arb::cell_gid_type>(t), std::get<arb::cell_local_label_type>(t)}; }), "Construct a cell_global_label identifier with tuple argument (gid, label):\n" " gid: The global identifier of the cell.\n" " label: The cell_local_label representing the label and selection policy of an item on the cell.\n") + .def(py::init([](const std::tuple<arb::cell_gid_type, arb::cell_tag_type>& t) { + return arb::cell_global_label_type{std::get<arb::cell_gid_type>(t), std::get<arb::cell_tag_type>(t)}; + }), + "Construct a cell_global_label identifier with tuple argument (gid, label):\n" + " gid: The global identifier of the cell.\n" + " label: The tag of an item on the cell.\n") .def_readwrite("gid", &arb::cell_global_label_type::gid, "The global identifier of the cell.") .def_readwrite("label", &arb::cell_global_label_type::label, @@ -103,6 +115,8 @@ void register_identifiers(py::module& m) { .def("__str__", [](arb::cell_global_label_type m) {return pprintf("<arbor.cell_global_label: gid {}, label ({}, {})>", m.gid, m.label.tag, m.label.policy);}) .def("__repr__",[](arb::cell_global_label_type m) {return pprintf("<arbor.cell_global_label: gid {}, label ({}, {})>", m.gid, m.label.tag, m.label.policy);}); + py::implicitly_convertible<std::tuple<arb::cell_gid_type, arb::cell_local_label_type>, arb::cell_global_label_type>(); + py::implicitly_convertible<std::tuple<arb::cell_gid_type, arb::cell_tag_type>, arb::cell_global_label_type>(); py::implicitly_convertible<py::tuple, arb::cell_global_label_type>(); py::class_<arb::cell_member_type> cell_member(m, "cell_member", diff --git a/python/label_dict.cpp b/python/label_dict.cpp index ff71d1481d84382750a00c64ac65b6ae4b75b731..3ec8713caadbf2cc97088003b1e71dbfc7ad247a 100644 --- a/python/label_dict.cpp +++ b/python/label_dict.cpp @@ -68,13 +68,13 @@ void register_label_dict(py::module& m) { }, py::keep_alive<0, 1>()) .def("append", [](label_dict_proxy& l, const label_dict_proxy& other, const char* prefix) { - l.import(other, prefix); + return l.extend(other, prefix); }, "other"_a, "The label_dict to be imported" "prefix"_a="", "optional prefix appended to the region and locset labels", "Import the entries of a another label dictionary with an optional prefix.") .def("update", [](label_dict_proxy& l, const label_dict_proxy& other) { - l.import(other); + return l.extend(other); }, "other"_a, "The label_dict to be imported" "Import the entries of a another label dictionary.") diff --git a/python/label_dict.hpp b/python/label_dict.hpp index 9d62fd1455bee646b3d0dc75327ba56c23cbac26..1e604512349f56e97327a02b0b0f72456827e2ba 100644 --- a/python/label_dict.hpp +++ b/python/label_dict.hpp @@ -48,11 +48,11 @@ struct label_dict_proxy { return locsets.size() + regions.size() + iexpressions.size(); } - void import(const label_dict_proxy& other, std::string prefix = "") { - dict.import(other.dict, prefix); - + auto& extend(const label_dict_proxy& other, std::string prefix = "") { + dict.extend(other.dict, prefix); clear_cache(); update_cache(); + return *this; } void set(const std::string& name, const std::string& desc) { diff --git a/python/morphology.cpp b/python/morphology.cpp index dacf19ede0fe8f121d404c3c5fc342825e0eb6d1..1a3c305e488ed4e213d79a592cdff3bdd502b00e 100644 --- a/python/morphology.cpp +++ b/python/morphology.cpp @@ -51,12 +51,23 @@ void register_morphology(py::module& m) { py::class_<arb::isometry> isometry(m, "isometry"); py::class_<arb::place_pwlin> place(m, "place_pwlin"); py::class_<arb::segment_tree> segment_tree(m, "segment_tree"); - py::class_<arborio::asc_morphology> asc_morphology(m, "asc_morphology", "The morphology and label dictionary meta-data loaded from a Neurolucida ASCII (.asc) file."); - py::class_<arborio::nml_morphology_data> nml_morph_data(m, "neuroml_morph_data"); py::class_<arborio::neuroml> neuroml(m, "neuroml"); py::class_<arb::mprovider> prov(m, "morphology_provider"); py::class_<arb::msegment> msegment(m, "msegment"); + py::class_<arborio::nml_metadata> nml_meta(m, "nml_metadata"); + py::class_<arborio::asc_metadata> asc_meta(m, + "asc_metadata", + "Neurolucida metadata type: Spines and marker sets."); + py::class_<arborio::loaded_morphology> loaded_morphology(m, + "loaded_morphology", + "The morphology and label dictionary meta-data loaded from file."); + + py::class_<arborio::swc_metadata> swc_meta(m, + "swc_metadata", + "SWC metadata type: empty."); + + // arb::mlocation location .def(py::init( @@ -84,9 +95,8 @@ void register_morphology(py::module& m) { .def(py::init<double, double, double, double>(), "x"_a, "y"_a, "z"_a, "radius"_a, "Create an mpoint object from parameters x, y, z, and radius, specified in µm.") - .def(py::init([](py::tuple t) { - if (py::len(t)!=4) throw std::runtime_error("tuple length != 4"); - return arb::mpoint{t[0].cast<double>(), t[1].cast<double>(), t[2].cast<double>(), t[3].cast<double>()}; }), + .def(py::init([](const std::tuple<double, double, double, double>& t) { + return arb::mpoint{std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t)}; }), "Create an mpoint object from a tuple (x, y, z, radius), specified in µm.") .def_readonly("x", &arb::mpoint::x, "X coordinate [μm].") .def_readonly("y", &arb::mpoint::y, "Y coordinate [μm].") @@ -101,6 +111,7 @@ void register_morphology(py::module& m) { .def("__repr__", [](const arb::mpoint& p) {return util::pprintf("{}", p);}); + py::implicitly_convertible<const std::tuple<double, double, double, double>&, arb::mpoint>(); py::implicitly_convertible<py::tuple, arb::mpoint>(); // arb::msegment @@ -315,18 +326,15 @@ void register_morphology(py::module& m) { return util::pprintf("<arbor.morphology:\n{}>", m); }); - using morph_or_tree = std::variant<arb::segment_tree, arb::morphology>; + py::implicitly_convertible<arb::segment_tree, arb::morphology>(); // Function that creates a morphology/segment_tree from an swc file. // Wraps calls to C++ functions arborio::parse_swc() and arborio::load_swc_arbor(). m.def("load_swc_arbor", - [](py::object fn, bool raw) -> morph_or_tree { + [](py::object fn) -> arborio::loaded_morphology { try { auto contents = util::read_file_or_buffer(fn); auto data = arborio::parse_swc(contents); - if (raw) { - return arborio::load_swc_arbor_raw(data); - } return arborio::load_swc_arbor(data); } catch (arborio::swc_error& e) { @@ -334,8 +342,8 @@ void register_morphology(py::module& m) { throw pyarb_error(util::pprintf("Arbor SWC: parse error: {}", e.what())); } }, - "filename_or_stream"_a, "raw"_a=false, - "Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by Arbor.\n" + "filename_or_stream"_a, + "Generate a morphology/segment_tree from an SWC file following the rules prescribed by Arbor.\n" "Specifically:\n" " * Single-segment somas are disallowed.\n" " * There are no special rules related to somata. They can be one or multiple branches\n" @@ -343,13 +351,10 @@ void register_morphology(py::module& m) { " * A segment is always created between a sample and its parent, meaning there\n" " are no gaps in the resulting morphology."); m.def("load_swc_neuron", - [](py::object fn, bool raw) -> morph_or_tree { + [](py::object fn) -> arborio::loaded_morphology { try { auto contents = util::read_file_or_buffer(fn); auto data = arborio::parse_swc(contents); - if (raw) { - return arborio::load_swc_neuron_raw(data); - } return arborio::load_swc_neuron(data); } catch (arborio::swc_error& e) { @@ -357,33 +362,70 @@ void register_morphology(py::module& m) { throw pyarb_error(util::pprintf("NEURON SWC: parse error: {}", e.what())); } }, - "filename_or_stream"_a, "raw"_a=false, - "Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by NEURON.\n" + "filename_or_stream"_a, + "Generate a morphology from an SWC file following the rules prescribed by NEURON.\n" "See the documentation https://docs.arbor-sim.org/en/latest/fileformat/swc.html\n" "for a detailed description of the interpretation."); // Neurolucida ASCII, or .asc, file format support. - - asc_morphology + py::class_<arborio::asc_color> color(m, + "asc_color", + "Neurolucida color tag."); + color + .def_readonly("red", &arborio::asc_color::r) + .def_readonly("blue", &arborio::asc_color::b) + .def_readonly("green", &arborio::asc_color::g); + + + py::class_<arborio::asc_spine> spine(m, + "asc_spine", + "Neurolucida spine marker."); + spine + .def_readonly("name", &arborio::asc_spine::name) + .def_readonly("location", &arborio::asc_spine::location); + + + py::enum_<arborio::asc_marker> marker(m, + "asc_marker", + "Neurolucida marker type."); + marker + .value("dot", arborio::asc_marker::dot) + .value("cross", arborio::asc_marker::cross) + .value("circle", arborio::asc_marker::circle) + .value("none", arborio::asc_marker::none); + + py::class_<arborio::asc_marker_set> marker_set(m, + "asc_marker_set", + "Neurolucida marker set type."); + + marker_set + .def_readonly("name", &arborio::asc_marker_set::name) + .def_readonly("marker", &arborio::asc_marker_set::marker) + .def_readonly("color", &arborio::asc_marker_set::color) + .def_readonly("locations", &arborio::asc_marker_set::locations); + + asc_meta + .def_readonly("markers", &arborio::asc_metadata::markers) + .def_readonly("spines", &arborio::asc_metadata::spines); + + loaded_morphology .def_readonly("morphology", - &arborio::asc_morphology::morphology, + &arborio::loaded_morphology::morphology, "The cable cell morphology.") .def_readonly("segment_tree", - &arborio::asc_morphology::segment_tree, + &arborio::loaded_morphology::segment_tree, "The raw segment tree.") + .def_readonly("metadata", + &arborio::loaded_morphology::metadata, + "File type specific metadata.") .def_property_readonly("labels", - [](const arborio::asc_morphology& m) {return label_dict_proxy(m.labels);}, - "The four canonical regions are labeled 'soma', 'axon', 'dend' and 'apic'."); - - using asc_morph_or_tree = std::variant<arb::segment_tree, arborio::asc_morphology>; + [](const arborio::loaded_morphology& m) {return label_dict_proxy(m.labels);}, + "Any labels defined by the loaded file."); m.def("load_asc", - [](py::object fn, bool raw) -> asc_morph_or_tree { + [](py::object fn) -> arborio::loaded_morphology { try { auto contents = util::read_file_or_buffer(fn); - if (raw) { - return arborio::parse_asc_string_raw(contents.c_str()); - } return arborio::parse_asc_string(contents.c_str()); } catch (std::exception& e) { @@ -391,31 +433,28 @@ void register_morphology(py::module& m) { throw pyarb_error(util::pprintf("error loading neurolucida asc file: {}", e.what())); } }, - "filename_or_stream"_a, "raw"_a=false, - "Load a morphology or segment_tree (raw=True) and meta data from a Neurolucida ASCII .asc file."); + "filename_or_stream"_a, + "Load a morphology or segment_tree and meta data from a Neurolucida ASCII .asc file."); // arborio::morphology_data - nml_morph_data + nml_meta .def_readonly("cell_id", - &arborio::nml_morphology_data::cell_id, + &arborio::nml_metadata::cell_id, "Cell id, or empty if morphology was taken from a top-level <morphology> element.") .def_readonly("id", - &arborio::nml_morphology_data::id, + &arborio::nml_metadata::id, "Morphology id.") - .def_readonly("morphology", - &arborio::nml_morphology_data::morphology, - "Morphology constructed from a signle NeuroML <morphology> element.") .def("segments", - [](const arborio::nml_morphology_data& md) {return label_dict_proxy(md.segments);}, + [](const arborio::nml_metadata& md) {return label_dict_proxy(md.segments);}, "Label dictionary containing one region expression for each segment id.") .def("named_segments", - [](const arborio::nml_morphology_data& md) {return label_dict_proxy(md.named_segments);}, + [](const arborio::nml_metadata& md) {return label_dict_proxy(md.named_segments);}, "Label dictionary containing one region expression for each name applied to one or more segments.") .def("groups", - [](const arborio::nml_morphology_data& md) {return label_dict_proxy(md.groups);}, + [](const arborio::nml_metadata& md) {return label_dict_proxy(md.groups);}, "Label dictionary containing one region expression for each segmentGroup id.") .def_readonly("group_segments", - &arborio::nml_morphology_data::group_segments, + &arborio::nml_metadata::group_segments, "Map from segmentGroup ids to their corresponding segment ids."); // arborio::neuroml diff --git a/python/pyarb.cpp b/python/pyarb.cpp index a835da796369123fc1cac842a5c0dbd36c5c5497..7f77d673a205e0482f86ac52e975d40b43b6435c 100644 --- a/python/pyarb.cpp +++ b/python/pyarb.cpp @@ -9,6 +9,7 @@ #include <arbor/version.hpp> #include "pyarb.hpp" +#include "arbor/morph/primitives.hpp" // Forward declarations of functions used to register API // types and functions to be exposed to Python. @@ -96,6 +97,7 @@ PYBIND11_MODULE(_arbor, m) { pybind11::register_exception<arb::file_not_found_error>(m, "ArbFileNotFoundError", PyExc_FileNotFoundError); pybind11::register_exception<arb::zero_thread_requested_error>(m, "ArbValueError", PyExc_ValueError); + pybind11::implicitly_convertible<const std::tuple<double, double, double, double>&, arb::mpoint>(); #ifdef ARB_MPI_ENABLED pyarb::register_mpi(m); diff --git a/python/stubs/arbor/__init__.pyi b/python/stubs/arbor/__init__.pyi index 12a28c97542f832931e69a9e5ca229d25441ef21..83f19fd01b9664d59e9add1ba97d832d4ea0bbc2 100644 --- a/python/stubs/arbor/__init__.pyi +++ b/python/stubs/arbor/__init__.pyi @@ -5,7 +5,11 @@ from arbor._arbor import MechCatItemIterator from arbor._arbor import MechCatKeyIterator from arbor._arbor import MechCatValueIterator from arbor._arbor import allen_catalogue -from arbor._arbor import asc_morphology +from arbor._arbor import asc_color +from arbor._arbor import asc_marker +from arbor._arbor import asc_marker_set +from arbor._arbor import asc_metadata +from arbor._arbor import asc_spine from arbor._arbor import axial_resistivity from arbor._arbor import backend from arbor._arbor import bbp_catalogue @@ -81,6 +85,7 @@ from arbor._arbor import load_catalogue from arbor._arbor import load_component from arbor._arbor import load_swc_arbor from arbor._arbor import load_swc_neuron +from arbor._arbor import loaded_morphology from arbor._arbor import location from arbor._arbor import mechanism from arbor._arbor import mechanism_field @@ -94,8 +99,8 @@ from arbor._arbor import morphology_provider from arbor._arbor import mpoint from arbor._arbor import msegment from arbor._arbor import neuroml -from arbor._arbor import neuroml_morph_data from arbor._arbor import neuron_cable_properties +from arbor._arbor import nml_metadata from arbor._arbor import partition_by_group from arbor._arbor import partition_hint from arbor._arbor import partition_load_balance @@ -118,6 +123,7 @@ from arbor._arbor import spike from arbor._arbor import spike_recording from arbor._arbor import spike_source_cell from arbor._arbor import stochastic_catalogue +from arbor._arbor import swc_metadata from arbor._arbor import synapse from arbor._arbor import temperature from arbor._arbor import threshold_detector @@ -134,7 +140,11 @@ __all__ = [ "MechCatKeyIterator", "MechCatValueIterator", "allen_catalogue", - "asc_morphology", + "asc_color", + "asc_marker", + "asc_marker_set", + "asc_metadata", + "asc_spine", "axial_resistivity", "backend", "bbp_catalogue", @@ -211,6 +221,7 @@ __all__ = [ "load_component", "load_swc_arbor", "load_swc_neuron", + "loaded_morphology", "location", "mechanism", "mechanism_field", @@ -226,8 +237,8 @@ __all__ = [ "mpoint", "msegment", "neuroml", - "neuroml_morph_data", "neuron_cable_properties", + "nml_metadata", "partition_by_group", "partition_hint", "partition_load_balance", @@ -250,6 +261,7 @@ __all__ = [ "spike_recording", "spike_source_cell", "stochastic_catalogue", + "swc_metadata", "synapse", "temperature", "threshold_detector", @@ -271,17 +283,17 @@ __config__: dict = { "neuroml": True, "bundled": True, "version": "0.9.1-dev", - "source": "2023-12-08T14:40:50+01:00 327c56d229571dac097e7400a9b5e04fc8d7a514 modified", + "source": "2024-03-01T14:59:23+01:00 dcdfe101f389cb4854ac3d0a067feeb280600c88 modified", "build_config": "DEBUG", "arch": "native", "prefix": "/usr/local", - "python_lib_path": "/opt/homebrew/lib/python3.11/site-packages", + "python_lib_path": "/usr/local/lib/python3.12/site-packages", "binary_path": "bin", "lib_path": "lib", "data_path": "share", "CXX": "/opt/homebrew/bin/clang++", "pybind-version": "2.11.1", - "timestamp": "Jan 2 2024 09:57:33", + "timestamp": "Mar 4 2024 20:56:20", } __version__: str = "0.9.1-dev" mnpos: int = 4294967295 diff --git a/python/stubs/arbor/_arbor/__init__.pyi b/python/stubs/arbor/_arbor/__init__.pyi index a0ae1484c9289fb442e6b12abc4d53bf54aae61a..94e4178308e9d49469609239efb68182f884bc0b 100644 --- a/python/stubs/arbor/_arbor/__init__.pyi +++ b/python/stubs/arbor/_arbor/__init__.pyi @@ -14,7 +14,11 @@ __all__ = [ "MechCatKeyIterator", "MechCatValueIterator", "allen_catalogue", - "asc_morphology", + "asc_color", + "asc_marker", + "asc_marker_set", + "asc_metadata", + "asc_spine", "axial_resistivity", "backend", "bbp_catalogue", @@ -90,6 +94,7 @@ __all__ = [ "load_component", "load_swc_arbor", "load_swc_neuron", + "loaded_morphology", "location", "mechanism", "mechanism_field", @@ -104,8 +109,8 @@ __all__ = [ "mpoint", "msegment", "neuroml", - "neuroml_morph_data", "neuron_cable_properties", + "nml_metadata", "partition_by_group", "partition_hint", "partition_load_balance", @@ -128,6 +133,7 @@ __all__ = [ "spike_recording", "spike_source_cell", "stochastic_catalogue", + "swc_metadata", "synapse", "temperature", "threshold_detector", @@ -155,28 +161,88 @@ class MechCatValueIterator: def __iter__(self) -> MechCatValueIterator: ... def __next__(self) -> mechanism_info: ... -class asc_morphology: +class asc_color: """ - The morphology and label dictionary meta-data loaded from a Neurolucida ASCII (.asc) file. + Neurolucida color tag. """ @property - def labels(self) -> label_dict: - """ - The four canonical regions are labeled 'soma', 'axon', 'dend' and 'apic'. - """ + def blue(self) -> int: ... + @property + def green(self) -> int: ... + @property + def red(self) -> int: ... + +class asc_marker: + """ + Neurolucida marker type. + + Members: + dot + + cross + + circle + + none + """ + + __members__: typing.ClassVar[ + dict[str, asc_marker] + ] # value = {'dot': <asc_marker.dot: 0>, 'cross': <asc_marker.cross: 2>, 'circle': <asc_marker.circle: 1>, 'none': <asc_marker.none: 3>} + circle: typing.ClassVar[asc_marker] # value = <asc_marker.circle: 1> + cross: typing.ClassVar[asc_marker] # value = <asc_marker.cross: 2> + dot: typing.ClassVar[asc_marker] # value = <asc_marker.dot: 0> + none: typing.ClassVar[asc_marker] # value = <asc_marker.none: 3> + def __eq__(self, other: typing.Any) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __init__(self, value: int) -> None: ... + def __int__(self) -> int: ... + def __ne__(self, other: typing.Any) -> bool: ... + def __repr__(self) -> str: ... + def __setstate__(self, state: int) -> None: ... + def __str__(self) -> str: ... @property - def morphology(self) -> morphology: - """ - The cable cell morphology. - """ + def name(self) -> str: ... + @property + def value(self) -> int: ... + +class asc_marker_set: + """ + Neurolucida marker set type. + """ @property - def segment_tree(self) -> segment_tree: - """ - The raw segment tree. - """ + def color(self) -> asc_color: ... + @property + def locations(self) -> list[mpoint]: ... + @property + def marker(self) -> asc_marker: ... + @property + def name(self) -> str: ... + +class asc_metadata: + """ + Neurolucida metadata type: Spines and marker sets. + """ + + @property + def markers(self) -> list[asc_marker_set]: ... + @property + def spines(self) -> list[asc_spine]: ... + +class asc_spine: + """ + Neurolucida spine marker. + """ + + @property + def location(self) -> mpoint: ... + @property + def name(self) -> str: ... class axial_resistivity: """ @@ -199,9 +265,9 @@ class backend: __members__: typing.ClassVar[ dict[str, backend] - ] # value = {'gpu': <backend.gpu: 1>, 'multicore': <backend.multicore: 0>} - gpu: typing.ClassVar[backend] # value = <backend.gpu: 1> - multicore: typing.ClassVar[backend] # value = <backend.multicore: 0> + ] # value = {'gpu': <backend.gpu: 0>, 'multicore': <backend.multicore: 1>} + gpu: typing.ClassVar[backend] # value = <backend.gpu: 0> + multicore: typing.ClassVar[backend] # value = <backend.multicore: 1> def __eq__(self, other: typing.Any) -> bool: ... def __getstate__(self) -> int: ... def __hash__(self) -> int: ... @@ -413,8 +479,6 @@ class cable_global_properties: @property def axial_resistivity(self) -> float | None: ... - @axial_resistivity.setter - def axial_resistivity(self, arg1: float) -> None: ... @property def catalogue(self) -> catalogue: """ @@ -445,16 +509,10 @@ class cable_global_properties: @property def membrane_capacitance(self) -> float | None: ... - @membrane_capacitance.setter - def membrane_capacitance(self, arg1: float) -> None: ... @property def membrane_potential(self) -> float | None: ... - @membrane_potential.setter - def membrane_potential(self, arg1: float) -> None: ... @property def temperature(self) -> float | None: ... - @temperature.setter - def temperature(self, arg1: float) -> None: ... class cable_probe_point_info: """ @@ -595,13 +653,21 @@ class cell_global_label: """ @typing.overload - def __init__(self, arg0: tuple) -> None: + def __init__(self, arg0: tuple[int, cell_local_label]) -> None: """ Construct a cell_global_label identifier with tuple argument (gid, label): gid: The global identifier of the cell. label: The cell_local_label representing the label and selection policy of an item on the cell. """ + @typing.overload + def __init__(self, arg0: tuple[int, str]) -> None: + """ + Construct a cell_global_label identifier with tuple argument (gid, label): + gid: The global identifier of the cell. + label: The tag of an item on the cell. + """ + def __repr__(self) -> str: ... def __str__(self) -> str: ... @property @@ -683,7 +749,15 @@ class cell_local_label: """ @typing.overload - def __init__(self, arg0: tuple) -> None: + def __init__(self, arg0: tuple[str, selection_policy]) -> None: + """ + Construct a cell_local_label identifier with tuple argument (label, policy): + label: The identifier of a group of one or more items on a cell. + policy: The policy for selecting one of possibly multiple items associated with the label. + """ + + @typing.overload + def __init__(self, arg0: tuple[str, selection_policy]) -> None: """ Construct a cell_local_label identifier with tuple argument (label, policy): label: The identifier of a group of one or more items on a cell. @@ -980,10 +1054,10 @@ class decor: def paint( self, region: str, - Vm: units.quantity | str | None = None, - cm: units.quantity | str | None = None, - rL: units.quantity | str | None = None, - tempK: units.quantity | str | None = None, + Vm: units.quantity | tuple[units.quantity, str] | None = None, + cm: units.quantity | tuple[units.quantity, str] | None = None, + rL: units.quantity | tuple[units.quantity, str] | None = None, + tempK: units.quantity | tuple[units.quantity, str] | None = None, ) -> decor: """ Set cable properties on a region. @@ -992,6 +1066,7 @@ class decor: * cm: membrane capacitance [F/m²]. * rL: axial resistivity [Ω·cm]. * tempK: temperature [Kelvin]. + Each value can be given as a plain quantity or a tuple of (quantity, 'scale') where scale is an iexpr. """ @typing.overload @@ -1000,10 +1075,10 @@ class decor: region: str, *, ion: str, - int_con: units.quantity | None = None, - ext_con: units.quantity | None = None, - rev_pot: units.quantity | None = None, - diff: units.quantity | None = None, + int_con: units.quantity | tuple[units.quantity, str] | None = None, + ext_con: units.quantity | tuple[units.quantity, str] | None = None, + rev_pot: units.quantity | tuple[units.quantity, str] | None = None, + diff: units.quantity | tuple[units.quantity, str] | None = None, ) -> decor: """ Set ion species properties conditions on a region. @@ -1012,6 +1087,7 @@ class decor: * rev_pot: reversal potential [mV]. * method: mechanism for calculating reversal potential. * diff: diffusivity [m^2/s]. + Each value can be given as a plain quantity or a tuple of (quantity, 'scale') where scale is an iexpr. """ def paintings( @@ -1561,7 +1637,7 @@ class label_dict: """ @staticmethod - def append(*args, **kwargs) -> None: + def append(*args, **kwargs) -> label_dict: """ Import the entries of a another label dictionary with an optional prefix. """ @@ -1608,7 +1684,7 @@ class label_dict: def items(self) -> typing.Iterator: ... def keys(self) -> typing.Iterator: ... - def update(self, other: label_dict) -> None: + def update(self, other: label_dict) -> label_dict: """ The label_dict to be importedImport the entries of a another label dictionary. """ @@ -1733,6 +1809,35 @@ class lif_probe_metadata: Probe metadata associated with a LIF cell probe. """ +class loaded_morphology: + """ + The morphology and label dictionary meta-data loaded from file. + """ + + @property + def labels(self) -> label_dict: + """ + Any labels defined by the loaded file. + """ + + @property + def metadata(self) -> swc_metadata | asc_metadata | nml_metadata: + """ + File type specific metadata. + """ + + @property + def morphology(self) -> morphology: + """ + The cable cell morphology. + """ + + @property + def segment_tree(self) -> segment_tree: + """ + The raw segment tree. + """ + class location: """ A location on a cable cell. @@ -1893,7 +1998,7 @@ class membrane_potential: Setting the initial membrane voltage. """ - def __init__(self, arg0: units.quantity, arg1: str | None) -> None: ... + def __init__(self, arg0: units.quantity) -> None: ... def __repr__(self) -> str: ... class meter_manager: @@ -1941,40 +2046,6 @@ class morphology: A cell morphology. """ - def __init__(self, arg0: segment_tree) -> None: ... - def __str__(self) -> str: ... - def branch_children(self, i: int) -> list[int]: - """ - The child branches of branch i. - """ - - def branch_parent(self, i: int) -> int: - """ - The parent branch of branch i. - """ - - def branch_segments(self, i: int) -> list[msegment]: - """ - A list of the segments in branch i, ordered from proximal to distal ends of the branch. - """ - - def to_segment_tree(self) -> segment_tree: - """ - Convert this morphology to a segment_tree. - """ - - @property - def empty(self) -> bool: - """ - Whether the morphology is empty. - """ - - @property - def num_branches(self) -> int: - """ - The number of branches in the morphology. - """ - class morphology_provider: def __init__(self, morphology: morphology) -> None: """ @@ -2001,7 +2072,7 @@ class mpoint: """ @typing.overload - def __init__(self, arg0: tuple) -> None: + def __init__(self, arg0: tuple[float, float, float, float]) -> None: """ Create an mpoint object from a tuple (x, y, z, radius), specified in µm. """ @@ -2064,14 +2135,14 @@ class neuroml: def cell_morphology( self, cell_id: str, allow_spherical_root: bool = False - ) -> neuroml_morph_data | None: + ) -> loaded_morphology | None: """ Retrieve nml_morph_data associated with cell_id. """ def morphology( self, morph_id: str, allow_spherical_root: bool = False - ) -> neuroml_morph_data | None: + ) -> loaded_morphology | None: """ Retrieve top-level nml_morph_data associated with morph_id. """ @@ -2081,7 +2152,7 @@ class neuroml: Query top-level standalone morphologies. """ -class neuroml_morph_data: +class nml_metadata: def groups(self) -> label_dict: """ Label dictionary containing one region expression for each segmentGroup id. @@ -2115,12 +2186,6 @@ class neuroml_morph_data: Morphology id. """ - @property - def morphology(self) -> morphology: - """ - Morphology constructed from a signle NeuroML <morphology> element. - """ - class partition_hint: """ Provide a hint on how the cell groups should be partitioned. @@ -2896,6 +2961,11 @@ class spike_source_cell: def __repr__(self) -> str: ... def __str__(self) -> str: ... +class swc_metadata: + """ + SWC metadata type: empty. + """ + class synapse: """ For placing a synaptic mechanism on a locset. @@ -3150,11 +3220,9 @@ def lif_probe_voltage(tag: str) -> probe: Probe specification for LIF cell membrane voltage. """ -def load_asc( - filename_or_stream: typing.Any, raw: bool = False -) -> segment_tree | asc_morphology: +def load_asc(filename_or_stream: typing.Any) -> loaded_morphology: """ - Load a morphology or segment_tree (raw=True) and meta data from a Neurolucida ASCII .asc file. + Load a morphology or segment_tree and meta data from a Neurolucida ASCII .asc file. """ def load_catalogue(arg0: typing.Any) -> catalogue: ... @@ -3163,11 +3231,9 @@ def load_component(filename_or_descriptor: typing.Any) -> cable_component: Load arbor-component (decor, morphology, label_dict, cable_cell) from file. """ -def load_swc_arbor( - filename_or_stream: typing.Any, raw: bool = False -) -> segment_tree | morphology: +def load_swc_arbor(filename_or_stream: typing.Any) -> loaded_morphology: """ - Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by Arbor. + Generate a morphology/segment_tree from an SWC file following the rules prescribed by Arbor. Specifically: * Single-segment somas are disallowed. * There are no special rules related to somata. They can be one or multiple branches @@ -3176,11 +3242,9 @@ def load_swc_arbor( are no gaps in the resulting morphology. """ -def load_swc_neuron( - filename_or_stream: typing.Any, raw: bool = False -) -> segment_tree | morphology: +def load_swc_neuron(filename_or_stream: typing.Any) -> loaded_morphology: """ - Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by NEURON. + Generate a morphology from an SWC file following the rules prescribed by NEURON. See the documentation https://docs.arbor-sim.org/en/latest/fileformat/swc.html for a detailed description of the interpretation. """ diff --git a/python/stubs/arbor/_arbor/py.typed b/python/stubs/arbor/_arbor/py.typed deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/python/stubs/arbor/_arbor/units.pyi b/python/stubs/arbor/_arbor/units.pyi index aa567155747f5974d9ff785e8f8bc23409ea8c40..664b21ae329d1cfd12b9afdcbee758cf05100e0a 100644 --- a/python/stubs/arbor/_arbor/units.pyi +++ b/python/stubs/arbor/_arbor/units.pyi @@ -166,22 +166,22 @@ mM: unit # value = umol/L mS: unit # value = mS mV: unit # value = mV mega: unit # value = 1000000 -micro: unit # value = 9.99999997475242708e-07 -milli: unit # value = 0.00100000004749745131 +micro: unit # value = 9.99999999999999955e-07 +milli: unit # value = 0.00100000000000000002 mm: unit # value = mm mm2: unit # value = mm^2 mol: unit # value = mol ms: unit # value = ms nA: unit # value = nA nF: unit # value = nF -nano: unit # value = 9.99999971718068537e-10 +nano: unit # value = 1e-09 nil: unit # value = nm: unit # value = nm nm2: unit # value = nm^2 ns: unit # value = ns pA: unit # value = pA pF: unit # value = pF -pico: unit # value = 9.999999960041972e-13 +pico: unit # value = 10e-13 rad: unit # value = rad s: unit # value = s uA: unit # value = uA diff --git a/python/stubs/arbor/py.typed b/python/stubs/arbor/py.typed deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/python/stubs/py.typed b/python/stubs/py.typed deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/python/test/unit/test_io.py b/python/test/unit/test_io.py index dec6be09b5edc8398168b0ccceaf5217221d246b..06098818023bbd3418d5bbbe82ca674fb958f884 100644 --- a/python/test/unit/test_io.py +++ b/python/test/unit/test_io.py @@ -5,7 +5,6 @@ import arbor as A from pathlib import Path from tempfile import TemporaryDirectory as TD from io import StringIO -from functools import partial acc = """(arbor-component @@ -153,7 +152,10 @@ class TestAccIo(unittest.TestCase): class TestSwcArborIo(unittest.TestCase): @staticmethod def loaders(): - return (A.load_swc_arbor, partial(A.load_swc_arbor, raw=True)) + return ( + lambda f: A.load_swc_arbor(f).morphology, + lambda f: A.load_swc_arbor(f).segment_tree, + ) def test_stringio(self): load_string(self.loaders(), swc_arbor) @@ -171,7 +173,10 @@ class TestSwcArborIo(unittest.TestCase): class TestSwcNeuronIo(unittest.TestCase): @staticmethod def loaders(): - return (A.load_swc_neuron, partial(A.load_swc_neuron, raw=True)) + return ( + lambda f: A.load_swc_neuron(f).morphology, + lambda f: A.load_swc_neuron(f).segment_tree, + ) def test_stringio(self): load_string(self.loaders(), swc_neuron) @@ -189,7 +194,10 @@ class TestSwcNeuronIo(unittest.TestCase): class TestAscIo(unittest.TestCase): @staticmethod def loaders(): - return (A.load_asc, partial(A.load_asc, raw=True)) + return ( + lambda f: A.load_asc(f).morphology, + lambda f: A.load_asc(f).segment_tree, + ) def test_stringio(self): load_string(self.loaders(), asc) diff --git a/test/ubench/fvm_discretize.cpp b/test/ubench/fvm_discretize.cpp index 7d0c6bb836ea5cdc2fa198edf2533c56bf3ba210..ce9cc719ccaa26006ffb1ccf3c4247ff72979fe1 100644 --- a/test/ubench/fvm_discretize.cpp +++ b/test/ubench/fvm_discretize.cpp @@ -26,7 +26,7 @@ arb::morphology from_swc(const std::string& path) { std::ifstream in(path); if (!in) throw std::runtime_error("could not open "+path); - return arborio::load_swc_arbor(arborio::parse_swc(in)); + return arborio::load_swc_arbor(arborio::parse_swc(in)).morphology; } void run_cv_geom(benchmark::State& state) { diff --git a/test/unit/test_asc.cpp b/test/unit/test_asc.cpp index ec2cad1685e38b351e556c415bfa0b682d7c49f8..f441541b66bd9aead8ed753e7845d254990d55cb 100644 --- a/test/unit/test_asc.cpp +++ b/test/unit/test_asc.cpp @@ -16,7 +16,7 @@ TEST(asc, file_not_found) { // Declare the implementation of the parser that takes a string input namespace arborio { -asc_morphology parse_asc_string(const char* input); +loaded_morphology parse_asc_string(const char* input); } // Test different forms of empty files. @@ -300,3 +300,68 @@ TEST(asc, soma_connection) { EXPECT_EQ(m.branch_segments(7)[0].prox, (arb::mpoint{0,-5, 0, 1})); } } + +// Soma composed of 2 branches, and a dendrite with a bit more interesting branching. +const char *asc_w_spines_and_markers ="((CellBody)\n" +" (0 0 0 4)\n" +" <(11 12 13 14 S1)>\n" +" (Cross (Color Black) (Name \"M1\") (0 0 0 1) (0 0 1 1))\n" +")\n" +"((Dendrite)\n" +" (0 2 0 2)\n" +" (0 5 0 2)\n" +" (Dot (Color Black) (Name \"M2\") (0 0 0 1))\n" +" <(1 2 3 4 S2)>\n" +" (\n" +" (-5 5 0 2)\n" +" (\n" +" (-5 5 0 2)\n" +" |\n" +" (6 5 0 2)\n" +" )\n" +" |\n" +" (6 5 0 2)\n" +" )\n" +" )"; + +TEST(asc, spine) { + { + auto result = arborio::parse_asc_string(asc_w_spines_and_markers); + const auto& m = result.morphology; + EXPECT_EQ(m.num_branches(), 7u); + // Test soma + EXPECT_EQ(m.branch_segments(0)[0].prox, (arb::mpoint{0, 0, 0, 2})); + EXPECT_EQ(m.branch_segments(0)[0].dist, (arb::mpoint{0,-2, 0, 2})); + EXPECT_EQ(m.branch_segments(1)[0].prox, (arb::mpoint{0, 0, 0, 2})); + EXPECT_EQ(m.branch_segments(1)[0].dist, (arb::mpoint{0, 2, 0, 2})); + // Test dendrite proximal ends + EXPECT_EQ(m.branch_segments(2)[0].prox, (arb::mpoint{ 0, 2, 0, 1})); + EXPECT_EQ(m.branch_segments(3)[0].prox, (arb::mpoint{ 0, 5, 0, 1})); + EXPECT_EQ(m.branch_segments(4)[0].prox, (arb::mpoint{-5, 5, 0, 1})); + EXPECT_EQ(m.branch_segments(5)[0].prox, (arb::mpoint{-5, 5, 0, 1})); + EXPECT_EQ(m.branch_segments(6)[0].prox, (arb::mpoint{ 0, 5, 0, 1})); + // Now check metadata + auto d = std::get<arborio::asc_metadata>(result.metadata); + EXPECT_EQ(d.spines.size(), 2); + EXPECT_EQ(d.spines[0].location.x, 11); + EXPECT_EQ(d.spines[0].location.y, 12); + EXPECT_EQ(d.spines[0].location.z, 13); + EXPECT_EQ(d.spines[0].location.radius, 7); + EXPECT_EQ(d.spines[0].name, "S1"); + + EXPECT_EQ(d.spines[1].location.x, 1); + EXPECT_EQ(d.spines[1].location.y, 2); + EXPECT_EQ(d.spines[1].location.z, 3); + EXPECT_EQ(d.spines[1].location.radius, 2); + EXPECT_EQ(d.spines[1].name, "S2"); + + EXPECT_EQ(d.markers.size(), 2); + EXPECT_EQ(d.markers[0].locations.size(), 2); + EXPECT_EQ(d.markers[0].name, "M1"); + EXPECT_EQ(d.markers[0].marker, arborio::asc_marker::cross); + + EXPECT_EQ(d.markers[1].locations.size(), 1); + EXPECT_EQ(d.markers[1].name, "M2"); + EXPECT_EQ(d.markers[1].marker, arborio::asc_marker::dot); + } +} diff --git a/test/unit/test_morphology.cpp b/test/unit/test_morphology.cpp index 5c305d850e108471cf09254c5d11f685cc47b3f9..2104428d726f8cdffa5e5871c564ab41f87f62ef 100644 --- a/test/unit/test_morphology.cpp +++ b/test/unit/test_morphology.cpp @@ -322,7 +322,8 @@ TEST(morphology, swc) { auto swc = arborio::parse_swc(fid); // Build a segmewnt_tree from swc samples. - auto m = arborio::load_swc_arbor(swc); + auto lm = arborio::load_swc_arbor(swc); + const auto& m = lm.morphology; EXPECT_EQ(221u, m.num_branches()); // 219 branches + 2 from divided soma. } #endif diff --git a/test/unit/test_nml_morphology.cpp b/test/unit/test_nml_morphology.cpp index 8c6ecfc039959a3846a4cd50c671107099fc8780..d2d0cfb27089258dd9c862a9e0016d7e74541561 100644 --- a/test/unit/test_nml_morphology.cpp +++ b/test/unit/test_nml_morphology.cpp @@ -78,16 +78,18 @@ R"~( std::sort(c_ids.begin(), c_ids.end()); EXPECT_EQ((svector{"c3", "c4"}), c_ids); - arborio::nml_morphology_data mdata; - - mdata = N.cell_morphology("c4").value(); - EXPECT_EQ("c4", mdata.cell_id); - EXPECT_EQ("m4", mdata.id); - - mdata = N.cell_morphology("c3").value(); - EXPECT_EQ("c3", mdata.cell_id); - EXPECT_EQ("m1", mdata.id); - + { + auto mdata = N.cell_morphology("c4").value(); + auto meta = std::get<arborio::nml_metadata>(mdata.metadata); + EXPECT_EQ("c4", meta.cell_id); + EXPECT_EQ("m4", meta.id); + } + { + auto mdata = N.cell_morphology("c3").value(); + auto meta = std::get<arborio::nml_metadata>(mdata.metadata); + EXPECT_EQ("c3", meta.cell_id); + EXPECT_EQ("m1", meta.id); + } EXPECT_THROW(N.cell_morphology("mr. bobbins").value(), std::bad_optional_access); } @@ -166,8 +168,9 @@ R"~( { auto m1 = N.morphology("m1").value(); + auto d1 = std::get<arborio::nml_metadata>(m1.metadata); label_dict labels; - labels.import(m1.segments, "seg:"); + labels.extend(d1.segments, "seg:"); mprovider P(m1.morphology, labels); EXPECT_TRUE(region_eq(P, reg::named("seg:0"), reg::all())); @@ -178,9 +181,10 @@ R"~( } { - arborio::nml_morphology_data m2 = N.morphology("m2").value(); + auto m2 = N.morphology("m2").value(); + auto d2 = std::get<arborio::nml_metadata>(m2.metadata); label_dict labels; - labels.import(m2.segments, "seg:"); + labels.extend(d2.segments, "seg:"); mprovider P(m2.morphology, labels); mextent seg0_extent = thingify(reg::named("seg:0"), P); @@ -206,9 +210,10 @@ R"~( } { - arborio::nml_morphology_data m3 = N.morphology("m3").value(); + auto m3 = N.morphology("m3").value(); + auto d3 = std::get<arborio::nml_metadata>(m3.metadata); label_dict labels; - labels.import(m3.segments, "seg:"); + labels.extend(d3.segments, "seg:"); mprovider P(m3.morphology, labels); mextent seg0_extent = thingify(reg::named("seg:0"), P); @@ -240,9 +245,10 @@ R"~( } { for (const char* m_name: {"m4", "m5"}) { - arborio::nml_morphology_data m4_or_5 = N.morphology(m_name).value(); + auto m4_or_5 = N.morphology(m_name).value(); + auto d4_or_5 = std::get<arborio::nml_metadata>(m4_or_5.metadata); label_dict labels; - labels.import(m4_or_5.segments, "seg:"); + labels.extend(d4_or_5.segments, "seg:"); mprovider P(m4_or_5.morphology, labels); mextent seg0_extent = thingify(reg::named("seg:0"), P); @@ -329,9 +335,10 @@ R"~( arborio::neuroml N(doc); { - arborio::nml_morphology_data m1 = N.morphology("m1", allow_spherical_root).value(); + auto m1 = N.morphology("m1", allow_spherical_root).value(); + auto d1 = std::get<arborio::nml_metadata>(m1.metadata); label_dict labels; - labels.import(m1.segments, "seg:"); + labels.extend(d1.segments, "seg:"); mprovider P(m1.morphology, labels); EXPECT_TRUE(region_eq(P, reg::branch(0), reg::all())); @@ -358,9 +365,10 @@ R"~( } { // With spherical root _not_ provided, treat it just as a simple zero-length segment. - arborio::nml_morphology_data m1 = N.morphology("m1", none).value(); + auto m1 = N.morphology("m1", none).value(); + auto d1 = std::get<arborio::nml_metadata>(m1.metadata); label_dict labels; - labels.import(m1.segments, "seg:"); + labels.extend(d1.segments, "seg:"); mprovider P(m1.morphology, labels); EXPECT_TRUE(region_eq(P, reg::branch(0), reg::all())); @@ -371,9 +379,10 @@ R"~( EXPECT_EQ(p0, G.at(mlocation{0, 1})); } { - arborio::nml_morphology_data m2 = N.morphology("m2", allow_spherical_root).value(); + auto m2 = N.morphology("m2", allow_spherical_root).value(); + auto d2 = std::get<arborio::nml_metadata>(m2.metadata); label_dict labels; - labels.import(m2.segments, "seg:"); + labels.extend(d2.segments, "seg:"); mprovider P(m2.morphology, labels); EXPECT_TRUE(region_eq(P, reg::branch(0), reg::all())); @@ -387,9 +396,10 @@ R"~( (p0==points[1] && p1==points[0])); } { - arborio::nml_morphology_data m3 = N.morphology("m3", allow_spherical_root).value(); + auto m3 = N.morphology("m3", allow_spherical_root).value(); + auto d3 = std::get<arborio::nml_metadata>(m3.metadata); label_dict labels; - labels.import(m3.segments, "seg:"); + labels.extend(d3.segments, "seg:"); mprovider P(m3.morphology, labels); place_pwlin G(P.morphology()); @@ -409,9 +419,10 @@ R"~( EXPECT_EQ(p2, s1d); } { - arborio::nml_morphology_data m4 = N.morphology("m4", allow_spherical_root).value(); + auto m4 = N.morphology("m4", allow_spherical_root).value(); + auto d4 = std::get<arborio::nml_metadata>(m4.metadata); label_dict labels; - labels.import(m4.segments, "seg:"); + labels.extend(d4.segments, "seg:"); mprovider P(m4.morphology, labels); place_pwlin G(P.morphology()); @@ -593,22 +604,16 @@ R"~( using reg::named; { - arborio::nml_morphology_data m1 = N.morphology("m1").value(); - label_dict labels; - labels.import(m1.segments); - labels.import(m1.groups); - mprovider P(m1.morphology, labels); + auto m1 = N.morphology("m1").value(); + mprovider P(m1.morphology, m1.labels); EXPECT_TRUE(region_eq(P, named("group-a"), named("0"))); EXPECT_TRUE(region_eq(P, named("group-b"), named("2"))); EXPECT_TRUE(region_eq(P, named("group-c"), join(named("2"), named("1")))); } { - arborio::nml_morphology_data m2 = N.morphology("m2").value(); - label_dict labels; - labels.import(m2.segments); - labels.import(m2.groups); - mprovider P(m2.morphology, labels); + auto m2 = N.morphology("m2").value(); + mprovider P(m2.morphology, m2.labels); EXPECT_TRUE(region_eq(P, named("group-a"), join(named("0"), named("2")))); EXPECT_TRUE(region_eq(P, named("group-c"), join(named("0"), named("1"), named("2")))); @@ -762,11 +767,8 @@ R"~( arborio::neuroml N(doc); - arborio::nml_morphology_data m1 = N.morphology("m1").value(); - label_dict labels; - labels.import(m1.segments); - labels.import(m1.groups); - mprovider P(m1.morphology, labels); + auto m1 = N.morphology("m1").value(); + mprovider P(m1.morphology, m1.labels); // Note: paths/subTrees respect segment parent–child relationships, // not morphological distality. diff --git a/test/unit/test_swcio.cpp b/test/unit/test_swcio.cpp index dacd87805ea07a38df0f5a81f67dfdedb416d34b..918f735d7ca90ab4fe8d553014ba7e01630058d3 100644 --- a/test/unit/test_swcio.cpp +++ b/test/unit/test_swcio.cpp @@ -270,7 +270,8 @@ TEST(swc_parser, arbor_compliant) { {7, 3, p4.x, p4.y, p4.z, p4.radius, 4} }; - auto morpho = load_swc_arbor(swc); + auto loaded = load_swc_arbor(swc); + const auto& morpho = loaded.morphology; ASSERT_EQ(3u, morpho.num_branches()); EXPECT_EQ(mnpos, morpho.branch_parent(0)); @@ -315,7 +316,8 @@ TEST(swc_parser, arbor_compliant) { {4, 3, p3.x, p3.y, p3.z, p3.radius, 2}, }; - auto morpho = load_swc_arbor(swc); + auto loaded = load_swc_arbor(swc); + auto morpho = loaded.morphology; ASSERT_EQ(2u, morpho.num_branches()); EXPECT_EQ(mnpos, morpho.branch_parent(0)); @@ -377,7 +379,9 @@ TEST(swc_parser, neuron_compliant) { std::vector<swc_record> swc{ {1, 1, p0.x, p0.y, p0.z, p0.radius, -1} }; - auto morpho = load_swc_neuron(swc); + + auto loaded = load_swc_neuron(swc); + const auto& morpho = loaded.morphology; mpoint prox{p0.x-p0.radius, p0.y, p0.z, p0.radius}; mpoint dist{p0.x+p0.radius, p0.y, p0.z, p0.radius}; @@ -406,7 +410,8 @@ TEST(swc_parser, neuron_compliant) { {1, 1, p0.x, p0.y, p0.z, p0.radius, -1}, {2, 1, p1.x, p1.y, p1.z, p1.radius, 1} }; - auto morpho = load_swc_neuron(swc); + auto loaded = load_swc_arbor(swc); + const auto& morpho = loaded.morphology; ASSERT_EQ(1u, morpho.num_branches()); @@ -431,7 +436,9 @@ TEST(swc_parser, neuron_compliant) { {2, 1, p1.x, p1.y, p1.z, p1.radius, 1}, {3, 1, p2.x, p2.y, p2.z, p2.radius, 2} }; - auto morpho = load_swc_neuron(swc); + + auto loaded = load_swc_neuron(swc); + const auto& morpho = loaded.morphology; ASSERT_EQ(1u, morpho.num_branches()); @@ -466,7 +473,8 @@ TEST(swc_parser, neuron_compliant) { {10, 1, p4.x, p4.y, p4.z, p4.radius, 8}, {12, 1, p5.x, p5.y, p5.z, p5.radius, 10} }; - auto morpho = load_swc_neuron(swc); + auto loaded = load_swc_neuron(swc); + const auto& morpho = loaded.morphology; ASSERT_EQ(1u, morpho.num_branches()); @@ -507,7 +515,8 @@ TEST(swc_parser, neuron_compliant) { {2, 3, p1.x, p1.y, p1.z, p1.radius, 1}, {3, 3, p2.x, p2.y, p2.z, p2.radius, 2} }; - auto morpho = load_swc_neuron(swc); + auto loaded = load_swc_neuron(swc); + const auto& morpho = loaded.morphology; mpoint prox{-10, 0, 0, 10}; mpoint dist{ 10, 0, 0, 10}; @@ -548,7 +557,8 @@ TEST(swc_parser, neuron_compliant) { {23, 1, p0.x, p0.y, p0.z, p0.radius, -1}, {83, 3, p1.x, p1.y, p1.z, p1.radius, 23} }; - auto morpho = load_swc_neuron(swc); + auto loaded = load_swc_neuron(swc); + const auto& morpho = loaded.morphology; mpoint prox{-10, 0, 0, 10}; mpoint dist{ 10, 0, 0, 10}; @@ -592,7 +602,8 @@ TEST(swc_parser, neuron_compliant) { {3, 3, p2.x, p2.y, p2.z, p2.radius, 2}, {4, 3, p3.x, p3.y, p3.z, p3.radius, 3} }; - auto morpho = load_swc_neuron(swc); + auto loaded = load_swc_neuron(swc); + const auto& morpho = loaded.morphology; ASSERT_EQ(1u, morpho.num_branches()); @@ -658,7 +669,8 @@ TEST(swc_parser, neuron_compliant) { {18, 3, -8, 15, 0, 1, 17} }; - auto morpho = load_swc_neuron(swc); + auto loaded = load_swc_neuron(swc); + const auto& morpho = loaded.morphology; ASSERT_EQ(10u, morpho.num_branches()); @@ -739,6 +751,7 @@ TEST(swc_parser, neuron_compliant) { EXPECT_EQ(p17, segs_9[0].dist); } } + TEST(swc_parser, not_neuron_compliant) { using namespace arborio; {