diff --git a/CMakeLists.txt b/CMakeLists.txt index ef36beed8e..0e4914ecd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -357,8 +357,7 @@ if (BUILD_TESTING) OPTIONS "BENCHMARK_ENABLE_TESTING OFF" "CMAKE_BUILD_TYPE release" "BUILD_SHARED_LIBS OFF") CPMFindPackage(NAME googletest GITHUB_REPOSITORY google/googletest - GIT_TAG release-1.12.1 - VERSION 1.12.1 + VERSION 1.17.0 OPTIONS "INSTALL_GTEST OFF" "BUILD_GMOCK OFF") if(benchmark_ADDED) target_link_libraries(ext-bench INTERFACE benchmark) diff --git a/arbor/backends/multicore/shared_state.cpp b/arbor/backends/multicore/shared_state.cpp index 96fe82d9a9..7a1cddb81d 100644 --- a/arbor/backends/multicore/shared_state.cpp +++ b/arbor/backends/multicore/shared_state.cpp @@ -23,7 +23,6 @@ #include "multicore_common.hpp" #include "shared_state.hpp" -#include "fvm.hpp" namespace arb { namespace multicore { @@ -183,7 +182,6 @@ void istim_state::add_current(const arb_value_type time, array& current_density) } // shared_state methods: - shared_state::shared_state(task_system_handle, // ignored in mc backend arb_size_type n_cell, arb_size_type n_cv_, @@ -400,10 +398,8 @@ void shared_state::instantiate(arb::mechanism& m, bool peer_indices = !pos_data.peer_cv.empty(); // store indices for random number generation - if (m.mech_.n_random_variables) { - store.gid_ = pos_data.gid; - store.idx_ = pos_data.idx; - } + store.gid_ = pos_data.gid; + if (m.mech_.n_random_variables) store.idx_ = pos_data.idx; // Allocate view pointers (except globals!) store.state_vars_.resize(m.mech_.n_state_vars); m.ppack_.state_vars = store.state_vars_.data(); @@ -533,5 +529,14 @@ void shared_state::instantiate(arb::mechanism& m, } } +void shared_state::update_density_data(cell_gid_type lid, arb_mechanism_ppack& ppack, cell_gid_type pid, arb_value_type val) { + for (auto idx = 0ul; idx < ppack.width; ++idx) { + if (lid != ppack.vec_ci[ppack.node_index[idx]]) continue; + std::cerr << "old=" << ppack.parameters[pid][idx]; + ppack.parameters[pid][idx] = val; + std::cerr << " new=" << ppack.parameters[pid][idx] << '\n'; + } +} + } // namespace multicore } // namespace arb diff --git a/arbor/backends/multicore/shared_state.hpp b/arbor/backends/multicore/shared_state.hpp index 5353ecd8da..3df41947e2 100644 --- a/arbor/backends/multicore/shared_state.hpp +++ b/arbor/backends/multicore/shared_state.hpp @@ -239,6 +239,8 @@ struct ARB_ARBOR_API shared_state: sample_time_host = util::range_pointer_view(sample_time); sample_value_host = util::range_pointer_view(sample_value); } + + void update_density_data(cell_gid_type gid, arb_mechanism_ppack& ppack, cell_gid_type pid, arb_value_type val); }; // For debugging only: diff --git a/arbor/backends/shared_state_base.hpp b/arbor/backends/shared_state_base.hpp index 51bd2afd63..e404c1cd43 100644 --- a/arbor/backends/shared_state_base.hpp +++ b/arbor/backends/shared_state_base.hpp @@ -42,8 +42,8 @@ struct shared_state_base { // samples auto n_samples = util::sum_by(samples, [] (const auto& s) {return s.size();}); if (d->sample_time.size() < n_samples) { - d->sample_time = array(n_samples); - d->sample_value = array(n_samples); + d->sample_time.resize(n_samples); + d->sample_value.resize(n_samples); } initialize(samples, d->sample_events); // thresholds @@ -80,6 +80,11 @@ struct shared_state_base { } } + void update_density_data(cell_gid_type gid, arb_mechanism_ppack& ppack, cell_gid_type pid, arb_value_type val) { + auto d = static_cast(this); + d->update_density_data(gid, ppack, pid, val); + } + arb_value_type* mechanism_state_data(const mechanism& m, const std::string& key) { auto d = static_cast(this); diff --git a/arbor/benchmark_cell_group.cpp b/arbor/benchmark_cell_group.cpp index 36887fc8ea..09f927ce9c 100644 --- a/arbor/benchmark_cell_group.cpp +++ b/arbor/benchmark_cell_group.cpp @@ -11,6 +11,7 @@ #include "profile/profiler_macro.hpp" #include "util/span.hpp" +#include "util/maputil.hpp" template void serialize(arb::serializer& s, const K& k, const arb::benchmark_cell_group&); @@ -23,8 +24,7 @@ benchmark_cell_group::benchmark_cell_group(const std::vector& gid const recipe& rec, cell_label_range& cg_sources, cell_label_range& cg_targets): - gids_(gids) -{ + gids_(gids) { for (auto gid: gids_) { if (!rec.get_probes(gid).empty()) { throw bad_cell_probe(cell_kind::benchmark, gid); @@ -47,28 +47,39 @@ benchmark_cell_group::benchmark_cell_group(const std::vector& gid } void benchmark_cell_group::reset() { - for (auto& c: cells_) { - c.time_sequence.reset(); - } - + for (auto& c: cells_) c.time_sequence.reset(); clear_spikes(); } -void benchmark_cell_group::t_serialize(serializer& ser, const std::string& k) const { - serialize(ser, k, *this); -} -void benchmark_cell_group::t_deserialize(serializer& ser, const std::string& k) { - deserialize(ser, k, *this); +void +benchmark_cell_group::edit_cell(cell_gid_type gid, std::any cell_edit) { + try { + auto bench_edit = std::any_cast(cell_edit); + auto lid = util::binary_search_index(gids_, gid); + if (!lid) throw arb::arbor_internal_error{"gid " + std::to_string(gid) + " erroneuosly dispatched to cell group."}; + benchmark_cell& lowered = cells_[*lid]; + auto tmp = benchmark_cell{.source=lowered.source, .target=lowered.target, .time_sequence=std::move(lowered.time_sequence), .realtime_ratio=lowered.realtime_ratio}; + bench_edit(tmp); + if (tmp.source != lowered.source) throw bad_cell_edit(gid, "Source is not editable."); + if (tmp.target != lowered.target) throw bad_cell_edit(gid, "Target is not editable."); + // Write back + lowered.time_sequence = std::move(tmp.time_sequence); + lowered.realtime_ratio = tmp.realtime_ratio; + } + catch (const std::bad_any_cast&) { + throw bad_cell_edit(gid, "Not a Benchmark editor (C++ type-id: '" + std::string{cell_edit.type().name()} + "')"); + } } -cell_kind benchmark_cell_group::get_cell_kind() const { - return cell_kind::benchmark; -} +void benchmark_cell_group::t_serialize(serializer& ser, const std::string& k) const { serialize(ser, k, *this); } + +void benchmark_cell_group::t_deserialize(serializer& ser, const std::string& k) { deserialize(ser, k, *this); } + +cell_kind benchmark_cell_group::get_cell_kind() const { return cell_kind::benchmark; } void benchmark_cell_group::advance(epoch ep, time_type dt, - const event_lane_subrange& event_lanes) -{ + const event_lane_subrange& event_lanes) { using std::chrono::high_resolution_clock; using duration_type = std::chrono::duration; @@ -97,13 +108,9 @@ void benchmark_cell_group::advance(epoch ep, PL(); }; -const std::vector& benchmark_cell_group::spikes() const { - return spikes_; -} +const std::vector& benchmark_cell_group::spikes() const { return spikes_; } -void benchmark_cell_group::clear_spikes() { - spikes_.clear(); -} +void benchmark_cell_group::clear_spikes() { spikes_.clear(); } void benchmark_cell_group::add_sampler(sampler_association_handle h, cell_member_predicate probeset_ids, diff --git a/arbor/benchmark_cell_group.hpp b/arbor/benchmark_cell_group.hpp index 118165a726..3f8f732dc8 100644 --- a/arbor/benchmark_cell_group.hpp +++ b/arbor/benchmark_cell_group.hpp @@ -32,6 +32,8 @@ class benchmark_cell_group: public cell_group { void remove_all_samplers() override {} + void edit_cell(cell_gid_type gid, std::any edit) override; + ARB_SERDES_ENABLE(benchmark_cell_group, cells_, spikes_, gids_); void t_serialize(serializer& ser, const std::string& k) const override; diff --git a/arbor/cable_cell_group.cpp b/arbor/cable_cell_group.cpp index f04ac941d6..260ecbf4ef 100644 --- a/arbor/cable_cell_group.cpp +++ b/arbor/cable_cell_group.cpp @@ -18,6 +18,7 @@ #include "util/partition.hpp" #include "util/range.hpp" #include "util/span.hpp" +#include "util/maputil.hpp" namespace arb { @@ -478,6 +479,20 @@ void cable_cell_group::remove_all_samplers() { sampler_map_.clear(); } +void +cable_cell_group::edit_cell(cell_gid_type gid, std::any cell_edit) { + auto lid = util::binary_search_index(gids_, gid); + if (!lid) throw arb::arbor_internal_error{"gid " + std::to_string(gid) + " erroneuosly dispatched to cell group."}; + + if (auto cable_edit = std::any_cast(&cell_edit); cable_edit) { + lowered_->edit_density_parameter(gid, *lid, *cable_edit); + } + else { + throw bad_cell_edit(gid, "Not a Cable Cell editor (C++ type-id: '" + std::string{cell_edit.type().name()} + "')"); + } + +} + std::vector cable_cell_group::get_probe_metadata(const cell_address_type& probeset_id) const { // SAFETY: Probe associations are fixed after construction, so we do not // need to grab the mutex. diff --git a/arbor/cable_cell_group.hpp b/arbor/cable_cell_group.hpp index 7403d82ef4..a79b61e868 100644 --- a/arbor/cable_cell_group.hpp +++ b/arbor/cable_cell_group.hpp @@ -44,6 +44,8 @@ struct ARB_ARBOR_API cable_cell_group: public cell_group { void remove_all_samplers() override; + void edit_cell(cell_gid_type gid, std::any edit) override; + std::vector get_probe_metadata(const cell_address_type&) const override; ARB_SERDES_ENABLE(cable_cell_group, gids_, spikes_, lowered_); diff --git a/arbor/cell_group.hpp b/arbor/cell_group.hpp index f7689dd973..6ac757a836 100644 --- a/arbor/cell_group.hpp +++ b/arbor/cell_group.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "epoch.hpp" #include "event_lane.hpp" @@ -20,8 +21,7 @@ // ranges are needed to map (gid, label) pairs to their corresponding lid sets. namespace arb { -class cell_group { -public: +struct cell_group { virtual ~cell_group() = default; virtual cell_kind get_cell_kind() const = 0; @@ -32,20 +32,23 @@ class cell_group { virtual const std::vector& spikes() const = 0; virtual void clear_spikes() = 0; - // Sampler association methods below should be thread-safe, as they might be invoked - // from a sampler call back called from a different cell group running on a different thread. - + // Sampler association methods below should be thread-safe, as they might be + // invoked from a sampler call back called from a different cell group + // running on a different thread. virtual void add_sampler(sampler_association_handle, cell_member_predicate, schedule, sampler_function) = 0; virtual void remove_sampler(sampler_association_handle) = 0; virtual void remove_all_samplers() = 0; - // Probe metadata queries might also be called while a simulation is running, and so should - // also be thread-safe. + // allow editing of certain cell properties + virtual void edit_cell(cell_gid_type gid, std::any edit) = 0; + // Probe metadata queries might also be called while a simulation is + // running, and so should also be thread-safe. virtual std::vector get_probe_metadata(const cell_address_type&) const { return {}; } + // trampolines for serialization virtual void t_serialize(serializer& s, const std::string&) const = 0; - virtual void t_deserialize(serializer& s, const std::string&) = 0; + virtual void t_deserialize(serializer& s, const std::string&) = 0; }; using cell_group_ptr = std::unique_ptr; diff --git a/arbor/fvm_lowered_cell.hpp b/arbor/fvm_lowered_cell.hpp index de36605c67..fbaf1150a7 100644 --- a/arbor/fvm_lowered_cell.hpp +++ b/arbor/fvm_lowered_cell.hpp @@ -231,12 +231,15 @@ struct fvm_initialization_data { struct fvm_lowered_cell { virtual void reset() = 0; - virtual fvm_initialization_data initialize(const std::vector& gids, const recipe& rec) = 0; + virtual fvm_initialization_data initialize(const std::vector& gids, + const recipe& rec) = 0; virtual fvm_integration_result integrate(const timestep_range& dts, const event_lane_subrange& event_lanes, const std::vector>& staged_samples) = 0; + virtual void edit_density_parameter(cell_gid_type gid, cell_gid_type lid, const cable_cell_density_editor& edit) = 0; + virtual arb_value_type time() const = 0; virtual ~fvm_lowered_cell() {} diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp index 7499ce75c1..feb8abaa16 100644 --- a/arbor/fvm_lowered_cell_impl.hpp +++ b/arbor/fvm_lowered_cell_impl.hpp @@ -12,6 +12,8 @@ #include #include +#include + #include #include #include @@ -55,8 +57,7 @@ struct fvm_lowered_cell_impl: public fvm_lowered_cell { value_type time() const override { return state_->time; } - //Exposed for testing purposes - std::vector& mechanisms() { return mechanisms_; } + void edit_density_parameter(cell_gid_type, cell_gid_type, const cable_cell_density_editor&) override; ARB_SERDES_ENABLE(fvm_lowered_cell_impl, seed_, state_); @@ -317,6 +318,24 @@ fvm_lowered_cell_impl::add_probes(const std::vector& gid } } +template void +fvm_lowered_cell_impl::edit_density_parameter(cell_gid_type gid, + cell_gid_type lid, + const cable_cell_density_editor& edit) { + std::cerr << "gid=" << gid << " lid=" << lid << '\n'; + auto mech = std::find_if(mechanisms_.begin(), mechanisms_.end(), [&edit](const auto& m) { return m->internal_name() == edit.mechanism; }); + if (mech == mechanisms_.end()) throw bad_cell_edit{gid, "no such mechanism: " + edit.mechanism}; + auto ptr = mech->get(); + if (ptr->kind() != arb_mechanism_kind_density) throw bad_cell_edit{gid, "not a density mechanism: " + edit.mechanism}; + auto params = util::make_range(ptr->mech_.parameters, ptr->mech_.parameters + ptr->mech_.n_parameters); + for (const auto& [key, val]: edit.values) { + auto param = std::find_if(params.begin(), params.end(), [&key](const auto& p) { return p.name == key; }); + if (params.end() == param) throw bad_cell_edit{gid, "no paramter " + key + " in mechanism: " + edit.mechanism}; + auto pid = param - params.begin(); + state_->update_density_data(lid, ptr->ppack_, pid, val); + } +} + template fvm_initialization_data fvm_lowered_cell_impl::initialize(const std::vector& gids, const recipe& rec) { @@ -402,8 +421,8 @@ fvm_lowered_cell_impl::initialize(const std::vector& gid auto it = util::max_element_by(fvm_info.num_sources, [](auto elem) {return util::second(elem);}); max_detector = it->second; } - std::vector src_to_spike, cv_to_cell; + std::vector src_to_spike; if (post_events_) { for (auto cell_idx: make_span(ncell)) { for (auto lid: make_span(fvm_info.num_sources[gids[cell_idx]])) { @@ -411,9 +430,10 @@ fvm_lowered_cell_impl::initialize(const std::vector& gid } } src_to_spike.shrink_to_fit(); - cv_to_cell = D.geometry.cv_to_cell; } + auto cv_to_cell = D.geometry.cv_to_cell; + // map control volume ids to global cell ids std::vector cv_to_gid(D.geometry.cv_to_cell.size()); std::transform(D.geometry.cv_to_cell.begin(), D.geometry.cv_to_cell.end(), @@ -439,9 +459,9 @@ fvm_lowered_cell_impl::initialize(const std::vector& gid data_alignment? data_alignment: 1u, seed_); + target_handles_.resize(mech_data.n_target); // Keep track of mechanisms by name for probe lookup. std::unordered_map mechptr_by_name; - target_handles_.resize(mech_data.n_target); unsigned mech_id = 0; for (const auto& [name, config]: mech_data.mechanisms) { @@ -535,7 +555,7 @@ fvm_lowered_cell_impl::initialize(const std::vector& gid voltage_mechanisms_.emplace_back(mech.release()); break; } - default:; + default: throw invalid_mechanism_kind(config.kind); } } diff --git a/arbor/include/arbor/arbexcept.hpp b/arbor/include/arbor/arbexcept.hpp index 22ced920ba..d03ff85e11 100644 --- a/arbor/include/arbor/arbexcept.hpp +++ b/arbor/include/arbor/arbexcept.hpp @@ -47,6 +47,12 @@ struct ARB_SYMBOL_VISIBLE resolution_disabled: arbor_exception { {} }; +struct ARB_SYMBOL_VISIBLE bad_cell_edit: arbor_exception { + bad_cell_edit(cell_gid_type gid, std::string why): + arbor_exception("Cannot edit cell gid=" + std::to_string(gid) + ": " + why) + {} + std::string why; +}; struct ARB_SYMBOL_VISIBLE dup_cell_probe: arbor_exception { dup_cell_probe(cell_kind kind, cell_gid_type gid, cell_tag_type tag); diff --git a/arbor/include/arbor/benchmark_cell.hpp b/arbor/include/arbor/benchmark_cell.hpp index f8569fe310..c15ac25f1e 100644 --- a/arbor/include/arbor/benchmark_cell.hpp +++ b/arbor/include/arbor/benchmark_cell.hpp @@ -22,15 +22,11 @@ struct ARB_SYMBOL_VISIBLE benchmark_cell { // 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 - double realtime_ratio; - - benchmark_cell() = default; - benchmark_cell(cell_tag_type source, cell_tag_type target, schedule seq, double ratio): - source(source), target(target), time_sequence(seq), realtime_ratio(ratio) {}; + double realtime_ratio = 1.0; ARB_SERDES_ENABLE(benchmark_cell, source, target, time_sequence, realtime_ratio); }; -} // namespace arb - +using benchmark_cell_editor = std::function; +} // namespace arb diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp index 8215c2e0f2..65138c95e9 100644 --- a/arbor/include/arbor/cable_cell.hpp +++ b/arbor/include/arbor/cable_cell.hpp @@ -242,6 +242,14 @@ using location_assignment = std::conditional_t::value | std::unordered_map>, mlocation_map>; +// Allowed edits on cable cells + +// Overwrite a list of named parameters on a given mechanism +struct cable_cell_density_editor { + std::string mechanism; + std::unordered_map values; +}; + // High-level abstract representation of a cell. struct ARB_SYMBOL_VISIBLE cable_cell { using lid_range_map = std::unordered_multimap; @@ -275,10 +283,10 @@ struct ARB_SYMBOL_VISIBLE cable_cell { const mprovider& provider() const; // Convenience access to placed items. - const std::unordered_map>& synapses() const; - const std::unordered_map>& junctions() const; - const mlocation_map& detectors() const; - const mlocation_map& stimuli() const; + const location_assignment& synapses() const; + const location_assignment& junctions() const; + const location_assignment& detectors() const; + const location_assignment& stimuli() const; // Convenience access to painted items. const region_assignment densities() const; diff --git a/arbor/include/arbor/lif_cell.hpp b/arbor/include/arbor/lif_cell.hpp index 2af3c2bf7c..bc29fd9456 100644 --- a/arbor/include/arbor/lif_cell.hpp +++ b/arbor/include/arbor/lif_cell.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -32,4 +34,6 @@ struct ARB_SYMBOL_VISIBLE lif_probe_metadata {}; // Sample value type: `double` struct ARB_SYMBOL_VISIBLE lif_probe_voltage {}; +using lif_cell_editor = std::function; + } // namespace arb diff --git a/arbor/include/arbor/mechanism.hpp b/arbor/include/arbor/mechanism.hpp index b011a1a284..e1752212cc 100644 --- a/arbor/include/arbor/mechanism.hpp +++ b/arbor/include/arbor/mechanism.hpp @@ -95,7 +95,7 @@ class mechanism { // Per-cell group identifier for an instantiated mechanism. unsigned mechanism_id() const { return ppack_.mechanism_id; } - arb_mechanism_type mech_; + arb_mechanism_type mech_; arb_mechanism_interface iface_; arb_mechanism_ppack ppack_; diff --git a/arbor/include/arbor/simulation.hpp b/arbor/include/arbor/simulation.hpp index a2c5d69ecb..e870cda051 100644 --- a/arbor/include/arbor/simulation.hpp +++ b/arbor/include/arbor/simulation.hpp @@ -23,7 +23,7 @@ using spike_export_function = std::function&)>; using epoch_function = std::function; // simulation_state comprises private implementation for simulation class. -class simulation_state; +struct simulation_state; class simulation_builder; @@ -46,6 +46,8 @@ class ARB_ARBOR_API simulation { void update(const recipe& rec); + void edit_cell(cell_gid_type gid, std::any edit); + void reset(); time_type run(const units::quantity& tfinal, const units::quantity& dt); diff --git a/arbor/include/arbor/spike_source_cell.hpp b/arbor/include/arbor/spike_source_cell.hpp index 16b91a46a1..7b53b8e143 100644 --- a/arbor/include/arbor/spike_source_cell.hpp +++ b/arbor/include/arbor/spike_source_cell.hpp @@ -11,12 +11,14 @@ namespace arb { struct ARB_SYMBOL_VISIBLE spike_source_cell { cell_tag_type source; // Label of source. - std::vector seqs; + std::vector schedules; spike_source_cell() = delete; template - spike_source_cell(cell_tag_type source, Seqs&&... seqs): source(std::move(source)), seqs{std::forward(seqs)...} {} - spike_source_cell(cell_tag_type source, std::vector seqs): source(std::move(source)), seqs(std::move(seqs)) {} + spike_source_cell(cell_tag_type source, Seqs&&... seqs): source(std::move(source)), schedules{std::forward(seqs)...} {} + spike_source_cell(cell_tag_type source, std::vector seqs): source(std::move(source)), schedules(std::move(seqs)) {} }; +using spike_source_cell_editor = std::function; + } // namespace arb diff --git a/arbor/lif_cell_group.cpp b/arbor/lif_cell_group.cpp index 5af84c8874..2138fc28b0 100644 --- a/arbor/lif_cell_group.cpp +++ b/arbor/lif_cell_group.cpp @@ -4,6 +4,7 @@ #include "lif_cell_group.hpp" #include "profile/profiler_macro.hpp" #include "util/rangeutil.hpp" +#include "util/maputil.hpp" #include "util/span.hpp" using namespace arb; @@ -39,12 +40,47 @@ lif_cell_group::lif_cell_group(const std::vector& gids, } } } + + if (!util::is_sorted(gids_)) throw arb::arbor_internal_error{"gids must be sorted?!"}; } -cell_kind lif_cell_group::get_cell_kind() const { - return cell_kind::lif; +void +lif_cell_group::edit_cell(cell_gid_type gid, std::any cell_edit) { + try { + auto lif_edit = std::any_cast(cell_edit); + auto lid = util::binary_search_index(gids_, gid); + if (!lid) throw arb::arbor_internal_error{"gid " + std::to_string(gid) + " erroneuosly dispatched to cell group."}; + auto& lowered = cells_[*lid]; + auto tmp = lif_cell{ + .source = lowered.source, + .target = lowered.target, + .tau_m = lowered.tau_m * U::ms, + .V_th = lowered.V_th * U::mV, + .C_m = lowered.C_m * U::pF, + .E_L = lowered.E_L * U::mV, + .E_R = lowered.E_R * U::mV, + .V_m = lowered.V_m * U::mV, + .t_ref = lowered.t_ref * U::ms + }; + lif_edit(tmp); + // NOTE: we forbid writing to V_m? Reasons + // * the cell might be in the refractory period which causes semantic issues + // - return to normal or not? + // - what should probes return + // * V_m is the _initial state_ only + if (tmp.V_m.value_as(U::mV) != lowered.V_m) throw bad_cell_edit(gid, "Initial voltage is not editable."); + if (tmp.source != lowered.source) throw bad_cell_edit(gid, "Source is not editable."); + if (tmp.target != lowered.target) throw bad_cell_edit(gid, "Target is not editable."); + // Write back + lowered = lif_lowered_cell{tmp}; + } + catch (const std::bad_any_cast& ){ + throw bad_cell_edit(gid, "Not a LIF editor (C++ type-id: '" + std::string{cell_edit.type().name()} + "')"); + } } +cell_kind lif_cell_group::get_cell_kind() const { return cell_kind::lif; } + void lif_cell_group::advance(epoch ep, time_type dt, const event_lane_subrange& event_lanes) { PE(advance:lif); for (auto lid: util::make_span(gids_.size())) { diff --git a/arbor/lif_cell_group.hpp b/arbor/lif_cell_group.hpp index 4cb1bc1546..da844bcf05 100644 --- a/arbor/lif_cell_group.hpp +++ b/arbor/lif_cell_group.hpp @@ -82,6 +82,8 @@ struct ARB_ARBOR_API lif_cell_group: public cell_group { virtual void t_serialize(serializer& ser, const std::string& k) const override; virtual void t_deserialize(serializer& ser, const std::string& k) override; + void edit_cell(cell_gid_type gid, std::any edit) override; + private: enum class lif_probe_kind { voltage }; diff --git a/arbor/simulation.cpp b/arbor/simulation.cpp index 02ea46c498..97c4858a9a 100644 --- a/arbor/simulation.cpp +++ b/arbor/simulation.cpp @@ -87,12 +87,13 @@ ARB_ARBOR_API void merge_cell_events(time_type t_from, PL(); } -class simulation_state { -public: +struct simulation_state { simulation_state(const recipe& rec, const domain_decomposition& decomp, context ctx, arb_seed_type seed); void update(const recipe& rec); + void edit_cell(cell_gid_type gid, std::any edit); + void reset(); time_type run(time_type tfinal, time_type dt); @@ -191,11 +192,7 @@ class simulation_state { std::vector> event_generators_; // Hash table for looking up the the local index of a cell with a given gid - struct gid_local_info { - cell_size_type cell_index; - cell_size_type group_index; - }; - std::unordered_map gid_to_local_; + std::unordered_map gid_to_local_; communicator communicator_; context ctx_; @@ -309,7 +306,7 @@ void simulation_state::update(const recipe& rec) { for (const auto& group_info: ddc_.groups()) { for (auto gid: group_info.gids) { // Store mapping of gid to local cell index. - gid_to_local_[gid] = {lidx, grpidx}; + gid_to_local_[gid] = grpidx; // Set up the event generators for cell gid. event_generators_[lidx] = rec.event_generators(gid); // Resolve event_generator targets; each event generator gets their own resolver state. @@ -564,33 +561,36 @@ void simulation_state::remove_all_samplers() { } std::vector simulation_state::get_probe_metadata(const cell_address_type& probeset_id) const { - if (auto linfo = util::value_by_key(gid_to_local_, probeset_id.gid)) { - return cell_groups_.at(linfo->group_index)->get_probe_metadata(probeset_id); - } - else { - return {}; + if (auto gidx = util::value_by_key(gid_to_local_, probeset_id.gid)) { + return cell_groups_.at(*gidx)->get_probe_metadata(probeset_id); } + return {}; } // Simulation class implementations forward to implementation class. simulation_builder simulation::create(recipe const & rec) { return {rec}; }; -simulation::simulation( - const recipe& rec, - context ctx, - const domain_decomposition& decomp, - arb_seed_type seed) -{ +simulation::simulation(const recipe& rec, + context ctx, + const domain_decomposition& decomp, + arb_seed_type seed) { impl_.reset(new simulation_state(rec, decomp, ctx, seed)); } -void simulation::reset() { - impl_->reset(); -} +void simulation::reset() { impl_->reset(); } void simulation::update(const recipe& rec) { impl_->update(rec); } +// facilitate cell editig +void simulation::edit_cell(cell_gid_type gid, std::any edit) { impl_->edit_cell(gid, edit); } +void simulation_state::edit_cell(cell_gid_type gid, std::any edit) { + if (gid >= ddc_.num_global_cells()) throw std::range_error{"Not a valid gid: " + std::to_string(gid)}; + if (auto gidx = util::value_by_key(gid_to_local_, gid)) { + cell_groups_[*gidx]->edit_cell(gid, edit); + } +} + time_type simulation::run(const units::quantity& tfinal, const units::quantity& dt) { auto dt_ms = dt.value_as(units::ms); if (dt_ms <= 0.0 || !std::isfinite(dt_ms)) throw domain_error("Finite time-step must be supplied."); diff --git a/arbor/spike_source_cell_group.cpp b/arbor/spike_source_cell_group.cpp index 651980aa44..747fd96377 100644 --- a/arbor/spike_source_cell_group.cpp +++ b/arbor/spike_source_cell_group.cpp @@ -8,16 +8,15 @@ #include "profile/profiler_macro.hpp" #include "spike_source_cell_group.hpp" #include "util/span.hpp" +#include "util/maputil.hpp" namespace arb { -spike_source_cell_group::spike_source_cell_group( - const std::vector& gids, - const recipe& rec, - cell_label_range& cg_sources, - cell_label_range& cg_targets): - gids_(gids) -{ +spike_source_cell_group::spike_source_cell_group(const std::vector& gids, + const recipe& rec, + cell_label_range& cg_sources, + cell_label_range& cg_targets): + gids_(gids) { for (auto gid: gids_) { if (!rec.get_probes(gid).empty()) { throw bad_cell_probe(cell_kind::spike_source, gid); @@ -30,8 +29,9 @@ spike_source_cell_group::spike_source_cell_group( cg_targets.add_cell(); try { auto cell = util::any_cast(rec.get_cell_description(gid)); - time_sequences_.emplace_back(cell.seqs); + time_sequences_.emplace_back(cell.schedules); cg_sources.add_label(hash_value(cell.source), {0, 1}); + sources_.push_back(cell.source); } catch (std::bad_any_cast& e) { throw bad_cell_description(cell_kind::spike_source, gid); @@ -39,9 +39,7 @@ spike_source_cell_group::spike_source_cell_group( } } -cell_kind spike_source_cell_group::get_cell_kind() const { - return cell_kind::spike_source; -} +cell_kind spike_source_cell_group::get_cell_kind() const { return cell_kind::spike_source; } void spike_source_cell_group::advance(epoch ep, time_type dt, const event_lane_subrange& event_lanes) { PE(advance:sscell); @@ -68,22 +66,36 @@ void spike_source_cell_group::reset() { clear_spikes(); } -const std::vector& spike_source_cell_group::spikes() const { - return spikes_; +void +spike_source_cell_group::edit_cell(cell_gid_type gid, std::any cell_edit) { + try { + auto source_edit = std::any_cast(cell_edit); + auto lid = util::binary_search_index(gids_, gid); + if (!lid) throw arb::arbor_internal_error{"gid " + std::to_string(gid) + " erroneuosly dispatched to cell group."}; + auto idx = *lid; + auto tmp = spike_source_cell{sources_[idx], std::move(time_sequences_[idx])}; + source_edit(tmp); + // NOTE: we forbid writing to V_m? Reasons + // * the cell might be in the refractory period which causes semantic issues + // - return to normal or not? + // - what should probes return + // * V_m is the _initial state_ only + if (tmp.source != sources_[idx]) throw bad_cell_edit(gid, "Source is not editable."); + // Write back + time_sequences_[idx] = std::move(tmp.schedules); + } + catch (const std::bad_any_cast&) { + throw bad_cell_edit(gid, "Not a source cell editor (C++ typid: '" + std::string{cell_edit.type().name()} + "')"); + } } -void spike_source_cell_group::clear_spikes() { - spikes_.clear(); -} +const std::vector& spike_source_cell_group::spikes() const { return spikes_; } -void spike_source_cell_group::t_serialize(serializer& ser, const std::string& k) const { - serialize(ser, k, *this); -} +void spike_source_cell_group::clear_spikes() { spikes_.clear(); } -void spike_source_cell_group::t_deserialize(serializer& ser, const std::string& k) { - deserialize(ser, k, *this); -} +void spike_source_cell_group::t_serialize(serializer& ser, const std::string& k) const { serialize(ser, k, *this); } +void spike_source_cell_group::t_deserialize(serializer& ser, const std::string& k) { deserialize(ser, k, *this); } void spike_source_cell_group::add_sampler(sampler_association_handle, cell_member_predicate, schedule, sampler_function) {} diff --git a/arbor/spike_source_cell_group.hpp b/arbor/spike_source_cell_group.hpp index 44e89d97c1..8ae449d81a 100644 --- a/arbor/spike_source_cell_group.hpp +++ b/arbor/spike_source_cell_group.hpp @@ -35,12 +35,15 @@ class ARB_ARBOR_API spike_source_cell_group: public cell_group { void remove_all_samplers() override {} - ARB_SERDES_ENABLE(spike_source_cell_group, spikes_, gids_, time_sequences_); + ARB_SERDES_ENABLE(spike_source_cell_group, sources_, spikes_, gids_, time_sequences_); - virtual void t_serialize(serializer& ser, const std::string& k) const override; - virtual void t_deserialize(serializer& ser, const std::string& k) override; + void t_serialize(serializer& ser, const std::string& k) const override; + void t_deserialize(serializer& ser, const std::string& k) override; + + void edit_cell(cell_gid_type gid, std::any edit) override; private: + std::vector sources_; std::vector spikes_; std::vector gids_; std::vector> time_sequences_; diff --git a/example/bench/bench.cpp b/example/bench/bench.cpp index 0b4367238d..d5a9d60055 100644 --- a/example/bench/bench.cpp +++ b/example/bench/bench.cpp @@ -94,7 +94,7 @@ class bench_recipe: public arb::recipe { // different MPI ranks and threads. auto sched = arb::poisson_schedule(params_.cell.spike_freq_hz*arb::units::Hz, gid); - return arb::benchmark_cell("src", "tgt", sched, params_.cell.realtime_ratio); + return arb::benchmark_cell{.source="src", .target="tgt", .time_sequence=sched, .realtime_ratio=params_.cell.realtime_ratio}; } arb::cell_kind get_cell_kind(arb::cell_gid_type gid) const override { diff --git a/example/drybench/drybench.cpp b/example/drybench/drybench.cpp index 15e439584a..c2df9d557a 100644 --- a/example/drybench/drybench.cpp +++ b/example/drybench/drybench.cpp @@ -96,7 +96,7 @@ class tile_desc: public arb::tile { arb::util::unique_any get_cell_description(cell_gid_type gid) const override { auto gen = arb::poisson_schedule(params_.cell.spike_freq_hz*arb::units::Hz, gid); - return arb::benchmark_cell("src", "tgt", std::move(gen), params_.cell.realtime_ratio); + return arb::benchmark_cell{.source="src", .target="tgt", .time_sequence=std::move(gen), .realtime_ratio=params_.cell.realtime_ratio}; } cell_kind get_cell_kind(cell_gid_type gid) const override { diff --git a/test/ubench/mech_vec.cpp b/test/ubench/mech_vec.cpp index d8ac020167..276dee8288 100644 --- a/test/ubench/mech_vec.cpp +++ b/test/ubench/mech_vec.cpp @@ -2,11 +2,7 @@ // // Start with pas (passive dendrite) mechanism -// NOTE: This targets an earlier version of the Arbor API and -// will need to be reworked in order to compile. - #include -#include #include #include @@ -16,13 +12,18 @@ #include "execution_context.hpp" #include "fvm_lowered_cell_impl.hpp" +#include "../unit/common.hpp" + using namespace arb; using backend = arb::multicore::backend; using fvm_cell = arb::fvm_lowered_cell_impl; + +ACCESS_BIND(std::vector fvm_cell::*, private_mechanisms_ptr, &fvm_cell::mechanisms_) + mechanism_ptr& find_mechanism(const std::string& name, fvm_cell& cell) { - auto &mechs = cell.mechanisms(); + auto &mechs = cell.*private_mechanisms_ptr; auto it = std::find_if(mechs.begin(), mechs.end(), [&](mechanism_ptr& m){return m->internal_name()==name;}); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index a6887f4bf7..6856647f0d 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -64,6 +64,7 @@ set(unit_sources test_any_visitor.cpp test_backend.cpp test_cable_cell.cpp + test_cell_editor.cpp test_counter.cpp test_cv_geom.cpp test_cv_layout.cpp diff --git a/test/unit/test_cell_editor.cpp b/test/unit/test_cell_editor.cpp new file mode 100644 index 0000000000..dcb9d31823 --- /dev/null +++ b/test/unit/test_cell_editor.cpp @@ -0,0 +1,663 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "util/span.hpp" + +constexpr double epsilon = 1e-6; +#ifdef ARB_GPU_ENABLED +constexpr int with_gpu = 0; +#else +constexpr int with_gpu = -1; +#endif + + +using namespace arb::units::literals; + +struct lif_recipe: arb::recipe { + + struct param_t { + double weight = 0; + double cm_pF = 0; + size_t n_100 = 0; + size_t n_200 = 0; + }; + + lif_recipe(double w, double cm_pf): weight(w), C_m(cm_pf*arb::units::pF) {} + + arb::cell_size_type num_cells() const override { return N; } + arb::cell_kind get_cell_kind(arb::cell_gid_type) const override { return arb::cell_kind::lif; } + arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override { + auto cell = arb::lif_cell{"src", "tgt"}; + cell.C_m = C_m; + return cell; + } + + std::vector event_generators(arb::cell_gid_type gid) const override { + return {arb::regular_generator({"tgt"}, weight, 0_ms, 0.5_ms)}; + } + + arb::cell_size_type N = 10; + + double weight = 100; + arb::units::quantity C_m = 20_pF; +}; + +TEST(edit_lif, no_edit) { + using param_t = lif_recipe::param_t; + // check base case at 20pF + // weight c_m t=100 200 + for (const auto& param: {param_t{ 10.0, 20.0, 20, 50}, + param_t{ 100.0, 20.0, 330, 670}, + param_t{1000.0, 20.0, 500, 1000}}) { + auto rec = lif_recipe{param.weight, param.cm_pF}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + // check base case at 40pF + // weight c_m t=100 200 + for (const auto& param: {param_t{ 10.0, 40.0, 00, 0}, + param_t{ 100.0, 40.0, 250, 500}, + param_t{1000.0, 40.0, 500, 1000}}) { + auto rec = lif_recipe{param.weight, param.cm_pF}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } +} + +TEST(edit_lif, edit) { + using param_t = lif_recipe::param_t; + + arb::lif_cell_editor edit = [](arb::lif_cell& cell) { cell.C_m = 40_pF; }; + + auto ctx = arb::make_context(); + // scan group sizes + for (auto group: arb::util::make_span(1, 10)) { + auto phm = arb::partition_hint_map{ + {arb::cell_kind::lif, + arb::partition_hint{ + .cpu_group_size=std::size_t(group), + .gpu_group_size=std::size_t(group), + .prefer_gpu=true, + } + } + }; + // check transition from 20pF -> 40pF for cell gid=0 + // weight c_m t=100 200 + for (const auto& param: {param_t{ 10.0, 20.0, 20, 47}, + param_t{ 100.0, 20.0, 330, 661}, + param_t{1000.0, 20.0, 500, 1000}}) { + auto rec = lif_recipe{param.weight, param.cm_pF}; + auto ddc = arb::partition_load_balance(rec, ctx, phm); + auto sim = arb::simulation{rec, ctx, ddc}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.edit_cell(0, edit); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + // check transition from 20pF -> 40pF for half of cells + // weight c_m t=100 200 + for (const auto& param: {param_t{ 10.0, 20.0, 20, 35}, + param_t{ 100.0, 20.0, 330, 625}, + param_t{1000.0, 20.0, 500, 1000}}) { + auto rec = lif_recipe{param.weight, param.cm_pF}; + auto ddc = arb::partition_load_balance(rec, ctx, phm); + auto sim = arb::simulation{rec, ctx, ddc}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + for (arb::cell_gid_type gid = 0; gid < rec.num_cells(); gid += 2) sim.edit_cell(gid, edit); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + // check transition from 20pF -> 40pF for all cells + // weight c_m t=100 200 + for (const auto& param: {param_t{ 10.0, 20.0, 20, 20}, + param_t{ 100.0, 20.0, 330, 580}, + param_t{1000.0, 20.0, 500, 1000}}) { + auto rec = lif_recipe{param.weight, param.cm_pF}; + auto ddc = arb::partition_load_balance(rec, ctx, phm); + auto sim = arb::simulation{rec, ctx, ddc}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + for (arb::cell_gid_type gid = 0; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + // edits are idempotent + // check transition from 20pF -> 40pF for all cells + // weight c_m t=100 200 + for (const auto& param: {param_t{ 10.0, 20.0, 20, 20}, + param_t{ 100.0, 20.0, 330, 580}, + param_t{1000.0, 20.0, 500, 1000}}) { + auto rec = lif_recipe{param.weight, param.cm_pF}; + auto ddc = arb::partition_load_balance(rec, ctx, phm); + auto sim = arb::simulation{rec, ctx, ddc}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + for (arb::cell_gid_type gid = 0; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + for (arb::cell_gid_type gid = 0; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + + } +} + +TEST(edit_lif, errors) { + auto rec = lif_recipe{0, 0}; + auto sim = arb::simulation{rec}; + // Check that errors are actually thrown. + EXPECT_THROW(sim.edit_cell( 0, arb::lif_cell_editor([](auto& cell) { cell.V_m = 42_mV; })), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell( 0, arb::lif_cell_editor([](auto& cell) { cell.source = "foo"; })), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell( 0, arb::lif_cell_editor([](auto& cell) { cell.target = "foo"; })), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell( 0, 42), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell(42, arb::lif_cell_editor([](arb::lif_cell& cell) { cell.C_m = 40_pF; })), std::range_error); +} + +struct bench_recipe: arb::recipe { + + struct param_t { + double rtr = 1.0; // real time + double nu_kHz = 1.0; // freq + size_t n_100 = 0; // spikes after N ms + size_t n_200 = 0; + }; + + bench_recipe(double r, double f): ratio(r), freq(f*arb::units::kHz) {} + + arb::cell_size_type num_cells() const override { return N; } + arb::cell_kind get_cell_kind(arb::cell_gid_type) const override { return arb::cell_kind::benchmark; } + arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override { + return arb::benchmark_cell{.source="src", .target="tgt", .time_sequence=arb::regular_schedule(1/freq), .realtime_ratio=ratio}; + } + + arb::cell_size_type N = 10; + + double ratio = 1.0; + arb::units::quantity freq = 1_kHz; +}; + +TEST(edit_bench, no_edit) { + using param_t = bench_recipe::param_t; + // rtr nu 100 200 + for (const auto& param: {param_t{1e-4, 1.0, 1000, 2000}, // 10 cells x 100ms x 1kHz = 1000 spikes + param_t{1e-4, 2.0, 2000, 4000}, + param_t{1e-4, 4.0, 4000, 8000}}) { + auto rec = bench_recipe{param.rtr, param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } +} + +TEST(edit_bench, edit_rate) { + using param_t = bench_recipe::param_t; + // rtr nu 100 200 + for (const auto& param: {param_t{1e-4, 1.0, 1000, 2100}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{1e-4, 2.0, 2000, 4200}, + param_t{1e-4, 4.0, 4000, 8400}}) { + arb::benchmark_cell_editor edit = [&](auto& cell) { cell.time_sequence = arb::regular_schedule(1.0/(2.0_kHz * param.nu_kHz)); }; + auto rec = bench_recipe{param.rtr, param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + sim.edit_cell(5, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1e-4, 1.0, 1000, 2500}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{1e-4, 2.0, 2000, 5000}, + param_t{1e-4, 4.0, 4000, 10000}}) { + arb::benchmark_cell_editor edit = [&](auto& cell) { cell.time_sequence = arb::regular_schedule(1.0/(2.0_kHz * param.nu_kHz)); }; + auto rec = bench_recipe{param.rtr, param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); gid += 2) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1e-4, 1.0, 1000, 3000}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{1e-4, 2.0, 2000, 6000}, + param_t{1e-4, 4.0, 4000, 12000}}) { + arb::benchmark_cell_editor edit = [&](auto& cell) { cell.time_sequence = arb::regular_schedule(1.0/(2.0_kHz * param.nu_kHz)); }; + auto rec = bench_recipe{param.rtr, param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + +} + +TEST(edit_bench, edit_schedule) { + auto eps = 0.06; + + using param_t = bench_recipe::param_t; + // rtr nu 100 200 + for (const auto& param: {param_t{1e-4, 1.0, 1000, 2000}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{1e-4, 2.0, 2000, 4000}, + param_t{1e-4, 4.0, 4000, 8000}}) { + arb::benchmark_cell_editor edit = [&](auto& cell) { cell.time_sequence = arb::poisson_schedule(1.0_ms/param.nu_kHz); }; + auto rec = bench_recipe{param.rtr, param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + sim.edit_cell(5, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_GE(sim.num_spikes(), param.n_200*(1.0 - eps)); + EXPECT_LE(sim.num_spikes(), param.n_200*(1.0 + eps)); + + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1e-4, 1.0, 1000, 2000}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{1e-4, 2.0, 2000, 4000}, + param_t{1e-4, 4.0, 4000, 8000}}) { + arb::benchmark_cell_editor edit = [&](auto& cell) { cell.time_sequence = arb::poisson_schedule(1.0_ms/param.nu_kHz); }; + auto rec = bench_recipe{param.rtr, param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); gid += 2) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_GE(sim.num_spikes(), param.n_200*(1.0 - eps)); + EXPECT_LE(sim.num_spikes(), param.n_200*(1.0 + eps)); + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1e-4, 1.0, 1000, 2000}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{1e-4, 2.0, 2000, 4000}, + param_t{1e-4, 4.0, 4000, 8000}}) { + arb::benchmark_cell_editor edit = [&](auto& cell) { cell.time_sequence = arb::poisson_schedule(1.0_ms/param.nu_kHz); }; + auto rec = bench_recipe{param.rtr, param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_GE(sim.num_spikes(), param.n_200*(1.0 - eps)); + EXPECT_LE(sim.num_spikes(), param.n_200*(1.0 + eps)); + } +} + +TEST(edit_benchmark, errors) { + auto rec = bench_recipe{1, 1}; + auto sim = arb::simulation{rec}; + // Check that errors are actually thrown. + EXPECT_THROW(sim.edit_cell( 0, arb::benchmark_cell_editor([](auto& cell) { cell.source = "foo"; })), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell( 0, arb::benchmark_cell_editor([](auto& cell) { cell.target = "foo"; })), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell( 0, 42), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell(42, arb::benchmark_cell_editor([](auto& cell) { cell.realtime_ratio = 42; })), std::range_error); +} + +TEST(edit_benchmark, do_nothing_does_nothing) { + arb::benchmark_cell_editor edit = [](auto& cell) { cell.time_sequence = arb::poisson_schedule(1.0_kHz, 42);}; + arb::benchmark_cell_editor noop = [](auto& cell) {}; + + size_t n_noop = 0; + { + auto rec = bench_recipe{1e-4, 1.0}; + auto sim = arb::simulation{rec}; + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, noop); + sim.run(200_ms, 0.1_ms); + n_noop = sim.num_spikes(); + } + size_t n_expt = 0; + { + auto rec = bench_recipe{1e-4, 1.0}; + auto sim = arb::simulation{rec}; + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + sim.run(200_ms, 0.1_ms); + n_expt = sim.num_spikes(); + } + EXPECT_EQ(n_expt, n_noop); + EXPECT_GE(n_noop, 2100); + EXPECT_LE(n_noop, 2400); +} + + +struct source_recipe: arb::recipe { + + struct param_t { + double nu_kHz = 1.0; // freq + size_t n_100 = 0; // spikes after N ms + size_t n_200 = 0; + }; + + source_recipe(double f): freq(f*arb::units::kHz) {} + + arb::cell_size_type num_cells() const override { return N; } + arb::cell_kind get_cell_kind(arb::cell_gid_type) const override { return arb::cell_kind::spike_source; } + arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override { + return arb::spike_source_cell{"tgt", arb::regular_schedule(1/freq)}; + } + + arb::cell_size_type N = 10; + arb::units::quantity freq = 1_kHz; +}; + +TEST(edit_source, no_edit) { + using param_t = source_recipe::param_t; + // rtr nu 100 200 + for (const auto& param: {param_t{1.0, 1000, 2000}, // 10 cells x 100ms x 1kHz = 1000 spikes + param_t{2.0, 2000, 4000}, + param_t{4.0, 4000, 8000}}) { + auto rec = source_recipe{param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } +} + +TEST(edit_source, edit_rate) { + using param_t = source_recipe::param_t; + // rtr nu 100 200 + for (const auto& param: {param_t{1.0, 1000, 2100}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{2.0, 2000, 4200}, + param_t{4.0, 4000, 8400}}) { + arb::spike_source_cell_editor edit = [&](auto& cell) { cell.schedules = {arb::regular_schedule(1.0/(2.0_kHz * param.nu_kHz))}; }; + auto rec = source_recipe{param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + sim.edit_cell(5, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1.0, 1000, 2500}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{2.0, 2000, 5000}, + param_t{4.0, 4000, 10000}}) { + arb::spike_source_cell_editor edit = [&](auto& cell) { cell.schedules = {arb::regular_schedule(1.0/(2.0_kHz * param.nu_kHz))}; }; + auto rec = source_recipe{param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); gid += 2) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1.0, 1000, 3000}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{2.0, 2000, 6000}, + param_t{4.0, 4000, 12000}}) { + arb::spike_source_cell_editor edit = [&](auto& cell) { cell.schedules = {arb::regular_schedule(1.0/(2.0_kHz * param.nu_kHz))}; }; + auto rec = source_recipe{param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_EQ(sim.num_spikes(), param.n_200); + } + +} + +TEST(edit_source, do_nothing_does_nothing) { + arb::spike_source_cell_editor edit = [](auto& cell) { cell.schedules = {arb::poisson_schedule(1.0_kHz, 42) };}; + arb::spike_source_cell_editor noop = [](arb::spike_source_cell& cell) {}; + + size_t n_noop = 0; + { + auto rec = source_recipe{1.0}; + auto sim = arb::simulation{rec}; + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, noop); + sim.run(200_ms, 0.1_ms); + n_noop = sim.num_spikes(); + } + size_t n_expt = 0; + { + auto rec = source_recipe{1.0}; + auto sim = arb::simulation{rec}; + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + sim.run(200_ms, 0.1_ms); + n_expt = sim.num_spikes(); + } + EXPECT_EQ(n_expt, n_noop); + EXPECT_GE(n_noop, 2100); + EXPECT_LE(n_noop, 2400); +} + +TEST(edit_source, edit_schedule) { + auto eps = 0.06; + + using param_t = source_recipe::param_t; + // rtr nu 100 200 + for (const auto& param: {param_t{1.0, 1000, 2100}, // one cell adds 100ms x 2kHz, the others stay at 1Khz => 100 spikes extra + param_t{2.0, 2000, 4100}, + param_t{4.0, 4000, 8100}}) { + arb::spike_source_cell_editor edit = [&](auto& cell) { cell.schedules.push_back(arb::poisson_schedule(1.0_ms/param.nu_kHz)); }; + auto rec = source_recipe{param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + sim.edit_cell(5, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_GE(sim.num_spikes(), param.n_200*(1.0 - eps)); + EXPECT_LE(sim.num_spikes(), param.n_200*(1.0 + eps)); + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1.0, 1000, 2500}, + param_t{2.0, 2000, 5000}, + param_t{4.0, 4000, 10000}}) { + arb::spike_source_cell_editor edit = [&](auto& cell) { cell.schedules.push_back(arb::poisson_schedule(1.0_ms/param.nu_kHz)); }; + auto rec = source_recipe{param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); gid += 2) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_GE(sim.num_spikes(), param.n_200*(1.0 - eps)); + EXPECT_LE(sim.num_spikes(), param.n_200*(1.0 + eps)); + } + + // rtr nu 100 200 + for (const auto& param: {param_t{1.0, 1000, 3000}, // 0-100: 1000 spikes 100-200: 1000 + ~1000 + param_t{2.0, 2000, 6000}, + param_t{4.0, 4000, 12000}}) { + arb::spike_source_cell_editor edit = [&](auto& cell) { cell.schedules.push_back(arb::poisson_schedule(1.0_ms/param.nu_kHz)); }; + auto rec = source_recipe{param.nu_kHz}; + auto sim = arb::simulation{rec}; + sim.run(100_ms, 0.1_ms); + for (auto gid = 0u; gid < rec.num_cells(); ++gid) sim.edit_cell(gid, edit); + EXPECT_EQ(sim.num_spikes(), param.n_100); + sim.run(200_ms, 0.1_ms); + EXPECT_GE(sim.num_spikes(), param.n_200*(1.0 - eps)); + EXPECT_LE(sim.num_spikes(), param.n_200*(1.0 + eps)); + } +} + +TEST(edit_spike_source, errors) { + auto rec = source_recipe{1}; + auto sim = arb::simulation{rec}; + // Check that errors are actually thrown. + EXPECT_THROW(sim.edit_cell( 0, arb::spike_source_cell_editor([](auto& cell) { cell.source = "foo"; })), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell( 0, 42), arb::bad_cell_edit); + EXPECT_THROW(sim.edit_cell(42, arb::spike_source_cell_editor([](auto& cell) {})), std::range_error); +} + +constexpr size_t N = 4; +constexpr double eps = 1e-6; +constexpr double T = 40; +constexpr double dt = 1; +constexpr size_t n_step = T/dt; +using result_t = std::vector>; + +struct cable_recipe: arb::recipe { + cable_recipe() { + props.default_parameters = arb::neuron_parameter_defaults; + } + + arb::cell_size_type num_cells() const override { return N; } + arb::cell_kind get_cell_kind(arb::cell_gid_type) const override { return arb::cell_kind::cable; } + arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override { + // Create a cable cell + // + // +------+ + // | hh |=== pas === + // +------+ + // + auto dec = arb::decor{} + .paint(arb::reg::tagged(1), arb::density("hh", {{"gkbar", 0.036}})) + .paint(arb::reg::tagged(2), arb::density("pas")) + .place(arb::ls::location(0, 0.5), arb::i_clamp::box(10_ms, 20_ms, 100_pA), "ic1") + ; + auto par = arb::mnpos; + auto seg = arb::segment_tree{}; + par = seg.append(par, { 0, 0, 0, 42}, {10, 0, 0, 42}, 1); // soma + par = seg.append(par, {10, 0, 0, 23}, {20, 0, 0, 23}, 2); // dendrite + auto mrf = arb::morphology{seg}; + auto lbl = arb::label_dict{}; + auto cvp = arb::cv_policy_max_extent(1.0); + return arb::cable_cell{mrf, dec, lbl, cvp}; + } + + virtual std::vector get_probes(arb::cell_gid_type gid) const override { return {{arb::cable_probe_membrane_voltage{arb::ls::location(0, 0.5)}, "Um"}}; } + std::any get_global_properties(arb::cell_kind) const override { return props; } + + arb::cable_cell_global_properties props; + +}; + +testing::AssertionResult all_near(const std::vector& a, const result_t& b, int iy, double eps) { + if (a.size() != b.size()) return testing::AssertionFailure() << "sequences differ in length" + << " #expected=" << b.size() + << " #received=" << a.size(); + std::stringstream res; + res << std::setprecision(9); + for (size_t ix = 0; ix < a.size(); ++ix) { + // printf("%9.6f, ", b[ix][iy]); + auto ax = a[ix]; + auto bx = b[ix][iy]; + if (fabs(ax - bx) > eps) { + res << " elements " << ax << " and " << bx << " differ at index " << ix << ", " << iy << "."; + break; + } + } + std::string str = res.str(); + std::cerr << res.str(); + if (str.empty()) return testing::AssertionSuccess(); + else return testing::AssertionFailure() << str; +} + +TEST(edit_cable, errors) { + auto rec = cable_recipe{}; + auto sim = arb::simulation{rec}; + // wrong editor + EXPECT_THROW(sim.edit_cell( 0, arb::spike_source_cell_editor([](auto& cell) { cell.source = "foo"; })), arb::bad_cell_edit); + // non-existant gid + EXPECT_THROW(sim.edit_cell(42, arb::cable_cell_density_editor{.mechanism="hh", .values={{"gnabar", 23}}}), std::range_error); + // non-existant mechanism + EXPECT_THROW(sim.edit_cell( 0, arb::cable_cell_density_editor{.mechanism="bar", .values={}}), arb::bad_cell_edit); + // non-existant parameter + EXPECT_THROW(sim.edit_cell( 0, arb::cable_cell_density_editor{.mechanism="hh", .values={{"foobar", 23}}}), arb::bad_cell_edit); +} + +TEST(edit_cable, hh) { + result_t sample_values; + sample_values.resize(n_step); + auto sampler = [&sample_values](arb::probe_metadata pm, std::size_t n, const arb::sample_record* samples) { + auto gid = pm.id.gid; + for (std::size_t ix = 0; ix < n; ++ix) { + sample_values[ix][gid] = *arb::util::any_cast(samples[ix].data); + } + }; + + std::vector unedited = {-65.000000, -65.976650, -66.650927, -67.003375, -67.167843, -67.211650, -67.190473, -67.136324, -67.069902, -67.002777, -66.941313, -65.466084, -64.416894, -63.769651, -63.388147, -63.232318, -63.250644, -63.392280, -63.602123, -63.830035, -64.038928, -64.208130, -64.331221, -64.411239, -64.455972, -64.474584, -64.475653, -64.466276, -64.451815, -64.436002, -64.421192, -65.752674, -66.673119, -67.139232, -67.345711, -67.391215, -67.353235, -67.274783, -67.182795, -67.091783}; + std::vector edited = {-65.000000, -67.510461, -68.850726, -69.323006, -69.412256, -69.310662, -69.143188, -68.961183, -68.793752, -68.650870, -68.535689, -67.105997, -66.245441, -65.838325, -65.701969, -65.744705, -65.876993, -66.038533, -66.190518, -66.314565, -66.405806, -66.467100, -66.504513, -66.524656, -66.533345, -66.535117, -66.533209, -66.529750, -66.526015, -66.522677, -66.520013, -67.778855, -68.485211, -68.769411, -68.846721, -68.811386, -68.730803, -68.636385, -68.546405, -68.468299 }; + + auto ctx = arb::make_context({arbenv::default_concurrency(), with_gpu}); + auto rec = cable_recipe{}; + + // results must be invariant under the group size, even if it doesn't divide into N + for (size_t g_size = 1; g_size <= N; ++g_size) { + // ... and the gid targeted + for (size_t gid = 0; gid < N; ++gid) { + auto sim = arb::simulation{rec, ctx, partition_load_balance(rec, ctx, {{arb::cell_kind::cable, arb::partition_hint{.cpu_group_size=g_size}}})}; + sim.add_sampler(arb::all_probes, arb::regular_schedule(dt*arb::units::ms), sampler); + sim.edit_cell(gid, arb::cable_cell_density_editor{.mechanism="hh", .values={{"gkbar", 0.08}}}); + sim.run(T*arb::units::ms, dt*arb::units::ms); + // all gids present 'unedited' traces, except the one gid we targeted + for (size_t col = 0; col < N; ++col) { + if (col == gid) { + EXPECT_TRUE(all_near(edited, sample_values, col, eps)); + } + else { + EXPECT_TRUE(all_near(unedited, sample_values, col, eps)); + } + } + } + } +} + +TEST(edit_cable, pas) { + result_t sample_values; + sample_values.resize(n_step); + auto sampler = [&sample_values](arb::probe_metadata pm, std::size_t n, const arb::sample_record* samples) { + auto gid = pm.id.gid; + for (std::size_t ix = 0; ix < n; ++ix) { + sample_values[ix][gid] = *arb::util::any_cast(samples[ix].data); + } + }; + + std::vector unedited = {-65.000000, -65.976650, -66.650927, -67.003375, -67.167843, -67.211650, -67.190473, -67.136324, -67.069902, -67.002777, -66.941313, -65.466084, -64.416894, -63.769651, -63.388147, -63.232318, -63.250644, -63.392280, -63.602123, -63.830035, -64.038928, -64.208130, -64.331221, -64.411239, -64.455972, -64.474584, -64.475653, -64.466276, -64.451815, -64.436002, -64.421192, -65.752674, -66.673119, -67.139232, -67.345711, -67.391215, -67.353235, -67.274783, -67.182795, -67.091783}; + std::vector edited = {-65.000000, -69.757544, -69.936277, -69.930119, -69.928897, -69.923681, -69.921390, -69.918695, -69.916944, -69.915342, -69.914131, -69.830342, -69.826531, -69.825903, -69.825334, -69.824923, -69.824554, -69.824259, -69.824007, -69.823799, -69.823624, -69.823477, -69.823354, -69.823251, -69.823164, -69.823091, -69.823029, -69.822978, -69.822934, -69.822897, -69.822866, -69.905645, -69.908623, -69.908538, -69.908525, -69.908448, -69.908413, -69.908370, -69.908339, -69.908311 }; + + auto ctx = arb::make_context({arbenv::default_concurrency(), with_gpu}); + auto rec = cable_recipe{}; + + // results must be invariant under the group size, even if it doesn't divide into N + for (size_t g_size = 1; g_size <= N; ++g_size) { + // ... and the gid targeted + for (size_t gid = 0; gid < N; ++gid) { + auto sim = arb::simulation{rec, ctx, partition_load_balance(rec, ctx, {{arb::cell_kind::cable, arb::partition_hint{.cpu_group_size=g_size}}})}; + sim.add_sampler(arb::all_probes, arb::regular_schedule(dt*arb::units::ms), sampler); + sim.edit_cell(gid, arb::cable_cell_density_editor{.mechanism="pas", .values={{"g", 0.08}}}); + sim.run(T*arb::units::ms, dt*arb::units::ms); + // all gids present 'unedited' traces, except the one gid we targeted + for (size_t col = 0; col < N; ++col) { + if (col == gid) { + EXPECT_TRUE(all_near(edited, sample_values, col, eps)); + } + else { + EXPECT_TRUE(all_near(unedited, sample_values, col, eps)); + } + } + } + } +} diff --git a/test/unit/test_diffusion.cpp b/test/unit/test_diffusion.cpp index 238c021864..3e4da5baa7 100644 --- a/test/unit/test_diffusion.cpp +++ b/test/unit/test_diffusion.cpp @@ -138,22 +138,22 @@ TEST(diffusion, errors) { { // Cannot R/W Xd w/o setting diffusivity auto rec = linear{30, 3, 1}.add_decay(); - ASSERT_THROW(run(rec, {}), illegal_diffusive_mechanism); + ASSERT_THROW((void)run(rec, {}), illegal_diffusive_mechanism); } { // Cannot R/W Xd w/o setting diffusivity auto rec = linear{30, 3, 1}.add_inject(); - ASSERT_THROW(run(rec, {}), illegal_diffusive_mechanism); + ASSERT_THROW((void)run(rec, {}), illegal_diffusive_mechanism); } { // No negative diffusivity auto rec = linear{30, 3, 1}.set_diffusivity(-42.0, "(all)"_reg); - ASSERT_THROW(run(rec, {}), cable_cell_error); + ASSERT_THROW((void)run(rec, {}), cable_cell_error); } { // No negative diffusivity auto rec = linear{30, 3, 1}.set_diffusivity(-42.0); - ASSERT_THROW(run(rec, {}), cable_cell_error); + ASSERT_THROW((void)run(rec, {}), cable_cell_error); } } @@ -368,19 +368,19 @@ TEST(diffusion, elided_arrays) { // NOTE: We are still, strictly speaking, failing this test since we don't // get the expected values. However, as we are just checking that we // can still probe elided arrary, that's OK. - EXPECT_NO_THROW(run(rec, exp, "nai")); - EXPECT_NO_THROW(run(rec, exp, "nao")); - EXPECT_NO_THROW(run(rec, exp, "nad")); - EXPECT_NO_THROW(run(rec, exp, "cnai")); - EXPECT_NO_THROW(run(rec, exp, "cnao")); - EXPECT_NO_THROW(run(rec, exp, "cnad")); + EXPECT_NO_THROW((void)run(rec, exp, "nai")); + EXPECT_NO_THROW((void)run(rec, exp, "nao")); + EXPECT_NO_THROW((void)run(rec, exp, "nad")); + EXPECT_NO_THROW((void)run(rec, exp, "cnai")); + EXPECT_NO_THROW((void)run(rec, exp, "cnao")); + EXPECT_NO_THROW((void)run(rec, exp, "cnad")); rec.set_diffusivity(1.0); - EXPECT_NO_THROW(run(rec, exp, "nai")); - EXPECT_NO_THROW(run(rec, exp, "nao")); - EXPECT_NO_THROW(run(rec, exp, "nad")); - EXPECT_NO_THROW(run(rec, exp, "cnai")); - EXPECT_NO_THROW(run(rec, exp, "cnao")); - EXPECT_NO_THROW(run(rec, exp, "cnad")); + EXPECT_NO_THROW((void)run(rec, exp, "nai")); + EXPECT_NO_THROW((void)run(rec, exp, "nao")); + EXPECT_NO_THROW((void)run(rec, exp, "nad")); + EXPECT_NO_THROW((void)run(rec, exp, "cnai")); + EXPECT_NO_THROW((void)run(rec, exp, "cnao")); + EXPECT_NO_THROW((void)run(rec, exp, "cnad")); }