diff --git a/arbor/arbexcept.cpp b/arbor/arbexcept.cpp
index 520883fe6dbcb5e32dacc41a97e093db07aa0517..00f0d8632a30509faaa701875097aaeaed537b24 100644
--- a/arbor/arbexcept.cpp
+++ b/arbor/arbexcept.cpp
@@ -44,9 +44,9 @@ zero_thread_requested_error::zero_thread_requested_error(unsigned nbt):
     nbt(nbt)
 {}
 
-bad_probe_id::bad_probe_id(cell_member_type probe_id):
-    arbor_exception(pprintf("bad probe id {}", probe_id)),
-    probe_id(probe_id)
+bad_probeset_id::bad_probeset_id(cell_member_type probeset_id):
+    arbor_exception(pprintf("bad probe id {}", probeset_id)),
+    probeset_id(probeset_id)
 {}
 
 gj_unsupported_lid_selection_policy::gj_unsupported_lid_selection_policy(cell_gid_type gid, cell_tag_type label):
diff --git a/arbor/benchmark_cell_group.cpp b/arbor/benchmark_cell_group.cpp
index 8abe81c2ba32c2ad4126ea110100179e12bfd498..1819edefed75fafd9b5b1666d6c41824e631450d 100644
--- a/arbor/benchmark_cell_group.cpp
+++ b/arbor/benchmark_cell_group.cpp
@@ -95,7 +95,7 @@ void benchmark_cell_group::clear_spikes() {
 }
 
 void benchmark_cell_group::add_sampler(sampler_association_handle h,
-                                   cell_member_predicate probe_ids,
+                                   cell_member_predicate probeset_ids,
                                    schedule sched,
                                    sampler_function fn,
                                    sampling_policy policy) {}
diff --git a/arbor/benchmark_cell_group.hpp b/arbor/benchmark_cell_group.hpp
index ea64cbcedd423a218a5da21aea80e485c197f8ee..c106836a16ed2c812330e829a93b7b5b003e83ee 100644
--- a/arbor/benchmark_cell_group.hpp
+++ b/arbor/benchmark_cell_group.hpp
@@ -28,7 +28,7 @@ public:
 
     void clear_spikes() override;
 
-    void add_sampler(sampler_association_handle h, cell_member_predicate probe_ids, schedule sched, sampler_function fn, sampling_policy policy) override;
+    void add_sampler(sampler_association_handle h, cell_member_predicate probeset_ids, schedule sched, sampler_function fn, sampling_policy policy) override;
 
     void remove_sampler(sampler_association_handle h) override {}
 
diff --git a/arbor/fvm_lowered_cell.hpp b/arbor/fvm_lowered_cell.hpp
index a3bff57d86d425d91ada0a000d26fc3d61f64a7d..0316907b59a5bbe096ce55812a19efd368053f04 100644
--- a/arbor/fvm_lowered_cell.hpp
+++ b/arbor/fvm_lowered_cell.hpp
@@ -188,9 +188,9 @@ struct probe_association_map {
         return data.size();
     }
 
-    // Return range of fvm_probe_data values associated with probe_id.
-    auto data_on(cell_member_type probe_id) const {
-        return util::transform_view(util::make_range(data.equal_range(probe_id)), util::second);
+    // Return range of fvm_probe_data values associated with probeset_id.
+    auto data_on(cell_member_type probeset_id) const {
+        return util::transform_view(util::make_range(data.equal_range(probeset_id)), util::second);
     }
 };
 
diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp
index 8621ef7eec14d455ba0ffed83f6ffb65db951121..accfcb15641ac2b30dc5c11274113b674a4f33aa 100644
--- a/arbor/fvm_lowered_cell_impl.hpp
+++ b/arbor/fvm_lowered_cell_impl.hpp
@@ -632,11 +632,11 @@ fvm_initialization_data fvm_lowered_cell_impl<Backend>::initialize(
                 D, mech_data, fvm_info.target_handles, mechptr_by_name);
 
             if (!probe_data.empty()) {
-                cell_member_type probe_id{gid, i};
-                fvm_info.probe_map.tag[probe_id] = pi.tag;
+                cell_member_type probeset_id{gid, i};
+                fvm_info.probe_map.tag[probeset_id] = pi.tag;
 
                 for (auto& data: probe_data) {
-                    fvm_info.probe_map.data.insert({probe_id, std::move(data)});
+                    fvm_info.probe_map.data.insert({probeset_id, std::move(data)});
                 }
             }
         }
diff --git a/arbor/include/arbor/arbexcept.hpp b/arbor/include/arbor/arbexcept.hpp
index 1a5679a1f042b42edbff5aa2255c4729f215b591..4acfb6a7bb2fead64499141517d61cc80d9f3be3 100644
--- a/arbor/include/arbor/arbexcept.hpp
+++ b/arbor/include/arbor/arbexcept.hpp
@@ -67,9 +67,9 @@ struct ARB_SYMBOL_VISIBLE bad_global_property: arbor_exception {
     cell_kind kind;
 };
 
