diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp
index 90dcd74438148e6adf4d9cd262af4ea004303a21..55bdb9392f483743c30256274e8d4fd7f61b7246 100644
--- a/arbor/cable_cell.cpp
+++ b/arbor/cable_cell.cpp
@@ -17,20 +17,117 @@ using value_type = cable_cell::value_type;
 using index_type = cable_cell::index_type;
 using size_type = cable_cell::size_type;
 
-cable_cell::cable_cell() {
-    // insert a placeholder segment for the soma
-    segments_.push_back(make_segment<placeholder_segment>());
-    parents_.push_back(0);
-}
+struct cable_cell_impl {
+    using value_type = cable_cell::value_type;
+    using index_type = cable_cell::index_type;
+    using size_type  = cable_cell::size_type;
+
+    using stimulus_instance     = cable_cell::stimulus_instance;
+    using synapse_instance      = cable_cell::synapse_instance;
+    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() {
+        segments.push_back(make_segment<placeholder_segment>());
+        parents.push_back(0);
+    }
 
-void cable_cell::assert_valid_segment(index_type i) const {
-    if (i>=num_segments()) {
-        throw cable_cell_error("no such segment");
+    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;
+
+        // unique_ptr's cannot be copy constructed, do a manual assignment
+        segments.reserve(other.segments.size());
+        for (const auto& s: other.segments) {
+            segments.push_back(s->clone());
+        }
     }
+
+    cable_cell_impl(cable_cell_impl&& other) = default;
+
+    // storage for connections
+    std::vector<index_type> parents;
+
+    // the segments
+    std::vector<segment_ptr> segments;
+
+    // the stimuli
+    std::vector<stimulus_instance> stimuli;
+
+    // the synapses
+    std::vector<synapse_instance> synapses;
+
+    // the gap_junctions
+    std::vector<gap_junction_instance> gap_junction_sites;
+
+    // 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;
+
+    template <typename Desc, typename T>
+    lid_range place(const mlocation_list& locs, const Desc& desc, std::vector<T>& list) {
+        const auto first = list.size();
+
+        list.reserve(first+locs.size());
+        for (auto loc: locs) {
+            list.push_back({loc, desc});
+        }
+
+        return lid_range(first, list.size());
+    }
+
+    lid_range place_gj(const mlocation_list& locs) {
+        const auto first = gap_junction_sites.size();
+
+        gap_junction_sites.insert(gap_junction_sites.end(), locs.begin(), locs.end());
+
+        return lid_range(first, gap_junction_sites.size());
+    }
+
+    void assert_valid_segment(index_type i) const {
+        if (i>=segments.size()) {
+            throw cable_cell_error("no such segment");
+        }
+    }
+
+    bool valid_location(const mlocation& loc) const {
+        return test_invariants(loc) && loc.branch<segments.size();
+    }
+};
+
+using impl_ptr = std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)>;
+impl_ptr make_impl(cable_cell_impl* c) {
+    return impl_ptr(c, [](cable_cell_impl* p){delete p;});
 }
 
+cable_cell::cable_cell():
+    impl_(make_impl(new cable_cell_impl()))
+{}
+
+cable_cell::cable_cell(const cable_cell& other):
+    default_parameters(other.default_parameters),
+    impl_(make_impl(new cable_cell_impl(*other.impl_)))
+{}
+
 size_type cable_cell::num_segments() const {
-    return segments_.size();
+    return impl_->segments.size();
 }
 
 //
@@ -41,8 +138,8 @@ soma_segment* cable_cell::add_soma(value_type radius, point_type center) {
     if (has_soma()) {
         throw cable_cell_error("cell already has soma");
     }
-    segments_[0] = make_segment<soma_segment>(radius, center);
-    return segments_[0]->as_soma();
+    impl_->segments[0] = make_segment<soma_segment>(radius, center);
+    return impl_->segments[0]->as_soma();
 }
 
 cable_segment* cable_cell::add_cable(index_type parent, segment_ptr&& cable) {
@@ -54,41 +151,210 @@ cable_segment* cable_cell::add_cable(index_type parent, segment_ptr&& cable) {
         throw cable_cell_error("parent index out of range");
     }
 
-    segments_.push_back(std::move(cable));
-    parents_.push_back(parent);
+    impl_->segments.push_back(std::move(cable));
+    impl_->parents.push_back(parent);
 
-    return segments_.back()->as_cable();
+    return impl_->segments.back()->as_cable();
 }
 
 segment* cable_cell::segment(index_type index) {
-    assert_valid_segment(index);
-    return segments_[index].get();
+    impl_->assert_valid_segment(index);
+    return impl_->segments[index].get();
 }
+
 segment const* cable_cell::parent(index_type index) const {
-    assert_valid_segment(index);
-    return segments_[parents_[index]].get();
+    impl_->assert_valid_segment(index);
+    return impl_->segments[impl_->parents[index]].get();
 }
 
 segment const* cable_cell::segment(index_type index) const {
-    assert_valid_segment(index);
-    return segments_[index].get();
+    impl_->assert_valid_segment(index);
+    return impl_->segments[index].get();
+}
+
+const std::vector<segment_ptr>& cable_cell::segments() const {
+    return impl_->segments;
+}
+
+const std::vector<index_type>& cable_cell::parents() const {
+    return impl_->parents;
+}
+
+value_type cable_cell::segment_length_constant(value_type frequency, index_type segidx,
+    const cable_cell_parameter_set& global_defaults) const
+{
+    return 0.5/segment_mean_attenuation(frequency, segidx, global_defaults);
 }
 
 bool cable_cell::has_soma() const {
     return !segment(0)->is_placeholder();
 }
 
-void cable_cell::paint(const std::string& target, const std::string& description) {
-    auto it = regions_.find(target);
+const std::vector<cable_cell::gap_junction_instance>& cable_cell::gap_junction_sites() const {
+    return impl_->gap_junction_sites;
+}
+
+const std::vector<cable_cell::synapse_instance>& cable_cell::synapses() const {
+    return impl_->synapses;
+}
+
+const std::vector<cable_cell::detector_instance>& cable_cell::detectors() const {
+    return impl_->spike_detectors;
+}
+
+const std::vector<cable_cell::stimulus_instance>& cable_cell::stimuli() const {
+    return impl_->stimuli;
+}
+
+void cable_cell::set_regions(cable_cell::region_map r) {
+    impl_->regions = std::move(r);
+}
+
+void cable_cell::set_locsets(cable_cell::locset_map l) {
+    impl_->locations = std::move(l);
+}
+
+//
+// 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) {
+    auto it = impl_->regions.find(target);
 
     // Nothing to do if there are no regions that match.
-    if (it==regions_.end()) return;
+    if (it==impl_->regions.end()) return;
+
+    for (auto c: it->second) {
+        if (c.prox_pos!=0 || c.dist_pos!=1) {
+            throw cable_cell_error(util::pprintf(
+                "cable_cell does not support regions with partial branches: \"{}\": {}",
+                target, c));
+        }
+        segment(c.branch)->add_mechanism(std::move(desc));
+    }
+}
+
+//
+// Placers.
+//
+// Implementation of user API for placing discrete items on cell morphology,
+// such as synapses, spike detectors and stimuli.
+//
+
+//
+// Synapses
+//
+
+lid_range cable_cell::place(const std::string& target, const mechanism_desc& desc) {
+    const auto first = impl_->synapses.size();
+
+    const auto it = impl_->locations.find(target);
+    if (it==impl_->locations.end()) return lid_range(first, first);
+
+    return impl_->place(it->second, desc, impl_->synapses);
+}
+
+/*
+lid_range cable_cell::place(const locset& ls, const mechanism_desc& desc) {
+    const auto locs = thingify(ls, impl_->morph);
+    return impl_->place(locs, desc, impl_->synapses);
+}
+*/
+
+lid_range cable_cell::place(const mlocation& loc, const mechanism_desc& desc) {
+    if (!impl_->valid_location(loc)) {
+        throw cable_cell_error(util::pprintf(
+            "Attempt to add synapse at invalid location: \"{}\"", loc));
+    }
+    return impl_->place({loc}, desc, impl_->synapses);
+}
+
+//
+// Stimuli
+//
+
+lid_range cable_cell::place(const std::string& target, const i_clamp& desc) {
+    const auto first = impl_->stimuli.size();
+
+    const auto it = impl_->locations.find(target);
+    if (it==impl_->locations.end()) return lid_range(first, first);
+
+    return impl_->place(it->second, desc, impl_->stimuli);
+}
+
+/*
+lid_range cable_cell::place(const locset& ls, const i_clamp& desc) {
+    const auto locs = thingify(ls, impl_->morph);
+    return impl_->place(locs, desc, impl_->stimuli);
+}
+*/
+
+lid_range cable_cell::place(const mlocation& loc, const i_clamp& desc) {
+    if (!impl_->valid_location(loc)) {
+        throw cable_cell_error(util::pprintf(
+            "Attempt to add stimulus at invalid location: {}", loc));
+    }
+    return impl_->place({loc}, desc, impl_->stimuli);
+}
 
