diff --git a/scripts/tsplot b/scripts/tsplot index 8e89cbda96451680bfc9de8087ba558c0282defc..4280a9dc902cfa738e5eaf8bb7043008d96baae0 100755 --- a/scripts/tsplot +++ b/scripts/tsplot @@ -44,6 +44,7 @@ def parse_clargs(): help='plot series with same KEYs on the same axes') P.add_argument('-s', '--select', metavar='EXPR,...', dest='select', type=lambda s: s.split(','), + action='append', help='select only series matching EXPR') P.add_argument('-o', '--output', metavar='FILE', dest='outfile', help='save plot to file FILE') @@ -212,7 +213,7 @@ def read_json_timeseries(j, axis='time', select=[]): meta = dict(j.items() + {'label': key, 'data': None, 'units': units(key)}.items()) del meta['data'] - if all([run_select(sel, meta) for sel in select]): + if not select or any([all([run_select(s, meta) for s in term]) for term in select]): ts_list.append(TimeSeries(times, jdata[key], **meta)) return ts_list @@ -422,7 +423,7 @@ args = parse_clargs() tss = [] axis = args.axis if args.axis else 'time' for filename in args.inputs: - select = args.select if args.select else [] + select = args.select with open(filename) as f: j = json.load(f) tss.extend(read_json_timeseries(j, axis, select)) diff --git a/tests/test_common_cells.hpp b/tests/test_common_cells.hpp index 41e2fea727b9e217899a0f8717b7085b7fc0f84a..b74a18c384a52301c45585d978c380be179c1e01 100644 --- a/tests/test_common_cells.hpp +++ b/tests/test_common_cells.hpp @@ -13,6 +13,7 @@ namespace mc { * diameter: 18.8 µm * mechanisms: HH (default params) * bulk resistivitiy: 100 Ω·cm + * capacitance: 0.01 F/m² [default] * * Stimuli: * soma centre, t=[10 ms, 110 ms), 0.1 nA @@ -37,6 +38,7 @@ inline cell make_cell_soma_only(bool with_stim = true) { * * Common properties: * bulk resistivity: 100 Ω·cm + * capacitance: 0.01 F/m² [default] * * Soma: * mechanisms: HH (default params) @@ -80,6 +82,7 @@ inline cell make_cell_ball_and_stick(bool with_stim = true) { * * Common properties: * bulk resistivity: 100 Ω·cm + * capacitance: 0.01 F/m² [default] * * Soma: * mechanisms: HH (default params) @@ -126,6 +129,7 @@ inline cell make_cell_ball_and_taper(bool with_stim = true) { * * Common properties: * bulk resistivity: 100 Ω·cm + * capacitance: 0.01 F/m² [default] * * Soma: * mechanisms: HH (default params) @@ -173,11 +177,12 @@ inline cell make_cell_ball_and_3stick(bool with_stim = true) { * * Common properties: * mechanisms: passive - * membrane conductance: 0.000025 S·cm¯² ( = 1/(4Ω·m²) ) + * membrane conductance: 0.000025 S/cm² ( = 1/(4Ω·m²) ) * membrane reversal potential: -65 mV (default) * diameter: 1 µm * length: 1000 µm * bulk resistivity: 100 Ω·cm + * capacitance: 0.01 F/m² [default] * compartments: 4 * * Stimulus: @@ -185,21 +190,43 @@ inline cell make_cell_ball_and_3stick(bool with_stim = true) { * * Note: zero-volume soma added with same mechanisms, as * work-around for some existing fvm modelling issues. + * + * TODO: Set the correct values when parameters are generally + * settable! + * + * We can't currently change leak parameters + * from defaults, so we scale other electrical parameters + * proportionally. */ inline cell make_cell_simple_cable(bool with_stim = true) { cell c; c.add_soma(0); - c.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 100); + c.add_cable(0, segmentKind::dendrite, 0.5, 0.5, 1000); + + double r_L = 100; + double c_m = 0.01; + double gbar = 0.000025; + double I = 0.1; + + // fudge factor! can't change passive membrane + // conductance from gbar0 = 0.001 + + double gbar0 = 0.001; + double f = gbar/gbar0; + + // scale everything else + r_L *= f; + c_m /= f; + I /= f; 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); + seg->mechanism("membrane").set("r_L", r_L); + seg->mechanism("membrane").set("c_m", c_m); + // seg->mechanism("pas").set("g", gbar); + if (seg->is_dendrite()) { seg->set_compartments(4); } @@ -208,7 +235,7 @@ inline cell make_cell_simple_cable(bool with_stim = true) { if (with_stim) { // stimulus in the middle of our zero-volume 'soma' // corresponds to proximal end of cable. - c.add_stimulus({0,0.5}, {0., HUGE_VAL, 0.1}); + c.add_stimulus({0,0.5}, {0., HUGE_VAL, I}); } return c; } diff --git a/tests/validation/trace_analysis.cpp b/tests/validation/trace_analysis.cpp index d641e34bcbad20ea1c64aece9d47f9c17a4856f0..a754cfe228b8893a6d557eb40092cbde8de4dfa5 100644 --- a/tests/validation/trace_analysis.cpp +++ b/tests/validation/trace_analysis.cpp @@ -83,7 +83,7 @@ util::optional<trace_peak> peak_delta(const trace_data& a, const trace_data& b) auto p = local_maxima(a); auto q = local_maxima(b); - if (p.size()!=q.size()) return util::nothing; + if (p.size()!=q.size() || p.empty()) return util::nothing; auto max_delta = p[0]-q[0]; diff --git a/tests/validation/validate_ball_and_stick.cpp b/tests/validation/validate_ball_and_stick.cpp index 7927a748edc09830b151b68350ce6b35cfb3615a..e01eb4cf9c6f95a2631e1bbf370762967df71487 100644 --- a/tests/validation/validate_ball_and_stick.cpp +++ b/tests/validation/validate_ball_and_stick.cpp @@ -33,10 +33,11 @@ template < > void run_ncomp_convergence_test( const char* model_name, - const char* refdata_path, + const char* ref_data_path, const cell& c, SamplerInfoSeq& samplers, - float t_end=100.f) + float t_end=100.f, + float dt=0.001) { using nlohmann::json; @@ -52,7 +53,7 @@ void run_ncomp_convergence_test( bool run_validation = false; std::map<std::string, trace_data> ref_data; try { - ref_data = V.load_traces(refdata_path); + ref_data = V.load_traces(ref_data_path); run_validation = std::all_of(keys.begin(), keys.end(), [&](const char* key) { return ref_data.count(key)>0; }); @@ -60,7 +61,7 @@ void run_ncomp_convergence_test( EXPECT_TRUE(run_validation); } catch (std::runtime_error&) { - ADD_FAILURE() << "failure loading reference data: " << refdata_path; + ADD_FAILURE() << "failure loading reference data: " << ref_data_path; } std::map<std::string, std::vector<conv_entry<int>>> conv_results; @@ -79,7 +80,7 @@ void run_ncomp_convergence_test( m.attach_sampler(se.probe, se.sampler.template sampler<>()); } - m.run(100, 0.001); + m.run(t_end, dt); for (auto& se: samplers) { std::string key = se.label; @@ -202,279 +203,7 @@ TEST(rallpack1, numeric_ref) { "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 - - using namespace nlohmann; - - using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>; - auto& V = g_trace_io; - - bool verbose = V.verbose(); - int max_ncomp = V.max_ncomp(); - - // load validation data - auto ref_data = V.load_traces("neuron_ball_and_stick.json"); - bool run_validation = - ref_data.count("soma.mid") && - ref_data.count("dend.mid") && - ref_data.count("dend.end"); - - EXPECT_TRUE(run_validation); - - // generate test data - cell c = make_cell_ball_and_stick(); - add_common_voltage_probes(c); - - float sample_dt = .025; - std::pair<const char *, simple_sampler> samplers[] = { - {"soma.mid", simple_sampler(sample_dt)}, - {"dend.mid", simple_sampler(sample_dt)}, - {"dend.end", simple_sampler(sample_dt)} - }; - - std::map<std::string, std::vector<conv_entry<int>>> conv_results; - - for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) { - for (auto& se: samplers) { - se.second.reset(); - } - c.cable(1)->set_compartments(ncomp); - model<lowered_cell> m(singleton_recipe{c}); - - // the ball-and-stick-cell (should) have three voltage probes: - // centre of soma, centre of dendrite, end of dendrite. - - m.attach_sampler({0u, 0u}, samplers[0].second.sampler<>()); - m.attach_sampler({0u, 1u}, samplers[1].second.sampler<>()); - m.attach_sampler({0u, 2u}, samplers[2].second.sampler<>()); - - m.run(100, 0.001); - - for (auto& se: samplers) { - std::string key = se.first; - const simple_sampler& s = se.second; - - // save trace - json meta = { - {"name", "membrane voltage"}, - {"model", "ball_and_stick"}, - {"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: util::transform_view(samplers, util::first)) { - SCOPED_TRACE(key); - - const auto& results = conv_results[key]; - assert_convergence(results); - } -} - -TEST(ball_and_taper, neuron_ref) { - // compare voltages against reference data produced from - // nrn/ball_and_taper.py - - using namespace nlohmann; - - using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>; - auto& V = g_trace_io; - - bool verbose = V.verbose(); - int max_ncomp = V.max_ncomp(); - - // load validation data - auto ref_data = V.load_traces("neuron_ball_and_taper.json"); - bool run_validation = - ref_data.count("soma.mid") && - ref_data.count("taper.mid") && - ref_data.count("taper.end"); - - EXPECT_TRUE(run_validation); - - // generate test data - cell c = make_cell_ball_and_taper(); - add_common_voltage_probes(c); - - float sample_dt = .025; - std::pair<const char *, simple_sampler> samplers[] = { - {"soma.mid", simple_sampler(sample_dt)}, - {"taper.mid", simple_sampler(sample_dt)}, - {"taper.end", simple_sampler(sample_dt)} - }; - - std::map<std::string, std::vector<conv_entry<int>>> conv_results; - - for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) { - for (auto& se: samplers) { - se.second.reset(); - } - c.cable(1)->set_compartments(ncomp); - model<lowered_cell> m(singleton_recipe{c}); - - // the ball-and-stick-cell (should) have three voltage probes: - // centre of soma, centre of dendrite, end of dendrite. - - m.attach_sampler({0u, 0u}, samplers[0].second.sampler<>()); - m.attach_sampler({0u, 1u}, samplers[1].second.sampler<>()); - m.attach_sampler({0u, 2u}, samplers[2].second.sampler<>()); - - m.run(100, 0.001); - - for (auto& se: samplers) { - std::string key = se.first; - const simple_sampler& s = se.second; - - // save trace - json meta = { - {"name", "membrane voltage"}, - {"model", "ball_and_taper"}, - {"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: util::transform_view(samplers, util::first)) { - SCOPED_TRACE(key); - - const auto& results = conv_results[key]; - assert_convergence(results); - } -} - - -TEST(ball_and_3stick, neuron_ref) { - // compare voltages against reference data produced from - // nrn/ball_and_3stick.py - - using namespace nlohmann; - - using lowered_cell = fvm::fvm_multicell<double, cell_local_size_type>; - auto& V = g_trace_io; - - bool verbose = V.verbose(); - int max_ncomp = V.max_ncomp(); - - // load validation data - auto ref_data = V.load_traces("neuron_ball_and_3stick.json"); - bool run_validation = - ref_data.count("soma.mid") && - ref_data.count("dend1.mid") && - ref_data.count("dend1.end") && - ref_data.count("dend2.mid") && - ref_data.count("dend2.end") && - ref_data.count("dend3.mid") && - ref_data.count("dend3.end"); - - - EXPECT_TRUE(run_validation); - - // generate test data - cell c = make_cell_ball_and_3stick(); - add_common_voltage_probes(c); - - float sample_dt = .025; - std::pair<const char *, simple_sampler> samplers[] = { - {"soma.mid", simple_sampler(sample_dt)}, - {"dend1.mid", simple_sampler(sample_dt)}, - {"dend1.end", simple_sampler(sample_dt)}, - {"dend2.mid", simple_sampler(sample_dt)}, - {"dend2.end", simple_sampler(sample_dt)}, - {"dend3.mid", simple_sampler(sample_dt)}, - {"dend3.end", simple_sampler(sample_dt)} - }; - - std::map<std::string, std::vector<conv_entry<int>>> conv_results; - - for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) { - for (auto& se: samplers) { - se.second.reset(); - } - c.cable(1)->set_compartments(ncomp); - c.cable(2)->set_compartments(ncomp); - c.cable(3)->set_compartments(ncomp); - model<lowered_cell> m(singleton_recipe{c}); - - // the ball-and-3stick-cell (should) have seven voltage probes: - // centre of soma, followed by centre of section, end of section - // for each of the three dendrite sections. - - for (unsigned i = 0; i < util::size(samplers); ++i) { - m.attach_sampler({0u, i}, samplers[i].second.sampler<>()); - } - - m.run(100, 0.001); - - for (auto& se: samplers) { - std::string key = se.first; - const simple_sampler& s = se.second; - - // save trace - json meta = { - {"name", "membrane voltage"}, - {"model", "ball_and_3stick"}, - {"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: util::transform_view(samplers, util::first)) { - SCOPED_TRACE(key); - - const auto& results = conv_results[key]; - assert_convergence(results); - } + samplers, + 250.f); } -#endif diff --git a/tests/validation/validate_soma.cpp b/tests/validation/validate_soma.cpp index 3e848b65856691bef455a6ab0b76dd9c7c1dda1e..31eb784add73f5b675e8b2943e8faa453a0afb26 100644 --- a/tests/validation/validate_soma.cpp +++ b/tests/validation/validate_soma.cpp @@ -32,11 +32,21 @@ TEST(soma, numeric_ref) { bool verbose = V.verbose(); // load validation data - //auto ref_data = V.load_traces("neuron_soma.json"); - auto ref_data = V.load_traces("numeric_soma.json"); + + bool run_validation = false; + std::map<std::string, trace_data> ref_data; const char* key = "soma.mid"; - bool run_validation = ref_data.count(key); - EXPECT_TRUE(run_validation); + + const char* ref_data_path = "numeric_soma.json"; + try { + ref_data = V.load_traces(ref_data_path); + run_validation = ref_data.count(key); + + EXPECT_TRUE(run_validation); + } + catch (std::runtime_error&) { + ADD_FAILURE() << "failure loading reference data: " << ref_data_path; + } // generate test data cell c = make_cell_soma_only(); diff --git a/validation/ref/numeric/CMakeLists.txt b/validation/ref/numeric/CMakeLists.txt index 7aaf6b7ae3157ede5cf66eadf82d73f73cbb107b..68dc379216190d25ecf29aefb352a189d3913803 100644 --- a/validation/ref/numeric/CMakeLists.txt +++ b/validation/ref/numeric/CMakeLists.txt @@ -5,3 +5,7 @@ add_validation_data( DEPENDS numeric_soma.jl HHChannels.jl COMMAND julia numeric_soma.jl) +add_validation_data( + OUTPUT numeric_rallpack1.json + DEPENDS numeric_rallpack1.jl PassiveCable.jl + COMMAND julia numeric_rallpack1.jl) diff --git a/validation/ref/numeric/PassiveCable.jl b/validation/ref/numeric/PassiveCable.jl index 76450490ce5c4f265403edd35f2bedad4ad7c3bb..6928b74c33948be7162885040cb89870ae245596 100644 --- a/validation/ref/numeric/PassiveCable.jl +++ b/validation/ref/numeric/PassiveCable.jl @@ -18,26 +18,29 @@ export cable_normalize, cable, rallpack1 # # Return: # g(x, t) +# +# TODO: verify correctness when L≠1 -function cable_normalized(x, t, L; tol=1e-8) +function cable_normalized(x::Float64, t::Float64, L::Float64; tol=1e-8) if t<=0 return 0.0 else ginf = -cosh(L-x)/sinh(L) sum = exp(-t/L) + Ltol = L*tol for k = countfrom(1) a = k*pi/L - e = exp(-t*(1+a^2)) + b = exp(-t*(1+a^2)) - sum += 2/L*e*cos(a*x)/(1+a^2) - resid_ub = e/(L*a^3*t) + sum += 2*b*cos(a*x)/(1+a^2) + resid_ub = b/(a^3*t) - if resid_ub<tol + if resid_ub<Ltol break end end - return ginf+sum + return ginf+sum/L; end end diff --git a/validation/ref/numeric/numeric_rallpack1.jl b/validation/ref/numeric/numeric_rallpack1.jl new file mode 100644 index 0000000000000000000000000000000000000000..d0f5997e66310cada8b7258249768b19c3850012 --- /dev/null +++ b/validation/ref/numeric/numeric_rallpack1.jl @@ -0,0 +1,60 @@ +#!/usr/bin/env julia + +include("PassiveCable.jl") + +using JSON +using SIUnits.ShortUnits +using PassiveCable + +function run_cable(x_prop, ts) + # physical properties + + # f is a fudge factor. rM needs to be the same + # the same as in nestmc, where we cannot yet set + # the membrane conductance parameter. Scaling + # other parameters proportionally, however, + # gives the same dynamics. + + f = 0.1/4 + + diam = 1.0µm # cable diameter + L = 1.0mm # cable length + I = 0.1nA /f # current injection + rL = 1.0Ω*m *f # bulk resistivity + erev = -65.0mV # (passive) reversal potential + rM = 4Ω*m^2 *f # membrane resistivity + cM = 0.01F/m^2 /f # membrane specific capacitance + + # convert to linear resistivity, length and time constants + area = pi*diam^2/4 + r = rL/area + + lambda = sqrt(diam/4 * rM/rL) + tau = cM*rM + + # compute solutions + tol = 1e-8mV + return [cable(L*x_prop, t, L, lambda, tau, r, erev, -I, tol=tol) for t in ts] +end + +function run_rallpack1(x_prop, ts) + return [rallpack1(0.001*x_prop, t/s)*V for t in ts] +end +# Generate traces at x=0, x=0.3L, x=L + +ts = collect(0s: 0.025ms: 250ms) +trace = Dict( + :name => "membrane voltage", + :sim => "numeric", + :model => "rallpack1", + :units => "mV", + :data => Dict( + :time => map(t->t/ms, ts), + symbol("cable.x0.0") => map(v->v/mV, run_cable(0, ts)), + symbol("cable.x0.3") => map(v->v/mV, run_rallpack1(0.3, ts)), + symbol("cable.x1.0") => map(v->v/mV, run_cable(1.0, ts)) + ) +) + +println(JSON.json([trace])) +