diff --git a/arborio/CMakeLists.txt b/arborio/CMakeLists.txt index 4caba22857fb6fc0f77cb1dc5d45fa8f51d036f7..14522ad6253fad35e13f61b5715999e2644f2a3a 100644 --- a/arborio/CMakeLists.txt +++ b/arborio/CMakeLists.txt @@ -5,9 +5,9 @@ set(arborio-sources ) if(ARB_WITH_NEUROML) list(APPEND arborio-sources - arbornml.cpp - parse_morphology.cpp - with_xml.cpp + neuroml.cpp + nml_parse_morphology.cpp + xml.cpp xmlwrap.cpp ) find_package(LibXml2 REQUIRED) diff --git a/arborio/include/arborio/arbornml.hpp b/arborio/include/arborio/neuroml.hpp similarity index 75% rename from arborio/include/arborio/arbornml.hpp rename to arborio/include/arborio/neuroml.hpp index 0f3d939e5049450245754201889eeabcd12abfda..401841d37093e0e4cda891785fbdd07de7d4981e 100644 --- a/arborio/include/arborio/arbornml.hpp +++ b/arborio/include/arborio/neuroml.hpp @@ -20,21 +20,14 @@ struct neuroml_exception: std::runtime_error { {} }; -// Generic XML error (as reported by libxml2). -struct xml_error: neuroml_exception { - xml_error(const std::string& xml_error_msg, unsigned line = 0); - std::string xml_error_msg; - unsigned line; -}; - // Can't parse NeuroML if we don't have a document. -struct no_document: neuroml_exception { - no_document(); +struct nml_no_document: neuroml_exception { + nml_no_document(); }; // Generic error parsing NeuroML data. -struct parse_error: neuroml_exception { - parse_error(const std::string& error_msg, unsigned line = 0); +struct nml_parse_error: neuroml_exception { + nml_parse_error(const std::string& error_msg, unsigned line = 0); std::string error_msg; unsigned line; }; @@ -42,24 +35,24 @@ struct parse_error: neuroml_exception { // NeuroML morphology error: improper segment data, e.g. bad id specification, // segment parent does not exist, fractionAlong is out of bounds, missing // required <proximal> data. -struct bad_segment: neuroml_exception { - bad_segment(unsigned long long segment_id, unsigned line = 0); +struct nml_bad_segment: neuroml_exception { + nml_bad_segment(unsigned long long segment_id, unsigned line = 0); unsigned long long segment_id; unsigned line; }; // NeuroML morphology error: improper segmentGroup data, e.g. malformed // element data, missing referenced segments or groups, etc. -struct bad_segment_group: neuroml_exception { - bad_segment_group(const std::string& group_id, unsigned line = 0); +struct nml_bad_segment_group: neuroml_exception { + nml_bad_segment_group(const std::string& group_id, unsigned line = 0); std::string group_id; unsigned line; }; // A segment or segmentGroup ultimately refers to itself via `parent` // or `include` elements respectively. -struct cyclic_dependency: neuroml_exception { - cyclic_dependency(const std::string& id, unsigned line = 0); +struct nml_cyclic_dependency: neuroml_exception { + nml_cyclic_dependency(const std::string& id, unsigned line = 0); std::string id; unsigned line; }; @@ -70,7 +63,7 @@ struct cyclic_dependency: neuroml_exception { // Note: segment id values are interpreted as unsigned long long values; // parsing larger segment ids will throw an exception. -struct morphology_data { +struct nml_morphology_data { // Cell id, or empty if morphology was taken from a top-level <morphology> element. std::optional<std::string> cell_id; @@ -115,8 +108,8 @@ struct neuroml { // Parse and retrieve top-level morphology or morphology associated with a cell. // Return nullopt if not found. - std::optional<morphology_data> morphology(const std::string& morph_id) const; - std::optional<morphology_data> cell_morphology(const std::string& cell_id) const; + std::optional<nml_morphology_data> morphology(const std::string& morph_id) const; + std::optional<nml_morphology_data> cell_morphology(const std::string& cell_id) const; ~neuroml(); diff --git a/arborio/include/arborio/with_xml.hpp b/arborio/include/arborio/xml.hpp similarity index 61% rename from arborio/include/arborio/with_xml.hpp rename to arborio/include/arborio/xml.hpp index d176daa00413a182869fcec94aa61d12de45e42a..702827c3c9cdd57b48389ac7400f091ace91130c 100644 --- a/arborio/include/arborio/with_xml.hpp +++ b/arborio/include/arborio/xml.hpp @@ -1,13 +1,25 @@ #pragma once +#include <stdexcept> +#include <string> + +// XML related interfaces deriving from the underlying XML implementation library. + +namespace arborio { + +// Generic XML error (as reported by libxml2). +struct xml_error: std::runtime_error { + xml_error(const std::string& xml_error_msg, unsigned line = 0); + std::string xml_error_msg; + unsigned line; +}; + // Wrap initialization and cleanup of libxml2 library. // // Use of `with_xml` is only necessary if arborio is being // used in a multithreaded context and the client code is // not managing libxml2 initialization and cleanup. -namespace arborio { - struct with_xml { with_xml(); ~with_xml(); diff --git a/arborio/neurolucida.cpp b/arborio/neurolucida.cpp index 705082d51b6443dcd5b5ea142712654b1a9b96f1..4bccd0dbb16f1c40e3472b89a9899fa00b99b6dc 100644 --- a/arborio/neurolucida.cpp +++ b/arborio/neurolucida.cpp @@ -26,6 +26,10 @@ asc_unsupported::asc_unsupported(const std::string& error_msg): message(error_msg) {} + +namespace { +// Parse functions and internal representations kept in unnamed namespace. + struct parse_error { struct cpp_info { const char* file; @@ -513,6 +517,8 @@ parse_hopefully<sub_tree> parse_sub_tree(asc::lexer& L) { return tree; } +} // namespace + // Perform the parsing of the input as a string. asc_morphology parse_asc_string(const char* input) { diff --git a/arborio/arbornml.cpp b/arborio/neuroml.cpp similarity index 76% rename from arborio/arbornml.cpp rename to arborio/neuroml.cpp index 0ee14b0814f425eed81c281da00628660ec05fc9..63e1bfc0943ce3cb047a2f147133be29a056a8a3 100644 --- a/arborio/arbornml.cpp +++ b/arborio/neuroml.cpp @@ -3,37 +3,33 @@ #include <string> #include <vector> -#include <arborio/arbornml.hpp> +#include <arborio/neuroml.hpp> -#include "parse_morphology.hpp" +#include "nml_parse_morphology.hpp" #include "xmlwrap.hpp" using std::optional; using std::nullopt; +using namespace arborio::xmlwrap; + namespace arborio { static std::string fmt_error(const char* prefix, const std::string& err, unsigned line) { return prefix + (line==0? err: "line " + std::to_string(line) + ": " + err); } -xml_error::xml_error(const std::string& xml_error_msg, unsigned line): - neuroml_exception(fmt_error("xml error: ", xml_error_msg, line)), - xml_error_msg(xml_error_msg), - line(line) -{} - -no_document::no_document(): +nml_no_document::nml_no_document(): neuroml_exception("no NeuroML document to parse") {} -parse_error::parse_error(const std::string& error_msg, unsigned line): +nml_parse_error::nml_parse_error(const std::string& error_msg, unsigned line): neuroml_exception(fmt_error("parse error: ", error_msg, line)), error_msg(error_msg), line(line) {} -bad_segment::bad_segment(unsigned long long segment_id, unsigned line): +nml_bad_segment::nml_bad_segment(unsigned long long segment_id, unsigned line): neuroml_exception( fmt_error( "bad morphology segment: ", @@ -43,7 +39,7 @@ bad_segment::bad_segment(unsigned long long segment_id, unsigned line): line(line) {} -bad_segment_group::bad_segment_group(const std::string& group_id, unsigned line): +nml_bad_segment_group::nml_bad_segment_group(const std::string& group_id, unsigned line): neuroml_exception( fmt_error( "bad morphology segmentGroup: ", @@ -53,7 +49,7 @@ bad_segment_group::bad_segment_group(const std::string& group_id, unsigned line) line(line) {} -cyclic_dependency::cyclic_dependency(const std::string& id, unsigned line): +nml_cyclic_dependency::nml_cyclic_dependency(const std::string& id, unsigned line): neuroml_exception( fmt_error( "cyclic dependency: ", @@ -74,7 +70,7 @@ struct neuroml_impl { } xml_xpathctx make_context() const { - if (!doc) throw no_document{}; + if (!doc) throw nml_no_document{}; auto ctx = xpath_context(doc); ctx.register_ns("nml", "http://www.neuroml.org/schema/neuroml2"); @@ -120,15 +116,15 @@ std::vector<std::string> neuroml::morphology_ids() const { return result; } -optional<morphology_data> neuroml::morphology(const std::string& morph_id) const { +optional<nml_morphology_data> neuroml::morphology(const std::string& morph_id) const { xml_error_scope err; auto ctx = impl_->make_context(); auto matches = ctx.query("//nml:neuroml/nml:morphology[@id="+xpath_escape(morph_id)+"]"); - return matches.empty()? nullopt: optional(parse_morphology_element(ctx, matches[0])); + return matches.empty()? nullopt: optional(nml_parse_morphology_element(ctx, matches[0])); } -optional<morphology_data> neuroml::cell_morphology(const std::string& cell_id) const { +optional<nml_morphology_data> neuroml::cell_morphology(const std::string& cell_id) const { xml_error_scope err; auto ctx = impl_->make_context(); auto matches = ctx.query( @@ -137,7 +133,7 @@ optional<morphology_data> neuroml::cell_morphology(const std::string& cell_id) c if (matches.empty()) return nullopt; - morphology_data M = parse_morphology_element(ctx, matches[0]); + nml_morphology_data M = nml_parse_morphology_element(ctx, matches[0]); M.cell_id = cell_id; return M; } diff --git a/arborio/parse_morphology.cpp b/arborio/nml_parse_morphology.cpp similarity index 82% rename from arborio/parse_morphology.cpp rename to arborio/nml_parse_morphology.cpp index d7adc67a7a1edc28aa64d0a601322e01ba37ce8b..baff719a928b88cfde9e1c1d2bed1fb22897cfa2 100644 --- a/arborio/parse_morphology.cpp +++ b/arborio/nml_parse_morphology.cpp @@ -14,9 +14,9 @@ #include <arbor/morph/stitch.hpp> #include <arbor/util/expected.hpp> -#include <arborio/arbornml.hpp> +#include <arborio/neuroml.hpp> -#include "parse_morphology.hpp" +#include "nml_parse_morphology.hpp" #include "xmlwrap.hpp" using std::optional; @@ -24,8 +24,14 @@ using arb::region; using arb::util::expected; using arb::util::unexpected; +using namespace std::literals; +using namespace arborio::xmlwrap; + namespace arborio { +// Implementation utility classes: + +namespace { // Box is a container of size 0 or 1. template <typename X> @@ -126,6 +132,25 @@ expected<std::vector<std::size_t>, cycle_detected> topological_sort(std::size_t return depth; } +template <typename T> +struct propx { + explicit propx(xml_node n, const char* attr, optional<T> dflt = std::nullopt) { + if (auto x = n.prop<T>(attr, dflt)) { + result_ = std::move(x.value()); + } + else { + throw nml_parse_error(x.error().error, x.error().line); + } + } + + operator T() && { return std::move(result_); } + operator T() const& { return result_; } + T result_; +}; + +} // namespace + + // Internal representations of NeuroML segment and segmentGroup data: struct neuroml_segment { @@ -201,14 +226,14 @@ struct neuroml_segment_tree { // Build index, throw on duplicate id. for (std::size_t i = 0; i<n_seg; ++i) { if (!index_.insert({segments_[i].id, i}).second) { - throw bad_segment(segments_[i].id, segments_[i].line); + throw nml_bad_segment(segments_[i].id, segments_[i].line); } } // Check parent relationship is sound. for (const auto& s: segments_) { if (s.parent_id && !index_.count(*s.parent_id)) { - throw bad_segment(s.id, s.line); // No such parent id. + throw nml_bad_segment(s.id, s.line); // No such parent id. } } @@ -225,12 +250,12 @@ struct neuroml_segment_tree { } else { const auto& seg = segments_[depths.error().index]; - throw cyclic_dependency(nl_to_string(seg.id), seg.line); + throw nml_cyclic_dependency(nl_to_string(seg.id), seg.line); } std::sort(segments_.begin(), segments_.end(), [](auto& a, auto& b) { return a.tdepth<b.tdepth; }); // Check for multiple roots: - if (n_seg>1 && segments_[1].tdepth==0) throw bad_segment(segments_[1].id, segments_[1].line); + if (n_seg>1 && segments_[1].tdepth==0) throw nml_bad_segment(segments_[1].id, segments_[1].line); // Update index: for (std::size_t i = 0; i<n_seg; ++i) { @@ -251,7 +276,7 @@ private: std::unordered_map<non_negative, std::vector<non_negative>> children_; }; -std::unordered_map<std::string, std::vector<non_negative>> evaluate_segment_groups( +static std::unordered_map<std::string, std::vector<non_negative>> evaluate_segment_groups( std::vector<neuroml_segment_group_info> groups, const neuroml_segment_tree& segtree) { @@ -300,7 +325,7 @@ std::unordered_map<std::string, std::vector<non_negative>> evaluate_segment_grou } } catch (...) { - throw bad_segment_group(g.id, line); + throw nml_bad_segment_group(g.id, line); } } @@ -308,7 +333,7 @@ std::unordered_map<std::string, std::vector<non_negative>> evaluate_segment_grou std::unordered_map<std::string, std::size_t> index; for (std::size_t i = 0; i<n_group; ++i) { if (!index.insert({groups[i].id, i}).second) { - throw bad_segment_group(groups[i].id, groups[i].line); + throw nml_bad_segment_group(groups[i].id, groups[i].line); } } @@ -318,7 +343,7 @@ std::unordered_map<std::string, std::vector<non_negative>> evaluate_segment_grou const auto& includes = groups[i].includes; index_to_included_indices[i].reserve(includes.size()); for (auto& id: includes) { - if (!index.count(id)) throw bad_segment_group(groups[i].id, groups[i].line); + if (!index.count(id)) throw nml_bad_segment_group(groups[i].id, groups[i].line); index_to_included_indices[i].push_back(index.at(id)); } } @@ -332,7 +357,7 @@ std::unordered_map<std::string, std::vector<non_negative>> evaluate_segment_grou } else { const auto& group = groups[depths.error().index]; - throw cyclic_dependency(group.id, group.line); + throw nml_cyclic_dependency(group.id, group.line); } // Accumulate included group segments, following topological order. @@ -363,7 +388,7 @@ std::unordered_map<std::string, std::vector<non_negative>> evaluate_segment_grou return group_seg_map; } -arb::stitched_morphology construct_morphology(const neuroml_segment_tree& segtree) { +static arb::stitched_morphology construct_morphology(const neuroml_segment_tree& segtree) { arb::stitch_builder builder; if (segtree.empty()) return arb::stitched_morphology{builder}; @@ -384,9 +409,9 @@ arb::stitched_morphology construct_morphology(const neuroml_segment_tree& segtre return arb::stitched_morphology(std::move(builder)); } -morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph) { - morphology_data M; - M.id = morph.prop<std::string>("id", std::string{}); +nml_morphology_data nml_parse_morphology_element(xml_xpathctx ctx, xml_node morph) { + nml_morphology_data M; + M.id = propx<std::string>(morph, "id", ""s); std::vector<neuroml_segment> segments; @@ -401,47 +426,47 @@ morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph) { try { seg.id = -1; - seg.id = n.prop<non_negative>("id"); - std::string name = n.prop<std::string>("name", std::string{}); + seg.id = propx<non_negative>(n, "id"); + std::string name = propx<std::string>(n, "name", ""s); auto result = ctx.query(n, q_parent); if (!result.empty()) { line = result[0].line(); - seg.parent_id = result[0].prop<non_negative>("segment"); - seg.along = result[0].prop<double>("fractionAlong", 1.0); + seg.parent_id = propx<non_negative>(result[0], "segment"); + seg.along = propx<double>(result[0], "fractionAlong", 1.0); } result = ctx.query(n, q_proximal); if (!result.empty()) { line = result[0].line(); - double x = result[0].prop<double>("x"); - double y = result[0].prop<double>("y"); - double z = result[0].prop<double>("z"); - double diameter = result[0].prop<double>("diameter"); - if (diameter<0) throw bad_segment(seg.id, n.line()); + double x = propx<double>(result[0], "x"); + double y = propx<double>(result[0], "y"); + double z = propx<double>(result[0], "z"); + double diameter = propx<double>(result[0], "diameter"); + if (diameter<0) throw nml_bad_segment(seg.id, n.line()); seg.proximal = arb::mpoint{x, y, z, diameter/2}; } - if (!seg.parent_id && !seg.proximal) throw bad_segment(seg.id, n.line()); + if (!seg.parent_id && !seg.proximal) throw nml_bad_segment(seg.id, n.line()); result = ctx.query(n, q_distal); if (!result.empty()) { line = result[0].line(); - double x = result[0].prop<double>("x"); - double y = result[0].prop<double>("y"); - double z = result[0].prop<double>("z"); - double diameter = result[0].prop<double>("diameter"); - if (diameter<0) throw bad_segment(seg.id, n.line()); + double x = propx<double>(result[0], "x"); + double y = propx<double>(result[0], "y"); + double z = propx<double>(result[0], "z"); + double diameter = propx<double>(result[0], "diameter"); + if (diameter<0) throw nml_bad_segment(seg.id, n.line()); seg.distal = arb::mpoint{x, y, z, diameter/2}; } else { - throw bad_segment(seg.id, n.line()); + throw nml_bad_segment(seg.id, n.line()); } } - catch (parse_error& e) { - throw bad_segment(seg.id, line); + catch (nml_parse_error& e) { + throw nml_bad_segment(seg.id, line); } seg.line = n.line(); @@ -468,16 +493,16 @@ morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph) { int line = n.line(); // for error context! try { - group.id = n.prop<std::string>("id"); + group.id = propx<std::string>(n, "id"); for (auto elem: ctx.query(n, q_member)) { line = elem.line(); - auto seg_id = elem.prop<non_negative>("segment"); - if (!segtree.contains(seg_id)) throw bad_segment_group(group.id, line); - group.segments.push_back(elem.prop<non_negative>("segment")); + auto seg_id = propx<non_negative>(elem, "segment"); + if (!segtree.contains(seg_id)) throw nml_bad_segment_group(group.id, line); + group.segments.push_back(propx<non_negative>(elem, "segment")); } for (auto elem: ctx.query(n, q_include)) { line = elem.line(); - group.includes.push_back(elem.prop<std::string>("segmentGroup")); + group.includes.push_back(propx<std::string>(elem, "segmentGroup")); } // Treat `<path>` and `<subTree>` identically: @@ -490,11 +515,11 @@ morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph) { sub.line = line; if (!froms.empty()) { line = froms[0].line(); - sub.from = froms[0].template prop<non_negative>("segment"); + sub.from = propx<non_negative>(froms[0], "segment"); } if (!tos.empty()) { line = tos[0].line(); - sub.to = tos[0].template prop<non_negative>("segment"); + sub.to = propx<non_negative>(tos[0], "segment"); } return sub; @@ -507,8 +532,8 @@ morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph) { group.subtrees.push_back(parse_subtree_elem(elem)); } } - catch (parse_error& e) { - throw bad_segment_group(group.id, line); + catch (nml_parse_error& e) { + throw nml_bad_segment_group(group.id, line); } group.line = n.line(); diff --git a/arborio/nml_parse_morphology.hpp b/arborio/nml_parse_morphology.hpp new file mode 100644 index 0000000000000000000000000000000000000000..3d4b5792c120d458d3c183873ff6a2793ba05240 --- /dev/null +++ b/arborio/nml_parse_morphology.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <arborio/neuroml.hpp> +#include "xmlwrap.hpp" + +namespace arborio { + +nml_morphology_data nml_parse_morphology_element(xmlwrap::xml_xpathctx ctx, xmlwrap::xml_node morph); + +} // namespace arborio diff --git a/arborio/parse_morphology.hpp b/arborio/parse_morphology.hpp deleted file mode 100644 index 0ced56cd683067a4ebc12fc94c9fafdd82fa6354..0000000000000000000000000000000000000000 --- a/arborio/parse_morphology.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include <arborio/arbornml.hpp> -#include "xmlwrap.hpp" - -namespace arborio { - -morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph); - -} // namespace arborio diff --git a/arborio/with_xml.cpp b/arborio/xml.cpp similarity index 53% rename from arborio/with_xml.cpp rename to arborio/xml.cpp index 269c174d31b6da67a4f2fd841150ad5065079f8a..6e8a19e4a37722290405dc1e39e2dd6b2e928f87 100644 --- a/arborio/with_xml.cpp +++ b/arborio/xml.cpp @@ -1,9 +1,20 @@ -#include <arborio/with_xml.hpp> +#include <stdexcept> +#include <string> #include <libxml/parser.h> +#include <arborio/xml.hpp> + +// Implementations for exposed libxml2 interfaces. + namespace arborio { +xml_error::xml_error(const std::string& xml_error_msg, unsigned line): + std::runtime_error(std::string("xml error: ") + (line? "line " + std::to_string(line): "") + xml_error_msg), + xml_error_msg(xml_error_msg), + line(line) +{} + with_xml::with_xml(): run_cleanup_(true) { // Initialize before any multithreaded access by library or client code. xmlInitParser(); diff --git a/arborio/xmlwrap.cpp b/arborio/xmlwrap.cpp index 126f2ca6f1ff740cfbc4215c7e63e2a9f1efcafb..6f791b2b922d4d34d1f498b014c9733c3093dd49 100644 --- a/arborio/xmlwrap.cpp +++ b/arborio/xmlwrap.cpp @@ -15,6 +15,7 @@ #include "xmlwrap.hpp" namespace arborio { +namespace xmlwrap { namespace detail { @@ -123,4 +124,5 @@ xml_error_scope::~xml_error_scope() { xmlStructuredErrorContext = structured_context_; } +} // namespace xmlwrap } // namespace arborio diff --git a/arborio/xmlwrap.hpp b/arborio/xmlwrap.hpp index 0993b9ba0665d5f7002c614ef416a69cea007667..a95942a9171af76700ace4b66d033a7ee377026c 100644 --- a/arborio/xmlwrap.hpp +++ b/arborio/xmlwrap.hpp @@ -14,9 +14,16 @@ #include <libxml/xpath.h> #include <libxml/xpathInternals.h> -#include "arborio/arbornml.hpp" +#include <arbor/util/expected.hpp> +#include <arborio/xml.hpp> namespace arborio { +namespace xmlwrap { + +struct bad_property { + std::string error; + unsigned line = 0; +}; // `non_negative` represents the corresponding constraint in the schema, which // can mean any arbitrarily large non-negtative integer value. @@ -101,14 +108,18 @@ struct xml_node: protected xml_base<xmlNode> { bool has_prop(const char* name) const { return xmlHasProp(get(), (const xmlChar*)name); } template <typename T> - T prop(const char* name, std::optional<T> default_value = std::nullopt) const { + arb::util::expected<T, bad_property> prop(const char* name, std::optional<T> default_value = std::nullopt) const { + using arb::util::unexpected; + xmlChar* c = xmlGetProp(get(), (const xmlChar*)(name)); if (!c) { - return default_value? default_value.value(): throw parse_error("missing required attribute", get()->line); + if (default_value) return default_value.value(); + else return unexpected(bad_property{"missing required attribute", get()->line}); } T v; - return nl_from_cstr(v, reinterpret_cast<const char*>(c))? v: throw parse_error("attribute type error", get()->line); + if (nl_from_cstr(v, reinterpret_cast<const char*>(c))) return v; + else return unexpected(bad_property{"attribute type error", get()->line}); } using base::get; // (unsafe access) @@ -314,4 +325,5 @@ struct xml_error_scope { void* structured_context_; }; +} // namespace xmlwrap } // namespace arborio diff --git a/doc/cpp/morphology.rst b/doc/cpp/morphology.rst index 20b0a46fa96dcc960bb33121d7e5e4252a005788..80ab858ef394c416c2111db3be834776491a5698 100644 --- a/doc/cpp/morphology.rst +++ b/doc/cpp/morphology.rst @@ -334,14 +334,14 @@ CV will have an extent on the branch longer than ``max_extent`` micrometres. Supported morphology formats ----------------------------- +============================ Arbor supports morphologies described using the SWC file format and the NeuroML file format. .. _cppswc: SWC -^^^ +--- Arbor supports reading morphologies described using the `SWC <http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html>`_ file format. And @@ -415,13 +415,14 @@ 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 SWC specifications. + .. _cppasc: Neurolucida ASCII -^^^^^^^^^^^^^^^^^^ +----------------- Arbor supports reading morphologies described using the -:ref:`Neurolucida <format_asc>`_ file format. +: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, which a simple struct with two members representing the morphology and a label dictionary with labeled @@ -438,38 +439,36 @@ regions and locations. Parse a Neurolucida ASCII file. Throws an exception if there is an error parsing the file. + .. _cppneuroml: NeuroML -^^^^^^^ +------- -Arbor offers limited support for models described in -`NeuroML version 2 <https://neuroml.org/neuromlv2>`_. -This is not built by default, but can be enabled by -providing the `-DARB_NEUROML=ON` argument to CMake at -configuration time (see :ref:`install-neuroml`). This will -build the ``arborio`` libray with neuroml support. +Arbor offers limited support for models described in `NeuroML version 2 +<https://neuroml.org/neuromlv2>`_. This is not built by default, but can be +enabled by providing the `-DARB_WITH_NEUROML=ON` argument to CMake at configuration +time (see :ref:`install-neuroml`). This will build the ``arborio`` libray with +neuroml support. -The ``arborio`` library uses `libxml2 <http://xmlsoft.org/>`_ -for XML parsing. Applications using NeuroML through ``arborio`` -will need to link against ``libxml2`` in addition, though this -is performed implicitly within CMake projects that add ``arbor::arborio`` -as a link library. +The ``arborio`` library uses `libxml2 <http://xmlsoft.org/>`_ for XML parsing. +Applications using NeuroML through ``arborio`` will need to link against +``libxml2`` in addition, though this is performed implicitly within CMake +projects that add ``arbor::arborio`` as a link library. -All classes and functions provided by the ``arborio`` library -are provided in the ``arborio`` namespace. +All classes and functions provided by the ``arborio`` library are provided in +the ``arborio`` namespace. Libxml2 interface -================= +^^^^^^^^^^^^^^^^^ -Libxml2 offers threadsafe XML parsing, but not by default. If -the application uses NeuromML support from ``arborio`` in an -unthreaded context, or has already explicitly initialized ``libxml2``, -nothing more needs to be done. Otherwise, the ``libxml2`` function -``xmlInitParser()`` must be called explicitly. +Libxml2 offers threadsafe XML parsing, but not by default. If the application +uses NeuromML support from ``arborio`` in an unthreaded context, or has already +explicitly initialized ``libxml2``, nothing more needs to be done. Otherwise, +the ``libxml2`` function ``xmlInitParser()`` must be called explicitly. ``arborio`` provides a helper guard object for this purpose, defined -in ``arborio/with_xml.hpp``: +in ``arborio/xml.hpp``: .. cpp:namespace:: arborio @@ -478,16 +477,25 @@ in ``arborio/with_xml.hpp``: An RAII guard object that calls ``xmlInitParser()`` upon construction, and ``xmlCleanupParser()`` upon destruction. The constructor takes no parameters. +Unhandleable exceptions from ``libxml2`` are forwarded via an exception +``xml_error``, derived from ``std::runtime_error``. + NeuroML2 morphology support -=========================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^ NeuroML documents are represented by the ``arborio::neuroml`` class, which in turn provides methods for the identification and translation -of morphology data. ``neuroml`` objects are moveable and move-assignable, but not copyable. +of morphology data. ``neuroml`` objects are moveable and move-assignable, +but not copyable. An implementation limitation restricts valid segment id values to those which can be represented by an ``unsigned long long`` value. +``arborio::neuroml`` methods can throw an ``arborio::xml_error`` in the instance that +the underlying libxml2 library reports a problem that cannot be handled by the ``arborio`` +library. Otherwise, exceptions derived from ``aborio::neuroml_exception`` can be thrown +when encountering problems interpreting the NeuroML document (see :ref:`cppneuromlexceptions` below). + .. cpp:class:: neuroml .. cpp:function:: neuroml(std::string) @@ -502,24 +510,22 @@ those which can be represented by an ``unsigned long long`` value. Return the id of each top-level ``<morphology>`` element defined in the NeuroML document. - .. cpp:function:: std::optional<morphology_data> morphology(const std::string&) const + .. cpp:function:: std::optional<nml_morphology_data> morphology(const std::string&) const Return a representation of the top-level morphology with the supplied identifier, or - ``std::nullopt`` if no such morphology could be found. Parse errors or an inconsistent - representation will raise an exception derived from ``neuroml_exception``. + ``std::nullopt`` if no such morphology could be found. - .. cpp:function:: std::optional<morphology_data> cell_morphology(const std::string&) const + .. cpp:function:: std::optional<nml_morphology_data> cell_morphology(const std::string&) 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. Parse errors or an - inconsistent representation will raise an exception derived from ``neuroml_exception``. + or ``std::nullopt`` if the cell or its morphology could not be found. The morphology representation contains the corresponding Arbor ``arb::morphology`` object, label dictionaries for regions corresponding to its segments and segment groups by name and id, and a map providing the explicit list of segments contained within each defined segment group. -.. cpp:class:: morphology_data +.. cpp:class:: nml_morphology_data .. cpp:member:: std::optional<std::string> cell_id @@ -551,39 +557,37 @@ segment group. A map from each segment group id to its corresponding collection of segments. +.. _cppneuromlexceptions: + Exceptions -========== +^^^^^^^^^^ -All NeuroML-specific exceptions are defined in ``arborio/arbornml.hpp``, and are +All NeuroML-specific exceptions are defined in ``arborio/neuroml.hpp``, and are derived from ``arborio::neuroml_exception`` which in turn is derived from ``std::runtime_error``. -With the exception of the ``no_document`` exception, all contain an unsigned member ``line`` +With the exception of the ``nml_no_document`` exception, all contain an unsigned member ``line`` which is intended to identify the problematic construct within the document. -.. cpp:class:: xml_error: neuroml_exception - - A generic XML error generated by the ``libxml2`` library. - -.. cpp:class:: no_document: neuroml_exception +.. cpp:class:: nml_no_document: neuroml_exception - A request was made on an :cpp:class:`neuroml` document without any content. + A request was made to parse text which could not be interpreted as an XML document. -.. cpp:class:: parse_error: neuroml_exception +.. cpp:class:: nml_parse_error: neuroml_exception Failure parsing an element or attribute in the NeuroML document. These can be generated if the document does not confirm to the NeuroML2 schema, for example. -.. cpp:class:: bad_segment: neuroml_exception +.. cpp:class:: nml_bad_segment: neuroml_exception A ``<segment>`` element has an improper ``id`` attribue, refers to a non-existent parent, is missing a required parent or proximal element, or otherwise is missing a mandatory child element or has a malformed child element. -.. cpp:class:: bad_segment_group: neuroml_exception +.. cpp:class:: nml_bad_segment_group: neuroml_exception A ``<segmentGroup>`` element has a malformed child element or references a non-existent segment group or segment. -.. cpp:class:: cyclic_dependency: neuroml_exception +.. cpp:class:: nml_cyclic_dependency: neuroml_exception A segment or segment group ultimately refers to itself via ``parent`` diff --git a/python/morphology.cpp b/python/morphology.cpp index d15457855ddcad2ddd237941704999d9cacbd414..8c736369df5e89485c381ae8c8d4aca13eb4bef8 100644 --- a/python/morphology.cpp +++ b/python/morphology.cpp @@ -16,7 +16,7 @@ #include <arborio/neurolucida.hpp> #ifdef ARB_NEUROML_ENABLED -#include <arborio/arbornml.hpp> +#include <arborio/neuroml.hpp> #endif #include "error.hpp" @@ -398,28 +398,28 @@ void register_morphology(py::module& m) { #ifdef ARB_NEUROML_ENABLED // arborio::morphology_data - py::class_<arborio::morphology_data> nml_morph_data(m, "neuroml_morph_data"); + py::class_<arborio::nml_morphology_data> nml_morph_data(m, "neuroml_morph_data"); nml_morph_data .def_readonly("cell_id", - &arborio::morphology_data::cell_id, + &arborio::nml_morphology_data::cell_id, "Cell id, or empty if morphology was taken from a top-level <morphology> element.") .def_readonly("id", - &arborio::morphology_data::id, + &arborio::nml_morphology_data::id, "Morphology id.") .def_readonly("morphology", - &arborio::morphology_data::morphology, + &arborio::nml_morphology_data::morphology, "Morphology constructed from a signle NeuroML <morphology> element.") .def("segments", - [](const arborio::morphology_data& md) {return label_dict_proxy(md.segments);}, + [](const arborio::nml_morphology_data& md) {return label_dict_proxy(md.segments);}, "Label dictionary containing one region expression for each segment id.") .def("named_segments", - [](const arborio::morphology_data& md) {return label_dict_proxy(md.named_segments);}, + [](const arborio::nml_morphology_data& 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::morphology_data& md) {return label_dict_proxy(md.groups);}, + [](const arborio::nml_morphology_data& md) {return label_dict_proxy(md.groups);}, "Label dictionary containing one region expression for each segmentGroup id.") .def_readonly("group_segments", - &arborio::morphology_data::group_segments, + &arborio::nml_morphology_data::group_segments, "Map from segmentGroup ids to their corresponding segment ids."); // arborio::neuroml diff --git a/test/unit/test_nml_morphology.cpp b/test/unit/test_nml_morphology.cpp index 6860aa80543c9dbf8087f9e3491861969a260489..ff69fe25549bbb3ae70e4433340dbe96fb9f8179 100644 --- a/test/unit/test_nml_morphology.cpp +++ b/test/unit/test_nml_morphology.cpp @@ -6,8 +6,8 @@ #include <arbor/morph/place_pwlin.hpp> #include <arbor/morph/primitives.hpp> -#include <arborio/arbornml.hpp> -#include <arborio/with_xml.hpp> +#include <arborio/neuroml.hpp> +#include <arborio/xml.hpp> #include "../test/gtest.h" #include "morph_pred.hpp" @@ -89,7 +89,7 @@ R"~( std::sort(c_ids.begin(), c_ids.end()); EXPECT_EQ((svector{"c3", "c4"}), c_ids); - arborio::morphology_data mdata; + arborio::nml_morphology_data mdata; mdata = N.cell_morphology("c4").value(); EXPECT_EQ("c4", mdata.cell_id); @@ -176,7 +176,7 @@ R"~( arborio::neuroml N(doc); { - arborio::morphology_data m1 = N.morphology("m1").value(); + arborio::nml_morphology_data m1 = N.morphology("m1").value(); label_dict labels; labels.import(m1.segments, "seg:"); mprovider P(m1.morphology, labels); @@ -189,7 +189,7 @@ R"~( } { - arborio::morphology_data m2 = N.morphology("m2").value(); + arborio::nml_morphology_data m2 = N.morphology("m2").value(); label_dict labels; labels.import(m2.segments, "seg:"); mprovider P(m2.morphology, labels); @@ -217,7 +217,7 @@ R"~( } { - arborio::morphology_data m3 = N.morphology("m3").value(); + arborio::nml_morphology_data m3 = N.morphology("m3").value(); label_dict labels; labels.import(m3.segments, "seg:"); mprovider P(m3.morphology, labels); @@ -251,7 +251,7 @@ R"~( } { for (const char* m_name: {"m4", "m5"}) { - arborio::morphology_data m4_or_5 = N.morphology(m_name).value(); + arborio::nml_morphology_data m4_or_5 = N.morphology(m_name).value(); label_dict labels; labels.import(m4_or_5.segments, "seg:"); mprovider P(m4_or_5.morphology, labels); @@ -355,12 +355,12 @@ R"~( arborio::neuroml N(doc); - EXPECT_THROW(N.morphology("no-proximal").value(), arborio::bad_segment); - EXPECT_THROW(N.morphology("no-such-parent").value(), arborio::bad_segment); - EXPECT_THROW(N.morphology("cyclic-dependency").value(), arborio::cyclic_dependency); - EXPECT_THROW(N.morphology("duplicate-id").value(), arborio::bad_segment); - EXPECT_THROW(N.morphology("bad-segment-id").value(), arborio::bad_segment); - EXPECT_THROW(N.morphology("another-bad-segment-id").value(), arborio::bad_segment); + EXPECT_THROW(N.morphology("no-proximal").value(), arborio::nml_bad_segment); + EXPECT_THROW(N.morphology("no-such-parent").value(), arborio::nml_bad_segment); + EXPECT_THROW(N.morphology("cyclic-dependency").value(), arborio::nml_cyclic_dependency); + EXPECT_THROW(N.morphology("duplicate-id").value(), arborio::nml_bad_segment); + EXPECT_THROW(N.morphology("bad-segment-id").value(), arborio::nml_bad_segment); + EXPECT_THROW(N.morphology("another-bad-segment-id").value(), arborio::nml_bad_segment); } TEST(neuroml, simple_groups) { @@ -444,7 +444,7 @@ R"~( using reg::named; { - arborio::morphology_data m1 = N.morphology("m1").value(); + arborio::nml_morphology_data m1 = N.morphology("m1").value(); label_dict labels; labels.import(m1.segments); labels.import(m1.groups); @@ -455,7 +455,7 @@ R"~( EXPECT_TRUE(region_eq(P, named("group-c"), join(named("2"), named("1")))); } { - arborio::morphology_data m2 = N.morphology("m2").value(); + arborio::nml_morphology_data m2 = N.morphology("m2").value(); label_dict labels; labels.import(m2.segments); labels.import(m2.groups); @@ -509,9 +509,9 @@ R"~( arborio::neuroml N(doc); - EXPECT_THROW(N.morphology("no-such-segment").value(), arborio::bad_segment_group); - EXPECT_THROW(N.morphology("no-such-group").value(), arborio::bad_segment_group); - EXPECT_THROW(N.morphology("cyclic-dependency").value(), arborio::cyclic_dependency); + EXPECT_THROW(N.morphology("no-such-segment").value(), arborio::nml_bad_segment_group); + EXPECT_THROW(N.morphology("no-such-group").value(), arborio::nml_bad_segment_group); + EXPECT_THROW(N.morphology("cyclic-dependency").value(), arborio::nml_cyclic_dependency); } @@ -613,7 +613,7 @@ R"~( arborio::neuroml N(doc); - arborio::morphology_data m1 = N.morphology("m1").value(); + arborio::nml_morphology_data m1 = N.morphology("m1").value(); label_dict labels; labels.import(m1.segments); labels.import(m1.groups);