-    for (auto i: it->second) {
-        segment(i)->add_mechanism(description);
+//
+// Gap junctions.
+//
+
+lid_range cable_cell::place(const std::string& target, gap_junction_site) {
+    const auto first = impl_->stimuli.size();
+
+    const auto it = impl_->locations.find(target);
+    if (it==impl_->locations.end()) return lid_range(first, first);
+
+    return impl_->place_gj(it->second);
+}
+
+/*
+lid_range cable_cell::place(const locset& ls, gap_junction_site) {
+    const auto locs = thingify(ls, impl_->morph);
+    return impl_->place_gj(locs);
+}
+*/
+
+lid_range cable_cell::place(const mlocation& loc, gap_junction_site) {
+    if (!impl_->valid_location(loc)) {
+        throw cable_cell_error(util::pprintf(
+            "Attempt to add gap junction site at invalid location: {}", loc));
     }
+    return impl_->place_gj({loc});
+}
+
+//
+// Spike detectors.
+//
+lid_range cable_cell::place(const std::string& target, const threshold_detector& desc) {
+    const auto first = impl_->stimuli.size();
+
+    const auto it = impl_->locations.find(target);
+    if (it==impl_->locations.end()) return lid_range(first, first);
+
+    return impl_->place(it->second, desc.threshold, impl_->spike_detectors);
 }
 
+/*
+lid_range cable_cell::place(const locset& ls, const threshold_detector& desc) {
+    const auto locs = thingify(ls, impl_->morph);
+    return impl_->place(locs, desc.threshold, impl_->spike_detectors);
+}
+*/
+
+lid_range cable_cell::place(const mlocation& loc, const threshold_detector& desc) {
+    if (!impl_->valid_location(loc)) {
+        throw cable_cell_error(util::pprintf(
+            "Attempt to add spike detector at invalid location: {}", loc));
+    }
+    return impl_->place({loc}, desc.threshold, impl_->spike_detectors);
+}
+
+
 soma_segment* cable_cell::soma() {
     return has_soma()? segment(0)->as_soma(): nullptr;
 }
@@ -98,13 +364,13 @@ const soma_segment* cable_cell::soma() const {
 }
 
 cable_segment* cable_cell::cable(index_type index) {
-    assert_valid_segment(index);
+    impl_->assert_valid_segment(index);
     auto cable = segment(index)->as_cable();
     return cable? cable: throw cable_cell_error("segment is not a cable segment");
 }
 
 const cable_segment* cable_cell::cable(index_type index) const {
-    assert_valid_segment(index);
+    impl_->assert_valid_segment(index);
     auto cable = segment(index)->as_cable();
     return cable? cable: throw cable_cell_error("segment is not a cable segment");
 }
@@ -118,18 +384,17 @@ std::vector<size_type> cable_cell::compartment_counts() const {
     return comp_count;
 }
 
-size_type cable_cell::num_compartments() const {
-    return util::sum_by(segments_,
-            [](const segment_ptr& s) { return s->num_compartments(); });
+const em_morphology* cable_cell::morphology() const {
+    return &(impl_->morph);
 }
 
-void cable_cell::add_stimulus(mlocation loc, i_clamp stim) {
-    (void)segment(loc.branch); // assert loc.segment in range
-    stimuli_.push_back({loc, std::move(stim)});
+void cable_cell::set_morphology(em_morphology m) {
+    impl_->morph = std::move(m);
 }
 
-void cable_cell::add_detector(mlocation loc, double threshold) {
-    spike_detectors_.push_back({loc, threshold});
+size_type cable_cell::num_compartments() const {
+    return util::sum_by(impl_->segments,
+            [](const segment_ptr& s) { return s->num_compartments(); });
 }
 
 // Approximating wildly by ignoring O(x) effects entirely, the attenuation b
@@ -145,8 +410,10 @@ value_type cable_cell::segment_mean_attenuation(
     value_type frequency, index_type segidx,
     const cable_cell_parameter_set& global_defaults) const
 {
-    value_type R = default_parameters.axial_resistivity.value_or(global_defaults.axial_resistivity.value());
-    value_type C = default_parameters.membrane_capacitance.value_or(global_defaults.membrane_capacitance.value());
+    value_type R = default_parameters.axial_resistivity.value_or(
+            global_defaults.axial_resistivity.value());
+    value_type C = default_parameters.membrane_capacitance.value_or(
+            global_defaults.membrane_capacitance.value());
 
     value_type length_factor = 0; // [1/õm]
 
@@ -186,10 +453,10 @@ cable_cell make_cable_cell(const morphology& m,
                            bool compartments_from_discretization)
 {
     using point3d = cable_cell::point_type;
-    cable_cell newcell;
+    cable_cell cell;
 
     if (!m.num_branches()) {
-        return newcell;
+        return cell;
     }
 
     // Add the soma.
@@ -197,7 +464,7 @@ cable_cell make_cable_cell(const morphology& m,
 
     // If there is no spherical root/soma use a zero-radius soma.
     double srad = m.spherical_root()? loc.radius: 0.;
-    newcell.add_soma(srad, point3d(loc.x, loc.y, loc.z));
+    cell.add_soma(srad, point3d(loc.x, loc.y, loc.z));
 
     auto& samples = m.samples();
     for (auto i: util::make_span(1, m.num_branches())) {
@@ -232,7 +499,7 @@ cable_cell make_cable_cell(const morphology& m,
         if (!m.spherical_root()) {
             pid = pid==mnpos? 0: pid+1;
         }
-        auto cable = newcell.add_cable(pid, kind, radii, points);
+        auto cable = cell.add_cable(pid, make_segment<cable_segment>(kind, radii, points));
         if (compartments_from_discretization) {
             cable->as_cable()->set_compartments(radii.size()-1);
         }
@@ -242,23 +509,21 @@ cable_cell make_cable_cell(const morphology& m,
     // Ignores the pointsets, for now.
     auto em = em_morphology(m); // for converting labels to "segments"
 
-    std::unordered_map<std::string, std::vector<msize_t>> regions;
+    std::unordered_map<std::string, mcable_list> regions;
     for (auto r: dictionary.regions()) {
-        mcable_list L = thingify(r.second, em);
-        std::vector<msize_t> bids;
-        for (auto c: L) {
-            if (c.prox_pos!=0 || c.dist_pos!=1) {
-                throw cable_cell_error(util::pprintf(
-                    "cable_cell does not support regions with partial branches: \"{}\": {}",
-                    r.first, c));
-            }
-            bids.push_back(c.branch);
-        }
-        regions[r.first] = bids;
+        regions[r.first] = thingify(r.second, em);
     }
-    newcell.set_regions(std::move(regions));
+    cell.set_regions(std::move(regions));
+
+    std::unordered_map<std::string, mlocation_list> locsets;
+    for (auto l: dictionary.locsets()) {
+        locsets[l.first] = thingify(l.second, em);
+    }
+    cell.set_locsets(std::move(locsets));
+
+    cell.set_morphology(std::move(em));
 
-    return newcell;
+    return cell;
 }
 
 } // namespace arb
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index 2303aaff7f63e70277b005b0c38e7fd1f9c307ea..2e35ded6c276c9a441c756b71b1134cc26d3b034 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+
 #include <unordered_map>
 #include <string>
 #include <vector>
@@ -16,22 +17,17 @@
 
 namespace arb {
 
-// Current clamp description for stimulus specification.
-
-struct i_clamp {
-    using value_type = double;
-
-    value_type delay = 0;      // [ms]
-    value_type duration = 0;   // [ms]
-    value_type amplitude = 0;  // [nA]
-
-    i_clamp(value_type delay, value_type duration, value_type amplitude):
-        delay(delay), duration(duration), amplitude(amplitude)
-    {}
+// Pair of indexes that describe range of local indices.
+// Returned by cable_cell::place() calls, so that the caller can
+// refer to targets, detectors, etc on the cell.
+struct lid_range {
+    cell_lid_type begin;
+    cell_lid_type end;
+    lid_range(cell_lid_type b, cell_lid_type e):
+        begin(b), end(e) {}
 };
 
 // Probe type for cell descriptions.
-
 struct cell_probe_address {
     enum probe_kind {
         membrane_voltage, membrane_current
@@ -41,7 +37,10 @@ struct cell_probe_address {
     probe_kind kind;
 };
 
-/// high-level abstract representation of a cell and its segments
+// Forward declare the implementation, for PIMPL.
+struct cable_cell_impl;
+
+// High-level abstract representation of a cell and its segments
 class cable_cell {
 public:
     using index_type = cell_lid_type;
@@ -49,9 +48,10 @@ public:
     using value_type = double;
     using point_type = point<value_type>;
 
-    using gap_junction_instance = mlocation;
+    using region_map = std::unordered_map<std::string, mcable_list>;
+    using locset_map = std::unordered_map<std::string, mlocation_list>;
 
-    using region_map = std::unordered_map<std::string, std::vector<msize_t>>;
+    using gap_junction_instance = mlocation;
 
     struct synapse_instance {
         mlocation location;
@@ -74,30 +74,11 @@ public:
     cable_cell();
 
     /// Copy constructor
-    cable_cell(const cable_cell& other):
-        default_parameters(other.default_parameters),
-        parents_(other.parents_),
-        stimuli_(other.stimuli_),
-        synapses_(other.synapses_),
-        gap_junction_sites_(other.gap_junction_sites_),
-        spike_detectors_(other.spike_detectors_),
-        regions_(other.regions_)
-    {
-        // unique_ptr's cannot be copy constructed, do a manual assignment
-        segments_.reserve(other.segments_.size());
-        for (const auto& s: other.segments_) {
-            segments_.push_back(s->clone());
-        }
-    }
+    cable_cell(const cable_cell& other);
 
     /// Move constructor
     cable_cell(cable_cell&& other) = default;
 
-    /// Return the kind of cell, used for grouping into cell_groups
-    cell_kind get_cell_kind() const  {
-        return cell_kind::cable;
-    }
-
     /// add a soma to the cell
     /// radius must be specified
     soma_segment* add_soma(value_type radius, point_type center=point_type());
@@ -107,102 +88,85 @@ public:
     /// cable is the segment that will be moved into the cell
     cable_segment* add_cable(index_type parent, segment_ptr&& cable);
 
-    /// add a cable by constructing it in place
-    /// parent is the index of the parent segment for the cable section
-    /// args are the arguments to be used to consruct the new cable
-    template <typename... Args>
-    cable_segment* add_cable(index_type parent, Args&&... args);
-
-    /// the number of segments in the cell
-    size_type num_segments() const;
-
     bool has_soma() const;
 
     class segment* segment(index_type index);
     const class segment* parent(index_type index) const;
     const class segment* segment(index_type index) const;
 
-    /// access pointer to the soma
-    /// returns nullptr if the cell has no soma
+    // access pointer to the soma
+    // returns nullptr if the cell has no soma
+    // LEGACY
     soma_segment* soma();
     const soma_segment* soma() const;
 
-    /// access pointer to a cable segment
-    /// will throw an cable_cell_error exception if
-    /// the cable index is not valid
+    // access pointer to a cable segment
+    // will throw an cable_cell_error exception if
+    // the cable index is not valid
+    // LEGACY
     cable_segment* cable(index_type index);
     const cable_segment* cable(index_type index) const;
 
-    /// the total number of compartments over all segments
-    size_type num_compartments() const;
+    const std::vector<segment_ptr>& segments() const;
 
-    std::vector<segment_ptr> const& segments() const {
-        return segments_;
-    }
+    // the number of segments in the cell
+    size_type num_segments() const;
 
-    /// return a vector with the compartment count for each segment in the cell
+    // return a vector with the compartment count for each segment in the cell
+    // LEGACY
     std::vector<size_type> compartment_counts() const;
 
-    //////////////////
-    // stimuli
-    //////////////////
-    void add_stimulus(mlocation loc, i_clamp stim);
-
-    std::vector<stimulus_instance>&
-    stimuli() {
-        return stimuli_;
-    }
-
-    const std::vector<stimulus_instance>&
-    stimuli() const {
-        return stimuli_;
-    }
-
-    //////////////////
-    // painters
-    //////////////////
-    void paint(const std::string& target, const std::string& description);
-
-    //////////////////
-    // synapses
-    //////////////////
-    void add_synapse(mlocation loc, mechanism_desc p)
-    {
-        synapses_.push_back(synapse_instance{loc, std::move(p)});
-    }
-    const std::vector<synapse_instance>& synapses() const {
-        return synapses_;
-    }
-
-    //////////////////
-    // gap-junction
-    //////////////////
-    void add_gap_junction(mlocation location)
-    {
-        gap_junction_sites_.push_back(location);
-    }
-    const std::vector<gap_junction_instance>& gap_junction_sites() const {
-        return gap_junction_sites_;
-    }
-
-    //////////////////
+    // The total number of compartments in the discretised cell.
+    // 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& target, mechanism_desc);
+
+    // Synapses.
+    lid_range place(const std::string& target, const mechanism_desc&);
+    lid_range place(const mlocation&, const mechanism_desc&);  // LEGACY
+    //lid_range place(const locset&, const mechanism_desc&);
+
+    // Stimuli.
+    lid_range place(const std::string& target, const i_clamp&);
+    lid_range place(const mlocation&, const i_clamp&);  // LEGACY
+    //lid_range place(const locset&, const i_clamp&);
+
+    // Gap junctions.
+    lid_range place(const std::string&, gap_junction_site);
+    lid_range place(const mlocation& loc, gap_junction_site);  // LEGACY
+    //lid_range place(const locset&, gap_junction_site);
+
     // spike detectors
-    //////////////////
-    void add_detector(mlocation loc, double threshold);
+    lid_range place(const std::string&, const threshold_detector&);
+    lid_range place(const mlocation&, const threshold_detector&);  // LEGACY
+    //lid_range place(const locset&, const threshold_detector&);
+
+    //
+    // access to placed items
+    //
 
-    std::vector<detector_instance>&
-    detectors() {
-        return spike_detectors_;
-    }
+    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 std::vector<detector_instance>&
-    detectors() const {
-        return spike_detectors_;
-    }
+    // These setters are temporary, for "side-loading" in make_cable_cell.
+    // In the regions, locset and morphology descriptions will be passed directly
+    // to the cable_cell constructor.
+    void set_regions(region_map r);
+    void set_locsets(locset_map l);
+    void set_morphology(em_morphology m);
 
-    void set_regions(region_map r) {
-        regions_ = std::move(r);
-    }
+    const em_morphology* morphology() const;
 
     // Checks that two cells have the same
     //  - number and type of segments
@@ -212,9 +176,7 @@ public:
     friend bool cell_basic_equality(const cable_cell&, const cable_cell&);
 
     // Public view of parent indices vector.
-    const std::vector<index_type>& parents() const {
-        return parents_;
-    }
+    const std::vector<index_type>& parents() const;
 
     // Approximate per-segment mean attenuation b(f) at given frequency f,
     // ignoring membrane resistance [1/µm].
@@ -225,50 +187,13 @@ public:
     // Hines and Carnevale (2001), "NEURON: A Tool for Neuroscientists",
     // Neuroscientist 7, pp. 123-135.
     value_type segment_length_constant(value_type frequency, index_type segidx,
-        const cable_cell_parameter_set& global_defaults) const
-    {
-        return 0.5/segment_mean_attenuation(frequency, segidx, global_defaults);
-    }
+        const cable_cell_parameter_set& global_defaults) const;
 
 private:
-    void assert_valid_segment(index_type) const;
-
-    // storage for connections
-    std::vector<index_type> parents_;
-
-    // the segments
-    std::vector<segment_ptr> segments_;
-
-    // the stimuli
-    std::vector<stimulus_instance> stimuli_;
 
-    // the synapses
-    std::vector<synapse_instance> synapses_;
-
-    // the gap_junctions
-    std::vector<gap_junction_instance> gap_junction_sites_;
-
-    // the sensors
-    std::vector<detector_instance> spike_detectors_;
-
-    // Named regions, oh my.
-    region_map regions_;
+    std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)> impl_;
 };
 
-// create a cable by forwarding cable construction parameters provided by the user
-template <typename... Args>
-cable_segment* cable_cell::add_cable(cable_cell::index_type parent, Args&&... args)
-{
-    // check for a valid parent id
-    if (parent>=num_segments()) {
-        throw cable_cell_error("parent index of cell segment is out of range");
-    }
-    segments_.push_back(make_segment<cable_segment>(std::forward<Args>(args)...));
-    parents_.push_back(parent);
-
-    return segments_.back()->as_cable();
-}
-
 // Create a cable cell from a morphology specification.
 // If compartments_from_discretization is true, set number of compartments
 // in each segment to be the number of piecewise linear sections in the
diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp
index 2c147c0ce25667eb91ee1b0ee12e285e4cbfc754..8ef752abae28868b00d2feecb11ebd065d61e77e 100644
--- a/arbor/include/arbor/cable_cell_param.hpp
+++ b/arbor/include/arbor/cable_cell_param.hpp
@@ -16,6 +16,30 @@ struct cable_cell_error: arbor_exception {
         arbor_exception("cable_cell: "+what) {}
 };
 
+// Current clamp description for stimulus specification.
+struct i_clamp {
+    using value_type = double;
+
+    value_type delay = 0;      // [ms]
+    value_type duration = 0;   // [ms]
+    value_type amplitude = 0;  // [nA]
+
+    i_clamp() = default;
+
+    i_clamp(value_type delay, value_type duration, value_type amplitude):
+        delay(delay), duration(duration), amplitude(amplitude)
+    {}
+};
+
+// Threshold detector description.
+struct threshold_detector {
+    double threshold;
+};
+
+// Tag type for dispatching cable_cell::place() calls that add gap junction sites.
+struct gap_junction_site {};
+
+
 // Mechanism description, viz. mechanism name and
 // (non-global) parameter settings. Used to assign
 // density and point mechanisms to segments and
diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp
index 00c7e97386025aa9dbbeea782bb0ed178e01733a..63e0e6923e1fc4ecf772cb6c7de0c53f43e1eb81 100644
--- a/arbor/include/arbor/morph/locset.hpp
+++ b/arbor/include/arbor/morph/locset.hpp
@@ -17,10 +17,10 @@ namespace arb {
 // interface for concretising locsets.
 class em_morphology;
 
+class locset;
+
 class locset {
 public:
-    locset() = delete;
-
     template <typename Impl,
               typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, locset>::value>>
     explicit locset(Impl&& impl):
@@ -40,6 +40,12 @@ public:
         return *this;
     }
 
+    // The default constructor creates an empty "nil" set.
+    locset();
+
+    // Construct an explicit location set with a single location.
+    locset(mlocation other);
+
     template <typename Impl,
               typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, locset>::value>>
     locset& operator=(Impl&& other) {
diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp
index 0c9ed9b0e6726ee811fa3579c8fefc198f0f7329..16d41ec763479797434a39f533e6b29ec8afcc68 100644
--- a/arbor/include/arbor/morph/morphology.hpp
+++ b/arbor/include/arbor/morph/morphology.hpp
@@ -21,6 +21,7 @@ class morphology {
 public:
     morphology(sample_tree m, bool use_spherical_root);
     morphology(sample_tree m);
+    morphology();
 
     // Whether the root of the morphology is spherical.
     bool spherical_root() const;
diff --git a/arbor/include/arbor/morph/region.hpp b/arbor/include/arbor/morph/region.hpp
index d3d7f735d553202da1070300be948a57619bed47..1cb884b025bde88fdb3440e4f0bd14a1e9dcb8a9 100644
--- a/arbor/include/arbor/morph/region.hpp
+++ b/arbor/include/arbor/morph/region.hpp
@@ -19,8 +19,6 @@ class em_morphology;
 
 class region {
 public:
-    region() = delete;
-
     template <typename Impl,
               typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, region>::value>>
     explicit region(Impl&& impl):
@@ -32,6 +30,9 @@ public:
 
     region(region&& other) = default;
 
+    // The default constructor creates an empty "nil" region.
+    region();
+
     region(const region& other):
         impl_(other.impl_->clone()) {}
 
@@ -110,6 +111,9 @@ private:
 
 namespace reg {
 
+// An empty region.
+region nil();
+
 // An explicit cable section.
 region cable(mcable);
 
diff --git a/arbor/morph/em_morphology.cpp b/arbor/morph/em_morphology.cpp
index ba6d275131217ae32a7b175c25e893ee0fb23e31..f00865e3c59259730e020fff7370d715451b0b5e 100644
--- a/arbor/morph/em_morphology.cpp
+++ b/arbor/morph/em_morphology.cpp
@@ -25,6 +25,8 @@ em_morphology::em_morphology(const morphology& m):
     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.;
@@ -52,7 +54,7 @@ em_morphology::em_morphology(const morphology& m):
         for (auto i: idx) {
             sample_locs_[i] = {msize_t(b), (dist2root_[i]-start)/len};
         }
-        // For ensure that all non-spherical branches have their last sample 
+        // Ensure that all non-spherical branches have their last sample 
         if (idx.size()>1u) {
             sample_locs_[idx.back()] = mlocation{msize_t(b), 1};
         }
@@ -72,6 +74,10 @@ em_morphology::em_morphology(const morphology& m):
     }
 }
 
+em_morphology::em_morphology():
+    em_morphology(morphology())
+{}
+
 const morphology& em_morphology::morph() const {
     return morph_;
 }
diff --git a/arbor/morph/em_morphology.hpp b/arbor/morph/em_morphology.hpp
index 94aa5b0fe783551928d3506d5cdc5910d2ea0d5b..1e47dd59b5dd4c32ec33fd1e5bfc7f11ba315063 100644
--- a/arbor/morph/em_morphology.hpp
+++ b/arbor/morph/em_morphology.hpp
@@ -19,6 +19,7 @@ class em_morphology {
     std::vector<double> branch_lengths_;
 
 public:
+    em_morphology();
     em_morphology(const morphology& m);
 
     const morphology& morph() const;
diff --git a/arbor/morph/label_dict.cpp b/arbor/morph/label_dict.cpp
index a6bf10c8a546f83bd0fed9866eb00a0fff0d85e2..e72c40067ebf50e5618fe65d25ca03e1b7f61a03 100644
--- a/arbor/morph/label_dict.cpp
+++ b/arbor/morph/label_dict.cpp
@@ -24,16 +24,7 @@ void label_dict::set(const std::string& name, arb::locset ls) {
         throw morphology_error(util::pprintf(
                 "Attempt to add a locset \"{}\" to a label dictionary that already contains a region with the same name.", name));
     }
-    // First remove an entry with the same name if it exists.
-    // Has to be this way, because insert_or_assign() is C++17, and we
-    // can't use operator[] because locset is not default constructable.
-    auto it = locsets_.find(name);
-    if (it!=locsets_.end()) {
-        it->second = std::move(ls);
-    }
-    else {
-        locsets_.emplace(name, std::move(ls));
-    }
+    locsets_[name] = std::move(ls);
 }
 
 void label_dict::set(const std::string& name, arb::region reg) {
@@ -41,16 +32,7 @@ void label_dict::set(const std::string& name, arb::region reg) {
         throw morphology_error(util::pprintf(
                 "Attempt to add a region \"{}\" to a label dictionary that already contains a locset with the same name.", name));
     }
-    // First remove an entry with the same name if it exists.
-    // Has to be this way, because insert_or_assign() is C++17, and we
-    // can't use operator[] because region is not default constructable.
-    auto it = regions_.find(name);
-    if (it!=regions_.end()) {
-        it->second = std::move(reg);
-    }
-    else {
-        regions_.emplace(name, std::move(reg));
-    }
+    regions_[name] = std::move(reg);
 }
 
 util::optional<const region&> label_dict::region(const std::string& name) const {
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
index f1215c4eaf2fa301988eff86736e373b7d8cda09..55c0752ccf366594db1c6fb211e91a4d85fe72f9 100644
--- a/arbor/morph/locset.cpp
+++ b/arbor/morph/locset.cpp
@@ -244,4 +244,12 @@ locset sum(locset lhs, locset rhs) {
     return locset(ls::lsum(std::move(lhs), std::move(rhs)));
 }
 
+locset::locset() {
+    *this = ls::nil();
+}
+
+locset::locset(mlocation other) {
+    *this = ls::location(other);
+}
+
 } // namespace arb
diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp
index 35a73def3deffcf56ec3c0b45146cabde9f6c6d4..ee78c8532ef3a2121aeed365ee0fe1b9cfb53999 100644
--- a/arbor/morph/morphology.cpp
+++ b/arbor/morph/morphology.cpp
@@ -182,6 +182,10 @@ morphology::morphology(sample_tree m):
     impl_(std::make_shared<const morphology_impl>(std::move(m)))
 {}
 
+morphology::morphology():
+    morphology(sample_tree())
+{}
+
 // The parent branch of branch b.
 msize_t morphology::branch_parent(msize_t b) const {
     return impl_->branch_parents_[b];
diff --git a/arbor/morph/region.cpp b/arbor/morph/region.cpp
index 90d999b7fe0a5fcbf4a78aff428987e23c187d61..b456c42e19da81d06fa8d615be573d7e65dede88 100644
--- a/arbor/morph/region.cpp
+++ b/arbor/morph/region.cpp
@@ -98,6 +98,23 @@ mcable_list remove_cover(mcable_list cables, const em_morphology& m) {
     return merge(cables);
 }
 
+//
+// Null/empty region
+//
+struct nil_ {};
+
+region nil() {
+    return region{nil_{}};
+}
+
+mcable_list thingify_(const nil_& x, const em_morphology& m) {
+    return {};
+}
+
+std::ostream& operator<<(std::ostream& o, const nil_& x) {
+    return o << "nil";
+}
+
 //
 // Explicit cable section
 //
@@ -300,4 +317,8 @@ region join(region l, region r) {
     return region{reg::reg_or(std::move(l), std::move(r))};
 }
 
+region::region() {
+    *this = reg::nil();
+}
+
 } // namespace arb
diff --git a/example/dryrun/dryrun.cpp b/example/dryrun/dryrun.cpp
index e54bc1a70faeb29bc30a3e001e2dbeb1f18678d3..ff300d93951699d26a4557598d91ffccafde49cb 100644
--- a/example/dryrun/dryrun.cpp
+++ b/example/dryrun/dryrun.cpp
@@ -364,7 +364,7 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
             for (unsigned j=0; j<2; ++j) {
                 if (dis(gen)<bp) {
                     sec_ids.push_back(nsec++);
-                    auto dend = cell.add_cable(sec, arb::section_kind::dendrite, dend_radius, dend_radius, l);
+                    auto dend = cell.add_cable(sec, arb::make_segment<arb::cable_segment>(arb::section_kind::dendrite, dend_radius, dend_radius, l));
                     dend->set_compartments(nc);
                     dend->add_mechanism("pas");
                 }
@@ -377,10 +377,10 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     }
 
     // Add spike threshold detector at the soma.
-    cell.add_detector({0,0}, 10);
+    cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.add_synapse({1, 0.5}, "expsyn");
+    cell.place(arb::mlocation{1, 0.5}, "expsyn");
 
     return cell;
 }
diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp
index 8212beec6b8d181de0cf96d089b6dd59c96d007a..4e3aa846f75176670c1060b23abab22c5812b617 100644
--- a/example/gap_junctions/gap_junctions.cpp
+++ b/example/gap_junctions/gap_junctions.cpp
@@ -363,23 +363,23 @@ arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration)
     set_reg_params();
     setup_seg(soma);
 
-    auto dend = cell.add_cable(0, arb::section_kind::dendrite, 3.0/2.0, 3.0/2.0, 300); //cable 1
+    auto dend = cell.add_cable(0, arb::make_segment<arb::cable_segment>(arb::section_kind::dendrite, 3.0/2.0, 3.0/2.0, 300)); //cable 1
     dend->set_compartments(1);
     set_reg_params();
     setup_seg(dend);
 
-    cell.add_detector({0,0}, 10);
+    cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
-    cell.add_gap_junction({0, 1});
-    cell.add_gap_junction({1, 1});
+    cell.place(arb::mlocation{0, 1}, arb::gap_junction_site{});
+    cell.place(arb::mlocation{1, 1}, arb::gap_junction_site{});
 
     if (!gid) {
         arb::i_clamp stim(0, stim_duration, 0.4);
-        cell.add_stimulus({0, 0.5}, stim);
+        cell.place(arb::mlocation{0, 0.5}, stim);
     }
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.add_synapse({1, 0.5}, "expsyn");
+    cell.place(arb::mlocation{1, 0.5}, "expsyn");
 
     return cell;
 }
diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp
index 9c18c5deba2430698db2ee81c934ce1de2335b62..9d48f0c6bb1f999a2785eb564219c3720b44b13a 100644
--- a/example/generators/generators.cpp
+++ b/example/generators/generators.cpp
@@ -55,7 +55,7 @@ public:
         // Add one synapse at the soma.
         // This synapse will be the target for all events, from both
         // event_generators.
-        c.add_synapse({0, 0.5}, "expsyn");
+        c.place(arb::mlocation{0, 0.5}, "expsyn");
 
         return std::move(c);
     }
diff --git a/example/ring/ring.cpp b/example/ring/ring.cpp
index 4cc95891bbe7a41e0c24933a031c1368b7173376..ac7d344f304f569a6a7dbba52a08c200db0b748d 100644
--- a/example/ring/ring.cpp
+++ b/example/ring/ring.cpp
@@ -367,7 +367,7 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
             for (unsigned j=0; j<2; ++j) {
                 if (dis(gen)<bp) {
                     sec_ids.push_back(nsec++);
-                    auto dend = cell.add_cable(sec, arb::section_kind::dendrite, dend_radius, dend_radius, l);
+                    auto dend = cell.add_cable(sec, arb::make_segment<arb::cable_segment>(arb::section_kind::dendrite, dend_radius, dend_radius, l));
                     dend->set_compartments(nc);
                     dend->add_mechanism("pas");
                 }
@@ -380,14 +380,14 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     }
 
     // Add spike threshold detector at the soma.
-    cell.add_detector({0,0}, 10);
+    cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.add_synapse({1, 0.5}, "expsyn");
+    cell.place(arb::mlocation{1, 0.5}, "expsyn");
 
     // Add additional synapses that will not be connected to anything.
     for (unsigned i=1u; i<params.synapses; ++i) {
-        cell.add_synapse({1, 0.5}, "expsyn");
+        cell.place(arb::mlocation{1, 0.5}, "expsyn");
     }
 
     return cell;
diff --git a/example/single/single.cpp b/example/single/single.cpp
index 6673ef09460ca8352d774391c5b0d42b0531e58c..501969f5de77ca9aafbe9fc7853cfd0945e248a5 100644
--- a/example/single/single.cpp
+++ b/example/single/single.cpp
@@ -77,7 +77,7 @@ struct single_recipe: public arb::recipe {
 
         arb::cell_lid_type last_segment = c.num_segments()-1;
         arb::mlocation end_last_segment = { last_segment, 1. };
-        c.add_synapse(end_last_segment, "exp2syn");
+        c.place(end_last_segment, "exp2syn");
 
         return c;
     }
diff --git a/modcc/solvers.cpp b/modcc/solvers.cpp
index 1c8ca27e7bf3d0a1cea7bd1766ae00fe92aa1155..c3b42d574c72382f8d2e276960f59d7094b025e1 100644
--- a/modcc/solvers.cpp
+++ b/modcc/solvers.cpp
@@ -390,7 +390,8 @@ void SparseSolverVisitor::finalize() {
 
     // State variable updates given by rhs/diagonal for reduced matrix.
     Location loc;
-    for (unsigned i = 0; i<A_.nrow(); ++i) {
+    auto nrow = A_.nrow();
+    for (unsigned i = 0; i<nrow; ++i) {
         const symge::sym_row& row = A_[i];
         unsigned rhs_col = A_.augcol();
         unsigned lhs_col = -1;
@@ -401,7 +402,7 @@ void SparseSolverVisitor::finalize() {
             }
         }
 
-        if (lhs_col==-1) {
+        if (lhs_col==unsigned(-1)) {
             throw std::logic_error("zero row in sparse solver matrix");
         }
 
@@ -480,7 +481,8 @@ void LinearSolverVisitor::finalize() {
 
     // State variable updates given by rhs/diagonal for reduced matrix.
     Location loc;
-    for (unsigned i = 0; i < A_.nrow(); ++i) {
+    auto nrow = A_.nrow();
+    for (unsigned i = 0; i < nrow; ++i) {
         const symge::sym_row& row = A_[i];
         unsigned rhs = A_.augcol();
         unsigned lhs = -1;
@@ -491,7 +493,7 @@ void LinearSolverVisitor::finalize() {
             }
         }
 
-        if (lhs==-1) {
+        if (lhs==unsigned(-1)) {
             throw std::logic_error("zero row in linear solver matrix");
         }
 
diff --git a/python/cells.cpp b/python/cells.cpp
index ed332548bada6508f605b220fb15fc3bdeb5e1d0..21a93db2d8df0b9f6f7c7fc0c38d002e1df48d0f 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -115,7 +115,7 @@ arb::cable_cell make_cable_cell(arb::cell_gid_type gid, const cell_parameters& p
             for (unsigned j=0; j<2; ++j) {
                 if (dis(gen)<bp) {
                     sec_ids.push_back(nsec++);
-                    auto dend = cell.add_cable(sec, arb::section_kind::dendrite, dend_radius, dend_radius, l);
+                    auto dend = cell.add_cable(sec, arb::make_segment<arb::cable_segment>(arb::section_kind::dendrite, dend_radius, dend_radius, l));
                     dend->set_compartments(nc);
                     dend->add_mechanism("pas");
                 }
@@ -128,14 +128,14 @@ arb::cable_cell make_cable_cell(arb::cell_gid_type gid, const cell_parameters& p
     }
 
     // Add spike threshold detector at the soma.
-    cell.add_detector({0,0}, 10);
+    cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.add_synapse({1, 0.5}, "expsyn");
+    cell.place(arb::mlocation{1, 0.5}, "expsyn");
 
     // Add additional synapses.
     for (unsigned i=1u; i<params.synapses; ++i) {
-        cell.add_synapse({1, 0.5}, "expsyn");
+        cell.place(arb::mlocation{1, 0.5}, "expsyn");
     }
 
     return cell;
diff --git a/test/common_cells.hpp b/test/common_cells.hpp
index 0694bf6daf874a97904bde52815fb77b5dd52eab..dd40bc91d0add02c8f101651d685d1bc6ab8a316 100644
--- a/test/common_cells.hpp
+++ b/test/common_cells.hpp
@@ -27,7 +27,7 @@ inline cable_cell make_cell_soma_only(bool with_stim = true) {
     soma->add_mechanism("hh");
 
     if (with_stim) {
-        c.add_stimulus({0,0.5}, {10., 100., 0.1});
+        c.place(mlocation{0,0.5}, i_clamp{10., 100., 0.1});
     }
 
     return c;
@@ -60,7 +60,7 @@ inline cable_cell make_cell_ball_and_stick(bool with_stim = true) {
     auto soma = c.add_soma(12.6157/2.0);
     soma->add_mechanism("hh");
 
-    c.add_cable(0, section_kind::dendrite, 1.0/2, 1.0/2, 200.0);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 1.0/2, 1.0/2, 200.0));
 
     for (auto& seg: c.segments()) {
         if (seg->is_dendrite()) {
@@ -70,7 +70,7 @@ inline cable_cell make_cell_ball_and_stick(bool with_stim = true) {
     }
 
     if (with_stim) {
-        c.add_stimulus({1,1}, {5., 80., 0.3});
+        c.place(mlocation{1,1}, i_clamp{5., 80., 0.3});
     }
     return c;
 }
@@ -104,7 +104,7 @@ inline cable_cell make_cell_ball_and_taper(bool with_stim = true) {
     auto soma = c.add_soma(12.6157/2.0);
     soma->add_mechanism("hh");
 
-    c.add_cable(0, section_kind::dendrite, 1.0/2, 0.4/2, 200.0);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 1.0/2, 0.4/2, 200.0));
 
     for (auto& seg: c.segments()) {
         if (seg->is_dendrite()) {
@@ -114,7 +114,7 @@ inline cable_cell make_cell_ball_and_taper(bool with_stim = true) {
     }
 
     if (with_stim) {
-        c.add_stimulus({1,1}, {5., 80., 0.3});
+        c.place(mlocation{1,1}, i_clamp{5., 80., 0.3});
     }
     return c;
 }
@@ -172,7 +172,7 @@ inline cable_cell make_cell_ball_and_squiggle(bool with_stim = true) {
     }
 
     if (with_stim) {
-        c.add_stimulus({1,1}, {5., 80., 0.3});
+        c.place(mlocation{1,1}, i_clamp{5., 80., 0.3});
     }
     return c;
 }
@@ -207,9 +207,9 @@ inline cable_cell make_cell_ball_and_3stick(bool with_stim = true) {
     auto soma = c.add_soma(12.6157/2.0);
     soma->add_mechanism("hh");
 
-    c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100);
-    c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100);
-    c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
+    c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
+    c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
 
     for (auto& seg: c.segments()) {
         if (seg->is_dendrite()) {
@@ -219,8 +219,8 @@ inline cable_cell make_cell_ball_and_3stick(bool with_stim = true) {
     }
 
     if (with_stim) {
-        c.add_stimulus({2,1}, {5.,  80., 0.45});
-        c.add_stimulus({3,1}, {40., 10.,-0.2});
+        c.place(mlocation{2,1}, i_clamp{5.,  80., 0.45});
+        c.place(mlocation{3,1}, i_clamp{40., 10.,-0.2});
     }
     return c;
 }
@@ -253,7 +253,7 @@ inline cable_cell make_cell_simple_cable(bool with_stim = true) {
     c.default_parameters.membrane_capacitance = 0.01;
 
     c.add_soma(0);
-    c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 1000);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 1000));
 
     double gbar = 0.000025;
     double I = 0.1;
@@ -272,7 +272,7 @@ inline cable_cell make_cell_simple_cable(bool with_stim = true) {
     if (with_stim) {
         // stimulus in the middle of our zero-volume 'soma'
         // corresponds to proximal end of cable.
-        c.add_stimulus({0,0.5}, {0., INFINITY, I});
+        c.place(mlocation{0,0.5}, i_clamp{0., INFINITY, I});
     }
     return c;
 }
diff --git a/test/unit/test_cable_cell.cpp b/test/unit/test_cable_cell.cpp
index f297a50d5c4035ce8d9981604e5297827cf1ffb0..49d16a9e74b4e82d79f4a7f4b7350d0e8423db96 100644
--- a/test/unit/test_cable_cell.cpp
+++ b/test/unit/test_cable_cell.cpp
@@ -2,6 +2,7 @@
 
 #include <arbor/cable_cell.hpp>
 #include <arbor/math.hpp>
+#include <arbor/morph/locset.hpp>
 
 #include "tree.hpp"
 
@@ -37,9 +38,6 @@ TEST(cable_cell, soma) {
         auto s = c.soma();
         EXPECT_EQ(s->radius(), soma_radius);
         EXPECT_EQ(s->center().is_set(), true);
-
-        // add expression template library for points
-        //EXPECT_EQ(s->center(), point<double>(0,0,1));
     }
 }
 
@@ -78,7 +76,7 @@ TEST(cable_cell, add_segment) {
 
         c.add_cable(
             0,
-            section_kind::dendrite, cable_radius, cable_radius, cable_length
+            make_segment<cable_segment>(section_kind::dendrite, cable_radius, cable_radius, cable_length)
         );
 
         EXPECT_EQ(c.num_segments(), 2u);
@@ -95,9 +93,9 @@ TEST(cable_cell, add_segment) {
 
         c.add_cable(
             0,
-            section_kind::dendrite,
-            std::vector<double>{cable_radius, cable_radius, cable_radius, cable_radius},
-            std::vector<double>{cable_length, cable_length, cable_length}
+            make_segment<cable_segment>(section_kind::dendrite,
+                                        std::vector<double>{cable_radius, cable_radius, cable_radius, cable_radius},
+                                        std::vector<double>{cable_length, cable_length, cable_length})
         );
 
         EXPECT_EQ(c.num_segments(), 2u);
@@ -196,14 +194,13 @@ TEST(cable_cell, clone) {
 
     cable_cell c;
     c.add_soma(2.1);
-    c.add_cable(0, section_kind::dendrite, 0.3, 0.2, 10);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.3, 0.2, 10));
     c.segment(1)->set_compartments(3);
-    c.add_cable(1, section_kind::dendrite, 0.2, 0.15, 20);
+    c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.2, 0.15, 20));
     c.segment(2)->set_compartments(5);
 
-    c.add_synapse({1, 0.3}, "expsyn");
-
-    c.add_detector({0, 0.5}, 10.0);
+    c.place(mlocation{1, 0.3}, "expsyn");
+    c.place(mlocation{0, 0.5}, threshold_detector{10.0});
 
     // make clone
 
@@ -230,20 +227,10 @@ TEST(cable_cell, clone) {
 
     // check clone is independent
 
-    c.add_cable(2, section_kind::dendrite, 0.15, 0.1, 20);
+    c.add_cable(2, make_segment<cable_segment>(section_kind::dendrite, 0.15, 0.1, 20));
     EXPECT_NE(c.num_segments(), d.num_segments());
 
-    d.detectors()[0].threshold = 13.0;
-    ASSERT_EQ(1u, c.detectors().size());
-    ASSERT_EQ(1u, d.detectors().size());
-    EXPECT_NE(c.detectors()[0].threshold, d.detectors()[0].threshold);
-
     c.segment(1)->set_compartments(7);
     EXPECT_NE(c.segment(1)->num_compartments(), d.segment(1)->num_compartments());
     EXPECT_EQ(c.segment(2)->num_compartments(), d.segment(2)->num_compartments());
 }
-
-TEST(cable_cell, get_kind) {
-    cable_cell c;
-    EXPECT_EQ(cell_kind::cable, c.get_cell_kind());
-}
diff --git a/test/unit/test_domain_decomposition.cpp b/test/unit/test_domain_decomposition.cpp
index 9acd94e09dbe8888f6b17a40295b2d291f33f9fd..30524dec9ff0aac23ea52dc61f86c753227b50ae 100644
--- a/test/unit/test_domain_decomposition.cpp
+++ b/test/unit/test_domain_decomposition.cpp
@@ -66,7 +66,7 @@ namespace {
         arb::util::unique_any get_cell_description(cell_gid_type) const override {
             cable_cell c;
             c.add_soma(20);
-            c.add_gap_junction({0,1});
+            c.place(mlocation{0,1}, gap_junction_site{});
             return {std::move(c)};
         }
 
diff --git a/test/unit/test_event_delivery.cpp b/test/unit/test_event_delivery.cpp
index cafba40f1982fde675cccc26d0cce059b6a7e320..a4d4b632c7edca338f6719c31a6c2181aa3c9713 100644
--- a/test/unit/test_event_delivery.cpp
+++ b/test/unit/test_event_delivery.cpp
@@ -29,9 +29,9 @@ struct test_recipe: public n_cable_cell_recipe {
     static cable_cell test_cell() {
         cable_cell c;
         c.add_soma(10.)->add_mechanism("pas");
-        c.add_synapse({0, 0.5}, "expsyn");
-        c.add_detector({0, 0.5}, -64);
-        c.add_gap_junction({0, 0.5});
+        c.place(mlocation{0, 0.5}, "expsyn");
+        c.place(mlocation{0, 0.5}, threshold_detector{-64});
+        c.place(mlocation{0, 0.5}, gap_junction_site{});
         return c;
     }
 
diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp
index 3bd981cd580c884e1dd9fcbaef2af08f4785fce1..6ad308015644738ab31c73b50fdd30897cc85dc5 100644
--- a/test/unit/test_fvm_layout.cpp
+++ b/test/unit/test_fvm_layout.cpp
@@ -10,6 +10,7 @@
 #include "util/maputil.hpp"
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
+#include "io/sepval.hpp"
 
 #include "common.hpp"
 #include "unit_test_catalogue.hpp"
@@ -99,17 +100,17 @@ namespace {
         s = c2.add_soma(14./2);
         s->add_mechanism("hh");
 
-        s = c2.add_cable(0, section_kind::dendrite, 1.0/2, 1.0/2, 200);
+        s = c2.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 1.0/2, 1.0/2, 200));
         s->parameters.membrane_capacitance = 0.017;
 
-        s = c2.add_cable(1, section_kind::dendrite, 0.8/2, 0.8/2, 300);
+        s = c2.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.8/2, 0.8/2, 300));
         s->parameters.membrane_capacitance = 0.013;
 
-        s = c2.add_cable(1, section_kind::dendrite, 0.7/2, 0.7/2, 180);
+        s = c2.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.7/2, 0.7/2, 180));
         s->parameters.membrane_capacitance = 0.018;
 
-        c2.add_stimulus({2,1}, {5.,  80., 0.45});
-        c2.add_stimulus({3,1}, {40., 10.,-0.2});
+        c2.place(mlocation{2,1}, i_clamp{5.,  80., 0.45});
+        c2.place(mlocation{3,1}, i_clamp{40., 10.,-0.2});
 
         for (auto& seg: c2.segments()) {
             if (seg->is_dendrite()) {
@@ -289,10 +290,10 @@ TEST(fvm_layout, mech_index) {
     check_two_cell_system(cells);
 
     // Add four synapses of two varieties across the cells.
-    cells[0].add_synapse({1, 0.4}, "expsyn");
-    cells[0].add_synapse({1, 0.4}, "expsyn");
-    cells[1].add_synapse({2, 0.4}, "exp2syn");
-    cells[1].add_synapse({3, 0.4}, "expsyn");
+    cells[0].place(mlocation{1, 0.4}, "expsyn");
+    cells[0].place(mlocation{1, 0.4}, "expsyn");
+    cells[1].place(mlocation{2, 0.4}, "exp2syn");
+    cells[1].place(mlocation{3, 0.4}, "expsyn");
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
@@ -337,9 +338,49 @@ TEST(fvm_layout, mech_index) {
     EXPECT_EQ(ivec({0,6}), M.ions.at("k"s).cv);
 }
 
+struct exp_instance {
+    int cv;
+    int multiplicity;
+    std::vector<unsigned> targets;
+    double e;
+    double tau;
+
+    template <typename Seq>
+    exp_instance(int cv, const Seq& tgts, double e, double tau):
+        cv(cv), multiplicity(util::size(tgts)), e(e), tau(tau)
+    {
+        targets.reserve(util::size(tgts));
+        for (auto t: tgts) targets.push_back(t);
+        util::sort(targets);
+    }
+
+    bool matches(const exp_instance& I) const {
+        return I.cv==cv && I.e==e && I.tau==tau && I.targets==targets;
+    }
+
+    bool is_in(const arb::fvm_mechanism_config& C) const {
+        std::vector<unsigned> _;
+        auto part = util::make_partition(_, C.multiplicity);
+        auto& evals = *value_by_key(C.param_values, "e");
+        // Handle both expsyn and exp2syn by looking for "tau1" if "tau"
+        // parameter is not found.
+        auto& tauvals = value_by_key(C.param_values, "tau")?
+            *value_by_key(C.param_values, "tau"):
+            *value_by_key(C.param_values, "tau1");
+
+        for (auto i: make_span(C.multiplicity.size())) {
+            exp_instance other(C.cv[i],
+                               util::subrange_view(C.target, part[i]),
+                               evals[i],
+                               tauvals[i]);
+            if (matches(other)) return true;
+        }
+        return false;
+    }
+};
+
 TEST(fvm_layout, coalescing_synapses) {
     using ivec = std::vector<fvm_index_type>;
-    using fvec = std::vector<fvm_value_type>;
 
     auto syn_desc = [&](const char* name, double val0, double val1) {
         mechanism_desc m(name);
@@ -363,14 +404,16 @@ TEST(fvm_layout, coalescing_synapses) {
     gprop_coalesce.default_parameters = neuron_parameter_defaults;
     gprop_coalesce.coalesce_synapses = true;
 
+    using L=std::initializer_list<unsigned>;
+
     {
         cable_cell cell = make_cell_ball_and_stick();
 
         // Add synapses of two varieties.
-        cell.add_synapse({1, 0.3}, "expsyn");
-        cell.add_synapse({1, 0.5}, "expsyn");
-        cell.add_synapse({1, 0.7}, "expsyn");
-        cell.add_synapse({1, 0.9}, "expsyn");
+        cell.place(mlocation{1, 0.3}, "expsyn");
+        cell.place(mlocation{1, 0.5}, "expsyn");
+        cell.place(mlocation{1, 0.7}, "expsyn");
+        cell.place(mlocation{1, 0.9}, "expsyn");
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -383,10 +426,10 @@ TEST(fvm_layout, coalescing_synapses) {
         cable_cell cell = make_cell_ball_and_stick();
 
         // Add synapses of two varieties.
-        cell.add_synapse({1, 0.3}, "expsyn");
-        cell.add_synapse({1, 0.5}, "exp2syn");
-        cell.add_synapse({1, 0.7}, "expsyn");
-        cell.add_synapse({1, 0.9}, "exp2syn");
+        cell.place(mlocation{1, 0.3}, "expsyn");
+        cell.place(mlocation{1, 0.5}, "exp2syn");
+        cell.place(mlocation{1, 0.7}, "expsyn");
+        cell.place(mlocation{1, 0.9}, "exp2syn");
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -402,10 +445,10 @@ TEST(fvm_layout, coalescing_synapses) {
     {
         cable_cell cell = make_cell_ball_and_stick();
 
-        cell.add_synapse({1, 0.3}, "expsyn");
-        cell.add_synapse({1, 0.5}, "expsyn");
-        cell.add_synapse({1, 0.7}, "expsyn");
-        cell.add_synapse({1, 0.9}, "expsyn");
+        cell.place(mlocation{1, 0.3}, "expsyn");
+        cell.place(mlocation{1, 0.5}, "expsyn");
+        cell.place(mlocation{1, 0.7}, "expsyn");
+        cell.place(mlocation{1, 0.9}, "expsyn");
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_no_coalesce, {cell}, D);
@@ -418,10 +461,10 @@ TEST(fvm_layout, coalescing_synapses) {
         cable_cell cell = make_cell_ball_and_stick();
 
         // Add synapses of two varieties.
-        cell.add_synapse({1, 0.3}, "expsyn");
-        cell.add_synapse({1, 0.5}, "exp2syn");
-        cell.add_synapse({1, 0.7}, "expsyn");
-        cell.add_synapse({1, 0.9}, "exp2syn");
+        cell.place(mlocation{1, 0.3}, "expsyn");
+        cell.place(mlocation{1, 0.5}, "exp2syn");
+        cell.place(mlocation{1, 0.7}, "expsyn");
+        cell.place(mlocation{1, 0.9}, "exp2syn");
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_no_coalesce, {cell}, D);
@@ -438,10 +481,10 @@ TEST(fvm_layout, coalescing_synapses) {
         cable_cell cell = make_cell_ball_and_stick();
 
         // Add synapses of two varieties.
-        cell.add_synapse({1, 0.3}, "expsyn");
-        cell.add_synapse({1, 0.3}, "expsyn");
-        cell.add_synapse({1, 0.7}, "expsyn");
-        cell.add_synapse({1, 0.7}, "expsyn");
+        cell.place(mlocation{1, 0.3}, "expsyn");
+        cell.place(mlocation{1, 0.3}, "expsyn");
+        cell.place(mlocation{1, 0.7}, "expsyn");
+        cell.place(mlocation{1, 0.7}, "expsyn");
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
@@ -454,74 +497,80 @@ TEST(fvm_layout, coalescing_synapses) {
         cable_cell cell = make_cell_ball_and_stick();
 
         // Add synapses of two varieties.
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn", 0, 0.2));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn", 0, 0.2));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn", 0.1, 0.2));
-        cell.add_synapse({1, 0.7}, syn_desc("expsyn", 0.1, 0.2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 0.2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 0.2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0.1, 0.2));
+        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 0.1, 0.2));
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
-        auto &expsyn_config = M.mechanisms.at("expsyn");
-        EXPECT_EQ(ivec({2, 2, 4}), expsyn_config.cv);
-        EXPECT_EQ(ivec({2, 1, 1}), expsyn_config.multiplicity);
-        EXPECT_EQ(fvec({0, 0.1, 0.1}), expsyn_config.param_values[0].second);
-        EXPECT_EQ(fvec({0.2, 0.2, 0.2}), expsyn_config.param_values[1].second);
+        std::vector<exp_instance> instances{
+            exp_instance(2, L{0, 1}, 0., 0.2),
+            exp_instance(2, L{2}, 0.1, 0.2),
+            exp_instance(4, L{3}, 0.1, 0.2),
+        };
+        auto& config = M.mechanisms.at("expsyn");
+        for (auto& instance: instances) {
+            EXPECT_TRUE(instance.is_in(config));
+        }
     }
     {
         cable_cell cell = make_cell_ball_and_stick();
 
         // Add synapses of two varieties.
-        cell.add_synapse({1, 0.7}, syn_desc("expsyn", 0, 3));
-        cell.add_synapse({1, 0.7}, syn_desc("expsyn", 1, 3));
-        cell.add_synapse({1, 0.7}, syn_desc("expsyn", 0, 3));
-        cell.add_synapse({1, 0.7}, syn_desc("expsyn", 1, 3));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn", 0, 2));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn", 1, 2));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn", 0, 2));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn", 1, 2));
+        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 0, 3));
+        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 1, 3));
+        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 0, 3));
+        cell.place(mlocation{1, 0.7}, syn_desc("expsyn", 1, 3));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 1, 2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 0, 2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn", 1, 2));
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
-        auto &expsyn_config = M.mechanisms.at("expsyn");
-        EXPECT_EQ(ivec({2, 2, 4, 4}), expsyn_config.cv);
-        EXPECT_EQ(ivec({4, 6, 5, 7, 0, 2, 1, 3}), expsyn_config.target);
-        EXPECT_EQ(ivec({2, 2, 2, 2}), expsyn_config.multiplicity);
-        EXPECT_EQ(fvec({0, 1, 0, 1}), expsyn_config.param_values[0].second);
-        EXPECT_EQ(fvec({2, 2, 3, 3}), expsyn_config.param_values[1].second);
+        std::vector<exp_instance> instances{
+            exp_instance(2, L{4, 6}, 0.0, 2.0),
+            exp_instance(2, L{5, 7}, 1.0, 2.0),
+            exp_instance(4, L{0, 2}, 0.0, 3.0),
+            exp_instance(4, L{1, 3}, 1.0, 3.0),
+        };
+        auto& config = M.mechanisms.at("expsyn");
+        for (auto& instance: instances) {
+            EXPECT_TRUE(instance.is_in(config));
+        }
     }
     {
         cable_cell cell = make_cell_ball_and_stick();
 
         // Add synapses of two varieties.
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn",  1, 2));
-        cell.add_synapse({1, 0.3}, syn_desc_2("exp2syn", 4, 1));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn",  1, 2));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn",  5, 1));
-        cell.add_synapse({1, 0.3}, syn_desc_2("exp2syn", 1, 3));
-        cell.add_synapse({1, 0.3}, syn_desc("expsyn",  1, 2));
-        cell.add_synapse({1, 0.7}, syn_desc_2("exp2syn", 2, 2));
-        cell.add_synapse({1, 0.7}, syn_desc_2("exp2syn", 2, 1));
-        cell.add_synapse({1, 0.7}, syn_desc_2("exp2syn", 2, 1));
-        cell.add_synapse({1, 0.7}, syn_desc_2("exp2syn", 2, 2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  1, 2));
+        cell.place(mlocation{1, 0.3}, syn_desc_2("exp2syn", 4, 1));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  1, 2));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  5, 1));
+        cell.place(mlocation{1, 0.3}, syn_desc_2("exp2syn", 1, 3));
+        cell.place(mlocation{1, 0.3}, syn_desc("expsyn",  1, 2));
+        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 2));
+        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 1));
+        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 1));
+        cell.place(mlocation{1, 0.7}, syn_desc_2("exp2syn", 2, 2));
 
         fvm_discretization D = fvm_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
