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