diff --git a/miniapp/CMakeLists.txt b/miniapp/CMakeLists.txt
index 4aee39a3d82b045b8aa84c449ab4401d0d0a09b1..07ce5d13502fedb9e7343d5676236836692aa7dd 100644
--- a/miniapp/CMakeLists.txt
+++ b/miniapp/CMakeLists.txt
@@ -3,8 +3,9 @@ set(HEADERS
 set(MINIAPP_SOURCES
     #    mpi.cpp
     io.cpp
-    miniapp.cpp
+#    miniapp.cpp
     recipes.cpp
+    model.cpp
 )
 
 add_executable(miniapp.exe ${MINIAPP_SOURCES} ${HEADERS})
diff --git a/miniapp/model.cpp b/miniapp/model.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bf7ae4385d62da3d8bebebbdd04f435bb8a826ca
--- /dev/null
+++ b/miniapp/model.cpp
@@ -0,0 +1,188 @@
+#include <cstdlib>
+#include <vector>
+
+#include "catypes.hpp"
+#include "cell.hpp"
+#include "cell_group.hpp"
+#include "communication/communicator.hpp"
+#include "communication/global_policy.hpp"
+#include "fvm_cell.hpp"
+#include "profiling/profiler.hpp"
+#include "threading/threading.hpp"
+
+namespace nest {
+namespace mc {
+
+struct model {
+    using cell_group = cell_group<fvm::fvm_cell<double, cell_local_size_type>>;
+
+    void reset() {
+        t_ = 0.;
+        // otherwise unimplemented
+        std::abort();
+    }
+
+    double run(double tuntil, double dt) {
+        while (t_<tuntil) {
+            auto tstep = std::min(t_+dt, tunitl);
+            threading::parallel_for::apply(
+                0u, cell_groups.size(),
+                [&](unsigned i) {
+                    auto &group = cell_group[i];
+
+                    util::profiler_enter("stepping","events");
+                    group.enqueue_events(communicator.queue(i));
+                    util::profiler_leave();
+
+                    group.advance(tstep, dt);
+
+                    util::profiler_enter("events");
+                    communicator.add_spikes(group.spikes());
+                    group.clear_spikes();
+                    util::profiler_leave(2);
+                });
+
+            util::profiler_enter("stepping", "exchange");
+            communicator.exchange();
+            util::profiler_leave(2);
+
+            t_ += delta;
+        }
+        return t_;
+    }
+
+    explicit model(const recipe &rec, float sample_dt) {
+        // crude load balancing:
+        auto num_domains = global_policy::size();
+        auto domain_id = global_policy::id();
+        auto num_cells = rec.num_cells();
+
+        cell_gid_type cell_from = (cell_gid_type)(num_cells*(domain_id/(double)num_domains));
+        cell_gid_type cell_to = (cell_gid_type)(num_cells*((domain_id+1)/(double)num_domains));
+
+        // construct cell groups (one cell per group) and attach samplers
+        cell_groups.resize(cell_to-cell_from);
+        samplers.resize(cell_to-cell_from);
+
+        threading::parallel_for::apply(cell_from, cell_to, 
+            [&](cell_gid_type i) {
+                util::profiler_enter("setup", "cells");
+                auto cell = cell_group(rec.get_cell(i));
+                auto idx = i-cell_from;
+                cell_groups[idx] = cell_group(cell);
+
+                cell_local_index_type j = 0;
+                for (const auto& probe: cell.probes()) {
+                    samplers[idx].emplace_back({i,j}, probe.kind, probe.location, sample_dt);
+                    const auto &sampler = samplers[idx].back();
+                    cell_groups[idx].add_sampler(sampler, sampler.next_sample_t());
+                }
+                util::profiler_leave(2);
+            });
+
+        // initialise communicator
+        communicator = communicator_type(cell_from, cell_to);
+    }
+
+private:
+    double t_ = 0.;
+    std::vector<cell_group> cell_groups;
+    std::vector<std::vector<sample_to_trace>> samplers;
+    communicator_type communicator;
+};
+
+
+// move sampler code to another source file...
+struct sample_trace {
+    struct sample_type {
+        float time;
+        double value;
+    };
+
+    std::string name;
+    std::string units;
+    cell_gid_type cell_gid;
+    cell_index_type probe_index;
+    std::vector<sample_type> samples;
+};
+
+struct sample_to_trace {
+    float next_sample_t() const { return t_next_sample_; }
+
+    optional<float> operator()(float t, double v) {
+        if (t<t_next_sample_) {
+            return t_next_sample_;
+        }
+
+        trace.samples.push_back({t,v});
+        return t_next_sample_+=sample_dt_;
+    }
+
+    sample_to_trace(cell_member_type probe_id,
+                    const std::string &name,
+                    const std::string &units,
+                    float dt,
+                    float t_start=0):
+        trace_{{name, units, probe_id.gid, probe_id.index}},
+        sample_dt_(dt),
+        t_next_sample_(t_start)
+    {}
+
+    sample_to_trace(cell_member_type probe_id,
+                    probeKind kind,
+                    segment_location loc,
+                    float dt,
+                    float t_start=0):
+        sample_to_trace(probe_id, "", "", dt, t_start)
+    {
+        std::string name = "";
+        std::string units = "";
+
+        switch (kind) {
+        case probeKind::mebrane_voltage:
+            name = "v";
+            units = "mV";
+            break;
+        case probeKind::mebrane_current:
+            name = "i";
+            units = "mA/cm^2";
+            break;
+        default: ;
+        }
+
+        trace_.name = name + (loc.segment? "dend": "soma");
+        trace_.units = units;
+    }
+
+    void write_trace(const std::string& prefix = "trace_") const {
+        // do not call during simulation: thread-unsafe access to traces.
+        auto path = prefix + std::to_string(trace.cell_gid) +
+                    "." + std::to_string(trace.probe_index) + ".json";
+
+        nlohmann::json jrep;
+        jrep["name"] = trace.name;
+        jrep["units"] = trace.units;
+        jrep["cell"] = trace.cell_gid;
+        jrep["probe"] = trace.probe_index;
+
+        auto& jt = jrep["data"]["time"];
+        auto& jy = jrep["data"][trace.name];
+
+        for (const auto& sample: trace.samples) {
+            jt.push_back(sample.time);
+            jy.push_back(sample.value);
+        }
+        std::ofstream file(path);
+        file << std::setw(1) << jrep << std::endl;
+    }
+
+private:
+    sample_trace trace_;
+
+    float sample_dt_;
+    float t_next_sample_;
+
+};
+
+} // namespace mc
+} // namespace nest
diff --git a/src/cell_group.hpp b/src/cell_group.hpp
index fdefa72c5833c43901854ce7081623a754304bd8..6d3688a327dbe41d2c16b4d2626d64fae801db6d 100644
--- a/src/cell_group.hpp
+++ b/src/cell_group.hpp
@@ -3,6 +3,7 @@
 #include <cstdint>
 #include <vector>
 
+#include <catypes.hpp>
 #include <cell.hpp>
 #include <event_queue.hpp>
 #include <spike.hpp>
@@ -27,60 +28,41 @@ struct sampler {
 template <typename Cell>
 class cell_group {
 public:
-    using index_type = uint32_t;
+    using index_type = cell_gid_type;
     using cell_type = Cell;
     using value_type = typename cell_type::value_type;
     using size_type  = typename cell_type::value_type;
     using spike_detector_type = spike_detector<Cell>;
+    using source_id_type = cell_member_type;
 
     struct spike_source_type {
-        index_type index;
+        source_id_type source_id;
         spike_detector_type source;
     };
 
     cell_group() = default;
 
-    cell_group(const cell& c) :
-        cell_{c}
+    cell_group(cell_gid_type gid, const cell& c) :
+        gid_base_{gid}, cell_{c}
     {
         cell_.voltage()(memory::all) = -65.;
         cell_.initialize();
 
+        source_id_type source_id={gid_base_,0};
         for (auto& d : c.detectors()) {
-            spike_sources_.push_back( {
-                0u, spike_detector_type(cell_, d.location, d.threshold, 0.f)
+            ++source_id.index;
+            spike_sources_.push_back({
+                source_id, spike_detector_type(cell_, d.location, d.threshold, 0.f)
             });
         }
     }
 
-    void set_source_gids(index_type gid) {
-        for (auto& s : spike_sources_) {
-            s.index = gid++;
-        }
-    }
-
-    void set_target_gids(index_type lid) {
-        first_target_gid_ = lid;
-    }
-
-    index_type num_probes() const {
-        return cell_.num_probes();
-    }
-
-    void set_probe_gids(index_type gid) {
-        first_probe_gid_ = gid;
-    }
-
-    std::pair<index_type, index_type> probe_gid_range() const {
-        return { first_probe_gid_, first_probe_gid_+cell_.num_probes() };
-    }
-
     void advance(double tfinal, double dt) {
         while (cell_.time()<tfinal) {
             // take any pending samples
             float cell_time = cell_.time();
 
-                nest::mc::util::profiler_enter("sampling");
+            util::profiler_enter("sampling");
             while (auto m = sample_events_.pop_if_before(cell_time)) {
                 auto& sampler = samplers_[m->sampler_index];
                 EXPECTS((bool)sampler.sample);
@@ -92,7 +74,7 @@ public:
                     sample_events_.push(*m);
                 }
             }
-                nest::mc::util::profiler_leave();
+            util::profiler_leave();
 
             // look for events in the next time step
             auto tstep = std::min(tfinal, cell_.time()+dt);
@@ -105,11 +87,11 @@ public:
                 std::cerr << "warning: solution out of bounds\n";
             }
 
-                nest::mc::util::profiler_enter("events");
+            util::profiler_enter("events");
             // check for new spikes
             for (auto& s : spike_sources_) {
                 if (auto spike = s.source.test(cell_, cell_.time())) {
-                    spikes_.push_back({s.index, spike.get()});
+                    spikes_.push_back({s.source_id, spike.get()});
                 }
             }
 
@@ -123,7 +105,7 @@ public:
                     cell_.apply_event(e.get());
                 }
             }
-                nest::mc::util::profiler_leave();
+            util::profiler_leave();
         }
 
     }
@@ -136,10 +118,8 @@ public:
         }
     }
 