-        auto &expsyn_config = M.mechanisms.at("expsyn");
-        EXPECT_EQ(ivec({2, 2}), expsyn_config.cv);
-        EXPECT_EQ(ivec({0, 2, 5, 3}), expsyn_config.target);
-        EXPECT_EQ(ivec({3, 1}), expsyn_config.multiplicity);
-        EXPECT_EQ(fvec({1, 5}), expsyn_config.param_values[0].second);
-        EXPECT_EQ(fvec({2, 1}), expsyn_config.param_values[1].second);
+        for (auto &instance: {exp_instance(2, L{0,2,5}, 1, 2),
+                              exp_instance(2, L{3},     5, 1)}) {
+            EXPECT_TRUE(instance.is_in(M.mechanisms.at("expsyn")));
+        }
 
-        auto &exp2syn_config = M.mechanisms.at("exp2syn");
-        EXPECT_EQ(ivec({2, 2, 4, 4}), exp2syn_config.cv);
-        EXPECT_EQ(ivec({4, 1, 7, 8, 6, 9}), exp2syn_config.target);
-        EXPECT_EQ(ivec({1, 1, 2, 2}), exp2syn_config.multiplicity);
-        EXPECT_EQ(fvec({1, 4, 2, 2}), exp2syn_config.param_values[0].second);
-        EXPECT_EQ(fvec({3, 1, 1, 2}), exp2syn_config.param_values[1].second);
+        for (auto &instance: {exp_instance(2, L{4},   1, 3),
+                              exp_instance(2, L{1},   4, 1),
+                              exp_instance(4, L{7,8}, 2, 1),
+                              exp_instance(4, L{6,9}, 2, 2)}) {
+            EXPECT_TRUE(instance.is_in(M.mechanisms.at("exp2syn")));
+        }
     }
 }
 
