diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index a6a497a9d21c0bb9ab0ea718f4138e43e2115c11..55a23170326052d30ee745672826b8bea291393a 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -33,14 +33,13 @@ set(arbor_sources
     morph/embed_pwlin.cpp
     morph/label_dict.cpp
     morph/locset.cpp
-    morph/mbranch.cpp
     morph/morphexcept.cpp
     morph/morphology.cpp
     morph/mprovider.cpp
     morph/place_pwlin.cpp
     morph/primitives.cpp
     morph/region.cpp
-    morph/sample_tree.cpp
+    morph/segment_tree.cpp
     merge_events.cpp
     simulation.cpp
     partition_load_balance.cpp
diff --git a/arbor/cv_policy.cpp b/arbor/cv_policy.cpp
index fa39de7d481c78f0eac865f6b763498636eb1b34..468d6a7067cee907583d578535b9ca8330053522 100644
--- a/arbor/cv_policy.cpp
+++ b/arbor/cv_policy.cpp
@@ -139,20 +139,13 @@ locset cv_policy_fixed_per_branch::cv_boundary_points(const cable_cell& cell) co
     return unique_sum(locset(std::move(points)), ls::cboundary(domain_));
 }
 
-locset cv_policy_every_sample::cv_boundary_points(const cable_cell& cell) const {
+locset cv_policy_every_segment::cv_boundary_points(const cable_cell& cell) const {
     const unsigned nbranch = cell.morphology().num_branches();
     if (!nbranch) return ls::nil();
 
-    // Always include branch proximal points, so that forks are trivial.
-
-    return
-        unique_sum(ls::cboundary(domain_),
-            ls::restrict(
-                util::foldl(
-                    [](locset l, msize_t sidx) { return sum(std::move(l), ls::sample(sidx)); },
-                    ls::on_branches(0),
-                    util::make_span(cell.morphology().num_samples())),
-                domain_));
+    return unique_sum(
+                ls::cboundary(domain_),
+                ls::restrict(ls::segment_boundaries(), domain_));
 }
 
 } // namespace arb
diff --git a/arbor/include/arbor/cv_policy.hpp b/arbor/include/arbor/cv_policy.hpp
index f59fe418fc6a027daca96dbc3e975a2c4d66c45a..b8869f02e36c1cf22684a5996aa929c800de85e6 100644
--- a/arbor/include/arbor/cv_policy.hpp
+++ b/arbor/include/arbor/cv_policy.hpp
@@ -183,12 +183,12 @@ private:
     cv_policy_flag::value flags_;
 };
 
