diff --git a/tests/test_common_cells.hpp b/tests/test_common_cells.hpp index 8186c5ed0c678ba9b3ccd06b130ea62d63a65e5d..41e2fea727b9e217899a0f8717b7085b7fc0f84a 100644 --- a/tests/test_common_cells.hpp +++ b/tests/test_common_cells.hpp @@ -196,6 +196,10 @@ inline cell make_cell_simple_cable(bool with_stim = true) { for (auto& seg: c.segments()) { seg->add_mechanism(pas_parameters()); seg->mechanism("membrane").set("r_L", 100); + + // parameter support not there yet, so leave + // as default and modify reference data for now. + // seg->mechanism("pas").set("g", 0.000025); if (seg->is_dendrite()) { seg->set_compartments(4); } diff --git a/tests/validation/validate_ball_and_stick.cpp b/tests/validation/validate_ball_and_stick.cpp index 0a57fa840e52e70ea7dcaef4dc14c0f54ee86d93..7927a748edc09830b151b68350ce6b35cfb3615a 100644 --- a/tests/validation/validate_ball_and_stick.cpp +++ b/tests/validation/validate_ball_and_stick.cpp @@ -21,8 +21,191 @@ using namespace nest::mc; -// TODO: further consolidate common code +struct sampler_info { + const char* label; + cell_member_type probe; + simple_sampler sampler; +}; + +template < + typename lowered_cell, + typename SamplerInfoSeq +> +void run_ncomp_convergence_test( + const char* model_name, + const char* refdata_path, + const cell& c, + SamplerInfoSeq& samplers, + float t_end=100.f) +{ + using nlohmann::json; + + SCOPED_TRACE(model_name); + auto& V = g_trace_io; + bool verbose = V.verbose(); + int max_ncomp = V.max_ncomp(); + + auto keys = util::transform_view(samplers, + [](const sampler_info& se) { return se.label; }); + + bool run_validation = false; + std::map<std::string, trace_data> ref_data; + try { + ref_data = V.load_traces(refdata_path); + + run_validation = std::all_of(keys.begin(), keys.end(), + [&](const char* key) { return ref_data.count(key)>0; }); + + EXPECT_TRUE(run_validation); + } + catch (std::runtime_error&) { + ADD_FAILURE() << "failure loading reference data: " << refdata_path; + } + + std::map<std::string, std::vector<conv_entry<int>>> conv_results; + + for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) { + for (auto& seg: c.segments()) { + if (!seg->is_soma()) { + seg->set_compartments(ncomp); + } + } + model<lowered_cell> m(singleton_recipe{c}); + + // reset samplers and attach to probe locations + for (auto& se: samplers) { + se.sampler.reset(); + m.attach_sampler(se.probe, se.sampler.template sampler<>()); + } + + m.run(100, 0.001); + + for (auto& se: samplers) { + std::string key = se.label; + const simple_sampler& s = se.sampler; + + // save trace + json meta = { + {"name", "membrane voltage"}, + {"model", model_name}, + {"sim", "nestmc"}, + {"ncomp", ncomp}, + {"units", "mV"}}; + + V.save_trace(key, s.trace, meta); + + // compute metrics + if (run_validation) { + double linf = linf_distance(s.trace, ref_data[key]); + auto pd = peak_delta(s.trace, ref_data[key]); + + conv_results[key].push_back({key, ncomp, linf, pd}); + } + } + } + + if (verbose && run_validation) { + report_conv_table(std::cout, conv_results, "ncomp"); + } + + for (auto key: keys) { + SCOPED_TRACE(key); + + const auto& results = conv_results[key]; + assert_convergence(results); + } +} + +TEST(ball_and_stick, neuron_ref) { + using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>; + + cell c = make_cell_ball_and_stick(); + add_common_voltage_probes(c); + + float sample_dt = 0.025f; + sampler_info samplers[] = { + {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)}, + {"dend.mid", {0u, 1u}, simple_sampler(sample_dt)}, + {"dend.end", {0u, 2u}, simple_sampler(sample_dt)} + }; + + run_ncomp_convergence_test<lowered_cell>( + "ball_and_stick", + "neuron_ball_and_stick.json", + c, + samplers); +} + +TEST(ball_and_taper, neuron_ref) { + using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>; + + cell c = make_cell_ball_and_taper(); + add_common_voltage_probes(c); + + float sample_dt = 0.025f; + sampler_info samplers[] = { + {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)}, + {"taper.mid", {0u, 1u}, simple_sampler(sample_dt)}, + {"taper.end", {0u, 2u}, simple_sampler(sample_dt)} + }; + + run_ncomp_convergence_test<lowered_cell>( + "ball_and_taper", + "neuron_ball_and_taper.json", + c, + samplers); +} + +TEST(ball_and_3stick, neuron_ref) { + using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>; + + cell c = make_cell_ball_and_3stick(); + add_common_voltage_probes(c); + + float sample_dt = 0.025f; + sampler_info samplers[] = { + {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)}, + {"dend1.mid", {0u, 1u}, simple_sampler(sample_dt)}, + {"dend1.end", {0u, 2u}, simple_sampler(sample_dt)}, + {"dend2.mid", {0u, 3u}, simple_sampler(sample_dt)}, + {"dend2.end", {0u, 4u}, simple_sampler(sample_dt)}, + {"dend3.mid", {0u, 5u}, simple_sampler(sample_dt)}, + {"dend3.end", {0u, 6u}, simple_sampler(sample_dt)} + }; + + run_ncomp_convergence_test<lowered_cell>( + "ball_and_3stick", + "neuron_ball_and_3stick.json", + c, + samplers); +} + +TEST(rallpack1, numeric_ref) { + using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>; + + cell c = make_cell_simple_cable(); + + // three probes: left end, 30% along, right end. + c.add_probe({{1, 0.0}, probeKind::membrane_voltage}); + c.add_probe({{1, 0.3}, probeKind::membrane_voltage}); + c.add_probe({{1, 1.0}, probeKind::membrane_voltage}); + + float sample_dt = 0.025f; + sampler_info samplers[] = { + {"cable.x0.0", {0u, 0u}, simple_sampler(sample_dt)}, + {"cable.x0.3", {0u, 1u}, simple_sampler(sample_dt)}, + {"cable.x1.0", {0u, 2u}, simple_sampler(sample_dt)}, + }; + + run_ncomp_convergence_test<lowered_cell>( + "rallpack1", + "numeric_rallpack1.json", + c, + samplers); +} + +#if 0 TEST(ball_and_stick, neuron_ref) { // compare voltages against reference data produced from // nrn/ball_and_stick.py @@ -293,4 +476,5 @@ TEST(ball_and_3stick, neuron_ref) { assert_convergence(results); } } +#endif diff --git a/tests/validation/validate_soma.cpp b/tests/validation/validate_soma.cpp index 26d919b07c10a5966734c0e12dd7a7296e55f139..3e848b65856691bef455a6ab0b76dd9c7c1dda1e 100644 --- a/tests/validation/validate_soma.cpp +++ b/tests/validation/validate_soma.cpp @@ -20,7 +20,7 @@ using namespace nest::mc; -TEST(soma, neuron_ref) { +TEST(soma, numeric_ref) { // compare voltages against reference data produced from // nrn/ball_and_taper.py @@ -32,7 +32,8 @@ TEST(soma, neuron_ref) { bool verbose = V.verbose(); // load validation data - auto ref_data = V.load_traces("neuron_soma.json"); + //auto ref_data = V.load_traces("neuron_soma.json"); + auto ref_data = V.load_traces("numeric_soma.json"); const char* key = "soma.mid"; bool run_validation = ref_data.count(key); EXPECT_TRUE(run_validation); diff --git a/tests/validation/validation_data.hpp b/tests/validation/validation_data.hpp index 70e99a52227b70f9772dd94370ec38fb7301e2d1..254d6eeed77cd1d99c3ba31a924a8db361e7ff21 100644 --- a/tests/validation/validation_data.hpp +++ b/tests/validation/validation_data.hpp @@ -70,7 +70,7 @@ public: } private: - util::path datadir_ = DATADIR "/validation"; + util::path datadir_ = DATADIR; std::ofstream out_; nlohmann::json jtraces_ = nlohmann::json::array(); bool verbose_flag_ = false; diff --git a/validation/ref/neuron/nrn_validation.py b/validation/ref/neuron/nrn_validation.py index 66563885b05ba9212bd017ea1c90b514c93c6ff2..a5ffa2d2e238148d68dee137dd92daa17ce89d89 100644 --- a/validation/ref/neuron/nrn_validation.py +++ b/validation/ref/neuron/nrn_validation.py @@ -33,7 +33,7 @@ default_model_parameters = { 'g_pas': 0.001, # Passive conductance in S/cm^2 'e_pas': -65.0, # Leak reversal potential in mV 'Ra': 100.0, # Intracellular resistivity in Ω·cm - 'cm': 1.0, # Membrane areal capacitance in µF/cm2 + 'cm': 1.0, # Membrane areal capacitance in µF/cm^2 'tau': 2.0, # Exponential synapse time constant 'tau1': 0.5, # Exp2 synapse tau1 'tau2': 2.0, # Exp2 synapse tau2 diff --git a/validation/ref/neuron/soma.py b/validation/ref/neuron/soma.py index f00d6bf012263d45a57209e49a7d094ba07f38d5..706af0f559a3172284bb45d9ca503e47a9f63cc2 100644 --- a/validation/ref/neuron/soma.py +++ b/validation/ref/neuron/soma.py @@ -6,9 +6,6 @@ import nrn_validation as V V.override_defaults_from_args() -# dendrite geometry: all 100 µm long, 1 µm diameter. -geom = [(0,1), (100, 1)] - model = V.VModel() model.add_soma(18.8, Ra=100) diff --git a/validation/ref/numeric/CMakeLists.txt b/validation/ref/numeric/CMakeLists.txt index a697439f0f8681efa9f0a0016d023e1897e7684c..7aaf6b7ae3157ede5cf66eadf82d73f73cbb107b 100644 --- a/validation/ref/numeric/CMakeLists.txt +++ b/validation/ref/numeric/CMakeLists.txt @@ -1,4 +1,7 @@ # note: function add_validation_data defined in validation/CMakeLists.txt -# placeholder -add_validation_data(OUTPUT foo.out DEPENDS foo.sh COMMAND bash foo.sh) +add_validation_data( + OUTPUT numeric_soma.json + DEPENDS numeric_soma.jl HHChannels.jl + COMMAND julia numeric_soma.jl) + diff --git a/validation/ref/numeric/HHChannels.jl b/validation/ref/numeric/HHChannels.jl new file mode 100644 index 0000000000000000000000000000000000000000..8ce1bd993fb8aa2f86f68dc25e885a4e6e45c832 --- /dev/null +++ b/validation/ref/numeric/HHChannels.jl @@ -0,0 +1,143 @@ +module HHChannels + +export Stim, run_hh + +using Sundials +using SIUnits.ShortUnits + +immutable HHParam + c_m # membrane spacific capacitance + gnabar # Na channel cross-membrane conductivity + gkbar # K channel cross-membrane conductivity + gl # Leak conductivity + ena # Na channel reversal potential + ek # K channel reversal potential + el # Leak reversal potential + q10 # temperature dependent rate coefficient + # (= 3^((T-T₀)/10K) with T₀ = 6.3 °C) + + # constructor with default values, corresponding + # to a resting potential of -65 mV and temperature 6.3 °C + HHParam(; + #c_m = 0.01nF*m^-2, + c_m = 0.01F*m^-2, + gnabar = .12S*cm^-2, + ena = 115.0mV + -65.0mV, + gkbar = .036S*cm^-2, + ek = -12.0mV + -65.0mV, + gl = .0003S*cm^-2, + el = -54.3mV, + q10 = 1 + ) = new(c_m, gnabar, gkbar, gl, ena, ek, el, q10) + +end + +immutable Stim + t0 # start time of stimulus + t1 # stop time of stimulus + j # stimulus current density + + Stim() = new(0s, 0s, 0A/m^2) + Stim(t0, t1, j) = new(t0, t1, j) +end + +vtrap(x,y) = x/(exp(x/y) - 1.0) + +# "m" sodium activation system +function m_lims(v, q10) + alpha = .1mV^-1 * vtrap(-(v+40mV),10mV) + beta = 4 * exp(-(v+65mV)/18mV) + sum = alpha + beta + mtau = 1ms / (q10*sum) + minf = alpha/sum + return mtau, minf +end + +# "h" sodium inactivation system +function h_lims(v, q10) + alpha = 0.07*exp(-(v+65mV)/20mV) + beta = 1 / (exp(-(v+35mV)/10mV) + 1) + sum = alpha + beta + htau = 1ms / (q10*sum) + hinf = alpha/sum + return htau, hinf +end + +# "n" potassium activation system +function n_lims(v, q10) + alpha = .01mV^-1 * vtrap(-(v+55mV),10mV) + beta = .125*exp(-(v+65mV)/80mV) + sum = alpha + beta + ntau = 1ms / (q10*sum) + ninf = alpha/sum + return ntau, ninf +end + +# Choose initial conditions for the system such that the gating variables +# are at steady state for the user-specified voltage v +function initial_conditions(v, q10) + mtau, minf = m_lims(v, q10) + htau, hinf = h_lims(v, q10) + ntau, ninf = n_lims(v, q10) + + return (v, minf, hinf, ninf) +end + +# Given time t and state (v, m, h, n), +# return (vdot, mdot, hdot, ndot) +function f(t, state; p=HHParam(), stim=Stim()) + v, m, h, n = state + + # calculate current density due to ion channels + gna = p.gnabar*m*m*m*h + gk = p.gkbar*n*n*n*n + + + ina = gna*(v - p.ena) + ik = gk*(v - p.ek) + il = p.gl*(v - p.el) + + itot = ik + ina + il + + # calculate current density due to stimulus + if t>=stim.t0 && t<stim.t1 + itot -= stim.j + end + + # calculate the voltage dependent rates for the gating variables + mtau, minf = m_lims(v, p.q10) + htau, hinf = h_lims(v, p.q10) + ntau, ninf = n_lims(v, p.q10) + + return (-itot/p.c_m, (minf-m)/mtau, (hinf-h)/htau, (ninf-n)/ntau) +end + +function run_hh(t_end; v0=-65mV, stim=Stim(), param=HHParam(), sample_dt=0.01ms) + v_scale = 1V + t_scale = 1s + + v0, m0, h0, n0 = initial_conditions(v0, param.q10) + y0 = [ v0/v_scale, m0, h0, n0 ] + + samples = collect(0s: sample_dt: t_end) + + fbis(t, y, ydot) = begin + vdot, mdot, hdot, ndot = + f(t*t_scale, (y[1]*v_scale, y[2], y[3], y[4]), stim=stim, p=param) + + ydot[1], ydot[2], ydot[3], ydot[4] = + vdot*t_scale/v_scale, mdot*t_scale, hdot*t_scale, ndot*t_scale + + return Sundials.CV_SUCCESS + end + + # Ideally would run with vector absolute tolerance to account for v_scale, + # but this would prevent us using the nice cvode wrapper. + + res = Sundials.cvode(fbis, y0, map(t->t/t_scale, samples), abstol=1e-6, reltol=5e-10) + + # Use map here because of issues with type deduction with arrays and SIUnits. + return samples, map(v->v*v_scale, res[:, 1]) +end + +end # module HHChannels diff --git a/validation/ref/numeric/hh_soma.jl b/validation/ref/numeric/hh_soma.jl deleted file mode 100644 index 1db5813f0349b45f90ac3db1dcdef18352716058..0000000000000000000000000000000000000000 --- a/validation/ref/numeric/hh_soma.jl +++ /dev/null @@ -1,138 +0,0 @@ -using Sundials -using SIUnits.ShortUnits - -c_m = 0.01nF*m^-2 - -radius = 18.8μm / 2 -surface_area = 4 * pi * radius * radius - -gnabar = .12S*cm^-2 -gkbar = .036S*cm^-2 -gl = .0003S*cm^-2 -el = -54.3mV -#celsius= 6.3 -#q10 = 3^((celsius - 6.3)/10) -q10 = 1 - -# define the resting potential for the membrane -vrest = -65.0mV - -# define the resting potentials for ion species -ena = 115.0mV+vrest -ek = -12.0mV+vrest -eca = 12.5mV*log(2.0/5e-5) - -vtrap(x,y) = x/(exp(x/y) - 1.0) - -function print() - println("q10 ", q10) - println("vrest ", vrest) - println("ena ", ena) - println("ek ", ek) - println("eca ", eca) -end - -#"m" sodium activation system -function m_lims(v) - alpha = .1mV^-1 * vtrap(-(v+40mV),10mV) - beta = 4 * exp(-(v+65mV)/18mV) - sum = alpha + beta - mtau = 1ms / (q10*sum) - minf = alpha/sum - return mtau, minf -end - -#"h" sodium inactivation system -function h_lims(v) - alpha = 0.07*exp(-(v+65mV)/20mV) - beta = 1 / (exp(-(v+35mV)/10mV) + 1) - sum = alpha + beta - htau = 1ms / (q10*sum) - hinf = alpha/sum - return htau, hinf -end - -#"n" potassium activation system -function n_lims(v) - alpha = .01mV^-1 * vtrap(-(v+55mV),10mV) - beta = .125*exp(-(v+65mV)/80mV) - sum = alpha + beta - ntau = 1ms / (q10*sum) - ninf = alpha/sum - return ntau, ninf -end - -# v = y[1] V -# m = y[2] -# h = y[3] -# n = y[4] - -# dv/dt = ydot[1] V/s -# dm/dt = ydot[2] /s -# dh/dt = ydot[3] /s -# dn/dt = ydot[4] /s - -# choose initial conditions for the system such that the gating variables -# are at steady state for the user-specified voltage v -function initial_conditions(v) - mtau, minf = m_lims(v) - htau, hinf = h_lims(v) - ntau, ninf = n_lims(v) - - return [v/V, minf, hinf, ninf] -end - -# calculate the lhs of the ODE system -function f(t, y, ydot) - # copy variables into helper variable - v = y[1]V - m, h, n = y[2], y[3], y[4] - - # calculate current due to ion channels - gna = gnabar*m*m*m*h - gk = gkbar*n*n*n*n - ina = gna*(v - ena) - ik = gk*(v - ek) - il = gl*(v - el) - imembrane = ik + ina + il - - # calculate current due to stimulus - #c.add_stimulus({0,0.5}, {10., 100., 0.1}); - ielectrode = 0.0nA / surface_area - time = t*ms - if time>=10ms && time<100ms - ielectrode = 0.1nA / surface_area - end - - # calculate the total membrane current - i = -imembrane + ielectrode - - # calculate the voltage dependent rates for the gating variables - mtau, minf = m_lims(v) - ntau, ninf = n_lims(v) - htau, hinf = h_lims(v) - - # set the derivatives - # note values are in SI units, as determined by the scaling factors: - ydot[1] = i/c_m / (V/s) - ydot[2] = (minf-m)/mtau / (1/s) - ydot[3] = (hinf-h)/htau / (1/s) - ydot[4] = (ninf-n)/ntau / (1/s) - - return Sundials.CV_SUCCESS -end - -########################################################### -# now we actually run the model -########################################################### - -# from 0 to 100 ms in 1000 steps -t = collect(linspace(0.0, 0.1, 1001)); - -# these tolerances are as tight as they will go without breaking convergence of the iterative schemes -y0 = initial_conditions(vrest) -res = Sundials.cvode(f, y0, t, abstol=1e-6, reltol=5e-10); - -#using Plots -#gr() -#plot(t, res[:,1]) diff --git a/validation/ref/numeric/numeric_soma.jl b/validation/ref/numeric/numeric_soma.jl new file mode 100644 index 0000000000000000000000000000000000000000..6c431f845cbf5acbb3a1825e8d52951482d255a1 --- /dev/null +++ b/validation/ref/numeric/numeric_soma.jl @@ -0,0 +1,27 @@ +#!/usr/bin/env julia + +include("HHChannels.jl") + +using JSON +using SIUnits.ShortUnits +using HHChannels + +radius = 18.8µm/2 +area = 4*pi*radius^2 + +stim = Stim(10ms, 100ms, 0.1nA/area) +ts, vs = run_hh(100ms, stim=stim, sample_dt=0.025ms) + +trace = Dict( + :name => "membrane voltage", + :sim => "numeric", + :model => "soma", + :units => "mV", + :data => Dict( + :time => map(t->t/ms, ts), + symbol("soma.mid") => map(v->v/mV, vs) + ) +) + +println(JSON.json([trace])) +