diff --git a/external/modparser b/external/modparser
index b200bf6376a2dc30edea98fcc2375fc9be095135..ad8d926fd4a3e92cdcfb7b77c527550f859496d2 160000
--- a/external/modparser
+++ b/external/modparser
@@ -1 +1 @@
-Subproject commit b200bf6376a2dc30edea98fcc2375fc9be095135
+Subproject commit ad8d926fd4a3e92cdcfb7b77c527550f859496d2
diff --git a/external/vector b/external/vector
index c8678e80660cd63b293ade80ece8eed2249c1b06..6a9798d87f83246f36c999540d4a7810fb33d199 160000
--- a/external/vector
+++ b/external/vector
@@ -1 +1 @@
-Subproject commit c8678e80660cd63b293ade80ece8eed2249c1b06
+Subproject commit 6a9798d87f83246f36c999540d4a7810fb33d199
diff --git a/miniapp/io.cpp b/miniapp/io.cpp
index 344fd56135b232bef3563d6a3cc61b570868a360..348489ac6bb03491e3573d0c9d61b369325bba76 100644
--- a/miniapp/io.cpp
+++ b/miniapp/io.cpp
@@ -110,7 +110,7 @@ static void update_option(util::optional<T>& opt, const nlohmann::json& j, const
 }
 
 // Read options from (optional) json file and command line arguments.
