diff --git a/miniapps/miniapp/miniapp_recipes.cpp b/miniapps/miniapp/miniapp_recipes.cpp
index cae791fcb4761485409015399d6e417b288c3100..9ab4cf3ae0a46f36fb769a34f3de7bdf0ae7fec1 100644
--- a/miniapps/miniapp/miniapp_recipes.cpp
+++ b/miniapps/miniapp/miniapp_recipes.cpp
@@ -5,6 +5,7 @@
 
 #include <cell.hpp>
 #include <dss_cell_description.hpp>
+#include <event_generator.hpp>
 #include <rss_cell.hpp>
 #include <morphology.hpp>
 #include <util/debug.hpp>
@@ -175,6 +176,10 @@ public:
         }
     }
 
+    std::vector<event_generator_ptr> event_generators(cell_gid_type) const override {
+        return {};
+    }
+
 protected:
     template <typename RNG>
     cell_connection draw_connection_params(RNG& rng) const {
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4e1b4db31a6c17b18f59f812d625f0c4d3ead903..fd29c43e070fbac8d35fe9e1bbc45934a1e72838 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -9,6 +9,7 @@ set(BASE_SOURCES
     hardware/memory.cpp
     hardware/node_info.cpp
     hardware/power.cpp
+    merge_events.cpp
     model.cpp
     morphology.cpp
     partition_load_balance.cpp
diff --git a/src/common_types.hpp b/src/common_types.hpp
index f817ea6e9cd92ead859f38d5a4ddfb24100375ad..2cc3203454943f20691caef3ffa109eaa4948c3d 100644
--- a/src/common_types.hpp
+++ b/src/common_types.hpp
@@ -7,6 +7,7 @@
 
 #include <cstddef>
 #include <functional>
+#include <limits>
 #include <iosfwd>
 #include <type_traits>
 
@@ -53,6 +54,7 @@ DEFINE_LEXICOGRAPHIC_ORDERING(cell_member_type,(a.gid,a.index),(b.gid,b.index))
 // For storing time values [ms]
 
 using time_type = float;
+constexpr time_type max_time = std::numeric_limits<time_type>::max();
 
 // Extra contextual information associated with a probe.
 
diff --git a/src/event_generator.hpp b/src/event_generator.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a461198c9f599e297ac4d5449659250de62fbbb
--- /dev/null
+++ b/src/event_generator.hpp
@@ -0,0 +1,243 @@
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <random>
+
+#include <common_types.hpp>
+#include <event_queue.hpp>
+#include <util/range.hpp>
+#include <util/rangeutil.hpp>
+
+namespace arb {
+
+// An event_generator generates a sequence of events to be delivered to a cell.
+// The sequence of events is always in ascending order, i.e. each event will be
+// greater than the event that proceded it, where events are ordered by:
+//  - delivery time;
+//  - then target id for events with the same delivery time;
+//  - then weight for events with the same delivery time and target.
+struct event_generator {
+    // Return the next event in the stream.
+    // Returns the same event if called multiple times without calling pop().
+    virtual postsynaptic_spike_event next() = 0;
+
+    // Move the generator to the next event in the stream.
+    virtual void pop() = 0;
+
+    // Reset the generator to the same state that it had on construction.
+    virtual void reset() = 0;
+
+    // Update state of the generator such that the event returned by next() is
+    // the first event with delivery time >= t.
+    virtual void advance(time_type t) = 0;
+
+    virtual ~event_generator() {};
+};
+
+inline
+postsynaptic_spike_event terminal_pse() {
+    return postsynaptic_spike_event{cell_member_type{0,0}, max_time, 0};
+}
+
+// Generator that feeds events that are specified with a vector.
+// Makes a copy of the input sequence of events.
+struct vector_backed_generator: public event_generator {
+    using pse = postsynaptic_spike_event;
+    vector_backed_generator(pse_vector events):
+        events_(std::move(events)),
+        it_(events_.begin())
+    {
+        if (!std::is_sorted(events_.begin(), events_.end())) {
+            util::sort(events_);
+        }
+    }
+
+    postsynaptic_spike_event next() override {
+        return it_==events_.end()? terminal_pse(): *it_;
+    }
+
+    void pop() override {
+        if (it_!=events_.end()) {
+            ++it_;
+        }
+    }
+
+    void reset() override {
+        it_ = events_.begin();
+    }
+
+    void advance(time_type t) override {
+        it_ = std::lower_bound(events_.begin(), events_.end(), t, event_time_less());
+    }
+
+private:
+    std::vector<postsynaptic_spike_event> events_;
+    std::vector<postsynaptic_spike_event>::const_iterator it_;
+};
+
+// Generator for events in a generic sequence.
+// The generator keeps a reference to a Seq, i.e. it does not own the sequence.
+// Care must be taken to avoid lifetime issues, to ensure that the generator
+// does not outlive the sequence.
+template <typename Seq>
+struct seq_generator: public event_generator {
+    using pse = postsynaptic_spike_event;
+    seq_generator(Seq& events):
+        events_(events),
+        it_(std::begin(events_))
+    {
+        EXPECTS(std::is_sorted(events_.begin(), events_.end()));
+    }
+
+    postsynaptic_spike_event next() override {
+        return it_==events_.end()? terminal_pse(): *it_;
+    }
+
+    void pop() override {
+        if (it_!=events_.end()) {
+            ++it_;
+        }
+    }
+
+    void reset() override {
+        it_ = events_.begin();
+    }
+
+    void advance(time_type t) override {
+        it_ = std::lower_bound(events_.begin(), events_.end(), t, event_time_less());
+    }
+
+private:
+
+    const Seq& events_;
+    typename Seq::const_iterator it_;
+};
+
+// Generates a set of regularly spaced events:
+//  * with delivery times t=t_start+n*dt, ∀ t ∈ [t_start, t_stop)
+//  * with a set target and weight
+struct regular_generator: public event_generator {
+    using pse = postsynaptic_spike_event;
+
+    regular_generator(cell_member_type target,
+                      float weight,
+                      time_type tstart,
+                      time_type dt,
+                      time_type tstop=max_time):
+        target_(target),
+        weight_(weight),
+        step_(0),
+        t_start_(tstart),
+        dt_(dt),
+        t_stop_(tstop)
+    {}
+
+    postsynaptic_spike_event next() override {
+        const auto t = time();
+        return t<t_stop_?
+            postsynaptic_spike_event{target_, t, weight_}:
+            terminal_pse();
+    }
+
+    void pop() override {
+        ++step_;
+    }
+
+    void advance(time_type t0) override {
+        t0 = std::max(t0, t_start_);
+        step_ = (t0-t_start_)/dt_;
+
+        // Finding the smallest value for step_ that satisfies the condition
+        // that time() >= t0 is unfortunately a horror show because floating
+        // point precission.
+        while (step_ && time()>=t0) {
+            --step_;
+        }
+        while (time()<t0) {
+            ++step_;
+        }
+    }
+
+    void reset() override {
+        step_ = 0;
+    }
+
+private:
+    time_type time() const {
+        return t_start_ + step_*dt_;
+    }
+
+    cell_member_type target_;
+    float weight_;
+    std::size_t step_;
+    time_type t_start_;
+    time_type dt_;
+    time_type t_stop_;
+};
+
+// Generates a stream of events at times described by a Poisson point process
+// with rate_per_ms spikes per ms.
+template <typename RandomNumberEngine>
+struct poisson_generator: public event_generator {
+    using pse = postsynaptic_spike_event;
+
+    poisson_generator(cell_member_type target,
+                      float weight,
+                      RandomNumberEngine rng,
+                      time_type tstart,
+                      time_type rate_per_ms,
+                      time_type tstop=max_time):
+        exp_(rate_per_ms),
+        reset_state_(std::move(rng)),
+        target_(target),
+        weight_(weight),
+        t_start_(tstart),
+        t_stop_(tstop)
+    {
+        reset();
+    }
+
+    postsynaptic_spike_event next() override {
+        return next_<t_stop_?
+            postsynaptic_spike_event{target_, next_, weight_}:
+            terminal_pse();
+    }
+
+    void pop() override {
+        next_ += exp_(rng_);
+    }
+
+    void advance(time_type t0) override {
+        while (next_<t0) {
+            pop();
+        }
+    }
+
+    void reset() override {
+        rng_ = reset_state_;
+        next_ = t_start_;
+        pop();
+    }
+
+private:
+    std::exponential_distribution<time_type> exp_;
+    RandomNumberEngine rng_;
+    const RandomNumberEngine reset_state_;
+    const cell_member_type target_;
+    const float weight_;
+    const time_type t_start_;
+    const time_type t_stop_;
+    time_type next_;
+
+};
+
+using event_generator_ptr = std::unique_ptr<event_generator>;
+
+template <typename T, typename... Args>
+event_generator_ptr make_event_generator(Args&&... args) {
+    return event_generator_ptr(new T(std::forward<Args>(args)...));
+}
+
+} // namespace arb
+
diff --git a/src/merge_events.cpp b/src/merge_events.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5cda151b3e4be3bbfc64e1cc7c1891a626fdfb7
--- /dev/null
+++ b/src/merge_events.cpp
@@ -0,0 +1,205 @@
+#include <set>
+#include <vector>
+
+#include <backends.hpp>
+#include <cell_group.hpp>
+#include <cell_group_factory.hpp>
+#include <domain_decomposition.hpp>
+#include <merge_events.hpp>
+#include <model.hpp>
+#include <recipe.hpp>
+#include <util/filter.hpp>
+#include <util/span.hpp>
+#include <util/unique_any.hpp>
+#include <profiling/profiler.hpp>
+
+namespace arb {
+
+namespace impl {
+
+// The tournament tree data structure is used to merge k sorted lists of events.
+// See online for high-level information about tournament trees.
+//
+// This implementation maintains a heap-like data structure, with entries of type:
+//      std::pair<unsigned, post_synaptic_event>
+// where the unsigned ∈ [0, k-1] is the id of the list from which the event was
+// drawn. The id is stored so that the operation of removing the most recent event
+// knows which leaf node needs to be updated (i.e. the leaf node of the list from
+// which the most recent event was drawn).
+//
+// unsigned is used for storing the index, because if drawing events from more
+// event generators than can be counted using an unsigned a complete redesign
+// will be needed.
+tourney_tree::tourney_tree(std::vector<event_generator_ptr>& input):
+    input_(input),
+    n_lanes_(input_.size())
+{
+    // Must have at least 1 queue
+    EXPECTS(n_lanes_);
+    // Maximum value in unsigned limits how many queues we can have
+    EXPECTS(n_lanes_<(1u<<(sizeof(unsigned)*8u-1u)));
+
+    leaves_ = next_power_2(n_lanes_);
+    nodes_ = 2u*(leaves_-1u)+1u; // 2*l-1 with overflow protection
+
+    // Allocate space for the tree nodes
+    heap_.resize(nodes_);
+    // Set the leaf nodes
+    for (auto i=0u; i<leaves_; ++i) {
+        heap_[leaf(i)] = i<n_lanes_?
+            key_val(i, input[i]->next()):
+            key_val(i, terminal_pse()); // null leaf node
+    }
+    // Walk the tree to initialize the non-leaf nodes
+    setup(0);
+}
+
+void tourney_tree::print() const {
+    auto nxt=1u;
+    for (auto i=0u; i<nodes_; ++i) {
+        if (i==nxt-1) { nxt*=2; std::cout << "\n";}
+        std::cout << "{" << heap_[i].first << "," << heap_[i].second << "}\n";
+    }
+}
+
+bool tourney_tree::empty() const {
+    return event(0).time == max_time;
+}
+
+bool tourney_tree::empty(time_type t) const {
+    return event(0).time >= t;
+}
+
+postsynaptic_spike_event tourney_tree::head() const {
+    return event(0);
+}
+
+// Remove the smallest (most recent) event from the tree, then update the
+// tree so that head() returns the next event.
+void tourney_tree::pop() {
+    unsigned lane = id(0);
+    unsigned i = leaf(lane);
+    // draw the next event from the input lane
+    input_[lane]->pop();
+    // place event the leaf node for this lane
+    event(i) = input_[lane]->next();
+
+    // re-heapify the tree with a single walk from leaf to root
+    while ((i=parent(i))) {
+        merge_up(i);
+    }
+    merge_up(0); // handle the root
+}
+
+void tourney_tree::setup(unsigned i) {
+    if (is_leaf(i)) return;
+    setup(left(i));
+    setup(right(i));
+    merge_up(i);
+};
+
+// Update the value at node i of the tree to be the smallest
+// of its left and right children.
+// The result is undefined for leaf nodes.
+void tourney_tree::merge_up(unsigned i) {
+    const auto l = left(i);
+    const auto r = right(i);
+    heap_[i] = event(l)<event(r)? heap_[l]: heap_[r];
+}
+
+// The tree is stored using the standard heap indexing scheme.
+
+unsigned tourney_tree::parent(unsigned i) const {
+    return (i-1)>>1;
+}
+unsigned tourney_tree::left(unsigned i) const {
+    return (i<<1) + 1;
+}
+unsigned tourney_tree::right(unsigned i) const {
+    return left(i)+1;
+}
+unsigned tourney_tree::leaf(unsigned i) const {
+    return i+leaves_-1;
+}
+bool tourney_tree::is_leaf(unsigned i) const {
+    return i>=leaves_-1;
+}
+const unsigned& tourney_tree::id(unsigned i) const {
+    return heap_[i].first;
+}
+postsynaptic_spike_event& tourney_tree::event(unsigned i) {
+    return heap_[i].second;
+}
+const postsynaptic_spike_event& tourney_tree::event(unsigned i) const {
+    return heap_[i].second;
+}
+
+unsigned tourney_tree::next_power_2(unsigned x) const {
+    unsigned n = 1;
+    while (n<x) n<<=1;
+    return n;
+}
+
+} // namespace impl
+
+void merge_events(time_type t0, time_type t1,
+                  const pse_vector& lc, pse_vector& events,
+                  std::vector<event_generator_ptr>& generators,
+                  pse_vector& lf)
+{
+    using std::distance;
+    using std::lower_bound;
+
+    // Sort events from the communicator in place.
+    util::sort(events);
+
+    // Clear lf to store merged list.
+    lf.clear();
+
+    // Merge the incoming event sequences into a single vector in lf
+    if (generators.size()) {
+        // Handle the case where the cell has event generators.
+        // This is performed in two steps:
+        //  Step 1 : Use tournament tree to merge all events in lc, events and
+        //           generators to be delivered in the time interval [t₀, t₁).
+        //  Step 2 : Use std::merge to append events in lc and events with
+        //           delivery times in the interval [t₁, ∞).
+        EXPECTS(generators.size()>2u);
+
+        // Make an event generator with all the events in events.
+        generators[0] = make_event_generator<seq_generator<pse_vector>>(events);
+
+        // Make an event generator with all the events in lc with time >= t0
+        auto lc_it = lower_bound(lc.begin(), lc.end(), t0, event_time_less());
+        auto lc_range = util::make_range(lc_it, lc.end());
+        generators[1] = make_event_generator<seq_generator<decltype(lc_range)>>(lc_range);
+
+        // Perform k-way merge of all events in events, lc and the generators
+        // that are due to be delivered in the interval [t₀, t₁)
+        impl::tourney_tree tree(generators);
+        while (!tree.empty(t1)) {
+            lf.push_back(tree.head());
+            tree.pop();
+        }
+
+        // Find first event in lc with delivery time >= t1
+        lc_it = lower_bound(lc.begin(), lc.end(), t1, event_time_less());
+        // Find first event in events with delivery time >= t1
+        auto ev_it = lower_bound(events.begin(), events.end(), t1, event_time_less());
+        const auto m = lf.size();
+        const auto n = m + distance(lc_it, lc.end()) + distance(ev_it, events.end());
+        lf.resize(n);
+        std::merge(ev_it, events.end(), lc_it, lc.end(), lf.begin()+m);
+    }
+    else {
+        // Handle the case where the cell has no event generators: only events
+        // in lc and lf with delivery times >= t0 must be merged, which can be
+        // handles with a single call to std::merge.
+        auto pos = std::lower_bound(lc.begin(), lc.end(), t0, event_time_less());
+        lf.resize(events.size()+distance(pos, lc.end()));
+        std::merge(events.begin(), events.end(), pos, lc.end(), lf.begin());
+    }
+}
+
+} // namespace arb
+
diff --git a/src/merge_events.hpp b/src/merge_events.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..bba08cbd54f651d26043080c3f27fb8a1e2d1c8e
--- /dev/null
+++ b/src/merge_events.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <algorithm>
+#include <vector>
+
+#include <event_generator.hpp>
+#include <event_queue.hpp>
+#include <profiling/profiler.hpp>
+
+namespace arb {
+
+// merge_events generates a sorted list of postsynaptic events that are to be
+// delivered after the current epoch ends. It merges events from multiple
+// sources:
+//  lc : the list of currently enqueued events
+//  events : an unsorted list of events from the communicator
+//  generators : a set of event_generators
+//
+// The time intervales are illustrated below, along with the range of times
+// for events in each of lc, events and generators
+//  * t the start of the current epoch (not required to perform the merge).
+//  * tâ‚€ the start of the next epoch
+//  * t₁ the end of the next epoch
+//
+//   t      t₀     t₁
+//   |------|------|
+//
+//   [----------------------] lc
+//          [---------------] events
+//          [------) generators
+//
+// The output list, stored in lf, will contain all the following:
+//  * all events in events list
+//  * events in lc with time >= tâ‚€
+//  * events from each generator with time < t₁
+// All events in lc that are to be delivered before tâ‚€ are discared, along with
+// events from generators after t₁. The generators are left in a state where
+// the next event in the generator is the first event with deliver time >= t₁.
+void merge_events(time_type t0,
+                  time_type t1,
+                  const pse_vector& lc,
+                  pse_vector& events,
+                  std::vector<event_generator_ptr>& generators,
+                  pse_vector& lf);
+
+namespace impl {
+    // The tournament tree is used internally by the merge_events method, and
+    // it is not intended for use elsewhere. It is exposed here for unit testing
+    // of its functionality.
+    class tourney_tree {
+        using key_val = std::pair<unsigned, postsynaptic_spike_event>;
+
+    public:
+        tourney_tree(std::vector<event_generator_ptr>& input);
+        bool empty() const;
+        bool empty(time_type t) const;
+        postsynaptic_spike_event head() const;
+        void pop();
+        void print() const;
+
+    private:
+        void setup(unsigned i);
+        void merge_up(unsigned i);
+        void update_lane(unsigned lane);
+        unsigned parent(unsigned i) const;
+        unsigned left(unsigned i) const;
+        unsigned right(unsigned i) const;
+        unsigned leaf(unsigned i) const;
+        bool is_leaf(unsigned i) const;
+        const unsigned& id(unsigned i) const;
+        postsynaptic_spike_event& event(unsigned i);
+        const postsynaptic_spike_event& event(unsigned i) const;
+        unsigned next_power_2(unsigned x) const;
+
+        std::vector<key_val> heap_;
+        const std::vector<event_generator_ptr>& input_;
+        unsigned leaves_;
+        unsigned nodes_;
+        unsigned n_lanes_;
+    };
+}
+
+} // namespace arb
diff --git a/src/model.cpp b/src/model.cpp
index 0495cbdab42ff536c94acad09b5420d796429df6..87144a3461e63f9076ed4ec7aa1ef4c0326fa555 100644
--- a/src/model.cpp
+++ b/src/model.cpp
@@ -5,6 +5,7 @@
 #include <cell_group.hpp>
 #include <cell_group_factory.hpp>
 #include <domain_decomposition.hpp>
+#include <merge_events.hpp>
 #include <model.hpp>
 #include <recipe.hpp>
 #include <util/filter.hpp>
@@ -14,15 +15,30 @@
 
 namespace arb {
 
-void merge_events(time_type tfinal, const pse_vector& lc, pse_vector& events, pse_vector& lf);
-
 model::model(const recipe& rec, const domain_decomposition& decomp):
     communicator_(rec, decomp)
 {
+    event_generators_.resize(communicator_.num_local_cells());
     cell_local_size_type lidx = 0;
-    for (auto i: util::make_span(0, decomp.groups.size())) {
-        for (auto gid: decomp.groups[i].gids) {
+    const auto& grps = decomp.groups;
+    for (auto i: util::make_span(0, grps.size())) {
+        for (auto gid: grps[i].gids) {
+            // Store mapping of gid to local cell index.
             gid_to_local_[gid] = lidx++;
+
+            // Set up the event generators for cell gid.
+            auto rec_gens = rec.event_generators(gid);
+            auto& gens = event_generators_[lidx];
+            if (rec_gens.size()) {
+                // Allocate two empty event generators that will be used to
+                // merge events from the communicator and those already queued
+                // for delivery in future epochs.
+                gens.reserve(2+rec_gens.size());
+                gens.resize(2);
+                for (auto& g: rec_gens) {
+                    gens.push_back(std::move(g));
+                }
+            }
         }
     }
 
@@ -56,6 +72,14 @@ void model::reset() {
         }
     }
 
+    for (auto& lane: event_generators_) {
+        for (auto& gen: lane) {
+            if (gen) {
+                gen->reset();
+            }
+        }
+    }
+
     communicator_.reset();
 
     current_spikes().clear();
@@ -120,8 +144,10 @@ time_type model::run(time_type tfinal, time_type dt) {
                 const auto epid = epoch_.id;
                 merge_events(
                     epoch_.tfinal,
+                    epoch_.tfinal+std::min(t_+t_interval, tfinal),
                     event_lanes(epid)[i],
                     events[i],
+                    event_generators_[i],
                     event_lanes(epid+1)[i]);
             });
         PL(2);
@@ -246,35 +272,4 @@ void model::inject_events(const pse_vector& events) {
     }
 }
 
-// Merge events that are to be delivered from two lists into a sorted list.
-// Events are sorted by delivery time, then target, then weight.
-//
-//  tfinal: The time at which the current epoch finishes. The output list, `lc`,
-//          will contain all events with delivery times equal to or greater than
-//          tfinal.
-//  lc: Sorted set of events to be delivered before and after `tfinal`.
-//  events: Unsorted list of events with delivery time greater than or equal to
-//      tfinal. May be modified by the call.
-//  lf: Will hold a list of all postsynaptic events in `events` and `lc` that
-//      have delivery times greater than or equal to `tfinal`.
-void merge_events(time_type tfinal, const pse_vector& lc, pse_vector& events, pse_vector& lf) {
-    // Merge the incoming events with events in lc that are not to be delivered
-    // in this epoch, and store the result in lf.
-
-    // STEP 1: sort events in place in events[l]
-    PE("sort");
-    util::sort(events);
-    PL();
-
-    // STEP 2: clear lf to store merged list
-    lf.clear();
-
-    // STEP 3: merge new events and future events from lc into lf
-    PE("merge");
-    auto pos = std::lower_bound(lc.begin(), lc.end(), tfinal, event_time_less());
-    lf.resize(events.size()+std::distance(pos, lc.end()));
-    std::merge(events.begin(), events.end(), pos, lc.end(), lf.begin());
-    PL();
-}
-
 } // namespace arb
diff --git a/src/model.hpp b/src/model.hpp
index b4abcdabe9576c17f316c797269ad5ba2143c8c4..268e31e99c594adacdb77209a26ec7168ec08b6a 100644
--- a/src/model.hpp
+++ b/src/model.hpp
@@ -70,6 +70,9 @@ private:
     time_type t_ = 0.;
     std::vector<cell_group_ptr> cell_groups_;
 
+    // one set of event_generators for each local cell
+    std::vector<std::vector<event_generator_ptr>> event_generators_;
+
     using local_spike_store_type = thread_private_spike_store;
     util::double_buffer<local_spike_store_type> local_spikes_;
 
diff --git a/src/recipe.hpp b/src/recipe.hpp
index 106fac4be60252528c0ec9f7da24ab5bc127ec8c..54ba235d3521a34ce46a7b840883c408384bd202 100644
--- a/src/recipe.hpp
+++ b/src/recipe.hpp
@@ -7,6 +7,7 @@
 
 #include <cell.hpp>
 #include <common_types.hpp>
+#include <event_generator.hpp>
 #include <util/unique_any.hpp>
 
 namespace arb {
@@ -63,6 +64,8 @@ public:
     virtual cell_size_type num_targets(cell_gid_type) const = 0;
     virtual cell_size_type num_probes(cell_gid_type) const = 0;
 
+    virtual std::vector<event_generator_ptr> event_generators(cell_gid_type) const = 0;
+
     virtual std::vector<cell_connection> connections_on(cell_gid_type) const = 0;
     virtual probe_info get_probe(cell_member_type probe_id) const = 0;
 
diff --git a/tests/global_communication/test_communicator.cpp b/tests/global_communication/test_communicator.cpp
index 147daead5124b2c7acc34022505f724be40efb38..21d7be0ed1429c7ea2c5716adcf9627f9580ee76 100644
--- a/tests/global_communication/test_communicator.cpp
+++ b/tests/global_communication/test_communicator.cpp
@@ -213,6 +213,10 @@ namespace {
                         1.0f)};     // delay
         }
 
+        std::vector<event_generator_ptr> event_generators(cell_gid_type) const override {
+            return {};
+        }
+
         probe_info get_probe(cell_member_type) const override {
             throw std::logic_error("no probes");
         }
@@ -282,6 +286,10 @@ namespace {
             return cons;
         }
 
+        std::vector<event_generator_ptr> event_generators(cell_gid_type) const override {
+            return {};
+        }
+
         probe_info get_probe(cell_member_type) const override {
             throw std::logic_error("no probes");
         }
diff --git a/tests/global_communication/test_domain_decomposition.cpp b/tests/global_communication/test_domain_decomposition.cpp
index 745d1641fbde5be54e30c632e698067f5f2960be..119dded9f51c4209faaf3f5df44af00bb91278c7 100644
--- a/tests/global_communication/test_domain_decomposition.cpp
+++ b/tests/global_communication/test_domain_decomposition.cpp
@@ -54,6 +54,10 @@ namespace {
             return {};
         }
 
+        std::vector<event_generator_ptr> event_generators(cell_gid_type) const override {
+            return {};
+        }
+
         probe_info get_probe(cell_member_type) const override {
             throw std::logic_error("no probes");
         }
diff --git a/tests/simple_recipes.hpp b/tests/simple_recipes.hpp
index 5417c0fbe9c12a04fb93a3a3e9d38dc83a980a1d..509f6dab90a7e4171b34a70fa47ff7a93e23238f 100644
--- a/tests/simple_recipes.hpp
+++ b/tests/simple_recipes.hpp
@@ -6,6 +6,7 @@
 #include <vector>
 
 #include <cell.hpp>
+#include <event_generator.hpp>
 #include <recipe.hpp>
 
 namespace arb {
@@ -19,6 +20,10 @@ public:
         return probes_.count(i)? probes_.at(i).size(): 0;
     }
 
+    std::vector<event_generator_ptr> event_generators(cell_gid_type) const override {
+        return {};
+    }
+
     std::vector<cell_connection> connections_on(cell_gid_type) const override {
         return {};
     }
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 7410c7249a933bd798bd79e508f4086fc655fd6c..11ad7927747e4489907c11347f73e1d9de5e929a 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -44,8 +44,9 @@ set(TEST_SOURCES
     test_domain_decomposition.cpp
     test_dss_cell_group.cpp
     test_either.cpp
-    test_event_queue.cpp
     test_event_binner.cpp
+    test_event_generators.cpp
+    test_event_queue.cpp
     test_filter.cpp
     test_fvm_multi.cpp
     test_mc_cell_group.cpp
diff --git a/tests/unit/test_domain_decomposition.cpp b/tests/unit/test_domain_decomposition.cpp
index 0b988fe37622f3ec8e78fa6efa5d9a54f721c7c2..c6cf69fa02b1f831a3fb33a047fb91a7c0e77979 100644
--- a/tests/unit/test_domain_decomposition.cpp
+++ b/tests/unit/test_domain_decomposition.cpp
@@ -46,6 +46,10 @@ namespace {
             return {};
         }
 
+        std::vector<event_generator_ptr> event_generators(cell_gid_type) const override {
+            return {};
+        }
+
         probe_info get_probe(cell_member_type) const override {
             throw std::logic_error("no probes");
         }
diff --git a/tests/unit/test_event_generators.cpp b/tests/unit/test_event_generators.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..76b6c6c30976e6ee0dd4bac2747bf5b0c123b546
--- /dev/null
+++ b/tests/unit/test_event_generators.cpp
@@ -0,0 +1,264 @@
+#include "../gtest.h"
+#include "common.hpp"
+
+#include <event_generator.hpp>
+#include <util/rangeutil.hpp>
+
+using namespace arb;
+using pse = postsynaptic_spike_event;
+
+namespace{
+    pse_vector draw(event_generator& gen, time_type t0, time_type t1) {
+        gen.reset();
+        gen.advance(t0);
+        pse_vector v;
+        while (gen.next().time<t1) {
+            v.push_back(gen.next());
+            gen.pop();
+        }
+        return v;
+    }
+}
+
+TEST(event_generators, vector_backed) {
+    std::vector<pse> in = {
+        {{0, 0}, 0.1, 1.0},
+        {{0, 0}, 1.0, 2.0},
+        {{0, 0}, 1.0, 3.0},
+        {{0, 0}, 1.5, 4.0},
+        {{0, 0}, 2.3, 5.0},
+        {{0, 0}, 3.0, 6.0},
+        {{0, 0}, 3.5, 7.0},
+    };
+
+    vector_backed_generator gen(in);
+
+    // Test pop, next and reset.
+    for (auto e: in) {
+        EXPECT_EQ(e, gen.next());
+        gen.pop();
+    }
+    gen.reset();
+    for (auto e: in) {
+        EXPECT_EQ(e, gen.next());
+        gen.pop();
+    }
+
+    // The loop above should have drained all events from gen, so we expect
+    // that the next() event will be the special terminal_pse event.
+    EXPECT_EQ(gen.next(), terminal_pse());
+}
+
+TEST(event_generators, regular) {
+    // make a regular generator that generates its first event at t=2ms and subsequent
+    // events regularly spaced 0.5 ms apart.
+    time_type t0 = 2.0;
+    time_type dt = 0.5;
+    cell_member_type target = {42, 3};
+    float weight = 3.14;
+
+    //regular_generator gen(t0, dt, target, weight);
+    regular_generator gen(target, weight, t0, dt);
+
+    // helper for building a set of 
+    auto expected = [&] (std::vector<time_type> times) {
+        pse_vector events;
+        for (auto t: times) {
+            events.push_back({target, t, weight});
+        }
+        return events;
+    };
+
+    // Test pop, next and reset.
+    for (auto e:  expected({2.0, 2.5, 3.0, 3.5, 4.0, 4.5})) {
+        EXPECT_EQ(e, gen.next());
+        gen.pop();
+    }
+    gen.reset();
+    for (auto e:  expected({2.0, 2.5, 3.0, 3.5, 4.0, 4.5})) {
+        EXPECT_EQ(e, gen.next());
+        gen.pop();
+    }
+    gen.reset();
+
+    // Test advance
+    gen.advance(10.1);
+    EXPECT_EQ(gen.next().time, time_type(10.5));
+    gen.advance(12);
+    EXPECT_EQ(gen.next().time, time_type(12));
+}
+
+// Test for rounding problems with large time values and the regular generator
+TEST(event_generator, regular_rounding) {
+    // make a regular generator that generates its first event at t=2ms and subsequent
+    // events regularly spaced 0.5 ms apart.
+    time_type t0 = 2.0;
+    time_type dt = 0.5;
+    cell_member_type target = {42, 3};
+    float weight = 3.14;
+
+    // Test for rounding problems with large time values.
+    // To better understand why this is an issue, uncomment the following:
+    //   float T = 1802667.0f, DT = 0.024999f;
+    //   std::size_t N = std::floor(T/DT);
+    //   std::cout << "T " << T << " DT " << DT << " N " << N
+    //             << " T-N*DT " << T - (N*DT) << " P " << (T - (N*DT))/DT  << "\n";
+    t0 = 1802667.0f;
+    dt = 0.024999f;
+    time_type int_len = 5*dt;
+    time_type t1 = t0 + int_len;
+    time_type t2 = t1 + int_len;
+    auto gen = regular_generator(target, weight, t0, dt);
+
+    // Take the interval I_a: t ∈ [t0, t2)
+    // And the two sub-interavls
+    //      I_l: t ∈ [t0, t1)
+    //      I_r: t ∈ [t1, t2)
+    // Such that I_a = I_l ∪ I_r.
+    // If we draw events from each interval then merge them, we expect same set
+    // of events as when we draw from that large interval.
+    pse_vector int_l = draw(gen, t0, t1);
+    pse_vector int_r = draw(gen, t1, t2);
+    pse_vector int_a = draw(gen, t0, t2);
+
+    pse_vector int_merged = int_l;
+    util::append(int_merged, int_r);
+
+    EXPECT_TRUE(int_l.front().time >= t0);
+    EXPECT_TRUE(int_l.back().time  <  t1);
+    EXPECT_TRUE(int_r.front().time >= t1);
+    EXPECT_TRUE(int_r.back().time  <  t2);
+    EXPECT_EQ(int_a, int_merged);
+    EXPECT_TRUE(std::is_sorted(int_a.begin(), int_a.end()));
+}
+
+TEST(event_generators, seq) {
+    std::vector<pse> in = {
+        {{0, 0}, 0.1, 1.0},
+        {{0, 0}, 1.0, 2.0},
+        {{0, 0}, 1.0, 3.0},
+        {{0, 0}, 1.5, 4.0},
+        {{0, 0}, 2.3, 5.0},
+        {{0, 0}, 3.0, 6.0},
+        {{0, 0}, 3.5, 7.0},
+    };
+
+    auto events = [&in] (int b, int e) {
+        return pse_vector(in.begin()+b, in.begin()+e);
+    };
+
+    seq_generator<pse_vector> gen(in);
+
+    // Test pop, next and reset.
+    for (auto e: in) {
+        EXPECT_EQ(e, gen.next());
+        gen.pop();
+    }
+    gen.reset();
+    for (auto e: in) {
+        EXPECT_EQ(e, gen.next());
+        gen.pop();
+    }
+    // The loop above should have drained all events from gen, so we expect
+    // that the next() event will be the special terminal_pse event.
+    EXPECT_EQ(gen.next(), terminal_pse());
+
+    gen.reset();
+
+    // Update of the input sequence, and run tests again to
+    // verify that results reflect the new set of input events.
+    in = {
+        {{0, 0}, 1.5, 4.0},
+        {{0, 0}, 2.3, 5.0},
+        {{0, 0}, 3.0, 6.0},
+        {{0, 0}, 3.5, 7.0},
+    };
+
+    // a range that includes all the events
+    EXPECT_EQ(in, draw(gen, 0, 4));
+
+
+    // a strict subset including the first event
+    EXPECT_EQ(events(0, 2), draw(gen, 0, 3));
+
+
+    // a strict subset including the last event
+    EXPECT_EQ(events(2, 4), draw(gen, 3.0, 5));
+
+
+    // subset that excludes first and last entries
+    EXPECT_EQ(events(1, 3), draw(gen, 2, 3.2));
+
+
+    // empty subset in the middle of range
+    EXPECT_EQ(pse_vector{}, draw(gen, 2, 2));
+
+
+    // empty subset before first event
+    EXPECT_EQ(pse_vector{}, draw(gen, 0, 0.05));
+
+
+    // empty subset after last event
+    EXPECT_EQ(pse_vector{}, draw(gen, 10, 11));
+
+}
+
+TEST(event_generators, poisson) {
+    std::mt19937_64 G;
+    using pgen = poisson_generator<std::mt19937_64>;
+
+    time_type t0 = 0;
+    time_type t1 = 10;
+    time_type lambda = 10; // expect 10 events per ms
+    cell_member_type target{4, 2};
+    float weight = 42;
+    pgen gen(target, weight, G, t0, lambda);
+
+    pse_vector int1;
+    while (gen.next().time<t1) {
+        int1.push_back(gen.next());
+        gen.pop();
+    }
+    // Test that the output is sorted
+    EXPECT_TRUE(std::is_sorted(int1.begin(), int1.end()));
+
+    // Reset and generate the same sequence of events
+    gen.reset();
+    pse_vector int2;
+    while (gen.next().time<t1) {
+        int2.push_back(gen.next());
+        gen.pop();
+    }
+
+    // Assert that the same sequence was generated
+    EXPECT_EQ(int1, int2);
+}
+
+// Test a poisson generator that has a tstop past which no events
+// should be generated.
+TEST(event_generators, poisson_terminates) {
+    std::mt19937_64 G;
+    using pgen = poisson_generator<std::mt19937_64>;
+
+    time_type t0 = 0;
+    time_type t1 = 10;
+    time_type t2 = 1e7; // pick a time far past the end of the interval [t0, t1)
+    time_type lambda = 10; // expect 10 events per ms
+    cell_member_type target{4, 2};
+    float weight = 42;
+    // construct generator with explicit end time t1
+    pgen gen(target, weight, G, t0, lambda, t1);
+
+    pse_vector events;
+    // pull events off the generator well past the end of the end time t1
+    while (gen.next().time<t2) {
+        events.push_back(gen.next());
+        gen.pop();
+    }
+
+    // the generator should be exhausted
+    EXPECT_EQ(gen.next(), terminal_pse());
+
+    // the last event should be less than the end time
+    EXPECT_TRUE(events.back().time<t1);
+}
diff --git a/tests/unit/test_merge_events.cpp b/tests/unit/test_merge_events.cpp
index f60fca0944128a20fe44bf5a48de623bc8a78fc0..481f8cf9379813192b812415b3e1e0297937f9a6 100644
--- a/tests/unit/test_merge_events.cpp
+++ b/tests/unit/test_merge_events.cpp
@@ -1,25 +1,12 @@
 #include "../gtest.h"
 
 #include <event_queue.hpp>
+#include <merge_events.hpp>
 #include <model.hpp>
 
-namespace arb {
-    // Declare prototype of the merge_events function, because it is only
-    // defined in the TU of model.cpp
-    void merge_events(time_type tfinal, const pse_vector& lc, pse_vector& events, pse_vector& lf);
-} // namespace arb
-
 using namespace arb;
 
-std::ostream& operator<<(std::ostream& o, const pse_vector& events) {
-    o << "{{"; for (auto e: events) o << " " << e;
-    o << "}}";
-    return o;
-}
-
-using pse = postsynaptic_spike_event;
-auto ev_bind = [] (const pse& e){ return std::tie(e.time, e.target, e.weight); };
-auto ev_less = [] (const pse& l, const pse& r){ return ev_bind(l)<ev_bind(r); };
+std::vector<event_generator_ptr> empty_gens;
 
 // Test the trivial case of merging empty sets
 TEST(merge_events, empty)
@@ -28,7 +15,7 @@ TEST(merge_events, empty)
     pse_vector lc;
     pse_vector lf;
 
-    merge_events(0, lc, events, lf);
+    merge_events(0, max_time, lc, events, empty_gens, lf);
 
     EXPECT_EQ(lf.size(), 0u);
 }
@@ -43,7 +30,7 @@ TEST(merge_events, no_overlap)
         {{0, 0}, 3, 3},
     };
     // Check that the inputs satisfy the precondition that lc is sorted.
