diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index 2257032d3f4754a461d15d9c837a9ea95c48c05c..24e73acd1a904f874c61252fe01c46735c95871d 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -28,11 +28,13 @@ set(arbor_sources
     mechcat.cpp
     memory/cuda_wrappers.cpp
     memory/util.cpp
-    morph/em_morphology.cpp
+    morph/embed_pwlin.cpp
     morph/label_dict.cpp
     morph/locset.cpp
     morph/mbranch.cpp
+    morph/morphexcept.cpp
     morph/morphology.cpp
+    morph/mprovider.cpp
     morph/primitives.cpp
     morph/region.cpp
     morph/sample_tree.cpp
diff --git a/arbor/algorithms.hpp b/arbor/algorithms.hpp
index ec4b2dc45df025c77ae8ab3d67a555343ea48585..722edd0724c6878c1ecf4c6c7afc97b833259d72 100644
--- a/arbor/algorithms.hpp
+++ b/arbor/algorithms.hpp
@@ -26,19 +26,11 @@
 namespace arb {
 namespace algorithms {
 
-template <typename C>
-typename util::sequence_traits<C>::value_type
-sum(C const& c)
-{
-    using value_type = typename util::sequence_traits<C>::value_type;
-    return std::accumulate(std::cbegin(c), std::cend(c), value_type{0});
-}
-
 template <typename C>
 typename util::sequence_traits<C>::value_type
 mean(C const& c)
 {
-    return sum(c)/util::size(c);
+    return util::sum(c)/util::size(c);
 }
 
 // returns the prefix sum of c in the form `[0, c[0], c[0]+c[1], ..., sum(c)]`.
diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp
index d90094e834713776401cc38a8cc46c0599842a23..6dea2d3991bb90191d8470dd16b64cc46dbe3d02 100644
--- a/arbor/cable_cell.cpp
+++ b/arbor/cable_cell.cpp
@@ -1,18 +1,23 @@
 #include <sstream>
+#include <unordered_map>
 #include <vector>
 
 #include <arbor/cable_cell.hpp>
 #include <arbor/morph/label_dict.hpp>
 #include <arbor/morph/morphology.hpp>
+#include <arbor/morph/mprovider.hpp>
 #include <arbor/segment.hpp>
 
-#include "morph/em_morphology.hpp"
+#include "util/piecewise.hpp"
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
 #include "util/strprintf.hpp"
 
 namespace arb {
 
+using region_map = std::unordered_map<std::string, mcable_list>;
+using locset_map = std::unordered_map<std::string, mlocation_list>;
+
 using value_type = cable_cell::value_type;
 using index_type = cable_cell::index_type;
 using size_type = cable_cell::size_type;
@@ -27,13 +32,10 @@ struct cable_cell_impl {
     using gap_junction_instance = cable_cell::gap_junction_instance;
     using detector_instance     = cable_cell::detector_instance;
 
-    using region_map = cable_cell::region_map;
-    using locset_map = cable_cell::locset_map;
-
     cable_cell_impl(const arb::morphology& m,
                     const label_dict& dictionary,
                     bool compartments_from_discretization):
-        morph(m)
+        provider(m, dictionary)
     {
         using point = cable_cell::point_type;
         if (!m.num_branches()) {
@@ -97,28 +99,18 @@ struct cable_cell_impl {
                 segments.back()->as_cable()->set_compartments(ncomp);
             }
         }
-
-        for (auto& r: dictionary.regions()) {
-            regions[r.first] = thingify(r.second, morph);
-        }
-
-        for (auto& l: dictionary.locsets()) {
-            locations[l.first] = thingify(l.second, morph);
-        }
     }
 
     cable_cell_impl(): cable_cell_impl({},{},false) {}
 
-    cable_cell_impl(const cable_cell_impl& other) {
-        parents = other.parents;
-        stimuli = other.stimuli;
-        synapses = other.synapses;
-        gap_junction_sites = other.gap_junction_sites;
-        spike_detectors = other.spike_detectors;
-        regions = other.regions;
-        morph = other.morph;
-        locations = other.locations;
-
+    cable_cell_impl(const cable_cell_impl& other):
+        parents(other.parents),
+        stimuli(other.stimuli),
+        synapses(other.synapses),
+        gap_junction_sites(other.gap_junction_sites),
+        spike_detectors(other.spike_detectors),
+        provider(other.provider)
+    {
         // unique_ptr's cannot be copy constructed, do a manual assignment
         segments.reserve(other.segments.size());
         for (const auto& s: other.segments) {
@@ -146,14 +138,8 @@ struct cable_cell_impl {
     // the sensors
     std::vector<detector_instance> spike_detectors;
 
-    // Named regions
-    region_map regions;
-
-    // Named location sets
-    locset_map locations;
-
-    // Underlying embedded morphology
-    em_morphology morph;
+    // Embedded morphology and labelled region/locset lookup.
+    mprovider provider;
 
     template <typename Desc, typename T>
     lid_range place(const mlocation_list& locs, const Desc& desc, std::vector<T>& list) {
@@ -167,13 +153,8 @@ struct cable_cell_impl {
     }
 
     template <typename Desc, typename T>
-    lid_range place(const std::string& target, const Desc& desc, std::vector<T>& list) {
-        const auto first = list.size();
-
-        const auto it = locations.find(target);
-        if (it==locations.end()) return lid_range(first, first);
-
-        return place(it->second, desc, list);
+    lid_range place(const locset& locs, const Desc& desc, std::vector<T>& list) {
+        return place(thingify(locs, provider), desc, list);
     }
 
     lid_range place_gj(const mlocation_list& locs) {
@@ -184,13 +165,8 @@ struct cable_cell_impl {
         return lid_range(first, gap_junction_sites.size());
     }
 
-    lid_range place_gj(const std::string& target) {
-        const auto first = gap_junction_sites.size();
-
-        const auto it = locations.find(target);
-        if (it==locations.end()) return lid_range(first, first);
-
-        return place_gj(it->second);
+    lid_range place_gj(const locset& locs) {
+        return place_gj(thingify(locs, provider));
     }
 
     void assert_valid_segment(index_type i) const {
@@ -203,16 +179,6 @@ struct cable_cell_impl {
         return test_invariants(loc) && loc.branch<segments.size();
     }
 
-    template <typename F>
-    void paint(const std::string& target, F&& f) {
-        auto it = regions.find(target);
-
-        // Nothing to do if there are no regions that match.
-        if (it==regions.end()) return;
-
-        paint(it->second, std::forward<F>(f));
-    }
-
     template <typename F>
     void paint(const mcable_list& cables, F&& f) {
         for (auto c: cables) {
@@ -224,6 +190,11 @@ struct cable_cell_impl {
             f(segments[c.branch]);
         }
     }
+
+    template <typename F>
+    void paint(const region& reg, F&& f) {
+        paint(thingify(reg, provider), std::forward<F>(f));
+    }
 };
 
 using impl_ptr = std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)>;
@@ -294,33 +265,32 @@ const std::vector<cable_cell::stimulus_instance>& cable_cell::stimuli() const {
     return impl_->stimuli;
 }
 
-const em_morphology* cable_cell::morphology() const {
-    return &(impl_->morph);
+const concrete_embedding& cable_cell::embedding() const {
+    return impl_->provider.embedding();
+}
+
+const arb::morphology& cable_cell::morphology() const {
+    return impl_->provider.morphology();
 }
 
+const mprovider& cable_cell::provider() const {
+    return impl_->provider;
+}
+
+
 //
 // Painters.
 //
 // Implementation of user API for painting density channel and electrical properties on cells.
 //
 
-void cable_cell::paint(const std::string& target, mechanism_desc desc) {
-    impl_->paint(target,
-                 [&desc](segment_ptr& s){return s->add_mechanism(desc);});
-}
-
-void cable_cell::paint(const std::string& target, cable_cell_local_parameter_set params) {
-    impl_->paint(target,
-                 [&params](segment_ptr& s){return s->parameters = params;});
-}
-
 void cable_cell::paint(const region& target, mechanism_desc desc) {
-    impl_->paint(thingify(target, impl_->morph),
+    impl_->paint(target,
                  [&desc](segment_ptr& s){return s->add_mechanism(desc);});
 }
 
 void cable_cell::paint(const region& target, cable_cell_local_parameter_set params) {
-    impl_->paint(thingify(target, impl_->morph),
+    impl_->paint(target,
                  [&params](segment_ptr& s){return s->parameters = params;});
 }
 
@@ -335,47 +305,32 @@ void cable_cell::paint(const region& target, cable_cell_local_parameter_set para
 // Synapses.
 //
 
-lid_range cable_cell::place(const std::string& target, const mechanism_desc& desc) {
-    return impl_->place(target, desc, impl_->synapses);
-}
-
 lid_range cable_cell::place(const locset& ls, const mechanism_desc& desc) {
-    return impl_->place(thingify(ls, impl_->morph), desc, impl_->synapses);
+    return impl_->place(ls, desc, impl_->synapses);
 }
 
 //
 // Stimuli.
 //
 
-lid_range cable_cell::place(const std::string& target, const i_clamp& desc) {
-    return impl_->place(target, desc, impl_->stimuli);
-}
-
 lid_range cable_cell::place(const locset& ls, const i_clamp& desc) {
-    return impl_->place(thingify(ls, impl_->morph), desc, impl_->stimuli);
+    return impl_->place(ls, desc, impl_->stimuli);
 }
 
 //
 // Gap junctions.
 //
 
-lid_range cable_cell::place(const std::string& target, gap_junction_site) {
-    return impl_->place_gj(target);
-}
-
 lid_range cable_cell::place(const locset& ls, gap_junction_site) {
-    return impl_->place_gj(thingify(ls, impl_->morph));
+    return impl_->place_gj(ls);
 }
 
 //
 // Spike detectors.
 //
-lid_range cable_cell::place(const std::string& target, const threshold_detector& desc) {
-    return impl_->place(target, desc.threshold, impl_->spike_detectors);
-}
 
 lid_range cable_cell::place(const locset& ls, const threshold_detector& desc) {
-    return impl_->place(thingify(ls, impl_->morph), desc.threshold, impl_->spike_detectors);
+    return impl_->place(ls, desc.threshold, impl_->spike_detectors);
 }
 
 //
diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp
index 9a13e1a93ac17a32c69b9387a834653698f9e904..f6843b80b462111474cfbc4cdb6f78934c1112b8 100644
--- a/arbor/cable_cell_param.cpp
+++ b/arbor/cable_cell_param.cpp
@@ -7,7 +7,6 @@
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/morph/locset.hpp>
 
-#include "morph/em_morphology.hpp"
 #include "util/maputil.hpp"
 
 namespace arb {
@@ -72,8 +71,8 @@ cable_cell_local_parameter_set neuron_parameter_defaults = {
 // Discretization policy implementations:
 
 locset cv_policy_max_extent::cv_boundary_points(const cable_cell& cell) const {
-    const auto& emorph = *cell.morphology();
-    const unsigned nbranch = emorph.num_branches();
+    const unsigned nbranch = cell.morphology().num_branches();
+    const auto& embed = cell.embedding();
     if (!nbranch || max_extent_<=0) return ls::nil();
 
     std::vector<mlocation> points;
@@ -87,7 +86,7 @@ locset cv_policy_max_extent::cv_boundary_points(const cable_cell& cell) const {
 
     const double oomax_extent = 1./max_extent_;
     while (bidx<nbranch) {
-        unsigned ncv = std::ceil(emorph.branch_length(bidx)*oomax_extent);
+        unsigned ncv = std::ceil(embed.branch_length(bidx)*oomax_extent);
         double ooncv = 1./ncv;
 
         if (flags_&cv_policy_flag::interior_forks) {
@@ -108,7 +107,7 @@ locset cv_policy_max_extent::cv_boundary_points(const cable_cell& cell) const {
 }
 
 locset cv_policy_fixed_per_branch::cv_boundary_points(const cable_cell& cell) const {
-    const unsigned nbranch = cell.morphology()->num_branches();
+    const unsigned nbranch = cell.morphology().num_branches();
     if (!nbranch) return ls::nil();
 
     std::vector<mlocation> points;
diff --git a/arbor/fvm_compartment.hpp b/arbor/fvm_compartment.hpp
index 4790f595e3f15d07fa192ccd870dda2a8ead0649..9f5713fc9ffb16bb66afaec9a8291c77e8311d7b 100644
--- a/arbor/fvm_compartment.hpp
+++ b/arbor/fvm_compartment.hpp
@@ -7,7 +7,6 @@
 #include <arbor/math.hpp>
 #include <arbor/util/compat.hpp>
 
-#include "algorithms.hpp"
 #include "util/iterutil.hpp"
 #include "util/partition.hpp"
 #include "util/rangeutil.hpp"
@@ -88,7 +87,7 @@ public:
     template <typename Radii, typename Lengths>
     div_compartment_by_ends(size_type n, const Radii& radii, const Lengths& lengths):
         oon_(1/value_type(n)),
-        length_(algorithms::sum(lengths)),
+        length_(util::sum(lengths)),
         ra_(util::front(radii)),
         rb_(util::back(radii))
     {}
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index d6c8ff4f2129d7fdbac954f74ba832a58a8bfb90..9745d922d2f4f4b5c4c2504571ef639a6a79d156 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -1,7 +1,5 @@
 #pragma once
 
-
-#include <unordered_map>
 #include <string>
 #include <vector>
 
@@ -11,6 +9,7 @@
 #include <arbor/constants.hpp>
 #include <arbor/mechcat.hpp>
 #include <arbor/morph/label_dict.hpp>
+#include <arbor/morph/mprovider.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/segment.hpp>
@@ -48,9 +47,6 @@ public:
     using value_type = double;
     using point_type = point<value_type>;
 
-    using region_map = std::unordered_map<std::string, mcable_list>;
-    using locset_map = std::unordered_map<std::string, mlocation_list>;
-
     using gap_junction_instance = mlocation;
 
     struct synapse_instance {
@@ -84,6 +80,11 @@ public:
                const label_dict& dictionary={},
                bool compartments_from_discretization=false);
 
+    /// Access to morphology and embedding
+    const concrete_embedding& embedding() const;
+    const arb::morphology& morphology() const;
+    const mprovider& provider() const;
+
     // the number of branches in the cell
     size_type num_branches() const;
 
@@ -120,48 +121,36 @@ public:
     // LEGACY
     size_type num_compartments() const;
 
-    //
     // Painters and placers.
     //
     // Used to describe regions and locations where density channels, stimuli,
     // synapses, gap juncitons and detectors are located.
-    //
 
     // Density channels.
-    void paint(const std::string&, mechanism_desc);
     void paint(const region&, mechanism_desc);
 
     // Properties.
-    void paint(const std::string&, cable_cell_local_parameter_set);
     void paint(const region&, cable_cell_local_parameter_set);
 
     // Synapses.
-    lid_range place(const std::string&, const mechanism_desc&);
     lid_range place(const locset&, const mechanism_desc&);
 
     // Stimuli.
-    lid_range place(const std::string&, const i_clamp&);
     lid_range place(const locset&, const i_clamp&);
 
     // Gap junctions.
-    lid_range place(const std::string&, gap_junction_site);
     lid_range place(const locset&, gap_junction_site);
 
-    // spike detectors
-    lid_range place(const std::string&, const threshold_detector&);
+    // Spike detectors.
     lid_range place(const locset&, const threshold_detector&);
 
-    //
-    // access to placed items
-    //
+    // Access to placed items.
 
     const std::vector<synapse_instance>& synapses() const;
     const std::vector<gap_junction_instance>& gap_junction_sites() const;
     const std::vector<detector_instance>& detectors() const;
     const std::vector<stimulus_instance>& stimuli() const;
 
-    const em_morphology* morphology() const;
-
     // Checks that two cells have the same
     //  - number and type of segments
     //  - volume and area properties of each segment
@@ -184,7 +173,6 @@ public:
         const cable_cell_parameter_set& global_defaults) const;
 
 private:
-
     std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)> impl_;
 };
 
diff --git a/arbor/include/arbor/morph/embed_pwlin.hpp b/arbor/include/arbor/morph/embed_pwlin.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..82d83a570bc6f95d163acd8d838869f1a47a9591
--- /dev/null
+++ b/arbor/include/arbor/morph/embed_pwlin.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+// Embedding of cell morphology as 1-d tree with piecewise linear radius.
+
+#include <vector>
+
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
+
+namespace arb {
+
+struct embed_pwlin_data;
+
+namespace util {
+template <typename X> struct pw_elements;
+}
+
+// Piecewise-constant functions are represented as scalar (double)
+// values defined over contiguous intervals.
+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);
+    }
+
+    // Interpolated radius at location.
+    double radius(mlocation) const;
+
+    // Computed length of mcable.
+    double integrate_length(mcable c) const;
+    double integrate_length(msize_t bid, const pw_constant_fn&) const;
+
+    // Membrane surface area of given mcable.
+    double integrate_area(mcable c) const;
+    double integrate_area(msize_t bid, const pw_constant_fn&) const;
+
+    // Integrated inverse cross-sectional area of given mcable.
+    double integrate_ixa(mcable c) const;
+    double integrate_ixa(msize_t bid, const pw_constant_fn&) const;
+
+    // Length of whole branch.
+    double branch_length(msize_t bid) const {
+        return integrate_length(mcable{bid, 0, 1});
+    }
+
+private:
+    std::vector<mlocation> sample_locations_;
+    std::shared_ptr<embed_pwlin_data> data_;
+};
+
+} // namespace arb
+
+
diff --git a/arbor/include/arbor/morph/error.hpp b/arbor/include/arbor/morph/error.hpp
deleted file mode 100644
index 14c40be496cc6be0b8873072a3e18433876c3a4d..0000000000000000000000000000000000000000
--- a/arbor/include/arbor/morph/error.hpp
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma once
-
-#include <stdexcept>
-
-#include <arbor/arbexcept.hpp>
-
-namespace arb {
-
-struct morphology_error: public arbor_exception {
-    morphology_error(const char* what): arbor_exception(what) {}
-    morphology_error(const std::string& what): arbor_exception(what) {}
-};
-
-} // namespace arb
-
diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp
index a9a64cbb663122577d07fe19ff076eb115efac26..2e39b39f03096c2450f51b24fa364e3a07df5d66 100644
--- a/arbor/include/arbor/morph/locset.hpp
+++ b/arbor/include/arbor/morph/locset.hpp
@@ -13,9 +13,7 @@
 
 namespace arb {
 
-// Forward declare the backend em_morphology type, required for defining the
-// interface for concretising locsets.
-class em_morphology;
+struct mprovider;
 
 class locset;
 
@@ -46,6 +44,10 @@ public:
     // Construct an explicit location set with a single location.
     locset(mlocation other);
 
+    // Implicitly convert string to named locset expression.
+    locset(std::string label);
+    locset(const char* label);
+
     template <typename Impl,
               typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, locset>::value>>
     locset& operator=(Impl&& other) {
@@ -59,7 +61,7 @@ public:
         return *this;
     }
 
-    friend mlocation_list thingify(const locset& p, const em_morphology& m) {
+    friend mlocation_list thingify(const locset& p, const mprovider& m) {
         return p.impl_->thingify(m);
     }
 
@@ -88,7 +90,7 @@ private:
         virtual ~interface() {}
         virtual std::unique_ptr<interface> clone() = 0;
         virtual std::ostream& print(std::ostream&) = 0;
-        virtual mlocation_list thingify(const em_morphology&) = 0;
+        virtual mlocation_list thingify(const mprovider&) = 0;
     };
 
     std::unique_ptr<interface> impl_;
@@ -102,7 +104,7 @@ private:
             return std::unique_ptr<interface>(new wrap<Impl>(wrapped));
         }
 
-        virtual mlocation_list thingify(const em_morphology& m) override {
+        virtual mlocation_list thingify(const mprovider& m) override {
             return thingify_(wrapped, m);
         }
 
@@ -128,6 +130,9 @@ locset terminal();
 // The root node of a morphology.
 locset root();
 
+// Named locset.
+locset named(std::string);
+
 // The null (empty) set.
 locset nil();
 
diff --git a/arbor/include/arbor/morph/morphexcept.hpp b/arbor/include/arbor/morph/morphexcept.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2c97947daeb420b9cbd01b76221778160cda8e47
--- /dev/null
+++ b/arbor/include/arbor/morph/morphexcept.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <string>
+
+#include <arbor/arbexcept.hpp>
+#include <arbor/morph/primitives.hpp>
+
+namespace arb {
+
+struct morphology_error: public arbor_exception {
+    morphology_error(const std::string& what): arbor_exception(what) {}
+};
+
+struct invalid_mlocation: morphology_error {
+    invalid_mlocation(mlocation loc);
+    mlocation loc;
+};
+
+struct no_such_branch: morphology_error {
+    no_such_branch(msize_t bid);
+    msize_t bid;
+};
+
+struct invalid_mcable: morphology_error {
+    invalid_mcable(mcable cable);
+    mcable cable;
+};
+
+struct invalid_sample_parent: morphology_error {
+    invalid_sample_parent(msize_t parent, msize_t tree_size);
+    msize_t parent;
+    msize_t tree_size;
+};
+
+struct label_type_mismatch: morphology_error {
+    label_type_mismatch(const std::string& label);
+    std::string label;
+};
+
+struct incomplete_branch: morphology_error {
+    incomplete_branch(msize_t bid);
+    msize_t bid;
+};
+
+struct unbound_name: morphology_error {
+    unbound_name(const std::string& name);
+    std::string name;
+};
+
+struct circular_definition: morphology_error {
+    circular_definition(const std::string& name);
+    std::string name;
+};
+
+} // namespace arb
+
diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp
index 4e90d30a483cd03ebf53da76450622f02403cb64..bd36d65fb78e7ba3ee718b30b8e1a44b68294840 100644
--- a/arbor/include/arbor/morph/morphology.hpp
+++ b/arbor/include/arbor/morph/morphology.hpp
@@ -41,6 +41,9 @@ public:
     // The child branches of branch b.
     const std::vector<msize_t>& branch_children(msize_t b) const;
 
+    // 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;
 
@@ -56,4 +59,9 @@ public:
     friend std::ostream& operator<<(std::ostream&, const morphology&);
 };
 
+// Morphology utility functions.
+mlocation_list minset(const morphology&, const mlocation_list&);
+
+mlocation canonical(const morphology&, mlocation);
+
 } // namespace arb
diff --git a/arbor/include/arbor/morph/mprovider.hpp b/arbor/include/arbor/morph/mprovider.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..727550ccc957cce8700ef872e85bcbff2a8fb5be
--- /dev/null
+++ b/arbor/include/arbor/morph/mprovider.hpp
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+
+#include <arbor/morph/embed_pwlin.hpp>
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/label_dict.hpp>
+#include <arbor/util/either.hpp>
+
+namespace arb {
+
+using concrete_embedding = embed_pwlin;
+
+struct mprovider {
+    mprovider(arb::morphology m, const label_dict& dict): mprovider(m, &dict) {}
+    explicit mprovider(arb::morphology m): mprovider(m, nullptr) {}
+
+    // Throw exception on missing or recursive definition.
+    const mcable_list& region(const std::string& name) const;
+    const mlocation_list& locset(const std::string& name) const;
+
+    // Read-only access to morphology and constructed embedding.
+    const auto& morphology() const { return morphology_; }
+    const auto& embedding() const { return embedding_; }
+
+private:
+    mprovider(arb::morphology m, const label_dict* ldptr):
+        morphology_(m), embedding_(m), label_dict_ptr(ldptr) { init(); }
+
+    arb::morphology morphology_;
+    concrete_embedding embedding_;
+
+    struct circular_def {};
+
+    // Maps are mutated only during initialization phase of mprovider.
+    mutable std::unordered_map<std::string, util::either<mcable_list, circular_def>> regions_;
+    mutable std::unordered_map<std::string, util::either<mlocation_list, circular_def>> locsets_;
+
+    // Non-null only during initialization phase.
+    mutable const label_dict* label_dict_ptr;
+
+    // Perform greedy initialization of concrete region, locset maps.
+    void init();
+};
+
+} // namespace arb
diff --git a/arbor/include/arbor/morph/primitives.hpp b/arbor/include/arbor/morph/primitives.hpp
index 52f9ea295b4d754682ca18b0d6aa6311e0ee4297..4b9e223b993db2659e94c00d78796c9b5f53f76c 100644
--- a/arbor/include/arbor/morph/primitives.hpp
+++ b/arbor/include/arbor/morph/primitives.hpp
@@ -69,6 +69,11 @@ std::ostream& operator<<(std::ostream& o, const mlocation_list& l);
 // and that the locations in the vector are ordered.
 bool test_invariants(const mlocation_list&);
 
+// Multiset operations on location lists.
+mlocation_list sum(const mlocation_list&, const mlocation_list&);
+mlocation_list join(const mlocation_list&, const mlocation_list&);
+mlocation_list intersection(const mlocation_list&, const mlocation_list&);
+
 // Describe an unbranched cable in the morphology.
 struct mcable {
     // The id of the branch on which the cable lies.
diff --git a/arbor/include/arbor/morph/region.hpp b/arbor/include/arbor/morph/region.hpp
index 1cb884b025bde88fdb3440e4f0bd14a1e9dcb8a9..6f0fd63e2358633f34a464b9eff0af8799234035 100644
--- a/arbor/include/arbor/morph/region.hpp
+++ b/arbor/include/arbor/morph/region.hpp
@@ -13,9 +13,7 @@
 
 namespace arb {
 
-// Forward declare the backend em_morphology type, required for defining the
-// interface for concretising locsets.
-class em_morphology;
+struct mprovider;
 
 class region {
 public:
@@ -54,7 +52,11 @@ public:
         return *this;
     }
 
-    friend mcable_list thingify(const region& r, const em_morphology& m) {
+    // Implicitly convert string to named region expression.
+    region(std::string label);
+    region(const char* label);
+
+    friend mcable_list thingify(const region& r, const mprovider& m) {
         return r.impl_->thingify(m);
     }
 
@@ -83,7 +85,7 @@ private:
         virtual ~interface() {}
         virtual std::unique_ptr<interface> clone() = 0;
         virtual std::ostream& print(std::ostream&) = 0;
-        virtual mcable_list thingify(const em_morphology&) = 0;
+        virtual mcable_list thingify(const mprovider&) = 0;
     };
 
     std::unique_ptr<interface> impl_;
@@ -97,7 +99,7 @@ private:
             return std::unique_ptr<interface>(new wrap<Impl>(wrapped));
         }
 
-        virtual mcable_list thingify(const em_morphology& m) override {
+        virtual mcable_list thingify(const mprovider& m) override {
             return thingify_(wrapped, m);
         }
 
@@ -128,6 +130,9 @@ region tagged(int id);
 // Region with all segments in a cell.
 region all();
 
+// Region associated with a name.
+region named(std::string);
+
 } // namespace reg
 
 } // namespace arb
diff --git a/arbor/util/either.hpp b/arbor/include/arbor/util/either.hpp
similarity index 99%
rename from arbor/util/either.hpp
rename to arbor/include/arbor/util/either.hpp
index faea135a63a4e6c04e49071872f6d5e41c00df3c..08cc66122f8e20c7f604b28fec20572cf5990bdd 100644
--- a/arbor/util/either.hpp
+++ b/arbor/include/arbor/util/either.hpp
@@ -13,8 +13,6 @@
 
 #include <arbor/util/uninitialized.hpp>
 
-#include "util/meta.hpp"
-
 namespace arb {
 namespace util {
 
@@ -97,7 +95,7 @@ namespace detail {
     };
 } // namespace detail
 
-constexpr std::size_t variant_npos = static_cast<std::size_t>(-1); // emulating C++17 variant type 
+constexpr std::size_t variant_npos = static_cast<std::size_t>(-1); // emulating C++17 variant type
 
 template <typename A, typename B>
 class either: public detail::either_data<A, B> {
diff --git a/arbor/mechcat.cpp b/arbor/mechcat.cpp
index 1c2a5d0bc213671a6ee656d648d48533f4ec656f..8d2a9c19ab2cd5a2220d02ea289b55f3ac9d4296 100644
--- a/arbor/mechcat.cpp
+++ b/arbor/mechcat.cpp
@@ -6,8 +6,8 @@
 
 #include <arbor/arbexcept.hpp>
 #include <arbor/mechcat.hpp>
+#include <arbor/util/either.hpp>
 
-#include "util/either.hpp"
 #include "util/maputil.hpp"
 
 /* Notes on implementation:
diff --git a/arbor/morph/em_morphology.cpp b/arbor/morph/em_morphology.cpp
deleted file mode 100644
index 8e49c3a9d42401d95038d5fbbeea28edeb23237a..0000000000000000000000000000000000000000
--- a/arbor/morph/em_morphology.cpp
+++ /dev/null
@@ -1,194 +0,0 @@
-#include <mutex>
-#include <stack>
-#include <vector>
-
-#include <arbor/morph/error.hpp>
-#include <arbor/morph/morphology.hpp>
-#include <arbor/morph/primitives.hpp>
-
-#include "morph/em_morphology.hpp"
-#include "util/rangeutil.hpp"
-#include "util/span.hpp"
-#include "util/strprintf.hpp"
-
-namespace arb {
-
-em_morphology::em_morphology(const morphology& m):
-    morph_(m)
-{
-    using util::count_along;
-    using util::make_span;
-
-    auto& parents = morph_.sample_parents();
-    auto& samples = morph_.samples();
-
-    const auto ns = morph_.num_samples();
-    const auto nb = morph_.num_branches();
-
-    if (!ns) return;
-
-    // Cache distance of each sample from the root.
-    dist2root_.resize(ns);
-    dist2root_[0] = 0.;
-    for (auto i: make_span(1, ns)) {
-        const auto p = parents[i];
-        dist2root_[i] = dist2root_[p] + distance(samples[p], samples[i]);
-    }
-    // Cache the legth of each branch.
-    branch_lengths_.reserve(nb);
-    for (auto i: make_span(nb)) {
-        auto idx = util::make_range(morph_.branch_indexes(i));
-        branch_lengths_.push_back(dist2root_[idx.back()]- dist2root_[idx.front()]);
-    }
-    if (morph_.spherical_root()) {
-        branch_lengths_[0] = samples[0].loc.radius*2;
-    }
-
-    // Cache the sample locations.
-    // Iterate backwards over branches distal to root, so that the parent branch at
-    // fork points will label its distal sample.
-    sample_locs_.resize(ns);
-    for (int b=nb-1; b>=0; --b) {
-        auto idx = util::make_range(morph_.branch_indexes(b));
-        double len = branch_lengths_[b];
-        // Handle 0 length branch.
-        len = len==0.? 1.: len;
-        double start = dist2root_[idx.front()];
-        for (auto i: idx) {
-            sample_locs_[i] = {msize_t(b), (dist2root_[i]-start)/len};
-        }
-        // Ensure that all non-spherical branches have their last sample 
-        if (idx.size()>1u) {
-            sample_locs_[idx.back()] = mlocation{msize_t(b), 1};
-        }
-    }
-    sample_locs_[0] = mlocation{0, 0.};
-
-    // Cache the location of terminal and fork points.
-    auto& props = morph_.sample_props();
-    for (auto i: count_along(props)) {
-        auto p = props[i];
-        if (is_terminal(p)) {
-            terminals_.push_back(sample2loc(i));
-        }
-        if (is_fork(p)) {
-            forks_.push_back(sample2loc(i));
-        }
-    }
-}
-
-em_morphology::em_morphology():
-    em_morphology(morphology())
-{}
-
-const morphology& em_morphology::morph() const {
-    return morph_;
-}
-
-mlocation em_morphology::root() const {
-    return {0,0};
-}
-
-mlocation em_morphology::sample2loc(msize_t sid) const {
-    if (sid>=morph_.num_samples()) {
-        throw morphology_error(util::pprintf("Sample {} does not exist in morpology", sid));
-    }
-    return sample_locs_[sid];
-}
-
-mlocation_list em_morphology::terminals() const {
-    return terminals_;
-}
-
-mlocation_list em_morphology::cover(mlocation loc, bool include_loc) const {
-    mlocation_list L{};
-    if (include_loc) L.push_back(loc);
-
-    // If the location is not at the end of a branch, it is its own cover.
-    if (loc.pos>0. && loc.pos<1.) return L;
-
-    // First location is {0,0} on a spherical root: nothing more to do.
-    if (loc==mlocation{0,0} && morph_.spherical_root()) {
-        return L;
-    }
-
-    if (loc.pos==1) {
-        // The location is at the end of a branch:
-        //      add the location at the start of each child branch.
-        for (auto b: morph_.branch_children(loc.branch)) {
-            L.push_back({b, 0});
-        }
-    }
-    else if (loc.pos==0) {
-        // The location is at the start of a branch:
-        //      add the location at the end of the parent branch, and locations
-        //      at the start of each child of the parent branch.
-        auto p = morph_.branch_parent(loc.branch);
-        if (p!=mnpos) L.push_back({p, 1});
-        for (auto b: morph_.branch_children(p)) {
-            if (b!=loc.branch) L.push_back({b, 0});
-        }
-    }
-
-    util::sort(L);
-
-    return L;
-}
-
-void em_morphology::assert_valid_location(mlocation loc) const {
-    if (!test_invariants(loc)) {
-        throw morphology_error(util::pprintf("Invalid location {}", loc));
-    }
-    if (loc.branch>=morph_.num_branches()) {
-        throw morphology_error(util::pprintf("Location {} does not exist in morpology", loc));
-    }
-}
-
-mlocation em_morphology::canonicalize(mlocation loc) const {
-    assert_valid_location(loc);
-
-    // Test if location is at the start of a branch.
-    if (loc.pos==0.) {
-        auto p = morph_.branch_parent(loc.branch);
-        return p==mnpos? root(): mlocation{p, 1};
-    }
-    return loc;
-}
-
-mlocation_list em_morphology::minset(const mlocation_list& in) const {
-    mlocation_list L;
-
-    std::stack<msize_t> stack;
-
-    // All root branches must be searched.
-    for (auto c: morph_.branch_children(mnpos)) {
-        stack.push(c);
-    }
-
-    // Depth-first traversal of the branch tree.
-    while (!stack.empty()) {
-        auto branch = stack.top();
-        stack.pop();
-
-        // Search for a location on branch.
-        auto it = std::lower_bound(in.begin(), in.end(), mlocation{branch, 0});
-
-        // If found, insert to the minset and skip the rest of this sub-tree.
-        if (it!=in.end() && it->branch==branch) {
-            L.push_back(*it);
-            continue;
-        }
-
-        // No location on this branch, so continue searching in this sub-tree.
-        for (auto c: morph_.branch_children(branch)) {
-            stack.push(c);
-        }
-    }
-
-    util::sort(L);
-
-    return L;
-}
-
-} // namespace arb
-
diff --git a/arbor/morph/em_morphology.hpp b/arbor/morph/em_morphology.hpp
deleted file mode 100644
index 1feb88cda374c17985a269856e2857bce91591d4..0000000000000000000000000000000000000000
--- a/arbor/morph/em_morphology.hpp
+++ /dev/null
@@ -1,57 +0,0 @@
-#pragma once
-
-#include <vector>
-
-#include <arbor/morph/morphology.hpp>
-#include <arbor/morph/primitives.hpp>
-
-namespace arb {
-
-class em_morphology {
-    morphology morph_;
-
-    mlocation_list terminals_;
-    mlocation_list forks_;
-    mlocation_list sample_locs_;
-
-    // distance from sample to root
-    std::vector<double> dist2root_;
-    std::vector<double> branch_lengths_;
-
-public:
-    em_morphology();
-    em_morphology(const morphology& m);
-
-    const morphology& morph() const;
-
-    // Convenience methods for morphology access
-    // that are forwarded directly to the morphology object:
-
-    auto empty() const { return morph_.empty(); }
-    auto spherical_root() const { return morph_.spherical_root(); }
-    auto num_branches() const { return morph_.num_branches(); }
-    auto num_samples() const { return morph_.num_samples(); }
-    auto branch_parent(msize_t b) const { return morph_.branch_parent(b); }
-    auto branch_children(msize_t b) const { return morph_.branch_children(b); }
-
-    // Access to computed and cached data:
-
-    mlocation_list terminals() const;
-    mlocation root() const;
-
-    mlocation sample2loc(msize_t sid) const;
-
-    void assert_valid_location(mlocation) const;
-    mlocation canonicalize(mlocation) const;
-
-    // Find all locations on the morphology that share the same canonoical
-    // representation of loc.
-    // If include_loc is false, the input location is excluded from the result.
-    mlocation_list cover(mlocation, bool include_loc=true) const;
-
-    mlocation_list minset(const mlocation_list&) const;
-
-    double branch_length(msize_t bid) const { return branch_lengths_.at(bid); }
-};
-
-} // namespace arb
diff --git a/arbor/morph/embed_pwlin.cpp b/arbor/morph/embed_pwlin.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e04436b4598224ab3a975ac60f5c527e40fd11b2
--- /dev/null
+++ b/arbor/morph/embed_pwlin.cpp
@@ -0,0 +1,204 @@
+#include <cstddef>
+#include <utility>
+#include <vector>
+
+#include <arbor/morph/embed_pwlin.hpp>
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
+
+#include "util/piecewise.hpp"
+#include "util/range.hpp"
+#include "util/rangeutil.hpp"
+#include "util/ratelem.hpp"
+#include "util/span.hpp"
+
+namespace arb {
+
+using util::rat_element;
+
+template <unsigned p, unsigned q>
+using pw_ratpoly = util::pw_elements<rat_element<p, q>>;
+
+template <unsigned p, unsigned q>
+using branch_pw_ratpoly = std::vector<pw_ratpoly<p, q>>;
+
+template <unsigned p, unsigned q>
+double interpolate(const branch_pw_ratpoly<p, q>& f, unsigned bid, double pos) {
+    const auto& pw = f.at(bid);
+    unsigned index = pw.index_of(pos);
+
+    const auto& element = pw.element(index);
+    std::pair<double, double> bounds = pw.interval(index);
+
+    if (bounds.first==bounds.second) return element[0];
+    else {
+        double x = (pos-bounds.first)/(bounds.second-bounds.first);
+        return element(x);
+    }
+}
+
+// Length, area, and ixa are polynomial or rational polynomial functions of branch position,
+// continuos and monotonically increasing with respect to distance from root.
+//
+// Integration wrt a piecewise constant function is performed by taking the difference between
+// interpolated values at the end points of each constant interval.
+
+template <unsigned p, unsigned q>
+double integrate(const branch_pw_ratpoly<p, q>& f, unsigned bid, const pw_constant_fn& g) {
+    double accum = 0;
+    for (msize_t i = 0; i<g.size(); ++i) {
+        std::pair<double, double> interval = g.interval(i);
+        accum += g.element(i)*(interpolate(f, bid, interval.second)-interpolate(f, bid, interval.first));
+    }
+    return accum;
+}
+
+struct embed_pwlin_data {
+    branch_pw_ratpoly<1, 0> length;
+    branch_pw_ratpoly<1, 0> radius;
+    branch_pw_ratpoly<2, 0> area;
+    branch_pw_ratpoly<1, 1> ixa;
+
+    explicit embed_pwlin_data(msize_t n_branch):
+        length(n_branch),
+        radius(n_branch),
+        area(n_branch),
+        ixa(n_branch)
+    {}
+};
+
+double embed_pwlin::radius(mlocation loc) const {
+    return interpolate(data_->radius, loc.branch, loc.pos);
+}
+
+double embed_pwlin::integrate_length(msize_t bid, const pw_constant_fn& g) const {
+    return integrate(data_->length, bid, g);
+}
+
+double embed_pwlin::integrate_area(msize_t bid, const pw_constant_fn& g) const {
+    return integrate(data_->area, bid, g);
+}
+
+double embed_pwlin::integrate_ixa(msize_t bid, const pw_constant_fn& g) const {
+    return integrate(data_->ixa, bid, g);
+}
+
+// Cable versions of integration methods:
+
+double embed_pwlin::integrate_length(mcable c) const {
+    return integrate_length(c.branch, pw_constant_fn{{c.prox_pos, c.dist_pos}, {1.}});
+}
+
+double embed_pwlin::integrate_area(mcable c) const {
+    return integrate_area(c.branch, pw_constant_fn{{c.prox_pos, c.dist_pos}, {1.}});
+}
+
+double embed_pwlin::integrate_ixa(mcable c) const {
+    return integrate_ixa(c.branch, pw_constant_fn{{c.prox_pos, c.dist_pos}, {1.}});
+}
+
+// Initialization, creation of geometric data.
+
+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);
+
+    if (!n_branch) return;
+
+    const auto& samples = m.samples();
+    sample_locations_.resize(m.num_samples());
+
+    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);
+
+            // Treat spherical root as area-equivalent cylinder.
+            double r = samples[0].loc.radius;
+
+            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));
+
+            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};
+        }
+        else {
+            arb_assert(sample_indices.size()>1);
+
+            std::vector<double> sample_distance;
+            sample_distance.reserve(samples.size());
+            sample_distance.push_back(0.);
+
+            for (auto i: util::count_along(sample_indices)) {
+                if (!i) continue;
+
+                double d = distance(samples[sample_indices[i-1]], samples[sample_indices[i]]);
+                sample_distance.push_back(sample_distance.back()+d);
+            }
+
+            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[1];
+            double ixa_0 = parent=mnpos? 0: data_->ixa[parent].back().second[1];
+
+            if (length_scale==0) {
+                // Zero-length branch? Weird, but make best show of it.
+                double r = samples[sample_indices[0]].loc.radius;
+                data_->radius[bid].push_back(0., 1., rat_element<1, 0>(r, r));
+                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 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;
+                }
+            }
+
+            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());
+        }
+    }
+};
+
+} // namespace arb
diff --git a/arbor/morph/label_dict.cpp b/arbor/morph/label_dict.cpp
index e72c40067ebf50e5618fe65d25ca03e1b7f61a03..9b00acbc65ecf27be82984551f8de1f5c43e55b2 100644
--- a/arbor/morph/label_dict.cpp
+++ b/arbor/morph/label_dict.cpp
@@ -1,18 +1,10 @@
-#include <algorithm>
-#include <set>
-#include <fstream>
 #include <unordered_map>
 
-#include <arbor/morph/error.hpp>
-#include <arbor/morph/locset.hpp>
+#include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/label_dict.hpp>
+#include <arbor/morph/locset.hpp>
 #include <arbor/morph/region.hpp>
 
-#include "util/filter.hpp"
-#include "util/span.hpp"
-#include "util/strprintf.hpp"
-#include "io/sepval.hpp"
-
 namespace arb {
 
 size_t label_dict::size() const {
@@ -21,16 +13,14 @@ size_t label_dict::size() const {
 
 void label_dict::set(const std::string& name, arb::locset ls) {
     if (regions_.count(name)) {
-        throw morphology_error(util::pprintf(
-                "Attempt to add a locset \"{}\" to a label dictionary that already contains a region with the same name.", name));
+        throw label_type_mismatch(name);
     }
     locsets_[name] = std::move(ls);
 }
 
 void label_dict::set(const std::string& name, arb::region reg) {
     if (locsets_.count(name)) {
-        throw morphology_error(util::pprintf(
-                "Attempt to add a region \"{}\" to a label dictionary that already contains a locset with the same name.", name));
+        throw label_type_mismatch(name);
     }
     regions_[name] = std::move(reg);
 }
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
index c21bb3d69ded5d2b146f525d1217ebcb99c3a50f..dd81e56e4ef6236a69a23fa9dd0cfae1d68c51c0 100644
--- a/arbor/morph/locset.cpp
+++ b/arbor/morph/locset.cpp
@@ -2,109 +2,34 @@
 #include <iostream>
 #include <numeric>
 
-#include <arbor/morph/error.hpp>
 #include <arbor/morph/locset.hpp>
-#include <arbor/morph/region.hpp>
+#include <arbor/morph/morphexcept.hpp>
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/mprovider.hpp>
+#include <arbor/morph/primitives.hpp>
 
 #include "util/rangeutil.hpp"
 #include "util/strprintf.hpp"
 
-#include "morph/em_morphology.hpp"
-
 namespace arb {
 namespace ls {
 
-//
-// Functions for taking the sum, union and intersection of location_lists (multisets).
-//
-
-using it_t = mlocation_list::iterator;
-using const_it_t = mlocation_list::const_iterator;
-
-// Advance an iterator to the first value that is not equal to its current
-// value, or end, whichever comes first.
-template <typename T>
-T next_unique(T& it, T end) {
-    const auto& x = *it;
-    ++it;
-    while (it!=end && *it==x) ++it;
-    return it;
-};
-
-// Return the number of times that the value at it is repeated. Advances the
-// iterator to the first value not equal to its current value, or end,
-// whichever comse first.
-template <typename T>
-int multiplicity(T& it, T end) {
-    const auto b = it;
-    return std::distance(b, next_unique(it, end));
-};
-
-mlocation_list sum(const mlocation_list& lhs, const mlocation_list& rhs) {
-    mlocation_list v;
-    v.resize(lhs.size() + rhs.size());
-    std::merge(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), v.begin());
-    return v;
-}
-
-mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs) {
-    mlocation_list L;
-    L.reserve(lhs.size()+rhs.size());
-
-    auto l    = lhs.begin();
-    auto lend = lhs.end();
-    auto r    = rhs.begin();
-    auto rend = rhs.end();
-
-    auto at_end = [&]() { return l==lend || r==rend; };
-    while (!at_end()) {
-        auto x = (*l<*r) ? *l: *r;
-        auto count = (*l<*r)? multiplicity(l, lend):
-                     (*r<*l)? multiplicity(r, rend):
-                     std::max(multiplicity(l, lend), multiplicity(r, rend));
-        L.insert(L.end(), count, x);
+// Throw on invalid mlocation.
+void assert_valid(mlocation x) {
+    if (!test_invariants(x)) {
+        throw invalid_mlocation(x);
     }
-    L.insert(L.end(), l, lend);
-    L.insert(L.end(), r, rend);
-
-    return L;
-}
-
-mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs) {
-    mlocation_list L;
-    L.reserve(lhs.size()+rhs.size());
-
-    auto l    = lhs.begin();
-    auto lend = lhs.end();
-    auto r    = rhs.begin();
-    auto rend = rhs.end();
-
-    auto at_end = [&]() { return l==lend || r==rend; };
-    while (!at_end()) {
-        if (*l==*r) {
-            auto x = *l;
-            auto count = std::min(multiplicity(l, lend), multiplicity(r, rend));
-            L.insert(L.end(), count, x);
-        }
-        else if (*l<*r) {
-            next_unique(l, lend);
-        }
-        else {
-            next_unique(r, rend);
-        }
-    }
-
-    return L;
 }
 
-// Null set
+// Empty locset.
+
 struct nil_ {};
 
 locset nil() {
     return locset{nil_{}};
 }
 
-mlocation_list thingify_(const nil_& x, const em_morphology& m) {
+mlocation_list thingify_(const nil_& x, const mprovider&) {
     return {};
 }
 
@@ -112,20 +37,22 @@ std::ostream& operator<<(std::ostream& o, const nil_& x) {
     return o << "nil";
 }
 
-// An explicit location
+// An explicit location.
+
 struct location_ {
     mlocation loc;
 };
 
 locset location(mlocation loc) {
-    if (!test_invariants(loc)) {
-        throw morphology_error(util::pprintf("invalid location {}", loc));
-    }
+    assert_valid(loc);
     return locset{location_{loc}};
 }
 
-mlocation_list thingify_(const location_& x, const em_morphology& m) {
-    m.assert_valid_location(x.loc);
+mlocation_list thingify_(const location_& x, const mprovider& p) {
+    assert_valid(x.loc);
+    if (x.loc.branch>=p.morphology().num_branches()) {
+        throw no_such_branch(x.loc.branch);
+    }
     return {x.loc};
 }
 
@@ -134,7 +61,8 @@ std::ostream& operator<<(std::ostream& o, const location_& x) {
 }
 
 
-// Location corresponding to a sample id
+// Location corresponding to a sample id.
+
 struct sample_ {
     msize_t index;
 };
@@ -143,83 +71,111 @@ locset sample(msize_t index) {
     return locset{sample_{index}};
 }
 
-mlocation_list thingify_(const sample_& x, const em_morphology& m) {
-    return {m.sample2loc(x.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 nodes on a morphology
+// Set of terminal points (most distal points).
+
 struct terminal_ {};
 
 locset terminal() {
     return locset{terminal_{}};
 }
 
-mlocation_list thingify_(const terminal_&, const em_morphology& m) {
-    return m.terminals();
+mlocation_list thingify_(const terminal_&, const mprovider& p) {
+    mlocation_list locs;
+    util::assign(locs, util::transform_view(p.morphology().terminal_branches(),
+        [](msize_t bid) { return mlocation{bid, 1.}; }));
+
+    return locs;
 }
 
 std::ostream& operator<<(std::ostream& o, const terminal_& x) {
     return o << "terminal";
 }
 
-// the root node of a morphology
+// Root location (most proximal point).
+
 struct root_ {};
 
 locset root() {
     return locset{root_{}};
 }
 
-mlocation_list thingify_(const root_&, const em_morphology& m) {
-    return {m.root()};
+mlocation_list thingify_(const root_&, const mprovider& p) {
+    return {mlocation{0, 0.}};
 }
 
 std::ostream& operator<<(std::ostream& o, const root_& x) {
     return o << "root";
 }
 
-// intersection of two point sets
+// Named locset.
+
+struct named_ {
+    std::string name;
+};
+
+locset named(std::string name) {
+    return locset(named_{std::move(name)});
+}
+
+mlocation_list thingify_(const named_& n, const mprovider& p) {
+    return p.locset(n.name);
+}
+
+std::ostream& operator<<(std::ostream& o, const named_& x) {
+    return o << "(named \"" << x.name << "\")";
+}
+
+
+// Intersection of two point sets.
+
 struct land {
     locset lhs;
     locset rhs;
     land(locset lhs, locset rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
 };
 
-mlocation_list thingify_(const land& P, const em_morphology& m) {
-    return intersection(thingify(P.lhs, m), thingify(P.rhs, m));
+mlocation_list thingify_(const land& P, const mprovider& p) {
+    return intersection(thingify(P.lhs, p), thingify(P.rhs, p));
 }
 
 std::ostream& operator<<(std::ostream& o, const land& x) {
     return o << "(intersect " << x.lhs << " " << x.rhs << ")";
 }
 
-// union of two point sets
+// Union of two point sets.
+
 struct lor {
     locset lhs;
     locset rhs;
     lor(locset lhs, locset rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
 };
 
-mlocation_list thingify_(const lor& P, const em_morphology& m) {
-    return join(thingify(P.lhs, m), thingify(P.rhs, m));
+mlocation_list thingify_(const lor& P, const mprovider& p) {
+    return join(thingify(P.lhs, p), thingify(P.rhs, p));
 }
 
 std::ostream& operator<<(std::ostream& o, const lor& x) {
     return o << "(join " << x.lhs << " " << x.rhs << ")";
 }
 
-// sum of two point sets
+// Sum of two point sets.
+
 struct lsum {
     locset lhs;
     locset rhs;
     lsum(locset lhs, locset rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
 };
 
-mlocation_list thingify_(const lsum& P, const em_morphology& m) {
-    return sum(thingify(P.lhs, m), thingify(P.rhs, m));
+mlocation_list thingify_(const lsum& P, const mprovider& p) {
+    return sum(thingify(P.lhs, p), thingify(P.rhs, p));
 }
 
 std::ostream& operator<<(std::ostream& o, const lsum& x) {
@@ -248,8 +204,17 @@ locset::locset() {
     *this = ls::nil();
 }
 
-locset::locset(mlocation other) {
-    *this = ls::location(other);
+locset::locset(mlocation loc) {
+    *this = ls::location(loc);
+}
+
+locset::locset(std::string name) {
+    *this = ls::named(std::move(name));
+}
+
+locset::locset(const char* name) {
+    *this = ls::named(name);
 }
 
+
 } // namespace arb
diff --git a/arbor/morph/morphexcept.cpp b/arbor/morph/morphexcept.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..21463f20c185bacee26c211109f16397946dc00e
--- /dev/null
+++ b/arbor/morph/morphexcept.cpp
@@ -0,0 +1,55 @@
+#include <string>
+#include <sstream>
+
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/morphexcept.hpp>
+
+#include "util/strprintf.hpp"
+
+namespace arb {
+
+using arb::util::pprintf;
+
+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)),
+    bid(bid)
+{}
+
+invalid_mcable::invalid_mcable(mcable cable):
+    morphology_error(pprintf("invalid mcable {}", cable)),
+    cable(cable)
+{}
+
+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)),
+    bid(bid)
+{}
+
+unbound_name::unbound_name(const std::string& name):
+    morphology_error(pprintf("no definition for '{}'", name)),
+    name(name)
+{}
+
+circular_definition::circular_definition(const std::string& name):
+    morphology_error(pprintf("definition of '{}' requires a definition for '{}'", name, name)),
+    name(name)
+{}
+
+} // namespace arb
+
diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp
index 34253041750bba913d66d0acf25bd4adc4401b94..3b59fef868ee5c0999ba03186427ab52dda21d59 100644
--- a/arbor/morph/morphology.cpp
+++ b/arbor/morph/morphology.cpp
@@ -1,42 +1,31 @@
-#include <cmath>
 #include <iostream>
-#include <unordered_map>
+#include <stack>
 #include <utility>
 
-#include <arbor/math.hpp>
-#include <arbor/morph/error.hpp>
+#include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/sample_tree.hpp>
 #include <arbor/morph/primitives.hpp>
 
-#include "algorithms.hpp"
-#include "io/sepval.hpp"
 #include "morph/mbranch.hpp"
+#include "util/rangeutil.hpp"
 #include "util/span.hpp"
-#include "util/strprintf.hpp"
 
-namespace arb {
+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)
 {
-    using util::make_span;
-
-    const char* errstr_single_sample_root =
-        "A morphology with only one sample must have a spherical root";
-    const char* errstr_incomplete_cable =
-        "A branch must contain at least two samples";
-
-    if (parents.empty()) return {};
-
     auto nsamp = parents.size();
+    if (!nsamp) return {};
 
     // Enforce that a morphology with one sample has a spherical root.
     if (!spherical_root && nsamp==1u) {
-        throw morphology_error(errstr_single_sample_root);
+        throw incomplete_branch(0);
     }
 
     std::vector<int> bids(nsamp);
@@ -70,7 +59,7 @@ std::vector<mbranch> branches_from_parent_index(const std::vector<msize_t>& pare
     if (spherical_root) {
         for (auto i: make_span(1, nbranches)) { // skip the root.
             if (branches[i].size()<2u) {
-                throw morphology_error(errstr_incomplete_cable);
+                throw incomplete_branch(i);
             }
         }
     }
@@ -109,6 +98,7 @@ struct morphology_impl {
     std::vector<impl::mbranch> 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);
@@ -134,11 +124,7 @@ morphology_impl::morphology_impl(sample_tree m):
 }
 
 void morphology_impl::init() {
-    using util::make_span;
-    using util::count_along;
-
     auto nsamp = samples_.size();
-
     if (!nsamp) return;
 
     // Generate branches.
@@ -158,6 +144,15 @@ void morphology_impl::init() {
             root_children_.push_back(i);
         }
     }
+
+    // Collect terminal branches.
+    terminal_branches_.reserve(nbranch);
+    for (auto i: make_span(nbranch)) {
+        if (branch_children_[i].empty()) {
+            terminal_branches_.push_back(i);
+        }
+    }
+    terminal_branches_.shrink_to_fit();
 }
 
 std::ostream& operator<<(std::ostream& o, const morphology_impl& m) {
@@ -205,6 +200,10 @@ const std::vector<msize_t>& morphology::branch_children(msize_t b) const {
     return b==mnpos? impl_->root_children_: impl_->branch_children_[b];
 }
 
+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_;
@@ -236,5 +235,49 @@ std::ostream& operator<<(std::ostream& o, const morphology& m) {
     return o << *m.impl_;
 }
 
+// Utilities.
+
+mlocation_list minset(const morphology& m, const mlocation_list& in) {
+    mlocation_list L;
+
+    std::stack<msize_t> stack;
+
+    // All root branches must be searched.
+    for (auto c: m.branch_children(mnpos)) {
+        stack.push(c);
+    }
+
+    // Depth-first traversal of the branch tree.
+    while (!stack.empty()) {
+        auto branch = stack.top();
+        stack.pop();
+
+        // Search for a location on branch.
+        auto it = std::lower_bound(in.begin(), in.end(), mlocation{branch, 0});
+
+        // If found, insert to the minset and skip the rest of this sub-tree.
+        if (it!=in.end() && it->branch==branch) {
+            L.push_back(*it);
+            continue;
+        }
+
+        // No location on this branch, so continue searching in this sub-tree.
+        for (auto c: m.branch_children(branch)) {
+            stack.push(c);
+        }
+    }
+
+    util::sort(L);
+    return L;
+}
+
+mlocation canonical(const morphology& m, mlocation loc) {
+    if (loc.pos==0) {
+        msize_t parent = m.branch_parent(loc.branch);
+        return parent==mnpos? mlocation{0, 0.}: mlocation{parent, 1.};
+    }
+    return loc;
+}
+
 } // namespace arb
 
diff --git a/arbor/morph/mprovider.cpp b/arbor/morph/mprovider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8b8cb216c91e519e9007b05d62728412581c71c9
--- /dev/null
+++ b/arbor/morph/mprovider.cpp
@@ -0,0 +1,75 @@
+#include <string>
+#include <utility>
+
+#include <arbor/morph/label_dict.hpp>
+#include <arbor/morph/locset.hpp>
+#include <arbor/morph/morphexcept.hpp>
+#include <arbor/morph/mprovider.hpp>
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/region.hpp>
+
+namespace arb {
+
+void mprovider::init() {
+    // Evaluate each named region or locset in provided dictionary
+    // to populate concrete regions_, locsets_ maps.
+
+    if (!label_dict_ptr) return;
+
+    for (const auto& pair: label_dict_ptr->regions()) {
+        (void)region(pair.first);
+    }
+
+    for (const auto& pair: label_dict_ptr->locsets()) {
+        (void)locset(pair.first);
+    }
+
+    label_dict_ptr = nullptr;
+}
+
+// Evaluation of a named region or locset requires the recursive evaluation of
+// any component regions or locsets in its definition.
+//
+// During the initialization phase, 'named' expressions will be looked up in the
+// provided label_dict, and the maps updated accordingly. Post-initialization,
+// label_dict_ptr will be null, and concrete regions/locsets will only be retrieved
+// from the maps established during initialization.
+
+template <typename RegOrLocMap, typename LabelDictMap, typename Err>
+static const auto& try_lookup(const mprovider& provider, const std::string& name, RegOrLocMap& map, const LabelDictMap* dict_ptr, Err errval) {
+    auto it = map.find(name);
+    if (it==map.end()) {
+        if (dict_ptr) {
+            map.emplace(name, errval);
+
+            auto it = dict_ptr->find(name);
+            if (it==dict_ptr->end()) {
+                throw unbound_name(name);
+            }
+
+            return (map[name] = thingify(it->second, provider)).first();
+        }
+        else {
+            throw unbound_name(name);
+        }
+    }
+    else if (!it->second) {
+        throw circular_definition(name);
+    }
+    else {
+        return it->second.first();
+    }
+}
+
+const mcable_list& mprovider::region(const std::string& name) const {
+    const auto* regions_ptr = label_dict_ptr? &(label_dict_ptr->regions()): nullptr;
+    return try_lookup(*this, name, regions_, regions_ptr, circular_def{});
+}
+
+const mlocation_list& mprovider::locset(const std::string& name) const {
+    const auto* locsets_ptr = label_dict_ptr? &(label_dict_ptr->locsets()): nullptr;
+    return try_lookup(*this, name, locsets_, locsets_ptr, circular_def{});
+}
+
+
+} // namespace arb
diff --git a/arbor/morph/primitives.cpp b/arbor/morph/primitives.cpp
index 486959bb60cd2cef279dd54ec5d6806c433a1d51..f98ddfcf5d0423b4f736f8fb64fce390d01372b9 100644
--- a/arbor/morph/primitives.cpp
+++ b/arbor/morph/primitives.cpp
@@ -1,3 +1,4 @@
+#include <algorithm>
 #include <ostream>
 
 #include <arbor/math.hpp>
@@ -9,6 +10,30 @@
 
 namespace arb {
 
+namespace {
+
+// Advance an iterator to the first value that is not equal to its current
+// value, or end, whichever comes first.
+template <typename T>
+T next_unique(T& it, T end) {
+    const auto& x = *it;
+    ++it;
+    while (it!=end && *it==x) ++it;
+    return it;
+};
+
+// Return the number of times that the value at it is repeated. Advances the
+// iterator to the first value not equal to its current value, or end,
+// whichever comse first.
+template <typename T>
+int multiplicity(T& it, T end) {
+    const auto b = it;
+    return std::distance(b, next_unique(it, end));
+};
+
+} // anonymous namespace
+
+
 // interpolate between two points.
 mpoint lerp(const mpoint& a, const mpoint& b, double u) {
     return { math::lerp(a.x, b.x, u),
@@ -43,6 +68,63 @@ bool test_invariants(const mlocation& l) {
     return (0.<=l.pos && l.pos<=1.) && l.branch!=mnpos;
 }
 
+mlocation_list sum(const mlocation_list& lhs, const mlocation_list& rhs) {
+    mlocation_list v;
+    v.resize(lhs.size() + rhs.size());
+    std::merge(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), v.begin());
+    return v;
+}
+
+mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs) {
+    mlocation_list L;
+    L.reserve(lhs.size()+rhs.size());
+
+    auto l    = lhs.begin();
+    auto lend = lhs.end();
+    auto r    = rhs.begin();
+    auto rend = rhs.end();
+
+    auto at_end = [&]() { return l==lend || r==rend; };
+    while (!at_end()) {
+        auto x = (*l<*r) ? *l: *r;
+        auto count = (*l<*r)? multiplicity(l, lend):
+                     (*r<*l)? multiplicity(r, rend):
+                     std::max(multiplicity(l, lend), multiplicity(r, rend));
+        L.insert(L.end(), count, x);
+    }
+    L.insert(L.end(), l, lend);
+    L.insert(L.end(), r, rend);
+
+    return L;
+}
+
+mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs) {
+    mlocation_list L;
+    L.reserve(lhs.size()+rhs.size());
+
+    auto l    = lhs.begin();
+    auto lend = lhs.end();
+    auto r    = rhs.begin();
+    auto rend = rhs.end();
+
+    auto at_end = [&]() { return l==lend || r==rend; };
+    while (!at_end()) {
+        if (*l==*r) {
+            auto x = *l;
+            auto count = std::min(multiplicity(l, lend), multiplicity(r, rend));
+            L.insert(L.end(), count, x);
+        }
+        else if (*l<*r) {
+            next_unique(l, lend);
+        }
+        else {
+            next_unique(r, rend);
+        }
+    }
+
+    return L;
+}
+
 bool test_invariants(const mcable& c) {
     return (0.<=c.prox_pos && c.prox_pos<=c.dist_pos && c.dist_pos<=1.) && c.branch!=mnpos;
 }
diff --git a/arbor/morph/region.cpp b/arbor/morph/region.cpp
index b456c42e19da81d06fa8d615be573d7e65dede88..f0536d6110cfb6fefc38ef6305dd2e9e7245880b 100644
--- a/arbor/morph/region.cpp
+++ b/arbor/morph/region.cpp
@@ -2,12 +2,12 @@
 #include <string>
 #include <vector>
 
-#include <arbor/morph/error.hpp>
 #include <arbor/morph/locset.hpp>
 #include <arbor/morph/primitives.hpp>
+#include <arbor/morph/morphexcept.hpp>
+#include <arbor/morph/mprovider.hpp>
 #include <arbor/morph/region.hpp>
 
-#include "morph/em_morphology.hpp"
 #include "util/span.hpp"
 #include "util/strprintf.hpp"
 #include "util/range.hpp"
@@ -16,6 +16,15 @@
 namespace arb {
 namespace reg {
 
+// Head and tail of an mcable as mlocations.
+inline mlocation head(mcable c) {
+    return mlocation{c.branch, c.prox_pos};
+}
+
+inline mlocation tail(mcable c) {
+    return mlocation{c.branch, c.dist_pos};
+}
+
 // Returns true iff cable sections a and b:
 //  1. are on the same branch
 //  2. overlap, i.e. their union is not empty.
@@ -57,36 +66,61 @@ mcable_list merge(const mcable_list& v) {
     return L;
 }
 
+// List of colocated mlocations, excluding the parameter.
+mlocation_list colocated(mlocation loc, const morphology& m) {
+    mlocation_list L{};
+
+    // Note: if the location is not at the end of a branch, there are no
+    // other colocated points.
+
+    if (loc.pos==0) {
+        // Include head of each branch with same parent,
+        // and end of parent branch if not mnpos.
+
+        auto p = m.branch_parent(loc.branch);
+        if (p!=mnpos) L.push_back({p, 1});
+
+        for (auto b: m.branch_children(p)) {
+            if (b!=loc.branch) L.push_back({b, 0});
+        }
+    }
+    else if (loc.pos==1) {
+        // Include head of each child branch.
+
+        for (auto b: m.branch_children(loc.branch)) {
+            L.push_back({b, 0});
+        }
+    }
+
+    return L;
+}
+
 // Insert a zero-length region at the start of each child branch for every cable
 // that includes the end of a branch.
 // Insert a zero-length region at the end of the parent branch for each cable
 // that includes the start of the branch.
-mcable_list cover(mcable_list cables, const em_morphology& m) {
-    mcable_list L;
+mcable_list cover(mcable_list cables, const morphology& m) {
+    mcable_list L = cables;
+
     for (auto& c: cables) {
-        if (c.prox_pos==0) {
-            for (auto& x: m.cover(mlocation{c.branch, 0}, false)) {
-                L.push_back({x.branch, x.pos, x.pos});
-            }
+        for (auto& x: colocated(head(c), m)) {
+            L.push_back({x.branch, x.pos, x.pos});
         }
-        if (c.dist_pos==1) {
-            for (auto& x: m.cover(mlocation{c.branch, 1}, false)) {
-                L.push_back({x.branch, x.pos, x.pos});
-            }
+        for (auto& x: colocated(tail(c), m)) {
+            L.push_back({x.branch, x.pos, x.pos});
         }
     }
-    L.insert(L.end(), cables.begin(), cables.end());
-    util::sort(L);
 
+    util::sort(L);
     return L;
 }
 
-mcable_list remove_cover(mcable_list cables, const em_morphology& m) {
+mcable_list remove_cover(mcable_list cables, const morphology& m) {
     // Find all zero-length cables at the end of cables, and convert to
     // their canonical representation.
     for (auto& c: cables) {
         if (c.dist_pos==0 || c.prox_pos==1) {
-            auto cloc = m.canonicalize({c.branch, c.prox_pos});
+            auto cloc = canonical(m, head(c));
             c = {cloc.branch, cloc.pos, cloc.pos};
         }
     }
@@ -98,16 +132,16 @@ mcable_list remove_cover(mcable_list cables, const em_morphology& m) {
     return merge(cables);
 }
 
-//
-// Null/empty region
-//
+
+// Empty region.
+
 struct nil_ {};
 
 region nil() {
     return region{nil_{}};
 }
 
-mcable_list thingify_(const nil_& x, const em_morphology& m) {
+mcable_list thingify_(const nil_& x, const mprovider&) {
     return {};
 }
 
@@ -115,9 +149,8 @@ std::ostream& operator<<(std::ostream& o, const nil_& x) {
     return o << "nil";
 }
 
-//
-// Explicit cable section
-//
+
+// Explicit cable section.
 
 struct cable_ {
     mcable cable;
@@ -125,7 +158,7 @@ struct cable_ {
 
 region cable(mcable c) {
     if (!test_invariants(c)) {
-        throw morphology_error(util::pprintf("Invalid cable section {}", c));
+        throw invalid_mcable(c);
     }
     return region(cable_{c});
 }
@@ -134,13 +167,10 @@ region branch(msize_t bid) {
     return cable({bid, 0, 1});
 }
 
-mcable_list thingify_(const cable_& reg, const em_morphology& em) {
-    auto& m = em.morph();
-
-    if (reg.cable.branch>=m.num_branches()) {
-        throw morphology_error(util::pprintf("Branch {} does not exist in morpology", reg.cable.branch));
+mcable_list thingify_(const cable_& reg, const mprovider& p) {
+    if (reg.cable.branch>=p.morphology().num_branches()) {
+        throw no_such_branch(reg.cable.branch);
     }
-
     return {reg.cable};
 }
 
@@ -148,9 +178,9 @@ std::ostream& operator<<(std::ostream& o, const cable_& c) {
     return o << c.cable;
 }
 
-//
-// region with all segments with the same numeric tag
-//
+
+// Region with all segments with the same numeric tag.
+
 struct tagged_ {
     int tag;
 };
@@ -159,8 +189,9 @@ region tagged(int id) {
     return region(tagged_{id});
 }
 
-mcable_list thingify_(const tagged_& reg, const em_morphology& em) {
-    auto& m = em.morph();
+mcable_list thingify_(const tagged_& reg, const mprovider& p) {
+    const auto& m = p.morphology();
+    const auto& e = p.embedding();
     size_t nb = m.num_branches();
 
     std::vector<mcable> L;
@@ -193,8 +224,8 @@ mcable_list thingify_(const tagged_& reg, const em_morphology& em) {
             auto first = start-1;
             auto last = std::find_if(start, end, not_matches);
 
-            auto l = first==beg? 0.: em.sample2loc(*first).pos;
-            auto r = last==end?  1.: em.sample2loc(*(last-1)).pos;
+            auto l = first==beg? 0.: e.sample_location(*first).pos;
+            auto r = last==end?  1.: e.sample_location(*(last-1)).pos;
             L.push_back({i, l, r});
 
             // Find the next sample in the branch that matches reg.tag.
@@ -211,17 +242,17 @@ std::ostream& operator<<(std::ostream& o, const tagged_& t) {
     return o << "(tag " << t.tag << ")";
 }
 
-//
-// region with all segments in a cell
-//
+
+// Region comprising whole morphology.
+
 struct all_ {};
 
 region all() {
     return region(all_{});
 }
 
-mcable_list thingify_(const all_&, const em_morphology& m) {
-    auto nb = m.morph().num_branches();
+mcable_list thingify_(const all_&, const mprovider& p) {
+    auto nb = p.morphology().num_branches();
     mcable_list branches;
     branches.reserve(nb);
     for (auto i: util::make_span(nb)) {
@@ -234,21 +265,42 @@ std::ostream& operator<<(std::ostream& o, const all_& t) {
     return o << "all";
 }
 
-//
-// intersection of two regions.
-//
+
+// Named region.
+
+struct named_ {
+    std::string name;
+};
+
+region named(std::string name) {
+    return region(named_{std::move(name)});
+}
+
+mcable_list thingify_(const named_& n, const mprovider& p) {
+    return p.region(n.name);
+}
+
+std::ostream& operator<<(std::ostream& o, const named_& x) {
+    return o << "(named \"" << x.name << "\")";
+}
+
+
+// Intersection of two regions.
+
 struct reg_and {
     region lhs;
     region rhs;
     reg_and(region lhs, region rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
 };
 
-mcable_list thingify_(const reg_and& P, const em_morphology& m) {
+mcable_list thingify_(const reg_and& P, const mprovider& p) {
+    auto& m = p.morphology();
+
     using cable_it = std::vector<mcable>::const_iterator;
     using cable_it_pair = std::pair<cable_it, cable_it>;
 
-    auto lhs = cover(thingify(P.lhs, m), m);
-    auto rhs = cover(thingify(P.rhs, m), m);
+    auto lhs = cover(thingify(P.lhs, p), m);
+    auto rhs = cover(thingify(P.rhs, p), m);
 
     // Perform intersection
     cable_it_pair it{lhs.begin(), rhs.begin()};
@@ -279,18 +331,18 @@ std::ostream& operator<<(std::ostream& o, const reg_and& x) {
     return o << "(intersect " << x.lhs << " " << x.rhs << ")";
 }
 
-//
-// union of two point sets
-//
+
+// Union of two regions.
+
 struct reg_or {
     region lhs;
     region rhs;
     reg_or(region lhs, region rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
 };
 
-mcable_list thingify_(const reg_or& P, const em_morphology& m) {
-    auto lhs = thingify(P.lhs, m);
-    auto rhs = thingify(P.rhs, m);
+mcable_list thingify_(const reg_or& P, const mprovider& p) {
+    auto lhs = thingify(P.lhs, p);
+    auto rhs = thingify(P.rhs, p);
     mcable_list L;
     L.resize(lhs.size() + rhs.size());
 
@@ -321,4 +373,12 @@ region::region() {
     *this = reg::nil();
 }
 
+region::region(std::string label) {
+    *this = reg::named(std::move(label));
+}
+
+region::region(const char* label) {
+    *this = reg::named(label);
+}
+
 } // namespace arb
diff --git a/arbor/morph/sample_tree.cpp b/arbor/morph/sample_tree.cpp
index d8f82c10b33652407a32f7cde86c6331653d2417..15ec4d8cf64cd84fbd44701a7d22409e240e7454 100644
--- a/arbor/morph/sample_tree.cpp
+++ b/arbor/morph/sample_tree.cpp
@@ -1,12 +1,10 @@
-#include <cmath>
+#include <iostream>
+#include <stdexcept>
 #include <vector>
 
-#include <arbor/math.hpp>
-
-#include <arbor/morph/error.hpp>
+#include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/sample_tree.hpp>
 
-#include "algorithms.hpp"
 #include "io/sepval.hpp"
 #include "util/span.hpp"
 
@@ -14,8 +12,7 @@ namespace arb {
 
 sample_tree::sample_tree(std::vector<msample> samples, std::vector<msize_t> parents) {
     if (samples.size()!=parents.size()) {
-        throw std::runtime_error(
-            "The same number of samples and parent indices used to create a sample morphology");
+        throw std::invalid_argument("sample and parent vectors differ in size");
     }
     reserve(samples.size());
     for (auto i: util::make_span(samples.size())) {
@@ -30,14 +27,11 @@ void sample_tree::reserve(msize_t n) {
 }
 
 msize_t sample_tree::append(msize_t p, const msample& s) {
-    if (empty()) {
-        if (p!=mnpos) throw morphology_error("Parent id of root sample must be mnpos");
-    }
-    else if (p>=size()) {
-        throw morphology_error("Parent id of a sample must be less than the sample id");
+    if ((empty() && p!=mnpos) || p>=size()) {
+        if (p!=mnpos) throw invalid_sample_parent(p, size());
     }
-    auto id = size();
 
+    auto id = size();
     samples_.push_back(s);
     parents_.push_back(p);
 
diff --git a/arbor/profile/meter_manager.cpp b/arbor/profile/meter_manager.cpp
index d3e4097eff06cfc05273a9dbe8e3d66adcb38442..6b17ac44bc647c4877953e393452fa5579074faa 100644
--- a/arbor/profile/meter_manager.cpp
+++ b/arbor/profile/meter_manager.cpp
@@ -171,7 +171,7 @@ std::ostream& operator<<(std::ostream& o, const meter_report& report) {
                 // Energy measurements are per "per node", so only normalise
                 // by the number of ranks per node. TODO, this is an
                 // approximation: better reduce a subset of measurements.
-                double energy = algorithms::sum(m.measurements[cp_index])/doms_per_host*1e-3;
+                double energy = util::sum(m.measurements[cp_index])/doms_per_host*1e-3;
                 sums[m_index] += energy;
                 o << strprintf("%16.3f", energy);
             }
diff --git a/arbor/util/partition.hpp b/arbor/util/partition.hpp
index 0f360fd2bd78bf2cadda452f80b53ea5268a5c1b..e4190b262ebb677336071878d048d4ba91b79b06 100644
--- a/arbor/util/partition.hpp
+++ b/arbor/util/partition.hpp
@@ -5,7 +5,8 @@
 #include <stdexcept>
 #include <type_traits>
 
-#include "util/either.hpp"
+#include <arbor/util/either.hpp>
+
 #include "util/meta.hpp"
 #include "util/partition_iterator.hpp"
 #include "util/range.hpp"
diff --git a/arbor/util/piecewise.hpp b/arbor/util/piecewise.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a7a11ab029374f2321e6a5e6ee92762011d85877
--- /dev/null
+++ b/arbor/util/piecewise.hpp
@@ -0,0 +1,481 @@
+#pragma once
+
+// Create/manipulate 1-d piece-wise defined objects.
+//
+// Using vectors everywhere here for ease; consider making
+// something more container/sequence-generic later.
+
+#include <initializer_list>
+#include <iterator>
+#include <type_traits>
+#include <vector>
+
+#include "util/meta.hpp"
+#include "util/partition.hpp"
+
+namespace arb {
+namespace util {
+
+using pw_size_type = unsigned;
+constexpr pw_size_type pw_npos = -1;
+
+// Generic random access const iterator for a collection
+// providing operator[].
+
+template <typename T>
+struct indexed_const_iterator {
+    using size_type = decltype(util::size(std::declval<T>()));
+    using difference_type = std::make_signed_t<size_type>;
+
+    using value_type = decltype(std::declval<T>()[0]);
+    struct pointer {
+        value_type v;
+        const value_type* operator->() const { return &v; }
+    };
+
+    using reference = value_type;
+    using iterator_category = std::random_access_iterator_tag;
+
+    const T* ptr_ = nullptr;
+    size_type i_ = 0;
+
+    bool operator==(indexed_const_iterator x) const { return ptr_ == x.ptr_ && i_ == x.i_; }
+    bool operator!=(indexed_const_iterator x) const { return !(*this==x); }
+    bool operator<=(indexed_const_iterator x) const { return i_<=x.i_; }
+    bool operator<(indexed_const_iterator x)  const { return i_<x.i_; }
+    bool operator>=(indexed_const_iterator x) const { return i_>=x.i_; }
+    bool operator>(indexed_const_iterator x)  const { return i_>x.i_; }
+
+    difference_type operator-(indexed_const_iterator x) const { return i_-x.i_; }
+    indexed_const_iterator& operator++() { return ++i_, *this; }
+    indexed_const_iterator& operator--() { return --i_, *this; }
+    indexed_const_iterator operator++(int) { auto x = *this; return ++i_, x; }
+    indexed_const_iterator operator--(int) { auto x = *this; return --i_, x; }
+
+    indexed_const_iterator operator+(difference_type n) { return indexed_const_iterator{ptr_, i_+n}; }
+    indexed_const_iterator operator-(difference_type n) { return indexed_const_iterator{ptr_, i_-n}; }
+    indexed_const_iterator& operator+=(difference_type n) { return i_+=n, *this; }
+    indexed_const_iterator& operator-=(difference_type n) { return i_-=n, *this; }
+
+    friend indexed_const_iterator operator+(difference_type n, indexed_const_iterator x) {
+        indexed_const_iterator r(std::move(x));
+        return r+=n;
+    }
+
+    reference operator*() const { return (*ptr_)[i_]; }
+    pointer operator->() const { return pointer{(*ptr_)[i_]}; }
+};
+
+
+template <typename X = void>
+struct pw_elements {
+    using size_type = pw_size_type;
+    static constexpr size_type npos = pw_npos;
+
+    using value_type = std::pair<std::pair<double, double>, X>;
+
+    // Consistency requirements:
+    // 1. empty() || element.size()+1 = vertex.size()
+    // 2. vertex[i]<=vertex[j] for i<=j.
+
+    std::vector<double> vertex_;
+    std::vector<X> element_;
+
+    // Ctors and assignment:
+
+    pw_elements() = default;
+
+    template <typename VSeq, typename ESeq>
+    pw_elements(const VSeq& vs, const ESeq& es) {
+        assign(vs, es);
+    }
+
+    pw_elements(std::initializer_list<double> vs, std::initializer_list<X> es) {
+        assign(vs, es);
+    }
+
+    pw_elements(pw_elements&&) = default;
+    pw_elements(const pw_elements&) = default;
+
+    template <typename Y>
+    explicit pw_elements(const pw_elements<Y>& from):
+        vertex_(from.vertex_), element_(from.element_.begin(), from.element_.end())
+    {}
+
+    pw_elements& operator=(pw_elements&&) = default;
+    pw_elements& operator=(const pw_elements&) = default;
+
+    // Access:
+
+    auto intervals() const { return util::partition_view(vertex_); }
+    auto interval(size_type i) const { return intervals()[i]; }
+
+    auto bounds() const { return intervals().bounds(); }
+
+    size_type size() const { return element_.size(); }
+    bool empty() const { return size()==0; }
+
+    bool operator==(const pw_elements& x) const {
+        return vertex_==x.vertex_ && element_==x.element_;
+    }
+
+    bool operator!=(const pw_elements& x) const { return !(*this==x); }
+
+    const auto& elements() const { return element_; }
+    const auto& vertices() const { return vertex_; }
+
+    X& element(size_type i) & { return element_[i]; }
+    const X& element(size_type i) const & { return element_[i]; }
+    value_type operator[](size_type i) const { return value_type{interval(i), element(i)}; }
+
+    using const_iterator = indexed_const_iterator<pw_elements<X>>;
+    using iterator = const_iterator;
+
+    const_iterator cbegin() const { return const_iterator{this, 0}; }
+    const_iterator begin() const { return cbegin(); }
+    const_iterator cend() const { return const_iterator{this, size()}; }
+    const_iterator end() const { return cend(); }
+    value_type front() const { return (*this)[0]; }
+    value_type back() const { return (*this)[size()-1]; }
+
+    size_type index_of(double x) const {
+        if (empty()) return npos;
+
+        auto partn = intervals();
+        if (x == partn.bounds().second) return size()-1;
+        else return partn.index(x);
+    }
+
+    // mutating operations:
+
+    void reserve(size_type n) {
+        vertex_.reserve(n+1);
+        element_.reserve(n);
+    }
+
+    void clear() {
+        vertex_.clear();
+        element_.clear();
+    }
+
+    template <typename U>
+    void push_back(double left, double right, U&& elem) {
+        if (!empty() && left!=vertex_.back()) {
+            throw std::runtime_error("noncontiguous element");
+        }
+
+        if (right<left) {
+            throw std::runtime_error("inverted element");
+        }
+
+        // Extend element_ first in case a conversion/copy/move throws.
+
+        element_.push_back(std::forward<U>(elem));
+        if (vertex_.empty()) vertex_.push_back(left);
+        vertex_.push_back(right);
+    }
+
+    template <typename U>
+    void push_back(double right, U&& elem) {
+        if (empty()) {
+            throw std::runtime_error("require initial left vertex for element");
+        }
+
+        push_back(vertex_.back(), right, elem);
+    }
+
+    void assign(std::initializer_list<double> vs, std::initializer_list<X> es) {
+        using util::make_range;
+        assign(make_range(vs.begin(), vs.end()), make_range(es.begin(), es.end()));
+    }
+
+    template <typename Seq1, typename Seq2>
+    void assign(const Seq1& vertices, const Seq2& elements) {
+        using std::begin;
+        using std::end;
+
+        auto vi = begin(vertices);
+        auto ve = end(vertices);
+
+        auto ei = begin(elements);
+        auto ee = end(elements);
+
+        if (ei==ee) { // empty case
+            if (vi!=ve) {
+                throw std::runtime_error("vertex list too long");
+            }
+            clear();
+            return;
+        }
+
+        double left = *vi++;
+        if (vi==ve) {
+            throw std::runtime_error("vertex list too short");
+        }
+
+        clear();
+
+        double right = *vi++;
+        push_back(left, right, *ei++);
+
+        while (ei!=ee) {
+            if (vi==ve) {
+                throw std::runtime_error("vertex list too short");
+            }
+            double right = *vi++;
+            push_back(right, *ei++);
+        }
+
+        if (vi!=ve) {
+            throw std::runtime_error("vertex list too long");
+        }
+    }
+};
+
+// With X = void, present the element intervals only,
+// keeping othewise the same interface.
+
+template <> struct pw_elements<void> {
+    using size_type = pw_size_type;
+    static constexpr size_type npos = pw_npos;
+
+    std::vector<double> vertex_;
+
+    using value_type = std::pair<double, double>;
+
+    // ctors and assignment:
+
+    template <typename VSeq>
+    explicit pw_elements(const VSeq& vs) { assign(vs); }
+
+    pw_elements(std::initializer_list<double> vs) { assign(vs); }
+
+    pw_elements() = default;
+    pw_elements(pw_elements&&) = default;
+    pw_elements(const pw_elements&) = default;
+
+    template <typename Y>
+    explicit pw_elements(const pw_elements<Y>& from):
+        vertex_(from.vertex_) {}
+
+    pw_elements& operator=(pw_elements&&) = default;
+    pw_elements& operator=(const pw_elements&) = default;
+
+    // access:
+
+    auto intervals() const { return util::partition_view(vertex_); }
+    auto interval(size_type i) const { return intervals()[i]; }
+    value_type operator[](size_type i) const { return interval(i); }
+
+    auto bounds() const { return intervals().bounds(); }
+
+    size_type size() const { return vertex_.empty()? 0: vertex_.size()-1; }
+    bool empty() const { return vertex_.empty(); }
+
+    bool operator==(const pw_elements& x) const { return vertex_==x.vertex_; }
+    bool operator!=(const pw_elements& x) const { return !(*this==x); }
+
+    const auto& vertices() const { return vertex_; }
+
+    size_type index_of(double x) const {
+        if (empty()) return npos;
+
+        auto partn = intervals();
+        if (x == partn.bounds().second) return size()-1;
+        else return partn.index(x);
+    }
+
+    using const_iterator = indexed_const_iterator<pw_elements<void>>;
+    using iterator = const_iterator;
+
+    const_iterator cbegin() const { return const_iterator{this, 0}; }
+    const_iterator begin() const { return cbegin(); }
+    const_iterator cend() const { return const_iterator{this, size()}; }
+    const_iterator end() const { return cend(); }
+    value_type front() const { return (*this)[0]; }
+    value_type back() const { return (*this)[size()-1]; }
+
+    // mutating operations:
+
+    void reserve(size_type n) { vertex_.reserve(n+1); }
+    void clear() { vertex_.clear(); }
+
+    void push_back(double left, double right) {
+        if (!empty() && left!=vertex_.back()) {
+            throw std::runtime_error("noncontiguous element");
+        }
+
+        if (right<left) {
+            throw std::runtime_error("inverted element");
+        }
+
+        if (vertex_.empty()) vertex_.push_back(left);
+        vertex_.push_back(right);
+    }
+
+    void push_back(double right) {
+        if (empty()) {
+            throw std::runtime_error("require initial left vertex for element");
+        }
+        vertex_.push_back(right);
+    }
+
+    void assign(std::initializer_list<double> vs) {
+        assign(util::make_range(vs.begin(), vs.end()));
+    }
+
+    template <typename Seq1>
+    void assign(const Seq1& vertices) {
+        using std::begin;
+        using std::end;
+
+        auto vi = begin(vertices);
+        auto ve = end(vertices);
+
+        if (vi==ve) {
+            clear();
+            return;
+        }
+
+        double left = *vi++;
+        if (vi==ve) {
+            throw std::runtime_error("vertex list too short");
+        }
+
+        clear();
+
+        double right = *vi++;
+        push_back(left, right);
+
+        while (vi!=ve) {
+            double right = *vi++;
+            push_back(right);
+        }
+    }
+};
+
+template <typename X>
+using pw_element = typename pw_elements<X>::value_type;
+
+namespace impl {
+    template <typename A, typename B>
+    struct piecewise_pairify {
+        std::pair<A, B> operator()(
+            double left, double right,
+            const pw_element<A> a_elem,
+            const pw_element<B> b_elem) const
+         {
+            return {a_elem.second, b_elem.second};
+        }
+    };
+
+    template <typename X>
+    struct piecewise_pairify<X, void> {
+        X operator()(
+            double left, double right,
+            const pw_element<X> a_elem,
+            const pw_element<void> b_elem) const
+        {
+            return a_elem.second;
+        }
+    };
+
+    template <typename X>
+    struct piecewise_pairify<void, X> {
+        X operator()(
+            double left, double right,
+            const pw_element<void> a_elem,
+            const pw_element<X> b_elem) const
+        {
+            return b_elem.second;
+        }
+    };
+}
+
+// TODO: Consider making a lazy `zip_view` version of zip.
+
+// Combine functional takes four arguments: 
+//     double left, double right, pw_elements<A>::value_type, pw_elements<B>::value_type b>
+//
+// Default combine functional returns std::pair<A, B>, unless one of A and B is void.
+
+template <typename A, typename B, typename Combine = impl::piecewise_pairify<A, B>>
+auto zip(const pw_elements<A>& a, const pw_elements<B>& b, Combine combine = {})
+{
+    using Out = decltype(combine(0., 0., a.front(), b.front()));
+    pw_elements<Out> z;
+    if (a.empty() || b.empty()) return z;
+
+    double lmax = std::max(a.bounds().first, b.bounds().first);
+    double rmin = std::min(a.bounds().second, b.bounds().second);
+    if (rmin<lmax) return z;
+
+    double left = lmax;
+    pw_size_type ai = a.intervals().index(left);
+    pw_size_type bi = b.intervals().index(left);
+
+    if (rmin==left) {
+        z.push_back(left, left, combine(left, left, a[ai], b[bi]));
+        return z;
+    }
+
+    double a_right = a.interval(ai).second;
+    double b_right = b.interval(bi).second;
+
+    while (left<rmin) {
+        double right = std::min(a_right, b_right);
+        right = std::min(right, rmin);
+
+        z.push_back(left, right, combine(left, right, a[ai], b[bi]));
+        if (a_right<=right) {
+            a_right = a.interval(++ai).second;
+        }
+        if (b_right<=right) {
+            b_right = b.interval(++bi).second;
+        }
+
+        left = right;
+    }
+    return z;
+}
+
+inline pw_elements<void> zip(const pw_elements<void>& a, const pw_elements<void>& b) {
+    pw_elements<void> z;
+    if (a.empty() || b.empty()) return z;
+
+    double lmax = std::max(a.bounds().first, b.bounds().first);
+    double rmin = std::min(a.bounds().second, b.bounds().second);
+    if (rmin<lmax) return z;
+
+    double left = lmax;
+    pw_size_type ai = a.intervals().index(left);
+    pw_size_type bi = b.intervals().index(left);
+
+    if (rmin==left) {
+        z.push_back(left, left);
+        return z;
+    }
+
+    double a_right = a.interval(ai).second;
+    double b_right = b.interval(bi).second;
+
+    while (left<rmin) {
+        double right = std::min(a_right, b_right);
+        right = std::min(right, rmin);
+
+        z.push_back(left, right);
+        if (a_right<=right) {
+            a_right = a.interval(++ai).second;
+        }
+        if (b_right<=right) {
+            b_right = b.interval(++bi).second;
+        }
+
+        left = right;
+    }
+    return z;
+}
+
+
+} // namespace util
+} // namespace arb
diff --git a/arbor/util/range.hpp b/arbor/util/range.hpp
index 9f175abf98e06e971e8280e28755bf6d8ffe41a7..7eaddb5369e0c14424ff629620d4babf2c114096 100644
--- a/arbor/util/range.hpp
+++ b/arbor/util/range.hpp
@@ -28,7 +28,6 @@
 
 #include <arbor/assert.hpp>
 
-#include "util/either.hpp"
 #include "util/counter.hpp"
 #include "util/iterutil.hpp"
 #include "util/meta.hpp"
diff --git a/arbor/util/rangeutil.hpp b/arbor/util/rangeutil.hpp
index 25e74a0ca83384cef258c46eef172cdc25a3d240..224d228753858837b083736f65a4a0bb43a8ce5e 100644
--- a/arbor/util/rangeutil.hpp
+++ b/arbor/util/rangeutil.hpp
@@ -193,6 +193,17 @@ bool any_of(const Seq& seq, const Predicate& pred) {
     return std::any_of(canon.begin(), canon.end(), pred);
 }
 
+// Accumulate over range
+
+template <
+    typename Seq,
+    typename Value = typename util::sequence_traits<Seq>::value_type
+>
+Value sum(const Seq& seq, Value base = Value{}) {
+    auto canon = canonical_view(seq);
+    return std::accumulate(canon.begin(), canon.end(), base);
+}
+
 // Accumulate by projection `proj`
 
 template <
diff --git a/arbor/util/ratelem.hpp b/arbor/util/ratelem.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7a01cada42df3e244f7a1ca726cb8a87e52b9796
--- /dev/null
+++ b/arbor/util/ratelem.hpp
@@ -0,0 +1,173 @@
+#pragma once
+
+// An element representing a segment of a rational polynomial function
+// of order p, q, as determined by its values on n = p+q+1 nodes at
+// [0, 1/n, ..., 1].
+//
+// Rational polynomial interpolation scheme from:
+// F. M. Larkin (1967). Some techniques for rational interpolation.
+// The Computer Journal 10(2), pp. 178–187.
+//
+// TODO: Consider implementing a more generally robust scheme, e.g.
+// S. L. Loi and A. W. McInnes (1983). An algorithm for generalized
+// rational interpolation. BIT Numerical Mathematics 23(1),
+// pp. 105–117. doi:10.1007/BF01937330
+//
+// Current implementation should be sufficient for interpolation of
+// monotonic ratpoly functions of low order, but we can revisit
+// this is if the limitations of the Larkin method bite us.
+
+#include <algorithm>
+#include <array>
+#include <functional>
+#include <type_traits>
+#include <utility>
+
+#include <arbor/math.hpp>
+
+namespace arb {
+namespace util {
+
+namespace impl {
+
+template <unsigned n, unsigned sz>
+struct array_init_n {
+    template <typename A, typename X, typename... Tail>
+    static void set(A& array, X value, Tail... tail) {
+        array[sz-n] = std::move(value);
+        array_init_n<n-1, sz>::set(array, std::forward<Tail>(tail)...);
+    }
+};
+
+template <unsigned sz>
+struct array_init_n<0, sz> {
+    template <typename A>
+    static void set(A& array) {}
+};
+
+// TODO: C++17. The following is much nicer with if constexpr...
+
+template <unsigned a, unsigned c, unsigned k, bool upper>
+struct rat_eval {
+    static double eval(const std::array<double, 1+a+c>& g, double x) {
+        std::array<double, a+c> h;
+        interpolate(h, g, x);
+        return rat_eval<a-1, c, k+1, upper>::eval(h, g, x);
+    }
+
+    static double eval(const std::array<double, 1+a+c>& g, const std::array<double, 2+a+c>&, double x) {
+        std::array<double, a+c> h;
+        interpolate(h, g, x);
+        return rat_eval<a-1, c, k+1, upper>::eval(h, g, x);
+    }
+
+    static void interpolate(std::array<double, a+c>& h, const std::array<double, a+c+1>& g, double x) {
+        constexpr double ook = 1./k;
+        for (unsigned i = 0; i<a+c; ++i) {
+            if (upper) {
+                h[i] = ook*((x - i)*g[i+1] + (i+k - x)*g[i]);
+            }
+            else {
+                // Using h[i] = k/(g[i+1]/(x - i) + g[i]/(i+k - x)) is more robust to
+                // singularities, but the expense should not be necessary if we stick
+                // to strictly monotonic elements for rational polynomials.
+                h[i] = k*g[i]*g[i+1]/(g[i]*(x - i) + g[i+1]*(i+k - x));
+            }
+        }
+    }
+};
+
+template <unsigned k, bool upper>
+struct rat_eval<0, 0, k, upper> {
+    static double eval(const std::array<double, 1>& g, double) {
+        return g[0];
+    }
+
+    static double eval(const std::array<double, 1>& g, const std::array<double, 2>&, double) {
+        return g[0];
+    }
+};
+
+template <unsigned c, unsigned k, bool upper>
+struct rat_eval<0, c, k, upper> {
+    static double eval(const std::array<double, 1+c>& g, const std::array<double, 2+c>& p, double x) {
+        // 'rhombus' interpolation
+        std::array<double, c> h;
+        for (unsigned i = 0; i<c; ++i) {
+            h[i] = p[i+1] + k/((x - i)/(g[i+1]-p[i+1]) + (i+k - x)/(g[i]-p[i+1]));
+        }
+
+        return rat_eval<0, c-1, k+1, upper>::eval(h, g, x);
+    }
+};
+
+} // namespace impl
+
+template <unsigned p, unsigned q>
+struct rat_element {
+    // Construct from function evaluated on nodes.
+    template <
+        typename F,
+        typename _ = std::enable_if_t<std::is_convertible<decltype(std::declval<F>()(0.0)), double>::value>
+    >
+    explicit rat_element(F&& fn, _* = nullptr) {
+        if (size()>1) {
+            for (unsigned i = 0; i<size(); ++i) data_[i] = fn(i/(size()-1.0));
+        }
+        else {
+            data_[0] = fn(0);
+        }
+    }
+
+    // Construct from node values.
+    template <typename... Tail>
+    rat_element(double y0, Tail... tail) {
+        impl::array_init_n<p+q+1, p+q+1>::set(data_, y0, tail...);
+    }
+
+    // Construct from node values in array or std::array.
+    template <typename Y>
+    rat_element(const std::array<Y, 1+p+q>& a) { unchecked_range_init(a); }
+
+    template <typename Y>
+    rat_element(Y (&a)[1+p+q]) { unchecked_range_init(a); }
+
+    // Number of nodes.
+    static constexpr unsigned size() { return 1+p+q; }
+
+    // Rational interpolation at x.
+    double operator()(double x) const {
+        // upper j => interpolate polynomials first;
+        // !upper => interpolate reciprocal polynomials first;
+        // a is the number of (reciprocal) polynomial interpolation steps;
+        // c is the number of 'rhombus' interpolation steps.
+
+        constexpr bool upper = p>=q;
+        constexpr unsigned a = upper? p-q+(q>0): q-p+(p>0);
+        constexpr unsigned c = p+q-a;
+
+        return impl::rat_eval<a, c, 1, upper>::eval(data_, x*(p+q));
+    }
+
+    // Node values.
+    double operator[](unsigned i) const {
+        return data_.at(i);
+    }
+
+    double& operator[](unsigned i) {
+        return data_.at(i);
+    }
+
+private:
+    template <typename C>
+    void unchecked_range_init(const C& a) {
+        using std::begin;
+        using std::end;
+        std::copy(begin(a), end(a), data_.begin());
+    }
+
+    std::array<double, 1+p+q> data_;
+};
+
+} // namespace util
+} // namespace arb
diff --git a/arbor/util/sentinel.hpp b/arbor/util/sentinel.hpp
index 09b985130a584398f4fa285996b368dddaca3258..7a47227e80931b672573efaeb71812b66f15016b 100644
--- a/arbor/util/sentinel.hpp
+++ b/arbor/util/sentinel.hpp
@@ -2,7 +2,8 @@
 
 #include <type_traits>
 
-#include <util/meta.hpp>
+#include <arbor/util/either.hpp>
+#include "util/meta.hpp"
 
 /*
  * Use a proxy iterator to present a range as having the same begin and
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 3e557a08a44667baa0f9861184e02ad6e2bd1da6..8deea24dcce1ab01983d8a176b770427ad338faa 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -89,7 +89,6 @@ set(unit_sources
     test_dry_run_context.cpp
     test_double_buffer.cpp
     test_either.cpp
-    test_em_morphology.cpp
     test_event_binner.cpp
     test_event_delivery.cpp
     test_event_generators.cpp
@@ -113,15 +112,20 @@ set(unit_sources
     test_mechinfo.cpp
     test_merge_events.cpp
     test_morphology.cpp
+    test_morph_embedding.cpp
+    test_morph_expr.cpp
+    test_morph_primitives.cpp
     test_multi_event_stream.cpp
     test_optional.cpp
     test_padded.cpp
     test_partition.cpp
     test_partition_by_constraint.cpp
     test_path.cpp
+    test_piecewise.cpp
     test_point.cpp
     test_probe.cpp
     test_range.cpp
+    test_ratelem.cpp
     test_segment.cpp
     test_schedule.cpp
     test_spike_source.cpp
diff --git a/test/unit/test_algorithms.cpp b/test/unit/test_algorithms.cpp
index e77e3f9978007d18d3189d0b2f31fd227f18fdba..ab975e0cef8ab41a292856e02875dc6bd332837b 100644
--- a/test/unit/test_algorithms.cpp
+++ b/test/unit/test_algorithms.cpp
@@ -9,28 +9,13 @@
 #include "algorithms.hpp"
 #include "util/index_into.hpp"
 #include "util/meta.hpp"
+#include "util/rangeutil.hpp"
 
 // (Pending abstraction of threading interface)
 #include <arbor/version.hpp>
 #include "threading/threading.hpp"
 #include "common.hpp"
 
-TEST(algorithms, sum)
-{
-    // sum of 10 times 2 is 20
-    std::vector<int> v1(10, 2);
-    EXPECT_EQ(10*2, arb::algorithms::sum(v1));
-
-    // make an array 1:20 and sum it up using formula for arithmetic sequence
-    auto n = 20;
-    std::vector<int> v2(n);
-    // can't use iota because the Intel compiler optimizes it out, despite
-    // the result being required in EXPECT_EQ
-    // std::iota(v2.begin(), v2.end(), 1);
-    for(auto i=0; i<n; ++i) { v2[i] = i+1; }
-    EXPECT_EQ((n+1)*n/2, arb::algorithms::sum(v2));
-}
-
 TEST(algorithms, make_index)
 {
     {
@@ -39,7 +24,7 @@ TEST(algorithms, make_index)
 
         EXPECT_EQ(index.size(), 11u);
         EXPECT_EQ(index.front(), 0);
-        EXPECT_EQ(index.back(), arb::algorithms::sum(v));
+        EXPECT_EQ(index.back(), arb::util::sum(v));
     }
 
     {
@@ -49,7 +34,7 @@ TEST(algorithms, make_index)
 
         EXPECT_EQ(index.size(), 11u);
         EXPECT_EQ(index.front(), 0);
-        EXPECT_EQ(index.back(), arb::algorithms::sum(v));
+        EXPECT_EQ(index.back(), arb::util::sum(v));
     }
 }
 
diff --git a/test/unit/test_compartments.cpp b/test/unit/test_compartments.cpp
index a86ef3e65f0850894e1af0ed7134abcbc29bb4a0..deb5996ee18d806e63a4d26fa9d6552af3fbc136 100644
--- a/test/unit/test_compartments.cpp
+++ b/test/unit/test_compartments.cpp
@@ -5,8 +5,8 @@
 
 #include <arbor/math.hpp>
 
-#include "algorithms.hpp"
 #include "fvm_compartment.hpp"
+#include "util/rangeutil.hpp"
 #include "util/span.hpp"
 #include "util/transform.hpp"
 
@@ -15,7 +15,7 @@ using namespace arb::math;
 
 using arb::util::make_span;
 using arb::util::transform_view;
-using arb::algorithms::sum;
+using arb::util::sum;
 
 // Divided compartments
 // (FVM-friendly compartment data)
diff --git a/test/unit/test_cv_policy.cpp b/test/unit/test_cv_policy.cpp
index 273a413ef8423372ff8a63a9987bc616de4a67db..aa11e16782001a8dc4bfcbc496f26a1ab366a2f1 100644
--- a/test/unit/test_cv_policy.cpp
+++ b/test/unit/test_cv_policy.cpp
@@ -8,8 +8,8 @@
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/locset.hpp>
+#include <arbor/morph/mprovider.hpp>
 
-#include "morph/em_morphology.hpp"
 #include "util/filter.hpp"
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
@@ -72,7 +72,7 @@ TEST(cv_policy, explicit_policy) {
         cable_cell cell(m);
 
         locset result = pol.cv_boundary_points(cell);
-        EXPECT_EQ(thingify(lset, *cell.morphology()), thingify(result, *cell.morphology()));
+        EXPECT_EQ(thingify(lset, cell.provider()), thingify(result, cell.provider()));
     }
 }
 
@@ -90,10 +90,10 @@ TEST(cv_policy, empty_morphology) {
     };
 
     cable_cell cell(m_empty);
-    auto empty_loclist = thingify(ls::nil(), *cell.morphology());
+    auto empty_loclist = thingify(ls::nil(), cell.provider());
 
     for (auto& pol: policies) {
-        EXPECT_EQ(empty_loclist, thingify(pol.cv_boundary_points(cell), *cell.morphology()));
+        EXPECT_EQ(empty_loclist, thingify(pol.cv_boundary_points(cell), cell.provider()));
     }
 }
 
@@ -115,8 +115,8 @@ TEST(cv_policy, single_root_cv) {
         cable_cell cell(morph);
 
         for (auto& polpair: policy_pairs) {
-            mlocation_list p1 = thingify(polpair.first.cv_boundary_points(cell), *cell.morphology());
-            mlocation_list p2 = thingify(polpair.second.cv_boundary_points(cell), *cell.morphology());
+            mlocation_list p1 = thingify(polpair.first.cv_boundary_points(cell), cell.provider());
+            mlocation_list p2 = thingify(polpair.second.cv_boundary_points(cell), cell.provider());
 
             auto p1_no_b0 = util::filter(p1, [](mlocation l) { return l.branch>0; });
             mlocation_list expected = {{0,0}, {0,1}};
@@ -139,14 +139,14 @@ TEST(cv_policy, fixed_per_branch) {
             cv_policy pol = cv_policy_fixed_per_branch(4);
             locset points = pol.cv_boundary_points(cell);
             locset expected = as_locset(L{0, 0}, L{0, 0.25}, L{0, 0.5}, L{0, 0.75}, L{0, 1});
-            EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology()));
+            EXPECT_EQ(thingify(expected, cell.provider()), thingify(points, cell.provider()));
         }
         {
             // interior fork points
             cv_policy pol = cv_policy_fixed_per_branch(4, interior_forks);
             locset points = pol.cv_boundary_points(cell);
             locset expected = as_locset(L{0, 0.125}, L{0, 0.375}, L{0, 0.625}, L{0, 0.875});
-            EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology()));
+            EXPECT_EQ(thingify(expected, cell.provider()), thingify(points, cell.provider()));
         }
     }
 
@@ -163,7 +163,7 @@ TEST(cv_policy, fixed_per_branch) {
                 L{0, 0}, L{0, 0.5}, L{0,1}, L{1, 0}, L{1, 0.5}, L{1,1}, L{2, 0}, L{2, 0.5}, L{2,1},
                 L{3, 0}, L{3, 0.5}, L{3,1}, L{4, 0}, L{4, 0.5}, L{4,1}, L{5, 0}, L{5, 0.5}, L{5,1}
             );
-            EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology()));
+            EXPECT_EQ(thingify(expected, cell.provider()), thingify(points, cell.provider()));
         }
         {
             // interior fork points
@@ -173,7 +173,7 @@ TEST(cv_policy, fixed_per_branch) {
                 L{0, 0.25}, L{0, 0.75}, L{1, 0.25}, L{1, 0.75}, L{2, 0.25}, L{2, 0.75},
                 L{3, 0.25}, L{3, 0.75}, L{4, 0.25}, L{4, 0.75}, L{5, 0.25}, L{5, 0.75}
             );
-            EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology()));
+            EXPECT_EQ(thingify(expected, cell.provider()), thingify(points, cell.provider()));
         }
     }
 }
@@ -185,7 +185,7 @@ TEST(cv_policy, max_extent) {
     // root branch only
     for (auto& morph: {m_sph_b1, m_reg_b1}) {
         cable_cell cell(morph);
-        ASSERT_EQ(1.0, cell.morphology()->branch_length(0));
+        ASSERT_EQ(1.0, cell.embedding().branch_length(0));
 
         {
             // extent of 0.25 should give exact fp calculation, giving
@@ -193,31 +193,31 @@ TEST(cv_policy, max_extent) {
             cv_policy pol = cv_policy_max_extent(0.25);
             locset points = pol.cv_boundary_points(cell);
             locset expected = as_locset(L{0, 0}, L{0, 0.25}, L{0, 0.5}, L{0, 0.75}, L{0, 1});
-            EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology()));
+            EXPECT_EQ(thingify(expected, cell.provider()), thingify(points, cell.provider()));
         }
         {
             cv_policy pol = cv_policy_max_extent(0.25, interior_forks);
             locset points = pol.cv_boundary_points(cell);
             locset expected = as_locset(L{0, 0.125}, L{0, 0.375}, L{0, 0.625}, L{0, 0.875});
-            EXPECT_EQ(thingify(expected, *cell.morphology()), thingify(points, *cell.morphology()));
+            EXPECT_EQ(thingify(expected, cell.provider()), thingify(points, cell.provider()));
         }
     }
 
     // cell with varying branch lengths; extent not exact fraction.
     {
         cable_cell cell(m_mlt_b6);
-        ASSERT_EQ(1.0, cell.morphology()->branch_length(0));
-        ASSERT_EQ(1.0, cell.morphology()->branch_length(1));
-        ASSERT_EQ(2.0, cell.morphology()->branch_length(2));
-        ASSERT_EQ(4.0, cell.morphology()->branch_length(3));
-        ASSERT_EQ(1.0, cell.morphology()->branch_length(4));
-        ASSERT_EQ(2.0, cell.morphology()->branch_length(5));
+        ASSERT_EQ(1.0, cell.embedding().branch_length(0));
+        ASSERT_EQ(1.0, cell.embedding().branch_length(1));
+        ASSERT_EQ(2.0, cell.embedding().branch_length(2));
+        ASSERT_EQ(4.0, cell.embedding().branch_length(3));
+        ASSERT_EQ(1.0, cell.embedding().branch_length(4));
+        ASSERT_EQ(2.0, cell.embedding().branch_length(5));
 
         {
             // max extent of 0.6 should give two CVs on branches of length 1,
             // four CVs on branches of length 2, and seven CVs on the branch of length 4.
             cv_policy pol = cv_policy_max_extent(0.6);
-            mlocation_list points = thingify(pol.cv_boundary_points(cell), *cell.morphology());
+            mlocation_list points = thingify(pol.cv_boundary_points(cell), cell.provider());
 
             mlocation_list points_b012 = util::assign_from(util::filter(points, [](mlocation l) { return l.branch<3; }));
             mlocation_list expected_b012 = {
diff --git a/test/unit/test_either.cpp b/test/unit/test_either.cpp
index 79344e211edccf0fdec3d9585245f6d7a55e10c3..4d18d47c60f1b126c5a09cca7d52a723cf4ed47b 100644
--- a/test/unit/test_either.cpp
+++ b/test/unit/test_either.cpp
@@ -2,8 +2,9 @@
 #include <array>
 #include <algorithm>
 
+#include <arbor/util/either.hpp>
+
 #include "../gtest.h"
-#include "util/either.hpp"
 
 // TODO: coverage!
 
diff --git a/test/unit/test_em_morphology.cpp b/test/unit/test_em_morphology.cpp
deleted file mode 100644
index 36cbf05f5b1e0a686445c70db71cd47d3097f70a..0000000000000000000000000000000000000000
--- a/test/unit/test_em_morphology.cpp
+++ /dev/null
@@ -1,694 +0,0 @@
-/*
- * Unit tests for em_morphology, region, locset, label_dict.
- */
-
-#include <cmath>
-#include <string>
-#include <vector>
-
-#include "../test/gtest.h"
-
-#include <arbor/math.hpp>
-#include <arbor/morph/error.hpp>
-#include <arbor/morph/morphology.hpp>
-#include <arbor/morph/sample_tree.hpp>
-#include <arbor/morph/locset.hpp>
-#include <arbor/morph/region.hpp>
-
-#include "io/sepval.hpp"
-#include "morph/em_morphology.hpp"
-#include "util/span.hpp"
-
-template <typename T>
-std::ostream& operator<<(std::ostream& o, const std::vector<T>& v) {
-    return o << "[" << arb::io::csv(v) << "]";
-}
-
-// Test the morphology meta-data that is cached on construction of
-// em_morpholgy, e.g. interpolation information and terminal nodes.
-TEST(em_morphology, cache) {
-    using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
-    using loc = arb::mlocation;
-    constexpr auto npos = arb::mnpos;
-
-    // A single unbranched cable with 5 sample points.
-    // The cable has length 10 μm, with samples located at
-    // 0 μm, 1 μm, 3 μm, 7 μm and 10 μm.
-    {
-        pvec parents = {npos, 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},
-        };
-        arb::sample_tree sm(samples, parents);
-        arb::em_morphology em(arb::morphology(sm, false));
-        EXPECT_EQ(em.sample2loc(0), (loc{0,0}));
-        EXPECT_EQ(em.sample2loc(1), (loc{0,0.1}));
-        EXPECT_EQ(em.sample2loc(2), (loc{0,0.3}));
-        EXPECT_EQ(em.sample2loc(3), (loc{0,0.7}));
-        EXPECT_EQ(em.sample2loc(4), (loc{0,1}));
-
-        EXPECT_EQ(em.root(),      (loc{0,0}));
-        EXPECT_EQ(em.terminals(), (arb::mlocation_list{{0,1}}));
-
-        EXPECT_EQ(10., em.branch_length(0));
-    }
-
-    // Eight samples
-    //
-    //  sample ids:
-    //            0
-    //           1 3
-    //          2   4
-    //             5 6
-    //                7
-    {   // Spherical root.
-        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::em_morphology em(arb::morphology(sm, true));
-
-        EXPECT_EQ(em.sample2loc(0), (loc{0,0}));
-        EXPECT_EQ(em.sample2loc(1), (loc{1,0}));
-        EXPECT_EQ(em.sample2loc(2), (loc{1,1}));
-        EXPECT_EQ(em.sample2loc(3), (loc{2,0}));
-        EXPECT_EQ(em.sample2loc(4), (loc{2,1}));
-        EXPECT_EQ(em.sample2loc(5), (loc{3,1}));
-        EXPECT_EQ(em.sample2loc(6), (loc{4,0.5}));
-        EXPECT_EQ(em.sample2loc(7), (loc{4,1}));
-
-        EXPECT_EQ(em.root(),      (loc{0,0}));
-        EXPECT_EQ(em.terminals(), (arb::mlocation_list{{1,1}, {3,1}, {4,1}}));
-
-        ASSERT_EQ(5u, em.morph().num_branches());
-        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 = {npos, 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},
-        };
-        arb::sample_tree sm(samples, parents);
-        arb::em_morphology em(arb::morphology(sm, false));
-
-        EXPECT_EQ(em.sample2loc(0), (loc{0,0}));
-        EXPECT_EQ(em.sample2loc(1), (loc{0,0.1}));
-        EXPECT_EQ(em.sample2loc(2), (loc{0,1}));
-        EXPECT_EQ(em.sample2loc(3), (loc{1,0.1}));
-        EXPECT_EQ(em.sample2loc(4), (loc{1,1}));
-        EXPECT_EQ(em.sample2loc(5), (loc{2,1}));
-        EXPECT_EQ(em.sample2loc(6), (loc{3,0.15}));
-        EXPECT_EQ(em.sample2loc(7), (loc{3,1}));
-
-        EXPECT_EQ(em.root(),      (loc{0,0}));
-        EXPECT_EQ(em.terminals(), (arb::mlocation_list{{0,1}, {2,1}, {3,1}}));
-
-        ASSERT_EQ(4u, em.morph().num_branches());
-        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));
-    }
-}
-
-TEST(em_morphology, cover) {
-    using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
-    using ll = arb::mlocation_list;
-    constexpr auto npos = arb::mnpos;
-    using arb::util::make_span;
-
-    // Eight samples
-    //            0
-    //           1 3
-    //          2   4
-    //             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);
-
-    // non-spherical root.
-    {
-        arb::em_morphology em(arb::morphology(sm, false));
-
-        for (arb::msize_t i: make_span(em.morph().num_branches())) {
-            for (auto j: make_span(1,9)) {
-                arb::mlocation l{i, j/10.};
-                EXPECT_EQ(em.cover(l), (ll{l}));
-            }
-        }
-
-        EXPECT_EQ(em.cover({0,0}), (ll{{0,0}, {1,0}}));
-        EXPECT_EQ(em.cover({0,1}), (ll{{0,1}}));
-        EXPECT_EQ(em.cover({1,1}), (ll{{1,1}, {2,0}, {3,0}}));
-        EXPECT_EQ(em.cover({2,1}), (ll{{2,1}}));
-        EXPECT_EQ(em.cover({3,1}), (ll{{3,1}}));
-
-        EXPECT_EQ(em.cover({1,0}), (ll{{0,0}, {1,0}}));
-        EXPECT_EQ(em.cover({2,0}), (ll{{1,1}, {2,0}, {3,0}}));
-        EXPECT_EQ(em.cover({3,0}), (ll{{1,1}, {2,0}, {3,0}}));
-    }
-    // spherical root.
-    {
-        arb::em_morphology em(arb::morphology(sm, true));
-
-        EXPECT_EQ(em.cover({0,0}), (ll{{0,0}}));
-        EXPECT_EQ(em.cover({0,1}), (ll{{0,1}, {1,0}, {2,0}}));
-        EXPECT_EQ(em.cover({1,1}), (ll{{1,1}}));
-        EXPECT_EQ(em.cover({2,1}), (ll{{2,1}, {3,0}, {4,0}}));
-        EXPECT_EQ(em.cover({3,1}), (ll{{3,1}}));
-        EXPECT_EQ(em.cover({4,1}), (ll{{4,1}}));
-        EXPECT_EQ(em.cover({1,0}), (ll{{0,1}, {1,0}, {2,0}}));
-        EXPECT_EQ(em.cover({2,0}), (ll{{0,1}, {1,0}, {2,0}}));
-        EXPECT_EQ(em.cover({3,0}), (ll{{2,1}, {3,0}, {4,0}}));
-        EXPECT_EQ(em.cover({4,0}), (ll{{2,1}, {3,0}, {4,0}}));
-    }
-}
-
-TEST(em_morphology, minset) {
-    using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
-    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::em_morphology em(arb::morphology(sm, false));
-
-        EXPECT_EQ((ll{}), em.minset(ll{}));
-        EXPECT_EQ((ll{{2,0.1}}), em.minset(ll{{2,0.1}}));
-        EXPECT_EQ((ll{{0,0.5}, {1,0.5}}), em.minset(ll{{0,0.5}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}}), em.minset(ll{{0,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {1,0}}), em.minset(ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {1,0.5}}), em.minset(ll{{0,0}, {0,0.5}, {1,0.5}, {2,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {2,0.5}}), em.minset(ll{{0,0}, {0,0.5}, {2,0.5}}));
-        EXPECT_EQ((ll{{0,0}, {2,0.5}, {3,0}}), em.minset(ll{{0,0}, {0,0.5}, {2,0.5}, {3,0}, {3,1}}));
-        EXPECT_EQ((ll{{0,0}, {1,0}}), em.minset(ll{{0,0}, {0,0.5}, {1,0}, {2,0.5}, {3,0}, {3,1}}));
-    }
-    {
-        arb::em_morphology em(arb::morphology(sm, true));
-
-        EXPECT_EQ((ll{}), em.minset(ll{}));
-        EXPECT_EQ((ll{{2,0.1}}), em.minset(ll{{2,0.1}}));
-        EXPECT_EQ((ll{{0,0.5}}), em.minset(ll{{0,0.5}, {1,0.5}}));
-        EXPECT_EQ((ll{{0,0.5}}), em.minset(ll{{0,0.5}}));
-        EXPECT_EQ((ll{{0,0}}), em.minset(ll{{0,0}, {0,0.5}, {1,0}, {1,0.5}}));
-        EXPECT_EQ((ll{{1,0.5}, {3,0.1}, {4,0.5}}), em.minset(ll{{1,0.5}, {1,1}, {3,0.1}, {4,0.5}, {4,0.7}}));
-    }
-}
-
-// Test expressions that describe location sets (locset).
-TEST(locset, expressions) {
-    auto root = arb::ls::root();
-    auto term = arb::ls::terminal();
-    auto samp = arb::ls::sample(42);
-    auto loc = arb::ls::location({2, 0.5});
-
-    auto to_string = [](auto&& x) {
-        std::stringstream s;
-        s << x;
-        return s.str();
-    };
-
-    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(loc), "(location 2 0.5)");
-
-    // Location positions have to be in the range [0,1].
-    // Assert that an exception is thrown if and invalide location is requested.
-    EXPECT_THROW(arb::ls::location({2, 1.5}), arb::morphology_error);
-    EXPECT_THROW(arb::ls::location({arb::mnpos, 0.5}), arb::morphology_error);
-}
-
-// Test expressions that describe regions.
-TEST(region, expressions) {
-    using arb::reg::cable;
-    auto to_string = [](auto&& x) {
-        std::stringstream s;
-        s << x;
-        return s.str();
-    };
-
-    auto c1 = arb::reg::cable({1,0,1});
-    auto c2 = arb::reg::cable({4,0.1,0.5});
-    auto c3 = join(cable({4,0.1,0.5}), cable({3,0,1}));
-    auto b1 = arb::reg::branch(1);
-    auto t1 = arb::reg::tagged(1);
-    auto t2 = arb::reg::tagged(2);
-    auto t3 = arb::reg::tagged(3);
-    auto all = arb::reg::all();
-
-    EXPECT_EQ(to_string(c1), "(cable 1 0 1)");
-    EXPECT_EQ(to_string(c2), "(cable 4 0.1 0.5)");
-    EXPECT_EQ(to_string(c3), "(join (cable 4 0.1 0.5) (cable 3 0 1))");
-    EXPECT_EQ(to_string(b1), "(cable 1 0 1)");
-    EXPECT_EQ(to_string(t1), "(tag 1)");
-    EXPECT_EQ(to_string(t2), "(tag 2)");
-    EXPECT_EQ(to_string(intersect(c1, t2)), "(intersect (cable 1 0 1) (tag 2))");
-    EXPECT_EQ(to_string(join(c1, t2)),  "(join (cable 1 0 1) (tag 2))");
-    EXPECT_EQ(to_string(join(t1, t2, t3)), "(join (join (tag 1) (tag 2)) (tag 3))");
-    EXPECT_EQ(to_string(intersect(t1, t2, t3)), "(intersect (intersect (tag 1) (tag 2)) (tag 3))");
-    EXPECT_EQ(to_string(intersect(join(c1, t2), c2)),  "(intersect (join (cable 1 0 1) (tag 2)) (cable 4 0.1 0.5))");
-    EXPECT_EQ(to_string(all), "all");
-
-    EXPECT_THROW(arb::reg::cable({1, 0, 1.1}), arb::morphology_error);
-    EXPECT_THROW(arb::reg::branch(-1), arb::morphology_error);
-}
-
-TEST(locset, thingify) {
-    using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
-    using ll = arb::mlocation_list;
-    const auto npos = arb::mnpos;
-
-    auto root = arb::ls::root();
-    auto term = arb::ls::terminal();
-    auto samp = arb::ls::sample(4);
-    auto midb2 = arb::ls::location({2, 0.5});
-    auto midb1 = arb::ls::location({1, 0.5});
-    auto begb0 = arb::ls::location({0, 0});
-    auto begb1 = arb::ls::location({1, 0});
-    auto begb2 = arb::ls::location({2, 0});
-    auto begb3 = arb::ls::location({3, 0});
-    auto begb4 = arb::ls::location({4, 0});
-
-    // Eight samples
-    //
-    //            0
-    //           1 3
-    //          2   4
-    //             5 6
-    //                7
-    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::em_morphology em(arb::morphology(sm, true));
-
-        EXPECT_EQ(thingify(root, em),  (ll{{0,0}}));
-        EXPECT_EQ(thingify(term, em),  (ll{{1,1},{3,1},{4,1}}));
-        EXPECT_EQ(thingify(samp, em),  (ll{{2,1}}));
-        EXPECT_EQ(thingify(midb2, em), (ll{{2,0.5}}));
-        EXPECT_EQ(thingify(midb1, em), (ll{{1,0.5}}));
-        EXPECT_EQ(thingify(begb0, em), (ll{{0,0}}));
-        EXPECT_EQ(thingify(begb1, em), (ll{{1,0}}));
-        EXPECT_EQ(thingify(begb2, em), (ll{{2,0}}));
-        EXPECT_EQ(thingify(begb3, em), (ll{{3,0}}));
-        EXPECT_EQ(thingify(begb4, em), (ll{{4,0}}));
-    }
-    {
-        arb::em_morphology em(arb::morphology(sm, false));
-
-        EXPECT_EQ(thingify(root, em),  (ll{{0,0}}));
-        EXPECT_EQ(thingify(term, em),  (ll{{0,1},{2,1},{3,1}}));
-        EXPECT_EQ(thingify(samp, em),  (ll{{1,1}}));
-        EXPECT_EQ(thingify(midb2, em), (ll{{2,0.5}}));
-        EXPECT_EQ(thingify(midb1, em), (ll{{1,0.5}}));
-        EXPECT_EQ(thingify(begb0, em), (ll{{0,0}}));
-        EXPECT_EQ(thingify(begb1, em), (ll{{1,0}}));
-        EXPECT_EQ(thingify(begb2, em), (ll{{2,0}}));
-        EXPECT_EQ(thingify(begb3, em), (ll{{3,0}}));
-        // In the absence of a spherical root, there is no branch 4.
-        EXPECT_THROW(thingify(begb4, em), arb::morphology_error);
-    }
-}
-
-// Forward declare implementation of join, union & intersect on location lists
-// for testing.
-namespace arb { namespace ls {
-    mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs);
-    mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs);
-    mlocation_list sum(const mlocation_list& lhs, const mlocation_list& rhs);
-}}
-
-arb::mlocation_list ml(std::vector<int> bids) {
-    arb::mlocation_list L;
-    for (arb::msize_t b: bids) L.push_back({b, 0});
-    return L;
-}
-
-
-// Test out multiset joins, intersections and sums
-TEST(locset, join_intersect_sum) {
-    using ll = arb::mlocation_list;
-    using arb::ls::intersection;
-    using arb::ls::sum;
-    using arb::ls::join;
-
-    {
-        ll lhs{};
-        ll rhs{};
-        EXPECT_EQ(sum(lhs, rhs), ll{});
-        EXPECT_EQ(join(lhs, rhs), ll{});
-        EXPECT_EQ(intersection(lhs, rhs), ll{});
-    }
-    {
-        ll lhs{};
-        ll rhs = ml({0,1});
-        EXPECT_EQ(sum(lhs, rhs), rhs);
-        EXPECT_EQ(join(lhs, rhs), rhs);
-        EXPECT_EQ(intersection(lhs, rhs), ll{});
-    }
-    {
-        ll lhs = ml({1});
-        ll rhs = ml({1});
-        EXPECT_EQ(sum(lhs,  rhs), ml({1,1}));
-        EXPECT_EQ(join(lhs, rhs), ml({1}));
-        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
-    }
-    {
-        ll lhs = ml({1,1});
-        ll rhs = ml({1});
-        EXPECT_EQ(sum(lhs,  rhs), ml({1,1,1}));
-        EXPECT_EQ(join(lhs, rhs), ml({1,1}));
-        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
-    }
-    {
-        ll lhs = ml({0,3});
-        ll rhs = ml({1,2});
-        EXPECT_EQ(sum(lhs,  rhs), ml({0,1,2,3}));
-        EXPECT_EQ(join(lhs, rhs), ml({0,1,2,3}));
-        EXPECT_EQ(intersection(lhs, rhs), ll{});
-    }
-    {
-        ll lhs = ml({0,1,3});
-        ll rhs = ml({0,1,3});
-        EXPECT_EQ(sum(lhs, rhs), ml({0,0,1,1,3,3}));
-        EXPECT_EQ(join(lhs, rhs), lhs);
-        EXPECT_EQ(intersection(lhs, rhs), lhs);
-    }
-    {
-        ll lhs = ml({0,1,3});
-        ll rhs = ml({1,2});
-        EXPECT_EQ(sum(lhs, rhs), ml({0,1,1,2,3}));
-        EXPECT_EQ(join(lhs, rhs), ml({0,1,2,3}));
-        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
-    }
-    {
-        ll lhs = ml({0,1,1,3});
-        ll rhs = ml({1,2});
-        EXPECT_EQ(sum(lhs, rhs), ml({0,1,1,1,2,3}));
-        EXPECT_EQ(join(lhs, rhs), ml({0,1,1,2,3}));
-        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
-    }
-    {
-        ll lhs = ml({0,1,1,3,5,5,12});
-        ll rhs = ml({1,2,2,5,5,5});
-        EXPECT_EQ(sum(lhs, rhs),  ml({0,1,1,1,2,2,3,5,5,5,5,5,12}));
-        EXPECT_EQ(join(lhs, rhs), ml({0,1,1,2,2,3,5,5,5,12}));
-        EXPECT_EQ(intersection(lhs, rhs), ml({1,5,5}));
-    }
-}
-
-TEST(region, thingify) {
-    using pvec = std::vector<arb::msize_t>;
-    using svec = std::vector<arb::msample>;
-    //using cab = arb::mcable;
-    using cl = arb::mcable_list;
-    constexpr auto npos = arb::mnpos;
-
-    // A single unbranched cable with 5 sample points.
-    // The cable has length 10 μm, with samples located at
-    // 0 μm, 1 μm, 3 μm, 7 μm and 10 μm.
-    {
-        pvec parents = {npos, 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},
-        };
-        arb::sample_tree sm(samples, parents);
-        arb::em_morphology em(arb::morphology(sm, false));
-
-        auto h1  = arb::reg::cable({0, 0, 0.5});
-        auto h2  = arb::reg::cable({0, 0.5, 1});
-        auto t1  = arb::reg::tagged(1);
-        auto t2  = arb::reg::tagged(2);
-        auto all = arb::reg::all();
-
-        // Concrete cable lists
-        cl h1_{{0, 0.0, 0.5}};
-        cl h2_{{0, 0.5, 1.0}};
-        cl t1_{{0, 0.0, 0.1}, {0, 0.3, 0.7}};
-        cl t2_{{0, 0.1, 0.3}, {0, 0.7, 1.0}};
-        cl all_{{0, 0, 1}};
-        cl empty_{};
-
-        EXPECT_EQ(thingify(h1, em), h1_);
-        EXPECT_EQ(thingify(h2, em), h2_);
-        EXPECT_EQ(thingify(t1, em), t1_);
-        EXPECT_EQ(thingify(t2, em), t2_);
-        EXPECT_EQ(thingify(join(h1, h2), em), all_);
-
-        EXPECT_EQ(thingify(intersect(h1, h2), em), (cl{{0, 0.5, 0.5}}));
-
-        EXPECT_EQ(thingify(intersect(h1, h1), em), h1_);
-        EXPECT_EQ(thingify(intersect(t1, t1), em), t1_);
-        EXPECT_EQ(thingify(join(t1, t2), em), all_);
-        EXPECT_EQ(thingify(intersect(all, t1), em), t1_);
-        EXPECT_EQ(thingify(intersect(all, t2), em), t2_);
-        EXPECT_EQ(thingify(join(all, t1), em), all_);
-        EXPECT_EQ(thingify(join(all, t2), em), all_);
-        EXPECT_EQ(thingify(join(h1, t1), em), (cl{{0, 0, 0.7}}));
-        EXPECT_EQ(thingify(join(h1, t2), em), (cl{{0, 0, 0.5}, {0, 0.7, 1}}));
-        EXPECT_EQ(thingify(intersect(h2, t1), em), (cl{{0, 0.5, 0.7}}));
-    }
-
-
-    // Test handling of spherical soma on multi-branch morphologies
-    //
-    //  sample ids:
-    //              0           |
-    //            1   3         |
-    //          2       4       |
-    //  tags:
-    //              1           |
-    //            3   2         |
-    //          3       2       |
-    {
-        pvec parents = {npos, 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},
-        };
-
-        // with a spherical root
-        arb::sample_tree sm(samples, parents);
-        arb::em_morphology em(arb::morphology(sm, true));
-
-        using arb::reg::tagged;
-        using arb::reg::branch;
-        using arb::reg::all;
-
-        EXPECT_EQ(thingify(tagged(1), em), (arb::mcable_list{{0,0,1}}));
-        EXPECT_EQ(thingify(tagged(2), em), (arb::mcable_list{{2,0,1}}));
-        EXPECT_EQ(thingify(tagged(3), em), (arb::mcable_list{{1,0,1}}));
-        EXPECT_EQ(thingify(join(tagged(1), tagged(2), tagged(3)), em), (arb::mcable_list{{0,0,1}, {1,0,1}, {2,0,1}}));
-        EXPECT_EQ(thingify(join(tagged(1), tagged(2), tagged(3)), em), thingify(all(), em));
-    }
-
-    // Test multi-level morphologies.
-    //
-    // Eight samples
-    //
-    //  sample ids:
-    //            0
-    //           1 3
-    //          2   4
-    //             5 6
-    //                7
-    //  tags:
-    //            1
-    //           3 2
-    //          3   2
-    //             4 3
-    //                3
-    {
-        pvec parents = {npos, 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}, 2},
-            {{  0,100,  0,  2}, 2},
-            {{100,100,  0,  2}, 4},
-            {{  0,200,  0,  2}, 3},
-            {{  0,300,  0,  2}, 3},
-        };
-        arb::sample_tree sm(samples, parents);
-
-        // Without spherical root
-        arb::em_morphology em(arb::morphology(sm, false));
-
-        using arb::reg::tagged;
-        using arb::reg::branch;
-        using arb::reg::all;
-        using arb::reg::cable;
-        using arb::mcable;
-        auto soma = tagged(1);
-        auto axon = tagged(2);
-        auto dend = tagged(3);
-        auto apic = tagged(4);
-        auto b1  = branch(1);
-        auto b3  = branch(3);
-        auto b13 = join(b1, b3);
-
-        cl empty_{};
-        cl soma_ = empty_;
-
-        // Whole branches:
-        mcable b0_{0,0,1};
-        mcable b1_{1,0,1};
-        mcable b2_{2,0,1};
-        mcable b3_{3,0,1};
-        cl all_  = {b0_,b1_,b2_,b3_};
-
-        mcable end1_{1,1,1};
-        mcable root_{0,0,0};
-
-        EXPECT_EQ(thingify(all(), em), all_);
-        EXPECT_EQ(thingify(soma, em), empty_);
-        EXPECT_EQ(thingify(axon, em), (cl{b1_}));
-        EXPECT_EQ(thingify(dend, em), (cl{b0_,b3_}));
-        EXPECT_EQ(thingify(apic, em), (cl{b2_}));
-        EXPECT_EQ(thingify(join(dend, apic), em), (cl{b0_,b2_,b3_}));
-        EXPECT_EQ(thingify(join(axon, join(dend, apic)), em), all_);
-
-        // Test that intersection correctly generates zero-length cables at
-        // parent-child interfaces.
-        EXPECT_EQ(thingify(intersect(apic, dend), em), (cl{end1_}));
-        EXPECT_EQ(thingify(intersect(apic, axon), em), (cl{end1_}));
-        EXPECT_EQ(thingify(intersect(axon, dend), em), (cl{root_, end1_}));
-
-        // Test some more interesting intersections and unions.
-
-        //    123456789 123456789
-        //   |---------|---------| lhs
-        //   |  -----  |   ---   | rhs
-        //   |  xxxxx  |   xxx   | rand
-        //   |xxxxxxxxx|xxxxxxxxx| ror
-        auto lhs  = b13;
-        auto rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
-        auto rand = cl{         {1,.2,.7}, {3,.3,.6}};
-        auto ror  = cl{         {1,.0,1.}, {3,.0,1.}};
-        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
-        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
-
-        // Assert communtativity
-        std::swap(lhs, rhs);
-        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
-        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
-
-        //    123456789 123456789
-        //   |   ----- | ----    | lhs
-        //   |  -----  |   ---   | rhs
-        //   |   xxxx  |   xx    | rand
-        //   |  xxxxxx | xxxxx   | ror
-        lhs  = join(cable({1,.3,.8}), cable({3,.1,.5}));
-        rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
-        rand = cl{         {1,.3,.7}, {3,.3,.5}};
-        ror  = cl{         {1,.2,.8}, {3,.1,.6}};
-        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
-        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
-
-        // Assert communtativity
-        std::swap(lhs, rhs);
-        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
-        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
-
-        //    123456789 123456789
-        //   | -- -    | --- --- | lhs
-        //   |  -----  |   ---   | rhs
-        //   |  x x    |   x x   | rand
-        //   | xxxxxx  | xxxxxxx | ror
-        lhs  = join(cable({1,.1,.3}), cable({1,.4,.5}), cable({3,.1,.4}), cable({3,.5,.9}));
-        rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
-        rand = cl{         {1,.2,.3}, {1,.4,.5}, {3,.3,.4}, {3,.5,.6}};
-        ror  = cl{         {1,.1,.7},            {3,.1,.9}};
-        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
-        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
-
-        // Assert communtativity
-        std::swap(lhs, rhs);
-        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
-        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
-    }
-}
-
diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp
index 2808d3ea8fb379791b938989fb8e411d9a67bd8f..e1a0a5f82ea84b4bba3156d15955b8765d53bc62 100644
--- a/test/unit/test_fvm_layout.cpp
+++ b/test/unit/test_fvm_layout.cpp
@@ -11,7 +11,6 @@
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
 #include "io/sepval.hpp"
-#include "morph/em_morphology.hpp"
 
 #include "common.hpp"
 #include "unit_test_catalogue.hpp"
diff --git a/test/unit/test_morph_embedding.cpp b/test/unit/test_morph_embedding.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0c72b62fec69a9819ae8e83fa7082c3f740ebd61
--- /dev/null
+++ b/test/unit/test_morph_embedding.cpp
@@ -0,0 +1,241 @@
+#include <cmath>
+#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 "util/piecewise.hpp"
+
+#include "../test/gtest.h"
+#include "common.hpp"
+
+using namespace arb;
+using embedding = embed_pwlin;
+
+::testing::AssertionResult location_eq(const morphology& m, mlocation a, mlocation b) {
+    a = canonical(m, a);
+    b = canonical(m, b);
+
+    if (a.branch!=b.branch) {
+        return ::testing::AssertionFailure()
+            << "branch ids " << a.branch << " and " << b.branch << " differ";
+    }
+
+    using FP = testing::internal::FloatingPoint<double>;
+    if (FP(a.pos).AlmostEquals(FP(b.pos))) {
+        return ::testing::AssertionSuccess();
+    }
+    else {
+        return ::testing::AssertionFailure()
+            << "location positions " << a.pos << " and " << b.pos << " differ";
+    }
+}
+
+TEST(embedding, samples_and_branch_length) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+    using loc = mlocation;
+
+    // A single unbranched cable with 5 sample points.
+    // The cable has length 10 μm, with samples located at
+    // 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},
+        };
+        sample_tree sm(samples, parents);
+        morphology m(sm, false);
+
+        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})));
+
+        EXPECT_EQ(10., em.branch_length(0));
+    }
+
+    // Eight samples
+    //
+    //  sample ids:
+    //            0
+    //           1 3
+    //          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);
+
+        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());
+
+        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));
+    }
+}
+
+// TODO: integrator tests
+
+
+TEST(embedding, partial_branch_length) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+    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}
+    };
+
+    morphology m(sample_tree(samples, parents), false);
+    embedding em(m);
+
+    EXPECT_DOUBLE_EQ(30., em.branch_length(0));
+    EXPECT_DOUBLE_EQ(30., em.integrate_length(mcable{0, 0., 1.}));
+    EXPECT_DOUBLE_EQ(15., em.integrate_length(mcable{0, 0.25, 0.75}));
+
+    EXPECT_DOUBLE_EQ(10., em.branch_length(1));
+    EXPECT_DOUBLE_EQ(10., em.integrate_length(mcable{1, 0., 1.}));
+    EXPECT_DOUBLE_EQ(7.5, em.integrate_length(mcable{1, 0.25, 1.0}));
+
+    // expect 2*0.25+3*0.5 = 2.0 times corresponding cable length.
+    pw_elements<double> pw({0.25, 0.5, 1.}, {2., 3.});
+    EXPECT_DOUBLE_EQ(20., em.integrate_length(1, pw));
+}
+
+TEST(embedding, partial_area) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+    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}
+    };
+
+    morphology m(sample_tree(samples, parents), false);
+    embedding em(m);
+
+    // Cable 1: single truncated cone, length L = 10,
+    // radius r₀ = 10 (pos 0) to r₁ = 5 (pos 1).
+    //
+    // Expect cable area = 2πLr√(1 + m²)
+    // where m = δr/L and r = (r₀+r₁)/2 = r₀ + δr/2.
+
+    constexpr double pi = math::pi<double>;
+    double cable1_area = 2*pi*10*7.5*std::sqrt(1.25);
+
+    constexpr double reltol = 1e-10;
+
+    EXPECT_TRUE(near_relative(cable1_area, em.integrate_area(mcable{1, 0., 1.}), reltol));
+
+    // Weighted area within cable 0:
+    // a)  proportional segment [0.1, 0.3]:
+    //         truncated cone length 6,
+    //         r₀ = 13; r₁ = 19, slope = 1
+    //
+    // b)  proportional segment [0.3, 0.9]:
+    //         truncated cone length 1,
+    //         r₀ = 19, r₁ = 20, slope = 1
+    //         truncated cone length 17
+    //         r₀ = 20, r₁ = 11.5, slope = -0.5
+
+    EXPECT_TRUE(near_relative(13., em.radius(mlocation{0, 0.1}), reltol));
+    EXPECT_TRUE(near_relative(19., em.radius(mlocation{0, 0.3}), reltol));
+    EXPECT_TRUE(near_relative(11.5, em.radius(mlocation{0, 0.9}), reltol));
+
+    pw_elements<double> pw({0.1, 0.3, 0.9}, {5., 7.});
+    double sub_area1 = pi*6*(13+19)*std::sqrt(2);
+    double sub_area2 = pi*1*(19+20)*std::sqrt(2);
+    double sub_area3 = pi*17*(20+11.5)*std::sqrt(1.25);
+
+    EXPECT_TRUE(near_relative(sub_area1, em.integrate_area(mcable{0, 0.1, 0.3}), reltol));
+    EXPECT_TRUE(near_relative(sub_area2, em.integrate_area(mcable{0, 0.3, 1/3.}), reltol));
+    EXPECT_TRUE(near_relative(sub_area3, em.integrate_area(mcable{0, 1/3., 0.9}), reltol));
+
+    double expected_pw_area = 5.*sub_area1+7.*(sub_area2+sub_area3);
+    EXPECT_TRUE(near_relative(expected_pw_area, em.integrate_area(0, pw), reltol));
+
+    // Integrated inverse cross-sectional area in cable 1 from 0.1 to 0.4:
+    // radius r₀ = 9.5, r₁ = 8, length = 3.
+
+    double expected_ixa = 3/(9.5*8)/pi;
+    EXPECT_TRUE(near_relative(expected_ixa, em.integrate_ixa(mcable{1, 0.1, 0.4}), reltol));
+}
diff --git a/test/unit/test_morph_expr.cpp b/test/unit/test_morph_expr.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6304baa47e765974f9f727cc2765865f85626c7f
--- /dev/null
+++ b/test/unit/test_morph_expr.cpp
@@ -0,0 +1,475 @@
+#include "../test/gtest.h"
+
+#include <vector>
+
+#include <arbor/morph/embed_pwlin.hpp>
+#include <arbor/morph/locset.hpp>
+#include <arbor/morph/morphexcept.hpp>
+#include <arbor/morph/morphology.hpp>
+#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"
+
+using namespace arb;
+using embedding = embed_pwlin;
+
+::testing::AssertionResult cable_eq(mcable a, mcable b) {
+    if (a.branch!=b.branch) {
+        return ::testing::AssertionFailure()
+            << "cables " << a << " and " << b << " differ";
+    }
+
+    using FP = testing::internal::FloatingPoint<double>;
+    if (FP(a.prox_pos).AlmostEquals(FP(b.prox_pos)) && FP(a.dist_pos).AlmostEquals(FP(b.dist_pos))) {
+        return ::testing::AssertionSuccess();
+    }
+    else {
+        return ::testing::AssertionFailure()
+            << "cables " << a << " and " << b << " differ";
+    }
+}
+
+::testing::AssertionResult cablelist_eq(const mcable_list& as, const mcable_list& bs) {
+    if (as.size()!=bs.size()) {
+        return ::testing::AssertionFailure()
+            << "cablelists " << as << " and " << bs << " differ";
+    }
+
+    for (auto i: util::count_along(as)) {
+        auto result = cable_eq(as[i], bs[i]);
+        if (!result) return ::testing::AssertionFailure()
+            << "cablelists " << as << " and " << bs << " differ";
+    }
+    return ::testing::AssertionSuccess();
+}
+
+TEST(region, expr_repn) {
+    using util::to_string;
+
+    auto c1 = reg::cable({1, 0, 1});
+    auto c2 = reg::cable({4, 0.125, 0.5});
+    auto c3 = join(reg::cable({4, 0.125, 0.5}), reg::cable({3, 0, 1}));
+    auto b1 = reg::branch(1);
+    auto t1 = reg::tagged(1);
+    auto t2 = reg::tagged(2);
+    auto t3 = reg::tagged(3);
+    auto all = reg::all();
+
+    EXPECT_EQ(to_string(c1), "(cable 1 0 1)");
+    EXPECT_EQ(to_string(c2), "(cable 4 0.125 0.5)");
+    EXPECT_EQ(to_string(c3), "(join (cable 4 0.125 0.5) (cable 3 0 1))");
+    EXPECT_EQ(to_string(b1), "(cable 1 0 1)");
+    EXPECT_EQ(to_string(t1), "(tag 1)");
+    EXPECT_EQ(to_string(t2), "(tag 2)");
+    EXPECT_EQ(to_string(intersect(c1, t2)), "(intersect (cable 1 0 1) (tag 2))");
+    EXPECT_EQ(to_string(join(c1, t2)),  "(join (cable 1 0 1) (tag 2))");
+    EXPECT_EQ(to_string(join(t1, t2, t3)), "(join (join (tag 1) (tag 2)) (tag 3))");
+    EXPECT_EQ(to_string(intersect(t1, t2, t3)), "(intersect (intersect (tag 1) (tag 2)) (tag 3))");
+    EXPECT_EQ(to_string(intersect(join(c1, t2), c2)),  "(intersect (join (cable 1 0 1) (tag 2)) (cable 4 0.125 0.5))");
+    EXPECT_EQ(to_string(all), "all");
+}
+
+TEST(region, invalid_mcable) {
+    EXPECT_NO_THROW(reg::cable({123, 0.5, 0.8}));
+    EXPECT_THROW(reg::cable({1, 0, 1.1}), invalid_mcable);
+    EXPECT_THROW(reg::branch(-1), invalid_mcable);
+}
+
+TEST(locset, expr_repn) {
+    using util::to_string;
+
+    auto root = ls::root();
+    auto term = ls::terminal();
+    auto samp = ls::sample(42);
+    auto loc = ls::location({2, 0.5});
+
+    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(loc), "(location 2 0.5)");
+}
+
+TEST(region, invalid_mlocation) {
+    // Location positions have to be in the range [0,1].
+    EXPECT_NO_THROW(ls::location({123, 0.0}));
+    EXPECT_NO_THROW(ls::location({123, 0.02}));
+    EXPECT_NO_THROW(ls::location({123, 1.0}));
+
+    EXPECT_THROW(ls::location({0, 1.5}), invalid_mlocation);
+    EXPECT_THROW(ls::location({unsigned(-1), 0.}), invalid_mlocation);
+}
+
+// Name evaluation (thingify) tests:
+
+TEST(locset, thingify_named) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+
+    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});
+    {
+        label_dict dict;
+        dict.set("banana", banana);
+        dict.set("cake", cake);
+
+        mprovider mp(morphology(sm, false), dict);
+        EXPECT_EQ(thingify(locset("cake"), mp), thingify(cake, mp));
+        EXPECT_EQ(thingify(locset("banana"), mp), thingify(banana, mp));
+
+        EXPECT_THROW(thingify(locset("durian"), mp), unbound_name);
+    }
+    {
+        label_dict dict;
+        dict.set("banana", banana);
+        dict.set("cake", cake);
+        dict.set("topping", locset("fruit"));
+        dict.set("fruit", locset("strawberry"));
+
+        EXPECT_THROW(mprovider(morphology(sm, false), dict), unbound_name);
+    }
+    {
+        label_dict dict;
+        dict.set("banana", banana);
+        dict.set("cake", cake);
+        dict.set("topping", locset("fruit"));
+        dict.set("fruit", sum(locset("banana"), locset("topping")));
+
+        EXPECT_THROW(mprovider(morphology(sm, false), dict), circular_definition);
+    }
+}
+
+TEST(region, thingify_named) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+
+    region banana = reg::branch(0);
+    region cake = reg::cable(mcable{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});
+    {
+        label_dict dict;
+        dict.set("banana", banana);
+        dict.set("cake", cake);
+
+        mprovider mp(morphology(sm, false), dict);
+        EXPECT_EQ(thingify(region("cake"), mp), thingify(cake, mp));
+        EXPECT_EQ(thingify(region("banana"), mp), thingify(banana, mp));
+
+        EXPECT_THROW(thingify(region("durian"), mp), unbound_name);
+    }
+    {
+        label_dict dict;
+        dict.set("banana", banana);
+        dict.set("cake", cake);
+        dict.set("topping", region("fruit"));
+        dict.set("fruit", region("strawberry"));
+
+        EXPECT_THROW(mprovider(morphology(sm, false), dict), unbound_name);
+    }
+    {
+        label_dict dict;
+        dict.set("banana", banana);
+        dict.set("cake", cake);
+        dict.set("topping", region("fruit"));
+        dict.set("fruit", join(region("cake"), region("topping")));
+
+        EXPECT_THROW(mprovider(morphology(sm, false), dict), circular_definition);
+    }
+}
+
+// Embedded evaluation (thingify) tests:
+
+TEST(locset, thingify) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+    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});
+    auto begb1 = ls::location({1, 0});
+    auto begb2 = ls::location({2, 0});
+    auto begb3 = ls::location({3, 0});
+    auto begb4 = ls::location({4, 0});
+
+    // Eight samples
+    //
+    //            0
+    //           1 3
+    //          2   4
+    //             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},
+    };
+    sample_tree sm(samples, 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}}));
+    }
+    {
+        mprovider mp(morphology(sm, false));
+
+        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}}));
+        EXPECT_EQ(thingify(begb1, mp), (ll{{1,0}}));
+        EXPECT_EQ(thingify(begb2, mp), (ll{{2,0}}));
+        EXPECT_EQ(thingify(begb3, mp), (ll{{3,0}}));
+
+        // In the absence of a spherical root, there is no branch 4.
+        EXPECT_THROW(thingify(begb4, mp), no_such_branch);
+    }
+}
+
+TEST(region, thingify) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+    using cl = mcable_list;
+
+    // A single unbranched cable with 5 sample points.
+    // The cable has length 10 μm, with samples located at
+    // 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},
+        };
+        sample_tree sm(samples, parents);
+        mprovider mp(morphology(sm, false));
+
+        auto h1  = reg::cable({0, 0, 0.5});
+        auto h2  = reg::cable({0, 0.5, 1});
+        auto t1  = reg::tagged(1);
+        auto t2  = reg::tagged(2);
+        auto all = reg::all();
+
+        // Concrete cable lists
+        cl h1_{{0, 0.0, 0.5}};
+        cl h2_{{0, 0.5, 1.0}};
+        cl t1_{{0, 0.0, 0.1}, {0, 0.3, 0.7}};
+        cl t2_{{0, 0.1, 0.3}, {0, 0.7, 1.0}};
+        cl all_{{0, 0, 1}};
+        cl empty_{};
+
+        EXPECT_EQ(thingify(h1, mp), h1_);
+        EXPECT_EQ(thingify(h2, mp), h2_);
+        EXPECT_EQ(thingify(join(h1, h2), mp), all_);
+        EXPECT_EQ(thingify(intersect(h1, h2), mp), (cl{{0, 0.5, 0.5}}));
+
+        EXPECT_TRUE(cablelist_eq(thingify(t1, mp), t1_));
+        EXPECT_TRUE(cablelist_eq(thingify(t2, mp), t2_));
+        EXPECT_TRUE(cablelist_eq(thingify(intersect(h1, h1), mp), h1_));
+        EXPECT_TRUE(cablelist_eq(thingify(intersect(t1, t1), mp), t1_));
+        EXPECT_TRUE(cablelist_eq(thingify(join(t1, t2), mp), all_));
+        EXPECT_TRUE(cablelist_eq(thingify(intersect(all, t1), mp), t1_));
+        EXPECT_TRUE(cablelist_eq(thingify(intersect(all, t2), mp), t2_));
+        EXPECT_TRUE(cablelist_eq(thingify(join(all, t1), mp), all_));
+        EXPECT_TRUE(cablelist_eq(thingify(join(all, t2), mp), all_));
+        EXPECT_TRUE(cablelist_eq(thingify(join(h1, t1), mp), (cl{{0, 0, 0.7}})));
+        EXPECT_TRUE(cablelist_eq(thingify(join(h1, t2), mp), (cl{{0, 0, 0.5}, {0, 0.7, 1}})));
+        EXPECT_TRUE(cablelist_eq(thingify(intersect(h2, t1), mp), (cl{{0, 0.5, 0.7}})));
+    }
+
+
+    // Test handling of spherical soma on multi-branch morphologies
+    //
+    //  sample ids:
+    //              0           |
+    //            1   3         |
+    //          2       4       |
+    //  tags:
+    //              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},
+        };
+
+        // with a spherical root
+        sample_tree sm(samples, parents);
+        mprovider mp(morphology(sm, true));
+
+        using reg::tagged;
+        using reg::branch;
+        using reg::all;
+
+        EXPECT_EQ(thingify(tagged(1), mp), (mcable_list{{0,0,1}}));
+        EXPECT_EQ(thingify(tagged(2), mp), (mcable_list{{2,0,1}}));
+        EXPECT_EQ(thingify(tagged(3), mp), (mcable_list{{1,0,1}}));
+        EXPECT_EQ(thingify(join(tagged(1), tagged(2), tagged(3)), mp), (mcable_list{{0,0,1}, {1,0,1}, {2,0,1}}));
+        EXPECT_EQ(thingify(join(tagged(1), tagged(2), tagged(3)), mp), thingify(all(), mp));
+    }
+
+    // Test multi-level morphologies.
+    //
+    // Eight samples
+    //
+    //  sample ids:
+    //            0
+    //           1 3
+    //          2   4
+    //             5 6
+    //                7
+    //  tags:
+    //            1
+    //           3 2
+    //          3   2
+    //             4 3
+    //                3
+    {
+        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}, 2},
+            {{  0,100,  0,  2}, 2},
+            {{100,100,  0,  2}, 4},
+            {{  0,200,  0,  2}, 3},
+            {{  0,300,  0,  2}, 3},
+        };
+        sample_tree sm(samples, parents);
+
+        // Without spherical root
+        mprovider mp(morphology(sm, false));
+
+        using reg::tagged;
+        using reg::branch;
+        using reg::all;
+        using reg::cable;
+
+        auto soma = tagged(1);
+        auto axon = tagged(2);
+        auto dend = tagged(3);
+        auto apic = tagged(4);
+        auto b1  = branch(1);
+        auto b3  = branch(3);
+        auto b13 = join(b1, b3);
+
+        cl empty_{};
+        cl soma_ = empty_;
+
+        // Whole branches:
+        mcable b0_{0,0,1};
+        mcable b1_{1,0,1};
+        mcable b2_{2,0,1};
+        mcable b3_{3,0,1};
+        cl all_  = {b0_,b1_,b2_,b3_};
+
+        mcable end1_{1,1,1};
+        mcable root_{0,0,0};
+
+        EXPECT_EQ(thingify(all(), mp), all_);
+        EXPECT_EQ(thingify(soma, mp), empty_);
+        EXPECT_EQ(thingify(axon, mp), (cl{b1_}));
+        EXPECT_EQ(thingify(dend, mp), (cl{b0_,b3_}));
+        EXPECT_EQ(thingify(apic, mp), (cl{b2_}));
+        EXPECT_EQ(thingify(join(dend, apic), mp), (cl{b0_,b2_,b3_}));
+        EXPECT_EQ(thingify(join(axon, join(dend, apic)), mp), all_);
+
+        // Test that intersection correctly generates zero-length cables at
+        // parent-child interfaces.
+        EXPECT_EQ(thingify(intersect(apic, dend), mp), (cl{end1_}));
+        EXPECT_EQ(thingify(intersect(apic, axon), mp), (cl{end1_}));
+        EXPECT_EQ(thingify(intersect(axon, dend), mp), (cl{root_, end1_}));
+
+        // Test some more interesting intersections and unions.
+
+        //    123456789 123456789
+        //   |---------|---------| lhs
+        //   |  -----  |   ---   | rhs
+        //   |  xxxxx  |   xxx   | rand
+        //   |xxxxxxxxx|xxxxxxxxx| ror
+        auto lhs  = b13;
+        auto rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
+        auto rand = cl{         {1,.2,.7}, {3,.3,.6}};
+        auto ror  = cl{         {1,.0,1.}, {3,.0,1.}};
+        EXPECT_EQ(thingify(intersect(lhs, rhs), mp), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), mp), ror);
+
+        // Assert communtativity
+        std::swap(lhs, rhs);
+        EXPECT_EQ(thingify(intersect(lhs, rhs), mp), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), mp), ror);
+
+        //    123456789 123456789
+        //   |   ----- | ----    | lhs
+        //   |  -----  |   ---   | rhs
+        //   |   xxxx  |   xx    | rand
+        //   |  xxxxxx | xxxxx   | ror
+        lhs  = join(cable({1,.3,.8}), cable({3,.1,.5}));
+        rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
+        rand = cl{         {1,.3,.7}, {3,.3,.5}};
+        ror  = cl{         {1,.2,.8}, {3,.1,.6}};
+        EXPECT_EQ(thingify(intersect(lhs, rhs), mp), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), mp), ror);
+
+        // Assert communtativity
+        std::swap(lhs, rhs);
+        EXPECT_EQ(thingify(intersect(lhs, rhs), mp), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), mp), ror);
+
+        //    123456789 123456789
+        //   | -- -    | --- --- | lhs
+        //   |  -----  |   ---   | rhs
+        //   |  x x    |   x x   | rand
+        //   | xxxxxx  | xxxxxxx | ror
+        lhs  = join(cable({1,.1,.3}), cable({1,.4,.5}), cable({3,.1,.4}), cable({3,.5,.9}));
+        rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
+        rand = cl{         {1,.2,.3}, {1,.4,.5}, {3,.3,.4}, {3,.5,.6}};
+        ror  = cl{         {1,.1,.7},            {3,.1,.9}};
+        EXPECT_EQ(thingify(intersect(lhs, rhs), mp), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), mp), ror);
+
+        // Assert communtativity
+        std::swap(lhs, rhs);
+        EXPECT_EQ(thingify(intersect(lhs, rhs), mp), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), mp), ror);
+    }
+}
diff --git a/test/unit/test_morph_primitives.cpp b/test/unit/test_morph_primitives.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c627e589b9fe31bb10c7f25f2b6eea7012ef3a6b
--- /dev/null
+++ b/test/unit/test_morph_primitives.cpp
@@ -0,0 +1,122 @@
+#include <iostream>
+#include <vector>
+
+#include <arbor/morph/primitives.hpp>
+
+#include "../gtest.h"
+
+using namespace arb;
+
+::testing::AssertionResult mpoint_eq(const mpoint& a, const mpoint& b) {
+    if (a.x==b.x && a.y==b.y && a.z==b.z && a.radius==b.radius) {
+        return ::testing::AssertionSuccess();
+    }
+    else {
+        return ::testing::AssertionFailure()
+            << "mpoints " << a << " and " << b << " differ.";
+    }
+}
+
+TEST(morph_primitives, lerp) {
+    mpoint a{1., 2., 3., 4.};
+    mpoint b{2., 4., 7., 12.};
+
+    EXPECT_TRUE(mpoint_eq(a, lerp(a, b, 0)));
+    EXPECT_TRUE(mpoint_eq(b, lerp(a, b, 1)));
+
+    EXPECT_TRUE(mpoint_eq(mpoint{1.5, 3., 5., 8.}, lerp(a, b, 0.5)));
+    EXPECT_TRUE(mpoint_eq(mpoint{1.25, 2.5, 4., 6.}, lerp(a, b, 0.25)));
+}
+
+TEST(morph_primitives, is_collocated) {
+    EXPECT_TRUE(is_collocated(mpoint{1., 2., 3., 4.}, mpoint{1., 2., 3., 4.}));
+    EXPECT_TRUE(is_collocated(mpoint{1., 2., 3., 4.}, mpoint{1., 2., 3., 4.5}));
+    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) {
+    // 2² + 3² + 6² = 7²
+    EXPECT_DOUBLE_EQ(7., distance(mpoint{1.5, 2.5, 3.5, 4.5}, mpoint{3.5, 5.5, 9.5, 0.31}));
+}
+
+TEST(morph_primitives, join_intersect_sum) {
+    auto ml = [](const std::vector<int>& bids) {
+        mlocation_list L;
+        for (msize_t b: bids) L.push_back({b, 0});
+        return L;
+    };
+
+    using ll = mlocation_list;
+
+    {
+        ll lhs{};
+        ll rhs{};
+        EXPECT_EQ(sum(lhs, rhs), ll{});
+        EXPECT_EQ(join(lhs, rhs), ll{});
+        EXPECT_EQ(intersection(lhs, rhs), ll{});
+    }
+    {
+        ll lhs{};
+        ll rhs = ml({0,1});
+        EXPECT_EQ(sum(lhs, rhs), rhs);
+        EXPECT_EQ(join(lhs, rhs), rhs);
+        EXPECT_EQ(intersection(lhs, rhs), ll{});
+    }
+    {
+        ll lhs = ml({1});
+        ll rhs = ml({1});
+        EXPECT_EQ(sum(lhs,  rhs), ml({1,1}));
+        EXPECT_EQ(join(lhs, rhs), ml({1}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({1,1});
+        ll rhs = ml({1});
+        EXPECT_EQ(sum(lhs,  rhs), ml({1,1,1}));
+        EXPECT_EQ(join(lhs, rhs), ml({1,1}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({0,3});
+        ll rhs = ml({1,2});
+        EXPECT_EQ(sum(lhs,  rhs), ml({0,1,2,3}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,2,3}));
+        EXPECT_EQ(intersection(lhs, rhs), ll{});
+    }
+    {
+        ll lhs = ml({0,1,3});
+        ll rhs = ml({0,1,3});
+        EXPECT_EQ(sum(lhs, rhs), ml({0,0,1,1,3,3}));
+        EXPECT_EQ(join(lhs, rhs), lhs);
+        EXPECT_EQ(intersection(lhs, rhs), lhs);
+    }
+    {
+        ll lhs = ml({0,1,3});
+        ll rhs = ml({1,2});
+        EXPECT_EQ(sum(lhs, rhs), ml({0,1,1,2,3}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,2,3}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({0,1,1,3});
+        ll rhs = ml({1,2});
+        EXPECT_EQ(sum(lhs, rhs), ml({0,1,1,1,2,3}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,1,2,3}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({0,1,1,3,5,5,12});
+        ll rhs = ml({1,2,2,5,5,5});
+        EXPECT_EQ(sum(lhs, rhs),  ml({0,1,1,1,2,2,3,5,5,5,5,5,12}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,1,2,2,3,5,5,5,12}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1,5,5}));
+    }
+}
diff --git a/test/unit/test_morphology.cpp b/test/unit/test_morphology.cpp
index eb34f28939ec7574e02bbd9fa51d3cb4f6cf3c49..d4d51b7e572ada70f2dbf3d73a54d84ed2491c2c 100644
--- a/test/unit/test_morphology.cpp
+++ b/test/unit/test_morphology.cpp
@@ -1,7 +1,3 @@
-/*
- * Unit tests for sample_tree and morphology.
- */
-
 #include <fstream>
 #include <cmath>
 #include <string>
@@ -9,24 +5,13 @@
 
 #include "../test/gtest.h"
 
-#include <arbor/math.hpp>
-#include <arbor/morph/error.hpp>
+#include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/sample_tree.hpp>
 #include <arbor/cable_cell.hpp>
 
-#include "algorithms.hpp"
-#include "io/sepval.hpp"
 #include "morph/mbranch.hpp"
-#include "morph/em_morphology.hpp"
-#include "util/range.hpp"
 #include "util/span.hpp"
-#include "util/strprintf.hpp"
-
-template <typename T>
-std::ostream& operator<<(std::ostream& o, const std::vector<T>& v) {
-    return o << "[" << arb::io::csv(v) << "]";
-}
 
 // Test basic functions on properties of mpoint
 TEST(morphology, mpoint) {
@@ -175,7 +160,7 @@ TEST(morphology, branches_from_parent_index) {
 
         // A cable morphology can't be constructed from a single sample.
         EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), false),
-                     arb::morphology_error);
+                     arb::incomplete_branch);
     }
     {
         pvec parents = {npos, 0};
@@ -186,7 +171,7 @@ TEST(morphology, branches_from_parent_index) {
 
         // 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::morphology_error);
+                     arb::incomplete_branch);
     }
 
     {
@@ -211,7 +196,7 @@ TEST(morphology, branches_from_parent_index) {
         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::morphology_error);
+        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);
@@ -248,7 +233,7 @@ TEST(morphology, branches_from_parent_index) {
         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::morphology_error);
