diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9bedee3de0d7a52497259c182e34dd1a2b16be1a..70135e7d77117e77113b0fd99f172cf57c7c0fdd 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -154,7 +154,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
 # in the same CMakeLists.txt in which the target is defined.
 
 # Interface library `arbor-config-defs` collects configure-time defines
-# for arbor, arborenv and arbornml, of the form ARB_HAVE_XXX. These
+# for arbor, arborenv, arborio and arbornml, of the form ARB_HAVE_XXX. These
 # defines should _not_ be used in any installed public headers.
 
 add_library(arbor-config-defs INTERFACE)
@@ -181,6 +181,13 @@ add_library(arbornml-private-deps INTERFACE)
 target_link_libraries(arbornml-private-deps INTERFACE arbor-config-defs)
 install(TARGETS arbornml-private-deps EXPORT arbor-targets)
 
+# Interface library `arborio-private-deps` collects dependencies, options etc.
+# for the arborio library.
+
+add_library(arborio-private-deps INTERFACE)
+target_link_libraries(arborio-private-deps INTERFACE arbor-config-defs)
+install(TARGETS arborio-private-deps EXPORT arbor-targets)
+
 # Interface library `arbor-public-deps` collects requirements for the
 # users of the arbor library (e.g. mpi) that will become part
 # of arbor's PUBLIC interface.
@@ -404,6 +411,9 @@ add_subdirectory(arbor)
 # arborenv, arborenv-public-headers:
 add_subdirectory(arborenv)
 
+# arborio, arborio-public-headers:
+add_subdirectory(arborio)
+
 # arbornml, arbornml-public-headers:
 if(ARB_WITH_NEUROML)
     add_subdirectory(arbornml)
@@ -459,6 +469,7 @@ set(arbor_override_import_lang)
 set(arbor_add_import_libs)
 set(arborenv_add_import_libs)
 set(arbornml_add_import_libs)
+set(arborio_add_import_libs)
 
 if(ARB_WITH_GPU)
     set(arbor_override_import_lang CXX)
diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index 0368fc88613d5213bdc9582a473ad4ae6dbcf39f..5f135d92ab3d6816b2a02777fbbe8f656f851356 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -53,7 +53,6 @@ set(arbor_sources
     schedule.cpp
     spike_event_io.cpp
     spike_source_cell_group.cpp
-    swcio.cpp
     s_expr.cpp
     threading/threading.cpp
     thread_private_spike_store.cpp
diff --git a/arbor/swcio.cpp b/arbor/swcio.cpp
deleted file mode 100644
index c17b375ecf14e226f81083a5308be2fc5ee75558..0000000000000000000000000000000000000000
--- a/arbor/swcio.cpp
+++ /dev/null
@@ -1,193 +0,0 @@
-#include <iostream>
-#include <limits>
-#include <string>
-#include <sstream>
-#include <unordered_map>
-#include <unordered_set>
-#include <vector>
-
-#include <arbor/morph/segment_tree.hpp>
-#include <arbor/swcio.hpp>
-
-#include "io/save_ios.hpp"
-#include "util/rangeutil.hpp"
-
-namespace arb {
-
-// SWC exceptions:
-
-swc_error::swc_error(const std::string& msg, int record_id):
-    arbor_exception(msg+": sample id "+std::to_string(record_id)),
-    record_id(record_id)
-{}
-
-swc_no_such_parent::swc_no_such_parent(int record_id):
-    swc_error("missing SWC parent record", record_id)
-{}
-
-swc_record_precedes_parent::swc_record_precedes_parent(int record_id):
-    swc_error("SWC parent id is not less than sample id", record_id)
-{}
-
-swc_duplicate_record_id::swc_duplicate_record_id(int record_id):
-    swc_error("duplicate SWC sample id", record_id)
-{}
-
-swc_spherical_soma::swc_spherical_soma(int record_id):
-    swc_error("SWC with spherical somata are not supported", record_id)
-{}
-
-bad_swc_data::bad_swc_data(int record_id):
-    swc_error("Cannot interpret bad SWC data", record_id)
-{}
-
-// Record I/O
-
-std::ostream& operator<<(std::ostream& out, const swc_record& record) {
-    io::save_ios_flags save(out);
-
-    out.precision(std::numeric_limits<double>::digits10+2);
-    return out << record.id << ' ' << record.tag << ' '
-               << record.x  << ' ' << record.y   << ' ' << record.z << ' ' << record.r << ' '
-               << record.parent_id << '\n';
-}
-
-std::istream& operator>>(std::istream& in, swc_record& record) {
-    std::string line;
-    if (!getline(in, line, '\n')) return in;
-
-    swc_record r;
-    std::istringstream s(line);
-    s >> r.id >> r.tag >> r.x >> r.y >> r.z >> r.r >> r.parent_id;
-    if (s) {
-        record = r;
-    }
-    else {
-        in.setstate(std::ios_base::failbit);
-    }
-
-    return in;
-}
-
-// Parse SWC format data (comments and sequence of SWC records).
-
-static std::vector<swc_record> sort_and_validate_swc(std::vector<swc_record> records, swc_mode mode) {
-    if (records.empty()) return {};
-
-    std::unordered_set<int> seen;
-    std::size_t n_rec = records.size();
-    int first_id = records[0].id;
-    int first_tag = records[0].tag;
-
-    if (records.size()<2) {
-        throw swc_spherical_soma(first_id);
-    }
-
-    for (std::size_t i = 0; i<n_rec; ++i) {
-        swc_record& r = records[i];
-
-        if (r.parent_id>=r.id) {
-            throw swc_record_precedes_parent(r.id);
-        }
-
-        if (!seen.insert(r.id).second) {
-            throw swc_duplicate_record_id(r.id);
-        }
-    }
-
-    util::sort_by(records, [](auto& r) { return r.id; });
-    bool first_tag_match = false;
-
-    for (std::size_t i = 0; i<n_rec; ++i) {
-        const swc_record& r = records[i];
-        first_tag_match |= r.parent_id==first_id && r.tag==first_tag;
-
-        if ((i==0 && r.parent_id!=-1) || (i>0 && !seen.count(r.parent_id))) {
-            throw swc_no_such_parent(r.id);
-        }
-    }
-
-    if (mode==swc_mode::strict && !first_tag_match) {
-        throw swc_spherical_soma(first_id);
-    }
-
-    return records;
-}
-
-swc_data parse_swc(std::istream& in, swc_mode mode) {
-    // Collect any initial comments (lines beginning with '#').
-
-    swc_data data;
-    std::string line;
-
-    while (in) {
-        auto c = in.get();
-        if (c=='#') {
-            getline(in, line, '\n');
-            auto from = line.find_first_not_of(" \t");
-            if (from != std::string::npos) {
-                data.metadata.append(line, from);
-            }
-            data.metadata += '\n';
-        }
-        else {
-            in.unget();
-            break;
-        }
-    }
-
-    swc_record r;
-    while (in && in >> r) {
-        data.records.push_back(r);
-    }
-
-    data.records = sort_and_validate_swc(std::move(data.records), mode);
-    return data;
-}
-
-swc_data parse_swc(const std::string& text, swc_mode mode) {
-    std::istringstream is(text);
-    return parse_swc(is, mode);
-}
-
-swc_data parse_swc(std::vector<swc_record> records, swc_mode mode) {
-    swc_data data;
-    data.records = sort_and_validate_swc(std::move(records), mode);
-    return data;
-}
-
-segment_tree as_segment_tree(const std::vector<swc_record>& records) {
-    if (records.empty()) return {};
-    if (records.size()<2) throw bad_swc_data{records.front().id};
-
-    segment_tree tree;
-    std::size_t n_seg = records.size()-1;
-    tree.reserve(n_seg);
-
-    std::unordered_map<int, std::size_t> id_to_index;
-    id_to_index[records[0].id] = 0;
-
-    // ith segment is built from i+1th SWC record and its parent.
-    for (std::size_t i = 1; i<n_seg+1; ++i) {
-        const auto& dist = records[i];
-
-        auto iter = id_to_index.find(dist.parent_id);
-        if (iter==id_to_index.end()) throw bad_swc_data{dist.id};
-        auto parent_idx = iter->second;
-
-        const auto& prox = records[parent_idx];
-        msize_t seg_parent = parent_idx? parent_idx-1: mnpos;
-
-        tree.append(seg_parent,
-            mpoint{prox.x, prox.y, prox.z, prox.r},
-            mpoint{dist.x, dist.y, dist.z, dist.r},
-            dist.tag);
-
-        id_to_index[dist.id] = i;
-    }
-
-    return tree;
-}
-
-} // namespace arb
-
diff --git a/arborio/CMakeLists.txt b/arborio/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e07ce90d4ad50dcedece2352043487c5aab618d6
--- /dev/null
+++ b/arborio/CMakeLists.txt
@@ -0,0 +1,21 @@
+set(arborio-sources
+    swcio.cpp
+)
+
+add_library(arborio ${arborio-sources})
+
+add_library(arborio-public-headers INTERFACE)
+target_include_directories(arborio-public-headers INTERFACE
+    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+    $<INSTALL_INTERFACE:include>
+)
+
+target_link_libraries(arborio PUBLIC arbor arborio-public-headers)
+target_link_libraries(arborio PRIVATE arbor-config-defs arborio-private-deps)
+
+install(DIRECTORY include/arborio
+    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
+    FILES_MATCHING PATTERN "*.hpp")
+
+install(TARGETS arborio-public-headers EXPORT arbor-targets)
+install(TARGETS arborio EXPORT arbor-targets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/arbor/include/arbor/swcio.hpp b/arborio/include/arborio/swcio.hpp
similarity index 60%
rename from arbor/include/arbor/swcio.hpp
rename to arborio/include/arborio/swcio.hpp
index db5b672f2cfdf39f576699ba39f734d4b9db9944..9fff6d9020d895cf60bebe6698380b23c1654cb9 100644
--- a/arbor/include/arbor/swcio.hpp
+++ b/arborio/include/arborio/swcio.hpp
@@ -7,12 +7,12 @@
 #include <arbor/arbexcept.hpp>
 #include <arbor/morph/segment_tree.hpp>
 
-namespace arb {
+namespace arborio {
 
 // SWC exceptions are thrown by `parse_swc`, and correspond
 // to inconsistent, or in `strict` mode, dubious SWC data.
 
-struct swc_error: public arbor_exception {
+struct swc_error: public arb::arbor_exception {
     swc_error(const std::string& msg, int record_id);
     int record_id;
 };
@@ -38,9 +38,59 @@ struct swc_spherical_soma: swc_error {
     explicit swc_spherical_soma(int record_id);
 };
 
-// Bad or inconsistent SWC data was fed to an `swc_data` consumer.
-struct bad_swc_data: swc_error {
-    explicit bad_swc_data(int record_id);
+// Smells like a non-spherical soma.
+struct swc_non_spherical_soma: swc_error {
+    explicit swc_non_spherical_soma(int record_id);
+};
+
+// Missing soma.
+struct swc_no_soma: swc_error {
+    explicit swc_no_soma(int record_id);
+};
+
+// Non-consecutive soma samples.
+struct swc_non_consecutive_soma: swc_error {
+    explicit swc_non_consecutive_soma(int record_id);
+};
+
+// Non-serial soma samples.
+struct swc_non_serial_soma: swc_error {
+    explicit swc_non_serial_soma(int record_id);
+};
+
+// Sample connecting to the middle of a soma causing an unsupported branch.
+struct swc_branchy_soma: swc_error {
+    explicit swc_branchy_soma(int record_id);
+};
+
+// Sample connecting to the middle of a soma causing an unsupported branch.
+struct swc_collocated_soma: swc_error {
+    explicit swc_collocated_soma(int record_id);
+};
+
+// Sample is not part of a segment
+struct swc_single_sample_segment: swc_error {
+    explicit swc_single_sample_segment(int record_id);
+};
+
+// Segment cannot have samples with different tags
+struct swc_mismatched_tags: swc_error {
+    explicit swc_mismatched_tags(int record_id);
+};
+
+// Only tags 1, 2, 3, 4 supported
+struct swc_unsupported_tag: swc_error {
+    explicit swc_unsupported_tag(int record_id);
+};
+
+// No gaps allowed
+struct swc_unsupported_gaps: swc_error {
+    explicit swc_unsupported_gaps(int record_id);
+};
+
+// Can't form a segment from a single sample
+struct swc_bad_description: swc_error {
+    explicit swc_bad_description(int record_id);
 };
 
 struct swc_record {
@@ -119,11 +169,31 @@ swc_data parse_swc(std::vector<swc_record>, swc_mode = swc_mode::strict);
 // 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.
 
-segment_tree as_segment_tree(const std::vector<swc_record>&);
+arb::segment_tree as_segment_tree(const std::vector<swc_record>&);
 
-inline segment_tree as_segment_tree(const swc_data& data) {
+inline arb::segment_tree as_segment_tree(const swc_data& data) {
     return as_segment_tree(data.records);
 }
 
+// As above, will convert a valid, ordered sequence of SWC records to a morphological
+// segment tree.
+//
+// Note that 'one-point soma' SWC files are supported here; the swc_data is expected
+// to abide by the restrictions of `relaxed` mode parsing as described above.
+//
+// These functions comply with inferred SWC rules from the Allen institute and Neuron.
+// These rules are explicitly listed in the docs.
+
+arb::segment_tree load_swc_neuron(const std::vector<swc_record>& records);
+
+inline arb::segment_tree load_swc_neuron(const swc_data& data) {
+    return load_swc_neuron(data.records);
+}
+
+arb::segment_tree load_swc_allen(std::vector<swc_record>& records, bool no_gaps=false);
+
+inline arb::segment_tree load_swc_allen(swc_data& data) {
+    return load_swc_allen(data.records);
+}
 
-} // namespace arb
+} // namespace arborio
diff --git a/arborio/swcio.cpp b/arborio/swcio.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..984dc61ad520df5f2885766ae452637a3e4dd4c6
--- /dev/null
+++ b/arborio/swcio.cpp
@@ -0,0 +1,503 @@
+#include <cmath>
+#include <ios>
+#include <iostream>
+#include <limits>
+#include <numeric>
+#include <set>
+#include <string>
+#include <sstream>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include <arbor/morph/segment_tree.hpp>
+
+#include <arborio/swcio.hpp>
+
+namespace arborio {
+
+// SWC exceptions:
+
+swc_error::swc_error(const std::string& msg, int record_id):
+    arbor_exception(msg+": sample id "+std::to_string(record_id)),
+    record_id(record_id)
+{}
+
+swc_no_such_parent::swc_no_such_parent(int record_id):
+    swc_error("Missing SWC parent record", record_id)
+{}
+
+swc_record_precedes_parent::swc_record_precedes_parent(int record_id):
+    swc_error("SWC parent id is not less than sample id", record_id)
+{}
+
+swc_duplicate_record_id::swc_duplicate_record_id(int record_id):
+    swc_error("duplicate SWC sample id", record_id)
+{}
+
+swc_spherical_soma::swc_spherical_soma(int record_id):
+    swc_error("SWC with spherical somata are not supported", record_id)
+{}
+
+swc_non_spherical_soma::swc_non_spherical_soma(int record_id):
+    swc_error("SWC with multi-sample somata are not supported", record_id)
+{}
+
+swc_no_soma::swc_no_soma(int record_id):
+    swc_error("No soma (tag 1) found at the root", record_id)
+{}
+
+swc_non_consecutive_soma::swc_non_consecutive_soma (int record_id):
+    swc_error("Soma samples (tag 1) are not listed consecutively", record_id)
+{}
+
+swc_non_serial_soma::swc_non_serial_soma (int record_id):
+    swc_error("Soma samples (tag 1) are not listed serially", record_id)
+{}
+
+swc_branchy_soma::swc_branchy_soma (int record_id):
+    swc_error("Non-soma sample (tag >= 1) connected to a non-distal sample of the soma", record_id)
+{}
+
+swc_collocated_soma::swc_collocated_soma(int record_id):
+    swc_error("The samples that make the soma (tag 1) are not allowed to be collocated", record_id)
+{}
+
+swc_single_sample_segment::swc_single_sample_segment(int record_id):
+    swc_error("Segments connected to the soma (tag 1) must have 2 samples with the same tag", record_id)
+{}
+
+swc_mismatched_tags::swc_mismatched_tags(int record_id):
+    swc_error("Every record not attached to the soma (tag 1) must have the same tag as its parent", record_id)
+{}
+
+swc_unsupported_tag::swc_unsupported_tag(int record_id):
+    swc_error("Every record must have a tag of 2, 3 or 4, except for the first which must have tag 1", record_id)
+{}
+
+swc_unsupported_gaps::swc_unsupported_gaps(int record_id):
+    swc_error("No gaps are allowed between the soma and any axons, dendrites or apical dendrites", record_id)
+{}
+
+swc_bad_description::swc_bad_description(int record_id):
+    swc_error("Need at least 2 samples to form a segment", record_id)
+{}
+
+
+// Record I/O:
+
+std::ostream& operator<<(std::ostream& out, const swc_record& record) {
+    std::ios_base::fmtflags flags(out.flags());
+
+    out.precision(std::numeric_limits<double>::digits10+2);
+    out << record.id << ' ' << record.tag << ' '
+        << record.x  << ' ' << record.y   << ' ' << record.z << ' ' << record.r << ' '
+        << record.parent_id << '\n';
+
+    out.flags(flags);
+
+    return out;
+}
+
+std::istream& operator>>(std::istream& in, swc_record& record) {
+    std::string line;
+    if (!getline(in, line, '\n')) return in;
+
+    swc_record r;
+    std::istringstream s(line);
+    s >> r.id >> r.tag >> r.x >> r.y >> r.z >> r.r >> r.parent_id;
+    if (s) {
+        record = r;
+    }
+    else {
+        in.setstate(std::ios_base::failbit);
+    }
+
+    return in;
+}
+
+// Parse SWC format data (comments and sequence of SWC records).
+
+static std::vector<swc_record> sort_and_validate_swc(std::vector<swc_record> records, swc_mode mode) {
+    if (records.empty()) return {};
+
+    std::unordered_set<int> seen;
+    std::size_t n_rec = records.size();
+    int first_id = records[0].id;
+    int first_tag = records[0].tag;
+
+    if (records.size()<2) {
+        throw swc_spherical_soma(first_id);
+    }
+
+    for (std::size_t i = 0; i<n_rec; ++i) {
+        swc_record& r = records[i];
+
+        if (r.parent_id>=r.id) {
+            throw swc_record_precedes_parent(r.id);
+        }
+
+        if (!seen.insert(r.id).second) {
+            throw swc_duplicate_record_id(r.id);
+        }
+    }
+
+    std::sort(records.begin(), records.end(), [](const auto& lhs, const auto& rhs) { return lhs.id < rhs.id; });
+    bool first_tag_match = false;
+
+    for (std::size_t i = 0; i<n_rec; ++i) {
+        const swc_record& r = records[i];
+        first_tag_match |= r.parent_id==first_id && r.tag==first_tag;
+
+        if ((i==0 && r.parent_id!=-1) || (i>0 && !seen.count(r.parent_id))) {
+            throw swc_no_such_parent(r.id);
+        }
+    }
+
+    if (mode==swc_mode::strict && !first_tag_match) {
+        throw swc_spherical_soma(first_id);
+    }
+
+    return records;
+}
+
+swc_data parse_swc(std::istream& in, swc_mode mode) {
+    // Collect any initial comments (lines beginning with '#').
+
+    swc_data data;
+    std::string line;
+
+    while (in) {
+        auto c = in.get();
+        if (c=='#') {
+            getline(in, line, '\n');
+            auto from = line.find_first_not_of(" \t");
+            if (from != std::string::npos) {
+                data.metadata.append(line, from);
+            }
+            data.metadata += '\n';
+        }
+        else {
+            in.unget();
+            break;
+        }
+    }
+
+    swc_record r;
+    while (in && in >> r) {
+        data.records.push_back(r);
+    }
+
+    data.records = sort_and_validate_swc(std::move(data.records), mode);
+    return data;
+}
+
+swc_data parse_swc(const std::string& text, swc_mode mode) {
+    std::istringstream is(text);
+    return parse_swc(is, mode);
+}
+
+swc_data parse_swc(std::vector<swc_record> records, swc_mode mode) {
+    swc_data data;
+    data.records = sort_and_validate_swc(std::move(records), mode);
+    return data;
+}
+
+arb::segment_tree as_segment_tree(const std::vector<swc_record>& records) {
+    if (records.empty()) return {};
+    if (records.size()<2) throw swc_bad_description{records.front().id};
+
+    arb::segment_tree tree;
+    std::size_t n_seg = records.size()-1;
+    tree.reserve(n_seg);
+
+    std::unordered_map<int, std::size_t> id_to_index;
+    id_to_index[records[0].id] = 0;
+
+    // ith segment is built from i+1th SWC record and its parent.
+    for (std::size_t i = 1; i<n_seg+1; ++i) {
+        const auto& dist = records[i];
+
+        auto iter = id_to_index.find(dist.parent_id);
+        if (iter==id_to_index.end()) throw swc_no_such_parent{dist.id};
+        auto parent_idx = iter->second;
+
+        const auto& prox = records[parent_idx];
+        arb::msize_t seg_parent = parent_idx? parent_idx-1: arb::mnpos;
+
+        tree.append(seg_parent,
+            arb::mpoint{prox.x, prox.y, prox.z, prox.r},
+            arb::mpoint{dist.x, dist.y, dist.z, dist.r},
+            dist.tag);
+
+        id_to_index[dist.id] = i;
+    }
+
+    return tree;
+}
+
+arb::segment_tree load_swc_neuron(const std::vector<swc_record>& records) {
+    if (records.empty()) return {};
+
+    const int soma_tag = 1;
+    auto soma_prox = records.front();
+
+    // Assert that root sample has tag 1.
+    if (soma_prox.tag != soma_tag) throw swc_no_soma{soma_prox.id};
+
+    // check for single soma cell
+    bool has_children = false;
+
+    // Map of SWC record id to index in `records`.
+    std::unordered_map<int, std::size_t> record_index;
+    record_index[soma_prox.id] = 0;
+
+    // Vector of records that make up the soma
+    std::vector<swc_record> soma_records = {soma_prox};
+    int prev_tag = soma_prox.tag;
+    int prev_id = soma_prox.id;
+
+    // Preliminary error checking and building the record_index
+    for (std::size_t i = 1; i < records.size(); ++i) {
+        const auto& r = records[i];
+        record_index[r.id] = i;
+
+        if (r.tag == soma_tag) {
+            if (prev_tag != soma_tag)   throw swc_non_consecutive_soma{r.id};
+            if (prev_id != r.parent_id) throw swc_non_serial_soma{r.id};
+            soma_records.push_back(r);
+        }
+
+        // Find record index of the parent
+        auto parent_iter = record_index.find(r.parent_id);
+
+        if (parent_iter == record_index.end() || records[parent_iter->second].id == r.id) throw swc_no_such_parent{r.id};
+
+        if (r.tag != soma_tag && records[parent_iter->second].tag == soma_tag) {
+            if (r.parent_id != soma_records.back().id) throw swc_branchy_soma{r.id};
+            has_children = true;
+        }
+
+        prev_tag = r.tag;
+        prev_id = r.id;
+    }
+
+    arb::segment_tree tree;
+    tree.reserve(records.size());
+
+    // Map of SWC record id to index in `tree`.
+    std::unordered_map<int, arb::msize_t> tree_index;
+
+    // First, construct the soma
+    if (soma_records.size() == 1) {
+        if (!has_children) {
+            // Model the soma as a 1 cylinder with total length=2*radius, extended along the y axis
+            tree.append(arb::mnpos, {soma_prox.x, soma_prox.y - soma_prox.r, soma_prox.z, soma_prox.r},
+                        {soma_prox.x, soma_prox.y + soma_prox.r, soma_prox.z, soma_prox.r}, soma_tag);
+            return tree;
+        }
+        // Model the soma as a 2 cylinders with total length=2*radius, extended along the y axis, centered at the sample
+        auto p = tree.append(arb::mnpos, {soma_prox.x, soma_prox.y - soma_prox.r, soma_prox.z, soma_prox.r},
+                             {soma_prox.x, soma_prox.y, soma_prox.z, soma_prox.r}, soma_tag);
+        tree.append(p, {soma_prox.x, soma_prox.y, soma_prox.z, soma_prox.r},
+                    {soma_prox.x, soma_prox.y + soma_prox.r, soma_prox.z, soma_prox.r}, soma_tag);
+        tree_index[soma_prox.id] = p;
+    }
+    else {
+        if (!has_children) {
+            // Don't split soma at the midpoint
+            arb::msize_t parent = arb::mnpos;
+            bool collocated_samples = true;
+            for (std::size_t i = 0; i < soma_records.size() - 1; ++i) {
+                const auto& p0 = soma_records[i];
+                const auto& p1 = soma_records[i + 1];
+                parent = tree.append(parent, {p0.x, p0.y, p0.z, p0.r}, {p1.x, p1.y, p1.z, p1.r}, 1);
+                collocated_samples &= ((p0.x == p1.x) && (p0.y == p1.y) && (p0.z == p1.z));
+            }
+            if (collocated_samples) {
+                throw swc_collocated_soma{records[0].id};
+            }
+            return tree;
+        }
+        // Calculate segment lengths
+        bool collocated_samples = true;
+        std::vector<double> soma_segment_lengths;
+        for (std::size_t i = 0; i < soma_records.size() - 1; ++i) {
+            const auto& p0 = soma_records[i];
+            const auto& p1 = soma_records[i + 1];
+            soma_segment_lengths.push_back(distance(arb::mpoint{p0.x, p0.y, p0.z, p0.r}, arb::mpoint{p1.x, p1.y, p1.z, p1.r}));
+            collocated_samples &= ((p0.x == p1.x) && (p0.y == p1.y) && (p0.z == p1.z));
+        }
+        if (collocated_samples) {
+            throw swc_collocated_soma{records[0].id};
+        }
+        double midlength = std::accumulate(soma_segment_lengths.begin(), soma_segment_lengths.end(), 0.) / 2;
+
+        std::size_t idx = 0;
+        for (; idx < soma_segment_lengths.size(); ++idx) {
+            auto l = soma_segment_lengths[idx];
+            if (midlength > l) {
+                midlength -= l;
+                continue;
+            }
+            break;
+        }
+
+        // Interpolate along the segment that contains the midpoint of the soma
+        double pos_on_segment = midlength / soma_segment_lengths[idx];
+
+        auto& r0 = soma_records[idx];
+        auto& r1 = soma_records[idx + 1];
+
+        auto x = r0.x + pos_on_segment * (r1.x - r0.x);
+        auto y = r0.y + pos_on_segment * (r1.y - r0.y);
+        auto z = r0.z + pos_on_segment * (r1.z - r0.z);
+        auto r = r0.r + pos_on_segment * (r1.r - r0.r);
+
+        arb::mpoint mid_soma = {x, y, z, r};
+
+        // Construct the soma
+        arb::msize_t parent = arb::mnpos;
+        for (std::size_t i = 0; i < idx; ++i) {
+            const auto& p0 = soma_records[i];
+            const auto& p1 = soma_records[i + 1];
+            parent = tree.append(parent, {p0.x, p0.y, p0.z, p0.r}, {p1.x, p1.y, p1.z, p1.r}, 1);
+        }
+        auto soma_seg = tree.append(parent, {r0.x, r0.y, r0.z, r0.r}, mid_soma, 1);
+
+        arb::mpoint r1_p{r1.x, r1.y, r1.z, r1.r};
+        parent = mid_soma != r1_p ? tree.append(soma_seg, mid_soma, r1_p, 1) : soma_seg;
+
+        for (std::size_t i = idx + 1; i < soma_records.size() - 1; ++i) {
+            const auto& p0 = soma_records[i];
+            const auto& p1 = soma_records[i + 1];
+            parent = tree.append(parent, {p0.x, p0.y, p0.z, p0.r}, {p1.x, p1.y, p1.z, p1.r}, 1);
+        }
+
+        tree_index[soma_records.back().id] = soma_seg;
+    }
+
+    // Build branches off soma.
+    std::set<int> unused_samples; // Samples that are not part of a segment
+    for (const auto& r: records) {
+        // Skip the soma samples
+        if (r.tag == soma_tag) continue;
+
+        const auto p = r.parent_id;
+
+        // Find parent segment of the record
+        auto pseg_iter = tree_index.find(p);
+        if (pseg_iter == tree_index.end()) throw swc_no_such_parent{r.id};
+
+        // Find parent record of the record
+        auto prec_iter = record_index.find(p);
+        if (prec_iter == record_index.end() || records[prec_iter->second].id == r.id) throw swc_no_such_parent{r.id};
+
+        // If the sample has a soma sample as its parent don't create a segment.
+        if (records[prec_iter->second].tag == soma_tag) {
+            // Map the sample id to the segment id of the soma (parent)
+            tree_index[r.id] = pseg_iter->second;
+            unused_samples.insert(r.id);
+            continue;
+        }
+
+        const auto& prox = records[prec_iter->second];
+        tree_index[r.id] = tree.append(pseg_iter->second, {prox.x, prox.y, prox.z, prox.r}, {r.x, r.y, r.z, r.r}, r.tag);
+        unused_samples.erase(prox.id);
+    }
+
+    if (!unused_samples.empty()) {
+        throw swc_single_sample_segment(*unused_samples.begin());
+    }
+    return tree;
+}
+
+arb::segment_tree load_swc_allen(std::vector<swc_record>& records, bool no_gaps) {
+    // Assert that the file contains at least one sample.
+    if (records.empty()) return {};
+
+    // Map of SWC record id to index in `records`.
+    std::unordered_map<int, std::size_t> record_index;
+
+    int soma_id = records[0].id;
+    record_index[soma_id] = 0;
+
+    // Assert that root sample has tag 1.
+    if (records[0].tag != 1) {
+        throw swc_no_soma{records[0].id};
+    }
+
+    for (std::size_t i = 1; i < records.size(); ++i) {
+        const auto& r = records[i];
+        record_index[r.id] = i;
+
+        // Find record index of the parent
+        auto p = r.parent_id;
+        auto parent_iter = record_index.find(p);
+
+        if (parent_iter == record_index.end() || records[parent_iter->second].id == r.id)
+        {
+            throw swc_no_such_parent{r.id};
+        }
+
+        // Assert that all samples have the same tag as their parent, except those attached to the soma.
+        if (p != soma_id && r.tag != records[parent_iter->second].tag) {
+            throw swc_mismatched_tags{r.id};
+        }
+
+        // Assert that all non-root samples have a tag of 2, 3, or 4.
+        if (r.tag < 2) throw swc_non_spherical_soma{r.id};
+        if (r.tag > 4) throw swc_unsupported_tag{r.id};
+    }
+
+    // Translate the morphology so that the soma is centered at the origin (0,0,0)
+    arb::mpoint sloc{records[0].x, records[0].y, records[0].z, records[0].r};
+    for (auto& r: records) {
+        r.x -= sloc.x;
+        r.y -= sloc.y;
+        r.z -= sloc.z;
+    }
+
+    arb::segment_tree tree;
+
+    // Model the spherical soma as a cylinder with length=2*radius.
+    // The cylinder is centred on the origin, and extended along the y axis.
+    double soma_rad = sloc.radius;
+    tree.append(arb::mnpos, {0, -soma_rad, 0, soma_rad}, {0, soma_rad, 0, soma_rad}, 1);
+
+    // Build branches off soma.
+    std::unordered_map<int, arb::msize_t> smap; // SWC record id -> segment id
+    std::set<int> unused_samples;               // Samples that are not part of a segment
+    for (const auto& r: records) {
+        int id = r.id;
+        if (id == soma_id) continue;
+
+        // If sample i has the root as its parent don't create a segment.
+        if (r.parent_id == soma_id) {
+            if (no_gaps) {
+                // Assert that this branch starts on the "surface" of the spherical soma.
+                auto d = std::fabs(soma_rad - std::sqrt(r.x * r.x + r.y * r.y + r.z * r.z));
+                if (d > 1e-3) { // 1 nm tolerance
+                    throw swc_unsupported_gaps{r.id};
+                }
+            }
+            // This maps axons and apical dendrites to soma.prox, and dendrites to soma.dist.
+            smap[id] = r.tag == 3 ? 0 : arb::mnpos;
+            unused_samples.insert(id);
+            continue;
+        }
+
+        const auto p = r.parent_id;
+        const auto& prox = records[record_index[p]];
+        smap[id] = tree.append(smap.at(p), {prox.x, prox.y, prox.z, prox.r}, {r.x, r.y, r.z, r.r}, r.tag);
+        unused_samples.erase(p);
+    }
+
+    if (!unused_samples.empty()) {
+        throw swc_single_sample_segment{*unused_samples.begin()};
+    }
+
+    return tree;
+}
+} // namespace arborio
+
diff --git a/doc/concepts/morphology.rst b/doc/concepts/morphology.rst
index 123562e33d2f17bc12bc5c311a96dd74eabad5f7..e5b03e06a4e683a86ab19f798322f9b6ee7416f1 100644
--- a/doc/concepts/morphology.rst
+++ b/doc/concepts/morphology.rst
@@ -482,6 +482,102 @@ because it has two children: the dendrites attached to its distal end.
       :width: 900
       :align: center
 
+.. _morph-formats:
+
+Supported file formats
+----------------------
+
+Arbor supports morphologies described using the SWC file format and the NeuroML file format.
+
+SWC
+~~~
+
+Arbor supports reading morphologies described using the
+`SWC <http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html>`_ file format.
+SWC files describe the morphology as a list of samples with an id, an `x,y,z` location is space, a radius, a tag
+and a parent id. Arbor parses these samples, performs some checks, then generates a
+:ref:`segment tree <morph-segment_tree>` describing the morphology according to one of three possible
+interpretations.
+
+The SWC file format specifications are not very detailed, which has lead different simulators to interpret
+SWC files in different ways, especially when it comes to the soma. Arbor has its own an interpretation that
+is powerful, and simple to understand at the same time. However, we have also developed functions that will
+interpret SWC files similarly to how the NEURON simulator would, and how the Allen Institute would.
+
+Despite the differences between the interpretations, there is a common set of checks that are always performed
+to check the validity of SWC files:
+   * Check that there are no duplicate ids.
+   * Check that the parent id of a sample is less than the id of the sample.
+   * Check that the parent id of a sample refers to an existing sample.
+
+In addition, all interpretations agree that a *segment* is (in the common case) constructed between a sample and
+its parent and inherits the tag of the sample; and if more than 1 sample have the same parent, the parent sample
+is interpreted as a fork point in the morphology, and acts as the proximal point to a new branch for each of its
+"child" samples. There a couple of exceptions to these rules which are listed below.
+
+Arbor interpretation:
+"""""""""""""""""""""
+In addition to the previously listed checks, the arbor interpretation explicitly disallows SWC files where the soma is
+described by a single sample. It constructs the soma from 2 or more samples that form 1 or more segments in the segment
+tree. A *segment* is always constructed between a sample and its parent. This means that there are no gaps in the
+resulting segment tree and morphology.
+
+Arbor has no magic rules or transformations for the soma. It can be a single branch or multiple branches; segments
+of a different tag can connect to its distal end, proximal end or anywhere in the middle. For example, to create a
+morphology with a single segment soma; a single segment axon connected to one end of the soma; and a single segment
+dendrite connected to the other end of the soma, the following swc file can be used:
+
+.. code:: Python
+
+   # id, tag,   x, y, z,   r, parent
+      1,   1,   0, 0, 0,   1, -1
+      2,   1,   2, 0, 0,   1,  1
+      3,   2,  -3, 0, 0, 0.7,  1
+      4,   3,  20, 0, 0,   1,  2
+
+Samples 1 and 2 will form the soma; samples 1 and 3 will form the axon, connected to the soma at the proximal end;
+samples 2 and 4 will form the dendrite, connected to the soma at the distal end. The morphology will look something
+like this:
+
+.. figure:: ../gen-images/swc_morph.svg
+   :width: 400
+   :align: center
+
+
+Allen interpretation:
+"""""""""""""""""""""
+In addition to the previously mentioned checks, the Allen interpretation expects a single-sample soma to be the first
+sample of the file and to be interpreted as a spherical soma. Arbor represents the spherical soma as a cylinder with
+length and diameter equal to the diameter of the sample representing the sphere.
+
+This interpretation also expects that samples have the same tag as their parent samples, with the exception of samples
+that have the soma sample as a parent. In this case, when a sample's parent is the soma, no *segment* is created
+between the 2 samples; instead there is a gap in the morphology (represented electrically as a zero-resistance wire).
+Samples with the soma as a parent start new segments, that connect to the distal end of the soma if they are dendrites,
+or to the proximal end of the soma if they are axons or apical dendrites. Only axons, dendrites and apical dendrites
+(tags 2, 3 and 4 respectively) are allowed in this interpretation, in addition to the spherical soma.
+
+Finally the Allen institute interpretation of SWC files centers the morphology around the soma at the origin (0, 0, 0)
+and all samples are translated in space towards the origin.
+
+NEURON interpretation:
+""""""""""""""""""""""
+The NEURON interpretation was obtained by experimenting with the `Import3d_SWC_read` function. We came up with the
+following set of rules that govern NEURON's SWC behavior and enforced them in arbor's NEURON-complaint SWC
+interpreter:
+   * SWC files must contain a soma sample and it must to be the first sample.
+   * A soma is represented by a series of n≥1 unbranched, serially listed samples.
+   * A soma is constructed as a single cylinder with diameter equal to the piecewise average diameter of all the
+   segments forming the soma.
+   * A single-sample soma at is constructed as a cylinder with length=diameter.
+   * If a non-soma sample is to have a soma sample as its parent, it must have the most distal sample of the soma
+   as the parent.
+   * Every non-soma sample that has a soma sample as its parent, attaches to the created soma cylinder at its midpoint.
+   * If a non-soma sample has a soma sample as its parent, no segment is created between the sample and its parent,
+   instead that sample is the proximal point of a new segment, and there is a gap in the morphology (represented
+   electrically as a zero-resistance wire)
+   * To create a segment with a certain tag, that is to be attached to the soma, we need at least 2 samples with that
+   tag.
 
 API
 ---
diff --git a/doc/cpp/cable_cell.rst b/doc/cpp/cable_cell.rst
index cab2b45a57a68fba92fdf4e2114c170610014c16..29320f6b5f58195970cffc733953ce29c2c864ae 100644
--- a/doc/cpp/cable_cell.rst
+++ b/doc/cpp/cable_cell.rst
@@ -145,6 +145,84 @@ by two stitches:
    cell.paint("\"soma\"", "hh");
 
 
+Supported morphology formats:
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Arbor supports morphologies described using the SWC file format and the NeuroML file format.
+
+SWC
+"""
+
+Arbor supports reading morphologies described using the
+`SWC <http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html>`_ file format. And
+has three different interpretation of that format.
+
+A :cpp:func:`parse_swc()` function is used to parse the SWC file and generate a :cpp:type:`swc_data` object.
+This object contains a vector of :cpp:type:`swc_record` objects that represent the SWC samples, with a number of
+basic checks performed on them. The :cpp:type:`swc_data` object can then be used to generate a
+:cpp:type:`segment_tree` object using one of the following functions: (See the morphology concepts
+:ref:`page <morph-formats>` for more details).
+
+  * :cpp:func:`as_segment_tree`
+  * :cpp:func:`load_swc_allen`
+  * :cpp:func:`load_swc_neuron`
+
+.. cpp:class:: swc_record
+
+   .. cpp:member:: int id
+
+      ID of the record
+
+   .. cpp:member:: int tag
+
+       Structure identifier (tag).
+
+   .. cpp:member:: double x
+
+      x coordinate in space.
+
+   .. cpp:member:: double y
+
+      y coordinate in space.
+
+   .. cpp:member:: double z
+
+      z coordinate in space.
+
+   .. cpp:member:: double r
+
+      Sample radius.
+
+   .. cpp:member:: int parent_id
+
+      Record parent's sample ID.
+
+.. cpp:class:: swc_data
+
+   .. cpp:member:: std::string metadata
+
+      Contains the comments of an SWC file.
+
+   .. cpp:member:: std::vector<swc_record> records
+
+      Stored the list of samples from an SWC file, after performing some checks.
+
+.. cpp:function:: swc_data parse_swc(std::istream&)
+
+   Returns an `swc_data` object given an std::istream object.
+
+.. cpp:function:: segment_tree as_segment_tree(const swc_data& data)
+
+   Returns a segment tree constructed according to Arbor's SWC specifications.
+
+.. cpp:function:: segment_tree load_swc_allen(swc_data& data)
+
+   Returns a segment tree constructed according to the Allen Institute's SWC specifications.
+
+.. cpp:function:: segment_tree load_swc_neuron(swc_data& data)
+
+   Returns a segment tree constructed according to NEURON's SWC specifications.
+
 .. _locsets-and-regions:
 
 Identifying sites and subsets of the morphology
diff --git a/doc/python/morphology.rst b/doc/python/morphology.rst
index 046eef2df0b97fbe95eea160f42233c7dcd3d44b..f183f28064bca2ff017ba2d031703ff981d5ee9d 100644
--- a/doc/python/morphology.rst
+++ b/doc/python/morphology.rst
@@ -264,7 +264,8 @@ Cell morphology
 
 .. py:function:: load_swc(filename)
 
-    Loads the morphology in an SWC file as a :class:`segment_tree`.
+    Loads the morphology in an SWC file as a :class:`segment_tree` according to arbor's SWC specifications.
+    (See the morphology concepts :ref:`page <morph-formats>` for more details).
 
     The samples in the SWC files are treated as the end points of segments, where a
     sample and its parent form a segment.
@@ -291,6 +292,28 @@ Cell morphology
     :param str filename: the name of the SWC file.
     :rtype: segment_tree
 
+.. py:function:: load_swc_neuron(filename)
+
+    Loads the morphology in an SWC file as a :class:`segment_tree` according to NEURON's SWC specifications.
+    Specifically:
+        * The first sample must be a soma sample.
+        * The soma is represented by a series of n≥1 unbranched, serially listed samples.
+        * The soma is constructed as a single cylinder with diameter equal to the piecewise average diameter of all the
+          segments forming the soma.
+        * A single-sample soma at is constructed as a cylinder with length=diameter.
+        * If a non-soma sample is to have a soma sample as its parent, it must have the most distal sample of the soma
+          as the parent.
+        * Every non-soma sample that has a soma sample as its parent, attaches to the created soma cylinder at its midpoint.
+        * If a non-soma sample has a soma sample as its parent, no segment is created between the sample and its parent,
+          instead that sample is the proximal point of a new segment, and there is a gap in the morphology (represented
+          electrically as a zero-resistance wire)
+        * To create a segment with a certain tag, that is to be attached to the soma, we need at least 2 samples with that
+          tag.
+
+    :param str filename: the name of the SWC file.
+    :rtype: segment_tree
+
+
 .. py:function:: load_swc_allen(filename, no_gaps=False)
 
     Generate a segment tree from an SWC file following the rules prescribed by
diff --git a/doc/scripts/inputs.py b/doc/scripts/inputs.py
index d3cd78efc1b39735e0578c9b30c5ff253168658f..1a323c3ae012acfe155d41950d7719c3111b2a2e 100644
--- a/doc/scripts/inputs.py
+++ b/doc/scripts/inputs.py
@@ -84,6 +84,11 @@ tmp = [
     [[Segment((-3.0, 0.0, 1.5), (-5.5, -0.2, 0.5), 2), Segment((-5.5, -0.2, 0.5), (-14.5, -0.1, 0.5), 2)]],]
 ysoma_morph3 = representation.make_morph(tmp)
 
+tmp = [
+    [[Segment((-3.0, 0.0, 0.7), (0.0, 0.0, 1.0), 2)], [Segment((0.0, 0.0, 1.0), (2.0, 0.0,  1.0), 1)], [Segment((2.0, 0.0, 1.0), (20.0, 0.0, 1.0), 3)]],
+]
+swc_morph = representation.make_morph(tmp)
+
 
 ############# locsets
 
diff --git a/doc/scripts/make_images.py b/doc/scripts/make_images.py
index 0df83945c9063a7b80ce3fd821eae19b240c6b1b..b1fbb2de2a1833c893b26d29e5fc4dfb81932a9d 100644
--- a/doc/scripts/make_images.py
+++ b/doc/scripts/make_images.py
@@ -235,6 +235,7 @@ def generate(path=''):
     morph_image([inputs.label_morph],    ['segments'], path+'/label_seg.svg')
     morph_image([inputs.detached_morph], ['segments'], path+'/detached_seg.svg')
     morph_image([inputs.stacked_morph],  ['segments'], path+'/stacked_seg.svg')
+    morph_image([inputs.swc_morph],      ['segments'], path+'/swc_morph.svg')
 
     morph_image([inputs.label_morph, inputs.label_morph], ['segments', 'branches'], path+'/label_morph.svg')
     morph_image([inputs.detached_morph, inputs.detached_morph], ['segments', 'branches'], path+'/detached_morph.svg')
diff --git a/example/single/CMakeLists.txt b/example/single/CMakeLists.txt
index ad5257c6e70364152b03ba1b4637d75b729bd8ac..1a2c889e611be54bb888698cf19106e2b9058f6e 100644
--- a/example/single/CMakeLists.txt
+++ b/example/single/CMakeLists.txt
@@ -1,3 +1,3 @@
 add_executable(single-cell EXCLUDE_FROM_ALL single.cpp)
 add_dependencies(examples single-cell)
-target_link_libraries(single-cell PRIVATE arbor arborenv arbor-sup ext-tinyopt)
+target_link_libraries(single-cell PRIVATE arbor arborio arborenv arbor-sup ext-tinyopt)
diff --git a/example/single/single.cpp b/example/single/single.cpp
index f280096269998f2b8d977e96dfa152da77e446a3..f859009f24585996c62bc6fa8fafd8b95d8940d3 100644
--- a/example/single/single.cpp
+++ b/example/single/single.cpp
@@ -10,10 +10,11 @@
 #include <arbor/cable_cell.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/segment_tree.hpp>
-#include <arbor/swcio.hpp>
 #include <arbor/simulation.hpp>
 #include <arbor/simple_sampler.hpp>
 
+#include <arborio/swcio.hpp>
+
 #include <tinyopt/tinyopt.h>
 
 struct options {
@@ -157,5 +158,5 @@ arb::morphology read_swc(const std::string& path) {
     std::ifstream f(path);
     if (!f) throw std::runtime_error("unable to open SWC file: "+path);
 
-    return arb::morphology(arb::as_segment_tree(arb::parse_swc(f)));
+    return arb::morphology(arborio::as_segment_tree(arborio::parse_swc(f)));
 }
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index 5344e1f771575c7cbbd8090c38bd9d60c4a1786e..de05eeb06c4bdf9c764692a1493d21d1a3a1113c 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -40,7 +40,7 @@ set(pyarb_source
 # use by both the Python wrapper target (pyarb) and for the
 # unit tests of the C++ components in the Python wrapper.
 add_library(pyarb_obj OBJECT ${pyarb_source})
-target_link_libraries(pyarb_obj PRIVATE arbor pybind11::module)
+target_link_libraries(pyarb_obj PRIVATE arbor arborio pybind11::module)
 
 # The Python library. MODULE will make a Python-exclusive model.
 add_library(pyarb MODULE $<TARGET_OBJECTS:pyarb_obj>)
@@ -53,7 +53,7 @@ set_target_properties(pyarb PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}" SUFFIX "
 
 # This dependency has to be spelt out again, despite being added to
 # pyarb_obj because CMake.
-target_link_libraries(pyarb PRIVATE arbor pybind11::module)
+target_link_libraries(pyarb PRIVATE arbor arborio pybind11::module)
 
 # Add support for mpi4py if available.
 if (ARB_WITH_MPI)
diff --git a/python/morphology.cpp b/python/morphology.cpp
index ef44b2dad621947d3ce0fdeedf0489b69428a9a9..dd420097ce4d97f22ab3e4bd111bc580d6399bcd 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -6,13 +6,30 @@
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/segment_tree.hpp>
-#include <arbor/swcio.hpp>
+
+#include <arborio/swcio.hpp>
 
 #include "error.hpp"
 #include "strprintf.hpp"
 
 namespace pyarb {
 
+arb::segment_tree load_swc_neuron(const std::string& fname) {
+    std::ifstream fid{fname};
+    if (!fid.good()) {
+        throw pyarb_error(util::pprintf("can't open file '{}'", fname));
+    }
+    try {
+        auto records = arborio::parse_swc(fid, arborio::swc_mode::relaxed).records;
+        return arborio::load_swc_neuron(records);
+    }
+    catch (arborio::swc_error& e) {
+        // Try to produce helpful error messages for SWC parsing errors.
+        throw pyarb_error(
+                util::pprintf("NEURON SWC: error parsing {}: {}", fname, e.what()));
+    }
+}
+
 arb::segment_tree load_swc_allen(const std::string& fname, bool no_gaps=false) {
         std::ifstream fid{fname};
         if (!fid.good()) {
@@ -20,91 +37,10 @@ arb::segment_tree load_swc_allen(const std::string& fname, bool no_gaps=false) {
         }
         try {
             using namespace arb;
-            auto records = parse_swc(fid, swc_mode::relaxed).records;
-
-            // Assert that the file contains at least one sample.
-            if (records.empty()) {
-                throw pyarb_error("Allen SWC: empty file");
-            }
-
-            // Map of SWC record id to index in `records`.
-            std::unordered_map<int, std::size_t> record_index;
-
-            int soma_id = records[0].id;
-            record_index[soma_id] = 0;
-
-            // Assert that root sample has tag 1.
-            if (records[0].tag!=1) {
-                throw pyarb_error("Allen SWC: the soma record does not have tag 1");
-            }
-
-            for (std::size_t i = 1; i<records.size(); ++i) {
-                const auto& r = records[i];
-                record_index[r.id] = i;
-
-                // Assert that all samples have the same tag as their parent, except those attached to the soma.
-                if (auto p = r.parent_id; p!=soma_id && r.tag!=records[record_index[p]].tag) {
-                    throw pyarb_error(
-                        "Allen SWC: every record not attached to the soma must have the same tag as its parent");
-                }
-
-                // Assert that all non-root samples have a tag of 2, 3, or 4.
-                if (r.tag<2 || r.tag>4) {
-                    throw pyarb_error(
-                        "Allen SWC: every record must have a tag of 2, 3 or 4, except for the first which must have tag 1");
-                }
-            }
-
-            // Translate the morphology so that the soma is centered at the origin (0,0,0)
-            mpoint sloc{records[0].x, records[0].y, records[0].z, records[0].r};
-            for (auto& r: records) {
-                r.x -= sloc.x;
-                r.y -= sloc.y;
-                r.z -= sloc.z;
-            }
-
-            segment_tree tree;
-
-            // Model the spherical soma as a cylinder with length=2*radius.
-            // The cylinder is centred on the origin, and extended along the y axis.
-            double soma_rad = sloc.radius;
-            tree.append(mnpos, {0, -soma_rad, 0, soma_rad}, {0, soma_rad, 0, soma_rad}, 1);
-
-            // Build branches off soma.
-            std::unordered_map<int, msize_t> smap; // SWC record id -> segment id
-            std::set<int> unused_samples;
-            for (const auto& r: records) {
-                int id = r.id;
-                if (id==soma_id) continue;
-
-                // If sample i has the root as its parent don't create a segment.
-                if (r.parent_id==soma_id) {
-                    if (no_gaps) {
-                        // Assert that this branch starts on the "surface" of the spherical soma.
-                        auto d = std::fabs(soma_rad - std::sqrt(r.x*r.x + r.y*r.y + r.z*r.z));
-                        if (d>1e-3) { // 1 nm tolerance
-                            throw pyarb_error("Allen SWC: no gaps are allowed between the soma and any axons, dendrites or apical dendrites");
-                        }
-                    }
-                    // This maps axons and apical dendrites to soma.prox, and dendrites to soma.dist.
-                    smap[id] = r.tag==3? 0: mnpos;
-                    unused_samples.insert(id);
-                    continue;
-                }
-
-                const auto p = r.parent_id;
-                const auto& prox = records[record_index[p]];
-                smap[id] = tree.append(smap.at(p), {prox.x, prox.y, prox.z, prox.r}, {r.x, r.y, r.z, r.r}, r.tag);
-                unused_samples.erase(p);
-            }
-
-            if (!unused_samples.empty()) {
-                throw pyarb_error("Allen SWC: Every branch must contain at least one segment");
-            }
-
-            return tree;
+            auto records = arborio::parse_swc(fid, arborio::swc_mode::relaxed).records;
+            return arborio::load_swc_allen(records, no_gaps);
         }
-        catch (arb::swc_error& e) {
+        catch (arborio::swc_error& e) {
             // Try to produce helpful error messages for SWC parsing errors.
             throw pyarb_error(
                 util::pprintf("Allen SWC: error parsing {}: {}", fname, e.what()));
@@ -239,10 +175,10 @@ void register_morphology(pybind11::module& m) {
                 throw pyarb_error(util::pprintf("can't open file '{}'", fname));
             }
             try {
-                auto records = arb::parse_swc(fid).records;
-                return arb::as_segment_tree(records);
+                auto records = arborio::parse_swc(fid).records;
+                return arborio::as_segment_tree(records);
             }
-            catch (arb::swc_error& e) {
+            catch (arborio::swc_error& e) {
                 // Try to produce helpful error messages for SWC parsing errors.
                 throw pyarb_error(
                     util::pprintf("error parsing {}: {}", fname, e.what()));
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 33b43cabd681e7986804a0feed8e82afccbd2908..3fa832387e893e792ef7304dd6de792ef73d9486 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -220,7 +220,7 @@ endif()
 target_compile_options(unit PRIVATE ${ARB_CXXOPT_ARCH})
 target_compile_definitions(unit PRIVATE "-DDATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/swc\"")
 target_include_directories(unit PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
-target_link_libraries(unit PRIVATE gtest arbor arborenv arbor-private-headers arbor-sup)
+target_link_libraries(unit PRIVATE gtest arbor arborenv arborio arbor-private-headers arbor-sup)
 
 if(ARB_WITH_NEUROML)
     target_link_libraries(unit PRIVATE arbornml)
diff --git a/test/unit/test_morphology.cpp b/test/unit/test_morphology.cpp
index f35d8217a582049ff709a43b5537150f17a6b1f3..4f110981f03574841cb17857e01a5dc9fb41f8aa 100644
--- a/test/unit/test_morphology.cpp
+++ b/test/unit/test_morphology.cpp
@@ -10,7 +10,8 @@
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/segment_tree.hpp>
-#include <arbor/swcio.hpp>
+
+#include <arborio/swcio.hpp>
 
 #include "util/span.hpp"
 
@@ -318,10 +319,10 @@ TEST(morphology, swc) {
     }
 
     // Load swc samples from file.
-    auto swc = arb::parse_swc(fid, arb::swc_mode::strict);
+    auto swc = arborio::parse_swc(fid, arborio::swc_mode::strict);
 
     // Build a segmewnt_tree from swc samples.
-    auto sm = arb::as_segment_tree(swc);
+    auto sm = arborio::as_segment_tree(swc);
     EXPECT_EQ(5798u, sm.size()); // SWC data contains 5799 samples.
 
     // Test that the morphology contains the expected number of branches.
diff --git a/test/unit/test_swcio.cpp b/test/unit/test_swcio.cpp
index debcb072109ed22912b6bd68b3ed9b0d497bed93..9de95c337c6d77d3ed36f1ba1d8069f8844d85da 100644
--- a/test/unit/test_swcio.cpp
+++ b/test/unit/test_swcio.cpp
@@ -6,7 +6,8 @@
 #include <arbor/cable_cell.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/segment_tree.hpp>
-#include <arbor/swcio.hpp>
+
+#include <arborio/swcio.hpp>
 
 #include "../gtest.h"
 
@@ -16,7 +17,10 @@
 #   define DATADIR "../data"
 #endif
 
-using namespace arb;
+using namespace arborio;
+using arb::segment_tree;
+using arb::mpoint;
+using arb::mnpos;
 
 TEST(swc_record, construction) {
     swc_record record(1, 7, 1., 2., 3., 4., -1);
@@ -208,14 +212,14 @@ TEST(swc_parser, segment_tree) {
             {1, 1, 0., 0., 0., 1., -1},
             {5, 3, 1., 1., 1., 1., 2}
         };
-        EXPECT_THROW(as_segment_tree(swc), bad_swc_data);
+        EXPECT_THROW(as_segment_tree(swc), swc_no_such_parent);
     }
     {
         // A single SWC record will throw.
         std::vector<swc_record> swc{
             {1, 1, 0., 0., 0., 1., -1}
         };
-        EXPECT_THROW(as_segment_tree(swc), bad_swc_data);
+        EXPECT_THROW(as_segment_tree(swc), swc_bad_description);
     }
     {
         // Otherwise, ensure segment ends and tags correspond.
@@ -257,6 +261,666 @@ TEST(swc_parser, segment_tree) {
         EXPECT_EQ(p4, tree.segments()[3].dist);
     }
 }
+TEST(swc_parser, allen_compliant) {
+    using namespace arborio;
+    {
+        // One-point soma; interpretted as 1 segment
+        mpoint p0{0, 0, 0, 10};
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1}
+        };
+        segment_tree tree = load_swc_allen(swc);
+
+        mpoint prox{p0.x, p0.y-p0.radius, p0.z, p0.radius};
+        mpoint dist{p0.x, p0.y+p0.radius, p0.z, p0.radius};
+
+        ASSERT_EQ(1u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(prox,  tree.segments()[0].prox);
+        EXPECT_EQ(dist,  tree.segments()[0].dist);
+    }
+    {
+        // One-point soma, two-point dendrite
+        mpoint p0{0,   0, 0, 10};
+        mpoint p1{0,   0, 0,  5};
+        mpoint p2{0, 200, 0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2}
+        };
+        segment_tree tree = load_swc_allen(swc);
+
+        mpoint prox{0, -10, 0, 10};
+        mpoint dist{0,  10, 0, 10};
+
+        ASSERT_EQ(2u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(prox,  tree.segments()[0].prox);
+        EXPECT_EQ(dist,    tree.segments()[0].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[1]);
+        EXPECT_EQ(3,   tree.segments()[1].tag);
+        EXPECT_EQ(p1,  tree.segments()[1].prox);
+        EXPECT_EQ(p2,  tree.segments()[1].dist);
+    }
+    {
+        // 1-point soma, 2-point dendrite, 2-point axon
+        mpoint p0{0, 0,  0,  1};
+        mpoint p1{0, 0, 10, 10};
+        mpoint p2{0, 0, 20, 10};
+        mpoint p3{0, 0, 21, 10};
+        mpoint p4{0, 0, 30, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 2, p3.x, p3.y, p3.z, p3.radius,  1},
+            {5, 2, p4.x, p4.y, p4.z, p4.radius,  4}
+        };
+        segment_tree tree = load_swc_allen(swc);
+
+        mpoint prox{0, -1, 0, 1};
+        mpoint dist{0,  1, 0, 1};
+
+        ASSERT_EQ(3u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(prox,  tree.segments()[0].prox);
+        EXPECT_EQ(dist,  tree.segments()[0].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[1]);
+        EXPECT_EQ(3,   tree.segments()[1].tag);
+        EXPECT_EQ(p1,  tree.segments()[1].prox);
+        EXPECT_EQ(p2,  tree.segments()[1].dist);
+
+        EXPECT_EQ(mnpos,  tree.parents()[2]);
+        EXPECT_EQ(2,   tree.segments()[2].tag);
+        EXPECT_EQ(p3,  tree.segments()[2].prox);
+        EXPECT_EQ(p4,  tree.segments()[2].dist);
+    }
+}
+
+TEST(swc_parser, not_allen_compliant) {
+    using namespace arborio;
+    {
+        // multi-point soma
+        mpoint p0{0, 0, -10, 10};
+        mpoint p1{0, 0,   0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1}
+        };
+        EXPECT_THROW(load_swc_allen(swc), swc_non_spherical_soma);
+    }
+    {
+        // unsupported tag
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 5, p1.x, p1.y, p1.z, p1.radius,  1}
+        };
+        EXPECT_THROW(load_swc_allen(swc), swc_unsupported_tag);
+    }
+    {
+        // 1-point soma; 2-point dendrite; 1-point axon connected to the proximal end of the dendrite
+        mpoint p0{0, 0, -15, 10};
+        mpoint p1{0, 0,   0, 10};
+        mpoint p2{0, 0,  80, 10};
+        mpoint p3{0, 0, -80, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 2, p3.x, p3.y, p3.z, p3.radius,  2}
+        };
+        EXPECT_THROW(load_swc_allen(swc), swc_mismatched_tags);
+    }
+    {
+        // 1-point soma and 1-point dendrite
+        mpoint p0{0,   0, 0, 10};
+        mpoint p1{0, 200, 0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  1}
+        };
+        EXPECT_THROW(load_swc_allen(swc), swc_single_sample_segment);
+    }
+    {
+        // 2-point dendrite and 1-point soma at the end
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0,   0,  10,  1};
+        mpoint p2{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+            {1, 3, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 1, p2.x, p2.y, p2.z, p2.radius,  2}
+        };
+        EXPECT_THROW(load_swc_allen(swc), swc_no_soma);
+    }
+    {
+        // non-existent parent sample
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  4}
+        };
+        EXPECT_THROW(load_swc_allen(swc), swc_no_such_parent);
+    }
+    {
+        // parent sample is self
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  2}
+        };
+        EXPECT_THROW(load_swc_allen(swc), swc_no_such_parent);
+    }
+}
+
+TEST(swc_parser, neuron_compliant) {
+    using namespace arborio;
+    {
+        // One-point soma; interpretted as 1 segment
+
+        mpoint p0{0, 0, 0, 10};
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        mpoint prox{p0.x, p0.y-p0.radius, p0.z, p0.radius};
+        mpoint dist{p0.x, p0.y+p0.radius, p0.z, p0.radius};
+
+        ASSERT_EQ(1u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(prox,  tree.segments()[0].prox);
+        EXPECT_EQ(dist,  tree.segments()[0].dist);
+    }
+    {
+        // Two-point soma; interpretted as 1
+        mpoint p0{0, 0, -10, 10};
+        mpoint p1{0, 0,   0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        ASSERT_EQ(1u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(p0,    tree.segments()[0].prox);
+        EXPECT_EQ(p1,    tree.segments()[0].dist);
+    }
+    {
+        // Three-point soma; interpretted as 2 segments
+        mpoint p0{0, 0, -10, 10};
+        mpoint p1{0, 0,   0, 10};
+        mpoint p2{0, 0,  10, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 1, p2.x, p2.y, p2.z, p2.radius,  2}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        ASSERT_EQ(2u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(p0,    tree.segments()[0].prox);
+        EXPECT_EQ(p1,    tree.segments()[0].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[1]);
+        EXPECT_EQ(1,   tree.segments()[1].tag);
+        EXPECT_EQ(p1,  tree.segments()[1].prox);
+        EXPECT_EQ(p2,  tree.segments()[1].dist);
+    }
+    {
+        // 6-point soma; interpretted as 5 segments
+        mpoint p0{0, 0,  -5, 2};
+        mpoint p1{0, 0,   0, 5};
+        mpoint p2{0, 0,   2, 6};
+        mpoint p3{0, 0,   6, 1};
+        mpoint p4{0, 0,  10, 7};
+        mpoint p5{0, 0,  15, 2};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 1, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 1, p3.x, p3.y, p3.z, p3.radius,  3},
+            {5, 1, p4.x, p4.y, p4.z, p4.radius,  4},
+            {6, 1, p5.x, p5.y, p5.z, p5.radius,  5}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+
+        ASSERT_EQ(5u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(p0,    tree.segments()[0].prox);
+        EXPECT_EQ(p1,    tree.segments()[0].dist);
+
+        EXPECT_EQ(0u, tree.parents()[1]);
+        EXPECT_EQ(1,  tree.segments()[1].tag);
+        EXPECT_EQ(p1, tree.segments()[1].prox);
+        EXPECT_EQ(p2, tree.segments()[1].dist);
+
+        EXPECT_EQ(1u,  tree.parents()[2]);
+        EXPECT_EQ(1,   tree.segments()[2].tag);
+        EXPECT_EQ(p2,  tree.segments()[2].prox);
+        EXPECT_EQ(p3,  tree.segments()[2].dist);
+
+        EXPECT_EQ(2u, tree.parents()[3]);
+        EXPECT_EQ(1,  tree.segments()[3].tag);
+        EXPECT_EQ(p3, tree.segments()[3].prox);
+        EXPECT_EQ(p4, tree.segments()[3].dist);
+
+        EXPECT_EQ(3u, tree.parents()[4]);
+        EXPECT_EQ(1,  tree.segments()[4].tag);
+        EXPECT_EQ(p4, tree.segments()[4].prox);
+        EXPECT_EQ(p5, tree.segments()[4].dist);
+    }
+    {
+        // One-point soma, two-point dendrite
+        mpoint p0{0,   0, 0, 10};
+        mpoint p1{0,   0, 0,  5};
+        mpoint p2{0, 200, 0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        mpoint prox{0, -10, 0, 10};
+        mpoint dist{0,  10, 0, 10};
+
+        ASSERT_EQ(3u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(prox,  tree.segments()[0].prox);
+        EXPECT_EQ(p0,    tree.segments()[0].dist);
+
+        EXPECT_EQ(0u,    tree.parents()[1]);
+        EXPECT_EQ(1,     tree.segments()[1].tag);
+        EXPECT_EQ(p0,    tree.segments()[1].prox);
+        EXPECT_EQ(dist,  tree.segments()[1].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[2]);
+        EXPECT_EQ(3,   tree.segments()[2].tag);
+        EXPECT_EQ(p1,  tree.segments()[2].prox);
+        EXPECT_EQ(p2,  tree.segments()[2].dist);
+    }
+    {
+        // 6-point soma, 2-point dendrite
+        mpoint p0{0, 0,  -5, 2};
+        mpoint p1{0, 0,   0, 5};
+        mpoint p2{0, 0,   2, 6};
+        mpoint p3{0, 0,   6, 1};
+        mpoint p4{0, 0,  10, 7};
+        mpoint p5{0, 0,  15, 2};
+        mpoint p6{0, 0,  16, 1};
+        mpoint p7{0, 0,  55, 9};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 1, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 1, p3.x, p3.y, p3.z, p3.radius,  3},
+            {5, 1, p4.x, p4.y, p4.z, p4.radius,  4},
+            {6, 1, p5.x, p5.y, p5.z, p5.radius,  5},
+            {7, 3, p6.x, p6.y, p6.z, p6.radius,  6},
+            {8, 3, p7.x, p7.y, p7.z, p7.radius,  7}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        mpoint mid {0, 0, 5, 2.25};
+
+        ASSERT_EQ(7u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(p0,    tree.segments()[0].prox);
+        EXPECT_EQ(p1,    tree.segments()[0].dist);
+
+        EXPECT_EQ(0u, tree.parents()[1]);
+        EXPECT_EQ(1,  tree.segments()[1].tag);
+        EXPECT_EQ(p1, tree.segments()[1].prox);
+        EXPECT_EQ(p2, tree.segments()[1].dist);
+
+        EXPECT_EQ(1u,  tree.parents()[2]);
+        EXPECT_EQ(1,   tree.segments()[2].tag);
+        EXPECT_EQ(p2,  tree.segments()[2].prox);
+        EXPECT_EQ(mid, tree.segments()[2].dist);
+
+        EXPECT_EQ(2u,  tree.parents()[3]);
+        EXPECT_EQ(1,   tree.segments()[3].tag);
+        EXPECT_EQ(mid, tree.segments()[3].prox);
+        EXPECT_EQ(p3,  tree.segments()[3].dist);
+
+        EXPECT_EQ(3u, tree.parents()[4]);
+        EXPECT_EQ(1,  tree.segments()[4].tag);
+        EXPECT_EQ(p3, tree.segments()[4].prox);
+        EXPECT_EQ(p4, tree.segments()[4].dist);
+
+        EXPECT_EQ(4u, tree.parents()[5]);
+        EXPECT_EQ(1,  tree.segments()[5].tag);
+        EXPECT_EQ(p4, tree.segments()[5].prox);
+        EXPECT_EQ(p5, tree.segments()[5].dist);
+
+        EXPECT_EQ(2u, tree.parents()[6]);
+        EXPECT_EQ(3,  tree.segments()[6].tag);
+        EXPECT_EQ(p6, tree.segments()[6].prox);
+        EXPECT_EQ(p7, tree.segments()[6].dist);
+    }
+    {
+        // Two-point soma, two-point dendrite
+        mpoint p0{0,   0, -20, 10};
+        mpoint p1{0,   0,   0,  4};
+        mpoint p2{0,   0,   0, 10};
+        mpoint p3{0, 200,   0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 3, p3.x, p3.y, p3.z, p3.radius,  3}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        mpoint mid{0, 0, -10, 7};
+
+        ASSERT_EQ(3u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(p0,    tree.segments()[0].prox);
+        EXPECT_EQ(mid,   tree.segments()[0].dist);
+
+        EXPECT_EQ(0u,    tree.parents()[1]);
+        EXPECT_EQ(1,     tree.segments()[1].tag);
+        EXPECT_EQ(mid,   tree.segments()[1].prox);
+        EXPECT_EQ(p1,    tree.segments()[1].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[2]);
+        EXPECT_EQ(3,   tree.segments()[2].tag);
+        EXPECT_EQ(p2,  tree.segments()[2].prox);
+        EXPECT_EQ(p3,  tree.segments()[2].dist);
+    }
+    {
+        // 2-point soma; 2-point dendrite; 1-point axon connected to the proximal end of the dendrite
+        mpoint p0{0, 0, -15, 10};
+        mpoint p1{0, 0,   0,  3};
+        mpoint p2{0, 0,   0, 10};
+        mpoint p3{0, 0,  80, 10};
+        mpoint p4{0, 0, -80, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 3, p3.x, p3.y, p3.z, p3.radius,  3},
+            {5, 2, p4.x, p4.y, p4.z, p4.radius,  3}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        mpoint mid{0, 0, -7.5, 6.5};
+
+        ASSERT_EQ(4u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(p0,    tree.segments()[0].prox);
+        EXPECT_EQ(mid,   tree.segments()[0].dist);
+
+        EXPECT_EQ(0u,    tree.parents()[1]);
+        EXPECT_EQ(1,     tree.segments()[1].tag);
+        EXPECT_EQ(mid,   tree.segments()[1].prox);
+        EXPECT_EQ(p1,    tree.segments()[1].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[2]);
+        EXPECT_EQ(3,   tree.segments()[2].tag);
+        EXPECT_EQ(p2,  tree.segments()[2].prox);
+        EXPECT_EQ(p3,  tree.segments()[2].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[3]);
+        EXPECT_EQ(2,   tree.segments()[3].tag);
+        EXPECT_EQ(p2,  tree.segments()[3].prox);
+        EXPECT_EQ(p4,  tree.segments()[3].dist);
+    }
+    {
+        // 2-point soma, 2-point dendrite, 2-point axon
+        mpoint p0{0, 0,  0,  1};
+        mpoint p1{0, 0,  9,  2};
+        mpoint p2{0, 0, 10, 10};
+        mpoint p3{0, 0, 20, 10};
+        mpoint p4{0, 0, 21, 10};
+        mpoint p5{0, 0, 30, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 3, p3.x, p3.y, p3.z, p3.radius,  3},
+            {5, 2, p4.x, p4.y, p4.z, p4.radius,  4},
+            {6, 2, p5.x, p5.y, p5.z, p5.radius,  5}
+        };
+        segment_tree tree = load_swc_neuron(swc);
+
+        mpoint mid{0, 0, 4.5, 1.5};
+
+        ASSERT_EQ(5u, tree.segments().size());
+
+        EXPECT_EQ(mnpos, tree.parents()[0]);
+        EXPECT_EQ(1,     tree.segments()[0].tag);
+        EXPECT_EQ(p0,    tree.segments()[0].prox);
+        EXPECT_EQ(mid,   tree.segments()[0].dist);
+
+        EXPECT_EQ(0u,    tree.parents()[1]);
+        EXPECT_EQ(1,     tree.segments()[1].tag);
+        EXPECT_EQ(mid,   tree.segments()[1].prox);
+        EXPECT_EQ(p1,    tree.segments()[1].dist);
+
+        EXPECT_EQ(0u,  tree.parents()[2]);
+        EXPECT_EQ(3,   tree.segments()[2].tag);
+        EXPECT_EQ(p2,  tree.segments()[2].prox);
+        EXPECT_EQ(p3,  tree.segments()[2].dist);
+
+        EXPECT_EQ(2u,  tree.parents()[3]);
+        EXPECT_EQ(2,   tree.segments()[3].tag);
+        EXPECT_EQ(p3,  tree.segments()[3].prox);
+        EXPECT_EQ(p4,  tree.segments()[3].dist);
+
+        EXPECT_EQ(3u,  tree.parents()[4]);
+        EXPECT_EQ(2,   tree.segments()[4].tag);
+        EXPECT_EQ(p4,  tree.segments()[4].prox);
+        EXPECT_EQ(p5,  tree.segments()[4].dist);
+    }
+}
+
+TEST(swc_parser, not_neuron_compliant) {
+    using namespace arborio;
+    {
+        // Two-point collocated soma
+        mpoint p0{0, 0, 0, 5};
+        mpoint p1{0, 0, 0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1}
+        };
+
+        EXPECT_THROW(load_swc_neuron(swc), swc_collocated_soma);
+    }
+    {
+        // 3-point soma joined in the middle (1-0-2)
+        mpoint p0{0, 0,   0, 10};
+        mpoint p1{0, 0, -10, 10};
+        mpoint p2{0, 0,  10, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 1, p2.x, p2.y, p2.z, p2.radius,  1}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_non_serial_soma);
+    }
+    {
+        // 4-point branching soma
+        mpoint p0{0,  0,  0, 10};
+        mpoint p1{0,  0, 10, 10};
+        mpoint p2{0, -5, 20, 10};
+        mpoint p3{0,  5, 20, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 1, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 1, p3.x, p3.y, p3.z, p3.radius,  2}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_non_serial_soma);
+    }
+    {
+        // 1-point soma and 1-point dendrite
+        mpoint p0{0,   0, 0, 10};
+        mpoint p1{0, 200, 0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 3, p1.x, p1.y, p1.z, p1.radius,  1}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_single_sample_segment);
+    }
+    {
+        // 2-point soma and 1-point dendrite
+        mpoint p0{0,   0, -10, 10};
+        mpoint p1{0,   0,   0, 10};
+        mpoint p2{0, 200,   0, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_single_sample_segment);
+    }
+    {
+        // 2-point soma and two 1-point dendrite
+        mpoint p0{0,  0, -20, 10};
+        mpoint p1{0,  0,   0, 10};
+        mpoint p2{0, -5,  80, 10};
+        mpoint p3{0,  5, -90, 10};
+
+        std::vector<swc_record> swc{
+            {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+            {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+            {3, 3, p2.x, p2.y, p2.z, p2.radius,  2},
+            {4, 3, p3.x, p3.y, p3.z, p3.radius,  2}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_single_sample_segment);
+    }
+    {
+        // 2-point dendrite and 1-point soma at the end
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0,   0,  10,  1};
+        mpoint p2{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+                {1, 3, p0.x, p0.y, p0.z, p0.radius, -1},
+                {2, 3, p1.x, p1.y, p1.z, p1.radius,  1},
+                {3, 1, p2.x, p2.y, p2.z, p2.radius,  2}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_no_soma);
+    }
+    {
+        // 3-point non-consecutive soma and a 2 point dendrite
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0,   0,  10,  1};
+        mpoint p2{0,   0,  10, 10};
+        mpoint p3{0, 200,  20, 10};
+        mpoint p4{0,   0,  20,  1};
+
+        std::vector<swc_record> swc{
+                {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+                {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+                {3, 3, p2.x, p2.y, p2.z, p2.radius,  2},
+                {4, 3, p3.x, p3.y, p3.z, p3.radius,  3},
+                {5, 1, p4.x, p4.y, p4.z, p4.radius,  2}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_non_consecutive_soma);
+    }
+    {
+        // 3-point soma and a 2 point dendrite connected in the middle of the soma
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0,   0,  10,  1};
+        mpoint p2{0,   0,  20,  1};
+        mpoint p3{0,   0,  10, 10};
+        mpoint p4{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+                {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+                {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+                {3, 1, p2.x, p2.y, p2.z, p2.radius,  2},
+                {4, 3, p3.x, p3.y, p3.z, p3.radius,  2},
+                {5, 3, p4.x, p4.y, p4.z, p4.radius,  4}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_branchy_soma);
+    }
+    {
+        // non-existent parent sample
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0,   0,  10,  1};
+        mpoint p2{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+                {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+                {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+                {3, 3, p2.x, p2.y, p2.z, p2.radius,  4}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_no_such_parent);
+    }
+    {
+        // parent sample is self
+        mpoint p0{0,   0,   0,  1};
+        mpoint p1{0,   0,  10,  1};
+        mpoint p2{0, 200,  20, 10};
+
+        std::vector<swc_record> swc{
+                {1, 1, p0.x, p0.y, p0.z, p0.radius, -1},
+                {2, 1, p1.x, p1.y, p1.z, p1.radius,  1},
+                {3, 3, p2.x, p2.y, p2.z, p2.radius,  3}
+        };
+        EXPECT_THROW(load_swc_neuron(swc), swc_no_such_parent);
+    }
+}
 
 // hipcc bug in reading DATADIR
 #ifndef ARB_HIP