-    EXPECT_TRUE(std::is_sorted(lc.begin(), lc.end(), ev_less));
+    EXPECT_TRUE(std::is_sorted(lc.begin(), lc.end()));
 
     // These events should be removed from lf by merge_events, and replaced
     // with events to be delivered after t=10
@@ -60,7 +47,7 @@ TEST(merge_events, no_overlap)
         {{0, 0}, 11, 1},
     };
 
-    merge_events(10, lc, events, lf);
+    merge_events(10, max_time, lc, events, empty_gens, lf);
 
     pse_vector expected = {
         {{8, 0}, 10, 4},
@@ -69,7 +56,7 @@ TEST(merge_events, no_overlap)
         {{0, 0}, 12, 1},
     };
 
-    EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end(), ev_less));
+    EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end()));
     EXPECT_EQ(expected, lf);
 }
 
@@ -85,7 +72,7 @@ TEST(merge_events, overlap)
         {{8, 0}, 10, 2},
         {{0, 0}, 11, 3},
     };
-    EXPECT_TRUE(std::is_sorted(lc.begin(), lc.end(), ev_less));
+    EXPECT_TRUE(std::is_sorted(lc.begin(), lc.end()));
 
     pse_vector lf;
 
@@ -98,7 +85,7 @@ TEST(merge_events, overlap)
         {{7, 0}, 10, 8},
     };
 
