diff --git a/arbor/morph/segment_tree.cpp b/arbor/morph/segment_tree.cpp index 43c353b5bc08058d15b52a9996867473f6bde411..3249ed5fdfab3f63a1b5f08a5c5cd56c375560d1 100644 --- a/arbor/morph/segment_tree.cpp +++ b/arbor/morph/segment_tree.cpp @@ -26,7 +26,7 @@ node_p yes = [](node_t) { return true; }; std::map<msize_t, std::vector<msize_t>> tree_to_children(const segment_tree& tree) { const auto& parents = tree.parents(); std::map<msize_t, std::vector<msize_t>> result; - for (auto ix = 0; ix < tree.size(); ++ix) result[parents[ix]].push_back(ix); + for (msize_t ix = 0; ix < tree.size(); ++ix) result[parents[ix]].push_back(ix); for (auto& [k, v]: result) std::sort(v.begin(), v.end()); return result; } @@ -105,7 +105,7 @@ equivalent(const segment_tree& a, auto bs = fetch_children(b_cursor, b.segments(), b_children_of); todo.pop_back(); if (as.size() != bs.size()) return false; - for (auto ix = 0; ix < as.size(); ++ix) { + for (msize_t ix = 0; ix < as.size(); ++ix) { if ((as[ix].prox != bs[ix].prox) || (as[ix].dist != bs[ix].dist) || (as[ix].tag != bs[ix].tag)) return false; diff --git a/arborio/include/arborio/neurolucida.hpp b/arborio/include/arborio/neurolucida.hpp index 3fd4d1fc8f83e694c46747d46ea7fb88f7ead720..a260c784eac56ad1ace95e232f1f72058b60699b 100644 --- a/arborio/include/arborio/neurolucida.hpp +++ b/arborio/include/arborio/neurolucida.hpp @@ -45,8 +45,10 @@ struct asc_morphology { // Perform the parsing of the input as a string. ARB_ARBORIO_API asc_morphology parse_asc_string(const char* input); +ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input); // Load asc morphology from file with name filename. ARB_ARBORIO_API asc_morphology load_asc(std::string filename); +ARB_ARBORIO_API arb::segment_tree load_asc_raw(std::string filename); } // namespace arborio diff --git a/arborio/neurolucida.cpp b/arborio/neurolucida.cpp index 832a7d2451f3b3a2a55bc8966c2a4c265076c1a9..4c8d46a1f52ba62cef6e4b00b84859b51c2a703b 100644 --- a/arborio/neurolucida.cpp +++ b/arborio/neurolucida.cpp @@ -610,7 +610,7 @@ parse_hopefully<sub_tree> parse_sub_tree(asc::lexer& L) { // Perform the parsing of the input as a string. -asc_morphology parse_asc_string(const char* input) { +ARB_ARBORIO_API arb::segment_tree parse_asc_string_raw(const char* input) { asc::lexer lexer(input); std::vector<sub_tree> sub_trees; @@ -768,6 +768,14 @@ asc_morphology parse_asc_string(const char* input) { } } + return stree; +} + + +ARB_ARBORIO_API asc_morphology parse_asc_string(const char* input) { + // Parse segment tree + arb::segment_tree stree = parse_asc_string_raw(input); + // Construct the morphology. arb::morphology morphology(stree); @@ -781,7 +789,8 @@ asc_morphology parse_asc_string(const char* input) { return {stree, std::move(morphology), std::move(labels)}; } -ARB_ARBORIO_API asc_morphology load_asc(std::string filename) { + +inline std::string read_file(std::string filename) { std::ifstream fid(filename); if (!fid.good()) { @@ -796,9 +805,21 @@ ARB_ARBORIO_API asc_morphology load_asc(std::string filename) { fstr.assign((std::istreambuf_iterator<char>(fid)), std::istreambuf_iterator<char>()); + return fstr; +} + +ARB_ARBORIO_API asc_morphology load_asc(std::string filename) { + std::string fstr = read_file(filename); return parse_asc_string(fstr.c_str()); } + +ARB_ARBORIO_API arb::segment_tree load_asc_raw(std::string filename) { + std::string fstr = read_file(filename); + return parse_asc_string_raw(fstr.c_str()); +} + + } // namespace arborio diff --git a/python/morphology.cpp b/python/morphology.cpp index 52bf43bba503f55c650531088df0a9c5ef536fd9..d1ca3446855d70cfb46bbe6cf24d8ab0bb11fd5c 100644 --- a/python/morphology.cpp +++ b/python/morphology.cpp @@ -356,18 +356,25 @@ void register_morphology(py::module& m) { [](const arborio::asc_morphology& m) {return label_dict_proxy(m.labels);}, "The four canonical regions are labeled 'soma', 'axon', 'dend' and 'apic'."); + using asc_morph_or_tree = std::variant<arb::segment_tree, arborio::asc_morphology>; + m.def("load_asc", - [](py::object fn) { + [](py::object fn, bool raw) -> asc_morph_or_tree { try { auto contents = util::read_file_or_buffer(fn); - return arborio::load_asc(contents); + if (raw) { + return arborio::parse_asc_string_raw(contents.c_str()); + } + return arborio::parse_asc_string(contents.c_str()); } catch (std::exception& e) { // Try to produce helpful error messages for SWC parsing errors. throw pyarb_error(util::pprintf("error loading neurolucida asc file: {}", e.what())); } }, - "filename"_a, "Load a morphology and meta data from a Neurolucida ASCII .asc file."); + "filename_or_stream"_a, + pybind11::arg_v("raw", false, "Return a segment tree instead of a fully formed morphology"), + "Load a morphology or segment_tree and meta data from a Neurolucida ASCII .asc file."); #ifdef ARB_NEUROML_ENABLED diff --git a/python/test/unit/test_io.py b/python/test/unit/test_io.py index b3eda93b99f6ca19d937c951f08882538376215a..3c2eea72688e2cc06d689a4ac187996ffb2a8864 100644 --- a/python/test/unit/test_io.py +++ b/python/test/unit/test_io.py @@ -5,6 +5,8 @@ import arbor as A from pathlib import Path from tempfile import TemporaryDirectory as TD from io import StringIO +from functools import partial + acc = """(arbor-component (meta-data @@ -62,32 +64,141 @@ acc = """(arbor-component """ -class TestAccIo(unittest.TestCase): - def test_stringio(self): - sio = StringIO(acc) - A.load_component(sio) +swc_arbor = """1 1 -5.0 0.0 0.0 5.0 -1 +2 1 0.0 0.0 0.0 5.0 1 +3 1 5.0 0.0 0.0 5.0 2 +""" - def test_fileio(self): - fn = "test.acc" + +swc_neuron = """1 1 0.1 0.2 0.3 0.4 -1 +""" + + +asc = """((CellBody)\ + (0 0 0 4)\ +)\ +((Dendrite)\ + (0 2 0 2)\ + (0 5 0 2)\ + (\ + (-5 5 0 2)\ + |\ + (6 5 0 2)\ + )\ +)\ +((Axon)\ + (0 -2 0 2)\ + (0 -5 0 2)\ + (\ + (-5 -5 0 2)\ + |\ + (6 -5 0 2)\ + )\ +) +""" + + +def load_string(loaders, morph_str): + for loader in loaders: + sio = StringIO(morph_str) + loader(sio) + + +def load_file(loaders, morph_str, morph_fn): + for loader in loaders: with TD() as tmp: tmp = Path(tmp) - with open(tmp / fn, "w") as fd: - fd.write(acc) - with open(tmp / fn) as fd: - A.load_component(fd) + with open(tmp / morph_fn, "w") as fd: + fd.write(morph_str) + with open(tmp / morph_fn) as fd: + loader(fd) - def test_nameio(self): - fn = "test.acc" + +def load_name(loaders, morph_str, morph_fn): + for loader in loaders: with TD() as tmp: tmp = Path(tmp) - with open(tmp / fn, "w") as fd: - fd.write(acc) - A.load_component(str(tmp / fn)) + with open(tmp / morph_fn, "w") as fd: + fd.write(morph_str) + loader(str(tmp / morph_fn)) - def test_pathio(self): - fn = "test.acc" + +def load_pathio(loaders, morph_str, morph_fn): + for loader in loaders: with TD() as tmp: tmp = Path(tmp) - with open(tmp / fn, "w") as fd: - fd.write(acc) - A.load_component(tmp / fn) + with open(tmp / morph_fn, "w") as fd: + fd.write(morph_str) + loader(tmp / morph_fn) + + +class TestAccIo(unittest.TestCase): + @staticmethod + def loaders(): + return (A.load_component,) + + def test_stringio(self): + load_string(self.loaders(), acc) + + def test_fileio(self): + load_file(self.loaders(), acc, "test.acc") + + def test_nameio(self): + load_name(self.loaders(), acc, "test.acc") + + def test_pathio(self): + load_pathio(self.loaders(), acc, "test.acc") + + +class TestSwcArborIo(unittest.TestCase): + @staticmethod + def loaders(): + return (A.load_swc_arbor, partial(A.load_swc_arbor, raw=True)) + + def test_stringio(self): + load_string(self.loaders(), swc_arbor) + + def test_fileio(self): + load_file(self.loaders(), swc_arbor, "test.swc") + + def test_nameio(self): + load_name(self.loaders(), swc_arbor, "test.swc") + + def test_pathio(self): + load_pathio(self.loaders(), swc_arbor, "test.swc") + + +class TestSwcNeuronIo(unittest.TestCase): + @staticmethod + def loaders(): + return (A.load_swc_neuron, partial(A.load_swc_neuron, raw=True)) + + def test_stringio(self): + load_string(self.loaders(), swc_neuron) + + def test_fileio(self): + load_file(self.loaders(), swc_neuron, "test.swc") + + def test_nameio(self): + load_name(self.loaders(), swc_neuron, "test.swc") + + def test_pathio(self): + load_pathio(self.loaders(), swc_neuron, "test.swc") + + +class TestAscIo(unittest.TestCase): + @staticmethod + def loaders(): + return (A.load_asc, partial(A.load_asc, raw=True)) + + def test_stringio(self): + load_string(self.loaders(), asc) + + def test_fileio(self): + load_file(self.loaders(), asc, "test.asc") + + def test_nameio(self): + load_name(self.loaders(), asc, "test.asc") + + def test_pathio(self): + load_pathio(self.loaders(), asc, "test.asc")