diff --git a/arbor/cell_group.hpp b/arbor/cell_group.hpp
index 4c73352c48810daf5ed32ab2b853a8dd073c2387..c624946b473ee24b45d4113556476ddb32804fb0 100644
--- a/arbor/cell_group.hpp
+++ b/arbor/cell_group.hpp
@@ -37,6 +37,13 @@ public:
     virtual void add_sampler(sampler_association_handle, cell_member_predicate, schedule, sampler_function, sampling_policy) = 0;
     virtual void remove_sampler(sampler_association_handle) = 0;
     virtual void remove_all_samplers() = 0;
+
+    // Probe metadata queries might also be called while a simulation is running, and so should
+    // also be thread-safe.
+
+    virtual std::vector<probe_metadata> get_probe_metadata(cell_member_type) const {
+        return {};
+    }
 };
 
 using cell_group_ptr = std::unique_ptr<cell_group>;
diff --git a/arbor/fvm_lowered_cell.hpp b/arbor/fvm_lowered_cell.hpp
index 83f934586ae4e7a6a11a16390d9fd7548be220ff..9a4f397219f172928d05e01f53410e694cde8e00 100644
--- a/arbor/fvm_lowered_cell.hpp
+++ b/arbor/fvm_lowered_cell.hpp
@@ -11,6 +11,7 @@
 #include <arbor/fvm_types.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/recipe.hpp>
+#include <arbor/util/any_ptr.hpp>
 #include <arbor/util/variant.hpp>
 
 #include "backends/event.hpp"
@@ -40,12 +41,18 @@ struct fvm_integration_result {
 struct fvm_probe_scalar {
     probe_handle raw_handles[1] = {nullptr};
     util::variant<mlocation, cable_probe_point_info> metadata;
+
+    util::any_ptr get_metadata_ptr() const {
+        return util::visit([](const auto& x) -> util::any_ptr { return &x; }, metadata);
+    }
 };
 
 struct fvm_probe_interpolated {
     probe_handle raw_handles[2] = {nullptr, nullptr};
     double coef[2];
     mlocation metadata;
+
+    util::any_ptr get_metadata_ptr() const { return &metadata; }
 };
 
 struct fvm_probe_multi {
@@ -56,6 +63,10 @@ struct fvm_probe_multi {
         raw_handles.shrink_to_fit();
         util::visit([](auto& v) { v.shrink_to_fit(); }, metadata);
     }
+
+    util::any_ptr get_metadata_ptr() const {
+        return util::visit([](const auto& x) -> util::any_ptr { return &x; }, metadata);
+    }
 };
 
 struct fvm_probe_weighted_multi {
@@ -68,6 +79,8 @@ struct fvm_probe_weighted_multi {
         weight.shrink_to_fit();
         metadata.shrink_to_fit();
     }
+
+    util::any_ptr get_metadata_ptr() const { return &metadata; }
 };
 
 // Trans-membrane currents require special handling!
@@ -88,11 +101,16 @@ struct fvm_probe_membrane_currents {
         weight.shrink_to_fit();
         cv_cables_divs.shrink_to_fit();
     }
+
+    util::any_ptr get_metadata_ptr() const { return &metadata; }
 };
 
 struct missing_probe_info {
     // dummy data...
     std::array<probe_handle, 0> raw_handles;
+    void* metadata = nullptr;
+
+    util::any_ptr get_metadata_ptr() const { return util::any_ptr{}; }
 };
 
 struct fvm_probe_data {
@@ -123,6 +141,10 @@ struct fvm_probe_data {
                 info));
     }
 
+    util::any_ptr get_metadata_ptr() const {
+        return util::visit([](const auto& i) -> util::any_ptr { return i.get_metadata_ptr(); }, info);
+    }
+
     sample_size_type n_raw() const { return raw_handle_range().size(); }
 
     explicit operator bool() const { return !util::get_if<missing_probe_info>(info); }
diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp
index 5d7d35e09512b0b13589f83f111e827ddc22dae1..170e1fbb15191c5c5c0594eca11556fe303768fd 100644
--- a/arbor/include/arbor/morph/locset.hpp
+++ b/arbor/include/arbor/morph/locset.hpp
@@ -159,6 +159,11 @@ locset uniform(region reg, unsigned left, unsigned right, uint64_t seed);
 // Proportional location on every branch.
 locset on_branches(double pos);
 
+// Proportional locations on each component:
+// For each component C of the region, find locations L
+// s.t. dist(h, L) = r * max {dist(h, t) | t is a distal point in C}.
+locset on_components(double relpos, region reg);
+
 // Support of a locset (x s.t. x in locset).
 locset support(locset);
 