-    merge_events(10, lc, events, lf);
+    merge_events(10, max_time, lc, events, empty_gens, lf);
 
     pse_vector expected = {
         {{7, 0}, 10, 8}, // from events
@@ -110,6 +97,147 @@ TEST(merge_events, overlap)
         {{0, 0}, 12, 1}, // from events
     };
 
-    EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end(), ev_less));
+    EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end()));
+    EXPECT_EQ(expected, lf);
+}
+
+// Test the merge_events method with event generators.
+TEST(merge_events, X)
+{
+    const time_type t0 = 10;
+    const time_type t1 = 20;
+
+    pse_vector lc = {
+        {{0, 0}, 1, 1},
+        {{0, 0}, 5, 1},
+        // The current epoch ends at t=10, so all events from here down are expected in lf.
+        {{8, 0}, 10, 2},
+        {{0, 0}, 11, 3},
+        {{8, 0}, 20, 2},
+        {{0, 0}, 21, 3},
+    };
+    EXPECT_TRUE(std::is_sorted(lc.begin(), lc.end()));
+
+    pse_vector lf;
+
+    pse_vector events = {
+        {{0, 0}, 12, 1},
+        {{1, 0}, 15, 2},
+        {{2, 0}, 22, 3},
+        {{3, 0}, 26, 4},
+    };
+
+    std::vector<event_generator_ptr> generators(2);
+    generators.push_back(
+        make_event_generator<regular_generator>
+        (cell_member_type{4,2}, 42.f, t0, 5));
+
+    merge_events(t0, t1, lc, events, generators, lf);
+
+    pse_vector expected = {
+        {{4, 2}, 10, 42}, // from generator
+        {{8, 0}, 10, 2},  // from lc
+        {{0, 0}, 11, 3},  // from lc
+        {{0, 0}, 12, 1},  // from events
+        {{1, 0}, 15, 2},  // from events
+        {{4, 2}, 15, 42}, // from generator
+        {{8, 0}, 20, 2},  // from lc
+        {{0, 0}, 21, 3},  // from lc
+        {{2, 0}, 22, 3},  // from events
+        {{3, 0}, 26, 4},  // from events
+    };
+
+    EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end()));
+    EXPECT_EQ(expected, lf);
+}
+
+// Test the tournament tree for merging two small sequences 
+TEST(merge_events, tourney_seq)
+{
+    pse_vector g1 = {
+        {{0, 0}, 1, 1},
+        {{0, 0}, 2, 2},
+        {{0, 0}, 3, 3},
+        {{0, 0}, 4, 4},
+        {{0, 0}, 5, 5},
+    };
+
+    pse_vector g2 = {
+        {{0, 0}, 1.5, 1},
+        {{0, 0}, 2.5, 2},
+        {{0, 0}, 3.5, 3},
+        {{0, 0}, 4.5, 4},
+        {{0, 0}, 5.5, 5},
+    };
+
+    std::vector<event_generator_ptr> generators;
+    generators.push_back(make_event_generator<seq_generator<pse_vector>>(g1));
+    generators.push_back(make_event_generator<seq_generator<pse_vector>>(g2));
+    impl::tourney_tree tree(generators);
+
+    pse_vector lf;
+    while (!tree.empty()) {
+        lf.push_back(tree.head());
+        tree.pop();
+    }
+
+    EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end()));
+    auto expected = g1;
+    util::append(expected, g2);
+    util::sort(expected);
+
     EXPECT_EQ(expected, lf);
 }
