From 60445a43c524585cae9322759341bc615f9ad6a4 Mon Sep 17 00:00:00 2001 From: Thorsten Hater <24411438+thorstenhater@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:46:52 +0100 Subject: [PATCH] Ux/units (#2233) The core issue here is to add units to the user facing API. I decided on using the LLNL/units library, which offers conversion and checking at runtime. Runtime is a requirement -- as much as I love static guarantees --, but keeping the interface uniform between Python and C++ is a must. While setting this up, I noticed the severe lack of IDE/LSP support for Arbor, so I added typing stubs using https://github.com/sizmailov/pybind11-stubgen. The conjunction of typing and units exposed misuse of pybind11 in several places, so next I had to massage the ordering of bindings, adjust the specification of default arguments, and add the odd missing binding. The schedule/event generator interface was tightened up, hiding the `*_impl` structs and exposing only the type erased `schedule` object. That in turn required de-generification of the Poisson schedule. Now, Mersenne twister is the only choice and I will remove that later on for the CBRNG we are already using elsewhere. Currently, units are used for: - [X] simulation - [X] schedule/generator - [x] paintables - [X] placeables - [X] iclamp - [X] threshold - [X] connections - [X] gap junctions Adding units to mechanism interfaces is _interesting_ but requires more work and thought, so I'll defer that to a later point in time. We'd need to adjust modcc to expose and **check** units and devise a scheme to handle missing units. Generic TODOs; some might spin off into separate issues. - [x] ~~rename py::iclamp OR cpp::i_clamp for consistency~~ covered by #2239 - [x] use scale/base for iexpr paintables for consistency with scaled_mech - [x] ~~Use CBRNG for Poisson schedule~~ covered by #2243 - [ ] Automate stub generation. A wishlist item, requires installing extra software. - [x] Properly integrate units w/ spack. NB. Units doesn't have a spackage. Closes #1983 Closes #2032 --------- Co-authored-by: boeschf <48126478+boeschf@users.noreply.github.com> --- .github/workflows/test-spack.yml | 1 + .gitmodules | 3 + CMakeLists.txt | 14 + arbor/backends/gpu/threshold_watcher.hpp | 2 +- arbor/cable_cell.cpp | 6 +- arbor/cable_cell_param.cpp | 51 +- arbor/fvm_layout.cpp | 56 +- arbor/include/arbor/cable_cell.hpp | 3 +- arbor/include/arbor/cable_cell_param.hpp | 218 +- arbor/include/arbor/event_generator.hpp | 48 +- arbor/include/arbor/iexpr.hpp | 12 +- arbor/include/arbor/lif_cell.hpp | 19 +- arbor/include/arbor/morph/isometry.hpp | 2 +- arbor/include/arbor/recipe.hpp | 20 +- arbor/include/arbor/schedule.hpp | 229 +- arbor/include/arbor/simulation.hpp | 4 +- arbor/include/arbor/spike_event.hpp | 1 - arbor/include/arbor/units.hpp | 189 + arbor/lif_cell_group.cpp | 4 +- arbor/lif_cell_group.hpp | 42 +- arbor/schedule.cpp | 270 +- arbor/simulation.cpp | 18 +- arborio/cableio.cpp | 82 +- arborio/parse_helpers.hpp | 3 +- doc/concepts/decor.rst | 3 +- doc/concepts/units.rst | 192 + doc/cpp/cable_cell.rst | 8 +- doc/cpp/interconnectivity.rst | 4 +- doc/cpp/lif_cell.rst | 28 +- doc/fileformat/cable_cell.rst | 10 +- doc/index.rst | 1 + doc/python/interconnectivity.rst | 4 +- doc/python/lif_cell.rst | 18 +- doc/python/recipe.rst | 2 +- doc/tutorial/calcium_stdp_curve.rst | 88 +- doc/tutorial/network_ring.rst | 131 +- doc/tutorial/network_ring_gpu.rst | 144 +- .../network_two_cells_gap_junctions.rst | 78 +- doc/tutorial/probe_lfpykit.rst | 127 +- doc/tutorial/single_cell_allen.rst | 141 +- doc/tutorial/single_cell_detailed.rst | 343 +- doc/tutorial/single_cell_detailed_recipe.rst | 181 +- doc/tutorial/single_cell_model.rst | 189 +- doc/tutorial/single_cell_recipe.rst | 113 +- example/bench/bench.cpp | 10 +- example/brunel/brunel.cpp | 22 +- example/busyring/ring.cpp | 32 +- example/diffusion/diffusion.cpp | 16 +- example/drybench/drybench.cpp | 16 +- example/dryrun/branch_cell.hpp | 20 +- example/dryrun/dryrun.cpp | 16 +- example/gap_junctions/gap_junctions.cpp | 16 +- example/generators/generators.cpp | 18 +- example/lfp/lfp.cpp | 13 +- example/ornstein_uhlenbeck/ou.cpp | 4 +- example/plasticity/branch_cell.hpp | 6 +- example/plasticity/plasticity.cpp | 15 +- example/probe-demo/probe-demo.cpp | 11 +- example/remote/remote.cpp | 25 +- example/ring/branch_cell.hpp | 10 +- example/ring/ring.cpp | 16 +- example/single/single.cpp | 13 +- example/v_clamp/v-clamp.cpp | 5 +- ext/CMakeLists.txt | 16 + ext/sup/include/sup/export.hpp | 41 + ext/units | 1 + .../stochastic/calcium_based_synapse.mod | 22 +- python/CMakeLists.txt | 3 + python/cable_cell_io.cpp | 14 +- python/cells.cpp | 770 +- python/example/brunel.py | 71 +- python/example/calcium_stdp.py | 231 +- python/example/diffusion.py | 49 +- python/example/dynamic-catalogue.py | 25 +- python/example/gap_junctions.py | 69 +- python/example/network_ring.py | 120 +- python/example/network_ring_gpu.py | 94 +- python/example/network_ring_mpi.py | 70 +- .../network_two_cells_gap_junctions.py | 284 +- python/example/ou_lif/ou_lif.py | 131 +- python/example/ou_lif/traces.svg | 41310 ++++++++-------- python/example/plasticity.py | 11 +- python/example/probe_lfpykit.py | 154 +- python/example/single_cell_allen.py | 128 +- .../l5pc/C060114A7_axon_replacement.acc | 4 +- .../l5pc/C060114A7_modified.acc | 4 +- .../single_cell_bluepyopt/l5pc/l5pc_decor.acc | 28 +- .../l5pc/l5pc_label_dict.acc | 4 +- .../simplecell/simple_cell_decor.acc | 6 +- .../simplecell/simple_cell_label_dict.acc | 4 +- python/example/single_cell_cable.py | 74 +- python/example/single_cell_detailed.py | 143 +- python/example/single_cell_detailed_recipe.py | 150 +- python/example/single_cell_model.py | 47 +- python/example/single_cell_nml.py | 57 +- python/example/single_cell_recipe.py | 80 +- python/example/single_cell_stdp.py | 57 +- python/example/single_cell_swc.py | 56 +- python/example/v-clamp.py | 38 +- python/identifiers.cpp | 22 + python/label_dict.cpp | 88 + python/{proxy.hpp => label_dict.hpp} | 6 +- python/mechanism.cpp | 2 - python/morphology.cpp | 122 +- python/probes.cpp | 22 +- python/profiler.cpp | 12 +- python/pyarb.cpp | 26 +- python/recipe.cpp | 9 +- python/schedule.cpp | 182 +- python/schedule.hpp | 69 +- python/simulation.cpp | 73 +- python/single_cell_model.cpp | 23 +- python/stubs/arbor/__init__.pyi | 287 + python/stubs/arbor/_arbor/__init__.pyi | 2991 ++ python/stubs/arbor/_arbor/env.pyi | 46 + python/stubs/arbor/_arbor/py.typed | 0 python/stubs/arbor/_arbor/units.pyi | 185 + python/stubs/arbor/py.typed | 0 python/stubs/py.typed | 0 python/test/fixtures.py | 74 +- python/test/unit/test_catalogues.py | 45 +- python/test/unit/test_clear_samplers.py | 11 +- python/test/unit/test_decor.py | 15 +- python/test/unit/test_event_generators.py | 7 +- python/test/unit/test_io.py | 22 +- python/test/unit/test_multiple_connections.py | 113 +- python/test/unit/test_probes.py | 13 +- python/test/unit/test_profiling.py | 38 +- python/test/unit/test_schedules.py | 152 +- python/test/unit/test_spikes.py | 11 +- python/units.cpp | 129 + python/util.hpp | 3 +- spack/package.py | 8 +- test/common_cells.cpp | 17 +- test/common_cells.hpp | 2 - test/simple_recipes.hpp | 7 +- test/ubench/merge.cpp | 12 +- test/unit-distributed/test_communicator.cpp | 53 +- .../test_domain_decomposition.cpp | 7 - test/unit/test_cable_cell.cpp | 5 +- test/unit/test_cable_cell_group.cpp | 7 +- test/unit/test_cable_cell_group_gpu.cpp | 6 +- test/unit/test_cv_layout.cpp | 11 +- test/unit/test_diffusion.cpp | 46 +- test/unit/test_domain_decomposition.cpp | 2 - test/unit/test_event_delivery.cpp | 4 +- test/unit/test_event_generators.cpp | 14 +- test/unit/test_fvm_layout.cpp | 39 +- test/unit/test_fvm_lowered.cpp | 26 +- test/unit/test_lif_cell_group.cpp | 54 +- test/unit/test_merge_events.cpp | 24 +- test/unit/test_probe.cpp | 74 +- test/unit/test_recipe.cpp | 192 +- test/unit/test_s_expr.cpp | 118 +- test/unit/test_schedule.cpp | 159 +- test/unit/test_sde.cpp | 20 +- test/unit/test_serdes.cpp | 24 +- test/unit/test_simulation.cpp | 49 +- test/unit/test_spike_source.cpp | 43 +- test/unit/test_spikes.cpp | 10 +- test/unit/test_synapses.cpp | 2 - test/unit/test_v_clamp.cpp | 14 +- 162 files changed, 28834 insertions(+), 24765 deletions(-) create mode 100644 arbor/include/arbor/units.hpp create mode 100644 doc/concepts/units.rst create mode 100644 ext/sup/include/sup/export.hpp create mode 160000 ext/units create mode 100644 python/label_dict.cpp rename python/{proxy.hpp => label_dict.hpp} (98%) create mode 100644 python/stubs/arbor/__init__.pyi create mode 100644 python/stubs/arbor/_arbor/__init__.pyi create mode 100644 python/stubs/arbor/_arbor/env.pyi create mode 100644 python/stubs/arbor/_arbor/py.typed create mode 100644 python/stubs/arbor/_arbor/units.pyi create mode 100644 python/stubs/arbor/py.typed create mode 100644 python/stubs/py.typed create mode 100644 python/units.cpp diff --git a/.github/workflows/test-spack.yml b/.github/workflows/test-spack.yml index 23439c7bc4..b78a347580 100644 --- a/.github/workflows/test-spack.yml +++ b/.github/workflows/test-spack.yml @@ -35,6 +35,7 @@ jobs: uses: actions/checkout@v3 with: path: arbor + submodules: recursive - name: clone spack develop if: ${{ matrix.spack-version == 'develop' }} diff --git a/.gitmodules b/.gitmodules index cd515e5542..5ab557ea13 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,3 +26,6 @@ path = ext/pugixml url = https://github.com/zeux/pugixml.git branch = master +[submodule "ext/units"] + path = ext/units + url = https://github.com/LLNL/units.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a0e9e85d9..a765d7b2f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -259,6 +259,9 @@ install(FILES mechanisms/BuildModules.cmake DESTINATION ${ARB_INSTALL_DATADIR}) cmake_dependent_option(ARB_USE_BUNDLED_FMT "Use bundled FMT lib." ON "ARB_USE_BUNDLED_LIBS" OFF) cmake_dependent_option(ARB_USE_BUNDLED_PUGIXML "Use bundled XML lib." ON "ARB_USE_BUNDLED_LIBS" OFF) cmake_dependent_option(ARB_USE_BUNDLED_GTEST "Use bundled GoogleTest." ON "ARB_USE_BUNDLED_LIBS" OFF) +# TODO When we get a units spack package... +#cmake_dependent_option(ARB_USE_BUNDLED_UNITS "Use bundled LLNL units." ON "ARB_USE_BUNDLED_LIBS" OFF) +set(ARB_USE_BUNDLED_UNITS ON CACHE STRING "Use bundled LLNL units.") cmake_dependent_option(ARB_USE_BUNDLED_JSON "Use bundled Niels Lohmann's json library." ON "ARB_USE_BUNDLED_LIBS" OFF) if(NOT ARB_USE_BUNDLED_JSON) @@ -290,9 +293,20 @@ else() endif() endif() +add_library(ext-units INTERFACE) +if(ARB_USE_BUNDLED_UNITS) + target_link_libraries(ext-units INTERFACE units::units) +else() + message(FATAL, "TODO: At the time of Arbor 0.10.0 there is no Spack package") +endif() + + add_subdirectory(ext) install(TARGETS ext-hwloc EXPORT arbor-targets) install(TARGETS ext-random123 EXPORT arbor-targets) +target_link_libraries(arbor-public-deps INTERFACE ext-units) +install(TARGETS ext-units EXPORT arbor-targets) +install(TARGETS units compile_flags_target EXPORT arbor-targets) # Keep track of packages we need to add to the generated CMake config # file for arbor. diff --git a/arbor/backends/gpu/threshold_watcher.hpp b/arbor/backends/gpu/threshold_watcher.hpp index ccf15066fb..d3d7e5824f 100644 --- a/arbor/backends/gpu/threshold_watcher.hpp +++ b/arbor/backends/gpu/threshold_watcher.hpp @@ -72,7 +72,7 @@ class threshold_watcher { v_prev_(num_cv), // TODO: allocates enough space for 10 spikes per watch. // A more robust approach might be needed to avoid overflows. - stack_(10*size(), context.gpu) + stack_(100*size(), context.gpu) { crossings_.reserve(stack_.capacity()); // reset() needs to be called before this is ready for use diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp index 210fdd3cea..21dd4e5392 100644 --- a/arbor/cable_cell.cpp +++ b/arbor/cable_cell.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -10,9 +9,6 @@ #include #include -#include "util/piecewise.hpp" -#include "util/rangeutil.hpp" -#include "util/span.hpp" #include "util/strprintf.hpp" namespace arb { @@ -37,7 +33,7 @@ std::string show(const paintable& item) { else if constexpr (std::is_same_v) { os << "axial-resistivity"; } - else if constexpr (std::is_same_v) { + else if constexpr (std::is_same_v) { os << "temperature-kelvin"; } else if constexpr (std::is_same_v) { diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp index e3b425de7a..a5c4e20792 100644 --- a/arbor/cable_cell_param.cpp +++ b/arbor/cable_cell_param.cpp @@ -75,30 +75,30 @@ cable_cell_parameter_set neuron_parameter_defaults = { std::vector cable_cell_parameter_set::serialize() const { std::vector D; if (init_membrane_potential) { - D.push_back(arb::init_membrane_potential{*this->init_membrane_potential}); + D.push_back(arb::init_membrane_potential{*this->init_membrane_potential*units::mV}); } if (temperature_K) { - D.push_back(arb::temperature_K{*this->temperature_K}); + D.push_back(arb::temperature{*this->temperature_K*units::Kelvin}); } if (axial_resistivity) { - D.push_back(arb::axial_resistivity{*this->axial_resistivity}); + D.push_back(arb::axial_resistivity{*this->axial_resistivity*units::Ohm*units::cm}); } if (membrane_capacitance) { - D.push_back(arb::membrane_capacitance{*this->membrane_capacitance}); + D.push_back(arb::membrane_capacitance{*this->membrane_capacitance*units::F/units::m2}); } for (const auto& [name, data]: ion_data) { if (data.init_int_concentration) { - D.push_back(init_int_concentration{name, *data.init_int_concentration}); + D.push_back(init_int_concentration{name, *data.init_int_concentration*units::mM}); } if (data.init_ext_concentration) { - D.push_back(init_ext_concentration{name, *data.init_ext_concentration}); + D.push_back(init_ext_concentration{name, *data.init_ext_concentration*units::mM}); } if (data.init_reversal_potential) { - D.push_back(init_reversal_potential{name, *data.init_reversal_potential}); + D.push_back(init_reversal_potential{name, *data.init_reversal_potential*units::mV}); } if (data.diffusivity) { - D.push_back(ion_diffusivity{name, *data.diffusivity}); + D.push_back(ion_diffusivity{name, *data.diffusivity*units::m2/units::s}); } } @@ -133,32 +133,32 @@ decor& decor::set_default(defaultable what) { [this] (auto&& p) { using T = std::decay_t; if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.init_membrane_potential = *p.value.get_scalar(); + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + defaults_.init_membrane_potential = *p.scale.get_scalar()*p.value; } else if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.axial_resistivity = *p.value.get_scalar(); + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + defaults_.axial_resistivity = *p.scale.get_scalar()*p.value; } - else if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.temperature_K = *p.value.get_scalar(); + else if constexpr (std::is_same_v) { + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + defaults_.temperature_K = *p.scale.get_scalar()*p.value; } else if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.membrane_capacitance = *p.value.get_scalar(); + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + defaults_.membrane_capacitance = *p.scale.get_scalar()*p.value; } else if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.ion_data[p.ion].init_int_concentration = *p.value.get_scalar(); + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + defaults_.ion_data[p.ion].init_int_concentration = *p.scale.get_scalar()*p.value; } else if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.ion_data[p.ion].init_ext_concentration = *p.value.get_scalar(); + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + defaults_.ion_data[p.ion].init_ext_concentration = *p.scale.get_scalar()*p.value; } else if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.ion_data[p.ion].init_reversal_potential = *p.value.get_scalar(); + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + defaults_.ion_data[p.ion].init_reversal_potential = *p.scale.get_scalar()*p.value; } else if constexpr (std::is_same_v) { defaults_.reversal_potential_method[p.ion] = p.method; @@ -167,8 +167,9 @@ decor& decor::set_default(defaultable what) { defaults_.discretization = std::forward(p); } else if constexpr (std::is_same_v) { - if (p.value.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; - defaults_.ion_data[p.ion].diffusivity = p.value.get_scalar(); + if (p.scale.type() != iexpr_type::scalar) throw cable_cell_error{"Default values cannot have a scale."}; + auto s = p.scale.get_scalar(); + defaults_.ion_data[p.ion].diffusivity = s ? std::optional{*s*p.value} : s; } }, what); diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp index 5baa95c036..2e667031f3 100644 --- a/arbor/fvm_layout.cpp +++ b/arbor/fvm_layout.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -20,17 +19,11 @@ #include "fvm_layout.hpp" #include "threading/threading.hpp" #include "util/maputil.hpp" -#include "util/meta.hpp" -#include "util/partition.hpp" #include "util/piecewise.hpp" #include "util/pw_over_cable.hpp" #include "util/rangeutil.hpp" -#include "util/transform.hpp" -#include "util/unique.hpp" #include "util/strprintf.hpp" -#include - namespace arb { using util::assign; @@ -275,16 +268,16 @@ fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global double dflt_resistivity = *(dflt.axial_resistivity | global_dflt.axial_resistivity); double dflt_capacitance = *(dflt.membrane_capacitance | global_dflt.membrane_capacitance); - double dflt_potential = *(dflt.init_membrane_potential | global_dflt.init_membrane_potential); + double dflt_potential = *(dflt.init_membrane_potential | global_dflt.init_membrane_potential); double dflt_temperature = *(dflt.temperature_K | global_dflt.temperature_K); - const auto& assignments = cell.region_assignments(); - const auto& resistivity = assignments.get(); - const auto& capacitance = assignments.get(); - const auto& potential = assignments.get(); - const auto& temperature = assignments.get(); - const auto& diffusivity = assignments.get(); - const auto& provider = cell.provider(); + const auto& assignments = cell.region_assignments(); + const auto& resistivity = assignments.get(); + const auto& capacitance = assignments.get(); + const auto& potential = assignments.get(); + const auto& temperature_K = assignments.get(); + const auto& diffusivity = assignments.get(); + const auto& provider = cell.provider(); struct inv_diff { iexpr value; @@ -310,8 +303,9 @@ fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global auto diffusive = std::any_of(data.begin(), data.end(), [](const auto& kv) { - const auto& v = kv.second.value.get_scalar(); - return !v || *v != 0.0 || *v == *v; + const auto& [k, v] = kv; + auto s = v.scale.get_scalar(); + return !s || *s*v.value != 0.0; }); if (diffusive) { // Provide a (non-sensical) default. @@ -337,7 +331,7 @@ fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global for (msize_t i = 0; i double { auto ie = thingify(par.value, provider); auto sc = ie->eval(provider, cable); if (def <= 0.0 || std::isnan(def)) { @@ -360,9 +354,9 @@ fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global for (msize_t i = 0; ieval(provider, cable); + const axial_resistivity& par) -> double { + auto ie = thingify(par.scale, provider); + auto sc = par.value*ie->eval(provider, cable); return sc; }; ax_res_0.emplace_back(pw_over_cable(resistivity, cable, dflt_resistivity, scale_param)); @@ -419,15 +413,15 @@ fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global double cv_length = 0; for (mcable cable: cv_cables) { - auto scale_param = [&](const auto&, const auto& par) { - auto ie = thingify(par.value, provider); - auto sc = ie->eval(provider, cable); + auto scale_param = [&](const auto&, const auto& par) -> double { + auto ie = thingify(par.scale, provider); + auto sc = par.value*ie->eval(provider, cable); return sc; }; - auto pw_capacitance = pw_over_cable(capacitance, cable, dflt_capacitance, scale_param); - auto pw_potential = pw_over_cable(potential, cable, dflt_potential, scale_param); - auto pw_temperature = pw_over_cable(temperature, cable, dflt_temperature, scale_param); + auto pw_capacitance = pw_over_cable(capacitance, cable, dflt_capacitance, scale_param); + auto pw_potential = pw_over_cable(potential, cable, dflt_potential, scale_param); + auto pw_temperature = pw_over_cable(temperature_K, cable, dflt_temperature, scale_param); D.cv_area[i] += embedding.integrate_area(cable); D.cv_capacitance[i] += embedding.integrate_area(cable.branch, pw_capacitance); @@ -544,7 +538,7 @@ bool cables_intersect_location(Seq&& cables, const mlocation& x) { auto eqr = std::equal_range(begin(cables), end(cables), x.branch, cmp_branch{}); return util::any_of(util::make_range(eqr), - [&x](const mcable& c) { return c.prox_pos<=x.pos && x.pos<=c.dist_pos; }); + [&x](const mcable& c) { return c.prox_pos<=x.pos && x.pos<=c.dist_pos; }); } voltage_reference_pair fvm_voltage_reference_points(const morphology& morph, const cv_geometry& geom, arb_size_type cell_idx, const mlocation& site) { @@ -1312,9 +1306,9 @@ make_ion_config(fvm_ion_map build_data, for (const mcable& cable: data.D.geometry.cables(cv)) { auto scale_param = [&](const auto&, - const auto& par) { - auto ie = thingify(par.value, provider); - auto sc = ie->eval(provider, cable); + const auto& par) -> double { + auto ie = thingify(par.scale, provider); + auto sc = par.value*ie->eval(provider, cable); return sc; }; diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp index e119508a49..8295823160 100644 --- a/arbor/include/arbor/cable_cell.hpp +++ b/arbor/include/arbor/cable_cell.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -239,7 +238,7 @@ using location_assignment = using cable_cell_region_map = static_typed_map; using cable_cell_location_map = static_typed_map #include #include +#include namespace arb { +namespace U = arb::units; + // Specialized arbor exception for errors in cell building. struct ARB_SYMBOL_VISIBLE cable_cell_error: arbor_exception { cable_cell_error(const std::string& what): - arbor_exception("cable_cell: "+what) {} + arbor_exception("cable_cell: " + what) {} }; // Ion inital concentration and reversal potential @@ -30,28 +33,43 @@ struct ARB_SYMBOL_VISIBLE cable_cell_error: arbor_exception { // separately (see below). struct cable_cell_ion_data { - std::optional init_int_concentration; - std::optional init_ext_concentration; - std::optional init_reversal_potential; - std::optional diffusivity; + std::optional init_int_concentration; // mM + std::optional init_ext_concentration; // mM + std::optional init_reversal_potential; // mV + std::optional diffusivity; // m²/s }; -// Clamp current is described by a sine wave with amplitude governed by a -// piecewise linear envelope. A frequency of zero indicates that the current is -// simply that given by the envelope. -// -// The envelope is given by a series of envelope_point values: -// * The time points must be monotonically increasing. -// * Onset and initial amplitude is given by the first point. -// * The amplitude for time after the last time point is that of the last -// amplitude point; an explicit zero amplitude point must be provided if the -// envelope is intended to have finite support. -// -// Periodic envelopes are not supported, but may well be a feature worth -// considering in the future. - +/** + * Current clamp; described by a sine wave with amplitude governed by a + * piecewise linear envelope. A frequency of zero indicates that the current is + * simply that given by the envelope. + * + * The envelope is given by a series of envelope_point values: + * * The time points must be monotonically increasing. + * * Onset and initial amplitude is given by the first point. + * * The amplitude for time after the last time point is that of the last + * amplitude point; an explicit zero amplitude point must be provided if the + * envelope is intended to have finite support. + * + * Periodic envelopes are not supported, but may well be a feature worth + * considering in the future. + */ struct ARB_SYMBOL_VISIBLE i_clamp { struct envelope_point { + /** + * Current at point in time + * + * @param t, must be convertible to time + * @param amplitude must be convertible to current + */ + envelope_point(const U::quantity& time, + const U::quantity& current): + t(time.value_as(U::ms)), + amplitude(current.value_as(U::nA)) { + + if (std::isnan(t)) throw std::domain_error{"Time must be finite and convertible to ms."}; + if (std::isnan(amplitude)) throw std::domain_error{"Amplitude must be finite and convertible to nA."}; + } double t; // [ms] double amplitude; // [nA] }; @@ -64,69 +82,147 @@ struct ARB_SYMBOL_VISIBLE i_clamp { // a trivial stimulus, providing no current at all. i_clamp() = default; - // The simple constructor describes a constant amplitude stimulus starting from t=0. - explicit i_clamp(double amplitude, double frequency = 0, double phase = 0): - envelope({{0., amplitude}}), - frequency(frequency), - phase(phase) + /** + * Constant amplitude stimulus starting at t = 0. + * + * @param amplitude must be convertible to current + * @param frequency, must be convertible to frequency; gives a sine current if not zero + * @param frequency, must be convertible to radians, phase shift of sine. + */ + + explicit i_clamp(const U::quantity& amplitude, + const U::quantity& frequency = 0*U::kHz, + const U::quantity& phase = 0*U::rad): + i_clamp{{{0.0*U::ms, amplitude}}, frequency, phase} {} // Describe a stimulus by envelope and frequency. - explicit i_clamp(std::vector envelope, double frequency = 0, double phase = 0): + explicit i_clamp(std::vector envelope, + const U::quantity& f = 0*U::kHz, + const U::quantity& phi = 0*U::rad): envelope(std::move(envelope)), - frequency(frequency), - phase(phase) - {} + frequency(f.value_as(U::kHz)), + phase(phi.value_as(U::rad)) + { + if (std::isnan(frequency)) throw std::domain_error{"Frequency must be finite and convertible to kHz."}; + if (std::isnan(phase)) throw std::domain_error{"Phase must be finite and convertible to rad."}; + } // A 'box' stimulus with fixed onset time, duration, and constant amplitude. - static i_clamp box(double onset, double duration, double amplitude, double frequency = 0, double phase = 0) { - return i_clamp({{onset, amplitude}, {onset+duration, amplitude}, {onset+duration, 0.}}, frequency, phase); + static i_clamp box(const U::quantity& onset, + const U::quantity& duration, + const U::quantity& amplitude, + const U::quantity& frequency = 0*U::kHz, + const U::quantity& phase = 0*U::rad) { + return i_clamp({{onset, amplitude}, {onset+duration, amplitude}, {onset+duration, 0.*U::nA}}, + frequency, + phase); } - }; // Threshold detector description. struct ARB_SYMBOL_VISIBLE threshold_detector { - double threshold; + threshold_detector(const U::quantity& m): threshold(m.value_as(U::mV)) { + if (std::isnan(threshold)) throw std::domain_error{"Threshold must be finite and in [mV]."}; + } + static threshold_detector from_raw_millivolts(double v) { return {v*U::mV}; } + double threshold; // [mV] }; // Setter types for painting physical and ion parameters or setting // cell-wide default: struct ARB_SYMBOL_VISIBLE init_membrane_potential { - iexpr value = NAN; // [mV] + double value = NAN; // [mV] + iexpr scale = 1; // [1] + + init_membrane_potential() = default; + init_membrane_potential(const U::quantity& m, iexpr scale=1): + value(m.value_as(U::mV)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [mV]."}; + } }; -struct ARB_SYMBOL_VISIBLE temperature_K { - iexpr value = NAN; // [K] + +struct ARB_SYMBOL_VISIBLE temperature { + double value = NAN; // [K] + iexpr scale = 1; // [1] + + temperature() = default; + temperature(const U::quantity& m, iexpr scale=1): + value(m.value_as(U::Kelvin)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [K]."}; + } }; struct ARB_SYMBOL_VISIBLE axial_resistivity { - iexpr value = NAN; // [Ω·cm] + double value = NAN; // [Ω·cm] + iexpr scale = 1; // [1] + + axial_resistivity() = default; + axial_resistivity(const U::quantity& m, iexpr scale=1): + value(m.value_as(U::cm*U::Ohm)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [Ω·cm]."}; + } }; struct ARB_SYMBOL_VISIBLE membrane_capacitance { - iexpr value = NAN; // [F/m²] + double value = NAN; // [F/m²] + iexpr scale = 1; // [1] + + membrane_capacitance() = default; + membrane_capacitance(const U::quantity& m, iexpr scale=1): + value(m.value_as(U::F/U::m2)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [F/m²]."}; + } }; struct ARB_SYMBOL_VISIBLE init_int_concentration { std::string ion = ""; - iexpr value = NAN; // [mM] + double value = NAN; // [mM] + iexpr scale = 1; // [1] + + init_int_concentration() = default; + init_int_concentration(const std::string& ion, const U::quantity& m, iexpr scale=1): + ion{ion}, value(m.value_as(U::mM)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [mM]."}; + } }; struct ARB_SYMBOL_VISIBLE ion_diffusivity { std::string ion = ""; - iexpr value = NAN; // [m^2/s] + double value = NAN; // [m²/s] + iexpr scale = 1; // [1] + + ion_diffusivity() = default; + ion_diffusivity(const std::string& ion, const U::quantity& m, iexpr scale=1): + ion{ion}, value(m.value_as(U::m2/U::s)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [m²/s]."}; + } }; struct ARB_SYMBOL_VISIBLE init_ext_concentration { std::string ion = ""; - iexpr value = NAN; // [mM] + double value = NAN; // [mM] + iexpr scale = 1; // [1] + + init_ext_concentration() = default; + init_ext_concentration(const std::string& ion, const U::quantity& m, iexpr scale=1): + ion{ion}, value(m.value_as(U::mM)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [mM]."}; + } }; struct ARB_SYMBOL_VISIBLE init_reversal_potential { std::string ion = ""; - iexpr value = NAN; // [mV] + double value = NAN; // [mV] + iexpr scale = 1; // [1] + + init_reversal_potential() = default; + init_reversal_potential(const std::string& ion, const U::quantity& m, iexpr scale=1): + ion{ion}, value(m.value_as(U::mV)), scale{scale} { + if (std::isnan(value)) throw std::domain_error{"Value must be finite and in [mV]."}; + } }; // Mechanism description, viz. mechanism name and @@ -150,8 +246,12 @@ struct ARB_SYMBOL_VISIBLE mechanism_desc { }; // implicit - mechanism_desc(std::string name): name_(std::move(name)) {} - mechanism_desc(const char* name): name_(name) {} + mechanism_desc(std::string name): name_(std::move(name)) { + if (name_.empty()) throw cable_cell_error("mechanism_desc: null name"); + } + mechanism_desc(const char* name): name_(name) { + if (name_.empty()) throw cable_cell_error("mechanism_desc: null name"); + } mechanism_desc() = default; mechanism_desc(const mechanism_desc&) = default; @@ -255,7 +355,7 @@ struct ARB_SYMBOL_VISIBLE scaled_mechanism { using paintable = std::variant -#include -#include -#include -#include #include #include @@ -101,34 +96,29 @@ struct event_generator { inline event_generator empty_generator( cell_local_label_type target, - float weight) -{ + float weight) { return event_generator(std::move(target), weight, schedule()); } // Generate events at integer multiples of dt that lie between tstart and tstop. -inline event_generator regular_generator( - cell_local_label_type target, - float weight, - time_type tstart, - time_type dt, - time_type tstop=terminal_time) -{ +inline event_generator regular_generator(cell_local_label_type target, + float weight, + const units::quantity& tstart, + const units::quantity& dt, + const units::quantity& tstop=terminal_time*units::ms) { return event_generator(std::move(target), weight, regular_schedule(tstart, dt, tstop)); } -template -inline event_generator poisson_generator( - cell_local_label_type target, - float weight, - time_type tstart, - time_type rate_kHz, - const RNG& rng, - time_type tstop=terminal_time) -{ - return event_generator(std::move(target), weight, poisson_schedule(tstart, rate_kHz, rng, tstop)); +inline event_generator poisson_generator(cell_local_label_type target, + float weight, + const units::quantity& tstart, + const units::quantity& rate_kHz, + seed_type seed = default_seed, + const units::quantity& tstop=terminal_time*units::ms) { + // TODO(TH) handle seed + return event_generator(std::move(target), weight, poisson_schedule(tstart, rate_kHz, seed, tstop)); } @@ -137,10 +127,16 @@ inline event_generator poisson_generator( template inline event_generator explicit_generator(cell_local_label_type target, float weight, - const S& s) -{ + const S& s) { return event_generator(std::move(target), weight, explicit_schedule(s)); } +template inline +event_generator explicit_generator_from_milliseconds(cell_local_label_type target, + float weight, + const S& s) { + return event_generator(std::move(target), weight, explicit_schedule_from_milliseconds(s)); +} + } // namespace arb diff --git a/arbor/include/arbor/iexpr.hpp b/arbor/include/arbor/iexpr.hpp index 15d8452795..c0eea2de90 100644 --- a/arbor/include/arbor/iexpr.hpp +++ b/arbor/include/arbor/iexpr.hpp @@ -74,14 +74,14 @@ struct ARB_SYMBOL_VISIBLE iexpr { static iexpr distal_distance(region reg); static iexpr interpolation(double prox_value, - locset prox_list, - double dist_value, - locset dist_list); + locset prox_list, + double dist_value, + locset dist_list); static iexpr interpolation(double prox_value, - region prox_list, - double dist_value, - region dist_list); + region prox_list, + double dist_value, + region dist_list); static iexpr radius(double scale); diff --git a/arbor/include/arbor/lif_cell.hpp b/arbor/include/arbor/lif_cell.hpp index fedee7a256..e9d5c2fdf5 100644 --- a/arbor/include/arbor/lif_cell.hpp +++ b/arbor/include/arbor/lif_cell.hpp @@ -3,27 +3,30 @@ #include #include #include +#include namespace arb { +namespace U = arb::units; +using namespace U::literals; + // Model parameters of leaky integrate and fire neuron model. struct ARB_SYMBOL_VISIBLE lif_cell { cell_tag_type source; // Label of source. cell_tag_type target; // Label of target. // Neuronal parameters. - double tau_m = 10; // Membrane potential decaying constant [ms]. - double V_th = 10; // Firing threshold [mV]. - double C_m = 20; // Membrane capacitance [pF]. - double E_L = 0; // Resting potential [mV]. - double E_R = E_L; // Reset potential [mV]. - double V_m = E_L; // Initial value of the Membrane potential [mV]. - double t_ref = 2; // Refractory period [ms]. + U::quantity tau_m = 10_ms; // Membrane potential decaying constant [ms]. + U::quantity V_th = 10_mV; // Firing threshold [mV]. + U::quantity C_m = 20_pF; // Membrane capacitance [pF]. + U::quantity E_L = 0_mV; // Resting potential [mV]. + U::quantity E_R = 0_mV; // Reset potential [mV]. + U::quantity V_m = 0_mV; // Initial value of the Membrane potential [mV]. + U::quantity t_ref = 2_ms; // Refractory period [ms]. lif_cell() = default; lif_cell(cell_tag_type source, cell_tag_type target): source(std::move(source)), target(std::move(target)) {} - ARB_SERDES_ENABLE(lif_cell, source, target, tau_m, V_th, C_m, E_L, E_R, V_m, t_ref); }; // LIF probe metadata, to be passed to sampler callbacks. Intentionally left blank. diff --git a/arbor/include/arbor/morph/isometry.hpp b/arbor/include/arbor/morph/isometry.hpp index cb06ee28dd..3c10bd1c51 100644 --- a/arbor/include/arbor/morph/isometry.hpp +++ b/arbor/include/arbor/morph/isometry.hpp @@ -7,6 +7,7 @@ namespace arb { // Represent a 3-d isometry as a rotation (via quaterion q_) // and a subsequent translation (tx_, ty_, tz_). struct ARB_ARBOR_API isometry { + using quaternion = arb::math::quaternion; isometry() = default; @@ -28,7 +29,6 @@ struct ARB_ARBOR_API isometry { } private: - using quaternion = arb::math::quaternion; quaternion q_{1, 0, 0, 0}; double tx_ = 0, ty_ = 0, tz_ = 0; diff --git a/arbor/include/arbor/recipe.hpp b/arbor/include/arbor/recipe.hpp index 797490f320..dbcba5fcde 100644 --- a/arbor/include/arbor/recipe.hpp +++ b/arbor/include/arbor/recipe.hpp @@ -11,6 +11,8 @@ namespace arb { +namespace U = arb::units; + struct probe_info { cell_tag_type tag; @@ -40,7 +42,6 @@ struct probe_info { // are notionally described in terms of external cell identifiers instead // of internal gids, but we are not making the distinction between the // two in the current code. These two types could well be merged. - template struct cell_connection_base { // Connection end-points are represented by pairs @@ -48,11 +49,14 @@ struct cell_connection_base { L source; cell_local_label_type target; - float weight; - float delay; + float weight; // [()] + float delay; // [ms] - cell_connection_base(L src, cell_local_label_type dst, float w, float d): - source(std::move(src)), target(std::move(dst)), weight(w), delay(d) {} + cell_connection_base(L src, cell_local_label_type dst, float w, const U::quantity& d): + source(std::move(src)), target(std::move(dst)), weight(w), delay(d.value_as(U::ms)) { + if (std::isnan(weight)) throw std::out_of_range("Connection weight must be finite."); + if (std::isnan(delay) || delay < 0) throw std::out_of_range("Connection delay must be non-negative and infinite in units of [ms]."); + } }; using cell_connection = cell_connection_base; @@ -61,10 +65,12 @@ using ext_cell_connection = cell_connection_base; struct gap_junction_connection { cell_global_label_type peer; cell_local_label_type local; - double weight; //unit-less + double weight; // [()] gap_junction_connection(cell_global_label_type peer, cell_local_label_type local, double g): - peer(std::move(peer)), local(std::move(local)), weight(g) {} + peer(std::move(peer)), local(std::move(local)), weight(g) { + if (std::isnan(weight)) throw std::out_of_range("Gap junction weight must be finite."); + } }; struct ARB_ARBOR_API has_gap_junctions { diff --git a/arbor/include/arbor/schedule.hpp b/arbor/include/arbor/schedule.hpp index da3921bc36..8856cceac2 100644 --- a/arbor/include/arbor/schedule.hpp +++ b/arbor/include/arbor/schedule.hpp @@ -1,190 +1,39 @@ #pragma once #include -#include #include -#include #include #include #include +#include #include #include #include #include #include +#include // Time schedules for probe–sampler associations. namespace arb { +using engine_type = std::mt19937_64; +using seed_type = std::remove_cv_t; + +constexpr static auto default_seed = engine_type::default_seed; + using time_event_span = std::pair; inline time_event_span as_time_event_span(const std::vector& v) { - return {v.data(), v.data()+v.size()}; + return {v.data(), v.data() + v.size()}; } -// Common schedules - -// Default schedule is empty. - -struct ARB_ARBOR_API empty_schedule { - void reset() {} - time_event_span events(time_type t0, time_type t1) { - static time_type no_time; - return {&no_time, &no_time}; - } -}; - -// Schedule at k·dt for integral k≥0 within the interval [t0, t1). -struct ARB_ARBOR_API regular_schedule_impl { - explicit regular_schedule_impl(time_type t0, time_type dt, time_type t1): - t0_(t0), t1_(t1), dt_(dt), oodt_(1./dt) - { - if (t0_<0) t0_ = 0; - }; - - void reset() {} - time_event_span events(time_type t0, time_type t1); - - ARB_SERDES_ENABLE(regular_schedule_impl, t0_, t1_, dt_, oodt_); - -private: - time_type t0_, t1_, dt_; - time_type oodt_; - - std::vector times_; -}; - -// Schedule at times given explicitly via a provided sorted sequence. -struct ARB_ARBOR_API explicit_schedule_impl { - explicit_schedule_impl(const explicit_schedule_impl&) = default; - explicit_schedule_impl(explicit_schedule_impl&&) = default; - - template - explicit explicit_schedule_impl(const Seq& seq): - start_index_(0) - { - using std::begin; - using std::end; - - times_.assign(begin(seq), end(seq)); - arb_assert(std::is_sorted(times_.begin(), times_.end())); - } - - void reset() { - start_index_ = 0; - } - - time_event_span events(time_type t0, time_type t1); - - ARB_SERDES_ENABLE(explicit_schedule_impl, start_index_, times_); - -private: - std::ptrdiff_t start_index_; - std::vector times_; -}; - -// Schedule at Poisson point process with rate 1/mean_dt, -// restricted to non-negative times. -template -class poisson_schedule_impl { -public: - poisson_schedule_impl(time_type tstart, time_type rate_kHz, const RandomNumberEngine& rng, time_type tstop): - tstart_(tstart), exp_(rate_kHz), rng_(rng), reset_state_(rng), next_(tstart), tstop_(tstop) - { - arb_assert(tstart_>=0); - arb_assert(tstart_ <= tstop_); - step(); - } - - void reset() { - rng_ = reset_state_; - next_ = tstart_; - step(); - } - - time_event_span events(time_type t0, time_type t1) { - // if we start after the maximal allowed time, we have nothing to do - if (t0 >= tstop_) return {}; - - // restrict by maximal allowed time - t1 = std::min(t1, tstop_); - - times_.clear(); - - while (next_ exp_; - RandomNumberEngine rng_; - RandomNumberEngine reset_state_; - time_type next_; - std::vector times_; - time_type tstop_; -}; - // Type erased wrapper // A schedule describes a sequence of time values used for sampling. Schedules // are queried monotonically in time: if two method calls `events(t0, t1)` // and `events(t2, t3)` are made without an intervening call to `reset()`, // then 0 ≤ _t0_ ≤ _t1_ ≤ _t2_ ≤ _t3_. - -template -void serialize(arb::serializer& s, const K& k, const arb::explicit_schedule_impl&); -template -void deserialize(arb::serializer& s, const K& k, arb::explicit_schedule_impl&); - -template -void serialize(arb::serializer& s, const K& k, const arb::regular_schedule_impl&); -template -void deserialize(arb::serializer& s, const K& k, arb::regular_schedule_impl&); - -template -ARB_ARBOR_API void serialize(::arb::serializer& ser, - const K& k, - const arb::empty_schedule& t) { - ser.begin_write_map(::arb::to_serdes_key(k)); - ser.end_write_map(); -} - -template -ARB_ARBOR_API void deserialize(::arb::serializer& ser, - const K& k, - arb::empty_schedule& t) { - ser.begin_read_map(::arb::to_serdes_key(k)); - ser.end_read_map(); -} - -// These are custom to get the reset in. -template -void serialize(::arb::serializer& ser, const K& k, const ::arb::poisson_schedule_impl& t) { - ser.begin_write_map(arb::to_serdes_key(k)); - ARB_SERDES_WRITE(tstart_); - ARB_SERDES_WRITE(tstop_); - ser.end_write_map(); -} - -template -void deserialize(::arb::serializer& ser, const K& k, ::arb::poisson_schedule_impl& t) { - ser.begin_read_map(arb::to_serdes_key(k)); - ARB_SERDES_READ(tstart_); - ARB_SERDES_READ(tstop_); - ser.end_read_map(); - t.reset(); -} - -class schedule { -public: +struct ARB_ARBOR_API schedule { schedule(); template , schedule>>> @@ -206,12 +55,13 @@ class schedule { return *this; } - time_event_span events(time_type t0, time_type t1) { - return impl_->events(t0, t1); - } + time_event_span events(time_type t0, time_type t1) { return impl_->events(t0, t1); } void reset() { impl_->reset(); } + // Discard the next n events. Used in tests. + auto discard(std::size_t n) { return impl_->discard(n); } + template friend ARB_ARBOR_API void serialize(serializer& s, const K& k, const schedule& v) { v.impl_->t_serialize(s, to_serdes_key(k)); } template @@ -219,10 +69,10 @@ class schedule { )); } private: - struct interface { virtual time_event_span events(time_type t0, time_type t1) = 0; virtual void reset() = 0; + virtual void discard(std::size_t n) = 0; virtual std::unique_ptr clone() = 0; virtual ~interface() {} virtual void t_serialize(serializer&, const std::string&k) const = 0; @@ -237,46 +87,37 @@ class schedule { struct wrap: interface { explicit wrap(const Impl& impl): wrapped(impl) {} explicit wrap(Impl&& impl): wrapped(std::move(impl)) {} - virtual time_event_span events(time_type t0, time_type t1) override { return wrapped.events(t0, t1); } - virtual void reset() override { wrapped.reset(); } - virtual iface_ptr clone() override { return std::make_unique>(wrapped); } - virtual void t_serialize(serializer& s, const std::string& k) const override { serialize(s, k, wrapped); } - virtual void t_deserialize(serializer& s, const std::string& k) override { deserialize(s, k, wrapped); } + time_event_span events(time_type t0, time_type t1) override { return wrapped.events(t0, t1); } + void reset() override { wrapped.reset(); } + void discard(std::size_t n) override { return wrapped.discard(n); } + iface_ptr clone() override { return std::make_unique>(wrapped); } + void t_serialize(serializer& s, const std::string& k) const override { wrapped.t_serialize(s, k); } + void t_deserialize(serializer& s, const std::string& k) override { wrapped.t_deserialize(s, k); } Impl wrapped; }; }; // Constructors -inline schedule::schedule(): schedule(empty_schedule{}) {} -inline schedule regular_schedule(time_type t0, - time_type dt, - time_type t1 = std::numeric_limits::max()) { - return schedule(regular_schedule_impl(t0, dt, t1)); -} +/// Regular schedule with start `t0`, interval `dt`, and optional end `t1`. +schedule ARB_ARBOR_API regular_schedule(const units::quantity& t0, + const units::quantity& dt, + const units::quantity& t1 = std::numeric_limits::max()*units::ms); -inline schedule regular_schedule(time_type dt) { - return regular_schedule(0, dt); -} +/// Regular schedule with interval `dt`. +schedule ARB_ARBOR_API regular_schedule(const units::quantity& dt); -template -inline schedule explicit_schedule(const Seq& seq) { - return schedule(explicit_schedule_impl(seq)); -} +schedule ARB_ARBOR_API explicit_schedule(const std::vector& seq); +schedule ARB_ARBOR_API explicit_schedule_from_milliseconds(const std::vector& seq); -inline schedule explicit_schedule(const std::initializer_list& seq) { - return schedule(explicit_schedule_impl(seq)); -} +schedule ARB_ARBOR_API poisson_schedule(const units::quantity& tstart, + const units::quantity& rate, + seed_type seed = default_seed, + const units::quantity& tstop=terminal_time*units::ms); -template -inline schedule poisson_schedule(time_type rate_kHz, const RandomNumberEngine& rng, time_type tstop=terminal_time) { - return schedule(poisson_schedule_impl(0., rate_kHz, rng, tstop)); -} - -template -inline schedule poisson_schedule(time_type tstart, time_type rate_kHz, const RandomNumberEngine& rng, time_type tstop=terminal_time) { - return schedule(poisson_schedule_impl(tstart, rate_kHz, rng, tstop)); -} +schedule ARB_ARBOR_API poisson_schedule(const units::quantity& rate, + seed_type seed = default_seed, + const units::quantity& tstop=terminal_time*units::ms); } // namespace arb diff --git a/arbor/include/arbor/simulation.hpp b/arbor/include/arbor/simulation.hpp index c6cf845d9c..b5de7b4a5c 100644 --- a/arbor/include/arbor/simulation.hpp +++ b/arbor/include/arbor/simulation.hpp @@ -1,8 +1,6 @@ #pragma once -#include #include -#include #include #include @@ -50,7 +48,7 @@ class ARB_ARBOR_API simulation { void reset(); - time_type run(time_type tfinal, time_type dt); + time_type run(const units::quantity& tfinal, const units::quantity& dt); // Minimum delay in network τ time_type min_delay(); diff --git a/arbor/include/arbor/spike_event.hpp b/arbor/include/arbor/spike_event.hpp index d3ffdb5024..d44cd7cd44 100644 --- a/arbor/include/arbor/spike_event.hpp +++ b/arbor/include/arbor/spike_event.hpp @@ -3,7 +3,6 @@ #include #include -#include #include #include diff --git a/arbor/include/arbor/units.hpp b/arbor/include/arbor/units.hpp new file mode 100644 index 0000000000..c361209a56 --- /dev/null +++ b/arbor/include/arbor/units.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include + +namespace arb::units { + +using quantity = ::units::precise_measurement; + +// Allow unary minus on quantities. Seemingly doesn't catch literals such as -10_mV +inline quantity operator-(const quantity& q) { return (-1*q); } + +using unit = ::units::precise_unit; +using ::units::to_string; +using ::units::unit_cast_from_string; + + +using ::units::precise::pico; +using ::units::precise::nano; +using ::units::precise::micro; +using ::units::precise::milli; +using ::units::precise::centi; +using ::units::precise::deci; + +using ::units::precise::kilo; +using ::units::precise::mega; +using ::units::precise::giga; + +using ::units::precise::deg; +using ::units::precise::rad; + +constexpr inline auto Celsius = ::units::precise::degC; +constexpr inline auto Kelvin = ::units::precise::Kelvin; + +using ::units::precise::s; +using ::units::precise::ms; +constexpr inline auto us = micro * s; +using ::units::precise::ns; + +using ::units::precise::m; +constexpr inline auto cm = centi * m; +constexpr inline auto mm = milli * m; +constexpr inline auto um = micro * m; +constexpr inline auto nm = nano * m; + +constexpr inline auto Ohm = ::units::precise::ohm; +constexpr inline auto kOhm = kilo * Ohm; +constexpr inline auto MOhm = mega * Ohm; + +// Siemens +constexpr inline auto S = Ohm.pow(-1); +constexpr inline auto mS = milli * S; +constexpr inline auto uS = micro * S; + +constexpr inline auto A = ::units::precise::Ampere; +constexpr inline auto mA = milli * A; +constexpr inline auto uA = micro * A; +constexpr inline auto nA = nano * A; +constexpr inline auto pA = pico * A; + +constexpr inline auto V = ::units::precise::volt; +constexpr inline auto mV = milli * V; + +constexpr inline auto Hz = ::units::precise::second.pow(-1); +constexpr inline auto kHz = kilo * Hz; + +constexpr inline auto F = ::units::precise::farad; +constexpr inline auto mF = milli * F; +constexpr inline auto uF = micro * F; +constexpr inline auto nF = nano * F; +constexpr inline auto pF = pico * F; + +constexpr inline auto m2 = m*m; +constexpr inline auto cm2 = cm*cm; +constexpr inline auto mm2 = mm*mm; +constexpr inline auto um2 = um*um; +constexpr inline auto nm2 = nm*nm; + +constexpr inline auto nil = ::units::precise::one; + +// Coulomb +constexpr inline auto C = ::units::precise::coulomb; + +// mol and molarity +using ::units::precise::mol; +constexpr inline auto M = mol / m.pow(3); +constexpr inline auto mM = milli * M; + +using ::units::is_valid; + +namespace literals { +constexpr inline quantity operator ""_s(long double v) { return v*s; } +constexpr inline quantity operator ""_ms(long double v) { return v*ms; } +constexpr inline quantity operator ""_us(long double v) { return v*us; } +constexpr inline quantity operator ""_ns(long double v) { return v*ns; } + +constexpr inline quantity operator ""_m(long double v) { return v*m; } +constexpr inline quantity operator ""_cm(long double v) { return v*cm; } +constexpr inline quantity operator ""_mm(long double v) { return v*mm; } +constexpr inline quantity operator ""_um(long double v) { return v*um; } +constexpr inline quantity operator ""_nm(long double v) { return v*nm; } + +constexpr inline quantity operator ""_m2(long double v) { return v*m2; } +constexpr inline quantity operator ""_cm2(long double v) { return v*cm2; } +constexpr inline quantity operator ""_mm2(long double v) { return v*mm2; } +constexpr inline quantity operator ""_um2(long double v) { return v*um2; } +constexpr inline quantity operator ""_nm2(long double v) { return v*nm2; } + +constexpr inline quantity operator ""_Ohm(long double v) { return v*Ohm; } +constexpr inline quantity operator ""_kOhm(long double v) { return v*kOhm; } +constexpr inline quantity operator ""_MOhm(long double v) { return v*MOhm; } + +constexpr inline quantity operator ""_S(long double v) { return v*S; } +constexpr inline quantity operator ""_mS(long double v) { return v*mS; } +constexpr inline quantity operator ""_uS(long double v) { return v*uS; } + +constexpr inline quantity operator ""_A(long double v) { return v*A; } +constexpr inline quantity operator ""_mA(long double v) { return v*mA; } +constexpr inline quantity operator ""_uA(long double v) { return v*uA; } +constexpr inline quantity operator ""_nA(long double v) { return v*nA; } +constexpr inline quantity operator ""_pA(long double v) { return v*pA; } + +constexpr inline quantity operator ""_V(long double v) { return v*V; } +constexpr inline quantity operator ""_mV(long double v) { return v*mV; } + +constexpr inline quantity operator ""_Hz(long double v) { return v*Hz; } +constexpr inline quantity operator ""_kHz(long double v) { return v*kHz; } + +constexpr inline quantity operator ""_F(long double v) { return v*F; } +constexpr inline quantity operator ""_mF(long double v) { return v*mF; } +constexpr inline quantity operator ""_uF(long double v) { return v*uF; } +constexpr inline quantity operator ""_nF(long double v) { return v*nF; } +constexpr inline quantity operator ""_pF(long double v) { return v*pF; } + +constexpr inline quantity operator ""_mol(long double v) { return v*mol; } +constexpr inline quantity operator ""_M(long double v) { return v*M; } +constexpr inline quantity operator ""_mM(long double v) { return v*mM; } + +constexpr inline quantity operator ""_C(long double v) { return v*C; } + +constexpr inline quantity operator ""_s(unsigned long long v) { return v*s; } +constexpr inline quantity operator ""_ms(unsigned long long v) { return v*ms; } +constexpr inline quantity operator ""_us(unsigned long long v) { return v*us; } +constexpr inline quantity operator ""_ns(unsigned long long v) { return v*ns; } + +constexpr inline quantity operator ""_m(unsigned long long v) { return v*m; } +constexpr inline quantity operator ""_cm(unsigned long long v) { return v*cm; } +constexpr inline quantity operator ""_mm(unsigned long long v) { return v*mm; } +constexpr inline quantity operator ""_um(unsigned long long v) { return v*um; } +constexpr inline quantity operator ""_nm(unsigned long long v) { return v*nm; } + +constexpr inline quantity operator ""_m2(unsigned long long v) { return v*m2; } +constexpr inline quantity operator ""_cm2(unsigned long long v) { return v*cm2; } +constexpr inline quantity operator ""_mm2(unsigned long long v) { return v*mm2; } +constexpr inline quantity operator ""_um2(unsigned long long v) { return v*um2; } +constexpr inline quantity operator ""_nm2(unsigned long long v) { return v*nm2; } + +constexpr inline quantity operator ""_Ohm(unsigned long long v) { return v*Ohm; } +constexpr inline quantity operator ""_kOhm(unsigned long long v) { return v*kOhm; } +constexpr inline quantity operator ""_MOhm(unsigned long long v) { return v*MOhm; } + +constexpr inline quantity operator ""_S(unsigned long long v) { return v*S; } +constexpr inline quantity operator ""_mS(unsigned long long v) { return v*mS; } +constexpr inline quantity operator ""_uS(unsigned long long v) { return v*uS; } + +constexpr inline quantity operator ""_A(unsigned long long v) { return v*A; } +constexpr inline quantity operator ""_mA(unsigned long long v) { return v*mA; } +constexpr inline quantity operator ""_uA(unsigned long long v) { return v*uA; } +constexpr inline quantity operator ""_nA(unsigned long long v) { return v*nA; } +constexpr inline quantity operator ""_pA(unsigned long long v) { return v*pA; } + +constexpr inline quantity operator ""_V(unsigned long long v) { return v*V; } +constexpr inline quantity operator ""_mV(unsigned long long v) { return v*mV; } + +constexpr inline quantity operator ""_Hz(unsigned long long v) { return v*Hz; } +constexpr inline quantity operator ""_kHz(unsigned long long v) { return v*kHz; } + +constexpr inline quantity operator ""_F(unsigned long long v) { return v*F; } +constexpr inline quantity operator ""_mF(unsigned long long v) { return v*mF; } +constexpr inline quantity operator ""_uF(unsigned long long v) { return v*uF; } +constexpr inline quantity operator ""_nF(unsigned long long v) { return v*nF; } +constexpr inline quantity operator ""_pF(unsigned long long v) { return v*pF; } + +constexpr inline quantity operator ""_mol(unsigned long long v) { return v*mol; } +constexpr inline quantity operator ""_M(unsigned long long v) { return v*M; } +constexpr inline quantity operator ""_mM(unsigned long long v) { return v*mM; } + +constexpr inline quantity operator ""_C(unsigned long long v) { return v*C; } +} // literals +} // units diff --git a/arbor/lif_cell_group.cpp b/arbor/lif_cell_group.cpp index 6698d0edef..5af84c8874 100644 --- a/arbor/lif_cell_group.cpp +++ b/arbor/lif_cell_group.cpp @@ -18,7 +18,7 @@ lif_cell_group::lif_cell_group(const std::vector& gids, for (auto gid: gids_) { const auto& cell = util::any_cast(rec.get_cell_description(gid)); // set up cell state - cells_.push_back(cell); + cells_.emplace_back(cell); last_time_updated_.push_back(0.0); last_time_sampled_.push_back(-1.0); // tell our caller about this cell's connections @@ -95,7 +95,7 @@ void lif_cell_group::reset() { // produce voltage V_m at t1, given cell state at t0 and no spikes in [t0, t1) static double -lif_decay(const lif_cell& cell, double t0, double t1) { +lif_decay(const lif_lowered_cell& cell, double t0, double t1) { return (cell.V_m - cell.E_L)*exp((t0 - t1)/cell.tau_m) + cell.E_L; } diff --git a/arbor/lif_cell_group.hpp b/arbor/lif_cell_group.hpp index 9572d06bd8..ff922334ad 100644 --- a/arbor/lif_cell_group.hpp +++ b/arbor/lif_cell_group.hpp @@ -16,6 +16,46 @@ namespace arb { +// Model parameters of leaky integrate and fire neuron model. +struct ARB_SYMBOL_VISIBLE lif_lowered_cell { + cell_tag_type source; // Label of source. + cell_tag_type target; // Label of target. + + // Neuronal parameters. + double tau_m = 10; // Membrane potential decaying constant [ms]. + double V_th = 10; // Firing threshold [mV]. + double C_m = 20; // Membrane capacitance [pF]. + double E_L = 0; // Resting potential [mV]. + double E_R = E_L; // Reset potential [mV]. + double V_m = E_L; // Initial value of the Membrane potential [mV]. + double t_ref = 2; // Refractory period [ms]. + + lif_lowered_cell() = default; + lif_lowered_cell(const lif_cell& lif) { + source = lif.source; + target = lif.target; + + tau_m = lif.tau_m.value_as(U::ms); + V_th = lif.V_th.value_as(U::mV); + C_m = lif.C_m.value_as(U::pF); + E_L = lif.E_L.value_as(U::mV); + E_R = lif.E_R.value_as(U::mV); + V_m = lif.V_m.value_as(U::mV); + t_ref = lif.t_ref.value_as(U::ms); + + if (std::isnan(V_th)) throw std::out_of_range("V_th must be finite and in [mV]"); + if (std::isnan(tau_m) || tau_m < 0) throw std::out_of_range("tau_m must be positive, finite, and in [ms]"); + if (std::isnan(C_m) || C_m < 0) throw std::out_of_range("C_m must be positive, finite, and in [pF]"); + if (std::isnan(E_L)) throw std::out_of_range("E_L must be finite and in [mV]"); + if (std::isnan(E_R)) throw std::out_of_range("E_R must be finite and in [mV]"); + if (std::isnan(V_m)) throw std::out_of_range("V_m must be finite and in [mV]"); + if (std::isnan(t_ref) || t_ref < 0) throw std::out_of_range("t_ref must be positive, finite, and in [ms]"); + } + + ARB_SERDES_ENABLE(lif_lowered_cell, source, target, tau_m, V_th, C_m, E_L, E_R, V_m, t_ref); +}; + + struct ARB_ARBOR_API lif_cell_group: public cell_group { lif_cell_group() = default; @@ -59,7 +99,7 @@ struct ARB_ARBOR_API lif_cell_group: public cell_group { std::vector gids_; // Cells that belong to this group. - std::vector cells_; + std::vector cells_; // Spikes that are generated (not necessarily sorted). std::vector spikes_; diff --git a/arbor/schedule.cpp b/arbor/schedule.cpp index 2a0d6befd6..b31545d1d6 100644 --- a/arbor/schedule.cpp +++ b/arbor/schedule.cpp @@ -1,53 +1,269 @@ #include -#include -#include -#include #include #include #include -// Implementations for specific schedules. - namespace arb { -// Regular schedule implementation. +// Schedule at Poisson point process with rate 1/mean_dt, +// restricted to non-negative times. +struct poisson_schedule_impl { + poisson_schedule_impl(time_type tstart, time_type rate_kHz, seed_type seed, time_type tstop): + tstart_(tstart), rate_(rate_kHz), exp_(rate_kHz), rng_(seed), seed_(seed), next_(tstart), tstop_(tstop) { + if (!std::isfinite(tstart_)) throw std::domain_error("Poisson schedule: start must be finite and in [ms]"); + if (!std::isfinite(tstop_)) throw std::domain_error("Poisson schedule: stop must be finite and in [ms]"); + if (!std::isfinite(rate_kHz)) throw std::domain_error("Poisson schedule: rate must be finite and in [kHz]"); + if (!std::isfinite(tstart_) || tstart_ < 0) throw std::domain_error("Poisson schedule: start must be >= 0 and finite."); + if (!std::isfinite(tstop_) || tstop_ < tstart_) throw std::domain_error("Poisson schedule: stop must be >= start and finite."); + step(); + } + + void reset() { + rng_ = engine_type{seed_}; + if (discard_ > 0) rng_.discard(discard_); + exp_ = std::exponential_distribution{rate_}; + next_ = tstart_; + step(); + } + + void discard(std::size_t n) { discard_ = n; reset(); } -time_event_span regular_schedule_impl::events(time_type t0, time_type t1) { - times_.clear(); + time_event_span events(time_type t0, time_type t1) { + // if we start after the maximal allowed time, we have nothing to do + if (t0 >= tstop_) return {}; - t0 = std::max(t0, t0_); - t1 = std::min(t1, t1_); + // restrict by maximal allowed time + t1 = std::min(t1, tstop_); - if (t1>t0) { - times_.reserve(1+std::size_t((t1-t0)*oodt_)); + times_.clear(); - long long n = t0*oodt_; - time_type t = n*dt_; + while (next_ < t0) { step(); } - while (t + void t_serialize(::arb::serializer& ser, const K& k) const { + const auto& t = *this; + ser.begin_write_map(arb::to_serdes_key(k)); + ARB_SERDES_WRITE(tstart_); + ARB_SERDES_WRITE(tstop_); + ser.end_write_map(); + } + + template + void t_deserialize(::arb::serializer& ser, const K& k) { + auto& t = *this; + ser.begin_read_map(arb::to_serdes_key(k)); + ARB_SERDES_READ(tstart_); + ARB_SERDES_READ(tstop_); + ser.end_read_map(); + t.reset(); + } + + time_type tstart_; + time_type rate_; + std::exponential_distribution exp_; + engine_type rng_; + seed_type seed_; + time_type next_; + std::vector times_; + time_type tstop_; + std::size_t discard_ = 0; +}; + +schedule poisson_schedule(const units::quantity& tstart, + const units::quantity& rate, + seed_type seed, + const units::quantity& tstop) { + return schedule(poisson_schedule_impl(tstart.value_as(units::ms), + rate.value_as(units::kHz), + seed, + tstop.value_as(units::ms))); +} + +schedule poisson_schedule(const units::quantity& rate, + seed_type seed, + const units::quantity& tstop) { + return poisson_schedule(0.*units::ms, rate, seed, tstop); +} + + +struct empty_schedule_impl { + void reset() {} + time_event_span events(time_type t0, time_type t1) { + static time_type no_time; + return {&no_time, &no_time}; + } + + + void t_serialize(::arb::serializer& ser, + const std::string& k) const { + ser.begin_write_map(::arb::to_serdes_key(k)); + ser.end_write_map(); + } + + void t_deserialize(::arb::serializer& ser, + const std::string& k) { + ser.begin_read_map(::arb::to_serdes_key(k)); + ser.end_read_map(); + } + + void discard(std::size_t) {} +}; + +schedule::schedule(): schedule(empty_schedule_impl{}) {} + +// Schedule at k·dt for integral k≥0 within the interval [t0, t1). +struct ARB_ARBOR_API regular_schedule_impl { + explicit regular_schedule_impl(time_type t0, time_type dt, time_type t1): + t0_(t0), t1_(t1), dt_(dt), oodt_(1./dt) { + if (!std::isfinite(t0_)) throw std::domain_error("Regular schedule: start must be finite and in [ms]"); + if (!std::isfinite(t1_)) throw std::domain_error("Regular schedule: stop must be finite and in [ms]"); + if (!std::isfinite(dt_)) throw std::domain_error("Regular schedule: step must be finite and in [ms]"); + if (dt_ <= 0) throw std::domain_error("regular schedule: dt must be > 0 and finite."); + if (t0_ < 0) throw std::domain_error("regular schedule: start must be >= 0 and finite."); + if (t1_ < t0_) throw std::domain_error("regular schedule: stop must be >= start and finite."); + }; + + void reset() {} + + ARB_SERDES_ENABLE(regular_schedule_impl, t0_, t1_, dt_, oodt_); + + template + void t_serialize(::arb::serializer& ser, const K& k) const { + const auto& t = *this; + ser.begin_write_map(arb::to_serdes_key(k)); + ARB_SERDES_WRITE(t0_); + ARB_SERDES_WRITE(t1_); + ARB_SERDES_WRITE(dt_); + ser.end_write_map(); + } + + template + void t_deserialize(::arb::serializer& ser, const K& k) { + auto& t = *this; + ser.begin_read_map(arb::to_serdes_key(k)); + ARB_SERDES_READ(t0_); + ARB_SERDES_READ(t1_); + ARB_SERDES_READ(dt_); + oodt_ = 1.0/dt_; + ser.end_read_map(); + } + + time_event_span events(time_type t0, time_type t1) { + times_.clear(); + + t0 = std::max(t0, t0_); + t1 = std::min(t1, t1_); + + if (t1>t0) { + times_.reserve(1+std::size_t((t1-t0)*oodt_)); + + long long n = t0*oodt_; + time_type t = n*dt_; + + while (t times_; +}; + +schedule regular_schedule(const units::quantity& t0, + const units::quantity& dt, + const units::quantity& t1) { + return schedule(regular_schedule_impl(t0.value_as(units::ms), + dt.value_as(units::ms), + t1.value_as(units::ms))); } -// Explicit schedule implementation. +schedule regular_schedule(const units::quantity& dt) { return regular_schedule(0*units::ms, dt); } -time_event_span explicit_schedule_impl::events(time_type t0, time_type t1) { - time_event_span view = as_time_event_span(times_); +// Schedule at times given explicitly via a provided sorted sequence. +struct explicit_schedule_impl { + explicit_schedule_impl(const explicit_schedule_impl&) = default; + explicit_schedule_impl(explicit_schedule_impl&&) = default; - const time_type* lb = std::lower_bound(view.first+start_index_, view.second, t0); - const time_type* ub = std::lower_bound(lb, view.second, t1); + explicit explicit_schedule_impl(std::vector seq): + start_index_(0), times_(std::move(seq)) { + time_type last = -1; + for (auto t: times_) { + if (!std::isfinite(t)) throw std::domain_error("explicit schedule: times must be finite and in [ms]"); + if (t < 0) throw std::domain_error("explicit schedule: times must be >= 0 and finite."); + if (t < last) throw std::domain_error("explicit schedule: times must be sorted."); + last = t; + } + } - start_index_ = ub-view.first; - return {lb, ub}; + void reset() { start_index_ = 0; } + + time_event_span events(time_type t0, time_type t1) { + time_event_span view = as_time_event_span(times_); + + const time_type* lb = std::lower_bound(view.first+start_index_, view.second, t0); + const time_type* ub = std::lower_bound(lb, view.second, t1); + + start_index_ = ub-view.first; + return {lb, ub}; + } + + template + void t_serialize(::arb::serializer& ser, const K& k) const { + const auto& t = *this; + ser.begin_write_map(arb::to_serdes_key(k)); + ARB_SERDES_WRITE(start_index_); + ARB_SERDES_WRITE(times_); + ser.end_write_map(); + } + + template + void t_deserialize(::arb::serializer& ser, const K& k) { + auto& t = *this; + ser.begin_read_map(arb::to_serdes_key(k)); + ARB_SERDES_READ(start_index_); + ARB_SERDES_READ(times_); + ser.end_read_map(); + } + + void discard(std::size_t) {} + + std::ptrdiff_t start_index_; + std::vector times_; +}; + +schedule explicit_schedule(const std::vector& seq) { + std::vector res; + res.reserve(seq.size()); + for (const auto& t: seq) res.push_back(t.value_as(units::ms)); + return schedule(explicit_schedule_impl(std::move(res))); } +schedule explicit_schedule_from_milliseconds(const std::vector& seq) { + return schedule(explicit_schedule_impl(seq)); +} + + } // namespace arb diff --git a/arbor/simulation.cpp b/arbor/simulation.cpp index dac25d6fef..95963e85fd 100644 --- a/arbor/simulation.cpp +++ b/arbor/simulation.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -15,13 +14,10 @@ #include "cell_group.hpp" #include "cell_group_factory.hpp" #include "communication/communicator.hpp" -#include "execution_context.hpp" #include "merge_events.hpp" #include "thread_private_spike_store.hpp" #include "threading/threading.hpp" -#include "util/filter.hpp" #include "util/maputil.hpp" -#include "util/partition.hpp" #include "util/span.hpp" #include "profile/profiler_macro.hpp" @@ -398,6 +394,9 @@ time_type simulation_state::run(time_type tfinal, time_type dt) { // Requires state at end of run(), with epoch_.id==k: // * U(k) and D(k) have completed. + if (!std::isfinite(tfinal) || tfinal < 0) throw std::domain_error("simulation: tfinal must be finite, positive, and in [ms]"); + if (!std::isfinite(dt) || tfinal < 0) throw std::domain_error("simulation: dt must be finite, positive, and in [ms]"); + if (tfinal<=epoch_.t1) return epoch_.t1; // Compute following epoch, with max time tfinal. @@ -596,11 +595,12 @@ void simulation::reset() { void simulation::update(const connectivity& rec) { impl_->update(rec); } -time_type simulation::run(time_type tfinal, time_type dt) { - if (dt <= 0.0) { - throw domain_error("Finite time-step must be supplied."); - } - return impl_->run(tfinal, dt); +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::isnan(dt_ms)) throw domain_error("Finite time-step must be supplied."); + auto tfinal_ms = tfinal.value_as(units::ms); + if (tfinal_ms <= 0.0 || std::isnan(tfinal_ms)) throw domain_error("Finite time-step must be supplied."); + return impl_->run(tfinal_ms, dt_ms); } sampler_association_handle simulation::add_sampler( diff --git a/arborio/cableio.cpp b/arborio/cableio.cpp index b0383b74a9..4ca05cc64a 100644 --- a/arborio/cableio.cpp +++ b/arborio/cableio.cpp @@ -19,7 +19,7 @@ namespace arborio { using namespace arb; -ARB_ARBORIO_API std::string acc_version() {return "0.1-dev";} +ARB_ARBORIO_API std::string acc_version() {return "0.9-dev";} cableio_parse_error::cableio_parse_error(const std::string& msg, const arb::src_location& loc): arb::arbor_exception(msg+" at :"+ @@ -44,28 +44,31 @@ s_expr mksexp(const iexpr& j) { return parse_s_expr(s.str()); } s_expr mksexp(const init_membrane_potential& p) { - return slist("membrane-potential"_symbol, mksexp(p.value)); + return slist("membrane-potential"_symbol, p.value, mksexp(p.scale)); +} +s_expr mksexp(const arb::units::quantity& p) { + return slist("quantity"_symbol, p.value(), s_expr(arb::units::to_string(p.units()))); } s_expr mksexp(const axial_resistivity& r) { - return slist("axial-resistivity"_symbol, mksexp(r.value)); + return slist("axial-resistivity"_symbol, r.value, mksexp(r.scale)); } -s_expr mksexp(const temperature_K& t) { - return slist("temperature-kelvin"_symbol, mksexp(t.value)); +s_expr mksexp(const temperature& t) { + return slist("temperature-kelvin"_symbol, t.value, mksexp(t.scale)); } s_expr mksexp(const membrane_capacitance& c) { - return slist("membrane-capacitance"_symbol, mksexp(c.value)); + return slist("membrane-capacitance"_symbol, c.value, mksexp(c.scale)); } s_expr mksexp(const init_int_concentration& c) { - return slist("ion-internal-concentration"_symbol, s_expr(c.ion), mksexp(c.value)); + return slist("ion-internal-concentration"_symbol, s_expr(c.ion), c.value, mksexp(c.scale)); } s_expr mksexp(const init_ext_concentration& c) { - return slist("ion-external-concentration"_symbol, s_expr(c.ion), mksexp(c.value)); + return slist("ion-external-concentration"_symbol, s_expr(c.ion), c.value, mksexp(c.scale)); } s_expr mksexp(const init_reversal_potential& c) { - return slist("ion-reversal-potential"_symbol, s_expr(c.ion), mksexp(c.value)); + return slist("ion-reversal-potential"_symbol, s_expr(c.ion), c.value, mksexp(c.scale)); } s_expr mksexp(const ion_diffusivity& c) { - return slist("ion-diffusivity"_symbol, s_expr(c.ion), mksexp(c.value)); + return slist("ion-diffusivity"_symbol, s_expr(c.ion), c.value, mksexp(c.scale)); } s_expr mksexp(const i_clamp& c) { std::vector evlps; @@ -224,15 +227,13 @@ using branch_tuple = std::tuple>; using version_tuple = std::tuple; // Define makers for defaultables, paintables, placeables -#define ARBIO_DEFINE_ONE_ARG(name) arb::name make_##name(double val) { return arb::name{val};} +#define ARBIO_DEFINE_ONE_ARG(name) arb::name make_##name(double val) { return arb::name(val);} #define ARBIO_DEFINE_ION_ARG(name) arb::name make_##name(const std::string& ion, double val) { return arb::name{ion, val};} -#define ARBIO_DEFINE_IEXPR_ION_ARG(name) arb::name make_##name(const std::string& ion, iexpr val) { return arb::name{ion, val};} -#define ARBIO_DEFINE_IEXPR_ARG(name) arb::name make_##name(iexpr val) { return arb::name{val}; } +#define ARBIO_DEFINE_IEXPR_ION_ARG(name) arb::name make_##name(const std::string& ion, double base, iexpr scale) { arb::name res; res.ion = ion; res.value = base; res.scale = scale; return res; } +#define ARBIO_DEFINE_IEXPR_ARG(name) arb::name make_##name(double base, iexpr scale) { arb::name res; res.value = base; res.scale = scale; return res; } -ARB_PP_FOREACH(ARBIO_DEFINE_ONE_ARG, threshold_detector) -ARB_PP_FOREACH(ARBIO_DEFINE_IEXPR_ARG, init_membrane_potential, temperature_K, axial_resistivity, membrane_capacitance) -ARB_PP_FOREACH(ARBIO_DEFINE_ION_ARG, ion_diffusivity) -ARB_PP_FOREACH(ARBIO_DEFINE_IEXPR_ION_ARG, init_int_concentration, init_ext_concentration, init_reversal_potential) +ARB_PP_FOREACH(ARBIO_DEFINE_IEXPR_ARG, init_membrane_potential, temperature, axial_resistivity, membrane_capacitance) +ARB_PP_FOREACH(ARBIO_DEFINE_IEXPR_ION_ARG, ion_diffusivity, init_int_concentration, init_ext_concentration, init_reversal_potential) #undef ARBIO_DEFINE_ONE_ARG #undef ARBIO_DEFINE_ION_ARG @@ -244,18 +245,22 @@ std::vector make_envelope(const std::vector(x); - return arb::i_clamp::envelope_point{std::get<0>(t), std::get<1>(t)}; + return arb::i_clamp::envelope_point{std::get<0>(t)*arb::units::ms, std::get<1>(t)*arb::units::nA}; }); return envlp; } arb::i_clamp make_i_clamp(const std::vector& envlp, double freq, double phase) { - return arb::i_clamp(envlp, freq, phase); + return arb::i_clamp(envlp, freq*arb::units::kHz, phase*arb::units::rad); } pulse_tuple make_envelope_pulse(double delay, double duration, double amplitude) { return pulse_tuple{delay, duration, amplitude}; } arb::i_clamp make_i_clamp_pulse(const pulse_tuple& p, double freq, double phase) { - return arb::i_clamp::box(std::get<0>(p), std::get<1>(p), std::get<2>(p), freq, phase); + return arb::i_clamp::box(std::get<0>(p)*arb::units::ms, + std::get<1>(p)*arb::units::ms, + std::get<2>(p)*arb::units::nA, + freq*arb::units::kHz, + phase*arb::units::rad); } arb::cv_policy make_cv_policy(const cv_policy& p) { return p; @@ -652,26 +657,21 @@ parse_hopefully eval(const s_expr& e, const eval_map& map, const eval_ return util::unexpected(cableio_parse_error("Expression is not integer, real expression of the form (op ) nor tuple of the form (e0 e1 ... en)", location(e))); } -#define ARBIO_ADD_EVAL(name, fun, ty) { name, make_call(fun, "'" name "' with 1 argument (val:" #ty ")") } -#define ARBIO_ADD_ION_EVAL(name, fun, ty) { name, make_call(fun, "'" name "' with 2 argument (ion:string, val:" #ty ")") } +arb::units::quantity make_quantity(double v, std::string u) { return v*arb::units::unit_cast_from_string(u); } + +#define ARBIO_ADD_EVAL(name, fun) { name, make_call(fun, "'" name "' with 2 arguments: val:real and ex:iexpr") } +#define ARBIO_ADD_ION_EVAL(name, fun) { name, make_call(fun, "'" name "' with 3 arguments: ion:string, val:real, and ex:iexpr") } eval_map named_evals{ - ARBIO_ADD_EVAL("membrane-potential", make_init_membrane_potential, double), - ARBIO_ADD_EVAL("membrane-potential", make_init_membrane_potential, iexpr), - ARBIO_ADD_EVAL("temperature-kelvin", make_temperature_K, double), - ARBIO_ADD_EVAL("temperature-kelvin", make_temperature_K, iexpr), - ARBIO_ADD_EVAL("axial-resistivity", make_axial_resistivity, double), - ARBIO_ADD_EVAL("axial-resistivity", make_axial_resistivity, iexpr), - ARBIO_ADD_EVAL("membrane-capacitance", make_membrane_capacitance, double), - ARBIO_ADD_EVAL("membrane-capacitance", make_membrane_capacitance, iexpr), - ARBIO_ADD_ION_EVAL("ion-internal-concentration", make_init_int_concentration, double), - ARBIO_ADD_ION_EVAL("ion-internal-concentration", make_init_int_concentration, iexpr), - ARBIO_ADD_ION_EVAL("ion-external-concentration", make_init_ext_concentration, double), - ARBIO_ADD_ION_EVAL("ion-external-concentration", make_init_ext_concentration, iexpr), - ARBIO_ADD_ION_EVAL("ion-reversal-potential", make_init_reversal_potential, double), - ARBIO_ADD_ION_EVAL("ion-reversal-potential", make_init_reversal_potential, iexpr), - ARBIO_ADD_ION_EVAL("ion-diffusivity", make_ion_diffusivity, double), - // Intentionally left out! ARBIO_ADD_ION_EVAL("ion-diffusivity", make_ion_diffusivity, iexpr), + ARBIO_ADD_EVAL("membrane-potential", make_init_membrane_potential), + ARBIO_ADD_EVAL("temperature-kelvin", make_temperature), + ARBIO_ADD_EVAL("axial-resistivity", make_axial_resistivity), + ARBIO_ADD_EVAL("membrane-capacitance", make_membrane_capacitance), + ARBIO_ADD_ION_EVAL("ion-internal-concentration", make_init_int_concentration), + ARBIO_ADD_ION_EVAL("ion-external-concentration", make_init_ext_concentration), + ARBIO_ADD_ION_EVAL("ion-reversal-potential", make_init_reversal_potential), + ARBIO_ADD_ION_EVAL("ion-diffusivity", make_ion_diffusivity), + {"quantity", make_call(make_quantity, "'quantity' with a value:real and a unit:string")}, {"envelope", make_arg_vec_call(make_envelope, "'envelope' with one or more pairs of start time and amplitude (start:real amplitude:real)")}, {"envelope-pulse", make_call(make_envelope_pulse, @@ -680,7 +680,7 @@ eval_map named_evals{ "'current-clamp' with 3 arguments (env:envelope freq:real phase:real)")}, {"current-clamp", make_call(make_i_clamp_pulse, "'current-clamp' with 3 arguments (env:envelope_pulse freq:real phase:real)")}, - {"threshold-detector", make_call(make_threshold_detector, + {"threshold-detector", make_call(arb::threshold_detector::from_raw_millivolts, "'threshold-detector' with 1 argument (threshold:real)")}, {"mechanism", make_mech_call("'mechanism' with a name argument, and 0 or more parameter settings" "(name:string (param:string val:real))")}, @@ -700,7 +700,7 @@ eval_map named_evals{ {"place", make_call(make_place, "'place' with 3 arguments (ls:locset mech:synapse name:string)")}, {"paint", make_call(make_paint, "'paint' with 2 arguments (reg:region v:membrane-potential)")}, - {"paint", make_call(make_paint, "'paint' with 2 arguments (reg:region v:temperature-kelvin)")}, + {"paint", make_call(make_paint, "'paint' with 2 arguments (reg:region v:temperature-kelvin)")}, {"paint", make_call(make_paint, "'paint' with 2 arguments (reg:region v:membrane-capacitance)")}, {"paint", make_call(make_paint, "'paint' with 2 arguments (reg:region v:axial-resistivity)")}, {"paint", make_call(make_paint, "'paint' with 2 arguments (reg:region v:ion-internal-concentration)")}, @@ -711,7 +711,7 @@ eval_map named_evals{ {"paint", make_call>(make_paint, "'paint' with 2 arguments (reg:region v:scaled-mechanism)")}, {"default", make_call(make_default, "'default' with 1 argument (v:membrane-potential)")}, - {"default", make_call(make_default, "'default' with 1 argument (v:temperature-kelvin)")}, + {"default", make_call(make_default, "'default' with 1 argument (v:temperature-kelvin)")}, {"default", make_call(make_default, "'default' with 1 argument (v:membrane-capacitance)")}, {"default", make_call(make_default, "'default' with 1 argument (v:axial-resistivity)")}, {"default", make_call(make_default, "'default' with 1 argument (v:ion-internal-concentration)")}, diff --git a/arborio/parse_helpers.hpp b/arborio/parse_helpers.hpp index 5b0c37f9a7..e1bc02f4cf 100644 --- a/arborio/parse_helpers.hpp +++ b/arborio/parse_helpers.hpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -12,6 +11,8 @@ #include #include #include +#include + namespace arborio { using namespace arb; diff --git a/doc/concepts/decor.rst b/doc/concepts/decor.rst index 89e5ee7d4a..ea96543e74 100644 --- a/doc/concepts/decor.rst +++ b/doc/concepts/decor.rst @@ -204,7 +204,8 @@ resistivity, and membrane capacitance, as well as all ion parameters # initial value for the membrane potential as inhomogeneous expression. - decor.paint('(all)', Vm='(mul 42 (diameter))') + # we give a pair of a base value and a scaling iexpr + decor.paint('(all)', Vm=(23, '(mul 42 (diameter))')) .. _cablecell-ions: diff --git a/doc/concepts/units.rst b/doc/concepts/units.rst new file mode 100644 index 0000000000..d52ed30ef2 --- /dev/null +++ b/doc/concepts/units.rst @@ -0,0 +1,192 @@ +.. _units: + +Units in Arbor +============== + +.. note:: + + This is a work in progress. The near goal term is to make this coverage + complete, but expect some exceptions. Notably, the interfaces of individual + mechanism are not yet integrated, since NMODL files -- despite explicitly + specifying units -- do not make good use of the feature. + +A large part of the interface of Arbor -- both in C++ and Python -- is covered +by units of measurement. This gives the API a way to communicate the intended +units of parameters and return values and users can largely use their preferred +system of units, as automatic conversion is provided. For performance reasons, +this extends to the outermost layer only, after which Arbor uses its own +internal system of measurement. + +We leverage the `units library `_ published by LLNL. + +Arbor is focussed on SI units, and we provide the following presets for both the +C++ and Python modules. + +.. table:: Provided dimensionalities and units + :widths: auto + + ============= ======= + Dimension Unit + ============= ======= + Temperature Kelvin + Celsius + Length m + cm + mm + um + nm + Time s + ms + us + ns + Resistance Ohm + kOhm + MOhm + Conductivity S + mS + uS + Current A + mA + uA + nA + pA + Potential V + mV + Frequency Hz + kHz + Capacity F + mF + uF + nF + pF + Area m2 + cm2 + mm2 + um2 + nm2 + Charge C + Mol mol + Molarity M = mol/l + mM + Angle rad + deg + ============= ======= + +Further units may be derived from existing ones by mean of multiplication and +division with the obvious semantics, the existing metric prefixes, or by extending +the catalogue of units via the underlying units library. + +.. table:: Provided metric prefixes + :widths: auto + + ============= ======= ============= ======= + Prefix Scale Prefix Scale + ============= ======= ============= ======= + pico 1e-12 kilo 1e3 + nano 1e-9 mega 1e6 + micro 1e-6 giga 1e9 + milli 1e-3 + centi 1e-2 + ============= ======= ============= ======= + +Parameters are passed into Arbor via a ``quantity``, which comprise a value and +a unit. We construct a quantity by multiplication of a scalar value by a unit. +Multiplication of two quantities will result in the pointwise product of the +values and units; like one would expect. + +.. code-block:: python + + # two kilometers, dimension is length + l = 2 * km + + # three kilometers, but with the scaling factored out + s = 3 * kilo * m + + # multiplication of two lengths gives an area + a = l * s + # is now 6 square kilometers + +Units and quantities work intuitively and largely the same across C++ and +Python, but we provide some details below. + +C++ +--- + +Units are defined in the ``units`` namespace, and exist at runtime, since we +need to cater to dynamical language bindings. In the ``units::literals`` +namespace, we find user defined literals for all units above, e.g. ``10_mV``. +Integral powers of units are constructed using the ``.pow(int)`` member, e.g. +``m2 = m.pow(2)``. Units and quantities can be converted to and from strings +using the ``std::string to_string(const T&)`` and ``T from_string_cast(const std::string&)`` +functions. Conversion between different units is done like this + +.. code-block:: c++ + + namespace U = arb::units; + + // membrane capacitance in SI + auto c_m = 42*U::F/U::m.pow(2) // same as 42*U::F*U::m.pow(-2) + // convert to different unit and extract value + auto c_m_ = c_m.value_as(U::uF*U::cm.pow(-2)) + // invalid conversions result in NaN values, so check + if (std::isnan(c_m_)) throw std::domain_error("Invalid value"); + +however, Arbor does this whenever values pass its interface. + +.. cpp::namespace:: arb::units + +.. cpp:class:: unit + + Describes a unit of measurement. + + .. method:: pow(int) + + Raise unit to integral power. + +.. cpp:class:: quantity + + A tuple of a value and a unit of measurement. + + .. method:: value_as(unit) + + Convert to another unit and return converted value, possibly NaN, if + malformed. + + +Python +------ + +Units are defined in the ``units`` sub-module. Integral powers of units are +constructed using the ``**`` operator, e.g. ``m2 = m ** 2``. Units and +quantities can be converted to a string using the ``str()`` function. +Conversion between different units is done like this + +.. code-block:: python + + from arbor import units as U + from math import isnan + + # membrane capacitance in SI + c_m = 42*U.F/U.m**2 + # convert to different unit and extract value + c_m_ = c_m.value_as(U.uF*U.cm**-2) + # invalid conversions result in NaN values, so check + if isnan(c_m_): + raise ValueError("Invalid value") + +however, Arbor does this whenever values pass its interface. + +.. currentmodule:: arbor.units + +.. class:: unit + + Describes a unit of measurement. + +.. class:: quantity + + A tuple of a value and a unit of measurement. + + .. method:: value_as + + Convert to another unit and return converted value, possibly NaN, if + malformed. diff --git a/doc/cpp/cable_cell.rst b/doc/cpp/cable_cell.rst index 9013db7dcd..1b564c3374 100644 --- a/doc/cpp/cable_cell.rst +++ b/doc/cpp/cable_cell.rst @@ -169,19 +169,19 @@ value should be taken from the cell or global parameter set. Internal and external concentrations are given in millimolars, i.e. mol/m³. Reversal potential is given in millivolts. - .. cpp:member:: util::optional init_membrane_potential + .. cpp:member:: util::optional init_membrane_potential Initial membrane potential in millivolts. - .. cpp:member:: util::optional temperature_K + .. cpp:member:: util::optional temperature Local temperature in Kelvin. - .. cpp:member:: util::optional axial_resistivity + .. cpp:member:: util::optional axial_resistivity Local resistivity of the intracellular medium, in ohm-centimetres. - .. cpp:member:: util::optional membrane_capacitance + .. cpp:member:: util::optional membrane_capacitance Local areal capacitance of the cell membrane, in Farads per square metre. diff --git a/doc/cpp/interconnectivity.rst b/doc/cpp/interconnectivity.rst index 9f572b1f90..9bd2bc49a9 100644 --- a/doc/cpp/interconnectivity.rst +++ b/doc/cpp/interconnectivity.rst @@ -34,9 +34,9 @@ Interconnectivity the `expsyn` synapse interprets it as a conductance with units μS (micro-Siemens). - .. cpp:member:: float delay + .. cpp:member:: units::quantity delay - Delay of the connection (milliseconds). + Delay of the connection (milliseconds), must be positive and finite. .. cpp:class:: ext_cell_connection diff --git a/doc/cpp/lif_cell.rst b/doc/cpp/lif_cell.rst index 60a72881a4..dec8f34e4f 100644 --- a/doc/cpp/lif_cell.rst +++ b/doc/cpp/lif_cell.rst @@ -25,30 +25,30 @@ LIF cells The label of the single built-in target on the cell. Used for forming connections to the cell in the :cpp:class:`recipe` by creating a :cpp:class:`connection`. - .. cpp:member:: double tau_m + .. cpp:member:: const arb::units::quantity& tau_m - Membrane potential decaying constant [ms]. + Membrane potential decaying constant [ms]. Must be finite and positive. - .. cpp:member:: double V_th + .. cpp:member:: const arb::units::quantity& V_th - Firing threshold [mV]. + Firing threshold [mV], must be finite. - .. cpp:member:: double C_m + .. cpp:member:: const arb::units::quantity& C_m - Membrane capacitance [pF]. + Membrane capacitance [pF], must be finite and positive. - .. cpp:member:: double E_L + .. cpp:member:: const arb::units::quantity& E_L - Resting potential [mV]. + Resting potential [mV], must be finite. - .. cpp:member:: double E_R + .. cpp:member:: const arb::units::quantity& E_R - Reset potential [mV]. + Reset potential [mV], must be finite. - .. cpp:member:: double V_m + .. cpp:member:: const arb::units::quantity& V_m - Initial value of the Membrane potential [mV]. + Initial value of the Membrane potential [mV], must be finite. - .. cpp:member:: double t_ref + .. cpp:member:: const arb::units::quantity& t_ref - Refractory period [ms]. + Refractory period [ms]. Must be finite and positive. diff --git a/doc/fileformat/cable_cell.rst b/doc/fileformat/cable_cell.rst index 7038520287..be35921ba7 100644 --- a/doc/fileformat/cable_cell.rst +++ b/doc/fileformat/cable_cell.rst @@ -396,7 +396,7 @@ Parsable arbor-components and meta-data The formats described above can be used to generate a :ref:`label dictionary `, :ref:`decoration `, :ref:`morphology `, or :ref:`cable cell ` object. These are denoted as arbor-components. Arbor-components need to be accompanied by *meta-data* -specifying the version of the format being used. The only version currently supported is ``0.1-dev``. +specifying the version of the format being used. The only version currently supported is ``0.9-dev``. .. label:: (version val:string) @@ -418,7 +418,7 @@ Label-dict .. code:: lisp (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (label-dict (region-def "my_soma" (tag 1)) (locset-def "root" (root)))) @@ -429,7 +429,7 @@ Decoration .. code:: lisp (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (decor (default (membrane-potential -55.000000)) (place (locset "root") (synapse (mechanism "expsyn")) "root_synapse") @@ -441,7 +441,7 @@ Morphology .. code:: lisp (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (morphology (branch 0 -1 (segment 0 (point 0 0 0 2) (point 4 0 0 2) 1) @@ -454,7 +454,7 @@ Cable-cell .. code:: lisp (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (cable-cell (label-dict (region-def "my_soma" (tag 1)) diff --git a/doc/index.rst b/doc/index.rst index 66401c9bc7..a7d623b98c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -150,6 +150,7 @@ A full list of our software attributions can be found `here `. -The recipe is where the different cells and the :ref:`connections ` between them are defined. - -Step **(5)** shows a class definition for a recipe with multiple cells. Instantiating the class requires the desired -number of cells as input. Compared to the :ref:`simple cell recipe `, the main differences -are connecting the cells **(8)**, returning a configurable number of cells **(6)** and returning a new cell per ``gid`` **(7)**. - -Step **(8)** creates an :py:class:`arbor.connection` between consecutive cells. If a cell has gid ``gid``, the -previous cell has a gid ``(gid-1)%self.ncells``. The connection has a weight of 0.01 (inducing a conductance of 0.01 μS -in the target mechanism ``expsyn``) and a delay of 5 ms. -The first two arguments to :py:class:`arbor.connection` are the **source** and **target** of the connection. - -The **source** is a :py:class:`arbor.cell_global_label` object containing a cell index ``gid``, the source label -corresponding to a valid detector label on the cell and an optional selection policy (for choosing a single detector -out of potentially many detectors grouped under the same label - remember, in this case the number of detectors labeled -'detector' is 1). -The :py:class:`arbor.cell_global_label` can be initialized with a ``(gid, label)`` tuple, in which case the selection -policy is the default :py:attr:`arbor.selection_policy.univalent`; or a ``(gid, (label, policy))`` tuple. - -The **target** is a :py:class:`arbor.cell_local_label` object containing a cell index ``gid``, the target label -corresponding to a valid synapse label on the cell and an optional selection policy (for choosing a single synapse -out of potentially many synapses grouped under the same label - remember, in this case the number of synapses labeled -'syn' is 1). -The :py:class:`arbor.cell_local_label` can be initialized with a ``label`` string, in which case the selection -policy is the default :py:attr:`arbor.selection_policy.univalent`; or a ``(label, policy)`` tuple. The ``gid`` -of the target cell doesn't need to be explicitly added to the connection, it is the argument to the -:py:func:`arbor.recipe.connections_on` method. - -Step **(9)** attaches an :py:class:`arbor.event_generator` on the 0th target (synapse) on the 0th cell; this means it -is connected to the ``"synapse_site"`` on cell 0. This initiates the signal cascade through the network. The -:class:`arbor.explicit_schedule` in instantiated with a list of times in milliseconds, so here a single event at the 1 -ms mark is emitted. Note that this synapse is connected twice, once to the event generator, and once to another cell. +.. literalinclude:: ../../python/example/network_ring.py + :language: python + :lines: 70-112 + +To create a model with multiple connected cells, we need to use a +:py:class:`recipe `. The recipe is where the different cells and +the :ref:`connections ` between them are defined. + +Step **(5)** shows a class definition for a recipe with multiple cells. +Instantiating the class requires the desired number of cells as input. Compared +to the :ref:`simple cell recipe `, the main +differences are connecting the cells **(8)**, returning a configurable number of +cells **(6)** and returning a new cell per ``gid`` **(7)**. + +Step **(8)** creates an :py:class:`arbor.connection` between consecutive cells. +If a cell has gid ``gid``, the previous cell has a gid ``(gid-1)%self.ncells``. +The connection has a weight of 0.01 (inducing a conductance of 0.01 μS in the +target mechanism ``expsyn``) and a delay of 5 ms. The first two arguments to +:py:class:`arbor.connection` are the **source** and **target** of the +connection. + +The **source** is a :py:class:`arbor.cell_global_label` object containing a cell +index ``gid``, the source label corresponding to a valid detector label on the +cell and an optional selection policy (for choosing a single detector out of +potentially many detectors grouped under the same label - remember, in this case +the number of detectors labeled 'detector' is 1). The +:py:class:`arbor.cell_global_label` can be initialized with a ``(gid, label)`` +tuple, in which case the selection policy is the default +:py:attr:`arbor.selection_policy.univalent`; or a ``(gid, (label, policy))`` +tuple. + +The **target** is a :py:class:`arbor.cell_local_label` object containing a cell +index ``gid``, the target label corresponding to a valid synapse label on the +cell and an optional selection policy (for choosing a single synapse out of +potentially many synapses grouped under the same label - remember, in this case +the number of synapses labeled 'syn' is 1). The +:py:class:`arbor.cell_local_label` can be initialized with a ``label`` string, +in which case the selection policy is the default +:py:attr:`arbor.selection_policy.univalent`; or a ``(label, policy)`` tuple. The +``gid`` of the target cell doesn't need to be explicitly added to the +connection, it is the argument to the :py:func:`arbor.recipe.connections_on` +method. + +Step **(9)** attaches an :py:class:`arbor.event_generator` on the 0th target +(synapse) on the 0th cell; this means it is connected to the ``"synapse_site"`` +on cell 0. This initiates the signal cascade through the network. The +:class:`arbor.explicit_schedule` in instantiated with a list of times in +milliseconds, so here a single event at the 1 ms mark is emitted. Note that this +synapse is connected twice, once to the event generator, and once to another +cell. Step **(10)** places a :term:`probe` at the ``"root"`` of each cell. -Step **(11)** instantiates the recipe with 4 cells. - .. literalinclude:: ../../python/example/network_ring.py :language: python - :lines: 74-122 + :lines: 115-117 + +Step **(11)** instantiates the recipe with 4 cells. The execution ************* -To create a simulation, we need at minimum to supply the recipe, and in addition can supply a :class:`arbor.context` -and :py:class:`arbor.domain_decomposition`. The first lets Arbor know what hardware it should use, the second how to -destribute the work over that hardware. By default, contexts are configured to use 1 thread and domain decompositons to -divide work equally over all threads. +To create a simulation, we need at minimum to supply the recipe, and in addition +can supply a :class:`arbor.context` and :py:class:`arbor.domain_decomposition`. +The first lets Arbor know what hardware it should use, the second how to +destribute the work over that hardware. By default, contexts are configured to +use 1 thread and domain decompositons to divide work equally over all threads. -Step **(12)** creates a simulation object from the recipe. Optionally, the :py:class:`~arbor.simulation` constructor takes two more -parameters: a :class:`arbor.context` and a :class:`arbor.domain_decomposition`. In :ref:`a followup of this tutorial ` that will be demonstrated. -For now, it is enough to know that for simulations that don't require customized execution those arguments can be left out. Without -further arguments Arbor will use all locally available threads. +.. literalinclude:: ../../python/example/network_ring.py + :language: python + :lines: 119-137 + +Step **(12)** creates a simulation object from the recipe. Optionally, the +:py:class:`~arbor.simulation` constructor takes two more parameters: a +:class:`arbor.context` and a :class:`arbor.domain_decomposition`. In :ref:`a +followup of this tutorial ` that will be demonstrated. For now, it +is enough to know that for simulations that don't require customized execution +those arguments can be left out. Without further arguments Arbor will use all +locally available threads. -Step **(13)** sets all spike generators to record using the :py:class:`arbor.spike_recording.all` policy. -This means the timestamps of the generated events will be kept in memory. Be default, these are discarded. +Step **(13)** sets all spike generators to record using the +:py:class:`arbor.spike_recording.all` policy. This means the timestamps of the +generated events will be kept in memory. Be default, these are discarded. In addition to having the timestamps of spikes, we want to extract the voltage as a function of time. @@ -138,10 +165,6 @@ these handles for later use. Step **(15)** executes the simulation for a duration of 100 ms. -.. literalinclude:: ../../python/example/network_ring.py - :language: python - :lines: 124-140 - The results *********** @@ -149,7 +172,7 @@ Step **(16)** prints the timestamps of the spikes: .. literalinclude:: ../../python/example/network_ring.py :language: python - :lines: 142-145 + :lines: 139-142 Step **(17)** generates a plot of the sampling data. :py:func:`arbor.simulation.samples` takes a ``handle`` of the probe we wish to examine. It returns a list @@ -161,7 +184,7 @@ It could have described a :term:`locset`.) .. literalinclude:: ../../python/example/network_ring.py :language: python - :lines: 147- + :lines: 144- Since we have created ``ncells`` cells, we have ``ncells`` traces. We should be seeing phase shifted traces, as the action potential propagated through the network. diff --git a/doc/tutorial/network_ring_gpu.rst b/doc/tutorial/network_ring_gpu.rst index 5be7ba570b..6f0c0b2f53 100644 --- a/doc/tutorial/network_ring_gpu.rst +++ b/doc/tutorial/network_ring_gpu.rst @@ -3,7 +3,10 @@ GPU and profiling ================= -In this example, the ring network created in an :ref:`earlier tutorial ` will be used to run the model with a GPU. In addition, it is shown how to profile the performance difference. Only the differences with that tutorial will be described. +In this example, the ring network created in an :ref:`earlier tutorial +` will be used to run the model with a GPU. In addition, it +is shown how to profile the performance difference. Only the differences with +that tutorial will be described. .. Note:: @@ -17,19 +20,25 @@ In this example, the ring network created in an :ref:`earlier tutorial ` describes the hardware resources on which the simulation will run. -It contains the thread pool used to parallelise work on the local CPU, and optionally describes GPU resources -and the MPI communicator for distributed simulations. In some other examples, the :class:`arbor.single_cell_model` -object created the execution context :class:`arbor.context` behind the scenes. The details of the execution -context can be customized by the user. We may specify the number of threads in the thread pool; determine the -id of the GPU to be used; or create our own MPI communicator. - -Step **(11)** creates a hardware context where we set the :py:attr:`~arbor.proc_allocation.gpu_id`. This requires -that you have built Arbor manually, with GPU support (see :ref:`here ` how to do that). On a regular -consumer device with a single GPU, the index you should pass is ``0``. Change the value to run the example with and -without GPU. The number of threads :class:`~arbor.context.threads` are (when no MPI is used) set to -:py:func:`arbor.env.thread_concurrency`. This value corresponds to the number of locally available threads as best as -can be established by Arbor at the start of the program. +An :ref:`execution context ` describes the hardware resources on +which the simulation will run. It contains the thread pool used to parallelise +work on the local CPU, and optionally describes GPU resources and the MPI +communicator for distributed simulations. In some other examples, the +:class:`arbor.single_cell_model` object created the execution context +:class:`arbor.context` behind the scenes. The details of the execution context +can be customized by the user. We may specify the number of threads in the +thread pool; determine the id of the GPU to be used; or create our own MPI +communicator. + +Step **(11)** creates a hardware context where we set the +:py:attr:`~arbor.proc_allocation.gpu_id`. This requires that you have built +Arbor manually, with GPU support (see :ref:`here ` how to do +that). On a regular consumer device with a single GPU, the index you should pass +is ``0``. Change the value to run the example with and without GPU. The number +of threads :class:`~arbor.context.threads` are (when no MPI is used) set to +:py:func:`arbor.env.thread_concurrency`. This value corresponds to the number of +locally available threads as best as can be established by Arbor at the start of +the program. .. note:: @@ -42,12 +51,17 @@ can be established by Arbor at the start of the program. Profiling ********* -Arbor comes with a :class:`arbor.meter_manager` to help you profile your simulations. In this case, you can run the -example with ``gpu_id=None`` and ``gpu_id=0`` and observe the difference with the :class:`~arbor.meter_manager`. +Arbor comes with a :class:`arbor.meter_manager` to help you profile your +simulations. In this case, you can run the example with ``gpu_id=None`` and +``gpu_id=0`` and observe the difference with the :class:`~arbor.meter_manager`. +If you are interested in more detailled report, Arbor also offers a region based +profiler which is aimed at developers and must be enabled at build time. -Step **(12)** sets up the meter manager and starts it using the (only) context. This way, only Arbor related execution is measured, not Python code. +Step **(12)** sets up the meter manager and starts it using the (only) context. +This way, only Arbor related execution is measured, not Python code. -Step **(13)** instantiates the recipe and sets the first checkpoint on the meter manager. We now have the time it took to construct the recipe. +Step **(13)** instantiates the recipe and sets the first checkpoint on the meter +manager. We now have the time it took to construct the recipe. .. literalinclude:: ../../python/example/network_ring_gpu.py :language: python @@ -56,32 +70,41 @@ Step **(13)** instantiates the recipe and sets the first checkpoint on the meter The domain decomposition ************************ -The domain decomposition describes the distribution of the cells over the available computational resources. -The :class:`arbor.single_cell_model` also handled that without our knowledge in the previous examples. -Now, we have to define it ourselves. - -The :class:`arbor.domain_decomposition` class can be manually created by the user, by deciding which cells -go on which ranks. Or we can use a load balancer that can partition the cells across ranks according to -some rules. Arbor provides :class:`arbor.partition_load_balance`, which, using the recipe and execution -context, creates the :class:`arbor.domain_decomposition` object for us. - -A way to customize :class:`arbor.partition_load_balance` is by providing a :class:`arbor.partition_hint`. They let -you configure how cells are distributed over the resources in the :class:`~arbor.context`, but without requiring you -to know the precise configuration of a :class:`~arbor.context` up front. Whether you run your simulation on your -laptop CPU, desktop GPU, CPU cluster of GPU cluster, using :class:`partition hints` you can -just say: use GPUs, if available. You only have to change the :class:`~arbor.context` to actually define which -hardware Arbor will execute on. - -Step **(14)** creates a :class:`arbor.partition_hint`, and tells it to put 1000 cells in a groups allocated to GPUs, -and to prefer the utilisation of the GPU if present. In fact, the default distribution strategy of -:class:`arbor.partition_load_balance` already spreads out cells as evenly as possible over CPUs, and groups -(up to 1000) on GPUs, so strictly speaking it was not necessary to give that part of the hint. -Lastly, a dictionary is created with which hints are assigned to a particular :class:`arbor.cell_kind`. -Different kinds may favor different execution, hence the option. -In this simulation, there are only :class:`arbor.cell_kind.cable`, so we assign the hint to that kind. - -Step **(15)** creates a :class:`arbor.partition_load_balance` with the recipe, context and hints created above. -Another checkpoint will help us understand how long creating the load balancer took. +The domain decomposition describes the distribution of the cells over the +available computational resources. The :class:`arbor.single_cell_model` also +handled that without our knowledge in the previous examples. Now, we have to +define it ourselves. + +The :class:`arbor.domain_decomposition` class can be manually created by the +user, by deciding which cells go on which ranks. Or we can use a load balancer +that can partition the cells across ranks according to some rules. Arbor +provides :class:`arbor.partition_load_balance`, which, using the recipe and +execution context, creates the :class:`arbor.domain_decomposition` object for +us. + +A way to customize :class:`arbor.partition_load_balance` is by providing a +:class:`arbor.partition_hint`. They let you configure how cells are distributed +over the resources in the :class:`~arbor.context`, but without requiring you to +know the precise configuration of a :class:`~arbor.context` up front. Whether +you run your simulation on your laptop CPU, desktop GPU, CPU cluster of GPU +cluster, using :class:`partition hints` you can just say: +use GPUs, if available. You only have to change the :class:`~arbor.context` to +actually define which hardware Arbor will execute on. + +Step **(14)** creates a :class:`arbor.partition_hint`, and tells it to put 1000 +cells in a groups allocated to GPUs, and to prefer the utilisation of the GPU if +present. In fact, the default distribution strategy of +:class:`arbor.partition_load_balance` already spreads out cells as evenly as +possible over CPUs, and groups (up to 1000) on GPUs, so strictly speaking it was +not necessary to give that part of the hint. Lastly, a dictionary is created +with which hints are assigned to a particular :class:`arbor.cell_kind`. +Different kinds may favor different execution, hence the option. In this +simulation, there are only :class:`arbor.cell_kind.cable`, so we assign the hint +to that kind. + +Step **(15)** creates a :class:`arbor.partition_load_balance` with the recipe, +context and hints created above. Another checkpoint will help us understand how +long creating the load balancer took. .. literalinclude:: ../../python/example/network_ring_gpu.py :language: python @@ -91,38 +114,43 @@ Another checkpoint will help us understand how long creating the load balancer t The simulation ************** -Step **(16)** creates a :class:`arbor.simulation`, sets the spike recorders to record, creates a :term:`handle` -to their eventual results and makes another checkpoint. +Step **(16)** creates a :class:`arbor.simulation`, sets the spike recorders to +record, creates a :term:`handle` to their eventual results and makes another +checkpoint. .. literalinclude:: ../../python/example/network_ring_gpu.py :language: python - :lines: 150-154 + :lines: 150-156 The execution ************* -Step **(17)** runs the simulation. Since we have more cells this time, which are connected in series, -it will take some time for the action potential to propagate. In the :ref:`ring network ` -we could see it takes about 5 ms for the signal to propagate through one cell, so let's set the runtime to -``5*ncells``. Then, another checkpoint, so that we'll know how long the simulation took. +Step **(17)** runs the simulation. Since we have more cells this time, which are +connected in series, it will take some time for the action potential to +propagate. In the :ref:`ring network ` we could see it +takes about 5 ms for the signal to propagate through one cell, so let's set the +runtime to ``5*ncells``. Then, another checkpoint, so that we'll know how long +the simulation took. .. literalinclude:: ../../python/example/network_ring_gpu.py :language: python - :lines: 156-159 + :lines: 158-161 The results *********** -The scientific results should be similar, other than number of cells, to those in :ref:`ring network `, -so we'll not discuss them here. Let's turn our attention to the :class:`~arbor.meter_manager`. +The scientific results should be similar, other than number of cells, to those +in :ref:`ring network `, so we'll not discuss them here. +Let's turn our attention to the :class:`~arbor.meter_manager`. .. literalinclude:: ../../python/example/network_ring_gpu.py :language: python - :lines: 161-163 + :lines: 163-165 -Step **(18)** shows how :class:`arbor.meter_report` can be used to read out the :class:`~arbor.meter_manager`. -It generates a table with the time between checkpoints. As an example, the following table is the result of a run -on a 2019 laptop CPU: +Step **(18)** shows how :class:`arbor.meter_report` can be used to read out the +:class:`~arbor.meter_manager`. It generates a table with the time between +checkpoints. As an example, the following table is the result of a run on a 2019 +laptop CPU: :: diff --git a/doc/tutorial/network_two_cells_gap_junctions.rst b/doc/tutorial/network_two_cells_gap_junctions.rst index 8e1293ec80..241bfb37a6 100644 --- a/doc/tutorial/network_two_cells_gap_junctions.rst +++ b/doc/tutorial/network_two_cells_gap_junctions.rst @@ -3,9 +3,11 @@ Two cells connected via a gap junction ====================================== -In this example, we will set up two cells connected via a gap junction. -The cells have different leak potentials. -We will investigate how the equilibrium potentials of the two cells change because of the gap junction connection. +In this example, we will set up two cells connected via a gap junction. Each of +the cells has a passive leak current as its only dynamics. This plus the gap +junction will produce an equilibrium potential different from both the resting +potentials. We will investigate how the equilibrium potentials of the two cells +change due of the gap junction connection. .. figure:: network_two_cells_gap_junctions_circuit.svg :width: 400 @@ -22,6 +24,9 @@ We will investigate how the equilibrium potentials of the two cells change becau 3. Running the simulation and extracting the results. 4. Adding a gap junction connection. +We assume prior exposure to the concepts of cable cells, recipes, and simple +networks. + Walk-through ************ @@ -29,54 +34,73 @@ We set up a recipe for the simulation of two cells .. literalinclude:: ../../python/example/network_two_cells_gap_junctions.py :language: python - :lines: 13-45 + :lines: 13-37 in which we store the relevant parameters for the two cells, all of which are -shared except the equilibrium potentials in ``Vms``. Next, we define callbacks -to define our cell population: +shared except the equilibrium potentials in ``Vms``. These are used to build +the network. + +Let's quickly check some standard callbacks: -- ``num_cells`` returns the number of cells in the network, ie 2 +- ``num_cells`` returns the number of cells in the network, fixed as 2 - ``cell_kind`` specifies that we handle ``cable_cell`` exclusively. -- ``global_properties`` returns a list of standard parameters based on the defaults of the NEURON simulator. -- ``cell_description`` member function constructs the morphology and sets the properties of the cells as well as the gap junction mechanisms and the discretization policy. It returns the finished ``cable_cell``, matching the ``cell_kind`` callback. +- ``global_properties`` returns a list of standard parameters based on the + defaults of the NEURON simulator. +- ``probes`` record the membrane potential at the cell mid. -.. literalinclude:: ../../python/example/network_two_cells_gap_junctions.py - :language: python - :lines: 49-93 +The two remaining methods are: + +``cell_description`` +-------------------- -We build the network conections, here a single, bidirectional gap junction +We construct a basic, single segment morphology from the ``length`` and +``radius`` parameters. The decor sets the basic parameters and adds the passive +leak current ``pas`` with the given resting value ``Vms[gid]`` and conductivity +``g``. .. literalinclude:: ../../python/example/network_two_cells_gap_junctions.py :language: python - :lines: 97-105 + :lines: 58 -And, finally, we return a set of probes which are passed in during construction +The only new item is the placement of the gap junction endpoint at ``midpoint`` +with the basic, builtin ``gj`` dynamics type (other dynamics may be defined and +used). -.. literalinclude:: ../../python/example/network_two_cells_gap_junctions.py - :language: python - :lines: 108-109 -We parse the command line arguments which are used to set parameters in the recipe +``gap_junctions_on`` +------------------- + +Similar to ``connections_on``, this method returns a list of gap junction +connections and these are defined in the same manner. .. literalinclude:: ../../python/example/network_two_cells_gap_junctions.py :language: python - :lines: 113-147 + :lines: 69-70 + +By ``(gid + 1) % 2`` we define two connections, one per cell, between the cells. +This is due to the uni-directional definition of gap junctions in Arbor. -Next, we define a list of probes and construct the recipe and simulation +Running the simulation +********************** + +To allow runtime configuration, we define a parser for command line arguments +which are used to set parameters in the recipe .. literalinclude:: ../../python/example/network_two_cells_gap_junctions.py :language: python - :lines: 149-154 + :lines: 76-98 + -Having set up the simulation, we setting probe sampling on a regular grid with -width equal to the timestep :math:`dt`. Now, we can run it and access the sampling values +We then set up the simulation and configure sampling width equal to the timestep +:math:`dt`. Now, we can run the network. .. literalinclude:: ../../python/example/network_two_cells_gap_junctions.py :language: python - :lines: 156-178 + :lines: 100-110 All that is left to do is to put this into a plot. The output plot below shows -how the potential of the two cells approaches their equilibrium potentials +how the potential of the two cells approaches their equilibrium potentials, which +can be computed from the given parameters. .. math:: @@ -90,7 +114,7 @@ how the potential of the two cells approaches their equilibrium potentials The full code -************* +************** You can find the full code of the example at ``python/examples/network_two_cells_gap_junctions.py`` diff --git a/doc/tutorial/probe_lfpykit.rst b/doc/tutorial/probe_lfpykit.rst index 5d25710010..b1126b2b2d 100644 --- a/doc/tutorial/probe_lfpykit.rst +++ b/doc/tutorial/probe_lfpykit.rst @@ -5,11 +5,12 @@ Extracellular signals (LFPykit) This example takes elements from other tutorials to create a geometrically detailed single cell model from an SWC morphology file, and adds predictions of -extracellular potentials using the `LFPykit `_ Python library. -LFPykit provides a few different classes facilitating -calculations of extracellular potentials and related electroencephalography (EEG) -and magnetoencephalography (MEG) signals from geometrically detailed neuron models under various assumptions. -These are signals that mainly stem from transmembrane currents. +extracellular potentials using the `LFPykit +`_ Python library. LFPykit provides a +few different classes facilitating calculations of extracellular potentials and +related electroencephalography (EEG) and magnetoencephalography (MEG) signals +from geometrically detailed neuron models under various assumptions. These are +signals that mainly stem from transmembrane currents. .. Note:: @@ -25,16 +26,15 @@ These are signals that mainly stem from transmembrane currents. The line source approximation ----------------------------- -First, let's describe how one can compute extracellular potentials from transmembrane currents of a number of segments, -assuming that each segment can be treated as an equivalent line current source using a formalism invented -by Gary R. Holt and Christof Koch [1]_. -The implementation used in this tutorial rely on :class:`lfpykit.LineSourcePotential`. -This class conveniently defines a 2D linear response matrix -:math:`\mathbf{M}` between transmembrane current array -:math:`\mathbf{I}` (nA) of a neuron model's -geometry (:class:`lfpykit.CellGeometry`) and the -corresponding extracellular electric potential in different extracellular locations -:math:`\mathbf{V}_{e}` (mV) so +First, let's describe how one can compute extracellular potentials from +transmembrane currents of a number of segments, assuming that each segment can +be treated as an equivalent line current source using a formalism invented by +Gary R. Holt and Christof Koch [1]_. The implementation used in this tutorial +rely on :class:`lfpykit.LineSourcePotential`. This class conveniently defines a +2D linear response matrix :math:`\mathbf{M}` between transmembrane current array +:math:`\mathbf{I}` (nA) of a neuron model's geometry +(:class:`lfpykit.CellGeometry`) and the corresponding extracellular electric +potential in different extracellular locations :math:`\mathbf{V}_{e}` (mV) so .. math:: \mathbf{V}_{e} = \mathbf{M} \mathbf{I} @@ -60,16 +60,18 @@ electrode point contact to the axis of the line segment is denoted segment is denoted :math:`h_{ji}` and longitudinal distance from the other end of the segment is denoted :math:`l_{ji}= L_i + h_{ji}`. -.. Note:: - - **Assumptions:** +.. admonition:: Assumptions - 1. The extracellular conductivity :math:`\sigma` is infinite, homogeneous, frequency independent (linear) and isotropic - 2. Each segment is treated as a straight line source with homogeneous current density between its start and end point coordinate. - Although Arbor allows segments to be defined as conical frusta with varying radius, we shall assume that any variation in - radius is small relative to overall segment length. + 1. The extracellular conductivity :math:`\sigma` is infinite, homogeneous, + frequency independent (linear) and isotropic + 2. Each segment is treated as a straight line source with homogeneous + current density between its start and end point coordinate. Although + Arbor allows segments to be defined as conical frusta with varying + radius, we shall assume that any variation in radius is small relative to + overall segment length. 3. Each extracellular measurement site is treated as a point - 4. The minimum distance to a line source is set equal to the average segment radius to avoid singularities. + 4. The minimum distance to a line source is set equal to the average segment + radius to avoid singularities. .. _tutorial_lfpykit-model: @@ -85,13 +87,14 @@ First we import some required modules: .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 14-17 + :lines: 15-18 -We define a basic :class:`Recipe ` class, holding a cell and three probes (voltage, stimulus current and total current): +We define a basic :class:`Recipe ` class, holding a cell and three +probes (voltage, stimulus current and total current): .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 22-54 + :lines: 24-51 Then, load a morphology on ``SWC`` file format (interpreted according to :ref:`Arbor's specifications `). Similar to the tutorial :ref:`"A simple single cell model" `, @@ -103,89 +106,81 @@ we use the morphology file in ``python/example/single_cell_detailed.swc``: A graphic depiction of ``single_cell_detailed.swc``. -Pass the filename as an argument to the simulation script: - -.. literalinclude:: ../../python/example/probe_lfpykit.py - :language: python - :lines: 57-66 - -As a target for a current stimuli we define an :class:`arbor.location` : +Read the morphology from a file or use the default .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 68-69 + :lines: 54-65 -Next, we let a basic function define our cell model, taking the morphology and clamp location as -input. -The function defines various attributes (:class:`~arbor.label_dict`, :class:`~arbor.decor`) for -the cell model, -sets sinusoid current clamp as stimuli using :class:`~arbor.iclamp` -defines discretization policy (:class:`~arbor.cv_policy_fixed_per_branch`) -and returns the corresponding :class:`~arbor.place_pwlin` and :class:`~arbor.cable_cell` objects for use later: +As a target for a current stimulus we define .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 72-114 + :lines: 67-68 -Store the function output: +Next, we define our cell model, based on the morphology and clamp location. We +set various attributes (:class:`~arbor.label_dict`, :class:`~arbor.decor`) for +the cell model, attaches a sinusoid current using :class:`~arbor.iclamp`, and +sets discretization policy ats :class:`~arbor.cv_policy_fixed_per_branch`. .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 117-118 + :lines: 70-94 -Next, we instantiate :class:`Recipe`, and execute the model for a few hundred ms, -sampling the different signals every 1 ms: +Next, we instantiate :class:`Recipe`, configure the sampling, and execute the +model for a few hundred ms: .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 120-133 + :lines: 99-112 -Extract recorded membrane voltages, electrode and transmembrane currents. -Note that membrane voltages at branch points and intersections between CVs are dropped as -we only illustrate membrane voltages of segments with finite lengths. +Extract recorded membrane voltages, electrode and transmembrane currents. Note +that membrane voltages at branch points and intersections between CVs are +dropped as we only illustrate membrane voltages of segments with finite lengths. .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 135-150 + :lines: 114-133 -Finally we sum the stimulation and transmembrane currents, allowing the stimuli to mimic a synapse -current embedded in the membrane itself rather than an intracellular electrode current: +Finally we sum the stimulation and transmembrane currents, allowing the stimuli +to mimic a synapse current embedded in the membrane itself rather than an +intracellular electrode current: .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 152-158 + :lines: 135-137 .. _tutorial_lfpykit-lfpykit: Compute extracellular potentials -------------------------------- -Here we utilize the LFPykit library to map -transmembrane currents recorded during the -simulation to extracellular potentials in vicinity to the cell. -We shall account for every segment in each CV using the so-called line-source approximation described :ref:`above `. +Here we utilize the LFPykit library to map trans-membrane currents recorded +during the simulation to extracellular potentials in vicinity to the cell. We +shall account for every segment in each CV using the so-called line-source +approximation described :ref:`above `. -First we define a couple of inherited classes to interface LFPykit -(as this library is not solely written for Arbor). -Starting with a class inherited from :class:`lfpykit.CellGeometry`: +First, we define a couple of inherited classes to interface with LFPykit, as +this library is not solely written for Arbor. Starting with a class inherited +from :class:`lfpykit.CellGeometry`: .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 168-200 + :lines: 145-180 Then, a class inherited from :class:`lfpykit.LineSourcePotential`. Other use cases may inherit from any other parent class defined in :mod:`lfpykit.models` in a similar manner: .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 202-251 + :lines: 183-231 -With these two classes one may then compute extracellular potentials from transmembrane -currents in space with a few lines of code: +With these two classes one may then compute extracellular potentials from +transmembrane currents in space with a few lines of code: .. literalinclude:: ../../python/example/probe_lfpykit.py :language: python - :lines: 254-276 + :lines: 234-256 .. _tutorial_lfpykit-illustration: diff --git a/doc/tutorial/single_cell_allen.rst b/doc/tutorial/single_cell_allen.rst index b1fae81faf..652e919312 100644 --- a/doc/tutorial/single_cell_allen.rst +++ b/doc/tutorial/single_cell_allen.rst @@ -37,33 +37,47 @@ In the "Biophysical - all active" model (zip) file you'll find: We will replicate the "Sweep 35" experiment, which applies a current of 150 nA for a duration of 1 s. -The morphology --------------- +Morphology and Labels +--------------------- .. literalinclude:: ../../python/example/single_cell_allen.py :language: python :dedent: - :lines: 74-80 + :lines: 80-81 -Step **(1)** loads the ``swc`` file using :func:`arbor.load_swc_neuron`. Since the ``swc`` specification is informal, a few different interpretations exist, and we use the appropriate one. The interpretations are described :ref:`here `. +Step **(1)** loads the ``swc`` file using :func:`arbor.load_swc_neuron`. Since +the ``swc`` specification is informal, a few different interpretations exist, +and we use the appropriate one. The interpretations are described :ref:`here +`. -Step **(2)** sets the labels to the defaults of the ``swc`` -`specification `_, -plus a label for the midpoint of the soma. (You can verify in the ``swc`` file, the first branch is the soma.) +.. literalinclude:: ../../python/example/single_cell_allen.py + :language: python + :dedent: + :lines: 83-86 + +Step **(2)** sets the labels to the defaults of the ``swc`` `specification +`_, +plus a label for the midpoint of the soma. (You can verify in the ``swc`` file, +the first branch is the soma.) The parameter fit ----------------- -The most complicated part is transferring the values for the appropriate parameters in parameter fit file to an -:class:`arbor.decor`. The file file is a ``json`` file, which is fortunate; Python comes with a ``json`` package -in its standard library. The `passive` and `conditions` block contains cell-wide defaults, while the `genome` -section contains the parameters for all the mechanism properties. In certain cases, parameters names include the -mechanism name, so some processing needs to take place. - -Step **(3)** shows the precise steps needed to load the fit parameter file into a list of global properties, -region specific properties, reversal potentials, and mechanism parameters. This is not a generic function that will successfully load any Allen model, but it can be used as a starting point. The function composes 4 components out of the ``json`` file: - -1. global electro-physiological parameters, +The most complicated part is transferring the values for the appropriate +parameters in parameter fit file to an :class:`arbor.decor`. The file file is a +``json`` file, which is fortunate; Python comes with a ``json`` package in its +standard library. The `passive` and `conditions` block contains cell-wide +defaults, while the `genome` section contains the parameters for all the +mechanism properties. In certain cases, parameters names include the mechanism +name, so some processing needs to take place. + +Step **(3)** shows the precise steps needed to load the fit parameter file into +a list of global properties, region specific properties, reversal potentials, +and mechanism parameters. This is not a generic function that will successfully +load any Allen model, but it can be used as a starting point. The function +composes 4 components out of the ``json`` file: + +1. global electro-physiological parameters 2. a set of electro-physiological parameters per region, 3. a set of reversal potentials per ion species and region, 4. a set of mechanisms with parameters per region. @@ -71,34 +85,44 @@ region specific properties, reversal potentials, and mechanism parameters. This .. literalinclude:: ../../python/example/single_cell_allen.py :language: python :dedent: - :lines: 12-72,82,83 + :lines: 19-76 The decor --------- -With the ingredients for the :class:`arbor.decor` extracted, we continue with the function that will return the cable cell from the model as an :class:`arbor.cable_cell`. +With the ingredients for the :class:`arbor.decor` extracted, we continue with +the function that will return the cable cell from the model as an +:class:`arbor.cable_cell`. .. literalinclude:: ../../python/example/single_cell_allen.py :language: python :dedent: - :lines: 85-117 + :lines: 79-138 Step **(4)** creates an empty :class:`arbor.decor`. -Step **(5)** assigns global (cell-wide) properties using :func:`arbor.decor.set_property`. In addition, initial -internal and external calcium concentrations are set, and configured to be determined by the Nernst equation. +Step **(5)** assigns global (cell-wide) properties using +:func:`arbor.decor.set_property`. In addition, initial internal and external +calcium concentrations are set, and configured to be determined by the Nernst +equation. .. note:: - Setting the calcium reversal potential to be determined by the Nernst equation has to be done manually, in order to mirror - `an implicit Neuron behavior `_, - for which the fit parameters were obtained. This behavior can be stated as the following rule: - If the internal or external concentration of an ion is written, and its reversal potential is read but not - written, then the Nernst equation is used continuously during the simulation to update the reversal potential of - the ion according to the Nernst equation + Setting the calcium reversal potential to be determined by the Nernst + equation has to be done manually, in order to mirror `an implicit Neuron + behavior + `_, + for which the fit parameters were obtained. This behavior can be stated as + the following rule: -Step **(6)** overrides the global properties for all *regions* for which the fit parameters file specifies adapted -values. Regional properties are :func:`painted `, and are painted over (e.g. replacing) the defaults. + If the internal or external concentration of an ion is written, and its + reversal potential is read but not written, then the Nernst equation is used + continuously during the simulation to update the reversal potential of the + ion according to the Nernst equation + +Step **(6)** overrides the global properties for all *regions* for which the fit +parameters file specifies adapted values. Regional properties are :func:`painted +`, and are painted over (e.g. replacing) the defaults. Step **(7)** sets the regional reversal potentials. @@ -106,10 +130,12 @@ Step **(8)** assigns the regional mechanisms. Now that the electro-physiology is all set up, let's move on to the experimental setup. -Step **(9)** configures the :class:`stimulus ` of 150 nA for a duration of 1 s, starting after 200 ms -of the start of the simulation. We'll also install a :class:`arbor.threshold_detector` that triggers at -40 mV. (The -location is usually the soma, as is confirmed by coordinates found in the experimental dataset at -``488683423.nwb/general/intracellular_ephys/Electrode 1/location``) +Step **(9)** configures the :class:`stimulus ` of 150 nA for a +duration of 1 s, starting after 200 ms of the start of the simulation. We'll +also install a :class:`arbor.threshold_detector` that triggers at -40 mV. (The +location is usually the soma, as is confirmed by coordinates found in the +experimental dataset at ``488683423.nwb/general/intracellular_ephys/Electrode +1/location``) Step **(10)** specifies a maximum :term:`control volume` length of 20 μm. @@ -121,45 +147,56 @@ The model .. literalinclude:: ../../python/example/single_cell_allen.py :language: python :dedent: - :lines: 120-131 + :lines: 141-152 -Step **(12)** instantiates the :class:`arbor.cable_cell` and an :class:`arbor.single_cell_model`. +Step **(12)** instantiates the :class:`arbor.cable_cell` and an +:class:`arbor.single_cell_model`. -Step **(13)** shows how to install a probe to the ``"midpoint"``, with a sampling frequency of 200 kHz. +Step **(13)** shows how to install a probe to the ``"midpoint"``, with a +sampling frequency of 200 kHz. -Step **(14)** installs the :class:`arbor.allen_catalogue`, thereby making its mechanisms available to the definitions added to the decor. +Step **(14)** installs the :class:`arbor.allen_catalogue`, thereby making its +mechanisms available to the definitions added to the decor. Step **(15)** starts the simulation for a duration of 1.4 s and a timestep of 5 ms. The result ---------- -Let's look at the result! In step **(16)** we first load the reference generated with Neuron and the AllenSDK. -Then, we extract Arbor's output, accessible after the simulation ran at -:class:`arbor.single_cell_model.traces`. Then, we plot them, together with the :class:`arbor.single_cell_model.spikes` in step **(17)**. +Let's look at the result! In step **(16)** we first load the reference generated +with Neuron and the AllenSDK. Then, we extract Arbor's output, accessible after +the simulation ran at :class:`arbor.single_cell_model.traces`. Then, we plot +them, together with the :class:`arbor.single_cell_model.spikes` in step +**(17)**. .. literalinclude:: ../../python/example/single_cell_allen.py :language: python :dedent: - :lines: 133- + :lines: 154- .. figure:: single_cell_allen_result.svg :width: 400 :align: center - Plot of experiment 35 of the Allen model, compared to the reference generated by the AllenSDK. In green: the threshold detector output; in shaded grey: the stimulus. + Plot of experiment 35 of the Allen model, compared to the reference + generated by the AllenSDK. In green: the threshold detector output; in + shaded grey: the stimulus. .. note:: - The careful observer notices that this trace does not match the experimental data shown on the Allen website - (or in the ``488683423.nwb`` file). Sweep 35 clearly has 5 spikes, not 4. That is because in the Allen SDK, - the axon in the ``swc`` file is replaced with a stub, see - `this paper `_ and `this AllenSDK Issue `_. - However, that adapted morphology is not exportable back to a modified ``swc`` file. When we tried to mimic - the procedure, we did not obtain the experimental trace. - - Therefore, we used the unmodified morphology in Arbor *and* the Neuron reference (by commenting out the - changes the Allen SDK makes to the morphology) in order to make a 1:1 comparison possible. + The careful observer notices that this trace does not match the experimental + data shown on the Allen website (or in the ``488683423.nwb`` file). Sweep 35 + clearly has 5 spikes, not 4. That is because in the Allen SDK, the axon in the + ``swc`` file is replaced with a stub, see `this paper + `_ and `this + AllenSDK Issue `_. + However, that adapted morphology is not exportable back to a modified ``swc`` + file. When we tried to mimic the procedure, we did not obtain the experimental + trace. + + Therefore, we used the unmodified morphology in Arbor *and* the Neuron + reference (by commenting out the changes the Allen SDK makes to the + morphology) in order to make a 1:1 comparison possible. The full code ------------- diff --git a/doc/tutorial/single_cell_detailed.rst b/doc/tutorial/single_cell_detailed.rst index d7cc8f0868..c5e638ba36 100644 --- a/doc/tutorial/single_cell_detailed.rst +++ b/doc/tutorial/single_cell_detailed.rst @@ -41,39 +41,54 @@ We begin by constructing the following morphology: :width: 600 :align: center -This can be done by manually building a segment tree: +This can be done by manually building a segment tree. The important bit here is +that ``append`` will take an id to attach to and return the newly added id. This +is exceptionally handy when building a tree structure, as we can elect to +remember or overwrite the last id. Alternatively, you could use numeric ids -- +they are just sequentially numbered by insertion order --, but we find that this +becomes tedious quickly. The image above shows the numeric ids for the specific +insertion order below, but different orders will produce the same morphology. .. code-block:: python - import arbor - from arbor import mpoint - from arbor import mnpos - - # Define the morphology by manually building a segment tree - + # Construct an empty segment tree. tree = arbor.segment_tree() - # Start with segment 0: a cylindrical soma with tag 1 - tree.append(mnpos, mpoint(0.0, 0.0, 0.0, 2.0), mpoint( 40.0, 0.0, 0.0, 2.0), tag=1) - # Construct the first section of the dendritic tree with tag 3, - # comprised of segments 1 and 2, attached to soma segment 0. - tree.append(0, mpoint(40.0, 0.0, 0.0, 0.8), mpoint( 80.0, 0.0, 0.0, 0.8), tag=3) - tree.append(1, mpoint(80.0, 0.0, 0.0, 0.8), mpoint(120.0, -5.0, 0.0, 0.8), tag=3) - # Construct the rest of the dendritic tree: segments 3, 4 and 5. - tree.append(2, mpoint(120.0, -5.0, 0.0, 0.8), mpoint(200.0, 40.0, 0.0, 0.4), tag=3) - tree.append(3, mpoint(200.0, 40.0, 0.0, 0.4), mpoint(260.0, 60.0, 0.0, 0.2), tag=3) - tree.append(2, mpoint(120.0, -5.0, 0.0, 0.5), mpoint(190.0, -30.0, 0.0, 0.5), tag=3) - # Construct a special region of the tree made of segments 6, 7, and 8 - # differentiated from the rest of the tree using tag 4. - tree.append(5, mpoint(190.0, -30.0, 0.0, 0.5), mpoint(240.0, -70.0, 0.0, 0.2), tag=4) - tree.append(5, mpoint(190.0, -30.0, 0.0, 0.5), mpoint(230.0, -10.0, 0.0, 0.2), tag=4) - tree.append(7, mpoint(230.0, -10.0, 0.0, 0.2), mpoint(360.0, -20.0, 0.0, 0.2), tag=4) - # Construct segments 9 and 10 that make up the axon with tag 2. - # Segment 9 is at the root, where its proximal end will be connected to the - # proximal end of the soma segment. - tree.append(mnpos, mpoint( 0.0, 0.0, 0.0, 2.0), mpoint( -70.0, 0.0, 0.0, 0.4), tag=2) - tree.append(9, mpoint(-70.0, 0.0, 0.0, 0.4), mpoint(-100.0, 0.0, 0.0, 0.4), tag=2) - + # The root of the tree has no parent + root = arbor.mnpos + + # The root segment: a cylindrical soma with tag 1 + # NOTE: append returns the added segment's id, which we can use to + # attach the next segments. + soma = tree.append(root, (0.0, 0.0, 0.0, 2.0), (40.0, 0.0, 0.0, 2.0), tag=1) + + # Attach the first section of the dendritic tree with tag 3 to the soma + # up to the first fork + dend = tree.append(soma, (40.0, 0.0, 0.0, 0.8), ( 80.0, 0.0, 0.0, 0.8), tag=3) + dend = tree.append(dend, (80.0, 0.0, 0.0, 0.8), (120.0, -5.0, 0.0, 0.8), tag=3) + + # Construct upper part of the first fork + # NOTE: We do not overwrite the parent here, as we need to attach the + # lower fork later. Instead we use new names for this branch. + dend_u = tree.append(dend, (120.0, -5.0, 0.0, 0.8), (200.0, 40.0, 0.0, 0.4), tag=3) + dend_u = tree.append(dend_u, (200.0, 40.0, 0.0, 0.4), (260.0, 60.0, 0.0, 0.2), tag=3) + + # Construct lower part of the first fork + dend_l = tree.append(dend, (120.0, -5.0, 0.0, 0.5), (190.0, -30.0, 0.0, 0.5), tag=3) + + # Attach another fork to the last segment, ``p``. + # Upper part + dend_lu = tree.append(dend_l, (190.0, -30.0, 0.0, 0.5), (240.0, -70.0, 0.0, 0.2), tag=4) + # Lower part + dend_ll = tree.append(dend_l, (190.0, -30.0, 0.0, 0.5), (230.0, -10.0, 0.0, 0.2), tag=4) + dend_ll = tree.append(dend_ll, (230.0, -10.0, 0.0, 0.2), (360.0, -20.0, 0.0, 0.2), tag=4) + + # Construct the axon with tag 2, attaching to the root ``mnpos``, where its + # proximal end will be connected to the proximal end of the soma segment implicitly. + axon = tree.append(root, (0.0, 0.0, 0.0, 2.0), (-70.0, 0.0, 0.0, 0.4), tag=2) + axon = tree.append(axon, (-70.0, 0.0, 0.0, 0.4), (-100.0, 0.0, 0.0, 0.4), tag=2) + + # Turn segment tree into a morphology. morph = arbor.morphology(tree); The same morphology can be represented using an SWC file (interpreted according @@ -97,31 +112,39 @@ The morphology can then be loaded from ``single_cell_detailed.swc`` in the follo .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 10-20 + :lines: 11-22 + +We allow passing of any valid SWC file so you can easily experiment with your +own morphologies but employ some Python magic to grab the default file which +lives in the same directory as the example. In general, using a data file for +constructing morphologies should be preferred. + The label dictionary ^^^^^^^^^^^^^^^^^^^^ -Next, we can define **region** and **location** expressions and give them labels. -The regions and locations are defined using an Arbor-specific DSL, and the labels -can be stored in a :class:`arbor.label_dict`. +Next, we can define **region** and **location** expressions and give them +labels. The regions and locations are defined using an Arbor-specific DSL, and +the labels can be stored in a :class:`arbor.label_dict`. .. Note:: - The expressions in the label dictionary don't actually refer to any concrete regions - or locations of the morphology at this point. They are merely descriptions that can be - applied to any morphology, and depending on its geometry, they will generate different - regions and locations. However, we will show some figures illustrating the effect of - applying these expressions to the above morphology, in order to better visualize the - final cell. + The expressions in the label dictionary don't actually refer to any concrete + regions or locations of the morphology at this point. They are merely queries + that can be applied to any morphology, and depending on its geometry, they + will generate different regions and locations. However, we will show some + figures illustrating the effect of applying these expressions to the above + morphology, in order to better visualize the final cell. - More information on region and location expressions is available :ref:`here `. + More information on region and location expressions is available :ref:`here + `. The SWC file format allows association of ``tags`` with parts of the morphology and reserves tag values 1-4 for commonly used sections (see `here `__ -for the SWC file format). In Arbor, these tags can be added to a :class:`arbor.label_dict` using -the :meth:`~arbor.label_dict.add_swc_tags` method, which will define +for the SWC file format). In Arbor, these tags can be added to a +:class:`arbor.label_dict` using the :meth:`~arbor.label_dict.add_swc_tags` +method, which will define .. list-table:: Default SWC Tags :widths: 25 25 50 @@ -162,9 +185,9 @@ Both ways will generate the following regions when applied to the previously def From left to right: regions "soma", "axon", "dend" and "last" -We can also define a region that represents the whole cell; and to make things a bit more interesting, -a region that includes the parts of the morphology that have a radius greater than 1.5 μm. This is done -in the following way: +We can also define a region that represents the whole cell; and to make things a +bit more interesting, a region that includes the parts of the morphology that +have a radius greater than 1.5 μm. This is done in the following way: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python @@ -227,6 +250,23 @@ This will generate the following 2 locsets when applied to the previously define Left: locset "custom_terminal"; right: locset "axon_terminal" +.. note:: + + We show the use of the label dictionary here, but everywhere a label is + valid, you can use the DSL expression directly, so + + .. code-block:: python + + decor.paint('(all)', arbor.density("pas")) + + is perfectly acceptable, as is + + .. code-block:: python + + all = '(all)' + decor.paint(all, arbor.density("pas")) + + The decorations ^^^^^^^^^^^^^^^ @@ -243,79 +283,87 @@ region or location expressions. More information on decors can be found :ref:`here `. -The decor object can have default values for properties, which can then be overridden on specific -regions. It is in general better to explicitly set all the default properties of your cell, -to avoid the confusion to having simulator-specific default values. This will therefore be our first -step: +The decor object can have default values for properties, which can then be +overridden on specific regions. It is in general better to explicitly set all +the default properties of your cell, to avoid the confusion to having +simulator-specific default values. This will therefore be our first step: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 48-52 - -We have set the default initial membrane voltage to -55 mV; the default initial -temperature to 300 K; the default axial resistivity to 35.4 Ω·cm; and the default membrane -capacitance to 0.01 F/m². - -We also set the initial properties of the *na* and *k* ions because they will be utilized -by the density mechanisms that we will be adding shortly. -For both ions we set the default initial concentration and external concentration measures in mM; -and we set the default initial reversal potential in mV. For the *na* ion, we additionally indicate -the the progression on the reversal potential during the simulation will be dictated by the -`Nernst equation `_. - -It happens, however, that we want the temperature of the "custom" region defined in the label -dictionary earlier to be colder, and the initial voltage of the "soma" region to be higher. -We can override the default properties by *painting* new values on the relevant regions using -:meth:`arbor.decor.paint`. + :lines: 48-62 + +We have set the default initial membrane voltage mV; the default initial +temperature; the default axial resistivity to; and the default membrane +capacitance to. Also, the initial properties of the *na* and *k* ions have been +changed. They will be utilized by the density mechanisms that we will be adding +shortly. For both ions we set the default initial concentration and external +concentration measures; and we set the default initial reversal potential. For +the *na* ion, we additionally indicate the the progression on the reversal +potential during the simulation will be dictated by the `Nernst equation +`_. In the case that defaults are +not set at the cell level, there is also a global default which we will define a +bit later. + +It happens, however, that we want the temperature of the "custom" region defined +in the label dictionary earlier to be colder, and the initial voltage of the +"soma" region to be higher. We can override the default properties by *painting* +new values on the relevant regions using :meth:`arbor.decor.paint`. .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 53-55 + :lines: 63-65 -With the default and initial values taken care of, we now add some density mechanisms. Let's *paint* -a *pas* density mechanism everywhere on the cell using the previously defined "all" region; an *hh* -density mechanism on the "custom" region; and an *Ih* density mechanism on the "dend" region. The *Ih* -mechanism has a custom 'gbar' parameter. +With the default and initial values taken care of, we now add some density +mechanisms. Let's *paint* a *pas* density mechanism everywhere on the cell using +the previously defined "all" region; an *hh* density mechanism on the "custom" +region; and an *Ih* density mechanism on the "dend" region. The *Ih* mechanism +has a custom 'gbar' parameter. .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 8,56-59 + :lines: 66-69 -The decor object is also used to *place* stimuli and threshold detectors on the cell using :meth:`arbor.decor.place`. -We place 3 current clamps of 2 nA on the "root" locset defined earlier, starting at time = 10, 30, 50 ms and -lasting 1ms each. As well as threshold detectors on the "axon_terminal" locset for voltages above -10 mV. -Every placement gets a label. The labels of detectors and synapses are used to form connection from and to them -in the recipe. +The decor object is also used to *place* stimuli and threshold detectors on the +cell using :meth:`arbor.decor.place`. We place 3 current clamps of 2 nA on the +"root" locset defined earlier, starting at time = 10, 30, 50 ms and lasting 1ms +each. As well as threshold detectors on the "axon_terminal" locset for voltages +above -10 mV. Every placement gets a label. The labels of detectors and synapses +are used to form connection from and to them in the recipe. .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 60-64 + :lines: 70-74 .. Note:: - The number of individual locations in the ``'axon_terminal'`` locset depends on the underlying morphology and the - number of axon branches in the morphology. The number of detectors that get added on the cell is equal to the number - of locations in the locset, and the label ``'detector'`` refers to all of them. If we want to refer to a single - detector from the group (to form a network connection for example), we need a :py:class:`arbor.selection_policy`. - -Finally, there's one last property that impacts the behavior of a model: the discretisation. -Cells in Arbor are simulated as discrete components called control volumes (CV). The size of -a CV has an impact on the accuracy of the results of the simulation. Usually, smaller CVs -are more accurate because they simulate the continuous nature of a neuron more closely. - -The user controls the discretisation using a :class:`arbor.cv_policy`. There are a few different policies to -choose from, and they can be composed with one another. In this example, we would like the "soma" region -to be a single CV, and the rest of the morphology to be comprised of CVs with a maximum length of 1 μm: + The number of individual locations in the ``'axon_terminal'`` locset depends + on the underlying morphology and the number of axon branches in the + morphology. The number of detectors that get added on the cell is equal to + the number of locations in the locset, and the label ``'detector'`` refers to + all of them collectively. If we want to refer to a single detector from the + group (to form a network connection for example), we need a + :py:class:`arbor.selection_policy`. + +Finally, there's one last property that impacts the behavior of a model: the +discretisation. Cells in Arbor are simulated as discrete components called +control volumes (CV). The size of a CV has an impact on the accuracy of the +results of the simulation. Usually, smaller CVs are more accurate because they +simulate the continuous nature of a neuron more closely. + +The user controls the discretisation using a :class:`arbor.cv_policy`. There are +a few different policies to choose from, and they can be composed with one +another. In this example, we would like the "soma" region to be a single CV, and +the rest of the morphology to be comprised of CVs with a maximum length of 1 μm: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 65-66 + :lines: 75-76 Finally, we create the cell. .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 68-70 + :lines: 79-80 The model ********* @@ -324,75 +372,82 @@ Having created the cell, we construct an :class:`arbor.single_cell_model`. .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 72-74 + :lines: 82-83 The global properties ^^^^^^^^^^^^^^^^^^^^^ The global properties of a single cell model include: -1. The **mechanism catalogue**: A mechanism catalogue is a collection of density and point - mechanisms. Arbor has 3 built-in mechanism catalogues: ``default``, ``allen`` and ``bbp``. The mechanism - catalogue in the global properties of the model must include the catalogues of all the - mechanisms painted on the cell decor. The default is to use the ``default_catalogue``. +1. The **mechanism catalogue**: A mechanism catalogue is a collection of density + and point mechanisms. Arbor has 3 built-in mechanism catalogues: ``default``, + ``allen`` and ``bbp``. The mechanism catalogue in the global properties of + the model must include the catalogues of all the mechanisms painted on the + cell decor. The default is to use the ``default_catalogue``. -2. The default **parameters**: The initial membrane voltage; the initial temperature; the - axial resistivity; the membrane capacitance; the ion parameters; and the discretisation - policy. +2. The default **parameters**: The initial membrane voltage; the initial + temperature; the axial resistivity; the membrane capacitance; the ion + parameters; and the discretisation policy. .. Note:: - You may notice that the same parameters can be set both at the cell level and at - the model level. This is intentional. The model parameters apply to all the cells in a model, - whereas the cell parameters apply only to that specific cell. - - The idea is that the user is able to define a set of global properties for all cells in a model - which can then be overridden for individual cells, and overridden yet again on certain - regions of the cells. - - You may now be wondering why this is needed for the `single cell model` where there is only one - cell by design. You can use this feature to ease moving from simulating a set of single cell models - to simulating a network of these cells. - For example, a user may choose to individually test several single cell models before simulating - their interactions. By using the same global properties for each *model*, and customizing the *cell* - global properties, it becomes possible to use the cell descriptions of each cell, unchanged, in a + You may notice that the same parameters can be set both at the cell level and + at the model level. This is intentional. The model parameters apply to all + the cells in a model, whereas the cell parameters apply only to that specific + cell. + + The idea is that the user is able to define a set of global properties for + all cells in a model which can then be overridden for individual cells, and + overridden yet again on certain regions of the cells. + + You may now be wondering why this is needed for the `single cell model` where + there is only one cell by design. You can use this feature to ease moving + from simulating a set of single cell models to simulating a network of these + cells. For example, a user may choose to individually test several single + cell models before simulating their interactions. By using the same global + properties for each *model*, and customizing the *cell* global properties, it + becomes possible to use the cell descriptions of each cell, unchanged, in a larger network model. -Earlier in the example we mentioned that it is better to explicitly set all the default properties -of your cell, while that is true, it is better yet to set the default properties of the entire -model: +Earlier in the example we mentioned that it is better to explicitly set all the +default properties of your cell, while that is true, it is better yet to set the +default properties of the entire model: .. _tutorialsinglecellswc-gprop: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 76-80 + :lines: 85-94 -We set the same properties as we did earlier when we were creating the *decor* of the cell, except -for the initial membrane voltage, which is -65 mV as opposed to -55 mV. +We set the same properties as we did earlier when we were creating the *decor* +of the cell, except for the initial membrane voltage, which is -65 mV as opposed +to -55 mV. -During the decoration step, we also made use of 3 mechanisms: *pas*, *hh* and *Ih*. As it happens, -the *pas* and *hh* mechanisms are in the default Arbor catalogue, whereas the *Ih* mechanism is in -the "allen" catalogue. We can extend the default catalogue as follow: +During the decoration step, we also made use of 3 mechanisms: *pas*, *hh* and +*Ih*. As it happens, the *pas* and *hh* mechanisms are in the default Arbor +catalogue, whereas the *Ih* mechanism is in the "allen" catalogue. We can extend +the default catalogue as follow: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 82-86 + :lines: 96-100 Now all three mechanisms in the *decor* object have been made available to the model. The probes ^^^^^^^^^^ -The model is almost ready for simulation. Except that the only output we would be able to -measure at this point is the spikes from the threshold detectors placed in the decor. +The model itself is ready for simulation. Except that the only output we would +be able to measure at this point is the spikes from the threshold detectors +placed in the decor. -The :class:`arbor.single_cell_model` can also measure the voltage on specific locations of the cell. -We can indicate the location we would like to probe using labels from the :class:`label_dict`: +The :class:`arbor.single_cell_model` can also measure the voltage on specific +locations of the cell. We can indicate the location we would like to probe using +labels from the :class:`label_dict`: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 88-92 + :lines: 102-104 The simulation ^^^^^^^^^^^^^^ @@ -401,31 +456,35 @@ The cell and model descriptions are now complete and we can run the simulation: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 94-102 + :lines: 106-107 The results ^^^^^^^^^^^ -Finally we move on to the data collection segment of the example. We have added a threshold detector -on the "axon_terminal" locset. The :class:`arbor.single_cell_model` automatically registers all -spikes on the cell from all threshold detectors on the cell and saves the times at which they occurred. +Finally we move on to the data collection segment of the example. We have added +a threshold detector on the "axon_terminal" locset. The +:class:`arbor.single_cell_model` automatically registers all spikes on the cell +from all threshold detectors on the cell and saves the times at which they +occurred. .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 98-102 + :lines: 109-112 -A more interesting result of the simulation is perhaps the output of the voltage probe previously -placed on the "custom_terminal" locset. The model saves the output of the probes as [time, value] -pairs which can then be plotted. We use `pandas` and `seaborn` for the plotting, but the user can -choose the any other library: +A more interesting result of the simulation is perhaps the output of the voltage +probe previously placed on the "custom_terminal" locset. The model saves the +output of the probes as [time, value] pairs which can then be plotted. We use +`pandas` and `seaborn` for the plotting, but the user can choose the any other +library: .. literalinclude:: ../../python/example/single_cell_detailed.py :language: python - :lines: 5,6,104- + :lines: 114-138 -The following plot is generated. The orange line is slightly delayed from the blue line, which is -what we'd expect because branch 4 is longer than branch 3 of the morphology. We also see 3 spikes, -corresponding to each of the current clamps placed on the cell. +The following plot is generated. The orange line is slightly delayed from the +blue line, which is what we'd expect because branch 4 is longer than branch 3 of +the morphology. We also see 3 spikes, corresponding to each of the current +clamps placed on the cell. .. figure:: single_cell_detailed_result.svg :width: 400 diff --git a/doc/tutorial/single_cell_detailed_recipe.rst b/doc/tutorial/single_cell_detailed_recipe.rst index 959d06241c..a7bf6b2157 100644 --- a/doc/tutorial/single_cell_detailed_recipe.rst +++ b/doc/tutorial/single_cell_detailed_recipe.rst @@ -20,75 +20,82 @@ This time, we'll learn a bit more about setting up advanced features using a :cl The cell ******** -We can copy the cell description code or reuse ``single_cell_detailed.swc`` from the -:ref:`original example ` where it is explained in detail. +We reuse the cell construction code from :ref:`original example +` where it is explained in detail. +Constructing cells outside the recipe is not required, but may be +convenient and potentially faster if many copies of the same cell +are required. The recipe ********** -The :class:`arbor.single_cell_model` of the original example created a :class:`arbor.recipe` under -the hood, and abstracted away the details so we were unaware of its existence. In this example, we will -examine the recipe in detail: how to create one, and why it is needed. +The :class:`arbor.single_cell_model` used in the original example created a +:class:`arbor.recipe` under the hood, and abstracted away the details so we were +unaware of its existence. In this example, we will examine the recipe in detail: +how to create one and why it is needed. .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py :language: python - :lines: 74-109 + :lines: 78-126 Let's go through the recipe point by point. -Step **(5)** creates a ``single_recipe`` class that inherits from :class:`arbor.recipe`. -:meth:`arbor.recipe.num_cells`, :meth:`arbor.recipe.cell_kind` and :meth:`arbor.recipe.cell_description` -always have to be implemented by the user. We'll also implement :meth:`arbor.recipe.global_properties` to be able -to decorate :class:`arbor.cell_kind.cable` cells with mechanisms and :meth:`arbor.recipe.probes` to be able to -insert the probe. - -Step **(5.1)** defines the class constructor. As per :class:`arbor.recipe` instructions, we call -``arbor.recipe.__init__(self)`` to ensure correct initialization of memory in the C++ class. - -We then create the ``self.the_props`` variable. This will hold the global properties of the model, which apply -to all the cells in the network. We initialize it with :class:`arbor.cable_global_properties`, which comes with the -``default`` mechanism catalogue built-in. We set all the properties of the system similar to what we did in -the :ref:`original example `. One last important step is to extend ``self.the_props`` -to include the Allen catalogue, because it holds the *Ih* mechanism. The *hh* and *pas* mechanisms came with the -default catalogue. - -Step **(5.2)** overrides the :meth:`~arbor.recipe.num_cells` method. It takes no arguments. We simply return 1, -as we are only simulating one cell in this example. - -Step **(5.3)** overrides the :meth:`~arbor.recipe.cell_kind` method. It takes one argument: ``gid``. -Given the gid, this method returns the kind of the cell. Our defined cell is a -:class:`arbor.cell_kind.cable`, so we simply return that. - -Step **(5.4)** overrides the :meth:`~arbor.recipe.cell_description` method. It takes one argument: ``gid``. -Given the gid, this method returns the cell description which is the cell object passed to the constructor -of the recipe. We return ``cell``, the cell created just above. - -Step **(5.5)** overrides the :meth:`~arbor.recipe.probes` method. It takes one argument: ``gid``. -Given the gid, this method returns all the probes on the cell. The probes can be of many different kinds -measuring different quantities on different locations of the cell. Like in the original example, we will -create the voltage probe at the ``"custom_terminal"`` locset. This probe was registered directly using the -:class:`arbor.single_cell_model` object. Now it has to be explicitly created and registered in the recipe. - -Step **(5.6)** overrides the :meth:`~arbor.recipe.global_properties` method. It takes one argument: ``kind``. -This method returns the default global properties of the model which apply to all cells in the network of -that kind. We only use ``cable`` cells in this example (but there are more) and thus always return a -``cable_cell_properties`` object. We return ``self.the_props`` which we defined in step **(1)**. - -.. Note:: - - You may wonder why the method :meth:`arbor.recipe.cell_kind` is required, since it can be inferred by examining the cell description. - The recipe was designed to allow building simulations efficiently in a distributed system with minimum - communication. Some parts of the model initialization require only the cell kind, - not the full cell description which can be quite expensive to build. Providing these - descriptions separately saves time and resources for the user. - - More information on the recipe can be found :ref:`here `. - -Now we can instantiate a ``single_recipe`` object. +Step **(5.1)** defines the class constructor. As per :class:`arbor.recipe` +instructions, we call ``arbor.recipe.__init__(self)`` as the very first thing. +This is to ensure correct initialization. + +We then create the ``self.the_props`` variable. This will hold the global +properties of the model, which apply to all the cells in the network. We +initialize it with :class:`arbor.cable_global_properties`, which comes with the +``default`` mechanism catalogue built-in. We set all the properties of the +system similar to what we did in the :ref:`original example +`. One last important step is to extend +``self.the_props`` to include the Allen catalogue, because it holds the *Ih* +mechanism. The *hh* and *pas* mechanisms came with the default catalogue. + +Step **(5.2)** overrides the :meth:`~arbor.recipe.num_cells` method. It takes no +arguments and returns the count of cells in the simulation. The global id (gid) +of cells runs between 0 and the value returned from here and is used to query +recipes for cell descriptions. Technically, this method doesn't need overriding, +but the default is zero, resulting in an empty simulation. + +Step **(5.3)** overrides the :meth:`~arbor.recipe.cell_kind` method. It takes +one argument: ``gid``. Given the gid, this method returns the kind of the cell. +Our defined cell is a :class:`arbor.cell_kind.cable`, so we simply return that. +Arbor uses the kind to determine what description is expected from ``cell_description``; +if the two do not match, an error will occur. + +Step **(5.4)** overrides the :meth:`~arbor.recipe.cell_description` method. It +takes one argument: ``gid``. Given the gid, this method returns the cell +description which is the cell object passed to the constructor of the recipe. We +return ``cell``, the cell created just above. + +.. note:: + + While splitting the kind and description into two methods may seem redundant, + it allows Arbor to optimize the simulation layout before constructing any + cells. + +Step **(5.5)** overrides the :meth:`~arbor.recipe.probes` method. It takes one +argument: ``gid``. Given the gid, this method returns all the probes on the +cell. The probes can be of many different kinds measuring different quantities +on different locations of the cell. Like in the original example, we will create +the voltage probe at the ``"custom_terminal"`` locset. This probe was registered +directly using the :class:`arbor.single_cell_model` object. Now it has to be +explicitly created and registered in the recipe. + +Step **(5.6)** overrides the :meth:`~arbor.recipe.global_properties` method. It +takes one argument: ``kind``. This method returns the default global properties +of the model which apply to all cells in the network of that kind. We only use +``cable`` cells in this example (but there are more) and thus always return a +``cable_cell_properties`` object. We return ``self.the_props`` which we defined +in step **(1)**. .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py :language: python - :lines: 112-113 + :lines: 129-130 + +Now we can instantiate a ``single_recipe`` object. The simulation ************** @@ -100,55 +107,60 @@ This was handled by the :class:`arbor.single_cell_model` object in the original .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py :language: python - :lines: 115-122 + :lines: 135-138 -We would like to get a list of the spikes on the cell during the runtime of the simulation, and we would like -to plot the voltage registered by the probe on the "custom_terminal" locset. +We would like to get a list of the spikes on the cell during the runtime of the +simulation, and we would like to plot the voltage registered by the probe on the +"custom_terminal" locset. Without the call to ``sample``, the probe will be +present, but no data will recorded. This can help to toggle different probes and +possible save some memory on the unsampled ones. Sampling requires a unqiue +identifier for the probe we want to attach to, which consists of the gid of the +cell and the label we gave to the ``place`` method when setting the probe. Each +sampler is identified by an opaque handle that can be used to retrieve the +recorded data. -The lines handling probe sampling warrant a second look. First, we declared ``probeset_id`` to be a -:class:`arbor.cell_member`, with :class:`arbor.cell_member.gid` = 0 and :class:`arbor.cell_member.index` = 0. -This variable serves as a global identifier of a probe on a cell, namely the first declared probe on the -cell with ``gid = 0``, which is id of the only probe we created on the only cell in the model. - -Next, we instructed the simulation to sample ``probeset_id`` at a frequency of 50 kHz. That function returns a -:term:`handle` which we will use to :ref:`extract the results ` of the sampling after running the simulation. - -We can now run the simulation we just instantiated for a duration of 100 ms with a time step of 0.025 ms. +We can now run the simulation we just instantiated for a duration of 100 ms with +a time step of 0.025 ms. .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py :language: python - :lines: 124-125 + :lines: 140-141 The results *********** -The last step is result collection. We instructed the simulation to record the spikes on the cell, and -to sample the probe. +The last step is result collection. We instructed the simulation to record the +spikes on the cell, and to sample the probe. We can print the times of the spikes: .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py :language: python - :lines: 127-131 + :lines: 143-147 The probe results, again, warrant some more explanation: .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py :language: python - :lines: 133-137 - -``sim.samples()`` takes a ``handle`` of the probe we wish to examine. It returns a list -of ``(data, meta)`` terms: ``data`` being the time and value series of the probed quantity; and -``meta`` being the location of the probe. The size of the returned list depends on the number of -discrete locations pointed to by the handle. We placed the probe on the "custom_terminal" locset which is -represented by 2 locations on the morphology. We therefore expect the length of ``sim.samples(handle)`` -to be 2. - -We plot the results using pandas and seaborn as we did in the original example, and expect the same results: + :lines: 159 + +``sim.samples`` takes a ``handle`` that is associated to the probe we wish to +examine. These opaque objects are returned from the calls to ``sim.sample``. +Each call returns a list of ``(data, meta)`` objects. Here, ``meta`` describes +the location set of the probe, which can be a single place or a list of places. +The other item -- ``data`` -- is a numpy array comprising one column for the +time and one for each eantry in the location list. The size of the returned list +depends on the number of discrete locations pointed to by the handle. We placed +the probe on the "custom_terminal" locset which is represented by 2 locations on +the morphology. We therefore expect the length of ``sim.samples(handle)`` to +be 2. + +We plot the results using pandas and seaborn as we did in the original example, +and expect the same results: .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py :language: python - :lines: 139- + :lines: 149- The following plot is generated. Identical to the plot of the original example. @@ -160,4 +172,5 @@ The following plot is generated. Identical to the plot of the original example. The full code ************* -You can find the full code of the example at ``python/examples/single_cell_detailed_recipe.py``. +You can find the full code of the example at +``python/examples/single_cell_detailed_recipe.py``. diff --git a/doc/tutorial/single_cell_model.rst b/doc/tutorial/single_cell_model.rst index 49b096345c..769ea8380e 100644 --- a/doc/tutorial/single_cell_model.rst +++ b/doc/tutorial/single_cell_model.rst @@ -12,7 +12,8 @@ introduce Arbor's cell modelling concepts and approach. **Concepts covered in this example:** - 1. Intro to building a morphology from a :class:`arbor.segment_tree`. + 0. The Arbor library and units. + 1. Intro to morphologies. 2. Intro to region and locset expressions. 3. Intro to decors and cell decorations. 4. Building a :class:`arbor.cable_cell` object. @@ -21,56 +22,96 @@ introduce Arbor's cell modelling concepts and approach. .. _tutorialsinglecell-cell: +Setup and introduction to units +------------------------------- + +We begin by importing the Arbor library and its unit support. + +.. literalinclude:: ../../python/example/single_cell_model.py + :language: python + :lines: 4-5 + +As we will refer to both quite often, we assign aliases ``A`` and ``U``, to +minimize typing. Over the course of this introduction, you will notice that most +of Arbor's user interface is making use of units. This requires a bit of typing, +but makes the physical quantities obvious and allows for easy conversion of +models. You can use any sensible unit for a given dimension and Arbor will +convert as needed, e.g. you can write ``5 * U.mm`` instead of ``5000 * U.um``. +Handing a mismatching dimension to a method will cause a runtime error, so in +the example above, ``5 * U.mV`` will be rejected. + The cell -------- -The most trivial representation of a cell in Arbor is to model the entire cell as a -single cylinder. The following example shows the steps required to construct a model of a -cylindrical cell with a length of 6 μm and a radius of 3 μm; Hodgkin–Huxley dynamics -and a current clamp stimulus, then run the model for 30 ms. +The most trivial representation of a cell in Arbor is to model the entire cell +as a single cylinder. The following example shows the steps required to +construct a model of a cylindrical cell with a length of 6 μm and a radius of 3 +μm; Hodgkin–Huxley dynamics and a current clamp stimulus, then run the model for +30 ms. -The first step is to construct the cell. In Arbor, the abstract representation used to -define a cell with branching cable morphology is a ``cable_cell``, which holds a -description of the cell's morphology, named regions and locations on the morphology, and -descriptions of ion channels, synapses, threshold detectors and electrical properties. +The first step is to construct the cell. In Arbor, the abstract representation +used to define a cell with branching cable morphology is a ``cable_cell``, which +holds a description of the cell's morphology, named regions and locations on the +morphology, and descriptions of ion channels, synapses, threshold detectors and +electrical properties. We will go over these one by one. -Our *single-segment HH cell* has a simple morphology and dynamics, constructed as follows: +Our *cell* has a simple morphology comprising a single segment, which is why we +use an explicit construction. Normally, one would read the morphology from file +and Arbor handles most standard formats natively. .. literalinclude:: ../../python/example/single_cell_model.py :language: python - :lines: 4,6-23 - -Step **(1)** constructs a :class:`arbor.segment_tree` (see also :ref:`segment tree`). -The segment tree is the representation used to construct the morphology of a cell. A segment is -a tapered cone with a tag; the tag can be used to classify the type of the segment (for example -soma, dendrite etc). To create a segment tree representing our single-cylinder cell, we need to add -one segment to our ``tree`` object. We use the :meth:`arbor.segment_tree.append` method, which takes -4 arguments: the parent segment which does not exist for the first segment, so we use :class:`arbor.mnpos`; -the proximal :class:`arbor.mpoint` (location and radius) of the segment; the distal :class:`arbor.mpoint` -of the segment; and the tag. - -Step **(2)** creates a dictionary of labels (:class:`arbor.label_dict`). Labels give -names to :term:`regions` and :term:`location` described using a DSL -based on s-expressions. Labels from the dictionary can then be used to facilitate adding synapses, -dynamics, stimuli and probes to the cell. We add two labels: - -* ``soma`` defines a *region* with ``(tag 1)``. Note that this corresponds to the - ``tag`` parameter that was used to define the single segment in step (1). -* ``midpoint`` defines a *location* at ``(location 0 0.5)``, which is the mid point ``0.5`` - of branch ``0``, which corresponds to the midpoint of the soma on the morphology defined in step (1). - -Step **(3)** constructs a :class:`arbor.decor` that describes the distribution and placement -of dynamics and properties on a cell. The cell's default properties can be modified, and we can use -:meth:`arbor.decor.paint` and :meth:`arbor.decor.place` to further customise it in the -following way: - -* :meth:`arbor.decor.set_property` is used to set some default properties on the entire cell. - In the above example we set the initial membrane potential to -40 mV. -* :meth:`arbor.decor.paint` is used to set properties or add dynamics to a region of the cell. - We call this method 'painting' to convey that we are working on sections of a cell, as opposed to - precise locations: for example, we might want to *paint* a density ion channel on all dendrites, - and then *place* a synapse at the tip of the axon. In the above example we paint - HH dynamics on the region we previously named ``"soma"`` in our label dictionary. + :lines: 9-11 + +This constructs a :class:`arbor.segment_tree` (see also :ref:`segment +tree`) containing a single segment. You can skip the rest of +this paragraph on first reading, it explains the details of constructing a +morphology from scratch. The segment tree is the representation used to +construct the morphology of a cell. A segment is a tapered cone with a tag; the +tag can be used to classify the type of the segment (for example soma, dendrite +etc). To create a segment tree representing our single-cylinder cell, we need to +add one segment to our ``tree`` object. We use the +:meth:`arbor.segment_tree.append` method, which takes 4 arguments: the parent +segment which does not exist for the first segment, so we use +:class:`arbor.mnpos`; the proximal :class:`arbor.mpoint` (location and radius) +of the segment; the distal :class:`arbor.mpoint` of the segment; and the tag. + +.. literalinclude:: ../../python/example/single_cell_model.py + :language: python + :lines: 13-14 + +Next, we create a dictionary of labels +(:class:`arbor.label_dict`) to assign properties to. This is a +handy tool to connect part of your morphology to semantically meaningful names. +Labels give names to :term:`regions` and :term:`location` +described using a DSL based on s-expressions. Labels from the dictionary can +then be used to facilitate adding synapses, dynamics, stimuli and probes to the +cell. We add two labels: + +* ``soma`` defines a *region* with ``(tag 1)``. Note that this corresponds to + the ``tag`` parameter that was used to define the single segment in step (1). +* ``midpoint`` defines a *location* at ``(location 0 0.5)``, which is the mid + point ``0.5`` of branch ``0``, which corresponds to the midpoint of the soma + on the morphology defined in step (1). + +.. literalinclude:: ../../python/example/single_cell_model.py + :language: python + :lines: 16-23 + +The final piece constructs a :class:`arbor.decor` describing the distribution +and placement of dynamics and properties on a cell. The cell's default +properties can be modified, and we can use :meth:`arbor.decor.paint` and +:meth:`arbor.decor.place` to further customise it in the following way: + +* :meth:`arbor.decor.set_property` is used to set some default properties on the + entire cell. In the above example we set the initial membrane potential + to -40 mV. +* :meth:`arbor.decor.paint` is used to set properties or add dynamics to a + region of the cell. We call this method 'painting' to convey that we are + working on sections of a cell, as opposed to precise locations: for example, + we might want to *paint* a density ion channel on all dendrites, and then + *place* a synapse at the tip of the axon. In the above example we paint HH + dynamics on the region we previously named ``"soma"`` in our label dictionary. * :meth:`arbor.decor.place` is used to add objects on a precise :class:`arbor.location` on a cell. Examples of objects that are *placed* are synapses, threshold detectors, current stimuli, and probes. In the above example we place a current stimulus @@ -78,15 +119,23 @@ following way: on the location we previously labelled ``"midpoint"``. We also place a :class:`arbor.threshold_detector` with a threshold of -10 mV on the same location. -Step **(4)** constructs the :class:`arbor.cable_cell` from the segment tree and dictionary of labelled regions and locations. +.. literalinclude:: ../../python/example/single_cell_model.py + :language: python + :lines: 25-26 + +The three ingredients -- morphology, labels, and decor -- are joined into a cable cell. The single cell model --------------------- -Once the cell description has been built, the next step is to build and run the simulation. -Arbor provides an interface for constructing single cell models with the -:class:`arbor.single_cell_model` helper that creates a model from a cell description, with -an interface for recording outputs and running the simulation. +Once the cell description has been built, the next step is to build and run the +simulation. Arbor provides an interface for constructing single cell models with +the :class:`arbor.single_cell_model` helper that creates a model from a cell +description, with an interface for recording outputs and running the simulation. + +.. literalinclude:: ../../python/example/single_cell_model.py + :language: python + :lines: 28-29 The single cell model has 4 main functions: @@ -95,38 +144,44 @@ The single cell model has 4 main functions: 3. It **runs** the simulation. 4. It collects **spikes** from threshold detectors and voltage **traces** from registered probes. -Right now, we'll only set a probe and run the simulation. +Right now, we'll only set a probe. The model is complete without, but to see +the results, we need to extract some data. .. literalinclude:: ../../python/example/single_cell_model.py :language: python - :lines: 25-32 + :lines: 31-32 -Step **(5)** instantiates the :class:`arbor.single_cell_model` -with our single-compartment cell. +Note, that the probe is given a location from the label dictionary ``midpoint``, +the value to record ``voltage``, the sampling frequency, and finally a tag by +which we can reference it later, here ``Um``. -Step **(6)** adds a :meth:`arbor.single_cell_model.probe` -used to record variables from the model. Three pieces of information are -provided: the type of quantity we want probed (voltage), the location where we want to -probe ('"midpoint"'), and the frequency at which we want to sample (10 kHz). +Now, we can start the actual simulation: -Step **(7)** runs the actual simulation for a duration of 30 ms. +.. literalinclude:: ../../python/example/single_cell_model.py + :language: python + :lines: 34-35 The results ----------- -Our cell and model have been defined and we have run our simulation. Now we can look at what -the threshold detector and a voltage probes from our model have produced. +Our cell and model have been defined and we have run our simulation. Now we can +look at the resulting spikes and membrane potential. + +.. literalinclude:: ../../python/example/single_cell_model.py + :language: python + :lines: 37-40 + +To print the spike times, we use :meth:`arbor.single_cell_model.spikes`. A +single spike should be generated at around the same time the stimulus we +provided in step (3) gets activated (10ms). + +And, finally, we plot the membrane potential .. literalinclude:: ../../python/example/single_cell_model.py :language: python - :lines: 34-51 + :lines: 42-48 -Step **(8)** accesses :meth:`arbor.single_cell_model.spikes` -to print the spike times. A single spike should be generated at around the same time the stimulus -we provided in step (3) gets activated (10ms). -Step **(9)** plots the measured potentials during the runtime of the simulation. The sampled quantities -can be accessed through :meth:`arbor.single_cell_model.traces`. We should be seeing something like this: .. figure:: single_cell_model_result.svg @@ -138,4 +193,6 @@ We should be seeing something like this: The full code ------------- -You can find the source code for this example in full at ``python/examples/single_cell_model.py``. +You can find the source code for this example in full at +``python/examples/single_cell_model.py`` which comes in at roughly 10 lines of +Python to define and simulate a cell from scratch. diff --git a/doc/tutorial/single_cell_recipe.rst b/doc/tutorial/single_cell_recipe.rst index 40d3233578..d5046d850a 100644 --- a/doc/tutorial/single_cell_recipe.rst +++ b/doc/tutorial/single_cell_recipe.rst @@ -6,10 +6,10 @@ A simple single cell recipe This example builds the same single cell model as :ref:`tutorialsinglecell`, except using a :class:`arbor.recipe` and :class:`arbor.simulation` instead of a :class:`arbor.single_cell_model`. -Recipes are an important concept in Arbor. They represent the most versatile tool -for building a complex network of cells. We will go though this example of a model -of a single cell, before using the recipe to represent more complex networks in -subsequent examples. +Recipes are an important concept in Arbor. They represent the most versatile +tool for building a complex network of cells. We will go though this example of +a model of a single cell, before using the recipe to represent more complex +networks in subsequent examples. .. Note:: @@ -32,18 +32,20 @@ where construction of the cell is explained in detail. The recipe ---------- -In the :ref:`original example `, the :class:`arbor.single_cell_model` creates -a :class:`arbor.recipe` under the hood, and abstracts away a few details that you may want control over -in more complex simulations. Let's go into those abstractions and create an analogous :class:`arbor.recipe` -manually. +In the :ref:`original example `, the +:class:`arbor.single_cell_model` creates a :class:`arbor.recipe` under the hood, +and abstracts away a few details that you may want control over in more complex +simulations. Let's go into those abstractions and create an analogous +:class:`arbor.recipe` manually. -Creating a recipe starts with creating a class that inherits from :class:`arbor.recipe`. There are a number of -methods that *must* be overridden, and a number than *can optionally* be overridden, as explained in the -:class:`arbor.recipe` documentation. Beyond this, it is up to you, the user, to structure your code as you -find convenient. +Creating a recipe starts with creating a class that inherits from +:class:`arbor.recipe`. There are a number of methods that *must* be overridden, +and a number than *can optionally* be overridden, as explained in the +:class:`arbor.recipe` documentation. Beyond this, it is up to you, the user, to +structure your code as you find convenient. -One of the methods that must be overridden is :meth:`arbor.recipe.num_cells`. It returns `0` by default and -models without cells are quite boring! +One of the methods that must be overridden is :meth:`arbor.recipe.num_cells`. It +returns `0` by default and models without cells are quite boring! .. literalinclude:: ../../python/example/single_cell_recipe.py :language: python @@ -51,78 +53,87 @@ models without cells are quite boring! Step **(4)** describes the recipe that will reflect our single cell model. -Step **(4.1)** defines the class constructor. It can take any shape you need, but it -is important to call base class' constructor. If the overridden methods of the class -need to return an object, it may be a good idea to have the returned object be a -member of the class. With this constructor, we could easily change the cell and probes -of the model, should we want to do so. Here we initialize the cell properties to match -Neuron's defaults using Arbor's built-in :meth:`arbor.neuron_cable_properties` and -extend with Arbor's own :meth:`arbor.default_catalogue`. +Step **(4.1)** defines the class constructor. It can take any shape you need, +but it is important to call base class' constructor as the first action. If the +overridden methods of the class need to return an object, it may be a good idea +to have the returned object be a member of the class. With this constructor, we +could easily change the cell and probes of the model, should we want to do so. +Here we initialize the cell properties to match Neuron's defaults using Arbor's +built-in :meth:`arbor.neuron_cable_properties` and extend with Arbor's own +:meth:`arbor.default_catalogue`. -Step **(4.2)** defines that this model has one cell. +Step **(4.2)** states that this model has one cell. -Step **(4.3)** returns :class:`arbor.cell_kind.cable`, the :class:`arbor.cell_kind` -associated with the cable cell defined above. If you mix multiple cell kinds and -descriptions in one recipe, make sure a particular ``gid`` returns matching cell kinds -and descriptions. +Step **(4.3)** returns :class:`arbor.cell_kind.cable`, the +:class:`arbor.cell_kind` associated with the cable cell defined above. If you +mix multiple cell kinds and descriptions in one recipe, make sure a particular +``gid`` returns matching cell kinds and descriptions. -Step **(4.4)** returns the cell description defined earlier. If we -were modelling multiple cells of different kinds, we would need to make sure that the +Step **(4.4)** returns the cell description defined earlier. If we were +modelling multiple cells of different kinds, we would need to make sure that the cell returned by :meth:`arbor.recipe.cell_description` has the same cell kind as returned by :meth:`arbor.recipe.cell_kind` for every :gen:`gid`. -Step **(4.5)** returns the same probe as in the ``single_cell_model``: a single voltage probe located at "midpoint". +Step **(4.5)** returns the same probe as in the ``single_cell_model``: a single +voltage probe located at "midpoint". -Step **(4.6)** returns the properties that will be applied to all cells of that kind in the model. +Step **(4.6)** returns the properties that will be applied to all cells of that +kind in the model. -More methods can be overridden if your model requires that, see :class:`arbor.recipe` for options. +More methods may be overridden if your model requires that, see +:class:`arbor.recipe` for options. Now we instantiate the recipe .. literalinclude:: ../../python/example/single_cell_recipe.py :language: python - :lines: 64-67 + :lines: 60-61 The simulation -------------- -:class:`arbor.single_cell_model` does not only take care of the recipe, it also takes -care of defining how the simulation will be run. When you create and use your own -recipe, you can to do this manually, in the form of defining a execution context -and a domain decomposition. Fortunately, the default constructors of -:class:`arbor.context` and :class:`arbor.partition_load_balance` are sufficient for -this model, and is what :class:`arbor.single_cell_model` does under the hood! -In addition, if all you need is the default context and domain decomposition, they can be -left out and the :class:`arbor.simulation` object can be contructed from just the recipe. +:class:`arbor.single_cell_model` does not only take care of the recipe, it also +takes care of defining how the simulation will be run. When you create and use +your own recipe, you can to do this manually, in the form of defining a +execution context and a domain decomposition. Fortunately, the default +constructors of :class:`arbor.context` and :class:`arbor.partition_load_balance` +are sufficient for this model, and is what :class:`arbor.single_cell_model` does +under the hood! In addition, if all you need is the default context and domain +decomposition, they can be left out and the :class:`arbor.simulation` object can +be contructed from just the recipe. The details of manual hardware configuration will be left for another tutorial. .. literalinclude:: ../../python/example/single_cell_recipe.py :language: python - :lines: 68-79 + :lines: 63-73 Step **(6)** instantiates the simulation. Step **(7)** sets up the probe added in step 5. In the -:class:`arbor.single_cell_model` version of this example, the probe frequency and -simulation duration are the same. Note that the frequency is set with a :class:`arbor.regular_schedule`, -which takes a time and not a frequency. Also note that spike recording must be -switched on. For :ref:`extraction of the probe traces ` later on, we store a :term:`handle`. +:class:`arbor.single_cell_model` version of this example, the probe frequency +and simulation duration are the same. Note that the frequency is set with a +:class:`arbor.regular_schedule`, which takes a time and not a frequency. Also +note that spike recording must be switched on. For :ref:`extraction of the probe +traces ` later on, we store a :term:`handle`. Then, we +start the simulation. The results ----------------------------------------------------- +----------- Apart from creating :class:`arbor.recipe` ourselves, we have changed nothing -about this simulation compared to :ref:`the original tutorial `. -If we create the same analysis of the results we therefore expect the same results. +about this simulation compared to :ref:`the original tutorial +`. If we create the same analysis of the results we +therefore expect the same results. .. literalinclude:: ../../python/example/single_cell_recipe.py :language: python - :lines: 80- + :lines: 75- Step **(8)** plots the measured potentials during the runtime of the simulation. -Retrieving the sampled quantities is a little different, these have to be accessed -through the simulation object: :meth:`arbor.simulation.spikes` and :meth:`arbor.simulation.samples`. +Retrieving the sampled quantities is a little different, these have to be +accessed through the simulation object: :meth:`arbor.simulation.spikes` and +:meth:`arbor.simulation.samples`. We should be seeing something like this: diff --git a/example/bench/bench.cpp b/example/bench/bench.cpp index 1057ff74f9..0b4367238d 100644 --- a/example/bench/bench.cpp +++ b/example/bench/bench.cpp @@ -30,6 +30,8 @@ #include #endif +namespace U = arb::units; + struct bench_params { struct cell_params { double spike_freq_hz = 10; // Frequency in hz that cell will generate (poisson) spikes. @@ -85,14 +87,12 @@ class bench_recipe: public arb::recipe { } arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override { - std::mt19937_64 rng(gid); - // The time_sequence of the cell produces the series of time points at // which it will spike. We use a poisson_schedule with a random sequence // seeded with the gid. In this way, a cell's random stream depends only // on its gid, and will hence give reproducable results when run with // different MPI ranks and threads. - auto sched = arb::poisson_schedule(1e-3*params_.cell.spike_freq_hz, rng); + 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); } @@ -120,7 +120,7 @@ class bench_recipe: public arb::recipe { // Draw random source and adjust to avoid self-connections if neccesary. arb::cell_gid_type src = dist(rng); if (src>=gid) ++src; - arb::cell_connection con({src, "src"}, {"tgt"}, 1.f, params_.network.min_delay); + arb::cell_connection con({src, "src"}, {"tgt"}, 1.f, params_.network.min_delay*U::ms); cons.push_back(con); } @@ -169,7 +169,7 @@ int main(int argc, char** argv) { meters.checkpoint("model-build", context); // Run the simulation for 100 ms, with time steps of 0.01 ms. - sim.run(params.duration, 0.01); + sim.run(params.duration*arb::units::ms, 0.01*arb::units::ms); meters.checkpoint("model-run", context); // write meters diff --git a/example/brunel/brunel.cpp b/example/brunel/brunel.cpp index b68e8e3322..bfcfca9d0b 100644 --- a/example/brunel/brunel.cpp +++ b/example/brunel/brunel.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -129,20 +128,17 @@ class brunel_recipe: public recipe { util::unique_any get_cell_description(cell_gid_type gid) const override { auto cell = lif_cell("src", "tgt"); - cell.tau_m = 10; - cell.V_th = 10; - cell.C_m = 20; - cell.E_L = 0; - cell.V_m = 0; - cell.t_ref = 2; + cell.tau_m = 10*U::ms; + cell.V_th = 10*U::mV; + cell.C_m = 20*U::pF; + cell.E_L = 0*U::mV; + cell.V_m = 0*U::mV; + cell.t_ref = 2*U::ms; return cell; } std::vector event_generators(cell_gid_type gid) const override { - std::mt19937_64 G; - G.seed(gid + seed_); - time_type t0 = 0; - return {poisson_generator({"tgt"}, weight_ext_, t0, lambda_, G)}; + return {poisson_generator({"tgt"}, weight_ext_, 0*arb::units::ms, lambda_*arb::units::kHz, gid + seed_)}; } private: @@ -263,7 +259,7 @@ int main(int argc, char** argv) { meters.checkpoint("model-init", context); // Run simulation. - sim.run(options.tfinal, options.dt); + sim.run(options.tfinal*arb::units::ms, options.dt*arb::units::ms); meters.checkpoint("model-simulate", context); @@ -324,7 +320,7 @@ void add_subset(cell_gid_type gid, while(m) { cell_gid_type val = dis(gen); if (!seen.count(val)) { - conns.push_back({{val, src}, {tgt}, weight, delay}); + conns.push_back({{val, src}, {tgt}, weight, delay*U::ms}); seen.insert(val); m--; } diff --git a/example/busyring/ring.cpp b/example/busyring/ring.cpp index a955a0a34a..1d53fceb38 100644 --- a/example/busyring/ring.cpp +++ b/example/busyring/ring.cpp @@ -1,11 +1,8 @@ #include #include #include -#include #include #include -#include -#include #include @@ -41,6 +38,7 @@ using arb::time_type; using arb::cable_probe_membrane_voltage; using namespace arborio::literals; +namespace U = arb::units; // Writes voltage trace as a json file. void write_trace_json(std::string fname, const arb::trace_data& trace); @@ -90,7 +88,7 @@ class ring_recipe: public arb::recipe { const auto group_start = s*group; const auto group_end = std::min(group_start+s, num_cells_); cell_gid_type src = gid==group_start? group_end-1: gid-1; - cons.push_back(arb::cell_connection({src, "d"}, {"p"}, event_weight_, min_delay_)); + cons.push_back(arb::cell_connection({src, "d"}, {"p"}, event_weight_, min_delay_*U::ms)); // Used to pick source cell for a connection. std::uniform_int_distribution dist(0, num_cells_-2); @@ -104,7 +102,7 @@ class ring_recipe: public arb::recipe { if (src==gid) ++src; const float delay = min_delay_+delay_dist(src_gen); cons.push_back( - arb::cell_connection({src, "d"}, {"p"}, 0.f, delay)); + arb::cell_connection({src, "d"}, {"p"}, 0.f, delay*U::ms)); } return cons; } @@ -113,7 +111,7 @@ class ring_recipe: public arb::recipe { // This generates a single event that will kick start the spiking on the sub-ring. std::vector event_generators(cell_gid_type gid) const override { if (gid%params_.ring_size == 0) { - return {arb::explicit_generator({"p"}, event_weight_, std::vector{1.0f})}; + return {arb::explicit_generator_from_milliseconds({"p"}, event_weight_, std::vector{1.0})}; } else { return {}; } @@ -231,7 +229,7 @@ int main(int argc, char** argv) { // the cell_member type points to (cell 0, probe 0) auto probe_id = arb::cell_address_type{0, "Um"}; // The schedule for sampling is 10 samples every 1 ms. - auto sched = arb::regular_schedule(0.1); + auto sched = arb::regular_schedule(0.1*U::ms); // Now attach the sampler at probe_id, with sampling schedule sched, writing to voltage sim.add_sampler(arb::one_probe(probe_id), sched, arb::make_simple_sampler(voltage)); } @@ -250,7 +248,7 @@ int main(int argc, char** argv) { // Run the simulation. if (root) sim.set_epoch_callback(arb::epoch_progress_bar()); if (root) std::cout << "running simulation\n" << std::endl; - sim.run(params.duration, params.dt); + sim.run(params.duration*U::ms, params.dt*U::ms); meters.checkpoint("model-run", context); @@ -394,14 +392,14 @@ arb::cable_cell complex_cell(arb::cell_gid_type gid, const cell_parameters& para arb::decor decor; - decor.paint(rall, arb::init_reversal_potential{"k", -107.0}); - decor.paint(rall, arb::init_reversal_potential{"na", 53.0}); + decor.paint(rall, arb::init_reversal_potential{"k", -107.0*U::mV}); + decor.paint(rall, arb::init_reversal_potential{"na", 53.0*U::mV}); - decor.paint(soma, arb::axial_resistivity{133.577}); - decor.paint(soma, arb::membrane_capacitance{4.21567e-2}); + decor.paint(soma, arb::axial_resistivity{133.577*U::Ohm*U::cm}); + decor.paint(soma, arb::membrane_capacitance{4.21567e-2*U::F/U::m2}); - decor.paint(dend, arb::axial_resistivity{68.355}); - decor.paint(dend, arb::membrane_capacitance{2.11248e-2}); + decor.paint(dend, arb::axial_resistivity{68.355*U::Ohm*U::cm}); + decor.paint(dend, arb::membrane_capacitance{2.11248e-2*U::F/U::m2}); decor.paint(soma, arb::density("pas/e=-76.4024", {{"g", 0.000119174}})); decor.paint(soma, arb::density("NaV", {{"gbar", 0.0499779}})); @@ -423,7 +421,7 @@ arb::cable_cell complex_cell(arb::cell_gid_type gid, const cell_parameters& para decor.place(syns, arb::synapse("expsyn"), "s"); } - decor.place(cntr, arb::threshold_detector{-20.0}, "d"); + decor.place(cntr, arb::threshold_detector{-20.0*U::mV}, "d"); decor.set_default(arb::cv_policy_every_segment()); @@ -443,10 +441,10 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param decor.paint(soma, arb::density{"hh"}); decor.paint(dnds, arb::density{"pas"}); - decor.set_default(arb::axial_resistivity{100}); // [Ω·cm] + decor.set_default(arb::axial_resistivity{100*U::Ohm*U::cm}); // [Ω·cm] // Add spike threshold detector at the soma. - decor.place(arb::mlocation{0,0}, arb::threshold_detector{10}, "d"); + decor.place(arb::mlocation{0,0}, arb::threshold_detector{10*U::mV}, "d"); // Add a synapse to proximal end of first dendrite. decor.place(arb::mlocation{1, 0}, arb::synapse{"expsyn"}, "p"); diff --git a/example/diffusion/diffusion.cpp b/example/diffusion/diffusion.cpp index ba2405b074..0d2952baca 100644 --- a/example/diffusion/diffusion.cpp +++ b/example/diffusion/diffusion.cpp @@ -21,18 +21,20 @@ using namespace arborio::literals; using namespace arb; +namespace U = arb::units; + struct linear: public recipe { linear(double ext, double dx, double Xi, double beta): l{ext}, d{dx}, i{Xi}, b{beta} { gprop.default_parameters = neuron_parameter_defaults; gprop.default_parameters.discretization = cv_policy_max_extent{d}; - gprop.add_ion("bla", 1, 23, 42, 0, b); + gprop.add_ion("bla", 1, 23*U::mM, 42*U::mM, 0*U::mV, b*U::m2/U::s); } cell_size_type num_cells() const override { return 1; } cell_kind get_cell_kind(cell_gid_type) const override { return cell_kind::cable; } std::any get_global_properties(cell_kind) const override { return gprop; } std::vector get_probes(cell_gid_type) const override { return {{cable_probe_ion_diff_concentration_cell{"na"}, "nad"}}; } - std::vector event_generators(cell_gid_type) const override { return {explicit_generator({"Zap"}, 0.005, std::vector{0.f})}; } + std::vector event_generators(cell_gid_type) const override { return {explicit_generator_from_milliseconds({"Zap"}, 0.005, std::vector{0.})}; } util::unique_any get_cell_description(cell_gid_type) const override { // Stick morphology // -----|----- @@ -41,9 +43,9 @@ struct linear: public recipe { tree.append(0, { -l, 0, 0, 3}, {l, 0, 0, 3}, 2); // Setup decor decor; - decor.set_default(init_int_concentration{"na", i}); - decor.set_default(ion_diffusivity{"na", b}); - decor.paint("(tag 1)"_reg, ion_diffusivity{"na", b}); + decor.set_default(init_int_concentration{"na", i*U::mM}); + decor.set_default(ion_diffusivity{"na", b*U::mM}); + decor.paint("(tag 1)"_reg, ion_diffusivity{"na", b*U::mM}); decor.place("(location 0 0.5)"_ls, synapse("inject/x=bla", {{"alpha", 200.0*l}}), "Zap"); decor.paint("(all)"_reg, density("decay/x=bla")); return cable_cell({tree}, decor); @@ -123,7 +125,7 @@ int main(int argc, char** argv) { auto C = make_context({1, O.gpu}); auto R = linear{O.L, O.dx, O.Xi, O.dX}; simulation S(R, C, partition_load_balance(R, C)); - S.add_sampler(all_probes, regular_schedule(O.ds), sampler); - S.run(O.T, O.dt); + S.add_sampler(all_probes, regular_schedule(O.ds*U::ms), sampler); + S.run(O.T*U::ms, O.dt*U::ms); out.close(); } diff --git a/example/drybench/drybench.cpp b/example/drybench/drybench.cpp index c5300d1aaa..15e439584a 100644 --- a/example/drybench/drybench.cpp +++ b/example/drybench/drybench.cpp @@ -1,10 +1,4 @@ -/* - * A miniapp that demonstrates how to use dry_run mode - * - */ - #include -#include #include #include @@ -30,6 +24,8 @@ #include #include +namespace U = arb::units; + struct bench_params { struct cell_params { double spike_freq_hz = 20; // Frequency in hz that cell will generate (poisson) spikes. @@ -79,7 +75,6 @@ std::ostream& operator<<(std::ostream& o, const bench_params& p); using arb::cell_gid_type; using arb::cell_lid_type; using arb::cell_size_type; -using arb::cell_member_type; using arb::cell_kind; using arb::time_type; @@ -100,8 +95,7 @@ class tile_desc: public arb::tile { } arb::util::unique_any get_cell_description(cell_gid_type gid) const override { - using RNG = std::mt19937_64; - auto gen = arb::poisson_schedule(params_.cell.spike_freq_hz/1000, RNG(gid)); + 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); } @@ -120,7 +114,7 @@ class tile_desc: public arb::tile { for (unsigned i=0; i=gid) ++src; - conns.push_back(arb::cell_connection({src, "src"}, {"tgt"}, 1.f, params_.network.min_delay)); + conns.push_back(arb::cell_connection({src, "src"}, {"tgt"}, 1.f, params_.network.min_delay*U::ms)); } return conns; @@ -162,7 +156,7 @@ int main(int argc, char** argv) { meters.checkpoint("model-init", ctx); // Run the simulation for 100 ms, with time steps of 0.025 ms. - sim.run(params.duration, 0.025); + sim.run(params.duration*arb::units::ms, 0.025*arb::units::ms); meters.checkpoint("model-run", ctx); diff --git a/example/dryrun/branch_cell.hpp b/example/dryrun/branch_cell.hpp index 6b930d5d17..665e4f53ed 100644 --- a/example/dryrun/branch_cell.hpp +++ b/example/dryrun/branch_cell.hpp @@ -17,6 +17,8 @@ using namespace arborio::literals; +namespace U = arb::units; + // Parameters used to generate the random cell morphologies. struct cell_parameters { cell_parameters() = default; @@ -35,7 +37,7 @@ struct cell_parameters { unsigned synapses = 1; }; -cell_parameters parse_cell_parameters(nlohmann::json& json) { +inline cell_parameters parse_cell_parameters(nlohmann::json& json) { cell_parameters params; sup::param_from_json(params.max_depth, "depth", json); sup::param_from_json(params.branch_probs, "branch-probs", json); @@ -55,7 +57,7 @@ double interp(const std::array& r, unsigned i, unsigned n) { return r[0] + p*(r1-r0); } -arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params) { +inline arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params) { arb::segment_tree tree; // Add soma. @@ -110,12 +112,14 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param labels.set("dend", tagged(dtag)); auto decor = arb::decor() - .set_default(arb::axial_resistivity{100}) // [Ω·cm] - .paint("soma"_lab, arb::density("hh")) // Add HH dynamics to soma. - .paint("dend"_lab, arb::density("pas")) // Leaky current everywhere else. - .place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector") // Add spike threshold detector at the soma. - .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "synapse") // Add a synapse to the mid point of the first dendrite. - .set_default(arb::cv_policy_every_segment()); // Make a CV between every sample in the sample tree. + .set_default(arb::axial_resistivity{100*U::Ohm*U::cm}) // [Ω·cm] + .paint("soma"_lab, arb::density("hh")) // Add HH dynamics to soma. + .paint("dend"_lab, arb::density("pas")) // Leaky current everywhere else. + .place(arb::mlocation{0,0}, + arb::threshold_detector{10*U::mV}, + "detector") // Add spike threshold detector at the soma. + .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "synapse") // Add a synapse to the mid point of the first dendrite. + .set_default(arb::cv_policy_every_segment()); // Make a CV between every sample in the sample tree. // Add additional synapses that will not be connected to anything. for (unsigned i=1u; i #include #include @@ -56,7 +51,6 @@ run_params read_options(int argc, char** argv); using arb::cell_gid_type; using arb::cell_lid_type; using arb::cell_size_type; -using arb::cell_member_type; using arb::cell_kind; using arb::time_type; @@ -103,7 +97,7 @@ class tile_desc: public arb::tile { auto src = source_distribution(src_gen); if (src>=gid) ++src; - return {arb::cell_connection({src, "detector"}, {"synapse"}, event_weight_, min_delay_)}; + return {arb::cell_connection({src, "detector"}, {"synapse"}, event_weight_, min_delay_*U::ms)}; } // Return an event generator on every 20th gid. This function needs to generate events @@ -112,7 +106,7 @@ class tile_desc: public arb::tile { std::vector event_generators(cell_gid_type gid) const override { std::vector gens; if (gid%20 == 0) { - gens.push_back(arb::explicit_generator({"synapse"}, event_weight_, std::vector{1.0f})); + gens.push_back(arb::explicit_generator_from_milliseconds({"synapse"}, event_weight_, std::vector{1.0})); } return gens; } @@ -179,8 +173,8 @@ int main(int argc, char** argv) { // The id of the only probe on the cell: the cell_member type points to (cell 0, probe 0) auto probeset_id = arb::cell_address_type{0, "Um"}; - // The schedule for sampling is 10 samples every 1 ms. - auto sched = arb::regular_schedule(1); + // The schedule for sampling every 1 ms. + auto sched = arb::regular_schedule(1*arb::units::ms); // This is where the voltage samples will be stored as (time, value) pairs arb::trace_vector voltage; // Now attach the sampler at probeset_id, with sampling schedule sched, writing to voltage @@ -198,7 +192,7 @@ int main(int argc, char** argv) { meters.checkpoint("model-init", ctx); // Run the simulation for 100 ms, with time steps of 0.025 ms. - sim.run(params.duration, 0.025); + sim.run(params.duration*arb::units::ms, 0.025*arb::units::ms); meters.checkpoint("model-run", ctx); diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp index 75e8f12dbf..bf20ad6fac 100644 --- a/example/gap_junctions/gap_junctions.cpp +++ b/example/gap_junctions/gap_junctions.cpp @@ -34,6 +34,8 @@ using namespace arborio::literals; +namespace U = arb::units; + #ifdef ARB_MPI_ENABLED #include #include @@ -84,7 +86,7 @@ class gj_recipe: public arb::recipe { if(gid % params_.n_cells_per_cable || (int)gid - 1 < 0) { return{}; } - return {arb::cell_connection({gid - 1, "detector"}, {"syn"}, params_.event_weight, params_.event_min_delay)}; + return {arb::cell_connection({gid - 1, "detector"}, {"syn"}, params_.event_weight, params_.event_min_delay*U::ms)}; } std::vector get_probes(cell_gid_type gid) const override { @@ -175,7 +177,7 @@ int main(int argc, char** argv) { // Set up the probe that will measure voltage in the cell. - auto sched = arb::regular_schedule(0.025); + auto sched = arb::regular_schedule(0.025*U::ms); // This is where the voltage samples will be stored as (time, value) pairs std::vector> voltage_traces(decomp.num_local_cells()); @@ -200,7 +202,7 @@ int main(int argc, char** argv) { std::cout << "running simulation" << std::endl; // Run the simulation for 100 ms, with time steps of 0.025 ms. - sim.run(params.sim_duration, 0.025); + sim.run(params.sim_duration*U::ms, 0.025*U::ms); meters.checkpoint("model-run", context); @@ -275,15 +277,15 @@ arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration) tree.append(0, {0,0,2*soma_rad, dend_rad}, {0,0,2*soma_rad+300, dend_rad}, 3); // dendrite auto decor = arb::decor{} - .set_default(arb::axial_resistivity{100}) // [Ω·cm] - .set_default(arb::membrane_capacitance{0.018}) // [F/m²] + .set_default(arb::axial_resistivity{100*U::Ohm*U::cm}) // [Ω·cm] + .set_default(arb::membrane_capacitance{0.018*U::F/U::m2}) // [F/m²] // Paint density channels on all parts of the cell .paint("(all)"_reg, arb::density{"nax", {{"gbar", 0.04}, {"sh", 10}}}) .paint("(all)"_reg, arb::density{"kdrmt", {{"gbar", 0.0001}}}) .paint("(all)"_reg, arb::density{"kamt", {{"gbar", 0.004}}}) .paint("(all)"_reg, arb::density{"pas/e=-65", {{"g", 1.0/12000.0}}}) // Add a spike detector to the soma. - .place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector") + .place(arb::mlocation{0,0}, arb::threshold_detector{10*U::mV}, "detector") // Add two gap junction sites. .place(arb::mlocation{0, 1}, arb::junction{"gj"}, "local_1") .place(arb::mlocation{0, 0}, arb::junction{"gj"}, "local_0") @@ -292,7 +294,7 @@ arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration) // Attach a stimulus to the first cell of the first group if (!gid) { - auto stim = arb::i_clamp::box(0, stim_duration, 0.4); + auto stim = arb::i_clamp::box(0*U::ms, stim_duration*U::ms, 0.4*U::nA); decor.place(arb::mlocation{0, 0.5}, stim, "stim"); } diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp index 08adb2cf86..858fe05e6b 100644 --- a/example/generators/generators.cpp +++ b/example/generators/generators.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include @@ -99,15 +98,14 @@ class generator_recipe: public arb::recipe { // Add excitatory generator gens.push_back( - arb::poisson_generator({"syn"}, // Target synapse index on cell `gid` - w_e, // Weight of events to deliver - t0, // Events start being delivered from this time - lambda_e, // Expected frequency (kHz) - RNG(29562872))); // Random number generator to use + arb::poisson_generator({"syn"}, // Target synapse index on cell `gid` + w_e, // Weight of events to deliver + t0*arb::units::ms, // Events start being delivered from this time + lambda_e*arb::units::kHz, // Expected frequency (kHz) + 29562872)); // Random number generator to use // Add inhibitory generator - gens.emplace_back( - arb::poisson_generator({"syn"}, w_i, t0, lambda_i, RNG(86543891))); + gens.emplace_back(arb::poisson_generator({"syn"}, w_i, t0*arb::units::ms, lambda_i*arb::units::kHz, 86543891)); return gens; } @@ -138,14 +136,14 @@ int main() { // The id of the only probe on the cell: the cell_member type points to (cell 0, probe 0) auto probeset_id = arb::cell_address_type{0, "Um"}; // The schedule for sampling is 10 samples every 1 ms. - auto sched = arb::regular_schedule(0.1); + auto sched = arb::regular_schedule(0.1*arb::units::ms); // This is where the voltage samples will be stored as (time, value) pairs arb::trace_vector voltage; // Now attach the sampler at probeset_id, with sampling schedule sched, writing to voltage sim.add_sampler(arb::one_probe(probeset_id), sched, arb::make_simple_sampler(voltage)); // Run the simulation for 100 ms, with time steps of 0.01 ms. - sim.run(100, 0.01); + sim.run(100*arb::units::ms, 0.01*arb::units::ms); // Write the samples to a json file. write_trace_json(voltage.at(0)); diff --git a/example/lfp/lfp.cpp b/example/lfp/lfp.cpp index 041b186293..b02a5a97cf 100644 --- a/example/lfp/lfp.cpp +++ b/example/lfp/lfp.cpp @@ -19,12 +19,13 @@ using std::any; using arb::util::any_cast; -using arb::util::any_ptr; using arb::util::unique_any; using arb::cell_gid_type; using namespace arborio::literals; +namespace U = arb::units; + // Recipe represents one cable cell with one synapse, together with probes for total trans-membrane current, membrane voltage, // ionic current density, and synaptic conductance. A sequence of spikes are presented to the one synapse on the cell. @@ -83,8 +84,8 @@ struct lfp_demo_recipe: public arb::recipe { synapse_location_ = "(on-components 0.5 (tag 1))"_ls; auto dec = decor() // Use NEURON defaults for reversal potentials, ion concentrations etc., but override ra, cm. - .set_default(axial_resistivity{100}) // [Ω·cm] - .set_default(membrane_capacitance{0.01}) // [F/m²] + .set_default(axial_resistivity{100*U::Ohm*U::cm}) // [Ω·cm] + .set_default(membrane_capacitance{0.01*U::F/U::m2}) // [F/m²] // Twenty CVs per branch on the dendrites (tag 4). .set_default(cv_policy_fixed_per_branch(20, arb::reg::tagged(4))) // Add pas and hh mechanisms: @@ -185,7 +186,7 @@ int main(int argc, char** argv) { const double dt = 0.1; // [ms] // Weight 0.005 μS, onset at t = 0 ms, mean frequency 0.1 kHz. - auto events = arb::poisson_generator({"syn"}, .005, 0., 0.1, std::minstd_rand{}); + auto events = arb::poisson_generator({"syn"}, .005, 0.*U::ms, 100*U::Hz); lfp_demo_recipe recipe(events); arb::simulation sim(recipe); @@ -203,7 +204,7 @@ int main(int argc, char** argv) { lfp_sampler lfp(placed_cell, current_cables, electrodes, 3.0); - auto sample_schedule = arb::regular_schedule(sample_dt); + auto sample_schedule = arb::regular_schedule(sample_dt*U::ms); sim.add_sampler(arb::one_probe({0, "Itotal"}), sample_schedule, lfp.callback()); arb::trace_vector membrane_voltage; @@ -215,7 +216,7 @@ int main(int argc, char** argv) { arb::trace_vector synapse_g; sim.add_sampler(arb::one_probe({0, "expsyn-g"}), sample_schedule, make_simple_sampler(synapse_g)); - sim.run(t_stop, dt); + sim.run(t_stop*U::ms, dt*U::ms); // Output results in JSON format suitable for plotting by plot-lfp.py script. diff --git a/example/ornstein_uhlenbeck/ou.cpp b/example/ornstein_uhlenbeck/ou.cpp index d3669e62bc..369a54856f 100644 --- a/example/ornstein_uhlenbeck/ou.cpp +++ b/example/ornstein_uhlenbeck/ou.cpp @@ -120,10 +120,10 @@ int main(int argc, char** argv) { // setup sampler and add it to the simulation with regular schedule std::vector data; sampler s{data, ncvs, nsteps}; - sim.add_sampler(arb::all_probes, arb::regular_schedule(dt), s); + sim.add_sampler(arb::all_probes, arb::regular_schedule(dt*arb::units::ms), s); // run the simulation - sim.run(nsteps*dt, dt); + sim.run(nsteps*dt*arb::units::ms, dt*arb::units::ms); // evaluate the mean for each time step across the ensembe of realizations // (each control volume is a independent realization of the Ornstein-Uhlenbeck process) diff --git a/example/plasticity/branch_cell.hpp b/example/plasticity/branch_cell.hpp index 527f26012c..23567cd93a 100644 --- a/example/plasticity/branch_cell.hpp +++ b/example/plasticity/branch_cell.hpp @@ -118,7 +118,7 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param decor.set_default(arb::axial_resistivity{100}); // [Ω·cm] // Add spike threshold detector at the soma. - decor.place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector"); + decor.place(arb::mlocation{0,0}, arb::threshold_detector{10*arb::units::mV}, "detector"); // Add a synapse to the mid point of the first dendrite. decor.place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "primary_syn"); @@ -132,7 +132,5 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param // Make a CV between every sample in the sample tree. decor.set_default(arb::cv_policy_every_segment()); - arb::cable_cell cell(arb::morphology(tree), labels, decor); - - return cell; + return {arb::morphology(tree), labels, decor}; } diff --git a/example/plasticity/plasticity.cpp b/example/plasticity/plasticity.cpp index ad07a91bc0..90299d8a85 100644 --- a/example/plasticity/plasticity.cpp +++ b/example/plasticity/plasticity.cpp @@ -13,6 +13,8 @@ using namespace arborio::literals; +namespace U = arb::units; + // Fan out network with n members // - one spike source at gid 0 // - n-1 passive, soma-only cable cells @@ -55,17 +57,18 @@ struct recipe: public arb::recipe { return {}; } // Connect cell `to` to the spike source - void add_connection(arb::cell_gid_type to) { assert(to > 0); connected[to] = {arb::cell_connection({0, src}, {syn}, weight, delay)}; } + void add_connection(arb::cell_gid_type to) { assert(to > 0); connected[to] = { + arb::cell_connection({0, src}, {syn}, weight, delay*U::ms)}; } // Return the cell at gid arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override { // source at gid 0 - if (gid == 0) return arb::spike_source_cell{src, arb::regular_schedule(f_spike)}; + if (gid == 0) return arb::spike_source_cell{src, arb::regular_schedule(f_spike*arb::units::ms)}; // all others are receiving cable cells; single CV w/ HH arb::segment_tree tree; tree.append(arb::mnpos, {-r_soma, 0, 0, r_soma}, {r_soma, 0, 0, r_soma}, 1); auto decor = arb::decor{} .paint(all, arb::density("hh", {{"gl", 5}})) .place(center, arb::synapse("expsyn"), syn) - .place(center, arb::threshold_detector{-10.0}, det) + .place(center, arb::threshold_detector{-10.0*arb::units::mV}, det) .set_default(arb::cv_policy_every_segment()); return arb::cable_cell({tree}, decor); } @@ -110,11 +113,11 @@ int main(int argc, char** argv) { rec.add_connection(1); auto ctx = arb::make_context(arb::proc_allocation{8, -1}); auto sim = arb::simulation(rec, ctx); - sim.add_sampler(arb::all_probes, arb::regular_schedule(dt), sampler); + sim.add_sampler(arb::all_probes, arb::regular_schedule(dt*arb::units::ms), sampler); sim.set_global_spike_callback(spike_cb); print_header(0, 1); - sim.run(1.0, dt); + sim.run(1.0*arb::units::ms, dt*arb::units::ms); rec.add_connection(2); print_header(1, 2); - sim.run(2.0, dt); + sim.run(2.0*arb::units::ms, dt*arb::units::ms); } diff --git a/example/probe-demo/probe-demo.cpp b/example/probe-demo/probe-demo.cpp index 19982c66c5..ed58297ae7 100644 --- a/example/probe-demo/probe-demo.cpp +++ b/example/probe-demo/probe-demo.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -119,15 +118,15 @@ struct cable_recipe: public arb::recipe { auto decor = arb::decor{} .paint(arb::reg::all(), arb::density("hh")) // HH mechanism over whole cell. - .place(arb::mlocation{0, 0.}, arb::i_clamp{1.}, "iclamp") // Inject a 1 nA current indefinitely. + .place(arb::mlocation{0, 0.}, arb::i_clamp{1.*arb::units::nA}, "iclamp") // Inject a 1 nA current indefinitely. .place(arb::mlocation{0, 0.}, arb::synapse("expsyn"), "synapse1") // a synapse .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "synapse2"); // another synapse return arb::cable_cell(tree, decor); } virtual std::vector event_generators(arb::cell_gid_type) const override { - return {arb::poisson_generator({"synapse1"}, .005, 0., 0.1, std::minstd_rand{}), - arb::poisson_generator({"synapse2"}, .1, 0., 0.1, std::minstd_rand{})}; + return {arb::poisson_generator({"synapse1"}, .005, 0.*arb::units::ms, 0.1*arb::units::kHz), + arb::poisson_generator({"synapse2"}, .1, 0.*arb::units::ms, 0.1*arb::units::kHz)}; } }; @@ -144,13 +143,13 @@ int main(int argc, char** argv) { arb::simulation sim(R); sim.add_sampler(arb::all_probes, - arb::regular_schedule(opt.sample_dt), + arb::regular_schedule(opt.sample_dt*arb::units::ms), opt.scalar_probe? scalar_sampler: vector_sampler); // CSV header for sample output: std::cout << "t, " << (opt.scalar_probe? "x, ": "x0, x1, ") << opt.value_name << '\n'; - sim.run(opt.sim_end, opt.sim_dt); + sim.run(opt.sim_end*arb::units::ms, opt.sim_dt*arb::units::ms); } catch (to::option_error& e) { to::usage_error(argv[0], "[OPTIONS]... PROBE\nTry '--help' for more information.", e.what()); diff --git a/example/remote/remote.cpp b/example/remote/remote.cpp index bc75669b72..9bbdf7c99e 100644 --- a/example/remote/remote.cpp +++ b/example/remote/remote.cpp @@ -1,11 +1,14 @@ #include #include -#include #include #include #include #include +#include +#include + +namespace U = arb::units; #include #include @@ -19,20 +22,20 @@ struct remote_recipe: public arb::recipe { arb::cell_size_type num_cells() const override { return size; } arb::util::unique_any get_cell_description(arb::cell_gid_type) const override { auto lif = arb::lif_cell("src", "tgt"); - lif.tau_m = 2.0; - lif.V_th = -10.0; - lif.C_m = 20.0; - lif.E_L = -23.0; - lif.V_m = -23.0; - lif.E_R = -23.0; - lif.t_ref = 0.2; + lif.tau_m = 2.0*U::ms; + lif.V_th = -10.0*U::mV; + lif.C_m = 20.0*U::pF; + lif.E_L = -23.0*U::mV; + lif.V_m = -23.0*U::mV; + lif.E_R = -23.0*U::mV; + lif.t_ref = 0.2*U::ms; return lif; } std::vector external_connections_on(arb::cell_gid_type) const override { std::vector res; // Invent some fictious cells outside of Arbor's realm for (arb::cell_gid_type gid = 0; gid < 10; gid++) { - res.emplace_back(arb::cell_remote_label_type{gid}, arb::cell_local_label_type{"tgt"}, weight, delay); + res.emplace_back(arb::cell_remote_label_type{gid}, arb::cell_local_label_type{"tgt"}, weight, delay*U::ms); } return res; } @@ -75,9 +78,9 @@ int main() { std::cerr << "[ARB]" << mpi.rank << " " << mpi.local_rank << '\n'; std::cerr << "[ARB] Got min delay=" << mid << '\n'; sim.add_sampler(arb::all_probes, - arb::regular_schedule(0.05), + arb::regular_schedule(0.05*arb::units::ms), sampler); - sim.run(T, dt); + sim.run(T*arb::units::ms, dt*arb::units::ms); std::cout << std::fixed << std::setprecision(4); std::cerr << "[ARB] Trace\n"; for (const auto& [t, v]: trace) std::cout << " " << t << " " << v << '\n'; diff --git a/example/ring/branch_cell.hpp b/example/ring/branch_cell.hpp index 61740a9929..92aa9f6119 100644 --- a/example/ring/branch_cell.hpp +++ b/example/ring/branch_cell.hpp @@ -17,6 +17,8 @@ using namespace arborio::literals; +namespace U = arb::units; + // Parameters used to generate the random cell morphologies. struct cell_parameters { cell_parameters() = default; @@ -35,7 +37,7 @@ struct cell_parameters { unsigned synapses = 1; }; -cell_parameters parse_cell_parameters(nlohmann::json& json) { +inline cell_parameters parse_cell_parameters(nlohmann::json& json) { cell_parameters params; sup::param_from_json(params.max_depth, "depth", json); sup::param_from_json(params.branch_probs, "branch-probs", json); @@ -55,7 +57,7 @@ double interp(const std::array& r, unsigned i, unsigned n) { return r[0] + p*(r1-r0); } -arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params) { +inline arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params) { arb::segment_tree tree; // Add soma. @@ -113,8 +115,8 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param auto decor = arb::decor{} .paint("soma"_lab, arb::density("hh")) .paint("dend"_lab, arb::density("pas")) - .set_default(arb::axial_resistivity{100}) // [Ω·cm] - .place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector") // Add spike threshold detector at the soma. + .set_default(arb::axial_resistivity{100*U::Ohm*U::cm}) // [Ω·cm] + .place(arb::mlocation{0,0}, arb::threshold_detector{10*U::mV}, "detector") // Add spike threshold detector at the soma. .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "primary_syn"); // Add a synapse to the mid point of the first dendrite. // Add additional synapses that will not be connected to anything. if (params.synapses > 1) { diff --git a/example/ring/ring.cpp b/example/ring/ring.cpp index 22d201f955..db488dfcd4 100644 --- a/example/ring/ring.cpp +++ b/example/ring/ring.cpp @@ -1,10 +1,4 @@ -/* - * A miniapp that demonstrates how to make a ring model - * - */ - #include -#include #include #include #include @@ -90,7 +84,7 @@ class ring_recipe: public arb::recipe { std::vector connections_on(cell_gid_type gid) const override { std::vector cons; cell_gid_type src = gid? gid-1: num_cells_-1; - cons.push_back(arb::cell_connection({src, "detector"}, {"primary_syn"}, event_weight_, min_delay_)); + cons.push_back(arb::cell_connection({src, "detector"}, {"primary_syn"}, event_weight_, min_delay_*U::ms)); return cons; } @@ -99,7 +93,7 @@ class ring_recipe: public arb::recipe { std::vector event_generators(cell_gid_type gid) const override { std::vector gens; if (!gid) { - gens.push_back(arb::explicit_generator({"primary_syn"}, event_weight_, std::vector{1.0f})); + gens.push_back(arb::explicit_generator_from_milliseconds({"primary_syn"}, event_weight_, std::vector{1.0})); } return gens; } @@ -168,8 +162,8 @@ int main(int argc, char** argv) { // The id of the only probe on the cell: the cell_member type points to (cell 0, probe 0) auto probeset_id = arb::cell_address_type{0, "Um"}; - // The schedule for sampling is 10 samples every 1 ms. - auto sched = arb::regular_schedule(1); + // The schedule for sampling every 1 ms. + auto sched = arb::regular_schedule(1*arb::units::ms); // This is where the voltage samples will be stored as (time, value) pairs arb::trace_vector voltage; // Now attach the sampler at probeset_id, with sampling schedule sched, writing to voltage @@ -191,7 +185,7 @@ int main(int argc, char** argv) { } std::cout << "running simulation\n" << std::endl; // Run the simulation for 100 ms, with time steps of 0.025 ms. - sim.run(params.duration, 0.025); + sim.run(params.duration*arb::units::ms, 0.025*arb::units::ms); meters.checkpoint("model-run", context); diff --git a/example/single/single.cpp b/example/single/single.cpp index fe07488916..d5590a5520 100644 --- a/example/single/single.cpp +++ b/example/single/single.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -87,18 +86,18 @@ int main(int argc, char** argv) { arb::simulation sim(R); // Attach a sampler to the probe described in the recipe, sampling every 0.1 ms. - arb::trace_vector traces; - sim.add_sampler(arb::all_probes, arb::regular_schedule(0.1), arb::make_simple_sampler(traces)); - - // Trigger the single synapse (target is gid 0, index 0) at t = 1 ms with - // the given weight. + sim.add_sampler(arb::all_probes, + arb::regular_schedule(0.1*arb::units::ms), + arb::make_simple_sampler(traces)); + // Trigger the single synapse (target is gid 0, index 0) at t = 1 ms + // with the given weight. arb::spike_event spike = {0, 1., opt.syn_weight}; arb::cell_spike_events cell_spikes = {0, {spike}}; sim.inject_events({cell_spikes}); - sim.run(opt.t_end, opt.dt); + sim.run(opt.t_end*arb::units::ms, opt.dt*arb::units::ms); for (auto entry: traces.at(0)) { std::cout << entry.t << ", " << entry.v << "\n"; diff --git a/example/v_clamp/v-clamp.cpp b/example/v_clamp/v-clamp.cpp index 15eeb65e8f..dbbc15170c 100644 --- a/example/v_clamp/v-clamp.cpp +++ b/example/v_clamp/v-clamp.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -92,7 +91,7 @@ int main(int argc, char** argv) { // Attach a sampler to the probe described in the recipe, sampling every 0.1 ms. arb::trace_vector traces; - sim.add_sampler(arb::all_probes, arb::regular_schedule(0.1), arb::make_simple_sampler(traces)); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.1*arb::units::ms), arb::make_simple_sampler(traces)); // Trigger the single synapse (target is gid 0, index 0) at t = 1 ms with // the given weight. @@ -101,7 +100,7 @@ int main(int argc, char** argv) { arb::cell_spike_events cell_spikes = {0, {spike}}; sim.inject_events({cell_spikes}); - sim.run(opt.t_end, opt.dt); + sim.run(opt.t_end*arb::units::ms, opt.dt*arb::units::ms); for (auto entry: traces.at(0)) { std::cout << entry.t << ", " << entry.v << "\n"; diff --git a/ext/CMakeLists.txt b/ext/CMakeLists.txt index 23b9c65879..009a5bc223 100644 --- a/ext/CMakeLists.txt +++ b/ext/CMakeLists.txt @@ -118,3 +118,19 @@ else() target_link_libraries(ext-gtest INTERFACE GTest::gtest GTest::gtest_main) endif() endif() + +if (ARB_USE_BUNDLED_UNITS) + set(UNITS_ENABLE_TESTS OFF CACHE INTERNAL "") + set(UNITS_BUILD_STATIC_LIBRARY ON CACHE INTERNAL "") + set(UNITS_BUILD_SHARED_LIBRARY OFF CACHE INTERNAL "") + set(UNITS_BUILD_CONVERTER_APP OFF CACHE INTERNAL "") + set(UNITS_BUILD_WEBSERVER OFF CACHE INTERNAL "") + set(UNITS_INSTALL ON CACHE INTERNAL "") + # set(UNITS_NAMESPACE "llnl::units" CACHE INTERNAL "") + + add_subdirectory("${PROJECT_SOURCE_DIR}/ext/units" "${PROJECT_BINARY_DIR}/ext/units") + + mark_as_advanced(UNITS_BUILD_OBJECT_LIBRARY) + mark_as_advanced(UNITS_HEADER_ONLY) + mark_as_advanced(UNITS_NAMESPACE) +endif() diff --git a/ext/sup/include/sup/export.hpp b/ext/sup/include/sup/export.hpp new file mode 100644 index 0000000000..a446e3daee --- /dev/null +++ b/ext/sup/include/sup/export.hpp @@ -0,0 +1,41 @@ +#pragma once + +//#ifndef ARB_EXPORT_DEBUG +//# define ARB_EXPORT_DEBUG +//#endif + +#include + +/* library build type (ARB_SUP_STATIC_LIBRARY/ARB_SUP_SHARED_LIBRARY) */ +#define ARB_SUP_STATIC_LIBRARY + +#ifndef ARB_SUP_EXPORTS +# if defined(arbor_sup_EXPORTS) + /* we are building arbor-sup dynamically */ +# ifdef ARB_EXPORT_DEBUG +# pragma message "we are building arbor-sup dynamically" +# endif +# define ARB_SUP_API ARB_SYMBOL_EXPORT +# elif defined(arbor_sup_EXPORTS_STATIC) + /* we are building arbor-sup statically */ +# ifdef ARB_EXPORT_DEBUG +# pragma message "we are building arbor-sup statically" +# endif +# define ARB_SUP_API +# else + /* we are using the library arbor-sup */ +# if defined(ARB_SUP_SHARED_LIBRARY) + /* we are importing arbor-sup dynamically */ +# ifdef ARB_EXPORT_DEBUG +# pragma message "we are importing arbor-sup dynamically" +# endif +# define ARB_SUP_API ARB_SYMBOL_IMPORT +# else + /* we are importing arbor-sup statically */ +# ifdef ARB_EXPORT_DEBUG +# pragma message "we are importing arbor-sup statically" +# endif +# define ARB_SUP_API +# endif +# endif +#endif diff --git a/ext/units b/ext/units new file mode 160000 index 0000000000..e7aff9f8e4 --- /dev/null +++ b/ext/units @@ -0,0 +1 @@ +Subproject commit e7aff9f8e4cc1ce19b1ea7e7095036e64123601f diff --git a/mechanisms/stochastic/calcium_based_synapse.mod b/mechanisms/stochastic/calcium_based_synapse.mod index 4d45a19e1c..d21656a8b4 100644 --- a/mechanisms/stochastic/calcium_based_synapse.mod +++ b/mechanisms/stochastic/calcium_based_synapse.mod @@ -58,19 +58,9 @@ PARAMETER { tau_Ca = 20 (ms) } -ASSIGNED { - one_over_tau - one_over_tau_Ca - sigma_over_sqrt_tau -} - INITIAL { c = 0 rho = rho_0 - - one_over_tau = 1/tau - one_over_tau_Ca = 1/tau_Ca - sigma_over_sqrt_tau = sigma/(tau^0.5) } BREAKPOINT { @@ -82,12 +72,16 @@ WHITE_NOISE { } DERIVATIVE state { - LOCAL hsp - LOCAL hsd + LOCAL hsp, hsd, d_rho, d_w + hsp = step_right(c - theta_p) hsd = step_right(c - theta_d) - rho' = (-rho*(1-rho)*(rho_star-rho) + gamma_p*(1-rho)*hsp - gamma_d*rho*hsd)*one_over_tau + (hsp + hsd)^0.5*sigma_over_sqrt_tau*W - c' = -c*one_over_tau_Ca + + d_rho = (rho*(rho - 1)*(rho_star - rho) + gamma_p*(1 - rho)*hsp - gamma_d*rho*hsd)/tau + d_w = sigma*((hsp + hsd)/tau)^0.5 + + rho' = d_rho + d_w*W + c' = -c/tau_Ca } NET_RECEIVE(weight) { diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index de26283cd2..21b0bdc16f 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -36,11 +36,13 @@ set(pyarb_source mpi.cpp profiler.cpp pyarb.cpp + label_dict.cpp recipe.cpp schedule.cpp simulation.cpp single_cell_model.cpp env.cpp + units.cpp ) set_property(SOURCE config.cpp PROPERTY COMPILE_DEFINITIONS ARB_BINARY="${CMAKE_INSTALL_BINDIR}" APPEND) @@ -117,4 +119,5 @@ endif() install(TARGETS pyarb DESTINATION ${_python_module_install_path}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py DESTINATION ${_python_module_install_path}) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/stubs/arbor/ DESTINATION ${_python_module_install_path}) install(FILES ${PROJECT_SOURCE_DIR}/VERSION ${PROJECT_SOURCE_DIR}/README.md ${PROJECT_SOURCE_DIR}/LICENSE DESTINATION ${_python_module_install_path}) diff --git a/python/cable_cell_io.cpp b/python/cable_cell_io.cpp index 61d8d9f0ed..e3cff7aa80 100644 --- a/python/cable_cell_io.cpp +++ b/python/cable_cell_io.cpp @@ -3,8 +3,6 @@ #include #include -#include -#include #include #include @@ -13,8 +11,7 @@ #include "error.hpp" #include "util.hpp" -#include "strprintf.hpp" -#include "proxy.hpp" +#include "label_dict.hpp" namespace pyarb { @@ -53,6 +50,9 @@ void write_component(const arborio::cable_cell_component& component, py::object } void register_cable_loader(pybind11::module& m) { + pybind11::class_ component_meta_data(m, "component_meta_data"); + pybind11::class_ cable_component(m, "cable_component"); + m.def("load_component", &load_component, pybind11::arg("filename_or_descriptor"), @@ -75,9 +75,7 @@ void register_cable_loader(pybind11::module& m) { "Write decor to file."); m.def("write_component", - [](const arb::label_dict& d, py::object fn) { - return write_component(d, fn); - }, + [](const label_dict_proxy& d, py::object fn) { return write_component(d.dict, fn); }, pybind11::arg("object"), pybind11::arg("filename_or_descriptor"), "Write label_dict to file."); @@ -99,12 +97,10 @@ void register_cable_loader(pybind11::module& m) { "Write cable_cell to file."); // arborio::meta_data - pybind11::class_ component_meta_data(m, "component_meta_data"); component_meta_data .def_readwrite("version", &arborio::meta_data::version, "cable-cell component version."); // arborio::cable_cell_component - pybind11::class_ cable_component(m, "cable_component"); cable_component .def_readwrite("meta_data", &arborio::cable_cell_component::meta, "cable-cell component meta-data.") .def_property_readonly( diff --git a/python/cells.cpp b/python/cells.cpp index c1c446b53c..93415fb6a8 100644 --- a/python/cells.cpp +++ b/python/cells.cpp @@ -1,15 +1,12 @@ #include -#include -#include #include #include -#include #include -#include #include -#include #include +#include +#include #include #include @@ -18,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -27,21 +25,21 @@ #include #include #include +#include -#include "arbor/cable_cell_param.hpp" -#include "arbor/cv_policy.hpp" #include "conversion.hpp" #include "error.hpp" -#include "proxy.hpp" -#include "pybind11/cast.h" -#include "pybind11/stl.h" -#include "pybind11/pytypes.h" +#include "label_dict.hpp" #include "schedule.hpp" #include "strprintf.hpp" #include "util.hpp" namespace pyarb { +namespace U = arb::units; + +namespace py = pybind11; + template std::string to_string(const T& t) { std::stringstream ss; @@ -78,12 +76,21 @@ std::string to_string(const arb::cable_cell_global_properties& props) { } s += "}\n"; s += util::pprintf(" parameters: {Vm: {}, cm: {}, rL: {}, tempK: {}}\n", - D.init_membrane_potential, D.membrane_capacitance, - D.axial_resistivity, D.temperature_K); + D.init_membrane_potential, D.membrane_capacitance, + D.axial_resistivity, D.temperature_K); s += "}"; return s; } +struct ion_settings { + int charge = 0; + std::optional internal_concentration; + std::optional external_concentration; + std::optional diffusivity; + std::optional reversal_potential; + std::string reversal_potential_method = "const"; +}; + // // cv_policy helpers // @@ -110,7 +117,7 @@ arb::cv_policy make_cv_policy_max_extent(double cv_length, const std::string& re // Helper for finding a mechanism description in a Python object. // Allows rev_pot_method to be specified with string or mechanism_desc -std::optional maybe_method(pybind11::object method) { +std::optional maybe_method(py::object method) { if (!method.is_none()) { if (auto m=try_cast(method)) { return *m; @@ -132,7 +139,8 @@ std::optional maybe_method(pybind11::object method) { std::string lif_str(const arb::lif_cell& c){ return util::pprintf( "", - c.tau_m, c.V_th, c.C_m, c.E_L, c.V_m, c.t_ref); + U::to_string(c.tau_m), U::to_string(c.V_th), U::to_string(c.C_m), + U::to_string(c.E_L), U::to_string(c.V_m), U::to_string(c.t_ref)); } @@ -146,29 +154,77 @@ std::string scaled_density_desc_str(const arb::scaled_mechanism& p mechanism_desc_str(p.t_mech.mech), util::dictionary_csv(p.scale_expr)); } -void register_cells(pybind11::module& m) { - using namespace pybind11::literals; - using std::optional; +// Argument to construct a paintable object. +using Q = U::quantity; +using QnS = std::tuple; +using paintable_arg = std::variant; - // arb::spike_source_cell +std::tuple value_and_scale(const paintable_arg& arg) { + if (std::holds_alternative(arg)) { + return {std::get(arg), 1}; + } + else { + const auto& [val, scale] = std::get(arg); + return {val, arborio::parse_iexpr_expression(scale).unwrap()}; + } +} - pybind11::class_ spike_source_cell(m, "spike_source_cell", +void register_cells(py::module& m) { + using namespace py::literals; + using std::optional; + + py::class_ spike_source_cell(m, "spike_source_cell", "A spike source cell, that generates a user-defined sequence of spikes that act as inputs for other cells in the network."); + py::class_ cell_cv_data(m, "cell_cv_data", + "Provides information on the CVs representing the discretization of a cable-cell."); + py::class_ benchmark_cell(m, "benchmark_cell", + "A benchmarking cell, used by Arbor developers to test communication performance.\n" + "A benchmark cell generates spikes at a user-defined sequence of time points, and\n" + "the time taken to integrate a cell can be tuned by setting the realtime_ratio,\n" + "for example if realtime_ratio=2, a cell will take 2 seconds of CPU time to\n" + "simulate 1 second.\n"); + py::class_ lif_cell(m, "lif_cell", "A leaky integrate-and-fire cell."); + py::class_ cv_policy(m, "cv_policy", "Describes the rules used to discretize (compartmentalise) a cable cell morphology."); + py::class_ py_ion_data(m, "ion_settings"); + py::class_ gprop(m, "cable_global_properties"); + py::class_ decor(m, "decor", + "Description of the decorations to be applied to a cable cell, that is the painted,\n" + "placed and defaulted properties, mecahanisms, ion species etc."); + py::class_ cable_cell(m, "cable_cell", + "Represents morphologically-detailed cell models, with morphology represented as a\n" + "tree of one-dimensional cable segments."); + py::class_ membrane_potential(m, "membrane_potential", "Setting the initial membrane voltage."); + py::class_ revpot_method(m, "reversal_potential_method", "Describes the mechanism used to compute eX for ion X."); + py::class_ membrane_capacitance(m, "membrane_capacitance", "Setting the membrane capacitance."); + py::class_ temperature_K(m, "temperature", "Setting the temperature."); + py::class_ axial_resistivity(m, "axial_resistivity", "Setting the axial resistivity."); + py::class_ reversal_potential(m, "reversal_potential", "Setting the initial reversal potential."); + py::class_ int_concentration(m, "int_concentration", "Setting the initial internal ion concentration."); + py::class_ ext_concentration(m, "ext_concentration", "Setting the initial external ion concentration."); + py::class_ ion_diffusivity(m, "ion_diffusivity", "Setting the ion diffusivity."); + py::class_ density(m, "density", "For painting a density mechanism on a region."); + py::class_ voltage_process(m, "voltage_process", "For painting a voltage_process mechanism on a region."); + py::class_> scaled_mechanism(m, "scaled_mechanism", "For painting a scaled density mechanism on a region."); + py::class_ ion_data(m, "ion_data"); + py::class_ detector(m, "threshold_detector", "A spike detector, generates a spike when voltage crosses a threshold. Can be used as source endpoint for an arbor.connection."); + py::class_ synapse(m, "synapse", "For placing a synaptic mechanism on a locset."); + py::class_ junction(m, "junction", "For placing a gap-junction mechanism on a locset."); + py::class_ i_clamp(m, "iclamp", "A current clamp for injecting a DC or fixed frequency current governed by a piecewise linear envelope."); spike_source_cell - .def(pybind11::init<>( + .def(py::init<>( [](arb::cell_tag_type source_label, const regular_schedule_shim& sched){ return arb::spike_source_cell{std::move(source_label), sched.schedule()};}), "source_label"_a, "schedule"_a, "Construct a spike source cell with a single source labeled 'source_label'.\n" "The cell generates spikes on 'source_label' at regular intervals.") - .def(pybind11::init<>( + .def(py::init<>( [](arb::cell_tag_type source_label, const explicit_schedule_shim& sched){ return arb::spike_source_cell{std::move(source_label), sched.schedule()};}), "source_label"_a, "schedule"_a, "Construct a spike source cell with a single source labeled 'source_label'.\n" "The cell generates spikes on 'source_label' at a sequence of user-defined times.") - .def(pybind11::init<>( + .def(py::init<>( [](arb::cell_tag_type source_label, const poisson_schedule_shim& sched){ return arb::spike_source_cell{std::move(source_label), sched.schedule()};}), "source_label"_a, "schedule"_a, @@ -177,29 +233,20 @@ void register_cells(pybind11::module& m) { .def("__repr__", [](const arb::spike_source_cell&){return "";}) .def("__str__", [](const arb::spike_source_cell&){return "";}); - // arb::benchmark_cell - - pybind11::class_ benchmark_cell(m, "benchmark_cell", - "A benchmarking cell, used by Arbor developers to test communication performance.\n" - "A benchmark cell generates spikes at a user-defined sequence of time points, and\n" - "the time taken to integrate a cell can be tuned by setting the realtime_ratio,\n" - "for example if realtime_ratio=2, a cell will take 2 seconds of CPU time to\n" - "simulate 1 second.\n"); - benchmark_cell - .def(pybind11::init<>( + .def(py::init<>( [](arb::cell_tag_type source_label, arb::cell_tag_type target_label, const regular_schedule_shim& sched, double ratio){ return arb::benchmark_cell{std::move(source_label), std::move(target_label), sched.schedule(), ratio};}), "source_label"_a, "target_label"_a,"schedule"_a, "realtime_ratio"_a=1.0, "Construct a benchmark cell that generates spikes on 'source_label' at regular intervals.\n" "The cell has one source labeled 'source_label', and one target labeled 'target_label'.") - .def(pybind11::init<>( + .def(py::init<>( [](arb::cell_tag_type source_label, arb::cell_tag_type target_label, const explicit_schedule_shim& sched, double ratio){ return arb::benchmark_cell{std::move(source_label), std::move(target_label),sched.schedule(), ratio};}), "source_label"_a, "target_label"_a, "schedule"_a, "realtime_ratio"_a=1.0, "Construct a benchmark cell that generates spikes on 'source_label' at a sequence of user-defined times.\n" "The cell has one source labeled 'source_label', and one target labeled 'target_label'.") - .def(pybind11::init<>( + .def(py::init<>( [](arb::cell_tag_type source_label, arb::cell_tag_type target_label, const poisson_schedule_shim& sched, double ratio){ return arb::benchmark_cell{std::move(source_label), std::move(target_label), sched.schedule(), ratio};}), "source_label"_a, "target_label"_a, "schedule"_a, "realtime_ratio"_a=1.0, @@ -208,17 +255,35 @@ void register_cells(pybind11::module& m) { .def("__repr__", [](const arb::benchmark_cell&){return "";}) .def("__str__", [](const arb::benchmark_cell&){return "";}); - // arb::lif_cell - - pybind11::class_ lif_cell(m, "lif_cell", - "A leaky integrate-and-fire cell."); - lif_cell - .def(pybind11::init<>( - [](arb::cell_tag_type source_label, arb::cell_tag_type target_label){ - return arb::lif_cell(std::move(source_label), std::move(target_label));}), + .def(py::init<>( + [](arb::cell_tag_type source_label, + arb::cell_tag_type target_label, + std::optional tau_m, + std::optional V_th, + std::optional C_m, + std::optional E_L, + std::optional V_m, + std::optional t_ref) { + auto cell = arb::lif_cell{std::move(source_label), std::move(target_label)}; + if (tau_m) cell.tau_m = *tau_m; + if (V_th) cell.V_th = *V_th; + if (C_m) cell.C_m = *C_m; + if (E_L) cell.E_L = *E_L; + if (V_m) cell.V_m = *V_m; + if (t_ref) cell.t_ref = *t_ref; + return cell; + }), "source_label"_a, "target_label"_a, - "Construct a lif cell with one source labeled 'source_label', and one target labeled 'target_label'.") + py::kw_only(), "tau_m"_a=py::none(), "V_th"_a=py::none(), "C_m"_a=py::none(), "E_L"_a=py::none(), "V_m"_a=py::none(), "t_ref"_a=py::none(), + "Construct a lif cell with one source labeled 'source_label', and one target labeled 'target_label'." + "Can optionally take physical parameters:\n" + " * tau_m: Membrane potential decaying constant [ms].\n" + " * V_th: Firing threshold [mV].\n" + " * C_m: Membrane capacitance [pF].\n" + " * E_L: Resting potential [mV].\n" + " * V_m: Initial value of the Membrane potential [mV].\n" + " * t_ref: Refractory period [ms].") .def_readwrite("tau_m", &arb::lif_cell::tau_m, "Membrane potential decaying constant [ms].") .def_readwrite("V_th", &arb::lif_cell::V_th, @@ -240,96 +305,14 @@ void register_cells(pybind11::module& m) { .def("__repr__", &lif_str) .def("__str__", &lif_str); - // arb::label_dict - - pybind11::class_ label_dict(m, "label_dict", - "A dictionary of labelled region and locset definitions, with a\n" - "unique label assigned to each definition."); - label_dict - .def(pybind11::init<>(), - "Create an empty label dictionary.") - .def(pybind11::init&>(), - "Initialize a label dictionary from a dictionary with string labels as keys," - " and corresponding definitions as strings.") - .def(pybind11::init(), - "Initialize a label dictionary from another one") - .def(pybind11::init([](pybind11::iterator& it) { - label_dict_proxy ld; - for (; it != pybind11::iterator::sentinel(); ++it) { - const auto tuple = it->cast(); - const auto key = tuple[0].cast(); - const auto value = tuple[1].cast(); - ld.set(key, value); - } - return ld; - }), - "Initialize a label dictionary from an iterable of key, definition pairs") - .def("add_swc_tags", - [](label_dict_proxy& l) { return l.add_swc_tags(); }, - "Add standard SWC tagged regions.\n" - " - soma: (tag 1)\n" - " - axon: (tag 2)\n" - " - dend: (tag 3)\n" - " - apic: (tag 4)") - .def("__setitem__", - [](label_dict_proxy& l, const char* name, const char* desc) { - l.set(name, desc);}) - .def("__getitem__", - [](label_dict_proxy& l, const char* name) { - if (auto v = l.getitem(name)) return v.value(); - throw pybind11::key_error(name); - }) - .def("__len__", &label_dict_proxy::size) - .def("__iter__", - [](const label_dict_proxy &ld) { - return pybind11::make_key_iterator(ld.cache.begin(), ld.cache.end());}, - pybind11::keep_alive<0, 1>()) - .def("__contains__", - [](const label_dict_proxy &ld, const char* name) { - return ld.contains(name);}) - .def("keys", - [](const label_dict_proxy &ld) { - return pybind11::make_key_iterator(ld.cache.begin(), ld.cache.end());}, - pybind11::keep_alive<0, 1>()) - .def("items", - [](const label_dict_proxy &ld) { - return pybind11::make_iterator(ld.cache.begin(), ld.cache.end());}, - pybind11::keep_alive<0, 1>()) - .def("values", - [](const label_dict_proxy &ld) { - return pybind11::make_value_iterator(ld.cache.begin(), ld.cache.end()); - }, - pybind11::keep_alive<0, 1>()) - .def("append", [](label_dict_proxy& l, const label_dict_proxy& other, const char* prefix) { - l.import(other, prefix); - }, - "other"_a, "The label_dict to be imported" - "prefix"_a="", "optional prefix appended to the region and locset labels", - "Import the entries of a another label dictionary with an optional prefix.") - .def("update", [](label_dict_proxy& l, const label_dict_proxy& other) { - l.import(other); - }, - "other"_a, "The label_dict to be imported" - "Import the entries of a another label dictionary.") - .def_readonly("regions", &label_dict_proxy::regions, - "The region definitions.") - .def_readonly("locsets", &label_dict_proxy::locsets, - "The locset definitions.") - .def("__repr__", [](const label_dict_proxy& d){return d.to_string();}) - .def("__str__", [](const label_dict_proxy& d){return d.to_string();}); - - // arb::cv_policy wrappers - - pybind11::class_ cv_policy(m, "cv_policy", - "Describes the rules used to discretize (compartmentalise) a cable cell morphology."); cv_policy - .def(pybind11::init([](const std::string& expression) { return arborio::parse_cv_policy_expression(expression).unwrap(); }), + .def(py::init([](const std::string& expression) { return arborio::parse_cv_policy_expression(expression).unwrap(); }), "expression"_a, "A valid CV policy expression") .def_property_readonly("domain", [](const arb::cv_policy& p) {return util::pprintf("{}", p.domain());}, "The domain on which the policy is applied.") - .def(pybind11::self + pybind11::self) - .def(pybind11::self | pybind11::self) + .def(py::self + py::self) + .def(py::self | py::self) .def("__repr__", [](const arb::cv_policy& p) { std::stringstream ss; ss << p; @@ -370,27 +353,25 @@ void register_cells(pybind11::module& m) { "Policy to use the same number of CVs for each branch."); // arb::cell_cv_data - pybind11::class_ cell_cv_data(m, "cell_cv_data", - "Provides information on the CVs representing the discretization of a cable-cell."); cell_cv_data .def_property_readonly("num_cv", [](const arb::cell_cv_data& data){return data.size();}, "Return the number of CVs in the cell.") .def("cables", [](const arb::cell_cv_data& d, unsigned index) { - if (index >= d.size()) throw pybind11::index_error("index out of range"); + if (index >= d.size()) throw py::index_error("index out of range"); return d.cables(index); }, "index"_a, "Return a list of cables representing the CV at the given index.") .def("children", [](const arb::cell_cv_data& d, unsigned index) { - if (index >= d.size()) throw pybind11::index_error("index out of range"); + if (index >= d.size()) throw py::index_error("index out of range"); return d.children(index); }, "index"_a, "Return a list of indices of the CVs representing the children of the CV at the given index.") .def("parent", [](const arb::cell_cv_data& d, unsigned index) { - if (index >= d.size()) throw pybind11::index_error("index out of range"); + if (index >= d.size()) throw py::index_error("index out of range"); return d.parent(index); }, "index"_a, @@ -414,9 +395,9 @@ void register_cells(pybind11::module& m) { "Only 'area' and 'length' are supported)", integrate_along)); auto object_vec = arb::intersect_region(arborio::parse_region_expression(reg).unwrap(), cvs, integrate_area); - auto tuple_vec = std::vector(object_vec.size()); + auto tuple_vec = std::vector(object_vec.size()); std::transform(object_vec.begin(), object_vec.end(), tuple_vec.begin(), - [](const auto& t) { return pybind11::make_tuple(t.idx, t.proportion); }); + [](const auto& t) { return py::make_tuple(t.idx, t.proportion); }); return tuple_vec; }, "reg"_a, "A region on a cell", @@ -427,79 +408,74 @@ void register_cells(pybind11::module& m) { "`proportion` is the proportion of the CV (itegrated by area or length) included in the region." ); - pybind11::class_ membrane_potential(m, "membrane_potential", "Setting the initial membrane voltage."); membrane_potential - .def(pybind11::init([](double v) -> arb::init_membrane_potential { return {v}; })) - .def("__repr__", [](const arb::init_membrane_potential& d){ - return "Vm=" + to_string(d.value);}); + .def(py::init([](const U::quantity& v) -> arb::init_membrane_potential { return {v }; })) + .def("__repr__", + [](const arb::init_membrane_potential& d){ + return "Vm=" + to_string(d.value) + " scale=" + to_string(d.scale);}); + + revpot_method + .def(py::init([](const std::string& ion, + const arb::mechanism_desc& d) -> arb::ion_reversal_potential_method { + return {ion, d}; + })) + .def("__repr__", [](const arb::ion_reversal_potential_method& d) { + return "ion" + d.ion + " method=" + d.method.name();}); - pybind11::class_ membrane_capacitance(m, "membrane_capacitance", "Setting the membrane capacitance."); membrane_capacitance - .def(pybind11::init([](double v) -> arb::membrane_capacitance { return {v}; })) + .def(py::init([](const U::quantity& v) -> arb::membrane_capacitance { return {v}; })) .def("__repr__", [](const arb::membrane_capacitance& d){return "Cm=" + to_string(d.value);}); - pybind11::class_ temperature_K(m, "temperature_K", "Setting the temperature."); temperature_K - .def(pybind11::init([](double v) -> arb::temperature_K { return {v}; })) - .def("__repr__", [](const arb::temperature_K& d){return "T=" + to_string(d.value);}); + .def(py::init([](const U::quantity& v) -> arb::temperature { return {v}; })) + .def("__repr__", [](const arb::temperature& d){return "T=" + to_string(d.value);}); - pybind11::class_ axial_resistivity(m, "axial_resistivity", "Setting the axial resistivity."); axial_resistivity - .def(pybind11::init([](double v) -> arb::axial_resistivity { return {v}; })) + .def(py::init([](const U::quantity& v) -> arb::axial_resistivity { return {v}; })) .def("__repr__", [](const arb::axial_resistivity& d){return "Ra" + to_string(d.value);}); - pybind11::class_ reversal_potential(m, "reversal_potential", "Setting the initial reversal potential."); reversal_potential - .def(pybind11::init([](const std::string& i, double v) -> arb::init_reversal_potential { return {i, v}; })) + .def(py::init([](const std::string& i, const U::quantity& v) -> arb::init_reversal_potential { return {i, v}; })) .def("__repr__", [](const arb::init_reversal_potential& d){return "e" + d.ion + "=" + to_string(d.value);}); - pybind11::class_ int_concentration(m, "int_concentration", "Setting the initial internal ion concentration."); int_concentration - .def(pybind11::init([](const std::string& i, double v) -> arb::init_int_concentration { return {i, v}; })) + .def(py::init([](const std::string& i, const U::quantity& v) -> arb::init_int_concentration { return {i, v}; })) .def("__repr__", [](const arb::init_int_concentration& d){return d.ion + "i" + "=" + to_string(d.value);}); - pybind11::class_ ext_concentration(m, "ext_concentration", "Setting the initial external ion concentration."); ext_concentration - .def(pybind11::init([](const std::string& i, double v) -> arb::init_ext_concentration { return {i, v}; })) + .def(py::init([](const std::string& i, const U::quantity& v) -> arb::init_ext_concentration { return {i, v}; })) .def("__repr__", [](const arb::init_ext_concentration& d){return d.ion + "o" + "=" + to_string(d.value);}); - pybind11::class_ ion_diffusivity(m, "ion_diffusivity", "Setting the ion diffusivity."); ion_diffusivity - .def(pybind11::init([](const std::string& i, double v) -> arb::ion_diffusivity { return {i, v}; })) + .def(py::init([](const std::string& i, const U::quantity& v) -> arb::ion_diffusivity { return {i, v}; })) .def("__repr__", [](const arb::ion_diffusivity& d){return "D" + d.ion + "=" + to_string(d.value);}); - pybind11::class_ density(m, "density", "For painting a density mechanism on a region."); density - .def(pybind11::init([](const std::string& name) {return arb::density(name);})) - .def(pybind11::init([](arb::mechanism_desc mech) {return arb::density(mech);})) - .def(pybind11::init([](const std::string& name, const std::unordered_map& params) {return arb::density(name, params);})) - .def(pybind11::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::density(mech, params);})) - .def(pybind11::init([](const std::string& name, pybind11::kwargs parms) {return arb::density(name, util::dict_to_map(parms));})) - .def(pybind11::init([](arb::mechanism_desc mech, pybind11::kwargs params) {return arb::density(mech, util::dict_to_map(params));})) + .def(py::init([](const std::string& name) {return arb::density(name);})) + .def(py::init([](arb::mechanism_desc mech) {return arb::density(mech);})) + .def(py::init([](const std::string& name, const std::unordered_map& params) {return arb::density(name, params);})) + .def(py::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::density(mech, params);})) + .def(py::init([](const std::string& name, py::kwargs parms) {return arb::density(name, util::dict_to_map(parms));})) + .def(py::init([](arb::mechanism_desc mech, py::kwargs params) {return arb::density(mech, util::dict_to_map(params));})) .def_readonly("mech", &arb::density::mech, "The underlying mechanism.") .def("__repr__", [](const arb::density& d){return "";}) .def("__str__", [](const arb::density& d){return "";}); - pybind11::class_ voltage_process(m, "voltage_process", "For painting a voltage_process mechanism on a region."); voltage_process - .def(pybind11::init([](const std::string& name) {return arb::voltage_process(name);})) - .def(pybind11::init([](arb::mechanism_desc mech) {return arb::voltage_process(mech);})) - .def(pybind11::init([](const std::string& name, const std::unordered_map& params) {return arb::voltage_process(name, params);})) - .def(pybind11::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::voltage_process(mech, params);})) - .def(pybind11::init([](arb::mechanism_desc mech, pybind11::kwargs params) {return arb::voltage_process(mech, util::dict_to_map(params));})) - .def(pybind11::init([](const std::string& name, pybind11::kwargs parms) {return arb::voltage_process(name, util::dict_to_map(parms));})) + .def(py::init([](const std::string& name) {return arb::voltage_process(name);})) + .def(py::init([](arb::mechanism_desc mech) {return arb::voltage_process(mech);})) + .def(py::init([](const std::string& name, const std::unordered_map& params) {return arb::voltage_process(name, params);})) + .def(py::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::voltage_process(mech, params);})) + .def(py::init([](arb::mechanism_desc mech, py::kwargs params) {return arb::voltage_process(mech, util::dict_to_map(params));})) + .def(py::init([](const std::string& name, py::kwargs parms) {return arb::voltage_process(name, util::dict_to_map(parms));})) .def_readonly("mech", &arb::voltage_process::mech, "The underlying mechanism.") .def("__repr__", [](const arb::voltage_process& d){return "";}) .def("__str__", [](const arb::voltage_process& d){return "";}); - // arb::scaled_mechanism - - pybind11::class_> scaled_mechanism( - m, "scaled_mechanism", "For painting a scaled density mechanism on a region."); scaled_mechanism - .def(pybind11::init( + .def(py::init( [](arb::density dens) { return arb::scaled_mechanism(std::move(dens)); })) - .def(pybind11::init( + .def(py::init( [](arb::density dens, const std::unordered_map& scales) { auto s = arb::scaled_mechanism(std::move(dens)); for (const auto& [k, v]: scales) { @@ -507,8 +483,8 @@ void register_cells(pybind11::module& m) { } return s; })) - .def(pybind11::init( - [](arb::density dens, pybind11::kwargs scales) { + .def(py::init( + [](arb::density dens, py::kwargs scales) { auto s = arb::scaled_mechanism(std::move(dens)); for (const auto& [k, v]: util::dict_to_map(scales)) { s.scale(k, arborio::parse_iexpr_expression(v).unwrap()); @@ -521,8 +497,8 @@ void register_cells(pybind11::module& m) { s.scale(std::move(name), arborio::parse_iexpr_expression(ex).unwrap()); return s; }, - pybind11::arg("name"), - pybind11::arg("ex"), + py::arg("name"), + py::arg("ex"), "Add a scaling expression to a parameter.") .def("__repr__", [](const arb::scaled_mechanism& d) { @@ -532,60 +508,60 @@ void register_cells(pybind11::module& m) { return " " + scaled_density_desc_str(d) + ">"; }); - // arb::synapse - - pybind11::class_ synapse(m, "synapse", "For placing a synaptic mechanism on a locset."); synapse - .def(pybind11::init([](const std::string& name) {return arb::synapse(name);})) - .def(pybind11::init([](arb::mechanism_desc mech) {return arb::synapse(mech);})) - .def(pybind11::init([](const std::string& name, const std::unordered_map& params) {return arb::synapse(name, params);})) - .def(pybind11::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::synapse(mech, params);})) - .def(pybind11::init([](const std::string& name, pybind11::kwargs parms) {return arb::synapse(name, util::dict_to_map(parms));})) - .def(pybind11::init([](arb::mechanism_desc mech, pybind11::kwargs params) {return arb::synapse(mech, util::dict_to_map(params));})) + .def(py::init([](const std::string& name) {return arb::synapse(name);})) + .def(py::init([](arb::mechanism_desc mech) {return arb::synapse(mech);})) + .def(py::init([](const std::string& name, const std::unordered_map& params) {return arb::synapse(name, params);})) + .def(py::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::synapse(mech, params);})) + .def(py::init([](const std::string& name, py::kwargs parms) {return arb::synapse(name, util::dict_to_map(parms));})) + .def(py::init([](arb::mechanism_desc mech, py::kwargs params) {return arb::synapse(mech, util::dict_to_map(params));})) .def_readonly("mech", &arb::synapse::mech, "The underlying mechanism.") .def("__repr__", [](const arb::synapse& s){return "";}) .def("__str__", [](const arb::synapse& s){return "";}); - // arb::junction - - pybind11::class_ junction(m, "junction", "For placing a gap-junction mechanism on a locset."); junction - .def(pybind11::init([](const std::string& name) {return arb::junction(name);})) - .def(pybind11::init([](arb::mechanism_desc mech) {return arb::junction(mech);})) - .def(pybind11::init([](const std::string& name, const std::unordered_map& params) {return arb::junction(name, params);})) - .def(pybind11::init([](const std::string& name, pybind11::kwargs parms) {return arb::junction(name, util::dict_to_map(parms));})) - .def(pybind11::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::junction(mech, params);})) - .def(pybind11::init([](arb::mechanism_desc mech, pybind11::kwargs params) {return arb::junction(mech, util::dict_to_map(params));})) + .def(py::init([](const std::string& name) {return arb::junction(name);})) + .def(py::init([](arb::mechanism_desc mech) {return arb::junction(mech);})) + .def(py::init([](const std::string& name, const std::unordered_map& params) {return arb::junction(name, params);})) + .def(py::init([](const std::string& name, py::kwargs parms) {return arb::junction(name, util::dict_to_map(parms));})) + .def(py::init([](arb::mechanism_desc mech, const std::unordered_map& params) {return arb::junction(mech, params);})) + .def(py::init([](arb::mechanism_desc mech, py::kwargs params) {return arb::junction(mech, util::dict_to_map(params));})) .def_readonly("mech", &arb::junction::mech, "The underlying mechanism.") .def("__repr__", [](const arb::junction& j){return "";}) .def("__str__", [](const arb::junction& j){return "";}); - // arb::i_clamp - - pybind11::class_ i_clamp(m, "iclamp", - "A current clamp for injecting a DC or fixed frequency current governed by a piecewise linear envelope."); i_clamp - .def(pybind11::init( - [](double ts, double dur, double cur, double frequency, double phase) { + .def(py::init( + [](const U::quantity& ts, + const U::quantity& dur, + const U::quantity& cur, + const U::quantity& frequency, + const U::quantity& phase) { return arb::i_clamp::box(ts, dur, cur, frequency, phase); - }), "tstart"_a, "duration"_a, "current"_a, pybind11::kw_only(), "frequency"_a=0, "phase"_a=0, - "Construct finite duration current clamp, constant amplitude") - .def(pybind11::init( - [](double cur, double frequency, double phase) { + }), + "tstart"_a, "duration"_a, "current"_a, + py::kw_only(), py::arg_v("frequency", 0*U::kHz, "0.0*arbor.units.kHz"), py::arg_v("phase", 0*U::rad, "0.0*arbor.units.rad"), + "Construct finite duration current clamp, constant amplitude") + .def(py::init( + [](const U::quantity& cur, + const U::quantity& frequency, + const U::quantity& phase) { return arb::i_clamp{cur, frequency, phase}; - }), "current"_a, pybind11::kw_only(), "frequency"_a=0, "phase"_a=0, - "Construct constant amplitude current clamp") - .def(pybind11::init( - [](std::vector> envl, double frequency, double phase) { - arb::i_clamp clamp; - for (const auto& p: envl) { - clamp.envelope.push_back({p.first, p.second}); - } - clamp.frequency = frequency; - clamp.phase = phase; - return clamp; - }), "envelope"_a, pybind11::kw_only(), "frequency"_a=0, "phase"_a=0, - "Construct current clamp according to (time, amplitude) linear envelope") + }), + "current"_a, + py::kw_only(), py::arg_v("frequency", 0*U::kHz, "0.0*arbor.units.kHz"), py::arg_v("phase", 0*U::rad, "0.0*arbor.units.rad"), + "Construct constant amplitude current clamp") + .def(py::init( + [](std::vector> envl, + const U::quantity& frequency, + const U::quantity& phase) { + std::vector env; + for (const auto& [t, a]: envl) env.push_back({t, a}); + return arb::i_clamp{env, frequency, phase}; + }), + "envelope"_a, + py::kw_only(), py::arg_v("frequency", 0*U::kHz, "0.0*arbor.units.kHz"), py::arg_v("phase", 0*U::rad, "0.0*arbor.units.rad"), + "Construct current clamp according to (time, amplitude) linear envelope") .def_property_readonly("envelope", [](const arb::i_clamp& obj) { std::vector> envl; @@ -598,16 +574,13 @@ void register_cells(pybind11::module& m) { .def_readonly("frequency", &arb::i_clamp::frequency, "Oscillation frequency (kHz), zero implies DC stimulus.") .def_readonly("phase", &arb::i_clamp::phase, "Oscillation initial phase (rad)") .def("__repr__", [](const arb::i_clamp& c) { - return util::pprintf("", c.frequency);}) + return util::pprintf("", c.frequency);}) .def("__str__", [](const arb::i_clamp& c) { - return util::pprintf("", c.frequency);}); + return util::pprintf("", c.frequency);}); - // arb::threshold_detector - pybind11::class_ detector(m, "threshold_detector", - "A spike detector, generates a spike when voltage crosses a threshold. Can be used as source endpoint for an arbor.connection."); detector - .def(pybind11::init( - [](double thresh) { return arb::threshold_detector{thresh}; }), + .def(py::init( + [](const U::quantity& thresh) { return arb::threshold_detector{thresh}; }), "threshold"_a, "Voltage threshold of spike detector [mV]") .def_readonly("threshold", &arb::threshold_detector::threshold, "Voltage threshold of spike detector [mV]") .def("__repr__", [](const arb::threshold_detector& d){ @@ -615,24 +588,12 @@ void register_cells(pybind11::module& m) { .def("__str__", [](const arb::threshold_detector& d){ return util::pprintf("(threshold_detector {})", d.threshold);}); - // arb::cable_cell_global_properties - pybind11::class_ ion_data(m, "ion_data"); ion_data .def_readonly("internal_concentration", &arb::cable_cell_ion_data::init_int_concentration, "Internal concentration.") .def_readonly("external_concentration", &arb::cable_cell_ion_data::init_ext_concentration, "External concentration.") .def_readonly("diffusivity", &arb::cable_cell_ion_data::diffusivity, "Diffusivity.") .def_readonly("reversal_concentration", &arb::cable_cell_ion_data::init_reversal_potential, "Reversal potential."); - struct ion_settings { - int charge = 0; - std::optional internal_concentration; - std::optional external_concentration; - std::optional diffusivity; - std::optional reversal_potential; - std::string reversal_potential_method = "const"; - }; - - pybind11::class_ py_ion_data(m, "ion_settings"); ion_data .def_property_readonly("charge", [](const ion_settings& s) { return s.charge; }, "Valence.") .def_property_readonly("internal_concentration", [](const ion_settings& s) { return s.internal_concentration; }, "Internal concentration.") @@ -641,46 +602,42 @@ void register_cells(pybind11::module& m) { .def_property_readonly("reversal_potential", [](const ion_settings& s) { return s.reversal_potential; }, "Reversal potential.") .def_property_readonly("reversal_potential_method", [](const ion_settings& s) { return s.reversal_potential_method; }, "Reversal potential method."); - pybind11::class_ gprop(m, "cable_global_properties"); gprop - .def(pybind11::init<>()) - .def(pybind11::init()) + .def(py::init<>()) + .def(py::init()) .def("check", [](const arb::cable_cell_global_properties& props) { arb::check_global_properties(props);}, "Test whether all default parameters and ion species properties have been set.") .def_readwrite("coalesce_synapses", &arb::cable_cell_global_properties::coalesce_synapses, "Flag for enabling/disabling linear syanpse coalescing.") - // set cable properties - .def_property("membrane_potential", - [](const arb::cable_cell_global_properties& props) { return props.default_parameters.init_membrane_potential; }, - [](arb::cable_cell_global_properties& props, double u) { props.default_parameters.init_membrane_potential = u; }) .def_property("membrane_voltage_limit", [](const arb::cable_cell_global_properties& props) { return props.membrane_voltage_limit_mV; }, [](arb::cable_cell_global_properties& props, std::optional u) { props.membrane_voltage_limit_mV = u; }) - .def_property("membrane_capacitance", - [](const arb::cable_cell_global_properties& props) { return props.default_parameters.membrane_capacitance; }, - [](arb::cable_cell_global_properties& props, double u) { props.default_parameters.membrane_capacitance = u; }) - .def_property("temperature", - [](const arb::cable_cell_global_properties& props) { return props.default_parameters.temperature_K; }, - [](arb::cable_cell_global_properties& props, double u) { props.default_parameters.temperature_K = u; }) - .def_property("axial_resistivity", - [](const arb::cable_cell_global_properties& props) { return props.default_parameters.axial_resistivity; }, - [](arb::cable_cell_global_properties& props, double u) { props.default_parameters.axial_resistivity = u; }) + // set cable properties + .def_property_readonly("membrane_potential", + [](const arb::cable_cell_global_properties& props) { return props.default_parameters.init_membrane_potential; }) + .def_property_readonly("membrane_capacitance", + [](const arb::cable_cell_global_properties& props) { return props.default_parameters.membrane_capacitance; }) + .def_property_readonly("temperature", + [](const arb::cable_cell_global_properties& props) { return props.default_parameters.temperature_K; }) + .def_property_readonly("axial_resistivity", + [](const arb::cable_cell_global_properties& props) { return props.default_parameters.axial_resistivity; }) .def("set_property", [](arb::cable_cell_global_properties& props, - optional Vm, optional cm, - optional rL, optional tempK) - { - if (Vm) props.default_parameters.init_membrane_potential = Vm; - if (cm) props.default_parameters.membrane_capacitance=cm; - if (rL) props.default_parameters.axial_resistivity=rL; - if (tempK) props.default_parameters.temperature_K=tempK; + optional Vm, optional cm, + optional rL, optional tempK) { + if (Vm) props.default_parameters.init_membrane_potential=Vm.value().value_as(U::mV); + if (cm) props.default_parameters.membrane_capacitance=cm.value().value_as(U::F/U::m2); + if (rL) props.default_parameters.axial_resistivity=rL.value().value_as(U::Ohm*U::cm); + if (tempK) props.default_parameters.temperature_K=tempK.value().value_as(U::Kelvin); }, - pybind11::arg_v("Vm", pybind11::none(), "initial membrane voltage [mV]."), - pybind11::arg_v("cm", pybind11::none(), "membrane capacitance [F/m²]."), - pybind11::arg_v("rL", pybind11::none(), "axial resistivity [Ω·cm]."), - pybind11::arg_v("tempK", pybind11::none(), "temperature [Kelvin]."), - "Set global default values for cable and cell properties.") + "Vm"_a=py::none(), "cm"_a=py::none(), "rL"_a=py::none(), "tempK"_a=py::none(), + "Set global default values for cable and cell properties.\n" + " * Vm: initial membrane voltage [mV].\n" + " * cm: membrane capacitance [F/m²].\n" + " * rL: axial resistivity [Ω·cm].\n" + " * tempK: temperature [Kelvin].\n" + "These values can be overridden on specific regions using the paint interface.") // add/modify ion species .def("unset_ion", [](arb::cable_cell_global_properties& props, const char* ion) { @@ -690,41 +647,40 @@ void register_cells(pybind11::module& m) { }, "Remove ion species from properties.") .def("set_ion", - [](arb::cable_cell_global_properties& props, const char* ion, - optional valence, optional int_con, - optional ext_con, optional rev_pot, - pybind11::object method, optional diff) - { - if (!props.ion_species.count(ion) && !valence) { - throw std::runtime_error(util::pprintf("New ion species: '{}', missing valence", ion)); - } - if (valence) props.ion_species[ion] = *valence; - - auto& data = props.default_parameters.ion_data[ion]; - if (int_con) data.init_int_concentration = *int_con; - if (ext_con) data.init_ext_concentration = *ext_con; - if (rev_pot) data.init_reversal_potential = *rev_pot; - if (diff) data.diffusivity = *diff; - - if (auto m = maybe_method(method)) { - props.default_parameters.reversal_potential_method[ion] = *m; - } - }, - pybind11::arg("ion"), - pybind11::arg_v("valence", pybind11::none(), "valence of the ion species."), - pybind11::arg_v("int_con", pybind11::none(), "initial internal concentration [mM]."), - pybind11::arg_v("ext_con", pybind11::none(), "initial external concentration [mM]."), - pybind11::arg_v("rev_pot", pybind11::none(), "reversal potential [mV]."), - pybind11::arg_v("method", pybind11::none(), "method for calculating reversal potential."), - pybind11::arg_v("diff", pybind11::none(), "diffusivity [m^2/s]."), - "Set the global default properties of ion species named 'ion'.\n" - "There are 3 ion species predefined in arbor: 'ca', 'na' and 'k'.\n" - "If 'ion' in not one of these ions it will be added to the list, making it\n" - "available to mechanisms. The user has to provide the valence of a previously\n" - "undefined ion the first time this function is called with it as an argument.\n" - "Species concentrations and reversal potential can be overridden on\n" - "specific regions using the paint interface, while the method for calculating\n" - "reversal potential is global for all compartments in the cell, and can't be\n" + [](arb::cable_cell_global_properties& props, const char* ion, + optional valence, optional int_con, + optional ext_con, optional rev_pot, + py::object method, optional diff) { + if (!props.ion_species.count(ion) && !valence) { + throw std::runtime_error(util::pprintf("New ion species: '{}', missing valence", ion)); + } + if (valence) props.ion_species[ion] = *valence; + + auto& data = props.default_parameters.ion_data[ion]; + if (int_con) data.init_int_concentration = int_con.value().value_as(U::mM); + if (ext_con) data.init_ext_concentration = ext_con.value().value_as(U::mM); + if (rev_pot) data.init_reversal_potential = rev_pot.value().value_as(U::mV); + if (diff) data.diffusivity = diff.value().value_as(U::m2/U::s); + + if (auto m = maybe_method(method)) { + props.default_parameters.reversal_potential_method[ion] = *m; + } + }, + "ion"_a, "valence"_a=py::none(), "int_con"_a=py::none(), "ext_con"_a=py::none(), "rev_pot"_a=py::none(), "method"_a =py::none(), "diff"_a=py::none(), + "Set the global default properties of ion species named 'ion'.\n" + " * valence: valence of the ion species [e].\n" + " * int_con: initial internal concentration [mM].\n" + " * ext_con: initial external concentration [mM].\n" + " * rev_pot: reversal potential [mV].\n" + " * method: mechanism for calculating reversal potential.\n" + " * diff: diffusivity [m^2/s].\n" + "There are 3 ion species predefined in arbor: 'ca', 'na' and 'k'.\n" + "If 'ion' in not one of these ions it will be added to the list, making it\n" + "available to mechanisms. The user has to provide the valence of a previously\n" + "undefined ion the first time this function is called with it as an argument.\n" + "Species concentrations and reversal potential can be overridden on\n" + "specific regions using the paint interface, while the method for calculating\n" + "reversal potential is global for all compartments in the cell, and can't be\n" "overriden locally.") .def_property_readonly("ion_data", [](const arb::cable_cell_global_properties& props) { return props.default_parameters.ion_data; }) @@ -766,56 +722,55 @@ void register_cells(pybind11::module& m) { }, "default NEURON cable_global_properties"); - // arb::decor - - pybind11::class_ decor(m, "decor", - "Description of the decorations to be applied to a cable cell, that is the painted,\n" - "placed and defaulted properties, mecahanisms, ion species etc."); decor - .def(pybind11::init<>()) - .def(pybind11::init()) + .def(py::init<>()) + .def(py::init()) // Set cell-wide default values for properties .def("set_property", - [](arb::decor& d, - optional Vm, optional cm, - optional rL, optional tempK) - { - if (Vm) d.set_default(arb::init_membrane_potential{*Vm}); - if (cm) d.set_default(arb::membrane_capacitance{*cm}); - if (rL) d.set_default(arb::axial_resistivity{*rL}); - if (tempK) d.set_default(arb::temperature_K{*tempK}); - return d; - }, - pybind11::arg_v("Vm", pybind11::none(), "initial membrane voltage [mV]."), - pybind11::arg_v("cm", pybind11::none(), "membrane capacitance [F/m²]."), - pybind11::arg_v("rL", pybind11::none(), "axial resistivity [Ω·cm]."), - pybind11::arg_v("tempK", pybind11::none(), "temperature [Kelvin]."), - "Set default values for cable and cell properties. These values can be overridden on specific regions using the paint interface.") + [](arb::decor& d, + optional Vm, optional cm, + optional rL, optional tempK) { + if (Vm) d.set_default(arb::init_membrane_potential{*Vm}); + if (cm) d.set_default(arb::membrane_capacitance{*cm}); + if (rL) d.set_default(arb::axial_resistivity{*rL}); + if (tempK) d.set_default(arb::temperature{*tempK}); + return d; + }, + "Vm"_a=py::none(), "cm"_a=py::none(), "rL"_a=py::none(), "tempK"_a=py::none(), + "Set default values for cable and cell properties:\n" + " * Vm: initial membrane voltage [mV].\n" + " * cm: membrane capacitance [F/m²].\n" + " * rL: axial resistivity [Ω·cm].\n" + " * tempK: temperature [Kelvin].\n" + "These values can be overridden on specific regions using the paint interface.") // modify parameters for an ion species. .def("set_ion", - [](arb::decor& d, const char* ion, - optional int_con, optional ext_con, - optional rev_pot, pybind11::object method, - optional diff) - { - if (int_con) d.set_default(arb::init_int_concentration{ion, *int_con}); - if (ext_con) d.set_default(arb::init_ext_concentration{ion, *ext_con}); - if (rev_pot) d.set_default(arb::init_reversal_potential{ion, *rev_pot}); - if (diff) d.set_default(arb::ion_diffusivity{ion, *diff}); - if (auto m = maybe_method(method)) d.set_default(arb::ion_reversal_potential_method{ion, *m}); - return d; - }, - pybind11::arg("ion"), - pybind11::arg_v("int_con", pybind11::none(), "initial internal concentration [mM]."), - pybind11::arg_v("ext_con", pybind11::none(), "initial external concentration [mM]."), - pybind11::arg_v("rev_pot", pybind11::none(), "reversal potential [mV]."), - pybind11::arg_v("method", pybind11::none(), "method for calculating reversal potential."), - pybind11::arg_v("diff", pybind11::none(), "diffusivity [m^2/s]."), - "Set the properties of ion species named 'ion' that will be applied\n" - "by default everywhere on the cell. Species concentrations and reversal\n" - "potential can be overridden on specific regions using the paint interface, \n" - "while the method for calculating reversal potential is global for all\n" - "compartments in the cell, and can't be overriden locally.") + [](arb::decor& d, const char* ion, + optional int_con, optional ext_con, + optional rev_pot, py::object method, + optional diff) { + if (int_con) d.set_default(arb::init_int_concentration{ion, *int_con}); + if (ext_con) d.set_default(arb::init_ext_concentration{ion, *ext_con}); + if (rev_pot) d.set_default(arb::init_reversal_potential{ion, *rev_pot}); + if (diff) d.set_default(arb::ion_diffusivity{ion, *diff}); + if (auto m = maybe_method(method)) d.set_default(arb::ion_reversal_potential_method{ion, *m}); + return d; + }, + "ion"_a, "int_con"_a=py::none(), "ext_con"_a=py::none(), "rev_pot"_a=py::none(), "method"_a =py::none(), "diff"_a=py::none(), + "Set the cell-level properties of ion species named 'ion'.\n" + " * int_con: initial internal concentration [mM].\n" + " * ext_con: initial external concentration [mM].\n" + " * rev_pot: reversal potential [mV].\n" + " * method: mechanism for calculating reversal potential.\n" + " * diff: diffusivity [m^2/s].\n" + "There are 3 ion species predefined in arbor: 'ca', 'na' and 'k'.\n" + "If 'ion' in not one of these ions it will be added to the list, making it\n" + "available to mechanisms. The user has to provide the valence of a previously\n" + "undefined ion the first time this function is called with it as an argument.\n" + "Species concentrations and reversal potential can be overridden on\n" + "specific regions using the paint interface, while the method for calculating\n" + "reversal potential is global for all compartments in the cell, and can't be\n" + "overriden locally.") .def("paintings", [](arb::decor& dec) { std::vector> result; @@ -862,77 +817,67 @@ void register_cells(pybind11::module& m) { .def("paint", [](arb::decor& dec, const char* region, - optional> Vm, - optional> cm, - optional> rL, - optional> tempK) { - auto r = arborio::parse_region_expression(region).unwrap(); + optional Vm, optional cm, + optional rL, optional tempK) { + auto reg = arborio::parse_region_expression(region).unwrap(); if (Vm) { - if (std::holds_alternative(*Vm)) { - dec.paint(r, arb::init_membrane_potential{std::get(*Vm)}); - } - else { - const auto& s = std::get(*Vm); - auto ie = arborio::parse_iexpr_expression(s).unwrap(); - dec.paint(r, arb::init_membrane_potential{ie}); - } + const auto& [v, s] = value_and_scale(*Vm); + dec.paint(reg, arb::init_membrane_potential{v, s}); } if (cm) { - if (std::holds_alternative(*cm)) { - dec.paint(r, arb::membrane_capacitance{std::get(*cm)}); - } - else { - const auto& s = std::get(*cm); - auto ie = arborio::parse_iexpr_expression(s).unwrap(); - dec.paint(r, arb::membrane_capacitance{ie}); - } + const auto& [v, s] = value_and_scale(*cm); + dec.paint(reg, arb::membrane_capacitance{v, s}); } if (rL) { - if (std::holds_alternative(*rL)) { - dec.paint(r, arb::axial_resistivity{std::get(*rL)}); - } - else { - const auto& s = std::get(*rL); - auto ie = arborio::parse_iexpr_expression(s).unwrap(); - dec.paint(r, arb::axial_resistivity{ie}); - } + const auto& [v, s] = value_and_scale(*rL); + dec.paint(reg, arb::axial_resistivity{v, s}); } if (tempK) { - if (std::holds_alternative(*tempK)) { - dec.paint(r, arb::temperature_K{std::get(*tempK)}); - } - else { - const auto& s = std::get(*tempK); - auto ie = arborio::parse_iexpr_expression(s).unwrap(); - dec.paint(r, arb::temperature_K{ie}); - } + const auto& [v, s] = value_and_scale(*tempK); + dec.paint(reg, arb::temperature{v, s}); } return dec; }, - pybind11::arg("region"), - pybind11::arg_v("Vm", pybind11::none(), "initial membrane voltage [mV]."), - pybind11::arg_v("cm", pybind11::none(), "membrane capacitance [F/m²]."), - pybind11::arg_v("rL", pybind11::none(), "axial resistivity [Ω·cm]."), - pybind11::arg_v("tempK", pybind11::none(), "temperature [Kelvin]."), - "Set cable properties on a region.") + "region"_a, "Vm"_a=py::none(), "cm"_a=py::none(), "rL"_a=py::none(), "tempK"_a=py::none(), + "Set cable properties on a region.\n" + "Set global default values for cable and cell properties.\n" + " * Vm: initial membrane voltage [mV].\n" + " * cm: membrane capacitance [F/m²].\n" + " * rL: axial resistivity [Ω·cm].\n" + " * tempK: temperature [Kelvin].\n" + "Each value can be given as a plain quantity or a tuple of (quantity, 'scale') where scale is an iexpr.") // Paint ion species initial conditions on a region. .def("paint", [](arb::decor& dec, const char* region, const char* name, - optional int_con, optional ext_con, - optional rev_pot, optional diff) { + optional int_con, optional ext_con, + optional rev_pot, optional diff) { auto r = arborio::parse_region_expression(region).unwrap(); - if (int_con) dec.paint(r, arb::init_int_concentration{name, *int_con}); - if (ext_con) dec.paint(r, arb::init_ext_concentration{name, *ext_con}); - if (rev_pot) dec.paint(r, arb::init_reversal_potential{name, *rev_pot}); - if (diff) dec.paint(r, arb::ion_diffusivity{name, *diff}); + if (int_con) { + const auto& [v, s] = value_and_scale(*int_con); + dec.paint(r, arb::init_int_concentration{name, v, s}); + } + if (ext_con) { + const auto& [v, s] = value_and_scale(*ext_con); + dec.paint(r, arb::init_ext_concentration{name, v, s}); + } + if (rev_pot) { + const auto& [v, s] = value_and_scale(*rev_pot); + dec.paint(r, arb::init_reversal_potential{name, v, s}); + } + if (diff) { + const auto& [v, s] = value_and_scale(*diff); + dec.paint(r, arb::ion_diffusivity{name, v, s}); + } return dec; }, - "region"_a, pybind11::kw_only(), "ion_name"_a, - pybind11::arg_v("int_con", pybind11::none(), "Initial internal concentration [mM]"), - pybind11::arg_v("ext_con", pybind11::none(), "Initial external concentration [mM]"), - pybind11::arg_v("rev_pot", pybind11::none(), "Initial reversal potential [mV]"), - pybind11::arg_v("diff", pybind11::none(), "Diffusivity [m^2/s]"), - "Set ion species properties conditions on a region.") + "region"_a, py::kw_only(), "ion"_a, "int_con"_a=py::none(), "ext_con"_a=py::none(), "rev_pot"_a=py::none(), "diff"_a=py::none(), + "Set ion species properties conditions on a region.\n" + " * int_con: initial internal concentration [mM].\n" + " * ext_con: initial external concentration [mM].\n" + " * rev_pot: reversal potential [mV].\n" + " * method: mechanism for calculating reversal potential.\n" + " * diff: diffusivity [m^2/s].\n" + "Each value can be given as a plain quantity or a tuple of (quantity, 'scale') where scale is an iexpr.\n") // Place synapses .def("place", [](arb::decor& dec, const char* locset, const arb::synapse& mechanism, const char* label_name) { @@ -967,39 +912,30 @@ void register_cells(pybind11::module& m) { "The group of spike detectors has the label 'label', used for forming connections between cells.") .def("discretization", [](arb::decor& dec, const arb::cv_policy& p) { return dec.set_default(p); }, - pybind11::arg("policy"), + py::arg("policy"), "A cv_policy used to discretise the cell into compartments for simulation") .def("discretization", [](arb::decor& dec, const std::string& p) { return dec.set_default(arborio::parse_cv_policy_expression(p).unwrap()); }, - pybind11::arg("policy"), + py::arg("policy"), "An s-expression string representing a cv_policy used to discretise the " "cell into compartments for simulation"); - // arb::cable_cell - - pybind11::class_ cable_cell(m, "cable_cell", - "Represents morphologically-detailed cell models, with morphology represented as a\n" - "tree of one-dimensional cable segments."); cable_cell - .def(pybind11::init( + .def(py::init( [](const arb::morphology& m, const arb::decor& d, const std::optional& l) { if (l) return arb::cable_cell(m, d, l->dict); return arb::cable_cell(m, d); }), - "morphology"_a, - "decor"_a, - pybind11::arg_v("labels", pybind11::none(), "Labels"), + "morphology"_a, "decor"_a, "labels"_a=py::none(), "Construct with a morphology, decor, and label dictionary.") - .def(pybind11::init( + .def(py::init( [](const arb::segment_tree& t, const arb::decor& d, const std::optional& l) { if (l) return arb::cable_cell({t}, d, l->dict); return arb::cable_cell({t}, d); }), - "segment_tree"_a, - "decor"_a, - pybind11::arg_v("labels", pybind11::none(), "Labels"), + "segment_tree"_a, "decor"_a, "labels"_a=py::none(), "Construct with a morphology derived from a segment tree, decor, and label dictionary.") .def_property_readonly("num_branches", [](const arb::cable_cell& c) {return c.morphology().num_branches();}, diff --git a/python/example/brunel.py b/python/example/brunel.py index 00beea6bfa..f163b9ab43 100755 --- a/python/example/brunel.py +++ b/python/example/brunel.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -import arbor +import arbor as A +from arbor import units as U import argparse import numpy as np from numpy.random import RandomState @@ -32,7 +33,7 @@ def sample_subset(gen, gid, start, end, m): return idx[:m] -class brunel_recipe(arbor.recipe): +class brunel_recipe(A.recipe): def __init__( self, nexc, @@ -45,7 +46,7 @@ def __init__( poiss_lambda, seed=42, ): - arbor.recipe.__init__(self) + A.recipe.__init__(self) # Make sure that in_degree_prop in the interval (0, 1] if not 0.0 < in_degree_prop <= 1.0: @@ -56,7 +57,7 @@ def __init__( self.ncells_exc_ = nexc self.ncells_inh_ = ninh - self.delay_ = delay + self.delay_ = delay * U.ms self.seed_ = seed # Set up the parameters. @@ -67,25 +68,25 @@ def __init__( self.in_degree_inh_ = round(in_degree_prop * ninh) # each cell receives next incoming Poisson sources with mean rate poiss_lambda, which is equivalent # to a single Poisson source with mean rate next*poiss_lambda - self.lambda_ = next * poiss_lambda + self.lambda_ = next * poiss_lambda * U.kHz def num_cells(self): return self.ncells_exc_ + self.ncells_inh_ def cell_kind(self, gid): - return arbor.cell_kind.lif + return A.cell_kind.lif def connections_on(self, gid): gen = RandomState(gid + self.seed_) connections = [] # Add incoming excitatory connections. connections = [ - arbor.connection((i, "src"), "tgt", self.weight_exc_, self.delay_) + A.connection((i, "src"), "tgt", self.weight_exc_, self.delay_) for i in sample_subset(gen, gid, 0, self.ncells_exc_, self.in_degree_exc_) ] # Add incoming inhibitory connections. connections += [ - arbor.connection((i, "src"), "tgt", self.weight_inh_, self.delay_) + A.connection((i, "src"), "tgt", self.weight_inh_, self.delay_) for i in sample_subset( gen, gid, @@ -98,19 +99,25 @@ def connections_on(self, gid): return connections def cell_description(self, gid): - cell = arbor.lif_cell("src", "tgt") - cell.tau_m = 10 - cell.V_th = 10 - cell.C_m = 20 - cell.E_L = 0 - cell.V_m = 0 - cell.t_ref = 2 - return cell + return A.lif_cell( + "src", + "tgt", + tau_m=10 * U.ms, + V_th=10 * U.mV, + V_m=0 * U.mV, + E_L=0 * U.mV, + C_m=20 * U.pF, + t_ref=2 * U.ms, + ) def event_generators(self, gid): - t0 = 0 - sched = arbor.poisson_schedule(t0, self.lambda_, gid + self.seed_) - return [arbor.event_generator("tgt", self.weight_ext_, sched)] + return [ + A.event_generator( + "tgt", + self.weight_ext_, + A.poisson_schedule(freq=self.lambda_, seed=gid + self.seed_), + ) + ] if __name__ == "__main__": @@ -228,12 +235,12 @@ def event_generators(self, gid): for k, v in vars(opt).items(): print(f"{k} = {v}") - context = arbor.context() - if arbor.config()["profiling"]: - arbor.profiler_initialize(context) + context = A.context() + if A.config()["profiling"]: + A.profiler_initialize(context) print(context) - meters = arbor.meter_manager() + meters = A.meter_manager() meters.start(context) recipe = brunel_recipe( @@ -250,27 +257,27 @@ def event_generators(self, gid): meters.checkpoint("recipe-create", context) - hint = arbor.partition_hint() + hint = A.partition_hint() hint.cpu_group_size = 5000 - hints = {arbor.cell_kind.lif: hint} - decomp = arbor.partition_load_balance(recipe, context, hints) + hints = {A.cell_kind.lif: hint} + decomp = A.partition_load_balance(recipe, context, hints) print(decomp) meters.checkpoint("load-balance", context) - sim = arbor.simulation(recipe, context, decomp) - sim.record(arbor.spike_recording.all) + sim = A.simulation(recipe, context, decomp) + sim.record(A.spike_recording.all) meters.checkpoint("simulation-init", context) - sim.run(opt.tfinal, opt.dt) + sim.run(opt.tfinal * U.ms, opt.dt * U.ms) meters.checkpoint("simulation-run", context) # Print profiling information - print(arbor.meter_report(meters, context)) - if arbor.config()["profiling"]: - print(arbor.profiler_summary()) + print(A.meter_report(meters, context)) + if A.config()["profiling"]: + print(A.profiler_summary()) # Print spike times print(f"{len(sim.spikes())} spikes generated.") diff --git a/python/example/calcium_stdp.py b/python/example/calcium_stdp.py index 4d3d576ea4..b642beea8c 100644 --- a/python/example/calcium_stdp.py +++ b/python/example/calcium_stdp.py @@ -10,19 +10,17 @@ # The synapse dynamics is affected by additive white noise. The results reproduce the spike # timing-dependent plasticity curve for the DP case described in Table S1 (supplemental material). -import arbor -import random -import multiprocessing -import numpy # You may have to pip install these. -import pandas # You may have to pip install these. -import seaborn # You may have to pip install these. +import arbor as A +from arbor import units as U +import numpy as np # You may have to pip install these. +import pandas as pd # You may have to pip install these. +import seaborn as sns # You may have to pip install these. # (1) Set simulation paramters - -# Spike response delay (ms) -D = 13.7 -# Spike frequency in Hertz -f = 1.0 +# Spike response delay +D = 13.7 * U.ms +# Spike frequency +f = 1000 * U.Hz # Number of spike pairs num_spikes = 30 # time lag resolution @@ -32,155 +30,113 @@ # Ensemble size per initial value ensemble_per_rho_0 = 100 # Simulation time step -dt = 0.1 -# List of initial values for 2 states +dt = 0.1 * U.ms +# List of initial values for 2 states; we need a synapse for each sample path rho_0 = [0] * ensemble_per_rho_0 + [1] * ensemble_per_rho_0 -# We need a synapse for each sample path -num_synapses = len(rho_0) # Time lags between spike pairs (post-pre: < 0, pre-post: > 0) -stdp_dt = numpy.arange(-stdp_max_dt, stdp_max_dt + stdp_dt_step, stdp_dt_step) - +stdp_dt = np.arange(-stdp_max_dt, stdp_max_dt + stdp_dt_step, stdp_dt_step) * U.ms +# Time between stimuli +T = 1000.0 / f +# Simulation duration +tfinal = num_spikes * T # (2) Make the cell - # Create a morphology with a single (cylindrical) segment of length=diameter=6 μm -tree = arbor.segment_tree() -tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1) +tree = A.segment_tree() +tree.append(A.mnpos, (-3, 0, 0, 3), (3, 0, 0, 3), tag=1) # Define the soma and its midpoint -labels = arbor.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) +labels = A.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) # Create and set up a decor object decor = ( - arbor.decor() - .set_property(Vm=-40) - .paint('"soma"', arbor.density("pas")) - .place('"midpoint"', arbor.synapse("expsyn"), "driving_synapse") - .place('"midpoint"', arbor.threshold_detector(-10), "detector") + A.decor() + .set_property(Vm=-40 * U.mV) + .paint('"soma"', A.density("pas")) + .place('"midpoint"', A.synapse("expsyn"), "driving_synapse") + .place('"midpoint"', A.threshold_detector(-10 * U.mV), "detector") ) -for i in range(num_synapses): - mech = arbor.mechanism("calcium_based_synapse") - mech.set("rho_0", rho_0[i]) - decor.place('"midpoint"', arbor.synapse(mech), f"calcium_synapse_{i}") +for ix, rho in enumerate(rho_0): + decor.place( + '"midpoint"', + A.synapse("calcium_based_synapse", rho_0=rho), + f"calcium_synapse_{ix}", + ) # Create cell -cell = arbor.cable_cell(tree, decor, labels) - - -# (3) Create extended catalogue including stochastic mechanisms - -cable_properties = arbor.neuron_cable_properties() -cable_properties.catalogue = arbor.default_catalogue() -cable_properties.catalogue.extend(arbor.stochastic_catalogue(), "") - +cell = A.cable_cell(tree, decor, labels) -# (4) Recipe - -class stdp_recipe(arbor.recipe): - def __init__(self, cell, props, gens): - arbor.recipe.__init__(self) +# (3) Recipe +class stdp_recipe(A.recipe): + def __init__(self, cell, time_lags): + A.recipe.__init__(self) self.the_cell = cell - self.the_props = props - self.the_gens = gens + # create extended catalogue including stochastic mechanisms + self.the_props = A.neuron_cable_properties() + self.the_props.catalogue.extend(A.stochastic_catalogue(), "") + self.time_lags = time_lags + self.num = len(time_lags) def num_cells(self): - return 1 + return self.num - def cell_kind(self, gid): - return arbor.cell_kind.cable + def cell_kind(self, _): + return A.cell_kind.cable - def cell_description(self, gid): + def cell_description(self, _): return self.the_cell - def global_properties(self, kind): + def global_properties(self, _): return self.the_props - def probes(self, gid): - return [ - arbor.cable_probe_point_state_cell("calcium_based_synapse", "rho", "rho") - ] + def probes(self, _): + return [A.cable_probe_point_state_cell("calcium_based_synapse", "rho", "rho")] def event_generators(self, gid): - return self.the_gens - - -# (5) run simulation for a given time lag - - -def run(time_lag): - # Time between stimuli - T = 1000.0 / f - - # Simulation duration - t1 = num_spikes * T + # Time difference between post and pre spike including delay + d = D - self.time_lags[gid] + # Stimulus and sample times + t0_post = max(-d.value, 0) * U.ms + t0_pre = max(d.value, 0) * U.ms + sched_post = A.regular_schedule(t0_post, T, tfinal) + sched_pre = A.regular_schedule(t0_pre, T, tfinal) - # Time difference between post and pre spike including delay - d = -time_lag + D + # Create strong enough driving stimulus + generators = [A.event_generator("driving_synapse", 1.0, sched_post)] - # Stimulus and sample times - t0_post = 0.0 if d >= 0 else -d - t0_pre = d if d >= 0 else 0.0 - stimulus_times_post = numpy.arange(t0_post, t1, T) - stimulus_times_pre = numpy.arange(t0_pre, t1, T) - sched_post = arbor.explicit_schedule(stimulus_times_post) - sched_pre = arbor.explicit_schedule(stimulus_times_pre) + # Stimulus for calcium synapses + for ix, _ in enumerate(rho_0): + # Zero weight -> just modify synaptic weight via stdp + generators.append( + A.event_generator(f"calcium_synapse_{ix}", 0.0, sched_pre) + ) + return generators - # Create strong enough driving stimulus - generators = [arbor.event_generator("driving_synapse", 1.0, sched_post)] - # Stimulus for calcium synapses - for i in range(num_synapses): - # Zero weight -> just modify synaptic weight via stdp - generators.append(arbor.event_generator(f"calcium_synapse_{i}", 0.0, sched_pre)) +# (4) run simulation for all lags +# Create recipe +rec = stdp_recipe(cell, stdp_dt) - # Create recipe - recipe = stdp_recipe(cell, cable_properties, generators) +# Create simulation +print(A.config()) +sim = A.simulation(rec, seed=42) - # Select one thread and no GPU - alloc = arbor.proc_allocation(threads=1, gpu_id=None) - context = arbor.context(alloc, mpi=None) - domains = arbor.partition_load_balance(recipe, context) +# Register probe to read out stdp curve +handles = [ + sim.sample((gid, "rho"), A.explicit_schedule([tfinal - dt])) + for gid in range(len(stdp_dt)) +] - # Get random seed - random_seed = random.getrandbits(64) +sim.record(A.spike_recording.all) - # Create simulation - sim = arbor.simulation(recipe, context, domains, random_seed) +# Run simulation +sim.run(tfinal, dt) - # Register prope to read out stdp curve - handle = sim.sample((0, "rho"), arbor.explicit_schedule([t1 - dt])) - # Run simulation - sim.run(t1, dt) - - # Process sampled data - data, meta = sim.samples(handle)[0] - data_down = data[-1, 1 : ensemble_per_rho_0 + 1] - data_up = data[-1, ensemble_per_rho_0 + 1 :] - # Initial fraction of synapses in DOWN state - beta = 0.5 - # Synaptic strength ratio UP to DOWN (w1/w0) - b = 5 - # Transition indicator form DOWN to UP - P_UA = (data_down > 0.5).astype(float) - # Transition indicator from UP to DOWN - P_DA = (data_up < 0.5).astype(float) - # Return change in synaptic strength - ds_A = ( - (1 - P_UA) * beta - + P_DA * (1 - beta) - + b * (P_UA * beta + (1 - P_DA) * (1 - beta)) - ) / (beta + (1 - beta) * b) - return pandas.DataFrame({"ds": ds_A, "ms": time_lag, "type": "Arbor"}) - - -with multiprocessing.Pool() as p: - results = p.map(run, stdp_dt) - -results = map(run, stdp_dt) - -ref = numpy.array( +# (5) Process sampled data +# Add reference +ref = np.array( [ [-100, 0.9793814432989691], [-95, 0.981715028725338], @@ -225,11 +181,30 @@ def run(time_lag): [100, 0.9918730512544945], ] ) -df_ref = pandas.DataFrame({"ds": ref[:, 1], "ms": ref[:, 0], "type": "Reference"}) -df = pandas.concat(results, ignore_index=True) -df = pandas.concat([df, df_ref], ignore_index=True) -plt = seaborn.relplot(kind="line", data=df, x="ms", y="ds", hue="type") +results = [pd.DataFrame({"ds": ref[:, 1], "ms": ref[:, 0], "type": "Reference"})] +for handle, time_lag in zip(handles, stdp_dt): + data, _ = sim.samples(handle)[0] + data_down = data[-1, 1 : ensemble_per_rho_0 + 1] + data_up = data[-1, ensemble_per_rho_0 + 1 :] + # Initial fraction of synapses in DOWN state + beta = 0.5 + # Synaptic strength ratio UP to DOWN (w1/w0) + b = 5 + # Transition indicator form DOWN to UP + P_UA = (data_down > 0.5).astype(float) + # Transition indicator from UP to DOWN + P_DA = (data_up < 0.5).astype(float) + # Return change in synaptic strength + ds_A = ( + (1 - P_UA) * beta + + P_DA * (1 - beta) + + b * (P_UA * beta + (1 - P_DA) * (1 - beta)) + ) / (beta + (1 - beta) * b) + results.append(pd.DataFrame({"ds": ds_A, "ms": time_lag.value, "type": "Arbor"})) + +df = pd.concat(results, ignore_index=True) +plt = sns.relplot(kind="line", data=df, x="ms", y="ds", hue="type") plt.set_xlabels("lag time difference (ms)") plt.set_ylabels("change in synaptic strenght (after/before)") plt._legend.set_title("") diff --git a/python/example/diffusion.py b/python/example/diffusion.py index cc51201a6c..8dda5e78f7 100644 --- a/python/example/diffusion.py +++ b/python/example/diffusion.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import arbor as A +from arbor import units as U import seaborn as sns import matplotlib.pyplot as plt @@ -15,35 +16,43 @@ def __init__(self, cell, probes): def num_cells(self): return 1 - def cell_kind(self, gid): + def cell_kind(self, _): return A.cell_kind.cable - def cell_description(self, gid): + def cell_description(self, _): return self.the_cell - def probes(self, gid): + def probes(self, _): return self.the_probes - def global_properties(self, kind): + def global_properties(self, _): return self.the_props - def event_generators(self, gid): - return [A.event_generator("Zap", 0.005, A.explicit_schedule([0.0]))] + def event_generators(self, _): + return [A.event_generator("Zap", 0.005, A.explicit_schedule([0.0 * U.ms]))] tree = A.segment_tree() -s = tree.append(A.mnpos, A.mpoint(-3, 0, 0, 3), A.mpoint(3, 0, 0, 3), tag=1) -_ = tree.append(s, A.mpoint(3, 0, 0, 1), A.mpoint(33, 0, 0, 1), tag=3) - -dec = A.decor() -dec.set_ion("na", int_con=0.0, diff=0.005) -dec.place("(location 0 0.5)", A.synapse("inject/x=na", {"alpha": 200.0}), "Zap") -dec.paint("(all)", A.density("decay/x=na")) -dec.discretization(A.cv_policy("(max-extent 5)")) - -# Set up ion diffusion -dec.set_ion("na", int_con=1.0, ext_con=140, rev_pot=50, diff=0.005) -dec.paint("(tag 1)", ion_name="na", int_con=100.0, diff=0.01) +s = A.mnpos +s = tree.append(s, (-3, 0, 0, 3), (3, 0, 0, 3), tag=1) +_ = tree.append(s, (3, 0, 0, 1), (33, 0, 0, 1), tag=3) + +dec = ( + A.decor() + .set_ion("na", int_con=0.0 * U.mM, diff=0.005 * U.m2 / U.s) + .place("(location 0 0.5)", A.synapse("inject/x=na", {"alpha": 200.0}), "Zap") + .paint("(all)", A.density("decay/x=na")) + .discretization(A.cv_policy("(max-extent 5)")) + # Set up ion diffusion + .set_ion( + "na", + int_con=1.0 * U.mM, + ext_con=140 * U.mM, + rev_pot=50 * U.mV, + diff=0.005 * U.m2 / U.s, + ) + .paint("(tag 1)", ion="na", int_con=100.0 * U.mM, diff=0.01 * U.m2 / U.s) +) prb = [ A.cable_probe_ion_diff_concentration_cell("na", "nad"), @@ -51,9 +60,9 @@ def event_generators(self, gid): cel = A.cable_cell(tree, dec) rec = recipe(cel, prb) sim = A.simulation(rec) -hdl = (sim.sample((0, "nad"), A.regular_schedule(0.1)),) +hdl = (sim.sample((0, "nad"), A.regular_schedule(0.1 * U.ms)),) -sim.run(tfinal=0.5) +sim.run(tfinal=0.5 * U.ms) sns.set_theme() fg, ax = plt.subplots() diff --git a/python/example/dynamic-catalogue.py b/python/example/dynamic-catalogue.py index 6ed1a83f71..da14b77fca 100644 --- a/python/example/dynamic-catalogue.py +++ b/python/example/dynamic-catalogue.py @@ -2,20 +2,21 @@ from pathlib import Path -import arbor as arb +import arbor as A +from arbor import units as U cat = Path("cat-catalogue.so").resolve() -class recipe(arb.recipe): +class recipe(A.recipe): def __init__(self): - arb.recipe.__init__(self) - self.tree = arb.segment_tree() - self.tree.append(arb.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1) - self.props = arb.neuron_cable_properties() - self.props.catalogue = arb.load_catalogue(cat) - d = arb.decor().paint("(all)", "dummy").set_property(Vm=0.0) - self.cell = arb.cable_cell(self.tree, d) + A.recipe.__init__(self) + self.tree = A.segment_tree() + self.tree.append(A.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1) + self.props = A.neuron_cable_properties() + self.props.catalogue = A.load_catalogue(cat) + d = A.decor().paint("(all)", "dummy").set_property(Vm=0.0 * U.mV) + self.cell = A.cable_cell(self.tree, d) def global_properties(self, _): return self.props @@ -24,7 +25,7 @@ def num_cells(self): return 1 def cell_kind(self, gid): - return arb.cell_kind.cable + return A.cell_kind.cable def cell_description(self, gid): return self.cell @@ -40,5 +41,5 @@ def cell_description(self, gid): exit(1) rcp = recipe() -sim = arb.simulation(rcp) -sim.run(tfinal=30) +sim = A.simulation(rcp) +sim.run(tfinal=30 * U.ms) diff --git a/python/example/gap_junctions.py b/python/example/gap_junctions.py index 9dd5ac27fa..67c028a661 100644 --- a/python/example/gap_junctions.py +++ b/python/example/gap_junctions.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -import arbor -import pandas -import seaborn +import arbor as A +from arbor import units as U +import pandas as pd +import seaborn as sns import matplotlib.pyplot as plt # Construct chains of cells linked with gap junctions, @@ -20,18 +21,16 @@ def make_cable_cell(gid): # Build a segment tree - tree = arbor.segment_tree() + tree = A.segment_tree() # Soma with radius 5 μm and length 2 * radius = 10 μm, (tag = 1) - s = tree.append( - arbor.mnpos, arbor.mpoint(-10, 0, 0, 5), arbor.mpoint(0, 0, 0, 5), tag=1 - ) + s = tree.append(A.mnpos, A.mpoint(-10, 0, 0, 5), A.mpoint(0, 0, 0, 5), tag=1) # Single dendrite with radius 2 μm and length 40 μm, (tag = 2) - tree.append(s, arbor.mpoint(0, 0, 0, 2), arbor.mpoint(40, 0, 0, 2), tag=2) + tree.append(s, A.mpoint(0, 0, 0, 2), A.mpoint(40, 0, 0, 2), tag=2) # Label dictionary for cell components - labels = arbor.label_dict( + labels = A.label_dict( { # Mark location for synapse site at midpoint of dendrite (branch 0 soma + dendrite) "synapse_site": "(location 0 0.6)", @@ -44,26 +43,26 @@ def make_cable_cell(gid): # Paint dynamics onto the cell, hh on soma and passive properties on dendrite decor = ( - arbor.decor() - .paint('"soma"', arbor.density("hh")) - .paint('"dend"', arbor.density("pas")) + A.decor() + .paint('"soma"', A.density("hh")) + .paint('"dend"', A.density("pas")) # Attach one synapse and gap junction each on their labeled sites - .place('"synapse_site"', arbor.synapse("expsyn"), "syn") - .place('"gj_site"', arbor.junction("gj"), "gj") + .place('"synapse_site"', A.synapse("expsyn"), "syn") + .place('"gj_site"', A.junction("gj"), "gj") # Attach detector to cell root - .place('"root"', arbor.threshold_detector(-10), "detector") + .place('"root"', A.threshold_detector(-10 * U.mV), "detector") ) - return arbor.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) # Create a recipe that generates connected chains of cells -class chain_recipe(arbor.recipe): +class chain_recipe(A.recipe): def __init__(self, ncells_per_chain, nchains): - arbor.recipe.__init__(self) + A.recipe.__init__(self) self.nchains = nchains self.ncells_per_chain = ncells_per_chain - self.props = arbor.neuron_cable_properties() + self.props = A.neuron_cable_properties() def num_cells(self): return self.ncells_per_chain * self.nchains @@ -72,7 +71,7 @@ def cell_description(self, gid): return make_cable_cell(gid) def cell_kind(self, gid): - return arbor.cell_kind.cable + return A.cell_kind.cable # Create synapse connection between last cell of one chain and first cell of following chain def connections_on(self, gid): @@ -80,9 +79,7 @@ def connections_on(self, gid): return [] else: src = gid - 1 - w = 0.05 - d = 10 - return [arbor.connection((src, "detector"), "syn", w, d)] + return [A.connection((src, "detector"), "syn", 0.05, 10 * U.ms)] # Create gap junction connections between a cell within a chain and its neighbor(s) def gap_junctions_on(self, gid): @@ -95,23 +92,23 @@ def gap_junctions_on(self, gid): prev_cell = gid - 1 if next_cell < chain_end: - conns.append(arbor.gap_junction_connection((gid + 1, "gj"), "gj", 0.015)) + conns.append(A.gap_junction_connection((gid + 1, "gj"), "gj", 0.015)) if prev_cell >= chain_begin: - conns.append(arbor.gap_junction_connection((gid - 1, "gj"), "gj", 0.015)) + conns.append(A.gap_junction_connection((gid - 1, "gj"), "gj", 0.015)) return conns # Event generator at first cell def event_generators(self, gid): if gid == 0: - sched = arbor.explicit_schedule([1]) + sched = A.explicit_schedule([1 * U.ms]) weight = 0.1 - return [arbor.event_generator("syn", weight, sched)] + return [A.event_generator("syn", weight, sched)] return [] # Place a probe at the root of each cell def probes(self, gid): - return [arbor.cable_probe_membrane_voltage('"root"', "Um")] + return [A.cable_probe_membrane_voltage('"root"', "Um")] def global_properties(self, kind): return self.props @@ -130,18 +127,18 @@ def global_properties(self, kind): recipe = chain_recipe(ncells_per_chain, nchains) # Create a default simulation -sim = arbor.simulation(recipe) +sim = A.simulation(recipe) # Set spike generators to record -sim.record(arbor.spike_recording.all) +sim.record(A.spike_recording.all) # Sampler handles = [ - sim.sample((gid, "Um"), arbor.regular_schedule(0.1)) for gid in range(ncells) + sim.sample((gid, "Um"), A.regular_schedule(0.1 * U.ms)) for gid in range(ncells) ] -# Run simulation for 100 ms -sim.run(100) +# Run simulation +sim.run(100 * U.ms) print("Simulation finished") # Print spike times @@ -155,11 +152,11 @@ def global_properties(self, kind): for gid in range(ncells): samples, meta = sim.samples(handles[gid])[0] df_list.append( - pandas.DataFrame( + pd.DataFrame( {"t/ms": samples[:, 0], "U/mV": samples[:, 1], "Cell": f"cell {gid}"} ) ) -df = pandas.concat(df_list, ignore_index=True) -seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", hue="Cell", errorbar=None) +df = pd.concat(df_list, ignore_index=True) +sns.relplot(data=df, kind="line", x="t/ms", y="U/mV", hue="Cell", errorbar=None) plt.show() diff --git a/python/example/network_ring.py b/python/example/network_ring.py index 73f7a4ce6a..7dc460d494 100755 --- a/python/example/network_ring.py +++ b/python/example/network_ring.py @@ -1,54 +1,48 @@ #!/usr/bin/env python3 # This script is included in documentation. Adapt line numbers if touched. -import arbor +import arbor as A +from arbor import units as U import pandas # You may have to pip install these import seaborn # You may have to pip install these from math import sqrt -print(arbor.__path__) - -# Construct a cell with the following morphology. -# The soma (at the root of the tree) is marked 's', and -# the end of each branch i is marked 'bi'. -# -# b1 -# / -# s----b0 -# \ -# b2 - def make_cable_cell(gid): # (1) Build a segment tree - tree = arbor.segment_tree() - + # The dendrite (dend) attaches to the soma and has two simple segments + # attached. + # + # left + # / + # soma - dend + # \ + # right + tree = A.segment_tree() + root = A.mnpos # Soma (tag=1) with radius 6 μm, modelled as cylinder of length 2*radius - s = tree.append( - arbor.mnpos, arbor.mpoint(-12, 0, 0, 6), arbor.mpoint(0, 0, 0, 6), tag=1 - ) - - # (b0) Single dendrite (tag=3) of length 50 μm and radius 2 μm attached to soma. - b0 = tree.append(s, arbor.mpoint(0, 0, 0, 2), arbor.mpoint(50, 0, 0, 2), tag=3) - + soma = tree.append(root, (-12, 0, 0, 6), (0, 0, 0, 6), tag=1) + # Single dendrite (tag=3) of length 50 μm and radius 2 μm attached to soma. + dend = tree.append(soma, (0, 0, 0, 2), (50, 0, 0, 2), tag=3) # Attach two dendrites (tag=3) of length 50 μm to the end of the first dendrite. - # (b1) Radius tapers from 2 to 0.5 μm over the length of the dendrite. - tree.append( - b0, - arbor.mpoint(50, 0, 0, 2), - arbor.mpoint(50 + 50 / sqrt(2), 50 / sqrt(2), 0, 0.5), + # Radius tapers from 2 to 0.5 μm over the length of the dendrite. + l = 50 / sqrt(2) + _ = tree.append( + dend, + (50, 0, 0, 2), + (50 + l, l, 0, 0.5), tag=3, ) - # (b2) Constant radius of 1 μm over the length of the dendrite. - tree.append( - b0, - arbor.mpoint(50, 0, 0, 1), - arbor.mpoint(50 + 50 / sqrt(2), -50 / sqrt(2), 0, 1), + # Constant radius of 1 μm over the length of the dendrite. + _ = tree.append( + dend, + (50, 0, 0, 1), + (50 + l, -l, 0, 1), tag=3, ) # Associate labels to tags - labels = arbor.label_dict( + labels = A.label_dict( { "soma": "(tag 1)", "dend": "(tag 3)", @@ -61,29 +55,27 @@ def make_cable_cell(gid): # (3) Create a decor and a cable_cell decor = ( - arbor.decor() + A.decor() # Put hh dynamics on soma, and passive properties on the dendrites. - .paint('"soma"', arbor.density("hh")).paint('"dend"', arbor.density("pas")) + .paint('"soma"', A.density("hh")).paint('"dend"', A.density("pas")) # (4) Attach a single synapse. - .place('"synapse_site"', arbor.synapse("expsyn"), "syn") + .place('"synapse_site"', A.synapse("expsyn"), "syn") # Attach a detector with threshold of -10 mV. - .place('"root"', arbor.threshold_detector(-10), "detector") + .place('"root"', A.threshold_detector(-10 * U.mV), "detector") ) - return arbor.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) # (5) Create a recipe that generates a network of connected cells. -class ring_recipe(arbor.recipe): +class ring_recipe(A.recipe): def __init__(self, ncells): - # The base C++ class constructor must be called first, to ensure that - # all memory in the C++ class is initialized correctly. - arbor.recipe.__init__(self) + # Base class constructor must be called first for proper initialization. + A.recipe.__init__(self) self.ncells = ncells - self.props = arbor.neuron_cable_properties() + self.props = A.neuron_cable_properties() - # (6) The num_cells method that returns the total number of cells in the model - # must be implemented. + # (6) Returns the total number of cells in the model; must be implemented. def num_cells(self): return self.ncells @@ -91,31 +83,32 @@ def num_cells(self): def cell_description(self, gid): return make_cable_cell(gid) - # The kind method returns the type of cell with gid. - # Note: this must agree with the type returned by cell_description. - def cell_kind(self, gid): - return arbor.cell_kind.cable + # Return the type of cell; must be implemented and match cell_description. + def cell_kind(self, _): + return A.cell_kind.cable - # (8) Make a ring network. For each gid, provide a list of incoming connections. + # (8) For each gid, provide a list of incoming connections. def connections_on(self, gid): + # This defines the ring by connecting from the last gid. The first src + # comes from the _last_ gid, closing the ring. src = (gid - 1) % self.ncells w = 0.01 # 0.01 μS on expsyn - d = 5 # ms delay - return [arbor.connection((src, "detector"), "syn", w, d)] + d = 5 * U.ms + return [A.connection((src, "detector"), "syn", w, d)] # (9) Attach a generator to the first cell in the ring. def event_generators(self, gid): if gid == 0: - sched = arbor.explicit_schedule([1]) # one event at 1 ms + sched = A.explicit_schedule([1 * U.ms]) # one event at 1 ms weight = 0.1 # 0.1 μS on expsyn - return [arbor.event_generator("syn", weight, sched)] + return [A.event_generator("syn", weight, sched)] return [] # (10) Place a probe at the root of each cell. def probes(self, gid): - return [arbor.cable_probe_membrane_voltage('"root"', "Um")] + return [A.cable_probe_membrane_voltage('"root"', "Um")] - def global_properties(self, kind): + def global_properties(self, _): return self.props @@ -129,18 +122,18 @@ def global_properties(self, kind): # - Use GPU if present # - No MPI # Other constructors of simulation can be used to change all of these. -sim = arbor.simulation(recipe) +sim = A.simulation(recipe) # (13) Set spike generators to record -sim.record(arbor.spike_recording.all) +sim.record(A.spike_recording.all) # (14) Attach a sampler to the voltage probe on cell 0. Sample rate of 10 sample every ms. handles = [ - sim.sample((gid, "Um"), arbor.regular_schedule(0.1)) for gid in range(ncells) + sim.sample((gid, "Um"), A.regular_schedule(0.1 * U.ms)) for gid in range(ncells) ] # (15) Run simulation for 100 ms -sim.run(100) +sim.run(100 * U.ms) print("Simulation finished") # (16) Print spike times @@ -150,16 +143,15 @@ def global_properties(self, kind): # (17) Plot the recorded voltages over time. print("Plotting results ...") -df_list = [] +dfs = [] for gid in range(ncells): samples, meta = sim.samples(handles[gid])[0] - df_list.append( + dfs.append( pandas.DataFrame( {"t/ms": samples[:, 0], "U/mV": samples[:, 1], "Cell": f"cell {gid}"} ) ) - -df = pandas.concat(df_list, ignore_index=True) +df = pandas.concat(dfs, ignore_index=True) seaborn.relplot( data=df, kind="line", x="t/ms", y="U/mV", hue="Cell", errorbar=None ).savefig("network_ring_result.svg") diff --git a/python/example/network_ring_gpu.py b/python/example/network_ring_gpu.py index e54e099992..fb46bdb565 100644 --- a/python/example/network_ring_gpu.py +++ b/python/example/network_ring_gpu.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # This script is included in documentation. Adapt line numbers if touched. -import arbor -import pandas # You may have to pip install these -import seaborn # You may have to pip install these +import arbor as A +from arbor import units as U +import pandas as pd # You may have to pip install these +import seaborn as sns # You may have to pip install these from math import sqrt # Construct a cell with the following morphology. @@ -17,37 +18,36 @@ # b2 -def make_cable_cell(gid): +def make_cable_cell(_): # (1) Build a segment tree - tree = arbor.segment_tree() + tree = A.segment_tree() # Soma (tag=1) with radius 6 μm, modelled as cylinder of length 2*radius - s = tree.append( - arbor.mnpos, arbor.mpoint(-12, 0, 0, 6), arbor.mpoint(0, 0, 0, 6), tag=1 - ) + s = A.mnpos + s = tree.append(s, (-12, 0, 0, 6), (0, 0, 0, 6), tag=1) # Single dendrite (tag=3) of length 50 μm and radius 2 μm attached to soma. - b0 = tree.append(s, arbor.mpoint(0, 0, 0, 2), arbor.mpoint(50, 0, 0, 2), tag=3) + b0 = tree.append(s, (0, 0, 0, 2), (50, 0, 0, 2), tag=3) # Attach two dendrites (tag=3) of length 50 μm to the end of the first dendrite. # As there's no further use for them, we discard the returned handles. # (b1) Radius tapers from 2 to 0.5 μm over the length of the dendrite. _ = tree.append( b0, - arbor.mpoint(50, 0, 0, 2), - arbor.mpoint(50 + 50 / sqrt(2), 50 / sqrt(2), 0, 0.5), + (50, 0, 0, 2), + (50 + 50 / sqrt(2), 50 / sqrt(2), 0, 0.5), tag=3, ) # (b2) Constant radius of 1 μm over the length of the dendrite. _ = tree.append( b0, - arbor.mpoint(50, 0, 0, 1), - arbor.mpoint(50 + 50 / sqrt(2), -50 / sqrt(2), 0, 1), + (50, 0, 0, 1), + (50 + 50 / sqrt(2), -50 / sqrt(2), 0, 1), tag=3, ) # Associate labels to tags - labels = arbor.label_dict() + labels = A.label_dict() labels["soma"] = "(tag 1)" labels["dend"] = "(tag 3)" @@ -57,31 +57,31 @@ def make_cable_cell(gid): labels["root"] = "(root)" # (3) Create a decor and a cable_cell - decor = arbor.decor() + decor = A.decor() # Put hh dynamics on soma, and passive properties on the dendrites. - decor.paint('"soma"', arbor.density("hh")) - decor.paint('"dend"', arbor.density("pas")) + decor.paint('"soma"', A.density("hh")) + decor.paint('"dend"', A.density("pas")) # (4) Attach a single synapse. - decor.place('"synapse_site"', arbor.synapse("expsyn"), "syn") + decor.place('"synapse_site"', A.synapse("expsyn"), "syn") # Attach a detector with threshold of -10 mV. - decor.place('"root"', arbor.threshold_detector(-10), "detector") + decor.place('"root"', A.threshold_detector(-10 * U.mV), "detector") - cell = arbor.cable_cell(tree, decor, labels) + cell = A.cable_cell(tree, decor, labels) return cell # (5) Create a recipe that generates a network of connected cells. -class ring_recipe(arbor.recipe): +class ring_recipe(A.recipe): def __init__(self, ncells): # The base C++ class constructor must be called first, to ensure that # all memory in the C++ class is initialized correctly. - arbor.recipe.__init__(self) + A.recipe.__init__(self) self.ncells = ncells - self.props = arbor.neuron_cable_properties() + self.props = A.neuron_cable_properties() # (6) The num_cells method that returns the total number of cells in the model # must be implemented. @@ -94,40 +94,40 @@ def cell_description(self, gid): # The kind method returns the type of cell with gid. # Note: this must agree with the type returned by cell_description. - def cell_kind(self, gid): - return arbor.cell_kind.cable + def cell_kind(self, _): + return A.cell_kind.cable # (8) Make a ring network. For each gid, provide a list of incoming connections. def connections_on(self, gid): src = (gid - 1) % self.ncells w = 0.01 # 0.01 μS on expsyn d = 5 # ms delay - return [arbor.connection((src, "detector"), "syn", w, d)] + return [A.connection((src, "detector"), "syn", w, d * U.ms)] # (9) Attach a generator to the first cell in the ring. def event_generators(self, gid): if gid == 0: - sched = arbor.explicit_schedule([1]) # one event at 1 ms + sched = A.explicit_schedule([1 * U.ms]) # one event at 1 ms weight = 0.1 # 0.1 μS on expsyn - return [arbor.event_generator("syn", weight, sched)] + return [A.event_generator("syn", weight, sched)] return [] # (10) Place a probe at the root of each cell. - def probes(self, gid): - return [arbor.cable_probe_membrane_voltage('"root"', "Um")] + def probes(self, _): + return [A.cable_probe_membrane_voltage('"root"', "Um")] - def global_properties(self, kind): + def global_properties(self, _): return self.props # (11) Set up the hardware context # gpu_id set to None will not use a GPU. # gpu_id=0 instructs Arbor to the first GPU present in your system -context = arbor.context(gpu_id=None) +context = A.context(gpu_id=None) print(context) # (12) Set up and start the meter manager -meters = arbor.meter_manager() +meters = A.meter_manager() meters.start(context) # (13) Instantiate recipe @@ -136,36 +136,38 @@ def global_properties(self, kind): meters.checkpoint("recipe-create", context) # (14) Define a hint at to the execution. -hint = arbor.partition_hint() +hint = A.partition_hint() hint.prefer_gpu = True hint.gpu_group_size = 1000 print(hint) -hints = {arbor.cell_kind.cable: hint} +hints = {A.cell_kind.cable: hint} # (15) Domain decomp -decomp = arbor.partition_load_balance(recipe, context, hints) +decomp = A.partition_load_balance(recipe, context, hints) print(decomp) meters.checkpoint("load-balance", context) # (16) Simulation init and set spike generators to record -sim = arbor.simulation(recipe, context, decomp) -sim.record(arbor.spike_recording.all) -handles = [sim.sample((gid, "Um"), arbor.regular_schedule(1)) for gid in range(ncells)] +sim = A.simulation(recipe, context, decomp) +sim.record(A.spike_recording.all) +handles = [ + sim.sample((gid, "Um"), A.regular_schedule(1 * U.ms)) for gid in range(ncells) +] meters.checkpoint("simulation-init", context) # (17) Run simulation -sim.run(ncells * 5) +sim.run(ncells * 5 * U.ms) print("Simulation finished") meters.checkpoint("simulation-run", context) # (18) Results # Print profiling information -print(f"{arbor.meter_report(meters, context)}") +print(f"{A.meter_report(meters, context)}") # Print spike times print("spikes:") -for sp in sim.spikes(): - print(" ", sp) +for (gid, lid), t in sim.spikes(): + print(f" * t={t:.3f}ms gid={gid} lid={lid}") # Plot the recorded voltages over time. print("Plotting results ...") @@ -173,12 +175,12 @@ def global_properties(self, kind): for gid in range(ncells): samples, meta = sim.samples(handles[gid])[0] df_list.append( - pandas.DataFrame( + pd.DataFrame( {"t/ms": samples[:, 0], "U/mV": samples[:, 1], "Cell": f"cell {gid}"} ) ) -df = pandas.concat(df_list, ignore_index=True) -seaborn.relplot( +df = pd.concat(df_list, ignore_index=True) +sns.relplot( data=df, kind="line", x="t/ms", y="U/mV", hue="Cell", errorbar=None ).savefig("network_ring_gpu_result.svg") diff --git a/python/example/network_ring_mpi.py b/python/example/network_ring_mpi.py index 73621685ed..98dfb73b1e 100644 --- a/python/example/network_ring_mpi.py +++ b/python/example/network_ring_mpi.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # This script is included in documentation. Adapt line numbers if touched. -import arbor -import pandas +import arbor as A +from arbor import units as U +import pandas as pd from math import sqrt # Run with srun -n NJOBS python network_ring_mpi.py @@ -20,35 +21,34 @@ def make_cable_cell(gid): # (1) Build a segment tree - tree = arbor.segment_tree() + tree = A.segment_tree() # Soma (tag=1) with radius 6 μm, modelled as cylinder of length 2*radius - s = tree.append( - arbor.mnpos, arbor.mpoint(-12, 0, 0, 6), arbor.mpoint(0, 0, 0, 6), tag=1 - ) + s = A.mnpos + s = tree.append(s, (-12, 0, 0, 6), (0, 0, 0, 6), tag=1) # Single dendrite (tag=3) of length 50 μm and radius 2 μm attached to soma. - b0 = tree.append(s, arbor.mpoint(0, 0, 0, 2), arbor.mpoint(50, 0, 0, 2), tag=3) + b0 = tree.append(s, (0, 0, 0, 2), (50, 0, 0, 2), tag=3) # Attach two dendrites (tag=3) of length 50 μm to the end of the first dendrite. # As there's no further use for them, we discard the returned handles. # (b1) Radius tapers from 2 to 0.5 μm over the length of the dendrite. _ = tree.append( b0, - arbor.mpoint(50, 0, 0, 2), - arbor.mpoint(50 + 50 / sqrt(2), 50 / sqrt(2), 0, 0.5), + (50, 0, 0, 2), + (50 + 50 / sqrt(2), 50 / sqrt(2), 0, 0.5), tag=3, ) # (b2) Constant radius of 1 μm over the length of the dendrite. _ = tree.append( b0, - arbor.mpoint(50, 0, 0, 1), - arbor.mpoint(50 + 50 / sqrt(2), -50 / sqrt(2), 0, 1), + (50, 0, 0, 1), + (50 + 50 / sqrt(2), -50 / sqrt(2), 0, 1), tag=3, ) # Associate labels to tags - labels = arbor.label_dict( + labels = A.label_dict( { "soma": "(tag 1)", "dend": "(tag 3)", @@ -61,26 +61,26 @@ def make_cable_cell(gid): # (3) Create a decor and a cable_cell decor = ( - arbor.decor() + A.decor() # Put hh dynamics on soma, and passive properties on the dendrites. - .paint('"soma"', arbor.density("hh")).paint('"dend"', arbor.density("pas")) + .paint('"soma"', A.density("hh")).paint('"dend"', A.density("pas")) # (4) Attach a single synapse. - .place('"synapse_site"', arbor.synapse("expsyn"), "syn") + .place('"synapse_site"', A.synapse("expsyn"), "syn") # Attach a detector with threshold of -10 mV. - .place('"root"', arbor.threshold_detector(-10), "detector") + .place('"root"', A.threshold_detector(-10), "detector") ) - return arbor.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) # (5) Create a recipe that generates a network of connected cells. -class ring_recipe(arbor.recipe): +class ring_recipe(A.recipe): def __init__(self, ncells): # The base C++ class constructor must be called first, to ensure that # all memory in the C++ class is initialized correctly. - arbor.recipe.__init__(self) + A.recipe.__init__(self) self.ncells = ncells - self.props = arbor.neuron_cable_properties() + self.props = A.neuron_cable_properties() # (6) The num_cells method that returns the total number of cells in the model # must be implemented. @@ -94,26 +94,26 @@ def cell_description(self, gid): # The kind method returns the type of cell with gid. # Note: this must agree with the type returned by cell_description. def cell_kind(self, gid): - return arbor.cell_kind.cable + return A.cell_kind.cable # (8) Make a ring network. For each gid, provide a list of incoming connections. def connections_on(self, gid): src = (gid - 1) % self.ncells w = 0.01 # 0.01 μS on expsyn - d = 5 # ms delay - return [arbor.connection((src, "detector"), "syn", w, d)] + d = 5 * U.ms # delay + return [A.connection((src, "detector"), "syn", w, d)] # (9) Attach a generator to the first cell in the ring. def event_generators(self, gid): if gid == 0: - sched = arbor.explicit_schedule([1]) # one event at 1 ms + sched = A.explicit_schedule([1 * U.ms]) # one event at 1 ms weight = 0.1 # 0.1 μS on expsyn - return [arbor.event_generator("syn", weight, sched)] + return [A.event_generator("syn", weight, sched)] return [] # (10) Place a probe at the root of each cell. def probes(self, gid): - return [arbor.cable_probe_membrane_voltage('"root"')] + return [A.cable_probe_membrane_voltage('"root"')] def global_properties(self, kind): return self.props @@ -124,24 +124,24 @@ def global_properties(self, kind): recipe = ring_recipe(ncells) # (12) Create an MPI communicator, and use it to create a hardware context -arbor.mpi_init() -comm = arbor.mpi_comm() +A.mpi_init() +comm = A.mpi_comm() print(comm) -context = arbor.context(mpi=comm) +context = A.context(mpi=comm) print(context) # (13) Create a default domain decomposition and simulation -sim = arbor.simulation(recipe, context) +sim = A.simulation(recipe, context) # (14) Set spike generators to record -sim.record(arbor.spike_recording.all) +sim.record(A.spike_recording.all) # (15) Attach a sampler to the voltage probe on cell 0. Sample rate of 1 sample every ms. # Sampling period increased w.r.t network_ring.py to reduce amount of data -handles = [sim.sample((gid, 0), arbor.regular_schedule(1)) for gid in range(ncells)] +handles = [sim.sample((gid, 0), A.regular_schedule(1 * U.ms)) for gid in range(ncells)] # (16) Run simulation -sim.run(ncells * 5) +sim.run(ncells * 5 * U.ms) print("Simulation finished") # (17) Store the recorded voltages @@ -151,11 +151,11 @@ def global_properties(self, kind): if len(sim.samples(handles[gid])): samples, meta = sim.samples(handles[gid])[0] df_list.append( - pandas.DataFrame( + pd.DataFrame( {"t/ms": samples[:, 0], "U/mV": samples[:, 1], "Cell": f"cell {gid}"} ) ) if len(df_list): - df = pandas.concat(df_list, ignore_index=True) + df = pd.concat(df_list, ignore_index=True) df.to_csv(f"result_mpi_{context.rank}.csv", float_format="%g") diff --git a/python/example/network_two_cells_gap_junctions.py b/python/example/network_two_cells_gap_junctions.py index 5a2db8574a..c129823072 100755 --- a/python/example/network_two_cells_gap_junctions.py +++ b/python/example/network_two_cells_gap_junctions.py @@ -1,216 +1,148 @@ #!/usr/bin/env python3 -from builtins import enumerate -import arbor -import argparse +import arbor as A +from arbor import units as U +from argparse import ArgumentParser import numpy as np - -import pandas # You may have to pip install these. -import seaborn # You may have to pip install these. +import pandas as pd # You may have to pip install these. +import seaborn as sns # You may have to pip install these. import matplotlib.pyplot as plt -class TwoCellsWithGapJunction(arbor.recipe): - def __init__( - self, probes, Vms, length, radius, cm, rL, g, gj_g, cv_policy_max_extent - ): +class TwoCellsWithGapJunction(A.recipe): + def __init__(self, Vms, length, radius, cm, rL, g, gj_g, max_extent): """ - probes -- list of probes - Vms -- membrane leak potentials of the two cells length -- length of cable in μm radius -- radius of cable in μm - cm -- membrane capacitance in F/m^2 + cm -- membrane capacitance in F/m² rL -- axial resistivity in Ω·cm - g -- membrane conductivity in S/cm^2 + g -- membrane conductivity in S/cm² gj_g -- gap junction conductivity in μS - - cv_policy_max_extent -- maximum extent of control volume in μm + max_extent -- maximum extent of control volume in μm """ - # The base C++ class constructor must be called first, to ensure that - # all memory in the C++ class is initialized correctly. - arbor.recipe.__init__(self) - - self.the_probes = probes + # Call base constructor first to ensure proper initialization + A.recipe.__init__(self) - self.Vms = Vms - self.length = length - self.radius = radius - self.cm = cm - self.rL = rL - self.g = g - self.gj_g = gj_g - - self.cv_policy_max_extent = cv_policy_max_extent - - self.the_props = arbor.neuron_cable_properties() + self.Vms = [Vm * U.mV for Vm in Vms] + self.length = length * U.um + self.radius = radius * U.um + self.area = self.length * 2 * np.pi * self.radius + self.cm = cm * U.F / U.m2 + self.rL = rL * U.Ohm * U.cm + self.g = g * U.S / U.cm2 + self.gj_g = gj_g * U.uS + self.max_extent = max_extent + self.the_props = A.neuron_cable_properties() def num_cells(self): return 2 - def cell_kind(self, gid): - assert gid in [0, 1] - return arbor.cell_kind.cable + def cell_kind(self, _): + return A.cell_kind.cable - def global_properties(self, kind): - assert kind == arbor.cell_kind.cable + def global_properties(self, _): return self.the_props def cell_description(self, gid): - """A high level description of the cell with global identifier gid. + tree = A.segment_tree() + r, l = self.radius.value, self.length.value + tree.append(A.mnpos, (0, 0, 0, r), (l, 0, 0, r), tag=1) - For example the morphology, synapses and ion channels required - to build a multi-compartment neuron. - """ - assert gid in [0, 1] - - tree = arbor.segment_tree() - - tree.append( - arbor.mnpos, - arbor.mpoint(0, 0, 0, self.radius), - arbor.mpoint(self.length, 0, 0, self.radius), - tag=1, - ) - - labels = arbor.label_dict({"cell": "(tag 1)", "gj_site": "(location 0 0.5)"}) + labels = A.label_dict({"midpoint": "(location 0 0.5)"}) decor = ( - arbor.decor() - .set_property(Vm=self.Vms[gid]) - .set_property(cm=self.cm) - .set_property(rL=self.rL) - # add a gap junction mechanism at the "gj_site" location and label that specific mechanism on that location "gj_label" - .place('"gj_site"', arbor.junction("gj", g=self.gj_g), "gj_label") - .paint('"cell"', arbor.density(f"pas/e={self.Vms[gid]}", g=self.g)) + A.decor() + .set_property(Vm=self.Vms[gid], cm=self.cm, rL=self.rL) + .place('"midpoint"', A.junction("gj", g=self.gj_g.value), "gj") + .paint("(all)", A.density(f"pas/e={self.Vms[gid].value}", g=self.g.value)) ) - if self.cv_policy_max_extent is not None: - policy = arbor.cv_policy_max_extent(self.cv_policy_max_extent) - decor.discretization(policy) + if self.max_extent is not None: + decor.discretization(A.cv_policy_max_extent(self.max_extent)) else: - decor.discretization(arbor.cv_policy_single()) + decor.discretization(A.cv_policy_single()) - return arbor.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) def gap_junctions_on(self, gid): - # create a bidirectional gap junction from cell 0 at label "gj_label" to cell 1 at label "gj_label" and back. - if gid == 0: - tgt = 1 - elif gid == 1: - tgt = 0 - else: - raise RuntimeError("Invalid GID for example.") - return [arbor.gap_junction_connection((tgt, "gj_label"), "gj_label", 1)] - - def probes(self, gid): - assert gid in [0, 1] - return self.the_probes - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Two cells connected via a gap junction" + return [A.gap_junction_connection(((gid + 1) % 2, "gj"), "gj", 1)] + + def probes(self, _): + return [A.cable_probe_membrane_voltage('"midpoint"', "Um")] + + +# parse the command line arguments +parser = ArgumentParser(description="Two cells connected via a gap junction") + +parser.add_argument( + "--Vms", + help="membrane leak potentials [mV]", + type=float, + default=[-100, -60], + nargs=2, +) +parser.add_argument("--length", help="cell length [μm]", type=float, default=100) +parser.add_argument("--radius", help="cell radius [μm]", type=float, default=3) +parser.add_argument( + "--cm", help="membrane capacitance [F/m²]", type=float, default=0.005 +) +parser.add_argument("--rL", help="axial resistivity [Ω·cm]", type=float, default=90) +parser.add_argument("--g", help="leak conductivity [S/cm²]", type=float, default=0.001) +parser.add_argument( + "--gj_g", help="gap junction conductivity [μS]", type=float, default=0.01 +) +parser.add_argument("--max-extent", help="discretization length [μm]", type=float) + +args = parser.parse_args() + +# set up membrane voltage probes at the position of the gap junction +rec = TwoCellsWithGapJunction(**vars(args)) + +# configure the simulation and handles for the probes +sim = A.simulation(rec) + +T = 5 * U.ms +dt = 0.01 * U.ms + +# generate handles for all probes and gids. +handles = [sim.sample((gid, "Um"), A.regular_schedule(dt)) for gid in [0, 1]] + +# run the simulation +sim.run(tfinal=T, dt=dt) + +# retrieve the sampled membrane voltages +print("Plotting results ...") +df_list = [] +for gid, handle in enumerate(handles): + data, meta = sim.samples(handle)[0] + df_list.append( + pd.DataFrame({"t/ms": data[:, 0], "U/mV": data[:, 1], "Cell": f"{gid}"}) ) +df = pd.concat(df_list, ignore_index=True) - parser.add_argument( - "--Vms", - help="membrane leak potentials in mV", - type=float, - default=[-100, -60], - nargs=2, - ) - parser.add_argument("--length", help="cell length in μm", type=float, default=100) - parser.add_argument("--radius", help="cell radius in μm", type=float, default=3) - parser.add_argument( - "--cm", help="membrane capacitance in F/m^2", type=float, default=0.005 - ) - parser.add_argument( - "--rL", help="axial resistivity in Ω·cm", type=float, default=90 - ) - parser.add_argument( - "--g", help="membrane conductivity in S/cm^2", type=float, default=0.001 - ) +# plot the membrane potentials of the two cells as function of time +fg, ax = plt.subplots() +sns.lineplot(ax=ax, data=df, x="t/ms", y="U/mV", hue="Cell", errorbar=None) - parser.add_argument( - "--gj_g", help="gap junction conductivity in μS", type=float, default=0.01 - ) +# use total and gap junction conductance to compute weight +w = (rec.gj_g + rec.area * rec.g) / (2 * rec.gj_g + rec.area * rec.g) - parser.add_argument( - "--cv_policy_max_extent", - help="maximum extent of control volume in μm", - type=float, - ) - # parse the command line arguments - args = parser.parse_args() - - # set up membrane voltage probes at the position of the gap junction - probes = [arbor.cable_probe_membrane_voltage('"gj_site"', "Um")] - recipe = TwoCellsWithGapJunction(probes, **vars(args)) - - # configure the simulation and handles for the probes - sim = arbor.simulation(recipe) - - T = 5 - dt = 0.01 - handles = [ - sim.sample((gid, "Um"), arbor.regular_schedule(dt)) - for i, _ in enumerate(probes) - for gid in range(recipe.num_cells()) - ] - - # run the simulation for 5 ms - sim.run(tfinal=T, dt=dt) - - # retrieve the sampled membrane voltages and convert to a pandas DataFrame - print("Plotting results ...") - df_list = [] - for probe, handle in enumerate(handles): - samples, meta = sim.samples(handle)[0] - df_list.append( - pandas.DataFrame( - {"t/ms": samples[:, 0], "U/mV": samples[:, 1], "Cell": f"{probe}"} - ) - ) - df = pandas.concat(df_list, ignore_index=True) - - fig, ax = plt.subplots() - - # plot the membrane potentials of the two cells as function of time - seaborn.lineplot(ax=ax, data=df, x="t/ms", y="U/mV", hue="Cell", errorbar=None) - - # area of cells - area = args.length * 1e-6 * 2 * np.pi * args.radius * 1e-6 - - # total and gap junction conductance in base units - cell_g = area * args.g / 1e-4 - si_gj_g = args.gj_g * 1e-6 - - # weight - w = (si_gj_g + cell_g) / (2 * si_gj_g + cell_g) - - # indicate the expected equilibrium potentials - for i, j in [[0, 1], [1, 0]]: - weighted_potential = args.Vms[i] + w * (args.Vms[j] - args.Vms[i]) - ax.axhline(weighted_potential, linestyle="dashed", color="black", alpha=0.5) - ax.text( - 2, - weighted_potential, - f"$\\tilde U_{j} = U_{j} + w\\cdot(U_{j} - U_{i})$", - va="center", - ha="center", - backgroundcolor="w", - ) - ax.text( - 2, args.Vms[j], f"$U_{j}$", va="center", ha="center", backgroundcolor="w" - ) +# indicate the expected equilibrium potentials +def note(ax, x, y, txt): + ax.text(x, y, txt, va="center", ha="center", backgroundcolor="w") + - ax.set_xlim(0, T) +for i, j in [[0, 1], [1, 0]]: + Vj, Vi = args.Vms[j], args.Vms[i] + Vw = Vi + w.value * (Vj - Vi) + ax.axhline(Vi, linestyle="dashed", color="black", alpha=0.5) + ax.axhline(Vw, linestyle="dashed", color="black", alpha=0.5) + note(ax, 2, Vw, rf"$\tilde U_{j} = U_{j} + w\cdot(U_{j} - U_{i})$") + note(ax, 2, Vj, rf"$U_{j}$") - # plot the initial/nominal resting potentials - for gid, Vm in enumerate(args.Vms): - ax.axhline(Vm, linestyle="dashed", color="black", alpha=0.5) +ax.set_xlim(0, T.value) - fig.savefig("two_cell_gap_junctions_result.svg") +fg.savefig("two_cell_gap_junctions_result.svg") diff --git a/python/example/ou_lif/ou_lif.py b/python/example/ou_lif/ou_lif.py index dde3240480..fe992a14c7 100644 --- a/python/example/ou_lif/ou_lif.py +++ b/python/example/ou_lif/ou_lif.py @@ -4,7 +4,8 @@ import random import subprocess -import arbor as arb +import arbor as A +from arbor import units as U import numpy as np import matplotlib.pyplot as plt @@ -14,8 +15,8 @@ def make_catalogue(): out = subprocess.getoutput("arbor-build-catalogue ou_lif . --cpu True") print(out) # load the new catalogue and extend it with builtin stochastic catalogue - cat = arb.load_catalogue("./ou_lif-catalogue.so") - cat.extend(arb.stochastic_catalogue(), "") + cat = A.load_catalogue("./ou_lif-catalogue.so") + cat.extend(A.stochastic_catalogue(), "") return cat @@ -23,16 +24,16 @@ def make_cell(): # cell morphology # =============== - tree = arb.segment_tree() + tree = A.segment_tree() radius = 1e-10 # radius of cylinder (in µm) height = 2 * radius # height of cylinder (in µm) tree.append( - arb.mnpos, - arb.mpoint(-height / 2, 0, 0, radius), - arb.mpoint(height / 2, 0, 0, radius), + A.mnpos, + A.mpoint(-height / 2, 0, 0, radius), + A.mpoint(height / 2, 0, 0, radius), tag=1, ) - labels = arb.label_dict({"center": "(location 0 0.5)"}) + labels = A.label_dict({"center": "(location 0 0.5)"}) # LIF density mechanism # ===================== @@ -50,7 +51,7 @@ def make_cell(): # reversal potential in mV V_rev = -65.0 # spiking threshold in mV - V_th = -55.0 + V_th = -55.0 * U.mV # initial synaptic weight in nC h_0 = 4.20075 # leak resistance in MOhm @@ -58,7 +59,7 @@ def make_cell(): # membrane time constant in ms tau_mem = 2.0 - lif = arb.mechanism("lif") + lif = A.mechanism("lif") lif.set("R_leak", R_leak) lif.set("R_reset", 1e-10) # set to initial value to zero (background input is applied via stochastic ou_bg_mech) @@ -66,7 +67,7 @@ def make_cell(): lif.set("i_factor", i_factor) lif.set("V_rev", V_rev) lif.set("V_reset", -70.0) - lif.set("V_th", V_th) + lif.set("V_th", V_th.value_as(U.mV)) # refractory time in ms lif.set("t_ref", tau_mem) @@ -80,7 +81,7 @@ def make_cell(): # volatility in nA sigma_bg = 0.5 # instantiate mechanism - ou_bg = arb.mechanism("ou_input") + ou_bg = A.mechanism("ou_input") ou_bg.set("mu", mu_bg) ou_bg.set("sigma", sigma_bg) ou_bg.set("tau", tau_syn) @@ -99,7 +100,7 @@ def make_cell(): # volatility in nA sigma_stim = np.sqrt((1000.0 * N * f) / (2 * tau_syn)) * w_out # instantiate mechanism - ou_stim = arb.mechanism("ou_input") + ou_stim = A.mechanism("ou_input") ou_stim.set("mu", mu_stim) ou_stim.set("sigma", sigma_stim) ou_stim.set("tau", tau_syn) @@ -107,26 +108,26 @@ def make_cell(): # paint and place mechanisms # ========================== - decor = arb.decor() - decor.set_property(Vm=V_rev, cm=c_mem) - decor.paint("(all)", arb.density(lif)) - decor.place('"center"', arb.synapse(ou_stim), "ou_stim") - decor.place('"center"', arb.synapse(ou_bg), "ou_bg") - decor.place('"center"', arb.threshold_detector(V_th), "spike_detector") + decor = A.decor() + decor.set_property(Vm=V_rev * U.mV, cm=c_mem * U.Ohm * U.cm) + decor.paint("(all)", A.density(lif)) + decor.place('"center"', A.synapse(ou_stim), "ou_stim") + decor.place('"center"', A.synapse(ou_bg), "ou_bg") + decor.place('"center"', A.threshold_detector(V_th), "spike_detector") - return arb.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) -class ou_recipe(arb.recipe): +class ou_recipe(A.recipe): def __init__(self, cell, cat): - arb.recipe.__init__(self) + A.recipe.__init__(self) # simulation runtime parameters in ms - self.runtime = 20000 - self.dt = 0.2 + self.runtime = 20 * U.s + self.dt = 0.2 * U.ms # initialize catalogue and cell properties - self.the_props = arb.neuron_cable_properties() + self.the_props = A.neuron_cable_properties() self.the_props.catalogue = cat self.the_cell = cell @@ -141,7 +142,7 @@ def __init__(self, cell, cat): } def cell_kind(self, gid): - return arb.cell_kind.cable + return A.cell_kind.cable def cell_description(self, gid): return self.the_cell @@ -155,9 +156,9 @@ def num_cells(self): def probes(self, gid): # probe membrane potential, total current, and external input currents return [ - arb.cable_probe_membrane_voltage('"center"'), - arb.cable_probe_total_ion_current_cell(), - arb.cable_probe_point_state_cell("ou_input", "I_ou"), + A.cable_probe_membrane_voltage('"center"', "Um"), + A.cable_probe_total_ion_current_cell("Itot"), + A.cable_probe_point_state_cell("ou_input", "I_ou", "Iou"), ] def event_generators(self, gid): @@ -170,81 +171,83 @@ def event_generators(self, gid): gens.extend(self.get_generators(self.bg_prot)) return gens - # Returns arb.event_generator instances that describe the specifics of a given + # Returns A.event_generator instances that describe the specifics of a given # input/stimulation protocol for a mechanism implementing an Ornstein-Uhlenbeck process. # Here, if the value of the 'weight' parameter is 1, stimulation is switched on, # whereas if it is -1, stimulation is switched off. def get_generators(self, protocol): prot_name = protocol["scheme"] # name of the protocol (defining its structure) - start_time = protocol["time_start"] # time at which the stimulus starts in s + start_time = ( + protocol["time_start"] * U.ms + ) # time at which the stimulus starts in ms label = protocol["label"] # target synapse (mechanism label) if prot_name == "ONEPULSE": # create regular schedules to implement a stimulation pulse that lasts for 0.1 s - stim_on = arb.event_generator( + stim_on = A.event_generator( label, 1, - arb.regular_schedule(start_time, self.dt, start_time + self.dt), + A.regular_schedule(start_time, self.dt, start_time + self.dt), ) - stim_off = arb.event_generator( + stim_off = A.event_generator( label, -1, - arb.regular_schedule( - start_time + 100, self.dt, start_time + 100 + self.dt + A.regular_schedule( + start_time + 0.1 * U.s, self.dt, start_time + 0.1 * U.s + self.dt ), ) return [stim_on, stim_off] elif prot_name == "TRIPLET": # create regular schedules to implement pulses that last for 0.1 s each - stim1_on = arb.event_generator( + stim1_on = A.event_generator( label, 1, - arb.regular_schedule(start_time, self.dt, start_time + self.dt), + A.regular_schedule(start_time, self.dt, start_time + self.dt), ) - stim1_off = arb.event_generator( + stim1_off = A.event_generator( label, -1, - arb.regular_schedule( - start_time + 100, self.dt, start_time + 100 + self.dt + A.regular_schedule( + start_time + 0.1 * U.s, self.dt, start_time + 0.1 * U.s + self.dt ), ) - stim2_on = arb.event_generator( + stim2_on = A.event_generator( label, 1, - arb.regular_schedule( - start_time + 500, self.dt, start_time + 500 + self.dt + A.regular_schedule( + start_time + 0.5 * U.s, self.dt, start_time + 0.5 * U.s + self.dt ), ) - stim2_off = arb.event_generator( + stim2_off = A.event_generator( label, -1, - arb.regular_schedule( - start_time + 600, self.dt, start_time + 600 + self.dt + A.regular_schedule( + start_time + 0.6 * U.s, self.dt, start_time + 0.6 * U.s + self.dt ), ) - stim3_on = arb.event_generator( + stim3_on = A.event_generator( label, 1, - arb.regular_schedule( - start_time + 1000, self.dt, start_time + 1000 + self.dt + A.regular_schedule( + start_time + 1 * U.s, self.dt, start_time + 1 * U.s + self.dt ), ) - stim3_off = arb.event_generator( + stim3_off = A.event_generator( label, -1, - arb.regular_schedule( - start_time + 1100, self.dt, start_time + 1100 + self.dt + A.regular_schedule( + start_time + 1.1 * U.s, self.dt, start_time + 1.1 * U.s + self.dt ), ) return [stim1_on, stim1_off, stim2_on, stim2_off, stim3_on, stim3_off] elif prot_name == "FULL": # create a regular schedule that lasts for the full runtime - stim_on = arb.event_generator( + stim_on = A.event_generator( label, 1, - arb.regular_schedule(start_time, self.dt, start_time + self.dt), + A.regular_schedule(start_time, self.dt, start_time + self.dt), ) return [stim_on] @@ -266,23 +269,23 @@ def get_generators(self, protocol): print("random_seed = " + str(random_seed)) # select one thread and no GPU - alloc = arb.proc_allocation(threads=1, gpu_id=None) - context = arb.context(alloc, mpi=None) - domains = arb.partition_load_balance(recipe, context) + alloc = A.proc_allocation(threads=1, gpu_id=None) + context = A.context(alloc, mpi=None) + domains = A.partition_load_balance(recipe, context) # create simulation - sim = arb.simulation(recipe, context, domains, seed=random_seed) + sim = A.simulation(recipe, context, domains, seed=random_seed) # create schedule for recording - reg_sched = arb.regular_schedule(0, recipe.dt, recipe.runtime) + reg_sched = A.regular_schedule(0 * U.ms, recipe.dt, recipe.runtime) # set handles to probe membrane potential and currents gid = 0 - handle_mem = sim.sample((gid, 0), reg_sched) # membrane potential - handle_tot_curr = sim.sample((gid, 1), reg_sched) # total current - handle_curr = sim.sample((gid, 2), reg_sched) # input current + handle_mem = sim.sample((gid, "Um"), reg_sched) # membrane potential + handle_tot_curr = sim.sample((gid, "Itot"), reg_sched) # total current + handle_curr = sim.sample((gid, "Iou"), reg_sched) # input current - sim.record(arb.spike_recording.all) + sim.record(A.spike_recording.all) sim.run(tfinal=recipe.runtime, dt=recipe.dt) # get traces and spikes from simulator diff --git a/python/example/ou_lif/traces.svg b/python/example/ou_lif/traces.svg index 13a3c49c53..b9a3a5c876 100644 --- a/python/example/ou_lif/traces.svg +++ b/python/example/ou_lif/traces.svg @@ -1,16 +1,16 @@ - + - 2022-11-09T15:50:52.860599 + 2023-11-30T19:09:25.458406 image/svg+xml - Matplotlib v3.6.1, https://matplotlib.org/ + Matplotlib v3.7.1, https://matplotlib.org/ @@ -21,18 +21,18 @@ - - @@ -41,17 +41,17 @@ z - - + - + - + - + - + - + @@ -165,12 +165,12 @@ z - + - + - + - + - + - + @@ -243,12 +243,12 @@ z - + - + @@ -260,12 +260,12 @@ z - + - + @@ -277,12 +277,12 @@ z - + - + @@ -296,17 +296,17 @@ z - - + - + @@ -314,12 +314,12 @@ L -3.5 0 - + - + @@ -329,12 +329,12 @@ L -3.5 0 - + - + @@ -343,7 +343,7 @@ L -3.5 0 - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + - - - - - - + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + - @@ -7013,12 +6784,12 @@ z - + - + @@ -7026,12 +6797,12 @@ z - + - + @@ -7042,12 +6813,12 @@ z - + - + @@ -7058,12 +6829,12 @@ z - + - + @@ -7074,12 +6845,12 @@ z - + - + @@ -7091,12 +6862,12 @@ z - + - + @@ -7108,12 +6879,12 @@ z - + - + @@ -7125,12 +6896,12 @@ z - + - + @@ -7142,12 +6913,12 @@ z - + - + @@ -7161,12 +6932,12 @@ z - + - + - @@ -7216,12 +6955,12 @@ z - + - + @@ -7230,12 +6969,12 @@ z - + - + @@ -7244,19 +6983,19 @@ z - + - + - + - + @@ -7326,335 +7065,308 @@ z - + - - - - - @@ -7662,12 +7374,12 @@ z - + - + @@ -7675,12 +7387,12 @@ z - + - + @@ -7691,12 +7403,12 @@ z - + - + @@ -7707,12 +7419,12 @@ z - + - + @@ -7723,12 +7435,12 @@ z - + - + @@ -7740,12 +7452,12 @@ z - + - + @@ -7757,12 +7469,12 @@ z - + - + @@ -7774,12 +7486,12 @@ z - + - + @@ -7791,12 +7503,12 @@ z - + - + @@ -7810,12 +7522,12 @@ z - + - + @@ -7823,12 +7535,12 @@ z - + - + @@ -7838,12 +7550,12 @@ z - + - + @@ -7854,12 +7566,12 @@ z - + - + @@ -7869,7 +7581,7 @@ z - + - + - - - - - @@ -8205,12 +7945,12 @@ z - + - + @@ -8218,12 +7958,12 @@ z - + - + @@ -8234,12 +7974,12 @@ z - + - + @@ -8250,12 +7990,12 @@ z - + - + @@ -8266,12 +8006,12 @@ z - + - + @@ -8283,12 +8023,12 @@ z - + - + @@ -8300,12 +8040,12 @@ z - + - + @@ -8317,12 +8057,12 @@ z - + - + @@ -8334,12 +8074,12 @@ z - + - + @@ -8350,7 +8090,7 @@ z - + - + - + @@ -8394,12 +8134,12 @@ z - + - + @@ -8408,12 +8148,12 @@ z - + - + @@ -8421,12 +8161,12 @@ z - + - + @@ -8434,19 +8174,19 @@ z - + - + - + - + - - - - - - + + - - + + - - + + - - + + diff --git a/python/example/plasticity.py b/python/example/plasticity.py index ab57d099a9..f88ed93128 100644 --- a/python/example/plasticity.py +++ b/python/example/plasticity.py @@ -1,4 +1,5 @@ import arbor as A +from arbor import units as U class recipe(A.recipe): @@ -10,7 +11,7 @@ def __init__(self, n): self.cells = n # Uniform weights and delays for the connectivity. self.weight = 0.75 - self.delay = 0.1 + self.delay = 0.1 * U.ms # Track the connections from the source cell to a given gid. self.connected = set() @@ -34,7 +35,7 @@ def global_properties(self, kind): def cell_description(self, gid): # Cell 0 is reserved for the spike source, spiking every 0.0125ms. if gid == 0: - return A.spike_source_cell("source", A.regular_schedule(0.0125)) + return A.spike_source_cell("source", A.regular_schedule(12.5 * U.us)) # All cells >= 1 are cable cells w/ a simple, soma-only morphology # comprising two segments of radius r=3um and length l=3um *each*. # @@ -54,7 +55,7 @@ def cell_description(self, gid): # - synapse to receive incoming spikes from the source cell. decor.place("(location 0 0.5)", A.synapse("expsyn"), "synapse") # - detector for reporting spikes on the cable cells. - decor.place("(location 0 0.5)", A.threshold_detector(-10.0), "detector") + decor.place("(location 0 0.5)", A.threshold_detector(-10.0 * U.mV), "detector") # return the cable cell description return A.cable_cell(tree, decor) @@ -92,7 +93,7 @@ def add_connection_to_spike_source(self, to): sim = A.simulation(rec, ctx) sim.record(A.spike_recording.all) # then run the simulation for a bit -sim.run(0.25, 0.025) +sim.run(0.25 * U.ms, 0.025 * U.ms) # update the simulation to # # spike_source ----> cable_cell @@ -101,7 +102,7 @@ def add_connection_to_spike_source(self, to): rec.add_connection_to_spike_source(2) sim.update(rec) # and run the simulation for another bit. -sim.run(0.5, 0.025) +sim.run(0.5 * U.ms, 0.025 * U.ms) # when finished, print spike times and locations. source_spikes = 0 print("Spikes:") diff --git a/python/example/probe_lfpykit.py b/python/example/probe_lfpykit.py index a382b9ef74..d83b2732d2 100644 --- a/python/example/probe_lfpykit.py +++ b/python/example/probe_lfpykit.py @@ -13,13 +13,15 @@ # import modules import sys import numpy as np -import arbor +import arbor as A +from arbor import units as U import lfpykit import matplotlib.pyplot as plt from matplotlib.collections import PolyCollection +from pathlib import Path -class Recipe(arbor.recipe): +class Recipe(A.recipe): def __init__(self, cell): super().__init__() @@ -29,108 +31,88 @@ def __init__(self, cell): self.iprobeset_id = (0, 1) self.cprobeset_id = (0, 2) - self.the_props = arbor.neuron_cable_properties() - def num_cells(self): return 1 - def num_sources(self, gid): - return 0 - - def cell_kind(self, gid): - return arbor.cell_kind.cable + def cell_kind(self, _): + return A.cell_kind.cable - def cell_description(self, gid): + def cell_description(self, _): return self.the_cell - def global_properties(self, kind): - return self.the_props + def global_properties(self, _): + return A.neuron_cable_properties() - def probes(self, gid): + def probes(self, _): return [ - arbor.cable_probe_membrane_voltage_cell("Um-all"), - arbor.cable_probe_total_current_cell("Itotal-all"), - arbor.cable_probe_stimulus_current_cell("Istim-all"), + A.cable_probe_membrane_voltage_cell("Um-all"), + A.cable_probe_total_current_cell("Itotal-all"), + A.cable_probe_stimulus_current_cell("Istim-all"), ] # Read the SWC filename from input -# Example from docs: single_cell_detailed.swc -if len(sys.argv) < 2: - print("No SWC file passed to the program") - sys.exit(0) - -filename = sys.argv[1] - -# define morphology (needed for ``arbor.place_pwlin`` and ``arbor.cable_cell`` below) -morphology = arbor.load_swc_arbor(filename) +if len(sys.argv) == 1: + print("No SWC file passed to the program, using default.") + filename = Path(__file__).parent / "single_cell_detailed.swc" +elif len(sys.argv) == 2: + filename = Path(sys.argv[1]) +else: + print("Usage: single_cell_detailed.py [SWC file name]") + sys.exit(1) + +# define morphology (needed for ``A.place_pwlin`` and ``A.cable_cell`` below) +morphology = A.load_swc_arbor(filename) # define a location on morphology for current clamp -clamp_location = arbor.location(4, 1 / 6) - - -def make_cable_cell(morphology, clamp_location): - # number of CVs per branch - cvs_per_branch = 3 - - # Label dictionary - labels = arbor.label_dict() +clamp_location = A.location(4, 1 / 6) + +# define a sinusoid input current +iclamp = A.iclamp( + 5 * U.ms, # stimulation onset + 1e8 * U.ms, # stimulation duration + -0.001 * U.nA, # stimulation amplitude + frequency=100 * U.Hz, # stimulation frequency + phase=0 * U.rad, # stimulation phase +) - # decor - decor = ( - arbor.decor() - # set initial voltage, temperature, axial resistivity, membrane capacitance - .set_property( - Vm=-65, # Initial membrane voltage (mV) - tempK=300, # Temperature (Kelvin) - rL=10000, # Axial resistivity (Ω cm) - cm=0.01, # Membrane capacitance (F/m**2) - ) - # set passive mechanism all over - # passive mech w. leak reversal potential (mV) - .paint("(all)", arbor.density("pas/e=-65", g=0.0001)) +decor = ( + A.decor() + # set initial voltage, temperature, axial resistivity, membrane capacitance + .set_property( + Vm=-65 * U.mV, # Initial membrane voltage (mV) + tempK=300 * U.Kelvin, # Temperature (Kelvin) + rL=10 * U.kOhm * U.cm, # Axial resistivity (Ω cm) + cm=0.01 * U.F / U.m2, # Membrane capacitance (F/m**2) ) + # set passive mech w. leak reversal potential (mV) + .paint("(all)", A.density("pas/e=-65", g=0.0001)) + # attach the stimulus + .place(str(clamp_location), iclamp, "iclamp") + # use a fixed 3 CVs per branch + .discretization(A.cv_policy_fixed_per_branch(3)) +) - # set number of CVs per branch - policy = arbor.cv_policy_fixed_per_branch(cvs_per_branch) - decor.discretization(policy) - - # place sinusoid input current - iclamp = arbor.iclamp( - 5, # stimulation onset (ms) - 1e8, # stimulation duration (ms) - -0.001, # stimulation amplitude (nA) - frequency=0.1, # stimulation frequency (kHz) - phase=0, - ) # stimulation phase) - decor.place(str(clamp_location), iclamp, '"iclamp"') - - # create ``arbor.place_pwlin`` object - p = arbor.place_pwlin(morphology) - - # create cell and set properties - cell = arbor.cable_cell(morphology, decor, labels) - - return p, cell - - -# get place_pwlin and cable_cell objects -p, cell = make_cable_cell(morphology, clamp_location) +# place_pwlin can be queried with region/locset expressions to obtain +# geometrical objects, like points and segments, essentially recovering +# geometry from morphology. +ppwl = A.place_pwlin(morphology) +cell = A.cable_cell(morphology, decor) # instantiate recipe with cell -recipe = Recipe(cell) +rec = Recipe(cell) # instantiate simulation -sim = arbor.simulation(recipe) +sim = A.simulation(rec) # set up sampling on probes with sampling every 1 ms -schedule = arbor.regular_schedule(1.0) +schedule = A.regular_schedule(1.0 * U.ms) v_handle = sim.sample(0, "Um-all", schedule) i_handle = sim.sample(0, "Itotal-all", schedule) c_handle = sim.sample(0, "Istim-all", schedule) # run simulation for 500 ms of simulated activity and collect results. -sim.run(tfinal=500) +sim.run(tfinal=500 * U.ms) # extract time, V_m, I_m and I_c for each CV V_m_samples, V_m_meta = sim.samples(v_handle)[0] @@ -144,8 +126,8 @@ def make_cable_cell(morphology, clamp_location): V_m_samples = V_m_samples[:, np.r_[True, inds]] V_m_meta = np.array(V_m_meta)[inds].tolist() -# assert that the remaining cables comprising the metadata for each probe -# are identical, as well as the reported sample times. +# assert that the remaining cables comprising the metadata for each probe are +# identical, as well as the reported sample times. assert V_m_meta == I_m_meta assert (V_m_samples[:, 0] == I_m_samples[:, 0]).all() @@ -169,16 +151,16 @@ def make_cable_cell(morphology, clamp_location): class ArborCellGeometry(lfpykit.CellGeometry): """ Class inherited from ``lfpykit.CellGeometry`` for easier forward-model - predictions in Arbor that keeps track of arbor.segment information + predictions in Arbor that keeps track of A.segment information for each CV. Parameters ---------- - p: ``arbor.place_pwlin`` object - 3-d locations and cables in a morphology (cf. ``arbor.place_pwlin``) + p: ``A.place_pwlin`` object + 3-d locations and cables in a morphology (cf. ``A.place_pwlin``) cables: ``list`` - ``list`` of corresponding ``arbor.cable`` objects where transmembrane - currents are recorded (cf. ``arbor.cable_probe_total_current_cell``) + ``list`` of corresponding ``A.cable`` objects where transmembrane + currents are recorded (cf. ``A.cable_probe_total_current_cell``) See also -------- @@ -190,7 +172,7 @@ def __init__(self, p, cables): CV_ind = np.array([], dtype=int) # tracks which CV owns segment for i, m in enumerate(cables): segs = p.segments([m]) - for j, seg in enumerate(segs): + for seg in segs: x = np.row_stack([x, [seg.prox.x, seg.dist.x]]) y = np.row_stack([y, [seg.prox.y, seg.dist.y]]) z = np.row_stack([z, [seg.prox.z, seg.dist.z]]) @@ -253,7 +235,7 @@ def get_transformation_matrix(self): # create ``ArborCellGeometry`` instance -cell_geometry = ArborCellGeometry(p, I_m_meta) +cell_geometry = ArborCellGeometry(ppwl, I_m_meta) # define locations where extracellular potential is predicted in vicinity # of cell. @@ -421,7 +403,7 @@ def colorbar( ax.add_collection(get_segment_outlines(cell_geometry)) # add marker denoting clamp location -point = p.at(clamp_location) +point = ppwl.at(clamp_location) ax.plot(point.x, point.y, "ko", ms=10, label="stimulus") ax.legend() diff --git a/python/example/single_cell_allen.py b/python/example/single_cell_allen.py index df96149279..6daaf09f91 100644 --- a/python/example/single_cell_allen.py +++ b/python/example/single_cell_allen.py @@ -2,14 +2,19 @@ from collections import defaultdict from dataclasses import dataclass +from typing import Optional import json -import arbor -import seaborn -import pandas +import arbor as A +from arbor import units as U +import seaborn as sns +import pandas as pd import matplotlib.pyplot as plt +from pathlib import Path +here = Path(__file__).parent -# (3) A function that parses the Allen parameter fit file into components for an arbor.decor + +# (3) A function that parses the Allen parameter fit file into components for an A.decor # NB. Needs to be adjusted when using a different model def load_allen_fit(fit): with open(fit) as fd: @@ -18,10 +23,10 @@ def load_allen_fit(fit): # cable parameters convenience class @dataclass class parameters: - cm: float = None - tempK: float = None - Vm: float = None - rL: float = None + cm: Optional[float] = None + tempK: Optional[float] = None + Vm: Optional[float] = None + rL: Optional[float] = None param = defaultdict(parameters) mechs = defaultdict(dict) @@ -71,30 +76,44 @@ class parameters: return default, regs, ions, mechs, fit["fitting"][0]["junction_potential"] -def make_cell(swc, fit): +def make_cell(base, swc, fit): # (1) Load the swc file passed into this function - morphology = arbor.load_swc_neuron(swc) + morphology = A.load_swc_neuron(base / swc) + # (2) Label the region tags found in the swc with the names used in the parameter fit file. - # In addition, label the midpoint of the somarbor. - labels = arbor.label_dict().add_swc_tags() + # In addition, label the midpoint of the soma. + labels = A.label_dict().add_swc_tags() labels["midpoint"] = "(location 0 0.5)" - # (3) A function that parses the Allen parameter fit file into components for an arbor.decor - dflt, regions, ions, mechanisms, offset = load_allen_fit(fit) + # (3) A function that parses the Allen parameter fit file into components + dflt, regions, ions, mechanisms, offset = load_allen_fit(base / fit) # (4) Instantiate an empty decor. - decor = arbor.decor() + decor = A.decor() + # (5) assign global electro-physiology parameters - decor.set_property(tempK=dflt.tempK, Vm=dflt.Vm, cm=dflt.cm, rL=dflt.rL) + decor.set_property( + tempK=dflt.tempK * U.Kelvin, + Vm=dflt.Vm * U.mV, + cm=dflt.cm * U.F / U.m2, + rL=dflt.rL * U.Ohm * U.cm, + ) + # (6) override regional electro-physiology parameters for region, vs in regions: - decor.paint(f'"{region}"', tempK=vs.tempK, Vm=vs.Vm, cm=vs.cm, rL=vs.rL) + decor.paint( + f'"{region}"', + tempK=vs.tempK * U.Kelvin, + Vm=vs.Vm * U.Vm, + cm=vs.cm * U.F / U.m2, + rL=vs.rL * U.Ohm * U.cm, + ) + # (7) set reversal potentials for region, ion, e in ions: - decor.paint(f'"{region}"', ion_name=ion, rev_pot=e) - decor.set_ion( - "ca", int_con=5e-5, ext_con=2.0, method=arbor.mechanism("nernst/x=ca") - ) + decor.paint(f'"{region}"', ion=ion, rev_pot=e) + decor.set_ion("ca", int_con=5e-5 * U.mM, ext_con=2.0 * U.mM, method="nernst/x=ca") + # (8) assign ion dynamics for region, mech, values in mechanisms: nm = mech @@ -106,58 +125,61 @@ def make_cell(swc, fit): sp = "," else: vs[k] = v - decor.paint(f'"{region}"', arbor.density(arbor.mechanism(nm, vs))) + decor.paint(f'"{region}"', A.density(A.mechanism(nm, vs))) + # (9) attach stimulus and detector - decor.place('"midpoint"', arbor.iclamp(200, 1000, 0.15), "ic") - decor.place('"midpoint"', arbor.threshold_detector(-40), "sd") + decor.place('"midpoint"', A.iclamp(0.2 * U.s, 1 * U.s, 150 * U.pA), "ic") + decor.place('"midpoint"', A.threshold_detector(-40 * U.mV), "sd") + # (10) discretisation strategy: max compartment length - decor.discretization(arbor.cv_policy_max_extent(20)) + decor.discretization(A.cv_policy_max_extent(20)) # (11) Create cell - return arbor.cable_cell(morphology, decor, labels), offset + return A.cable_cell(morphology, decor, labels), offset # (12) Create cell, model -cell, offset = make_cell("single_cell_allen.swc", "single_cell_allen_fit.json") -model = arbor.single_cell_model(cell) +cell, offset = make_cell(here, "single_cell_allen.swc", "single_cell_allen_fit.json") +model = A.single_cell_model(cell) # (13) Set the probe -model.probe("voltage", '"midpoint"', frequency=200) +model.probe("voltage", '"midpoint"', "Um", frequency=1 / (5 * U.us)) # (14) Install the Allen mechanism catalogue. -model.properties.catalogue.extend(arbor.allen_catalogue(), "") +model.properties.catalogue.extend(A.allen_catalogue(), "") # (15) Run simulation -model.run(tfinal=1400, dt=0.005) +model.run(tfinal=1.4 * U.s, dt=5 * U.us) # (16) Load and scale reference reference = ( - 1000.0 * pandas.read_csv("single_cell_allen_neuron_ref.csv")["U/mV"].values[:-1] - + offset + 1e3 * pd.read_csv(here / "single_cell_allen_neuron_ref.csv")["U/mV"] + offset ) # (17) Plot -df_list = [] -df_list.append( - pandas.DataFrame( - { - "t/ms": model.traces[0].time, - "U/mV": model.traces[0].value, - "Simulator": "Arbor", - } - ) -) -df_list.append( - pandas.DataFrame( - {"t/ms": model.traces[0].time, "U/mV": reference, "Simulator": "Neuron"} - ) -) -df = pandas.concat(df_list, ignore_index=True) -seaborn.relplot( - data=df, kind="line", x="t/ms", y="U/mV", hue="Simulator", errorbar=None +df = pd.concat( + [ + pd.DataFrame( + { + "Simulator": "Arbor", + "t/ms": model.traces[0].time, + "U/mV": model.traces[0].value, + } + ), + pd.DataFrame( + { + "Simulator": "Neuron", + "t/ms": model.traces[0].time, + "U/mV": reference.values, + } + ), + ], + ignore_index=True, ) + +sns.relplot(data=df, kind="line", x="t/ms", y="U/mV", hue="Simulator", errorbar=None) plt.scatter( - model.spikes, [-40] * len(model.spikes), color=seaborn.color_palette()[2], zorder=20 + model.spikes, [-40] * len(model.spikes), color=sns.color_palette()[2], zorder=20 ) plt.bar( 200, @@ -168,4 +190,4 @@ def make_cell(swc, fit): label="Stimulus", color="0.9", ) -plt.savefig("single_cell_allen_result.svg") +plt.savefig("single_cell_allen_result.pdf") diff --git a/python/example/single_cell_bluepyopt/l5pc/C060114A7_axon_replacement.acc b/python/example/single_cell_bluepyopt/l5pc/C060114A7_axon_replacement.acc index 9318d73175..ad5cfa7de8 100644 --- a/python/example/single_cell_bluepyopt/l5pc/C060114A7_axon_replacement.acc +++ b/python/example/single_cell_bluepyopt/l5pc/C060114A7_axon_replacement.acc @@ -1,6 +1,6 @@ (arbor-component (meta-data - (version "0.1-dev")) + (version "0.9-dev")) (morphology (branch 0 -1 (segment 0 @@ -18,4 +18,4 @@ (segment 3 (point 262.494171 -39.637466 -3.380000 0.460000) (point 262.242889 -54.635361 -3.380000 0.460000) - 2)))) \ No newline at end of file + 2)))) diff --git a/python/example/single_cell_bluepyopt/l5pc/C060114A7_modified.acc b/python/example/single_cell_bluepyopt/l5pc/C060114A7_modified.acc index 0f0910d58e..317282f7d5 100644 --- a/python/example/single_cell_bluepyopt/l5pc/C060114A7_modified.acc +++ b/python/example/single_cell_bluepyopt/l5pc/C060114A7_modified.acc @@ -1,6 +1,6 @@ (arbor-component (meta-data - (version "0.1-dev")) + (version "0.9-dev")) (morphology (branch 0 -1 (segment 0 @@ -21699,4 +21699,4 @@ (segment 5374 (point 262.494171 -39.637466 -3.380000 0.460000) (point 262.242889 -54.635361 -3.380000 0.460000) - 2)))) \ No newline at end of file + 2)))) diff --git a/python/example/single_cell_bluepyopt/l5pc/l5pc_decor.acc b/python/example/single_cell_bluepyopt/l5pc/l5pc_decor.acc index 89c7735b8f..29831b6715 100644 --- a/python/example/single_cell_bluepyopt/l5pc/l5pc_decor.acc +++ b/python/example/single_cell_bluepyopt/l5pc/l5pc_decor.acc @@ -1,20 +1,20 @@ (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (decor - (default (membrane-potential -65)) - (default (temperature-kelvin 307.14999999999998)) - (default (membrane-capacitance 0.01)) - (default (axial-resistivity 100)) + (default (membrane-potential -65 (scalar 1.0))) + (default (temperature-kelvin 307.14999999999998 (scalar 1.0))) + (default (membrane-capacitance 0.01 (scalar 1.0))) + (default (axial-resistivity 100 (scalar 1.0))) (paint (region "all") (density (mechanism "default::pas/e=-75" ("g" 3.0000000000000001e-05)))) - (paint (region "apic") (ion-reversal-potential "na" 50)) - (paint (region "apic") (ion-reversal-potential "k" -85)) - (paint (region "apic") (membrane-capacitance 0.02)) + (paint (region "apic") (ion-reversal-potential "na" 50 (scalar 1.0))) + (paint (region "apic") (ion-reversal-potential "k" -85 (scalar 1.0))) + (paint (region "apic") (membrane-capacitance 0.02 (scalar 1.0))) (paint (region "apic") (density (mechanism "BBP::NaTs2_t" ("gNaTs2_tbar" 0.026145000000000002)))) (paint (region "apic") (density (mechanism "BBP::SKv3_1" ("gSKv3_1bar" 0.0042259999999999997)))) (paint (region "apic") (density (mechanism "BBP::Im" ("gImbar" 0.00014300000000000001)))) (paint (region "apic") (scaled-mechanism (density (mechanism "BBP::Ih" ("gIhbar" 8.0000000000000007e-05))) ("gIhbar" (add (scalar -0.86960000000000004) (mul (scalar 2.0870000000000002) (exp (mul (distance (region "soma")) (scalar 0.0030999999999999999) ) ) ) )))) - (paint (region "axon") (ion-reversal-potential "na" 50)) - (paint (region "axon") (ion-reversal-potential "k" -85)) + (paint (region "axon") (ion-reversal-potential "na" 50 (scalar 1.0))) + (paint (region "axon") (ion-reversal-potential "k" -85 (scalar 1.0))) (paint (region "axon") (density (mechanism "BBP::NaTa_t" ("gNaTa_tbar" 3.1379679999999999)))) (paint (region "axon") (density (mechanism "BBP::Nap_Et2" ("gNap_Et2bar" 0.0068269999999999997)))) (paint (region "axon") (density (mechanism "BBP::K_Pst" ("gK_Pstbar" 0.97353800000000001)))) @@ -24,14 +24,14 @@ (paint (region "axon") (density (mechanism "BBP::Ca_HVA" ("gCa_HVAbar" 0.00098999999999999999)))) (paint (region "axon") (density (mechanism "BBP::Ca_LVAst" ("gCa_LVAstbar" 0.0087519999999999994)))) (paint (region "axon") (density (mechanism "BBP::CaDynamics_E2" ("gamma" 0.0029099999999999998) ("decay" 287.19873100000001)))) - (paint (region "dend") (membrane-capacitance 0.02)) + (paint (region "dend") (membrane-capacitance 0.02 (scalar 1.0))) (paint (region "dend") (density (mechanism "BBP::Ih" ("gIhbar" 8.0000000000000007e-05)))) - (paint (region "soma") (ion-reversal-potential "na" 50)) - (paint (region "soma") (ion-reversal-potential "k" -85)) + (paint (region "soma") (ion-reversal-potential "na" 50 (scalar 1.0))) + (paint (region "soma") (ion-reversal-potential "k" -85 (scalar 1.0))) (paint (region "soma") (density (mechanism "BBP::NaTs2_t" ("gNaTs2_tbar" 0.98395500000000002)))) (paint (region "soma") (density (mechanism "BBP::SKv3_1" ("gSKv3_1bar" 0.30347200000000002)))) (paint (region "soma") (density (mechanism "BBP::SK_E2" ("gSK_E2bar" 0.0084069999999999995)))) (paint (region "soma") (density (mechanism "BBP::Ca_HVA" ("gCa_HVAbar" 0.00099400000000000009)))) (paint (region "soma") (density (mechanism "BBP::Ca_LVAst" ("gCa_LVAstbar" 0.00033300000000000002)))) (paint (region "soma") (density (mechanism "BBP::CaDynamics_E2" ("gamma" 0.00060899999999999995) ("decay" 210.48528400000001)))) - (paint (region "soma") (density (mechanism "BBP::Ih" ("gIhbar" 8.0000000000000007e-05)))))) \ No newline at end of file + (paint (region "soma") (density (mechanism "BBP::Ih" ("gIhbar" 8.0000000000000007e-05)))))) diff --git a/python/example/single_cell_bluepyopt/l5pc/l5pc_label_dict.acc b/python/example/single_cell_bluepyopt/l5pc/l5pc_label_dict.acc index 08c4efd5da..c15ec60578 100644 --- a/python/example/single_cell_bluepyopt/l5pc/l5pc_label_dict.acc +++ b/python/example/single_cell_bluepyopt/l5pc/l5pc_label_dict.acc @@ -1,9 +1,9 @@ (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (label-dict (region-def "all" (all)) (region-def "apic" (tag 4)) (region-def "axon" (tag 2)) (region-def "dend" (tag 3)) (region-def "soma" (tag 1)) - (region-def "myelin" (tag 5)))) \ No newline at end of file + (region-def "myelin" (tag 5)))) diff --git a/python/example/single_cell_bluepyopt/simplecell/simple_cell_decor.acc b/python/example/single_cell_bluepyopt/simplecell/simple_cell_decor.acc index e5af159ce8..c46b2578ef 100644 --- a/python/example/single_cell_bluepyopt/simplecell/simple_cell_decor.acc +++ b/python/example/single_cell_bluepyopt/simplecell/simple_cell_decor.acc @@ -1,5 +1,5 @@ (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (decor - (paint (region "soma") (membrane-capacitance 0.01)) - (paint (region "soma") (density (mechanism "default::hh" ("gnabar" 0.10299326453483033) ("gkbar" 0.027124836082684685)))))) \ No newline at end of file + (paint (region "soma") (membrane-capacitance 0.01 (scalar 1))) + (paint (region "soma") (density (mechanism "default::hh" ("gnabar" 0.10299326453483033) ("gkbar" 0.027124836082684685)))))) diff --git a/python/example/single_cell_bluepyopt/simplecell/simple_cell_label_dict.acc b/python/example/single_cell_bluepyopt/simplecell/simple_cell_label_dict.acc index 08c4efd5da..c15ec60578 100644 --- a/python/example/single_cell_bluepyopt/simplecell/simple_cell_label_dict.acc +++ b/python/example/single_cell_bluepyopt/simplecell/simple_cell_label_dict.acc @@ -1,9 +1,9 @@ (arbor-component - (meta-data (version "0.1-dev")) + (meta-data (version "0.9-dev")) (label-dict (region-def "all" (all)) (region-def "apic" (tag 4)) (region-def "axon" (tag 2)) (region-def "dend" (tag 3)) (region-def "soma" (tag 1)) - (region-def "myelin" (tag 5)))) \ No newline at end of file + (region-def "myelin" (tag 5)))) diff --git a/python/example/single_cell_cable.py b/python/example/single_cell_cable.py index dc3f525e1d..fc6cceb156 100755 --- a/python/example/single_cell_cable.py +++ b/python/example/single_cell_cable.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 -import arbor +import arbor as A +from arbor import units as U import argparse import numpy as np -import pandas -import seaborn # You may have to pip install these. +import pandas as pd +import seaborn as sns # You may have to pip install these. -class Cable(arbor.recipe): +class Cable(A.recipe): def __init__( self, probes, @@ -40,7 +41,7 @@ def __init__( cv_policy_max_extent -- maximum extent of control volume in μm """ - arbor.recipe.__init__(self) + A.recipe.__init__(self) self.the_probes = probes @@ -51,25 +52,19 @@ def __init__( self.rL = rL self.g = g - self.stimulus_start = stimulus_start - self.stimulus_duration = stimulus_duration - self.stimulus_amplitude = stimulus_amplitude + self.stimulus_start = stimulus_start * U.ms + self.stimulus_duration = stimulus_duration * U.ms + self.stimulus_amplitude = stimulus_amplitude * U.nA self.cv_policy_max_extent = cv_policy_max_extent - self.the_props = arbor.neuron_cable_properties() + self.the_props = A.neuron_cable_properties() def num_cells(self): return 1 - def num_sources(self, _): - return 0 - def cell_kind(self, _): - return arbor.cell_kind.cable - - def probes(self, _): - return self.the_probes + return A.cell_kind.cable def global_properties(self, _): return self.the_props @@ -81,34 +76,39 @@ def cell_description(self, _): to build a multi-compartment neuron. """ - tree = arbor.segment_tree() + tree = A.segment_tree() tree.append( - arbor.mnpos, - arbor.mpoint(0, 0, 0, self.radius), - arbor.mpoint(self.length, 0, 0, self.radius), + A.mnpos, + (0, 0, 0, self.radius), + (self.length, 0, 0, self.radius), tag=1, ) - labels = arbor.label_dict({"cable": "(tag 1)", "start": "(location 0 0)"}) + labels = A.label_dict({"cable": "(tag 1)", "start": "(location 0 0)"}) decor = ( - arbor.decor() - .set_property(Vm=self.Vm, cm=self.cm, rL=self.rL) - .paint('"cable"', arbor.density(f"pas/e={self.Vm}", g=self.g)) + A.decor() + .set_property( + Vm=self.Vm * U.mV, cm=self.cm * U.F / U.m2, rL=self.rL * U.Ohm * U.cm + ) + .paint('"cable"', A.density(f"pas/e={self.Vm}", g=self.g)) .place( '"start"', - arbor.iclamp( + A.iclamp( self.stimulus_start, self.stimulus_duration, self.stimulus_amplitude ), "iclamp", ) ) - policy = arbor.cv_policy_max_extent(self.cv_policy_max_extent) + policy = A.cv_policy_max_extent(self.cv_policy_max_extent) decor.discretization(policy) - return arbor.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) + + def probes(self, _): + return self.the_probes def get_rm(g): @@ -196,34 +196,32 @@ def get_tmax(data): probe_locations = [ (f"(location 0 {r})", f"Um-(0, {r})") for r in np.linspace(0, 1, 11) ] - probes = [ - arbor.cable_probe_membrane_voltage(loc, tag) for loc, tag in probe_locations - ] + probes = [A.cable_probe_membrane_voltage(loc, tag) for loc, tag in probe_locations] recipe = Cable(probes, **vars(args)) # configure the simulation and handles for the probes - sim = arbor.simulation(recipe) - dt = 0.001 + sim = A.simulation(recipe) + dt = 1 * U.us handles = [ - sim.sample((0, tag), arbor.regular_schedule(dt)) for _, tag in probe_locations + sim.sample((0, tag), A.regular_schedule(dt)) for _, tag in probe_locations ] # run the simulation for 30 ms - sim.run(tfinal=30, dt=dt) + sim.run(tfinal=30 * U.ms, dt=dt) - # retrieve the sampled membrane voltages and convert to a pandas DataFrame + # retrieve the sampled membrane voltages and convert to a pd DataFrame print("Plotting results ...") df_list = [] for probe in range(len(handles)): samples, meta = sim.samples(handles[probe])[0] df_list.append( - pandas.DataFrame( + pd.DataFrame( {"t/ms": samples[:, 0], "U/mV": samples[:, 1], "Probe": f"{probe}"} ) ) - df = pandas.concat(df_list, ignore_index=True) - seaborn.relplot( + df = pd.concat(df_list, ignore_index=True) + sns.relplot( data=df, kind="line", x="t/ms", y="U/mV", hue="Probe", errorbar=None ).set(xlim=(9, 14)).savefig("single_cell_cable_result.svg") diff --git a/python/example/single_cell_detailed.py b/python/example/single_cell_detailed.py index 1c3d7312bc..b58e2f924a 100755 --- a/python/example/single_cell_detailed.py +++ b/python/example/single_cell_detailed.py @@ -1,28 +1,28 @@ #!/usr/bin/env python3 # This script is included in documentation. Adapt line numbers if touched. -import arbor -import pandas -import seaborn +import arbor as A +from arbor import units as U +import pandas as pd +import seaborn as sns import sys -from arbor import density +from pathlib import Path # (1) Read the morphology from an SWC file. +if len(sys.argv) == 1: + print("No SWC file passed to the program, using default.") + filename = Path(__file__).parent / "single_cell_detailed.swc" +elif len(sys.argv) == 2: + filename = Path(sys.argv[1]) +else: + print("Usage: single_cell_detailed.py [SWC file name]") + sys.exit(1) -# Read the SWC filename from input -# Example from docs: single_cell_detailed.swc -if len(sys.argv) < 2: - print("No SWC file passed to the program") - sys.exit(0) - -filename = sys.argv[1] -morph = arbor.load_swc_arbor(filename) +morph = A.load_swc_arbor(filename) # (2) Create and populate the label dictionary. - - -labels = arbor.label_dict( +labels = A.label_dict( { # Regions: # Add a label for a region that includes the whole morphology @@ -43,70 +43,78 @@ ).add_swc_tags() # (3) Create and populate the decor. -# NB. This can be written more compactly using method chaining - -decor = arbor.decor() -# Set the default properties of the cell (this overrides the model defaults). -decor.set_property(Vm=-55) -decor.set_ion("na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na") -decor.set_ion("k", int_con=54.4, ext_con=2.5, rev_pot=-77) -# Override the cell defaults. -decor.paint('"custom"', tempK=270) -decor.paint('"soma"', Vm=-50) -# Paint density mechanisms. -decor.paint('"all"', density("pas")) -decor.paint('"custom"', density("hh")) -decor.paint('"dend"', density("Ih", gbar=0.001)) -# Place stimuli and detectors. -decor.place('"root"', arbor.iclamp(10, 1, current=2), "iclamp0") -decor.place('"root"', arbor.iclamp(30, 1, current=2), "iclamp1") -decor.place('"root"', arbor.iclamp(50, 1, current=2), "iclamp2") -decor.place('"axon_terminal"', arbor.threshold_detector(-10), "detector") -# Set discretisation: Soma as one CV, 1um everywhere else -decor.discretization('(replace (single (region "soma")) (max-extent 1.0))') +decor = ( + A.decor() + # Set the default properties of the cell (this overrides the model defaults). + .set_property( + Vm=-55 * U.mV, + tempK=300 * U.Kelvin, + rL=35.4 * U.Ohm * U.cm, + cm=0.01 * U.F / U.m2, + ) + .set_ion( + "na", + int_con=10 * U.mM, + ext_con=140 * U.mM, + rev_pot=50 * U.mV, + method="nernst/na", + ) + .set_ion("k", int_con=54.4 * U.mM, ext_con=2.5 * U.mM, rev_pot=-77 * U.mV) + # Override the cell defaults. + .paint('"custom"', tempK=270 * U.Kelvin) + .paint('"soma"', Vm=-50 * U.mV) + # Paint density mechanisms. + .paint('"all"', A.density("pas")) + .paint('"custom"', A.density("hh")) + .paint('"dend"', A.density("Ih", gbar=0.001)) + # Place stimuli and detectors. + .place('"root"', A.iclamp(10 * U.ms, 1 * U.ms, current=2 * U.nA), "iclamp0") + .place('"root"', A.iclamp(30 * U.ms, 1 * U.ms, current=2 * U.nA), "iclamp1") + .place('"root"', A.iclamp(50 * U.ms, 1 * U.ms, current=2 * U.nA), "iclamp2") + .place('"axon_terminal"', A.threshold_detector(-10 * U.mV), "detector") + # Set discretisation: Soma as one CV, 1um everywhere else + .discretization('(replace (single (region "soma")) (max-extent 1.0))') +) # (4) Create the cell. - -cell = arbor.cable_cell(morph, decor, labels) +cell = A.cable_cell(morph, decor, labels) # (5) Construct the model - -model = arbor.single_cell_model(cell) +model = A.single_cell_model(cell) # (6) Set the model default properties - -model.properties.set_property(Vm=-65, tempK=300, rL=35.4, cm=0.01) -model.properties.set_ion("na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na") -model.properties.set_ion("k", int_con=54.4, ext_con=2.5, rev_pot=-77) - -# Extend the default catalogue with the Allen catalogue. -# The function takes a second string parameter that can prefix -# the name of the mechanisms to avoid collisions between catalogues -# in this case we have no collisions so we use an empty prefix string. -model.properties.catalogue.extend(arbor.allen_catalogue(), "") +model.properties.set_property( + Vm=-65 * U.mV, tempK=300 * U.Kelvin, rL=35.4 * U.Ohm * U.cm, cm=0.01 * U.F / U.m2 +) +model.properties.set_ion( + "na", int_con=10 * U.mM, ext_con=140 * U.mM, rev_pot=50 * U.mV, method="nernst/na" +) +model.properties.set_ion( + "k", int_con=54.4 * U.mM, ext_con=2.5 * U.mM, rev_pot=-77 * U.mV +) + +# Extend the default catalogue with the Allen catalogue. The function takes a +# second string parameter that can prefix the name of the mechanisms to avoid +# collisions between catalogues in this case we have no collisions so we use an +# empty prefix string. +model.properties.catalogue.extend(A.allen_catalogue(), "") # (7) Add probes. - -# Add voltage probes on the "custom_terminal" locset -# which sample the voltage at 50 kHz -model.probe("voltage", where='"custom_terminal"', tag="Um", frequency=50) +# Add a voltage probe on "custom_terminal" +model.probe("voltage", where='"custom_terminal"', tag="Um", frequency=50 * U.kHz) # (8) Run the simulation for 100 ms, with a dt of 0.025 ms - -model.run(tfinal=100, dt=0.025) +model.run(tfinal=100 * U.ms, dt=25 * U.us) # (9) Print the spikes. - print(len(model.spikes), "spikes recorded:") for s in model.spikes: - print(s) + print(f" * t={s:.3f} ms") # (10) Plot the voltages - -df_list = [] -for t in model.traces: - df_list.append( - pandas.DataFrame( +df = pd.concat( + [ + pd.DataFrame( { "t/ms": t.time, "U/mV": t.value, @@ -114,9 +122,12 @@ "Variable": t.variable, } ) - ) -df = pandas.concat(df_list, ignore_index=True) -seaborn.relplot( + for t in model.traces + ], + ignore_index=True, +) + +sns.relplot( data=df, kind="line", x="t/ms", diff --git a/python/example/single_cell_detailed_recipe.py b/python/example/single_cell_detailed_recipe.py index c68d192dbf..5d28457e98 100644 --- a/python/example/single_cell_detailed_recipe.py +++ b/python/example/single_cell_detailed_recipe.py @@ -1,27 +1,27 @@ #!/usr/bin/env python3 # This script is included in documentation. Adapt line numbers if touched. -import arbor -import pandas -import seaborn +import arbor as A +from arbor import units as U +import pandas as pd +import seaborn as sns import sys -from arbor import density +from pathlib import Path # (1) Read the morphology from an SWC file. +if len(sys.argv) == 1: + print("No SWC file passed to the program, using default.") + filename = Path(__file__).parent / "single_cell_detailed.swc" +elif len(sys.argv) == 2: + filename = Path(sys.argv[1]) +else: + print("Usage: single_cell_detailed.py [SWC file name]") + sys.exit(1) -# Read the SWC filename from input -# Example from docs: single_cell_detailed.swc - -if len(sys.argv) < 2: - print("No SWC file passed to the program") - sys.exit(0) - -filename = sys.argv[1] -morph = arbor.load_swc_arbor(filename) +morph = A.load_swc_arbor(filename) # (2) Create and populate the label dictionary. - -labels = arbor.label_dict( +labels = A.label_dict( { # Regions: # Add a label for a region that includes the whole morphology @@ -42,51 +42,68 @@ ).add_swc_tags() # Add SWC pre-defined regions # (3) Create and populate the decor. - decor = ( - arbor.decor() + A.decor() # Set the default properties of the cell (this overrides the model defaults). - .set_property(Vm=-55) - .set_ion("na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na") - .set_ion("k", int_con=54.4, ext_con=2.5, rev_pot=-77) + .set_property(Vm=-55 * U.mV) + .set_ion( + "na", + int_con=10 * U.mM, + ext_con=140 * U.mM, + rev_pot=50 * U.mV, + method="nernst/na", + ) + .set_ion("k", int_con=54.4 * U.mM, ext_con=2.5 * U.mM, rev_pot=-77 * U.mV) # Override the cell defaults. - .paint('"custom"', tempK=270) - .paint('"soma"', Vm=-50) + .paint('"custom"', tempK=270 * U.Kelvin) + .paint('"soma"', Vm=-50 * U.mV) # Paint density mechanisms. - .paint('"all"', density("pas")) - .paint('"custom"', density("hh")) - .paint('"dend"', density("Ih", gbar=0.001)) + .paint('"all"', A.density("pas")) + .paint('"custom"', A.density("hh")) + .paint('"dend"', A.density("Ih", gbar=0.001)) # Place stimuli and detectors. - .place('"root"', arbor.iclamp(10, 1, current=2), "iclamp0") - .place('"root"', arbor.iclamp(30, 1, current=2), "iclamp1") - .place('"root"', arbor.iclamp(50, 1, current=2), "iclamp2") - .place('"axon_terminal"', arbor.threshold_detector(-10), "detector") + .place('"root"', A.iclamp(10 * U.ms, 1 * U.ms, current=2 * U.nA), "iclamp0") + .place('"root"', A.iclamp(30 * U.ms, 1 * U.ms, current=2 * U.nA), "iclamp1") + .place('"root"', A.iclamp(50 * U.ms, 1 * U.ms, current=2 * U.nA), "iclamp2") + .place('"axon_terminal"', A.threshold_detector(-10 * U.mV), "detector") # Set discretisation: Soma as one CV, 1um everywhere else .discretization('(replace (single (region "soma")) (max-extent 1.0))') ) # (4) Create the cell. - -cell = arbor.cable_cell(morph, decor, labels) +cell = A.cable_cell(morph, decor, labels) -# (5) Create a class that inherits from arbor.recipe -class single_recipe(arbor.recipe): +# (5) Create a class that inherits from A.recipe +class single_recipe(A.recipe): # (5.1) Define the class constructor def __init__(self): # The base C++ class constructor must be called first, to ensure that # all memory in the C++ class is initialized correctly. - arbor.recipe.__init__(self) - - self.the_props = arbor.cable_global_properties() - self.the_props.set_property(Vm=-65, tempK=300, rL=35.4, cm=0.01) + A.recipe.__init__(self) + + self.the_props = A.cable_global_properties() + self.the_props.set_property( + Vm=-65 * U.mV, + tempK=300 * U.Kelvin, + rL=35.4 * U.Ohm * U.cm, + cm=0.01 * U.F / U.m2, + ) self.the_props.set_ion( - ion="na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na" + ion="na", + int_con=10 * U.mM, + ext_con=140 * U.mM, + rev_pot=50 * U.mV, + method="nernst/na", ) - self.the_props.set_ion(ion="k", int_con=54.4, ext_con=2.5, rev_pot=-77) - self.the_props.set_ion(ion="ca", int_con=5e-5, ext_con=2, rev_pot=132.5) - self.the_props.catalogue.extend(arbor.allen_catalogue(), "") + self.the_props.set_ion( + ion="k", int_con=54.4 * U.mM, ext_con=2.5 * U.mM, rev_pot=-77 * U.mV + ) + self.the_props.set_ion( + ion="ca", int_con=5e-5 * U.mM, ext_con=2 * U.mM, rev_pot=132.5 * U.mV + ) + self.the_props.catalogue.extend(A.allen_catalogue(), "") # (5.2) Override the num_cells method def num_cells(self): @@ -94,7 +111,7 @@ def num_cells(self): # (5.3) Override the cell_kind method def cell_kind(self, _): - return arbor.cell_kind.cable + return A.cell_kind.cable # (5.4) Override the cell_description method def cell_description(self, _): @@ -102,10 +119,10 @@ def cell_description(self, _): # (5.5) Override the probes method def probes(self, _): - return [arbor.cable_probe_membrane_voltage('"custom_terminal"', "Um")] + return [A.cable_probe_membrane_voltage('"custom_terminal"', "Um")] # (5.6) Override the global_properties method - def global_properties(self, gid): + def global_properties(self, _): return self.the_props @@ -113,42 +130,39 @@ def global_properties(self, gid): recipe = single_recipe() # (6) Create a simulation -sim = arbor.simulation(recipe) +sim = A.simulation(recipe) # Instruct the simulation to record the spikes and sample the probe -sim.record(arbor.spike_recording.all) +sim.record(A.spike_recording.all) -handle = sim.sample((0, "Um"), arbor.regular_schedule(0.02)) +handle = sim.sample((0, "Um"), A.regular_schedule(0.02 * U.ms)) # (7) Run the simulation -sim.run(tfinal=100, dt=0.025) +sim.run(tfinal=100 * U.ms, dt=0.025 * U.ms) -# (8) Print or display the results +# (8) Print spikes spikes = sim.spikes() print(len(spikes), "spikes recorded:") -for s in spikes: - print(s) - -data = [] -meta = [] -for d, m in sim.samples(handle): - data.append(d) - meta.append(m) - -df_list = [] -for i in range(len(data)): - df_list.append( - pandas.DataFrame( +for (gid, lid), t in spikes: + print(f" * t={t:.3f}ms gid={gid} lid={lid}") + +# (8) Plot the membrane potential +df = pd.concat( + [ + pd.DataFrame( { - "t/ms": data[i][:, 0], - "U/mV": data[i][:, 1], - "Location": str(meta[i]), + "t/ms": data[:, 0], + "U/mV": data[:, 1], + "Location": str(meta), "Variable": "voltage", } ) - ) -df = pandas.concat(df_list, ignore_index=True) -seaborn.relplot( + for data, meta in sim.samples(handle) + ], + ignore_index=True, +) + +sns.relplot( data=df, kind="line", x="t/ms", diff --git a/python/example/single_cell_model.py b/python/example/single_cell_model.py index c744738d73..9903494cee 100755 --- a/python/example/single_cell_model.py +++ b/python/example/single_cell_model.py @@ -1,52 +1,49 @@ #!/usr/bin/env python3 # This script is included in documentation. Adapt line numbers if touched. -import arbor -import pandas # You may have to pip install these. -import seaborn # You may have to pip install these. +import arbor as A +from arbor import units as U +import pandas as pd # You may have to pip install these. +import seaborn as sns # You may have to pip install these. # (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm -tree = arbor.segment_tree() -tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1) +tree = A.segment_tree() +tree.append(A.mnpos, A.mpoint(-3, 0, 0, 3), A.mpoint(3, 0, 0, 3), tag=1) # (2) Define the soma and its midpoint -labels = arbor.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) +labels = A.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) # (3) Create and set up a decor object - decor = ( - arbor.decor() - .set_property(Vm=-40) - .paint('"soma"', arbor.density("hh")) - .place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp") - .place('"midpoint"', arbor.threshold_detector(-10), "detector") + A.decor() + .set_property(Vm=-40 * U.mV) + .paint('"soma"', A.density("hh")) + .place('"midpoint"', A.iclamp(10 * U.ms, 2 * U.ms, 0.8 * U.nA), "iclamp") + .place('"midpoint"', A.threshold_detector(-10 * U.mV), "detector") ) # (4) Create cell and the single cell model based on it -cell = arbor.cable_cell(tree, decor, labels) +cell = A.cable_cell(tree, decor, labels) # (5) Make single cell model. -m = arbor.single_cell_model(cell) +m = A.single_cell_model(cell) # (6) Attach voltage probe sampling at 10 kHz (every 0.1 ms). -m.probe("voltage", '"midpoint"', tag="Um", frequency=10) +m.probe("voltage", '"midpoint"', tag="Um", frequency=10 * U.kHz) # (7) Run simulation for 30 ms of simulated activity. -m.run(tfinal=30) +m.run(tfinal=30 * U.ms) # (8) Print spike times. -if len(m.spikes) > 0: - print("{} spikes:".format(len(m.spikes))) - for s in m.spikes: - print("{:3.3f}".format(s)) -else: - print("no spikes") +print("{} spikes:".format(len(m.spikes))) +for s in m.spikes: + print(f" * {s:3.3f} ms") # (9) Plot the recorded voltages over time. print("Plotting results ...") -seaborn.set_theme() # Apply some styling to the plot -df = pandas.DataFrame({"t/ms": m.traces[0].time, "U/mV": m.traces[0].value}) -seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", errorbar=None).savefig( +sns.set_theme() # Apply some styling to the plot +df = pd.DataFrame({"t/ms": m.traces[0].time, "U/mV": m.traces[0].value}) +sns.relplot(data=df, kind="line", x="t/ms", y="U/mV", errorbar=None).savefig( "single_cell_model_result.svg" ) diff --git a/python/example/single_cell_nml.py b/python/example/single_cell_nml.py index adbdb268a6..c6ea816939 100755 --- a/python/example/single_cell_nml.py +++ b/python/example/single_cell_nml.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import arbor -from arbor import mechanism as mech -import pandas -import seaborn +import arbor as A +from arbor import units as U +import pandas as pd +import seaborn as sns import sys # Load a cell morphology from an nml file. @@ -14,7 +14,7 @@ filename = sys.argv[1] # Read the NeuroML morphology from the file. -morpho_nml = arbor.neuroml(filename) +morpho_nml = A.neuroml(filename) # Read the morphology data associated with morphology "m1". morpho_data = morpho_nml.morphology("m1") @@ -28,7 +28,7 @@ morpho_groups = morpho_data.groups() # Create new label dict with some locsets. -labels = arbor.label_dict( +labels = A.label_dict( { "stim_site": "(location 1 0.5)", # site for the stimulus, in the middle of branch 1. "axon_end": '(restrict-to (terminal) (region "axon"))', # end of the axon. @@ -45,52 +45,51 @@ print("Label dictionary locsets: ", labels.locsets, "\n") decor = ( - arbor.decor() + A.decor() # Set initial membrane potential to -55 mV - .set_property(Vm=-55) + .set_property(Vm=-55 * U.mV) # Use Nernst to calculate reversal potential for calcium. - .set_ion("ca", method=mech("nernst/x=ca")) + .set_ion("ca", method="nernst/x=ca") # hh mechanism on the soma and axon. - .paint('"soma"', arbor.density("hh")) - .paint('"axon"', arbor.density("hh")) + .paint('"soma"', A.density("hh")) + .paint('"axon"', A.density("hh")) # pas mechanism the dendrites. - .paint('"dend"', arbor.density("pas")) + .paint('"dend"', A.density("pas")) # Increase resistivity on dendrites. - .paint('"dend"', rL=500) + .paint('"dend"', rL=500 * U.Ohm * U.cm) # Attach stimuli that inject 4 nA current for 1 ms, starting at 3 and 8 ms. - .place('"root"', arbor.iclamp(10, 1, current=5), "iclamp0") - .place('"stim_site"', arbor.iclamp(3, 1, current=0.5), "iclamp1") - .place('"stim_site"', arbor.iclamp(10, 1, current=0.5), "iclamp2") - .place('"stim_site"', arbor.iclamp(8, 1, current=4), "iclamp3") + .place('"root"', A.iclamp(10 * U.ms, 1 * U.ms, current=5 * U.nA), "iclamp0") + .place('"stim_site"', A.iclamp(3 * U.ms, 1 * U.ms, current=0.5 * U.nA), "iclamp1") + .place('"stim_site"', A.iclamp(10 * U.ms, 1 * U.ms, current=0.5 * U.nA), "iclamp2") + .place('"stim_site"', A.iclamp(8 * U.ms, 1 * U.ms, current=4 * U.nA), "iclamp3") # Detect spikes at the soma with a voltage threshold of -10 mV. - .place('"axon_end"', arbor.threshold_detector(-10), "detector") + .place('"axon_end"', A.threshold_detector(-10 * U.mV), "detector") # Set discretisation: Soma as one CV, 1um everywhere else .discretization('(replace (single (region "soma")) (max-extent 1.0))') ) # Combine morphology with region and locset definitions to make a cable cell. -cell = arbor.cable_cell(morpho, decor, labels) +cell = A.cable_cell(morpho, decor, labels) print(cell.locations('"axon_end"')) # Make single cell model. -m = arbor.single_cell_model(cell) +m = A.single_cell_model(cell) # Attach voltage probes that sample at 50 kHz. -m.probe("voltage", where='"root"', tag="Um-root", frequency=50) -m.probe("voltage", where='"stim_site"', tag="Um-stim", frequency=50) -m.probe("voltage", where='"axon_end"', tag="Um-axon", frequency=50) +m.probe("voltage", where='"root"', tag="Um-root", frequency=50 * U.kHz) +m.probe("voltage", where='"stim_site"', tag="Um-stim", frequency=50 * U.kHz) +m.probe("voltage", where='"axon_end"', tag="Um-axon", frequency=50 * U.kHz) # Simulate the cell for 15 ms. -tfinal = 15 -m.run(tfinal) +m.run(15 * U.ms) print("Simulation done.") # Print spike times. if len(m.spikes) > 0: print("{} spikes:".format(len(m.spikes))) for s in m.spikes: - print(" {:7.4f}".format(s)) + print(f" {s:7.4f} ms") else: print("no spikes") @@ -99,7 +98,7 @@ df_list = [] for t in m.traces: df_list.append( - pandas.DataFrame( + pd.DataFrame( { "t/ms": t.time, "U/mV": t.value, @@ -109,9 +108,9 @@ ) ) -df = pandas.concat(df_list, ignore_index=True) +df = pd.concat(df_list, ignore_index=True) -seaborn.relplot( +sns.relplot( data=df, kind="line", x="t/ms", diff --git a/python/example/single_cell_recipe.py b/python/example/single_cell_recipe.py index 9ebc1c01e5..dba7f3a29b 100644 --- a/python/example/single_cell_recipe.py +++ b/python/example/single_cell_recipe.py @@ -1,44 +1,40 @@ #!/usr/bin/env python3 # This script is included in documentation. Adapt line numbers if touched. -import arbor -import pandas # You may have to pip install these. -import seaborn # You may have to pip install these. +import arbor as A +from arbor import units as U +import pandas as pd # You may have to pip install these. +import seaborn as sns # You may have to pip install these. # The corresponding generic recipe version of `single_cell_model.py`. # (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm - -tree = arbor.segment_tree() -tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1) +tree = A.segment_tree() +tree.append(A.mnpos, A.mpoint(-3, 0, 0, 3), A.mpoint(3, 0, 0, 3), tag=1) # (2) Define the soma and its midpoint - -labels = arbor.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) +labels = A.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) # (3) Create cell and set properties - decor = ( - arbor.decor() - .set_property(Vm=-40) - .paint('"soma"', arbor.density("hh")) - .place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp") - .place('"midpoint"', arbor.threshold_detector(-10), "detector") + A.decor() + .set_property(Vm=-40 * U.mV) + .paint('"soma"', A.density("hh")) + .place('"midpoint"', A.iclamp(10 * U.ms, 2 * U.ms, 0.8 * U.nA), "iclamp") + .place('"midpoint"', A.threshold_detector(-10 * U.mV), "detector") ) -cell = arbor.cable_cell(tree, decor, labels) +cell = A.cable_cell(tree, decor, labels) + # (4) Define a recipe for a single cell and set of probes upon it. # This constitutes the corresponding generic recipe version of # `single_cell_model.py`. - - -class single_recipe(arbor.recipe): - # (4.1) The base class constructor must be called first, to ensure that - # all memory in the wrapped C++ class is initialized correctly. +class single_recipe(A.recipe): + # (4.1) Base constructor must be called, to ensure correct initialization. def __init__(self): - arbor.recipe.__init__(self) - self.the_props = arbor.neuron_cable_properties() + A.recipe.__init__(self) + self.the_props = A.neuron_cable_properties() # (4.2) Override the num_cells method def num_cells(self): @@ -46,7 +42,7 @@ def num_cells(self): # (4.3) Override the cell_kind method def cell_kind(self, _): - return arbor.cell_kind.cable + return A.cell_kind.cable # (4.4) Override the cell_description method def cell_description(self, gid): @@ -54,7 +50,7 @@ def cell_description(self, gid): # (4.5) Override the probes method with a voltage probe located on "midpoint" def probes(self, _): - return [arbor.cable_probe_membrane_voltage('"midpoint"', "Um")] + return [A.cable_probe_membrane_voltage('"midpoint"', "Um")] # (4.6) Override the global_properties method def global_properties(self, kind): @@ -62,37 +58,31 @@ def global_properties(self, kind): # (5) Instantiate recipe. - recipe = single_recipe() -# (6) Create simulation. When their defaults are sufficient, context and domain decomposition don't -# have to be manually specified and the simulation can be created with just the recipe as argument. - -sim = arbor.simulation(recipe) - -# (7) Create and run simulation and set up 10 kHz (every 0.1 ms) sampling on the probe. -# The probe is located on cell 0, and is the 0th probe on that cell, thus has probeset_id (0, 0). +# (6) Create simulation. When their defaults are sufficient, context and domain +# decomposition don't have to be manually specified and the simulation can be +# created with just the recipe as argument. +sim = A.simulation(recipe) -sim.record(arbor.spike_recording.all) -handle = sim.sample((0, "Um"), arbor.regular_schedule(0.1)) -sim.run(tfinal=30) +# (7) Create and run simulation and set up 10 kHz (every 0.1 ms) sampling on the +# probe. The probe is located on cell 0, and is the 0th probe on that cell, thus +# has probeset_id (0, 0). +sim.record(A.spike_recording.all) +handle = sim.sample((0, "Um"), A.regular_schedule(0.1 * U.ms)) +sim.run(tfinal=30 * U.ms) # (8) Collect results. - spikes = sim.spikes() data, meta = sim.samples(handle)[0] -if len(spikes) > 0: - print("{} spikes:".format(len(spikes))) - for t in spikes["time"]: - print("{:3.3f}".format(t)) -else: - print("no spikes") +print("{} spikes:".format(len(spikes))) +for t in spikes["time"]: + print(f" * {t:3.3f} ms") print("Plotting results ...") - -df = pandas.DataFrame({"t/ms": data[:, 0], "U/mV": data[:, 1]}) -seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", errorbar=None).savefig( +df = pd.DataFrame({"t/ms": data[:, 0], "U/mV": data[:, 1]}) +sns.relplot(data=df, kind="line", x="t/ms", y="U/mV", errorbar=None).savefig( "single_cell_recipe_result.svg" ) diff --git a/python/example/single_cell_stdp.py b/python/example/single_cell_stdp.py index 9e84012717..d460fbd7d6 100755 --- a/python/example/single_cell_stdp.py +++ b/python/example/single_cell_stdp.py @@ -1,71 +1,68 @@ #!/usr/bin/env python3 -import arbor +import arbor as A +from arbor import units as U import numpy as np import pandas as pd import seaborn as sns # You may have to pip install these. -class single_recipe(arbor.recipe): +class single_recipe(A.recipe): def __init__(self, dT, n_pairs): - arbor.recipe.__init__(self) + A.recipe.__init__(self) self.dT = dT self.n_pairs = n_pairs - self.the_props = arbor.neuron_cable_properties() + self.the_props = A.neuron_cable_properties() def num_cells(self): return 1 def cell_kind(self, gid): - return arbor.cell_kind.cable + return A.cell_kind.cable def cell_description(self, gid): - tree = arbor.segment_tree() - tree.append( - arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1 - ) + tree = A.segment_tree() + tree.append(A.mnpos, (-3, 0, 0, 3), (3, 0, 0, 3), tag=1) - labels = arbor.label_dict({"soma": "(tag 1)", "center": "(location 0 0.5)"}) + labels = A.label_dict({"soma": "(tag 1)", "center": "(location 0 0.5)"}) decor = ( - arbor.decor() - .set_property(Vm=-40) - .paint("(all)", arbor.density("hh")) - .place('"center"', arbor.threshold_detector(-10), "detector") - .place('"center"', arbor.synapse("expsyn"), "synapse") + A.decor() + .set_property(Vm=-40 * U.mV) + .paint("(all)", A.density("hh")) + .place('"center"', A.threshold_detector(-10 * U.mV), "detector") + .place('"center"', A.synapse("expsyn"), "synapse") .place( '"center"', - arbor.synapse("expsyn_stdp", max_weight=1.0), + A.synapse("expsyn_stdp", max_weight=1.0), "stpd_synapse", ) ) - return arbor.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) def event_generators(self, gid): """two stimuli: one that makes the cell spike, the other to monitor STDP""" - stimulus_times = np.linspace(50, 500, self.n_pairs) + stimulus_times = np.linspace(50, 500, self.n_pairs) * U.ms # strong enough stimulus - spike = arbor.event_generator( - "synapse", 1.0, arbor.explicit_schedule(stimulus_times) - ) + spike = A.event_generator("synapse", 1.0, A.explicit_schedule(stimulus_times)) # zero weight -> just modify synaptic weight via stdp - stdp = arbor.event_generator( - "stpd_synapse", 0.0, arbor.explicit_schedule(stimulus_times - self.dT) + stdp = A.event_generator( + "stpd_synapse", 0.0, A.explicit_schedule(stimulus_times - self.dT) ) return [spike, stdp] def probes(self, gid): def mk(s, t): - return arbor.cable_probe_point_state(1, "expsyn_stdp", state=s, tag=t) + return A.cable_probe_point_state(1, "expsyn_stdp", state=s, tag=t) return [ - arbor.cable_probe_membrane_voltage('"center"', "Um"), + A.cable_probe_membrane_voltage('"center"', "Um"), mk("g", "state-g"), mk("apost", "state-apost"), mk("apre", "state-apre"), @@ -79,11 +76,11 @@ def global_properties(self, kind): def run(dT, n_pairs=1, do_plots=False): recipe = single_recipe(dT, n_pairs) - sim = arbor.simulation(recipe) + sim = A.simulation(recipe) - sim.record(arbor.spike_recording.all) + sim.record(A.spike_recording.all) - reg_sched = arbor.regular_schedule(0.1) + reg_sched = A.regular_schedule(0.1 * U.ms) handles = { "U": sim.sample((0, "Um"), reg_sched), "g": sim.sample((0, "state-g"), reg_sched), @@ -92,7 +89,7 @@ def run(dT, n_pairs=1, do_plots=False): "weight_plastic": sim.sample((0, "state-weight"), reg_sched), } - sim.run(tfinal=600) + sim.run(tfinal=0.6 * U.s) if do_plots: print("Plotting detailed results ...") @@ -107,7 +104,7 @@ def run(dT, n_pairs=1, do_plots=False): return weight_plastic[:, 1][-1] -data = np.array([(dT, run(dT)) for dT in np.arange(-20, 20, 0.5)]) +data = np.array([(dT, run(dT * U.ms)) for dT in np.arange(-20, 20, 0.5)]) df = pd.DataFrame({"t/ms": data[:, 0], "dw": data[:, 1]}) print("Plotting results ...") sns.relplot(data=df, x="t/ms", y="dw", kind="line", errorbar=None).savefig( diff --git a/python/example/single_cell_swc.py b/python/example/single_cell_swc.py index d5cbf46d69..097c11b60b 100755 --- a/python/example/single_cell_swc.py +++ b/python/example/single_cell_swc.py @@ -10,9 +10,10 @@ # preserve surface area and correct starting locations of cables # attached to the soma. -import arbor -import pandas -import seaborn +import arbor as A +from arbor import units as U +import pandas as pd +import seaborn as sns import sys # Load a cell morphology from an swc file. @@ -22,10 +23,10 @@ sys.exit(0) filename = sys.argv[1] -morpho = arbor.load_swc_arbor(filename) +morpho = A.load_swc_arbor(filename) # Define the regions and locsets in the model. -labels = arbor.label_dict( +labels = A.label_dict( { "root": "(root)", # the start of the soma in this morphology is at the root of the cell. "stim_site": "(location 0 0.5)", # site for the stimulus, in the middle of branch 0. @@ -34,46 +35,43 @@ ).add_swc_tags() # Finally, add the SWC default labels. decor = ( - arbor.decor() + A.decor() # Set initial membrane potential to -55 mV - .set_property(Vm=-55) + .set_property(Vm=-55 * U.mV) # Use Nernst to calculate reversal potential for calcium. - .set_ion("ca", method=arbor.mechanism("nernst/x=ca")) + .set_ion("ca", method=A.mechanism("nernst/x=ca")) # hh mechanism on the soma and axon. - .paint('"soma"', arbor.density("hh")) - .paint('"axon"', arbor.density("hh")) + .paint('"soma"', A.density("hh")) + .paint('"axon"', A.density("hh")) # pas mechanism the dendrites. - .paint('"dend"', arbor.density("pas")) + .paint('"dend"', A.density("pas")) # Increase resistivity on dendrites. - .paint('"dend"', rL=500) + .paint('"dend"', rL=500 * U.Ohm * U.cm) # Attach stimuli that inject 4 nA current for 1 ms, starting at 3 and 8 ms. - .place('"root"', arbor.iclamp(10, 1, current=5), "iclamp0") - .place('"stim_site"', arbor.iclamp(3, 1, current=0.5), "iclamp1") - .place('"stim_site"', arbor.iclamp(10, 1, current=0.5), "iclamp2") - .place('"stim_site"', arbor.iclamp(8, 1, current=4), "iclamp3") + .place('"root"', A.iclamp(10 * U.ms, 1 * U.ms, current=5 * U.nA), "iclamp0") + .place('"stim_site"', A.iclamp(3 * U.ms, 1 * U.ms, current=0.5 * U.nA), "iclamp1") + .place('"stim_site"', A.iclamp(10 * U.ms, 1 * U.ms, current=0.5 * U.nA), "iclamp2") + .place('"stim_site"', A.iclamp(8 * U.ms, 1 * U.ms, current=4 * U.nA), "iclamp3") # Detect spikes at the soma with a voltage threshold of -10 mV. - .place('"axon_end"', arbor.threshold_detector(-10), "detector") + .place('"axon_end"', A.threshold_detector(-10 * U.mV), "detector") # Create the policy used to discretise the cell into CVs. # Use a single CV for the soma, and CVs of maximum length 1 μm elsewhere. .discretization('(replace (single (region "soma")) (max-extent 1.0))') ) # Combine morphology with region and locset definitions to make a cable cell. -cell = arbor.cable_cell(morpho, decor, labels) - -print(cell.locations('"axon_end"')) +cell = A.cable_cell(morpho, decor, labels) # Make single cell model. -m = arbor.single_cell_model(cell) +m = A.single_cell_model(cell) # Attach voltage probes that sample at 50 kHz. -m.probe("voltage", tag="Um-root", where='"root"', frequency=50) -m.probe("voltage", tag="Um-stim", where='"stim_site"', frequency=50) -m.probe("voltage", tag="Um-axon", where='"axon_end"', frequency=50) +m.probe("voltage", tag="Um-root", where='"root"', frequency=50 * U.kHz) +m.probe("voltage", tag="Um-stim", where='"stim_site"', frequency=50 * U.kHz) +m.probe("voltage", tag="Um-axon", where='"axon_end"', frequency=50 * U.kHz) # Simulate the cell for 15 ms. -tfinal = 15 -m.run(tfinal) +m.run(15 * U.ms) print("Simulation done.") # Print spike times. @@ -89,7 +87,7 @@ df_list = [] for t in m.traces: df_list.append( - pandas.DataFrame( + pd.DataFrame( { "t/ms": t.time, "U/mV": t.value, @@ -99,9 +97,9 @@ ) ) -df = pandas.concat(df_list, ignore_index=True) +df = pd.concat(df_list, ignore_index=True) -seaborn.relplot( +sns.relplot( data=df, kind="line", x="t/ms", diff --git a/python/example/v-clamp.py b/python/example/v-clamp.py index 819c0925c7..3b17c05df9 100755 --- a/python/example/v-clamp.py +++ b/python/example/v-clamp.py @@ -1,38 +1,38 @@ #!/usr/bin/env python3 -import arbor -import pandas # You may have to pip install these. -import seaborn # You may have to pip install these. +import arbor as A +from arbor import units as U +import pandas as pd # You may have to pip install these. +import seaborn as sns # You may have to pip install these. # (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm -tree = arbor.segment_tree() -tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1) +tree = A.segment_tree() +tree.append(A.mnpos, A.mpoint(-3, 0, 0, 3), A.mpoint(3, 0, 0, 3), tag=1) # (2) Define the soma and its midpoint -labels = arbor.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) +labels = A.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) # (3) Create and set up a decor object - decor = ( - arbor.decor() - .set_property(Vm=-40) - .paint('"soma"', arbor.density("hh")) - .paint('"soma"', arbor.voltage_process("v_clamp/v0=-42")) - .place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp") - .place('"midpoint"', arbor.threshold_detector(-10), "detector") + A.decor() + .set_property(Vm=-40 * U.mV) + .paint('"soma"', A.density("hh")) + .paint('"soma"', A.voltage_process("v_clamp/v0=-42")) + .place('"midpoint"', A.iclamp(10 * U.ms, 2 * U.ms, 0.8 * U.nA), "iclamp") + .place('"midpoint"', A.threshold_detector(-10 * U.mV), "detector") ) # (4) Create cell and the single cell model based on it -cell = arbor.cable_cell(tree, decor, labels) +cell = A.cable_cell(tree, decor, labels) # (5) Make single cell model. -m = arbor.single_cell_model(cell) +m = A.single_cell_model(cell) # (6) Attach voltage probe sampling at 10 kHz (every 0.1 ms). -m.probe("voltage", '"midpoint"', "Um", frequency=10) +m.probe("voltage", '"midpoint"', "Um", frequency=10 * U.kHz) # (7) Run simulation for 30 ms of simulated activity. -m.run(tfinal=30) +m.run(tfinal=30 * U.ms) # (8) Print spike times. if len(m.spikes) > 0: @@ -44,8 +44,8 @@ # (9) Plot the recorded voltages over time. print("Plotting results ...") -df = pandas.DataFrame({"t/ms": m.traces[0].time, "U/mV": m.traces[0].value}) -seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", errorbar=None).savefig( +df = pd.DataFrame({"t/ms": m.traces[0].time, "U/mV": m.traces[0].value}) +sns.relplot(data=df, kind="line", x="t/ms", y="U/mV", errorbar=None).savefig( "v-clamp.svg" ) diff --git a/python/identifiers.cpp b/python/identifiers.cpp index 5a9ff9a1eb..e58a8a035d 100644 --- a/python/identifiers.cpp +++ b/python/identifiers.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include #include "strprintf.hpp" @@ -21,6 +23,11 @@ void register_identifiers(py::module& m) { .value("univalent", arb::lid_selection_policy::assert_univalent, "Assert that there is only one possible location associated with a labeled item on the cell. The model throws an exception if the assertion fails."); + py::class_ cell_address(m, "cell_address"); + cell_address + .def_readwrite("gid", &arb::cell_address_type::gid) + .def_readwrite("tag", &arb::cell_address_type::tag); + py::class_ cell_local_label_type(m, "cell_local_label", "For local identification of an item.\n\n" "cell_local_label identifies:\n" @@ -146,6 +153,21 @@ void register_identifiers(py::module& m) { "Use GPU backend.") .value("multicore", arb::backend_kind::multicore, "Use multicore backend."); + + // Probes + py::class_ probe(m, "probe"); + probe + .def("__repr__", [](const arb::probe_info& p){return util::pprintf("", p.tag);}) + .def("__str__", [](const arb::probe_info& p){return util::pprintf("", p.tag);}); + + py::class_ spike(m, "spike"); + spike + .def(py::init([](const arb::cell_member_type& m, arb::time_type t) -> arb::spike { return {m, t}; })) + .def_readwrite("source", &arb::spike::source, "The global identifier of the cell.") + .def_readwrite("time", &arb::spike::time, "The time of spike.") + .def("__repr__", [](const arb::spike& s){return util::pprintf("", s);}) + .def("__str__", [](const arb::spike& s){return util::pprintf("", s);}); + } } // namespace pyarb diff --git a/python/label_dict.cpp b/python/label_dict.cpp new file mode 100644 index 0000000000..ff71d1481d --- /dev/null +++ b/python/label_dict.cpp @@ -0,0 +1,88 @@ +#include "label_dict.hpp" + +#include +#include + +namespace pyarb { + +namespace py = pybind11; +using namespace py::literals; +void register_label_dict(py::module& m) { + + py::class_ label_dict(m, "label_dict", + "A dictionary of labelled region and locset definitions, with a\n" + "unique label assigned to each definition."); + label_dict + .def(py::init<>(), + "Create an empty label dictionary.") + .def(py::init&>(), + "Initialize a label dictionary from a dictionary with string labels as keys," + " and corresponding definitions as strings.") + .def(py::init(), + "Initialize a label dictionary from another one") + .def(py::init([](py::iterator& it) { + label_dict_proxy ld; + for (; it != py::iterator::sentinel(); ++it) { + const auto tuple = it->cast(); + const auto key = tuple[0].cast(); + const auto value = tuple[1].cast(); + ld.set(key, value); + } + return ld; + }), + "Initialize a label dictionary from an iterable of key, definition pairs") + .def("add_swc_tags", + [](label_dict_proxy& l) { return l.add_swc_tags(); }, + "Add standard SWC tagged regions.\n" + " - soma: (tag 1)\n" + " - axon: (tag 2)\n" + " - dend: (tag 3)\n" + " - apic: (tag 4)") + .def("__setitem__", + [](label_dict_proxy& l, const char* name, const char* desc) { + l.set(name, desc);}) + .def("__getitem__", + [](label_dict_proxy& l, const char* name) { + if (auto v = l.getitem(name)) return v.value(); + throw py::key_error(name); + }) + .def("__len__", &label_dict_proxy::size) + .def("__iter__", + [](const label_dict_proxy &ld) { + return py::make_key_iterator(ld.cache.begin(), ld.cache.end());}, + py::keep_alive<0, 1>()) + .def("__contains__", + [](const label_dict_proxy &ld, const char* name) { + return ld.contains(name);}) + .def("keys", + [](const label_dict_proxy &ld) { + return py::make_key_iterator(ld.cache.begin(), ld.cache.end());}, + py::keep_alive<0, 1>()) + .def("items", + [](const label_dict_proxy &ld) { + return py::make_iterator(ld.cache.begin(), ld.cache.end());}, + py::keep_alive<0, 1>()) + .def("values", + [](const label_dict_proxy &ld) { + return py::make_value_iterator(ld.cache.begin(), ld.cache.end()); + }, + py::keep_alive<0, 1>()) + .def("append", [](label_dict_proxy& l, const label_dict_proxy& other, const char* prefix) { + l.import(other, prefix); + }, + "other"_a, "The label_dict to be imported" + "prefix"_a="", "optional prefix appended to the region and locset labels", + "Import the entries of a another label dictionary with an optional prefix.") + .def("update", [](label_dict_proxy& l, const label_dict_proxy& other) { + l.import(other); + }, + "other"_a, "The label_dict to be imported" + "Import the entries of a another label dictionary.") + .def_readonly("regions", &label_dict_proxy::regions, + "The region definitions.") + .def_readonly("locsets", &label_dict_proxy::locsets, + "The locset definitions.") + .def("__repr__", [](const label_dict_proxy& d){return d.to_string();}) + .def("__str__", [](const label_dict_proxy& d){return d.to_string();}); +} +} diff --git a/python/proxy.hpp b/python/label_dict.hpp similarity index 98% rename from python/proxy.hpp rename to python/label_dict.hpp index 31333e132e..9d62fd1455 100644 --- a/python/proxy.hpp +++ b/python/label_dict.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include @@ -27,9 +27,7 @@ struct label_dict_proxy { label_dict_proxy() = default; label_dict_proxy(const str_map& in) { - for (auto& i: in) { - set(i.first, i.second); - } + for (auto& [k, v]: in) set(k, v); } label_dict_proxy& add_swc_tags() { diff --git a/python/mechanism.cpp b/python/mechanism.cpp index 5092b17e83..31600b5bdb 100644 --- a/python/mechanism.cpp +++ b/python/mechanism.cpp @@ -1,5 +1,4 @@ #include -#include #include #include "pybind11/pytypes.h" @@ -12,7 +11,6 @@ #include "arbor/mechinfo.hpp" #include "util.hpp" -#include "conversion.hpp" #include "strprintf.hpp" namespace pyarb { diff --git a/python/morphology.cpp b/python/morphology.cpp index 3c0e3579a9..dacf19ede0 100644 --- a/python/morphology.cpp +++ b/python/morphology.cpp @@ -1,4 +1,3 @@ -#include #include #include @@ -22,7 +21,7 @@ #include "util.hpp" #include "error.hpp" -#include "proxy.hpp" +#include "label_dict.hpp" #include "strprintf.hpp" namespace py = pybind11; @@ -42,15 +41,23 @@ void check_trailing(std::istream& in, std::string fname) { void register_morphology(py::module& m) { using namespace py::literals; - // - // primitives: points, segments, locations, cables... etc. - // - m.attr("mnpos") = arb::mnpos; + py::class_ morph(m, "morphology", "A cell morphology."); + py::class_ location(m, "location", "A location on a cable cell."); + py::class_ extent(m, "extent", "A potentially empty region on a morphology."); + py::class_ mpoint(m, "mpoint"); + py::class_ cable(m, "cable"); + py::class_ isometry(m, "isometry"); + py::class_ place(m, "place_pwlin"); + py::class_ segment_tree(m, "segment_tree"); + py::class_ asc_morphology(m, "asc_morphology", "The morphology and label dictionary meta-data loaded from a Neurolucida ASCII (.asc) file."); + py::class_ nml_morph_data(m, "neuroml_morph_data"); + py::class_ neuroml(m, "neuroml"); + py::class_ prov(m, "morphology_provider"); + py::class_ msegment(m, "msegment"); + // arb::mlocation - py::class_ location(m, "location", - "A location on a cable cell."); location .def(py::init( [](arb::msize_t branch, double pos) { @@ -73,7 +80,6 @@ void register_morphology(py::module& m) { [](arb::mlocation l) { return util::pprintf("(location {} {})", l.branch, l.pos); }); // arb::mpoint - py::class_ mpoint(m, "mpoint"); mpoint .def(py::init(), "x"_a, "y"_a, "z"_a, "radius"_a, @@ -98,14 +104,12 @@ void register_morphology(py::module& m) { py::implicitly_convertible(); // arb::msegment - py::class_ msegment(m, "msegment"); msegment .def_readonly("prox", &arb::msegment::prox, "the location and radius of the proximal end.") .def_readonly("dist", &arb::msegment::dist, "the location and radius of the distal end.") .def_readonly("tag", &arb::msegment::tag, "tag meta-data."); // arb::mcable - py::class_ cable(m, "cable"); cable .def(py::init( [](arb::msize_t bid, double prox, double dist) { @@ -127,7 +131,6 @@ void register_morphology(py::module& m) { .def("__repr__", [](const arb::mcable& c) { return util::pprintf("{}", c); }); // arb::isometry - py::class_ isometry(m, "isometry"); isometry .def(py::init<>(), "Construct a trivial isometry.") .def("__call__", [](arb::isometry& iso, arb::mpoint& p) { @@ -178,10 +181,9 @@ void register_morphology(py::module& m) { "Construct a rotation isometry of angle theta about the given axis in the direction described by a tuple."); // arb::place_pwlin - py::class_ place(m, "place_pwlin"); place .def(py::init(), - "morphology"_a, "isometry"_a=arb::isometry{}, + "morphology"_a, py::arg_v("isometry", arb::isometry(), "id"), "Construct a piecewise-linear placement object from the given morphology and optional isometry.") .def("at", &arb::place_pwlin::at, "location"_a, "Return an interpolated mpoint corresponding to the location argument.") @@ -210,7 +212,6 @@ void register_morphology(py::module& m) { "Returns the location and its distance from the point."); // arb::place_pwlin - py::class_ prov(m, "morphology_provider"); prov .def(py::init(), "morphology"_a, @@ -228,14 +229,7 @@ void register_morphology(py::module& m) { }, "Turn a region into an extent."); - - - // - // Higher-level data structures (segment_tree, morphology) - // - // arb::segment_tree - py::class_ segment_tree(m, "segment_tree"); segment_tree // constructors .def(py::init<>()) @@ -290,6 +284,37 @@ void register_morphology(py::module& m) { .def("__str__", [](const arb::segment_tree& s) { return util::pprintf("", s);}); + // arb::morphology + morph + // constructors + .def(py::init( + [](arb::segment_tree t){ + return arb::morphology(std::move(t)); + })) + // morphology's interface is read-only by design, so most of it can + // be implemented as read-only properties. + .def_property_readonly("empty", + [](const arb::morphology& m){return m.empty();}, + "Whether the morphology is empty.") + .def_property_readonly("num_branches", + [](const arb::morphology& m){return m.num_branches();}, + "The number of branches in the morphology.") + .def("branch_parent", &arb::morphology::branch_parent, + "i"_a, "The parent branch of branch i.") + .def("branch_children", &arb::morphology::branch_children, + "i"_a, "The child branches of branch i.") + .def("branch_segments", + [](const arb::morphology& m, arb::msize_t i) { + return m.branch_segments(i); + }, + "i"_a, "A list of the segments in branch i, ordered from proximal to distal ends of the branch.") + .def("to_segment_tree", &arb::morphology::to_segment_tree, + "Convert this morphology to a segment_tree.") + .def("__str__", + [](const arb::morphology& m) { + return util::pprintf("", m); + }); + using morph_or_tree = std::variant; // Function that creates a morphology/segment_tree from an swc file. @@ -309,9 +334,8 @@ void register_morphology(py::module& m) { throw pyarb_error(util::pprintf("Arbor SWC: parse error: {}", e.what())); } }, - "filename_or_stream"_a, - pybind11::arg_v("raw", false, "Return a segment tree instead of a fully formed morphology"), - "Generate a morphology/segment_tree from an SWC file following the rules prescribed by Arbor.\n" + "filename_or_stream"_a, "raw"_a=false, + "Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by Arbor.\n" "Specifically:\n" " * Single-segment somas are disallowed.\n" " * There are no special rules related to somata. They can be one or multiple branches\n" @@ -333,50 +357,13 @@ void register_morphology(py::module& m) { throw pyarb_error(util::pprintf("NEURON SWC: parse error: {}", e.what())); } }, - "filename_or_stream"_a, - pybind11::arg_v("raw", false, "Return a segment tree instead of a fully formed morphology"), - "Generate a morphology from an SWC file following the rules prescribed by NEURON.\n" + "filename_or_stream"_a, "raw"_a=false, + "Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by NEURON.\n" "See the documentation https://docs.arbor-sim.org/en/latest/fileformat/swc.html\n" "for a detailed description of the interpretation."); - - // arb::morphology - - py::class_ morph(m, "morphology"); - morph - // constructors - .def(py::init( - [](arb::segment_tree t){ - return arb::morphology(std::move(t)); - })) - // morphology's interface is read-only by design, so most of it can - // be implemented as read-only properties. - .def_property_readonly("empty", - [](const arb::morphology& m){return m.empty();}, - "Whether the morphology is empty.") - .def_property_readonly("num_branches", - [](const arb::morphology& m){return m.num_branches();}, - "The number of branches in the morphology.") - .def("branch_parent", &arb::morphology::branch_parent, - "i"_a, "The parent branch of branch i.") - .def("branch_children", &arb::morphology::branch_children, - "i"_a, "The child branches of branch i.") - .def("branch_segments", - [](const arb::morphology& m, arb::msize_t i) { - return m.branch_segments(i); - }, - "i"_a, "A list of the segments in branch i, ordered from proximal to distal ends of the branch.") - .def("to_segment_tree", &arb::morphology::to_segment_tree, - "Convert this morphology to a segment_tree.") - .def("__str__", - [](const arb::morphology& m) { - return util::pprintf("", m); - }); - // Neurolucida ASCII, or .asc, file format support. - py::class_ asc_morphology(m, "asc_morphology", - "The morphology and label dictionary meta-data loaded from a Neurolucida ASCII (.asc) file."); asc_morphology .def_readonly("morphology", &arborio::asc_morphology::morphology, @@ -404,12 +391,10 @@ void register_morphology(py::module& m) { throw pyarb_error(util::pprintf("error loading neurolucida asc file: {}", e.what())); } }, - "filename_or_stream"_a, - pybind11::arg_v("raw", false, "Return a segment tree instead of a fully formed morphology"), - "Load a morphology or segment_tree and meta data from a Neurolucida ASCII .asc file."); + "filename_or_stream"_a, "raw"_a=false, + "Load a morphology or segment_tree (raw=True) and meta data from a Neurolucida ASCII .asc file."); // arborio::morphology_data - py::class_ nml_morph_data(m, "neuroml_morph_data"); nml_morph_data .def_readonly("cell_id", &arborio::nml_morphology_data::cell_id, @@ -434,7 +419,6 @@ void register_morphology(py::module& m) { "Map from segmentGroup ids to their corresponding segment ids."); // arborio::neuroml - py::class_ neuroml(m, "neuroml"); neuroml // constructors .def(py::init( diff --git a/python/probes.cpp b/python/probes.cpp index 161c438be3..ad734fff89 100644 --- a/python/probes.cpp +++ b/python/probes.cpp @@ -153,11 +153,8 @@ void register_probe_meta_maps(pyarb_global_ptr g) { }); } - - // Wrapper functions around cable_cell probe types that return arb::probe_info values: // (Probe tag value is implicitly left at zero.) - arb::probe_info cable_probe_membrane_voltage(const char* where, const std::string& tag) { return {arb::cable_probe_membrane_voltage{arborio::parse_locset_expression(where).unwrap()}, tag}; } @@ -242,7 +239,6 @@ arb::probe_info lif_probe_voltage(const std::string& tag) { return {arb::lif_probe_voltage{}, tag}; } - // Add wrappers to module, recorder factories to global data. void register_cable_probes(pybind11::module& m, pyarb_global_ptr global_ptr) { @@ -251,11 +247,13 @@ void register_cable_probes(pybind11::module& m, pyarb_global_ptr global_ptr) { // Probe metadata wrappers: - py::class_ lif_probe_metadata(m, "lif_probe_metadata", - "Probe metadata associated with a LIF cell probe."); + py::class_ lif_probe_metadata(m, + "lif_probe_metadata", + "Probe metadata associated with a LIF cell probe."); - py::class_ cable_probe_point_info(m, "cable_probe_point_info", - "Probe metadata associated with a cable cell probe for point process state."); + py::class_ cable_probe_point_info(m, + "cable_probe_point_info", + "Probe metadata associated with a cable cell probe for point process state."); cable_probe_point_info .def_readwrite("target", &arb::cable_probe_point_info::target, @@ -281,11 +279,11 @@ void register_cable_probes(pybind11::module& m, pyarb_global_ptr global_ptr) { "Probe specification for cable cell membrane voltage associated with each cable in each CV.", "tag"_a); m.def("cable_probe_axial_current", &cable_probe_axial_current, - "Probe specification for cable cell axial current at points in a location set.", - "where"_a, "tag"_a); + "Probe specification for cable cell axial current at points in a location set.", + "where"_a, "tag"_a); m.def("cable_probe_total_ion_current_density", &cable_probe_total_ion_current_density, - "Probe specification for cable cell total transmembrane current density excluding capacitive currents at points in a location set.", - "where"_a, "tag"_a); + "Probe specification for cable cell total transmembrane current density excluding capacitive currents at points in a location set.", + "where"_a, "tag"_a); m.def("cable_probe_total_ion_current_cell", &cable_probe_total_ion_current_cell, "Probe specification for cable cell total transmembrane current excluding capacitive currents for each cable in each CV.", diff --git a/python/profiler.cpp b/python/profiler.cpp index b3fe1011be..c23f7a80c3 100644 --- a/python/profiler.cpp +++ b/python/profiler.cpp @@ -1,5 +1,3 @@ -#include - #include #include @@ -32,9 +30,9 @@ void register_profiler(pybind11::module& m) { manager.checkpoint(name, ctx.context); }, "name"_a, "context"_a, - "Create a new checkpoint. Records the time since the last checkpoint\ - (or the call to start if no previous checkpoints exist),\ - and restarts the timer for the next checkpoint.") + "Create a new checkpoint. Records the time since the last checkpoint" + "(or the call to start if no previous checkpoints exist)," + "and restarts the timer for the next checkpoint.") .def_property_readonly("checkpoint_names", &arb::profile::meter_manager::checkpoint_names, "A list of all metering checkpoint names.") .def_property_readonly("times", &arb::profile::meter_manager::times, @@ -65,8 +63,8 @@ void register_profiler(pybind11::module& m) { arb::profile::print_profiler_summary(stream, limit); return stream.str(); }, - pybind11::arg_v("limit", 0.0, "Print contributions above percent. Defaults to showing all.") - ); + "limit"_a=0.0, + "Show summary of the profile; printing contributions above `limit` percent. Defaults to showing all."); #endif } diff --git a/python/pyarb.cpp b/python/pyarb.cpp index e15194f29a..a835da7963 100644 --- a/python/pyarb.cpp +++ b/python/pyarb.cpp @@ -30,6 +30,8 @@ void register_schedules(pybind11::module& m); void register_simulation(pybind11::module& m, pyarb_global_ptr); void register_arborenv(pybind11::module& m); void register_single_cell(pybind11::module& m); +void register_units(pybind11::module& m); +void register_label_dict(pybind11::module& m); #ifdef ARB_MPI_ENABLED void register_mpi(pybind11::module& m); @@ -47,22 +49,26 @@ PYBIND11_MODULE(_arbor, m) { m.doc() = "arbor: multicompartment neural network models."; m.attr("__version__") = ARB_VERSION; - pyarb::register_cable_loader(m); + // NOTE: This is precisely ordered so that we do not leak C++ types! + pyarb::register_units(m); + pyarb::register_identifiers(m); + pyarb::register_label_dict(m); + pyarb::register_schedules(m); + pyarb::register_event_generators(m); + pyarb::register_morphology(m); pyarb::register_cable_probes(m, global_ptr); + pyarb::register_mechanisms(m); pyarb::register_cells(m); + + pyarb::register_cable_loader(m); pyarb::register_config(m); pyarb::register_contexts(m); + pyarb::register_recipe(m); pyarb::register_domain_decomposition(m); - pyarb::register_event_generators(m); - pyarb::register_identifiers(m); - pyarb::register_mechanisms(m); - pyarb::register_morphology(m); pyarb::register_profiler(m); - pyarb::register_recipe(m); - pyarb::register_schedules(m); pyarb::register_simulation(m, global_ptr); - pyarb::register_single_cell(m); pyarb::register_arborenv(m); + pyarb::register_single_cell(m); // This is the fallback. All specific translators take precedence by being // registered *later*. @@ -87,8 +93,8 @@ PYBIND11_MODULE(_arbor, m) { }); // Translate Arbor errors -> Python exceptions. - pybind11::register_exception(m, "FileNotFoundError", PyExc_FileNotFoundError); - pybind11::register_exception(m, "ValueError", PyExc_ValueError); + pybind11::register_exception(m, "ArbFileNotFoundError", PyExc_FileNotFoundError); + pybind11::register_exception(m, "ArbValueError", PyExc_ValueError); #ifdef ARB_MPI_ENABLED diff --git a/python/recipe.cpp b/python/recipe.cpp index bd1e01cff2..667be8dec3 100644 --- a/python/recipe.cpp +++ b/python/recipe.cpp @@ -21,6 +21,8 @@ namespace pyarb { +namespace U = arb::units; + // Convert a cell description inside a Python object to a cell description in a // unique_any, as required by the recipe interface. // This helper is only to be called while holding the GIL. We require this guard @@ -137,7 +139,7 @@ void register_recipe(pybind11::module& m) { "Describes a connection between two cells:\n" " Defined by source and destination end points (that is pre-synaptic and post-synaptic respectively), a connection weight and a delay time."); cell_connection - .def(pybind11::init(), + .def(pybind11::init(), "source"_a, "dest"_a, "weight"_a, "delay"_a, "Construct a connection with arguments:\n" " source: The source end point of the connection.\n" @@ -213,10 +215,5 @@ void register_recipe(pybind11::module& m) { .def("__str__", [](const ::pyarb::recipe&){return "";}) .def("__repr__", [](const ::pyarb::recipe&){return "";}); - // Probes - pybind11::class_ probe(m, "probe"); - probe - .def("__repr__", [](const arb::probe_info& p){return util::pprintf("", p.tag);}) - .def("__str__", [](const arb::probe_info& p){return util::pprintf("", p.tag);}); } } // namespace pyarb diff --git a/python/schedule.cpp b/python/schedule.cpp index 70950a4a60..e1061d3631 100644 --- a/python/schedule.cpp +++ b/python/schedule.cpp @@ -13,57 +13,63 @@ namespace py = pybind11; namespace pyarb { std::ostream& operator<<(std::ostream& o, const regular_schedule_shim& x) { - return o << ""; + if (x.tstop.has_value()) { + return o << ""; + } + else { + return o << ""; + } } std::ostream& operator<<(std::ostream& o, const explicit_schedule_shim& e) { - o << ""; -}; + return o << ""; +} std::ostream& operator<<(std::ostream& o, const poisson_schedule_shim& p) { - return o << ""; -}; +} static std::vector as_vector(std::pair ts) { return std::vector(ts.first, ts.second); } -// -// regular_schedule shim -// - -regular_schedule_shim::regular_schedule_shim(arb::time_type t0, arb::time_type delta_t, py::object t1) { +regular_schedule_shim::regular_schedule_shim(const arb::units::quantity& t0, + const arb::units::quantity& delta_t, + std::optional t1) { set_tstart(t0); set_dt(delta_t); set_tstop(t1); } -regular_schedule_shim::regular_schedule_shim(arb::time_type delta_t) { - set_tstart(0.); +regular_schedule_shim::regular_schedule_shim(const arb::units::quantity& delta_t) { + set_tstart(0.*arb::units::ms); set_dt(delta_t); } -void regular_schedule_shim::set_tstart(arb::time_type t) { - pyarb::assert_throw(is_nonneg()(t), "tstart must be a non-negative number"); +void regular_schedule_shim::set_tstart(const arb::units::quantity& t) { + pyarb::assert_throw(is_nonneg()(t.value()), "tstart must be a non-negative number"); + pyarb::assert_throw(arb::units::is_valid(t.convert_to(arb::units::ms)), "must be convertible to time"); tstart = t; -}; +} -void regular_schedule_shim::set_tstop(py::object t) { - tstop = py2optional( - t, "tstop must be a non-negative number, or None", is_nonneg()); -}; +void regular_schedule_shim::set_tstop(std::optional t) { + if (t.has_value()) { + pyarb::assert_throw(arb::units::is_valid(t.value().convert_to(arb::units::ms)), "must be convertible to time"); + } + tstop = t; +} -void regular_schedule_shim::set_dt(arb::time_type delta_t) { - pyarb::assert_throw(is_positive()(delta_t), "dt must be a positive number"); - dt = delta_t; -}; +void regular_schedule_shim::set_dt(const arb::units::quantity& t) { + pyarb::assert_throw(is_positive()(t.value()), "dt must be a positive number"); + pyarb::assert_throw(arb::units::is_valid(t.convert_to(arb::units::ms)), "must be convertible to time"); + dt = t; +} regular_schedule_shim::time_type regular_schedule_shim::get_tstart() const { return tstart; @@ -78,10 +84,9 @@ regular_schedule_shim::opt_time_type regular_schedule_shim::get_tstop() const { } arb::schedule regular_schedule_shim::schedule() const { - return arb::regular_schedule( - tstart, - dt, - tstop.value_or(arb::terminal_time)); + return arb::regular_schedule(tstart, + dt, + tstop.value_or(arb::terminal_time*arb::units::ms)); } std::vector regular_schedule_shim::events(arb::time_type t0, arb::time_type t1) { @@ -93,17 +98,15 @@ std::vector regular_schedule_shim::events(arb::time_type t0, arb return as_vector(sched.events(t0, t1)); } -// -// explicit_schedule shim -// - -//struct explicit_schedule_shim { -explicit_schedule_shim::explicit_schedule_shim(std::vector t) { - set_times(t); +explicit_schedule_shim::explicit_schedule_shim(const std::vector& seq) { + std::vector ts; + ts.reserve(seq.size()); + for (const auto t: seq) ts.push_back(t.value_as(arb::units::ms)); + set_times_ms(std::move(ts)); } // getter and setter (in order to assert when being set) -void explicit_schedule_shim::set_times(std::vector t) { +void explicit_schedule_shim::set_times_ms(std::vector t) { times = std::move(t); // Sort the times in ascending order if necessary @@ -118,12 +121,12 @@ void explicit_schedule_shim::set_times(std::vector t) { } }; -std::vector explicit_schedule_shim::get_times() const { +std::vector explicit_schedule_shim::get_times_ms() const { return times; } arb::schedule explicit_schedule_shim::schedule() const { - return arb::explicit_schedule(times); + return arb::explicit_schedule_from_milliseconds(times); } std::vector explicit_schedule_shim::events(arb::time_type t0, arb::time_type t1) { @@ -135,71 +138,50 @@ std::vector explicit_schedule_shim::events(arb::time_type t0, ar return as_vector(sched.events(t0, t1)); } -// -// poisson_schedule shim -// - -poisson_schedule_shim::poisson_schedule_shim( - arb::time_type ts, - arb::time_type f, - rng_type::result_type s, - py::object tstop) -{ +poisson_schedule_shim::poisson_schedule_shim(const arb::units::quantity& ts, + const arb::units::quantity& f, + arb::seed_type s, + const arb::units::quantity& tstop) { set_tstart(ts); set_freq(f); seed = s; set_tstop(tstop); } -poisson_schedule_shim::poisson_schedule_shim(arb::time_type f) { - set_tstart(0.); - set_freq(f); - seed = 0; -} - -void poisson_schedule_shim::set_tstart(arb::time_type t) { - pyarb::assert_throw(is_nonneg()(t), "tstart must be a non-negative number"); +void poisson_schedule_shim::set_tstart(const arb::units::quantity& t) { + pyarb::assert_throw(is_nonneg()(t.value()), "tstart must be a non-negative number"); tstart = t; }; -void poisson_schedule_shim::set_freq(arb::time_type f) { - pyarb::assert_throw(is_nonneg()(f), "frequency must be a non-negative number"); +void poisson_schedule_shim::set_freq(const arb::units::quantity& f) { + pyarb::assert_throw(is_nonneg()(f.value()), "frequency must be a non-negative number"); freq = f; }; -void poisson_schedule_shim::set_tstop(py::object t) { - tstop = py2optional( - t, "tstop must be a non-negative number, or None", is_nonneg()); +void poisson_schedule_shim::set_tstop(const arb::units::quantity& t) { + pyarb::assert_throw(is_nonneg()(t.value()), "frequency must be a non-negative number"); + tstop = t; }; -arb::time_type poisson_schedule_shim::get_tstart() const { - return tstart; -} - -arb::time_type poisson_schedule_shim::get_freq() const { - return freq; -} - -poisson_schedule_shim::opt_time_type poisson_schedule_shim::get_tstop() const { - return tstop; -} - arb::schedule poisson_schedule_shim::schedule() const { - return arb::poisson_schedule(tstart, freq, rng_type(seed), tstop.value_or(arb::terminal_time)); + return arb::poisson_schedule(tstart, freq, seed, tstop); } -std::vector poisson_schedule_shim::events(arb::time_type t0, arb::time_type t1) { - pyarb::assert_throw(is_nonneg()(t0), "t0 must be a non-negative number"); - pyarb::assert_throw(is_nonneg()(t1), "t1 must be a non-negative number"); +std::vector poisson_schedule_shim::events(const arb::units::quantity& t0, + const arb::units::quantity& t1) { + auto beg = t0.value_as(arb::units::ms); + auto end = t1.value_as(arb::units::ms); + pyarb::assert_throw(is_nonneg()(beg), "t0 must be a non-negative number"); + pyarb::assert_throw(is_nonneg()(end), "t1 must be a non-negative number"); arb::schedule sched = poisson_schedule_shim::schedule(); - return as_vector(sched.events(t0, t1)); + return as_vector(sched.events(beg, end)); } void register_schedules(py::module& m) { using namespace py::literals; - using time_type = arb::time_type; + using time_type = arb::units::quantity; py::class_ schedule_base(m, "schedule_base", "Schedule abstract base class."); @@ -208,13 +190,13 @@ void register_schedules(py::module& m) { "Describes a regular schedule with multiples of dt within the interval [tstart, tstop)."); regular_schedule - .def(py::init(), + .def(py::init>(), "tstart"_a, "dt"_a, "tstop"_a = py::none(), "Construct a regular schedule with arguments:\n" " tstart: The delivery time of the first event in the sequence [ms].\n" " dt: The interval between time points [ms].\n" " tstop: No events delivered after this time [ms], None by default.") - .def(py::init(), + .def(py::init(), "dt"_a, "Construct a regular schedule, starting from t = 0 and never terminating, with arguments:\n" " dt: The interval between time points [ms].\n") @@ -240,10 +222,10 @@ void register_schedules(py::module& m) { "times"_a, "Construct an explicit schedule with argument:\n" " times: A list of times [ms], [] by default.") - .def_property("times", &explicit_schedule_shim::get_times, &explicit_schedule_shim::set_times, + .def_property("times_ms", &explicit_schedule_shim::get_times_ms, &explicit_schedule_shim::set_times_ms, "A list of times [ms].") .def("events", &explicit_schedule_shim::events, - "A view of monotonically increasing time values in the half-open interval [t0, t1).") + "A view of monotonically increasing time values in the half-open interval [t0, t1) in [ms].") .def("__str__", util::to_string) .def("__repr__", util::to_string); @@ -252,17 +234,19 @@ void register_schedules(py::module& m) { "Describes a schedule according to a Poisson process within the interval [tstart, tstop)."); poisson_schedule - .def(py::init(), - "tstart"_a = 0., "freq"_a, "seed"_a = 0, "tstop"_a = py::none(), - "Construct a Poisson schedule with arguments:\n" - " tstart: The delivery time of the first event in the sequence [ms], 0 by default.\n" - " freq: The expected frequency [kHz].\n" - " seed: The seed for the random number generator, 0 by default.\n" - " tstop: No events delivered after this time [ms], None by default.") - .def(py::init(), - "freq"_a, - "Construct a Poisson schedule, starting from t = 0, default seed, with:\n" - " freq: The expected frequency [kHz], 10 by default.\n") + .def(py::init<>( + [](const time_type& f, + const time_type& t0, + arb::seed_type s, + std::optional t1) -> poisson_schedule_shim { + return poisson_schedule_shim{t0, f, s, t1.value_or(arb::terminal_time*arb::units::ms)}; + }), + "freq"_a, py::kw_only(), py::arg_v("tstart", 0.*arb::units::ms, "0.0*arbor.units.ms"), "seed"_a = 0, "tstop"_a=py::none(), + "Construct a Poisson schedule with arguments:\n" + " tstart: The delivery time of the first event in the sequence [ms], 0 by default.\n" + " freq: The expected frequency [kHz].\n" + " seed: The seed for the random number generator, 0 by default.\n" + " tstop: No events delivered after this time [ms], None by default.") .def_property("tstart", &poisson_schedule_shim::get_tstart, &poisson_schedule_shim::set_tstart, "The delivery time of the first event in the sequence [ms].") .def_property("freq", &poisson_schedule_shim::get_freq, &poisson_schedule_shim::set_freq, diff --git a/python/schedule.hpp b/python/schedule.hpp index 619f77236a..e321cb4d23 100644 --- a/python/schedule.hpp +++ b/python/schedule.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -16,8 +15,6 @@ namespace pyarb { // to an arb::schedule object. struct schedule_shim_base { schedule_shim_base() = default; - schedule_shim_base(const schedule_shim_base&) = delete; - schedule_shim_base& operator=(schedule_shim_base&) = delete; virtual ~schedule_shim_base() {} virtual arb::schedule schedule() const = 0; @@ -28,20 +25,23 @@ struct schedule_shim_base { // a regular_schedule in python are manipulating this type. This is converted to // an arb::regular_schedule when a C++ recipe is created from a Python recipe. struct regular_schedule_shim: schedule_shim_base { - using time_type = arb::time_type; + using time_type = arb::units::quantity; using opt_time_type = std::optional; - time_type tstart = {}; - time_type dt = 0; - opt_time_type tstop = {}; + time_type tstart; + time_type dt = 0*arb::units::ms; + opt_time_type tstop; - regular_schedule_shim(time_type t0, time_type delta_t, pybind11::object t1); - explicit regular_schedule_shim(time_type delta_t); + regular_schedule_shim(const time_type& t0, + const time_type& delta_t, + opt_time_type t1); + + explicit regular_schedule_shim(const time_type& delta_t); // getter and setter (in order to assert when being set) - void set_tstart(time_type t); - void set_dt(time_type delta_t); - void set_tstop(pybind11::object t); + void set_tstart(const time_type& t); + void set_dt(const time_type& delta_t); + void set_tstop(opt_time_type t); time_type get_tstart() const; time_type get_dt() const; @@ -60,11 +60,11 @@ struct explicit_schedule_shim: schedule_shim_base { std::vector times; explicit_schedule_shim() = default; - explicit_schedule_shim(std::vector t); + explicit_schedule_shim(const std::vector& t); // getter and setter (in order to assert when being set) - void set_times(std::vector t); - std::vector get_times() const; + void set_times_ms(std::vector t); + std::vector get_times_ms() const; arb::schedule schedule() const override; @@ -76,28 +76,35 @@ struct explicit_schedule_shim: schedule_shim_base { // Python are manipulating this type. This is converted to an // arb::poisson_schedule when a C++ recipe is created from a Python recipe. struct poisson_schedule_shim: schedule_shim_base { - using rng_type = std::mt19937_64; - using opt_time_type = std::optional; + arb::units::quantity tstart; // ms + arb::units::quantity freq; // kHz + arb::units::quantity tstop; // ms + arb::seed_type seed = arb::default_seed; - arb::time_type tstart; // ms - arb::time_type freq; // kHz - opt_time_type tstop; // ms - rng_type::result_type seed; + poisson_schedule_shim(const poisson_schedule_shim&) = default; + poisson_schedule_shim(poisson_schedule_shim&&) = default; - poisson_schedule_shim(arb::time_type ts, arb::time_type f, rng_type::result_type s, pybind11::object tstop); - poisson_schedule_shim(arb::time_type f); + poisson_schedule_shim() = default; + ~poisson_schedule_shim() = default; - void set_tstart(arb::time_type t); - void set_freq(arb::time_type f); - void set_tstop(pybind11::object t); + poisson_schedule_shim(const arb::units::quantity& ts, + const arb::units::quantity& f, + arb::seed_type s, + const arb::units::quantity& tstop); - arb::time_type get_tstart() const; - arb::time_type get_freq() const; - opt_time_type get_tstop() const; + void set_tstart(const arb::units::quantity& t); + void set_freq(const arb::units::quantity& f); + void set_tstop(const arb::units::quantity& f); - arb::schedule schedule() const override; + const auto& get_tstop() const { return tstop; } + const auto& get_tstart() const { return tstart; } + const auto& get_freq() const { return freq; } + + // TODO(TH) this should be symmetrical... + std::vector events(const arb::units::quantity& t0, const arb::units::quantity& t1); + + arb::schedule schedule() const; - std::vector events(arb::time_type t0, arb::time_type t1); }; } diff --git a/python/simulation.cpp b/python/simulation.cpp index 71abd3b0be..d7ea8a9ad2 100644 --- a/python/simulation.cpp +++ b/python/simulation.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include @@ -116,7 +118,7 @@ class simulation_shim { } } - arb::time_type run(arb::time_type tfinal, arb::time_type dt) { + arb::time_type run(const arb::units::quantity& tfinal, const arb::units::quantity& dt) { return sim_->run(tfinal, dt); } @@ -201,8 +203,8 @@ class simulation_shim { } }; -void register_simulation(pybind11::module& m, pyarb_global_ptr global_ptr) { - using namespace pybind11::literals; +void register_simulation(py::module& m, pyarb_global_ptr global_ptr) { + using namespace py::literals; py::enum_(m, "spike_recording") .value("off", spike_recording::off) @@ -216,57 +218,58 @@ void register_simulation(pybind11::module& m, pyarb_global_ptr global_ptr) { simulation // A custom constructor that wraps a python recipe with arb::recipe_shim // before forwarding it to the arb::recipe constructor. - .def(pybind11::init( + .def(py::init( [global_ptr](std::shared_ptr& rec, - const std::shared_ptr& ctx_, - const std::optional& decomp, + std::optional> ctx_, + std::optional decomp, std::uint64_t seed) { - try { - auto ctx = ctx_ ? ctx_ : std::make_shared(make_context_shim()); - auto dec = decomp.value_or(arb::partition_load_balance(recipe_shim(rec), ctx->context)); - return new simulation_shim(rec, *ctx, dec, seed, global_ptr); - } - catch (...) { - py_reset_and_throw(); - throw; - } - }), - // Release the python gil, so that callbacks into the python recipe don't deadlock. - pybind11::call_guard(), - "Initialize the model described by a recipe, with cells and network distributed\n" - "according to the domain decomposition and computational resources described by a context.", + try { + auto ctx = ctx_.value_or(std::make_shared(make_context_shim())); + auto dec = decomp.value_or(arb::partition_load_balance(recipe_shim(rec), ctx->context)); + return new simulation_shim(rec, *ctx, dec, seed, global_ptr); + } + catch (...) { + py_reset_and_throw(); + throw; + } + }), + // Release the python gil, so that callbacks into the python recipe don't deadlock. + py::call_guard(), "recipe"_a, - pybind11::arg_v("context", pybind11::none(), "Execution context"), - pybind11::arg_v("domains", pybind11::none(), "Domain decomposition"), - pybind11::arg_v("seed", 0u, "Random number generator seed")) + "context"_a=py::none(), + "domains"_a=py::none(), + "seed"_a=0u, + "Initialize the model described by a recipe, with cells and network distributed\n" + "according to the domain decomposition and computational resources described by a\n" + "context. Initialize PRNG using seed") .def("set_remote_spike_filter", - &simulation_shim::set_remote_spike_filter, - "pred"_a, - "Add a callback to filter spikes going out over external connections. `pred` is" - "a callable on the `spike` type. **Caution**: This will be extremely slow; use C++ " - "if you want to make use of this.") + &simulation_shim::set_remote_spike_filter, + "pred"_a, + "Add a callback to filter spikes going out over external connections. `pred` is" + "a callable on the `spike` type. **Caution**: This will be extremely slow; use C++ " + "if you want to make use of this.") .def("update", &simulation_shim::update, - pybind11::call_guard(), + py::call_guard(), "Rebuild the connection table from recipe::connections_on and the event" "generators based on recipe::event_generators.", "recipe"_a) .def("deserialize", &simulation_shim::deserialize, - pybind11::call_guard(), + py::call_guard(), "Deserialize the simulation object from a JSON string." "json"_a) .def("serialize", &simulation_shim::serialize, - pybind11::call_guard(), + py::call_guard(), "Serialize the simulation object to a JSON string.") .def("reset", &simulation_shim::reset, - pybind11::call_guard(), + py::call_guard(), "Reset the state of the simulation to its initial state.") .def("clear_samplers", &simulation_shim::clear_samplers, - pybind11::call_guard(), + py::call_guard(), "Clearing spike and sample information. restoring memory") .def("run", &simulation_shim::run, - pybind11::call_guard(), + py::call_guard(), "Run the simulation from current simulation time to tfinal [ms], with maximum time step size dt [ms].", - "tfinal"_a, "dt"_a=0.025) + "tfinal"_a, py::arg_v("dt", 0.025*arb::units::ms, "0.025*arbor.units.ms")) .def("record", &simulation_shim::record, "Disable or enable local or global spike recording.") .def("spikes", &simulation_shim::spikes, diff --git a/python/single_cell_model.cpp b/python/single_cell_model.cpp index 020a637373..6bd0f7b001 100644 --- a/python/single_cell_model.cpp +++ b/python/single_cell_model.cpp @@ -18,7 +18,7 @@ #include "event_generator.hpp" #include "error.hpp" #include "strprintf.hpp" -#include "proxy.hpp" +#include "label_dict.hpp" using arb::util::any_cast; @@ -34,7 +34,7 @@ namespace pyarb { // Stores the location and sampling frequency for a probe in a single cell model. struct probe_site { arb::locset locset; // Location of sample on morphology. - double frequency; // Sampling frequency [kHz]. + arb::units::quantity frequency; // Sampling frequency [kHz]. arb::cell_tag_type tag; // Tag = unique name }; @@ -161,12 +161,15 @@ class single_cell_model { // m.probe('voltage', '(location 2 0.5)') // m.probe('voltage', 'term') - void probe(const std::string& what, const arb::locset& where, const arb::cell_tag_type& tag, double frequency) { + void probe(const std::string& what, + const arb::locset& where, + const arb::cell_tag_type& tag, + const arb::units::quantity& frequency) { if (what != "voltage") { throw pyarb_error( util::pprintf("{} does not name a valid variable to trace (currently only 'voltage' is supported)", what)); } - if (frequency<=0) { + if (frequency.value() <= 0) { throw pyarb_error( util::pprintf("sampling frequency is not greater than zero", what)); } @@ -177,7 +180,7 @@ class single_cell_model { event_generators_.push_back(event_generator); } - void run(double tfinal, double dt) { + void run(const arb::units::quantity& tfinal, const arb::units::quantity& dt) { single_cell_recipe rec(cell_, probes_, gprop, event_generators_); auto domdec = arb::partition_load_balance(rec, ctx_); @@ -238,24 +241,24 @@ void register_single_cell(pybind11::module& m) { const label_dict_proxy& l) -> single_cell_model { return single_cell_model(arb::cable_cell({m}, d, l.dict)); }), - "tree"_a, "decor"_a, "labels"_a=arb::decor{}, + "tree"_a, "decor"_a, "labels"_a=label_dict_proxy{}, "Build single cell model from cable cell components") .def(pybind11::init([](const arb::morphology& m, const arb::decor& d, const label_dict_proxy& l) -> single_cell_model { return single_cell_model(arb::cable_cell(m, d, l.dict)); }), - "morph"_a, "decor"_a, "labels"_a=arb::decor{}, + "morph"_a, "decor"_a, "labels"_a=label_dict_proxy{}, "Build single cell model from cable cell components") .def(pybind11::init(), "cell"_a, "Initialise a single cell model for a cable cell.") .def("run", &single_cell_model::run, "tfinal"_a, - "dt"_a = 0.025, + pybind11::arg_v("dt", 0.025*arb::units::ms, "0.025*arbor.units.ms"), "Run model from t=0 to t=tfinal ms.") .def("probe", - [](single_cell_model& m, const char* what, const char* where, const char* tag, double frequency) { + [](single_cell_model& m, const char* what, const char* where, const char* tag, const arb::units::quantity& frequency) { m.probe(what, arborio::parse_locset_expression(where).unwrap(), tag, frequency);}, "what"_a, "where"_a, "tag"_a, "frequency"_a, "Sample a variable on the cell.\n" @@ -264,7 +267,7 @@ void register_single_cell(pybind11::module& m) { " tag: Unique name for this probe.\n" " frequency: The target frequency at which to sample [kHz].") .def("probe", - [](single_cell_model& m, const char* what, const arb::mlocation& where, const char* tag, double frequency) { + [](single_cell_model& m, const char* what, const arb::mlocation& where, const char* tag, const arb::units::quantity& frequency) { m.probe(what, where, tag, frequency);}, "what"_a, "where"_a, "tag"_a, "frequency"_a, "Sample a variable on the cell.\n" diff --git a/python/stubs/arbor/__init__.pyi b/python/stubs/arbor/__init__.pyi new file mode 100644 index 0000000000..12a28c9754 --- /dev/null +++ b/python/stubs/arbor/__init__.pyi @@ -0,0 +1,287 @@ +from __future__ import annotations +from arbor._arbor import ArbFileNotFoundError +from arbor._arbor import ArbValueError +from arbor._arbor import MechCatItemIterator +from arbor._arbor import MechCatKeyIterator +from arbor._arbor import MechCatValueIterator +from arbor._arbor import allen_catalogue +from arbor._arbor import asc_morphology +from arbor._arbor import axial_resistivity +from arbor._arbor import backend +from arbor._arbor import bbp_catalogue +from arbor._arbor import benchmark_cell +from arbor._arbor import cable +from arbor._arbor import cable_cell +from arbor._arbor import cable_component +from arbor._arbor import cable_global_properties +from arbor._arbor import cable_probe_axial_current +from arbor._arbor import cable_probe_density_state +from arbor._arbor import cable_probe_density_state_cell +from arbor._arbor import cable_probe_ion_current_cell +from arbor._arbor import cable_probe_ion_current_density +from arbor._arbor import cable_probe_ion_diff_concentration +from arbor._arbor import cable_probe_ion_diff_concentration_cell +from arbor._arbor import cable_probe_ion_ext_concentration +from arbor._arbor import cable_probe_ion_ext_concentration_cell +from arbor._arbor import cable_probe_ion_int_concentration +from arbor._arbor import cable_probe_ion_int_concentration_cell +from arbor._arbor import cable_probe_membrane_voltage +from arbor._arbor import cable_probe_membrane_voltage_cell +from arbor._arbor import cable_probe_point_info +from arbor._arbor import cable_probe_point_state +from arbor._arbor import cable_probe_point_state_cell +from arbor._arbor import cable_probe_stimulus_current_cell +from arbor._arbor import cable_probe_total_current_cell +from arbor._arbor import cable_probe_total_ion_current_cell +from arbor._arbor import cable_probe_total_ion_current_density +from arbor._arbor import catalogue +from arbor._arbor import cell_address +from arbor._arbor import cell_cv_data +from arbor._arbor import cell_global_label +from arbor._arbor import cell_kind +from arbor._arbor import cell_local_label +from arbor._arbor import cell_member +from arbor._arbor import component_meta_data +from arbor._arbor import config +from arbor._arbor import connection +from arbor._arbor import context +from arbor._arbor import cv_data +from arbor._arbor import cv_policy +from arbor._arbor import cv_policy_every_segment +from arbor._arbor import cv_policy_explicit +from arbor._arbor import cv_policy_fixed_per_branch +from arbor._arbor import cv_policy_max_extent +from arbor._arbor import cv_policy_single +from arbor._arbor import decor +from arbor._arbor import default_catalogue +from arbor._arbor import density +from arbor._arbor import domain_decomposition +from arbor._arbor import env +from arbor._arbor import event_generator +from arbor._arbor import explicit_schedule +from arbor._arbor import ext_concentration +from arbor._arbor import extent +from arbor._arbor import gap_junction_connection +from arbor._arbor import group_description +from arbor._arbor import iclamp +from arbor._arbor import int_concentration +from arbor._arbor import intersect_region +from arbor._arbor import ion_data +from arbor._arbor import ion_dependency +from arbor._arbor import ion_diffusivity +from arbor._arbor import ion_settings +from arbor._arbor import isometry +from arbor._arbor import junction +from arbor._arbor import label_dict +from arbor._arbor import lif_cell +from arbor._arbor import lif_probe_metadata +from arbor._arbor import lif_probe_voltage +from arbor._arbor import load_asc +from arbor._arbor import load_catalogue +from arbor._arbor import load_component +from arbor._arbor import load_swc_arbor +from arbor._arbor import load_swc_neuron +from arbor._arbor import location +from arbor._arbor import mechanism +from arbor._arbor import mechanism_field +from arbor._arbor import mechanism_info +from arbor._arbor import membrane_capacitance +from arbor._arbor import membrane_potential +from arbor._arbor import meter_manager +from arbor._arbor import meter_report +from arbor._arbor import morphology +from arbor._arbor import morphology_provider +from arbor._arbor import mpoint +from arbor._arbor import msegment +from arbor._arbor import neuroml +from arbor._arbor import neuroml_morph_data +from arbor._arbor import neuron_cable_properties +from arbor._arbor import partition_by_group +from arbor._arbor import partition_hint +from arbor._arbor import partition_load_balance +from arbor._arbor import place_pwlin +from arbor._arbor import poisson_schedule +from arbor._arbor import print_config +from arbor._arbor import probe +from arbor._arbor import proc_allocation +from arbor._arbor import recipe +from arbor._arbor import regular_schedule +from arbor._arbor import reversal_potential +from arbor._arbor import reversal_potential_method +from arbor._arbor import scaled_mechanism +from arbor._arbor import schedule_base +from arbor._arbor import segment_tree +from arbor._arbor import selection_policy +from arbor._arbor import simulation +from arbor._arbor import single_cell_model +from arbor._arbor import spike +from arbor._arbor import spike_recording +from arbor._arbor import spike_source_cell +from arbor._arbor import stochastic_catalogue +from arbor._arbor import synapse +from arbor._arbor import temperature +from arbor._arbor import threshold_detector +from arbor._arbor import trace +from arbor._arbor import units +from arbor._arbor import voltage_process +from arbor._arbor import write_component +from . import _arbor + +__all__ = [ + "ArbFileNotFoundError", + "ArbValueError", + "MechCatItemIterator", + "MechCatKeyIterator", + "MechCatValueIterator", + "allen_catalogue", + "asc_morphology", + "axial_resistivity", + "backend", + "bbp_catalogue", + "benchmark_cell", + "build_catalogue", + "cable", + "cable_cell", + "cable_component", + "cable_global_properties", + "cable_probe_axial_current", + "cable_probe_density_state", + "cable_probe_density_state_cell", + "cable_probe_ion_current_cell", + "cable_probe_ion_current_density", + "cable_probe_ion_diff_concentration", + "cable_probe_ion_diff_concentration_cell", + "cable_probe_ion_ext_concentration", + "cable_probe_ion_ext_concentration_cell", + "cable_probe_ion_int_concentration", + "cable_probe_ion_int_concentration_cell", + "cable_probe_membrane_voltage", + "cable_probe_membrane_voltage_cell", + "cable_probe_point_info", + "cable_probe_point_state", + "cable_probe_point_state_cell", + "cable_probe_stimulus_current_cell", + "cable_probe_total_current_cell", + "cable_probe_total_ion_current_cell", + "cable_probe_total_ion_current_density", + "catalogue", + "cell_address", + "cell_cv_data", + "cell_global_label", + "cell_kind", + "cell_local_label", + "cell_member", + "component_meta_data", + "config", + "connection", + "context", + "cv_data", + "cv_policy", + "cv_policy_every_segment", + "cv_policy_explicit", + "cv_policy_fixed_per_branch", + "cv_policy_max_extent", + "cv_policy_single", + "decor", + "default_catalogue", + "density", + "domain_decomposition", + "env", + "event_generator", + "explicit_schedule", + "ext_concentration", + "extent", + "gap_junction_connection", + "group_description", + "iclamp", + "int_concentration", + "intersect_region", + "ion_data", + "ion_dependency", + "ion_diffusivity", + "ion_settings", + "isometry", + "junction", + "label_dict", + "lif_cell", + "lif_probe_metadata", + "lif_probe_voltage", + "load_asc", + "load_catalogue", + "load_component", + "load_swc_arbor", + "load_swc_neuron", + "location", + "mechanism", + "mechanism_field", + "mechanism_info", + "membrane_capacitance", + "membrane_potential", + "meter_manager", + "meter_report", + "mnpos", + "modcc", + "morphology", + "morphology_provider", + "mpoint", + "msegment", + "neuroml", + "neuroml_morph_data", + "neuron_cable_properties", + "partition_by_group", + "partition_hint", + "partition_load_balance", + "place_pwlin", + "poisson_schedule", + "print_config", + "probe", + "proc_allocation", + "recipe", + "regular_schedule", + "reversal_potential", + "reversal_potential_method", + "scaled_mechanism", + "schedule_base", + "segment_tree", + "selection_policy", + "simulation", + "single_cell_model", + "spike", + "spike_recording", + "spike_source_cell", + "stochastic_catalogue", + "synapse", + "temperature", + "threshold_detector", + "trace", + "units", + "voltage_process", + "write_component", +] + +def build_catalogue(): ... +def modcc(): ... + +__config__: dict = { + "mpi": False, + "mpi4py": False, + "gpu": None, + "vectorize": True, + "profiling": False, + "neuroml": True, + "bundled": True, + "version": "0.9.1-dev", + "source": "2023-12-08T14:40:50+01:00 327c56d229571dac097e7400a9b5e04fc8d7a514 modified", + "build_config": "DEBUG", + "arch": "native", + "prefix": "/usr/local", + "python_lib_path": "/opt/homebrew/lib/python3.11/site-packages", + "binary_path": "bin", + "lib_path": "lib", + "data_path": "share", + "CXX": "/opt/homebrew/bin/clang++", + "pybind-version": "2.11.1", + "timestamp": "Jan 2 2024 09:57:33", +} +__version__: str = "0.9.1-dev" +mnpos: int = 4294967295 diff --git a/python/stubs/arbor/_arbor/__init__.pyi b/python/stubs/arbor/_arbor/__init__.pyi new file mode 100644 index 0000000000..aabd4f3de2 --- /dev/null +++ b/python/stubs/arbor/_arbor/__init__.pyi @@ -0,0 +1,2991 @@ +""" +arbor: multicompartment neural network models. +""" +from __future__ import annotations +import typing +from . import env +from . import units + +__all__ = [ + "ArbFileNotFoundError", + "ArbValueError", + "MechCatItemIterator", + "MechCatKeyIterator", + "MechCatValueIterator", + "allen_catalogue", + "asc_morphology", + "axial_resistivity", + "backend", + "bbp_catalogue", + "benchmark_cell", + "cable", + "cable_cell", + "cable_component", + "cable_global_properties", + "cable_probe_axial_current", + "cable_probe_density_state", + "cable_probe_density_state_cell", + "cable_probe_ion_current_cell", + "cable_probe_ion_current_density", + "cable_probe_ion_diff_concentration", + "cable_probe_ion_diff_concentration_cell", + "cable_probe_ion_ext_concentration", + "cable_probe_ion_ext_concentration_cell", + "cable_probe_ion_int_concentration", + "cable_probe_ion_int_concentration_cell", + "cable_probe_membrane_voltage", + "cable_probe_membrane_voltage_cell", + "cable_probe_point_info", + "cable_probe_point_state", + "cable_probe_point_state_cell", + "cable_probe_stimulus_current_cell", + "cable_probe_total_current_cell", + "cable_probe_total_ion_current_cell", + "cable_probe_total_ion_current_density", + "catalogue", + "cell_address", + "cell_cv_data", + "cell_global_label", + "cell_kind", + "cell_local_label", + "cell_member", + "component_meta_data", + "config", + "connection", + "context", + "cv_data", + "cv_policy", + "cv_policy_every_segment", + "cv_policy_explicit", + "cv_policy_fixed_per_branch", + "cv_policy_max_extent", + "cv_policy_single", + "decor", + "default_catalogue", + "density", + "domain_decomposition", + "env", + "event_generator", + "explicit_schedule", + "ext_concentration", + "extent", + "gap_junction_connection", + "group_description", + "iclamp", + "int_concentration", + "intersect_region", + "ion_data", + "ion_dependency", + "ion_diffusivity", + "ion_settings", + "isometry", + "junction", + "label_dict", + "lif_cell", + "lif_probe_metadata", + "lif_probe_voltage", + "load_asc", + "load_catalogue", + "load_component", + "load_swc_arbor", + "load_swc_neuron", + "location", + "mechanism", + "mechanism_field", + "mechanism_info", + "membrane_capacitance", + "membrane_potential", + "meter_manager", + "meter_report", + "mnpos", + "morphology", + "morphology_provider", + "mpoint", + "msegment", + "neuroml", + "neuroml_morph_data", + "neuron_cable_properties", + "partition_by_group", + "partition_hint", + "partition_load_balance", + "place_pwlin", + "poisson_schedule", + "print_config", + "probe", + "proc_allocation", + "recipe", + "regular_schedule", + "reversal_potential", + "reversal_potential_method", + "scaled_mechanism", + "schedule_base", + "segment_tree", + "selection_policy", + "simulation", + "single_cell_model", + "spike", + "spike_recording", + "spike_source_cell", + "stochastic_catalogue", + "synapse", + "temperature", + "threshold_detector", + "trace", + "units", + "voltage_process", + "write_component", +] + +class ArbFileNotFoundError(FileNotFoundError): + pass + +class ArbValueError(ValueError): + pass + +class MechCatItemIterator: + def __iter__(self) -> MechCatItemIterator: ... + def __next__(self) -> tuple[str, mechanism_info]: ... + +class MechCatKeyIterator: + def __iter__(self) -> MechCatKeyIterator: ... + def __next__(self) -> str: ... + +class MechCatValueIterator: + def __iter__(self) -> MechCatValueIterator: ... + def __next__(self) -> mechanism_info: ... + +class asc_morphology: + """ + The morphology and label dictionary meta-data loaded from a Neurolucida ASCII (.asc) file. + """ + + @property + def labels(self) -> label_dict: + """ + The four canonical regions are labeled 'soma', 'axon', 'dend' and 'apic'. + """ + @property + def morphology(self) -> morphology: + """ + The cable cell morphology. + """ + @property + def segment_tree(self) -> segment_tree: + """ + The raw segment tree. + """ + +class axial_resistivity: + """ + Setting the axial resistivity. + """ + + def __init__(self, arg0: units.quantity) -> None: ... + def __repr__(self) -> str: ... + +class backend: + """ + Enumeration used to indicate which hardware backend to execute a cell group on. + + Members: + + gpu : Use GPU backend. + + multicore : Use multicore backend. + """ + + __members__: typing.ClassVar[ + dict[str, backend] + ] # value = {'gpu': , 'multicore': } + gpu: typing.ClassVar[backend] # value = + multicore: typing.ClassVar[backend] # value = + def __eq__(self, other: typing.Any) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __init__(self, value: int) -> None: ... + def __int__(self) -> int: ... + def __ne__(self, other: typing.Any) -> bool: ... + def __repr__(self) -> str: ... + def __setstate__(self, state: int) -> None: ... + def __str__(self) -> str: ... + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... + +class benchmark_cell: + """ + A benchmarking cell, used by Arbor developers to test communication performance. + A benchmark cell generates spikes at a user-defined sequence of time points, and + the time taken to integrate a cell can be tuned by setting the realtime_ratio, + for example if realtime_ratio=2, a cell will take 2 seconds of CPU time to + simulate 1 second. + """ + + @typing.overload + def __init__( + self, + source_label: str, + target_label: str, + schedule: regular_schedule, + realtime_ratio: float = 1.0, + ) -> None: + """ + Construct a benchmark cell that generates spikes on 'source_label' at regular intervals. + The cell has one source labeled 'source_label', and one target labeled 'target_label'. + """ + @typing.overload + def __init__( + self, + source_label: str, + target_label: str, + schedule: explicit_schedule, + realtime_ratio: float = 1.0, + ) -> None: + """ + Construct a benchmark cell that generates spikes on 'source_label' at a sequence of user-defined times. + The cell has one source labeled 'source_label', and one target labeled 'target_label'. + """ + @typing.overload + def __init__( + self, + source_label: str, + target_label: str, + schedule: poisson_schedule, + realtime_ratio: float = 1.0, + ) -> None: + """ + Construct a benchmark cell that generates spikeson 'source_label' at times defined by a Poisson sequence. + The cell has one source labeled 'source_label', and one target labeled 'target_label'. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +class cable: + __hash__: typing.ClassVar[None] = None + def __eq__(self, arg0: cable) -> bool: ... + def __init__(self, branch: int, prox: float, dist: float) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def branch(self) -> int: + """ + The id of the branch on which the cable lies. + """ + @property + def dist(self) -> float: + """ + The relative position of the distal end of the cable on its branch ∈ [0,1]. + """ + @property + def prox(self) -> float: + """ + The relative position of the proximal end of the cable on its branch ∈ [0,1]. + """ + +class cable_cell: + """ + Represents morphologically-detailed cell models, with morphology represented as a + tree of one-dimensional cable segments. + """ + + @typing.overload + def __init__( + self, morphology: morphology, decor: decor, labels: label_dict | None = None + ) -> None: + """ + Construct with a morphology, decor, and label dictionary. + """ + @typing.overload + def __init__( + self, segment_tree: segment_tree, decor: decor, labels: label_dict | None = None + ) -> None: + """ + Construct with a morphology derived from a segment tree, decor, and label dictionary. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def cables(self, label: str) -> list[cable]: + """ + The cable segments of the cell morphology for a region label. + """ + def locations(self, label: str) -> list[location]: + """ + The locations of the cell morphology for a locset label. + """ + @property + def num_branches(self) -> int: + """ + The number of unbranched cable sections in the morphology. + """ + +class cable_component: + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def component(self) -> morphology | label_dict | decor | cable_cell: + """ + cable-cell component. + """ + @property + def meta_data(self) -> component_meta_data: + """ + cable-cell component meta-data. + """ + @meta_data.setter + def meta_data(self, arg0: component_meta_data) -> None: ... + +class cable_global_properties: + membrane_voltage_limit: float | None + @typing.overload + def __init__(self) -> None: ... + @typing.overload + def __init__(self, arg0: cable_global_properties) -> None: ... + def __str__(self) -> str: ... + def check(self) -> None: + """ + Test whether all default parameters and ion species properties have been set. + """ + def set_ion( + self, + ion: str, + valence: int | None = None, + int_con: units.quantity | None = None, + ext_con: units.quantity | None = None, + rev_pot: units.quantity | None = None, + method: typing.Any = None, + diff: units.quantity | None = None, + ) -> None: + """ + Set the global default properties of ion species named 'ion'. + * valence: valence of the ion species [e]. + * int_con: initial internal concentration [mM]. + * ext_con: initial external concentration [mM]. + * rev_pot: reversal potential [mV]. + * method: mechanism for calculating reversal potential. + * diff: diffusivity [m^2/s]. + There are 3 ion species predefined in arbor: 'ca', 'na' and 'k'. + If 'ion' in not one of these ions it will be added to the list, making it + available to mechanisms. The user has to provide the valence of a previously + undefined ion the first time this function is called with it as an argument. + Species concentrations and reversal potential can be overridden on + specific regions using the paint interface, while the method for calculating + reversal potential is global for all compartments in the cell, and can't be + overriden locally. + """ + def set_property( + self, + Vm: units.quantity | None = None, + cm: units.quantity | None = None, + rL: units.quantity | None = None, + tempK: units.quantity | None = None, + ) -> None: + """ + Set global default values for cable and cell properties. + * Vm: initial membrane voltage [mV]. + * cm: membrane capacitance [F/m²]. + * rL: axial resistivity [Ω·cm]. + * tempK: temperature [Kelvin]. + These values can be overridden on specific regions using the paint interface. + """ + def unset_ion(self, arg0: str) -> None: + """ + Remove ion species from properties. + """ + @property + def axial_resistivity(self) -> float | None: ... + @axial_resistivity.setter + def axial_resistivity(self, arg1: float) -> None: ... + @property + def catalogue(self) -> catalogue: + """ + The mechanism catalogue. + """ + @catalogue.setter + def catalogue(self, arg0: catalogue) -> None: ... + @property + def coalesce_synapses(self) -> bool: + """ + Flag for enabling/disabling linear syanpse coalescing. + """ + @coalesce_synapses.setter + def coalesce_synapses(self, arg0: bool) -> None: ... + @property + def ion_data(self) -> dict[str, ion_data]: ... + @property + def ion_reversal_potential(self) -> dict[str, mechanism]: ... + @property + def ion_valence(self) -> dict[str, int]: ... + @property + def ions(self) -> dict[str, ion_settings]: + """ + Return a view of all ion settings. + """ + @property + def membrane_capacitance(self) -> float | None: ... + @membrane_capacitance.setter + def membrane_capacitance(self, arg1: float) -> None: ... + @property + def membrane_potential(self) -> float | None: ... + @membrane_potential.setter + def membrane_potential(self, arg1: float) -> None: ... + @property + def temperature(self) -> float | None: ... + @temperature.setter + def temperature(self, arg1: float) -> None: ... + +class cable_probe_point_info: + """ + Probe metadata associated with a cable cell probe for point process state. + """ + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def location(self) -> location: + """ + Location of point process instance on cell. + """ + @location.setter + def location(self, arg0: location) -> None: ... + @property + def multiplicity(self) -> int: + """ + Number of coalesced point processes (linear synapses) associated with this instance. + """ + @multiplicity.setter + def multiplicity(self, arg0: int) -> None: ... + @property + def target(self) -> int: + """ + The target index of the point process instance on the cell. + """ + @target.setter + def target(self, arg0: int) -> None: ... + +class catalogue: + def __contains__(self, name: str) -> bool: + """ + Is 'name' in the catalogue? + """ + def __getitem__(self, arg0: str) -> mechanism_info: ... + @typing.overload + def __init__(self) -> None: ... + @typing.overload + def __init__(self, arg0: catalogue) -> None: ... + def __iter__(self) -> MechCatKeyIterator: + """ + Return an iterator over all mechanism names in this catalogues. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def derive( + self, + name: str, + parent: str, + globals: dict[str, float] = {}, + ions: dict[str, str] = {}, + ) -> None: ... + def extend(self, other: catalogue, prefix: str) -> None: + """ + Import another catalogue, possibly with a prefix. Will overwrite in case of name collisions. + """ + def is_derived(self, name: str) -> bool: + """ + Is 'name' a derived mechanism or can it be implicitly derived? + """ + def items(self) -> MechCatItemIterator: + """ + Return an iterator over all (name, mechanism) tuples in this catalogues. + """ + def keys(self) -> MechCatKeyIterator: + """ + Return an iterator over all mechanism names in this catalogues. + """ + def values(self) -> MechCatValueIterator: + """ + Return an iterator over all mechanism info values in this catalogues. + """ + +class cell_address: + gid: int + tag: str + +class cell_cv_data: + """ + Provides information on the CVs representing the discretization of a cable-cell. + """ + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def cables(self, index: int) -> list[cable]: + """ + Return a list of cables representing the CV at the given index. + """ + def children(self, index: int) -> list[int]: + """ + Return a list of indices of the CVs representing the children of the CV at the given index. + """ + def parent(self, index: int) -> int: + """ + Return the index of the CV representing the parent of the CV at the given index. + """ + @property + def num_cv(self) -> int: + """ + Return the number of CVs in the cell. + """ + +class cell_global_label: + """ + For global identification of an item. + + cell_global_label members: + (1) a unique cell identified by its gid. + (2) a cell_local_label, referring to a labeled group of items on the cell and a policy for selecting a single item out of the group. + """ + + @typing.overload + def __init__(self, gid: int, label: str) -> None: + """ + Construct a cell_global_label identifier from a gid and a label argument identifying an item on the cell. + The default round_robin policy is used for selecting one of possibly multiple items on the cell associated with the label. + """ + @typing.overload + def __init__(self, gid: int, label: cell_local_label) -> None: + """ + Construct a cell_global_label identifier with arguments: + gid: The global identifier of the cell. + label: The cell_local_label representing the label and selection policy of an item on the cell. + """ + @typing.overload + def __init__(self, arg0: tuple) -> None: + """ + Construct a cell_global_label identifier with tuple argument (gid, label): + gid: The global identifier of the cell. + label: The cell_local_label representing the label and selection policy of an item on the cell. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def gid(self) -> int: + """ + The global identifier of the cell. + """ + @gid.setter + def gid(self, arg0: int) -> None: ... + @property + def label(self) -> cell_local_label: + """ + The cell_local_label representing the label and selection policy of an item on the cell. + """ + @label.setter + def label(self, arg0: cell_local_label) -> None: ... + +class cell_kind: + """ + Enumeration used to identify the cell kind, used by the model to group equal kinds in the same cell group. + + Members: + + benchmark : Proxy cell used for benchmarking. + + cable : A cell with morphology described by branching 1D cable segments. + + lif : Leaky-integrate and fire neuron. + + spike_source : Proxy cell that generates spikes from a spike sequence provided by the user. + """ + + __members__: typing.ClassVar[ + dict[str, cell_kind] + ] # value = {'benchmark': , 'cable': , 'lif': , 'spike_source': } + benchmark: typing.ClassVar[cell_kind] # value = + cable: typing.ClassVar[cell_kind] # value = + lif: typing.ClassVar[cell_kind] # value = + spike_source: typing.ClassVar[cell_kind] # value = + def __eq__(self, other: typing.Any) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __init__(self, value: int) -> None: ... + def __int__(self) -> int: ... + def __ne__(self, other: typing.Any) -> bool: ... + def __repr__(self) -> str: ... + def __setstate__(self, state: int) -> None: ... + def __str__(self) -> str: ... + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... + +class cell_local_label: + """ + For local identification of an item. + + cell_local_label identifies: + (1) a labeled group of one or more items on one or more locations on the cell. + (2) a policy for selecting one of the items. + """ + + @typing.overload + def __init__(self, label: str) -> None: + """ + Construct a cell_local_label identifier from a label argument identifying a group of one or more items on a cell. + The default round_robin policy is used for selecting one of possibly multiple items associated with the label. + """ + @typing.overload + def __init__(self, label: str, policy: selection_policy) -> None: + """ + Construct a cell_local_label identifier with arguments: + label: The identifier of a group of one or more items on a cell. + policy: The policy for selecting one of possibly multiple items associated with the label. + """ + @typing.overload + def __init__(self, arg0: tuple) -> None: + """ + Construct a cell_local_label identifier with tuple argument (label, policy): + label: The identifier of a group of one or more items on a cell. + policy: The policy for selecting one of possibly multiple items associated with the label. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def label(self) -> str: + """ + The identifier of a a group of one or more items on a cell. + """ + @label.setter + def label(self, arg0: str) -> None: ... + @property + def policy(self) -> selection_policy: + """ + The policy for selecting one of possibly multiple items associated with the label. + """ + @policy.setter + def policy(self, arg0: selection_policy) -> None: ... + +class cell_member: + """ + For global identification of a cell-local item. + + Items of cell_member must: + (1) be associated with a unique cell, identified by the member gid; + (2) identify an item within a cell-local collection by the member index. + """ + + @typing.overload + def __init__(self, gid: int, index: int) -> None: + """ + Construct a cell member identifier with arguments: + gid: The global identifier of the cell. + index: The cell-local index of the item. + """ + @typing.overload + def __init__(self, arg0: tuple) -> None: + """ + Construct a cell member identifier with tuple argument (gid, index): + gid: The global identifier of the cell. + index: The cell-local index of the item. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def gid(self) -> int: + """ + The global identifier of the cell. + """ + @gid.setter + def gid(self, arg0: int) -> None: ... + @property + def index(self) -> int: + """ + Cell-local index of the item. + """ + @index.setter + def index(self, arg0: int) -> None: ... + +class component_meta_data: + @property + def version(self) -> str: + """ + cable-cell component version. + """ + @version.setter + def version(self, arg0: str) -> None: ... + +class connection: + """ + Describes a connection between two cells: + Defined by source and destination end points (that is pre-synaptic and post-synaptic respectively), a connection weight and a delay time. + """ + + def __init__( + self, + source: cell_global_label, + dest: cell_local_label, + weight: float, + delay: units.quantity, + ) -> None: + """ + Construct a connection with arguments: + source: The source end point of the connection. + dest: The destination end point of the connection. + weight: The weight delivered to the target synapse (unit defined by the type of synapse target). + delay: The delay of the connection [ms]. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def delay(self) -> float: + """ + The delay time of the connection [ms]. + """ + @delay.setter + def delay(self, arg0: float) -> None: ... + @property + def dest(self) -> cell_local_label: + """ + The destination label of the connection. + """ + @dest.setter + def dest(self, arg0: cell_local_label) -> None: ... + @property + def source(self) -> cell_global_label: + """ + The source gid and label of the connection. + """ + @source.setter + def source(self, arg0: cell_global_label) -> None: ... + @property + def weight(self) -> float: + """ + The weight of the connection. + """ + @weight.setter + def weight(self, arg0: float) -> None: ... + +class context: + """ + An opaque handle for the hardware resources used in a simulation. + """ + + @typing.overload + def __init__(self) -> None: + """ + Construct a local context with proc_allocation = env.default_allocation(). + """ + @typing.overload + def __init__( + self, + *, + threads: int = 1, + gpu_id: typing.Any = None, + mpi: typing.Any = None, + inter: typing.Any = None, + bind_procs: bool = False, + bind_threads: bool = False, + ) -> None: + """ + Construct a context with arguments: + threads: The number of threads available locally for execution. Must be set to 1 at minimum. 1 by default. + gpu_id: The identifier of the GPU to use, None by default. Only available if arbor.__config__['gpu']!="none". + mpi: The MPI communicator, None by default. Only available if arbor.__config__['mpi']==True. + inter: An MPI intercommunicator used to connect to external simulations, None by default. Only available if arbor.__config__['mpi']==True. + bind_procs: Create process binding mask. + bind_threads: Create thread binding mask. + """ + @typing.overload + def __init__( + self, + alloc: proc_allocation, + *, + mpi: typing.Any = None, + inter: typing.Any = None, + ) -> None: + """ + Construct a context with arguments: + alloc: The computational resources to be used for the simulation. + mpi: The MPI communicator, None by default. Only available if arbor.__config__['mpi']==True. + inter: An MPI intercommunicator used to connect to external simulations, None by default. Only available if arbor.__config__['mpi']==True. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def has_gpu(self) -> bool: + """ + Whether the context has a GPU. + """ + @property + def has_mpi(self) -> bool: + """ + Whether the context uses MPI for distributed communication. + """ + @property + def rank(self) -> int: + """ + The numeric id of the local domain (equivalent to MPI rank). + """ + @property + def ranks(self) -> int: + """ + The number of distributed domains (equivalent to the number of MPI ranks). + """ + @property + def threads(self) -> int: + """ + The number of threads in the context's thread pool. + """ + +class cv_policy: + """ + Describes the rules used to discretize (compartmentalise) a cable cell morphology. + """ + + def __add__(self, arg0: cv_policy) -> cv_policy: ... + def __init__(self, expression: str) -> None: + """ + A valid CV policy expression + """ + def __or__(self, arg0: cv_policy) -> cv_policy: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def domain(self) -> str: + """ + The domain on which the policy is applied. + """ + +class decor: + """ + Description of the decorations to be applied to a cable cell, that is the painted, + placed and defaulted properties, mecahanisms, ion species etc. + """ + + @typing.overload + def __init__(self) -> None: ... + @typing.overload + def __init__(self, arg0: decor) -> None: ... + def defaults( + self, + ) -> list[ + membrane_potential + | axial_resistivity + | temperature + | membrane_capacitance + | ion_diffusivity + | int_concentration + | ext_concentration + | reversal_potential + | reversal_potential_method + | cv_policy + ]: + """ + Return a view of all defaults. + """ + @typing.overload + def discretization(self, policy: cv_policy) -> decor: + """ + A cv_policy used to discretise the cell into compartments for simulation + """ + @typing.overload + def discretization(self, policy: str) -> decor: + """ + An s-expression string representing a cv_policy used to discretise the cell into compartments for simulation + """ + @typing.overload + def paint(self, region: str, mechanism: density) -> decor: + """ + Associate a density mechanism with a region. + """ + @typing.overload + def paint(self, region: str, mechanism: voltage_process) -> decor: + """ + Associate a voltage process mechanism with a region. + """ + @typing.overload + def paint(self, region: str, mechanism: scaled_mechanism) -> None: + """ + Associate a scaled density mechanism with a region. + """ + @typing.overload + def paint( + self, + region: str, + Vm: units.quantity | str | None = None, + cm: units.quantity | str | None = None, + rL: units.quantity | str | None = None, + tempK: units.quantity | str | None = None, + ) -> decor: + """ + Set cable properties on a region. + Set global default values for cable and cell properties. + * Vm: initial membrane voltage [mV]. + * cm: membrane capacitance [F/m²]. + * rL: axial resistivity [Ω·cm]. + * tempK: temperature [Kelvin]. + """ + @typing.overload + def paint( + self, + region: str, + *, + ion: str, + int_con: units.quantity | None = None, + ext_con: units.quantity | None = None, + rev_pot: units.quantity | None = None, + diff: units.quantity | None = None, + ) -> decor: + """ + Set ion species properties conditions on a region. + * int_con: initial internal concentration [mM]. + * ext_con: initial external concentration [mM]. + * rev_pot: reversal potential [mV]. + * method: mechanism for calculating reversal potential. + * diff: diffusivity [m^2/s]. + """ + def paintings( + self, + ) -> list[ + tuple[ + str, + membrane_potential + | axial_resistivity + | temperature + | membrane_capacitance + | ion_diffusivity + | int_concentration + | ext_concentration + | reversal_potential + | density + | voltage_process + | scaled_mechanism, + ] + ]: + """ + Return a view of all painted items. + """ + @typing.overload + def place(self, locations: str, synapse: synapse, label: str) -> decor: + """ + Place one instance of 'synapse' on each location in 'locations'.The group of synapses has the label 'label', used for forming connections between cells. + """ + @typing.overload + def place(self, locations: str, junction: junction, label: str) -> decor: + """ + Place one instance of 'junction' on each location in 'locations'.The group of junctions has the label 'label', used for forming gap-junction connections between cells. + """ + @typing.overload + def place(self, locations: str, iclamp: iclamp, label: str) -> decor: + """ + Add a current stimulus at each location in locations.The group of current stimuli has the label 'label'. + """ + @typing.overload + def place(self, locations: str, detector: threshold_detector, label: str) -> decor: + """ + Add a voltage spike detector at each location in locations.The group of spike detectors has the label 'label', used for forming connections between cells. + """ + def placements( + self, + ) -> list[tuple[str, iclamp | threshold_detector | synapse | junction, str]]: + """ + Return a view of all placed items. + """ + def set_ion( + self, + ion: str, + int_con: units.quantity | None = None, + ext_con: units.quantity | None = None, + rev_pot: units.quantity | None = None, + method: typing.Any = None, + diff: units.quantity | None = None, + ) -> decor: + """ + Set the cell-level properties of ion species named 'ion'. + * int_con: initial internal concentration [mM]. + * ext_con: initial external concentration [mM]. + * rev_pot: reversal potential [mV]. + * method: mechanism for calculating reversal potential. + * diff: diffusivity [m^2/s]. + There are 3 ion species predefined in arbor: 'ca', 'na' and 'k'. + If 'ion' in not one of these ions it will be added to the list, making it + available to mechanisms. The user has to provide the valence of a previously + undefined ion the first time this function is called with it as an argument. + Species concentrations and reversal potential can be overridden on + specific regions using the paint interface, while the method for calculating + reversal potential is global for all compartments in the cell, and can't be + overriden locally. + """ + def set_property( + self, + Vm: units.quantity | None = None, + cm: units.quantity | None = None, + rL: units.quantity | None = None, + tempK: units.quantity | None = None, + ) -> decor: + """ + Set default values for cable and cell properties: + * Vm: initial membrane voltage [mV]. + * cm: membrane capacitance [F/m²]. + * rL: axial resistivity [Ω·cm]. + * tempK: temperature [Kelvin]. + These values can be overridden on specific regions using the paint interface. + """ + +class density: + """ + For painting a density mechanism on a region. + """ + + @typing.overload + def __init__(self, arg0: str) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism) -> None: ... + @typing.overload + def __init__(self, arg0: str, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: str, **kwargs) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, **kwargs) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def mech(self) -> mechanism: + """ + The underlying mechanism. + """ + +class domain_decomposition: + """ + The domain decomposition is responsible for describing the distribution of cells across cell groups and domains. + """ + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def gid_domain(self, gid: int) -> int: + """ + Query the domain id that a cell assigned to (using global identifier gid). + """ + @property + def domain_id(self) -> int: + """ + The index of the local domain. + Always 0 for non-distributed models, and corresponds to the MPI rank for distributed runs. + """ + @property + def groups(self) -> list[group_description]: + """ + Descriptions of the cell groups on the local domain. + """ + @property + def num_domains(self) -> int: + """ + Number of domains that the model is distributed over. + """ + @property + def num_global_cells(self) -> int: + """ + Total number of cells in the global model (sum of num_local_cells over all domains). + """ + @property + def num_groups(self) -> int: + """ + Total number of cell groups in the local domain. + """ + @property + def num_local_cells(self) -> int: + """ + Total number of cells in the local domain. + """ + +class event_generator: + def __init__( + self, target: cell_local_label, weight: float, sched: schedule_base + ) -> None: + """ + Construct an event generator with arguments: + target: The target synapse label and selection policy. + weight: The weight of events to deliver. + sched: A schedule of the events. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def target(self) -> cell_local_label: + """ + The target synapse (gid, local_id). + """ + @target.setter + def target(self, arg0: cell_local_label) -> None: ... + @property + def weight(self) -> float: + """ + The weight of events to deliver. + """ + @weight.setter + def weight(self, arg0: float) -> None: ... + +class explicit_schedule(schedule_base): + """ + Describes an explicit schedule at a predetermined (sorted) sequence of times. + """ + + @typing.overload + def __init__(self) -> None: + """ + Construct an empty explicit schedule. + """ + @typing.overload + def __init__(self, times: list[units.quantity]) -> None: + """ + Construct an explicit schedule with argument: + times: A list of times [ms], [] by default. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def events(self, arg0: float, arg1: float) -> list[float]: + """ + A view of monotonically increasing time values in the half-open interval [t0, t1) in [ms]. + """ + @property + def times_ms(self) -> list[float]: + """ + A list of times [ms]. + """ + @times_ms.setter + def times_ms(self, arg1: list[float]) -> None: ... + +class ext_concentration: + """ + Setting the initial external ion concentration. + """ + + def __init__(self, arg0: str, arg1: units.quantity) -> None: ... + def __repr__(self) -> str: ... + +class extent: + """ + A potentially empty region on a morphology. + """ + +class gap_junction_connection: + """ + Describes a gap junction between two gap junction sites. + """ + + def __init__( + self, peer: cell_global_label, local: cell_local_label, weight: float + ) -> None: + """ + Construct a gap junction connection with arguments: + peer: remote half of the gap junction connection. + local: local half of the gap junction connection. + weight: Gap junction connection weight [unit-less]. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def local(self) -> cell_local_label: + """ + Local label of the gap junction connection. + """ + @local.setter + def local(self, arg0: cell_local_label) -> None: ... + @property + def peer(self) -> cell_global_label: + """ + Remote gid and label of the gap junction connection. + """ + @peer.setter + def peer(self, arg0: cell_global_label) -> None: ... + @property + def weight(self) -> float: + """ + Gap junction connection weight [unit-less]. + """ + @weight.setter + def weight(self, arg0: float) -> None: ... + +class group_description: + """ + The indexes of a set of cells of the same kind that are grouped together in a cell group. + """ + + def __init__(self, kind: cell_kind, gids: list[int], backend: backend) -> None: + """ + Construct a group description with cell kind, list of gids, and backend kind. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def backend(self) -> backend: + """ + The hardware backend on which the cell group will run. + """ + @property + def gids(self) -> list[int]: + """ + The list of gids of the cells in the group. + """ + @property + def kind(self) -> cell_kind: + """ + The type of cell in the cell group. + """ + +class iclamp: + """ + A current clamp for injecting a DC or fixed frequency current governed by a piecewise linear envelope. + """ + + @typing.overload + def __init__( + self, + tstart: units.quantity, + duration: units.quantity, + current: units.quantity, + *, + frequency: units.quantity = ..., + phase: units.quantity = ..., + ) -> None: + """ + Construct finite duration current clamp, constant amplitude + """ + @typing.overload + def __init__( + self, + current: units.quantity, + *, + frequency: units.quantity = ..., + phase: units.quantity = ..., + ) -> None: + """ + Construct constant amplitude current clamp + """ + @typing.overload + def __init__( + self, + envelope: list[tuple[units.quantity, units.quantity]], + *, + frequency: units.quantity = ..., + phase: units.quantity = ..., + ) -> None: + """ + Construct current clamp according to (time, amplitude) linear envelope + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def envelope(self) -> list[tuple[float, float]]: + """ + List of (time [ms], amplitude [nA]) points comprising the piecewise linear envelope + """ + @property + def frequency(self) -> float: + """ + Oscillation frequency (kHz), zero implies DC stimulus. + """ + @property + def phase(self) -> float: + """ + Oscillation initial phase (rad) + """ + +class int_concentration: + """ + Setting the initial internal ion concentration. + """ + + def __init__(self, arg0: str, arg1: units.quantity) -> None: ... + def __repr__(self) -> str: ... + +class ion_data: + @property + def charge(self) -> int: + """ + Valence. + """ + @property + def diffusivity(self) -> float | None: + """ + Diffusivity. + """ + @property + def external_concentration(self) -> float | None: + """ + External concentration. + """ + @property + def internal_concentration(self) -> float | None: + """ + Internal concentration. + """ + @property + def reversal_concentration(self) -> float | None: + """ + Reversal potential. + """ + @property + def reversal_potential(self) -> float | None: + """ + Reversal potential. + """ + @property + def reversal_potential_method(self) -> str: + """ + Reversal potential method. + """ + +class ion_dependency: + """ + Information about a mechanism's dependence on an ion species. + """ + + def __init__(self, arg0: ion_dependency) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def read_rev_pot(self) -> bool: ... + @property + def write_ext_con(self) -> bool: ... + @property + def write_int_con(self) -> bool: ... + @property + def write_rev_pot(self) -> bool: ... + +class ion_diffusivity: + """ + Setting the ion diffusivity. + """ + + def __init__(self, arg0: str, arg1: units.quantity) -> None: ... + def __repr__(self) -> str: ... + +class ion_settings: + pass + +class isometry: + @staticmethod + @typing.overload + def rotate(theta: float, x: float, y: float, z: float) -> isometry: + """ + Construct a rotation isometry of angle theta about the axis in direction (x, y, z). + """ + @staticmethod + @typing.overload + def rotate(theta: float, axis: tuple) -> isometry: + """ + Construct a rotation isometry of angle theta about the given axis in the direction described by a tuple. + """ + @staticmethod + @typing.overload + def translate(x: float, y: float, z: float) -> isometry: + """ + Construct a translation isometry from displacements x, y, and z. + """ + @staticmethod + @typing.overload + def translate(arg0: tuple) -> isometry: + """ + Construct a translation isometry from the first three components of a tuple. + """ + @staticmethod + @typing.overload + def translate(arg0: mpoint) -> isometry: + """ + Construct a translation isometry from the x, y, and z components of an mpoint. + """ + @typing.overload + def __call__(self, arg0: mpoint) -> mpoint: + """ + Apply isometry to mpoint argument. + """ + @typing.overload + def __call__(self, arg0: tuple) -> tuple: + """ + Apply isometry to first three components of tuple argument. + """ + def __init__(self) -> None: + """ + Construct a trivial isometry. + """ + def __mul__(self, arg0: isometry) -> isometry: ... + +class junction: + """ + For placing a gap-junction mechanism on a locset. + """ + + @typing.overload + def __init__(self, arg0: str) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism) -> None: ... + @typing.overload + def __init__(self, arg0: str, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: str, **kwargs) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, **kwargs) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def mech(self) -> mechanism: + """ + The underlying mechanism. + """ + +class label_dict: + """ + A dictionary of labelled region and locset definitions, with a + unique label assigned to each definition. + """ + + @staticmethod + def append(*args, **kwargs) -> None: + """ + Import the entries of a another label dictionary with an optional prefix. + """ + def __contains__(self, arg0: str) -> bool: ... + def __getitem__(self, arg0: str) -> str: ... + @typing.overload + def __init__(self) -> None: + """ + Create an empty label dictionary. + """ + @typing.overload + def __init__(self, arg0: dict[str, str]) -> None: + """ + Initialize a label dictionary from a dictionary with string labels as keys, and corresponding definitions as strings. + """ + @typing.overload + def __init__(self, arg0: label_dict) -> None: + """ + Initialize a label dictionary from another one + """ + @typing.overload + def __init__(self, arg0: typing.Iterator) -> None: + """ + Initialize a label dictionary from an iterable of key, definition pairs + """ + def __iter__(self) -> typing.Iterator: ... + def __len__(self) -> int: ... + def __repr__(self) -> str: ... + def __setitem__(self, arg0: str, arg1: str) -> None: ... + def __str__(self) -> str: ... + def add_swc_tags(self) -> label_dict: + """ + Add standard SWC tagged regions. + - soma: (tag 1) + - axon: (tag 2) + - dend: (tag 3) + - apic: (tag 4) + """ + def items(self) -> typing.Iterator: ... + def keys(self) -> typing.Iterator: ... + def update(self, other: label_dict) -> None: + """ + The label_dict to be importedImport the entries of a another label dictionary. + """ + def values(self) -> typing.Iterator: ... + @property + def locsets(self) -> list[str]: + """ + The locset definitions. + """ + @property + def regions(self) -> list[str]: + """ + The region definitions. + """ + +class lif_cell: + """ + A leaky integrate-and-fire cell. + """ + + def __init__( + self, + source_label: str, + target_label: str, + *, + tau_m: units.quantity | None = None, + V_th: units.quantity | None = None, + C_m: units.quantity | None = None, + E_L: units.quantity | None = None, + V_m: units.quantity | None = None, + t_ref: units.quantity | None = None, + ) -> None: + """ + Construct a lif cell with one source labeled 'source_label', and one target labeled 'target_label'.Can optionally take physical parameters: + * tau_m: Membrane potential decaying constant [ms]. + * V_th: Firing threshold [mV]. + * C_m: Membrane capacitance [pF]. + * E_L: Resting potential [mV]. + * V_m: Initial value of the Membrane potential [mV]. + * t_ref: Refractory period [ms]. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def C_m(self) -> units.quantity: + """ + Membrane capacitance [pF]. + """ + @C_m.setter + def C_m(self, arg0: units.quantity) -> None: ... + @property + def E_L(self) -> units.quantity: + """ + Resting potential [mV]. + """ + @E_L.setter + def E_L(self, arg0: units.quantity) -> None: ... + @property + def E_R(self) -> units.quantity: + """ + Reset potential [mV]. + """ + @E_R.setter + def E_R(self, arg0: units.quantity) -> None: ... + @property + def V_m(self) -> units.quantity: + """ + Initial value of the Membrane potential [mV]. + """ + @V_m.setter + def V_m(self, arg0: units.quantity) -> None: ... + @property + def V_th(self) -> units.quantity: + """ + Firing threshold [mV]. + """ + @V_th.setter + def V_th(self, arg0: units.quantity) -> None: ... + @property + def source(self) -> str: + """ + Label of the single build-in source on the cell. + """ + @source.setter + def source(self, arg0: str) -> None: ... + @property + def t_ref(self) -> units.quantity: + """ + Refractory period [ms]. + """ + @t_ref.setter + def t_ref(self, arg0: units.quantity) -> None: ... + @property + def target(self) -> str: + """ + Label of the single build-in target on the cell. + """ + @target.setter + def target(self, arg0: str) -> None: ... + @property + def tau_m(self) -> units.quantity: + """ + Membrane potential decaying constant [ms]. + """ + @tau_m.setter + def tau_m(self, arg0: units.quantity) -> None: ... + +class lif_probe_metadata: + """ + Probe metadata associated with a LIF cell probe. + """ + +class location: + """ + A location on a cable cell. + """ + + __hash__: typing.ClassVar[None] = None + def __eq__(self, arg0: location) -> bool: ... + def __init__(self, branch: int, pos: float) -> None: + """ + Construct a location specification holding: + branch: The id of the branch. + pos: The relative position (from 0., proximal, to 1., distal) on the branch. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def branch(self) -> int: + """ + The id of the branch. + """ + @property + def pos(self) -> float: + """ + The relative position on the branch (∈ [0.,1.], where 0. means proximal and 1. distal). + """ + +class mechanism: + @typing.overload + def __init__(self, name: str) -> None: + """ + The name of the mechanism + """ + @typing.overload + def __init__(self, name: str, params: dict[str, float]) -> None: + """ + Example usage setting parameters: + m = arbor.mechanism('expsyn', {'tau': 1.4}) + will create parameters for the 'expsyn' mechanism, with the provided value + for 'tau' overrides the default. If a parameter is not set, the default + (as defined in NMODL) is used. + + Example overriding a global parameter: + m = arbor.mechanism('nernst/R=8.3145,F=96485') + """ + @typing.overload + def __init__(self, name: str, **kwargs) -> None: + """ + Example usage setting parameters: + m = arbor.mechanism('expsyn', tau=1.4}) + will create parameters for the 'expsyn' mechanism, with the provided value + for 'tau' overrides the default. If a parameter is not set, the default + (as defined in NMODL) is used. + + Example overriding a global parameter: + m = arbor.mechanism('nernst/R=8.3145,F=96485') + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def set(self, name: str, value: float) -> None: + """ + Set parameter value. + """ + @property + def name(self) -> str: + """ + The name of the mechanism. + """ + @property + def values(self) -> dict[str, float]: + """ + A dictionary of parameter values with parameter name as key. + """ + +class mechanism_field: + """ + Basic information about a mechanism field. + """ + + def __init__(self, arg0: mechanism_field) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def default(self) -> float: ... + @property + def max(self) -> float: ... + @property + def min(self) -> float: ... + @property + def units(self) -> str: ... + +class mechanism_info: + """ + Meta data about a mechanism's fields and ion dependendencies. + """ + + def __init__(self, arg0: mechanism_info) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def globals(self) -> dict[str, mechanism_field]: + """ + Global fields have one value common to an instance of a mechanism, are constant in time and set at instantiation. + """ + @property + def ions(self) -> dict[str, ion_dependency]: + """ + Ion dependencies. + """ + @property + def kind(self) -> str: + """ + String representation of the kind of the mechanism. + """ + @property + def linear(self) -> bool: + """ + True if a synapse mechanism has linear current contributions so that multiple instances on the same compartment can be coalesced. + """ + @property + def parameters(self) -> dict[str, mechanism_field]: + """ + Parameter fields may vary across the extent of a mechanism, but are constant in time and set at instantiation. + """ + @property + def post_events(self) -> bool: + """ + True if a synapse mechanism has a `POST_EVENT` procedure defined. + """ + @property + def state(self) -> dict[str, mechanism_field]: + """ + State fields vary in time and across the extent of a mechanism, and potentially can be sampled at run-time. + """ + +class membrane_capacitance: + """ + Setting the membrane capacitance. + """ + + def __init__(self, arg0: units.quantity) -> None: ... + def __repr__(self) -> str: ... + +class membrane_potential: + """ + Setting the initial membrane voltage. + """ + + def __init__(self, arg0: units.quantity, arg1: str | None) -> None: ... + def __repr__(self) -> str: ... + +class meter_manager: + """ + Manage metering by setting checkpoints and starting the timing region. + """ + + def __init__(self) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def checkpoint(self, name: str, context: context) -> None: + """ + Create a new checkpoint. Records the time since the last checkpoint(or the call to start if no previous checkpoints exist),and restarts the timer for the next checkpoint. + """ + def start(self, context: context) -> None: + """ + Start the metering. Records a time stamp, that marks the start of the first checkpoint timing region. + """ + @property + def checkpoint_names(self) -> list[str]: + """ + A list of all metering checkpoint names. + """ + @property + def times(self) -> list[float]: + """ + A list of all metering times. + """ + +class meter_report: + """ + Summarises the performance meter results, used to print a report to screen or file. + If a distributed context is used, the report will contain a summary of results from all MPI ranks. + """ + + def __init__(self, manager: meter_manager, context: context) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +class morphology: + """ + A cell morphology. + """ + + def __init__(self, arg0: segment_tree) -> None: ... + def __str__(self) -> str: ... + def branch_children(self, i: int) -> list[int]: + """ + The child branches of branch i. + """ + def branch_parent(self, i: int) -> int: + """ + The parent branch of branch i. + """ + def branch_segments(self, i: int) -> list[msegment]: + """ + A list of the segments in branch i, ordered from proximal to distal ends of the branch. + """ + def to_segment_tree(self) -> segment_tree: + """ + Convert this morphology to a segment_tree. + """ + @property + def empty(self) -> bool: + """ + Whether the morphology is empty. + """ + @property + def num_branches(self) -> int: + """ + The number of branches in the morphology. + """ + +class morphology_provider: + def __init__(self, morphology: morphology) -> None: + """ + Construct a morphology provider. + """ + def reify_locset(self, arg0: str) -> list[location]: + """ + Turn a locset into a list of locations. + """ + def reify_region(self, arg0: str) -> extent: + """ + Turn a region into an extent. + """ + +class mpoint: + __hash__: typing.ClassVar[None] = None + def __eq__(self, arg0: mpoint) -> bool: ... + @typing.overload + def __init__(self, x: float, y: float, z: float, radius: float) -> None: + """ + Create an mpoint object from parameters x, y, z, and radius, specified in µm. + """ + @typing.overload + def __init__(self, arg0: tuple) -> None: + """ + Create an mpoint object from a tuple (x, y, z, radius), specified in µm. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def radius(self) -> float: + """ + Radius of cable at sample location centred at coordinates [μm]. + """ + @property + def x(self) -> float: + """ + X coordinate [μm]. + """ + @property + def y(self) -> float: + """ + Y coordinate [μm]. + """ + @property + def z(self) -> float: + """ + Z coordinate [μm]. + """ + +class msegment: + @property + def dist(self) -> mpoint: + """ + the location and radius of the distal end. + """ + @property + def prox(self) -> mpoint: + """ + the location and radius of the proximal end. + """ + @property + def tag(self) -> int: + """ + tag meta-data. + """ + +class neuroml: + def __init__(self, arg0: typing.Any) -> None: + """ + Construct NML morphology from filename or stream. + """ + def cell_ids(self) -> list[str]: + """ + Query top-level cells. + """ + def cell_morphology( + self, cell_id: str, allow_spherical_root: bool = False + ) -> neuroml_morph_data | None: + """ + Retrieve nml_morph_data associated with cell_id. + """ + def morphology( + self, morph_id: str, allow_spherical_root: bool = False + ) -> neuroml_morph_data | None: + """ + Retrieve top-level nml_morph_data associated with morph_id. + """ + def morphology_ids(self) -> list[str]: + """ + Query top-level standalone morphologies. + """ + +class neuroml_morph_data: + def groups(self) -> label_dict: + """ + Label dictionary containing one region expression for each segmentGroup id. + """ + def named_segments(self) -> label_dict: + """ + Label dictionary containing one region expression for each name applied to one or more segments. + """ + def segments(self) -> label_dict: + """ + Label dictionary containing one region expression for each segment id. + """ + @property + def cell_id(self) -> str | None: + """ + Cell id, or empty if morphology was taken from a top-level element. + """ + @property + def group_segments(self) -> dict[str, list[int]]: + """ + Map from segmentGroup ids to their corresponding segment ids. + """ + @property + def id(self) -> str: + """ + Morphology id. + """ + @property + def morphology(self) -> morphology: + """ + Morphology constructed from a signle NeuroML element. + """ + +class partition_hint: + """ + Provide a hint on how the cell groups should be partitioned. + """ + + max_size: typing.ClassVar[int] = 18446744073709551615 + def __init__( + self, + cpu_group_size: int = 1, + gpu_group_size: int = 18446744073709551615, + prefer_gpu: bool = True, + ) -> None: + """ + Construct a partition hint with arguments: + cpu_group_size: The size of cell group assigned to CPU, each cell in its own group by default. + Must be positive, else set to default value. + gpu_group_size: The size of cell group assigned to GPU, all cells in one group by default. + Must be positive, else set to default value. + prefer_gpu: Whether GPU is preferred, True by default. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def cpu_group_size(self) -> int: + """ + The size of cell group assigned to CPU. + """ + @cpu_group_size.setter + def cpu_group_size(self, arg0: int) -> None: ... + @property + def gpu_group_size(self) -> int: + """ + The size of cell group assigned to GPU. + """ + @gpu_group_size.setter + def gpu_group_size(self, arg0: int) -> None: ... + @property + def prefer_gpu(self) -> bool: + """ + Whether GPU usage is preferred. + """ + @prefer_gpu.setter + def prefer_gpu(self, arg0: bool) -> None: ... + +class place_pwlin: + def __init__(self, morphology: morphology, isometry: isometry = ...) -> None: + """ + Construct a piecewise-linear placement object from the given morphology and optional isometry. + """ + def all_at(self, location: location) -> list[mpoint]: + """ + Return list of all possible interpolated mpoints corresponding to the location argument. + """ + def all_segments(self, arg0: list[cable]) -> list[msegment]: + """ + Return maximal list of non-overlapping full or partial msegments whose union is coterminous with the extent of the given list of cables. + """ + def at(self, location: location) -> mpoint: + """ + Return an interpolated mpoint corresponding to the location argument. + """ + def closest(self, arg0: float, arg1: float, arg2: float) -> tuple: + """ + Find the location on the morphology that is closest to a 3d point. Returns the location and its distance from the point. + """ + def segments(self, arg0: list[cable]) -> list[msegment]: + """ + Return minimal list of full or partial msegments whose union is coterminous with the extent of the given list of cables. + """ + +class poisson_schedule(schedule_base): + """ + Describes a schedule according to a Poisson process within the interval [tstart, tstop). + """ + + def __init__( + self, + freq: units.quantity, + *, + tstart: units.quantity = ..., + seed: int = 0, + tstop: units.quantity | None = None, + ) -> None: + """ + Construct a Poisson schedule with arguments: + tstart: The delivery time of the first event in the sequence [ms], 0 by default. + freq: The expected frequency [kHz]. + seed: The seed for the random number generator, 0 by default. + tstop: No events delivered after this time [ms], None by default. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def events(self, arg0: units.quantity, arg1: units.quantity) -> list[float]: + """ + A view of monotonically increasing time values in the half-open interval [t0, t1). + """ + @property + def freq(self) -> units.quantity: + """ + The expected frequency [kHz]. + """ + @freq.setter + def freq(self, arg1: units.quantity) -> None: ... + @property + def seed(self) -> int: + """ + The seed for the random number generator. + """ + @seed.setter + def seed(self, arg0: int) -> None: ... + @property + def tstart(self) -> units.quantity: + """ + The delivery time of the first event in the sequence [ms]. + """ + @tstart.setter + def tstart(self, arg1: units.quantity) -> None: ... + @property + def tstop(self) -> units.quantity: + """ + No events delivered after this time [ms]. + """ + @tstop.setter + def tstop(self, arg1: units.quantity) -> None: ... + +class probe: + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +class proc_allocation: + """ + Enumerates the computational resources on a node to be used for simulation. + """ + + def __init__( + self, + *, + threads: int = 1, + gpu_id: typing.Any = None, + bind_procs: bool = False, + bind_threads: bool = False, + ) -> None: + """ + Construct an allocation with arguments: + threads: The number of threads available locally for execution. Must be set to 1 at minimum. 1 by default. + gpu_id: The identifier of the GPU to use, None by default. + bind_procs: Create process binding mask. + bind_threads: Create thread binding mask. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def bind_procs(self) -> bool: + """ + Try to bind MPI procs? + """ + @bind_procs.setter + def bind_procs(self, arg1: bool) -> None: ... + @property + def bind_threads(self) -> bool: + """ + Try to bind threads? + """ + @bind_threads.setter + def bind_threads(self, arg1: bool) -> None: ... + @property + def gpu_id(self) -> int | None: + """ + The identifier of the GPU to use. + Corresponds to the integer parameter used to identify GPUs in CUDA API calls. + """ + @gpu_id.setter + def gpu_id(self, arg1: typing.Any) -> None: ... + @property + def has_gpu(self) -> bool: + """ + Whether a GPU is being used (True/False). + """ + @property + def threads(self) -> int: + """ + The number of threads available locally for execution. + """ + @threads.setter + def threads(self, arg1: int) -> None: ... + +class recipe: + """ + A description of a model, describing the cells and the network via a cell-centric interface. + """ + + def __init__(self) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def cell_description(self, gid: int) -> typing.Any: + """ + High level description of the cell with global identifier gid. + """ + def cell_kind(self, gid: int) -> cell_kind: + """ + The kind of cell with global identifier gid. + """ + def connections_on(self, gid: int) -> list[connection]: + """ + A list of all the incoming connections to gid, [] by default. + """ + def event_generators(self, gid: int) -> list[typing.Any]: + """ + A list of all the event generators that are attached to gid, [] by default. + """ + def external_connections_on(self, gid: int) -> list[connection]: + """ + A list of all the incoming connections from _remote_ locations to gid, [] by default. + """ + def gap_junctions_on(self, gid: int) -> list[gap_junction_connection]: + """ + A list of the gap junctions connected to gid, [] by default. + """ + def global_properties(self, kind: cell_kind) -> typing.Any: + """ + The default properties applied to all cells of type 'kind' in the model. + """ + def num_cells(self) -> int: + """ + The number of cells in the model. + """ + def probes(self, gid: int) -> list[probe]: + """ + The probes to allow monitoring. + """ + +class regular_schedule(schedule_base): + """ + Describes a regular schedule with multiples of dt within the interval [tstart, tstop). + """ + + @typing.overload + def __init__( + self, + tstart: units.quantity, + dt: units.quantity, + tstop: units.quantity | None = None, + ) -> None: + """ + Construct a regular schedule with arguments: + tstart: The delivery time of the first event in the sequence [ms]. + dt: The interval between time points [ms]. + tstop: No events delivered after this time [ms], None by default. + """ + @typing.overload + def __init__(self, dt: units.quantity) -> None: + """ + Construct a regular schedule, starting from t = 0 and never terminating, with arguments: + dt: The interval between time points [ms]. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def events(self, arg0: float, arg1: float) -> list[float]: + """ + A view of monotonically increasing time values in the half-open interval [t0, t1). + """ + @property + def dt(self) -> units.quantity: + """ + The interval between time points [ms]. + """ + @dt.setter + def dt(self, arg1: units.quantity) -> None: ... + @property + def tstart(self) -> units.quantity: + """ + The delivery time of the first event in the sequence [ms]. + """ + @tstart.setter + def tstart(self, arg1: units.quantity) -> None: ... + @property + def tstop(self) -> units.quantity | None: + """ + No events delivered after this time [ms]. + """ + @tstop.setter + def tstop(self, arg1: units.quantity | None) -> None: ... + +class reversal_potential: + """ + Setting the initial reversal potential. + """ + + def __init__(self, arg0: str, arg1: units.quantity) -> None: ... + def __repr__(self) -> str: ... + +class reversal_potential_method: + """ + Describes the mechanism used to compute eX for ion X. + """ + + def __init__(self, arg0: str, arg1: mechanism) -> None: ... + def __repr__(self) -> str: ... + +class scaled_mechanism: + """ + For painting a scaled density mechanism on a region. + """ + + @typing.overload + def __init__(self, arg0: density) -> None: ... + @typing.overload + def __init__(self, arg0: density, arg1: dict[str, str]) -> None: ... + @typing.overload + def __init__(self, arg0: density, **kwargs) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def scale(self, name: str, ex: str) -> scaled_mechanism: + """ + Add a scaling expression to a parameter. + """ + +class schedule_base: + """ + Schedule abstract base class. + """ + +class segment_tree: + def __init__(self) -> None: ... + def __str__(self) -> str: ... + @typing.overload + def append(self, parent: int, prox: mpoint, dist: mpoint, tag: int) -> int: + """ + Append a segment to the tree. + """ + @typing.overload + def append(self, parent: int, dist: mpoint, tag: int) -> int: + """ + Append a segment to the tree. + """ + @typing.overload + def append( + self, parent: int, x: float, y: float, z: float, radius: float, tag: int + ) -> int: + """ + Append a segment to the tree, using the distal location of the parent segment as the proximal end. + """ + def apply_isometry(self, arg0: isometry) -> segment_tree: + """ + Apply an isometry to all segments in the tree. + """ + def equivalent(self, arg0: segment_tree) -> bool: + """ + Two trees are equivalent, but not neccessarily identical, ie they have the same segments and structure. + """ + def is_fork(self, i: int) -> bool: + """ + True if segment has more than one child. + """ + def is_root(self, i: int) -> bool: + """ + True if segment has no parent. + """ + def is_terminal(self, i: int) -> bool: + """ + True if segment has no children. + """ + def join_at(self, arg0: int, arg1: segment_tree) -> segment_tree: + """ + Join two subtrees at a given id, such that said id becomes the parent of the inserted sub-tree. + """ + def reserve(self, arg0: int) -> None: ... + def split_at(self, arg0: int) -> tuple[segment_tree, segment_tree]: + """ + Split into a pair of trees at the given id, such that one tree is the subtree rooted at id and the other is the original tree without said subtree. + """ + def tag_roots(self, arg0: int) -> list[int]: + """ + Get roots of tag region of this segment tree. + """ + @property + def empty(self) -> bool: + """ + Indicates whether the tree is empty (i.e. whether it has size 0) + """ + @property + def parents(self) -> list[int]: + """ + A list with the parent index of each segment. + """ + @property + def segments(self) -> list[msegment]: + """ + A list of the segments. + """ + @property + def size(self) -> int: + """ + The number of segments in the tree. + """ + +class selection_policy: + """ + Enumeration used to identify a selection policy, used by the model for selecting one of possibly multiple locations on the cell associated with a labeled item. + + Members: + + round_robin : Iterate round-robin over all possible locations. + + round_robin_halt : Halts at the current location until the round_robin policy is called (again). + + univalent : Assert that there is only one possible location associated with a labeled item on the cell. The model throws an exception if the assertion fails. + """ + + __members__: typing.ClassVar[ + dict[str, selection_policy] + ] # value = {'round_robin': , 'round_robin_halt': , 'univalent': } + round_robin: typing.ClassVar[ + selection_policy + ] # value = + round_robin_halt: typing.ClassVar[ + selection_policy + ] # value = + univalent: typing.ClassVar[ + selection_policy + ] # value = + def __eq__(self, other: typing.Any) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __init__(self, value: int) -> None: ... + def __int__(self) -> int: ... + def __ne__(self, other: typing.Any) -> bool: ... + def __repr__(self) -> str: ... + def __setstate__(self, state: int) -> None: ... + def __str__(self) -> str: ... + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... + +class simulation: + """ + The executable form of a model. + A simulation is constructed from a recipe, and then used to update and monitor model state. + """ + + @staticmethod + def deserialize(*args, **kwargs) -> None: ... + def __init__( + self, + recipe: recipe, + context: context | None = None, + domains: domain_decomposition | None = None, + seed: int = 0, + ) -> None: + """ + Initialize the model described by a recipe, with cells and network distributed + according to the domain decomposition and computational resources described by a + context. Initialize PRNG using seed + """ + def clear_samplers(self) -> None: + """ + Clearing spike and sample information. restoring memory + """ + @typing.overload + def probe_metadata(self, probeset_id: cell_address) -> list: + """ + Retrieve metadata associated with given probe id. + """ + @typing.overload + def probe_metadata(self, addr: tuple[int, str]) -> list: + """ + Retrieve metadata associated with given probe id. + """ + @typing.overload + def probe_metadata(self, gid: int, tag: str) -> list: + """ + Retrieve metadata associated with given probe id. + """ + def progress_banner(self) -> None: + """ + Show a text progress bar during simulation. + """ + def record(self, arg0: spike_recording) -> None: + """ + Disable or enable local or global spike recording. + """ + def remove_all_samplers(self, arg0: int) -> None: + """ + Remove all sampling on the simulatr. + """ + def remove_sampler(self, handle: int) -> None: + """ + Remove sampling associated with the given handle. + """ + def reset(self) -> None: + """ + Reset the state of the simulation to its initial state. + """ + def run(self, tfinal: units.quantity, dt: units.quantity = ...) -> float: + """ + Run the simulation from current simulation time to tfinal [ms], with maximum time step size dt [ms]. + """ + @typing.overload + def sample(self, probeset_id: cell_address, schedule: schedule_base) -> int: + """ + Record data from probes with given probeset_id according to supplied schedule. + Returns handle for retrieving data or removing the sampling. + """ + @typing.overload + def sample(self, gid: int, tag: str, schedule: schedule_base) -> int: + """ + Record data from probes with given probeset_id=(gid, tag) according to supplied schedule. + Returns handle for retrieving data or removing the sampling. + """ + @typing.overload + def sample(self, probeset_id: tuple[int, str], schedule: schedule_base) -> int: + """ + Record data from probes with given probeset_id=(gid, tag) according to supplied schedule. + Returns handle for retrieving data or removing the sampling. + """ + def samples(self, handle: int) -> list: + """ + Retrieve sample data as a list, one element per probe associated with the query. + """ + def serialize(self) -> str: + """ + Serialize the simulation object to a JSON string. + """ + def set_remote_spike_filter(self, pred: typing.Callable[[spike], bool]) -> None: + """ + Add a callback to filter spikes going out over external connections. `pred` isa callable on the `spike` type. **Caution**: This will be extremely slow; use C++ if you want to make use of this. + """ + def spikes(self) -> typing.Any: + """ + Retrieve recorded spikes as numpy array. + """ + def update(self, recipe: recipe) -> None: + """ + Rebuild the connection table from recipe::connections_on and the eventgenerators based on recipe::event_generators. + """ + +class single_cell_model: + """ + Wrapper for simplified description, and execution, of single cell models. + """ + + @typing.overload + def __init__( + self, tree: segment_tree, decor: decor, labels: label_dict = ... + ) -> None: + """ + Build single cell model from cable cell components + """ + @typing.overload + def __init__( + self, morph: morphology, decor: decor, labels: label_dict = ... + ) -> None: + """ + Build single cell model from cable cell components + """ + @typing.overload + def __init__(self, cell: cable_cell) -> None: + """ + Initialise a single cell model for a cable cell. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def event_generator(self, event_generator: event_generator) -> None: + """ + Register an event generator. + event_generator: An Arbor event generator. + """ + @typing.overload + def probe(self, what: str, where: str, tag: str, frequency: units.quantity) -> None: + """ + Sample a variable on the cell. + what: Name of the variable to record (currently only 'voltage'). + where: Location on cell morphology at which to sample the variable. + tag: Unique name for this probe. + frequency: The target frequency at which to sample [kHz]. + """ + @typing.overload + def probe( + self, what: str, where: location, tag: str, frequency: units.quantity + ) -> None: + """ + Sample a variable on the cell. + what: Name of the variable to record (currently only 'voltage'). + where: Location on cell morphology at which to sample the variable. + tag: Unique name for this probe. + frequency: The target frequency at which to sample [kHz]. + """ + def run(self, tfinal: units.quantity, dt: units.quantity = ...) -> None: + """ + Run model from t=0 to t=tfinal ms. + """ + @property + def cable_cell(self) -> cable_cell: + """ + The cable cell held by this model. + """ + @property + def properties(self) -> cable_global_properties: + """ + Global properties. + """ + @properties.setter + def properties(self, arg0: cable_global_properties) -> None: ... + @property + def spikes(self) -> list[float]: + """ + Holds spike times [ms] after a call to run(). + """ + @property + def traces(self) -> list[trace]: + """ + Holds sample traces after a call to run(). + """ + +class spike: + def __init__(self, arg0: cell_member, arg1: float) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def source(self) -> cell_member: + """ + The global identifier of the cell. + """ + @source.setter + def source(self, arg0: cell_member) -> None: ... + @property + def time(self) -> float: + """ + The time of spike. + """ + @time.setter + def time(self, arg0: float) -> None: ... + +class spike_recording: + """ + Members: + + off + + local + + all + """ + + __members__: typing.ClassVar[ + dict[str, spike_recording] + ] # value = {'off': , 'local': , 'all': } + all: typing.ClassVar[spike_recording] # value = + local: typing.ClassVar[spike_recording] # value = + off: typing.ClassVar[spike_recording] # value = + def __eq__(self, other: typing.Any) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __init__(self, value: int) -> None: ... + def __int__(self) -> int: ... + def __ne__(self, other: typing.Any) -> bool: ... + def __repr__(self) -> str: ... + def __setstate__(self, state: int) -> None: ... + def __str__(self) -> str: ... + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... + +class spike_source_cell: + """ + A spike source cell, that generates a user-defined sequence of spikes that act as inputs for other cells in the network. + """ + + @typing.overload + def __init__(self, source_label: str, schedule: regular_schedule) -> None: + """ + Construct a spike source cell with a single source labeled 'source_label'. + The cell generates spikes on 'source_label' at regular intervals. + """ + @typing.overload + def __init__(self, source_label: str, schedule: explicit_schedule) -> None: + """ + Construct a spike source cell with a single source labeled 'source_label'. + The cell generates spikes on 'source_label' at a sequence of user-defined times. + """ + @typing.overload + def __init__(self, source_label: str, schedule: poisson_schedule) -> None: + """ + Construct a spike source cell with a single source labeled 'source_label'. + The cell generates spikes on 'source_label' at times defined by a Poisson sequence. + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +class synapse: + """ + For placing a synaptic mechanism on a locset. + """ + + @typing.overload + def __init__(self, arg0: str) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism) -> None: ... + @typing.overload + def __init__(self, arg0: str, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: str, **kwargs) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, **kwargs) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def mech(self) -> mechanism: + """ + The underlying mechanism. + """ + +class temperature: + """ + Setting the temperature. + """ + + def __init__(self, arg0: units.quantity) -> None: ... + def __repr__(self) -> str: ... + +class threshold_detector: + """ + A spike detector, generates a spike when voltage crosses a threshold. Can be used as source endpoint for an arbor.connection. + """ + + def __init__(self, threshold: units.quantity) -> None: + """ + Voltage threshold of spike detector [mV] + """ + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def threshold(self) -> float: + """ + Voltage threshold of spike detector [mV] + """ + +class trace: + """ + Values and meta-data for a sample-trace on a single cell model. + """ + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def location(self) -> location: + """ + Location on cell morphology. + """ + @property + def time(self) -> list[float]: + """ + Time stamps of samples [ms]. + """ + @property + def value(self) -> list[float]: + """ + Sample values. + """ + @property + def variable(self) -> str: + """ + Name of the variable being recorded. + """ + +class voltage_process: + """ + For painting a voltage_process mechanism on a region. + """ + + @typing.overload + def __init__(self, arg0: str) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism) -> None: ... + @typing.overload + def __init__(self, arg0: str, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, arg1: dict[str, float]) -> None: ... + @typing.overload + def __init__(self, arg0: mechanism, **kwargs) -> None: ... + @typing.overload + def __init__(self, arg0: str, **kwargs) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def mech(self) -> mechanism: + """ + The underlying mechanism. + """ + +def allen_catalogue() -> catalogue: ... +def bbp_catalogue() -> catalogue: ... +def cable_probe_axial_current(where: str, tag: str) -> probe: + """ + Probe specification for cable cell axial current at points in a location set. + """ + +def cable_probe_density_state( + where: str, mechanism: str, state: str, tag: str +) -> probe: + """ + Probe specification for a cable cell density mechanism state variable at points in a location set. + """ + +def cable_probe_density_state_cell(mechanism: str, state: str, tag: str) -> probe: + """ + Probe specification for a cable cell density mechanism state variable on each cable in each CV where defined. + """ + +def cable_probe_ion_current_cell(ion: str, tag: str) -> probe: + """ + Probe specification for cable cell ionic current across each cable in each CV. + """ + +def cable_probe_ion_current_density(where: str, ion: str, tag: str) -> probe: + """ + Probe specification for cable cell ionic current density at points in a location set. + """ + +def cable_probe_ion_diff_concentration(where: str, ion: str, tag: str) -> probe: + """ + Probe specification for cable cell diffusive ionic concentration at points in a location set. + """ + +def cable_probe_ion_diff_concentration_cell(ion: str, tag: str) -> probe: + """ + Probe specification for cable cell diffusive ionic concentration for each cable in each CV. + """ + +def cable_probe_ion_ext_concentration(where: str, ion: str, tag: str) -> probe: + """ + Probe specification for cable cell external ionic concentration at points in a location set. + """ + +def cable_probe_ion_ext_concentration_cell(ion: str, tag: str) -> probe: + """ + Probe specification for cable cell external ionic concentration for each cable in each CV. + """ + +def cable_probe_ion_int_concentration(where: str, ion: str, tag: str) -> probe: + """ + Probe specification for cable cell internal ionic concentration at points in a location set. + """ + +def cable_probe_ion_int_concentration_cell(ion: str, tag: str) -> probe: + """ + Probe specification for cable cell internal ionic concentration for each cable in each CV. + """ + +def cable_probe_membrane_voltage(where: str, tag: str) -> probe: + """ + Probe specification for cable cell membrane voltage interpolated at points in a location set. + """ + +def cable_probe_membrane_voltage_cell(tag: str) -> probe: + """ + Probe specification for cable cell membrane voltage associated with each cable in each CV. + """ + +def cable_probe_point_state(target: int, mechanism: str, state: str, tag: str) -> probe: + """ + Probe specification for a cable cell point mechanism state variable value at a given target index. + """ + +def cable_probe_point_state_cell(mechanism: str, state: str, tag: str) -> probe: + """ + Probe specification for a cable cell point mechanism state variable value at every corresponding target. + """ + +def cable_probe_stimulus_current_cell(tag: str) -> probe: + """ + Probe specification for cable cell stimulus current across each cable in each CV. + """ + +def cable_probe_total_current_cell(tag: str) -> probe: + """ + Probe specification for cable cell total transmembrane current for each cable in each CV. + """ + +def cable_probe_total_ion_current_cell(tag: str) -> probe: + """ + Probe specification for cable cell total transmembrane current excluding capacitive currents for each cable in each CV. + """ + +def cable_probe_total_ion_current_density(where: str, tag: str) -> probe: + """ + Probe specification for cable cell total transmembrane current density excluding capacitive currents at points in a location set. + """ + +def config() -> dict: + """ + Get Arbor's configuration. + """ + +def cv_data(cell: cable_cell) -> cell_cv_data | None: + """ + Returns a cell_cv_data object representing the CVs comprising the cable-cell according to the discretization policy provided in the decor of the cell. Returns None if no CV-policy was provided in the decor. + """ + +def cv_policy_every_segment(domain: str = "(all)") -> cv_policy: + """ + Policy to create one compartment per component of a region. + """ + +def cv_policy_explicit(locset: str, domain: str = "(all)") -> cv_policy: + """ + Policy to create compartments at explicit locations. + """ + +def cv_policy_fixed_per_branch(n: int, domain: str = "(all)") -> cv_policy: + """ + Policy to use the same number of CVs for each branch. + """ + +def cv_policy_max_extent(length: float, domain: str = "(all)") -> cv_policy: + """ + Policy to use as many CVs as required to ensure that no CV has a length longer than a given value. + """ + +def cv_policy_single(domain: str = "(all)") -> cv_policy: + """ + Policy to create one compartment per component of a region. + """ + +def default_catalogue() -> catalogue: ... +def intersect_region(reg: str, data: cell_cv_data, integrate_along: str) -> list[tuple]: + """ + Returns a list of [index, proportion] tuples identifying the CVs present in the region. + `index` is the index of the CV in the cell_cv_data object provided as an argument. + `proportion` is the proportion of the CV (itegrated by area or length) included in the region. + """ + +def lif_probe_voltage(tag: str) -> probe: + """ + Probe specification for LIF cell membrane voltage. + """ + +def load_asc( + filename_or_stream: typing.Any, raw: bool = False +) -> segment_tree | asc_morphology: + """ + Load a morphology or segment_tree (raw=True) and meta data from a Neurolucida ASCII .asc file. + """ + +def load_catalogue(arg0: typing.Any) -> catalogue: ... +def load_component(filename_or_descriptor: typing.Any) -> cable_component: + """ + Load arbor-component (decor, morphology, label_dict, cable_cell) from file. + """ + +def load_swc_arbor( + filename_or_stream: typing.Any, raw: bool = False +) -> segment_tree | morphology: + """ + Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by Arbor. + Specifically: + * Single-segment somas are disallowed. + * There are no special rules related to somata. They can be one or multiple branches + and other segments can connect anywhere along them. + * A segment is always created between a sample and its parent, meaning there + are no gaps in the resulting morphology. + """ + +def load_swc_neuron( + filename_or_stream: typing.Any, raw: bool = False +) -> segment_tree | morphology: + """ + Generate a morphology/segment_tree (raw=False/True) from an SWC file following the rules prescribed by NEURON. + See the documentation https://docs.arbor-sim.org/en/latest/fileformat/swc.html + for a detailed description of the interpretation. + """ + +def neuron_cable_properties() -> cable_global_properties: + """ + default NEURON cable_global_properties + """ + +def partition_by_group( + recipe: recipe, context: context, groups: list[group_description] +) -> domain_decomposition: + """ + Construct a domain_decomposition that assigned the groups of cell provided as argument + to the local hardware resources described by context on the calling rank. + The cell_groups are guaranteed to be present on the calling rank. + """ + +def partition_load_balance( + recipe: recipe, context: context, hints: dict[cell_kind, partition_hint] = {} +) -> domain_decomposition: + """ + Construct a domain_decomposition that distributes the cells in the model described by recipe + over the distributed and local hardware resources described by context. + Optionally, provide a dictionary of partition hints for certain cell kinds, by default empty. + """ + +def print_config() -> None: + """ + Print Arbor's configuration. + """ + +def stochastic_catalogue() -> catalogue: ... +@typing.overload +def write_component( + object: cable_component, filename_or_descriptor: typing.Any +) -> None: + """ + Write cable_component to file. + """ + +@typing.overload +def write_component(object: decor, filename_or_descriptor: typing.Any) -> None: + """ + Write decor to file. + """ + +@typing.overload +def write_component(object: label_dict, filename_or_descriptor: typing.Any) -> None: + """ + Write label_dict to file. + """ + +@typing.overload +def write_component(object: morphology, filename_or_descriptor: typing.Any) -> None: + """ + Write morphology to file. + """ + +@typing.overload +def write_component(object: cable_cell, filename_or_descriptor: typing.Any) -> None: + """ + Write cable_cell to file. + """ + +__version__: str = "0.9.1-dev" +mnpos: int = 4294967295 diff --git a/python/stubs/arbor/_arbor/env.pyi b/python/stubs/arbor/_arbor/env.pyi new file mode 100644 index 0000000000..103e233033 --- /dev/null +++ b/python/stubs/arbor/_arbor/env.pyi @@ -0,0 +1,46 @@ +""" +Wrappers for arborenv. +""" +from __future__ import annotations +import arbor._arbor +import typing + +__all__ = [ + "default_allocation", + "default_concurrency", + "default_gpu", + "find_private_gpu", + "get_env_num_threads", + "thread_concurrency", +] + +def default_allocation() -> arbor._arbor.proc_allocation: + """ + Attempts to detect the number of locally available CPU cores. Returns 1 if unable to detect the number of cores. Use with caution in combination with MPI. + """ + +def default_concurrency() -> arbor._arbor.proc_allocation: + """ + Returns number of threads to use from get_env_num_threads(), or else from thread_concurrency() if get_env_num_threads() returns zero. + """ + +def default_gpu() -> int | None: + """ + Determine GPU id to use from the ARBENV_GPU_ID environment variable, or from the first available GPU id of those detected. + """ + +def find_private_gpu(arg0: typing.Any) -> None: + """ + Identify a private GPU id per node, only available if built with GPU and MPI. + mpi: The MPI communicator. + """ + +def get_env_num_threads() -> int: + """ + Retrieve user-specified number of threads to use from the environment variable ARBENV_NUM_THREADS. + """ + +def thread_concurrency() -> int: + """ + Attempts to detect the number of locally available CPU cores. Returns 1 if unable to detect the number of cores. Use with caution in combination with MPI. + """ diff --git a/python/stubs/arbor/_arbor/py.typed b/python/stubs/arbor/_arbor/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/stubs/arbor/_arbor/units.pyi b/python/stubs/arbor/_arbor/units.pyi new file mode 100644 index 0000000000..f5a79e4944 --- /dev/null +++ b/python/stubs/arbor/_arbor/units.pyi @@ -0,0 +1,185 @@ +""" +Units and quantities for driving the user interface. +""" +from __future__ import annotations +import typing + +__all__ = [ + "A", + "C", + "Celsius", + "F", + "Hz", + "Kelvin", + "M", + "MOhm", + "Ohm", + "S", + "V", + "cm", + "cm2", + "deg", + "giga", + "kHz", + "kOhm", + "kilo", + "m", + "m2", + "mA", + "mM", + "mS", + "mV", + "mega", + "micro", + "milli", + "mm", + "mm2", + "mol", + "ms", + "nA", + "nF", + "nano", + "nil", + "nm", + "nm2", + "ns", + "pA", + "pF", + "pico", + "quantity", + "rad", + "s", + "uA", + "uF", + "uS", + "um", + "um2", + "unit", + "us", +] + +class quantity: + """ + A quantity, comprising a magnitude and a unit. + """ + + __hash__: typing.ClassVar[None] = None + def __add__(self, arg0: quantity) -> quantity: ... + def __eq__(self, arg0: quantity) -> bool: ... + @typing.overload + def __mul__(self, arg0: quantity) -> quantity: ... + @typing.overload + def __mul__(self, arg0: float) -> quantity: ... + @typing.overload + def __mul__(self, arg0: unit) -> quantity: ... + def __ne__(self, arg0: quantity) -> bool: ... + def __pow__(self: unit, arg0: int) -> unit: ... + def __repr__(self) -> str: + """ + Convert quantity to string. + """ + def __rmul__(self, arg0: float) -> quantity: ... + def __rtruediv__(self, arg0: float) -> quantity: ... + def __str__(self) -> str: + """ + Convert quantity to string. + """ + def __sub__(self, arg0: quantity) -> quantity: ... + @typing.overload + def __truediv__(self, arg0: quantity) -> quantity: ... + @typing.overload + def __truediv__(self, arg0: float) -> quantity: ... + @typing.overload + def __truediv__(self, arg0: unit) -> quantity: ... + def value_as(self, unit: unit) -> float: + """ + Convert quantity to given unit and return magnitude. + """ + @property + def units(self) -> unit: + """ + Return units. + """ + @property + def value(self) -> float: + """ + Return magnitude. + """ + +class unit: + """ + A unit. + """ + + __hash__: typing.ClassVar[None] = None + def __eq__(self, arg0: unit) -> bool: ... + @typing.overload + def __mul__(self, arg0: unit) -> unit: ... + @typing.overload + def __mul__(self, arg0: float) -> quantity: ... + def __ne__(self, arg0: unit) -> bool: ... + def __pow__(self, arg0: int) -> unit: ... + def __repr__(self) -> str: + """ + Convert unit to string. + """ + def __rmul__(self, arg0: float) -> quantity: ... + def __rtruediv__(self, arg0: float) -> quantity: ... + def __str__(self) -> str: + """ + Convert unit to string. + """ + @typing.overload + def __truediv__(self, arg0: unit) -> unit: ... + @typing.overload + def __truediv__(self, arg0: float) -> quantity: ... + +A: unit # value = A +C: unit # value = C +Celsius: unit # value = °C +F: unit # value = F +Hz: unit # value = Hz +Kelvin: unit # value = K +M: unit # value = mol/m^3 +MOhm: unit # value = 1/uS +Ohm: unit # value = 1/S +S: unit # value = S +V: unit # value = V +cm: unit # value = cm +cm2: unit # value = cm^2 +deg: unit # value = deg +giga: unit # value = 1000000000 +kHz: unit # value = kHz +kOhm: unit # value = 1/mS +kilo: unit # value = 1000 +m: unit # value = m +m2: unit # value = m^2 +mA: unit # value = mA +mM: unit # value = umol/L +mS: unit # value = mS +mV: unit # value = mV +mega: unit # value = 1000000 +micro: unit # value = 9.99999997475242708e-07 +milli: unit # value = 0.00100000004749745131 +mm: unit # value = mm +mm2: unit # value = mm^2 +mol: unit # value = mol +ms: unit # value = ms +nA: unit # value = nA +nF: unit # value = nF +nano: unit # value = 9.99999971718068537e-10 +nil: unit # value = +nm: unit # value = nm +nm2: unit # value = nm^2 +ns: unit # value = ns +pA: unit # value = pA +pF: unit # value = pF +pico: unit # value = 9.999999960041972e-13 +rad: unit # value = rad +s: unit # value = s +uA: unit # value = uA +uF: unit # value = uF +uS: unit # value = uS +um: unit # value = um +um2: unit # value = um^2 +us: unit # value = us diff --git a/python/stubs/arbor/py.typed b/python/stubs/arbor/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/stubs/py.typed b/python/stubs/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/test/fixtures.py b/python/test/fixtures.py index 11b9011750..1071653855 100644 --- a/python/test/fixtures.py +++ b/python/test/fixtures.py @@ -1,4 +1,5 @@ -import arbor +import arbor as A +from arbor import units as U import functools from functools import lru_cache as cache from pathlib import Path @@ -6,8 +7,8 @@ import atexit import inspect -_mpi_enabled = arbor.__config__["mpi"] -_mpi4py_enabled = arbor.__config__["mpi4py"] +_mpi_enabled = A.__config__["mpi"] +_mpi4py_enabled = A.__config__["mpi4py"] # The API of `functools`'s caches went through a bunch of breaking changes from # 3.6 to 3.9. Patch them up in a local `cache` function. @@ -78,13 +79,13 @@ def _finalize_mpi(): MPI.Finalize() else: - arbor.mpi_finalize() + A.mpi_finalize() @_fixture def context(): """ - Fixture that produces an MPI sensitive `arbor.context` + Fixture that produces an MPI sensitive `A.context` """ if _mpi_enabled: if _mpi4py_enabled: @@ -94,13 +95,13 @@ def context(): print("Context fixture initializing mpi4py", flush=True) MPI.Initialize() atexit.register(_finalize_mpi) - return arbor.context(arbor.proc_allocation(), mpi=MPI.COMM_WORLD) - elif not arbor.mpi_is_initialized(): + return A.context(A.proc_allocation(), mpi=MPI.COMM_WORLD) + elif not A.mpi_is_initialized(): print("Context fixture initializing mpi", flush=True) - arbor.mpi_init() + A.mpi_init() atexit.register(_finalize_mpi) - return arbor.context(arbor.proc_allocation(), mpi=arbor.mpi_comm()) - return arbor.context(arbor.proc_allocation()) + return A.context(A.proc_allocation(), mpi=A.mpi_comm()) + return A.context(A.proc_allocation()) class _BuildCatError(Exception): @@ -165,16 +166,16 @@ def _build_cat(name, path, context): @repo_path() def dummy_catalogue(repo_path): """ - Fixture that returns a dummy `arbor.catalogue` + Fixture that returns a dummy `A.catalogue` which contains the `dummy` mech. """ path = repo_path / "test" / "unit" / "dummy" cat_path = _build_cat("dummy", path) - return arbor.load_catalogue(str(cat_path)) + return A.load_catalogue(str(cat_path)) @_fixture -class empty_recipe(arbor.recipe): +class empty_recipe(A.recipe): """ Blank recipe fixture. """ @@ -183,14 +184,14 @@ class empty_recipe(arbor.recipe): @_fixture -class art_spiker_recipe(arbor.recipe): +class art_spiker_recipe(A.recipe): """ Recipe fixture with 3 artificial spiking cells and one cable cell. """ def __init__(self): super().__init__() - self.the_props = arbor.neuron_cable_properties() + self.the_props = A.neuron_cable_properties() self.trains = [[0.8, 2, 2.1, 3], [0.4, 2, 2.2, 3.1, 4.5], [0.2, 2, 2.8, 3]] def num_cells(self): @@ -198,9 +199,9 @@ def num_cells(self): def cell_kind(self, gid): if gid < 3: - return arbor.cell_kind.spike_source + return A.cell_kind.spike_source else: - return arbor.cell_kind.cable + return A.cell_kind.cable def connections_on(self, gid): return [] @@ -215,41 +216,44 @@ def probes(self, gid): if gid < 3: return [] else: - return [arbor.cable_probe_membrane_voltage('"midpoint"', "Um")] + return [A.cable_probe_membrane_voltage('"midpoint"', "Um")] def _cable_cell_elements(self): # (1) Create a morphology with a single (cylindrical) segment of length=diameter # = # 6 μm - tree = arbor.segment_tree() + tree = A.segment_tree() tree.append( - arbor.mnpos, - arbor.mpoint(-3, 0, 0, 3), - arbor.mpoint(3, 0, 0, 3), + A.mnpos, + (-3, 0, 0, 3), + (3, 0, 0, 3), tag=1, ) # (2) Define the soma and its midpoint - labels = arbor.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) + labels = A.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"}) # (3) Create cell and set properties - decor = arbor.decor() - decor.set_property(Vm=-40) - decor.paint('"soma"', arbor.density("hh")) - decor.place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp") - decor.place('"midpoint"', arbor.threshold_detector(-10), "detector") + decor = ( + A.decor() + .set_property(Vm=-40 * U.mV) + .paint('"soma"', A.density("hh")) + .place('"midpoint"', A.iclamp(10 * U.ms, 2 * U.ms, 0.8 * U.nA), "iclamp") + .place('"midpoint"', A.threshold_detector(-10 * U.mV), "detector") + ) # return tuple of tree, labels, and decor for creating a cable cell (can still - # be modified before calling arbor.cable_cell()) + # be modified before calling A.cable_cell()) return tree, labels, decor def cell_description(self, gid): if gid < 3: - return arbor.spike_source_cell( - "src", arbor.explicit_schedule(self.trains[gid]) - ) + return A.spike_source_cell("src", self.schedule(gid)) else: tree, labels, decor = self._cable_cell_elements() - return arbor.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) + + def schedule(self, gid): + return A.explicit_schedule([t * U.ms for t in self.trains[gid]]) @_fixture @@ -274,5 +278,5 @@ def sum_weight_hh_spike_2(): @context() @art_spiker_recipe() def art_spiking_sim(context, art_spiker_recipe): - dd = arbor.partition_load_balance(art_spiker_recipe, context) - return arbor.simulation(art_spiker_recipe, context, dd) + dd = A.partition_load_balance(art_spiker_recipe, context) + return A.simulation(art_spiker_recipe, context, dd) diff --git a/python/test/unit/test_catalogues.py b/python/test/unit/test_catalogues.py index fef3965e9c..a8437cab4d 100644 --- a/python/test/unit/test_catalogues.py +++ b/python/test/unit/test_catalogues.py @@ -1,29 +1,30 @@ from .. import fixtures import unittest -import arbor as arb +import arbor as A +from arbor import units as U """ tests for (dynamically loaded) catalogues """ -class recipe(arb.recipe): +class recipe(A.recipe): def __init__(self): - arb.recipe.__init__(self) - self.tree = arb.segment_tree() - self.tree.append(arb.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1) - self.props = arb.neuron_cable_properties() + A.recipe.__init__(self) + self.tree = A.segment_tree() + self.tree.append(A.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1) + self.props = A.neuron_cable_properties() try: - self.props.catalogue = arb.load_catalogue("dummy-catalogue.so") + self.props.catalogue = A.load_catalogue("dummy-catalogue.so") except Exception: print("Catalogue not found. Are you running from build directory?") raise - self.props.catalogue = arb.default_catalogue() + self.props.catalogue = A.default_catalogue() - d = arb.decor() - d.paint("(all)", arb.density("pas")) - d.set_property(Vm=0.0) - self.cell = arb.cable_cell(self.tree, d) + d = A.decor() + d.paint("(all)", A.density("pas")) + d.set_property(Vm=0.0 * U.mV) + self.cell = A.cable_cell(self.tree, d) def global_properties(self, _): return self.props @@ -32,7 +33,7 @@ def num_cells(self): return 1 def cell_kind(self, gid): - return arb.cell_kind.cable + return A.cell_kind.cable def cell_description(self, gid): return self.cell @@ -41,7 +42,7 @@ def cell_description(self, gid): class TestCatalogues(unittest.TestCase): def test_nonexistent(self): with self.assertRaises(FileNotFoundError): - arb.load_catalogue("_NO_EXIST_.so") + A.load_catalogue("_NO_EXIST_.so") @fixtures.dummy_catalogue() def test_shared_catalogue(self, dummy_catalogue): @@ -58,10 +59,10 @@ def test_shared_catalogue(self, dummy_catalogue): def test_simulation(self): rcp = recipe() - ctx = arb.context() - dom = arb.partition_load_balance(rcp, ctx) - sim = arb.simulation(rcp, ctx, dom) - sim.run(tfinal=30) + ctx = A.context() + dom = A.partition_load_balance(rcp, ctx) + sim = A.simulation(rcp, ctx, dom) + sim.run(tfinal=30 * U.ms) def test_empty(self): def len(cat): @@ -70,9 +71,9 @@ def len(cat): def hash_(cat): return hash(" ".join(sorted(cat))) - cat = arb.catalogue() - ref = arb.default_catalogue() - other = arb.default_catalogue() + cat = A.catalogue() + ref = A.default_catalogue() + other = A.default_catalogue() # Test empty constructor self.assertEqual(0, len(cat), "Expected no mechanisms in `arbor.catalogue()`.") # Test empty extend @@ -98,7 +99,7 @@ def hash_(cat): hash_(cat), "Extending empty with cat should turn empty into cat.", ) - cat = arb.catalogue() + cat = A.catalogue() cat.extend(other, "prefix/") self.assertNotEqual( hash_(other), diff --git a/python/test/unit/test_clear_samplers.py b/python/test/unit/test_clear_samplers.py index fcfb6d7d95..bc7b3e2540 100644 --- a/python/test/unit/test_clear_samplers.py +++ b/python/test/unit/test_clear_samplers.py @@ -5,6 +5,7 @@ import unittest import arbor as A import numpy as np +from arbor import units as U from .. import fixtures from .. import cases @@ -21,11 +22,11 @@ class TestClearSamplers(unittest.TestCase): def test_spike_clearing(self, art_spiking_sim): sim = art_spiking_sim sim.record(A.spike_recording.all) - handle = sim.sample((3, "Um"), A.regular_schedule(0.1)) + handle = sim.sample((3, "Um"), A.regular_schedule(0.1 * U.ms)) # baseline to test against Run in exactly the same stepping to make sure there are no rounding differences - sim.run(3, 0.01) - sim.run(5, 0.01) + sim.run(3 * U.ms, 0.01 * U.ms) + sim.run(5 * U.ms, 0.01 * U.ms) spikes = sim.spikes() times = spikes["time"].tolist() gids = spikes["source"]["gid"].tolist() @@ -34,7 +35,7 @@ def test_spike_clearing(self, art_spiking_sim): sim.reset() # simulated with clearing the memory inbetween the steppings - sim.run(3, 0.01) + sim.run(3 * U.ms, 0.01 * U.ms) spikes = sim.spikes() times_t = spikes["time"].tolist() gids_t = spikes["source"]["gid"].tolist() @@ -51,7 +52,7 @@ def test_spike_clearing(self, art_spiking_sim): self.assertEqual(0, data_test.size) # run the next part of the simulation - sim.run(5, 0.01) + sim.run(5 * U.ms, 0.01 * U.ms) spikes = sim.spikes() times_t.extend(spikes["time"].tolist()) gids_t.extend(spikes["source"]["gid"].tolist()) diff --git a/python/test/unit/test_decor.py b/python/test/unit/test_decor.py index 59dcf0d900..18f71f57cc 100644 --- a/python/test/unit/test_decor.py +++ b/python/test/unit/test_decor.py @@ -2,6 +2,7 @@ import unittest import arbor as A +from arbor import units as U """ Tests for decor and decoration wrappers. @@ -12,29 +13,31 @@ class TestDecorClasses(unittest.TestCase): def test_iclamp(self): # Constant amplitude iclamp: - clamp = A.iclamp(10) + clamp = A.iclamp(10 * U.nA) self.assertEqual(0, clamp.frequency) self.assertEqual([(0, 10)], clamp.envelope) - clamp = A.iclamp(10, frequency=20) + clamp = A.iclamp(current=10 * U.nA, frequency=20 * U.kHz) self.assertEqual(20, clamp.frequency) self.assertEqual([(0, 10)], clamp.envelope) # Square pulse: - clamp = A.iclamp(100, 20, 3) + clamp = A.iclamp(100 * U.ms, 20 * U.ms, 3 * U.nA) self.assertEqual(0, clamp.frequency) self.assertEqual([(100, 3), (120, 3), (120, 0)], clamp.envelope) - clamp = A.iclamp(100, 20, 3, frequency=7) + clamp = A.iclamp(100 * U.ms, 20 * U.ms, 3 * U.nA, frequency=7 * U.kHz) self.assertEqual(7, clamp.frequency) self.assertEqual([(100, 3), (120, 3), (120, 0)], clamp.envelope) # Explicit envelope: envelope = [(1, 10), (3, 30), (5, 50), (7, 0)] - clamp = A.iclamp(envelope) + clamp = A.iclamp([(t * U.ms, i * U.nA) for t, i in envelope]) self.assertEqual(0, clamp.frequency) self.assertEqual(envelope, clamp.envelope) - clamp = A.iclamp(envelope, frequency=7) + clamp = A.iclamp( + [(t * U.ms, i * U.nA) for t, i in envelope], frequency=7 * U.kHz + ) self.assertEqual(7, clamp.frequency) self.assertEqual(envelope, clamp.envelope) diff --git a/python/test/unit/test_event_generators.py b/python/test/unit/test_event_generators.py index b43387029c..d3e352c524 100644 --- a/python/test/unit/test_event_generators.py +++ b/python/test/unit/test_event_generators.py @@ -5,6 +5,7 @@ import unittest import arbor as arb +from arbor import units as U """ all tests for event generators (regular, explicit, poisson) @@ -14,7 +15,7 @@ class TestEventGenerator(unittest.TestCase): def test_event_generator_regular_schedule(self): cm = arb.cell_local_label("tgt0") - rs = arb.regular_schedule(2.0, 1.0, 100.0) + rs = arb.regular_schedule(2.0 * U.ms, 1.0 * U.ms, 100.0 * U.ms) rg = arb.event_generator(cm, 3.14, rs) self.assertEqual(rg.target.label, "tgt0") self.assertEqual(rg.target.policy, arb.selection_policy.univalent) @@ -22,14 +23,14 @@ def test_event_generator_regular_schedule(self): def test_event_generator_explicit_schedule(self): cm = arb.cell_local_label("tgt1", arb.selection_policy.round_robin) - es = arb.explicit_schedule([0, 1, 2, 3, 4.4]) + es = arb.explicit_schedule([0 * U.ms, 1 * U.ms, 2 * U.ms, 3 * U.ms, 4.4 * U.ms]) eg = arb.event_generator(cm, -0.01, es) self.assertEqual(eg.target.label, "tgt1") self.assertEqual(eg.target.policy, arb.selection_policy.round_robin) self.assertAlmostEqual(eg.weight, -0.01) def test_event_generator_poisson_schedule(self): - ps = arb.poisson_schedule(0.0, 10.0, 0) + ps = arb.poisson_schedule(freq=10.0 * U.kHz, seed=0) pg = arb.event_generator("tgt2", 42.0, ps) self.assertEqual(pg.target.label, "tgt2") self.assertEqual(pg.target.policy, arb.selection_policy.univalent) diff --git a/python/test/unit/test_io.py b/python/test/unit/test_io.py index 7c2cdee510..dec6be09b5 100644 --- a/python/test/unit/test_io.py +++ b/python/test/unit/test_io.py @@ -10,7 +10,7 @@ acc = """(arbor-component (meta-data - (version "0.1-dev")) + (version "0.9-dev")) (cable-cell (morphology (branch 0 -1 @@ -25,25 +25,25 @@ (location 0 0.5))) (decor (default - (membrane-potential -40.000000)) + (membrane-potential -40.000000 (scalar 1))) (default - (ion-internal-concentration "ca" 0.000050)) + (ion-internal-concentration "ca" 0.000050 (scalar 1))) (default - (ion-external-concentration "ca" 2.000000)) + (ion-external-concentration "ca" 2.000000 (scalar 1))) (default - (ion-reversal-potential "ca" 132.457934)) + (ion-reversal-potential "ca" 132.457934 (scalar 1))) (default - (ion-internal-concentration "k" 54.400000)) + (ion-internal-concentration "k" 54.400000 (scalar 1))) (default - (ion-external-concentration "k" 2.500000)) + (ion-external-concentration "k" 2.500000 (scalar 1))) (default - (ion-reversal-potential "k" -77.000000)) + (ion-reversal-potential "k" -77.000000 (scalar 1))) (default - (ion-internal-concentration "na" 10.000000)) + (ion-internal-concentration "na" 10.000000 (scalar 1))) (default - (ion-external-concentration "na" 140.000000)) + (ion-external-concentration "na" 140.000000 (scalar 1))) (default - (ion-reversal-potential "na" 50.000000)) + (ion-reversal-potential "na" 50.000000 (scalar 1))) (paint (tag 1) (density diff --git a/python/test/unit/test_multiple_connections.py b/python/test/unit/test_multiple_connections.py index 7f8af4fcc4..b15a5d9236 100644 --- a/python/test/unit/test_multiple_connections.py +++ b/python/test/unit/test_multiple_connections.py @@ -6,7 +6,8 @@ import types import numpy as np -import arbor as arb +import arbor as A +from arbor import units as U from .. import fixtures """ @@ -38,7 +39,7 @@ def __init__(self, args): # Method creating a new mechanism for a synapse with STDP def create_syn_mechanism(self, scale_contrib=1): # create new synapse mechanism - syn_mechanism = arb.mechanism("expsyn_stdp") + syn_mechanism = A.mechanism("expsyn_stdp") # set pre- and postsynaptic contributions for STDP syn_mechanism.set("Apre", 0.01 * scale_contrib) @@ -89,9 +90,7 @@ def rr_main(self, context, art_spiker_recipe, weight, weight2): def cell_description(self, gid): # spike source neuron if gid < 3: - return arb.spike_source_cell( - "spike_source", arb.explicit_schedule(self.trains[gid]) - ) + return A.spike_source_cell("spike_source", self.schedule(gid)) # spike-receiving cable neuron elif gid == 3: @@ -101,16 +100,16 @@ def cell_description(self, gid): decor.place( '"midpoint"', - arb.synapse(create_syn_mechanism(scale_stdp)), + A.synapse(create_syn_mechanism(scale_stdp)), "postsyn_target", ) # place synapse for input from one presynaptic neuron at the center of the soma decor.place( '"midpoint"', - arb.synapse(create_syn_mechanism(scale_stdp)), + A.synapse(create_syn_mechanism(scale_stdp)), "postsyn_target", ) # place synapse for input from another presynaptic neuron at the center of the soma # (using the same label as above!) - return arb.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) art_spiker_recipe.cell_description = types.MethodType( cell_description, art_spiker_recipe @@ -140,15 +139,15 @@ def cell_description(self, gid): self.assertAlmostEqual(connections_from_recipe[3].delay, 1.4) # construct domain_decomposition and simulation object - sim = arb.simulation(art_spiker_recipe, context) - sim.record(arb.spike_recording.all) + sim = A.simulation(art_spiker_recipe, context) + sim.record(A.spike_recording.all) # create schedule and handle to record the membrane potential of neuron 3 - reg_sched = arb.regular_schedule(0, self.dt, self.runtime) + reg_sched = A.regular_schedule(0 * U.ms, self.dt * U.ms, self.runtime * U.ms) handle_mem = sim.sample((3, "Um"), reg_sched) # run the simulation - sim.run(self.runtime, self.dt) + sim.run(self.runtime * U.ms, self.dt * U.ms) return sim, handle_mem @@ -175,29 +174,29 @@ def connections_on(self, gid): # incoming to neuron 3 elif gid == 3: - source_label_0 = arb.cell_global_label( + source_label_0 = A.cell_global_label( 0, "spike_source" ) # referring to the "spike_source" label of neuron 0 - source_label_1 = arb.cell_global_label( + source_label_1 = A.cell_global_label( 1, "spike_source" ) # referring to the "spike_source" label of neuron 1 - target_label_rr = arb.cell_local_label( - "postsyn_target", arb.selection_policy.round_robin + target_label_rr = A.cell_local_label( + "postsyn_target", A.selection_policy.round_robin ) # referring to the current item in the "postsyn_target" label group of neuron 3, moving to the next item afterwards - conn_0_3_n1 = arb.connection( - source_label_0, target_label_rr, weight, 0.2 + conn_0_3_n1 = A.connection( + source_label_0, target_label_rr, weight, 0.2 * U.ms ) # first connection from neuron 0 to 3 - conn_0_3_n2 = arb.connection( - source_label_0, target_label_rr, weight, 0.2 + conn_0_3_n2 = A.connection( + source_label_0, target_label_rr, weight, 0.2 * U.ms ) # second connection from neuron 0 to 3 # NOTE: this is not connecting to the same target label item as 'conn_0_3_n1' because 'round_robin' has been used before! - conn_1_3_n1 = arb.connection( - source_label_1, target_label_rr, weight2, 1.4 + conn_1_3_n1 = A.connection( + source_label_1, target_label_rr, weight2, 1.4 * U.ms ) # first connection from neuron 1 to 3 - conn_1_3_n2 = arb.connection( - source_label_1, target_label_rr, weight2, 1.4 + conn_1_3_n2 = A.connection( + source_label_1, target_label_rr, weight2, 1.4 * U.ms ) # second connection from neuron 1 to 3 # NOTE: this is not connecting to the same target label item as 'conn_1_3_n1' because 'round_robin' has been used before! @@ -237,31 +236,31 @@ def connections_on(self, gid): # incoming to neuron 3 elif gid == 3: - source_label_0 = arb.cell_global_label( + source_label_0 = A.cell_global_label( 0, "spike_source" ) # referring to the "spike_source" label of neuron 0 - source_label_1 = arb.cell_global_label( + source_label_1 = A.cell_global_label( 1, "spike_source" ) # referring to the "spike_source" label of neuron 1 - target_label_rr_halt = arb.cell_local_label( - "postsyn_target", arb.selection_policy.round_robin_halt + target_label_rr_halt = A.cell_local_label( + "postsyn_target", A.selection_policy.round_robin_halt ) # referring to the current item in the "postsyn_target" label group of neuron 3 - target_label_rr = arb.cell_local_label( - "postsyn_target", arb.selection_policy.round_robin + target_label_rr = A.cell_local_label( + "postsyn_target", A.selection_policy.round_robin ) # referring to the current item in the "postsyn_target" label group of neuron 3, moving to the next item afterwards - conn_0_3_n1 = arb.connection( - source_label_0, target_label_rr_halt, weight, 0.2 + conn_0_3_n1 = A.connection( + source_label_0, target_label_rr_halt, weight, 0.2 * U.ms ) # first connection from neuron 0 to 3 - conn_0_3_n2 = arb.connection( - source_label_0, target_label_rr, weight, 0.2 + conn_0_3_n2 = A.connection( + source_label_0, target_label_rr, weight, 0.2 * U.ms ) # second connection from neuron 0 to 3 - conn_1_3_n1 = arb.connection( - source_label_1, target_label_rr_halt, weight2, 1.4 + conn_1_3_n1 = A.connection( + source_label_1, target_label_rr_halt, weight2, 1.4 * U.ms ) # first connection from neuron 1 to 3 - conn_1_3_n2 = arb.connection( - source_label_1, target_label_rr, weight2, 1.4 + conn_1_3_n2 = A.connection( + source_label_1, target_label_rr, weight2, 1.4 * U.ms ) # second connection from neuron 1 to 3 return [conn_0_3_n1, conn_0_3_n2, conn_1_3_n1, conn_1_3_n2] @@ -298,25 +297,25 @@ def connections_on(self, gid): # incoming to neuron 3 elif gid == 3: - source_label_0 = arb.cell_global_label( + source_label_0 = A.cell_global_label( 0, "spike_source" ) # referring to the "spike_source" label of neuron 0 - source_label_1 = arb.cell_global_label( + source_label_1 = A.cell_global_label( 1, "spike_source" ) # referring to the "spike_source" label of neuron 1 - target_label_uni_n1 = arb.cell_local_label( - "postsyn_target_1", arb.selection_policy.univalent + target_label_uni_n1 = A.cell_local_label( + "postsyn_target_1", A.selection_policy.univalent ) # referring to an only item in the "postsyn_target_1" label group of neuron 3 - target_label_uni_n2 = arb.cell_local_label( - "postsyn_target_2", arb.selection_policy.univalent + target_label_uni_n2 = A.cell_local_label( + "postsyn_target_2", A.selection_policy.univalent ) # referring to an only item in the "postsyn_target_2" label group of neuron 3 - conn_0_3 = arb.connection( - source_label_0, target_label_uni_n1, weight, 0.2 + conn_0_3 = A.connection( + source_label_0, target_label_uni_n1, weight, 0.2 * U.ms ) # connection from neuron 0 to 3 - conn_1_3 = arb.connection( - source_label_1, target_label_uni_n2, weight2, 1.4 + conn_1_3 = A.connection( + source_label_1, target_label_uni_n2, weight2, 1.4 * U.ms ) # connection from neuron 1 to 3 return [conn_0_3, conn_1_3] @@ -331,9 +330,7 @@ def connections_on(self, gid): def cell_description(self, gid): # spike source neuron if gid < 3: - return arb.spike_source_cell( - "spike_source", arb.explicit_schedule(self.trains[gid]) - ) + return A.spike_source_cell("spike_source", self.schedule(gid)) # spike-receiving cable neuron elif gid == 3: @@ -341,17 +338,17 @@ def cell_description(self, gid): decor.place( '"midpoint"', - arb.synapse(create_syn_mechanism()), + A.synapse(create_syn_mechanism()), "postsyn_target_1", ) # place synapse for input from one presynaptic neuron at the center of the soma decor.place( '"midpoint"', - arb.synapse(create_syn_mechanism()), + A.synapse(create_syn_mechanism()), "postsyn_target_2", ) # place synapse for input from another presynaptic neuron at the center of the soma # (using another label as above!) - return arb.cable_cell(tree, decor, labels) + return A.cable_cell(tree, decor, labels) art_spiker_recipe.cell_description = types.MethodType( cell_description, art_spiker_recipe @@ -371,15 +368,15 @@ def cell_description(self, gid): self.assertAlmostEqual(connections_from_recipe[1].delay, 1.4) # construct simulation object - sim = arb.simulation(art_spiker_recipe, context) - sim.record(arb.spike_recording.all) + sim = A.simulation(art_spiker_recipe, context) + sim.record(A.spike_recording.all) # create schedule and handle to record the membrane potential of neuron 3 - reg_sched = arb.regular_schedule(0, self.dt, self.runtime) + reg_sched = A.regular_schedule(0 * U.ms, self.dt * U.ms, self.runtime * U.ms) handle_mem = sim.sample((3, "Um"), reg_sched) # run the simulation - sim.run(self.runtime, self.dt) + sim.run(self.runtime * U.ms, self.dt * U.ms) # evaluate the outcome self.evaluate_outcome(sim, handle_mem) diff --git a/python/test/unit/test_probes.py b/python/test/unit/test_probes.py index 417ea1d51d..f72bfb4e3e 100644 --- a/python/test/unit/test_probes.py +++ b/python/test/unit/test_probes.py @@ -2,6 +2,7 @@ import unittest import arbor as A +from arbor import units as U import numpy as np """ @@ -22,7 +23,7 @@ def __init__(self): dec.place("(location 0 0.08)", A.synapse("expsyn"), "syn0") dec.place("(location 0 0.09)", A.synapse("exp2syn"), "syn1") - dec.place("(location 0 0.1)", A.iclamp(20.0), "iclamp") + dec.place("(location 0 0.1)", A.iclamp(20.0 * U.nA), "iclamp") dec.paint("(all)", A.density("hh")) self.cell = A.cable_cell(st, dec) @@ -184,9 +185,9 @@ def probes(self, gid): def cell_description(self, gid): cell = A.lif_cell("src", "tgt") - cell.E_L = -42 - cell.V_m = -23 - cell.t_ref = 0.2 + cell.E_L = -42 * U.mV + cell.V_m = -23 * U.mV + cell.t_ref = 0.2 * U.ms return cell @@ -202,8 +203,8 @@ def test_probe_addr_metadata(self): def test_probe_result(self): rec = lif_recipe() sim = A.simulation(rec) - hdl = sim.sample(0, "Um", A.regular_schedule(0.1)) - sim.run(1.0, 0.05) + hdl = sim.sample(0, "Um", A.regular_schedule(0.1 * U.ms)) + sim.run(1.0 * U.ms, 0.05 * U.ms) smp = sim.samples(hdl) exp = np.array( [ diff --git a/python/test/unit/test_profiling.py b/python/test/unit/test_profiling.py index 9249573c87..80a0f8d688 100644 --- a/python/test/unit/test_profiling.py +++ b/python/test/unit/test_profiling.py @@ -4,7 +4,8 @@ import unittest -import arbor as arb +import arbor as A +from arbor import units as U import functools """ @@ -30,17 +31,17 @@ def wrapped(*args, **kwargs): return inner_decorator -class a_recipe(arb.recipe): +class a_recipe(A.recipe): def __init__(self): - arb.recipe.__init__(self) - self.props = arb.neuron_cable_properties() + A.recipe.__init__(self) + self.props = A.neuron_cable_properties() self.trains = [[0.8, 2, 2.1, 3], [0.4, 2, 2.2, 3.1, 4.5], [0.2, 2, 2.8, 3]] def num_cells(self): return 3 def cell_kind(self, gid): - return arb.cell_kind.spike_source + return A.cell_kind.spike_source def connections_on(self, gid): return [] @@ -55,44 +56,45 @@ def probes(self, gid): return [] def cell_description(self, gid): - return arb.spike_source_cell("src", arb.explicit_schedule(self.trains[gid])) + sched = A.explicit_schedule([t * U.ms for t in self.trains[gid]]) + return A.spike_source_cell("src", sched) def skipWithoutSupport(): - return not bool(arb.config().get("profiling", False)) + return not bool(A.config().get("profiling", False)) class TestProfiling(unittest.TestCase): def test_support(self): - self.assertTrue("profiling" in arb.config(), "profiling key not in config") - profiling_support = arb.config()["profiling"] + self.assertTrue("profiling" in A.config(), "profiling key not in config") + profiling_support = A.config()["profiling"] self.assertEqual(bool, type(profiling_support), "profiling flag should be bool") if profiling_support: self.assertTrue( - hasattr(arb, "profiler_initialize"), + hasattr(A, "profiler_initialize"), "missing profiling interface with profiling support", ) self.assertTrue( - hasattr(arb, "profiler_summary"), + hasattr(A, "profiler_summary"), "missing profiling interface with profiling support", ) else: self.assertFalse( - hasattr(arb, "profiler_initialize"), + hasattr(A, "profiler_initialize"), "profiling interface without profiling support", ) self.assertFalse( - hasattr(arb, "profiler_summary"), + hasattr(A, "profiler_summary"), "profiling interface without profiling support", ) @lazy_skipIf(skipWithoutSupport, "run test only with profiling support") def test_summary(self): - context = arb.context() - arb.profiler_initialize(context) + context = A.context() + A.profiler_initialize(context) recipe = a_recipe() - dd = arb.partition_load_balance(recipe, context) - arb.simulation(recipe, context, dd).run(1) - summary = arb.profiler_summary() + dd = A.partition_load_balance(recipe, context) + A.simulation(recipe, context, dd).run(1 * U.ms) + summary = A.profiler_summary() self.assertEqual(str, type(summary), "profiler summary must be str") self.assertTrue(summary, "empty summary") diff --git a/python/test/unit/test_schedules.py b/python/test/unit/test_schedules.py index 0a7c747ec1..d23d81e2a0 100644 --- a/python/test/unit/test_schedules.py +++ b/python/test/unit/test_schedules.py @@ -4,7 +4,8 @@ import unittest -import arbor as arb +import arbor as A +from arbor import units as U """ all tests for schedules (regular, explicit, poisson) @@ -13,28 +14,28 @@ class TestRegularSchedule(unittest.TestCase): def test_none_ctor_regular_schedule(self): - rs = arb.regular_schedule(tstart=0, dt=0.1, tstop=None) - self.assertEqual(rs.dt, 0.1) + rs = A.regular_schedule(tstart=0 * U.ms, dt=0.1 * U.ms, tstop=None) + self.assertEqual(rs.dt, 0.1 * U.ms) def test_tstart_dt_tstop_ctor_regular_schedule(self): - rs = arb.regular_schedule(10.0, 1.0, 20.0) - self.assertEqual(rs.tstart, 10.0) - self.assertEqual(rs.dt, 1.0) - self.assertEqual(rs.tstop, 20.0) + rs = A.regular_schedule(10.0 * U.ms, 1.0 * U.ms, 20.0 * U.ms) + self.assertEqual(rs.tstart, 10.0 * U.ms) + self.assertEqual(rs.dt, 1.0 * U.ms) + self.assertEqual(rs.tstop, 20.0 * U.ms) def test_set_tstart_dt_tstop_regular_schedule(self): - rs = arb.regular_schedule(0.1) - self.assertAlmostEqual(rs.dt, 0.1, places=1) - rs.tstart = 17.0 - rs.dt = 0.5 - rs.tstop = 42.0 - self.assertEqual(rs.tstart, 17.0) - self.assertAlmostEqual(rs.dt, 0.5, places=1) - self.assertEqual(rs.tstop, 42.0) + rs = A.regular_schedule(0.1 * U.ms) + self.assertAlmostEqual(rs.dt.value_as(U.ms), 0.1, places=1) + rs.tstart = 17.0 * U.ms + rs.dt = 0.5 * U.ms + rs.tstop = 42.0 * U.ms + self.assertEqual(rs.tstart, 17.0 * U.ms) + self.assertAlmostEqual(rs.dt.value_as(U.ms), 0.5, places=1) + self.assertEqual(rs.tstop, 42.0 * U.ms) def test_events_regular_schedule(self): expected = [0, 0.25, 0.5, 0.75, 1.0] - rs = arb.regular_schedule(tstart=0.0, dt=0.25, tstop=1.25) + rs = A.regular_schedule(tstart=0.0 * U.ms, dt=0.25 * U.ms, tstop=1.25 * U.ms) self.assertEqual(expected, rs.events(0.0, 1.25)) self.assertEqual(expected, rs.events(0.0, 5.0)) self.assertEqual([], rs.events(5.0, 10.0)) @@ -43,41 +44,34 @@ def test_exceptions_regular_schedule(self): with self.assertRaisesRegex( RuntimeError, "tstart must be a non-negative number" ): - arb.regular_schedule(tstart=-1.0, dt=0.1) + A.regular_schedule(tstart=-1.0 * U.ms, dt=0.1 * U.ms) with self.assertRaisesRegex(RuntimeError, "dt must be a positive number"): - arb.regular_schedule(dt=-0.1) + A.regular_schedule(dt=-0.1 * U.ms) with self.assertRaisesRegex(RuntimeError, "dt must be a positive number"): - arb.regular_schedule(dt=0) + A.regular_schedule(dt=0 * U.ms) with self.assertRaises(TypeError): - arb.regular_schedule(dt=None) + A.regular_schedule(dt=None) with self.assertRaises(TypeError): - arb.regular_schedule(dt="dt") - with self.assertRaisesRegex( - RuntimeError, "tstop must be a non-negative number, or None" - ): - arb.regular_schedule(tstart=0, dt=0.1, tstop="tstop") + A.regular_schedule(dt="dt") + with self.assertRaises(TypeError): + A.regular_schedule(tstart=0 * U.ms, dt=0.1 * U.ms, tstop="tstop") with self.assertRaisesRegex(RuntimeError, "t0 must be a non-negative number"): - rs = arb.regular_schedule(0.0, 1.0, 10.0) + rs = A.regular_schedule(0.0 * U.ms, 1.0 * U.ms, 10.0 * U.ms) rs.events(-1, 0) with self.assertRaisesRegex(RuntimeError, "t1 must be a non-negative number"): - rs = arb.regular_schedule(0.0, 1.0, 10.0) + rs = A.regular_schedule(0.0 * U.ms, 1.0 * U.ms, 10.0 * U.ms) rs.events(0, -10) class TestExplicitSchedule(unittest.TestCase): def test_times_contor_explicit_schedule(self): - es = arb.explicit_schedule([1, 2, 3, 4.5]) - self.assertEqual(es.times, [1, 2, 3, 4.5]) - - def test_set_times_explicit_schedule(self): - es = arb.explicit_schedule() - es.times = [42, 43, 44, 55.5, 100] - self.assertEqual(es.times, [42, 43, 44, 55.5, 100]) + es = A.explicit_schedule([t * U.ms for t in range(1, 6)]) + self.assertEqual(es.events(0, 1000000), [1, 2, 3, 4, 5]) def test_events_explicit_schedule(self): times = [0.1, 0.3, 1.0, 2.2, 1.25, 1.7] expected = [0.1, 0.3, 1.0] - es = arb.explicit_schedule(times) + es = A.explicit_schedule([t * U.ms for t in times]) for i in range(len(expected)): self.assertAlmostEqual(expected[i], es.events(0.0, 1.25)[i], places=2) expected = [0.3, 1.0, 1.25, 1.7] @@ -85,47 +79,47 @@ def test_events_explicit_schedule(self): self.assertAlmostEqual(expected[i], es.events(0.3, 1.71)[i], places=2) def test_exceptions_explicit_schedule(self): - with self.assertRaisesRegex( - RuntimeError, "explicit time schedule cannot contain negative values" - ): - arb.explicit_schedule([-1]) + with self.assertRaises(RuntimeError): + A.explicit_schedule([-1 * U.ms]) with self.assertRaises(TypeError): - arb.explicit_schedule(["times"]) + A.explicit_schedule(["times"]) with self.assertRaises(TypeError): - arb.explicit_schedule([None]) + A.explicit_schedule([None]) with self.assertRaises(TypeError): - arb.explicit_schedule([[1, 2, 3]]) + A.explicit_schedule([[1, 2, 3]]) with self.assertRaisesRegex(RuntimeError, "t1 must be a non-negative number"): - rs = arb.regular_schedule(0.1) + rs = A.regular_schedule(0.1 * U.ms) rs.events(1.0, -1.0) class TestPoissonSchedule(unittest.TestCase): def test_freq_poisson_schedule(self): - ps = arb.poisson_schedule(42.0) - self.assertEqual(ps.freq, 42.0) + ps = A.poisson_schedule(42.0 * U.kHz) + self.assertEqual(ps.freq, 42.0 * U.kHz) def test_freq_tstart_contor_poisson_schedule(self): - ps = arb.poisson_schedule(freq=5.0, tstart=4.3) - self.assertEqual(ps.freq, 5.0) - self.assertEqual(ps.tstart, 4.3) + ps = A.poisson_schedule(freq=5.0 * U.kHz, tstart=4.3 * U.ms) + self.assertEqual(ps.freq, 5.0 * U.kHz) + self.assertEqual(ps.tstart, 4.3 * U.ms) def test_freq_seed_contor_poisson_schedule(self): - ps = arb.poisson_schedule(freq=5.0, seed=42) - self.assertEqual(ps.freq, 5.0) + ps = A.poisson_schedule(freq=5.0 * U.kHz, seed=42) + self.assertEqual(ps.freq, 5.0 * U.kHz) self.assertEqual(ps.seed, 42) def test_tstart_freq_seed_contor_poisson_schedule(self): - ps = arb.poisson_schedule(10.0, 100.0, 1000) - self.assertEqual(ps.tstart, 10.0) - self.assertEqual(ps.freq, 100.0) + ps = A.poisson_schedule(tstart=10.0 * U.ms, freq=100.0 * U.kHz, seed=1000) + self.assertEqual(ps.tstart, 10.0 * U.ms) + self.assertEqual(ps.freq, 100.0 * U.kHz) self.assertEqual(ps.seed, 1000) def test_events_poisson_schedule(self): expected = [17.4107, 502.074, 506.111, 597.116] - ps = arb.poisson_schedule(0.0, 0.01, 0) + ps = A.poisson_schedule(tstart=0.0 * U.ms, freq=0.01 * U.kHz, seed=0) for i in range(len(expected)): - self.assertAlmostEqual(expected[i], ps.events(0.0, 600.0)[i], places=3) + self.assertAlmostEqual( + expected[i], ps.events(0.0 * U.ms, 600.0 * U.ms)[i], places=3 + ) expected = [ 5030.22, 5045.75, @@ -140,50 +134,52 @@ def test_events_poisson_schedule(self): 5808.33, ] for i in range(len(expected)): - self.assertAlmostEqual(expected[i], ps.events(5000.0, 6000.0)[i], places=2) + self.assertAlmostEqual( + expected[i], ps.events(5000.0 * U.ms, 6000.0 * U.ms)[i], places=2 + ) def test_exceptions_poisson_schedule(self): with self.assertRaises(TypeError): - arb.poisson_schedule() + A.poisson_schedule() with self.assertRaises(TypeError): - arb.poisson_schedule(tstart=10.0) + A.poisson_schedule(tstart=10.0 * U.ms) with self.assertRaises(TypeError): - arb.poisson_schedule(seed=1432) + A.poisson_schedule(seed=1432) with self.assertRaisesRegex( RuntimeError, "tstart must be a non-negative number" ): - arb.poisson_schedule(freq=34.0, tstart=-10.0) + A.poisson_schedule(freq=34.0 * U.kHz, tstart=-10.0 * U.ms) with self.assertRaises(TypeError): - arb.poisson_schedule(freq=34.0, tstart=None) + A.poisson_schedule(freq=34.0 * U.kHz, tstart=None) with self.assertRaises(TypeError): - arb.poisson_schedule(freq=34.0, tstart="tstart") + A.poisson_schedule(freq=34.0, tstart="tstart") with self.assertRaisesRegex( RuntimeError, "frequency must be a non-negative number" ): - arb.poisson_schedule(freq=-100.0) + A.poisson_schedule(freq=-100.0 * U.kHz) with self.assertRaises(TypeError): - arb.poisson_schedule(freq="freq") + A.poisson_schedule(freq="freq") with self.assertRaises(TypeError): - arb.poisson_schedule(freq=34.0, seed=-1) + A.poisson_schedule(freq=34.0 * U.kHz, seed=-1) with self.assertRaises(TypeError): - arb.poisson_schedule(freq=34.0, seed=10.0) + A.poisson_schedule(freq=34.0 * U.kHz, seed=10.0) with self.assertRaises(TypeError): - arb.poisson_schedule(freq=34.0, seed="seed") + A.poisson_schedule(freq=34.0 * U.kHz, seed="seed") with self.assertRaises(TypeError): - arb.poisson_schedule(freq=34.0, seed=None) + A.poisson_schedule(freq=34.0 * U.kHz, seed=None) with self.assertRaisesRegex(RuntimeError, "t0 must be a non-negative number"): - ps = arb.poisson_schedule(0, 0.01) - ps.events(-1.0, 1.0) + ps = A.poisson_schedule(tstart=0 * U.ms, freq=0.01 * U.kHz) + ps.events(-1.0 * U.ms, 1.0 * U.ms) with self.assertRaisesRegex(RuntimeError, "t1 must be a non-negative number"): - ps = arb.poisson_schedule(0, 0.01) - ps.events(1.0, -1.0) - with self.assertRaisesRegex( - RuntimeError, "tstop must be a non-negative number, or None" - ): - arb.poisson_schedule(0, 0.1, tstop="tstop") - ps.events(1.0, -1.0) + ps = A.poisson_schedule(tstart=0 * U.ms, freq=0.01 * U.kHz) + ps.events(1.0 * U.ms, -1.0 * U.ms) + with self.assertRaises(TypeError): + ps = A.poisson_schedule(tstart=0 * U.ms, freq=0.1 * U.kHz, tstop="tstop") + ps.events(1.0 * U.ms, -1.0 * U.ms) def test_tstop_poisson_schedule(self): tstop = 50 - events = arb.poisson_schedule(0.0, 1, 0, tstop).events(0, 100) + events = A.poisson_schedule( + tstart=0.0 * U.ms, freq=1 * U.kHz, seed=0, tstop=tstop * U.ms + ).events(0 * U.ms, 100 * U.ms) self.assertTrue(max(events) < tstop) diff --git a/python/test/unit/test_spikes.py b/python/test/unit/test_spikes.py index cd1e6b81e5..146a3a0acd 100644 --- a/python/test/unit/test_spikes.py +++ b/python/test/unit/test_spikes.py @@ -4,6 +4,7 @@ import unittest import arbor as A +from arbor import units as U from .. import fixtures """ @@ -18,11 +19,11 @@ def test_spikes_sorted(self, art_spiking_sim): sim = art_spiking_sim sim.record(A.spike_recording.all) # run simulation in 5 steps, forcing 5 epochs - sim.run(1, 0.01) - sim.run(2, 0.01) - sim.run(3, 0.01) - sim.run(4, 0.01) - sim.run(5, 0.01) + sim.run(1 * U.ms, 0.01 * U.ms) + sim.run(2 * U.ms, 0.01 * U.ms) + sim.run(3 * U.ms, 0.01 * U.ms) + sim.run(4 * U.ms, 0.01 * U.ms) + sim.run(5 * U.ms, 0.01 * U.ms) spikes = sim.spikes() times = spikes["time"].tolist() diff --git a/python/units.cpp b/python/units.cpp new file mode 100644 index 0000000000..53df38aa41 --- /dev/null +++ b/python/units.cpp @@ -0,0 +1,129 @@ +#include +#include + +#include + +namespace pyarb { + +namespace py = pybind11; + +void register_units(py::module& m) { + using namespace py::literals; + + auto u = m.def_submodule("units", "Units and quantities for driving the user interface."); + + py::class_ unit(u, "unit", "A unit."); + py::class_ quantity(u, "quantity", "A quantity, comprising a magnitude and a unit."); + + unit + .def(py::self * py::self) + .def(py::self == py::self) + .def(py::self != py::self) + .def(py::self / py::self) + .def(py::self * double()) + .def(py::self / double()) + .def(double() * py::self) + .def(double() / py::self) + .def("__pow__", [](const arb::units::unit &b, int e) { return b.pow(e); }, py::is_operator()) + .def("__str__", + [](const arb::units::unit& u) { return arb::units::to_string(u); }, + "Convert unit to string.") + .def("__repr__", + [](const arb::units::unit& u) { return arb::units::to_string(u); }, + "Convert unit to string."); + + quantity + .def(py::self * py::self) + .def(py::self / py::self) + .def(py::self == py::self) + .def(py::self != py::self) + .def(py::self + py::self) + .def(py::self - py::self) + .def(py::self * double()) + .def(py::self / double()) + .def(double() * py::self) + .def(double() / py::self) + .def(py::self * arb::units::unit()) + .def(py::self / arb::units::unit()) + .def("__pow__", [](const arb::units::unit &b, int e) { return b.pow(e); }, py::is_operator()) + .def("value_as", + [](const arb::units::quantity& q, const arb::units::unit& u) { return q.value_as(u); }, + "unit"_a, + "Convert quantity to given unit and return magnitude.") + .def_property_readonly("value", + [](const arb::units::quantity& q) { return q.value(); }, + "Return magnitude.") + .def_property_readonly("units", + [](const arb::units::quantity& q) { return q.units(); }, + "Return units.") + .def("__str__", + [](const arb::units::quantity& q) { return arb::units::to_string(q); }, + "Convert quantity to string.") + .def("__repr__", + [](const arb::units::quantity& q) { return arb::units::to_string(q); }, + "Convert quantity to string."); + + u.attr("m") = py::cast(arb::units::m); + u.attr("cm") = py::cast(arb::units::cm); + u.attr("mm") = py::cast(arb::units::mm); + u.attr("um") = py::cast(arb::units::um); + u.attr("nm") = py::cast(arb::units::nm); + + u.attr("m2") = py::cast(arb::units::m2); + u.attr("cm2") = py::cast(arb::units::cm2); + u.attr("mm2") = py::cast(arb::units::mm2); + u.attr("um2") = py::cast(arb::units::um2); + u.attr("nm2") = py::cast(arb::units::nm2); + + u.attr("s") = py::cast(arb::units::s); + u.attr("ms") = py::cast(arb::units::ms); + u.attr("us") = py::cast(arb::units::us); + u.attr("ns") = py::cast(arb::units::ns); + u.attr("Hz") = py::cast(arb::units::Hz); + u.attr("kHz") = py::cast(arb::units::kHz); + + u.attr("Ohm") = py::cast(arb::units::Ohm); + u.attr("kOhm") = py::cast(arb::units::kOhm); + u.attr("MOhm") = py::cast(arb::units::MOhm); + + u.attr("S") = py::cast(arb::units::S); + u.attr("mS") = py::cast(arb::units::mS); + u.attr("uS") = py::cast(arb::units::uS); + + u.attr("F") = py::cast(arb::units::F); + u.attr("uF") = py::cast(arb::units::uF); + u.attr("nF") = py::cast(arb::units::nF); + u.attr("pF") = py::cast(arb::units::pF); + + u.attr("A") = py::cast(arb::units::A); + u.attr("mA") = py::cast(arb::units::mA); + u.attr("uA") = py::cast(arb::units::uA); + u.attr("nA") = py::cast(arb::units::nA); + u.attr("pA") = py::cast(arb::units::pA); + + u.attr("V") = py::cast(arb::units::V); + u.attr("mV") = py::cast(arb::units::mV); + + u.attr("C") = py::cast(arb::units::C); + + u.attr("rad") = py::cast(arb::units::rad); + u.attr("deg") = py::cast(arb::units::deg); + + u.attr("Kelvin") = py::cast(arb::units::Kelvin); + u.attr("Celsius") = py::cast(arb::units::Celsius); + + u.attr("mol") = py::cast(arb::units::mol); + u.attr("M") = py::cast(arb::units::M); + u.attr("mM") = py::cast(arb::units::mM); + + u.attr("pico") = py::cast(arb::units::pico); + u.attr("nano") = py::cast(arb::units::nano); + u.attr("micro") = py::cast(arb::units::micro); + u.attr("milli") = py::cast(arb::units::milli); + u.attr("kilo") = py::cast(arb::units::kilo); + u.attr("mega") = py::cast(arb::units::mega); + u.attr("giga") = py::cast(arb::units::giga); + + u.attr("nil") = py::cast(arb::units::nil); +} +} // pyarb diff --git a/python/util.hpp b/python/util.hpp index 846b62cc18..b111d4eafc 100644 --- a/python/util.hpp +++ b/python/util.hpp @@ -3,10 +3,11 @@ #include #include +#include + #include #include "strprintf.hpp" -#include "error.hpp" namespace pyarb { namespace util { diff --git a/spack/package.py b/spack/package.py index 50c1c5ea2b..460dc48f75 100644 --- a/spack/package.py +++ b/spack/package.py @@ -14,13 +14,15 @@ class Arbor(CMakePackage, CudaPackage): git = "https://github.com/arbor-sim/arbor.git" url = "https://github.com/arbor-sim/arbor/releases/download/v0.8.1/arbor-v0.9.0-full.tar.gz" maintainers = ("thorstenhater", "brenthuisman", "haampie") + submodules = True - version("master", branch="master") - version("develop") + version("master", branch="master", submodules=True) + version("develop", branch="master", submodules=True) version( "0.9.0", sha256="5f9740955c821aca81e23298c17ad64f33f635756ad9b4a0c1444710f564306a", url="https://github.com/arbor-sim/arbor/releases/download/v0.9.0/arbor-v0.9.0-full.tar.gz", + submodules=True, ) version( "0.8.1", @@ -132,6 +134,8 @@ def cmake_args(self): self.spec.compiler.name, self.spec.compiler.version ) args.append("-DARB_CXX_FLAGS_TARGET=" + opt_flags) + # Needed, spack has no units package + args.append("-DARB_USE_BUNDLED_UNITS=ON") return args diff --git a/test/common_cells.cpp b/test/common_cells.cpp index 5d1f3aa2bc..903e4e0a43 100644 --- a/test/common_cells.cpp +++ b/test/common_cells.cpp @@ -1,5 +1,4 @@ #include -#include "arbor/morph/morphology.hpp" #include "common_cells.hpp" namespace arb { @@ -181,7 +180,9 @@ cable_cell_description make_cell_soma_only(bool with_stim) { auto c = builder.make_cell(); c.decorations.paint("soma"_lab, density("hh")); if (with_stim) { - c.decorations.place(builder.location({0,0.5}), i_clamp{10., 100., 0.1}, "cc"); + c.decorations.place(builder.location({0,0.5}), + i_clamp::box(10.*arb::units::ms, 100.*arb::units::ms, 0.1*arb::units::nA), + "cc"); } return {c.morph, c.labels, c.decorations}; @@ -216,7 +217,9 @@ cable_cell_description make_cell_ball_and_stick(bool with_stim) { c.decorations.paint("soma"_lab, density("hh")); c.decorations.paint("dend"_lab, density("pas")); if (with_stim) { - c.decorations.place(builder.location({1,1}), i_clamp{5, 80, 0.3}, "cc"); + c.decorations.place(builder.location({1,1}), + i_clamp::box(5*arb::units::ms, 80*arb::units::ms, 0.3*arb::units::nA), + "cc"); } return {c.morph, c.labels, c.decorations}; @@ -254,8 +257,12 @@ cable_cell_description make_cell_ball_and_3stick(bool with_stim) { c.decorations.paint("soma"_lab, density("hh")); c.decorations.paint("dend"_lab, density("pas")); if (with_stim) { - c.decorations.place(builder.location({2,1}), i_clamp{5., 80., 0.45}, "cc0"); - c.decorations.place(builder.location({3,1}), i_clamp{40., 10.,-0.2}, "cc1"); + c.decorations.place(builder.location({2,1}), + i_clamp::box(5.*arb::units::ms, 80.*arb::units::ms, 0.45*arb::units::nA), + "cc0"); + c.decorations.place(builder.location({3,1}), + i_clamp::box(40.*arb::units::ms, 10.*arb::units::ms,-0.2*arb::units::nA), + "cc1"); } return {c.morph, c.labels, c.decorations}; diff --git a/test/common_cells.hpp b/test/common_cells.hpp index 9b22af70e7..6e9cda85a7 100644 --- a/test/common_cells.hpp +++ b/test/common_cells.hpp @@ -1,5 +1,3 @@ -#include - #include #include #include diff --git a/test/simple_recipes.hpp b/test/simple_recipes.hpp index 504d54d9dc..52b6638f41 100644 --- a/test/simple_recipes.hpp +++ b/test/simple_recipes.hpp @@ -10,13 +10,14 @@ #include #include #include +#include #include -#include "util/rangeutil.hpp" - namespace arb { +namespace U = units; + // Common functionality: maintain an unordered map of probe data // per gid, built with `add_probe()`. @@ -48,7 +49,7 @@ class simple_recipe_base: public recipe { } void add_ion(const std::string& ion_name, int charge, double init_iconc, double init_econc, double init_revpot) { - cell_gprop_.add_ion(ion_name, charge, init_iconc, init_econc, init_revpot); + cell_gprop_.add_ion(ion_name, charge, init_iconc*U::mM, init_econc*U::mM, init_revpot*U::mV); } void nernst_ion(const std::string& ion_name) { diff --git a/test/ubench/merge.cpp b/test/ubench/merge.cpp index 1097fa7341..c179ff2f89 100644 --- a/test/ubench/merge.cpp +++ b/test/ubench/merge.cpp @@ -1,5 +1,3 @@ -#include -#include #include #include @@ -10,20 +8,18 @@ #include #include -constexpr auto T = 1000.0; // ms - -using rndgen = std::mt19937_64; +constexpr auto T = 1000.0*arb::units::ms; // ms struct payload { payload(std::size_t ncells, std::size_t ev_per_cell) { auto dt = T/ev_per_cell; for(auto cell = 0ull; cell < ncells; ++cell) { - auto gen = arb::poisson_schedule(1/dt, rndgen{cell}); - auto times = gen.events(0, T); + auto gen = arb::poisson_schedule(1/dt, cell); + auto times = gen.events(0, T.value_as(arb::units::ms)); evts.emplace_back(); auto& evt = evts.back(); for (auto t: arb::util::make_range(times)) { - evt.emplace_back(arb::spike_event{42, t, 0.23}); + evt.emplace_back(42, t, 0.23); ++size; } span.emplace_back(arb::util::make_range(evt.data(), evt.data() + evt.size())); diff --git a/test/unit-distributed/test_communicator.cpp b/test/unit-distributed/test_communicator.cpp index 9f06a3dda9..0100a30e0b 100644 --- a/test/unit-distributed/test_communicator.cpp +++ b/test/unit-distributed/test_communicator.cpp @@ -1,7 +1,6 @@ #include #include "test.hpp" -#include #include #include @@ -11,7 +10,6 @@ #include #include "communication/communicator.hpp" -#include "execution_context.hpp" #include "fvm_lowered_cell.hpp" #include "lif_cell_group.hpp" #include "cable_cell_group.hpp" @@ -24,6 +22,7 @@ #endif using namespace arb; +namespace U = arb::units; TEST(communicator, policy_basics) { @@ -204,7 +203,7 @@ namespace { tree.append(arb::mnpos, {0, 0, 0.0, 1.0}, {0, 0, 200, 1.0}, 1); arb::decor decor; decor.set_default(arb::cv_policy_fixed_per_branch(10)); - decor.place(arb::mlocation{0, 0.5}, arb::threshold_detector{10}, "src"); + decor.place(arb::mlocation{0, 0.5}, arb::threshold_detector{10*arb::units::mV}, "src"); decor.place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "tgt"); return arb::cable_cell(arb::morphology(tree), decor); } @@ -221,10 +220,9 @@ namespace { // delay is 1 cell_global_label_type src = {gid==0? size_-1: gid-1, "src"}; cell_local_label_type dst = {"tgt"}; - return {cell_connection( - src, dst, // end points - float(gid), // weight - 1.0f)}; // delay + return {cell_connection(src, dst, // end points + float(gid), // weight + 1.0*U::ms)}; // delay } std::any get_global_properties(arb::cell_kind kind) const override { @@ -277,7 +275,7 @@ namespace { tree.append(arb::mnpos, {0, 0, 0.0, 1.0}, {0, 0, 200, 1.0}, 1); arb::decor decor; decor.set_default(arb::cv_policy_fixed_per_branch(10)); - decor.place(arb::mlocation{0, 0.5}, arb::threshold_detector{10}, "src"); + decor.place(arb::mlocation{0, 0.5}, arb::threshold_detector{10*arb::units::mV}, "src"); decor.place(arb::ls::uniform(arb::reg::all(), 0, size_, gid), arb::synapse("expsyn"), "tgt"); return arb::cable_cell(arb::morphology(tree), decor); } @@ -289,12 +287,10 @@ namespace { std::vector cons; cons.reserve(size_); for (auto sid: util::make_span(0, size_)) { - cell_connection con( - {sid, {"src", arb::lid_selection_policy::round_robin}}, // source - {"tgt", arb::lid_selection_policy::round_robin}, // destination - float(gid+sid), // weight - 1.0f); // delay - cons.push_back(con); + cons.emplace_back(cell_connection{{sid, {"src", arb::lid_selection_policy::round_robin}}, // source + {"tgt", arb::lid_selection_policy::round_robin}, // destination + float(gid+sid), // weight + 1.0f*U::ms}); // delay } return cons; } @@ -354,8 +350,8 @@ namespace { decor.place(arb::ls::uniform(arb::reg::all(), 2, 2, gid), arb::synapse("expsyn"), "synapses_1"); } else { - decor.place(arb::ls::uniform(arb::reg::all(), 0, 2, gid), arb::threshold_detector{10}, "detectors_0"); - decor.place(arb::ls::uniform(arb::reg::all(), 3, 3, gid), arb::threshold_detector{10}, "detectors_1"); + decor.place(arb::ls::uniform(arb::reg::all(), 0, 2, gid), arb::threshold_detector{10*arb::units::mV}, "detectors_0"); + decor.place(arb::ls::uniform(arb::reg::all(), 3, 3, gid), arb::threshold_detector{10*arb::units::mV}, "detectors_1"); } return arb::cable_cell(arb::morphology(tree), decor); } @@ -402,20 +398,17 @@ namespace { if (gid%3 != 1) { for (auto sid: util::make_span(0, ncells_)) { if (sid%3 == 1) { - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - - cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_1", pol::assert_univalent}, 1.0, 1.0}); - - cons.push_back({{sid, "detectors_1", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - cons.push_back({{sid, "detectors_1", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0}); - - cons.push_back({{sid, "detectors_1", pol::assert_univalent}, {"synapses_1", pol::round_robin}, 1.0, 1.0}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_0", pol::round_robin}, {"synapses_1", pol::assert_univalent}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_1", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_1", pol::round_robin}, {"synapses_0", pol::round_robin}, 1.0, 1.0*U::ms}); + cons.push_back({{sid, "detectors_1", pol::assert_univalent}, {"synapses_1", pol::round_robin}, 1.0, 1.0*U::ms}); } } } diff --git a/test/unit-distributed/test_domain_decomposition.cpp b/test/unit-distributed/test_domain_decomposition.cpp index 40e76c6dc8..4c4ccea430 100644 --- a/test/unit-distributed/test_domain_decomposition.cpp +++ b/test/unit-distributed/test_domain_decomposition.cpp @@ -1,10 +1,5 @@ #include -#include -#include -#include -#include -#include #include #include @@ -16,9 +11,7 @@ #include #include "util/span.hpp" - #include "../simple_recipes.hpp" -#include "test.hpp" #ifdef TEST_MPI #include diff --git a/test/unit/test_cable_cell.cpp b/test/unit/test_cable_cell.cpp index 8b0a4d460d..7e8930fee0 100644 --- a/test/unit/test_cable_cell.cpp +++ b/test/unit/test_cable_cell.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -35,9 +36,9 @@ TEST(cable_cell, lid_ranges) { // Note: there are 2 terminal points. decorations.place("term"_lab, synapse("expsyn"), "t0"); decorations.place("term"_lab, synapse("expsyn"), "t1"); - decorations.place("term"_lab, threshold_detector{-10}, "s0"); + decorations.place("term"_lab, threshold_detector{-10*arb::units::mV}, "s0"); decorations.place(empty_sites, synapse("expsyn"), "t2"); - decorations.place("term"_lab, threshold_detector{-20}, "s1"); + decorations.place("term"_lab, threshold_detector{-20*arb::units::mV}, "s1"); decorations.place(three_sites, synapse("expsyn"), "t3"); decorations.place("term"_lab, synapse("exp2syn"), "t3"); diff --git a/test/unit/test_cable_cell_group.cpp b/test/unit/test_cable_cell_group.cpp index f143df0a25..cbb3871454 100644 --- a/test/unit/test_cable_cell_group.cpp +++ b/test/unit/test_cable_cell_group.cpp @@ -7,7 +7,6 @@ #include "epoch.hpp" #include "fvm_lowered_cell.hpp" #include "cable_cell_group.hpp" -#include "util/rangeutil.hpp" #include "common.hpp" #include "../common_cells.hpp" @@ -29,8 +28,8 @@ namespace { auto d = builder.make_cell(); d.decorations.paint("soma"_lab, density("hh")); d.decorations.paint("dend"_lab, density("pas")); - d.decorations.place(builder.location({1,1}), i_clamp::box(5, 80, 0.3), "clamp0"); - d.decorations.place(builder.location({0, 0}), threshold_detector{0}, "detector0"); + d.decorations.place(builder.location({1,1}), i_clamp::box(5*arb::units::ms, 80*arb::units::ms, 0.3*arb::units::nA), "clamp0"); + d.decorations.place(builder.location({0, 0}), threshold_detector{0*arb::units::mV}, "detector0"); return d; } } @@ -72,7 +71,7 @@ TEST(cable_cell_group, sources) { for (int i=0; i<20; ++i) { auto desc = make_cell(); if (i==0 || i==3 || i==17) { - desc.decorations.place(mlocation{0, 0.3}, threshold_detector{2.3}, "detector1"); + desc.decorations.place(mlocation{0, 0.3}, threshold_detector{2.3*arb::units::mV}, "detector1"); } cells.emplace_back(desc); diff --git a/test/unit/test_cable_cell_group_gpu.cpp b/test/unit/test_cable_cell_group_gpu.cpp index 3f5fbfdaad..d2dcff02fc 100644 --- a/test/unit/test_cable_cell_group_gpu.cpp +++ b/test/unit/test_cable_cell_group_gpu.cpp @@ -15,6 +15,8 @@ using namespace arb; using namespace arborio::literals; +namespace U = arb::units; + namespace { cable_cell_description make_cell() { soma_cell_builder builder(12.6157/2.0); @@ -22,8 +24,8 @@ namespace { auto d = builder.make_cell(); d.decorations.paint("soma"_lab, density("hh")); d.decorations.paint("dend"_lab, density("pas")); - d.decorations.place(builder.location({1,1}), i_clamp::box(5, 80, 0.3), "clamp0"); - d.decorations.place(builder.location({0, 0}), threshold_detector{0}, "detector0"); + d.decorations.place(builder.location({1,1}), i_clamp::box(5*U::ms, 80*U::ms, 0.3*U::nA), "clamp0"); + d.decorations.place(builder.location({0, 0}), threshold_detector{0*U::mV}, "detector0"); return d; } } diff --git a/test/unit/test_cv_layout.cpp b/test/unit/test_cv_layout.cpp index 8852f61aba..67512a8868 100644 --- a/test/unit/test_cv_layout.cpp +++ b/test/unit/test_cv_layout.cpp @@ -6,15 +6,16 @@ #include #include +#include + #include "fvm_layout.hpp" #include "util/span.hpp" -#include "common.hpp" #include "common_morphologies.hpp" #include "../common_cells.hpp" using namespace arb; -using util::make_span; +namespace U = arb::units; TEST(cv_layout, empty) { using namespace common_morphology; @@ -92,9 +93,9 @@ TEST(cv_layout, cable) { params.init_membrane_potential = 0; decor decs; - decs.paint(reg::cable(0, 0.0, 0.2), init_membrane_potential{10}); - decs.paint(reg::cable(0, 0.2, 0.7), init_membrane_potential{20}); - decs.paint(reg::cable(0, 0.7, 1.0), init_membrane_potential{30}); + decs.paint(reg::cable(0, 0.0, 0.2), init_membrane_potential{10*U::mV}); + decs.paint(reg::cable(0, 0.2, 0.7), init_membrane_potential{20*U::mV}); + decs.paint(reg::cable(0, 0.7, 1.0), init_membrane_potential{30*U::mV}); cable_cell c(morph, decs); params.discretization = cv_policy_explicit(ls::nil()); diff --git a/test/unit/test_diffusion.cpp b/test/unit/test_diffusion.cpp index 348b135ca8..54529cd5a7 100644 --- a/test/unit/test_diffusion.cpp +++ b/test/unit/test_diffusion.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -26,6 +25,7 @@ using namespace std::string_literals; using namespace arborio::literals; using namespace arb; +namespace U = arb::units; constexpr double epsilon = 1e-6; #ifdef ARB_GPU_ENABLED @@ -55,7 +55,7 @@ struct linear: public recipe { std::vector event_generators(arb::cell_gid_type gid) const override { std::vector result; for (const auto& [t, w]: inject_at) { - result.push_back(arb::explicit_generator({"Zap"}, w, std::vector{t})); + result.push_back(arb::explicit_generator_from_milliseconds({"Zap"}, w, std::vector{t})); } return result; } @@ -72,13 +72,13 @@ struct linear: public recipe { linear& add_inject() { decor.place("(location 0 0.5)"_ls, arb::synapse("inject/x=na", {{"alpha", 200.0*cv_length}}), "Zap"); return *this; } linear& add_event(double t, float w) { inject_at.push_back({t, w}); return *this; } linear& set_diffusivity(double d, std::optional rg = {}) { - if (rg) decor.paint(*rg, ion_diffusivity{"na", d}); - else decor.set_default(ion_diffusivity{"na", d}); + if (rg) decor.paint(*rg, ion_diffusivity{"na", d*U::m2/U::s}); + else decor.set_default(ion_diffusivity{"na", d*U::m2/U::s}); return *this; } linear& set_concentration(double d, std::optional rg = {}) { - if (rg) decor.paint(*rg, init_int_concentration{"na", d}); - else decor.set_default(init_int_concentration{"na", d}); + if (rg) decor.paint(*rg, init_int_concentration{"na", d*U::mM}); + else decor.set_default(init_int_concentration{"na", d*U::mM}); return *this; } }; @@ -117,8 +117,8 @@ testing::AssertionResult run(const linear& rec, const result_t exp) { }; auto ctx = make_context({arbenv::default_concurrency(), with_gpu}); auto sim = simulation{rec, ctx, partition_load_balance(rec, ctx)}; - sim.add_sampler(arb::all_probes, arb::regular_schedule(0.1), sampler); - sim.run(0.11, 0.01); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.1*arb::units::ms), sampler); + sim.run(0.11*arb::units::ms, 0.01*arb::units::ms); return all_near(sample_values, exp, epsilon); } @@ -315,37 +315,37 @@ TEST(diffusion, setting_diffusivity) { // BAD: Trying to use a diffusive ion, but b=0. { R r; - r.gprop.add_ion("bla", 1, 23, 42, 0, 0); - EXPECT_THROW(simulation(r).run(1, 1), illegal_diffusive_mechanism); + r.gprop.add_ion("bla", 1, 23*U::mM, 42*U::mM, 0*U::mV, 0*U::m2/U::s); + EXPECT_THROW(simulation(r).run(1*arb::units::ms, 1*arb::units::ms), illegal_diffusive_mechanism); } // BAD: Trying to use a partially diffusive ion { R r; - r.gprop.add_ion("bla", 1, 23, 42, 0, 0); - r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13}); - EXPECT_THROW(simulation(r).run(1, 1), cable_cell_error); + r.gprop.add_ion("bla", 1, 23*U::mM, 42*U::mM, 0*U::mV, 0*U::m2/U::s); + r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13*U::m2/U::s}); + EXPECT_THROW(simulation(r).run(1*arb::units::ms, 1*arb::units::ms), cable_cell_error); } // OK: Using the global default { R r; - r.gprop.add_ion("bla", 1, 23, 42, 0, 8); - r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13}); - EXPECT_NO_THROW(simulation(r).run(1, 1)); + r.gprop.add_ion("bla", 1, 23*U::mM, 42*U::mM, 0*U::mV, 8*U::m2/U::s); + r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13*U::m2/U::s}); + EXPECT_NO_THROW(simulation(r).run(1*arb::units::ms, 1*arb::units::ms)); } // OK: Using the cell default { R r; - r.gprop.add_ion("bla", 1, 23, 42, 0, 0); - r.dec.set_default(ion_diffusivity{"bla", 8}); - r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13}); - EXPECT_NO_THROW(simulation(r).run(1, 1)); + r.gprop.add_ion("bla", 1, 23*U::mM, 42*U::mM, 0*U::mV, 0*U::m2/U::s); + r.dec.set_default(ion_diffusivity{"bla", 8*U::m2/U::s}); + r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13*U::m2/U::s}); + EXPECT_NO_THROW(simulation(r).run(1*arb::units::ms, 1*arb::units::ms)); } // BAD: Using an unknown species { R r; - r.dec.set_default(ion_diffusivity{"bla", 8}); - r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13}); - EXPECT_THROW(simulation(r).run(1, 1), cable_cell_error); + r.dec.set_default(ion_diffusivity{"bla", 8*U::m2/U::s}); + r.dec.paint("(tag 1)"_reg, ion_diffusivity{"bla", 13*U::m2/U::s}); + EXPECT_THROW(simulation(r).run(1*arb::units::ms, 1*arb::units::ms), cable_cell_error); } } diff --git a/test/unit/test_domain_decomposition.cpp b/test/unit/test_domain_decomposition.cpp index ab8a7c3e4a..10e0b754c6 100644 --- a/test/unit/test_domain_decomposition.cpp +++ b/test/unit/test_domain_decomposition.cpp @@ -1,7 +1,5 @@ #include -#include - #include #include #include diff --git a/test/unit/test_event_delivery.cpp b/test/unit/test_event_delivery.cpp index 89718a1958..8800224e24 100644 --- a/test/unit/test_event_delivery.cpp +++ b/test/unit/test_event_delivery.cpp @@ -37,7 +37,7 @@ struct test_recipe: public n_cable_cell_recipe { decor decorations; decorations.place(mlocation{0, 0.5}, synapse("expsyn"), "synapse"); - decorations.place(mlocation{0, 0.5}, threshold_detector{-64}, "detector"); + decorations.place(mlocation{0, 0.5}, threshold_detector{-64*arb::units::mV}, "detector"); decorations.place(mlocation{0, 0.5}, junction("gj"), "gapjunction"); cable_cell c(st, decorations, labels); @@ -74,7 +74,7 @@ std::vector run_test_sim(const recipe& R, const group_gids_type& } sim.inject_events(cell_events); - sim.run((n+1)*ev_delta_t, 0.01); + sim.run((n+1)*ev_delta_t*arb::units::ms, 0.01*arb::units::ms); std::vector spike_gids; util::sort_by(spikes, [](auto s) { return s.time; }); diff --git a/test/unit/test_event_generators.cpp b/test/unit/test_event_generators.cpp index 024fbc1a08..54a8dbae6f 100644 --- a/test/unit/test_event_generators.cpp +++ b/test/unit/test_event_generators.cpp @@ -5,10 +5,6 @@ #include #include -#include "util/rangeutil.hpp" - -#include "common.hpp" - using namespace arb; namespace{ @@ -18,7 +14,7 @@ namespace{ } TEST(event_generators, assign_and_copy) { - event_generator gen = regular_generator({"l2"}, 5., 0.5, 0.75); + event_generator gen = regular_generator({"l2"}, 5., 0.5*arb::units::ms, 0.75*arb::units::ms); gen.resolve_label([](const cell_local_label_type&) {return 2;}); spike_event expected{2, 0.75, 5.}; @@ -57,7 +53,7 @@ TEST(event_generators, regular) { cell_lid_type lid = 3; float weight = 3.14; - event_generator gen = regular_generator(label, weight, t0, dt); + event_generator gen = regular_generator(label, weight, t0*arb::units::ms, dt*arb::units::ms); gen.resolve_label([lid](const cell_local_label_type&) {return lid;}); // Helper for building a set of expected events. @@ -93,7 +89,7 @@ TEST(event_generators, seq) { expected.push_back({0, time, weight}); } - event_generator gen = explicit_generator(l0, weight, times); + event_generator gen = explicit_generator_from_milliseconds(l0, weight, times); gen.resolve_label([](const cell_local_label_type&) {return 0;}); EXPECT_EQ(expected, as_vector(gen.events(0, 100.))); gen.reset(); @@ -126,8 +122,6 @@ TEST(event_generators, seq) { } TEST(event_generators, poisson) { - std::mt19937_64 G; - time_type t0 = 0; time_type t1 = 10; time_type lambda = 10; // expect 10 events per ms @@ -135,7 +129,7 @@ TEST(event_generators, poisson) { cell_lid_type lid = 2; float weight = 42; - event_generator gen = poisson_generator(label, weight, t0, lambda, G); + event_generator gen = poisson_generator(label, weight, t0*arb::units::ms, lambda*arb::units::kHz); gen.resolve_label([lid](const cell_local_label_type&) {return lid;}); pse_vector int1 = as_vector(gen.events(0, t1)); diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp index 5569171d7d..956bc965d6 100644 --- a/test/unit/test_fvm_layout.cpp +++ b/test/unit/test_fvm_layout.cpp @@ -21,7 +21,6 @@ #include "util/maputil.hpp" #include "util/rangeutil.hpp" #include "util/span.hpp" -#include "io/sepval.hpp" #include "common.hpp" #include "common_morphologies.hpp" @@ -43,6 +42,8 @@ using fvm_cell = arb::fvm_lowered_cell_impl; // instantiate template class template class arb::fvm_lowered_cell_impl; +namespace U = arb::units; + namespace { struct system { std::vector builders; @@ -71,10 +72,12 @@ namespace { auto description = builder.make_cell(); description.decorations.paint("soma"_lab, density("hh")); description.decorations.paint("dend"_lab, density("pas")); - description.decorations.place(builder.location({1,1}), i_clamp{5, 80, 0.3}, "clamp"); - + description.decorations.place(builder.location({1,1}), + i_clamp{5*arb::units::nA, 80*arb::units::kHz, 0.3*arb::units::rad}, + "clamp"); s.builders.push_back(std::move(builder)); descriptions.push_back(description); + } // Cell 1: ball and 3-stick, but with uneven dendrite @@ -119,14 +122,18 @@ namespace { auto c1 = reg::cable(b1-1, b.location({b1, 0}).pos, 1); auto c2 = reg::cable(b2-1, b.location({b2, 0}).pos, 1); auto c3 = reg::cable(b3-1, b.location({b3, 0}).pos, 1); - desc.decorations.paint(c1, membrane_capacitance{0.017}); - desc.decorations.paint(c2, membrane_capacitance{0.013}); - desc.decorations.paint(c3, membrane_capacitance{0.018}); + desc.decorations.paint(c1, membrane_capacitance{0.017*U::F*U::m.pow(-2)}); + desc.decorations.paint(c2, membrane_capacitance{0.013*U::F*U::m.pow(-2)}); + desc.decorations.paint(c3, membrane_capacitance{0.018*U::F*U::m.pow(-2)}); - desc.decorations.place(b.location({2,1}), i_clamp{5., 80., 0.45}, "clamo0"); - desc.decorations.place(b.location({3,1}), i_clamp{40., 10.,-0.2}, "clamp1"); + desc.decorations.place(b.location({2,1}), + i_clamp::box( 5.*arb::units::ms, 80.*arb::units::ms, 0.45*arb::units::nA), + "clamp0"); + desc.decorations.place(b.location({3,1}), + i_clamp::box(40.*arb::units::ms, 10.*arb::units::ms, -0.2*arb::units::nA), + "clamp1"); - desc.decorations.set_default(axial_resistivity{90}); + desc.decorations.set_default(axial_resistivity{90*U::Ohm*U::cm}); s.builders.push_back(std::move(b)); descriptions.push_back(desc); @@ -1619,9 +1626,9 @@ TEST(fvm_layout, revpot) { gprop.catalogue = make_unit_test_catalogue(); gprop.ion_species = {{"a", 1}, {"b", 2}, {"c", 3}}; - gprop.add_ion("a", 1, 10., 0, 0); - gprop.add_ion("b", 2, 30., 0, 0); - gprop.add_ion("c", 3, 50., 0, 0); + gprop.add_ion("a", 1, 10.*U::mM, 0*U::mM, 0*U::mV); + gprop.add_ion("b", 2, 30.*U::mM, 0*U::mM, 0*U::mV); + gprop.add_ion("c", 3, 50.*U::mM, 0*U::mM, 0*U::mV); gprop.default_parameters.reversal_potential_method["a"] = "write_eX/a"; mechanism_desc write_eb_ec = "write_multiple_eX/x=b,y=c"; @@ -1955,7 +1962,7 @@ TEST(fvm_layout, inhomogeneous_parameters) { // capacitance scales with CV area { auto decor = arb::decor{} - .set_default(membrane_capacitance{23.0}); + .set_default(membrane_capacitance{23.0*U::F*U::m.pow(-2)}); auto D = fvm_cv_discretize({morph, decor}, param); for (unsigned ix = 0; ix < D.size(); ++ix) { EXPECT_NEAR(D.cv_area[ix]*23.0, D.cv_capacitance[ix], 1e-6); @@ -1966,8 +1973,8 @@ TEST(fvm_layout, inhomogeneous_parameters) { // NOTE the diameter is evaluated at a _different_ spot (CV center!) { auto decor = arb::decor{} - .set_default(membrane_capacitance{23.0}) - .paint(reg::tagged(1), membrane_capacitance{23.0*iexpr::diameter()}); + .set_default(membrane_capacitance{23.0*U::F*U::m.pow(-2)}) + .paint(reg::tagged(1), membrane_capacitance{23.0*U::F*U::m.pow(-2), iexpr::diameter()}); auto D = fvm_cv_discretize({morph, decor}, param); EXPECT_EQ(D.size(), 30ul); for (unsigned ix = 0; ix < D.size(); ++ix) { @@ -1981,7 +1988,7 @@ TEST(fvm_layout, inhomogeneous_parameters) { // Defaults do not have a scale { auto decor = arb::decor{}; - EXPECT_THROW(decor.set_default(membrane_capacitance{23.0*iexpr::diameter()}), arb::cable_cell_error); + EXPECT_THROW(decor.set_default(membrane_capacitance{23.0*U::F*U::m.pow(-2), iexpr::diameter()}), arb::cable_cell_error); } } diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp index 28cc19562e..ef83d361fa 100644 --- a/test/unit/test_fvm_lowered.cpp +++ b/test/unit/test_fvm_lowered.cpp @@ -33,6 +33,7 @@ #include "../common_cells.hpp" #include "../simple_recipes.hpp" + using namespace std::string_literals; using namespace arborio::literals; @@ -229,7 +230,7 @@ TEST(fvm_lowered, target_handles) { descriptions[1].decorations.place(mlocation{2, 0.2}, synapse("exp2syn"), "syn2"); descriptions[1].decorations.place(mlocation{2, 0.8}, synapse("expsyn"), "syn3"); - descriptions[1].decorations.place(mlocation{0, 0}, threshold_detector{3.3}, "detector"); + descriptions[1].decorations.place(mlocation{0, 0}, threshold_detector{3.3*arb::units::mV}, "detector"); cable_cell cells[] = {descriptions[0], descriptions[1]}; @@ -285,9 +286,9 @@ TEST(fvm_lowered, stimulus) { auto desc = make_cell_ball_and_stick(false); // At end of stick - desc.decorations.place(mlocation{0,1}, i_clamp::box(5., 80., 0.3), "clamp0"); + desc.decorations.place(mlocation{0,1}, i_clamp::box(5.*arb::units::ms, 80.*arb::units::ms, 0.3*arb::units::nA), "clamp0"); // On the soma CV, which is over the approximate interval: (cable 0 0 0.1) - desc.decorations.place(mlocation{0,0.05}, i_clamp::box(1., 2., 0.1), "clamp1"); + desc.decorations.place(mlocation{0,0.05}, i_clamp::box(1.*arb::units::ms, 2.*arb::units::ms, 0.1*arb::units::nA), "clamp1"); std::vector cells{desc}; @@ -356,7 +357,8 @@ TEST(fvm_lowered, ac_stimulus) { const double max_time = 8; // (ms) // Envelope is linear ramp from 0 to max_time. - dec.place(mlocation{0, 0}, i_clamp({{0, 0}, {max_time, max_amplitude}, {max_time, 0}}, freq, phase), "clamp"); + dec.place(mlocation{0, 0}, + i_clamp({{0*arb::units::ms, 0*arb::units::nA}, {max_time*arb::units::ms, max_amplitude*arb::units::nA}, {max_time*arb::units::ms, 0*arb::units::nA}}, freq*arb::units::kHz, phase*arb::units::rad), "clamp"); std::vector cells = {cable_cell(tree, dec)}; cable_cell_global_properties gprop; @@ -470,12 +472,12 @@ TEST(fvm_lowered, derived_mechs) { } }; - float times[] = {10.f, 20.f}; + std::vector times{10.f, 20.f}; auto decomp = partition_load_balance(rec, context); simulation sim(rec, context, decomp); - sim.add_sampler(all_probes, explicit_schedule(times), sampler); - sim.run(30.0, 1.f/1024); + sim.add_sampler(all_probes, explicit_schedule_from_milliseconds(times), sampler); + sim.run(30.0*arb::units::ms, 1.f/1024*arb::units::ms); ASSERT_EQ(2u, samples[0].size()); ASSERT_EQ(2u, samples[1].size()); @@ -505,7 +507,7 @@ TEST(fvm_lowered, null_region) { auto decomp = partition_load_balance(rec, context); simulation sim(rec, context, decomp); - EXPECT_NO_THROW(sim.run(30.0, 1.f/1024)); + EXPECT_NO_THROW(sim.run(30.0*arb::units::ms, 1.f/1024*arb::units::ms)); } @@ -826,7 +828,7 @@ TEST(fvm_lowered, post_events_shared_state) { auto ndetectors = detectors_per_cell_[gid]; auto offset = 1.0 / ndetectors; for (unsigned i = 0; i < ndetectors; ++i) { - decor.place(arb::mlocation{0, offset * i}, arb::threshold_detector{10}, "detector"+std::to_string(i)); + decor.place(arb::mlocation{0, offset * i}, arb::threshold_detector{10*arb::units::mV}, "detector"+std::to_string(i)); } decor.place(arb::mlocation{0, 0.5}, synapse_, "syanpse"); @@ -920,15 +922,15 @@ TEST(fvm_lowered, label_data) { decor.set_default(arb::cv_policy_fixed_per_branch(10)); decor.place(uniform(all(), 0, 3, 42), arb::synapse("expsyn"), "4_synapses"); decor.place(uniform(all(), 4, 4, 42), arb::synapse("expsyn"), "1_synapse"); - decor.place(uniform(all(), 5, 5, 42), arb::threshold_detector{10}, "1_detector"); + decor.place(uniform(all(), 5, 5, 42), arb::threshold_detector{10*arb::units::mV}, "1_detector"); cells_.push_back(arb::cable_cell(arb::morphology(tree), decor)); } { arb::decor decor; decor.set_default(arb::cv_policy_fixed_per_branch(10)); - decor.place(uniform(all(), 0, 2, 24), arb::threshold_detector{10}, "3_detectors"); - decor.place(uniform(all(), 3, 4, 24), arb::threshold_detector{10}, "2_detectors"); + decor.place(uniform(all(), 0, 2, 24), arb::threshold_detector{10*arb::units::mV}, "3_detectors"); + decor.place(uniform(all(), 3, 4, 24), arb::threshold_detector{10*arb::units::mV}, "2_detectors"); decor.place(uniform(all(), 5, 6, 24), arb::junction("gj"), "2_gap_junctions"); decor.place(uniform(all(), 7, 7, 24), arb::junction("gj"), "1_gap_junction"); diff --git a/test/unit/test_lif_cell_group.cpp b/test/unit/test_lif_cell_group.cpp index 7a03cd8022..8e13b222fa 100644 --- a/test/unit/test_lif_cell_group.cpp +++ b/test/unit/test_lif_cell_group.cpp @@ -12,9 +12,11 @@ #include #include -#include "lif_cell_group.hpp" - using namespace arb; + +namespace U = arb::units; +using namespace U::literals; + // Simple ring network of LIF neurons. // with one regularly spiking cell (fake cell) connected to the first cell in the ring. class ring_recipe: public arb::recipe { @@ -44,14 +46,14 @@ class ring_recipe: public arb::recipe { std::vector connections; // gid-1 >= 0 since gid != 0 auto src_gid = (gid - 1) % n_lif_cells_; - cell_connection conn({src_gid, "src"}, {"tgt"}, weight_, delay_); + cell_connection conn({src_gid, "src"}, {"tgt"}, weight_, delay_*U::ms); connections.push_back(conn); // If first LIF cell, then add // the connection from the last LIF cell as well if (gid == 1) { auto src_gid = n_lif_cells_; - cell_connection conn({src_gid, "src"}, {"tgt"}, weight_, delay_); + cell_connection conn({src_gid, "src"}, {"tgt"}, weight_, delay_*U::ms); connections.push_back(conn); } @@ -62,7 +64,7 @@ class ring_recipe: public arb::recipe { // regularly spiking cell. if (gid == 0) { // Produces just a single spike at time 0ms. - return spike_source_cell("src", explicit_schedule({0.f})); + return spike_source_cell("src", explicit_schedule_from_milliseconds({0.})); } // LIF cell. auto cell = lif_cell("src", "tgt"); @@ -90,14 +92,8 @@ class path_recipe: public arb::recipe { } std::vector connections_on(cell_gid_type gid) const override { - if (gid == 0) { - return {}; - } - std::vector connections; - cell_connection conn({gid-1, "src"}, {"tgt"}, weight_, delay_); - connections.push_back(conn); - - return connections; + if (gid == 0) return {}; + return {{{gid-1, "src"}, {"tgt"}, weight_, delay_*U::ms}}; } util::unique_any get_cell_description(cell_gid_type gid) const override { @@ -127,17 +123,17 @@ class probe_recipe: public arb::recipe { for (size_t ix = 0; ix < n_conn_; ++ix) res.emplace_back(cell_global_label_type{0, "src"}, cell_local_label_type{"tgt"}, 0.0, - 0.005); + 5*U::us); return res; } util::unique_any get_cell_description(cell_gid_type gid) const override { auto cell = lif_cell("src", "tgt"); if (gid == 0) { - cell.E_R = -23; - cell.V_m = -18; - cell.E_L = -13; - cell.t_ref = 0.8; - cell.tau_m = 5; + cell.E_R = -23.0*U::mV; + cell.V_m = -18.0*U::mV; + cell.E_L = -13.0*U::mV; + cell.t_ref = 0.8*U::ms; + cell.tau_m = 5*U::ms; } return cell; } @@ -149,7 +145,7 @@ class probe_recipe: public arb::recipe { } } std::vector event_generators(cell_gid_type) const override { - return {regular_generator({"tgt"}, 200.0, 2.0, 1.0, 6.0)}; + return {regular_generator({"tgt"}, 200.0, 2.0*U::ms, 1.0*U::ms, 6.0*U::ms)}; } size_t n_conn_ = 0; @@ -195,10 +191,7 @@ TEST(lif_cell_group, spikes) { events.push_back({0, {{0, 50, 1000}}}); sim.inject_events(events); - - time_type tfinal = 100; - time_type dt = 0.01; - sim.run(tfinal, dt); + sim.run(100*U::ms, 0.01*U::ms); // we expect 4 spikes: 2 by both neurons EXPECT_EQ(4u, sim.num_spikes()); @@ -211,9 +204,6 @@ TEST(lif_cell_group, ring) double weight = 1000; double delay = 1; - // Total simulation time. - time_type simulation_time = 100; - auto recipe = ring_recipe(num_lif_cells, weight, delay); // Creates a simulation with a ring recipe of lif neurons simulation sim(recipe); @@ -227,7 +217,7 @@ TEST(lif_cell_group, ring) ); // Runs the simulation for simulation_time with given timestep - sim.run(simulation_time, 0.01); + sim.run(100*U::ms, 0.01*U::ms); // The total number of cells in all the cell groups. // There is one additional fake cell (regularly spiking cell). EXPECT_EQ(num_lif_cells + 1u, recipe.num_cells()); @@ -274,7 +264,7 @@ TEST(lif_cell_group, probe) { auto rec = probe_recipe{}; auto sim = simulation(rec); - sim.add_sampler(all_probes, regular_schedule(0.025), fun); + sim.add_sampler(all_probes, regular_schedule(0.025*U::ms), fun); std::vector spikes; @@ -282,7 +272,7 @@ TEST(lif_cell_group, probe) { [&spikes](const std::vector& spk) { for (const auto& s: spk) spikes.push_back(s.time); } ); - sim.run(10, 0.005); + sim.run(10*U::ms, 0.005*U::ms); std::vector exp = {{ 0, -18 }, { 0.025, -17.9750624 }, { 0.05, -17.9502492 }, @@ -710,7 +700,7 @@ TEST(lif_cell_group, probe_with_connections) { auto rec = probe_recipe{5}; auto sim = simulation(rec); - sim.add_sampler(all_probes, regular_schedule(0.025), fun); + sim.add_sampler(all_probes, regular_schedule(0.025*U::ms), fun); std::vector spikes; @@ -718,7 +708,7 @@ TEST(lif_cell_group, probe_with_connections) { [&spikes](const std::vector& spk) { for (const auto& s: spk) spikes.push_back(s.time); } ); - sim.run(10, 0.005); + sim.run(10*U::ms, 0.005*U::ms); std::vector exp = {{ 0, -18 }, { 0.025, -17.9750624 }, { 0.05, -17.9502492 }, diff --git a/test/unit/test_merge_events.cpp b/test/unit/test_merge_events.cpp index 7b17c4094b..98060b610e 100644 --- a/test/unit/test_merge_events.cpp +++ b/test/unit/test_merge_events.cpp @@ -28,13 +28,15 @@ static void merge_events( const pse_vector& old_events, pse_vector& pending, std::vector& generators, - pse_vector& new_events) -{ + pse_vector& new_events) { util::sort(pending); - merge_cell_events(t_from, t_to, util::range_pointer_view(old_events), util::range_pointer_view(pending), generators, new_events); + merge_cell_events(t_from, t_to, + util::range_pointer_view(old_events), + util::range_pointer_view(pending), + generators, + new_events); } - std::vector empty_gens; // Test the trivial case of merging empty sets @@ -157,7 +159,7 @@ TEST(merge_events, X) {0, 26, 4}, }; - auto gen = regular_generator({"l0"}, 42.f, t0, 5); + auto gen = regular_generator({"l0"}, 42.f, t0*arb::units::ms, 5*arb::units::ms); gen.resolve_label([](const cell_local_label_type&) {return 2;}); std::vector generators = {gen}; @@ -203,8 +205,8 @@ TEST(merge_events, tourney_seq) util::sort(expected); auto - g1 = explicit_generator(l0, w1, times), - g2 = explicit_generator(l0, w2, times); + g1 = explicit_generator_from_milliseconds(l0, w1, times), + g2 = explicit_generator_from_milliseconds(l0, w2, times); g1.resolve_label([](const cell_local_label_type&) {return 0;}); g2.resolve_label([](const cell_local_label_type&) {return 0;}); @@ -223,9 +225,7 @@ TEST(merge_events, tourney_seq) } // Test the tournament tree on a large set of Poisson generators. -TEST(merge_events, tourney_poisson) -{ - using rndgen = std::mt19937_64; +TEST(merge_events, tourney_poisson) { // Number of poisson generators. // Not a power of 2, so that there will be "null" leaf nodes in the // tournament tree. @@ -241,8 +241,8 @@ TEST(merge_events, tourney_poisson) float weight = i; // the first and last generators have the same seed to test that sorting // of events with the same time but different weights works properly. - rndgen G(i%(ngen-1)); - auto gen = poisson_generator(label, weight, t0, lambda, G); + auto G = i%(ngen-1); + auto gen = poisson_generator(label, weight, t0*arb::units::ms, lambda*arb::units::kHz, G); gen.resolve_label([lid](const cell_local_label_type&) {return lid;}); generators.push_back(std::move(gen)); } diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp index fc87b1cd43..6b82baecc7 100644 --- a/test/unit/test_probe.cpp +++ b/test/unit/test_probe.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include @@ -40,6 +39,7 @@ using namespace arb; using util::any_cast; +namespace U = arb::units; using multicore_fvm_cell = fvm_lowered_cell_impl; using multicore_shared_state = multicore::backend::shared_state; @@ -110,7 +110,7 @@ void run_v_i_probe_test(context ctx) { bs.decorations.set_default(cv_policy_fixed_per_branch(1)); - auto stim = i_clamp::box(0, 100, 0.3); + auto stim = i_clamp::box(0.*U::ms, 100*U::ms, 0.3*U::nA); bs.decorations.place(mlocation{1, 1}, stim, "clamp"); cable1d_recipe rec((cable_cell(bs))); @@ -778,15 +778,15 @@ void run_axial_and_ion_current_sampled_probe_test(context ctx) { cv_policy policy = cv_policy_fixed_per_branch(n_cv); d.set_default(policy); - d.place(mlocation{0, 0}, i_clamp(0.3), "clamp"); + d.place(mlocation{0, 0}, i_clamp(0.3*U::nA), "clamp"); // The time constant will be membrane capacitance / membrane conductance. // For τ = 0.1 ms, set conductance to 0.01 S/cm² and membrance capacitance // to 0.01 F/m². d.paint(reg::all(), density("ca_linear", {{"g", 0.01}})); // [S/cm²] - d.set_default(membrane_capacitance{0.01}); // [F/m²] - const double tau = 0.1; // [ms] + d.set_default(membrane_capacitance{0.01*U::F/U::m2}); // [F/m²] + auto tau = 0.1*U::ms; cable1d_recipe rec(cable_cell(m, d)); rec.catalogue() = cat; @@ -826,7 +826,7 @@ void run_axial_and_ion_current_sampled_probe_test(context ctx) { std::vector i_axial(n_axial_probe); std::vector i_memb(n_cv), i_stim(n_cv); - sim.add_sampler(all_probes, explicit_schedule({20*tau}), + sim.add_sampler(all_probes, explicit_schedule(std::vector{20*tau}), [&](probe_metadata pm, std::size_t n_sample, const sample_record* samples) { @@ -869,13 +869,13 @@ void run_axial_and_ion_current_sampled_probe_test(context ctx) { } }); - const double dt = 0.025; // [ms] - sim.run(20*tau+dt, dt); + auto dt = 0.025*U::ms; // [ms] + sim.run(20*tau + dt, dt); ASSERT_EQ(n_cv, i_memb.size()); ASSERT_EQ(n_cv, i_stim.size()); - for (unsigned i = 0; i auto run_simple_samplers(const arb::context& ctx, - double t_end, + U::quantity t_end, const std::vector& cells, const cell_address_type& probe, const std::vector& probe_addrs, - const std::vector& when) { + const std::vector& when) { cable1d_recipe rec(cells, false); rec.catalogue() = make_unit_test_catalogue(global_default_catalogue()); unsigned n_probe = probe_addrs.size(); @@ -917,17 +917,17 @@ auto run_simple_samplers(const arb::context& ctx, make_simple_sampler(traces[i])); } - sim.run(t_end, 0.025); + sim.run(t_end, 0.025*U::ms); return traces; } template auto run_simple_sampler(const arb::context& ctx, - double t_end, + U::quantity t_end, const std::vector& cells, const cell_address_type& probe, const std::any& probe_addr, - const std::vector& when) { + const std::vector& when) { return run_simple_samplers(ctx, t_end, cells, probe, {probe_addr}, when).at(0); } @@ -945,10 +945,11 @@ void run_multi_probe_test(context ctx) { d.paint(reg::branch(2), density("param_as_state", {{"p", 20.}})); d.paint(reg::branch(5), density("param_as_state", {{"p", 50.}})); - auto tracev = run_simple_sampler(ctx, 0.1, + auto tracev = run_simple_sampler(ctx, 0.1*U::ms, {cable_cell{m, d}}, {0, "probe"}, - cable_probe_density_state{ls::terminal(), "param_as_state", "s"}, {0.}); + cable_probe_density_state{ls::terminal(), "param_as_state", "s"}, + {0.0*U::ms}); // Expect to have received a sample on each of the terminals of branches 1, 2, and 5. ASSERT_EQ(3u, tracev.size()); @@ -984,14 +985,14 @@ void run_v_sampled_probe_test(context ctx) { // samples at the same point on each cell will give the same value at // 0.3 ms, but different at 0.6 ms. - d0.place(mlocation{1, 1}, i_clamp::box(0, 0.5, 1.), "clamp0"); - d1.place(mlocation{1, 1}, i_clamp::box(0, 1.0, 1.), "clamp1"); + d0.place(mlocation{1, 1}, i_clamp::box(0*U::ms, 0.5*U::ms, 1.*U::nA), "clamp0"); + d1.place(mlocation{1, 1}, i_clamp::box(0*U::ms, 1.0*U::ms, 1.*U::nA), "clamp1"); mlocation probe_loc{1, 0.2}; std::vector cells = {{bs.morph, d0, bs.labels}, {bs.morph, d1, bs.labels}}; - const double t_end = 1.; // [ms] - std::vector when = {0.3, 0.6}; // Sample at 0.3 and 0.6 ms. + const auto t_end = 1.*U::ms; // [ms] + std::vector when = {0.3*U::ms, 0.6*U::ms}; // Sample at 0.3 and 0.6 ms. auto trace0 = run_simple_sampler(ctx, t_end, cells, {0, "Um-loc"}, cable_probe_membrane_voltage{probe_loc}, @@ -1038,14 +1039,14 @@ void run_total_current_probe_test(context ctx) { // For τ = 0.1 ms, set conductance to 0.01 S/cm² and membrance capacitance // to 0.01 F/m². - const double tau = 0.1; // [ms] - d0.place(mlocation{0, 0}, i_clamp(0.3), "clamp0"); + auto tau = 0.1*U::ms; // [ms] + d0.place(mlocation{0, 0}, i_clamp(0.3*U::nA), "clamp0"); d0.paint(reg::all(), density("ca_linear", {{"g", 0.01}})); // [S/cm²] - d0.set_default(membrane_capacitance{0.01}); // [F/m²] + d0.set_default(membrane_capacitance{0.01*U::F/U::m2}); // [F/m²] // Tweak membrane capacitance on cells[1] so as to change dynamics a bit. auto d1 = d0; - d1.set_default(membrane_capacitance{0.009}); // [F/m²] + d1.set_default(membrane_capacitance{0.009*U::F/U::m2}); // [F/m²] // We'll run each set of tests twice: once with a trivial (zero-volume) CV // at the fork points, and once with a non-trivial CV centred on the fork @@ -1069,19 +1070,22 @@ void run_total_current_probe_test(context ctx) { for (unsigned i = 0; i<2; ++i) { SCOPED_TRACE(i); - const double t_end = 21*tau; // [ms] + auto t_end = 21*tau; // [ms] traces[i] = run_simple_sampler, mcable_list>(ctx, t_end, cells, {i, "Itotal"}, - cable_probe_total_current_cell{}, {tau, 20*tau}).at(0); + cable_probe_total_current_cell{}, + {tau, 20*tau}).at(0); ion_traces[i] = run_simple_sampler, mcable_list>(ctx, t_end, cells, {i, "Iion"}, - cable_probe_total_ion_current_cell{}, {tau, 20*tau}).at(0); + cable_probe_total_ion_current_cell{}, + {tau, 20*tau}).at(0); stim_traces[i] = run_simple_sampler, mcable_list>(ctx, t_end, cells, {i, "Istim"}, - cable_probe_stimulus_current_cell{}, {tau, 20*tau}).at(0); + cable_probe_stimulus_current_cell{}, + {tau, 20*tau}).at(0); ASSERT_EQ(2u, traces[i].size()); ASSERT_EQ(2u, ion_traces[i].size()); @@ -1157,19 +1161,21 @@ void run_stimulus_probe_test(context ctx) { // Model two simple stick cable cells, 3 CVs each, and stimuli on cell 0, cv 1 // and cell 1, cv 2. Run both cells in the same cell group. - const double stim_until = 1.; // [ms] + auto stim_from = 0.*U::ms; + auto stim_until = 1.*U::ms; + auto m = make_stick_morphology(); cv_policy policy = cv_policy_fixed_per_branch(3); decor d0, d1; d0.set_default(policy); - d0.place(mlocation{0, 0.5}, i_clamp::box(0., stim_until, 10.), "clamp0"); - d0.place(mlocation{0, 0.5}, i_clamp::box(0., stim_until, 20.), "clamp1"); + d0.place(mlocation{0, 0.5}, i_clamp::box(stim_from, stim_until, 10.*U::nA), "clamp0"); + d0.place(mlocation{0, 0.5}, i_clamp::box(stim_from, stim_until, 20.*U::nA), "clamp1"); double expected_stim0 = 30; d1.set_default(policy); - d1.place(mlocation{0, 1}, i_clamp::box(0., stim_until, 30.), "clamp0"); - d1.place(mlocation{0, 1}, i_clamp::box(0., stim_until, -10.), "clamp1"); + d1.place(mlocation{0, 1}, i_clamp::box(stim_from, stim_until, 30.*U::nA), "clamp0"); + d1.place(mlocation{0, 1}, i_clamp::box(stim_from, stim_until, -10.*U::nA), "clamp1"); double expected_stim1 = 20; std::vector cells = {{m, d0}, {m, d1}}; @@ -1181,7 +1187,7 @@ void run_stimulus_probe_test(context ctx) { for (unsigned i: {0u, 1u}) { traces[i] = run_simple_sampler, mcable_list>(ctx, 2.5*stim_until, cells, {i, "Istim"}, cable_probe_stimulus_current_cell{}, - {stim_until/2, 2*stim_until}).at(0); + {0.5*stim_until, 2*stim_until}).at(0); ASSERT_EQ(3u, traces[i].meta.size()); for ([[maybe_unused]] unsigned cv: {0u, 1u, 2u}) { diff --git a/test/unit/test_recipe.cpp b/test/unit/test_recipe.cpp index 27b04cb749..c388f357b7 100644 --- a/test/unit/test_recipe.cpp +++ b/test/unit/test_recipe.cpp @@ -17,83 +17,73 @@ #include "../common_cells.hpp" using namespace arb; -using arb::util::make_span; +namespace U = arb::units; namespace { - class custom_recipe: public recipe { - public: - custom_recipe(std::vector cells, - std::vector> conns, - std::vector> gjs, - std::vector> gens): - num_cells_(cells.size()), - connections_(conns), - gap_junctions_(gjs), - event_generators_(gens), - cells_(cells) {} - - cell_size_type num_cells() const override { - return num_cells_; - } - arb::util::unique_any get_cell_description(cell_gid_type gid) const override { - return cells_.at(gid); - } - cell_kind get_cell_kind(cell_gid_type gid) const override { - return cell_kind::cable; - } - std::vector gap_junctions_on(cell_gid_type gid) const override { - return gap_junctions_.at(gid); - } - std::vector connections_on(cell_gid_type gid) const override { - return connections_.at(gid); - } - std::vector event_generators(cell_gid_type gid) const override { - return event_generators_.at(gid); - } - std::any get_global_properties(cell_kind) const override { - arb::cable_cell_global_properties a; - a.default_parameters = arb::neuron_parameter_defaults; - return a; - } - - private: - cell_size_type num_cells_; - std::vector> connections_; - std::vector> gap_junctions_; - std::vector> event_generators_; - std::vector cells_; - }; - - cable_cell custom_cell(cell_size_type num_detectors, cell_size_type num_synapses, cell_size_type num_gj) { - arb::segment_tree tree; - tree.append(arb::mnpos, {0,0,0,10}, {0,0,20,10}, 1); // soma - tree.append(0, {0,0, 20, 2}, {0,0, 320, 2}, 3); // dendrite - - arb::cable_cell cell(tree, {}); - - arb::decor decorations; - - // Add a num_detectors detectors to the cell. - for (auto i: util::make_span(num_detectors)) { - decorations.place(arb::mlocation{0,(double)i/num_detectors}, arb::threshold_detector{10}, "detector"+std::to_string(i)); - } - - // Add a num_synapses synapses to the cell. - for (auto i: util::make_span(num_synapses)) { - decorations.place(arb::mlocation{0,(double)i/num_synapses}, arb::synapse("expsyn"), "synapse"+std::to_string(i)); - } - - // Add a num_gj gap_junctions to the cell. - for (auto i: util::make_span(num_gj)) { - decorations.place(arb::mlocation{0,(double)i/num_gj}, arb::junction("gj"), "gapjunction"+std::to_string(i)); - } - - return arb::cable_cell(tree, decorations); +struct custom_recipe: public recipe { + custom_recipe(std::vector cells, + std::vector> conns, + std::vector> gjs, + std::vector> gens): + num_cells_(cells.size()), + connections_(std::move(conns)), + gap_junctions_(std::move(gjs)), + event_generators_(std::move(gens)), + cells_(std::move(cells)) { + gprop.default_parameters = arb::neuron_parameter_defaults; } + + cell_size_type num_cells() const override { return num_cells_; } + arb::util::unique_any get_cell_description(cell_gid_type gid) const override { return cells_.at(gid); } + cell_kind get_cell_kind(cell_gid_type gid) const override { return cell_kind::cable; } + std::vector gap_junctions_on(cell_gid_type gid) const override { return gap_junctions_.at(gid); } + std::vector connections_on(cell_gid_type gid) const override { return connections_.at(gid); } + std::vector event_generators(cell_gid_type gid) const override { return event_generators_.at(gid); } + std::any get_global_properties(cell_kind) const override { return gprop; } + +private: + cell_size_type num_cells_; + std::vector> connections_; + std::vector> gap_junctions_; + std::vector> event_generators_; + std::vector cells_; + arb::cable_cell_global_properties gprop; +}; + +cable_cell custom_cell(cell_size_type num_detectors, cell_size_type num_synapses, cell_size_type num_gj) { + arb::segment_tree tree; + tree.append(arb::mnpos, {0,0,0,10}, {0,0,20,10}, 1); // soma + tree.append(0, {0,0, 20, 2}, {0,0, 320, 2}, 3); // dendrite + + arb::cable_cell cell(tree, {}); + + arb::decor decorations; + + // Add a num_detectors detectors to the cell. + for (auto i: util::make_span(num_detectors)) { + decorations.place(arb::mlocation{0,(double)i/num_detectors}, + arb::threshold_detector{10*arb::units::mV}, "detector"+std::to_string(i)); + } + + // Add a num_synapses synapses to the cell. + for (auto i: util::make_span(num_synapses)) { + decorations.place(arb::mlocation{0,(double)i/num_synapses}, + arb::synapse("expsyn"), + "synapse"+std::to_string(i)); + } + + // Add a num_gj gap_junctions to the cell. + for (auto i: util::make_span(num_gj)) { + decorations.place(arb::mlocation{0,(double)i/num_gj}, + arb::junction("gj"), + "gapjunction"+std::to_string(i)); + } + + return arb::cable_cell(tree, decorations); } +} // namespace -TEST(recipe, gap_junctions) -{ +TEST(recipe, gap_junctions) { auto context = make_context({arbenv::default_concurrency(), -1}); auto cell_0 = custom_cell(0, 0, 3); @@ -139,13 +129,13 @@ TEST(recipe, connections) auto cell_1 = custom_cell(2, 1, 0); std::vector conns_0, conns_1; { - conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1}, - {{1, "detector1"}, {"synapse0"}, 0.1, 0.1}, - {{1, "detector0"}, {"synapse1"}, 0.2, 0.4}}; + conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{1, "detector1"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{1, "detector0"}, {"synapse1"}, 0.2, 0.4*U::ms}}; - conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2}, - {{0, "detector0"}, {"synapse0"}, 0.3, 0.1}, - {{0, "detector0"}, {"synapse0"}, 0.1, 0.8}}; + conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2*U::ms}, + {{0, "detector0"}, {"synapse0"}, 0.3, 0.1*U::ms}, + {{0, "detector0"}, {"synapse0"}, 0.1, 0.8*U::ms}}; auto recipe_0 = custom_recipe({cell_0, cell_1}, {conns_0, conns_1}, {{}, {}}, {{}, {}}); auto decomp_0 = partition_load_balance(recipe_0, context); @@ -153,13 +143,13 @@ TEST(recipe, connections) EXPECT_NO_THROW(simulation(recipe_0, context, decomp_0)); } { - conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1}, - {{2, "detector1"}, {"synapse0"}, 0.1, 0.1}, - {{1, "detector0"}, {"synapse1"}, 0.2, 0.4}}; + conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{2, "detector1"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{1, "detector0"}, {"synapse1"}, 0.2, 0.4*U::ms}}; - conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2}, - {{0, "detector0"}, {"synapse0"}, 0.3, 0.1}, - {{0, "detector0"}, {"synapse0"}, 0.1, 0.8}}; + conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2*U::ms}, + {{0, "detector0"}, {"synapse0"}, 0.3, 0.1*U::ms}, + {{0, "detector0"}, {"synapse0"}, 0.1, 0.8*U::ms}}; auto recipe_1 = custom_recipe({cell_0, cell_1}, {conns_0, conns_1}, {{}, {}}, {{}, {}}); auto decomp_1 = partition_load_balance(recipe_1, context); @@ -167,13 +157,13 @@ TEST(recipe, connections) EXPECT_THROW(simulation(recipe_1, context, decomp_1), arb::bad_connection_source_gid); } { - conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1}, - {{1, "detector1"}, {"synapse0"}, 0.1, 0.1}, - {{1, "detector3"}, {"synapse1"}, 0.2, 0.4}}; + conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{1, "detector1"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{1, "detector3"}, {"synapse1"}, 0.2, 0.4*U::ms}}; - conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2}, - {{0, "detector0"}, {"synapse0"}, 0.3, 0.1}, - {{0, "detector0"}, {"synapse0"}, 0.1, 0.8}}; + conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2*U::ms}, + {{0, "detector0"}, {"synapse0"}, 0.3, 0.1*U::ms}, + {{0, "detector0"}, {"synapse0"}, 0.1, 0.8*U::ms}}; auto recipe_2 = custom_recipe({cell_0, cell_1}, {conns_0, conns_1}, {{}, {}}, {{}, {}}); auto decomp_2 = partition_load_balance(recipe_2, context); @@ -181,13 +171,13 @@ TEST(recipe, connections) EXPECT_THROW(simulation(recipe_2, context, decomp_2), arb::bad_connection_label); } { - conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1}, - {{1, "detector1"}, {"synapse0"}, 0.1, 0.1}, - {{1, "detector0"}, {"synapse1"}, 0.2, 0.4}}; + conns_0 = {{{1, "detector0"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{1, "detector1"}, {"synapse0"}, 0.1, 0.1*U::ms}, + {{1, "detector0"}, {"synapse1"}, 0.2, 0.4*U::ms}}; - conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2}, - {{0, "detector0"}, {"synapse9"}, 0.3, 0.1}, - {{0, "detector0"}, {"synapse0"}, 0.1, 0.8}}; + conns_1 = {{{0, "detector0"}, {"synapse0"}, 0.1, 0.2*U::ms}, + {{0, "detector0"}, {"synapse9"}, 0.3, 0.1*U::ms}, + {{0, "detector0"}, {"synapse0"}, 0.1, 0.8*U::ms}}; auto recipe_4 = custom_recipe({cell_0, cell_1}, {conns_0, conns_1}, {{}, {}}, {{}, {}}); auto decomp_4 = partition_load_balance(recipe_4, context); @@ -203,18 +193,18 @@ TEST(recipe, event_generators) { auto cell_1 = custom_cell(2, 1, 0); { std::vector - gens_0 = {arb::explicit_generator({"synapse0"}, 0.1, std::vector{1.0}), - arb::explicit_generator({"synapse1"}, 0.1, std::vector{2.0})}, - gens_1 = {arb::explicit_generator({"synapse0"}, 0.1, std::vector{1.0})}; + gens_0 = {arb::explicit_generator_from_milliseconds({"synapse0"}, 0.1, std::vector{1.0}), + arb::explicit_generator_from_milliseconds({"synapse1"}, 0.1, std::vector{2.0})}, + gens_1 = {arb::explicit_generator_from_milliseconds({"synapse0"}, 0.1, std::vector{1.0})}; auto recipe_0 = custom_recipe({cell_0, cell_1}, {{}, {}}, {{}, {}}, {gens_0, gens_1}); auto decomp_0 = partition_load_balance(recipe_0, context); - EXPECT_NO_THROW(simulation(recipe_0, context, decomp_0).run(1, 0.1)); + EXPECT_NO_THROW(simulation(recipe_0, context, decomp_0).run(1*arb::units::ms, 0.1*arb::units::ms)); } { std::vector - gens_0 = {arb::regular_generator({"totally-not-a-synapse-42"}, 0.1, 0, 0.001)}, + gens_0 = {arb::regular_generator({"totally-not-a-synapse-42"}, 0.1, 0*arb::units::ms, 0.001*arb::units::ms)}, gens_1 = {}; auto recipe_0 = custom_recipe({cell_0, cell_1}, {{}, {}}, {{}, {}}, {gens_0, gens_1}); diff --git a/test/unit/test_s_expr.cpp b/test/unit/test_s_expr.cpp index 5a7b8c876c..17e9dc3df2 100644 --- a/test/unit/test_s_expr.cpp +++ b/test/unit/test_s_expr.cpp @@ -558,28 +558,28 @@ std::ostream& operator<<(std::ostream& o, const threshold_detector& p) { return o << "(threshold-detector " << p.threshold << ')'; } std::ostream& operator<<(std::ostream& o, const init_membrane_potential& p) { - return o << "(membrane-potential " << p.value << ')'; + return o << "(membrane-potential " << p.value << " " << p.scale << ')'; } -std::ostream& operator<<(std::ostream& o, const temperature_K& p) { - return o << "(temperature-kelvin " << p.value << ')'; +std::ostream& operator<<(std::ostream& o, const temperature& p) { + return o << "(temperature-kelvin " << p.value << " " << p.scale << ')'; } std::ostream& operator<<(std::ostream& o, const axial_resistivity& p) { - return o << "(axial-resistivity " << p.value << ')'; + return o << "(axial-resistivity " << p.value << " " << p.scale << ')'; } std::ostream& operator<<(std::ostream& o, const membrane_capacitance& p) { - return o << "(membrane-capacitance " << p.value << ')'; + return o << "(membrane-capacitance " << p.value << " " << p.scale << ')'; } std::ostream& operator<<(std::ostream& o, const init_int_concentration& p) { - return o << "(ion-internal-concentration \"" << p.ion << "\" " << p.value << ')'; + return o << "(ion-internal-concentration \"" << p.ion << "\" " << p.value << " " << p.scale << ')'; } std::ostream& operator<<(std::ostream& o, const ion_diffusivity& p) { - return o << "(ion-diffusivity \"" << p.ion << "\" " << p.value << ')'; + return o << "(ion-diffusivity \"" << p.ion << "\" " << p.value << " " << p.scale << ')'; } std::ostream& operator<<(std::ostream& o, const init_ext_concentration& p) { - return o << "(ion-external-concentration \"" << p.ion << "\" " << p.value << ')'; + return o << "(ion-external-concentration \"" << p.ion << "\" " << p.value << " " << p.scale << ')'; } std::ostream& operator<<(std::ostream& o, const init_reversal_potential& p) { - return o << "(ion-reversal-potential \"" << p.ion << "\" " << p.value << ')'; + return o << "(ion-reversal-potential \"" << p.ion << "\" " << p.value << " " << p.scale << ')'; } std::ostream& operator<<(std::ostream& o, const mechanism_desc& m) { o << "(mechanism \"" << m.name() << "\""; @@ -710,35 +710,15 @@ std::string round_trip_component(std::istream& stream) { } } -TEST(decor_literals, double_to_iexpr_promotion) { - using namespace cable_s_expr; - std::vector> literals = { - {"(membrane-potential -65.1)", "(membrane-potential (scalar -65.1))"}, - {"(temperature-kelvin 301)", "(temperature-kelvin (scalar 301))"}, - {"(axial-resistivity 102)", "(axial-resistivity (scalar 102))"}, - }; - - for (const auto& [in, out]: literals) { - std::string res; - if (auto x = arborio::parse_expression(in)) { - std::visit([&](auto&& p){res = to_string(p);}, *(eval_cast_variant(*x))); - } - else { - res = x.error().what(); - } - EXPECT_EQ(res, out); - } -} - TEST(decor_literals, round_tripping) { auto paint_default_literals = { - "(membrane-potential (scalar -65.1))", - "(temperature-kelvin (scalar 301))", - "(axial-resistivity (scalar 102))", - "(membrane-capacitance (scalar 0.01))", - "(ion-internal-concentration \"ca\" (scalar 75.1))", - "(ion-external-concentration \"h\" (scalar -50.1))", - "(ion-reversal-potential \"na\" (scalar 30))"}; + "(membrane-potential 2 (scalar -65.1))", + "(temperature-kelvin 3 (scalar 301))", + "(axial-resistivity 4 (scalar 102))", + "(membrane-capacitance 5 (scalar 0.01))", + "(ion-internal-concentration \"ca\" 6 (scalar 75.1))", + "(ion-external-concentration \"h\" 7 (scalar -50.1))", + "(ion-reversal-potential \"na\" 8 (scalar 30))"}; auto paint_literals = { "(voltage-process (mechanism \"hh\"))", "(density (mechanism \"hh\"))", @@ -791,25 +771,25 @@ TEST(decor_literals, round_tripping) { TEST(decor_expressions, round_tripping) { using namespace cable_s_expr; auto decorate_paint_literals = { - "(paint (region \"all\") (membrane-potential (scalar -65.1)))", - "(paint (tag 1) (temperature-kelvin (scalar 301)))", - "(paint (distal-interval (location 3 0)) (axial-resistivity (scalar 102)))", - "(paint (join (region \"dend\") (all)) (membrane-capacitance (scalar 0.01)))", - "(paint (radius-gt (tag 3) 1) (ion-internal-concentration \"ca\" (scalar 75.1)))", - "(paint (intersect (cable 2 0 0.5) (region \"axon\")) (ion-external-concentration \"h\" (scalar -50.1)))", - "(paint (region \"my_region\") (ion-reversal-potential \"na\" (scalar 30)))", + "(paint (region \"all\") (membrane-potential 1 (scalar -65.1)))", + "(paint (tag 1) (temperature-kelvin 2 (scalar 301)))", + "(paint (distal-interval (location 3 0)) (axial-resistivity 2 (scalar 102)))", + "(paint (join (region \"dend\") (all)) (membrane-capacitance 3 (scalar 0.01)))", + "(paint (radius-gt (tag 3) 1) (ion-internal-concentration \"ca\" 4 (scalar 75.1)))", + "(paint (intersect (cable 2 0 0.5) (region \"axon\")) (ion-external-concentration \"h\" 5 (scalar -50.1)))", + "(paint (region \"my_region\") (ion-reversal-potential \"na\" 6 (scalar 30)))", "(paint (cable 2 0.1 0.4) (density (mechanism \"hh\")))", "(paint (cable 2 0.1 0.4) (scaled-mechanism (density (mechanism \"pas\" (\"g\" 0.02))) (\"g\" (exp (add (distance 2.1 (region \"my_region\")) (scalar 3.2))))))", "(paint (all) (density (mechanism \"pas\" (\"g\" 0.02))))" }; auto decorate_default_literals = { - "(default (membrane-potential (scalar -65.1)))", - "(default (temperature-kelvin (scalar 301)))", - "(default (axial-resistivity (scalar 102)))", - "(default (membrane-capacitance (scalar 0.01)))", - "(default (ion-internal-concentration \"ca\" (scalar 75.1)))", - "(default (ion-external-concentration \"h\" (scalar -50.1)))", - "(default (ion-reversal-potential \"na\" (scalar 30)))", + "(default (membrane-potential 1 (scalar -65.1)))", + "(default (temperature-kelvin 2 (scalar 301)))", + "(default (axial-resistivity 3 (scalar 102)))", + "(default (membrane-capacitance 4 (scalar 0.01)))", + "(default (ion-internal-concentration \"ca\" 5 (scalar 75.1)))", + "(default (ion-external-concentration \"h\" 6 (scalar -50.1)))", + "(default (ion-reversal-potential \"na\" 7 (scalar 30)))", "(default (ion-reversal-potential-method \"ca\" (mechanism \"nernst/ca\")))", "(default (cv-policy (max-extent 2 (region \"soma\") 2)))" }; @@ -875,14 +855,19 @@ TEST(morphology_literals, round_tripping) { } } +TEST(decor, quantity) { + std::string q = "(quantity 10.0 \"Ohm\")"; + parse_expression(q).value(); +} + TEST(decor, round_tripping) { std::string component_str = "(arbor-component \n" " (meta-data \n" " (version \"" + arborio::acc_version() +"\"))\n" " (decor \n" " (default \n" - " (axial-resistivity \n" - " (scalar 100)))\n" + " (axial-resistivity 100.000000 \n" + " (scalar 1)))\n" " (default \n" " (ion-reversal-potential-method \"na\" \n" " (mechanism \"nernst\")))\n" @@ -915,7 +900,7 @@ TEST(decor, round_tripping) { " (join \n" " (tag 1)\n" " (tag 2))\n" - " (ion-internal-concentration \"ca\" \n" + " (ion-internal-concentration \"ca\" 1.000000 \n" " (scalar 0.5)))\n" " (place \n" " (location 0 0)\n" @@ -1225,11 +1210,10 @@ TEST(doc_expressions, parse) { "(mechanism \"hh\" (\"gl\" 0.5) (\"el\" 2))", "(ion-reversal-potential-method \"ca\" (mechanism \"nernst/ca\"))", "(current-clamp (envelope (0 10) (50 10) (50 0)) 40 0.25)", - "(paint (tag 1) (membrane-capacitance 0.02))", + "(paint (tag 1) (membrane-capacitance 0.02 (scalar 1)))", "(place (locset \"mylocset\") (threshold-detector 10) \"mydetectors\")", - "(default (membrane-potential -65))", - "(segment 3 (point 0 0 0 5) (point 0 0 10 2) 1)"}) - { + "(default (membrane-potential -65.000000 (scalar 1)))", + "(segment 3 (point 0 0 0 5) (point 0 0 10 2) 1)"}) { EXPECT_TRUE(arborio::parse_expression(expr)); } @@ -1241,9 +1225,9 @@ TEST(doc_expressions, parse) { " (region-def \"my_region\" (radius-ge (region \"my_soma\") 1.5))\n" " (locset-def \"terminal\" (terminal)))", "(decor\n" - " (default (membrane-potential -55.000000))\n" - " (paint (region \"custom\") (temperature-kelvin 270))\n" - " (paint (region \"soma\") (membrane-potential -50.000000))\n" + " (default (membrane-potential -55.000000 (scalar 1)))\n" + " (paint (region \"custom\") (temperature-kelvin 270 (scalar 1)))\n" + " (paint (region \"soma\") (membrane-potential -50.000000 (scalar 1)))\n" " (paint (all) (density (mechanism \"pas\")))\n" " (paint (tag 4) (density (mechanism \"Ih\" (\"gbar\" 0.001))))\n" " (place (locset \"root\") (synapse (mechanism \"expsyn\")) \"root_synapse\")\n" @@ -1274,9 +1258,9 @@ TEST(doc_expressions, parse) { " (region-def \"my_region\" (radius-ge (region \"my_soma\") 1.5))\n" " (locset-def \"terminal\" (terminal)))\n" " (decor\n" - " (default (membrane-potential -55.000000))\n" - " (paint (region \"my_soma\") (temperature-kelvin 270))\n" - " (paint (region \"my_region\") (membrane-potential -50.000000))\n" + " (default (membrane-potential -55.000000 (scalar 1)))\n" + " (paint (region \"my_soma\") (temperature-kelvin 270 (scalar 1)))\n" + " (paint (region \"my_region\") (membrane-potential -50.000000 (scalar 1)))\n" " (paint (tag 4) (density (mechanism \"Ih\" (\"gbar\" 0.001))))\n" " (place (locset \"root\") (synapse (mechanism \"expsyn\")) \"root_synapse\")\n" " (place (location 1 0.2) (junction (mechanism \"gj\")) \"terminal_gj\"))\n" @@ -1315,9 +1299,9 @@ TEST(doc_expressions, parse) { "(arbor-component\n" " (meta-data (version \"" + arborio::acc_version() +"\"))\n" " (decor\n" - " (default (membrane-potential -55.000000))\n" + " (default (membrane-potential -55.000000 (scalar 1)))\n" " (place (locset \"root\") (synapse (mechanism \"expsyn\")) \"root_synapse\")\n" - " (paint (region \"my_soma\") (temperature-kelvin 270))))", + " (paint (region \"my_soma\") (temperature-kelvin 270 (scalar 1)))))", "(arbor-component\n" " (meta-data (version \"" + arborio::acc_version() +"\"))\n" " (morphology\n" @@ -1332,9 +1316,9 @@ TEST(doc_expressions, parse) { " (region-def \"my_soma\" (tag 1))\n" " (locset-def \"root\" (root)))\n" " (decor\n" - " (default (membrane-potential -55.000000))\n" + " (default (membrane-potential -55.000000 (scalar 1)))\n" " (place (locset \"root\") (synapse (mechanism \"expsyn\")) \"root_synapse\")\n" - " (paint (region \"my_soma\") (temperature-kelvin 270)))\n" + " (paint (region \"my_soma\") (temperature-kelvin 270 (scalar 1))))\n" " (morphology\n" " (branch 0 -1\n" " (segment 0 (point 0 0 0 2) (point 4 0 0 2) 1)\n" diff --git a/test/unit/test_schedule.cpp b/test/unit/test_schedule.cpp index 8452385160..6f6e98f307 100644 --- a/test/unit/test_schedule.cpp +++ b/test/unit/test_schedule.cpp @@ -17,9 +17,8 @@ using namespace testing; using time_range = util::range; -// Pull events from n non-contiguous subintervals of [t0, t1) -// and check for monotonicity and boundedness. - +// Pull events from n non-contiguous subintervals of [t0, t1) and check for +// monotonicity and boundedness. void run_invariant_checks(schedule S, time_type t0, time_type t1, unsigned n, int seed=0) { if (!n) return; @@ -45,14 +44,13 @@ void run_invariant_checks(schedule S, time_type t0, time_type t1, unsigned n, in } } -// Take events from n contiguous intervals comprising [t0, t1), reset, and -// then compare with events taken from a different set of contiguous -// intervals comprising [t0, t1). - +// Take events from n contiguous intervals comprising [t0, t1), reset, and then +// compare with events taken from a different set of contiguous intervals +// comprising [t0, t1). void run_reset_check(schedule S, time_type t0, time_type t1, unsigned n, int seed=0) { if (!n) return; - std::minstd_rand R(seed); + engine_type R(seed); std::uniform_real_distribution U(t0, t1); std::vector first_div = {t0, t1}; @@ -87,7 +85,7 @@ TEST(schedule, regular) { // Use exact fp representations for strict equality testing. std::vector expected = {0, 0.25, 0.5, 0.75, 1.0}; - schedule S = regular_schedule(0.25); + schedule S = regular_schedule(0.25*arb::units::ms); EXPECT_EQ(expected, as_vector(S.events(0, 1.25))); S.reset(); @@ -100,12 +98,12 @@ TEST(schedule, regular) { TEST(schedule, regular_invariants) { SCOPED_TRACE("regular_invariants"); - run_invariant_checks(regular_schedule(0.3), 3, 12, 7); + run_invariant_checks(regular_schedule(0.3*arb::units::ms), 3, 12, 7); } TEST(schedule, regular_reset) { SCOPED_TRACE("regular_reset"); - run_reset_check(regular_schedule(0.3), 3, 12, 7); + run_reset_check(regular_schedule(0.3*arb::units::ms), 3, 12, 7); } TEST(schedule, regular_rounding) { @@ -120,7 +118,7 @@ TEST(schedule, regular_rounding) { time_type t0 = t1-10*dt; time_type t2 = t1+10*dt; - schedule S = regular_schedule(t0, dt); + schedule S = regular_schedule(t0*arb::units::ms, dt*arb::units::ms); auto int_l = as_vector(S.events(t0, t1)); auto int_r = as_vector(S.events(t1, t2)); @@ -144,10 +142,10 @@ TEST(schedule, regular_rounding) { } TEST(schedule, explicit_schedule) { - time_type times[] = {0.1, 0.3, 1.0, 1.25, 1.7, 2.2}; - std::vector expected = {0.1, 0.3, 1.0}; + std::vector times{0.1, 0.3, 1.0, 1.25, 1.7, 2.2}; + std::vector expected{0.1, 0.3, 1.0}; - schedule S = explicit_schedule(times); + schedule S = explicit_schedule_from_milliseconds(times); EXPECT_EQ(expected, as_vector(S.events(0, 1.25))); S.reset(); @@ -161,47 +159,19 @@ TEST(schedule, explicit_schedule) { TEST(schedule, explicit_invariants) { SCOPED_TRACE("explicit_invariants"); - time_type times[] = {0.1, 0.3, 0.4, 0.42, 2.1, 2.3, 6.01, 9, 9.1, 9.8, 10, 11.2, 13}; - run_invariant_checks(explicit_schedule(times), 0.4, 10.2, 5); + std::vector times{0.1, 0.3, 0.4, 0.42, 2.1, 2.3, 6.01, 9, 9.1, 9.8, 10, 11.2, 13}; + run_invariant_checks(explicit_schedule_from_milliseconds(times), 0.4, 10.2, 5); } TEST(schedule, explicit_reset) { SCOPED_TRACE("explicit_reset"); - time_type times[] = {0.1, 0.3, 0.4, 0.42, 2.1, 2.3, 6.01, 9, 9.1, 9.8, 10, 11.2, 13}; - run_reset_check(explicit_schedule(times), 0.4, 10.2, 5); + std::vector times{0.1, 0.3, 0.4, 0.42, 2.1, 2.3, 6.01, 9, 9.1, 9.8, 10, 11.2, 13}; + run_reset_check(explicit_schedule_from_milliseconds(times), 0.4, 10.2, 5); } -// A Uniform Random Bit Generator[*] adaptor that deliberately -// skews the generated numbers by raising their quantile to -// the given power. -// -// [*] Not actually uniform. - -template -struct skew_adaptor { - using result_type = typename RNG::result_type; - static constexpr result_type min() { return RNG::min(); } - static constexpr result_type max() { return RNG::max(); } - - explicit skew_adaptor(double power): power_(power) {} - result_type operator()() { - constexpr double scale = (double)(max()-min()); - constexpr double ooscale = 1./scale; - - double x = ooscale*(G_()-min()); - x = std::pow(x, power_); - return min()+scale*x; - } - -private: - RNG G_; - double power_; -}; - -template -double poisson_schedule_dispersion(int nbin, double rate_kHz, RNG& G) { - schedule S = poisson_schedule(rate_kHz, G); +double poisson_schedule_dispersion(int nbin, double rate_kHz) { + schedule S = poisson_schedule(rate_kHz*arb::units::kHz); std::vector bin(nbin); for (auto t: time_range(S.events(0, nbin))) { @@ -238,8 +208,7 @@ TEST(schedule, poisson_uniformity) { constexpr double chi2_lb = 888.56352318146696; constexpr double chi2_ub = 1118.9480663231843; - std::mt19937_64 G; - double dispersion = poisson_schedule_dispersion(N, .813, G); + double dispersion = poisson_schedule_dispersion(N, .813); double test_value = N*dispersion; EXPECT_GT(test_value, chi2_lb); EXPECT_LT(test_value, chi2_ub); @@ -247,34 +216,12 @@ TEST(schedule, poisson_uniformity) { // Run one sample K-S test for uniformity, with critical // value for the finite K-S statistic Dn of α=0.01. - schedule S = poisson_schedule(100., G); + schedule S = poisson_schedule(100.*arb::units::kHz); auto events = as_vector(S.events(0,1)); int n = (int)events.size(); double dn = ks::dn_statistic(events); EXPECT_LT(ks::dn_cdf(dn, n), 0.99); - - // Check that these tests fail for a non-Poisson - // source. - - skew_adaptor W(1.5); - dispersion = poisson_schedule_dispersion(N, .813, W); - test_value = N*dispersion; - - EXPECT_FALSE(test_value>=chi2_lb && test_value<=chi2_ub); - - S = poisson_schedule(100., W); - events = as_vector(S.events(0,1)); - n = (int)events.size(); - dn = ks::dn_statistic(events); - - // This test is currently failing, because we can't - // use a sufficiently high `n` in the `dn_cdf` function - // to get enough discrimination from the K-S test at - // 1%. TODO: Fix this by implementing n>140 case in - // `dn_cdf`. - - // EXPECT_GT(ks::dn_cdf(dn, n), 0.99); } TEST(schedule, poisson_rate) { @@ -284,37 +231,26 @@ TEST(schedule, poisson_rate) { constexpr double alpha = 0.01; constexpr double lambda = 123.4; - std::mt19937_64 G; - schedule S = poisson_schedule(lambda, G); + schedule S = poisson_schedule(lambda*arb::units::kHz); int n = (int)time_range(S.events(0, 1)).size(); double cdf = poisson::poisson_cdf_approx(n, lambda); EXPECT_GT(cdf, alpha/2); - EXPECT_LT(cdf, 1-alpha/2); - - // Check that the test fails for a non-Poisson - // source. - - skew_adaptor W(1.5); - S = poisson_schedule(lambda, W); - n = (int)time_range(S.events(0, 1)).size(); - cdf = poisson::poisson_cdf_approx(n, lambda); - - EXPECT_FALSE(cdf>=alpha/2 && cdf<=1-alpha/2); + EXPECT_LT(cdf, 1 - alpha/2); } TEST(schedule, poisson_invariants) { SCOPED_TRACE("poisson_invariants"); - std::mt19937_64 G; - G.discard(100); - run_invariant_checks(poisson_schedule(0.81, G), 5.1, 15.3, 7); + auto sched = poisson_schedule(0.81*arb::units::kHz); + sched.discard(100); + run_invariant_checks(sched, 5.1, 15.3, 7); } TEST(schedule, poisson_reset) { SCOPED_TRACE("poisson_reset"); - std::mt19937_64 G; - G.discard(200); - run_reset_check(poisson_schedule(.11, G), 1, 10, 7); + auto sched = poisson_schedule(0.11*arb::units::kHz); + sched.discard(200); + run_reset_check(sched, 1, 10, 7); } TEST(schedule, poisson_offset) { @@ -322,43 +258,38 @@ TEST(schedule, poisson_offset) { // same sequence, after the offset, as a regular zero-based Poisson. const double offset = 3.3; - - std::mt19937_64 G1; - G1.discard(300); - + auto T = 100.0; + auto sched1 = poisson_schedule(.234*arb::units::kHz); + sched1.discard(300); std::vector expected; - for (auto t: as_vector(poisson_schedule(.234, G1).events(0., 100.))) { + for (auto t: as_vector(sched1.events(0., T))) { t += offset; - if (t<100.) { - expected.push_back(t); - } + if (t < T) expected.push_back(t); } - std::mt19937_64 G2; - G2.discard(300); - + auto sched2 = poisson_schedule(offset*arb::units::ms, .234*arb::units::kHz); + sched2.discard(300); EXPECT_TRUE(seq_almost_eq(expected, - as_vector(poisson_schedule(offset, .234, G2).events(0., 100.)))); + as_vector(sched2.events(0., 100.)))); } TEST(schedule, poisson_offset_reset) { SCOPED_TRACE("poisson_reset"); - std::mt19937_64 G; - G.discard(400); - run_reset_check(poisson_schedule(3.3, 9.1, G), 1, 10, 7); + auto sched = poisson_schedule(3.3*arb::units::ms, 0.81*arb::units::kHz); + sched.discard(400); + run_reset_check(sched, 1, 10, 7); } TEST(schedule, poisson_tstop) { SCOPED_TRACE("poisson_tstop"); - std::mt19937_64 G; - G.discard(500); - - const double tstop = 50; - auto const times = as_vector(poisson_schedule(0, .234, G, tstop).events(0., 100.)); + auto T = 50.0; + auto sched = poisson_schedule(0*arb::units::ms, 0.234*arb::units::kHz, default_seed, T*arb::units::ms); + sched.discard(500); + auto const times = as_vector(sched.events(0., 100.)); auto const max = std::max_element(begin(times), end(times)); EXPECT_TRUE(max != end(times)); - EXPECT_TRUE(*max <= tstop); + EXPECT_TRUE(*max <= T); } diff --git a/test/unit/test_sde.cpp b/test/unit/test_sde.cpp index 7383b7ef02..87f19824f3 100644 --- a/test/unit/test_sde.cpp +++ b/test/unit/test_sde.cpp @@ -1,9 +1,9 @@ - #include #include #include #include +#include #include @@ -14,6 +14,8 @@ #include #include #include +#include + #ifdef ARB_GPU_ENABLED #include "memory/gpu_wrappers.hpp" #endif @@ -23,6 +25,8 @@ #include "unit_test_catalogue.hpp" #include "../simple_recipes.hpp" +namespace U = arb::units; + // ============================ // helper classes and functions // ============================ @@ -409,7 +413,7 @@ TEST(sde, reproducibility) { // simulation parameters unsigned ncells = 4; unsigned ncvs = 2; - double const dt = 0.5; + auto const dt = 0.5*arb::units::ms; unsigned nsteps = 6; // Decorations with a bunch of stochastic processes @@ -490,7 +494,7 @@ TEST(sde, normality) { unsigned ncells = 4; unsigned nsynapses = 100; unsigned ncvs = 100; - double const dt = 0.5; + auto dt = 0.5*arb::units::ms;; unsigned nsteps = 50; // make labels (and locations for synapses) @@ -647,7 +651,7 @@ TEST(sde, solver) { unsigned ncells = 4; unsigned nsynapses = 2000; unsigned ncvs = 1; - double const dt = 1.0/512; // need relatively small time steps due to low accuracy + auto dt = 1.0/512*arb::units::ms; // need relatively small time steps due to low accuracy unsigned nsteps = 100; unsigned nsims = 4; @@ -773,7 +777,7 @@ TEST(sde, solver) { auto test = [&] (auto func, const auto& stats) { for (unsigned int i=1; i event_generators(arb::cell_gid_type gid) const override { std::vector res; - if (!gid) res.push_back(arb::regular_generator({"synapse"}, 1, 0.5, 0.73)); + if (!gid) res.push_back(arb::regular_generator({"synapse"}, 1, 0.5*arb::units::ms, 0.73*arb::units::ms)); return {}; } @@ -190,8 +192,8 @@ void sampler(arb::probe_metadata pm, } TEST(serdes, single_cell) { - double dt = 0.5; - double T = 5; + auto dt = 0.5*arb::units::ms; + auto T = 5*arb::units::ms; // Result std::vector result_pre; @@ -231,8 +233,8 @@ TEST(serdes, single_cell) { } TEST(serdes, network) { - double dt = 0.5; - double T = 5; + auto dt = 0.5*arb::units::ms; + auto T = 5*arb::units::ms; // Result std::vector result_pre; @@ -300,8 +302,8 @@ TEST(serdes, host_device_arrays) { } TEST(serdes, single_cell_gpu) { - double dt = 0.5; - double T = 5; + auto dt = 0.5*arb::units::ms; + auto T = 5*arb::units::ms; // Result std::vector result_pre; @@ -340,8 +342,8 @@ TEST(serdes, single_cell_gpu) { } TEST(serdes, network_gpu) { - double dt = 0.5; - double T = 5; + auto dt = 0.5*arb::units::ms; + auto T = 5*arb::units::ms; // Result std::vector result_pre; diff --git a/test/unit/test_simulation.cpp b/test/unit/test_simulation.cpp index 88bdec4072..8fa4c08d57 100644 --- a/test/unit/test_simulation.cpp +++ b/test/unit/test_simulation.cpp @@ -1,6 +1,5 @@ #include -#include #include #include @@ -17,18 +16,15 @@ #include "util/rangeutil.hpp" #include "util/transform.hpp" -#include "common.hpp" using namespace arb; +namespace U = arb::units; struct play_spikes: public recipe { play_spikes(std::vector spike_times): spike_times_(std::move(spike_times)) {} cell_size_type num_cells() const override { return spike_times_.size(); } cell_kind get_cell_kind(cell_gid_type) const override { return cell_kind::spike_source; } - util::unique_any get_cell_description(cell_gid_type gid) const override { - return spike_source_cell("src", spike_times_.at(gid)); - } - + util::unique_any get_cell_description(cell_gid_type gid) const override { return spike_source_cell("src", spike_times_.at(gid)); } std::vector spike_times_; }; @@ -52,7 +48,7 @@ TEST(simulation, null) { auto c = arb::make_context(); auto d = arb::partition_load_balance(r, c); auto s = arb::simulation(r, c, d); - s.run(0.05, 0.01); + s.run(0.05*arb::units::ms, 0.01*arb::units::ms); } // Test with simulation builder @@ -60,22 +56,23 @@ TEST(simulation, null_builder) { auto r = null_recipe{}; { arb::simulation s = arb::simulation::create(r); - s.run(0.05, 0.01); + s.run(0.05*arb::units::ms, + 0.01*arb::units::ms); } { arb::simulation s = arb::simulation::create(r).set_seed(42); - s.run(0.05, 0.01); + s.run(0.05*arb::units::ms, 0.01*arb::units::ms); } { auto c = arb::make_context(); arb::simulation s = arb::simulation::create(r).set_context(c); - s.run(0.05, 0.01); + s.run(0.05*arb::units::ms, 0.01*arb::units::ms); } { auto c = arb::make_context(); auto d = arb::partition_load_balance(r, c); arb::simulation s = arb::simulation::create(r).set_context(c).set_decomposition(d); - s.run(0.05, 0.01); + s.run(0.05*arb::units::ms, 0.01*arb::units::ms); } } @@ -85,7 +82,7 @@ TEST(simulation, spike_global_callback) { std::vector spike_times; for (unsigned i = 0; i expected_spikes; @@ -107,7 +104,7 @@ TEST(simulation, spike_global_callback) { double tfinal = 0.7*t_max; constexpr double dt = 0.01; - sim.run(tfinal, dt); + sim.run(tfinal*arb::units::ms, dt*arb::units::ms); auto spike_lt = [](spike a, spike b) { return a.time connections_on(cell_gid_type target) const override { - if (target) { - return {cell_connection({target-1, "src"}, {"tgt"}, weight_, delay_)}; - } - else { - return {}; - } + if (target) return {cell_connection({target-1, "src"}, {"tgt"}, weight_, delay_*U::ms)}; + return {}; } std::vector event_generators(cell_gid_type target) const override { - if (target) { - return {}; - } - else { - return {event_generator({"tgt"}, weight_, triggers_)}; - } + if (target) return {}; + return {event_generator({"tgt"}, weight_, triggers_)}; } static constexpr double weight_ = 2.0; @@ -162,7 +151,7 @@ TEST(simulation, restart) { std::vector trigger_times = {1., 2., 3.}; double delay = 10; unsigned n = 5; - lif_chain rec(n, delay, explicit_schedule(trigger_times)); + lif_chain rec(n, delay, explicit_schedule_from_milliseconds(trigger_times)); // Expect spike times to be almost exactly according to trigger times, // plus delays along the chain of cells. @@ -203,7 +192,7 @@ TEST(simulation, restart) { double t = 0; do { double run_to = std::min(tfinal, t + run_time); - t = sim.run(run_to, dt); + t = sim.run(run_to*arb::units::ms, dt*arb::units::ms); ASSERT_EQ(t, run_to); } while (t // Test that a spike_source_cell_group identifies itself with the correct // cell_kind enum value. -TEST(spike_source, cell_kind) -{ +TEST(spike_source, cell_kind) { ss_recipe rec(1u, spike_source_cell("src", explicit_schedule({}))); cell_label_range srcs, tgts; spike_source_cell_group group({0}, rec, srcs, tgts); @@ -37,8 +36,7 @@ static std::vector spike_times(const std::vector& evs) { // Test that a spike_source_cell_group produces a sequence of spikes with spike // times corresponding to the underlying time_seq. -TEST(spike_source, matches_time_seq) -{ +TEST(spike_source, matches_time_seq) { auto test_seq = [](schedule seq) { ss_recipe rec(1u, spike_source_cell("src", seq)); cell_label_range srcs, tgts; @@ -57,16 +55,14 @@ TEST(spike_source, matches_time_seq) EXPECT_EQ(spike_times(group.spikes()), as_vector(seq.events(10, 20))); }; - std::mt19937_64 G; - test_seq(regular_schedule(0, 1)); - test_seq(poisson_schedule(10, G)); // produce many spikes in each interval - test_seq(poisson_schedule(1e-6, G)); // very unlikely to produce any spikes in either interval + test_seq(regular_schedule(1*arb::units::ms)); + test_seq(poisson_schedule(10*arb::units::kHz)); // produce many spikes in each interval + test_seq(poisson_schedule(1e-6*arb::units::kHz)); // very unlikely to produce any spikes in either interval } // Test that a spike_source_cell_group will produce the same sequence of spikes // after being reset. -TEST(spike_source, reset) -{ +TEST(spike_source, reset) { auto test_seq = [](schedule seq) { ss_recipe rec(1u, spike_source_cell("src", seq)); cell_label_range srcs, tgts; @@ -87,16 +83,14 @@ TEST(spike_source, reset) EXPECT_EQ(spikes1, spikes2); }; - std::mt19937_64 G; - test_seq(regular_schedule(0, 1)); - test_seq(poisson_schedule(10, G)); // produce many spikes in each interval - test_seq(poisson_schedule(1e-6, G)); // very unlikely to produce any spikes in either interval + test_seq(regular_schedule(10*arb::units::ms)); + test_seq(poisson_schedule(100*arb::units::kHz)); // produce many spikes in each interval + test_seq(poisson_schedule(1e-6*arb::units::kHz)); // very unlikely to produce any spikes in either interval } // Test that a spike_source_cell_group will produce the expected // output when the underlying time_seq is finite. -TEST(spike_source, exhaust) -{ +TEST(spike_source, exhaust) { // This test assumes that seq will exhaust itself before t=10 ms. auto test_seq = [](schedule seq) { ss_recipe rec(1u, spike_source_cell("src", seq)); @@ -112,12 +106,11 @@ TEST(spike_source, exhaust) EXPECT_LT(group.spikes().back().time, time_type(10)); }; - test_seq(regular_schedule(0, 1, 5)); - test_seq(explicit_schedule({0.3, 2.3, 4.7})); + test_seq(regular_schedule(0*arb::units::ms, 1*arb::units::ms, 5*arb::units::ms)); + test_seq(explicit_schedule_from_milliseconds(std::vector{0.3, 2.3, 4.7})); } -TEST(spike_source, multiple) -{ +TEST(spike_source, multiple) { // This test assumes that seq will exhaust itself before t=10 ms. auto test_seq = [](auto&&... seqs) { std::vector schedules{seqs...}; @@ -145,13 +138,7 @@ TEST(spike_source, multiple) EXPECT_LT(group.spikes().back().time, time_type(10)); }; - auto seqs = std::vector{regular_schedule(0, 1, 5), - explicit_schedule({0.3, 2.3, 4.7})}; + std::vector seqs{regular_schedule(0*arb::units::ms, 1*arb::units::ms, 5*arb::units::ms), + explicit_schedule_from_milliseconds(std::vector{0.3, 2.3, 4.7})}; test_seq(seqs); - test_seq(std::vector{regular_schedule(0, 1, 5), - explicit_schedule({0.3, 2.3, 4.7})}); - test_seq(regular_schedule(0, 1, 5), - explicit_schedule({0.3, 2.3, 4.7})); - auto reg_sched = regular_schedule(0, 1, 5); - test_seq(reg_sched, explicit_schedule({0.3, 2.3, 4.7})); } diff --git a/test/unit/test_spikes.cpp b/test/unit/test_spikes.cpp index c27b6d1990..031b9bce89 100644 --- a/test/unit/test_spikes.cpp +++ b/test/unit/test_spikes.cpp @@ -203,8 +203,8 @@ TEST(SPIKES_TEST_CLASS, threshold_watcher) { } TEST(SPIKES_TEST_CLASS, threshold_watcher_interpolation) { - double dt = 0.025; - double duration = 1; + auto dt = 0.025*arb::units::ms; + auto duration = 1*arb::units::ms; arb::segment_tree tree; tree.append(arb::mnpos, { -6.3, 0.0, 0.0, 6.3}, { 6.3, 0.0, 0.0, 6.3}, 1); @@ -223,8 +223,8 @@ TEST(SPIKES_TEST_CLASS, threshold_watcher_interpolation) { for (unsigned i = 0; i < 8; i++) { arb::decor decor; decor.set_default(arb::cv_policy_every_segment()); - decor.place("mid"_lab, arb::threshold_detector{10}, "detector"); - decor.place("mid"_lab, arb::i_clamp::box(0.01+i*dt, duration, 0.5), "clamp"); + decor.place("mid"_lab, arb::threshold_detector{10*arb::units::mV}, "detector"); + decor.place("mid"_lab, arb::i_clamp::box(0.01*arb::units::ms + i*dt, duration, 0.5*arb::units::nA), "clamp"); decor.place("mid"_lab, arb::synapse("expsyn"), "synapse"); arb::cable_cell cell(morpho, decor, dict); @@ -243,7 +243,7 @@ TEST(SPIKES_TEST_CLASS, threshold_watcher_interpolation) { } for (unsigned i = 1; i < spikes.size(); ++i) { - EXPECT_NEAR(dt, spikes[i].time - spikes[i-1].time, 1e-4); + EXPECT_NEAR(dt.value(), spikes[i].time - spikes[i-1].time, 1e-4); } } diff --git a/test/unit/test_synapses.cpp b/test/unit/test_synapses.cpp index 2c00ce0382..8e56151faa 100644 --- a/test/unit/test_synapses.cpp +++ b/test/unit/test_synapses.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include @@ -10,7 +9,6 @@ #include #include "backends/multicore/fvm.hpp" -#include "util/maputil.hpp" #include "util/range.hpp" #include "../common_cells.hpp" diff --git a/test/unit/test_v_clamp.cpp b/test/unit/test_v_clamp.cpp index 2142d45ce6..cb5dd6d460 100644 --- a/test/unit/test_v_clamp.cpp +++ b/test/unit/test_v_clamp.cpp @@ -41,7 +41,7 @@ struct v_proc_recipe: public arb::recipe { {arb::cable_probe_membrane_voltage{"(location 0 0.625)"_ls}, "Um-(0, 0.625)"}}; // dend center: 0.75/2 + 0.25 } std::vector event_generators(arb::cell_gid_type) const override { - return {arb::regular_generator({"tgt"}, 5.0, 0.2, 0.05)}; + return {arb::regular_generator({"tgt"}, 5.0, 0.2*arb::units::ms, 0.05*arb::units::ms)}; } std::any get_global_properties(arb::cell_kind) const override { return gprop; } @@ -90,8 +90,8 @@ TEST(v_process, clamp) { } }; auto sim = arb::simulation(v_proc_recipe{true, false}); - sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05), fun); - sim.run(1.0, 0.005); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05*arb::units::ms), fun); + sim.run(1.0*arb::units::ms, 0.005*arb::units::ms); um_s_type exp_soma{{ 0, -65 }, { 0.05, -42 }, @@ -158,8 +158,8 @@ TEST(v_process, limit) { } }; auto sim = arb::simulation(v_proc_recipe{false, true}); - sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05), fun); - sim.run(1.0, 0.005); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05*arb::units::ms), fun); + sim.run(1.0*arb::units::ms, 0.005*arb::units::ms); um_s_type exp_soma{{ 0, -65 }, { 0.05, -60 }, @@ -228,8 +228,8 @@ TEST(v_process, clamp_fine) { auto rec = v_proc_recipe{true, false}; rec.gprop.default_parameters.discretization = arb::cv_policy_max_extent(0.5); auto sim = arb::simulation(rec); - sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05), fun); - sim.run(1.0, 0.005); + sim.add_sampler(arb::all_probes, arb::regular_schedule(0.05*arb::units::ms), fun); + sim.run(1.0*arb::units::ms, 0.005*arb::units::ms); um_s_type exp_soma{{ 0, -65 }, { 0.05, -42 },