@@ -543,14 +592,14 @@ TEST(fvm_layout, synapse_targets) {
         return mechanism_desc(name).set("e", syn_e.at(idx));
     };
 
-    cells[0].add_synapse({1, 0.9}, syn_desc("expsyn", 0));
-    cells[0].add_synapse({0, 0.5}, syn_desc("expsyn", 1));
-    cells[0].add_synapse({1, 0.4}, syn_desc("expsyn", 2));
+    cells[0].place(mlocation{1, 0.9}, syn_desc("expsyn", 0));
+    cells[0].place(mlocation{0, 0.5}, syn_desc("expsyn", 1));
+    cells[0].place(mlocation{1, 0.4}, syn_desc("expsyn", 2));
 
-    cells[1].add_synapse({2, 0.4}, syn_desc("exp2syn", 3));
-    cells[1].add_synapse({1, 0.4}, syn_desc("exp2syn", 4));
-    cells[1].add_synapse({3, 0.4}, syn_desc("expsyn", 5));
-    cells[1].add_synapse({3, 0.7}, syn_desc("exp2syn", 6));
+    cells[1].place(mlocation{2, 0.4}, syn_desc("exp2syn", 3));
+    cells[1].place(mlocation{1, 0.4}, syn_desc("exp2syn", 4));
+    cells[1].place(mlocation{3, 0.4}, syn_desc("expsyn", 5));
+    cells[1].place(mlocation{3, 0.7}, syn_desc("exp2syn", 6));
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
@@ -639,9 +688,9 @@ TEST(fvm_layout, density_norm_area) {
     cable_cell& c = cells[0];
     auto soma = c.add_soma(12.6157/2.0);
 
-    c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100);
-    c.add_cable(1, section_kind::dendrite, 0.5, 0.1, 200);
-    c.add_cable(1, section_kind::dendrite, 0.4, 0.4, 150);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
+    c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.1, 200));
+    c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.4, 0.4, 150));
 
     auto& segs = c.segments();
 