-struct cv_policy_every_sample: cv_policy_base {
-    explicit cv_policy_every_sample(region domain = reg::all()):
+struct cv_policy_every_segment: cv_policy_base {
+    explicit cv_policy_every_segment(region domain = reg::all()):
          domain_(std::move(domain)) {}
 
     cv_policy_base_ptr clone() const override {
-        return cv_policy_base_ptr(new cv_policy_every_sample(*this));
+        return cv_policy_base_ptr(new cv_policy_every_segment(*this));
     }
 
     locset cv_boundary_points(const cable_cell&) const override;
@@ -196,7 +196,6 @@ struct cv_policy_every_sample: cv_policy_base {
 
 private:
     region domain_;
-    cv_policy_flag::value flags_;
 };
 
 inline cv_policy default_cv_policy() {
diff --git a/arbor/include/arbor/math.hpp b/arbor/include/arbor/math.hpp
index 08a5ff4e73a200c72c0608eea1c2ccaf99ae4481..cd228184f392b1e510e5ac075719c098c7f028c3 100644
--- a/arbor/include/arbor/math.hpp
+++ b/arbor/include/arbor/math.hpp
@@ -45,18 +45,6 @@ T constexpr volume_frustrum(T L, T r1, T r2) {
     return pi<T>/T(3) * (square(r1+r2) - r1*r2) * L;
 }
 
-// Volume of a sphere radius r.
-template <typename T>
-T constexpr volume_sphere(T r) {
-    return T(4)/T(3) * pi<T> * cube(r);
-}
-
-// Surface area of a sphere radius r.
-template <typename T>
-T constexpr area_sphere(T r) {
-    return T(4) * pi<T> * square(r);
-}
-
 // Linear interpolation by u in interval [a,b]: (1-u)*a + u*b.
 template <typename T, typename U>
 T constexpr lerp(T a, T b, U u) {
diff --git a/arbor/include/arbor/morph/embed_pwlin.hpp b/arbor/include/arbor/morph/embed_pwlin.hpp
index 0a352a5708029cfe2d5ee7e40ef81f3caa8d3aa7..ac065005e1a3a7f7390d7284c6c8a23fc1870472 100644
--- a/arbor/include/arbor/morph/embed_pwlin.hpp
+++ b/arbor/include/arbor/morph/embed_pwlin.hpp
@@ -22,8 +22,14 @@ using pw_constant_fn = util::pw_elements<double>;
 struct embed_pwlin {
     explicit embed_pwlin(const arb::morphology& m);
 
-    mlocation sample_location(msize_t sid) const {
-        return sample_locations_.at(sid);
+    // Locations that mark the boundaries between segments.
+    const std::vector<mlocation>& segment_locations() const {
+        return segment_locations_;
+    }
+
+    std::pair<const mlocation*, const mlocation*> branch_segment_locations(msize_t i) const {
+        const mlocation* p = segment_locations_.data();
+        return std::make_pair(p+branch_segment_part_[i], p+branch_segment_part_[i+1]);
     }
 
     // Interpolated radius at location.
@@ -58,9 +64,9 @@ struct embed_pwlin {
         return integrate_length(mcable{bid, 0, 1});
     }
 
-
 private:
-    std::vector<mlocation> sample_locations_;
+    std::vector<mlocation> segment_locations_;
+    std::vector<msize_t> branch_segment_part_;
     std::shared_ptr<embed_pwlin_data> data_;
 };
 
diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp
index d6e3772b5970a83443530d70f2ba4c943eedd326..5d7d35e09512b0b13589f83f111e827ddc22dae1 100644
--- a/arbor/include/arbor/morph/locset.hpp
+++ b/arbor/include/arbor/morph/locset.hpp
@@ -121,9 +121,6 @@ namespace ls {
 // Explicit location on morphology.
 locset location(msize_t branch, double pos);
 
-// Location of a sample.
-locset sample(msize_t);
-
 // Set of terminal nodes on a morphology.
 locset terminal();
 
@@ -152,6 +149,9 @@ locset cboundary(region reg);
 // Returns all locations in a locset that are also in the region.
 locset restrict(locset ls, region reg);
 
+// Returns locations that mark the segments.
+locset segment_boundaries();
+
 // A range `left` to `right` of randomly selected locations with a
 // uniform distribution from region `reg` generated using `seed`
 locset uniform(region reg, unsigned left, unsigned right, uint64_t seed);
diff --git a/arbor/include/arbor/morph/morphexcept.hpp b/arbor/include/arbor/morph/morphexcept.hpp
index 9620f6dc02fb1a71973ec4173037a7a5cc0a32d4..bd7872c8aa6ad5febd66e00ddedc471c6d0a5934 100644
--- a/arbor/include/arbor/morph/morphexcept.hpp
+++ b/arbor/include/arbor/morph/morphexcept.hpp
@@ -21,6 +21,11 @@ struct no_such_branch: morphology_error {
     msize_t bid;
 };
 
+struct no_such_segment: arbor_exception {
+    explicit no_such_segment(msize_t sid);
+    msize_t sid;
+};
+
 struct invalid_mcable: morphology_error {
     invalid_mcable(mcable cable);
     mcable cable;
@@ -30,8 +35,8 @@ struct invalid_mcable_list: morphology_error {
     invalid_mcable_list();
 };
 
-struct invalid_sample_parent: morphology_error {
-    invalid_sample_parent(msize_t parent, msize_t tree_size);
+struct invalid_segment_parent: morphology_error {
+    invalid_segment_parent(msize_t parent, msize_t tree_size);
     msize_t parent;
     msize_t tree_size;
 };
diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp
index 208c9926f6941ddd7d5cc3e1355921b2132b3d34..a4a6b622a48577d2138f9a02d65766a10603645d 100644
--- a/arbor/include/arbor/morph/morphology.hpp
+++ b/arbor/include/arbor/morph/morphology.hpp
@@ -6,35 +6,26 @@
 
 #include <arbor/util/lexcmp_def.hpp>
 #include <arbor/morph/primitives.hpp>
-#include <arbor/morph/sample_tree.hpp>
+#include <arbor/morph/segment_tree.hpp>
 
 namespace arb {
 
 struct morphology_impl;
 
-using mindex_range = std::pair<const msize_t*, const msize_t*>;
-
 class morphology {
     // Hold an immutable copy of the morphology implementation.
     std::shared_ptr<const morphology_impl> impl_;
 
 public:
-    morphology(sample_tree m, bool use_spherical_root);
-    morphology(sample_tree m);
+    morphology(segment_tree m);
     morphology();
 
     // Empty/default-constructed morphology?
     bool empty() const;
 
-    // Whether the root of the morphology is spherical.
-    bool spherical_root() const;
-
     // The number of branches in the morphology.
     msize_t num_branches() const;
 
-    // The number of samples in the morphology.
-    msize_t num_samples() const;
-
     // The parent branch of branch b.
     // Return mnpos if branch has no parent.
     msize_t branch_parent(msize_t b) const;
@@ -46,17 +37,8 @@ public:
     // Branches with no children.
     const std::vector<msize_t>& terminal_branches() const;
 
-    // Range of indexes into the sample points in branch b.
-    mindex_range branch_indexes(msize_t b) const;
-
-    // All of the samples in the morphology.
-    const std::vector<msample>& samples() const;
-
-    // The parent sample of sample i.
-    const std::vector<msize_t>& sample_parents() const;
-
-    // Point properties of samples in the morphology.
-    const std::vector<point_prop>& sample_props() const;
+    // Range of segments in a branch.
+    const std::vector<msegment>& branch_segments(msize_t b) const;
 
     friend std::ostream& operator<<(std::ostream&, const morphology&);
 };
diff --git a/arbor/include/arbor/morph/primitives.hpp b/arbor/include/arbor/morph/primitives.hpp
index f325cc20e6baba6641bbd3f8415f79012ada1fbb..cf75c0d4c5078397f1b39bfeff066f5bd181ea18 100644
--- a/arbor/include/arbor/morph/primitives.hpp
+++ b/arbor/include/arbor/morph/primitives.hpp
@@ -22,6 +22,7 @@ struct mpoint {
     double x, y, z;  // [µm]
     double radius;   // [μm]
 
+    friend bool operator==(const mpoint& l, const mpoint& r);
     friend std::ostream& operator<<(std::ostream&, const mpoint&);
     friend bool operator==(const mpoint& a, const mpoint& b) {
         return a.x==b.x && a.y==b.y && a.z==b.z && a.radius==b.radius;
@@ -43,25 +44,16 @@ enum class comp_op {
     ge
 };
 
-// A morphology sample consists of a location and an integer tag.
-// When loaded from an SWC file, the tag will correspond to the SWC label,
-// which are standardised as follows:
-//  1 - soma
-//  2 - axon
-//  3 - (basal) dendrite
-//  4 - apical dendrite
-// http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html
-// However, any positive integer tag can be provided and labelled dynamically.
-
-struct msample {
-    mpoint loc;
+// Describe a cable segment between two adjacent samples.
+struct msegment {
+    mpoint prox;
+    mpoint dist;
     int tag;
 
-    friend std::ostream& operator<<(std::ostream&, const msample&);
+    friend bool operator==(const msegment&, const msegment&);
+    friend std::ostream& operator<<(std::ostream&, const msegment&);
 };
 
-bool is_collocated(const msample& a, const msample& b);
-double distance(const msample& a, const msample& b);
 
 // Describe a specific location on a morpholology.
 struct mlocation {
@@ -125,31 +117,6 @@ std::ostream& operator<<(std::ostream& o, const mcable_list& c);
 // and that the cables in the vector are ordered.
 bool test_invariants(const mcable_list&);
 
-using point_prop = std::uint8_t;
-enum point_prop_mask: point_prop {
-    point_prop_mask_none = 0,
-    point_prop_mask_root = 1,
-    point_prop_mask_fork = 2,
-    point_prop_mask_terminal = 4,
-    point_prop_mask_collocated = 8
-};
-
-#define ARB_PROP(prop) \
-constexpr bool is_##prop(point_prop p) {\
-    return p&point_prop_mask_##prop;\
-} \
-inline void set_##prop(point_prop& p) {\
-    p |= point_prop_mask_##prop;\
-} \
-inline void unset_##prop(point_prop& p) {\
-    p &= ~point_prop_mask_##prop;\
-}
-
-ARB_PROP(root)
-ARB_PROP(fork)
-ARB_PROP(terminal)
-ARB_PROP(collocated)
-
 } // namespace arb
 
 ARB_DEFINE_HASH(arb::mcable, a.branch, a.prox_pos, a.dist_pos);
diff --git a/arbor/include/arbor/morph/sample_tree.hpp b/arbor/include/arbor/morph/sample_tree.hpp
deleted file mode 100644
index 2ae690ebdc13cc9e95e440c763515de9e696c3a1..0000000000000000000000000000000000000000
--- a/arbor/include/arbor/morph/sample_tree.hpp
+++ /dev/null
@@ -1,56 +0,0 @@
-#pragma once
-
-#include <cassert>
-#include <functional>
-#include <vector>
-#include <string>
-
-#include <arbor/swcio.hpp>
-#include <arbor/morph/primitives.hpp>
-
-namespace arb {
-
-/// Morphology composed of samples.
-class sample_tree {
-    std::vector<msample> samples_;
-    std::vector<msize_t> parents_;
-    std::vector<point_prop> props_;
-
-public:
-    sample_tree() = default;
-    sample_tree(std::vector<msample>, std::vector<msize_t>);
-
-    // Reserve space for n samples.
-    void reserve(msize_t n);
-
-    // The append functions return a handle to the last sample appended by the call.
-
-    // Append a single sample.
-    msize_t append(msize_t p, const msample& s); // to sample p.
-    msize_t append(const msample& s); // to the last sample in the tree.
-
-    // Append a sequence of samples.
-    msize_t append(msize_t p, const std::vector<msample>& slist); // to sample p.
-    msize_t append(const std::vector<msample>& slist); // to the last sample in the tree.
-
-    // The number of samples in the tree.
-    msize_t size() const;
-    bool empty() const;
-
-    // The samples in the tree.
-    const std::vector<msample>& samples() const;
-
-    // The parent index of the samples.
-    const std::vector<msize_t>& parents() const;
-
-    // The properties of the samples.
-    const std::vector<point_prop>& properties() const;
-
-    friend std::ostream& operator<<(std::ostream&, const sample_tree&);
-};
-
-/// Build a sample tree from a sequence of swc records.
-sample_tree swc_as_sample_tree(const std::vector<swc_record>& swc_records);
-
-} // namesapce arb
-
diff --git a/arbor/include/arbor/morph/segment_tree.hpp b/arbor/include/arbor/morph/segment_tree.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3686965d9f992abad2aa63b20091c45f67e0eab
--- /dev/null
+++ b/arbor/include/arbor/morph/segment_tree.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <cassert>
+#include <functional>
+#include <vector>
+#include <string>
+
+#include <arbor/swcio.hpp>
+#include <arbor/morph/primitives.hpp>
+
+namespace arb {
+
+/// Morphology composed of segments.
+class segment_tree {
+    struct child_prop {
+        int count;
+        bool is_fork() const { return count>1; }
+        bool is_terminal() const { return count==0; }
+        int increment() { return ++count;}
+    };
+
+    std::vector<msegment> segments_;
+    std::vector<msize_t> parents_;
+    std::vector<child_prop> seg_children_;
+
+public:
+    segment_tree() = default;
+
+    // Reserve space for n segments.
+    void reserve(msize_t n);
+
+    // The append functions return a handle to the last segment appended by the call.
+
+    // Append a single segment.
+    msize_t append(msize_t p, const mpoint& prox, const mpoint& dist, int tag);
+    msize_t append(msize_t p, const mpoint& dist, int tag);
+
+    // The number of segments in the tree.
+    msize_t size() const;
+    bool empty() const;
+
+    // The segments in the tree.
+    const std::vector<msegment>& segments() const;
+
+    // The parent index of the segments.
+    const std::vector<msize_t>& parents() const;
+
+    // Interfaces for querying the properties of the segments by index.
+
+    bool is_fork(msize_t i) const;
+    bool is_terminal(msize_t i) const;
+    bool is_root(msize_t i) const;
+
+    friend std::ostream& operator<<(std::ostream&, const segment_tree&);
+};
+
+/// Build a sample tree from a sequence of swc records.
+segment_tree swc_as_segment_tree(const std::vector<swc_record>& swc_records);
+
+} // namesapce arb
+
+
diff --git a/arbor/morph/embed_pwlin.cpp b/arbor/morph/embed_pwlin.cpp
index 64971a03e62d5f7b6780ca4cc7e4eeade5b3aae3..2e381804a8ae99f010e684d52c057ce450b183ac 100644
--- a/arbor/morph/embed_pwlin.cpp
+++ b/arbor/morph/embed_pwlin.cpp
@@ -201,108 +201,91 @@ embed_pwlin::embed_pwlin(const arb::morphology& m) {
     constexpr double pi = math::pi<double>;
     msize_t n_branch = m.num_branches();
     data_ = std::make_shared<embed_pwlin_data>(n_branch);
+    branch_segment_part_.reserve(n_branch+1);
 
     if (!n_branch) return;
 
-    const auto& samples = m.samples();
-    sample_locations_.resize(m.num_samples());
+    branch_segment_part_.push_back(0);
 
-    double proj_shift = samples.front().loc.z;
+    double proj_shift = m.branch_segments(0).front().prox.z;
 
     for (msize_t bid = 0; bid<n_branch; ++bid) {
         unsigned parent = m.branch_parent(bid);
-        auto sample_indices = util::make_range(m.branch_indexes(bid));
-        if (bid==0 && m.spherical_root()) {
-            arb_assert(sample_indices.size()==1);
+        auto& segments = m.branch_segments(bid);
+        arb_assert(segments.size());
 
-            // Treat spherical root as area-equivalent cylinder.
-            double r = samples[0].loc.radius;
+        branch_segment_part_.push_back(branch_segment_part_.back()+segments.size()+1);
 
-            data_->directed_projection[bid].push_back(0., 1., rat_element<1, 0>(-r, r));
-            data_->length[bid].push_back(0., 1., rat_element<1, 0>(0, r*2));
-            data_->radius[bid].push_back(0., 1., rat_element<1, 0>(r, r));
-
-            double cyl_area = 4*pi*r*r;
-            data_->area[bid].push_back(0., 1., rat_element<2, 0>(0., cyl_area*0.5, cyl_area));
+        std::vector<double> seg_pos;
+        seg_pos.reserve(segments.size()+1);
+        seg_pos.push_back(0.);
 
-            double cyl_ixa = 2.0/(pi*r);
-            data_->ixa[bid].push_back(0., 1., rat_element<1, 1>(0., cyl_ixa*0.5, cyl_ixa));
-
-            sample_locations_[0] = mlocation{0, 0.5};
+        for (auto &seg: segments) {
+            double d = distance(seg.prox, seg.dist);
+            seg_pos.push_back(seg_pos.back()+d);
         }
-        else {
-            arb_assert(sample_indices.size()>1);
+        double branch_length = seg_pos.back();
+        double length_scale = branch_length>0? 1./branch_length: 0;
+        for (auto& d: seg_pos) {
+            d *= length_scale;
+        }
+        seg_pos.back() = 1; // Circumvent any rounding infelicities.
 
-            std::vector<double> sample_distance;
-            sample_distance.reserve(samples.size());
-            sample_distance.push_back(0.);
+        for (auto d: seg_pos) {
+            segment_locations_.push_back({bid, d});
+        }
 
-            for (auto i: util::count_along(sample_indices)) {
-                if (!i) continue;
+        double length_0 = parent==mnpos? 0: data_->length[parent].back().second[1];
+        data_->length[bid].push_back(0., 1, rat_element<1, 0>(length_0, length_0+branch_length));
 
-                double d = distance(samples[sample_indices[i-1]], samples[sample_indices[i]]);
-                sample_distance.push_back(sample_distance.back()+d);
-            }
+        double area_0 = parent==mnpos? 0: data_->area[parent].back().second[2];
+        double ixa_0 = parent==mnpos? 0: data_->ixa[parent].back().second[2];
 
-            double branch_length = sample_distance.back();
-            double length_scale = branch_length>0? 1./branch_length: 0;
-
-            for (auto i: util::count_along(sample_indices)) {
-                sample_locations_[sample_indices[i]] = mlocation{bid, length_scale*sample_distance[i]};
-            }
-            sample_locations_[sample_indices.back()].pos = 1.; // Circumvent any rounding infelicities.
-
-            double length_0 = parent==mnpos? 0: data_->length[parent].back().second[1];
-            data_->length[bid].push_back(0., 1, rat_element<1, 0>(length_0, length_0+branch_length));
-
-            double area_0 = parent==mnpos? 0: data_->area[parent].back().second[2];
-            double ixa_0 = parent==mnpos? 0: data_->ixa[parent].back().second[2];
-
-            if (length_scale==0) {
-                // Zero-length branch? Weird, but make best show of it.
-                double r = samples[sample_indices[0]].loc.radius;
-                double z = samples[sample_indices[0]].loc.z;
-                data_->radius[bid].push_back(0., 1., rat_element<1, 0>(r, r));
-                data_->directed_projection[bid].push_back(0., 1., rat_element<1, 0>(z-proj_shift, z-proj_shift));
-                data_->area[bid].push_back(0., 1., rat_element<2, 0>(area_0, area_0, area_0));
-                data_->ixa[bid].push_back(0., 1., rat_element<1, 1>(ixa_0, ixa_0, ixa_0));
-            }
-            else {
-                for (auto i: util::count_along(sample_indices)) {
-                    if (!i) continue;
-
-                    double p0 = i>1? sample_locations_[sample_indices[i-1]].pos: 0;
-                    double p1 = sample_locations_[sample_indices[i]].pos;
-                    if (p0==p1) continue;
-
-                    double z0 = samples[sample_indices[i-1]].loc.z - proj_shift;
-                    double z1 = samples[sample_indices[i]].loc.z - proj_shift;
-                    data_->directed_projection[bid].push_back(p0, p1, rat_element<1, 0>(z0, z1));
-
-                    double r0 = samples[sample_indices[i-1]].loc.radius;
-                    double r1 = samples[sample_indices[i]].loc.radius;
-                    data_->radius[bid].push_back(p0, p1, rat_element<1, 0>(r0, r1));
-
-                    double dx = (p1-p0)*branch_length;
-                    double dr = r1-r0;
-                    double c = pi*std::sqrt(dr*dr+dx*dx);
-                    double area_half = area_0 + (0.75*r0+0.25*r1)*c;
-                    double area_1 = area_0 + (r0+r1)*c;
-                    data_->area[bid].push_back(p0, p1, rat_element<2, 0>(area_0, area_half, area_1));
-                    area_0 = area_1;
-
-                    double ixa_half = ixa_0 + dx/(pi*r0*(r0+r1));
-                    double ixa_1 = ixa_0 + dx/(pi*r0*r1);
-                    data_->ixa[bid].push_back(p0, p1, rat_element<1, 1>(ixa_0, ixa_half, ixa_1));
-                    ixa_0 = ixa_1;
-                }
+        if (length_scale==0) {
+            // Zero-length branch? Weird, but make best show of it.
+            auto& s = segments.front().prox;
+            double r = s.radius;
+            double z = s.z;
+            data_->radius[bid].push_back(0., 1., rat_element<1, 0>(r, r));
+            data_->directed_projection[bid].push_back(0., 1., rat_element<1, 0>(z-proj_shift, z-proj_shift));
+            data_->area[bid].push_back(0., 1., rat_element<2, 0>(area_0, area_0, area_0));
+            data_->ixa[bid].push_back(0., 1., rat_element<1, 1>(ixa_0, ixa_0, ixa_0));
+        }
+        else {
+            for (auto i: util::count_along(segments)) {
+                auto prox = segments[i].prox;
+                auto dist = segments[i].dist;
+
+                double p0 = seg_pos[i];
+                double p1 = seg_pos[i+1];
+                if (p0==p1) continue;
+
+                double z0 = prox.z - proj_shift;
+                double z1 = dist.z - proj_shift;
+                data_->directed_projection[bid].push_back(p0, p1, rat_element<1, 0>(z0, z1));
+
+                double r0 = prox.radius;
+                double r1 = dist.radius;
+                data_->radius[bid].push_back(p0, p1, rat_element<1, 0>(r0, r1));
+
+                double dx = (p1-p0)*branch_length;
+                double dr = r1-r0;
+                double c = pi*std::sqrt(dr*dr+dx*dx);
+                double area_half = area_0 + (0.75*r0+0.25*r1)*c;
+                double area_1 = area_0 + (r0+r1)*c;
+                data_->area[bid].push_back(p0, p1, rat_element<2, 0>(area_0, area_half, area_1));
+                area_0 = area_1;
+
+                double ixa_half = ixa_0 + dx/(pi*r0*(r0+r1));
+                double ixa_1 = ixa_0 + dx/(pi*r0*r1);
+                data_->ixa[bid].push_back(p0, p1, rat_element<1, 1>(ixa_0, ixa_half, ixa_1));
+                ixa_0 = ixa_1;
             }
 
             arb_assert((data_->radius[bid].size()>0));
             arb_assert((data_->radius[bid].bounds()==std::pair<double, double>(0., 1.)));
             arb_assert((data_->area[bid].bounds()==std::pair<double, double>(0., 1.)));
             arb_assert((data_->ixa[bid].bounds()==std::pair<double, double>(0., 1.)));
-            arb_assert(sample_locations_.size()==m.samples().size());
         }
     }
 };
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
index 393f9b025a35939fcd6ae1430b641e806eb5acfb..dca7c313a770e23c39b0e86dd6ae56ee57892f84 100644
--- a/arbor/morph/locset.cpp
+++ b/arbor/morph/locset.cpp
@@ -96,25 +96,6 @@ std::ostream& operator<<(std::ostream& o, const location_list_& x) {
     return o << ')';
 }
 
-// Location corresponding to a sample id.
-
-struct sample_: locset_tag {
-    explicit sample_(msize_t index): index(index) {}
-    msize_t index;
-};
-
-locset sample(msize_t index) {
-    return locset{sample_{index}};
-}
-
-mlocation_list thingify_(const sample_& x, const mprovider& p) {
-    return {canonical(p.morphology(), p.embedding().sample_location(x.index))};
-}
-
-std::ostream& operator<<(std::ostream& o, const sample_& x) {
-    return o << "(sample " << x.index << ")";
-}
-
 // Set of terminal points (most distal points).
 
 struct terminal_: locset_tag {};
@@ -151,6 +132,23 @@ std::ostream& operator<<(std::ostream& o, const root_& x) {
     return o << "(root)";
 }
 
+// Locations that mark interface between segments.
+
+struct segments_: locset_tag {};
+
+locset segment_boundaries() {
+    return locset{segments_{}};
+}
+
+mlocation_list thingify_(const segments_&, const mprovider& p) {
+    return p.embedding().segment_locations();
+}
+
+std::ostream& operator<<(std::ostream& o, const segments_& x) {
+    return o << "(segment_boundaries)";
+}
+
+
 // Proportional location on every branch.
 
 struct on_branches_ { double pos; };
diff --git a/arbor/morph/mbranch.cpp b/arbor/morph/mbranch.cpp
deleted file mode 100644
index b52b1e1372912300fd3da78e528b1d4548e3dc54..0000000000000000000000000000000000000000
--- a/arbor/morph/mbranch.cpp
+++ /dev/null
@@ -1,29 +0,0 @@
-#include <ostream>
-
-#include <arbor/morph/primitives.hpp>
-
-#include "io/sepval.hpp"
-#include "morph/mbranch.hpp"
-
-namespace arb {
-namespace impl{
-
-//
-//  mbranch implementation
-//
-
-bool operator==(const mbranch& l, const mbranch& r) {
-    return l.parent_id==r.parent_id && l.index==r.index;
-}
-
-std::ostream& operator<<(std::ostream& o, const mbranch& b) {
-    o <<"mbranch([" << io::csv(b.index) << "], ";
-    if (b.parent_id==mnpos) o << "none)";
-    else  o << b.parent_id << ")";
-    return o;
-}
-
-
-} // namespace impl
-} // namespace arb
-
diff --git a/arbor/morph/mbranch.hpp b/arbor/morph/mbranch.hpp
deleted file mode 100644
index 5d680d1d23af1ac9fd7de79f8ba302c868643042..0000000000000000000000000000000000000000
--- a/arbor/morph/mbranch.hpp
+++ /dev/null
@@ -1,38 +0,0 @@
-#pragma once
-
-/*
- * Definitions and prototypes used in the morphology implementation.
- * This header is used to simplify unit testing of the morphology implementation.
- */
-
-#include <cmath>
-#include <vector>
-
-#include <arbor/morph/primitives.hpp>
-
-namespace arb {
-namespace impl{
-
-// An unbranched cable segment that has root, terminal or fork point at each end.
-struct mbranch {
-    std::vector<msize_t> index;  // sample index
-    msize_t parent_id = mnpos;   // branch index
-
-    mbranch() = default;
-    mbranch(std::vector<msize_t> idx, msize_t parent):
-        index(std::move(idx)), parent_id(parent) {}
-
-    bool is_sphere()  const { return size()==1u; }
-    msize_t size()    const { return index.size(); }
-    bool has_parent() const { return parent_id!=mnpos;}
-
-    friend bool operator==(const mbranch& l, const mbranch& r);
-    friend std::ostream& operator<<(std::ostream& o, const mbranch& b);
-};
-
-std::vector<mbranch> branches_from_parent_index(const std::vector<msize_t>& parents,
-                                                const std::vector<point_prop>& props,
-                                                bool spherical_root);
-
-} // namespace impl
-} // namespace arb
diff --git a/arbor/morph/morphexcept.cpp b/arbor/morph/morphexcept.cpp
index 3364127573b08269f343f197d3dc08fad05c8f0c..ac9222bbc8898afaec79c1d04683808df294e500 100644
--- a/arbor/morph/morphexcept.cpp
+++ b/arbor/morph/morphexcept.cpp
@@ -10,16 +10,26 @@ namespace arb {
 
 using arb::util::pprintf;
 
+static std::string msize_string(msize_t x) {
+    return x==mnpos? "mnpos": pprintf("{}", x);
+}
+
 invalid_mlocation::invalid_mlocation(mlocation loc):
     morphology_error(pprintf("invalid mlocation {}", loc)),
     loc(loc)
 {}
 
 no_such_branch::no_such_branch(msize_t bid):
-    morphology_error(pprintf("no such branch id {}", bid)),
+    morphology_error(pprintf("no such branch id {}", msize_string(bid))),
     bid(bid)
 {}
 
+no_such_segment::no_such_segment(msize_t id):
+    arbor_exception(pprintf("segment {} out of bounds", id)),
+    sid(id)
+{}
+
+
 invalid_mcable::invalid_mcable(mcable cable):
     morphology_error(pprintf("invalid mcable {}", cable)),
     cable(cable)
@@ -29,19 +39,13 @@ invalid_mcable_list::invalid_mcable_list():
     morphology_error("bad mcable_list")
 {}
 
-invalid_sample_parent::invalid_sample_parent(msize_t parent, msize_t tree_size):
-    morphology_error(pprintf("invalid sample parent {} for a sample tree of size {}", parent, tree_size)),
-    parent(parent),
-    tree_size(tree_size)
-{}
-
 label_type_mismatch::label_type_mismatch(const std::string& label):
     morphology_error(pprintf("label \"{}\" is already bound to a different type of object", label)),
     label(label)
 {}
 
 incomplete_branch::incomplete_branch(msize_t bid):
-    morphology_error(pprintf("insufficent samples to define branch id {}", bid)),
+    morphology_error(pprintf("insufficent samples to define branch id {}", msize_string(bid))),
     bid(bid)
 {}
 
@@ -55,5 +59,11 @@ circular_definition::circular_definition(const std::string& name):
     name(name)
 {}
 
+invalid_segment_parent::invalid_segment_parent(msize_t parent, msize_t tree_size):
+    morphology_error(pprintf("invalid segment parent {} for a segment tree of size {}", msize_string(parent), tree_size)),
+    parent(parent),
+    tree_size(tree_size)
+{}
+
 } // namespace arb
 
diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp
index 623eb7d9bc0d51c79fde9724796606f075c08dfc..ee861e749461f1bb49961e5216a62d1ee5410ae9 100644
--- a/arbor/morph/morphology.cpp
+++ b/arbor/morph/morphology.cpp
@@ -5,84 +5,67 @@
 
 #include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/morphology.hpp>
-#include <arbor/morph/sample_tree.hpp>
+#include <arbor/morph/segment_tree.hpp>
 #include <arbor/morph/primitives.hpp>
 
-#include "morph/mbranch.hpp"
+#include "io/sepval.hpp"
 #include "util/mergeview.hpp"
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
+#include "util/transform.hpp"
 
 using arb::util::make_span;
 
 namespace arb {
 namespace impl {
 
-std::vector<mbranch> branches_from_parent_index(const std::vector<msize_t>& parents,
-                                                const std::vector<point_prop>& props,
-                                                bool spherical_root)
-{
-    auto nsamp = parents.size();
-    if (!nsamp) return {};
-
-    // Enforce that a morphology with one sample has a spherical root.
-    if (!spherical_root && nsamp==1u) {
-        throw incomplete_branch(0);
-    }
-
-    std::vector<int> bids(nsamp);
-    int nbranches = spherical_root? 1: 0;
-    for (auto i: make_span(1, nsamp)) {
-        auto p = parents[i];
-        bool first = is_root(props[p]) || is_fork(props[p]);
-        bids[i] = first? nbranches++: bids[p];
-    }
-
-    std::vector<mbranch> branches(nbranches);
-    for (auto i: make_span(nsamp)) {
-        auto p = parents[i];
-        auto& branch = branches[bids[i]];
-        if (!branch.size()) {
-            bool null_root = spherical_root? p==mnpos: p==mnpos||p==0;
-            branch.parent_id = null_root? mnpos: bids[p];
-
-            // Add the distal sample from the parent branch if the parent is not
-            // a spherical root or mnpos.
-            if (p!=mnpos && !(p==0 && spherical_root)) {
-                branch.index.push_back(p);
-            }
+auto branches_from_segment_tree(const segment_tree& tree) {
+    using rtype = std::pair<std::vector<msize_t>,
+                            std::vector<std::vector<msegment>>>;
+    rtype branches;
+    auto& branch_parents = branches.first;
+    auto& branch_segs = branches.second;
+    auto& seg_parents = tree.parents();
+    auto& segs = tree.segments();
+
+    auto nsegs = seg_parents.size();
+    if (!nsegs) return branches;
+
+    // Determine which branch each segment belongs to while counting the number
+    // of branches in the morphology.
+    std::vector<msize_t> bids(nsegs);
+    int nbranches = 1;
+    bids[0] = 0;
+    for (auto i: make_span(1, nsegs)) {
+        if (tree.is_root(i)) {
+            bids[i] = nbranches++;
+        }
+        else {
+            auto p = seg_parents[i];
+            bids[i] = tree.is_fork(p)? nbranches++: bids[p];
         }
-
-        branch.index.push_back(i);
     }
 
-    // Enforce that all cable branches that are potentially connected to a spherical
-    // root contain at least two samples.
-    if (spherical_root) {
-        for (auto i: make_span(1, nbranches)) { // skip the root.
-            if (branches[i].size()<2u) {
-                throw incomplete_branch(i);
-            }
+    // A working vector used to track whether the first segment in a branch has been visited.
+    std::vector<char> visited(nbranches);
+
+    branch_segs.resize(nbranches);
+    branch_parents.resize(nbranches);
+    // Construct all of the cable segments for all of the branches.
+    for (auto i: make_span(nsegs)) {
+        auto p = seg_parents[i];
+        auto b = bids[i];
+        // If this is the first sample in the branch, set the branch's parent branch.
+        if (!visited[b]) {
+            branch_parents[b] = p==mnpos? mnpos: bids[p];
+            visited[b] = 1;
         }
+        branch_segs[b].push_back(segs[i]);
     }
 
     return branches;
 }
 
-// Returns false if one of the root's children has the same tag as the root.
-bool root_sample_tag_differs_from_children(const sample_tree& st) {
-    if (st.empty()) return false;
-    auto& P = st.parents();
-    auto& S = st.samples();
-    auto root_tag = S.front().tag;
-    for (auto i: util::make_span(1, st.size())) {
-        if (0u==P[i] && S[i].tag==root_tag) {
-            return false;
-        }
-    }
-    return true;
-}
-
 } // namespace impl
 
 //
@@ -90,55 +73,35 @@ bool root_sample_tag_differs_from_children(const sample_tree& st) {
 //
 
 struct morphology_impl {
-    // The sample tree of sample points and their parent-child relationships.
-    sample_tree samples_;
-
-    // Indicates whether the soma is a sphere.
-    bool spherical_root_ = false;
-
     // Branch state.
-    std::vector<impl::mbranch> branches_;
+    std::vector<std::vector<msegment>> branches_;
     std::vector<msize_t> branch_parents_;
     std::vector<msize_t> root_children_;
     std::vector<msize_t> terminal_branches_;
     std::vector<std::vector<msize_t>> branch_children_;
 
-    morphology_impl(sample_tree m, bool use_spherical_root);
-    morphology_impl(sample_tree m);
+    morphology_impl(const segment_tree& m);
 
     void init();
 
     friend std::ostream& operator<<(std::ostream&, const morphology_impl&);
 };
 
-morphology_impl::morphology_impl(sample_tree m, bool use_spherical_root):
-    samples_(std::move(m)),
-    spherical_root_(use_spherical_root)
-{
-    init();
-}
-
-morphology_impl::morphology_impl(sample_tree m):
-    samples_(std::move(m)),
-    spherical_root_(impl::root_sample_tag_differs_from_children(samples_))
-{
-    init();
-}
-
-void morphology_impl::init() {
-    auto nsamp = samples_.size();
+morphology_impl::morphology_impl(const segment_tree& tree) {
+    auto nsamp = tree.size();
     if (!nsamp) return;
 
     // Generate branches.
-    branches_ = impl::branches_from_parent_index(samples_.parents(), samples_.properties(), spherical_root_);
+    auto B = impl::branches_from_segment_tree(tree);
+    branches_ = std::move(B.second);
+    branch_parents_ = std::move(B.first);
     auto nbranch = branches_.size();
 
     // Generate branch tree.
     branch_children_.resize(nbranch);
     branch_parents_.reserve(nbranch);
     for (auto i: make_span(nbranch)) {
-        auto id = branches_[i].parent_id;
-        branch_parents_.push_back(id);
+        auto id = branch_parents_[i];
         if (id!=mnpos) {
             branch_children_[id].push_back(i);
         }
@@ -158,29 +121,29 @@ void morphology_impl::init() {
 }
 
 std::ostream& operator<<(std::ostream& o, const morphology_impl& m) {
-    o << "morphology: "
-      << m.samples_.size() << " samples, "
-      << m.branches_.size() << " branches.";
-    for (auto i: util::make_span(m.branches_.size()))
-        o << "\n  branch " << i << ": " << m.branches_[i];
-
-    return o;
+    if (m.branches_.empty()) {
+        return o << "(morphology ())";
+    }
+    bool first = true;
+    o << "(morphology\n  (";
+    for (auto i: util::make_span(m.branches_.size())) {
+        if (!first) o << "\n  ";
+        o << "(" << m.branch_parents_[i] << " (" << io::sepval(m.branches_[i], " ") << "))";
+        first = false;
+    }
+    return o << "))";
 }
 
 //
 // morphology implementation
 //
 
-morphology::morphology(sample_tree m, bool use_spherical_root):
-    impl_(std::make_shared<const morphology_impl>(std::move(m), use_spherical_root))
-{}
-
-morphology::morphology(sample_tree m):
+morphology::morphology(segment_tree m):
     impl_(std::make_shared<const morphology_impl>(std::move(m)))
 {}
 
 morphology::morphology():
-    morphology(sample_tree())
+    morphology(segment_tree())
 {}
 
 bool morphology::empty() const {
@@ -192,11 +155,6 @@ msize_t morphology::branch_parent(msize_t b) const {
     return impl_->branch_parents_[b];
 }
 
-// The parent sample of sample i.
-const std::vector<msize_t>& morphology::sample_parents() const {
-    return impl_->samples_.parents();
-}
-
 // The child branches of branch b.
 const std::vector<msize_t>& morphology::branch_children(msize_t b) const {
     return b==mnpos? impl_->root_children_: impl_->branch_children_[b];
@@ -206,27 +164,8 @@ const std::vector<msize_t>& morphology::terminal_branches() const {
     return impl_->terminal_branches_;
 }
 
-// Whether the root of the morphology is spherical.
-bool morphology::spherical_root() const {
-    return impl_->spherical_root_;
-}
-
-mindex_range morphology::branch_indexes(msize_t b) const {
-    const auto& idx = impl_->branches_[b].index;
-    return std::make_pair(idx.data(), idx.data()+idx.size());
-}
-
-const std::vector<msample>& morphology::samples() const {
-    return impl_->samples_.samples();
-}
-
-// Point properties of samples in the morphology.
-const std::vector<point_prop>& morphology::sample_props() const {
-    return impl_->samples_.properties();
-}
-
-msize_t morphology::num_samples() const {
-    return impl_->samples_.size();
+const std::vector<msegment>& morphology::branch_segments(msize_t b) const {
+    return impl_->branches_[b];
 }
 
 msize_t morphology::num_branches() const {
diff --git a/arbor/morph/place_pwlin.cpp b/arbor/morph/place_pwlin.cpp
index 4f7a78ef55ea7f2daf752afd26f6142c5f4475ec..9fe9afdb1643e88886fc1e8820fe27cec00dcb4e 100644
--- a/arbor/morph/place_pwlin.cpp
+++ b/arbor/morph/place_pwlin.cpp
@@ -8,6 +8,7 @@
 
 #include "morph/pwlin_common.hpp"
 #include "util/piecewise.hpp"
+#include "util/rangeutil.hpp"
 #include "util/ratelem.hpp"
 #include "util/span.hpp"
 
@@ -36,106 +37,48 @@ place_pwlin::place_pwlin(const arb::morphology& m, const isometry& iso) {
 
     if (!n_branch) return;
 
-    const auto& samples = m.samples();
-
     std::vector<double> sample_pos_on_branch;
 
+    std::vector<double> seg_pos;
     for (msize_t bid = 0; bid<n_branch; ++bid) {
-        auto sample_indices = util::make_range(m.branch_indexes(bid));
-        if (bid==0 && m.spherical_root()) {
-            arb_assert(sample_indices.size()==1);
-            arb_assert(sample_indices.front()==0);
-
-            // Use the next distinct sample, if it exists, to determine the
-            // proximal-distal axis for a spherical root.
-
-            mpoint c = samples[0].loc;
-            mpoint d = c;
-            for (msize_t i = 1; i<samples.size(); ++i) {
-                const auto p = samples[i].loc;
-                if (p.x!=c.x || p.y!=c.y || p.z!=c.z) {
-                    d = p;
-                    break;
-                }
-            }
-
-            mpoint u0 = c, u1 = c;
+        auto& segments = m.branch_segments(bid);
+        arb_assert(!segments.empty());
 
-            if (c.x!=d.x || c.y!=d.y || c.z!=d.z) {
-                double dx = d.x-c.x;
-                double dy = d.y-c.y;
-                double dz = d.z-c.z;
-                double scale = c.radius/std::sqrt(dx*dx+dy*dy+dz*dz);
-
-                u0.x -= dx*scale;
-                u0.y -= dy*scale;
-                u0.z -= dz*scale;
+        seg_pos.reserve(segments.size()+1);
+        seg_pos = {0};
+        for (auto& seg: segments) {
+            seg_pos.push_back(seg_pos.back()+distance(seg.prox, seg.dist));
+        }
 
-                u1.x += dx*scale;
-                u1.y += dy*scale;
-                u1.z += dz*scale;
-            }
-            else {
-                // No luck, so pick distal as negative z-axis.
-                u0.z += c.radius;
-                u1.z -= c.radius;
-            }
+        double branch_length = seg_pos.back();
+        double length_scale = branch_length>0? 1./branch_length: 0;
+        for (auto& x: seg_pos) {
+            x *= length_scale;
+        }
 
-            u0 = iso.apply(u0);
-            u1 = iso.apply(u1);
+        if (length_scale==0) {
+            // Zero-length branch case?
+            mpoint p = iso.apply(segments[0].prox);
 
-            data_->x[bid].push_back(0., 1., rat_element<1, 0>(u0.x, u1.x));
-            data_->y[bid].push_back(0., 1., rat_element<1, 0>(u0.y, u1.y));
-            data_->z[bid].push_back(0., 1., rat_element<1, 0>(u0.z, u1.z));
-            data_->r[bid].push_back(0., 1., rat_element<1, 0>(u0.radius, u1.radius));
+            data_->x[bid].push_back(0., 1., rat_element<1, 0>(p.x, p.x));
+            data_->y[bid].push_back(0., 1., rat_element<1, 0>(p.y, p.y));
+            data_->z[bid].push_back(0., 1., rat_element<1, 0>(p.z, p.z));
+            data_->r[bid].push_back(0., 1., rat_element<1, 0>(p.radius, p.radius));
         }
         else {
-            arb_assert(sample_indices.size()>1);
-
-            sample_pos_on_branch.reserve(samples.size());
-            sample_pos_on_branch = {0};
-
-            for (auto i: util::count_along(sample_indices)) {
-                if (!i) continue;
-
-                sample_pos_on_branch.push_back(
-                    sample_pos_on_branch.back()+
-                    distance(samples[sample_indices[i-1]], samples[sample_indices[i]]));
-            }
-
-            double branch_length = sample_pos_on_branch.back();
-            double length_scale = branch_length>0? 1./branch_length: 0;
-
-            for (auto& x: sample_pos_on_branch) {
-                x *= length_scale;
-            }
-
-            if (length_scale==0) {
-                // Zero-length branch case?
-
-                mpoint p = iso.apply(samples[sample_indices[0]].loc);
-
-                data_->x[bid].push_back(0., 1., rat_element<1, 0>(p.x, p.x));
-                data_->y[bid].push_back(0., 1., rat_element<1, 0>(p.y, p.y));
-                data_->z[bid].push_back(0., 1., rat_element<1, 0>(p.z, p.z));
-                data_->r[bid].push_back(0., 1., rat_element<1, 0>(p.radius, p.radius));
-            }
-            else {
-                for (auto i: util::count_along(sample_indices)) {
-                    if (!i) continue;
-
-                    double p0 = i>1? sample_pos_on_branch[i-1]: 0;
-                    double p1 = sample_pos_on_branch[i];
-                    if (p0==p1) continue;
-
-                    mpoint u0 = iso.apply(samples[sample_indices[i-1]].loc);
-                    mpoint u1 = iso.apply(samples[sample_indices[i]].loc);
-
-                    data_->x[bid].push_back(p0, p1, rat_element<1, 0>(u0.x, u1.x));
-                    data_->y[bid].push_back(p0, p1, rat_element<1, 0>(u0.y, u1.y));
-                    data_->z[bid].push_back(p0, p1, rat_element<1, 0>(u0.z, u1.z));
-                    data_->r[bid].push_back(p0, p1, rat_element<1, 0>(u0.radius, u1.radius));
-                }
+            for (auto i: util::count_along(segments)) {
+                auto& seg = segments[i];
+                double p0 = seg_pos[i];
+                double p1 = seg_pos[i+1];
+                if (p0==p1) continue;
+
+                mpoint u0 = iso.apply(seg.prox);
+                mpoint u1 = iso.apply(seg.dist);
+
+                data_->x[bid].push_back(p0, p1, rat_element<1, 0>(u0.x, u1.x));
+                data_->y[bid].push_back(p0, p1, rat_element<1, 0>(u0.y, u1.y));
+                data_->z[bid].push_back(p0, p1, rat_element<1, 0>(u0.z, u1.z));
+                data_->r[bid].push_back(p0, p1, rat_element<1, 0>(u0.radius, u1.radius));
             }
         }
     }
diff --git a/arbor/morph/primitives.cpp b/arbor/morph/primitives.cpp
index 40d9061b4ad430697f8236017c611095a9d265a7..ced745abf2010645ac9b9b83a3b50a710c61afbc 100644
--- a/arbor/morph/primitives.cpp
+++ b/arbor/morph/primitives.cpp
@@ -57,14 +57,6 @@ double distance(const mpoint& a, const mpoint& b) {
     return std::sqrt(dx*dx + dy*dy + dz*dz);
 }
 
-bool is_collocated(const msample& a, const msample& b) {
-    return is_collocated(a.loc, b.loc);
-}
-
-double distance(const msample& a, const msample& b) {
-    return distance(a.loc, b.loc);
-}
-
 bool test_invariants(const mlocation& l) {
     return (0.<=l.pos && l.pos<=1.) && l.branch!=mnpos;
 }
@@ -140,12 +132,16 @@ bool test_invariants(const mcable_list& l) {
         && l.end()==std::find_if(l.begin(), l.end(), [](const mcable& c) {return !test_invariants(c);});
 }
 
+bool operator==(const msegment& l, const msegment& r) {
+    return l.prox==r.prox && l.dist==r.dist && l.tag==r.tag;
+}
+
 std::ostream& operator<<(std::ostream& o, const mpoint& p) {
     return o << "(point " << p.x << " " << p.y << " " << p.z << " " << p.radius << ")";
 }
 
-std::ostream& operator<<(std::ostream& o, const msample& s) {
-    return o << "(sample " << s.loc << " " << s.tag << ")";
+std::ostream& operator<<(std::ostream& o, const msegment& s) {
+    return o << "(segment " << s.prox << " " << s.dist << " " << s.tag << ")";
 }
 
 std::ostream& operator<<(std::ostream& o, const mlocation& l) {
diff --git a/arbor/morph/region.cpp b/arbor/morph/region.cpp
index 89bfce428cf09eaf90e274edbc60c544f99dafd2..6658ed453a6104fb90443fcbc2f75bb34f66f796 100644
--- a/arbor/morph/region.cpp
+++ b/arbor/morph/region.cpp
@@ -146,36 +146,24 @@ mextent thingify_(const tagged_& reg, const mprovider& p) {
 
     std::vector<mcable> L;
     L.reserve(nb);
-    auto& samples = m.samples();
-    auto matches     = [reg, &samples](msize_t i) {return samples[i].tag==reg.tag;};
-    auto not_matches = [&matches](msize_t i) {return !matches(i);};
+    auto matches     = [reg](auto& seg){return seg.tag==reg.tag;};
+    auto not_matches = [&matches](auto& seg) {return !matches(seg);};
 
     for (msize_t i: util::make_span(nb)) {
-        auto ids = util::make_range(m.branch_indexes(i)); // range of sample ids in branch.
-        size_t ns = util::size(ids);        // number of samples in branch.
+        auto& segs = m.branch_segments(i); // Range of segments in the branch.
+        auto locs = util::make_range(e.branch_segment_locations(i));
 
-        if (ns==1) {
-            // The branch is a spherical soma
-            if (samples[0].tag==reg.tag) {
-                L.push_back({0,0,1});
-            }
-            continue;
-        }
-
-        // The branch has at least 2 samples.
-        // Start at begin+1 because a segment gets its tag from its distal sample.
-        auto beg = std::cbegin(ids);
-        auto end = std::cend(ids);
+        auto beg = std::cbegin(segs);
+        auto end = std::cend(segs);
 
-        // Find the next sample that matches reg.tag.
-        auto start = std::find_if(beg+1, end, matches);
+        // Find the next section that matches reg.tag.
+        auto start = std::find_if(beg, end, [reg](auto& seg){return seg.tag==reg.tag;});
         while (start!=end) {
             // find the next sample that does not match reg.tag
-            auto first = start-1;
             auto last = std::find_if(start, end, not_matches);
 
-            auto l = first==beg? 0.: e.sample_location(*first).pos;
-            auto r = last==end?  1.: e.sample_location(*(last-1)).pos;
+            auto l = locs[start-beg].pos;
+            auto r = last==end? 1: locs[last-beg].pos;
             L.push_back({i, l, r});
 
             // Find the next sample in the branch that matches reg.tag.
diff --git a/arbor/morph/sample_tree.cpp b/arbor/morph/sample_tree.cpp
deleted file mode 100644
index 4ba6d256ac42697f4978f05a4c97922376480ea4..0000000000000000000000000000000000000000
--- a/arbor/morph/sample_tree.cpp
+++ /dev/null
@@ -1,127 +0,0 @@
-#include <iostream>
-#include <stdexcept>
-#include <vector>
-
-#include <arbor/morph/morphexcept.hpp>
-#include <arbor/morph/sample_tree.hpp>
-
-#include "io/sepval.hpp"
-#include "util/span.hpp"
-#include "util/transform.hpp"
-
-namespace arb {
-
-sample_tree::sample_tree(std::vector<msample> samples, std::vector<msize_t> parents) {
-    if (samples.size()!=parents.size()) {
-        throw std::invalid_argument("sample and parent vectors differ in size");
-    }
-    reserve(samples.size());
-    for (auto i: util::make_span(samples.size())) {
-        append(parents[i], samples[i]);
-    }
-}
-
-void sample_tree::reserve(msize_t n) {
-    samples_.reserve(n);
-    parents_.reserve(n);
-    props_.reserve(n);
-}
-
-msize_t sample_tree::append(msize_t p, const msample& s) {
-    if ((empty() && p!=mnpos) || p>=size()) {
-        if (p!=mnpos) throw invalid_sample_parent(p, size());
-    }
-
-    auto id = size();
-    samples_.push_back(s);
-    parents_.push_back(p);
-
-    // Set the point properties for the new point, and update those of its parent as needed.
-    point_prop prop = point_prop_mask_none;
-    if (!id) {
-        // if adding the first sample mark it as root
-        set_root(prop);
-    }
-    else {
-        // Mark the new node as terminal, and unset the parent sample's terminal bit.
-        set_terminal(prop);
-        const bool term_parent = is_terminal(props_[p]); // track if the parent was terminal.
-        unset_terminal(props_[p]);
-
-        // Mark if the new sample is collocated with its parent.
-        if (is_collocated(s, samples_[p])) {
-            set_collocated(prop);
-        }
-
-        // Set parent to be a fork if it was not a terminal point before the
-        // new sample was added (and if it isn't the root).
-        if (p && !term_parent) {
-            set_fork(props_[p]);
-        }
-    }
-    props_.push_back(prop);
-
-    return id;
-}
-
-msize_t sample_tree::append(const msample& s) {
-    return append(empty()? mnpos: size()-1, s);
-}
-
-msize_t sample_tree::append(msize_t p, const std::vector<msample>& slist) {
-    if (!slist.size()) return size();
-
-    for (auto& s: slist) {
-        p = append(p, s);
-    }
-
-    return p;
-}
-
-msize_t sample_tree::append(const std::vector<msample>& slist) {
-    return append(empty()? mnpos: size()-1, slist);
-}
-
-msize_t sample_tree::size() const {
-    return samples_.size();
-}
-
-bool sample_tree::empty() const {
-    return samples_.empty();
-}
-
-const std::vector<msample>& sample_tree::samples() const {
-    return samples_;
-}
-
-const std::vector<msize_t>& sample_tree::parents() const {
-    return parents_;
-}
-
-const std::vector<point_prop>& sample_tree::properties() const {
-    return props_;
-}
-
-std::ostream& operator<<(std::ostream& o, const sample_tree& m) {
-    auto tstr = util::transform_view(m.parents_,
-            [](msize_t i) -> std::string {
-                return i==mnpos? "npos": std::to_string(i);
-            });
-    return o << "(sample_tree (\n  " << io::sepval(m.samples_, "\n  ") <<  ")\n"
-             << "  (" << io::sepval(tstr, ' ') <<  "))";
-}
-
-sample_tree swc_as_sample_tree(const std::vector<swc_record>& swc_records) {
-    sample_tree m;
-    m.reserve(swc_records.size());
-
-    for (auto i: util::count_along(swc_records)) {
-        auto& r = swc_records[i];
-        // The parent of soma must be mnpos, while in SWC files is -1
-        msize_t p = i==0? mnpos: r.parent_id;
-        m.append(p, msample{{r.x, r.y, r.z, r.r}, r.tag});
-    }
-    return m;
-}
-
-} // namespace arb
diff --git a/arbor/morph/segment_tree.cpp b/arbor/morph/segment_tree.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8629cef536287a475ceea878eb4737b84894f1fe
--- /dev/null
+++ b/arbor/morph/segment_tree.cpp
@@ -0,0 +1,126 @@
+#include <iostream>
+#include <stdexcept>
+#include <unordered_map>
+#include <vector>
+
+#include <arbor/morph/morphexcept.hpp>
+#include <arbor/morph/segment_tree.hpp>
+
+#include "io/sepval.hpp"
+#include "util/span.hpp"
+#include "util/transform.hpp"
+
+namespace arb {
+
+void segment_tree::reserve(msize_t n) {
+    segments_.reserve(n);
+    parents_.reserve(n);
+    seg_children_.reserve(n);
+}
+
+msize_t segment_tree::append(msize_t p, const mpoint& prox, const mpoint& dist, int tag) {
+    if (p>=size() && p!=mnpos) {
+        throw invalid_segment_parent(p, size());
+    }
+
+    auto id = size();
+    segments_.push_back(msegment{prox, dist, tag});
+    parents_.push_back(p);
+
+    // Set the point properties for the new point, and update those of the parent.
+    seg_children_.push_back({});
+    if (p!=mnpos) {
+        seg_children_[p].increment();
+    }
+
+    return id;
+}
+
+msize_t segment_tree::append(msize_t p, const mpoint& dist, int tag) {
+    // If attaching to the root both prox and dist ends must be specified.
+    if (p==mnpos) {
+        throw invalid_segment_parent(p, size());
+    }
+    if (p>=size()) {
+        throw invalid_segment_parent(p, size());
+    }
+    return append(p, segments_[p].dist, dist, tag);
+}
+
+msize_t segment_tree::size() const {
+    return segments_.size();
+}
+
+bool segment_tree::empty() const {
+    return segments_.empty();
+}
+
+const std::vector<msegment>& segment_tree::segments() const {
+    return segments_;
+}
+
+const std::vector<msize_t>& segment_tree::parents() const {
+    return parents_;
+}
+
+bool segment_tree::is_fork(msize_t i) const {
+    if (i>=size()) throw no_such_segment(i);
+    return seg_children_[i].is_fork();
+}
+bool segment_tree::is_terminal(msize_t i) const {
+    if (i>=size()) throw no_such_segment(i);
+    return seg_children_[i].is_terminal();
+}
+bool segment_tree::is_root(msize_t i) const {
+    if (i>=size()) throw no_such_segment(i);
+    return parents_[i]==mnpos;
+}
+
+std::ostream& operator<<(std::ostream& o, const segment_tree& m) {
+    auto tstr = util::transform_view(m.parents_,
+            [](msize_t i) -> std::string {
+                return i==mnpos? "npos": std::to_string(i);
+            });
+    bool one_line = m.size()<2u;
+    return o << "(segment_tree (" << (one_line? "": "\n  ") << io::sepval(m.segments_, "\n  ")
+             << (one_line? ") (": ")\n  (")
+             << io::sepval(tstr, ' ') <<  "))";
+}
+
+segment_tree swc_as_segment_tree(const std::vector<swc_record>& swc_records) {
+    if (swc_records.size()<2) {
+        throw "At least two swc records are required to construct a segment tree.";
+    }
+
+    auto point = [&swc_records] (msize_t i) {
+        auto& r = swc_records[i];
+        return mpoint{r.x, r.y, r.z, r.r};
+    };
+    auto tag = [&swc_records] (msize_t i) {
+        return swc_records[i].tag;
+    };
+
+    segment_tree tree;
+    tree.reserve(swc_records.size());
+
+    std::unordered_map<msize_t, msize_t> segmap;
+
+    tree.append(mnpos, point(0), point(1), tag(1));
+    segmap[0] = mnpos;
+    segmap[1] = 0;
+    for (unsigned i=2; i<swc_records.size(); ++i) {
+        msize_t p = segmap[swc_records[i].parent_id];
+        if (p==mnpos) {
+            tree.append(p, point(0), point(i), tag(i));
+        }
+        else {
+            tree.append(p, point(i), tag(i));
+        }
+        segmap[i] = i-1;
+    }
+
+    return tree;
+}
+
+} // namespace arb
+
diff --git a/example/dryrun/branch_cell.hpp b/example/dryrun/branch_cell.hpp
index b4181c868811803391b61d8a9bf650534da68a8e..51d674f243a7dfd1160d11eec9f2b7682b6c9827 100644
--- a/example/dryrun/branch_cell.hpp
+++ b/example/dryrun/branch_cell.hpp
@@ -7,7 +7,9 @@
 
 #include <arbor/common_types.hpp>
 #include <arbor/cable_cell.hpp>
+#include <arbor/morph/segment_tree.hpp>
 
+#include <string>
 #include <sup/json_params.hpp>
 
 // Parameters used to generate the random cell morphologies.
@@ -49,11 +51,12 @@ double interp(const std::array<T,2>& r, unsigned i, unsigned n) {
 }
 
 arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params) {
-    arb::sample_tree tree;
+    arb::segment_tree tree;
 
     // Add soma.
-    double soma_radius = 12.6157/2.0;
-    tree.append(arb::mnpos, {{0,0,0,soma_radius}, 1}); // For area of 500 μm².
+    double srad = 12.6157/2.0; // soma radius
+    int stag = 1; // soma tag
+    tree.append(arb::mnpos, {0, 0,-srad, srad}, {0, 0, srad, srad}, stag); // For area of 500 μm².
 
     std::vector<std::vector<unsigned>> levels;
     levels.push_back({0});
@@ -62,9 +65,10 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     std::mt19937 gen(gid);
     std::uniform_real_distribution<double> dis(0, 1);
 
-    double dend_radius = 0.5; // Diameter of 1 μm for each cable.
+    double drad = 0.5; // Diameter of 1 μm for each dendrite cable.
+    int dtag = 3;      // Dendrite tag.
 
-    double dist_from_soma = soma_radius;
+    double dist_from_soma = srad; // Start dendrite at the edge of the soma.
     for (unsigned i=0; i<params.max_depth; ++i) {
         // Branch prob at this level.
         double bp = interp(params.branch_probs, i, params.max_depth);
@@ -78,14 +82,12 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
             for (unsigned j=0; j<2; ++j) {
                 if (dis(gen)<bp) {
                     auto z = dist_from_soma;
-                    auto p = tree.append(sec, {{0,0,z,dend_radius}, 3});
-                    if (nc>1) {
-                        auto dz = l/nc;
-                        for (unsigned k=1; k<nc; ++k) {
-                            p = tree.append(p, {{0,0,z+k*dz, dend_radius}, 3});
-                        }
+                    auto dz = l/nc;
+                    auto p = sec;
+                    for (unsigned k=0; k<nc; ++k) {
+                        p = tree.append(p, {0,0,z+(k+1)*dz, drad}, dtag);
                     }
-                    sec_ids.push_back(tree.append(p, {{0,0,z+l,dend_radius}, 3}));
+                    sec_ids.push_back(p);
                 }
             }
         }
@@ -100,20 +102,20 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     arb::label_dict d;
 
     using arb::reg::tagged;
-    d.set("soma",      tagged(1));
-    d.set("dendrites", join(tagged(3), tagged(4)));
+    d.set("soma",      tagged(stag));
+    d.set("dend", tagged(dtag));
 
-    arb::cable_cell cell(arb::morphology(tree, true), d);
+    arb::cable_cell cell(arb::morphology(tree), d);
 
     cell.paint("soma", "hh");
-    cell.paint("dendrites", "pas");
+    cell.paint("dend", "pas");
     cell.default_parameters.axial_resistivity = 100; // [Ω·cm]
 
     // Add spike threshold detector at the soma.
     cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.place(arb::mlocation{1, 0.5}, "expsyn");
+    cell.place(arb::mlocation{0, 0.5}, "expsyn");
 
     // Add additional synapses that will not be connected to anything.
     for (unsigned i=1u; i<params.synapses; ++i) {
@@ -121,7 +123,7 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     }
 
     // Make a CV between every sample in the sample tree.
-    cell.default_parameters.discretization = arb::cv_policy_every_sample();
+    cell.default_parameters.discretization = arb::cv_policy_every_segment();
 
     return cell;
 }
diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp
index a2db630be5aef7d7ccee9c9c1764fff3ca070b49..69c95b882a7cc7be017f5b243dbb1d2cd0384add 100644
--- a/example/gap_junctions/gap_junctions.cpp
+++ b/example/gap_junctions/gap_junctions.cpp
@@ -279,12 +279,11 @@ void write_trace_json(const std::vector<arb::trace_vector<double>>& traces, unsi
 
 arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration) {
     // Create the sample tree that defines the morphology.
-    arb::sample_tree tree;
+    arb::segment_tree tree;
     double soma_rad = 22.360679775/2.0; // convert diameter to radius in μm
-    tree.append({{0,0,0,soma_rad}, 1}); // soma is a single sample point
-    double dend_rad = 3./2;
-    tree.append(0, {{0,0,soma_rad,     dend_rad}, 3});  // proximal point of the dendrite
-    tree.append(1, {{0,0,soma_rad+300, dend_rad}, 3});  // distal end of the dendrite
+    tree.append(arb::mnpos, {0,0,0,soma_rad}, {0,0,2*soma_rad,soma_rad}, 1); // soma
+    double dend_rad = 3./2; // μm
+    tree.append(0, {0,0,2*soma_rad, dend_rad}, {0,0,2*soma_rad+300, dend_rad}, 3);  // dendrite
 
     // Create a label dictionary that creates a single region that covers the whole cell.
     arb::label_dict d;
diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp
index 5a0aa4d23f1bf40cdd4d9191162b84d01b8f8e79..66121c07b473e981bc528ce7e01dc7ae73eea2f8 100644
--- a/example/generators/generators.cpp
+++ b/example/generators/generators.cpp
@@ -47,9 +47,9 @@ public:
     //    capacitance: 0.01 F/m² [default]
     //    synapses: 1 * expsyn
     arb::util::unique_any get_cell_description(cell_gid_type gid) const override {
-        arb::sample_tree tree;
+        arb::segment_tree tree;
         double r = 18.8/2.0; // convert 18.8 μm diameter to radius
-        tree.append({{0,0,0,r}, 1});
+        tree.append(arb::mnpos, {0,0,-r,r}, {0,0,r,r}, 1);
 
         arb::label_dict d;
         d.set("soma", arb::reg::tagged(1));
diff --git a/example/lfp/lfp.cpp b/example/lfp/lfp.cpp
index 268fd286f283d8238d67d3e873e2a8fa4b60688f..6a0066b641d4baf7f75d8f5ced9cc583b77143cd 100644
--- a/example/lfp/lfp.cpp
+++ b/example/lfp/lfp.cpp
@@ -7,9 +7,10 @@
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/place_pwlin.hpp>
 #include <arbor/morph/region.hpp>
+#include <arbor/morph/segment_tree.hpp>
+#include <arbor/sampling.hpp>
 #include <arbor/simple_sampler.hpp>
 #include <arbor/simulation.hpp>
-#include <arbor/sampling.hpp>
 #include <arbor/util/any.hpp>
 #include <arbor/util/any_ptr.hpp>
 
@@ -51,11 +52,11 @@ struct lfp_demo_recipe: public arb::recipe {
         return cell_;
     }
 
-    virtual std::vector<arb::event_generator> event_generators(cell_gid_type) const {
+    virtual std::vector<arb::event_generator> event_generators(cell_gid_type) const override {
         return {events_};
     }
 
-    any get_global_properties(arb::cell_kind) const {
+    any get_global_properties(arb::cell_kind) const override {
         arb::cable_cell_global_properties gprop;
         gprop.default_parameters = arb::neuron_parameter_defaults;
         return gprop;
@@ -70,13 +71,11 @@ private:
         using namespace arb;
 
         // Set up morphology as two branches:
+        segment_tree tree;
         // * soma, length 20 μm radius 10 μm, with SWC tag 1.
+        tree.append(arb::mnpos, {0, 0, 10, 10}, {0, 0, -10, 10}, 1);
         // * apical dendrite, length 490 μm, radius 1 μm, with SWC tag 4.
-        sample_tree tree;
-        tree.append({{0, 0, +10, 10}, 1}); // (root point)
-        tree.append({{0, 0, -10, 10}, 1});
-        tree.append(0, {{0, 0,   10, 1}, 4}); // attach to root point.
-        tree.append({{0, 0, 500, 1}, 4});
+        tree.append(arb::mnpos, {0, 0, 10, 1},  {0, 0, 500, 1}, 4);
 
         cell_ = cable_cell(tree);
 
@@ -84,8 +83,8 @@ private:
         cell_.default_parameters.axial_resistivity = 100;     // [Ω·cm]
         cell_.default_parameters.membrane_capacitance = 0.01; // [F/m²]
 
-        // Twenty CVs per branch, except for the soma.
-        cell_.default_parameters.discretization = cv_policy_fixed_per_branch(20, cv_policy_flag::single_root_cv);
+        // Twenty CVs per branch on the dendrites (tag 4).
+        cell_.default_parameters.discretization = cv_policy_fixed_per_branch(20, arb::reg::tagged(4));
 
         // Add pas and hh mechanisms:
         cell_.paint(reg::tagged(1), "hh"); // (default parameters)
@@ -260,10 +259,9 @@ int main(int argc, char** argv) {
     std::vector<std::vector<std::array<double, 3>>> samples;
     for (unsigned branch = 0; branch<cell_morphology.num_branches(); ++branch) {
         samples.push_back({});
-        auto branch_range = cell_morphology.branch_indexes(branch);
-        for (auto i_ptr = branch_range.first; i_ptr!=branch_range.second; ++i_ptr) {
-            arb::msample s = cell_morphology.samples()[*i_ptr];
-            samples.back().push_back(std::array<double, 3>{s.loc.x, s.loc.z, s.loc.radius});
+        for (auto& seg: cell_morphology.branch_segments(branch)) {
+            samples.back().push_back(std::array<double, 3>{seg.prox.x, seg.prox.z, seg.prox.radius});
+            samples.back().push_back(std::array<double, 3>{seg.dist.x, seg.dist.z, seg.dist.radius});
         }
     }
 
diff --git a/example/probe-demo/probe-demo.cpp b/example/probe-demo/probe-demo.cpp
index 895dc659995a2e35ba2f228cd4c7cc036c848578..0598f02888843cbadde9649f529c7efd1e73c68e 100644
--- a/example/probe-demo/probe-demo.cpp
+++ b/example/probe-demo/probe-demo.cpp
@@ -117,8 +117,9 @@ struct cable_recipe: public arb::recipe {
         const double length = 1000; // [µm]
         const double diam = 1; // [µm]
 
-        sample_tree samples({msample{0, 0, 0, 0.5*diam}, msample{length, 0, 0, 0.5*diam}}, {mnpos, 0u});
-        cable_cell c(samples);
+        segment_tree tree;
+        tree.append(arb::mnpos, {0, 0, 0, 0.5*diam}, {length, 0, 0, 0.5*diam}, 1);
+        cable_cell c(tree);
 
         c.paint(reg::all(), "hh"); // HH mechanism over whole cell.
         c.place(mlocation{0, 0.}, i_clamp{0., INFINITY, 1.}); // Inject a 1 nA current indefinitely.
diff --git a/example/ring/branch_cell.hpp b/example/ring/branch_cell.hpp
index b4181c868811803391b61d8a9bf650534da68a8e..51d674f243a7dfd1160d11eec9f2b7682b6c9827 100644
--- a/example/ring/branch_cell.hpp
+++ b/example/ring/branch_cell.hpp
@@ -7,7 +7,9 @@
 
 #include <arbor/common_types.hpp>
 #include <arbor/cable_cell.hpp>
+#include <arbor/morph/segment_tree.hpp>
 
+#include <string>
 #include <sup/json_params.hpp>
 
 // Parameters used to generate the random cell morphologies.
@@ -49,11 +51,12 @@ double interp(const std::array<T,2>& r, unsigned i, unsigned n) {
 }
 
 arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params) {
-    arb::sample_tree tree;
+    arb::segment_tree tree;
 
     // Add soma.
-    double soma_radius = 12.6157/2.0;
-    tree.append(arb::mnpos, {{0,0,0,soma_radius}, 1}); // For area of 500 μm².
+    double srad = 12.6157/2.0; // soma radius
+    int stag = 1; // soma tag
+    tree.append(arb::mnpos, {0, 0,-srad, srad}, {0, 0, srad, srad}, stag); // For area of 500 μm².
 
     std::vector<std::vector<unsigned>> levels;
     levels.push_back({0});
@@ -62,9 +65,10 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     std::mt19937 gen(gid);
     std::uniform_real_distribution<double> dis(0, 1);
 
-    double dend_radius = 0.5; // Diameter of 1 μm for each cable.
+    double drad = 0.5; // Diameter of 1 μm for each dendrite cable.
+    int dtag = 3;      // Dendrite tag.
 
-    double dist_from_soma = soma_radius;
+    double dist_from_soma = srad; // Start dendrite at the edge of the soma.
     for (unsigned i=0; i<params.max_depth; ++i) {
         // Branch prob at this level.
         double bp = interp(params.branch_probs, i, params.max_depth);
@@ -78,14 +82,12 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
             for (unsigned j=0; j<2; ++j) {
                 if (dis(gen)<bp) {
                     auto z = dist_from_soma;
-                    auto p = tree.append(sec, {{0,0,z,dend_radius}, 3});
-                    if (nc>1) {
-                        auto dz = l/nc;
-                        for (unsigned k=1; k<nc; ++k) {
-                            p = tree.append(p, {{0,0,z+k*dz, dend_radius}, 3});
-                        }
+                    auto dz = l/nc;
+                    auto p = sec;
+                    for (unsigned k=0; k<nc; ++k) {
+                        p = tree.append(p, {0,0,z+(k+1)*dz, drad}, dtag);
                     }
-                    sec_ids.push_back(tree.append(p, {{0,0,z+l,dend_radius}, 3}));
+                    sec_ids.push_back(p);
                 }
             }
         }
@@ -100,20 +102,20 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     arb::label_dict d;
 
     using arb::reg::tagged;
-    d.set("soma",      tagged(1));
-    d.set("dendrites", join(tagged(3), tagged(4)));
+    d.set("soma",      tagged(stag));
+    d.set("dend", tagged(dtag));
 
-    arb::cable_cell cell(arb::morphology(tree, true), d);
+    arb::cable_cell cell(arb::morphology(tree), d);
 
     cell.paint("soma", "hh");
-    cell.paint("dendrites", "pas");
+    cell.paint("dend", "pas");
     cell.default_parameters.axial_resistivity = 100; // [Ω·cm]
 
     // Add spike threshold detector at the soma.
     cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.place(arb::mlocation{1, 0.5}, "expsyn");
+    cell.place(arb::mlocation{0, 0.5}, "expsyn");
 
     // Add additional synapses that will not be connected to anything.
     for (unsigned i=1u; i<params.synapses; ++i) {
@@ -121,7 +123,7 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     }
 
     // Make a CV between every sample in the sample tree.
-    cell.default_parameters.discretization = arb::cv_policy_every_sample();
+    cell.default_parameters.discretization = arb::cv_policy_every_segment();
 
     return cell;
 }
diff --git a/example/ring/ring.cpp b/example/ring/ring.cpp
index dc623b8a44ec91aafd12ba0ad535e9e6c8d6741b..0a2137988ad56c9dca71c828e5827af25b5858e9 100644
--- a/example/ring/ring.cpp
+++ b/example/ring/ring.cpp
@@ -43,7 +43,7 @@ struct ring_params {
     std::string name = "default";
     unsigned num_cells = 10;
     double min_delay = 10;
-    double duration = 100;
+    double duration = 200;
     cell_parameters cell;
 };
 
diff --git a/example/single/single.cpp b/example/single/single.cpp
index ac8e67e52aea49a5eed8741136b54c7c7a04ff58..920849e3c8d87336811def28d0752ad1ee7e5dfd 100644
--- a/example/single/single.cpp
+++ b/example/single/single.cpp
@@ -8,7 +8,7 @@
 #include <arbor/load_balance.hpp>
 #include <arbor/cable_cell.hpp>
 #include <arbor/morph/morphology.hpp>
-#include <arbor/morph/sample_tree.hpp>
+#include <arbor/morph/segment_tree.hpp>
 #include <arbor/swcio.hpp>
 #include <arbor/simulation.hpp>
 #include <arbor/simple_sampler.hpp>
@@ -128,7 +128,7 @@ options parse_options(int argc, char** argv) {
             opt.swc_file = swc.value();
         }
         else if (auto nseg = parse<unsigned>(arg, 'n', "cv-per-branch")) {
-            opt.policy = arb::cv_policy_fixed_per_branch(nseg.value(), arb::cv_policy_flag::single_root_cv);
+            opt.policy = arb::cv_policy_fixed_per_branch(nseg.value());
         }
         else {
             usage(argv[0], "[-m|--morphology SWCFILE] [-d|--dt TIME] [-t|--t-end TIME] [-w|--weight WEIGHT] [-n|--cv-per-branch N]");
@@ -144,18 +144,17 @@ options parse_options(int argc, char** argv) {
 // to 0.2 µm.
 
 arb::morphology default_morphology() {
-    arb::sample_tree samples;
+    arb::segment_tree tree;
 
-    auto p = samples.append(arb::msample{{  0.0, 0.0, 0.0, 6.3}, 1});
-    p = samples.append(p, arb::msample{{  6.3, 0.0, 0.0, 0.5}, 3});
-    p = samples.append(p, arb::msample{{206.3, 0.0, 0.0, 0.2}, 3});
+    tree.append(arb::mnpos, { -6.3, 0.0, 0.0, 6.3}, {  6.3, 0.0, 0.0, 6.3}, 1);
+    tree.append(         0, {  6.3, 0.0, 0.0, 0.5}, {206.3, 0.0, 0.0, 0.2}, 3);
 
-    return arb::morphology(std::move(samples));
+    return arb::morphology(tree);
 }
 
 arb::morphology read_swc(const std::string& path) {
     std::ifstream f(path);
     if (!f) throw std::runtime_error("unable to open SWC file: "+path);
 
-    return arb::morphology(arb::swc_as_sample_tree(arb::parse_swc_file(f)));
+    return arb::morphology(arb::swc_as_segment_tree(arb::parse_swc_file(f)));
 }
diff --git a/python/cells.cpp b/python/cells.cpp
index 89fe1fc696267858268f66ab3b10a30f576327b7..30d72bf3a78ef7178d12b8c148782be40655f07f 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -7,6 +7,7 @@
 #include <arbor/morph/label_dict.hpp>
 #include <arbor/morph/locset.hpp>
 #include <arbor/morph/region.hpp>
+#include <arbor/morph/segment_tree.hpp>
 #include <arbor/schedule.hpp>
 #include <arbor/spike_source_cell.hpp>
 #include <arbor/util/any.hpp>
@@ -470,12 +471,11 @@ void register_cells(pybind11::module& m) {
                 return arb::cable_cell(m, labels.dict);
             }), "morphology"_a, "labels"_a)
         .def(pybind11::init(
-            [](const arb::sample_tree& t, const label_dict_proxy& labels) {
+            [](const arb::segment_tree& t, const label_dict_proxy& labels) {
                 return arb::cable_cell(arb::morphology(t), labels.dict);
             }),
-            "morphology"_a, "labels"_a,
-            "Construct with a morphology derived from a sample_tree, with automatic detection of whether\n"
-            "the morphology has a spherical root/soma.")
+            "segment_tree"_a, "labels"_a,
+            "Construct with a morphology derived from a segment tree.")
         .def_property_readonly("num_branches",
             [](const arb::cable_cell& c) {return c.morphology().num_branches();},
             "The number of unbranched cable sections in the morphology.")
@@ -603,9 +603,9 @@ void register_cells(pybind11::module& m) {
             [](arb::cable_cell& c, const char* label) {return c.concrete_region(label).cables();},
             "label"_a, "The cable segments of the cell morphology for a region label.")
         // Discretization control.
-        .def("compartments_on_samples",
-            [](arb::cable_cell& c) {c.default_parameters.discretization = arb::cv_policy_every_sample{};},
-            "Decompose each branch into compartments defined by sample locations.")
+        .def("compartments_on_segments",
+            [](arb::cable_cell& c) {c.default_parameters.discretization = arb::cv_policy_every_segment{};},
+            "Decompose each branch into compartments defined by segments.")
         .def("compartments_length",
             [](arb::cable_cell& c, double len) {
                 c.default_parameters.discretization = arb::cv_policy_max_extent{len};
diff --git a/python/example/ring.py b/python/example/ring.py
index ee58e1e9c64e42d2a28e2454278a7eb27c2e843f..8ddaaf54e31d0efcc2d48f76bb43aee6f31c8d8c 100644
--- a/python/example/ring.py
+++ b/python/example/ring.py
@@ -15,8 +15,8 @@ import matplotlib.pyplot as plt
 def make_cable_cell(gid):
     b = arbor.flat_cell_builder()
 
-    # Soma with radius 6 μm.
-    s  = b.add_sphere(6, "soma")
+    # Soma with radius 6 μm, modelled as cylinder of length 2*radius
+    s  = b.add_cable(length=12, radius=6, name="soma", ncomp=1)
     # Single dendrite of length 100 μm and radius 2 μm attached to soma.
     b1 = b.add_cable(parent=s, length=100, radius=2, name="dend", ncomp=1)
     # Attach two dendrites of length 50 μm to the end of the first dendrite.
diff --git a/python/example/single_cell_builder.py b/python/example/single_cell_builder.py
index 486b328a248e1a593b2117ab30f0325f2bd2f3b8..ad38d656215b51df2a1738efef7548aba265c22d 100644
--- a/python/example/single_cell_builder.py
+++ b/python/example/single_cell_builder.py
@@ -10,29 +10,30 @@ b = arbor.flat_cell_builder()
 # The soma (at the root of the tree) is marked 's', and
 # the end of each branch i is marked 'bi'.
 #
-#               b5
+#               b4
 #              /
 #             /
-#            b2---b4
+#            b1---b3
 #           /
 #          /
-# s-------b1
+# s-------b0
 #          \
 #           \
-#            b3
+#            b2
 
-# Add a spherical soma with radius 6 um
-s  = b.add_sphere(6, "soma")
+# Start with a spherical soma with radius 6 μm,
+# approximated with a cylinder of: length = diameter = 12 μm.
+s  = b.add_cable(length=12, radius=6, name="soma", ncomp=1)
 
 # Add the dendrite cables, labelling those closest to the soma "dendn",
 # and those furthest with "dendx" because we will set different electrical
 # properties for the two regions.
-b1 = b.add_cable(parent=s,  length=100, radius=2, name="dendn", ncomp=100)
+b0 = b.add_cable(parent=s,  length=100, radius=2, name="dendn", ncomp=100)
 # Radius tapers from 2 to 0.5 over the length of the branch.
-b2 = b.add_cable(parent=b1, length= 50, radius=(2,0.5), name="dendn", ncomp=50)
-b3 = b.add_cable(parent=b1, length= 50, radius=1, name="dendn", ncomp=50)
-b4 = b.add_cable(parent=b2, length= 50, radius=1, name="dendx", ncomp=50)
-b5 = b.add_cable(parent=b2, length= 50, radius=1, name="dendx", ncomp=50)
+b1 = b.add_cable(parent=b0, length= 50, radius=(2,0.5), name="dendn", ncomp=50)
+b2 = b.add_cable(parent=b0, length= 50, radius=1, name="dendn", ncomp=50)
+b3 = b.add_cable(parent=b1, length= 50, radius=1, name="dendx", ncomp=50)
+b4 = b.add_cable(parent=b1, length= 50, radius=1, name="dendx", ncomp=50)
 
 # Combine the "dendn" and "dendx" regions into a single "dend" region.
 # The dendrites were labelled as such so that we can set different
@@ -40,7 +41,7 @@ b5 = b.add_cable(parent=b2, length= 50, radius=1, name="dendx", ncomp=50)
 # set other properties on the whole dendrites.
 b.add_label('dend', '(join (region "dendn") (region "dendx"))')
 # Location of stimuli, in the middle of branch 2.
-b.add_label('stim_site', '(location 2 0.5)')
+b.add_label('stim_site', '(location 1 0.5)')
 # The root of the tree (equivalent to '(location 0 0)')
 b.add_label('root', '(root)')
 # The tips of the dendrites (3 locations at b4, b3, b5).
diff --git a/python/example/single_cell_swc.py b/python/example/single_cell_swc.py
index 993f6d987aef0ae5543e08133d346ab93483ed47..1cf996233d140f144fa119a4a79d230dd6cd66ea 100644
--- a/python/example/single_cell_swc.py
+++ b/python/example/single_cell_swc.py
@@ -1,3 +1,13 @@
+# NOTE: deprecating spherical roots changes the behavior of this model.
+# There is no soma, because only the root sample has tag 1, which will be
+# ignored as it is always the proximal end of any cable segment.
+# The fix is to:
+#   - Write an swc interpreter that inserts a cylinder with the
+#     appropriate properties.
+#   - Extend the cable-only descriptions to handle detached cables, to
+#     preserve surface area and correct starting locations of cables
+#     attached to the soma.
+
 import arbor
 from arbor import mechanism as mech
 from arbor import location as loc
@@ -5,19 +15,23 @@ import matplotlib.pyplot as plt
 
 # Load a cell morphology from an swc file.
 # The model has 31 branches, including soma, dendrites and axon.
-tree = arbor.load_swc('../../test/unit/swc/example.swc')
+#tree = arbor.load_swc('../../test/unit/swc/example.swc')
+tree = arbor.load_swc('example.swc')
 
 # Define the regions and locsets in the model.
 defs = {'soma': '(tag 1)',  # soma has tag 1 in swc files.
         'axon': '(tag 2)',  # axon has tag 2 in swc files.
         'dend': '(tag 3)',  # dendrites have tag 3 in swc files.
         'root': '(root)',   # the start of the soma in this morphology is at the root of the cell.
-        'stim_site': '(location 1 0.5)'} # site for the stimulus, in the middle of branch 1.
+        'stim_site': '(location 0 0.5)', # site for the stimulus, in the middle of branch 1.
+        'axon_end': '(restrict (terminal) (region "axon"))'} # end of the axon.
 labels = arbor.label_dict(defs)
 
 # Combine morphology with region and locset definitions to make a cable cell.
 cell = arbor.cable_cell(tree, labels)
 
+print(cell.locations('axon_end'))
+
 # Set initial membrane potential to -55 mV
 cell.set_properties(Vm=-55)
 # Use Nernst to calculate reversal potential for calcium.
@@ -30,8 +44,8 @@ cell.paint('dend', 'pas')
 # Increase resistivity on dendrites.
 cell.paint('dend', rL=500)
 # Attach stimuli that inject 0.8 nA currents for 1 ms, starting at 3 and 8 ms.
-cell.place('stim_site', arbor.iclamp(3, 1, current=0.8))
-cell.place('stim_site', arbor.iclamp(8, 1, 0.8))
+cell.place('stim_site', arbor.iclamp(3, 1, current=0.5))
+cell.place('stim_site', arbor.iclamp(8, 1, current=1))
 # Detect spikes at the soma with a voltage threshold of -10 mV.
 cell.place('root', arbor.spike_detector(-10))
 
@@ -44,8 +58,8 @@ m = arbor.single_cell_model(cell)
 # Attach voltage probes that sample at 50 kHz.
 m.probe('voltage', where='root',  frequency=50000)
 m.probe('voltage', where=loc(2,1),  frequency=50000)
-m.probe('voltage', where=loc(4,1),  frequency=50000)
-m.probe('voltage', where=loc(30,1), frequency=50000)
+m.probe('voltage', where='stim_site',  frequency=50000)
+m.probe('voltage', where='axon_end', frequency=50000)
 
 # Simulate the cell for 15 ms.
 tfinal=15
@@ -68,7 +82,7 @@ legend_labels = ['{}: {}'.format(s.variable, s.location) for s in m.traces]
 ax.legend(legend_labels)
 ax.set(xlabel='time (ms)', ylabel='voltage (mV)', title='swc morphology demo')
 plt.xlim(0,tfinal)
-plt.ylim(-80,50)
+plt.ylim(-80,80)
 ax.grid()
 
 plot_to_file=False
diff --git a/python/flat_cell_builder.cpp b/python/flat_cell_builder.cpp
index 53662f86b25a948062f5548d4d1380cff52c1a99..e201677389c864465f5f7c8e3d6dfb06ee5db345 100644
--- a/python/flat_cell_builder.cpp
+++ b/python/flat_cell_builder.cpp
@@ -4,7 +4,7 @@
 #include <arbor/cable_cell.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
-#include <arbor/morph/sample_tree.hpp>
+#include <arbor/morph/segment_tree.hpp>
 
 #include "conversion.hpp"
 #include "error.hpp"
@@ -15,12 +15,9 @@
 namespace pyarb {
 
 class flat_cell_builder {
-    // The sample tree describing the morphology: constructed on the fly as
-    // the cables/spheres are added with add_cable/add_sphere.
-    arb::sample_tree tree_;
-
-    // The distal sample id of each cable.
-    std::vector<arb::msize_t> cable_distal_id_;
+    // The segment tree describing the morphology, constructed additatively with
+    // segments that are attached to existing segments using add_cable.
+    arb::segment_tree tree_;
 
     // The number of unique region names used to label cables as they
     // are added to the cell.
@@ -28,6 +25,8 @@ class flat_cell_builder {
     // Map from region names to the tag used to identify them.
     std::unordered_map<std::string, int> tag_map_;
 
+    std::vector<arb::msize_t> cable_distal_segs_;
+
     arb::label_dict dict_;
 
     // The morphology is cached, and only updated on request when it is out of date.
@@ -35,26 +34,20 @@ class flat_cell_builder {
     mutable arb::morphology morpho_;
     mutable std::mutex mutex_;
 
-    // Set on construction and unchanged thereafter.
-    // Indicates whether 
-    bool spherical_ = false;
-
 public:
 
     flat_cell_builder() = default;
 
-    arb::msize_t add_sphere(double radius, const char* name) {
-        cached_morpho_ = false;
-        spherical_ = true;
-        if (size()) {
-            throw pyarb_error("Add soma to non-empty cell.");
-        }
-        tree_.append({{0,0,0,radius}, get_tag(name)});
-        cable_distal_id_.push_back(0);
-        return 0;
+
+    // Add a new cable that is attached to the last cable added to the cell.
+    // Returns the id of the new cable.
+    arb::msize_t add_cable(double len,
+                           double r1, double r2, const char* region, int ncomp)
+    {
+        return add_cable(size()? size()-1: arb::mnpos, len, r1, r2, region, ncomp);
     }
 
-    // Add a new branch that is attached to parent.
+    // Add a new cable that is attached to the parent cable.
     // Returns the id of the new cable.
     arb::msize_t add_cable(arb::msize_t parent, double len,
                            double r1, double r2, const char* region, int ncomp)
@@ -71,42 +64,23 @@ public:
         int tag = get_tag(region);
         const bool at_root = parent==mnpos;
 
-        // Can't attach a cable to the root on a cell with spherical soma.
-        if (at_root && spherical_) {
-            throw pyarb_error("Invalid parent id.");
-        }
         // Parent id must be in the range [0, size())
         if (!at_root && parent>=size()) {
             throw pyarb_error("Invalid parent id.");
         }
 
-        // Calculating the sample that is the parent of this branch is
-        // neccesarily complicated to handle cable segments that are attached
-        // to the root.
-        arb::msize_t p = at_root? (size()? 0: mnpos): cable_distal_id_[parent];
-
-        double z = at_root? 0:                      // attach to root of non-spherical cell
-                   spherical_&&!parent? soma_rad(): // attach to spherical root
-                   tree_.samples()[p].loc.z;        // attach to end of a cable
-
-        // Only add a first point at the very beginning of the cable if
-        // the cable is not attached to another
-        const bool add_first_point = p==arb::mnpos      // attached to the "root"
-                                  || (!p && spherical_) // attached to a spherical root
-                                  || (r1!=tree_.samples()[p].loc.radius);
-                                                        // proximal radius does not match r1
-        if (add_first_point) {
-            p = tree_.append(p, {{0,0,z,r1}, tag});
-        }
-        if (ncomp>1) {
-            double dz = len/ncomp;
-            double dr = (r2-r1)/ncomp;
-            for (auto i=1; i<ncomp; ++i) {
-                p = tree_.append(p, {{0,0,z+i*dz, r1+i*dr}, tag});
-            }
+        arb::msize_t p = at_root? mnpos: cable_distal_segs_[parent];
+
+        double z = at_root? 0:                      // attach to root
+                   tree_.segments()[p].dist.z;      // attach to end of a cable
+
+        double dz = len/ncomp;
+        double dr = (r2-r1)/ncomp;
+        for (auto i=0; i<ncomp; ++i) {
+            p = tree_.append(p, {0,0,z+i*dz, r1+i*dr}, {0,0,z+(i+1)*dz, r1+(i+1)*dr}, tag);
         }
-        p = tree_.append(p, {{0,0,z+len,r2}, tag});
-        cable_distal_id_.push_back(p);
+
+        cable_distal_segs_.push_back(p);
 
         return size()-1;
     }
@@ -151,7 +125,7 @@ public:
         }
     }
 
-    const arb::sample_tree& samples() const {
+    const arb::segment_tree& segments() const {
         return tree_;
     }
 
@@ -170,16 +144,15 @@ public:
     const arb::morphology& morphology() const {
         const std::lock_guard<std::mutex> guard(mutex_);
         if (!cached_morpho_) {
-            morpho_ = arb::morphology(tree_, spherical_);
+            morpho_ = arb::morphology(tree_);
             cached_morpho_ = true;
         }
         return morpho_;
     }
 
     arb::cable_cell build() const {
-        // Make cable_cell from sample tree and dictionary.
         auto c = arb::cable_cell(morphology(), dict_);
-        c.default_parameters.discretization = arb::cv_policy_every_sample{};
+        c.default_parameters.discretization = arb::cv_policy_every_segment{};
         return c;
     }
 
@@ -217,14 +190,9 @@ public:
         }
     }
 
-    // Only valid if called on a non-empty tree with spherical soma.
-    double soma_rad() const {
-        return tree_.samples()[0].loc.radius;
-    }
-
-    // The number of cable segements (plus one optional soma) used to construct the cell.
+    // The number of cable segements used in the cell.
     std::size_t size() const {
-        return cable_distal_id_.size();
+        return tree_.size();
     }
 };
 
@@ -234,8 +202,24 @@ void register_flat_builder(pybind11::module& m) {
     pybind11::class_<flat_cell_builder> builder(m, "flat_cell_builder");
     builder
         .def(pybind11::init<>())
-        .def("add_sphere", &flat_cell_builder::add_sphere,
-            "radius"_a, "name"_a)
+        .def("add_cable",
+                [](flat_cell_builder& b, double len, pybind11::object rad, const char* name, int ncomp) {
+                    using pybind11::isinstance;
+                    using pybind11::cast;
+                    if (auto radius = try_cast<double>(rad) ) {
+                        return b.add_cable(len, *radius, *radius, name, ncomp);
+                    }
+
+                    if (auto radii = try_cast<std::pair<double, double>>(rad)) {
+                        return b.add_cable(len, radii->first, radii->second, name, ncomp);
+                    }
+                    else {
+                        throw pyarb_error(
+                            "Radius parameter is not a scalar (constant branch radius) or "
+                            "a tuple (radius at proximal and distal ends respectively).");
+                    }
+                },
+            "length"_a, "radius"_a, "name"_a, "ncomp"_a=1)
         .def("add_cable",
                 [](flat_cell_builder& b, arb::msize_t p, double len, pybind11::object rad, const char* name, int ncomp) {
                     using pybind11::isinstance;
@@ -256,8 +240,8 @@ void register_flat_builder(pybind11::module& m) {
             "parent"_a, "length"_a, "radius"_a, "name"_a, "ncomp"_a=1)
         .def("add_label", &flat_cell_builder::add_label,
             "name"_a, "description"_a)
-        .def_property_readonly("samples",
-            [](const flat_cell_builder& b) { return b.samples(); })
+        .def_property_readonly("segments",
+            [](const flat_cell_builder& b) { return b.segments(); })
         .def_property_readonly("labels",
             [](const flat_cell_builder& b) { return b.labels(); })
         .def_property_readonly("morphology",
diff --git a/python/morph_parse.cpp b/python/morph_parse.cpp
index ba35b9a0c4a3bffc56a76be49745b28a0cdac8ab..2df63fc59bde458b682b8f923fa2a5194b69db47 100644
--- a/python/morph_parse.cpp
+++ b/python/morph_parse.cpp
@@ -227,8 +227,6 @@ std::unordered_multimap<std::string, evaluator> eval_map {
                             "'location' with 2 arguments: (branch_id:integer position:real)")},
     {"terminal", make_call<>(arb::ls::terminal,
                             "'terminal' with 0 arguments")},
-    {"sample",  make_call<int>(arb::ls::sample,
-                            "'sample' with 1 argument: (sample_id:integer)")},
     {"distal",  make_call<arb::region>(arb::ls::most_distal,
                             "'distal' with 1 argument: (reg:region)")},
     {"proximal",make_call<arb::region>(arb::ls::most_proximal,
diff --git a/python/morphology.cpp b/python/morphology.cpp
index ea2ea8101b9ea64c632782f16444dab0fb5cd1fd..ec634edd20673fe187320fc428bd01934f6f5d20 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -5,7 +5,7 @@
 
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
-#include <arbor/morph/sample_tree.hpp>
+#include <arbor/morph/segment_tree.hpp>
 #include <arbor/swcio.hpp>
 
 #include "error.hpp"
@@ -17,7 +17,7 @@ void register_morphology(pybind11::module& m) {
     using namespace pybind11::literals;
 
     //
-    //  primitives: points, samples, locations, cables... etc.
+    //  primitives: points, segments, locations, cables... etc.
     //
 
     m.attr("mnpos") = arb::mnpos;
@@ -45,37 +45,32 @@ void register_morphology(pybind11::module& m) {
         .def("__repr__",
             [](arb::mlocation l) { return util::pprintf("(location {} {})", l.branch, l.pos); });
 
-    // arb::msample
-    pybind11::class_<arb::msample> msample(m, "sample");
-    msample
+    // arb::mpoint
+    pybind11::class_<arb::mpoint> mpoint(m, "mpoint");
+    mpoint
         .def(pybind11::init(
-            [](arb::mpoint loc, int tag){
-                return arb::msample{loc, tag};}),
-            "location"_a, "tag"_a)
-        .def(pybind11::init(
-            [](double x, double y, double z, double r, int tag){
-                return arb::msample{{x,y,z,r}, tag};}),
-            "x"_a, "y"_a, "z"_a, "radius"_a, "tag"_a,
-            "spatial values {x, y, z, radius} are in μm.")
-        .def_readonly("x", &arb::msample::loc)
-        .def_property_readonly("x",
-                [](const arb::msample& s) {return s.loc.x;},
-                "X coordinate [μm].")
-        .def_property_readonly("y",
-                [](const arb::msample& s) {return s.loc.y;},
-                "Y coordinate [μm].")
-        .def_property_readonly("z",
-                [](const arb::msample& s) {return s.loc.z;},
-                "Z coordinate [μm].")
-        .def_readonly("tag", &arb::msample::tag,
-            "Property tag of sample. "
-            "If loaded from standard SWC file the following tags are used: soma=1, axon=2, dendrite=3, apical dendrite=4, however arbitrary tags can be used.")
+                [](double x, double y, double z, double r) {
+                    return arb::mpoint{x,y,z,r};
+                }),
+                "x"_a, "y"_a, "z"_a, "radius"_a, "All values in μm.")
+        .def_readonly("x", &arb::mpoint::x, "X coordinate [μm].")
+        .def_readonly("y", &arb::mpoint::y, "Y coordinate [μm].")
+        .def_readonly("z", &arb::mpoint::z, "Z coordinate [μm].")
+        .def_readonly("radius", &arb::mpoint::radius,
+            "Radius of cable at sample location centered at coordinates [μm].")
         .def("__str__",
-            [](const arb::msample& s) {
-                return util::pprintf("{}", s);})
+            [](const arb::mpoint& p) {
+                return util::pprintf("<arbor.mpoint: x {}, y {}, z {}, radius {}>", p.x, p.y, p.z, p.radius);
+            })
         .def("__repr__",
-            [](const arb::msample& s) {
-                return util::pprintf("{}", s);});
+            [](const arb::mpoint& p) {return util::pprintf("{}>", p);});
+
+    // arb::msegment
+    pybind11::class_<arb::msegment> msegment(m, "msegment");
+    msegment
+        .def_readonly("prox", &arb::msegment::prox, "the location and radius of the proximal end.")
+        .def_readonly("dist", &arb::msegment::dist, "the location and radius of the distal end.")
+        .def_readonly("tag", &arb::msegment::tag, "tag meta-data.");
 
     // arb::mcable
     pybind11::class_<arb::mcable> cable(m, "cable");
@@ -99,47 +94,46 @@ void register_morphology(pybind11::module& m) {
         .def("__repr__", [](const arb::mcable& c) { return util::pprintf("{}", c); });
 
     //
-    // Higher-level data structures (sample_tree, morphology)
+    // Higher-level data structures (segment_tree, morphology)
     //
 
-    // arb::sample_tree
-    pybind11::class_<arb::sample_tree> sample_tree(m, "sample_tree");
-    sample_tree
+    // arb::segment_tree
+    pybind11::class_<arb::segment_tree> segment_tree(m, "segment_tree");
+    segment_tree
         // constructors
         .def(pybind11::init<>())
         // modifiers
-        .def("reserve", &arb::sample_tree::reserve)
-        .def("append", [](arb::sample_tree& t, arb::msample s){return t.append(s);},
-                "Append a sample whose parent is the last sample added to the tree.")
-        .def("append", [](arb::sample_tree& t, arb::msize_t p, arb::msample s){return t.append(p, s);},
-                "parent"_a, "sample"_a,
-                "Append a sample.")
+        .def("reserve", &arb::segment_tree::reserve)
+        .def("append", [](arb::segment_tree& t, arb::msize_t parent, arb::mpoint prox, arb::mpoint dist, int tag) {
+                            return t.append(parent, prox, dist, tag);
+                          },
+                "parent"_a, "prox"_a, "dist"_a, "tag"_a,
+                "Append a segment to the tree.")
+        .def("append", [](arb::segment_tree& t, arb::msize_t parent, arb::mpoint dist, int tag) {
+                            return t.append(parent, dist, tag);
+                          },
+                "parent"_a, "dist"_a, "tag"_a,
+                "Append a segment to the tree.")
         .def("append",
-                [](arb::sample_tree& t, double x, double y, double z, double radius, int tag) {
-                    return t.append(arb::msample{{x,y,z,radius}, tag});
-                },
-                "x"_a, "y"_a, "z"_a, "radius"_a, "tag"_a,
-                "Append a sample whose parent is the last sample added to the tree.")
-        .def("append",
-                [](arb::sample_tree& t, arb::msize_t p, double x, double y, double z, double radius, int tag) {
-                    return t.append(p, arb::msample{{x,y,z,radius}, tag});
+                [](arb::segment_tree& t, arb::msize_t p, double x, double y, double z, double radius, int tag) {
+                    return t.append(p, arb::mpoint{x,y,z,radius}, tag);
                 },
                 "parent"_a, "x"_a, "y"_a, "z"_a, "radius"_a, "tag"_a,
-                "Append a sample.")
+                "Append a segment to the tree, using the distal location of the parent segment as the proximal end.")
         // properties
-        .def_property_readonly("empty", [](const arb::sample_tree& st){return st.empty();},
+        .def_property_readonly("empty", [](const arb::segment_tree& st){return st.empty();},
                 "Indicates whether the sample tree is empty (i.e. whether it has size 0)")
-        .def_property_readonly("size", [](const arb::sample_tree& st){return st.size();},
+        .def_property_readonly("size", [](const arb::segment_tree& st){return st.size();},
                 "The number of samples in the sample tree.")
-        .def_property_readonly("parents", [](const arb::sample_tree& st){return st.parents();},
+        .def_property_readonly("parents", [](const arb::segment_tree& st){return st.parents();},
                 "A list with the parent index of each sample.")
-        .def_property_readonly("samples", [](const arb::sample_tree& st){return st.samples();},
+        .def_property_readonly("segments", [](const arb::segment_tree& st){return st.segments();},
                 "A list of the samples.")
-        .def("__str__", [](const arb::sample_tree& s) {
-                return util::pprintf("<arbor.sample_tree:\n{}>", s);});
+        .def("__str__", [](const arb::segment_tree& s) {
+                return util::pprintf("<arbor.segment_tree:\n{}>", s);});
 
-    // Function that creates a sample_tree from an swc file.
-    // Wraps calls to C++ functions arb::parse_swc_file() and arb::swc_as_sample_tree().
+    // Function that creates a segment_tree from an swc file.
+    // Wraps calls to C++ functions arb::parse_swc_file() and arb::swc_as_segment_tree().
     m.def("load_swc",
         [](std::string fname) {
             std::ifstream fid{fname};
@@ -149,7 +143,7 @@ void register_morphology(pybind11::module& m) {
             try {
                 auto records = arb::parse_swc_file(fid);
                 arb::swc_canonicalize(records);
-                return arb::swc_as_sample_tree(records);
+                return arb::swc_as_segment_tree(records);
             }
             catch (arb::swc_error& e) {
                 // Try to produce helpful error messages for SWC parsing errors.
@@ -158,7 +152,7 @@ void register_morphology(pybind11::module& m) {
                                   e.line_number, fname, e.what()));
             }
         },
-        "Load an swc file and convert to a sample_tree.");
+        "Load an swc file and convert to a segment_tree.");
 
     // arb::morphology
 
@@ -166,43 +160,26 @@ void register_morphology(pybind11::module& m) {
     morph
         // constructors
         .def(pybind11::init(
-                [](arb::sample_tree t){
+                [](arb::segment_tree t){
                     return arb::morphology(std::move(t));
                 }))
-        .def(pybind11::init(
-                [](arb::sample_tree t, bool spherical_root) {
-                    return arb::morphology(std::move(t), spherical_root);
-                }), "sample_tree"_a, "spherical_root"_a)
         // morphology's interface is read-only by design, so most of it can
         // be implemented as read-only properties.
         .def_property_readonly("empty",
                 [](const arb::morphology& m){return m.empty();},
                 "Whether the morphology is empty.")
-        .def_property_readonly("spherical_root",
-                [](const arb::morphology& m){return m.spherical_root();},
-                "Whether the root of the morphology is spherical.")
         .def_property_readonly("num_branches",
                 [](const arb::morphology& m){return m.num_branches();},
                 "The number of branches in the morphology.")
-        .def_property_readonly("num_samples",
-                [](const arb::morphology& m){return m.num_samples();},
-                "The number of samples in the morphology.")
-        .def_property_readonly("samples",
-                [](const arb::morphology& m){return m.samples();},
-                "All of the samples in the morphology.")
-        .def_property_readonly("sample_parents",
-                [](const arb::morphology& m){return m.sample_parents();},
-                "The parent indexes of each sample.")
         .def("branch_parent", &arb::morphology::branch_parent,
                 "i"_a, "The parent branch of branch i.")
         .def("branch_children", &arb::morphology::branch_children,
                 "i"_a, "The child branches of branch i.")
-        .def("branch_indexes",
+        .def("branch_segments",
                 [](const arb::morphology& m, arb::msize_t i) {
-                    auto p = m.branch_indexes(i);
-                    return std::vector<arb::msize_t>(p.first, p.second);
+                    return m.branch_segments(i);
                 },
-                "i"_a, "Range of indexes into the sample points in branch i.")
+                "i"_a, "A list of the segments in branch i, ordered from proximal to distal ends of the branch.")
         .def("__str__",
                 [](const arb::morphology& m) {
                     return util::pprintf("<arbor.morphology:\n{}>", m);
diff --git a/test/common_cells.cpp b/test/common_cells.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..43cbcc92da557c80afc1a31ec0705fff6c744e02
--- /dev/null
+++ b/test/common_cells.cpp
@@ -0,0 +1,262 @@
+#include "common_cells.hpp"
+
+namespace arb {
+
+// Generate a segment tree from a sequence of points and parent index.
+arb::segment_tree segments_from_points(
+        std::vector<arb::mpoint> points,
+        std::vector<arb::msize_t> parents,
+        std::vector<int> tags)
+{
+    using arb::mnpos;
+    using arb::msize_t;
+
+    const auto np = points.size();
+
+    // Sanity check the input.
+    if (parents.size()!=np || np<2) {
+        throw std::runtime_error("segments_from_points: every point must have a parent.");
+    }
+    if (tags.size() && tags.size()!=np) {
+        throw std::runtime_error("segments_from_points: every point must have a tag.");
+    }
+    auto tag = [&tags](msize_t i) {return tags.size()? tags[i]: 1;};
+    arb::segment_tree tree;
+    std::unordered_map<msize_t, msize_t> segmap;
+
+    tree.append(mnpos, points[0], points[1], tag(1));
+    segmap[0] = mnpos;
+    segmap[1] = 0;
+    for (unsigned i=2; i<np; ++i) {
+        auto p = segmap[parents[i]];
+        if (p==mnpos) {
+            tree.append(p, points[0], points[i], tag(i));
+        }
+        else {
+            tree.append(p, points[i], tag(i));
+        }
+        segmap[i] = i-1;
+    }
+
+    return tree;
+}
+
+
+int soma_cell_builder::get_tag(const std::string& name) {
+    auto it = tag_map.find(name);
+    // If the name is not in the map, make a unique tag.
+    // Tags start from 1.
+    if (it==tag_map.end()) {
+        tag_map[name] = ++tag_count;
+        return tag_count;
+    }
+    return it->second;
+}
+
+soma_cell_builder::soma_cell_builder(double r) {
+    auto tag = get_tag("soma");
+    tree.append(arb::mnpos, {0,0,0,r}, {0,0,2*r,r}, tag);
+    cv_boundaries.push_back({0, 1.});
+    branch_distal_id.push_back(0);
+    n_soma_children = 0;
+}
+
+mlocation soma_cell_builder::location(mlocation loc) const {
+    if (loc.branch>=branch_distal_id.size()) {
+        throw cable_cell_error("location not on cable_cell.");
+    }
+    if (n_soma_children==0) {
+        return loc;
+    }
+    if (n_soma_children==1) {
+        if (loc.branch<2) {
+            double soma_len  = tree.segments()[branch_distal_id[0]].dist.z;
+            double total_len = tree.segments()[branch_distal_id[1]].dist.z;
+            // relative position of the end of the soma on the first branch
+            double split = soma_len/total_len;
+            double pos = loc.branch==0?
+                split*loc.pos:
+                split + (1-split)*loc.pos;
+            return {0, pos};
+        }
+        return {loc.branch-1, loc.pos};
+    }
+    return loc;
+}
+
+mcable soma_cell_builder::cable(mcable cab) const {
+    if (cab.branch>=branch_distal_id.size()) {
+        throw cable_cell_error("cable not on cable_cell.");
+    }
+    auto beg = location({cab.branch, cab.prox_pos});
+    auto end = location({cab.branch, cab.dist_pos});
+    return {beg.branch, beg.pos, end.pos};
+}
+
+// Add a new branch that is attached to parent_branch.
+// Returns the id of the new branch.
+msize_t soma_cell_builder::add_branch(
+        msize_t parent_branch,
+        double len, double r1, double r2, int ncomp,
+        const std::string& region)
+{
+    // Get tag id of region (add a new tag if region does not already exist).
+    int tag = get_tag(region);
+
+    msize_t p = branch_distal_id[parent_branch];
+    auto& ploc =  tree.segments()[p].dist;
+
+    double z = ploc.z;
+    if (ploc.radius!=r1) {
+        p = tree.append(p, {0,0,z,r1}, tag);
+    }
+    if (ncomp>1) {
+        double dz = len/ncomp;
+        double dr = (r2-r1)/ncomp;
+        for (auto i=1; i<ncomp; ++i) {
+            p = tree.append(p, {0,0,z+i*dz, r1+i*dr}, tag);
+        }
+    }
+    p = tree.append(p, {0,0,z+len,r2}, tag);
+    branch_distal_id.push_back(p);
+
+    msize_t bid = branch_distal_id.size()-1;
+    for (int i = 0; i<ncomp; ++i) {
+        cv_boundaries.push_back(mlocation{bid, (2*i+1.)/(2.*ncomp)});
+    }
+
+    if (!parent_branch) ++n_soma_children;
+
+    return bid;
+}
+
+cable_cell soma_cell_builder::make_cell() const {
+    // Test that a valid tree was generated, that is, every branch has
+    // either 0 children, or at least 2 children.
+    for (auto i: branch_distal_id) {
+        // skip soma
+        if (i<2) continue;
+        if (!tree.is_fork(i) && !tree.is_terminal(i)) {
+            throw cable_cell_error(
+                "attempt to construct a cable_cell from a soma_cell_builder "
+                "where a non soma branch has only one child branch.");
+        }
+    }
+
+    // Make label dictionary with one entry for each tag.
+    label_dict dict;
+    for (auto& tag: tag_map) {
+        dict.set(tag.first, reg::tagged(tag.second));
+    }
+
+    // Make cable_cell from sample tree and dictionary.
+    cable_cell c(tree, dict);
+    auto boundaries = cv_boundaries;
+    for (auto& b: boundaries) {
+        b = location(b);
+    }
+    c.default_parameters.discretization = cv_policy_explicit(boundaries);
+    return c;
+}
+
+/*
+ * Create cell with just a soma:
+ *
+ * Soma:
+ *    diameter: 18.8 µm
+ *    mechanisms: HH (default params)
+ *    bulk resistivitiy: 100 Ω·cm [default]
+ *    capacitance: 0.01 F/m² [default]
+ *
+ * Stimuli:
+ *    soma centre, t=[10 ms, 110 ms), 0.1 nA
+ */
+
+cable_cell make_cell_soma_only(bool with_stim) {
+    soma_cell_builder builder(18.8/2.0);
+
+    auto c = builder.make_cell();
+    c.paint("soma", "hh");
+    if (with_stim) {
+        c.place(builder.location({0,0.5}), i_clamp{10., 100., 0.1});
+    }
+
+    return c;
+}
+
+/*
+ * Create cell with a soma and unbranched dendrite:
+ *
+ * Common properties:
+ *    bulk resistivity: 100 Ω·cm [default]
+ *    capacitance: 0.01 F/m² [default]
+ *
+ * Soma:
+ *    mechanisms: HH (default params)
+ *    diameter: 12.6157 µm
+ *
+ * Dendrite:
+ *    mechanisms: passive (default params)
+ *    diameter: 1 µm
+ *    length: 200 µm
+ *    compartments: 4
+ *
+ * Stimulus:
+ *    end of dendrite, t=[5 ms, 85 ms), 0.3 nA
+ */
+
+cable_cell make_cell_ball_and_stick(bool with_stim) {
+    soma_cell_builder builder(12.6157/2.0);
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 4, "dend");
+
+    auto c = builder.make_cell();
+    c.paint("soma", "hh");
+    c.paint("dend", "pas");
+    if (with_stim) {
+        c.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
+    }
+
+    return c;
+}
+
+/*
+ * Create cell with a soma and three-branch dendrite with single branch point:
+ *
+ * O----======
+ *
+ * Common properties:
+ *    bulk resistivity: 100 Ω·cm [default]
+ *    capacitance: 0.01 F/m² [default]
+ *
+ * Soma:
+ *    mechanisms: HH (default params)
+ *    diameter: 12.6157 µm
+ *
+ * Dendrites:
+ *    mechanisms: passive (default params)
+ *    diameter: 1 µm
+ *    length: 100 µm
+ *    compartments: 4
+ *
+ * Stimulus:
+ *    end of first terminal branch, t=[5 ms, 85 ms), 0.45 nA
+ *    end of second terminal branch, t=[40 ms, 50 ms), -0.2 nA
+ */
+
+cable_cell make_cell_ball_and_3stick(bool with_stim) {
+    soma_cell_builder builder(12.6157/2.0);
+    builder.add_branch(0, 100, 0.5, 0.5, 4, "dend");
+    builder.add_branch(1, 100, 0.5, 0.5, 4, "dend");
+    builder.add_branch(1, 100, 0.5, 0.5, 4, "dend");
+
+    auto c = builder.make_cell();
+    c.paint("soma", "hh");
+    c.paint("dend", "pas");
+    if (with_stim) {
+        c.place(builder.location({2,1}), i_clamp{5.,  80., 0.45});
+        c.place(builder.location({3,1}), i_clamp{40., 10.,-0.2});
+    }
+
+    return c;
+}
+} // namespace arb
diff --git a/test/common_cells.hpp b/test/common_cells.hpp
index 83d972ae9ad7f953df0a4f14dc662196488ceebe..bad660a9b0d9434b4e4d89dd97e5637e9026c0a4 100644
--- a/test/common_cells.hpp
+++ b/test/common_cells.hpp
@@ -7,86 +7,35 @@
 
 namespace arb {
 
+// Generate a segment tree from a sequence of points and parent index.
+arb::segment_tree segments_from_points(std::vector<arb::mpoint> points,
+                                       std::vector<arb::msize_t> parents,
+                                       std::vector<int> tags={});
+
 class soma_cell_builder {
-    double soma_rad;
-    sample_tree tree;
+    segment_tree tree;
     std::vector<msize_t> branch_distal_id;
     std::unordered_map<std::string, int> tag_map;
-    locset cv_boundaries = mlocation{0, 1.};
+    mlocation_list cv_boundaries;
     int tag_count = 0;
+    int n_soma_children = 0;
 
     // Get tag id of region.
     // Add a new tag if region with that name has not already had a tag associated with it.
-    int get_tag(const std::string& name) {
-        auto it = tag_map.find(name);
-        // If the name is not in the map, make a unique tag.
-        // Tags start from 1.
-        if (it==tag_map.end()) {
-            tag_map[name] = ++tag_count;
-            return tag_count;
-        }
-        return it->second;
-    }
+    int get_tag(const std::string& name);
 
 public:
-    soma_cell_builder(double r): soma_rad(r) {
-        tree.append({{0,0,0,r}, get_tag("soma")});
-        branch_distal_id.push_back(0);
-    }
+    soma_cell_builder(double r);
 
     // Add a new branch that is attached to parent_branch.
     // Returns the id of the new branch.
     msize_t add_branch(msize_t parent_branch, double len, double r1, double r2, int ncomp,
-                    const std::string& region)
-    {
-        // Get tag id of region (add a new tag if region does not already exist).
-        int tag = get_tag(region);
-
-        msize_t p = branch_distal_id[parent_branch];
-        double z = parent_branch? tree.samples()[p].loc.z: soma_rad;
-
-        p = tree.append(p, {{0,0,z,r1}, tag});
-        if (ncomp>1) {
-            double dz = len/ncomp;
-            double dr = (r2-r1)/ncomp;
-            for (auto i=1; i<ncomp; ++i) {
-                p = tree.append(p, {{0,0,z+i*dz, r1+i*dr}, tag});
-            }
-        }
-        p = tree.append(p, {{0,0,z+len,r2}, tag});
-        branch_distal_id.push_back(p);
-
-        msize_t bid = branch_distal_id.size()-1;
-        for (int i = 0; i<ncomp; ++i) {
-            cv_boundaries = sum(cv_boundaries,  mlocation{bid, (2*i+1.)/(2.*ncomp)});
-        }
-        return bid;
-    }
-
-    cable_cell make_cell() const {
-        // Test that a valid tree was generated, that is, every branch has
-        // either 0 children, or at least 2 children.
-        for (auto i: branch_distal_id) {
-            if (i==0) continue;
-            auto prop = tree.properties()[i];
-            if (!is_fork(prop) && !is_terminal(prop)) {
-                throw cable_cell_error(
-                    "attempt to construct a cable_cell from a soma_cell_builder "
-                    "where a branch has only one child branch.");
-            }
-        }
-
-        // Make label dictionary with one entry for each tag.
-        label_dict dict;
-        for (auto& tag: tag_map) {
-            dict.set(tag.first, reg::tagged(tag.second));
-        }
-
-        // Make cable_cell from sample tree and dictionary.
-        cable_cell c(tree, dict);
-        c.default_parameters.discretization = cv_policy_explicit(cv_boundaries);
-        return c;
-    }
+                    const std::string& region);
+
+    mlocation location(mlocation) const;
+    mcable cable(mcable) const;
+
+    cable_cell make_cell() const;
 };
 
 /*
@@ -102,17 +51,7 @@ public:
  *    soma centre, t=[10 ms, 110 ms), 0.1 nA
  */
 
-inline cable_cell make_cell_soma_only(bool with_stim = true) {
-    soma_cell_builder builder(18.8/2.0);
-
-    auto c = builder.make_cell();
-    c.paint("soma", "hh");
-    if (with_stim) {
-        c.place(mlocation{0,0.5}, i_clamp{10., 100., 0.1});
-    }
-
-    return c;
-}
+cable_cell make_cell_soma_only(bool with_stim = true);
 
 /*
  * Create cell with a soma and unbranched dendrite:
@@ -135,19 +74,7 @@ inline cable_cell make_cell_soma_only(bool with_stim = true) {
  *    end of dendrite, t=[5 ms, 85 ms), 0.3 nA
  */
 
-inline cable_cell make_cell_ball_and_stick(bool with_stim = true) {
-    soma_cell_builder builder(12.6157/2.0);
-    builder.add_branch(0, 200, 1.0/2, 1.0/2, 4, "dend");
-
-    auto c = builder.make_cell();
-    c.paint("soma", "hh");
-    c.paint("dend", "pas");
-    if (with_stim) {
-        c.place(mlocation{1,1}, i_clamp{5, 80, 0.3});
-    }
-
-    return c;
-}
+cable_cell make_cell_ball_and_stick(bool with_stim = true);
 
 /*
  * Create cell with a soma and three-branch dendrite with single branch point:
@@ -173,21 +100,6 @@ inline cable_cell make_cell_ball_and_stick(bool with_stim = true) {
  *    end of second terminal branch, t=[40 ms, 50 ms), -0.2 nA
  */
 
-inline cable_cell make_cell_ball_and_3stick(bool with_stim = true) {
-    soma_cell_builder builder(12.6157/2.0);
-    builder.add_branch(0, 100, 0.5, 0.5, 4, "dend");
-    builder.add_branch(1, 100, 0.5, 0.5, 4, "dend");
-    builder.add_branch(1, 100, 0.5, 0.5, 4, "dend");
-
-    auto c = builder.make_cell();
-    c.paint("soma", "hh");
-    c.paint("dend", "pas");
-    if (with_stim) {
-        c.place(mlocation{2,1}, i_clamp{5.,  80., 0.45});
-        c.place(mlocation{3,1}, i_clamp{40., 10.,-0.2});
-    }
-
-    return c;
-}
+cable_cell make_cell_ball_and_3stick(bool with_stim = true);
 
 } // namespace arb
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index c13aa40bc82116bf3c43122b6cc00068e6acad18..ee680570c7fef37f59ead714d18047c889efd6bf 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -68,7 +68,7 @@ endforeach()
 # set(proto_mechanisms pas hh expsyn exp2syn test_kin1 test_kinlva test_ca)
 # set(mech_proto_dir "${CMAKE_CURRENT_BINARY_DIR}/mech_proto")
 # file(MAKE_DIRECTORY "${mech_proto_dir}")
-# 
+#
 # build_modules(
 #     ${proto_mechanisms}
 #     SOURCE_DIR "${PROJECT_SOURCE_DIR}/mechanisms/mod"
@@ -82,6 +82,7 @@ endforeach()
 # Unit test sources
 
 set(unit_sources
+    ../common_cells.cpp
     test_algorithms.cpp
     test_any.cpp
     test_any_visitor.cpp
@@ -138,10 +139,11 @@ set(unit_sources
     test_range.cpp
     test_ratelem.cpp
     test_schedule.cpp
-    test_spike_source.cpp
     test_scope_exit.cpp
+    test_segment_tree.cpp
     test_simd.cpp
     test_span.cpp
+    test_spike_source.cpp
     test_spikes.cpp
     test_spike_store.cpp
     test_stats.cpp
diff --git a/test/unit/common.hpp b/test/unit/common.hpp
index 54db33d3aa75436913a37044efd1f244badd8019..00f569caa4f39a513f551b6b8ee79f8522d75d4c 100644
--- a/test/unit/common.hpp
+++ b/test/unit/common.hpp
@@ -8,7 +8,9 @@
 #include <cmath>
 #include <string>
 #include <type_traits>
+#include <unordered_map>
 #include <utility>
+#include <vector>
 
 #include "../gtest.h"
 
@@ -16,7 +18,7 @@
 
 namespace std {
     template <typename A, typename B>
-    std::ostream& operator<<(std::ostream& out, const std::pair<A, B>& p) {
+    ::std::ostream& operator<<(::std::ostream& out, const ::std::pair<A, B>& p) {
         return out << '(' << p.first << ',' << p.second << ')';
     }
 }
diff --git a/test/unit/common_morphologies.hpp b/test/unit/common_morphologies.hpp
index de00aac7e494db36fb21f6fadf3da04c810a4929..46a7dd4e5216e9e740dc1c98a3ed9987b1100b34 100644
--- a/test/unit/common_morphologies.hpp
+++ b/test/unit/common_morphologies.hpp
@@ -2,16 +2,35 @@
 
 // A set of morphologies for testing discretization.
 
+#include "util/span.hpp"
+
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/segment_tree.hpp>
+
+#include <cstring>
 #include <utility>
 #include <vector>
-#include <arbor/morph/morphology.hpp>
+
+#include "io/sepval.hpp"
 
 namespace common_morphology {
 
-inline std::vector<arb::msample> make_morph_samples(unsigned n) {
-    std::vector<arb::msample> ms;
-    for (unsigned i = 0; i<n; ++i) ms.push_back({{0., 0., (double)i, 0.5}, 5});
-    return ms;
+inline arb::morphology make_morph(std::vector<arb::msize_t> parents, const char* name="") {
+    auto nseg = parents.size();;
+    auto point = [](int i) {return arb::mpoint{0, 0, (double)i, 0.5};};
+    int tag = 1;
+
+    if (!parents.size()) return {};
+
+    arb::segment_tree tree;
+    tree.append(arb::mnpos, point(-1), point(0), tag);
+    for (arb::msize_t i=1; i<nseg; ++i) {
+        int p = parents[i]==arb::mnpos? -1: parents[i];
+        tree.append(parents[i], point(p), point(i), tag);
+    }
+
+    return arb::morphology(tree);
 }
 
 // Test morphologies for CV determination:
@@ -20,16 +39,16 @@ inline std::vector<arb::msample> make_morph_samples(unsigned n) {
 
 static const arb::morphology m_empty;
 
-// One branch.
-static const arb::morphology m_reg_b1{arb::sample_tree(make_morph_samples(2), {arb::mnpos, 0u}), false};
+// regular root, one branch
+static const arb::morphology m_reg_b1 = make_morph({arb::mnpos});
 
 // Six branches:
 // branch 0 has child branches 1 and 2; branch 2 has child branches 3, 4 and 5.
-static const arb::morphology m_reg_b6{arb::sample_tree(make_morph_samples(7), {arb::mnpos, 0u, 1u, 1u, 2u, 2u, 2u}), false};
+static const arb::morphology m_reg_b6 = make_morph({arb::mnpos, 0u, 0u, 1u, 1u, 1u});
 
 // Six branches, mutiple top level branches:
 // branch 0 has child branches 1 and 2; branch 3 has child branches 4 and 5.
-static const arb::morphology m_mlt_b6{arb::sample_tree(make_morph_samples(7), {arb::mnpos, 0u, 1u, 1u, 0u, 4u, 4u}), false};
+static const arb::morphology m_mlt_b6 = make_morph({arb::mnpos, 0u, 0u, arb::mnpos, 3u, 3u});
 
 static std::pair<const char*, arb::morphology> test_morphologies[] = {
     {"m_empty",  m_empty},
diff --git a/test/unit/test_cv_policy.cpp b/test/unit/test_cv_policy.cpp
index bba0fa1531736602163a966dfae4eb70365375ce..2ffbfbc3afe4281020b6ccbbe2e12a9998d7031f 100644
--- a/test/unit/test_cv_policy.cpp
+++ b/test/unit/test_cv_policy.cpp
@@ -15,6 +15,7 @@
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
 
+#include "common_cells.hpp"
 #include "common_morphologies.hpp"
 #include "morph_pred.hpp"
 
@@ -266,26 +267,27 @@ TEST(cv_policy, max_extent) {
     }
 }
 
-TEST(cv_policy, every_sample) {
+TEST(cv_policy, every_segment) {
     using namespace cv_policy_flag;
 
     // Cell with root branch and two child branches, with multiple samples per branch.
     // Fork is at (0., 0., 4.0).
 
-    std::vector<msample> ms;
+    std::vector<mpoint> points;
 
-    ms.push_back({{  0.,   0., 0., 0.5}, 5});
-    for (auto i: make_span(4)) ms.push_back({{  0.,   0., i+1., 0.5}, 5});
-    for (auto i: make_span(4)) ms.push_back({{  0., i+1.,  4.0, 0.5}, 5});
-    for (auto i: make_span(4)) ms.push_back({{i+1.,    0,  4.0, 0.5}, 5});
+    points.push_back({  0.,   0., 0., 0.5});
+    for (auto i: make_span(4)) points.push_back({  0.,   0., i+1., 0.5});
+    for (auto i: make_span(4)) points.push_back({  0., i+1.,  4.0, 0.5});
+    for (auto i: make_span(4)) points.push_back({i+1.,    0,  4.0, 0.5});
 
     std::vector<msize_t> parents = {mnpos, 0, 1, 2, 3, 4, 5, 6, 7, 4, 9, 10, 11 };
-    morphology m{sample_tree(ms, parents), false};
+    auto tree = segments_from_points(points, parents);
+    morphology m{tree};
 
     // Including all samples:
     {
         cable_cell cell(m);
-        cv_policy pol = cv_policy_every_sample();
+        cv_policy pol = cv_policy_every_segment();
 
         mlocation_list expected = {
             {0, 0}, {0, 0.25}, {0, 0.5}, {0, 0.75}, {0, 1},
@@ -299,7 +301,7 @@ TEST(cv_policy, every_sample) {
     {
         cable_cell cell(m);
         region reg = join(reg::branch(1), reg::branch(2));
-        cv_policy pol = cv_policy_every_sample(reg);
+        cv_policy pol = cv_policy_every_segment(reg);
 
         // Get samples from branches 1 and 2, plus boundary points from completions of each
         // branch, viz. (0, 1), (2, 0), (1, 1) from branch 1 and (0, 1), (1, 0), (2, 1) from
@@ -327,7 +329,7 @@ TEST(cv_policy, domain) {
     EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_fixed_per_branch(3, reg1, interior_forks).domain()));
     EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_max_extent(3, reg1).domain()));
     EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_max_extent(3, reg1, interior_forks).domain()));
-    EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_every_sample(reg1).domain()));
+    EXPECT_TRUE(region_eq(cell.provider(), reg1, cv_policy_every_segment(reg1).domain()));
 
     EXPECT_TRUE(region_eq(cell.provider(), join(reg1, reg2), (cv_policy_single(reg1)+cv_policy_single(reg2)).domain()));
     EXPECT_TRUE(region_eq(cell.provider(), join(reg1, reg2), (cv_policy_single(reg1)|cv_policy_single(reg2)).domain()));
diff --git a/test/unit/test_event_delivery.cpp b/test/unit/test_event_delivery.cpp
index dea55b57179b2c92159a9d96190f3e672af19f47..d21fa4a5ec7f578bb35f33a14fadcc3b85bb36e7 100644
--- a/test/unit/test_event_delivery.cpp
+++ b/test/unit/test_event_delivery.cpp
@@ -6,10 +6,11 @@
 // * Inject events one per cell in a given order, and confirm generated spikes
 //   are in the same order.
 
+#include <arbor/cable_cell.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/domain_decomposition.hpp>
+#include <arbor/morph/segment_tree.hpp>
 #include <arbor/simulation.hpp>
-#include <arbor/cable_cell.hpp>
 #include <arbor/spike.hpp>
 #include <arbor/spike_event.hpp>
 
@@ -27,8 +28,8 @@ struct test_recipe: public n_cable_cell_recipe {
     explicit test_recipe(int n): n_cable_cell_recipe(n, test_cell()) {}
 
     static cable_cell test_cell() {
-        sample_tree st;
-        st.append({0,0,0,10,1});
+        segment_tree st;
+        st.append(mnpos, {0,0, 0,10}, {0,0,20,10}, 1);
 
         label_dict d;
         d.set("soma", arb::reg::tagged(1));
diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp
index 4d4be869b9f96e9273367e4975e64b358d803f7c..dc5f9527e8cd3764f632a0dd4abae9672e9929e0 100644
--- a/test/unit/test_fvm_layout.cpp
+++ b/test/unit/test_fvm_layout.cpp
@@ -7,6 +7,8 @@
 #include <arbor/mechcat.hpp>
 #include <arbor/util/optional.hpp>
 
+#include "arbor/morph/morphology.hpp"
+#include "arbor/morph/segment_tree.hpp"
 #include "fvm_layout.hpp"
 #include "util/maputil.hpp"
 #include "util/rangeutil.hpp"
@@ -26,18 +28,35 @@ using util::count_along;
 using util::value_by_key;
 
 namespace {
-    std::vector<cable_cell> two_cell_system() {
+    struct system {
+        std::vector<soma_cell_builder> builders;
         std::vector<cable_cell> cells;
+    };
+
+    system two_cell_system() {
+        system s;
+        auto& cells = s.cells;
+
+        // Cell 0: simple ball and stick
+        {
+            soma_cell_builder builder(12.6157/2.0);
+            builder.add_branch(0, 200, 1.0/2, 1.0/2, 4, "dend");
 
-        // Cell 0: simple ball and stick (see common_cells.hpp)
-        cells.push_back(make_cell_ball_and_stick());
+            cells.push_back(builder.make_cell());
+            auto& cell = cells.back();
+            cell.paint("soma", "hh");
+            cell.paint("dend", "pas");
+            cell.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
+
+            s.builders.push_back(std::move(builder));
+        }
 
         // Cell 1: ball and 3-stick, but with uneven dendrite
         // length and heterogeneous electrical properties:
         //
         // Bulk resistivity: 90 Ω·cm
         // capacitance:
-        //    soma:       0.01  F/m² [default]
+        //    soma:      0.01  F/m² [default]
         //    branch 1:  0.017 F/m²
         //    branch 2:  0.013 F/m²
         //    branch 3:  0.018 F/m²
@@ -60,46 +79,54 @@ namespace {
         //
         // All dendrite branches with 4 compartments.
 
-        soma_cell_builder builder(7.);
-        auto b1 = builder.add_branch(0, 200, 0.5,  0.5, 4,  "dend");
-        auto b2 = builder.add_branch(1, 300, 0.4,  0.4, 4,  "dend");
-        auto b3 = builder.add_branch(1, 180, 0.35, 0.35, 4, "dend");
-        auto cell = builder.make_cell();
+        {
+            soma_cell_builder b(7.);
+            auto b1 = b.add_branch(0, 200, 0.5,  0.5, 4,  "dend");
+            auto b2 = b.add_branch(1, 300, 0.4,  0.4, 4,  "dend");
+            auto b3 = b.add_branch(1, 180, 0.35, 0.35, 4, "dend");
+            cells.push_back(b.make_cell());
+            auto& cell = cells.back();
 
-        cell.paint("soma", "hh");
-        cell.paint("dend", "pas");
+            cell.paint("soma", "hh");
+            cell.paint("dend", "pas");
 
-        using ::arb::reg::branch;
-        cell.paint(branch(b1), membrane_capacitance{0.017});
-        cell.paint(branch(b2), membrane_capacitance{0.013});
-        cell.paint(branch(b3), membrane_capacitance{0.018});
+            using ::arb::reg::branch;
+            auto c1 = reg::cable(b1-1, b.location({b1, 0}).pos, 1);
+            auto c2 = reg::cable(b2-1, b.location({b2, 0}).pos, 1);
+            auto c3 = reg::cable(b3-1, b.location({b3, 0}).pos, 1);
+            cell.paint(c1, membrane_capacitance{0.017});
+            cell.paint(c2, membrane_capacitance{0.013});
+            cell.paint(c3, membrane_capacitance{0.018});
 
-        cell.place(mlocation{2,1}, i_clamp{5.,  80., 0.45});
-        cell.place(mlocation{3,1}, i_clamp{40., 10.,-0.2});
+            cell.place(b.location({2,1}), i_clamp{5.,  80., 0.45});
+            cell.place(b.location({3,1}), i_clamp{40., 10.,-0.2});
 
-        cell.default_parameters.axial_resistivity = 90;
+            cell.default_parameters.axial_resistivity = 90;
 
-        cells.push_back(std::move(cell));
+            s.builders.push_back(std::move(b));
+        }
 
-        return cells;
+        return s;
     }
 
     void check_two_cell_system(std::vector<cable_cell>& cells) {
         ASSERT_EQ(2u, cells.size());
-        ASSERT_EQ(2u, cells[0].morphology().num_branches());
-        ASSERT_EQ(4u, cells[1].morphology().num_branches());
+        ASSERT_EQ(1u, cells[0].morphology().num_branches());
+        ASSERT_EQ(3u, cells[1].morphology().num_branches());
     }
 } // namespace
 
 TEST(fvm_layout, mech_index) {
-    std::vector<cable_cell> cells = two_cell_system();
+    auto system = two_cell_system();
+    auto& cells = system.cells;
+    auto& builders = system.builders;
     check_two_cell_system(cells);
 
     // Add four synapses of two varieties across the cells.
-    cells[0].place(mlocation{1, 0.4}, "expsyn");
-    cells[0].place(mlocation{1, 0.4}, "expsyn");
-    cells[1].place(mlocation{2, 0.4}, "exp2syn");
-    cells[1].place(mlocation{3, 0.4}, "expsyn");
+    cells[0].place(builders[0].location({1, 0.4}), "expsyn");
+    cells[0].place(builders[0].location({1, 0.4}), "expsyn");
+    cells[1].place(builders[1].location({2, 0.4}), "exp2syn");
+    cells[1].place(builders[1].location({3, 0.4}), "expsyn");
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
@@ -208,13 +235,16 @@ TEST(fvm_layout, coalescing_synapses) {
 
     using L=std::initializer_list<unsigned>;
 
+    soma_cell_builder builder(12.6157/2.0);
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 4, "dend");
+
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
-        cell.place(mlocation{1, 0.3}, "expsyn");
-        cell.place(mlocation{1, 0.5}, "expsyn");
-        cell.place(mlocation{1, 0.7}, "expsyn");
-        cell.place(mlocation{1, 0.9}, "expsyn");
+        cell.place(builder.location({1, 0.3}), "expsyn");
+        cell.place(builder.location({1, 0.5}), "expsyn");
+        cell.place(builder.location({1, 0.7}), "expsyn");
+        cell.place(builder.location({1, 0.9}), "expsyn");
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -224,13 +254,13 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_EQ(ivec({1, 1, 1, 1}), expsyn_config.multiplicity);
     }
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(mlocation{1, 0.3}, "expsyn");
-        cell.place(mlocation{1, 0.5}, "exp2syn");
-        cell.place(mlocation{1, 0.7}, "expsyn");
-        cell.place(mlocation{1, 0.9}, "exp2syn");
+        cell.place(builder.location({1, 0.3}), "expsyn");
+        cell.place(builder.location({1, 0.5}), "exp2syn");
+        cell.place(builder.location({1, 0.7}), "expsyn");
+        cell.place(builder.location({1, 0.9}), "exp2syn");
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -244,12 +274,12 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_EQ(ivec({1, 1}), exp2syn_config.multiplicity);
     }
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
-        cell.place(mlocation{1, 0.3}, "expsyn");
-        cell.place(mlocation{1, 0.5}, "expsyn");
-        cell.place(mlocation{1, 0.7}, "expsyn");
-        cell.place(mlocation{1, 0.9}, "expsyn");
+        cell.place(builder.location({1, 0.3}), "expsyn");
+        cell.place(builder.location({1, 0.5}), "expsyn");
+        cell.place(builder.location({1, 0.7}), "expsyn");
+        cell.place(builder.location({1, 0.9}), "expsyn");
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_no_coalesce, {cell}, D);
@@ -259,13 +289,13 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_TRUE(expsyn_config.multiplicity.empty());
     }
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(mlocation{1, 0.3}, "expsyn");
-        cell.place(mlocation{1, 0.5}, "exp2syn");
-        cell.place(mlocation{1, 0.7}, "expsyn");
-        cell.place(mlocation{1, 0.9}, "exp2syn");
+        cell.place(builder.location({1, 0.3}), "expsyn");
+        cell.place(builder.location({1, 0.5}), "exp2syn");
+        cell.place(builder.location({1, 0.7}), "expsyn");
+        cell.place(builder.location({1, 0.9}), "exp2syn");
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_no_coalesce, {cell}, D);
@@ -279,13 +309,13 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_TRUE(exp2syn_config.multiplicity.empty());
     }
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(mlocation{1, 0.3}, "expsyn");
-        cell.place(mlocation{1, 0.3}, "expsyn");
-        cell.place(mlocation{1, 0.7}, "expsyn");
-        cell.place(mlocation{1, 0.7}, "expsyn");
+        cell.place(builder.location({1, 0.3}), "expsyn");
+        cell.place(builder.location({1, 0.3}), "expsyn");
+        cell.place(builder.location({1, 0.7}), "expsyn");
+        cell.place(builder.location({1, 0.7}), "expsyn");
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -295,13 +325,13 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_EQ(ivec({2, 2}), expsyn_config.multiplicity);
     }
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 0.2));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 0.2));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0.1, 0.2));
-        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 0.1, 0.2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 0.2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 0.2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0.1, 0.2));
+        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 0.1, 0.2));
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -317,17 +347,17 @@ TEST(fvm_layout, coalescing_synapses) {
         }
     }
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 0, 3));
-        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 1, 3));
-        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 0, 3));
-        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 1, 3));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 2));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 1, 2));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 2));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 1, 2));
+        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 0, 3));
+        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 1, 3));
+        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 0, 3));
+        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 1, 3));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 1, 2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 1, 2));
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -344,19 +374,19 @@ TEST(fvm_layout, coalescing_synapses) {
         }
     }
     {
-        cable_cell cell = make_cell_ball_and_stick();
+        auto cell = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  1, 2));
-        cell.place(mlocation{1, 0.3}, syn_desc_2("exp2syn", 4, 1));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  1, 2));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  5, 1));
-        cell.place(mlocation{1, 0.3}, syn_desc_2("exp2syn", 1, 3));
-        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  1, 2));
-        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 2));
-        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 1));
-        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 1));
-        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
+        cell.place(builder.location({1, 0.3}), syn_desc_2("exp2syn", 4, 1));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  5, 1));
+        cell.place(builder.location({1, 0.3}), syn_desc_2("exp2syn", 1, 3));
+        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
+        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 2));
+        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 1));
+        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 1));
+        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 2));
 
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -376,7 +406,9 @@ TEST(fvm_layout, coalescing_synapses) {
 }
 
 TEST(fvm_layout, synapse_targets) {
-    std::vector<cable_cell> cells = two_cell_system();
+    auto system = two_cell_system();
+    auto& cells = system.cells;
+    auto& builders = system.builders;
 
     // Add synapses with different parameter values so that we can
     // ensure: 1) CVs for each synapse mechanism are sorted while
@@ -393,14 +425,14 @@ TEST(fvm_layout, synapse_targets) {
         return mechanism_desc(name).set("e", syn_e.at(idx));
     };
 
-    cells[0].place(mlocation{1, 0.9}, syn_desc("expsyn", 0));
-    cells[0].place(mlocation{0, 0.5}, syn_desc("expsyn", 1));
-    cells[0].place(mlocation{1, 0.4}, syn_desc("expsyn", 2));
+    cells[0].place(builders[0].location({1, 0.9}), syn_desc("expsyn", 0));
+    cells[0].place(builders[0].location({0, 0.5}), syn_desc("expsyn", 1));
+    cells[0].place(builders[0].location({1, 0.4}), syn_desc("expsyn", 2));
 
-    cells[1].place(mlocation{2, 0.4}, syn_desc("exp2syn", 3));
-    cells[1].place(mlocation{1, 0.4}, syn_desc("exp2syn", 4));
-    cells[1].place(mlocation{3, 0.4}, syn_desc("expsyn", 5));
-    cells[1].place(mlocation{3, 0.7}, syn_desc("exp2syn", 6));
+    cells[1].place(builders[1].location({2, 0.4}), syn_desc("exp2syn", 3));
+    cells[1].place(builders[1].location({1, 0.4}), syn_desc("exp2syn", 4));
+    cells[1].place(builders[1].location({3, 0.4}), syn_desc("expsyn", 5));
+    cells[1].place(builders[1].location({3, 0.7}), syn_desc("exp2syn", 6));
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
@@ -522,11 +554,11 @@ TEST(fvm_layout, density_norm_area) {
     std::vector<double> expected_gl(ncv, dflt_gl);
 
     // Last 1/6 of branch 1
-    double seg1_area_right = cells[0].embedding().integrate_area(mcable{1, 5./6., 1.});
+    double seg1_area_right = cells[0].embedding().integrate_area(builder.cable({1, 5./6., 1.}));
     // First 1/6 of branch 2
-    double seg2_area_left = cells[0].embedding().integrate_area(mcable{2, 0., 1./6.});
+    double seg2_area_left = cells[0].embedding().integrate_area(builder.cable({2, 0., 1./6.}));
     // First 1/6 of branch 3
-    double seg3_area_left = cells[0].embedding().integrate_area(mcable{3, 0., 1./6.});
+    double seg3_area_left = cells[0].embedding().integrate_area(builder.cable({3, 0., 1./6.}));
 
     // CV 0: soma
     // CV1: left of branch 1
@@ -610,19 +642,19 @@ TEST(fvm_layout, density_norm_area_partial) {
     auto cell = builder.make_cell();
     cell.default_parameters.discretization = cv_policy_fixed_per_branch(1);
 
-    cell.paint(mcable{1, 0., 0.3}, hh_begin);
-    cell.paint(mcable{1, 0.4, 1.}, hh_end);
+    cell.paint(builder.cable({1, 0., 0.3}), hh_begin);
+    cell.paint(builder.cable({1, 0.4, 1.}), hh_end);
 
     std::vector<cable_cell> cells{std::move(cell)};
 
+    // Area of whole cell (which is area of the 1 branch)
+    double area = cells[0].embedding().integrate_area({0, 0., 1});
     // First 30% of branch 1.
-    double b1_area_begin = cells[0].embedding().integrate_area(mcable{1, 0., 0.3});
+    double b1_area_begin = cells[0].embedding().integrate_area(builder.cable({1, 0., 0.3}));
     // Last 60% of branch 1.
-    double b1_area_end = cells[0].embedding().integrate_area(mcable{1, 0.4, 1.});
-    // All of branch 1.
-    double b1_area = cells[0].embedding().integrate_area(mcable{1, 0., 1.});
+    double b1_area_end = cells[0].embedding().integrate_area(builder.cable({1, 0.4, 1.}));
 
-    double expected_norm_area = (b1_area_begin+b1_area_end)/b1_area;
+    double expected_norm_area = (b1_area_begin+b1_area_end)/area;
     double expected_gnabar = dflt_gnabar;
     double expected_gkbar = (dflt_gkbar*b1_area_begin + end_gkbar*b1_area_end)/(b1_area_begin + b1_area_end);
     double expected_gl = (dflt_gl*b1_area_begin + end_gl*b1_area_end)/(b1_area_begin + b1_area_end);
@@ -699,29 +731,29 @@ TEST(fvm_layout, ion_weights) {
     // 1/2 of branch 1 and the initial 1/2 of branches 2 and 3.
     //
     // Geometry:
-    //   soma 0: radius 5 µm
-    //   dend 1: 100 µm long, 1 µm diameter cylinder, tag 2
-    //   dend 2: 200 µm long, 1 µm diameter cylinder, tag 3
-    //   dend 3: 100 µm long, 1 µm diameter cylinder, tag 4
+    //   soma 0: radius 5 µm, area 100π μm²
+    //   dend 1: 100 µm long, 1 µm diameter cylinder, area 100π μm²
+    //   dend 2: 200 µm long, 1 µm diameter cylinder, area 200π μm²
+    //   dend 3: 100 µm long, 1 µm diameter cylinder, area 100π μm²
     //
     // The radius of the soma is chosen such that the surface area of soma is
     // the same as a 100µm dendrite, which makes it easier to describe the
     // expected weights.
 
-    auto construct_cell = []() {
-        soma_cell_builder builder(5);
-        builder.add_branch(0, 100, 0.5, 0.5, 1, "dend");
-        builder.add_branch(1, 200, 0.5, 0.5, 1, "dend");
-        builder.add_branch(1, 100, 0.5, 0.5, 1, "dend");
-        return builder.make_cell();
-    };
+    soma_cell_builder builder(5);
+    builder.add_branch(0, 100, 0.5, 0.5, 1, "dend");
+    builder.add_branch(1, 200, 0.5, 0.5, 1, "dend");
+    builder.add_branch(1, 100, 0.5, 0.5, 1, "dend");
 
     using uvec = std::vector<fvm_size_type>;
     using ivec = std::vector<fvm_index_type>;
     using fvec = std::vector<fvm_value_type>;
 
+    //uvec mech_branches[] = {
+        //{0}, {0,2}, {2, 3}, {0, 1, 2, 3}, {3}
+    //};
     uvec mech_branches[] = {
-        {0}, {0,2}, {2, 3}, {0, 1, 2, 3}, {3}
+        {0}, {0,2}
     };
 
     ivec expected_ion_cv[] = {
@@ -748,10 +780,11 @@ TEST(fvm_layout, ion_weights) {
 
     for (auto run: count_along(mech_branches)) {
         SCOPED_TRACE("run "+std::to_string(run));
-        auto c = construct_cell();
+        auto c = builder.make_cell();
 
         for (auto i: mech_branches[run]) {
-            c.paint(reg::branch(i), "test_ca");
+            auto cab = builder.cable({i, 0, 1});
+            c.paint(reg::cable(cab.branch, cab.prox_pos, cab.dist_pos), "test_ca");
         }
 
         std::vector<cable_cell> cells{std::move(c)};
@@ -847,8 +880,10 @@ TEST(fvm_layout, vinterp_cable) {
     // should interpolate between that and the parent.
 
     // Cable cell with just one branch, non-spherical root.
-    morphology morph(sample_tree({msample{0., 0., 0., 1.}, msample{10., 0., 0., 1.}}, {mnpos, 0u}));
-    cable_cell cell(morph);
+    arb::segment_tree tree;
+    tree.append(mnpos, { 0,0,0,1}, {10,0,0,1}, 1);
+    arb::morphology m(tree);
+    cable_cell cell(m);
 
     // CV midpoints at branch pos 0.1, 0.3, 0.5, 0.7, 0.9.
     // Expect voltage reference locations to be CV modpoints.
@@ -901,10 +936,12 @@ TEST(fvm_layout, vinterp_forked) {
     // interpolation.
 
     // Cable cell with three branchses; branches 0 has child branches 1 and 2.
-    morphology morph(sample_tree(
-            {{0., 0., 0., 1.}, {10., 0., 0., 1}, {10., 20., 0., 1}, {10., -20., 0., 1}},
-            {mnpos, 0u, 1u, 1u}));
-    cable_cell cell(morph);
+    segment_tree tree;
+    tree.append(mnpos, {0., 0., 0., 1.}, {10., 0., 0., 1}, 1);
+    tree.append(    0, {10., 20., 0., 1}, 1);
+    tree.append(    0, {10.,-20., 0., 1}, 1);
+    morphology m(tree);
+    cable_cell cell(m);
 
     // CV 0 contains branch 0 and the fork point; CV 1 and CV 2 have CV 0 as parent,
     // and contain branches 1 and 2 respectively, excluding the fork point.
@@ -1003,10 +1040,12 @@ TEST(fvm_layout, iinterp) {
     // Use the same cell/discretiazation as in vinterp_forked test:
 
     // Cable cell with three branchses; branches 0 has child branches 1 and 2.
-    morphology morph(sample_tree(
-            {{0., 0., 0., 1.}, {10., 0., 0., 1}, {10., 20., 0., 1}, {10., -20., 0., 1}},
-            {mnpos, 0u, 1u, 1u}));
-    cable_cell cell(morph);
+    segment_tree tree;
+    tree.append(mnpos, {0., 0., 0., 1.}, {10., 0., 0., 1}, 1);
+    tree.append(    0, {10., 20., 0., 1}, 1);
+    tree.append(    0, {10.,-20., 0., 1}, 1);
+    morphology m(tree);
+    cable_cell cell(m);
 
     // CV 0 contains branch 0 and the fork point; CV 1 and CV 2 have CV 0 as parent,
     // and contain branches 1 and 2 respectively, excluding the fork point.
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index 4110db15708690fc5c7ca3bcafba487a96501ff5..1771eda6085b34217a4a19f9b3922286cf2a854e 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -266,12 +266,12 @@ TEST(fvm_lowered, target_handles) {
         make_cell_ball_and_3stick()
     };
 
-    EXPECT_EQ(cells[0].morphology().num_branches(), 2u);
-    EXPECT_EQ(cells[1].morphology().num_branches(), 4u);
+    EXPECT_EQ(cells[0].morphology().num_branches(), 1u);
+    EXPECT_EQ(cells[1].morphology().num_branches(), 3u);
 
     // (in increasing target order)
-    cells[0].place(mlocation{1, 0.4}, "expsyn");
-    cells[0].place(mlocation{0, 0.5}, "expsyn");
+    cells[0].place(mlocation{0, 0.7}, "expsyn");
+    cells[0].place(mlocation{0, 0.3}, "expsyn");
     cells[1].place(mlocation{2, 0.2}, "exp2syn");
     cells[1].place(mlocation{2, 0.8}, "expsyn");
 
@@ -282,9 +282,9 @@ TEST(fvm_lowered, target_handles) {
     probe_association_map probe_map;
 
     auto test_target_handles = [&](fvm_cell& cell) {
-        mechanism *expsyn = find_mechanism(cell, "expsyn");
+        mechanism* expsyn = find_mechanism(cell, "expsyn");
         ASSERT_TRUE(expsyn);
-        mechanism *exp2syn = find_mechanism(cell, "exp2syn");
+        mechanism* exp2syn = find_mechanism(cell, "exp2syn");
         ASSERT_TRUE(exp2syn);
 
         unsigned expsyn_id = expsyn->mechanism_id();
@@ -341,8 +341,10 @@ TEST(fvm_lowered, stimulus) {
     std::vector<cable_cell> cells;
     cells.push_back(make_cell_ball_and_stick(false));
 
-    cells[0].place(mlocation{1,1},   i_clamp{5., 80., 0.3});
-    cells[0].place(mlocation{0,0.5}, i_clamp{1., 2.,  0.1});
+    // At end of stick
+    cells[0].place(mlocation{0,1},   i_clamp{5., 80., 0.3});
+    // On the soma CV, which is over the approximate interval: (cable 0 0 0.1)
+    cells[0].place(mlocation{0,0.05}, i_clamp{1., 2.,  0.1});
 
     const fvm_size_type soma_cv = 0u;
     const fvm_size_type tip_cv = 5u;
@@ -427,9 +429,9 @@ TEST(fvm_lowered, derived_mechs) {
 
     std::vector<cable_cell> cells;
     cells.reserve(3);
+    soma_cell_builder builder(6);
+    builder.add_branch(0, 100, 0.5, 0.5, 4, "dend");
     for (int i = 0; i<3; ++i) {
-        soma_cell_builder builder(6);
-        builder.add_branch(0, 100, 0.5, 0.5, 4, "dend");
         auto cell = builder.make_cell();
 
         switch (i) {
@@ -451,7 +453,7 @@ TEST(fvm_lowered, derived_mechs) {
     rec.catalogue() = make_unit_test_catalogue();
     rec.catalogue().derive("custom_kin1", "test_kin1", {{"tau", 20.0}});
 
-    cable_probe_total_ion_current_density where{mlocation{1, 0.3}};
+    cable_probe_total_ion_current_density where{builder.location({1, 0.3})};
     rec.add_probe(0, 0, where);
     rec.add_probe(1, 0, where);
     rec.add_probe(2, 0, where);
@@ -758,8 +760,8 @@ TEST(fvm_lowered, point_ionic_current) {
 // Test area-weighted linear combination of ion species concentrations
 
 TEST(fvm_lowered, weighted_write_ion) {
-    // Create a cell with 4 branches (same morphopology as in fvm_layout.ion_weights test):
-    //   - Soma (branch 0) plus three dendrites (1, 2, 3) meeting at a branch point.
+    // Create a cell with 3 branches (same morphopology as in fvm_layout.ion_weights test):
+    //   - Soma (part of branch 0) plus three dendrites (d1, d2, d3) meeting at a branch point.
     //   - Dendritic segments are given 1 compartments each.
     //
     //          /
@@ -773,10 +775,10 @@ TEST(fvm_lowered, weighted_write_ion) {
     // 1/2 of branch 1 and the initial 1/2 of branches 2 and 3.
     //
     // Geometry:
-    //   soma 0: radius 5 µm
-    //   dend 1: 100 µm long, 1 µm diameter cylinder
-    //   dend 2: 200 µm long, 1 µm diameter cylinder
-    //   dend 3: 100 µm long, 1 µm diameter cylinder
+    //   soma 0:  10 µm long, 10 µm diameter cylinder: area = 100π μm²
+    //   dend 1: 100 µm long,  1 µm diameter cylinder: area = 100π μm²
+    //   dend 2: 200 µm long,  1 µm diameter cylinder: area = 200π μm²
+    //   dend 3: 100 µm long,  1 µm diameter cylinder: area = 100π μm²
     //
     // The radius of the soma is chosen such that the surface area of soma is
     // the same as a 100 µm dendrite, which makes it easier to describe the
@@ -802,10 +804,10 @@ TEST(fvm_lowered, weighted_write_ion) {
     const double con_ext = 120;
 
     // Ca ion reader test_kinlva on CV 2 and 3 via branch 2:
-    c.paint(reg::branch(2), "test_kinlva");
+    c.paint(reg::branch(1), "test_kinlva");
 
     // Ca ion writer test_ca on CV 2 and 4 via branch 3:
-    c.paint(reg::branch(3), "test_ca");
+    c.paint(reg::branch(2), "test_ca");
 
     cable1d_recipe rec(c);
     rec.catalogue() = make_unit_test_catalogue();
@@ -897,7 +899,7 @@ TEST(fvm_lowered, gj_coords_simple) {
         soma_cell_builder b(2.1);
         b.add_branch(0, 10, 0.3, 0.2, 5, "dend");
         auto c = b.make_cell();
-        c.place(mlocation{1, 0.8}, gap_junction_site{});
+        c.place(b.location({1, 0.8}), gap_junction_site{});
         cells.push_back(std::move(c));
     }
 
@@ -905,7 +907,7 @@ TEST(fvm_lowered, gj_coords_simple) {
         soma_cell_builder b(2.4);
         b.add_branch(0, 10, 0.3, 0.2, 2, "dend");
         auto c = b.make_cell();
-        c.place(mlocation{1, 1}, gap_junction_site{});
+        c.place(b.location({1, 1}), gap_junction_site{});
         cells.push_back(std::move(c));
     }
 
@@ -980,7 +982,7 @@ TEST(fvm_lowered, gj_coords_complex) {
     b0.add_branch(0, 8, 0.3, 0.2, 4, "dend");
 
     auto c0 = b0.make_cell();
-    mlocation c0_gj[2] = {{1, 1}, {1, 0.5}};
+    mlocation c0_gj[2] = {b0.location({1, 1}), b0.location({1, 0.5})};
 
     c0.place(c0_gj[0], gap_junction_site{});
     c0.place(c0_gj[1], gap_junction_site{});
@@ -991,7 +993,7 @@ TEST(fvm_lowered, gj_coords_complex) {
     b1.add_branch(1,  5, 0.2, 0.2, 5, "dend");
 
     auto c1 = b1.make_cell();
-    mlocation c1_gj[4] = {{2, 1}, {1, 1}, {1, 0.45}, {1, 0.1}};
+    mlocation c1_gj[4] = {b1.location({2, 1}), b1.location({1, 1}), b1.location({1, 0.45}), b1.location({1, 0.1})};
 
     c1.place(c1_gj[0], gap_junction_site{});
     c1.place(c1_gj[1], gap_junction_site{});
@@ -1007,7 +1009,7 @@ TEST(fvm_lowered, gj_coords_complex) {
     b2.add_branch(2, 4, 0.2, 0.2, 2, "dend");
 
     auto c2 = b2.make_cell();
-    mlocation c2_gj[3] = {{1, 0.5}, {4, 1}, {2, 1}};
+    mlocation c2_gj[3] = {b2.location({1, 0.5}), b2.location({4, 1}), b2.location({2, 1})};
 
     c2.place(c2_gj[0], gap_junction_site{});
     c2.place(c2_gj[1], gap_junction_site{});
diff --git a/test/unit/test_mc_cell_group.cpp b/test/unit/test_mc_cell_group.cpp
index 1be195302555b8cf917c8bc1c78031c92f489d42..2971974752102f622f80b9cca10aa7e3f4bc0d89 100644
--- a/test/unit/test_mc_cell_group.cpp
+++ b/test/unit/test_mc_cell_group.cpp
@@ -26,8 +26,8 @@ namespace {
         cable_cell c = builder.make_cell();
         c.paint("soma", "hh");
         c.paint("dend", "pas");
-        c.place(mlocation{1,1}, i_clamp{5, 80, 0.3});
-        c.place(mlocation{0, 0}, threshold_detector{0});
+        c.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
+        c.place(builder.location({0, 0}), threshold_detector{0});
         return c;
     }
 }
@@ -66,7 +66,7 @@ TEST(mc_cell_group, sources) {
     for (int i=0; i<20; ++i) {
         cells.push_back(make_cell());
         if (i==0 || i==3 || i==17) {
-            cells.back().place(mlocation{1, 0.3}, threshold_detector{2.3});
+            cells.back().place(mlocation{0, 0.3}, threshold_detector{2.3});
         }
 
         EXPECT_EQ(1u + (i==0 || i==3 || i==17), cells.back().detectors().size());
diff --git a/test/unit/test_mc_cell_group_gpu.cpp b/test/unit/test_mc_cell_group_gpu.cpp
index e92e8926b3a217858893a51cc4817b134acfeabb..5a6d11cccfa414dab265b74d8e4123db15978daf 100644
--- a/test/unit/test_mc_cell_group_gpu.cpp
+++ b/test/unit/test_mc_cell_group_gpu.cpp
@@ -27,8 +27,8 @@ namespace {
         cable_cell c = builder.make_cell();
         c.paint("soma", "hh");
         c.paint("dend", "pas");
-        c.place(mlocation{1,1}, i_clamp{5, 80, 0.3});
-        c.place(mlocation{0, 0}, threshold_detector{0});
+        c.place(builder.location({1, 1}), i_clamp{5, 80, 0.3});
+        c.place(builder.location({0, 0}), threshold_detector{0});
         return c;
     }
 }
diff --git a/test/unit/test_morph_embedding.cpp b/test/unit/test_morph_embedding.cpp
index b079c602331c93a0354c54c9f83ae625ecce2b95..f8de6f4a8048b0fbfb1b525cc90fb1374a26c9a2 100644
--- a/test/unit/test_morph_embedding.cpp
+++ b/test/unit/test_morph_embedding.cpp
@@ -1,16 +1,18 @@
 #include <cmath>
+#include <unordered_map>
 #include <vector>
 
 #include <arbor/math.hpp>
 #include <arbor/morph/embed_pwlin.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
-#include <arbor/morph/sample_tree.hpp>
+#include <arbor/morph/segment_tree.hpp>
 
 #include "util/piecewise.hpp"
 
 #include "../test/gtest.h"
 #include "common.hpp"
+#include "common_cells.hpp"
 
 using namespace arb;
 using embedding = embed_pwlin;
@@ -34,9 +36,9 @@ using embedding = embed_pwlin;
     }
 }
 
-TEST(embedding, samples_and_branch_length) {
+TEST(embedding, segments_and_branch_length) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
     using loc = mlocation;
 
     // A single unbranched cable with 5 sample points.
@@ -44,23 +46,26 @@ TEST(embedding, samples_and_branch_length) {
     // 0 μm, 1 μm, 3 μm, 7 μm and 10 μm.
     {
         pvec parents = {mnpos, 0, 1, 2, 3};
-        svec samples = {
-            {{  0,  0,  0,  2}, 1},
-            {{  1,  0,  0,  2}, 1},
-            {{  3,  0,  0,  2}, 1},
-            {{  7,  0,  0,  2}, 1},
-            {{ 10,  0,  0,  2}, 1},
+        svec points = {
+            { 0,  0,  0,  2},
+            { 1,  0,  0,  2},
+            { 3,  0,  0,  2},
+            { 7,  0,  0,  2},
+            {10,  0,  0,  2},
         };
-        sample_tree sm(samples, parents);
-        morphology m(sm, false);
+        morphology m(segments_from_points(points, parents));
 
         embedding em(m);
 
-        EXPECT_TRUE(location_eq(m, em.sample_location(0), (loc{0,0})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(1), (loc{0,0.1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(2), (loc{0,0.3})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(3), (loc{0,0.7})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(4), (loc{0,1})));
+        auto nloc = 5u;
+        EXPECT_EQ(nloc, em.segment_locations().size());
+        auto locs = arb::util::make_range(em.branch_segment_locations(0));
+        EXPECT_EQ(nloc, locs.size());
+        EXPECT_TRUE(location_eq(m, locs[0], (loc{0,0})));
+        EXPECT_TRUE(location_eq(m, locs[1], (loc{0,0.1})));
+        EXPECT_TRUE(location_eq(m, locs[2], (loc{0,0.3})));
+        EXPECT_TRUE(location_eq(m, locs[3], (loc{0,0.7})));
+        EXPECT_TRUE(location_eq(m, locs[4], (loc{0,1})));
 
         EXPECT_EQ(10., em.branch_length(0));
     }
@@ -73,93 +78,66 @@ TEST(embedding, samples_and_branch_length) {
     //          2   4
     //             5 6
     //                7
-    {   // Spherical root.
-        pvec parents = {mnpos, 0, 1, 0, 3, 4, 4, 6};
-
-        svec samples = {
-            {{  0,  0,  0, 10}, 1},
-            {{ 10,  0,  0,  2}, 3},
-            {{100,  0,  0,  2}, 3},
-            {{  0, 10,  0,  2}, 3},
-            {{  0,100,  0,  2}, 3},
-            {{100,100,  0,  2}, 3},
-            {{  0,200,  0,  2}, 3},
-            {{  0,300,  0,  2}, 3},
-        };
-        sample_tree sm(samples, parents);
-        morphology m(sm, true);
-        ASSERT_EQ(5u, m.num_branches());
-
-        embedding em(m);
+    pvec parents = {mnpos, 0, 1, 0, 3, 4, 4, 6};
+
+    svec points = {
+        {  0,  0,  0,  2},
+        { 10,  0,  0,  2},
+        {100,  0,  0,  2},
+        {  0, 10,  0,  2},
+        {  0,100,  0,  2},
+        {100,100,  0,  2},
+        {  0,130,  0,  2},
+        {  0,300,  0,  2},
+    };
+    morphology m(segments_from_points(points, parents));
 
-        EXPECT_TRUE(location_eq(m, em.sample_location(0), (loc{0,0.5})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(1), (loc{1,0})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(2), (loc{1,1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(3), (loc{2,0})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(4), (loc{2,1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(5), (loc{3,1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(6), (loc{4,0.5})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(7), (loc{4,1})));
-
-        EXPECT_EQ(20.,  em.branch_length(0));
-        EXPECT_EQ(90.,  em.branch_length(1));
-        EXPECT_EQ(90.,  em.branch_length(2));
-        EXPECT_EQ(100., em.branch_length(3));
-        EXPECT_EQ(200., em.branch_length(4));
-    }
-    {   // No Spherical root
-        pvec parents = {mnpos, 0, 1, 0, 3, 4, 4, 6};
-
-        svec samples = {
-            {{  0,  0,  0,  2}, 1},
-            {{ 10,  0,  0,  2}, 3},
-            {{100,  0,  0,  2}, 3},
-            {{  0, 10,  0,  2}, 3},
-            {{  0,100,  0,  2}, 3},
-            {{100,100,  0,  2}, 3},
-            {{  0,130,  0,  2}, 3},
-            {{  0,300,  0,  2}, 3},
-        };
-        sample_tree sm(samples, parents);
-        morphology m(sm, false);
-        ASSERT_EQ(4u, m.num_branches());
+    ASSERT_EQ(4u, m.num_branches());
 
-        embedding em(m);
+    embedding em(m);
 
-        EXPECT_TRUE(location_eq(m, em.sample_location(0), (loc{0,0})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(1), (loc{0,0.1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(2), (loc{0,1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(3), (loc{1,0.1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(4), (loc{1,1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(5), (loc{2,1})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(6), (loc{3,0.15})));
-        EXPECT_TRUE(location_eq(m, em.sample_location(7), (loc{3,1})));
-
-        EXPECT_EQ(100., em.branch_length(0));
-        EXPECT_EQ(100., em.branch_length(1));
-        EXPECT_EQ(100., em.branch_length(2));
-        EXPECT_EQ(200., em.branch_length(3));
-    }
+    auto locs = arb::util::make_range(em.branch_segment_locations(0));
+    EXPECT_TRUE(location_eq(m, locs[0], (loc{0,0})));
+    EXPECT_TRUE(location_eq(m, locs[1], (loc{0,0.1})));
+    EXPECT_TRUE(location_eq(m, locs[2], (loc{0,1})));
+
+    locs = em.branch_segment_locations(1);
+    EXPECT_TRUE(location_eq(m, locs[0], (loc{1,0})));
+    EXPECT_TRUE(location_eq(m, locs[1], (loc{1,0.1})));
+    EXPECT_TRUE(location_eq(m, locs[2], (loc{1,1})));
+
+    locs = em.branch_segment_locations(2);
+    EXPECT_TRUE(location_eq(m, locs[0], (loc{2,0})));
+    EXPECT_TRUE(location_eq(m, locs[1], (loc{2,1})));
+
+    locs = em.branch_segment_locations(3);
+    EXPECT_TRUE(location_eq(m, locs[0], (loc{3,0})));
+    EXPECT_TRUE(location_eq(m, locs[1], (loc{3,0.15})));
+    EXPECT_TRUE(location_eq(m, locs[2], (loc{3,1})));
+
+    EXPECT_EQ(100., em.branch_length(0));
+    EXPECT_EQ(100., em.branch_length(1));
+    EXPECT_EQ(100., em.branch_length(2));
+    EXPECT_EQ(200., em.branch_length(3));
 }
 
 // TODO: integrator tests
 
-
 TEST(embedding, partial_branch_length) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
     using util::pw_elements;
 
     pvec parents = {mnpos, 0, 1, 2, 2};
-    svec samples = {
-        {{  0,  0,  0, 10}, 1},
-        {{ 10,  0,  0, 20}, 1},
-        {{ 30,  0,  0, 10}, 1},
-        {{ 30, 10,  0, 5},  2},
-        {{ 30,  0, 50, 5},  2}
+    svec points = {
+        { 0,  0,  0, 10},
+        {10,  0,  0, 20},
+        {30,  0,  0, 10},
+        {30, 10,  0,  5},
+        {30,  0, 50,  5}
     };
 
-    morphology m(sample_tree(samples, parents), false);
+    morphology m(segments_from_points(points, parents));
     embedding em(m);
 
     EXPECT_DOUBLE_EQ(30., em.branch_length(0));
@@ -183,20 +161,20 @@ TEST(embedding, partial_branch_length) {
 
 TEST(embedding, partial_area) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
     using util::pw_elements;
     using testing::near_relative;
 
     pvec parents = {mnpos, 0, 1, 2, 2};
-    svec samples = {
-        {{  0,  0,  0, 10}, 1},
-        {{ 10,  0,  0, 20}, 1},
-        {{ 30,  0,  0, 10}, 1},
-        {{ 30, 10,  0, 5},  2},
-        {{ 30,  0, 50, 5},  2}
+    svec points = {
+        { 0,  0,  0, 10},
+        {10,  0,  0, 20},
+        {30,  0,  0, 10},
+        {30, 10,  0,  5},
+        {30,  0, 50,  5}
     };
 
-    morphology m(sample_tree(samples, parents), false);
+    morphology m(segments_from_points(points, parents));
     embedding em(m);
 
     // Cable 1: single truncated cone, length L = 10,
diff --git a/test/unit/test_morph_expr.cpp b/test/unit/test_morph_expr.cpp
index 40307aa0b8630b3266ce70afc84c1edd195e3535..fd5ddbafa14f9b92eaeab240ee1bc39711bca54f 100644
--- a/test/unit/test_morph_expr.cpp
+++ b/test/unit/test_morph_expr.cpp
@@ -9,11 +9,12 @@
 #include <arbor/morph/mprovider.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/region.hpp>
-#include <arbor/morph/sample_tree.hpp>
 
 #include "util/span.hpp"
 #include "util/strprintf.hpp"
 
+#include "common.hpp"
+#include "common_cells.hpp"
 #include "morph_pred.hpp"
 
 using namespace arb;
@@ -66,16 +67,13 @@ TEST(locset, expr_repn) {
 
     auto root = ls::root();
     auto term = ls::terminal();
-    auto samp = ls::sample(42);
     auto loc = ls::location(2, 0.5);
     auto bdy = ls::boundary(reg::tagged(1));
 
     EXPECT_EQ(to_string(root), "(root)");
     EXPECT_EQ(to_string(term), "(terminal)");
     EXPECT_EQ(to_string(sum(root, term)), "(sum (root) (terminal))");
-    EXPECT_EQ(to_string(sum(root, term, samp)), "(sum (sum (root) (terminal)) (sample 42))");
-    EXPECT_EQ(to_string(sum(root, term, samp, loc)), "(sum (sum (sum (root) (terminal)) (sample 42)) (location 2 0.5))");
-    EXPECT_EQ(to_string(samp), "(sample 42)");
+    EXPECT_EQ(to_string(sum(root, term, loc)), "(sum (sum (root) (terminal)) (location 2 0.5))");
     EXPECT_EQ(to_string(loc), "(location 2 0.5)");
     EXPECT_EQ(to_string(bdy), "(boundary (tag 1))");
 }
@@ -94,18 +92,18 @@ TEST(locset, invalid_mlocation) {
 
 TEST(locset, thingify_named) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
 
     locset banana = ls::root();
     locset cake = ls::terminal();
 
-    sample_tree sm(svec{ {{0,0,0,1},1}, {{10,0,0,1},1} }, pvec{mnpos, 0});
+    auto sm = segments_from_points(svec{ {0,0,0,1}, {10,0,0,1} }, pvec{mnpos, 0});
     {
         label_dict dict;
         dict.set("banana", banana);
         dict.set("cake", cake);
 
-        mprovider mp(morphology(sm, false), dict);
+        mprovider mp(morphology(sm), dict);
         EXPECT_EQ(thingify(locset("cake"), mp), thingify(cake, mp));
         EXPECT_EQ(thingify(locset("banana"), mp), thingify(banana, mp));
 
@@ -118,7 +116,7 @@ TEST(locset, thingify_named) {
         dict.set("topping", locset("fruit"));
         dict.set("fruit", locset("strawberry"));
 
-        EXPECT_THROW(mprovider(morphology(sm, false), dict), unbound_name);
+        EXPECT_THROW(mprovider(morphology(sm), dict), unbound_name);
     }
     {
         label_dict dict;
@@ -127,26 +125,26 @@ TEST(locset, thingify_named) {
         dict.set("topping", locset("fruit"));
         dict.set("fruit", sum(locset("banana"), locset("topping")));
 
-        EXPECT_THROW(mprovider(morphology(sm, false), dict), circular_definition);
+        EXPECT_THROW(mprovider(morphology(sm), dict), circular_definition);
     }
 }
 
 TEST(region, thingify_named) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
 
     region banana = reg::branch(0);
     region cake = reg::cable(0, 0.2, 0.3);
 
     // copy-paste ftw
 
-    sample_tree sm(svec{ {{0,0,0,1},1}, {{10,0,0,1},1} }, pvec{mnpos, 0});
+    auto sm = segments_from_points(svec{ {0,0,0,1}, {10,0,0,1} }, pvec{mnpos, 0});
     {
         label_dict dict;
         dict.set("banana", banana);
         dict.set("cake", cake);
 
-        mprovider mp(morphology(sm, false), dict);
+        mprovider mp(morphology(sm), dict);
         EXPECT_EQ(thingify(region("cake"), mp), thingify(cake, mp));
         EXPECT_EQ(thingify(region("banana"), mp), thingify(banana, mp));
 
@@ -159,7 +157,7 @@ TEST(region, thingify_named) {
         dict.set("topping", region("fruit"));
         dict.set("fruit", region("strawberry"));
 
-        EXPECT_THROW(mprovider(morphology(sm, false), dict), unbound_name);
+        EXPECT_THROW(mprovider(morphology(sm), dict), unbound_name);
     }
     {
         label_dict dict;
@@ -168,7 +166,7 @@ TEST(region, thingify_named) {
         dict.set("topping", region("fruit"));
         dict.set("fruit", join(region("cake"), region("topping")));
 
-        EXPECT_THROW(mprovider(morphology(sm, false), dict), circular_definition);
+        EXPECT_THROW(mprovider(morphology(sm), dict), circular_definition);
     }
 }
 
@@ -176,11 +174,10 @@ TEST(region, thingify_named) {
 
 TEST(locset, thingify) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
     using ll = mlocation_list;
     auto root = ls::root();
     auto term = ls::terminal();
-    auto samp = ls::sample(4);
     auto midb2 = ls::location(2, 0.5);
     auto midb1 = ls::location(1, 0.5);
     auto begb0 = ls::location(0, 0);
@@ -198,43 +195,23 @@ TEST(locset, thingify) {
     //             5 6
     //                7
     pvec parents = {mnpos, 0, 1, 0, 3, 4, 4, 6};
-    svec samples = {
-        {{  0,  0,  0,  2}, 3},
-        {{ 10,  0,  0,  2}, 3},
-        {{100,  0,  0,  2}, 3},
-        {{  0, 10,  0,  2}, 3},
-        {{  0,100,  0,  2}, 3},
-        {{100,100,  0,  2}, 3},
-        {{  0,200,  0,  2}, 3},
-        {{  0,300,  0,  2}, 3},
+    svec points = {
+        {  0,  0,  0,  2},
+        { 10,  0,  0,  2},
+        {100,  0,  0,  2},
+        {  0, 10,  0,  2},
+        {  0,100,  0,  2},
+        {100,100,  0,  2},
+        {  0,200,  0,  2},
+        {  0,300,  0,  2},
     };
-    sample_tree sm(samples, parents);
+    auto sm = segments_from_points(points, parents);
 
     {
-        mprovider mp(morphology(sm, true));
-
-        EXPECT_EQ(thingify(root, mp),  (ll{{0,0}}));
-        EXPECT_EQ(thingify(term, mp),  (ll{{1,1},{3,1},{4,1}}));
-        EXPECT_EQ(thingify(samp, mp),  (ll{{2,1}}));
-        EXPECT_EQ(thingify(midb2, mp), (ll{{2,0.5}}));
-        EXPECT_EQ(thingify(midb1, mp), (ll{{1,0.5}}));
-        EXPECT_EQ(thingify(begb0, mp), (ll{{0,0}}));
-        EXPECT_EQ(thingify(begb1, mp), (ll{{1,0}}));
-        EXPECT_EQ(thingify(begb2, mp), (ll{{2,0}}));
-        EXPECT_EQ(thingify(begb3, mp), (ll{{3,0}}));
-        EXPECT_EQ(thingify(begb4, mp), (ll{{4,0}}));
-
-        // Check round-trip of implicit locset conversions.
-        // (Use a locset which is non-trivially a multiset in order to
-        // test the fold in the constructor.)
-        EXPECT_EQ(thingify(multi, mp), thingify(locset(thingify(multi, mp)), mp));
-    }
-    {
-        mprovider mp(morphology(sm, false));
+        auto mp = mprovider(morphology(sm));
 
         EXPECT_EQ(thingify(root, mp),  (ll{{0,0}}));
         EXPECT_EQ(thingify(term, mp),  (ll{{0,1},{2,1},{3,1}}));
-        EXPECT_EQ(thingify(samp, mp),  (ll{{1,1}}));
         EXPECT_EQ(thingify(midb2, mp), (ll{{2,0.5}}));
         EXPECT_EQ(thingify(midb1, mp), (ll{{1,0.5}}));
         EXPECT_EQ(thingify(begb0, mp), (ll{{0,0}}));
@@ -246,7 +223,7 @@ TEST(locset, thingify) {
         EXPECT_THROW(thingify(begb4, mp), no_such_branch);
     }
     {
-        mprovider mp(morphology(sm, false));
+        auto mp = mprovider(morphology(sm));
 
         auto all = reg::all();
         auto ls0 = thingify(ls::uniform(all,  0,  9, 12), mp);
@@ -301,7 +278,7 @@ TEST(locset, thingify) {
         EXPECT_TRUE(found == 2);
     }
     {
-        mprovider mp(morphology(sm, false));
+        auto mp = mprovider(morphology(sm));
         auto sub_reg = join(reg::cable(0, 0.2, 0.7), reg::cable(1, 0.1, 1), reg::cable(3, 0.5, 0.6));
 
         auto ls0 = thingify(ls::uniform(sub_reg, 0, 10000, 72), mp);
@@ -331,7 +308,8 @@ TEST(locset, thingify) {
 
 TEST(region, thingify_simple_morphologies) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
+    using tvec = std::vector<int>;
     using cl = mcable_list;
 
     // A single unbranched cable with 5 sample points.
@@ -339,15 +317,17 @@ TEST(region, thingify_simple_morphologies) {
     // 0 μm, 1 μm, 3 μm, 7 μm and 10 μm.
     {
         pvec parents = {mnpos, 0, 1, 2, 3};
-        svec samples = {
-            {{  0,  0,  0,  2}, 1},
-            {{  1,  0,  0,  2}, 1},
-            {{  3,  0,  0,  2}, 2},
-            {{  7,  0,  0,  2}, 1},
-            {{ 10,  0,  0,  2}, 2},
+        svec points = {
+            { 0,  0,  0,  2},
+            { 1,  0,  0,  2},
+            { 3,  0,  0,  2},
+            { 7,  0,  0,  2},
+            {10,  0,  0,  2},
         };
-        sample_tree sm(samples, parents);
-        mprovider mp(morphology(sm, false));
+        tvec tags = {1, 1, 2, 1, 2};
+
+        auto sm = segments_from_points(points, parents, tags);
+        auto mp = mprovider(morphology(sm));
 
         auto h1  = reg::cable(0, 0, 0.5);
         auto h2  = reg::cable(0, 0.5, 1);
@@ -398,25 +378,28 @@ TEST(region, thingify_simple_morphologies) {
     //
     //  sample ids:
     //              0           |
-    //            1   3         |
-    //          2       4       |
+    //              1           |
+    //            2   4         |
+    //          3       5       |
     //  tags:
     //              1           |
+    //              1           |
     //            3   2         |
     //          3       2       |
     {
-        pvec parents = {mnpos, 0, 1, 0, 3};
-        svec samples = {
-            {{  0,  0,  0,  2}, 1},
-            {{ 10,  0,  0,  2}, 3},
-            {{100,  0,  0,  2}, 3},
-            {{  0, 10,  0,  2}, 2},
-            {{  0,100,  0,  2}, 2},
+        pvec parents = {mnpos, 0, 1, 2, 1, 4};
+        svec points = {
+            {  0,  0,  0,  5},
+            { 10,  0,  0,  5},
+            { 10,  0,  0,  2},
+            {100,  0,  0,  2},
+            { 10,  0,  0,  2},
+            {100,  0,  0,  2},
         };
+        tvec tags = {1, 1, 3, 3, 2, 2};
 
-        // Build morphology with a spherical root
-        sample_tree sm(samples, parents);
-        mprovider mp(morphology(sm, true));
+        auto sm = segments_from_points(points, parents, tags);
+        auto mp = mprovider(morphology(sm));
 
         using ls::location;
         using reg::tagged;
@@ -431,9 +414,9 @@ TEST(region, thingify_simple_morphologies) {
         locset end1_   = location(1,1);
 
         auto reg0_ = distal_interval(start1_, 45);
-        auto reg1_ = distal_interval(mid0_,   74);
+        auto reg1_ = distal_interval(mid0_,   50);
         auto reg2_ = proximal_interval(end1_, 45);
-        auto reg3_ = proximal_interval(end1_, 91);
+        auto reg3_ = proximal_interval(end1_, 95);
         auto reg4_ = distal_interval(end1_, 100);
         auto reg5_ = distal_interval(start1_, 0);
         auto reg6_ = proximal_interval(start1_, 0);
@@ -444,9 +427,9 @@ TEST(region, thingify_simple_morphologies) {
         EXPECT_TRUE(region_eq(mp, join(tagged(1), tagged(2), tagged(3)), mcable_list{{0,0,1}, {1,0,1}, {2,0,1}}));
         EXPECT_TRUE(region_eq(mp, join(tagged(1), tagged(2), tagged(3)), all()));
         EXPECT_TRUE(region_eq(mp, reg0_, mcable_list{{1,0,0.5}}));
-        EXPECT_TRUE(region_eq(mp, reg1_, mcable_list{{0,0.5,1}, {1,0,0.8}, {2,0,0.8}}));
+        EXPECT_TRUE(region_eq(mp, reg1_, mcable_list{{0,0.5,1}, {1,0,0.5}, {2,0,0.5}}));
         EXPECT_TRUE(region_eq(mp, reg2_, mcable_list{{1,0.5,1}}));
-        EXPECT_TRUE(region_eq(mp, reg3_, mcable_list{{0, 0.75, 1}, {1,0,1}}));
+        EXPECT_TRUE(region_eq(mp, reg3_, mcable_list{{0, 0.5, 1}, {1,0,1}}));
         EXPECT_TRUE(region_eq(mp, reg4_, mcable_list{{1,1,1}}));
         EXPECT_TRUE(region_eq(mp, reg5_, mcable_list{{1,0,0}}));
         EXPECT_TRUE(region_eq(mp, reg6_, mcable_list{{1,0,0}}));
@@ -455,7 +438,8 @@ TEST(region, thingify_simple_morphologies) {
 
 TEST(region, thingify_moderate_morphologies) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
+    using tvec = std::vector<int>;
     using cl = mcable_list;
 
     // Test multi-level morphologies.
@@ -476,20 +460,21 @@ TEST(region, thingify_moderate_morphologies) {
     //                3
     {
         pvec parents = {mnpos, 0, 1, 0, 3, 4, 4, 6};
-        svec samples = {
-            {{  0,  0,  0,  1}, 1},
-            {{ 10,  0,  0,  1}, 3},
-            {{100,  0,  0,  3}, 3},
-            {{  0, 10,  0,  1}, 2},
-            {{  0,100,  0,  5}, 2},
-            {{100,100,  0,  2}, 4},
-            {{  0,200,  0,  1}, 3},
-            {{  0,300,  0,  3}, 3},
+        svec points = {
+            {  0,  0,  0,  1},
+            { 10,  0,  0,  1},
+            {100,  0,  0,  3},
+            {  0, 10,  0,  1},
+            {  0,100,  0,  5},
+            {100,100,  0,  2},
+            {  0,200,  0,  1},
+            {  0,300,  0,  3},
         };
-        sample_tree sm(samples, parents);
+        tvec tags = {1, 3, 3, 2, 2, 4, 3, 3};
+        auto sm = segments_from_points(points, parents, tags);
 
         // Without spherical root
-        mprovider mp(morphology(sm, false));
+        auto mp = mprovider(morphology(sm));
 
         using ls::location;
         using reg::tagged;
@@ -764,25 +749,26 @@ TEST(region, thingify_moderate_morphologies) {
 
 TEST(region, thingify_complex_morphologies) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
     {
         pvec parents = {mnpos, 0, 1, 0, 3, 4, 5, 5, 7, 7, 4, 10};
-        svec samples = {
-                {{  0,  0,  0,  2}, 3}, //0
-                {{ 10,  0,  0,  2}, 3}, //1
-                {{100,  0,  0,  2}, 3}, //2
-                {{  0, 10,  0,  2}, 3}, //3
-                {{  0,100,  0,  2}, 3}, //4
-                {{100,100,  0,  2}, 3}, //5
-                {{200,100,  0,  2}, 3}, //6
-                {{100,200,  0,  2}, 3}, //7
-                {{200,200,  0,  2}, 3}, //8
-                {{100,300,  0,  2}, 3}, //9
-                {{  0,200,  0,  2}, 3}, //10
-                {{  0,300,  0,  2}, 3}, //11
+        svec points = {
+                {  0,  0,  0,  2}, //0
+                { 10,  0,  0,  2}, //1
+                {100,  0,  0,  2}, //2
+                {  0, 10,  0,  2}, //3
+                {  0,100,  0,  2}, //4
+                {100,100,  0,  2}, //5
+                {200,100,  0,  2}, //6
+                {100,200,  0,  2}, //7
+                {200,200,  0,  2}, //8
+                {100,300,  0,  2}, //9
+                {  0,200,  0,  2}, //10
+                {  0,300,  0,  2}, //11
         };
-        sample_tree sm(samples, parents);
-        auto m = morphology(sm, false);
+
+        auto sm = segments_from_points(points, parents);
+        auto m = morphology(sm);
         {
             mprovider mp(m);
             using reg::cable;
@@ -818,23 +804,23 @@ TEST(region, thingify_complex_morphologies) {
     }
     {
         pvec parents = {mnpos, 0, 1, 1, 2, 3, 0, 6, 7, 8, 7};
-        svec samples = {
-                {{  0, 10, 10,  1}, 1},
-                {{  0, 30, 30,  1}, 2},
-                {{  0, 60,-20,  1}, 2},
-                {{  0, 90, 70,  1}, 2},
-                {{  0, 80,-10,  1}, 2},
-                {{  0,100,-40,  1}, 2},
-                {{  0,-50,-50,  1}, 2},
-                {{  0, 20,-30,  2}, 2},
-                {{  0, 40,-80,  2}, 2},
-                {{  0,-30,-80,  3}, 2},
-                {{  0, 90,-70,  5}, 2}
+        svec points = {
+                {0, 10, 10, 1},
+                {0, 30, 30, 1},
+                {0, 60,-20, 1},
+                {0, 90, 70, 1},
+                {0, 80,-10, 1},
+                {0,100,-40, 1},
+                {0,-50,-50, 1},
+                {0, 20,-30, 2},
+                {0, 40,-80, 2},
+                {0,-30,-80, 3},
+                {0, 90,-70, 5},
         };
-        sample_tree sm(samples, parents);
+        auto sm = segments_from_points(points, parents);
 
         // Without spherical root
-        mprovider mp(morphology(sm, false));
+        auto mp = mprovider(morphology(sm));
 
         using reg::all;
         using reg::nil;
diff --git a/test/unit/test_morph_place.cpp b/test/unit/test_morph_place.cpp
index 7ba2308efc8a54301f58b375b3ceb3ca3c94f502..dc7bebb5eb3a743fc05519a9e1e75cbc68fe4490 100644
--- a/test/unit/test_morph_place.cpp
+++ b/test/unit/test_morph_place.cpp
@@ -5,12 +5,12 @@
 #include <arbor/morph/place_pwlin.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
-#include <arbor/morph/sample_tree.hpp>
 
 #include "util/piecewise.hpp"
 
 #include "../test/gtest.h"
 #include "common.hpp"
+#include "common_cells.hpp"
 
 using namespace arb;
 
@@ -183,21 +183,21 @@ TEST(isometry, compose) {
 
 TEST(place_pwlin, cable) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
 
     // L-shaped simple cable.
     // 0.25 of the length in the z-direction,
     // 0.75 of the length in the x-direction.
 
     pvec parents = {mnpos, 0, 1};
-    svec samples = {
-        {{  0,  0,  0,  2}, 1},
-        {{  0,  0,  1,  2}, 1},
-        {{  3,  0,  1,  2}, 1}
+    svec points = {
+        {0,  0,  0,  2},
+        {0,  0,  1,  2},
+        {3,  0,  1,  2}
     };
 
-    sample_tree sm(samples, parents);
-    morphology m(sm, false);
+    auto sm = segments_from_points(points, parents);
+    morphology m(sm);
 
     {
         // With no transformation:
@@ -251,28 +251,28 @@ TEST(place_pwlin, cable) {
 
 TEST(place_pwlin, branched) {
     using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
+    using svec = std::vector<mpoint>;
 
     // Y-shaped branched morphology.
     // Second branch (branch 1) tapers radius.
 
     pvec parents = {mnpos, 0, 1, 2, 3, 4, 2, 6};
-    svec samples = {
+    svec points = {
         // branch 0
-        {{  0,  0,  0,  2}, 1},
-        {{  0,  0,  1,  2}, 1},
-        {{  3,  0,  1,  2}, 1},
+        { 0,  0,  0,  2},
+        { 0,  0,  1,  2},
+        { 3,  0,  1,  2},
         // branch 1
-        {{  3,  0,  1,  1.0}, 1},
-        {{  3,  1,  1,  1.0}, 1},
-        {{  3,  2,  1,  0.0}, 1},
+        { 3,  0,  1,  1.0},
+        { 3,  1,  1,  1.0},
+        { 3,  2,  1,  0.0},
         // branch 2
-        {{  3,  0,  1,  2} , 1},
-        {{  3,  -1, 1,  2} , 1}
+        { 3,  0,  1,  2},
+        { 3,  -1, 1,  2}
     };
 
-    sample_tree sm(samples, parents);
-    morphology m(sm, false);
+    auto sm = segments_from_points(points, parents);
+    morphology m(sm);
 
     isometry iso = isometry::translate(2, 3, 4);
     place_pwlin pl(m, iso);
diff --git a/test/unit/test_morph_primitives.cpp b/test/unit/test_morph_primitives.cpp
index c627e589b9fe31bb10c7f25f2b6eea7012ef3a6b..c077cdde39e78ffa3ecfaf914664accd913c92fe 100644
--- a/test/unit/test_morph_primitives.cpp
+++ b/test/unit/test_morph_primitives.cpp
@@ -34,12 +34,6 @@ TEST(morph_primitives, is_collocated) {
     EXPECT_FALSE(is_collocated(mpoint{1., 2., 2.5, 4.}, mpoint{1., 2., 3., 4}));
     EXPECT_FALSE(is_collocated(mpoint{1., 2.5, 3., 4.}, mpoint{1., 2., 3., 4}));
     EXPECT_FALSE(is_collocated(mpoint{2.5, 2, 3., 4.}, mpoint{1., 2., 3., 4}));
-
-    EXPECT_TRUE(is_collocated(msample{{1., 2., 3., 4.},3}, msample{{1., 2., 3., 4.},3}));
-    EXPECT_TRUE(is_collocated(msample{{1., 2., 3., 4.},3}, msample{{1., 2., 3., 4.5},1}));
-    EXPECT_FALSE(is_collocated(msample{{1., 2., 2.5, 4.},3}, msample{{1., 2., 3., 4},3}));
-    EXPECT_FALSE(is_collocated(msample{{1., 2.5, 3., 4.},3}, msample{{1., 2., 3., 4},3}));
-    EXPECT_FALSE(is_collocated(msample{{2.5, 2, 3., 4.},3}, msample{{1., 2., 3., 4},3}));
 }
 
 TEST(morph_primitives, distance) {
diff --git a/test/unit/test_morphology.cpp b/test/unit/test_morphology.cpp
index 48d987488bac96473b15ec5fb95f09c3b47d7fa1..5cc929b611b6837b0156fa19c85a8876a2b76858 100644
--- a/test/unit/test_morphology.cpp
+++ b/test/unit/test_morphology.cpp
@@ -7,10 +7,10 @@
 
 #include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/morphology.hpp>
-#include <arbor/morph/sample_tree.hpp>
 #include <arbor/cable_cell.hpp>
 
-#include "morph/mbranch.hpp"
+#include "arbor/morph/primitives.hpp"
+#include "arbor/morph/segment_tree.hpp"
 #include "util/span.hpp"
 
 #include "morph_pred.hpp"
@@ -36,348 +36,111 @@ TEST(morphology, mpoint) {
     EXPECT_FALSE(arb::is_collocated(mp{2,0,1}, mp{2,0,3}));
 }
 
-TEST(morphology, point_props) {
-    arb::point_prop p = arb::point_prop_mask_none;
-
-    EXPECT_FALSE(arb::is_terminal(p));
-    EXPECT_FALSE(arb::is_fork(p));
-    EXPECT_FALSE(arb::is_root(p));
-    EXPECT_FALSE(arb::is_collocated(p));
-
-    arb::set_root(p);
-    EXPECT_FALSE(arb::is_terminal(p));
-    EXPECT_FALSE(arb::is_fork(p));
-    EXPECT_TRUE(arb::is_root(p));
-    EXPECT_FALSE(arb::is_collocated(p));
-
-    arb::set_terminal(p);
-    EXPECT_TRUE(arb::is_terminal(p));
-    EXPECT_FALSE(arb::is_fork(p));
-    EXPECT_TRUE(arb::is_root(p));
-    EXPECT_FALSE(arb::is_collocated(p));
-
-    arb::unset_root(p);
-    EXPECT_TRUE(arb::is_terminal(p));
-    EXPECT_FALSE(arb::is_fork(p));
-    EXPECT_FALSE(arb::is_root(p));
-    EXPECT_FALSE(arb::is_collocated(p));
-
-    arb::set_collocated(p);
-    EXPECT_TRUE(arb::is_terminal(p));
-    EXPECT_FALSE(arb::is_fork(p));
-    EXPECT_FALSE(arb::is_root(p));
-    EXPECT_TRUE(arb::is_collocated(p));
-
-    arb::set_fork(p);
-    EXPECT_TRUE(arb::is_terminal(p));
-    EXPECT_TRUE(arb::is_fork(p));
-    EXPECT_FALSE(arb::is_root(p));
-    EXPECT_TRUE(arb::is_collocated(p));
-
-    arb::unset_fork(p);
-    arb::unset_terminal(p);
-    arb::unset_collocated(p);
-    EXPECT_FALSE(arb::is_terminal(p));
-    EXPECT_FALSE(arb::is_fork(p));
-    EXPECT_FALSE(arb::is_root(p));
-    EXPECT_FALSE(arb::is_collocated(p));
-}
-
-// TODO: test sample_tree marks properties correctly
-
-// Test internal function that parses a parent list, and marks
-// each node as either root, sequential, fork or terminal.
-TEST(sample_tree, properties) {
-    const auto npos = arb::mnpos;
-    using arb::sample_tree;
-    using pp = arb::point_prop;
-    using pvec = std::vector<arb::msize_t>;
-
-    pp c = arb::point_prop_mask_collocated;
-    pp r = arb::point_prop_mask_root;
-    pp t = arb::point_prop_mask_terminal;
-    pp s = arb::point_prop_mask_none;
-    pp f = arb::point_prop_mask_fork;
-    pp tc = t+c;
-    pp sc = s+c;
-    pp fc = f+c;
-
-    // make a sample tree from a parent vector with non-collocated points.
-    auto make_tree = [] (const pvec& parents) {
-        sample_tree st;
-        for (auto p: parents) st.append(p, {{0.,0.,double(st.size()),1.}, 1});
-        return st;
-    };
-    // make a sample tree from a parent vector with collocated points.
-    auto make_colloc_tree = [] (const pvec& parents) {
-        sample_tree st;
-        for (auto p: parents) st.append(p, {{0.,0.,0.,1.}, 1});
-        return st;
-    };
-
-    {
-        EXPECT_EQ(make_tree({npos}).properties(), std::vector<pp>{r});
-        EXPECT_EQ(make_colloc_tree({npos}).properties(), std::vector<pp>{r});
-    }
-
-    {
-        EXPECT_EQ(make_tree({npos,0}).properties(), (std::vector<pp>{r,t}));
-        EXPECT_EQ(make_colloc_tree({npos,0}).properties(), (std::vector<pp>{r,tc}));
-    }
-
-    {
-        EXPECT_EQ(make_tree({npos,0,1,2}).properties(), (std::vector<pp>{r,s,s,t}));
-        EXPECT_EQ(make_colloc_tree({npos,0,1,2}).properties(), (std::vector<pp>{r,sc,sc,tc}));
-    }
-
-    {
-        EXPECT_EQ(make_tree({npos,0,1,2,0,4,5}).properties(), (std::vector<pp>{r,s,s,t,s,s,t}));
-        EXPECT_EQ(make_colloc_tree({npos,0,1,2,0,4,5}).properties(), (std::vector<pp>{r,sc,sc,tc,sc,sc,tc}));
-    }
-
-    {
-        EXPECT_EQ(make_tree({npos,0,1,2,3,2,4,4,7}).properties(), (std::vector<pp>{r,s,f,s,f,t,t,s,t}));
-        EXPECT_EQ(make_colloc_tree({npos,0,1,2,3,2,4,4,7}).properties(), (std::vector<pp>{r,sc,fc,sc,fc,tc,tc,sc,tc}));
-    }
-}
-
-TEST(morphology, branches_from_parent_index) {
-    const auto npos = arb::mnpos;
-    using pvec = std::vector<arb::msize_t>;
-    using mb = arb::impl::mbranch;
-
-    // make a sample tree from a parent vector with non-collocated points.
-    auto make_tree = [] (const pvec& parents) {
-        arb::sample_tree st;
-        for (auto p: parents) st.append(p, {{0.,0.,double(st.size()),1.}, 1});
-        return st;
-    };
-
-    {   // single sample: can only be used to build a morphology with one spherical branch
-        pvec parents{npos};
-        auto tree = make_tree(parents);
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), true);
-        EXPECT_EQ(1u, bc.size());
-        EXPECT_EQ(mb({0}, npos), bc[0]);
-
-        // A cable morphology can't be constructed from a single sample.
-        EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), false),
-                     arb::incomplete_branch);
-    }
-    {
-        pvec parents = {npos, 0};
-        auto tree = make_tree(parents);
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false);
-        EXPECT_EQ(1u, bc.size());
-        EXPECT_EQ(mb({0,1}, npos), bc[0]);
-
-        // A morphology can't be constructed with a spherical soma from two samples.
-        EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), true),
-                     arb::incomplete_branch);
-    }
-
-    {
-        pvec parents{npos, 0, 1};
-
-        // With cable soma: one cable with 3 samples.
-        auto tree = make_tree(parents);
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false);
-        EXPECT_EQ(1u, bc.size());
-        EXPECT_EQ(mb({0,1,2},npos), bc[0]);
-
-        // With spherical soma: one sphere and a 2-segment cable.
-        // The cable branch is attached to the sphere (i.e. the sphere is the parent branch).
-        auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true);
-        EXPECT_EQ(2u, bs.size());
-        EXPECT_EQ(mb({0},npos), bs[0]);
-        EXPECT_EQ(mb({1,2},0), bs[1]);
-    }
-
-    {
-        pvec parents{npos, 0, 0};
-        auto tree = make_tree(parents);
-
-        // A spherical root is not valid: each cable branch would have only one sample.
-        EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), true), arb::incomplete_branch);
-
-        // Two cables, with two samples each, with the first sample in each being the root
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false);
-        EXPECT_EQ(2u, bc.size());
-        EXPECT_EQ(mb({0,1},npos), bc[0]);
-        EXPECT_EQ(mb({0,2},npos), bc[1]);
-    }
-
-    {
-        pvec parents{npos, 0, 1, 2};
-        auto tree = make_tree(parents);
-
-        // With cable soma: one cable with 4 samples.
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false);
-        EXPECT_EQ(1u, bc.size());
-        EXPECT_EQ(mb({0,1,2,3},npos), bc[0]);
-
-        // With spherical soma: one sphere and on 3-segment cable.
-        // The cable branch is attached to the sphere (i.e. the sphere is the parent branch).
-        auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true);
-        EXPECT_EQ(2u, bs.size());
-        EXPECT_EQ(mb({0},npos), bs[0]);
-        EXPECT_EQ(mb({1,2,3},0), bs[1]);
-    }
-
-    {
-        pvec parents{npos, 0, 1, 0};
-        auto tree = make_tree(parents);
-
-        // With cable soma: two cables with 3 and 2 samples respectively.
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false);
-        EXPECT_EQ(2u, bc.size());
-        EXPECT_EQ(mb({0,1,2},npos), bc[0]);
-        EXPECT_EQ(mb({0,3},npos), bc[1]);
-
-        // A spherical root is not valid: the second cable branch would have only one sample.
-        EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), true), arb::incomplete_branch);
-    }
-
-    {
-        pvec parents{npos, 0, 1, 0, 3};
-        auto tree = make_tree(parents);
-
-        // With cable soma: two cables with 3 samples each [0,1,2] and [0,3,4]
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false);
-        EXPECT_EQ(2u, bc.size());
-        EXPECT_EQ(mb({0,1,2},npos), bc[0]);
-        EXPECT_EQ(mb({0,3,4},npos), bc[1]);
-
-        // With spherical soma: one sphere and 2 2-sample cables.
-        // each of the cable branches is attached to the sphere (i.e. the sphere is the parent branch).
-        auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true);
-        EXPECT_EQ(3u, bs.size());
-        EXPECT_EQ(mb({0},npos), bs[0]);
-        EXPECT_EQ(mb({1,2},0),  bs[1]);
-        EXPECT_EQ(mb({3,4},0),  bs[2]);
-    }
-
-    {
-        pvec parents{npos, 0, 1, 0, 3, 4, 4, 6};
-        auto tree = make_tree(parents);
-
-        // With cable soma: 4 cables: [0,1,2] [0,3,4] [4,5] [4,6,7]
-        auto bc = arb::impl::branches_from_parent_index(parents, tree.properties(), false);
-        EXPECT_EQ(4u, bc.size());
-        EXPECT_EQ(mb({0,1,2},npos), bc[0]);
-        EXPECT_EQ(mb({0,3,4},npos), bc[1]);
-        EXPECT_EQ(mb({4,5},  1),    bc[2]);
-        EXPECT_EQ(mb({4,6,7},1),    bc[3]);
-
-        // With spherical soma: 1 sphere and 4 cables: [1,2] [3,4] [4,5] [4,6,7]
-        auto bs = arb::impl::branches_from_parent_index(parents, tree.properties(), true);
-        EXPECT_EQ(5u, bs.size());
-        EXPECT_EQ(mb({0},   npos), bs[0]);
-        EXPECT_EQ(mb({1,2}, 0),    bs[1]);
-        EXPECT_EQ(mb({3,4}, 0),    bs[2]);
-        EXPECT_EQ(mb({4,5}, 2),    bs[3]);
-        EXPECT_EQ(mb({4,6,7}, 2),  bs[4]);
-    }
-}
-
 // For different parent index vectors, attempt multiple valid and invalid sample sets.
 TEST(morphology, construction) {
     constexpr auto npos = arb::mnpos;
     using arb::util::make_span;
-    using ms = arb::msample;
+    using mp = arb::mpoint;
     using pvec = std::vector<arb::msize_t>;
     {
         pvec p = {npos, 0};
-        std::vector<ms> s = {
-            {{0.0, 0.0, 0.0, 1.0}, 1},
-            {{0.0, 0.0, 1.0, 1.0}, 1} };
+        std::vector<mp> s = {
+            {0.0, 0.0, 0.0, 1.0},
+            {0.0, 0.0, 1.0, 1.0}};
 
-        arb::sample_tree sm(s, p);
-        auto m = arb::morphology(sm);
+        arb::segment_tree tree;
+        tree.append(npos, s[0], s[1], 1);
+        auto m = arb::morphology(tree);
 
         EXPECT_EQ(1u, m.num_branches());
     }
     {
         pvec p = {npos, 0, 1};
-        { // 2-segment cable (1 seg soma, 1 seg dendrite)
-            std::vector<ms> s = {
-                {{0.0, 0.0, 0.0, 5.0}, 1},
-                {{0.0, 0.0, 5.0, 1.0}, 1},
-                {{0.0, 0.0, 8.0, 1.0}, 2} };
-
-            arb::sample_tree sm(s, p);
-            auto m = arb::morphology(sm);
-
-            EXPECT_EQ(1u, m.num_branches());
-        }
-        { // spherical soma and single-segment cable
-            std::vector<ms> s = {
-                {{0.0, 0.0, 0.0, 5.0}, 1},
-                {{0.0, 0.0, 1.0, 1.0}, 2},
-                {{0.0, 0.0, 8.0, 1.0}, 2} };
-
-            arb::sample_tree sm(s, p);
-            auto m = arb::morphology(sm);
+        // 2-segment cable
+        std::vector<mp> s = {
+            {0.0, 0.0, 0.0, 5.0},
+            {0.0, 0.0, 1.0, 1.0},
+            {0.0, 0.0, 8.0, 1.0} };
+
+        arb::segment_tree sm;
+        sm.append(npos, s[0], s[1], 2);
+        sm.append(   0, s[1], s[2], 2);
+        auto m = arb::morphology(sm);
 
-            EXPECT_EQ(2u, m.num_branches());
-        }
+        EXPECT_EQ(1u, m.num_branches());
     }
     {
         //              0       |
         //            1   3     |
         //          2           |
         pvec p = {npos, 0, 1, 0};
-        {
-            // two cables: 1x2 segments, 1x1 segment.
-            std::vector<ms> s = {
-                {{0.0, 0.0, 0.0, 5.0}, 1},
-                {{0.0, 0.0, 5.0, 1.0}, 1},
-                {{0.0, 0.0, 6.0, 1.0}, 2},
-                {{0.0, 4.0, 0.0, 1.0}, 1}};
-
-            arb::sample_tree sm(s, p);
-            auto m = arb::morphology(sm);
-
-            EXPECT_EQ(2u, m.num_branches());
-        }
-        {
-            // error: spherical soma with a single point cable attached via sample 3
-            std::vector<ms> s = {
-                {{0.0, 0.0, 0.0, 5.0}, 1},
-                {{0.0, 0.0, 5.0, 1.0}, 2},
-                {{0.0, 0.0, 8.0, 1.0}, 2},
-                {{0.0, 5.0, 0.0, 1.0}, 2}};
-
-            arb::sample_tree sm(s, p);
-            EXPECT_THROW((arb::morphology(sm)), arb::incomplete_branch);
-        }
+        // two cables: 1x2 segments, 1x1 segment.
+        std::vector<mp> s = {
+            {0.0, 0.0, 0.0, 5.0},
+            {0.0, 0.0, 5.0, 1.0},
+            {0.0, 0.0, 6.0, 1.0},
+            {0.0, 4.0, 0.0, 1.0}};
+
+        arb::segment_tree tree;
+        tree.append(npos, s[0], s[1], 1);
+        tree.append(   0, s[1], s[2], 2);
+        tree.append(npos, s[0], s[3], 1);
+        auto m = arb::morphology(tree);
+
+        EXPECT_EQ(2u, m.num_branches());
     }
     {
         //              0       |
         //            1   3     |
         //          2       4    |
-        pvec p = {npos, 0, 1, 0, 3};
-        {
-            // two cables: 1x2 segments, 1x1 segment.
-            std::vector<ms> s = {
-                {{0.0, 0.0, 0.0, 5.0}, 1},
-                {{0.0, 0.0, 5.0, 1.0}, 2},
-                {{0.0, 0.0, 8.0, 1.0}, 2},
-                {{0.0, 5.0, 0.0, 1.0}, 2},
-                {{0.0, 8.0, 0.0, 1.0}, 2}};
-
-            arb::sample_tree sm(s, p);
-            auto m = arb::morphology(sm);
-
-            EXPECT_EQ(3u, m.num_branches());
-        }
+
+        // two cables: 2 segments each
+        std::vector<mp> s = {
+            {0.0, 0.0, 0.0, 5.0},
+            {0.0, 0.0, 5.0, 1.0},
+            {0.0, 0.0, 8.0, 1.0},
+            {0.0, 5.0, 0.0, 1.0},
+            {0.0, 8.0, 0.0, 1.0}};
+
+        arb::segment_tree tree;
+        tree.append(npos, s[0], s[1], 1);
+        tree.append(   0, s[1], s[2], 1);
+        tree.append(npos, s[0], s[3], 1);
+        tree.append(   2, s[3], s[4], 1);
+        auto m = arb::morphology(tree);
+
+        EXPECT_EQ(2u, m.num_branches());
+    }
+    {
+        //              0       |
+        //            1   3     |
+        //          2    4  5   |
+        pvec p = {npos, 0, 1, 0};
+        // 4 cables
+        std::vector<mp> s = {
+            {0.0, 0.0, 0.0, 5.0},
+            {0.0, 0.0, 5.0, 1.0},
+            {0.0, 0.0, 6.0, 1.0},
+            {0.0, 4.0, 0.0, 1.0},
+            {0.0, 4.0, 1.0, 1.0},
+            {0.0, 4.0, 2.0, 1.0}};
+
+        arb::segment_tree tree;
+        tree.append(npos, s[0], s[1], 1);
+        tree.append(   0, s[1], s[2], 2);
+        tree.append(npos, s[0], s[3], 1);
+        tree.append(   2, s[3], s[4], 1);
+        tree.append(   2, s[4], s[5], 1);
+        auto m = arb::morphology(tree);
+
+        EXPECT_EQ(4u, m.num_branches());
     }
 }
 
 // test that morphology generates branch child-parent structure correctly.
 TEST(morphology, branches) {
     using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
+    using svec = std::vector<arb::mpoint>;
     auto npos = arb::mnpos;
 
     auto check_terminal_branches = [](const arb::morphology& m) {
@@ -391,13 +154,14 @@ TEST(morphology, branches) {
     };
 
     {
-        // 0
-        pvec parents = {npos};
-        svec samples = {
-            {{ 0,0,0,3}, 1},
+        // 0 - 1
+        svec p = {
+            {0.,0.,0.,3.},
+            {10.,0.,0.,3.},
         };
-        arb::sample_tree sm(samples, parents);
-        arb::morphology m(sm);
+        arb::segment_tree tree;
+        tree.append(npos, p[0], p[1], 1);
+        arb::morphology m(tree);
 
         EXPECT_EQ(1u, m.num_branches());
         EXPECT_EQ(npos, m.branch_parent(0));
@@ -406,93 +170,97 @@ TEST(morphology, branches) {
         check_terminal_branches(m);
     }
     {
-        // 0 - 1
-        pvec parents = {npos, 0};
-        svec samples = {
-            {{ 0,0,0,3}, 1},
-            {{ 10,0,0,3}, 1},
+        // 0 - 1 - 2
+
+        // the morphology is a single unbranched cable.
+        svec p = {
+            {0.,0.,0.,3.},
+            {10.,0.,0.,3.},
+            {100,0,0,3},
         };
-        arb::sample_tree sm(samples, parents);
-        arb::morphology m(sm);
+        arb::segment_tree tree;
+        tree.append(npos, p[0], p[1], 1);
+        tree.append(   0, p[1], p[2], 1);
+        arb::morphology m(tree);
 
         EXPECT_EQ(1u, m.num_branches());
         EXPECT_EQ(npos, m.branch_parent(0));
         EXPECT_EQ(pvec{}, m.branch_children(0));
+        EXPECT_EQ(pvec{0}, m.terminal_branches());
 
         check_terminal_branches(m);
     }
     {
-        // 0 - 1 - 2
-        pvec parents = {npos, 0, 1};
-        {
-            // All samples have same tag -> the morphology is a single unbranched cable.
-            svec samples = {
-                {{ 0,0,0,3}, 1},
-                {{10,0,0,3}, 1},
-                {{100,0,0,3}, 1},
-            };
-            arb::sample_tree sm(samples, parents);
-            arb::morphology m(sm);
-
-            EXPECT_EQ(1u, m.num_branches());
-            EXPECT_EQ(npos, m.branch_parent(0));
-            EXPECT_EQ(pvec{}, m.branch_children(0));
-
-            check_terminal_branches(m);
-        }
-        {
-            // First sample has unique tag -> spherical soma attached to a single-segment cable.
-            svec samples = {
-                {{  0,0,0,10}, 1},
-                {{ 10,0,0, 3}, 3},
-                {{100,0,0, 3}, 3},
-            };
-            arb::sample_tree sm(samples, parents);
-            arb::morphology m(sm);
-
-            EXPECT_EQ(2u, m.num_branches());
-            EXPECT_EQ(npos, m.branch_parent(0));
-            EXPECT_EQ(0u,   m.branch_parent(1));
-            EXPECT_EQ(pvec{1}, m.branch_children(0));
-            EXPECT_EQ(pvec{},  m.branch_children(1));
-
-            check_terminal_branches(m);
-        }
+        // 6 segemnts and six branches.
+        // A single branch at the root that bifurcates, and a further 3 branches
+        // attached to the first bifurcation.
+        //       0      |
+        //      / \     |
+        //     1   2    |
+        //   / | \      |
+        //  3  4  5     |
+
+        svec p = {
+            {0., 0.,0.,3.},
+            {1., 0.,0.,3.},
+            {2., 1.,0.,3.},
+            {2.,-1.,0.,3.},
+            {3., 2.,0.,3.},
+            {3., 1.,0.,3.},
+            {3., 0.,0.,3.},
+        };
+        arb::segment_tree tree;
+        tree.append(npos, p[0], p[1], 1);
+        tree.append(   0, p[1], p[2], 1);
+        tree.append(   0, p[1], p[3], 1);
+        tree.append(   1, p[2], p[4], 1);
+        tree.append(   1, p[2], p[5], 1);
+        tree.append(   1, p[2], p[6], 1);
+        arb::morphology m(tree);
+
+        EXPECT_EQ(6u, m.num_branches());
+        EXPECT_EQ(npos, m.branch_parent(0));
+        EXPECT_EQ(  0u, m.branch_parent(1));
+        EXPECT_EQ(  0u, m.branch_parent(2));
+        EXPECT_EQ(  1u, m.branch_parent(3));
+        EXPECT_EQ(  1u, m.branch_parent(4));
+        EXPECT_EQ(  1u, m.branch_parent(5));
+        EXPECT_EQ((pvec{1,2}), m.branch_children(0));
+        EXPECT_EQ((pvec{3,4,5}), m.branch_children(1));
+        EXPECT_EQ((pvec{}), m.branch_children(2));
+        EXPECT_EQ((pvec{}), m.branch_children(3));
+        EXPECT_EQ((pvec{}), m.branch_children(4));
+        EXPECT_EQ((pvec{}), m.branch_children(5));
+        EXPECT_EQ((pvec{2,3,4,5}), m.terminal_branches());
+
+        check_terminal_branches(m);
     }
     {
-        // 2 - 0 - 1
-        pvec parents = {npos, 0, 0};
-
-        svec samples = {
-            {{  0, 0,0, 5}, 3},
-            {{ 10, 0,0, 5}, 3},
-            {{  0,10,0, 5}, 3},
+        //     0      |
+        //    / \     |
+        //   1   2    |
+
+        svec p = {
+            { 0, 0,0, 5},
+            {10, 0,0, 5},
+            { 0,10,0, 5},
         };
-        arb::sample_tree sm(samples, parents);
-        arb::morphology m(sm);
+        arb::segment_tree tree;
+        tree.append(npos, p[0], p[1], 3);
+        tree.append(npos, p[0], p[2], 3);
+        arb::morphology m(tree);
 
         EXPECT_EQ(2u, m.num_branches());
         EXPECT_EQ(npos, m.branch_parent(0));
         EXPECT_EQ(npos,   m.branch_parent(1));
         EXPECT_EQ(pvec{}, m.branch_children(0));
         EXPECT_EQ(pvec{},  m.branch_children(1));
+        EXPECT_EQ((pvec{0,1}), m.terminal_branches());
 
         check_terminal_branches(m);
     }
     {
-        // 0 - 1 - 2 - 3
-        pvec parents = {npos, 0, 1, 2};
-    }
-    {
-        //              0       |
-        //             / \      |
-        //            1   3     |
-        //           /          |
-        //          2           |
-        pvec parents = {npos, 0, 1, 0};
-    }
-    {
-        // Eight samples
+        // 7 segments, 4 branches
         //
         //              0           |
         //             / \          |
@@ -503,61 +271,37 @@ TEST(morphology, branches) {
         //                5   6     |
         //                     \    |
         //                      7   |
-        pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
-        {
-            svec samples = {
-                {{  0,  0,  0, 10}, 1},
-                {{ 10,  0,  0,  2}, 3},
-                {{100,  0,  0,  2}, 3},
-                {{  0, 10,  0,  2}, 3},
-                {{  0,100,  0,  2}, 3},
-                {{100,100,  0,  2}, 3},
-                {{  0,200,  0,  2}, 3},
-                {{  0,300,  0,  2}, 3},
-            };
-            arb::sample_tree sm(samples, parents);
-            arb::morphology m(sm);
-
-            EXPECT_EQ(5u, m.num_branches());
-            EXPECT_EQ(npos, m.branch_parent(0));
-            EXPECT_EQ(0u,   m.branch_parent(1));
-            EXPECT_EQ(0u,   m.branch_parent(2));
-            EXPECT_EQ(2u,   m.branch_parent(3));
-            EXPECT_EQ(2u,   m.branch_parent(4));
-            EXPECT_EQ((pvec{1,2}), m.branch_children(0));
-            EXPECT_EQ((pvec{}),    m.branch_children(1));
-            EXPECT_EQ((pvec{3,4}), m.branch_children(2));
-            EXPECT_EQ((pvec{}),    m.branch_children(3));
-            EXPECT_EQ((pvec{}),    m.branch_children(4));
-
-            check_terminal_branches(m);
-        }
-        {
-            svec samples = {
-                {{  0,  0,  0, 10}, 3},
-                {{ 10,  0,  0,  2}, 3},
-                {{100,  0,  0,  2}, 3},
-                {{  0, 10,  0,  2}, 3},
-                {{  0,100,  0,  2}, 3},
-                {{100,100,  0,  2}, 3},
-                {{  0,200,  0,  2}, 3},
-                {{  0,300,  0,  2}, 3},
-            };
-            arb::sample_tree sm(samples, parents);
-            arb::morphology m(sm);
-
-            EXPECT_EQ(4u, m.num_branches());
-            EXPECT_EQ(npos, m.branch_parent(0));
-            EXPECT_EQ(npos, m.branch_parent(1));
-            EXPECT_EQ(1u,   m.branch_parent(2));
-            EXPECT_EQ(1u,   m.branch_parent(3));
-            EXPECT_EQ((pvec{}),    m.branch_children(0));
-            EXPECT_EQ((pvec{2,3}), m.branch_children(1));
-            EXPECT_EQ((pvec{}),    m.branch_children(2));
-            EXPECT_EQ((pvec{}),    m.branch_children(3));
-
-            check_terminal_branches(m);
-        }
+        svec p = {
+            {  0,  0,  0, 10},
+            { 10,  0,  0,  2},
+            {100,  0,  0,  2},
+            {  0, 10,  0,  2},
+            {  0,100,  0,  2},
+            {100,100,  0,  2},
+            {  0,200,  0,  2},
+            {  0,300,  0,  2},
+        };
+        arb::segment_tree tree;
+        tree.append(npos, p[0], p[1], 1);
+        tree.append(   0, p[1], p[2], 1);
+        tree.append(npos, p[0], p[3], 1);
+        tree.append(   2, p[3], p[4], 1);
+        tree.append(   3, p[4], p[5], 1);
+        tree.append(   3, p[4], p[6], 1);
+        tree.append(   5, p[6], p[7], 1);
+        arb::morphology m(tree);
+
+        EXPECT_EQ(4u, m.num_branches());
+        EXPECT_EQ(npos, m.branch_parent(0));
+        EXPECT_EQ(npos, m.branch_parent(1));
+        EXPECT_EQ(1u,   m.branch_parent(2));
+        EXPECT_EQ(1u,   m.branch_parent(3));
+        EXPECT_EQ((pvec{}),    m.branch_children(0));
+        EXPECT_EQ((pvec{2,3}), m.branch_children(1));
+        EXPECT_EQ((pvec{}),    m.branch_children(2));
+        EXPECT_EQ((pvec{}),    m.branch_children(3));
+
+        check_terminal_branches(m);
     }
 }
 
@@ -575,116 +319,79 @@ TEST(morphology, swc) {
     // Load swc samples from file.
     auto swc_samples = arb::parse_swc_file(fid);
 
-    // Build a sample_tree from swc samples.
-    auto sm = arb::swc_as_sample_tree(swc_samples);
-    EXPECT_EQ(1058u, sm.size()); // file contains 195 samples
+    // Build a segmewnt_tree from swc samples.
+    auto sm = arb::swc_as_segment_tree(swc_samples);
+    EXPECT_EQ(1057u, sm.size()); // file contains 195 samples
 
     // Test that the morphology contains the expected number of branches.
     auto m = arb::morphology(sm);
-    EXPECT_EQ(31u, m.num_branches());
+    EXPECT_EQ(30u, m.num_branches());
 }
 #endif
 
-TEST(morphology, minset) {
-    using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
-    using ll = arb::mlocation_list;
+arb::morphology make_4_branch_morph() {
+    using svec = std::vector<arb::mpoint>;
     constexpr auto npos = arb::mnpos;
 
-    // Eight samples
-    //                  no-sphere  sphere
-    //          sample   branch    branch
-    //            0         0         0
-    //           1 3       0 1       1 2
-    //          2   4     0   1     1   2
-    //             5 6       2 3       3 4
-    //                7         3         4
-    pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
-    svec samples = {
-        {{  0,  0,  0,  2}, 3},
-        {{ 10,  0,  0,  2}, 3},
-        {{100,  0,  0,  2}, 3},
-        {{  0, 10,  0,  2}, 3},
-        {{  0,100,  0,  2}, 3},
-        {{100,100,  0,  2}, 3},
-        {{  0,200,  0,  2}, 3},
-        {{  0,300,  0,  2}, 3},
+    // Eight points, 7 segments, 3 branches.
+    //          sample   branch
+    //            0         0
+    //           1 3       0 1
+    //          2   4     0   1
+    //             5 6       2 3
+    //                7         3
+    svec p = {
+        {  0,  0,  0,  2},
+        { 10,  0,  0,  2},
+        {100,  0,  0,  2},
+        {  0, 10,  0,  2},
+        {  0,100,  0,  2},
+        {100,100,  0,  2},
+        {  0,200,  0,  2},
+        {  0,300,  0,  2},
     };
-    arb::sample_tree sm(samples, parents);
-    {
-        arb::morphology m(sm, false);
-
-        EXPECT_EQ((ll{}), minset(m, ll{}));
-        EXPECT_EQ((ll{{2,0.1}}), minset(m, ll{{2,0.1}}));
-        EXPECT_EQ((ll{{0,0.5}, {1,0.5}}), minset(m, ll{{0,0.5}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}}), minset(m, ll{{0,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {1,0}}), minset(m, ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {1,0.5}}), minset(m, ll{{0,0}, {0,0.5}, {1,0.5}, {2,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {2,0.5}}), minset(m, ll{{0,0}, {0,0.5}, {2,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {2,0.5}, {3,0}}), minset(m, ll{{0,0}, {0,0.5}, {2,0.5}, {3,0}, {3,1}}));
-        EXPECT_EQ((ll{{0,0}, {1,0}}), minset(m, ll{{0,0}, {0,0.5}, {1,0}, {2,0.5}, {3,0}, {3,1}}));
-    }
-    {
-        arb::morphology m(sm, true);
-
-        EXPECT_EQ((ll{}), minset(m, ll{}));
-        EXPECT_EQ((ll{{2,0.1}}), minset(m, ll{{2,0.1}}));
-        EXPECT_EQ((ll{{0,0.5}}), minset(m, ll{{0,0.5}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}}), minset(m, ll{{0,0.5}}));
-        EXPECT_EQ((ll{{0,0}}), minset(m, ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
-        EXPECT_EQ((ll{{1,0.5}, {3,0.1}, {4,0.5}}), minset(m, ll{{1,0.5}, {1,1}, {3,0.1}, {4,0.5}, {4,0.7}}));
-    }
+    arb::segment_tree tree;
+    tree.append(npos, p[0], p[1], 1);
+    tree.append(   0, p[1], p[2], 1);
+    tree.append(npos, p[0], p[3], 1);
+    tree.append(   2, p[3], p[4], 1);
+    tree.append(   3, p[4], p[5], 1);
+    tree.append(   3, p[4], p[6], 1);
+    tree.append(   4, p[6], p[7], 1);
+
+    return arb::morphology(tree);
+}
+
+TEST(morphology, minset) {
+    auto m = make_4_branch_morph();
+
+    using ll = arb::mlocation_list;
+
+    EXPECT_EQ((ll{}), minset(m, ll{}));
+    EXPECT_EQ((ll{{2,0.1}}), minset(m, ll{{2,0.1}}));
+    EXPECT_EQ((ll{{0,0.5}, {1,0.5}}), minset(m, ll{{0,0.5}, {1,0.5}}));
+    EXPECT_EQ((ll{{0,0.5}}), minset(m, ll{{0,0.5}}));
+    EXPECT_EQ((ll{{0,0}, {1,0}}), minset(m, ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
+    EXPECT_EQ((ll{{0,0}, {1,0.5}}), minset(m, ll{{0,0}, {0,0.5}, {1,0.5}, {2,0.5}}));
+    EXPECT_EQ((ll{{0,0}, {2,0.5}}), minset(m, ll{{0,0}, {0,0.5}, {2,0.5}}));
+    EXPECT_EQ((ll{{0,0}, {2,0.5}, {3,0}}), minset(m, ll{{0,0}, {0,0.5}, {2,0.5}, {3,0}, {3,1}}));
+    EXPECT_EQ((ll{{0,0}, {1,0}}), minset(m, ll{{0,0}, {0,0.5}, {1,0}, {2,0.5}, {3,0}, {3,1}}));
 }
 
 TEST(morphology, maxset) {
-    using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
+    auto m = make_4_branch_morph();
+
     using ll = arb::mlocation_list;
-    constexpr auto npos = arb::mnpos;
 
-    // Eight samples
-    //                  no-sphere  sphere
-    //          sample   branch    branch
-    //            0         0         0
-    //           1 3       0 1       1 2
-    //          2   4     0   1     1   2
-    //             5 6       2 3       3 4
-    //                7         3         4
-    pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
-    svec samples = {
-        {{  0,  0,  0,  2}, 3},
-        {{ 10,  0,  0,  2}, 3},
-        {{100,  0,  0,  2}, 3},
-        {{  0, 10,  0,  2}, 3},
-        {{  0,100,  0,  2}, 3},
-        {{100,100,  0,  2}, 3},
-        {{  0,200,  0,  2}, 3},
-        {{  0,300,  0,  2}, 3},
-    };
-    arb::sample_tree sm(samples, parents);
-    {
-        arb::morphology m(sm, false);
-
-        EXPECT_EQ((ll{}), maxset(m, ll{}));
-        EXPECT_EQ((ll{{2,0.1}}), maxset(m, ll{{2,0.1}}));
-        EXPECT_EQ((ll{{0,0.5}, {1,0.5}}), maxset(m, ll{{0,0.5}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}}), maxset(m, ll{{0,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}, {1,0.5}}), maxset(m, ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}, {2,0.5}}), maxset(m, ll{{0,0}, {0,0.5}, {1,0.5}, {2,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}, {2,0.5}}), maxset(m, ll{{0,0}, {0,0.5}, {2,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}, {2,0.5}, {3,1}}), maxset(m, ll{{0,0}, {0,0.5}, {2,0.5}, {3,0}, {3,1}}));
-        EXPECT_EQ((ll{{2,0.5}, {3,0.5}}), maxset(m, ll{{1,0.5}, {2,0.5}, {3,0}, {3,0.2}, {3,0.5}}));
-    }
-    {
-        arb::morphology m(sm, true);
-
-        EXPECT_EQ((ll{}), maxset(m, ll{}));
-        EXPECT_EQ((ll{{2,0.1}}), maxset(m, ll{{2,0.1}}));
-        EXPECT_EQ((ll{{1,0.5}}), maxset(m, ll{{0,0.5}, {1,0.5}}));
-        EXPECT_EQ((ll{{1,0}, {2,0}}), maxset(m, ll{{2,0}, {1,0}, {0,0}}));
-        EXPECT_EQ((ll{{1,0.5}}), maxset(m, ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
-        EXPECT_EQ((ll{{1,1}, {3,0.1}, {4,0.7}}), maxset(m, ll{{1,0.5}, {1,1}, {3,0.1}, {4,0.5}, {4,0.7}}));
-    }
+    EXPECT_EQ((ll{}), maxset(m, ll{}));
+    EXPECT_EQ((ll{{2,0.1}}), maxset(m, ll{{2,0.1}}));
+    EXPECT_EQ((ll{{0,0.5}, {1,0.5}}), maxset(m, ll{{0,0.5}, {1,0.5}}));
+    EXPECT_EQ((ll{{0,0.5}}), maxset(m, ll{{0,0.5}}));
+    EXPECT_EQ((ll{{0,0.5}, {1,0.5}}), maxset(m, ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
+    EXPECT_EQ((ll{{0,0.5}, {2,0.5}}), maxset(m, ll{{0,0}, {0,0.5}, {1,0.5}, {2,0.5}}));
+    EXPECT_EQ((ll{{0,0.5}, {2,0.5}}), maxset(m, ll{{0,0}, {0,0.5}, {2,0.5}}));
+    EXPECT_EQ((ll{{0,0.5}, {2,0.5}, {3,1}}), maxset(m, ll{{0,0}, {0,0.5}, {2,0.5}, {3,0}, {3,1}}));
+    EXPECT_EQ((ll{{2,0.5}, {3,0.5}}), maxset(m, ll{{1,0.5}, {2,0.5}, {3,0}, {3,0.2}, {3,0.5}}));
 }
 
 // Testing mextent; intersection and join operations are
@@ -694,32 +401,9 @@ TEST(mextent, invariants) {
     using namespace arb;
     using testing::cablelist_eq;
 
-    using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
-    using cl = mcable_list;
+    auto m = make_4_branch_morph();
 
-    // Eight samples
-    //                  no-sphere
-    //          sample   branch
-    //            0         0
-    //           1 3       0 1
-    //          2   4     0   1
-    //             5 6       2 3
-    //                7         3
-    pvec parents = {mnpos, 0, 1, 0, 3, 4, 4, 6};
-    svec samples = {
-        {{  0,  0,  0,  2}, 3},
-        {{ 10,  0,  0,  2}, 3},
-        {{100,  0,  0,  2}, 3},
-        {{  0, 10,  0,  2}, 3},
-        {{  0,100,  0,  2}, 3},
-        {{100,100,  0,  2}, 3},
-        {{  0,200,  0,  2}, 3},
-        {{  0,300,  0,  2}, 3},
-    };
-
-    // Instantiate morphology with spherical_root=false.
-    morphology m(sample_tree(samples, parents), false);
+    using cl = mcable_list;
 
     mextent x1(cl{{1, 0.25, 0.75}});
     ASSERT_TRUE(x1.test_invariants(m));
@@ -749,31 +433,9 @@ TEST(mextent, intersects) {
     using namespace arb;
     using testing::cablelist_eq;
 
-    using pvec = std::vector<msize_t>;
-    using svec = std::vector<msample>;
-    using cl = mcable_list;
-
-    // Eight samples
-    //                  no-sphere
-    //          sample   branch
-    //            0         0
-    //           1 3       0 1
-    //          2   4     0   1
-    //             5 6       2 3
-    //                7         3
-    pvec parents = {mnpos, 0, 1, 0, 3, 4, 4, 6};
-    svec samples = {
-        {{  0,  0,  0,  2}, 3},
-        {{ 10,  0,  0,  2}, 3},
-        {{100,  0,  0,  2}, 3},
-        {{  0, 10,  0,  2}, 3},
-        {{  0,100,  0,  2}, 3},
-        {{100,100,  0,  2}, 3},
-        {{  0,200,  0,  2}, 3},
-        {{  0,300,  0,  2}, 3},
-    };
+    auto m = make_4_branch_morph();
 
-    morphology m(sample_tree(samples, parents));
+    using cl = mcable_list;
 
     mextent x1(cl{{1, 0.25, 0.75}});
     EXPECT_TRUE(x1.intersects(mlocation{1, 0.25}));
diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp
index ee17642978dd83596772b83ff5a8016884742f14..0d6a48c2826a09d2a99cdb2858968925da8db4ec 100644
--- a/test/unit/test_probe.cpp
+++ b/test/unit/test_probe.cpp
@@ -81,12 +81,17 @@ struct backend_access<gpu::backend> {
 // linearly tapered branches.
 
 static morphology make_y_morphology() {
-    return morphology(sample_tree(
-        {msample{{0., 0., 0., 1.}, 0},
-         msample{{100., 0., 0., 0.8}, 0},
-         msample{{100., 100., 0., 0.5}, 0},
-         msample{{100., 0, 100., 0.4}, 0}},
-        {mnpos, 0u, 1u, 1u}));
+    segment_tree tree;
+    tree.append(mnpos, {0., 0., 0., 1.}, {100., 0., 0., 0.8}, 1);
+    tree.append(0, {100., 100., 0., 0.5}, 0);
+    tree.append(0, {100., 0,  100., 0.4}, 0);
+    return {tree};
+}
+
+static morphology make_stick_morphology() {
+    segment_tree tree;
+    tree.append(mnpos, {0., 0., 0., 1.}, {100., 0., 0., 1.0}, 1);
+    return {tree};
 }
 
 template <typename Backend>
@@ -94,7 +99,11 @@ void run_v_i_probe_test(const context& ctx) {
     using fvm_cell = typename backend_access<Backend>::fvm_cell;
     auto deref = [](const fvm_value_type* p) { return backend_access<Backend>::deref(p); };
 
-    cable_cell bs = make_cell_ball_and_stick(false);
+    soma_cell_builder builder(12.6157/2.0);
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+    cable_cell bs = builder.make_cell();
+
     bs.default_parameters.discretization = cv_policy_fixed_per_branch(1);
 
     i_clamp stim(0, 100, 0.3);
@@ -256,7 +265,10 @@ void run_expsyn_g_probe_test(const context& ctx) {
     mlocation loc0{1, 0.8};
     mlocation loc1{1, 1.0};
 
-    cable_cell bs = make_cell_ball_and_stick(false);
+    soma_cell_builder builder(12.6157/2.0);
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+    cable_cell bs = builder.make_cell();
     bs.place(loc0, "expsyn");
     bs.place(loc1, "expsyn");
     bs.default_parameters.discretization = cv_policy_fixed_per_branch(2);
@@ -485,7 +497,7 @@ void run_ion_density_probe_test(const context& ctx) {
 
     // Simple constant diameter cable, 3 CVs.
 
-    cable_cell cable(sample_tree({msample{{0., 0., 0., 1.}, 0}, msample{{100., 0., 0., 1.}, 0}}, {mnpos, 0u}));
+    cable_cell cable(make_stick_morphology());
     cable.default_parameters.discretization = cv_policy_fixed_per_branch(3);
 
     // Calcium ions everywhere, half written by write_ca1, half by write_ca2.
@@ -647,7 +659,8 @@ void run_partial_density_probe_test(const context& ctx) {
 
     // Each cell is a simple constant diameter cable, with 3 CVs each.
 
-    morphology m(sample_tree({msample{{0., 0., 0., 1.}, 0}, msample{{100., 0., 0., 1.}, 0}}, {mnpos, 0u}));
+    auto m = make_stick_morphology();
+
     cells[0] = cable_cell(m);
     cells[0].default_parameters.discretization = cv_policy_fixed_per_branch(3);
 
@@ -760,7 +773,7 @@ void run_axial_and_ion_current_sampled_probe_test(const context& ctx) {
 
     // Cell is a tapered cable with 3 CVs.
 
-    morphology m(sample_tree({msample{{0., 0., 0., 1.}, 0}, msample{{100., 0., 0., 0.8}, 0}}, {mnpos, 0u}));
+    auto m = make_stick_morphology();
     cable_cell cell(m);
 
     const unsigned n_cv = 3;
@@ -947,7 +960,12 @@ void run_multi_probe_test(const context& ctx) {
 
 template <typename Backend>
 void run_v_sampled_probe_test(const context& ctx) {
-    cable_cell bs = make_cell_ball_and_stick(false);
+    soma_cell_builder builder(12.6157/2.0);
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+    builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+
+    cable_cell bs = builder.make_cell();
+
     bs.default_parameters.discretization = cv_policy_fixed_per_branch(1);
 
     std::vector<cable_cell> cells = {bs, bs};
@@ -1116,7 +1134,11 @@ void run_exact_sampling_probe_test(const context& ctx) {
         adhoc_recipe() {
             gprop_.default_parameters = neuron_parameter_defaults;
 
-            cells_.assign(4, make_cell_ball_and_stick(false));
+            soma_cell_builder builder(12.6157/2.0);
+            builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+            builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
+
+            cells_.assign(4, builder.make_cell());
             cells_[0].place(mlocation{1, 0.1}, "expsyn");
             cells_[1].place(mlocation{1, 0.1}, "exp2syn");
             cells_[2].place(mlocation{1, 0.9}, "expsyn");
diff --git a/test/unit/test_segment_tree.cpp b/test/unit/test_segment_tree.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f5baff1d7aba5163dabbd0f2fd293b138e78098
--- /dev/null
+++ b/test/unit/test_segment_tree.cpp
@@ -0,0 +1,113 @@
+#include <algorithm>
+#include <fstream>
+#include <cmath>
+#include <random>
+#include <string>
+#include <vector>
+
+#include "../test/gtest.h"
+
+#include <arbor/morph/morphexcept.hpp>
+
+#include "arbor/morph/primitives.hpp"
+#include "util/span.hpp"
+
+#include "morph_pred.hpp"
+
+// Test basic functions on properties of mpoint
+TEST(segment_tree, mpoint) {
+    using mp = arb::mpoint;
+
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{0   , 0   , 0   }), 0.);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{3.14, 0   , 0   }), 3.14);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{0   , 3.14, 0   }), 3.14);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{0   , 0   , 3.14}), 3.14);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{1   , 2   , 3   }), std::sqrt(14.));
+
+    EXPECT_TRUE(arb::is_collocated(mp{0,0,0}, mp{0,0,0}));
+    EXPECT_TRUE(arb::is_collocated(mp{3,0,0}, mp{3,0,0}));
+    EXPECT_TRUE(arb::is_collocated(mp{0,3,0}, mp{0,3,0}));
+    EXPECT_TRUE(arb::is_collocated(mp{0,0,3}, mp{0,0,3}));
+    EXPECT_TRUE(arb::is_collocated(mp{2,0,3}, mp{2,0,3}));
+
+    EXPECT_FALSE(arb::is_collocated(mp{1,0,3}, mp{2,0,3}));
+    EXPECT_FALSE(arb::is_collocated(mp{2,1,3}, mp{2,0,3}));
+    EXPECT_FALSE(arb::is_collocated(mp{2,0,1}, mp{2,0,3}));
+}
+
+TEST(segment_tree, empty) {
+    using mp = arb::mpoint;
+    arb::segment_tree tree;
+    EXPECT_TRUE(tree.empty());
+    tree.append(arb::mnpos, mp{0,0,0,1}, mp{0,0,1,1}, 1);
+    EXPECT_FALSE(tree.empty());
+}
+
+TEST(segment_tree, invalid_append) {
+    using mp = arb::mpoint;
+    using arb::mnpos;
+    arb::segment_tree tree;
+
+    // Test that appropriate exceptions are thrown when attempting to create
+    // a segment by implicitly extending from the root.
+    EXPECT_THROW(tree.append(mnpos, mp{0,0,0,1}, 1), arb::invalid_segment_parent);
+
+    tree.append(mnpos, mp{0,0,0,1}, mp{0,0,1,1}, 1);
+
+    EXPECT_THROW(tree.append(mnpos, mp{0,0,0,1}, 1), arb::invalid_segment_parent);
+
+    // Test that an exception is thrown when attempting to append to
+    // a segment that is not in the tree (parent>=num_segs)
+    EXPECT_THROW(tree.append(1, mp{0,0,0,1}, mp{0,0,1,1}, 1), arb::invalid_segment_parent);
+    EXPECT_THROW(tree.append(2, mp{0,0,0,1}, mp{0,0,1,1}, 1), arb::invalid_segment_parent);
+    EXPECT_THROW(tree.append(2, mp{0,0,1,1}, 1), arb::invalid_segment_parent);
+}
+
+// Generate some random morphologies of different sizes, and verify that
+// the correct tree is constructed.
+TEST(segment_tree, fuzz) {
+    using mp = arb::mpoint;
+    using arb::mnpos;
+
+    int nrun = 10;
+    int max_size = 1<<12;
+
+    std::mt19937 gen(0);
+
+    auto make_point = [](arb::msize_t i) {return mp{0,0,double(i),1};};
+
+    for (auto nseg=2; nseg<=max_size; nseg*=2) {
+        for (int run=0; run<nrun; ++run) {
+            arb::segment_tree tree;
+            std::vector<arb::msize_t> parents;
+
+            parents.reserve(nseg);
+            tree.reserve(nseg);
+            for (int i=0; i<nseg; ++i) {
+                std::uniform_int_distribution<int> distrib(-1, i-1);
+
+                // Draw random segment to attach to.
+                arb::msize_t p = distrib(gen);
+                parents.push_back(p);
+
+                // Attach segment.
+                // Use an implicit append every third time when possible.
+                if (p!=mnpos && i%3)
+                    tree.append(p, make_point(i), i);
+                else
+                    tree.append(p, make_point(p), make_point(i), i);
+
+                // Validate that the correct number of segments created.
+                EXPECT_EQ(tree.size(), std::size_t(i+1));
+            }
+            EXPECT_EQ(parents, tree.parents());
+            for (int i=0; i<nseg; ++i) {
+                arb::msegment seg = tree.segments()[i];
+                EXPECT_EQ(seg.prox, make_point(parents[i]));
+                EXPECT_EQ(seg.dist, make_point(i));
+                EXPECT_EQ(seg.tag, i);
+            }
+        }
+    }
+}
+