diff --git a/python/morphology.cpp b/python/morphology.cpp index ec634edd20673fe187320fc428bd01934f6f5d20..c5b3d20920f0d5f313145bed8b534d1e74a53637 100644 --- a/python/morphology.cpp +++ b/python/morphology.cpp @@ -13,6 +13,102 @@ namespace pyarb { +arb::segment_tree load_swc_allen(const std::string& fname, bool no_gaps=false) { + std::ifstream fid{fname}; + if (!fid.good()) { + throw pyarb_error(util::pprintf("can't open file '{}'", fname)); + } + try { + using namespace arb; + auto records = parse_swc_file(fid); + + // Assert that the file contains at least one sample. + if (records.empty()) { + throw pyarb_error("Allen SWC: empty file"); + } + + // 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"); + } + + // Assert that all non-root samples have a tag of 2, 3, or 4. + auto it = std::find_if( + records.begin()+1, records.end(), + [](auto& r){return r.tag<2 || r.tag>4;}); + if (it!=records.end()) { + 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"); + } + + // Assert that all samples have the same tag as their parent, except + // those attached to the soma. + it = std::find_if( + records.begin()+1, records.end(), + [&records](auto& r){auto p = r.parent_id; return p && r.tag!=records[p].tag;}); + if (it!=records.end()) { + throw pyarb_error( + "Allen SWC: every record not attached to the soma must have the same tag as its parent"); + } + + // 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<msize_t, msize_t> pmap; + std::set<msize_t> unused_samples; + const auto nrec = records.size(); + for (unsigned i=1; i<nrec; ++i) { + const auto& r = records[i]; + // If sample i has the root as its parent don't create a segment. + if (r.parent_id==0) { + 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. + pmap[i] = r.tag==3? 0: mnpos; + unused_samples.insert(i); + continue; + } + + const auto p = r.parent_id; + const auto& prox = records[p]; + const auto& dist = records[i]; + tree.append(pmap.at(p), {prox.x, prox.y, prox.z, prox.r}, {dist.x, dist.y, dist.z, dist.r}, r.tag); + pmap[i] = tree.size() - 1; + unused_samples.erase(p); + } + + if (!unused_samples.empty()) { + throw pyarb_error("Allen SWC: Every branch must contain at least one segment"); + } + + return tree; + } + catch (arb::swc_error& e) { + // Try to produce helpful error messages for SWC parsing errors. + throw pyarb_error( + util::pprintf("Allen SWC: error parsing line {} of '{}': {}", + e.line_number, fname, e.what())); + } +} + void register_morphology(pybind11::module& m) { using namespace pybind11::literals; @@ -142,7 +238,6 @@ void register_morphology(pybind11::module& m) { } try { auto records = arb::parse_swc_file(fid); - arb::swc_canonicalize(records); return arb::swc_as_segment_tree(records); } catch (arb::swc_error& e) { @@ -154,6 +249,25 @@ void register_morphology(pybind11::module& m) { }, "Load an swc file and convert to a segment_tree."); + m.def("load_swc_allen", &load_swc_allen, + "filename"_a, "no_gaps"_a=false, + "Generate a segment tree from an SWC file following the rules prescribed by\n" + "AllenDB and Sonata. Specifically:\n" + "* The first sample (the root) is treated as the center of the soma.\n" + "* The first morphology is translated such that the soma is centered at (0,0,0).\n" + "* The first sample has tag 1 (soma).\n" + "* All other samples have tags 2, 3 or 4 (axon, apic and dend respectively)\n" + "SONATA prescribes that there should be no gaps, however the models in AllenDB\n" + "have gaps between the start of sections and the soma. The flag no_gaps can be\n" + "used to enforce this requirement.\n" + "\n" + "Arbor does not support modelling the soma as a sphere, so a cylinder with length\n" + "equal to the soma diameter is used. The cylinder is centered on the origin, and\n" + "aligned along the z axis.\n" + "Axons and apical dendrites are attached to the proximal end of the cylinder, and\n" + "dendrites to the distal end, with a gap between the start of each branch and the\n" + "end of the soma cylinder to which it is attached."); + // arb::morphology pybind11::class_<arb::morphology> morph(m, "morphology");