@@ -803,9 +852,9 @@ TEST(fvm_layout, ion_weights) {
     auto construct_cell = [](cable_cell& c) {
         c.add_soma(5);
 
-        c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100);
-        c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 200);
-        c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100);
+        c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
+        c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 200));
+        c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
 
         for (auto& s: c.segments()) s->set_compartments(1);
     };
@@ -875,9 +924,9 @@ TEST(fvm_layout, revpot) {
     auto construct_cell = [](cable_cell& c) {
         c.add_soma(5);
 
-        c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100);
-        c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 200);
-        c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100);
+        c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
+        c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 200));
+        c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
 
         for (auto& s: c.segments()) s->set_compartments(1);
 
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index 42726b6b15511743a58fdec44518521cc4f23fc9..a82132f497579e916fee647bd3a7fab7c84b6798 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -94,7 +94,7 @@ public:
     arb::util::unique_any get_cell_description(cell_gid_type gid) const override {
         cable_cell c;
         c.add_soma(20);
-        c.add_gap_junction({0, 1});
+        c.place(mlocation{0, 1}, gap_junction_site{});
         return {std::move(c)};
     }
 
@@ -162,7 +162,7 @@ public:
     arb::util::unique_any get_cell_description(cell_gid_type) const override {
         cable_cell c;
         c.add_soma(20);
-        c.add_gap_junction({0,1});
+        c.place(mlocation{0,1}, gap_junction_site{});
         return {std::move(c)};
     }
 