+        EXPECT_THROW(arb::impl::branches_from_parent_index(parents, tree.properties(), true), arb::incomplete_branch);
     }
 
     {
@@ -362,7 +347,7 @@ TEST(morphology, construction) {
                 {{0.0, 5.0, 0.0, 1.0}, 2}};
 
             arb::sample_tree sm(s, p);
-            EXPECT_THROW((arb::morphology(sm)), arb::morphology_error);
+            EXPECT_THROW((arb::morphology(sm)), arb::incomplete_branch);
         }
     }
     {
@@ -392,6 +377,17 @@ TEST(morphology, branches) {
     using pvec = std::vector<arb::msize_t>;
     using svec = std::vector<arb::msample>;
     auto npos = arb::mnpos;
+
+    auto check_terminal_branches = [](const arb::morphology& m) {
+        pvec expected;
+        arb::msize_t n = m.num_branches();
+
+        for (arb::msize_t i = 0; i<n; ++i) {
+            if (m.branch_children(i).empty()) expected.push_back(i);
+        }
+        EXPECT_EQ(expected, m.terminal_branches());
+    };
+
     {
         // 0
         pvec parents = {npos};
@@ -404,6 +400,8 @@ TEST(morphology, branches) {
         EXPECT_EQ(1u, m.num_branches());
         EXPECT_EQ(npos, m.branch_parent(0));
         EXPECT_EQ(pvec{}, m.branch_children(0));
+
+        check_terminal_branches(m);
     }
     {
         // 0 - 1
@@ -418,6 +416,8 @@ TEST(morphology, branches) {
         EXPECT_EQ(1u, m.num_branches());
         EXPECT_EQ(npos, m.branch_parent(0));
         EXPECT_EQ(pvec{}, m.branch_children(0));
+
+        check_terminal_branches(m);
     }
     {
         // 0 - 1 - 2
@@ -435,6 +435,8 @@ TEST(morphology, branches) {
             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.
@@ -451,6 +453,8 @@ TEST(morphology, branches) {
             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);
         }
     }
     {
@@ -470,6 +474,8 @@ TEST(morphology, branches) {
         EXPECT_EQ(npos,   m.branch_parent(1));
         EXPECT_EQ(pvec{}, m.branch_children(0));
         EXPECT_EQ(pvec{},  m.branch_children(1));
+
+        check_terminal_branches(m);
     }
     {
         // 0 - 1 - 2 - 3
@@ -521,6 +527,8 @@ TEST(morphology, branches) {
             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 = {
@@ -545,6 +553,8 @@ TEST(morphology, branches) {
             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);
         }
     }
 }
@@ -574,3 +584,54 @@ TEST(morphology, swc) {
     EXPECT_EQ(31u, c.num_branches());
 }
 
+TEST(morphology, minset) {
+    using pvec = std::vector<arb::msize_t>;
+    using svec = std::vector<arb::msample>;
+    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{}), 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}}));
+    }
+}
+
diff --git a/test/unit/test_piecewise.cpp b/test/unit/test_piecewise.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..03047be6c591964c5bdc28c9d0ac9c799de24db9
--- /dev/null
+++ b/test/unit/test_piecewise.cpp
@@ -0,0 +1,332 @@
+#include "../gtest.h"
+
+#include <list>
+
+#include "util/piecewise.hpp"
+#include "util/rangeutil.hpp"
+
+using namespace arb;
+using util::pw_elements;
+using util::pw_element;
+using util::pw_npos;
+
+TEST(piecewise, eq) {
+    pw_elements<int> p1((double[3]){1., 1.5, 2.}, (int[2]){3, 4});
+    pw_elements<int> p2(p1);
+    EXPECT_EQ(p2, p1);
+
+    pw_elements<int> p3((double[3]){1., 1.7, 2.}, (int[2]){3, 4});
+    EXPECT_NE(p3, p1);
+
+    pw_elements<int> p4((double[3]){1., 1.5, 2.}, (int[2]){3, 5});
+    EXPECT_NE(p4, p1);
+
+    pw_elements<int> p5(p1);
+    p5.push_back(2., 7);
+    EXPECT_NE(p5, p1);
+
+    pw_elements<> v1((double[3]){1., 1.5, 2.});
+    pw_elements<> v2(v1);
+    EXPECT_EQ(v2, v1);
+
+    pw_elements<> v3((double[3]){1., 1.7, 2.});
+    EXPECT_NE(v3, v1);
+
+    pw_elements<> v5(v1);
+    v5.push_back(2.);
+    EXPECT_NE(v5, v1);
+}
+
+TEST(piecewise, generic_ctors) {
+    pw_elements<int> p1({1., 1.5, 2.}, {3, 4});
+    pw_elements<int> p2(std::list<float>{1.f, 1.5f, 2.f}, std::list<long>{3, 4});
+
+    EXPECT_EQ(p1, p2);
+
+    pw_elements<> v1(p1);
+    EXPECT_EQ((std::vector<double>{1., 1.5, 2.}), v1.vertices());
+
+    pw_elements<> v3(std::list<float>{1.f, 1.5f, 2.f});
+    EXPECT_EQ(v1, v3);
+
+    EXPECT_THROW(pw_elements<>({2.}), std::runtime_error);
+    EXPECT_THROW(pw_elements<int>({2.}, {1}), std::runtime_error);
+    EXPECT_THROW(pw_elements<int>({1., 2.}, {}), std::runtime_error);
+    EXPECT_THROW(pw_elements<int>({1., 2.}, {1, 3}), std::runtime_error);
+}
+
+TEST(piecewise, size) {
+    pw_elements<int> p1;
+    EXPECT_EQ(0u, p1.size());
+    EXPECT_TRUE(p1.empty());
+
+    pw_elements<int> p2({1., 2.}, {1});
+    EXPECT_EQ(1u, p2.size());
+    EXPECT_FALSE(p2.empty());
+
+    pw_elements<> v1;
+    EXPECT_EQ(0u, v1.size());
+    EXPECT_TRUE(v1.empty());
+
+    pw_elements<> v2({1., 2.});
+    EXPECT_EQ(1u, v2.size());
+    EXPECT_FALSE(v2.empty());
+}
+
+TEST(piecewise, assign) {
+    pw_elements<int> p;
+
+    double v[5] = {1., 1.5, 2., 2.5, 3.};
+    int x[4] = {10, 8, 9, 4};
+    p.assign(v, x);
+
+    ASSERT_EQ(4u, p.size());
+
+    EXPECT_EQ(10, p[0].second);
+    EXPECT_EQ( 8, p[1].second);
+    EXPECT_EQ( 9, p[2].second);
+    EXPECT_EQ( 4, p[3].second);
+
+    using dp = std::pair<double, double>;
+    EXPECT_EQ(dp(1.0, 1.5), p.interval(0));
+    EXPECT_EQ(dp(1.5, 2.0), p.interval(1));
+    EXPECT_EQ(dp(2.0, 2.5), p.interval(2));
+    EXPECT_EQ(dp(2.5, 3.0), p.interval(3));
+
+    pw_elements<int> q1(p);
+    pw_elements<int> q2;
+    q2 = p;
+    pw_elements<int> q3(p);
+    q3.assign(p.vertices(), p.elements());
+
+    EXPECT_EQ((std::vector<double>{1.0, 1.5, 2.0, 2.5, 3.0}), p.vertices());
+    EXPECT_EQ((std::vector<int>{10, 8, 9, 4}), p.elements());
+
+    EXPECT_EQ(q1.vertices(), p.vertices());
+    EXPECT_EQ(q2.vertices(), p.vertices());
+    EXPECT_EQ(q3.vertices(), p.vertices());
+
+    EXPECT_EQ(q1.elements(), p.elements());
+    EXPECT_EQ(q2.elements(), p.elements());
+    EXPECT_EQ(q3.elements(), p.elements());
+
+    q3.assign({}, {});
+    EXPECT_TRUE(q3.empty());
+
+    q3.assign({1., 2.}, {3});
+    pw_elements<int> q4(q3);
+    ASSERT_EQ(q3, q4);
+
+    // bad assign should throw but preserve value
+    ASSERT_THROW(q4.assign({3.}, {}), std::runtime_error);
+    EXPECT_EQ(q3, q4);
+}
+
+TEST(piecewise, assign_void) {
+    pw_elements<> p;
+
+    double v[5] = {1., 1.5, 2., 2.5, 3.};
+    p.assign(v);
+
+    ASSERT_EQ(4u, p.size());
+
+    using dp = std::pair<double, double>;
+    EXPECT_EQ(dp(1.0, 1.5), p.interval(0));
+    EXPECT_EQ(dp(1.5, 2.0), p.interval(1));
+    EXPECT_EQ(dp(2.0, 2.5), p.interval(2));
+    EXPECT_EQ(dp(2.5, 3.0), p.interval(3));
+
+    pw_elements<> q1(p);
+    pw_elements<> q2;
+    q2 = p;
+    pw_elements<> q3(p);
+    q3.assign(p.vertices());
+
+    EXPECT_EQ((std::vector<double>{1.0, 1.5, 2.0, 2.5, 3.0}), p.vertices());
+
+    EXPECT_EQ(q1.vertices(), p.vertices());
+    EXPECT_EQ(q2.vertices(), p.vertices());
+    EXPECT_EQ(q3.vertices(), p.vertices());
+
+    q3.assign({});
+    EXPECT_TRUE(q3.empty());
+
+    q3.assign({1., 2.});
+    pw_elements<> q4(q3);
+    ASSERT_EQ(q3, q4);
+
+    // bad assign should throw but preserve value
+    ASSERT_THROW(q4.assign({3.}), std::runtime_error);
+    EXPECT_EQ(q3, q4);
+}
+
+TEST(piecewise, access) {
+    pw_elements<int> p;
+
+    double v[5] = {1., 1.5, 2., 2.5, 3.};
+    int x[4] = {10, 8, 9, 4};
+    p.assign(v, x);
+
+    for (unsigned i = 0; i<4; ++i) {
+        EXPECT_EQ(v[i], p[i].first.first);
+        EXPECT_EQ(v[i+1], p[i].first.second);
+
+        EXPECT_EQ(v[i], p.interval(i).first);
+        EXPECT_EQ(v[i+1], p.interval(i).second);
+
+        EXPECT_EQ(x[i], p[i].second);
+        EXPECT_EQ(x[i], p.element(i));
+    }
+
+    EXPECT_EQ(p[0], p.front());
+    EXPECT_EQ(p[3], p.back());
+
+    unsigned j = 0;
+    for (auto entry: p) {
+        EXPECT_EQ(p[j++], entry);
+    }
+}
+
+TEST(piecewise, bounds) {
+    pw_elements<int> p{{1., 1.5, 2., 2.5, 3.}, {10, 8, 9, 4}};
+
+    EXPECT_EQ(1., p.bounds().first);
+    EXPECT_EQ(3., p.bounds().second);
+
+    pw_elements<> v{{1., 1.5, 2., 2.5, 3.}};
+
+    EXPECT_EQ(1., v.bounds().first);
+    EXPECT_EQ(3., v.bounds().second);
+}
+
+TEST(piecewise, index_of) {
+    pw_elements<int> p{{1., 1.5, 2., 2.5, 3.}, {10, 8, 9, 4}};
+
+    EXPECT_EQ(pw_npos, p.index_of(0.3));
+    EXPECT_EQ(0u, p.index_of(1.));
+    EXPECT_EQ(0u, p.index_of(1.1));
+    EXPECT_EQ(1u, p.index_of(1.5));
+    EXPECT_EQ(1u, p.index_of(1.6));
+    EXPECT_EQ(2u, p.index_of(2));
+    EXPECT_EQ(3u, p.index_of(2.9));
+    EXPECT_EQ(3u, p.index_of(3));
+    EXPECT_EQ(pw_npos, p.index_of(3.1));
+
+    pw_elements<> v(p);
+
+    EXPECT_EQ(pw_npos, v.index_of(0.3));
+    EXPECT_EQ(0u, v.index_of(1.));
+    EXPECT_EQ(0u, v.index_of(1.1));
+    EXPECT_EQ(1u, v.index_of(1.5));
+    EXPECT_EQ(1u, v.index_of(1.6));
+    EXPECT_EQ(2u, v.index_of(2));
+    EXPECT_EQ(3u, v.index_of(2.9));
+    EXPECT_EQ(3u, v.index_of(3));
+    EXPECT_EQ(pw_npos, v.index_of(3.1));
+
+    pw_elements<int> p0;
+    pw_elements<> v0;
+
+    EXPECT_EQ(pw_npos, p0.index_of(0.));
+    EXPECT_EQ(pw_npos, v0.index_of(0.));
+}
+
+TEST(piecewise, push) {
+    pw_elements<int> q;
+    using dp = std::pair<double, double>;
+
+    // Need left hand side!
+    EXPECT_THROW(q.push_back(3.1, 4), std::runtime_error);
+
+    q.clear();
+    q.push_back(1.1, 3.1, 4);
+    q.push_back(3.1, 4.3, 5);
+    EXPECT_EQ(dp(1.1, 3.1), q.interval(0));
+    EXPECT_EQ(dp(3.1, 4.3), q.interval(1));
+    EXPECT_EQ(4, q[0].second);
+    EXPECT_EQ(5, q[1].second);
+
+    q.push_back(7.2, 6);
+    EXPECT_EQ(dp(4.3, 7.2), q.interval(2));
+    EXPECT_EQ(6, q[2].second);
+
+    // Supplied left side doesn't match current right.
+    EXPECT_THROW(q.push_back(7.4, 9.1, 7), std::runtime_error);
+}
+
+TEST(piecewise, push_void) {
+    pw_elements<> p;
+    using dp = std::pair<double, double>;
+
+    // Need left hand side!
+    EXPECT_THROW(p.push_back(3.1), std::runtime_error);
+
+    p.clear();
+    p.push_back(0.1, 0.2);
+    p.push_back(0.2, 0.4);
+    p.push_back(0.5);
+
+    EXPECT_EQ(3u, p.size());
+    EXPECT_EQ((std::vector<double>{0.1, 0.2, 0.4, 0.5}), p.vertices());
+    EXPECT_EQ(dp(0.2,0.4), p.interval(1));
+
+    // Supplied left side doesn't match current right.
+    EXPECT_THROW(p.push_back(0.7, 0.9), std::runtime_error);
+}
+
+TEST(piecewise, zip) {
+    pw_elements<int> p03;
+    p03.assign((double [3]){0., 1.5, 3.}, (int [2]){10, 11});
+
+    pw_elements<int> p14;
+    p14.assign((double [5]){1, 2.25, 3., 3.5, 4.}, (int [4]){3, 4, 5, 6});
+
+    using ii = std::pair<int, int>;
+    pw_elements<ii> p03_14 = zip(p03, p14);
+    EXPECT_EQ(1., p03_14.bounds().first);
+    EXPECT_EQ(3., p03_14.bounds().second);
+
+    EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), p03_14.vertices());
+    EXPECT_EQ((std::vector<ii>{ii(10, 3), ii(11, 3), ii(11, 4)}), p03_14.elements());
+
+    pw_elements<ii> p14_03 = zip(p14, p03);
+    EXPECT_EQ(p03_14.vertices(), p14_03.vertices());
+
+    std::vector<ii> flipped = util::assign_from(util::transform_view(p14_03.elements(),
+        [](ii p) { return ii{p.second, p.first}; }));
+    EXPECT_EQ(p03_14.elements(), flipped);
+
+    pw_elements<> v03;
+    v03.assign((double [3]){0., 1.5, 3.});
+
+    EXPECT_EQ((std::vector<int>{3, 3, 4}), zip(v03, p14).elements());
+    EXPECT_EQ((std::vector<int>{3, 3, 4}), zip(p14, v03).elements());
+
+    EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), zip(v03, p14).vertices());
+    EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), zip(p14, v03).vertices());
+
+    auto project = [](double l, double r, pw_element<void>, const pw_element<int>& b) -> double {
+        double b_width = b.first.second-b.first.first;
+        return b.second*(r-l)/b_width;
+    };
+
+    pw_elements<void> vxx; // elements cover bounds of p14
+    vxx.assign((double [6]){0.2, 1.7, 1.95, 2.325, 2.45, 4.9});
+
+    pw_elements<double> pxx = zip(vxx, p14, project);
+    double p14_sum = util::sum(util::transform_view(p14, [](auto v) { return v.second; }));
+    double pxx_sum = util::sum(util::transform_view(pxx, [](auto v) { return v.second; }));
+    EXPECT_DOUBLE_EQ(p14_sum, pxx_sum);
+
+}
+
+TEST(piecewise, zip_void) {
+    pw_elements<> p03;
+    p03.assign((double [3]){0., 1.5, 3.});
+
+    pw_elements<> p14;
+    p14.assign((double [5]){1, 2.25, 3., 3.5, 4.});
+
+    EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), zip(p03, p14).vertices());
+    EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), zip(p14, p03).vertices());
+}
diff --git a/test/unit/test_range.cpp b/test/unit/test_range.cpp
index b15c86abb54523ed7c38a262c822d1d2752c3fad..39a80f45ba027f057fffb3c09dc4a1d06797bd31 100644
--- a/test/unit/test_range.cpp
+++ b/test/unit/test_range.cpp
@@ -478,6 +478,26 @@ TEST(range, sort) {
     EXPECT_EQ(X, (std::vector<foo>{{0, 5}, {1, 4}, {2, 3}, {3, 2}, {4, 1}, {5, 0}}));
 }
 
