From 2c08a566bfc78ab10a4f5ab22fc60c7fcb7e2e4c Mon Sep 17 00:00:00 2001 From: Thorsten Hater <24411438+thorstenhater@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:34:05 +0200 Subject: [PATCH] ASCII art for segment tree (#2224) Allow ASCII rendering of segment trees and morphologies for debugging Example `segment_tree` ``` | | +-[-- id=1161 --]-+-[-- id=1709 --]---[-- id=2234 --]-+-[-- id=2625 --]---[-- id=3172 --] | | | | +-[-- id=2928 --]---[-- id=3765 --] | | | | | | | +-[-- id=1721 --]---[-- id=3504 --]---[-- id=3846 --] | | | | | +-[-- id=1867 --] | | | +-[-- id=1451 --]-+-[-- id=2356 --] | +-[-- id=2471 --]---[-- id=2670 --]---[-- id=4031 --] | | +-[-- id=35 --]---[-- id=263 --]-+-[-- id=409 --]-+-[-- id=475 --]---[-- id=1347 --] | | +-[-- id=480 --]-+-[-- id=1764 --]-+-[-- id=1912 --] | | | | +-[-- id=2975 --] | | | | | | | +-[-- id=2047 --] | | | | | +-[-- id=3124 --] | | | +-[-- id=548 --]-+-[-- id=1436 --]---[-- id=1699 --] | | +-[-- id=2194 --] | | +-[-- id=3250 --] | | +-[-- id=3281 --] | | +-[-- id=3589 --] | | | +-[-- id=651 --]-+-[-- id=943 --]-+-[-- id=1024 --]-+-[-- id=1070 --]-+-[-- id=1477 --]-+-[-- id=1939 --] | | | | | | +-[-- id=3457 --] | | | | | | | | | | | +-[-- id=2475 --] | | | | | | | | | +-[-- id=1929 --] | | | | | | | +-[-- id=1314 --]---[-- id=2677 --] | | | | | +-[-- id=1793 --]---[-- id=2792 --] | | | +-[-- id=1391 --]-+-[-- id=2548 --] | +-[-- id=2743 --] | | +-[-- id=1621 --]---[-- id=3879 --] +-[-- id=1914 --]-+-[-- id=2572 --] | +-[-- id=3385 --] | +-[-- id=3991 --] ``` and the same snippet in the equivalent morphology ``` +-<-- id=217 len=1 -->-+-<-- id=301 len=1 -->-+-<-- id=310 len=2 -->-+-<-- id=2246 len=2 --> | | | +-<-- id=2830 len=1 --> | | | | | +-<-- id=323 len=1 -->-+-<-- id=361 len=3 --> | | | +-<-- id=1696 len=1 -->-+-<-- id=2166 len=1 --> | | | | +-<-- id=2260 len=2 --> | | | | | | | +-<-- id=1735 len=1 -->-+-<-- id=1818 len=1 --> | | | | +-<-- id=2118 len=1 --> | | | | | | | +-<-- id=1909 len=3 --> | | | +-<-- id=2772 len=1 --> | | | +-<-- id=3004 len=1 --> | | | | | +-<-- id=344 len=1 -->-+-<-- id=714 len=1 -->-+-<-- id=937 len=2 --> | | | | +-<-- id=1292 len=2 --> | | | | +-<-- id=1698 len=1 -->-+-<-- id=2036 len=1 --> | | | | +-<-- id=2382 len=1 --> | | | | | | | | | | | +-<-- id=864 len=4 --> | | | +-<-- id=2412 len=2 --> | | | | | +-<-- id=1570 len=1 --> | | | +-<-- id=355 len=1 -->-+-<-- id=928 len=1 -->-+-<-- id=2200 len=2 --> | | | +-<-- id=2427 len=1 --> | | | +-<-- id=2523 len=1 --> | | | +-<-- id=2988 len=1 --> | | | | | +-<-- id=1303 len=1 -->-+-<-- id=2169 len=1 -->-+-<-- id=2334 len=1 --> | | | | +-<-- id=2708 len=2 --> | | | | | | | +-<-- id=2647 len=1 --> | | | | | +-<-- id=1399 len=1 --> | | | +-<-- id=704 len=2 --> | +-<-- id=1157 len=2 --> ``` Closes #2132 --- arbor/include/arbor/morph/segment_tree.hpp | 3 +- arbor/morph/segment_tree.cpp | 2 - arborio/CMakeLists.txt | 3 +- arborio/debug.cpp | 99 ++++++++++++++++++++++ arborio/include/arborio/debug.hpp | 16 ++++ doc/cpp/morphology.rst | 69 ++++++++++++++- doc/python/morphology.rst | 12 +++ python/morphology.cpp | 7 ++ test/unit/CMakeLists.txt | 1 + test/unit/test_asc.cpp | 3 - test/unit/test_debug.cpp | 49 +++++++++++ 11 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 arborio/debug.cpp create mode 100644 arborio/include/arborio/debug.hpp create mode 100644 test/unit/test_debug.cpp diff --git a/arbor/include/arbor/morph/segment_tree.hpp b/arbor/include/arbor/morph/segment_tree.hpp index 0d9c6b56..814d7d2c 100644 --- a/arbor/include/arbor/morph/segment_tree.hpp +++ b/arbor/include/arbor/morph/segment_tree.hpp @@ -88,5 +88,4 @@ apply(const segment_tree&, const isometry&); // Roots of regions of specific tag in segment tree ARB_ARBOR_API std::vector<msize_t> tag_roots(const segment_tree& in, int tag); - -} // namespace arb \ No newline at end of file +} // namespace arb diff --git a/arbor/morph/segment_tree.cpp b/arbor/morph/segment_tree.cpp index 92eb13d6..551e213e 100644 --- a/arbor/morph/segment_tree.cpp +++ b/arbor/morph/segment_tree.cpp @@ -1,4 +1,3 @@ -#include <stdexcept> #include <map> #include <vector> @@ -246,6 +245,5 @@ ARB_ARBOR_API std::vector<msize_t> tag_roots(const segment_tree& t, int tag) { return tag_roots; } - } // namespace arb diff --git a/arborio/CMakeLists.txt b/arborio/CMakeLists.txt index 5a4bae42..6016a341 100644 --- a/arborio/CMakeLists.txt +++ b/arborio/CMakeLists.txt @@ -7,7 +7,8 @@ set(arborio-sources label_parse.cpp neuroml.cpp networkio.cpp - nml_parse_morphology.cpp) + nml_parse_morphology.cpp + debug.cpp) add_library(arborio ${arborio-sources}) diff --git a/arborio/debug.cpp b/arborio/debug.cpp new file mode 100644 index 00000000..17becc18 --- /dev/null +++ b/arborio/debug.cpp @@ -0,0 +1,99 @@ +#include <arborio/debug.hpp> + +#include <arbor/morph/primitives.hpp> + +#include <map> +#include <numeric> + +namespace arborio { + +template <typename T, typename P> +std::vector<std::string> render(const T& tree, + arb::msize_t root, + const std::multimap<arb::msize_t, arb::msize_t>& children, + P print) { + // ASCII art elements + // TODO these could be customizable, but need conformant lengths + const std::string vline = " | "; + const std::string hline = "---"; + const std::string blank = " "; + const std::string split = "-+-"; + const std::string start = " +-"; + + auto n_child = children.count(root); + auto seg = print(root, tree); + if (0 == n_child) return {seg}; + + auto sep = std::string(seg.size(), ' '); + const auto& [beg, end] = children.equal_range(root); + + std::vector res = {seg}; + arb::msize_t cdx = 0; + for (auto it = beg; it != end; ++it) { + const auto& [parent, child] = *it; + auto rows = render(tree, child, children, print); + auto rdx = 0; + for (const auto& row: rows) { + // Append the first row directly onto our segments, this [- -] -- [- -] + if (rdx == 0) { + // The first child of a node may span a sub-tree + if (cdx == 0) { + res.back() += split + row; + } else { + // Other children get connected to the vertical line + res.push_back(sep + start + row); + } + cdx++; + } else { + // If there are more children, extend the subtree by showing a + // vertical line + res.push_back(sep + (cdx < n_child ? vline : blank) + row); + } + ++rdx; + } + } + // res.push_back(sep); + return res; +} + +ARB_ARBORIO_API std::string default_segment_printer(const arb::msize_t id, const arb::segment_tree&) { + auto lbl = (id == arb::mnpos) ? "(root)" : std::to_string(id); + return "[-- id=" + lbl + " --]" ; +} + +std::string ARB_ARBORIO_API default_branch_printer(const arb::msize_t id, const arb::morphology& mrf) { + auto lbl = (id == arb::mnpos) ? std::string("(root)") : std::to_string(id); + return "<-- id=" + std::to_string(id) + " len=" + std::to_string(mrf.branch_segments(id).size()) + " -->" ; +} + +ARB_ARBORIO_API std::string show(const arb::segment_tree& tree) { + if (tree.empty()) return ""; + + std::multimap<arb::msize_t, arb::msize_t> children; + const auto& ps = tree.parents(); + for (arb::msize_t idx = 0; idx < tree.size(); ++idx) { + auto parent = ps[idx]; + children.emplace(parent, idx); + } + + auto res = render(tree, 0, children, default_segment_printer); + return std::accumulate(res.begin(), res.end(), + std::string{}, + [](auto lhs, auto rhs) { return lhs + rhs + "\n"; }); +} + +ARB_ARBORIO_API std::string show(const arb::morphology& mrf) { + if (mrf.empty()) return ""; + + std::multimap<arb::msize_t, arb::msize_t> children; + for (arb::msize_t idx = 0; idx < mrf.num_branches(); ++idx) { + auto parent = mrf.branch_parent(idx); + children.emplace(parent, idx); + } + + auto res = render(mrf, 0, children, default_branch_printer); + return std::accumulate(res.begin(), res.end(), + std::string{}, + [](auto lhs, auto rhs) { return lhs + rhs + "\n"; }); +} +} diff --git a/arborio/include/arborio/debug.hpp b/arborio/include/arborio/debug.hpp new file mode 100644 index 00000000..84faf529 --- /dev/null +++ b/arborio/include/arborio/debug.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include <string> +#include <functional> +#include <vector> + +#include <arbor/export.hpp> +#include <arborio/export.hpp> + +#include <arbor/morph/segment_tree.hpp> +#include <arbor/morph/morphology.hpp> + +namespace arborio { +ARB_ARBORIO_API std::string show(const arb::segment_tree&); +ARB_ARBORIO_API std::string show(const arb::morphology&); +} diff --git a/doc/cpp/morphology.rst b/doc/cpp/morphology.rst index 3bf52f7f..0de4df53 100644 --- a/doc/cpp/morphology.rst +++ b/doc/cpp/morphology.rst @@ -31,7 +31,6 @@ consistent parent-child indexing, and with ``n`` segments numbered from ``0`` to .. cpp:class:: segment_tree - .. cpp:function:: segment_tree() Construct an empty segment tree. @@ -66,6 +65,10 @@ consistent parent-child indexing, and with ``n`` segments numbered from ``0`` to A list of the segments. +.. cpp:function:: std::string show(const arb::segment_tree&) + + Return a string representation of the tree. + .. cpp:function:: std::pair<segment_tree, segment_tree> split_at(const segment_tree& t, msize_t id) Split a segment_tree into a pair of subtrees at the given id, @@ -100,9 +103,47 @@ consistent parent-child indexing, and with ``n`` segments numbered from ``0`` to Morphology API -------------- -.. todo:: +.. cpp:class:: morphology + + .. cpp:function:: morphology() + + Construct an empty morphology. + + .. cpp:function:: morphology(const segment_tree&) + + Construct a morphology from a segment tree. + + .. cpp:function:: segment_tree to_segment_tree() const + + Reconcstruct the underlying segment tree. + + .. cpp:function:: bool empty() const + + Is this the trivial morphology? + + .. cpp:function:: msize_t num_branches() const + + The number of branches in the morphology. + + .. cpp:function:: msize_t branch_parent(msize_t b) const + + The parent branch of branch ``b``. Return ``mnpos`` if branch has no parent. - Describe morphology methods. + .. cpp:function:: const std::vector<msize_t>& branch_children(msize_t b) const + + The child branches of branch ``b``. If b is ``mnpos``, return root branches. + + .. cpp:function:: const std::vector<msize_t>& terminal_branches() const + + Branches with no children. + + .. cpp:function:: const std::vector<msegment>& branch_segments(msize_t b) const + + Range of segments in a branch. + +.. cpp:function:: std::string show(const arb::morphology&) + + Return a string representation of the tree underlying the morphology. .. _cppcablecell-morphology-construction: @@ -200,6 +241,28 @@ by two stitches: cable_cell cell(stitched.morphology(), dec, stitched.labels()); +Debug Ouput +----------- + +Tree representations of :cpp:type:`segment_tree` and :cpp:type:`morphology` can +be obtained by including ``arborio/debug.hpp`` which contains a series of +:cpp:func:`show` functions that return ASCII renderings of the given object. + +Example for an arbitrary segment tree + +.. code:: + + [-- id=0 --]-+-[-- id=1 --] + +-[-- id=2 --]-+-[-- id=3 --] + +-[-- id=4 --] + +and for the equivalent morphology + +.. code:: + + <-- id=0 len=1 -->-+-<-- id=1 len=1 --> + +-<-- id=2 len=1 -->-+-<-- id=3 len=1 --> + +-<-- id=4 len=1 --> .. _locsets-and-regions: diff --git a/doc/python/morphology.rst b/doc/python/morphology.rst index b649a3c5..691c4042 100644 --- a/doc/python/morphology.rst +++ b/doc/python/morphology.rst @@ -299,6 +299,12 @@ Cable cell morphology A list of the segments. + .. method:: show + + Return a string containing an ASCII rendering of the tree. + + :return: string + .. py:class:: morphology A *morphology* describes the geometry of a cell as unbranched cables @@ -352,6 +358,12 @@ Cable cell morphology :param int i: branch index :rtype: list[msegment] + .. method:: show + + Return a string containing an ASCII rendering of the morphology. + + :return: string + .. py:class:: place_pwlin A :class:`place_pwlin` object allows the querying of the 3-d location of locations and cables diff --git a/python/morphology.cpp b/python/morphology.cpp index 1a3c305e..4ab870ab 100644 --- a/python/morphology.cpp +++ b/python/morphology.cpp @@ -18,6 +18,7 @@ #include <arborio/swcio.hpp> #include <arborio/neurolucida.hpp> #include <arborio/neuroml.hpp> +#include <arborio/debug.hpp> #include "util.hpp" #include "error.hpp" @@ -292,6 +293,9 @@ void register_morphology(py::module& m) { .def("tag_roots", [](const arb::segment_tree& t, int tag) { return arb::tag_roots(t, tag); }, "Get roots of tag region of this segment tree.") + .def("show", + [] (const arb::segment_tree& t) { return arborio::show(t); }, + "Return an ASCII representation of this segment tree.") .def("__str__", [](const arb::segment_tree& s) { return util::pprintf("<arbor.segment_tree:\n{}>", s);}); @@ -321,6 +325,9 @@ void register_morphology(py::module& m) { "i"_a, "A list of the segments in branch i, ordered from proximal to distal ends of the branch.") .def("to_segment_tree", &arb::morphology::to_segment_tree, "Convert this morphology to a segment_tree.") + .def("show", + [] (const arb::morphology& t) { return arborio::show(t); }, + "Return an ASCII representation.") .def("__str__", [](const arb::morphology& m) { return util::pprintf("<arbor.morphology:\n{}>", m); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index b75a4555..96f7e6d3 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -149,6 +149,7 @@ set(unit_sources test_vector.cpp test_version.cpp test_v_clamp.cpp + test_debug.cpp # unit test driver test.cpp diff --git a/test/unit/test_asc.cpp b/test/unit/test_asc.cpp index 0c06fbdc..5e0443b7 100644 --- a/test/unit/test_asc.cpp +++ b/test/unit/test_asc.cpp @@ -1,6 +1,3 @@ -#include <iostream> -#include <fstream> - #include <arbor/cable_cell.hpp> #include <arbor/morph/primitives.hpp> #include <arbor/morph/segment_tree.hpp> diff --git a/test/unit/test_debug.cpp b/test/unit/test_debug.cpp new file mode 100644 index 00000000..eb944e99 --- /dev/null +++ b/test/unit/test_debug.cpp @@ -0,0 +1,49 @@ +#include <arbor/morph/morphology.hpp> +#include <arbor/morph/segment_tree.hpp> + +#include <arborio/debug.hpp> + +#include <gtest/gtest.h> + +TEST(debug_io, single) { + arb::segment_tree tree; + arb::msize_t par = arb::mnpos; + tree.append(par, {0, 0, 0, 5}, {0, 0, 10, 5}, 42); + + EXPECT_EQ("[-- id=0 --]\n", arborio::show(tree)); + EXPECT_EQ("<-- id=0 len=1 -->\n", arborio::show(arb::morphology{tree})); +} + +TEST(debug_io, fork) { + arb::segment_tree tree; + arb::msize_t par = arb::mnpos; + par = tree.append(par, {0, 0, 0, 5}, {0, 0, 10, 5}, 42); + tree.append(par, {0, 0, 10, 5}, {0, 1, 10, 5}, 23); + tree.append(par, {0, 0, 10, 5}, {0, -1, 10, 5}, 23); + + EXPECT_EQ("[-- id=0 --]-+-[-- id=1 --]\n" + " +-[-- id=2 --]\n", + arborio::show(tree)); + EXPECT_EQ("<-- id=0 len=1 -->-+-<-- id=1 len=1 -->\n" + " +-<-- id=2 len=1 -->\n", + arborio::show(arb::morphology{tree})); +} + +TEST(debug_io, complex) { + arb::segment_tree tree; + arb::msize_t lvl0 = arb::mnpos; + lvl0 = tree.append(lvl0, {0, 0, 0, 5}, {0, 0, 10, 5}, 42); + tree.append(lvl0, {0, 0, 10, 5}, {0, 1, 10, 5}, 23); + auto lvl1 = tree.append(lvl0, {0, 0, 10, 5}, {0, -1, 10, 5}, 23); + tree.append(lvl1, {0, -1, 10, 5}, { 1, -1, 10, 5}, 23); + tree.append(lvl1, {0, -1, 10, 5}, {-1, -1, 10, 5}, 23); + + EXPECT_EQ("[-- id=0 --]-+-[-- id=1 --]\n" + " +-[-- id=2 --]-+-[-- id=3 --]\n" + " +-[-- id=4 --]\n", + arborio::show(tree)); + EXPECT_EQ("<-- id=0 len=1 -->-+-<-- id=1 len=1 -->\n" + " +-<-- id=2 len=1 -->-+-<-- id=3 len=1 -->\n" + " +-<-- id=4 len=1 -->\n", + arborio::show(arb::morphology{tree})); +} -- GitLab