@@ -257,12 +257,12 @@ TEST(fvm_lowered, target_handles) {
     EXPECT_EQ(cells[1].num_segments(), 4u);
 
     // (in increasing target order)
-    cells[0].add_synapse({1, 0.4}, "expsyn");
-    cells[0].add_synapse({0, 0.5}, "expsyn");
-    cells[1].add_synapse({2, 0.2}, "exp2syn");
-    cells[1].add_synapse({2, 0.8}, "expsyn");
+    cells[0].place(mlocation{1, 0.4}, "expsyn");
+    cells[0].place(mlocation{0, 0.5}, "expsyn");
+    cells[1].place(mlocation{2, 0.2}, "exp2syn");
+    cells[1].place(mlocation{2, 0.8}, "expsyn");
 
-    cells[1].add_detector({0, 0}, 3.3);
+    cells[1].place(mlocation{0, 0}, threshold_detector{3.3});
 
     std::vector<target_handle> targets;
     std::vector<fvm_index_type> cell_to_intdom;
@@ -321,8 +321,8 @@ TEST(fvm_lowered, stimulus) {
     std::vector<cable_cell> cells;
     cells.push_back(make_cell_ball_and_stick(false));
 
-    cells[0].add_stimulus({1,1},   {5., 80., 0.3});
-    cells[0].add_stimulus({0,0.5}, {1., 2.,  0.1});
+    cells[0].place(mlocation{1,1},   i_clamp{5., 80., 0.3});
+    cells[0].place(mlocation{0,0.5}, i_clamp{1., 2.,  0.1});
 
     const fvm_size_type soma_cv = 0u;
     const fvm_size_type tip_cv = 5u;
@@ -401,7 +401,7 @@ TEST(fvm_lowered, derived_mechs) {
     for (int i = 0; i<3; ++i) {
         cable_cell& c = cells[i];
         c.add_soma(6.0);
-        c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100);
+        c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
 
         c.segment(1)->set_compartments(4);
         for (auto& seg: c.segments()) {
@@ -612,7 +612,7 @@ TEST(fvm_lowered, point_ionic_current) {
     double soma_area_m2 = 4*math::pi<double>*r*r*1e-12; // [m²]
 
     // Event weight is translated by point_ica_current into a current contribution in nA.
-    c.add_synapse({0u, 0.5}, "point_ica_current");
+    c.place(mlocation{0u, 0.5}, "point_ica_current");
 
     cable1d_recipe rec(c);
     rec.catalogue() = make_unit_test_catalogue();
@@ -676,9 +676,9 @@ TEST(fvm_lowered, weighted_write_ion) {
     cable_cell c;
     c.add_soma(5);
 
-    c.add_cable(0, section_kind::dendrite, 0.5, 0.5, 100);
-    c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 200);
-    c.add_cable(1, section_kind::dendrite, 0.5, 0.5, 100);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
+    c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 200));
+    c.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.5, 0.5, 100));
 
     for (auto& s: c.segments()) s->set_compartments(1);
 
@@ -770,15 +770,15 @@ TEST(fvm_lowered, gj_coords_simple) {
     std::vector<cable_cell> cells;
     cable_cell c, d;
     c.add_soma(2.1);
-    c.add_cable(0, section_kind::dendrite, 0.3, 0.2, 10);
+    c.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.3, 0.2, 10));
     c.segment(1)->set_compartments(5);