diff --git a/arbor/include/arbor/morph/region.hpp b/arbor/include/arbor/morph/region.hpp
index 5e60d945a372d1763ba28feda44709ceb4d68871..ebdedff41db03c70244a1b7ef84fc323fcfa3e01 100644
--- a/arbor/include/arbor/morph/region.hpp
+++ b/arbor/include/arbor/morph/region.hpp
@@ -133,10 +133,10 @@ region branch(msize_t);
 // Region with all segments with segment tag id.
 region tagged(int id);
 
-// Region with all segments distal from another region
+// Region up to `distance` distal from points in `start`.
 region distal_interval(locset start, double distance);
 
-// Region with all segments proximal from another region
+// Region up to `distance` proximal from points in `start`.
 region proximal_interval(locset end, double distance);
 
 // Region with all segments with radius less than/less than or equal to r
diff --git a/arbor/include/arbor/simulation.hpp b/arbor/include/arbor/simulation.hpp
index 3dc177a4467455fb8d1c7ee2c7b2cad6afdda4e0..c26d022577f47837538686ff68953c79c8b3d74c 100644
--- a/arbor/include/arbor/simulation.hpp
+++ b/arbor/include/arbor/simulation.hpp
@@ -39,6 +39,10 @@ public:
 
     void remove_all_samplers();
 
+    // Return probe metadata, one entry per probe associated with supplied probe id,
+    // or an empty vector if no local match for probe id.
+    std::vector<probe_metadata> get_probe_metadata(cell_member_type probe_id) const;
+
     std::size_t num_spikes() const;
 
     // Set event binning policy on all our groups.
