From 397a66b55c5ae7618e03eec7f9522c7510eb0d51 Mon Sep 17 00:00:00 2001
From: Sebastian Schmitt <sebastian.schmitt@kip.uni-heidelberg.de>
Date: Fri, 3 Dec 2021 13:16:44 +0100
Subject: [PATCH] lmorpho (#1746)

Add the lmorpho utility back to Arbor

The original utility was removed because it was a maintenance burden with no users.
Now users are requesting the feature, this PR adds the old code, and updates it
to work with the new morphology description API.
---
 CMakeLists.txt          |   1 +
 lmorpho/CMakeLists.txt  |   8 +
 lmorpho/README.md       | 115 ++++++++++++
 lmorpho/lmorpho.cpp     | 121 ++++++++++++
 lmorpho/lsys_models.cpp | 252 +++++++++++++++++++++++++
 lmorpho/lsys_models.hpp |  11 ++
 lmorpho/lsystem.cpp     | 403 ++++++++++++++++++++++++++++++++++++++++
 lmorpho/lsystem.hpp     | 134 +++++++++++++
 8 files changed, 1045 insertions(+)
 create mode 100644 lmorpho/CMakeLists.txt
 create mode 100644 lmorpho/README.md
 create mode 100644 lmorpho/lmorpho.cpp
 create mode 100644 lmorpho/lsys_models.cpp
 create mode 100644 lmorpho/lsys_models.hpp
 create mode 100644 lmorpho/lsystem.cpp
 create mode 100644 lmorpho/lsystem.hpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index ccdfe0db..ace085f3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -556,3 +556,4 @@ install(
         cmake/FindUnwind.cmake
     DESTINATION "${cmake_config_dir}")
 
+add_subdirectory(lmorpho)
diff --git a/lmorpho/CMakeLists.txt b/lmorpho/CMakeLists.txt
new file mode 100644
index 00000000..21262d16
--- /dev/null
+++ b/lmorpho/CMakeLists.txt
@@ -0,0 +1,8 @@
+add_executable(lmorpho lmorpho.cpp lsystem.cpp lsys_models.cpp)
+
+target_link_libraries(lmorpho PRIVATE arbor arbor-sup ext-tinyopt arborio)
+
+# TODO: resolve public headers
+target_link_libraries(lmorpho PRIVATE arbor-private-headers)
+
+install(TARGETS lmorpho RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/lmorpho/README.md b/lmorpho/README.md
new file mode 100644
index 00000000..561bbebd
--- /dev/null
+++ b/lmorpho/README.md
@@ -0,0 +1,115 @@
+# `lmporho` — generate random neuron morphologies
+
+`lmorpho` implements an L-system geometry generator for the purpose of
+generating a series of artificial neuron morphologies as described by a set of
+stasticial parameters.
+
+## Method
+
+The basic algorithm used is that of [Burke (1992)][burke1992], with extensions
+for embedding in 3-d taken from [Ascoli (2001)][ascoli2001].
+
+A dendritic tree is represented by a piece-wise linear embedding of a rooted
+tree into R³×R⁺, describing the position in space and non-negative radius.
+Morphologies are represented as a sequence of these trees together with a point
+and radius describing a (spherical) soma.
+
+The generation process for a tree successively grows the terminal points by
+some length ΔL each step, changing its orientation each step according to a
+random distribution. After growing, there is a radius-dependent probability
+that it will bifurcate or terminate. Parameters describing these various
+probability distributions constitute the L-system model.
+
+The distributions used in [Ascoli (2001)][ascoli2001] can be constant (i.e. not
+random), uniform over some interval, truncated normal, or a mixture
+distribution of these. In the present version of the code, mixture
+distributions are not supported.
+
+## Output
+
+Generated morphologies can be output either in
+[SWC format](http://research.mssm.edu/cnic/swc.html), or as 'parent vectors',
+which describe the topology of the associated tree.
+
+Entry _i_ of the (zero-indexed) parent vector gives the index of the proximal
+unbranched section to which it connects. Somata have a parent index of -1.
+
+Morphology information can be written one each to separate files or
+concatenated. If concatenated, the parent vector indices will be shifted as
+required to maintain consistency.
+
+## Models
+
+Two L-system parameter sets are provided as 'built-in'; if neither model is
+selected, a simple, non-biological default model is used.
+
+As mixture distributions are not yet supported in `lmorpho`, these models are
+simplified approximations to those in the literature as described below.
+
+### Motor neuron model
+
+The 'motoneuron' model corresponds to the adult cat spinal alpha-motoneuron
+model described in [Ascoli (2001)][ascoli2001], based in turn on the model of
+[Burke (1992)][burke1992].
+
+The model parameters in Ascoli are not complete; missing parameters were taken
+from the corresponding ArborVitae model parameters in the same paper, and
+somatic diameters were taken from [Cullheim (1987)][cullheim1987].
+
+### Purkinke neuron model
+
+The 'purkinke' model corresponds to the guinea pig Purkinje cell model from
+[Ascoli (2001)][ascoli2001], which was fit to data from [Rapp
+(1994)][rapp1994]. Somatic diameters are a simple fit to the three measurements
+in the original source.
+
+Some required parameters are not recorded in [Ascoli (2001)][ascoli2001];
+the correlation component `diam_child_a` and the `length_step` ΔL are
+taken from the corresponding L-Neuron parameter description in the
+[L-Neuron database](l-neuron-database). These should be verified by
+fitting against the digitized morphologies as found in the database.
+
+Produced neurons from this model do not look especially realistic.
+
+### Other models
+
+There is not yet support for loading in arbitrary parameter sets, or for
+translating or approximating the parameters that can be found in the
+[L-Neuron database][l-neuron-database]. Support for parameter estimation
+from a population of discretized morphologies would be useful.
+
+## References
+
+1. Ascoli, G. A., Krichmar, J. L., Scorcioni, R., Nasuto, S. J., Senft, S. L.
+   and Krichmar, G. L. (2001). Computer generation and quantitative morphometric
+   analysis of virtual neurons. _Anatomy and Embryology_ _204_(4), 283–301.
+   [DOI: 10.1007/s004290100201][ascoli2001]
+
+2. Burke, R. E., Marks, W. B. and Ulfhake, B. (1992). A parsimonious description
+   of motoneuron dendritic morphology using computer simulation.
+   _The Journal of Neuroscience_ _12_(6), 2403–2416. [online][burke1992]
+
+3. Cullheim, S., Fleshman, J. W., Glenn, L. L., and Burke, R. E. (1987).
+   Membrane area and dendritic structure in type-identified triceps surae alpha
+   motoneurons. _The Journal of Comparitive Neurology_ _255_(1), 68–81.
+   [DOI: 10.1002/cne.902550106][cullheim1987]
+
+4. Rapp, M., Segev, I. and Yaom, Y. (1994). Physiology, morphology and detailed
+   passive models of guinea-pig cerebellar Purkinje cells.
+   _The Journal of Physiology_, _474_, 101–118.
+   [DOI: 10.1113/jphysiol.1994.sp020006][rapp1994].
+
+
+[ascoli2001]: http://dx.doi.org/10.1007/s004290100201
+    "Ascoli et al. (2001). Computer generation […] of virtual neurons."
+
+[burke1992]: http://www.jneurosci.org/content/12/6/2403
+    "Burke et al. (1992). A parsimonious description of motoneuron dendritic morphology […]"
+
+[cullheim1987]: http://dx.doi.org/10.1002/cne.902550106
+    "Cullheim et al. (1987). Membrane area and dendritic structure in […] alpha motoneurons."
+
+[rapp1994]: http://dx.doi.org/10.1113/jphysiol.1994.sp020006
+    "Rapp et al. (1994). Physiology, morphology […] of guinea-pig cerebellar Purkinje cells."
+
+[l-neuron-database]: http://krasnow1.gmu.edu/cn3/L-Neuron/database/index.html
diff --git a/lmorpho/lmorpho.cpp b/lmorpho/lmorpho.cpp
new file mode 100644
index 00000000..d6ed240f
--- /dev/null
+++ b/lmorpho/lmorpho.cpp
@@ -0,0 +1,121 @@
+#include <algorithm>
+#include <iostream>
+#include <fstream>
+#include <map>
+#include <sstream>
+#include <vector>
+
+#include <tinyopt/tinyopt.h>
+
+#include "lsystem.hpp"
+#include "lsys_models.hpp"
+
+#include <arbor/cable_cell.hpp>
+#include <arborio/cableio.hpp>
+#include <arborio/label_parse.hpp>
+
+const char* usage_str =
+"[OPTION]...\n"
+"\n"
+"  -n, --count=N      Number of morphologies to generate.\n"
+"  -m, --model=MODEL  Use L-system MODEL for generation (see below).\n"
+"  --acc=FILE         Output morphologies as ACC to FILE.\n"
+"  -h, --help         Emit this message and exit.\n"
+"\n"
+"Generate artificial neuron morphologies based on L-system descriptions.\n"
+"\n"
+"If a FILE argument contains a '%', then one file will be written for\n"
+"each generated morphology, with the '%' replaced by the index of the\n"
+"morphology, starting from zero.\n"
+"A FILE argument of '-' corresponds to standard output.\n"
+"\n"
+"Currently supported MODELs:\n"
+"    motoneuron    Adult cat spinal alpha-motoneurons, based on models\n"
+"                  and data in Burke 1992 and Ascoli 2001.\n"
+"    purkinje      Guinea pig Purkinje cells, basd on models and data\n"
+"                  from Rapp 1994 and Ascoli 2001.\n";
+
+int main(int argc, char** argv) {
+    // options
+    int n_morph = 1;
+    std::optional<unsigned> rng_seed = 0;
+    lsys_param P;
+    bool help = false;
+    std::optional<std::string> acc_file;
+
+    std::pair<const char*, const lsys_param*> models[] = {
+        {"motoneuron", &alpha_motoneuron_lsys},
+        {"purkinje", &purkinje_lsys}
+    };
+
+
+    try {
+        for (auto arg = argv+1; *arg; ) {
+            bool ok = false;
+            ok |= (n_morph << to::parse<int>(arg, "--n", "--count")).has_value();
+            ok |= (rng_seed << to::parse<int>(arg, "--s", "--seed")).has_value();
+            ok |= (acc_file << to::parse<std::string>(arg, "-f", "--acc")).has_value();
+            const lsys_param* o;
+            if((o << to::parse<const lsys_param*>(arg, to::keywords(models), "-m", "--model")).has_value()) {
+                P = *o;
+            }
+            ok |= (help << to::parse(arg, "-h", "--help")).has_value();
+            if (!ok) throw to::option_error("unrecognized argument", *arg);
+        }
+
+        if (help) {
+            to::usage(argv[0], usage_str);
+            return 0;
+        }
+
+        std::minstd_rand g;
+        if (rng_seed) {
+            g.seed(rng_seed.value());
+        }
+
+        using namespace arborio::literals;
+
+        auto apical = apical_lsys;
+        auto basal = basal_lsys;
+
+        for (int i = 0; i < n_morph; ++i) {
+            const arb::segment_tree morph = generate_morphology(P.diam_soma, std::vector<lsys_param>{apical, basal}, g);
+
+            arb::label_dict labels;
+            labels.set("soma", "(tag 1)"_reg);
+            labels.set("basal", "(tag 3)"_reg);
+            labels.set("apical", "(tag 4)"_reg);
+            arb::decor decor;
+
+            arb::cable_cell cell(morph, labels, decor);
+
+            if(acc_file) {
+                std::string filename = acc_file.value();
+                const std::string toReplace("%");
+                auto pos = filename.find(toReplace);
+                if(pos != std::string::npos) {
+                    filename.replace(pos, toReplace.length(), std::to_string(i));
+                }
+
+                if(filename == "-") {
+                    arborio::write_component(std::cout, cell);
+                } else {
+                    std::ofstream of(filename);
+                    arborio::write_component(of, cell);
+                }
+
+            }
+        }
+
+    }  catch (to::option_error& e) {
+        std::cerr << argv[0] << ": " << e.what() << "\n";
+        std::cerr << "Try '" << argv[0] << " --help' for more information.\n";
+        std::exit(2);
+    }
+    catch (std::exception& e) {
+        std::cerr << "caught exception: " << e.what() << "\n";
+        std::exit(1);
+    }
+
+}
+
diff --git a/lmorpho/lsys_models.cpp b/lmorpho/lsys_models.cpp
new file mode 100644
index 00000000..6b560b90
--- /dev/null
+++ b/lmorpho/lsys_models.cpp
@@ -0,0 +1,252 @@
+#include <cmath>
+
+#include "lsystem.hpp"
+#include "lsys_models.hpp"
+
+static constexpr double inf = INFINITY;
+
+// Predefined parameters for two classes of neurons. Numbers taken primarily
+// from Ascoli et al. 2001, but some details (soma diameters for example)
+// taken from earlier literature.
+//
+// Without mixture distribution support, these aren't entirely faithful to
+// the original sources.
+//
+// Refer to `README.md` for references and more information.
+// https://krasnow1.gmu.edu/cn3/L-Neuron/database/moto/bu/motoburk.prm
+lsys_param make_alpha_motoneuron_lsys() {
+    lsys_param L;
+
+    // Soma diameter [µm].
+    L.diam_soma = { 47.0, 65.5, 57.6, 5.0 }; // somadiam
+
+    // Number of dendritic trees (rounded to nearest int).
+    L.n_tree = { 8.0, 16.0 }; // numtree
+
+    // Initial dendrite diameter [µm]. (Dstem)
+    L.diam_initial = { 3.0, 12.0 }; // initdiam
+
+    // Dendrite step length [µm]. (ΔL)
+    L.length_step = { 25 };
+
+    // Initial roll (intrinsic rotation about x-axis) [degrees].
+    L.roll_initial = { -180.0, 180.0 };
+
+    // Initial pitch (intrinsic rotation about y-axis) [degrees].
+    L.pitch_initial = { 0.0, 180.0 }; // biforient
+
+    // Tortuousness: roll within section over ΔL [degrees].
+    L.roll_section = { -180.0, 180.0 };
+
+    // Tortuousness: pitch within section over ΔL [degrees].
+    L.pitch_section = { -15.0, 15.0, 0.0, 5.0 };
+
+    // Taper rate: diameter decrease per unit length.
+    L.taper = { -1.25e-3 };
+
+    // Branching torsion: roll at branch point [degrees].
+    L.roll_at_branch = { 0.0, 180.0 };
+
+    // Branch angle between siblings [degrees].
+    L.branch_angle = { 1.0, 172.0, 45.0, 20.0 }; // bifamplitude
+
+    // Correlated child branch radius distribution parameters.
+    L.diam_child_a = -0.2087;
+    L.diam_child_r = { 0.2, 1.8, 0.8255, 0.2125 };
+
+    // Termination probability parameters [1/µm].
+    L.pterm_k1 = 2.62e-2;
+    L.pterm_k2 = -2.955;
+
+    // Bifurcation probability parameters [1/μm].
+    L.pbranch_ov_k1 = 2.58e-5;
+    L.pbranch_ov_k2 = 2.219;
+    L.pbranch_nov_k1 = 2.34e-3;
+    L.pbranch_nov_k2 = 0.194;
+
+    // Absolute maximum dendritic extent [µm].
+    L.max_extent = 2000;
+
+    return L;
+}
+
+lsys_param alpha_motoneuron_lsys = make_alpha_motoneuron_lsys();
+
+// Some parameters missing from the literature are taken from
+// the `purkburk.prm` L-Neuron parameter file:
+// http://krasnow1.gmu.edu/cn3/L-Neuron/database/purk/bu/purkburk.prm
+// Note 'Pov' and 'Pterm' numbers are swapped in Ascoli fig 3B.
+
+lsys_param make_purkinje_lsys() {
+    lsys_param L;
+
+    // Soma diameter [µm].
+    // (Gaussian fit to 3 samples rom Rapp 1994); truncate to 2σ.
+    L.diam_soma = { 22.12,  27.68, 24.9, 1.39 };
+
+    // Number of dendritic trees (rounded to nearest int).
+    L.n_tree = { 1 };
+
+    // Initial dendrite diameter [µm]. (Dstem)
+    L.diam_initial = { 4.8, 7.6, 6.167, 1.069 };
+
+    // Dendrite step length [µm]. (ΔL)
+    L.length_step = { 2.3 }; // from `purkburk.prm`
+
+    // Tortuousness: roll within section over ΔL [degrees].
+    L.roll_section = { 0 };
+
+    // Tortuousness: pitch within section over ΔL [degrees].
+    L.pitch_section = { -5, 5 }; // from `purkburk.prm`
+
+    // Taper rate: diameter decrease per unit length.
+    L.taper = { -inf, inf, -0.010, 0.022 };
+
+    // Branching torsion: roll at branch point [degrees].
+    L.roll_at_branch = { -inf, inf, 0.0, 6.5 }; // from `purkburj.prm`
+
+    // Branch angle between siblings [degrees].
+    L.branch_angle = { 1.0, 179.0, 71.0, 40.0 };
+
+    // Correlated child branch radius distribution parameters.
+    L.diam_child_a = 5.47078; // from `purkburk.prm`
+    L.diam_child_r = { 0, inf, 0.112, 0.035 };
+
+    // Termination probability parameters [1/µm].
+    L.pterm_k1 = 0.1973; // from `purkburj.prm`; 0.4539 in Ascoli.
+    L.pterm_k2 = -1.1533;
+
+    // Bifurcation probability parameters [1/μm].
+    L.pbranch_ov_k1 = 0.1021; // from `purkburk.prm`; 0.0355 in Ascoli.
+    L.pbranch_ov_k2 = 0.7237;
+    L.pbranch_nov_k1 = 0.1021; // from `purkburk.prm`; 0.2349 in Ascoli.
+    L.pbranch_nov_k2 = -0.0116;
+
+    // Absolute maximum dendritic extent [µm].
+    L.max_extent = 20000;
+
+    return L;
+}
+
+lsys_param purkinje_lsys = make_purkinje_lsys();
+
+
+lsys_param make_apical_lsys() {
+    lsys_param L;
+
+    // Soma diameter [µm].
+    L.diam_soma = { 47.0, 65.5, 57.6, 5.0 }; // somadiam
+
+    // Number of dendritic trees (rounded to nearest int).
+    L.n_tree = { 1 }; // numtree
+
+    // Initial dendrite diameter [µm]. (Dstem)
+    L.diam_initial = { 10, 20 }; // initdiam
+
+    // Dendrite step length [µm]. (ΔL)
+    L.length_step = { 25 };
+
+    // Initial roll (intrinsic rotation about x-axis) [degrees].
+    L.roll_initial = { 0 };
+
+    // Initial pitch (intrinsic rotation about y-axis) [degrees].
+    L.pitch_initial = { 0 }; // biforient
+
+    // Tortuousness: roll within section over ΔL [degrees].
+    L.roll_section = { -180.0, 180.0 };
+
+    // Tortuousness: pitch within section over ΔL [degrees].
+    L.pitch_section = { -15.0, 15.0, 0.0, 5.0 };
+
+    // Taper rate: diameter decrease per unit length.
+    L.taper = { -1.25e-3 };
+
+    // Branching torsion: roll at branch point [degrees].
+    L.roll_at_branch = { 0.0, 180.0 };
+
+    // Branch angle between siblings [degrees].
+    L.branch_angle = { 1.0, 172.0, 45.0, 20.0 }; // bifamplitude
+
+    // Correlated child branch radius distribution parameters.
+    L.diam_child_a = -0.2087;
+    L.diam_child_r = { 0.2, 1.8, 0.8255, 0.2125 };
+
+    // Termination probability parameters [1/µm].
+    L.pterm_k1 = 2.62e-2;
+    L.pterm_k2 = -2.955;
+
+    // Bifurcation probability parameters [1/μm].
+    L.pbranch_ov_k1 = 0.58e-5;
+    L.pbranch_ov_k2 = 2.219;
+    L.pbranch_nov_k1 = 2.34e-3;
+    L.pbranch_nov_k2 = 0.194;
+
+    // Absolute maximum dendritic extent [µm].
+    L.max_extent = 2000;
+
+    L.tag = 4;
+
+    return L;
+}
+
+lsys_param apical_lsys = make_apical_lsys();
+
+lsys_param make_basal_lsys() {
+    lsys_param L;
+
+    // Soma diameter [µm].
+    L.diam_soma = { 47.0, 65.5, 57.6, 5.0 }; // somadiam
+
+    // Number of dendritic trees (rounded to nearest int).
+    L.n_tree = { 1, 20, 6, 2}; // numtree
+
+    // Initial dendrite diameter [µm]. (Dstem)
+    L.diam_initial = { 3.0, 12.0 }; // initdiam
+
+    // Dendrite step length [µm]. (ΔL)
+    L.length_step = { 25 };
+
+    // Initial roll (intrinsic rotation about x-axis) [degrees].
+    L.roll_initial = { -180, 180 };
+
+    // Initial pitch (intrinsic rotation about y-axis) [degrees].
+    L.pitch_initial = { 140, 220 }; // biforient
+
+    // Tortuousness: roll within section over ΔL [degrees].
+    L.roll_section = { -180.0, 180.0 };
+
+    // Tortuousness: pitch within section over ΔL [degrees].
+    L.pitch_section = { -15.0, 15.0, 0.0, 5.0 };
+
+    // Taper rate: diameter decrease per unit length.
+    L.taper = { -1.25e-3 };
+
+    // Branching torsion: roll at branch point [degrees].
+    L.roll_at_branch = { 0.0, 180.0 };
+
+    // Branch angle between siblings [degrees].
+    L.branch_angle = { 1.0, 172.0, 45.0, 20.0 }; // bifamplitude
+
+    // Correlated child branch radius distribution parameters.
+    L.diam_child_a = -0.2087;
+    L.diam_child_r = { 0.2, 1.8, 0.8255, 0.2125 };
+
+    // Termination probability parameters [1/µm].
+    L.pterm_k1 = 2.62e-2;
+    L.pterm_k2 = -2.955;
+
+    // Bifurcation probability parameters [1/μm].
+    L.pbranch_ov_k1 = 1.58e-5;
+    L.pbranch_ov_k2 = 2.219;
+    L.pbranch_nov_k1 = 2.34e-3;
+    L.pbranch_nov_k2 = 0.194;
+
+    // Absolute maximum dendritic extent [µm].
+    L.max_extent = 1000;
+
+    L.tag = 3;
+
+    return L;
+}
+
+lsys_param basal_lsys = make_basal_lsys();
diff --git a/lmorpho/lsys_models.hpp b/lmorpho/lsys_models.hpp
new file mode 100644
index 00000000..35a02b8f
--- /dev/null
+++ b/lmorpho/lsys_models.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "lsystem.hpp"
+
+// Predefined parameters for two classes of neurons.
+// See lsys_models.cc for details.
+
+extern lsys_param alpha_motoneuron_lsys;
+extern lsys_param apical_lsys;
+extern lsys_param basal_lsys;
+extern lsys_param purkinje_lsys;
diff --git a/lmorpho/lsystem.cpp b/lmorpho/lsystem.cpp
new file mode 100644
index 00000000..fc6ae025
--- /dev/null
+++ b/lmorpho/lsystem.cpp
@@ -0,0 +1,403 @@
+#include <cmath>
+#include <iostream>
+#include <random>
+#include <stack>
+#include <vector>
+#include <map>
+
+#include <arbor/math.hpp>
+
+#include "lsystem.hpp"
+
+using namespace arb::math;
+
+// L-system implementation.
+
+// Random distribution used in the specification of L-system parameters.
+// It can be a constant, uniform over an interval, or truncated normal.
+// Note that mixture distributions of the above should be, but isn't yet,
+// implemented.
+
+struct lsys_distribution {
+    using result_type = double;
+    using param_type = lsys_distribution_param;
+
+    lsys_distribution(const param_type& p): p_(p) { init(); }
+
+    lsys_distribution(): p_() { init(); }
+    lsys_distribution(result_type x): p_(x) { init(); }
+    lsys_distribution(result_type lb, result_type ub): p_(lb, ub) { init(); }
+    lsys_distribution(result_type lb, result_type ub, result_type mu, result_type sigma):
+        p_(lb, ub, mu, sigma)
+    {
+        init();
+    }
+
+    param_type param() const { return p_; }
+    void param(const param_type& pbis) { p_=pbis; }
+
+    result_type min() const { return p_.lb; }
+    result_type max() const {
+        return p_.kind==param_type::constant? min(): p_.ub;
+    }
+
+    bool operator==(const lsys_distribution& y) const {
+        return p_==y.p_;
+    }
+
+    bool operator!=(const lsys_distribution& y) const {
+        return p_!=y.p_;
+    }
+
+    // omitting out ostream, istream methods
+    // (...)
+
+    template <typename Gen>
+    result_type operator()(Gen& g, const param_type &p) const {
+        lsys_distribution dist(p);
+        return dist(g);
+    }
+
+    template <typename Gen>
+    result_type operator()(Gen& g) const {
+        switch (p_.kind) {
+        case param_type::constant:
+            return p_.lb;
+        case param_type::uniform:
+            return uniform_dist(g);
+        case param_type::normal:
+            // TODO: replace with a robust truncated normal generator!
+            for (;;) {
+                result_type r = normal_dist(g);
+                if (r>=p_.lb && r<=p_.ub) return r;
+            }
+        }
+        return 0;
+    }
+
+private:
+    param_type p_;
+    mutable std::uniform_real_distribution<double> uniform_dist;
+    mutable std::normal_distribution<double> normal_dist;
+
+    void init() {
+        switch (p_.kind) {
+        case param_type::uniform:
+            uniform_dist = std::uniform_real_distribution<double>(p_.lb, p_.ub);
+            break;
+        case param_type::normal:
+            normal_dist = std::normal_distribution<result_type>(p_.mu, p_.sigma);
+            break;
+        default: ;
+        }
+    }
+};
+
+class burke_correlated_r {
+    double a_;
+    lsys_distribution r_;
+
+public:
+    struct result_type { double rho1, rho2; };
+
+    burke_correlated_r(double a, lsys_distribution_param r): a_(a), r_(r) {}
+
+    template <typename Gen>
+    result_type operator()(Gen& g) const {
+        double r1 = r_(g);
+        double r2 = r_(g);
+        return { r1+a_*r2, r2+a_*r1 };
+    }
+};
+
+class burke_exp_test {
+    double k1_;
+    double k2_;
+
+public:
+    using result_type = int;
+
+    burke_exp_test(double k1, double k2): k1_(k1), k2_(k2) {}
+
+    template <typename Gen>
+    result_type operator()(double delta_l, double radius, Gen& g) const {
+        std::bernoulli_distribution B(delta_l*k1_*std::exp(k2_*radius*2.0));
+        return B(g);
+    }
+};
+
+class burke_exp2_test {
+    double ak1_;
+    double ak2_;
+    double bk1_;
+    double bk2_;
+
+public:
+    using result_type = int;
+
+    burke_exp2_test(double ak1, double ak2, double bk1, double bk2):
+        ak1_(ak1), ak2_(ak2), bk1_(bk1), bk2_(bk2) {}
+
+    template <typename Gen>
+    result_type operator()(double delta_l, double radius, Gen& g) const {
+        double diam = 2.*radius;
+        std::bernoulli_distribution B(
+            delta_l*std::min(ak1_*std::exp(ak2_*diam), bk1_*std::exp(bk2_*diam)));
+
+        return B(g);
+    }
+};
+
+// L-system parameter set with instantiated distributions.
+struct lsys_sampler {
+    // Create distributions once from supplied parameter set.
+    explicit lsys_sampler(const lsys_param& P):
+        diam_soma(P.diam_soma),
+        n_tree(P.n_tree),
+        diam_initial(P.diam_initial),
+        length_step(P.length_step),
+        roll_initial(P.roll_initial),
+        pitch_initial(P.pitch_initial),
+        roll_section(P.roll_section),
+        pitch_section(P.pitch_section),
+        taper(P.taper),
+        roll_at_branch(P.roll_at_branch),
+        branch_angle(P.branch_angle),
+        diam_child(P.diam_child_a, P.diam_child_r),
+        term_test(P.pterm_k1, P.pterm_k2),
+        branch_test(P.pbranch_ov_k1, P.pbranch_ov_k2,
+            P.pbranch_nov_k1, P.pbranch_nov_k2),
+        max_extent(P.max_extent)
+    {}
+
+    lsys_distribution diam_soma;
+    lsys_distribution n_tree;
+    lsys_distribution diam_initial;
+    lsys_distribution length_step;
+    lsys_distribution roll_initial;
+    lsys_distribution pitch_initial;
+    lsys_distribution roll_section;
+    lsys_distribution pitch_section;
+    lsys_distribution taper;
+    lsys_distribution roll_at_branch;
+    lsys_distribution branch_angle;
+    burke_correlated_r diam_child;
+    burke_exp_test term_test;
+    burke_exp2_test branch_test;
+    double max_extent;
+};
+
+struct section_point {
+    double x;
+    double y;
+    double z;
+    double r;
+    friend std::ostream& operator<<(std::ostream& os, const section_point& obj);
+};
+
+std::ostream& operator<<(std::ostream& os, const section_point& obj) {
+    os << obj.x << " " << obj.y << " " << obj.z << " " << obj.r << '\n';
+    return os;
+}
+
+struct section_tip {
+    section_point point = {0., 0., 0., 0.};
+    quaternion rotation = {1., 0., 0., 0.};
+    double somatic_distance = 0.;
+};
+
+
+struct section_geometry {
+    unsigned id = 0; // ids should be contigously numbered from 1 in the morphology.
+    unsigned parent_id = 0;
+    bool terminal = false;
+    std::vector<section_point> points;
+    double length = 0; // µm
+
+    section_geometry() = default;
+    section_geometry(unsigned id, unsigned parent_id, bool terminal, std::vector<section_point> points, double length) :
+        id(id), parent_id(parent_id), terminal(terminal), points(std::move(points)), length(length)
+    {}
+
+    friend std::ostream& operator<<(std::ostream& os, const section_geometry& obj);
+};
+
+std::ostream& operator<<(std::ostream& os, const section_geometry& obj) {
+    os << "id: " << obj.id << '\n';
+    os << "parent_id: " << obj.parent_id << '\n';
+    os << "terminal: " << obj.terminal << '\n';
+    os << "length: " << obj.length << '\n';
+    os << "points: " << '\n';
+    for (auto& p : obj.points) {
+        os << p << '\n';
+    }
+    return os;
+}
+
+struct grow_result {
+    std::vector<section_point> points;
+    std::vector<section_tip> children;
+    double length = 0.;
+};
+
+constexpr double deg_to_rad = 3.1415926535897932384626433832795/180.0;
+
+template <typename Gen>
+grow_result grow(section_tip tip, const lsys_sampler& S, Gen &g) {
+    constexpr quaternion xaxis = {0, 1, 0, 0};
+    static std::uniform_real_distribution<double> U;
+
+    grow_result result;
+    std::vector<section_point>& points = result.points;
+
+    points.push_back(tip.point);
+    for (;;) {
+        quaternion step = xaxis^tip.rotation;
+        double dl = S.length_step(g);
+        tip.point.x += dl*step.x;
+        tip.point.y += dl*step.y;
+        tip.point.z += dl*step.z;
+        tip.point.r += dl*0.5*S.taper(g);
+        tip.somatic_distance += dl;
+        result.length += dl;
+
+        if (tip.point.r<0) tip.point.r = 0;
+
+        double phi = S.roll_section(g);
+        double theta = S.pitch_section(g);
+        tip.rotation *= rotation_x(deg_to_rad*phi);
+        tip.rotation *= rotation_y(deg_to_rad*theta);
+
+        points.push_back(tip.point);
+
+        if (tip.point.r==0 || tip.somatic_distance>=S.max_extent) {
+            return result;
+        }
+
+        if (S.branch_test(dl, tip.point.r, g)) {
+            auto r = S.diam_child(g);
+            // ignore branch if we get non-positive radii
+            if (r.rho1>0 && r.rho2>0) {
+                double branch_phi = S.roll_at_branch(g);
+                double branch_angle = S.branch_angle(g);
+                double branch_theta1 = U(g)*branch_angle;
+                double branch_theta2 = branch_theta1-branch_angle;
+
+                tip.rotation *= rotation_x(deg_to_rad*branch_phi);
+
+                section_tip t1 = tip;
+                t1.point.r = t1.point.r * r.rho1;
+                t1.rotation *= rotation_y(deg_to_rad*branch_theta1);
+
+                section_tip t2 = tip;
+                t2.point.r = t2.point.r * r.rho2;
+                t2.rotation *= rotation_y(deg_to_rad*branch_theta2);
+
+                result.children = {t1, t2};
+                return result;
+            }
+        }
+
+        if (S.term_test(dl, tip.point.r, g)) {
+            return result;
+        }
+    }
+}
+
+std::vector<section_geometry> generate_sections(double soma_radius, lsys_param P, lsys_generator &g) {
+    constexpr quaternion xaxis = {0, 1, 0, 0};
+
+    lsys_sampler S(P);
+
+    struct section_start {
+        section_tip tip;
+        unsigned parent_id;
+    };
+    std::stack<section_start> starts;
+    unsigned next_id = 1u;
+
+    int n = (int)std::round(S.n_tree(g));
+    for (int i=0; i<n; ++i) {
+        double phi = S.roll_initial(g);
+        double theta = S.pitch_initial(g);
+        double radius = 0.5*S.diam_initial(g);
+
+        section_tip tip;
+        tip.rotation = rotation_x(deg_to_rad*phi)*rotation_y(deg_to_rad*theta);
+        tip.somatic_distance = 0.0;
+
+        auto p = (soma_radius*xaxis)^tip.rotation;
+        tip.point = {p.x, p.y, p.z, radius};
+
+        starts.push({tip, 0u});
+    }
+
+    std::vector<section_geometry> sections;
+
+    while (!starts.empty() && sections.size() < P.max_sections) {
+        auto start = starts.top();
+        starts.pop();
+
+        auto branch = grow(start.tip, S, g);
+        section_geometry section{next_id++, start.parent_id, branch.children.empty(), std::move(branch.points), branch.length};
+
+        for (auto child: branch.children) {
+            starts.push({child, section.id});
+        }
+
+        sections.push_back(std::move(section));
+    }
+
+    return sections;
+}
+
+arb::segment_tree generate_morphology(const lsys_distribution_param& soma, std::vector<lsys_param> Ps, lsys_generator &g) {
+
+    double const soma_diameter = lsys_distribution(soma)(g);
+    double const soma_radius = 0.5*soma_diameter;
+
+    arb::segment_tree morph;
+    morph.append(
+                arb::mnpos, arb::mpoint{-soma_diameter, 0, 0, soma_diameter},
+                arb::mpoint{soma_diameter, 0, 0, soma_diameter}, 1);
+
+    for(auto P : Ps) {
+        auto sections = generate_sections(soma_radius, P, g);
+
+        // map from internal representation to Arbor ids
+        std::map<size_t, arb::msize_t> parent_end_id;
+
+        // soma
+        parent_end_id[0] = 0;
+
+        // dendrites:
+        for (auto& sec: sections) {
+
+            // the first section must have the soma as parent
+            size_t parent = parent_end_id.at(sec.parent_id);
+
+            const auto& points = sec.points;
+            if (points.size() < 2) {
+                throw std::runtime_error("segment has only 1 point");
+            }
+
+            // Include first point only for dendrites segments attached to soma.
+            if (sec.parent_id==0) {
+                parent = morph.append(parent,
+                                      arb::mpoint{points[0].x, points[0].y, points[0].z, points[0].r},
+                                      arb::mpoint{points[1].x, points[1].y, points[1].z, points[1].r}, P.tag);
+            }
+
+            // Remaining points.
+            for (unsigned i = 1; i < points.size(); ++i) {
+                const auto& p = points[i];
+                parent = morph.append(parent, arb::mpoint{p.x, p.y, p.z, p.r}, P.tag);
+            }
+
+            parent_end_id[sec.id] = parent;
+        }
+    }
+
+    return morph;
+}
+
diff --git a/lmorpho/lsystem.hpp b/lmorpho/lsystem.hpp
new file mode 100644
index 00000000..3130ba64
--- /dev/null
+++ b/lmorpho/lsystem.hpp
@@ -0,0 +1,134 @@
+#pragma once
+
+#include <random>
+
+#include <arbor/morph/segment_tree.hpp>
+
+struct lsys_param;
+
+using lsys_generator = std::minstd_rand;
+
+class lsys_distribution_param;
+arb::segment_tree generate_morphology(const lsys_distribution_param& soma, std::vector<lsys_param> Ps, lsys_generator& g);
+
+// The distribution parameters used in the specification of the L-system parameters.
+// The distribution can be a constant, uniform over an interval, or truncated normal.
+// Note that mixture distributions of the above should be, but isn't yet,
+// implemented.
+
+struct lsys_distribution_param {
+    using result_type = double;
+    enum lsys_kind {
+        constant, uniform, normal
+    };
+
+    lsys_kind kind;
+    result_type lb; // lower bound
+    result_type ub; // upper bound (used for only non-constant)
+    result_type mu; // mean (used only for truncated Gaussian)
+    result_type sigma; // s.d. (used only for truncated Gaussian)
+
+    bool operator==(const lsys_distribution_param &p) const {
+        if (kind!=p.kind) return false;
+        switch (kind) {
+        case constant:
+            return lb == p.lb;
+        case uniform:
+            return lb == p.lb && ub == p.ub;
+        case normal:
+            return lb == p.lb && ub == p.ub && mu == p.mu && sigma == p.sigma;
+        }
+        return false;
+    }
+
+    bool operator!=(const lsys_distribution_param &p) const {
+        return !(*this==p);
+    }
+
+    // zero or one parameter => constant
+    lsys_distribution_param():
+        kind(constant), lb(0.0) {}
+
+    lsys_distribution_param(result_type x):
+        kind(constant), lb(x) {}
+
+    // two paramter => uniform
+    lsys_distribution_param(result_type lb, result_type ub):
+        kind(uniform), lb(lb), ub(ub) {}
+
+    // four paramter => truncated normal
+    lsys_distribution_param(result_type lb, result_type ub, result_type mu, result_type sigma):
+        kind(normal), lb(lb), ub(ub), mu(mu), sigma(sigma) {}
+};
+
+
+// Parameters as referenced in Ascoli 2001 are given in parentheses below.
+// Defaults below are chosen to be safe, boring and not biological.
+struct lsys_param {
+    // Soma diameter [µm].
+    lsys_distribution_param diam_soma = { 20 };
+
+    // Number of dendritic trees (rounded to nearest int). (Ntree)
+    lsys_distribution_param n_tree = { 1 };
+
+    // Initial dendrite diameter [µm]. (Dstem)
+    lsys_distribution_param diam_initial = { 2 };
+
+    // Dendrite step length [µm]. (ΔL)
+    lsys_distribution_param length_step = { 25 };
+
+    // Dendrites grow along x-axis after application of (instrinsic) rotations;
+    // roll (about x-axis) is applied first, followed by pitch (about y-axis).
+
+    // Initial roll (intrinsic rotation about x-axis) [degrees]. (Taz)
+    lsys_distribution_param roll_initial = { -45, 45 };
+
+    // Initial pitch (intrinsic rotation about y-axis) [degrees]. (Tel)
+    lsys_distribution_param pitch_initial = { -45, 45 };
+
+    // Tortuousness: roll within section over ΔL [degrees]. (Eaz)
+    lsys_distribution_param roll_section = { -45, 45 };
+
+    // Tortuousness: pitch within section over ΔL [degrees]. (Eel)
+    lsys_distribution_param pitch_section = { -45, 45 };
+
+    // Taper rate: diameter decrease per unit length. (TPRB)
+    lsys_distribution_param taper = { 0 };
+
+    // Branching torsion: roll at branch point [degrees]. (Btor)
+    lsys_distribution_param roll_at_branch = { 0 };
+
+    // Branch angle between siblings [degrees]. (Bamp)
+    lsys_distribution_param branch_angle = { 45 };
+
+    // Child branch diameter ratios are typically anticorrelated.
+    // The Burke 1992 parameterization describes the two child ratios
+    // as d1 = r1 + a*r2 and d2 = r2 + a*r1 where a is a constant
+    // determined by the correlation, and r1 and r2 are drawn from
+    // a common distribution (`d_child_r` below).
+    double diam_child_a = 0;
+    lsys_distribution_param diam_child_r = { 0.5 };
+
+    // Bifurcation and termination probabilities per unit-length below are given by
+    // P = k1 * exp(k2*diameter), described by the paremeters k1 and k2 [1/µm].
+
+    // Termination probability parameters [1/µm]. (k1trm, k2trm)
+    double pterm_k1 = 0.05;
+    double pterm_k2 = -2;
+
+    // Bifurcation probability is given by minimum of two probabilities, `pbranch_ov' and
+    // `pbranch_nov' below.
+    double pbranch_ov_k1 = 0.01;
+    double pbranch_ov_k2 = 0;
+    double pbranch_nov_k1 = 0.01;
+    double pbranch_nov_k2 = 0;
+
+    // Absolute maximum dendritic extent [µm]. (Forces termination of algorithm)
+    double max_extent = 2000;
+
+    // Absolute maximum number of unbranched sections. (Forces termination of algorithm)
+    unsigned max_sections = 10000;
+
+    size_t tag = 0;
+};
+
-- 
GitLab