-    c.add_gap_junction({1, 0.8});
+    c.place(mlocation{1, 0.8}, gap_junction_site{});
     cells.push_back(std::move(c));
 
     d.add_soma(2.4);
-    d.add_cable(0, section_kind::dendrite, 0.3, 0.2, 10);
+    d.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.3, 0.2, 10));
     d.segment(1)->set_compartments(2);
-    d.add_gap_junction({1, 1});
+    d.place(mlocation{1, 1}, gap_junction_site{});
     cells.push_back(std::move(d));
 
     fvm_discretization D = fvm_discretize(cells, neuron_parameter_defaults);
@@ -846,41 +846,41 @@ TEST(fvm_lowered, gj_coords_complex) {
 
     // Make 3 cells
     c0.add_soma(2.1);
-    c0.add_cable(0, section_kind::dendrite, 0.3, 0.2, 8);
+    c0.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.3, 0.2, 8));
     c0.segment(1)->set_compartments(4);
 
     c1.add_soma(1.4);
-    c1.add_cable(0, section_kind::dendrite, 0.3, 0.5, 12);
+    c1.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.3, 0.5, 12));
     c1.segment(1)->set_compartments(6);
-    c1.add_cable(1, section_kind::dendrite, 0.3, 0.2, 9);
+    c1.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.3, 0.2, 9));
     c1.segment(2)->set_compartments(3);