diff --git a/arbor/mc_cell_group.cpp b/arbor/mc_cell_group.cpp
index f5d85ff572bac32d9c3672726828d560b74ffe23..2b6950d2e0bbce24be3205e988d2ba22f1e9e1fb 100644
--- a/arbor/mc_cell_group.cpp
+++ b/arbor/mc_cell_group.cpp
@@ -62,8 +62,8 @@ void mc_cell_group::reset() {
     spikes_.clear();
 
     sample_events_.clear();
-    for (auto &assoc: sampler_map_) {
-        assoc.sched.reset();
+    for (auto &entry: sampler_map_) {
+        entry.second.sched.reset();
     }
 
     for (auto& b: binners_) {
@@ -133,10 +133,7 @@ void run_samples(
        sample_records.push_back(sample_record{time_type(raw_times[i]), &raw_samples[i]});
     }
 
-    // Metadata may be an mlocation or cell_lid_type (for point mechanism state probes).
-    util::visit(
-        [&](auto& metadata) { sc.sampler({sc.probe_id, sc.tag, sc.index, &metadata}, n_sample, sample_records.data()); },
-        p.metadata);
+    sc.sampler({sc.probe_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -166,7 +163,7 @@ void run_samples(
         sample_records.push_back(sample_record{time_type(raw_times[offset]), &ctmp[j]});
     }
 
-    sc.sampler({sc.probe_id, sc.tag, sc.index, &p.metadata}, n_sample, sample_records.data());
+    sc.sampler({sc.probe_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -196,10 +193,7 @@ void run_samples(
         sample_records.push_back(sample_record{time_type(raw_times[offset]), &csample_ranges[j]});
     }
 
-    // Metadata may be an mlocation_list or mcable_list.
-    util::visit(
-        [&](auto& metadata) { sc.sampler({sc.probe_id, sc.tag, sc.index, &metadata}, n_sample, sample_records.data()); },
-        p.metadata);
+    sc.sampler({sc.probe_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -242,8 +236,7 @@ void run_samples(
         sample_records.push_back(sample_record{time_type(raw_times[offset]), &csample_ranges[j]});
     }
 
-    // (Unlike fvm_probe_multi, we only have mcable_list metadata.)
-    sc.sampler({sc.probe_id, sc.tag, sc.index, &p.metadata}, n_sample, sample_records.data());
+    sc.sampler({sc.probe_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -305,7 +298,7 @@ void run_samples(
         sample_records.push_back(sample_record{time_type(raw_times[offset]), &csample_ranges[j]});
     }
 
-    sc.sampler({sc.probe_id, sc.tag, sc.index, &p.metadata}, n_sample, sample_records.data());
+    sc.sampler({sc.probe_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 // Generic run_samples dispatches on probe info variant type.
@@ -391,37 +384,44 @@ void mc_cell_group::advance(epoch ep, time_type dt, const event_lane_subrange& e
 
     std::vector<deliverable_event> exact_sampling_events;
 
-    for (auto& sa: sampler_map_) {
-        auto sample_times = util::make_range(sa.sched.events(tstart, ep.tfinal));
-        if (sample_times.empty()) {
-            continue;
-        }
-
-        sample_size_type n_times = sample_times.size();
-        max_samples_per_call = std::max(max_samples_per_call, n_times);
+    {
+        std::lock_guard<std::mutex> guard(sampler_mex_);
 
-        for (cell_member_type pid: sa.probe_ids) {
-            auto cell_index = gid_index_map_.at(pid.gid);
+        for (auto& sm_entry: sampler_map_) {
+            // Ignore sampler_association_handle, just need the association itself.
+            sampler_association& sa = sm_entry.second;
 
-            probe_tag tag = probe_map_.tag.at(pid);
-            unsigned index = 0;
-            for (const fvm_probe_data& pdata: probe_map_.data_on(pid)) {
-                call_info.push_back({sa.sampler, pid, tag, index++, &pdata, n_samples, n_samples + n_times*pdata.n_raw()});
-                auto intdom = cell_to_intdom_[cell_index];
+            auto sample_times = util::make_range(sa.sched.events(tstart, ep.tfinal));
+            if (sample_times.empty()) {
+                continue;
+            }
 
-                for (auto t: sample_times) {
-                    for (probe_handle h: pdata.raw_handle_range()) {
-                        sample_event ev{t, (cell_gid_type)intdom, {h, n_samples++}};
-                        sample_events.push_back(ev);
-                    }
-                    if (sa.policy==sampling_policy::exact) {
-                        target_handle h(-1, 0, intdom);
-                        exact_sampling_events.push_back({t, h, 0.f});
+            sample_size_type n_times = sample_times.size();
+            max_samples_per_call = std::max(max_samples_per_call, n_times);
+
+            for (cell_member_type pid: sa.probe_ids) {
+                auto cell_index = gid_index_map_.at(pid.gid);
+
+                probe_tag tag = probe_map_.tag.at(pid);
+                unsigned index = 0;
+                for (const fvm_probe_data& pdata: probe_map_.data_on(pid)) {
+                    call_info.push_back({sa.sampler, pid, tag, index++, &pdata, n_samples, n_samples + n_times*pdata.n_raw()});
+                    auto intdom = cell_to_intdom_[cell_index];
+
+                    for (auto t: sample_times) {
+                        for (probe_handle h: pdata.raw_handle_range()) {
+                            sample_event ev{t, (cell_gid_type)intdom, {h, n_samples++}};
+                            sample_events.push_back(ev);
+                        }
+                        if (sa.policy==sampling_policy::exact) {
+                            target_handle h(-1, 0, intdom);
+                            exact_sampling_events.push_back({t, h, 0.f});
+                        }
                     }
                 }
             }
+            arb_assert(n_samples==call_info.back().end_offset);
         }
-        arb_assert(n_samples==call_info.back().end_offset);
     }
 
     // Sort exact sampling events into staged events for delivery.
@@ -481,20 +481,45 @@ void mc_cell_group::advance(epoch ep, time_type dt, const event_lane_subrange& e
 void mc_cell_group::add_sampler(sampler_association_handle h, cell_member_predicate probe_ids,
                                 schedule sched, sampler_function fn, sampling_policy policy)
 {
+    std::lock_guard<std::mutex> guard(sampler_mex_);
+
     std::vector<cell_member_type> probeset =
         util::assign_from(util::filter(util::keys(probe_map_.tag), probe_ids));
 
     if (!probeset.empty()) {
-        sampler_map_.add(h, sampler_association{std::move(sched), std::move(fn), std::move(probeset), policy});
+        auto result = sampler_map_.insert({h, sampler_association{std::move(sched), std::move(fn), std::move(probeset), policy}});
+        arb_assert(result.second);
     }
 }
 
 void mc_cell_group::remove_sampler(sampler_association_handle h) {
-    sampler_map_.remove(h);
+    std::lock_guard<std::mutex> guard(sampler_mex_);
+    sampler_map_.erase(h);
 }
 
 void mc_cell_group::remove_all_samplers() {
+    std::lock_guard<std::mutex> guard(sampler_mex_);
     sampler_map_.clear();
 }
 
+std::vector<probe_metadata> mc_cell_group::get_probe_metadata(cell_member_type probe_id) const {
+    // Probe associations are fixed after construction, so we do not need to grab the mutex.
+
+    util::optional<probe_tag> maybe_tag = util::value_by_key(probe_map_.tag, probe_id);
+    if (!maybe_tag) {
+        return {};
+    }
+
+    auto data = probe_map_.data_on(probe_id);
+
+    std::vector<probe_metadata> result;
+    result.reserve(data.size());
+    unsigned index = 0;
+    for (const fvm_probe_data& item: data) {
+        result.push_back(probe_metadata{probe_id, *maybe_tag, index++, item.get_metadata_ptr()});
+    }
+
+    return result;
+}
+
 } // namespace arb
diff --git a/arbor/mc_cell_group.hpp b/arbor/mc_cell_group.hpp
index 8285a4f0648f1c5239b98cf12ae32e9f9df00821..e342dbf13633d981677f5a06e9c07b5ad3d22145 100644
--- a/arbor/mc_cell_group.hpp
+++ b/arbor/mc_cell_group.hpp
@@ -3,6 +3,7 @@
 #include <cstdint>
 #include <functional>
 #include <iterator>
+#include <mutex>
 #include <unordered_map>
 #include <vector>
 
@@ -57,6 +58,8 @@ public:
 
     void remove_all_samplers() override;
 
+    std::vector<probe_metadata> get_probe_metadata(cell_member_type probe_id) const override;
+
 private:
     // List of the gids of the cells in the group.
     std::vector<cell_gid_type> gids_;
@@ -94,6 +97,9 @@ private:
     // Collection of samplers to be run against probes in this group.
     sampler_association_map sampler_map_;
 
+    // Mutex for thread-safe access to sampler associations.
+    std::mutex sampler_mex_;
+
     // Lookup table for target ids -> local target handle indices.
     std::vector<std::size_t> target_handle_divisions_;
 };
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
index dca7c313a770e23c39b0e86dd6ae56ee57892f84..365c66a66c97f558a0069c2a6176cb3c4b1226bf 100644
--- a/arbor/morph/locset.cpp
+++ b/arbor/morph/locset.cpp
@@ -260,14 +260,13 @@ mlocation_list thingify_(const boundary_& n, const mprovider& p) {
     mlocation_list L;
 
     for (const mextent& comp: comps) {
-        mlocation_list proximal_set, distal_set;
+        arb_assert(!comp.empty());
+        arb_assert(thingify_(most_proximal_{region{comp}}, p).size()==1u);
 
-        for (const mcable& c: comp) {
-            proximal_set.push_back({c.branch, c.prox_pos});
-            distal_set.push_back({c.branch, c.dist_pos});
-        }
+        mlocation_list distal_set;
+        util::assign(distal_set, util::transform_view(comp, [](auto c) { return dist_loc(c); }));
 
-        L = sum(L, minset(p.morphology(), proximal_set));
+        L = sum(L, {prox_loc(comp.front())});
         L = sum(L, maxset(p.morphology(), distal_set));
     }
     return support(std::move(L));
@@ -297,13 +296,16 @@ mlocation_list thingify_(const cboundary_& n, const mprovider& p) {
     mlocation_list L;
 
     for (const mextent& comp: comps) {
-        mlocation_list proximal_set, distal_set;
-
         mextent ccomp = thingify(reg::complete(comp), p);
-        for (const mcable& c: ccomp.cables()) {
-            proximal_set.push_back({c.branch, c.prox_pos});
-            distal_set.push_back({c.branch, c.dist_pos});
-        }
+
+        // Note: if component contains the head of a top-level cable,
+        // the completion might not be connected (!).
+
+        mlocation_list proximal_set;
+        util::assign(proximal_set, util::transform_view(ccomp, [](auto c) { return prox_loc(c); }));
+
+        mlocation_list distal_set;
+        util::assign(distal_set, util::transform_view(ccomp, [](auto c) { return dist_loc(c); }));
 
         L = sum(L, minset(p.morphology(), proximal_set));
         L = sum(L, maxset(p.morphology(), distal_set));
@@ -315,6 +317,83 @@ std::ostream& operator<<(std::ostream& o, const cboundary_& x) {
     return o << "(cboundary " << x.reg << ")";
 }
 
+// Proportional on components of a region.
+
+struct on_components_: locset_tag {
+    explicit on_components_(double relpos, region reg):
+        relpos(relpos), reg(std::move(reg)) {}
+    double relpos;
+    region reg;
+};
+
+locset on_components(double relpos, region reg) {
+    return locset(on_components_(relpos, std::move(reg)));
+}
+
+mlocation_list thingify_(const on_components_& n, const mprovider& p) {
+    if (n.relpos<0 || n.relpos>1) {
+        return {};
+    }
+
+    std::vector<mextent> comps = components(p.morphology(), thingify(n.reg, p));
+    std::vector<mlocation> L;
+
+    for (const mextent& comp: comps) {
+        arb_assert(!comp.empty());
+        arb_assert(thingify_(most_proximal_{region{comp}}, p).size()==1u);
+
+        mlocation prox = prox_loc(comp.front());
+        auto d_from_prox = [&](mlocation x) { return p.embedding().integrate_length(prox, x); };
+
+        if (n.relpos==0) {
+            L.push_back(prox);
+        }
+        else if (n.relpos==1) {
+            double diameter = 0;
+            mlocation_list most_distal = {prox};
+
+            for (mcable c: comp) {
+                mlocation x = dist_loc(c);
+                double d = d_from_prox(x);
+
+                if (d>diameter) {
+                    most_distal = {x};
+                    diameter = d;
+                }
+                else if (d==diameter) {
+                    most_distal.push_back(x);
+                }
+            }
+
+            util::append(L, most_distal);
+        }
+        else {
+            double diameter = util::max_value(util::transform_view(comp,
+                [&](auto c) { return d_from_prox(dist_loc(c)); }));
+
+            double d = n.relpos*diameter;
+            for (mcable c: comp) {
+                double d0 = d_from_prox(prox_loc(c));
+                double d1 = d_from_prox(dist_loc(c));
+
+                if (d0<=d && d<=d1) {
+                    double s = d0==d1? 0: (d-d0)/(d1-d0);
+                    s = std::min(1.0, std::fma(s, c.dist_pos-c.prox_pos, c.prox_pos));
+                    L.push_back(mlocation{c.branch, s});
+                }
+            }
+
+        }
+    }
+
+    util::sort(L);
+    return L;
+}
+
+std::ostream& operator<<(std::ostream& o, const on_components_& x) {
+    return o << "(on_components " << x.relpos << " " << x.reg << ")";
+}
+
 // Uniform locset.
 
 struct uniform_ {
diff --git a/arbor/sampler_map.hpp b/arbor/sampler_map.hpp
index 50d0ff9b4a00df551752a65d2810a206e5fb2659..495a676d4b3030586f4b4f8c01e651e0c726f2c6 100644
--- a/arbor/sampler_map.hpp
+++ b/arbor/sampler_map.hpp
@@ -1,20 +1,15 @@
 #pragma once
 
-/*
- * Helper classes for managing sampler/schedule associations in
- * cell group classes (see sampling_api doc).
- */
+// Helper classes for managing sampler/schedule associations in
+// cell group classes (see sampling_api doc).
 
-#include <functional>
-#include <mutex>
 #include <unordered_map>
+#include <vector>
 
 #include <arbor/common_types.hpp>
 #include <arbor/sampling.hpp>
 #include <arbor/schedule.hpp>
 
-#include "util/transform.hpp"
-
 namespace arb {
 
 // An association between a samplers, schedule, and set of probe ids, as provided
@@ -27,38 +22,6 @@ struct sampler_association {
     sampling_policy policy;
 };
 
-// Maintain a set of associations paired with handles used for deletion.
-
-class sampler_association_map {
-public:
-    void add(sampler_association_handle h, sampler_association assoc) {
-        std::lock_guard<std::mutex> lock(m_);
-        map_.insert({h, std::move(assoc)});
-    }
-
-    void remove(sampler_association_handle h) {
-        std::lock_guard<std::mutex> lock(m_);
-        map_.erase(h);
-    }
-
-    void clear() {
-        std::lock_guard<std::mutex> lock(m_);
-        map_.clear();
-    }
-
-private:
-    using assoc_map = std::unordered_map<sampler_association_handle, sampler_association>;
-    assoc_map map_;
-    std::mutex m_;
-
-    static sampler_association& second(assoc_map::value_type& p) { return p.second; }
-    auto assoc_view() { return util::transform_view(map_, &sampler_association_map::second); }
-
-public:
-    // Range-like view presents just the associations, omitting the handles.
-
-    auto begin() { return assoc_view().begin(); }
-    auto end()   { return assoc_view().end(); }
-};
+using sampler_association_map = std::unordered_map<sampler_association_handle, sampler_association>;
 
 } // namespace arb
diff --git a/arbor/simulation.cpp b/arbor/simulation.cpp
index 7e32ec774cc7218f03c68849fd714d5e94debcf3..726e798ed516be5e8d9b6a5903fad0f6bb8031c6 100644
--- a/arbor/simulation.cpp
+++ b/arbor/simulation.cpp
@@ -65,6 +65,8 @@ public:
 
     void remove_all_samplers();
 
+    std::vector<probe_metadata> get_probe_metadata(cell_member_type) const;
+
     std::size_t num_spikes() const {
         return communicator_.num_spikes();
     }
@@ -98,7 +100,11 @@ private:
     std::unique_ptr<spike_double_buffer> local_spikes_;
 
     // Hash table for looking up the the local index of a cell with a given gid
-    std::unordered_map<cell_gid_type, cell_size_type> gid_to_local_;
+    struct gid_local_info {
+        cell_size_type cell_index;
+        cell_size_type group_index;
+    };
+    std::unordered_map<cell_gid_type, gid_local_info> gid_to_local_;
 
     communicator communicator_;
 
@@ -146,16 +152,18 @@ simulation_state::simulation_state(
     pending_events_.resize(num_local_cells);
 
     event_generators_.resize(num_local_cells);
-    cell_local_size_type lidx = 0;
+    cell_size_type lidx = 0;
+    cell_size_type grpidx = 0;
     for (const auto& group_info: decomp.groups) {
         for (auto gid: group_info.gids) {
             // Store mapping of gid to local cell index.
-            gid_to_local_[gid] = lidx;
+            gid_to_local_[gid] = gid_local_info{lidx, grpidx};
 
             // Set up the event generators for cell gid.
             event_generators_[lidx] = rec.event_generators(gid);
             ++lidx;
         }
+        ++grpidx;
     }
 
     // Generate the cell groups in parallel, with one task per cell group.
@@ -398,6 +406,15 @@ void simulation_state::remove_all_samplers() {
     sassoc_handles_.clear();
 }
 
+std::vector<probe_metadata> simulation_state::get_probe_metadata(cell_member_type probe_id) const {
+    if (auto linfo = util::value_by_key(gid_to_local_, probe_id.gid)) {
+        return cell_groups_.at(linfo->group_index)->get_probe_metadata(probe_id);
+    }
+    else {
+        return {};
+    }
+}
+
 void simulation_state::set_binning_policy(binning_kind policy, time_type bin_interval) {
     foreach_group(
         [&](cell_group_ptr& group) { group->set_binning_policy(policy, bin_interval); });
@@ -410,9 +427,9 @@ void simulation_state::inject_events(const pse_vector& events) {
         if (e.time<t_) {
             throw bad_event_time(e.time, t_);
         }
-        // gid_to_local_ maps gid to index into local set of cells.
+        // gid_to_local_ maps gid to index in local cells and of corresponding cell group.
         if (auto lidx = util::value_by_key(gid_to_local_, e.target.gid)) {
-            pending_events_[*lidx].push_back(e);
+            pending_events_[lidx->cell_index].push_back(e);
         }
     }
 }
@@ -452,6 +469,10 @@ void simulation::remove_all_samplers() {
     impl_->remove_all_samplers();
 }
 
+std::vector<probe_metadata> simulation::get_probe_metadata(cell_member_type probe_id) const {
+    return impl_->get_probe_metadata(probe_id);
+}
+
 std::size_t simulation::num_spikes() const {
     return impl_->num_spikes();
 }
diff --git a/example/lfp/lfp.cpp b/example/lfp/lfp.cpp
index 6a0066b641d4baf7f75d8f5ced9cc583b77143cd..38217129f7f8556d17846f4441634a8424ee46a3 100644
--- a/example/lfp/lfp.cpp
+++ b/example/lfp/lfp.cpp
@@ -18,6 +18,7 @@ using arb::util::any;
 using arb::util::any_cast;
 using arb::util::any_ptr;
 using arb::cell_gid_type;
+using arb::cell_member_type;
 
 // Recipe represents one cable cell with one synapse, together with probes for total trans-membrane current, membrane voltage,
 // ionic current density, and synaptic conductance. A sequence of spikes are presented to the one synapse on the cell.
@@ -70,12 +71,11 @@ private:
     void make_cell() {
         using namespace arb;
 
-        // Set up morphology as two branches:
         segment_tree tree;
-        // * soma, length 20 μm radius 10 μm, with SWC tag 1.
-        tree.append(arb::mnpos, {0, 0, 10, 10}, {0, 0, -10, 10}, 1);
-        // * apical dendrite, length 490 μm, radius 1 μm, with SWC tag 4.
-        tree.append(arb::mnpos, {0, 0, 10, 1},  {0, 0, 500, 1}, 4);
+        // Soma, length 20 μm radius 10 μm, with SWC tag 1.
+        auto soma_apex = tree.append(arb::mnpos, {0, 0, -10, 10}, {0, 0, 10, 10}, 1);
+        // Apical dendrite, length 490 μm, radius 1 μm, with SWC tag 4.
+        tree.append(soma_apex, {0, 0, 10, 1},  {0, 0, 500, 1}, 4);
 
         cell_ = cable_cell(tree);
 
@@ -90,8 +90,8 @@ private:
         cell_.paint(reg::tagged(1), "hh"); // (default parameters)
         cell_.paint(reg::tagged(4), mechanism_desc("pas").set("e", -70));
 
-        // Add exponential synapse at centre of soma (0.5 along branch 0).
-        synapse_location_ = mlocation{0, 0.5};
+        // Add exponential synapse at centre of soma.
+        synapse_location_ = ls::on_components(0.5, reg::tagged(1));
         cell_.place(synapse_location_, mechanism_desc("expsyn").set("e", 0).set("tau", 2));
     }
 };
@@ -99,17 +99,14 @@ private:
 struct position { double x, y, z; };
 
 struct lfp_sampler {
-    lfp_sampler(const arb::place_pwlin& p, std::vector<position> electrodes, double sigma):
-        placement(p), electrodes(std::move(electrodes)), sigma(sigma) {}
-
-    // Compute response coefficients for each electrode, given a set of cable-like sources.
-    void initialize(const arb::mcable_list& cables) {
+    lfp_sampler(const arb::place_pwlin& placement, const arb::mcable_list& cables, const std::vector<position>& electrodes, double sigma) {
+        // Compute response coefficients for each electrode, given a set of cable-like sources.
         const unsigned n_electrode = electrodes.size();
         response.assign(n_electrode, std::vector<double>(cables.size()));
 
         std::vector<arb::mpoint> midpoints;
         std::transform(cables.begin(), cables.end(), std::back_inserter(midpoints),
-            [this](const auto& c) { return placement.at({c.branch, 0.5*(c.prox_pos+c.dist_pos)}); });
+            [&placement](const auto& c) { return placement.at({c.branch, 0.5*(c.prox_pos+c.dist_pos)}); });
 
         const double coef = 1/(4*M_PI*sigma); // [Ω·m]
         for (unsigned i = 0; i<n_electrode; ++i) {
@@ -126,27 +123,9 @@ struct lfp_sampler {
         }
     }
 
-    void reset() {
-        response.clear();
-        lfp_time.clear();
-        lfp_voltage.clear();
-    }
-
-    bool is_initialized() const {
-        return !response.empty();
-    }
-
     // On receipt of a sequence of cell-wide current samples, apply response matrix and save results to lfp_voltage.
     arb::sampler_function callback() {
         return [this](arb::probe_metadata pm, std::size_t n, const arb::sample_record* samples) {
-            auto cables_ptr = any_cast<const arb::mcable_list*>(pm.meta);
-            assert(cables_ptr);
-
-            if (!is_initialized()) {
-                // The first time we get metadata, build the response matrix.
-                initialize(*cables_ptr);
-            }
-
             std::vector<double> currents;
             lfp_voltage.resize(response.size());
 
@@ -167,9 +146,6 @@ struct lfp_sampler {
     std::vector<std::vector<double>> lfp_voltage; // [mV] (one vector per electrode)
 
 private:
-    const arb::place_pwlin placement; // Represents cell morphology in space.
-    const std::vector<position> electrodes;    // [μm]
-    const double sigma;                        // [S/m]
     std::vector<std::vector<double>> response; // [MΩ]
 };
 
@@ -220,11 +196,16 @@ int main(int argc, char** argv) {
         {30, 0, 100}
     };
 
-    auto sample_schedule = arb::regular_schedule(sample_dt);
-
     arb::morphology cell_morphology = any_cast<arb::cable_cell>(R.get_cell_description(0)).morphology();
     arb::place_pwlin placed_cell(cell_morphology);
-    lfp_sampler lfp(placed_cell, electrodes, 3.0);
+
+    auto probe0_metadata = sim.get_probe_metadata(cell_member_type{0, 0});
+    assert(probe0_metadata.size()==1); // Should only be one probe associated with this id.
+    arb::mcable_list current_cables = *any_cast<const arb::mcable_list*>(probe0_metadata.at(0).meta);
+
+    lfp_sampler lfp(placed_cell, current_cables, electrodes, 3.0);
+
+    auto sample_schedule = arb::regular_schedule(sample_dt);
     sim.add_sampler(arb::one_probe({0, 0}), sample_schedule, lfp.callback(), arb::sampling_policy::exact);
 
     arb::trace_vector<double, arb::mlocation> membrane_voltage;
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index 1771eda6085b34217a4a19f9b3922286cf2a854e..5075872077fa91ff7ddbb80cf3571edae9b869f2 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -23,7 +23,6 @@
 #include "fvm_lowered_cell.hpp"
 #include "fvm_lowered_cell_impl.hpp"
 #include "mech_private_field_access.hpp"
-#include "sampler_map.hpp"
 #include "util/meta.hpp"
 #include "util/maputil.hpp"
 #include "util/rangeutil.hpp"
diff --git a/test/unit/test_kinetic_linear.cpp b/test/unit/test_kinetic_linear.cpp
index b9d5707f82cdad9eb14f07dc9fa404c70d9bb311..7ed540cc6f18616ff24215fe11d01031f0de5f96 100644
--- a/test/unit/test_kinetic_linear.cpp
+++ b/test/unit/test_kinetic_linear.cpp
@@ -13,7 +13,6 @@
 #include "mech_private_field_access.hpp"
 #include "fvm_lowered_cell.hpp"
 #include "fvm_lowered_cell_impl.hpp"
-#include "sampler_map.hpp"
 #include "simple_recipes.hpp"
 #include "unit_test_catalogue.hpp"
 
diff --git a/test/unit/test_morph_expr.cpp b/test/unit/test_morph_expr.cpp
index fd5ddbafa14f9b92eaeab240ee1bc39711bca54f..0b42a5417618492c63d79f7ea009216c6f9ed298 100644
--- a/test/unit/test_morph_expr.cpp
+++ b/test/unit/test_morph_expr.cpp
@@ -218,9 +218,24 @@ TEST(locset, thingify) {
         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);
+
+        auto oc_beg_b0 = ls::on_components(0, reg::branch(0));
+        auto oc_mid_b1 = ls::on_components(0.5, reg::branch(1));
+        auto oc_mid_b123 = ls::on_components(0.5, join(reg::branch(1), reg::branch(2), reg::branch(3)));
+        auto oc_end_b123 = ls::on_components(1, join(reg::branch(1), reg::branch(2), reg::branch(3)));
+        auto oc_end_b123h = ls::on_components(1, join(reg::branch(1), reg::branch(2), reg::cable(3, 0, 0.5)));
+        auto oc_mid_b02 = ls::on_components(0.5, join(reg::branch(0), reg::branch(2)));
+        auto oc_end_b02 = ls::on_components(1, join(reg::branch(0), reg::branch(2)));
+
+        EXPECT_EQ(thingify(oc_beg_b0, mp),  (ll{{0, 0}}));
+        EXPECT_EQ(thingify(oc_mid_b1, mp),  (ll{{1, 0.5}}));
+        EXPECT_EQ(thingify(oc_mid_b123, mp),  (ll{{2, 0.5}, {3, 0.25}}));
+        EXPECT_EQ(thingify(oc_end_b123, mp),  (ll{{3, 1}}));
+        EXPECT_EQ(thingify(oc_end_b123h, mp),  (ll{{2, 1}, {3, 0.5}}));
+        EXPECT_EQ(thingify(oc_mid_b02, mp),  (ll{{0, 0.5}, {2, 0.5}}));
+        EXPECT_EQ(thingify(oc_end_b02, mp),  (ll{{0, 1}, {2, 1}}));
+        EXPECT_EQ(thingify(ls::on_components(0.25, reg::cable(1, 0.5, 1)), mp),  (ll{{1, 0.625}}));
     }
     {
         auto mp = mprovider(morphology(sm));
diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp
index 0d6a48c2826a09d2a99cdb2858968925da8db4ec..7b363f2adbb6c81be5aa6fd6d1847d1b74da8723 100644
--- a/test/unit/test_probe.cpp
+++ b/test/unit/test_probe.cpp
@@ -13,6 +13,7 @@
 #include <arbor/schedule.hpp>
 #include <arbor/simple_sampler.hpp>
 #include <arbor/simulation.hpp>
+#include <arbor/util/any_ptr.hpp>
 #include <arbor/util/pp_util.hpp>
 #include <arbor/version.hpp>
 #include <arborenv/gpu_env.hpp>
@@ -1275,3 +1276,57 @@ TEST(probe, gpu_##x) { \
 ARB_PP_FOREACH(RUN_GPU, PROBE_TESTS)
 
 #endif // def ARB_GPU_ENABLED
+
+// Test simulator `get_probe_metadata` interface.
+// (No need to run this on GPU back-end as well.)
+
+TEST(probe, get_probe_metadata) {
+    // Reuse multiprobe test set-up to confirm simulator::get_probe_metadata returns
+    // correct vector of metadata.
+
+    cable_cell cell(common_morphology::m_mlt_b6);
+
+    // Paint mechanism on branches 1, 2, and 5, omitting branch 4.
+    cell.paint(reg::branch(1), mechanism_desc("param_as_state").set("p", 10.));
+    cell.paint(reg::branch(2), mechanism_desc("param_as_state").set("p", 20.));
+    cell.paint(reg::branch(5), mechanism_desc("param_as_state").set("p", 50.));
+
+    cable1d_recipe rec({cell}, false);
+    rec.catalogue() = make_unit_test_catalogue(global_default_catalogue());
+    rec.add_probe(0, 7, cable_probe_density_state{ls::terminal(), "param_as_state", "s"});
+
+    context ctx = make_context();
+    partition_hint_map phints = {
+       {cell_kind::cable, {partition_hint::max_size, partition_hint::max_size, true}}
+    };
+    simulation sim(rec, partition_load_balance(rec, ctx, phints), ctx);
+
+    std::vector<probe_metadata> mm = sim.get_probe_metadata({0, 0});
+    ASSERT_EQ(3u, mm.size());
+
+    EXPECT_EQ((cell_member_type{0, 0}), mm[0].id);
+    EXPECT_EQ((cell_member_type{0, 0}), mm[1].id);
+    EXPECT_EQ((cell_member_type{0, 0}), mm[2].id);
+
+    EXPECT_EQ(0u, mm[0].index);
+    EXPECT_EQ(1u, mm[1].index);
+    EXPECT_EQ(2u, mm[2].index);
+
+    EXPECT_EQ(7, mm[0].tag);
+    EXPECT_EQ(7, mm[1].tag);
+    EXPECT_EQ(7, mm[2].tag);
+
+    const mlocation* l0 = util::any_cast<const mlocation*>(mm[0].meta);
+    const mlocation* l1 = util::any_cast<const mlocation*>(mm[1].meta);
+    const mlocation* l2 = util::any_cast<const mlocation*>(mm[2].meta);
+
+    ASSERT_TRUE(l0);
+    ASSERT_TRUE(l1);
+    ASSERT_TRUE(l2);
+
+    std::vector<mlocation> locs = {*l0, *l1, *l2};
+    util::sort(locs);
+    EXPECT_EQ((mlocation{1, 1.}), locs[0]);
+    EXPECT_EQ((mlocation{2, 1.}), locs[1]);
+    EXPECT_EQ((mlocation{5, 1.}), locs[2]);
+}