From 0d20df25e206ffa465a64ad3758d1ae24c5e8ec2 Mon Sep 17 00:00:00 2001 From: Sam Yates <yates@cscs.ch> Date: Thu, 26 Jul 2018 20:04:00 +0200 Subject: [PATCH] Return view from schedule, replace time_seq. (#536) Reduce redundant functionality across event_generator, time_seq and schedule by providing a low-heap overhead interface to schedule and using that for time sequences in event_generator and specialized cell groups. * Have schedule return pair of pointers as view to generated times. * Fix missing DEBUG/TRACE functionality. * Use rate instead of mean_dt for Poisson schedule. * Move merge_events() functionality to simulator.cpp. * Migrate event_generator to event span interface. * Migrate tourney_tree to event span interface. * Only invoke tourney_tree merge if generators have events in the epoch. * Use schedule for times in event_generator implementations. * Replace seq_generator with explicit_generator that keeps a copy of events. * Replace vector_backed_generator and poisson_generator with schedule_generator. * Replace time_seq uses with schedule. * Add default empty schedule. * Move rounding error test for regular time sequence into schedule test. * Update sampling API documentation for schedule. --- arbor/benchmark_cell_group.cpp | 9 +- arbor/io/trace.hpp | 6 +- arbor/mc_cell_group.cpp | 3 +- arbor/merge_events.cpp | 131 ++++--------- arbor/merge_events.hpp | 44 +---- arbor/schedule.cpp | 27 +-- arbor/simulation.cpp | 98 +++++++--- arbor/spike_source_cell_group.cpp | 11 +- arbor/spike_source_cell_group.hpp | 5 +- arbor/util/range.hpp | 5 + doc/sampling_api.rst | 12 +- example/bench/recipe.cpp | 11 +- example/brunel/brunel_miniapp.cpp | 4 +- example/generators/event_gen.cpp | 13 +- example/generators/readme.md | 12 +- example/miniapp/miniapp_recipes.cpp | 6 +- include/arbor/benchmark_cell.hpp | 4 +- include/arbor/event_generator.hpp | 285 +++++++++++----------------- include/arbor/schedule.hpp | 78 ++++++-- include/arbor/spike_source_cell.hpp | 4 +- include/arbor/time_sequence.hpp | 248 ------------------------ test/unit/CMakeLists.txt | 1 - test/unit/test_event_generators.cpp | 87 +++------ test/unit/test_lif_cell_group.cpp | 3 +- test/unit/test_merge_events.cpp | 74 ++++++-- test/unit/test_schedule.cpp | 102 +++++++--- test/unit/test_spike_source.cpp | 68 ++++--- test/unit/test_time_seq.cpp | 175 ----------------- 28 files changed, 542 insertions(+), 984 deletions(-) delete mode 100644 include/arbor/time_sequence.hpp delete mode 100644 test/unit/test_time_seq.cpp diff --git a/arbor/benchmark_cell_group.cpp b/arbor/benchmark_cell_group.cpp index 2da67ef0..4c24ff42 100644 --- a/arbor/benchmark_cell_group.cpp +++ b/arbor/benchmark_cell_group.cpp @@ -3,7 +3,7 @@ #include <arbor/benchmark_cell.hpp> #include <arbor/recipe.hpp> -#include <arbor/time_sequence.hpp> +#include <arbor/schedule.hpp> #include "cell_group.hpp" #include "profile/profiler_macro.hpp" @@ -50,7 +50,6 @@ void benchmark_cell_group::advance(epoch ep, // Micro-seconds to advance in this epoch. auto us = 1e3*(ep.tfinal-t_); for (auto i: util::make_span(0, gids_.size())) { - auto& tseq = cells_[i].time_sequence; // Expected time to complete epoch in micro seconds. const double duration_us = cells_[i].realtime_ratio*us; const auto gid = gids_[i]; @@ -58,9 +57,9 @@ void benchmark_cell_group::advance(epoch ep, // Start timer. auto start = high_resolution_clock::now(); - while (tseq.front()<ep.tfinal) { - spikes_.push_back({{gid, 0u}, tseq.front()}); - tseq.pop(); + auto spike_times = util::make_range(cells_[i].time_sequence.events(t_, ep.tfinal)); + for (auto t: spike_times) { + spikes_.push_back({{gid, 0u}, t}); } // Wait until the expected time to advance has elapsed. Use a busy-wait diff --git a/arbor/io/trace.hpp b/arbor/io/trace.hpp index 15984715..3bb71054 100644 --- a/arbor/io/trace.hpp +++ b/arbor/io/trace.hpp @@ -35,6 +35,8 @@ namespace arb { namespace impl { + inline void debug_emit_csv(std::ostream&) {} + template <typename Head, typename... Tail> void debug_emit_csv(std::ostream& out, const Head& head, const Tail&... tail) { out << head; @@ -44,7 +46,9 @@ namespace impl { debug_emit_csv(out, tail...); } - void debug_emit_trace_leader(std::ostream&, const char* file, int line, const char* vars); + inline void debug_emit_trace_leader(std::ostream& out, const char* file, int line, const char* vars) { + out << file << ':' << line << ": " << vars << ": "; + } struct emit_nl_locked: public io::locked_ostream { emit_nl_locked(std::streambuf* buf): diff --git a/arbor/mc_cell_group.cpp b/arbor/mc_cell_group.cpp index 54793ddc..f0680809 100644 --- a/arbor/mc_cell_group.cpp +++ b/arbor/mc_cell_group.cpp @@ -19,6 +19,7 @@ #include "util/filter.hpp" #include "util/maputil.hpp" #include "util/partition.hpp" +#include "util/range.hpp" #include "util/span.hpp" namespace arb { @@ -127,7 +128,7 @@ void mc_cell_group::advance(epoch ep, time_type dt, const event_lane_subrange& e sample_size_type max_samples_per_call = 0; for (auto& sa: sampler_map_) { - auto sample_times = sa.sched.events(tstart, ep.tfinal); + auto sample_times = util::make_range(sa.sched.events(tstart, ep.tfinal)); if (sample_times.empty()) { continue; } diff --git a/arbor/merge_events.cpp b/arbor/merge_events.cpp index 096dd169..04355188 100644 --- a/arbor/merge_events.cpp +++ b/arbor/merge_events.cpp @@ -2,21 +2,24 @@ #include <set> #include <vector> +#include <arbor/assert.hpp> #include <arbor/common_types.hpp> -#include <arbor/domain_decomposition.hpp> -#include <arbor/recipe.hpp> +#include <arbor/math.hpp> -#include "cell_group.hpp" -#include "cell_group_factory.hpp" +#include "io/trace.hpp" #include "merge_events.hpp" -#include "util/filter.hpp" -#include "util/span.hpp" #include "profile/profiler_macro.hpp" namespace arb { namespace impl { +// A postsynaptic spike event that has delivery time set to +// terminal_time, used as a sentinel in `tourney_tree`. + +static constexpr spike_event terminal_pse{cell_member_type{0,0}, terminal_time, 0}; + + // The tournament tree data structure is used to merge k sorted lists of events. // See online for high-level information about tournament trees. // @@ -30,25 +33,27 @@ namespace impl { // 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>& input): + +tourney_tree::tourney_tree(std::vector<event_span>& input): input_(input), n_lanes_(input_.size()) { - // Must have at least 1 queue - arb_assert(n_lanes_); - // Maximum value in unsigned limits how many queues we can have - arb_assert(n_lanes_<(1u<<(sizeof(unsigned)*8u-1u))); + // Must have at least 1 queue. + arb_assert(n_lanes_>=1u); + + leaves_ = math::next_pow2(n_lanes_); - leaves_ = next_power_2(n_lanes_); - nodes_ = 2u*(leaves_-1u)+1u; // 2*l-1 with overflow protection + // Must be able to fit leaves in unsigned count. + arb_assert(leaves_>=n_lanes_); + nodes_ = 2*leaves_-1; // 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].front()): - key_val(i, make_terminal_pse()); // null leaf node + key_val(i, input[i].empty()? terminal_pse: input[i].front()): + key_val(i, terminal_pse); // null leaf node } // Walk the tree to initialize the non-leaf nodes setup(0); @@ -70,10 +75,6 @@ bool tourney_tree::empty() const { return event(0).time == terminal_time; } -bool tourney_tree::empty(time_type t) const { - return event(0).time >= t; -} - spike_event tourney_tree::head() const { return event(0); } @@ -83,10 +84,15 @@ spike_event tourney_tree::head() const { 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].front(); + auto& in = input_[lane]; + + if (!in.empty()) { + ++in.left; + } + + event(i) = in.empty()? terminal_pse: in.front(); // re-heapify the tree with a single walk from leaf to root while ((i=parent(i))) { @@ -138,83 +144,16 @@ const 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>& generators, - pse_vector& lf) -{ - using std::distance; - using std::lower_bound; - - PE(communication_enqueue_sort); - - // Sort events from the communicator in place. - util::sort(events); - // Clear lf to store merged list. - lf.clear(); - - PL(); - - - // 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â‚, ∞). - arb_assert(generators.size()>2u); - - PE(communication_enqueue_setup); - // Make an event generator with all the events in events. - generators[0] = 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] = seq_generator<decltype(lc_range)>(lc_range); - PL(); - - PE(communication_enqueue_tree); - // 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(); - } - PL(); - - PE(communication_enqueue_merge); - // 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); - PL(); - } - else { - PE(communication_enqueue_merge); - // 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()); - PL(); +void tree_merge_events(std::vector<event_span>& sources, pse_vector& out) { + PE(communication_enqueue_tree); + impl::tourney_tree tree(sources); + while (!tree.empty()) { + out.push_back(tree.head()); + tree.pop(); } + PL(); } } // namespace arb diff --git a/arbor/merge_events.hpp b/arbor/merge_events.hpp index eb058fec..6427d753 100644 --- a/arbor/merge_events.hpp +++ b/arbor/merge_events.hpp @@ -8,42 +8,15 @@ #include "event_queue.hpp" #include "profile/profiler_macro.hpp" +#include "util/range.hpp" + +// Merge a collection of sorted event sequences into a sorted output sequence. 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 -// pending_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 -// [---------------] pending_events -// [------) generators -// -// The output list, stored in lf, will contain all the following: -// * all events in pending_events -// * 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& pending_events, - std::vector<event_generator>& generators, - pse_vector& lf); +using event_span = util::range<const spike_event*>; + +void tree_merge_events(std::vector<event_span>& sources, pse_vector& out); namespace impl { // The tournament tree is used internally by the merge_events method, and @@ -53,9 +26,8 @@ namespace impl { using key_val = std::pair<unsigned, spike_event>; public: - tourney_tree(std::vector<event_generator>& input); + tourney_tree(std::vector<event_span>& input); bool empty() const; - bool empty(time_type t) const; spike_event head() const; void pop(); friend std::ostream& operator<<(std::ostream&, const tourney_tree&); @@ -75,7 +47,7 @@ namespace impl { unsigned next_power_2(unsigned x) const; std::vector<key_val> heap_; - std::vector<event_generator>& input_; + std::vector<event_span>& input_; unsigned leaves_; unsigned nodes_; unsigned n_lanes_; diff --git a/arbor/schedule.cpp b/arbor/schedule.cpp index 2bfc246e..2a0d6bef 100644 --- a/arbor/schedule.cpp +++ b/arbor/schedule.cpp @@ -1,5 +1,6 @@ #include <algorithm> #include <iterator> +#include <numeric> #include <utility> #include <vector> @@ -12,12 +13,14 @@ namespace arb { // Regular schedule implementation. -std::vector<time_type> regular_schedule_impl::events(time_type t0, time_type t1) { - std::vector<time_type> ts; +time_event_span regular_schedule_impl::events(time_type t0, time_type t1) { + times_.clear(); + + t0 = std::max(t0, t0_); + t1 = std::min(t1, t1_); - t0 = t0<0? 0: t0; if (t1>t0) { - ts.reserve(1+std::size_t((t1-t0)*oodt_)); + times_.reserve(1+std::size_t((t1-t0)*oodt_)); long long n = t0*oodt_; time_type t = n*dt_; @@ -27,22 +30,24 @@ std::vector<time_type> regular_schedule_impl::events(time_type t0, time_type t1) } while (t<t1) { - ts.push_back(t); + times_.push_back(t); t = (++n)*dt_; } } - return ts; + return as_time_event_span(times_); } // Explicit schedule implementation. -std::vector<time_type> explicit_schedule_impl::events(time_type t0, time_type t1) { - auto lb = std::lower_bound(times_.begin()+start_index_, times_.end(), t0); - auto ub = std::lower_bound(times_.begin()+start_index_, times_.end(), t1); +time_event_span explicit_schedule_impl::events(time_type t0, time_type t1) { + time_event_span view = as_time_event_span(times_); + + const time_type* lb = std::lower_bound(view.first+start_index_, view.second, t0); + const time_type* ub = std::lower_bound(lb, view.second, t1); - start_index_ = std::distance(times_.begin(), ub); - return std::vector<time_type>(lb, ub); + start_index_ = ub-view.first; + return {lb, ub}; } } // namespace arb diff --git a/arbor/simulation.cpp b/arbor/simulation.cpp index dcef80c4..24587676 100644 --- a/arbor/simulation.cpp +++ b/arbor/simulation.cpp @@ -2,8 +2,9 @@ #include <set> #include <vector> -#include <arbor/recipe.hpp> #include <arbor/domain_decomposition.hpp> +#include <arbor/generic_event.hpp> +#include <arbor/recipe.hpp> #include <arbor/schedule.hpp> #include <arbor/simulation.hpp> @@ -95,8 +96,6 @@ private: // Hash table for looking up the the local index of a cell with a given gid std::unordered_map<cell_gid_type, cell_size_type> gid_to_local_; - util::optional<cell_size_type> local_cell_index(cell_gid_type); - communicator communicator_; task_system_handle task_system_; @@ -150,18 +149,7 @@ simulation_state::simulation_state( 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)); - } - } + event_generators_[lidx] = rec.event_generators(gid); ++lidx; } } @@ -196,11 +184,10 @@ void simulation_state::reset() { } } - // Reset all event generators, and advance to t_. + // Reset all event generators. for (auto& lane: event_generators_) { for (auto& gen: lane) { gen.reset(); - gen.advance(t_); } } @@ -294,6 +281,15 @@ time_type simulation_state::run(time_type tfinal, time_type dt) { return t_; } +template <typename Seq, typename Value, typename Less = std::less<>> +auto split_sorted_range(Seq&& seq, const Value& v, Less cmp = Less{}) { + auto canon = util::canonical_view(seq); + auto it = std::lower_bound(canon.begin(), canon.end(), v, cmp); + return std::make_pair( + util::make_range(seq.begin(), it), + util::make_range(it, seq.end())); +} + // Populate the event lanes for epoch+1 (i.e event_lanes_[epoch+1)] // Update each lane in parallel, if supported by the threading backend. // On completion event_lanes[epoch+1] will contain sorted lists of events with @@ -302,18 +298,72 @@ time_type simulation_state::run(time_type tfinal, time_type dt) { // event_lanes[epoch]: take all events ≥ t_from // event_generators : take all events < t_to // pending_events : take all events + +// merge_cell_events() is a separate function for unit testing purposes. +void merge_cell_events( + time_type t_from, + time_type t_to, + event_span old_events, + event_span pending, + std::vector<event_generator>& generators, + pse_vector& new_events) +{ + PE(communication_enqueue_setup); + new_events.clear(); + old_events = split_sorted_range(old_events, t_from, event_time_less()).second; + PL(); + + if (!generators.empty()) { + PE(communication_enqueue_setup); + // Tree-merge events in [t_from, t_to) from old, pending and generator events. + + std::vector<event_span> spanbuf; + spanbuf.reserve(2+generators.size()); + + auto old_split = split_sorted_range(old_events, t_to, event_time_less()); + auto pending_split = split_sorted_range(pending, t_to, event_time_less()); + + spanbuf.push_back(old_split.first); + spanbuf.push_back(pending_split.first); + + for (auto& g: generators) { + event_span evs = g.events(t_from, t_to); + if (!evs.empty()) { + spanbuf.push_back(evs); + } + } + PL(); + + PE(communication_enqueue_tree); + tree_merge_events(spanbuf, new_events); + PL(); + + old_events = old_split.second; + pending = pending_split.second; + } + + // Merge (remaining) old and pending events. + PE(communication_enqueue_merge); + auto n = new_events.size(); + new_events.resize(n+pending.size()+old_events.size()); + std::merge(pending.begin(), pending.end(), old_events.begin(), old_events.end(), new_events.begin()+n); + PL(); +} + void simulation_state::setup_events(time_type t_from, time_type t_to, std::size_t epoch) { const auto n = communicator_.num_local_cells(); threading::parallel_for::apply(0, n, task_system_.get(), [&](cell_size_type i) { - merge_events( - t_from, t_to, - event_lanes(epoch)[i], // in: the current event lane - pending_events_[i], // in: events from the communicator - event_generators_[i], // in: event generators for this lane - event_lanes(epoch+1)[i]); // out: the event lane for the next epoch + PE(communication_enqueue_sort); + util::sort(pending_events_[i]); + PL(); + + event_span pending = util::range_pointer_view(pending_events_[i]); + event_span old_events = util::range_pointer_view(event_lanes(epoch)[i]); + + merge_cell_events(t_from, t_to, old_events, pending, event_generators_[i], event_lanes(epoch+1)[i]); pending_events_[i].clear(); - }); + }); } sampler_association_handle simulation_state::add_sampler( diff --git a/arbor/spike_source_cell_group.cpp b/arbor/spike_source_cell_group.cpp index aea7deaa..5b309e40 100644 --- a/arbor/spike_source_cell_group.cpp +++ b/arbor/spike_source_cell_group.cpp @@ -2,7 +2,7 @@ #include <arbor/recipe.hpp> #include <arbor/spike_source_cell.hpp> -#include <arbor/time_sequence.hpp> +#include <arbor/schedule.hpp> #include "cell_group.hpp" #include "profile/profiler_macro.hpp" @@ -34,14 +34,14 @@ void spike_source_cell_group::advance(epoch ep, time_type dt, const event_lane_s PE(advance_sscell); for (auto i: util::count_along(gids_)) { - auto& tseq = time_sequences_[i]; const auto gid = gids_[i]; - while (tseq.front()<ep.tfinal) { - spikes_.push_back({{gid, 0u}, tseq.front()}); - tseq.pop(); + for (auto t: util::make_range(time_sequences_[i].events(t_, ep.tfinal))) { + spikes_.push_back({{gid, 0u}, t}); } } + t_ = ep.tfinal; + PL(); }; @@ -49,6 +49,7 @@ void spike_source_cell_group::reset() { for (auto& s: time_sequences_) { s.reset(); } + t_ = 0; clear_spikes(); } diff --git a/arbor/spike_source_cell_group.hpp b/arbor/spike_source_cell_group.hpp index 1fcf8a88..ff2d4b49 100644 --- a/arbor/spike_source_cell_group.hpp +++ b/arbor/spike_source_cell_group.hpp @@ -5,8 +5,8 @@ #include <arbor/common_types.hpp> #include <arbor/recipe.hpp> #include <arbor/sampling.hpp> +#include <arbor/schedule.hpp> #include <arbor/spike.hpp> -#include <arbor/time_sequence.hpp> #include "cell_group.hpp" #include "epoch.hpp" @@ -36,9 +36,10 @@ public: void remove_all_samplers() override {} private: + time_type t_ = 0; std::vector<spike> spikes_; std::vector<cell_gid_type> gids_; - std::vector<time_seq> time_sequences_; + std::vector<schedule> time_sequences_; }; } // namespace arb diff --git a/arbor/util/range.hpp b/arbor/util/range.hpp index 3f692016..154f8c35 100644 --- a/arbor/util/range.hpp +++ b/arbor/util/range.hpp @@ -60,6 +60,11 @@ struct range { left(std::forward<U1>(l)), right(std::forward<U2>(r)) {} + template <typename U1, typename U2> + range(const std::pair<U1, U2>& p): + left(p.first), right(p.second) + {} + template < typename U1, typename U2, diff --git a/doc/sampling_api.rst b/doc/sampling_api.rst index a5abcfde..2ec63824 100644 --- a/doc/sampling_api.rst +++ b/doc/sampling_api.rst @@ -225,13 +225,21 @@ A ``schedule`` object has two methods: void schedule::reset(); - std::vector<time_type> events(time_type t0, time_type t1) + time_event_span events(time_type t0, time_type t1) -The ``events(t0, t1)`` method returns a vector of monotonically +A ``time_event_span`` is a ``std::pair`` of pointers `const time_type*`, +representing a view into an internally maintained collection of generated +time values. + +The ``events(t0, t1)`` method returns a view of monotonically increasing time values in the half-open interval ``[t0, t1)``. Successive calls to ``events`` — without an intervening call to ``reset()`` — must request strictly subsequent intervals. +The data represented by the returned ``time_event_span`` view is valid +for the lifetime of the ``schedule`` object, and is invalidated by any +subsequent call to ``reset()`` or ``events()``. + The ``reset()`` method resets the state such that events can be retrieved from again from time zero. A schedule that is reset must then produce the same sequence of time points, that is, it must exhibit repeatable diff --git a/example/bench/recipe.cpp b/example/bench/recipe.cpp index b8dd5f3f..aa9c94b8 100644 --- a/example/bench/recipe.cpp +++ b/example/bench/recipe.cpp @@ -2,7 +2,7 @@ #include <arbor/benchmark_cell.hpp> #include <arbor/common_types.hpp> -#include <arbor/time_sequence.hpp> +#include <arbor/schedule.hpp> #include "recipe.hpp" @@ -15,19 +15,16 @@ cell_size_type bench_recipe::num_cells() const { } arb::util::unique_any bench_recipe::get_cell_description(cell_gid_type gid) const { - using rng_type = std::mt19937_64; + std::mt19937_64 rng(gid); arb::benchmark_cell cell; cell.realtime_ratio = params_.cell.realtime_ratio; // The time_sequence of the cell produces the series of time points at - // which it will spike. We use a poisson_time_seq with a random sequence + // which it will spike. We use a poisson_schedule with a random sequence // seeded with the gid. In this way, a cell's random stream depends only // on its gid, and will hence give reproducable results when run with // different MPI ranks and threads. - cell.time_sequence = - arb::poisson_time_seq<rng_type>( - rng_type(gid), 0, 1e-3*params_.cell.spike_freq_hz); - + cell.time_sequence = arb::poisson_schedule(1e-3*params_.cell.spike_freq_hz, rng); return std::move(cell); } diff --git a/example/brunel/brunel_miniapp.cpp b/example/brunel/brunel_miniapp.cpp index 9de099c0..19f3df93 100644 --- a/example/brunel/brunel_miniapp.cpp +++ b/example/brunel/brunel_miniapp.cpp @@ -126,12 +126,10 @@ public: std::mt19937_64 G; G.seed(gid + seed_); - using pgen = poisson_generator<std::mt19937_64>; - time_type t0 = 0; cell_member_type target{gid, 0}; - gens.emplace_back(pgen(target, weight_ext_, G, t0, lambda_)); + gens.emplace_back(poisson_generator(target, weight_ext_, t0, lambda_, G)); return gens; } diff --git a/example/generators/event_gen.cpp b/example/generators/event_gen.cpp index fd7423d4..a2e3d595 100644 --- a/example/generators/event_gen.cpp +++ b/example/generators/event_gen.cpp @@ -76,7 +76,6 @@ public: arb_assert(gid==0); // There is only one cell in the model using RNG = std::mt19937_64; - using pgen = arb::poisson_generator<RNG>; auto hz_to_freq = [](double hz) { return hz*1e-3; }; time_type t0 = 0; @@ -92,15 +91,15 @@ public: // Add excitatory generator gens.push_back( - pgen(cell_member_type{0,0}, // Target synapse (gid, local_id). - w_e, // Weight of events to deliver - RNG(29562872), // Random number generator to use - t0, // Events start being delivered from this time - lambda_e)); // Expected frequency (events per ms) + poisson_generator(cell_member_type{0,0}, // Target synapse (gid, local_id). + w_e, // Weight of events to deliver + t0, // Events start being delivered from this time + lambda_e, // Expected frequency (kHz) + RNG(29562872))); // Random number generator to use // Add inhibitory generator gens.emplace_back( - pgen(cell_member_type{0,0}, w_i, RNG(86543891), t0, lambda_i)); + poisson_generator(cell_member_type{0,0}, w_i, t0, lambda_i, RNG(86543891))); return gens; } diff --git a/example/generators/readme.md b/example/generators/readme.md index c9146c1a..f3f5c870 100644 --- a/example/generators/readme.md +++ b/example/generators/readme.md @@ -92,9 +92,6 @@ The implementation of this with hard-coded frequencies and weights is: // The type of random number generator to use. using RNG = std::mt19937_64; - // The poisson_generator is templated on the random number generator type. - using pgen = arb::poisson_generator<RNG>; - auto hz_to_freq = [](double hz) { return hz*1e-3; }; time_type t0 = 0; @@ -109,15 +106,16 @@ The implementation of this with hard-coded frequencies and weights is: // Add excitatory generator gens.emplace_back( - pgen(cell_member_type{0,0}, // Target synapse (gid, local_id). + arb::poisson_generator( + cell_member_type{0,0}, // Target synapse (gid, local_id). w_e, // Weight of events to deliver - RNG(29562872), // Random number generator to use t0, // Events start being delivered from this time - lambda_e)); // Expected frequency (events per ms) + lambda_e, // Expected frequency (events per ms) + RNG(29562872))); // Random number generator to use // Add inhibitory generator gens.emplace_back( - pgen(cell_member_type{0,0}, w_i, RNG(86543891), t0, lambda_i)); + arb::poisson_generator(cell_member_type{0,0}, w_i, t0, lambda_i, RNG(86543891))); return gens; } diff --git a/example/miniapp/miniapp_recipes.cpp b/example/miniapp/miniapp_recipes.cpp index 50838d1a..62d0ac99 100644 --- a/example/miniapp/miniapp_recipes.cpp +++ b/example/miniapp/miniapp_recipes.cpp @@ -7,8 +7,8 @@ #include <arbor/event_generator.hpp> #include <arbor/mc_cell.hpp> #include <arbor/morphology.hpp> +#include <arbor/schedule.hpp> #include <arbor/spike_source_cell.hpp> -#include <arbor/time_sequence.hpp> #include "io.hpp" @@ -87,9 +87,9 @@ public: } util::unique_any get_cell_description(cell_gid_type i) const override { - // The last 'cell' is a spike source cell. + // The last 'cell' is a spike source cell, producing one spike at t = 0. if (i == ncell_) { - return util::unique_any(spike_source_cell{regular_time_seq(0.0, 0.1, 0.1)}); + return util::unique_any(spike_source_cell{explicit_schedule({0.})}); } auto gen = std::mt19937(i); // TODO: replace this with hashing generator... diff --git a/include/arbor/benchmark_cell.hpp b/include/arbor/benchmark_cell.hpp index cbb32ba1..44795027 100644 --- a/include/arbor/benchmark_cell.hpp +++ b/include/arbor/benchmark_cell.hpp @@ -1,6 +1,6 @@ #pragma once -#include <arbor/time_sequence.hpp> +#include <arbor/schedule.hpp> namespace arb { @@ -9,7 +9,7 @@ namespace arb { struct benchmark_cell { // Describes the time points at which spikes are to be generated. - time_seq time_sequence; + schedule time_sequence; // Time taken in ms to advance the cell one ms of simulation time. // If equal to 1, then a single cell can be advanced in realtime diff --git a/include/arbor/event_generator.hpp b/include/arbor/event_generator.hpp index 02ad77b8..247c53a3 100644 --- a/include/arbor/event_generator.hpp +++ b/include/arbor/event_generator.hpp @@ -9,48 +9,67 @@ #include <arbor/common_types.hpp> #include <arbor/generic_event.hpp> #include <arbor/spike_event.hpp> -#include <arbor/time_sequence.hpp> +#include <arbor/schedule.hpp> namespace arb { -// Generate a postsynaptic spike event that has delivery time set to -// terminal_time. Such events are used as sentinels, to indicate the -// end of a sequence. -inline constexpr -spike_event make_terminal_pse() { - return spike_event{cell_member_type{0,0}, terminal_time, 0}; -} - -inline -bool is_terminal_pse(const spike_event& e) { - return e.time==terminal_time; -} +// 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. +// +// An `event_generator` supports two operations: +// +// `void event_generator::reset()` +// +// Reset generator state. +// +// `event_seq event_generator::events(time_type to, time_type from)` +// +// Provide a non-owning view on to the events in the time interval +// [to, from). +// +// The `event_seq` type is a pair of `spike_event` pointers that +// provide a view onto an internally-maintained contiguous sequence +// of generated spike event objects. This view is valid only for +// the lifetime of the generator, and is invalidated upon a call +// to `reset` or another call to `events`. +// +// Calls to the `events` method must be monotonic in time: without an +// intervening call to `reset`, two successive calls `events(t0, t1)` +// and `events(t2, t3)` to the same event generator must satisfy +// 0 ≤ t0 ≤ t1 ≤ t2 ≤ t3. +// +// `event_generator` objects have value semantics, and use type erasure +// to wrap implementation details. An `event_generator` can be constructed +// from an onbject of an implementation class Impl that is copy-constructible +// and otherwise provides `reset` and `events` methods following the +// API described above. +// +// Some pre-defined event generators are included: +// - `empty_generator`: produces no events +// - `schedule_generator`: events to a fixed target according to a time schedule + +using event_seq = std::pair<const spike_event*, const spike_event*>; // The simplest possible generator that generates no events. // Declared ahead of event_generator so that it can be used as the default // generator. struct empty_generator { - spike_event front() { - return spike_event{cell_member_type{0,0}, terminal_time, 0}; - } - void pop() {} void reset() {} - void advance(time_type t) {}; + event_seq events(time_type, time_type) { + return {&no_event, &no_event}; + } + +private: + static spike_event no_event; }; -// 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. class event_generator { public: - // - // copy, move and constructor interface - // - event_generator(): event_generator(empty_generator()) {} template <typename Impl> @@ -70,40 +89,18 @@ public: return *this; } - // - // event generator interface - // - - // Get the current event in the stream. - // Does not modify the state of the stream, i.e. multiple calls to - // front() will return the same event in the absence of calls to pop(), - // advance() or reset(). - spike_event front() { - return impl_->front(); - } - - // Move the generator to the next event in the stream. - void pop() { - impl_->pop(); - } - - // Reset the generator to the same state that it had on construction. void reset() { impl_->reset(); } - // Update state of the generator such that the event returned by front() is - // the first event with delivery time >= t. - void advance(time_type t) { - return impl_->advance(t); + event_seq events(time_type t0, time_type t1) { + return impl_->events(t0, t1); } private: struct interface { - virtual spike_event front() = 0; - virtual void pop() = 0; - virtual void advance(time_type t) = 0; virtual void reset() = 0; + virtual event_seq events(time_type, time_type) = 0; virtual std::unique_ptr<interface> clone() = 0; virtual ~interface() {} }; @@ -115,16 +112,8 @@ private: explicit wrap(const Impl& impl): wrapped(impl) {} explicit wrap(Impl&& impl): wrapped(std::move(impl)) {} - spike_event front() override { - return wrapped.front(); - } - - void pop() override { - return wrapped.pop(); - } - - void advance(time_type t) override { - return wrapped.advance(t); + event_seq events(time_type t0, time_type t1) override { + return wrapped.events(t0, t1); } void reset() override { @@ -139,153 +128,93 @@ private: }; }; -// Generator that feeds events that are specified with a vector. -// Makes a copy of the input sequence of events. -struct vector_backed_generator { - using pse = spike_event; - vector_backed_generator(cell_member_type target, float weight, std::vector<time_type> samples): - target_(target), - weight_(weight), - tseq_(std::move(samples)) - {} - spike_event front() { - return spike_event{target_, tseq_.front(), weight_}; - } +// Generate events with a fixed target and weight according to +// a provided time schedule. - void pop() { - tseq_.pop(); - } +struct schedule_generator { + schedule_generator(cell_member_type target, float weight, schedule sched): + target_(target), weight_(weight), sched_(std::move(sched)) + {} void reset() { - tseq_.reset(); + sched_.reset(); } - void advance(time_type t) { - tseq_.advance(t); - } + event_seq events(time_type t0, time_type t1) { + auto ts = sched_.events(t0, t1); -private: - cell_member_type target_; - float weight_; - vector_time_seq tseq_; -}; + events_.clear(); + events_.reserve(ts.second-ts.first); -// 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 { - using pse = spike_event; - seq_generator(Seq& events): - events_(events), - it_(std::begin(events_)) - { - arb_assert(std::is_sorted(events_.begin(), events_.end())); - } - - spike_event front() { - return it_==events_.end()? make_terminal_pse(): *it_; - } - - void pop() { - if (it_!=events_.end()) { - ++it_; + for (auto i = ts.first; i!=ts.second; ++i) { + events_.push_back(spike_event{target_, *i, weight_}); } - } - void reset() { - it_ = events_.begin(); - } - - void advance(time_type t) { - it_ = std::lower_bound(events_.begin(), events_.end(), t, event_time_less()); + return {events_.data(), events_.data()+events_.size()}; } private: - const Seq& events_; - typename Seq::const_iterator it_; + pse_vector events_; + cell_member_type target_; + float weight_; + schedule sched_; }; -// 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 { - using pse = spike_event; - - regular_generator(cell_member_type target, - float weight, - time_type tstart, - time_type dt, - time_type tstop=terminal_time): - target_(target), - weight_(weight), - tseq_(tstart, dt, tstop) - {} +// Convenience routines for making schedule_generator: - spike_event front() { - return spike_event{target_, tseq_.front(), weight_}; - } +inline event_generator regular_generator( + cell_member_type target, float weight, time_type tstart, time_type dt, time_type tstop=terminal_time) +{ + return schedule_generator(target, weight, regular_schedule(tstart, dt, tstop)); +} - void pop() { - tseq_.pop(); - } +template <typename RNG> +inline event_generator poisson_generator( + cell_member_type target, float weight, time_type tstart, time_type rate_kHz, const RNG& rng) +{ + return schedule_generator(target, weight, poisson_schedule(tstart, rate_kHz, rng)); +} - void advance(time_type t0) { - tseq_.advance(t0); - } - void reset() { - tseq_.reset(); - } +// Generate events from a predefined sorted event sequence. -private: - cell_member_type target_; - float weight_; - regular_time_seq tseq_; -}; +struct explicit_generator { + explicit_generator() = default; + explicit_generator(const explicit_generator&) = default; + explicit_generator(explicit_generator&&) = default; -// 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 { - using pse = spike_event; - - poisson_generator(cell_member_type target, - float weight, - RandomNumberEngine rng, - time_type tstart, - time_type rate_per_ms, - time_type tstop=terminal_time): - target_(target), - weight_(weight), - tseq_(std::move(rng), tstart, rate_per_ms, tstop) + template <typename Seq> + explicit_generator(const Seq& events): + start_index_(0) { - reset(); - } + using std::begin; + using std::end; - spike_event front() { - return spike_event{target_, tseq_.front(), weight_}; + events_ = pse_vector(begin(events), end(events)); + arb_assert(std::is_sorted(events_.begin(), events_.end())); } - void pop() { - tseq_.pop(); + void reset() { + start_index_ = 0; } - void advance(time_type t0) { - tseq_.advance(t0); - } + event_seq events(time_type t0, time_type t1) { + const spike_event* lb = events_.data()+start_index_; + const spike_event* ub = events_.data()+events_.size(); - void reset() { - tseq_.reset(); + lb = std::lower_bound(lb, ub, t0, event_time_less{}); + ub = std::lower_bound(lb, ub, t1, event_time_less{}); + + start_index_ = ub-events_.data(); + return {lb, ub}; } private: - const cell_member_type target_; - const float weight_; - poisson_time_seq<RandomNumberEngine> tseq_; + pse_vector events_; + std::size_t start_index_ = 0; }; + } // namespace arb diff --git a/include/arbor/schedule.hpp b/include/arbor/schedule.hpp index 7ef42afc..4a9213e0 100644 --- a/include/arbor/schedule.hpp +++ b/include/arbor/schedule.hpp @@ -4,6 +4,7 @@ #include <iterator> #include <memory> #include <random> +#include <utility> #include <vector> #include <arbor/assert.hpp> @@ -14,6 +15,12 @@ namespace arb { +using time_event_span = std::pair<const time_type*, const time_type*>; + +inline time_event_span as_time_event_span(const std::vector<time_type>& v) { + return {&v[0], &v[0]+v.size()}; +} + // A schedule describes a sequence of time values used for sampling. Schedules // are queried monotonically in time: if two method calls `events(t0, t1)` // and `events(t2, t3)` are made without an intervening call to `reset()`, @@ -21,6 +28,8 @@ namespace arb { class schedule { public: + schedule(); + template <typename Impl> explicit schedule(const Impl& impl): impl_(new wrap<Impl>(impl)) {} @@ -40,7 +49,7 @@ public: return *this; } - std::vector<time_type> events(time_type t0, time_type t1) { + time_event_span events(time_type t0, time_type t1) { return impl_->events(t0, t1); } @@ -48,7 +57,7 @@ public: private: struct interface { - virtual std::vector<time_type> events(time_type t0, time_type t1) = 0; + virtual time_event_span events(time_type t0, time_type t1) = 0; virtual void reset() = 0; virtual std::unique_ptr<interface> clone() = 0; virtual ~interface() {} @@ -61,7 +70,7 @@ private: explicit wrap(const Impl& impl): wrapped(impl) {} explicit wrap(Impl&& impl): wrapped(std::move(impl)) {} - virtual std::vector<time_type> events(time_type t0, time_type t1) { + virtual time_event_span events(time_type t0, time_type t1) { return wrapped.events(t0, t1); } @@ -77,27 +86,51 @@ private: }; }; +// Default schedule is empty. + +class empty_schedule { +public: + void reset() {} + time_event_span events(time_type t0, time_type t1) { + static time_type no_time; + return {&no_time, &no_time}; + } +}; + +inline schedule::schedule(): schedule(empty_schedule{}) {} // Common schedules -// Schedule at k·dt for integral k≥0. +// Schedule at k·dt for integral k≥0 within the interval [t0, t1). class regular_schedule_impl { public: - explicit regular_schedule_impl(time_type dt): - dt_(dt), oodt_(1./dt) {}; + explicit regular_schedule_impl(time_type t0, time_type dt, time_type t1): + t0_(t0), t1_(t1), dt_(dt), oodt_(1./dt) + { + if (t0_<0) t0_ = 0; + }; void reset() {} - std::vector<time_type> events(time_type t0, time_type t1); + time_event_span events(time_type t0, time_type t1); private: - time_type dt_; + time_type t0_, t1_, dt_; time_type oodt_; + + std::vector<time_type> times_; }; +inline schedule regular_schedule( + time_type t0, time_type dt, time_type t1 = std::numeric_limits<time_type>::max()) +{ + return schedule(regular_schedule_impl(t0, dt, t1)); +} + inline schedule regular_schedule(time_type dt) { - return schedule(regular_schedule_impl(dt)); + return regular_schedule(0, dt); } + // Schedule at times given explicitly via a provided sorted sequence. class explicit_schedule_impl { public: @@ -119,7 +152,7 @@ public: start_index_ = 0; } - std::vector<time_type> events(time_type t0, time_type t1); + time_event_span events(time_type t0, time_type t1); private: std::ptrdiff_t start_index_; @@ -131,13 +164,17 @@ inline schedule explicit_schedule(const Seq& seq) { return schedule(explicit_schedule_impl(seq)); } +inline schedule explicit_schedule(const std::initializer_list<time_type>& seq) { + return schedule(explicit_schedule_impl(seq)); +} + // Schedule at Poisson point process with rate 1/mean_dt, // restricted to non-negative times. template <typename RandomNumberEngine> class poisson_schedule_impl { public: - poisson_schedule_impl(time_type tstart, time_type mean_dt, const RandomNumberEngine& rng): - tstart_(tstart), exp_(1./mean_dt), rng_(rng), reset_state_(rng), next_(tstart) + poisson_schedule_impl(time_type tstart, time_type rate_kHz, const RandomNumberEngine& rng): + tstart_(tstart), exp_(rate_kHz), rng_(rng), reset_state_(rng), next_(tstart) { arb_assert(tstart_>=0); step(); @@ -149,19 +186,19 @@ public: step(); } - std::vector<time_type> events(time_type t0, time_type t1) { - std::vector<time_type> ts; + time_event_span events(time_type t0, time_type t1) { + times_.clear(); while (next_<t0) { step(); } while (next_<t1) { - ts.push_back(next_); + times_.push_back(next_); step(); } - return ts; + return as_time_event_span(times_); } private: @@ -174,16 +211,17 @@ private: RandomNumberEngine rng_; RandomNumberEngine reset_state_; time_type next_; + std::vector<time_type> times_; }; template <typename RandomNumberEngine> -inline schedule poisson_schedule(time_type mean_dt, const RandomNumberEngine& rng) { - return schedule(poisson_schedule_impl<RandomNumberEngine>(0., mean_dt, rng)); +inline schedule poisson_schedule(time_type rate_kHz, const RandomNumberEngine& rng) { + return schedule(poisson_schedule_impl<RandomNumberEngine>(0., rate_kHz, rng)); } template <typename RandomNumberEngine> -inline schedule poisson_schedule(time_type tstart, time_type mean_dt, const RandomNumberEngine& rng) { - return schedule(poisson_schedule_impl<RandomNumberEngine>(tstart, mean_dt, rng)); +inline schedule poisson_schedule(time_type tstart, time_type rate_kHz, const RandomNumberEngine& rng) { + return schedule(poisson_schedule_impl<RandomNumberEngine>(tstart, rate_kHz, rng)); } } // namespace arb diff --git a/include/arbor/spike_source_cell.hpp b/include/arbor/spike_source_cell.hpp index 74b66e5b..0e9bbf17 100644 --- a/include/arbor/spike_source_cell.hpp +++ b/include/arbor/spike_source_cell.hpp @@ -1,6 +1,6 @@ #pragma once -#include <arbor/time_sequence.hpp> +#include <arbor/schedule.hpp> namespace arb { @@ -8,7 +8,7 @@ namespace arb { // recipe::cell_kind(gid) returning cell_kind::spike_source struct spike_source_cell { - time_seq seq; + schedule seq; }; } // namespace arb diff --git a/include/arbor/time_sequence.hpp b/include/arbor/time_sequence.hpp deleted file mode 100644 index 9d2d0029..00000000 --- a/include/arbor/time_sequence.hpp +++ /dev/null @@ -1,248 +0,0 @@ -#pragma once - -#include <algorithm> -#include <memory> -#include <random> -#include <type_traits> - -#include <arbor/common_types.hpp> - -namespace arb { - -struct empty_time_seq { - time_type front() { return terminal_time; } - void pop() {} - void reset() {} - void advance(time_type t) {}; -}; - -// An time_sequence generates a sequence of time points. -// The sequence of times is always monotonically increasing, i.e. each time is -// not earlier than the time that proceded it. -class time_seq { -public: - // - // copy, move and constructor interface - // - - time_seq(): time_seq(empty_time_seq()) {} - - template < - typename Impl, - typename = std::enable_if_t< - !std::is_same<std::decay_t<Impl>, time_seq>::value> - > - time_seq(Impl&& impl): - impl_(new wrap<Impl>(std::forward<Impl>(impl))) - {} - - time_seq(time_seq&& other) = default; - time_seq& operator=(time_seq&& other) = default; - - time_seq(const time_seq& other): - impl_(other.impl_->clone()) - {} - - time_seq& operator=(const time_seq& other) { - impl_ = other.impl_->clone(); - return *this; - } - - // - // time sequence interface - // - - // Get the current time in the stream. - // Does not modify the state of the stream, i.e. multiple calls to - // front() will return the same time in the absence of calls to pop(), - // advance() or reset(). - time_type front() { - return impl_->front(); - } - - // Move the generator to the front time in the stream. - void pop() { - impl_->pop(); - } - - // Reset the generator to the same state that it had on construction. - void reset() { - impl_->reset(); - } - - // Update state of the generator such that the time returned by front() is - // the first time with delivery time >= t. - void advance(time_type t) { - return impl_->advance(t); - } - -private: - struct interface { - virtual time_type front() = 0; - virtual void pop() = 0; - virtual void advance(time_type t) = 0; - virtual void reset() = 0; - virtual std::unique_ptr<interface> clone() = 0; - virtual ~interface() {} - }; - - std::unique_ptr<interface> impl_; - - template <typename Impl> - struct wrap: interface { - explicit wrap(const Impl& impl): wrapped(impl) {} - explicit wrap(Impl&& impl): wrapped(std::move(impl)) {} - - time_type front() override { - return wrapped.front(); - } - void pop() override { - return wrapped.pop(); - } - void advance(time_type t) override { - return wrapped.advance(t); - } - void reset() override { - wrapped.reset(); - } - std::unique_ptr<interface> clone() override { - return std::unique_ptr<interface>(new wrap<Impl>(wrapped)); - } - - Impl wrapped; - }; -}; - -// Sequence of time points prescribed by a vector -struct vector_time_seq { - vector_time_seq(std::vector<time_type> seq): - seq_(std::move(seq)) - { - // Ensure that the time values are sorted. - if (!std::is_sorted(seq_.begin(), seq_.end())) { - std::sort(seq_.begin(), seq_.end()); - } - reset(); - } - - time_type front() { - return it_==seq_.end()? terminal_time: *it_; - } - - void pop() { - if (it_!=seq_.end()) { - ++it_; - } - } - - void advance(time_type t0) { - it_ = std::lower_bound(seq_.begin(), seq_.end(), t0); - } - - void reset() { - it_ = seq_.begin(); - } - -private: - std::vector<time_type> seq_; - std::vector<time_type>::const_iterator it_; -}; - -// Generates a set of regularly spaced time samples. -// t=t_start+n*dt, ∀ t ∈ [t_start, t_stop) -struct regular_time_seq { - regular_time_seq(time_type tstart, - time_type dt, - time_type tstop=terminal_time): - step_(0), - t_start_(tstart), - dt_(dt), - t_stop_(tstop) - {} - - time_type front() { - const auto t = time(); - return t<t_stop_? t: terminal_time; - } - - void pop() { - ++step_; - } - - void advance(time_type t0) { - 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() { - step_ = 0; - } - -private: - time_type time() const { - return t_start_ + step_*dt_; - } - - std::size_t step_; - time_type t_start_; - time_type dt_; - time_type t_stop_; -}; - -// Generates a stream of time points described by a Poisson point process -// with rate_per_ms samples per ms. -template <typename RandomNumberEngine> -struct poisson_time_seq { - poisson_time_seq(RandomNumberEngine rng, - time_type tstart, - time_type rate_per_ms, - time_type tstop=terminal_time): - exp_(rate_per_ms), - reset_state_(std::move(rng)), - t_start_(tstart), - t_stop_(tstop) - { - reset(); - } - - time_type front() { - return next_<t_stop_? next_: terminal_time; - } - - void pop() { - next_ += exp_(rng_); - } - - void advance(time_type t0) { - while (next_<t0) { - pop(); - } - } - - void reset() { - rng_ = reset_state_; - next_ = t_start_; - pop(); - } - -private: - std::exponential_distribution<time_type> exp_; - RandomNumberEngine rng_; - const RandomNumberEngine reset_state_; - const time_type t_start_; - const time_type t_stop_; - time_type next_; -}; - -} // namespace arb - diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index b2e99d0d..42205d7d 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -71,7 +71,6 @@ set(unit_sources test_strprintf.cpp test_swcio.cpp test_synapses.cpp - test_time_seq.cpp test_thread.cpp test_tree.cpp test_transform.cpp diff --git a/test/unit/test_event_generators.cpp b/test/unit/test_event_generators.cpp index 6004986c..d79eb423 100644 --- a/test/unit/test_event_generators.cpp +++ b/test/unit/test_event_generators.cpp @@ -10,15 +10,8 @@ using namespace arb; namespace{ - pse_vector draw(event_generator& gen, time_type t0, time_type t1) { - gen.reset(); - gen.advance(t0); - pse_vector v; - while (gen.front().time<t1) { - v.push_back(gen.front()); - gen.pop(); - } - return v; + pse_vector as_vector(event_seq s) { + return pse_vector(s.first, s.second); } } @@ -30,8 +23,7 @@ TEST(event_generators, regular) { cell_member_type target = {42, 3}; float weight = 3.14; - //regular_generator gen(t0, dt, target, weight); - regular_generator gen(target, weight, t0, dt); + event_generator gen = regular_generator(target, weight, t0, dt); // helper for building a set of auto expected = [&] (std::vector<time_type> times) { @@ -42,23 +34,15 @@ TEST(event_generators, regular) { 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.front()); - gen.pop(); - } - gen.reset(); - for (auto e: expected({2.0, 2.5, 3.0, 3.5, 4.0, 4.5})) { - EXPECT_EQ(e, gen.front()); - gen.pop(); - } + EXPECT_EQ(expected({2.0, 2.5, 3.0, 3.5, 4.0, 4.5}), as_vector(gen.events(0, 5))); + + // Test reset and re-generate. gen.reset(); + EXPECT_EQ(expected({2.0, 2.5, 3.0, 3.5, 4.0, 4.5}), as_vector(gen.events(0, 5))); - // Test advance - gen.advance(10.1); - EXPECT_EQ(gen.front().time, time_type(10.5)); - gen.advance(12); - EXPECT_EQ(gen.front().time, time_type(12)); + // Test later intervals. + EXPECT_EQ(expected({10.5}), as_vector(gen.events(10.1, 10.7))); + EXPECT_EQ(expected({12, 12.5}), as_vector(gen.events(12, 12.7))); } TEST(event_generators, seq) { @@ -76,57 +60,44 @@ TEST(event_generators, seq) { return pse_vector(in.begin()+b, in.begin()+e); }; - event_generator gen = seq_generator<pse_vector>(in); - - // Test pop, next and reset. - for (auto e: in) { - EXPECT_EQ(e, gen.front()); - gen.pop(); - } + event_generator gen = explicit_generator(in); + EXPECT_EQ(in, as_vector(gen.events(0, 100.))); gen.reset(); - for (auto e: in) { - EXPECT_EQ(e, gen.front()); - gen.pop(); - } - // The loop above should have drained all events from gen, so we expect - // that the front() event will be the special terminal_pse event. - EXPECT_TRUE(is_terminal_pse(gen.front())); - + EXPECT_EQ(in, as_vector(gen.events(0, 100.))); gen.reset(); - // Update of the input sequence, and run tests again to - // verify that results reflect the new set of input events. + // Check reported sub-intervals against a smaller set of 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}, }; + gen = explicit_generator(in); + + auto draw = [](event_generator& gen, time_type t0, time_type t1) { + gen.reset(); + return as_vector(gen.events(t0, t1)); + }; // 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)); @@ -134,32 +105,22 @@ TEST(event_generators, seq) { 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.front().time<t1) { - int1.push_back(gen.front()); - gen.pop(); - } + event_generator gen = poisson_generator(target, weight, t0, lambda, G); + + pse_vector int1 = as_vector(gen.events(0, t1)); // 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.front().time<t1) { - int2.push_back(gen.front()); - gen.pop(); - } - - // Assert that the same sequence was generated + pse_vector int2 = as_vector(gen.events(0, t1)); EXPECT_EQ(int1, int2); } diff --git a/test/unit/test_lif_cell_group.cpp b/test/unit/test_lif_cell_group.cpp index 80b1909b..25297e25 100644 --- a/test/unit/test_lif_cell_group.cpp +++ b/test/unit/test_lif_cell_group.cpp @@ -6,6 +6,7 @@ #include <arbor/load_balance.hpp> #include <arbor/threadinfo.hpp> #include <arbor/recipe.hpp> +#include <arbor/schedule.hpp> #include <arbor/simulation.hpp> #include <arbor/spike_source_cell.hpp> @@ -61,7 +62,7 @@ public: // regularly spiking cell. if (gid == 0) { // Produces just a single spike at time 0ms. - return spike_source_cell{vector_time_seq({0.f})}; + return spike_source_cell{explicit_schedule({0.f})}; } // LIF cell. return lif_cell(); diff --git a/test/unit/test_merge_events.cpp b/test/unit/test_merge_events.cpp index b906c02e..d1756f4e 100644 --- a/test/unit/test_merge_events.cpp +++ b/test/unit/test_merge_events.cpp @@ -1,12 +1,39 @@ #include "../gtest.h" -#include <event_queue.hpp> -#include <merge_events.hpp> +#include <vector> +#include <arbor/event_generator.hpp> +#include <arbor/spike_event.hpp> + +#include "merge_events.hpp" #include "util/rangeutil.hpp" +namespace arb { +void merge_cell_events( + time_type t_from, + time_type t_to, + event_span old_events, + event_span pending, + std::vector<event_generator>& generators, + pse_vector& new_events); +} // namespace arb + using namespace arb; +// Wrapper for arb::merge_cell_events. +static void merge_events( + time_type t_from, + time_type t_to, + const pse_vector& old_events, + pse_vector& pending, + std::vector<event_generator>& generators, + pse_vector& new_events) +{ + util::sort(pending); + merge_cell_events(t_from, t_to, util::range_pointer_view(old_events), util::range_pointer_view(pending), generators, new_events); +} + + std::vector<event_generator> empty_gens; // Test the trivial case of merging empty sets @@ -21,6 +48,7 @@ TEST(merge_events, empty) EXPECT_EQ(lf.size(), 0u); } + // Test the case where there are no events in lc that are to be delivered // after tfinal. TEST(merge_events, no_overlap) @@ -128,9 +156,9 @@ TEST(merge_events, X) {{3, 0}, 26, 4}, }; - std::vector<event_generator> generators(2); - generators.emplace_back( - regular_generator(cell_member_type{4,2}, 42.f, t0, 5)); + std::vector<event_generator> generators = { + regular_generator(cell_member_type{4,2}, 42.f, t0, 5) + }; merge_events(t0, t1, lc, events, generators, lf); @@ -154,7 +182,7 @@ TEST(merge_events, X) // Test the tournament tree for merging two small sequences TEST(merge_events, tourney_seq) { - pse_vector g1 = { + pse_vector evs1 = { {{0, 0}, 1, 1}, {{0, 0}, 2, 2}, {{0, 0}, 3, 3}, @@ -162,7 +190,7 @@ TEST(merge_events, tourney_seq) {{0, 0}, 5, 5}, }; - pse_vector g2 = { + pse_vector evs2 = { {{0, 0}, 1.5, 1}, {{0, 0}, 2.5, 2}, {{0, 0}, 3.5, 3}, @@ -170,10 +198,13 @@ TEST(merge_events, tourney_seq) {{0, 0}, 5.5, 5}, }; - std::vector<event_generator> generators; - generators.emplace_back(seq_generator<pse_vector>(g1)); - generators.emplace_back(seq_generator<pse_vector>(g2)); - impl::tourney_tree tree(generators); + event_generator g1 = explicit_generator(evs1); + event_generator g2 = explicit_generator(evs2); + + std::vector<event_span> spans; + spans.emplace_back(g1.events(0, terminal_time)); + spans.emplace_back(g2.events(0, terminal_time)); + impl::tourney_tree tree(spans); pse_vector lf; while (!tree.empty()) { @@ -182,8 +213,8 @@ TEST(merge_events, tourney_seq) } EXPECT_TRUE(std::is_sorted(lf.begin(), lf.end())); - auto expected = g1; - util::append(expected, g2); + auto expected = evs1; + util::append(expected, evs2); util::sort(expected); EXPECT_EQ(expected, lf); @@ -209,17 +240,16 @@ TEST(merge_events, tourney_poisson) // of events with the same time but different weights works properly. rndgen G(i%(ngen-1)); generators.emplace_back( - poisson_generator<std::mt19937_64>(tgt, weight, G, t0, lambda)); + poisson_generator(tgt, weight, t0, lambda, G)); } // 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.front().time<tfinal) { - expected.push_back(gen.front()); - gen.pop(); - } + event_span evs = gen.events(t0, tfinal); + util::append(expected, evs); + // Reset the generator so that it is ready to generate the same // events again for the tournament tree test. gen.reset(); @@ -228,9 +258,13 @@ TEST(merge_events, tourney_poisson) util::sort(expected); // Generate output using tournament tree in lf. - impl::tourney_tree tree(generators); + std::vector<event_span> spans; + for (auto& gen: generators) { + spans.emplace_back(gen.events(t0, tfinal)); + } + impl::tourney_tree tree(spans); pse_vector lf; - while (!tree.empty(tfinal)) { + while (!tree.empty()) { lf.push_back(tree.head()); tree.pop(); } diff --git a/test/unit/test_schedule.cpp b/test/unit/test_schedule.cpp index 0796352b..c49a1a0e 100644 --- a/test/unit/test_schedule.cpp +++ b/test/unit/test_schedule.cpp @@ -6,8 +6,8 @@ #include <arbor/common_types.hpp> #include <arbor/schedule.hpp> -#include <util/partition.hpp> -#include <util/rangeutil.hpp> +#include "util/partition.hpp" +#include "util/rangeutil.hpp" #include "common.hpp" #include "stats.hpp" @@ -15,6 +15,8 @@ using namespace arb; using namespace testing; +using time_range = util::range<const time_type*>; + // Pull events from n non-contiguous subintervals of [t0, t1) // and check for monotonicity and boundedness. @@ -31,7 +33,7 @@ void run_invariant_checks(schedule S, time_type t0, time_type t1, unsigned n, in bool skip = false; for (auto ival: util::partition_view(divisions)) { if (!skip) { - auto ts = S.events(ival.first, ival.second); + time_range ts = S.events(ival.first, ival.second); EXPECT_TRUE(std::is_sorted(ts.begin(), ts.end())); if (!ts.empty()) { @@ -63,31 +65,37 @@ void run_reset_check(schedule S, time_type t0, time_type t1, unsigned n, int see std::vector<time_type> first; for (auto ival: util::partition_view(first_div)) { - util::append(first, S.events(ival.first, ival.second)); + time_range ts = S.events(ival.first, ival.second); + util::append(first, ts); } S.reset(); std::vector<time_type> second; for (auto ival: util::partition_view(second_div)) { - util::append(second, S.events(ival.first, ival.second)); + time_range ts = S.events(ival.first, ival.second); + util::append(second, ts); } EXPECT_EQ(first, second); } +static std::vector<time_type> as_vector(std::pair<const time_type*, const time_type*> ts) { + return std::vector<time_type>(ts.first, ts.second); +} + TEST(schedule, regular) { // Use exact fp representations for strict equality testing. std::vector<time_type> expected = {0, 0.25, 0.5, 0.75, 1.0}; schedule S = regular_schedule(0.25); - EXPECT_EQ(expected, S.events(0, 1.25)); + EXPECT_EQ(expected, as_vector(S.events(0, 1.25))); S.reset(); - EXPECT_EQ(expected, S.events(0, 1.25)); + EXPECT_EQ(expected, as_vector(S.events(0, 1.25))); S.reset(); expected = {0.25, 0.5, 0.75, 1.0}; - EXPECT_EQ(expected, S.events(0.1, 1.01)); + EXPECT_EQ(expected, as_vector(S.events(0.1, 1.01))); } TEST(schedule, regular_invariants) { @@ -100,19 +108,54 @@ TEST(schedule, regular_reset) { run_reset_check(regular_schedule(0.3), 3, 12, 7); } +TEST(schedule, regular_rounding) { + // Test for consistent behaviour in the face of rounding at large time values. + // Example: with t1, dt below, and int n = floor(t0/dt), + // then n*dt is not the smallest multiple of dt greater than or equal to t0. + // In fact, (n-4)*dt is still greater than t0. + + time_type t1 = 1802667.f; + time_type dt = 0.024999f; + + time_type t0 = t1-10*dt; + time_type t2 = t1+10*dt; + + schedule S = regular_schedule(t0, dt); + auto int_l = as_vector(S.events(t0, t1)); + auto int_r = as_vector(S.events(t1, t2)); + + S.reset(); + auto int_a = as_vector(S.events(t0, t2)); + + EXPECT_GE(int_l.front(), t0); + EXPECT_LT(int_l.back(), t1); + + EXPECT_GE(int_r.front(), t1); + EXPECT_LT(int_r.back(), t2); + + EXPECT_GE(int_a.front(), t0); + EXPECT_LT(int_a.back(), t2); + + std::vector<time_type> int_merged = int_l; + util::append(int_merged, int_r); + + EXPECT_EQ(int_merged, int_a); + EXPECT_TRUE(util::is_sorted(int_a)); +} + TEST(schedule, explicit_schedule) { time_type times[] = {0.1, 0.3, 1.0, 1.25, 1.7, 2.2}; std::vector<time_type> expected = {0.1, 0.3, 1.0}; schedule S = explicit_schedule(times); - EXPECT_EQ(expected, S.events(0, 1.25)); + EXPECT_EQ(expected, as_vector(S.events(0, 1.25))); S.reset(); - EXPECT_EQ(expected, S.events(0, 1.25)); + EXPECT_EQ(expected, as_vector(S.events(0, 1.25))); S.reset(); expected = {0.3, 1.0, 1.25, 1.7}; - EXPECT_EQ(expected, S.events(0.3, 1.71)); + EXPECT_EQ(expected, as_vector(S.events(0.3, 1.71))); } TEST(schedule, explicit_invariants) { @@ -157,11 +200,11 @@ private: }; template <typename RNG> -double poisson_schedule_dispersion(int nbin, double mean_dt, RNG& G) { - schedule S = poisson_schedule(mean_dt, G); +double poisson_schedule_dispersion(int nbin, double rate_kHz, RNG& G) { + schedule S = poisson_schedule(rate_kHz, G); std::vector<int> bin(nbin); - for (auto t: S.events(0, nbin)) { + for (auto t: time_range(S.events(0, nbin))) { int j = (int)t; if (j<0 || j>=nbin) { throw std::logic_error("poisson schedule result out of bounds"); @@ -196,7 +239,7 @@ TEST(schedule, poisson_uniformity) { constexpr double chi2_ub = 1118.9480663231843; std::mt19937_64 G; - double dispersion = poisson_schedule_dispersion(N, 1.23, G); + double dispersion = poisson_schedule_dispersion(N, .813, G); double test_value = N*dispersion; EXPECT_GT(test_value, chi2_lb); EXPECT_LT(test_value, chi2_ub); @@ -204,8 +247,8 @@ TEST(schedule, poisson_uniformity) { // Run one sample K-S test for uniformity, with critical // value for the finite K-S statistic Dn of α=0.01. - schedule S = poisson_schedule(1./100, G); - auto events = S.events(0,1); + schedule S = poisson_schedule(100., G); + auto events = as_vector(S.events(0,1)); int n = (int)events.size(); double dn = ks::dn_statistic(events); @@ -215,13 +258,13 @@ TEST(schedule, poisson_uniformity) { // source. skew_adaptor<std::mt19937_64> W(1.5); - dispersion = poisson_schedule_dispersion(N, 1.23, W); + dispersion = poisson_schedule_dispersion(N, .813, W); test_value = N*dispersion; EXPECT_FALSE(test_value>=chi2_lb && test_value<=chi2_ub); - S = poisson_schedule(1./100, W); - events = S.events(0,1); + S = poisson_schedule(100., W); + events = as_vector(S.events(0,1)); n = (int)events.size(); dn = ks::dn_statistic(events); @@ -242,8 +285,8 @@ TEST(schedule, poisson_rate) { constexpr double lambda = 123.4; std::mt19937_64 G; - schedule S = poisson_schedule(1./lambda, G); - int n = (int)S.events(0,1).size(); + schedule S = poisson_schedule(lambda, G); + int n = (int)time_range(S.events(0, 1)).size(); double cdf = poisson::poisson_cdf_approx(n, lambda); EXPECT_GT(cdf, alpha/2); @@ -253,8 +296,8 @@ TEST(schedule, poisson_rate) { // source. skew_adaptor<std::mt19937_64> W(1.5); - S = poisson_schedule(1./lambda, W); - n = (int)S.events(0,1).size(); + S = poisson_schedule(lambda, W); + n = (int)time_range(S.events(0, 1)).size(); cdf = poisson::poisson_cdf_approx(n, lambda); EXPECT_FALSE(cdf>=alpha/2 && cdf<=1-alpha/2); @@ -264,14 +307,14 @@ TEST(schedule, poisson_invariants) { SCOPED_TRACE("poisson_invariants"); std::mt19937_64 G; G.discard(100); - run_invariant_checks(poisson_schedule(12.3, G), 5.1, 15.3, 7); + run_invariant_checks(poisson_schedule(0.81, G), 5.1, 15.3, 7); } TEST(schedule, poisson_reset) { SCOPED_TRACE("poisson_reset"); std::mt19937_64 G; G.discard(200); - run_reset_check(poisson_schedule(9.1, G), 1, 10, 7); + run_reset_check(poisson_schedule(.11, G), 1, 10, 7); } TEST(schedule, poisson_offset) { @@ -284,7 +327,7 @@ TEST(schedule, poisson_offset) { G1.discard(300); std::vector<time_type> expected; - for (auto t: poisson_schedule(3.4, G1).events(0., 100.)) { + for (auto t: as_vector(poisson_schedule(.234, G1).events(0., 100.))) { t += offset; if (t<100.) { expected.push_back(t); @@ -294,13 +337,14 @@ TEST(schedule, poisson_offset) { std::mt19937_64 G2; G2.discard(300); - EXPECT_TRUE(seq_almost_eq<time_type>(expected, poisson_schedule(offset, 3.4, G2).events(0., 100.))); + EXPECT_TRUE(seq_almost_eq<time_type>(expected, + as_vector(poisson_schedule(offset, .234, G2).events(0., 100.)))); } TEST(schedule, poisson_offset_reset) { SCOPED_TRACE("poisson_reset"); std::mt19937_64 G; G.discard(400); - run_reset_check(poisson_schedule(0.3, 9.1, G), 1, 10, 7); + run_reset_check(poisson_schedule(3.3, 9.1, G), 1, 10, 7); } diff --git a/test/unit/test_spike_source.cpp b/test/unit/test_spike_source.cpp index b877a64a..8892b774 100644 --- a/test/unit/test_spike_source.cpp +++ b/test/unit/test_spike_source.cpp @@ -1,7 +1,8 @@ #include "../gtest.h" +#include <arbor/schedule.hpp> +#include <arbor/spike.hpp> #include <arbor/spike_source_cell.hpp> -#include <arbor/time_sequence.hpp> #include <arbor/util/unique_any.hpp> #include "spike_source_cell_group.hpp" @@ -10,57 +11,61 @@ using namespace arb; using ss_recipe = homogeneous_recipe<cell_kind::spike_source, spike_source_cell>; -using pseq = arb::poisson_time_seq<std::mt19937_64>; // Test that a spike_source_cell_group identifies itself with the correct // cell_kind enum value. TEST(spike_source, cell_kind) { - ss_recipe rec(1u, spike_source_cell{vector_time_seq({})}); + ss_recipe rec(1u, spike_source_cell{explicit_schedule({})}); spike_source_cell_group group({0}, rec); EXPECT_EQ(cell_kind::spike_source, group.get_cell_kind()); } -// Test that a spike_source_cell_group produces a sequence spikes with spike +static std::vector<time_type> as_vector(std::pair<const time_type*, const time_type*> ts) { + return std::vector<time_type>(ts.first, ts.second); +} + +static std::vector<time_type> spike_times(const std::vector<spike>& evs) { + std::vector<time_type> ts; + for (auto& s: evs) { + ts.push_back(s.time); + } + return ts; +} + +// Test that a spike_source_cell_group produces a sequence of spikes with spike // times corresponding to the underlying time_seq. TEST(spike_source, matches_time_seq) { - auto test_seq = [](arb::time_seq seq) { + auto test_seq = [](schedule seq) { ss_recipe rec(1u, spike_source_cell{seq}); spike_source_cell_group group({0}, rec); // epoch ending at 10ms epoch ep(0, 10); group.advance(ep, 1, {}); - for (auto s: group.spikes()) { - EXPECT_EQ(s.time, seq.front()); - seq.pop(); - } - EXPECT_TRUE(seq.front()>=ep.tfinal); + EXPECT_EQ(spike_times(group.spikes()), as_vector(seq.events(0, 10))); + group.clear_spikes(); // advance to 20 ms and repeat ep.advance(20); group.advance(ep, 1, {}); - for (auto s: group.spikes()) { - EXPECT_EQ(s.time, seq.front()); - seq.pop(); - } - EXPECT_TRUE(seq.front()>=ep.tfinal); + EXPECT_EQ(spike_times(group.spikes()), as_vector(seq.events(10, 20))); }; std::mt19937_64 G; - test_seq(arb::regular_time_seq(0,1)); - test_seq(pseq(G, 0., 10)); // produce many spikes in each interval - test_seq(pseq(G, 0., 1e-6)); // very unlikely to produce any spikes in either interval + test_seq(regular_schedule(0, 1)); + test_seq(poisson_schedule(10, G)); // produce many spikes in each interval + test_seq(poisson_schedule(1e-6, G)); // very unlikely to produce any spikes in either interval } // Test that a spike_source_cell_group will produce the same sequence of spikes // after being reset. TEST(spike_source, reset) { - auto test_seq = [](arb::time_seq seq) { + auto test_seq = [](schedule seq) { ss_recipe rec(1u, spike_source_cell{seq}); spike_source_cell_group group({0}, rec); @@ -80,9 +85,9 @@ TEST(spike_source, reset) }; std::mt19937_64 G; - test_seq(arb::regular_time_seq(0,1)); - test_seq(pseq(G, 0., 10)); // produce many spikes in each interval - test_seq(pseq(G, 0., 1e-6)); // very unlikely to produce any spikes in either interval + test_seq(regular_schedule(0, 1)); + test_seq(poisson_schedule(10, G)); // produce many spikes in each interval + test_seq(poisson_schedule(1e-6, G)); // very unlikely to produce any spikes in either interval } // Test that a spike_source_cell_group will produce the expected @@ -90,26 +95,19 @@ TEST(spike_source, reset) TEST(spike_source, exhaust) { // This test assumes that seq will exhaust itself before t=10 ms. - auto test_seq = [](arb::time_seq seq) { + auto test_seq = [](schedule seq) { ss_recipe rec(1u, spike_source_cell{seq}); spike_source_cell_group group({0}, rec); // epoch ending at 10ms epoch ep(0, 10); group.advance(ep, 1, {}); - auto spikes = group.spikes(); - for (auto s: group.spikes()) { - EXPECT_EQ(s.time, seq.front()); - seq.pop(); - } - // The sequence shoule be exhausted, in which case the next value in the - // sequence should be marked as time_max. - EXPECT_EQ(seq.front(), arb::terminal_time); + EXPECT_EQ(spike_times(group.spikes()), as_vector(seq.events(0, 10))); + // Check that the last spike was before the end of the epoch. - EXPECT_LT(spikes.back().time, time_type(10)); + EXPECT_LT(group.spikes().back().time, time_type(10)); }; - std::mt19937_64 G; - test_seq(arb::regular_time_seq(0,1,5)); - test_seq(pseq(G, 0., 10, 5)); + test_seq(regular_schedule(0, 1, 5)); + test_seq(explicit_schedule({0.3, 2.3, 4.7})); } diff --git a/test/unit/test_time_seq.cpp b/test/unit/test_time_seq.cpp deleted file mode 100644 index b4166279..00000000 --- a/test/unit/test_time_seq.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#include "../gtest.h" - -#include <vector> - -#include <arbor/time_sequence.hpp> - -#include "util/rangeutil.hpp" - -#include "common.hpp" - -using namespace arb; - -namespace{ - // Helper function that draws all samples in the half open interval - // t ∈ [t0, t1) from a time_seq. - std::vector<time_type> draw(time_seq& gen, time_type t0, time_type t1) { - gen.reset(); - gen.advance(t0); - std::vector<time_type> v; - while (gen.front()<t1) { - v.push_back(gen.front()); - gen.pop(); - } - return v; - } -} - -TEST(time_seq, vector) { - std::vector<time_type> times = {0.1, 1.0, 1.0, 1.5, 2.3, 3.0, 3.5, }; - - vector_time_seq seq(times); - - // Test pop, next and reset. - for (auto t: times) { - EXPECT_EQ(t, seq.front()); - seq.pop(); - } - seq.reset(); - for (auto t: times) { - EXPECT_EQ(t, seq.front()); - seq.pop(); - } - - // The loop above should have drained all samples from seq, so we expect - // that the front() time sample will be terminal_time. - EXPECT_EQ(seq.front(), terminal_time); -} - -TEST(time_seq, regular) { - // make a regular generator that generates its first event at t=2ms and subsequent - // events regularly spaced 0.5 ms apart. - regular_time_seq seq(2, 0.5); - - // Test pop, next and reset. - for (auto e: {2.0, 2.5, 3.0, 3.5, 4.0, 4.5}) { - EXPECT_EQ(e, seq.front()); - seq.pop(); - } - seq.reset(); - for (auto e: {2.0, 2.5, 3.0, 3.5, 4.0, 4.5}) { - EXPECT_EQ(e, seq.front()); - seq.pop(); - } - seq.reset(); - - // Test advance() - - seq.advance(10.1); - // next event greater ≥ 10.1 should be 10.5 - EXPECT_EQ(seq.front(), time_type(10.5)); - - seq.advance(12); - // next event greater ≥ 12 should be 12 - EXPECT_EQ(seq.front(), time_type(12)); -} - -// Test for rounding problems with large time values and the regular sequence -TEST(time_seq, regular_rounding) { - // make a regular generator that generates its first time point at t=2ms - // and subsequent times regularly spaced 0.5 ms apart. - time_type t0 = 2.0; - time_type dt = 0.5; - - // 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; - time_seq seq = regular_time_seq(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 points from each interval then merge them, we expect same set - // of points as when we draw from that large interval. - std::vector<time_type> int_l = draw(seq, t0, t1); - std::vector<time_type> int_r = draw(seq, t1, t2); - std::vector<time_type> int_a = draw(seq, t0, t2); - - std::vector<time_type> int_merged = int_l; - util::append(int_merged, int_r); - - EXPECT_TRUE(int_l.front() >= t0); - EXPECT_TRUE(int_l.back() < t1); - EXPECT_TRUE(int_r.front() >= t1); - EXPECT_TRUE(int_r.back() < t2); - EXPECT_EQ(int_a, int_merged); - EXPECT_TRUE(std::is_sorted(int_a.begin(), int_a.end())); -} - -TEST(time_seq, poisson) { - std::mt19937_64 G; - using pseq = poisson_time_seq<std::mt19937_64>; - - time_type t0 = 0; - time_type t1 = 10; - time_type lambda = 10; // expect 10 samples per ms - - pseq seq(G, t0, lambda); - - std::vector<time_type> int1; - while (seq.front()<t1) { - int1.push_back(seq.front()); - seq.pop(); - } - // Test that the output is sorted - EXPECT_TRUE(std::is_sorted(int1.begin(), int1.end())); - - // Reset and generate the same sequence of time points - seq.reset(); - std::vector<time_type> int2; - while (seq.front()<t1) { - int2.push_back(seq.front()); - seq.pop(); - } - - // Assert that the same sequence was generated - EXPECT_EQ(int1, int2); -} - -// Test a poisson generator that has a tstop past which no samples -// should be generated. -TEST(time_seq, poisson_terminates) { - std::mt19937_64 G; - using pseq = poisson_time_seq<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 samples per ms - - // construct sequence with explicit end time t1 - pseq seq(G, t0, lambda, t1); - - std::vector<time_type> sequence; - // pull samples off the sequence well past the end of the end time t1 - while (seq.front()<t2) { - sequence.push_back(seq.front()); - seq.pop(); - } - - // the sequence should be exhausted - EXPECT_EQ(seq.front(), terminal_time); - - // the last sample should be less than the end time - EXPECT_TRUE(sequence.back()<t1); -} -- GitLab