-    const std::vector<spike<index_type>>&
-    spikes() const {
-        return spikes_;
-    }
+    const std::vector<spike<source_id_type>>&
+    spikes() const { return spikes_; }
 
     cell_type&       cell()       { return cell_; }
     const cell_type& cell() const { return cell_; }
@@ -160,7 +140,8 @@ public:
     }
 
 private:
-
+    /// gid of first cell in group
+    cell_gid_type gid_base_;
 
     /// the lowered cell state (e.g. FVM) of the cell
     cell_type cell_;
@@ -169,7 +150,7 @@ private:
     std::vector<spike_source_type> spike_sources_;
 
     //. spikes that are generated
-    std::vector<spike<index_type>> spikes_;
+    std::vector<spike<source_id_type>> spikes_;
 
     /// pending events to be delivered
     event_queue<postsynaptic_spike_event> events_;
diff --git a/src/communication/communicator.hpp b/src/communication/communicator.hpp
index 01cdb7ff6b93396650efecdc7d1de05ca666a93c..693407753950fde1a204620745cf815a8386ff7b 100644
--- a/src/communication/communicator.hpp
+++ b/src/communication/communicator.hpp
@@ -29,76 +29,33 @@ namespace communication {
 template <typename CommunicationPolicy>
 class communicator {
 public:
-    using id_type = uint32_t;
+    using id_type = cell_gid_type;
     using communication_policy_type = CommunicationPolicy;
 
     using spike_type = spike<id_type>;
 
     communicator() = default;
 
-    communicator(id_type n_groups, std::vector<id_type> target_counts) :
-        num_groups_local_(n_groups),
-        num_targets_local_(target_counts.size())
+    // 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)
     {
-        target_map_ = nest::mc::algorithms::make_index(target_counts);
-        num_targets_local_ = target_map_.back();
+        auto num_groups_local_ = cell_gid_to_-cell_gid_from_;
 
         // create an event queue for each target group
         events_.resize(num_groups_local_);
-
-        // make maps for converting lid to gid
-        target_gid_map_ = communication_policy_.make_map(num_targets_local_);
-        group_gid_map_  = communication_policy_.make_map(num_groups_local_);
-
-        // transform the target ids from lid to gid
-        auto first_target = target_gid_map_[domain_id()];
-        for (auto &id : target_map_) {
-            id += first_target;
-        }
-    }
-
-    id_type target_gid_from_group_lid(id_type lid) const {
-        EXPECTS(lid<num_groups_local_);
-        return target_map_[lid];
     }
 
-    id_type group_gid_from_group_lid(id_type lid) const {
-        EXPECTS(lid<num_groups_local_);
-        return group_gid_map_[domain_id()] + lid;
-    }
 
     void add_connection(connection con) {
         EXPECTS(is_local_target(con.destination()));
         connections_.push_back(con);
     }
 
-    bool is_local_target(id_type gid) {
-        return gid>=target_gid_map_[domain_id()]
-            && gid<target_gid_map_[domain_id()+1];
-    }
-
-    bool is_local_group(id_type gid) {
-        return gid>=group_gid_map_[domain_id()]
-            && gid<group_gid_map_[domain_id()+1];
-    }
-
-    /// return the global id of the first group in domain d
-    /// the groups in domain d are in the contiguous half open range
-    ///     [domain_first_group(d), domain_first_group(d+1))
-    id_type group_gid_first(int d) const {
-        return group_gid_map_[d];
-    }
-
-    id_type target_lid(id_type gid) {
-        EXPECTS(is_local_group(gid)); 
-
-        return gid - target_gid_map_[domain_id()];
-    }
-
-    id_type group_lid(id_type gid) {
-        EXPECTS(is_local_group(gid));
-
-        return gid - group_gid_map_[domain_id()];
+    bool is_local_cell(id_type gid) const {
+        return gid>=cell_gid_from_ && gid<cell_gid_to_;
     }
 
     // builds the optimized data structure
@@ -117,19 +74,6 @@ public:
         return communication_policy_.min(local_min);
     }
 
-    // return the local group index of the group which hosts the target with
-    // global id gid
-    id_type local_group_from_global_target(id_type gid) {
-        // assert that gid is in range
-        EXPECTS(is_local_target(gid));
-
-        return
-            std::distance(
-                target_map_.begin(),
-                std::upper_bound(target_map_.begin(), target_map_.end(), gid)
-            ) - 1;
-    }
-
     void add_spike(spike_type s) {
         thread_spikes().push_back(s);
     }
@@ -144,7 +88,6 @@ public:
     }
 
     void exchange() {
-
         // global all-to-all to gather a local copy of the global spike list
         // on each node
         //profiler_.enter("global exchange");
@@ -170,7 +113,7 @@ public:
 
             // generate an event for each target
             for (auto it=targets.first; it!=targets.second; ++it) {
-                auto gidx = local_group_from_global_target(it->destination());
+                auto gidx = it->destination().gid - cell_gid_from_;
 
                 events_[gidx].push_back(it->make_event(spike));
             }
@@ -181,18 +124,7 @@ public:
         //profiler_.leave(); // event generation
     }
 
-    uint64_t num_spikes() const
-    {
-        return num_spikes_;
-    }
-
-    int domain_id() const {
-        return communication_policy_.id();
-    }
-
-    int num_domains() const {
-        return communication_policy_.size();
-    }
+    uint64_t num_spikes() const { return num_spikes_; }
 
     const std::vector<postsynaptic_spike_event>& queue(int i) const {
         return events_[i];
@@ -242,27 +174,9 @@ private:
     std::vector<connection> connections_;
     std::vector<std::vector<postsynaptic_spike_event>> events_;
 
-    // local target group i has targets in the half open range
-    //      [target_map_[i], target_map_[i+1])
-    std::vector<id_type> target_map_;
-
     // for keeping track of how time is spent where
     //util::Profiler profiler_;
 
-    // the number of groups and targets handled by this communicator
-    id_type num_groups_local_;
-    id_type num_targets_local_;
-
-    // index maps for the global distribution of groups and targets
-
-    // communicator i has the groups in the half open range :
-    //      [group_gid_map_[i], group_gid_map_[i+1])
-    std::vector<id_type> group_gid_map_;
-
-    // communicator i has the targets in the half open range :
-    //      [target_gid_map_[i], target_gid_map_[i+1])
-    std::vector<id_type> target_gid_map_;
-
     communication_policy_type communication_policy_;
 
     uint64_t num_spikes_ = 0u;
diff --git a/src/spike.hpp b/src/spike.hpp
index b0b5a9bbb973408ee7300c2b6d169537d6c9987e..2779738987d62daf5d1da834a5667c79b4e57f58 100644
--- a/src/spike.hpp
+++ b/src/spike.hpp
@@ -6,13 +6,10 @@
 namespace nest {
 namespace mc {
 
-template <
-    typename I,
-    typename = typename std::enable_if<std::is_integral<I>::value>
->
+template <typename I>
 struct spike {
     using id_type = I;
-    id_type source = 0;
+    id_type source = id_type{};
     float time = -1.;
 
     spike() = default;
diff --git a/tests/unit/test_cell_group.cpp b/tests/unit/test_cell_group.cpp
index 033cd3109b57d1a74362a2bd0a9424027567d0a5..bdd8bcbc53045d437a378a660d00ffc83f7d48a7 100644
--- a/tests/unit/test_cell_group.cpp
+++ b/tests/unit/test_cell_group.cpp
@@ -1,5 +1,3 @@
-#include <limits>
-
 #include "gtest.h"
 
 #include <catypes.hpp>
@@ -36,7 +34,7 @@ TEST(cell_group, test)
 
     using cell_type = cell_group<fvm::fvm_cell<double, cell_local_size_type>>;
 
-    auto cell = cell_type{make_cell()};
+    auto cell = cell_type{0, make_cell()};
 
     cell.advance(50, 0.01);
 
diff --git a/tests/validation/validate_synapses.cpp b/tests/validation/validate_synapses.cpp
index c95f5af1d93efaf4a9b5794ef84e530e1d1196ec..e33848d5439ef41257a643be69eede914cfda077 100644
--- a/tests/validation/validate_synapses.cpp
+++ b/tests/validation/validate_synapses.cpp
@@ -105,9 +105,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(cell);
-        group.set_source_gids(0);
-        group.set_target_gids(0);
+        cell_group<lowered_cell> group(0, cell);
 
         // add the 3 spike events to the queue
         group.enqueue_events(synthetic_events);