diff --git a/nrn/ball_and_stick.py b/nrn/ball_and_stick.py index ce857fc1a103ab20e00e2b2a60ade74ecffa7de9..5fe977fe594ada556ebce1da09c9a15c3f3b03e3 100644 --- a/nrn/ball_and_stick.py +++ b/nrn/ball_and_stick.py @@ -1,13 +1,20 @@ -from neuron import h, gui +from timeit import default_timer as timer +import os.path +from matplotlib import pyplot import numpy as np +import json +import argparse +from neuron import gui, h + +parser = argparse.ArgumentParser(description='generate spike train ball and stick model with hh channels at soma and pas channels in dendrite') +parser.add_argument('--plot', action='store_true', dest='plot') +args = parser.parse_args() soma = h.Section(name='soma') dend = h.Section(name='dend') dend.connect(soma(1)) -h.psection(sec=soma) - # Surface area of cylinder is 2*pi*r*h (sealed ends are implicit). # Here we make a square cylinder in that the diameter # is equal to the height, so diam = h. ==> Area = 4*pi*r^2 @@ -16,7 +23,6 @@ h.psection(sec=soma) soma.L = soma.diam = 12.6157 # Makes a soma of 500 microns squared. dend.L = 200 # microns dend.diam = 1 # microns -print "Surface area of soma =", h.area(0.5, sec=soma) for sec in h.allsec(): sec.Ra = 100 # Axial resistance in Ohm * cm @@ -34,51 +40,99 @@ dend.insert('pas') dend.g_pas = 0.001 # Passive conductance in S/cm2 dend.e_pas = -65 # Leak reversal potential mV -#for sec in h.allsec(): -# h.psection(sec=sec) - stim = h.IClamp(dend(1)) stim.delay = 5 -stim.dur = 1 -stim.amp = 5*0.1 +stim.dur = 80 +stim.amp = 3*0.1 -somastyles = ['b-', 'r-'] -dendstyles = ['b-.', 'r-.'] -dt = [0.02, 0.001] -nsegs = [10, 100] -from matplotlib import pyplot -pyplot.figure(figsize=(8,4)) # Default figsize is (8,6) -pyplot.grid() -pos = 0 -for pos in [0] : - dend.nseg=nsegs[pos] - - v_soma = h.Vector() # Membrane potential vector - v_dend = h.Vector() # Membrane potential vector - t_vec = h.Vector() # Time stamp vector - v_soma.record(soma(0.5)._ref_v) - v_dend.record(dend(1.0)._ref_v) - t_vec.record(h._ref_t) - simdur = 25.0 +if args.plot : + pyplot.figure(figsize=(8,4)) # Default figsize is (8,6) + pyplot.grid() - h.tstop = simdur - h.dt = dt[pos] - h.run() +simdur = 100.0 +h.tstop = simdur +h.dt = 0.001 + +start = timer() +results = [] +for nseg in [5, 11, 21, 41, 81, 161] : - pyplot.plot(t_vec, v_soma, somastyles[pos], linewidth=2) - pyplot.plot(t_vec, v_dend, dendstyles[pos], linewidth=2) + print 'simulation with ', nseg, ' compartments in dendrite...' -pyplot.xlabel('time (ms)') -pyplot.ylabel('mV') + dend.nseg=nseg -data = np.loadtxt('../tests/v.dat') -t = data[:,0] -nfields = data.shape[1] + # record voltages + v_soma = h.Vector() # soma + v_dend = h.Vector() # middle of dendrite + v_clamp= h.Vector() # end of dendrite at clamp location -v = data[:,1] -pyplot.plot(t, v, 'g', linewidth=2) -v = data[:,2] -pyplot.plot(t, v, 'g-.', linewidth=2) + v_soma.record( soma(0.5)._ref_v) + v_dend.record( dend(0.5)._ref_v) + v_clamp.record(dend(1.0)._ref_v) + + # record spikes + # this is a bit verbose, no? + spike_counter_soma = h.APCount(soma(0.5)) + spike_counter_soma.thresh = 0 + spike_counter_dend = h.APCount(dend(0.5)) + spike_counter_dend.thresh = -10 + spike_counter_clamp = h.APCount(dend(1.0)) + spike_counter_clamp.thresh = 10 + + spikes_soma = h.Vector() # soma + spikes_dend = h.Vector() # middle of dendrite + spikes_clamp= h.Vector() # end of dendrite at clamp location + + spike_counter_soma.record(spikes_soma) + spike_counter_dend.record(spikes_dend) + spike_counter_clamp.record(spikes_clamp) + + # record time stamps + t_vec = h.Vector() + t_vec.record(h._ref_t) + + # finally it's time to run the simulation + h.run() -pyplot.show() + results.append( + { + "nseg": nseg, + "dt" : h.dt, + "measurements": { + "soma" : { + "thresh" : spike_counter_soma.thresh, + "spikes" : spikes_soma.to_python() + }, + "dend" : { + "thresh" : spike_counter_dend.thresh, + "spikes" : spikes_dend.to_python() + }, + "clamp" : { + "thresh" : spike_counter_clamp.thresh, + "spikes" : spikes_clamp.to_python() + } + } + } + ) + + if args.plot : + pyplot.plot(t_vec, v_soma, 'k', linewidth=1, label='soma ' + str(nseg)) + pyplot.plot(t_vec, v_dend, 'b', linewidth=1, label='dend ' + str(nseg)) + pyplot.plot(t_vec, v_clamp, 'r', linewidth=1, label='clamp ' + str(nseg)) + +# time the simulations +end = timer() +print "took ", end-start, " seconds" + +# save the spike info as in json format +fp = open('ball_and_stick.json', 'w') +json.dump(results, fp, indent=2) + +if args.plot : + pyplot.xlabel('time (ms)') + pyplot.ylabel('mV') + pyplot.xlim([0, simdur]) + pyplot.legend() + + pyplot.show() diff --git a/nrn/nest_soma.py b/nrn/nest_soma.py deleted file mode 100644 index 4acec3ed5cb1ac670abc58acc6099dc1dbf696ed..0000000000000000000000000000000000000000 --- a/nrn/nest_soma.py +++ /dev/null @@ -1,23 +0,0 @@ -import os.path -from matplotlib import pyplot -import numpy as np - -step = 0 -while os.path.isfile('../tests/v_' + str(step) + '.dat') : - fname = '../tests/v_' + str(step) + '.dat' - print 'loading ' + fname - data = np.loadtxt(fname) - t = data[:,0] - v = data[:,1] - pyplot.plot(t, v, 'b', label=fname) - step = step + 1 - -pyplot.xlabel('time (ms)') -pyplot.ylabel('mV') - -pyplot.xlim([87.5,89.5]) -#pyplot.ylim([0,40]) - -pyplot.legend() -pyplot.show() - diff --git a/nrn/soma.py b/nrn/soma.py index 65661b6f4f3739e2e6f5c743e2f8058bd469ed8d..3af4f50f3e6e1ae7761591ab68fd32a1187f9c87 100644 --- a/nrn/soma.py +++ b/nrn/soma.py @@ -50,14 +50,14 @@ h.tstop = simdur # run neuron with multiple dt start = timer() results = [] -for dt in [0.02, 0.01, 0.001, 0.0001]: +for dt in [0.05, 0.02, 0.01, 0.001, 0.0001]: h.dt = dt h.run() results.append({"dt": dt, "spikes": s_vec.to_python()}) if args.plot : pyplot.plot(t_vec, v_vec, label='neuron ' + str(dt)) -end = timer() +end = timer() print "took ", end-start, " seconds" # save the spike info as in json format diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4cc842363ad1474e408f7b44e9bb717197bb33fb..11b88107784c30865635a48e9386b726505e9618 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,6 +32,7 @@ set(TEST_SOURCES set(VALIDATION_SOURCES # unit tests validate_soma.cpp + validate_ball_and_stick.cpp # unit test driver validate.cpp diff --git a/tests/test_run.cpp b/tests/test_run.cpp index 41f17ab5187c640093352eea88e20dae018c78a3..aa4d9bba21448e6607d1490af61512a6ba25c812 100644 --- a/tests/test_run.cpp +++ b/tests/test_run.cpp @@ -48,7 +48,7 @@ TEST(run, single_compartment) results[1].push_back(model.voltage()[0]); } - write_vis_file("v.dat", results); + testing::write_vis_file("v.dat", results); } TEST(run, ball_and_stick) @@ -102,7 +102,7 @@ TEST(run, ball_and_stick) results[2].push_back(model.voltage()[pos]); } - write_vis_file("v.dat", results); + testing::write_vis_file("v.dat", results); } TEST(run, cable) diff --git a/tests/util.hpp b/tests/util.hpp index 19b0bfbb92a8a12c041ef05ff897ff7306fb7154..7a0027448e46c860471aeed68ebd8ed8e411dfe0 100644 --- a/tests/util.hpp +++ b/tests/util.hpp @@ -2,10 +2,17 @@ #include <iostream> #include <string> #include <vector> +#include <iomanip> #include <cmath> #include <util.hpp> +#include <json/src/json.hpp> + +// helpful code for running tests +// a bit messy: refactor when it gets heavier and obvious patterns emerge... + +namespace testing{ [[gnu::unused]] static void write_vis_file(const std::string& fname, std::vector<std::vector<double>> values) @@ -32,6 +39,28 @@ void write_vis_file(const std::string& fname, std::vector<std::vector<double>> v } } +[[gnu::unused]] static +nlohmann::json +load_spike_data(const std::string& input_name) +{ + nlohmann::json cell_data; + auto fid = std::ifstream(input_name); + if(!fid.is_open()) { + std::cerr << "error : unable to open file " << input_name + << " : run the validation generation script first\n"; + return {}; + } + + try { + fid >> cell_data; + } + catch (...) { + std::cerr << "error : incorrectly formatted json file " << input_name << "\n"; + return {}; + } + return cell_data; +} + template <typename T> std::vector<T> find_spikes(std::vector<T> const& v, T threshold, T dt) { @@ -72,16 +101,25 @@ struct spike_comparison { } }; +[[gnu::unused]] static std::ostream& operator<< (std::ostream& o, spike_comparison const& spikes) { - return o << "[" << spikes.min << ", " << spikes.max << "], " - << "mean " << spikes.mean << ", rms " << spikes.rms - << ", diffs " << spikes.diff; + // use snprintf because C++ is just awful for formatting output + char buffer[512]; + snprintf( + buffer, sizeof(buffer), + "min,max = %10.8f,%10.8f | mean,rms = %10.8f,%10.8f | max_rel = %10.8f", + spikes.min, spikes.max, spikes.mean, spikes.rms, + spikes.max_relative_error()*100 + ); + return o << buffer; } template <typename T> -spike_comparison compare_spikes(std::vector<T> const& spikes, std::vector<T> const& baseline) +spike_comparison compare_spikes( + std::vector<T> const& spikes, + std::vector<T> const& baseline) { spike_comparison c; @@ -112,3 +150,4 @@ spike_comparison compare_spikes(std::vector<T> const& spikes, std::vector<T> con return c; } +} // namespace testing diff --git a/tests/validate_ball_and_stick.cpp b/tests/validate_ball_and_stick.cpp new file mode 100644 index 0000000000000000000000000000000000000000..eac29e2e84f20653acdb7555714f9ed6c6e3e555 --- /dev/null +++ b/tests/validate_ball_and_stick.cpp @@ -0,0 +1,162 @@ +#include <fstream> + +#include "gtest.h" +#include "util.hpp" + +#include <cell.hpp> +#include <fvm.hpp> + +#include <json/src/json.hpp> + +// compares results with those generated by nrn/soma.py +TEST(ball_and_stick, neuron_baseline) +{ + using namespace nest::mc; + using namespace nlohmann; + + nest::mc::cell cell; + + // setup global state for the mechanisms + nest::mc::mechanisms::setup_mechanism_helpers(); + + // Soma with diameter 12.6157 um and HH channel + auto soma = cell.add_soma(12.6157/2.0); + soma->add_mechanism(hh_parameters()); + + // add dendrite of length 200 um and diameter 1 um with passive channel + auto dendrite = cell.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 200); + dendrite->add_mechanism(pas_parameters()); + + dendrite->mechanism("membrane").set("r_L", 100); // no effect for single compartment cell + + // add stimulus + cell.add_stimulus({1,1}, {5., 80., 0.3}); + + // load data from file + auto cell_data = testing::load_spike_data("../nrn/ball_and_stick.json"); + EXPECT_TRUE(cell_data.size()>0); + if(cell_data.size()==0) return; + + json& nrn = + *std::max_element( + cell_data.begin(), cell_data.end(), + [](json& lhs, json& rhs) {return lhs["nseg"]<rhs["nseg"];} + ); + + auto& measurements = nrn["measurements"]; + + double dt = nrn["dt"]; + double tfinal = 100.; // ms + int nt = tfinal/dt; + + // inline type for storing the results of a simulation along with + // the information required to compare two simulations for accuracy + struct result { + std::vector<std::vector<double>> spikes; + std::vector<std::vector<double>> baseline_spikes; + std::vector<testing::spike_comparison> comparisons; + std::vector<double> thresh; + int n_comparments; + + result(int nseg, double dt, + std::vector<std::vector<double>> &v, + json& measurements + ) { + n_comparments = nseg; + baseline_spikes = { + measurements["soma"]["spikes"], + measurements["dend"]["spikes"], + measurements["clamp"]["spikes"] + }; + thresh = { + measurements["soma"]["thresh"], + measurements["dend"]["thresh"], + measurements["clamp"]["thresh"] + }; + for(auto i=0; i<3; ++i) { + // calculate the NEST MC spike times + spikes.push_back + (testing::find_spikes(v[i], thresh[i], dt)); + // compare NEST MC and baseline NEURON spike times + comparisons.push_back + (testing::compare_spikes(spikes[i], baseline_spikes[i])); + } + } + }; + + std::vector<result> results; + for(auto run_index=0u; run_index<cell_data.size(); ++run_index) { + auto& run = cell_data[run_index]; + int num_compartments = run["nseg"]; + dendrite->set_compartments(num_compartments); + std::vector<std::vector<double>> v(3); + + // make the lowered finite volume cell + fvm::fvm_cell<double, int> model(cell); + auto graph = cell.model(); + + // set initial conditions + using memory::all; + model.voltage()(all) = -65.; + model.initialize(); // have to do this _after_ initial conditions are set + + // run the simulation + auto soma_comp = nest::mc::find_compartment_index({0,0}, graph); + auto dend_comp = nest::mc::find_compartment_index({1,0.5}, graph); + auto clamp_comp = nest::mc::find_compartment_index({1,1.0}, graph); + v[0].push_back(model.voltage()[soma_comp]); + v[1].push_back(model.voltage()[dend_comp]); + v[2].push_back(model.voltage()[clamp_comp]); + for(auto i=0; i<nt; ++i) { + model.advance(dt); + // save voltage at soma + v[0].push_back(model.voltage()[soma_comp]); + v[1].push_back(model.voltage()[dend_comp]); + v[2].push_back(model.voltage()[clamp_comp]); + } + + results.push_back( {run["nseg"], dt, v, measurements} ); + } + + // print results + for(auto& r : results){ + // select the location with the largest error for printing + auto m = + std::max_element( + r.comparisons.begin(), r.comparisons.end(), + [](testing::spike_comparison& l, testing::spike_comparison& r) + {return l.max_relative_error()<r.max_relative_error();} + ); + std::cout << std::setw(5) << r.n_comparments + << " compartments : " << *m + << "\n"; + } + + // sort results in ascending order of compartments + std::sort( + results.begin(), results.end(), + [](const result& l, const result& r) + {return l.n_comparments<r.n_comparments;} + ); + + // the strategy for testing is the following: + // 1. test that the solution converges to the finest reference solution as + // the number of compartments increases (i.e. spatial resolution is + // refined) + for(auto i=1u; i<results.size(); ++i) { + for(auto j=0; j<3; ++j) { + EXPECT_TRUE( + results[i].comparisons[j].max_relative_error() + < results[i-1].comparisons[j].max_relative_error() + ); + } + } + + // 2. test that the best solution (i.e. with most compartments) matches the + // reference solution closely (less than 0.1% over the course of 100ms + // simulation) + for(auto j=0; j<3; ++j) { + EXPECT_TRUE(results.back().comparisons[j].max_relative_error()*100<0.1); + } +} + diff --git a/tests/validate_soma.cpp b/tests/validate_soma.cpp index a90e0fa8cd59057afd6ca9ea91d3990c93e9ffcc..b65b4ecb3a0de12d475c6380b24b43b824b65599 100644 --- a/tests/validate_soma.cpp +++ b/tests/validate_soma.cpp @@ -8,8 +8,9 @@ #include <json/src/json.hpp> -// based on hh/Neuron/steps_A.py -TEST(soma, resolutions) +// compares results with those generated by nrn/soma.py +// single compartment model with HH channels +TEST(soma, neuron_baseline) { using namespace nest::mc; using namespace nlohmann; @@ -31,24 +32,19 @@ TEST(soma, resolutions) fvm::fvm_cell<double, int> model(cell); // load data from file - std::string input_name = "../nrn/soma.json"; - json cell_data; - { - auto fid = std::ifstream(input_name); - if(!fid.is_open()) { - std::cerr << "error : unable to open file " << input_name - << " : run the validation generation script first\n"; - return; - } + auto cell_data = testing::load_spike_data("../nrn/soma.json"); + EXPECT_TRUE(cell_data.size()>0); + if(cell_data.size()==0) return; - try { - fid >> cell_data; - } - catch (...) { - std::cerr << "error : incorrectly formatted json file " << input_name << "\n"; - return; - } - } + json& nrn = + *std::min_element( + cell_data.begin(), cell_data.end(), + [](json& lhs, json& rhs) {return lhs["dt"]<rhs["dt"];} + ); + std::vector<double> nrn_spike_times = nrn["spikes"]; + + std::cout << "baseline with time step size " << nrn["dt"] << " ms\n"; + std::cout << "baseline spikes : " << nrn["spikes"] << "\n"; for(auto& run : cell_data) { std::vector<double> v; @@ -69,15 +65,99 @@ TEST(soma, resolutions) v.push_back(model.voltage()[0]); } - // get the spike times from the NEST MC and NEURON simulations respectively - auto nst_spike_times = find_spikes(v, 0., dt); - auto nrn_spike_times = run["spikes"].get<std::vector<double>>(); - auto comparison = compare_spikes(nst_spike_times, nrn_spike_times); + // get the spike times from the NEST MC simulation + auto nst_spike_times = testing::find_spikes(v, 0., dt); + // compare NEST MC and baseline NEURON results + auto comparison = testing::compare_spikes(nst_spike_times, nrn_spike_times); // Assert that relative error is less than 1%. - // For a 100 ms simulation this asserts that the difference between NEST and NEURON - // at a given time step size is less than 1 ms. + // For a 100 ms simulation this asserts that the difference between + // NEST and the most accurate NEURON simulation is less than 1 ms. + std::cout << "MAX ERROR @ " << dt << " is " << comparison.max_relative_error()*100. << "\n"; EXPECT_TRUE(comparison.max_relative_error()*100. < 1.); } } +// check if solution converges +TEST(soma, convergence) +{ + using namespace nest::mc; + + nest::mc::cell cell; + + // setup global state for the mechanisms + nest::mc::mechanisms::setup_mechanism_helpers(); + + // Soma with diameter 18.8um and HH channel + auto soma = cell.add_soma(18.8/2.0); + soma->mechanism("membrane").set("r_L", 123); // no effect for single compartment cell + soma->add_mechanism(hh_parameters()); + + // add stimulus to the soma + cell.add_stimulus({0,0.5}, {10., 100., 0.1}); + + // make the lowered finite volume cell + fvm::fvm_cell<double, int> model(cell); + + // generate baseline solution with small dt=0.0001 + std::vector<double> baseline_spike_times; + { + auto dt = 1e-4; + std::vector<double> v; + + // set initial conditions + using memory::all; + model.voltage()(all) = -65.; + model.initialize(); // have to do this _after_ initial conditions are set + + // run the simulation + auto tfinal = 120.; // ms + int nt = tfinal/dt; + v.push_back(model.voltage()[0]); + for(auto i=0; i<nt; ++i) { + model.advance(dt); + // save voltage at soma + v.push_back(model.voltage()[0]); + } + + baseline_spike_times = testing::find_spikes(v, 0., dt); + } + + testing::spike_comparison previous; + for(auto dt : {0.05, 0.02, 0.01, 0.005, 0.001}) { + std::vector<double> v; + + // set initial conditions + using memory::all; + model.voltage()(all) = -65.; + model.initialize(); // have to do this _after_ initial conditions are set + + // run the simulation + auto tfinal = 120.; // ms + int nt = tfinal/dt; + v.push_back(model.voltage()[0]); + for(auto i=0; i<nt; ++i) { + model.advance(dt); + // save voltage at soma + v.push_back(model.voltage()[0]); + } + + // get the spike times from the NEST MC simulation + auto nst_spike_times = testing::find_spikes(v, 0., dt); + + // compare NEST MC and baseline NEURON results + auto comparison = testing::compare_spikes(nst_spike_times, baseline_spike_times); + std::cout << "dt " << std::setw(8) << dt << " : " << comparison << "\n"; + if(!previous.is_valid()) { + previous = std::move(comparison); + } + else { + // Assert that relative error is less than 1%. + // For a 100 ms simulation this asserts that the difference between + // NEST and the most accurate NEURON simulation is less than 1 ms. + EXPECT_TRUE(comparison.max_relative_error() < previous.max_relative_error()); + EXPECT_TRUE(comparison.rms < previous.rms); + EXPECT_TRUE(comparison.max < previous.max); + } + } +}