-struct ARB_SYMBOL_VISIBLE bad_probe_id: arbor_exception {
-    explicit bad_probe_id(cell_member_type id);
-    cell_member_type probe_id;
+struct ARB_SYMBOL_VISIBLE bad_probeset_id: arbor_exception {
+    explicit bad_probeset_id(cell_member_type id);
+    cell_member_type probeset_id;
 };
 
 struct ARB_SYMBOL_VISIBLE gj_kind_mismatch: arbor_exception {
diff --git a/arbor/include/arbor/simulation.hpp b/arbor/include/arbor/simulation.hpp
index c2dd5311ec2ef6d71a0cb0bbb0bd1ed8232217bd..205c8f940f0edf775bf6afa646669021060e35a5 100644
--- a/arbor/include/arbor/simulation.hpp
+++ b/arbor/include/arbor/simulation.hpp
@@ -41,7 +41,7 @@ public:
     // Note: sampler functions may be invoked from a different thread than that
     // which called the `run` method.
 
-    sampler_association_handle add_sampler(cell_member_predicate probe_ids,
+    sampler_association_handle add_sampler(cell_member_predicate probeset_ids,
         schedule sched, sampler_function f, sampling_policy policy = sampling_policy::lax);
 
     void remove_sampler(sampler_association_handle);
@@ -50,7 +50,7 @@ public:
 
     // 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::vector<probe_metadata> get_probe_metadata(cell_member_type probeset_id) const;
 
     std::size_t num_spikes() const;
 
diff --git a/arbor/lif_cell_group.cpp b/arbor/lif_cell_group.cpp
index e7d24e955db339dda496f2e3df14b432d0760b73..0fa71789ec73ee47148e657e05ccc89dc4eda65a 100644
--- a/arbor/lif_cell_group.cpp
+++ b/arbor/lif_cell_group.cpp
@@ -59,7 +59,7 @@ void lif_cell_group::clear_spikes() {
 }
 
 // TODO: implement sampler
-void lif_cell_group::add_sampler(sampler_association_handle h, cell_member_predicate probe_ids,
+void lif_cell_group::add_sampler(sampler_association_handle h, cell_member_predicate probeset_ids,
                                     schedule sched, sampler_function fn, sampling_policy policy) {}
 void lif_cell_group::remove_sampler(sampler_association_handle h) {}
 void lif_cell_group::remove_all_samplers() {}
diff --git a/arbor/mc_cell_group.cpp b/arbor/mc_cell_group.cpp
index 4507afae383dc3deda8a847d881736e3bc3f311f..0510c60f9b2528e2ec979380a0ad1ce856b52257 100644
--- a/arbor/mc_cell_group.cpp
+++ b/arbor/mc_cell_group.cpp
@@ -94,7 +94,7 @@ void mc_cell_group::set_binning_policy(binning_kind policy, time_type bin_interv
 
 struct sampler_call_info {
     sampler_function sampler;
-    cell_member_type probe_id;
+    cell_member_type probeset_id;
     probe_tag tag;
     unsigned index;
     const fvm_probe_data* pdata_ptr;
@@ -151,7 +151,7 @@ void run_samples(
        sample_records.push_back(sample_record{time_type(raw_times[i]), &raw_samples[i]});
     }
 
-    sc.sampler({sc.probe_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
+    sc.sampler({sc.probeset_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -181,7 +181,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.get_metadata_ptr()}, n_sample, sample_records.data());
+    sc.sampler({sc.probeset_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -211,7 +211,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.get_metadata_ptr()}, n_sample, sample_records.data());
+    sc.sampler({sc.probeset_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -254,7 +254,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.get_metadata_ptr()}, n_sample, sample_records.data());
+    sc.sampler({sc.probeset_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -301,7 +301,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.get_metadata_ptr()}, n_sample, sample_records.data());
+    sc.sampler({sc.probeset_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 void run_samples(
@@ -375,7 +375,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.get_metadata_ptr()}, n_sample, sample_records.data());
+    sc.sampler({sc.probeset_id, sc.tag, sc.index, p.get_metadata_ptr()}, n_sample, sample_records.data());
 }
 
 // Generic run_samples dispatches on probe info variant type.
@@ -476,7 +476,7 @@ void mc_cell_group::advance(epoch ep, time_type dt, const event_lane_subrange& e
             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) {
+            for (cell_member_type pid: sa.probeset_ids) {
                 auto cell_index = gid_index_map_.at(pid.gid);
 
                 probe_tag tag = probe_map_.tag.at(pid);
@@ -555,13 +555,13 @@ 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,
+void mc_cell_group::add_sampler(sampler_association_handle h, cell_member_predicate probeset_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));
+        util::assign_from(util::filter(util::keys(probe_map_.tag), probeset_ids));
 
     if (!probeset.empty()) {
         auto result = sampler_map_.insert({h, sampler_association{std::move(sched), std::move(fn), std::move(probeset), policy}});
@@ -579,21 +579,21 @@ void mc_cell_group::remove_all_samplers() {
     sampler_map_.clear();
 }
 
-std::vector<probe_metadata> mc_cell_group::get_probe_metadata(cell_member_type probe_id) const {
+std::vector<probe_metadata> mc_cell_group::get_probe_metadata(cell_member_type probeset_id) const {
     // Probe associations are fixed after construction, so we do not need to grab the mutex.
 
-    std::optional<probe_tag> maybe_tag = util::value_by_key(probe_map_.tag, probe_id);
+    std::optional<probe_tag> maybe_tag = util::value_by_key(probe_map_.tag, probeset_id);
     if (!maybe_tag) {
         return {};
     }
 
-    auto data = probe_map_.data_on(probe_id);
+    auto data = probe_map_.data_on(probeset_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()});
+        result.push_back(probe_metadata{probeset_id, *maybe_tag, index++, item.get_metadata_ptr()});
     }
 
     return result;
diff --git a/arbor/mc_cell_group.hpp b/arbor/mc_cell_group.hpp
index 3640c8cf248a66e1e43f790665f1b11bd9ddbe77..1379dae2bdd1f031bf9361d2439562216cb0caaf 100644
--- a/arbor/mc_cell_group.hpp
+++ b/arbor/mc_cell_group.hpp
@@ -52,14 +52,14 @@ public:
         spikes_.clear();
     }
 
-    void add_sampler(sampler_association_handle h, cell_member_predicate probe_ids,
+    void add_sampler(sampler_association_handle h, cell_member_predicate probeset_ids,
                      schedule sched, sampler_function fn, sampling_policy policy) override;
 
     void remove_sampler(sampler_association_handle h) override;
 
     void remove_all_samplers() override;
 
-    std::vector<probe_metadata> get_probe_metadata(cell_member_type probe_id) const override;
+    std::vector<probe_metadata> get_probe_metadata(cell_member_type probeset_id) const override;
 
 private:
     // List of the gids of the cells in the group.
diff --git a/arbor/sampler_map.hpp b/arbor/sampler_map.hpp
index 495a676d4b3030586f4b4f8c01e651e0c726f2c6..e1781ee7ab2c9bdd8f4fe00526f6a3174cbfc4a8 100644
--- a/arbor/sampler_map.hpp
+++ b/arbor/sampler_map.hpp
@@ -18,7 +18,7 @@ namespace arb {
 struct sampler_association {
     schedule sched;
     sampler_function sampler;
-    std::vector<cell_member_type> probe_ids;
+    std::vector<cell_member_type> probeset_ids;
     sampling_policy policy;
 };
 
diff --git a/arbor/simulation.cpp b/arbor/simulation.cpp
index f604e19bc560e03f9e078e330f54bc29c8fd5577..562db162668b0a4f4a55e448058c4a73b9f5e501 100644
--- a/arbor/simulation.cpp
+++ b/arbor/simulation.cpp
@@ -96,7 +96,7 @@ public:
 
     time_type run(time_type tfinal, time_type dt);
 
-    sampler_association_handle add_sampler(cell_member_predicate probe_ids,
+    sampler_association_handle add_sampler(cell_member_predicate probeset_ids,
         schedule sched, sampler_function f, sampling_policy policy = sampling_policy::lax);
 
     void remove_sampler(sampler_association_handle);
@@ -449,7 +449,7 @@ time_type simulation_state::run(time_type tfinal, time_type dt) {
 }
 
 sampler_association_handle simulation_state::add_sampler(
-        cell_member_predicate probe_ids,
+        cell_member_predicate probeset_ids,
         schedule sched,
         sampler_function f,
         sampling_policy policy)
@@ -457,7 +457,7 @@ sampler_association_handle simulation_state::add_sampler(
     sampler_association_handle h = sassoc_handles_.acquire();
 
     foreach_group(
-        [&](cell_group_ptr& group) { group->add_sampler(h, probe_ids, sched, f, policy); });
+        [&](cell_group_ptr& group) { group->add_sampler(h, probeset_ids, sched, f, policy); });
 
     return h;
 }
@@ -476,9 +476,9 @@ 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);
+std::vector<probe_metadata> simulation_state::get_probe_metadata(cell_member_type probeset_id) const {
+    if (auto linfo = util::value_by_key(gid_to_local_, probeset_id.gid)) {
+        return cell_groups_.at(linfo->group_index)->get_probe_metadata(probeset_id);
     }
     else {
         return {};
@@ -528,12 +528,12 @@ time_type simulation::run(time_type tfinal, time_type dt) {
 }
 
 sampler_association_handle simulation::add_sampler(
-    cell_member_predicate probe_ids,
+    cell_member_predicate probeset_ids,
     schedule sched,
     sampler_function f,
     sampling_policy policy)
 {
-    return impl_->add_sampler(std::move(probe_ids), std::move(sched), std::move(f), policy);
+    return impl_->add_sampler(std::move(probeset_ids), std::move(sched), std::move(f), policy);
 }
 
 void simulation::remove_sampler(sampler_association_handle h) {
@@ -544,8 +544,8 @@ 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::vector<probe_metadata> simulation::get_probe_metadata(cell_member_type probeset_id) const {
+    return impl_->get_probe_metadata(probeset_id);
 }
 
 std::size_t simulation::num_spikes() const {
diff --git a/arbor/spike_source_cell_group.hpp b/arbor/spike_source_cell_group.hpp
index 4a54237e6c87b147e6988bbe3f1e7db94b0df34a..aefe4a0d50fccfa13f8927c5c8fa04bf67b03b40 100644
--- a/arbor/spike_source_cell_group.hpp
+++ b/arbor/spike_source_cell_group.hpp
@@ -31,7 +31,7 @@ public:
 
     void clear_spikes() override;
 
-    void add_sampler(sampler_association_handle h, cell_member_predicate probe_ids, schedule sched, sampler_function fn, sampling_policy policy) override;
+    void add_sampler(sampler_association_handle h, cell_member_predicate probeset_ids, schedule sched, sampler_function fn, sampling_policy policy) override;
 
     void remove_sampler(sampler_association_handle h) override {}
 
diff --git a/doc/concepts/probe_sample.rst b/doc/concepts/probe_sample.rst
index 2dd2632556660f4a673463f26aaf7fb369d0f001..6ea48b0d534bcca20411a24cfb9eb9e543a78278 100644
--- a/doc/concepts/probe_sample.rst
+++ b/doc/concepts/probe_sample.rst
@@ -3,8 +3,61 @@
 Cable cell probing and sampling
 ===============================
 
-.. TODO::
-    WIP!
+Definitions
+***********
+
+.. glossary::
+
+    probe
+        A measurement that can be performed on a cell. Each cell kind will have its own sorts of probe.
+        Cable cells (:py:attr:`arbor.cable_probe`) allow the monitoring of membrane voltage, total membrane
+        current, mechanism state, and a number of other quantities, measured either over the whole cell,
+        or at specific sites (see :ref:`pycablecell-probesample`).
+
+    vector probe
+        Certain probes work over a :term:`region` rather than a :term:`locset`. This means that, depending
+        settings such as :term:`cv policy`, data is sampled as a function of distance, yielding multiple data points.
+        Such probes are distinguished from regular probes as "vector probes".
+        
+    probeset
+        A set of probes. Probes are placed on locsets, and therefore may describe more than one probe.
+
+    probeset address
+        Probesets are located at a probeset address, and the collection of probeset addresses for a given cell is
+        provided by the :py:class:`recipe` object. One address may correspond to more than one probe:
+        as an example, a request for membrane voltage on a cable cell at sites specified by a locset
+        expression will generate one probe for each site in that locset expression.
+
+        See :ref:`pycablecell-probesample` for a list of objects that return a probeset address.
+
+    probeset id
+        A designator for a probeset as specified by a recipe. The *probeset id* is a
+        :py:class:`cell_member` referring to a specific cell by gid, and the index into the list of
+        probeset addresses returned by the recipe for that gid.
+
+    metadata
+        Each probe has associated metadata describing, for example, the location on a cell where the
+        measurement is being taken, or other such identifying information. Metadata for the probes
+        associated with a :term:`probeset id` can be retrieved from the simulation object, and is also provided
+        along with any recorded samples.
+
+    sampler
+    sample_recorder
+        A sampler is something that receives :term:`probeset` data. It amounts to setting a particular probeset to a
+        particular measuring schedule, and then having a :term:`handle` with which to access the recorded probeset data later on.
+
+    sample
+        A record of data corresponding to the value at a specific *probe* at a specific time.
+
+    handle
+        A handle for reaching sampling data associated to a sampler, which is associated to a probeset.
+        Setting a sampler (through :py:func:`simulation.sample`) returns handles. Sampled data can be retrieved
+        by passing the handle associated to a sampler (associated to a probeset) to :py:func:`simulation.samples`.
+
+    schedule
+        An object representing a series of monotonically increasing points in time, used for determining
+        sample times (see :ref:`pyrecipe`).
+
 
 API
 ---
diff --git a/doc/cpp/probe_sample.rst b/doc/cpp/probe_sample.rst
index 76e16bfac2599945a6f253363eb8fa86b36eb810..cb7b08cfbbc5a2dbf666c2e340e850416b987c89 100644
--- a/doc/cpp/probe_sample.rst
+++ b/doc/cpp/probe_sample.rst
@@ -8,11 +8,10 @@ Cable cell probing and sampling
 Cable cell probes
 -----------------
 
-Various properties of a a cable cell can be sampled via one of the cable cell
-specific probe address described below. They fall into two classes: scalar
+Various properties of a cable cell can be sampled. They fall into two classes: scalar
 probes are associated with a single real value, such as a membrane voltage
 or mechanism state value at a particular location; vector probes return
-multiple values corresponding to a quantity sampled over a whole cell.
+multiple values corresponding to a quantity sampled at a set of points on the cell.
 
 The sample data associated with a cable cell probe will either be a ``double``
 for scalar probes, or a ``cable_sample_range`` describing a half-open range
@@ -56,7 +55,7 @@ Mechanism state queries however will throw a ``cable_cell_error`` exception
 at simulation initialization if the requested state variable does not exist
 on the mechanism.
 
-Cable cell probe addresses that are described by a ``locset`` may generate more
+Cable cell probeset addresses that are described by a ``locset`` may generate more
 than one concrete probe: there will be one per location in the locset that is
 satisfiable. Sampler callback functions can distinguish between different
 probes with the same address and id by examining their index and/or
@@ -277,7 +276,7 @@ If the mechanism is not defined at a particular site, that site is ignored.
 
 *  Sample value: ``double``. State variable value.
 
-*  Metadata: ``mlocation``. Location as given in the probe address.
+*  Metadata: ``mlocation``. Location as given in the probeset address.
 
 
 .. code::
@@ -387,16 +386,17 @@ as a way of passing additional metadata about a probe to any sampler
 that polls it, with a view to samplers that handle multiple probes,
 possibly with different value types.
 
-Probe addresses are decoupled from the cell descriptions themselves —
+Probeset addresses are decoupled from the cell descriptions themselves —
 this allows a recipe implementation to construct probes independently
 of the cells themselves. It is the responsibility of a cell group implementation
-to parse the probe address objects wrapped in the ``any address`` field.
+to parse the probeset address objects wrapped in the ``any address`` field,
+thus the order of probes returned is important.
 
 The _k_th element of the vector returned by ``get_probes(gid)`` is
 identified with a probe-id: ``cell_member_type{gid, k}``.
 
-One probe address may describe more than one concrete probe, depending
-upon the interpretation of the probe address by the cell group. In this
+One probeset address may describe more than one concrete probe, depending
+upon the interpretation of the probeset address by the cell group. In this
 instance, each of the concrete probes will be associated with the
 same probe-id. Samplers can distinguish between different probes with
 the same id by their probe index (see below).
@@ -413,9 +413,9 @@ will be passed to a sampler function or function object:
     .. code-block:: cpp
 
             struct probe_metadata {
-                cell_member_type id; // probe id
+                cell_member_type id; // probeset id
                 probe_tag tag;       // probe tag associated with id
-                unsigned index;      // index of probe source within those supplied by probe id
+                unsigned index;      // index of probe source within those supplied by probeset id
                 util::any_ptr meta;  // probe-specific metadata
             };
 
@@ -425,7 +425,7 @@ will be passed to a sampler function or function object:
 where the parameters are respectively the probe metadata, the number of
 samples, and finally a pointer to the sequence of sample records.
 
-The ``probe_id``, identifies the probe by its probe-id (see above).
+The ``probeset_id``, identifies the probe by its probe-id (see above).
 
 The ``probe_tag`` in the metadata is the key given in the ``probe_info``
 returned by the recipe.
@@ -433,8 +433,8 @@ returned by the recipe.
 The ``index`` identifies which of the possibly multiple probes associated
 with the probe-id is the source of the samples.
 
-The ``any_ptr`` value iin the metadata points to const probe-specific metadata;
-the type of the metadata will depend upon the probe address specified in the
+The ``any_ptr`` value in the metadata points to const probe-specific metadata;
+the type of the metadata will depend upon the probeset address specified in the
 ``probe_info`` provided by the recipe.
 
 One ``sample_record`` struct contains one sample of the probe data at a
@@ -452,12 +452,12 @@ given simulation time point:
 The ``data`` field points to the sample data, wrapped in ``any_ptr`` for
 type-checked access. The exact representation will depend on the nature of
 the object that is being probed, but it should depend only on the cell type and
-probe address.
+probeset address.
 
 The data pointed to by ``data``, and the sample records themselves, are
 only guaranteed to be valid for the duration of the call to the sampler
 function. A simple sampler implementation for ``double`` data, assuming
-one probe per probe id, might be as follows:
+one probe per probeset id, might be as follows:
 
 .. container:: example-code
 
@@ -499,7 +499,7 @@ Polling rates, policies and sampler functions are set through the
             using cell_member_predicate = std::function<bool (cell_member_type)>;
 
             sampler_association_handle simulation::add_sampler(
-                cell_member_predicate probe_ids,
+                cell_member_predicate probeset_ids,
                 schedule sched,
                 sampler_function fn,
                 sampling_policy policy = sampling_policy::lax);
@@ -511,7 +511,7 @@ Polling rates, policies and sampler functions are set through the
 Multiple samplers can then be associated with the same probe locations.
 The handle returned is only used for managing the lifetime of the
 association. The ``cell_member_predicate`` parameter defines the
-set of probe ids in terms of a membership test.
+set of probeset ids in terms of a membership test.
 
 Two helper functions are provided for making ``cell_member_predicate`` objects:
 
@@ -519,10 +519,10 @@ Two helper functions are provided for making ``cell_member_predicate`` objects:
 
    .. code-block:: cpp
 
-           // Match all probe ids.
+           // Match all probeset ids.
            cell_member_predicate all_probes = [](cell_member_type pid) { return true; };
 
-           // Match just one probe id.
+           // Match just one probeset id.
            cell_member_predicate one_probe(cell_member_type pid) {
                return [pid](cell_member_type x) { return pid==x; };
            }
@@ -538,21 +538,21 @@ implemented in the future, but cell groups are in general not required
 to support any policy other than ``lax``.
 
 The simulation object will pass on the sampler setting request to the cell
-group that owns the given probe id. The ``cell_group`` interface will be
+group that owns the given probeset id. The ``cell_group`` interface will be
 correspondingly extended:
 
 .. container:: api-code
 
    .. code-block:: cpp
 
-           void cell_group::add_sampler(sampler_association_handle h, cell_member_predicate probe_ids, sample_schedule sched, sampler_function fn, sampling_policy policy);
+           void cell_group::add_sampler(sampler_association_handle h, cell_member_predicate probeset_ids, sample_schedule sched, sampler_function fn, sampling_policy policy);
 
            void cell_group::remove_sampler(sampler_association_handle);
 
            void cell_group::remove_all_samplers();
 
 Cell groups will invoke the corresponding sampler function directly, and
-may aggregate multiple samples with the same probe id in one call to the
+may aggregate multiple samples with the same probeset id in one call to the
 sampler. Calls to the sampler are synchronous, in the sense that
 processing of the cell group state does not proceed while the sampler
 function is being executed, but the times of the samples given to the
diff --git a/doc/cpp/simulation.rst b/doc/cpp/simulation.rst
index 929ed2768948ac15f219cabc93ecac06b0e943b6..9332e20056821c09b0f76668cda4d453b91b2a7c 100644
--- a/doc/cpp/simulation.rst
+++ b/doc/cpp/simulation.rst
@@ -105,7 +105,7 @@ Class documentation
     **I/O:**
 
     .. cpp:function:: sampler_association_handle add_sampler(\
-                        cell_member_predicate probe_ids,\
+                        cell_member_predicate probeset_ids,\
                         schedule sched,\
                         sampler_function f,\
                         sampling_policy policy = sampling_policy::lax)
diff --git a/doc/python/cell.rst b/doc/python/cell.rst
index 6ea77ad989268895b108d95f310e815faeaa4c68..b932bc823a6f5e9933ccf7fa584b2a117d07b9d6 100644
--- a/doc/python/cell.rst
+++ b/doc/python/cell.rst
@@ -97,6 +97,7 @@ The types defined below are used as identifiers for cells and members of cell-lo
            # Create the global label referring to the group of items labeled "syn0"
            # on cell 5
            global_label = arbor.cell_global_label(5, local_label)
+
 .. class:: cell_member
 
     .. function:: cell_member(gid, index)
@@ -109,9 +110,8 @@ The types defined below are used as identifiers for cells and members of cell-lo
         * be associated with a unique cell, identified by the member :attr:`gid`;
         * identify an item within a cell-local collection by the member :attr:`index`.
 
-        An example is uniquely identifying a probe description in the model.
-        Each probe description has a cell (with :attr:`gid`), and an :attr:`index` into
-        the set of probe descriptions on the cell.
+        An example is uniquely identifying a probeset on the model:
+        ``arbor.cell_member(12, 3)`` can be used to identify the probeset with :attr:`index` 3 on the cell with :attr:`gid` 12.
 
         Lexicographically ordered by :attr:`gid`, then :attr:`index`.
 
diff --git a/doc/python/probe_sample-diag.dia b/doc/python/probe_sample-diag.dia
new file mode 100644
index 0000000000000000000000000000000000000000..4274a7ebd3ce0814c9ed01fc2f220f27d02481fa
Binary files /dev/null and b/doc/python/probe_sample-diag.dia differ
diff --git a/doc/python/probe_sample-diag.svg b/doc/python/probe_sample-diag.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d6d9108fe4da311feae189cddd8c98a6d9aaf4eb
--- /dev/null
+++ b/doc/python/probe_sample-diag.svg
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
+<svg width="31cm" height="15cm" viewBox="317 116 606 297" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <g id="Achtergrond">
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 0.2; stroke: #000000" x="419.401" y="118" width="104.599" height="278" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.7" y="140.881">
+        <tspan x="471.7" y="140.881">probeset</tspan>
+        <tspan x="471.7" y="156.881"></tspan>
+        <tspan x="471.7" y="172.881"></tspan>
+        <tspan x="471.7" y="188.881"></tspan>
+        <tspan x="471.7" y="204.881"></tspan>
+        <tspan x="471.7" y="220.881"></tspan>
+        <tspan x="471.7" y="236.881"></tspan>
+        <tspan x="471.7" y="252.881"></tspan>
+        <tspan x="471.7" y="268.881"></tspan>
+        <tspan x="471.7" y="284.881"></tspan>
+        <tspan x="471.7" y="300.881"></tspan>
+        <tspan x="471.7" y="316.881"></tspan>
+        <tspan x="471.7" y="332.881"></tspan>
+        <tspan x="471.7" y="348.881"></tspan>
+        <tspan x="471.7" y="364.881"></tspan>
+        <tspan x="471.7" y="380.881"></tspan>
+      </text>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="395" y="369">
+      <tspan x="395" y="369"></tspan>
+    </text>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="318.4" y="184" width="65.65" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="351.225" y="206.881">
+        <tspan x="351.225" y="206.881">handle</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="435.926" y="184" width="71.25" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.551" y="206.881">
+        <tspan x="471.551" y="206.881">probe 1</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="435.926" y="247" width="71.25" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.551" y="269.881">
+        <tspan x="471.551" y="269.881">probe 2</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 20; stroke: #000000" x="435.926" y="311" width="75.25" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="473.551" y="333.881">
+        <tspan x="473.551" y="333.881">probe ...</tspan>
+      </text>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="384.05" y1="203" x2="426.19" y2="203"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="433.69,203 423.69,208 426.19,203 423.69,198 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="384.05" y1="203" x2="430.329" y2="267.586"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="434.698,273.682 424.809,268.466 430.329,267.586 432.937,262.641 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="384.05" y1="203" x2="432.537" y2="330.401"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="435.205,337.41 426.975,329.843 432.537,330.401 436.321,326.286 "/>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="547.924" y="184" width="86.15" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="590.999" y="206.881">
+        <tspan x="590.999" y="206.881">meta (str)</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="547.924" y="247" width="114.75" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="605.299" y="269.881">
+        <tspan x="605.299" y="269.881">data (ndarray)</tspan>
+      </text>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="507.176" y1="203" x2="538.188" y2="203"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="545.688,203 535.688,208 538.188,203 535.688,198 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="507.176" y1="203" x2="542.636" y2="257.825"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="546.71,264.122 537.08,258.441 542.636,257.825 545.477,253.01 "/>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="700.924" y="247" width="120.5" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="761.174" y="269.881">
+        <tspan x="761.174" y="269.881">[0]: time, value</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="700.924" y="311" width="120.5" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="761.174" y="333.881">
+        <tspan x="761.174" y="333.881">[1]: time, value</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 20; stroke: #000000" x="700.926" y="374.4" width="124.5" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="763.176" y="397.281">
+        <tspan x="763.176" y="397.281">[...]: time, value</tspan>
+      </text>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="761.174" y="330">
+      <tspan x="761.174" y="330"></tspan>
+    </text>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="662.674" y1="266" x2="691.264" y2="266"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="698.764,266 688.764,271 691.264,266 688.764,261 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="662.674" y1="266" x2="696.498" y2="330.867"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="699.966,337.517 690.909,330.962 696.498,330.867 699.776,326.339 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="662.674" y1="266" x2="698.195" y2="384.077"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="700.356,391.259 692.687,383.123 698.195,384.077 702.263,380.242 "/>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="700.824" y="184" width="220.9" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="811.274" y="206.881">
+        <tspan x="811.274" y="206.881">location expression for probe 1</tspan>
+      </text>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="811.274" y="203">
+      <tspan x="811.274" y="203"></tspan>
+    </text>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="634.074" y1="203" x2="691.088" y2="203"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="698.588,203 688.588,208 691.088,203 688.588,198 "/>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="811.274" y="203">
+      <tspan x="811.274" y="203"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="473.55" y="330">
+      <tspan x="473.55" y="330"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="763.174" y="393.4">
+      <tspan x="763.174" y="393.4"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="473.55" y="330">
+      <tspan x="473.55" y="330"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="763.174" y="393.4">
+      <tspan x="763.174" y="393.4"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="811.274" y="203">
+      <tspan x="811.274" y="203"></tspan>
+    </text>
+    <text font-size="12.7998" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="351.225" y="203">
+      <tspan x="351.225" y="203"></tspan>
+    </text>
+    <text font-size="12.7998" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.7" y="257">
+      <tspan x="471.7" y="257"></tspan>
+    </text>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #000000" x1="419.401" y1="118" x2="388.647" y2="175.418"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="385.106,182.029 385.42,170.853 388.647,175.418 394.235,175.575 "/>
+    </g>
+  </g>
+</svg>
diff --git a/doc/python/probe_sample.rst b/doc/python/probe_sample.rst
index 41374bd15b92ebc87b3ef17d31b85c9a00859bce..3ac24a0ccd58def042d59cdc3a705154efdb30f1 100644
--- a/doc/python/probe_sample.rst
+++ b/doc/python/probe_sample.rst
@@ -5,23 +5,144 @@ Cable cell probing and sampling
 
 .. module:: arbor
 
-Cable cell probe addresses are defined analogously to their counterparts in
-the C++ API (see :ref:`cablecell-probes` for details). Sample data recorded
-by the Arbor simulation object is returned in the form of a NumPy array,
-with the first column holding sample times, and subsequent columns holding
-the corresponding scalar- or vector-valued sample.
-
-Location expressions will be realised as zero or more specific sites on
-a cell; probe addresses defined over location expressions will describe zero,
+.. figure:: probe_sample-diag.svg
+    :width: 800
+    :align: center
+
+    A schematic view of how :term:`handles <handle>` let you access sampled data measured at a :term:`probeset`.
+    A probeset is a probe placed on a locset (which may describe more than one point). 
+    When setting a probe on a locset a :term:`sampler` is created.
+    When this sampler is set to sampling (at a certain schedule), a handle is returned.
+    This figure demonstrates how sampling data can be accessed through the handle associated to the probeset.
+    See below for a possible result for ``data``.
+
+.. code-block:: python
+   
+   print(data) # The probe data printed, as found in single_cell_recipe.py
+   [[ 0.00000000e+00 -4.00000000e+01]
+    [ 1.00000000e-01 -5.40211646e+01]
+    [ 2.00000000e-01 -6.19670534e+01]
+    ...
+    [ 2.99000000e+01 -6.44564354e+01]]
+
+Sample data recorded by the Arbor simulation object is returned in the form 
+of a NumPy array, with the first column holding sample times, and subsequent 
+columns holding the corresponding scalar- or vector-valued sample.
+
+Probesets are defined over a location expression and will describe zero,
 one, or more probes, one per site. They are evaluated in the context of
 the cell on which the probe is attached.
 
+:term:`Vector probes <vector probe>` are a kind of probes that samples over a region, rather than a :term:`locset`.
+This means that they may output more than a single data point per timestamp. The layout of the outputs as returned
+by :func:`~arbor.simulation.samples` is slightly different, but contains the same sort of information as regular
+:term:`probesets <probeset>`.
+
+.. figure:: probe_sample_vector-diag.svg
+    :width: 800
+    :align: center
+
+    The structure of the data returned is slightly different when a :term:`vector probe` is sampled.
+    The same kind of information is included however. Instead of returning a list per :term:`probe` in a :term:`probeset`,
+    the data and metadata now have an extra dimension to cover for the multitude of subregions.
+
+
 Each of the functions described below generates an opaque :class:`probe`
 object for use in the recipe :py:func:`recipe.probes` method.
 
 More information on probes, probe metadata, and sampling can be found
 in the documentation for the class :class:`simulation`.
 
+.. note::
+
+   Cable cell probesets are defined analogously to their counterparts in
+   the C++ API (see :ref:`cablecell-probes` for details). Some details 
+   like `probe_tag` are not exposed in Python, as having Python probe callbacks
+   has proven to be too slow.
+
+Example
+-------
+
+
+.. code-block:: python
+   
+   import arbor
+
+   tree = arbor.segment_tree()
+   p = tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1)
+   tree.append(p, arbor.mpoint(3, 0, 0, 3), arbor.mpoint(-3, 0, 0, 3), tag=2)
+   tree.append(p, arbor.mpoint(3, 0, 0, 3), arbor.mpoint(-3, 0, 0, 3), tag=2)
+
+   decor = (
+      arbor.decor()
+      .set_property(Vm=-40)
+      .paint('"soma"', arbor.density("hh"))
+      .place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp"))
+
+   cell = arbor.cable_cell(tree, labels, decor)
+
+   class single_recipe(arbor.recipe):
+      def __init__(self):
+         arbor.recipe.__init__(self)
+
+      def num_cells(self):
+         return 1
+
+      def cell_kind(self, gid):
+         return arbor.cell_kind.cable
+
+      def cell_description(self, gid):
+         return cell
+
+      def probes(self, gid):
+         return [arbor.cable_probe_membrane_voltage('(location 0 0.5)'),
+                  arbor.cable_probe_membrane_voltage_cell(),
+                  arbor.cable_probe_membrane_voltage('(join (location 0 0) (location 0 1))'),
+                  ]
+
+      # (4.6) Override the global_properties method
+      def global_properties(self, kind):
+         return arbor.neuron_cable_properties()
+
+   recipe = single_recipe()
+   sim = arbor.simulation(recipe)
+   handles = [sim.sample((0, n), arbor.regular_schedule(0.1))
+            for n in range(3) ]
+   sim.run(tfinal=1)
+
+   for hd in handles:
+      print("Handle", hd)
+      for d, m in sim.samples(hd):
+         print(" * Meta:", m)
+         print(" * Payload:", d.shape)
+
+This script, has a single (scalar) probe, a single vector probe, and a probeset involving two scalar probes.
+The script is complete and can be run with Arbor installed, and will output:
+
+.. code-block::
+
+   Handle 0
+   * Meta: (location 0 0.5)
+   * Payload: (10, 2)
+   Handle 1
+   * Meta: [(cable 0 0 1), (cable 0 1 1), (cable 1 0 0), (cable 2 0 0), (cable 1 0 1), (cable 2 0 1)]
+   * Payload: (10, 7)
+   Handle 2
+   * Meta: (location 0 0)
+   * Payload: (10, 2)
+   * Meta: (location 0 1)
+   * Payload: (10, 2)
+
+
+API
+---
+
+.. class:: probe
+
+    An opaque object that is the Python representation of :cpp:class:`probe_info`.
+    
+    See below for ways to create probes.
+
 Membrane voltage
    .. py:function:: cable_probe_membrane_voltage(where)
 
@@ -37,6 +158,8 @@ Membrane voltage
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
 Axial current
    .. py:function:: cable_probe_axial_current(where)
 
@@ -60,6 +183,8 @@ Ionic current
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
 Total ionic current
    .. py:function:: cable_probe_total_ion_current_density(where)
 
@@ -75,6 +200,8 @@ Total ionic current
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
 Total transmembrane current
    .. py:function:: cable_probe_total_current_cell()
 
@@ -83,6 +210,8 @@ Total transmembrane current
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
 Total stimulus current
    .. py:function:: cable_probe_stimulus_current_cell()
 
@@ -90,6 +219,8 @@ Total stimulus current
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
 Density mechanism state variable
    .. py:function:: cable_probe_density_state(where, mechanism, state)
 
@@ -105,6 +236,8 @@ Density mechanism state variable
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
 Point process state variable
    .. py:function:: cable_probe_point_state(target, mechanism, state)
 
@@ -128,6 +261,8 @@ Point process state variable
    Metadata: a list of :class:`cable_point_probe_info` values, one for each matching
    target.
 
+   Kind: :term:`vector probe`.
+
 Ionic internal concentration
    .. py:function:: cable_probe_ion_int_concentration(where, ion)
 
@@ -143,6 +278,8 @@ Ionic internal concentration
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
 Ionic external concentration
    .. py:function:: cable_probe_ion_ext_concentration(where, ion)
 
@@ -158,3 +295,5 @@ Ionic external concentration
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+   Kind: :term:`vector probe`.
+
diff --git a/doc/python/probe_sample_vector-diag.dia b/doc/python/probe_sample_vector-diag.dia
new file mode 100644
index 0000000000000000000000000000000000000000..aa0c3b47dca4b07cf43a6bd04061ef9d25fc7288
Binary files /dev/null and b/doc/python/probe_sample_vector-diag.dia differ
diff --git a/doc/python/probe_sample_vector-diag.svg b/doc/python/probe_sample_vector-diag.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9e81bca85ebb0bad81a3ee62a6b0ab926438a7e7
--- /dev/null
+++ b/doc/python/probe_sample_vector-diag.svg
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
+<svg width="34cm" height="16cm" viewBox="317 87 676 308" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <g id="Achtergrond">
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 0.2; stroke: #000000" x="419.4" y="118" width="104.599" height="276.2" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.7" y="139.981">
+        <tspan x="471.7" y="139.981">probeset</tspan>
+        <tspan x="471.7" y="155.981"></tspan>
+        <tspan x="471.7" y="171.981"></tspan>
+        <tspan x="471.7" y="187.981"></tspan>
+        <tspan x="471.7" y="203.981"></tspan>
+        <tspan x="471.7" y="219.981"></tspan>
+        <tspan x="471.7" y="235.981"></tspan>
+        <tspan x="471.7" y="251.981"></tspan>
+        <tspan x="471.7" y="267.981"></tspan>
+        <tspan x="471.7" y="283.981"></tspan>
+        <tspan x="471.7" y="299.981"></tspan>
+        <tspan x="471.7" y="315.981"></tspan>
+        <tspan x="471.7" y="331.981"></tspan>
+        <tspan x="471.7" y="347.981"></tspan>
+        <tspan x="471.7" y="363.981"></tspan>
+        <tspan x="471.7" y="379.981"></tspan>
+      </text>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="395" y="369">
+      <tspan x="395" y="369"></tspan>
+    </text>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="318.4" y="184" width="65.65" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="351.225" y="206.881">
+        <tspan x="351.225" y="206.881">handle</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="432.6" y="184" width="77.9" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.55" y="206.881">
+        <tspan x="471.55" y="206.881">probes</tspan>
+      </text>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="384.05" y1="203" x2="422.864" y2="203"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="430.364,203 420.364,208 422.864,203 420.364,198 "/>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="548.55" y="184" width="104.9" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="601" y="206.881">
+        <tspan x="601" y="206.881">meta (list)</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="547.924" y="247" width="114.75" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="605.299" y="269.881">
+        <tspan x="605.299" y="269.881">data (ndarray)</tspan>
+      </text>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="510.5" y1="203" x2="538.814" y2="203"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="546.314,203 536.314,208 538.814,203 536.314,198 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="510.5" y1="203" x2="542.952" y2="257.629"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="546.782,264.078 537.376,258.034 542.952,257.629 545.974,252.926 "/>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="700.574" y="248" width="290.45" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="845.799" y="270.881">
+        <tspan x="845.799" y="270.881">[0]: time, value probe 1, value probe 2, ...</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="700.948" y="299.996" width="290.45" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="846.173" y="322.877">
+        <tspan x="846.173" y="322.877">[1]: time, value probe 1, value probe 2, ...</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 20; stroke: #000000" x="701.012" y="351.474" width="290.4" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="846.212" y="374.355">
+        <tspan x="846.212" y="374.355">[...]: time, value probe 1, value probe 2, ..</tspan>
+      </text>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="846.174" y="318.996">
+      <tspan x="846.174" y="318.996"></tspan>
+    </text>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="662.674" y1="266" x2="690.841" y2="266.743"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="698.339,266.941 688.21,271.676 690.841,266.743 688.474,261.679 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="662.674" y1="266" x2="695.863" y2="320.193"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="699.78,326.589 690.294,320.673 695.863,320.193 698.821,315.45 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="662.674" y1="266" x2="697.658" y2="361.334"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="700.242,368.375 692.103,360.709 697.658,361.334 701.491,357.264 "/>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="834.5" y="151.709">
+      <tspan x="834.5" y="151.709"></tspan>
+    </text>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="653.45" y1="203" x2="696.829" y2="115.803"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="700.17,109.088 700.193,120.268 696.829,115.803 691.239,115.814 "/>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="834.5" y="151.709">
+      <tspan x="834.5" y="151.709"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="473.55" y="330">
+      <tspan x="473.55" y="330"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="846.212" y="370.474">
+      <tspan x="846.212" y="370.474"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="473.55" y="330">
+      <tspan x="473.55" y="330"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="846.212" y="370.474">
+      <tspan x="846.212" y="370.474"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="834.5" y="151.709">
+      <tspan x="834.5" y="151.709"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="351.226" y="203">
+      <tspan x="351.226" y="203"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.7" y="256.1">
+      <tspan x="471.7" y="256.1"></tspan>
+    </text>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #000000" x1="419.4" y1="118" x2="388.647" y2="175.417"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="385.106,182.029 385.42,170.853 388.647,175.417 394.235,175.574 "/>
+    </g>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="471.55" y="203">
+      <tspan x="471.55" y="203"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="845.798" y="267">
+      <tspan x="845.798" y="267"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="846.212" y="370.474">
+      <tspan x="846.212" y="370.474"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="601" y="203">
+      <tspan x="601" y="203"></tspan>
+    </text>
+    <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="834.5" y="151.709">
+      <tspan x="834.5" y="151.709"></tspan>
+    </text>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="701.166" y="88.0856" width="288" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="845.166" y="110.967">
+        <tspan x="845.166" y="110.967">[0]: region expression probe 1</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="701.166" y="137.086" width="288.417" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="845.374" y="159.967">
+        <tspan x="845.374" y="159.967">[1]: region expression probe 2</tspan>
+      </text>
+    </g>
+    <g>
+      <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 20; stroke: #000000" x="701.124" y="184.286" width="288.458" height="38" rx="0" ry="0"/>
+      <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="845.353" y="207.167">
+        <tspan x="845.353" y="207.167">[...]: region expression probe ...</tspan>
+      </text>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="653.45" y1="203" x2="691.388" y2="203.228"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="698.888,203.273 688.858,208.213 691.388,203.228 688.918,198.213 "/>
+    </g>
+    <g>
+      <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="653.45" y1="203" x2="694.223" y2="162.911"/>
+      <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="699.572,157.653 695.946,168.23 694.223,162.911 688.935,161.099 "/>
+    </g>
+  </g>
+</svg>
diff --git a/doc/python/recipe.rst b/doc/python/recipe.rst
index d50ed3fdcb20257df6bd0ab7219a57961d9b5f7b..f3a4c5a11bfbb27fa6e9757f0ca4b45126d1c335 100644
--- a/doc/python/recipe.rst
+++ b/doc/python/recipe.rst
@@ -88,10 +88,10 @@ Recipe
 
     .. function:: probes(gid)
 
-        Returns a list specifying the probe addresses describing probes on the cell ``gid``.
-        Each address in the list is an opaque object of type :class:`probe` produced by
-        cell kind-specific probe address functions. Each probe address in the list
-        has a corresponding probe id of type :class:`cell_member`: an id ``(gid, i)``
+        Returns a list specifying the probesets describing probes on the cell ``gid``.
+        Each element in the list is an opaque object of type :class:`probe` produced by
+        cell kind-specific probeset functions. Each probeset in the list
+        has a corresponding probeset id of type :class:`cell_member`: an id ``(gid, i)``
         refers to the probes described by the ith entry in the list returned by ``get_probes(gid)``.
 
         By default returns an empty list.
diff --git a/doc/python/simulation.rst b/doc/python/simulation.rst
index 7d8b785107d223289aac71d24360f1ae2dd6bf7b..943291e38ab2d06b4cb92f0456f23a66254faaa8 100644
--- a/doc/python/simulation.rst
+++ b/doc/python/simulation.rst
@@ -114,25 +114,25 @@ over the local and distributed hardware resources (see :ref:`pydomdec`). Then, t
 
     **Sampling probes:**
 
-    .. function:: sample(probe_id, schedule, policy)
+    .. function:: sample(probeset_id, schedule, policy)
 
-        Set up a sampling schedule for the probes associated with the supplied probe_id of type :py:class:`cell_member`.
+        Set up a sampling schedule for the probes associated with the supplied probeset_id of type :py:class:`cell_member`.
         The schedule is any schedule object, as might be used with an event generator — see :ref:`pyrecipe` for details.
         The policy is of type :py:class:`sampling_policy`. It can be omitted, in which case the sampling will accord with the
         ``sampling_policy.lax`` policy.
 
-        The method returns a handle which can be used in turn to retrieve the sampled data from the simulator or to
+        The method returns a :term:`handle` which can be used in turn to retrieve the sampled data from the simulator or to
         remove the corresponding sampling process.
 
-    .. function:: probe_metadata(probe_id)
+    .. function:: probe_metadata(probeset_id)
 
-        Retrieve probe metadata for the probes associated with the given probe_id of type :py:class:`cell_member`.
+        Retrieve probe metadata for the probes associated with the given probeset_id of type :py:class:`cell_member`.
         The result will be a list, with one entry per probe; the specifics of each metadata entry will depend upon
         the kind of probe in question.
 
     .. function:: remove_sampler(handle)
 
-        Disable the sampling process referenced by the argument ``handle`` and remove any associated recorded data.
+        Disable the sampling process referenced by the argument :term:`handle` and remove any associated recorded data.
 
     .. function:: remove_all_samplers()
 
@@ -140,25 +140,28 @@ over the local and distributed hardware resources (see :ref:`pydomdec`). Then, t
 
     .. function:: samples(handle)
 
-        Retrieve a list of sample data associated with the given ``handle``.
-        There will be one entry in the list per probe associated with the :term:`probe id` used when the sampling was set up.
-        For example, if a probe was placed on a locset describing three positions, the returned list will contain three elements.
+        Retrieve a list of sample data associated with the given :term:`handle`.
+        There will be one entry in the list per probe associated with the :term:`probeset id` used when the sampling was set up. 
+        Each entry is a pair ``(samples, meta)`` where ``meta`` is the probe metadata as would be returned by
+        ``probe_metadata(probeset_id)``, and ``samples`` contains the recorded values.
+        
+        For example, if a probe was placed on a locset describing three positions, ``samples(handle)`` will
+        return a list of length `3`. In each element, each corresponding to a location in the locset,
+        you'll find a tuple containing ``metadata`` and ``data``, where ``metadata`` will be a string describing the location,
+        and ``data`` will (usually) be a ``numpy.ndarray``.
 
         An empty list will be returned if no output was recorded for the cell. For simulations
         that are distributed using MPI, handles associated with non-local cells will return an
         empty list.
         It is the responsibility of the caller to gather results over the ranks.
 
-        Each entry is a pair ``(samples, meta)`` where ``meta`` is the probe metadata as would be returned by
-        ``probe_metadata(probe_id)``, and ``samples`` contains the recorded values.
-
-        The format of the recorded values will depend upon the specifics of the probe, though generally it will
+        The format of the recorded values (``data``) will depend upon the specifics of the probe, though generally it will
         be a NumPy array, with the first column corresponding to sample time and subsequent columns holding
         the value or values that were sampled from that probe at that time.
 
     .. function:: progress_banner()
 
-        Print a progress bar during simulation, with elapsed miliseconds and percentage of simulation completed.
+        Print a progress bar during simulation, with elapsed milliseconds and percentage of simulation completed.
 
 **Types:**
 
@@ -263,44 +266,6 @@ Spikes recorded during a simulation are returned as a NumPy structured datatype
 Recording samples
 -----------------
 
-Definitions
-***********
-
-.. glossary::
-
-    probe
-        A measurement that can be performed on a cell. Each cell kind will have its own sorts of probe;
-        Cable cells (:py:attr:`arbor.cable_probe`) allow the monitoring of membrane voltage, total membrane
-        current, mechanism state, and a number of other quantities, measured either over the whole cell,
-        or at specific sites (see :ref:`pycablecell-probesample`).
-
-        Probes are described by probe addresses, and the collection of probe addresses for a given cell is
-        provided by the :py:class:`recipe` object. One address may correspond to more than one probe:
-        as an example, a request for membrane voltage on a cable cell at sites specified by a location
-        expression will generate one probe for each site in that location expression.
-
-    probe id
-        A designator for one or more probes as specified by a recipe. The *probe id* is a
-        :py:class:`cell_member` referring to a specific cell by gid, and the index into the list of
-        probe addresses returned by the recipe for that gid.
-
-    metadata
-        Each probe has associated metadata describing, for example, the location on a cell where the
-        measurement is being taken, or other such identifying information. Metadata for the probes
-        associated with a :term:`probe id` can be retrieved from the simulation object, and is also provided
-        along with any recorded samples.
-
-    sampler
-        A sampler is something that receives probe data. It amounts to setting a particular :term:`probe` to a
-        particular measuring schedule, and then having a handle with which to access the recorded probe data later on.
-
-    sample
-        A record of data corresponding to the value at a specific *probe* at a specific time.
-
-    schedule
-        An object representing a series of monotonically increasing points in time, used for determining
-        sample times (see :ref:`pyrecipe`).
-
 Procedure
 *********
 
@@ -309,23 +274,23 @@ There are three parts to the process of recording cell data over a simulation.
 1. Describing what to measure.
 
    The recipe object must provide a method :py:func:`recipe.probes` that returns a list of
-   probe addresses for the cell with a given ``gid``. The kth element of the list corresponds
-   to the :term:`probe id` ``(gid, k)``.
+   probeset addresses for the cell with a given ``gid``. The kth element of the list corresponds
+   to the :term:`probeset id` ``(gid, k)``.
 
-   Each probe address is an opaque object describing what to measure and where, and each cell kind
+   Each probeset address is an opaque object describing what to measure and where, and each cell kind
    will have its own set of functions for generating valid address specifications. Possible cable
    cell probes are described in the cable cell documentation: :ref:`pycablecell-probesample`.
 
 2. Instructing the simulator to record data.
 
    Recording is set up with the method :py:func:`simulation.sample`
-   as described above. It returns a handle that is used to retrieve the recorded data after
+   as described above. It returns a :term:`handle` that is used to retrieve the recorded data after
    simulation.
 
 3. Retrieve recorded data.
 
-   The method :py:func:`simulation.samples` takes a handle and returns the recorded data as a list,
-   with one entry for each probe associated with the :term:`probe id` that was used in step 2 above. Each
+   The method :py:func:`simulation.samples` takes a :term:`handle` and returns the recorded data as a list,
+   with one entry for each probe associated with the :term:`probeset id` that was used in step 2 above. Each
    entry will be a tuple ``(data, meta)`` where ``meta`` is the metadata associated with the
    probe, and ``data`` contains all the data sampled on that probe over the course of the
    simulation.
@@ -350,7 +315,7 @@ Example
 
     sim = arbor.simulation(recipe, decomp, context)
 
-    # Sample probe id (0, 0) (first probe id on cell 0) every 0.1 ms with exact sample timing:
+    # Sample probeset id (0, 0) (first probeset id on cell 0) every 0.1 ms with exact sample timing:
 
     handle = sim.sample((0, 0), arbor.regular_schedule(0.1), arbor.sampling_policy.exact)
 
diff --git a/doc/tutorial/network_ring.rst b/doc/tutorial/network_ring.rst
index 43c5d5a3af1bcf7e017fd3cf2cd27b910ed9b451..e7170c12526be70efe9b1b67b33a4101d90dd672 100644
--- a/doc/tutorial/network_ring.rst
+++ b/doc/tutorial/network_ring.rst
@@ -131,9 +131,9 @@ This means the timestamps of the generated events will be kept in memory. Be def
 In addition to having the timestamps of spikes, we want to extract the voltage as a function of time.
 
 Step **(14)** sets the probes (step **10**) to measure at a certain schedule. This is sometimes described as
-attaching a :term:`sampler` to a :term:`probe`. :py:func:`arbor.simulation.sample` expects a :term:`probe id` and the
-desired schedule (here: a recording frequency of 10 kHz, or a ``dt`` of 0.1 ms). Note that the probe id is a separate index from those of
-:term:`connection` endpoints; probe ids correspond to the index of the list produced by
+attaching a :term:`sampler` to a :term:`probe`. :py:func:`arbor.simulation.sample` expects a :term:`probeset id` and the
+desired schedule (here: a recording frequency of 10 kHz, or a ``dt`` of 0.1 ms). Note that the probeset id is a separate index from those of
+:term:`connection` endpoints; probeset ids correspond to the index of the list produced by
 :py:func:`arbor.recipe.probes` on cell ``gid``.
 
 :py:func:`arbor.simulation.sample` returns a handle to the :term:`samples <sample>` that will be recorded. We store
diff --git a/example/dryrun/dryrun.cpp b/example/dryrun/dryrun.cpp
index 6f38d968238449effb77280ab0c5b10297727918..2e00e204a149b1f990a297979d5fcb2d2c614fe6 100644
--- a/example/dryrun/dryrun.cpp
+++ b/example/dryrun/dryrun.cpp
@@ -178,13 +178,13 @@ int main(int argc, char** argv) {
         arb::simulation sim(recipe, ctx);
 
         // The id of the only probe on the cell: the cell_member type points to (cell 0, probe 0)
-        auto probe_id = cell_member_type{0, 0};
+        auto probeset_id = cell_member_type{0, 0};
         // The schedule for sampling is 10 samples every 1 ms.
         auto sched = arb::regular_schedule(1);
         // This is where the voltage samples will be stored as (time, value) pairs
         arb::trace_vector<double> voltage;
-        // Now attach the sampler at probe_id, with sampling schedule sched, writing to voltage
-        sim.add_sampler(arb::one_probe(probe_id), sched, arb::make_simple_sampler(voltage));
+        // Now attach the sampler at probeset_id, with sampling schedule sched, writing to voltage
+        sim.add_sampler(arb::one_probe(probeset_id), sched, arb::make_simple_sampler(voltage));
 
         // Set up recording of spikes to a vector on the root process.
         std::vector<arb::spike> recorded_spikes;
diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp
index 2887cb50c848bc94c803b21bab4987fbbbb071f4..5d5d8c128c847c52f357c53e08db6791b8f935b8 100644
--- a/example/gap_junctions/gap_junctions.cpp
+++ b/example/gap_junctions/gap_junctions.cpp
@@ -180,7 +180,7 @@ int main(int argc, char** argv) {
         // This is where the voltage samples will be stored as (time, value) pairs
         std::vector<arb::trace_vector<double>> voltage_traces(decomp.num_local_cells());
 
-        // Now attach the sampler at probe_id, with sampling schedule sched, writing to voltage
+        // Now attach the sampler at probeset_id, with sampling schedule sched, writing to voltage
         unsigned j=0;
         for (auto g : decomp.groups()) {
             for (auto i : g.gids) {
diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp
index 44c5f9c4fd122d9b4c4ce5432cfd19e3787eb1d0..c0b32d37a5b538fbdde18aef9cf4badd9ad26088 100644
--- a/example/generators/generators.cpp
+++ b/example/generators/generators.cpp
@@ -137,13 +137,13 @@ int main() {
     // Set up the probe that will measure voltage in the cell.
 
     // The id of the only probe on the cell: the cell_member type points to (cell 0, probe 0)
-    auto probe_id = cell_member_type{0, 0};
+    auto probeset_id = cell_member_type{0, 0};
     // The schedule for sampling is 10 samples every 1 ms.
     auto sched = arb::regular_schedule(0.1);
     // This is where the voltage samples will be stored as (time, value) pairs
     arb::trace_vector<double> voltage;
-    // Now attach the sampler at probe_id, with sampling schedule sched, writing to voltage
-    sim.add_sampler(arb::one_probe(probe_id), sched, arb::make_simple_sampler(voltage));
+    // Now attach the sampler at probeset_id, with sampling schedule sched, writing to voltage
+    sim.add_sampler(arb::one_probe(probeset_id), sched, arb::make_simple_sampler(voltage));
 
     // Run the simulation for 100 ms, with time steps of 0.01 ms.
     sim.run(100, 0.01);
diff --git a/example/generators/readme.md b/example/generators/readme.md
index 843d5664732c9c8262ead9d8ac252a110ac73832..fc73a3a49b5e0ede6debdfb6761c502e612faabc 100644
--- a/example/generators/readme.md
+++ b/example/generators/readme.md
@@ -165,19 +165,19 @@ We must attach a sampler to this probe to get sample values.
 The sampling interface is rich, and can be extended in many ways.
 For our simple use case there are three bits of information that need to be provided when creating a sampler
 
-1. The `probe_id` that identifies the probe (generated in the recipe).
+1. The `probeset_id` that identifies the probe (generated in the recipe).
 2. The schedule to use for sampling (in our case 10 samples every ms).
 3. The location where we want to save the samples for outputing later.
 
 ```C++
     // The id of the only probe (cell 0, probe 0)
-    auto probe_id = cell_member_type{0, 0};
+    auto probeset_id = cell_member_type{0, 0};
     // The schedule for sampling is 10 samples every 1 ms.
     auto sched = arb::regular_schedule(0.1);
     // Where the voltage samples will be stored as (time, value) pairs
     arb::trace_data<double> voltage;
     // Now attach the sampler:
-    sim.add_sampler(arb::one_probe(probe_id), sched, arb::make_simple_sampler(voltage));
+    sim.add_sampler(arb::one_probe(probeset_id), sched, arb::make_simple_sampler(voltage));
 ```
 
 When the simulation is run, the `simple_sampler` that we attached to the probe will store the sample values as (time, voltage) pairs in the `voltage` vector.
diff --git a/example/ring/ring.cpp b/example/ring/ring.cpp
index fa47d3f2159a818a69c1b0380792eac8f26dc439..72c4889e56eeab6bbf2e74999846cd93632a64e4 100644
--- a/example/ring/ring.cpp
+++ b/example/ring/ring.cpp
@@ -168,13 +168,13 @@ int main(int argc, char** argv) {
         // Set up the probe that will measure voltage in the cell.
 
         // The id of the only probe on the cell: the cell_member type points to (cell 0, probe 0)
-        auto probe_id = cell_member_type{0, 0};
+        auto probeset_id = cell_member_type{0, 0};
         // The schedule for sampling is 10 samples every 1 ms.
         auto sched = arb::regular_schedule(1);
         // This is where the voltage samples will be stored as (time, value) pairs
         arb::trace_vector<double> voltage;
-        // Now attach the sampler at probe_id, with sampling schedule sched, writing to voltage
-        sim.add_sampler(arb::one_probe(probe_id), sched, arb::make_simple_sampler(voltage));
+        // Now attach the sampler at probeset_id, with sampling schedule sched, writing to voltage
+        sim.add_sampler(arb::one_probe(probeset_id), sched, arb::make_simple_sampler(voltage));
 
         // Set up recording of spikes to a vector on the root process.
         std::vector<arb::spike> recorded_spikes;
diff --git a/python/example/probe_lfpykit.py b/python/example/probe_lfpykit.py
index 10e769345a871c2276a5ff2d8ecc975ade1c932c..8519b933f73e7e1c52a099ed57394ea6b47374b0 100644
--- a/python/example/probe_lfpykit.py
+++ b/python/example/probe_lfpykit.py
@@ -25,9 +25,9 @@ class Recipe(arbor.recipe):
 
         self.the_cell = cell
 
-        self.vprobe_id = (0, 0)
-        self.iprobe_id = (0, 1)
-        self.cprobe_id = (0, 2)
+        self.vprobeset_id = (0, 0)
+        self.iprobeset_id = (0, 1)
+        self.cprobeset_id = (0, 2)
 
         self.the_props = arbor.neuron_cable_properties()
 
@@ -125,9 +125,9 @@ sim = arbor.simulation(recipe)
 
 # set up sampling on probes with sampling every 1 ms
 schedule = arbor.regular_schedule(1.0)
-v_handle = sim.sample(recipe.vprobe_id, schedule, arbor.sampling_policy.exact)
-i_handle = sim.sample(recipe.iprobe_id, schedule, arbor.sampling_policy.exact)
-c_handle = sim.sample(recipe.cprobe_id, schedule, arbor.sampling_policy.exact)
+v_handle = sim.sample(recipe.vprobeset_id, schedule, arbor.sampling_policy.exact)
+i_handle = sim.sample(recipe.iprobeset_id, schedule, arbor.sampling_policy.exact)
+c_handle = sim.sample(recipe.cprobeset_id, schedule, arbor.sampling_policy.exact)
 
 # run simulation for 500 ms of simulated activity and collect results.
 sim.run(tfinal=500)
diff --git a/python/example/single_cell_detailed_recipe.py b/python/example/single_cell_detailed_recipe.py
index e13f8a85b38e12abe264203d7f66cfb1587bfd09..4443b9f3acf8c3c5bbb30eb79b423cdf5aab67ff 100644
--- a/python/example/single_cell_detailed_recipe.py
+++ b/python/example/single_cell_detailed_recipe.py
@@ -119,8 +119,8 @@ sim = arbor.simulation(recipe)
 # Instruct the simulation to record the spikes and sample the probe
 sim.record(arbor.spike_recording.all)
 
-probe_id = arbor.cell_member(0, 0)
-handle = sim.sample(probe_id, arbor.regular_schedule(0.02))
+probeset_id = arbor.cell_member(0, 0)
+handle = sim.sample(probeset_id, arbor.regular_schedule(0.02))
 
 # (7) Run the simulation
 sim.run(tfinal=100, dt=0.025)
diff --git a/python/example/single_cell_recipe.py b/python/example/single_cell_recipe.py
index 6e12fd546da5f99c28dee13a6dac92d8e5cf498d..315d0a088cb2b45d67dd8989cc19948d76d77e80 100644
--- a/python/example/single_cell_recipe.py
+++ b/python/example/single_cell_recipe.py
@@ -71,7 +71,7 @@ recipe = single_recipe()
 sim = arbor.simulation(recipe)
 
 # (7) Create and run simulation and set up 10 kHz (every 0.1 ms) sampling on the probe.
-# The probe is located on cell 0, and is the 0th probe on that cell, thus has probe_id (0, 0).
+# The probe is located on cell 0, and is the 0th probe on that cell, thus has probeset_id (0, 0).
 
 sim.record(arbor.spike_recording.all)
 handle = sim.sample((0, 0), arbor.regular_schedule(0.1))
diff --git a/python/simulation.cpp b/python/simulation.cpp
index 90689f843d4288c879d9e7124470fd0c8196eb3e..647522d0b37987183f98a0f862d79d80bb375ee9 100644
--- a/python/simulation.cpp
+++ b/python/simulation.cpp
@@ -126,18 +126,18 @@ public:
         return py::array_t<arb::spike>(py::ssize_t(spike_record_.size()), spike_record_.data());
     }
 
-    py::list get_probe_metadata(arb::cell_member_type probe_id) const {
+    py::list get_probe_metadata(arb::cell_member_type probeset_id) const {
         py::list result;
-        for (auto&& pm: sim_->get_probe_metadata(probe_id)) {
+        for (auto&& pm: sim_->get_probe_metadata(probeset_id)) {
              result.append(global_ptr_->probe_meta_converters.convert(pm.meta));
         }
         return result;
     }
 
-    arb::sampler_association_handle sample(arb::cell_member_type probe_id, const pyarb::schedule_shim_base& sched, arb::sampling_policy policy) {
+    arb::sampler_association_handle sample(arb::cell_member_type probeset_id, const pyarb::schedule_shim_base& sched, arb::sampling_policy policy) {
         std::shared_ptr<sample_recorder_vec> recorders{new sample_recorder_vec};
 
-        for (const arb::probe_metadata& pm: sim_->get_probe_metadata(probe_id)) {
+        for (const arb::probe_metadata& pm: sim_->get_probe_metadata(probeset_id)) {
             recorders->push_back(global_ptr_->recorder_factories.make_recorder(pm.meta));
         }
 
@@ -145,7 +145,7 @@ public:
         // is kept in sampler_map_; the two copies share the same recorder data.
 
         sampler_callback cb{std::move(recorders)};
-        auto sah = sim_->add_sampler(arb::one_probe(probe_id), sched.schedule(), cb, policy);
+        auto sah = sim_->add_sampler(arb::one_probe(probeset_id), sched.schedule(), cb, policy);
         sampler_map_.insert({sah, cb});
 
         return sah;
@@ -234,11 +234,11 @@ void register_simulation(pybind11::module& m, pyarb_global_ptr global_ptr) {
             "Retrieve recorded spikes as numpy array.")
         .def("probe_metadata", &simulation_shim::get_probe_metadata,
             "Retrieve metadata associated with given probe id.",
-            "probe_id"_a)
+            "probeset_id"_a)
         .def("sample", &simulation_shim::sample,
-            "Record data from probes with given probe_id according to supplied schedule.\n"
+            "Record data from probes with given probeset_id according to supplied schedule.\n"
             "Returns handle for retrieving data or removing the sampling.",
-            "probe_id"_a, "schedule"_a, "policy"_a = arb::sampling_policy::lax)
+            "probeset_id"_a, "schedule"_a, "policy"_a = arb::sampling_policy::lax)
         .def("samples", &simulation_shim::samples,
             "Retrieve sample data as a list, one element per probe associated with the query.",
             "handle"_a)
diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp
index ca541823faa3cca46e41489e8a04857c534cefca..7cf962832fb4b147794d1872fc78ea08088c4b9a 100644
--- a/test/unit/test_probe.cpp
+++ b/test/unit/test_probe.cpp
@@ -755,12 +755,12 @@ void run_partial_density_probe_test(const context& ctx) {
     cell_lid_type probe_lid = 0;
     for (auto tp: test_probes) {
         for (cell_gid_type gid: {0, 1}) {
-            cell_member_type probe_id{gid, probe_lid};
+            cell_member_type probeset_id{gid, probe_lid};
             if (std::isnan(tp.expected[gid])) {
-                EXPECT_EQ(0u, probe_map.data.count(probe_id));
+                EXPECT_EQ(0u, probe_map.data.count(probeset_id));
             }
             else {
-                probe_handle h = get_probe_raw_handle(probe_id);
+                probe_handle h = get_probe_raw_handle(probeset_id);
                 EXPECT_DOUBLE_EQ(tp.expected[gid], deref(h));
             }
         }