diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3e24708e205be14c1705aee348927c035e0021ed..bbbff66024ff0dac90a0cc42c9116e918e68bd89 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,6 +1,7 @@
 set(BASE_SOURCES
     common_types_io.cpp
     cell.cpp
+    event_binner.cpp
     morphology.cpp
     parameter_list.cpp
     profiling/memory_meter.cpp
diff --git a/src/cell_group.hpp b/src/cell_group.hpp
index 50c696c3c157c2523a946c058da3649ae919ad8c..229885425818d112b626b61914a3c4e2b514ef7a 100644
--- a/src/cell_group.hpp
+++ b/src/cell_group.hpp
@@ -8,6 +8,7 @@
 #include <algorithms.hpp>
 #include <cell.hpp>
 #include <common_types.hpp>
+#include <event_binner.hpp>
 #include <event_queue.hpp>
 #include <spike.hpp>
 #include <util/debug.hpp>
@@ -19,76 +20,6 @@
 namespace nest {
 namespace mc {
 
-enum class binning_kind {
-    none,
-    regular,   // => round time down to multiple of binning interval.
-    following, // => round times down to previous event if within binning interval.
-};
-
-class event_binner {
-public:
-    using time_type = spike::time_type;
-
-    void reset() {
-        last_event_times_.clear();
-    }
-
-    event_binner(): policy_(binning_kind::none), bin_interval_(0) {}
-
-    event_binner(binning_kind policy, time_type bin_interval):
-        policy_(policy), bin_interval_(bin_interval)
-    {}
-
-    // Determine binned time for an event based on policy.
-    // If `t_min` is specified, the binned time will be no lower than `t_min`.
-    // Otherwise the returned binned time will be less than or equal to the parameter `t`,
-    // and within `bin_interval_`.
-
-    time_type bin(cell_gid_type id, time_type t, time_type t_min = std::numeric_limits<time_type>::lowest()) {
-        time_type t_binned = t;
-
-        switch (policy_) {
-        case binning_kind::none:
-            break;
-        case binning_kind::regular:
-            if (bin_interval_>0) {
-                t_binned = std::floor(t/bin_interval_)*bin_interval_;
-            }
-            break;
-        case binning_kind::following:
-            if (auto last_t = last_event_time(id)) {
-                if (t-*last_t<bin_interval_) {
-                    t_binned = *last_t;
-                }
-            }
-            update_last_event_time(id, t_binned);
-            break;
-        default:
-            throw std::logic_error("unrecognized binning policy");
-        }
-
-        return std::max(t_binned, t_min);
-    }
-
-private:
-    binning_kind policy_;
-
-    // Interval in which event times can be aliased.
-    time_type bin_interval_;
-
-    // (Consider replacing this with a vector-backed store.)
-    std::unordered_map<cell_gid_type, time_type> last_event_times_;
-
-    util::optional<time_type> last_event_time(cell_gid_type id) {
-        auto it = last_event_times_.find(id);
-        return it==last_event_times_.end()? util::nothing: util::just(it->second);
-    }
-
-    void update_last_event_time(cell_gid_type id, time_type t) {
-        last_event_times_[id] = t;
-    }
-};
-
 template <typename LoweredCell>
 class cell_group {
 public:
diff --git a/src/event_binner.cpp b/src/event_binner.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6279db9b26e799387b9f3fdb580afc8a09347c3a
--- /dev/null
+++ b/src/event_binner.cpp
@@ -0,0 +1,58 @@
+#include <algorithm>
+#include <cmath>
+#include <limits>
+#include <stdexcept>
+#include <unordered_map>
+
+#include <common_types.hpp>
+#include <event_binner.hpp>
+#include <spike.hpp>
+#include <util/optional.hpp>
+
+namespace nest {
+namespace mc {
+
+void event_binner::reset() {
+    last_event_times_.clear();
+}
+
+event_binner::time_type
+event_binner::bin(cell_gid_type id, time_type t, time_type t_min) {
+    time_type t_binned = t;
+
+    switch (policy_) {
+    case binning_kind::none:
+        break;
+    case binning_kind::regular:
+        if (bin_interval_>0) {
+            t_binned = std::floor(t/bin_interval_)*bin_interval_;
+        }
+        break;
+    case binning_kind::following:
+        if (auto last_t = last_event_time(id)) {
+            if (t-*last_t<bin_interval_) {
+                t_binned = *last_t;
+            }
+        }
+        update_last_event_time(id, t_binned);
+        break;
+    default:
+        throw std::logic_error("unrecognized binning policy");
+    }
+
+    return std::max(t_binned, t_min);
+}
+
+util::optional<event_binner::time_type>
+event_binner::last_event_time(cell_gid_type id) {
+    auto it = last_event_times_.find(id);
+    return it==last_event_times_.end()? util::nothing: util::just(it->second);
+}
+
+void event_binner::update_last_event_time(cell_gid_type id, time_type t) {
+    last_event_times_[id] = t;
+}
+
+} // namespace mc
+} // namespace nest
+
diff --git a/src/event_binner.hpp b/src/event_binner.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a18b9bb0ce3f6d6fa2a14799321794b98d634e01
--- /dev/null
+++ b/src/event_binner.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <limits>
+#include <unordered_map>
+
+#include <common_types.hpp>
+#include <spike.hpp>
+#include <util/optional.hpp>
+
+namespace nest {
+namespace mc {
+
+enum class binning_kind {
+    none,
+    regular,   // => round time down to multiple of binning interval.
+    following, // => round times down to previous event if within binning interval.
+};
+
+class event_binner {
+public:
+    using time_type = spike::time_type;
+
+    event_binner(): policy_(binning_kind::none), bin_interval_(0) {}
+
+    event_binner(binning_kind policy, time_type bin_interval):
+        policy_(policy), bin_interval_(bin_interval)
+    {}
+
+    void reset();
+
+    // Determine binned time for an event based on policy.
+    // If `t_min` is specified, the binned time will be no lower than `t_min`.
+    // Otherwise the returned binned time will be less than or equal to the parameter `t`,
+    // and within `bin_interval_`.
+
+    time_type bin(cell_gid_type id,
+                  time_type t,
+                  time_type t_min = std::numeric_limits<time_type>::lowest());
+
+private:
+    binning_kind policy_;
+
+    // Interval in which event times can be aliased.
+    time_type bin_interval_;
+
+    // (Consider replacing this with a vector-backed store.)
+    std::unordered_map<cell_gid_type, time_type> last_event_times_;
+
+    util::optional<time_type> last_event_time(cell_gid_type id);
+
+    void update_last_event_time(cell_gid_type id, time_type t);
+};
+
+} // namespace mc
+} // namespace nest
+
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 50548d733e16f30f0374478e49dc23f2da9fa5ac..eb2170e5e7601a77b862132124fa9a51e2ba7488 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -38,6 +38,7 @@ set(TEST_SOURCES
     test_cycle.cpp
     test_either.cpp
     test_event_queue.cpp
+    test_event_binner.cpp
     test_filter.cpp
     test_fvm_multi.cpp
     test_cell_group.cpp
diff --git a/tests/unit/test_cell_group.cpp b/tests/unit/test_cell_group.cpp
index af5382d740c05c0b48fb73fd74e78b2cf286e1d1..0016b24e2bc74d1edf0989c1f214d3a9c2c9476f 100644
--- a/tests/unit/test_cell_group.cpp
+++ b/tests/unit/test_cell_group.cpp
@@ -60,101 +60,3 @@ TEST(cell_group, sources) {
         }
     }
 }
-
-TEST(cell_group, event_binner) {
-    using testing::seq_almost_eq;
-
-    std::pair<cell_gid_type, float> binning_test_data[] = {
-        { 11, 0.50 },
-        { 12, 0.70 },
-        { 14, 0.73 },
-        { 11, 1.80 },
-        { 12, 1.83 },
-        { 11, 1.90 },
-        { 11, 2.00 },
-        { 14, 2.00 },
-        { 11, 2.10 },
-        { 14, 2.30 }
-    };
-
-    std::unordered_map<cell_gid_type, std::vector<float>> ev_times;
-    std::vector<float> expected;
-
-    auto run_binner = [&](event_binner&& binner) {
-        ev_times.clear();
-        for (auto p: binning_test_data) {
-            ev_times[p.first].push_back(binner.bin(p.first, p.second));
-        }
-    };
-
-    run_binner(event_binner{binning_kind::none, 0});
-
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[11], (float []){0.50, 1.80, 1.90, 2.00, 2.10}));
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[12], (float []){0.70, 1.83}));
-    EXPECT_TRUE(ev_times[13].empty());
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[14], (float []){0.73, 2.00, 2.30}));
-
-    run_binner(event_binner{binning_kind::regular, 0.25});
-
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[11], (float []){0.50, 1.75, 1.75, 2.00, 2.00}));
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[12], (float []){0.50, 1.75}));
-    EXPECT_TRUE(ev_times[13].empty());
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[14], (float []){0.50, 2.00, 2.25}));
-
-    run_binner(event_binner{binning_kind::following, 0.25});
-
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[11], (float []){0.50, 1.80, 1.80, 1.80, 2.10}));
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[12], (float []){0.70, 1.83}));
-    EXPECT_TRUE(ev_times[13].empty());
-    EXPECT_TRUE(seq_almost_eq<float>(ev_times[14], (float []){0.73, 2.00, 2.30}));
-}
-
-TEST(cell_group, event_binner_with_min) {
-    using testing::seq_almost_eq;
-
-    struct test_time {
-        float time;
-        float t_min;
-    };
-    test_time test_data[] = {
-        {0.8f, 1.0f},
-        {1.6f, 1.0f},
-        {1.9f, 1.8f},
-        {2.0f, 1.8f},
-        {2.2f, 1.8f}
-    };
-
-    std::vector<float> times;
-    auto run_binner = [&](event_binner&& binner, bool use_min) {
-        times.clear();
-        for (auto p: test_data) {
-            if (use_min) {
-                times.push_back(binner.bin(0, p.time, p.t_min));
-            }
-            else {
-                times.push_back(binner.bin(0, p.time));
-            }
-        }
-    };
-
-    // 'none' binning
-    run_binner(event_binner{binning_kind::none, 0.5}, false);
-    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){0.8, 1.6, 1.9, 2.0, 2.2}));
-
-    run_binner(event_binner{binning_kind::none, 0.5}, true);
-    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){1.0, 1.6, 1.9, 2.0, 2.2}));
-
-    // 'regular' binning
-    run_binner(event_binner{binning_kind::regular, 0.5}, false);
-    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){0.5, 1.5, 1.5, 2.0, 2.0}));
-
-    run_binner(event_binner{binning_kind::regular, 0.5}, true);
-    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){1.0, 1.5, 1.8, 2.0, 2.0}));
-
-    // 'following' binning
-    run_binner(event_binner{binning_kind::following, 0.5}, false);
-    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){0.8, 1.6, 1.6, 1.6, 2.2}));
-
-    run_binner(event_binner{binning_kind::following, 0.5}, true);
-    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){1.0, 1.6, 1.8, 1.8, 2.2}));
-}
diff --git a/tests/unit/test_event_binner.cpp b/tests/unit/test_event_binner.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b99501e082b96c65079def7b702e9f998e39eb20
--- /dev/null
+++ b/tests/unit/test_event_binner.cpp
@@ -0,0 +1,105 @@
+#include "../gtest.h"
+
+#include <event_binner.hpp>
+
+#include "common.hpp"
+
+using namespace nest::mc;
+
+TEST(event_binner, basic) {
+    using testing::seq_almost_eq;
+
+    std::pair<cell_gid_type, float> binning_test_data[] = {
+        { 11, 0.50 },
+        { 12, 0.70 },
+        { 14, 0.73 },
+        { 11, 1.80 },
+        { 12, 1.83 },
+        { 11, 1.90 },
+        { 11, 2.00 },
+        { 14, 2.00 },
+        { 11, 2.10 },
+        { 14, 2.30 }
+    };
+
+    std::unordered_map<cell_gid_type, std::vector<float>> ev_times;
+    std::vector<float> expected;
+
+    auto run_binner = [&](event_binner&& binner) {
+        ev_times.clear();
+        for (auto p: binning_test_data) {
+            ev_times[p.first].push_back(binner.bin(p.first, p.second));
+        }
+    };
+
+    run_binner(event_binner{binning_kind::none, 0});
+
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[11], (float []){0.50, 1.80, 1.90, 2.00, 2.10}));
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[12], (float []){0.70, 1.83}));
+    EXPECT_TRUE(ev_times[13].empty());
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[14], (float []){0.73, 2.00, 2.30}));
+
+    run_binner(event_binner{binning_kind::regular, 0.25});
+
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[11], (float []){0.50, 1.75, 1.75, 2.00, 2.00}));
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[12], (float []){0.50, 1.75}));
+    EXPECT_TRUE(ev_times[13].empty());
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[14], (float []){0.50, 2.00, 2.25}));
+
+    run_binner(event_binner{binning_kind::following, 0.25});
+
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[11], (float []){0.50, 1.80, 1.80, 1.80, 2.10}));
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[12], (float []){0.70, 1.83}));
+    EXPECT_TRUE(ev_times[13].empty());
+    EXPECT_TRUE(seq_almost_eq<float>(ev_times[14], (float []){0.73, 2.00, 2.30}));
+}
+
+TEST(event_binner, with_min) {
+    using testing::seq_almost_eq;
+
+    struct test_time {
+        float time;
+        float t_min;
+    };
+    test_time test_data[] = {
+        {0.8f, 1.0f},
+        {1.6f, 1.0f},
+        {1.9f, 1.8f},
+        {2.0f, 1.8f},
+        {2.2f, 1.8f}
+    };
+
+    std::vector<float> times;
+    auto run_binner = [&](event_binner&& binner, bool use_min) {
+        times.clear();
+        for (auto p: test_data) {
+            if (use_min) {
+                times.push_back(binner.bin(0, p.time, p.t_min));
+            }
+            else {
+                times.push_back(binner.bin(0, p.time));
+            }
+        }
+    };
+
+    // 'none' binning
+    run_binner(event_binner{binning_kind::none, 0.5}, false);
+    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){0.8, 1.6, 1.9, 2.0, 2.2}));
+
+    run_binner(event_binner{binning_kind::none, 0.5}, true);
+    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){1.0, 1.6, 1.9, 2.0, 2.2}));
+
+    // 'regular' binning
+    run_binner(event_binner{binning_kind::regular, 0.5}, false);
+    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){0.5, 1.5, 1.5, 2.0, 2.0}));
+
+    run_binner(event_binner{binning_kind::regular, 0.5}, true);
+    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){1.0, 1.5, 1.8, 2.0, 2.0}));
+
+    // 'following' binning
+    run_binner(event_binner{binning_kind::following, 0.5}, false);
+    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){0.8, 1.6, 1.6, 1.6, 2.2}));
+
+    run_binner(event_binner{binning_kind::following, 0.5}, true);
+    EXPECT_TRUE(seq_almost_eq<float>(times, (float []){1.0, 1.6, 1.8, 1.8, 2.2}));
+}