+TEST(range, sum) {
+    std::string words[] = { "fish", "cakes", "!" };
+
+    auto result = util::sum(words);
+    EXPECT_EQ("fishcakes!", result);
+
+    result = util::sum(words, "tasty"s);
+    EXPECT_EQ("tastyfishcakes!", result);
+
+    struct uwrap {
+        unsigned value = 0;
+        uwrap(unsigned v): value(v) {}
+
+        uwrap operator+(const std::string& s) { return value+s.size(); }
+    };
+
+    auto count = util::sum(words, uwrap{3});
+    EXPECT_EQ(3u+4u+5u+1u, count.value);
+}
+
 TEST(range, sum_by) {
     std::string words[] = { "fish", "cakes", "!" };
     auto prepend_ = [](const std::string& x) { return "_"+x; };
diff --git a/test/unit/test_ratelem.cpp b/test/unit/test_ratelem.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f393ae75456522ea83a7320c4d643f650baef7a6
--- /dev/null
+++ b/test/unit/test_ratelem.cpp
@@ -0,0 +1,115 @@
+#include "../gtest.h"
+
+#include <list>
+
+#include "util/ratelem.hpp"
+#include "util/rangeutil.hpp"
+
+using namespace arb;
+using util::rat_element;
+
+TEST(ratelem, direct_ctor) {
+    rat_element<0, 0> x00(3.5);
+    EXPECT_EQ(1u, x00.size());
+    EXPECT_EQ(3.5, x00[0]);
+
+    rat_element<1, 3> x13(1.1, 2.2, 3.3, 4.4, 5.5);
+    EXPECT_EQ(5u, x13.size());
+    EXPECT_EQ(1.1, x13[0]);
+    EXPECT_EQ(2.2, x13[1]);
+    EXPECT_EQ(3.3, x13[2]);
+    EXPECT_EQ(4.4, x13[3]);
+    EXPECT_EQ(5.5, x13[4]);
+
+    std::array<float, 4> x21_arr{1.25f, 1.5f, 0.5f, 2.25f};
+    rat_element<2, 1> x21(x21_arr);
+    EXPECT_EQ(4u, x21.size());
+    EXPECT_EQ(1.25, x21[0]);
+    EXPECT_EQ(1.5,  x21[1]);
+    EXPECT_EQ(0.5,  x21[2]);
+    EXPECT_EQ(2.25, x21[3]);
+
+    int x20_arr[3] = {3, 2, 4};
+    rat_element<2, 0> x20(x20_arr);
+    EXPECT_EQ(3u, x20.size());
+    EXPECT_EQ(3., x20[0]);
+    EXPECT_EQ(2., x20[1]);
+    EXPECT_EQ(4., x20[2]);
+}
+
+TEST(ratelem, fn_ctor) {
+    auto f = [](double x) { return 1+x*x; };
+
+    rat_element<0, 0> x00(f);
+    EXPECT_EQ(1., x00[0]);
+
+    rat_element<1, 3> x12(f);
+    EXPECT_EQ(f(0.00), x12[0]);
+    EXPECT_EQ(f(0.25), x12[1]);
+    EXPECT_EQ(f(0.50), x12[2]);
+    EXPECT_EQ(f(0.75), x12[3]);
+    EXPECT_EQ(f(1.00), x12[4]);
+}
+
+
+TEST(ratelem, constants) {
+    // (Only expect this to work for polynomial interpolators).
+    auto k = [](double c) { return [c](double x) { return c; }; };
+
+    rat_element<0, 0> k00(k(2.));
+    rat_element<1, 0> k10(k(3.));
+    rat_element<2, 0> k20(k(4.));
+    rat_element<3, 0> k30(k(5.));
+
+    double xs[] = {0., 0.1, 0.3, 0.5, 0.7, 0.9, 1.};
+    for (auto x: xs) {
+        EXPECT_DOUBLE_EQ(2., k00(x));
+        EXPECT_DOUBLE_EQ(3., k10(x));
+        EXPECT_DOUBLE_EQ(4., k20(x));
+        EXPECT_DOUBLE_EQ(5., k30(x));
+    }
+}
+
+template <unsigned p_, unsigned q_>
+struct wrap_pq {
+    static constexpr unsigned p = p_;
+    static constexpr unsigned q = q_;
+};
+
+template <typename PQ>
+struct ratelem_pq: public testing::Test {};
+
+using pq_types = ::testing::Types<
+    wrap_pq<0, 0>, wrap_pq<1, 0>, wrap_pq<2, 0>, wrap_pq<3, 0>,
+    wrap_pq<0, 1>, wrap_pq<1, 1>, wrap_pq<2, 1>, wrap_pq<3, 1>,
+    wrap_pq<0, 2>, wrap_pq<1, 2>, wrap_pq<2, 2>, wrap_pq<3, 2>
+>;
+
+// Compute 1+x+x^2+...+x^n
+template <unsigned n>
+constexpr double upoly(double x) { return x*upoly<n-1>(x)+1.; }
+
+template <>
+constexpr double upoly<0>(double x) { return 1.; }
+
+TYPED_TEST_CASE(ratelem_pq, pq_types);
+
+TYPED_TEST(ratelem_pq, interpolate_monotonic) {
+    constexpr unsigned p = TypeParam::p;
+    constexpr unsigned q = TypeParam::q;
+
+    // Pick f to have irreducible order p, q.
+    auto f = [](double x) { return upoly<p>(x)/upoly<q>(2*x); };
+    rat_element<p, q> fpq(f);
+
+    // Check values both on and off nodes.
+    for (unsigned i = 0; i<=1+p+q; ++i) {
+        double x = (double)i/(1+p+q);
+        EXPECT_DOUBLE_EQ(f(x), fpq(x));
+    }
+
+    for (unsigned i = 1; i<p+q; ++i) {
+        double x = (double)i/(p+q);
+        EXPECT_DOUBLE_EQ(f(x), fpq(x));
+    }
+}