-    c1.add_cable(1, section_kind::dendrite, 0.2, 0.2, 15);
+    c1.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.2, 0.2, 15));
     c1.segment(3)->set_compartments(5);
 
     c2.add_soma(2.9);
-    c2.add_cable(0, section_kind::dendrite, 0.3, 0.5, 4);
+    c2.add_cable(0, make_segment<cable_segment>(section_kind::dendrite, 0.3, 0.5, 4));
     c2.segment(1)->set_compartments(2);
-    c2.add_cable(1, section_kind::dendrite, 0.4, 0.2, 6);
+    c2.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.4, 0.2, 6));
     c2.segment(2)->set_compartments(2);
-    c2.add_cable(1, section_kind::dendrite, 0.1, 0.2, 8);
+    c2.add_cable(1, make_segment<cable_segment>(section_kind::dendrite, 0.1, 0.2, 8));
     c2.segment(3)->set_compartments(2);
-    c2.add_cable(2, section_kind::dendrite, 0.2, 0.2, 4);
+    c2.add_cable(2, make_segment<cable_segment>(section_kind::dendrite, 0.2, 0.2, 4));
     c2.segment(4)->set_compartments(2);
-    c2.add_cable(2, section_kind::dendrite, 0.2, 0.2, 4);
+    c2.add_cable(2, make_segment<cable_segment>(section_kind::dendrite, 0.2, 0.2, 4));
     c2.segment(5)->set_compartments(2);
 
     // Add 5 gap junctions
-    c0.add_gap_junction({1, 1});
-    c0.add_gap_junction({1, 0.5});
+    c0.place(mlocation{1, 1}, gap_junction_site{});
+    c0.place(mlocation{1, 0.5}, gap_junction_site{});
 
-    c1.add_gap_junction({2, 1});
-    c1.add_gap_junction({1, 1});
-    c1.add_gap_junction({1, 0.45});
-    c1.add_gap_junction({1, 0.1});
+    c1.place(mlocation{2, 1}, gap_junction_site{});
+    c1.place(mlocation{1, 1}, gap_junction_site{});
+    c1.place(mlocation{1, 0.45}, gap_junction_site{});
+    c1.place(mlocation{1, 0.1}, gap_junction_site{});
 
-    c2.add_gap_junction({1, 0.5});
-    c2.add_gap_junction({4, 1});
-    c2.add_gap_junction({2, 1});
+    c2.place(mlocation{1, 0.5}, gap_junction_site{});
+    c2.place(mlocation{4, 1}, gap_junction_site{});
+    c2.place(mlocation{2, 1}, gap_junction_site{});
 
     cells.push_back(std::move(c0));
     cells.push_back(std::move(c1));
@@ -958,7 +958,7 @@ TEST(fvm_lowered, cell_group_gj) {
         cable_cell c;
         c.add_soma(2.1);
         if (i % 2 == 0) {
-            c.add_gap_junction({0, 1});
+            c.place(mlocation{0, 1}, gap_junction_site{});
         }
         if (i < 10) {
             cell_group0.push_back(std::move(c));
diff --git a/test/unit/test_mc_cell_group.cpp b/test/unit/test_mc_cell_group.cpp
index 5a453dd6d6056979e14c311e97222f05c26ca330..25415273f1d19c78a1fc0ec5710ff3eeb94f0c87 100644
--- a/test/unit/test_mc_cell_group.cpp
+++ b/test/unit/test_mc_cell_group.cpp
@@ -23,7 +23,7 @@ namespace {
     cable_cell make_cell() {
         auto c = make_cell_ball_and_stick();
 
-        c.add_detector({0, 0}, 0);
+        c.place(mlocation{0, 0}, threshold_detector{0});
         c.segment(1)->set_compartments(101);
 
         return c;
@@ -63,7 +63,7 @@ TEST(mc_cell_group, sources) {
     for (int i=0; i<20; ++i) {
         cells.push_back(make_cell());
         if (i==0 || i==3 || i==17) {
-            cells.back().add_detector({1, 0.3}, 2.3);
+            cells.back().place(mlocation{1, 0.3}, threshold_detector{2.3});
         }
 
         EXPECT_EQ(1u + (i==0 || i==3 || i==17), cells.back().detectors().size());
diff --git a/test/unit/test_mc_cell_group_gpu.cpp b/test/unit/test_mc_cell_group_gpu.cpp
index 58524dce64d37c09177d667897a043376ac9a62b..05b3d68193969ea63461e956fc3be763948b26cc 100644
--- a/test/unit/test_mc_cell_group_gpu.cpp
+++ b/test/unit/test_mc_cell_group_gpu.cpp
@@ -21,7 +21,7 @@ namespace {
     cable_cell make_cell() {
         auto c = make_cell_ball_and_stick();
 
-        c.add_detector({0, 0}, 0);
+        c.place(mlocation{0, 0}, threshold_detector{0});
         c.segment(1)->set_compartments(101);
 
         return c;
diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp
index 5afba4f3e89a1f040eaf2e6764a4d58b49192464..7ac92d3c395a403d72ae9ef15f76535b148ac4ad 100644
--- a/test/unit/test_probe.cpp
+++ b/test/unit/test_probe.cpp
@@ -24,7 +24,7 @@ TEST(probe, fvm_lowered_cell) {
     cable_cell bs = make_cell_ball_and_stick(false);
 
     i_clamp stim(0, 100, 0.3);
-    bs.add_stimulus({1, 1}, stim);
+    bs.place(mlocation{1, 1}, stim);
 
     cable1d_recipe rec(bs);
 
diff --git a/test/unit/test_synapses.cpp b/test/unit/test_synapses.cpp
index b7983f1ebadbb90924da1a461c3c1b52b2a68ee6..03695d9345eebe3cbc8cf267d0ee50810d6a7d0d 100644
--- a/test/unit/test_synapses.cpp
+++ b/test/unit/test_synapses.cpp
@@ -38,9 +38,9 @@ TEST(synapses, add_to_cell) {
     auto soma = cell.add_soma(12.6157/2.0);
     soma->add_mechanism("hh");
 
-    cell.add_synapse({0, 0.1}, "expsyn");
-    cell.add_synapse({1, 0.2}, "exp2syn");
-    cell.add_synapse({0, 0.3}, "expsyn");
+    cell.place(mlocation{0, 0.1}, "expsyn");
+    cell.place(mlocation{0, 0.2}, "exp2syn");
+    cell.place(mlocation{0, 0.3}, "expsyn");
 
     EXPECT_EQ(3u, cell.synapses().size());
     const auto& syns = cell.synapses();
@@ -49,13 +49,16 @@ TEST(synapses, add_to_cell) {
     EXPECT_EQ(syns[0].location.pos, 0.1);
     EXPECT_EQ(syns[0].mechanism.name(), "expsyn");
 
-    EXPECT_EQ(syns[1].location.branch, 1u);
+    EXPECT_EQ(syns[1].location.branch, 0u);
     EXPECT_EQ(syns[1].location.pos, 0.2);
     EXPECT_EQ(syns[1].mechanism.name(), "exp2syn");
 
     EXPECT_EQ(syns[2].location.branch, 0u);
     EXPECT_EQ(syns[2].location.pos, 0.3);
     EXPECT_EQ(syns[2].mechanism.name(), "expsyn");
+
+    // adding a synapse to an invalid branch location should throw.
+    EXPECT_THROW(cell.place(mlocation{1, 0.3}, "expsyn"), arb::cable_cell_error);
 }
 
 template <typename Seq>