+
+// Test the tournament tree on a large set of Poisson generators.
+TEST(merge_events, tourney_poisson)
+{
+    using rndgen = std::mt19937_64;
+    // Number of poisson generators.
+    // Not a power of 2, so that there will be "null" leaf nodes in the
+    // tournament tree.
+    auto ngen = 100u;
+    time_type tfinal = 10;
+    time_type t0 = 0;
+    time_type lambda = 10; // expected: tfinal*lambda=1000 events per generator
+
+    std::vector<event_generator_ptr> generators;
+    for (auto i=0u; i<ngen; ++i) {
+        cell_member_type tgt{0, i};
+        float weight = i;
+        // the first and last generators have the same seed to test that sorting
+        // of events with the same time but different weights works properly.
+        rndgen G(i%(ngen-1));
+        generators.push_back(
+            make_event_generator<
+                poisson_generator<std::mt19937_64>>
+                (tgt, weight, G, t0, lambda));
+    }
+
+    // manually generate the expected output
+    pse_vector expected;
+    for (auto& gen: generators) {
+        // Push all events before tfinal in gen to the expected values.
+        while (gen->next().time<tfinal) {
+            expected.push_back(gen->next());
+            gen->pop();
+        }
+        // Reset the generator so that it is ready to generate the same
+        // events again for the tournament tree test.
+        gen->reset();
+    }
+    // Manually sort the expected events.
+    util::sort(expected);
+
+    // Generate output using tournament tree in lf.
+    impl::tourney_tree tree(generators);
+    pse_vector lf;
+    while (!tree.empty(tfinal)) {
+        lf.push_back(tree.head());
+        tree.pop();
+    }
+
+    // Test output of tournament tree.
+    EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end()));
+    EXPECT_EQ(lf, expected);
+}