-cl_options read_options(int argc, char** argv) {
+cl_options read_options(int argc, char** argv, bool allow_write) {
 
     // Default options:
     const cl_options defopts{
@@ -121,6 +121,8 @@ cl_options read_options(int argc, char** argv) {
         100.,       // tfinal
         0.025,      // dt
         false,      // all_to_all
+        false,      // ring
+        1,          // group_size
         false,      // probe_soma_only
         0.0,        // probe_ratio
         "trace_",   // trace_prefix
@@ -170,6 +172,11 @@ cl_options read_options(int argc, char** argv) {
             false, defopts.dt, "time", cmd);
         TCLAP::SwitchArg all_to_all_arg(
             "m","alltoall","all to all network", cmd, false);
+        TCLAP::SwitchArg ring_arg(
+            "r","ring","ring network", cmd, false);
+        TCLAP::ValueArg<uint32_t> group_size_arg(
+            "g", "group-size", "number of cells per cell group",
+            false, defopts.compartments_per_segment, "integer", cmd);
         TCLAP::ValueArg<double> probe_ratio_arg(
             "p", "probe-ratio", "proportion between 0 and 1 of cells to probe",
             false, defopts.probe_ratio, "proportion", cmd);
@@ -206,6 +213,8 @@ cl_options read_options(int argc, char** argv) {
                     update_option(options.dt, fopts, "dt");
                     update_option(options.tfinal, fopts, "tfinal");
                     update_option(options.all_to_all, fopts, "all_to_all");
+                    update_option(options.ring, fopts, "ring");
+                    update_option(options.group_size, fopts, "group_size");
                     update_option(options.probe_ratio, fopts, "probe_ratio");
                     update_option(options.probe_soma_only, fopts, "probe_soma_only");
                     update_option(options.trace_prefix, fopts, "trace_prefix");
@@ -239,12 +248,22 @@ cl_options read_options(int argc, char** argv) {
         update_option(options.tfinal, tfinal_arg);
         update_option(options.dt, dt_arg);
         update_option(options.all_to_all, all_to_all_arg);
+        update_option(options.ring, ring_arg);
+        update_option(options.group_size, group_size_arg);
         update_option(options.probe_ratio, probe_ratio_arg);
         update_option(options.probe_soma_only, probe_soma_only_arg);
         update_option(options.trace_prefix, trace_prefix_arg);
         update_option(options.trace_max_gid, trace_max_gid_arg);
         update_option(options.spike_file_output, spike_output_arg);
 
+        if (options.all_to_all && options.ring) {
+            throw usage_error("can specify at most one of --ring and --all-to-all");
+        }
+
+        if (options.group_size<1) {
+            throw usage_error("minimum of one cell per group");
+        }
+
         save_file = ofile_arg.getValue();
     }
     catch (TCLAP::ArgException& e) {
@@ -252,7 +271,7 @@ cl_options read_options(int argc, char** argv) {
     }
 
     // Save option values if requested.
-    if (save_file != "") {
+    if (save_file != "" && allow_write) {
         std::ofstream fid(save_file);
         if (fid) {
             try {
@@ -265,6 +284,8 @@ cl_options read_options(int argc, char** argv) {
                 fopts["dt"] = options.dt;
                 fopts["tfinal"] = options.tfinal;
                 fopts["all_to_all"] = options.all_to_all;
+                fopts["ring"] = options.ring;
+                fopts["group_size"] = options.group_size;
                 fopts["probe_ratio"] = options.probe_ratio;
                 fopts["probe_soma_only"] = options.probe_soma_only;
                 fopts["trace_prefix"] = options.trace_prefix;
@@ -297,6 +318,8 @@ std::ostream& operator<<(std::ostream& o, const cl_options& options) {
     o << "  simulation time      : " << options.tfinal << "\n";
     o << "  dt                   : " << options.dt << "\n";
     o << "  all to all network   : " << (options.all_to_all ? "yes" : "no") << "\n";
+    o << "  ring network         : " << (options.ring ? "yes" : "no") << "\n";
+    o << "  group size           : " << options.group_size << "\n";
     o << "  probe ratio          : " << options.probe_ratio << "\n";
     o << "  probe soma only      : " << (options.probe_soma_only ? "yes" : "no") << "\n";
     o << "  trace prefix         : " << options.trace_prefix << "\n";
diff --git a/miniapp/io.hpp b/miniapp/io.hpp
index c18e688c33ceff0c3966b5d66ae8eb1680bc48a1..ac769d436b6a36550afe2e64b33e0b35a69f3f80 100644
--- a/miniapp/io.hpp
+++ b/miniapp/io.hpp
@@ -21,6 +21,8 @@ struct cl_options {
     double tfinal;
     double dt;
     bool all_to_all;
+    bool ring;
+    uint32_t group_size;
     bool probe_soma_only;
     double probe_ratio;
     std::string trace_prefix;
@@ -49,7 +51,7 @@ public:
 
 std::ostream& operator<<(std::ostream& o, const cl_options& opt);
 
-cl_options read_options(int argc, char** argv);
+cl_options read_options(int argc, char** argv, bool allow_write = true);
 
 
 } // namespace io
diff --git a/miniapp/miniapp.cpp b/miniapp/miniapp.cpp
index bc4133cb15ceecc5c96b8b7649f286980acb232f..2b5df5d963305f82263d4cb3676d949989d90ea2 100644
--- a/miniapp/miniapp.cpp
+++ b/miniapp/miniapp.cpp
@@ -9,10 +9,9 @@
 
 #include <common_types.hpp>
 #include <cell.hpp>
-#include <cell_group.hpp>
 #include <communication/communicator.hpp>
 #include <communication/global_policy.hpp>
-#include <fvm_cell.hpp>
+#include <fvm_multicell.hpp>
 #include <io/exporter_spike_file.hpp>
 #include <mechanism_catalogue.hpp>
 #include <model.hpp>
@@ -29,7 +28,8 @@
 using namespace nest::mc;
 
 using global_policy = communication::global_policy;
-using lowered_cell = fvm::fvm_cell<double, cell_local_size_type>;
+using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>;
+//using lowered_cell = fvm::fvm_cell<double, cell_local_size_type>;
 using model_type = model<lowered_cell>;
 using time_type = model_type::time_type;
 using sample_trace_type = sample_trace<time_type, model_type::value_type>;
@@ -51,7 +51,7 @@ int main(int argc, char** argv) {
         banner();
 
         // read parameters
-        io::cl_options options = io::read_options(argc, argv);
+        io::cl_options options = io::read_options(argc, argv, global_policy::id()==0);
         std::cout << options << "\n";
         std::cout << "\n";
         std::cout << ":: simulation to " << options.tfinal << " ms in "
@@ -66,8 +66,16 @@ int main(int argc, char** argv) {
         auto recipe = make_recipe(options, pdist);
         auto cell_range = distribute_cells(recipe->num_cells());
 
+        std::vector<cell_gid_type> group_divisions;
+        for (auto i = cell_range.first; i<cell_range.second; i+=options.group_size) {
+            group_divisions.push_back(i);
+        }
+        group_divisions.push_back(cell_range.second);
+
+        EXPECTS(group_divisions.front() == cell_range.first);
+        EXPECTS(group_divisions.back() == cell_range.second);
 
-        model_type m(*recipe, cell_range.first, cell_range.second);
+        model_type m(*recipe, util::partition_view(group_divisions));
 
         auto register_exporter = [] (const io::cl_options& options) {
             return
@@ -183,6 +191,9 @@ std::unique_ptr<recipe> make_recipe(const io::cl_options& options, const probe_d
     if (options.all_to_all) {
         return make_basic_kgraph_recipe(options.cells, p, pdist);
     }
+    else if (options.ring) {
+        return make_basic_ring_recipe(options.cells, p, pdist);
+    }
     else {
         return make_basic_rgraph_recipe(options.cells, p, pdist);
     }
diff --git a/scripts/tsplot b/scripts/tsplot
index 81a5da78c06a24d71f727431ef237a18f2a13b0b..15977538a3ce5d977c7559e536d2e03af845e4e4 100755
--- a/scripts/tsplot
+++ b/scripts/tsplot
@@ -33,13 +33,19 @@ def parse_clargs():
     P.add_argument('inputs', metavar='FILE', nargs='+',
                    help='time series data in JSON format')
     P.add_argument('-t', '--trange', metavar='RANGE', dest='trange',
-                   type=parse_range_spec, 
+                   type=parse_range_spec,
                    help='restrict time axis to RANGE (see below)')
     P.add_argument('-g', '--group', metavar='KEY,...', dest='groupby',
-                   type=lambda s: s.split(','), 
+                   type=lambda s: s.split(','),
                    help='plot series with same KEYs on the same axes')
     P.add_argument('-o', '--output', metavar='FILE', dest='outfile',
                    help='save plot to file FILE')
+    P.add_argument('--dpi', metavar='NUM', dest='dpi',
+                   type=int,
+                   help='set dpi for output image')
+    P.add_argument('--scale', metavar='NUM', dest='scale',
+                   type=float,
+                   help='scale size of output image by NUM')
     P.add_argument('-x', '--exclude', metavar='NUM', dest='exclude',
                    type=float,
                    help='remove extreme points outside NUM times the 0.9-interquantile range of the median')
@@ -82,7 +88,7 @@ class TimeSeries:
         l_, lq, median, uq, u_ = np.percentile(yfinite, [0, 5.0, 50.0, 95.0, 100])
         lb = median - iqr_factor*(uq-lq)
         ub = median + iqr_factor*(uq-lq)
-        
+
         np_err_save = np.seterr(all='ignore')
         yex = np.ma.masked_where(np.isfinite(self.y)&(self.y<=ub)&(self.y>=lb), self.y)
         np.seterr(**np_err_save)
@@ -104,7 +110,7 @@ class TimeSeries:
 
     def units(self):
         return self.meta.get('units',"")
-        
+
     def trange(self):
         return (min(self.t), max(self.t))
 
@@ -155,7 +161,7 @@ def read_json_timeseries(source):
         ts_list.append(TimeSeries(times, jdata[key], **meta))
 
     return ts_list
-        
+
 def range_join(r, s):
     return (min(r[0], s[0]), max(r[1], s[1]))
 
@@ -236,7 +242,7 @@ def make_palette(cm_name, n, cmin=0, cmax=1):
                                M.cm.get_cmap(cm_name))
     return [smap.to_rgba((2*i+1)/float(2*n)) for i in xrange(n)]
 
-def plot_plots(plot_groups, save=None):
+def plot_plots(plot_groups, save=None, dpi=None, scale=None):
     nplots = len(plot_groups)
     plot_groups = sorted(plot_groups, key=lambda g: g.group_label())
 
@@ -268,7 +274,7 @@ def plot_plots(plot_groups, save=None):
                 lab = unit
 
             return lab
-            
+
         uniq_units = list(set([s.units() for s in group.series]))
         uniq_units.sort()
         if len(uniq_units)>2:
@@ -297,7 +303,7 @@ def plot_plots(plot_groups, save=None):
                 cm, n in zip(['hot', 'winter'],  [len(x) for x in series_by_unit])]
 
         lines = cycle(["-",(0,(3,1))])
-        
+
         first_plot = True
         for ui in xrange(len(uniq_units)):
             if not first_plot:
@@ -310,7 +316,7 @@ def plot_plots(plot_groups, save=None):
 
             plot.get_yaxis().get_major_formatter().set_useOffset(False)
             plot.get_yaxis().set_major_locator(M.ticker.MaxNLocator(nbins=6))
-            
+
             plot.set_xlim(trange)
 
             colours = cycle(palette[ui])
@@ -336,10 +342,14 @@ def plot_plots(plot_groups, save=None):
     axis_ymin = min([ax.get_position().ymin for ax in figure.axes])
     figure.text(0.5, axis_ymin - float(3)/figure.dpi, 'time', ha='center', va='center')
     if save:
-        figure.savefig(save)
+        if scale:
+            base = figure.get_size_inches()
+            figure.set_size_inches((base[0]*scale, base[1]*scale))
+
+        figure.savefig(save, dpi=dpi)
     else:
         P.show()
-        
+
 args = parse_clargs()
 tss = []
 for filename in args.inputs:
@@ -360,4 +370,4 @@ plots = gather_ts_plots(tss, groupby)
 if not args.outfile:
     M.interactive(False)
 
-plot_plots(plots, save=args.outfile)
+plot_plots(plots, save=args.outfile, dpi=args.dpi, scale=args.scale)
diff --git a/src/algorithms.hpp b/src/algorithms.hpp
index 37eeaf370acd54ef326f70cc4ae17e46f1c6f2b0..7be1406ac7581df6924c146e6b86aaa8201755bb 100644
--- a/src/algorithms.hpp
+++ b/src/algorithms.hpp
@@ -51,6 +51,17 @@ C make_index(C const& c)
     return out;
 }
 
+/// test for membership within half-open interval
+template <typename X, typename I, typename J>
+bool in_interval(const X& x, const I& lower, const J& upper) {
+    return x>=lower && x<upper;
+}
+
+template <typename X, typename I, typename J>
+bool in_interval(const X& x, const std::pair<I, J>& bounds) {
+    return x>=bounds.first && x<bounds.second;
+}
+
 /// works like std::is_sorted(), but with stronger condition that succesive
 /// elements must be greater than those before them
 template <typename C>
@@ -243,7 +254,7 @@ std::vector<typename C::value_type> make_parent_index(
         return {};
     }
 
-    EXPECTS(parent_index.size() == branch_index.back());
+    EXPECTS(parent_index.size()-branch_index.back() == 0);
     EXPECTS(has_contiguous_compartments(parent_index));
     EXPECTS(is_strictly_monotonic_increasing(branch_index));
 
diff --git a/src/cell.cpp b/src/cell.cpp
index e96cae69aca834d61d81c9c53c9488532a3b5b69..f247a2a8a83e327abce312ca31b31c5e560b8ce6 100644
--- a/src/cell.cpp
+++ b/src/cell.cpp
@@ -188,7 +188,7 @@ void cell::add_stimulus(segment_location loc, i_clamp stim)
             )
         );
     }
-    stimulii_.push_back({loc, std::move(stim)});
+    stimuli_.push_back({loc, std::move(stim)});
 }
 
 void cell::add_detector(segment_location loc, double threshold)
diff --git a/src/cell.hpp b/src/cell.hpp
index 746549289f1b718433388e92f5f1ca9d13d433c1..bd83b1b182238377cdd9e7642c60d0be31fe4892 100644
--- a/src/cell.hpp
+++ b/src/cell.hpp
@@ -129,18 +129,18 @@ public:
     compartment_model model() const;
 
     //////////////////
-    // stimulii
+    // stimuli
     //////////////////
     void add_stimulus(segment_location loc, i_clamp stim);
 
     std::vector<stimulus_instance>&
-    stimulii() {
-        return stimulii_;
+    stimuli() {
+        return stimuli_;
     }
 
     const std::vector<stimulus_instance>&
-    stimulii() const {
-        return stimulii_;
+    stimuli() const {
+        return stimuli_;
     }
 
     //////////////////
@@ -188,8 +188,8 @@ private:
     // the segments
     std::vector<segment_ptr> segments_;
 
-    // the stimulii
-    std::vector<stimulus_instance> stimulii_;
+    // the stimuli
+    std::vector<stimulus_instance> stimuli_;
 
     // the synapses
     std::vector<synapse_instance> synapses_;
diff --git a/src/cell_group.hpp b/src/cell_group.hpp
index 77276f49e52f12dc2b8205a7d24113d7409365f4..86a9059361110259d72a85389adf81dda55c8fdf 100644
--- a/src/cell_group.hpp
+++ b/src/cell_group.hpp
@@ -2,14 +2,18 @@
 
 #include <cstdint>
 #include <functional>
+#include <iterator>
 #include <vector>
 
+#include <algorithms.hpp>
 #include <cell.hpp>
 #include <common_types.hpp>
 #include <event_queue.hpp>
 #include <spike.hpp>
 #include <spike_source.hpp>
-#include <util/singleton.hpp>
+#include <util/debug.hpp>
+#include <util/partition.hpp>
+#include <util/range.hpp>
 
 #include <profiling/profiler.hpp>
 
@@ -36,28 +40,37 @@ public:
 
     cell_group() = default;
 
-    cell_group(cell_gid_type gid, const cell& c) :
-        gid_base_{gid}
+    template <typename Cells>
+    cell_group(cell_gid_type first_gid, const Cells& cells):
+        gid_base_{first_gid}
     {
-        detector_handles_.resize(c.detectors().size());
-        target_handles_.resize(c.synapses().size());
-        probe_handles_.resize(c.probes().size());
+        // Create lookup structure for probe and target ids.
+        build_handle_partitions(cells);
+        std::size_t n_probes = probe_handle_divisions_.back();
+        std::size_t n_targets = target_handle_divisions_.back();
+        std::size_t n_detectors =
+            algorithms::sum(util::transform_view(cells, [](const cell& c) { return c.detectors().size(); }));
 
-        cell_.initialize(util::singleton_view(c), detector_handles_, target_handles_, probe_handles_);
+        // Allocate space to store handles.
+        detector_handles_.resize(n_detectors);
+        target_handles_.resize(n_targets);
+        probe_handles_.resize(n_probes);
 
-        // Create spike detectors and associate them with globally unique source ids,
-        // as specified by cell gid and cell-local zero-based index.
+        cell_.initialize(cells, detector_handles_, target_handles_, probe_handles_);
 
+        // Create spike detectors and associate them with globally unique source ids.
         cell_gid_type source_gid = gid_base_;
-        cell_lid_type source_lid = 0u;
-
         unsigned i = 0;
-        for (auto& d : c.detectors()) {
-            cell_member_type source_id{source_gid, source_lid++};
-
-            spike_sources_.push_back({
-                source_id, spike_detector_type(cell_, detector_handles_[i],  d.threshold, 0.f)
-            });
+        for (const auto& cell: cells) {
+            cell_lid_type source_lid = 0u;
+            for (auto& d: cell.detectors()) {
+                cell_member_type source_id{source_gid, source_lid++};
+
+                spike_sources_.push_back({
+                    source_id, spike_detector_type(cell_, detector_handles_[i++],  d.threshold, 0.f)
+                });
+            }
+            ++source_gid;
         }
     }
 
@@ -65,7 +78,7 @@ public:
         clear_spikes();
         clear_events();
         reset_samplers();
-        //initialize_cells();
+        cell_.reset();
         for (auto& spike_source: spike_sources_) {
             spike_source.source.reset(cell_, 0.f);
         }
@@ -112,13 +125,13 @@ public:
 
             // apply events
             if (next) {
-                auto handle = target_handles_[next->target.index];
+                auto handle = get_target_handle(next->target);
                 cell_.deliver_event(handle, next->weight);
                 // apply events that are due within some epsilon of the current
                 // time step. This should be a parameter. e.g. with for variable
                 // order time stepping, use the minimum possible time step size.
                 while(auto e = events_.pop_if_before(cell_.time()+dt/10.)) {
-                    auto handle = target_handles_[e->target.index];
+                    auto handle = get_target_handle(e->target);
                     cell_.deliver_event(handle, e->weight);
                 }
             }
@@ -151,9 +164,10 @@ public:
     }
 
     void add_sampler(cell_member_type probe_id, sampler_function s, time_type start_time = 0) {
-        EXPECTS(probe_id.gid==gid_base_);
+        auto handle = get_probe_handle(probe_id);
+
         auto sampler_index = uint32_t(samplers_.size());
-        samplers_.push_back({probe_handles_[probe_id.index], s});
+        samplers_.push_back({handle, s});
         sampler_start_times_.push_back(start_time);
         sample_events_.push({sampler_index, start_time});
     }
@@ -173,7 +187,7 @@ public:
     }
 
     value_type probe(cell_member_type probe_id) const {
-        return cell_.probe(probe_handles_[probe_id.index]);
+        return cell_.probe(get_probe_handle(probe_id));
     }
 
 private:
@@ -216,6 +230,50 @@ private:
 
     /// collection of samplers to be run against probes in this group
     std::vector<sampler_entry> samplers_;
+
+    /// lookup table for probe ids -> local probe handle indices
+    std::vector<std::size_t> probe_handle_divisions_;
+
+    /// lookup table for target ids -> local target handle indices
+    std::vector<std::size_t> target_handle_divisions_;
+
+    /// build handle index lookup tables
+    template <typename Cells>
+    void build_handle_partitions(const Cells& cells) {
+        auto probe_counts = util::transform_view(cells, [](const cell& c) { return c.probes().size(); });
+        auto target_counts = util::transform_view(cells, [](const cell& c) { return c.synapses().size(); });
+
+        make_partition(probe_handle_divisions_, probe_counts);
+        make_partition(target_handle_divisions_, target_counts);
+    }
+
+    /// use handle partition to get index from id
+    template <typename Divisions>
+    std::size_t handle_partition_lookup(const Divisions& divisions, cell_member_type id) const {
+        // NB: without any assertion checking, this would just be:
+        // return divisions[id.gid-gid_base_]+id.index;
+
+        EXPECTS(id.gid>=gid_base_);
+
+        auto handle_partition = util::partition_view(divisions);
+        EXPECTS(id.gid-gid_base_<handle_partition.size());
+
+        auto ival = handle_partition[id.gid-gid_base_];
+        std::size_t i = ival.first + id.index;
+        EXPECTS(i<ival.second);
+
+        return i;
+    }
+
+    /// get probe handle from probe id
+    probe_handle get_probe_handle(cell_member_type probe_id) const {
+        return probe_handles_[handle_partition_lookup(probe_handle_divisions_, probe_id)];
+    }
+
+    /// get target handle from target id
+    target_handle get_target_handle(cell_member_type target_id) const {
+        return target_handles_[handle_partition_lookup(target_handle_divisions_, target_id)];
+    }
 };
 
 } // namespace mc
diff --git a/src/communication/communicator.hpp b/src/communication/communicator.hpp
index 13d4a54303da74aefbcab0bc4d0ecc3636d7f472..764020dcfc4accda25c7074469fa515f7f755ab4 100644
--- a/src/communication/communicator.hpp
+++ b/src/communication/communicator.hpp
@@ -13,6 +13,7 @@
 #include <spike.hpp>
 #include <util/debug.hpp>
 #include <util/double_buffer.hpp>
+#include <util/partition.hpp>
 
 namespace nest {
 namespace mc {
@@ -41,19 +42,18 @@ public:
     using event_queue =
         std::vector<postsynaptic_spike_event<time_type>>;
 
-    communicator() = default;
+    using gid_partition_type =
+        util::partition_range<std::vector<cell_gid_type>::const_iterator>;
 
-    // TODO
-    // for now, still assuming one-to-one association cells <-> groups,
-    // so that 'group' gids as represented by their first cell gid are
-    // contiguous.
-    communicator(id_type cell_from, id_type cell_to):
-        cell_gid_from_(cell_from), cell_gid_to_(cell_to)
+    communicator() {}
+
+    explicit communicator(gid_partition_type cell_gid_partition):
+        cell_gid_partition_(cell_gid_partition)
     {}
 
     cell_local_size_type num_groups_local() const
     {
-        return cell_gid_to_-cell_gid_from_;
+        return cell_gid_partition_.size();
     }
 
     void add_connection(connection_type con) {
@@ -63,7 +63,7 @@ public:
 
     /// returns true if the cell with gid is on the domain of the caller
     bool is_local_cell(id_type gid) const {
-        return gid>=cell_gid_from_ && gid<cell_gid_to_;
+        return algorithms::in_interval(gid, cell_gid_partition_.bounds());
     }
 
     /// builds the optimized data structure
@@ -135,9 +135,8 @@ public:
 
 private:
     std::size_t cell_group_index(cell_gid_type cell_gid) const {
-        // this will be more elaborate when there is more than one cell per cell group
-        EXPECTS(cell_gid>=cell_gid_from_ && cell_gid<cell_gid_to_);
-        return cell_gid-cell_gid_from_;
+        EXPECTS(is_local_cell(cell_gid));
+        return cell_gid_partition_.index(cell_gid);
     }
 
     std::vector<connection_type> connections_;
@@ -145,8 +144,8 @@ private:
     communication_policy_type communication_policy_;
 
     uint64_t num_spikes_ = 0u;
-    id_type cell_gid_from_;
-    id_type cell_gid_to_;
+
+    gid_partition_type cell_gid_partition_;
 };
 
 } // namespace communication
diff --git a/src/communication/mpi.hpp b/src/communication/mpi.hpp
index 9ebb0ca412d83dbb600471417beb299fd1a55a8b..472c64039ba8b4b74e96adf97cc6f63782a69f72 100644
--- a/src/communication/mpi.hpp
+++ b/src/communication/mpi.hpp
@@ -11,6 +11,7 @@
 
 #include <algorithms.hpp>
 #include <communication/gathered_vector.hpp>
+#include <util/debug.hpp>
 
 namespace nest {
 namespace mc {
@@ -97,7 +98,7 @@ namespace mpi {
     }
 
     template <typename T>
-    std::vector<T> gather_all(const std::vector<T> &values) {
+    std::vector<T> gather_all(const std::vector<T>& values) {
         static_assert(
             true,//std::is_trivially_copyable<T>::value,
             "gather_all can only be performed on trivally copyable types");
@@ -149,6 +150,10 @@ namespace mpi {
             MPI_COMM_WORLD
         );
 
+        for (auto& d : displs) {
+            d /= traits::count();
+        }
+
         return gathered_type(
             std::move(buffer),
             std::vector<count_type>(displs.begin(), displs.end())
diff --git a/src/compartment.hpp b/src/compartment.hpp
index da6bbcf57298593a60f7510822832060c7a89846..083155ffb7c5a3ea0d9d089740b6d7a3345ae234 100644
--- a/src/compartment.hpp
+++ b/src/compartment.hpp
@@ -3,7 +3,10 @@
 #include <iterator>
 #include <utility>
 
-#include "common_types.hpp"
+#include <common_types.hpp>
+#include <util/counter.hpp>
+#include <util/span.hpp>
+#include <util/transform.hpp>
 
 namespace nest {
 namespace mc {
@@ -34,132 +37,47 @@ struct compartment {
     value_type length;
 };
 
-/// The simplest type of compartment iterator :
-///     - divide a segment into n compartments of equal length
-///     - assume that the radius varies linearly from one end of the segment
-///       to the other
-class compartment_iterator :
-    public std::iterator<std::forward_iterator_tag, compartment>
-{
-
-    public:
-
-    using base = std::iterator<std::forward_iterator_tag, compartment>;
-    using size_type = base::value_type::size_type;
-    using real_type = base::value_type::value_type;
-
-    compartment_iterator() = delete;
-
-    compartment_iterator(
-        size_type idx,
-        real_type len,
-        real_type rad,
-        real_type delta_rad
-    )
-    :   index_(idx),
-        radius_(rad),
-        delta_radius_(delta_rad),
-        length_(len)
-    { }
-
-    compartment_iterator(compartment_iterator const& other) = default;
-
-    compartment_iterator& operator++()
-    {
-        index_++;
-        radius_ += delta_radius_;
-        return *this;
-    }
-
-    compartment_iterator operator++(int)
-    {
-        compartment_iterator ret(*this);
-        operator++();
-        return ret;
-    }
-
-    compartment operator*() const
-    {
-        return
-            compartment(
-                index_, length_, radius_, radius_ + delta_radius_
-            );
-    }
-
-    bool operator==(compartment_iterator const& other) const
-    {
-        return other.index_ == index_;
-    }
-
-    bool operator!=(compartment_iterator const& other) const
-    {
-        return !operator==(other);
-    }
-
-    private :
-
-    size_type index_;
-    real_type radius_;
-    const real_type delta_radius_;
-    const real_type length_;
-};
+// (NB: auto type deduction and lambda in C++14 will simplify the following)
 
-class compartment_range {
+template <typename size_type, typename real_type>
+class compartment_maker {
 public:
-    using size_type = compartment_iterator::size_type;
-    using real_type = compartment_iterator::real_type;
-
-    compartment_range(
-        size_type num_compartments,
-        real_type radius_L,
-        real_type radius_R,
-        real_type length
-    )
-    :   num_compartments_(num_compartments),
-        radius_L_(radius_L),
-        radius_R_(radius_R),
-        length_(length)
+    compartment_maker(size_type n, real_type length, real_type rL, real_type rR):
+        r0_{rL},
+        dr_{(rR-rL)/n},
+        dx_{length/n}
     {}
 
-    compartment_iterator begin() const
-    {
-        return {0, compartment_length(), radius_L_, delta_radius()};
-    }
-
-    compartment_iterator cbegin() const
-    {
-        return begin();
+    compartment operator()(size_type i) const {
+        return compartment(i, dx_, r0_+i*dr_, r0_+(i+1)*dr_);
     }
 
-    /// With 0-based indexing compartment number "num_compartments_" is
-    /// one past the end
-    compartment_iterator end() const
-    {
-        return {num_compartments_, 0, 0, 0};
-    }
+private:
+    real_type r0_;
+    real_type dr_;
+    real_type dx_;
+};
 
-    compartment_iterator cend() const
-    {
-        return end();
-    }
+template <typename size_type, typename real_type>
+using compartment_iterator =
+    util::transform_iterator<util::counter<size_type>, compartment_maker<size_type, real_type>>;
 
-    real_type delta_radius() const
-    {
-        return (radius_R_ - radius_L_) / num_compartments_;
-    }
+template <typename size_type, typename real_type>
+using compartment_range =
+    util::range<compartment_iterator<size_type, real_type>>;
 
-    real_type compartment_length() const
-    {
-        return length_ / num_compartments_;
-    }
 
-    private:
-
-    size_type num_compartments_;
-    real_type radius_L_;
-    real_type radius_R_;
-    real_type length_;
-};
+template <typename size_type, typename real_type>
+compartment_range<size_type, real_type> make_compartment_range(
+    size_type num_compartments,
+    real_type radius_L,
+    real_type radius_R,
+    real_type length)
+{
+    return util::transform_view(
+        util::span<size_type>(0, num_compartments),
+        compartment_maker<size_type, real_type>(num_compartments, length, radius_L, radius_R));
+}
 
 } // namespace mc
 } // namespace nest
diff --git a/src/fvm_cell.hpp b/src/fvm_cell.hpp
index 83fd8b769974f8447b3930d8f2477a0732d139fe..1d9ff96568dc3c2817944145330e9a7178c19e05 100644
--- a/src/fvm_cell.hpp
+++ b/src/fvm_cell.hpp
@@ -288,7 +288,7 @@ private:
     /// the ion species
     std::map<mechanisms::ionKind, ion_type> ions_;
 
-    std::vector<std::pair<uint32_t, i_clamp>> stimulii_;
+    std::vector<std::pair<uint32_t, i_clamp>> stimuli_;
 
     std::vector<std::pair<const vector_type fvm_cell::*, uint32_t>> probes_;
 
@@ -531,10 +531,10 @@ void fvm_cell<T, I>::initialize(
     ion_ca().internal_concentration()(all) = 5e-5;          // mM
     ion_ca().external_concentration()(all) = 2.0;           // mM
 
-    // add the stimulii
-    for(const auto& stim : cell.stimulii()) {
+    // add the stimuli
+    for(const auto& stim : cell.stimuli()) {
         auto idx = find_compartment_index(stim.location, graph);
-        stimulii_.push_back( {idx, stim.clamp} );
+        stimuli_.push_back( {idx, stim.clamp} );
     }
 
     // record probe locations by index into corresponding state vector
@@ -633,8 +633,8 @@ void fvm_cell<T, I>::advance(T dt) {
         PL();
     }
 
-    // add current contributions from stimulii
-    for (auto& stim : stimulii_) {
+    // add current contributions from stimuli
+    for (auto& stim : stimuli_) {
         auto ie = stim.second.amplitude(t_); // [nA]
         auto loc = stim.first;
 
diff --git a/src/fvm_multicell.hpp b/src/fvm_multicell.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..72d22b25270bb1eb71a7a3307e39c2ad17bec62d
--- /dev/null
+++ b/src/fvm_multicell.hpp
@@ -0,0 +1,625 @@
+#pragma once
+
+#include <algorithm>
+#include <iterator>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <algorithms.hpp>
+#include <cell.hpp>
+#include <event_queue.hpp>
+#include <ion.hpp>
+#include <math.hpp>
+#include <matrix.hpp>
+#include <mechanism.hpp>
+#include <mechanism_catalogue.hpp>
+#include <profiling/profiler.hpp>
+#include <segment.hpp>
+#include <stimulus.hpp>
+#include <util.hpp>
+#include <util/debug.hpp>
+#include <util/meta.hpp>
+#include <util/partition.hpp>
+#include <util/span.hpp>
+
+#include <vector/include/Vector.hpp>
+
+namespace nest {
+namespace mc {
+namespace fvm {
+
+template <typename Value, typename Index>
+class fvm_multicell {
+public:
+    fvm_multicell() = default;
+
+    /// the real number type
+    using value_type = Value;
+
+    /// the integral index type
+    using size_type = Index;
+
+    /// the container used for indexes
+    using index_type = memory::HostVector<size_type>;
+
+    /// the container used for values
+    using vector_type = memory::HostVector<value_type>;
+
+    /// API for cell_group (see above):
+
+    using detector_handle = size_type;
+    using target_handle = std::pair<size_type, size_type>;
+    using probe_handle = std::pair<const vector_type fvm_multicell::*, size_type>;
+
+    void resting_potential(value_type potential_mV) {
+        resting_potential_ = potential_mV;
+    }
+
+    template <typename Cells, typename Detectors, typename Targets, typename Probes>
+    void initialize(
+        const Cells& cells,           // collection of nest::mc::cell descriptions
+        Detectors& detector_handles,  // (write) where to store detector handles
+        Targets& target_handles,      // (write) where to store target handles
+        Probes& probe_handles);       // (write) where to store probe handles
+
+    void reset();
+
+    void deliver_event(target_handle h, value_type weight) {
+        mechanisms_[synapse_base_+h.first]->net_receive(h.second, weight);
+    }
+
+    value_type detector_voltage(detector_handle h) const {
+        return voltage_[h]; // detector_handle is just the compartment index
+    }
+
+    value_type probe(probe_handle h) const {
+        return (this->*h.first)[h.second];
+    }
+
+    void advance(value_type dt);
+
+    /// Following types and methods are public only for testing:
+
+    /// the type used to store matrix information
+    using matrix_type = matrix<value_type, size_type>;
+
+    /// mechanism type
+    using mechanism_type =
+        nest::mc::mechanisms::mechanism_ptr<value_type, size_type>;
+
+    /// ion species storage
+    using ion_type = mechanisms::ion<value_type, size_type>;
+
+    /// view into index container
+    using index_view = typename index_type::view_type;
+    using const_index_view = typename index_type::const_view_type;
+
+    /// view into value container
+    using vector_view = typename vector_type::view_type;
+    using const_vector_view = typename vector_type::const_view_type;
+
+    /// build the matrix for a given time step
+    void setup_matrix(value_type dt);
+
+    /// which requires const_view in the vector library
+    const matrix_type& jacobian() {
+        return matrix_;
+    }
+
+    /// return list of CV areas in :
+    ///          um^2
+    ///     1e-6.mm^2
+    ///     1e-8.cm^2
+    const_vector_view cv_areas() const {
+        return cv_areas_;
+    }
+
+    /// return the capacitance of each CV surface
+    /// this is the total capacitance, not per unit area,
+    /// i.e. equivalent to sigma_i * c_m
+    const_vector_view cv_capacitance() const {
+        return cv_capacitance_;
+    }
+
+    /// return the voltage in each CV
+    vector_view       voltage()       { return voltage_; }
+    const_vector_view voltage() const { return voltage_; }
+
+    /// return the current in each CV
+    vector_view       current()       { return current_; }
+    const_vector_view current() const { return current_; }
+
+    std::size_t size() const { return matrix_.size(); }
+
+    /// return reference to in iterable container of the mechanisms
+    std::vector<mechanism_type>& mechanisms() { return mechanisms_; }
+
+    /// return reference to list of ions
+    std::map<mechanisms::ionKind, ion_type>&       ions()       { return ions_; }
+    std::map<mechanisms::ionKind, ion_type> const& ions() const { return ions_; }
+
+    /// return reference to sodium ion
+    ion_type&       ion_na()       { return ions_[mechanisms::ionKind::na]; }
+    ion_type const& ion_na() const { return ions_[mechanisms::ionKind::na]; }
+
+    /// return reference to calcium ion
+    ion_type&       ion_ca()       { return ions_[mechanisms::ionKind::ca]; }
+    ion_type const& ion_ca() const { return ions_[mechanisms::ionKind::ca]; }
+
+    /// return reference to pottasium ion
+    ion_type&       ion_k()       { return ions_[mechanisms::ionKind::k]; }
+    ion_type const& ion_k() const { return ions_[mechanisms::ionKind::k]; }
+
+    /// flags if solution is physically realistic.
+    /// here we define physically realistic as the voltage being within reasonable bounds.
+    /// use a simple test of the voltage at the soma is reasonable, i.e. in the range
+    ///     v_soma \in (-1000mv, 1000mv)
+    bool is_physical_solution() const {
+        auto v = voltage_[0];
+        return (v>-1000.) && (v<1000.);
+    }
+
+    value_type time() const { return t_; }
+
+    std::size_t num_probes() const { return probes_.size(); }
+
+private:
+    /// current time [ms]
+    value_type t_ = value_type{0};
+
+    /// resting potential (initial voltage condition)
+    value_type resting_potential_ = -65;
+
+    /// the linear system for implicit time stepping of cell state
+    matrix_type matrix_;
+
+    /// index for fast lookup of compartment index ranges of segments
+    index_type segment_index_;
+
+    /// cv_areas_[i] is the surface area of CV i [µm^2]
+    vector_type cv_areas_;
+
+    /// alpha_[i] is the following value at the CV face between
+    /// CV i and its parent, required when constructing linear system
+    ///     face_alpha_[i] = area_face  / (c_m * r_L * delta_x);
+    vector_type face_alpha_; // [µm·m^2/cm/s ≡ 10^5 µm^2/ms]
+
+    /// cv_capacitance_[i] is the capacitance of CV i per unit area (i.e. c_m) [F/m^2]
+    vector_type cv_capacitance_;
+
+    /// the average current density over the surface of each CV [mA/cm^2]
+    /// current_ = i_m - i_e
+    vector_type current_;
+
+    /// the potential in each CV [mV]
+    vector_type voltage_;
+
+    /// Where point mechanisms start in the mechanisms_ list.
+    std::size_t synapse_base_;
+
+    /// the set of mechanisms present in the cell
+    std::vector<mechanism_type> mechanisms_;
+
+    /// the ion species
+    std::map<mechanisms::ionKind, ion_type> ions_;
+
+    std::vector<std::pair<uint32_t, i_clamp>> stimuli_;
+
+    std::vector<std::pair<const vector_type fvm_multicell::*, uint32_t>> probes_;
+
+    // mechanism factory
+    using mechanism_catalogue = nest::mc::mechanisms::catalogue<value_type, size_type>;
+
+    // perform area and capacitance calculation on initialization
+    void compute_cv_area_unnormalized_capacitance(
+        std::pair<size_type, size_type> comp_ival, const segment* seg, index_type &parent);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////// Implementation ////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T, typename I>
+void fvm_multicell<T, I>::compute_cv_area_unnormalized_capacitance(
+    std::pair<size_type, size_type> comp_ival,
+    const segment* seg,
+    index_type &parent)
+{
+    using util::left;
+    using util::right;
+    using util::size;
+
+    // precondition: group_parent_index[j] holds the correct value for
+    // j in [base_comp, base_comp+segment.num_compartments()].
+
+    auto ncomp = comp_ival.second-comp_ival.first;
+
+    if (auto soma = seg->as_soma()) {
+        // confirm assumption that there is one compartment in soma
+        if (ncomp!=1) {
+            throw std::logic_error("soma allocated more than one compartment");
+        }
+        auto i = comp_ival.first;
+        auto area = math::area_sphere(soma->radius());
+
+        cv_areas_[i] += area;
+        cv_capacitance_[i] += area * soma->mechanism("membrane").get("c_m").value;
+    }
+    else if (auto cable = seg->as_cable()) {
+        // loop over each compartment in the cable
+        // each compartment has the face between two CVs at its centre
+        // the centers of the CVs are the end points of the compartment
+        //
+        //  __________________________________
+        //  | ........ | .cvleft. |    cv    |
+        //  | ........ L ........ C          R
+        //  |__________|__________|__________|
+        //
+        //  The compartment has end points marked L and R (left and right).
+        //  The left compartment is assumed to be closer to the soma
+        //  (i.e. it follows the minimal degree ordering)
+        //  The face is at the center, marked C.
+        //  The full control volume to the left (marked with .)
+
+        auto c_m = cable->mechanism("membrane").get("c_m").value;
+        auto r_L = cable->mechanism("membrane").get("r_L").value;
+        const auto& compartments = cable->compartments();
+
+        EXPECTS(size(compartments)==ncomp);
+
+        for (auto i: util::make_span(comp_ival)) {
+            const auto& c = compartments[i-comp_ival.first];
+            auto j = parent[i];
+
+            auto radius_center = math::mean(c.radius);
+            auto area_face = math::area_circle(radius_center);
+            face_alpha_[i] = area_face / (c_m * r_L * c.length);
+
+            auto halflen = c.length/2;
+            auto al = math::area_frustrum(halflen, left(c.radius), radius_center);
+            auto ar = math::area_frustrum(halflen, right(c.radius), radius_center);
+
+            cv_areas_[j] += al;
+            cv_areas_[i] += ar;
+            cv_capacitance_[j] += al * c_m;
+            cv_capacitance_[i] += ar * c_m;
+        }
+    }
+    else {
+        throw std::domain_error("FVM lowering encountered unsuported segment type");
+    }
+}
+
+template <typename T, typename I>
+template <typename Cells, typename Detectors, typename Targets, typename Probes>
+void fvm_multicell<T, I>::initialize(
+    const Cells& cells,
+    Detectors& detector_handles,
+    Targets& target_handles,
+    Probes& probe_handles)
+{
+    using util::transform_view;
+    using util::make_partition;
+    using util::make_span;
+    using util::size;
+
+    // count total detectors, targets and probes for validation of handle container sizes
+    size_type detectors_count = 0u;
+    size_type targets_count = 0u;
+    size_type probes_count = 0u;
+    size_type detectors_size = size(detector_handles);
+    size_type targets_size = size(target_handles);
+    size_type probes_size = size(probe_handles);
+
+    auto ncell = size(cells);
+    auto cell_num_compartments =
+        transform_view(cells, [](const cell& c) { return c.num_compartments(); });
+
+    std::vector<cell_lid_type> cell_comp_bounds;
+    auto cell_comp_part = make_partition(cell_comp_bounds, cell_num_compartments);
+    auto ncomp = cell_comp_part.bounds().second;
+
+    // initialize storage from total compartment count
+    cv_areas_   = vector_type{ncomp, T{0}};
+    face_alpha_ = vector_type{ncomp, T{0}};
+    cv_capacitance_ = vector_type{ncomp, T{0}};
+    current_    = vector_type{ncomp, T{0}};
+    voltage_    = vector_type{ncomp, T{resting_potential_}};
+
+    // create maps for mechanism initialization.
+    std::map<std::string, std::vector<std::pair<size_type, size_type>>> mech_map;
+    std::vector<std::vector<cell_lid_type>> syn_mech_map;
+    std::map<std::string, std::size_t> syn_mech_indices;
+
+    // initialize vector used for matrix creation.
+    index_type group_parent_index{ncomp, 0};
+
+    // create each cell:
+    auto target_hi = target_handles.begin();
+    auto detector_hi = detector_handles.begin();
+    auto probe_hi = probe_handles.begin();
+
+    for (size_type i = 0; i<ncell; ++i) {
+        const auto& c = cells[i];
+        auto comp_ival = cell_comp_part[i];
+
+        auto graph = c.model();
+
+        for (auto k: make_span(comp_ival)) {
+            group_parent_index[k] = graph.parent_index[k-comp_ival.first]+comp_ival.first;
+        }
+
+        auto seg_num_compartments =
+            transform_view(c.segments(), [](const segment_ptr& s) { return s->num_compartments(); });
+        auto nseg = seg_num_compartments.size();
+
+        std::vector<cell_lid_type> seg_comp_bounds;
+        auto seg_comp_part = make_partition(seg_comp_bounds, seg_num_compartments, comp_ival.first);
+
+        for (size_type j = 0; j<nseg; ++j) {
+            const auto& seg = c.segment(j);
+            const auto& seg_comp_ival = seg_comp_part[j];
+
+            compute_cv_area_unnormalized_capacitance(seg_comp_ival, seg, group_parent_index);
+
+            for (const auto& mech: seg->mechanisms()) {
+                if (mech.name()!="membrane") {
+                    mech_map[mech.name()].push_back(seg_comp_ival);
+                }
+            }
+        }
+
+        for (const auto& syn: c.synapses()) {
+            EXPECTS(targets_count < targets_size);
+
+            const auto& name = syn.mechanism.name();
+            std::size_t syn_mech_index = 0;
+            if (syn_mech_indices.count(name)==0) {
+                syn_mech_index = syn_mech_map.size();
+                syn_mech_indices[name] = syn_mech_index;
+                syn_mech_map.push_back(std::vector<size_type>{});
+            }
+            else {
+                syn_mech_index = syn_mech_indices[name];
+            }
+
+            auto& map_entry = syn_mech_map[syn_mech_index];
+
+            size_type syn_comp = comp_ival.first+find_compartment_index(syn.location, graph);
+            size_type syn_index = map_entry.size();
+            map_entry.push_back(syn_comp);
+
+            *target_hi++ = target_handle{syn_mech_index, syn_index};
+            ++targets_count;
+        }
+
+        // normalize capacitance across cell
+        for (auto k: make_span(comp_ival)) {
+            cv_capacitance_[k] /= cv_areas_[k];
+        }
+
+        // add the stimuli
+        for (const auto& stim: c.stimuli()) {
+            auto idx = comp_ival.first+find_compartment_index(stim.location, graph);
+            stimuli_.push_back({idx, stim.clamp});
+        }
+
+        // detector handles are just their corresponding compartment indices
+        for (const auto& detector: c.detectors()) {
+            EXPECTS(detectors_count < detectors_size);
+
+            auto comp = comp_ival.first+find_compartment_index(detector.location, graph);
+            *detector_hi++ = comp;
+            ++detectors_count;
+        }
+
+        // record probe locations by index into corresponding state vector
+        for (const auto& probe: c.probes()) {
+            EXPECTS(probes_count < probes_size);
+
+            auto comp = comp_ival.first+find_compartment_index(probe.location, graph);
+            switch (probe.kind) {
+            case probeKind::membrane_voltage:
+                *probe_hi++ = {&fvm_multicell::voltage_, comp};
+                break;
+            case probeKind::membrane_current:
+                *probe_hi++ = {&fvm_multicell::current_, comp};
+                break;
+            default:
+                throw std::logic_error("unrecognized probeKind");
+            }
+            ++probes_count;
+        }
+    }
+
+    // confirm write-parameters were appropriately sized
+    EXPECTS(detectors_size==detectors_count);
+    EXPECTS(targets_size==targets_count);
+    EXPECTS(probes_size==probes_count);
+
+    // initalize matrix
+    matrix_ = matrix_type(group_parent_index);
+
+    // create density mechanisms
+    std::vector<size_type> mech_comp_indices;
+    mech_comp_indices.reserve(ncomp);
+
+    for (auto& mech: mech_map) {
+        mech_comp_indices.clear();
+        for (auto comp_ival: mech.second) {
+            util::append(mech_comp_indices, make_span(comp_ival));
+        }
+
+        mechanisms_.push_back(
+            mechanism_catalogue::make(mech.first, voltage_, current_, mech_comp_indices)
+        );
+    }
+
+    // create point (synapse) mechanisms
+    synapse_base_ = mechanisms_.size();
+    for (const auto& syni: syn_mech_indices) {
+        const auto& mech_name = syni.first;
+
+        auto mech = mechanism_catalogue::make(mech_name, voltage_, current_, syn_mech_map[syni.second]);
+        mech->set_areas(cv_areas_);
+        mechanisms_.push_back(std::move(mech));
+    }
+
+    // build the ion species
+    for(auto ion : mechanisms::ion_kinds()) {
+        // find the compartment indexes of all compartments that have a
+        // mechanism that depends on/influences ion
+        std::set<int> index_set;
+        for(auto& mech : mechanisms_) {
+            if(mech->uses_ion(ion)) {
+                for(auto idx : mech->node_index()) {
+                    index_set.insert(idx);
+                }
+            }
+        }
+        std::vector<cell_lid_type> indexes(index_set.begin(), index_set.end());
+
+        // create the ion state
+        if(indexes.size()) {
+            ions_.emplace(ion, index_type(indexes));
+        }
+
+        // join the ion reference in each mechanism into the cell-wide ion state
+        for(auto& mech : mechanisms_) {
+            if(mech->uses_ion(ion)) {
+                mech->set_ion(ion, ions_[ion]);
+            }
+        }
+    }
+
+    // FIXME: Hard code parameters for now.
+    //        Take defaults for reversal potential of sodium and potassium from
+    //        the default values in Neuron.
+    //        Neuron's defaults are defined in the file
+    //          nrn/src/nrnoc/membdef.h
+    auto all = memory::all;
+
+    constexpr value_type DEF_vrest = -65.0; // same name as #define in Neuron
+
+    ion_na().reversal_potential()(all)     = 115+DEF_vrest; // mV
+    ion_na().internal_concentration()(all) =  10.0;         // mM
+    ion_na().external_concentration()(all) = 140.0;         // mM
+
+    ion_k().reversal_potential()(all)     = -12.0+DEF_vrest;// mV
+    ion_k().internal_concentration()(all) =  54.4;          // mM
+    ion_k().external_concentration()(all) =  2.5;           // mM
+
+    ion_ca().reversal_potential()(all)     = 12.5 * std::log(2.0/5e-5);// mV
+    ion_ca().internal_concentration()(all) = 5e-5;          // mM
+    ion_ca().external_concentration()(all) = 2.0;           // mM
+
+    // initialise mechanism and voltage state
+    reset();
+}
+
+template <typename T, typename I>
+void fvm_multicell<T, I>::setup_matrix(T dt) {
+    using memory::all;
+
+    // convenience accesors to matrix storage
+    auto l = matrix_.l();
+    auto d = matrix_.d();
+    auto u = matrix_.u();
+    auto p = matrix_.p();
+    auto rhs = matrix_.rhs();
+
+    //  The matrix has the following layout in memory
+    //  where j is the parent index of i, i.e. i<j
+    //
+    //      d[i] is the diagonal entry at a_ii
+    //      u[i] is the upper triangle entry at a_ji
+    //      l[i] is the lower triangle entry at a_ij
+    //
+    //       d[j] . . u[i]
+    //        .  .     .
+    //        .     .  .
+    //       l[i] . . d[i]
+    //
+
+    d(all) = cv_areas_; // [µm^2]
+    for (auto i=1u; i<d.size(); ++i) {
+        auto a = 1e5*dt * face_alpha_[i];
+
+        d[i] +=  a;
+        l[i]  = -a;
+        u[i]  = -a;
+
+        // add contribution to the diagonal of parent
+        d[p[i]] += a;
+    }
+
+    // the RHS of the linear system is
+    //      V[i] - dt/cm*(im - ie)
+    auto factor = 10.*dt; //  units: 10·ms/(F/m^2)·(mA/cm^2) ≡ mV
+    for(auto i=0u; i<d.size(); ++i) {
+        rhs[i] = cv_areas_[i]*(voltage_[i] - factor/cv_capacitance_[i]*current_[i]);
+    }
+}
+
+template <typename T, typename I>
+void fvm_multicell<T, I>::reset() {
+    voltage_(memory::all) = resting_potential_;
+    t_ = 0.;
+    for (auto& m : mechanisms_) {
+        m->nrn_init();
+    }
+}
+
+template <typename T, typename I>
+void fvm_multicell<T, I>::advance(T dt) {
+    using memory::all;
+
+    PE("current");
+    current_(all) = 0.;
+
+    // update currents from ion channels
+    for(auto& m : mechanisms_) {
+        PE(m->name().c_str());
+        m->set_params(t_, dt);
+        m->nrn_current();
+        PL();
+    }
+
+    // add current contributions from stimuli
+    for (auto& stim : stimuli_) {
+        auto ie = stim.second.amplitude(t_); // [nA]
+        auto loc = stim.first;
+
+        // note: current_ in [mA/cm^2], ie in [nA], cv_areas_ in [µm^2].
+        // unit scale factor: [nA/µm^2]/[mA/cm^2] = 100
+        current_[loc] -= 100*ie/cv_areas_[loc];
+    }
+    PL();
+
+    // solve the linear system
+    PE("matrix", "setup");
+    setup_matrix(dt);
+    PL(); PE("solve");
+    matrix_.solve();
+    PL();
+    voltage_(all) = matrix_.rhs();
+    PL();
+
+    // integrate state of gating variables etc.
+    PE("state");
+    for(auto& m : mechanisms_) {
+        PE(m->name().c_str());
+        m->nrn_state();
+        PL();
+    }
+    PL();
+
+    t_ += dt;
+}
+
+} // namespace fvm
+} // namespace mc
+} // namespace nest
+
diff --git a/src/mechanism.hpp b/src/mechanism.hpp
index c9c801fd198c764d61f3acf653055c8957daf00e..d826b2994f4dac39ef15a53f720acf1bf7fae603 100644
--- a/src/mechanism.hpp
+++ b/src/mechanism.hpp
@@ -1,7 +1,9 @@
 #pragma once
 
+#include <algorithm>
 #include <memory>
 #include <string>
+#include <util/meta.hpp>
 
 #include "indexed_view.hpp"
 #include "ion.hpp"
@@ -16,9 +18,7 @@ enum class mechanismKind {point, density};
 
 template <typename T, typename I>
 class mechanism {
-
 public:
-
     using value_type  = T;
     using size_type   = I;
 
@@ -27,15 +27,13 @@ public:
     using view_type   = typename vector_type::view_type;
     using index_type  = memory::HostVector<size_type>;
     using index_view  = typename index_type::view_type;
+    using const_index_view  = typename index_type::const_view_type;
     using indexed_view_type = indexed_view<value_type, size_type>;
 
     using ion_type    = ion<value_type, size_type>;
 
-    mechanism(view_type vec_v, view_type vec_i, index_view node_index)
-    :   vec_v_(vec_v)
-    ,   vec_i_(vec_i)
-    ,   node_index_(node_index)
-    ,   vec_area_(nullptr, 0)
+    mechanism(view_type vec_v, view_type vec_i, const_index_view node_index):
+        vec_v_(vec_v), vec_i_(vec_i), node_index_(node_index), vec_area_(nullptr, 0)
     {}
 
     std::size_t size() const
@@ -88,7 +86,7 @@ mechanism_ptr<typename M::value_type, typename M::size_type>
 make_mechanism(
     typename M::view_type  vec_v,
     typename M::view_type  vec_i,
-    typename M::index_view node_indices
+    typename M::const_index_view node_indices
 ) {
     return util::make_unique<M>(vec_v, vec_i, node_indices);
 }
diff --git a/src/mechanism_catalogue.hpp b/src/mechanism_catalogue.hpp
index c9d3e6de2771a1b18fa05f104dd257b8c7fe6dd9..5e79cd9f489558717de1132b502b5c211eff09f9 100644
--- a/src/mechanism_catalogue.hpp
+++ b/src/mechanism_catalogue.hpp
@@ -18,19 +18,22 @@ template <typename T, typename I>
 struct catalogue {
     using view_type = typename mechanism<T, I>::view_type;
     using index_view = typename mechanism<T, I>::index_view;
+    using const_index_view = typename mechanism<T, I>::const_index_view;
 
+    template <typename Indices>
     static mechanism_ptr<T, I> make(
         const std::string& name,
         view_type vec_v,
         view_type vec_i,
-        index_view node_indices)
+        Indices const& node_indices)
     {
         auto entry = mech_map.find(name);
         if (entry==mech_map.end()) {
             throw std::out_of_range("no such mechanism");
         }
 
-        return entry->second(vec_v, vec_i, node_indices);
+        const_index_view node_view(node_indices);
+        return entry->second(vec_v, vec_i, node_view);
     }
 
     static bool has(const std::string& name) {
@@ -38,11 +41,15 @@ struct catalogue {
     }
 
 private:
-    using maker_type = mechanism_ptr<T, I> (*)(view_type, view_type, index_view);
+    using maker_type = mechanism_ptr<T, I> (*)(view_type, view_type, const_index_view);
     static const std::map<std::string, maker_type> mech_map;
 
     template <template <typename, typename> class mech>
-    static mechanism_ptr<T, I> maker(view_type vec_v, view_type vec_i, index_view node_indices) {
+    static mechanism_ptr<T, I> maker(
+        view_type vec_v,
+        view_type vec_i,
+        const_index_view node_indices)
+    {
         return make_mechanism<mech<T, I>>(vec_v, vec_i, node_indices);
     }
 };
diff --git a/src/model.hpp b/src/model.hpp
index 5d0ce4fd8bcbe010cec3629e4b5d1adde38bcd96..2298e5af8365151654b2ca8a266f0dce2f82041b 100644
--- a/src/model.hpp
+++ b/src/model.hpp
@@ -15,6 +15,8 @@
 #include <recipe.hpp>
 #include <thread_private_spike_store.hpp>
 #include <util/nop.hpp>
+#include <util/partition.hpp>
+#include <util/range.hpp>
 
 #include "trace_sampler.hpp"
 
@@ -37,27 +39,36 @@ public:
         probe_spec probe;
     };
 
-    model(const recipe& rec, cell_gid_type cell_from, cell_gid_type cell_to):
-        cell_from_(cell_from),
-        cell_to_(cell_to),
-        communicator_(cell_from, cell_to)
+    template <typename Iter>
+    model(const recipe& rec, const util::partition_range<Iter>& groups):
+        cell_group_divisions_(groups.divisions().begin(), groups.divisions().end())
     {
+        // set up communicator based on partition
+        communicator_ = communicator_type{gid_partition()};
+
         // generate the cell groups in parallel, with one task per cell group
-        cell_groups_ = std::vector<cell_group_type>{cell_to_-cell_from_};
+        cell_groups_ = std::vector<cell_group_type>{gid_partition().size()};
         threading::parallel_vector<probe_record> probes;
 
-        threading::parallel_for::apply(cell_from_, cell_to_,
+        threading::parallel_for::apply(0, cell_groups_.size(),
             [&](cell_gid_type i) {
                 PE("setup", "cells");
-                auto cell = rec.get_cell(i);
-                auto idx = i-cell_from_;
-                cell_groups_[idx] = cell_group_type(i, cell);
-
-                cell_lid_type j = 0;
-                for (const auto& probe: cell.probes()) {
-                    cell_member_type probe_id{i,j++};
-                    probes.push_back({probe_id, probe});
+
+                auto gids = gid_partition()[i];
+                std::vector<cell> cells{gids.second-gids.first};
+
+                for (auto gid: util::make_span(gids)) {
+                    auto i = gid-gids.first;
+                    cells[i] = rec.get_cell(gid);
+
+                    cell_lid_type j = 0;
+                    for (const auto& probe: cells[i].probes()) {
+                        cell_member_type probe_id{gid, j++};
+                        probes.push_back({probe_id, probe});
+                    }
                 }
+
+                cell_groups_[i] = cell_group_type(gids.first, cells);
                 PL(2);
             });
 
@@ -65,7 +76,7 @@ public:
         probes_.assign(probes.begin(), probes.end());
 
         // generate the network connections
-        for (cell_gid_type i=cell_from_; i<cell_to_; ++i) {
+        for (cell_gid_type i: util::make_span(gid_partition().bounds())) {
             for (const auto& cc: rec.connections_on(i)) {
                 connection<time_type> conn{cc.source, cc.dest, cc.weight, cc.delay};
                 communicator_.add_connection(conn);
@@ -187,11 +198,11 @@ public:
     }
 
     void attach_sampler(cell_member_type probe_id, sampler_function f, time_type tfrom = 0) {
-        // TODO: translate probe_id.gid to appropriate group, but for now 1-1.
-        if (probe_id.gid<cell_from_ || probe_id.gid>=cell_to_) {
+        if (!algorithms::in_interval(probe_id.gid, gid_partition().bounds())) {
             return;
         }
-        cell_groups_[probe_id.gid-cell_from_].add_sampler(probe_id, f, tfrom);
+
+        cell_groups_[gid_partition().index(probe_id.gid)].add_sampler(probe_id, f, tfrom);
     }
 
     const std::vector<probe_record>& probes() const { return probes_; }
@@ -199,12 +210,14 @@ public:
     std::size_t num_spikes() const {
         return communicator_.num_spikes();
     }
+
     std::size_t num_groups() const {
         return cell_groups_.size();
     }
+
     std::size_t num_cells() const {
-        // TODO: fix when the assumption that there is one cell per cell group is no longer valid
-        return num_groups();
+        auto bounds = gid_partition().bounds();
+        return bounds.second-bounds.first;
     }
 
     // register a callback that will perform a export of the global
@@ -220,18 +233,22 @@ public:
     }
 
 private:
-    cell_gid_type cell_from_;
-    cell_gid_type cell_to_;
+    std::vector<cell_gid_type> cell_group_divisions_;
+
+    auto gid_partition() const -> decltype(util::partition_view(cell_group_divisions_)) {
+        return util::partition_view(cell_group_divisions_);
+    }
+
     time_type t_ = 0.;
     std::vector<cell_group_type> cell_groups_;
     communicator_type communicator_;
     std::vector<probe_record> probes_;
 
     using event_queue_type = typename communicator_type::event_queue;
-    util::double_buffer< std::vector<event_queue_type> > event_queues_;
+    util::double_buffer<std::vector<event_queue_type>> event_queues_;
 
     using local_spike_store_type = thread_private_spike_store<time_type>;
-    util::double_buffer< local_spike_store_type > local_spikes_;
+    util::double_buffer<local_spike_store_type> local_spikes_;
 
     spike_export_function global_export_callback_ = util::nop_function;
     spike_export_function local_export_callback_ = util::nop_function;
diff --git a/src/parameter_list.cpp b/src/parameter_list.cpp
index 86c641aa809e6efe1c8f03247cc64f9f1b3a01f0..2ca508175f58a402165fd0d56b744148cdb2b267 100644
--- a/src/parameter_list.cpp
+++ b/src/parameter_list.cpp
@@ -6,88 +6,92 @@
 namespace nest {
 namespace mc {
 
-    bool parameter_list::add_parameter(parameter p)
-    {
-        if(has_parameter(p.name)) {
-            return false;
-        }
+bool parameter_list::add_parameter(parameter p) {
+    if (has_parameter(p.name)) {
+        return false;
+    }
 
-        parameters_.push_back(std::move(p));
+    parameters_.push_back(std::move(p));
 
-        return true;
-    }
+    return true;
+}
 
-    bool parameter_list::has_parameter(std::string const& n) const
-    {
-        return find_by_name(n) != parameters_.end();
-    }
+bool parameter_list::has_parameter(std::string const& n) const {
+    return find_by_name(n) != parameters_.end();
+}
 
-    int parameter_list::num_parameters() const
-    {
-        return parameters_.size();
-    }
+int parameter_list::num_parameters() const {
+    return parameters_.size();
+}
 
-    // returns true if parameter was succesfully updated
-    // returns false if parameter was not updated, i.e. if
-    //  - no parameter with name n exists
-    //  - value is not in the valid range
-    bool parameter_list::set(std::string const& n, parameter_list::value_type v)
-    {
-        auto p = find_by_name(n);
-        if(p!=parameters_.end()) {
-            if(p->is_in_range(v)) {
-                p->value = v;
-                return true;
-            }
+// returns true if parameter was succesfully updated
+// returns false if parameter was not updated, i.e. if
+//  - no parameter with name n exists
+//  - value is not in the valid range
+bool parameter_list::set(std::string const& n, parameter_list::value_type v) {
+    auto p = find_by_name(n);
+    if(p!=parameters_.end()) {
+        if(p->is_in_range(v)) {
+            p->value = v;
+            return true;
         }
-        return false;
     }
+    return false;
+}
 
-    parameter& parameter_list::get(std::string const& n)
-    {
-        auto it = find_by_name(n);
-        if(it==parameters_.end()) {
-            throw std::domain_error(
-                "parameter list does not contain parameter"
-            );
-        }
-        return *it;
+parameter& parameter_list::get(std::string const& n) {
+    auto it = find_by_name(n);
+    if (it==parameters_.end()) {
+        throw std::domain_error(
+            "parameter list does not contain parameter"
+        );
     }
+    return *it;
+}
 
-    std::string const& parameter_list::name() const {
-        return mechanism_name_;
+const parameter& parameter_list::get(std::string const& n) const {
+    auto it = find_by_name(n);
+    if (it==parameters_.end()) {
+        throw std::domain_error(
+            "parameter list does not contain parameter"
+        );
     }
+    return *it;
+}
 
-    std::vector<parameter> const& parameter_list::parameters() const
-    {
-        return parameters_;
-    }
+std::string const& parameter_list::name() const {
+    return mechanism_name_;
+}
 
-    auto parameter_list::find_by_name(std::string const& n)
-        -> decltype(parameters_.begin())
-    {
-        return
-            std::find_if(
-                parameters_.begin(), parameters_.end(),
-                [&n](parameter const& p) {return p.name == n;}
-            );
-    }
+std::vector<parameter> const& parameter_list::parameters() const {
+    return parameters_;
+}
+
+auto parameter_list::find_by_name(std::string const& n)
+    -> decltype(parameters_.begin())
+{
+    return
+        std::find_if(
+            parameters_.begin(), parameters_.end(),
+            [&n](parameter const& p) {return p.name == n;}
+        );
+}
+
+auto parameter_list::find_by_name(std::string const& n) const
+    -> decltype(parameters_.begin())
+{
+    return
+        std::find_if(
+            parameters_.begin(), parameters_.end(),
+            [&n](parameter const& p) {return p.name == n;}
+        );
+}
 
-    auto parameter_list::find_by_name(std::string const& n) const
-        -> decltype(parameters_.begin())
-    {
-        return
-            std::find_if(
-                parameters_.begin(), parameters_.end(),
-                [&n](parameter const& p) {return p.name == n;}
-            );
-    }
 } // namespace mc
 } // namespace nest
 
 std::ostream&
-operator<<(std::ostream& o, nest::mc::parameter const& p)
-{
+operator<<(std::ostream& o, nest::mc::parameter const& p) {
     return o
         << "parameter("
         << "name " << p.name
@@ -97,8 +101,7 @@ operator<<(std::ostream& o, nest::mc::parameter const& p)
 }
 
 std::ostream&
-operator<<(std::ostream& o, nest::mc::parameter_list const& l)
-{
+operator<<(std::ostream& o, nest::mc::parameter_list const& l) {
     o << "parameters \"" << l.name() << "\" :\n";
     for(nest::mc::parameter const& p : l.parameters()) {
         o << " " << p << "\n";
diff --git a/src/parameter_list.hpp b/src/parameter_list.hpp
index 0c06988756a077fdbb860fc8f42043b178b1b7e7..eb3ee8fe2f3c3ce1018bb113c966cb556e12916e 100644
--- a/src/parameter_list.hpp
+++ b/src/parameter_list.hpp
@@ -102,6 +102,7 @@ namespace mc {
         //  - value is not in the valid range
         bool set(std::string const& n, value_type v);
         parameter& get(std::string const& n);
+        const parameter& get(std::string const& n) const;
 
         std::string const& name() const;
 
diff --git a/src/segment.hpp b/src/segment.hpp
index 248306a6c3a976d123b9d412b93c09156305086d..5a9782b9ce41d8748bb5af6234ab9640c349f360 100644
--- a/src/segment.hpp
+++ b/src/segment.hpp
@@ -418,9 +418,9 @@ public:
     }
 
     /// iterable range type for simple compartment representation
-    compartment_range compartments() const
+    compartment_range<size_type, value_type> compartments() const
     {
-        return {num_compartments(), radii_.front(), radii_.back(), length()};
+        return make_compartment_range(num_compartments(), radii_.front(), radii_.back(), length());
     }
 
 private:
diff --git a/src/util/debug.hpp b/src/util/debug.hpp
index be2b3d51e4d7bf9b5d4c5fb9b7c78855c5e4b4c5..f189852dc1bd49685920af2c0effdabf5a9ac132 100644
--- a/src/util/debug.hpp
+++ b/src/util/debug.hpp
@@ -78,5 +78,6 @@ void debug_emit_trace(const char* file, int line, const char* varlist, const Arg
        (void)((condition) || \
        nest::mc::util::global_failed_assertion_handler(#condition, __FILE__, __LINE__, DEBUG_FUNCTION_NAME))
 #else
-    #define EXPECTS(condition)
+    #define EXPECTS(condition) \
+       (void)(false && (condition))
 #endif // def WITH_ASSERTIONS
diff --git a/src/util/partition.hpp b/src/util/partition.hpp
index b35f81de8fa1f40217e331dd96e09a11eba1c174..425d41f164c3d6ecc27df1fd6d844f9917a55d8e 100644
--- a/src/util/partition.hpp
+++ b/src/util/partition.hpp
@@ -29,12 +29,19 @@ class partition_range: public range<partition_iterator<I>> {
 public:
     using typename base::iterator;
     using typename base::value_type;
+    using typename base::size_type;
     using base::left;
     using base::right;
     using base::front;
     using base::back;
     using base::empty;
 
+    // `npos` is returned by the `index()` method if the search fails;
+    // analogous to `std::string::npos`.
+    static constexpr size_type npos = static_cast<size_type>(-1);
+
+    partition_range() = default;
+
     template <typename Seq>
     partition_range(const Seq& s): base{std::begin(s), upto(std::begin(s), std::end(s))} {
         EXPECTS(is_valid());
@@ -62,6 +69,11 @@ public:
         return iterator{std::prev(i)};
     }
 
+    size_type index(const inner_value_type& x) const {
+        iterator i = find(x);
+        return i==right? npos: i-left;
+    }
+
     // access to underlying divisions
     range<I> divisions() const {
         return {left.get(), std::next(right.get())};
diff --git a/src/util/partition_iterator.hpp b/src/util/partition_iterator.hpp
index f13b82f25d43c604680ff4e96b3efd6936e98aea..dae1475ce91382801d6d827fd3f66f30da9c7a8d 100644
--- a/src/util/partition_iterator.hpp
+++ b/src/util/partition_iterator.hpp
@@ -37,6 +37,8 @@ public:
     using pointer = const value_type*;
     using reference = const value_type&;
 
+    partition_iterator() = default;
+
     template <
         typename J,
         typename = enable_if_t<!std::is_same<decay_t<J>, partition_iterator>::value>
diff --git a/src/util/range.hpp b/src/util/range.hpp
index 9e3bd62a620ca40ddad3b3d178be95c4738faef1..df5625d0e6dc1e315e18d1e2e1a0e4b4ccecbd94 100644
--- a/src/util/range.hpp
+++ b/src/util/range.hpp
@@ -172,6 +172,42 @@ range<const T*> singleton_view(const T& item) {
     return {&item, &item+1};
 }
 
+/*
+ * Range/container utility functions
+ */
+
+template <typename Container, typename Seq>
+Container& append(Container &c, const Seq& seq) {
+    auto canon = canonical_view(seq);
+    c.insert(c.end(), std::begin(canon), std::end(canon));
+    return c;
+}
+
+template <typename AssignableContainer, typename Seq>
+AssignableContainer& assign(AssignableContainer& c, const Seq& seq) {
+    auto canon = canonical_view(seq);
+    c.assign(std::begin(canon), std::end(canon));
+    return c;
+}
+
+template <typename Seq>
+range<typename sequence_traits<Seq>::iterator_type, typename sequence_traits<Seq>::sentinel_type>
+range_view(Seq& seq) {
+    return make_range(std::begin(seq), std::end(seq));
+}
+
+template <
+    typename Seq,
+    typename Iter = typename sequence_traits<Seq>::iterator_type,
+    typename Size = typename sequence_traits<Seq>::size_type
+>
+enable_if_t<is_forward_iterator<Iter>::value, range<Iter>>
+subrange_view(Seq& seq, Size bi, Size ei) {
+    Iter b = std::next(std::begin(seq), bi);
+    Iter e = std::next(b, ei-bi);
+    return make_range(b, e);
+}
+
 } // namespace util
 } // namespace mc
 } // namespace nest
diff --git a/src/util/singleton.hpp b/src/util/singleton.hpp
deleted file mode 100644
index c718eef3624af2088daa9d84abb00401ed1bd6d4..0000000000000000000000000000000000000000
--- a/src/util/singleton.hpp
+++ /dev/null
@@ -1,63 +0,0 @@
-#pragma once
-
-/*
- * Present a single object as a (non-owning) container with one
- * element.
- *
- * (Will be subsumed by range/view code.)
- */
-
-#include <algorithm>
-
-namespace nest {
-namespace mc {
-namespace util {
-
-template <typename X>
-struct singleton_adaptor {
-    using size_type = std::size_t;
-    using difference_type = std::ptrdiff_t;
-    using value_type = X;
-    using reference = X&;
-    using const_reference = const X&;
-    using iterator = X*;
-    using const_iterator = const X*;
-
-    X* xp;
-    singleton_adaptor(X& x): xp(&x) {}
-
-    const X* cbegin() const { return xp; }
-    const X* begin() const { return xp; }
-    X* begin() { return xp; }
-
-    const X* cend() const { return xp+1; }
-    const X* end() const { return xp+1; }
-    X* end() { return xp+1; }
-
-    std::size_t size() const { return 1u; }
-
-    bool empty() const { return false; }
-
-    const X* front() const { return *xp; }
-    X* front() { return *xp; }
-
-    const X* back() const { return *xp; }
-    X* back() { return *xp; }
-
-    const X* operator[](difference_type) const { return *xp; }
-    X* operator[](difference_type) { return *xp; }
-
-    void swap(singleton_adaptor& s) { std::swap(xp, s.xp); }
-    friend void swap(singleton_adaptor& r, singleton_adaptor& s) {
-        r.swap(s);
-    }
-};
-
-template <typename X>
-singleton_adaptor<X> singleton_view(X& x) {
-    return singleton_adaptor<X>(x);
-}
-
-} // namespace util
-} // namespace mc
-} // namespace nest
diff --git a/src/util/transform.hpp b/src/util/transform.hpp
index d6c930dbdc2cb653e94433ff0424fdc5612cbe3f..a5ab0b348a61fb0f3a174058dc31fdb128776e24 100644
--- a/src/util/transform.hpp
+++ b/src/util/transform.hpp
@@ -33,10 +33,12 @@ class transform_iterator: public iterator_adaptor<transform_iterator<I, F>, I> {
 
 public:
     using typename base::difference_type;
-    using value_type = decltype(f_(*inner_));
+    using value_type = typename std::result_of<F (inner_value_type)>::type;
     using pointer = const value_type*;
     using reference = const value_type&;
 
+    transform_iterator() = default;
+
     template <typename J, typename G>
     transform_iterator(J&& c, G&& g): inner_(std::forward<J>(c)), f_(std::forward<G>(g)) {}
 
diff --git a/tests/global_communication/CMakeLists.txt b/tests/global_communication/CMakeLists.txt
index cc1e1af8b6567a2fc887815fa4eaeb25c561841e..800e7d193dcb335d2ee8ae164ed38b947eaae313 100644
--- a/tests/global_communication/CMakeLists.txt
+++ b/tests/global_communication/CMakeLists.txt
@@ -4,6 +4,7 @@ set(HEADERS
 set(COMMUNICATION_SOURCES
     test_exporter_spike_file.cpp
     test_communicator.cpp
+    test_mpi_gather_all.cpp
 
     # unit test driver
     test.cpp
diff --git a/tests/global_communication/test_mpi_gather_all.cpp b/tests/global_communication/test_mpi_gather_all.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..50fbf4052da14b057c6ed003d51cfa8b13436e93
--- /dev/null
+++ b/tests/global_communication/test_mpi_gather_all.cpp
@@ -0,0 +1,100 @@
+#include "gtest.h"
+
+#ifdef WITH_MPI
+
+#include <cstring>
+#include <vector>
+
+#include <communication/mpi_global_policy.hpp>
+#include <communication/mpi.hpp>
+#include <util/range.hpp>
+
+using namespace nest::mc;
+using namespace nest::mc::communication;
+
+struct big_thing {
+    big_thing() {}
+    big_thing(int i): value_(i) {}
+
+    bool operator==(const big_thing& other) const {
+        return value_==other.value_ && !std::memcmp(salt_, other.salt_, sizeof(salt_));
+    }
+
+    bool operator!=(const big_thing& other) const {
+        return !(*this==other);
+    }
+
+private:
+    int value_;
+    char salt_[32] = "it's a lovely day for a picnic";
+};
+
+TEST(mpi, gather_all) {
+    using policy = mpi_global_policy;
+
+    int id = policy::id();
+
+    std::vector<big_thing> data;
+    // odd ranks: three items; even ranks: one item.
+    if (id%2) {
+        data = { id, id+7, id+8 };
+    }
+    else {
+        data = { id };
+    }
+
+    std::vector<big_thing> expected;
+    for (int i = 0; i<policy::size(); ++i) {
+        if (i%2) {
+            int rank_data[] = { i, i+7, i+8 };
+            util::append(expected, rank_data);
+        }
+        else {
+            int rank_data[] = { i };
+            util::append(expected, rank_data);
+        }
+    }
+
+    auto gathered = mpi::gather_all(data);
+
+    EXPECT_EQ(expected, gathered);
+}
+
+TEST(mpi, gather_all_with_partition) {
+    using policy = mpi_global_policy;
+
+    int id = policy::id();
+
+    std::vector<big_thing> data;
+    // odd ranks: three items; even ranks: one item.
+    if (id%2) {
+        data = { id, id+7, id+8 };
+    }
+    else {
+        data = { id };
+    }
+
+    std::vector<big_thing> expected_values;
+    std::vector<unsigned> expected_divisions;
+
+    expected_divisions.push_back(0);
+    for (int i = 0; i<policy::size(); ++i) {
+        if (i%2) {
+            int rank_data[] = { i, i+7, i+8 };
+            util::append(expected_values, rank_data);
+            expected_divisions.push_back(expected_divisions.back()+util::size(rank_data));
+        }
+        else {
+            int rank_data[] = { i };
+            util::append(expected_values, rank_data);
+            expected_divisions.push_back(expected_divisions.back()+util::size(rank_data));
+        }
+    }
+
+    auto gathered = mpi::gather_all_with_partition(data);
+
+    EXPECT_EQ(expected_values, gathered.values());
+    EXPECT_EQ(expected_divisions, gathered.partition());
+}
+
+#endif // WITH_MPI
diff --git a/tests/test_common_cells.hpp b/tests/test_common_cells.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c4641c879950fb6a5249cc25f38c3e6006df69ea
--- /dev/null
+++ b/tests/test_common_cells.hpp
@@ -0,0 +1,112 @@
+#include <cell.hpp>
+#include <parameter_list.hpp>
+
+namespace nest {
+namespace mc {
+
+/*
+ * Create cell with just a soma:
+ *
+ * Soma:
+ *    diameter: 18.8 µm
+ *    mechanisms: membrane, HH
+ *    memrane resistance: 123 Ω·cm
+ *
+ * Stimuli:
+ *    soma centre, t=[10 ms, 110 ms), 0.1 nA
+ */
+
+inline cell make_cell_soma_only() {
+    cell c;
+
+    auto soma = c.add_soma(18.8/2.0);
+    soma->mechanism("membrane").set("r_L", 123);
+    soma->add_mechanism(hh_parameters());
+
+    c.add_stimulus({0,0.5}, {10., 100., 0.1});
+
+    return c;
+}
+
+/*
+ * Create cell with a soma and unbranched dendrite:
+ *
+ * Soma:
+ *    mechanisms: HH
+ *    diameter: 12.6157 µm
+ *
+ * Dendrite:
+ *    mechanisms: none
+ *    diameter: 1 µm
+ *    length: 200 µm
+ *    membrane resistance: 100 Ω·cm
+ *    compartments: 4
+ *
+ * Stimulus:
+ *    end of dendrite, t=[5 ms, 85 ms), 0.3 nA
+ */
+
+inline cell make_cell_ball_and_stick() {
+    cell c;
+
+    auto soma = c.add_soma(12.6157/2.0);
+    soma->add_mechanism(hh_parameters());
+
+    auto dendrite = c.add_cable(0, segmentKind::dendrite, 1.0/2, 1.0/2, 200.0);
+    dendrite->add_mechanism(pas_parameters());
+    dendrite->mechanism("membrane").set("r_L", 100);
+    dendrite->set_compartments(4);
+
+    c.add_stimulus({1,1}, {5., 80., 0.3});
+
+    return c;
+}
+
+/*
+ * Create cell with a soma and three-segment dendrite with single branch point:
+ *
+ * O----======
+ *
+ * Soma:
+ *    mechanisms: HH
+ *    diameter: 12.6157 µm
+ *
+ * Dendrites:
+ *    mechanisms: membrane
+ *    diameter: 1 µm
+ *    length: 100 µm
+ *    membrane resistance: 100 Ω·cm
+ *    compartments: 4
+ *
+ * Stimulus:
+ *    end of first terminal branch, t=[5 ms, 85 ms), 0.45 nA
+ *    end of second terminal branch, t=[40 ms, 50 ms), -0.2 nA
+ */
+
+inline cell make_cell_ball_and_3sticks() {
+    cell c;
+
+    auto soma = c.add_soma(12.6157/2.0);
+    soma->add_mechanism(hh_parameters());
+
+    // add dendrite of length 200 um and diameter 1 um with passive channel
+    c.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 100);
+    c.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100);
+    c.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100);
+
+    for (auto& seg: c.segments()) {
+        if (seg->is_dendrite()) {
+            seg->add_mechanism(pas_parameters());
+            seg->mechanism("membrane").set("r_L", 100);
+            seg->set_compartments(4);
+        }
+    }
+
+    c.add_stimulus({2,1}, {5.,  80., 0.45});
+    c.add_stimulus({3,1}, {40., 10.,-0.2});
+
+    return c;
+}
+
+} // namespace mc
+} // namespace nest
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 6b34d70125bb4d9792f8abb6b68daf411dfe78d8..1c286863c3e92a9c8f2a11eba8679f66094b3c72 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -18,6 +18,7 @@ set(TEST_SOURCES
     test_either.cpp
     test_event_queue.cpp
     test_fvm.cpp
+    test_fvm_multi.cpp
     test_cell_group.cpp
     test_lexcmp.cpp
     test_mask_stream.cpp
diff --git a/tests/unit/test_cell_group.cpp b/tests/unit/test_cell_group.cpp
index 3aed222029f4535a85d9713849cff184a3684531..9b3f6e6a0b13f7338558889e40e47d7fe7ed99c8 100644
--- a/tests/unit/test_cell_group.cpp
+++ b/tests/unit/test_cell_group.cpp
@@ -1,27 +1,19 @@
 #include "gtest.h"
 
+#include <cell_group.hpp>
 #include <common_types.hpp>
 #include <fvm_cell.hpp>
-#include <cell_group.hpp>
+#include <util/range.hpp>
+
+#include "../test_common_cells.hpp"
 
 nest::mc::cell make_cell() {
     using namespace nest::mc;
 
-    nest::mc::cell cell;
-
-    // Soma with diameter 12.6157 um and HH channel
-    auto soma = cell.add_soma(12.6157/2.0);
-    soma->add_mechanism(hh_parameters());
-
-    // add dendrite of length 200 um and diameter 1 um with passive channel
-    auto dendrite = cell.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 200);
-    dendrite->add_mechanism(pas_parameters());
-    dendrite->set_compartments(101);
-
-    dendrite->mechanism("membrane").set("r_L", 100);
+    nest::mc::cell cell = make_cell_ball_and_stick();
 
     cell.add_detector({0, 0}, 0);
-    cell.add_stimulus({1, 1}, {5., 80., 0.3});
+    cell.segment(1)->set_compartments(101);
 
     return cell;
 }
@@ -31,7 +23,7 @@ TEST(cell_group, test)
     using namespace nest::mc;
 
     using cell_group_type = cell_group<fvm::fvm_cell<double, cell_local_size_type>>;
-    auto group = cell_group_type{0, make_cell()};
+    auto group = cell_group_type{0, util::singleton_view(make_cell())};
 
     group.advance(50, 0.01);
 
@@ -53,7 +45,7 @@ TEST(cell_group, sources)
     cell.add_detector({1, 0.3}, 2.3);
 
     cell_gid_type first_gid = 37u;
-    auto group = cell_group_type{first_gid, cell};
+    auto group = cell_group_type{first_gid, util::singleton_view(cell)};
 
     // expect group sources to be lexicographically sorted by source id
     // with gids in cell group's range and indices starting from zero
diff --git a/tests/unit/test_compartments.cpp b/tests/unit/test_compartments.cpp
index c167b16cce5585a43efc13fc9f136025b21f7811..8bc034ade553675ac8f85ce19bc8bfccf5d14d5d 100644
--- a/tests/unit/test_compartments.cpp
+++ b/tests/unit/test_compartments.cpp
@@ -2,8 +2,8 @@
 
 #include "gtest.h"
 
-#include "../src/compartment.hpp"
-#include "../src/util.hpp"
+#include <compartment.hpp>
+#include <util.hpp>
 
 using nest::mc::util::left;
 using nest::mc::util::right;
@@ -12,7 +12,6 @@ using nest::mc::util::right;
 // are correctly stored in members
 TEST(compartments, compartment)
 {
-
     {
         nest::mc::compartment c(100, 1.2, 2.1, 2.2);
         EXPECT_EQ(c.index, 100u);
@@ -36,98 +35,27 @@ TEST(compartments, compartment)
     }
 }
 
-TEST(compartments, compartment_iterator)
+TEST(compartments, make_compartment_range)
 {
-    // iterator with arguments
-    //  idx = 0
-    //  len = 2.5
-    //  rad = 1
-    //  delta_rad = 2
-    nest::mc::compartment_iterator it(0, 2.5, 1, 2);
-
-    // so each time the iterator is incremented
-    //   idx is incremented by 1
-    //   len is unchanged
-    //   rad is incremented by 2
-
-    // check the prefix increment
-    ++it;
-    {
-        auto c = *it;
-        EXPECT_EQ(c.index, 1u);
-        EXPECT_EQ(left(c.radius), 3.0);
-        EXPECT_EQ(right(c.radius), 5.0);
-        EXPECT_EQ(c.length, 2.5);
-    }
+    using namespace nest::mc;
+    auto rng = make_compartment_range(10, 1.0, 2.0, 10.);
 
-    // check postfix increment
+    EXPECT_EQ((*rng.begin()).index, 0u);
+    EXPECT_EQ((*rng.end()).index, 10u);
+    EXPECT_NE(rng.begin(), rng.end());
 
-    // returned iterator should be unchanged
-    {
-        auto c = *(it++);
-        EXPECT_EQ(c.index, 1u);
-        EXPECT_EQ(left(c.radius), 3.0);
-        EXPECT_EQ(right(c.radius), 5.0);
-        EXPECT_EQ(c.length, 2.5);
-    }
-    // while the iterator itself was updated
-    {
-        auto c = *it;
-        EXPECT_EQ(c.index, 2u);
-        EXPECT_EQ(left(c.radius), 5.0);
-        EXPECT_EQ(right(c.radius), 7.0);
-        EXPECT_EQ(c.length, 2.5);
-    }
-
-    // check that it can be copied and compared
-    {
-        // copy iterator
-        auto it2 = it;
-        auto c = *it2;
-        EXPECT_EQ(c.index, 2u);
-        EXPECT_EQ(left(c.radius), 5.0);
-        EXPECT_EQ(right(c.radius), 7.0);
-        EXPECT_EQ(c.length, 2.5);
-
-        // comparison
-        EXPECT_EQ(it2, it);
-        it2++;
-        EXPECT_NE(it2, it);
-
-        // check the copy has updated correctly when incremented
-        c= *it2;
-        EXPECT_EQ(c.index, 3u);
-        EXPECT_EQ(left(c.radius), 7.0);
-        EXPECT_EQ(right(c.radius), 9.0);
-        EXPECT_EQ(c.length, 2.5);
-    }
-}
-
-TEST(compartments, compartment_range)
-{
-    {
-        nest::mc::compartment_range rng(10, 1.0, 2.0, 10.);
-
-        EXPECT_EQ((*rng.begin()).index, 0u);
-        EXPECT_EQ((*rng.end()).index, 10u);
-        EXPECT_NE(rng.begin(), rng.end());
-
-        unsigned count = 0;
-        for(auto c : rng) {
-            EXPECT_EQ(c.index, count);
-            auto er = 1.0 + double(count)/10.;
-            EXPECT_TRUE(std::fabs(left(c.radius)-er)<1e-15);
-            EXPECT_TRUE(std::fabs(right(c.radius)-(er+0.1))<1e-15);
-            EXPECT_EQ(c.length, 1.0);
-            ++count;
-        }
-        EXPECT_EQ(count, 10u);
+    unsigned count = 0;
+    for (auto c : rng) {
+        EXPECT_EQ(c.index, count);
+        auto er = 1.0 + double(count)/10.;
+        EXPECT_DOUBLE_EQ(left(c.radius), er);
+        EXPECT_DOUBLE_EQ(right(c.radius), er+0.1);
+        EXPECT_EQ(c.length, 1.0);
+        ++count;
     }
+    EXPECT_EQ(count, 10u);
 
     // test case of zero length range
-    {
-        nest::mc::compartment_range rng(0, 1.0, 1.0, 0.);
-
-        EXPECT_EQ(rng.begin(), rng.end());
-    }
+    auto rng_empty = make_compartment_range(0, 1.0, 1.0, 0.);
+    EXPECT_EQ(rng_empty.begin(), rng_empty.end());
 }
diff --git a/tests/unit/test_fvm.cpp b/tests/unit/test_fvm.cpp
index 39fdb6c837f71820c3fd6981f9be2bc38cee91c8..6d067182f972a0bc65147e8345f3043230d70d9a 100644
--- a/tests/unit/test_fvm.cpp
+++ b/tests/unit/test_fvm.cpp
@@ -5,42 +5,16 @@
 #include <common_types.hpp>
 #include <cell.hpp>
 #include <fvm_cell.hpp>
-#include <util/singleton.hpp>
+#include <util/range.hpp>
 
+#include "../test_common_cells.hpp"
 #include "../test_util.hpp"
 
 TEST(fvm, cable)
 {
     using namespace nest::mc;
 
-    nest::mc::cell cell;
-
-    cell.add_soma(6e-4); // 6um in cm
-
-    // 1um radius and 4mm long, all in cm
-    cell.add_cable(0, segmentKind::dendrite, 1e-4, 1e-4, 4e-1);
-    cell.add_cable(0, segmentKind::dendrite, 1e-4, 1e-4, 4e-1);
-
-    //std::cout << cell.segment(1)->area() << " is the area\n";
-    EXPECT_EQ(cell.model().tree.num_segments(), 3u);
-
-    // add passive to all 3 segments in the cell
-    for(auto& seg :cell.segments()) {
-        seg->add_mechanism(pas_parameters());
-    }
-
-    cell.soma()->add_mechanism(hh_parameters());
-    cell.segment(2)->add_mechanism(hh_parameters());
-
-    auto& soma_hh = cell.soma()->mechanism("hh");
-
-    soma_hh.set("gnabar", 0.12);
-    soma_hh.set("gkbar", 0.036);
-    soma_hh.set("gl", 0.0003);
-    soma_hh.set("el", -54.387);
-
-    cell.segment(1)->set_compartments(4);
-    cell.segment(2)->set_compartments(4);
+    nest::mc::cell cell = make_cell_ball_and_3sticks();
 
     using fvm_cell = fvm::fvm_cell<double, cell_lid_type>;
 
@@ -53,7 +27,8 @@ TEST(fvm, cable)
 
     auto& J = fvcell.jacobian();
 
-    EXPECT_EQ(cell.num_compartments(), 9u);
+    // 1 (soma) + 3 (dendritic segments) × 4 compartments
+    EXPECT_EQ(cell.num_compartments(), 13u);
 
     // assert that the matrix has one row for each compartment
     EXPECT_EQ(J.size(), cell.num_compartments());
@@ -69,21 +44,11 @@ TEST(fvm, init)
 {
     using namespace nest::mc;
 
-    nest::mc::cell cell;
-
-    cell.add_soma(12.6157/2.0);
-    //auto& props = cell.soma()->properties;
-
-    cell.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 200);
+    nest::mc::cell cell = make_cell_ball_and_stick();
 
     const auto m = cell.model();
     EXPECT_EQ(m.tree.num_segments(), 2u);
 
-    // in this context (i.e. attached to a segment on a high-level cell)
-    // a mechanism is essentially a set of parameters
-    // - the only "state" is that used to define parameters
-    cell.soma()->add_mechanism(hh_parameters());
-
     auto& soma_hh = cell.soma()->mechanism("hh");
 
     soma_hh.set("gnabar", 0.12);
@@ -112,4 +77,3 @@ TEST(fvm, init)
 
     fvcell.setup_matrix(0.01);
 }
-
diff --git a/tests/unit/test_fvm_multi.cpp b/tests/unit/test_fvm_multi.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fbd4de99b91198128c6b3d0f0098f38f8440984d
--- /dev/null
+++ b/tests/unit/test_fvm_multi.cpp
@@ -0,0 +1,142 @@
+#include <fstream>
+
+#include "gtest.h"
+
+#include <common_types.hpp>
+#include <cell.hpp>
+#include <fvm_multicell.hpp>
+#include <util/range.hpp>
+
+#include "../test_util.hpp"
+#include "../test_common_cells.hpp"
+
+TEST(fvm_multi, cable)
+{
+    using namespace nest::mc;
+
+    nest::mc::cell cell=make_cell_ball_and_3sticks();
+
+    using fvm_cell = fvm::fvm_multicell<double, cell_lid_type>;
+
+    std::vector<fvm_cell::target_handle> targets;
+    std::vector<fvm_cell::detector_handle> detectors;
+    std::vector<fvm_cell::probe_handle> probes;
+
+    fvm_cell fvcell;
+    fvcell.initialize(util::singleton_view(cell), detectors, targets, probes);
+
+    auto& J = fvcell.jacobian();
+
+    // 1 (soma) + 3 (dendritic segments) × 4 compartments
+    EXPECT_EQ(cell.num_compartments(), 13u);
+
+    // assert that the matrix has one row for each compartment
+    EXPECT_EQ(J.size(), cell.num_compartments());
+
+    fvcell.setup_matrix(0.02);
+
+    // assert that the number of cv areas is the same as the matrix size
+    // i.e. both should equal the number of compartments
+    EXPECT_EQ(fvcell.cv_areas().size(), J.size());
+}
+
+TEST(fvm_multi, init)
+{
+    using namespace nest::mc;
+
+    nest::mc::cell cell = make_cell_ball_and_stick();
+
+    const auto m = cell.model();
+    EXPECT_EQ(m.tree.num_segments(), 2u);
+
+    auto& soma_hh = cell.soma()->mechanism("hh");
+
+    soma_hh.set("gnabar", 0.12);
+    soma_hh.set("gkbar", 0.036);
+    soma_hh.set("gl", 0.0003);
+    soma_hh.set("el", -54.3);
+
+    // check that parameter values were set correctly
+    EXPECT_EQ(cell.soma()->mechanism("hh").get("gnabar").value, 0.12);
+    EXPECT_EQ(cell.soma()->mechanism("hh").get("gkbar").value, 0.036);
+    EXPECT_EQ(cell.soma()->mechanism("hh").get("gl").value, 0.0003);
+    EXPECT_EQ(cell.soma()->mechanism("hh").get("el").value, -54.3);
+
+    cell.segment(1)->set_compartments(10);
+
+    using fvm_cell = fvm::fvm_multicell<double, cell_lid_type>;
+    std::vector<fvm_cell::target_handle> targets;
+    std::vector<fvm_cell::detector_handle> detectors;
+    std::vector<fvm_cell::probe_handle> probes;
+
+    fvm_cell fvcell;
+    fvcell.initialize(util::singleton_view(cell), detectors, targets, probes);
+
+    auto& J = fvcell.jacobian();
+    EXPECT_EQ(J.size(), 11u);
+
+    fvcell.setup_matrix(0.01);
+}
+
+TEST(fvm_multi, multi_init)
+{
+    using namespace nest::mc;
+
+    nest::mc::cell cells[] = {
+        make_cell_ball_and_stick(),
+        make_cell_ball_and_3sticks()
+    };
+
+    EXPECT_EQ(cells[0].num_segments(), 2u);
+    EXPECT_EQ(cells[0].segment(1)->num_compartments(), 4u);
+    EXPECT_EQ(cells[1].num_segments(), 4u);
+    EXPECT_EQ(cells[1].segment(1)->num_compartments(), 4u);
+    EXPECT_EQ(cells[1].segment(2)->num_compartments(), 4u);
+    EXPECT_EQ(cells[1].segment(3)->num_compartments(), 4u);
+
+    cells[0].add_synapse({1, 0.4}, parameter_list("expsyn"));
+    cells[0].add_synapse({1, 0.4}, parameter_list("expsyn"));
+    cells[1].add_synapse({2, 0.4}, parameter_list("exp2syn"));
+    cells[1].add_synapse({3, 0.4}, parameter_list("expsyn"));
+
+    cells[1].add_detector({0, 0}, 3.3);
+
+    using fvm_cell = fvm::fvm_multicell<double, cell_lid_type>;
+    std::vector<fvm_cell::target_handle> targets(4);
+    std::vector<fvm_cell::detector_handle> detectors(1);
+    std::vector<fvm_cell::probe_handle> probes;
+
+    fvm_cell fvcell;
+    fvcell.initialize(cells, detectors, targets, probes);
+
+    auto& J = fvcell.jacobian();
+    EXPECT_EQ(J.size(), 5u+13u);
+
+    // check indices in instantiated mechanisms
+    for (const auto& mech: fvcell.mechanisms()) {
+        if (mech->name()=="hh") {
+            // HH on somas of two cells, with group compartment indices
+            // 0 and 5.
+            ASSERT_EQ(mech->node_index().size(), 2u);
+            EXPECT_EQ(mech->node_index()[0], 0u);
+            EXPECT_EQ(mech->node_index()[1], 5u);
+        }
+        if (mech->name()=="expsyn") {
+            // Three expsyn synapses, two in second compartment
+            // of dendrite segment of first cell, one in second compartment
+            // of last segment of second cell.
+            ASSERT_EQ(mech->node_index().size(), 3u);
+            EXPECT_EQ(mech->node_index()[0], 2u);
+            EXPECT_EQ(mech->node_index()[1], 2u);
+            EXPECT_EQ(mech->node_index()[2], 15u);
+        }
+        if (mech->name()=="exp2syn") {
+            // One exp2syn synapse, in second compartment
+            // of penultimate segment of second cell.
+            ASSERT_EQ(mech->node_index().size(), 1u);
+            EXPECT_EQ(mech->node_index()[0], 11u);
+        }
+    }
+
+    fvcell.setup_matrix(0.01);
+}
diff --git a/tests/unit/test_probe.cpp b/tests/unit/test_probe.cpp
index 13a12ad6bd067ed91b37f2867f22b0f6405ba89a..cf7562c94fbf9ddcf33e16308d418dbdddd0ebf7 100644
--- a/tests/unit/test_probe.cpp
+++ b/tests/unit/test_probe.cpp
@@ -3,7 +3,7 @@
 #include <common_types.hpp>
 #include <cell.hpp>
 #include <fvm_cell.hpp>
-#include <util/singleton.hpp>
+#include <util/range.hpp>
 
 TEST(probe, instantiation)
 {
diff --git a/tests/validation/validate_ball_and_stick.cpp b/tests/validation/validate_ball_and_stick.cpp
index 59e952de58de61ae32cdbfb721ac991ec2119f8b..2a5ec45ccd575dc5f12ad79f565996d584b41a22 100644
--- a/tests/validation/validate_ball_and_stick.cpp
+++ b/tests/validation/validate_ball_and_stick.cpp
@@ -3,10 +3,13 @@
 
 #include <common_types.hpp>
 #include <cell.hpp>
-#include <fvm_cell.hpp>
-#include <util/singleton.hpp>
+//#include <fvm_cell.hpp>
+#include <fvm_multicell.hpp>
+#include <util/range.hpp>
 
 #include "gtest.h"
+
+#include "../test_common_cells.hpp"
 #include "../test_util.hpp"
 #include "validation_data.hpp"
 
@@ -16,20 +19,7 @@ TEST(ball_and_stick, neuron_baseline)
     using namespace nest::mc;
     using namespace nlohmann;
 
-    nest::mc::cell cell;
-
-    // Soma with diameter 12.6157 um and HH channel
-    auto soma = cell.add_soma(12.6157/2.0);
-    soma->add_mechanism(hh_parameters());
-
-    // add dendrite of length 200 um and diameter 1 um with passive channel
-    auto dendrite = cell.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 200);
-    dendrite->add_mechanism(pas_parameters());
-
-    dendrite->mechanism("membrane").set("r_L", 100);
-
-    // add stimulus
-    cell.add_stimulus({1,1}, {5., 80., 0.3});
+    nest::mc::cell cell = make_cell_ball_and_stick();
 
     // load data from file
     auto cell_data = testing::g_validation_data.load("ball_and_stick.json");
@@ -83,16 +73,16 @@ TEST(ball_and_stick, neuron_baseline)
         }
     };
 
-    using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>;
+    using fvm_cell = fvm::fvm_multicell<double, cell_local_size_type>;
     std::vector<fvm_cell::detector_handle> detectors(cell.detectors().size());
     std::vector<fvm_cell::target_handle> targets(cell.synapses().size());
     std::vector<fvm_cell::probe_handle> probes(cell.probes().size());
 
     std::vector<result> results;
-    for(auto run_index=0u; run_index<cell_data.size(); ++run_index) {
+    for (auto run_index=0u; run_index<cell_data.size(); ++run_index) {
         auto& run = cell_data[run_index];
         int num_compartments = run["nseg"];
-        dendrite->set_compartments(num_compartments);
+        cell.segment(1)->set_compartments(num_compartments);
         std::vector<std::vector<double>> v(3);
 
         // make the lowered finite volume cell
@@ -164,26 +154,7 @@ TEST(ball_and_3stick, neuron_baseline)
     using namespace nest::mc;
     using namespace nlohmann;
 
-    nest::mc::cell cell;
-
-    // Soma with diameter 12.6157 um and HH channel
-    auto soma = cell.add_soma(12.6157/2.0);
-    soma->add_mechanism(hh_parameters());
-
-    // add dendrite of length 200 um and diameter 1 um with passive channel
-    std::vector<cable_segment*> dendrites;
-    dendrites.push_back(cell.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 100));
-    dendrites.push_back(cell.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100));
-    dendrites.push_back(cell.add_cable(1, segmentKind::dendrite, 0.5, 0.5, 100));
-
-    for(auto dend : dendrites) {
-        dend->add_mechanism(pas_parameters());
-        dend->mechanism("membrane").set("r_L", 100);
-    }
-
-    // add stimulus
-    cell.add_stimulus({2,1}, {5.,  80., 0.45});
-    cell.add_stimulus({3,1}, {40., 10.,-0.2});
+    nest::mc::cell cell = make_cell_ball_and_3sticks();
 
     // load data from file
     auto cell_data = testing::g_validation_data.load("ball_and_3stick.json");
@@ -237,7 +208,7 @@ TEST(ball_and_3stick, neuron_baseline)
         }
     };
 
-    using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>;
+    using fvm_cell = fvm::fvm_multicell<double, cell_local_size_type>;
     std::vector<fvm_cell::detector_handle> detectors(cell.detectors().size());
     std::vector<fvm_cell::target_handle> targets(cell.synapses().size());
     std::vector<fvm_cell::probe_handle> probes(cell.probes().size());
@@ -247,8 +218,10 @@ TEST(ball_and_3stick, neuron_baseline)
     for(auto run_index=0u; run_index<cell_data.size(); ++run_index) {
         auto& run = cell_data[run_index];
         int num_compartments = run["nseg"];
-        for(auto dend : dendrites) {
-            dend->set_compartments(num_compartments);
+        for (auto& seg: cell.segments()) {
+            if (seg->is_dendrite()) {
+                seg->set_compartments(num_compartments);
+            }
         }
         std::vector<std::vector<double>> v(3);
 
diff --git a/tests/validation/validate_soma.cpp b/tests/validation/validate_soma.cpp
index af4d32826acb12bcc522daf7aa3f84c80dcc6bb2..8952ba30c93d5c7121fa09c6092e281d39564eb4 100644
--- a/tests/validation/validate_soma.cpp
+++ b/tests/validation/validate_soma.cpp
@@ -4,10 +4,12 @@
 #include <common_types.hpp>
 #include <cell.hpp>
 #include <fvm_cell.hpp>
-#include <util/singleton.hpp>
+#include <util/range.hpp>
 
 #include "gtest.h"
+
 #include "../test_util.hpp"
+#include "../test_common_cells.hpp"
 #include "validation_data.hpp"
 
 // compares results with those generated by nrn/soma.py
@@ -17,15 +19,7 @@ TEST(soma, neuron_baseline)
     using namespace nest::mc;
     using namespace nlohmann;
 
-    nest::mc::cell cell;
-
-    // Soma with diameter 18.8um and HH channel
-    auto soma = cell.add_soma(18.8/2.0);
-    soma->mechanism("membrane").set("r_L", 123); // no effect for single compartment cell
-    soma->add_mechanism(hh_parameters());
-
-    // add stimulus to the soma
-    cell.add_stimulus({0,0.5}, {10., 100., 0.1});
+    nest::mc::cell cell = make_cell_soma_only();
 
     // make the lowered finite volume cell
     using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>;
@@ -86,15 +80,7 @@ TEST(soma, convergence)
 {
     using namespace nest::mc;
 
-    nest::mc::cell cell;
-
-    // Soma with diameter 18.8um and HH channel
-    auto soma = cell.add_soma(18.8/2.0);
-    soma->mechanism("membrane").set("r_L", 123); // no effect for single compartment cell
-    soma->add_mechanism(hh_parameters());
-
-    // add stimulus to the soma
-    cell.add_stimulus({0,0.5}, {10., 100., 0.1});
+    nest::mc::cell cell = make_cell_soma_only();
 
     // make the lowered finite volume cell
     using fvm_cell = fvm::fvm_cell<double, cell_local_size_type>;
diff --git a/tests/validation/validate_synapses.cpp b/tests/validation/validate_synapses.cpp
index 9aa90abef6f6178a6285f9a1d5db2e9d1cb1d4f1..d1de3df06945b33466ccbbc105d412196c0a5f0b 100644
--- a/tests/validation/validate_synapses.cpp
+++ b/tests/validation/validate_synapses.cpp
@@ -8,6 +8,7 @@
 #include <cell_group.hpp>
 #include <fvm_cell.hpp>
 #include <mechanism_interface.hpp>
+#include <util/range.hpp>
 
 #include "gtest.h"
 #include "../test_util.hpp"
@@ -108,7 +109,7 @@ void run_neuron_baseline(const char* syn_type, const char* data_file)
         std::vector<std::vector<double>> v(2);
 
         // make the lowered finite volume cell
-        cell_group<lowered_cell> group(0, cell);
+        cell_group<lowered_cell> group(0, util::singleton_view(cell));
 
         // add the 3 spike events to the queue
         group.enqueue_events(synthetic_events);