From bf2ffa73154a6b6e3c0677e4d80856d416ca0583 Mon Sep 17 00:00:00 2001 From: Alan Date: Sun, 19 Jun 2022 13:45:53 -0500 Subject: [PATCH] copying to personal repo --- .gitignore | 31 + LICENSE | 30 + README.rst | 343 +++ Windows_setup.md | 321 +++ _config.yml | 1 + cnmodel/__init__.py | 25 + cnmodel/cells/__init__.py | 23 + cnmodel/cells/bushy.py | 967 ++++++++ cnmodel/cells/cartwheel.py | 447 ++++ cnmodel/cells/cell.py | 1200 ++++++++++ cnmodel/cells/dstellate.py | 897 +++++++ cnmodel/cells/hh.py | 47 + cnmodel/cells/msoprincipal.py | 516 ++++ cnmodel/cells/octopus.py | 718 ++++++ cnmodel/cells/pyramidal.py | 482 ++++ cnmodel/cells/sgc.py | 467 ++++ cnmodel/cells/tests/cell_data/SGC_rat_a.pk | 3 + cnmodel/cells/tests/cell_data/SGC_rat_bm.pk | 3 + .../tests/cell_data/bushy-mouse-typeII.pk | 3 + .../cell_data/bushy_guineapig-typeII-I.pk | 3 + .../tests/cell_data/bushy_guineapig-typeII.pk | 3 + .../cells/tests/cell_data/cartwheel_rat_I.pk | 3 + .../cell_data/dstellate_guineapig-typeI-II.pk | 3 + .../cell_data/dstellate_mouse-typeI-II.pk | 3 + .../cell_data/octopus_guineapig-typeII-o.pk | 3 + .../cells/tests/cell_data/pyramidal_rat_I.pk | 3 + .../cell_data/tstellate_guineapig-typeI-c.pk | 3 + .../cell_data/tstellate_guineapig-typeI-t.pk | 3 + .../cell_data/tstellate_mouse-typeI-c.pk | 3 + .../cell_data/tuberculoventral_mouse_I.pk | 3 + cnmodel/cells/tests/test_cells.py | 227 ++ cnmodel/cells/tstellate.py | 1089 +++++++++ cnmodel/cells/tuberculoventral.py | 619 +++++ cnmodel/custom_init.hoc | 23 + cnmodel/data/__init__.py | 18 + cnmodel/data/_db.py | 321 +++ cnmodel/data/connectivity.py | 234 ++ cnmodel/data/ionchannels.py | 582 +++++ cnmodel/data/populations.py | 37 + cnmodel/data/synapses.py | 503 ++++ cnmodel/data/tests/test_db.py | 72 + cnmodel/decorator/__init__.py | 9 + cnmodel/decorator/decorator.py | 392 +++ cnmodel/mechanisms/CaPCalyx.mod | 109 + cnmodel/mechanisms/Gly5GC.mod | 134 ++ cnmodel/mechanisms/Gly5PL.mod | 149 ++ cnmodel/mechanisms/Gly5State.mod | 147 ++ cnmodel/mechanisms/Gly6S.mod | 131 + cnmodel/mechanisms/Iclamp2.mod | 64 + cnmodel/mechanisms/NMDA.mod | 142 ++ cnmodel/mechanisms/NMDA_Kampa.mod | 212 ++ cnmodel/mechanisms/adex.mod | 125 + cnmodel/mechanisms/ampa_trussell.mod | 224 ++ cnmodel/mechanisms/atm.mod | 129 + cnmodel/mechanisms/bkpkj.mod | 98 + cnmodel/mechanisms/cabpump.mod | 157 ++ cnmodel/mechanisms/cadiff.mod | 50 + cnmodel/mechanisms/cadyn.mod | 100 + cnmodel/mechanisms/cap.mod | 86 + cnmodel/mechanisms/capmp.mod | 71 + cnmodel/mechanisms/capump.mod | 32 + cnmodel/mechanisms/cleftXmtr.mod | 88 + cnmodel/mechanisms/gly.mod | 65 + cnmodel/mechanisms/gly2.mod | 196 ++ cnmodel/mechanisms/hcno.mod | 101 + cnmodel/mechanisms/hcno_bo.mod | 103 + cnmodel/mechanisms/iStim.mod | 48 + cnmodel/mechanisms/ihpkj.mod | 56 + cnmodel/mechanisms/ihpyr.mod | 134 ++ cnmodel/mechanisms/ihpyr_adj.mod | 142 ++ cnmodel/mechanisms/ihsgc_apical.mod | 155 ++ cnmodel/mechanisms/ihsgc_basalmiddle.mod | 154 ++ cnmodel/mechanisms/ihvcn.mod | 79 + cnmodel/mechanisms/inav11.mod | 188 ++ cnmodel/mechanisms/jsrnaf.mod | 136 ++ cnmodel/mechanisms/ka.mod | 104 + cnmodel/mechanisms/kcnq.mod | 72 + cnmodel/mechanisms/kdpyr.mod | 92 + cnmodel/mechanisms/kht.mod | 111 + cnmodel/mechanisms/kif.mod | 128 + cnmodel/mechanisms/kir.mod | 108 + cnmodel/mechanisms/kis.mod | 123 + cnmodel/mechanisms/klt.mod | 109 + cnmodel/mechanisms/kpkj.mod | 94 + cnmodel/mechanisms/kpkj2.mod | 67 + cnmodel/mechanisms/kpkjslow.mod | 63 + cnmodel/mechanisms/kpksk.mod | 111 + cnmodel/mechanisms/leak.mod | 32 + cnmodel/mechanisms/multisite.mod | 379 +++ cnmodel/mechanisms/na.mod | 100 + cnmodel/mechanisms/nacn.mod | 103 + cnmodel/mechanisms/nacncoop.mod | 138 ++ cnmodel/mechanisms/nap.mod | 130 + cnmodel/mechanisms/napyr.mod | 137 ++ cnmodel/mechanisms/pkjlk.mod | 17 + cnmodel/mechanisms/rsg.mod | 196 ++ cnmodel/mechanisms/tests/test_mechanisms.py | 40 + cnmodel/mechanisms/vecevent.mod | 72 + cnmodel/morphology/__init__.py | 12 + cnmodel/morphology/bushy_stick.hoc | 90 + cnmodel/morphology/hoc_reader.py | 622 +++++ cnmodel/morphology/morphology.py | 31 + cnmodel/morphology/octopus_spencer_stick.hoc | 76 + cnmodel/morphology/tstellate_stick.hoc | 82 + cnmodel/morphology/tv_stick.hoc | 32 + cnmodel/populations/__init__.py | 13 + cnmodel/populations/bushy.py | 58 + cnmodel/populations/dstellate.py | 42 + cnmodel/populations/population.py | 443 ++++ cnmodel/populations/pyramidal.py | 26 + cnmodel/populations/sgc.py | 87 + cnmodel/populations/tstellate.py | 43 + cnmodel/populations/tuberculoventral.py | 46 + cnmodel/protocols/__init__.py | 7 + cnmodel/protocols/cc.py | 91 + cnmodel/protocols/democlamp.py | 185 ++ cnmodel/protocols/iv_curve.py | 612 +++++ cnmodel/protocols/population_test.py | 150 ++ cnmodel/protocols/protocol.py | 43 + cnmodel/protocols/simple_synapse_test.py | 111 + cnmodel/protocols/synapse_test.py | 779 ++++++ cnmodel/protocols/vc_curve.py | 206 ++ cnmodel/synapses/__init__.py | 22 + cnmodel/synapses/exp2_psd.py | 75 + cnmodel/synapses/glu_psd.py | 146 ++ cnmodel/synapses/gly_psd.py | 398 ++++ cnmodel/synapses/psd.py | 41 + cnmodel/synapses/simple_terminal.py | 52 + cnmodel/synapses/stochastic_terminal.py | 393 +++ cnmodel/synapses/synapse.py | 15 + cnmodel/synapses/terminal.py | 32 + .../tests/test_data/dstellate_bushy.pk | 3 + .../tests/test_data/dstellate_dstellate.pk | 3 + .../tests/test_data/dstellate_tstellate.pk | 3 + cnmodel/synapses/tests/test_data/sgc_bushy.pk | 3 + .../synapses/tests/test_data/sgc_dstellate.pk | 3 + .../synapses/tests/test_data/sgc_tstellate.pk | 3 + cnmodel/synapses/tests/test_psd.py | 190 ++ cnmodel/synapses/tests/test_synapses.py | 119 + cnmodel/util/Params.py | 69 + cnmodel/util/PlotHelpers.py | 1415 +++++++++++ cnmodel/util/__init__.py | 9 + cnmodel/util/ccstim.py | 125 + cnmodel/util/compare_simple_multisynapses.py | 658 +++++ cnmodel/util/difftreewidget/DataTreeWidget.py | 131 + cnmodel/util/difftreewidget/DiffTreeWidget.py | 170 ++ cnmodel/util/difftreewidget/README | 2 + cnmodel/util/difftreewidget/TableWidget.py | 515 ++++ cnmodel/util/difftreewidget/__init__.py | 1 + cnmodel/util/expfitting.py | 83 + cnmodel/util/filelock.py | 133 ++ cnmodel/util/find_point.py | 95 + cnmodel/util/fitting.py | 219 ++ cnmodel/util/get_anspikes.py | 477 ++++ cnmodel/util/matlab_proc.py | 402 ++++ cnmodel/util/nrnutils.py | 218 ++ cnmodel/util/process.py | 111 + cnmodel/util/pynrnutilities.py | 734 ++++++ cnmodel/util/pyqtgraphPlotHelpers.py | 1505 ++++++++++++ cnmodel/util/random_seed.py | 28 + cnmodel/util/sound.py | 1415 +++++++++++ cnmodel/util/stim.py | 87 + cnmodel/util/talbotetalTicks.py | 208 ++ cnmodel/util/tests/test_expfitting.py | 36 + cnmodel/util/tests/test_matlab.py | 40 + cnmodel/util/tests/test_sound.py | 98 + cnmodel/util/tests/test_stim.py | 29 + cnmodel/util/user_tester.py | 205 ++ doc/Makefile | 20 + doc/make.bat | 242 ++ doc/numpydoc-0.5/LICENSE.txt | 94 + doc/numpydoc-0.5/MANIFEST.in | 2 + doc/numpydoc-0.5/PKG-INFO | 16 + doc/numpydoc-0.5/README.rst | 57 + doc/numpydoc-0.5/numpydoc.egg-info/PKG-INFO | 16 + .../numpydoc.egg-info/SOURCES.txt | 23 + .../numpydoc.egg-info/dependency_links.txt | 1 + .../numpydoc.egg-info/top_level.txt | 1 + doc/numpydoc-0.5/numpydoc/__init__.py | 3 + doc/numpydoc-0.5/numpydoc/comment_eater.py | 181 ++ doc/numpydoc-0.5/numpydoc/compiler_unparse.py | 882 +++++++ doc/numpydoc-0.5/numpydoc/docscrape.py | 567 +++++ doc/numpydoc-0.5/numpydoc/docscrape_sphinx.py | 284 +++ doc/numpydoc-0.5/numpydoc/linkcode.py | 84 + doc/numpydoc-0.5/numpydoc/numpydoc.py | 201 ++ doc/numpydoc-0.5/numpydoc/phantom_import.py | 181 ++ doc/numpydoc-0.5/numpydoc/plot_directive.py | 697 ++++++ .../numpydoc/tests/test_docscrape.py | 841 +++++++ .../numpydoc/tests/test_linkcode.py | 5 + .../numpydoc/tests/test_phantom_import.py | 14 + .../numpydoc/tests/test_plot_directive.py | 15 + .../numpydoc/tests/test_traitsdoc.py | 13 + doc/numpydoc-0.5/numpydoc/traitsdoc.py | 143 ++ doc/numpydoc-0.5/setup.cfg | 5 + doc/numpydoc-0.5/setup.py | 32 + doc/source/an_model.rst | 28 + doc/source/architecture.rst | 180 ++ doc/source/architecture.svg | 1740 ++++++++++++++ doc/source/cells.rst | 106 + doc/source/conf.py | 167 ++ doc/source/data.rst | 37 + doc/source/decorator.rst | 16 + doc/source/examples.rst | 31 + doc/source/examples/figures.rst | 22 + doc/source/examples/play_test_sounds.rst | 10 + doc/source/examples/plot_hcno_kinetics.rst | 10 + doc/source/examples/test_an_model.rst | 10 + doc/source/examples/test_bushy_variation.rst | 10 + doc/source/examples/test_ccstim.rst | 10 + doc/source/examples/test_cells.rst | 10 + doc/source/examples/test_circuit.rst | 10 + doc/source/examples/test_decorator.rst | 10 + doc/source/examples/test_mechanisms.rst | 10 + doc/source/examples/test_mso_inputs.rst | 10 + doc/source/examples/test_physiology.rst | 10 + doc/source/examples/test_populations.rst | 10 + doc/source/examples/test_sgc_input.rst | 10 + doc/source/examples/test_sgc_input_PSTH.rst | 10 + .../examples/test_sgc_input_phaselocking.rst | 10 + doc/source/examples/test_simple_synapses.rst | 10 + doc/source/examples/test_sound_stim.rst | 10 + doc/source/examples/test_sounds.rst | 10 + doc/source/examples/test_synapses.rst | 10 + doc/source/examples/toy_model.rst | 10 + doc/source/index.rst | 32 + doc/source/modules.rst | 18 + doc/source/morphology.rst | 26 + doc/source/populations.rst | 54 + doc/source/protocols.rst | 72 + doc/source/readme.rst | 1 + doc/source/synapses.rst | 83 + doc/source/util.rst | 147 ++ examples/LC_bushy.hoc | 955 ++++++++ examples/__init__.py | 1 + examples/figures.py | 64 + examples/figures.sh | 44 + examples/gif | 1 + examples/play_test_sounds.py | 146 ++ examples/plot_hcno_kinetics.py | 81 + examples/stim172_geese.wav | Bin 0 -> 176444 bytes examples/test_adex.py | 340 +++ examples/test_an_model.py | 102 + examples/test_bushy_variation.py | 1047 ++++++++ examples/test_ccstim.py | 77 + examples/test_cells.py | 515 ++++ examples/test_circuit.py | 60 + examples/test_decorator.py | 151 ++ examples/test_mechanisms.py | 317 +++ examples/test_mso_inputs.py | 208 ++ examples/test_physiology.py | 704 ++++++ examples/test_populations.py | 59 + examples/test_sgc_input.py | 96 + examples/test_sgc_input_PSTH.py | 431 ++++ examples/test_sgc_input_phaselocking.py | 248 ++ examples/test_simple_synapses.py | 97 + examples/test_sound_stim.py | 116 + examples/test_sounds.py | 70 + examples/test_synapses.py | 210 ++ examples/toy_model.py | 312 +++ modified_dend/hoc_trial_run_dendrites.py | 178 ++ .../network_pauser_prototype_dendrites.py | 298 +++ .../Figures/3 presentation pyramidal cell.png | Bin 0 -> 55192 bytes project/Figures/Final graph.png | Bin 0 -> 82445 bytes .../Network with TV DS 200 presentations.png | Bin 0 -> 205508 bytes ... TV DS and Clamp 200 presentations 4-1.png | Bin 0 -> 184012 bytes project/Figures/examples.png | Bin 0 -> 17913 bytes project/Figures/examples2.png | Bin 0 -> 19972 bytes ...ynapse 500 presentations test_sgc_psth.png | Bin 0 -> 97436 bytes project/Figures/test 1 pres.png | Bin 0 -> 79704 bytes project/__init__.py | 1 + project/graphs.py | 167 ++ project/hoc_trial_run_cartwheel.py | 160 ++ project/hoc_trial_run_dendrites.py | 150 ++ project/network_pauser_prototype_cartwheel.py | 311 +++ project/network_pauser_prototype_dendrites.py | 298 +++ .../Ad041599_062_Buildup_psth.txt | 2107 +++++++++++++++++ .../Ad081098_065_PauserBuildup_psth.txt | 1810 ++++++++++++++ .../Ad081998_199_WideChopper_psth.txt | 1392 +++++++++++ project/previous_version/histgen.py | 96 + project/previous_version/test_network_PSTH.py | 451 ++++ .../previous_version/test_single_cell_psth.py | 412 ++++ .../tone_pyram_sgc_input_PSTH2.7.py | 397 ++++ project/util/__init__.py | 0 project/util/tools.py | 35 + setup.py | 25 + test.py | 48 + todo.rst | 44 + 287 files changed, 54032 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 Windows_setup.md create mode 100644 _config.yml create mode 100755 cnmodel/__init__.py create mode 100644 cnmodel/cells/__init__.py create mode 100644 cnmodel/cells/bushy.py create mode 100644 cnmodel/cells/cartwheel.py create mode 100644 cnmodel/cells/cell.py create mode 100644 cnmodel/cells/dstellate.py create mode 100644 cnmodel/cells/hh.py create mode 100644 cnmodel/cells/msoprincipal.py create mode 100644 cnmodel/cells/octopus.py create mode 100644 cnmodel/cells/pyramidal.py create mode 100644 cnmodel/cells/sgc.py create mode 100644 cnmodel/cells/tests/cell_data/SGC_rat_a.pk create mode 100644 cnmodel/cells/tests/cell_data/SGC_rat_bm.pk create mode 100644 cnmodel/cells/tests/cell_data/bushy-mouse-typeII.pk create mode 100644 cnmodel/cells/tests/cell_data/bushy_guineapig-typeII-I.pk create mode 100644 cnmodel/cells/tests/cell_data/bushy_guineapig-typeII.pk create mode 100644 cnmodel/cells/tests/cell_data/cartwheel_rat_I.pk create mode 100644 cnmodel/cells/tests/cell_data/dstellate_guineapig-typeI-II.pk create mode 100644 cnmodel/cells/tests/cell_data/dstellate_mouse-typeI-II.pk create mode 100644 cnmodel/cells/tests/cell_data/octopus_guineapig-typeII-o.pk create mode 100644 cnmodel/cells/tests/cell_data/pyramidal_rat_I.pk create mode 100644 cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-c.pk create mode 100644 cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-t.pk create mode 100644 cnmodel/cells/tests/cell_data/tstellate_mouse-typeI-c.pk create mode 100644 cnmodel/cells/tests/cell_data/tuberculoventral_mouse_I.pk create mode 100644 cnmodel/cells/tests/test_cells.py create mode 100644 cnmodel/cells/tstellate.py create mode 100644 cnmodel/cells/tuberculoventral.py create mode 100755 cnmodel/custom_init.hoc create mode 100644 cnmodel/data/__init__.py create mode 100644 cnmodel/data/_db.py create mode 100644 cnmodel/data/connectivity.py create mode 100644 cnmodel/data/ionchannels.py create mode 100644 cnmodel/data/populations.py create mode 100644 cnmodel/data/synapses.py create mode 100644 cnmodel/data/tests/test_db.py create mode 100755 cnmodel/decorator/__init__.py create mode 100644 cnmodel/decorator/decorator.py create mode 100644 cnmodel/mechanisms/CaPCalyx.mod create mode 100755 cnmodel/mechanisms/Gly5GC.mod create mode 100755 cnmodel/mechanisms/Gly5PL.mod create mode 100755 cnmodel/mechanisms/Gly5State.mod create mode 100755 cnmodel/mechanisms/Gly6S.mod create mode 100644 cnmodel/mechanisms/Iclamp2.mod create mode 100644 cnmodel/mechanisms/NMDA.mod create mode 100644 cnmodel/mechanisms/NMDA_Kampa.mod create mode 100644 cnmodel/mechanisms/adex.mod create mode 100644 cnmodel/mechanisms/ampa_trussell.mod create mode 100644 cnmodel/mechanisms/atm.mod create mode 100644 cnmodel/mechanisms/bkpkj.mod create mode 100644 cnmodel/mechanisms/cabpump.mod create mode 100644 cnmodel/mechanisms/cadiff.mod create mode 100644 cnmodel/mechanisms/cadyn.mod create mode 100644 cnmodel/mechanisms/cap.mod create mode 100644 cnmodel/mechanisms/capmp.mod create mode 100644 cnmodel/mechanisms/capump.mod create mode 100755 cnmodel/mechanisms/cleftXmtr.mod create mode 100755 cnmodel/mechanisms/gly.mod create mode 100755 cnmodel/mechanisms/gly2.mod create mode 100644 cnmodel/mechanisms/hcno.mod create mode 100644 cnmodel/mechanisms/hcno_bo.mod create mode 100755 cnmodel/mechanisms/iStim.mod create mode 100644 cnmodel/mechanisms/ihpkj.mod create mode 100644 cnmodel/mechanisms/ihpyr.mod create mode 100644 cnmodel/mechanisms/ihpyr_adj.mod create mode 100755 cnmodel/mechanisms/ihsgc_apical.mod create mode 100755 cnmodel/mechanisms/ihsgc_basalmiddle.mod create mode 100644 cnmodel/mechanisms/ihvcn.mod create mode 100755 cnmodel/mechanisms/inav11.mod create mode 100644 cnmodel/mechanisms/jsrnaf.mod create mode 100644 cnmodel/mechanisms/ka.mod create mode 100644 cnmodel/mechanisms/kcnq.mod create mode 100644 cnmodel/mechanisms/kdpyr.mod create mode 100644 cnmodel/mechanisms/kht.mod create mode 100644 cnmodel/mechanisms/kif.mod create mode 100755 cnmodel/mechanisms/kir.mod create mode 100644 cnmodel/mechanisms/kis.mod create mode 100644 cnmodel/mechanisms/klt.mod create mode 100644 cnmodel/mechanisms/kpkj.mod create mode 100644 cnmodel/mechanisms/kpkj2.mod create mode 100644 cnmodel/mechanisms/kpkjslow.mod create mode 100644 cnmodel/mechanisms/kpksk.mod create mode 100644 cnmodel/mechanisms/leak.mod create mode 100644 cnmodel/mechanisms/multisite.mod create mode 100644 cnmodel/mechanisms/na.mod create mode 100755 cnmodel/mechanisms/nacn.mod create mode 100644 cnmodel/mechanisms/nacncoop.mod create mode 100755 cnmodel/mechanisms/nap.mod create mode 100644 cnmodel/mechanisms/napyr.mod create mode 100644 cnmodel/mechanisms/pkjlk.mod create mode 100644 cnmodel/mechanisms/rsg.mod create mode 100644 cnmodel/mechanisms/tests/test_mechanisms.py create mode 100644 cnmodel/mechanisms/vecevent.mod create mode 100755 cnmodel/morphology/__init__.py create mode 100644 cnmodel/morphology/bushy_stick.hoc create mode 100644 cnmodel/morphology/hoc_reader.py create mode 100644 cnmodel/morphology/morphology.py create mode 100644 cnmodel/morphology/octopus_spencer_stick.hoc create mode 100644 cnmodel/morphology/tstellate_stick.hoc create mode 100644 cnmodel/morphology/tv_stick.hoc create mode 100644 cnmodel/populations/__init__.py create mode 100644 cnmodel/populations/bushy.py create mode 100644 cnmodel/populations/dstellate.py create mode 100644 cnmodel/populations/population.py create mode 100644 cnmodel/populations/pyramidal.py create mode 100644 cnmodel/populations/sgc.py create mode 100644 cnmodel/populations/tstellate.py create mode 100644 cnmodel/populations/tuberculoventral.py create mode 100644 cnmodel/protocols/__init__.py create mode 100644 cnmodel/protocols/cc.py create mode 100644 cnmodel/protocols/democlamp.py create mode 100644 cnmodel/protocols/iv_curve.py create mode 100644 cnmodel/protocols/population_test.py create mode 100644 cnmodel/protocols/protocol.py create mode 100644 cnmodel/protocols/simple_synapse_test.py create mode 100644 cnmodel/protocols/synapse_test.py create mode 100644 cnmodel/protocols/vc_curve.py create mode 100644 cnmodel/synapses/__init__.py create mode 100644 cnmodel/synapses/exp2_psd.py create mode 100644 cnmodel/synapses/glu_psd.py create mode 100644 cnmodel/synapses/gly_psd.py create mode 100644 cnmodel/synapses/psd.py create mode 100644 cnmodel/synapses/simple_terminal.py create mode 100644 cnmodel/synapses/stochastic_terminal.py create mode 100644 cnmodel/synapses/synapse.py create mode 100644 cnmodel/synapses/terminal.py create mode 100644 cnmodel/synapses/tests/test_data/dstellate_bushy.pk create mode 100644 cnmodel/synapses/tests/test_data/dstellate_dstellate.pk create mode 100644 cnmodel/synapses/tests/test_data/dstellate_tstellate.pk create mode 100644 cnmodel/synapses/tests/test_data/sgc_bushy.pk create mode 100644 cnmodel/synapses/tests/test_data/sgc_dstellate.pk create mode 100644 cnmodel/synapses/tests/test_data/sgc_tstellate.pk create mode 100644 cnmodel/synapses/tests/test_psd.py create mode 100644 cnmodel/synapses/tests/test_synapses.py create mode 100755 cnmodel/util/Params.py create mode 100755 cnmodel/util/PlotHelpers.py create mode 100644 cnmodel/util/__init__.py create mode 100644 cnmodel/util/ccstim.py create mode 100644 cnmodel/util/compare_simple_multisynapses.py create mode 100644 cnmodel/util/difftreewidget/DataTreeWidget.py create mode 100644 cnmodel/util/difftreewidget/DiffTreeWidget.py create mode 100644 cnmodel/util/difftreewidget/README create mode 100644 cnmodel/util/difftreewidget/TableWidget.py create mode 100644 cnmodel/util/difftreewidget/__init__.py create mode 100755 cnmodel/util/expfitting.py create mode 100644 cnmodel/util/filelock.py create mode 100644 cnmodel/util/find_point.py create mode 100644 cnmodel/util/fitting.py create mode 100644 cnmodel/util/get_anspikes.py create mode 100644 cnmodel/util/matlab_proc.py create mode 100644 cnmodel/util/nrnutils.py create mode 100644 cnmodel/util/process.py create mode 100755 cnmodel/util/pynrnutilities.py create mode 100755 cnmodel/util/pyqtgraphPlotHelpers.py create mode 100644 cnmodel/util/random_seed.py create mode 100644 cnmodel/util/sound.py create mode 100644 cnmodel/util/stim.py create mode 100644 cnmodel/util/talbotetalTicks.py create mode 100644 cnmodel/util/tests/test_expfitting.py create mode 100644 cnmodel/util/tests/test_matlab.py create mode 100644 cnmodel/util/tests/test_sound.py create mode 100644 cnmodel/util/tests/test_stim.py create mode 100644 cnmodel/util/user_tester.py create mode 100644 doc/Makefile create mode 100644 doc/make.bat create mode 100644 doc/numpydoc-0.5/LICENSE.txt create mode 100644 doc/numpydoc-0.5/MANIFEST.in create mode 100644 doc/numpydoc-0.5/PKG-INFO create mode 100644 doc/numpydoc-0.5/README.rst create mode 100644 doc/numpydoc-0.5/numpydoc.egg-info/PKG-INFO create mode 100644 doc/numpydoc-0.5/numpydoc.egg-info/SOURCES.txt create mode 100644 doc/numpydoc-0.5/numpydoc.egg-info/dependency_links.txt create mode 100644 doc/numpydoc-0.5/numpydoc.egg-info/top_level.txt create mode 100644 doc/numpydoc-0.5/numpydoc/__init__.py create mode 100644 doc/numpydoc-0.5/numpydoc/comment_eater.py create mode 100644 doc/numpydoc-0.5/numpydoc/compiler_unparse.py create mode 100644 doc/numpydoc-0.5/numpydoc/docscrape.py create mode 100644 doc/numpydoc-0.5/numpydoc/docscrape_sphinx.py create mode 100644 doc/numpydoc-0.5/numpydoc/linkcode.py create mode 100644 doc/numpydoc-0.5/numpydoc/numpydoc.py create mode 100644 doc/numpydoc-0.5/numpydoc/phantom_import.py create mode 100644 doc/numpydoc-0.5/numpydoc/plot_directive.py create mode 100644 doc/numpydoc-0.5/numpydoc/tests/test_docscrape.py create mode 100644 doc/numpydoc-0.5/numpydoc/tests/test_linkcode.py create mode 100644 doc/numpydoc-0.5/numpydoc/tests/test_phantom_import.py create mode 100644 doc/numpydoc-0.5/numpydoc/tests/test_plot_directive.py create mode 100644 doc/numpydoc-0.5/numpydoc/tests/test_traitsdoc.py create mode 100644 doc/numpydoc-0.5/numpydoc/traitsdoc.py create mode 100644 doc/numpydoc-0.5/setup.cfg create mode 100644 doc/numpydoc-0.5/setup.py create mode 100644 doc/source/an_model.rst create mode 100644 doc/source/architecture.rst create mode 100644 doc/source/architecture.svg create mode 100644 doc/source/cells.rst create mode 100644 doc/source/conf.py create mode 100644 doc/source/data.rst create mode 100644 doc/source/decorator.rst create mode 100644 doc/source/examples.rst create mode 100644 doc/source/examples/figures.rst create mode 100644 doc/source/examples/play_test_sounds.rst create mode 100644 doc/source/examples/plot_hcno_kinetics.rst create mode 100644 doc/source/examples/test_an_model.rst create mode 100644 doc/source/examples/test_bushy_variation.rst create mode 100644 doc/source/examples/test_ccstim.rst create mode 100644 doc/source/examples/test_cells.rst create mode 100644 doc/source/examples/test_circuit.rst create mode 100644 doc/source/examples/test_decorator.rst create mode 100644 doc/source/examples/test_mechanisms.rst create mode 100644 doc/source/examples/test_mso_inputs.rst create mode 100644 doc/source/examples/test_physiology.rst create mode 100644 doc/source/examples/test_populations.rst create mode 100644 doc/source/examples/test_sgc_input.rst create mode 100644 doc/source/examples/test_sgc_input_PSTH.rst create mode 100644 doc/source/examples/test_sgc_input_phaselocking.rst create mode 100644 doc/source/examples/test_simple_synapses.rst create mode 100644 doc/source/examples/test_sound_stim.rst create mode 100644 doc/source/examples/test_sounds.rst create mode 100644 doc/source/examples/test_synapses.rst create mode 100644 doc/source/examples/toy_model.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/modules.rst create mode 100644 doc/source/morphology.rst create mode 100644 doc/source/populations.rst create mode 100644 doc/source/protocols.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/synapses.rst create mode 100644 doc/source/util.rst create mode 100644 examples/LC_bushy.hoc create mode 100644 examples/__init__.py create mode 100644 examples/figures.py create mode 100755 examples/figures.sh create mode 100755 examples/gif create mode 100644 examples/play_test_sounds.py create mode 100755 examples/plot_hcno_kinetics.py create mode 100755 examples/stim172_geese.wav create mode 100755 examples/test_adex.py create mode 100644 examples/test_an_model.py create mode 100644 examples/test_bushy_variation.py create mode 100644 examples/test_ccstim.py create mode 100755 examples/test_cells.py create mode 100644 examples/test_circuit.py create mode 100755 examples/test_decorator.py create mode 100644 examples/test_mechanisms.py create mode 100644 examples/test_mso_inputs.py create mode 100644 examples/test_physiology.py create mode 100644 examples/test_populations.py create mode 100644 examples/test_sgc_input.py create mode 100644 examples/test_sgc_input_PSTH.py create mode 100644 examples/test_sgc_input_phaselocking.py create mode 100644 examples/test_simple_synapses.py create mode 100644 examples/test_sound_stim.py create mode 100644 examples/test_sounds.py create mode 100644 examples/test_synapses.py create mode 100755 examples/toy_model.py create mode 100644 modified_dend/hoc_trial_run_dendrites.py create mode 100644 modified_dend/network_pauser_prototype_dendrites.py create mode 100644 project/Figures/3 presentation pyramidal cell.png create mode 100644 project/Figures/Final graph.png create mode 100644 project/Figures/Network with TV DS 200 presentations.png create mode 100644 project/Figures/Network with TV DS and Clamp 200 presentations 4-1.png create mode 100644 project/Figures/examples.png create mode 100644 project/Figures/examples2.png create mode 100644 project/Figures/pyram synapse 500 presentations test_sgc_psth.png create mode 100644 project/Figures/test 1 pres.png create mode 100644 project/__init__.py create mode 100644 project/graphs.py create mode 100644 project/hoc_trial_run_cartwheel.py create mode 100644 project/hoc_trial_run_dendrites.py create mode 100644 project/network_pauser_prototype_cartwheel.py create mode 100644 project/network_pauser_prototype_dendrites.py create mode 100644 project/previous_version/Ad041599_062_Buildup_psth.txt create mode 100644 project/previous_version/Ad081098_065_PauserBuildup_psth.txt create mode 100644 project/previous_version/Ad081998_199_WideChopper_psth.txt create mode 100644 project/previous_version/histgen.py create mode 100644 project/previous_version/test_network_PSTH.py create mode 100644 project/previous_version/test_single_cell_psth.py create mode 100644 project/previous_version/tone_pyram_sgc_input_PSTH2.7.py create mode 100644 project/util/__init__.py create mode 100644 project/util/tools.py create mode 100644 setup.py create mode 100644 test.py create mode 100644 todo.rst diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c83aa6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +*.pyc +*.npz +*.lock +*.bak +*.dat +cache +i386 +cnmodel/mechanisms/*.c +cnmodel/mechanisms/*.o +cnmodel/an_model/model/*.mexmaci64 +x86_64 +cnmodel/an_model +doc/build +build +dist/ +cnmodel.egg* +.cache* +.pytest* +Figure6* +project/__pycache__ +project/testing.py +how +*.dll +.idea/ +project/run_data/ +*__pycache__/ +.gitattributes + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc37192 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright (c) 2017 Paul B. Manis, Luke Campagnola, University of North Carolina +at Chapel Hill +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..15e08c5 --- /dev/null +++ b/README.rst @@ -0,0 +1,343 @@ +Changes +======= + +This version of cnmodel runs under Python3.6 or later, using Neuron 7.6. New features include a method for changing the data tables on the fly without editing the original tables, and a tool for fitting Exp2Syn "simple" PSCs to the multisite PSC data (or, potentially, to experimental data) to get parameters for the synapse description table. + +About CNModel +============= + +CNModel is a Python-based interface to NEURON models of cochlear nucleus neurons, synapses, network connectivity. To drive the model with sound stimuli, the Zilany et al (2010, 2014) auditory periphery model is used to generate auditory nerve spike trains (either via the "cochlea" Python package or by the original MATLAB model; see below). The overall goal is to provide a platform for modeling networks of cochlear nucleus neurons with different levels of biological realism in the context of an accurate simulation of auditory nerve input. + +At the lowest level are NEURON-based implementations of ion channels and synapses. Ion channel models for potassium channels are derived largely from the measurements and models of Rothman and Manis (2003abc), and Kanold and Manis (1999, 2001); other channels are derived or modified from the literature. Cell models are in turn based on the insertion of ion channels in densities based on measurements in guinea pig and mouse. The "point" somatic cell models, which form the base set of models in CNModel, replicate the data reported in the original papers. + +The postsynaptic receptor/conductance models are based on kinetic models of glutamate (Raman and Trussell, 1992) and glycinergic receptors, as adjusted to match measurements of synaptic conductances from the mouse cochlear nucleus collected in Xie and Manis, 2013. The glutamate receptor models include desensitization and the effects of internal polyamine receptor block, based on the kinetic scheme of Woodhull (1982). + +The presynaptic release model includes a multisite, probabilistic synapse that includes time-dependent changes in release probability based on the Dittman, Kreitzer and Regehr (J Neurosci. 2000 Feb 15;20(4):1374-85) kinetic scheme. Although detailed, this model is computationally expensive and likely not suitable for large scale network simulations. Other simpler models of synapses are also included. + +Network connectivity may be defined programmatically, or based on a table of connectivity patterns. A table with estimates derived from the literature is included in the source. + +Included in this package is a set of test suites for different components. An overriding set of unit tests is available to confirm performance of the base models against a set of reference runs, several of which are in turn directly traceable to the original manuscripts. The test suites are useful in verifying performance of the model after modifications of the code or changes in the installation (upgrades of Python or Matlab, for example). + +A manuscript describing this package has been published: +-------------------------------------------------------- + + Paul B. Manis, Luke Campagnola, + A biophysical modelling platform of the cochlear nucleus and other auditory circuits: + From channels to networks, + Hearing Research, + Volume 360, + 2018, + Pages 76-91, + ISSN 0378-5955, + https://doi.org/10.1016/j.heares.2017.12.017. + Open Access: http://www.sciencedirect.com/science/article/pii/S037859551730360X + +If you use this package, we would appreciate it if you cite our work in any publications or abstracts. + + +Installation requirements +------------------------- +This package depends on the following: + +1. Python 3.6 with numpy (1.14.3), scipy (1.1.0), lmfit (0.9.11), matplotlib (3.0.0), faulthandler, and pyqtgraph (0.11.0). The cochlea module requires pandas as well. + An Anaconda install with the appropriate scientific packages works well:: + + conda install python=3.6 pyqt pyqtgraph matplotlib numpy scipy pandas pytest cython + pip install resampy + pip install lmfit + pip install cochlea + + or: + + conda create --name py3mpl3 python=3.6 pyqt pyqtgraph matplotlib=3 numpy scipy pandas pytest cython + pip install resampy + pip install lmfit + pip install cochlea + + + (Note that under MacOSX, python 3.7 is usable, but the Windows versio of Matlab R2018b is restricted + to python 3.6) + +2. A Python-linked version of NEURON (www.neuron.yale.edu). The code has been tested with NEURON 7.5 and 7.6. +3. A C compiler (gcc). Needed for compilation of mechanisms for NEURON. +4. The Zilany et al (JASA 2014) auditory periphery model. This can be provided one of two ways: + + * The Python-based cochlea model by Rudnicki and Hemmert at https://github.com/mrkrd/cochlea. + This is probably best installed via pip per the instructions on the PyPi site: ``pip install cochlea``. + * The original MATLAB-based Zilany model; requires MATLAB 2011 or later. A C compiler will also + be needed to build this model. The model should be placed in the directory + ``cnmodel/cnmodel/an_model/model``, with the following components: ANmodel.m, complex.c, complex.h, + complex.hpp, ffGn.m, fituaudiogram2.m, mexANmodel.m, model_IHC.c, model_Synapse.c, + and the THRESHOLD_ALL_* files. When cnmodel attempts to access this code the first time, + it will perform the necessary compilation. + +5. neuronvis (optional) available at https://github.com/campagnola/neuronvis or https://github.com/pbmanis/neuronvis). + This provides 3D visualization for morphology. + +After the code is installed, enter the cnmodel directory and compile the NEURON mod files:: + + $ nrnivmodl cnmodel/mechanisms + +This will create a directory ("x86_64" or "special") in the top cnmodel directory with the compiled mechanisms. + +Under Windows 10, use:: + + $ mknrndll cnmodel\mechanisms + +to do the same thing. + + +For more detailed information on setup in a Windows environment, see the file Windows_setup.md. Thanks to Laurel Carney for prompting the generation of this set of instructions, and for identifying issues on Windows. + +Windows caveat: +-------------- +Manually compile the mex files (using Matlab, go to the an_model/models folder, and use mexANmodel.m to compile the files). Then, add the an_model/model folder to the Matlab path, so that it can find the files when needed. + +For more detailed information on setup in a Windows environment, see the file Windows_setup.md. Thanks to Laurel Carney for prompting the generation of this set of instructions, and for identifying issues on Windows. + +Note: *This package is not yet compatible with Python 3.x. Neuron is not yet compatible with Python 3.x* + + +Testing +------- + + +Make sure you are in the cnmodel directory, and that you have selected the right environment in Anaconda (in +my case, this is usually py3mpl3). + +At this point:: + +After the code is installed, enter the cnmodel directory and compile the NEURON mod files:: + + $ nrnivmodl cnmodel/mechanisms + +This will create a directory ("x86_64" or "special") in the top cnmodel directory with the compiled mechanisms. + +Then:: + + $ python examples/toy_model.py + +should generate a plot with several sets of traces showing responses of individual neuron models to depolarizing and hyperpolarizing current steps. + +The test suite should be run as:: + + $ python test.py + +This will test each of the models against reference data, the synapse mechanisms, a number of internal routines, and the auditory nerve model. The tests should pass for each component. Failures may indicate incorrect installation or incorrect function within individual components. These should be corrected before proceeding with simulations. + + +Note +---- +Under Windows, it may be best to use the standard windows command terminal rather than the "bash" terminal provided by NEURON, at least to run the Python scripts. + +Matlab +------ +This version has been tested with the Matlab AN model of Zilany et al., 2014. +Before using, you will need to compile the C code in an_model using Matlab's mex tool. First however, change the following code: +In model_Synapse.c: + +Change (line 63 in the source):: + + $ int nrep, pxbins, lp, outsize[2], totalstim; + +to:: + $ int nrep, pxbins, lp, totalstim; + $ size_t outsize[2]; + +Likewise, in model_IHC.c, change:: + + $ int nrep, pxbins, lp, outsize[2], totalstim, species; + +to:: + + $ int nrep, pxbins, lp, totalstim, species; + $ size_t outsize[2]; + +Then, in Matlab, go to the cnmodel/an_model/model directory, and run:: + + $ mexANmodel + +Then, cd to an_model and run:: + + $ testANmodel + +to confirm that the model is installed and working. +(You may need to add the model directory to the Matlab path.) + + +Figures +------- + +The data for the figures in the manuscript (Manis and Campagnola, Hearing Research 2018) can be generated using the bash script "figures.sh" in the examples subdirectory. +From the main cnmodel directory:: + + $ ./examples figures.sh fignum + +where fignum is one of 2a, 2b, 2c, 3, 4, 5, 6a, 6b, or 7. + +Note that Figure 7 may take several **hours** to generate. + +Example code and tests +---------------------- + +A number of additional tests are included in the examples directory. + + +- `test_an_model.py` verifies that the auditory nerve model can be run. If necessary, it will compile (using MEX) the mechanisms for matlab. +- `test_ccstim.py` tests the generation of different stimulus waveforms by the pulse generator module. +- `test_cells.py` runs different cell models in current or voltage clamp. + Usage:: + + test_cells.py celltype species[-h] [--type TYPE] [--temp TEMP] [-m MORPHOLOGY] + [--nav NAV] [--ttx] [-p PULSETYPE] [--vc | --cc | --rmp] + + For example: ``python test_cells.py bushy mouse --cc --temp 34`` + + +- `test_cells.py` can run protocols on selected cell models. + Usage:: + + test_cells.py [-h] [--type TYPE] [--temp TEMP] [-m MORPHOLOGY] + [--nav NAV] [--ttx] [-p PULSETYPE] [--vc | --cc | --rmp] + celltype species + +- `test_circuit.py` tests the generation of circuits with populations of cells. No simulations are run. +- `test_decorator.py` generates an IV curve for the reconstructed cell LC_bushy.hoc (Figure 5B,C) +- `test_mechanisms.py` runs a voltage clamp I/V protocol on a selected mechanism and displays the result. + Usage:: + + python test_mechanisms.py + + Available channel mechanisms: + + ========== ========= ========== ============= ================== + CaPCalyx KIR bkpkj hcno hcnobo + hh hpkj ihpyr ihsgcApical ihsgcBasalMiddle + ihvcn jsrna k_ion ka kcnq + kdpyr kht kif kis klt + kpkj kpkj2 kpkjslow kpksk leak + lkpkj na naRsg na_ion nacn + nacncoop nap napyr nav11 + ========== ========= ========== ============= ================== + +- `test_mso_inputs.py` runs a circuit that creates a point MSO neuron, innervated by bushy cells from independent "ears". This demonstrates how to construct a binaural circuit using CNModel. +- `test_physiology.py` runs a large VCN circuit that converges onto a single bushy cell. This run can take a long time. The output was used to create Figure 7 of the manuscript. +- `test_populations.py` tests synaptic connections between two cell types. Usage:: + + python test_populations.py + +- `test_sgc_input_phaselocking.py` tests phase locking with SGC inputs to a bushy cell. +- `test_sgc_input_PSTH.py` shows SGC inputs and postsynaptic bushy cell PSTHs. +- `test_sgc_input.py` demonstrates SGC input to a VCN bushy cell. +- `test_simple_synapses.py` tests simple Exp2Syn inputs to different cell types. Usage:: + + python test_synapses.py + + Supported cell types: sgc, bushy, tstellate, dstellate, tuberculoventral, pyramidal +- `test_sound_stim.py` generates spike trains from the selected model (cochlea, matlab) and plots rate-intensity functions for the 3 different SR groups. +- `test_sounds.py` generates waveforms for different kinds of sounds included in the sounds class. +- `test_synapses.py` evokes spikes in a presynaptic cell while recording the postsynaptic potential. Usage:: + + python test_synapses.py + + Supported cell types: sgc, bushy, tstellate, dstellate +- `toy_model.py` generates IV plots for each of the principal point cell types included in CNModel. This is the code that generates Figure 3 of the manuscript. + +Potential Issues and Solutions +------------------------------ + +1. Occasionally one of the AN spike train files, which are stored in the directory `cnmodel/an_model/cache`, become locked. This can occur if the calling routines are aborted (^C, ^Z) in the middle of a transaction accessing the cache file, or perhaps during when parallel processing is enabled and a routine fails or is aborted. In this case, a file with the extension ``".lock"`` exists, which prevents the an_model code from accessing the file. The ``".lock"`` file needs to be deleted from the cache directory. + + * First, print a list of the locked files:: + + $ find /path/to/cache -name '*.lock' + + * Where /path/to/cache may be something like `cnmodel/an_model/cache`. + There is most likely only one such file in the diretory. + + * Next, to delete the files:: + + $ find /path/to/cache -name '*.lock' -delete + + * Under Windows (and other OS's), you should be able do accomplish the same thing + with the File Explorer/Finder, limiting the files by extension. + + +References +---------- + +1. Cao XJ, Oertel D. The magnitudes of hyperpolarization-activated and +low-voltage-activated potassium currents co-vary in neurons of the ventral +cochlear nucleus. J Neurophysiol. 2011 Aug;106(2):630-40. doi: +10.1152/jn.00015.2010. Epub 2011 May 11. PubMed PMID: 21562186; PubMed Central +PMCID: PMC3154804. + +2. Cao XJ, Oertel D. Auditory nerve fibers excite targets through synapses that +vary in convergence, strength, and short-term plasticity. J Neurophysiol. 2010 +Nov;104(5):2308-20. doi: 10.1152/jn.00451.2010. Epub 2010 Aug 25. PubMed PMID: +20739600; PubMed Central PMCID: PMC3350034. + +3. Dittman JS, Kreitzer AC, Regehr WG. Interplay between facilitation, depression, +and residual calcium at three presynaptic terminals. J Neurosci. 2000 +Feb 15;20(4):1374-85. PubMed PMID: 10662828. + +1. Isaacson JS, Walmsley B. Counting quanta: direct measurements of transmitter +release at a central synapse. Neuron. 1995 Oct;15(4):875-84. + +4. Kanold PO, Manis PB. A physiologically based model of discharge pattern +regulation by transient K+ currents in cochlear nucleus pyramidal cells. J +Neurophysiol. 2001 Feb;85(2):523-38. PubMed PMID: 11160490. + +5. Kanold PO, Manis PB. Transient potassium currents regulate the discharge +patterns of dorsal cochlear nucleus pyramidal cells. J Neurosci. 1999 Mar +15;19(6):2195-208. PubMed PMID: 10066273. + +6. Liu Q, Manis PB, Davis RL. Ih and HCN channels in murine spiral ganglion +neurons: tonotopic variation, local heterogeneity, and kinetic model. J Assoc Res +Otolaryngol. 2014 Aug;15(4):585-99. doi: 10.1007/s10162-014-0446-z. Epub 2014 Feb +21. Erratum in: J Assoc Res Otolaryngol. 2014 Aug;15(4):601. PubMed PMID: +24558054; PubMed Central PMCID: PMC4141436. + +7. Raman IM, Trussell LO. The kinetics of the response to glutamate and kainate +in neurons of the avian cochlear nucleus. Neuron. 1992 Jul;9(1):173-86. PubMed +PMID: 1352983. + +8. Rothman JS, Manis PB. The roles potassium currents play in regulating the +electrical activity of ventral cochlear nucleus neurons. J Neurophysiol. 2003 +Jun;89(6):3097-113. PubMed PMID: 12783953. + +9. Rothman JS, Manis PB. Kinetic analyses of three distinct potassium +conductances in ventral cochlear nucleus neurons. J Neurophysiol. 2003 +Jun;89(6):3083-96. PubMed PMID: 12783952. + +10. Rothman JS, Manis PB. Differential expression of three distinct potassium +currents in the ventral cochlear nucleus. J Neurophysiol. 2003 Jun;89(6):3070-82. +PubMed PMID: 12783951. + +11. Rothman JS, Young ED, Manis PB. Convergence of auditory nerve fibers onto +bushy cells in the ventral cochlear nucleus: implications of a computational +model. J Neurophysiol. 1993 Dec;70(6):2562-83. PubMed PMID: 8120599. + +12. Woodhull AM. Ionic blockage of sodium channels in nerve. J Gen Physiol. 1973 +Jun;61(6):687-708. PubMed PMID: 4541078; PubMed Central PMCID: PMC2203489. + +13. Xie R, Manis PB. Target-specific IPSC kinetics promote temporal processing in +auditory parallel pathways. J Neurosci. 2013 Jan 23;33(4):1598-614. doi: +10.1523/JNEUROSCI.2541-12.2013. PubMed PMID: 23345233; PubMed Central PMCID: +PMC3737999. + +14. Zilany MS, Bruce IC, Carney LH. Updated parameters and expanded simulation +options for a model of the auditory periphery. J Acoust Soc Am. 2014 +Jan;135(1):283-6. doi: 10.1121/1.4837815. PubMed PMID: 24437768; PubMed Central +PMCID: PMC3985897. + +15. Zilany MS, Carney LH. Power-law dynamics in an auditory-nerve model can +account for neural adaptation to sound-level statistics. J Neurosci. 2010 Aug +4;30(31):10380-90. doi: 10.1523/JNEUROSCI.0647-10.2010. PubMed PMID: 20685981; +PubMed Central PMCID: PMC2935089. + +16. Zilany MS, Bruce IC, Nelson PC, Carney LH. A phenomenological model of the +synapse between the inner hair cell and auditory nerve: long-term adaptation with +power-law dynamics. J Acoust Soc Am. 2009 Nov;126(5):2390-412. doi: +10.1121/1.3238250. PubMed PMID: 19894822; PubMed Central PMCID: PMC2787068. + diff --git a/Windows_setup.md b/Windows_setup.md new file mode 100644 index 0000000..015d523 --- /dev/null +++ b/Windows_setup.md @@ -0,0 +1,321 @@ +CNMODEL Setup for Windows Systems +================================= + +(as of 2 March 2018, tested on Windows 10) +On a bare system, you will need to install the following packages: + + 1. Anaconda Python 2.7 (this may also include the Microsoft Visual Studio for Python) + 2. git + 3. msvc2.7forpython (required for cochlea) + 4. external python package from PyPi: cochlea + 5. NEURON7.5 (this must be installed last) + 6. cnmodel, cloned from the main repository with git + 7. build the .dll file for the Neuron mechanisms used in cnmodel + +Some installs are accomplished via a graphical interface (anaconda, git, msvc, neuron). +Other parts of the install are done from the terminal (Windows "Command Prompt" - look in the Windows Accessories) + +Follow the instructions below to do this installation. The order only partially matters - msvcforpython must be installed before cochlea can be built; python should be installed before NEURON; and git is required get the cnmodel repository. + +Step 1: Install Anaconda python for Python 2.7 for your system. +Note that NEURON is not yet compatible with Python 3.x + +https://repo.continuum.io/archive/ +The most recent versions of the Anaconda installer (5.x) are OK to use. +Check the box to set DOS path variables so can run from command prompt. +Install only for yourself. + +If you want, create an environment for python 2.7, Using the standard windows terminal ("Command Prompt"), install the additional required packages:: + + conda create --name models python=2.7 pyqt pyqtgraph matplotlib numpy scipy pandas pytest faulthandler + +Note: this may take a while, and will install many other packages that are required dependencies. + +Now, activate the environment:: + + conda activate models + +Or else, just install everything to the root environment: +conda install pyqt pyqtgraph matplotlib numpy scipy pytest faulthandler + +If you are working with a previous Anaconda installation, you might need to do these steps:: + + conda update --all + conda update qt + +The following must be retrieved from the PyPi repository (I had problems with the one in the anaconda repo):: + + pip install lmfit + +Step 2: Install git (a widely used source control module):: + + https://git-scm.com/downloads + +Step 3: Install MS visual studio for python:: + + https://www.visualstudio.com/vs/python/ + +Step 4: Install cochlea (Rudnicki and Hemmert's implementation of several models under python):: + + pip install cochlea + +This should build quickly and have no errors. + +Step 5: Install NEURON7.5 for mswindows from:: + + www.neuron.yale.edu. + +Restart the command window so that the paths to the python installation are updated. +Make sure that python and neuron are installed correctly together by testing the ability to import NEURON:: + + (base) pbmanis: Python $ python + Python 2.7.14 |Anaconda custom (64-bit)| (default, Dec 7 2017, 11:07:58) + [GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> from neuron import h + NEURON -- VERSION 7.5 master (6b4c19f) 2017-09-25 + Duke, Yale, and the BlueBrain Project -- Copyright 1984-2016 + See http://neuron.yale.edu/neuron/credits + + >>> + +(^Z to exit) + +Step 6: Go to where you want to put the model code repository, and clone the repo:: + + git clone https://github.com/cnmodel/cnmodel.git + +Jump into the cnmodel directory. + +Step 7: Find the mknrndll gui from where you installed neuron with the Explorer (the program in the nrn folder). Run the program, and select the directory cnmodel\cnmodel\mechanisms, then build. +This will make nrnmech.dll in the mechanisms directory. +Copy the nrnmech.dll into the main cnmodel directory. + +Step 8: Run:: + + python setup.py develop + +in the main cnmodel directory to make a library version in the anaconda site-packages directory. This will make cnmodel accessible as an import into python from any location. However, you will (on Windows) need to copy the nrnmech.dll file to the directory that you are starting in. (If you import cnmodel and there is not a list of mechanisms after the Neuron banner, then nrnmech.dll was not found). + +Running +======= + +The following should be all that is needed once everything is set up. + +Go the the cnmodel main directory. + +if you set up a Python environment, activate it to make sure you have the right dependencies available. + +Now, these scripts should run just fine:: + + python examples/test_mechanisms.py klt + python examples/toy_model.py + python examples/test_synapses.py sgc bushy + python examples/test_phaselocking.py + +You should also be able to run the full battery of tests:: + + python test.py + +All the tests should pass (except perhaps with the exception dstellate->dstellate; this passes under MacOSX so we will need to investigate why it fails under Windows). MacOSX and Windows test results are listed below. + +Notes +===== + +1. You cannot use the "bash" terminal window that comes with neuron - it doesn't set the paths correctly for access to the anaconda python. + +2. Some of the graphical displays are not correctly sized in Windows. You may need to expand the window to see all the plots (specifically, toy_model has this problem). + +3. Report occurrences of failures to "import PyQt4" to: https://github.com/cnmodel/cnmodel/issues. + +4. The test suite (python test.py) may fail on one test: dstellate -> dstellate synapses (as of 23Feb2018), and if you do not have Matlab, the Matlab test will be skipped. + + +Things solved with Windows +========================== + +1. The test suite works and most tests pass (python test.py). +2. Qt5 and current anaconda install are acceptable (we are no longer trying to force Qt4). +3. The prior dependency on pylibrary has been removed. + + +Potential problems +================== + +This software has been tested primarily on Mac OSX and Linux systems. Some tests have been performed on Windows. If issues are found, please report them via github as an issue (as noted above). + +Test results +============ + +Windows 10 (24 Feb 2018; Qt5 Branch) +------------------------------------ + +:: + C:\Users\pbmanis\Desktop\Python\cnmodel>python test.py > testresults.txt + NEURON -- VERSION 7.5 master (6b4c19f) 2017-09-25 + Duke, Yale, and the BlueBrain Project -- Copyright 1984-2016 + See http://neuron.yale.edu/neuron/credits + + loading membrane mechanisms from C:\Users\pbmanis\Desktop\Python\cnmodel\nrnmech.dll + Additional mechanisms from files + CaPCalyx.mod Gly5GC.mod Gly5PL.mod Gly5State.mod Gly6S.mod Iclamp2.mod NMDA.mod NMDA_Kampa.mod + ampa_trussell.mod bkpkj.mod cabpump.mod cadiff.mod cadyn.mod cap.mod capmp.mod capump.mod cleftXmtr.mod + gly.mod gly2.mod hcno.mod hcno_bo.mod iStim.mod ihpkj.mod ihpyr.mod ihsgc_apical.mod ihsgc_ + basalmiddle.mod ihvcn.mod inav11.mod jsrnaf.mod ka.mod kcnq.mod kdpyr.mod kht.mod kif.mod + kir.mod kis.mod klt.mod kpkj.mod kpkj2.mod kpkjslow.mod kpksk.mod leak.mod multisite.mod + na.mod nacn.mod nacncoop.mod nap.mod napyr.mod pkjlk.mod rsg.mod vecevent.mod + + Testing with flags: -v --tb=short cnmodel/ + ============================= test session starts ============================= + platform win32 -- Python 2.7.14, pytest-3.3.2, py-1.5.2, pluggy-0.6.0 -- C:\Users\pbmanis\Anaconda2\python.exe + cachedir: .cache + rootdir: C:\Users\pbmanis\Desktop\Python\cnmodel, inifile: + collecting ... collected 36 items + + cnmodel/an_model/tests/test_cache.py::test_cache PASSED [ 2%] + cnmodel/an_model/tests/test_cache.py::test_parallel PASSED [ 5%] + cnmodel/cells/tests/test_cells.py::test_bushy PASSED [ 8%] + cnmodel/cells/tests/test_cells.py::test_bushy21 PASSED [ 11%] + cnmodel/cells/tests/test_cells.py::test_bushy_mouse PASSED [ 13%] + cnmodel/cells/tests/test_cells.py::test_tstellate PASSED [ 16%] + cnmodel/cells/tests/test_cells.py::test_tstellate_mouse PASSED [ 19%] + cnmodel/cells/tests/test_cells.py::test_tstellatet PASSED [ 22%] + cnmodel/cells/tests/test_cells.py::test_dstellate PASSED [ 25%] + cnmodel/cells/tests/test_cells.py::test_dstellate_mouse PASSED [ 27%] + cnmodel/cells/tests/test_cells.py::test_octopus PASSED [ 30%] + cnmodel/cells/tests/test_cells.py::test_pyramidal PASSED [ 33%] + cnmodel/cells/tests/test_cells.py::test_tuberculoventral PASSED [ 36%] + cnmodel/cells/tests/test_cells.py::test_cartwheel PASSED [ 38%] + cnmodel/cells/tests/test_cells.py::test_sgc_basal_middle PASSED [ 41%] + cnmodel/cells/tests/test_cells.py::test_sgc_apical PASSED [ 44%] + cnmodel/data/tests/test_db.py::test_db PASSED [ 47%] + cnmodel/mechanisms/tests/test_mechanisms.py::test_max_open_probability PASSED [ 50%] + cnmodel/synapses/tests/test_psd.py::test_sgc_bushy_psd PASSED [ 52%] + cnmodel/synapses/tests/test_psd.py::test_sgc_tstellate_psd PASSED [ 55%] + cnmodel/synapses/tests/test_psd.py::test_sgc_dstellate_psd PASSED [ 58%] + cnmodel/synapses/tests/test_psd.py::test_sgc_octopus_psd PASSED [ 61%] + cnmodel/synapses/tests/test_synapses.py::test_sgc_bushy PASSED [ 63%] + cnmodel/synapses/tests/test_synapses.py::test_sgc_tstellate PASSED [ 66%] + cnmodel/synapses/tests/test_synapses.py::test_sgc_tstellate2 PASSED [ 69%] + cnmodel/synapses/tests/test_synapses.py::test_sgc_dstellate PASSED [ 72%] + cnmodel/synapses/tests/test_synapses.py::test_dstellate_bushy PASSED [ 75%] + cnmodel/synapses/tests/test_synapses.py::test_dstellate_tstellate PASSED [ 77%] + cnmodel/synapses/tests/test_synapses.py::test_dstellate_dstellate FAILED [ 80%] + cnmodel/util/tests/test_expfitting.py::test_fit1 PASSED [ 83%] + cnmodel/util/tests/test_expfitting.py::test_fit2 PASSED [ 86%] + cnmodel/util/tests/test_matlab.py::test_matlab SKIPPED [ 88%] + cnmodel/util/tests/test_sound.py::test_conversions PASSED [ 91%] + cnmodel/util/tests/test_sound.py::test_tonepip PASSED [ 94%] + cnmodel/util/tests/test_sound.py::test_noisepip PASSED [ 97%] + cnmodel/util/tests/test_stim.py::test_make_pulse PASSED [100%] + + ================================== FAILURES =================================== + __________________________ test_dstellate_dstellate ___________________________ + cnmodel\synapses\tests\test_synapses.py:40: in test_dstellate_dstellate + SynapseTester('dstellate', 'dstellate') + cnmodel\synapses\tests\test_synapses.py:72: in __init__ + UserTester.__init__(self, "%s_%s" % (pre, post), pre, post) + cnmodel\util\user_tester.py:33: in __init__ + self.assert_test_info(*args, **kwds) + cnmodel\synapses\tests\test_synapses.py:108: in assert_test_info + super(SynapseTester, self).assert_test_info(*args, **kwds) + cnmodel\util\user_tester.py:127: in assert_test_info + self.compare_results(result, expect) + cnmodel\util\user_tester.py:60: in compare_results + self.compare_results(info[k], expect[k]) + cnmodel\util\user_tester.py:63: in compare_results + self.compare_results(info[i], expect[i]) + cnmodel\util\user_tester.py:79: in compare_results + self.compare_results(info[k], expect[k]) + cnmodel\util\user_tester.py:71: in compare_results + assert np.all(inans == enans) + E AssertionError + ---------------------------- Captured stdout call ----------------------------- + << D-stellate: JSR Stellate Type I-II cell model created >> + << D-stellate: JSR Stellate Type I-II cell model created >> + Elapsed time for 1 Repetions: 0.355309 + =============== 1 failed, 34 passed, 1 skipped in 96.33 seconds =============== + + + +Mac OSX (24 Feb 2018; Qt5 Branch) +--------------------- + +:: + (base) pbmanis: cnmodel [qt5+]$ python test.py + NEURON -- VERSION 7.5 master (6b4c19f) 2017-09-25 + Duke, Yale, and the BlueBrain Project -- Copyright 1984-2016 + See http://neuron.yale.edu/neuron/credits + + loading membrane mechanisms from x86_64/.libs/libnrnmech.so + Additional mechanisms from files + cnmodel/mechanisms//CaPCalyx.mod cnmodel/mechanisms//Gly5GC.mod cnmodel/mechanisms//Gly5PL.mod + cnmodel/mechanisms//Gly5State.mod cnmodel/mechanisms//Gly6S.mod cnmodel/mechanisms//Iclamp2.mod + cnmodel/mechanisms//NMDA.mod cnmodel/mechanisms//NMDA_Kampa.mod + cnmodel/mechanisms//ampa_trussell.mod cnmodel/mechanisms//bkpkj.mod + cnmodel/mechanisms//cabpump.mod cnmodel/mechanisms//cadiff.mod cnmodel/mechanisms//cadyn.mod + cnmodel/mechanisms//cap.mod cnmodel/mechanisms//capmp.mod + cnmodel/mechanisms//capump.mod cnmodel/mechanisms//cleftXmtr.mod + cnmodel/mechanisms//gly.mod cnmodel/mechanisms//gly2.mod cnmodel/mechanisms//hcno.mod + cnmodel/mechanisms//hcno_bo.mod cnmodel/mechanisms//iStim.mod cnmodel/mechanisms//ihpkj.mod + cnmodel/mechanisms//ihpyr.mod cnmodel/mechanisms//ihsgc_apical.mod + cnmodel/mechanisms//ihsgc_basalmiddle.mod cnmodel/mechanisms//ihvcn.mod + cnmodel/mechanisms//inav11.mod cnmodel/mechanisms//jsrnaf.mod + cnmodel/mechanisms//ka.mod cnmodel/mechanisms//kcnq.mod cnmodel/mechanisms//kdpyr.mod + cnmodel/mechanisms//kht.mod cnmodel/mechanisms//kif.mod cnmodel/mechanisms//kir.mod + cnmodel/mechanisms//kis.mod cnmodel/mechanisms//klt.mod cnmodel/mechanisms//kpkj.mod + cnmodel/mechanisms//kpkj2.mod cnmodel/mechanisms//kpkjslow.mod cnmodel/mechanisms//kpksk.mod + cnmodel/mechanisms//leak.mod cnmodel/mechanisms//multisite.mod cnmodel/mechanisms//na.mod + cnmodel/mechanisms//nacn.mod cnmodel/mechanisms//nacncoop.mod cnmodel/mechanisms//nap.mod c + nmodel/mechanisms//napyr.mod cnmodel/mechanisms//pkjlk.mod cnmodel/mechanisms//rsg.mod + cnmodel/mechanisms//vecevent.mod + Testing with flags: -v --tb=short cnmodel/ + =============================================== test session starts =============================================== + platform darwin -- Python 2.7.14, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 -- /Users/pbmanis/anaconda/bin/python + cachedir: .cache + rootdir: /Users/pbmanis/Desktop/Python/cnmodel, inifile: + collected 36 items + + cnmodel/an_model/tests/test_cache.py::test_cache PASSED + cnmodel/an_model/tests/test_cache.py::test_parallel PASSED + cnmodel/cells/tests/test_cells.py::test_bushy PASSED + cnmodel/cells/tests/test_cells.py::test_bushy21 PASSED + cnmodel/cells/tests/test_cells.py::test_bushy_mouse PASSED + cnmodel/cells/tests/test_cells.py::test_tstellate PASSED + cnmodel/cells/tests/test_cells.py::test_tstellate_mouse PASSED + cnmodel/cells/tests/test_cells.py::test_tstellatet PASSED + cnmodel/cells/tests/test_cells.py::test_dstellate PASSED + cnmodel/cells/tests/test_cells.py::test_dstellate_mouse PASSED + cnmodel/cells/tests/test_cells.py::test_octopus PASSED + cnmodel/cells/tests/test_cells.py::test_pyramidal PASSED + cnmodel/cells/tests/test_cells.py::test_tuberculoventral PASSED + cnmodel/cells/tests/test_cells.py::test_cartwheel PASSED + cnmodel/cells/tests/test_cells.py::test_sgc_basal_middle PASSED + cnmodel/cells/tests/test_cells.py::test_sgc_apical PASSED + cnmodel/data/tests/test_db.py::test_db PASSED + cnmodel/mechanisms/tests/test_mechanisms.py::test_max_open_probability PASSED + cnmodel/synapses/tests/test_psd.py::test_sgc_bushy_psd PASSED + cnmodel/synapses/tests/test_psd.py::test_sgc_tstellate_psd PASSED + cnmodel/synapses/tests/test_psd.py::test_sgc_dstellate_psd PASSED + cnmodel/synapses/tests/test_psd.py::test_sgc_octopus_psd PASSED + cnmodel/synapses/tests/test_synapses.py::test_sgc_bushy PASSED + cnmodel/synapses/tests/test_synapses.py::test_sgc_tstellate PASSED + cnmodel/synapses/tests/test_synapses.py::test_sgc_tstellate2 PASSED + cnmodel/synapses/tests/test_synapses.py::test_sgc_dstellate PASSED + cnmodel/synapses/tests/test_synapses.py::test_dstellate_bushy PASSED + cnmodel/synapses/tests/test_synapses.py::test_dstellate_tstellate PASSED + cnmodel/synapses/tests/test_synapses.py::test_dstellate_dstellate PASSED + cnmodel/util/tests/test_expfitting.py::test_fit1 PASSED + cnmodel/util/tests/test_expfitting.py::test_fit2 PASSED + cnmodel/util/tests/test_matlab.py::test_matlab PASSED + cnmodel/util/tests/test_sound.py::test_conversions PASSED + cnmodel/util/tests/test_sound.py::test_tonepip PASSED + cnmodel/util/tests/test_sound.py::test_noisepip PASSED + cnmodel/util/tests/test_stim.py::test_make_pulse PASSED + + =========================================== 36 passed in 94.05 seconds ============================================ + (base) pbmanis: cnmodel [qt5+]$ + + + + diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/cnmodel/__init__.py b/cnmodel/__init__.py new file mode 100755 index 0000000..14906eb --- /dev/null +++ b/cnmodel/__init__.py @@ -0,0 +1,25 @@ +__author__ = "Paul B. Manis and Luke Campagnola" +__version__ = "0.32a" + +try: + import faulthandler + + faulthandler.enable() +except ImportError: + pass + +import logging + +logging.basicConfig(level=logging.INFO, format="[%(process)s] %(message)s") +import os + +dirname = os.path.abspath(os.path.dirname(__file__)) +libpath = os.path.join(dirname, "..") +import neuron + +try: + neuron.h.MultiSiteSynapse +except AttributeError: + neuron.load_mechanisms(libpath) +# flag to allow unit tests to store / overwrite test results +AUDIT_TESTS = False diff --git a/cnmodel/cells/__init__.py b/cnmodel/cells/__init__.py new file mode 100644 index 0000000..b69cd1c --- /dev/null +++ b/cnmodel/cells/__init__.py @@ -0,0 +1,23 @@ +""" +Cell definitions for models. + +This class includes a number of different cell definitions and default +conductances for point models. +""" + +from .bushy import * +from .tstellate import * +from .dstellate import * +from .cartwheel import * +from .pyramidal import * +from .sgc import * +from .octopus import * +from .tuberculoventral import * +from .msoprincipal import * +from .hh import * + +from .cell import Cell + + +def cell_from_section(sec): + return Cell.from_section(sec) diff --git a/cnmodel/cells/bushy.py b/cnmodel/cells/bushy.py new file mode 100644 index 0000000..1661c5f --- /dev/null +++ b/cnmodel/cells/bushy.py @@ -0,0 +1,967 @@ +from __future__ import print_function +from neuron import h +from collections import OrderedDict +from .cell import Cell +from .. import synapses +from ..util import nstomho +from ..util import Params +import numpy as np +from .. import data +import pprint + +pp = pprint.PrettyPrinter(indent=4, width=60) + +__all__ = ["Bushy", "BushyRothman"] + + +class Bushy(Cell): + + type = "bushy" + + @classmethod + def create(cls, model="RM03", **kwds): + if model == "RM03": + return BushyRothman(**kwds) + else: + raise ValueError("Bushy model %s is unknown", model) + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is designed to pass the unit test (loc=0.5) + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for bushy cell + + kwds: dictionary of options. + Two are currently handled: + postsite : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + + if psd_type == "simple": + if terminal.cell.type in ["sgc", "dstellate", "tuberculoventral"]: + weight = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="weight", + ) + tau1 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau1", + ) + tau2 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau2", + ) + erev = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="erev", + ) + return self.make_exp2_psd( + post_sec, + terminal, + weight=weight, + loc=loc, + tau1=tau1, + tau2=tau2, + erev=erev, + ) + else: + raise TypeError( + "Cannot make simple PSD for %s => %s" + % (terminal.cell.type, self.type) + ) + + elif psd_type == "multisite": + if terminal.cell.type == "sgc": + # Max conductances for the glu mechanisms are calibrated by + # running `synapses/tests/test_psd.py`. The test should fail + # if these values are incorrect + self.AMPAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="AMPAR_gmax", + ) + * 1e3 + ) + self.NMDAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_gmax", + ) + * 1e3 + ) + self.Pr = data.get( + "sgc_synapse", species=self.species, post_type=self.type, field="Pr" + ) + self.NMDAR_vshift = data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_vshift", + ) + # adjust gmax to correct for initial Pr + self.AMPAR_gmax = self.AMPAR_gmax / self.Pr + self.NMDAR_gmax = self.NMDAR_gmax / self.Pr + + # original values (now in synapses.py): + # self.AMPA_gmax = 3.314707700918133*1e3 # factor of 1e3 scales to pS (.mod mechanisms) from nS. + # self.NMDA_gmax = 0.4531929783503451*1e3 + if "AMPAScale" in kwds: # normally, this should not be done! + self.AMPAR_gmax = ( + self.AMPAR_gmax * kwds["AMPAScale"] + ) # allow scaling of AMPA conductances + if "NMDAScale" in kwds: + self.NMDAR_gmax = self.NMDAR_gmax * kwds["NMDAScale"] # and NMDA... + return self.make_glu_psd( + post_sec, + terminal, + self.AMPAR_gmax, + self.NMDAR_gmax, + loc=loc, + nmda_vshift=self.NMDAR_vshift, + ) + elif terminal.cell.type == "dstellate": + return self.make_gly_psd(post_sec, terminal, psdtype="glyslow", loc=loc) + elif terminal.cell.type == "tuberculoventral": + return self.make_gly_psd(post_sec, terminal, psdtype="glyslow", loc=loc) + else: + raise TypeError( + "Cannot make PSD for %s => %s" % (terminal.cell.type, self.type) + ) + else: + raise ValueError("Unsupported psd type %s" % psd_type) + + def make_terminal(self, post_cell, term_type, **kwds): + if term_type == "simple": + return synapses.SimpleTerminal(self.soma, post_cell, **kwds) + + elif term_type == "multisite": + if post_cell.type in ["mso"]: + nzones = data.get( + "bushy_synapse", + species=self.species, + post_type=post_cell.type, + field="n_rsites", + ) + delay = data.get( + "bushy_synapse", + species=self.species, + post_type=post_cell.type, + field="delay", + ) + else: + raise NotImplementedError( + "No knowledge as to how to connect Bushy cell to cell type %s" + % type(post_cell) + ) + pre_sec = self.soma + return synapses.StochasticTerminal( + pre_sec, + post_cell, + nzones=nzones, + spike_source=self.spike_source, + delay=delay, + **kwds, + ) + else: + raise ValueError("Unsupported terminal type %s" % term_type) + + +class BushyRothman(Bushy): + """ + VCN bushy cell models. + Rothman and Manis, 2003abc (Type II, Type II-I) + Xie and Manis, 2013 + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + species="guineapig", + modelType=None, + modelName=None, + debug=False, + temperature=None, + ): + """ + Create a bushy cell, using the default parameters for guinea pig from + R&M2003, as a type II cell. + Additional modifications to the cell can be made by calling methods below. + + Parameters + ---------- + morphology : string (default: None) + Name of a .hoc file representing the morphology. This file is used to constructe + an electrotonic (cable) model. + If None (default), then a "point" (really, single cylinder) model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels is inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: None) + nach selects the type of sodium channel that will be used in the model. A channel mechanism + by that name must exist. The default channel is set to 'nacn' (R&M03) + + temperature : float (default: 22) + temperature to run the cell at. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + This flag duplicates the effects of tetrodotoxin in the model. Currently, the flag is not implemented. + + species: string (default 'guineapig') + species defines the pattern of ion channel densities that will be inserted, according to + prior measurements in various species. Note that + if a decorator function is specified, this argument is ignored as the decorator will + specify the channel density. + + modelName: string (default: None) + modelName specifies the source conductance pattern (RM03, XM13, etc). + modelName is passed to the decorator, or to species_scaling to adjust point (single cylinder) models. + + modelType: string (default: None) + modelType specifies the subtype of the cell model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point (single cylinder) models. + + debug: boolean (default: False) + When True, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + + """ + super(BushyRothman, self).__init__() + self.i_test_range = { + "pulse": (-1, 1, 0.05) + } # note that this might get reset with decorator according to channels + # Changing the default values will cause the unit tests to fail! + if modelType == None: + modelType = "II" + if species == "guineapig": + modelName = "RM03" + temp = 22.0 + if nach == None: + nach = "na" + if species == "mouse": + temp = 34.0 + if modelName is None: + modelName = "XM13" + if nach is None: + nach = "na" + self.debug = debug + self.status = { + "species": species, + "cellClass": self.type, + "modelType": modelType, + "modelName": modelName, + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "hillock": False, + "initialsegment": False, + "myelinatedaxon": False, + "unmyelinatedaxon": False, + "na": nach, + "ttx": ttx, + "name": self.type, + "morphology": morphology, + "decorator": decorator, + "temperature": temperature, + } + + self.spike_threshold = -40 + self.vrange = [-70.0, -55.0] # set a default vrange for searching for rmp + if self.debug: + print( + "model type, model name, species: ", modelType, modelName, species, nach + ) + + self.c_m = 0.9e-6 # default in units of F/cm^2 + + self._valid_temperatures = (temp,) + if self.status["temperature"] == None: + self.status["temperature"] = temp + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + if self.debug: + print("<< Bushy model: Creating point cell >>") + soma = h.Section( + name="Bushy_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + if self.debug: + print( + "<< Bushy model: Creating cell with morphology from %s >>" + % morphology + ) + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma, does not use tables. + self.mechanisms = ["klt", "kht", "ihvcn", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ena = self.e_na + self.soma.ek = self.e_k + self.soma().ihvcn.eh = self.e_h + self.soma().leak.erev = self.e_leak + self.c_m = 0.9 + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments, with tables. + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + if debug: + print(" << Created cell >>") + + def get_cellpars(self, dataset, species="guineapig", modelType="II"): + """ + Read data for ion channels and cell parameters from the tables + """ + # cell_type = self.map_celltype(cell_type) + # print('getcellpars: dataset, species, mmodeltype: ', dataset, species, modelType) + # print('model name: ', self.status['modelName']) + cellcap = data.get( + dataset, species=species, model_type=modelType, field="soma_Cap" + ) + chtype = data.get( + dataset, species=species, model_type=modelType, field="na_type" + ) + pars = Params(cap=cellcap, natype=chtype) + # print('pars cell/chtype: ') + if self.debug: + pars.show() + if self.status["modelName"] == "RM03": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "klt_gbar", + "ih_gbar", + "leak_gbar", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + if self.status["modelName"] == "XM13": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "klt_gbar", + "ihvcn_gbar", + "leak_gbar", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + if self.status["modelName"] == "mGBC": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "klt_gbar", + "ihvcn_gbar", + "leak_gbar", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + + return pars + + def species_scaling(self, species="guineapig", modelType="II", silent=True): + """ + This is called for POINT CELLS ONLY + Adjust all of the conductances and the cell size according to the species requested. + This scaling should be used ONLY for point models, as no other compartments + are scaled. + + This scaling routine also sets the temperature for the model to a default value. Some models + can be run at multiple temperatures, and so a default from one of the temperatures is used. + The calling cell.set_temperature(newtemp) will change the conductances and reinitialize + the cell to the new temperature settings. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be one of mouse, cat, guineapig + + modelType: string (default: 'II') + definition of model type from RM03 models, type II or type II-I + + silent : boolean (default: True) + run silently (True) or verbosely (False) + + """ + # print '\nSpecies scaling: %s %s' % (species, type) + knownspecies = ["mouse", "guineapig", "cat"] + + soma = self.soma + # cellType = self.map_celltype(modelType) + + if species == "mouse": + # use conductance levels determined from Cao et al., J. Neurophys., 2007. as + # model description in Xie and Manis 2013. Note that + # conductances were not scaled for temperature (rates were) + # so here we reset the default Q10's for conductance (g) to 1.0 + if modelType not in ["II", "II-I"]: + raise ValueError( + "\nModel type %s is not implemented for mouse bushy cells" + % modelType + ) + if self.debug: + print( + " Setting conductances for mouse bushy cell (%s), Xie and Manis, 2013" + % modelType + ) + if modelname == "XM13": + dataset = "XM13_channels" + elif modelname == "XM13nacncoop": + dataset = "XM13_channels_nacncoop" + elif modelname.startswith("mGBC"): + dataset = "mGBC_channels" + else: + raise ValueError( + f"ModelName {modelname:s} not recognized for mouse bushy cells" + ) + self.vrange = [-68.0, -55.0] # set a default vrange for searching for rmp + self.i_test_range = {"pulse": (-1.0, 1.0, 0.05)} + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.status["temperature"] = 34.0 + + pars = self.get_cellpars(dataset, species=species, modelType=modelType) + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + self.adjust_na_chans(soma, sf=1.0) + soma().kht.gbar = nstomho(pars.kht_gbar, self.somaarea) + soma().klt.gbar = nstomho(pars.klt_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.ihvcn_gbar, self.somaarea) + soma().leak.gbar = nstomho(pars.leak_gbar, self.somaarea) + self.axonsf = 0.57 + + elif species == "guineapig": + if self.debug: + print( + " Setting conductances for guinea pig %s bushy cell, Rothman and Manis, 2003" + % modelType + ) + self._valid_temperatures = (22.0, 38.0) + if self.status["temperature"] is None: + self.status["temperature"] = 22.0 + self.i_test_range = {"pulse": (-0.4, 0.4, 0.02)} + sf = 1.0 + if ( + self.status["temperature"] == 38.0 + ): # adjust for 2003 model conductance levels at 38 + sf = 2 # Q10 of 2, 22->38C. (p3106, R&M2003c) + # note that kinetics are scaled in the mod file. + dataset = "RM03_channels" + pars = self.get_cellpars(dataset, species=species, modelType=modelType) + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + self.adjust_na_chans(soma, sf=sf) + soma().kht.gbar = nstomho(pars.kht_gbar, self.somaarea) + soma().klt.gbar = nstomho(pars.klt_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.ih_gbar, self.somaarea) + soma().leak.gbar = nstomho(pars.leak_gbar, self.somaarea) + + self.axonsf = 0.57 + + else: + errmsg = ( + 'Species "%s" or model type "%s" is not recognized for Bushy cells.' + % (species, modelType) + ) + errmsg += "\n Valid species are: \n" + for s in knownspecies: + errmsg += " %s\n" % s + errmsg += "-" * 40 + raise ValueError(errmsg) + + self.status["species"] = species + self.status["modelType"] = modelType + self.check_temperature() + # self.cell_initialize(vrange=self.vrange) # no need to do this just yet. + if not silent: + print(" set cell as: ", species) + print(" with Vm rest = %6.3f" % self.vm0) + + # def channel_manager(self, modelType='RM03', cell_type='bushy-II'): + # """ + # This routine defines channel density maps and distance map patterns + # for each type of compartment in the cell. The maps + # are used by the ChannelDecorator class (specifically, its private + # \_biophys function) to decorate the cell membrane. + # These settings are only used if the decorator is called; otherwise + # for point cells, the species_scaling routine defines the channel + # densities. + # + # Parameters + # ---------- + # modelType : string (default: 'RM03') + # A string that defines the type of the model. Currently, 3 types are implemented: + # RM03: Rothman and Manis, 2003 somatic densities for guinea pig + # XM13: Xie and Manis, 2013, somatic densities for mouse + # mGBC: experimental mouse globular bushy cell with dendrites, axon, hillock and initial segment, for + # use with fully reconstructed neurons. + # + # Returns + # ------- + # Nothing + # + # Notes + # ----- + # This routine defines the following variables for the class: + # + # * conductances (gBar) + # * a channelMap (dictonary of channel densities in defined anatomical compartments) + # * a current injection range for IV's (used for testing) + # * a distance map, which defines how each conductance in a selected compartment + # changes with distance from the soma. The current implementation includes both + # linear and exponential gradients, + # the minimum conductance at the end of the gradient, and the space constant or + # slope for the gradient. + # + # """ + # + # + # dataset = '%s_channels' % modelType + # decorationmap = dataset + '_compartments' + # # print('dataset: {0:s} decorationmap: {1:s}'.format(dataset, decorationmap)) + # cellpars = self.get_cellpars(dataset, species=self.status['species'], celltype=cell_type) + # refarea = 1e-3*cellpars.cap / self.c_m + # + # table = data.get_table_info(dataset) + # chscale = data.get_table_info(decorationmap) + # pars = {} + # # retrive the conductances from the data set + # for g in table['field']: + # x = data.get(dataset, species=self.status['species'], cell_type=cell_type, + # field=g) + # if not isinstance(x, float): + # continue + # if '_gbar' in g: + # pars[g] = x/refarea + # else: + # pars[g] = x + # + # self.channelMap = OrderedDict() + # for c in chscale['compartment']: + # self.channelMap[c] = {} + # for g in pars.keys(): + # if g not in chscale['parameter']: + # # print ('Parameter %s not found in chscale parameters!' % g) + # continue + # scale = data.get(decorationmap, species=self.status['species'], cell_type=cell_type, + # compartment=c, parameter=g) + # if '_gbar' in g: + # self.channelMap[c][g] = pars[g]*scale + # else: + # self.channelMap[c][g] = pars[g] + # + # self.irange = np.linspace(-0.6, 1, 9) + + def get_distancemap(self): + return { + "dend": { + "klt": {"gradient": "exp", "gminf": 0.0, "lambda": 50.0}, + "kht": {"gradient": "exp", "gminf": 0.0, "lambda": 50.0}, + "nav11": {"gradient": "exp", "gminf": 0.0, "lambda": 50.0}, + }, # linear with distance, gminf (factor) is multiplied by gbar + "dendrite": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nav11": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + }, # linear with distance, gminf (factor) is multiplied by gbar + "apic": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nav11": {"gradient": "exp", "gminf": 0.0, "lambda": 200.0}, + }, # gradients are: flat, linear, exponential + } + # self.check_temperature() + # return + # + + # + # + # if modelType == 'RM03': + # # + # # Create a model based on the Rothman and Manis 2003 conductance set from guinea pig + # # + # self.c_m = 0.9E-6 # default in units of F/cm^2 + # self._valid_temperatures = (22., 38.) + # sf = 1.0 + # if self.status['temperature'] == None: + # self.status['temperature'] = 22. + # if self.status['temperature'] == 38: + # sf = 3.03 + # dataset = 'RM03_channels' + # pars = self.get_cellpars(dataset, species=self.status['species'], celltype='bushy-II') + # refarea = 1e-3*pars.cap / self.c_m + # self.gBar = Params(nabar=sf*pars.soma_na_gbar/refarea, # 1000.0E-9/refarea, + # khtbar=sf*pars.soma_kht_gbar/refarea, + # kltbar=sf*pars.soma_klt_gbar/refarea, + # ihbar=sf*pars.soma_ih_gbar/refarea, + # leakbar=sf*pars.soma_leak_gbar/refarea, + # ) + # print 'RM03 gbar:\n', self.gBar.show() + # + # self.channelMap = { + # 'axon': {'nacn': self.gBar.nabar, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar, 'ihvcn': 0., + # 'leak': self.gBar.leakbar / 2.}, + # 'hillock': {'nacn': self.gBar.nabar, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar, 'ihvcn': 0., + # 'leak': self.gBar.leakbar, }, + # 'initseg': {'nacn': self.gBar.nabar, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar, + # 'ihvcn': self.gBar.ihbar / 2., 'leak': self.gBar.leakbar, }, + # 'soma': {'nacn': self.gBar.nabar, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar, + # 'ihvcn': self.gBar.ihbar, 'leak': self.gBar.leakbar, }, + # 'dend': {'nacn': self.gBar.nabar, 'klt': self.gBar.kltbar * 0.5, 'kht': self.gBar.khtbar * 0.5, + # 'ihvcn': self.gBar.ihbar / 3., 'leak': self.gBar.leakbar * 0.5, }, + # 'apic': {'nacn': self.gBar.nabar, 'klt': self.gBar.kltbar * 0.2, 'kht': self.gBar.khtbar * 0.2, + # 'ihvcn': self.gBar.ihbar / 4., 'leak': self.gBar.leakbar * 0.2, }, + # } + # # self.irange = np.linspace(-1., 1., 21) + # self.distMap = {'dend': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'nacn': {'gradient': 'exp', 'gminf': 0., 'lambda': 100.}}, # linear with distance, gminf (factor) is multiplied by gbar + # 'apic': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'nacn': {'gradient': 'exp', 'gminf': 0., 'lambda': 100.}}, # gradients are: flat, linear, exponential + # } + # + # elif modelType == 'XM13': + # # + # # Create a model for a mouse bushy cell from Xie and Manis, 2013 + # # based on Cao and Oertel mouse conductance values + # # and Rothman and Manis kinetics. + # self.c_m = 0.9E-6 # default in units of F/cm^2 + # self._valid_temperatures = (34., ) + # if self.status['temperature'] == None: + # self.status['temperature'] = 34. + # dataset = 'XM13_channels' + # pars = self.get_cellpars(dataset, species=self.status['species'], celltype='bushy-II') + # refarea = 1e-3*pars.cap / self.c_m + # # self.gBar = Params(nabar=pars.soma_nav11_gbar/refarea, # 1000.0E-9/refarea, + # # khtbar=pars.soma_kht_gbar/refarea, + # # kltbar=pars.soma_klt_gbar/refarea, + # # ihbar=pars.soma_ihvcn_gbar/refarea, + # # leakbar=pars.soma_leak_gbar/refarea, + # # ) + # # print 'XM13 gbar:\n', self.gBar.show() + # # # create channel map: + # decorationmap = 'XM13_channels_bycompartment' + # + # table = data.get_table_info(dataset) + # pars = {} + # for g in table['field']: + # x = data.get(dataset, species=self.status['species'], cell_type='bushy-II', + # field=g) + # if not isinstance(x, float): + # continue + # pars[g] = (1./refarea)*data.get(dataset, species=self.status['species'], cell_type='bushy-II', + # field=g) + # chscale = data.get_table_info(decorationmap) + # self.channelMap1 = OrderedDict() + # # print chscale['parameter'] + # for c in chscale['compartment']: + # self.channelMap1[c] = {} + # for g in pars.keys(): + # # print g + # if g[5:] not in chscale['parameter']: + # continue + # scale = data.get(decorationmap, species=self.status['species'], cell_type='bushy-II', + # compartment=c, parameter=g[5:]) + # self.channelMap1[c][g] = pars[g]*scale + # + # # + # # self.channelMap = { + # # 'unmyelinatedaxon': {'nav11': self.gBar.nabar*1, 'klt': self.gBar.kltbar * 1.0, 'kht': self.gBar.khtbar, 'ihvcn': 0., + # # 'leak': self.gBar.leakbar * 0.25}, + # # 'hillock': {'nav11': self.gBar.nabar*2, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar*2.0, 'ihvcn': 0., + # # 'leak': self.gBar.leakbar, }, + # # 'initialsegment': {'nav11': self.gBar.nabar*3.0, 'klt': self.gBar.kltbar*1, 'kht': self.gBar.khtbar*2, + # # 'ihvcn': self.gBar.ihbar * 0.5, 'leak': self.gBar.leakbar, }, + # # 'soma': {'nav11': self.gBar.nabar*1.0, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar, + # # 'ihvcn': self.gBar.ihbar, 'leak': self.gBar.leakbar, }, + # # 'dend': {'nav11': self.gBar.nabar * 0.25, 'klt': self.gBar.kltbar *0.5, 'kht': self.gBar.khtbar *0.5, + # # 'ihvcn': self.gBar.ihbar *0.5, 'leak': self.gBar.leakbar * 0.5, }, + # # 'primarydendrite': {'nav11': self.gBar.nabar * 0.25, 'klt': self.gBar.kltbar *0.5, 'kht': self.gBar.khtbar *0.5, + # # 'ihvcn': self.gBar.ihbar *0.5, 'leak': self.gBar.leakbar * 0.5, }, + # # 'apic': {'nav11': self.gBar.nabar * 0.25, 'klt': self.gBar.kltbar * 0.25, 'kht': self.gBar.khtbar * 0.25, + # # 'ihvcn': self.gBar.ihbar *0.25, 'leak': self.gBar.leakbar * 0.25, }, + # # } + # import pprint + # # print 'original map:\n' + # # for k in self.channelMap.keys(): + # # print('Region: %s' % k) + # # if k in self.channelMap1.keys(): + # # print 'overlapping Region: %s' % k + # # for ch in self.channelMap[k].keys(): + # # # print ch + # # # print self.channelMap1[k].keys() + # # # print self.channelMap[k].keys() + # # if 'soma_' + ch + '_gbar' in self.channelMap1[k].keys(): + # # cx = u'soma_' + ch + u'_gbar' + # # # print ch, cx + # # print( ' {0:>4s} = {1:e} {2:e} {3:<5s}'.format(ch, self.channelMap[k][ch], self.channelMap1[k][cx], + # # str(np.isclose(self.channelMap[k][ch], self.channelMap1[k][cx])))) + # + # # print 'original: ', self.channelMap['soma'] + # self.channelMap = self.channelMap1 # use the data table + # # except need to remove soma_ from keys + # for k in self.channelMap.keys(): + # for n in self.channelMap[k].keys(): + # new_key = n.replace('_gbar', '') + # # new_key = n + # new_key = new_key.replace('soma_', '') + # # strip 'soma_' from key + # #print 'newkey: ', new_key, n + # self.channelMap[k][new_key] = self.channelMap[k].pop(n) + # + # print 'final map: ', self.channelMap['soma'] + # + # self.irange = np.linspace(-0.6, 1, 9) + # self.distMap = {'dend': {'klt': {'gradient': 'exp', 'gminf': 0., 'lambda': 50.}, + # 'kht': {'gradient': 'exp', 'gminf': 0., 'lambda': 50.}, + # 'nav11': {'gradient': 'exp', 'gminf': 0., 'lambda': 50.}}, # linear with distance, gminf (factor) is multiplied by gbar + # 'dendrite': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'nav11': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}}, # linear with distance, gminf (factor) is multiplied by gbar + # 'apic': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'nav11': {'gradient': 'exp', 'gminf': 0., 'lambda': 200.}}, # gradients are: flat, linear, exponential + # } + # + # elif modelType == 'mGBC': + # # bushy from Xie and Manis, 2013, based on Cao and Oertel mouse conductances, + # # BUT modified ad hoc for SBEM reconstructions. + # dataset = 'mGBC_channels' + # + # self._valid_temperatures = (34.,) + # if self.status['temperature'] == None: + # self.status['temperature'] = 34. + # pars = self.get_cellpars(dataset, species=self.status['species'], celltype='bushy-II') + # refarea = 1e-3*pars.cap / self.c_m + # print (pars.cap, pars.soma_kht_gbar, refarea) # refarea should be about 30e-6 + # + # self.gBar = Params(nabar=pars.soma_na_gbar/refarea, # 1000.0E-9/refarea, + # khtbar=pars.soma_kht_gbar/refarea, + # kltbar=pars.soma_klt_gbar/refarea, + # ihbar=pars.soma_ih_gbar/refarea, + # leakbar=pars.soma_leak_gbar/refarea, + # ) + # print 'mGBC gbar:\n', self.gBar.show() + # sodiumch = 'jsrna' + # self.channelMap = { + # 'axon': {sodiumch: self.gBar.nabar*1., 'klt': self.gBar.kltbar * 1.0, 'kht': self.gBar.khtbar, 'ihvcn': 0., + # 'leak': self.gBar.leakbar * 0.25}, + # 'unmyelinatedaxon': {sodiumch: self.gBar.nabar*3.0, 'klt': self.gBar.kltbar * 2.0, + # 'kht': self.gBar.khtbar*3.0, 'ihvcn': 0., + # 'leak': self.gBar.leakbar * 0.25}, + # 'myelinatedaxon': {sodiumch: self.gBar.nabar*0, 'klt': self.gBar.kltbar * 1e-2, + # 'kht': self.gBar.khtbar*1e-2, 'ihvcn': 0., + # 'leak': self.gBar.leakbar * 0.25*1e-3}, + # 'hillock': {sodiumch: self.gBar.nabar*4.0, 'klt': self.gBar.kltbar*1.0, 'kht': self.gBar.khtbar*3.0, + # 'ihvcn': 0., 'leak': self.gBar.leakbar, }, + # 'initseg': {sodiumch: self.gBar.nabar*3.0, 'klt': self.gBar.kltbar*2, 'kht': self.gBar.khtbar*2, + # 'ihvcn': self.gBar.ihbar * 0.5, 'leak': self.gBar.leakbar, }, + # 'soma': {sodiumch: self.gBar.nabar*0.65, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar*1.5, + # 'ihvcn': self.gBar.ihbar, 'leak': self.gBar.leakbar, }, + # 'dend': {sodiumch: self.gBar.nabar * 0.2, 'klt': self.gBar.kltbar *1, 'kht': self.gBar.khtbar *1, + # 'ihvcn': self.gBar.ihbar *0.5, 'leak': self.gBar.leakbar * 0.5, }, + # 'dendrite': {sodiumch: self.gBar.nabar * 0.2, 'klt': self.gBar.kltbar *1, 'kht': self.gBar.khtbar *1, + # 'ihvcn': self.gBar.ihbar *0.5, 'leak': self.gBar.leakbar * 0.5, }, + # 'apic': {sodiumch: self.gBar.nabar * 0.25, 'klt': self.gBar.kltbar * 0.25, 'kht': self.gBar.khtbar * 0.25, + # 'ihvcn': self.gBar.ihbar *0.25, 'leak': self.gBar.leakbar * 0.25, }, + # } + # self.irange = np.arange(-1.5, 2.1, 0.25 ) + # self.distMap = {'dend': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # sodiumch: {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}}, # linear with distance, gminf (factor) is multiplied by gbar + # 'dendrite': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 20.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 20.}, + # sodiumch: {'gradient': 'linear', 'gminf': 0., 'lambda': 20.}}, # linear with distance, gminf (factor) is multiplied by gbar + # 'apic': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # sodiumch: {'gradient': 'exp', 'gminf': 0., 'lambda': 200.}}, # gradients are: flat, linear, exponential + # } + # else: + # raise ValueError('model type %s is not implemented' % modelType) + # self.check_temperature() + + def adjust_na_chans(self, soma, sf=1.0, gbar=1000.0): + """ + adjust the sodium channel conductance + + Parameters + ---------- + soma : neuron section object + A soma object whose sodium channel complement will have its + conductances adjusted depending on the channel type + + gbar : float (default: 1000.) + The maximal conductance for the sodium channel + + Returns + ------- + Nothing : + + """ + + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) * sf + nach = self.status["na"] + if nach == "jsrna": + soma().jsrna.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("jsrna gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar + soma.ena = 50 # self.e_na + # print('gnabar: ', soma().nav11.gbar, ' vs: 0.0192307692308') + soma().nav11.vsna = 4.3 + if self.debug: + print("bushy using inva11") + if nach == "nacncoop": + soma().nacncoop.gbar = gnabar + soma().nacncoop.KJ = 2000.0 + soma().nacncoop.p = 0.25 + somae().nacncoop.vsna = 0.0 + soma.ena = self.e_na + if debug: + print("nacncoop gbar: ", soma().nacncoop.gbar) + elif nach in ["na", "nacn"]: + soma().na.gbar = gnabar + soma.ena = self.e_na + # soma().na.vsna = 0. + if self.debug: + print("na gbar: ", soma().na.gbar) + else: + raise ValueError( + "Sodium channel %s is not recognized for Bushy cells", nach + ) + + def add_axon(self): + """ + Add a default axon from the generic cell class to the bushy cell (see cell class). + """ + Cell.add_axon(self, self.c_m, self.R_a, self.axonsf) + + def add_pumps(self): + """ + Insert mechanisms for potassium ion management, sodium ion management, and a + sodium-potassium pump at the soma. + """ + soma = self.soma + soma.insert("k_conc") + + ki0_k_ion = 140 + soma().ki = ki0_k_ion + soma().ki0_k_conc = ki0_k_ion + soma().beta_k_conc = 0.075 + + soma.insert("na_conc") + nai0_na_ion = 5 + soma().nai = nai0_na_ion + soma().nai0_na_conc = nai0_na_ion + soma().beta_na_conc = 0.075 + + soma.insert("nakpump") + soma().nakpump.inakmax = 8 + soma().nao = 145 + soma().ko = 5 + soma().nakpump.Nai_inf = 5 + soma().nakpump.Ki_inf = 140 + soma().nakpump.ATPi = 5 + self.status["pumps"] = True + + def add_dendrites(self): + """ + Add a simple dendrite to the bushy cell. + """ + if self.debug: + print("Adding dendrite to Bushy model") + section = h.Section + primarydendrite = section(cell=self.soma) + primarydendrite.connect(self.soma) + primarydendrite.nseg = 10 + primarydendrite.L = 100.0 + primarydendrite.diam = 2.5 + primarydendrite.insert("klt") + primarydendrite.insert("ihvcn") + primarydendrite().klt.gbar = self.soma().klt.gbar / 2.0 + primarydendrite().ihvcn.gbar = self.soma().ihvcn.gbar / 2.0 + + primarydendrite.cm = self.c_m + primarydendrite.Ra = self.R_a + nsecd = range(0, 5) + secondarydendrite = [] + for ibd in nsecd: + secondarydendrite.append(section(cell=self.soma)) + for ibd in nsecd: + secondarydendrite[ibd].connect(primarydendrite) + secondarydendrite[ibd].diam = 1.0 + secondarydendrite[ibd].L = 15.0 + secondarydendrite[ibd].cm = self.c_m + secondarydendrite[ibd].Ra = self.R_a + self.primarydendrite = primarydendrite + self.secondarydendrite = secondarydendrite + self.status["dendrite"] = True + if self.debug: + print("Bushy: added dendrites") + h.topology() + self.add_section(maindend, "primarydendrite") + self.add_section(secdend, "secondarydendrite") diff --git a/cnmodel/cells/cartwheel.py b/cnmodel/cells/cartwheel.py new file mode 100644 index 0000000..3ad629f --- /dev/null +++ b/cnmodel/cells/cartwheel.py @@ -0,0 +1,447 @@ +from __future__ import print_function + +import numpy as np +from neuron import h + +from .cell import Cell +from .. import data +from .. import synapses +from ..util import Params +from ..util import nstomho + +__all__ = ["Cartwheel", "CartwheelDefault"] + + +class Cartwheel(Cell): + + type = "cartwheel" + + @classmethod + def create(cls, model="CW", **kwds): + if model == "CW": + return CartwheelDefault(**kwds) + else: + raise ValueError("Carthweel model is unknown", model) + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is to try to pass the default unit test (loc=0.5) + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for bushy cell + + kwds: dict of options. Two are currently handled: + postsize : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + """ + self.pre_sec = terminal.section + pre_cell = terminal.cell + post_sec = self.soma + + if psd_type == "simple": + if terminal.cell.type in [ + "sgc", + "dstellate", + "tuberculoventral", + "cartwheel", + ]: + weight = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="weight", + ) + tau1 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau1", + ) + tau2 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau2", + ) + erev = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="erev", + ) + return self.make_exp2_psd( + post_sec, + terminal, + weight=weight, + loc=loc, + tau1=tau1, + tau2=tau2, + erev=erev, + ) + else: + raise TypeError( + "Cannot make simple PSD for %s => %s" + % (terminal.cell.type, self.type) + ) + + else: + raise ValueError( + "Unsupported psd type %s for cartwheel cell (inputs not implemented yet)" + % psd_type + ) + + def make_terminal(self, post_cell, term_type, **kwds): + if term_type == "simple": + return synapses.SimpleTerminal(self.soma, post_cell, **kwds) + elif term_type == "multisite": + if post_cell.type in ["tuberculoventral", "pyramidal"]: + nzones = data.get( + "cartwheel_synapse", + species=self.species, + post_type=post_cell.type, + field="n_rsites", + ) + delay = data.get( + "cartwheel_synapse", + species=self.species, + post_type=post_cell.type, + field="delay", + ) + else: + raise NotImplementedError( + "No knowledge as to how to connect cartwheel cell to cell type %s" + % type(post_cell) + ) + pre_sec = self.soma + return synapses.StochasticTerminal( + pre_sec, + post_cell, + nzones=nzones, + spike_source=self.spike_source, + delay=delay, + **kwds + ) + else: + raise ValueError("Unsupported terminal type %s" % term_type) + + +class CartwheelDefault(Cartwheel, Cell): + """ + DCN cartwheel cell model. + + """ + + def __init__( + self, + morphology=None, + decorator=None, + ttx=False, + nach=None, + species="mouse", + modelType=None, + debug=False, + ): + """ + Create cartwheel cell model, based on a Purkinje cell model from Raman. + There are no variations available for this model. + + Parameters + ---------- + morphology : string (default: None) + Name of a .hoc file representing the morphology. This file is used to constructe + an electrotonic (cable) model. + If None (default), then a "point" (really, single cylinder) model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels is inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: None) + nach selects the type of sodium channel that will be used in the model. A channel mechanism + by that name must exist. The default is naRsg, a resurgent sodium channel model. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + This flag duplicates the effects of tetrodotoxin in the model. Currently, the flag is not implemented. + + species: string (default 'rat') + species defines the pattern of ion channel densities that will be inserted, according to + prior measurements in various species. Note that + if a decorator function is specified, this argument is ignored as the decorator will + specify the channel density. + + modelType: string (default: None) + modelType specifies the subtype of the cell model that will be used. + modelType is passed to the decorator, or to species_scaling to adjust point (single cylinder) models. + Only type "I" is recognized for the cartwheel cell model. + + debug: boolean (default: False) + When True, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + """ + super(CartwheelDefault, self).__init__() + if modelType == None: + modelType = "I" + if nach == None: + nach = "naRsg" + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "Cartwheel", + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + + self.i_test_range = {"pulse": (-0.2, 0.2, 0.02)} + # self.spike_threshold = 0 + self.vrange = [-75.0, -52.0] # set a default vrange for searching for rmp + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + soma = h.Section( + name="Cartwheel_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + # cm = 1 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + # v_potassium = -80 # potassium reversal potential + # v_sodium = 50 # sodium reversal potential + + self.mechanisms = [ + "naRsg", + "bkpkj", + "hpkj", + "kpkj", + "kpkj2", + "kpkjslow", + "kpksk", + "lkpkj", + "cap", + ] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.insert("cadiff") + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + + if debug: + print( + "<< Cartwheel: Modified version of Raman Purkinje cell model created >>" + ) + + def get_cellpars(self, dataset, species="guineapig", celltype="II"): + somaDia = data.get( + dataset, species=species, cell_type=celltype, field="soma_Dia" + ) + chtype = data.get( + dataset, species=species, cell_type=celltype, field="soma_na_type" + ) + pcabar = data.get( + dataset, species=species, cell_type=celltype, field="soma_pcabar" + ) + pars = Params(soma_Dia=somaDia, soma_natype=chtype, soma_pcabar=pcabar) + for g in [ + "soma_narsg_gbar", + "soma_kpkj_gbar", + "soma_kpkj2_gbar", + "soma_kpkjslow_gbar", + "soma_kpksk_gbar", + "soma_lkpkj_gbar", + "soma_bkpkj_gbar", + "soma_hpkj_gbar", + "soma_hpkj_eh", + "soma_lkpkj_e", + "soma_e_k", + "soma_e_na", + "soma_e_ca", + ]: + pars.additem( + g, data.get(dataset, species=species, cell_type=celltype, field=g) + ) + return pars + + def species_scaling(self, silent=True, species="mouse", modelType="I"): + """ + Adjust all of the conductances and the cell size according to the species requested. + This scaling should be used ONLY for point models, as no other compartments + are scaled. + + Parameters + ---------- + species : string (default: 'rat') + name of the species to use for scaling the conductances in the base point model + Must be one of mouse, cat, guineapig + + modelType: string (default: 'I') + definition of model type from RM03 models, type II or type II-I + + silent : boolean (default: True) + run silently (True) or verbosely (False) + + Note + ---- + For the cartwheel cell model, there is only a single scaling recognized. + """ + if species is not "mouse": + raise ValueError('Cartwheel species: only "mouse" is recognized') + if modelType is not "I": + raise ValueError('Cartwheel modelType: only "I" is recognized') + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + + pars = self.get_cellpars("CW_channels", species=species, celltype="cartwheel") + self.set_soma_size_from_Diam(pars.soma_Dia) + self.soma().bkpkj.gbar = nstomho(pars.soma_bkpkj_gbar, self.somaarea) + self.soma().hpkj.gbar = nstomho(pars.soma_hpkj_gbar, self.somaarea) + self.soma().kpkj.gbar = nstomho(pars.soma_kpkj_gbar, self.somaarea) + self.soma().kpkj2.gbar = nstomho(pars.soma_kpkj2_gbar, self.somaarea) + self.soma().kpkjslow.gbar = nstomho(pars.soma_kpkjslow_gbar, self.somaarea) + self.soma().kpksk.gbar = nstomho(pars.soma_kpksk_gbar, self.somaarea) + self.soma().lkpkj.gbar = nstomho(pars.soma_lkpkj_gbar, self.somaarea) + self.soma().naRsg.gbar = nstomho(pars.soma_narsg_gbar, self.somaarea) + self.soma().cap.pcabar = pars.soma_pcabar + self.soma().ena = pars.soma_e_na # 50 + self.soma().ek = pars.soma_e_k # -80 + self.soma().lkpkj.e = pars.soma_lkpkj_e # -65 + self.soma().hpkj.eh = pars.soma_hpkj_eh # -43 + self.soma().eca = pars.soma_e_ca # 50 + + self.status["na"] = pars.soma_natype + self.status["species"] = species + self.status["modelType"] = modelType + self.check_temperature() + if not silent: + print("set cell as: ", species) + print(" with Vm rest = %f" % self.vm0) + + # print 'set up' + + def i_currents(self, V): + """ + For the steady-state case, return the total current at voltage V + Used to find the zero current point. + Overrides i_currents in cells.py, because this model uses conductances + that are not specified in the default cell mode. + + Parameters + ---------- + V : float, mV (no default) + Voltage at which the current for each conductance is computed. + + Returns + ------- + I : float, nA + The sum of the currents at steady-state for all of the conductances. + """ + for part in self.all_sections.keys(): + for sec in self.all_sections[part]: + sec.v = V + h.celsius = self.status["temperature"] + h.finitialize() + self.ix = {} + + if "naRsg" in self.mechanisms: + self.ix["naRsg"] = self.soma().naRsg.gna * (V - self.soma().ena) + if "cap" in self.mechanisms: + a = self.soma().cap.pcabar * self.soma().cap.minf + self.ix["cap"] = a * self.ghk(V, self.soma().cao, self.soma().cai, 2) + if "kpkj" in self.mechanisms: + self.ix["kpkj"] = self.soma().kpkj.gk * (V - self.soma().ek) + if "kpkj2" in self.mechanisms: + self.ix["kpkj2"] = self.soma().kpkj2.gk * (V - self.soma().ek) + if "kpkjslow" in self.mechanisms: + self.ix["kpkjslow"] = self.soma().kpkjslow.gk * (V - self.soma().ek) + if "kpksk" in self.mechanisms: + self.ix["kpksk"] = self.soma().kpksk.gk * (V - self.soma().ek) + if "bkpkj" in self.mechanisms: + self.ix["bkpkj"] = self.soma().bkpkj.gbkpkj * (V - self.soma().ek) + if "hpkj" in self.mechanisms: + self.ix["hpkj"] = self.soma().hpkj.gh * (V - self.soma().hpkj.eh) + # leak + if "lkpkj" in self.mechanisms: + self.ix["lkpkj"] = self.soma().lkpkj.gbar * (V - self.soma().lkpkj.e) + return np.sum([self.ix[i] for i in self.ix]) + + def ghk(self, v, ci, co, z): + """ + GHK flux equation, used to calculate current density through calcium channels + rather than standard Nernst equation. + + Parameters + ---------- + v : float, mV + voltage for GHK calculation + ci : float, mM + internal ion concentration + co : float, mM + external ion concentraion + z : float, no units + valence + + Returns + ------- + flux : A/m^2 + + """ + F = 9.6485e4 # (coul) + R = 8.3145 # (joule/degC) + T = h.celsius + 273.19 # Kelvin + E = (1e-3) * v # convert mV to V + Ci = ci + (self.soma().cap.monovalPerm) * ( + self.soma().cap.monovalConc + ) # : Monovalent permeability + if ( + np.fabs(1 - np.exp(-z * (F * E) / (R * T))) < 1e-6 + ): # denominator is small -> Taylor series + ghk = ( + (1e-6) + * z + * F + * (Ci - co * np.exp(-z * (F * E) / (R * T))) + * (1 - (z * (F * E) / (R * T))) + ) + else: + ghk = ( + (1e-6) + * z ** 2.0 + * (E * F ** 2.0) + / (R * T) + * (Ci - co * np.exp(-z * (F * E) / (R * T))) + / (1 - np.exp(-z * (F * E) / (R * T))) + ) + return ghk diff --git a/cnmodel/cells/cell.py b/cnmodel/cells/cell.py new file mode 100644 index 0000000..0dcf21c --- /dev/null +++ b/cnmodel/cells/cell.py @@ -0,0 +1,1200 @@ +from __future__ import print_function +import weakref +import numpy as np +import scipy.optimize +from collections import OrderedDict +import neuron +from neuron import h +from ..util import nstomho, mho2ns +from ..util import custom_init +from .. import synapses +from .. import data +from .. import morphology +from .. import decorator + +""" +Term definitions: +cell class is the class of morphological cell: bushy, tstellate, etc. +Each cell class is implmeneted as a separate python class (no pun) +modelName is name of the source model used so it is like the type, but one level up). +ModelNames are RM03, XM13, and for other cell types may refer to the original model, +such as POK (Kanold pyramidal cell), MCG (McGinley octopus), Eager, etc. +These model designations may have only one model type (POK), or may have multiple types (RM03, XM13) +modelType refers to the Rothman and Manis 2003 model classes (I, II, I-c, I-t, II-1, I-II, etc) +These are physiologically based, but in the ion channel tables are mapped to morphological classes sort of, + + +""" + + +class Cell(object): + """ + Base class for all cell types. + + """ + + type = None + + # create a lookup table to map sections to their parent cell + sec_lookup = weakref.WeakValueDictionary() + + @classmethod + def from_section(cls, sec): + return cls.sec_lookup[sec.name()] + + def __init__(self): + # dictionary of all sections associated with this cell + self.hr = None # hoc reader - e.g., we have read a morphology file. + self.all_sections = {} + # the following section types (parts) are known to us: + for k in [ + "soma", + "maindend", + "secdend", + "dend", + "dendrite", + "primarydendrite", + "secondarydendrite", + "internode", + "initialsegment", + "axonnode", + "axon", + "unmyelinatedaxon", + "myelinatedaxon", + "hillock", + ]: + self.all_sections[k] = [] # initialize to an empty list + self.species = "mouse" + self.status = {} # dictionary of parameters used to instantiate the cell. + # Record synaptic inputs and projections + self.inputs = [] # inputs are recorded - synapse object, post_opts and kwds + self.outputs = [] + self.initial_mechanisms = None + # each cell has the following parameters: + self.totcap = None # total membrane capacitance (somatic) + self.somaarea = None # total soma area + self.initsegment = None # hold initial segment sections + self.axnode = None # hold nodes of ranvier sections + self.internode = None # hold internode sections + self.maindend = None # hold main dendrite sections + self.secdend = None # hold secondary dendrite sections + self.dendrite = None + self.axon = None + self.axonsf = None # axon diameter scale factor + # define defaults for these parameters (RM03 model defaults) + self.e_k = -70 # potassium reversal potential, mV + self.e_na = 55 + self.e_h = -43 + self.c_m = 0.9 # specific membrane capacitance, uf/cm^2 + self.R_a = 150 # axial resistivity of cytoplasm/axoplasm, ohm.cm + self.e_leak = -65 + # Recommended current (min, max, step) for testing this cell + self.i_test_range = ( + -0.5, + 0.5, + 0.05, + ) # defines default current steps for IC curve + + # Recommended threshold for detecting spikes from this cell + self.spike_threshold = -40 + + # Resting potential for this cell, determined by calling + # self.find_i0() + self.vm0 = None + + def check_temperature(self): + if self.status["temperature"] not in self._valid_temperatures: + tstring = ", ".join("%3.1f " % t for t in self._valid_temperatures) + raise ValueError( + "Cell %s %s %s temperature %3.1f is invalid; must be in: [%s]" + % ( + self.type, + self.status["species"], + self.status["modelType"], + self.status["temperature"], + tstring, + ) + ) + + def set_temperature(self, temperature): + """ + Set the temperature setting for this cell. + """ + if self.status["decorator"] is None: + if self.status["temperature"] is None: # only if not already set + self.status["temperature"] = temperature + self.species_scaling( + species=self.status["species"], modelType=self.status["modelType"] + ) + else: + self.status["temperature"] = temperature + + # self.decorate() # call the decorator + + def set_morphology(self, morphology_file=None): + """ + Set the cell's morphological structure from a file that defines sections + (for example, a morphology file read by neuronvis), or from a morphology + object that has already been retrieved/created. + + Parameters + ---------- + morphology_file : string or morphology object (default: None) + File name/path for the morphology file (for example, .hoc or .swc file) + Alternatively, this can be a morphology object returned by the morphology class. + + Returns + ------- + nothing + + """ + self.morphology_file = morphology_file # save the source file name + if isinstance(morphology_file, str): + if morphology_file.endswith(".hoc"): + self.morphology = morphology.HocReader(morphology_file) + elif morphology_file.endswith(".swc"): + self.morphology = morphology.SwcReader(morphology_file) + else: + raise ValueError("Unknown morphology file type [must be .hoc or .swc]") + elif isinstance(morphology_file, morphology.Morphology): + self.morphology = morphology_file + else: + print(morphology_file) + raise TypeError("Invalid morphology type: must be filename(str) or ") + self.hr = ( + self.morphology + ) # extensive renaming required in calling classes, temporary fix. + self.morphology.read_section_info() # not sure this is necessary... + # these were not instantiated when the file was read, but when the decorator was run. + for s in self.hr.sec_groups.keys(): + for sec in self.hr.sec_groups[s]: + section = self.hr.get_section(sec) + mechs = self.hr.get_mechanisms(sec) + if s == "myelinatedaxon": + section.cm = 0.002 + self.add_section(section, s) # add the section to the cell. + # print '\nmechanisms for section: %s', section + # self.print_mechs(section) + self.set_soma_size_from_Section( + self.soma + ) # this is used for reporting and setting g values... + if isinstance(self.soma, list): + self.distances(self.soma[1]) + else: + self.distances(self.soma) + self.hr.distanceMap = self.distanceMap + + def add_section(self, sec, sec_type): + """ + Add a section (or list of sections) to this cell. + This adds the section to self.all_sections[sec_type] and also allows + the cell to be accessed from the section using + cells.cell_from_section(). + + Notes: + + *sec_type* must be one of the keys already in self.all_sections. + + This method does not connect sections together; that must be + done manually. + + """ + if not isinstance(sec, list): + sec = [sec] + self.all_sections[sec_type].extend(sec) + for s in sec: + Cell.sec_lookup[s.name()] = self + + def list_sections(self): + # print self.all_sections + print("Known Section names:") + for sec in self.all_sections: + print(" %s" % sec) + s = self.all_sections[sec] + # print 's: ', s + if len(s) > 0: + print(" ------------------------------------------") + print(" Sections present:") + for u in s: + print( + " Type: %s (%s, %s): %s" + % ( + sec, + u.name(), + str(self.hr.get_section(u.name())), + Cell.sec_lookup[u.name()], + ) + ) + print(" ------------------------------------------") + else: + print(" No section of this type in cell") + + def get_section_type(self, sec): + for s in self.all_sections: + if sec in self.all_sections[s]: + return s + return None + + def get_post_sec(self, kwds): + """ + Get the postsynaptic section from the value of postsite + in kwds. This is typically called from the cell-specific make_psd method. + If the key 'postsite' is in the kwds dict, we look it up. + If not, then we use the soma section as a default instead. + + Parameters + ---------- + kwds : dict + dictionary of keywords, may have a key 'postsite' + + Returns: + loc, post_sec + the location (0-1) of the desired point process insertion, and + post_sec, the neuron section where that insertion will take place + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + return loc, post_sec + + def set_d_lambda(self, freq=100, d_lambda=0.1): + """ + Sets nseg in each section to an odd value so that its segments are no longer than + d_lambda x the AC length constant at frequency freq in that section. + The defaults are reasonable values for most models + Be sure to specify your own Ra and cm before calling geom_nseg() + + To understand why this works, + and the advantages of using an odd value for nseg, + see Hines, M.L. and Carnevale, N.T. NEURON: a tool for neuroscientists. The Neuroscientist 7:123-135, 2001. + This is a python version of the hoc code. + + Parameters + ---------- + freq : float, default=100. (Hz) + Frequency in Hz to use in computing nseg. + d_lambda : float, default=0.1 + fraction of AC length constant for minimum segment length + + """ + if self.hr is None: # no hoc reader file, so no adjustments + return + for st in self.all_sections.keys(): + for i, section in enumerate(self.all_sections[st]): + nseg = ( + int( + (section.L / (d_lambda * self._lambda_f(freq, section)) + 0.9) + / 2 + ) + * 2 + + 1 + ) + if nseg < 3: + nseg = 3 # ensure at least 3 segments per section... + section.nseg = nseg + + def _lambda_f(self, freq, section): + """ + get lambda_f for the section (internal) + + Parameters + ---------- + freq : float, default=100. (Hz) + Frequency in Hz to use in computing nseg. + section : Neuron section object + + Returns + ------- + section length normalized by the length constant at freq. + """ + self.hr.h("access %s" % section.name()) + if self.hr.h.n3d() < 2: + return 1e-5 * np.sqrt( + section.diam / (4.0 * np.pi * freq * section.Ra * section.cm) + ) + # above was too inaccurate with large variation in 3d diameter + # so now we use all 3-d points to get a better approximate lambda + x1 = self.hr.h.arc3d(0) + d1 = self.hr.h.diam3d(0) + lam = 0.001 + for i in range(int(self.hr.h.n3d()) - 1): + x2 = self.hr.h.arc3d(i) + d2 = self.hr.h.diam3d(i) + lam = lam + ((x2 - x1) / np.sqrt(d1 + d2)) + x1 = x2 + d1 = d2 + + # length of the section in units of lambda + lam = ( + lam + * np.sqrt(2.0) + * 1e-5 + * np.sqrt(4.0 * np.pi * freq * section.Ra * section.cm) + ) + return section.L / lam + + @property + def soma(self): + """ + First (or only) section in the "soma" section group. + """ + if isinstance(self.all_sections["soma"], list): + return self.all_sections["soma"][0] + else: + return self.all_sections["soma"] + + def decorate(self): + """ + decorate the cell with it's own class channel decorator + """ + self.decorated = decorator.Decorator(cell=self) + self.decorated.channelValidate(self, verify=False) + self.mechanisms = ( + self.hr.mechanisms + ) # copy out all of the mechanisms that were inserted + + # def channel_manager(self, modelType='RM03'): + # """ + # Every cell class should have a channel manager if it is set up to handle morphology. + # This function should be overridden in the class with an appropriate routine that + # builds the dictionary needed to decorate the cell. See the bushy cell class for + # an example. + # + # Parameters + # ---------- + # modelType : string (default: 'RM03') + # A string that identifies what type of model the channel manager will implement. + # This may be used to define different kinds of channels, or channel densities + # and compartmental placement for different cells. + # """ + # raise NotImplementedError("No channel manager exists for cells of the class: %s" % + # (self.__class__.__name__)) + + def connect(self, post_cell, pre_opts=None, post_opts=None, **kwds): + """ + Create a new synapse connecting this cell to a postsynaptic cell. + The synapse is automatically created using + pre_cell.make_terminal(post_cell, \**pre_opts) and + post_cell.make_psd(terminal, \**post_opts). + + By default, the cells decide which sections to connect. This can be + overridden by specifying 'section' in pre_opts and/or post_opts. + + Parameters + ---------- + post_cell : NEURON section (required) + The postsynaptic cell that will receive the connection. + pre_opts : dictionary of options for the presynaptic cell (default: None) + see the synapses class for valid options and format. + post_opts : diction of options for the postsynaptic cell (default: None) + see synapses class for valid options and format. + \**kwds : (optional) + argmuments that are passed to the synapses class. + + Returns + ------- + the synapse object + + """ + if pre_opts is None: + pre_opts = {} + if post_opts is None: + post_opts = {} + + synapse = synapses.Synapse(self, pre_opts, post_cell, post_opts, **kwds) + self.outputs.append(synapse) + post_cell.inputs.append([synapse, post_opts, kwds]) + + return synapse + + def print_connections(self): + """ + This is mostly for debugging ... + """ + print("outputs: ", self.outputs) + print("inputs: ", self.inputs) + + def make_terminal(self, post_cell, **kwds): + """ + Create a synaptic terminal release mechanism suitable for output + from this cell to post_sec + This routine is a placeholder and should be replaced in the specific + cell class with code that performs the required actions for that class. + + Parameters + ---------- + post_cell : the target terminal cell (required) + + \**kwds : parameters passed to the terminal + + """ + raise NotImplementedError( + "Cannot make Terminal connecting %s => %s" + % (self.__class__.__name__, post_cell.__class__.__name__) + ) + + def make_psd(self, terminal, **kwds): + """ + Create a PSD suitable for synaptic input from pre_sec. + This routine is a placeholder and should be overridden in the specific + cell class with code that performs the required actions for that class. + + Parameters + ---------- + terminal : the terminal that connects to the PSD (required) + + \**kwds : parameters passed to the terminal + + """ + pre_cell = terminal.cell + raise NotImplementedError( + "Cannot make PSD connecting %s => %s" + % (pre_cell.__class__.__name__, self.__class__.__name__) + ) + + def make_glu_psd(self, post_sec, terminal, AMPA_gmax, NMDA_gmax, **kwds): + # Get AMPAR kinetic constants from database + params = data.get( + "sgc_ampa_kinetics", + species=self.species, + post_type=self.type, + field=["Ro1", "Ro2", "Rc1", "Rc2", "PA"], + ) + + return synapses.GluPSD( + post_sec, + terminal, + ampa_gmax=AMPA_gmax, + nmda_gmax=NMDA_gmax, + ampa_params=dict( + Ro1=params["Ro1"], + Ro2=params["Ro2"], + Rc1=params["Rc1"], + Rc2=params["Rc2"], + PA=params["PA"], + ), + **kwds + ) + + def make_gly_psd(self, post_sec, terminal, psdtype, **kwds): + # Get GLY kinetic constants from database + params = data.get( + "gly_kinetics", + species=self.species, + post_type=self.type, + field=["KU", "KV", "XMax"], + ) + psd = synapses.GlyPSD(post_sec, terminal, psdType=psdtype, **kwds) + return psd + + def make_exp2_psd( + self, post_sec, terminal, weight=0.01, loc=0.5, tau1=0.1, tau2=0.3, erev=0.0 + ): + return synapses.Exp2PSD( + post_sec, terminal, weight=weight, loc=loc, tau1=tau1, tau2=tau2, erev=erev + ) + + def print_status(self): + print("\nCell model: %s" % self.__class__.__name__) + print(self.__doc__) + print(" Model Status:") + print("-" * 24) + for s in self.status.keys(): + print("{0:>12s} : {1:<12s}".format(s, repr(self.status[s]))) + print("-" * 32) + + def cell_initialize(self, showinfo=False, vrange=None, **kwargs): + """ + Initialize this cell to it's "rmp" under current conditions + All sections in the cell are set to the same value + """ + if self.vm0 is None: + self.vm0 = self.find_i0(showinfo=showinfo, vrange=vrange, **kwargs) + for part in self.all_sections.keys(): + for sec in self.all_sections[part]: + sec.v = self.vm0 + + def get_mechs(self, section): + """ + return a list of the mechanisms that are present in a section + a mechanism is required to have a gbar variable. + This routine should be called at the end of every cell creation routine. + """ + u = dir(section()) + mechs = [] + for m in u: + if m[0:2] == "__": + continue + if m in [ + "cm", + "diam", + "k_ion", + "na_ion", + "next", + "point_processes", + "sec", + "v", + "x", + ]: + continue # skip non-mechanisms known to us + try: + gx = eval("section()." + m + ".gbar") + mechs.append(m) + except: + pass + self.mechs = mechs + return mechs + + def print_mechs(self, section): + """ + print the mechanisms that are inserted into the specified section, + and their densities (in uS/cm^2) + + """ + print("\n Installed mechanisms:") + self.get_mechs(section) + # print eval('section().nav11.gbar') + + print("somaarea: {:.3e}".format(self.somaarea)) + print("Mechanisms:", end="") + for s in self.mechs: + print(" {:>8s} ".format(s), end="") + print("") + for m in self.mechs: + try: + gx = eval("section()." + m + ".gbar") + erev = 0.0 + if m == "leak": + erev = eval("section()." + m + ".erev") + if m in ["jsrna", "na", "nacn", "nav11", "nacncoop", "napyr", "nap"]: + erev = eval("section().ena") + if m in ["klt", "kht", "ka"]: + erev = eval("section().e%s" % m) + if m in ["kis", "kif", "kdpyr", "kcnq"]: + erev = eval("section().ek") + if m in ["hcno", "ihvcn", "hcnobo", "ihpyr", "ihpyr_adj"]: + erev = eval("section()." + m + ".eh") + print( + "{0:>12s} : {2:8.1f} nS {1:7.3e} mho/cm2 {3:>5.1f} mV".format( + m, gx, mho2ns(gx, self.somaarea), erev + ) + ) + except: + print("{0:>12s} : ".format(m)) + print("-" * 32) + + def print_all_mechs(self): + print(self.get_all_mechs()) + + def get_all_mechs(self): + """ + return a string with all the mechanisms + """ + res = "\nAll mechanisms in all sections: \n" + for part in self.all_sections.keys(): + if len(self.all_sections[part]) == 0: + # res += 'Cell part: %s hs not sections' % part + continue + res += "Cell part: %s\n" % part + for sec in self.all_sections[part]: + res += " Section: %s\n" % sec.name() + res += " %s" % self.get_mechs(sec) + "\n" + for m in self.get_mechs(sec): + gx = eval("sec()." + m + ".gbar") + res += " %s: %f\n" % (m, gx) + return res + + def save_all_mechs(self): + """ + get and save all of the initial mechanisms and their + maximal conductances when the cell is created. + We use this to get and check values later when the run + is actually done. + Note: some cell constructions may require that save_all_mechs + be done again after the initial "build". In this case, + setting the cell's initial_mechanisms property to None must + be done to allow a new configuration of mechanisms to be saved. + + """ + if self.initial_mechanisms is not None: + raise ValueError( + "Cells: Attempting to save initial mechanisms more than once" + ) + self.initial_mechanisms = {} + for part in self.all_sections.keys(): + self.initial_mechanisms[part] = {} + # print('Cell part: %s' % part ) + for sec in self.all_sections[part]: + # print(' Section: ', sec) + # print(' ', self.get_mechs(sec)) + self.initial_mechanisms[part][sec] = {} + for m in self.get_mechs(sec): + gx = eval("sec()." + m + ".gbar") + # print(' %s: %f' % (m, gx)) + self.initial_mechanisms[part][sec][m] = gx + + def check_all_mechs(self): + """ + Check that all mechanisms are the same as when we initially created the cell + """ + check = {} + for part in self.all_sections.keys(): + if part not in self.initial_mechanisms.keys(): + raise ValueError("Cell part %s was not in the original cell") + check[part] = {} + for sec in self.all_sections[part]: + # print(' Section: ', sec) + # print(' ', self.get_mechs(sec)) + if sec not in self.initial_mechanisms[part].keys(): + raise ValueError("Cell section was not in the original cell: ", sec) + check[part][sec] = sec + for m in self.get_mechs(sec): + gx = eval("sec()." + m + ".gbar") + # print(' %s: %f' % (m, gx)) + if m not in self.initial_mechanisms[part][sec].keys(): + raise ValueError( + "Mechanism %s was not in cell part %s, section = " + % (m, part), + sec, + ) + if self.initial_mechanisms[part][sec][m] != gx: + raise ValueError( + "Conductance for mechanism %s in cell part %s has changed (%f, %f), section = " + % (m, part, self.initial_mechanisms[part][sec][m], gx), + sec, + ) + return True + + def get_cellpars(self, dataset, species="guineapig", cell_type="II"): + raise NotImplementedError( + "get_cellpars should be reimplemented in the individual cell modules" + ) + + def channel_manager(self, modelName=None, modelType=None): + """ + This routine defines channel density maps and distance map patterns + for each type of compartment in the cell. The maps + are used by the ChannelDecorator class (specifically, its private + \_biophys function) to decorate the cell membrane. + These settings are only used if the decorator is called; otherwise + for point cells, the species_scaling routine defines the channel + densities. + + Parameters + ---------- + modelType : string (default: 'None' + A string that defines the type of the model. + These are determined in the tables in the data directory, for ionchannels.py + + Returns + ------- + Nothing + + Notes + ----- + This routine defines the following variables for the class: + + * conductances (gBar) + * a channelMap (dictonary of channel densities in defined anatomical compartments) + * a current injection range for IV's (used for testing) + * a distance map, which defines how each conductance in a selected compartment + changes with distance from the soma. The current implementation includes both + linear and exponential gradients, + the minimum conductance at the end of the gradient, and the space constant or + slope for the gradient. + + """ + + dataset = "%s_channels" % modelName + decorationmap = dataset + "_compartments" + # print('dataset: {0:s} decorationmap: {1:s}'.format(dataset, decorationmap)) + cellpars = self.get_cellpars( + dataset, species=self.status["species"], modelType=modelType + ) + refarea = 1e-3 * cellpars.cap / self.c_m + # ? print ('cellpars: ' ) + cellpars.show() + # print(' species: ', self.status['species']) + # print('m# odelType: ', modelType) + # print('dataset: ', dataset) + table = data._db.get_table_info(dataset) + # table = data.get_table_info('mGBC_channels') + # print(dir(data.ionchannels)) + # print( data.print_table('mGBC_channels')) + + if len(table.keys()) == 0: + raise ValueError("data table %s lacks keys - does it exist?" % dataset) + chscale = data._db.get_table_info(decorationmap) + pars = {} + # retrive the conductances from the data set + # print ('table keys: ', table.keys()) + # print('table: ', table) + # print('chscale: ', chscale) + for g in table["field"]: + x = data._db.get( + dataset, species=self.status["species"], model_type=modelType, field=g + ) + if not isinstance(x, float): + continue + if "_gbar" in g: + pars[g] = x / refarea + else: + pars[g] = x + + self.channelMap = OrderedDict() + for c in chscale["compartment"]: + self.channelMap[c] = {} + for g in pars.keys(): + if g not in chscale["parameter"]: + # print ('Parameter %s not found in chscale parameters!' % g) + continue + scale = data._db.get( + decorationmap, + species=self.status["species"], + model_type=modelType, + compartment=c, + parameter=g, + ) + if "_gbar" in g: + self.channelMap[c][g] = pars[g] * scale + else: + self.channelMap[c][g] = pars[g] + + self.irange = np.linspace(-0.6, 1, 9) + self.distMap = { + "dend": { + "klt": {"gradient": "exp", "gminf": 0.0, "lambda": 50.0}, + "kht": {"gradient": "exp", "gminf": 0.0, "lambda": 50.0}, + "nav11": {"gradient": "exp", "gminf": 0.0, "lambda": 50.0}, + }, # linear with distance, gminf (factor) is multiplied by gbar + "dendrite": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nav11": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + }, # linear with distance, gminf (factor) is multiplied by gbar + "apic": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nav11": {"gradient": "exp", "gminf": 0.0, "lambda": 200.0}, + }, # gradients are: flat, linear, exponential + } + self.check_temperature() + return + + def i_currents(self, V): + """ + For the steady-state case, return the total current at voltage V + Used to find the zero current point + vrange brackets the interval + Implemented here are the basic known mechanisms. If you add or need + more mechanisms, they either need to be accomadated in this routine, + or this routine needs to be implemented (overridden) in the + specific cell class. + + """ + for part in self.all_sections.keys(): + for sec in self.all_sections[part]: + sec.v = V + h.celsius = self.status["temperature"] + h.t = 0.0 + h.finitialize(V) + h.fcurrent() + self.ix = {} + + if "na" in self.mechanisms: + # print dir(self.soma().na) + try: + self.ix["na"] = self.soma().na.gna * (V - self.soma().ena) + except: + self.ix["na"] = self.soma().nav11.gna * (V - self.soma().ena) + if "jsrna" in self.mechanisms: + self.ix["jsrna"] = self.soma().jsrna.gna * (V - self.soma().ena) + if "nav11" in self.mechanisms: + self.ix["nav11"] = self.soma().nav11.gna * (V - self.soma().ena) + if "nacn" in self.mechanisms: + self.ix["nacn"] = self.soma().nacn.gna * (V - self.soma().ena) + if "napyr" in self.mechanisms: + self.ix["napyr"] = self.soma().napyr.gna * (V - self.soma().ena) + if "nap" in self.mechanisms: + self.ix["nap"] = self.soma().nap.gna * (V - self.soma().ena) + if "nacncoop" in self.mechanisms: + self.ix["nacncoop"] = self.soma().nacncoop.gna * (V - self.soma().ena) + + if "klt" in self.mechanisms: + self.ix["klt"] = self.soma().klt.gklt * (V - self.soma().ek) + if "kht" in self.mechanisms: + self.ix["kht"] = self.soma().kht.gkht * (V - self.soma().ek) + if "ka" in self.mechanisms: + self.ix["ka"] = self.soma().ka.gka * (V - self.soma().ek) + if "kdpyr" in self.mechanisms: + self.ix["kdpyr"] = self.soma().kdpyr.gk * (V - self.soma().ek) + if "kcnq" in self.mechanisms: + self.ix["kcnq"] = self.soma().kdcnq.gk * (V - self.soma().ek) + if "kis" in self.mechanisms: + self.ix["kis"] = self.soma().kis.gk * (V - self.soma().ek) + if "kif" in self.mechanisms: + self.ix["kif"] = self.soma().kif.gk * (V - self.soma().ek) + + if "ihvcn" in self.mechanisms: + self.ix["ihvcn"] = self.soma().ihvcn.gh * (V - self.soma().ihvcn.eh) + if "ihpyr" in self.mechanisms: + self.ix["ihpyr"] = self.soma().ihpyr.gh * (V - self.soma().ihpyr.eh) + if "ihpyr_adj" in self.mechanisms: + self.ix["ihpyr_adj"] = self.soma().ihpyr_adj.gh * ( + V - self.soma().ihpyr_adj.eh + ) + if "hcno" in self.mechanisms: + raise ValueError("HCNO is not supported - use hcnobo instead") + # self.ix['hcno'] = self.soma().hcno.gh*(V - self.soma().hcno.eh) + if "hcnobo" in self.mechanisms: + self.ix["hcnobo"] = self.soma().hcnobo.gh * (V - self.soma().hcnobo.eh) + + if "leak" in self.mechanisms: + self.ix["leak"] = self.soma().leak.gbar * (V - self.soma().leak.erev) + # print self.status['name'], self.status['type'], V, self.ix + isum = np.sum([self.ix[i] for i in self.ix]) + # print 'conductances: ', self.ix.keys() + # print 'V, isum, values: ', V, isum, [self.ix[i] for i in self.ix] + return isum + + def find_i0(self, vrange=None, showinfo=False): + """ + find the root of the system of equations in vrange. + Finds RMP fairly accurately as zero current level for current conductances. + + Parameters + ---------- + vrange : list of 2 floats (default: [-70, -55]) + The voltage range over which the root search will be performed. + + showinfo : boolean (default: False) + a flag to print out which roots were found and which mechanisms were in the cell + + Returns + ------- + The voltage at which I = 0 in the vrange specified + """ + if vrange is None: + vrange = self.vrange + # print( vrange) + # print (self.i_currents(V=vrange[0]), self.i_currents(V=vrange[1])) + # v0 = scipy.optimize.brentq(self.i_currents, vrange[0], vrange[1], maxiter=10000) + # print( 'v0: ', v0) + try: + v0 = scipy.optimize.brentq( + self.i_currents, vrange[0], vrange[1], maxiter=10000 + ) + except: + print("find i0 failed:") + print(self.ix) + i0 = self.i_currents(V=vrange[0]) + i1 = self.i_currents(V=vrange[1]) + ivi = [] + ivv = [] + for v in np.arange(vrange[0], vrange[1], 0.5): + ivi.append(self.i_currents(V=v)) + ivv.append(v) + print("iv: ") + for i in range(len(ivi)): + print("%6.1f %9.4f" % (ivv[i], ivi[i])) + print( + "This means the voltage range for the search might be too large\nor too far away from the target" + ) + raise ValueError( + "vrange not good for %s : %f at %6.1f, %f at %6.1f, temp=%6.1f" + % (self.status["name"], i0, vrange[0], i1, vrange[1], h.celsius) + ) + # check to be sure all the currents that are needed are calculated + # can't do this until i_currents has populated self.ix, so do it now... + for m in self.mechanisms: + if m not in self.ix.keys(): + raise ValueError( + "Mechanism %s in cell is missing from i_currents calculation", m + ) + + if showinfo: + print( + "\n [soma] find_i0 Species: %s cell type: %s Temp %6.1f" + % (self.status["species"], self.status["modelType"], h.celsius) + ) + print(" *** found V0 = %f" % v0) + print(" *** and cell has mechanisms: ", self.mechanisms) + return v0 + + def compute_rmrintau(self, auto_initialize=True, vrange=None): + """ + Run the model for 2 msec after initialization - then + compute the inverse of the sum of the conductances to get Rin at rest + compute Cm*Rin to get tau at rest + + Parameters + ---------- + auto_initialize : boolean (default: True) + If true, forces initialization of cell in NEURON befor the computation. + + Returns + ------- + A dictionary containing: Rin (Mohm), tau (ms) and Vm (mV) + + """ + gnames = { # R&M 03 and related: + "nacn": "gna", + "na": "gna", + "jsrna": "gna", + "nav11": "gna", + "nacncoop": "gna", + "leak": "gbar", + "klt": "gklt", + "kht": "gkht", + "ka": "gka", + "ihvcn": "gh", + "hcno": "gh", + "hcnobo": "gh", + # pyramidal cell specific: + "napyr": "gna", + "nap": "gnap", + "kdpyr": "gk", + "kif": "gkif", + "kis": "gkis", + "ihpyr": "gh", + "ihpyr_adj": "gh", + "kcnq": "gk", + # cartwheel cell specific: + "bkpkj": "gbkpkj", + "hpkj": "gh", + "kpkj": "gk", + "kpkj2": "gk", + "kpkjslow": "gk", + "kpksk": "gk", + "lkpkj": "gbar", + "naRsg": "gna", + # SGC Ih specific: + "ihsgcApical": "gh", + "ihsgcBasalMiddle": "gh", + } + if auto_initialize: + self.cell_initialize(vrange=vrange) + custom_init() + self.computeAreas() + gsum = 0.0 + soma_sections = self.all_sections["soma"] + # 1e-8*np.pi*soma.diam*soma.L + somaarea = np.sum([1e-8 * np.pi * s.L * s.diam for s in soma_sections]) + for sec in soma_sections: + u = self.get_mechs(sec) + for m in u: + # gx = 'section().'+m+'.'+gnames[m] + gm = "%s_%s" % (gnames[m], m) + gsum += getattr(sec(), gm) + # eval(gx) + # print('{0:>12s} : gx '.format(m)) + # convert gsum from us/cm2 to nS using cell area + # print ('gsum, self.somaarea: ', gsum, self.somaarea) + gs = mho2ns(gsum, self.somaarea) + Rin = 1e3 / gs # convert to megohms + tau = Rin * self.totcap * 1e-3 # convert to msec + return {"Rin": Rin, "tau": tau, "v": self.soma(0.5).v} + + def set_soma_size_from_Cm(self, cap): + """ + Use soma capacitance to set the cell size. Area of the open cylinder is same as a sphere of + the same diameter. + Compute area and save total capacitance as well + """ + self.totcap = cap + self.somaarea = self.totcap * 1e-6 / self.c_m # pf -> uF, cm = 1uf/cm^2 nominal + lstd = 1e4 * ((self.somaarea / np.pi) ** 0.5) # convert from cm to um + self.soma.diam = lstd + self.soma.L = lstd + + def set_soma_size_from_Diam(self, diam): + """ + Use diameter to set the cell size. Area of the open cylinder is same as a sphere of + the same diameter. + Compute area and total capacitance as well + """ + self.somaarea = 1e-8 * 4.0 * np.pi * (diam / 2.0) ** 2 # in microns^2 + self.totcap = self.c_m * self.somaarea * 1e6 + # lstd = diam # 1E4 * ((self.somaarea / np.pi) ** 0.5) # convert from cm to um + self.soma.diam = diam + self.soma.L = diam + + def set_soma_size_from_Section(self, soma): + self.soma.diam = soma.diam + self.soma.L = soma.L + self.somaarea = 1e-8 * np.pi * soma.diam * soma.L + self.totcap = self.c_m * self.somaarea * 1e6 + + def print_soma_info(self): + print("-" * 40) + print("Soma Parameters: ") + print(" Area: ", self.somaarea) + print(" Cap: ", self.totcap) + print(" L: ", self.soma.L) + print(" diam: ", self.soma.diam) + print(" cm: ", self.c_m) + print("-" * 40) + + def distances(self, section=None): + self.distanceMap = {} + if section is None: + self.hr.h("access %s" % self.soma.name()) # reference point + else: + self.hr.h("access %s" % section.name()) + d = self.hr.h.distance() + for sec in self.all_sections: + s = self.all_sections[sec] + if len(s) > 0: + for u in s: + self.hr.h("access %s" % u.name()) + self.distanceMap[u.name()] = ( + self.hr.h.distance(0.5) - d + ) # should be distance from first point + + def computeAreas(self): + self.areaMap = {} + for sec in self.all_sections: # keys for names of section types + s = self.all_sections[sec] # get all the sections of that type + sectype = self.get_section_type(s) + if len(s) > 0: + self.areaMap[sec] = {} + for u in s: + self.areaMap[sec][u] = np.pi * u.diam * u.L + else: + pass + # print(' No section of type %s in cell' % sec) + + def add_axon( + self, + c_m=1.0, + R_a=150, + axonsf=1.0, + nodes=5, + debug=False, + dia=None, + len=None, + seg=None, + ): + """ + Add an axon to the soma with an initial segment (tapered), and multiple nodes of Ranvier + The size of the axon is determined by self.axonsf, which in turn is set by the species + The somaarea is used to scale the density of ion channels in the initial segment + """ + nnodes = range(nodes) + axnode = [] + internode = [] + Section = h.Section + initsegment = Section(cell=self.soma) + initsegment.connect(self.soma) + for i in nnodes: + axnode.append(Section(cell=self.soma)) + internode.append(Section(cell=self.soma)) + axnode[0].connect(initsegment) + for i in nnodes: + internode[i].connect(axnode[i]) + if i < nnodes[-1]: + axnode[i + 1].connect(internode[i]) + + # create an initial segment + ninitseg = 21 + initsegment.nseg = ninitseg + initsegment.diam = 4.0 * axonsf + initsegment.L = 36.0 * axonsf + initsegment.cm = c_m # c_m + initsegment.Ra = R_a # R_a + initsegment.insert("nacn") # uses a standard Rothman sodium channel + initsegment.insert("kht") + initsegment.insert("klt") + initsegment.insert("ihvcn") + initsegment.insert("leak") + gnamax = nstomho(6000.0, self.somaarea) + gnamin = 0.0 * gnamax + + gnastep = (gnamax - gnamin) / ninitseg # taper sodium channel density + for ip, inseg in enumerate(initsegment): + gna = gnamin + ip * gnastep + if debug: + print("Initial segment %d: gnabar = %9.6f" % (ip, gna)) + inseg.nacn.gbar = gna + inseg.klt.gbar = 0.2 * nstomho(200.0, self.somaarea) + inseg.kht.gbar = nstomho(150.0, self.somaarea) + inseg.ihvcn.gbar = 0.0 * nstomho(20.0, self.somaarea) + inseg.leak.gbar = nstomho(2.0, self.somaarea) + inseg.ena = self.e_na + inseg.ek = self.e_k + inseg.leak.erev = self.e_leak + + for i in nnodes: + axnode[i] = self.loadaxnodes(axnode[i], self.somaarea, eleak=self.e_leak) + internode[i] = self.loadinternodes( + internode[i], self.somaarea, eleak=self.e_leak + ) + + if debug: + print("<< {:s} Axon Added >>".format(self.__class__.__name__)) + h.topology() + self.add_section(initsegment, "initialsegment") + self.add_section(axnode, "axonnode") + self.add_section(internode, "internode") + + @staticmethod + def loadaxnodes(axnode, somaarea, nodeLength=2.5, nodeDiameter=2.0, eleak=-65): + v_potassium = -80 # potassium reversal potential + v_sodium = 50 # sodium reversal potential + Ra = 150 + cm = 1.0 + axnode.nseg = 1 + axnode.L = nodeLength + axnode.diam = nodeDiameter + axnode.Ra = Ra + axnode.cm = cm + axnode.insert("nacn") + axnode.insert("kht") + axnode.insert("klt") + axnode.insert("leak") + axnode.insert("ihvcn") + for ax in axnode: + ax.nacn.gbar = nstomho(1000.0, somaarea) + ax.kht.gbar = nstomho(150.0, somaarea) + ax.klt.gbar = nstomho(200.0, somaarea) + ax.ihvcn.gbar = 0 + ax.leak.gbar = nstomho(2.0, somaarea) + ax.ena = v_sodium + ax.ek = v_potassium + ax.leak.erev = eleak + return axnode + + @staticmethod + def loadinternodes( + internode, somaarea, internodeLength=1000, internodeDiameter=10, eleak=-65 + ): + v_potassium = -80 # potassium reversal potential + v_sodium = 50 # sodium reversal potential + Ra = 150 + cm = 0.002 + + internode.nseg = 20 + internode.L = internodeLength + internode.diam = internodeDiameter + internode.Ra = Ra + internode.cm = cm + internode.insert("nacn") + internode.insert("kht") + internode.insert("leak") + for inno in internode: + inno.leak.gbar = nstomho(0.002, somaarea) + inno.nacn.gbar = 0 * nstomho(500.0, somaarea) + inno.kht.gbar = 0 * nstomho(150.0, somaarea) + inno.ek = v_potassium + inno.ena = v_sodium + inno.leak.erev = eleak + return internode diff --git a/cnmodel/cells/dstellate.py b/cnmodel/cells/dstellate.py new file mode 100644 index 0000000..fb65b3a --- /dev/null +++ b/cnmodel/cells/dstellate.py @@ -0,0 +1,897 @@ +from __future__ import print_function +from neuron import h +from ..util import nstomho +from .cell import Cell +from ..util import Params +from .. import synapses +from .. import data + +__all__ = ["DStellate", "DStellateRothman", "DStellateEager"] + + +class DStellate(Cell): + + type = "dstellate" + + @classmethod + def create(cls, model="RM03", **kwds): + if model == "RM03": + return DStellateRothman(**kwds) + elif model == "Eager": + return DStellateEager(**kwds) + elif model == "dummy": + return DummyDStellate(**kwds) + else: + raise ValueError("DStellate type %s is unknown", type) + + def __init__(self): + Cell.__init__(self) + self.spike_source = ( + None + ) # used by DummyDStellate to connect VecStim to terminal + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is designed to pass the unit test (loc=0.5) + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for bushy cell + + kwds: dictionary of options. + Two are currently handled: + postsize : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + + if psd_type == "simple": + if terminal.cell.type in ["sgc", "dstellate", "tuberculoventral"]: + weight = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="weight", + ) + tau1 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau1", + ) + tau2 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau2", + ) + erev = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="erev", + ) + return self.make_exp2_psd( + post_sec, + terminal, + weight=weight, + loc=loc, + tau1=tau1, + tau2=tau2, + erev=erev, + ) + else: + raise TypeError( + "Cannot make simple PSD for %s => %s" + % (terminal.cell.type, self.type) + ) + + elif psd_type == "multisite": + if terminal.cell.type == "sgc": + # Max conductances for the glu mechanisms are calibrated by + # running `synapses/tests/test_psd.py`. The test should fail + # if these values are incorrect + self.AMPAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="AMPAR_gmax", + ) + * 1e3 + ) + self.NMDAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_gmax", + ) + * 1e3 + ) + self.Pr = data.get( + "sgc_synapse", species=self.species, post_type=self.type, field="Pr" + ) + # adjust gmax to correct for initial Pr + self.AMPAR_gmax = self.AMPAR_gmax / self.Pr + self.NMDAR_gmax = self.NMDAR_gmax / self.Pr + # old values: + # AMPA_gmax = 0.22479596944138733*1e3 # factor of 1e3 scales to pS (.mod mechanisms) from nS. + # NMDA_gmax = 0.12281291946623739*1e3 + if "AMPAScale" in kwds: + self.AMPAR_gmax = ( + self.AMPAR_gmax * kwds["AMPAScale"] + ) # allow scaling of AMPA conductances + if "NMDAScale" in kwds: + self.NMDAR_gmax = self.NMDAR_gmax * kwds["NMDAScale"] + return self.make_glu_psd( + post_sec, terminal, self.AMPAR_gmax, self.NMDAR_gmax, loc=loc + ) + + elif terminal.cell.type == "dstellate": + # Get GLY kinetic constants from database + return self.make_gly_psd(post_sec, terminal, psdtype="glyfast", loc=loc) + elif terminal.cell.type == "tuberculoventral": + # Get GLY kinetic constants from database + return self.make_gly_psd(post_sec, terminal, psdtype="glyfast", loc=loc) + else: + raise TypeError( + "Cannot make PSD for %s => %s" % (terminal.cell.type, self.type) + ) + else: + raise ValueError("Unsupported psd type %s" % psd_type) + + def make_terminal(self, post_cell, term_type, **kwds): + if term_type == "simple": + return synapses.SimpleTerminal( + self.soma, post_cell, spike_source=self.spike_source, **kwds + ) + elif term_type == "multisite": + if post_cell.type in [ + "dstellate", + "tuberculoventral", + "pyramidal", + "bushy", + "tstellate", + ]: + nzones = data.get( + "dstellate_synapse", + species=self.species, + post_type=post_cell.type, + field="n_rsites", + ) + delay = data.get( + "dstellate_synapse", + species=self.species, + post_type=post_cell.type, + field="delay", + ) + else: + raise NotImplementedError( + "No knowledge as to how to connect D stellate cell to cell type %s" + % type(post_cell) + ) + pre_sec = self.soma + return synapses.StochasticTerminal( + pre_sec, + post_cell, + nzones=nzones, + spike_source=self.spike_source, + delay=delay, + **kwds + ) + else: + raise ValueError("Unsupported terminal type %s" % term_type) + + +class DStellateRothman(DStellate): + """ + VCN D-stellate model: + as a type I-II from Rothman and Manis, 2003 + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + species="guineapig", + modelType=None, + modelName=None, + debug=False, + ): + """ + initialize a radial stellate (D-stellate) cell, using the default parameters for guinea pig from + R&M2003, as a type I-II cell. + Modifications to the cell can be made by calling methods below. These include: + + * changing the sodium channel + * Changing "species" to mouse or cat (scales conductances) + * Shifting model type + + Parameters + ---------- + morphology : string (default: None) + Name of a .hoc file representing the morphology. This file is used to constructe + an electrotonic (cable) model. + If None (default), then a "point" (really, single cylinder) model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels is inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: None) + nach selects the type of sodium channel that will be used in the model. A channel mechanism + by that name must exist. A value of None will set the channel to a default for the model (nacn). + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + This flag duplicates the effects of tetrodotoxin in the model. Currently, the flag is not implemented. + + species: string (default 'guineapig') + species defines the pattern of ion channel densities that will be inserted, according to + prior measurements in various species. Note that + if a decorator function is specified, this argument is ignored as the decorator will + specify the channel density. + + modelType: string (default: None) + modelType specifies the subtype of the cell model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point (single cylinder) models. + + debug: boolean (default: False) + When True, there will be multiple printouts of progress and parameters. + + + Returns + ------- + Nothing + """ + + super(DStellateRothman, self).__init__() + if modelType == None: + modelType = "I-II" + if species == "guineapig": + modelName = "RM03" + temp = 22.0 + if nach == None: + nach = "nacn" + if species == "mouse": + temp = 34.0 + if modelName is None: + modelName = "XM13" + if nach is None: + nach = "na" + self.debug = debug + # if modelType == None: # allow us to pass None to get the default + # modelType = 'I-II' + # if nach == None: + # nach = 'na' + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "modelName": modelName, + "ttx": ttx, + "name": "DStellate", + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + self.i_test_range = { + "pulse": [(-0.3, 0.3, 0.03), (-0.05, 0.0, 0.005)] + } # set range for ic command test + self.vrange = [-75.0, -55.0] + self.spike_threshold = ( + -40.0 + ) # matches threshold in released CNModel (set in base cell class) + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + soma = h.Section( + name="DStellate_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.mechanisms = ["klt", "kht", "ihvcn", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ena = self.e_na + self.soma.ek = self.e_k + self.soma().leak.erev = self.e_leak + self.c_m = 0.9 + self.set_soma_size_from_Cm(12.0) + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + + if self.debug: + print("<< D-stellate: JSR Stellate Type I-II cell model created >>") + + def get_cellpars(self, dataset, species="guineapig", modelType="I-II"): + cellcap = data.get( + dataset, species=species, model_type=modelType, field="soma_Cap" + ) + chtype = data.get( + dataset, species=species, model_type=modelType, field="na_type" + ) + pars = Params(cap=cellcap, natype=chtype) + # pars.show() + + if self.status["modelName"] == "RM03": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "klt_gbar", + "ka_gbar", + "ih_gbar", + "leak_gbar", + "leak_erev", + "ih_eh", + "e_k", + "e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + if self.status["modelName"] == "XM13": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "klt_gbar", + "ka_gbar", + "ihvcn_gbar", + "leak_gbar", + "leak_erev", + "ih_eh", + "e_k", + "e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + if self.status["modelName"] == "mGBC": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "klt_gbar", + "ka_gbar", + "ihvcn_gbar", + "leak_gbar", + "leak_erev", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + return pars + + def species_scaling(self, species="guineapig", modelType="I-II", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + + Parameters + ---------- + species : string (default: 'guineapig') + A string specifying the species used for scaling. Recognized values are + 'mouse', 'guineapig', and 'cat' (cat is just a larger version of the guineapig) + + modelType : string (default: 'I-II') + A string specifying the version of the model to use. + Current choices are 'I-II' (others need to be implemented) + + silent : boolean (default: True) + Flag for printing debugging information. + + """ + soma = self.soma + celltype = modelType + if species == "mouse": + # use conductance levels from Cao et al., J. Neurophys., 2007. + dataset = "XM13_channels" + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + self.c_m = 0.9 + pars = self.get_cellpars(dataset, species=species, modelType=modelType) + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + # pars.show() + self.adjust_na_chans(soma, gbar=pars.na_gbar, sf=1.0) + soma().kht.gbar = nstomho(pars.kht_gbar, self.somaarea) + soma().klt.gbar = nstomho(pars.klt_gbar, self.somaarea) + # soma().ka.gbar = nstomho(pars.ka_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.ihvcn_gbar, self.somaarea) + soma().ihvcn.eh = pars.ih_eh # Rodrigues and Oertel, 2006 + soma().leak.gbar = nstomho(pars.leak_gbar, self.somaarea) + soma().leak.erev = pars.leak_erev + self.e_k = pars.e_k + self.e_na = pars.e_na + soma.ena = self.e_na + soma.ek = self.e_k + # soma().leak.erev = pars.leak_erev + self.axonsf = 0.5 + + elif species == "guineapig": # values from R&M 2003, Type II-I + dataset = "RM03_channels" + self.c_m = 0.9 + self._valid_temperatures = (22.0, 38.0) + if self.status["temperature"] is None: + self.set_temperature(22.0) + sf = 1.0 + if ( + self.status["temperature"] == 38.0 + ): # adjust for 2003 model conductance levels at 38 + sf = 3.03 # Q10 of 2, 22->38C. (p3106, R&M2003c) + self.i_test_range = {"pulse": (-0.3, 0.3, 0.03)} + self.vrange = [-75.0, -55.0] + pars = self.get_cellpars(dataset, species=species, modelType=modelType) + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + # pars.show() + self.adjust_na_chans(soma, gbar=pars.nacn_gbar, sf=sf) + soma().kht.gbar = nstomho(pars.kht_gbar, self.somaarea) + soma().klt.gbar = nstomho(pars.klt_gbar, self.somaarea) + # soma().ka.gbar = nstomho(pars.ka_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.ih_gbar, self.somaarea) + soma().leak.gbar = nstomho(pars.leak_gbar, self.somaarea) + soma().leak.erev = pars.leak_erev + self.axonsf = 0.5 + + else: + raise ValueError( + "Species %s or species-modelType %s is not recognized for D-Stellate cells" + % (species, modelType) + ) + self.status["species"] = species + self.status["modelType"] = modelType + self.check_temperature() + # self.cell_initialize(showinfo=False) + if not silent: + print("set cell as: ", species) + print(" with Vm rest = %6.3f" % self.vm0) + + def adjust_na_chans(self, soma, sf=1.0, gbar=1000.0): + """ + adjust the sodium channel conductance + + Parameters + ---------- + soma : soma object (no default) + soma object whose sodium channel complement will have it's + conductances adjusted depending on the channel type + + gbar : float (default: 1000.) + The conductance to be set for the sodium channel + + debug : boolean (default: False) + Flag for printing the conductance value and Na channel model + + Returns + ------- + Nothing + """ + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) * sf + nach = self.status["na"] + if nach == "jsrna": + soma().jsrna.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("jsrna gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar * 0.5 + soma.ena = self.e_na + soma().nav11.vsna = 4.3 + if self.debug: + print("bushy using inva11") + print("nav11 gbar: ", soma().nav11.gbar) + elif nach == "na": + soma().na.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("na gbar: ", soma().na.gbar) + elif nach == "nacn": + soma().nacn.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("nacn gbar: ", soma().nacn.gbar) + else: + raise ValueError( + "Dstellate setting Na channels: channel %s not known" % nach + ) + + def add_axon(self): + """ + Add a default axon from the generic cell class to the bushy cell (see cell class). + """ + Cell.add_axon(self, self.soma, self.somaarea, self.c_m, self.R_a, self.axonsf) + + def add_dendrites(self): + """ + Add simple unbranched dendrites to basic Rothman Type I-II model. + The dendrites have some kht and ih current + """ + cs = False # not implemented outside here - internal Cesium. + nDend = range(4) # these will be simple, unbranced, N=4 dendrites + dendrites = [] + for i in nDend: + dendrites.append(h.Section(cell=self.soma)) + for i in nDend: + dendrites[i].connect(self.soma) + dendrites[i].L = 300 # length of the dendrite (not tapered) + dendrites[i].diam = 1.25 # dendrite diameter + dendrites[i].nseg = 21 # # segments in dendrites + dendrites[i].Ra = 150 # ohm.cm + dendrites[i].insert("kht") + if cs is False: + dendrites[i]().kht.gbar = 0.005 # a little Ht + else: + dendrites[i]().kht.gbar = 0.0 + dendrites[i].insert("leak") # leak + dendrites[i]().leak.gbar = 0.0001 + dendrites[i].insert("ihvcn") # some H current + dendrites[i]().ihvcn.gbar = 0.0 # 0.001 + dendrites[i]().ihvcn.eh = -43.0 + self.maindend = dendrites + self.status["dendrites"] = True + self.add_section(self.maindend, "maindend") + + +class DummyDStellate(DStellate): + """ DStellate class with no cell body; this cell only replays a predetermined + spike train. Useful for testing, or replacing spike trains to determine + the importance of spike structures within a network. + """ + + def __init__(self, cf=None, species="mouse"): + """ + Parameters + ---------- + cf : float (default: None) + Required: the characteristic frequency for the DStellate + Really just for reference. + + """ + + DStellate.__init__(self) + self.vecstim = h.VecStim() + + # this causes the terminal to receive events from the VecStim: + self.spike_source = self.vecstim + + # just an empty section for holding the terminal + self.add_section(h.Section(), "soma") + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": None, + "species": species, + "modelType": "Dummy", + "modelName": "DummyDStellate", + "ttx": None, + "name": "DummyDStellate", + "morphology": None, + "decorator": None, + "temperature": None, + } + print("<< DStellate: Dummy DStellate Cell created >>") + + def set_spiketrain(self, times): + """ Set the times of spikes (in seconds) to be replayed by the cell. + """ + self._spiketrain = times + self._stvec = h.Vector(times) + self.vecstim.play(self._stvec) + + +class DStellateEager(DStellate): + """ + This is a model of the VCN D-Stellate cells as proposed by + Eager, M.A., Grayden, D.B., Burkitt, A.N., and Meffin, H., + "A neural circuit model of the ventral cochlear nucleus", + Internet: + http://citeseerx.ist.pus.edu/viewdoc/download?doi=10.1.79.9620.pdf&rep + =rep&type=pdf + also cited as: + Proceedings of the 10th Australian International Conference on + Speech Science and Technology, pp. 539-544, 2004. + It is based on the Rothman and Manis (2003c) model, + with small modifications. + Their model includes dendrites and an axon, which are added in this version + """ + + def __init__( + self, nach="na", ttx=False, species="guineapig", modelType="I-II", debug=False + ): + """ + Initialize the VCN D-stellate model of Eager et al. Some model parameters may be modified. + + Parameters + ---------- + nach : string (default: 'na') + Set the sodium channel model. Choices are 'na', 'nav11', 'jsrna' + + ttx : boolean (default: False) + ttx sets the sodium channel conductance to 0 + + species : string (default: 'guineapig') + species to use for conductance scaling + + modelType : string (default: 'I-II') + RM03 model type to use for conductances. + + debug : boolean (default: False) + Flag to use to enable print statements for debugging purposes. + + """ + super(DStellateEager, self).__init__() + + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "DStellateEager", + } + self.i_test_range = (-0.25, 0.25, 0.025) # set range for ic command test + + soma = h.Section(name="DStellateEager_Soma_%x" % id(self)) # one compartment + + soma.nseg = 1 + + if nach in ["nacn", "na"]: + soma.insert("na") + elif nach == "nav11": + soma.insert("nav11") + elif nach == "jsrna": + soma.insert("jsrna") + else: + raise ValueError("Sodium channel %s in type 1 cell not known" % nach) + self.debug = debug + soma.insert("kht") + soma.insert("klt") + soma.insert("ihvcn") + soma.insert("leak") + soma.ek = self.e_k + soma().leak.erev = self.e_leak + self.mechanisms = ["kht", "klt", "ihvcn", "leak", nach] + self.add_section(soma, "soma") + self.species_scaling( + silent=False, species=species, modelType=modelType + ) # set the default type II cell parameters + self.add_axon() # must follow species scaling so that area parameters are available + self.add_dendrites() # similar for dendrites + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(soma) + + if self.debug: + print("<< D-stellateEager: Eager DStellate Type I-II cell model created >>") + + def species_scaling(self, species="guineapig", modelType="I-II", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + + Parameters + ---------- + species : string (default: 'guineapig') + A string specifying the species used for scaling. Recognized values are + 'mouse', 'guineapig', and 'cat' (cat is just a larger version of the guineapig) + + modelType : string (default: 'I-II') + A string specifying the version of the model to use. + Current choices are 'I-II' (others need to be implemented) + + silent : boolean (default: True) + Flag for printing debugging information. + + """ + soma = self.soma + if species == "mouse" and modelType == "I-II": + # use conductance levels from Cao et al., J. Neurophys., 2007. + self.set_soma_size_from_Cm(25.0) + self.adjust_na_chans(soma, gbar=800.0) + soma().kht.gbar = nstomho(150.0, self.somaarea) + soma().klt.gbar = nstomho(20.0, self.somaarea) + soma().ihvcn.gbar = nstomho(2.0, self.somaarea) + soma().ihvcn.eh = -43 # Rodrigues and Oertel, 2006 + soma().leak.gbar = nstomho(2.0, self.somaarea) + self.axonsf = 0.5 + elif ( + species == "guineapig" and modelType == "I-II" + ): # values from R&M 2003, Type II-I + self.set_soma_size_from_Diam(25.0) + self.adjust_na_chans(soma, gbar=1000.0 * 0.75) + soma().kht.gbar = 0.02 # nstomho(150.0, self.somaarea) + soma().klt.gbar = 0.005 # nstomho(20.0, self.somaarea) + soma().ihvcn.gbar = 0.0002 # nstomho(2.0, self.somaarea) + soma().leak.gbar = 0.0005 # nstomho(2.0, self.somaarea) + self.axonsf = 1.0 + elif ( + species == "cat" and modelType == "I=II" + ): # a cat is a big guinea pig Type I + self.set_soma_size_from_Cm(35.0) + self.adjust_na_chans(soma) + soma().kht.gbar = nstomho(150.0, self.somaarea) + soma().klt.gbar = nstomho(20.0, self.somaarea) + soma().ihvcn.gbar = nstomho(2.0, self.somaarea) + soma().leak.gbar = nstomho(2.0, self.somaarea) + self.axonsf = 1.0 + else: + raise ValueError( + "Species %s or species-type %s is not recognized for D-StellateEager cells" + % (species, type) + ) + self.status["species"] = species + self.status["type"] = modelType + self.cell_initialize(showinfo=True) + if not silent: + print(" set cell as: ", species) + print(" with Vm rest = %6.3f" % self.vm0) + + def adjust_na_chans(self, soma, gbar=1000.0): + """ + adjust the sodium channel conductance + + Parameters + ---------- + soma : soma object (no default) + soma object whose sodium channel complement will have it's + conductances adjusted depending on the channel type + + gbar : float (default: 1000.) + The conductance to be set for the sodium channel + + Returns + ------- + Nothing + """ + + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) + nach = self.status["na"] + if nach == "jsrna": + soma().jsrna.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("using jsrna with gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar * 0.5 + soma.ena = self.e_na + soma().nav11.vsna = 4.3 + if self.debug: + print("using inva11 with gbar:", soma().na.gbar) + print("nav11 gbar: ", soma().nav11.gbar) + elif nach == "na": + soma().na.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("using na with gbar: ", soma().na.gbar) + elif nach == "nach": + soma().nach.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print(("uwing nacn with gbar: ", soma().nacn.gbar)) + else: + raise ValueError( + "DstellateEager setting Na channels: channel %s not known" % nach + ) + # print soma().na.gbar + + def add_axon(self): + """ + Adds an axon to the Eager. et al model + Cell.add_axon(self, nodes=1, c_m=self.c_m, R_a=self.R_a, axonsf=self.axonsf, dia=3.0, len=70, seg=2) + The Eager et al model just uses one cable, 70 microns long and 3 microns in dameter. + + Parameters + ---------- + None + + Returns + ------- + Nothing + """ + + naxons = 1 + axon = [] + for i in range(naxons): + axon.append(h.Section(cell=self.soma)) + for i in range(naxons): + axon[i].connect(self.soma) + axon[i].L = 70 + axon[i].diam = 3.0 + axon[i].Ra = 500 + axon[i].cm = 0.9 + axon[i].nseg = 2 + axon[i].insert("kht") + axon[i].insert("klt") + axon[i].insert("ihvcn") + axon[i].insert("leak") + axon[i].insert("na") + axon[i].ek = self.e_k + axon[i].ena = self.e_na + axon[i]().leak.erev = self.e_leak + axon[i]().na.gbar = 0.5 + axon[i]().klt.gbar = 0.005 + axon[i]().kht.gbar = 0.02 + axon[i]().ihvcn.gbar = 0.0002 + axon[i]().leak.gbar = 0.0005 + self.status["axon"] = True + self.add_section(axon, "axon") + + def add_dendrites(self): + """ + Adds dendrites to the Eager model. The Eager model uses simple passive dendrites. + + Parameters + ---------- + None + + Returns + ------- + Nothing + """ + + nDend = range(2) # these will be simple, unbranced, N=4 dendrites + dendrites = [] + for i in nDend: + dendrites.append(h.Section(cell=self.soma)) + for i in nDend: + dendrites[i].connect(self.soma) + dendrites[i].L = 1100 # length of the dendrite (not tapered) + dendrites[i].diam = 3.5 # dendrite diameter + dendrites[i].nseg = 5 # # segments in dendrites + dendrites[i].Ra = 1500 # ohm.cm + dendrites[i].insert("leak") # leak + dendrites[i]().leak.gbar = 0.00025 + dendrites[i]().leak.erev = self.e_leak + self.maindend = dendrites + self.status["dendrites"] = True + self.add_section(self.maindend, "maindend") diff --git a/cnmodel/cells/hh.py b/cnmodel/cells/hh.py new file mode 100644 index 0000000..3d12718 --- /dev/null +++ b/cnmodel/cells/hh.py @@ -0,0 +1,47 @@ +from __future__ import print_function +from neuron import h +import neuron as nrn +from ..util import nstomho + +from .cell import Cell + +__all__ = ["HH"] + + +class HH(Cell): + """ + Standard Hodgkin-Huxley mechanisms from NEURON + """ + + def __init__(self, debug=False, message=None): + super(HH, self).__init__() + + soma = h.Section( + name="HH_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + v_potassium = -80 # potassium reversal potential + v_sodium = 50 # sodium reversal potential + c_m = 1.0 + scalefactor = 1.0 # This determines the relative size of the cell + rinsf = 1.0 # input resistance adjustment (also current...) + totcap = 20.0 # scalefactor * 1.0 # cap in pF for cell + effcap = totcap # sometimes we change capacitance - that's effcap + somaarea = totcap * 1e-6 / c_m # pf -> uF, cm = 1uf/cm^2 nominal + lstd = 1e4 * ((somaarea / 3.14159) ** 0.5) # convert from cm to um + + soma.nseg = 1 + soma.diam = lstd + soma.L = lstd + + seg = soma + seg.insert("hh") + seg.insert("pas") + if debug: + if message is None: + print("<< Standard HH model created >>") + else: + print(message) + + self.add_section(soma, "soma") + + self.vm0 = -67.536 diff --git a/cnmodel/cells/msoprincipal.py b/cnmodel/cells/msoprincipal.py new file mode 100644 index 0000000..3e43685 --- /dev/null +++ b/cnmodel/cells/msoprincipal.py @@ -0,0 +1,516 @@ +from __future__ import print_function +from neuron import h + +from .cell import Cell + +# from .. import synapses +from ..util import nstomho +from ..util import Params +import numpy as np +from .. import data + +__all__ = ["MSO"] + + +class MSO(Cell): + + type = "mso" + + @classmethod + def create(cls, model="MSO-principal", **kwds): + if model == "MSO-principal": + return MSOPrincipal(**kwds) + else: + raise ValueError("MSO cell model %s is unknown", model) + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is designed to pass the unit test (loc=0.5) + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for MSO cell + + kwds: dictionary of options. + Two are currently handled: + postsite : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + + if psd_type == "simple": + return self.make_exp2_psd(post_sec, terminal, loc=loc) + elif psd_type == "multisite": + if terminal.cell.type == "bushy": + # Max conductances for the glu mechanisms are calibrated by + # running `synapses/tests/test_psd.py`. The test should fail + # if these values are incorrect + self.AMPAR_gmax = ( + data.get( + "bushy_synapse", + species=self.species, + post_type=self.type, + field="AMPAR_gmax", + ) + * 1e3 + ) + self.NMDAR_gmax = ( + data.get( + "bushy_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_gmax", + ) + * 1e3 + ) + self.Pr = data.get( + "bushy_synapse", + species=self.species, + post_type=self.type, + field="Pr", + ) + # adjust gmax to correct for initial Pr + self.AMPAR_gmax = self.AMPAR_gmax / self.Pr + self.NMDAR_gmax = self.NMDAR_gmax / self.Pr + if "AMPAScale" in kwds: # normally, this should not be done! + self.AMPAR_gmax = ( + self.AMPAR_gmax * kwds["AMPAScale"] + ) # allow scaling of AMPA conductances + if "NMDAScale" in kwds: + self.NMDAR_gmax = self.NMDAR_gmax * kwds["NMDAScale"] # and NMDA... + return self.make_glu_psd( + post_sec, terminal, self.AMPAR_gmax, self.NMDAR_gmax, loc=loc + ) + else: + raise TypeError( + "Cannot make PSD for %s => %s" % (terminal.cell.type, self.type) + ) + else: + raise ValueError("Unsupported psd type %s" % psd_type) + + +class MSOPrincipal(MSO): + """ + VCN MSO cell models. + Using Rothman and Manis, 2003abc (Type II) + MSO principal cell type + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + species="guineapig", + modelType=None, + debug=False, + temperature=None, + ): + """ + Create a MSO principal cell, using the default parameters for guinea pig from + R&M2003, as a type II cell. + Additional modifications to the cell can be made by calling methods below. + + Parameters + ---------- + morphology : string (default: None) + Name of a .hoc file representing the morphology. This file is used to constructe + an electrotonic (cable) model. + If None (default), then a "point" (really, single cylinder) model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels is inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: None) + nach selects the type of sodium channel that will be used in the model. A channel mechanism + by that name must exist. The default channel is set to 'nacn' (R&M03) + + temperature : float (default: 22) + temperature to run the cell at. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + This flag duplicates the effects of tetrodotoxin in the model. Currently, the flag is not implemented. + + temperature : float (default: None, sets to model default of 22) + temperature (deg C) to run the cell at. Must be a valid temperature for the model. + + species: string (default 'guineapig') + species defines the pattern of ion channel densities that will be inserted, according to + prior measurements in various species. Note that + if a decorator function is specified, this argument is ignored as the decorator will + specify the channel density. + + modelType: string (default: None) + modelType specifies the subtype of the cell model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point (single cylinder) models. + + debug: boolean (default: False) + When True, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + + """ + super(MSO, self).__init__() + self.i_test_range = { + "pulse": (-1, 1, 0.05) + } # note that this gets reset with decorator according to channels + # Changing the default values will cause the unit tests to fail! + if modelType == None: + modelType = "principal" + if nach == None and species == "guineapig": + nach = "na" + if nach == None and species == "mouse": + nach = "na" + self.i_test_range = {"pulse": (-1, 1.2, 0.05)} + + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "hillock": False, + "initialsegment": False, + "myelinatedaxon": False, + "unmyelinatedaxon": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "MSO", + "morphology": morphology, + "decorator": decorator, + "temperature": temperature, + } + + self.spike_threshold = -40 + self.vrange = [-70.0, -55.0] # set a default vrange for searching for rmp + print("model type, species: ", modelType, species, nach) + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + print("<< MSO model: Creating point principal cell >>") + soma = h.Section( + name="MSO_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + print( + "<< MSO principal cell model: Creating cell with morphology from %s >>" + % morphology + ) + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.mechanisms = ["klt", "kht", "ihvcn", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ena = self.e_na + self.soma.ek = self.e_k + self.soma().ihvcn.eh = self.e_h + self.soma().leak.erev = self.e_leak + self.c_m = 0.9 + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + + if debug: + print(" << Created cell >>") + + def get_cellpars(self, dataset, species="guineapig", celltype="principal"): + cellcap = data.get( + dataset, species=species, cell_type=celltype, field="soma_Cap" + ) + chtype = data.get( + dataset, species=species, cell_type=celltype, field="soma_na_type" + ) + pars = Params(cap=cellcap, natype=chtype) + for g in ["soma_kht_gbar", "soma_klt_gbar", "soma_ih_gbar", "soma_leak_gbar"]: + pars.additem( + g, data.get(dataset, species=species, cell_type=celltype, field=g) + ) + return pars + + def species_scaling(self, species="guineapig", modelType="principal", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + This scaling should be used ONLY for point models, as no other compartments + are scaled. + + This scaling routine also sets the temperature for the model to a default value. Some models + can be run at multiple temperatures, and so a default from one of the temperatures is used. + The calling cell.set_temperature(newtemp) will change the conductances and reinitialize + the cell to the new temperature settings. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be one of mouse, cat, guineapig + + modelType: string (default: 'principal') + definition of model type from RM03 models, principal cell for mso + + silent : boolean (default: True) + run silently (True) or verbosely (False) + + """ + # print '\nSpecies scaling: %s %s' % (species, type) + knownspecies = ["guineapig"] + + soma = self.soma + if modelType == "principal": + celltype = ( + "MSO-principal" + ) # There are other possiblities in the literature - this is just the main one + else: + raise ValueError("model type not recognized") + + if species == "guineapig": + print( + " Setting conductances for guinea pig %s MSO cell, based on Rothman and Manis, 2003 bushy cell" + % modelType + ) + self._valid_temperatures = (22.0, 38.0) + if self.status["temperature"] is None: + self.status["temperature"] = 22.0 + self.i_test_range = {"pulse": (-0.4, 0.4, 0.02)} + sf = 1.0 + if ( + self.status["temperature"] == 38.0 + ): # adjust for 2003 model conductance levels at 38 + sf = 2 # Q10 of 2, 22->38C. (p3106, R&M2003c) + # note that kinetics are scaled in the mod file. + dataset = "MSO_principal_channels" + pars = self.get_cellpars(dataset, species=species, celltype=celltype) + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + self.adjust_na_chans(soma, sf=sf) + soma().kht.gbar = nstomho(pars.soma_kht_gbar, self.somaarea) + soma().klt.gbar = nstomho(pars.soma_klt_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.soma_ih_gbar, self.somaarea) + soma().leak.gbar = nstomho(pars.soma_leak_gbar, self.somaarea) + + self.axonsf = 0.57 + + else: + errmsg = ( + 'Species "%s" or model type "%s" is not recognized for MSO cells.' + % (species, modelType) + ) + errmsg += "\n Valid species are: \n" + for s in knownspecies: + errmsg += " %s\n" % s + errmsg += "-" * 40 + raise ValueError(errmsg) + + self.status["species"] = species + self.status["modelType"] = modelType + self.check_temperature() + # self.cell_initialize(vrange=self.vrange) # no need to do this just yet. + if not silent: + print(" set cell as: ", species) + print(" with Vm rest = %6.3f" % self.vm0) + + def channel_manager(self, modelType="MSO-principal"): + """ + This routine defines channel density maps and distance map patterns + for each type of compartment in the cell. The maps + are used by the ChannelDecorator class (specifically, its private + \_biophys function) to decorate the cell membrane. + These settings are only used if the decorator is called; otherwise + for point cells, the species_scaling routine defines the channel + densities. + + Parameters + ---------- + modelType : string (default: 'RM03') + A string that defines the type of the model. Currently, only 1 type is implemented: + RM03: Rothman and Manis, 2003 somatic densities based on guinea pig bushy cell + + Returns + ------- + Nothing + + Notes + ----- + This routine defines the following variables for the class: + + * conductances (gBar) + * a channelMap (dictonary of channel densities in defined anatomical compartments) + * a current injection range for IV's (used for testing) + * a distance map, which defines how each conductance in a selected compartment + changes with distance from the soma. The current implementation includes both + linear and exponential gradients, + the minimum conductance at the end of the gradient, and the space constant or + slope for the gradient. + + """ + + self.c_m = 1e-6 # default in units of F/cm^2 + if modelType == "MSO-principal": + # + # Create a model based on the Rothman and Manis 2003 conductance set from guinea pig + # + self.c_m = 0.9e-6 # default in units of F/cm^2 + totcap = 12.0e-12 # in units of F, from Rothman and Manis, 2003. + refarea = totcap / self.c_m # area is in cm^2 + # MSO Rothman-Manis, guinea pig type II + # model gave cell conductance in nS, but we want S/cm^2 for NEURON + # so conversion is 1e-9*nS = uS, and refarea is already in cm2 + self._valid_temperatures = (22.0, 38.0) + sf = 1.0 + if self.status["temperature"] == None: + self.status["temperature"] = 22.0 + if self.status["temperature"] == 38: + sf = 3.03 + self.gBar = Params( + nabar=sf * 1000.0e-9 / refarea, + khtbar=sf * 150.0e-9 / refarea, + kltbar=sf * 200.0e-9 / refarea, + ihbar=sf * 20.0e-9 / refarea, + leakbar=sf * 2.0e-9 / refarea, + ) + print("MSO principal channels gbar:\n", self.gBar.show()) + + self.channelMap = { + "axon": { + "nacn": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar / 2.0, + }, + "hillock": { + "nacn": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar, + }, + "initseg": { + "nacn": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": self.gBar.ihbar / 2.0, + "leak": self.gBar.leakbar, + }, + "soma": { + "nacn": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": self.gBar.ihbar, + "leak": self.gBar.leakbar, + }, + "dend": { + "nacn": self.gBar.nabar, + "klt": self.gBar.kltbar * 0.5, + "kht": self.gBar.khtbar * 0.5, + "ihvcn": self.gBar.ihbar / 3.0, + "leak": self.gBar.leakbar * 0.5, + }, + "apic": { + "nacn": self.gBar.nabar, + "klt": self.gBar.kltbar * 0.2, + "kht": self.gBar.khtbar * 0.2, + "ihvcn": self.gBar.ihbar / 4.0, + "leak": self.gBar.leakbar * 0.2, + }, + } + # self.irange = np.linspace(-1., 1., 21) + self.distMap = { + "dend": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nacn": {"gradient": "exp", "gminf": 0.0, "lambda": 100.0}, + }, # linear with distance, gminf (factor) is multiplied by gbar + "apic": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nacn": {"gradient": "exp", "gminf": 0.0, "lambda": 100.0}, + }, # gradients are: flat, linear, exponential + } + + else: + raise ValueError("model type %s is not implemented" % modelType) + self.check_temperature() + + def adjust_na_chans(self, soma, sf=1.0, gbar=1000.0, debug=False): + """ + adjust the sodium channel conductance + + Parameters + ---------- + soma : neuron section object + A soma object whose sodium channel complement will have its + conductances adjusted depending on the channel type + + gbar : float (default: 1000.) + The maximal conductance for the sodium channel + + debug : boolean (false): + Verbose printing + + Returns + ------- + Nothing : + + """ + + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) * sf + nach = self.status["na"] + if nach == "jsrna": + soma().jsrna.gbar = gnabar + soma.ena = self.e_na + if debug: + print("jsrna gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar + soma.ena = 50 # self.e_na + # print('gnabar: ', soma().nav11.gbar, ' vs: 0.0192307692308') + soma().nav11.vsna = 4.3 + if debug: + print("MSO using inva11") + elif nach in ["na", "nacn"]: + soma().na.gbar = gnabar + soma.ena = self.e_na + if debug: + print("na gbar: ", soma().na.gbar) + else: + raise ValueError("Sodium channel %s is not recognized for MSO cells", nach) diff --git a/cnmodel/cells/octopus.py b/cnmodel/cells/octopus.py new file mode 100644 index 0000000..3d2d7bc --- /dev/null +++ b/cnmodel/cells/octopus.py @@ -0,0 +1,718 @@ +from __future__ import print_function +from neuron import h +from ..util import nstomho +from ..util import Params +import numpy as np +from .cell import Cell +from .. import data + +""" +Original hoc code from RMmodel.hoc +// including the "Octopus" cell: +proc set_Type2o() { + gbar_na = nstomho(1000) + gbar_kht = nstomho(150) + gbar_klt = nstomho(600) + gbar_ka = nstomho(0) + gbar_ih = nstomho(0) + gbar_hcno = nstomho(40) + gbar_leak = nstomho(2) + model = 6 + modelname = "Type IIo (Octopus)" + vm0 = -66.67 +} + +""" + +__all__ = ["Octopus", "OctopusRothman", "OctopusSpencer"] + + +class Octopus(Cell): + + type = "octopus" + + @classmethod + def create(cls, modelType="RM03", **kwds): + if modelType in ["RM03", "II-o"]: + return OctopusRothman(**kwds) + elif modelType == "Spencer": + return OctopusSpencer(**kwds) + else: + raise ValueError("Octopus cell type %s is unknown" % modelType) + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is to try to pass the default unit test (loc=0.5) + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for bushy cell + + kwds: dict of options. Two are currently handled: + postsize : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + + if psd_type == "simple": + if terminal.cell.type in ["sgc"]: + weight = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="weight", + ) + tau1 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau1", + ) + tau2 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau2", + ) + erev = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="erev", + ) + return self.make_exp2_psd( + post_sec, + terminal, + weight=weight, + loc=loc, + tau1=tau1, + tau2=tau2, + erev=erev, + ) + else: + raise TypeError( + "Cannot make simple PSD for %s => %s" + % (terminal.cell.type, self.type) + ) + + elif psd_type == "multisite": + if terminal.cell.type == "sgc": + # Max conductances for the glu mechanisms are calibrated by + # running `synapses/tests/test_psd.py`. The test should fail + # if these values are incorrect + self.AMPAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="AMPAR_gmax", + ) + * 1e3 + ) + self.NMDAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_gmax", + ) + * 1e3 + ) + self.Pr = data.get( + "sgc_synapse", species=self.species, post_type=self.type, field="Pr" + ) + # adjust gmax to correct for initial Pr + self.AMPAR_gmax = self.AMPAR_gmax / self.Pr + self.NMDAR_gmax = self.NMDAR_gmax / self.Pr + # AMPA_gmax = 3.314707700918133*1e3 # factor of 1e3 scales to pS (.mod mechanisms) from nS. + # NMDA_gmax = 0.4531929783503451*1e3 + if "AMPAScale" in kwds: + self.AMPAR_gmax = ( + self.AMPAR_gmax * kwds["AMPAScale"] + ) # allow scaling of AMPA conductances + if "NMDAScale" in kwds: + self.NMDAR_gmax = self.NMDAR_gmax * kwds["NMDAScale"] + return self.make_glu_psd( + post_sec, terminal, self.AMPAR_gmax, self.NMDAR_gmax, loc=loc + ) + elif terminal.cell.type == "dstellate": + return self.make_gly_psd(post_sec, terminal, psdtype="glyslow", loc=loc) + else: + raise TypeError( + "Cannot make PSD for %s => %s" % (terminal.cell.type, self.type) + ) + else: + raise ValueError("Unsupported psd type %s" % psd_type) + + +class OctopusRothman(Octopus, Cell): + """ + VCN octopus cell model (point cell). + Rothman and Manis, 2003abc (Type II, with high gklt and hcno - octopus cell h current). + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + species="guineapig", + modelType=None, + debug=False, + ): + """ + initialize the octopus cell, using the default parameters for guinea pig from + R&M2003, as a type II cell with modified conductances. + Modifications to the cell can be made by calling methods below. + + Parameters + ---------- + morphology : string (default: None) + a file name to read the cell morphology from. If a valid file is found, a cell is constructed + as a cable model from the hoc file. + If None (default), the only a point model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels aer inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: None) + nach selects the type of sodium channel that will be used in the model. A channel mechanism + by that name must exist. None implies the default channel (jsrna for this model). + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + Currently, this is not implemented. + + species: string (default 'guineapig') + species defines the channel density that will be inserted for different models. Note that + if a decorator function is specified, this argument is ignored. + + modelType: string (default: None) + modelType specifies the type of the model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point models. + + debug: boolean (default: False) + debug is a boolean flag. When set, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + """ + + super(OctopusRothman, self).__init__() + if modelType == None: + modelType = "II-o" + if nach == None and species == "guineapig": + nach = "jsrna" + if nach == None and species == "mouse": + nach = "nacn" + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "Octopus", + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + self.i_test_range = {"pulse": (-4.0, 4.0, 0.2)} + self.spike_threshold = -50 + self.vrange = [-70.0, -57.0] # set a default vrange for searching for rmp + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + soma = h.Section( + name="Octopus_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.e_leak = -73.0 # from McGinley et al., 2016 + self.e_h = -38.0 # from McGinley et al. + self.R_a = 195 # McGinley et al. + if self.status["species"] == "mouse": + self.mechanisms = ["klt", "kht", "hcnobo", "leak", nach] + else: + self.mechanisms = ["klt", "kht", "ihvcn", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ek = self.e_k + self.soma.ena = self.e_na + if self.status["species"] == "mouse": + self.soma().hcnobo.eh = self.e_h + else: + self.soma().ihvcn.eh = self.e_h + self.soma().leak.erev = self.e_leak + self.soma.Ra = self.R_a + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + + if debug: + print("<< octopus: octopus cell model created >>") + # print 'Cell created: ', self.status + + def species_scaling(self, species="guineapig", modelType="II-o", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + Used ONLY for point models. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be guineapig + + modelType: string (default: 'II-o') + definition of model type from RM03 models, currently limited to type II-o + + silent : boolean (default: True) + run silently (True) or verbosely (False) + """ + soma = self.soma + + if species == "guineapig" and modelType == "II-o": + self.c_m = 0.9 + self.set_soma_size_from_Cm(25.0) + self._valid_temperatures = (22.0, 38.0) + if self.status["temperature"] is None: + self.set_temperature(22.0) + sf = 1.0 + if ( + self.status["temperature"] == 38.0 + ): # adjust for 2003 model conductance levels at 38 + sf = 3.03 # Q10 of 2, 22->38C. (p3106, R&M2003c) + # note that kinetics are scaled in the mod file. + # self.print_soma_info() + self.adjust_na_chans(soma, sf=sf) + soma().kht.gbar = sf * nstomho(150.0, self.somaarea) # 6.1 mmho/cm2 + soma().klt.gbar = sf * nstomho( + 1000.0, self.somaarea + ) # 40.7 mmho/cm2 3195? + soma().ihvcn.gbar = sf * nstomho( + 30.0, self.somaarea + ) # 7.6 mmho/cm2, cf. Bal and Oertel, Spencer et al. 25 u dia cell 40ns? + soma().leak.gbar = sf * nstomho(2.0, self.somaarea) + self.axonsf = 1.0 + elif species == "mouse" and modelType == "II-o": + self.set_soma_size_from_Cm(25.0) + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + # self.print_soma_info() + self.adjust_na_chans(soma, gbar=3000.0) + soma().kht.gbar = nstomho(150.0, self.somaarea) # 6.1 mmho/cm2 + soma().klt.gbar = nstomho(3196.0, self.somaarea) # 40.7 mmho/cm2 + soma().hcnobo.gbar = nstomho( + 40.0, self.somaarea + ) # 7.6 mmho/cm2, cf. Bal and Oertel, Spencer et al. 25 u dia cell + soma().leak.gbar = nstomho(2.0, self.somaarea) + self.axonsf = 1.0 + else: + raise ValueError( + 'Species "%s" or species-type "%s" is not recognized for octopus cells' + % (species, type) + ) + self.status["species"] = species + self.status["modelType"] = modelType + # self.cell_initialize(showinfo=True) + self.check_temperature() + if not silent: + print("set cell as: ", species) + print(" with Vm rest = %6.3f" % self.vm0) + + def adjust_na_chans(self, soma, sf=1.0, gbar=1000.0, debug=False): + """ + adjust the sodium channel conductance + + Parameters + ---------- + soma : neuron section object + a soma object whose sodium channel complement will have it's + conductances adjusted depending on the channel type + + gbar : float (default: 1000.) + the maximal conductance for the sodium channel + + debug : boolean (false): + verbose printing + + Returns + ------- + Nothing + """ + + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = sf * nstomho(gbar, self.somaarea) # mmho/cm2 - 4244.1 moh - 4.2441 + nach = self.status["na"] + if nach == "jsrna": + soma().jsrna.gbar = gnabar + soma.ena = self.e_na + if debug: + print("octopus using jsrna, gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar + soma.ena = self.e_na + soma().nav11.vsna = 4.3 + if debug: + print("octopus using inva11, gbar:", soma().nav11.gbar) + elif nach in ["na", "nacn"]: + soma().nacn.gbar = gnabar + soma.ena = self.e_na + if debug: + print("octopus cell using na/nacn, gbar: ", soma().na.gbar) + else: + raise ValueError( + "Sodium channel %s is not recognized for octopus cells", nach + ) + + +class OctopusSpencer(Octopus, Cell): + """ + VCN octopus cell model (with dendrites). + Based on Spencer et al Front. Comput. Neurosci., 22 October 2012 + https://doi.org/10.3389/fncom.2012.00083 + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach="jsrna", + ttx=False, + species="guineapig", + modelType=None, + debug=False, + ): + """ + initialize the octopus cell, using the parameters Spencer et al. 2012 + Modifications to the cell can be made by calling methods below. + + Parameters + ---------- + morphology : string (default: None) + a file name to read the cell morphology from. If a valid file is found, a cell is constructed + as a cable model from the hoc file. + If None (default), the only a point model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels aer inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: 'na') + nach selects the type of sodium channel that will be used in the model. A channel mechanism + by that name must exist. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + Currently, this is not implemented. + + species: string (default 'guineapig') + species defines the channel density that will be inserted for different models. Note that + if a decorator function is specified, this argument is ignored. + + modelType: string (default: None) + modelType specifies the type of the model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point models. + + debug: boolean (default: False) + debug is a boolean flag. When set, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + """ + + super(OctopusSpencer, self).__init__() + if modelType == None: + modelType = "Spencer" + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "Octopus", + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + self.i_test_range = (-4.0, 6.0, 0.25) + self.spike_threshold = -50 + self.vrange = [-75.0, -63.0] # set a default vrange for searching for rmp + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + soma = h.Section( + name="Octopus_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + self.set_soma_size_from_Section(self.soma) + + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.e_leak = -62.0 # from Spencer et al., 2012 + self.e_h = -38.0 ## from Spencer et al., 2012 + self.R_a = 100.0 # from Spencer et al., 2012 + self.mechanisms = ["klt", "kht", "hcnobo", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ek = -70.0 # self.e_k + self.soma.ena = 55.0 # self.e_na + self.soma().hcnobo.eh = self.e_h + self.soma().leak.erev = self.e_leak + self.soma.Ra = self.R_a + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.decorated.channelValidate(self, verify=True) + # print 'Mechanisms inserted: ', self.mechanisms + self.get_mechs(self.soma) + # self.cell_initialize(vrange=self.vrange) + + if debug: + print("<< octopus: octopus cell model created >>") + # print 'Cell created: ', self.status + + def channel_manager(self, modelType="Spencer"): + """ + This routine defines channel density maps and distance map patterns + for each type of compartment in the cell. The maps + are used by the ChannelDecorator class (specifically, it's private + \_biophys function) to decorate the cell membrane. + + Parameters + ---------- + modelType : string (default: 'Spencer') + A string that defines the type of the model. Currently, 1 type is implemented: + Spencer : Spencer et al Front. Comput. Neurosci. 2012 + + Returns + ------- + Nothing + + Notes + ----- + This routine defines the following variables for the class: + # conductances (gBar) + # a channelMap (dictonary of channel densities in defined anatomical compartments) + # a current injection range for IV's (when testing) + # a distance map, which defines how selected conductances in selected compartments + will change with distance. This includes both linear and exponential gradients, + the minimum conductance at the end of the gradient, and the space constant or + slope for the gradient. + + """ + + # + # Create a model based on the Spencer model + # Channel decoration and stick model from Figure 2 + # densities from Tables 2 and 3 + if modelType == "Spencer": + # print self.c_m + self.c_m = 0.9 + # self.set_soma_size_from_Section(self.soma) + totcap = self.totcap + refarea = self.somaarea # totcap / self.c_m # see above for units + # self.print_soma_info() + self._valid_temperatures = ( + 34.0, + ) # 34 for consistency with other mouse models, but + # Spencer data used "33". This affects very slightly + # the HCN channel conductance. + if self.status["temperature"] is None: + self.set_temperature(34.0) + self.gBar = Params( + nabar=0.0, # 0.0407, # S/cm2 + nabar_ais=0.42441, + kltbar_ais=0.0, + khtbar_ais=0.0, + ihbar_ais=0.0, + kltbar_soma=0.0407, + khtbar_soma=0.0061, + ihbar_soma=0.0076, + kltbar_dend=0.0027, + khtbar_dend=0.0, + ihbar_dend=0.0006, + khtbar_hillock=0.0, + kltbar_hillock=0.0, + ihbar_hillock=0.0, + leakbar=0.0020, + ) + + self.channelMap = { + "soma": { + "jsrna": self.gBar.nabar, + "klt": self.gBar.kltbar_soma, + "kht": self.gBar.khtbar_soma, + "hcnobo": self.gBar.ihbar_soma, + "leak": self.gBar.leakbar, + }, + "hillock": { + "jsrna": 0.0, + "klt": self.gBar.kltbar_hillock, + "kht": self.gBar.khtbar_hillock, + "hcnobo": self.gBar.ihbar_hillock, + "leak": self.gBar.leakbar, + }, + # axon initial segment: + "unmyelinatedaxon": { + "jsrna": self.gBar.nabar_ais, + "klt": self.gBar.kltbar_ais, + "kht": self.gBar.khtbar_ais, + "hcnobo": self.gBar.ihbar_ais, + "leak": self.gBar.leakbar, + }, + "primarydendrite": { + "jsrna": 0.0, + "klt": self.gBar.kltbar_dend, + "kht": self.gBar.khtbar_dend, + "hcnobo": self.gBar.ihbar_dend, + "leak": self.gBar.leakbar, + }, + } + + self.distMap = { + "primarydendrite": { + "klt": {"gradient": "flat", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "flat", "gminf": 0.0, "lambda": 100.0}, + "hcnobo": {"gradient": "flat", "gminf": 0.0, "lambda": 100.0}, + } # all flat with distance + } + # reversal potential map + self.channelErevMap = { + "soma": { + "jsrna": 55.0, + "klt": -70, + "kht": -70, + "hcnobo": -38, + "leak": -62.0, + }, + "hillock": { + "jsrna": 55.0, + "klt": -70, + "kht": -70, + "hcnobo": -38, + "leak": -62.0, + }, + "unmyelinatedaxon": { + "jsrna": 55.0, + "klt": -70, + "kht": -70, + "hcnobo": -38, + "leak": -62.0, + }, + "primarydendrite": { + "jsrna": 55.0, + "klt": -70, + "kht": -70, + "hcnobo": -38, + "leak": -62.0, + }, + } + + else: + raise ValueError("model type %s is not implemented" % modelType) + self.check_temperature() + + def species_scaling(self, species="mouse", modelType="Spencer", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + Used ONLY for point models. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be guineapig + + modelType: string (default: 'II-o') + definition of model type from RM03 models, currently limited to type II-o + + silent : boolean (default: True) + run silently (True) or verbosely (False) + """ + soma = self.soma + + if species == "mouse" and modelType == "Spencer": + print("Octopus: Mouse, Spencer point model - not a valid model") + self.set_soma_size_from_Cm(25.0) + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + self.print_soma_info() + # self.adjust_na_chans(soma) + # soma().kht.gbar = 0.0061 # nstomho(150.0, self.somaarea) # 6.1 mmho/cm2 + # soma().klt.gbar = 0.0407 # nstomho(3196.0, self.somaarea) # 40.7 mmho/cm2 + # soma().hcnobo.gbar = 0.0076 #nstomho(40.0, self.somaarea) # 7.6 mmho/cm2, cf. Bal and Oertel, Spencer et al. 25 u dia cell + # soma().leak.gbar = 0.0005 # nstomho(2.0, self.somaarea) + self.axonsf = 1.0 + else: + raise ValueError( + 'Species "%s" or species-type "%s" is not recognized for octopus cells' + % (species, type) + ) + self.status["species"] = species + self.status["modelType"] = modelType + self.cell_initialize(showinfo=True) + if not silent: + print("set cell as: ", species) + print(" with Vm rest = %6.3f" % self.vm0) diff --git a/cnmodel/cells/pyramidal.py b/cnmodel/cells/pyramidal.py new file mode 100644 index 0000000..3b2329d --- /dev/null +++ b/cnmodel/cells/pyramidal.py @@ -0,0 +1,482 @@ +from __future__ import print_function + +import numpy as np +from neuron import h + +from .cell import Cell +from .. import data +from ..util import Params +from ..util import nstomho + +__all__ = ["Pyramidal", "PyramidalKanold"] + + +class Pyramidal(Cell): + + type = "pyramidal" + + @classmethod + def create(cls, model="POK", **kwds): + if model == "POK": + return PyramidalKanold(**kwds) + else: + raise ValueError("Pyramidal model %s is unknown", model) + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is to try to pass the default unit test (loc=0.5) + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for bushy cell + + kwds: dict of options. Two are currently handled: + postsize : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + + if psd_type == "simple": + if terminal.cell.type in [ + "sgc", + "dstellate", + "tuberculoventral", + "cartwheel", + ]: + weight = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="weight", + ) + tau1 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau1", + ) + tau2 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau2", + ) + erev = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="erev", + ) + return self.make_exp2_psd( + post_sec, + terminal, + weight=weight, + loc=loc, + tau1=tau1, + tau2=tau2, + erev=erev, + ) + else: + raise TypeError( + "Cannot make simple PSD for %s => %s" + % (terminal.cell.type, self.type) + ) + + elif psd_type == "multisite": + if terminal.cell.type == "sgc": + # Max conductances for the glu mechanisms are calibrated by + # running `synapses/tests/test_psd.py`. The test should fail + # if these values are incorrect + self.AMPAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="AMPAR_gmax", + ) + * 1e3 + ) + self.NMDAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_gmax", + ) + * 1e3 + ) + self.Pr = data.get( + "sgc_synapse", species=self.species, post_type=self.type, field="Pr" + ) + # adjust gmax to correct for initial Pr + self.AMPAR_gmax = self.AMPAR_gmax / self.Pr + self.NMDAR_gmax = self.NMDAR_gmax / self.Pr + if "AMPAScale" in kwds: + self.AMPA_gmax = ( + self.AMPA_gmax * kwds["AMPAScale"] + ) # allow scaling of AMPA conductances + if "NMDAScale" in kwds: + self.NMDA_gmax = self.NMDA_gmax * kwds["NMDAScale"] + return self.make_glu_psd( + post_sec, terminal, self.AMPAR_gmax, self.NMDAR_gmax, loc=loc + ) + elif terminal.cell.type == "dstellate": # WBI input -Voigt, Nelken, Young + return self.make_gly_psd(post_sec, terminal, psdtype="glyfast", loc=loc) + elif ( + terminal.cell.type == "tuberculoventral" + ): # TV cells talk to each other-Kuo et al. + return self.make_gly_psd(post_sec, terminal, psdtype="glyfast", loc=loc) + else: + raise TypeError( + "Cannot make PSD for %s => %s" % (terminal.cell.type, self.type) + ) + else: + raise ValueError("Unsupported psd type %s" % psd_type) + + +class PyramidalKanold(Pyramidal, Cell): + """ + DCN pyramidal cell + Kanold and Manis, 1999, 2001, 2005 + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + species="rat", + modelType=None, + debug=False, + ): + """ + initialize a pyramidal cell, based on the Kanold-Manis (2001) pyramidal cell model. + Modifications to the cell can be made by calling methods below. These include + converting to a model with modified size and conductances (experimental). + + Parameters + ---------- + morphology : string (default: None) + a file name to read the cell morphology from. If a valid file is found, a cell is constructed + as a cable model from the hoc file. + If None (default), the only a point model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels is inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: None) + nach selects the type of sodium channel that will be used in the model. A channel mechanim + by that name must exist. None implies the default channel, 'napyr'. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + Currently, this is not implemented. + + species: string (default 'guineapig') + species defines the channel density that will be inserted for different models. Note that + if a decorator function is specified, this argument is ignored (overridden by decorator). + + modelType: string (default: None) + modelType specifies the type of the model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point models. + + debug: boolean (default: False) + debug is a boolean flag. When set, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + + """ + super(PyramidalKanold, self).__init__() + if modelType == None: + modelType = "POK" + if nach == None: + nach = "napyr" + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "Pyramidal", + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + + self.i_test_range = {"pulse": (-0.3, 0.401, 0.02)} + self.vrange = [-75.0, -60.0] + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + soma = h.Section( + name="Pyramidal_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.mechanisms = [ + "napyr", + "kdpyr", + "kif", + "kis", + "ihpyr", + "leak", + "kcnq", + "nap", + ] + for mech in self.mechanisms: + try: + self.soma.insert(mech) + except ValueError: + print("WARNING: Mechanism %s not found" % mech) + self.soma().kif.kif_ivh = -89.6 + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type I-c cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + if debug: + print("<< PYR: POK Pyramidal Cell created >>") + + def get_cellpars(self, dataset, species="guineapig", celltype="II"): + cellcap = data.get( + dataset, species=species, cell_type=celltype, field="soma_Cap" + ) + chtype = data.get( + dataset, species=species, cell_type=celltype, field="soma_natype" + ) + pars = Params(cap=cellcap, natype=chtype) + for g in [ + "soma_napyr_gbar", + "soma_kdpyr_gbar", + "soma_kif_gbar", + "soma_kis_gbar", + "soma_kcnq_gbar", + "soma_nap_gbar", + "soma_ihpyr_gbar", + "soma_leak_gbar", + "soma_e_h", + "soma_leak_erev", + "soma_e_k", + "soma_e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, cell_type=celltype, field=g) + ) + return pars + + def species_scaling(self, species="rat", modelType="I", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + Used ONLY for point models. + + Parameters + ---------- + species : string (default: 'rat') + name of the species to use for scaling the conductances in the base point model + Must be 'rat' + + modelType: string (default: 'I') + definition of model type from Kanold and Manis, 2001 + choices are 'I' or 'POK' (canonical model) or + 'II', a modified model with more physiological surface area and KCNQ channels + + silent : boolean (default: True) + run silently (True) or verbosely (False) + """ + if modelType in ["I", "POK"]: + celltype = "pyramidal" + elif modelType in ["II"]: + celltype = "pyramidal-II" + else: + celltype = modelType + + dataset = "POK_channels" + + soma = self.soma + if species in ["rat", "mouse"] and modelType in [ + "I", + "POK", + "II", + ]: # canonical K&M2001 model cell + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + pars = self.get_cellpars(dataset, species=species, celltype=celltype) + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + soma().napyr.gbar = nstomho(pars.soma_napyr_gbar, self.somaarea) + soma().nap.gbar = nstomho( + pars.soma_nap_gbar, self.somaarea + ) # does not exist in canonical model + soma().kdpyr.gbar = nstomho(pars.soma_kdpyr_gbar, self.somaarea) + soma().kcnq.gbar = nstomho( + pars.soma_kcnq_gbar, self.somaarea + ) # does not exist in canonical model. + soma().kif.gbar = nstomho(pars.soma_kif_gbar, self.somaarea) + soma().kis.gbar = nstomho(pars.soma_kis_gbar, self.somaarea) + soma().ihpyr.gbar = nstomho(pars.soma_ihpyr_gbar, self.somaarea) + # soma().ihpyr_adj.q10 = 3.0 # no temp scaling to sta + soma().leak.gbar = nstomho(pars.soma_leak_gbar, self.somaarea) + soma().leak.erev = pars.soma_leak_erev + soma().ena = pars.soma_e_na + soma().ek = pars.soma_e_k + soma().ihpyr.eh = pars.soma_e_h + + # elif species in 'rat' and modelType == 'II': + # """ + # Modified canonical K&M2001 model cell + # In this model version, the specific membrane capacitance is modified + # so that the overall membrane time constant is consistent with experimental + # measures in slices. However, this is not a physiological value. Attempts + # to use the normal 1 uF/cm2 value were unsuccessful in establishing the expected + # ~12 msec time constant. + # This model also adds a KCNQ channel, as described by Li et al., 2012. + # """ + # self.c_m = 6.0 + # self.set_soma_size_from_Diam(30.0) + # # self.set_soma_size_from_Cm(80.0) + # # print 'diameter: %7.1f' % self.soma.diam + # self._valid_temperatures = (34.,) + # if self.status['temperature'] is None: + # self.set_temperature(34.) + # self.refarea = self.somaarea + # soma().napyr.gbar = nstomho(550, self.refarea) + # soma().nap.gbar = nstomho(60.0, self.refarea) + # soma().kcnq.gbar = nstomho(2, self.refarea) # pyramidal cells have kcnq: Li et al, 2011 (Thanos) + # soma().kdpyr.gbar = nstomho(180, self.refarea) # Normally 80. + # soma().kif.gbar = nstomho(150, self.refarea) # normally 150 + # soma().kis.gbar = nstomho(40, self.refarea) # 40 + # soma().ihpyr.gbar = nstomho(2.8, self.refarea) + # soma().leak.gbar = nstomho(0.5, self.refarea) + # soma().leak.erev = -62. # override default values in cell.py + # soma().ena = 50.0 + # soma().ek = -81.5 + # soma().ihpyr.eh = -43 + # if not self.status['dendrites']: + # self.add_dendrites() + + else: + raise ValueError( + "Species %s or species-modelType %s is not implemented for Pyramidal cells" + % (species, modelType) + ) + + self.status["species"] = species + self.status["modelType"] = modelType + # self.cell_initialize(showinfo=True) + self.check_temperature() + if not silent: + print("set cell as: ", species, modelType) + print(" with Vm rest = %f" % self.vm0) + print(self.status) + for m in self.mechanisms: + print("%s.gbar = %f" % (m, eval("soma().%s.gbar" % m))) + + def i_currents(self, V): + """ + For the steady-state case, return the total current at voltage V + Used to find the zero current point + vrange brackets the interval + Overrides i_currents in cells.py because we have a different set of currents + to compute. + """ + for part in self.all_sections.keys(): + for sec in self.all_sections[part]: + sec.v = V + h.celsius = self.status["temperature"] + h.finitialize() + self.ix = {} + + if "napyr" in self.mechanisms: + self.ix["napyr"] = self.soma().napyr.gna * (V - self.soma().ena) + if "nap" in self.mechanisms: + self.ix["nap"] = self.soma().nap.gnap * (V - self.soma().ena) + if "kdpyr" in self.mechanisms: + self.ix["kdpyr"] = self.soma().kdpyr.gk * (V - self.soma().ek) + if "kif" in self.mechanisms: + self.ix["kif"] = self.soma().kif.gkif * (V - self.soma().ek) + if "kis" in self.mechanisms: + self.ix["kis"] = self.soma().kis.gkis * (V - self.soma().ek) + if "kcnq" in self.mechanisms: + self.ix["kcnq"] = self.soma().kcnq.gk * (V - self.soma().ek) + if "ihpyr" in self.mechanisms: + self.ix["ihpyr"] = self.soma().ihpyr.gh * (V - self.soma().ihpyr.eh) + if "ihpyr_adj" in self.mechanisms: + self.ix["ihpyr_adj"] = self.soma().ihpyr_adj.gh * ( + V - self.soma().ihpyr_adj.eh + ) + # leak + if "leak" in self.mechanisms: + self.ix["leak"] = self.soma().leak.gbar * (V - self.soma().leak.erev) + return np.sum([self.ix[i] for i in self.ix]) + + def add_dendrites(self): + """ + Add simple unbranched dendrite. + The dendrites have some kd, kif and ih current + """ + nDend = range(2) # these will be simple, unbranced, N=4 dendrites + dendrites = [] + for i in nDend: + dendrites.append(h.Section(cell=self.soma)) + for i in nDend: + dendrites[i].connect(self.soma) + dendrites[i].L = 250 # length of the dendrite (not tapered) + dendrites[i].diam = 1 + dendrites[i].cm = self.c_m + # h('dendrites[i].diam(0:1) = 2:1') # dendrite diameter, with tapering + dendrites[i].nseg = 21 # # segments in dendrites + dendrites[i].Ra = 150 # ohm.cm + dendrites[i].insert("napyr") + dendrites[i]().napyr.gbar = 0.00 + dendrites[i].insert("kdpyr") + dendrites[i]().kdpyr.gbar = 0.002 # a little Ht + dendrites[i].insert("kif") + dendrites[i]().kif.gbar = 0.0001 # a little Ht + dendrites[i].insert("leak") # leak + dendrites[i]().leak.gbar = 0.00001 + dendrites[i].insert("ihpyr_adj") # some H current + # mechanism missing so the ihvcn mechanism need to be inserted + dendrites[i].insert('ihvcn') + dendrites[i]().ihvcn.gbar = 0.0 # 0.00002 + dendrites[i]().ihvcn.eh = -43.0 + self.maindend = dendrites + self.status["dendrites"] = True + self.add_section(self.maindend, "maindend") diff --git a/cnmodel/cells/sgc.py b/cnmodel/cells/sgc.py new file mode 100644 index 0000000..f885bb2 --- /dev/null +++ b/cnmodel/cells/sgc.py @@ -0,0 +1,467 @@ +from __future__ import print_function +from neuron import h +from ..util import nstomho +from ..util import Params +import numpy as np +from .cell import Cell +from .. import synapses +from .. import an_model +from .. import data + +__all__ = ["SGC", "SGC_TypeI", "DummySGC"] + + +class SGC(Cell): + type = "sgc" + + @classmethod + def create(cls, model="I", species="mouse", **kwds): + if model == "dummy": + return DummySGC(**kwds) + elif model == "I": + return SGC_TypeI(species=species, **kwds) + else: + raise ValueError("SGC model %s is unknown", model) + + def __init__(self, cf=None, sr=None): + Cell.__init__(self) + self._cf = cf + self._sr = sr + self.spike_source = None # used by DummySGC to connect VecStim to terminal + + @property + def cf(self): + """ Center frequency + """ + return self._cf + + @property + def sr(self): + """ Spontaneous rate group. 1=low, 2=mid, 3=high + """ + return self._sr + + def make_terminal(self, post_cell, term_type, **kwds): + """Create a StochasticTerminal and configure it according to the + postsynaptic cell type. + """ + pre_sec = self.soma + + # Return a simple terminal unless a stochastic terminal was requested. + if term_type == "simple": + return synapses.SimpleTerminal( + pre_sec, post_cell, spike_source=self.spike_source, **kwds + ) + elif term_type == "multisite": + n_rsites = data.get( + "sgc_synapse", + species="mouse", + post_type=post_cell.type, + field="n_rsites", + ) + opts = {"nzones": n_rsites, "delay": 0, "dep_flag": 1} + opts.update(kwds) + # when created, depflag is set True (1) so that we compute the DKR D*F to get release + # this can be modified prior to the run by setting the terminal(s) so that dep_flag is 0 + # (no DKR: constant release probability) + term = synapses.StochasticTerminal( + pre_sec, post_cell, spike_source=self.spike_source, **opts + ) + + kinetics = data.get( + "sgc_ampa_kinetics", + species="mouse", + post_type=post_cell.type, + field=["tau_g", "amp_g"], + ) + term.set_params(**kinetics) + dynamics = data.get( + "sgc_release_dynamics", + species="mouse", + post_type=post_cell.type, + field=["F", "k0", "kmax", "kd", "kf", "taud", "tauf", "dD", "dF"], + ) + term.set_params(**dynamics) + return term + else: + raise ValueError("Unsupported terminal type %s" % term_type) + + +class DummySGC(SGC): + """ SGC class with no cell body; this cell only replays a predetermined + spike train. + """ + + def __init__(self, cf=None, sr=None, simulator=None): + """ + Parameters + ---------- + cf : float (default: None) + Required: the characteristic frequency for the SGC + + sr : int (default None) + required : Selects the spontaneous rate group from the + Zilany et al (2010) model. 1 = LSR, 2 = MSR, 3 = HSR + + simulator : 'cochlea' | 'matlab' | None (default None) + Sets the simulator interface that will be used. All models + currently use the Zilany et al. model, but the simulator can + be run though a Python-interface directly to the Matlab code + as publicy available, (simulator='matlab'), or can be run through + Rudnicki & Hemmert's Python interface to the simulator's C code + (simulator='cochlea'). + + """ + self._simulator = simulator + SGC.__init__(self, cf, sr) + self.vecstim = h.VecStim() + + # this causes the terminal to receive events from the VecStim: + self.spike_source = self.vecstim + + # just an empty section for holding the terminal + self.add_section(h.Section(), "soma") + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": None, + "species": None, + "modelType": "dummy", + "ttx": False, + "name": "DummysGC", + "morphology": None, + "decorator": None, + "temperature": None, + } + + def set_spiketrain(self, times): + """ Set the times of spikes (in seconds) to be replayed by the cell. + """ + self._spiketrain = times + self._stvec = h.Vector(times) + self.vecstim.play(self._stvec) + + def set_sound_stim(self, stim, seed, simulator=None): + """ Set the sound stimulus used to generate this cell's spike train. + """ + self._sound_stim = stim + spikes = self.generate_spiketrain(stim, seed, simulator) + self.set_spiketrain(spikes) + + def generate_spiketrain(self, stim, seed, simulator=None): + if simulator is None: + simulator = self._simulator + spikes = an_model.get_spiketrain( + cf=self.cf, sr=self.sr, seed=seed, stim=stim, simulator=simulator + ) + return spikes * 1000 + + +class SGC_TypeI(SGC): + """ + Spiral ganglion cell model + + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + species="guineapig", + modelType="bm", + cf=None, + sr=None, + debug=False, + ): + """ + Initialize a spiral ganglion Type I cell, based on a bushy cell model. + Modifications to the cell can be made by calling the methods below. These include + converting to a model with modified size and conductances (experimental), and + and changing the sodium channel conductances. + + Parameters + ---------- + morphology : string (default: None) + a file name to read the cell morphology from. If a valid file is found, a cell is constructed + as a cable model from the hoc file. + If None (default), the only a point model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels aer inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: 'na') + nach selects the type of sodium channel that will be used in the model. A channel mechanim + by that name must exist. The default is jsrna (Rothman et al., 1993) + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + Currently, this is not implemented. + + species: string (default 'guineapig') + species defines the channel density that will be inserted for different models. Note that + if a decorator function is specified, this argument is ignored. + + modelType: string (default: None) + modelType specifies the type of the model that will be used. SGC model know about "a" (apical) + and "bm" (basal-middle) models, based on Liu et al., JARO, 2014. + modelType is passed to the decorator, or to species_scaling to adjust point models. + + cf : float (default: None) + The CF for the auditory nerve fiber that this SGC represents. + + sr : string (default: None) + The spontaneous rate group to which this fiber belongs. "LS", "MS", and "HS" are known values. + + debug: boolean (default: False) + debug is a boolean flag. When set, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + + """ + + super(SGC_TypeI, self).__init__(cf=cf, sr=sr) + if modelType == None: + modelType = "bm" # modelTypes are: a (apical), bm (basal middle) + if nach == None: + nach = "jsrna" + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "SGC", + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + + self.i_test_range = { + "pulse": [(-0.3, 0.3, 0.02), (-0.03, 0.0, 0.005)] + } # include finer range as well + self.vrange = [-75.0, -55.0] + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + soma = h.Section( + name="SGC_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.mechanisms = [nach, "klt", "kht", "leak"] + if modelType == "a": + self.mechanisms.append("ihsgcApical") + elif modelType == "bm": + self.mechanisms.append("ihsgcBasalMiddle") + else: + raise ValueError("Type %s not known for SGC model" % modelType) + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ek = self.e_k + self.soma().leak.erev = self.e_leak + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + if debug: + print("<< SGC: Spiral Ganglion Cell created >>") + + def get_cellpars(self, dataset, species="guineapig", celltype="sgc-a"): + cellcap = data.get( + dataset, species=species, cell_type=celltype, field="soma_Cap" + ) + chtype = data.get( + dataset, species=species, cell_type=celltype, field="soma_na_type" + ) + pars = Params(soma_Cap=cellcap, natype=chtype) + for g in [ + "soma_na_gbar", + "soma_kht_gbar", + "soma_klt_gbar", + "soma_ihap_gbar", + "soma_ihbm_gbar", + "soma_ihap_eh", + "soma_ihbm_eh", + "soma_leak_gbar", + "soma_leak_erev", + "soma_e_k", + "soma_e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, cell_type=celltype, field=g) + ) + return pars + + def species_scaling(self, silent=True, species="guineapig", modelType="a"): + """ + Adjust all of the conductances and the cell size according to the species requested. + Used ONLY for point models. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be one of mouse or guineapig + + modelType: string (default: 'a') + definition of HCN model type from Liu et al. JARO 2014: + 'a' for apical model + 'bm' for basal-middle model + + silent : boolean (default: True) + run silently (True) or verbosely (False) + + Returns + ------- + Nothing + + Notes + ----- + The 'guineapig' model uses the mouse HCN channel model, verbatim. This may not + be appropriate, given that the other conductances are scaled up. + + """ + + soma = self.soma + if modelType == "a": + celltype = "sgc-a" + elif modelType == "bm": + celltype = "sgc-bm" + else: + raise ValueError("SGC: unrecognized model type %s " % modelType) + + if species == "mouse": + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + par = self.get_cellpars( + "sgc_mouse_channels", species=species, celltype=celltype + ) + elif species == "guineapig": + # guinea pig data from Rothman and Manis, 2003, modelType II + self._valid_temperatures = (22.0,) + if self.status["temperature"] is None: + self.set_temperature(22.0) + par = self.get_cellpars( + "sgc_guineapig_channels", species=species, celltype=celltype + ) + + self.set_soma_size_from_Cm(par.soma_Cap) + self.adjust_na_chans(soma, gbar=par.soma_na_gbar) + soma().kht.gbar = nstomho(par.soma_kht_gbar, self.somaarea) + soma().klt.gbar = nstomho(par.soma_klt_gbar, self.somaarea) + if celltype == "sgc-a": + soma().ihsgcApical.gbar = nstomho(par.soma_ihap_gbar, self.somaarea) + soma().ihsgcApical.eh = par.soma_ihap_eh + elif celltype == "sgc-bm": + soma().ihsgcBasalMiddle.gbar = nstomho(par.soma_ihbm_gbar, self.somaarea) + soma().ihsgcBasalMiddle.eh = par.soma_ihbm_eh + else: + raise ValueError( + "Ihsgc modelType %s not recognized for species %s" % (celltype, species) + ) + soma().leak.gbar = nstomho(par.soma_leak_gbar, self.somaarea) + soma().leak.erev = par.soma_leak_erev + + self.status["species"] = species + self.status["modelType"] = modelType + self.check_temperature() + if not silent: + print("set cell as: ", species) + print(" with Vm rest = %f" % self.vm0) + + def adjust_na_chans(self, soma, gbar=1000.0, debug=False): + """ + adjust the sodium channel conductance + :param soma: a soma object whose sodium channel complement will have it's + conductances adjusted depending on the channel type + :return nothing: + """ + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) + nach = self.status["na"] + if nach == "jsrna": + soma().jsrna.gbar = gnabar + soma.ena = self.e_na + if debug: + print("jsrna gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar * 0.5 + soma.ena = self.e_na + soma().nav11.vsna = 4.3 + if debug: + print("sgc using inva11") + print("nav11 gbar: ", soma().nav11.gbar) + elif nach in ["na", "nacn"]: + soma().na.gbar = gnabar + soma.ena = self.e_na + if debug: + print("na gbar: ", soma().na.gbar) + else: + raise ValueError("Sodium channel %s is not recognized for SGC cells", nach) + + def i_currents(self, V): + """ + For the steady-state case, return the total current at voltage V + Used to find the zero current point + vrange brackets the interval + Implemented here are the basic RM03 mechanisms + This function should be replaced for specific cell types. + """ + for part in self.all_sections.keys(): + for sec in self.all_sections[part]: + sec.v = V + + h.t = 0.0 + h.celsius = self.status["temperature"] + h.finitialize() + self.ix = {} + if "na" in self.mechanisms: + # print dir(self.soma().na) + self.ix["na"] = self.soma().na.gna * (V - self.soma().ena) + if "jsrna" in self.mechanisms: + # print dir(self.soma().na) + self.ix["jsrna"] = self.soma().jsrna.gna * (V - self.soma().ena) + if "klt" in self.mechanisms: + self.ix["klt"] = self.soma().klt.gklt * (V - self.soma().ek) + if "kht" in self.mechanisms: + self.ix["kht"] = self.soma().kht.gkht * (V - self.soma().ek) + if "ihsgcApical" in self.mechanisms: + self.ix["ihsgcApical"] = self.soma().ihsgcApical.gh * ( + V - self.soma().ihsgcApical.eh + ) + if "ihsgcBasalMiddle" in self.mechanisms: + self.ix["ihsgcBasalMiddle"] = self.soma().ihsgcBasalMiddle.gh * ( + V - self.soma().ihsgcBasalMiddle.eh + ) + if "leak" in self.mechanisms: + self.ix["leak"] = self.soma().leak.gbar * (V - self.soma().leak.erev) + # print self.status['name'], self.status['type'], V, self.ix + return np.sum([self.ix[i] for i in self.ix]) diff --git a/cnmodel/cells/tests/cell_data/SGC_rat_a.pk b/cnmodel/cells/tests/cell_data/SGC_rat_a.pk new file mode 100644 index 0000000..6636f68 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/SGC_rat_a.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e60518d0332cd475f67e13e8f85e23b1b60dda4f252620b495bdfbc5dab43fe +size 7826 diff --git a/cnmodel/cells/tests/cell_data/SGC_rat_bm.pk b/cnmodel/cells/tests/cell_data/SGC_rat_bm.pk new file mode 100644 index 0000000..18d3b35 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/SGC_rat_bm.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0814086d71479adab9326978620e1af0ed153fdfc2c6f4fa0d2213c0b80131b +size 7729 diff --git a/cnmodel/cells/tests/cell_data/bushy-mouse-typeII.pk b/cnmodel/cells/tests/cell_data/bushy-mouse-typeII.pk new file mode 100644 index 0000000..50b349a --- /dev/null +++ b/cnmodel/cells/tests/cell_data/bushy-mouse-typeII.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dbd4717983175a81d362bd01015cb28baa145d78c6c460dd7ec6456f9f46e72 +size 8103 diff --git a/cnmodel/cells/tests/cell_data/bushy_guineapig-typeII-I.pk b/cnmodel/cells/tests/cell_data/bushy_guineapig-typeII-I.pk new file mode 100644 index 0000000..15980c5 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/bushy_guineapig-typeII-I.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34fbddf52a61283bd29622fe87e1fc8afc6f46160cdce3f0cdd581f89bfeba35 +size 7963 diff --git a/cnmodel/cells/tests/cell_data/bushy_guineapig-typeII.pk b/cnmodel/cells/tests/cell_data/bushy_guineapig-typeII.pk new file mode 100644 index 0000000..0cdff68 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/bushy_guineapig-typeII.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de245b95e0b1f66680ed1f79bc31739bce36aed9edd044d02b6effbd645e948e +size 8160 diff --git a/cnmodel/cells/tests/cell_data/cartwheel_rat_I.pk b/cnmodel/cells/tests/cell_data/cartwheel_rat_I.pk new file mode 100644 index 0000000..459f125 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/cartwheel_rat_I.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f7c79982ffa921ba17690de6b31ff27be75936acde0fa72b5d52e0e4abc54da +size 5506 diff --git a/cnmodel/cells/tests/cell_data/dstellate_guineapig-typeI-II.pk b/cnmodel/cells/tests/cell_data/dstellate_guineapig-typeI-II.pk new file mode 100644 index 0000000..bbe549e --- /dev/null +++ b/cnmodel/cells/tests/cell_data/dstellate_guineapig-typeI-II.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de64e933070f7c216d8f632cb56ee32b346bba282c24f453f818c686439b772a +size 8710 diff --git a/cnmodel/cells/tests/cell_data/dstellate_mouse-typeI-II.pk b/cnmodel/cells/tests/cell_data/dstellate_mouse-typeI-II.pk new file mode 100644 index 0000000..1ad0e76 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/dstellate_mouse-typeI-II.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e713ac67a7211e4d78c846bd8bdda4b738c49d5c50fb61c1974f12d45341937 +size 9506 diff --git a/cnmodel/cells/tests/cell_data/octopus_guineapig-typeII-o.pk b/cnmodel/cells/tests/cell_data/octopus_guineapig-typeII-o.pk new file mode 100644 index 0000000..5f16592 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/octopus_guineapig-typeII-o.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e86a6fb8651d0ed0a33e30273d49b8358436bf169899ee9d2bcda454f450225e +size 7747 diff --git a/cnmodel/cells/tests/cell_data/pyramidal_rat_I.pk b/cnmodel/cells/tests/cell_data/pyramidal_rat_I.pk new file mode 100644 index 0000000..fb79f6b --- /dev/null +++ b/cnmodel/cells/tests/cell_data/pyramidal_rat_I.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ebe0523b9260f3b9a87fc6e535f38cb8f0b5840aea056fe8764943883782db7 +size 16581 diff --git a/cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-c.pk b/cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-c.pk new file mode 100644 index 0000000..9f2f9db --- /dev/null +++ b/cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-c.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2536a5ca43d2f46fa6e6bf786d520ac12815b742896a3272a227660a91fc2c1 +size 7863 diff --git a/cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-t.pk b/cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-t.pk new file mode 100644 index 0000000..703fa03 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/tstellate_guineapig-typeI-t.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa5ddc40011269d7e4d00d70ac3053bd4d2d88293db5f8d6f9ae8225c3ed1bdc +size 8182 diff --git a/cnmodel/cells/tests/cell_data/tstellate_mouse-typeI-c.pk b/cnmodel/cells/tests/cell_data/tstellate_mouse-typeI-c.pk new file mode 100644 index 0000000..5d7b94d --- /dev/null +++ b/cnmodel/cells/tests/cell_data/tstellate_mouse-typeI-c.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2990652711db28fa846886f1399faecb4d4680fd8014cbc006636c215248263d +size 14815 diff --git a/cnmodel/cells/tests/cell_data/tuberculoventral_mouse_I.pk b/cnmodel/cells/tests/cell_data/tuberculoventral_mouse_I.pk new file mode 100644 index 0000000..6f7cbe0 --- /dev/null +++ b/cnmodel/cells/tests/cell_data/tuberculoventral_mouse_I.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f50fb610552b181dfd0130245ee41bcbe745d4fc856abfe7f5ad180e84773a5 +size 13267 diff --git a/cnmodel/cells/tests/test_cells.py b/cnmodel/cells/tests/test_cells.py new file mode 100644 index 0000000..858767a --- /dev/null +++ b/cnmodel/cells/tests/test_cells.py @@ -0,0 +1,227 @@ +import os, pickle, pprint +import numpy as np +import neuron + +import cnmodel +import cnmodel.cells as cells +from cnmodel.util import UserTester, reset +from cnmodel.protocols import IVCurve + +""" +Cell-type tests +""" + + +def test_bushy(): + reset(raiseError=False) + cell = cells.Bushy.create(species="guineapig", modelType="II") + CellTester("bushy_guineapig-typeII", cell) + + +def test_bushy21(): + reset(raiseError=False) + cell = cells.Bushy.create(species="guineapig", modelType="II-I") + CellTester("bushy_guineapig-typeII-I", cell) + + +def test_bushy_mouse(): + reset(raiseError=False) + cell = cells.Bushy.create(species="mouse", modelType="II") + CellTester("bushy-mouse-typeII", cell) + + +def test_tstellate(): + reset(raiseError=False) + cell = cells.TStellate.create(species="guineapig", modelType="I-c") + CellTester("tstellate_guineapig-typeI-c", cell) + + +def test_tstellate_mouse(): + reset(raiseError=False) + cell = cells.TStellate.create(species="mouse", modelType="I-c") + CellTester("tstellate_mouse-typeI-c", cell) + + +def test_tstellatet(): + reset(raiseError=False) + cell = cells.TStellate.create(species="guineapig", modelType="I-t") + CellTester("tstellate_guineapig-typeI-t", cell) + + +# not implemented yet +# def test_tstellatet_mouse(): +# reset(raiseError=False) +# cell = cells.TStellate.create(species='mouse', modelType='I-t') +# CellTester('tstellate_mouse-typeI-t', cell) + + +def test_dstellate(): + reset(raiseError=False) + cell = cells.DStellate.create(species="guineapig", modelType="I-II") + CellTester("dstellate_guineapig-typeI-II", cell) + + +def test_dstellate_mouse(): + reset(raiseError=False) + cell = cells.DStellate.create(species="mouse", modelType="I-II") + CellTester("dstellate_mouse-typeI-II", cell) + + +def test_octopus(): + reset(raiseError=False) + cell = cells.Octopus.create(species="guineapig", modelType="II-o") + CellTester("octopus_guineapig-typeII-o", cell) + + +def test_pyramidal(): + reset(raiseError=False) + cell = cells.Pyramidal.create(species="rat", modelType="I") + CellTester("pyramidal_rat_I", cell) + + +def test_tuberculoventral(): + reset(raiseError=False) + cell = cells.Tuberculoventral.create(species="mouse", modelType="TVmouse") + CellTester("tuberculoventral_mouse_I", cell) + + +def test_cartwheel(): + reset(raiseError=False) + cell = cells.Cartwheel.create(species="mouse", modelType="I") + CellTester("cartwheel_rat_I", cell) + + +def test_sgc_basal_middle(): + reset(raiseError=False) + cell = cells.SGC.create(species="mouse", modelType="bm") + CellTester("SGC_rat_bm", cell) + + +def test_sgc_apical(): + reset(raiseError=False) + cell = cells.SGC.create(species="mouse", modelType="a") + CellTester("SGC_rat_a", cell) + + +# +# Supporting functions +# + + +class CellTester(UserTester): + data_dir = "cell_data" + + def run_test(self, cell): + # run I/V test on cell + V0 = cell.find_i0(showinfo=True) + rmrintau = cell.compute_rmrintau(auto_initialize=False, vrange=None) + iv = IVCurve() + self.iv = iv + iv.run(cell.i_test_range, cell) + if self.audit: + iv.show(cell) + + info = dict( + temp=iv.temp, + icmd=iv.current_cmd, + spikes=iv.spike_times(), + rmp=iv.rest_vm(), + rm_taum=iv.input_resistance_tau(), + vpeak=iv.peak_vm(), + vss=iv.steady_vm(), + rmrintau=rmrintau, + ) + return info + + def assert_test_info(self, *args, **kwds): + try: + super(CellTester, self).assert_test_info(*args, **kwds) + finally: + if hasattr(self, "iv") and hasattr(self.iv, "win"): + self.iv.win.hide() + + +# def result_file(key): +# """ +# Return a file name to be used for storing / retrieving test results +# given *key*. +# """ +# path = os.path.dirname(__file__) +# return os.path.join(path, 'cell_data', key + '.pk') + +# def load_cell_info(key): +# """ +# Load prior test results for *key*. +# If there are no prior results, return None. +# """ +# fn = result_file(key) +# if os.path.isfile(fn): +# return pickle.load(open(fn, 'rb')) +# return None + +# def save_cell_info(info, key): +# """ +# Store test results for *key*. +# """ +# fn = result_file(key) +# dirname = os.path.dirname(fn) +# if not os.path.isdir(dirname): +# os.mkdir(dirname) +# pickle.dump(info, open(fn, 'wb')) + + +# The following is superseeded by the built in unit tests. +# def CellTester(key): +# """ +# Test *cell* and raise exception if the results do not match prior +# data. +# """ +# audit = cnmodel.AUDIT_TESTS + +## run I/V test on cell +# iv = IVCurve() +# iv.run(cell.i_test_range, cell) +# iv.show(cell) + +# try: +# info = dict( +# icmd=iv.current_cmd, +# spikes=iv.spike_times(), +# rmp=iv.rest_vm(), +# rm=iv.input_resistance(), +# vpeak=iv.peak_vm(), +# vss=iv.steady_vm(), +# ) + +# expect = load_cell_info(key) + +# if expect is not None: + +## Check test structures are the same +# assert len(info) == len(expect) +# for k in info: +# assert k in expect + +## Check data matches +# for k in info: +# if isinstance(info[k], list): +# assert len(info[k]) == len(expect[k]) +# for i in range(len(info[k])): +# assert np.allclose(info[k][i], expect[k][i]) +# else: +# assert np.allclose(info[k], expect[k]) +# else: +# if not audit: +# raise Exception("No prior test results for cell type '%s'. " +# "Run test.py --audit store new test data." % key) + +# print "\n=== New test results for %s: ===\n" % key +# pprint.pprint(info) +# print "Store new test results? [y/n]", +# yn = raw_input() +# if yn.lower().startswith('y'): +# save_cell_info(info, key) +# else: +# raise Exception("Rejected test results for '%s'" % key) +# finally: +# iv.win.hide() diff --git a/cnmodel/cells/tstellate.py b/cnmodel/cells/tstellate.py new file mode 100644 index 0000000..7b329c3 --- /dev/null +++ b/cnmodel/cells/tstellate.py @@ -0,0 +1,1089 @@ +from __future__ import print_function +from neuron import h +import numpy as np + +from .cell import Cell + +# from .. import synapses +from ..util import nstomho +from ..util import Params +from .. import data + +__all__ = ["TStellate", "TStellateRothman", "TStellateNav11"] + + +class TStellate(Cell): + + type = "tstellate" + + @classmethod + def create(cls, model="RM03", **kwds): + if model == "RM03": # original Rothman-Manis 2003, 22C, point cell, extendable + return TStellateRothman(**kwds) + elif model == "Nav11": # Xie-Manis, 2013, 37C, pointe cell, extendable + return TStellateNav11(**kwds) + else: + raise ValueError("TStellate type %s is unknown", type) + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is to try to pass the default unit test (loc=0.5) + + Scaling is corrected by initial release probability now. + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for bushy cell + + kwds: dict of options. + Available options: + postsize : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + # print('cells/tstellaty.py psd type: ', psd_type) + if psd_type == "simple": + if terminal.cell.type in ["sgc", "dstellate", "tuberculoventral"]: + weight = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="weight", + ) + tau1 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau1", + ) + tau2 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau2", + ) + erev = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="erev", + ) + return self.make_exp2_psd( + post_sec, + terminal, + weight=weight, + loc=loc, + tau1=tau1, + tau2=tau2, + erev=erev, + ) + else: + raise TypeError( + "Cannot make simple PSD for %s => %s" + % (terminal.cell.type, self.type) + ) + + # print('cells/tstellaty.py weight: ', weight) + elif psd_type == "multisite": + if terminal.cell.type == "sgc": + # Max conductances for the glu mechanisms are calibrated by + # running `synapses/tests/test_psd.py`. The test should fail + # if these values are incorrect + self.AMPAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="AMPAR_gmax", + ) + * 1e3 + ) + self.NMDAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_gmax", + ) + * 1e3 + ) + self.Pr = data.get( + "sgc_synapse", species=self.species, post_type=self.type, field="Pr" + ) + # adjust gmax to correct for initial Pr + self.AMPAR_gmax = self.AMPAR_gmax / self.Pr + self.NMDAR_gmax = self.NMDAR_gmax / self.Pr + # old values: + # AMPA_gmax = 0.22479596944138733*1e3 # factor of 1e3 scales to pS (.mod mechanisms) from nS. + # NMDA_gmax = 0.12281291946623739*1e3 + if "AMPAScale" in kwds: + self.AMPAR_gmax = ( + self.AMPAR_gmax * kwds["AMPAScale"] + ) # allow scaling of AMPA conductances + if "NMDAScale" in kwds: + self.NMDAR_gmax = self.NMDAR_gmax * kwds["NMDAScale"] + return self.make_glu_psd( + post_sec, terminal, self.AMPAR_gmax, self.NMDAR_gmax, loc=loc + ) + elif terminal.cell.type == "dstellate": + self.ds_gmax = ( + data.get( + "dstellate_synapse", + species=self.species, + post_type=self.type, + field="gly_gmax", + ) + * 1e3 + ) + # print('ds max: ', self.ds_gmax) + return self.make_gly_psd( + post_sec, terminal, psdtype="glyfast", loc=loc, gmax=self.ds_gmax + ) + elif terminal.cell.type == "tuberculoventral": + self.tv_gmax = ( + data.get( + "tuberculoventral_synapse", + species=self.species, + post_type=self.type, + field="gly_gmax", + ) + * 1e3 + ) + return self.make_gly_psd( + post_sec, terminal, psdtype="glyfast", loc=loc, gmax=self.tv_gmax + ) + else: + raise TypeError( + "Cannot make PSD for %s => %s" % (terminal.cell.type, self.type) + ) + else: + raise ValueError("Unsupported psd type %s" % psd_type) + + +class TStellateRothman(TStellate): + """ + VCN T-stellate base model. + Rothman and Manis, 2003abc (Type I-c, Type I-t) + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + temperature=None, + species="guineapig", + modelType=None, + modelName=None, + debug=False, + ): + """ + Initialize a planar stellate (T-stellate) cell, using the default parameters for guinea pig from + R&M2003, as a type I cell. + Modifications to the cell can be made by calling methods below. These include: + Converting to a type IA model (add transient K current) (species: guineapig-TypeIA). + Changing "species" to mouse or cat (scales conductances) + + Parameters + ---------- + morphology : string (default: None) + a file name to read the cell morphology from. If a valid file is found, a cell is constructed + as a cable model from the hoc file. + If None (default), the only a point model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels aer inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: None) + nach selects the type of sodium channel that will be used in the model. A channel mechanism + by that name must exist. The default is 'nacn', from R&M2003. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + Currently, this is not implemented. + + species: string (default 'guineapig') + species defines the channel density that will be inserted for different models. Note that + if a decorator function is specified, this argument is ignored. + + modelType: string (default: None) + modelType specifies the type of the model that will be used (e.g., "I-c", "I-t"). + modelType is passed to the decorator, or to species_scaling to adjust point models. + + debug: boolean (default: False) + debug is a boolean flag. When set, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + """ + + super(TStellateRothman, self).__init__() + self.i_test_range = {"pulse": (-0.15, 0.15, 0.01)} + if modelType is None: + modelType = "I-c" + if species == "guineapig": + modelName = "RM03" + temp = 22.0 + if nach == None: + nach = "nacn" + if species == "mouse": + temp = 34.0 + if modelName is None: + modelName = "XM13" + if nach == None: + nach = "nacn" + self.i_test_range = {"pulse": (-1.0, 1.0, 0.05)} + self.debug = debug + self.status = { + "species": species, + "cellClass": self.type, + "modelType": modelType, + "modelName": modelName, + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "ttx": ttx, + "name": self.type, + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + self.c_m = 0.9e-6 # default in units of F/cm^2 + self.spike_threshold = ( + -40.0 + ) # matches threshold in released CNModel (set in base cell class) + self.vrange = [-70.0, -55.0] + self._valid_temperatures = (temp,) + if self.status["temperature"] == None: + self.status["temperature"] = temp + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + if self.debug: + print( + "<< TStellate model: Creating point cell, type={:s} >>".format( + modelType + ) + ) + soma = h.Section( + name="TStellate_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + if self.debug: + print("<< TStellate: Creating cell with morphology = %s>>" % morphology) + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.mechanisms = ["kht", "ka", "ihvcn", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ek = self.e_k + self.soma.ena = self.e_na + self.soma().ihvcn.eh = self.e_h + self.soma().leak.erev = self.e_leak + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + if self.debug: + print("<< T-stellate: JSR Stellate Type 1 cell model created >>") + + def get_cellpars(self, dataset, species="guineapig", modelType="I-c"): + cellcap = data.get( + dataset, species=species, model_type=modelType, field="soma_Cap" + ) + chtype = data.get( + dataset, species=species, model_type=modelType, field="na_type" + ) + pars = Params(cap=cellcap, natype=chtype) + # pars.show() + + if self.status["modelName"] == "RM03": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "ka_gbar", + "ih_gbar", + "leak_gbar", + "leak_erev", + "ih_eh", + "e_k", + "e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + if self.status["modelName"] == "XM13": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "ka_gbar", + "ihvcn_gbar", + "leak_gbar", + "leak_erev", + "ih_eh", + "e_k", + "e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + if self.status["modelName"] == "mGBC": + for g in [ + "%s_gbar" % pars.natype, + "kht_gbar", + "ka_gbar", + "ihvcn_gbar", + "leak_gbar", + "leak_erev", + "ih_eh", + "e_k", + "e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, model_type=modelType, field=g) + ) + return pars + + def species_scaling(self, species="guineapig", modelType="I-c", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + Used ONLY for point models. + + This scaling routine also sets the temperature for the model to a default value. Some models + can be run at multiple temperatures, and so a default from one of the temperatures is used. + The calling cell.set_temperature(newtemp) will change the conductances and reinitialize + the cell to the new temperature settings. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be one of mouse, cat, guineapig + + modelType: string (default: 'I-c') + definition of model type from RM03 models, type I-c or type I-t + + silent : boolean (default: True) + run silently (True) or verbosely (False) + """ + soma = self.soma + if modelType == "I-c": + celltype = "tstellate" + elif modelType == "I-t": + celltype = "tstellate-t" + else: + raise ValueError("model type not recognized") + + if species == "mouse": # and modelType == 'I-c': + # use conductance levels from Cao et al., J. Neurophys., 2007. + # model description in Xie and Manis 2013. Note that + # conductances were not scaled for temperature (rates were) + # so here we reset the default Q10's for conductance (g) to 1.0 + if self.debug: + print( + " Setting Conductances for mouse I-c Tstellate cell, (modified from Xie and Manis, 2013)" + ) + self.c_m = 0.9 # default in units of F/cm^2 + dataset = "XM13_channels" + self.vrange = [-75.0, -55.0] + self.set_soma_size_from_Cm(25.0) + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + + pars = self.get_cellpars(dataset, species=species, modelType=modelType) + # pars.show() + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + self.adjust_na_chans(soma, gbar=pars.nacn_gbar, sf=1.0) + soma().kht.gbar = nstomho(pars.kht_gbar, self.somaarea) + soma().ka.gbar = nstomho(pars.ka_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.ihvcn_gbar, self.somaarea) + soma().ihvcn.eh = pars.ih_eh # Rodrigues and Oertel, 2006 + soma().leak.gbar = nstomho(pars.leak_gbar, self.somaarea) + soma().leak.erev = pars.leak_erev + self.e_k = pars.e_k + self.e_na = pars.e_na + soma.ena = self.e_na + soma.ek = self.e_k + self.axonsf = 0.5 + + elif species == "guineapig": + # and modelType == 'I-c': # values from R&M 2003, Type I + if self.debug: + print( + " Setting Conductances for Guinea Pig I-c, Rothman and Manis, 2003" + ) + dataset = "RM03_channels" + self.c_m = 0.9 # default in units of F/cm^2 + self.vrange = [-75.0, -55.0] + self._valid_temperatures = (22.0, 38.0) + if self.status["temperature"] is None: + self.set_temperature(22.0) + sf = 1.0 + if ( + self.status["temperature"] == 38.0 + ): # adjust for 2003 model conductance levels at 38 + sf = 3.03 # Q10 of 2, 22->38C. (p3106, R&M2003c) + # note that kinetics are scaled in the mod file. + pars = self.get_cellpars(dataset, species=species, modelType=modelType) + self.set_soma_size_from_Cm(pars.cap) + self.status["na"] = pars.natype + # pars.show() + self.adjust_na_chans(soma, gbar=pars.nacn_gbar, sf=sf) + soma().kht.gbar = nstomho(pars.kht_gbar, self.somaarea) + soma().ka.gbar = nstomho(pars.ka_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.ih_gbar, self.somaarea) + soma().leak.gbar = nstomho(pars.leak_gbar, self.somaarea) + soma().leak.erev = pars.leak_erev + self.axonsf = 0.5 + + else: + raise ValueError( + "Species %s or species-type %s is not recognized for T-stellate cells" + % (species, type) + ) + + self.status["species"] = species + self.status["modelType"] = modelType + self.check_temperature() + + # def channel_manager(self, modelType='RM03'): + # """ + # This routine defines channel density maps and distance map patterns + # for each type of compartment in the cell. The maps + # are used by the ChannelDecorator class (specifically, called from it's private + # _biophys function) to decorate the cell membrane with channels. + # + # Parameters + # ---------- + # modelType : string (default: 'RM03') + # A string that defines the type of the model. Currently, 3 types are implemented: + # RM03: Rothman and Manis, 2003 somatic densities for guinea pig + # XM13: Xie and Manis, 2013, somatic densities for mouse + # XM13PasDend: XM13, but with only passive dendrites, no channels. + # + # Returns + # ------- + # Nothing + # + # Notes + # ----- + # + # This routine defines the following variables for the class: + # + # - conductances (gBar) + # - a channelMap (dictonary of channel densities in defined anatomical compartments) + # - a current injection range for IV's (when testing) + # - a distance map, which defines how selected conductances in selected compartments + # will change with distance. This includes both linear and exponential gradients, + # the minimum conductance at the end of the gradient, and the space constant or + # slope for the gradient. + # + # """ + # if modelType == 'RM03': + # totcap = 12.0E-12 # TStellate cell (type I) from Rothman and Manis, 2003, as base model + # refarea = totcap / self.c_m # see above for units + # # Type I stellate Rothman and Manis, 2003c + # self._valid_temperatures = (22., 38.) + # if self.status['temperature'] is None: + # self.set_temperature(22.) + # sf = 1.0 + # if self.status['temperature'] == 38.: # adjust for 2003 model conductance levels at 38 + # sf = 3.03 # Q10 of 2, 22->38C. (p3106, R&M2003c) + # self.gBar = Params(nabar=sf*1000.0E-9/refarea, + # khtbar=sf*150.0E-9/refarea, + # kltbar=sf*0.0E-9/refarea, + # ihbar=sf*0.5E-9/refarea, + # leakbar=sf*2.0E-9/refarea, + # ) + # + # self.channelMap = { + # 'axon': {'nacn': 0.0, 'klt': 0., 'kht': self.gBar.khtbar, + # 'ihvcn': 0., 'leak': self.gBar.leakbar / 4.}, + # 'hillock': {'nacn': self.gBar.nabar, 'klt': 0., 'kht': self.gBar.khtbar, + # 'ihvcn': 0., 'leak': self.gBar.leakbar, }, + # 'initseg': {'nacn': self.gBar.nabar, 'klt': 0., 'kht': self.gBar.khtbar, + # 'ihvcn': self.gBar.ihbar / 2., + # 'leak': self.gBar.leakbar, }, + # 'soma': {'nacn': self.gBar.nabar, 'klt': self.gBar.kltbar, + # 'kht': self.gBar.khtbar, 'ihvcn': self.gBar.ihbar, + # 'leak': self.gBar.leakbar, }, + # 'dend': {'nacn': self.gBar.nabar / 2.0, 'klt': 0., 'kht': self.gBar.khtbar * 0.5, + # 'ihvcn': self.gBar.ihbar / 3., 'leak': self.gBar.leakbar * 0.5, }, + # 'apic': {'nacn': 0.0, 'klt': 0., 'kht': self.gBar.khtbar * 0.2, + # 'ihvcn': self.gBar.ihbar / 4., + # 'leak': self.gBar.leakbar * 0.2, }, + # } + # self.irange = np.linspace(-0.1, 0.1, 7) + # self.distMap = {'dend': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}}, # linear with distance, gminf (factor) is multiplied by gbar + # 'apic': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}}, # gradients are: flat, linear, exponential + # } + # + # elif modelType == 'XM13': + # totcap = 25.0E-12 # Base model from Xie and Manis, 2013 for type I stellate cell + # refarea = totcap / self.c_m # see above for units + # self._valid_temperatures = (34.,) + # if self.status['temperature'] is None: + # self.set_temperature(34.) + # self.gBar = Params(nabar=1800.0E-9/refarea, + # khtbar=250.0E-9/refarea, + # kltbar=0.0E-9/refarea, + # ihbar=18.0E-9/refarea, + # leakbar=8.0E-9/refarea, + # ) + # self.channelMap = { + # 'axon': {'nav11': 0.0, 'klt': 0., 'kht': self.gBar.khtbar, + # 'ihvcn': 0., 'leak': self.gBar.leakbar / 4.}, + # 'hillock': {'nav11': self.gBar.nabar, 'klt': 0., 'kht': self.gBar.khtbar, + # 'ihvcn': 0., + # 'leak': self.gBar.leakbar, }, + # 'initseg': {'nav11': self.gBar.nabar, 'klt': 0., 'kht': self.gBar.khtbar, + # 'ihvcn': self.gBar.ihbar / 2., + # 'leak': self.gBar.leakbar, }, + # 'soma': {'nav11': self.gBar.nabar, 'klt': self.gBar.kltbar, + # 'kht': self.gBar.khtbar, 'ihvcn': self.gBar.ihbar, + # 'leak': self.gBar.leakbar, }, + # 'dend': {'nav11': self.gBar.nabar, 'klt': 0., 'kht': self.gBar.khtbar * 0.5, + # 'ihvcn': self.gBar.ihbar / 3., 'leak': self.gBar.leakbar * 0.5, }, + # 'apic': {'nav11': 0.0, 'klt': 0., 'kht': self.gBar.khtbar * 0.2, + # 'ihvcn': self.gBar.ihbar / 4., + # 'leak': self.gBar.leakbar * 0.2, }, + # } + # self.irange = np.linspace(-0.5, 0.5, 9) + # self.distMap = {'dend': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'nav11': {'gradient': 'exp', 'gminf': 0., 'lambda': 100.}}, # linear with distance, gminf (factor) is multiplied by gbar + # 'apic': {'klt': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'kht': {'gradient': 'linear', 'gminf': 0., 'lambda': 100.}, + # 'nav11': {'gradient': 'exp', 'gminf': 0., 'lambda': 100.}}, # gradients are: flat, linear, exponential + # } + # + # elif modelType == 'XM13PasDend': + # # bushy form Xie and Manis, 2013, based on Cao and Oertel mouse conductances + # # passive dendritestotcap = 26.0E-12 # uF/cm2 + # totcap = 26.0E-12 # uF/cm2 + # refarea = totcap / self.c_m # see above for units + # self._valid_temperatures = (34.,) + # if self.status['temperature'] is None: + # self.set_temperature(34.) + # self.gBar = Params(nabar=1000.0E-9/refarea, + # khtbar=150.0E-9/refarea, + # kltbar=0.0E-9/refarea, + # ihbar=0.5E-9/refarea, + # leakbar=2.0E-9/refarea, + # ) + # self.channelMap = { + # 'axon': {'nav11': self.gBar.nabar*0, 'klt': self.gBar.kltbar * 0.25, 'kht': self.gBar.khtbar, 'ihvcn': 0., + # 'leak': self.gBar.leakbar * 0.25}, + # 'hillock': {'nav11': self.gBar.nabar, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar, 'ihvcn': 0., + # 'leak': self.gBar.leakbar, }, + # 'initseg': {'nav11': self.gBar.nabar*3, 'klt': self.gBar.kltbar*2, 'kht': self.gBar.khtbar*2, + # 'ihvcn': self.gBar.ihbar * 0.5, 'leak': self.gBar.leakbar, }, + # 'soma': {'nav11': self.gBar.nabar, 'klt': self.gBar.kltbar, 'kht': self.gBar.khtbar, + # 'ihvcn': self.gBar.ihbar, 'leak': self.gBar.leakbar, }, + # 'dend': {'nav11': self.gBar.nabar * 0.0, 'klt': self.gBar.kltbar*0 , 'kht': self.gBar.khtbar*0, + # 'ihvcn': self.gBar.ihbar*0, 'leak': self.gBar.leakbar*0.5, }, + # 'apic': {'nav11': self.gBar.nabar * 0.0, 'klt': self.gBar.kltbar * 0, 'kht': self.gBar.khtbar * 0., + # 'ihvcn': self.gBar.ihbar *0., 'leak': self.gBar.leakbar * 0.25, }, + # } + # self.irange = np.linspace(-1, 1, 21) + def get_distancemap(self): + return { + "dend": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 200.0}, + "kht": {"gradient": "llinear", "gminf": 0.0, "lambda": 200.0}, + "nav11": {"gradient": "linear", "gminf": 0.0, "lambda": 200.0}, + }, # linear with distance, gminf (factor) is multiplied by gbar + "apic": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nav11": {"gradient": "exp", "gminf": 0.0, "lambda": 200.0}, + }, # gradients are: flat, linear, exponential + } + # else: + # raise ValueError('model type %s is not implemented' % modelType) + # self.check_temperature() + + def adjust_na_chans(self, soma, sf=1.0, gbar=1000.0): + """ + Adjust the sodium channel conductance, depending on the type of conductance + + Parameters + ---------- + soma : NEURON section object (required) + This identifies the soma object whose sodium channel complement will have it's + conductances adjusted depending on the sodium channel type + gbar : float (default: 1000.) + The "maximal" conductance to be set in the model. + + Returns + ------- + Nothing + """ + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) * sf + nach = self.status["na"] + if nach == "jsrna": + soma().jsrna.gbar = gnabar * sf + soma.ena = self.e_na + if self.debug: + print("jsrna gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar + soma.ena = self.e_na + soma().nav11.vsna = 4.3 + if self.debug: + print("tstellate using inva11") + print("nav11 gbar: ", soma().nav11.gbar) + print("nav11 vsna: ", soma().nav11.vsna) + elif nach == "na": + soma().nacn.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("na gbar: ", soma().na.gbar) + elif nach == "nacn": + soma().nacn.gbar = gnabar + soma.ena = self.e_na + if self.debug: + print("nacn gbar: ", soma().nacn.gbar) + else: + raise ValueError( + "tstellate setting Na channels: channel %s not known" % nach + ) + + def add_axon(self): + Cell.add_axon(self, self.soma, self.somaarea, self.c_m, self.R_a, self.axonsf) + + def add_dendrites(self): + """ + Add simple unbranched dendrites to basic Rothman Type I models. + The dendrites have some kht and ih current + """ + cs = False # not implemented outside here - internal Cesium. + nDend = range(4) # these will be simple, unbranced, N=4 dendrites + dendrites = [] + for i in nDend: + dendrites.append(h.Section(cell=self.soma)) + for i in nDend: + dendrites[i].connect(self.soma) + dendrites[i].L = 200 # length of the dendrite (not tapered) + dendrites[i].diam = 1.5 # dendrite diameter + dendrites[i].nseg = 21 # # segments in dendrites + dendrites[i].Ra = 150 # ohm.cm + dendrites[i].insert("kht") + if cs is False: + dendrites[i]().kht.gbar = 0.005 # a little Ht + else: + dendrites[i]().kht.gbar = 0.0 + dendrites[i].insert("leak") # leak + dendrites[i]().leak.gbar = 0.0001 + dendrites[i].insert("ihvcn") # some H current + dendrites[i]().ihvcn.gbar = 0.0 # 0.001 + dendrites[i]().ihvcn.eh = -43.0 + self.maindend = dendrites + self.status["dendrites"] = True + self.add_section(self.maindend, "maindend") + + +class TStellateNav11(TStellate): + """ + VCN T-stellate cell (Mouse) from Xie and Manis, 2013. + Using nav11 sodium channel model. + + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach="nav11", + ttx=False, + species="mouse", + modelType=None, + debug=False, + ): + """ + Initialize a planar stellate (T-stellate) cell as a point model, using the default parameters for + mouse from Xie and Manis, 2013. + Modifications to the cell can be made by calling methods below. + Changing "species": This routine only supports "mouse" + *Note:* in the original model, the temperature scaling applied only to the rate constants, and not + to the conductance. Therefore, the conductances here need to be adjusted to compensate for the + way the mechanisms are currently implemented (so that they scale correctly to the values + used in Xie and Manis, 2013). This is done by setting q10g (the q10 for conductances) to 1 + before setting up the rest of the model parameters. For those conducantances in which a Q10 for + conductance is implemented, the value is typically 2. + + Parameters + ---------- + morphology : string (default: None) + a file name to read the cell morphology from. If a valid file is found, a cell is constructed + as a cable model from the hoc file. + If None (default), the only a point model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels aer inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: 'nav11') + nach selects the type of sodium channel that will be used in the model. A channel mechanims + by that name must exist. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + Currently, this is not implemented. + + species: string (default 'mouse') + species defines the channel density that will be inserted for different models. Note that + if a decorator function is specified, this argument is ignored. + + modelType: string (default: None) + modelType specifies the type of the model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point models. + + debug: boolean (default: False) + debug is a boolean flag. When set, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + """ + + super(TStellateNav11, self).__init__() + if modelType == None: + modelType = "XM13" + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "TStellate", + "morphology": morphology, + "decorator": decorator, + } + + self.i_test_range = (-1, 1.0, 0.05) + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + print( + "<< TStellate Xie&Manis 2013 model: Creating point cell, type={:s} >>".format( + modelType + ) + ) + soma = h.Section( + name="TStellate_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + print("<< TStellate Xie&Manis 2013 model: Creating structured cell >>") + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.mechanisms = ["kht", "ka", "ihvcn", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.soma.ek = self.e_k + self.soma.ena = self.e_na + self.soma().ihvcn.eh = self.e_h + self.soma().leak.erev = self.e_leak + self.soma().cm = 1.0 + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # set the default type II cell parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + + self.get_mechs(self.soma) + self.cell_initialize(vrange=self.vrange) + # self.print_mechs(self.soma) + if self.debug: + print("<< T-stellate: Xie&Manis 2013 cell model created >>") + + def species_scaling(self, species="mouse", modelType="I-c", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + Used ONLY for point models. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be one of mouse, cat, guineapig + + modelType: string (default: 'I-c') + definition of model type from RM03 models, type I-c or type I-t + + silent : boolean (default: True) + run silently (True) or verbosely (False) + """ + soma = self.soma + if species == "mouse" and modelType == "XM13": + # use conductance levels from Cao et al., J. Neurophys., 2007. + # original temp for model: 32 C + print("Mouse Tstellate cell, Xie and Manis, 2013") + self.set_soma_size_from_Cm(25.0) + self.adjust_na_chans(soma, gbar=800.0) # inav11 does not scale conductance + self.e_k = -84.0 + self.e_na = 50.0 + soma.ek = self.e_k + soma.ena = self.e_na + soma().kht.gbar = nstomho(250.0, self.somaarea) + soma().ka.gbar = nstomho(0.0, self.somaarea) + soma().ihvcn.gbar = nstomho(18.0, self.somaarea) + soma().ihvcn.eh = -43 # Rodrigues and Oertel, 2006 + soma().leak.gbar = nstomho(8.0, self.somaarea) + soma().leak.erev = -65.0 + + else: + raise ValueError( + "Species %s or species-type %s is not recognized for T-stellate XM13 cells" + % (species, type) + ) + + self.status["species"] = species + self.status["modelType"] = modelType + # self.cell_initialize(showinfo=False) + # if not silent: + # print 'set cell as: ', species + # print ' with Vm rest = %f' % self.vm0 + + def channel_manager(self, modelType="XM13"): + """ + This routine defines channel density maps and distance map patterns + for each type of compartment in the cell. The maps + are used by the ChannelDecorator class (specifically, called from it's private + _biophys function) to decorate the cell membrane with channels. + + Parameters + ---------- + modelType : string (default: 'XM13') + A string that defines the type of the model. Currently, 3 types are implemented: + XM13: Xie and Manis, 2013, somatic densities for mouse + XM13PasDend: XM13, but with only passive dendrites, no channels. + + Returns + ------- + Nothing + + Notes + ----- + + This routine defines the following variables for the class: + + - conductances (gBar) + - a channelMap (dictonary of channel densities in defined anatomical compartments) + - a current injection range for IV's (when testing) + - a distance map, which defines how selected conductances in selected compartments + will change with distance. This includes both linear and exponential gradients, + the minimum conductance at the end of the gradient, and the space constant or + slope for the gradient. + + """ + if modelType == "XM13": + totcap = ( + 25.0e-12 + ) # Base model from Xie and Manis, 2013 for type I stellate cell + refarea = totcap / self.c_m # see above for units + self.gBar = Params( + nabar=800.0e-9 / refarea, + khtbar=250.0e-9 / refarea, + kltbar=0.0e-9 / refarea, + ihbar=18.0e-9 / refarea, + leakbar=8.0e-9 / refarea, + ) + self.channelMap = { + "axon": { + "nav11": 0.0, + "klt": 0.0, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar / 4.0, + }, + "hillock": { + "nav11": self.gBar.nabar, + "klt": 0.0, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar, + }, + "initseg": { + "nav11": self.gBar.nabar, + "klt": 0.0, + "kht": self.gBar.khtbar, + "ihvcn": self.gBar.ihbar / 2.0, + "leak": self.gBar.leakbar, + }, + "soma": { + "nav11": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": self.gBar.ihbar, + "leak": self.gBar.leakbar, + }, + "dend": { + "nav11": self.gBar.nabar, + "klt": 0.0, + "kht": self.gBar.khtbar * 0.5, + "ihvcn": self.gBar.ihbar / 3.0, + "leak": self.gBar.leakbar * 0.5, + }, + } + self.irange = np.linspace(-1.0, 1.0, 21) + self.distMap = { + "dend": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "nav11": {"gradient": "exp", "gminf": 0.0, "lambda": 100.0}, + } # linear with distance, gminf (factor) is multiplied by gbar + } + + elif modelType == "XM13PasDend": + # bushy form Xie and Manis, 2013, based on Cao and Oertel mouse conductances + # passive dendrites + totcap = 26.0e-12 # uF/cm2 + refarea = totcap / self.c_m # see above for units + self.gBar = Params( + nabar=1000.0e-9 / refarea, + khtbar=150.0e-9 / refarea, + kltbar=0.0e-9 / refarea, + ihbar=0.5e-9 / refarea, + leakbar=2.0e-9 / refarea, + ) + self.channelMap = { + "axon": { + "nav11": self.gBar.nabar * 0, + "klt": self.gBar.kltbar * 0.25, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar * 0.25, + }, + "hillock": { + "nav11": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar, + }, + "initseg": { + "nav11": self.gBar.nabar * 3, + "klt": self.gBar.kltbar * 2, + "kht": self.gBar.khtbar * 2, + "ihvcn": self.gBar.ihbar * 0.5, + "leak": self.gBar.leakbar, + }, + "soma": { + "nav11": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": self.gBar.ihbar, + "leak": self.gBar.leakbar, + }, + "dend": { + "nav11": self.gBar.nabar * 0.0, + "klt": self.gBar.kltbar * 0, + "kht": self.gBar.khtbar * 0, + "ihvcn": self.gBar.ihbar * 0, + "leak": self.gBar.leakbar * 0.5, + }, + } + self.irange = np.linspace(-1, 1, 21) + self.distMap = { + "dend": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 200.0}, + "kht": {"gradient": "llinear", "gminf": 0.0, "lambda": 200.0}, + "nav11": {"gradient": "linear", "gminf": 0.0, "lambda": 200.0}, + } # linear with distance, gminf (factor) is multiplied by gbar + } + else: + raise ValueError("model type %s is not implemented" % modelType) + + def adjust_na_chans(self, soma, gbar=800.0): + """ + Adjust the sodium channel conductance, depending on the type of conductance + + Parameters + ---------- + soma : NEURON section object (required) + This identifies the soma object whose sodium channel complement will have it's + conductances adjusted depending on the sodium channel type + gbar : float (default: 800.) + The "maximal" conductance to be set in the model. + + Returns + ------- + Nothing + """ + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) + nach = self.status["na"] + if nach == "nav11": + soma().nav11.gbar = gnabar + soma.ena = self.e_na + soma().nav11.vsna = 4.3 + if self.debug: + print("tstellate using inva11") + else: + raise ValueError( + "tstellate setting Na channels only supporting nav11: channel %s not known" + % nach + ) diff --git a/cnmodel/cells/tuberculoventral.py b/cnmodel/cells/tuberculoventral.py new file mode 100644 index 0000000..85d3eb1 --- /dev/null +++ b/cnmodel/cells/tuberculoventral.py @@ -0,0 +1,619 @@ +from __future__ import print_function +from neuron import h +import numpy as np + +# import neuron as nrn + +from .cell import Cell +from .. import synapses +from ..util import nstomho +from ..util import Params +from .. import data + +__all__ = ["Tuberculoventral"] + + +class Tuberculoventral(Cell): + + type = "tuberculoventral" + + @classmethod + def create(cls, model="TVmouse", **kwds): + if model in ["TVmouse", "I"]: + return Tuberculoventral(**kwds) + elif model == "dummy": + return DummyTuberculoventral(**kwds) + else: + raise ValueError("Tuberculoventral type %s is unknown", model) + + def __init__(self): + Cell.__init__(self) + self.spike_source = ( + None + ) # used by DummyTuberculoventral to connect VecStim to terminal + + def make_psd(self, terminal, psd_type, **kwds): + """ + Connect a presynaptic terminal to one post section at the specified location, with the fraction + of the "standard" conductance determined by gbar. + The default condition is to try to pass the default unit test (loc=0.5) + + Parameters + ---------- + terminal : Presynaptic terminal (NEURON object) + + psd_type : either simple or multisite PSD for bushy cell + + kwds: dict of options. Two are currently handled: + postsize : expect a list consisting of [sectionno, location (float)] + AMPAScale : float to scale the ampa currents + + """ + if ( + "postsite" in kwds + ): # use a defined location instead of the default (soma(0.5) + postsite = kwds["postsite"] + loc = postsite[1] # where on the section? + uname = ( + "sections[%d]" % postsite[0] + ) # make a name to look up the neuron section object + post_sec = self.hr.get_section(uname) # Tell us where to put the synapse. + else: + loc = 0.5 + post_sec = self.soma + + if psd_type == "simple": + if terminal.cell.type in ["sgc", "dstellate", "tuberculoventral"]: + weight = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="weight", + ) + tau1 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau1", + ) + tau2 = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="tau2", + ) + erev = data.get( + "%s_synapse" % terminal.cell.type, + species=self.species, + post_type=self.type, + field="erev", + ) + return self.make_exp2_psd( + post_sec, + terminal, + weight=weight, + loc=loc, + tau1=tau1, + tau2=tau2, + erev=erev, + ) + else: + raise TypeError( + "Cannot make simple PSD for %s => %s" + % (terminal.cell.type, self.type) + ) + + elif psd_type == "multisite": + if terminal.cell.type == "sgc": + # Max conductances for the glu mechanisms are calibrated by + # running `synapses/tests/test_psd.py`. The test should fail + # if these values are incorrect + self.AMPAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="AMPAR_gmax", + ) + * 1e3 + ) + self.NMDAR_gmax = ( + data.get( + "sgc_synapse", + species=self.species, + post_type=self.type, + field="NMDAR_gmax", + ) + * 1e3 + ) + self.Pr = data.get( + "sgc_synapse", species=self.species, post_type=self.type, field="Pr" + ) + # adjust gmax to correct for initial Pr + self.AMPAR_gmax = self.AMPAR_gmax / self.Pr + self.NMDAR_gmax = self.NMDAR_gmax / self.Pr + if "AMPAScale" in kwds: + self.AMPA_gmax = ( + self.AMPA_gmax * kwds["AMPAScale"] + ) # allow scaling of AMPA conductances + if "NMDAScale" in kwds: + self.NMDA_gmax = self.NMDA_gmax * kwds["NMDAScale"] + return self.make_glu_psd( + post_sec, terminal, self.AMPAR_gmax, self.NMDAR_gmax, loc=loc + ) + elif terminal.cell.type == "dstellate": # WBI input -Voigt, Nelken, Young + return self.make_gly_psd(post_sec, terminal, psdtype="glyfast", loc=loc) + elif ( + terminal.cell.type == "tuberculoventral" + ): # TV cells talk to each other-Kuo et al. + return self.make_gly_psd(post_sec, terminal, psdtype="glyfast", loc=loc) + else: + raise TypeError( + "Cannot make PSD for %s => %s" % (terminal.cell.type, self.type) + ) + else: + raise ValueError("Unsupported psd type %s" % psd_type) + + def make_terminal(self, post_cell, term_type, **kwds): + pre_sec = self.soma + if term_type == "simple": + return synapses.SimpleTerminal( + pre_sec, post_cell, spike_source=self.spike_source, **kwds + ) + elif term_type == "multisite": + if post_cell.type in [ + "dstellate", + "tuberculoventral", + "pyramidal", + "bushy", + "tstellate", + ]: + nzones = data.get( + "tuberculoventral_synapse", + species=self.species, + post_type=post_cell.type, + field="n_rsites", + ) + delay = data.get( + "tuberculoventral_synapse", + species=self.species, + post_type=post_cell.type, + field="delay", + ) + else: + raise NotImplementedError( + "No knowledge as to how to connect tuberculoventral cell to cell type %s" + % type(post_cell) + ) + pre_sec = self.soma + return synapses.StochasticTerminal( + pre_sec, + post_cell, + nzones=nzones, + spike_source=self.spike_source, + delay=delay, + **kwds + ) + else: + raise ValueError("Unsupported terminal type %s" % term_type) + + +class Tuberculoventral(Tuberculoventral): + """ + Tuberculoventral Neuron (DCN) base model + Adapted from T-stellate model, using target parameters from Kuo et al. J. Neurophys. 2012 + """ + + def __init__( + self, + morphology=None, + decorator=None, + nach=None, + ttx=False, + species="mouse", + modelType=None, + debug=False, + ): + """ + Initialize a DCN Tuberculoventral cell, using the default parameters for guinea pig from + R&M2003, as a type I cell. + Modifications to the cell can be made by calling methods below. These include: + Converting to a type IA model (add transient K current) (species: guineapig-TypeIA). + Changing "species" to mouse or cat (scales conductances) + + Parameters + ---------- + morphology : string (default: None) + a file name to read the cell morphology from. If a valid file is found, a cell is constructed + as a cable model from the hoc file. + If None (default), the only a point model is made, exactly according to RM03. + + decorator : Python function (default: None) + decorator is a function that "decorates" the morphology with ion channels according + to a set of rules. + If None, a default set of channels aer inserted into the first soma section, and the + rest of the structure is "bare". + + nach : string (default: 'na') + nach selects the type of sodium channel that will be used in the model. A channel mechanims + by that name must exist. + + ttx : Boolean (default: False) + If ttx is True, then the sodium channel conductance is set to 0 everywhere in the cell. + Currently, this is not implemented. + + species: string (default 'guineapig') + species defines the channel density that will be inserted for different models. Note that + if a decorator function is specified, this argument is ignored. + + modelType: string (default: None) + modelType specifies the type of the model that will be used (e.g., "II", "II-I", etc). + modelType is passed to the decorator, or to species_scaling to adjust point models. + + debug: boolean (default: False) + debug is a boolean flag. When set, there will be multiple printouts of progress and parameters. + + Returns + ------- + Nothing + """ + super(Tuberculoventral, self).__init__() + if modelType == None: + modelType = "TVmouse" + if nach == None: + nach = "nacncoop" + self.debug = debug + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": nach, + "species": species, + "modelType": modelType, + "ttx": ttx, + "name": "Tuberculoventral", + "morphology": morphology, + "decorator": decorator, + "temperature": None, + } + + self.i_test_range = {"pulse": [(-0.35, 1.0, 0.05), (-0.04, 0.01, 0.01)]} + self.vrange = [-80.0, -60.0] # set a default vrange for searching for rmp + + if morphology is None: + """ + instantiate a basic soma-only ("point") model + """ + if self.debug: + print("<< Tuberculoventral model: Creating point cell >>") + soma = h.Section( + name="Tuberculoventral_Soma_%x" % id(self) + ) # one compartment of about 29000 um2 + soma.nseg = 1 + self.add_section(soma, "soma") + else: + """ + instantiate a structured model with the morphology as specified by + the morphology file + """ + if self.debug: + print("<< Tuberculoventral model: Creating structured cell >>") + self.set_morphology(morphology_file=morphology) + + # decorate the morphology with ion channels + if decorator is None: # basic model, only on the soma + self.mechanisms = ["kht", "ka", "ihvcn", "leak", nach] + for mech in self.mechanisms: + self.soma.insert(mech) + self.species_scaling( + silent=True, species=species, modelType=modelType + ) # adjust the default parameters + else: # decorate according to a defined set of rules on all cell compartments + self.decorate() + self.save_all_mechs() # save all mechanisms inserted, location and gbar values... + self.get_mechs(self.soma) + if self.debug: + print("<< Tuberculoventral cell model created >>") + + def get_cellpars(self, dataset, species="mouse", celltype="TVmouse"): + cellcap = data.get( + dataset, species=species, cell_type=celltype, field="soma_Cap" + ) + chtype = data.get( + dataset, species=species, cell_type=celltype, field="soma_na_type" + ) + pars = Params(soma_cap=cellcap, soma_na_type=chtype) + for g in [ + "soma_nacncoop_gbar", + "soma_kht_gbar", + "soma_ka_gbar", + "soma_ihvcn_gbar", + "soma_ihvcn_eh", + "soma_leak_gbar", + "soma_leak_erev", + "soma_e_k", + "soma_e_na", + ]: + pars.additem( + g, data.get(dataset, species=species, cell_type=celltype, field=g) + ) + return pars + + def species_scaling(self, species="guineapig", modelType="TVmouse", silent=True): + """ + Adjust all of the conductances and the cell size according to the species requested. + Used ONLY for point models. + + Parameters + ---------- + species : string (default: 'guineapig') + name of the species to use for scaling the conductances in the base point model + Must be one of mouse, cat, guineapig + + modelType: string (default: 'I-c') + definition of model type from RM03 models, type I-c or type I-t + + silent : boolean (default: True) + run silently (True) or verbosely (False) + """ + soma = self.soma + if self.debug: + print("modelType: ", modelType) + if modelType in ["TVmouse", "I"]: + celltype = "TVmouse" # modelType + modelType = "TVmouse" + else: + raise ValueError( + "Tuberuloventral: Model type %s not recognized" % modelType + ) + + if species == "mouse" and modelType in ["TVmouse", "I"]: + """#From Kuo 150 Mohm, 10 msec tau + Firing at 600 pA about 400 Hz + These values from brute_force runs, getting 380 Hz at 600 pA at 35C + Input resistance and vm is ok, time constnat is short + *** Rin: 168 tau: 7.8 v: -68.4 + Attempts to get longer time constant - cannot keep rate up. + """ + # Adapted from TStellate model type I-c' + self.vrange = [-80.0, -58.0] + self._valid_temperatures = (34.0,) + if self.status["temperature"] is None: + self.set_temperature(34.0) + + pars = self.get_cellpars("TV_channels", species="mouse", celltype=modelType) + self.set_soma_size_from_Cm(pars.soma_cap) + self.status["na"] = pars.soma_na_type + self.adjust_na_chans(soma, gbar=pars.soma_nacncoop_gbar, debug=self.debug) + soma().kht.gbar = nstomho(pars.soma_kht_gbar, self.somaarea) + soma().ka.gbar = nstomho(pars.soma_ka_gbar, self.somaarea) + soma().ihvcn.gbar = nstomho(pars.soma_ihvcn_gbar, self.somaarea) + soma().ihvcn.eh = pars.soma_ihvcn_eh + soma().leak.gbar = nstomho(pars.soma_leak_gbar, self.somaarea) + soma().leak.erev = pars.soma_leak_erev + self.e_leak = pars.soma_leak_erev + self.soma.ek = self.e_k = pars.soma_e_k + self.soma.ena = self.e_na = pars.soma_e_na + + self.axonsf = 0.5 + else: + raise ValueError( + "Species %s or species-type %s is not recognized for Tuberculoventralcells" + % (species, type) + ) + + self.status["species"] = species + self.status["modelType"] = modelType + self.check_temperature() + + def channel_manager(self, modelType="TVmouse"): + """ + This routine defines channel density maps and distance map patterns + for each type of compartment in the cell. The maps + are used by the ChannelDecorator class (specifically, it's private + _biophys function) to decorate the cell membrane. + + Parameters + ---------- + modelType : string (default: 'RM03') + A string that defines the type of the model. Currently, 3 types are implemented: + RM03: Rothman and Manis, 2003 somatic densities for guinea pig + XM13: Xie and Manis, 2013, somatic densities for mouse + XM13PasDend: XM13, but with only passive dendrites, no channels. + + Returns + ------- + Nothing + + Notes + ----- + + This routine defines the following variables for the class: + + - conductances (gBar) + - a channelMap (dictonary of channel densities in defined anatomical compartments) + - a current injection range for IV's (when testing) + - a distance map, which defines how selected conductances in selected compartments + will change with distance. This includes both linear and exponential gradients, + the minimum conductance at the end of the gradient, and the space constant or + slope for the gradient. + + """ + if modelType == "TVmouse": + print("decorate as tvmouse") + # totcap = 95.0E-12 # Tuberculoventral cell (type I), based on stellate, adjusted for Kuo et al. TV firing + self.set_soma_size_from_Section(self.soma) + totcap = self.totcap + refarea = self.somaarea # totcap / self.c_m # see above for units + self.gBar = Params( + nabar=1520.0e-9 / refarea, + khtbar=160.0e-9 / refarea, + kltbar=0.0e-9 / refarea, + kabar=65.0 / refarea, + ihbar=1.25e-9 / refarea, + leakbar=5.5e-9 / refarea, + ) + self.channelMap = { + "axon": { + "nacn": 0.0, + "klt": 0.0, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar / 4.0, + }, + "hillock": { + "nacn": self.gBar.nabar, + "klt": 0.0, + "kht": self.gBar.khtbar, + "ihvcn": 0.0, + "leak": self.gBar.leakbar, + }, + "initseg": { + "nacn": self.gBar.nabar, + "klt": 0.0, + "kht": self.gBar.khtbar, + "ihvcn": self.gBar.ihbar / 2.0, + "leak": self.gBar.leakbar, + }, + "soma": { + "nacn": self.gBar.nabar, + "klt": self.gBar.kltbar, + "kht": self.gBar.khtbar, + "ihvcn": self.gBar.ihbar, + "leak": self.gBar.leakbar, + }, + "dend": { + "nacn": self.gBar.nabar / 2.0, + "klt": 0.0, + "kht": self.gBar.khtbar * 0.5, + "ihvcn": self.gBar.ihbar / 3.0, + "leak": self.gBar.leakbar * 0.5, + }, + "apic": { + "nacn": 0.0, + "klt": 0.0, + "kht": self.gBar.khtbar * 0.2, + "ihvcn": self.gBar.ihbar / 4.0, + "leak": self.gBar.leakbar * 0.2, + }, + } + self.irange = np.linspace(-0.3, 0.6, 10) + self.distMap = { + "dend": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + }, # linear with distance, gminf (factor) is multiplied by gbar + "apic": { + "klt": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + "kht": {"gradient": "linear", "gminf": 0.0, "lambda": 100.0}, + }, # gradients are: flat, linear, exponential + } + else: + raise ValueError("model type %s is not implemented" % modelType) + + def adjust_na_chans(self, soma, gbar=1000.0, debug=False): + """ + Adjust the sodium channel conductance, depending on the type of conductance + + Parameters + ---------- + soma : NEURON section object (required) + This identifies the soma object whose sodium channel complement will have it's + conductances adjusted depending on the sodium channel type + gbar : float (default: 1000.) + The "maximal" conductance to be set in the model. + debug : boolean (default: False) + A flag the prints out messages to confirm the operations applied. + + Returns + ------- + Nothing + """ + if self.status["ttx"]: + gnabar = 0.0 + else: + gnabar = nstomho(gbar, self.somaarea) + nach = self.status["na"] + if nach == "nacncoop": + soma().nacncoop.gbar = gnabar + soma().nacncoop.KJ = 2000.0 + soma().nacncoop.p = 0.25 + soma.ena = self.e_na + if debug: + print("nacncoop gbar: ", soma().nacncoop.gbar) + elif nach == "jsrna": + soma().jsrna.gbar = gnabar + soma.ena = self.e_na + if debug: + print("jsrna gbar: ", soma().jsrna.gbar) + elif nach == "nav11": + soma().nav11.gbar = gnabar * 0.5 + soma.ena = self.e_na + soma().nav11.vsna = 4.3 + if debug: + print("Tuberculoventral using inva11") + print("nav11 gbar: ", soma().nav11.gbar) + elif nach == "na": + soma().na.gbar = gnabar + soma.ena = self.e_na + if debug: + print("na gbar: ", soma().na.gbar) + elif nach == "nacn": + soma().nacn.gbar = gnabar + soma.ena = self.e_na + if debug: + print("nacn gbar: ", soma().nacn.gbar) + else: + raise ValueError( + "Tuberculoventral setting Na channels: channel %s not known" % nach + ) + + +class DummyTuberculoventral(Tuberculoventral): + """ Tuberculoventral cell class with no cell body; this cell only replays a predetermined + spike train. Useful for testing, or replacing spike trains to determine + the importance of spike structures within a network. + """ + + def __init__(self, cf=None, species="mouse"): + """ + Parameters + ---------- + cf : float (default: None) + Required: the characteristic frequency for the TV cell + Really just for reference. + + """ + + Tuberculoventral.__init__(self) + self.vecstim = h.VecStim() + + # this causes the terminal to receive events from the VecStim: + self.spike_source = self.vecstim + + # just an empty section for holding the terminal + self.add_section(h.Section(), "soma") + self.status = { + "soma": True, + "axon": False, + "dendrites": False, + "pumps": False, + "na": None, + "species": species, + "modelType": "Dummy", + "modelName": "DummyTuberculoventral", + "ttx": None, + "name": "DummyTuberculoventral", + "morphology": None, + "decorator": None, + "temperature": None, + } + print("<< Tuberculoventral: Dummy Tuberculoventral Cell created >>") + + def set_spiketrain(self, times): + """ Set the times of spikes (in seconds) to be replayed by the cell. + """ + self._spiketrain = times + self._stvec = h.Vector(times) + self.vecstim.play(self._stvec) diff --git a/cnmodel/custom_init.hoc b/cnmodel/custom_init.hoc new file mode 100755 index 0000000..7fdbbbd --- /dev/null +++ b/cnmodel/custom_init.hoc @@ -0,0 +1,23 @@ +INITDUR = 100 // # ms to reach steady state +DTSTEP = 0.1 +proc init() { local temp + //print "Using Custom Init" + finitialize(v_init) + t = -2*INITDUR // jump to a time "before" 0 + temp = cvode.active() + if (temp != 0) { // if cvode is on, turn it off + cvode.active(0) + dt = DTSTEP + } + while (t < -INITDUR) { + fadvance() + } + if (temp != 0) { cvode.active(1) } // turn cvode back on if necessary + t = 0 + if (cvode.active()) { + cvode.re_init() + } else { + fcurrent() + } + frecord_init() +} diff --git a/cnmodel/data/__init__.py b/cnmodel/data/__init__.py new file mode 100644 index 0000000..e623a56 --- /dev/null +++ b/cnmodel/data/__init__.py @@ -0,0 +1,18 @@ +""" +The cnmodel.data package contains information about ion channel densities, +connectivity, synaptic properties, and population distributions. These values +are used by the Cell, Synapse, Population, and related classes to determine +all model construction parameters. + +Values are stored in python strings that contain human-readable tables with +provenance documentation. +""" + + +from ._db import get, get_source, add_table_data, report_changes, setval + + +from . import connectivity +from . import synapses +from . import populations +from . import ionchannels diff --git a/cnmodel/data/_db.py b/cnmodel/data/_db.py new file mode 100644 index 0000000..f4eb7cf --- /dev/null +++ b/cnmodel/data/_db.py @@ -0,0 +1,321 @@ +# -*- encoding: utf-8 -*- +from __future__ import print_function +from collections import OrderedDict +import re + + +# Unified collection point for all empirically-determined biophysical +# values. Each value is a tuple (val, source). +DATA = OrderedDict() + + +def get(*args, **kwds): + """ Get a single value from the database using the supplied arguments + to query. + + Optionally, one keyword argument may be a list of values, in which case + a dict will be returned containing {listval: dbval} pairs for each value in + the list. + """ + return _lookup(0, *args, **kwds) + + +def get_source(*args, **kwds): + """ Get the source of a single value from the database using the supplied + arguments to query. + + Optionally, one keyword argument may be a list of values, in which case + a dict will be returned containing {listval: dbval} pairs for each value in + the list. + """ + return _lookup(1, *args, **kwds) + + +def print_table(table): + for k in DATA.keys(): + if table == k[0]: + print("data key: ", k) + print(DATA[k][0]) + + +def get_table_info(table): + """ + Return a dictionary of row and column names in the table + """ + tinfo = {} + for k in DATA.keys(): + if table == k[0]: + for p in k: + if not isinstance(p, tuple): + continue + if p[0] not in tinfo.keys(): + tinfo[p[0]] = [] + if p[1] not in tinfo[p[0]]: + tinfo[p[0]].append(p[1]) + return tinfo + + +def _lookup(ind, *args, **kwds): + key = mk_key(*args, **kwds) + if isinstance(key, dict): + data = {} + for k, key in key.items(): + data[k] = DATA[key][ind] + return data + else: + return DATA[key][ind] + + +def setval(val, *args, **kwds): + key = mk_key(*args, **kwds) + oldval = None + # change_flag = False + if key in DATA: + # change_flag = True # any attempt to change key will set this + oldval = DATA[key] # save the previous stored value + # raise RuntimeError("Data key '%s' has already been set." % str(key)) + DATA[key] = val + return oldval + + +def mk_key(*args, **kwds): + # Make a unique key (or list of keys) used to access values from the + # database. The generated key is independent of the order that arguments + # are specified. + # + # Optionally, one keyword argument may have a list of values, in which case + # the function will return a dict containing {listval: key} pairs for each + # value in the list. + listkey = None + for k, v in kwds.items(): + if isinstance(v, (list, tuple)): + if listkey is not None: + raise TypeError("May only specify a list of values for one key.") + listkey = k + + if listkey is None: + return _mk_key(*args, **kwds) + else: + keys = {} + for v in kwds[listkey]: + kwds[listkey] = v + keys[v] = _mk_key(*args, **kwds) + return keys + + +def _mk_key(*args, **kwds): + key = list(args) + list(kwds.items()) + key.sort(key=lambda a: a[0] if isinstance(a, tuple) else a) + return tuple(key) + + +def add_table_data(name, row_key, col_key, data, **kwds): + """ + Read data like:: + + Description + + ------------------------------------ + col1 col2 col3 + row1 1.2 [1] 0.9e-6 [1] 27 [2] + row2 1.7 [1] [3] + row3 0.93 [2] 0.3e-6 3 [2] + + ------------------------------------ + + [1] citation 1 + [2] citation 2 + [3] missing because. + + + """ + if isinstance(data, str) and "\xc2" in data: + raise TypeError( + "Data table <%s> appears to contain unicode characters but" + "was not defined as unicode." % name + ) + + lines = data.split("\n") + + # First, split into description, table, and sources using ----- lines + desc = [] + table = [] + while lines: + line = lines.pop(0) + # print ">", line + if re.match(r"\s*-+\s*$", line): + # print "match!" + break + desc.append(line) + while lines: + line = lines.pop(0) + # print ">", line + if re.match(r"\s*-+\s*$", line): + # print "match!" + break + table.append(line) + + # print desc + # print table + + # parse remaining lines as sources + sources = parse_sources(lines) + # print sources + + # + # parse table + # table might be empty, so take care of that first. + if table == []: + return [] # no changes + + while len(table[0].strip()) == 0: + table.pop(0) + + spaces = [c == " " for c in table[0]] + cols = [0] + [i for i in range(1, len(spaces)) if spaces[i - 1] and not spaces[i]] + cols = cols + [max(map(len, table)) + 1] + # print spaces + # print cols + # Make sure columns are obeyed strictly + for i, line in enumerate(table): + for j, c in enumerate(cols[1:]): + if len(line) < c: + continue + if line[c - 1] != " ": + print("Table line with error: \n ", line) + raise Exception( + "Table <%s> line: %d, column: %s does not obey column boundaries." + % (name, i, j) + ) + + # Break table into cells + cells = [] + for line in table: + if line.strip() != "": + cells.append( + [line[cols[i] : cols[i + 1]].strip() for i in range(len(cols) - 1)] + ) + # print cells + + # Extract row/column names + col_names = cells.pop(0)[1:] + row_names = [cells[i].pop(0) for i in range(len(cells))] + if len(set(row_names)) != len(row_names): + for n in set(row_names): + row_names.remove(n) + raise NameError("Duplicate row names: %s" % row_names) + + # Parse cell values + for i in range(len(cells)): + for j in range(len(cells[0])): + cell = cells[i][j].strip() + m = re.match(r"([^\[]*)(\[([^\]]+)\])?", cell) # match like "0.7 [3]" + if m is None: + raise ValueError( + "Table cell (%d, %d) has bad format: '%s'" % (i, j, cell) + ) + + # parse value + # If the value contains '±' then a tuple is returned containing the values + # on either side. + val, _, source = m.groups() + # val = unicode(val) # python 2 + val = str(val) # python 3 + if val.strip() == "": + val = None + else: + parts = val.split(u"±") + vals = [] + for p in parts: + try: + p = int(p) + except ValueError: + try: + p = float(p) + except ValueError: + try: + p = str( + p.strip() + ) # allow strings to identify mechanisms also + except ValueError: + raise ValueError( + "Table cell (%d, %d) value has bad format: '%s'" + % (i, j, val) + ) + vals.append(p) + if len(vals) == 1: + val = vals[0] + else: + val = tuple(vals) + + # parse source + if source is not None: + try: + source = sources[source] + except KeyError: + raise ValueError( + "Table cell (%d, %d) has unknown source key: '%s'" + % (i, j, source) + ) + + cells[i][j] = (val, source) + + changes = [] # a list of parameters that are changed if we are rewriting a table + for i, row in enumerate(row_names): + for j, col in enumerate(col_names): + kwds[row_key] = row + kwds[col_key] = col + oldval = setval(cells[i][j], name, **kwds) + if oldval is not None and oldval != cells[i][j]: + key = mk_key(name, **kwds) + changes.append( + {"key": key, "new": cells[i][j], "old": oldval, "name": name} + ) + # changes.append({'name': name, 'row': row, 'col': col, 'new': cells[i][j], 'old': oldval}) + return changes + + +def report_changes(changes): + """ + For changes to data tables, give user a readout + """ + if len(changes) > 0: + anychg = False + for ch in changes: + # print(' >>> Changing %s, %s from default (%s) to %s' % (ch['row'], ch['col'], str(ch['new'][0]), str(ch['old'][0]))) + if str(ch["old"][0]) != str(ch["new"][0]): + if anychg is False: + print( + "\nWarning: Data Table '%s' (in memory) has been modified!" + % changes[0]["name"] + ) + anychg = True + print( + " >>> Changing %s, from default (%s) to %s" + % (ch["key"], str(ch["old"][0]), str(ch["new"][0])) + ) + + +def parse_sources(lines): + sources = {} + key = None + val = [] + for l in lines: + l = l.lstrip() + m = re.match(r"\s*\[([^\]]+)\]\s+(.*)$", l) + if m is not None: + key = m.groups()[0] + sources[key] = m.groups()[1].strip() + else: + if key is None: + if l == "": + continue + raise ValueError( + "Incorrect sources format--got text without " + 'citation index: "%s".' % l + ) + sources[key] += "\n" + l + return sources + + +# parse_sources('''\n\n[1] source 1\n it's cool.\n[2] source 2 is not\n'''.split('\n')) diff --git a/cnmodel/data/connectivity.py b/cnmodel/data/connectivity.py new file mode 100644 index 0000000..085b6e9 --- /dev/null +++ b/cnmodel/data/connectivity.py @@ -0,0 +1,234 @@ +# -*- encoding: utf-8 -*- +from ._db import add_table_data + +#: Mouse synaptic convregence table +mouse_convergence = u""" + +Convergence defines the average number of presynaptic cells of a particular +type (rows) that synapse onto a single postsynaptic cell of a particular +type (columns). +This connectivity matrix is currently incomplete. +Note: Bushy and pyramidal cells are known to have no (or very few) +collaterals within the CN, and so they are not listed as presynaptic cells in +this table. Octopus cells have collaterals (including in granule cell domains), +and should be added to this table when more data are available (Golding et al., +J. Neurosci. 15: 3138, 1995) + +---------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral +sgc 3.3±0.6 [2] 6.5±1.0 [2] 35±0 [3] 60±0 [2] 48±0 [5] 24±0 [5] +dstellate 7 [1] 20 [1] 3 [1] 0 [4] 15 [5] 15 [5] +tstellate 0 [6] 0 [6] 0 [6] 0 [6] 0 [6] 0 [6] +tuberculoventral 6 6 0 0 [4] 21 [5] 0 [7] +pyramidal 0 0 0 0 0 0 +---------------------------------------------------------------------------------------------- + +[1] Guesses based on Campagnola & Manis 2014 + +[2] Cao, X. & Oertel, D. (2010). Auditory nerve fibers excite targets through + synapses that vary in convergence, strength, and short-term plasticity. + Journal of Neurophysiology, 104(5), 2308–20. + Xie and Manis (unpublished): max EPSC = 3.4 ± 1.5 nA with ~0.3 nA steps + (Cao and Oertel, 2010) = ~11 AN inputs. However neither we nor Cao and Oertel + see that many clear steps in the responses, so use lower bound. + +[3] Lower bound based on estimates from unpublished data Xie and Manis (2017) + Assumptions: No discernable step sizes when increasing shock intensity + at ANFs in radiate multipolars (dstellate) + Measured: 0.034 ± 15 nA sEPSC @ -70 mV + Measured: Maximal current from AN stim = 1.2 ± 0.7 nA @ -70 mV + Assuming that each AN provides 1 input, then N = ~35 + +[4] Octopus cells are devoid of inhibitory input (Golding et al., J. Neurosci., 1995) + +[5] Convergence from Hancock and Voigt, Ann. Biomed. Eng. 27, 1999 and Zheng and Voigt, + Ann. Biomed. Eng., 34, 2006. Numbers are based on models for cat and gerbil, + respectively. Adjusted to 1/2 to avoid overexciting TV cells in network model. + +[6] tstellate cells have collaterals within the CN. It has been proposed that they + provide auditory-driven input to the DCN (Oertel and Young, ), and also synapse + within the VCN (Oertel, SFN abstract). These parameters may need to be adjusted + once the convergence and strength is known. + +[7] In the models of Hancock and Voigt (1999) and Zheng and Voigt (2006), the TV cells + have no connections with each other. However, Kuo et al. (J. Neurophysiol., 2015) + did see connections between pairs of TV cells in the mouse. + +""" + +add_table_data( + "convergence", + row_key="pre_type", + col_key="post_type", + species="mouse", + data=mouse_convergence, +) + + +mouse_convergence_range = u""" + +The convergence range table describes, for each type of connection from +presynaptic (rows) to postsynaptic (columns), the variance in frequency of +presynaptic cells relative to the postsynaptic cell. + +All values are expressed as the sigma for a lognormal distribution scaled to +the CF of the postsynaptic cell. + +---------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral +sgc 0.05 [1] 0.1 [1] 0.4 [1] 0.5 [5] 0.1 [1] 0.1 [1] +dstellate 0.208 [2] 0.347 [2] 0.5 [1] 0 0.2 [1] 0.2 [1] +tstellate 0.1 [4] 0.1 [4] 0 0 0 0 +tuberculoventral 0.069 [3] 0.111 [3] 0 0 0.15 [1] 0 +pyramidal 0 0 0 0 0 0 +---------------------------------------------------------------------------------------------- + +[1] Guess based on axonal / dendritic morphology. + +[2] Calculated from Campagnola & Manis 2014 fig. 7C + Distribution widths are given in stdev(octaves), so we multiply by ln(2) to + get the sigma for a lognormal distribution. + DS->Bushy: ln(2) * 0.3 = 0.208 + DS->TStellate: ln(2) * 0.5 = 0.347 + +[3] Calculated from Campagnola & Manis 2014 fig. 9C + Distribution widths are given in stdev(octaves), so we multiply by ln(2) to + get the sigma for a lognormal distribution. + TV->Bushy: ln(2) * 0.10 = 0.069 + TV->TStellate: ln(2) * 0.16 = 0.111 + +[4] Guess based on very limited information in Campagnola & Manis 2014 fig. 12 + +[5] Octopus cells get a wide range of ANF input (but weak on a per input basis) + For example, see McGinley et al., 2012 or Spencer et al., 2012. + + +""" + +add_table_data( + "convergence_range", + row_key="pre_type", + col_key="post_type", + species="mouse", + data=mouse_convergence_range, +) + +# -------------------------------------------------------------------------------------------- +guineapig_convergence = u""" + +Convergence defines the average number of presynaptic cells of a particular +type (rows) that synapse onto a single postsynaptic cell of a particular +type (columns). +This connectivity matrix is currently incomplete. +Note: Bushy and pyramidal cells are known to have no (or very few) +collaterals within the CN, and so they are not listed as presynaptic cells in +this table. Octopus cells have collaterals (including in granule cell domains), +and should be added to this table when more data are available (Golding et al., +J. Neurosci. 15: 3138, 1995) + +This table is just a guess... using mouse data... + +---------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral mso +sgc 3.3±0.6 [2] 6.5±1.0 [2] 35±0 [3] 60±0 [2] 48±0 [5] 24±0 [5] 0 +bushy 0 0 0 0 0 0 12 [8] +dstellate 7 [1] 20 [1] 3 [1] 0 [4] 15 [5] 15 [5] 0 +tstellate 0 [6] 0 [6] 0 [6] 0 [6] 0 [6] 0 [6] 0 +tuberculoventral 6 6 0 0 [4] 21 [5] 0 [7] 0 +pyramidal 0 0 0 0 0 0 0 +---------------------------------------------------------------------------------------------- + +[1] Guesses based on Campagnola & Manis 2014 (using mouse data on guinea pig cells) + +[2] Cao, X. & Oertel, D. (2010). Auditory nerve fibers excite targets through + synapses that vary in convergence, strength, and short-term plasticity. + Journal of Neurophysiology, 104(5), 2308–20. + Xie and Manis (unpublished): max EPSC = 3.4 ± 1.5 nA with ~0.3 nA steps + (Cao and Oertel, 2010) = ~11 AN inputs. However neither we nor Cao and Oertel + see that many clear steps in the responses, so use lower bound. + +[3] Lower bound based on estimates from unpublished data Xie and Manis (2017) + Assumptions: No discernable step sizes when increasing shock intensity + at ANFs in radiate multipolars (dstellate) + Measured: 0.034 ± 15 nA sEPSC @ -70 mV + Measured: Maximal current from AN stim = 1.2 ± 0.7 nA @ -70 mV + Assuming that each AN provides 1 input, then N = ~35 + +[4] Octopus cells are devoid of inhibitory input (Golding et al., J. Neurosci., 1995) + +[5] Convergence from Hancock and Voigt, Ann. Biomed. Eng. 27, 1999 and Zheng and Voigt, + Ann. Biomed. Eng., 34, 2006. Numbers are based on models for cat and gerbil, + respectively. Adjusted to 1/2 to avoid overexciting TV cells in network model. + +[6] tstellate cells have collaterals within the CN. It has been proposed that they + provide auditory-driven input to the DCN (Oertel and Young, ), and also synapse + within the VCN (Oertel, SFN abstract). These parameters may need to be adjusted + once the convergence and strength is known. + +[7] In the models of Hancock and Voigt (1999) and Zheng and Voigt (2006), the TV cells + have no connections with each other. However, Kuo et al. (J. Neurophysiol., 2015) + did see connections between pairs of TV cells in the mouse. + +[8] Bushy convergence to MSO is a guess +""" + +add_table_data( + "convergence", + row_key="pre_type", + col_key="post_type", + species="guineapig", + data=guineapig_convergence, +) + + +guineapig_convergence_range = u""" + +The convergence range table describes, for each type of connection from +presynaptic (rows) to postsynaptic (columns), the variance in frequency of +presynaptic cells relative to the postsynaptic cell. + +All values are expressed as the sigma for a lognormal distribution scaled to +the CF of the postsynaptic cell. + +*** This table is just a guess - using data from mouse... **** + +------------------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral mso +sgc 0.05 [1] 0.1 [1] 0.4 [1] 0.5 [5] 0.1 [1] 0.1 [1] 0 +bushy 0 0 0 0 0 0 0.05 [6] +dstellate 0.208 [2] 0.347 [2] 0.5 [1] 0 0.2 [1] 0.2 [1] 0 +tstellate 0.1 [4] 0.1 [4] 0 0 0 0 0 +tuberculoventral 0.069 [3] 0.111 [3] 0 0 0.15 [1] 0 0 +pyramidal 0 0 0 0 0 0 0 +-------------------------------------------------------------------------------------------------------- + +[1] Guess based on axonal / dendritic morphology. + +[2] Calculated from Campagnola & Manis 2014 fig. 7C (Using mouse data on guinea pig cells) + Distribution widths are given in stdev(octaves), so we multiply by ln(2) to + get the sigma for a lognormal distribution. + DS->Bushy: ln(2) * 0.3 = 0.208 + DS->TStellate: ln(2) * 0.5 = 0.347 + +[3] Calculated from Campagnola & Manis 2014 fig. 9C (Using mouse data on guinea pig cells) + Distribution widths are given in stdev(octaves), so we multiply by ln(2) to + get the sigma for a lognormal distribution. + TV->Bushy: ln(2) * 0.10 = 0.069 + TV->TStellate: ln(2) * 0.16 = 0.111 + +[4] Guess based on very limited information in Campagnola & Manis 2014 fig. 12 + +[5] Octopus cells get a wide range of ANF input (but weak on a per input basis) + For example, see McGinley et al., 2012 or Spencer et al., 2012. + +[6] MSO convergence from bushy cells is a guess. + +""" + +add_table_data( + "convergence_range", + row_key="pre_type", + col_key="post_type", + species="guineapig", + data=guineapig_convergence_range, +) diff --git a/cnmodel/data/ionchannels.py b/cnmodel/data/ionchannels.py new file mode 100644 index 0000000..9691c7f --- /dev/null +++ b/cnmodel/data/ionchannels.py @@ -0,0 +1,582 @@ +# -*- encoding: utf-8 -*- +from ._db import add_table_data + +""" +Ion channel density tables +All of the ion channel densities for the models implemented in cnmodel +are (or should be) stated here, and should not be modified in the +cnmodel code itself. + +""" + +add_table_data( + "RM03_channels", + row_key="field", + col_key="model_type", + species="guineapig", + data=u""" + +This table describes the ion channel densities (and voltage shifts if necessary) +for different cell types in the original Rothman Manis 2003 model. +Data from Table 1, except for "octopus" cells, which is modified (see note 3) +map to cell: bushy-II bushy-II-I tstellate tstellate-t bushy-I-II octopus +----------------------------------------------------------------------------------------------------------------------------------- + II II-I I-c I-t I-II II-o + +nacn_gbar 1000. [1] 1000. [1] 1000. [1] 1000. [1] 1000. [2] 1000. [3] +kht_gbar 150.0 [1] 150.0 [1] 150.0 [1] 80.0 [1] 150.0 [2] 150.0 [3] +klt_gbar 200.0 [1] 35.0 [1] 0.0 [1] 0.0 [1] 20.0 [2] 1000. [3] +ka_gbar 0.0 [1] 0.0 [1] 0.0 [1] 65.0 [1] 0.0 [2] 0.0 [3] +ih_gbar 20.0 [1] 3.5 [1] 0.5 [1] 0.5 [1] 2.0 [2] 30.0 [3] +leak_gbar 2.0 [1] 2.0 [1] 2.0 [1] 2.0 [1] 2.0 [2] 2.0 [3] +leak_erev -65 [1] -65 [1] -65 [1] -65 [1] -65 [2] -65 [3] +na_type nacn [1] nacn [1] nacn [1] nacn [1] nacn [2] nacn [3] +ih_type ihvcn [1] ihvcn [1] ihvcn [1] ihvcn [1] ihvcn [2] ihvcn [3] +soma_Cap 12.0 [1] 12.0 [1] 12.0 [1] 12.0 [1] 12.0 [2] 25.0 [3] +e_k -84 [1] -84 [1] -84 [1] -84 [2] -84 [2] -84 [2] +e_na 50. [1] 50. [1] 50. [1] 50. [2] 50. [2] 50. [2] +ih_eh -43 [1] -43 [1] -43 [1] -43 [2] -43 [2] -43 [2] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Rothman and Manis, 2003 + Age "adult", Temperature=22C + Units are nS. + +[2] Rothman and manis, 2003, model I-II + Some low-voltage K current, based on observations of + a single spike near threshold and regular firing for higher + currents (Xie and Manis, 2017) + +[3] Derived from Rothman and Manis, 2003, model II + Large amounts of low-voltage K current, and elevated HCN. Conductances + based on Rothman and Manis, 2003; concept from Cao and Oertel + +[4] Designation for elevated LTK and Ih for octopus cells + +""", +) + +add_table_data( + "XM13_channels", + row_key="field", + col_key="model_type", + species="mouse", + data=u""" + +This table describes the REFERENCE ion channel densities (and voltage shifts if necessary) +for different cell types based on the Xie and Manis 2013 models for mouse. + +The REFERENCE values are applied to "point" models, and to the soma of +compartmental models. +The names of the mechanisms must match a channel mechanism (Neuron .mod files) +and the following _(gbar, vshift, etc) must match an attribute of that channel +that can be accessed. + +----------------------------------------------------------------------------------------------------------------------------------- + II II-I I-c I-II I-t + +nav11_gbar 0000. [4] 0000. [4] 000. [4] 0. [4] 3000. [4] +nacn_gbar 1000. [1] 1000. [1] 3000. [1] 0000. [2] 0000. [1] +na_gbar 1000. [1] 1000. [1] 3000. [1] 1800. [2] 0000. [1] +kht_gbar 58.0 [1] 58.0 [1] 500.0 [1] 150.0 [2] 500.0 [1] +klt_gbar 80.0 [1] 20.0 [1] 0.0 [1] 14.0 [3] 0.0 [1] +ka_gbar 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [2] 125.0 [1] +ihvcn_gbar 30.0 [1] 30.0 [1] 18.0 [1] 2.0 [2] 18.0 [1] +leak_gbar 2.0 [1] 2.0 [1] 8.0 [1] 2.0 [2] 8.0 [1] +leak_erev -65 [1] -65 [1] -65 [1] -65 [2] -65 [1] +na_type nacn [1] nav11 [1] nacn [1] na [3] nav11 [1] +ih_type ihvcn [1] ihvcn [1] ihvcn [1] ihvcn [2] ihvcn [1] +soma_Cap 26.0 [1] 26.0 [1] 25.0 [1] 25.0 [2] 25.0 [1] +nav11_vshift 4.3 [1] 4.3 [1] 4.3 [1] 4.3 [1] 4.3 [1] +e_k -84 [1] -84 [1] -84 [1] -70 [3] -84 [1] +e_na 50. [1] 50. [1] 50. [1] 55. [3] 50. [1] +ih_eh -43 [1] -43 [1] -43 [1] -43 [2] -43 [1] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Uses channels from Rothman and Manis, 2003 + Conductances are for Mouse bushy cells + Xie and Manis, 2013 + Age "adult", Temperature=34C + Units are nS. + +[2] Rothman and manis, 2003, model I-II + Some low-voltage K current, based on observations of + a single spike near threshold and regular firing for higher + currents (Xie and Manis, 2017) + +[3] These values for the I-II (dstellate) are from the original checkpoint test + for cnmodel 12/2017. + +[4] nav11 channels were used in original Xie and Manis (2013) ms, but are not + used for mice in the master distribution of cnmodel, which used only the nacn + channels. + +""", +) + +add_table_data( + "XM13_channels_compartments", + row_key="parameter", + col_key="compartment", + species="mouse", + model_type="II", + data=u""" + +This table describes the ion channel densities relative to somatic densities, +e.g., relative to REFERENCE densities in the table XM13_channels. +and voltage shifts, for different compartments of the specified neuron, +Conductances will be calculated from the Model derived from Xie and Manis 2013 for mouse +(data table: mGVC_channels). + +------------------------------------------------------------------------------------------------------------------------------------------------------------------ + axon unmyelinatedaxon myelinatedaxon initialsegment hillock soma dendrite primarydendrite secondarydendrite + +nav11_gbar 3.0 [1] 3.0 [1] 0.0 [1] 5.0 [1] 5.0 [1] 1.0 [1] 0.5 [1] 0.50 [1] 0.25 [1] +kht_gbar 1.0 [1] 2.0 [1] 0.01 [1] 2.0 [1] 2.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.25 [1] +klt_gbar 1.0 [1] 1.0 [1] 0.01 [1] 1.0 [1] 1.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.25 [1] +ihvcn_gbar 0.0 [1] 0.0 [1] 0.0 [1] 0.5 [1] 0.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.5 [1] +leak_gbar 1.0 [1] 0.25 [1] 0.25e-3 [1] 1.0 [1] 1.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.5 [1] +leak_erev -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] +nav11_vshift 4.3 [1] 4.3 [1] 0.0 [1] 4.3 [1] 4.3 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] +na_type nav11 nav11 nav11 nav11 nav11 nav11 nav11 nav11 nav11 +ih_type ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +[1] Scaling is relative to soma scaling. Numbers are estimates based on general distribution from literature on cortical neurons. + + +""", +) + + +# ***** BEGINNING OF XM13_Channels for nacncoop version of model + + +add_table_data( + "XM13nacncoop_channels", + row_key="field", + col_key="model_type", + species="mouse", + data=u""" + +This table describes the REFERENCE ion channel densities (and voltage shifts if necessary) +for different cell types based on the Xie and Manis 2013 models for mouse, but using +the nacncoop mechanism (coooperative sodium channels) + +!!!!!!!!!!!! USAGE OF THIS TABLE SHOULD BE CONSIDERED EXPERIMENTAL !!!!!!!!!!!!!! + +The REFERENCE values are applied to "point" models, and to the soma of +compartmental models. +The names of the mechanisms must match a channel mechanism (Neuron .mod files) +and the following _(gbar, vshift, etc) must match an attribute of that channel +that can be accessed. + +----------------------------------------------------------------------------------------------------------------------------------- + II II-I I-c I-II I-t + +nacncoop_gbar 3000. [4] 1000. [4] 1000. [4] 1000. [4] 1000. [4] +kht_gbar 58.0 [1] 58.0 [1] 500.0 [1] 150.0 [2] 500.0 [1] +klt_gbar 80.0 [1] 20.0 [1] 0.0 [1] 14.0 [3] 0.0 [1] +ka_gbar 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [2] 125.0 [1] +ihvcn_gbar 30.0 [1] 30.0 [1] 18.0 [1] 2.0 [2] 18.0 [1] +leak_gbar 2.0 [1] 2.0 [1] 8.0 [1] 2.0 [2] 8.0 [1] +leak_erev -65 [1] -65 [1] -65 [1] -65 [2] -65 [1] +na_type nacncoop [1] nacncoop [1] nacncoop [1] nacncoop [3] nacncoop [1] +ih_type ihvcn [1] ihvcn [1] ihvcn [1] ihvcn [2] ihvcn [1] +soma_Cap 26.0 [1] 26.0 [1] 25.0 [1] 25.0 [2] 25.0 [1] +nacncoop_vshift 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] +e_k -84 [1] -84 [1] -84 [1] -70 [3] -84 [1] +e_na 50. [1] 50. [1] 50. [1] 55. [3] 50. [1] +ih_eh -43 [1] -43 [1] -43 [1] -43 [2] -43 [1] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Uses channels from Xie and Manis, 2013 + Age "adult", Temperature=34C + Units are nS. + +[2] Rothman and manis, 2003, model I-II + Some low-voltage K current, based on observations of + a single spike near threshold and regular firing for higher + currents (Xie and Manis, 2017) + +[3] These values for the I-II (dstellate) are from the original checkpoint test + for cnmodel 12/2017. + +[4] nav11 channels were used in original Xie and Manis (2013) ms, + However, this version uses cooperative na channels for faster activation + +""", +) + +add_table_data( + "XM13nacncooop_channels_compartments", + row_key="parameter", + col_key="compartment", + species="mouse", + model_type="II", + data=u""" + +!!!!!!!!!!!! USAGE OF THIS TABLE SHOULD BE CONSIDERED EXPERIMENTAL !!!!!!!!!!!!!! + +This table describes the ion channel densities relative to somatic densities, +e.g., relative to REFERENCE densities in the table XM13_nacncoop_channels. +and voltage shifts, for different compartments of the specified neuron, +Conductances will be calculated from the Model derived from Xie and Manis 2013 for mouse + +------------------------------------------------------------------------------------------------------------------------------------------------------------------ + axon unmyelinatedaxon myelinatedaxon initialsegment hillock soma dendrite primarydendrite secondarydendrite + +nacncoop_gbar 3.0 [1] 3.0 [1] 0.0 [1] 5.0 [1] 5.0 [1] 1.0 [1] 0.5 [1] 0.50 [1] 0.25 [1] +kht_gbar 1.0 [1] 2.0 [1] 0.01 [1] 2.0 [1] 2.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.25 [1] +klt_gbar 1.0 [1] 1.0 [1] 0.01 [1] 1.0 [1] 1.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.25 [1] +ihvcn_gbar 0.0 [1] 0.0 [1] 0.0 [1] 0.5 [1] 0.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.5 [1] +leak_gbar 1.0 [1] 0.25 [1] 0.25e-3 [1] 1.0 [1] 1.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.5 [1] +leak_erev -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] +nacncoop_vshift 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] +na_type nacncoop nacncoop nacncoop nacncoop nacncoop nacncoop nacncoop nacncoop nacncoop +ih_type ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +[1] Scaling is relative to soma scaling. Numbers are estimates based on general distribution from literature on cortical neurons. + + +""", +) + +# ***** END OF XM13_Channels for nacncoop version of model + +add_table_data( + "mGBC_channels", + row_key="field", + col_key="model_type", + species="mouse", + data=u""" + +This table describes the REFERENCE ion channel densities (and voltage shifts if necessary) +for different cell types based on the Xie and Manis 2013 models for mouse. + +The REFERENCE values are applied to "point" models, and to the soma of +compartmental models. +The names of the mechanisms must match a channel mechanism (Neuron .mod files) +and the following _(gbar, vshift, etc) must match an attribute of that channel +that can be accessed. + +----------------------------------------------------------------------------------------------------------------------------------- + II II-I I-c I-II I-t + +nav11_gbar 1600. [1] 1600. [1] 3000. [1] 1600. [2] 3000. [1] +kht_gbar 58.0 [1] 58.0 [1] 500.0 [1] 150.0 [2] 500.0 [1] +klt_gbar 80.0 [1] 14.0 [1] 0.0 [1] 20.0 [2] 0.0 [1] +ka_gbar 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [2] 125.0 [1] +ihvcn_gbar 30.0 [1] 30.0 [1] 18.0 [1] 2.0 [2] 18.0 [1] +leak_gbar 2.0 [1] 2.0 [1] 8.0 [1] 2.0 [2] 8.0 [1] +leak_erev -65 [1] -65 [1] -65 [1] -65 [2] -65 [1] +na_type nav11 [1] nav11 [1] nav11 [1] nav11 [1] nav11 [1] +ih_type ihvcn [1] ihvcn [1] ihvcn [1] ihvcn [2] ihvcn [1] +soma_Cap 26.0 [1] 26.0 [1] 25.0 [1] 26.0 [2] 25.0 [1] +nav11_vshift 4.3 [1] 4.3 [1] 4.3 [1] 4.3 [1] 4.3 [1] +e_k -84 [1] -84 [1] -84 [1] -84 [2] -84 [1] +e_na 50. [1] 50. [1] 50. [1] 50. [2] 50. [1] +ih_eh -43 [1] -43 [1] -43 [1] -43 [2] -43 [1] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Uses channels from Rothman and Manis, 2003, except for Na channels + Conductances are for Mouse bushy cells + Xie and Manis, 2013 + Age "adult", Temperature=34C + Units are nS. + +[2] Rothman and Manis, 2003, model I-II + Some low-voltage K current, based on observations of + a single spike near threshold and regular firing for higher + currents (Xie and Manis, 2017) + + +""", +) + + +add_table_data( + "mGBC_channels_compartments", + row_key="parameter", + col_key="compartment", + species="mouse", + model_type="II", + data=u""" + +This table describes the ion channel densities relative to somatic densities, +e.g., relative to REFERENCE densities in the table XM13_channels. +and voltage shifts, for different compartments of the specified neuron, +Conductances will be calculated from the Model for Xie and Manis 2013 for mouse +(data table: XM13_channels). + +------------------------------------------------------------------------------------------------------------------------------------------------------------------ + axon unmyelinatedaxon myelinatedaxon initialsegment hillock soma dendrite primarydendrite secondarydendrite + +nav11_gbar 3.0 [1] 3.0 [1] 0.0 [1] 3.0 [1] 2.0 [1] 1.0 [1] 0.25 [1] 0.25 [1] 0.25 [1] +kht_gbar 1.0 [1] 2.0 [1] 0.01 [1] 2.0 [1] 2.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.25 [1] +klt_gbar 1.0 [1] 1.0 [1] 0.01 [1] 1.0 [1] 1.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.25 [1] +ihvcn_gbar 0.0 [1] 0.0 [1] 0.0 [1] 0.5 [1] 0.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.5 [1] +leak_gbar 1.0 [1] 0.25 [1] 0.25e-3 [1] 1.0 [1] 1.0 [1] 1.0 [1] 0.5 [1] 0.5 [1] 0.5 [1] +leak_erev -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] -65. [1] +nav11_vshift 4.3 [1] 4.3 [1] 0.0 [1] 4.3 [1] 4.3 [1] 0.0 [1] 0.0 [1] 0.0 [1] 0.0 [1] +na_type nav11 nav11 nav11 nav11 nav11 nav11 nav11 nav11 nav11 +ih_type ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn ihvcn +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +[1] Scaling is relative to soma scaling. Numbers are estimates based on general distribution from literature on cortical neurons. + + +""", +) + + +add_table_data( + "POK_channels", + row_key="field", + col_key="cell_type", + species="rat", + data=u""" + +This table describes the ion channel densities and voltage shifts for rat DCN pyramidal cells, +from Kanold and Manis, 2001 + +------------------------------------------------------------------------------------------------------------------------------------------ + pyramidal + +soma_napyr_gbar 350.0 [1] +soma_nap_gbar 0. +soma_kdpyr_gbar 80.0 [1] +soma_kcnq_gbar 0. +soma_kif_gbar 150.0 [1] +soma_kis_gbar 40.0 [1] +soma_ihpyr_gbar 2.8 [1] +soma_leak_gbar 2.8 [1] +soma_leak_erev -62.0 [1] +soma_e_na 50. [1] +soma_e_k -81.5 [1] +soma_e_h -43.0 [1] +soma_natype napyr +soma_Cap 12 [1] +------------------------------------------------------------------------------------------------------------------------------------------ + +[1] Kanold and Manis, 1999, 2001, 2005 + Age P11-14, Temperature=22C + Units are nS. +[2] Adjustable q10 added for fitting + soma_ihpyr_adj_q10 1.0 [2] (removed for testing) + +""", +) + +add_table_data( + "CW_channels", + row_key="field", + col_key="cell_type", + species="mouse", + data=u""" + +This table describes the ion channel densities and voltage shifts +for a mouse carthweel cell model. +Ad-hoc model, based on a Purkinje cell model (ref [1]). + + +----------------------------------------------------------------------------------------------------------------------------------- + cartwheel + +soma_narsg_gbar 500.0 [1] +soma_bkpkj_gbar 2.0 +soma_kpkj_gbar 100. [1] +soma_kpkj2_gbar 50. +soma_kpkjslow_gbar 150 [1] +soma_kpksk_gbar 25.0 [1] +soma_lkpkj_gbar 5.0 [1] +soma_hpkj_gbar 5.0 [1] +soma_e_na 50. [1] +soma_e_k -80.0 [1] +soma_hpkj_eh -43.0 [1] +soma_lkpkj_e -65.0 [1] +soma_e_ca 50. +soma_na_type narsg +soma_pcabar 0.00015 [1] +soma_Dia 18 + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Channels from Khaliq, Gouwens and Raman, J. Neurosci. 2003 + Conductance levels modified. + +""", +) + +add_table_data( + "TV_channels", + row_key="field", + col_key="cell_type", + species="mouse", + data=u""" + +This table describes the ion channel densities and voltage shifts +for a mouse tuberculoventral cell model. +Ad-hoc model, based on the t-stellate cell model, but adjusted +to match the data from Kuo and Trussell. + +----------------------------------------------------------------------------------------------------------------------------------- + TVmouse + +soma_nacncoop_gbar 5800.0 [2] +soma_kht_gbar 400.0 [1] +soma_ihvcn_gbar 2.5 [2] +soma_ka_gbar 65.0 [1] +soma_leak_gbar 4.5 [1] +soma_leak_erev -72.0 [1] +soma_e_na 50. [1] +soma_e_k -81.5 [1] +soma_ihvcn_eh -43.0 [1] +soma_na_type nacncoop [2] +soma_Cap 35 [1] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Values obtained from brute force runs and comparision to + FI curve from Kuo, Lu and Trussell, J Neurophysiol. 2012 Aug 15; + 108(4): 1186–1198. + +[2] Cooperative sodium channel model, based on (see the mechanisms folder) + concepts and implementation similar to Oz et al. J.Comp. Neurosci. 39: 63, 2015, + and Huang et al., PloSOne 7:e37729, 2012. + + +""", +) + +add_table_data( + "sgc_mouse_channels", + row_key="field", + col_key="cell_type", + species="mouse", + data=u""" + +This table describes the ion channel densities (and voltage shifts if necessary) +for SGC cells, based on + +----------------------------------------------------------------------------------------------------------------------------------- + sgc-a sgc-bm + +sgc_name a bm +soma_na_gbar 350. [2] 350. [2] +soma_kht_gbar 58.0 [1] 58.0 [1] +soma_klt_gbar 80.0 [1] 80.0 [1] +soma_ihap_gbar 3.0 [3] 0.0 [1] +soma_ihap_eh -41.0 [3] -41.0 [3] +soma_ihbm_gbar 0.0 [3] 3.0 [3] +soma_ihbm_eh -41.0 [3] -41.0 [3] +soma_leak_gbar 2.0 [1] 2.0 [1] +soma_leak_erev -65 [1] -65 [1] +soma_na_type jsrna [2] jsrna [2] +soma_Cap 12.0 [1] 12.0 [1] +soma_e_k -84 [1] -84 [1] +soma_e_na 50. [1] 50. [1] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Model is based on the mouse bushy cell model (XM13, above), + but with a fast sodium channel from Rothman et al, 1993. and Ih currents + from Liu et al. 2014 + +[2] Sodium channel from Rothman, Young and Manis, J Neurophysiol. 1993 Dec;70(6):2562-83. + +[3] Ih Currents from Liu, Manis, Davis, J Assoc Res Otolaryngol. 2014 Aug;15(4):585-99. + doi: 10.1007/s10162-014-0446-z. Epub 2014 Feb 21. + Age "P10" (cultured SGC cells), Original data temperature=22C. + Units are nS. + +""", +) + + +add_table_data( + "sgc_guineapig_channels", + row_key="field", + col_key="cell_type", + species="guineapig", + data=u""" + +This table describes the ion channel densities (and voltage shifts if necessary) +for a model SGC cell, which is based on a bushy cell with a different Na channel. + +----------------------------------------------------------------------------------------------------------------------------------- + sgc-a sgc-bm + +sgc_name a bm +soma_na_gbar 1000. [2] 1000. [2] +soma_kht_gbar 150.0 [1] 150.0 [1] +soma_klt_gbar 200.0 [1] 200.0 [1] +soma_ihap_gbar 3.0 [3] 0.0 [3] +soma_ihap_eh -41.0 [3] -41.0 [3] +soma_ihbm_gbar 0.0 [3] 3.0 [3] +soma_ihbm_eh -41.0 [3] -41.0 [3] +soma_leak_gbar 2.0 [1] 2.0 [1] +soma_leak_erev -65 [1] -65 [1] +soma_na_type jsrna [2] jsrna [2] +soma_Cap 12.0 [1] 12.0 [1] +soma_e_k -84 [1] -84 [1] +soma_e_na 50. [1] 50. [1] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Model is based on the guinea pig bushy cell model (RM03, above), + but with a fast sodium channel from Rothman et al, 1993. and Ih currents + from Liu et al. 2014 + +[2] Sodium channel from Rothman, Young and Manis, J Neurophysiol. 1993 Dec;70(6):2562-83. + +[3] Ih Currents from Liu, Manis, Davis, J Assoc Res Otolaryngol. 2014 Aug;15(4):585-99. + doi: 10.1007/s10162-014-0446-z. Epub 2014 Feb 21. + Age "P10" (cultured SGC cells), Temperature=22C. + Units are nS. + +""", +) + +add_table_data( + "MSO_principal_channels", + row_key="field", + col_key="cell_type", + species="guineapig", + data=u""" + +This table describes the ion channel densities +for a putative MSO principal neuron based on the original Rothman Manis 2003 model for bushy cells. + +----------------------------------------------------------------------------------------------------------------------------------- + MSO-principal + +MSO_name Principal +soma_na_gbar 1000. [1] +soma_kht_gbar 150.0 [1] +soma_klt_gbar 200.0 [1] +soma_ka_gbar 0.0 [1] +soma_ih_gbar 20.0 [1] +soma_leak_gbar 2.0 [1] +soma_leak_erev -65 [1] +soma_na_type nacn [1] +soma_ih_type ihvcn [1] +soma_Cap 12.0 [1] +soma_e_k -84 [1] +soma_e_na 50. [1] +soma_ih_eh -43 [1] + +----------------------------------------------------------------------------------------------------------------------------------- + +[1] This MSO neuron model is basied on Rothman and Manis, 2003 bushy cell, type II + Age "adult", Temperature=22C + Units are nS. + + +""", +) diff --git a/cnmodel/data/populations.py b/cnmodel/data/populations.py new file mode 100644 index 0000000..35d60e8 --- /dev/null +++ b/cnmodel/data/populations.py @@ -0,0 +1,37 @@ +# -*- encoding: utf-8 -*- +from ._db import add_table_data + +add_table_data( + "populations", + row_key="field", + col_key="cell_type", + species="mouse", + data=u""" + +----------------------------------------------------------------------------------------------------- + sgc bushy tstellate dstellate octopus pyramidal tuberculoventral + +n_cells 10000 [1] 6500 [2] 6500 [2] 650 [3] 5000 3000 5000 +cf_min 2000 2000 2000 2000 2000 2000 2000 +cf_max 90000 90000 90000 90000 90000 90000 90000 +----------------------------------------------------------------------------------------------------- + +[1] ? + +[2] Rough estimate from allen brain atlas data: + Volume of VCN is 0.377 mm^3, by counting voxels with 'VCO' (101) label in Common Coordinate Framework atlas. + 753370 voxels * 0.5 * 10e-6**3 m^3/vox = 0.377 mm^3 + Counted Slc17a7 (pan-excitatory) cell bodies in a 500x500 um chunk of VCN + http://mouse.brain-map.org/experiment/siv?id=69014470&imageId=68856767&initImage=ish&coordSystem=pixel&x=7616.5&y=4144.5&z=1 + 266 cells in 500x500 um = 34707 cells / mm^2 + 34707**3/2 * 0.377 mm^3 = 13084 cells total + Assume half are bushy, half are T-stellate + +[3] Rough estimate from allen brain atlas data: + Similar to [2], using Gad1 inhibitory marker + http://mouse.brain-map.org/experiment/siv?id=75492764&imageId=75405134&initImage=ish&coordSystem=pixel&x=5320.5&y=3232.5&z=1 + 36 cells in 500x500 um = 144e6 / m^2 ~= 1728 / mm^2 + = 651 cells total (VCN, unilateral) + +""", +) diff --git a/cnmodel/data/synapses.py b/cnmodel/data/synapses.py new file mode 100644 index 0000000..5db3235 --- /dev/null +++ b/cnmodel/data/synapses.py @@ -0,0 +1,503 @@ +# -*- encoding: utf-8 -*- +from ._db import add_table_data + +# sgc old weights: +# bushy tstellate dstellate octopus pyramidal tuberculoventral +# weight 0.027 [12] 0.006 [12] 0.00064 [12] 0.0011 [12] 0.0023 [12] 0.0029 [12] +# tau1 0.1 [5] 0.1 [5] 0.2 [5] 0.1 [5] 0.1 [5] 0.1 [5] +# tau2 0.3 [5] 0.3 [5] 0.5 [5] 0.3 [5] 0.3 [5] 0.3 [5] +# erev 0 [5] 0 [5] 0 [5] 0 [5] 0 [5] 0 [5] + +add_table_data( + "sgc_synapse", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +AMPA_gmax and NMDA_gmax are the estimated average peak conductances (in nS) +resulting from an action potential in a single auditory nerve terminal, under +conditions that minimize the effects of short-term plasticity. +AMPA_gmax are from values measured at -65 mV (or -70mV), and represent SINGLE TERMINAL +conductances +AMPAR_gmax are the individual synapse postsynaptic conductance +NMDA_gmax values are taken as the fraction of the current that is NMDAR dependent +at +40 mV (see below) + +n_rsites is the number of release sites per SGC terminal. + +--------------------------------------------------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral cartwheel + +AMPA_gmax 21.05±15.4 [1] 4.6±3.1 [2] 0.49±0.29 [7] 0.87±0.23 [3] 0.6±0.3 [8] 2.2±1.5 [8] 0 +AMPAR_gmax 4.6516398 [10] 4.632848 [10] 1.7587450 [10] 16.975147 [10] 0.9 [8] 2.2 [8] 0 +NMDA_gmax 10.8±4.6 [1] 2.4±1.6 [2] 0.552±0.322 [7] 0.17±0.046 [3] 0.4±0.33 [8] 2.4±1.6 [8] 0 +NMDAR_gmax 0.4531933 [10] 1.2127097 [10] 0.9960820 [10] 0.6562702 [10] 0.2 [8] 1.2127097 [8] 0 +NMDAR_vsh -15.0 [12] -15.0 [12] -15.0 [12] -15.0 [12] -15.0 [12] -15.0 [12] 0 +NMDAR_vshift 0.0 [12] 0.0 [12] 0.0 [12] 0.0 [12] 0.0 [12] 0.0 [12] 0 +EPSC_cv 0.12 [8] 0.499759 [9] 0.886406 [9] 1.393382 [9] 0.499 [8] 0.499 [8] 0 +Pr 1.000 [11] 1.000 [11] 1.000 [11] 1.000 [11] 1.000 [8] 1.000 [8] 0 +n_rsites 100 [5] 4 [6] 1 [4] 1 [4] 2 [8] 2 [8] 0 +delay 0.600 0.600 0.600 0.600 0.600 0.600 0 +weight 0.020377 0.003679 0.000457 0.001311 0.000327 0.000808 0 +tau1 0.158 0.174 0.152 0.125 0.167 0.157 0 +tau2 0.246 1.501 1.652 0.251 1.489 1.641 0 +erev 0.0 0.0 0.0 0.0 0.0 0.0 0 +---------------------------------------------------------------------------------------------------------------------------------------- + +[1] Derived from Cao, X. & Oertel, D. (2010). Single-terminal conductance was + reported as 21.5±15.4 nS (1.4±1.0 nA at -65 mV). The ratio of NMDA current to + total current is 0.3, so AMPA and NMDA currents are: + AMPA_gmax = 21.5±15.4 nS (measured at -65 mV) + NMDA_gmax = 21.5±15.4 nS * 0.3 = 10.8±4.6 nS + Age>p17, Temperature=33C, [Mg2+]=1.3mM, [Ca2+]=2.4mM + Units are nS. + See also Pliss et al., J. Neurophys., 2009 (and note [12]) + +[2] Derived from Cao, X. & Oertel, D. (2010). Single-terminal conductance was + estimated as 4.6±3.1 nS. The ratio of NMDA current to + total current is 0.53, so AMPA and NMDA currents are: + AMPA_gmax = 4.6±3.1 nS + NMDA_gmax = 4.6±3.1 nS * 0.53 = 2.4±1.6 nS + Estimated number of inputs per AN fiber: + 0.3 nA step, 0.08 nA mini size = ~ 4 inputs per AN fiber + Age>p17, Temperature=33C, [Mg2+]=1.3mM, [Ca2+]=2.4mM + Units are nS + +[3] Derived from Cao, X. & Oertel, D. (2010). Single-terminal conductance was + estimated as 52±14 nS / 60 = 0.87±0.23 nS. The ratio of NMDA current to + total current is 0.2, so AMPA and NMDA currents are: + AMPA_gmax = 0.87±0.23 nS + NMDA_gmax = 0.87±0.23 nS * 0.2 = 0.17±0.046 nS + Age>p17, Temperature=33C, [Mg2+]=1.3mM, [Ca2+]=2.4mM + Units are nS + +[4] Assumption based on mini size and lack of discernable EPSC step (guess). + Should be verified. + +[5] Oleskevich & Walmsley ~2002, Wang & Manis 2005. Units are nS + +[6] A value of 45 would be chosen to satisfy the CV of EPSC amplitude determined in [9]. + However, those measures are for simultaneous stimulation of multiple AN fibers. + A value of 4 is included here to correspond to measures in Cao and Oertel (2010) + (see note [2]) + +[7] (Xie and Manis, Frontiers in Neural Circuits, 2017): + Measurements from CBA/CaJ mouse "radiate" multipolar cells in the AVCN. + Single terminal conductance = (1.2 ± 0.70 nA/70 mV)/ 35 inputs = 0.490 ± 0.286 nS + (see connections.py) + Single terminal conductance from mini = 34 pA/70 mV = 0.486 nS (single mini) + Assume same AMPA/NMDA ratio as tstellate cells, but measures made where NMDA = 0 + (at negative V): + AMPA_gmax = 0.490±0.286 nS + NMDA_gmax = 0.490±0.286 nS * 0.53/0.47 = 0.552±0.322 nS + Age > P35, Temperature=34C, [Mg2+]=1.5mM, [Ca2+]=2.5mM + +[8] Thin air. These are for testing the software, not necessarily for performing + real simulations. Note: Pyramidal cell strength has been reduced + because of large convergence and high input resistance of the reference cell model. + Release 1 (Nov 2017): + pyramidal + + 0.6 ±1.05 [8] + 1.8 [8] + 0.8±0.66 [8] + 0.4 [8] + -15.0 [12] + 0.499 [8] + 1.000 [8] + 2 [8] + + +[9] Reanalysis of evoked EPSCs in stellate cells (Manis/Xie, 2014) + +[10] Maximum AMPA open conductance per synaptic site (units are pS). + These values are calculated by running python cnmodel/synapses/tests/test_psd.py + for a specific cell type (if the cell uses the receptor mechanisms; this is + not necessary for simple exp2syn style mechanisms) + to ensure that maximum AMPA conductance during PSG matches [1, 2 or 3] + For a bushy cell, the original default values (bushy cell) were: + AMPAR_gmax 3.314707700918133 + NMDAR_gmax 0.4531929783503451 + These values will also depend on the number of release sites per + synapse (the total conductance is produce of site gmax and nsites). + + A note on the precision of these values: This precision is only + required for the tests of the model, as a way of ensuring numerical + equivalency after potential modifications of the code. The precision + of the value is in no way intended to specificy biological precision. + + For example, a change in the rate constants in the AMPA_Trussell AMPA + receptor model could (and probably would) change the open probability, + and therefore the maximal conductance of an EPSC. However, as this is + only a representation of the EPSC, the "receptor" conductance should + be scaled so that the computed EPSC has the same maximal conductance + as prior to the kinetic modifications. Because the receptor model is + numerically computed (and not analytically tractable without + additional knowledge of the ligand time course), a numerical solution + is required. + +[11] Pr is the initial release probability. The value can be computed by + setting Pr to 1 in this file, and running the cnmodel test_synapses.py + with the appropriate presynaptic source and postsynaptic target, + once all other parameters are set. The Pr is used to rescale + the AMPAR_gmax so that the total current matches the data in + AMPA_gmax in the table (on average). + +[12] NMDA_vshift is the voltage shift for the activation of the NMDAR's, relative + to 0 (standard in the NMDA_Kampa model). A negative value shifts the voltage + dependence to the right (depolarizing). + The value of the shift here (-15 mV) was chosen based on an exploration + of fitting functions against the NMDA-Kampa IV curve in an SGC-bushy cell + model, and comparing them against data. The functions were the modified + Woodhull function and a Boltzmann function, yielding values of 1.19 mM for + k0 and 0.78 for delta (tau decay at +40 mV of 16.4 ms), and Vr -3 mV, Vh + 16 mV for the Boltzmann fit. These are close to the values reported in + for NMDA currents in p14-p26 CBA/CaJ mice in Pliss et al. (J. Neurophys. + 102, 2627, 2009). Note: Pliss et al. agree with Cao and Oertel regarding + an approximate 10-fold difference between AMPA and NMDA conductance in + mouse bushy cells. An exact fit was not obtained, but no other parameters + of the NMDA_Kampa model were changed. + +[13] weight is the weight to use in a netcon object (NEURON) for "simple" + synapses based on the exp2syn mechanism. + Parameters Weight, tau1, tau2, delay and erev from comare_simple_multisynapses + run and curve fitting (all cells) + +""", +) + + +add_table_data( + "sgc_ampa_kinetics", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" +AMPA receptor kinetic values obtained by fitting the model of Raman and +Trussell (1992) to measured EPSCs in the mouse VCN. + +Ro1, Ro2, Rc1, Rc2, and PA are kinetic constants affecting the AMPA receptor +mechanism. tau_g and A affect the speed and amplitude of transmitter release +(implemented in the presynaptic release mechanism). +These parameters were selected to fit the model output to known EPSC shapes. + +PA is a polyamine block parameter ued in the AMPAR mechanism (concentration in micromolar). + +------------------------------------------------------------------------------------------------ + bushy tstellate dstellate pyramidal octopus tuberculoventral mso + +Ro1 107.85 [4] 39.25 [4] 39.25 [7] 39.25 [4] 107.85 [5] 39.25 [7] 107.85 [4] +Ro2 0.6193 [4] 4.40 [4] 4.40 [7] 4.40 [4] 0.6193 [5] 4.40 [7] 0.6193 [4] +Rc1 3.678 [4] 0.667 [4] 0.667 [7] 0.667 [4] 3.678 [5] 0.667 [7] 3.678 [4] +Rc2 0.3212 [4] 0.237 [4] 0.237 [7] 0.237 [4] 0.3212 [5] 0.237 [7] 0.3212 [4] +tau_g 0.10 [4] 0.25 [4] 0.25 [7] 0.25 [4] 0.10 [5] 0.25 [4] 0.10 [4] +amp_g 0.770 [4] 1.56625 [4] 1.56625 [7] 1.56625 [4] 0.770 [5] 1.56625 [4] 0.770 [4] + +PA 45 [12] 0.1 [12] 0.1 [7] 0.1 [12] 45 [5] 0.1 [7] 45 [12] + +------------------------------------------------------------------------------------------------ + +[4] Xie & Manis 2013, Table 2 + +[5] copied from bushy cells; no direct data. + +[7] Data copied from t-stellate column (no literature on these cells). Unpublished data suggests these + should be slightly different, but is complicated by electrotonically distant synaptic sites that + preclude accurate measurement of kinetics. + +[12] Wang & Manis (unpublished) + +""", +) + + +add_table_data( + "sgc_epsp_kinetics", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +EPSC shape parameters obtained from fits of Xie & Manis 2013 Equation 3 to measured EPSCs. + +------------------------------------------------------------------------------------------------ + bushy tstellate dstellate pyramidal octopus tuberculoventral + +tau_r 0.253 [11] 0.19 [11] 0.253 [13] +tau_f 0.16 [11] 1.073 [11] 0.16 [13] +tau_s 0.765 [11] 3.3082 [11] 0.765 [13] +F 0.984 [11] 0.917 [11] 0.984 [13] + +------------------------------------------------------------------------------------------------ + +[11] Xie & Manis 2013, Table 3 +[13] Copied from bushy cells; no direct data + +""", +) + + +add_table_data( + "sgc_release_dynamics", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +Kinetic parameters correspond to variables as described by Dittman et al. +(2000), their Table 1. + +F: ~ Resting release probability + +--------------------------------------------------------------------------------------------------------------- + bushy tstellate dstellate pyramidal octopus tuberculoventral + +F 0.29366 [1] 0.43435 [1] 0.43435 [2] 0.43435 [1] 0.29366 [14] 0.43435 [1] +k0 0.52313 [1] 0.06717 [1] 0.06717 [2] 0.06717 [1] 0.52313 [14] 0.06717 [1] +kmax 19.33805 [1] 52.82713 [1] 52.82713 [2] 52.82713 [1] 19.33805 [14] 52.82713 [1] +kd 0.11283 [1] 0.08209 [1] 0.08209 [2] 0.08209 [1] 0.11283 [14] 0.08209 [1] +ks 11.531 [1] 14.24460 [1] 14.24460 [2] 14.24460 [1] 11.531 [14] 14.24460 [1] +kf 17.78 [1] 18.16292 [1] 18.16292 [2] 18.16292 [1] 17.78 [14] 18.16292 [1] +taud 15.16 [1] 3.98 [1] 3.98 [2] 3.98 [1] 15.16 [14] 3.98 [1] +taus 17912.2 [1] 16917.120 [1] 16917.120 [2] 16917.120 [1] 17912.2 [14] 16917.120 [1] +tauf 9.75 [1] 11.38 [1] 11.38 [2] 11.38 [1] 9.75 [14] 11.38 [1] +dD 0.57771 [1] 2.46535 [1] 2.46535 [2] 2.46535 [1] 0.57771 [14] 2.46535 [1] +dF 0.60364 [1] 1.44543 [1] 1.44543 [2] 1.44543 [1] 0.60364 [14] 1.44543 [1] + +--------------------------------------------------------------------------------------------------------------- + +[1] Xie & Manis 2013, Table 1. Although independently measured in > P30 CBA/CaJ mice, + the values are similar to the measurements from Yang and Xu-Friedman, 2008 + in P14-P21 CBA/CaJ mice. + +[2] Data copied from t-stellate column (no literature on these cells) + +[14] Data copied from bushy cell column (no literature on these cells) +""", +) + + +add_table_data( + "gly_kinetics", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +Kinetic parameters for glycine receptor mechanisms. + +These are currently used for both DS and TV synapses, but should probably be +separated in the future. + +KV, KU, and XMax are kinetic parameters for the cleft transmitter mechanism. + + +------------------------------------------------------------------------------------------------ + bushy tstellate dstellate pyramidal tuberculoventral + +KV 1e9 [1] 531.0 [1] 531.0 [1] 531.0 [2] 531.0 [2] +KU 4.46 [1] 4.17 [1] 4.17 [1] 4.17 [2] 4.17 [2] +XMax 0.733 [1] 0.731 [1] 0.731 [1] 0.731 [2] 0.731 [2] + +------------------------------------------------------------------------------------------------ + +[1] Xie & Manis 2013 + +[2] Copied from tstellate data (Kuo et al., J. Neurophysiol. indicate glycinergic IPSCs in TV + and pyramidal cells are fast, with a decay time constant similar to that seen in tstellate + cells). In pyramidal cells, this is consistent with the brief cross-correlation tip (Voigt + and Young, 1980) and brief somatic current source (Manis and Brownell, 1983). + + +""", +) + +add_table_data( + "dstellate_synapse", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +DStellate Synapse values +gly_gmax is the default value in the program (scaled by Po for the receptors). See synapses/gly_psd.py +IPSC_cv is the coefficient of variation of the IPSC. (Not currently used in the model) +Pr is the release probabilty (not currently used); built into release mechanism for multisite synapses. +n_rsites is the number of release sites per dstellate terminal. + +--------------------------------------------------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral cartwheel + +gly_gmax 2.5 [1] 1.0 [5] 1.0 [2] 0. [2] 2.0 [3] 2.0 [3] 0±0 [2] +IPSC_cv 0.3 [3] 0.3 [3] 0.3 [3] 0.3 [3] 0.3 [3] 0.3 [3] 0.3 [3] +Pr 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] +n_rsites 10 [5] 5 [5] 5 [5] 0 [2] 5 [5] 25 [5] 0 [2] +delay 0.000 0.000 0.000 0 0.000 0.000 0 +weight 0.004131 0.004455 0.0 0 0.002228 0.012097 0 +tau1 0.187 0.152 0.152 0 0.152 0.152 0 +tau2 7.953 1.247 1.247 0 1.247 1.247 0 +erev -70.0 -70.0 -70.0 0 -70.0 -70.0 0 +--------------------------------------------------------------------------------------------------------------------------------------- + +[1] Estimate + +[2] No evidence for dstellate inputs to other d stellate cells or cartwheel cells. + Octopus cells do not get inhibitory input + +[3] Guess + +[4] Default value + +[5] Guess *educated* DS->TS from Xie and Manis, 2013. 99 pA mini @ 50 mV driving ~ 2 nS + +[6] delay from pre to post; default is 0 + +[7] Parameters Weight, tau1, tau2, delay and erev from comare_simple_multisynapses run and curve fitting (all cells) + +""", +) + + +add_table_data( + "tuberculoventral_synapse", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +Tuberculventral Synapse values +gly_gmax is the default value in the program (scaled by Po for the receptors). See synapses/gly_psd.py +IPSC_cv is the coefficient of variation of the IPSC. (Not currently used in the model) +Pr is the release probabilty (not currently used) +n_rsites is the number of release sites per tuberculoventral terminal. + +----------------------------------------------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral cartwheel + +gly_gmax 5.0 [3] 3.0 [3] 3.0 [3] 0. [2] 2.1±2.9 [6] 1.8±2.3 [6] 0±0 [6] +IPSC_cv 0.3 [3] 0.3 [3] 0.3 [3] 0.3 [3] 1.0 [3] 0.3 [3] 0.3 [3] +Pr 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] +n_rsites 6 [5] 6 [5] 0 [1] 0 [2] 6 [5] 6 [5] 6 [5] +delay 0.600 0.600 0 0 0.600 0.600 0 +weight 0.002371 0.008114 0 0 0.002705 0.002705 0 +tau1 0.190 0.149 0 0 0.149 0.149 0 +tau2 7.952 1.250 0 0 1.250 1.250 0 +erev -70.0 -70.0 0 0 -70.0 -70.0 0 +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Default value from GlyPSD + +[2] No evidence for tuberculo inputs to other d stellate cells or cartwheel cells. + Octopus cells do not get inhibitory input + +[3] Guess + +[4] Default value + +[5] Guess + +[6] Mouse data + TV conductance onto pyr cells: 2.1 nS SD 2.9 nS (Kuo et al., 2012) + TV conductance onto TV cells: 1.8 ns SD 2.3 nS. + +[7] Parameters Weight, tau1, tau2, delay and erev from comare_simple_multisynapses run and curve fitting (all cells) + Fitting done against 200 rep average for bushy, 500 rep average for all others. + +""", +) + +add_table_data( + "cartwheel_synapse", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +Cartwheel cell synapse values +gly_gmax is the default value in the program (scaled by Po for the receptors). See synapses/gly_psd.py +IPSC_cv is the coefficient of variation of the IPSC. (Not currently used in the model) +Pr is the release probabilty (not currently used) +n_rsites is the number of release sites per cartwheel cell terminal. + +----------------------------------------------------------------------------------------------------------------------------------- + bushy tstellate dstellate octopus pyramidal tuberculoventral cartwheel + +gly_gmax 0.0 [3] 0.0 [3] 0.0 [3] 0. [2] 2.1±2.9 [6] 0±0 [6] 1.8±2.3 [6] +IPSC_cv 0.3 [3] 0.3 [3] 0.3 [3] 0.3 [3] 1.0 [3] 0.3 [3] 0.3 [3] +Pr 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] 1.000 [4] +n_rsites 6 [5] 6 [5] 0 [1] 0 [2] 6 [5] 6 [5] 6 [5] +delay 0 [7] 0 0 0 0 0 0 +weight 0.01 0.01 0.01 0.0 0.01 0.01 0.01 +tau1 0.3 [5] 0.3 [5] 0.3 [5] 0.3 [5] 0.3 [5] 0.3 [5] 0.3 [5] +tau2 2.0 [5] 2.0 [5] 2.0 [5] 2.0 [5] 2.0 [5] 2.0 [5] 2.0 [5] +erev -70 [5] -70 [5] -70 [5] -70 [5] -70 [5] -70 [5] -70 [5] +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Default value from GlyPSD + +[2] No evidence for cartwheel inputs to Dstellate, bushy or tstellate cells. + Octopus cells do not get inhibitory input + +[3] Guess + +[4] Default value + +[5] Guess + +[6] Mouse data + TV conductance onto pyr cells: 2.1 nS SD 2.9 nS (Kuo et al., 2012) + TV conductance onto TV cells: 1.8 ns SD 2.3 nS. + +[7] delay from pre to post; default is just 0 + +""", +) + +add_table_data( + "bushy_synapse", + row_key="field", + col_key="post_type", + species="mouse", + data=u""" + +AMPA_gmax and NMDA_gmax are the estimated average peak conductances (in nS) +resulting from an action potential in a single presynaptic terminal under +conditions that minimize the effects of short-term plasticity. +AMPA_gmax are from values measured at -65 mV (or -70mV), and represent SINGLE TERMINAL +conductances +AMPAR_gmax are the individual synapse postsynaptic conductance +NMDA_gmax values are taken as the fraction of the current that is NMDAR dependent +at +40 mV (see below) + +n_rsites is the number of release sites per terminal. + +----------------------------------------------------------------------------------------------------------------------------------- + mso + +AMPA_gmax 21.05±15.4 [1] +AMPAR_gmax 4.6516398 [2] +NMDA_gmax 0 [3] +NMDAR_gmax 0 [3] +EPSC_cv 0.12 [4] +Pr 1.000 [5] +n_rsites 36 [6] +weight 0.01 +delay 0 +----------------------------------------------------------------------------------------------------------------------------------- + +[1] Taken from the mouse bushy cell model. + Units are nS. + +[2] See note [10] for the SGC-bushy synapse + +[3] Assume no NMDA receptors at this synapse + +[4] See SGC-bushy synapse + +[5] Just to scale with the multisite synapse model + +[6] This is a guess. + +""", +) diff --git a/cnmodel/data/tests/test_db.py b/cnmodel/data/tests/test_db.py new file mode 100644 index 0000000..5714e21 --- /dev/null +++ b/cnmodel/data/tests/test_db.py @@ -0,0 +1,72 @@ +# -*- encoding: utf-8 -*- +import pytest +import sys +from cnmodel import data + + +table = u""" + +Description of data +It has multiple lines + +And empty lines. + +------------------------------------------------------------------------------- + col1 col2 col3 +param1 15±6.5 [1] 2.2±1.5 [2] 0.87±0.23 [3] +param2 3 5 0.87±0.23 [3] +param3 3.4 7 [2] +param4 1 [12] +------------------------------------------------------------------------------- + +[1] Source 1 +[2] Multiline source + #2 + end of #2 +[3] Multiline source + #3 + end of #3 + +[12] another + source + + +""" + +data.add_table_data( + "test_data", row_key="param", col_key="col", data=table, extra="test_kwd" +) + + +def test_db(): + # this should only be a problem with Python 2, so we need to + # check which version we are running under before letting the test + # throw the exception: + if sys.version_info[0] == 2: + with pytest.raises(TypeError): + # raise exception if unicode is given in ono-unicode string + data.add_table_data("test_data", row_key="param", col_key="col", data=u"±") + + d = data.get("test_data", param="param1", col="col2", extra="test_kwd") + assert d == (2.2, 1.5) + + d = data.get( + "test_data", param="param1", col=["col1", "col2", "col3"], extra="test_kwd" + ) + assert d == {"col1": (15, 6.5), "col2": (2.2, 1.5), "col3": (0.87, 0.23)} + + d = data.get( + "test_data", param="param2", col=["col1", "col2", "col3"], extra="test_kwd" + ) + assert d == {"col1": 3, "col2": 5, "col3": (0.87, 0.23)} + + d = data.get( + "test_data", param="param3", col=["col1", "col2", "col3"], extra="test_kwd" + ) + assert d == {"col1": 3.4, "col2": None, "col3": 7} + + s = data.get_source("test_data", param="param1", col="col2", extra="test_kwd") + assert "end of #2" in s + + s = data.get_source("test_data", param="param2", col="col2", extra="test_kwd") + assert s is None diff --git a/cnmodel/decorator/__init__.py b/cnmodel/decorator/__init__.py new file mode 100755 index 0000000..5e0ec43 --- /dev/null +++ b/cnmodel/decorator/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/python +# +# Channel decorator class for cnmodel +# +# Paul B. Manis, Ph.D. January 2016 +# +from neuron import h + +from .decorator import Decorator diff --git a/cnmodel/decorator/decorator.py b/cnmodel/decorator/decorator.py new file mode 100644 index 0000000..1c2e6fc --- /dev/null +++ b/cnmodel/decorator/decorator.py @@ -0,0 +1,392 @@ +from __future__ import print_function + +__author__ = "pbmanis" + +""" +Decorator: +A class to insert biophysical mechanisms into a model. +This function attempts to automatically decorate a hoc-imported model set of sections +with appropriate conductances. + +The class takes as input the object hf, which is an instance of morphology +It also takes the cellType, a string that directs how conductances should be inserted. + +""" + +import string +import numpy as np +import cnmodel.util as nu +from cnmodel.util import Params + + +class Decorator: + def __init__(self, cell, parMap=None, verify=False): + + cellType = cell.type.lower() + self.channelInfo = Params( + newCm=1.0, + newRa=150.0, # standard value + newg_leak=0.000004935, + eK_def=-85, + eNa_def=50, + ca_init=70e-6, # free calcium in molar + v_init=-80, # mV + pharmManip={ + "TTX": False, + "ZD": False, + "Cd": False, + "DTX": False, + "TEA": False, + "XE": False, + }, + cellType=cell.status["cellClass"], + modelType=cell.status["modelType"], + modelName=cell.status["modelName"], + distanceMap=cell.hr.distanceMap, + parMap=parMap, + ) + self.excludeMechs = [] # ['ihvcn', 'kht', 'klt', 'nav11'] + print("modelType in dec: ", cell.status["modelType"]) + print("modelName in dec is ", cell.status["modelName"]) + print("cell type in dec: ", cellType) + cell.channel_manager( + modelName=cell.status["modelName"], modelType=cell.status["modelType"] + ) + # print 'Cell: \n', dir(cell) + # print 'mechanisms: ', cell.hr.mechanisms + # gmapper allows us tor remap the names of mechanisms and their conductance names, which may + # vary in the mod files. + # The versions in the mechanisms directory here have been systematized, but this + # dictionary may help when adding other conductances. + + self.gbar_mapper = { + "nacn": "gbar", + "kht": "gbar", + "klt": "gbar", + "leak": "gbar", + "ihvcn": "gbar", + "jsrna": "gbar", + "nav11": "gbar", + "nacncoop": "gbar", + "hcnobo": "gbar", + } + self.erev_mapper = { + "nacn": "ena", + "kht": "ek", + "klt": "ek", + "leak": "erev", + "ihvcn": "eh", + "jsrna": "ena", + "nav11": "ena", + "nacncoop": "ena", + "hcnobo": "eh", + } + self.vshift_mapper = { + "nacn": None, + "kht": None, + "klt": None, + "leak": None, + "ihvcn": None, + "jsrna": None, + "nav11": "vsna", + "nacncoop": "vsna", + "hcnobo": None, + } + self._biophys(cell, verify=verify) + print( + "\033[1;31;40m Decorator: Model Decorated with channels (if this appears more than once per cell, there is a problem)\033[0m" + ) + + def _biophys(self, cell, verify=False): + """ + Inputs: run parameter structure, model parameter structure + verify = True to run through the inserted mechanisms and see that they are really there. + Outputs: None + Action: Channel insertion into model + Side Effects: + Sets conductances in every different kind of section + Does not update any class variables (via self). + + original hoc code: Paul B. Manis, Ph.D. + 25 Sept. 2007 + Modified to use gca for HH formulation of calcium current + 14 Oct 2007 + converted for Python, 17 Oct 2012 (PB Manis) + modified to use new hf hoc_reader class to access section types and mechanisms 10-14 Feb 2014 pbmanis + """ + # check to see if we already did this + # createFlag = False + cellType = self.channelInfo.cellType + parMap = self.channelInfo.parMap + dmap = self.channelInfo.distanceMap + if self.channelInfo is None: + raise Exception("biophys - no parameters or info passed!") + if verify: + print( + "Biophys: Inserting channels as if cell type is {:s} with modelType {:s}".format( + cellType, self.channelInfo.modelType + ) + ) + + cell.hr.mechanisms = [] + for s in list(cell.hr.sec_groups.keys()): + sectype = self.remapSectionType(s.rsplit("[")[0]) + if sectype not in cell.channelMap.keys(): + print( + "encountered unknown section group type: %s Not decorating" + % sectype + ) + print("channels in map: ", cell.channelMap.keys()) + continue + # print 'Biophys: Section type: ', sectype, 'from: ', s + # print sectype + # print 'channel mapping keys: ', cell.channelMap[sectype].keys() + + # here we go through all themechanisms in the ionchannels table for this cell and compartment type + # note that a mechanism may have multiple parameters in the table (gbar, vshft), so we: + # a. only insert the mechanism once + # b. only adjust the relevant parameter + + for mechname in list(cell.channelMap[sectype].keys()): + mech = mechname.split("_")[0] # get the part before the _ + parameter = mechname.split("_")[1] # and the part after + if mech not in self.gbar_mapper.keys(): + print("Mechanism %s not found? " % mech) + continue + if mech in self.excludeMechs: + continue + if verify: + print( + "Biophys: section group: {:s} insert mechanism: {:s} at {:.8f}".format( + s, mech, cell.channelMap[sectype][mech] + ) + ) + if mech not in cell.hr.mechanisms: + cell.hr.mechanisms.append( + mech + ) # just add the mechanism to our list + x = nu.Mechanism(mech) + if cell.hr.sec_groups[s] == set(): + continue # no sections of this type + for sec in cell.hr.sec_groups[ + s + ]: # insert into all the sections of this type (group) + try: + x.insert_into(cell.hr.get_section(sec)) + except: + raise ValueError( + "Failed with mech: %s " % mech + ) # fail if cannot insert. + if verify: + print(" inserted %s into section " % mech, sec) + + gbar_setup = None + gbar = 0.0 + if parameter == "gbar": + gbar = self.gbarAdjust( + cell, sectype, mechname, sec + ) # map density by location/distance + gbar_setup = "%s_%s" % ( + self.gbar_mapper[mech], + mech, + ) # map name into .mod file name + # if parMap is not None and mech in parMap.keys(): # note, this allows parmap to have elements BESIDES mechanisms + # if verify: + # print 'parMap[mech]', mech, parMap[mech], gbar, + # gbar = gbar * parMap[mech] # change gbar here... + # if verify: + print("####### new gbar: ", gbar) + + vshift_setup = None + vshift = 0.0 + # print 'Parameter: ', parameter, mech, self.vshift_mapper[mech] + if parameter == "vshift" and self.vshift_mapper[mech] is not None: + vshift_setup = "%s_%s" % ( + self.vshift_mapper[mech], + mech, + ) # map voltage shift + vshift = cell.channelMap[sectype][mechname] + print( + "********* mech: gbar, vshift: ", gbar, vshift, vshift_setup + ) + exit() + + cell.hr.h.Ra = self.channelInfo.newRa + for sec in cell.hr.sec_groups[ + s + ]: # now set conductances and other parameters as requested + cell.hr.get_section(sec).Ra = self.channelInfo.newRa # set Ra here + if gbar_setup is not None: + setattr( + cell.hr.get_section(sec), gbar_setup, gbar + ) # set conductance magnitude + # print('gbar_setup: %s %s' % (sectype, gbar_setup), gbar) + if vshift_setup is not None: + try: + setattr( + cell.hr.get_section(sec), vshift_setup, vshift + ) # set conductance magnitude + except: + print(dir(cell.hr.get_section(sec))) + raise ValueError( + " cannot set mechanism attribute %s ... %s " + % (vshift_setup, vshift) + ) + # print('vshift_setup: %s %s' % (sectype, vshift_setup), vshift) + + if hasattr( + cell, "channelErevMap" + ): # may not always have this mapping + secobj = cell.hr.get_section( + sec + ) # get the NEURON section object + mechsinsec = cell.get_mechs( + secobj + ) # get list of mechanisms in this section + if ( + mech in mechsinsec + ): # confirm that the mechanism is really there + setrev = ( + False + ) # We try two ways for different mechanisms - just flag it + try: + setattr( + secobj, + self.erev_mapper[mech], + cell.channelErevMap[sectype][mech], + ) + setrev = True + continue # don't bother with second approach + except: + raise ValueError("erev set failed") + pass # no error + try: + setattr( + secobj(), + self.erev_mapper[mech] + "_" + mech, + cell.channelErevMap[sectype][mech], + ) + setrev = True + except: + raise ValueError("Erev2 set failed") + pass # no error report + if ( + not setrev + ): # here is our error report - soft, not crash. + print( + "Failed to set reversal potential in section %s for mechanism %s" + % (sec, mech) + ) + # if mech in mechinsec and mech in self.vshift_mapper.keys(): + # try: + # setattr(secobj, self.vshift_mapper[mech], cell.channelVshiftMap[sectype][mech]) + # except: + # raise ValueError('Failed to set vshift for mech: %s sectpe: %s' % (mech, sectype[mech])) + + if verify: + self.channelValidate(cell) + return cell + + def gbarAdjust(self, cell, sectype, mech, sec): + gbar = cell.channelMap[sectype][mech] + gbar_orig = gbar + if sectype not in cell.distMap.keys(): # no map for this section type + return gbar + elif mech not in cell.distMap[sectype].keys(): + return gbar + # mecanism exists in the distMap, so we will map gbar to distance from soma + method = cell.distMap[sectype][mech]["gradient"] # grab the type + gminf = cell.distMap[sectype][mech]["gminf"] + rate = cell.distMap[sectype][mech]["lambda"] + # print('sectype: %s mech: %s method: %s rate: %s' % (sectype, mech, method, rate)) + if method == "flat": + return gbar + if sec in self.channelInfo.distanceMap.keys(): + dist = self.channelInfo.distanceMap[sec] + else: # the sec should be in the map, but there could be a coding error that would break that relationship + raise NameError( + "gbarAdjust in channel_decorate.py: section %s not in distance map" + % sec + ) + if method == "linear": # rate is "half" point drop + # print('doing linear, orig gbar: ', gbar) + gbar = gbar - dist * (gbar - gminf) / rate + if gbar < 0.0: + gbar = 0.0 # clip + # print('sec dist: %f final gbar: %f' % (dist, gbar)) + elif method in ["exp", "expdown"]: + gbar = (gbar - gminf) * np.exp(-dist / rate) + gminf + if gbar < 0.0: + gbar = 0.0 + # print 'gbaradjust: orig/adj: ', gbar_orig, gbar, method, dist, sectype + return gbar + + def channelValidate(self, cell, verify=False): + """ + verify mechanisms insertions - + go through all the groups, and find inserted conductances and their values + print the results to the terminal + """ + print("\nChannel Validation") + print(" Looking for sec_groups: ", sorted(cell.hr.sec_groups.keys())) + print(" Available Channel Maps: ", sorted(cell.channelMap.keys())) + secstuff = {} + for s in list(cell.hr.sec_groups.keys()): + sectype = self.remapSectionType(s.rsplit("[")[0]) + if sectype not in cell.channelMap.keys(): + if sectype in ["undefined"]: # skip undefined sections + continue + print( + "\033[1;31;40m Validation: encountered unknown section group type: %s Cannot Validate" + % sectype + ) + print("Cell morphology file: %s \033[0m" % cell.morphology_file) + continue + # print 'Validating Section: %s' % s + for mech in list(cell.channelMap[sectype].keys()): + if mech not in self.gbar_mapper.keys(): + continue + if mech in self.excludeMechs: + continue + if verify: + print( + "\tSection: %-15ss found mechanism: %-8ss at %.5f" + % (s, mech, cell.channelMap[sectype][mech]) + ) + x = nu.Mechanism( + mech + ) # , {gmapper[mech]: self.channelMap[cellType][sectype][mech]}) + setup = "%s_%s" % (self.gbar_mapper[mech], mech) + for sec in cell.hr.sec_groups[s]: + bar = getattr(cell.hr.get_section(sec), setup) + # print 'mech', mech + # print 'bar: ', bar + try: + Erev = getattr(cell.hr.get_section(sec), self.erev_mapper[mech]) + except: + Erev = -999.0 + # print 'erev: ', Erev + if sec in secstuff.keys(): + secstuff[sec] += ", g_%s = %g [%.1f]" % (mech, bar, Erev) + else: + secstuff[sec] = "(%10s) g_%-6s = %g [%.1f] " % ( + sectype, + mech, + bar, + Erev, + ) + if verify: + for i, k in enumerate(secstuff.keys()): + print("**%-20s " % k, secstuff[k]) + + def remapSectionType(self, sectype): + if sectype in ["AXON_0"]: + sectype = "axon" + + if sectype in ["initseg", "initialsegment"]: + sectype = "initialsegment" + if sectype in ["dendscaled_0", "dendscaled_1", "dendscaled_2", "dendrite"]: + sectype = "dendrite" + if sectype in ["apical_dendrite"]: + sectype = "secondarydendrite" + return sectype diff --git a/cnmodel/mechanisms/CaPCalyx.mod b/cnmodel/mechanisms/CaPCalyx.mod new file mode 100644 index 0000000..26424b1 --- /dev/null +++ b/cnmodel/mechanisms/CaPCalyx.mod @@ -0,0 +1,109 @@ +TITLE CaPCalyx.mod The presynaptic calcium current at the MNTB calyx of Held + +COMMENT + +NEURON implementation of Calcium current from Borst and Sakmann, 1998. +Equations are basic HH; parameters are taken from Figure 8 legend in that paper. + +Original implementation by Paul B. Manis, October 2007 + +Contact: pmanis@med.unc.edu + +Modified 2/2014: Use Derivative block format. + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { +THREADSAFE + SUFFIX CaPCalyx + USEION ca READ eca WRITE ica + RANGE gbar, gcap, ica + GLOBAL minf, taum, alpha, beta +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) +: celsius = 22 (degC) : model is defined on measurements made at 23-24 deg in Germany + celsius (degC) + dt (ms) +: eca = 43.9 (mV) + eca (mV) + gbar = 0.01 (mho/cm2) <0,1e9> : target is 48.9 nS total + alpha (1/ms) + beta (1/ms) +} + +STATE { + m +} + +ASSIGNED { + ica (mA/cm2) + gcap (mho/cm2) + minf (1) + taum (ms) + +} + +LOCAL mexp + +BREAKPOINT { + SOLVE states METHOD cnexp + + gcap = gbar*m*m + ica = gcap*(v - eca) +} + +UNITSOFF + +INITIAL { + trates(v) + m = minf +} + +DERIVATIVE states { : Computes state variables m + rates(v) : at the current v and dt. + m' = (minf - m)/taum + :m = m + mexp*(minf-m) +:VERBATIM +: return 0; +:ENDVERBATIM +} + +LOCAL q10 + +PROCEDURE rates(v) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + q10 = 3^((celsius - 24)/10) : if you don't like room temp, it can be changed! + + minf = (1 / (1 + exp(-(v + 23.2) / 9.1))) + + alpha = 1.78*exp(v/23.3) + beta = 0.140*exp(-v/15.0) + taum = 1/(alpha + beta) +} + +PROCEDURE trates(v) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + LOCAL minc + TABLE minf, mexp + DEPEND dt, celsius FROM -150 TO 150 WITH 300 + + rates(v) : not consistently executed from here if usetable_hh == 1 + : so don't expect the tau values to be tracking along with + : the inf values in hoc + + minc = -dt * q10 + mexp = 1 - exp(minc/taum) +} + +UNITSON diff --git a/cnmodel/mechanisms/Gly5GC.mod b/cnmodel/mechanisms/Gly5GC.mod new file mode 100755 index 0000000..c14fc38 --- /dev/null +++ b/cnmodel/mechanisms/Gly5GC.mod @@ -0,0 +1,134 @@ +TITLE detailed model of Glycine receptors + +COMMENT +----------------------------------------------------------------------------- + + Kinetic model of Glycine-A receptors + ==================================== + + C -- C1 -- C2 -- O1 + | | + D1 -- D2 -- D3 + +----------------------------------------------------------------------------- + + This Model is based on: + Gentet LJ, Clements JD Binding site stoichiometry and the effects of + phosphorylation on human alpha1 homomeric glycine receptors J Physiol (Lond) + 2002 vol. 544 (Pt 1) pp. 97-106, Figure 7. + + Written by Paul Manis, UNC Chapel Hill, 2009 + Kinetic values are estimated from VCN glycine receptors. + + This model has desensitization states. + +----------------------------------------------------------------------------- + + This mod file does not include mechanisms for the release and time course + of transmitter; it is to be used in conjunction with a sepearate mechanism + to describe the release of transmitter and that provides the concentration + of transmitter in the synaptic cleft (to be connected to pointer C here). + +----------------------------------------------------------------------------- + +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + POINT_PROCESS GLYaGC + POINTER XMTR + RANGE C0, C1, C2, D1, D2, D3, O1, Open + RANGE g, gmax, f1, f2 + RANGE Erev + RANGE k1, km1, a1, b1, d1, r1, d2, r2, d3, r3, rd, dd + NONSPECIFIC_CURRENT i +} + +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (umho) = (micromho) + (mM) = (milli/liter) + (uM) = (micro/liter) +} + +PARAMETER { + + Erev = -70 (mV) : reversal potential + gmax = 500 (pS) : maximal conductance + +: Rates +: bushy cell + + k1 = 12.81 (/uM /ms) : binding + km1 = 0.0087 (/ms) : unbinding + a1 = 0.0194 (/ms) : opening + b1 = 1.138 (/ms) : closing + r1 = 5.19 (/ms) : desense 1 + d1 = 0.000462 (/ms) : return from d1 + r2 = 0.731 (/ms) : return from deep state + d2 = 1.641 (/ms) : going to deep state + r3 = 3.817 (/ms) : return from deep state + d3 = 1.806 (/ms) : going to deep state + rd = 1.0 (/ms) + dd = 1.0 (/ms) +} + + +ASSIGNED { + v (mV) : postsynaptic voltage + i (nA) : current = g*(v - Erev) + g (pS) : conductance + XMTR (mM) : pointer to glycine concentration + + f1 (/ms) : binding + f2 (/ms) : binding + Open (1) +} + +STATE { + : Channel states (all fractions) + C0 : unbound + C1 : single bound + C2 : double bound + D1 : desense, bound + O1 : open + D2 : Desense + D3 : Desense +} + +INITIAL { + XMTR = 0 + C0 = 1 + C1 = 0 + C2 = 0 + O1 = 0 + D1 = 0 + D2 = 0 + D3 = 0 +} + +BREAKPOINT { + SOLVE kstates METHOD sparse + Open = (O1) + g = gmax * Open + i = (1e-6) * g * (v - Erev) +} + +KINETIC kstates { + + f1 = k1 * (1e3) * XMTR + f2 = k1 * (1e3) * XMTR + + ~ C0 <-> C1 (f1,km1) + ~ C1 <-> C2 (f2,12.5*km1) + ~ C2 <-> O1 (a1,b1) + ~ C1 <-> D1 (r1, d1) + ~ C2 <-> D2 (r2, d2) + ~ D1 <-> D2 (rd, dd) + ~ D2 <-> D3 (r3, d3) + + CONSERVE C0+C1+C2+D1+D2+D3+O1 = 1 +} diff --git a/cnmodel/mechanisms/Gly5PL.mod b/cnmodel/mechanisms/Gly5PL.mod new file mode 100755 index 0000000..f2a6415 --- /dev/null +++ b/cnmodel/mechanisms/Gly5PL.mod @@ -0,0 +1,149 @@ +TITLE detailed model of Glycine receptors + +COMMENT +----------------------------------------------------------------------------- + + Kinetic model of Glycine-A receptors: Pascal Legendre (Mauthner Cell) + ==================================== + + + + C0--C1--C2--O1 + | + C3--O2 + +----------------------------------------------------------------------------- + + + This mod file does not include mechanisms for the release and time course + of transmitter; it is to be used in conjunction with a sepearate mechanism + to describe the release of transmitter and that provides the concentration + of transmitter in the synaptic cleft (to be connected to pointer C here). + + +----------------------------------------------------------------------------- + + Modified Paul Manis, UNC Chapel Hill, 2009 + Name, pointer name, kinetics are range variables, and kinetic values + are estimated from VCN glycine receptors. + + Note: This model does not have a desensitization state. + +----------------------------------------------------------------------------- + +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + POINT_PROCESS GLYaPL + POINTER XMTR + RANGE C0, C1, C2, C3, O1, O2, Open + RANGE g, gmax, f1, f2 + RANGE Erev + RANGE kon, koff, a1, b1, a2, b2, r, d + RANGE CellType : 0 for bushy, 1 for stellate + NONSPECIFIC_CURRENT i +} + +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (umho) = (micromho) + (mM) = (milli/liter) + (uM) = (micro/liter) +} + +PARAMETER { + + Erev = -70 (mV) : reversal potential + gmax = 500 (pS) : maximal conductance + CellType = 1 (1) : define cell type parameters + +: Rates + +: Stellate cell fit (1/1/10; excellent fit) + +: kon = 0.0236 (/uM /ms) : binding +: koff = 2.4 (/ms) : unbinding +: a1 = 1.707 (/ms) : opening +: b1 = 8.95 (/ms) : closing +: a2 = 0.325 (/ms) : opening +: b2 = 5.871 (/ms) : closing +: r = 2.019 (/ms) : return from deep state +: d = 28.87 (/ms) : going to deep state + +:if psdtype == 'glyfast': fit from 3/5/2010. error = 0.174 maxopen = 0.0385 +: See synapses.py + a1 = 1.000476 (/ms) : opening + a2 = 0.137903 (/ms) : opening + b1 = 1.700306 (/ms) : closing + koff = 13.143132 (/ms) : unbinding + kon = 0.038634 (/ms) : binding + r = 0.842504 (/ms) : return from deep state + b2 = 8.051435 (/ms) : closing + d = 12.821820 (/ms) : going to deep state + +} + + +ASSIGNED { + v (mV) : postsynaptic voltage + i (nA) : current = g*(v - Erev) + g (pS) : conductance + XMTR (mM) : pointer to glycine concentration + + f1 (/ms) : binding + f2 (/ms) : binding + koff2 (/ms) + Open (1) +} + +STATE { + : Channel states (all fractions) + C0 : unbound + C1 : single bound + C2 : double bound + C3 : bound but closed state to O2 + O1 : open + O2 : open +} + +INITIAL { + + XMTR = 0.0 + C0 = 1 + C1 = 0 + C2 = 0 + C3 = 0 + O1 = 0 + O2 = 0 +} + +BREAKPOINT { + SOLVE kstates METHOD sparse +:VERBATIM +: if (CGly > 0.0) { +: fprintf(stderr, "t = %f Xmtr = %f\n", t, XMTR); +: } +: ENDVERBATIM + Open = (O1 + O2) + g = gmax * Open + i = (1e-6) * g * (v - Erev) +} + +KINETIC kstates { + + f1 = 2.0 * kon * (1e3) * XMTR + f2 = kon * (1e3) * XMTR + koff2 = 2.0 * koff + + ~ C0 <-> C1 (f1,koff) + ~ C1 <-> C2 (f2,koff2) + ~ C2 <-> O1 (a1,b1) + ~ C2 <-> C3 (d, r) + ~ C3 <-> O2 (a2,b2) + + CONSERVE C0+C1+C2+C3+O1+O2 = 1 +} diff --git a/cnmodel/mechanisms/Gly5State.mod b/cnmodel/mechanisms/Gly5State.mod new file mode 100755 index 0000000..3820cfc --- /dev/null +++ b/cnmodel/mechanisms/Gly5State.mod @@ -0,0 +1,147 @@ +TITLE detailed model of Glycine receptors + +COMMENT +----------------------------------------------------------------------------- + + Kinetic model of Glycine-A receptors + ==================================== + + + + C -- C1 -- C2 + | | + O1 O2 + +----------------------------------------------------------------------------- + + + This mod file does not include mechanisms for the release and time course + of transmitter; it is to be used in conjunction with a sepearate mechanism + to describe the release of transmitter and that provides the concentration + of transmitter in the synaptic cleft (to be connected to pointer C here). + +----------------------------------------------------------------------------- + + Based on models + + Destexhe, A., Mainen, Z.F. and Sejnowski, T.J. Kinetic models of + synaptic transmission. In: Methods in Neuronal Modeling (2nd edition; + edited by Koch, C. and Segev, I.), MIT press, Cambridge, 1998, pp. 1-25. + + (electronic copy available at http://cns.iaf.cnrs-gif.fr) + + Written by Alain Destexhe, Laval University, 1995 + +----------------------------------------------------------------------------- + + Modified Paul Manis, UNC Chapel Hill, 2009 + Changed name, pointer name, kinetics are range variables, and kinetic values + are estimated from VCN glycine receptors. + + This model does not have a desensitization state. + +----------------------------------------------------------------------------- + +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + POINT_PROCESS GLYa5 + POINTER XMTR + RANGE C0, C1, C2, O1, O2, Open + RANGE g, gmax, f1, f2 + RANGE Erev + RANGE kf1, kf2, kb1, kb2, a1, b1, a2, b2 + NONSPECIFIC_CURRENT i +} + +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (umho) = (micromho) + (mM) = (milli/liter) + (uM) = (micro/liter) +} + +PARAMETER { + + Erev = -70 (mV) : reversal potential + gmax = 500 (pS) : maximal conductance + +: Rates + +: from fits to averaged ipsc data, stellate cells 1/1/10 + +: kf1 = 0.002930 (/uM /ms) : binding +: kf2 = 0.005936 (/uM /ms) : binding +: kb1 = 2.793 (/ms) : unbinding +: kb2 = 1.445 (/ms) : unbinding +: a1 = 1e-6 (/ms) : opening +: b1 = 129.0 (/ms) : closing +: a2 = 5.10 (/ms) : opening +: b2 = 2.79 (/ms) : closing + +: from fits to averaged ipsc data, bushy cells 1/1/10 + + kf1 = 0.0278 (/uM /ms) : binding + kf2 = 1e-6 (/uM /ms) : binding + kb1 = 0.000054 (/ms) : unbinding + kb2 = 0.000855 (/ms) : unbinding + a1 = 1e-6 (/ms) : opening + b1 = 129.0 (/ms) : closing + a2 = 5.10 (/ms) : opening + b2 = 2.79 (/ms) : closing + +} + + +ASSIGNED { + v (mV) : postsynaptic voltage + i (nA) : current = g*(v - Erev) + g (pS) : conductance + XMTR (mM) : pointer to glycine concentration + + f1 (/ms) : binding + f2 (/ms) : binding + Open (1) +} + +STATE { + : Channel states (all fractions) + C0 : unbound + C1 : single bound + C2 : double bound + O1 : open + O2 : open +} + +INITIAL { + C0 = 1 + C1 = 0 + C2 = 0 + O1 = 0 + O2 = 0 + XMTR = 0.0 +} + +BREAKPOINT { + SOLVE kstates METHOD sparse + Open = (O1 + O2) + g = gmax * Open + i = (1e-6) * g * (v - Erev) +} + +KINETIC kstates { + + f1 = kf1 * (1e3) * XMTR + f2 = kf2 * (1e3) * XMTR + + ~ C0 <-> C1 (f1,kb1) + ~ C1 <-> C2 (f2,kb2) + ~ C1 <-> O1 (a1,b1) + ~ C2 <-> O2 (a2,b2) + + CONSERVE C0+C1+C2+O1+O2 = 1 +} diff --git a/cnmodel/mechanisms/Gly6S.mod b/cnmodel/mechanisms/Gly6S.mod new file mode 100755 index 0000000..e796ee3 --- /dev/null +++ b/cnmodel/mechanisms/Gly6S.mod @@ -0,0 +1,131 @@ +TITLE Model of glycine receptors + +COMMENT +----------------------------------------------------------------------------- + + Kinetic model of glycine receptors + =============================== + + 6-state gating model with + 2 open states provide dual exponential response. + + O1 + | + C0 -- C1 -- C2 -- O2 + | + D1 + +----------------------------------------------------------------------------- + + This mod file does not include mechanisms for the release and time course + of transmitter; it is to be used in conjunction with a sepearate mechanism + to describe the release of transmitter and that provides the concentration + of transmitter in the synaptic cleft (to be connected to pointer C here). + + Default parameters are set for a miniature EPSC. + + +----------------------------------------------------------------------------- + +Paul B. Manis, Ph.D. 28 Dec 2009 + +----------------------------------------------------------------------------- +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + POINT_PROCESS Gly6S + POINTER XMTR + RANGE C0, C1, C2, D1, O1, O2, Open + RANGE Erev + RANGE Rb, Ru1, Ru2, Rd, Rr, Ro1, Rc1, Ro2, Rc2 + RANGE g, rb, gmax + RANGE CellType : 0 is bushy, 1 is stellate + NONSPECIFIC_CURRENT i +} + +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (umho) = (micromho) + (mM) = (milli/liter) + (uM) = (micro/liter) +} + +PARAMETER { + + Erev = -70 (mV) : reversal potential + gmax = 500 (pS) : maximal conductance + CellType = 0 (1) : Cell type definition. +: Rates +: Bushy cell IPSCs: (rates can be changed externally) +: Set per Fits 8 March 2010 (see Synapses.py as well) + Rd = 1.177999 (/ms) : desensitization + Rr = 0.000005 (/ms) : resensitization + Rb = 0.009403 (/mM /ms): binding + : diffusion limited + Ru2 = 0.000086 (/ms) : unbinding (2nd site) + Ro1 = 0.187858 (/ms) : opening (fast) + Ro2 = 1.064426 (/ms) : opening (slow) + Ru1 = 0.028696 (/ms) : unbinding (1st site) + Rc1 = 0.103625 (/ms) : closing + Rc2 = 1.730578 (/ms) : closing + +} + +ASSIGNED { + v (mV) : postsynaptic voltage + i (nA) : current = g*(v - Erev) + g (pS) : conductance + XMTR (mM) : pointer to glutamate concentration + rbind (/ms) : binding + Open (1) +} + +STATE { + : Channel states (all fractions) + C0 : unbound + C1 : single gly bound + C2 : double gly bound + D1 : double gly bound, desensitized + O1 : double gly bound, open state 1 + O2 : double gly bound, open state 2 +} + +INITIAL { + + XMTR = 0 + C0 = 1 + C1 = 0 + C2 = 0 + D1 = 0 + O1 = 0 + O2 = 0 +} + +BREAKPOINT { + SOLVE kstates METHOD sparse +:VERBATIM +: if (XMTR > 0.1) { +: fprintf(stderr, "t = %f XMTR = %f\n", t, XMTR); +: } +: ENDVERBATIM + Open = (O1 + O2) + g = gmax * Open + i = (1e-6)*g*(v-Erev) +} + + +KINETIC kstates { + + rbind = Rb * (1e3) * XMTR + + ~ C0 <-> C1 (rbind,Ru1) + ~ C1 <-> C2 (rbind*2.0,Ru2) + ~ C2 <-> O1 (Ro1,Rc1) + ~ C2 <-> D1 (Rd,Rr) + ~ C2 <-> O2 (Ro2,Rc2) + CONSERVE C0+C1+C2+D1+O1+O2 = 1 +} diff --git a/cnmodel/mechanisms/Iclamp2.mod b/cnmodel/mechanisms/Iclamp2.mod new file mode 100644 index 0000000..74d15ff --- /dev/null +++ b/cnmodel/mechanisms/Iclamp2.mod @@ -0,0 +1,64 @@ +COMMENT + +Iclamp2.mod: Electrode current injection, revised for our own use +I specifically needed more step changes in a single waveform than +were provided by the default code. This routine gives us 5 levels: +holding, 3 steps, and a final (holding?) level. + +This version generates a pulse of duration at amplitude I for +onset times in the vector onset[STEP] and durations in dur[STEP] +Each pulse is independent and pulses _could_ overlap. + +Since this is an electrode current, positive values of I depolarize the cell +and in the presence of the extracellular mechanism there will be a change +in vext since I is not a transmembrane current but a current injected +directly to the inside of the cell. + +(modified and borrowed extensively from other Neuron code, +2001-2002. Paul B. Manis) + +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +DEFINE NSTEP 5 : maximum number of steps supported in this mechanism + +NEURON { + POINT_PROCESS IClamp2 + RANGE onset, dur, amp, i + ELECTRODE_CURRENT i +} +UNITS { + (nA) = (nanoamp) +} +PARAMETER { +} + +ASSIGNED { + i (nA) + index + onset[NSTEP] (ms) + dur[NSTEP] (ms) + amp[NSTEP] (nA) + pu +} + +INITIAL { + i = 0 (nA) + index = 0 + pu = 0 +} +: +: at each onset time, a pulse of duration dur is generated. +: + +BREAKPOINT { + i = 0 + FROM index = 0 TO NSTEP-1 { + if(t > onset[index]) { + if(t < (dur[index]+onset[index])) { + i = i + amp[index] : allows overlap + } : end of if condition + } :end of second if condition + } : end of "FROM" (for) loop +} : end of breakpoint diff --git a/cnmodel/mechanisms/NMDA.mod b/cnmodel/mechanisms/NMDA.mod new file mode 100644 index 0000000..54cb110 --- /dev/null +++ b/cnmodel/mechanisms/NMDA.mod @@ -0,0 +1,142 @@ +TITLE NMDA receptor--one of the two input stimulation of our model + +: This mechanism is taken from the Neuron data base "exp2syn.mod" +: The original comment are below between "COMMENT" and "ENDCOMMENT". +: +: Our modifications: +: +: 1.We added a single receptor conductance factor: "g_max=0.000045 (uS)". +: An event of weight 1 generates a peak conductance of 1*g_max. +: The weight is equal to the number of ampa receptors open at peak conductance +: +: 2.The NMDA receptors are simulated using a slow rise time constant +: and a double-expontial decay time constant + +: The kinetic rate constants and channel conductance are taken from Franks KM, Bartol TM and Sejnowski TJ +: A Monte Carlo model reveals independent signaling at central glutamatergic synapses +: J Biophys (2002) 83(5):2333-48 +: and Spruston N, Jonas P and Sakmann B +: Dendritic glutamate receptor channels in rat hippocampal CA3 and CA1 neurons +: J Physiol (1995) 482(2): 325-352 +: correctd for physiological tempterature with Q10 from Hestrin S, Sah P and Nicoll RA +: Mechanisms generating the time course of dual component excitatory synaptic currents +: recorded in hippocampal slices +: Neuron (1990) 5: 247-253 +: +: Written by Lei Tian on 04/12/06 + + + +COMMENT +Two state kinetic scheme synapse described by rise time tau1, +and decay time constant tau2. The normalized peak condunductance is 1. +Decay time MUST be greater than rise time. + +The solution of A->G->bath with rate constants 1/tau1 and 1/tau2 is + A = a*exp(-t/tau1) and + G = a*tau2/(tau2-tau1)*(-exp(-t/tau1) + exp(-t/tau2)) + where tau1 < tau2 + +If tau2-tau1 -> 0 then we have a alphasynapse. +and if tau1 -> 0 then we have just single exponential decay. + +The factor is evaluated in the +initial block such that an event of weight 1 generates a +peak conductance of 1. + +Because the solution is a sum of exponentials, the +coupled equations can be solved as a pair of independent equations +by the more efficient cnexp method. + +ENDCOMMENT + + +NEURON { +THREADSAFE + POINT_PROCESS nmda + RANGE tau1, tau2, tau3, e, i, g_max, g, A, B, C ,k + NONSPECIFIC_CURRENT i + GLOBAL total,i2,g2 +: EXTERNAL Area_canmda +} + +UNITS { + (nA) = (nanoamp) + (mA) = (milliamp) + (mV) = (millivolt) + (uS) = (microsiemens) +} + +PARAMETER { + tau1 = 3.18 (ms) <1e-9,1e9> :rise time constant + tau2 = 57.14 (ms) <1e-9,1e9> :decay time constant + tau3 = 2000 (ms) <1e-9,1e9> :decay time constant + + g_max= 0.000045 (uS) : single channel conductance + e = 0 (mV) + mg = 1 (mM) + + Area (cm2) + k = 1e-06 (mA/nA) + Area_canmda = 1 +} + +ASSIGNED { + v (mV) + i (nA) + factor + total (uS) + g (uS) + + g2 (uS) : plot 'g' and 'i' in "nmda.mod". + i2 (mA/cm2) : global variables read in "canmda.mod" as 'inmda' and 'gnmda' to give us +} + +STATE { + A (uS) + B (uS) + C (uS) +} + +INITIAL { + LOCAL t_peak + total = 0 + if (tau1/tau2 > .9999) { + tau1 = .9999*tau2 + } + A = 0 + B = 0 + C = 0 + + factor=0.8279 :from matlab to make the peak of the conductance curve shape to be 1*weight (then multiply with g_max) + factor = 1/factor + + Area = Area_canmda +} + +BREAKPOINT { + SOLVE state METHOD cnexp + + g = g_max*(B*0.8+C*0.2-A) + i = g*(v - e)*1/(1+(exp(0.08(/mV) * -v)*(mg / 0.69))) + + g2=g :global variable can be read in 'canmda.mod' + i2=i*k/Area :to get a current in 'mA/cm2' and send it to 'canmda.mod' +} + +DERIVATIVE state { + A' = -A/tau1 + B' = -B/tau2 + C' = -C/tau3 +} + +NET_RECEIVE(weight (uS)) { + state_discontinuity(A, weight*factor) + state_discontinuity(B, weight*factor) + state_discontinuity(C, weight*factor) + total = total+weight + +} + + + diff --git a/cnmodel/mechanisms/NMDA_Kampa.mod b/cnmodel/mechanisms/NMDA_Kampa.mod new file mode 100644 index 0000000..cfd4526 --- /dev/null +++ b/cnmodel/mechanisms/NMDA_Kampa.mod @@ -0,0 +1,212 @@ +TITLE kinetic NMDA receptor model + +COMMENT +----------------------------------------------------------------------------- + + Kinetic model of NMDA receptors + =============================== + + 10-state gating model: + Kampa et al. (2004) J Physiol + + U -- Cl -- O + \ | \ \ + \ | \ \ + UMg -- ClMg - OMg + | | + D1 | + | \ | + D2 \ | + \ D1Mg + \ | + D2Mg +----------------------------------------------------------------------------- + + Based on voltage-clamp recordings of NMDA receptor-mediated currents in + nucleated patches of rat neocortical layer 5 pyramidal neurons (Kampa 2004), + this model was fit with AxoGraph directly to experimental recordings in + order to obtain the optimal values for the parameters. + +----------------------------------------------------------------------------- + + This mod file does not include mechanisms for the release and time course + of transmitter; it should to be used in conjunction with a separate mechanism + to describe the release of transmitter and timecourse of the concentration + of transmitter in the synaptic cleft (to be connected to pointer XMTR here). + +----------------------------------------------------------------------------- + + See details of NEURON kinetic models in: + + Destexhe, A., Mainen, Z.F. and Sejnowski, T.J. Kinetic models of + synaptic transmission. In: Methods in Neuronal Modeling (2nd edition; + edited by Koch, C. and Segev, I.), MIT press, Cambridge, 1996. + + + Written by Bjoern Kampa in 2004 + Lightly modified, Paul Manis 2010. + Note that data were taken at 23 deg C + Q10 was taken from native receptors: + Korinek M, Sedlacek M, Cais O, Dittert I, Vyklicky L Jr. Temperature +dependence of N-methyl-D-aspartate receptor channels and N-methyl-D-aspartate +receptor excitatory postsynaptic currents. Neuroscience. 2010 Feb +3;165(3):736-48. Epub 2009 Oct 31. PubMed PMID: 19883737. + +----------------------------------------------------------------------------- +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { +THREADSAFE + + POINT_PROCESS NMDA_Kampa + POINTER XMTR + RANGE U, Cl, D1, D2, Open, MaxOpen, UMg, ClMg, D1Mg, D2Mg, OMg + RANGE g, gmax, vshift, Erev, rb, rmb, rmu, rbMg,rmc1b,rmc1u,rmc2b,rmc2u + GLOBAL mg, Rb, Ru, Rd1, Rr1, Rd2, Rr2, Ro, Rc, Rmb, Rmu + GLOBAL RbMg, RuMg, Rd1Mg, Rr1Mg, Rd2Mg, Rr2Mg, RoMg, RcMg + GLOBAL Rmd1b,Rmd1u,Rmd2b,Rmd2u,rmd1b,rmd1u,rmd2b,rmd2u + GLOBAL Rmc1b,Rmc1u,Rmc2b,Rmc2u + GLOBAL vmin, vmax, valence, memb_fraction + NONSPECIFIC_CURRENT i +} + +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (umho) = (micromho) + (mM) = (milli/liter) + (uM) = (micro/liter) +} + +PARAMETER { + + Erev = 5 (mV) : reversal potential + gmax = 500 (pS) : maximal conductance + mg = 1 (mM) : external magnesium concentration + vmin = -120 (mV) + vmax = 100 (mV) + valence = -2 : parameters of voltage-dependent Mg block + memb_fraction = 0.8 + vshift = 0.0 (mV) + Q10 = 2.0 : temperature sensitivity (see above) + + : Maximum open probability with Mode=0 (no rectification). + : This is determined empirically by holding XMTR at a large + : value and v=40mV for 100 timesteps and measuring the + : maximum value of Open. + MaxOpen = 0.01988893957 (1) + +: Rates + + Rb = 10e-3 (/uM /ms) : binding + Ru = 5.6e-3 (/ms) : unbinding + Ro = 10e-3 (/ms) : opening + Rc = 273e-3 (/ms) : closing +: Rd1 = 2.2e-3 (/ms) : fast desensitisation + Rd1 = 0.1 (/ms) : fast desensitisation + Rr1 = 1.6e-3 (/ms) : fast resensitisation +: Rd2 = 0.43e-3 (/ms) : slow desensitisation + Rd2 = 1e-4 (/ms) : slow desensitisation + Rr2 = 0.5e-3 (/ms) : slow resensitisation + Rmb = 0.05e-3 (/uM /ms) : Mg binding Open + Rmu = 12800e-3 (/ms) : Mg unbinding Open + Rmc1b = 0.00005e-3 (/uM /ms) : Mg binding Closed + Rmc1u = 2.438312e-3 (/ms) : Mg unbinding Closed + Rmc2b = 0.00005e-3 (/uM /ms) : Mg binding Closed2 + Rmc2u = 5.041915e-3 (/ms) : Mg unbinding Closed2 + Rmd1b = 0.00005e-3 (/uM /ms) : Mg binding Desens1 + Rmd1u = 2.98874e-3 (/ms) : Mg unbinding Desens1 + Rmd2b = 0.00005e-3 (/uM /ms) : Mg binding Desens2 + Rmd2u = 2.953408e-3 (/ms) : Mg unbinding Desens2 + RbMg = 10e-3 (/uM /ms) : binding with Mg + RuMg = 17.1e-3 (/ms) : unbinding with Mg + RoMg = 10e-3 (/ms) : opening with Mg + RcMg = 548e-3 (/ms) : closing with Mg + Rd1Mg = 2.1e-3 (/ms) : fast desensitisation with Mg + Rr1Mg = 0.87e-3 (/ms) : fast resensitisation with Mg + Rd2Mg = 0.26e-3 (/ms) : slow desensitisation with Mg + Rr2Mg = 0.42e-3 (/ms) : slow resensitisation with Mg +} + +ASSIGNED { + v (mV) : postsynaptic voltage + i (nA) : current = g*(v - Erev) + g (pS) : conductance + XMTR (mM) : pointer to glutamate concentration + + rb (/ms) : binding, [glu] dependent + rmb (/ms) : blocking V and [Mg] dependent + rmu (/ms) : unblocking V and [Mg] dependent + rbMg (/ms) : binding, [glu] dependent + rmc1b (/ms) : blocking V and [Mg] dependent + rmc1u (/ms) : unblocking V and [Mg] dependent + rmc2b (/ms) : blocking V and [Mg] dependent + rmc2u (/ms) : unblocking V and [Mg] dependent + rmd1b (/ms) : blocking V and [Mg] dependent + rmd1u (/ms) : unblocking V and [Mg] dependent + rmd2b (/ms) : blocking V and [Mg] dependent + rmd2u (/ms) : unblocking V and [Mg] dependent + + qfac : Q10 + celsius (degC) +} + +STATE { + : Channel states (all fractions) + U : unbound + Cl : closed + D1 : desensitised 1 + D2 : desensitised 2 + Open : open + UMg : unbound with Mg + ClMg : closed with Mg + D1Mg : desensitised 1 with Mg + D2Mg : desensitised 2 with Mg + OMg : open with Mg +} + +INITIAL { + U = 1 + qfac = Q10^((celsius-23)/10 (degC))} + +BREAKPOINT { + SOLVE kstates METHOD sparse + + g = gmax * Open / MaxOpen + i = (1e-6) * g * (v - Erev) +} + +KINETIC kstates { + + rb = Rb * (1e3) * XMTR + rbMg = RbMg * (1e3) * XMTR + rmb = Rmb * mg * (1e3) * exp((v-40+vshift) * valence * memb_fraction /25 (mV)) + rmu = Rmu * exp((-1)*(v-40+vshift) * valence * (1-memb_fraction) /25 (mV)) + rmc1b = Rmc1b * mg * (1e3) * exp((v-40+vshift) * valence * memb_fraction /25 (mV)) + rmc1u = Rmc1u * exp((-1)*(v-40+vshift) * valence * (1-memb_fraction) /25 (mV)) + rmc2b = Rmc2b * mg * (1e3) * exp((v-40+vshift) * valence * memb_fraction /25 (mV)) + rmc2u = Rmc2u * exp((-1)*(v-40+vshift) * valence * (1-memb_fraction) /25 (mV)) + rmd1b = Rmd1b * mg * (1e3) * exp((v-40+vshift) * valence * memb_fraction /25 (mV)) + rmd1u = Rmd1u * exp((-1)*(v-40+vshift) * valence * (1-memb_fraction) /25 (mV)) + rmd2b = Rmd2b * mg * (1e3) * exp((v-40+vshift) * valence * memb_fraction /25 (mV)) + rmd2u = Rmd2u * exp((-1)*(v-40+vshift) * valence * (1-memb_fraction) /25 (mV)) + + ~ U <-> Cl (rb*qfac,Ru*qfac) + ~ Cl <-> Open (Ro*qfac,Rc*qfac) + ~ Cl <-> D1 (Rd1*qfac,Rr1*qfac) + ~ D1 <-> D2 (Rd2*qfac,Rr2*qfac) + ~ Open <-> OMg (rmb*qfac,rmu*qfac) + ~ UMg <-> ClMg (rbMg*qfac,RuMg*qfac) + ~ ClMg <-> OMg (RoMg*qfac,RcMg*qfac) + ~ ClMg <-> D1Mg (Rd1Mg*qfac,Rr1Mg*qfac) + ~ D1Mg <-> D2Mg (Rd2Mg*qfac,Rr2Mg*qfac) + ~ U <-> UMg (rmc1b*qfac,rmc1u*qfac) + ~ Cl <-> ClMg (rmc2b*qfac,rmc2u*qfac) + ~ D1 <-> D1Mg (rmd1b*qfac,rmd1u*qfac) + ~ D2 <-> D2Mg (rmd2b*qfac,rmd2u*qfac) + + CONSERVE U+Cl+D1+D2+Open+UMg+ClMg+D1Mg+D2Mg+OMg = 1 +} diff --git a/cnmodel/mechanisms/adex.mod b/cnmodel/mechanisms/adex.mod new file mode 100644 index 0000000..3b505fa --- /dev/null +++ b/cnmodel/mechanisms/adex.mod @@ -0,0 +1,125 @@ +: AdEx GIF model + +: This implementation is for the equations in: +: Naud R, Marcille N, Clopath C, Gerstner W. Firing patterns in the adaptive +: exponential integrate-and-fire model. Biol Cybern. 2008 Nov;99(4-5):335-47. doi: +: 10.1007/s00422-008-0264-7. Epub 2008 Nov 15. PubMed PMID: 19011922; PubMed +: Central PMCID: PMC2798047. + +: Which in turn is based on: +: Brette R, Gerstner W. Adaptive exponential integrate-and-fire model as an +: effective description of neuronal activity. J Neurophysiol. 2005 +: Nov;94(5):3637-42. Epub 2005 Jul 13. PubMed PMID: 16014787. +: +: Paul B. Manis +: 9 Nov 2017, Washington DC +: + + +NEURON { +: ARTIFICIAL_CELL AdEx + SUFFIX AdEx + RANGE gl, el, delt, vt, vr, w, b, cm, is, a, tauw + RANGE refract, Vm + NONSPECIFIC_CURRENT i + : m plays the role of voltage +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + cm = 200 (pF) + el = -70 (mV) : leak (RMP) + gl = 10 (nS) : resting input R + delt = 2 (mV) : spike threshold sharpness + vt = -50 (mV) + vr = -58 (mV): reset value after a spike + a = 2 (nS) + b = 0 (pA) + is = 0 (pA) + tauw = 30 (ms) + refract = 1 (ms) +} + +ASSIGNED { + i (mA/cm2) + t0(ms) : time of last spike + refractory : flag indicating when in a refractory period +} + +STATE { + w + Vm +} + +INITIAL { + Vm = el + t0 = t + w = 0 + + refractory = 0 : 0-integrates input, 1-refractory +} + +BREAKPOINT { + SOLVE states METHOD cnexp + + if (refractory == 0 && Vm <= 0.) { + states() + } + if (refractory == 1) { + if ((t-t0) >= refract){ + refractory = 0 + Vm = vr + states() + } + else { + Vm = 0. + } + } + if (refractory == 0 && Vm > 0.) { + refractory = 1 + t0 = t + Vm = 0. + w = w + b + } +} + + +DERIVATIVE states { : update adaptation variable w + LOCAL eterm, et + w' = (a*(Vm - el) - w)/tauw + eterm = (Vm-vt)/delt + if (eterm > 700 ) { : prevent overflow of the exponential term + : (it would be better to estimate the value... but for this + : implementation, not necessary as this will be the term + : that drives the model to spike - after that V is reset + : so the time evolution no longer matters) + et = 700. + } + else { + et = exp(eterm) + } + Vm' = ( -gl*(Vm-el) + gl*delt*et + is - w)/cm +} + + +COMMENT +NET_RECEIVE (w) { + if (refractory == 0) { : inputs integrated only when excitable + i = -gl*(v-el) + gl*delt*exp((Vm-vt)/delt) - w + m = i/cm + t0 = t + states() + if (m > 0) { + refractory = 1 + m = 0 + net_send(refractory, refractory) + net_event(t) + } + } else if (flag == 1) { : ready to integrate again + t0 = t + refractory = 0 + m = vr + } +} +ENDCOMMENT \ No newline at end of file diff --git a/cnmodel/mechanisms/ampa_trussell.mod b/cnmodel/mechanisms/ampa_trussell.mod new file mode 100644 index 0000000..206dc34 --- /dev/null +++ b/cnmodel/mechanisms/ampa_trussell.mod @@ -0,0 +1,224 @@ +TITLE Model of AMPA receptors + +COMMENT +----------------------------------------------------------------------------- + + Kinetic model of AMPA receptors + =============================== + + 6-state gating model: + (scheme 1 from Raman and Trussell, Neuron 9:173-186, 1992) + 2 open states provide dual exponential response. + + O1 + | + C -- C1 -- C2 -- O2 + | + D + +----------------------------------------------------------------------------- + + This mod file does not include mechanisms for the release and time course + of transmitter; it is to be used in conjunction with a separate mechanism + to describe the release of transmitter and that provides the concentration + of transmitter in the synaptic cleft (to be connected to pointer C here). + + Default parameters are set for a miniature EPSC. + +----------------------------------------------------------------------------- + Code based on Destexhe's ampa5.mod + + B. Graham, Dept. of Computing Science & Maths, University of Stirling + (Contact: b.graham@cs.stir.ac.uk) + (previously IANC, Division of Informatics, University of Edinburgh) + + CNS 2000 Version (19/11/02) + +----------------------------------------------------------------------------- + + Further modified: + + Paul Manis (Otolaryngology/HNS and Cell and Molecular Physiology, + UNC Chapel Hill. contact: pmanis@med.unc.edu) + + 3/15/2005 Modifications: + + 1. Added Q10/qfac to allow temperature scaling. All rates in the state model + are changed by the same factor. A Q10 of 1.5 gives a decay tau (single + exponential fit using Praxis algorithm in NEURON; using ampa_kinetics.hoc) + of about 850 usec at 22 deg C and 570 usec at 33 deg C. These are consistent + with the Raman and Trussell 1992 measurements in avians. The 850 usec is a + bit fast for an EPSC, and could probably be tuned by adjustment of some of + the parameters below. + + 2. Brought several variables out to global (rather than range) so that we + can change them - Q10 and gmax in particular. note that gmax is in pS. Only + local conductance etc. is in specified as RANGE. + + 3. Max open probability is less than unity, so a gmax of 2500 yields 100 pA + at -60 mV. Therefore scaling by mini size must take this into account. + + 3/28/2005 Paul B. Manis + Added rectification to AMPA R. Rectification is controlled by + polyamine-style block of receptor. See Donevan and Rogawski, 1995; Washburn + et al., 1997. The equations used here are from Washburn et al. The values + given in the equation at the break point were determined from EPSCs in 5 + 21-d old DBA mice. Blocker = 45 (uM), Kd = 31.32, zd = 1.029. Note that this + should also reduce the maximal conductance. Mode: if 1, use rectifying; if + 0, use non-rectifying. Default is 1 + + This point process uses XMTR as the transmitter concentration to operate on + the receptor kinetics. XMTR should be provided by another process that + controls release (e.g., COH calyx of Held, etc). An advantage of this is + that whatever release process is present, glutamate accumulates in the + cleft, and can drive desensitization etc. + +----------------------------------------------------------------------------- +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { +THREADSAFE + POINT_PROCESS AMPATRUSSELL + POINTER XMTR + RANGE C0, C1, C2, D, O1, O2 + + RANGE Rb, Ru1, Ru2, Rd, Rr, Ro1, Rc1, Ro2, Rc2, Open, MaxOpen + GLOBAL vmin, vmax + GLOBAL Q10, Mode + GLOBAL zd, Kd0 + RANGE g, rb, gmax, PA, Erev + NONSPECIFIC_CURRENT i +} + +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (umho) = (micromho) + (mM) = (milli/liter) + (uM) = (micro/liter) +} + +PARAMETER { + + Erev = 7 (mV) : reversal potential + gmax = 10 (pS) : maximal conductance + vmin = -120 (mV) + vmax = 100 (mV) + Q10 = 1.5 : temperature sensitivity + Mode = 0 : flag to control rectification calculation + +: polyamine block parameters (Wang & Manis unpublished data) + zd = 1.032 + PA = 45 + Kd0 = 31.e-6 + +: Rates + + Rb = 13 (/mM /ms): binding + : diffusion limited (DO NOT ADJUST) + Ru1 = 0.3 (/ms) : unbinding (1st site) + Ru2 = 200 (/ms) : unbinding (2nd site) + Rd = 30.0 (/ms) : desensitization (WAS30.0) + Rr = 0.02 (/ms) : resensitization + Ro1 = 100 (/ms) : opening (fast) + Rc1 = 2 (/ms) : closing + Ro2 = 2 (/ms) : opening (slow) + Rc2 = 0.25 (/ms) : closing + + Open = 0 (1) : total of all open states + + : Maximum open probability with Mode=0 (no rectification). + : This is determined empirically by holding XMTR at a large + : value for 100 timesteps and measuring the maximum value + : of Open. + MaxOpen = 0.72418772400 (1) + + aflag = 1 : Flag for control of printout of initial values..... + +} + +ASSIGNED { + v (mV) : postsynaptic voltage + i (nA) : current = g*(v - Erev) + g (pS) : conductance + g0 (pS) : conductance for voltage-dependent block by polyamines + gvdep (pS) : voltage-dependence of conductance + XMTR (mM) : pointer to glutamate concentration + rb (/ms) : binding + qfac : q10 factor for rate scaling + celsius (degC) + +} + +STATE { + : Channel states (all fractions) + C0 : unbound + C1 : single glu bound + C2 : double glu bound + D : single glu bound, desensitized + O1 : open state 1 + O2 : open state 2 +} + +INITIAL { + usetable = 0 + C0=1 + C1=0 + C2=0 + D=0 + O1=0 + O2=0 + Open = 0 + qfac = Q10^((celsius-22)/10) +: VERBATIM +: fprintf(stdout, "AMPA.MOD gmax: %f Q10 = %f celsius = %f\n", gmax, Q10, celsius); +: ENDVERBATIM + gvdepcalc(v) +} + +BREAKPOINT { + SOLVE kstates METHOD sparse + : VERBATIM + : fprintf(stderr, "kstates @ t=%7.2f Rb: %f XMTR: %f: gmax: %f, o1: %f o2: %f\n", t, Rb, XMTR, gmax, O1, O2); + : ENDVERBATIM + + gvdepcalc(v) + Open = O1 + O2 + g = gmax * Open / MaxOpen + if ( Mode == 1) { + g0 = 1.0 + 0.6*exp((v-50)/40) : eq. 5 of Washburn et al., 1997, slightly modified + gvdep = g0*(1/(1+PA/(Kd0*exp(-zd*v/25.3)))) + i = (1e-6) * g * gvdep * (v - Erev) + } + else { + i = (1e-6)*g*(v-Erev) + } +} + +KINETIC kstates { + + rb = Rb * XMTR + + ~ C0 <-> C1 (rb*qfac,Ru1*qfac) + ~ C1 <-> C2 (rb*qfac,Ru2*qfac) + ~ C2 <-> D (Rd*qfac,Rr*qfac) + ~ C2 <-> O1 (Ro1*qfac,Rc1*qfac) + ~ C2 <-> O2 (Ro2*qfac,Rc2*qfac) + CONSERVE C0+C1+C2+D+O1+O2 = 1 +} + +LOCAL g0 +PROCEDURE gvdepcalc(v) { + TABLE gvdep DEPEND PA, Kd0, zd FROM -100 TO 100 WITH 200 + : VERBATIM + : fprintf(stderr, "gvdepcalc starts "); + : ENDVERBATIM + g0 = 1.0 + 0.6*exp((v-50)/40) : eq. 5 of Washburn et al., 1997, slightly modified + gvdep = g0*(1/(1+PA/(Kd0*exp(-zd*v/25.3)))) + : VERBATIM + : fprintf(stderr, "& ends\n"); + : ENDVERBATIM +} diff --git a/cnmodel/mechanisms/atm.mod b/cnmodel/mechanisms/atm.mod new file mode 100644 index 0000000..1a7bbe8 --- /dev/null +++ b/cnmodel/mechanisms/atm.mod @@ -0,0 +1,129 @@ +: ATM GIF model + +: This implementation the adaptive theshold model (ATM) is for the equations in: +: Fontaine, B., Benichourx, V., Joris, P.X., and Brette, R. Prediciting +: spike timing in hhigy synchronous auditory neurons at different sound +: levels. J. Neurophysiol. 110: 1672-1688, 2013. + +: Which in turn is based on: +: Brette R, Gerstner W. Adaptive exponential integrate-and-fire model as an +: effective description of neuronal activity. J Neurophysiol. 2005 +: Nov;94(5):3637-42. Epub 2005 Jul 13. PubMed PMID: 16014787. +: +: Paul B. Manis +: 2 December 2017, Chapel Hill, NC +: +: Incomplete version + +NEURON { +: ARTIFICIAL_CELL ATM + SUFFIX ATM + RANGE gl, el, delt, vt, vr, alpha, beta, cm, is, a, tauw + RANGE refract, Vm + NONSPECIFIC_CURRENT i + : m plays the role of voltage +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + cm = 200 (pF) + el = -70 (mV) : leak (RMP) + gl = 10 (nS) : resting input R + delt = 2 (mV) : spike threshold sharpness + vr = -58 (mV): reset value after a spike + a = 2 (1) + b = 2 (1) + beta = 0 (1) + alpha = 0 + is = 0 (pA) + taut = 30 (ms) : threshold tau + refract = 1 (ms) +} + +ASSIGNED { + i (mA/cm2) + t0 (ms) : time of last spike + refractory : flag indicating when in a refractory period +} + +STATE { + w + Vm + vt +} + +INITIAL { + Vm = el + t0 = t + a = 0 + b = 0 + + refractory = 0 : 0-integrates input, 1-refractory +} + +BREAKPOINT { + SOLVE states METHOD cnexp + + if (refractory == 0 && Vm <= 0.) { + states() + } + if (refractory == 1) { + if ((t-t0) >= refract){ + refractory = 0 + Vm = vr + states() + } + else { + Vm = 0. + } + } + if (refractory == 0 && Vm > 0.) { + refractory = 1 + t0 = t + Vm = 0. + w = w + b + } +} + + +DERIVATIVE states { : update adaptation variable w + LOCAL eterm, et + vt' = (a*i - vt)/taut +COMMENT + eterm = (Vm-vt)/delt + if (eterm > 700 ) { : prevent overflow of the exponential term + : (it would be better to estimate the value... but for this + : implementation, not necessary as this will be the term + : that drives the model to spike - after that V is reset + : so the time evolution no longer matters) + et = 700. + } + else { + et = exp(eterm) + } +ENDCOMMENT + Vm' = gl*( -(Vm-el) + i)/cm +} + + +COMMENT +NET_RECEIVE (w) { + if (refractory == 0) { : inputs integrated only when excitable + i = -gl*(v-el) + gl*delt*exp((Vm-vt)/delt) - w + m = i/cm + t0 = t + states() + if (m > 0) { + refractory = 1 + m = 0 + net_send(refractory, refractory) + net_event(t) + } + } else if (flag == 1) { : ready to integrate again + t0 = t + refractory = 0 + m = vr + } +} +ENDCOMMENT \ No newline at end of file diff --git a/cnmodel/mechanisms/bkpkj.mod b/cnmodel/mechanisms/bkpkj.mod new file mode 100644 index 0000000..bccab47 --- /dev/null +++ b/cnmodel/mechanisms/bkpkj.mod @@ -0,0 +1,98 @@ +: BK-type Purkinje calcium-activated potassium current +: Created 8/19/02 - nwg + +NEURON { + THREADSAFE + SUFFIX bkpkj + USEION k READ ek WRITE ik + USEION ca READ cai + RANGE gbar, ik, gbkpkj + GLOBAL minf, mtau, hinf, htau, zinf, ztau + GLOBAL m_vh, m_k, mtau_y0, mtau_vh1, mtau_vh2, mtau_k1, mtau_k2 + GLOBAL z_coef, ztau + GLOBAL h_y0, h_vh, h_k, htau_y0, htau_vh1, htau_vh2, htau_k1, htau_k2 +} + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) + (mM) = (milli/liter) +} + +PARAMETER { + v (mV) + gbar = 0.007 (mho/cm2) + + m1 (ms) + m_vh = -28.9 (mV) + m_k = 6.2 (mV) + mtau_y0 = 0.505 (ms) : 0.000505 (s) + mtau_vh1 = -33.3 (mV) + mtau_k1 = -10 (mV) + mtau_vh2 = 86.4 (mV) + mtau_k2 = 10.1 (mV) + + z_coef = 0.001 (mM) + ztau = 1 (ms) + + h_y0 = 0.085 + h_vh = -32 (mV) + h_k = 5.8 (mV) + htau_y0 = 1.9 (ms) : 0.0019 (s) + htau_vh1 = -54.2 (mV) + htau_k1 = -12.9 (mV) + htau_vh2 = 48.5 (mV) + htau_k2 = 5.2 (mV) + + ek (mV) + cai (mM) +} + +ASSIGNED { + gbkpkj (mho/cm2) + minf + mtau (ms) + hinf + htau (ms) + zinf + + ik (mA/cm2) +} + +STATE { + m FROM 0 TO 1 + z FROM 0 TO 1 + h FROM 0 TO 1 +} + +BREAKPOINT { + SOLVE states METHOD cnexp + gbkpkj = gbar * m * m * m * z * z * h + ik = gbkpkj * (v - ek) +} + +DERIVATIVE states { + rates(v) + + m' = (minf - m) / mtau + h' = (hinf - h) / htau + z' = (zinf - z) / ztau +} + +PROCEDURE rates(Vm (mV)) { + LOCAL v + v = Vm + 5 + minf = 1 / (1 + exp(-(v - (m_vh)) / m_k)) + m1 = mtau_y0 + (1. (ms)/(exp((v+ mtau_vh1)/mtau_k1))) + mtau = m1 + (1. (ms)) * exp((v+mtau_vh2)/mtau_k2) + zinf = 1/(1 + z_coef / cai) + hinf = h_y0 + (1-h_y0) / (1+exp((v - h_vh)/h_k)) + htau = (htau_y0 + (1 (ms))/(exp((v + htau_vh1)/htau_k1)+exp((v+htau_vh2)/htau_k2))) +} + +INITIAL { + rates(v) + m = minf + z = zinf + h = hinf +} diff --git a/cnmodel/mechanisms/cabpump.mod b/cnmodel/mechanisms/cabpump.mod new file mode 100644 index 0000000..e18c0c7 --- /dev/null +++ b/cnmodel/mechanisms/cabpump.mod @@ -0,0 +1,157 @@ +TITLE Calcium ion accumulation and diffusion with pump +: The internal coordinate system is set up in PROCEDURE coord_cadifus() +: and must be executed before computing the concentrations. +: The scale factors set up in this procedure do not have to be recomputed +: when diam or DFree are changed. +: The amount of calcium in an annulus is ca[i]*diam^2*vol[i] with +: ca[0] being the second order correct concentration at the exact edge +: and ca[NANN-1] being the concentration at the exact center + +? interface +NEURON { +THREADSAFE + SUFFIX cadifpmp + USEION ca READ cao, ica WRITE cai, ica + RANGE ica_pmp, last_ica_pmp, k1, k2, k3, k4, DFree + GLOBAL vol, pump0 +} + +DEFINE NANN 10 + +UNITS { + (mV) = (millivolt) + (molar) = (1/liter) + (mM) = (millimolar) + (um) = (micron) + (mA) = (milliamp) + (mol) = (1) + FARADAY = (faraday) (coulomb) + PI = (pi) (1) + R = (k-mole) (joule/degC) +} + +PARAMETER { + DFree = 0.6 (um2/ms) <0,1e9> + beta = 50 <0, 1e9> + + k1 = 5e8 (/mM-s) <0, 1e10>:optional mm formulation + k2 = .25e6 (/s) <0, 1e10> + k3 = .5e3 (/s) <0, 1e10> + k4 = 5e0 (/mM-s) <0, 1e10> + pump0 = 3e-14 (mol/cm2) <0, 1e9> : set to 0 in hoc if this pump not wanted +} + +ASSIGNED { + celsius (degC) + diam (um) + v (millivolt) + cao (mM) + cai (mM) + ica (mA/cm2) + vol[NANN] (1) : gets extra cm2 when multiplied by diam^2 + ica_pmp (mA/cm2) + area1 (um2) + c1 (1+8 um5/ms) + c2 (1-10 um2/ms) + c3 (1-10 um2/ms) + c4 (1+8 um5/ms) + ica_pmp_last (mA/cm2) +} + +CONSTANT { + volo = 1 (liter) +} + +STATE { + ca[NANN] (mM) <1e-6> : ca[0] is equivalent to cai + pump (mol/cm2) <1e-15> + pumpca (mol/cm2) <1e-15> +} + +INITIAL {LOCAL total + parms() + FROM i=0 TO NANN-1 { + ca[i] = cai + } + pumpca = cai*pump*c1/c2 + total = pumpca + pump + if (total > 1e-9) { + pump = pump*(pump/total) + pumpca = pumpca*(pump/total) + } + ica_pmp = 0 + ica_pmp_last = 0 +} + +BREAKPOINT { + SOLVE state METHOD sparse + ica_pmp_last = ica_pmp + ica = ica_pmp +: printf("Breakpoint t=%g v=%g cai=%g ica=%g\n", t, v, cai, ica) +} + +LOCAL frat[NANN] : gets extra cm when multiplied by diam + +PROCEDURE coord() { + LOCAL r, dr2 + : cylindrical coordinate system with constant annuli thickness to + : center of cell. Note however that the first annulus is half thickness + : so that the concentration is second order correct spatially at + : the membrane or exact edge of the cell. + : note ca[0] is at edge of cell + : ca[NANN-1] is at center of cell + r = 1/2 :starts at edge (half diam) + dr2 = r/(NANN-1)/2 :half thickness of annulus + vol[0] = 0 + frat[0] = 2*r + FROM i=0 TO NANN-2 { + vol[i] = vol[i] + PI*(r-dr2/2)*2*dr2 :interior half + r = r - dr2 + frat[i+1] = 2*PI*r/(2*dr2) :exterior edge of annulus + : divided by distance between centers + r = r - dr2 + vol[i+1] = PI*(r+dr2/2)*2*dr2 :outer half of annulus + } +} + +KINETIC state { +: printf("Solve begin t=%g v=%g cai=%g ica_pmp=%g\n", t, v, cai, ica_pmp) + COMPARTMENT i, (1+beta)*diam*diam*vol[i]*1(um) {ca} + COMPARTMENT (1e10)*area1 {pump pumpca} + COMPARTMENT volo*(1e15) {cao} +? kinetics + ~ pumpca <-> pump + cao (c3, c4) + ica_pmp = (1e-4)*2*FARADAY*(f_flux - b_flux)/area1 + : all currents except pump + ~ ca[0] << (-(ica-ica_pmp_last)*PI*diam*1(um)*(1e4)*frat[0]/(2*FARADAY)) + :diffusion + FROM i=0 TO NANN-2 { + ~ ca[i] <-> ca[i+1] (DFree*frat[i+1]*1(um), DFree*frat[i+1]*1(um)) + } + :pump + ~ ca[0] + pump <-> pumpca (c1, c2) + cai = ca[0] : this assignment statement is used specially by cvode +: printf("Solve end cai=%g ica=%g ica_pmp=%g ica_pmp_last=%g\n", +: cai, ica, ica_pmp,ica_pmp_last) +} + +PROCEDURE parms() { + coord() + area1 = 2*PI*(diam/2) * 1(um) + c1 = (1e7)*area1 * k1 + c2 = (1e7)*area1 * k2 + c3 = (1e7)*area1 * k3 + c4 = (1e7)*area1 * k4 +} + +FUNCTION ss() (mM) { + SOLVE state STEADYSTATE sparse + ss = cai +} + +COMMENT +At this time, conductances (and channel states and currents are +calculated at the midpoint of a dt interval. Membrane potential and +concentrations are calculated at the edges of a dt interval. With +secondorder=2 everything turns out to be second order correct. +ENDCOMMENT diff --git a/cnmodel/mechanisms/cadiff.mod b/cnmodel/mechanisms/cadiff.mod new file mode 100644 index 0000000..53c78f6 --- /dev/null +++ b/cnmodel/mechanisms/cadiff.mod @@ -0,0 +1,50 @@ +: Ca diffusion in a Purkinje cell +: Created 8/15/02 - nwg + +NEURON { + SUFFIX cadiff + USEION ca READ ica, cai WRITE cai + RANGE ca + GLOBAL depth, beta +} + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) + (mM) = (milli/liter) + (um) = (micron) +} + +CONSTANT { + F = 9.6485e4 (coul) +} + +PARAMETER { + cai (mM) + dt (ms) + + depth = 0.1 (um) + beta = 1 (/ms) +} + +ASSIGNED { + ica (mA/cm2) +} + +STATE { + ca (mM) +} + +INITIAL { + ca = 0.0001 +} + +BREAKPOINT { + ca = ca + (10000.0) * dt * ( ( -1/(2*F)*ica / (depth)) - (0.0001) * beta * ca ) + + if ( ca < 1e-4 ) {: minimum 100 nM Ca + ca = 1e-4 + } + + cai = ca +} diff --git a/cnmodel/mechanisms/cadyn.mod b/cnmodel/mechanisms/cadyn.mod new file mode 100644 index 0000000..e61bc98 --- /dev/null +++ b/cnmodel/mechanisms/cadyn.mod @@ -0,0 +1,100 @@ +TITLE decay of submembrane calcium concentration +: +: Internal calcium concentration due to calcium currents and pump. +: Differential equations. +: +: This file contains two mechanisms: +: +: 1. Simple model of ATPase pump with 3 kinetic constants (Destexhe 1992) +: +: Cai + P <-> CaP -> Cao + P (k1,k2,k3) +: +: A Michaelis-Menten approximation is assumed, which reduces the complexity +: of the system to 2 parameters: +: kt = * k3 -> TIME CONSTANT OF THE PUMP +: kd = k2/k1 (dissociation constant) -> EQUILIBRIUM CALCIUM VALUE +: The values of these parameters are chosen assuming a high affinity of +: the pump to calcium and a low transport capacity (cfr. Blaustein, +: TINS, 11: 438, 1988, and references therein). +: +: For further information about this this mechanism, see Destexhe, A. +: Babloyantz, A. and Sejnowski, TJ. Ionic mechanisms for intrinsic slow +: oscillations in thalamic relay neurons. Biophys. J. 65: 1538-1552, 1993. +: +: +: 2. Simple first-order decay or buffering: +: +: Cai + B <-> ... +: +: which can be written as: +: +: dCai/dt = (cainf - Cai) / taur +: +: where cainf is the equilibrium intracellular calcium value (usually +: in the range of 200-300 nM) and taur is the time constant of calcium +: removal. The dynamics of submembranal calcium is usually thought to +: be relatively fast, in the 1-10 millisecond range (see Blaustein, +: TINS, 11: 438, 1988). +: +: All variables are range variables +: +: Written by Alain Destexhe, Salk Institute, Nov 12, 1992 +: + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + SUFFIX cadyn + USEION ca READ ica, cai WRITE cai + RANGE depth,kt,kd,cainf,taur +} + +UNITS { + (molar) = (1/liter) : moles do not appear in units + (mM) = (millimolar) + (um) = (micron) + (mA) = (milliamp) + (msM) = (ms mM) +} + +CONSTANT { + FARADAY = 96489 (coul) : moles do not appear in units +} + +PARAMETER { + depth = .1 (um) : depth of shell + taur = 1e10 (ms) : remove first-order decay + cainf = 1.4e-1 (mM) + kt = 1e-4 (mM/ms) + kd = 1e-4 (mM) +} + +STATE { + cai (mM) +} + +INITIAL { + cai = kd +} + +ASSIGNED { + ica (mA/cm2) + drive_channel (mM/ms) + drive_pump (mM/ms) +} + +BREAKPOINT { + SOLVE state METHOD cnexp +} + +DERIVATIVE state { + + drive_channel = - (10000) * ica / (2 * FARADAY * depth) + + if (drive_channel <= 0.) { drive_channel = 0. } : cannot pump inward + + drive_pump = -kt * cai / (cai + kd ) : Michaelis-Menten + + cai' = drive_channel + drive_pump + (cainf-cai)/taur +} + diff --git a/cnmodel/mechanisms/cap.mod b/cnmodel/mechanisms/cap.mod new file mode 100644 index 0000000..8d81c76 --- /dev/null +++ b/cnmodel/mechanisms/cap.mod @@ -0,0 +1,86 @@ +: HH P-type Calcium current +: Created 8/13/02 - nwg + +NEURON { + THREADSAFE + SUFFIX cap + USEION ca READ cai, cao WRITE ica + RANGE pcabar, ica + RANGE minf, mtau + RANGE monovalConc, monovalPerm +} + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) + (mM) = (milli/liter) + F = 9.6485e4 (coul) + R = 8.3145 (joule/degC) +} + +PARAMETER { + v (mV) + celsius (degC) + + pcabar = 0.00005 (cm/s) + monovalConc = 140 (mM) + monovalPerm = 0 + + cai (milli/liter) + cao (milli/liter) +} + +ASSIGNED { + ica (mA/cm2) + minf + mtau (ms) + T (degC) + E (volts) +} + +STATE { + m +} + +INITIAL { + rates(v) + m = minf +} + +BREAKPOINT { + SOLVE states METHOD cnexp + ica = (1e3) * pcabar * m * ghk(v, cai, cao, 2) +} + +DERIVATIVE states { + rates(v) + m' = (minf - m)/mtau +} + +FUNCTION ghk( v(mV), ci(mM), co(mM), z) (coul/cm3) { LOCAL Ci + T = celsius + 273.19 : Kelvin + E = (1e-3) * v + Ci = ci + (monovalPerm) * (monovalConc) : Monovalent permeability + if (fabs(1-exp(-z*(F*E)/(R*T))) < 1e-6) { : denominator is small -> Taylor series + ghk = (1e-6) * z * F * (Ci-co*exp(-z*(F*E)/(R*T)))*(1-(z*(F*E)/(R*T))) + } else { + ghk = (1e-6) * z^2*(E*F^2)/(R*T)*(Ci-co*exp(-z*(F*E)/(R*T)))/(1-exp(-z*(F*E)/(R*T))) + } +} + +PROCEDURE rates (v (mV)) { + UNITSOFF + minf = 1/(1+exp(-(v - (-19)) / 5.5)) + mtau = (mtau_func(v)) * 1e3 + UNITSON +} + +FUNCTION mtau_func( v (mV) ) (ms) { + UNITSOFF + if (v > -50) { + mtau_func = .000191 + .00376*exp(-((v-(-41.9))/27.8)^2) + } else { + mtau_func = .00026367 + .1278 * exp(.10327*v) + } + UNITSON +} diff --git a/cnmodel/mechanisms/capmp.mod b/cnmodel/mechanisms/capmp.mod new file mode 100644 index 0000000..ca4c0cf --- /dev/null +++ b/cnmodel/mechanisms/capmp.mod @@ -0,0 +1,71 @@ +NEURON { + SUFFIX capmp + USEION ca READ cao, ica, cai WRITE cai, ica + RANGE tau, width, cabulk, ica, pump0 +} + +UNITS { + (um) = (micron) + (molar) = (1/liter) + (mM) = (millimolar) + (uM) = (micromolar) + (mA) = (milliamp) + (mol) = (1) + FARADAY = (faraday) (coulomb) +} + +PARAMETER { + width = 0.1 (um) + tau = 1 (ms) + k1 = 5e8 (/mM-s) + k2 = 0.25e6 (/s) + k3 = 0.5e3 (/s) + k4 = 5e0 (/mM-s) + cabulk = 0.1 (uM) + pump0 = 3e-14 (mol/cm2) +} + +ASSIGNED { + cao (mM) : 2 + cai (mM) : 100e-6 + ica (mA/cm2) + ica_pmp (mA/cm2) + ica_pmp_last (mA/cm2) +} + +STATE { + cam (uM) <1e-6> + pump (mol/cm2) <1e-16> + capump (mol/cm2) <1e-16> +} + +INITIAL { + ica = 0 + ica_pmp = 0 + ica_pmp_last = 0 + SOLVE pmp STEADYSTATE sparse +} + +BREAKPOINT { + SOLVE pmp METHOD sparse + ica_pmp_last = ica_pmp + ica = ica_pmp +} + +KINETIC pmp { + ~ cabulk <-> cam (width/tau, width/tau) + ~ cam + pump <-> capump ((1e7)*k1, (1e10)*k2) + ~ capump <-> cao + pump ((1e10)*k3, (1e10)*k4) + ica_pmp = (1e-7)*2*FARADAY*(f_flux - b_flux) + + : ica_pmp_last vs ica_pmp needed because of STEADYSTATE calculation + ~ cam << (-(ica - ica_pmp_last)/(2*FARADAY)*(1e7)) + + CONSERVE pump + capump = (1e13)*pump0 + COMPARTMENT width {cam} : volume has dimensions of um + COMPARTMENT (1e13) {pump capump} : area is dimensionless + COMPARTMENT 1(um) {cabulk} + COMPARTMENT (1e3)*1(um) {cao} + + cai = (0.001)*cam +} diff --git a/cnmodel/mechanisms/capump.mod b/cnmodel/mechanisms/capump.mod new file mode 100644 index 0000000..a300f46 --- /dev/null +++ b/cnmodel/mechanisms/capump.mod @@ -0,0 +1,32 @@ +NEURON { + SUFFIX capump + USEION ca READ cai WRITE ica + RANGE vmax, kmp, ica + } + + UNITS { + (uM) = (micro/liter) + (mM) = (milli/liter) + (mA) = (milliamp) + } + + PARAMETER { + vmax = .0667 (mA/cm2) <0, 1e6>: at 6.3 deg, Q10 = 3 + kmp = .2 (uM) <0, 1e6> + } + + ASSIGNED { + celsius (degC) + ica (mA/cm2) + cai (mM) + } + + LOCAL Q, s_celsius + + BREAKPOINT { + if (s_celsius*1(degC) != celsius) { + s_celsius = celsius + Q = 3^((celsius - 6.3)/10 (degC)) + } + ica = vmax*Q*cai/(cai + (.001)*kmp) / 5.18 +} \ No newline at end of file diff --git a/cnmodel/mechanisms/cleftXmtr.mod b/cnmodel/mechanisms/cleftXmtr.mod new file mode 100755 index 0000000..fba6643 --- /dev/null +++ b/cnmodel/mechanisms/cleftXmtr.mod @@ -0,0 +1,88 @@ +COMMENT +cleftXmtr + +This is simple state model that generates "cleft" transmitter, through +the following scheme: + +A netreceive block receives the driving event. This forces XV (the vesicle +state) to be set to XMax to mimic the release of a vesicle. +Then: +XV --> XC --> XU +where XV is the vesicle transmitter, XC is the cleft transmitter and +XU is transmitter that has been taken up. The forward rates are finite, and the +reverse rates are 0 (XU is an absorbing state) + +The forward rate kv1 mimics simple first-order diffusion across the cleft +The forward rate ku1 mimics simple first-order uptake from the cleft + +The concentration XC is available to the program as Xmtr. +XMax is the max cleft concentration of transmitter. + +Because vesicle release events at a single presynaptic terminal can be nearly +simultaneous, it is important that this mechanism does not have a refractory +period. We also assume that the uptake mechanism is not saturable. + +Paul B. Manis, Ph.D. +UNC Chapel Hill +3 Jan 2010 + +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +DEFINE NSTEP 5 + +NEURON { + POINT_PROCESS cleftXmtr + POINTER pre + RANGE KV, KU, XMax + RANGE CXmtr, preThresh +} +UNITS { + (nA) = (nanoamp) +} + +PARAMETER { : Parameters are chosen from best fit to stellate cell data in VCN + KV = 531 (/ms) <0,1e9> : release rate from vesicle + KU = 4.17 (/ms) <0,1e3> : uptake rate + XMax = 0.731 (mM) + preThresh = 0 +} + +ASSIGNED { + pre + CXmtr (mM) + preLast (1) + tLast +} + +STATE { + XV : Vesicle transmitter (just released) + XC : Cleft transmitter (e.g., at receptor) + XU : Uptake state (dead state... ) +} + +INITIAL { + XV = 0 + XC = 0 (mM) + XU = 0 + CXmtr = 0.0 + preLast = 0.0 + tLast = 0.0 +} + +BREAKPOINT { + SOLVE kstates METHOD sparse + CXmtr = XC*XMax +} + +KINETIC kstates { + ~ XV <-> XC (KV, 0.0) + ~ XC <-> XU (KU, 0.0) + : note that this mechanism has no CONSERVATION : XU can accumulate as much + : as needed. +} + +NET_RECEIVE(conc (mM)) { : detect and cause a release event + XV = XV + 1 +} diff --git a/cnmodel/mechanisms/gly.mod b/cnmodel/mechanisms/gly.mod new file mode 100755 index 0000000..df4cb2a --- /dev/null +++ b/cnmodel/mechanisms/gly.mod @@ -0,0 +1,65 @@ +TITLE Gly synapse + +COMMENT + MODIFIED to be a faster GLY synapse, taken from GABA synapse + Paul B. Manis - 7 Feb 2000 + + simple alpha-synapse that generates a single PSP + ********************************************* + reference: McCormick, Wang & Huguenard (1993) + Cerebral Cortex 3(5), 387-398 + found in: cat reticular nucleus of thalamus + ********************************************* + Assembled for MyFirstNEURON by Arthur Houweling + + +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + POINT_PROCESS GlySynapse + USEION cl READ ecl VALENCE 1 + : negative valence not accepted by nrnivmodl + RANGE onset, gmaxIPSP, e, g, i, w + NONSPECIFIC_CURRENT i +} + +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (nS) = (nanomho) +} + +PARAMETER { + onset= 25 (ms) + gmaxIPSP= 0 (nS) + w= 1 : weight factor for gmaxIPSP + ecl (mV) + v (mV) + celsius (degC) +} + +ASSIGNED { + i (nA) + g (nS) + tadj +} + +UNITSOFF +INITIAL { + tadj = 3^((celsius-23.5)/10) +} + +BREAKPOINT { LOCAL tt + tt= (t-onset)*tadj + if ((t>onset)&&(tt<740)) { + : the exp() function does not accept arguments smaller than -745 + g = w*gmaxIPSP * exp(-tt/15) * (1-exp(-tt/0.5))/0.84 + } + else {g = 0} + : -ecl because negative valences can not be specified + i = g * (v-(-ecl)) +} +UNITSON + diff --git a/cnmodel/mechanisms/gly2.mod b/cnmodel/mechanisms/gly2.mod new file mode 100755 index 0000000..54cefb5 --- /dev/null +++ b/cnmodel/mechanisms/gly2.mod @@ -0,0 +1,196 @@ +COMMENT +----------------------------------------------------------------------------- +Simple synaptic mechanism derived for first order kinetics of +binding of transmitter to postsynaptic receptors. + +A. Destexhe & Z. Mainen, The Salk Institute, March 12, 1993. +Last modif. Sept 8, 1993. + +Reference: + +Destexhe, A., Mainen, Z. and Sejnowski, T.J. An efficient method for +computing synaptic conductances based on a kinetic model of receptor binding. +Neural Computation, 6: 14-18, 1994. +----------------------------------------------------------------------------- + +During the arrival of the presynaptic spike (detected by threshold +crossing), it is assumed that there is a brief pulse (duration=Cdur) +of neurotransmitter C in the synaptic cleft (the maximal concentration +of C is Cmax). Then, C is assumed to bind to a receptor Rc according +to the following first-order kinetic scheme: + +Rc + C ---(Alpha)--> Ro (1) + <--(Beta)--- + +where Rc and Ro are respectively the closed and open form of the +postsynaptic receptor, Alpha and Beta are the forward and backward +rate constants. If R represents the fraction of open gates Ro, +then one can write the following kinetic equation: + +dR/dt = Alpha * C * (1-R) - Beta * R (2) + +and the postsynaptic current is given by: + +Isyn = gmax * R * (V-Erev) (3) + +where V is the postsynaptic potential, gmax is the maximal conductance +of the synapse and Erev is the reversal potential. + +If C is assumed to occur as a pulse in the synaptic cleft, such as + +C _____ . . . . . . Cmax + | | + _____| |______ . . . 0 + t0 t1 + +then one can solve the kinetic equation exactly, instead of solving +one differential equation for the state variable and for each synapse, +which would be greatly time consuming... + +Equation (2) can be solved as follows: + +1. during the pulse (from t=t0 to t=t1), C = Cmax, which gives: + + R(t-t0) = Rinf + [ R(t0) - Rinf ] * exp (- (t-t0) / Rtau ) (4) + +where + Rinf = Alpha * Cmax / (Alpha * Cmax + Beta) +and + Rtau = 1 / (Alpha * Cmax + Beta) + +2. after the pulse (t>t1), C = 0, and one can write: + + R(t-t1) = R(t1) * exp (- Beta * (t-t1) ) (5) + +There is a pointer called "pre" which must be set to the variable which +is supposed to trigger synaptic release. This variable is usually the +presynaptic voltage but it can be the presynaptic calcium concentration, +or other. Prethresh is the value of the threshold at which the release is +initiated. + +Once pre has crossed the threshold value given by Prethresh, a pulse +of C is generated for a duration of Cdur, and the synaptic conductances +are calculated accordingly to eqs (4-5). Another event is not allowed to +occur for Deadtime milliseconds following after pre rises above threshold. + +The user specifies the presynaptic location in hoc via the statement + connect pre_GABA[i] , v.section(x) + +where x is the arc length (0 - 1) along the presynaptic section (the currently +specified section), and i is the synapse number (Which is located at the +postsynaptic location in the usual way via + postsynaptic_section {loc_GABA(i, x)} +Notice that loc_GABA() must be executed first since that function also +allocates space for the synapse. +----------------------------------------------------------------------------- + GLY SYNAPSE (GLY receptors) + + currently parameters are same as GABA-A until I get the Harty data in here + P. Manis 2/10/2000 + +----------------------------------------------------------------------------- +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + POINT_PROCESS GLY2 + POINTER pre + RANGE C, R, R0, R1, g, gmax, Erev, lastrelease, Prethresh + NONSPECIFIC_CURRENT i + GLOBAL Cmax, Cdur, Alpha, Beta, Deadtime, Rinf, Rtau +} +UNITS { + (nA) = (nanoamp) + (mV) = (millivolt) + (umho) = (micromho) + (mM) = (milli/liter) +} + +PARAMETER { + + Cmax = 1 (mM) : max transmitter concentration + Cdur = 1 (ms) : transmitter duration (rising phase) + Alpha = 0.53 (/ms mM) : forward (binding) rate + Beta = 0.18 (/ms) : backward (unbinding) rate + Erev = -80 (mV) : reversal potential + Prethresh = 0 : voltage level nec for release + Deadtime = 1 (ms) : mimimum time between release events + gmax (umho) : maximum conductance +} + +ASSIGNED { + v (mV) : postsynaptic voltage + i (nA) : current = g*(v - Erev) + g (umho) : conductance + C (mM) : transmitter concentration + R : fraction of open channels + R0 : open channels at start of release + R1 : open channels at end of release + Rinf : steady state channels open + Rtau (ms) : time constant of channel binding + pre : pointer to presynaptic variable + lastrelease (ms) : time of last spike +} + +INITIAL { + R = 0 + C = 0 + R0 = 0 + R1 = 0 + Rinf = Cmax*Alpha / (Cmax*Alpha + Beta) + Rtau = 1 / ((Alpha * Cmax) + Beta) + lastrelease = -999 +} + +BREAKPOINT { + SOLVE release + g = gmax * R + i = g*(v - Erev) +} + +PROCEDURE release() { LOCAL q + :will crash if user hasn't set pre with the connect statement + + q = ((t - lastrelease) - Cdur) : time since last release ended + + : ready for another release? + if (q > Deadtime) { + if (pre > Prethresh) { : spike occured? + C = Cmax : start new release + R0 = R + lastrelease = t + } + + } else if (q < 0) { : still releasing? + + : do nothing + + } else if (C == Cmax) { : in dead time after release + R1 = R + C = 0. + } + + if (C > 0) { : transmitter being released? + + R = Rinf + (R0 - Rinf) * exptable (- (t - lastrelease) / Rtau) + + } else { : no release occuring + + R = R1 * exptable (- Beta * (t - (lastrelease + Cdur))) + } + + VERBATIM + return 0; + ENDVERBATIM +} + +FUNCTION exptable(x) { + TABLE FROM -10 TO 10 WITH 2000 + + if ((x > -10) && (x < 10)) { + exptable = exp(x) + } else { + exptable = 0. + } +} diff --git a/cnmodel/mechanisms/hcno.mod b/cnmodel/mechanisms/hcno.mod new file mode 100644 index 0000000..337b2e1 --- /dev/null +++ b/cnmodel/mechanisms/hcno.mod @@ -0,0 +1,101 @@ +TITLE h current for Octopus cells of Cochlear Nucleus +: From Bal and Oertel (2000) +: M.Migliore Oct. 2001 +: Modified, P. Manis July 2014. + +NEURON { + THREADSAFE + SUFFIX hcno + NONSPECIFIC_CURRENT i + RANGE gbar, eh, gh + GLOBAL hinf, tau1, tau2 +} + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (um) = (micron) + R = (k-mole)(joule/degC) + F = (faraday)(kilocoulombs) +} + +PARAMETER { + gbar = 0.0005 (mho/cm2) + + vhalf1 = -50 (mV) : v 1/2 for forward + vhalf2 = -84 (mV) : v 1/2 for backward + gm1 = 0.3 :(mV) : slope for forward + gm2 = 0.6 : (mV) : slope for backward + zeta1 = 3 : (/ms) + zeta2 = 3 : (/ms) + a01 = 0.008 (/ms) + a02 = 0.0029 (/ms) + frac = 0.0 + c0 = 273.16 (degC) + thinf = -66 (mV) : inact inf slope + qinf = 7 (mV) : inact inf slope + q10tau = 4.5 : from Magee (1998) + v (mV) + q10g = 2.0 : Rothman... +} + + +ASSIGNED { + celsius (degC) + i (mA/cm2) + gh (mho/cm2) + eh (mV) : must be explicitly def. in hoc + hinf + tau1 (ms) + tau2 (ms) + qg () : computed q10 for gnabar based on q10g + q10 () + ssih +} + + +STATE { h1 h2 } + +BREAKPOINT { + SOLVE states METHOD derivimplicit + gh = qg*gbar*(h1*frac + h2*(1.0-frac)) + i = gh * (v - eh) +} + +INITIAL { + qg = q10g^((celsius-33.0)/10.0 (degC)) :note original measurements made at 33 C + q10 = q10tau^((celsius - 22.0)/10.0 (degC)) : if you don't like room temp, it can be changed! + rates(v) + h1=hinf + h2=hinf + ssih = 0. +} + +DERIVATIVE states { + rates(v) + h1' = (hinf - h1)/tau1 + h2' = (hinf - h2)/tau2 +} + +PROCEDURE rates(v (mV)) { + tau1 = bet1(v)/(q10*a01*(1.0+alp1(v))) + tau2 = bet2(v)/(q10*a02*(1.0+alp2(v))) + hinf = 1.0/(1.0+exp((v-thinf)/qinf)) +} + +FUNCTION alp1(v(mV)) { + alp1 = exp(1e-3*zeta1*(v-vhalf1)*F/(R*(c0+celsius))) +} + +FUNCTION bet1(v(mV)) { + bet1 = exp(1.e-3*zeta1*gm1*(v-vhalf1)*F/(R*(c0+celsius))) +} + +FUNCTION alp2(v(mV)) { + alp2 = exp(1.e-3*zeta2*(v-vhalf2)*F/(R*(c0+celsius))) +} + +FUNCTION bet2(v(mV)) { + bet2 = exp(1.e-3*zeta2*gm2*(v-vhalf2)*F/(R*(c0+celsius))) +} diff --git a/cnmodel/mechanisms/hcno_bo.mod b/cnmodel/mechanisms/hcno_bo.mod new file mode 100644 index 0000000..8da7466 --- /dev/null +++ b/cnmodel/mechanisms/hcno_bo.mod @@ -0,0 +1,103 @@ +TITLE h current for Octopus cells of Cochlear Nucleus +: From Bal and Oertel (2000) + +: Modified, P. Manis July 2014, 2017 +: Parameters from McGinley et al. paper + +NEURON { + THREADSAFE + SUFFIX hcnobo + NONSPECIFIC_CURRENT i + RANGE gbar, eh, gh, q10tau + GLOBAL hinf, tau1, tau2 +} + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (pS) = (picosiemens) + (um) = (micron) + R = (k-mole)(joule/degC) + F = (faraday)(kilocoulombs) +} + +PARAMETER { + gbar = 0.0005 (mho/cm2) + + vhalf1 = -70 (mV) : -50 (mV) : v 1/2 for forward + vhalf2 = -84 (mV) : v 1/2 for backward + gm1 = 0.3 :(mV) : slope for forward + gm2 = 0.6 : (mV) : slope for backward + zeta1 = 3 : (/ms) +: zeta2 = 3 : (/ms) + a01 = 4.8e-3 (/ms) : was 0.008 + a02 = 2.9e-3 (/ms) : was 0.0029 (/ms) + frac = 0.8 + c0 = 273.16 (degC) + thinf = -72.4 (mV) : inact inf slope + qinf = 5.3 (mV) : inact inf slope + q10tau = 4.5 : from Magee (1998) + v (mV) +} + + +ASSIGNED { + celsius (degC) + i (mA/cm2) + gh (mho/cm2) + eh (mV) : must be explicitly def. in hoc + hinf + tau1 (ms) + tau2 (ms) + q10 () + ssih + ct +} + + +STATE { h1 h2 } + +BREAKPOINT { + SOLVE states METHOD cnexp + : SOLVE states METHOD derivimplicit + gh = gbar*(h1*frac + h2*(1.0-frac)) + i = gh * (v - eh) +} + +INITIAL { + ct = 1e-3*zeta1*F/(R*(c0+celsius)) + + q10 = q10tau^((celsius - 33.0)/10.0 (degC)) : Measurements at 33 + rates(v) + h1=hinf + h2=hinf + ssih = 0. +} + +DERIVATIVE states { + rates(v) + h1' = (hinf - h1)/tau1 + h2' = (hinf - h2)/tau2 +} + +PROCEDURE rates(v (mV)) { + tau1 = bet1(v)/(q10*a01*(1.0+alp1(v))) + tau2 = bet2(v)/(q10*a02*(1.0+alp2(v))) + hinf = 1.0/(1.0+exp((v-thinf)/qinf)) +} + +FUNCTION alp1(v(mV)) { + alp1 = exp((v-vhalf1)*ct) +} + +FUNCTION bet1(v(mV)) { + bet1 = exp(gm1*(v-vhalf1)*ct) +} + +FUNCTION alp2(v(mV)) { + alp2 = exp((v-vhalf2)*ct) +} + +FUNCTION bet2(v(mV)) { + bet2 = exp(gm2*(v-vhalf2)*ct) +} diff --git a/cnmodel/mechanisms/iStim.mod b/cnmodel/mechanisms/iStim.mod new file mode 100755 index 0000000..f0c78ab --- /dev/null +++ b/cnmodel/mechanisms/iStim.mod @@ -0,0 +1,48 @@ +COMMENT +iStim + +This is a point current injection (like an electrode). +Positive values of the amplitude depolarize the cell +and in the presence of the extracellular mechanism there will be a change +in vext since i is not a transmembrane current but a current injected +directly to the inside of the cell. + +This is meant to be used with Vector Play... +ENDCOMMENT + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +DEFINE NSTEP 5 + +NEURON { + THREADSAFE + POINT_PROCESS iStim + RANGE dur, delay, iMax + ELECTRODE_CURRENT i +} +UNITS { + (nA) = (nanoamp) +} +PARAMETER { + dur (ms) <0,1e9> + delay (ms) <0,1e9> + iMax (nA) +} +ASSIGNED { +i (nA) +} + +INITIAL { + i = 0 +} + +BREAKPOINT { +COMMENT +if(t < delay || t > (delay+dur)) { + i = 0 + } + if(t >= delay && t <= (delay+dur)) { + i = iMax + } +ENDCOMMENT +} diff --git a/cnmodel/mechanisms/ihpkj.mod b/cnmodel/mechanisms/ihpkj.mod new file mode 100644 index 0000000..6575898 --- /dev/null +++ b/cnmodel/mechanisms/ihpkj.mod @@ -0,0 +1,56 @@ +: Ih current +: Created 8/6/02 - nwg + +NEURON { + THREADSAFE + SUFFIX hpkj + NONSPECIFIC_CURRENT i + RANGE gbar, gh, eh + GLOBAL ninf, ntau +} + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (S) = (siemens) +} + +PARAMETER { + v (mV) + + gbar = .0001 (S/cm2) + + eh = -30 (mV) +} + +ASSIGNED { + gh (mho/cm2) + i (mA/cm2) + ninf + ntau (ms) +} + +STATE { + n +} + +INITIAL { + rates(v) + n = ninf +} + +BREAKPOINT { + SOLVE states METHOD cnexp + gh = gbar * n + i = gh*(v - eh) +} + +DERIVATIVE states { + rates(v) + n' = (ninf - n)/ntau +} + +PROCEDURE rates(v (mV)) { + ninf = 1/(1+exp((v+90.1(mV))/9.9(mV))) + ntau = (1000) * (0.19 (s) + 0.72 (s)*exp(-((v-(-81.5(mV)))/11.9(mV))^2)) +} \ No newline at end of file diff --git a/cnmodel/mechanisms/ihpyr.mod b/cnmodel/mechanisms/ihpyr.mod new file mode 100644 index 0000000..7684ded --- /dev/null +++ b/cnmodel/mechanisms/ihpyr.mod @@ -0,0 +1,134 @@ +TITLE ihpyr.mod DCN pyramidal cell model H-current + +COMMENT + +This model is part a Dorsal Cochlear Nucleus Pyramidal point cell +based on kinetic data from Kanold and Manis (1999) and Kanold's dissertation (1999) + +-- 15 Jan 1999 P. Manis + +Added export of start states for some variables to do perturbation tests +These start values replace the "inf" values used in the initialization procedure +Note that if the start variable is set to a value less than 0, +then the default initialization will be done. Typically I use a value of -1 for this flagging +Note also that it is possible to set the initial values > 1 but this is meaningless in terms of +the present equations. +-- 5 Feb 1999 P. Manis + +Added Patrick's version of ih as ihpyr +Model is from Destexhe and Babloyantz 1993; Destexhe et al. 1993 + + +2/10/02. P. Manis. +7/23/2014 P. Manis - separated from pyr.mod. + +ENDCOMMENT + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) +} + + +NEURON { + THREADSAFE + SUFFIX ihpyr + USEION na READ ena WRITE ina + USEION k READ ek WRITE ik + NONSPECIFIC_CURRENT i +: USEION h READ eh WRITE ih VALENCE 1 + RANGE eh +: + RANGE gh, kh_m_inf, kh_n_inf, aih, gbar, ghvshift + RANGE kh_m_tau, kh_n_tau + +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ek (mV) : = -81.5 (mV) + ena (mV) : = 50.0 (mV) + gbar = 0.00025 (mho/cm2) <0,1e9> + ghvshift = 0 (mV) + eh (mV) : = -43.0(mV) +} + +STATE { + khm khn +} + +ASSIGNED { + gh (mho/cm2) + ina (mA/cm2) + ik (mA/cm2) + ih (mA/cm2) + i (mA/cm2) + kh_m_inf kh_n_inf + kh_m_tau kh_n_tau + aih +} + +BREAKPOINT { + SOLVE states METHOD cnexp + aih = khm*khn + gh = gbar*aih + ih = gh*(v - eh) + i = ih +} + +UNITSOFF + +INITIAL { + rates(v) + khm = kh_m_inf + khn = kh_n_inf +} + + +DERIVATIVE states { + rates(v) + khm' = (kh_m_inf - khm) / kh_m_tau + khn' = (kh_n_inf - khn) / kh_n_tau +} + + +LOCAL q10 + +PROCEDURE rates(v(mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + q10 = 3^((celsius - 22)/10 (degC)) + + :"kh" adaptation of Destexhe hyp-activated cation current by Patrick Kanold + kh_m_inf = kh_m(v) + kh_n_inf = kh_n(v) + kh_m_tau = kh_mt(v) + kh_n_tau = kh_nt(v) +} + +: Make these as functions so we can view them from hoc, although this +: may slow things down a bit + + +FUNCTION kh_m(x (mV)) { + kh_m = 1/(1+exp((x+68.9+ghvshift)/6.5)) +} + +FUNCTION kh_n(x (mV)) { + kh_n = 1/(1+exp((x+68.9+ghvshift)/6.5)) : same as kh_m, but for completeness, compute this +} + +FUNCTION kh_mt(v (mV)) { + kh_mt = exp((v+183.6+ghvshift)/15.24) +} + +FUNCTION kh_nt(v (mV)) { + kh_nt = exp((v+158.6+ghvshift)/11.2)/(1+exp((v+75+ghvshift)/5.5)) +} + + + diff --git a/cnmodel/mechanisms/ihpyr_adj.mod b/cnmodel/mechanisms/ihpyr_adj.mod new file mode 100644 index 0000000..8bc96fb --- /dev/null +++ b/cnmodel/mechanisms/ihpyr_adj.mod @@ -0,0 +1,142 @@ +TITLE ihpyr_adj.mod DCN pyramidal cell model H-current + +COMMENT + +This model is part a Dorsal Cochlear Nucleus Pyramidal point cell +based on kinetic data from Kanold and Manis (1999) and Kanold's dissertation (1999) + +-- 15 Jan 1999 P. Manis + +Added export of start states for some variables to do perturbation tests +These start values replace the "inf" values used in the initialization procedure +Note that if the start variable is set to a value less than 0, +then the default initialization will be done. Typically I use a value of -1 for this flagging +Note also that it is possible to set the initial values > 1 but this is meaningless in terms of +the present equations. +-- 5 Feb 1999 P. Manis + +Added Patrick's version of ih as ihpyr +Model is from Destexhe and Babloyantz 1993; Destexhe et al. 1993 + + +2/10/02. P. Manis. +7/23/2014 P. Manis - separated from pyr.mod. + +7/23/2018 P. Manis - created "ihpyr_adj" +ihpyr_adj has an adjustable q10 for fitting against experimental data + +ENDCOMMENT + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) +} + + +NEURON { + THREADSAFE + SUFFIX ihpyr_adj + USEION na READ ena WRITE ina + USEION k READ ek WRITE ik + NONSPECIFIC_CURRENT i +: USEION h READ eh WRITE ih VALENCE 1 + RANGE eh +: + RANGE gh, kh_m_inf, kh_n_inf, aih, gbar, ghvshift + RANGE kh_m_tau, kh_n_tau + GLOBAL q10, q10f + +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ek (mV) : = -81.5 (mV) + ena (mV) : = 50.0 (mV) + gbar = 0.00025 (mho/cm2) <0,1e9> + ghvshift = 0 (mV) + eh (mV) : = -43.0(mV) + q10 = 3.0 (1) +} + +STATE { + khm khn +} + +ASSIGNED { + gh (mho/cm2) + ina (mA/cm2) + ik (mA/cm2) + ih (mA/cm2) + i (mA/cm2) + kh_m_inf kh_n_inf + kh_m_tau kh_n_tau + aih + q10f (1) +} + +BREAKPOINT { + SOLVE states METHOD cnexp + aih = khm*khn + gh = gbar*aih + ih = gh*(v - eh) + i = ih +} + +UNITSOFF + +INITIAL { + rates(v) + khm = kh_m_inf + khn = kh_n_inf +} + + +DERIVATIVE states { + rates(v) + khm' = (kh_m_inf - khm) / kh_m_tau + khn' = (kh_n_inf - khn) / kh_n_tau +} + + +: LOCAL q10f + +PROCEDURE rates(v(mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + q10f = q10^((celsius - 22)/10 (degC)) + + :"kh" adaptation of Destexhe hyp-activated cation current by Patrick Kanold + : adding q10 does not shift activation curves + kh_m_inf = kh_m(v) + kh_n_inf = kh_n(v) + : adding the q10 just changes the rate for the taus (1/(a+b)) + kh_m_tau = kh_mt(v)/q10f + kh_n_tau = kh_nt(v)/q10f +} + +: Make these as functions so we can view them from hoc, although this +: may slow things down a bit + + +FUNCTION kh_m(x (mV)) { + kh_m = 1/(1+exp((x+68.9+ghvshift)/6.5)) +} + +FUNCTION kh_n(x (mV)) { + kh_n = 1/(1+exp((x+68.9+ghvshift)/6.5)) : same as kh_m, but for completeness, compute this +} + +FUNCTION kh_mt(v (mV)) { + kh_mt = exp((v+183.6+ghvshift)/15.24) +} + +FUNCTION kh_nt(v (mV)) { + kh_nt = exp((v+158.6+ghvshift)/11.2)/(1+exp((v+75+ghvshift)/5.5)) +} + + + diff --git a/cnmodel/mechanisms/ihsgc_apical.mod b/cnmodel/mechanisms/ihsgc_apical.mod new file mode 100755 index 0000000..65cc09d --- /dev/null +++ b/cnmodel/mechanisms/ihsgc_apical.mod @@ -0,0 +1,155 @@ +TITLE ihsgc-apical.mod - Spiral Ganglion Cell Ih current for Apical Region + +COMMENT +Ih for Spiral ganglion cells. +Kinetcs are based on average fits to mouse SGCs, +This model is for just the apical cell group. +Data used to establish the kinetic parameters were collected by +Qing Liu and Robin Davis (Rutgers). +Data were taken at room temperature. +Kinetic parameters were extracted by curve fitting for fast and +slow components from activation and deactivation (using +the program Ihfit4b.py). + +Implementation by Paul B. Manis, January-April, 2012. +Revised December 2013, January 2014. + # of parameters in the fit were decreased (tau uses one v and scale factor). +Parameters are shown in the tables in Liu et al., JARO 2014. + +March 13, 2014: Corrected version with boltzmax for slow component +July 2014: made threadsafe, changed solver + +pmanis@med.unc.edu + +Note: vshift parameter is nominally 0. This parameter can +shift the entire activation and rate curves, keeping them +in register for each component of the conductance. + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX ihsgcApical + NONSPECIFIC_CURRENT i + RANGE gbar, gh, ih, eh, vshift + RANGE vh, k, vhs, ks + RANGE rinf, rtau, sinf, stau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius = 22 (degC) + dt (ms) + gbar = 0.00318 (mho/cm2) <0,1e9> + eh = -41 (mV) + +: Parameters from kinetic analysis +: Format for NEURON MOD file: + +: (Run on date = 2014-01-01 12:55:35.786524 ) + +: lmfit, Constrained model t(v) = DC + 1/(a * exp((v+vh)/k1) + a*exp(-(v+vh)/k2)) +: A. Fast component (Fast trace): + +: Boltzmann: + vh = -101.831 (mV) + k = 12.431 (mV) + vshift = 0.0 (mV) + afast = 0.4225 : fraction that is fast. + +: Tau + taufac = 1.0 (1) + taumin = 0 (ms) + tausc1 = 0.00445778 (/ms) : (ms) + vtau1 = 87.0705 (mV) + kfac1 = 53.0338 (mV) + kfac2 = 21.5365 (mV) + + +: B. Slow component (Cyan trace): +: (Run on date = 2014-01-01 12:55:35.786883 ) + +: Boltzmann: + svh1 = -86.762 (mV) + sk1 = 4.430 (mV) : double boltzmann + svh2 = -115.227 (mV) + sk2 = 9.675 (mV) + svshift = 0.0 (mV) + sba2 = 0.400557 : relative amplitude slow component 2 compared to slow 1 (slow2/(slow2+slow1)) + aslow = 0.5775 : total slow + boltzmax = 0.5019571 : normalization factor + : (computed numerically in Sage to make double boltz max = 1.0) + +: stau + staufac = 1.0 (1) + staumin = 0 (ms) + stausc1 = 0.00093656 (/ms) : (ms) + svtau1 = 89.6097 (mV) + skfac1 = 25.392 (mV) + skfac2 = 26.4195 (mV) + +} + +STATE { + r + s +} + +ASSIGNED { + gh (mho/cm2) + i (mA/cm2) + ih (mA/cm2) + rinf + rtau (ms) + sinf + stau (ms) + q10 () +} + + +BREAKPOINT { + SOLVE states METHOD cnexp + gh = gbar*(afast*(r^2)+aslow*s) : Balance between fast and slow determined by afast and aslow + ih = gh*(v - eh) + i = ih +} + +INITIAL { + q10 = 3.0^((celsius - 22.0)/10.0 (degC)) : adjust for temperature... + rates(v) + r = rinf + s = sinf +} + +DERIVATIVE states { + rates(v) + r' = (rinf - r)/rtau + s' = (sinf - s)/stau +} + +LOCAL rt, st +PROCEDURE rates(v (mV)) { : Computes rate and activation at voltage = v. + +: fast component - standard HH-like kinetics. + rinf = 1.0 / (1+exp((v - vh + vshift) / k))^0.5 + rt = tausc1*exp((v + vtau1 + vshift) / kfac1) + tausc1*exp(-(v + vtau1 + vshift) / kfac2) + rtau = (taumin + taufac/rt) + +: slow component +: double boltzman activation function (decreasing conductance), unequal sharing. + sinf = 1. / (1 + exp((v - svh1 + vshift) / sk1)) + st = 1. / (1 + exp((v - svh2 + vshift) / sk2)) + sinf = (1-sba2)*sinf - sba2*st + sinf = sinf/boltzmax : make sinf [0..1] + stau = staufac / (stausc1*exp((v + svtau1 + vshift) / skfac1) + stausc1*exp(-(v + svtau1 + vshift) / skfac2)) + stau = (stau + staumin) +} + diff --git a/cnmodel/mechanisms/ihsgc_basalmiddle.mod b/cnmodel/mechanisms/ihsgc_basalmiddle.mod new file mode 100755 index 0000000..b034154 --- /dev/null +++ b/cnmodel/mechanisms/ihsgc_basalmiddle.mod @@ -0,0 +1,154 @@ +TITLE ihsgc-basalmiddle.mod - Spiral Ganglion Cell Ih current for basal and middle Regions + +COMMENT +Ih for Spiral ganglion cells. +Kinetcs are based on average fits to mouse SGCs, +This model is for the basal and middle cell groups (averaged). +Data used to establish the kinetic parameters were collected by +Qing Liu and Robin Davis (Rutgers). +Data were taken at room temperature. +Kinetic parameters were extracted by curve fitting for fast and +slow components from activation and deactivation (using +the program Ihfit4b.py). + +Implementation by Paul B. Manis, January-April, 2012. +Revised December 2013, January 2014. + # of parameters in the fit were decreased (tau uses one v and scale factor). +Parameters are shown in the tables in Liu et al., JARO 2014. + +March 13, 2014: Corrected version with boltzmax for slow component +July 2014: made threadsafe, changed solver + +pmanis@med.unc.edu + +Note: vshift parameter is nominally 0. This parameter can +shift the entire activation and rate curves, keeping them +in register for each component of the conductance. + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX ihsgcBasalMiddle + NONSPECIFIC_CURRENT i + RANGE gbar, gh, ih, eh, vshift + RANGE vh, k, vhs, ks + RANGE rinf, rtau, sinf, stau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius = 22 (degC) + dt (ms) + gbar = 0.00318 (mho/cm2) <0,1e9> + eh = -41 (mV) + +: Parameters from kinetic analysis +: Formatted for NEURON MOD file +: (Run on date = 2014-01-01 12:52:22.776598 ) + +: lmfit, Constrained model t(v) = DC + 1/(a * exp((v+vh)/k1) + a*exp(-(v+vh)/k2)) +: A. Fast component (Fast trace): + +: Boltzmann: + vh = -105.298 (mV) + k = 12.359 (mV) + vshift = 0.0 (mV) + afast = 0.4471 : fraction that is fast. + +: Tau + taufac = 1.0 (1) + taumin = 0 (ms) + tausc1 = 0.00417518 (/ms) + vtau1 = 87.0836 (mV) + kfac1 = 28.1667 (mV) + kfac2 = 21.4809 (mV) + + +: B. Slow component: +: (Run on date = 2014-01-01 12:52:22.777259 ) +: Boltzmann: + svh1 = -91.860 (mV) + sk1 = 4.883 (mV) : double boltzmann + svh2 = -110.209 (mV) + sk2 = 3.927 (mV) + svshift = 0.0 (mV) + sba2 = 0.337216 : relative amplitude slow component 2 compared to slow 1 (slow2/(slow2+slow1)) + aslow = 0.5529 : total slow + boltzmax = 0.5551729 : normalization factor + : (computed numerically in Sage to make double boltz max = 1.0) + +: stau + staufac = 1.0 (1) + staumin = 0 (ms) + stausc1 = 0.00104354 (/ms) + svtau1 = 105.816 (mV) + skfac1 = 40.0291 (mV) + skfac2 = 20.2273 (mV) + +} + +STATE { + r + s +} + +ASSIGNED { + gh (mho/cm2) + i (mA/cm2) + ih (mA/cm2) + rinf + rtau (ms) + sinf + stau (ms) + q10 () +} + + +BREAKPOINT { + SOLVE states METHOD cnexp + gh = gbar*(afast*(r^2)+aslow*s) : Balance between fast and slow determined by afast and aslow + ih = gh*(v - eh) + i = ih +} + + +INITIAL { + q10 = 3.0^((celsius - 22.0)/10.0 (degC)) : adjust for temperature... + rates(v) + r = rinf + s = sinf +} + +DERIVATIVE states { : Updates state variables r and s + rates(v) : at the current voltage + r' = (rinf - r )/rtau + s' = (sinf - s)/stau +} + +LOCAL rt, st +PROCEDURE rates(v (mV)) { : Computes rate and activation at voltage = v. + +: fast component - standard HH-like kinetics. + rinf = 1.0 / (1+exp((v - vh + vshift) / k))^0.5 + rt = tausc1*exp((v + vtau1 + vshift) / kfac1) + tausc1*exp(-(v + vtau1 + vshift) / kfac2) + rtau = (taumin + taufac/rt) + +: slow component +: double boltzman activation function (decreasing conductance), unequal sharing. + sinf = 1. / (1 + exp((v - svh1 + vshift) / sk1)) + st = 1. / (1 + exp((v - svh2 + vshift) / sk2)) + sinf = (1-sba2)*sinf - sba2*st + sinf = sinf/boltzmax : make sinf [0..1] + + stau = staufac / (stausc1*exp((v + svtau1 + vshift) / skfac1) + stausc1*exp(-(v + svtau1 + vshift) / skfac2)) + stau = (stau + staumin) +} diff --git a/cnmodel/mechanisms/ihvcn.mod b/cnmodel/mechanisms/ihvcn.mod new file mode 100644 index 0000000..c10d4e0 --- /dev/null +++ b/cnmodel/mechanisms/ihvcn.mod @@ -0,0 +1,79 @@ +TITLE jsr.mod VCN conductances + +COMMENT +Ih for VCN neurons - average from several studies in auditory neurons + +Implementation by Paul B. Manis, April (JHU) and Sept, (UNC)1999. +revised 2/28/04 pbm + +pmanis@med.unc.edu + +Modifed implementation; includes all temperature scaling, passes modlunit +7/10/2014 pbm + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX ihvcn + NONSPECIFIC_CURRENT i + RANGE gbar, gh, i, eh + GLOBAL rinf, rtau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + dt (ms) + gbar = 0.00318 (mho/cm2) <0,1e9> + q10tau = 3.0 +} + +STATE { + r +} + +ASSIGNED { + celsius (degC) + gh (mho/cm2) + eh (mV) + i (mA/cm2) + rinf + rtau (ms) + q10 () +} + +BREAKPOINT { + SOLVE states METHOD cnexp + + gh = gbar*r + i = gh*(v - eh) +} + +INITIAL { + q10 = q10tau^((celsius - 22)/10 (degC)) : if you don't like room temp, it can be changed! + rates(v) + r = rinf +} + +DERIVATIVE states { :Computes state variables m, h, and n + rates(v) : at the current v and dt. + r' = (rinf - r)/rtau +} + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + rinf = 1 / (1+exp((v + 76) / 7 (mV))) + rtau = (100000 (ms)/ (237*exp((v+60) / 12 (mV)) + 17*exp(-(v+60) / 14 (mV)))) + 25 + rtau = rtau/q10 + +} + diff --git a/cnmodel/mechanisms/inav11.mod b/cnmodel/mechanisms/inav11.mod new file mode 100755 index 0000000..4b3c454 --- /dev/null +++ b/cnmodel/mechanisms/inav11.mod @@ -0,0 +1,188 @@ +: +: ichanWT2005.mod +: +: Alan Goldin Lab, University of California, Irvine +: Jay Lickfett - Last Modified: 6 July 2005 +: +: This file is the Nav1.1 wild-type channel model described in: +: +: Barela et al. An Epilepsy Mutation in the Sodium Channel SCN1A That Decreases +: Channel Excitability. J. Neurosci. 26(10): p. 2714-2723 +: +: +: The model is derived from the one described in: +: +: Spampanato et al. (2004a) Increased Neuronal Firing in Computer Simulations +: of Sodium Channel Mutations that Cause Generalized Epilepsy with Febrile Seizures Plus. +: Journal of Neurophysiology 91:2040-2050 +: +: and +: +: Spampanato et al. (2004b) A Novel Epilepsy Mutation +: in the Sodium Channel SCN1A Identifies a Cytoplasmic Domain for +: Beta Subunit Interaction. J. Neurosci. 24(44):10022-10034 +: + +: delayed rectifier removed (p.b.manis 2/22/2009) + + + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (uF) = (microfarad) + (molar) = (1/liter) + (nA) = (nanoamp) + (mM) = (millimolar) + (um) = (micron) + (S) = (siemens) + FARADAY = 96520 (coul) + R = 8.3134 (joule/degC) + +} + + +NEURON { + THREADSAFE + SUFFIX nav11 + USEION na READ ena WRITE ina VALENCE 1 + RANGE gna + RANGE gbar + RANGE minf, mtau, hinf, htau, sinf, stau, inat, m, h, s + RANGE vsna : voltage shift parameter +} + + +INDEPENDENT {t FROM 0 TO 100 WITH 100 (ms)} + + +PARAMETER { + vsna = 4.3 (mV) + celsius (degC) + dt (ms) + ena (mV) + :enat = 50 (mV) + gbar = 0.1 (mho/cm2) + q10 = 3.0 (1) +} + + +ASSIGNED { + + v (mV) + gna (mho/cm2) + ina (mA/cm2) + minf hinf sinf + mtau (ms) htau (ms) stau (ms) + mexp hexp sexp +: vsna (mV) +} + + +STATE { + m h s +} + + +BREAKPOINT { + SOLVE states METHOD cnexp + gna = gbar*m*m*m*h*s + ina = gna*(v - ena) +} + + +UNITSOFF + + +INITIAL { + + trates(v) + + m = minf + h = hinf + s = sinf + +} + + +DERIVATIVE states { : Computes state variables m, h, s and n + : at the current v and dt. + rates(v) + m' = (minf - m)/mtau + h' = (hinf - h)/htau + s' = (sinf - s)/stau + +} + + +LOCAL qt + + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + LOCAL alpha, beta, sum + qt = q10^((celsius - 22)/10) : original recordings in Barela et al made at "room temperature" + + + : "m" sodium activation system + minf = f_minf(v) + mtau = f_mtau(v)/qt + + : "h" sodium fast inactivation system + hinf = f_hinf(v) + htau = f_htau(v)/qt + + : "s" sodium slow inactivation system + sinf = f_sinf(v) + stau = f_stau(v)/qt + +} + + +PROCEDURE trates(v (mV)) { :Build table with rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + LOCAL tinc + + TABLE minf, mexp, hinf, hexp, sinf, sexp, mtau, htau, stau + DEPEND dt, celsius FROM -100 TO 100 WITH 200 + + rates(v) : not consistently executed from here if usetable_hh == 1 + : so don't expect the tau values to be tracking along with + : the inf values in hoc + + tinc = -dt : * q10 q10 is handled in rates, above + mexp = 1 - exp(tinc/mtau) + hexp = 1 - exp(tinc/htau) + sexp = 1 - exp(tinc/stau) +} + +FUNCTION f_minf(v (mV)) { + f_minf = 1/(1+exp(-(v+27.4+vsna)*4.7*0.03937)) + + } +FUNCTION f_mtau(v (mV)) { + f_mtau = 0.15 + } + +FUNCTION f_hinf(v (mV)) { + f_hinf = 1/(1+exp((v+41.9+vsna)/6.7)) + } + +FUNCTION f_htau(v (mV)) { + f_htau = 23.12*exp(-0.5*((v+77.58+vsna)/43.92)^2) + } + + +FUNCTION f_sinf(v (mV)) { + f_sinf = 1/(1+exp((v+46.0+vsna)/6.6)) + } + +FUNCTION f_stau(v (mV)) { + f_stau = 1000*140.4*exp(-0.5*((v+71.3+vsna)/30.9)^2) + } + + +UNITSON + diff --git a/cnmodel/mechanisms/jsrnaf.mod b/cnmodel/mechanisms/jsrnaf.mod new file mode 100644 index 0000000..a8aad08 --- /dev/null +++ b/cnmodel/mechanisms/jsrnaf.mod @@ -0,0 +1,136 @@ +TITLE jsrnaf.mod VCN Na conductance, fast model + +COMMENT +gnaf is the modified form used in his +1993 M.S. thesis (as in Rothman et al., J. Neurophysiol. 70:2562, 1993), +with rapid recovery from inactivation for potentials below rest. + +Implementation by Paul B. Manis, April and Sept, 1999. +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +? interface +NEURON { +THREADSAFE + SUFFIX jsrna + USEION na READ ena WRITE ina + RANGE gbar + RANGE gna, vsna + RANGE minf, hinf, mtau, htau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + dt (ms) + ena = 55 (mV) + gbar = 0.25 (mho/cm2) <0,1e9> + vsna = 0 (mV) + q10 = 3.0 (1) +} + +STATE { + m h +} + +ASSIGNED { + gna (mho/cm2) + ina (mA/cm2) + minf hinf + mtau (ms) htau (ms) + celsius (degC) +} + +LOCAL mexp, hexp + +? currents +BREAKPOINT { + SOLVE states METHOD cnexp + gna = gbar*(m^3)*h + ina = gna*(v - ena) +} + +UNITSOFF + +INITIAL { + trates(v) + m = minf + h = hinf +} + +DERIVATIVE states { :Computes state variables m, h, and n + trates(v) : at the current v and dt. + m' = (minf - m)/mtau + h' = (hinf - h)/htau +} + +LOCAL qt + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. +LOCAL alpha, beta, sum + + qt = q10^((celsius - 22)/10 (degC)) : R&M'03 used 3 + +: Note qt temperature here cancels in minf (a/(a+b)) +:"m" sodium activation system - JSR + alpha = -0.36*qt*vtrap((v+49),-3) + beta = 0.4*qt*vtrap((v+58),20) + sum = alpha + beta + mtau = 1/sum + minf = alpha/sum + +:"h" sodium inactivation system - JSR + alpha = 2.4*qt/(1+exp((v+68-vsna)/3 (mV))) + 0.8*qt/(1+exp(v+61.3-vsna)) + beta = 3.6*qt/(1+exp(-(v+21-vsna)/10 (mV))) + sum = alpha + beta + htau = 1/sum + hinf = alpha/sum + + + + +: jsr modified sodium channel - defined in terms of alpha and beta this time +: am = (0.36*q10*(v+49))/(1-exp(-((v+49)/3))) +: am = -(0.36*q10*vtrap(-(v+49),3)) +: bm = -(0.40*q10*(v+58))/(1-exp((v+58)/20)) +: bm = (0.40*q10*vtrap((v+58),20)) +: ah = ((2.4*q10)/(1+exp((v+68)/3))) + (0.8*qten/(1+exp(v+61.3))) +: bh = (3.6*q10)/(1+exp(-(v+21)/10)) + +: minf = am/(am+bm) +: hinf = ah/(ah+bh) + +: mtau = 1/(am+bm) +: htau = 1/(ah+bh) + +} + +PROCEDURE trates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + LOCAL tinc + + rates(v) : not consistently executed from here if usetable_hh == 1 + : so don't expect the tau values to be tracking along with + : the inf values in hoc + + tinc = -dt : * q10 # handled in rates now + mexp = 1 - exp(tinc/mtau) + hexp = 1 - exp(tinc/htau) +} + +FUNCTION vtrap(x,y) { :Traps for 0 in denominator of rate eqns. + if (fabs(x/y) < 1e-6) { + vtrap = y*(1 - x/y/2) + }else{ + vtrap = x/(exp(x/y) - 1) + } +} + + diff --git a/cnmodel/mechanisms/ka.mod b/cnmodel/mechanisms/ka.mod new file mode 100644 index 0000000..c8cf1cf --- /dev/null +++ b/cnmodel/mechanisms/ka.mod @@ -0,0 +1,104 @@ +TITLE klt.mod The low threshold conductance of cochlear nucleus neurons + +COMMENT + +NEURON implementation of Jason Rothman's measurements of VCN conductances. + +This file implements the transient potassium current found in ventral cochlear +nucleus "Type I" cells, which are largely "stellate" or "multipolar" cells (Manis and +Marx, 1991; Rothman and Manis, 2003a,b; Manis et al, 1996). The current is likely + mediated by Kv4.2 potassium channel subunits, but this has not been directly +demonstrated. The specific implementation is described in Rothman and Manis, J. +Neurophysiol. 2003, in the appendix. Measurements were made from isolated +neurons from adult guinea pig, under reasonably stringent voltage clamp conditions. + The measured current is sensitive to 4-aminopyridine. +Original implementation by Paul B. Manis, April (JHU) and Sept, (UNC)1999. + +File split implementaiton, April 1, 2004. + +Contact: pmanis@med.unc.edu + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX ka + USEION k READ ek WRITE ik + RANGE gbar, gka, ik + GLOBAL ainf, binf, cinf, atau, btau, ctau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + dt (ms) + gbar = 0.00477 (mho/cm2) <0,1e9> + q10tau = 3.0 + q10g = 2.0 +} + +STATE { + a b c +} + +ASSIGNED { + celsius (degC) : model is defined on measurements made at room temp in Baltimore + ik (mA/cm2) + ek (mV) + gka (mho/cm2) + ainf binf cinf + atau (ms) btau (ms) ctau (ms) + qg () : computed q10 for gnabar based on q10g + q10 () +} + +LOCAL aexp, bexp, cexp + +BREAKPOINT { + SOLVE states METHOD cnexp + + gka = gbar*(a^4)*b*c + ik = gka*(v - ek) + +} + + +INITIAL { + qg = q10g^((celsius-22)/10 (degC)) + q10 = q10tau^((celsius - 22)/10 (degC)) : if you don't like room temp, it can be changed! + rates(v) + a = ainf + b = binf + c = cinf +} + +DERIVATIVE states { :Computes state variables m, h, and n + rates(v) : at the current v and dt. + a' = (ainf - a)/atau + b' = (binf - b)/btau + c' = (cinf - c)/ctau +} + + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + ainf = (1 / (1 + exp(-1*(v + 31) / 6 (mV))))^0.25 + binf = 1 / (1 + exp((v + 66) / 7 (mV)))^0.5 + cinf = 1 / (1 + exp((v + 66) / 7 (mV)))^0.5 + + atau = (100 (ms)/ (7*exp((v+60) / 14 (mV)) + 29*exp(-(v+60) / 24 (mV)))) + 0.1 + atau = atau/q10 + btau = (1000 (ms) / (14*exp((v+60) / 27 (mV)) + 29*exp(-(v+60) / 24 (mV)))) + 1 + btau = btau/q10 + ctau = (90 (ms)/ (1 + exp((-66-v) / 17 (mV)))) + 10 + ctau = ctau/q10 +} + diff --git a/cnmodel/mechanisms/kcnq.mod b/cnmodel/mechanisms/kcnq.mod new file mode 100644 index 0000000..e71e6fa --- /dev/null +++ b/cnmodel/mechanisms/kcnq.mod @@ -0,0 +1,72 @@ +TITLE KCNQ potassium channel for GPe neuron + +COMMENT + modeled by Gunay et al., 2008 + implemented in NEURON by Kitano, 2011 +Threadsafe and unit checking, P.B. Manis, 2014 + + ENDCOMMENT + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) +} + +NEURON { + THREADSAFE + SUFFIX kcnq + USEION k READ ek WRITE ik + RANGE gbar, gk, iKCNQ +} + +PARAMETER { + v (mV) + dt (ms) + gbar = 0.001 (mho/cm2) + iKCNQ = 0.0 (mA/cm2) + ek (mV) + + theta_m = -61.0 (mV) + k_m = 19.5 (mV) + tau_m0 = 6.7 (ms) + tau_m1 = 100.0 (ms) + phi_m = -61.0 (mV) + sigma_m0 = 35.0 (mV) + sigma_m1 = -25.0 (mV) +} + +STATE { + m +} + +ASSIGNED { + ik (mA/cm2) + gk (mho/cm2) + minf + taum (ms) +} + +BREAKPOINT { + SOLVE states METHOD cnexp + gk = gbar*m*m*m*m + ik = gk * (v-ek) + iKCNQ = ik +} + + +INITIAL { + settables(v) + m = minf +} + +DERIVATIVE states { + settables(v) + m' = (minf - m)/taum +} + +PROCEDURE settables(v (mV)) { + TABLE minf, taum FROM -100 TO 100 WITH 400 + + minf = 1.0 / (1.0 + exp((theta_m - v)/k_m)) + taum = tau_m0 + (tau_m1 - tau_m0)/(exp((phi_m - v)/sigma_m0) + exp((phi_m - v)/sigma_m1)) +} diff --git a/cnmodel/mechanisms/kdpyr.mod b/cnmodel/mechanisms/kdpyr.mod new file mode 100644 index 0000000..2e1334b --- /dev/null +++ b/cnmodel/mechanisms/kdpyr.mod @@ -0,0 +1,92 @@ +TITLE kdpyr.mod DCN pyramidal cell model, delayed rectifier + +COMMENT + +This is part of a model implements a Dorsal Cochlear Nucleus Pyramidal point cell +based on kinetic data from Kanold and Manis (1999) and Kanold's dissertation (1999) + +-- 15 Jan 1999 P. Manis + +Added export of start states for some variables to do perturbation tests +These start values replace the "inf" values used in the initialization procedure +Note that if the start variable is set to a value less than 0, +then the default initialization will be done. Typically I use a value of -1 for this flagging +Note also that it is possible to set the initial values > 1 but this is meaningless in terms of +the present equations. +-- 5 Feb 1999 P. Manis + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) +} + +NEURON { + THREADSAFE + SUFFIX kdpyr + USEION k READ ek WRITE ik + RANGE gbar, gk : delayed rectifier + RANGE ntau: time constants delayed rectifier + RANGE kd_avh + +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ek (mV) : = -81.5 (mV) + gbar = 0.006667 (mho/cm2) <0,1e9> + ntau = 0.5 (ms) <0.1,100> + kd_avh = -40 (mV) + } + +STATE { + n +} + +ASSIGNED { + gk (mho/cm2) + ik (mA/cm2) + ninf +} + +LOCAL nexp + +BREAKPOINT { + SOLVE states METHOD cnexp + gk = gbar*n*n + ik = gk*(v - ek) +} + +INITIAL { + rates(v) + n = ninf +} + +DERIVATIVE states { + rates(v) + n' = (ninf - n) / ntau +} + +LOCAL q10 + +PROCEDURE rates(v(mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + LOCAL alpha, beta, sum + TABLE ninf, ntau DEPEND celsius FROM -200 TO 100 WITH 400 + + q10 = 3^((celsius - 22)/10 (degC)) + : "n" potassium activation system + ninf = kd_m(v) +} + +: Make these as functions so we can view them from hoc, although this +: may slow things down a bit +FUNCTION kd_m(x (mV)) { : potassium activation + kd_m = 1/(1+exp(-(x-kd_avh)/(3 (mV)))) : flat time constants +} + diff --git a/cnmodel/mechanisms/kht.mod b/cnmodel/mechanisms/kht.mod new file mode 100644 index 0000000..9f8aa21 --- /dev/null +++ b/cnmodel/mechanisms/kht.mod @@ -0,0 +1,111 @@ +TITLE kht.mod The high threshold conductance of cochlear nucleus neurons + +COMMENT + +NEURON implementation of Jason Rothman's measurements of VCN conductances. + +This file implements the high threshold potassium current found in several brainstem + nuclei of the auditory system, including the spherical and globular bushy cells + (Manis and Marx, 1991; Rothman and Manis, 2003a,b) and multipolar (stellate) + cells of the ventral cochlear nucleus, principal cells of the medial + nucleus of the trapzoid body (Brew and Forsythe, 1995, Wang and Kaczmarek, + 1997) and neurons of the medial superior olive. The current is likely mediated by + Kv3.1 potassium channel subunits. The specific + implementation is described in Rothman and Manis, J. Neurophysiol. 2003, in the + appendix. Measurements were made from isolated neurons from adult guinea pig, + under reasonably stringent voltage clamp conditions. The measured current is + sensitive to 4-aminopyridine and TEA, but is spared by mamba snake toxi + dendrotoxin I. + + +Similar conductrances are found in the homologous neurons of the avian auditory +system (Reyes and Rubel; Zhang and Trussell; Rathouz and Trussell), and the +conductance described here, in the absence of more detailed kinetic measurements +, is probably suitable for use in modeling that system. + + +Original implementation by Paul B. Manis, April (JHU) and Sept, (UNC)1999. + +File split implementation, February 28, 2004. + +Contact: pmanis@med.unc.edu + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX kht + USEION k READ ek WRITE ik + RANGE gbar, gkht, ik, q10g + GLOBAL ninf, pinf, ntau, ptau +} + +:INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} +ASSIGNED { + celsius (degC) : model is defined on measurements made at room temp in Baltimore: 22 degC + ik (mA/cm2) + ek (mV) + gkht (mho/cm2) + pinf ninf + ptau (ms) + ntau (ms) + qg () : computed q10 for gnabar based on q10g + q10 () +} + +PARAMETER { + v (mV) + dt (ms) + gbar = 0.01592 (mho/cm2) <0,1e9> + nf = 0.85 <0,1> :proportion of n vs p kinetics + q10tau = 3.0 + q10g = 2.0 +} + +STATE { + n p +} + +LOCAL nexp, pexp + +BREAKPOINT { + SOLVE states METHOD cnexp + + gkht = qg*gbar*(nf*(n^2) + (1-nf)*p) + ik = gkht*(v - ek) +} + +INITIAL { + qg = q10g^((celsius-22)/10 (degC)) + q10 = q10tau^((celsius - 22)/10 (degC)) + rates(v) + p = pinf + n = ninf +} + +DERIVATIVE states { :Computes state variables m, h, and n + rates(v) : at the current v and dt. + n' = (ninf - n)/ntau + p' = (pinf - p)/ptau +} + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + ninf = (1 + exp(-(v + 15) / 5 (mV)))^-0.5 + pinf = 1 / (1 + exp(-(v + 23) / 6 (mV))) + + ntau = (100 (ms)/ (11*exp((v+60) / 24 (mV)) + 21*exp(-(v+60) / 23 (mV)))) + 0.7 + ntau = ntau/q10 + ptau = (100 (ms)/ (4*exp((v+60) / 32 (mV)) + 5*exp(-(v+60) / 22 (mV)))) + 5 + ptau = ptau/q10 + +} + + diff --git a/cnmodel/mechanisms/kif.mod b/cnmodel/mechanisms/kif.mod new file mode 100644 index 0000000..14694c9 --- /dev/null +++ b/cnmodel/mechanisms/kif.mod @@ -0,0 +1,128 @@ +TITLE kif.mod DCN pyramidal cell model fast transient current + +COMMENT + +This model implements a fast transient potassium current from +Dorsal Cochlear Nucleus Pyramidal cells +based on kinetic data from Kanold and Manis (1999) and Kanold's dissertation (1999) + -- 15 Jan 1999 P. Manis + 2/10/02. P. Manis. + Pulled from pyr.mod 7/24/2014 +ENDCOMMENT + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) +} + + NEURON { + SUFFIX kif + USEION k READ ek WRITE ik + + RANGE gkif, kif_a_inf, kif_i_inf : fast inactivating potassium current + RANGE akif, gbar + RANGE kif_a_tau, kif_i_tau + RANGE kif_a_start, kif_i_start + RANGE kif_ivh, kif_avh, kif_hivh +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ek (mV) : = -81.5 (mV) + ena (mV) : = 50.0 (mV) + gbar = 0.0125 (mho/cm2) <0,1e9> + + kif_ivh = -89.6 (mV) + kif_avh = -57.0 (mV) + kif_hivh = -87.0 (mV) + kif_a_start = -1 (1) + kif_i_start = -1 (1) + } + +STATE { + kifa kifi +} + +ASSIGNED { + gkif (mho/cm2) + ik (mA/cm2) + kif_a_inf (1) + kif_i_inf (1) + kif_a_tau (ms) + kif_i_tau (ms) + akif () + q10 () +} + + +BREAKPOINT { + SOLVE states METHOD cnexp + akif = kifa*kifa*kifa*kifa*kifi + gkif = gbar*akif + ik = gkif*(v - ek) +} + +INITIAL { + q10 = 3^((celsius - 22)/10 (degC)) + rates(v) + if(kif_a_start < 0) { : if xx_(i/a)_start is > 0, then perturbation is done at onset of computations. + kifa = kif_a_inf + } else { + kifa = kif_a_start + } + if(kif_i_start < 0) { + kifi = kif_i_inf + } else { + kifi = kif_i_start + } +} + +DERIVATIVE states { + rates(v) + kifa' = (kif_a_inf - kifa) / kif_a_tau + kifi' = (kif_i_inf - kifi) / kif_i_tau +} + + +PROCEDURE rates(v(mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. +: LOCAL alpha, beta, sum +: TABLE kif_a_inf, kif_a_tau, kif_i_inf, kif_i_tau DEPEND celsius, kif_avh, kif_ivh FROM -200 TO 100 WITH 400 + : "kif" fast inactivation potassium channel - activation and inactivation + kif_a_inf = kif_m(v) + kif_i_inf = kif_h(v) + kif_a_tau = kif_mt(v) + kif_i_tau = kif_ht(v) + } + +: Make these as functions so we can view them from hoc, although this +: may slow things down a bit + +FUNCTION kif_m(v ) { : ikif activation + kif_m = 1.0/(1+exp(-(v-kif_avh)/25.8 (mV))) +} + +FUNCTION kif_h(v (mV)) { : ikif inactivation + kif_h = 1.0/(1+exp((v-kif_ivh)/6.7 (mV))) +} + +FUNCTION kif_mt(v (mV)) (ms) { : ikif activation tau + LOCAL x + x = 0.15 * exp((v-kif_avh)/10 (mV)) + 0.3 *exp(-(v-kif_avh)/10 (mV)) + x = 0.5 + (1.0 /x) + kif_mt = (x * 1.0 (ms))/q10 +} + +FUNCTION kif_ht(v (mV)) (ms) { : ikif inactivation tau + LOCAL x + x = 0.015 * exp((v-kif_hivh)/20 (mV))+0.03*exp(-(v-kif_hivh)/20 (mV)) + x = 10 + (1./x) + kif_ht = (x * 1.0 (ms)) /q10 +} + + diff --git a/cnmodel/mechanisms/kir.mod b/cnmodel/mechanisms/kir.mod new file mode 100755 index 0000000..b567cf8 --- /dev/null +++ b/cnmodel/mechanisms/kir.mod @@ -0,0 +1,108 @@ +TITLE KIR channel +COMMENT +Reference: Steephen JE, Manchanda R (2009) Differences in biophysical +properties of nucleus accumbens medium spiny neurons emerging from +inactivation of inward rectifying potassium currents. J Comput Neurosci [PubMed] + +Found on ModelDB, 1/21/2013 PBManis + + +ENDCOMMENT + +NEURON { + SUFFIX KIR + USEION k READ ek WRITE ik + RANGE g, ik, gbar + GLOBAL minf, mtau +} + +UNITS { + (mA) = (milliamp) + (uA) = (microamp) + (mV) = (millivolt) + (mS) = (millimho) +} + +PARAMETER { + celsius (degC) + ek (mV) + gbar = 1.4e-4 (mho/cm2) <0,1e9> + m_vh = -82 (mV) : half activation + m_ve = 13 (mV) : slope +} + +ASSIGNED { + v (mV) + g (mho/cm2) + ik (mA/cm2) + minf (1) + mtau (ms) + qt (1) +} + +STATE { + m +} + +BREAKPOINT { + SOLVE states METHOD cnexp + g = gbar*m + ik = g*(v - ek) +} + +INITIAL { + qt = 3^((celsius-35)/10) + rates(v) + m = minf +} + +DERIVATIVE states { + rates(v) + m' = (minf-m)/mtau +} + +FUNCTION_TABLE tabmtau(v(mV)) (ms) + +: rates() computes rate and other constants at present v +: call once from hoc to initialize inf at resting v + +PROCEDURE rates(v(mV)) { +: mtau = tabmtau(v) + mtau = tabmtau(v)/qt + minf = 1/(1 + exp((v - m_vh)/m_ve)) +} + +COMMENT +/* TABLES +The tables here are built as vecs in hoc, and then loaded into the mod file arrays before use... + + * + * Steephen, J. E., & Manchanda, R. (2009). Differences in biophysical properties of nucleus accumbens medium spiny neurons emerging from inactivation of inward rectifying potassium currents. J Comput Neurosci, + * doi:10.1007/s10827-009-0161-7 + */ + +//KIR +objref vecmtau_KIR, vecv_KIR +vecmtau_KIR = new Vector() +vecv_KIR = new Vector() +vecv_KIR.indgen(-120, 0, 10) +vecmtau_KIR.append(7.465, 7.465, 7.465, 8, 9.435, 10.755, 12.12, 13.795, 15.385, 14.285, 11.765, 8.89, 8) // At 35 deg C +table_tabmtau_KIR(&vecmtau_KIR.x[0], vecv_KIR.size, &vecv_KIR.x[0]) + +//inKIR +objref vecv_inKIR, vechinf_inKIR, vechtau_inKIR, vecv_tau_inKIR +vecv_tau_inKIR = new Vector() +vechtau_inKIR= new Vector() +vecv_tau_inKIR.append(-120,-90, -50) +vechtau_inKIR.append(7.767, 15, 25.333) // At 35 deg C +table_tabhtau_inKIR(&vechtau_inKIR.x[0],vecv_tau_inKIR.size, &vecv_tau_inKIR.x[0]) + +vecv_inKIR = new Vector() +vechinf_inKIR = new Vector() +vecv_inKIR.append(-120,-90, -50) +vechinf_inKIR.append(0, 0.13, 1) +table_tabhinf_inKIR(&vechinf_inKIR.x[0], vecv_inKIR.size, &vecv_inKIR.x[0]) +table_tabmtau_inKIR(&vecmtau_KIR.x[0], vecv_KIR.size, &vecv_KIR.x[0]) + +ENDCOMMENT + diff --git a/cnmodel/mechanisms/kis.mod b/cnmodel/mechanisms/kis.mod new file mode 100644 index 0000000..42d844f --- /dev/null +++ b/cnmodel/mechanisms/kis.mod @@ -0,0 +1,123 @@ +TITLE kis.mod DCN pyramidal cell model Slow transient K current + +COMMENT + + +This model implements the slow transient potassium current from +Dorsal Cochlear Nucleus Pyramidal cells +based on kinetic data from Kanold and Manis (1999) and Kanold's dissertation (1999) + +-- 15 Jan 1999 P. Manis + +2/10/02, 7/24/2014. P. Manis. + +ENDCOMMENT + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) +} + + +NEURON { + THREADSAFE + SUFFIX kis + USEION k READ ek WRITE ik + RANGE gkis, kis_a_inf, kis_i_inf : fast inactivating potassium current + RANGE akis, gbar + RANGE kis_a_tau, kis_i_tau + RANGE kis_a_start, kis_i_start +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ek (mV) : = -81.5 (mV) + ena (mV) : = 50.0 (mV) + gbar = 0.0033333 (mho/cm2) <0,1e9> + + kis_ivh = -40.9 (mV) + kis_avh = -38.4 (mV) + kis_a_start = -1 + kis_i_start = -1 +} + +STATE { + kisa kisi +} + +ASSIGNED { + gkis (mho/cm2) + ik (mA/cm2) + kis_a_inf kis_i_inf + kis_a_tau (ms) + kis_i_tau (ms) + akis + q10 () +} + +BREAKPOINT { + SOLVE states METHOD cnexp + akis = kisa*kisa*kisa*kisa*kisi + gkis = gbar*akis + ik = gkis*(v - ek) +} + +INITIAL { + q10 = 3^((celsius - 22)/10 (degC)) + rates(v) + if(kis_a_start < 0) { : if xx_(i/a)_start is > 0, then perturbation is done at onset of computations. + kisa = kis_a_inf + } else { + kisa = kis_a_start + } + if(kis_i_start < 0) { + kisi = kis_i_inf + } else { + kisi = kis_i_start + } +} + + +DERIVATIVE states { + rates(v) + kisa' = (kis_a_inf - kisa) / kis_a_tau + kisi' = (kis_i_inf - kisi) / kis_i_tau +} + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + : "kis" fast inactivation potassium channel - activation and inactivation + kis_a_inf = kis_m(v) + kis_i_inf = kis_h(v) + kis_a_tau = kis_mt(v) + kis_i_tau = kis_ht(v) +} + +: Make these as functions so we can view them from hoc, although this +: may slow things down a bit + +FUNCTION kis_m(v (mV)) { : ikis activation + kis_m = 1/(1+exp(-(v-kis_avh)/23.7 (mV))) +} + +FUNCTION kis_h(v (mV)) { : ikis inactivation + kis_h = 1/(1+exp((v-kis_ivh)/9 (mV))) +} + +FUNCTION kis_mt(v (mV)) (ms) { : ikis activation tau + LOCAL x + x = 0.15*exp((v-kis_avh)/10 (mV)) + 0.3*exp(-(v-kis_avh)/10 (mV)) + x = 0.5 + (1.0 /x) + kis_mt = (x * 1.0 (ms))/q10 +} + +FUNCTION kis_ht(v (mV)) (ms) { : ikis inactivation tau + kis_ht = 200 (ms) +} + diff --git a/cnmodel/mechanisms/klt.mod b/cnmodel/mechanisms/klt.mod new file mode 100644 index 0000000..082ae89 --- /dev/null +++ b/cnmodel/mechanisms/klt.mod @@ -0,0 +1,109 @@ +TITLE klt.mod The low threshold conductance of cochlear nucleus neurons + +COMMENT + +NEURON implementation of Jason Rothman's measurements of VCN conductances. + +This file implements the low threshold potassium current found in several brainstem + nuclei of the auditory system, including the spherical and globular bushy cells + (Manis and Marx, 1991; Rothman and Manis, 2003a,b) and octopus cells (Bal and + Oertel, 2000) of the ventral cochlear nucleus, principal cells of the medial + nucleus of the trapzoid body (Brew and Forsythe, 1995, Wang and Kaczmarek, + 1997) and neurons of the medial superior olive. The current is likely mediated by + heteromultimers of Kv1.1 and Kv1.2 potassium channel subunits. The specific + implementation is described in Rothman and Manis, J. Neurophysiol. 2003, in the + appendix. Measurements were made from isolated neurons from adult guinea pig, + under reasonably stringent voltage clamp conditions. The measured current is + sensitive to the mamba snake toxin dendrotoxin-I. + + +Similar conductrances are found in the homologous neurons of the avian auditory +system (Reyes and Rubel; Zhang and Trussell; Rathouz and Trussell), and the +conductance described here, in the absence of more detailed kinetic measurements +, is probably suitable for use in modeling that system. + + +Original implementation by Paul B. Manis, April (JHU) and Sept, (UNC)1999. + +File split implementation, February 28, 2004. + +Contact: pmanis@med.unc.edu + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX klt + USEION k READ ek WRITE ik + RANGE gbar, gklt, ik, q10g + GLOBAL winf, zinf, wtau, ztau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + dt (ms) + gbar = 0.01592 (mho/cm2) <0,1e9> + zss = 0.5 <0,1> : steady state inactivation of glt + q10tau = 3.0 + q10g = 2.0 +} + +STATE { + w z +} + +ASSIGNED { + celsius (degC) : model is defined on measurements made at room temp in Baltimore + ik (mA/cm2) + ek (mV) + gklt (mho/cm2) + winf zinf + wtau (ms) ztau (ms) + qg () : computed q10 for gnabar based on q10g + q10 () +} + +LOCAL wexp, zexp + +BREAKPOINT { + SOLVE states METHOD cnexp + + gklt = qg*gbar*(w^4)*z + ik = gklt*(v - ek) +} + +INITIAL { + qg = q10g^((celsius-22)/10 (degC)) + q10 = q10tau^((celsius - 22)/10 (degC)) : if you don't like room temp, it can be changed! + rates(v) + w = winf + z = zinf +} + +DERIVATIVE states { :Computes state variables m, h, and n + rates(v) : at the current v and dt. + w' = (winf - w)/wtau + z' = (zinf - z)/ztau +} + + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + winf = (1 / (1 + exp(-(v + 48) / 6 (mV))))^0.25 + zinf = zss + ((1-zss) / (1 + exp((v + 71) / 10 (mV)))) + + wtau = (100 (ms)/ (6*exp((v+60) / 6 (mV)) + 16*exp(-(v+60) / 45 (mV)))) + 1.5 + wtau = wtau/q10 + ztau = (1000 (ms)/ (exp((v+60) / 20 (mV)) + exp(-(v+60) / 8 (mV)))) + 50 + ztau = ztau/q10 +} + diff --git a/cnmodel/mechanisms/kpkj.mod b/cnmodel/mechanisms/kpkj.mod new file mode 100644 index 0000000..e0d035b --- /dev/null +++ b/cnmodel/mechanisms/kpkj.mod @@ -0,0 +1,94 @@ +: HH TEA-sensitive Purkinje potassium current +: Created 8/5/02 - nwg + +NEURON { + THREADSAFE + SUFFIX kpkj + USEION k READ ek WRITE ik + RANGE gbar, ik, gk + GLOBAL minf, hinf, mtau, htau +} + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) +} + +PARAMETER { + v (mV) + + gbar = .004 (mho/cm2) + + mivh = -24 (mV) + mik = 15.4 (mV) + mty0 = .00012851 (s) + mtvh1 = 100.7 (mV) + mtk1 = 12.9 (mV) + mtvh2 = -56.0 (mV) + mtk2 = -23.1 (mV) + + hiy0 = .31 + hiA = .78 + hivh = -5.802 (mV) + hik = 11.2 (mV) + + ek (mV) +} + +ASSIGNED { + gk (mho/cm2) + ik (mA/cm2) + minf + mtau (ms) + hinf + htau (ms) +} + +STATE { + m + h +} + +INITIAL { + rates(v) + m = minf + h = hinf +} + +BREAKPOINT { + SOLVE states METHOD cnexp + gk = gbar * m^3 * h + ik = gk * (v - ek) +} + +DERIVATIVE states { + rates(v) + m' = (minf - m) / mtau + h' = (hinf - h) / htau +} + +PROCEDURE rates( Vm (mV)) { + LOCAL v + v = Vm + 11 (mV) : Account for Junction Potential + minf = 1/(1+exp(-(v-mivh)/mik)) + mtau = mtau_func(v) + hinf = hiy0 + hiA/(1+exp((v-hivh)/hik)) + htau = 1000 * htau_func(v) +} + +FUNCTION mtau_func (v (mV)) (ms) { + if (v < -35 (mV)) { + mtau_func = (1000)*(3.4225e-5+.00498*exp(-v/-28.29 (mV)))*3 (s) + } else { + mtau_func = (1000)*(mty0 + 1(s)/(exp((v+mtvh1)/mtk1)+exp((v+mtvh2)/mtk2))) + } +} + +FUNCTION htau_func(Vm (mV)) (ms) { + if ( Vm > 0) { + htau_func = (1000)*(0.0012 (s) + 0.0023(s)*exp(-0.141 *Vm / 1 (mV))) + } else { + htau_func = (1000)*(1.2202e-05(s) + .012(s) * exp(-((Vm-(-56.3 (mV)))/49.6 (mV))^2)) + } +} + \ No newline at end of file diff --git a/cnmodel/mechanisms/kpkj2.mod b/cnmodel/mechanisms/kpkj2.mod new file mode 100644 index 0000000..da6bacf --- /dev/null +++ b/cnmodel/mechanisms/kpkj2.mod @@ -0,0 +1,67 @@ +: HH Low TEA-sensitive Purkinje potassium current +: Created 8/7/02 - nwg + +NEURON { + THREADSAFE + SUFFIX kpkj2 + USEION k READ ek WRITE ik + RANGE gbar, ik, gk + GLOBAL ninf, ntau +} + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) +} + +PARAMETER { + v (mV) + gbar = .002 (mho/cm2) + + nivh = -24 (mV) + nik = 20.4 (mV) + + ek (mV) +} + +ASSIGNED { + gk (mho/cm2) + ik (mA/cm2) + ninf (1) + ntau (ms) +} + +STATE { + n +} + +INITIAL { + rates(v) + n = ninf +} + +BREAKPOINT { + SOLVE states METHOD cnexp + gk = gbar * n^4 + ik = gk * (v - ek) +} + +DERIVATIVE states { + rates(v) + n' = (ninf - n) / ntau +} + +PROCEDURE rates(Vm (mV)) { + LOCAL v + v = Vm + 11 : Account for Junction Potential + ninf = 1/(1+exp(-(v-nivh)/nik)) + ntau = 1000 * ntau_func(v) +} + +FUNCTION ntau_func(v (mV)) (ms) { + if (v < -20) { + ntau_func = 0.000688 (ms) + 1 (ms)/(exp((v+64.2 (mV))/6.5 (mV))+exp((v-141.5 (mV))/-34.8 (mV))) + } else { + ntau_func = 0.00016 (ms) + 0.0008 (ms) *exp(-0.0267 * v /(1 (mV))) + } +} diff --git a/cnmodel/mechanisms/kpkjslow.mod b/cnmodel/mechanisms/kpkjslow.mod new file mode 100644 index 0000000..42ca378 --- /dev/null +++ b/cnmodel/mechanisms/kpkjslow.mod @@ -0,0 +1,63 @@ +: HH Slow TEA-insensitive Purkinje potassium current +: Created 8/7/02 - nwg + +NEURON { + THREADSAFE + SUFFIX kpkjslow + USEION k READ ek WRITE ik + RANGE gbar, ik, gk + GLOBAL ninf, ntau +} + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) +} + +PARAMETER { + v (mV) + gbar = 0.004 (mho/cm2) + + nivh = -16.5 (mV) + nik = 18.4 (mV) + + ek (mV) +} + +ASSIGNED { + gk (mho/cm2) + ik (mA/cm2) + ninf + ntau (ms) +} + +STATE { + n +} + +INITIAL { + rates(v) + n = ninf +} + +BREAKPOINT { + SOLVE states METHOD cnexp + gk = gbar * n^4 + ik = gk * (v - ek) +} + +DERIVATIVE states { + rates(v) + n' = (ninf - n) / ntau +} + +PROCEDURE rates(Vm (mV)) { + LOCAL v + v = Vm + 11 : Account for Junction Potential + ninf = 1/(1+exp(-(v-nivh)/nik)) + ntau = 1000 * ntau_func(v) +} + +FUNCTION ntau_func(v (mV)) (ms){ + ntau_func = 0.000796 (ms) + 1 (ms)/(exp((v+73.2 (mV))/11.7 (mV))+exp((v-306.7 (mV))/-74.2(mV))) +} \ No newline at end of file diff --git a/cnmodel/mechanisms/kpksk.mod b/cnmodel/mechanisms/kpksk.mod new file mode 100644 index 0000000..dac0ea6 --- /dev/null +++ b/cnmodel/mechanisms/kpksk.mod @@ -0,0 +1,111 @@ +TITLE Slow Ca-dependent potassium current +: +: Ca++ dependent K+ current IC responsible for slow AHP +: Differential equations +: +: Model based on a first order kinetic scheme +: +: + n cai <-> (alpha,beta) +: +: Following this model, the activation fct will be half-activated at +: a concentration of Cai = (beta/alpha)^(1/n) = cac (parameter) +: +: The mod file is here written for the case n=2 (2 binding sites) +: --------------------------------------------- +: +: This current models the "slow" IK[Ca] (IAHP): +: - potassium current +: - activated by intracellular calcium +: - NOT voltage dependent +: +: A minimal value for the time constant has been added +: +: Ref: Destexhe et al., J. Neurophysiology 72: 803-818, 1994. +: +: Modifications by Arthur Houweling for use in MyFirstNEURON + + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + THREADSAFE + SUFFIX kpksk + USEION k READ ek WRITE ik + USEION ca READ cai + RANGE m_inf, tau_m, gbar, gk + GLOBAL beta, cac + RANGE ik +} + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (molar) = (1/liter) + (mM) = (millimolar) +} + + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ek (mV) + cai (mM) + gbar = .01 (mho/cm2) + beta = 0.002 (1/ms) : backward rate constant + cac = 0.010 (mM) : middle point of activation fct + taumin = 0.1 (ms) : minimal value of the time cst +} + + +STATE { + m +} + +ASSIGNED { + ik (mA/cm2) + gk (mho/cm2) + m_inf + tau_m (ms) + tadj () +} + +BREAKPOINT { + SOLVE states :METHOD euler + gk = gbar * m*m + ik = gk * (v - ek) +} + +:DERIVATIVE states { +: evaluate_fct(v,cai) +: +: m'= (m_inf-m) / tau_m +:} + +PROCEDURE states() { + evaluate_fct(v,cai) + + m= m + (1-exp(-dt/tau_m))*(m_inf-m) +} + +INITIAL { +: +: activation kinetics are assumed to be at 22 deg. C +: Q10 is assumed to be 3 +: + tadj = 3 ^ ((celsius-22.0)/10 (degC)) + + evaluate_fct(v,cai) + m = m_inf +} + +PROCEDURE evaluate_fct(v(mV),cai(mM)) { LOCAL car + + car = (cai/cac)^2 + + m_inf = car / ( 1 + car ) + tau_m = 1 / beta / (1 + car) / tadj + + if(tau_m < taumin) { tau_m = taumin } : min value of time cst +} diff --git a/cnmodel/mechanisms/leak.mod b/cnmodel/mechanisms/leak.mod new file mode 100644 index 0000000..867c7c8 --- /dev/null +++ b/cnmodel/mechanisms/leak.mod @@ -0,0 +1,32 @@ +TITLE passive (leak) membrane channel + +UNITS { + (mV) = (millivolt) + (mA) = (milliamp) +} + +NEURON { + THREADSAFE + SUFFIX leak + NONSPECIFIC_CURRENT i + RANGE gbar, erev, i +} + +PARAMETER { + v (mV) + gbar = 0.001 (mho/cm2) + erev = -65 (mV) +} + +ASSIGNED { + i (mA/cm2) +} + +INITIAL { + +} +BREAKPOINT { + i = gbar*(v - erev) +} + + diff --git a/cnmodel/mechanisms/multisite.mod b/cnmodel/mechanisms/multisite.mod new file mode 100644 index 0000000..022500a --- /dev/null +++ b/cnmodel/mechanisms/multisite.mod @@ -0,0 +1,379 @@ +TITLE Multisite synapse + +COMMENT +----------------------------------------------------------------------------- +Multi-site synapse with independent release sites. Each site operates independently +and releases a vesicle upon presynaptic depolarization with a probability +determined by the history of activity, using the Dittman and Regehr (1998, 2000) +model. + +Revised from coh2.mod, coh3.mod, and coh4.mod. +The Dittman and Regeher (1998, 2000) release model with +facilitation closely fits Auditory Nerve data from mouse over +a wide range of frequencies. +The model DOES NOT include the postsynaptic receptors or desensitization, since +these should be treated separately (couple XMTR to an AMPA receptor model, +such as the Trussell-Raman model) + +Range variables: +nZones: is the number of active zones simulated in this calyx model. Each zone + can be connected to a separate PSD. +F (0.4): The base release probability +k0 (1/1.75): /s, baseline recovery rate from depletion (slow rate) +kmax (1/0.025): /s, maximal recovery rate from depletion (fast rate) +td (0.05) : time constant for fast calcium-dependent recovery, sec +kd (0.7) : affinity of fast recovery process for calcium sensor +kf (0.5) : affinity of facilitation process +tf (0.01) : rate of facilitation process (slow) seconds +dD (0.02): calcium that drives recovery (ca influx per AP) +dF (0.02): calcium that drives facilitation + +Added latency and variable delay (latstd, latency standard deviation in msec) +around the mean spike time. 4/5/2011 pbm. + +Version 4 uses a log-normal distribution to determine release latencies. +The calculation is built-in instead of being passed through an array. +The lognormal distribution describes the individual vesicle release time +course at this synapse as measured by Isaacson and Walmsley, 1996. Note that +they used a gamma distribution in some plots, but the lognormal distribution +seems to fit their published data at least as well. +The parameters of the distribution, as well as the release latency, +are controlled by an exponential function whose parameters are initialized at +run time. +10/19/2011 Paul B. Manis, UNC Chapel Hill + +ENDCOMMENT + +DEFINE MAX_ZONES 1000 : maximum number of zones in this model +DEFINE EVENT_N 10000 : number of entries in the Event Distribution (e.g., as sampled) + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +NEURON { + THREADSAFE + POINT_PROCESS MultiSiteSynapse + RANGE F, k0, kmax, taud, kd, tauf, kf + RANGE nZones, multisite, rseed, latency, latstd, debug + RANGE dD, dF, XMTR, glu, CaDi, CaFi + RANGE Fn, Dn + RANGE TTotal + RANGE nRequests, nReleases + RANGE Identifier : just a number so we can report which instance is active + RANGE tau_g, amp_g + : Distributions for stochastic release and testing (Sept, Oct, 2011): + RANGE EventLatencies, EventTime : returns the first EVENT_N latencies and absolute times at which they were used + RANGE ev_index : count in the EventLatencies (in case we are "short") + : parameters for latency shift during repetitive stimulation (Oct 19, 2011) + RANGE Dep_Flag : Depression flag (0 to remove depression; 1 to allow DKR control of facilitation and depression) + RANGE Lat_Flag, Lat_t0, Lat_A0, Lat_tau : Lat_Flag = 0 means fixed latency (set by "latency" above) + : otherwise, latency = latency for t < Lat_t0 + : latency = latency + Lat_A0*(1-exp(-(t-Lat_t0)/Lat_tau)) + : parameters for lognorm distribution shift during repetitive stimulation (Oct 19, 2011) + RANGE LN_Flag, LN_t0, LN_A0, LN_tau : LN_Flag = 0 means fixed sigma as well + : otherwise, sigma = latstd for t < LN_t0 + : sigma = latstd + LN_A0*(1-exp(-(t-LN_t0)/LN_tau)) + + : externally assigned pointers to RNG functions + POINTER uniform_rng : for deciding the number of active synapses when multisite==0 +} + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (mM) = (milli/liter) + (uM) = (micro/liter) +} + +PARAMETER { + dt (ms) + amp_g = 1.0 (mM) : amplitude of transmitter pulse + tau_g = 0.5 (ms) : duration of transmitter pulse + dD = 0.02 (1) : calcium influx driving recovery per AP + dF = 0.02 (1) : calcium influx driving facilitation per AP + F = 0.5 (1) : basal facilitation + k0 = 0.0005714(/ms) : slow recovery from depletion (1.0/1.75) + kmax = 0.040 (/ms) : fast recovery from depletion (1/0.025) + taud = 50.0 (ms) : time constant for fast calcium dependent recovery + kd = 0.7 (1) : affinity of fast recovery process for calcium sensor + tauf = 10.0 (ms) : rate of slow facilitation process + kf = 0.5 (1) : affinity of slow facilitation process + : taus = 1 (ms) : defined by DKR but not used here + : ks = 0.5 (1) + : glu = 1 (mM) + rseed (1) : random number generator seed (for SCOP module) + latency = 0.0 (ms) + latstd = 0.0 (ms) + + : Time course of latency shift in release during repetitive stimulation + Lat_Flag = 0 (1) : 0 means fixed latency, 1 means lognormal distribution + Lat_t0 = 0.0 (ms) : minimum time since simulation start before changes in latency are calculated + Lat_A0 = 0.0 (ms) : size of latency shift from t0 to infinity + Lat_tau = 100.0 (ms) : rate of change of latency shift (from fit of a+b(1-exp(-t/tau))) + : Statistical control of log-normal release shape over time during repetitive stimulation + LN_Flag = 0 (1) : 0 means fixed values for all time + LN_t0 = 0.0 (ms) : : minimum time since simulation start before changes in distribution are calculated + LN_A0 = 0.0 (ms) : size of change in sigma from t0 to infinity + LN_tau = 100.0 (ms) : rate of change of sigma over time (from fit of a+b*(1-exp(-t/tau))) + + : control flags - if debug is 1, show all, if 2, just "some" + debug = 0 + Identifier = 0 + Dep_Flag = 1 (1) : 1 means use depression calculations; 0 means always set release probability to F +} + +ASSIGNED { + : Externally set assignments + nZones (1) : number of zones in the model + multisite (1) : whether zones are modeled individually (1) or as a single, variable-amplitude zone (0) + nRequests (1) + nReleases (1) + EventLatencies[EVENT_N] (0) + EventTime[EVENT_N] (0) + tRelease[MAX_ZONES] (ms) : time of last release + + : Internal calculated variables + Fn (1) + Dn (1) + CaDn (1) + CaFn (1) + CaDi (1) + CaFi (1) + eta (1) + tSpike (ms) : time of last spike + tstep(ms) + TTotal(0) + tspike (ms) + latzone (ms) + vesicleLatency (ms) + sigma (ms) + gindex (0) + ev_index (0) + scrand (0) + + uniform_rng +} + +: Function prototypes needed to assign RNG function pointers +VERBATIM +double nrn_random_pick(void* r); +void* nrn_random_arg(int argpos); +ENDVERBATIM + +: Return a pick from uniform distribution. +: (distribution parameters are set externally) +FUNCTION rand_uniform() { +VERBATIM + _lrand_uniform = nrn_random_pick(_p_uniform_rng); +ENDVERBATIM +} + +: Function to allow RNG to be externally set +PROCEDURE setUniformRNG() { +VERBATIM + { + void** pv = (void**)(&_p_uniform_rng); + *pv = nrn_random_arg(1); + } +ENDVERBATIM +} + + +STATE { + XMTR[MAX_ZONES] (mM) : per-zone neurotransmitter concentration + N_ACTIVE[MAX_ZONES] (1) : number of zones actively releasing +} + +INITIAL { +: VERBATIM +: fprintf(stdout, "MultiSiteSynapse: Calyx #%d Initialized with Random Seed: %d\n", (int)Identifier, (int)rseed); +: ENDVERBATIM + + TTotal = 0 + nRequests = 0 + nReleases = 0 + set_seed(rseed) + tSpike = -1e9 + latzone = 0.0 + sigma = 0.0 + vesicleLatency = 0.0 + gindex = 0 + ev_index = 0 + scrand = 0.0 + CaDi = 1.0 + CaFi = 0.0 + CaDn = 1.0 + CaFn = 0.0 + Fn = F + Dn = 1.0 + FROM i = 0 TO (nZones-1) { + XMTR[i] = 0 + N_ACTIVE[i] = 1 + tRelease[i] = tSpike + } + update_dkr(t-tSpike) +} + +BREAKPOINT { + SOLVE release +} + +LOCAL tz, n_relzones, amp +PROCEDURE release() { + : Once released, the transmitter packet has a defined smooth time course in the "cleft" + : represented by the product of rising and falling exponentials. + : update glutamate in cleft + if (multisite == 1) { + : Update glutamate waveform for each active release zone + n_relzones = nZones + } + else { + : Update aggregate glutamate waveform for only the first release zone + n_relzones = 1 + } + + FROM i = 0 TO (nZones-1) { : for each zone in the synapse + if (t >= tRelease[i] && t < tRelease[i] + 5.0 * tau_g) { + tz = t - tRelease[i] : time since onset of release + : calculate glutamate waveform (Xie & Manis 2013 Supplementary Eq. 1) + XMTR[i] = amp_g * (1.0-exp(-tz/(tau_g/3.0))) * exp(-(tz-(tau_g/3.0))/tau_g) + } + else { + XMTR[i] = 0 + } + } +} + + +PROCEDURE update_dkr(tstep (ms)) { + : update the facilitation and depletion variables + : from the Dittman-Regehr model. + : Updates are done with each new presynaptic AP event. + if(tstep > 0.0) { + CaDi = CaDi + dD + CaFi = CaFi + dF + CaDn = CaDi * exp (-tstep/taud) + CaFn = CaFi * exp (-tstep/tauf) + eta = (kd/CaDi + 1.0)/(kd/CaDi + exp(-tstep/taud)) + eta = eta^(-(kmax-k0)*taud) + Dn = 1.0-(1.0-(1.0-Fn)*Dn)*exp(-k0*tstep)*eta + Fn = F + (1.0-F)/(1.0+kf/CaFn) + CaDi = CaDn + CaFi = CaFn + } + if (Dep_Flag == 0) { : no depression + Dn = 1.0 : set to 1 + Fn = F : set to initial value to set release probability constant + } +: VERBATIM +: if (debug >= 2 ){ +: fprintf(stdout, "update start t = %f ts=%f: F=%7.2f CaDi = %g CaFi = %g\n", \ +: t, tstep, F, CaDi, CaFi); +: fprintf(stdout, " vars: taud=%g: tauf=%g kd = %g kmax= %g\n", taud, tauf, kd, kmax); +: fprintf(stdout, " CaDi = %g CaFi = %g\n", CaDi, CaFi); +: fprintf(stdout, " CaDn = %g CaFn = %g\n", CaDn, CaFn); +: fprintf(stdout, " eta: %g\n", eta); +: fprintf(stdout, " Fn=%7.2f Dn: %7.2f CaDi = %g CaFi = %g,\n", \ +: Fn, Dn, CaDi, CaFi); +: } +: ENDVERBATIM +} + + +NET_RECEIVE(weight) { + : A spike has been received; process synaptic release + + : First, update DKR state to determine new release probability + update_dkr(t - tSpike) + tSpike = t : save the time of spike + + TTotal = 0 : reset total transmitter from this calyx for each release + nRequests = nRequests + 1 : count the number of inputs that we received + + : Next, process vesicle release using new release probability + if (multisite == 1) { + release_multisite() + } + else { + release_singlesite() + } +} + + +PROCEDURE release_multisite() { + : Vesicle release procedure for multi-site terminal. + : Loops over multiple zones using release probability Fn*Dn to decide whether + : each site will release, and selecting an appropriate release latency. + + : The synapse can release one vesicle per AP per zone, with a probability 0 0.0) { + latzone = normrand(0.0, latstd) : set a latency for the zone with one draw from the distribution + latzone = exp(latzone) - 1.0 + vesicleLatency : the latency should not be too short.... + } + else { + latzone = vesicleLatency : fixed value + } + } + else { + sigma = latstd + LN_A0*(1-exp(-(t-LN_t0)/LN_tau)) : time-dependent std shift + latzone = normrand(0.0, sigma) + latzone = exp(latzone)-1.0 + vesicleLatency + } + if (latzone < 0.0) { : this is to be safe... must have causality. + latzone = 0.0 + } + if (ev_index < EVENT_N) { : save event distribution list for verification + EventLatencies[ev_index] = latzone + EventTime[ev_index] = t + ev_index = ev_index + 1 + } + + : release time for this event + tRelease[i] = t + latzone + } + } + } +} + + +PROCEDURE release_singlesite() { + LOCAL pr + tRelease[0] = t + pr = Fn * Dn + FROM i = 0 TO (nZones-1) { + if (rand_uniform() < pr) { + TTotal = TTotal + 1 : count total releases this trial. + } + } + : Tell PSD to multiply its final current by the number of active zones + N_ACTIVE[0] = TTotal + printf("Release: %f\n", TTotal) +} diff --git a/cnmodel/mechanisms/na.mod b/cnmodel/mechanisms/na.mod new file mode 100644 index 0000000..e9ba278 --- /dev/null +++ b/cnmodel/mechanisms/na.mod @@ -0,0 +1,100 @@ +TITLE na.mod A sodium channel for cochlear nucleus neurons + +COMMENT + +NEURON implementation of Jason Rothman's measurements of VCN conductances. + +This file implements the average brain sodium current used in the Rothman model. +In the absence of direct measurements in the VCN, this is a fair assumption. +The model differs from the one used in Rothman et al, (1993) in that the steep +voltage dependence of recovery from inactivation in that model is missing. This +may affect the refractory period. To use the other model, use najsr.mod instead. + +Original implementation by Paul B. Manis, April (JHU) and Sept, (UNC)1999. + +File split implementaiton, April 1, 2004. + +Contact: pmanis@med.unc.edu + +Modifed implementation; includes all temperature scaling, passes modlunit +7/10/2014 pbm + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX na + USEION na READ ena WRITE ina + RANGE gbar, gna, ina + GLOBAL hinf, minf, htau, mtau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + dt (ms) + ena (mV) + gbar = 0.07958 (mho/cm2) <0,1e9> + q10tau = 3.0 + q10g = 2.0 + +} + +STATE { + m h +} + +ASSIGNED { + celsius (degC) : model is defined on measurements made at room temp in Baltimore + ina (mA/cm2) + gna (mho/cm2) + minf hinf + mtau (ms) htau (ms) + qg () : computed q10 for gnabar based on q10g + q10 () + +} + +LOCAL mexp, hexp + +BREAKPOINT { + SOLVE states METHOD cnexp + + gna = qg*gbar*(m^3)*h + ina = gna*(v - ena) +} + +INITIAL { + qg = q10g^((celsius-22)/10 (degC)) + q10 = q10tau^((celsius - 22)/10 (degC)) : if you don't like room temp, it can be changed! + rates(v) + m = minf + h = hinf +} + +DERIVATIVE states { :Computes state variables m, h, and n + rates(v) : at the current v and dt. + m' = (minf - m)/mtau + h' = (hinf - h)/htau +} + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + +: average sodium channel + minf = 1 / (1+exp(-(v + 38) / 7 (mV))) + hinf = 1 / (1+exp((v + 65) / 6 (mV))) + + mtau = (10 (ms)/ (5*exp((v+60) / 18 (mV)) + 36*exp(-(v+60) / 25 (mV)))) + 0.04 + mtau = mtau/q10 + htau = (100 (ms)/ (7*exp((v+60) / 11 (mV)) + 10*exp(-(v+60) / 25 (mV)))) + 0.6 + htau = htau/q10 +} + diff --git a/cnmodel/mechanisms/nacn.mod b/cnmodel/mechanisms/nacn.mod new file mode 100755 index 0000000..c979504 --- /dev/null +++ b/cnmodel/mechanisms/nacn.mod @@ -0,0 +1,103 @@ +TITLE nacn.mod A sodium conductance for a ventral cochlear nucleus neuron model + +COMMENT + +NEURON implementation of Jason Rothman's measurements of VCN conductances. + +This file implements the average brain sodium current used in the Rothman model. +In the absence of direct measurements in the VCN, this is a fair assumption. +The model differs from the one used in Rothman et al, (1993) in that the steep +voltage dependence of recovery from inactivation in that model is missing. This +may affect the refractory period. To use the other model, use najsr.mod instead. + +Original implementation by Paul B. Manis, April (JHU) and Sept, (UNC)1999. + +File split implementaiton, April 1, 2004. + + Does not pass modlunit. +Should work at 22C and scales by Rothman and Manis, 2003c for temperature + +Contact: pmanis@med.unc.edu + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX nacn + USEION na READ ena WRITE ina + RANGE gbar, gna, ina + GLOBAL hinf, minf, htau, mtau +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) : 22 (degC) model is defined on measurements made at room temp in Baltimore + dt (ms) + ena (mV) + gbar = 0.07958 (mho/cm2) <0,1e9> + q10tau = 3.0 : q10 for rates +} + +STATE { + m h +} + +ASSIGNED { + ina (mA/cm2) + gna (mho/cm2) + minf hinf + mtau (ms) htau (ms) + q10 () + + } + +LOCAL mexp, hexp + +BREAKPOINT { + SOLVE states METHOD cnexp + gna = gbar*(m^3)*h + ina = gna*(v - ena) +} + +UNITSOFF + +INITIAL { + rates(v) + m = minf + h = hinf +} + +DERIVATIVE states { :Computes state variables m, h, and n + rates(v) : at the current v and dt. + m' = (minf - m)/mtau : m = m + mexp*(minf-m) + h' = (hinf - h)/htau : h = h + hexp*(hinf-h) + +} + +LOCAL qt + +PROCEDURE rates(v) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + q10 = q10tau^((celsius - 22)/10) : if you don't like room temp, it can be changed! + +: average sodium channel + minf = 1 / (1+exp(-(v + 38) / 7)) + hinf = 1 / (1+exp((v + 65) / 6)) + + mtau = (10 / (5*exp((v+60) / 18) + 36*exp(-(v+60) / 25))) + 0.04 + mtau = mtau/q10 + htau = (100 / (7*exp((v+60) / 11) + 10*exp(-(v+60) / 25))) + 0.6 + htau = htau/q10 +} + + +UNITSON diff --git a/cnmodel/mechanisms/nacncoop.mod b/cnmodel/mechanisms/nacncoop.mod new file mode 100644 index 0000000..da36e7a --- /dev/null +++ b/cnmodel/mechanisms/nacncoop.mod @@ -0,0 +1,138 @@ +TITLE nacn.mod A sodium conductance for a ventral cochlear nucleus neuron model + +COMMENT + +NEURON implementation of Jason Rothman's measurements of VCN conductances. + +This file implements a modified version of the average brain sodium current + used in the Rothman and Manis 2003 models. + +The model differs from the one used in Rothman et al, (1993) in that the steep +voltage dependence of recovery from inactivation in that model is missing. This +may affect the refractory period. To use the other model, use jsrnaf.mod instead. + +Original implementation by Paul B. Manis, April 1999 (JHU) and Sept 1999 (UNC-CH). + +File split implementation, April 1, 2004. + + +Version nacncoop implements a cooperative sodium channel model built on the kinetics +of the original nacn model (R&M2003c). The motivation is to make a sodium channel with +faster activation kinetics, by introducing cooperativity between a subset of channels. +The model is based on concepts and implementation similar to Oz et al. +J.Comp. Neurosci. 39: 63, 2015, and Huang et al., PloSOne 7:e37729, 2012. +The cooperative channels are modeled with the same kinetics as the non-cooperative +channels, but are treated as a separate subset (fraction: p). The cooperativity is +introduced by shifting the voltage "seen" by the channels by KJ*m^3*h, which moves +the channels to a faster regime (essentially, they experience a depolarized membrane +potential that depends on their current gating state, relative to the main population +of channels). + +A subpopulation of Na channels (p [0..1]) experiences a small voltage-dependent shift +in the gating kinetics. The shift is determined by KJ + +This version does not have all the temperature scaling. Does not pass modlunit. +Should work at 22C, appears to work at other temperatures ok. + +Contact: pmanis@med.unc.edu + +ENDCOMMENT + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) + (nA) = (nanoamp) +} + +NEURON { + THREADSAFE + SUFFIX nacncoop + USEION na READ ena WRITE ina + RANGE gbar, gna, ina, p, KJ + RANGE vsna : voltage shift parameter + GLOBAL hinf, minf, htau, mtau, hinf2, minf2, htau2, mtau2 +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) : 22 (degC) model is defined at room temp in Baltimore + dt (ms) + ena (mV) + gbar = 0.07958 (mho/cm2) <0,1e9> + q10 = 3.0 : q10 for rates + p = 0.0 (): fraction of cooperative channels (0-1) + KJ = 0 (mV) : coupling strength between cooperative channels (0-1000mV is usable range) + : setting either KJ = 0 or p = 0 will remove cooperativity. + vsna = 0 (mV) +} + +STATE { + m h m2 h2 +} + +ASSIGNED { + ina (mA/cm2) + gna (mho/cm2) + vNa (mV) : shifted V for cooperative behavior + minf hinf minf2 hinf2 + mtau (ms) htau (ms) mtau2 (ms) htau2 (ms) + + } + +LOCAL mexp, hexp, mexp2, hexp2 + +BREAKPOINT { + SOLVE states METHOD cnexp + + gna = gbar*(p*(m2^3*h2) + (1.-p)*(m^3)*h) + ina = gna*(v - ena) +} + +UNITSOFF + +INITIAL { + rates(v) + m = minf + h = hinf + m2 = minf2 + h2 = hinf2 + vNa = v + vsna + KJ*m^3*h +} + +DERIVATIVE states { :Computes state variables m, h, and n + rates(v) : at the current v and dt. + m' = (minf - m)/mtau + h' = (hinf - h)/htau + m2' = (minf2 - m2)/mtau2 + h2' = (hinf2 - h2)/htau2 + vNa = v + vsna + KJ*m^3*h : note addition of vsna shift here so that we do not add it in rates +} + +LOCAL qt + +PROCEDURE rates(v) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + + qt = q10^((celsius - 22)/10) : if you don't like room temp, it can be changed! + +: average sodium channel + minf = 1 / (1+exp(-(v + 38 + vsna) / 7)) + hinf = 1 / (1+exp((v + 65 + vsna) / 6)) + mtau = (10 / (5*exp((v + 60 + vsna) / 18) + 36*exp(-(v + 60+vsna) / 25))) + 0.04 + mtau = mtau/qt + htau = (100 / (7*exp((v + 60 + vsna) / 11) + 10*exp(-(v + 60 + vsna) / 25))) + 0.6 + htau = htau/qt + +: cooperative group of channels + minf2 = 1 / (1+exp(-(vNa + 38) / 7)) + hinf2 = 1 / (1+exp((vNa + 65) / 6)) + mtau2 = (10 / (5*exp((vNa+60) / 18) + 36*exp(-(vNa+60) / 25))) + 0.04 + mtau2 = mtau2/qt + htau2 = (100 / (7*exp((vNa+60) / 11) + 10*exp(-(vNa+60) / 25))) + 0.6 + htau2 = htau2/qt + +} + +UNITSON diff --git a/cnmodel/mechanisms/nap.mod b/cnmodel/mechanisms/nap.mod new file mode 100755 index 0000000..cf6470d --- /dev/null +++ b/cnmodel/mechanisms/nap.mod @@ -0,0 +1,130 @@ +TITLE nap.mod Persistent sodium conductance + +COMMENT + + +Persistent sodium current from deSchutter and Bower, J. Neurophys. +71:375, 1994. + + +2/10/02, 10/10/2014. P. Manis. + +ENDCOMMENT + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) +} + + +NEURON { + THREADSAFE + SUFFIX nap + USEION na READ ena WRITE ina + + RANGE nap_inf, nap_tau, napi_inf, napi_tau + RANGE gbar, gnap + RANGE nap_A, nap_B, nap_C, nap_D, nap_E, nap_F, nap_G, nap_H + +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ena (mV) : = 50.0 (mV) + gbar = 0.00001 (mho/cm2) <0,1e9> + q10tau = 3.0 + q10g = 2.0 + + nap_shift = 0 (mV) + + nap_A = 200 (/ms): parameters from Bowers et al. + nap_B = 1 + nap_C = -18 (mV) + nap_D = -16 (mV) + nap_E = 25 (/ms) + nap_F = 1 + nap_G = 58 (mV) + nap_H = 8 (mV) + } + +STATE { + nap napi + } + +ASSIGNED { + gnap (mho/cm2) + ina (mA/cm2) + nap_inf + nap_tau (ms) + nap_tau1 (/ms) + nap_tau2 (/ms) + napi_inf + napi_tau (ms) + napi_tau1 (/ms) + napi_tau2 (/ms) + qg () : computed q10 for gnabar based on q10g + q10 () +} + +BREAKPOINT { + SOLVE states METHOD cnexp + gnap = gbar*nap*nap*nap*napi + ina = gnap*(v-ena) +} + + +INITIAL { + qg = q10g^((celsius-22)/10 (degC)) + q10 = q10tau^((celsius - 22)/10 (degC)) : if you don't like room temp, it can be changed! + rates(v) + nap = nap_inf + napi = napi_inf +} + + +DERIVATIVE states { + rates(v) + nap' = (nap_inf - nap) / nap_tau + napi' = (napi_inf - napi) / napi_tau +} + + +PROCEDURE rates(v (mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + LOCAL x + : "nap" persistent sodium system + nap_inf = na_p(v + nap_shift) + nap_tau1 = nap_A/(nap_B + exp((v + nap_shift + nap_C)/nap_D)) + nap_tau2 = nap_E/(nap_F + exp((v + nap_shift + nap_G)/nap_H)) + nap_tau = 1./(nap_tau1 + nap_tau2) + + :nap_tau = na_ptau(v + nap_shift) + + : "nap" persistent sodium system - inactivation... + napi_inf = na_pi(v + nap_shift) + napi_tau1 = (1 (/ms)) /(0.06435/(1+exp((v + nap_shift + 73.26415)/3.71928 (mV)))) + napi_tau2 = (0.13496 (/ms))/(1 +exp((v + nap_shift + 10.27853)/(-9.09334 (mV)))) + napi_tau = 1 /(napi_tau1 + napi_tau2) +} + +LOCAL p + +FUNCTION na_p(v (mV)) { : persistent sodium activation +: Bowers + p = nap_A/(nap_B + exp((v + nap_shift + nap_C)/nap_D)) + na_p = p/(p+nap_E/(nap_F + exp((v + nap_shift + nap_G)/nap_H))) +} + + +FUNCTION na_pi(x (mV)) { : persistent sodium inactivation +: Bowers + na_pi = 0.06435/(1+exp((x+73.26415)/3.71928 (mV))) + na_pi = na_pi/(na_pi + (0.13496/(1+exp((v+10.27853)/(-9.09334 (mV)))))) +} + + diff --git a/cnmodel/mechanisms/napyr.mod b/cnmodel/mechanisms/napyr.mod new file mode 100644 index 0000000..a6200a1 --- /dev/null +++ b/cnmodel/mechanisms/napyr.mod @@ -0,0 +1,137 @@ +TITLE pyrna.mod DCN pyramidal cell model sodium channel + +COMMENT + +Revised version of DCN Pyramidal cell model sodium channel + +This model implements part of a Dorsal Cochlear Nucleus Pyramidal point cell +based on kinetic data from Kanold and Manis (1999) and Kanold's dissertation (1999) + +-- 15 Jan 1999 P. Manis + +This mechanism is the fast sodium channel portion of the model. + + +Orignal: 2/10/02. P. Manis. + +Extraced from Pyr.mod, 7/24/2014. + +ENDCOMMENT + + +UNITS { + (mA) = (milliamp) + (mV) = (millivolt) +} + + +NEURON { + THREADSAFE + SUFFIX napyr + USEION na READ ena WRITE ina + RANGE gna, minf, hinf, ninf, gbar : sodium channels and delayed rectifier + RANGE mtau, htau, ntau : time constants for sodium channels and delayed rectifier +} + +INDEPENDENT {t FROM 0 TO 1 WITH 1 (ms)} + +PARAMETER { + v (mV) + celsius (degC) + dt (ms) + ek (mV) : = -81.5 (mV) + ena (mV) : = 50.0 (mV) + gbar = 0.02857 (mho/cm2) <0,1e9> + mtau0 = 0.05 (ms) <0.01,100> + htau0 = 0.5 (ms) <0.1,100> + ntau = 0.5 (ms) <0.1,100> + } + +STATE { + m h + } + +ASSIGNED { + gna (mho/cm2) + ina (mA/cm2) + minf hinf mtau htau +} + +LOCAL mexp, hexp + +BREAKPOINT { + SOLVE states METHOD cnexp + gna = gbar*m*m*h + ina = gna*(v - ena) +} + +UNITSOFF + +INITIAL { + rates(v) + m = minf + h = hinf +} + +DERIVATIVE states { + rates(v) + m' = (minf - m) / mtau + h' = (hinf - h) / htau +} + +LOCAL q10 + + +PROCEDURE rates(v(mV)) { :Computes rate and other constants at current v. + :Call once from HOC to initialize inf at resting v. + LOCAL alpha, beta, sum + TABLE minf, mtau, hinf, htau DEPEND celsius FROM -200 TO 100 WITH 400 + +UNITSOFF + q10 = 3^((celsius - 22)/10) + + : "m" sodium activation system + minf = na_m(v) + mtau = na_mt(v) + + : "h" sodium inactivation system + hinf = na_h(v) + htau = na_ht(v) + +} + +: Make these as functions so we can view them from hoc, although this +: may slow things down a bit + +FUNCTION na_m(x) { : sodium activation + na_m = 1/(1+exp(-(x+38)/3.0)) : POK version +: na_m = alphbet(x,35,0,5,-10) :de Schutter (doesn't work well in our version) +: na_m = na_m/(na_m + alphbet(x,7,0,65,20)) +} + +FUNCTION na_mt(x) { : sodium activation with taus + na_mt = mtau0 : flat time constants +: na_mt = alphbet(x,35,0,5,-10) +: na_mt = 1/(na_mt + alphbet(x,7,0,65,20)) +} + +FUNCTION na_h(x) { : sodium inactivation + na_h = 1/(1+exp((x+43)/3.0)) : flat time constants (POK version) +: na_h = alphbet(x,0.225,1,80,10) +: na_h = na_h/(na_h + alphbet(x,7.5,0,-3,-18)) +} + +FUNCTION na_ht(x) { : sodium inactivation tau + na_ht = htau0 : POK: flat time constants +: na_ht = alphbet(x,0.225,1,80,10) : de Schutter version (doesn't work well with other stuff) +: na_ht = 1/(na_ht + alphbet(x,7.5,0,-3,-18)) +} + + +FUNCTION alphbet(x,A,B,C,D) { : alpha/beta general functions for + : transcrbing GENESIS models +alphbet = A/(B+exp((x+C)/D)) +} + +UNITSON + diff --git a/cnmodel/mechanisms/pkjlk.mod b/cnmodel/mechanisms/pkjlk.mod new file mode 100644 index 0000000..ae47848 --- /dev/null +++ b/cnmodel/mechanisms/pkjlk.mod @@ -0,0 +1,17 @@ +TITLE Purkinje Leak Current + +: A passive purkinje cell leak current +NEURON { + SUFFIX lkpkj + NONSPECIFIC_CURRENT i + RANGE i, e, gbar +} +PARAMETER { + gbar = 5e-5 (siemens/cm2) < 0, 1e9 > + e = -60.5 (millivolt) +} +ASSIGNED { + i (milliamp/cm2) + v (millivolt) +} +BREAKPOINT { i = gbar*(v - e) } diff --git a/cnmodel/mechanisms/rsg.mod b/cnmodel/mechanisms/rsg.mod new file mode 100644 index 0000000..3a3291d --- /dev/null +++ b/cnmodel/mechanisms/rsg.mod @@ -0,0 +1,196 @@ +TITLE Rsg sodium channel +: Resurgent sodium channel (with blocking particle) +: with updated kinetic parameters from Raman and Bean + +NEURON { + SUFFIX naRsg + USEION na READ ena WRITE ina + RANGE gna, gbar +} + +UNITS { + (mV) = (millivolt) + (S) = (siemens) +} + +PARAMETER { + gbar = .015 (S/cm2) + + : kinetic parameters + Con = 0.005 (/ms) : closed -> inactivated transitions + Coff = 0.5 (/ms) : inactivated -> closed transitions + Oon = .75 (/ms) : open -> Ineg transition + Ooff = 0.005 (/ms) : Ineg -> open transition + alpha = 150 (/ms) : activation + beta = 3 (/ms) : deactivation + gamma = 150 (/ms) : opening + delta = 40 (/ms) : closing, greater than BEAN/KUO = 0.2 + epsilon = 1.75 (/ms) : open -> Iplus for tau = 0.3 ms at +30 with x5 + zeta = 0.03 (/ms) : Iplus -> open for tau = 25 ms at -30 with x6 + + : Vdep + x1 = 20 (mV) : Vdep of activation (alpha) + x2 = -20 (mV) : Vdep of deactivation (beta) + x3 = 1e12 (mV) : Vdep of opening (gamma) + x4 = -1e12 (mV) : Vdep of closing (delta) + x5 = 1e12 (mV) : Vdep into Ipos (epsilon) + x6 = -25 (mV) : Vdep out of Ipos (zeta) +} + +ASSIGNED { + alfac : microscopic reversibility factors + btfac + + : rates + f01 (/ms) + f02 (/ms) + f03 (/ms) + f04 (/ms) + f0O (/ms) + fip (/ms) + f11 (/ms) + f12 (/ms) + f13 (/ms) + f14 (/ms) + f1n (/ms) + fi1 (/ms) + fi2 (/ms) + fi3 (/ms) + fi4 (/ms) + fi5 (/ms) + fin (/ms) + + b01 (/ms) + b02 (/ms) + b03 (/ms) + b04 (/ms) + b0O (/ms) + bip (/ms) + b11 (/ms) + b12 (/ms) + b13 (/ms) + b14 (/ms) + b1n (/ms) + bi1 (/ms) + bi2 (/ms) + bi3 (/ms) + bi4 (/ms) + bi5 (/ms) + bin (/ms) + + v (mV) + ena (mV) + ina (milliamp/cm2) + gna (S/cm2) +} + +STATE { + C1 FROM 0 TO 1 + C2 FROM 0 TO 1 + C3 FROM 0 TO 1 + C4 FROM 0 TO 1 + C5 FROM 0 TO 1 + I1 FROM 0 TO 1 + I2 FROM 0 TO 1 + I3 FROM 0 TO 1 + I4 FROM 0 TO 1 + I5 FROM 0 TO 1 + O FROM 0 TO 1 + B FROM 0 TO 1 + I6 FROM 0 TO 1 +} + +BREAKPOINT { + SOLVE activation METHOD sparse + gna = gbar * O : O is "open state" + ina = gna * (v - ena) +} + +INITIAL { + rates(v) + SOLVE seqinitial +} + +KINETIC activation +{ + rates(v) + ~ C1 <-> C2 (f01,b01) + ~ C2 <-> C3 (f02,b02) + ~ C3 <-> C4 (f03,b03) + ~ C4 <-> C5 (f04,b04) + ~ C5 <-> O (f0O,b0O) + ~ O <-> B (fip,bip) + ~ O <-> I6 (fin,bin) + ~ I1 <-> I2 (f11,b11) + ~ I2 <-> I3 (f12,b12) + ~ I3 <-> I4 (f13,b13) + ~ I4 <-> I5 (f14,b14) + ~ I5 <-> I6 (f1n,b1n) + ~ C1 <-> I1 (fi1,bi1) + ~ C2 <-> I2 (fi2,bi2) + ~ C3 <-> I3 (fi3,bi3) + ~ C4 <-> I4 (fi4,bi4) + ~ C5 <-> I5 (fi5,bi5) + +CONSERVE C1 + C2 + C3 + C4 + C5 + O + B + I1 + I2 + I3 + I4 + I5 + I6 = 1 +} + +LINEAR seqinitial { : sets initial equilibrium + ~ I1*bi1 + C2*b01 - C1*( fi1+f01) = 0 + ~ C1*f01 + I2*bi2 + C3*b02 - C2*(b01+fi2+f02) = 0 + ~ C2*f02 + I3*bi3 + C4*b03 - C3*(b02+fi3+f03) = 0 + ~ C3*f03 + I4*bi4 + C5*b04 - C4*(b03+fi4+f04) = 0 + ~ C4*f04 + I5*bi5 + O*b0O - C5*(b04+fi5+f0O) = 0 + ~ C5*f0O + B*bip + I6*bin - O*(b0O+fip+fin) = 0 + ~ O*fip + B*bip = 0 + + ~ C1*fi1 + I2*b11 - I1*( bi1+f11) = 0 + ~ I1*f11 + C2*fi2 + I3*b12 - I2*(b11+bi2+f12) = 0 + ~ I2*f12 + C3*fi3 + I4*bi3 - I3*(b12+bi3+f13) = 0 + ~ I3*f13 + C4*fi4 + I5*b14 - I4*(b13+bi4+f14) = 0 + ~ I4*f14 + C5*fi5 + I6*b1n - I5*(b14+bi5+f1n) = 0 + + ~ C1 + C2 + C3 + C4 + C5 + O + B + I1 + I2 + I3 + I4 + I5 + I6 = 1 +} + +PROCEDURE rates(v(mV) ) +{ + alfac = (Oon/Con)^(1/4) + btfac = (Ooff/Coff)^(1/4) + f01 = 4 * alpha * exp(v/x1) + f02 = 3 * alpha * exp(v/x1) + f03 = 2 * alpha * exp(v/x1) + f04 = 1 * alpha * exp(v/x1) + f0O = gamma * exp(v/x3) + fip = epsilon * exp(v/x5) + f11 = 4 * alpha * alfac * exp(v/x1) + f12 = 3 * alpha * alfac * exp(v/x1) + f13 = 2 * alpha * alfac * exp(v/x1) + f14 = 1 * alpha * alfac * exp(v/x1) + f1n = gamma * exp(v/x3) + fi1 = Con + fi2 = Con * alfac + fi3 = Con * alfac^2 + fi4 = Con * alfac^3 + fi5 = Con * alfac^4 + fin = Oon + + b01 = 1 * beta * exp(v/x2) + b02 = 2 * beta * exp(v/x2) + b03 = 3 * beta * exp(v/x2) + b04 = 4 * beta * exp(v/x2) + b0O = delta * exp(v/x4) + bip = zeta * exp(v/x6) + b11 = 1 * beta * btfac * exp(v/x2) + b12 = 2 * beta * btfac * exp(v/x2) + b13 = 3 * beta * btfac * exp(v/x2) + b14 = 4 * beta * btfac * exp(v/x2) + b1n = delta * exp(v/x4) + bi1 = Coff + bi2 = Coff * btfac + bi3 = Coff * btfac^2 + bi4 = Coff * btfac^3 + bi5 = Coff * btfac^4 + bin = Ooff +} + diff --git a/cnmodel/mechanisms/tests/test_mechanisms.py b/cnmodel/mechanisms/tests/test_mechanisms.py new file mode 100644 index 0000000..1a128c4 --- /dev/null +++ b/cnmodel/mechanisms/tests/test_mechanisms.py @@ -0,0 +1,40 @@ +import numpy as np +from neuron import h +from cnmodel.util import reset + + +def test_max_open_probability(): + reset( + raiseError=False + ) # reset() fails as unable to remove all neuron objects, unless we ignore the error + sec = h.Section() + + # Create AMPA and NMDA mechanisms + # AMPA uses mode=0; no rectification + apsd = h.AMPATRUSSELL(0.5, sec=sec) + # For NMDA we will hold the cell at +40 mV + npsd = h.NMDA_Kampa(0.5, sec=sec) + + # And a presynaptic terminal to provide XMTR input + term = h.MultiSiteSynapse(0.5, sec=sec) + term.nZones = 1 + h.setpointer(term._ref_XMTR[0], "XMTR", apsd) + h.setpointer(term._ref_XMTR[0], "XMTR", npsd) + + h.celsius = 34.0 + h.finitialize() + op = [[], []] + for i in range(100): + # force very high transmitter concentration for every timestep + term.XMTR[0] = 10000 + sec.v = 40.0 + h.fadvance() + op[0].append(apsd.Open) + op[1].append(npsd.Open) + + assert np.allclose(max(op[0]), apsd.MaxOpen) + assert np.allclose(max(op[1]), npsd.MaxOpen) + + +if __name__ == "__main__": + test_max_open_probability() diff --git a/cnmodel/mechanisms/vecevent.mod b/cnmodel/mechanisms/vecevent.mod new file mode 100644 index 0000000..5aadf4f --- /dev/null +++ b/cnmodel/mechanisms/vecevent.mod @@ -0,0 +1,72 @@ +: Vector stream of events +: From NEURON source: nrn/examples/nrniv/netcon/vecevent.mod + +NEURON { + ARTIFICIAL_CELL VecStim +} + +ASSIGNED { + index + etime (ms) + space +} + +INITIAL { + index = 0 + element() + if (index > 0) { + net_send(etime - t, 1) + } +} + +NET_RECEIVE (w) { + if (flag == 1) { + net_event(t) + element() + if (index > 0) { + net_send(etime - t, 1) + } + } +} + +VERBATIM +extern double* vector_vec(); +extern int vector_capacity(); +extern void* vector_arg(); +ENDVERBATIM + +PROCEDURE element() { +VERBATIM + { void* vv; int i, size; double* px; + i = (int)index; + if (i >= 0) { + vv = *((void**)(&space)); + if (vv) { + size = vector_capacity(vv); + px = vector_vec(vv); + if (i < size) { + etime = px[i]; + index += 1.; + }else{ + index = -1.; + } + }else{ + index = -1.; + } + } + } +ENDVERBATIM +} + +PROCEDURE play() { +VERBATIM + void** vv; + vv = (void**)(&space); + *vv = (void*)0; + if (ifarg(1)) { + *vv = vector_arg(1); + } +ENDVERBATIM +} + + diff --git a/cnmodel/morphology/__init__.py b/cnmodel/morphology/__init__.py new file mode 100755 index 0000000..4299d96 --- /dev/null +++ b/cnmodel/morphology/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/python +# +# Morphology definitions for models. +# +# This file includes readers for +# +# Paul B. Manis, Ph.D. 2009 (August - November 2009) +# +from neuron import h + +from .morphology import Morphology +from .hoc_reader import HocReader diff --git a/cnmodel/morphology/bushy_stick.hoc b/cnmodel/morphology/bushy_stick.hoc new file mode 100644 index 0000000..e0db2f1 --- /dev/null +++ b/cnmodel/morphology/bushy_stick.hoc @@ -0,0 +1,90 @@ +objref soma +soma = new SectionList() +objref primarydendrite +primarydendrite = new SectionList() +objref secondarydendrite +secondarydendrite = new SectionList() +objref hillock +hillock = new SectionList() +objref unmyelinatedaxon +unmyelinatedaxon = new SectionList() +objref myelinatedaxon +myelinatedaxon = new SectionList() + +create sections[9] +access sections[0] +soma.append() +sections[0] { + pt3dadd(0., 0., 0., 25.) + pt3dadd(0., 25., 0., 25.) +} + +// axon hillock, 15 microns long, tapered +access sections[1] +hillock.append() +connect sections[1](0), sections[0](0) +sections[1] { + pt3dadd(0., 0., 0., 2.5) + pt3dadd(0., -15., 0., 1.0) +} + +// initial segment, unmyelinated, 10 microns long, not tapering +access sections[2] +unmyelinatedaxon.append() +connect sections[2](0), sections[1](1) +sections[2] { + pt3dadd(0., -15., 0., 1.0) + pt3dadd(0., -25., 0., 1.0) + +} + +// beginning of myelinated axon, 20 micron, enlarging with distance +access sections[3] +myelinatedaxon.append() +connect sections[3](0), sections[2](1) +sections[3] { + pt3dadd(0., -25., 0., 1.0) + pt3dadd(0., -45., 0., 2.0) + +} + +access sections[4] +primarydendrite.append() +connect sections[4](0), sections[0](1) +sections[4] { + pt3dadd(0., 25., 0., 3.0) + pt3dadd(0., 45., 0., 2.0) +} + +access sections[5] +secondarydendrite.append() +connect sections[5](0), sections[4](1) +sections [5] { + pt3dadd(0., 45., 0., 2.0) + pt3dadd(0., 55., -50., 1.5) +} + +access sections[6] +secondarydendrite.append() +connect sections[6](0), sections[4](1) +sections [6] { + pt3dadd(0., 45., 0., 2.0) + pt3dadd(0., 55., 50., 1.5) +} + +access sections[7] +secondarydendrite.append() +connect sections[7](0), sections[4](1) +sections [7] { + pt3dadd(0., 45., 0., 2.0) + pt3dadd(-50., 55., 0., 1.5) +} + +access sections[8] +secondarydendrite.append() +connect sections[8](0), sections[4](1) +sections [8] { + pt3dadd(0., 45., 0., 2.0) + pt3dadd(50., 55., 0, 1.5) +} + diff --git a/cnmodel/morphology/hoc_reader.py b/cnmodel/morphology/hoc_reader.py new file mode 100644 index 0000000..41aac62 --- /dev/null +++ b/cnmodel/morphology/hoc_reader.py @@ -0,0 +1,622 @@ +from __future__ import print_function +from neuron import h +import neuron +import collections +import numpy as np +import pyqtgraph as pg +import os +import re +import os.path + +try: + basestring +except NameError: + basestring = str + + +class HocReader(object): + def __init__(self, hoc): + """ + Provides useful methods for reading hoc structures. + + Parameters + ---------- + hoc : :obj: `hoc` or str + Either a hoc object that hs been already created, or a string that defines a hoc file name. + """ + + self.file_loaded = False + if isinstance(hoc, basestring): + fullfile = os.path.join(os.getcwd(), hoc) + if not os.path.isfile(fullfile): + raise Exception("File not found: %s" % (fullfile)) + neuron.h.hoc_stdout( + "/dev/null" + ) # prevent junk from printing while reading the file + success = neuron.h.load_file(str(fullfile)) + neuron.h.hoc_stdout() + if success == 0: # indicates failure to read the file + raise NameError("Found file, but NEURON load failed: %s" % (fullfile)) + self.file_loaded = True + self.h = h # save a copy of the hoc object itself. + else: + self.h = hoc # just use the passed argument + self.file_loaded = True + + # geometry containers + self.edges = None + self.vertexes = None + + # all sections in the hoc {sec_name: hoc Section} + self.sections = collections.OrderedDict() + # {sec_name: index} indicates index into self.sections.values() where + # Section can be found. + self.sec_index = {} + # {sec_name: [mechanism, ...]} + self.mechanisms = collections.OrderedDict() + # {sec_group_name: set(sec_name, ...)} + self.sec_groups = {} + + # topology {section: (parent, [children,...])} + self.topology = {} + + # populate self.sections, self.sec_index, and self.mechanisms + self.read_section_info() + + # auto-generate section groups based on either hoc section lists, or + # on section name prefixes. + sec_lists = self.get_section_lists() + sec_prefixes = self.get_section_prefixes() + + # Add groupings by section list if possible: + if len(sec_lists) > 1: + self.add_groups_by_section_list(sec_lists) + + # Otherwise, try section prefixes + elif len(sec_prefixes) > 1: + for group, sections in sec_prefixes.items(): + self.add_section_group(group, sections) + + # generate topology + self._generate_topology() + + def get_section(self, sec_name): + """ + Get the section associated with the section name + + Parameters + ---------- + sec_name : str + The name of the section object. + + Returns + ------- + The hoc Section object with the given name. + + """ + try: + return self.sections[sec_name] + except KeyError: + raise KeyError("No section named '%s'" % sec_name) + + def get_section_prefixes(self): + """ + Go through all the sections and generate a dictionary mapping their + name prefixes to the list of sections with that prefix. + + For example, with sections names axon[0], axon[1], ais[0], and soma[0], + we would generate the following structure: + + {'axon': ['axon[0]', 'axon[1]'], + 'ais': ['ais[0]'], + 'soma': ['soma[0]']} + """ + prefixes = {} + regex = re.compile("(?P\w+)\[(\d*)\]") + for sec_name in self.sections: + g = regex.match(sec_name) + if g is None: + continue + prefix = g.group("prefix") + prefixes.setdefault(prefix, []).append(sec_name) + return prefixes + + def get_mechanisms(self, section): + """ + Get a set of all of the mechanisms inserted into a given section + + Parameters + ---------- + section : :obj: `NEURON section` + The NEURON section object. + + Returns + ------- + A list of mechanism names + + """ + return self.mechanisms[section] + + def get_density(self, section, mechanism): + """ + Get density mechanism that may be found the section. + mechanism is a list ['name', 'gbarname']. This is needed because + some mechanisms do not adhere to any convention and may have a different + kind of 'gbarname' than 'gbar_mechname' + returns the average of the conductance density, as that may range across different + values in a section (e.g., can vary by segments) + + Parameters + ---------- + section : :obj: `NEURON section` + The NEURON section object. + mechanism : list + mechanism is a list ['name', 'gbarname']. It is used to + retrieve the mechanism density from HOC as + `segment.name.gbarname`. + + Returns + ------- + Mean conductance of the selected mechanism in the section, averaged across all segments of the section. + """ + + gmech = [] + for seg in section: + try: + x = getattr(seg, mechanism[0]) + mecbar = "%s_%s" % (mechanism[1], mechanism[0]) + if mecbar in dir(x): + gmech.append(getattr(x, mechanism[1])) + else: + print( + "hoc_reader:get_density did not find the mechanism in dir x", + dir(x), + ) + except NameError: + return 0.0 + except: + print("hoc_reader:get_density failed to evaluate the mechanisms... ") + raise + + # print gmech + if len(gmech) == 0: + gmech = 0.0 + return np.mean(gmech) + + def get_sec_info(self, section, html=False): + """ + Get the info of the given section + modified from: neuronvisio + + Parameters + ---------- + section : :obj: `NEURON section` + The NEURON section object. + + Returns + ------- + str + containing the information, with html formatting. + """ + if html: + br = " " + bre = "" + lbrk = "
" + li = "
  • " + lie = "
  • " + else: + br = " " + bre = "" + li = "" + lie = "" + lbrk = "\n" + info = f"{br:s}Section Name:{bre:s} {section.name():s}{lbrk:s}" + info += f"{br:s}Length [um]:{bre:s} {section.L:f}{lbrk:s}" + info += f"{br:s}Diameter [um]:{bre:s} {section.diam:f}{lbrk:s}" + info += f"{br:s}Membrane Capacitance:{bre:s} {section.cm:f}{lbrk:s}" + info += f"{br:s}Axial Resistance :{bre:s} {section.Ra:f}{lbrk:s}" + info += f"{br:s}Number of Segments:{bre:s} {section.nseg:f}{lbrk:s}" + mechs = [] + for seg in section: + for mech in seg: + mechs.append(mech.name()) + mechs = set(mechs) # Excluding the repeating ones + + mech_info = f"{br:s}Mechanisms in the section{bre:s}{lbrk:s}" + for mech_name in mechs: + s = f"{li:s} {mech_name:s} {lie:s}" + mech_info += s + info += mech_info + return info + + def read_section_info(self): + """ + Read all the information about the sections in the current hoc file + Stores the result in the mechanisms class variable. + """ + # Collect list of all sections and their mechanism names. + self.sections = collections.OrderedDict() + self.mechanisms = collections.OrderedDict() + for i, sec in enumerate(self.h.allsec()): + self.sections[sec.name()] = sec + self.sec_index[sec.name()] = i + mechs = set() + for seg in sec: + for mech in seg: + mechs.add(mech.name()) + self.mechanisms[sec.name()] = mechs + + def hoc_namespace(self): + """ + Get a dict of the HOC namespace {'variable_name': hoc_object}. + NOTE: this method requires NEURON >= 7.3 + """ + names = {} + for hvar in dir(self.h): # look through the whole list, no other way + try: + # some variables can't be pointed to... + if hvar in [ + "nseg", + "diam_changed", + "nrn_shape_changed_", + "secondorder", + "stoprun", + ]: + continue + u = getattr(self.h, hvar) + names[hvar] = u + except: + continue + return names + + def find_hoc_hname(self, regex): + """ + Find hoc names matching a pattern + + Parameters + ---------- + regex : str + Regular expression (Python Re module) to search for. + + Returns + ------- + list + The names of HOC objects whose *hname* matches regex. + """ + objs = [] + ns = self.hoc_namespace() + for n, v in ns.items(): + try: + hname = v.hname() + if re.match(regex, hname): + objs.append(n) + except: + continue + return objs + + def add_section_group(self, name, sections, overwrite=False): + """ + Declare a grouping of sections (or section names). Sections may be + grouped by any arbitrary criteria (cell, anatomical type, etc). + + Parameters + ---------- + name : str + name of the section group + sections: list + section names or hoc Section objects. + + """ + if name in self.sec_groups and not overwrite: + raise Exception( + "Group name %s is already used (use overwrite=True)." % name + ) + + group = set() + for sec in sections: + if not isinstance(sec, basestring): + sec = sec.name() + group.add(sec) + self.sec_groups[name] = group + + def get_section_group(self, name): + """ + Get a section group by name + Parameters + ---------- + name : str + name of the group (dendrite, for example) + + Returns + ------- + The set of section names in the group *name*. + + """ + return self.sec_groups[name] + + def get_section_lists(self): + """ + Search through all of the hoc variables to find those that are "SectionLists" + """ + return self.find_hoc_hname(regex=r"SectionList\[") + # ns = self.hoc_namespace() + # return [name for name in ns if ns[name].hname().startswith('SectionList[')] + + def add_groups_by_section_list(self, names): + """ + Add a new section groups from the hoc variables indicated in *names*. + + Parameters + ----------- + names : list + List containing variable names as strings. Each name must refer to a list of + Sections in hoc. If a dict is supplied instead, then it + maps {hoc_list_name: section_group_name}. + + Side effects (modifies) + ----------------------- + calls add_section_group + + """ + # if a list is supplied, then the names of groups to create are + # exactly the same as the names of hoc lists. + if not isinstance(names, dict): + names = {name: name for name in names} + for hoc_name, group_name in names.items(): + var = getattr(self.h, hoc_name) + self.add_section_group(group_name, list(var)) + + def get_geometry(self): + """ + modified from:neuronvisio + Generate structures that describe the geometry of the sections and their segments (all segments are returned) + + Returns + ------- + vertexes : record array containing {pos: (x,y,z), dia, sec_id} + for each segment. + edges : array of pairs indicating the indexes of connected + vertexes. + Side effects + ------------ + Modifies vertexes and edges. + + """ + + # return cached geometry if this method has already run. + if self.vertexes is not None: + return self.vertexes, self.edges + + self.h.define_shape() + + # map segments (lines) to the section that contains them + self.segment_to_section = {} + + vertexes = [] + connections = [] + + for secid, sec in enumerate(self.sections.values()): + x_sec, y_sec, z_sec, d_sec = self.retrieve_coordinate(sec) + + for i, xi in enumerate(x_sec): + vertexes.append(((x_sec[i], y_sec[i], z_sec[i]), d_sec[i], secid)) + indx_geom_seg = len(vertexes) - 1 + if len(vertexes) > 1 and i > 0: + connections.append([indx_geom_seg, indx_geom_seg - 1]) + + self.edges = np.array(connections) + self.vertexes = np.empty( + len(vertexes), dtype=[("pos", float, 3), ("dia", float), ("sec_index", int)] + ) + self.vertexes[:] = vertexes + return self.vertexes, self.edges + + def retrieve_coordinate(self, section): + """Retrieve the coordinates of a section avoiding duplicates + + Parameters + ---------- + section : :obj: `NEURON section` + The NEURON section object. + + Returns + ------- + array + arrays of x, y, z, d for all the segments of the specified section. + + """ + + section.push() + x, y, z, d = [], [], [], [] + + tot_points = 0 + connect_next = False + for i in range(int(self.h.n3d())): + present = False + x_i = self.h.x3d(i) + y_i = self.h.y3d(i) + z_i = self.h.z3d(i) + d_i = self.h.diam3d(i) + # Avoiding duplicates in the sec + if x_i in x: + ind = len(x) - 1 - x[::-1].index(x_i) # Getting the index of last value + if y_i == y[ind]: + if z_i == z[ind]: + present = True + + if not present: + k = (x_i, y_i, z_i) + x.append(x_i) + y.append(y_i) + z.append(z_i) + d.append(d_i) + self.h.pop_section() + return (np.array(x), np.array(y), np.array(z), np.array(d)) + + def _generate_topology(self): + for name, sec in self.sections.items(): + sref = self.h.SectionRef(sec=sec) + parent = sref.parent().sec.name() if sref.has_parent() else None + if name not in self.topology: + self.topology[name] = [None, []] + self.topology[name][0] = parent + if parent is not None: + if parent not in self.topology: + self.topology[parent] = [None, []] + self.topology[parent][1].append(name) + + def get_branch(self, root): + """ + Return all sections in a branch, starting with root. + + Parameters + ---------- + root : :obj: `NEURON section` + The NEURON section object. + + """ + branch = [root] + childs = [root] + while len(childs) > 0: + new_childs = [] + for ch in childs: + new_childs.extend(self.topology[ch][1]) + childs = new_childs + branch.extend(childs) + return branch + + def translate_branch(self, root, dx): + """ + Move the branch beginning at *root* by *dx*. + + Parameters + ---------- + root : :obj: `NEURON section` + The NEURON section object. + dx : array + Which must be an array of length 3 defining the translation. + + """ + self.get_geometry() + dx[np.newaxis, :] + for name in self.get_branch(root): + sid = self.sec_index[name] + mask = self.vertexes["sec_index"] == sid + self.vertexes["pos"][mask] += dx + + def make_volume_data(self, resolution=1.0, max_size=500e6): + """ + Using the current state of vertexes, edges, generates a scalar field + useful for building isosurface or volumetric renderings. + + Parameters + ---------- + resolution: float, default=1.0 microns + width (um) of a single voxel in the scalar field. + max_size: int + maximum allowed scalar field size (bytes). + Returns + ------- + * 3D scalar field indicating distance from nearest membrane, + * 3D field indicating section IDs of nearest membrane, + * QTransform that maps from 3D array indexes to original vertex + coordinates. + """ + vertexes, lines = self.get_geometry() + + maxdia = vertexes["dia"].max() # maximum diameter (defines shape of kernel) + kernel_size = int(maxdia / resolution) + 3 # width of kernel + + # read vertex data + pos = vertexes["pos"] + d = vertexes["dia"] + sec_id = vertexes["sec_index"] + + # decide on dimensions of scalar field + mx = pos.max(axis=0) + mn = pos.min(axis=0) + diff = mx - mn + shape = tuple((diff / resolution + kernel_size).astype(int)) + + # prepare blank scalar field for drawing + size = np.dtype(np.float32).itemsize * shape[0] * shape[1] * shape[2] + if size > max_size: + raise Exception( + "Scalar field would be larger than max_size (%dMB > %dMB), resolution is%f" + % (size / 1e6, max_size / 1e6, resolution) + ) + scfield = np.zeros(shape, dtype=np.float32) + scfield[:] = -1000 + + # array for holding IDs of sections that contribute to each area + idfield = np.empty(shape, dtype=int) + idfield[:] = -1 + + # map vertex locations to voxels + vox_pos = pos.copy() + vox_pos -= mn.reshape((1, 3)) + vox_pos *= 1.0 / resolution + + # Define kernel used to draw scalar field along dendrites + def cone(i, j, k): + # value decreases linearly with distance from center of kernel. + w = kernel_size / 2 + return w - ((i - w) ** 2 + (j - w) ** 2 + (k - w) ** 2) ** 0.5 + + kernel = resolution * np.fromfunction(cone, (kernel_size,) * 3) + kernel -= kernel.max() + + def array_intersection(arr1, arr2, pos): + """ + Return slices used to access the overlapping area between two + arrays that are offset such that the origin of *arr2* is a *pos* + relative to *arr1*. + """ + s1 = [0] * 3 + s2 = [0] * 3 + t1 = [0] * 3 + t2 = [0] * 3 + pos = map(int, pos) + for axis in range(3): + s1[axis] = max(0, -pos[axis]) + s2[axis] = min(arr2.shape[axis], arr1.shape[axis] - pos[axis]) + t1[axis] = max(0, pos[axis]) + t2[axis] = min(arr1.shape[axis], pos[axis] + arr2.shape[axis]) + slice1 = (slice(t1[0], t2[0]), slice(t1[1], t2[1]), slice(t1[2], t2[2])) + slice2 = (slice(s1[0], s2[0]), slice(s1[1], s2[1]), slice(s1[2], s2[2])) + return slice1, slice2 + + # Draw lines into volume using *kernel* as the brush + vox_pos[:, 0] = np.clip(vox_pos[:, 0], 0, scfield.shape[0] - 1) + vox_pos[:, 1] = np.clip(vox_pos[:, 1], 0, scfield.shape[1] - 1) + vox_pos[:, 2] = np.clip(vox_pos[:, 2], 0, scfield.shape[2] - 1) + for c in range(lines.shape[0]): + i = lines[c, 0] + j = lines[c, 1] + p1 = vox_pos[i].copy() + p2 = vox_pos[j].copy() + diff = p2 - p1 + axis = np.argmax(np.abs(diff)) + dia = d[i] + nvoxels = abs(int(diff[axis])) + 1 + for k in range(nvoxels): + kern = kernel + (dia / 2.0) + sl1, sl2 = array_intersection( + scfield, kern, p1 + ) # find the overlapping area between the field and the kernel + idfield[sl1] = np.where( + scfield[sl1] > kern[sl2], idfield[sl1], sec_id[i] + ) + scfield[sl1] = np.where( + scfield[sl1] > kern[sl2], scfield[sl1], kern[sl2] + ) + dia += (d[j] - d[i]) / nvoxels + p1 += diff / nvoxels + + # return transform relating volume data to original vertex data + transform = pg.Transform3D() + w = resolution * kernel_size / 2 # offset introduced due to kernel + transform.translate(*(mn - w)) + transform.scale(resolution, resolution, resolution) + transform.translate(1, 1, 1) + return scfield, idfield, transform diff --git a/cnmodel/morphology/morphology.py b/cnmodel/morphology/morphology.py new file mode 100644 index 0000000..2d1e1e7 --- /dev/null +++ b/cnmodel/morphology/morphology.py @@ -0,0 +1,31 @@ +class Morphology(object): + """ + Base class for morphological structure + Provides the ability to read .hoc files for NEURON simulations. + + """ + + def init(): + pass + + def hocReader(self, filename=None): + """ + Access the hoc reader + + Parameters + ---------- + filename : str (default: None) + The hoc file to read and save as the morphology object + """ + self.morphology = hoc_reader(hoc=filename) + + def swcReader(self, filename=None): + """ + Access the swc reader (***NOT IMPLEMENTED***) + + Parameters + ---------- + filename : str (default: None) + The swc file to read and save as the morphology object + """ + raise ValueError("swcReader not implmented.") diff --git a/cnmodel/morphology/octopus_spencer_stick.hoc b/cnmodel/morphology/octopus_spencer_stick.hoc new file mode 100644 index 0000000..9712bdf --- /dev/null +++ b/cnmodel/morphology/octopus_spencer_stick.hoc @@ -0,0 +1,76 @@ +// Octopus cell, stick representation +// From Spencer et al., Front. Comput. Neurosci., 22 October 2012 | https://doi.org/10.3389/fncom.2012.00083 +// Figure 1. + +objref soma +soma = new SectionList() +objref primarydendrite +primarydendrite = new SectionList() +objref hillock +hillock = new SectionList() +objref unmyelinatedaxon +unmyelinatedaxon = new SectionList() + +create sections[7] + +access sections[0] +soma.append() +sections[0] { + pt3dadd(0., 0., 0., 25.) + pt3dadd(0., 25., 0., 25.) +} + +// axon hillock, 30 microns long, untapered +access sections[1] +hillock.append() +connect sections[1](0), sections[0](0) +sections[1] { + pt3dadd(0., 0., 0., 3) + pt3dadd(0., -30., 0., 3) +} + +// initial segment, unmyelinated, 10 microns long, not tapering +access sections[2] +unmyelinatedaxon.append() +connect sections[2](0), sections[1](1) +sections[2] { + pt3dadd(0., -30., 0., 3.0) + pt3dadd(0., -32., 0., 3.0) +} + +// angle dendrites out a bit from original model for visibility, so +// length for 20 u end deviation is 279.285 um, end pos 304.285 + +access sections[3] +primarydendrite.append() +connect sections[3](0), sections[0](1) +sections[3] { + pt3dadd(0., 25., 0., 3) + pt3dadd(20., 304.285, 0., 3) +} + +access sections[4] +primarydendrite.append() +connect sections[4](0), sections[0](1) +sections [4] { + pt3dadd(0., 25., 0., 3) + pt3dadd(0., 304.285, 20., 3) +} + +access sections[5] +primarydendrite.append() +connect sections[5](0), sections[0](1) +sections [5] { + pt3dadd(0., 25., 0., 3) + pt3dadd(-20., 304.285, 0., 3) +} + +access sections[6] +primarydendrite.append() +connect sections[6](0), sections[0](1) +sections [6] { + pt3dadd(0., 25., 0., 3) + pt3dadd(0., 304.285, -20., 3) +} + + diff --git a/cnmodel/morphology/tstellate_stick.hoc b/cnmodel/morphology/tstellate_stick.hoc new file mode 100644 index 0000000..f4ff679 --- /dev/null +++ b/cnmodel/morphology/tstellate_stick.hoc @@ -0,0 +1,82 @@ +objref soma +soma = new SectionList() +objref primarydendrite +primarydendrite = new SectionList() +objref secondarydendrite +secondarydendrite = new SectionList() +objref hillock +hillock = new SectionList() +objref unmyelinatedaxon +unmyelinatedaxon = new SectionList() +objref myelinatedaxon +myelinatedaxon = new SectionList() + +create sections[8] +access sections[0] +soma.append() +sections[0] { + pt3dadd(0., 0., 0., 25.) + pt3dadd(0., 25., 0., 25.) +} + +// axon hillock, 15 microns long, tapered +access sections[1] +hillock.append() +connect sections[1](0), sections[0](0) +sections[1] { + pt3dadd(0., 0., 0., 2.5) + pt3dadd(0., -15., 0., 1.0) +} + +// initial segment, unmyelinated, 10 microns long, not tapering +access sections[2] +unmyelinatedaxon.append() +connect sections[2](0), sections[1](1) +sections[2] { + pt3dadd(0., -15., 0., 1.0) + pt3dadd(0., -25., 0., 1.0) + +} + +// beginning of myelinated axon, 20 micron, enlarging with distance +access sections[3] +myelinatedaxon.append() +connect sections[3](0), sections[2](1) +sections[3] { + pt3dadd(0., -25., 0., 1.0) + pt3dadd(0., -45., 0., 2.0) + +} + +access sections[4] +primarydendrite.append() +connect sections[4](0), sections[0](1) +sections[4] { + pt3dadd(0., 25., 0., 2) + pt3dadd(125., 25., 25., 1.5) +} + +access sections[5] +primarydendrite.append() +connect sections[5](0), sections[0](1) +sections [5] { + pt3dadd(0., 25., 0., 2.0) + pt3dadd(-125., 25., 25., 1.5) +} + +access sections[6] +primarydendrite.append() +connect sections[6](0), sections[0](1) +sections [6] { + pt3dadd(0., 25., 0., 2.0) + pt3dadd(125, 25., -25., 1.5) +} + +access sections[7] +primarydendrite.append() +connect sections[7](0), sections[0](1) +sections [7] { + pt3dadd(0., 25., 0., 2.0) + pt3dadd(-125., 25., -25., 1.5) +} + diff --git a/cnmodel/morphology/tv_stick.hoc b/cnmodel/morphology/tv_stick.hoc new file mode 100644 index 0000000..deb10e4 --- /dev/null +++ b/cnmodel/morphology/tv_stick.hoc @@ -0,0 +1,32 @@ +objref soma +soma = new SectionList() +objref axon +axon = new SectionList() +objref dendrite +dendrite = new SectionList() + +create sections[3] +access sections[0] +soma.append() +sections[0] { + pt3dadd(0., 0., 0., 25.) + pt3dadd(0., 25., 0., 25.) +} + +access sections[1] +dendrite.append() +connect sections[1](0), sections[0](1) +sections[1] { + pt3dadd(0., 25., 0., 1.5) + pt3dadd(0., 220., 50., 1.5) +} + +access sections[2] +dendrite.append() +connect sections[2](0), sections[0](1) +sections[2] { + pt3dadd(0., 25., 0., 1.5) + pt3dadd(0., 220., -50., 1.5) +} + + diff --git a/cnmodel/populations/__init__.py b/cnmodel/populations/__init__.py new file mode 100644 index 0000000..5a22417 --- /dev/null +++ b/cnmodel/populations/__init__.py @@ -0,0 +1,13 @@ +""" +Definitions of cell populations for each cell type. + + +""" + +from .population import Population +from .bushy import Bushy +from .tstellate import TStellate +from .dstellate import DStellate +from .pyramidal import Pyramidal +from .tuberculoventral import Tuberculoventral +from .sgc import SGC diff --git a/cnmodel/populations/bushy.py b/cnmodel/populations/bushy.py new file mode 100644 index 0000000..99ebadc --- /dev/null +++ b/cnmodel/populations/bushy.py @@ -0,0 +1,58 @@ +import scipy.stats +import numpy as np + +from .population import Population +from .. import cells + + +class Bushy(Population): + """Population of bushy cells. + + Cells are distributed uniformly from 2kHz to 64kHz. + + Note that `cf` is the mean value used when selecting SGCs to connect; + it is NOT the measured CF of the cell (although it should be close). + """ + + type = "bushy" + + def __init__(self, species="mouse", **kwds): + freqs = self._get_cf_array(species) + fields = [ + ("cf", float), + ("input_sr", list), # distribution probability of SGC SR groups + ("sr", int), + ] + super(Bushy, self).__init__(species, len(freqs), fields=fields, **kwds) + self._cells["cf"] = freqs + self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))] + + def create_cell(self, cell_rec): + """ Return a single new cell to be used in this population. The + *cell_rec* argument is the row from self.cells that describes the cell + to be created. + """ + return cells.Bushy.create(species=self.species, **self._cell_args) + + def connection_stats(self, pop, cell_rec): + """ The population *pop* is being connected to the cell described in + *cell_rec*. Return the number of presynaptic cells that should be + connected and a dictionary of distributions used to select cells + from *pop*. + """ + size, dist = Population.connection_stats(self, pop, cell_rec) + + from .. import populations + + if isinstance(pop, populations.SGC): + # only select SGC inputs from a single SR group + # (this relationship is hypothesized based on reconstructions of + # endbulbs) + sr_vals = pop.cells["sr"] + u = np.random.choice(sr_vals) # assign input sr for this cell + # print('u: ', u) + # pick from one sr group for all inputs, with prob same as distribution in nerve + dist["sr"] = (sr_vals == u).astype(float) + self._cells["sr"] = u + + return size, dist diff --git a/cnmodel/populations/dstellate.py b/cnmodel/populations/dstellate.py new file mode 100644 index 0000000..246e8ea --- /dev/null +++ b/cnmodel/populations/dstellate.py @@ -0,0 +1,42 @@ +import scipy.stats +import numpy as np + +from .population import Population +from .. import cells + + +class DStellate(Population): + type = "dstellate" + + def __init__(self, species="mouse", **kwds): + # Note that `cf` is the mean value used when selecting SGCs to connect; + # it is NOT the measured CF of the cell (although it should be close). + freqs = self._get_cf_array(species) + fields = [ + ("cf", float), + ("input_sr", list), # distribution probability of SGC SR groups + ] + super(DStellate, self).__init__(species, len(freqs), fields=fields, **kwds) + self._cells["cf"] = freqs + self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))] + + def create_cell(self, cell_rec): + """ Return a single new cell to be used in this population. The + *cell_rec* argument is the row from self.cells that describes the cell + to be created. + """ + return cells.DStellate.create(species=self.species, **self._cell_args) + + def connection_stats(self, pop, cell_rec): + """ The population *pop* is being connected to the cell described in + *cell_rec*. Return the number of presynaptic cells that should be + connected and a dictionary of distributions used to select cells + from *pop*. + """ + size, dist = Population.connection_stats(self, pop, cell_rec) + + from .. import populations + + if isinstance(pop, populations.SGC): + dist["sr"] = (pop.cells["sr"] < 2).astype(float) + return size, dist diff --git a/cnmodel/populations/population.py b/cnmodel/populations/population.py new file mode 100644 index 0000000..5e2c6b7 --- /dev/null +++ b/cnmodel/populations/population.py @@ -0,0 +1,443 @@ +import logging +import scipy.stats +import numpy as np + +from .. import data + + +class Population(object): + """ + A Population represents a group of cell all having the same type. + + Populations provide methods for: + + * Adding cells to the population with characteristic distributions. + * Connecting the cells in one population to the cells in another. + * Automatically adding cells to satisfy connectivity requirements when + connecting populations together. + + Populations have a concept of a "natural" underlying distribution of + neurons, and behave as if all neurons in this distribution already exist + in the model. However, initially all neurons are virtual, and are only + instantiated to become a part of the running model if the neuron provides + synaptic input to another non-virtual neuron, or if the user explicitly + requests a recording of the neuron. + + Subclasses represent populations for a specific cell type, and at least + need to reimplement the `create_cell` and `connection_stats` methods. + """ + + def __init__(self, species, size, fields, synapsetype="multisite", **kwds): + self._species = species + self._post_connections = [] # populations this one connects to + self._pre_connections = [] # populations connecting to this one + self._synapsetype = synapsetype + # numpy record array with information about each cell in the + # population + fields = [ + ("id", int), + ("cell", object), + ("input_resolved", bool), + ("connections", object), # {pop: [cells], ...} + ] + fields + self._cells = np.zeros(size, dtype=fields) + self._cells["id"] = np.arange(size) + self._cell_indexes = {} # maps cell:index + self._cell_args = kwds + + @property + def cells(self): + """ The array of cells in this population. + + For all populations, this array has a 'cell' field that is either 0 + (for virtual cells) or a Cell instance (for real cells). + + Extra fields may be added by each Population subclass. + """ + return self._cells.copy() + + @property + def species(self): + return self._species + + def unresolved_cells(self): + """ Return indexes of all real cells whose inputs have not been + resolved. + """ + real = self._cells["cell"] != 0 + unresolved = self._cells["input_resolved"] == False + return np.argwhere(real & unresolved)[:, 0] + + def real_cells(self): + """ Return indexes of all real cells in this population. + + Initially, all cells in the population are virtual--they are accounted + for, but not actually instantiated as part of the NEURON simulation. + Virtual cells can be made real by calling `get_cell()`. This method + returns the indexes of all cells for which `get_cell()` has already + been invoked. + """ + return np.argwhere(self._cells["cell"] != 0)[:, 0] + + def connect(self, *pops): + """ Connect this population to any number of other populations. + + A connection is unidirectional; calling ``pop1.connect(pop2)`` can only + result in projections from pop1 to pop2. + + Note that the connection is purely symbolic at first; no cells are + actually connected by synapses at this time. + """ + self._post_connections.extend(pops) + for pop in pops: + pop._pre_connections.append(self) + + @property + def pre_connections(self): + """ The list of populations connected to this one. + """ + return self._pre_connections[:] + + def cell_connections(self, index): + """ Return a dictionary containing, for each population, a list of + cells connected to the cell in this population at *index*. + """ + return self._cells[index]["connections"] + + def resolve_inputs(self, depth=1, showlog=False): + """ For each _real_ cell in the population, select a set of + presynaptic partners from each connected population and generate a + synapse from each. + + Although it is allowed to call ``resolve_inputs`` multiple times for + a single population, each individual cell will only resolve its inputs + once. Therefore, it is recommended to create and connect all + populations before making any calls to ``resolve_inputs``. + """ + for i in self.unresolved_cells(): + # loop over all cells whose presynaptic inputs have not been resolved + cell = self._cells[i]["cell"] + if showlog: + logging.info("Resolving inputs for %s %d", self, i) + self._cells[i]["connections"] = {} + + # select cells from each population to connect to this cell + for pop in self._pre_connections: + pre_cells = self.connect_pop_to_cell(pop, i) + if showlog: + logging.info(" connected %d cells from %s", len(pre_cells), pop) + assert pre_cells is not None + self._cells[i]["connections"][pop] = pre_cells + self._cells[i]["input_resolved"] = True + + # recursively resolve inputs in connected populations + if depth > 1: + for pop in self.pre_connections: + pop.resolve_inputs(depth - 1, showlog=showlog) + + def connect_pop_to_cell(self, pop, cell_index): + """ Connect cells in a presynaptic population to the cell in this + population at *cell_index*, and return the presynaptic indexes of cells + that were connected. + + This method is responsible for choosing pairs of cells to be connected + by synapses, and may be overridden in subclasses. + + The default implementation calls `self.connection_stats()` to determine + the number and selection criteria of presynaptic cells. + """ + cell_rec = self._cells[cell_index] + cell = cell_rec["cell"] + size, dist = self.connection_stats(pop, cell_rec) + # Select SGCs from distribution, create, and connect to this cell + # todo: select sgcs with similar spont. rate? + pre_cells = pop.select(size=size, create=False, **dist) + for j in pre_cells: + pre_cell = pop.get_cell(j) + # use default settings for connecting these. + pre_cell.connect(cell, type=self._synapsetype) + return pre_cells + + def connection_stats(self, pop, cell_rec): + """ The population *pop* is being connected to the cell described in + *cell_rec*. + + This method is responsible for deciding the distributions of presynaptic + cell properties for any given postsynaptic cell (for example, a cell + with cf=10kHz might receive SGC input from 10 cells selected from a + normal distribution centered at 10kHz). + + The default implementation of this method uses the 'convergence' and + 'convergence_range' values from the data tables to specify a lognormal + distribution of presynaptic cells around the postsynaptic cell's CF. + + This method must return a tuple (size, dist) with the following values: + + * size: integer giving the number of cells that should be selected from + the presynaptic population and connected to the postsynaptic cell. + * dist: dictionary of {property_name: distribution} pairs that describe + how cells should be selected from the presynaptic population. See + keyword arguments to `select()` for more information on the content + of this dictionary. + """ + cf = cell_rec["cf"] + + # Convergence distributions (how many presynaptic + # cells to connect) + try: + n_connections = data.get( + "convergence", + species=self.species, + pre_type=pop.type, + post_type=self.type, + ) + except KeyError: + raise TypeError( + "Cannot connect population %s to %s; no convergence specified in data table." + % (pop, self) + ) + + if isinstance(n_connections, tuple): + size_dist = scipy.stats.norm(loc=n_connections[0], scale=n_connections[1]) + size = max(0, size_dist.rvs()) + else: + size = n_connections + size = int(size) # must be an integer at this point + + # Convergence ranges -- over what range of CFs should we + # select presynaptic cells. + try: + input_range = data.get( + "convergence_range", + species=self.species, + pre_type=pop.type, + post_type=self.type, + ) + except KeyError: + raise TypeError( + "Cannot connect population %s to %s; no convergence range specified in data table." + % (pop, self) + ) + + dist = {"cf": scipy.stats.lognorm(input_range, scale=cf)} + # print(cf, input_range, dist) + return size, dist + + def get_sgcsr_array(self, freqs, species="mouse"): + """ + Create an array of length (freqs) (number of SGCs) + Each entry is a value indicating the SR group, according to some proportion + 2 = high, 1 = medium, 0 = low + + Parameters + ---------- + freqs : nunpy array + + species : str (default: 'mouse') + name of the species for the map. + + Returns: + numpy array + An array matched to freqs, with SR's indicated numerically + """ + assert species == "mouse" # only mice so far. + nhs = np.random.random_sample( + freqs.shape[0] + ) # uniform random distribution across frequency + sr_array = np.zeros_like(freqs) # build array - initially all low sponts + sr_array[ + np.argwhere(nhs < 0.53) + ] = 2 # high spont (53% estimated from Taberner and Liberman, 2005) + sr_array[ + np.argwhere((nhs >= 0.53) & (nhs < 0.77)) + ] = 1 # medium spont, about 24% (1-20 sp/sec) + # the rest have SR value of 0, corresponding to the low-spont group + return sr_array + + def select_sgcsr_inputs(self, sr_array, weights): + """ + Subsample the arrays above to create a distribution for cells that might only get + a subset of inputs (for example, only msr and lsr fibers) + + Parameters + ---------- + sr_array : numpy array + the SR array to draw the samples from + + weights : 3 element list + Weights for [lsr, msr, hsr] ANFs. Proportions will be computed + from these weights (e.g., [1,1,1] is uniform for all fibers) + weights of [1,1,0] means all hsr fibers will be masked + + Returns: + numpy array of "dist" + Values of 0 are sgcs masked from input, 1 are ok + """ + assert len(weights) == 3 + + dist = np.zeros_like(sr_array) # boolean array, all values + norm_wt = 3.0 * np.array( + weights / np.sum(weights) + ) # fraction from within each group + for i in range(len(weights)): + dx = np.where(sr_array == i)[0] + ind = np.random.choice(len(dx), int(norm_wt[i] * len(dx))) + dist[dx[ind]] = 1 + return dist + + def _get_cf_array(self, species): + """Return the array of CF values that should be used when instantiating + this population. + + Commonly used by subclasses durin initialization. + """ + size = data.get( + "populations", species=species, cell_type=self.type, field="n_cells" + ) + fmin = data.get( + "populations", species=species, cell_type=self.type, field="cf_min" + ) + fmax = data.get( + "populations", species=species, cell_type=self.type, field="cf_max" + ) + s = (fmax / fmin) ** (1.0 / size) + freqs = fmin * s ** np.arange(size) + # print('frqs #: ', len(freqs)) + # Cut off at 40kHz because the auditory nerve model only goes that far :( + freqs = freqs[freqs <= 40e3] + + return freqs + + def select(self, size, create=False, **kwds): + """ Return a list of indexes for cells matching the selection criteria. + + The *size* argument specifies the number of cells to return. + + If *create* is True, then any selected cells that are virtual will be + instantiated. + + Each keyword argument must be the name of a field in self.cells. Values + may be: + + * A distribution (see scipy.stats), in which case the distribution + influences the selection of cells + * An array giving the probability to assign to each cell in the + population + * A number, in which case the cell(s) with the closest match + are returned. If this is used, it overrides all other criteria except + where they evaluate to 0. + + If multiple distributions are provided, then the product of the survival + functions of all distributions determines the probability of selecting + each cell. + """ + if len(kwds) == 0: + raise TypeError("Must specify at least one selection criteria") + + full_dist = np.ones(len(self._cells)) + nearest = None + nearest_field = None + for field, dist in kwds.items(): + if np.isscalar(dist): + if nearest is not None: + raise Exception( + "May not specify multiple single-valued selection criteria." + ) + nearest = dist + nearest_field = field + elif isinstance(dist, scipy.stats.distributions.rv_frozen): + vals = self._cells[field] + dens = np.diff(vals) + dens = np.concatenate([dens[:1], dens]) + pdf = dist.pdf(vals) * dens + full_dist *= pdf / pdf.sum() + elif isinstance(dist, np.ndarray): + full_dist *= dist + else: + raise TypeError("Distributed criteria must be array or rv_frozen.") + + # Select cells nearest to the requested value, but only pick from + # cells with nonzero probability. + if nearest is not None: + cells = [] + mask = full_dist == 0 + err = np.abs(self._cells[nearest_field] - nearest).astype(float) + for i in range(size): + err[mask] = np.inf + cell = np.argmin(err) + mask[cell] = True + cells.append(cell) + + # Select cells randomly from the specified combined probability + # distribution + else: + cells = [] + full_dist /= full_dist.sum() + vals = np.random.uniform(size=size) + vals.sort() + cumulative = np.cumsum(full_dist) + for val in vals: + u = np.argwhere(cumulative >= val) + if len(u) > 0: + cell = u[0, 0] + cells.append(cell) + if create: + self.create_cells(cells) + + return cells + + def get_cell(self, i, create=True): + """ Return the cell at index i. If the cell is virtual, then it will + be instantiated first unless *create* is False. + """ + if create and self._cells[i]["cell"] == 0: + self.create_cells([i]) + return self._cells[i]["cell"] + + def get_cell_index(self, cell): + """Return the index of *cell*. + """ + return self._cell_indexes[cell] + + def create_cells(self, cell_inds): + """ Instantiate each cell in *cell_inds*, which is a list of indexes into + self.cells. + """ + for i in cell_inds: + if self._cells[i]["cell"] != 0: + continue + cell = self.create_cell(self._cells[i]) + self._cells[i]["cell"] = cell + self._cell_indexes[cell] = i + + def create_cell(self, cell_rec): + """ Return a single new cell to be used in this population. The + *cell_rec* argument is the row from self.cells that describes the cell + to be created. + + Subclasses must reimplement this method. + """ + raise NotImplementedError() + + def __str__(self): + return "" % ( + type(self).__name__, + (self._cells["cell"] != 0).sum(), + len(self._cells), + ) + + def __getstate__(self): + """Return a picklable copy of self.__dict__. + + Note that we remove references to the actual cells in order to allow pickling. + """ + state = self.__dict__.copy() + state["_cells"] = state["_cells"].copy() + + for cell in state["_cells"]: + if cell["cell"] != 0: + cell["cell"] = cell[ + "cell" + ].type # replace neuron object with just the cell type + cell = str(cell) # make a string + return state diff --git a/cnmodel/populations/pyramidal.py b/cnmodel/populations/pyramidal.py new file mode 100644 index 0000000..a8306cc --- /dev/null +++ b/cnmodel/populations/pyramidal.py @@ -0,0 +1,26 @@ +import scipy.stats +import numpy as np + +from .population import Population +from .. import cells + + +class Pyramidal(Population): + type = "pyramidal" + + def __init__( + self, species="mouse", **kwds + ): # ***** NOTE Species - no direct data for mouse (uses RAT data) + # Note that `cf` is the mean value used when selecting SGCs to connect; + # it is NOT the measured CF of the cell (although it should be close). + freqs = self._get_cf_array(species) + fields = [("cf", float)] + super(Pyramidal, self).__init__(species, len(freqs), fields=fields, **kwds) + self._cells["cf"] = freqs + + def create_cell(self, cell_rec): + """ Return a single new cell to be used in this population. The + *cell_rec* argument is the row from self.cells that describes the cell + to be created. + """ + return cells.Pyramidal.create(species=self.species, **self._cell_args) diff --git a/cnmodel/populations/sgc.py b/cnmodel/populations/sgc.py new file mode 100644 index 0000000..f526acd --- /dev/null +++ b/cnmodel/populations/sgc.py @@ -0,0 +1,87 @@ +import logging +import numpy as np +import pyqtgraph.multiprocess as mp + +from .population import Population +from .. import cells + + +class SGC(Population): + """A population of spiral ganglion cells. + + The cell distribution is uniform from 2kHz to 64kHz, evenly divided between + spontaneous rate groups. + """ + + type = "sgc" + + def __init__(self, species="mouse", model="dummy", **kwds): + # Completely fabricated cell distribution: uniform from 2kHz to 40kHz, + # evenly divided between SR groups. We only go up to 40kHz because the + # auditory periphery model does not support >40kHz. + freqs = self._get_cf_array(species) + fields = [("cf", float), ("sr", int)] # 0=low sr, 1=mid sr, 2=high sr + super(SGC, self).__init__( + species, len(freqs), fields=fields, model=model, **kwds + ) + self._cells["cf"] = freqs + # old version: + # evenly distribute SR groups + # self._cells['sr'] = np.arange(len(freqs)) % 3 + # new version: + # draw from distributions matching approximate SR distribution + sr_vals = self.get_sgcsr_array(freqs, species="mouse") + self._cells["sr"] = sr_vals + + def set_seed(self, seed): + self.next_seed = seed + + def create_cell(self, cell_rec): + """ Return a single new cell to be used in this population. The + *cell_rec* argument is the row from self.cells that describes the cell + to be created. + """ + return cells.SGC.create( + species=self.species, + cf=cell_rec["cf"], + sr=cell_rec["sr"], + **self._cell_args + ) + + def connect_pop_to_cell(self, pop, index): + # SGC does not support any inputs + assert len(self.connections) == 0 + + def set_sound_stim(self, stim, parallel=False): + """Set a sound stimulus to generate spike trains for all (real) cells + in this population. + """ + real = self.real_cells() + logging.info("Assigning spike trains to %d SGC cells..", len(real)) + if not parallel: + for i, ind in enumerate(real): + # logging.info("Assigning spike train to SGC %d (%d/%d)", ind, i, len(real)) + cell = self.get_cell(ind) + cell.set_sound_stim(stim, self.next_seed) + self.next_seed += 1 + + else: + seeds = range(self.next_seed, self.next_seed + len(real)) + self.next_seed = seeds[-1] + 1 + tasks = zip(seeds, real) + trains = [None] * len(tasks) + # generate spike trains in parallel + with mp.Parallelize( + enumerate(tasks), + trains=trains, + progressDialog="Generating SGC spike trains..", + ) as tasker: + for i, x in tasker: + seed, ind = x + cell = self.get_cell(ind) + train = cell.generate_spiketrain(stim, seed) + tasker.trains[i] = train + # collected all trains; now assign to cells + for i, ind in enumerate(real): + cell = self.get_cell(ind) + cell.set_spiketrain(trains[i]) diff --git a/cnmodel/populations/tstellate.py b/cnmodel/populations/tstellate.py new file mode 100644 index 0000000..a6ffcfc --- /dev/null +++ b/cnmodel/populations/tstellate.py @@ -0,0 +1,43 @@ +import scipy.stats +import numpy as np + +from .population import Population +from .. import cells + + +class TStellate(Population): + type = "tstellate" + + def __init__(self, species="mouse", **kwds): + # Note that `cf` is the mean value used when selecting SGCs to connect; + # it is NOT the measured CF of the cell (although it should be close). + freqs = self._get_cf_array(species) + fields = [("cf", float), ("input_sr", list)] + super(TStellate, self).__init__(species, len(freqs), fields=fields, **kwds) + self._cells["cf"] = freqs + self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))] + + def create_cell(self, cell_rec): + """ Return a single new cell to be used in this population. The + *cell_rec* argument is the row from self.cells that describes the cell + to be created. + """ + return cells.TStellate.create(species=self.species, **self._cell_args) + + def connection_stats(self, pop, cell_rec): + """ The population *pop* is being connected to the cell described in + *cell_rec*. Return the number of presynaptic cells that should be + connected and a dictionary of distributions used to select cells + from *pop*. + """ + size, dist = Population.connection_stats(self, pop, cell_rec) + + from .. import populations + + if isinstance(pop, populations.SGC): + # Select SGC inputs from a all SR groups + sr_vals = pop.cells["sr"] + # print('SRs for TS: ', np.bincount(sr_vals)/sr_vals.shape[0], np.unique(sr_vals)) + dist["sr"] = (sr_vals < 3).astype(float) + + return size, dist diff --git a/cnmodel/populations/tuberculoventral.py b/cnmodel/populations/tuberculoventral.py new file mode 100644 index 0000000..c9cc753 --- /dev/null +++ b/cnmodel/populations/tuberculoventral.py @@ -0,0 +1,46 @@ +import scipy.stats +import numpy as np + +from .population import Population +from .. import cells + + +class Tuberculoventral(Population): + type = "tuberculoventral" + + def __init__(self, species="mouse", **kwds): + # Note that `cf` is the mean value used when selecting SGCs to connect; + # it is NOT the measured CF of the cell (although it should be close). + freqs = self._get_cf_array(species) + fields = [("cf", float), ("input_sr", list)] + super(Tuberculoventral, self).__init__( + species, len(freqs), fields=fields, **kwds + ) + self._cells["cf"] = freqs + self._cells["input_sr"] = [np.tile([1.0, 1.0, 1.0], len(freqs))] + + def create_cell(self, cell_rec): + """ Return a single new cell to be used in this population. The + *cell_rec* argument is the row from self.cells that describes the cell + to be created. + """ + return cells.Tuberculoventral.create(species=self.species, **self._cell_args) + + def connection_stats(self, pop, cell_rec): + """ The population *pop* is being connected to the cell described in + *cell_rec*. Return the number of presynaptic cells that should be + connected and a dictionary of distributions used to select cells + from *pop*. + """ + size, dist = Population.connection_stats(self, pop, cell_rec) + + from .. import populations + + if isinstance(pop, populations.SGC): + # only select SGC inputs from low- and medium SR. See: + # Spectral Integration by Type II Interneurons in Dorsal Cochlear Nucleus + # George A. Spirou, Kevin A. Davis, Israel Nelken, Eric D. Young + # Journal of Neurophysiology Aug 1999, 82 (2) 648-663; + dist["sr"] = (pop.cells["sr"] < 2).astype(float) + + return size, dist diff --git a/cnmodel/protocols/__init__.py b/cnmodel/protocols/__init__.py new file mode 100644 index 0000000..695ce44 --- /dev/null +++ b/cnmodel/protocols/__init__.py @@ -0,0 +1,7 @@ +from .iv_curve import IVCurve +from .vc_curve import VCCurve +from .cc import CurrentClamp +from .synapse_test import SynapseTest +from .simple_synapse_test import SimpleSynapseTest +from .protocol import Protocol +from .population_test import PopulationTest diff --git a/cnmodel/protocols/cc.py b/cnmodel/protocols/cc.py new file mode 100644 index 0000000..84f9463 --- /dev/null +++ b/cnmodel/protocols/cc.py @@ -0,0 +1,91 @@ +from neuron import h +import numpy as np +import scipy +import scipy.integrate +import scipy.stats + +try: + import pyqtgraph as pg + + HAVE_PG = True +except ImportError: + HAVE_PG = False + +from ..util.stim import make_pulse +from .protocol import Protocol + + +class CurrentClamp(Protocol): + def __init__(self): + super(CurrentClamp, self).__init__() + + def run(self, cell, cmd, temp=22, dt=0.025): + """ + Run a single current-clamp recording on *section*. + + Parameters + ---------- + cell : Cell + The Cell instance to test. IClamp will be attached to + cell.soma(0.5). + cmd : array + Array of current command values + temp : + temperature of simulation (22) + dt : + timestep of simulation (0.025) + """ + self.reset() + self.cell = cell + self.current_cmd = cmd + self.dt = dt + self.temp = temp + + tend = len(cmd) * dt + + # Configure IClamp + istim = h.iStim(0.5, sec=cell.soma) + istim.delay = 0 + istim.dur = 1e9 # these actually do not matter... + istim.iMax = 0.0 + i_stim = h.Vector(cmd) + i_stim.play(istim._ref_i, h.dt, 0) + + # Connect recording vectors + self["vm"] = cell.soma(0.5)._ref_v + self["i_inj"] = istim._ref_i + self["time"] = h._ref_t + + # GO + h.dt = dt + h.celsius = temp + h.tstop = tend + cell.initialize() + h.frecord_init() + while h.t < h.tstop: + h.fadvance() + + def show(self): + """ + Plot results from run() + """ + if not HAVE_PG: + raise Exception("Requires pyqtgraph") + + # + # Generate figure with subplots + # + app = pg.mkQApp() + win = pg.GraphicsWindow() + win.resize(1000, 800) + Vplot = win.addPlot(labels={"left": "Vm (mV)", "bottom": "Time (ms)"}) + win.nextRow() + Iplot = win.addPlot(labels={"left": "Iinj (nA)", "bottom": "Time (ms)"}) + + win.ci.layout.setRowStretchFactor(0, 10) + win.ci.layout.setRowStretchFactor(1, 5) + + Vplot.plot(self["time"], self["vm"]) + Iplot.plot(self["time"], self["i_inj"]) + + self.win = win diff --git a/cnmodel/protocols/democlamp.py b/cnmodel/protocols/democlamp.py new file mode 100644 index 0000000..44ee512 --- /dev/null +++ b/cnmodel/protocols/democlamp.py @@ -0,0 +1,185 @@ +import os +import os.path +from neuron import h +import pylibrary.Utility as U +from ..util import PlotHelpers as PH +import numpy as np +import scipy +import scipy.integrate +import scipy.stats + +try: + import pyqtgraph as pg + + HAVE_PG = True +except ImportError: + HAVE_PG = False + +from ..util.stim import make_pulse + +# import matplotlib as MP # must call first... before pylag/pyplot or backends +# MP.use('Qt4Agg') + +# import matplotlib.gridspec as GS +# import mpl_toolkits.axes_grid1.inset_locator as INSETS +# import mpl_toolkits.axes_grid1.anchored_artists as ANCHOR + +# stdFont = 'Arial' +# import matplotlib.pyplot as pylab +# pylab.rcParams['interactive'] = False +# pylab.rcParams['mathtext.default'] = 'sf' +## next setting allows pdf font to be readable in Adobe Illustrator +# pylab.rcParams['pdf.fonttype'] = 42 +# pylab.rcParams['figure.facecolor'] = 'white' + + +def run_democlamp(cell, dend, vsteps=[-60, -70, -60], tsteps=[10, 50, 100]): + """ + Does some stuff. + + """ + f1 = pylab.figure(1) + gs = GS.GridSpec(2, 2, width_ratios=[1, 1], height_ratios=[1, 1]) + + # note numbering for insets goes from 1 (upper right) to 4 (lower right) + # counterclockwise + pA = f1.add_subplot(gs[0]) + pAi = INSETS.inset_axes(pA, width="66%", height="40%", loc=2) + pB = f1.add_subplot(gs[1]) + pBi = INSETS.inset_axes(pB, width="66%", height="40%", loc=4) + pC = f1.add_subplot(gs[2]) + pCi = INSETS.inset_axes(pC, width="66%", height="40%", loc=2) + pD = f1.add_subplot(gs[3]) + pDi = INSETS.inset_axes(pD, width="66%", height="40%", loc=1) + # h.topology() + + Ld = 0.5 + Ld2 = 1.0 + + VClamp = h.SEClamp(0.5, cell) + VClamp.dur1 = tsteps[0] + VClamp.amp1 = vsteps[0] + VClamp.dur2 = tsteps[1] + VClamp.amp2 = vsteps[1] + VClamp.dur3 = tsteps[2] + VClamp.amp3 = vsteps[2] + Rs0 = 10.0 + VClamp.rs = Rs0 + compensation = [0.0, 70.0, 95.0] + cms = [cell.cm * (100.0 - c) / 100.0 for c in compensation] + + vrec = h.iStim(Ld, sec=dend[0]) + vrec.delay = 0 + vrec.dur = 1e9 # these actually do not matter... + vrec.iMax = 0.0 + vrec2 = h.iStim(Ld2, sec=dend[0]) + vrec2.delay = 0 + vrec2.dur = 1e9 # these actually do not matter... + vrec2.iMax = 0.0 + + stim = {} + stim["NP"] = 1 + stim["Sfreq"] = 20 # stimulus frequency + stim["delay"] = tsteps[0] + stim["dur"] = tsteps[1] + stim["amp"] = vsteps[1] + stim["PT"] = 0.0 + stim["hold"] = vsteps[0] + # (secmd, maxt, tstims) = make_pulse(stim) + tend = 79.5 + linetype = ["-", "-", "-"] + linethick = [0.5, 0.75, 1.25] + linecolor = [[0.66, 0.66, 0.66], [0.4, 0.4, 0.3], "k"] + n = 0 + vcmds = [-70, -20] + vplots = [(pA, pAi, pC, pCi), (pB, pBi, pD, pDi)] + for m, VX in enumerate(vcmds): + stim["amp"] = VX + pl = vplots[m] + print(m, VX) + (secmd, maxt, tstims) = make_pulse(stim) + for n, rsc in enumerate(compensation): + vec = {} + for var in ["VCmd", "i_inj", "time", "Vsoma", "Vdend", "Vdend2", "VCmdR"]: + vec[var] = h.Vector() + VClamp.rs = Rs0 * (100.0 - rsc) / 100.0 + cell.cm = cms[n] + # print VClamp.rs, cell.cm, VClamp.rs*cell.cm + vec["VCmd"] = h.Vector(secmd) + vec["Vsoma"].record(cell(0.5)._ref_v, sec=cell) + vec["Vdend"].record(dend[0](Ld)._ref_v, sec=dend[0]) + vec["time"].record(h._ref_t) + vec["i_inj"].record(VClamp._ref_i, sec=cell) + + vec["VCmdR"].record(VClamp._ref_vc, sec=cell) + VClamp.amp2 = VX + # vec['VCmd'].play(VClamp.amp2, h.dt, 0, sec=cell) + + h.tstop = tend + h.init() + h.finitialize(-60) + h.run() + vc = np.asarray(vec["Vsoma"]) + tc = np.asarray(vec["time"]) + + # now plot the data, raw and as insets + for k in [0, 1]: + pl[k].plot( + vec["time"], + vec["i_inj"], + color=linecolor[n], + linestyle=linetype[n], + linewidth=linethick[n], + ) + yl = pl[k].get_ylim() + if k == 0: + pass + # pl[k].set_ylim([1.5*yl[0], -1.5*yl[1]]) + else: + pass + + for k in [2, 3]: + pl[k].plot( + vec["time"], + vec["Vsoma"], + color=linecolor[n], + linestyle=linetype[n], + linewidth=linethick[n], + ) + pl[k].plot( + vec["time"], + vec["VCmdR"], + color=linecolor[n], + linestyle="--", + linewidth=1, + dashes=(1, 1), + ) + pl[k].plot( + vec["time"], + vec["Vdend"], + color=linecolor[n], + linestyle=linetype[n], + linewidth=linethick[n], + dashes=(3, 3), + ) + if VX < vsteps[0]: + pl[k].set_ylim([-72, -40]) + else: + pl[k].set_ylim([-62, VX + 30]) + + ptx = 10.8 + pBi.set_xlim([9.8, ptx]) + pAi.set_xlim([9.8, ptx]) + PH.setX(pAi, pCi) + PH.setX(pBi, pDi) + pD.set_ylim([-65, 10]) + # PH.setY(pC, pCi) # match Y limits + PH.cleanAxes([pA, pAi, pB, pBi, pC, pCi, pD, pDi]) + PH.formatTicks([pA, pB, pC, pD], axis="x", fmt="%d") + PH.formatTicks([pC, pD], axis="y", fmt="%d") + PH.calbar(pAi, [ptx - 1, 0, 0.2, 2.0]) + PH.calbar(pCi, [ptx - 1, -50.0, 0.2, 10]) + PH.calbar(pBi, [ptx - 1, 0, 0.2, 10]) + PH.calbar(pDi, [ptx - 1, -50.0, 0.2, 20]) + pylab.draw() + pylab.show() diff --git a/cnmodel/protocols/iv_curve.py b/cnmodel/protocols/iv_curve.py new file mode 100644 index 0000000..c13fc14 --- /dev/null +++ b/cnmodel/protocols/iv_curve.py @@ -0,0 +1,612 @@ +from __future__ import print_function +from neuron import h +import numpy as np +import scipy +import scipy.integrate +import scipy.stats +import scipy.optimize + +try: + import pyqtgraph as pg + + HAVE_PG = True +except ImportError: + HAVE_PG = False + +from ..util.stim import make_pulse +from ..util import fitting +from ..util import custom_init +from .protocol import Protocol + + +class IVCurve(Protocol): + def __init__(self): + super(IVCurve, self).__init__() + + def reset(self): + super(IVCurve, self).reset() + self.voltage_traces = [] + self.durs = None # durations of current steps + self.current_cmd = None # Current command levels + self.current_traces = [] + self.time_values = None + self.dt = None + self.initdelay = 0.0 + + def run( + self, + ivrange, + cell, + durs=None, + sites=None, + reppulse=None, + temp=22, + dt=0.025, + initdelay=0.0, + ): + """ + Run a current-clamp I/V curve on *cell*. + + Parameters + ---------- + ivrange : dict of list of tuples + Each item in the list is (min, max, step) describing a range of + levels to test. Range values are inclusive, so the max value may + appear in the test values. Using multiple ranges allows finer + measurements in some ranges. + For example:: + + {'pulse': [(-1., 0., 1.), (-0.1, 0., 0.02)], 'prepulse': [(-0.5, 0, 0.1)]} + + Optional keys include 'pulsedur' : the duration of the pulse, in ms + 'prepulsecur: the duration of the prepulse, in ms + The prepulse or the pulse can have a single value if the other is ranged. + cell : Cell + The Cell instance to test. + durs : tuple + durations of (pre, pulse, post) regions of the command + sites : list + Sections to add recording electrodes + reppulse : + stimulate with pulse train + temp : + temperature of simulation (32) + dt : + timestep of simulation (0.025) + + """ + self.reset() + self.cell = cell + self.initdelay = initdelay + self.dt = dt + self.temp = temp + # Calculate current pulse levels + icur = [] + precur = [0.0] + self.pre_current_cmd = [] + npresteps = 0 + if isinstance(ivrange["pulse"], tuple): + icmd = [ivrange["pulse"]] # convert to a list with tuple(s) embedded + else: + icmd = ivrange["pulse"] # was already a list with multiple tuples + for c in icmd: # unpack current levels for the main pulse + try: + (imin, imax, istep) = c # unpack a tuple... or list + except: + raise TypeError( + "run_iv arguments must be a dict with tuples {'pulse': (imin, imax, istep), 'prepulse': ...}" + ) + nstep = np.floor((imax - imin) / istep) + 1 + icur.extend(imin + istep * np.arange(nstep)) # build the list + self.current_cmd = np.array(sorted(icur)) + nsteps = self.current_cmd.shape[0] + # Configure IClamp + if durs is None: + durs = [10.0, 100.0, 50.0] # set default durs + + if "prepulse" in ivrange.keys(): + if isinstance(ivrange["prepulse"], tuple): + icmd = [ivrange["prepulse"]] # convert to a list with tuple(s) embedded + else: + icmd = ivrange["prepulse"] # was already a list with multiple tuples + precur = [] + for c in icmd: + try: + (imin, imax, istep) = c # unpack a tuple... or list + except: + raise TypeError( + "run_iv arguments must be a dict with tuples {'pulse': (imin, imax, istep), 'prepulse': ...}" + ) + nstep = np.floor((imax - imin) / istep) + 1 + precur.extend(imin + istep * np.arange(nstep)) # build the list + self.pre_current_cmd = np.array(sorted(precur)) + npresteps = self.pre_current_cmd.shape[0] + durs.insert(1, 50.0) + + self.durs = durs + # set up stimulation with a pulse train + if reppulse is not None: + stim = { + "NP": 10, + "Sfreq": 50.0, + "delay": 10.0, + "dur": 2, + "amp": 1.0, + "PT": 0.0, + "dt": self.dt, + } + elif "prepulse" in ivrange.keys(): + stim = { + "NP": 2, + "delay": durs[0], + "predur": durs[1], + "dur": durs[2], + "amp": 1.0, + "preamp": 0.0, + "dt": self.dt, + } + self.p_start = durs[0] + durs[1] + self.p_end = self.p_start + durs[2] + self.p_dur = durs[2] + else: + stim = { + "NP": 1, + "delay": durs[0], + "dur": durs[1], + "amp": 1.0, + "dt": self.dt, + } + self.p_start = durs[0] + self.p_end = self.p_start + durs[1] + self.p_dur = durs[1] + # print stim + # print('p_: ', self.p_start, self.p_end, self.p_dur) + istim = h.iStim(0.5, sec=cell.soma) + istim.delay = 5.0 + istim.dur = 1e9 # these actually do not matter... + istim.iMax = 0.0 + self.tend = np.sum(durs) # maxt + len(iextend)*stim['dt'] + + self.cell = cell + for i in range(nsteps): + # Generate current command for this level + stim["amp"] = self.current_cmd[i] + if npresteps > 0: + for j in range(npresteps): + stim["preamp"] = self.pre_current_cmd[j] + self.run_one(istim, stim, initflag=(i == 0 and j == 0)) + else: + self.run_one(istim, stim, initflag=(i == 0)) + + def run_one(self, istim, stim, initflag=True): + """ + Perform one run in current-clamp for the selected cell + and add the data to the traces + + Parameters + ---------- + istim : Stimulus electrode instance + stim : waveform information + initflag : boolean (default: True) + If true, force initialziation of the cell and computation of + point Rin, tau and Vm + """ + (secmd, maxt, tstims) = make_pulse(stim) + # print('maxt, dt*lencmd: ', maxt, len(secmd)*self.dt)# secmd = np.append(secmd, [0.]) + # print('stim: ', stim, self.tend) + + # connect current command vector + playvector = h.Vector(secmd) + playvector.play(istim._ref_i, h.dt, 0, sec=self.cell.soma) + + # Connect recording vectors + self["v_soma"] = self.cell.soma(0.5)._ref_v + # self['q10'] = self.cell.soma(0.5).ihpyr_adj._ref_q10 + # self['ih_ntau'] = self.cell.soma(0.5).ihpyr_adj._ref_kh_n_tau + self["i_inj"] = istim._ref_i + self["time"] = h._ref_t + + # h('secondorder=0') # direct call fails; let hoc do the work + h.celsius = self.cell.status["temperature"] + self.cell.cell_initialize() + h.dt = self.dt + custom_init(v_init=self.cell.vm0) + + h.t = 0.0 + h.tstop = self.tend + while h.t < h.tstop: + h.fadvance() + + self.voltage_traces.append(self["v_soma"]) + self.current_traces.append(self["i_inj"]) + self.time_values = np.array(self["time"] - self.initdelay) + # self.mon_q10 = np.array(self['q10']) + # self.mon_ih_ntau = np.array(self['ih_ntau']) + + def peak_vm(self, window=0.5): + """ + Parameters + ---------- + window : float (default: 0.5) + fraction of trace to look at to find peak value + + Returns + ------- + peak membrane voltage for each trace. + + """ + Vm = self.voltage_traces + Icmd = self.current_cmd + steps = len(Icmd) + peakStart = int(self.p_start / self.dt) + peakStop = int( + peakStart + (self.p_dur * window) / self.dt + ) # peak can be in first half + Vpeak = [] + for i in range(steps): + if Icmd[i] > 0: + Vpeak.append(Vm[i][peakStart:peakStop].max()) + else: + Vpeak.append(Vm[i][peakStart:peakStop].min()) + return np.array(Vpeak) + + def steady_vm(self, window=0.1): + """ + Parameters + ---------- + window: (float) default: 0.1 + fraction of window to use for steady-state measurement, taken + immediately before the end of the step + + Returns + ------- + steady-state membrane voltage for each trace. + + """ + Vm = self.voltage_traces + steps = len(Vm) + steadyStop = int((self.p_end) / self.dt) + steadyStart = int( + steadyStop - (self.p_end * window) / self.dt + ) # measure last 10% of trace + Vsteady = [Vm[i][steadyStart:steadyStop].mean() for i in range(steps)] + return np.array(Vsteady) + + def spike_times(self, threshold=None): + """ + Return an array of spike times for each trace. + + Parameters + ---------- + threshold: float (default: None) + Optional threshold at which to detect spikes. By + default, this queries cell.spike_threshold. + + Returns + ------- + list of spike times. + + """ + if threshold is None: + threshold = self.cell.spike_threshold + + Vm = self.voltage_traces + steps = len(Vm) + spikes = [] + for i in range(steps): + # dvdt = np.diff(Vm[i]) / self.dt + # mask = (dvdt > 40).astype(int) + mask = (Vm[i] > threshold).astype(int) + indexes = np.argwhere(np.diff(mask) == 1)[:, 0] + 1 + times = indexes.astype(float) * self.dt + spikes.append(times) + return spikes + + def spike_filter(self, spikes, window=(0.0, np.inf)): + """Filter the spikes to only those occurring in a defined window. + + Required to compute input resistance in traces with no spikes during + the stimulus, because some traces will have anodal break spikes. + + Parameters + ---------- + spikes : list + the list of spike trains returned from the spike_times method + window : (start, stop) + the window over which to look for spikes (in msec: default is + the entire trace). + + Returns + ------- + the spikes in a list + + """ + filteredspikes = [] + for i in range(len(spikes)): + winspikes = [] # spikes is arranged by current; so this is for one level + for j in range(len(spikes[i])): + if spikes[i][j] >= window[0] and spikes[i][j] <= window[1]: + winspikes.append(spikes[i][j]) + filteredspikes.append(winspikes) # now build filtered spike list + return filteredspikes + + def rest_vm(self): + """ + Parameters + ---------- + None + + Returns + ------- + The mean resting membrane potential. + """ + d = int(self.durs[0] / self.dt) + rvm = np.array( + [ + np.array(self.voltage_traces[i][d // 2 : d]).mean() + for i in range(len(self.voltage_traces)) + ] + ).mean() + return rvm + + def input_resistance_tau(self, vmin=-10.0, imax=0, return_fits=False): + """ + Estimate resting input resistance and time constant. + + Parameters + ---------- + vmin : float + minimum voltage to use in computation relative to resting + imax : float + maximum current to use in computation. + return_eval : bool + If True, return lmfit.ModelFit instances for the subthreshold trace + fits as well. + + Returns + ------- + dict : + Dict containing: + + * 'slope' and 'intercept' keys giving linear + regression for subthreshold traces near rest + * 'tau' giving the average first-exponential fit time constant + * 'fits' giving a record array of exponential fit data to subthreshold + traces. + + Analyzes only traces hyperpolarizing pulse traces near rest, with no + spikes. + + """ + Vss = self.steady_vm() + vmin += self.rest_vm() + Icmd = self.current_cmd + rawspikes = self.spike_times() + spikes = self.spike_filter(rawspikes, window=[self.p_start, self.p_end]) + steps = len(Icmd) + + nSpikes = np.array([len(s) for s in spikes]) + # find traces with Icmd < 0, Vm > -70, and no spikes. + vmask = Vss >= vmin + imask = Icmd <= imax + smask = nSpikes > 0 + mask = vmask & imask & ~smask + if mask.sum() < 2: + print( + "WARNING: Not enough traces to do linear regression in " + "IVCurve.input_resistance_tau()." + ) + print( + "{0:<15s}: {1:s}".format( + "vss", ", ".join(["{:.2f}".format(v) for v in Vss]) + ) + ) + print( + "{0:<15s}: {1:s}".format( + "Icmd", ", ".join(["{:.2f}".format(i) for i in Icmd]) + ) + ) + print("{0:<15s}: {1:s}".format("vmask", repr(vmask.astype(int)))) + print("{0:<15s}: {1:s} ".format("imask", repr(imask.astype(int)))) + print("{0:<15s}: {1:s}".format("spikemask", repr(smask.astype(int)))) + raise Exception( + "Not enough traces to do linear regression (see info above)." + ) + + # Use these to measure input resistance by linear regression. + reg = scipy.stats.linregress(Icmd[mask], Vss[mask]) + (slope, intercept, r, p, stderr) = reg + + # also measure the tau in the same traces: + pulse_start = int(self.p_start / self.dt) + pulse_stop = int((self.p_end) / self.dt) + + fits = [] + fit_inds = [] + tx = self.time_values[pulse_start:pulse_stop].copy() + for i, m in enumerate(mask): + if not m or (self.rest_vm() - Vss[i]) <= 1: + continue + trace = self.voltage_traces[i][pulse_start:pulse_stop] + + # find first minimum in the trace + min_ind = np.argmin(trace) + min_val = trace[min_ind] + min_diff = trace[0] - min_val + tau_est = min_ind * self.dt * (1.0 - 1.0 / np.e) + # print ('minind: ', min_ind, tau_est) + fit = fitting.Exp1().fit( + trace[:min_ind], + method="nelder", + x=tx[:min_ind], + xoffset=(tx[0], "fixed"), + yoffset=(min_val, -120.0, -10.0), + amp=(min_diff, 0.0, 50.0), + tau=(tau_est, 0.5, 50.0), + ) + + # find first maximum in the trace (following with first minimum) + max_ind = np.argmax(trace[min_ind:]) + min_ind + max_val = trace[max_ind] + max_diff = min_val - max_val + tau2_est = max_ind * self.dt * (1.0 - 1.0 / np.e) + amp1_est = fit.params["amp"].value + tau1_est = fit.params["tau"].value + amp2_est = fit.params["yoffset"] - max_val + # print('tau1, tau2est: ', tau1_est, tau2_est) + # fit up to first maximum with double exponential, using prior + # fit as seed. + fit = fitting.Exp2().fit( + trace[:max_ind], + method="nelder", + x=tx[:max_ind], + xoffset=(tx[0], "fixed"), + yoffset=(max_val, -120.0, -10.0), + amp1=(amp1_est, 0.0, 200.0), + tau1=(tau1_est, 0.5, 50.0), + amp2=(amp2_est, -200.0, -0.5), + tau_ratio=(tau2_est / tau1_est, 2.0, 50.0), + tau2="tau_ratio * tau1", + ) + + fits.append(fit) + fit_inds.append(i) + # convert fits to record array + # print len(fits) # fits[0].params + if len(fits) > 0: + key_order = sorted( + fits[0].params + ) # to ensure that unit tests remain stable + dtype = [(k, float) for k in key_order] + [("index", int)] + fit_data = np.empty(len(fits), dtype=dtype) + for i, fit in enumerate(fits): + for k, v in fit.params.items(): + fit_data[i][k] = v.value + fit_data[i]["index"] = fit_inds[i] + + if "tau" in fit_data.dtype.fields: + tau = fit_data["tau"].mean() + else: + tau = fit_data["tau1"].mean() + else: + slope = 0.0 + intercept = 0.0 + tau = 0.0 + fit_data = [] + ret = {"slope": slope, "intercept": intercept, "tau": tau, "fits": fit_data} + + if return_fits: + return ret, fits + else: + return ret + + def show(self, cell=None, rmponly=False): + """ + Plot results from run_iv() + + Parameters + ---------- + cell : cell object (default: None) + + """ + if not HAVE_PG: + raise Exception("Requires pyqtgraph") + + # + # Generate figure with subplots + # + app = pg.mkQApp() + win = pg.GraphicsWindow( + "%s %s (%s)" + % (cell.status["name"], cell.status["modelType"], cell.status["species"]) + ) + self.win = win + win.resize(1000, 800) + Vplot = win.addPlot(labels={"left": "Vm (mV)", "bottom": "Time (ms)"}) + rightGrid = win.addLayout(rowspan=2) + win.nextRow() + Iplot = win.addPlot(labels={"left": "Iinj (nA)", "bottom": "Time (ms)"}) + + IVplot = rightGrid.addPlot(labels={"left": "Vm (mV)", "bottom": "Icmd (nA)"}) + IVplot.showGrid(x=True, y=True) + rightGrid.nextRow() + spikePlot = rightGrid.addPlot( + labels={"left": "Iinj (nA)", "bottom": "Spike times (ms)"} + ) + rightGrid.nextRow() + FIplot = rightGrid.addPlot( + labels={"left": "Spike count", "bottom": "Iinj (nA)"} + ) + + win.ci.layout.setRowStretchFactor(0, 10) + win.ci.layout.setRowStretchFactor(1, 5) + + # + # Plot simulation and analysis results + # + Vm = self.voltage_traces + Iinj = self.current_traces + Icmd = self.current_cmd + t = self.time_values + steps = len(Icmd) + + # plot I, V traces + colors = [(i, steps * 3.0 / 2.0) for i in range(steps)] + for i in range(steps): + Vplot.plot(t, Vm[i], pen=colors[i]) + Iplot.plot(t, Iinj[i], pen=colors[i]) + + if rmponly: + return + # I/V relationships + IVplot.plot( + Icmd, + self.peak_vm(), + symbol="o", + symbolBrush=(50, 150, 50, 255), + symbolSize=4.0, + ) + IVplot.plot(Icmd, self.steady_vm(), symbol="s", symbolSize=4.0) + + # F/I relationship and raster plot + spikes = self.spike_times() + for i, times in enumerate(spikes): + spikePlot.plot( + x=times, + y=[Icmd[i]] * len(times), + pen=None, + symbol="d", + symbolBrush=colors[i], + symbolSize=4.0, + ) + FIplot.plot(x=Icmd, y=[len(s) for s in spikes], symbol="o", symbolSize=4.0) + + # Print Rm, Vrest + rmtau, fits = self.input_resistance_tau(return_fits=True) + s = rmtau["slope"] + i = rmtau["intercept"] + # tau1 = rmtau['fits']['tau1'].mean() + # tau2 = rmtau['fits']['tau2'].mean() + # print ("\nMembrane resistance (chord): {0:0.1f} MOhm Taum1: {1:0.2f} Taum2: {2:0.2f}".format(s, tau1, tau2)) + + # Plot linear subthreshold I/V relationship + ivals = np.array([Icmd.min(), Icmd.max()]) + vvals = s * ivals + i + line = pg.QtGui.QGraphicsLineItem(ivals[0], vvals[0], ivals[1], vvals[1]) + line.setPen(pg.mkPen(255, 0, 0, 70)) + line.setZValue(-10) + IVplot.addItem(line, ignoreBounds=True) + + # plot exponential fits + for fit in fits: + t = np.linspace(self.p_start, self.p_end, 1000) + y = fit.eval(x=t) + Vplot.plot( + t, y, pen={"color": (100, 100, 0), "style": pg.QtCore.Qt.DashLine} + ) + + # plot initial guess + # y = fit.eval(x=t, **fit.init_params.valuesdict()) + # Vplot.plot(t, y, pen={'color': 'b', 'style': pg.QtCore.Qt.DashLine}) + + print("Resting membrane potential: %0.1f mV\n" % self.rest_vm()) diff --git a/cnmodel/protocols/population_test.py b/cnmodel/protocols/population_test.py new file mode 100644 index 0000000..4d3dd59 --- /dev/null +++ b/cnmodel/protocols/population_test.py @@ -0,0 +1,150 @@ +from __future__ import print_function +import numpy as np +import pyqtgraph as pg + +from neuron import h + +import cnmodel.util as util +from .protocol import Protocol +from ..util import custom_init +from cnmodel.util import sound + + +class PopulationTest(Protocol): + def reset(self): + super(PopulationTest, self).reset() + + def run( + self, pops, cf=16e3, temp=34.0, dt=0.025, stim="sound", simulator="cochlea" + ): + """ + 1. Connect pop1 => pop2 + 2. Instantiate a single cell in pop2 + 3. Automatically generate presynaptic cells and synapses from pop1 + 4. Stimulate presynaptic cells and record postsynaptically + """ + + pre_pop, post_pop = pops + pre_pop.connect(post_pop) + self.pre_pop = pre_pop + self.post_pop = post_pop + + # start with one cell, selected from the user-selected population, that has + # a cf close to the center CF + post_cell_ind = post_pop.select(1, cf=cf, create=True)[0] + post_cell = post_pop.get_cell(post_cell_ind) + post_pop.resolve_inputs(depth=1) + post_sec = post_cell.soma + self.post_cell_ind = post_cell_ind + self.post_cell = post_cell + + pre_cell_inds = post_pop.cell_connections(post_cell_ind)[pre_pop] + pre_cells = [pre_pop.get_cell(i) for i in pre_cell_inds] + pre_secs = [cell.soma for cell in pre_cells] + self.pre_cells = pre_cells + self.pre_cell_inds = pre_cell_inds + self.stim = sound.TonePip( + rate=100e3, + duration=0.1, + f0=cf, + dbspl=60, + ramp_duration=2.5e-3, + pip_duration=0.05, + pip_start=[0.02], + ) + + ## + ## voltage clamp the target cell + ## + # clampV = 40.0 + # vccontrol = h.VClamp(0.5, sec=post_cell.soma) + # vccontrol.dur[0] = 10.0 + # vccontrol.amp[0] = clampV + # vccontrol.dur[1] = 100.0 + # vccontrol.amp[1] = clampV + # vccontrol.dur[2] = 20.0 + # vccontrol.amp[2] = clampV + + # + # set up stimulation of the presynaptic cells + # + self.stim_params = [] + self.istim = [] + for i, pre_cell in enumerate(pre_cells): + if stim == "sound": + pre_cell.set_sound_stim(self.stim, seed=i, simulator=simulator) + amp = 0.0 + else: + amp = 3.0 + istim = h.iStim(0.5, sec=pre_cell.soma) + stim = {} + stim["NP"] = 10 + stim["Sfreq"] = 100.0 # stimulus frequency + stim["delay"] = 10.0 + stim["dur"] = 0.5 + stim["amp"] = amp + stim["PT"] = 0.0 + stim["dt"] = dt + (secmd, maxt, tstims) = util.make_pulse(stim) + self.stim_params.append(stim) + + # istim current pulse train + i_stim_vec = h.Vector(secmd) + i_stim_vec.play(istim._ref_i, dt, 0, pre_cell.soma(0.5)) + self.istim.append((istim, i_stim_vec)) + self["istim"] = istim._ref_i + + # record presynaptic Vm + self["v_pre%d" % i] = pre_cell.soma(0.5)._ref_v + + self["t"] = h._ref_t + self["v_post"] = post_cell.soma(0.5)._ref_v + + # + # Run simulation + # + h.dt = dt + self.dt = dt + h.celsius = post_cell.status["temperature"] + self.temp = h.celsius + post_cell.cell_initialize() # proper initialization. + h.dt = self.dt + custom_init(v_init=post_cell.vm0) + h.t = 0.0 + h.tstop = 200.0 + while h.t < h.tstop: + h.fadvance() + + def show(self): + print( + "Connected %d presynaptic cells to 1 postsynaptic cell." + % len(self.pre_cell_inds) + ) + print("Postsynaptic CF = %0.2f" % self.post_pop.cells[self.post_cell_ind]["cf"]) + print("Presynaptic CF = %s" % self.pre_pop.cells[self.pre_cell_inds]["cf"]) + + self.win = pg.GraphicsWindow() + self.win.resize(1000, 1000) + + cmd_plot = self.win.addPlot(title="Stim") + try: + cmd_plot.plot(self["t"], self["istim"]) + except: + pass + + self.win.nextRow() + pre_plot = self.win.addPlot(title=self.pre_cells[0].type + " Vm") + for i in range(len(self.pre_cells)): + pre_plot.plot( + self["t"], + self["v_pre%d" % i], + pen=pg.mkPen( + pg.intColor(i, len(self.pre_cells)), + hues=len(self.pre_cells), + width=1.0, + ), + ) + + self.win.nextRow() + post_plot = self.win.addPlot(title="Post Cell: %s" % self.post_cell.type) + post_plot.plot(self["t"], self["v_post"]) diff --git a/cnmodel/protocols/protocol.py b/cnmodel/protocols/protocol.py new file mode 100644 index 0000000..cc138df --- /dev/null +++ b/cnmodel/protocols/protocol.py @@ -0,0 +1,43 @@ +from neuron import h +import numpy as np +from ..util import random_seed, custom_init + + +class Protocol(object): + """ + Base class providing common tools for running, analyzing, and displaying + simulations. + """ + + def __init__(self): + self.reset() + + def reset(self): + self._vectors = {} + + def run(self, seed=None): + """ + Run this protocol. + + Subclasses should extend this method. + """ + if seed is not None: + random_seed.set_seed(seed) + self.reset() + + def __setitem__(self, name, variable): + """ + Record *variable* during the next run. + """ + vec = h.Vector() + self._vectors[name] = vec + vec.record(variable) + + def __getitem__(self, name): + """ + Return a np array for previously recorded data given *name*. + """ + return np.array(self._vectors[name]) + + def custom_init(self, vinit=-60.0): + return custom_init(vinit) diff --git a/cnmodel/protocols/simple_synapse_test.py b/cnmodel/protocols/simple_synapse_test.py new file mode 100644 index 0000000..ee2469b --- /dev/null +++ b/cnmodel/protocols/simple_synapse_test.py @@ -0,0 +1,111 @@ +# -*- encoding: utf-8 -*- +from neuron import h +import pyqtgraph as pg + +import cnmodel.util as util +from .protocol import Protocol +from .. import cells + + +class SimpleSynapseTest(Protocol): + def reset(self): + super(SimpleSynapseTest, self).reset() + + def run( + self, + pre_sec, + post_sec, + temp=34.0, + dt=0.025, + vclamp=-65.0, + iterations=1, + tstop=200.0, + stim_params=None, + **kwds + ): + """ + """ + Protocol.run(self, **kwds) + + pre_cell = cells.cell_from_section(pre_sec) + post_cell = cells.cell_from_section(post_sec) + synapse = pre_cell.connect(post_cell, type="simple") + + self.synapse = synapse + self.pre_sec = synapse.terminal.section + self.post_sec = synapse.psd.section + self.pre_cell = pre_cell + self.post_cell = post_cell + + # + # voltage clamp the target cell + # + vccontrol = h.VClamp(0.5, sec=post_cell.soma) + vccontrol.dur[0] = tstop + vccontrol.amp[0] = vclamp + # vccontrol.dur[1] = 100.0 + # vccontrol.amp[1] = clampV + # vccontrol.dur[2] = 20.0 + # vccontrol.amp[2] = clampV + + # + # set up stimulation of the presynaptic axon/terminal + # + + istim = h.iStim(0.5, sec=pre_cell.soma) + stim = { + "NP": 10, + "Sfreq": 100.0, + "delay": 10.0, + "dur": 0.5, + "amp": 10.0, + "PT": 0.0, + "dt": dt, + } + if stim_params is not None: + stim.update(stim_params) + (secmd, maxt, tstims) = util.make_pulse(stim) + self.stim = stim + + if tstop is None: + tstop = len(secmd) * dt + + istim.delay = 0 + istim.dur = 1e9 # these actually do not matter... + istim.iMax = 0.0 + + # istim current pulse train + i_stim_vec = h.Vector(secmd) + i_stim_vec.play(istim._ref_i, dt, 0) + + # + # Run simulation + # + h.tstop = tstop # duration of a run + h.celsius = temp + h.dt = dt + self.temp = temp + self.dt = dt + for nrep in list(range(iterations)): # could do multiple runs.... + self.reset() + self["v_pre"] = pre_cell.soma(0.5)._ref_v + self["t"] = h._ref_t + self["v_soma"] = post_cell.soma(0.5)._ref_v + self["i_soma"] = vccontrol._ref_i + util.custom_init() + h.run() + + def show(self): + self.win = pg.GraphicsWindow() + self.win.resize(800, 800) + t = self["t"] + + p1 = self.win.addPlot(title=self.pre_cell.status["name"]) + p1.setLabels(left="V pre (mV)", bottom="Time (ms)") + p1.plot(t, self["v_pre"]) + + self.win.nextRow() + p2 = self.win.addPlot(title=self.post_cell.status["name"]) + p2.plot(t[1:], self["i_soma"][1:], pen=pg.mkPen("w", width=2)) + p2.setLabels(left="I post (nA)", bottom="Time (ms)") + p2.setXLink(p1) diff --git a/cnmodel/protocols/synapse_test.py b/cnmodel/protocols/synapse_test.py new file mode 100644 index 0000000..8897991 --- /dev/null +++ b/cnmodel/protocols/synapse_test.py @@ -0,0 +1,779 @@ +from __future__ import print_function +from collections import OrderedDict +from scipy import interpolate +import numpy as np +import pyqtgraph as pg +from neuron import h +import cnmodel.util as util +from .protocol import Protocol +from .. import cells +from ..synapses import GluPSD, GlyPSD, Exp2PSD +from ..util.find_point import find_crossing +import timeit + + +class SynapseTest(Protocol): + def reset(self): + super(SynapseTest, self).reset() + + def run( + self, + pre_sec, + post_sec, + n_synapses, + temp=34.0, + dt=0.025, + vclamp=40.0, + iterations=1, + tstop=240.0, + stim_params=None, + synapsetype="multisite", + **kwds + ): + """ + Basic synapse test. Connects sections of two cells with *n_synapses*. + The cells are allowed to negotiate the details of the connecting + synapse. The presynaptic soma is then driven with a pulse train + followed by a recovery pulse of varying delay. + + *stim_params* is an optional dictionary with keys 'NP', 'Sfreq', 'delay', + 'dur', 'amp'. + + Analyses: + + * Distribution of PSG amplitude, kinetics, and latency + * Synaptic depression / facilitation and recovery timecourses + """ + Protocol.run(self, **kwds) + + pre_cell = cells.cell_from_section(pre_sec) + post_cell = cells.cell_from_section(post_sec) + synapses = [] + for i in range(n_synapses): + synapses.append(pre_cell.connect(post_cell, type=synapsetype)) + if len(synapses) == 0: + raise ValueError("No synapses created for this cell combination!") + self.synapses = synapses + self.pre_sec = synapses[0].terminal.section + self.post_sec = synapses[0].psd.section + self.pre_cell = pre_cell + self.post_cell = post_cell + self.plots = {} # store plot information here + # + # voltage clamp the target cell + # + clampV = vclamp + vccontrol = h.VClamp(0.5, sec=post_cell.soma) + vccontrol.dur[0] = 10.0 + vccontrol.amp[0] = clampV + vccontrol.dur[1] = 100.0 + vccontrol.amp[1] = clampV + vccontrol.dur[2] = 20.0 + vccontrol.amp[2] = clampV + + # + # set up stimulation of the presynaptic axon/terminal + # + + istim = h.iStim(0.5, sec=pre_cell.soma) + stim = { + "NP": 10, + "Sfreq": 100.0, + "delay": 10.0, + "dur": 0.5, + "amp": 10.0, + "PT": 0.0, + "dt": dt, + } + if stim_params is not None: + stim.update(stim_params) + (secmd, maxt, tstims) = util.make_pulse(stim) + self.stim = stim + + if tstop is None: + tstop = len(secmd) * dt + + istim.delay = 0 + istim.dur = 1e9 # these actually do not matter... + istim.iMax = 0.0 + + # istim current pulse train + i_stim_vec = h.Vector(secmd) + i_stim_vec.play(istim._ref_i, dt, 0) + + # create hoc vectors for each parameter we wish to monitor and display + synapse = synapses[0] + self.all_psd = [] + if isinstance(synapses[0].psd, GlyPSD) or isinstance(synapses[0].psd, GluPSD): + for syn in synapses: + self.all_psd.extend(syn.psd.all_psd) + elif isinstance(synapses[0].psd, Exp2PSD): + for syn in synapses: + self.all_psd.append(syn) + + # for i, cleft in enumerate(synapse.psd.clefts): + # self['cleft_xmtr%d' % i] = cleft._ref_CXmtr + # self['cleft_pre%d' % i] = cleft._ref_pre + # self['cleft_xv%d' % i] = cleft._ref_XV + # self['cleft_xc%d' % i] = cleft._ref_XC + # self['cleft_xu%d' % i] = cleft._ref_XU + + # + # Run simulation + # + h.tstop = tstop # duration of a run + h.celsius = temp + h.dt = dt + self.temp = temp + self.dt = dt + self.isoma = [] + self.currents = {"ampa": [], "nmda": []} + self.all_releases = [] + self.all_release_events = [] + start_time = timeit.default_timer() + for nrep in list(range(iterations)): # could do multiple runs.... + self.reset() + self["v_pre"] = pre_cell.soma(0.5)._ref_v + self["t"] = h._ref_t + self["v_soma"] = pre_cell.soma(0.5)._ref_v + if not isinstance(synapse.psd, Exp2PSD): + self["relsite_xmtr"] = synapse.terminal.relsite._ref_XMTR[0] + + if isinstance(synapse.psd, GluPSD): + # make a synapse monitor for each release zone + self.all_nmda = [] + self.all_ampa = [] + for syn in synapses: + # collect all PSDs across all synapses + self.all_ampa.extend(syn.psd.ampa_psd) + self.all_nmda.extend(syn.psd.nmda_psd) + + # Record current through all PSDs individually + syn.psd.record("i", "g", "Open") + + # for k,p in enumerate(self.all_nmda): + # self['iNMDA%03d' % k] = p._ref_i + # self['opNMDA%03d' % k] = p._ref_Open + # for k,p in enumerate(self.all_ampa): + # self['iAMPA%03d' % k] = p._ref_i + # self['opAMPA%03d' % k] = p._ref_Open + + elif isinstance(synapse.psd, GlyPSD): + # Record current through all PSDs individually + for k, p in enumerate(self.all_psd): + self["iGLY%03d" % k] = p._ref_i + self["opGLY%03d" % k] = p._ref_Open + + psd = self.all_psd + if synapse.psd.psdType == "glyslow": + nstate = 7 + self["C0"] = psd[0]._ref_C0 + self["C1"] = psd[0]._ref_C1 + self["C2"] = psd[0]._ref_C2 + self["O1"] = psd[0]._ref_O1 + self["O2"] = psd[0]._ref_O2 + self["D1"] = psd[0]._ref_D1 + # self['D3'] = psd[0]._ref_D3 + # self['O1'] = psd[0]._ref_O1 + elif synapse.psd.psdType == "glyfast": + nstate = 7 + self["C0"] = psd[0]._ref_C0 + self["C1"] = psd[0]._ref_C1 + self["C2"] = psd[0]._ref_C2 + self["C3"] = psd[0]._ref_C3 + self["O1"] = psd[0]._ref_O1 + self["O2"] = psd[0]._ref_O2 + elif isinstance(synapse.psd, Exp2PSD): + self["iPSD"] = self.all_psd[0].psd.syn._ref_i + + if not isinstance(synapse.psd, Exp2PSD): + for i, s in enumerate(synapses): + s.terminal.relsite.rseed = util.random_seed.current_seed() + nrep + util.custom_init() + h.run() + + # add up psd current across all runs + if isinstance(synapse.psd, GluPSD): + iampa = np.zeros_like(synapse.psd.get_vector("ampa", "i")) + inmda = iampa.copy() + for syn in self.synapses: + for i in range(syn.psd.n_psd): + iampa += syn.psd.get_vector("ampa", "i", i) + inmda += syn.psd.get_vector("nmda", "i", i) + isoma = iampa + inmda + self.currents["ampa"].append(iampa) + self.currents["nmda"].append(inmda) + elif isinstance(synapse.psd, GlyPSD): + isoma = np.zeros_like(self["iGLY000"]) + for k in range(len(self.all_psd)): + isoma += self["iGLY%03d" % k] + elif isinstance(synapse.psd, Exp2PSD): + isoma = self["iPSD"] + self.isoma.append(isoma) + self.all_releases.append(self.release_timings()) + self.all_release_events.append(self.release_events()) + + elapsed = timeit.default_timer() - start_time + print("Elapsed time for %d Repetions: %f" % (iterations, elapsed)) + + def release_events(self, syn_no=0): + """ + Analyze results and return a dict of values related to terminal release + probability: + + n_zones: Array containing the number of release zones for each + synapse. + + n_requests: Array containing number of release requests for each + synapse. Note for multi-zone synapses, a single + presynaptic spike results in one release request _per_ + zone. + + n_releases: Array containing actual number of releases for each + synapse. + + tot_requests: The total number of release requests across all + release zones. + + tot_releases: The total number of releases. + + release_p: Release probability computed as + tot_releases / tot_requests + + + """ + synapse = self.synapses[syn_no] + + ret = { + "n_zones": [0], + "n_spikes": [0], + "n_requests": [0], + "n_releases": [0], + "tot_requests": 0, + "tot_releases": 0, + "release_p": 0.0, + } + # + # Count spikes and releases for each terminal + # + if not isinstance(self.synapses[0].psd, Exp2PSD): + ret["n_zones"] = np.array([syn.terminal.n_rzones for syn in self.synapses]) + ret["n_spikes"] = np.array( + [syn.terminal.relsite.nRequests for syn in self.synapses] + ) + ret["n_requests"] = ret["n_spikes"] * ret["n_zones"] + ret["n_releases"] = np.array( + [syn.terminal.relsite.nReleases for syn in self.synapses] + ) + + # + # Compute release probability + # + # total number of release requests + ret["tot_requests"] = ret["n_requests"].sum() + # total number of actual release events + ret["tot_releases"] = ret["n_releases"].sum() + + if ret["tot_requests"] > 0: + ret["release_p"] = float(ret["tot_releases"]) / ret["tot_requests"] + else: + ret["release_p"] = np.nan + + return ret + + def release_timings(self): + """ + Return a list of arrays (one array per synapse) describing the timing + and latency of release events. + """ + data = [] + if isinstance(self.synapses[0].psd, Exp2PSD): + return data + + for j in range(0, len(self.synapses)): + relsite = self.synapses[j].terminal.relsite + nev = int(relsite.ev_index) + ev = np.empty(nev, dtype=[("time", float), ("latency", float)]) + ev["latency"] = np.array(relsite.EventLatencies)[:nev] + ev["time"] = np.array(relsite.EventTime)[:nev] + data.append(ev) + return data + + def open_probability(self): + """ + Analyze results and return a dict of values related to psd open + probability: + + nmda: (imax, opmax) + ampa: (imax, opmax) + gly: (imax, opmax) + """ + synapse = self.synapses[0] + if isinstance(synapse.psd, GluPSD) and len(synapse.psd.nmda_psd) > 0: + # find a psd with ampa and nmda currents + nmImax = [] + amImax = [] + nmOmax = [] + amOmax = [] + # self.win.nextRow() + for syn in self.synapses: + for i in range(syn.psd.n_psd): + nm = np.abs(syn.psd.get_vector("nmda", "i", i)).max() + am = np.abs(syn.psd.get_vector("ampa", "i", i)).max() + opnm = np.abs(syn.psd.get_vector("nmda", "Open", i)).max() + opam = np.abs(syn.psd.get_vector("ampa", "Open", i)).max() + if nm > 1e-6 or am > 1e-6: # only count releases, not failures + nmImax.append(nm) + amImax.append(am) + nmOmax.append(opnm) + amOmax.append(opam) + break + if nmImax != 0: + break + + return { + "nmda": OrderedDict( + [ + ("Imax", np.mean(nmImax)), + ("Omax", np.mean(nmOmax)), + # ('OmaxMax', np.max(nmOmax)), # was used for testing... + # ('OmaxMin', np.min(nmOmax)) + ] + ), + "ampa": OrderedDict( + [ + ("Imax", np.mean(amImax)), + ("Omax", np.mean(amOmax)), + # ('OmaxMax', np.max(amOmax)), + # ('OmaxMin', np.min(amOmax)) + ] + ), + } + + elif isinstance(synapse.psd, GlyPSD) and len(synapse.psd.all_psd) > 0: + # find a psd with ampa and nmda currents + glyImax = 0 + glyOmax = 0 + for i in range(len(self.all_psd)): + imax = np.abs(self["iGLY%03d" % i]).max() + omax = np.abs(self["opGLY%03d" % i]).max() + + return {"gly": (glyImax, glyOmax)} + + elif isinstance(synapse.psd, Exp2PSD): + return {"Exp2PSD": (0.0, 0.0)} + + def analyze_events(self): + events = [] + for run in range(len(self.isoma)): + events.append(self.analyze_events_in_run(runno=run)) + return events + + def analyze_events_in_run(self, runno=0): + """ + Analyze postsynaptic events for peak, latency, and shape. + + Todo: + - This currently analyzes cumulative currents; might be better to + analyze individual PSD currents + - Measure decay time constant, rate of facilitation/depression, + recovery. + + """ + stim = self.stim + ipi = 1000.0 / stim["Sfreq"] # convert from Hz (seconds) to msec. + t_extend = 0.25 # allow response detection into the next frame + extend_pts = int(t_extend / self.dt) + + pscpts = ( + int(ipi / self.dt) + extend_pts + ) # number of samples to analyze for each psc + ipsc = np.zeros((stim["NP"], pscpts)) # storage for psc currents + tpsc = np.arange( + 0, ipi + t_extend, self.dt + ) # time values corresponding to *ipsc* + + # mpl.figure(num=220, facecolor='w') + # gpsc = mpl.subplot2grid((5, 2), (0, 0), rowspan=2, colspan=2) + psc_20_lat = np.zeros((stim["NP"], 1)) # latency to 20% of rising amplitude + psc_80_lat = np.zeros((stim["NP"], 1)) # latency to 80% of rising amplitude + psc_hw = np.zeros((stim["NP"], 1)) # width at half-height + psc_rt = np.zeros((stim["NP"], 1)) # 20-80 rise time + tp = np.zeros((stim["NP"], 1)) # pulse time relative to first pulse + events = np.zeros( + stim["NP"], + dtype=[ + ("20% latency", float), + ("80% latency", float), + ("half width", float), + ("half left", float), + ("half right", float), + ("rise time", float), + ("pulse time", float), + ("peak", float), + ("peak index", int), + ], + ) + events[:] = np.nan + + minLat = 0.0 # minimum latency for an event, in ms + minStart = int( + minLat / self.dt + ) # first index relative to pulse to search for psc peak + + for i in range(stim["NP"]): + tstart = stim["delay"] + i * ipi # pulse start time + events["pulse time"][i] = tstart + istart = int(tstart / self.dt) # pulse start index + tp[i] = tstart - stim["delay"] + iend = istart + pscpts + # print 'istart: %d iend: %d, len(isoma): %d\n' % (istart, iend, len(self.isoma[runno])) + ipsc[i, :] = np.abs(self.isoma[runno][istart:iend]) + psc_pk = minStart + np.argmax( + ipsc[i, minStart : -(extend_pts + 1)] + ) # position of the peak + + # print 'i, pscpk, ipsc[i,pscpk]: ', i, psc_pk, ipsc[i, psc_pk] + # print 'minLat: %f ipi+t_extend: %f, hdt: %f' % ((minLat, ipi+t_extend, self.dt)) + if psc_pk == minStart: + continue + pkval = ipsc[i, psc_pk] + events["peak"][i] = pkval + events["peak index"][i] = psc_pk + + # Find 20% and 80% crossing points to the left of the PSC peak + pscmin = ipsc[i, :psc_pk].min() + + lat20 = ( + find_crossing( + ipsc[i], + start=psc_pk, + direction=-1, + threshold=(pscmin + (pkval - pscmin) * 0.2), + ) + * self.dt + ) + lat80 = ( + find_crossing( + ipsc[i], + start=psc_pk, + direction=-1, + threshold=(pscmin + (pkval - pscmin) * 0.8), + ) + * self.dt + ) + events["20% latency"][i] = lat20 + events["80% latency"][i] = lat80 + + # Find 50% crossing points on either side of the PSC peak + psc_50l = ( + find_crossing( + ipsc[i], + start=psc_pk, + direction=-1, + threshold=(pscmin + (pkval - pscmin) * 0.5), + ) + * self.dt + ) + psc_50r = ( + find_crossing( + ipsc[i], + start=psc_pk, + direction=1, + threshold=(pscmin + (pkval - pscmin) * 0.5), + ) + * self.dt + ) + + events["half left"] = psc_50l + events["half right"] = psc_50r + + if not np.isnan(lat20) and not np.isnan(lat80): + events["rise time"][i] = lat80 - lat20 + else: + events["rise time"][i] = np.nan + if not np.isnan(psc_50r) and not np.isnan(psc_50l): + events["half width"][i] = float(psc_50r) - float(psc_50l) + # gpsc.plot(psc_50l, pkval * 0.5, 'k+') + # gpsc.plot(psc_50r, pkval * 0.5, 'k+') + # gpsc.plot(tpsc, ipsc[i, :].T) + else: + events["half width"][i] = np.nan + + return events + + def hide(self): + if hasattr(self, "win"): + self.win.hide() + + def show_result( + self, releasePlot=True, probabilityPlot=True, glyPlot=False, plotFocus="EPSC" + ): + synapse = self.synapses[0] + + # + # Print parameters related to release probability + # + events = self.release_events() + ns = len(self.synapses) + for i in range(ns): + if i < len(events["n_spikes"]) and events["n_spikes"][i] > 0: + v = ( + i, + events["n_spikes"][i], + events["n_zones"][i], + events["n_releases"][i], + ) + print("Synapse %d: spikes: %d zones: %d releases: %d" % v) + print("") + print("Total release requests: %d" % events["tot_requests"]) + print("Total release events: %d" % events["tot_releases"]) + print("Release probability: %8.3f" % events["release_p"]) + if not isinstance(synapse.psd, Exp2PSD): + prel_final = synapse.terminal.relsite.Dn * synapse.terminal.relsite.Fn + print("Final release probability (Dn * Fn): %8.3f" % prel_final) + + # + # Compute NMDA / AMPA open probability + # + print("") + oprob = self.open_probability() + if "gly" in oprob: + glyImax, glyOPmax = oprob["gly"] + print("Max GLYR Open Prob: %f" % (glyOPmax,)) + print("Max GLYR I: %f" % (glyImax,)) + elif "ampa" in oprob or "nmda" in oprob: + nmImax, nmOPmax = oprob["nmda"].values() + amImax, amOPmax = oprob["ampa"].values() + print("Max NMDAR Open Prob: %f AMPA Open Prob: %f" % (nmOPmax, amOPmax)) + print("Max NMDAR I: %f AMPA I: %f" % (nmImax, amImax)) + if nmImax + amImax != 0.0: + print(" N/(N+A): %f\n" % (nmImax / (nmImax + amImax))) + else: + print(" (no NMDA/AMPA current; release might have failed)") + + self.win = pg.GraphicsWindow() + self.win.resize(1000, 1000) + self.win.show() + + # + # Plot pre/postsynaptic currents + # + t = self["t"] + + p1 = self.win.addPlot(title=self.pre_cell.status["name"]) + p1.setLabels(left="V pre (mV)", bottom="Time (ms)") + p1.plot(t, self["v_pre"]) + self.plots["VPre"] = p1 + + if plotFocus == "EPSC": + self.win.nextRow() + p2 = self.win.addPlot(title=self.post_cell.status["name"]) + for i, isoma in enumerate(self.isoma): + p2.plot(t, isoma, pen=(i, len(self.isoma))) + p2.plot(t, np.mean(self.isoma, axis=0), pen=pg.mkPen("w", width=2)) + p2.setLabels(left="Total PSD current (nA)", bottom="Time (ms)") + p2.setXLink(p1) + self.plots["EPSC"] = p2 + else: + # todo: resurrect this? + g2 = mpl.subplot2grid((6, 1), (1, 0), rowspan=1) + g2.plot(t, self.isoma, color="cyan") + g3 = mpl.subplot2grid((6, 1), (2, 0)) + g3.plot(t, self["v_pre"], color="blue") + g3.plot(t, self["v_soma"], color="red") + g4 = mpl.subplot2grid((6, 1), (3, 0)) + p4 = g4.plot(t, self["relsite_xmtr"]) # glutamate + g4.axes.set_ylabel("relsite_xmtr") + g5 = mpl.subplot2grid((6, 1), (4, 0)) + for k, p in enumerate(synapse.psd.all_psd): + if p.hname().find("NMDA", 0, 6) >= 0: + g5.plot(t, self["isyn%03d" % k]) # current through nmdar + g5.axes.set_ylabel("inmda") + g6 = mpl.subplot2grid((6, 1), (5, 0)) + for k, p in enumerate(synapse.psd.all_psd): + if p.hname().find("NMDA", 0, 6) < 0: + g6.plot(t, self["isyn%03d" % k]) # glutamate + g6.axes.set_ylabel("iAMPA") + + # + # Analyze the individual events. + # EPSCs get rise time, latency, half-width, and decay tau estimates. + # + events = self.analyze_events() + + eventno = 0 + self.win.nextRow() + p3 = self.win.addPlot( + labels={"left": "20%-80% Latency (ms)", "bottom": "Pulse Time (ms)"} + ) + p3.plot( + events[eventno]["pulse time"], + events[eventno]["20% latency"], + pen=None, + symbol="o", + ) + p3.plot( + events[eventno]["pulse time"], + events[eventno]["80% latency"], + pen=None, + symbol="t", + ) + p3.setXLink(p1) + self.plots["latency2080"] = p3 + + self.win.nextRow() + p4 = self.win.addPlot( + labels={"left": "Half Width (ms)", "bottom": "Pulse Time (ms)"} + ) + p4.plot( + events[eventno]["pulse time"], + events[eventno]["half width"], + pen=None, + symbol="o", + ) + p4.setXLink(p1) + self.plots["halfwidth"] = p4 + + self.win.nextRow() + p5 = self.win.addPlot( + labels={"left": "Rise Time (ms)", "bottom": "Pulse Time (ms)"} + ) + p5.plot( + events[eventno]["pulse time"], + events[eventno]["rise time"], + pen=None, + symbol="o", + ) + p5.setXLink(p1) + self.plots["RT"] = p5 + + # + # Print average values from events + # + nst = range(self.stim["NP"]) + analysisWindow = [nst[0:2], nst[-5:]] + + RT_mean2080_early = np.nanmean(events[eventno]["rise time"][analysisWindow[0]]) + RT_mean2080_late = np.nanmean(events[eventno]["rise time"][analysisWindow[1]]) + Lat_mean20_early = np.nanmean(events[eventno]["20% latency"][analysisWindow[0]]) + Lat_mean20_late = np.nanmean(events[eventno]["20% latency"][analysisWindow[1]]) + HW_mean_early = np.nanmean(events[eventno]["half width"][analysisWindow[0]]) + HW_mean_late = np.nanmean(events[eventno]["half width"][analysisWindow[1]]) + print("\n--------------") + print("Means:") + print("--------------") + # print RT_mean2080_early + # print Lat_mean20_early + # print HW_mean_early + print( + "Early: RT {0:7.3f} ms Lat {1:7.3f} ms HW {2:7.3f} ms".format( + RT_mean2080_early, Lat_mean20_early, HW_mean_early + ) + ) + print( + "Late : RT {0:7.3f} ms Lat {1:7.3f} ms HW {2:7.3f} ms".format( + RT_mean2080_late, Lat_mean20_late, HW_mean_late + ) + ) + RT_std2080_early = np.nanstd(events[eventno]["rise time"][analysisWindow[0]]) + RT_std2080_late = np.nanstd(events[eventno]["rise time"][analysisWindow[1]]) + Lat_std20_early = np.nanstd(events[eventno]["20% latency"][analysisWindow[0]]) + Lat_std20_late = np.nanstd(events[eventno]["20% latency"][analysisWindow[1]]) + HW_std_early = np.nanstd(events[eventno]["half width"][analysisWindow[0]]) + HW_std_late = np.nanstd(events[eventno]["half width"][analysisWindow[1]]) + print("\n--------------") + print("Standard Deviations:") + print("--------------") + print( + "Early: RT {0:7.3f} ms Lat {1:7.3f} ms HW {2:7.3f} ms".format( + RT_std2080_early, Lat_std20_early, HW_std_early + ) + ) + print( + "Late : RT {0:7.3f} ms Lat {1:7.3f} ms HW {2:7.3f} ms".format( + RT_std2080_late, Lat_std20_late, HW_std_late + ) + ) + print("-----------------") + + # + # Plot release event distributions over time + # + if releasePlot: + self.win.nextRow() + p6 = self.win.addPlot( + labels={"left": "Release latency", "bottom": "Time (ms)"} + ) + p6.setXLink(p1) + self.plots["latency"] = p6 + p7 = self.win.addPlot( + labels={"left": "Release latency", "bottom": "Num. Releases"} + ) + p7.setYLink(p6) + self.plots["latency_distribution"] = p7 + self.win.ci.layout.setColumnFixedWidth(1, 200) + if not isinstance(self.synapses[0].psd, Exp2PSD): + + rel_events = self.all_releases + all_latencies = [] + for i, trial in enumerate(rel_events): + for syn in trial: + p6.plot( + syn["time"], + syn["latency"], + pen=None, + symbolBrush=(i, len(rel_events)), + symbolPen=(i, len(rel_events)), + symbolSize=4, + symbol="o", + ) + all_latencies.append(syn["latency"]) + all_latencies = np.concatenate(all_latencies) + (hist, binedges) = np.histogram(all_latencies) + curve = p7.plot( + binedges, + hist, + stepMode=True, + fillBrush=(100, 100, 255, 150), + fillLevel=0, + ) + curve.rotate(-90) + curve.scale(-1, 1) + + # if probabilityPlot: + # self.win.nextRow() + # p8 = self.win.addPlot(labels={'left': 'Release Prob', 'bottom': 'Time (ms)'}) + # p8.setXLink(p1) + # times = self.release_timings() + # for isyn, syn in enumerate(self.synapses): + # syn_events = self.release_events(syn_no=isyn) + # Pr = syn_events['n_releases']/syn_events['n_requests'] # Pr for each stimulus + # # print Pr + # + # i = 0 # ultimately would like to plot this for each synapse + # p8.plot(events[0]['pulse time'], Pr, pen=None, symbolBrush=(i, len(self.all_releases)), + # symbolPen=(i, len(events)), symbolSize=4, symbol='o') + + # + # Plot GlyR state variables + # + # if glyPlot: + + # i = 0 + # if synapse.psd.psdType == 'glyslow': + # mpl.figure(2) + # for var in ['C0', 'C1', 'C2', 'O1', 'O1', 'D1', 'Open']: + # mpl.subplot(nstate, 1, i + 1) + # mpl.plot(t, self[var]) + # mpl.ylabel(var) + # i = i + 1 + # if synapse.psd.psdType == 'glyfast': + # mpl.figure(2) + # for var in ['C0', 'C1', 'C2', 'C3', 'O1', 'O2', 'Open']: + # mpl.subplot(7, 1, i + 1) + # mpl.plot(t, self[var]) + # mpl.ylabel(var) + # i = i + 1 + # mpl.draw() + # mpl.show() diff --git a/cnmodel/protocols/vc_curve.py b/cnmodel/protocols/vc_curve.py new file mode 100644 index 0000000..b33e4cc --- /dev/null +++ b/cnmodel/protocols/vc_curve.py @@ -0,0 +1,206 @@ +import os +import os.path +from neuron import h +import numpy as np +import scipy +import scipy.integrate +import scipy.stats + +from .protocol import Protocol + + +try: + import pyqtgraph as pg + + HAVE_PG = True +except ImportError: + HAVE_PG = False +from ..util import custom_init +from ..util.stim import make_pulse + +# import matplotlib as MP # must call first... before pylag/pyplot or backends +# MP.use('Qt4Agg') + +# import matplotlib.gridspec as GS +# import mpl_toolkits.axes_grid1.inset_locator as INSETS +# import mpl_toolkits.axes_grid1.anchored_artists as ANCHOR + +# stdFont = 'Arial' +# import matplotlib.pyplot as pylab +# pylab.rcParams['interactive'] = False +# pylab.rcParams['mathtext.default'] = 'sf' +## next setting allows pdf font to be readable in Adobe Illustrator +# pylab.rcParams['pdf.fonttype'] = 42 +# pylab.rcParams['figure.facecolor'] = 'white' + + +class VCCurve(Protocol): + def __init__(self): + super(VCCurve, self).__init__() + + def reset(self): + super(VCCurve, self).reset() + self.voltage_traces = [] + self.current_traces = [] + self.durs = None # durations of current steps + self.voltage_cmd = None # Current command levels + self.time_values = None + self.dt = None + + def run(self, vcrange, cell, dt=0.025): + """ + Run voltage-clamp I/V curve. + + Parameters + ---------- + vmin : float + Minimum voltage step value + vmax : + Maximum voltage step value + vstep : + Voltage difference between steps + cell : + The Cell instance to test. + """ + self.reset() + self.cell = cell + try: + (vmin, vmax, vstep) = vcrange # unpack the tuple... + except: + raise TypeError("run_iv argument 1 must be a tuple (imin, imax, istep)") + + vstim = h.SEClamp(0.5, cell.soma) # set up a single-electrode clamp + vstim.dur1 = 50.0 + vstim.amp1 = -60 + vstim.dur2 = 500.0 + vstim.amp2 = -60.0 + vstim.dur3 = 400 + vstim.amp3 = -60.0 + vstim.rs = 0.01 + cell.soma.cm = 0.001 # reduce capacitative transients (cap compensation) + self.durs = [vstim.dur1, vstim.dur2, vstim.dur3] + self.amps = [vstim.amp1, vstim.amp2, vstim.amp3] + self.voltage_cmd = [] + tend = 900.0 + iv_nstepv = int(np.ceil((vmax - vmin) / vstep)) + iv_minv = vmin + iv_maxv = vmax + vstep = (iv_maxv - iv_minv) / iv_nstepv + for i in range(iv_nstepv): + self.voltage_cmd.append(float(i * vstep) + iv_minv) + nreps = iv_nstepv + h.dt = dt + self.dt = h.dt + for i in range(nreps): + # Connect recording vectors + self["v_soma"] = cell.soma(0.5)._ref_v + self["i_inj"] = vstim._ref_i + self["time"] = h._ref_t + vstim.amp2 = self.voltage_cmd[i] + custom_init(v_init=-60.0) + h.tstop = tend + self.cell.check_all_mechs() + while h.t < h.tstop: + h.fadvance() + self.voltage_traces.append(self["v_soma"]) + self.current_traces.append(self["i_inj"]) + self.time_values = np.array(self["time"]) + + def steady_im(self, window=0.1): + """ + Parameters + ---------- + window : float (default: 0.1) + fraction of window to use for steady-state measurement, taken + immediately before the end of the step + Returns + ------- + steady-state membrane current for each trace. + """ + Im = self.current_traces + steps = len(Im) + steadyStop = int((self.durs[0] + self.durs[1]) / self.dt) + steadyStart = int(steadyStop - (self.durs[1] * window) / self.dt) + Isteady = [Im[i][steadyStart:steadyStop].mean() for i in range(steps)] + return np.array(Isteady) + + def peak_im(self, window=0.4): + """ + Parameters + ---------- + window: float (default=0.4) + fraction of window to use for peak measurement, taken + immediately following the beginning of the step + Returns + ------ + steady-state membrane current for each trace. + """ + Im = self.current_traces + steps = len(Im) + peakStop = int((self.durs[0] + window * self.durs[1]) / self.dt) + peakStart = int(self.durs[0] / self.dt) + Vhold = self.amps[ + 0 + ] # np.mean([self.voltage_traces[i][:peakStart].mean() for i in range(steps)]) + Ipeak = [] + for i in range(steps): + if self.voltage_cmd[i] > Vhold: + Ipeak.append(Im[i][peakStart:peakStop].max()) + else: + Ipeak.append(Im[i][peakStart:peakStop].min()) + return np.array(Ipeak) + + def show(self, cell=None): + """ + Plot results from run_iv() + """ + if not HAVE_PG: + raise Exception("Requires pyqtgraph") + + # + # Generate figure with subplots + # + app = pg.mkQApp() + if cell is not None: + win = pg.GraphicsWindow( + "%s %s (%s)" + % ( + cell.status["name"], + cell.status["modelType"], + cell.status["species"], + ) + ) + else: + win = pg.GraphisWindow("Voltage Clamp") + self.win = win + win.resize(1000, 800) + Iplot = win.addPlot(labels={"left": "Im (nA)", "bottom": "Time (ms)"}) + rightGrid = win.addLayout(rowspan=2) + win.nextRow() + Vplot = win.addPlot(labels={"left": "V (mV)", "bottom": "Time (ms)"}) + + IVplot = rightGrid.addPlot(labels={"left": "Vm (mV)", "bottom": "Icmd (nA)"}) + IVplot.showGrid(x=True, y=True) + rightGrid.nextRow() + + win.ci.layout.setRowStretchFactor(0, 10) + win.ci.layout.setRowStretchFactor(1, 5) + + # + # Plot simulation and analysis results + # + Vm = self.voltage_traces + Iinj = self.current_traces + Vcmd = self.voltage_cmd + t = self.time_values + steps = len(Vcmd) + + # plot I, V traces + colors = [(i, steps * 3.0 / 2.0) for i in range(steps)] + for i in range(steps): + Vplot.plot(t, Vm[i], pen=colors[i]) + Iplot.plot(t, Iinj[i], pen=colors[i]) + + # I/V relationships + IVplot.plot(Vcmd, self.peak_im(), symbol="o", symbolBrush=(50, 150, 50, 255)) + IVplot.plot(Vcmd, self.steady_im(), symbol="s") diff --git a/cnmodel/synapses/__init__.py b/cnmodel/synapses/__init__.py new file mode 100644 index 0000000..eb36ab7 --- /dev/null +++ b/cnmodel/synapses/__init__.py @@ -0,0 +1,22 @@ +#!/usr/bin/python +# +# Synapse definitions for models. +# +# This file includes a number of different synapse definitions and default conductances +# for point models. Most are models from the lab for neurons of the cochlear nucleus. +# the synaptic receptor models are gleaned from the literature and sometimes fitted to the +# cochlear nucleus data. +# +# Paul B. Manis, Ph.D. 2009 (August - November 2009) +# +from neuron import h +import numpy as np + +from .synapse import Synapse +from .terminal import Terminal +from .psd import PSD +from .glu_psd import GluPSD +from .gly_psd import GlyPSD +from .stochastic_terminal import StochasticTerminal +from .simple_terminal import SimpleTerminal +from .exp2_psd import Exp2PSD diff --git a/cnmodel/synapses/exp2_psd.py b/cnmodel/synapses/exp2_psd.py new file mode 100644 index 0000000..5fbbc0e --- /dev/null +++ b/cnmodel/synapses/exp2_psd.py @@ -0,0 +1,75 @@ +import numpy as np +from neuron import h + +from .psd import PSD + + +class Exp2PSD(PSD): + """ + Simple double-exponential PSD from Neuron (fast). + """ + + def __init__( + self, section, terminal, weight=0.01, loc=0.5, tau1=0.1, tau2=0.3, erev=0 + ): + """ + Parameters + ---------- + section : Section + The postsynaptic section in which to insert the receptor mechanism. + terminal : Terminal + The presynaptic Terminal instance + weight : + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + + """ + PSD.__init__(self, section, terminal) + self.syn = h.Exp2Syn(loc, sec=section) + self.syn.tau1 = tau1 + self.syn.tau2 = tau2 + self.syn.e = erev + + terminal.connect(self.syn, weight=weight) + + @property + def n_psd(self): + """The number of postsynaptic densities represented by this object. + """ + return 1 + + def record(self, *args): + """Create a new set of vectors to record parameters for each release + site. + + + Parameters + ---------- + \*args : + Allowed parameters are 'i' (current), 'g' (conductnace), and 'Open' (open probability). + + """ + self.vectors = {"ampa": [], "nmda": []} + for receptor in self.vectors: + for mech in getattr(self, receptor + "_psd"): + vec = {} + for var in args: + vec[var] = h.Vector() + vec[var].record(getattr(mech, "_ref_" + var)) + self.vectors[receptor].append(vec) + + def get_vector(self, var): + """Return an array from a previously recorded vector. + + Parameters + ---------- + receptor : str + May be 'ampa' or 'nmda' + var : str + Allowed parameters are 'i' (current), 'g' (conductance), and 'Open' (open probability). + i : int, default=0 + The integer index of the psd (if this is a multi-site synapse) + + """ + v = self.vectors[receptor][i][var] + return np.array(v) diff --git a/cnmodel/synapses/glu_psd.py b/cnmodel/synapses/glu_psd.py new file mode 100644 index 0000000..4d3ae6e --- /dev/null +++ b/cnmodel/synapses/glu_psd.py @@ -0,0 +1,146 @@ +import numpy as np +from neuron import h + +from .psd import PSD + + +class GluPSD(PSD): + """ + Glutamatergic PSD with ionotropic AMPA / NMDA receptors + + This creates a set of postsynaptoc NMDA and AMPA receptors, one pair + per terminal release site. Receptors are connected to the XMTR range + variable of the terminal release mechanisms. + + Parameters + ---------- + section : Section instance + The postsynaptic section into which the receptor mechanisms should be + attached + terminal : Terminal instance + The presynaptic terminal that provides input to the receptor XMTR + variables. + ampa_gmax : float + Maximum conductance of AMPARs + nmda_gmax : float + Maximum conductance of NMDARs + gvar : float + Coefficient of variation for randomly adjusting ampa_gmax and nmda_gmax. + Note that ampa and nmda maximum conductances will be scaled together, + but the scale values will be selected randomly for each pair of + receptor mechanisms. + eRev : float + Reversal potential to use for both receptor types. + ampa_params : dict + Dictionary containing kinetic parameters for AMPA mechanism. Suggested + keys are Ro1, Ro2, Rc1, Rc2, and PA. + + Notes + ----- + + *ampa_gmax* and *nmda_gmax* should be provided as the maximum *measured* + conductances; these will be automatically corrected for the maximum open + probability of the receptor mechanisms. + + GluPSD does not include a cleft mechanism because AMPATRUSSELL implements + its own cleft and NMDA_Kampa is slow enough that a cleft would have + insignificant effect. + + """ + + def __init__( + self, + section, + terminal, + ampa_gmax, + nmda_gmax, + gvar=0, + eRev=0, + nmda_vshift=0, + ampa_params=None, + loc=0.5, + ): + PSD.__init__(self, section, terminal) + # print('\033[0;33;40m ^^^^^ GVAR = %.4f ^^^^^\033[0;37;40m ' % gvar) + ampa_params = {} if ampa_params is None else ampa_params + + # and then make a set of postsynaptic receptor mechanisms + ampa_psd = [] + nmda_psd = [] + relsite = terminal.relsite + self.section.push() + for i in range(0, terminal.n_rzones): + # create mechanisms + ampa = h.AMPATRUSSELL( + loc, self.section + ) # raman/trussell AMPA with rectification + nmda = h.NMDA_Kampa(loc, self.section) # Kampa state model NMDA receptors + + # Connect terminal to psd + h.setpointer(relsite._ref_XMTR[i], "XMTR", ampa) + h.setpointer(relsite._ref_XMTR[i], "XMTR", nmda) + + # Set any extra ampa parameters provided by the caller + # (Ro1, Ro2, Rc1, Rc2, PA, ...) + for k, v in ampa_params.items(): + setattr(ampa, k, v) + + # add a little variability - gvar is CV of amplitudes + v = 1.0 + gvar * np.random.standard_normal() + + # set gmax and eRev for each postsynaptic receptor mechanism + ampa.gmax = ampa_gmax * v + ampa.Erev = eRev + nmda.gmax = nmda_gmax * v + nmda.Erev = eRev + nmda.vshift = nmda_vshift + + ampa_psd.append(ampa) + nmda_psd.append(nmda) + + h.pop_section() + + self.ampa_psd = ampa_psd + self.nmda_psd = nmda_psd + self.all_psd = nmda_psd + ampa_psd + + @property + def n_psd(self): + """The number of postsynaptic densities represented by this object. + """ + return len(self.ampa_psd) + + def record(self, *args): + """Create a new set of vectors to record parameters for each release + site. + + Parameters + ---------- + \*args : + Allowed parameters are 'i' (current), 'g' (conductance), and 'Open' (open probability). + + """ + self.vectors = {"ampa": [], "nmda": []} + for receptor in self.vectors: + for mech in getattr(self, receptor + "_psd"): + vec = {} + for var in args: + vec[var] = h.Vector() + vec[var].record(getattr(mech, "_ref_" + var)) + self.vectors[receptor].append(vec) + + def get_vector(self, receptor, var, i=0): + """Return an array from a previously recorded vector. + + Parameters + ---------- + receptor : str + May be 'ampa' or 'nmda' + var : str + Allowed parameters are 'i' (current), 'g' (conductance), and 'Open' (open probability). + i : int, default=0 + The integer index of the psd (if this is a multi-site synapse) + + """ + v = self.vectors[receptor][i][var] + return np.array(v) diff --git a/cnmodel/synapses/gly_psd.py b/cnmodel/synapses/gly_psd.py new file mode 100644 index 0000000..84fd429 --- /dev/null +++ b/cnmodel/synapses/gly_psd.py @@ -0,0 +1,398 @@ +from __future__ import print_function +import numpy as np +from neuron import h + +from .psd import PSD + + +class GlyPSD(PSD): + """Glycinergic PSD + + This creates postsynaptoc glycinergic receptors. Receptors are connected to the XMTR range + variable of the terminal release mechanisms. + + Parameters + ---------- + section : Section + The postsynaptic section in which to insert the receptor mechanism. + terminal : Terminal + The presynaptic Terminal instance + params : dict, default=None + Dictionary of kinetic parameters to override {'KV', 'KU', 'XMax'} + gmax : float, default=1000. + maximal conductance unless overridden by values used in psdType + psdType : str, default='glyfast' + Kinetic model of receptors: possiblities are: + glyfast, glyslow, glyGC, glya5, or glyexp, as defined in the mechanisms. + message : str, default: None + placeholder for a message to be printed out when testing. + debug: bool, default=False + enable printing of internal debugging messages. + gvar : float, default=0 + coefficient of variation of the amplitudes for each of the release zones. + eRev : float, default=-70 + "Reversal" potential, or Nernst potential for ions through the receptor channel. + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + + """ + + def __init__( + self, + section, + terminal, + params=None, + gmax=1000.0, + psdType="glyfast", + message=None, + debug=False, + gvar=0, + eRev=-70, + loc=0.5, + ): + + PSD.__init__(self, section, terminal) + pre_sec = terminal.section + post_sec = section + + from .. import cells + + params = {} if params is None else params + + self.pre_cell = cells.cell_from_section(pre_sec) + self.post_cell = cells.cell_from_section(post_sec) + + self.psdType = psdType + self.gmax = gmax + glyslowPoMax = ( + 0.162297 + ) # thse were measured from the kinetic models in Synapses.py, as peak open P for the glycine receptors + glyfastPoMax = 0.038475 # also later verified, same numbers... + if self.psdType == "glyfast": + gmax /= ( + glyfastPoMax + ) # normalized to maximum open probability for this receptor + if self.psdType == "glyslow": + gmax /= glyslowPoMax # normalized to max open prob for the slow receptor. + + # print "Stochastic syn: j = %d of n_fibers = %d n_rzones = %d\n" % (j, n_fibers, n_rzones) + relzone = terminal.relsite + n_rzones = terminal.n_rzones + + # + # Create cleft mechanisms + # + clefts = [] + for k in range(0, n_rzones): + cl = h.cleftXmtr(loc, sec=post_sec) + clefts.append(cl) + + # and then make a set of postsynaptic receptor mechanisms + if self.psdType == "glyslow": + (psd, par) = self.template_Gly_PSD_State_Gly6S( + nReceptors=n_rzones, psdtype=self.psdType + ) + elif self.psdType == "glyfast": + (psd, par) = self.template_Gly_PSD_State_PL( + nReceptors=n_rzones, psdtype=self.psdType + ) + elif self.psdType == "glyGC": + (psd, par) = self.template_Gly_PSD_State_GC( + nReceptors=n_rzones, psdtype=self.psdType + ) + elif self.psdType == "glya5": + (psd, par) = self.template_Gly_PSD_State_Glya5( + nReceptors=n_rzones, psdtype=self.psdType + ) + elif self.psdType == "glyexp": + (psd, par) = self.template_Gly_PSD_exp( + nReceptors=n_rzones, psdtype=self.psdType + ) + else: + print("**PSDTYPE IS NOT RECOGNIZED: [%s]\n" % (self.psdType)) + exit() + if debug: + print("pre_sec: ", pre_sec) + + # Connect terminal to psd (or cleft) + self._cleft_netcons = [] + for k in range(0, n_rzones): + pre_sec.push() + netcon = h.NetCon(relzone._ref_XMTR[k], clefts[k], 0.1, 0.0, 1.0) + self._cleft_netcons.append(netcon) + h.pop_section() + + # set cleft transmitter kinetic parameters + for pname, pval in params.items(): + setattr(clefts[k], pname, pval) + + h.setpointer( + clefts[k]._ref_CXmtr, "XMTR", psd[k] + ) # connect transmitter release to the PSD + + v = 1.0 + gvar * np.random.standard_normal() + psd[k].gmax = ( + gmax * v + ) # add a little variability - gvar is CV of amplitudes + # print 'GLY psd %s %d gmax=%f' % (self.psdType, k, gmax) + psd[k].Erev = eRev # set the reversal potential + + par = list(par) + + if message is not None: + print(message) + + self.all_psd = psd + self.clefts = clefts + self.par = par + + # the following templates are a bit more complicated. + # The parameter names as defined in the model are returned + # but also those that are involved in the forward binding reactions are + # listed separately - this is to allow us to run curve fits that adjust + # only a subset of the parameters a time - e.g., the rising phase, then + # the falling phase with the rising phase fixed. + # the dictionary selection is made by selectpars in glycine_fit.py. + # + + def template_Gly_PSD_exp( + self, debug=False, nReceptors=2, cellname=None, message=None, loc=0.5 + ): + """ + Template to build simple glycinergic exp-style PSD + + Parameters + ---------- + nReceptors : int, default=2 + number of sites to implement + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + + Unused parameters: + cellname, debug, message. + + """ + + sec = self.section + psd = [] + sec.push() + for k in range(0, nReceptors): + psd.append(h.GLY2(loc, sec)) + h.pop_section() + par = ["alpha", "beta"] + p = [] + for n in par: + p.append(eval("psd[0]." + n)) # get default values from the mod file + return (psd, par, p) + + def template_Gly_PSD_State_Glya5( + self, debug=False, nReceptors=2, psdtype=None, message=None, loc=0.5 + ): + """ + Template to build PSD using state model of glycine receptors, model glya5.mod + + Parameters + ---------- + nReceptors : int, default=2 + number of sites to implement + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + + Unused parameters: + cellname, debug, message, psdtype. + + """ + sec = self.section + psd = [] + sec.push() + for k in range(0, nReceptors): + psd.append(h.GLYa5(loc, sec)) + h.pop_section() + par = { + "kf1": ("r", psd[0].kf1), # retreive values in the MOD file + "kf2": ("r", psd[0].kf2), + "kb1": ("r", psd[0].kb1), + "kb2": ("r", psd[0].kb2), + "a1": ("f", psd[0].a1), + "b1": ("f", psd[0].b1), + "a2": ("f", psd[0].a2), + "b2": ("f", psd[0].b2), + } + return (psd, par) + + def template_Gly_PSD_State_Gly6S( + self, debug=False, nReceptors=2, psdtype=None, message=None, loc=0.5 + ): + """ + Template to build PSD using state model of glycine receptors, model gly6s.mod + + Parameters + ---------- + nReceptors : int, default=2 + number of sites to implement + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + psdtype : str, default=None + resets the psd type to have slow kinetics if 'glyslow'. Any other string + defaults to kinetics in the mod file. + + Unused parameters: + cellname, debug, message. + + """ + sec = self.section + psd = [] + sec.push() + for k in range(0, nReceptors): + psd.append( + h.Gly6S(loc, sec) + ) # simple using Trussell model 6 states with desens + if debug: + print("Gly6S psdtype: ", psdtype) + if ( + psdtype == "glyslow" + ): # fit on 8 March 2010, error = 0.164, max open: 0.155 + psd[-1].Rd = 1.177999 + psd[-1].Rr = 0.000005 + psd[-1].Rb = 0.009403 + psd[-1].Ru2 = 0.000086 + psd[-1].Ro1 = 0.187858 + psd[-1].Ro2 = 1.064426 + psd[-1].Ru1 = 0.028696 + psd[-1].Rc1 = 0.103625 + psd[-1].Rc2 = 1.730578 + h.pop_section() + par = { + "Rb": ("n", psd[0].Rb), # retrive values in the MOD file + "Ru1": ("r", psd[0].Ru1), + "Ru2": ("r", psd[0].Ru2), + "Rd": ("f", psd[0].Rd), + "Rr": ("f", psd[0].Rr), + "Ro1": ("f", psd[0].Ro1), + "Ro2": ("r", psd[0].Ro2), + "Rc1": ("f", psd[0].Rc1), + "Rc2": ("r", psd[0].Rc2), + } + return (psd, par) + + def template_Gly_PSD_State_PL( + self, + debug=False, + nReceptors=2, + cellname=None, + psdtype=None, + message=None, + loc=0.5, + ): + """ + Template to build PSD using state model of glycine receptors, model glypl.mod + + Parameters + ---------- + nReceptors : int, default=2 + number of sites to implement + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + psdtype : str, default=None + resets the psd type to have slow kinetics if 'glyslow'. + 'glyfast' forces fast kinetics. + Any other string defaults to the default kinetics in the mod file. + + Unused parameters: + cellname, debug, message,. + + """ + sec = self.section + psd = [] + sec.push() + for k in range(0, nReceptors): + psd.append(h.GLYaPL(loc, sec)) # simple dextesche glycine receptors + if debug: + print("PL psdtype: ", psdtype) + if psdtype == "glyslow": + psd[-1].a1 = 0.000451 + psd[-1].a2 = 0.220 + psd[-1].b1 = 13.27 + psd[-1].b2 = 6.845 + psd[-1].kon = 0.00555 + psd[-1].koff = 2.256 + psd[-1].r = 1.060 + psd[-1].d = 55.03 + if ( + psdtype == "glyfast" + ): # fit from 3/5/2010. error = 0.174 maxopen = 0.0385 + psd[-1].a1 = 1.000476 + psd[-1].a2 = 0.137903 + psd[-1].b1 = 1.700306 + psd[-1].koff = 13.143132 + psd[-1].kon = 0.038634 + psd[-1].r = 0.842504 + psd[-1].b2 = 8.051435 + psd[-1].d = 12.821820 + h.pop_section() + par = { + "kon": ("r", psd[0].kon), # retrive values in the MOD file + "koff": ("r", psd[0].koff), + "a1": ("r", psd[0].a1), + "b1": ("r", psd[0].b1), + "a2": ("f", psd[0].a2), + "b2": ("f", psd[0].b2), + "r": ("f", psd[0].r), + "d": ("f", psd[0].d), + } + return (psd, par) + + def template_Gly_PSD_State_GC( + self, debug=False, nReceptors=2, psdtype=None, message=None, loc=0.5 + ): + """ + Template to build PSD using state model of glycine receptors, model glyaGC.mod + + Parameters + ---------- + nReceptors : int, default=2 + number of sites to implement + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + psdtype : str, default=None + resets the psd type to have slow kinetics if 'glyslow'. Any other string + defaults to predefined kinetics in the mod file. + + Unused parameters: + cellname, debug, message, psdtype. + + """ + sec = self.section + psd = [] + sec.push() + for k in range(0, nReceptors): + psd.append(h.GLYaGC(loc, sec)) # simple Dextesche glycine receptors + if psdtype == "glyslow": + psd[-1].k1 = 12.81 # (/uM /ms) : binding + psd[-1].km1 = 0.0087 # (/ms) : unbinding + psd[-1].a1 = 0.0195 # (/ms) : opening + psd[-1].b1 = 1.138 # (/ms) : closing + psd[-1].r1 = 6.13 # (/ms) : desense 1 + psd[-1].d1 = 0.000462 # (/ms) : return from d1 + psd[-1].r2 = 0.731 # (/ms) : return from deep state + psd[-1].d2 = 1.65 # (/ms) : going to deep state + psd[-1].r3 = 3.83 # (/ms) : return from deep state + psd[-1].d3 = 1.806 # (/ms) : going to deep state + psd[-1].rd = 1.04 # (/ms) + psd[-1].dd = 1.004 # (/ms) + h.pop_section() + par = { + "k1": ("r", psd[0].k1), # retrive values in the MOD file + "km1": ("r", psd[0].km1), + "a1": ("r", psd[0].a1), + "b1": ("r", psd[0].b1), + "r1": ("f", psd[0].r1), + "d1": ("f", psd[0].d1), + "r2": ("f", psd[0].r2), + "d2": ("f", psd[0].d2), + "r3": ("f", psd[0].r3), + "d3": ("f", psd[0].d3), + "rd": ("f", psd[0].rd), + "dd": ("f", psd[0].dd), + } + return (psd, par) diff --git a/cnmodel/synapses/psd.py b/cnmodel/synapses/psd.py new file mode 100644 index 0000000..c675b2e --- /dev/null +++ b/cnmodel/synapses/psd.py @@ -0,0 +1,41 @@ +class PSD(object): + """ + Base class for postsynaptic density mechanisms, possibly including cleft. + May accept either NetCon or pointer inputs from a Terminal, and directly + modifies the membrane potential and/or ion concentrations of the + postsynaptic cell. + """ + + def __init__(self, section, terminal): + """ + Parameters + ---------- + section : :obj:`NEURON section` + Set the section in the postsynaptic cell that the terminal is attached to. + + terminal : :obj:`Synapse` + + + """ + self._section = section + self._terminal = terminal + + @property + def section(self): + """ The cell section this PSD is attached to. + """ + return self._section + + @property + def cell(self): + """ The cell this PSD is attached to. + """ + from ..cells import Cell + + return Cell.from_section(self.section) + + @property + def terminal(self): + """ The presynaptic terminal connected to this PSD. + """ + return self._terminal diff --git a/cnmodel/synapses/simple_terminal.py b/cnmodel/synapses/simple_terminal.py new file mode 100644 index 0000000..8b6b1fb --- /dev/null +++ b/cnmodel/synapses/simple_terminal.py @@ -0,0 +1,52 @@ +from neuron import h + +from .terminal import Terminal + + +class SimpleTerminal(Terminal): + """ + Simple terminal using netcon. + """ + + def __init__(self, pre_sec, target_cell, spike_source=None, loc=0.5): + """ + Parameters + ---------- + pre_sec : :obj: `NEURON Section` + The presynaptic section that is monitored for spikes. The voltage + in this section is monitored to trigger the postsynaptic conductance + as the spike source + spike_source : :obj: `NEURON Section` + Overrides the pre_sec as the spike source for this terminal. + terminal : :obj: `Synapse Terminal` + The presynaptic Terminal instance + loc : float, default=0.5 + Position on the postsynaptic section to insert the mechanism, from [0..1]. + """ + + Terminal.__init__(self, pre_sec) + if spike_source is None: + spike_source = pre_sec(loc)._ref_v + self.spike_source = spike_source + self.pre_sec = pre_sec + + def connect(self, post, weight): + """ + Connect this terminal to a postsynaptic cell section + The synaptic delay is 0.5 msec, and the presynaptic + action potential threshold is -20 mV. + + Parameters + ---------- + post : :obj: `NEURON Section` + + weight : float + Strength of the connection + + """ + thresh = -20 + delay = 0.5 + self.netcon = h.NetCon( + self.spike_source, post, thresh, delay, weight, sec=self.pre_sec + ) + self.netcon.weight[0] = weight diff --git a/cnmodel/synapses/stochastic_terminal.py b/cnmodel/synapses/stochastic_terminal.py new file mode 100644 index 0000000..b49be40 --- /dev/null +++ b/cnmodel/synapses/stochastic_terminal.py @@ -0,0 +1,393 @@ +from __future__ import print_function +from neuron import h + +from .terminal import Terminal +from ..util import random_seed + +# utility class to create parameter lists... +# create like: p = Params(abc=2.0, defg = 3.0, lunch='sandwich') +# reference like p.abc, p.defg, etc. +class Params(object): + def __init__(self, **kwds): + self.__dict__.update(kwds) + + +class StochasticTerminal(Terminal): + """ + Axon terminal with multi-site sctochastic release mechanism. + """ + + def __init__( + self, + pre_sec, + target_cell, + nzones=1, + multisite=True, + message=None, + type="lognormal", + identifier=0, + stochastic_pars=None, + calcium_pars=None, + delay=0, + debug=False, + select=None, + spike_source=None, + dep_flag=1, + ): + """ + This routine creates a (potentially) multisite synapse using a NEURON mod file with: + - A MultiSiteSynapse release mechanism that includes stochastic release, with a lognormal + release latency distribution. The Facilitation and Depression of release are governed + by parameters obtaine from fitting the Dittman-Kreitzer-Regher (DKR) model (J Neurosci. 2000 Feb 15;20(4):1374-85.) + to experimental data at various + frequencies. + - A "cleft" mechanism (models diffusion of transmitter). Note that the cleft is inserted as part of the + presynaptic section, but is not connected to the postsynaptic side yet. + + Each release site is stochastically independent, but all sites within a terminal are drive by the + same presynaptic action potentials. + + Turning off the depression and facilitation in the kinetic portion of the model substantially decreases + the time the terminal mechanism takes to run. + + Parameters + ---------- + pre_sec : :obj:`section` + The NEURON section where the synaptic mechanisms should be inserted. + target_cell : :obj:`Cell` + The target cell object that the synapse will innervate. + nzones : int + The number of activate zones to insert into the section. + multisite : bool, default: True + A flag that determines whether the terminal actually creates multiple + release zones (True) or just creates a single release zone that + varies its amplitude based on the depression/facilitation state. + message : str + A message to when instantiating (mostly for verification of code flow). + type: str (default: 'lognormal') + 'lognormal' sets the release event latency distribution to use a lognormal function. Currently, + no other function is supported. + identifier : int (default: 0) + An identifier to associate with these release sites so we can find them later. + stochastic_pars : dict (default: None) + A dictionary of parameters (Param class) used to specifiy the stochastic behavior of this site, + including release latency, stdev, and lognormal distribution paramaters + calcium_pars : dict (default: None) + A dictionary of parameters (Param class) to determine the calcium channels in this section. + If None, then no calcium channels are inserted; otherwise, a P-type calcium conductance and a dynamic + mechanism are inserted, and their conductance is set. + delay : float (default: 0) + Delay time in msec from action potential until transmitter release for this terminal. + debug : bool (default: False) + Flag to print stuff out while debugging. + spike_source : :obj:`section` (default: None) + The input spike source to use in net con - default is to use pre_sec when set to None. + dep_flag : int (default: 1) + Set to 1 for depression mechanism (slows computation), 0 to turn off the depression calculations + + Returns + ------- + list + the list contains the terminal, the relsites, and the list of cleft mechanisms: + + - terminal: this is the pointer to the terminal section that was inserted (same as pre_sec if it was + specified) + - relsite: a list of the nzones release sites that were created + - cleft: a list of the nzones cleft mechanisms that were created. + + """ + Terminal.__init__(self, pre_sec) + + # set parameter control for the stochastic release of vesicles... + # this structure is passed to stochastic synapses, and replaces several variables + # that were previously defined in the call to that function. + + thresh = -30 # mV - AP detection on the presynaptic side. + + ANTerminals_Latency = 0.5 # latency + vPars = Params( + LN_Flag=1, + LN_t0=10.0, + LN_A0=0.05, + LN_tau=35, + LN_std=0.05, + Lat_Flag=1, + Lat_t0=10.0, + Lat_A0=0.140, + Lat_tau=21.5, + latency=ANTerminals_Latency, + ) + # NOTE: stochastic_pars must define parameters used by multisite, including: + # .delay is the netcon delay between the presynaptic AP and the start of release events + # .Latency is the latency to the mean release event... this could be confusing. + + if stochastic_pars is None: + stochastic_pars = vPars + + message = ( + " >> creating terminal with %d release zones using lognormal release latencies (coh4)" + % nzones + ) + if debug: + print(message) + terminal = pre_sec + # terminal.push() + if calcium_pars is not None: + terminal.insert("cap") # insert calcium channel density + terminal().cap.pcabar = calcium_pars.Ca_gbar + terminal.insert("cad") + + # Create point process to simulate multiple independent release zones. + relsite = h.MultiSiteSynapse(0.5, sec=terminal) + relsite.nZones = nzones + if multisite: + relsite.multisite = 1 + relsite.rseed = random_seed.current_seed() # use global random seed + relsite.latency = stochastic_pars.latency + relsite.latstd = stochastic_pars.LN_std + self.n_rzones = nzones + else: + relsite.multisite = 0 + self.release_rng = h.Random(random_seed.current_seed()) + self.release_rng.uniform(0, 1) + relsite.setUniformRNG(self.release_rng) + self.n_rzones = 1 + + relsite.Dep_Flag = dep_flag # control synaptic dynamics + if debug is True: + relsite.debug = 1 + relsite.Identifier = identifier + # if type == 'gamma': + # gd = gamma.rvs(2, size=10000)/2.0 # get a sample of 10000 events with a gamma dist of 2, set to mean of 1.0 + # if relsite.latstd > 0.0: + # gds = relsite.latency+std*(gd-1.0)/gd.std() # scale standard deviation + # else: + # gds = relsite.latency*np.ones((10000,1)) + # if type == 'lognormal': + # if std > 0.0: + # gds = lognormal(mean=0, sigma=relsite.latstd, size=10000) + # else: + # gds = np.zeros((10000, 1)) + # use the variable latency mode of COH4. And, it is lognormal no matter what. + # the parameters are defined in COH4.mod as follows + # Time course of latency shift in release during repetitive stimulation + # Lat_Flag = 0 (1) : 0 means fixed latency, 1 means lognormal distribution + # Lat_t0 = 0.0 (ms) : minimum time since simulation start before changes in latency are calculated + # Lat_A0 = 0.0 (ms) : size of latency shift from t0 to infinity + # Lat_tau = 100.0 (ms) : rate of change of latency shift (from fit of a+b(1-exp(-t/tau))) + # : Statistical control of log-normal release shape over time during repetive stimulation + # LN_Flag = 0 (1) : 0 means fixed values for all time + # LN_t0 = 0.0 (ms) : : minimum time since simulation start before changes in distribution are calculated + # LN_A0 = 0.0 (ms) : size of change in sigma from t0 to infinity + # LN_tau = 100.0 (ms) : rate of change of sigma over time (from fit of a+b*(1-exp(-t/tau))) + + relsite.LN_Flag = ( + stochastic_pars.LN_Flag + ) # enable use of lognormal release latency + relsite.LN_t0 = stochastic_pars.LN_t0 + relsite.LN_A0 = stochastic_pars.LN_A0 + relsite.LN_tau = stochastic_pars.LN_tau + relsite.Lat_Flag = stochastic_pars.Lat_Flag + relsite.Lat_t0 = stochastic_pars.Lat_t0 + relsite.Lat_A0 = stochastic_pars.Lat_A0 + relsite.Lat_tau = stochastic_pars.Lat_tau + # mpl.figure(2) + + h.pop_section() + self.relsite = relsite + + if spike_source is None: + spike_source = pre_sec(0.5)._ref_v + + self.netcon = h.NetCon(spike_source, relsite, thresh, delay, 1.0, sec=pre_sec) + self.netcon.weight[0] = 1 + self.netcon.threshold = -30.0 + + self.setPsdType(target_cell, select) + + def setPsdType(self, target_cell, select=None): + """ + Assign a postsynpatic density type - selection of receptors to be + associated with the presynaptic terminal. + + Parameters + ---------- + target_cell : :obj:`NEURON section` + Define the target cell so that the correct PSD is inserted + + select : str + Not used in current implementation. + + """ + # TODO: must resurrect this for inhibitory synapses. + # (and move constants out to their respective synapse locations) + # elif psdtype.startswith('gly'): + # self.setDF(target_cell, 'ipsc', select) # set the parameters for release + self.setDF(target_cell, "epsc") # set the parameters for release + + ################################################################################ + # The following routines set the synapse dynamics, based on measurements and fit + # to the DKR model. + ################################################################################ + + def setDF(self, target_cell, synapsetype, select=None): + """ + Set the facilitation/depression parameters for the multisite release model. + The parameters used here were obtained from an optimized fit of the DKR + model to stimulus and recovery data for the auditory nerve synapses onto bushy + and T-stellate cells at 100, 200 and 300 Hz (simultaneous fitting), for times out to about + 0.5 - 1.0 second. Data were collected by Dr. Ruili Xie and Dr. Yong Wang. + Fitting by Paul Manis. + + Parameters + ---------- + target_cell : :obj:`NEURON section` + Define the target cell so that the correct release kinetics are used + + synapsetype : str + String defining the type of synapse: 'ipsc' or 'epsc' + + select : str (default: None) + Select kinetcs from a particular example cell. + + """ + from .. import cells + + if isinstance(target_cell, cells.Bushy): + if synapsetype == "ipsc": + if select is None: + self.bushy_ipsc_average() + else: + self.bushy_ipsc_single(select=select) + elif isinstance(target_cell, cells.TStellate): + if synapsetype == "ipsc": + self.stellate_ipsc() + + def set_params(self, **params): + """Set arbitrary parameters on the release mechanism. + + Parameters + ---------- + \**params : dict + dictionary of parameters to set on the release mechanism. + + """ + for k, v in params.items(): + setattr(self.relsite, k, v) + + def stellate_ipsc(self): + """ Facilitation/Depression parameters for DKR model for IPSCs onto stellate cells. + IPSCs were derived from stimulation of the dorsal cochlear nucleus, and so represent + the glycinergic synapses from tuberculoventral neurons. + Data is average of 3 cells studied with recovery curves and individually fit, at 100 Hz. + """ + self.relsite.F = 0.23047 + self.relsite.k0 = 1.23636 + self.relsite.kmax = 45.34474 + self.relsite.taud = 98.09 + self.relsite.kd = 0.01183 + self.relsite.taus = 17614.50 + self.relsite.ks = 17.88618 + self.relsite.kf = 19.11424 + self.relsite.tauf = 32.28 + self.relsite.dD = 2.52072 + self.relsite.dF = 2.33317 + self.relsite.glu = 3.06948 + + def bushy_ipsc_average(self): + """ Facilitation/Depression parameters for DKR model for IPSCs onto bushy cells. + IPSCs were derived from stimulation of the dorsal cochlear nucleus, and so represent + the glycinergic synapses from tuberculoventral neurons. + Data for the kinetcs are the average of 16 bushy cells. + The individual fits were compiled, and an average computed for just the 100 Hz data + across the individual fits. This average was then fit to the DKR model + (also see Matthew Xu-Friedman's papers). + The individual cells show a great deal of variability, from straight depression, to + mixed depression/facilitaiton, to facilation alone. This set of parameters generates + a weak facilitation followed by depression back to baseline. + + There are other data sets in the source that are commented out. + """ + # print( "USING average kinetics for Bushy IPSCs") + + # average of 16cells for 100 Hz (to model); no recovery. + self.relsite.F = 0.18521 + self.relsite.k0 = 2.29700 + self.relsite.kmax = 27.6667 + self.relsite.taud = 0.12366 + self.relsite.kd = 0.12272 + self.relsite.taus = 9.59624 + self.relsite.ks = 8.854469 + self.relsite.kf = 5.70771 + self.relsite.tauf = 0.37752 + self.relsite.dD = 4.00335 + self.relsite.dF = 0.72605 + self.relsite.glu = 5.61985 + + def bushy_ipsc_single(self, select=None): + """ + Facilitation/Depression parameters for DKR model for IPSCs onto 3 individual bushy cells. + IPSCs were derived from stimulation of the dorsal cochlear nucleus, and so represent + the glycinergic synapses from tuberculoventral neurons. + + Parameters + ---------- + select : int, default=None + select which cell's measured kinetics will be used: + + 0: Use average + 1: use data from 30aug08f + 2: select = 2, use data from 30aug08h + 3: select = 3, use data from 31aug08b (single cell, clean dataset) + + """ + # print ("Using bushy ipsc") + + if select is None or select > 4 or select <= 0: + self.bushy_ipsc_average() + return + + if select is 1: # 30aug08f + # print ("using 30aug08f ipsc") + self.relsite.F = 0.221818 + self.relsite.k0 = 0.003636364 + self.relsite.kmax = 0.077562107 + self.relsite.taud = 0.300000 + self.relsite.kd = 1.112554 + self.relsite.taus = 3.500000 + self.relsite.ks = 0.600000 + self.relsite.kf = 3.730452 + self.relsite.tauf = 0.592129 + self.relsite.dD = 0.755537 + self.relsite.dF = 2.931578 + self.relsite.glu = 1.000000 + + if select is 2: # 30aug08h + # print ("using 30aug08H ipsc") + self.relsite.F = 0.239404 + self.relsite.k0 = 3.636364 / 1000.0 + self.relsite.kmax = 16.725479 / 1000.0 + self.relsite.taud = 0.137832 + self.relsite.kd = 0.000900 + self.relsite.taus = 3.500000 + self.relsite.ks = 0.600000 + self.relsite.kf = 4.311995 + self.relsite.tauf = 0.014630 + self.relsite.dD = 3.326148 + self.relsite.dF = 0.725512 + self.relsite.glu = 1.000000 + + if select is 3: + # print ("using IPSC#3 ") + self.relsite.F = 0.29594 + self.relsite.k0 = 0.44388 / 1000.0 + self.relsite.kmax = 15.11385 / 1000.0 + self.relsite.taud = 0.00260 + self.relsite.kd = 0.00090 + self.relsite.taus = 11.40577 + self.relsite.ks = 27.98783 + self.relsite.kf = 30.00000 + self.relsite.tauf = 0.29853 + self.relsite.dD = 3.70000 + self.relsite.dF = 2.71163 + self.relsite.glu = 4.97494 diff --git a/cnmodel/synapses/synapse.py b/cnmodel/synapses/synapse.py new file mode 100644 index 0000000..f6a31b9 --- /dev/null +++ b/cnmodel/synapses/synapse.py @@ -0,0 +1,15 @@ +from .stochastic_terminal import StochasticTerminal +from .psd import PSD + + +class Synapse(object): + """Encapsulates a synaptic connection between two cells. + + Instances of this class are created by calling `Cell.connect()`. + """ + + def __init__(self, pre_cell, pre_opts, post_cell, post_opts, type="multisite"): + pre_opts["term_type"] = type + post_opts["psd_type"] = type + self.terminal = pre_cell.make_terminal(post_cell, **pre_opts) + self.psd = post_cell.make_psd(self.terminal, **post_opts) diff --git a/cnmodel/synapses/terminal.py b/cnmodel/synapses/terminal.py new file mode 100644 index 0000000..61faea4 --- /dev/null +++ b/cnmodel/synapses/terminal.py @@ -0,0 +1,32 @@ +class Terminal(object): + """ + Base class for axon terminals. A terminal has a single postsynaptic + neuron, but may have multiple release zones. It defines a release mechanism + with a NetCon input (triggering from presynaptic voltage or calcium level) + and either NetCon or pointer output for driving a PSD. + + """ + + def __init__(self, section): + """ + Parameters + ---------- + section : :obj:`NEURON section` + Set the section in the postsynaptic cell that the terminal is attached to. + + """ + self._section = section + + @property + def section(self): + """ The cell section this terminal is attached to. + """ + return self._section + + @property + def cell(self): + """ The cell this terminal is attached to. + """ + from ..cells import Cell + + return Cell.from_section(self.section) diff --git a/cnmodel/synapses/tests/test_data/dstellate_bushy.pk b/cnmodel/synapses/tests/test_data/dstellate_bushy.pk new file mode 100644 index 0000000..46ef8e8 --- /dev/null +++ b/cnmodel/synapses/tests/test_data/dstellate_bushy.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:770319a9229c66e4796757854b0d1e341f5d376abf0284385360788c41c6d20b +size 11836 diff --git a/cnmodel/synapses/tests/test_data/dstellate_dstellate.pk b/cnmodel/synapses/tests/test_data/dstellate_dstellate.pk new file mode 100644 index 0000000..e2dcb0f --- /dev/null +++ b/cnmodel/synapses/tests/test_data/dstellate_dstellate.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83554699cefe547a22eb959c83cf9a86d62383d858a1b8d78716eae16b382e43 +size 6110 diff --git a/cnmodel/synapses/tests/test_data/dstellate_tstellate.pk b/cnmodel/synapses/tests/test_data/dstellate_tstellate.pk new file mode 100644 index 0000000..27e3da3 --- /dev/null +++ b/cnmodel/synapses/tests/test_data/dstellate_tstellate.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c812143a8582d423c7fababd2060bbbaa064b5b767f576cd117685af207c944e +size 11421 diff --git a/cnmodel/synapses/tests/test_data/sgc_bushy.pk b/cnmodel/synapses/tests/test_data/sgc_bushy.pk new file mode 100644 index 0000000..dc01dde --- /dev/null +++ b/cnmodel/synapses/tests/test_data/sgc_bushy.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42d5eed880a498157400a9fdc442341e7e32c1ef1db8d398ac016e27fcbb6286 +size 42555 diff --git a/cnmodel/synapses/tests/test_data/sgc_dstellate.pk b/cnmodel/synapses/tests/test_data/sgc_dstellate.pk new file mode 100644 index 0000000..d9a0184 --- /dev/null +++ b/cnmodel/synapses/tests/test_data/sgc_dstellate.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6176f9cbf15f5f81d36a7cf64e0d9a72e0187a0d647642b838f72c0373a3349e +size 8867 diff --git a/cnmodel/synapses/tests/test_data/sgc_tstellate.pk b/cnmodel/synapses/tests/test_data/sgc_tstellate.pk new file mode 100644 index 0000000..bd3a9ab --- /dev/null +++ b/cnmodel/synapses/tests/test_data/sgc_tstellate.pk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e62835df05a9f859bbae3d48639bdb4456ca91a7ce62405f618e692494d5c92c +size 10586 diff --git a/cnmodel/synapses/tests/test_psd.py b/cnmodel/synapses/tests/test_psd.py new file mode 100644 index 0000000..06bedc5 --- /dev/null +++ b/cnmodel/synapses/tests/test_psd.py @@ -0,0 +1,190 @@ +# -*- encoding: utf-8 -*- +from __future__ import print_function +import sys +import numpy as np +import pyqtgraph as pg +import neuron + +import cnmodel +import cnmodel.cells as cells +from cnmodel.protocols import SynapseTest +from cnmodel.util import random_seed, reset +from cnmodel import data + +""" +Check that sgc PSDs have correct AMPA and NMDA peak conductances / CV. +""" + + +def test_sgc_bushy_psd(plot=False): + sgc_psd_test(cells.Bushy, seed=23572385, tstop=4.0, plot=plot) + + +def test_sgc_tstellate_psd(plot=False): + sgc_psd_test(cells.TStellate, seed=34754398, plot=plot) + + +def test_sgc_dstellate_psd(plot=False): + sgc_psd_test(cells.DStellate, seed=54743998, plot=plot, n_syn=50) + + +def test_sgc_octopus_psd(plot=False): + sgc_psd_test(cells.Octopus, seed=54743998, plot=plot, n_syn=50) + + +def sgc_psd_test(cell_class, seed, plot=False, tstop=5.0, n_syn=20): + """ + Tests a multisite synapse from the SGC to a target cell. + The values returned from an actual set of runs of the synapse are compared + to the expected values in the synapses.py table. This is needed because + the maximal open probability of the receptor models is not 1, so the maximal + conductance per receptor needs to be adjusted empirically. If the measured current + does not match the expected current, then we print a message with the expected value, + and fail with an assert statment in the test. + The measurement itself is made in measure_gmax(). + + Parameters + ---------- + cell_class : an instance of the cell class + seed : int + random number seed for the call + plot : boolean (default False) + plot request, passed to measure_gmax + tstop : float (default 5.0 ms) + duration of run for measurement of gmax. Needs to be long enough to find the + maximum of the EPSC/IPSC. + n_syn : int (default 20) + number of synapses to instantiate for testing (to get an average value) + + """ + celltyp = cell_class.__name__.lower() + + random_seed.set_seed(seed) + reset( + raiseError=False + ) # avoid failure because we cannot release NEURON objects completely. + tsc = cell_class.create(ttx=True) + (ampa_gmax, nmda_gmax, epsc_cv) = measure_gmax( + tsc, n_syn=n_syn, tstop=tstop, plot=plot + ) + exp_ampa_gmax = data.get( + "sgc_synapse", species="mouse", post_type=celltyp, field="AMPA_gmax" + )[0] + exp_nmda_gmax = data.get( + "sgc_synapse", species="mouse", post_type=celltyp, field="NMDA_gmax" + )[0] + exp_epsc_cv = data.get( + "sgc_synapse", species="mouse", post_type=celltyp, field="EPSC_cv" + ) + ampa_correct = np.allclose(exp_ampa_gmax, ampa_gmax) + if not ampa_correct: + AMPAR_gmax = data.get( + "sgc_synapse", species="mouse", post_type=celltyp, field="AMPAR_gmax" + ) + ratio = exp_ampa_gmax / ampa_gmax + print( + "AMPA Receptor conductance in model should be %.16f (table is %.16f)" + % (AMPAR_gmax * ratio, AMPAR_gmax) + ) + nmda_correct = np.allclose(exp_nmda_gmax, nmda_gmax) + if not nmda_correct: + NMDAR_gmax = data.get( + "sgc_synapse", species="mouse", post_type=celltyp, field="NMDAR_gmax" + ) + ratio = exp_nmda_gmax / nmda_gmax + print("ratio: ", ratio, exp_nmda_gmax, nmda_gmax) + print( + "NMDA Receptor conductance in model should be %.16f (table is %.16f)" + % (NMDAR_gmax * ratio, NMDAR_gmax) + ) + cv_correct = abs(exp_epsc_cv / epsc_cv - 1.0) < 0.1 + print("cv_correct: ", cv_correct) + if not cv_correct: + ratio = exp_epsc_cv / epsc_cv + print( + "CV Receptor in synapses.py model should be %.6f (measured = %.6f; table = %.6f)" + % (epsc_cv * ratio, epsc_cv, exp_epsc_cv) + ) + print((abs(exp_epsc_cv / (epsc_cv * ratio) - 1.0) < 0.1)) + assert cv_correct + assert ampa_correct and nmda_correct + + +def measure_gmax(cell, n_syn=20, tstop=5.0, plot=False): + sgc = cells.SGC.create() + prot = SynapseTest() + + # Connect 20 synapses and stimulate once each + # Temp is 33 C and vm=+40 to match Cao & Oertel 2010 + prot.run( + sgc.soma, + cell.soma, + n_synapses=n_syn, + temp=33.0, + dt=0.025, + vclamp=40.0, + tstop=tstop, + stim_params={"NP": 1, "delay": 0.1}, + ) + + # For each synapse: + # * Add up ampa and nmda conductances across all sites + # (although SGC-TS synapses currently have only 1 site) + # * Keep track of the maximum ampa and nmda conductance + if plot: + global plt + plt = pg.plot() + ampa_gmax = [] + nmda_gmax = [] + epsc_gmax = [] + for syn in prot.synapses: + ampa = np.zeros_like(syn.psd.get_vector("ampa", "g")) + nmda = ampa.copy() + ampa_po = ampa.copy() + for i in range(syn.psd.n_psd): + + ampa += ( + syn.psd.get_vector("ampa", "g", i) * 1e-3 + ) # convert pS from mechanism to nS + nmda += syn.psd.get_vector("nmda", "g", i) * 1e-3 + if nmda[-1] - nmda[-2] > 0.001: + raise Exception("Did not reach nmda gmax; need longer run.") + amax = ampa.max() + ampa_gmax.append(amax) + nmda_gmax.append(nmda.max()) + epsc_gmax.append((ampa + nmda).max()) + tb = np.linspace(0.0, len(ampa) * prot.dt, len(ampa)) + if plot: + plt.plot(tb, ampa, pen="g") + plt.plot(tb, nmda, pen="y") + return ( + np.mean(ampa_gmax), + np.mean(nmda_gmax), + np.std(epsc_gmax) / np.mean(epsc_gmax), + ) + + +if __name__ == "__main__": + if len(sys.argv[0]) > 1: + testcell = sys.argv[1] + if testcell not in ["bushy", "tstellate", "dstellate", "octopus", "all"]: + print("PSD test for cell type %s is not yet supported." % testcell) + exit(1) + else: + if testcell in ["bushy"]: + test_sgc_bushy_psd(plot=True) + if testcell in ["tstellate"]: + test_sgc_tstellate_psd(plot=True) + if testcell in ["dstellate"]: + test_sgc_dstellate_psd(plot=True) + if testcell in ["octopus"]: + test_sgc_octopus_psd(plot=True) + if testcell in ["all"]: + test_sgc_bushy_psd(plot=True) + test_sgc_tstellate_psd(plot=True) + test_sgc_dstellate_psd(plot=True) + test_sgc_octopus(plot=True) + + # pg.show() + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/cnmodel/synapses/tests/test_synapses.py b/cnmodel/synapses/tests/test_synapses.py new file mode 100644 index 0000000..dd8ab8e --- /dev/null +++ b/cnmodel/synapses/tests/test_synapses.py @@ -0,0 +1,119 @@ +""" +Create presynaptic and postsynaptic neurons, automatically connect them with +a synapse, stimulate the presynaptic cell, and analyze the resulting PSCs +in the postsynaptic cell. +""" +import faulthandler + +faulthandler.enable() +import os, pickle, pprint +import numpy as np +import neuron + +import cnmodel +import cnmodel.cells as cells +from cnmodel.util import UserTester +from cnmodel.protocols import SynapseTest +from cnmodel.util import reset + +# +# Synapse tests +# +def test_sgc_bushy(): + SynapseTester("sgc", "bushy") + + +def test_sgc_tstellate(): + SynapseTester("sgc", "tstellate") + + +def test_sgc_tstellate2(): # again to test RNG stability + SynapseTester("sgc", "tstellate") + + +def test_sgc_dstellate(): + SynapseTester("sgc", "dstellate") + + +def test_dstellate_bushy(): + SynapseTester("dstellate", "bushy") + + +def test_dstellate_tstellate(): + SynapseTester("dstellate", "tstellate") + + +def test_dstellate_dstellate(): + SynapseTester("dstellate", "dstellate") + + +# +# Supporting functions +# +convergence = { + "sgc": {"bushy": 3, "tstellate": 6, "dstellate": 10, "dstellate_eager": 10}, + "dstellate": {"bushy": 10, "tstellate": 15, "dstellate": 5}, +} + + +def make_cell(typ): + if typ == "sgc": + cell = cells.SGC.create() + elif typ == "tstellate": + cell = cells.TStellate.create(debug=True, ttx=False) + elif ( + typ == "dstellate" + ): # Type I-II Rothman model, similiar excitability (Xie/Manis, unpublished) + cell = cells.DStellate.create(model="RM03", debug=True, ttx=False) + elif typ == "dstellate_eager": # From Eager et al. + cell = cells.DStellate.create(model="Eager", debug=True, ttx=False) + elif typ == "bushy": + cell = cells.Bushy.create(debug=True, ttx=False) + else: + raise ValueError("Unknown cell type '%s'" % typ) + return cell + + +class SynapseTester(UserTester): + def __init__(self, pre, post): + self.st = None + UserTester.__init__(self, "%s_%s" % (pre, post), pre, post) + + def run_test(self, pre, post): + # Make sure no objects are left over from previous tests + reset(raiseError=False) + + # seed random generator using the name of this test + seed = "%s_%s" % (pre, post) + + pre_cell = make_cell(pre) + post_cell = make_cell(post) + + n_term = convergence.get(pre, {}).get(post, None) + if n_term is None: + n_term = 1 + st = SynapseTest() + st.run(pre_cell.soma, post_cell.soma, n_term, seed=seed) + if self.audit: + st.show_result() + + info = dict( + rel_events=st.release_events(), + rel_timings=st.release_timings(), + open_prob=st.open_probability(), + event_analysis=st.analyze_events(), + ) + self.st = st + + # import weakref + # global last_syn + # last_syn = weakref.ref(st.synapses[0].terminal.relsi) + + return info + + def assert_test_info(self, *args, **kwds): + try: + super(SynapseTester, self).assert_test_info(*args, **kwds) + finally: + if self.st is not None: + self.st.hide() diff --git a/cnmodel/util/Params.py b/cnmodel/util/Params.py new file mode 100755 index 0000000..7e4a6e2 --- /dev/null +++ b/cnmodel/util/Params.py @@ -0,0 +1,69 @@ +# -*- encoding: utf-8 -*- + +import sys +import os +import unittest + + +class Params(object): + def __init__(self, **kwds): + """ + Utility class to create parameter lists + create using: + p = Params(abc=2.0, defg = 3.0, lunch='sandwich') + reference using: + p.abc, p.defg, etc. + Supports getting the keys, finding whether a key exists, returning the strucure as a simple dictionary, + and printing (show) the parameter structure. + """ + self.__dict__.update(kwds) + + def additem(self, key, value): + self.__dict__[key] = value + + def getkeys(self): + """ + Get the keys in the current dictionary + """ + return self.__dict__.keys() + + def haskey(self, key): + """ + Find out if the param list has a specific key in it + """ + if key in self.__dict__.keys(): + return True + else: + return False + + def todict(self): + """ + convert param list to standard dictionary + Useful when writing the data + """ + r = {} + for dictelement in self.__dict__: + if isinstance(self.__dict__[dictelement], Params): + # print 'nested: ', dictelement + r[dictelement] = self.__dict__[dictelement].todict() + else: + r[dictelement] = self.__dict__[dictelement] + return r + + def show(self, printFlag=True): + """ + print the parameter block created in Parameter Init + """ + print("-------- Parameter Block ----------") + for key in self.__dict__.keys(): + print("%15s = " % (key), eval("self.%s" % key)) + print("-------- ---------------------- ----------") + + +class ParamTests(unittest.TestCase): + def setUp(self): + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/cnmodel/util/PlotHelpers.py b/cnmodel/util/PlotHelpers.py new file mode 100755 index 0000000..a4fcc89 --- /dev/null +++ b/cnmodel/util/PlotHelpers.py @@ -0,0 +1,1415 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +PlotHelpers.py + +Routines to help use matplotlib and make cleaner plots +as well as get plots ready for publication. + +Modified to allow us to use a list of axes, and operate on all of those, +or to use just one axis if that's all that is passed. +Therefore, the first argument to these calls can either be an axes object, +or a list of axes objects. 2/10/2012 pbm. + +Plotter class: a simple class for managing figures with multiple plots. +Uses gridspec to build sets of axes. + +Created by Paul Manis on 2010-03-09. +Copyright 2010-2016 Paul Manis +Distributed under MIT/X11 license. See license.txt for more infofmation. + +""" + +import sys +import os +import string +from collections import OrderedDict + +stdFont = "Arial" +from matplotlib.ticker import FormatStrFormatter +from matplotlib.font_manager import FontProperties +from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker +from scipy.stats import gaussian_kde +import numpy as np +import matplotlib.pyplot as mpl +import matplotlib.gridspec as gridspec +from matplotlib.patches import Circle +from matplotlib.patches import Rectangle +from matplotlib.patches import Ellipse +from matplotlib.collections import PatchCollection +import matplotlib + +rcParams = matplotlib.rcParams +rcParams["svg.fonttype"] = "none" # No text as paths. Assume font installed. +rcParams["pdf.fonttype"] = 42 +rcParams["ps.fonttype"] = 42 +# rcParams['font.serif'] = ['Times New Roman'] +from matplotlib import rc + +rc("font", **{"family": "sans-serif", "sans-serif": ["Arial"]}) +# rcParams['font.sans-serif'] = ['Arial'] +# rcParams['font.family'] = 'sans-serif' +# check for LaTeX install - +from distutils.spawn import find_executable + +latex_avail = False +if find_executable("latex"): + latex_avail = True +rc("text", usetex=latex_avail) +rcParams["text.latex.unicode"] = latex_avail + + +def _ax_tolist(ax): + if isinstance(ax, list): + return ax + elif isinstance(ax, dict): + axlist = list(axl.keys()) + return [ax for ax in axl[axlist]] + else: + return [ax] + + +def nice_plot( + axl, spines=["left", "bottom"], position=10, direction="inward", axesoff=False +): + """ Adjust a plot so that it looks nicer than the default matplotlib plot. + Also allow quickaccess to things we like to do for publication plots, including: + using a calbar instead of an axes: calbar = [x0, y0, xs, ys] + inserting a reference line (grey, 3pt dashed, 0.5pt, at refline = y position) + + Parameters + ---------- + axl : list of axes objects + If a single axis object is present, it will be converted to a list here. + + spines : list of strings (default : ['left', 'bottom']) + Sets whether spines will occur on particular axes. Choices are 'left', 'right', + 'bottom', and 'top'. Chosen spines will be displayed, others are not + + position : float (default : 10) + Determines position of spines in points, typically outward by x points. The + spines are the main axes lines, not the tick marks + if the position is dict, then interpret as such. + + direction : string (default : 'inward') + Sets the direction of spines. Choices are 'inward' and 'outward' + + axesoff : boolean (default : False) + If true, forces the axes to be turned completely off. + + Returns + ------- + Nothing. + """ + # print 'NICEPLOT' + if type(axl) is not list: + axl = [axl] + for ax in axl: + if ax is None: + continue + # print 'ax: ', ax + for loc, spine in ax.spines.items(): + if loc in spines: + spine.set_color("k") + # print 'spine color : k' + if type(position) in [int, float]: + spine.set_position(("axes", position)) + elif type(position) is dict: + spine.set_position(("axes", position[loc])) + else: + raise ValueError( + "position must be int, float or dict [ex: ]{'left': -0.05, 'bottom': -0.05}]" + ) + else: + spine.set_color("none") + # print 'spine color : none' + if axesoff is True: + noaxes(ax) + + # turn off ticks where there is no spine, if there are axes + if "left" in spines and not axesoff: + ax.yaxis.set_ticks_position("left") + ax.yaxis.set_tick_params(color="k") + else: + ax.yaxis.set_ticks([]) # no yaxis ticks + + if "bottom" in spines and not axesoff: + ax.xaxis.set_ticks_position("bottom") + ax.xaxis.set_tick_params(color="k") + else: + ax.xaxis.set_ticks([]) # no xaxis ticks + + if direction == "inward": + ax.tick_params(axis="y", direction="in") + ax.tick_params(axis="x", direction="in") + else: + ax.tick_params(axis="y", direction="out") + ax.tick_params(axis="x", direction="out") + + +def noaxes(axl, whichaxes="xy"): + """ take away all the axis ticks and the lines + + Parameters + ---------- + + axl : list of axes objects + If a single axis object is present, it will be converted to a list here. + + whichaxes : string (default : 'xy') + Sets which axes are turned off. The presence of an 'x' in + the string turns off x, the presence of 'y' turns off y. + + Returns + ------- + Nothing + """ + if type(axl) is not list: + axl = [axl] + for ax in axl: + if ax is None: + continue + if "x" in whichaxes: + ax.xaxis.set_ticks([]) + if "y" in whichaxes: + ax.yaxis.set_ticks([]) + if "xy" == whichaxes: + ax.set_axis_off() + + +def setY(ax1, ax2): + """ + Set the Y limits for an axes from a source axes to + the target axes. + + Parameters + ---------- + + ax1 : axis object + The source axis object + ax2 : list of axes objects + If a single axis object is present, it will be converted to a list here. + These are the target axes objects that will take on the limits of the source. + + Returns + ------- + Nothing + + """ + if type(ax1) is list: + print("PlotHelpers: cannot use list as source to set Y axis") + return + ax2 = _ax_tolist(ax2) + # if type(ax2) is not list: + # ax2 = [ax2] + refy = ax1.get_ylim() + for ax in ax2: + ax.set_ylim(refy) + + +def setX(ax1, ax2): + """ + Set the X limits for an axes from a source axes to + the target axes. + + Parameters + ---------- + + ax1 : axis object + The source axis object + ax2 : list of axes objects + If a single axis object is present, it will be converted to a list here. + These are the target axes objects that will take on the limits of the source. + + Returns + ------- + Nothing + + """ + if type(ax1) is list: + print("PlotHelpers: cannot use list as source to set Y axis") + return + ax2 = _ax_tolist(ax2) + # if type(ax2) is not list: + # ax2 = [ax2] + refx = ax1.get_xlim() + for ax in ax2: + ax.set_xlim(refx) + + +def labelPanels( + axl, + axlist=None, + font="Arial", + fontsize=18, + weight="normal", + xy=(-0.05, 1.05), + horizontalalignment="right", + verticalalignment="bottom", + rotation=0.0, +): + """ + Provide labeling of panels in a figure with multiple subplots (axes) + + Parameters + ---------- + axl : list of axes objects + If a single axis object is present, it will be converted to a list here. + + axlist : list of string labels (default : None) + Contains a list of the string labels. If the default value is provided, + the axes will be lettered in alphabetical sequence. + + font : string (default : 'Arial') + Name of a valid font to use for the panel labels + + fontsize : float (default : 18, in points) + Font size to use for axis labeling + + weight : string (default : 'normal') + Font weight to use for labels. 'Bold', 'Italic', and 'Normal' are options + + xy : tuple (default : (-0.05, 1.05)) + A tuple (x,y) indicating where the label should go relative to the axis frame. + Values are normalized as a fraction of the frame size. + + Returns + ------- + list of the annotations + + """ + if isinstance(axl, dict): + axlist = list(axl.keys()) + axl = _ax_tolist(axl) + # if isinstance(axl, dict): + # axt = [axl[x] for x in axl] + # axlist = axl.keys() + # axl = axt + # if not isinstance(axl, list): + # axl = [axl] + if axlist is None: + axlist = string.ascii_uppercase[0 : len(axl)] + # assume we wish to go in sequence + if len(axlist) > len(axl): + raise ValueError( + "axl must have more entries than axlist: got axl=%d and axlist=%d for axlist:" + % (len(axl), len(axlist)), + axlist, + ) + font = FontProperties() + font.set_family("sans-serif") + font.set_weight = weight + font.set_size = fontsize + font.set_style("normal") + labels = [] + for i, ax in enumerate(axl): + if i >= len(axlist): + continue + if ax is None: + continue + if isinstance(ax, list): + ax = ax[0] + ann = ax.annotate( + axlist[i], + xy=xy, + xycoords="axes fraction", + annotation_clip=False, + color="k", + verticalalignment=verticalalignment, + weight=weight, + horizontalalignment=horizontalalignment, + fontsize=fontsize, + family="sans-serif", + rotation=rotation, + ) + labels.append(ann) + return labels + + +def listAxes(axd): + """ + make a list of the axes from the dictionary + """ + if type(axd) is not dict: + if type(axd) is list: + return axd + else: + print("listAxes expects dictionary or list; type not known (fix the code)") + raise + axl = [axd[x] for x in axd] + return axl + + +def cleanAxes(axl): + axl = _ax_tolist(axl) + for ax in axl: + if ax is None: + continue + for loc, spine in ax.spines.items(): + if loc in ["left", "bottom"]: + spine.set_visible(True) + elif loc in ["right", "top"]: + spine.set_visible(False) + # spine.set_color('none') + # do not draw the spine + else: + raise ValueError("Unknown spine location: %s" % loc) + # turn off ticks when there is no spine + ax.xaxis.set_ticks_position("bottom") + # pdb.set_trace() + ax.yaxis.set_ticks_position("left") # stopped working in matplotlib 1.10 + update_font(ax) + + +def setTicks(axl, axis="x", ticks=np.arange(0, 1.1, 1.0)): + axl = _ax_tolist(axl) + # if type(axl) is dict: + # axl = [axl[x] for x in axl.keys()] + # if type(axl) is not list: + # axl = [axl] + for ax in axl: + if ax is None: + continue + if axis == "x": + ax.set_xticks(ticks) + if axis == "y": + ax.set_yticks(ticks) + + +def formatTicks(axl, axis="xy", fmt="%d", font="Arial"): + """ + Convert tick labels to integers + To do just one axis, set axis = 'x' or 'y' + Control the format with the formatting string + """ + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + majorFormatter = FormatStrFormatter(fmt) + for ax in axl: + if ax is None: + continue + if "x" in axis: + ax.xaxis.set_major_formatter(majorFormatter) + if "y" in axis: + ax.yaxis.set_major_formatter(majorFormatter) + + +def autoFormatTicks(axl, axis="xy", font="Arial"): + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + for ax in axl: + if ax is None: + continue + if "x" in axis: + # print ax.get_xlim() + x0, x1 = ax.get_xlim() + setFormatter(ax, x0, x1, axis="x") + if "y" in axis: + y0, y1 = ax.get_xlim + setFormatter(ax, y0, y1, axis="y") + + +def setFormatter(axl, x0, x1, axis="x"): + axl = _ax_tolist(axl) + datarange = np.abs(x0 - x1) + mdata = np.ceil(np.log10(datarange)) + if mdata > 0 and mdata <= 4: + majorFormatter = FormatStrFormatter("%d") + elif mdata > 4: + majorFormatter = FormatStrFormatter("%e") + elif mdata <= 0 and mdata > -1: + majorFormatter = FormatStrFormatter("%5.1f") + elif mdata < -1 and mdata > -3: + majorFormatatter = FormatStrFormatter("%6.3f") + else: + majorFormatter = FormatStrFormatter("%e") + for ax in axl: + if axis == "x": + ax.xaxis.set_major_formatter(majorFormatter) + elif axis == "y": + ax.yaxis.set_major_formatter(majorFormatter) + + +def update_font(axl, size=9, font=stdFont): + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + fontProperties = { + "family": "sans-serif", #'sans-serif': font, + "weight": "normal", + "size": size, + } + for ax in axl: + if ax is None: + continue + for tick in ax.xaxis.get_major_ticks(): + # tick.label1.set_family('sans-serif') + # tick.label1.set_fontname(stdFont) + tick.label1.set_size(size) + + for tick in ax.yaxis.get_major_ticks(): + # tick.label1.set_family('sans-serif') + # tick.label1.set_fontname(stdFont) + tick.label1.set_size(size) + ax.set_xticklabels(ax.get_xticks(), fontProperties) + ax.set_yticklabels(ax.get_yticks(), fontProperties) + ax.xaxis.set_smart_bounds(True) + ax.yaxis.set_smart_bounds(True) + ax.tick_params(axis="both", labelsize=size) + + +def lockPlot(axl, lims, ticks=None): + """ + This routine forces the plot of invisible data to force the axes to take certain + limits and to force the tick marks to appear. + call with the axis and lims (limits) = [x0, x1, y0, y1] + """ + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + plist = [] + for ax in axl: + if ax is None: + continue + lpl = ax.plot( + [lims[0], lims[0], lims[1], lims[1]], + [lims[2], lims[3], lims[2], lims[3]], + color="none", + marker="", + linestyle="None", + ) + plist.extend(lpl) + ax.axis(lims) + return plist # just in case you want to modify these plots later. + + +def adjust_spines( + axl, spines=["left", "bottom"], direction="outward", distance=5, smart=True +): + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + for ax in axl: + if ax is None: + continue + # turn off ticks where there is no spine + if "left" in spines: + ax.yaxis.set_ticks_position("left") + else: + # no yaxis ticks + ax.yaxis.set_ticks([]) + + if "bottom" in spines: + ax.xaxis.set_ticks_position("bottom") + else: + # no xaxis ticks + ax.xaxis.set_ticks([]) + for loc, spine in ax.spines.items(): + if loc in spines: + spine.set_position((direction, distance)) # outward by 10 points + if smart is True: + spine.set_smart_bounds(True) + else: + spine.set_smart_bounds(False) + else: + spine.set_color("none") # don't draw spine + + +def getLayoutDimensions(n, pref="height"): + """ + Return a tuple of optimized layout dimensions for n axes + + Parameters + ---------- + n : int (no default): + Number of plots needed + pref : string (default : 'height') + prefered way to organized the plots (height, or width) + + Returns + ------- + (h, w) : tuple + height (rows) and width (columns) + + """ + nopt = np.sqrt(n) + inoptw = int(nopt) + inopth = int(nopt) + while inoptw * inopth < n: + if pref == "width": + inoptw += 1 + if inoptw * inopth > (n - inopth): + inoptw -= 1 + inopth += 1 + else: + inopth += 1 + if inoptw * inopth > (n - inoptw): + inopth -= 1 + inoptw += 1 + + return (inopth, inoptw) + + +def calbar( + axl, + calbar=None, + axesoff=True, + orient="left", + unitNames=None, + fontsize=11, + weight="normal", + font="Arial", +): + """ + draw a calibration bar and label it. The calibration bar is defined as: + [x0, y0, xlen, ylen] + """ + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + for ax in axl: + if ax is None: + continue + if axesoff is True: + noaxes(ax) + Hfmt = r"{:.0f}" + if calbar[2] < 1.0: + Hfmt = r"{:.1f}" + Vfmt = r" {:.0f}" + if calbar[3] < 1.0: + Vfmt = r" {:.1f}" + if unitNames is not None: + Vfmt = Vfmt + r" " + r"{:s}".format(unitNames["y"]) + Hfmt = Hfmt + r" " + r"{:s}".format(unitNames["x"]) + # print(Vfmt, unitNames['y']) + # print(Vfmt.format(calbar[3])) + font = FontProperties() + font.set_family("sans-serif") + font.set_weight = weight + font.set_size = fontsize + font.set_style("normal") + if calbar is not None: + if orient == "left": # vertical part is on the left + ax.plot( + [calbar[0], calbar[0], calbar[0] + calbar[2]], + [calbar[1] + calbar[3], calbar[1], calbar[1]], + color="k", + linestyle="-", + linewidth=1.5, + ) + ax.text( + calbar[0] + 0.05 * calbar[2], + calbar[1] + 0.5 * calbar[3], + Vfmt.format(calbar[3]), + horizontalalignment="left", + verticalalignment="center", + fontsize=fontsize, + weight=weight, + family="sans-serif", + ) + elif orient == "right": # vertical part goes on the right + ax.plot( + [calbar[0] + calbar[2], calbar[0] + calbar[2], calbar[0]], + [calbar[1] + calbar[3], calbar[1], calbar[1]], + color="k", + linestyle="-", + linewidth=1.5, + ) + ax.text( + calbar[0] + calbar[2] - 0.05 * calbar[2], + calbar[1] + 0.5 * calbar[3], + Vfmt.format(calbar[3]), + horizontalalignment="right", + verticalalignment="center", + fontsize=fontsize, + weight=weight, + family="sans-serif", + ) + else: + print("PlotHelpers.py: I did not understand orientation: %s" % (orient)) + print("plotting as if set to left... ") + ax.plot( + [calbar[0], calbar[0], calbar[0] + calbar[2]], + [calbar[1] + calbar[3], calbar[1], calbar[1]], + color="k", + linestyle="-", + linewidth=1.5, + ) + ax.text( + calbar[0] + 0.05 * calbar[2], + calbar[1] + 0.5 * calbar[3], + Vfmt.format(calbar[3]), + horizontalalignment="left", + verticalalignment="center", + fontsize=fontsize, + weight=weight, + family="sans-serif", + ) + ax.text( + calbar[0] + calbar[2] * 0.5, + calbar[1] - 0.1 * calbar[3], + Hfmt.format(calbar[2]), + horizontalalignment="center", + verticalalignment="top", + fontsize=fontsize, + weight=weight, + family="sans-serif", + ) + + +def referenceline( + axl, + reference=None, + limits=None, + color="0.33", + linestyle="--", + linewidth=0.5, + dashes=None, +): + """ + draw a reference line at a particular level of the data on the y axis + returns the line object. + """ + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + if reference is None: + refeference = 0.0 + for ax in axl: + if ax is None: + continue + if limits is None or type(limits) is not list or len(limits) != 2: + xlims = ax.get_xlim() + else: + xlims = limits + rl, = ax.plot( + [xlims[0], xlims[1]], + [reference, reference], + color=color, + linestyle=linestyle, + linewidth=linewidth, + ) + if dashes is not None: + rl.set_dashes(dashes) + return rl + + +def crossAxes(axl, xyzero=[0.0, 0.0], limits=[None, None, None, None]): + """ + Make plot(s) with crossed axes at the data points set by xyzero, and optionally + set axes limits + """ + axl = _ax_tolist(axl) + # if type(axl) is not list: + # axl = [axl] + for ax in axl: + if ax is None: + continue + # ax.set_title('spines at data (1,2)') + # ax.plot(x,y) + ax.spines["left"].set_position(("data", xyzero[0])) + ax.spines["right"].set_color("none") + ax.spines["bottom"].set_position(("data", xyzero[1])) + ax.spines["top"].set_color("none") + ax.spines["left"].set_smart_bounds(True) + ax.spines["bottom"].set_smart_bounds(True) + ax.xaxis.set_ticks_position("bottom") + ax.yaxis.set_ticks_position("left") + if limits[0] is not None: + ax.set_xlim(left=limits[0], right=limits[2]) + ax.set_ylim(bottom=limits[1], top=limits[3]) + + +def violin_plot(ax, data, pos, bp=False, median=False): + """ + create violin plots on an axis + """ + dist = max(pos) - min(pos) + w = min(0.15 * max(dist, 1.0), 0.5) + for d, p in zip(data, pos): + k = gaussian_kde(d) # calculates the kernel density + m = k.dataset.min() # lower bound of violin + M = k.dataset.max() # upper bound of violin + x = np.arange(m, M, (M - m) / 100.0) # support for violin + v = k.evaluate(x) # violin profile (density curve) + v = v / v.max() * w # scaling the violin to the available space + ax.fill_betweenx(x, p, v + p, facecolor="y", alpha=0.3) + ax.fill_betweenx(x, p, -v + p, facecolor="y", alpha=0.3) + if median: + ax.plot([p - 0.5, p + 0.5], [np.median(d), np.median(d)], "-") + if bp: + bpf = ax.boxplot(data, notch=0, positions=pos, vert=1) + mpl.setp(bpf["boxes"], color="black") + mpl.setp(bpf["whiskers"], color="black", linestyle="-") + + +# # from somewhere on the web: + + +class NiceScale: + def __init__(self, minv, maxv): + self.maxTicks = 6 + self.tickSpacing = 0 + self.lst = 10 + self.niceMin = 0 + self.niceMax = 0 + self.minPoint = minv + self.maxPoint = maxv + self.calculate() + + def calculate(self): + self.lst = self.niceNum(self.maxPoint - self.minPoint, False) + self.tickSpacing = self.niceNum(self.lst / (self.maxTicks - 1), True) + self.niceMin = np.floor(self.minPoint / self.tickSpacing) * self.tickSpacing + self.niceMax = np.ceil(self.maxPoint / self.tickSpacing) * self.tickSpacing + + def niceNum(self, lst, rround): + self.lst = lst + exponent = 0 # exponent of range */ + fraction = 0 # fractional part of range */ + niceFraction = 0 # nice, rounded fraction */ + + exponent = np.floor(np.log10(self.lst)) + fraction = self.lst / np.power(10, exponent) + + if self.lst: + if fraction < 1.5: + niceFraction = 1 + elif fraction < 3: + niceFraction = 2 + elif fraction < 7: + niceFraction = 5 + else: + niceFraction = 10 + else: + if fraction <= 1: + niceFraction = 1 + elif fraction <= 2: + niceFraction = 2 + elif fraction <= 5: + niceFraction = 5 + else: + niceFraction = 10 + + return niceFraction * np.power(10, exponent) + + def setMinMaxPoints(self, minPoint, maxPoint): + self.minPoint = minPoint + self.maxPoint = maxPoint + self.calculate() + + def setMaxTicks(self, maxTicks): + self.maxTicks = maxTicks + self.calculate() + + +def circles(x, y, s, c="b", ax=None, vmin=None, vmax=None, **kwargs): + """ + Make a scatter of circles plot of x vs y, where x and y are sequence + like objects of the same lengths. The size of circles are in data scale. + + Parameters + ---------- + x,y : scalar or array_like, shape (n, ) + Input data + s : scalar or array_like, shape (n, ) + Radius of circle in data scale (ie. in data unit) + c : color or sequence of color, optional, default : 'b' + `c` can be a single color format string, or a sequence of color + specifications of length `N`, or a sequence of `N` numbers to be + mapped to colors using the `cmap` and `norm` specified via kwargs. + Note that `c` should not be a single numeric RGB or + RGBA sequence because that is indistinguishable from an array of + values to be colormapped. `c` can be a 2-D array in which the + rows are RGB or RGBA, however. + ax : Axes object, optional, default: None + Parent axes of the plot. It uses gca() if not specified. + vmin, vmax : scalar, optional, default: None + `vmin` and `vmax` are used in conjunction with `norm` to normalize + luminance data. If either are `None`, the min and max of the + color array is used. (Note if you pass a `norm` instance, your + settings for `vmin` and `vmax` will be ignored.) + + Returns + ------- + paths : `~matplotlib.collections.PathCollection` + + Other parameters + ---------------- + kwargs : `~matplotlib.collections.Collection` properties + eg. alpha, edgecolors, facecolors, linewidths, linestyles, norm, cmap + + Examples + -------- + a = np.arange(11) + circles(a, a, a*0.2, c=a, alpha=0.5, edgecolor='none') + + License + -------- + This code is under [The BSD 3-Clause License] + (http://opensource.org/licenses/BSD-3-Clause) + """ + + # import matplotlib.colors as colors + + if ax is None: + ax = mpl.gca() + + if isinstance(c, str): + color = c # ie. use colors.colorConverter.to_rgba_array(c) + else: + color = None # use cmap, norm after collection is created + kwargs.update(color=color) + + if np.isscalar(x): + patches = [Circle((x, y), s)] + elif np.isscalar(s): + patches = [Circle((x_, y_), s) for x_, y_ in zip(x, y)] + else: + patches = [Circle((x_, y_), s_) for x_, y_, s_ in zip(x, y, s)] + collection = PatchCollection(patches, **kwargs) + + if color is None: + collection.set_array(np.asarray(c)) + if vmin is not None or vmax is not None: + collection.set_clim(vmin, vmax) + + ax.add_collection(collection) + ax.autoscale_view() + return collection + + +def rectangles(x, y, sw, sh=None, c="b", ax=None, vmin=None, vmax=None, **kwargs): + """ + Make a scatter of squares plot of x vs y, where x and y are sequence + like objects of the same lengths. The size of sqares are in data scale. + + Parameters + ---------- + x,y : scalar or array_like, shape (n, ) + Input data + s : scalar or array_like, shape (n, ) + side of square in data scale (ie. in data unit) + c : color or sequence of color, optional, default : 'b' + `c` can be a single color format string, or a sequence of color + specifications of length `N`, or a sequence of `N` numbers to be + mapped to colors using the `cmap` and `norm` specified via kwargs. + Note that `c` should not be a single numeric RGB or + RGBA sequence because that is indistinguishable from an array of + values to be colormapped. `c` can be a 2-D array in which the + rows are RGB or RGBA, however. + ax : Axes object, optional, default: None + Parent axes of the plot. It uses gca() if not specified. + vmin, vmax : scalar, optional, default: None + `vmin` and `vmax` are used in conjunction with `norm` to normalize + luminance data. If either are `None`, the min and max of the + color array is used. (Note if you pass a `norm` instance, your + settings for `vmin` and `vmax` will be ignored.) + + Returns + ------- + paths : `~matplotlib.collections.PathCollection` + + Other parameters + ---------------- + kwargs : `~matplotlib.collections.Collection` properties + eg. alpha, edgecolors, facecolors, linewidths, linestyles, norm, cmap + + Examples + -------- + a = np.arange(11) + squaress(a, a, a*0.2, c=a, alpha=0.5, edgecolor='none') + + License + -------- + This code is under [The BSD 3-Clause License] + (http://opensource.org/licenses/BSD-3-Clause) + """ + # import matplotlib.colors as colors + + if ax is None: + ax = mpl.gca() + + if isinstance(c, str): + color = c # ie. use colors.colorConverter.to_rgba_array(c) + else: + color = None # use cmap, norm after collection is created + kwargs.update(color=color) + if sh is None: + sh = sw + x = x - sw / 2.0 # offset as position specified is "lower left corner" + y = y - sh / 2.0 + if np.isscalar(x): + patches = [Rectangle((x, y), sw, sh)] + elif np.isscalar(sw): + patches = [Rectangle((x_, y_), sw, sh) for x_, y_ in zip(x, y)] + else: + patches = [ + Rectangle((x_, y_), sw_, sh_) for x_, y_, sw_, sh_ in zip(x, y, sw, sh) + ] + collection = PatchCollection(patches, **kwargs) + + if color is None: + collection.set_array(np.asarray(c)) + if vmin is not None or vmax is not None: + collection.set_clim(vmin, vmax) + + ax.add_collection(collection) + ax.autoscale_view() + return collection + + +def show_figure_grid(fig, figx=10.0, figy=10.0): + """ + Create a background grid with major and minor lines like graph paper + if using default figx and figy, the grid will be in units of the + overall figure on a [0,1,0,1] grid + if figx and figy are in units of inches or cm, then the grid + will be on that scale. + + Figure grid is useful when building figures and placing labels + at absolute locations on the figure. + + Parameters + ---------- + + fig : Matplotlib figure handle (no default): + The figure to which the grid will be applied + + figx : float (default: 10.) + # of major lines along the X dimension + + figy : float (default: 10.) + # of major lines along the Y dimension + + """ + backGrid = fig.add_axes([0, 0, 1, 1], frameon=False) + backGrid.set_ylim(0.0, figy) + backGrid.set_xlim(0.0, figx) + backGrid.grid(True) + + backGrid.set_yticks(np.arange(0.0, figy + 0.01, 1.0)) + backGrid.set_yticks(np.arange(0.0, figy + 0.01, 0.1), minor=True) + backGrid.set_xticks(np.arange(0.0, figx + 0.01, 1.0)) + backGrid.set_xticks(np.arange(0.0, figx + 0.01, 0.1), minor=True) + # backGrid.get_xaxis().set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + # backGrid.get_yaxis().set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + backGrid.grid(b=True, which="major", color="g", alpha=0.6, linewidth=0.8) + backGrid.grid(b=True, which="minor", color="g", alpha=0.4, linewidth=0.2) + return backGrid + + +def hide_figure_grid(fig, grid): + grid.grid(False) + + +def delete_figure_grid(fig, grid): + mpl.delete(grid) + + +class Plotter: + """ + The Plotter class provides a simple convenience for plotting data in + an row x column array. + """ + + def __init__( + self, + rcshape=None, + axmap=None, + arrangement=None, + title=None, + label=False, + roworder=True, + refline=None, + figsize=(11, 8.5), + fontsize=10, + position=0, + labeloffset=[0.0, 0.0], + labelsize=12, + ): + """ + Create an instance of the plotter. Generates a new matplotlib figure, + and sets up an array of subplots as defined, initializes the counters + + Examples + -------- + Ex. 1: + One way to generate plots on a standard grid, uses gridspec to specify an axis map: + labels = ['A', 'B1', 'B2', 'C1', 'C2', 'D', 'E', 'F'] + gr = [(0, 4, 0, 1), (0, 3, 1, 2), (3, 4, 1, 2), (0, 3, 2, 3), (3, 4, 2, 3), (5, 8, 0, 1), (5, 8, 1, 2), (5, 8, 2, 3)] + axmap = OrderedDict(zip(labels, gr)) + P = PH.Plotter((8, 1), axmap=axmap, label=True, figsize=(8., 6.)) + PH.show_figure_grid(P.figure_handle) + + Ex. 2: + Place plots on defined locations on the page - no messing with gridspec or subplots. + For this version, we just generate N subplots with labels (used to tag each plot) + The "sizer" array then maps the tags to specific panel locations + # define positions for each panel in Figure coordinages (0, 1, 0, 1) + # you don't have to use an ordered dict for this, I just prefer it when debugging + sizer = OrderedDict([('A', [0.08, 0.22, 0.55, 0.4]), ('B1', [0.40, 0.25, 0.65, 0.3]), ('B2', [0.40, 0.25, 0.5, 0.1]), + ('C1', [0.72, 0.25, 0.65, 0.3]), ('C2', [0.72, 0.25, 0.5, 0.1]), + ('D', [0.08, 0.25, 0.1, 0.3]), ('E', [0.40, 0.25, 0.1, 0.3]), ('F', [0.72, 0.25, 0.1, 0.3]), + ]) # dict elements are [left, width, bottom, height] for the axes in the plot. + gr = [(a, a+1, 0, 1) for a in range(0, 8)] # just generate subplots - shape does not matter + axmap = OrderedDict(zip(sizer.keys(), gr)) + P = PH.Plotter((8, 1), axmap=axmap, label=True, figsize=(8., 6.)) + PH.show_figure_grid(P.figure_handle) + P.resize(sizer) # perform positioning magic + P.axdict['B1'] access the plot associated with panel B1 + + Parameters + ---------- + rcshape : a list or tuple: 2x1 (no default) + rcshape is an array [row, col] telling us how many rows and columns to build. + default defines a rectangular array r x c of plots + a dict : + None: expect axmap to provide the input... + + axmap : + list of gridspec slices (default : None) + define slices for the axes of a gridspec, allowing for non-rectangular arrangements + The list is defined as: + [(r1t, r1b, c1l, c1r), slice(r2, c2)] + where r1t is the top for row 1 in the grid, r1b is the bottom, etc... + When using this mode, the axarr returned is a 1-D list, as if r is all plots indexed, + and the number of columns is 1. The results match in order the list entered in axmap + + + + arrangement: Ordered Dict (default: None) + Arrangement allows the data to be plotted according to a logical arrangement + The dict keys are the names ("groups") for each column, and the elements are + string names for the entities in the groups + + title : string (default: None) + Provide a title for the entire plot + + label : Boolean (default: False) + If True, sets labels on panels + + roworder : Boolean (default: True) + Define whether labels run in row order first or column order first + + refline : float (default: None) + Define the position of a reference line to be used in all panels + + figsize : tuple (default : (11, 8.5)) + Figure size in inches. Default is for a landscape figure + + fontsize : points (default : 10) + Defines the size of the font to use for panel labels + + position : position of spines (0 means close, 0.05 means break out) + x, y spines.. + Returns + ------- + Nothing + """ + self.arrangement = arrangement + self.fontsize = fontsize + self.referenceLines = {} + self.figure_handle = mpl.figure(figsize=figsize) # create the figure + self.figure_handle.set_size_inches(figsize[0], figsize[1], forward=True) + self.axlabels = [] + self.axdict = ( + OrderedDict() + ) # make axis label dictionary for indirect access (better!) + if isinstance(fontsize, int): + fontsize = {"tick": fontsize, "label": fontsize, "panel": fontsize} + gridbuilt = False + # compute label offsets + p = [0.0, 0.0] + if label: + if type(labeloffset) is int: + p = [labeloffset, labeloffset] + elif type(labeloffset) is dict: + p = [position["left"], position["bottom"]] + elif type(labeloffset) in [list, tuple]: + p = labeloffset + else: + p = [0.0, 0.0] + + # build axes arrays + # 1. nxm grid + if isinstance(rcshape, list) or isinstance(rcshape, tuple): + rc = rcshape + gs = gridspec.GridSpec(rc[0], rc[1]) # define a grid using gridspec + # assign to axarr + self.axarr = np.empty( + shape=(rc[0], rc[1]), dtype=object + ) # use a numpy object array, indexing features + ix = 0 + for r in range(rc[0]): + for c in range(rc[1]): + self.axarr[r, c] = mpl.subplot(gs[ix]) + ix += 1 + gridbuilt = True + # 2. specified values - starts with Nx1 subplots, then reorganizes according to shape boxes + elif isinstance(rcshape, dict): # true for OrderedDict also + nplots = len(list(rcshape.keys())) + gs = gridspec.GridSpec(nplots, 1) + rc = (nplots, 1) + self.axarr = np.empty( + shape=(rc[0], rc[1]), dtype=object + ) # use a numpy object array, indexing features + ix = 0 + for r in range(rc[0]): + for c in range(rc[1]): + self.axarr[r, c] = mpl.subplot(gs[ix]) + ix += 1 + gridbuilt = True + for k, pk in enumerate(rcshape.keys()): + self.axdict[pk] = self.axarr[k, 0] + plo = labeloffset + self.axlabels = labelPanels( + self.axarr.tolist(), + axlist=list(rcshape.keys()), + xy=(-0.095 + plo[0], 0.95 + plo[1]), + fontsize=fontsize["panel"], + ) + self.resize(rcshape) + else: + raise ValueError("Input rcshape must be list/tuple or dict") + + # create sublots + if axmap is not None: + if isinstance(axmap, list) and not gridbuilt: + self.axarr = np.empty(shape=(len(axmap), 1), dtype=object) + for k, g in enumerate(axmap): + self.axarr[k,] = mpl.subplot(gs[g[0] : g[1], g[2] : g[3]]) + elif isinstance(axmap, dict) or isinstance( + axmap, OrderedDict + ): # keys are panel labels + if not gridbuilt: + self.axarr = np.empty( + shape=(len(list(axmap.keys())), 1), dtype=object + ) + na = np.prod(self.axarr.shape) # number of axes + for k, pk in enumerate(axmap.keys()): + g = axmap[pk] # get the gridspec info + if not gridbuilt: + self.axarr[k,] = mpl.subplot(gs[g[0] : g[1], g[2] : g[3]]) + self.axdict[pk] = self.axarr.ravel()[k] + else: + raise TypeError("Plotter in PlotHelpers: axmap must be a list or dict") + + if len(self.axdict) == 0: + for i, a in enumerate(self.axarr.flatten()): + label = string.ascii_uppercase[i] + self.axdict[label] = a + + if title is not None: + self.figure_handle.canvas.set_window_title(title) + self.figure_handle.suptitle(title) + self.nrows = self.axarr.shape[0] + if len(self.axarr.shape) > 1: + self.ncolumns = self.axarr.shape[1] + else: + self.ncolumns = 1 + self.row_counter = 0 + self.column_counter = 0 + for i in range(self.nrows): + for j in range(self.ncolumns): + self.axarr[i, j].spines["top"].set_visible(False) + self.axarr[i, j].get_xaxis().set_tick_params( + direction="out", width=0.8, length=4.0 + ) + self.axarr[i, j].get_yaxis().set_tick_params( + direction="out", width=0.8, length=4.0 + ) + self.axarr[i, j].tick_params( + axis="both", which="major", labelsize=fontsize["tick"] + ) + # if i < self.nrows-1: + # self.axarr[i, j].xaxis.set_major_formatter(mpl.NullFormatter()) + nice_plot(self.axarr[i, j], position=position) + if refline is not None: + self.referenceLines[self.axarr[i, j]] = referenceline( + self.axarr[i, j], reference=refline + ) + + if label: + if isinstance(axmap, dict) or isinstance( + axmap, OrderedDict + ): # in case predefined... + self.axlabels = labelPanels( + self.axarr.ravel().tolist(), + axlist=list(axmap.keys()), + xy=(-0.095 + p[0], 0.95 + p[1]), + fontsize=fontsize["panel"], + ) + return + self.axlist = [] + if roworder == True: + for i in range(self.nrows): + for j in range(self.ncolumns): + self.axlist.append(self.axarr[i, j]) + else: + for i in range(self.ncolumns): + for j in range(self.nrows): + self.axlist.append(self.axarr[j, i]) + + if self.nrows * self.ncolumns > 26: # handle large plot using "A1..." + ctxt = string.ascii_uppercase[0 : self.ncolumns] # columns are lettered + rtxt = [ + str(x + 1) for x in range(self.nrows) + ] # rows are numbered, starting at 1 + axl = [] + for i in range(self.nrows): + for j in range(self.ncolumns): + axl.append(ctxt[j] + rtxt[i]) + self.axlabels = labelPanels( + self.axlist, axlist=axl, xy=(-0.35 + p[0], 0.75) + ) + else: + self.axlabels = labelPanels( + self.axlist, xy=(-0.095 + p[0], 0.95 + p[1]) + ) + + def _next(self): + """ + Private function + _next gets the axis pointer to the next row, column index that is available + Only sets internal variables + """ + self.column_counter += 1 + if self.column_counter >= self.ncolumns: + self.row_counter += 1 + self.column_counter = 0 + if self.row_counter >= self.nrows: + raise ValueError( + "Call to get next row exceeds the number of rows requested initially: %d" + % self.nrows + ) + + def getaxis(self, group=None): + """ + getaxis gets the current row, column counter, and calls _next to increment the counter + (so that the next getaxis returns the next available axis pointer) + + Parameters + ---------- + group : string (default: None) + forces the current axis to be selected from text name of a "group" + + Returns + ------- + the current axis or the axis associated with a group + """ + + if group is None: + currentaxis = self.axarr[self.row_counter, self.column_counter] + self._next() # prepare for next call + else: + currentaxis = self.getRC(group) + + return currentaxis + + def getRC(self, group): + """ + Get the axis associated with a group + + Parameters + ---------- + group : string (default: None) + returns the matplotlib axis associated with a text name of a "group" + + Returns + ------- + The matplotlib axis associated with the group name, or None if no group by + that name exists in the arrangement + """ + + if self.arrangement is None: + raise ValueError("specifying a group requires an arrangment dictionary") + # look for the group label in the arrangement dicts + for c, colname in enumerate(self.arrangement.keys()): + if group in self.arrangement[colname]: + # print ('column name, column: ', colname, self.arrangement[colname]) + # print ('group: ', group) + r = self.arrangement[colname].index( + group + ) # get the row position this way + return self.axarr[r, c] + print(("Group {:s} not in the arrangement".format(group))) + return None + + sizer = { + "A": {"pos": [0.08, 0.22, 0.50, 0.4]}, + "B1": {"pos": [0.40, 0.25, 0.60, 0.3]}, + "B2": {"pos": [0.40, 0.25, 0.5, 0.1]}, + "C1": {"pos": [0.72, 0.25, 0.60, 0.3]}, + "C2": {"pos": [0.72, 0.25, 0.5, 0.1]}, + "D": {"pos": [0.08, 0.25, 0.1, 0.3]}, + "E": {"pos": [0.40, 0.25, 0.1, 0.3]}, + "F": {"pos": [0.72, 0.25, 0.1, 0.3]}, + } + + def resize(self, sizer): + """ + Resize the graphs in the array. + + Parameters + ---------- + sizer : dict (no default) + A dictionary with keys corresponding to the plot labels. + The values for each key are a list (or tuple) of [left, width, bottom, height] + for each panel in units of the graph [0, 1, 0, 1]. + + sizer = {'A': {'pos': [0.08, 0.22, 0.50, 0.4], 'labelpos': (x,y), 'noaxes': True}, 'B1': {'pos': [0.40, 0.25, 0.60, 0.3], 'labelpos': (x,y)}, + 'B2': {'pos': [0.40, 0.25, 0.5, 0.1],, 'labelpos': (x,y), 'noaxes': False}, + 'C1': {'pos': [0.72, 0.25, 0.60, 0.3], 'labelpos': (x,y)}, 'C2': {'pos': [0.72, 0.25, 0.5, 0.1], 'labelpos': (x,y)}, + 'D': {'pos': [0.08, 0.25, 0.1, 0.3], 'labelpos': (x,y)}, + 'E': {'pos': [0.40, 0.25, 0.1, 0.3], 'labelpos': (x,y)}, 'F': {'pos': [0.72, 0.25, 0.1, 0.3],, 'labelpos': (x,y)} + } + Returns + ------- + Nothing + """ + + for i, s in enumerate(sizer.keys()): + ax = self.axdict[s] + bbox = ax.get_position() + bbox.x0 = sizer[s]["pos"][0] + bbox.x1 = sizer[s]["pos"][1] + sizer[s]["pos"][0] + bbox.y0 = sizer[s]["pos"][2] + bbox.y1 = ( + sizer[s]["pos"][3] + sizer[s]["pos"][2] + ) # offsets are in figure fractions + ax.set_position(bbox) + if "labelpos" in list(sizer[s].keys()) and len(sizer[s]["labelpos"]) == 2: + x, y = sizer[s]["labelpos"] + self.axlabels[i].set_x(x) + self.axlabels[i].set_y(y) + if "noaxes" in sizer[s] and sizer[s]["noaxes"] == True: + noaxes(ax) + + +if __name__ == "__main__": + # P = Plotter((3,3), axmap=[(0, 1, 0, 3), (1, 2, 0, 2), (2, 1, 2, 3), (2, 3, 0, 1), (2, 3, 1, 2)]) + labels = ["A", "B", "C", "D", "E", "F", "G", "H", "I"] + l = [(a, a + 2, 0, 1) for a in range(0, 6, 2)] + r = [(a, a + 1, 1, 2) for a in range(0, 6)] + axmap = OrderedDict(list(zip(labels, l + r))) + P = Plotter((6, 2), axmap=axmap, figsize=(6.0, 6.0), label=True) + # P = Plotter((2,3), label=True) # create a figure with plots + # for a in P.axarr.flatten(): + # a.plot(np.random.random(10), np.random.random(10)) + + # hfig, ax = mpl.subplots(2, 3) + axd = OrderedDict() + for i, a in enumerate(P.axarr.flatten()): + label = string.ascii_uppercase[i] + axd[label] = a + for a in list(axd.keys()): + axd[a].plot(np.random.random(10), np.random.random(10)) + nice_plot([axd[a] for a in axd], position=-0.1) + cleanAxes([axd["B"], axd["C"]]) + calbar([axd["B"], axd["C"]], calbar=[0.5, 0.5, 0.2, 0.2]) + # labelPanels([axd[a] for a in axd], axd.keys()) + # mpl.tight_layout(pad=2, w_pad=0.5, h_pad=2.0) + mpl.show() diff --git a/cnmodel/util/__init__.py b/cnmodel/util/__init__.py new file mode 100644 index 0000000..ebcbd71 --- /dev/null +++ b/cnmodel/util/__init__.py @@ -0,0 +1,9 @@ +from .stim import * +from .find_point import * +from .pynrnutilities import * +from .nrnutils import * +from .expfitting import * +from .user_tester import UserTester +from .get_anspikes import * +from .Params import * +from .talbotetalTicks import Extended diff --git a/cnmodel/util/ccstim.py b/cnmodel/util/ccstim.py new file mode 100644 index 0000000..ccc96f2 --- /dev/null +++ b/cnmodel/util/ccstim.py @@ -0,0 +1,125 @@ +__author__ = "pbmanis" +""" +ccstim +Generate current-clamp (or voltage-clamp) stimulus waveforms from a dictionary +used for vectory play modes in current clamp +(prior version was called 'makestim') + +Can generate several types of pulses + +""" + +import numpy as np + + +def ccstim(stim, dt, pulsetype="square"): + """ + Create stimulus pulse waveforms of different types. + + Parameters + ---------- + stim : dict + a dictionary with keys [required] + delay (delay to start of pulse train, msec [all] + duration: duration of pulses in train, msec [all] + Sfreq: stimulus train frequency (Hz) [timedSpikes] + PT: post-train test delay [all] + NP: number of pulses in the train [timedSpikes, exp] + amp: amplitude of the pulses in the train [all] + hypamp: amplitude of prehyperpolarizing pulse [hyp] + hypdur: duration of prehyperpolarizing pulse [hyp] + spikeTimes" times of spikes [timedSpikes] + + dt : time (microseconds) [required] + step time, in microseconds. Required parameter + + pulsetype : string (default: 'square') + Type of pulse to generate: one of square, hyp, timedspikes or exp + square produces a train of "square" (retangular) pulses levels 0 and ampitude + hyp is like square, but precedes the pulse train with a single prepulse + of hypamp and hypdur + timedspikes is like square, excpet the pulses are generated at times specified + in the spikeTimes key in the stim dictionary + exp: pulses with an exponential decay. + + TO DO: + add pulsetypes, including sine wave, rectified sine wave, etc. + + Returns + ------- + list containing [waveform (numpy array), + maxtime(float), + timebase (numpy array)] + """ + + assert dt is not None + assert "delay" in stim.keys() + delay = int(np.floor(stim["delay"] / dt)) + if pulsetype in ["square", "hyp", "exp"]: + ipi = int(np.floor((1000.0 / stim["Sfreq"]) / dt)) + pdur = int(np.floor(stim["duration"] / dt)) + posttest = int(np.floor(stim["PT"] / dt)) + if pulsetype not in ["timedSpikes"]: + NP = int(stim["NP"]) + else: + NP = len(stim["spikeTimes"]) + tstims = [0] * NP + if pulsetype == "hyp": + assert "hypamp" in stim.keys() + assert "hypdur" in stim.keys() + hypdur = int(np.floor(stim["hypdur"] / dt)) + delay0 = delay # save original delay + + if pulsetype in ["square", "hyp"]: + maxt = dt * (stim["delay"] + (ipi * (NP + 2)) + posttest + pdur * 2) + if pulsetype == "hyp": + maxt = maxt + dt * stim["hypdur"] + delay = delay + hypdur + w = np.zeros(int(np.floor(maxt / dt))) + for j in range(0, NP): + t = (delay + j * ipi) * dt + w[delay + ipi * j : delay + (ipi * j) + pdur] = stim["amp"] + tstims[j] = delay + ipi * j + if stim["PT"] > 0.0: + send = delay + ipi * j + for i in range(send + posttest, send + posttest + pdur): + w[i] = stim["amp"] + if pulsetype == "hyp": # fill in the prepulse now + for i in range(delay0, delay0 + hypdur): + w[i] = stim["hypamp"] + + if pulsetype == "timedSpikes": + maxt = np.max(stim["spikeTimes"]) + stim["PT"] + stim["duration"] * 2 + w = np.zeros(int(np.floor(maxt / dt))) + for j in range(len(stim["spikeTimes"])): + st = delay + int(np.floor(stim["spikeTimes"][j] / dt)) + t = st * dt + w[st : st + pdur] = stim["amp"] + tstims[j] = st + + if stim["PT"] > 0.0: + for i in range(st + posttest, st + posttest + pdur): + w[i] = stim["amp"] + + if pulsetype == "exp": + maxt = dt * (stim["delay"] + (ipi * (NP + 2)) + posttest + pdur * 2) + w = np.zeros(int(np.floor(maxt / dt))) + + for j in range(0, NP): + for i in range(0, len(w)): + if delay + ipi * j + i < len(w): + w[delay + ipi * j + i] = w[delay + ipi * j + i] + stim["amp"] * ( + (1.0 - np.exp(-i / (pdur / 3.0))) + * np.exp(-(i - (pdur / 3.0)) / pdur) + ) + tstims[j] = delay + ipi * j + if stim["PT"] > 0.0: + send = delay + ipi * j + for i in range(send + posttest, len(w)): + w[i] += ( + stim["amp"] + * (1.0 - np.exp(-i / (pdur / 3.0))) + * np.exp(-(i - (pdur / 3.0)) / pdur) + ) + + return (w, maxt, tstims) diff --git a/cnmodel/util/compare_simple_multisynapses.py b/cnmodel/util/compare_simple_multisynapses.py new file mode 100644 index 0000000..6f0f3a1 --- /dev/null +++ b/cnmodel/util/compare_simple_multisynapses.py @@ -0,0 +1,658 @@ +""" +Test synaptic connections between two different cell types. + +Usage: python compare_simple_multisynapses.py + +This script: + +1. Creates single pre- and postsynaptic cells +2. Creates a single synaptic terminal between the two cells, using the multisite synapse method. +3. Stimulates the presynaptic cell by current injection. +4. Records and analyzes the resulting post-synaptic events. +5. Repeats 3, 4 100 times to get an average postsynaptic event. +6. stores the resulting waveform in a pandas database + +This is used mainly to check that the strength, kinetics, and dynamics of +each synapse type is working as expected. + +Requires Python 3.6 +""" +import sys +import argparse +from pathlib import Path +import numpy as np + +# import pyqtgraph as pg +import matplotlib.pyplot as mpl +import neuron as h +import cnmodel.util.PlotHelpers as PH +from cnmodel.protocols import SynapseTest +from cnmodel import cells +from cnmodel.synapses import Synapse +import pickle +import lmfit + + +convergence = { + "sgc": { + "bushy": 1, + "tstellate": 1, + "dstellate": 1, + "octopus": 1, + "pyramidal": 1, + "tuberculoventral": 1, + }, + "dstellate": { + "bushy": 1, + "tstellate": 1, + "dstellate": 1, + "pyramidal": 1, + "tuberculoventral": 1, + }, + "tuberculoventral": { + "bushy": 1, + "tstellate": 1, + "pyramidal": 1, + "tuberculoventral": 1, + }, +} + + +class Exp2SynFitting: + """ + Fit waveform against Exp2SYN function (used by Neuron) + THe function is (from the documentation): + i = G * (v - e) i(nanoamps), g(micromhos); + G = weight * factor * (exp(-t/tau2) - exp(-t/tau1)) + WHere factor is evaluated when initializing(in neuron; see exp2syn.mod source) as: + tp = (tau1*tau2)/(tau2 - tau1) * log(tau2/tau1) (log10) + factor = -exp(-tp/tau1) + exp(-tp/tau2) + factor = 1/factor + + Parameters + ---------- + initpars : dict + dict of initial parameters. For example: {'tau1': 0.1, + 'tau2': 0.3, 'weight': 0.1, 'delay' : 0.0, 'erev': -80.} (erev in mV) + bounds : dict + dictionary of bounds for each parameter, with a list of lower and upper values. + + """ + + def __init__(self, initpars=None, bounds=None, functype="exp2syn"): + self.fitpars = lmfit.Parameters() + if functype == "exp2syn": # future handle other functions like alpha + # (Name, Value, Vary, Min, Max, Expr) + self.fitpars.add_many( + ("tau1", initpars["tau1"], True, 0.05, 25.0, None), + # ('tau2', initpars['tau2'], True, 0.1, 50., None), + ("tauratio", initpars["tauratio"], True, 1.0001, 100.0, None), + ("weight", initpars["weight"], True, 1e-6, 1, None), + ("erev", initpars["erev"], False), # do not adjust! + ("v", initpars["v"], False), + ("delay", initpars["delay"], True, 0.0, 5.0, None), + ) + self.func = self.exp2syn_err + else: + raise ValueError + + def fit(self, x, y, p, verbose=False): + """ + Perform the curve fit against the specified function + + Parameters + ---------- + x : time base for waveform (np array or list) + y : waveform (1-d np array or list) + p : parameters in lmfit Parameters structure + verbose : boolean (default: False) + If true, print the parameters in a nice format + """ + + kws = {"maxfev": 5000} + # print('p: ', p) + self.mim = lmfit.minimize( + self.func, p, method="least_squares", args=(x, y) + ) # , kws=kws) + if verbose: + lmfit.printfuncs.report_fit(self.mim.params) + fitpars = self.mim.params + return fitpars + + # @staticmethod + def exp2syn(self, x, tau1, tauratio, weight, erev, v, delay): + """ + Compute the exp2syn waveform current as it is done in Neuron + + Note on units: + The units are assumed to be "self consistent" + Thus, if the time base is in msec, tau1, tau2 and the delay are + in msec. erev and v should be in matching units (e.g., mV) + Note thtat the weight is not equal to the conducdtance G, because + of the scaling of the waveform + + Parameters + ---------- + x : time base + tau1 : rising tau for waveform + tau2 : falling tau for waveform + weight : amplitude of the waveform (conductance) + erev : reversal potential for ionic species (used to compute i) + v : holding voltage at which waveform is computed + delay : delay to start of function + + Returns + ------- + i, the calucated current trace for these parameters. + """ + # we handle the requirement that tau2 > tau1 by setting the + # expression in lmfit, and using tauratio rather than a direct + # tau2. + # if tau1/tau2 > 1.0: # make sure tau1 is less than tau2 + # tau1 = 0.999*tau2 + tau2 = tauratio * tau1 + tp = (tau1 * tau2) / (tau2 - tau1) * np.log(tau2 / tau1) + factor = -np.exp(-tp / tau1) + np.exp(-tp / tau2) + factor = 1.0 / factor + G = ( + weight + * factor + * (np.exp(-(x - delay) / tau2) - np.exp(-(x - delay) / tau1)) + ) + G[x - delay < 0] = 0.0 + i = G * (v - erev) # i(nanoamps), g(micromhos); + + return i + + def exp2syn_err(self, p, x, y): + return np.fabs( + y - self.exp2syn(x, **dict([(k, p.value) for k, p in p.items()])) + ) + + def factor(self, tau1, tauratio, weight): + """ + calculate tau-scaled weight + """ + tau2 = tau1 * tauratio + tp = (tau1 * tau2) / (tau2 - tau1) * np.log(tau2 / tau1) + factor = -np.exp(-tp / tau1) + np.exp(-tp / tau2) + factor = 1.0 / factor + G = weight * factor + return G + + +def testexp(): + """ + Test the exp2syn fitting function + """ + pars = { + "tau1": 0.1, + "tauratio": 2.0, + "weight": 0.1, + "erev": -70.0, + "v": -65.0, + "delay": 1, + } + F = Exp2SynFitting( + initpars={ + "tau1": 0.2, + "tauratio": 2.0, + "weight": 0.1, + "erev": -70.0, + "v": -65.0, + "delay": 0, + } + ) + t = np.arange(0, 10.0, 0.01) + p = F.fitpars + target = F.exp2syn( + t, + pars["tau1"], + pars["tauratio"], + pars["weight"], + pars["erev"], + pars["v"], + pars["delay"], + ) + # print(F.fitpars) + pars_fit = F.fit(t, target, F.fitpars) + print("\nTest fit result: ") + lmfit.printfuncs.report_fit(F.mim.params) + print("( tau2 = ", pars_fit["tau1"].value * pars_fit["tauratio"].value, ")") + print("target parameters: ", pars) + print("\n") + + +def compute_psc(synapsetype="multisite", celltypes=["sgc", "tstellate"]): + """ + Compute the PSC between the specified two cell tpes + The type of PSC is set by synspase type and must be 'multisite' or 'simple' + """ + assert synapsetype in ["multisite", "simple"] + + c = [] + for cellType in celltypes: + if cellType == "sgc": + cell = cells.SGC.create() + elif cellType == "tstellate": + cell = cells.TStellate.create(debug=True, ttx=False) + elif ( + cellType == "dstellate" + ): # Type I-II Rothman model, similiar excitability (Xie/Manis, unpublished) + cell = cells.DStellate.create(model="RM03", debug=True, ttx=False) + # elif cellType == 'dstellate_eager': # From Eager et al. + # cell = cells.DStellate.create(model='Eager', debug=True, ttx=False) + elif cellType == "bushy": + cell = cells.Bushy.create(debug=True, ttx=True) + elif cellType == "octopus": + cell = cells.Octopus.create(debug=True, ttx=True) + elif cellType == "tuberculoventral": + cell = cells.Tuberculoventral.create(debug=True, ttx=True) + elif cellType == "pyramidal": + cell = cells.Pyramidal.create(debug=True, ttx=True) + elif cellType == "cartwheel": + cell = cells.Cartwheel.create(debug=True, ttx=True) + else: + raise ValueError("Unknown cell type '%s'" % cellType) + c.append(cell) + + preCell, postCell = c + + print(f"Computing psc for connection {celltypes[0]:s} -> {celltypes[1]:s}") + nTerminals = convergence.get(celltypes[0], {}).get(celltypes[1], None) + if nTerminals is None: + nTerminals = 1 + print( + f"Warning: Unknown convergence for {celltypes[0]:s} -> {celltypes[1]:s}, ASSUMING {nTerminals:d} terminals" + ) + + if celltypes == ["sgc", "bushy"]: + niter = 50 + else: + niter = 200 + assert synapsetype in ["simple", "multisite"] + if synapsetype == "simple": + niter = 1 + st = SynapseTest() + dt = 0.010 + stim = { + "NP": 1, + "Sfreq": 100.0, + "delay": 0.0, + "dur": 0.5, + "amp": 10.0, + "PT": 0.0, + "dt": dt, + } + st.run( + preCell.soma, + postCell.soma, + nTerminals, + dt=dt, + vclamp=-65.0, + iterations=niter, + synapsetype=synapsetype, + tstop=50.0, + stim_params=stim, + ) + # st.show_result() # pyqtgraph plotting - + # st.plots['VPre'].setYRange(-70., 10.) + # st.plots['EPSC'].setYRange(-2.0, 0.5) + # st.plots['latency2080'].setYRange(0., 1.0) + # st.plots['halfwidth'].setYRange(0., 1.0) + # st.plots['RT'].setYRange(0., 0.2) + # st.plots['latency'].setYRange(0., 1.0) + # st.plots['latency_distribution'].setYRange(0., 1.0) + return st # need to keep st alive in memory + + +def fit_one(st, stk): + """ + Fit one trace to the exp2syn function + + Parameters + ---------- + + st : dict (no default) + A dictionary containing (at least) the following keys: + 't' : the time base for the trace to be fit + 'i' : a list of arrays or a 2d numpy array of the data to be fit + + Returns + ------- + pars : The fitting parameters + The fitted parameters are returned as an lmfit Parameters object, so access + individual parameters a pars['parname'].value + + fitted : The fitted waveform, of the same length as the source data waveform + """ + + if stk[0] in ["sgc", "tstellate", "granule"]: + erev = ( + 0.0 + ) # set erev according to the source cell (excitatory: 0, inhibitory: -80) + else: + erev = -70.0 # value used in gly_psd + print("\nstk, erev: ", stk, erev) + F = Exp2SynFitting( + initpars={ + "tau1": 1.0, + "tauratio": 5.0, + "weight": 0.0001, + "erev": erev, + "v": -65.0, + "delay": 0, + } + ) + t = st["t"] + p = F.fitpars + target = np.mean(np.array(st["i"]), axis=0) + # print(F.fitpars) + pars = F.fit(t, target, F.fitpars) + gw = F.factor(pars["tau1"].value, pars["tauratio"].value, pars["weight"].value) + pars.add("GWeight", value=gw, vary=False) + fitted = F.exp2syn( + st["t"], + pars["tau1"], + pars["tauratio"], + pars["weight"], + pars["erev"], + pars["v"], + pars["delay"], + ) + lmfit.printfuncs.report_fit(F.mim.params) + return (pars, fitted) + + +def fit_all(): + """ + Fit exp2syn against the traces in stm + This fits all pre-post cell pairs, and returns the fit + + """ + stm = read_pickle("multisite.pkl") + fits = {} + fitted = {} + # print('stm keys: ', stm.keys()) + # exit() + for i, stk in enumerate(stm.keys()): # for each pre-post cell pair + fitp, fit = fit_one(stm[stk], stk) + fits[stk] = fitp + + fitted[stk] = {"t": stm[stk]["t"], "i": [fit], "pars": fitp} + with (open("simple.pkl", "wb")) as fh: + pickle.dump(fitted, fh) + return fits + + +def plot_all(stm, sts=None): + P = PH.Plotter((3, 5), figsize=(11, 6)) + ax = P.axarr.ravel() + keypairorder = [] + for i, stk in enumerate(stm.keys()): + keypairorder.append(stk) + data = stm[stk] + idat = np.array(data["i"]) + ax[i].plot(np.array(data["t"]), np.mean(idat, axis=0), "c-", linewidth=1.5) + sd = np.std(idat, axis=0) + ax[i].plot( + np.array(data["t"]), np.mean(idat, axis=0) + sd, "c--", linewidth=0.5 + ) # for j in range(idat.shape[0]): + ax[i].plot( + np.array(data["t"]), np.mean(idat, axis=0) - sd, "c--", linewidth=0.5 + ) # for j in range(idat.shape[0]): + rel = 0 + for j in range(idat.shape[0]): + if np.min(idat[j, :]) < -1e-2 or np.max(idat[j, :]) > 1e-2: + rel += 1 + print(f"{str(stk):s} {rel:d} of {idat.shape[0]:d} {str(idat.shape):s}") + + # ax[i].plot(data['t'], idat[j], 'k-', linewidth=0.5, alpha=0.25) + ax[i].set_title("%s : %s" % (stk[0], stk[1]), fontsize=7) + + if sts is not None: # plot the matching exp2syn on this + for i, stks in enumerate(keypairorder): + datas = sts[stks] + isdat = np.array(datas["i"]) + ax[i].plot(datas["t"], np.mean(isdat, axis=0), "m-", linewidth=3, alpha=0.5) + mpl.show() + + +def print_exp2syn_fits(st): + stkeys = list(st.keys()) + postcells = [ + "bushy", + "tstellate", + "dstellate", + "octopus", + "pyramidal", + "tuberculoventral", + "cartwheel", + ] + fmts = { + "weight": "{0:<18.6f}", + "GWeight": "{0:<18.6f}", + "tau1": "{0:<18.3f}", + "tau2": "{0:<18.3f}", + "delay": "{0:<18.3f}", + "erev": "{0:<18.1f}", + } + pars = list(fmts.keys()) + for pre in ["sgc", "dstellate", "tuberculoventral"]: + firstline = False + vrow = dict.fromkeys(pars, False) + for v in pars: + for post in postcells: + if (pre, post) not in stkeys: + print("{0:<18s}".format("0"), end="") + continue + if not firstline: + print("\n%s" % pre.upper()) + print("{0:<18s}".format(" "), end="") + for i in range(len(postcells)): + print("{0:<18s}".format(postcells[i]), end="") + print() + firstline = True + if firstline: + if not vrow[v]: + if v == "tauratio": + print("{0:<18s}".format("tau2"), end="") + else: + print("{0:<18s}".format(v), end="") + vrow[v] = True + # print(fits[(pre, post)].keys()) + fits = st[(pre, post)]["pars"] + # print(v, fits) + if v in ["tauratio", "tau2"]: + print( + fmts[v].format(fits["tau1"].value * fits["tauratio"].value), + end="", + ) + else: + print(fmts[v].format(fits[v].value), end="") + print() + + +def read_pickle(mode): + """ + Read the pickled file generated by runall + + Parameters + ---------- + mode : str + either 'simple' or 'multisite' + + Returns: + the resulting data, which is a dictionary + """ + with (open(Path(mode).with_suffix(".pkl"), "rb")) as fh: + st = pickle.load(fh) + return st + + +def run_all(): + """ + Run all of the multisite synapse calculations for each cell pair + Save the results in the multisite.pkl file + """ + st = {} + pkst = {} + for pre in convergence.keys(): + for post in convergence[pre]: + # if pre == 'sgc' and post in ['bushy', 'tstellate']: + sti = compute_psc(synapsetype="multisite", celltypes=[pre, post]) + st[(pre, post)] = sti + # remove neuron objects before pickling + pkst[(pre, post)] = { + "t": sti["t"], + "i": sti.isoma, + "v": sti["v_soma"], + "pre": sti["v_pre"], + } + + with (open(Path("multisite").with_suffix(".pkl"), "wb")) as fh: + pickle.dump(pkst, fh) + plot_all(pkst) + + +def run_one(pre, post): + """ + Run all of the multisite synapse calculations for each cell pair + Save the results in the multisite.pkl file + """ + assert pre in ["sgc", "tstellate", "dstellate", "tuberculoventral"] + assert post in ["bushy", "tstellate", "dstellate", "tuberculoventral", "pyramidal"] + st = {} + pkst = read_pickle("multisite") + # if pre == 'sgc' and post in ['bushy', 'tstellate']: + sti = compute_psc(synapsetype="multisite", celltypes=[pre, post]) + st[(pre, post)] = sti + # remove neuron objects before pickling + pkst[(pre, post)] = { + "t": sti["t"], + "i": sti.isoma, + "v": sti["v_soma"], + "pre": sti["v_pre"], + } + + with (open(Path("multisite").with_suffix(".pkl"), "wb")) as fh: + pickle.dump(pkst, fh) + plot_all(pkst) + + +def main(): + parser = argparse.ArgumentParser( + description="Compare simple and multisite synapses" + ) + parser.add_argument( + "-m", + "--mode", + type=str, + dest="mode", + default="None", + choices=["simple", "multisite", "both"], + help="Select mode [simple, multisite, compare]", + ) + parser.add_argument( + "-r", + "--run", + action="store_true", + dest="run", + help="Run multisite models between all cell types", + ) + parser.add_argument( + "-o", + "--runone", + action="store_true", + dest="runone", + help="Run multisite models between specified cell types", + ) + parser.add_argument( + "--pre", + type=str, + dest="pre", + default="None", + help="Select presynaptic cell for runone", + ) + parser.add_argument( + "--post", + type=str, + dest="post", + default="None", + help="Select postsynaptic cell for runone", + ) + + parser.add_argument( + "-f", + "--fit", + action="store_true", + dest="fit", + help="Fit exp2syn waveforms to multisite data", + ) + parser.add_argument( + "-t", + "--test", + action="store_true", + dest="test", + help="Run the test on exp2syn fitting", + ) + parser.add_argument( + "-p", + "--plot", + action="store_true", + dest="plot", + help="Plot the current comparison between simple and multisite", + ) + parser.add_argument( + "-l", + "--list", + action="store_true", + dest="list", + help="List the simple fit parameters", + ) + + args = parser.parse_args() + + if args.test: + testexp() + exit() + + if args.run: + run_all() + exit() + + if args.runone: + run_one(args.pre, args.post) + exit() + + if args.fit: + fit_all() # self contained - always fits exp2syn against the current multistie data + ds = read_pickle("simple") + dm = read_pickle("multisite") + plot_all(dm, ds) + exit() + + if args.plot: + if args.mode in ["simple", "multisite"]: + d = read_pickle(args.mode) + plot_all(d) + exit() + elif args.mode in ["both"]: + ds = read_pickle("simple") + dm = read_pickle("multisite") + plot_all(dm, ds) + exit() + else: + print(f"Mode {args.mode:s} is not valid") + exit() + + if args.list: + ds = read_pickle("simple") + print_exp2syn_fits(ds) + + # if sys.flags.interactive == 0: + # pg.QtGui.QApplication.exec_() + + +if __name__ == "__main__": + main() diff --git a/cnmodel/util/difftreewidget/DataTreeWidget.py b/cnmodel/util/difftreewidget/DataTreeWidget.py new file mode 100644 index 0000000..11bcad0 --- /dev/null +++ b/cnmodel/util/difftreewidget/DataTreeWidget.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.pgcollections import OrderedDict +from .TableWidget import TableWidget +from pyqtgraph.python2_3 import asUnicode +import types, traceback +import numpy as np + +try: + import metaarray + + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + +__all__ = ["DataTreeWidget"] + + +class DataTreeWidget(QtGui.QTreeWidget): + """ + Widget for displaying hierarchical python data structures + (eg, nested dicts, lists, and arrays) + """ + + def __init__(self, parent=None, data=None): + QtGui.QTreeWidget.__init__(self, parent) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setData(data) + self.setColumnCount(3) + self.setHeaderLabels(["key / index", "type", "value"]) + self.setAlternatingRowColors(True) + + def setData(self, data, hideRoot=False): + """data should be a dictionary.""" + self.clear() + self.widgets = [] + self.nodes = {} + self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) + self.expandToDepth(3) + self.resizeColumnToContents(0) + + def buildTree(self, data, parent, name="", hideRoot=False, path=()): + if hideRoot: + node = parent + else: + node = QtGui.QTreeWidgetItem([name, "", ""]) + parent.addChild(node) + + # record the path to the node so it can be retrieved later + # (this is used by DiffTreeWidget) + self.nodes[path] = node + + typeStr, desc, childs, widget = self.parse(data) + node.setText(1, typeStr) + node.setText(2, desc) + + # Truncate description and add text box if needed + if len(desc) > 100: + desc = desc[:97] + "..." + if widget is None: + widget = QtGui.QPlainTextEdit(asUnicode(data)) + widget.setMaximumHeight(200) + widget.setReadOnly(True) + + # Add widget to new subnode + if widget is not None: + self.widgets.append(widget) + subnode = QtGui.QTreeWidgetItem(["", "", ""]) + node.addChild(subnode) + self.setItemWidget(subnode, 0, widget) + self.setFirstItemColumnSpanned(subnode, True) + + # recurse to children + for key, data in childs.items(): + self.buildTree(data, node, asUnicode(key), path=path + (key,)) + + def parse(self, data): + """ + Given any python object, return: + * type + * a short string representation + * a dict of sub-objects to be parsed + * optional widget to display as sub-node + """ + # defaults for all objects + typeStr = type(data).__name__ + if typeStr == "instance": + typeStr += ": " + data.__class__.__name__ + widget = None + desc = "" + childs = {} + + # type-specific changes + if isinstance(data, dict): + desc = "length=%d" % len(data) + if isinstance(data, OrderedDict): + childs = data + else: + childs = OrderedDict(sorted(data.items())) + elif isinstance(data, (list, tuple)): + desc = "length=%d" % len(data) + childs = OrderedDict(enumerate(data)) + elif HAVE_METAARRAY and ( + hasattr(data, "implements") and data.implements("MetaArray") + ): + childs = OrderedDict( + [("data", data.view(np.ndarray)), ("meta", data.infoCopy())] + ) + elif isinstance(data, np.ndarray): + desc = "shape=%s dtype=%s" % (data.shape, data.dtype) + table = TableWidget() + table.setData(data) + table.setMaximumHeight(200) + widget = table + elif isinstance( + data, types.TracebackType + ): ## convert traceback to a list of strings + frames = list( + map(str.strip, traceback.format_list(traceback.extract_tb(data))) + ) + # childs = OrderedDict([ + # (i, {'file': child[0], 'line': child[1], 'function': child[2], 'code': child[3]}) + # for i, child in enumerate(frames)]) + # childs = OrderedDict([(i, ch) for i,ch in enumerate(frames)]) + widget = QtGui.QPlainTextEdit(asUnicode("\n".join(frames))) + widget.setMaximumHeight(200) + widget.setReadOnly(True) + else: + desc = asUnicode(data) + + return typeStr, desc, childs, widget diff --git a/cnmodel/util/difftreewidget/DiffTreeWidget.py b/cnmodel/util/difftreewidget/DiffTreeWidget.py new file mode 100644 index 0000000..03a7ff1 --- /dev/null +++ b/cnmodel/util/difftreewidget/DiffTreeWidget.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.pgcollections import OrderedDict +from .DataTreeWidget import DataTreeWidget +import pyqtgraph.functions as fn +import types, traceback +import numpy as np + +__all__ = ["DiffTreeWidget"] + + +class DiffTreeWidget(QtGui.QWidget): + """ + Widget for displaying differences between hierarchical python data structures + (eg, nested dicts, lists, and arrays) + """ + + def __init__(self, parent=None, a=None, b=None): + QtGui.QWidget.__init__(self, parent) + self.layout = QtGui.QHBoxLayout() + self.setLayout(self.layout) + self.trees = [DataTreeWidget(self), DataTreeWidget(self)] + for t in self.trees: + self.layout.addWidget(t) + if a is not None: + self.setData(a, b) + + def setData(self, a, b): + """ + Set the data to be compared in this widget. + """ + self.data = (a, b) + self.trees[0].setData(a) + self.trees[1].setData(b) + + return self.compare(a, b) + + def compare(self, a, b, path=()): + """ + Compare data structure *a* to structure *b*. + + Return True if the objects match completely. + Otherwise, return a structure that describes the differences: + + { 'type': bool + 'len': bool, + 'str': bool, + 'shape': bool, + 'dtype': bool, + 'mask': array, + } + + + """ + bad = (255, 200, 200) + diff = [] + # generate typestr, desc, childs for each object + typeA, descA, childsA, _ = self.trees[0].parse(a) + typeB, descB, childsB, _ = self.trees[1].parse(b) + + if typeA != typeB: + self.setColor(path, 1, bad) + if descA != descB: + self.setColor(path, 2, bad) + + if isinstance(a, dict) and isinstance(b, dict): + keysA = set(a.keys()) + keysB = set(b.keys()) + for key in keysA - keysB: + self.setColor(path + (key,), 0, bad, tree=0) + for key in keysB - keysA: + self.setColor(path + (key,), 0, bad, tree=1) + for key in keysA & keysB: + self.compare(a[key], b[key], path + (key,)) + + elif isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): + for i in range(max(len(a), len(b))): + if len(a) <= i: + self.setColor(path + (i,), 0, bad, tree=1) + elif len(b) <= i: + self.setColor(path + (i,), 0, bad, tree=0) + else: + self.compare(a[i], b[i], path + (i,)) + + elif ( + isinstance(a, np.ndarray) + and isinstance(b, np.ndarray) + and a.shape == b.shape + ): + tableNodes = [tree.nodes[path].child(0) for tree in self.trees] + if a.dtype.fields is None and b.dtype.fields is None: + eq = self.compareArrays(a, b) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + # for i in np.argwhere(~eq): + + else: + if a.dtype == b.dtype: + for i, k in enumerate(a.dtype.fields.keys()): + eq = self.compareArrays(a[k], b[k]) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + # for j in np.argwhere(~eq): + + # dict: compare keys, then values where keys match + # list: + # array: compare elementwise for same shape + + def compareArrays(self, a, b): + intnan = -9223372036854775808 # happens when np.nan is cast to int + anans = np.isnan(a) | (a == intnan) + bnans = np.isnan(b) | (b == intnan) + eq = anans == bnans + mask = ~anans + eq[mask] = np.allclose(a[mask], b[mask]) + return eq + + def setColor(self, path, column, color, tree=None): + brush = fn.mkBrush(color) + + # Color only one tree if specified. + if tree is None: + trees = self.trees + else: + trees = [self.trees[tree]] + + for tree in trees: + item = tree.nodes[path] + item.setBackground(column, brush) + + def _compare(self, a, b): + """ + Compare data structure *a* to structure *b*. + """ + # Check test structures are the same + assert type(info) is type(expect) + if hasattr(info, "__len__"): + assert len(info) == len(expect) + + if isinstance(info, dict): + for k in info: + assert k in expect + for k in expect: + assert k in info + self.compare_results(info[k], expect[k]) + elif isinstance(info, list): + for i in range(len(info)): + self.compare_results(info[i], expect[i]) + elif isinstance(info, np.ndarray): + assert info.shape == expect.shape + assert info.dtype == expect.dtype + if info.dtype.fields is None: + intnan = -9223372036854775808 # happens when np.nan is cast to int + inans = np.isnan(info) | (info == intnan) + enans = np.isnan(expect) | (expect == intnan) + assert np.all(inans == enans) + mask = ~inans + assert np.allclose(info[mask], expect[mask]) + else: + for k in info.dtype.fields.keys(): + self.compare_results(info[k], expect[k]) + else: + try: + assert info == expect + except Exception: + raise NotImplementedError( + "Cannot compare objects of type %s" % type(info) + ) diff --git a/cnmodel/util/difftreewidget/README b/cnmodel/util/difftreewidget/README new file mode 100644 index 0000000..116dcbc --- /dev/null +++ b/cnmodel/util/difftreewidget/README @@ -0,0 +1,2 @@ +Copied from github.com/campagnola/pyqtgraph datatree-arrays branch; this gives us DiffTreeWidget. +This file can be removed if DiffTreeWidget has been merged into pyqtgraph. diff --git a/cnmodel/util/difftreewidget/TableWidget.py b/cnmodel/util/difftreewidget/TableWidget.py new file mode 100644 index 0000000..baff6d2 --- /dev/null +++ b/cnmodel/util/difftreewidget/TableWidget.py @@ -0,0 +1,515 @@ +# -*- coding: utf-8 -*- +import numpy as np +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import asUnicode, basestring +import pyqtgraph.metaarray as metaarray + + +__all__ = ["TableWidget"] + + +def _defersort(fn): + def defersort(self, *args, **kwds): + # may be called recursively; only the first call needs to block sorting + setSorting = False + if self._sorting is None: + self._sorting = self.isSortingEnabled() + setSorting = True + self.setSortingEnabled(False) + try: + return fn(self, *args, **kwds) + finally: + if setSorting: + self.setSortingEnabled(self._sorting) + self._sorting = None + + return defersort + + +class TableWidget(QtGui.QTableWidget): + """Extends QTableWidget with some useful functions for automatic data handling + and copy / export context menu. Can automatically format and display a variety + of data types (see :func:`setData() ` for more + information. + """ + + def __init__(self, *args, **kwds): + """ + All positional arguments are passed to QTableWidget.__init__(). + + ===================== ================================================= + **Keyword Arguments** + editable (bool) If True, cells in the table can be edited + by the user. Default is False. + sortable (bool) If True, the table may be soted by + clicking on column headers. Note that this also + causes rows to appear initially shuffled until + a sort column is selected. Default is True. + *(added in version 0.9.9)* + ===================== ================================================= + """ + + QtGui.QTableWidget.__init__(self, *args) + + self.itemClass = TableWidgetItem + + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) + self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + self.clear() + + kwds.setdefault("sortable", True) + kwds.setdefault("editable", False) + self.setEditable(kwds.pop("editable")) + self.setSortingEnabled(kwds.pop("sortable")) + + if len(kwds) > 0: + raise TypeError("Invalid keyword arguments '%s'" % kwds.keys()) + + self._sorting = None # used when temporarily disabling sorting + + self._formats = { + None: None + } # stores per-column formats and entire table format + self.sortModes = {} # stores per-column sort mode + + self.itemChanged.connect(self.handleItemChanged) + + self.contextMenu = QtGui.QMenu() + self.contextMenu.addAction("Copy Selection").triggered.connect(self.copySel) + self.contextMenu.addAction("Copy All").triggered.connect(self.copyAll) + self.contextMenu.addAction("Save Selection").triggered.connect(self.saveSel) + self.contextMenu.addAction("Save All").triggered.connect(self.saveAll) + + def clear(self): + """Clear all contents from the table.""" + QtGui.QTableWidget.clear(self) + self.verticalHeadersSet = False + self.horizontalHeadersSet = False + self.items = [] + self.setRowCount(0) + self.setColumnCount(0) + self.sortModes = {} + + def setData(self, data): + """Set the data displayed in the table. + Allowed formats are: + + * numpy arrays + * numpy record arrays + * metaarrays + * list-of-lists [[1,2,3], [4,5,6]] + * dict-of-lists {'x': [1,2,3], 'y': [4,5,6]} + * list-of-dicts [{'x': 1, 'y': 4}, {'x': 2, 'y': 5}, ...] + """ + self.clear() + self.appendData(data) + self.resizeColumnsToContents() + + @_defersort + def appendData(self, data): + """ + Add new rows to the table. + + See :func:`setData() ` for accepted + data types. + """ + startRow = self.rowCount() + + fn0, header0 = self.iteratorFn(data) + if fn0 is None: + self.clear() + return + it0 = fn0(data) + try: + first = next(it0) + except StopIteration: + return + fn1, header1 = self.iteratorFn(first) + if fn1 is None: + self.clear() + return + + firstVals = [x for x in fn1(first)] + self.setColumnCount(len(firstVals)) + + if not self.verticalHeadersSet and header0 is not None: + labels = [self.verticalHeaderItem(i).text() for i in range(self.rowCount())] + self.setRowCount(startRow + len(header0)) + self.setVerticalHeaderLabels(labels + header0) + self.verticalHeadersSet = True + if not self.horizontalHeadersSet and header1 is not None: + self.setHorizontalHeaderLabels(header1) + self.horizontalHeadersSet = True + + i = startRow + self.setRow(i, firstVals) + for row in it0: + i += 1 + self.setRow(i, [x for x in fn1(row)]) + + if ( + self._sorting + and self.horizontalHeadersSet + and self.horizontalHeader().sortIndicatorSection() >= self.columnCount() + ): + self.sortByColumn(0, QtCore.Qt.AscendingOrder) + + def setEditable(self, editable=True): + self.editable = editable + for item in self.items: + item.setEditable(editable) + + def setFormat(self, format, column=None): + """ + Specify the default text formatting for the entire table, or for a + single column if *column* is specified. + + If a string is specified, it is used as a format string for converting + float values (and all other types are converted using str). If a + function is specified, it will be called with the item as its only + argument and must return a string. Setting format = None causes the + default formatter to be used instead. + + Added in version 0.9.9. + + """ + if ( + format is not None + and not isinstance(format, basestring) + and not callable(format) + ): + raise ValueError( + "Format argument must string, callable, or None. (got %s)" % format + ) + + self._formats[column] = format + + if column is None: + # update format of all items that do not have a column format + # specified + for c in range(self.columnCount()): + if self._formats.get(c, None) is None: + for r in range(self.rowCount()): + item = self.item(r, c) + if item is None: + continue + item.setFormat(format) + else: + # set all items in the column to use this format, or the default + # table format if None was specified. + if format is None: + format = self._formats[None] + for r in range(self.rowCount()): + item = self.item(r, column) + if item is None: + continue + item.setFormat(format) + + def iteratorFn(self, data): + ## Return 1) a function that will provide an iterator for data and 2) a list of header strings + if isinstance(data, list) or isinstance(data, tuple): + return lambda d: d.__iter__(), None + elif isinstance(data, dict): + return lambda d: iter(d.values()), list(map(asUnicode, data.keys())) + elif hasattr(data, "implements") and data.implements("MetaArray"): + if data.axisHasColumns(0): + header = [ + asUnicode(data.columnName(0, i)) for i in range(data.shape[0]) + ] + elif data.axisHasValues(0): + header = list(map(asUnicode, data.xvals(0))) + else: + header = None + return self.iterFirstAxis, header + elif isinstance(data, np.ndarray): + return self.iterFirstAxis, None + elif isinstance(data, np.void): + return self.iterate, list(map(asUnicode, data.dtype.names)) + elif data is None: + return (None, None) + elif np.isscalar(data): + return self.iterateScalar, None + else: + msg = "Don't know how to iterate over data type: {!s}".format(type(data)) + raise TypeError(msg) + + def iterFirstAxis(self, data): + for i in range(data.shape[0]): + yield data[i] + + def iterate(self, data): + # for numpy.void, which can be iterated but mysteriously + # has no __iter__ (??) + for x in data: + yield x + + def iterateScalar(self, data): + yield data + + def appendRow(self, data): + self.appendData([data]) + + @_defersort + def addRow(self, vals): + row = self.rowCount() + self.setRowCount(row + 1) + self.setRow(row, vals) + + @_defersort + def setRow(self, row, vals): + if row > self.rowCount() - 1: + self.setRowCount(row + 1) + for col in range(len(vals)): + val = vals[col] + item = self.itemClass(val, row) + item.setEditable(self.editable) + sortMode = self.sortModes.get(col, None) + if sortMode is not None: + item.setSortMode(sortMode) + format = self._formats.get(col, self._formats[None]) + item.setFormat(format) + self.items.append(item) + self.setItem(row, col, item) + item.setValue(val) # Required--the text-change callback is invoked + # when we call setItem. + + def setSortMode(self, column, mode): + """ + Set the mode used to sort *column*. + + ============== ======================================================== + **Sort Modes** + value Compares item.value if available; falls back to text + comparison. + text Compares item.text() + index Compares by the order in which items were inserted. + ============== ======================================================== + + Added in version 0.9.9 + """ + for r in range(self.rowCount()): + item = self.item(r, column) + if hasattr(item, "setSortMode"): + item.setSortMode(mode) + self.sortModes[column] = mode + + def sizeHint(self): + # based on http://stackoverflow.com/a/7195443/54056 + width = sum(self.columnWidth(i) for i in range(self.columnCount())) + width += self.verticalHeader().sizeHint().width() + width += self.verticalScrollBar().sizeHint().width() + width += self.frameWidth() * 2 + height = sum(self.rowHeight(i) for i in range(self.rowCount())) + height += self.verticalHeader().sizeHint().height() + height += self.horizontalScrollBar().sizeHint().height() + return QtCore.QSize(width, height) + + def serialize(self, useSelection=False): + """Convert entire table (or just selected area) into tab-separated text values""" + if useSelection: + selection = self.selectedRanges()[0] + rows = list(range(selection.topRow(), selection.bottomRow() + 1)) + columns = list(range(selection.leftColumn(), selection.rightColumn() + 1)) + else: + rows = list(range(self.rowCount())) + columns = list(range(self.columnCount())) + + data = [] + if self.horizontalHeadersSet: + row = [] + if self.verticalHeadersSet: + row.append(asUnicode("")) + + for c in columns: + row.append(asUnicode(self.horizontalHeaderItem(c).text())) + data.append(row) + + for r in rows: + row = [] + if self.verticalHeadersSet: + row.append(asUnicode(self.verticalHeaderItem(r).text())) + for c in columns: + item = self.item(r, c) + if item is not None: + row.append(asUnicode(item.value)) + else: + row.append(asUnicode("")) + data.append(row) + + s = "" + for row in data: + s += "\t".join(row) + "\n" + return s + + def copySel(self): + """Copy selected data to clipboard.""" + QtGui.QApplication.clipboard().setText(self.serialize(useSelection=True)) + + def copyAll(self): + """Copy all data to clipboard.""" + QtGui.QApplication.clipboard().setText(self.serialize(useSelection=False)) + + def saveSel(self): + """Save selected data to file.""" + self.save(self.serialize(useSelection=True)) + + def saveAll(self): + """Save all data to file.""" + self.save(self.serialize(useSelection=False)) + + def save(self, data): + fileName = QtGui.QFileDialog.getSaveFileName( + self, "Save As..", "", "Tab-separated values (*.tsv)" + ) + if fileName == "": + return + open(fileName, "w").write(data) + + def contextMenuEvent(self, ev): + self.contextMenu.popup(ev.globalPos()) + + def keyPressEvent(self, ev): + if ev.key() == QtCore.Qt.Key_C and ev.modifiers() == QtCore.Qt.ControlModifier: + ev.accept() + self.copySel() + else: + QtGui.QTableWidget.keyPressEvent(self, ev) + + def handleItemChanged(self, item): + item.itemChanged() + + +class TableWidgetItem(QtGui.QTableWidgetItem): + def __init__(self, val, index, format=None): + QtGui.QTableWidgetItem.__init__(self, "") + self._blockValueChange = False + self._format = None + self._defaultFormat = "%0.3g" + self.sortMode = "value" + self.index = index + flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + self.setFlags(flags) + self.setValue(val) + self.setFormat(format) + + def setEditable(self, editable): + """ + Set whether this item is user-editable. + """ + if editable: + self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) + else: + self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable) + + def setSortMode(self, mode): + """ + Set the mode used to sort this item against others in its column. + + ============== ======================================================== + **Sort Modes** + value Compares item.value if available; falls back to text + comparison. + text Compares item.text() + index Compares by the order in which items were inserted. + ============== ======================================================== + """ + modes = ("value", "text", "index", None) + if mode not in modes: + raise ValueError("Sort mode must be one of %s" % str(modes)) + self.sortMode = mode + + def setFormat(self, fmt): + """Define the conversion from item value to displayed text. + + If a string is specified, it is used as a format string for converting + float values (and all other types are converted using str). If a + function is specified, it will be called with the item as its only + argument and must return a string. + + Added in version 0.9.9. + """ + if fmt is not None and not isinstance(fmt, basestring) and not callable(fmt): + raise ValueError( + "Format argument must string, callable, or None. (got %s)" % fmt + ) + self._format = fmt + self._updateText() + + def _updateText(self): + self._blockValueChange = True + try: + self._text = self.format() + self.setText(self._text) + finally: + self._blockValueChange = False + + def setValue(self, value): + self.value = value + self._updateText() + + def itemChanged(self): + """Called when the data of this item has changed.""" + if self.text() != self._text: + self.textChanged() + + def textChanged(self): + """Called when this item's text has changed for any reason.""" + self._text = self.text() + + if self._blockValueChange: + # text change was result of value or format change; do not + # propagate. + return + + try: + + self.value = type(self.value)(self.text()) + except ValueError: + self.value = str(self.text()) + + def format(self): + if callable(self._format): + return self._format(self) + if isinstance(self.value, (float, np.floating)): + if self._format is None: + return self._defaultFormat % self.value + else: + return self._format % self.value + else: + return asUnicode(self.value) + + def __lt__(self, other): + if self.sortMode == "index" and hasattr(other, "index"): + return self.index < other.index + if self.sortMode == "value" and hasattr(other, "value"): + return self.value < other.value + else: + return self.text() < other.text() + + +if __name__ == "__main__": + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + t = TableWidget() + win.setCentralWidget(t) + win.resize(800, 600) + win.show() + + ll = [[1, 2, 3, 4, 5]] * 20 + ld = [{"x": 1, "y": 2, "z": 3}] * 20 + dl = {"x": list(range(20)), "y": list(range(20)), "z": list(range(20))} + + a = np.ones((20, 5)) + ra = np.ones((20,), dtype=[("x", int), ("y", int), ("z", int)]) + + t.setData(ll) + + ma = metaarray.MetaArray( + np.ones((20, 3)), + info=[ + {"values": np.linspace(1, 5, 20)}, + {"cols": [{"name": "x"}, {"name": "y"}, {"name": "z"}]}, + ], + ) + t.setData(ma) diff --git a/cnmodel/util/difftreewidget/__init__.py b/cnmodel/util/difftreewidget/__init__.py new file mode 100644 index 0000000..9b261da --- /dev/null +++ b/cnmodel/util/difftreewidget/__init__.py @@ -0,0 +1 @@ +from .DiffTreeWidget import DiffTreeWidget diff --git a/cnmodel/util/expfitting.py b/cnmodel/util/expfitting.py new file mode 100755 index 0000000..efbc1a4 --- /dev/null +++ b/cnmodel/util/expfitting.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +expfitting.py +Provide single or double exponential fits to data. +""" + +import lmfit +import numpy as np +import scipy.optimize + + +class ExpFitting: + """ + Parameters + ---------- + nexp : int + 1 or 2 for single or double exponential fit + initpars : dict + dict of initial parameters. For example: {'dc': 0., + 'a1': 1., 't1': 3, 'a2' : 0.5, 'delta': 3.}, where + delta determines the ratio between the time constants. + bounds : dict + dictionary of bounds for each parameter, with a list of lower and upper values. + + """ + + def __init__(self, nexp=1, initpars=None, bounds=None): + self.fitpars = lmfit.Parameters() + if nexp == 1: + # (Name, Value, Vary, Min, Max, Expr) + self.fitpars.add_many( + ("dc", 0, True, -100.0, 0.0, None), + ("a1", 1.0, True, -25.0, 25.0, None), + ("t1", 10.0, True, 0.1, 50, None), + ) + self.efunc = self.exp1_err + elif nexp == 2: + self.fitpars.add_many( + ("dc", 0, True, -100.0, 0.0, None), + ("a1", 1.0, True, 0.0, 25.0, None), + ("t1", 10.0, True, 0.1, 50, None), + ("a2", 1.0, True, 0.0, 25.0, None), + ("delta", 3.0, True, 3.0, 100.0, None), + ) + if initpars is not None: + assert len(initpars) == 5 + for k, v in initpars.iteritems(): + self.fitpars[k].value = v + if bounds is not None: + assert len(bounds) == 5 + for k, v in bounds.iteritems(): + self.fitpars[k].min = v[0] + self.fitpars[k].max = v[1] + + self.efunc = self.exp2_err + else: + raise ValueError + + def fit(self, x, y, p, verbose=False): + + kws = {"maxfev": 5000} + mim = lmfit.minimize( + self.efunc, p, method="least_squares", args=(x, y) + ) # , kws=kws) + if verbose: + lmfit.printfuncs.report_fit(mim.params) + fitpars = mim.params + return fitpars + + @staticmethod + def exp1(x, dc, t1, a1): + return dc + a1 * np.exp(-x / t1) + + def exp1_err(self, p, x, y): + return np.fabs(y - self.exp1(x, **dict([(k, p.value) for k, p in p.items()]))) + + @staticmethod + def exp2(x, dc, t1, a1, a2, delta): + return dc + a1 * np.exp(-x / t1) + a2 * np.exp(-x / (t1 * delta)) + + def exp2_err(self, p, x, y): + return np.fabs(y - self.exp2(x, **dict([(k, p.value) for k, p in p.items()]))) diff --git a/cnmodel/util/filelock.py b/cnmodel/util/filelock.py new file mode 100644 index 0000000..50d8f3b --- /dev/null +++ b/cnmodel/util/filelock.py @@ -0,0 +1,133 @@ +# Copyright (c) 2009, Evan Fosmark +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. + +import os +import time +import errno + + +class FileLockException(Exception): + pass + + +class FileLock(object): + """ A file locking mechanism that has context-manager support so + you can use it in a with statement. This should be relatively cross + compatible as it doesn't rely on msvcrt or fcntl for the locking. + """ + + lock_count = {} # lock count for each locked file + lock_handles = {} # file handle for each locked file + + def __init__(self, file_name, timeout=10, delay=0.05): + """ Prepare the file locker. Specify the file to lock and optionally + the maximum timeout and the delay between each attempt to lock. + """ + self.fd = None + self.is_locked = False + self.lockfile = os.path.join(os.getcwd(), "%s.lock" % file_name) + self.file_name = file_name + self.timeout = timeout + self.delay = delay + + def acquire(self): + """ Acquire the lock, if possible. If the lock is in use, it check again + every `wait` seconds. It does this until it either gets the lock or + exceeds `timeout` number of seconds, in which case it throws + an exception. + """ + # Don't try to lock the same file more than once + if FileLock.lock_count.setdefault(self.lockfile, 0) > 0: + self.is_locked = True + self.fd = FileLock.lock_handles[self.lockfile] + FileLock.lock_count[self.lockfile] += 1 + return + + start_time = time.time() + while True: + try: + # create the cache directory if it does not exist (new installations will not have a cache directory) + stimdir = os.path.dirname(self.lockfile) + cachedir = os.path.dirname(stimdir) + if not os.path.isdir(cachedir): # make sure the cache exists + os.mkdir(cachedir) + if not os.path.isdir(stimdir): # and the specific stimulus dir + os.mkdir(stimdir) + self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR) + open(self.lockfile, "w").write(str(os.getpid())) + break + except OSError as e: + if e.errno != errno.EEXIST: + raise + if (time.time() - start_time) >= self.timeout: + try: + pid = open(self.lockfile).read() + except Exception: + pid = "[error reading lockfile: %s]" % sys.exc_info()[0] + raise FileLockException( + "Timeout occured. (%s is locked by pid %s)" + % (self.lockfile, pid) + ) + time.sleep(self.delay) + + self.is_locked = True + FileLock.lock_count[self.lockfile] += 1 + FileLock.lock_handles[self.lockfile] = self.fd + + def release(self): + """ Get rid of the lock by deleting the lockfile. + When working in a `with` statement, this gets automatically + called at the end. + """ + if self.is_locked: + self.is_locked = False + FileLock.lock_count[self.lockfile] -= 1 + if FileLock.lock_count[self.lockfile] == 0: + os.close(self.fd) + os.unlink(self.lockfile) + del FileLock.lock_handles[self.lockfile] + + def __enter__(self): + """ Activated when used in the with statement. + Should automatically acquire a lock to be used in the with block. + """ + if not self.is_locked: + self.acquire() + return self + + def __exit__(self, type, value, traceback): + """ Activated at the end of the with statement. + It automatically releases the lock if it isn't locked. + """ + if self.is_locked: + self.release() + + def __del__(self): + """ Make sure that the FileLock instance doesn't leave a lockfile + lying around. + """ + self.release() diff --git a/cnmodel/util/find_point.py b/cnmodel/util/find_point.py new file mode 100644 index 0000000..30228f7 --- /dev/null +++ b/cnmodel/util/find_point.py @@ -0,0 +1,95 @@ +from __future__ import print_function +from scipy import interpolate +import numpy as np + + +def find_point(x, y, peakindex, val, direction="left", limits=None): + """ + Given a waveform defined by *x* and *y* arrays, return the first time + at which the waveform crosses (y[peakindex] * val). The search begins at + *peakindex* and proceeds in *direction*. + + Optionally, *limits* may specify a smaller search region in the form of + (t0, t1, dt). + """ + # F = interpolate.UnivariateSpline(x, y, s=0) # declare function + # To find x at y then do: + istart = 0 + iend = len(y) + if limits is not None: + istart = int(limits[0] / limits[2]) + iend = int(limits[1] / limits[2]) + yToFind = y[peakindex] * val + if direction == "left": + yreduced = np.array(y[istart:peakindex]) - yToFind + try: + Fr = interpolate.UnivariateSpline(x[istart:peakindex], yreduced, s=0) + except: + print("find_point: insufficient time points for analysis") + print("arg lengths:", len(x[istart:peakindex]), len(yreduced)) + print("istart, peakindex: ", istart, peakindex) + print("ytofine: ", yToFind) + raise + res = float("nan") + return res + res = Fr.roots() + if len(res) > 1: + res = res[-1] + else: + yreduced = np.array(y[peakindex:iend]) - yToFind + try: + Fr = interpolate.UnivariateSpline(x[peakindex:iend], yreduced, s=0) + except: + print("find_point: insufficient time points for analysis?") + print("arg lengths:", len(x[peakindex:iend]), len(yreduced)) + raise + res = float("nan") + return res + res = Fr.roots() + if len(res) > 1: + res = res[0] + # pdb.set_trace() + try: + res.pop() + except: + pass + if not res: # tricky - an empty list is False, but does not evaluate to False + res = float("nan") # replace with a NaN + else: + res = float(res) # make sure is just a simple number (no arrays) + return res + + +def find_crossing(data, start=0, direction=1, threshold=0): + """Return the index at which *data* crosses *threshold*, starting + at index *start* and proceeding in *direction* (+/-1). + + The value returned is a float indicating the interpolated index + position where the data crosses threshold, or NaN if the threshold was + never crossed. + """ + + # Note: this function is very similar in purpose to find_point, but was + # added due to issues with interpolate.UnivariateSpline + + assert direction in (1, -1) + cross_rising = data[start] < threshold + + def test(x): + if cross_rising: + return x > threshold + else: + return x < threshold + + while True: + next_ind = start + direction + if next_ind < 0 or next_ind >= len(data): + return np.nan + + if test(data[next_ind]): + # crossed; return interpolated value + s1 = data[next_ind] - threshold + s2 = threshold = data[start] + return (next_ind * s2 + start * s1) / (s2 + s1) + + start = next_ind diff --git a/cnmodel/util/fitting.py b/cnmodel/util/fitting.py new file mode 100644 index 0000000..f4890e4 --- /dev/null +++ b/cnmodel/util/fitting.py @@ -0,0 +1,219 @@ +import numpy as np +import lmfit +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui + + +class FitModel(lmfit.Model): + """ Simple extension of lmfit.Model that allows one-line fitting. + + Example uses: + + # single exponential fit:: + + fit = expfitting.Exp1.fit(data, + x=time_vals, + xoffset=(0, 'fixed'), + yoffset=(yoff_guess, -120, 0), + amp=(amp_guess, 0, 50), + tau=(tau_guess, 0.1, 50)) + + # plot the fit:: + + fit_curve = fit.eval() + plot(time_vals, fit_curve) + + + # double exponential fit with tau ratio constraint + # note that 'tau_ratio' does not appear in the exp2 model; + # we can define new parameters here.:: + + fit = expfitting.Exp2.fit(data, + x=time_vals, + xoffset=(0, 'fixed'), + yoffset=(yoff_guess, -120, 0), + amp1=(amp_guess, 0, 50), + tau1=(tau_guess, 0.1, 50), + amp2=(-0.5, -50, 0), + tau_ratio=(10, 3, 50), + tau2='tau1 * tau_ratio' + ) + + """ + + def fit(self, data, interactive=False, **params): + """ Return a fit of data to this model. + + Parameters + ---------- + data : array + dependent data to fit + interactive : bool + If True, show a GUI used for interactively exploring fit parameters + + Extra keyword arguments are passed to make_params() if they are model + parameter names, or passed directly to Model.fit() for independent + variable names. + + Returns + ------- + fit of data to model as an lmfit object + """ + + fit_params = {} + model_params = {} + for k, v in params.items(): + if k in self.independent_vars or k in [ + "weights", + "method", + "scale_covar", + "iter_cb", + ]: + fit_params[k] = v + else: + model_params[k] = v + p = self.make_params(**model_params) + # print ('params: ', p) + # print ('fitparams: ', fit_params) + # import matplotlib.pyplot as mpl + # mpl.plot(data) + # mpl.show() + + fit = lmfit.Model.fit(self, data, params=p, **fit_params) + if interactive: + self.show_interactive(fit) + return fit + + def make_params(self, **params): + """ + Make parameters used for fitting with this model. + + Keyword arguments are used to generate parameters for the fit. Each + parameter may be specified by the following formats: + + param=value : + The initial value of the parameter + param=(value, 'fixed') : + Fixed value for the parameter + param=(value, min, max) : + Initial value and min, max values, which may be float or None + param='expression' : + Expression used to compute parameter value. See: + http://lmfit.github.io/lmfit-py/constraints.html#constraints-chapter + """ + p = lmfit.Parameters() + for k in self.param_names: + p.add(k) + + for param, val in params.items(): + if param not in p: + p.add(param) + + if isinstance(val, str): + p[param].expr = val + elif np.isscalar(val): + p[param].value = val + elif isinstance(val, tuple): + if len(val) == 2: + assert val[1] == "fixed" + p[param].value = val[0] + p[param].vary = False + elif len(val) == 3: + p[param].value = val[0] + p[param].min = val[1] + p[param].max = val[2] + else: + raise TypeError( + "Tuple parameter specifications must be (val, 'fixed')" + " or (val, min, max)." + ) + else: + raise TypeError("Invalid parameter specification: %r" % val) + + # set initial values for parameters with mathematical constraints + # this is to allow fit.eval(**fit.init_params) + global_ns = np.__dict__ + for param, val in params.items(): + if isinstance(val, str): + p[param].value = eval(val, global_ns, p.valuesdict()) + return p + + def show_interactive(self, fit=None): + """ Show an interactive GUI for exploring fit parameters. + """ + if not hasattr(self, "_interactive_win"): + self._interactive_win = FitExplorer(model=self, fit=fit) + self._interactive_win.show() + + +def exp1(x, xoffset, yoffset, tau, amp): + return yoffset + amp * np.exp(-(x - xoffset) / tau) + + +class Exp1(FitModel): + """ Single exponential fitting model. + + Parameters are xoffset, yoffset, amp, and tau. + """ + + def __init__(self): + FitModel.__init__(self, exp1, independent_vars=["x"]) + + +def exp2(x, xoffset, yoffset, tau1, amp1, tau2, amp2): + xoff = x - xoffset + return yoffset + amp1 * np.exp(-xoff / tau1) + amp2 * np.exp(-xoff / tau2) + + +class Exp2(FitModel): + """ Double exponential fitting model. + + Parameters are xoffset, yoffset, amp1, tau1, amp2, and tau2. + """ + + def __init__(self): + FitModel.__init__(self, exp2, independent_vars=["x"]) + + +class FitExplorer(QtGui.QWidget): + def __init__(self, model, fit): + QtGui.QWidget.__init__(self) + self.model = model + self.fit = fit + self.layout = QtGui.QGridLayout() + self.setLayout(self.layout) + self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal) + self.layout.addWidget(self.splitter) + self.ptree = pg.parametertree.ParameterTree() + self.splitter.addWidget(self.ptree) + self.plot = pg.PlotWidget() + self.splitter.addWidget(self.plot) + + self.params = pg.parametertree.Parameter.create( + name="param_root", + type="group", + children=[ + dict(name="fit", type="action"), + dict(name="parameters", type="group"), + ], + ) + + for k in fit.params: + p = pg.parametertree.Parameter.create( + name=k, type="float", value=fit.params[k].value + ) + self.params.param("parameters").addChild(p) + + self.ptree.setParameters(self.params) + + self.update_plots() + + self.params.param("parameters").sigTreeStateChanged.connect(self.update_plots) + + def update_plots(self): + for k in self.fit.params: + self.fit.params[k].value = self.params["parameters", k] + + self.plot.clear() + self.plot.plot(self.fit.data) + self.plot.plot(self.fit.eval(), pen="y") diff --git a/cnmodel/util/get_anspikes.py b/cnmodel/util/get_anspikes.py new file mode 100644 index 0000000..78abd2d --- /dev/null +++ b/cnmodel/util/get_anspikes.py @@ -0,0 +1,477 @@ +from __future__ import print_function + +__author__ = "pbmanis" +""" +ManageANSpikes is a class to read the output of the Zilany et al. 2009 AN model into +python, and provides services to access that data. + +Basic usage is to create an instance of the class, and specify the data directory +if necessary. + +You may then get the data in the format of a list using one of the "get" routines. +The data is pulled from the nearest CF or the specified CF. + +""" +import os +import re +import numpy as np +from collections import OrderedDict +import scipy.io +import matplotlib.pyplot as MP + + +class ManageANSpikes: + """ + ManageANSpikes is a class to read the output of the Zilany et al. 2009 AN model into + python, and provides services to access that data. + + Basic usage is to create an instance of the class, and specify the data directory + if necessary. + + You may then get the data in the format of a list using one of the "get" routines. + The data is pulled from the nearest CF or the specified CF. + + """ + + def __init__(self): + # self.datadir = environment['HOME'] + '/Desktop/Matlab/ZilanyCarney-JASAcode-2009/' + self.data_dir = os.environ["HOME"] + "/Desktop/Matlab/ANData/" + self.data_read_flag = False + self.dataType = "RI" + self.set_CF_map(4000, 38000, 25) # the default list + self.all_AN = None + + def get_data_dir(self): + return self.data_dir + + def set_data_dir(self, directory): + if os.path.isdir(directory): + self.data_dir = directory + else: + raise ValueError("ManageANSpikes.set_data_dir: Path %d is not a directory") + + def get_data_read(self): + return self.data_read_flag + + def get_CF_map(self): + return self.CF_map + + def set_CF_map(self, low, high, nfreq): + self.CF_map = np.round(np.logspace(np.log10(low), np.log10(high), nfreq)) + + def get_dataType(self): + return self.dataType + + def set_dataType(self, datatype): + if datatype in ["RI", "PL"]: # only for recognized data types + self.dataType = datatype + else: + raise ValueError( + "get_anspikes.set_dataType: unrecognized type %s " % datatype + ) + + def plot_RI_vs_F(self, freq=10000.0, spontclass="HS", display=False): + cfd = {} + if display: + MP.figure(10) + for i, fr in enumerate(self.CF_map): + self.read_AN_data(freq=freq, CF=fr, spontclass=spontclass, ignoreflag=True) + nsp = np.zeros(len(self.SPLs)) # this is the same regardless + for i, db in enumerate(self.SPLs): + spkl = self.combine_reps(self.spikelist[i]) + nsp[i] = len(spkl) # /self.SPLs[i] + if display: + MP.plot(self.SPLs, nsp) + + def plot_Rate_vs_F(self, freq=10000.0, spontclass="HS", display=False, SPL=0.0): + """ + assumes we have done a "read all" + + :param freq: + :param spontclass: + :param display: + :param SPL: + :return: + """ + cfd = {} + if display: + MP.figure(10) + for i, db in enumerate(self.SPLs): # plot will have lines at each SPL + # retrieve_from_all_AN(self, cf, SPL) # self.read_AN_data(freq=freq, CF=fr, spontclass=spontclass) + nsp = np.zeros(len(self.CF_map)) # this is the same regardless + for j, fr in enumerate(self.CF_map): + spikelist = self.retrieve_from_all_AN(fr, db) + spkl = self.combine_reps(spikelist) + nsp[j] = len(spkl) / len(spikelist) + if display: + MP.plot(self.CF_map, nsp) + + def getANatFandSPL(self, spontclass="MS", freq=10000.0, CF=None, SPL=0): + """ + Get the AN data at a particular frequency and SPL. Note that the tone freq is specified, + but the CF of the fiber might be different. If CF is None, we try to get the closest + fiber data to the tone Freq. + Other arguments are the spontaneous rate and the SPL level. + The data must exist, or we epically fail. + + :param spontclass: 'HS', 'MS', or 'LS' for high, middle or low spont groups (1,2,3 in Zilany et al model) + :param freq: The stimulus frequency, in Hz + :param CF: The 'characteristic frequency' of the desired AN fiber, in Hz + :param SPL: The sound pressure level, in dB SPL for the stimulus + :return: an array of nReps of spike times. + """ + + if CF is None: + closest = np.argmin( + np.abs(self.CF_map - freq) + ) # find closest to the stim freq + else: + closest = np.argmin( + np.abs(self.CF_map - CF) + ) # find closest to the stim freq + CF = self.CF_map[closest] + print("closest f: ", CF) + self.read_AN_data(spontclass=spontclass, freq=10000.0, CF=CF) + return self.get_AN_at_SPL(SPL) + + def get_AN_at_SPL(self, spl=None): + """ + grabs the AN data for the requested SPL for the data set currently loaded + The data must exist, or we epically fail. + + :param spl: sound pressure level, in dB SPL + :return: spike trains (times) as a list of nReps numpy arrays. + """ + + if not self.data_read_flag: + print("getANatSPL: No data read yet") + return None + try: + k = int(np.where(self.SPLs == spl)[0]) + except (ValueError, TypeError) as e: + print( + "get_anspikes::getANatSPL: spl=%6.1f not in list of spls:" % (spl), + self.SPLs, + ) + exit() # no match + # now clean up array to keep it iterable upon return + spkl = [[]] * len(self.spikelist[k]) + for i in xrange(len(self.spikelist[k])): + try: + len(self.spikelist[k][i]) + spkl[i] = self.spikelist[k][i] + except: + spkl[i] = [np.array(self.spikelist[k][i])] + return spkl # returns all the trials in the list + + def read_all_ANdata( + self, freq=10000.0, CFList=None, spontclass="HS", stim="BFTone" + ): + """ + Reads a bank of AN data, across frequency and intensity, for a given stimulus frequency + Assumptions: the bank of data is consistent in terms of nReps and SPLs + + :param freq: Tone stimulus frequency (if stim is Tone or BFTone) + :param CFList: A list of the CFs to read + :param spontclass: 'HS', 'MS', or 'LS', corresponding to spont rate groups + :param stim: Stimulus type - Tone/BFTone, or Noise + :return: Nothing. The data are stored in an array accessible directly or through a selector function + """ + + self.all_AN = OrderedDict( + [(f, None) for f in CFList] + ) # access through dictionary with keys as freq + for cf in CFList: + self.read_AN_data( + freq=freq, CF=cf, spontclass=spontclass, stim=stim, setflag=False + ) + self.all_AN[cf] = self.spikelist + self.data_read_flag = True # inform and block + + def retrieve_from_all_AN(self, cf, SPL): + """ + :param cf: + :param SPL: + :return: spike list (all nreps) at the cf and SPL requested, for the loaded stimulus set + + """ + + if not self.data_read_flag or self.all_AN is None: + print("get_anspikes::retrieve_from_all_AN: No data read yet: ") + exit() + + ispl = int(np.where(self.SPLs == float(SPL))[0]) + icf = self.all_AN.keys().index(cf) + spikelist = self.all_AN[cf][ispl] + spkl = [[]] * len(spikelist) + # print len(spikelist) + for i in xrange(len(spikelist)): # across trials + try: + len(spikelist[i]) + spkl[i] = spikelist[i] + except: + spkl[i] = [np.array(spikelist[i])] + # print 'cf: %f spl: %d nspk: %f' % (cf, SPL, len(spkl[i])) + return spikelist + + def read_AN_data( + self, + freq=10000, + CF=5300, + spontclass="HS", + display=False, + stim="BFTone", + setflag=True, + ignoreflag=True, + ): + """ + read responses of auditory nerve model of Zilany et al. (2009). + display = True plots the psth's for all ANF channels in the dataset + Request response to stimulus at Freq, for fiber with CF + and specified spont rate + This version is for rate-intensity runs March 2014. + Returns: Nothing + """ + + if not ignoreflag: + assert self.data_read_flag == False + # print 'Each instance of ManageANSPikes is allowed ONE data set to manage' + # print 'This is a design decision to avoid confusion about which data is in the instance' + # exit() + if stim in ["BFTone", "Tone"]: + fname = "%s_F%06.3f_CF%06.3f_%2s.mat" % ( + self.dataType, + freq / 1000.0, + CF / 1000.0, + spontclass, + ) + elif stim == "Noise": + fname = "%s_Noise_CF%06.3f_%2s.mat" % ( + self.dataType, + CF / 1000.0, + spontclass, + ) + + # print 'Reading: %s' % (fname) + try: + mfile = scipy.io.loadmat( + os.path.join(self.data_dir, fname), squeeze_me=True + ) + except IOError: + print("get_anspikes::read_AN_data: Failed to find data file %s" % (fname)) + print( + "Corresponding to Freq: %f CF: %f spontaneous rate class: %s" + % (freq, CF, spontclass) + ) + exit() + + n_spl = len(mfile[self.dataType]) + if display: + n = int(np.sqrt(n_spl)) + 1 + mg = np.meshgrid(range(n), range(n)) + mg = [mg[0].flatten(), mg[1].flatten()] + spkl = [[]] * n_spl + spl = np.zeros(n_spl) + for k in range(n_spl): + spkl[k] = mfile[self.dataType]["data"][k] # get data for one SPL + spl[k] = mfile[self.dataType]["SPL"][k] # get SPL for this set of runs + # print spkl[k].shape + if display: + self.display(spkl[k], k, n, mg) + + if display: + MP.show() + self.spikelist = spkl # save these so we can have a single point to parse them + self.SPLs = spl + self.n_reps = mfile[self.dataType]["nrep"] + if setflag: + self.data_read_flag = True + # return spkl, spl, mfile['RI']['nrep'] + + def getANatSPL(self, spl=None): + if not self.dataRead: + print("getANatSPL: No data read yet") + return None + try: + k = int(np.where(self.SPLs == spl)[0]) + except (ValueError, TypeError) as e: + print( + "get_anspikes::getANatSPL: spl=%6.1f not in list of spls:" % (spl), + self.SPLs, + ) + exit() # no match + # now clean up array to keep it iterable upon return + spkl = [[]] * len(self.spikelist[k]) + for i in xrange(len(self.spikelist[k])): + try: + len(self.spikelist[k][i]) + spkl[i] = self.spikelist[k][i] + except: + spkl[i] = [np.array(self.spikelist[k][i])] + return spkl # returns all the trials in the list + + def get_AN_info(self): + """ + :return: Dictionary of nReps and SPLs that are in the current data set (instance). + """ + + if not self.data_read_flag: + print("getANatSPL: No data read yet") + return None + return {"nReps": self.n_reps, "SPLs": self.SPLs} + + def read_cmrr_data(self, P, signal="S0", display=False): + """ + read responses of auditory nerve model of Zilany et al. (2009). + The required parameters are passed via the class P. + This includes: + + the SN (s2m) is the signal-to-masker ratio to select + the mode is 'CMR', 'CMD' (deviant) or 'REF' (reference, no flanking bands), + 'Tone', or 'Noise' + the Spont rate group + + signal is either 'S0' (signal present) or 'NS' (no signal) + display = True plots the psth's for all ANF channels in the dataset + Returns: + tuple of (spikelist and frequency list for modulated tones) + """ + + if P.modF == 10: + datablock = "%s_F4000.0" % (P.mode) + else: + datablock = "%s_F4000.0_M%06.1f" % ( + P.mode, + P.modF, + ) # 100 Hz modulation directory + # print P.mode + fs = os.listdir(self.datadir + datablock) + s_sn = "SN%03d" % (P.s2m) + fntemplate = "(\S+)_%s_%s_%s_%s.mat" % (s_sn, P.mode, signal, P.SR) + p = re.compile(fntemplate) + fl = [re.match(p, file) for file in fs] + fl = [f.group(0) for f in fl if f != None] # returns list of files matching... + fr = [float(f[1:7]) for f in fl] # frequency list + if display: + self.makeFig() + i = 0 + j = 0 + spkl = [[]] * len(fl) + n = int(np.sqrt(len(fl))) + mg = np.meshgrid(range(n), range(n)) + mg = [mg[0].flatten(), mg[1].flatten()] + for k, fi in enumerate(fl): + mfile = scipy.io.loadmat( + self.datadir + datablock + "/" + fi, squeeze_me=True + ) + spkl[k] = mfile["Cpsth"] + if display: + self.display(spkl[k], k, n, mg) + + if display: + MP.show() + return (spkl, fr) + + def make_fig(self): + self.fig = MP.figure(1) + + def combine_reps(self, spkl): + """ + + Just turns the spike list into one big linear sequence. + :param spkl: a spike train (nreps of numpy arrays) + :return: all the spikes in one long array. + """ + + # print spkl + allsp = np.array(self.flatten(spkl)) + # print allsp.shape + if allsp.shape == (): # nothing to show + return + # print allsp + spks = [] + for i in range(len(allsp)): + if allsp[i] == (): + continue + if isinstance(allsp[i], float): + spks.append(allsp[i]) + else: + spks.extend(np.array(allsp[i])) + return spks + + def display(self, spkl, k, n, mg): + # print spkl + spks = self.combine_reps(spkl) + if spks != []: + spks = np.sort(spks) + MP.hist(spks, 100) + return + + def flatten(self, x): + """ + flatten(sequence) -> list + + Returns a single, flat list which contains all elements retrieved + from the sequence and all recursively contained sub-sequences + (iterables). + """ + result = [] + if isinstance(x, float) or len([x]) <= 1: + return x + for el in x: + if hasattr(el, "__iter__") and not isinstance(el, basestring): + result.extend(self.flatten(el)) + else: + result.append(el) + return result + + +if __name__ == "__main__": + from cnmodel.util import Params + + modes = ["CMR", "CMD", "REF"] + sr_types = ["H", "M", "L"] + sr_type = sr_types[0] + sr_names = { + "H": "HS", + "M": "MS", + "L": "LS", + } # spontaneous rate groups (AN fiber input selection) + # define params for reading/testing. This is a subset of params... + P = Params( + mode=modes[0], + s2m=0, + n_rep=0, + start_rep=0, + sr=sr_names["L"], + modulation_freq=10, + DS_phase=0, + dataset="test", + sr_types=sr_types, + sr_names=sr_names, + fileVersion=1.5, + ) + + manager = ManageANSpikes() # create instance of the manager + print(dir(manager)) + test = "RI" + + if test == "all": + manager.read_all_ANdata( + freq=10000.0, CFList=manager.CF_map, spontclass="HS", stim="BFTone" + ) + # spkl = manager.retrieve_from_all_AN(manager.CF_map[0], 40.) + # spks = manager.combine_reps(spkl) + manager.plot_Rate_vs_F(freq=manager.CF_map[0], display=True, spontclass="HS") + MP.show() + + if test == manager.dataType: + spikes = manager.getANatFandSPL(spontclass="MS", freq=10000.0, CF=None, SPL=50) + spks = manager.combine_reps(spikes) + if spks != []: + print("spikes.") + manager.plot_RI_vs_F(freq=10000.0, display=True, spontclass="MS") + MP.figure(11) + spks = np.sort(spks) + MP.hist(spks, 100) + MP.show() diff --git a/cnmodel/util/matlab_proc.py b/cnmodel/util/matlab_proc.py new file mode 100644 index 0000000..66ab588 --- /dev/null +++ b/cnmodel/util/matlab_proc.py @@ -0,0 +1,402 @@ +from __future__ import print_function + +""" +Simple system for interfacing with a MATLAB process using stdin/stdout pipes. + +""" +from .process import Process +from io import StringIO +import scipy.io +import numpy as np +import tempfile +import os, sys, glob, signal +import weakref +import atexit + + +class MatlabProcess(object): + """ This class starts a new matlab process, allowing remote control of + the interpreter and transfer of data between python and matlab. + """ + + # Implements a MATLAB CLI that is a bit easier for us to parse + _bootstrap = r""" + while true + fprintf('\n::ready\n'); + + % Accumulate command lines until we see ::cmd_done + cmd = ''; + empty_count = 0; + while true + line = input('', 's'); + if length(line) == 0 + % If we encounter many empty lines, assume the host process + % has ended. Is there a better way to detect this? + empty_count = empty_count + 1; + if empty_count > 100 + fprintf('Shutting down!\n'); + exit; + end + end + if strcmp(line, '::cmd_done') + break + end + cmd = [cmd, line, sprintf('\n')]; + end + + % Evaluate command + try + %fprintf('EVAL: %s\n', cmd); + eval(cmd); + fprintf('\n::ok\n'); + catch err + fprintf('\n::err\n'); + fprintf(['::message:', err.message, '\n']); + fprintf(['::identifier:', err.identifier, '\n']); + for i = 1:length(err.stack) + frame = err.stack(i,1); + fprintf(['::stack:', frame.name, ' in ', frame.file, ' line ', frame.line]); + end + end + end + """ + + def __init__(self, executable=None, **kwds): + self.__closed = False + self.__refs = weakref.WeakValueDictionary() + + # Decide which executables to try + if executable is not None: + execs = [executable] + else: + execs = ["matlab"] # always pick the matlab in the path, if available + if sys.platform == "darwin": + installed = glob.glob("/Applications/MATLAB_R*") + installed.sort(reverse=True) + execs.extend([os.path.join(p, "bin", "matlab") for p in installed]) + + # try starting each in order until one works + self.__proc = None + for exe in execs: + try: + self.__proc = Process([exe, "-nodesktop", "-nosplash"], **kwds) + break + except Exception as e: + last_exception = e + pass + + # bail out if we couldn't start any + if self.__proc is None: + raise RuntimeError( + "Could not start MATLAB process.\nPaths attempted: %s " + "\nLast error: %s" % (str(execs), str(e)) + ) + + # Wait a moment for MATLAB to start up, + # read the version string + while True: + line = self.__proc.stdout.readline() + if "Copyright" in line: + # next line is version info + self.__version_str = self.__proc.stdout.readline().strip() + break + + # start input loop + self.__proc.stdin.write(self._bootstrap) + + # wait for input loop to be ready + while True: + line = self.__proc.stdout.readline() + if line == "::ready\n": + break + + atexit.register(self.close) + + def __call__(self, cmd, parse_result=True): + """ + Execute the specified statement(s) on the MATLAB interpreter and return + the output string or raise an exception if there was an error. + """ + assert not self.__closed, "MATLAB process has already closed." + if cmd[-1] != "\n": + cmd += "\n" + cmd += "::cmd_done\n" + self.__proc.stdout.read() + self.__proc.stdin.write(cmd) + + if parse_result: + return self._parse_result() + + def _parse_result(self): + assert not self.__closed, "MATLAB process has already closed." + output = [] + while True: + line = self.__proc.stdout.readline() + if line == "::ready\n": + break + output.append(line) + + for i in reversed(range(len(output))): + line = output[i] + if line == "::ok\n": + return "".join(output[:i]) + elif line == "::err\n": + raise MatlabError(output[i + 1 :], output[:i]) + + raise RuntimeError("No success/failure code found in output (printed above).") + + def _get(self, name): + """ + Transfer an object from MATLAB to Python. + """ + assert isinstance(name, str) + tmp = tempfile.mktemp(suffix=".mat") + out = self("save('%s', '%s', '-v7')" % (tmp, name)) + objs = scipy.io.loadmat(tmp) + os.remove(tmp) + return objs[name] + + def _set(self, **kwds): + """ + Transfer an object from Python to MATLAB and assign it to the given + variable name. + """ + tmp = tempfile.mktemp(suffix=".mat") + scipy.io.savemat(tmp, kwds) + self("load('%s')" % tmp) + os.remove(tmp) + + def _get_via_pipe(self, name): + """ + Transfer an object from MATLAB to Python. + + This method sends data over the pipe, but is less reliable than get(). + """ + assert isinstance(name, str) + out = self("save('stdio', '%s', '-v7')" % name) + start = stop = None + for i, line in enumerate(out): + if line.startswith("start_binary"): + start = i + elif line.startswith("::ok"): + stop = i + data = "".join(out[start + 1 : stop]) + io = StringIO(data[:-1]) + objs = scipy.io.loadmat(io) + return objs[name] + + def _set_via_pipe(self, **kwds): + """ + Transfer an object from Python to MATLAB and assign it to the given + variable name. + + This method sends data over the pipe, but is less reliable than set(). + """ + assert not self.__closed, "MATLAB process has already closed." + io = StringIO() + scipy.io.savemat(io, kwds) + io.seek(0) + strn = io.read() + self.__proc.stdout.read() + self.__proc.stdin.write("load('stdio')\n::cmd_done\n") + while True: + line = self.__proc.stdout.readline() + if line == "ack load stdio\n": + # now it is safe to send data + break + self.__proc.stdin.write(strn) + self.__proc.stdin.write("\n") + while True: + line = self.__proc.stdout.readline() + if line == "ack load finished\n": + break + self._parse_result() + + def exist(self, name): + if name == "exist": + return 5 + else: + for line in self("exist %s" % name).split("\n"): + try: + return int(line.strip()) + except ValueError: + pass + + def __getattr__(self, name): + if name not in self.__refs: + ex = self.exist(name) + if ex == 0: + raise AttributeError("No object named '%s' in matlab workspace." % name) + elif ex in (2, 3, 5): + r = MatlabFunction(self, name) + elif ex == 1: + r = self._mkref(name) + else: + typ = {4: "library file", 6: "P-file", 7: "folder", 8: "class"}[ex] + raise TypeError("Variable '%s' has unsupported type '%s'" % (name, typ)) + self.__refs[name] = r + return self.__refs[name] + + def _mkref(self, name): + assert name not in self.__refs + ref = MatlabReference(self, name) + self.__refs[name] = ref + return ref + + def __setattr__(self, name, value): + if name.startswith("_MatlabProcess__"): + object.__setattr__(self, name, value) + else: + self._set(**{name: value}) + + def close(self): + if self.__closed: + return + self("exit;\n", parse_result=False) + self.__closed = True + + +class MatlabReference(object): + """ Reference to a variable in the matlab workspace. + """ + + def __init__(self, proc, name): + self._proc = proc + self._name = name + + @property + def name(self): + return self._name + + def get(self): + return self._proc._get(self._name) + + def clear(self): + self._proc("clear %s;" % self._name) + + def __del__(self): + self.clear() + + +class MatlabFunction(object): + """ + Proxy to a MATLAB function. + + Calling this object transfers the arguments to MATLAB, invokes the function + remotely, and then transfers the return value back to Python. + """ + + def __init__(self, proc, name): + self._proc = proc + self._name = name + self._nargout = None + + @property + def nargout(self): + """ Number of output arguments for this function. + + For some functions, requesting nargout() will fail. In these cases, + the nargout property must be set manually before calling the function. + """ + if self._nargout is None: + cmd = "fprintf('%%d\\n', nargout('%s'));" % (self._name) + ret = self._proc(cmd) + self._nargout = int(ret.strip()) + return self._nargout + + @nargout.setter + def nargout(self, n): + self._nargout = n + + def __call__(self, *args, **kwds): + """ + Call this function with the given arguments. + + If _transfer is False, then the return values are left in MATLAB and + references to these values are returned instead. + """ + import pyqtgraph as pg + + _transfer = kwds.pop("_transfer", True) + assert len(kwds) == 0 + + # for generating unique variable names + rand = np.random.randint(1e12) + + # store args to temporary variables, excluding those already present + # in the workspace + argnames = [] + upload = {} + for i, arg in enumerate(args): + if isinstance(arg, MatlabReference): + argnames.append(arg.name) + elif np.isscalar(arg): + argnames.append(repr(arg)) + else: + argname = "%s_%d_%d" % (self._name, i, rand) + argnames.append(argname) + upload[argname] = arg + if len(upload) > 0: + self._proc._set(**upload) + + try: + # get number of output args + nargs = self.nargout + + # invoke function, fetch return value(s) + retvars = ["%s_rval_%d_%d" % (self._name, i, rand) for i in range(nargs)] + cmd = "[%s] = %s(%s);" % (",".join(retvars), self._name, ",".join(argnames)) + self._proc(cmd) + if _transfer: + ret = [self._proc._get(var) for var in retvars] + self._proc("clear %s;" % (" ".join(retvars))) + else: + ret = [self._proc._mkref(name) for name in retvars] + if len(ret) == 1: + ret = ret[0] + else: + ret = tuple(ret) + return ret + finally: + # clear all temp variables + clear = list(upload.keys()) + # if _transfer: + # clear += retvars + if len(clear) > 0: + cmd = "clear %s;" % (" ".join(clear)) + self._proc(cmd) + return ret + + +class MatlabError(Exception): + def __init__(self, error, output): + self.output = "".join(output) + for line in error: + self.stack = [] + if line.startswith("::message:"): + self.message = line[10:].strip() + elif line.startswith("::identifier:"): + self.identifier = line[13:].strip() + elif line.startswith("::stack:"): + self.stack.append(line[8:].strip()) + + def __repr__(self): + return "MatlabError(message=%s, identifier=%s)" % ( + repr(self.message), + repr(self.identifier), + ) + + def __str__(self): + if len(self.stack) > 0: + stack = "\nMATLAB Stack:\n%s\nMATLAB Error: " % "\n".join(self.stack) + return stack + self.message + else: + return self.message + + +if __name__ == "__main__": + p = MatlabProcess() + io = StringIO() + scipy.io.savemat(io, {"x": 1}) + io.seek(0) + strn = io.read() diff --git a/cnmodel/util/nrnutils.py b/cnmodel/util/nrnutils.py new file mode 100644 index 0000000..e342587 --- /dev/null +++ b/cnmodel/util/nrnutils.py @@ -0,0 +1,218 @@ +from __future__ import print_function + +""" +Wrapper classes to make working with NEURON easier. + +Author: Andrew P. Davison, UNIC, CNRS +""" + +__version__ = "0.3.0" + +from neuron import nrn, h, hclass + +h.load_file("stdrun.hoc") + +PROXIMAL = 0 +DISTAL = 1 + + +class Mechanism(object): + """ + Examples: + >>> leak = Mechanism('pas', {'e': -65, 'g': 0.0002}) + >>> hh = Mechanism('hh') + added set_parameters to allow post-instantiation parameter modification + """ + + def __init__(self, name, **parameters): + self.name = name + self.parameters = parameters + + def set_parameters(self, parameters): + self.parameters = parameters + + def insert_into(self, section): + section.insert(self.name) + for name, value in self.parameters.items(): + for segment in section: + mech = getattr(segment, self.name) + setattr(mech, name, value) + + +class Section(nrn.Section): + """ + Examples: + >>> soma = Section(L=30, diam=30, mechanisms=[hh, leak]) + >>> apical = Section(L=600, diam=2, nseg=5, mechanisms=[leak], + ... parent=soma, connection_point=DISTAL) + """ + + def __init__( + self, + L, + diam, + nseg=1, + Ra=100, + cm=1, + mechanisms=[], + parent=None, + connection_point=DISTAL, + ): + nrn.Section.__init__(self) + # set geometry + self.L = L + self.diam = diam + self.nseg = nseg + # set cable properties + self.Ra = Ra + self.cm = cm + # connect to parent section + if parent: + self.connect(parent, connection_point, PROXIMAL) + # add ion channels + for mechanism in mechanisms: + mechanism.insert_into(self) + + def add_synapses(self, label, type, locations=[0.5], **parameters): + if hasattr(self, label): + raise Exception("Can't overwrite synapse labels (to keep things simple)") + synapse_group = [] + for location in locations: + synapse = getattr(h, type)(location, sec=self) + for name, value in parameters.items(): + setattr(synapse, name, value) + synapse_group.append(synapse) + if len(synapse_group) == 1: + synapse_group = synapse_group[0] + setattr(self, label, synapse_group) + + add_synapse = add_synapses # for backwards compatibility + + def plot(self, variable, location=0.5, tmin=0, tmax=5, xmin=-80, xmax=40): + import neuron.gui + + self.graph = h.Graph() + h.graphList[0].append(self.graph) + self.graph.size(tmin, tmax, xmin, xmax) + self.graph.addvar("%s(%g)" % (variable, location), sec=self) + + def record_spikes(self, threshold=-30): + self.spiketimes = h.Vector() + self.spikecount = h.APCount(0.5, sec=self) + self.spikecount.thresh = threshold + self.spikecount.record(self.spiketimes) + + +def alias(attribute_path): + """ + Returns a new property, mapping an attribute nested in an object hierarchy + to a simpler name + + For example, suppose that an object of class A has an attribute b which + itself has an attribute c which itself has an attribute d. Then placing + e = alias('b.c.d') + in the class definition of A makes A.e an alias for A.b.c.d + """ + + parts = attribute_path.split(".") + attr_name = parts[-1] + attr_path = parts[:-1] + + def set(self, value): + obj = reduce(getattr, [self] + attr_path) + setattr(obj, attr_name, value) + + def get(self): + obj = reduce(getattr, [self] + attr_path) + return getattr(obj, attr_name) + + return property(fset=set, fget=get) + + +def uniform_property(section_list, attribute_path): + """ + Define a property that will have a uniform value across a list of sections. + + For example, suppose we define a neuron model as a class A, which contains + three compartments: soma, dendrite and axon. Then placing + + gnabar = uniform_property(["soma", "axon"], "hh.gnabar") + + in the class definition of A means that setting a.gnabar (where a is an + instance of A) will set the value of hh.gnabar in both the soma and axon, i.e. + + a.gnabar = 0.01 + + is equivalent to: + + for sec in [a.soma, a.axon]: + for seg in sec: + seg.hh.gnabar = 0.01 + + """ + parts = attribute_path.split(".") + attr_name = parts[-1] + attr_path = parts[:-1] + + def set(self, value): + for sec_name in section_list: + sec = getattr(self, sec_name) + for seg in sec: + obj = reduce(getattr, [seg] + attr_path) + setattr(obj, attr_name, value) + + def get(self): + sec = getattr(self, section_list[0]) + obj = reduce(getattr, [sec(0.5)] + attr_path) + return getattr(obj, attr_name) + + return property(fset=set, fget=get) + + +if __name__ == "__main__": + + class SimpleNeuron(object): + def __init__(self): + # define ion channel parameters + leak = Mechanism("pas", e=-65, g=0.0002) + hh = Mechanism("hh") + # create cable sections + self.soma = Section(L=30, diam=30, mechanisms=[hh]) + self.apical = Section( + L=600, + diam=2, + nseg=5, + mechanisms=[leak], + parent=self.soma, + connection_point=DISTAL, + ) + self.basilar = Section( + L=600, + diam=2, + nseg=5, + mechanisms=[leak], + parent=self.soma, + connection_point=0.5, + ) + self.axon = Section( + L=1000, diam=1, nseg=37, mechanisms=[hh], connection_point=0 + ) + # synaptic input + self.soma.add_synapses("syn", "AlphaSynapse", onset=0.5, gmax=0.05, e=0) + + gnabar = uniform_property(["soma", "axon"], "hh.gnabar") + gkbar = uniform_property(["soma", "axon"], "hh.gkbar") + + neuron = SimpleNeuron() + neuron.soma.plot("v") + neuron.apical.plot("v") + + print("gNa_bar: ", neuron.gnabar) + neuron.gnabar = 0.15 + assert neuron.soma(0.5).hh.gnabar == 0.15 + + h.dt = 0.025 + v_init = -65 + tstop = 5 + h.finitialize(v_init) + h.run() diff --git a/cnmodel/util/process.py b/cnmodel/util/process.py new file mode 100644 index 0000000..425a258 --- /dev/null +++ b/cnmodel/util/process.py @@ -0,0 +1,111 @@ +from __future__ import print_function + +""" +Utility class for spawning and controlling CLI processes. +See: http://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python +""" +import sys, time +from subprocess import PIPE, Popen +from threading import Thread + +try: + from Queue import Queue, Empty +except ImportError: + from queue import Queue, Empty # python 3.x + +ON_POSIX = "posix" in sys.builtin_module_names + + +class Process(object): + """ + Process encapsulates a subprocess with queued stderr/stdout pipes. + Wraps most methods from subprocess.Popen. + + For non-blocking reads, use proc.stdout.get_nowait() or + proc.stdout.get(timeout=0.1). + """ + + def __init__(self, exec_args, cwd=None): + self.proc = Popen( + exec_args, + stdout=PIPE, + stdin=PIPE, + stderr=PIPE, + bufsize=1, + close_fds=ON_POSIX, + universal_newlines=True, + cwd=cwd, + ) + self.stdin = self.proc.stdin + # self.stdin = PipePrinter(self.proc.stdin) + self.stdout = PipeQueue(self.proc.stdout) + self.stderr = PipeQueue(self.proc.stderr) + for method in ["poll", "wait", "send_signal", "kill", "terminate"]: + setattr(self, method, getattr(self.proc, method)) + + +class PipePrinter(object): + """ For debugging writes to a pipe. + """ + + def __init__(self, pipe): + self._pipe = pipe + + def __getattr__(self, attr): + return getattr(self._pipe, attr) + + def write(self, strn): + print("WRITE:" + repr(strn)) + return self._pipe.write(strn) + + +class PipeQueue(Queue): + """ + Queue that starts a second process to monitor a PIPE for new data. + This is needed to allow non-blocking pipe reads. + """ + + def __init__(self, pipe): + Queue.__init__(self) + self.thread = Thread(target=self.enqueue_output, args=(pipe, self)) + self.thread.daemon = True # thread dies with the program + self.thread.start() + + @staticmethod + def enqueue_output(out, queue): + for line in iter(out.readline, b""): + queue.put(line) + # print "READ: " + repr(line) + out.close() + + def read(self): + """ + Read all available lines from the queue, concatenated into a single string. + """ + out = "" + while True: + try: + out += self.get_nowait() + except Empty: + break + return out + + def readline(self, timeout=None): + """ Read a single line from the queue. + """ + # we break this up into multiple short reads to allow keyboard + # interrupts + start = time.time() + ret = "" + while True: + if timeout is not None: + remaining = start + timeout - time.time() + if remaining <= 0: + return "" + else: + remaining = 1 + + try: + return self.get(timeout=min(0.1, remaining)) + except Empty: + pass diff --git a/cnmodel/util/pynrnutilities.py b/cnmodel/util/pynrnutilities.py new file mode 100755 index 0000000..175abde --- /dev/null +++ b/cnmodel/util/pynrnutilities.py @@ -0,0 +1,734 @@ +from __future__ import print_function + +#!/usr/bin/python +# +# utilities for NEURON, in Python +# Module neuron for cnmodel + +import numpy as np +import numpy.ma as ma # masked array +import re, sys, gc, collections + +import neuron + + +_mechtype_cache = None + + +def all_mechanism_types(): + """Return a dictionary of all available mechanism types. + + Each dictionary key is the name of a mechanism and each value is + another dictionary containing information about the mechanism:: + + mechanism_types = { + 'mech_name1': { + 'point_process': bool, + 'artificial_cell': bool, + 'netcon_target': bool, + 'has_netevent': bool, + 'internal_type': int, + 'globals': {name:size, ...}, + 'parameters': {name:size, ...}, + 'assigned': {name:size, ...}, + 'state': {name:size, ...}, + }, + 'mech_name2': {...}, + 'mech_name3': {...}, + ... + } + + * point_process: False for distributed mechanisms, True for point + processes and artificial cells. + * artificial_cell: True for artificial cells, False otherwise + * netcon_target: True if the mechanism can receive NetCon events + * has_netevent: True if the mechanism can emit NetCon events + * internal_type: Integer specifying the NEURON internal type index of + the mechanism + * globals: dict of the name and vector size of the mechanism's global + variables + * parameters: dict of the name and vector size of the mechanism's + parameter variables + * assigned: dict of the name and vector size of the mechanism's + assigned variables + * state: dict of the name and vector size of the mechanism's state + variables + + + Note: The returned data structure is cached; do not modify it. + + For more information on global, parameter, assigned, and state + variables see: + http://www.neuron.yale.edu/neuron/static/docs/help/neuron/nmodl/nmodl.html + """ + global _mechtype_cache + if _mechtype_cache is None: + _mechtype_cache = collections.OrderedDict() + mname = neuron.h.ref("") + # Iterate over two mechanism types (distributed, point/artificial) + for i in [0, 1]: + mt = neuron.h.MechanismType(i) + nmech = int(mt.count()) + # Iterate over all mechanisms of this type + for j in range(nmech): + mt.select(j) + mt.selected(mname) + + # General mechanism properties + name = mname[0] # convert hoc string ptr to python str + + desc = { + "point_process": bool(i), + "netcon_target": bool(mt.is_netcon_target(j)), + "has_netevent": bool(mt.has_net_event(j)), + "artificial_cell": bool(mt.is_artificial(j)), + "internal_type": int(mt.internal_type()), + } + + # Collect information about 4 different types of variables + for k, ptype in [ + (-1, "globals"), + (1, "parameters"), + (2, "assigned"), + (3, "state"), + ]: + desc[ptype] = {} # collections.OrderedDict() + ms = neuron.h.MechanismStandard(name, k) + for l in range(int(ms.count())): + psize = ms.name(mname, l) + pname = mname[0] # parameter name + desc[ptype][pname] = int(psize) + + # Assemble everything in one place + _mechtype_cache[name] = desc + + return _mechtype_cache + + +def reset(raiseError=True): + """Introspect the NEURON kernel to verify that no objects are left over + from previous simulation runs. + """ + # Release objects held by an internal buffer + # See https://www.neuron.yale.edu/phpBB/viewtopic.php?f=2&t=3221 + neuron.h.Vector().size() + + # Make sure nothing is hanging around in an old exception or because of + # reference cycles + + # sys.exc_clear() + gc.collect(2) + neuron.h.Vector().size() + numsec = 0 + + remaining = [] + n = len(list(neuron.h.allsec())) + + if n > 0: + remaining.append((n, "Section")) + + n = len(neuron.h.List("NetCon")) + if n > 0: + remaining.append((n, "NetCon")) + + # No point processes or artificial cells left + for name, typ in all_mechanism_types().items(): + if typ["artificial_cell"] or typ["point_process"]: + n = len(neuron.h.List(name)) + if n > 0: + remaining.append((n, name)) + + if ( + len(remaining) > 0 and raiseError + ): # note that not raising the error leads to memory leak + msg = "Cannot reset--old objects have not been cleared: %s" % ", ".join( + ["%d %s" % rem for rem in remaining] + ) + raise RuntimeError(msg) + + +def custom_init(v_init=-60.0): + """ + Perform a custom initialization of the current model/section. + + This initialization follows the scheme outlined in the + NEURON book, 8.4.2, p 197 for initializing to steady state. + + N.B.: For a complex model with dendrites/axons and different channels, + this initialization will not find the steady-state the whole cell, + leading to initial transient currents. In that case, this initialization + should be followed with a 0.1-5 second run (depends on the rates in the + various channel mechanisms) with no current injection or active point + processes to allow the system to settle to a steady- state. Use either + h.svstate or h.batch_save to save states and restore them. Batch is + preferred + + Parameters + ---------- + v_init : float (default: -60 mV) + Voltage to start the initialization process. This should + be close to the expected resting state. + """ + inittime = -1e10 + tdt = neuron.h.dt # save current step size + dtstep = 1e9 + neuron.h.finitialize(v_init) + neuron.h.t = inittime # set time to large negative value (avoid activating + # point processes, we hope) + tmp = neuron.h.cvode.active() # check state of variable step integrator + if tmp != 0: # turn off CVode variable step integrator if it was active + neuron.h.cvode.active(0) # now just use backward Euler with large step + neuron.h.dt = dtstep + n = 0 + while neuron.h.t < -1e9: # Step forward + neuron.h.fadvance() + n += 1 + # print('advances: ', n) + if tmp != 0: + neuron.h.cvode.active(1) # restore integrator + neuron.h.t = 0 + if neuron.h.cvode.active(): + neuron.h.cvode.re_init() # update d(state)/dt and currents + else: + neuron.h.fcurrent() # recalculate currents + neuron.h.frecord_init() # save new state variables + neuron.h.dt = tdt # restore original time step + + +# routine to convert conductances from nS as given elsewhere +# to mho/cm2 as required by NEURON 1/28/99 P. Manis +# units: nano siemens, soma area in um^2 +# +def nstomho(ns, somaarea, refarea=None): + if refarea == None: + return 1e-9 * float(ns) / float(somaarea) + else: + return 1e9 * float(ns) / float(refarea) + + +def mho2ns(mho, somaarea): + return float(mho) * somaarea / 1e-9 + + +def spherearea(dia): + """ + given diameter in microns, return sphere area in cm2 + """ + r = dia * 1e-4 # convert to cm + return 4 * np.pi * r ** 2 + + +def get_sections(h): + """ + go through all the sections and find the names of the sections and all of their + parts (ids). Returns a dict, of sec: [id0, id1...] + + """ + secnames = {} + resec = re.compile("(\w+)\[(\d*)\]") + for sec in h.allsec(): + g = resec.match(sec.name()) + if g.group(1) not in secnames.keys(): + secnames[g.group(1)] = [int(g.group(2))] + else: + secnames[g.group(1)].append(int(g.group(2))) + return secnames + + +def all_objects(): + """ Return a dict of all objects known to NEURON. + + Keys are 'Section', 'Segment', 'Mechanism', 'Vector', 'PointProcess', + 'NetCon', ... + """ + objs = {} + objs["Section"] = list(h.all_sec()) + objs["Segment"] = [] + for sec in objs["Section"]: + objs["Segment"].extend(list(sec.allseg())) + objs["PointProcess"] = [] + for seg in objs["Segment"]: + objs["PointProcess"].extend(list(seg.point_processes())) + + return objs + + +def alpha(alpha=0.1, delay=1, amp=1.0, tdur=50.0, dt=0.010): + tvec = np.arange(0, tdur, dt) + aw = np.zeros(tvec.shape) + i = 0 + for t in tvec: + if t > delay: + aw[i] = ( + amp * (t - delay) * (1.0 / alpha) * np.exp(-(t - delay) / alpha) + ) # alpha waveform time course + else: + aw[i] = 0.0 + i += 1 + return (aw, tvec) + + +def syns( + alpha=0.1, + rate=10, + delay=0, + dur=50, + amp=1.0, + dt=0.020, + N=1, + mindur=120, + makewave=True, +): + """ Calculate a poisson train of alpha waves + with mean rate rate, with a delay and duration (in mseco) dt in msec. + N specifies the number of such independent waveforms to sum """ + deadtime = 0.7 + if dur + delay < mindur: + tvec = np.arange(0.0, mindur, dt) + else: + tvec = np.arange(0.0, dur + delay, dt) + npts = len(tvec) + ta = np.arange(0.0, 20.0, dt) + aw = ta * alpha * np.exp(-ta / alpha) / alpha # alpha waveform time course + spt = [[]] * N # list of spike times + wave = np.array([]) # waveform + sptime = [] + for j in range(0, N): + done = False + t = 0.0 + nsp = 0 + while not done: + a = np.random.sample(1) + if t < delay: + t = delay + continue + if t >= delay and t <= (delay + dur): + ti = -np.log(a) / ( + rate / 1000.0 + ) # convert to exponential distribution with rate + if ti < deadtime: + continue + t = t + ti # running time + if t > delay + dur: + done = True + continue + if nsp is 0: + sptime = t + nsp = nsp + 1 + else: + sptime = np.append(sptime, t) + nsp = nsp + 1 + if j is 0: + wavej = np.zeros(len(tvec)) + for i in range(0, len(sptime)): + st = int(sptime[i] / dt) + wavej[st] = wavej[st] + 1 + spt[j] = sptime + + if makewave: + w = np.convolve(wavej, aw / max(aw)) * amp + if len(w) < npts: + w = np.append(w, np.zeros(npts - len(w))) + if len(w) > npts: + w = w[0:npts] + if j is 0: + wave = w + else: + wave = wave + w + return (spt, wave, tvec, N) + + +def an_syn( + alpha=0.1, + spont=10, + driven=100, + delay=50, + dur=100, + post=20, + amp=0.1, + dt=0.020, + N=1, + makewave=True, +): + # constants for AN: + deadtime = 0.7 # min time between spikes, msec + trise = 0.2 # rise rate, ms + tfall = 0.5 # fall rate, ms + rss = driven / 1000.0 # spikes/millisecond + rr = 3 * rss # transient driven rate + rst = rss + taur = 3 # rapid decay, msec + taust = 10 + ton = delay # msec + stim_end = ton + dur + trace_end = stim_end + post + tvec = np.arange(0.0, trace_end, dt) # dt is in msec, so tvec is in milliseconds + ta = np.arange(0.0, 20.0, dt) + aw = ta * alpha * np.exp(-ta / alpha) / alpha # alpha waveform time course + spt = [[]] * N # list of spike times + wave = [[]] * N # waveform + for j in range(0, N): # for each + done = False + sptime = [] + qe = 0 + nsp = 0 + i = int(0) + if spont <= 0: + q = 1e6 + else: + q = 1000.0 / spont # q is in msec (spont in spikes/second) + t = 0.0 + while not done: + a = np.random.sample(1) + if t < ton: + if spont <= 0: + t = ton + continue + ti = -(np.log(a) / (spont / 1000.0)) # convert to msec + if ti < deadtime: # delete intervals less than deadtime + continue + t = t + ti + if t > ton: # if the interval would step us to the stimulus onset + t = ton # then set to to the stimulus onset + continue + if t >= ton and t < stim_end: + if t > ton: + rise = 1.0 - np.exp(-(t - ton) / trise) + else: + rise = 1.0 + ra = rr * np.exp(-(t - ton) / taur) + rs = rst * np.exp(-(t - ton) / taust) + q = rise * (ra + rs + rss) + ti = -np.log(a) / (q + spont / 1000) # random.negexp(1000/q) + if ti < deadtime: + continue + t = t + ti + if t > stim_end: # only include interval if it falls inside window + t = stim_end + continue + if t >= stim_end and t <= trace_end: + if spont <= 0.0: + t = trace_end + continue + if qe is 0: # have not calculated the new qe at end of stimulus + rise = 1.0 - np.exp(-(stim_end - ton) / trise) + ra = rr * np.exp(-(stim_end - ton) / taur) + rs = rst * np.exp(-(stim_end - ton) / taust) + qe = rise * (ra + rs + rss) # calculate the rate at the end + fall = np.exp(-(t - stim_end) / tfall) + q = qe * fall + ti = -np.log(a) / ( + q + spont / 1000.0 + ) # keeps rate from falling below spont rate + if ti < deadtime: + continue + t = t + ti + if t >= trace_end: + done = True + continue + # now add the spike time to the list + if nsp is 0: + sptime = t + nsp = nsp + 1 + else: + sptime = np.append(sptime, t) + nsp = nsp + 1 + # end of for loop on i + if j is 0: + wavej = np.zeros(len(tvec)) + for i in range(0, len(sptime)): + st = int(sptime[i] / dt) + wavej[st] = wavej[st] + 1 + spt[j] = sptime + npts = len(tvec) + if makewave: + w = np.convolve(wavej, aw / max(aw)) * amp + wave[j] = w[0:npts] + return (spt, wave, tvec, N) + + +def findspikes(t, v, thresh): + """ findspikes identifies the times of action potential in the trace v, with the + times in t. An action potential is simply timed at the first point that exceeds + the threshold. + """ + tm = np.array(t) + s0 = ( + np.array(v) > thresh + ) # np.where(v > thresh) # np.array(v) > thresh # find points above threshold + + # print ('v: ', v) + dsp = tm[s0] + if dsp.shape[0] == 1: + dsp = np.array(dsp) + sd = np.append(True, np.diff(dsp) > 1.0) # find first points of spikes + if len(dsp) > 0: + sp = dsp[sd] + else: + sp = [] + return sp # list of spike times. + + +def measure(mode, x, y, x0, x1): + """ return the mean and standard deviation of y in the window x0 to x1 + """ + xm = ma.masked_outside(x, x0, x1) + ym = ma.array(y, mask=ma.getmask(xm)) + if mode == "mean": + r1 = ma.mean(ym) + r2 = ma.std(ym) + if mode == "max": + r1 = ma.max(ym) + r2 = 0 + if mode == "min": + r1 = ma.min(ym) + r2 = 0 + if mode == "median": + r1 = ma.median(ym) + r2 = 0 + if mode == "p2p": # peak to peak + r1 = ma.ptp(ym) + r2 = 0 + return (r1, r2) + + +def mask(x, xm, x0, x1): + xmask = ma.masked_outside(xm, x0, x1) + xnew = ma.array(x, mask=ma.getmask(xmask)) + return xnew.compressed() + + +def vector_strength(spikes, freq): + """ + Calculate vector strength and related parameters from a spike train, for the specified frequency + :param spikes: Spike train, in msec. + :param freq: Stimulus frequency in Hz + :return: a dictionary containing: + + r: vector strength + n: number of spikes + R: Rayleigh coefficient + p: p value (is distribution not flat?) + ph: the circularized spike train over period of the stimulus freq, freq, in radians + d: the "dispersion" computed according to Ashida et al., 2010, etc. + """ + + per = 1e3 / freq # convert from Hz to period in msec + ph = 2 * np.pi * np.fmod(spikes, per) / (per) # convert to radians within a cycle + c = np.sum(np.cos(ph)) ** 2 + s = np.sum(np.sin(ph)) ** 2 + vs = (1.0 / len(ph)) * np.sqrt(c + s) # standard vector strength computation + n = len(spikes) + R = n * vs # Raleigh coefficient + Rp = np.exp(-n * vs * vs) # p value for n > 50 (see Ashida et al. 2010). + d = np.sqrt(2.0 * (1 - vs)) / (2 * np.pi * freq) + return {"r": vs, "n": n, "R": R, "p": Rp, "ph": ph, "d": d} + + +def isi_cv2(splist, binwidth=1, t0=0, t1=300, tgrace=25): + """ compute the cv and regularity according to Young et al., J. Neurophys, 60: 1, 1988. + Analysis is limited to isi's starting at or after t0 but before t1, and ending completely + before t1 + tgrace(to avoid end effects). t1 should correspond to the + the end of the stimulus + VERSION using dictionary for cvisi + """ + cvisit = np.arange(0, t1, binwidth) # build time bins + cvisi = {} # isi is dictionary, since each bin may have different length + for i in range(0, len(splist)): # for all the traces + isit = splist[i] # get the spike times for this trial [1:-1] + if len(isit) <= 1: # need 2 spikes to get an interval + continue + isib = np.floor(isit[0:-2] / binwidth) # discreetize + isii = np.diff(splist[i]) # isis. + for j in range(0, len(isib)): # loop over possible start time bins + if ( + isit[j] < t0 or isit[j] > t1 or isit[j + 1] > t1 + tgrace + ): # start time and interval in the window + continue + if isib[j] in cvisi: + print("spike in bin: %d" % (isib[j])) + cvisi[isib[j]] = np.append( + cvisi[isib[j]], isii[j] + ) # and add the isi in that bin + else: + cvisi[isib[j]] = isii[j] # create it + cvm = np.array([]) # set up numpy arrays for mean, std and time for cv analysis + cvs = np.array([]) + cvt = np.array([]) + for i in cvisi.keys(): # for each entry (possible bin) + c = [cvisi[i]] + s = c.shape + # print c + if len(s) > 1 and s[1] >= 3: # require 3 spikes in a bin for statistics + cvm = np.append(cvm, np.mean(c)) + cvs = np.append(cvs, np.std(c)) + cvt = np.append(cvt, i * binwidth) + return (cvisit, cvisi, cvt, cvm, cvs) + + +def isi_cv(splist, binwidth=1, t0=0, t1=300, tgrace=25): + """ compute the cv and regularity according to Young et al., J. Neurophys, 60: 1, 1988. + Analysis is limited to isi's starting at or after t0 but before t1, and ending completely + before t1 + tgrace(to avoid end effects). t1 should correspond to the + the end of the stimulus + Version using a list of numpy arrays for cvisi + """ + cvisit = np.arange(0, t1, binwidth) # build time bins + cvisi = [[]] * len(cvisit) + for i in range(0, len(splist)): # for all the traces + if len(splist[i]) < 2: # need at least 2 spikes + continue + isib = np.floor( + splist[i][0:-2] / binwidth + ) # begining spike times for each interval + isii = np.diff(splist[i]) # associated intervals + for j in range(0, len(isib)): # loop over spikes + if ( + splist[i][j] < t0 or splist[i][j] > t1 or splist[i][j + 1] > t1 + tgrace + ): # start time and interval in the window + continue + cvisi[int(isib[j])] = np.append( + cvisi[int(isib[j])], isii[j] + ) # and add the isi in that bin + cvm = np.array([]) # set up numpy arrays for mean, std and time for cv analysis + cvs = np.array([]) + cvt = np.array([]) + for i in range(0, len(cvisi)): # for each entry (possible bin) + c = cvisi[i] + if len(c) >= 3: # require 3 spikes in a bin for statistics + cvm = np.append(cvm, np.mean(c)) + cvs = np.append(cvs, np.std(c)) + cvt = np.append(cvt, i * binwidth) + return (cvisit, cvisi, cvt, cvm, cvs) + + +if __name__ == "__main__": + + test = "isicv" + + if test == "isicv": + """ this test is not perfect. Given an ISI, we calculate spike times + by drawing from a normal distribution whose standard deviation varies + with time, from 0 (regular) to 1 (irregular). As coded, the standard + deviation never reaches the target value because spikes fall before or + after previous spikes (thus reducing the stdev). Nonetheless, this shows + that the CV calculation works correctly. """ + nreps = 500 + # cv will be 0 for first 50 msec, 0.5 for next 50 msec, and 1 for next 50 msec + d = [[]] * nreps + isi = 5.0 # mean isi + # we create 100 msec of data where the CV goes from 0 to 1 + maxt = 100.0 + for i in range(nreps): + for j in range(int(maxt / isi) + 1): + t = float(j) * isi + sd = float(j) / isi + if sd == 0.0: + d[i] = np.append(d[i], t) + else: + d[i] = np.append(d[i], np.random.normal(t, sd, 1)) + for j in range(1, 10): # add more intervals at the end + te = t + float(j) * isi + d[i] = np.append(d[i], np.random.normal(te, sd, 1)) + d[i] = np.sort(d[i]) + # print d[i] + # print diff(d[i]) + sh = np.array([]) + for i in range(len(d)): + sh = np.append(sh, np.array(d[i])) + (hist, bins) = np.histogram(sh, bins=250, range=(0, 250), new=True) + if len(bins) > len(hist): + bins = bins[0 : len(hist)] + + pl.figure(1) + pl.subplot(2, 2, 1) + pl.plot(bins, hist) + + (cvisit, cvisi, cvt, cvm, cvs) = isi_cv( + d, binwidth=0.5, t0=0, t1=100, tgrace=25 + ) + order = np.argsort(cvt) + cvt = cvt[order] + cvs = cvs[order] + cvm = cvm[order] + pl.subplot(2, 2, 2) + pl.plot(cvt, cvm) + pl.hold(True) + pl.plot(cvt, cvs) + pl.subplot(2, 2, 4) + pl.plot(cvt, cvs / cvm) + pl.show() + + if test == "measure": + x = np.arange(0, 100, 0.1) + s = np.shape(x) + y = np.random.randn(s[0]) + for i in range(0, 4): + print("\ni is : %d" % (i)) + x0 = i * 20 + x1 = x0 + 20 + (r0, r1) = measure("mean", x, y, x0, x1) + print("mean: %f std: %f [0, 20]" % (r0, r1)) + (r0, r1) = measure("max", x, y, x0, x1) + print("max: %f std: %f [0, 20]" % (r0, r1)) + (r0, r1) = measure("min", x, y, x0, x1) + print("min: %f std: %f [0, 20]" % (r0, r1)) + (r0, r1) = measure("median", x, y, x0, x1) + print("median: %f std: %f [0, 20]" % (r0, r1)) + (r0, r1) = measure("p2p", x, y, x0, x1) + print("peak to peak: %f std: %f [0, 20]" % (r0, r1)) + + if test == "an_syn": + (s, w, t, n) = an_syn(N=50, spont=50, driven=150, post=100, makewave=True) + sh = np.array([]) + for i in range(len(s)): + sh = np.append(sh, np.array(s[i])) + (hist, bins) = np.histogram(sh, bins=250, range=(0, 250), new=True) + if len(bins) > len(hist): + bins = bins[0 : len(hist)] + + import pylab as pl + + pl.figure(1) + pl.subplot(2, 2, 1) + pl.plot(bins, hist) + + pl.subplot(2, 2, 3) + for i in range(len(w)): + pl.plot(t, w[i]) + pl.hold = True + (cvisit, cvisi, cvt, cvm, cvs) = isi_cv(s) + order = np.argsort(cvt) + cvt = cvt[order] + cvs = cvs[order] + cvm = cvm[order] + pl.subplot(2, 2, 2) + pl.plot(cvt, cvs / cvm) + pl.show() + + if test == "syns": + (s, w, t, n) = syns(rate=20, delay=0, dur=100.0, N=5, makewave=True) + sh = np.array([]) + for i in range(len(s)): + sh = np.append(sh, np.array(s[i])) + (hist, bins) = np.histogram(sh, bins=250, range=(0, 250), new=True) + if len(bins) > len(hist): + bins = bins[0 : len(hist)] + + import pylab as pl + + pl.figure(1) + pl.subplot(2, 2, 1) + pl.plot(bins, hist) + + pl.subplot(2, 2, 3) + pl.plot(t, w) + pl.hold = True + (cvisit, cvisi, cvt, cvm, cvs) = isi_cv(s) + order = np.argsort(cvt) + cvt = cvt[order] + cvs = cvs[order] + cvm = cvm[order] + pl.subplot(2, 2, 2) + pl.plot(cvt, cvs / cvm) + pl.show() diff --git a/cnmodel/util/pyqtgraphPlotHelpers.py b/cnmodel/util/pyqtgraphPlotHelpers.py new file mode 100755 index 0000000..ea61236 --- /dev/null +++ b/cnmodel/util/pyqtgraphPlotHelpers.py @@ -0,0 +1,1505 @@ +from __future__ import print_function + +# encoding: utf-8 +""" +pyqtgraphPlotHelpers.py + +Routines to help use pyqtgraph and make cleaner plots +as well as get plots read for publication. + +Intially copied from PlotHelpers.py for matplotlib. +Modified to allow us to use a list of axes, and operate on all of those, +or to use just one axis if that's all that is passed. +Therefore, the first argument to these calls can either be a pyqtgraph axis object, +or a list of axes objects. 2/10/2012 pbm. + +Created by Paul Manis on 2010-03-09. +""" + + +import string + +stdFont = "Arial" + +import scipy.stats +import numpy as np +import pyqtgraph as pg +from .talbotetalTicks import Extended # logical tick formatting... + +""" +Basic functions: +""" + + +def nice_plot( + plotlist, spines=["left", "bottom"], position=10, direction="inward", axesoff=False +): + """ Adjust a plot so that it looks nicer than the default matplotlib plot + Also allow quickaccess to things we like to do for publication plots, including: + using a calbar instead of an axes: calbar = [x0, y0, xs, ys] + inserting a reference line (grey, 3pt dashed, 0.5pt, at refline = y position) + + Paramaetrs + ---------- + plotlist : list + a plot handle or list of plot handles to which the "niceplot" will be applied + spines : list + a list of which axes should have spines. Not relevant for pyqtgraph + position : int + not relevant for pyqtgraph + direction : string + need to implement for pyqtgraph + axesoff : boolean + flag that forces plots to turn axes off + + """ + if type(plotlist) is not list: + plotlist = [plotlist] + for pl in plotlist: + if axesoff is True: + pl.hideAxis("bottom") + pl.hideAxis("left") + + +def noaxes(plotlist, whichaxes="xy"): + """ take away all the axis ticks and the lines + + Parameters + ---------- + plotlist : list + list of plot handles + whichaxes : string + string describing which axes to remove: 'x', 'y', or 'xy' for both + + """ + if type(plotlist) is not list: + plotlist = [plotlist] + for pl in plotlist: + if "x" in whichaxes: + pl.hideAxis("bottom") + if "y" in whichaxes: + pl.hideAxis("left") + + +def setY(ax1, ax2): + """ + Set the Y axis of all the plots in ax2 to be like ax1 + + Parameters + ---------- + ax1 : pyqtgraph plot instance + ax2 : list + list of target plots that will have the axes properties of ax1 + """ + if type(ax1) is list: + print("PlotHelpers: cannot use list as source to set Y axis") + return + if type(ax2) is not list: + ax2 = [ax2] + y = ax1.getAxis("left") + refy = y.range # return the current range + for ax in ax2: + ax.setRange(refy) + + +def setX(ax1, ax2): + """ + Set the X axis of all the plots in ax2 to be like ax1 + + Parameters + ---------- + ax1 : pyqtgraph plot instance + ax2 : list + list of target plots that will have the axes properties of ax1 + """ + if type(ax1) is list: + print("PlotHelpers: cannot use list as source to set X axis") + return + if type(ax2) is not list: + ax2 = [ax2] + x = ax1.getAxis("bottom") + refx = x.range + for ax in ax2: + ax.setrange(refx) + + +def labelPanels(axl, axlist=None, font="Arial", fontsize=18, weight="normal"): + """ + Label the panels like a specific panel + + Parameters + ---------- + axl : dict or list + axlist : list, optional + list of labels to use for the axes, defaults to None + font : str, optional + Font to use for the labels, defaults to Arial + fontsize : int, optional + Font size in points for the labels, defaults to 18 + weight : str, optional + Font weight to use, defaults to 'normal' + + """ + if type(axl) is dict: + axt = [axl[x] for x in axl] + axlist = axl.keys() + axl = axt + if type(axl) is not list: + axl = [axl] + if axlist is None: + axlist = string.uppercase(1, len(axl)) # assume we wish to go in sequence + + for i, ax in enumerate(axl): + labelText = pg.TextItem(axlist[i]) + y = ax.getAxis("left").range + x = ax.getAxis("bottom").range + ax.addItem(labelText) + labelText.setPos(x[0], y[1]) + + +def listAxes(axd): + """ + make a list of the axes from the dictionary of axes + + Parameters + ---------- + axd : dict + a dict of axes, whose values are returned in a list + + Returns + ------- + list : a list of the axes + + """ + if type(axd) is not dict: + if type(axd) is list: + return axd + else: + print("listAxes expects dictionary or list; type not known (fix the code)") + raise + axl = [axd[x] for x in axd] + return axl + + +def cleanAxes(axl): + """ + """ + if type(axl) is not list: + axl = [axl] + # does nothing at the moment, as axes are already "clean" + # for ax in axl: + # + # update_font(ax) + + +def formatTicks(axl, axis="xy", fmt="%d", font="Arial"): + """ + Convert tick labels to intergers + to do just one axis, set axis = 'x' or 'y' + control the format with the formatting string + """ + if type(axl) is not list: + axl = [axl] + + +def autoFormatTicks(axl, axis="xy", font="Arial"): + if type(axl) is not list: + axl = [axl] + for ax in axl: + if "x" in axis: + b = ax.getAxis("bottom") + x0 = b.range + # setFormatter(ax, x0, x1, axis = 'x') + if "y" in axis: + l = ax.getAxis("left") + y0 = l.range + + +# setFormatter(ax, y0, y1, axis = 'y') + + +def setFormatter(ax, x0, x1, axis="x"): + datarange = np.abs(x0 - x1) + mdata = np.ceil(np.log10(datarange)) + # if mdata > 0 and mdata <= 4: + # majorFormatter = FormatStrFormatter('%d') + # elif mdata > 4: + # majorFormatter = FormatStrFormatter('%e') + # elif mdata <= 0 and mdata > -1: + # majorFormatter = FormatStrFormatter('%5.1f') + # elif mdata < -1 and mdata > -3: + # majorFormatatter = FormatStrFormatter('%6.3f') + # else: + # majorFormatter = FormatStrFormatter('%e') + # if axis == 'x': + # ax.xaxis.set_major_formatter(majorFormatter) + # else: + # ax.yaxis.set_major_formatter(majorFormatter) + + +def update_font(axl, size=6, font=stdFont): + pass + # if type(axl) is not list: + # axl = [axl] + # fontProperties = {'family':'sans-serif','sans-serif':[font], + # 'weight' : 'normal', 'size' : size} + # for ax in axl: + # for tick in ax.xaxis.get_major_ticks(): + # tick.label1.set_family('sans-serif') + # tick.label1.set_fontname(stdFont) + # tick.label1.set_size(size) + # + # for tick in ax.yaxis.get_major_ticks(): + # tick.label1.set_family('sans-serif') + # tick.label1.set_fontname(stdFont) + # tick.label1.set_size(size) + # ax.set_xticklabels(ax.get_xticks(), fontProperties) + # ax.set_yticklabels(ax.get_yticks(), fontProperties) + # ax.xaxis.set_smart_bounds(True) + # ax.yaxis.set_smart_bounds(True) + # ax.tick_params(axis = 'both', labelsize = 9) + + +def lockPlot(axl, lims, ticks=None): + """ + This routine forces the plot of invisible data to force the axes to take certain + limits and to force the tick marks to appear. + call with the axis and lims = [x0, x1, y0, y1] + """ + if type(axl) is not list: + axl = [axl] + plist = [] + for ax in axl: + y = ax.getAxis("left") + x = ax.getAxis("bottom") + x.setRange(lims[0], lims[1]) + y.setRange(lims[2], lims[3]) + + +def adjust_spines( + axl, spines=("left", "bottom"), direction="outward", distance=5, smart=True +): + pass + # if type(axl) is not list: + # axl = [axl] + # for ax in axl: + # # turn off ticks where there is no spine + # if 'left' in spines: + # ax.yaxis.set_ticks_position('left') + # else: + # # no yaxis ticks + # ax.yaxis.set_ticks([]) + # + # if 'bottom' in spines: + # ax.xaxis.set_ticks_position('bottom') + # else: + # # no xaxis ticks + # ax.xaxis.set_ticks([]) + # for loc, spine in ax.spines.iteritems(): + # if loc in spines: + # spine.set_position((direction,distance)) # outward by 10 points + # if smart is True: + # spine.set_smart_bounds(True) + # else: + # spine.set_smart_bounds(False) + # else: + # spine.set_color('none') # don't draw spine + # return + # + + +def calbar(plotlist, calbar=None, axesoff=True, orient="left", unitNames=None): + """ draw a calibration bar and label it up. The calibration bar is defined as: + [x0, y0, xlen, ylen] + + Parameters + ---------- + plotlist : list + a plot item or a list of plot items for which a calbar will be applied + calbar : list, optional + a list with 4 elements, describing the calibration bar + [xposition, yposition, xlength, ylength] in units of the data inthe plot + defaults to None + axesoff : boolean, optional + Set true to turn off the standard axes, defaults to True + orient : text, optional + 'left': put vertical part of the bar on the left + 'right': put the vertical part of the bar on the right + defaults to 'left' + unitNames: str, optional + a dictionary with the names of the units to append to the calibration bar + lengths. Example: {'x': 'ms', 'y': 'nA'} + defaults to None + Returns + ------- + Nothing + """ + + if type(plotlist) is not list: + plotlist = [plotlist] + for pl in plotlist: + if axesoff is True: + noaxes(pl) + Vfmt = "%.0f" + if calbar[2] < 1.0: + Vfmt = "%.1f" + Hfmt = "%.0f" + if calbar[3] < 1.0: + Hfmt = "%.1f" + if unitNames is not None: + Vfmt = Vfmt + " " + unitNames["x"] + Hfmt = Hfmt + " " + unitNames["y"] + Vtxt = pg.TextItem(Vfmt % calbar[2], anchor=(0.5, 0.5), color=pg.mkColor("k")) + Htxt = pg.TextItem(Hfmt % calbar[3], anchor=(0.5, 0.5), color=pg.mkColor("k")) + # print pl + if calbar is not None: + if orient == "left": # vertical part is on the left + pl.plot( + [calbar[0], calbar[0], calbar[0] + calbar[2]], + [calbar[1] + calbar[3], calbar[1], calbar[1]], + pen=pg.mkPen("k"), + linestyle="-", + linewidth=1.5, + ) + ht = Htxt.setPos( + calbar[0] + 0.05 * calbar[2], calbar[1] + 0.5 * calbar[3] + ) + elif orient == "right": # vertical part goes on the right + pl.plot( + [calbar[0] + calbar[2], calbar[0] + calbar[2], calbar[0]], + [calbar[1] + calbar[3], calbar[1], calbar[1]], + pen=pg.mkPen("k"), + linestyle="-", + linewidth=1.5, + ) + ht = Htxt.setPos( + calbar[0] + calbar[2] - 0.05 * calbar[2], + calbar[1] + 0.5 * calbar[3], + ) + else: + print("PlotHelpers.py: I did not understand orientation: %s" % (orient)) + print("plotting as if set to left... ") + pl.plot( + [calbar[0], calbar[0], calbar[0] + calbar[2]], + [calbar[1] + calbar[3], calbar[1], calbar[1]], + pen=pg.mkPen("k"), + linestyle="-", + linewidth=1.5, + ) + ht = Htxt.setPos( + calbar[0] + 0.05 * calbar[2], calbar[1] + 0.5 * calbar[3] + ) + Htxt.setText(Hfmt % calbar[3]) + xc = float(calbar[0] + calbar[2] * 0.5) # always centered, below the line + yc = float(calbar[1] - 0.1 * calbar[3]) + vt = Vtxt.setPos(xc, yc) + Vtxt.setText(Vfmt % calbar[2]) + pl.addItem(Htxt) + pl.addItem(Vtxt) + + +def refline( + axl, + refline=None, + color=[64, 64, 64], + linestyle="--", + linewidth=0.5, + orient="horizontal", +): + """ + Draw a reference line at a particular level of the data on the y axis + + Parameters + ---------- + axl : list + axis handle or list of axis handles + refline : float, optional + the position of the reference line, defaults to None + color : list, optional + the RGB color list for the line, in format [r,g,b], defaults to [64, 64, 64] (faint grey line) + linestyle : str, optional + defines the linestyle to be used: -- for dash, . for doted, - for solid, -. for dash-dot, + -.. for -.., etc. + defaults to '--' (dashed) + linewidth : float, optional + width of the line, defaults to 0.5 + """ + if type(axl) is not list: + axl = [axl] + if linestyle == "--": + style = pg.QtCore.Qt.DashLine + elif linestyle == ".": + style = pg.QtCore.Qt.DotLine + elif linestyle == "-": + style = pg.QtCore.Qt.SolidLine + elif linestyle == "-.": + style = pg.QtCore.Qt.DsahDotLine + elif linestyle == "-..": + style = pg.QtCore.Qt.DashDotDotLine + else: + style = pg.QtCore.Qt.SolidLine # default is solid + if orient is "horizontal": + for ax in axl: + if refline is not None: + x = ax.getAxis("bottom") + xlims = x.range + ax.plot( + xlims, + [refline, refline], + pen=pg.mkPen(color, width=linewidth, style=style), + ) + if orient is "vertical": + for ax in axl: + if refline is not None: + y = ax.getAxis("left") + ylims = y.range + ax.plot( + [refline, refline], + [ylims[0] + 0.5, ylims[1] - 0.5], + pen=pg.mkPen(color, width=linewidth, style=style), + ) + + +def tickStrings(values, scale=1, spacing=None, tickPlacesAdd=1): + """Return the strings that should be placed next to ticks. This method is called + when redrawing the axis and is a good method to override in subclasses. + + Parameters + ---------- + values : array or list + An array or list of tick values + scale : float, optional + a scaling factor (see below), defaults to 1 + spacing : float, optional + spaceing between ticks (this is required since, in some instances, there may be only + one tick and thus no other way to determine the tick spacing). Defaults to None + tickPlacesToAdd : int, optional + the number of decimal places to add to the ticks, default is 1 + + Returns + ------- + list : a list containing the tick strings + + The scale argument is used when the axis label is displaying units which may have an SI scaling prefix. + When determining the text to display, use value*scale to correctly account for this prefix. + For example, if the axis label's units are set to 'V', then a tick value of 0.001 might + be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and + thus the tick should display 0.001 * 1000 = 1. + Copied rom pyqtgraph; we needed it here. + """ + if spacing is None: + spacing = np.mean(np.diff(values)) + places = max(0, np.ceil(-np.log10(spacing * scale))) + tickPlacesAdd + strings = [] + for v in values: + vs = v * scale + if abs(vs) < 0.001 or abs(vs) >= 10000: + vstr = "%g" % vs + else: + vstr = ("%%0.%df" % places) % vs + strings.append(vstr) + return strings + + +def crossAxes(axl, xyzero=[0.0, 0.0], limits=[None, None, None, None], **kwds): + """ + Make the plot(s) have crossed axes at the data points set by xyzero, and optionally + set axes limits + + Parameters + ---------- + axl : pyqtgraph plot/axes instance or list + the plot to modify + xyzero : list + A 2-element list for the placement of x=0 and y=0, defaults to [0., 0.] + limits : list + A 4-element list with the min and max limits of the axes, defaults to all None + **kwds : keyword arguments to pass to make_crossedAxes + + """ + if type(axl) is not list: + axl = [axl] + for ax in axl: + make_crossedAxes(ax, xyzero, limits, **kwds) + + +def make_crossedAxes( + ax, + xyzero=[0.0, 0.0], + limits=[None, None, None, None], + ndec=3, + density=(1.0, 1.0), + tickl=0.0125, + insideMargin=0.05, + pointSize=12, + tickPlacesAdd=(0, 0), +): + """ + Parameters + ---------- + axl : pyqtgraph plot/axes instance or list + the plot to modify + xyzero : list + A 2-element list for the placement of x=0 and y=0, defaults to [0., 0.] + limits : list + A 4-element list with the min and max limits of the axes, defaults to all None + ndec : int + Number of decimals (would be passed to talbotTicks if that was being called) + density : tuple + tick density (for talbotTicks), defaults to (1.0, 1.0) + tickl : float + Tick length, defaults to 0.0125 + insideMargin : float + Inside margin space for plot, defaults to 0.05 (5%) + pointSize : int + point size for tick text, defaults to 12 + tickPlacesAdd : tuple + number of decimal places to add in tickstrings for the ticks, pair for x and y axes, defaults to (0,0) + + Returns + ------- + Nothing + + + """ + # get axis limits + aleft = ax.getAxis("left") + abottom = ax.getAxis("bottom") + aleft.setPos(pg.Point(3.0, 0.0)) + yRange = aleft.range + xRange = abottom.range + hl = pg.InfiniteLine(pos=xyzero[0], angle=90, pen=pg.mkPen("k")) + ax.addItem(hl) + vl = pg.InfiniteLine(pos=xyzero[1], angle=0, pen=pg.mkPen("k")) + ax.addItem(vl) + ax.hideAxis("bottom") + ax.hideAxis("left") + # now create substitue tick marks and labels, using Talbot et al algorithm + xr = np.diff(xRange)[0] + yr = np.diff(yRange)[0] + xmin, xmax = ( + np.min(xRange) - xr * insideMargin, + np.max(xRange) + xr * insideMargin, + ) + ymin, ymax = ( + np.min(yRange) - yr * insideMargin, + np.max(yRange) + yr * insideMargin, + ) + xtick = ticks.Extended( + density=density[0], figure=None, range=(xmin, xmax), axis="x" + ) + ytick = ticks.Extended( + density=density[1], figure=None, range=(ymin, ymax), axis="y" + ) + xt = xtick() + yt = ytick() + ytk = yr * tickl + xtk = xr * tickl + y0 = xyzero[1] + x0 = xyzero[0] + tsx = tickStrings(xt, tickPlacesAdd=tickPlacesAdd[0]) + tsy = tickStrings(yt, tickPlacesAdd=tickPlacesAdd[1]) + for i, x in enumerate(xt): + t = pg.PlotDataItem(x=x * np.ones(2), y=[y0 - ytk, y0 + ytk], pen=pg.mkPen("k")) + ax.addItem(t) # tick mark + # put text in only if it does not overlap the opposite line + if x == y0: + continue + txt = pg.TextItem( + tsx[i], anchor=(0.5, 0), color=pg.mkColor("k") + ) # , size='10pt') + txt.setFont(pg.QtGui.QFont("Arial", pointSize=pointSize)) + txt.setPos(pg.Point(x, y0 - ytk)) + ax.addItem(txt) # , pos=pg.Point(x, y0-ytk)) + for i, y in enumerate(yt): + t = pg.PlotDataItem( + x=np.array([x0 - xtk, x0 + xtk]), y=np.ones(2) * y, pen=pg.mkPen("k") + ) + ax.addItem(t) + if y == x0: + continue + txt = pg.TextItem( + tsy[i], anchor=(1, 0.5), color=pg.mkColor("k") + ) # , size='10pt') + txt.setFont(pg.QtGui.QFont("Arial", pointSize=pointSize)) + txt.setPos(pg.Point(x0 - xtk, y)) + ax.addItem(txt) # , pos=pg.Point(x, y0-ytk)) + + +class polarPlot: + """ + Create a polar plot, as a PlotItem for pyqtgraph. + + + """ + + def __init__(self, plot=None): + """ + Instantiate a plot as a polar plot + + Parmeters + --------- + plot : pyqtgraph plotItem + the plot that will be converted to a polar plot, defaults to None + if None, then a new PlotItem will be created, accessible + as polarPlot.plotItem + """ + if plot is None: + self.plotItem = pg.PlotItem() # create a plot item for the plot + else: + self.plotItem = plot + self.plotItem.setAspectLocked() + self.plotItem.hideAxis("bottom") + self.plotItem.hideAxis("left") + self.gridSet = False + self.data = None + self.rMax = None + + def setAxes(self, steps=4, rMax=None, makeGrid=True): + """ + Make the polar plot axes + + Parameters + ---------- + steps : int, optional + The number of radial steps for the grid, defaults to 4 + rMax : float, optional + The maximum radius of the plot, defaults to None (the rMax is 1) + makeGrid : boolean, optional + Whether the grid will actually be plotted or not, defaults to True + + """ + if makeGrid is False or self.gridSet: + return + if rMax is None: + if self.data is None: + rMax = 1.0 + else: + rMax = np.max(self.data["y"]) + self.rMax = rMax + # Add radial grid lines (theta markers) + gridPen = pg.mkPen(width=0.55, color="k", style=pg.QtCore.Qt.DotLine) + ringPen = pg.mkPen(width=0.75, color="k", style=pg.QtCore.Qt.SolidLine) + for th in np.linspace(0.0, np.pi * 2, 8, endpoint=False): + rx = np.cos(th) * rMax + ry = np.sin(th) * rMax + self.plotItem.plot(x=[0, rx], y=[0.0, ry], pen=gridPen) + ang = th * 360.0 / (np.pi * 2) + # anchor is odd: 0,0 is upper left corner, 1,1 is lower right corner + if ang < 90.0: + x = 0.0 + y = 0.5 + elif ang == 90.0: + x = 0.5 + y = 1 + elif ang < 180: + x = 1.0 + y = 0.5 + elif ang == 180.0: + x = 1 + y = 0.5 + elif ang < 270: + x = 1 + y = 0 + elif ang == 270.0: + x = 0.5 + y = 0 + elif ang < 360: + x = 0 + y = 0 + ti = pg.TextItem("%d" % (int(ang)), color=pg.mkColor("k"), anchor=(x, y)) + self.plotItem.addItem(ti) + ti.setPos(rx, ry) + # add polar grid lines (r) + for gr in np.linspace(rMax / steps, rMax, steps): + circle = pg.QtGui.QGraphicsEllipseItem(-gr, -gr, gr * 2, gr * 2) + if gr < rMax: + circle.setPen(gridPen) + else: + circle.setPen(ringPen) + self.plotItem.addItem(circle) + ti = pg.TextItem("%d" % (int(gr)), color=pg.mkColor("k"), anchor=(1, 1)) + ti.setPos(gr, 0.0) + self.plotItem.addItem(ti) + self.gridSet = True + + def plot( + self, + r, + theta, + vectors=False, + arrowhead=True, + normalize=False, + sort=False, + **kwds + ): + """ + plot puts the data into a polar plot. + the plot will be converted to a polar graph + + Parameters + ---------- + r : list or numpy array + a list or array of radii + theta : list or numpy array + a list or array of angles (in radians) corresponding to the values in r + vectors : boolean, optional + vectors True means that plot is composed of vectors to each point radiating from the origin, defaults to False + arrowhead : boolean, optional + arrowhead True plots arrowheads at the end of the vectors, defaults to True + normalize : boolean, optional + normalize forces the plot to be scaled to the max values in r, defaults to False + sort : boolean, optional + causes data r, theta to be sorted by theta, defaults to False + **kwds are passed to the data plot call. + + """ + + # sort r, theta by r + + rs = np.array(r) + thetas = np.array(theta) + + if sort: + indx = np.argsort(thetas) + theta = thetas + if not isinstance(indx, np.int64): + for i, j in enumerate(indx): + rs[i] = r[j] + thetas[i] = theta[j] + + # Transform to cartesian and plot + if normalize: + rs = rs / np.max(rs) + x = rs * np.cos(thetas) + y = rs * np.sin(thetas) + try: + len(x) + except: + x = [x] + y = [y] + if vectors: # plot r,theta as lines from origin + for i, xi in enumerate(x): + # print x[i], y[i] + if arrowhead: + arrowAngle = -( + thetas[i] * 360 / (2 * np.pi) + 180 + ) # convert to degrees, and correct orientation + arrow = pg.ArrowItem( + angle=arrowAngle, tailLen=0, tailWidth=1.5, **kwds + ) + arrow.setPos(x[i], y[i]) + self.plotItem.addItem(arrow) + self.plotItem.plot([0.0, x[i]], [0.0, y[i]], **kwds) + + else: + self.plotItem.plot(x, y, **kwds) + self.rMax = np.max(y) + self.data = {"x": x, "y": y} + + def hist( + self, + r, + theta, + binwidth=np.pi / 6.0, + normalize=False, + density=False, + mode="straight", + **kwds + ): + """ + plot puts the data into a polar plot as a histogram of the number of observations + within a wedge + the plot will be converted to a polar graph + + Parameters + ---------- + r : list or numpy array + a list or array of radii + theta : list or numpy array + a list or array of angles (in radians) corresponding to the values in r + binwidth : bin width, in radians optional + vectors True means that plot is composed of vectors to each point radiating from the origin, defaults to 30 degrees (np.pi/6) + normalize : boolean, optional + normalize forces the plot to be scaled to the max values in r, defaults to False + density : boolean, optional + plot a count histogram, or a density histogram weighted by r values, defaults to False + mode : str, optional + 'straight' selects straight line between bars. 'arc' makes the end of the bar an arc (truer representation), defaults to 'straight' + **kwds are passed to the data plot call. + + Returns + ------- + tuple : (list of rHist, list of bins) + The histogram that was plotted (use for statistical comparisions) + """ + + rs = np.array(r) + thetas = np.array(theta) + twopi = np.pi * 2.0 + for i, t in enumerate(thetas): # restrict to positive half plane [0....2*pi] + while t < 0.0: + t += twopi + while t > twopi: + t -= twopi + thetas[i] = t + bins = np.arange(0, np.pi * 2 + 1e-12, binwidth) + # compute histogram + (rhist, rbins) = np.histogram(thetas, bins=bins, weights=rs, density=density) + # Transform to cartesian and plot + if normalize: + rhist = rhist / np.max(rhist) + xo = rhist * np.cos(bins[:-1]) # get cartesian form + xp = rhist * np.cos(bins[:-1] + binwidth) + yo = rhist * np.sin(bins[:-1]) + yp = rhist * np.sin(bins[:-1] + binwidth) + arcinc = np.pi / 100.0 # arc increments + for i in range(len(xp)): + if mode is "arc": + self.plotItem.plot( + [xo[i], 0.0, xp[i]], [yo[i], 0.0, yp[i]], **kwds + ) # "v" segement + arcseg = np.arange(bins[i], bins[i + 1], arcinc) + x = np.array(rhist[i] * np.cos(arcseg)) + y = np.array(rhist[i] * np.sin(arcseg)) + self.plotItem.plot(x, y, **kwds) + + else: + self.plotItem.plot( + [0.0, xo[i], xp[i], 0.0], [0.0, yo[i], yp[i], 0.0], **kwds + ) + self.data = {"x": xo, "y": yo} + self.rMax = np.max(yo) + return (rhist, rbins) + + def circmean(self, alpha, axis=None): + """ + Compute the circular mean of a set of angles along the axis + + Parameters + ---------- + alpha : numpy array + the angles to compute the circular mean of + axis : int + The axis of alpha for the computatoin, defaults to None + + Returns + ------- + float : the mean angle + """ + mean_angle = np.arctan2( + np.mean(np.sin(alpha), axis), np.mean(np.cos(alpha), axis) + ) + return mean_angle + + +def talbotTicks(axl, **kwds): + """ + Adjust the tick marks using the talbot et al algorithm, on an existing plot. + """ + if type(axl) is not list: + axl = [axl] + for ax in axl: + do_talbotTicks(ax, **kwds) + + +def do_talbotTicks( + ax, + ndec=3, + density=(1.0, 1.0), + insideMargin=0.05, + pointSize=None, + tickPlacesAdd=(0, 0), +): + """ + Change the axis ticks to use the talbot algorithm for ONE axis + Paramerters control the ticks + + Parameters + ---------- + ax : pyqtgraph axis instance + the axis to change the ticks on + ndec : int + Number of decimals (would be passed to talbotTicks if that was being called) + density : tuple + tick density (for talbotTicks), defaults to (1.0, 1.0) + insideMargin : float + Inside margin space for plot, defaults to 0.05 (5%) + pointSize : int + point size for tick text, defaults to 12 + tickPlacesAdd : tuple + number of decimal places to add in tickstrings for the ticks, pair for x and y axes, defaults to (0,0) + + """ + # get axis limits + aleft = ax.getAxis("left") + abottom = ax.getAxis("bottom") + yRange = aleft.range + xRange = abottom.range + # now create substitue tick marks and labels, using Talbot et al algorithm + xr = np.diff(xRange)[0] + yr = np.diff(yRange)[0] + xmin, xmax = ( + np.min(xRange) - xr * insideMargin, + np.max(xRange) + xr * insideMargin, + ) + ymin, ymax = ( + np.min(yRange) - yr * insideMargin, + np.max(yRange) + yr * insideMargin, + ) + xtick = ticks.Extended( + density=density[0], figure=None, range=(xmin, xmax), axis="x" + ) + ytick = ticks.Extended( + density=density[1], figure=None, range=(ymin, ymax), axis="y" + ) + xt = xtick() + yt = ytick() + xts = tickStrings(xt, scale=1, spacing=None, tickPlacesAdd=tickPlacesAdd[0]) + yts = tickStrings(yt, scale=1, spacing=None, tickPlacesAdd=tickPlacesAdd[1]) + xtickl = [[(x, xts[i]) for i, x in enumerate(xt)], []] # no minor ticks here + ytickl = [[(y, yts[i]) for i, y in enumerate(yt)], []] # no minor ticks here + + # ticks format: [ (majorTickValue1, majorTickString1), (majorTickValue2, majorTickString2), ... ], + aleft.setTicks(ytickl) + abottom.setTicks(xtickl) + # now set the point size (this may affect spacing from axis, and that would have to be adjusted - see the pyqtgraph google groups) + if pointSize is not None: + b = pg.QtGui.QFont() + b.setPixelSize(pointSize) + aleft.tickFont = b + abottom.tickFont = b + + +def violinPlotScatter(ax, data, symbolColor="k", symbolSize=4, symbol="o"): + """ + Plot data as violin plot with scatter and error bar + + Parameters + ---------- + ax : pyqtgraph plot instance + is the axs to plot into + data : dict + dictionary containing {pos1: data1, pos2: data2}, where pos is the x position for the data in data. Each data + set iis plotted as a separate column + symcolor : string, optional + color of the symbols, defaults to 'k' (black) + symbolSize : int, optional + Size of the symbols in the scatter plot, points, defaults to 4 + symbol : string, optoinal + The symbol to use, defaults to 'o' (circle) + """ + + y = [] + x = [] + xb = np.arange(0, len(data.keys()), 1) + ybm = [0] * len(data.keys()) # np.zeros(len(sdat.keys())) + ybs = [0] * len(data.keys()) # np.zeros(len(sdat.keys())) + for i, k in enumerate(data.keys()): + yvals = np.array(data[k]) + xvals = pg.pseudoScatter(yvals, spacing=0.4, bidir=True) * 0.2 + ax.plot( + x=xvals + i, + y=yvals, + pen=None, + symbol=symbol, + symbolSize=symbolSize, + symbolBrush=pg.mkBrush(symbolColor), + ) + y.append(yvals) + x.append([i] * len(yvals)) + ybm[i] = np.nanmean(yvals) + ybs[i] = np.nanstd(yvals) + mbar = pg.PlotDataItem( + x=np.array([xb[i] - 0.2, xb[i] + 0.2]), + y=np.array([ybm[i], ybm[i]]), + pen={"color": "k", "width": 0.75}, + ) + ax.addItem(mbar) + bar = pg.ErrorBarItem( + x=xb, + y=np.array(ybm), + height=np.array(ybs), + beam=0.2, + pen={"color": "k", "width": 0.75}, + ) + violin_plot(ax, y, xb, bp=False) + ax.addItem(bar) + ticks = [[(v, k) for v, k in enumerate(data.keys())], []] + ax.getAxis("bottom").setTicks(ticks) + + +def violin_plot(ax, data, pos, dist=0.0, bp=False): + """ + create violin plots on an axis + """ + + if data is None or len(data) == 0: + return # skip trying to do the plot + + dist = max(pos) - min(pos) + w = min(0.15 * max(dist, 1.0), 0.5) + for i, d in enumerate(data): + if d == [] or len(d) == 0: + continue + k = scipy.stats.gaussian_kde(d) # calculates the kernel density + m = k.dataset.min() # lower bound of violin + M = k.dataset.max() # upper bound of violin + y = np.arange(m, M, (M - m) / 100.0) # support for violin + v = k.evaluate(y) # violin profile (density curve) + v = v / v.max() * w # scaling the violin to the available space + c1 = pg.PlotDataItem(y=y, x=pos[i] + v, pen=pg.mkPen("k", width=0.5)) + c2 = pg.PlotDataItem(y=y, x=pos[i] - v, pen=pg.mkPen("k", width=0.5)) + # mean = k.dataset.mean() + # vm = k.evaluate(mean) + # vm = vm * w + # ax.plot(x=np.array([pos[i]-vm[0], pos[i]+vm[0]]), y=np.array([mean, mean]), pen=pg.mkPen('k', width=1.0)) + ax.addItem(c1) + ax.addItem(c2) + # ax.addItem(hbar) + f = pg.FillBetweenItem( + curve1=c1, curve2=c2, brush=pg.mkBrush((255, 255, 0, 96)) + ) + ax.addItem(f) + + if bp: + pass + # bpf = ax.boxplot(data, notch=0, positions=pos, vert=1) + # pylab.setp(bpf['boxes'], color='black') + # pylab.setp(bpf['whiskers'], color='black', linestyle='-') + + +def labelAxes(plot, xtext, ytext, **kwargs): + """ + helper to label up the plot + + Parameters + ----------- + plot : plot item + xtext : string + text for x axis + ytext : string + text for y axis + **kwargs : keywords + additional arguments to pass to pyqtgraph setLabel + """ + + plot.setLabel("bottom", xtext, **kwargs) + plot.setLabel("left", ytext, **kwargs) + + +def labelPanels(plot, label=None, **kwargs): + """ + helper to label up the plot + Inputs: plot item + text for x axis + text for yaxis + plot title (on top) OR + plot panel label (for example, "A", "A1") + """ + + if label is not None: + setPlotLabel(plot, plotlabel="%s" % label, **kwargs) + else: + setPlotLabel(plot, plotlabel="") + + +def labelTitles(plot, title=None, **kwargs): + """ + Set the title of a plotitem. Basic HTML formatting is allowed, along + with "size", "bold", "italic", etc.. + If the title is not defined, then a blank label is used + A title is a text label that appears centered above the plot, in + QGridLayout (position 0,2) of the plotitem. + params + ------- + :param plotitem: The plot item to label + :param title: The text string to use for the label + :kwargs: keywords to pass to the pg.LabelItem + :return: None + + """ + + if title is not None: + plot.setTitle(title="%s" % title, visible=True, **kwargs) + else: # clear the plot title + plot.setTitle(title=" ") + + +def setPlotLabel(plotitem, plotlabel="", **kwargs): + """ + Set the plotlabel of a plotitem. Basic HTML formatting is allowed, along + with "size", "bold", "italic", etc.. + If plotlabel is not defined, then a blank label is used + A plotlabel is a text label that appears the upper left corner of the + QGridLayout (position 0,0) of the plotitem. + params + ------- + :param plotitem: The plot item to label + :param plotlabel: The text string to use for the label + :kwargs: keywords to pass to the pg.LabelItem + :return: None + + """ + + plotitem.LabelItem = pg.LabelItem(plotlabel, **kwargs) + plotitem.LabelItem.setMaximumHeight(30) + plotitem.layout.setRowFixedHeight(0, 30) + plotitem.layout.addItem(plotitem.LabelItem, 0, 0) + plotitem.LabelItem.setVisible(True) + + +class LayoutMaker: + def __init__( + self, + win=None, + cols=1, + rows=1, + letters=True, + titles=False, + labelEdges=True, + margins=4, + spacing=4, + ticks="default", + ): + self.sequential_letters = string.ascii_uppercase + self.cols = cols + self.rows = rows + self.letters = letters + self.titles = titles + self.edges = labelEdges + self.margins = margins + self.spacing = spacing + self.rcmap = [None] * cols * rows + self.plots = None + self.win = win + self.ticks = ticks + self._makeLayout( + letters=letters, titles=titles, margins=margins, spacing=spacing + ) + # self.addLayout(win) + + # def addLayout(self, win=None): + # if win is not None: + # win.setLayout(self.gridLayout) + + def getCols(self): + return self.cols + + def getRows(self): + return self.rows + + def mapFromIndex(self, index): + """ + for a given index, return the row, col tuple associated with the index + """ + return self.rcmap[index] + + def getPlot(self, index): + """ + return the plot item in the list corresponding to the index n + """ + if isinstance(index, tuple): + r, c = index + elif isinstance(index, int): + r, c = self.rcmap[index] + else: + raise ValueError( + "pyqtgraphPlotHelpers, LayoutMaker plot: index must be int or tuple(r,c)" + ) + return self.plots[r][c] + + def plot(self, index, *args, **kwargs): + p = self.getPlot(index).plot(*args, **kwargs) + if self.ticks == "talbot": + talbotTicks(self.getPlot(index)) + return p + + def _makeLayout(self, letters=True, titles=True, margins=4, spacing=4): + """ + Create a multipanel plot. + The pyptgraph elements (widget, gridlayout, plots) are stored as class variables. + The layout is always a rectangular grid with shape (cols, rows) + if letters is true, then the plot is labeled "A, B, C..." Indices move horizontally first, then vertically + margins sets the margins around the outside of the plot + spacing sets the spacing between the elements of the grid + If a window was specified (self.win is not None) then the grid layout will derive from that window's central + item; otherwise we just make a gridLayout that can be put into another container somewhere. + """ + import string + + if self.win is not None: + self.gridLayout = ( + self.win.ci.layout + ) # the window's 'central item' is the main gridlayout. + else: + self.gridLayout = ( + pg.QtGui.QGridLayout() + ) # just create the grid layout to add to another item + self.gridLayout.setContentsMargins(margins, margins, margins, margins) + self.gridLayout.setSpacing(spacing) + self.plots = [[0 for x in xrange(self.cols)] for x in xrange(self.rows)] + i = 0 + for r in range(self.rows): + for c in range(self.cols): + self.plots[r][c] = self.win.addPlot(row=r, col=c) # pg.PlotWidget() + if letters: + labelPanels( + self.plots[r][c], + label=self.sequential_letters[i], + size="14pt", + bold=True, + ) + if titles: + labelTitles( + self.plots[r][c], + title=self.sequential_letters[i], + size="14pt", + bold=False, + ) + + self.rcmap[i] = (r, c) + i += 1 + if i > 25: + i = 0 + self.labelEdges("T(s)", "Y", edgeOnly=self.edges) + + def labelEdges(self, xlabel="T(s)", ylabel="Y", edgeOnly=True, **kwargs): + """ + label the axes on the outer edges of the gridlayout, leaving the interior axes clean + """ + (lastrow, lastcol) = self.rcmap[-1] + i = 0 + for (r, c) in self.rcmap: + if c == 0: + ylab = ylabel + elif edgeOnly: + ylab = "" + else: + ylab = ylabel + if r == self.rows - 1: # only the last row + xlab = xlabel + elif edgeOnly: # but not other rows + xlab = "" + else: + xlab = xlabel # otherwise, label it + labelAxes(self.plots[r][c], xlab, ylab, **kwargs) + i += 1 + + def axesEdges(self, edgeOnly=True): + """ + text labesls only on the axes on the outer edges of the gridlayout, + leaving the interior axes clean + """ + (lastrow, lastcol) = self.rcmap[-1] + i = 0 + for (r, c) in self.rcmap: + xshow = True + yshow = True + if edgeOnly and c > 0: + yshow = False + if edgeOnly and r < self.rows: # only the last row + yshow = False + ax = self.getPlot((r, c)) + leftaxis = ax.getAxis("left") + bottomaxis = ax.getAxis("bottom") + # print dir(self.plots[r][c]) + leftaxis.showValues = yshow + bottomaxis.showValues = xshow + i += 1 + + def columnAutoScale(self, col, axis="left"): + """ + autoscale the columns according to the max value in the column. + Finds outside range of column data, then sets the scale of all plots + in the column to that range + """ + atmax = None + atmin = None + for (r, c) in self.rcmap: + if c != col: + continue + ax = self.getPlot((r, c)) + thisaxis = ax.getAxis(axis) + amin, amax = thisaxis.range + if atmax is None: + atmax = amax + else: + if amax > atmax: + atmax = amax + if atmin is None: + atmin = amin + else: + if amin > atmin: + atmin = amin + + self.columnSetScale(col, axis=axis, range=(atmin, atmax)) + return (atmin, atmax) + + def columnSetScale(self, col, axis="left", range=(0.0, 1.0)): + """ + Set the column scale + """ + for (r, c) in self.rcmap: + if c != col: + continue + ax = self.getPlot((r, c)) + if axis == "left": + ax.setYRange(range[0], range[1]) + elif axis == "bottom": + ax.setXRange(range[0], range[1]) + + if self.ticks == "talbot": + talbotTicks(ax) + + def title(self, index, title="", **kwargs): + """ + add a title to a specific plot (specified by index) in the layout + """ + labelTitles(self.getPlot(index), title=title, **kwargs) + + +def figure(title=None, background="w"): + if background == "w": + pg.setConfigOption("background", "w") # set background to white + pg.setConfigOption("foreground", "k") + pg.mkQApp() + win = pg.GraphicsWindow(title=title) + return win + + +def show(): + pg.QApplication.instance().exec_() + + +def test_layout(win): + """ + Test the various plot types and modifications provided by the helpers above, + in the context of a layout with various kinds of plots. + """ + layout = LayoutMaker(cols=4, rows=2, win=win, labelEdges=True, ticks="talbot") + x = np.arange(0, 10.0, 0.1) + y = np.sin(x * 3.0) # make an interesting signal + r = np.random.random(10) # and a random signal + theta = np.linspace(0, 2.0 * np.pi, 10, endpoint=False) # r, theta for polar plots + for n in range(4 * 2): + if n not in [1, 2, 3, 4]: + layout.plot(n, x, y) + p = layout.getPlot(n) + if n == 0: # crossed axes plot + crossAxes( + p, + xyzero=[5.0, 0.0], + density=(0.75, 1.5), + tickPlacesAdd=(1, 0), + pointSize=12, + ) + layout.title(n, "Crossed Axes") + if n in [1, 2, 3]: # two differnt forms of polar plots + if n == 1: + po = polarPlot(p) + po.setAxes(rMax=np.max(r)) + po.plot(r, theta, pen=pg.mkPen("r")) + layout.title(n, "Polar Path") + + if n == 2: + po = polarPlot(p) + po.plot(r, theta, vectors=True, pen=pg.mkPen("k", width=2.0)) + po.setAxes(rMax=np.max(r)) + po.plot( + [np.mean(r)], + [po.circmean(theta)], + vectors=True, + pen=pg.mkPen("r", width=2.0), + ) + layout.title(n, "Polar Arrows") + if n == 3: + po = polarPlot(p) + po.hist( + r, + theta, + binwidth=np.pi / 6.0, + normalize=False, + density=False, + pen="r", + ) + po.hist( + r, + theta, + binwidth=np.pi / 6.0, + normalize=False, + density=False, + mode="arc", + pen="b", + ) + po.setAxes(rMax=None) + layout.title(n, "Polar Histogram") + + if n == 4: # violin plot with scatter plot data + data = { + 2: [3, 5, 7, 9, 2, 4, 6, 8, 7, 2, 3, 1, 2.5], + 3: [5, 6, 7, 9, 2, 8, 10, 9.5, 11], + } + violinPlotScatter(p, data, symbolColor="r") + p.setYRange(0, 12) + layout.title(n, "Violin Plots with PseudoScatter") + + if ( + n == 5 + ): # clean plot for physiology with baseline reference and a calibration bar + calbar( + p, + calbar=[7.0, -1.5, 2.0, 0.5], + axesoff=True, + orient="left", + unitNames={"x": "ms", "y": "nA"}, + ) + refline(p, refline=0.0, color=[64, 64, 64], linestyle="--", linewidth=0.5) + layout.title(n, "Calbar and Refline") + + # talbotTicks(layout.getPlot(1)) + layout.columnAutoScale(col=3, axis="left") + show() + + +def test_crossAxes(win): + layout = LayoutMaker(cols=1, rows=1, win=win, labelEdges=True) + x = np.arange(-1, 1.0, 0.01) + y = np.sin(x * 10.0) + layout.plot(0, x, y) + p = layout.getPlot(0) + crossAxes( + p, + xyzero=[0.0, 0.0], + limits=[None, None, None, None], + density=1.5, + tickPlacesAdd=1, + pointSize=12, + ) + show() + + +def test_polarPlot(win): + layout = LayoutMaker(cols=1, rows=1, win=win, labelEdges=True) + po = polarPlot(layout.getPlot((0, 0))) # convert rectangular plot to polar + po.setAxes(steps=4, rMax=100, makeGrid=True) # build the axes + nvecs = 50 + # th = np.linspace(-np.pi*2, np.pi*2-np.pi*2/nvecs, nvecs) + th = np.linspace(-np.pi * 4, 0, nvecs) + r = np.linspace(10, 100, nvecs) + po.plot( + r, th, vectors=True, arrowhead=True, symbols="o", pen=pg.mkPen("k", width=1.5) + ) # plot with arrowheads + nvecs = 8 + th = np.linspace(-np.pi * 2, np.pi * 2 - np.pi * 2 / nvecs, nvecs) + r = np.linspace(10, 100, nvecs) + # po.plot(r, th, vectors=True, arrowhead=False, symbols='o', pen=pg.mkPen('r', width=1.5)) # plot with just lines + + show() + + +if __name__ == "__main__": + win = figure(title="testing") + test_layout(win) + # test_crossAxes(win) + # test_polarPlot(win) diff --git a/cnmodel/util/random_seed.py b/cnmodel/util/random_seed.py new file mode 100644 index 0000000..44c2a6e --- /dev/null +++ b/cnmodel/util/random_seed.py @@ -0,0 +1,28 @@ +import numpy as np +import hashlib, struct + +_current_seed = 0 + + +def set_seed(seed): + """ + Set the random seed to be used globally. If a string is supplied, it + will be converted to int using hash(). + + This immediately seeds the numpy RNG. Any other RNGs must be seeded using + current_seed() + """ + if isinstance(seed, str): + seed = struct.unpack("=I", hashlib.md5(seed.encode("utf-8")).digest()[:4])[0] + np.random.seed(seed) + assert seed < 2 ** 64 # neuron RNG fails if seed is too large + global _current_seed + _current_seed = seed + return seed + + +def current_seed(): + """ + Return the currently-set global random seed. + """ + return _current_seed diff --git a/cnmodel/util/sound.py b/cnmodel/util/sound.py new file mode 100644 index 0000000..4126b02 --- /dev/null +++ b/cnmodel/util/sound.py @@ -0,0 +1,1415 @@ +""" +Tools for generating auditory stimuli. +""" +from __future__ import division +import numpy as np +import scipy +import scipy.io.wavfile +import resampy + + +def create(type, **kwds): + """ Create a Sound instance using a key returned by Sound.key(). + """ + cls = globals()[type] + return cls(**kwds) + + +class Sound(object): + """ + Base class for all sound stimulus generators. + """ + + def __init__(self, duration, rate=100e3, **kwds): + """ + Parameters + ---------- + duration: float (no default): + duration of the stimulus, in seconds + + rate : float (default: 100000.) + sample rate for sound generation + + """ + self.opts = {"rate": rate, "duration": duration} + self.opts.update(kwds) + self._time = None + self._sound = None + + @property + def sound(self): + """ + :obj:`array`: The generated sound array, expressed in Pascals. + """ + if self._sound is None: + self._sound = self.generate() + return self._sound + + @property + def time(self): + """ + :obj:`array`: The array of time values, expressed in seconds. + """ + if self._time is None: + self._time = np.linspace(0, self.opts["duration"], self.num_samples) + return self._time + + @property + def num_samples(self): + """ + int: The number of samples in the sound array. + """ + return 1 + int(self.opts["duration"] * self.opts["rate"]) + + @property + def dt(self): + """ + float: the sample period (time step between samples). + """ + return 1.0 / self.opts["rate"] + + @property + def duration(self): + """ + float: The duration of the sound + """ + return self.opts["duration"] + + def key(self): + """ + The sound can be recreated using ``create(**key)``. + :obj:`dict`: Return dict of parameters needed to completely describe this sound. + """ + k = self.opts.copy() + k["type"] = self.__class__.__name__ + return k + + def measure_dbspl(self, tstart, tend): + """ + Measure the sound pressure for the waveform in a window of time + + Parameters + ---------- + tstart : + time to start spl measurement (seconds). + + tend : + ending time for spl measurement (seconds). + + Returns + ------- + float : The measured amplitude (dBSPL) of the sound from tstart to tend + + """ + istart = int(tstart * self.opts["rate"]) + iend = int(tend * self.opts["rate"]) + return pa_to_dbspl(self.sound[istart:iend].std()) + + def generate(self): + """ + Generate and return the sound output. This method is defined by subclasses. + """ + raise NotImplementedError() + + def __getattr__(self, name): + if "opts" not in self.__dict__: + raise AttributeError(name) + if name in self.opts: + return self.opts[name] + else: + return object.__getattr__(self, name) + + +class TonePip(Sound): + """ Create one or more tone pips with cosine-ramped edges. + + Parameters + ---------- + rate : float + Sample rate in Hz + duration : float + Total duration of the sound + f0 : float or array-like + Tone frequency in Hz. Must be less than half of the sample rate. + dbspl : float + Maximum amplitude of tone in dB SPL. + pip_duration : float + Duration of each pip including ramp time. Must be at least + 2 * ramp_duration. + pip_start : array-like + Start times of each pip + ramp_duration : float + Duration of a single ramp period (from minimum to maximum). + This may not be more than half of pip_duration. + + """ + + def __init__(self, **kwds): + reqdWords = [ + "rate", + "duration", + "f0", + "dbspl", + "pip_duration", + "pip_start", + "ramp_duration", + ] + for k in reqdWords: + if k not in kwds.keys(): + raise TypeError("Missing required argument '%s'" % k) + if kwds["pip_duration"] < kwds["ramp_duration"] * 2: + raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + if kwds["f0"] > kwds["rate"] * 0.5: + raise ValueError("f0 must be less than (0.5 * rate).") + Sound.__init__(self, **kwds) + + def generate(self): + """ + Call to compute the tone pips + + Returns + ------- + array : + generated waveform + + """ + o = self.opts + return piptone( + self.time, + o["ramp_duration"], + o["rate"], + o["f0"], + o["dbspl"], + o["pip_duration"], + o["pip_start"], + ) + + +class FMSweep(Sound): + """ Create an FM sweep with either linear or logarithmic rates, + of a specified duration between two frequencies. + + Parameters + ---------- + rate : float + Sample rate in Hz + duration : float + Total duration of the sweep + start : float + t times of each pip + freqs : list + [f0, f1]: the start and stop times for the sweep + ramp : string + valid input for type of sweep (linear, logarithmic, etc) + dbspl : float + Maximum amplitude of pip in dB SPL. + """ + + def __init__(self, **kwds): + for k in ["rate", "duration", "start", "freqs", "ramp", "dbspl"]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + Sound.__init__(self, **kwds) + + def generate(self): + """ + Call to actually compute the the FM sweep + + Returns + ------- + array : + generated waveform + + """ + o = self.opts + return fmsweep( + self.time, o["start"], o["duration"], o["freqs"], o["ramp"], o["dbspl"] + ) + + +class NoisePip(Sound): + """ One or more noise pips with cosine-ramped edges. + + Parameters + ---------- + rate : float + Sample rate in Hz + duration : float + Total duration of the sound + seed : int >= 0 + Random seed + dbspl : float + Maximum amplitude of tone in dB SPL. + pip_duration : float + Duration of each pip including ramp time. Must be at least + 2 * ramp_duration. + pip_start : array-like + Start times of each pip + ramp_duration : float + Duration of a single ramp period (from minimum to maximum). + This may not be more than half of pip_duration. + + """ + + def __init__(self, **kwds): + for k in [ + "rate", + "duration", + "dbspl", + "pip_duration", + "pip_start", + "ramp_duration", + "seed", + ]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + if kwds["pip_duration"] < kwds["ramp_duration"] * 2: + raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + if kwds["seed"] < 0: + raise ValueError("Random seed must be integer > 0") + + Sound.__init__(self, **kwds) + + def generate(self): + """ + Call to compute the noise pips + + Returns + ------- + array : + generated waveform + + """ + o = self.opts + return pipnoise( + self.time, + o["ramp_duration"], + o["rate"], + o["dbspl"], + o["pip_duration"], + o["pip_start"], + o["seed"], + ) + + +class ClickTrain(Sound): + """ One or more clicks (rectangular pulses). + + Parameters + ---------- + rate : float + Sample rate in Hz + dbspl : float + Maximum amplitude of click in dB SPL. + click_duration : float + Duration of each click including ramp time. Must be at least + 1/rate. + click_starts : array-like + Start times of each click + """ + + def __init__(self, **kwds): + for k in ["rate", "duration", "dbspl", "click_duration", "click_starts"]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + if kwds["click_duration"] < 1.0 / kwds["rate"]: + raise ValueError("click_duration must be greater than sample rate.") + + Sound.__init__(self, **kwds) + + def generate(self): + o = self.opts + return clicks( + self.time, o["rate"], o["dbspl"], o["click_duration"], o["click_starts"] + ) + + +class SAMNoise(Sound): + """ One or more gaussian noise pips with cosine-ramped edges. + + Parameters + ---------- + rate : float + Sample rate in Hz + duration : float + Total duration of the sound + seed : int >= 0 + Random seed + dbspl : float + Maximum amplitude of pip in dB SPL. + pip_duration : float + Duration of each pip including ramp time. Must be at least + 2 * ramp_duration. + pip_start : array-like + Start times of each pip + ramp_duration : float + Duration of a single ramp period (from minimum to maximum). + This may not be more than half of pip_duration. + fmod : float + SAM modulation frequency + dmod : float + Modulation depth + """ + + def __init__(self, **kwds): + parms = [ + "rate", + "duration", + "seed", + "pip_duration", + "pip_start", + "ramp_duration", + "fmod", + "dmod", + "seed", + ] + for k in parms: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + if kwds["pip_duration"] < kwds["ramp_duration"] * 2: + raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + if kwds["seed"] < 0: + raise ValueError("Random seed must be integer > 0") + + Sound.__init__(self, **kwds) + + def generate(self): + """ + Call to compute the SAM noise + + Returns + ------- + array : + generated waveform + + """ + o = self.opts + o["phaseshift"] = 0.0 + return modnoise( + self.time, + o["ramp_duration"], + o["rate"], + o["f0"], + o["pip_duration"], + o["pip_start"], + o["dbspl"], + o["fmod"], + o["dmod"], + 0.0, + o["seed"], + ) + + +class SAMTone(Sound): + """ SAM tones with cosine-ramped edges. + + Parameters + ---------- + rate : float + Sample rate in Hz + duration : float + Total duration of the sound + f0 : float or array-like + Tone frequency in Hz. Must be less than half of the sample rate. + dbspl : float + Maximum amplitude of tone in dB SPL. + pip_duration : float + Duration of each pip including ramp time. Must be at least + 2 * ramp_duration. + pip_start : array-like + Start times of each pip + ramp_duration : float + Duration of a single ramp period (from minimum to maximum). + This may not be more than half of pip_duration. + fmod : float + SAM modulation frequency, Hz + dmod : float + Modulation depth, % + + """ + + def __init__(self, **kwds): + + for k in [ + "rate", + "duration", + "f0", + "dbspl", + "pip_duration", + "pip_start", + "ramp_duration", + "fmod", + "dmod", + ]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + if kwds["pip_duration"] < kwds["ramp_duration"] * 2: + raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + if kwds["f0"] > kwds["rate"] * 0.5: + raise ValueError("f0 must be less than (0.5 * rate).") + + Sound.__init__(self, **kwds) + + def generate(self): + """ + Call to compute a SAM tone + + Returns + ------- + array : + generated waveform + + """ + o = self.opts + basetone = piptone( + self.time, + o["ramp_duration"], + o["rate"], + o["f0"], + o["dbspl"], + o["pip_duration"], + o["pip_start"], + ) + return sinusoidal_modulation( + self.time, basetone, o["pip_start"], o["fmod"], o["dmod"], 0.0 + ) + + +def pa_to_dbspl(pa, ref=20e-6): + """ Convert Pascals (rms) to dBSPL. By default, the reference pressure is + 20 uPa. + """ + return 20 * np.log10(pa / ref) + + +def dbspl_to_pa(dbspl, ref=20e-6): + """ Convert dBSPL to Pascals (rms). By default, the reference pressure is + 20 uPa. + """ + return ref * 10 ** (dbspl / 20.0) + + +class SAMNoise(Sound): + """ One or more gaussian noise pips with cosine-ramped edges, sinusoidally modulated. + + Parameters + ---------- + rate : float + Sample rate in Hz + duration : float + Total duration of the sound + seed : int >= 0 + Random seed + dbspl : float + Maximum amplitude of pip in dB SPL. + pip_duration : float + Duration of each pip including ramp time. Must be at least + 2 * ramp_duration. + pip_start : array-like + Start times of each pip + ramp_duration : float + Duration of a single ramp period (from minimum to maximum). + This may not be more than half of pip_duration. + fmod : float + SAM modulation frequency + dmod : float + Modulation depth + + Returns + ------- + array : + waveform + + """ + + def __init__(self, **kwds): + for k in [ + "rate", + "duration", + "seed", + "pip_duration", + "pip_start", + "ramp_duration", + "fmod", + "dmod", + ]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + if kwds["pip_duration"] < kwds["ramp_duration"] * 2: + raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + if kwds["seed"] < 0: + raise ValueError("Random seed must be integer > 0") + + Sound.__init__(self, **kwds) + + def generate(self): + o = self.opts + basenoise = pipnoise( + self.time, + o["ramp_duration"], + o["rate"], + o["dbspl"], + o["pip_duration"], + o["pip_start"], + o["seed"], + ) + return sinusoidal_modulation( + self.time, basenoise, o["pip_start"], o["fmod"], o["dmod"], 0.0 + ) + + +class ComodulationMasking(Sound): + """ + Make a stimulus for comodulation masking release. + Note the parameter names are shortened so that the SGC generated filename + in cnmodel fits within system limits. + + Parameters + ---------- + rate : float + sample rate, in Hz + + dur : float + entire waveform duration in seconds + + pipst : float + time to start the test tone pips (seconds) + array, such as [0.25, 0.35, 0.45] + + pipdu : float + duration of the test (target) tone pips + + maskst : float + time to start the masker tones pips (seconds) + array, such as [0.1] + + maskdu : float + duration of the masker tone pips + + rf : float + rise/fall of the pips + + f0 : float (kHz) + Center frequency for the target tone, in kHz + + db : float + on-target masker and flankinb band intensity + In dB SPL (re 0.00002 dynes/cm2) + + s2n : float + signal re masker, in dbspl + + fmod : float + amplitude modulation frequency, in Hz + + dmod : float + amplitude modulation depth, in % + + fltype : string + Flanking type: + One of: + 'MultiTone' : multiple tones, with phase, spacing and # of bands specified as below + 'NBNoise' : the flanking stimulus is made up of narrow band noises (not implemented) + 'None' : no flanking sounds (just on-target stimuli) + + flspc : float + Spacing of flanking bands in octaves from the center frequency, f0 + + flgap : int + gap around the cf in flspc (e.g., 1 would skip the first adjacent band) + + flph : string + One of: + 'Comodulated': all of the flanking tones are comodulated in phase + with the target at the same frequency and depth + 'Codgh' : 'Grose and Hall codeviant': + The flanking bands have the same amplitude and frequency + as the target, but the phase of each band is different. Phases are + calculated so that all the bands wrap around 2*pi + 'Codvw' : 'Verhey and Winter codeviant': + The flanking bands have the same amplitude and frequency + as the target, but are out of phase with the on-frequency masker + 'Random': The phases are selected at random. This is probably best only + used when there are a large number of flanking bands. + + flspl : float + Flanking signal, in dbspl. + + flN : int + Number of flanking bands on either side of f0, spaced accordingly. + + Returns + ------- + array : + waveform + + """ + + def __init__(self, **kwds): + # print (kwds) + for k in [ + "rate", + "duration", + "pipdu", + "pipst", + "rf", + "maskst", + "maskdu", + # general: + "f0", + "dbspl", + "s2n", + "fmod", + "dmod", + # flankers: + "flgap", + "fltype", + "flspc", + "flph", + "flN", + ]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + if "flspl" not in kwds: + kwds["flspl"] = kwds["dbspl"] + # if 'mask_spl' not in kwds: + # kwds['mask_spl'] = kwds['dbspl'] + # if kwds['mask_spl'] is None: + # kwds['mask_spl'] = 0. + if kwds["flspl"] is None: + raise ValueError() + + Sound.__init__(self, **kwds) + + def generate(self): + + o = self.opts + # start with center tone + onfreqmasker = piptone( + self.time, o["rf"], o["rate"], o["f0"], o["flspl"], o["maskdu"], o["maskst"] + ) + tardelay = ( + 0.0 + ) # 1.5/o['fmod'] # delay by one and one half cycles (no target in first dip) + target = piptone( + self.time, + o["rf"], + o["rate"], + o["f0"], + o["dbspl"] + o["s2n"], + o["pipdu"] - tardelay, + [p + tardelay for p in o["pipst"]], + ) + # target = shape_signal(onfreqmasker, self.time, o['rf'], o['rate'], o['f0'], + # o['dbspl']+o['s2n'], o['pipdu']-tardelay, [p + tardelay for p in o['pipst']]) + if (o["dbspl"] + o["s2n"]) <= 0.0: + target = np.zeros_like(target) + onfreqmasker = sinusoidal_modulation( + self.time, onfreqmasker, o["maskst"], o["fmod"], o["dmod"], 0.0 + ) + # print("o['dbspl']+o['s2n']: ", o['dbspl']+o['s2n']) + # print('np.max(target): ', np.max(target)) + + # target = sinusoidal_modulation(self.time, target, [p + tardelay for p in o['pip_start']], + # o['fmod'], o['dmod'], 0.) + self.onmask = onfreqmasker + self.target = target + if o["fltype"] not in ["None", "Ref", "NBN", "Tone"]: + raise ValueError("Unknown flanking_type: %s" % o["fltype"]) + if o["fltype"] in ["NBN"]: + raise ValueError('Flanking type "NBNoise" is not yet implemented') + elif o["fltype"] in ["None"]: + return (onfreqmasker + target) / 2.0 # scaling... + elif o["fltype"] in ["Tone"]: + nband = int(o["flN"]) + gap = int(o["flgap"]) + octspace = o["flspc"] + f0 = o["f0"] + flankfs = [f0 * (2 ** (octspace * (k + 1 + gap))) for k in range(nband)] + flankfs.extend( + [f0 / ((2 ** (octspace * (k + 1 + gap)))) for k in range(nband)] + ) + flankfs = sorted(flankfs) + flanktone = [[]] * len(flankfs) + for i, fs in enumerate(flankfs): + flanktone[i] = piptone( + self.time, + o["rf"], + o["rate"], + flankfs[i], + o["flspl"], + o["maskdu"], + o["maskst"], + ) + elif o["fltype"] in ["None", "Ref"]: + return (onfreqmasker + target) / 2.0 # scaling... + if o["fltype"] == "NBN": + raise ValueError("Flanking type nbnoise not yet implemented") + + ph = 0.0 + if o["flph"] == "Comod": + ph = np.zeros(len(flankfs)) + elif o["flph"] == "Codvw": # verhey and winter: just 180 out of phase + ph = np.pi * np.ones(len(flankfs)) + elif o["flph"] in ["Codgh", "Codev"]: # with precession: Grose and Hall 89 + ph = 2.0 * np.pi * np.arange(-o["flN"], o["flN"] + 1, 1) / o["flN"] + elif o["flph"] == "Random": + ph = 2.0 * np.pi * np.arange(-o["flN"], o["flN"] + 1, 1) / o["flN"] + raise ValueError("Random flanking phases not implemented") + else: + raise ValueError( + "Masker Phase pattern of type %s is not implemented" % o["flph"] + ) + # print(('flanking phases: ', ph)) + # print (len(flanktone)) + # print(('flanking freqs: ', flankfs)) + for i, fs in enumerate(flankfs): + flanktone[i] = sinusoidal_modulation( + self.time, flanktone[i], o["maskst"], o["fmod"], o["dmod"], ph[i] + ) + if i == 0: + maskers = flanktone[i] + else: + maskers = maskers + flanktone[i] + signal = (onfreqmasker + maskers + target) / (o["flN"] + 2) + return signal + + +class DynamicRipple(Sound): + def __init__(self, **kwds): + for k in ["rate", "duration"]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + # if kwds['pip_duration'] < kwds['ramp_duration'] * 2: + # raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + import DMR + + self.dmr = DMR.DMR() + Sound.__init__(self, **kwds) + + def generate(self): + """ + Call to compute a dynamic ripple stimulus + + Returns + ------- + array : + + generated waveform + """ + o = self.opts + self.dmr.set_params(Fs=o["rate"], duration=o["duration"] + 1.0 / o["rate"]) + self.dmr.make_waveform() + self._time = self.dmr.vTime # get time from the generator, not linspace + return self.dmr.vStim + + +class SpeechShapedNoise(Sound): + """ + Adapted from http://www.srmathias.com/speech-shaped-noise/ + """ + + def __init__(self, **kwds): + for k in ["rate", "duration", "waveform", "samplingrate"]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + # if kwds['pip_duration'] < kwds['ramp_duration'] * 2: + # raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + Sound.__init__(self, **kwds) + + def generate(self): + o = self.opts + print("opts: ", o) + ssn, t = make_ssn( + o["rate"], o["duration"], o["waveform"].sound, o["samplingrate"] + ) + self._time = t # override time array because we read a wave file + # if self.opts['duration'] == 0: + # self.opts['duration'] = np.max(t) - 1./o['rate'] + return ssn + + +class RandomSpectrumShape(Sound): + """ + Random Spectral Shape stimuli + log-spaced tones + Amplitudes adjusted in groups of 4 or 8 (amp_group_size) + Amplitude SD (amp_sd) + Frequency range (octaves above and below f0) (octaves) + spacing (fraction of octave: e.g, 1/8 or 1/64 as 8 or 64) (spacing) + + Generates one sample + + Young and Calhoun, 2005 + Yu and Young, 2000 + """ + + def __init__(self, **kwds): + for k in [ + "rate", + "duration", + "f0", + "dbspl", + "pip_duration", + "pip_start", + "ramp_duration", + "amp_group_size", + "amp_sd", + "spacing", + "octaves", + ]: + if k not in kwds: + raise TypeError("Missing required argument '%s'" % k) + if kwds["pip_duration"] < kwds["ramp_duration"] * 2: + raise ValueError("pip_duration must be greater than (2 * ramp_duration).") + if kwds["f0"] > kwds["rate"] * 0.5: + raise ValueError("f0 must be less than (0.5 * rate).") + + Sound.__init__(self, **kwds) + + def generate(self): + o = self.opts + octaves = o["octaves"] + lowf = o["f0"] / octaves + highf = o["f0"] * octaves + freqlist = np.logspace( + np.log2(lowf), + np.log2(highf), + num=o["spacing"] * octaves * 2, + endpoint=True, + base=2, + ) + amplist = np.zeros_like(freqlist) + db = o["dbspl"] + # assign amplitudes + if db == None: + db = 80.0 + groupsize = o["amp_group_size"] + for i in range(0, len(freqlist), groupsize): + if o["amp_sd"] > 0.0: + a = np.random.normal(scale=o["amp_sd"]) + else: + a = 0.0 + amplist[i : i + groupsize] = 20.0 * np.log10(a + db) + for i in range(len(freqlist)): + wave = piptone( + self.time, + o["ramp_duration"], + o["rate"], + freqlist[i], + amplist[i], + o["pip_duration"], + o["pip_start"], + pip_phase=np.pi * 2.0 * np.random.rand(), + ) + if i == 0: + result = wave + else: + result = result + wave + return result / (np.sqrt(np.mean(result ** 2.0))) # scale by rms level + + +class ReadWavefile(Sound): + """ Read a .wav file from disk, possibly converting the sample rate and the scale + for use in driving the auditory nerve fiber model. + + Parameters + ---------- + wavefile : str + name of the .wav file to read + rate : float + Sample rate in Hz (waveform will be resampled to this rate) + channel: int (default: 0) + If wavefile has 2 channels, select 0 or 1 for the channel to read + dbspl : float or None + If specified, the wave file is scaled such that its overall dBSPL + (measured from RMS of the entire waveform) is equal to this value. + Either ``dbspl`` or ``scale`` must be specified. + scale : float or None + If specified, the wave data is multiplied by this value to yield values in dBSPL. + Either ``dbspl`` or ``scale`` must be specified. + delay: float (default: 0.) + Silent delay time to start sound, in s. Allows anmodel and cells to run to steady-state + maxdur : float or None (default: None) + If specified, maxdur defines the total duration of generated waveform to return (in seconds). + If None, the generated waveform duration will be the sum of any delay value and + the duration of the waveform from the wavefile. + + Returns + ------- + array : + waveform + + """ + + def __init__( + self, wavefile, rate, channel=0, dbspl=None, scale=None, delay=0.0, maxdur=None + ): + if dbspl is not None and scale is not None: + raise ValueError('Only one of "dbspl" or "scale" can be set') + duration = ( + 0.0 + ) # forced because of the way num_samples has to be calculated first + if delay < 0.0: + raise ValueError("ReadWavefile: delay must be > 0., got: %f" % delay) + if maxdur is not None and maxdur < 0: + raise ValueError( + "ReadWavefile: maxdur must be None or > 0., got: %f" % maxdur + ) + Sound.__init__( + self, + duration, + rate, + wavefile=wavefile, + channel=channel, + dbspl=dbspl, + scale=scale, + maxdur=maxdur, + delay=delay, + ) + + def generate(self): + """ + Read the wave file from disk, clip duration, resample if necessary, and scale + + Returns + ------- + array : generated waveform + """ + [fs_wav, stimulus] = scipy.io.wavfile.read( + self.opts["wavefile"] + ) # raw is a numpy array of integer, representing the samples + if len(stimulus.shape) > 1 and stimulus.shape[1] > 0: + stimulus = stimulus[:, self.opts["channel"]] # just use selected channel + fs_wav = float(fs_wav) + maxdur = self.opts["maxdur"] + delay = self.opts["delay"] + delay_array = np.zeros( + int(delay * fs_wav) + ) # build delay array (may have 0 length) + if maxdur is None: + maxdur = delay + len(stimulus) / fs_wav # true total length + maxpts = int(maxdur * fs_wav) + stimulus = np.hstack((delay_array, stimulus))[:maxpts] + + if self.opts["rate"] != fs_wav: + stimulus = resampy.resample(stimulus, fs_wav, self.opts["rate"]) + self.opts["duration"] = (stimulus.shape[0] - 1) / self.opts[ + "rate" + ] # compute the duration, match for linspace calculation used in time. + self._time = None + self.time # requesting time should cause recalulation of the time + if self.opts["dbspl"] is not None: + rms = np.sqrt(np.mean(stimulus ** 2.0)) # find rms of the waveform + stimulus = ( + dbspl_to_pa(self.opts["dbspl"]) * stimulus / rms + ) # scale into Pascals + if self.opts["scale"] is not None: + stimulus = stimulus * self.opts["scale"] + return stimulus + + +def sinusoidal_modulation(t, basestim, tstart, fmod, dmod, phaseshift): + """ + Impose a sinusoidal amplitude-modulation on the input waveform. + For dmod=100%, the envelope max is 2, the min is 0; for dmod = 0, the max and min are 1 + maintains equal energy for all modulation depths. + Equation from Rhode and Greenberg, J. Neurophys, 1994 (adding missing parenthesis) and + Sayles et al. J. Physiol. 2013 + The envelope can be phase shifted (useful for co-deviant stimuli). + + Parameters + ---------- + t : array + array of waveform time values (seconds) + basestim : array + array of waveform values that will be subject to sinulsoidal envelope modulation + tstart : float + time at which the base sound starts (modulation starts then, with 0 phase crossing) + (seconds) + fmod : float + modulation frequency (Hz) + dmod : float + modulation depth (percent) + phaseshift : float + modulation phase shift (starting phase, radians) + + """ + + env = 1.0 + (dmod / 100.0) * np.sin( + (2.0 * np.pi * fmod * (t - tstart)) + phaseshift - np.pi / 2 + ) # envelope... + return basestim * env + + +def modnoise(t, rt, Fs, F0, dur, start, dBSPL, FMod, DMod, phaseshift, seed): + """ + Generate an amplitude-modulated noise with linear ramps. + + Parameters + ---------- + t : array + array of waveform time values + rt : float + ramp duration + Fs : float + sample rate + F0 : float + tone frequency + dur : float + duration of noise + start : float + start time for noise + dBSPL : float + sound pressure of stimulus + FMod : float + modulation frequency + DMod : float + modulation depth percent + phaseshift : float + modulation phase + seed : int + seed for random number generator + + Returns + ------- + array : + waveform + + """ + irpts = int(rt * Fs) + mxpts = len(t) + 1 + pin = pipnoise(t, rt, Fs, dBSPL, dur, start, seed) + env = 1 + (DMod / 100.0) * np.sin( + (2 * np.pi * FMod * t) - np.pi / 2 + phaseshift + ) # envelope... + + pin = linearramp(pin, mxpts, irpts) + env = linearramp(env, mxpts, irpts) + return pin * env + + +def linearramp(pin, mxpts, irpts): + """ + Apply linear ramps to *pin*. + + Parameters + ---------- + pin : array + input waveform to apply ramp to + + mxpts : int + point in array to start ramp down + + irpts : int + duration of the ramp + + Returns + ------- + array : + waveform + + + Original (adapted from Manis; makeANF_CF_RI.m):: + + function [out] = ramp(pin, mxpts, irpts) + out = pin; + out(1:irpts)=pin(1:irpts).*(0:(irpts-1))/irpts; + out((mxpts-irpts):mxpts)=pin((mxpts-irpts):mxpts).*(irpts:-1:0)/irpts; + return; + end + """ + out = pin.copy() + r = np.linspace(0, 1, irpts) + irpts = int(irpts) + # print 'irpts: ', irpts + # print len(out) + out[:irpts] = out[:irpts] * r + # print out[mxpts-irpts:mxpts].shape + # print r[::-1].shape + out[mxpts - irpts - 1 : mxpts] = out[mxpts - irpts - 1 : mxpts] * r[::-1] + return out + + +def pipnoise(t, rt, Fs, dBSPL, pip_dur, pip_start, seed): + """ + Create a waveform with multiple sine-ramped noise pips. Output is in + Pascals. + + Parameters + ---------- + t : array + array of time values + rt : float + ramp duration + Fs : float + sample rate + dBSPL : float + maximum sound pressure level of pip + pip_dur : float + duration of pip including ramps + pip_start : float + list of starting times for multiple pips + seed : int + random seed + + Returns + ------- + array : + waveform + + """ + rng = np.random.RandomState(seed) + pin = np.zeros(t.size) + for start in pip_start: + # make pip template + pip_pts = int(pip_dur * Fs) + 1 + pip = dbspl_to_pa(dBSPL) * rng.randn(pip_pts) # unramped stimulus + + # add ramp + ramp_pts = int(rt * Fs) + 1 + ramp = np.sin(np.linspace(0, np.pi / 2.0, ramp_pts)) ** 2 + pip[:ramp_pts] *= ramp + pip[-ramp_pts:] *= ramp[::-1] + + ts = int(np.floor(start * Fs)) + pin[ts : ts + pip.size] += pip + + return pin + + +def piptone(t, rt, Fs, F0, dBSPL, pip_dur, pip_start): + """ + Create a waveform with multiple sine-ramped tone pips. Output is in + Pascals. + + Parameters + ---------- + t : array + array of time values + rt : float + ramp duration + Fs : float + sample rate + F0 : float + pip frequency + dBSPL : float + maximum sound pressure level of pip + pip_dur : float + duration of pip including ramps + pip_start : float + list of starting times for multiple pips + + Returns + ------- + array : + waveform + + """ + # make pip template + pip_pts = int(pip_dur * Fs) + 1 + pip_t = np.linspace(0, pip_dur, pip_pts) + pip = ( + np.sqrt(2) * dbspl_to_pa(dBSPL) * np.sin(2 * np.pi * F0 * pip_t) + ) # unramped stimulus + + # add ramp + ramp_pts = int(rt * Fs) + 1 + ramp = np.sin(np.linspace(0, np.pi / 2.0, ramp_pts)) ** 2 + pip[:ramp_pts] *= ramp + pip[-ramp_pts:] *= ramp[::-1] + + # apply template to waveform + pin = np.zeros(t.size) + ps = pip_start + if ~isinstance(ps, list): + ps = [ps] + for start in pip_start: + ts = int(np.floor(start * Fs)) + pin[ts : ts + pip.size] += pip + + return pin + + +def shape_signal(signal, t, rt, Fs, F0, dBSPL, pip_dur, pip_start): + """ + Create a waveform with multiple sine-ramped tone pips, based on the + signal (so output is *always* in phase with the reference signal waveform) + Output is in Pascals. + + Parameters + ---------- + t : array + array of time values + rt : float + ramp duration (risetime) + Fs : float + sample rate + F0 : float + pip frequency + dBSPL : float + maximum sound pressure level of pip + pip_dur : float + duration of pip including ramps + pip_start : float + list of starting times for multiple pips + + Returns + ------- + array : + waveform + + """ + pip_t = t + pip_pts = pip_t.shape[0] + pip = np.sqrt(2) * dbspl_to_pa(dBSPL) * signal # referencestimulus + + # make envelope with cos2 rise-fall + ramp_pts = int(rt * Fs) + 1 + env_pts = int(pip_dur * Fs) + envelope = np.ones(env_pts) + ramp = np.sin(np.linspace(0, np.pi / 2.0, ramp_pts)) ** 2 + envelope[:ramp_pts] *= ramp + envelope[-ramp_pts:] *= ramp[::-1] + + # apply envelope template to waveform + pin = np.zeros(t.size) + ps = pip_start + if ~isinstance(ps, list): + ps = [ps] + for start in pip_start: + ts = int(np.floor(start * Fs)) + pin[ts : ts + envelope.size] += pip[ts : ts + envelope.size] * envelope + + return pin + + +def clicks(t, Fs, dBSPL, click_duration, click_starts): + """ + Create a waveform with multiple retangular clicks. Output is in + Pascals. + + Parameters + ---------- + t : array + array of time values + Fs : float + sample frequency (Hz) + click_start : float (seconds) + delay to first click in train + click_duration : float (seconds) + duration of each click + click_interval : float (seconds) + interval between click starts + nclicks : int + number of clicks in the click train + dspl : float + maximum sound pressure level of pip + + Returns + ------- + array : + waveform + + """ + swave = np.zeros(t.size) + amp = dbspl_to_pa(dBSPL) + td = int(np.floor(click_duration * Fs)) + nclicks = len(click_starts) + for n in range(nclicks): + t0s = click_starts[n] # time for nth click + t0 = int(np.floor(t0s * Fs)) # index + if t0 + td > t.size: + raise ValueError("Clicks: train duration exceeds waveform duration") + swave[t0 : t0 + td] = amp + return swave + + +def fmsweep(t, start, duration, freqs, ramp, dBSPL): + """ + Create a waveform for an FM sweep over time. Output is in + Pascals. + + Parameters + ---------- + t : array + time array for waveform + start : float (seconds) + start time for sweep + duration : float (seconds) + duration of sweep + freqs : array (Hz) + Two-element array specifying the start and end frequency of the sweep + ramp : str + The shape of time course of the sweep (linear, logarithmic) + dBSPL : float + maximum sound pressure level of sweep + + Returns + ------- + array : + waveform + + + """ + + sw = scipy.signal.chirp( + t, freqs[0], duration, freqs[1], method=ramp, phi=0, vertex_zero=True + ) + sw = np.sqrt(2) * dbspl_to_pa(dBSPL) * sw + return sw + + +def make_ssn(rate, duration, sig, samplingrate): + """ + Speech-shaped noise + Adapted from http://www.srmathias.com/speech-shaped-noise/ + Created on Thu Jun 26 12:42:08 2014 + @author: smathias + """ + # note rate is currently ignored... + sig = np.array(sig).astype("float64") + if ( + rate != samplingrate + ): # interpolate to the current system sampling rate from the original rate + sig = np.interp( + np.arange(0, len(sig) / rate, 1.0 / rate), + np.arange(0, len(sig) / samplingrate), + 1.0 / samplingrate, + ) + sig = 2 * sig / np.max(sig) + z, t = noise_from_signal(sig, rate, keep_env=True) + return z, t + + +def noise_from_signal(x, fs=40000, keep_env=True): + """Create a noise with same spectrum as the input signal. + Parameters + ---------- + x : array_like + Input signal. + fs : int + Sampling frequency of the input signal. (Default value = 40000) + keep_env : bool + Apply the envelope of the original signal to the noise. (Default + value = False) + Returns + ------- + ndarray + Noise signal. + """ + x = np.asarray(x) + n_x = x.shape[-1] + n_fft = next_pow_2(n_x) + X = np.fft.rfft(x, next_pow_2(n_fft)) + # Randomize phase. + noise_mag = np.abs(X) * np.exp(2.0 * np.pi * 1j * np.random.random(X.shape[-1])) + noise = np.real(np.fft.irfft(noise_mag, n_fft)) + out = noise[:n_x] + if keep_env: + env = np.abs(scipy.signal.hilbert(x)) + [bb, aa] = scipy.signal.butter(6.0, 50.0 / (fs / 2.0)) # 50 Hz LP filter + env = scipy.signal.filtfilt(bb, aa, env) + out *= env + t = np.arange(0, (len(out)) / fs, 1.0 / fs) + return out, t diff --git a/cnmodel/util/stim.py b/cnmodel/util/stim.py new file mode 100644 index 0000000..f6f0861 --- /dev/null +++ b/cnmodel/util/stim.py @@ -0,0 +1,87 @@ +import numpy as np + + +def make_pulse(stim): + """ + Generate a pulse train for current / voltage command. Returns a tuple. + + + Parameters + ---------- + stim : dict + Holds parameters that determine stimulus shape: + + * delay : time before first pulse + * Sfreq : frequency of pulses + * dur : duration of one pulse or main pulse + * predur : duration of prepulse (default should be 0 for no prepulse) + * amp : pulse amplitude + * preamp : amplitude of prepulse + * PT : delay between end of train and test pulse (0 for no test) + * NP : number of pulses + * hold : holding level (optional) + * dt : timestep + + Returns + ------- + w : stimulus waveform + maxt : duration of waveform + tstims : index of each pulse in the train + """ + defaults = { + "delay": 10, + "Sfreq": 50, + "dur": 100, + "predur": 0.0, + "post": 50.0, + "amp": None, + "preamp": 0.0, + "PT": 0, + "NP": 1, + "hold": 0.0, + "dt": None, + } + for k in stim: + if k not in defaults: + raise Exception("Stim parameter '%s' not accepted." % k) + defaults.update(stim) + stim = defaults + for k, v in stim.items(): + if v is None: + raise Exception("Must specify stim parameter '%s'." % k) + + dt = stim["dt"] + delay = int(np.floor(stim["delay"] / dt)) + ipi = int(np.floor((1000.0 / stim["Sfreq"]) / dt)) + pdur = int(np.floor(stim["dur"] / dt)) + posttest = int(np.floor(stim["PT"] / dt)) + ndur = 5 + if stim["predur"] > 0.0: + predur = int(np.floor(stim["predur"] / dt)) + else: + predur = 0.0 + if stim["PT"] == 0: + ndur = 1 + + maxt = dt * (delay + predur + (ipi * (stim["NP"] + 3)) + posttest + pdur * ndur) + hold = stim.get("hold", None) + + w = np.zeros(int(np.floor(maxt / dt))) + if hold is not None: + w += hold + + # make pulse + tstims = [0] * int(stim["NP"]) + for j in range(0, int(stim["NP"])): + prestart = delay + start = int(prestart + predur + j * ipi) + if predur > 0.0: + w[prestart : prestart + predur] = stim["preamp"] + w[start : start + pdur] = stim["amp"] + tstims[j] = start + if stim["PT"] > 0.0: + for i in range(start + posttest, start + posttest + pdur): + w[i] = stim["amp"] + w = np.append(w, 0.0) + maxt = maxt + dt + return (w, maxt, tstims) diff --git a/cnmodel/util/talbotetalTicks.py b/cnmodel/util/talbotetalTicks.py new file mode 100644 index 0000000..a86bd95 --- /dev/null +++ b/cnmodel/util/talbotetalTicks.py @@ -0,0 +1,208 @@ +import math +import numpy as np + +# import matplotlib +# import matplotlib.pyplot as plt +# import matplotlib.ticker as tckr +# import matplotlib.transforms as mtransforms +# import matplotlib.mlab as mlab + +# An alpha version of the Talbot, Lin, Hanrahan tick mark generator for matplotlib. +# Described in "An Extension of Wilkinson's Algorithm for Positioning Tick Labels on Axes" +# by Justin Talbot, Sharon Lin, and Pat Hanrahan, InfoVis 2010. + +# Implementation by Justin Talbot +# This implementation is in the public domain. +# Report bugs to jtalbot@stanford.edu + +# A shortcoming: +# The weights used in the paper were designed for static plots where the extent of +# the tick marks unioned with the extent of the data defines the extent of the plot. +# In a plot where the extent of the plot is defined by the user (e.g. an interactive +# plot supporting panning and zooming), the weights don't work as well. In particular, +# you would want to retune them assuming that the tick labels must be inside +# the provided view range. You probably want higher weighting on simplicity and lower +# on coverage and possibly density. But I haven't experimented in any detail with this. +# +# If you do intend on using this for static plots in matplotlib, you should set +# only_inside to False in the call to Extended.extended. And then you should +# manually set your view extent to include the min and max ticks if they are outside +# the data range. This should produce the same results as the paper. + +# class Extended(tckr.Locator): +class Extended: + # density is labels per inch + def __init__(self, density=1, steps=None, figure=None, range=(0, 1), axis="x"): + """ + Keyword args: + """ + self._density = density + self._figure = figure + self._axis = axis + self.range = range + + if steps is None: + self._steps = [1, 5, 2, 2.5, 4, 3] + else: + self._steps = steps + + def coverage(self, dmin, dmax, lmin, lmax): + range = dmax - dmin + return 1 - 0.5 * ( + math.pow(dmax - lmax, 2) + math.pow(dmin - lmin, 2) + ) / math.pow(0.1 * range, 2) + + def coverage_max(self, dmin, dmax, span): + range = dmax - dmin + if span > range: + half = (span - range) / 2.0 + return 1 - math.pow(half, 2) / math.pow(0.1 * range, 2) + else: + return 1 + + def density(self, k, m, dmin, dmax, lmin, lmax): + r = (k - 1.0) / (lmax - lmin) + rt = (m - 1.0) / (max(lmax, dmax) - min(lmin, dmin)) + return 2 - max(r / rt, rt / r) + + def density_max(self, k, m): + if k >= m: + return 2 - (k - 1.0) / (m - 1.0) + else: + return 1 + + def simplicity(self, q, Q, j, lmin, lmax, lstep): + eps = 1e-10 + n = len(Q) + i = Q.index(q) + 1 + v = ( + 1 + if ( + (lmin % lstep < eps or (lstep - lmin % lstep) < eps) + and lmin <= 0 + and lmax >= 0 + ) + else 0 + ) + return (n - i) / (n - 1.0) + v - j + + def simplicity_max(self, q, Q, j): + n = len(Q) + i = Q.index(q) + 1 + v = 1 + return (n - i) / (n - 1.0) + v - j + + def legibility(self, lmin, lmax, lstep): + return 1 + + def legibility_max(self, lmin, lmax, lstep): + return 1 + + def extended( + self, + dmin, + dmax, + m, + Q=[1, 5, 2, 2.5, 4, 3], + only_inside=False, + w=[0.25, 0.2, 0.5, 0.05], + ): + n = len(Q) + best_score = -2.0 + + j = 1.0 + while j < float("infinity"): + for q in Q: + sm = self.simplicity_max(q, Q, j) + + if w[0] * sm + w[1] + w[2] + w[3] < best_score: + j = float("infinity") + break + + k = 2.0 + while k < float("infinity"): + dm = self.density_max(k, m) + + if w[0] * sm + w[1] + w[2] * dm + w[3] < best_score: + break + + delta = (dmax - dmin) / (k + 1.0) / j / q + z = math.ceil(math.log(delta, 10)) + + while z < float("infinity"): + step = j * q * math.pow(10, z) + cm = self.coverage_max(dmin, dmax, step * (k - 1.0)) + + if w[0] * sm + w[1] * cm + w[2] * dm + w[3] < best_score: + break + + min_start = math.floor(dmax / step) * j - (k - 1.0) * j + max_start = math.ceil(dmin / step) * j + + if min_start > max_start: + z = z + 1 + break + + for start in range(int(min_start), int(max_start) + 1): + lmin = start * (step / j) + lmax = lmin + step * (k - 1.0) + lstep = step + + s = self.simplicity(q, Q, j, lmin, lmax, lstep) + c = self.coverage(dmin, dmax, lmin, lmax) + d = self.density(k, m, dmin, dmax, lmin, lmax) + l = self.legibility(lmin, lmax, lstep) + + score = w[0] * s + w[1] * c + w[2] * d + w[3] * l + + if score > best_score and ( + not only_inside or (lmin >= dmin and lmax <= dmax) + ): + best_score = score + best = (lmin, lmax, lstep, q, k) + z = z + 1 + k = k + 1 + j = j + 1 + return best + + def __call__(self): + vmin, vmax = self.range # self.axis.get_view_interval() + fsize = {"x": 5.0, "y": 4.0} + size = fsize[self._axis] # self._figure.get_size_inches()[self._axis] + # density * size gives target number of intervals, + # density * size + 1 gives target number of tick marks, + # the density function converts this back to a density in data units (not inches) + # should probably make this cleaner. + best = self.extended( + vmin, + vmax, + self._density * size + 1.0, + only_inside=True, + w=[0.25, 0.2, 0.5, 0.05], + ) + locs = np.arange(best[4]) * best[2] + best[0] + return locs + + +if __name__ == "__main__": + pass + # fig = plt.figure() + # ax = fig.add_subplot(111) + # ax.plot(10*np.random.randn(100), 10*np.random.randn(100), 'o') + # + # xmin, xmax = ax.xaxis.get_data_interval() + # xrange = xmax-xmin + # xmin, xmax = (xmin - xrange * 0.05, xmax + xrange * 0.05) + # + # ymin, ymax = ax.yaxis.get_data_interval() + # yrange = ymax-ymin + # ymin, ymax = (ymin - yrange * 0.05, ymax + yrange * 0.05) + # + # ax.xaxis.set_view_interval(xmin, xmax, ignore=True) + # ax.yaxis.set_view_interval(ymin, ymax, ignore=True) + # ax.xaxis.set_major_locator(Extended(density=0.5, figure=fig, which=0)) + # ax.yaxis.set_major_locator(Extended(density=0.5, figure=fig, which=1)) + # + # ax.set_title('Talbot, Lin, Hanrahan 2010') + # + # plt.show() diff --git a/cnmodel/util/tests/test_expfitting.py b/cnmodel/util/tests/test_expfitting.py new file mode 100644 index 0000000..cf81900 --- /dev/null +++ b/cnmodel/util/tests/test_expfitting.py @@ -0,0 +1,36 @@ +from __future__ import print_function +from cnmodel.util import ExpFitting +import numpy as np + + +def test_fit1(): + fit = ExpFitting(nexp=1) + x = np.linspace(0.0, 50, 500) + p = [-50.0, 4.0, 5.0] + y = p[0] + p[1] * np.exp(-x / p[2]) + res = fit.fit(x, y, fit.fitpars) + pr = [float(res[k].value) for k in res.keys()] + print("\noriginal: ", p) + print("fit res: ", pr) + for i, v in enumerate(p): + assert np.allclose(v, pr[i]) + + +def test_fit2(): + fit = ExpFitting(nexp=2) + x = np.linspace(0.0, 50, 500) + p = [ + -50.0, + 4.0, + 5.0, + 1.0, + 4.5, + ] # last term is ratio of the two time constants (t2 = delta*t1) + y = p[0] + p[1] * np.exp(-x / p[2]) + p[3] * np.exp(-x / (p[2] * p[4])) + res = fit.fit(x, y, fit.fitpars) + pr = [float(res[k].value) for k in res.keys()] + print("\noriginal: ", p) + print("fit res: ", pr) + # we can only do this approximately for 2 exp fits + for i, v in enumerate(p): # test each one individually + assert np.allclose(pr[i] / v, 1.0, atol=1e-4, rtol=1e-2) diff --git a/cnmodel/util/tests/test_matlab.py b/cnmodel/util/tests/test_matlab.py new file mode 100644 index 0000000..d240b23 --- /dev/null +++ b/cnmodel/util/tests/test_matlab.py @@ -0,0 +1,40 @@ +import pytest +import numpy as np + +# from cnmodel.util.matlab_proc import MatlabProcess +import matlab.engine + + +def test_matlab(): + global proc + try: + # proc = MatlabProcess() + proc = matlab.engine.start_matlab() + except RuntimeError: + # no matlab available; skip this test + pytest.skip("MATLAB unavailable") + + base_vcount = proc.who(nargout=1) # .shape[0] + assert len(base_vcount) == 0 + + e4 = np.array(proc.eye(4, nargout=1)) + + assert isinstance(e4, np.ndarray) + assert np.all(e4 == np.eye(4)) + # assert proc.who().shape[0] == base_vcount + + o6_ref = np.array(proc.ones(6, nargout=1)) # _transfer=False)) + # o6 = o6_ref.get() + o6 = np.array(proc.ones(6, nargout=1)) + assert np.all(o6 == np.ones(6)) + # print(proc.who(nargout=1)) + # assert proc.who(nargout=1) == base_vcount + 1 + + del o6_ref + # assert proc.who().shape[0] == base_vcount + + proc.close() + + +if __name__ == "__main__": + test_matlab() diff --git a/cnmodel/util/tests/test_sound.py b/cnmodel/util/tests/test_sound.py new file mode 100644 index 0000000..fe39007 --- /dev/null +++ b/cnmodel/util/tests/test_sound.py @@ -0,0 +1,98 @@ +import numpy as np +from cnmodel.util import sound + + +def test_conversions(): + pa = np.array([3990.5, 20, 0.3639, 2e-5]) + db = np.array([166, 120, 85.2, 0]) + + assert np.allclose(sound.pa_to_dbspl(pa), db, atol=0.1, rtol=0.002) + assert np.allclose(sound.dbspl_to_pa(db), pa, atol=0.1, rtol=0.002) + + +def test_tonepip(): + rate = 100000 + dur = 0.1 + ps = 0.01 + rd = 0.02 + pd = 0.08 + db = 60 + s1 = sound.TonePip( + rate=rate, + duration=dur, + f0=5321, + dbspl=db, + pip_duration=pd, + pip_start=[ps], + ramp_duration=rd, + ) + + # test array sizes + assert s1.sound.size == s1.time.size == int(dur * rate) + 1 + + # test for consistency + assert np.allclose( + [s1.sound.min(), s1.sound.mean(), s1.sound.max()], + [-0.028284253158247834, -1.0954891976168953e-10, 0.028284270354167296], + ) + + # test that we got the requested amplitude + assert np.allclose(s1.measure_dbspl(ps + rd, ps + pd - rd), db, atol=0.1, rtol=0.01) + + # test for quiet before and after pip + assert np.all(s1.sound[: int(ps * rate) - 1] == 0) + assert np.all(s1.sound[int((ps + pd) * rate) + 1 :] == 0) + + # test the sound can be recreated from its key + key = s1.key() + s2 = sound.create(**key) + assert np.all(s1.time == s2.time) + assert np.all(s1.sound == s2.sound) + + +def test_noisepip(): + rate = 100000 + dur = 0.1 + ps = 0.01 + rd = 0.02 + pd = 0.08 + db = 60 + s1 = sound.NoisePip( + rate=rate, + duration=dur, + seed=184724, + dbspl=db, + pip_duration=pd, + pip_start=[ps], + ramp_duration=rd, + ) + + # test array sizes + assert s1.sound.size == s1.time.size == int(dur * rate) + 1 + + # test for consistency + assert np.allclose( + [s1.sound.min(), s1.sound.mean(), s1.sound.max()], + [-0.082260796003197786, -0.00018484322982972046, 0.069160217220832404], + ) + + # test that we got the requested amplitude + assert np.allclose(s1.measure_dbspl(ps + rd, ps + pd - rd), db, atol=0.1, rtol=0.01) + + # test for quiet before and after pip + assert np.all(s1.sound[: int(ps * rate) - 1] == 0) + assert np.all(s1.sound[int((ps + pd) * rate) + 1 :] == 0) + + # test the sound can be recreated from its key + key = s1.key() + s2 = sound.create(**key) + # also test new seed works, and does not affect other sounds + key["seed"] += 1 + s3 = sound.create(**key) + s3.sound # generate here to advance rng before generating s2 + + assert np.all(s1.time == s2.time) + assert np.all(s1.sound == s2.sound) + start = int(ps * rate) + 1 + end = int((ps + pd) * rate) - 1 + assert not np.any(s1.sound[start:end] == s3.sound[start:end]) diff --git a/cnmodel/util/tests/test_stim.py b/cnmodel/util/tests/test_stim.py new file mode 100644 index 0000000..2991c92 --- /dev/null +++ b/cnmodel/util/tests/test_stim.py @@ -0,0 +1,29 @@ +import numpy as np +from numpy.testing import assert_raises + +from cnmodel.util import stim +from neuron import h + +h.dt = 0.025 + + +def test_make_pulse(): + params = dict(delay=10, Sfreq=50, dur=1, amp=15, PT=0, NP=5) + + assert_raises(Exception, lambda: stim.make_pulse(params)) + params["dt"] = 0.025 + + w, maxt, times = stim.make_pulse(params) + + assert w.min() == 0.0 + assert w.max() == 15 + assert w.dtype == np.float64 + + triggers = np.argwhere(np.diff(w) > 0)[:, 0] + 1 + assert np.all(triggers == times) + assert w.sum() == 15 * len(times) * int(1 / h.dt) + + params["PT"] = 100 + w, maxt, times = stim.make_pulse(params) + triggers = np.argwhere(np.diff(w) > 0)[:, 0] + 1 + assert triggers[-1] - triggers[-2] == 100 / h.dt diff --git a/cnmodel/util/user_tester.py b/cnmodel/util/user_tester.py new file mode 100644 index 0000000..f410f41 --- /dev/null +++ b/cnmodel/util/user_tester.py @@ -0,0 +1,205 @@ +from __future__ import print_function +import os, sys, pickle, pprint +import numpy as np +import pyqtgraph as pg +from .. import AUDIT_TESTS + + +class UserTester(object): + """ + Base class for testing when a human is required to verify the results. + + When a test is passed by the user, its output is saved and used as a basis + for future tests. If future test results do not match the stored results, + then the user is asked to decide whether to fail the test, or pass the + test and store new results. + + Subclasses must reimplement run_test() to return a dictionary of results + to store. Optionally, compare_results and audit_result may also be + reimplemented to customize the testing behavior. + + By default, test results are stored in a 'test_data' directory relative + to the file that defines the UserTester subclass in use. + """ + + data_dir = "test_data" + + def __init__(self, key, *args, **kwds): + """Initialize with a string *key* that provides a short, unique + description of this test. All other arguments are passed to run_test(). + + *key* is used to determine the file name for storing test results. + """ + self.audit = AUDIT_TESTS + self.key = key + self.rtol = 1e-3 + self.args = args + self.assert_test_info(*args, **kwds) + + def run_test(self, *args, **kwds): + """ + Exceute the test. All arguments are taken from __init__. + Return a picklable dictionary of test results. + """ + raise NotImplementedError() + + def compare_results(self, key, info, expect): + """ + Compare *result* of the current test against the previously stored + result *expect*. If *expect* is None, then no previous result was + stored. + + If *result* and *expect* do not match, then raise an exception. + """ + # Check test structures are the same + assert type(info) is type(expect) + if hasattr(info, "__len__"): + assert len(info) == len(expect) + + if isinstance(info, dict): + for k in info: + assert k in expect + for k in expect: + assert k in info + self.compare_results(k, info[k], expect[k]) + elif isinstance(info, list): + for i in range(len(info)): + self.compare_results(key, info[i], expect[i]) + elif isinstance(info, np.ndarray): + assert info.shape == expect.shape + if len(info) == 0: + return + # assert info.dtype == expect.dtype + if info.dtype.fields is None: + intnan = -9223372036854775808 # happens when np.nan is cast to int + inans = np.isnan(info) | (info == intnan) + enans = np.isnan(expect) | (expect == intnan) + assert np.all(inans == enans) + mask = ~inans + if not np.allclose(info[mask], expect[mask], rtol=self.rtol): + print( + "\nComparing data array, shapes match: ", + info.shape == expect.shape, + ) + print("Model tested: %s, measure: %s" % (self.key, key)) + # print( 'args: ', dir(self.args[0])) + print("Array expected: ", expect[mask]) + print("Array received: ", info[mask]) + try: + self.args[0].print_all_mechs() + except: + print("args[0] is string: ", self.args[0]) + assert np.allclose(info[mask], expect[mask], rtol=self.rtol) + else: + for k in info.dtype.fields.keys(): + self.compare_results(k, info[k], expect[k]) + elif np.isscalar(info): + if not np.allclose(info, expect, rtol=self.rtol): + print("Comparing Scalar data, model: %s, measure: %s" % (self.key, key)) + # print 'args: ', dir(self.args[0]) + print( + "Expected: ", + expect, + ", received: ", + info, + " relative tolerance: ", + self.rtol, + ) + if isinstance(self.args[0], str): + pass + # print ': ', str + else: + self.args[0].print_all_mechs() + assert np.allclose(info, expect, rtol=self.rtol) + else: + try: + assert info == expect + except AssertionError: + raise + except Exception: + raise NotImplementedError( + "Cannot compare objects of type %s" % type(info) + ) + + def audit_result(self, info, expect): + """ Display results and ask the user to decide whether the test passed. + Return True for pass, False for fail. + + If *expect* is None, then no previous test results were stored. + """ + app = pg.mkQApp() + print("\n=== New test results for %s: ===\n" % self.key) + pprint.pprint(info) + + # we use DiffTreeWidget to display differences between large data structures, but + # this is not present in mainline pyqtgraph yet + if hasattr(pg, "DiffTreeWidget"): + win = pg.DiffTreeWidget() + else: + from cnmodel.util.difftreewidget import DiffTreeWidget + + win = DiffTreeWidget() + + win.resize(800, 800) + win.setData(expect, info) + win.show() + print("Store new test results? [y/n]") + yn = raw_input() + win.hide() + return yn.lower().startswith("y") + + def assert_test_info(self, *args, **kwds): + """ + Test *cell* and raise exception if the results do not match prior + data. + """ + result = self.run_test(*args, **kwds) + expect = self.load_test_result() + try: + assert expect is not None + self.compare_results(None, result, expect) + except: + if not self.audit: + if expect is None: + raise Exception( + "No prior test results for test '%s'. " + "Run test.py --audit store new test data." % self.key + ) + else: + raise + + store = self.audit_result(result, expect) + if store: + self.save_test_result(result) + else: + raise Exception("Rejected test results for '%s'" % self.key) + + def result_file(self): + """ + Return a file name to be used for storing / retrieving test results + given *self.key*. + """ + modfile = sys.modules[self.__class__.__module__].__file__ + path = os.path.dirname(modfile) + return os.path.join(path, self.data_dir, self.key + ".pk") + + def load_test_result(self): + """ + Load prior test results for *self.key*. + If there are no prior results, return None. + """ + fn = self.result_file() + if os.path.isfile(fn): + return pickle.load(open(fn, "rb"), encoding="latin1") + return None + + def save_test_result(self, result): + """ + Store test results for *self.key*. + Th e*result* argument must be picklable. + """ + fn = self.result_file() + dirname = os.path.dirname(fn) + if not os.path.isdir(dirname): + os.mkdir(dirname) + pickle.dump(result, open(fn, "wb")) diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..1eadde6 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = CNModel +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..b1825ae --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\cnmodel.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\cnmodel.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/doc/numpydoc-0.5/LICENSE.txt b/doc/numpydoc-0.5/LICENSE.txt new file mode 100644 index 0000000..b15c699 --- /dev/null +++ b/doc/numpydoc-0.5/LICENSE.txt @@ -0,0 +1,94 @@ +------------------------------------------------------------------------------- + The files + - numpydoc.py + - docscrape.py + - docscrape_sphinx.py + - phantom_import.py + have the following license: + +Copyright (C) 2008 Stefan van der Walt , Pauli Virtanen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- + The files + - compiler_unparse.py + - comment_eater.py + - traitsdoc.py + have the following license: + +This software is OSI Certified Open Source Software. +OSI Certified is a certification mark of the Open Source Initiative. + +Copyright (c) 2006, Enthought, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Enthought, Inc. nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------------------- + The file + - plot_directive.py + originates from Matplotlib (http://matplotlib.sf.net/) which has + the following license: + +Copyright (c) 2002-2008 John D. Hunter; All Rights Reserved. + +1. This LICENSE AGREEMENT is between John D. Hunter (“JDH”), and the Individual or Organization (“Licensee”) accessing and otherwise using matplotlib software in source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, JDH hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use matplotlib 0.98.3 alone or in any derivative version, provided, however, that JDH’s License Agreement and JDH’s notice of copyright, i.e., “Copyright (c) 2002-2008 John D. Hunter; All Rights Reserved” are retained in matplotlib 0.98.3 alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on or incorporates matplotlib 0.98.3 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to matplotlib 0.98.3. + +4. JDH is making matplotlib 0.98.3 available to Licensee on an “AS IS” basis. JDH MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, JDH MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB 0.98.3 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. JDH SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB 0.98.3 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING MATPLOTLIB 0.98.3, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between JDH and Licensee. This License Agreement does not grant permission to use JDH trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using matplotlib 0.98.3, Licensee agrees to be bound by the terms and conditions of this License Agreement. + diff --git a/doc/numpydoc-0.5/MANIFEST.in b/doc/numpydoc-0.5/MANIFEST.in new file mode 100644 index 0000000..5176d48 --- /dev/null +++ b/doc/numpydoc-0.5/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include numpydoc/tests *.py +include *.txt diff --git a/doc/numpydoc-0.5/PKG-INFO b/doc/numpydoc-0.5/PKG-INFO new file mode 100644 index 0000000..c363407 --- /dev/null +++ b/doc/numpydoc-0.5/PKG-INFO @@ -0,0 +1,16 @@ +Metadata-Version: 1.1 +Name: numpydoc +Version: 0.5 +Summary: Sphinx extension to support docstrings in Numpy format +Home-page: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt +Author: Pauli Virtanen and others +Author-email: pav@iki.fi +License: BSD +Description: UNKNOWN +Keywords: sphinx numpy +Platform: UNKNOWN +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Plugins +Classifier: License :: OSI Approved :: BSD License +Classifier: Topic :: Documentation +Requires: sphinx (>= 1.0.1) diff --git a/doc/numpydoc-0.5/README.rst b/doc/numpydoc-0.5/README.rst new file mode 100644 index 0000000..e2711e1 --- /dev/null +++ b/doc/numpydoc-0.5/README.rst @@ -0,0 +1,57 @@ +.. image:: https://travis-ci.org/numpy/numpydoc.png?branch=master + :target: https://travis-ci.org/numpy/numpydoc/ + +===================================== +numpydoc -- Numpy's Sphinx extensions +===================================== + +Numpy's documentation uses several custom extensions to Sphinx. These +are shipped in this ``numpydoc`` package, in case you want to make use +of them in third-party projects. + +The following extensions are available: + + - ``numpydoc``: support for the Numpy docstring format in Sphinx, and add + the code description directives ``np:function``, ``np-c:function``, etc. + that support the Numpy docstring syntax. + + - ``numpydoc.traitsdoc``: For gathering documentation about Traits attributes. + + - ``numpydoc.plot_directive``: Adaptation of Matplotlib's ``plot::`` + directive. Note that this implementation may still undergo severe + changes or eventually be deprecated. + +See `A Guide to NumPy/SciPy Documentation `_ +for how to write docs that use this extension. + + +numpydoc +======== + +Numpydoc inserts a hook into Sphinx's autodoc that converts docstrings +following the Numpy/Scipy format to a form palatable to Sphinx. + +Options +------- + +The following options can be set in conf.py: + +- numpydoc_use_plots: bool + + Whether to produce ``plot::`` directives for Examples sections that + contain ``import matplotlib``. + +- numpydoc_show_class_members: bool + + Whether to show all members of a class in the Methods and Attributes + sections automatically. + +- numpydoc_class_members_toctree: bool + + Whether to create a Sphinx table of contents for the lists of class + methods and attributes. If a table of contents is made, Sphinx expects + each entry to have a separate page. + +- numpydoc_edit_link: bool (DEPRECATED -- edit your HTML template instead) + + Whether to insert an edit link after docstrings. diff --git a/doc/numpydoc-0.5/numpydoc.egg-info/PKG-INFO b/doc/numpydoc-0.5/numpydoc.egg-info/PKG-INFO new file mode 100644 index 0000000..c363407 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc.egg-info/PKG-INFO @@ -0,0 +1,16 @@ +Metadata-Version: 1.1 +Name: numpydoc +Version: 0.5 +Summary: Sphinx extension to support docstrings in Numpy format +Home-page: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt +Author: Pauli Virtanen and others +Author-email: pav@iki.fi +License: BSD +Description: UNKNOWN +Keywords: sphinx numpy +Platform: UNKNOWN +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Plugins +Classifier: License :: OSI Approved :: BSD License +Classifier: Topic :: Documentation +Requires: sphinx (>= 1.0.1) diff --git a/doc/numpydoc-0.5/numpydoc.egg-info/SOURCES.txt b/doc/numpydoc-0.5/numpydoc.egg-info/SOURCES.txt new file mode 100644 index 0000000..b6246c0 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc.egg-info/SOURCES.txt @@ -0,0 +1,23 @@ +LICENSE.txt +MANIFEST.in +README.rst +setup.py +numpydoc/__init__.py +numpydoc/comment_eater.py +numpydoc/compiler_unparse.py +numpydoc/docscrape.py +numpydoc/docscrape_sphinx.py +numpydoc/linkcode.py +numpydoc/numpydoc.py +numpydoc/phantom_import.py +numpydoc/plot_directive.py +numpydoc/traitsdoc.py +numpydoc.egg-info/PKG-INFO +numpydoc.egg-info/SOURCES.txt +numpydoc.egg-info/dependency_links.txt +numpydoc.egg-info/top_level.txt +numpydoc/tests/test_docscrape.py +numpydoc/tests/test_linkcode.py +numpydoc/tests/test_phantom_import.py +numpydoc/tests/test_plot_directive.py +numpydoc/tests/test_traitsdoc.py \ No newline at end of file diff --git a/doc/numpydoc-0.5/numpydoc.egg-info/dependency_links.txt b/doc/numpydoc-0.5/numpydoc.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/doc/numpydoc-0.5/numpydoc.egg-info/top_level.txt b/doc/numpydoc-0.5/numpydoc.egg-info/top_level.txt new file mode 100644 index 0000000..a2954e3 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc.egg-info/top_level.txt @@ -0,0 +1 @@ +numpydoc diff --git a/doc/numpydoc-0.5/numpydoc/__init__.py b/doc/numpydoc-0.5/numpydoc/__init__.py new file mode 100644 index 0000000..0fce2cf --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/__init__.py @@ -0,0 +1,3 @@ +from __future__ import division, absolute_import, print_function + +from .numpydoc import setup diff --git a/doc/numpydoc-0.5/numpydoc/comment_eater.py b/doc/numpydoc-0.5/numpydoc/comment_eater.py new file mode 100644 index 0000000..f262c2d --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/comment_eater.py @@ -0,0 +1,181 @@ +from __future__ import division, absolute_import, print_function + +import sys + +if sys.version_info[0] >= 3: + from io import StringIO +else: + from io import StringIO + +import compiler +import inspect +import textwrap +import tokenize + +from .compiler_unparse import unparse + + +class Comment(object): + """ A comment block. + """ + + is_comment = True + + def __init__(self, start_lineno, end_lineno, text): + # int : The first line number in the block. 1-indexed. + self.start_lineno = start_lineno + # int : The last line number. Inclusive! + self.end_lineno = end_lineno + # str : The text block including '#' character but not any leading spaces. + self.text = text + + def add(self, string, start, end, line): + """ Add a new comment line. + """ + self.start_lineno = min(self.start_lineno, start[0]) + self.end_lineno = max(self.end_lineno, end[0]) + self.text += string + + def __repr__(self): + return "%s(%r, %r, %r)" % ( + self.__class__.__name__, + self.start_lineno, + self.end_lineno, + self.text, + ) + + +class NonComment(object): + """ A non-comment block of code. + """ + + is_comment = False + + def __init__(self, start_lineno, end_lineno): + self.start_lineno = start_lineno + self.end_lineno = end_lineno + + def add(self, string, start, end, line): + """ Add lines to the block. + """ + if string.strip(): + # Only add if not entirely whitespace. + self.start_lineno = min(self.start_lineno, start[0]) + self.end_lineno = max(self.end_lineno, end[0]) + + def __repr__(self): + return "%s(%r, %r)" % ( + self.__class__.__name__, + self.start_lineno, + self.end_lineno, + ) + + +class CommentBlocker(object): + """ Pull out contiguous comment blocks. + """ + + def __init__(self): + # Start with a dummy. + self.current_block = NonComment(0, 0) + + # All of the blocks seen so far. + self.blocks = [] + + # The index mapping lines of code to their associated comment blocks. + self.index = {} + + def process_file(self, file): + """ Process a file object. + """ + if sys.version_info[0] >= 3: + nxt = file.__next__ + else: + nxt = file.next + for token in tokenize.generate_tokens(nxt): + self.process_token(*token) + self.make_index() + + def process_token(self, kind, string, start, end, line): + """ Process a single token. + """ + if self.current_block.is_comment: + if kind == tokenize.COMMENT: + self.current_block.add(string, start, end, line) + else: + self.new_noncomment(start[0], end[0]) + else: + if kind == tokenize.COMMENT: + self.new_comment(string, start, end, line) + else: + self.current_block.add(string, start, end, line) + + def new_noncomment(self, start_lineno, end_lineno): + """ We are transitioning from a noncomment to a comment. + """ + block = NonComment(start_lineno, end_lineno) + self.blocks.append(block) + self.current_block = block + + def new_comment(self, string, start, end, line): + """ Possibly add a new comment. + + Only adds a new comment if this comment is the only thing on the line. + Otherwise, it extends the noncomment block. + """ + prefix = line[: start[1]] + if prefix.strip(): + # Oops! Trailing comment, not a comment block. + self.current_block.add(string, start, end, line) + else: + # A comment block. + block = Comment(start[0], end[0], string) + self.blocks.append(block) + self.current_block = block + + def make_index(self): + """ Make the index mapping lines of actual code to their associated + prefix comments. + """ + for prev, block in zip(self.blocks[:-1], self.blocks[1:]): + if not block.is_comment: + self.index[block.start_lineno] = prev + + def search_for_comment(self, lineno, default=None): + """ Find the comment block just before the given line number. + + Returns None (or the specified default) if there is no such block. + """ + if not self.index: + self.make_index() + block = self.index.get(lineno, None) + text = getattr(block, "text", default) + return text + + +def strip_comment_marker(text): + """ Strip # markers at the front of a block of comment text. + """ + lines = [] + for line in text.splitlines(): + lines.append(line.lstrip("#")) + text = textwrap.dedent("\n".join(lines)) + return text + + +def get_class_traits(klass): + """ Yield all of the documentation for trait definitions on a class object. + """ + # FIXME: gracefully handle errors here or in the caller? + source = inspect.getsource(klass) + cb = CommentBlocker() + cb.process_file(StringIO(source)) + mod_ast = compiler.parse(source) + class_ast = mod_ast.node.nodes[0] + for node in class_ast.code.nodes: + # FIXME: handle other kinds of assignments? + if isinstance(node, compiler.ast.Assign): + name = node.nodes[0].name + rhs = unparse(node.expr).strip() + doc = strip_comment_marker(cb.search_for_comment(node.lineno, default="")) + yield name, rhs, doc diff --git a/doc/numpydoc-0.5/numpydoc/compiler_unparse.py b/doc/numpydoc-0.5/numpydoc/compiler_unparse.py new file mode 100644 index 0000000..b69c66e --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/compiler_unparse.py @@ -0,0 +1,882 @@ +""" Turn compiler.ast structures back into executable python code. + + The unparse method takes a compiler.ast tree and transforms it back into + valid python code. It is incomplete and currently only works for + import statements, function calls, function definitions, assignments, and + basic expressions. + + Inspired by python-2.5-svn/Demo/parser/unparse.py + + fixme: We may want to move to using _ast trees because the compiler for + them is about 6 times faster than compiler.compile. +""" +from __future__ import division, absolute_import, print_function + +import sys +from compiler.ast import Const, Name, Tuple, Div, Mul, Sub, Add + +if sys.version_info[0] >= 3: + from io import StringIO +else: + from StringIO import StringIO + + +def unparse(ast, single_line_functions=False): + s = StringIO() + UnparseCompilerAst(ast, s, single_line_functions) + return s.getvalue().lstrip() + + +op_precedence = { + "compiler.ast.Power": 3, + "compiler.ast.Mul": 2, + "compiler.ast.Div": 2, + "compiler.ast.Add": 1, + "compiler.ast.Sub": 1, +} + + +class UnparseCompilerAst: + """ Methods in this class recursively traverse an AST and + output source code for the abstract syntax; original formatting + is disregarged. + """ + + ######################################################################### + # object interface. + ######################################################################### + + def __init__(self, tree, file=sys.stdout, single_line_functions=False): + """ Unparser(tree, file=sys.stdout) -> None. + + Print the source for tree to file. + """ + self.f = file + self._single_func = single_line_functions + self._do_indent = True + self._indent = 0 + self._dispatch(tree) + self._write("\n") + self.f.flush() + + ######################################################################### + # Unparser private interface. + ######################################################################### + + ### format, output, and dispatch methods ################################ + + def _fill(self, text=""): + "Indent a piece of text, according to the current indentation level" + if self._do_indent: + self._write("\n" + " " * self._indent + text) + else: + self._write(text) + + def _write(self, text): + "Append a piece of text to the current line." + self.f.write(text) + + def _enter(self): + "Print ':', and increase the indentation." + self._write(": ") + self._indent += 1 + + def _leave(self): + "Decrease the indentation level." + self._indent -= 1 + + def _dispatch(self, tree): + "_dispatcher function, _dispatching tree type T to method _T." + if isinstance(tree, list): + for t in tree: + self._dispatch(t) + return + meth = getattr(self, "_" + tree.__class__.__name__) + if tree.__class__.__name__ == "NoneType" and not self._do_indent: + return + meth(tree) + + ######################################################################### + # compiler.ast unparsing methods. + # + # There should be one method per concrete grammar type. They are + # organized in alphabetical order. + ######################################################################### + + def _Add(self, t): + self.__binary_op(t, "+") + + def _And(self, t): + self._write(" (") + for i, node in enumerate(t.nodes): + self._dispatch(node) + if i != len(t.nodes) - 1: + self._write(") and (") + self._write(")") + + def _AssAttr(self, t): + """ Handle assigning an attribute of an object + """ + self._dispatch(t.expr) + self._write("." + t.attrname) + + def _Assign(self, t): + """ Expression Assignment such as "a = 1". + + This only handles assignment in expressions. Keyword assignment + is handled separately. + """ + self._fill() + for target in t.nodes: + self._dispatch(target) + self._write(" = ") + self._dispatch(t.expr) + if not self._do_indent: + self._write("; ") + + def _AssName(self, t): + """ Name on left hand side of expression. + + Treat just like a name on the right side of an expression. + """ + self._Name(t) + + def _AssTuple(self, t): + """ Tuple on left hand side of an expression. + """ + + # _write each elements, separated by a comma. + for element in t.nodes[:-1]: + self._dispatch(element) + self._write(", ") + + # Handle the last one without writing comma + last_element = t.nodes[-1] + self._dispatch(last_element) + + def _AugAssign(self, t): + """ +=,-=,*=,/=,**=, etc. operations + """ + + self._fill() + self._dispatch(t.node) + self._write(" " + t.op + " ") + self._dispatch(t.expr) + if not self._do_indent: + self._write(";") + + def _Bitand(self, t): + """ Bit and operation. + """ + + for i, node in enumerate(t.nodes): + self._write("(") + self._dispatch(node) + self._write(")") + if i != len(t.nodes) - 1: + self._write(" & ") + + def _Bitor(self, t): + """ Bit or operation + """ + + for i, node in enumerate(t.nodes): + self._write("(") + self._dispatch(node) + self._write(")") + if i != len(t.nodes) - 1: + self._write(" | ") + + def _CallFunc(self, t): + """ Function call. + """ + self._dispatch(t.node) + self._write("(") + comma = False + for e in t.args: + if comma: + self._write(", ") + else: + comma = True + self._dispatch(e) + if t.star_args: + if comma: + self._write(", ") + else: + comma = True + self._write("*") + self._dispatch(t.star_args) + if t.dstar_args: + if comma: + self._write(", ") + else: + comma = True + self._write("**") + self._dispatch(t.dstar_args) + self._write(")") + + def _Compare(self, t): + self._dispatch(t.expr) + for op, expr in t.ops: + self._write(" " + op + " ") + self._dispatch(expr) + + def _Const(self, t): + """ A constant value such as an integer value, 3, or a string, "hello". + """ + self._dispatch(t.value) + + def _Decorators(self, t): + """ Handle function decorators (eg. @has_units) + """ + for node in t.nodes: + self._dispatch(node) + + def _Dict(self, t): + self._write("{") + for i, (k, v) in enumerate(t.items): + self._dispatch(k) + self._write(": ") + self._dispatch(v) + if i < len(t.items) - 1: + self._write(", ") + self._write("}") + + def _Discard(self, t): + """ Node for when return value is ignored such as in "foo(a)". + """ + self._fill() + self._dispatch(t.expr) + + def _Div(self, t): + self.__binary_op(t, "/") + + def _Ellipsis(self, t): + self._write("...") + + def _From(self, t): + """ Handle "from xyz import foo, bar as baz". + """ + # fixme: Are From and ImportFrom handled differently? + self._fill("from ") + self._write(t.modname) + self._write(" import ") + for i, (name, asname) in enumerate(t.names): + if i != 0: + self._write(", ") + self._write(name) + if asname is not None: + self._write(" as " + asname) + + def _Function(self, t): + """ Handle function definitions + """ + if t.decorators is not None: + self._fill("@") + self._dispatch(t.decorators) + self._fill("def " + t.name + "(") + defaults = [None] * (len(t.argnames) - len(t.defaults)) + list(t.defaults) + for i, arg in enumerate(zip(t.argnames, defaults)): + self._write(arg[0]) + if arg[1] is not None: + self._write("=") + self._dispatch(arg[1]) + if i < len(t.argnames) - 1: + self._write(", ") + self._write(")") + if self._single_func: + self._do_indent = False + self._enter() + self._dispatch(t.code) + self._leave() + self._do_indent = True + + def _Getattr(self, t): + """ Handle getting an attribute of an object + """ + if isinstance(t.expr, (Div, Mul, Sub, Add)): + self._write("(") + self._dispatch(t.expr) + self._write(")") + else: + self._dispatch(t.expr) + + self._write("." + t.attrname) + + def _If(self, t): + self._fill() + + for i, (compare, code) in enumerate(t.tests): + if i == 0: + self._write("if ") + else: + self._write("elif ") + self._dispatch(compare) + self._enter() + self._fill() + self._dispatch(code) + self._leave() + self._write("\n") + + if t.else_ is not None: + self._write("else") + self._enter() + self._fill() + self._dispatch(t.else_) + self._leave() + self._write("\n") + + def _IfExp(self, t): + self._dispatch(t.then) + self._write(" if ") + self._dispatch(t.test) + + if t.else_ is not None: + self._write(" else (") + self._dispatch(t.else_) + self._write(")") + + def _Import(self, t): + """ Handle "import xyz.foo". + """ + self._fill("import ") + + for i, (name, asname) in enumerate(t.names): + if i != 0: + self._write(", ") + self._write(name) + if asname is not None: + self._write(" as " + asname) + + def _Keyword(self, t): + """ Keyword value assignment within function calls and definitions. + """ + self._write(t.name) + self._write("=") + self._dispatch(t.expr) + + def _List(self, t): + self._write("[") + for i, node in enumerate(t.nodes): + self._dispatch(node) + if i < len(t.nodes) - 1: + self._write(", ") + self._write("]") + + def _Module(self, t): + if t.doc is not None: + self._dispatch(t.doc) + self._dispatch(t.node) + + def _Mul(self, t): + self.__binary_op(t, "*") + + def _Name(self, t): + self._write(t.name) + + def _NoneType(self, t): + self._write("None") + + def _Not(self, t): + self._write("not (") + self._dispatch(t.expr) + self._write(")") + + def _Or(self, t): + self._write(" (") + for i, node in enumerate(t.nodes): + self._dispatch(node) + if i != len(t.nodes) - 1: + self._write(") or (") + self._write(")") + + def _Pass(self, t): + self._write("pass\n") + + def _Printnl(self, t): + self._fill("print ") + if t.dest: + self._write(">> ") + self._dispatch(t.dest) + self._write(", ") + comma = False + for node in t.nodes: + if comma: + self._write(", ") + else: + comma = True + self._dispatch(node) + + def _Power(self, t): + self.__binary_op(t, "**") + + def _Return(self, t): + self._fill("return ") + if t.value: + if isinstance(t.value, Tuple): + text = ", ".join([name.name for name in t.value.asList()]) + self._write(text) + else: + self._dispatch(t.value) + if not self._do_indent: + self._write("; ") + + def _Slice(self, t): + self._dispatch(t.expr) + self._write("[") + if t.lower: + self._dispatch(t.lower) + self._write(":") + if t.upper: + self._dispatch(t.upper) + # if t.step: + # self._write(":") + # self._dispatch(t.step) + self._write("]") + + def _Sliceobj(self, t): + for i, node in enumerate(t.nodes): + if i != 0: + self._write(":") + if not (isinstance(node, Const) and node.value is None): + self._dispatch(node) + + def _Stmt(self, tree): + for node in tree.nodes: + self._dispatch(node) + + def _Sub(self, t): + self.__binary_op(t, "-") + + def _Subscript(self, t): + self._dispatch(t.expr) + self._write("[") + for i, value in enumerate(t.subs): + if i != 0: + self._write(",") + self._dispatch(value) + self._write("]") + + def _TryExcept(self, t): + self._fill("try") + self._enter() + self._dispatch(t.body) + self._leave() + + for handler in t.handlers: + self._fill("except ") + self._dispatch(handler[0]) + if handler[1] is not None: + self._write(", ") + self._dispatch(handler[1]) + self._enter() + self._dispatch(handler[2]) + self._leave() + + if t.else_: + self._fill("else") + self._enter() + self._dispatch(t.else_) + self._leave() + + def _Tuple(self, t): + + if not t.nodes: + # Empty tuple. + self._write("()") + else: + self._write("(") + + # _write each elements, separated by a comma. + for element in t.nodes[:-1]: + self._dispatch(element) + self._write(", ") + + # Handle the last one without writing comma + last_element = t.nodes[-1] + self._dispatch(last_element) + + self._write(")") + + def _UnaryAdd(self, t): + self._write("+") + self._dispatch(t.expr) + + def _UnarySub(self, t): + self._write("-") + self._dispatch(t.expr) + + def _With(self, t): + self._fill("with ") + self._dispatch(t.expr) + if t.vars: + self._write(" as ") + self._dispatch(t.vars.name) + self._enter() + self._dispatch(t.body) + self._leave() + self._write("\n") + + def _int(self, t): + self._write(repr(t)) + + def __binary_op(self, t, symbol): + # Check if parenthesis are needed on left side and then dispatch + has_paren = False + left_class = str(t.left.__class__) + if ( + left_class in op_precedence.keys() + and op_precedence[left_class] < op_precedence[str(t.__class__)] + ): + has_paren = True + if has_paren: + self._write("(") + self._dispatch(t.left) + if has_paren: + self._write(")") + # Write the appropriate symbol for operator + self._write(symbol) + # Check if parenthesis are needed on the right side and then dispatch + has_paren = False + right_class = str(t.right.__class__) + if ( + right_class in op_precedence.keys() + and op_precedence[right_class] < op_precedence[str(t.__class__)] + ): + has_paren = True + if has_paren: + self._write("(") + self._dispatch(t.right) + if has_paren: + self._write(")") + + def _float(self, t): + # if t is 0.1, str(t)->'0.1' while repr(t)->'0.1000000000001' + # We prefer str here. + self._write(str(t)) + + def _str(self, t): + self._write(repr(t)) + + def _tuple(self, t): + self._write(str(t)) + + ######################################################################### + # These are the methods from the _ast modules unparse. + # + # As our needs to handle more advanced code increase, we may want to + # modify some of the methods below so that they work for compiler.ast. + ######################################################################### + + +# # stmt +# def _Expr(self, tree): +# self._fill() +# self._dispatch(tree.value) +# +# def _Import(self, t): +# self._fill("import ") +# first = True +# for a in t.names: +# if first: +# first = False +# else: +# self._write(", ") +# self._write(a.name) +# if a.asname: +# self._write(" as "+a.asname) +# +## def _ImportFrom(self, t): +## self._fill("from ") +## self._write(t.module) +## self._write(" import ") +## for i, a in enumerate(t.names): +## if i == 0: +## self._write(", ") +## self._write(a.name) +## if a.asname: +## self._write(" as "+a.asname) +## # XXX(jpe) what is level for? +## +# +# def _Break(self, t): +# self._fill("break") +# +# def _Continue(self, t): +# self._fill("continue") +# +# def _Delete(self, t): +# self._fill("del ") +# self._dispatch(t.targets) +# +# def _Assert(self, t): +# self._fill("assert ") +# self._dispatch(t.test) +# if t.msg: +# self._write(", ") +# self._dispatch(t.msg) +# +# def _Exec(self, t): +# self._fill("exec ") +# self._dispatch(t.body) +# if t.globals: +# self._write(" in ") +# self._dispatch(t.globals) +# if t.locals: +# self._write(", ") +# self._dispatch(t.locals) +# +# def _Print(self, t): +# self._fill("print ") +# do_comma = False +# if t.dest: +# self._write(">>") +# self._dispatch(t.dest) +# do_comma = True +# for e in t.values: +# if do_comma:self._write(", ") +# else:do_comma=True +# self._dispatch(e) +# if not t.nl: +# self._write(",") +# +# def _Global(self, t): +# self._fill("global") +# for i, n in enumerate(t.names): +# if i != 0: +# self._write(",") +# self._write(" " + n) +# +# def _Yield(self, t): +# self._fill("yield") +# if t.value: +# self._write(" (") +# self._dispatch(t.value) +# self._write(")") +# +# def _Raise(self, t): +# self._fill('raise ') +# if t.type: +# self._dispatch(t.type) +# if t.inst: +# self._write(", ") +# self._dispatch(t.inst) +# if t.tback: +# self._write(", ") +# self._dispatch(t.tback) +# +# +# def _TryFinally(self, t): +# self._fill("try") +# self._enter() +# self._dispatch(t.body) +# self._leave() +# +# self._fill("finally") +# self._enter() +# self._dispatch(t.finalbody) +# self._leave() +# +# def _excepthandler(self, t): +# self._fill("except ") +# if t.type: +# self._dispatch(t.type) +# if t.name: +# self._write(", ") +# self._dispatch(t.name) +# self._enter() +# self._dispatch(t.body) +# self._leave() +# +# def _ClassDef(self, t): +# self._write("\n") +# self._fill("class "+t.name) +# if t.bases: +# self._write("(") +# for a in t.bases: +# self._dispatch(a) +# self._write(", ") +# self._write(")") +# self._enter() +# self._dispatch(t.body) +# self._leave() +# +# def _FunctionDef(self, t): +# self._write("\n") +# for deco in t.decorators: +# self._fill("@") +# self._dispatch(deco) +# self._fill("def "+t.name + "(") +# self._dispatch(t.args) +# self._write(")") +# self._enter() +# self._dispatch(t.body) +# self._leave() +# +# def _For(self, t): +# self._fill("for ") +# self._dispatch(t.target) +# self._write(" in ") +# self._dispatch(t.iter) +# self._enter() +# self._dispatch(t.body) +# self._leave() +# if t.orelse: +# self._fill("else") +# self._enter() +# self._dispatch(t.orelse) +# self._leave +# +# def _While(self, t): +# self._fill("while ") +# self._dispatch(t.test) +# self._enter() +# self._dispatch(t.body) +# self._leave() +# if t.orelse: +# self._fill("else") +# self._enter() +# self._dispatch(t.orelse) +# self._leave +# +# # expr +# def _Str(self, tree): +# self._write(repr(tree.s)) +## +# def _Repr(self, t): +# self._write("`") +# self._dispatch(t.value) +# self._write("`") +# +# def _Num(self, t): +# self._write(repr(t.n)) +# +# def _ListComp(self, t): +# self._write("[") +# self._dispatch(t.elt) +# for gen in t.generators: +# self._dispatch(gen) +# self._write("]") +# +# def _GeneratorExp(self, t): +# self._write("(") +# self._dispatch(t.elt) +# for gen in t.generators: +# self._dispatch(gen) +# self._write(")") +# +# def _comprehension(self, t): +# self._write(" for ") +# self._dispatch(t.target) +# self._write(" in ") +# self._dispatch(t.iter) +# for if_clause in t.ifs: +# self._write(" if ") +# self._dispatch(if_clause) +# +# def _IfExp(self, t): +# self._dispatch(t.body) +# self._write(" if ") +# self._dispatch(t.test) +# if t.orelse: +# self._write(" else ") +# self._dispatch(t.orelse) +# +# unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} +# def _UnaryOp(self, t): +# self._write(self.unop[t.op.__class__.__name__]) +# self._write("(") +# self._dispatch(t.operand) +# self._write(")") +# +# binop = { "Add":"+", "Sub":"-", "Mult":"*", "Div":"/", "Mod":"%", +# "LShift":">>", "RShift":"<<", "BitOr":"|", "BitXor":"^", "BitAnd":"&", +# "FloorDiv":"//", "Pow": "**"} +# def _BinOp(self, t): +# self._write("(") +# self._dispatch(t.left) +# self._write(")" + self.binop[t.op.__class__.__name__] + "(") +# self._dispatch(t.right) +# self._write(")") +# +# boolops = {_ast.And: 'and', _ast.Or: 'or'} +# def _BoolOp(self, t): +# self._write("(") +# self._dispatch(t.values[0]) +# for v in t.values[1:]: +# self._write(" %s " % self.boolops[t.op.__class__]) +# self._dispatch(v) +# self._write(")") +# +# def _Attribute(self,t): +# self._dispatch(t.value) +# self._write(".") +# self._write(t.attr) +# +## def _Call(self, t): +## self._dispatch(t.func) +## self._write("(") +## comma = False +## for e in t.args: +## if comma: self._write(", ") +## else: comma = True +## self._dispatch(e) +## for e in t.keywords: +## if comma: self._write(", ") +## else: comma = True +## self._dispatch(e) +## if t.starargs: +## if comma: self._write(", ") +## else: comma = True +## self._write("*") +## self._dispatch(t.starargs) +## if t.kwargs: +## if comma: self._write(", ") +## else: comma = True +## self._write("**") +## self._dispatch(t.kwargs) +## self._write(")") +# +# # slice +# def _Index(self, t): +# self._dispatch(t.value) +# +# def _ExtSlice(self, t): +# for i, d in enumerate(t.dims): +# if i != 0: +# self._write(': ') +# self._dispatch(d) +# +# # others +# def _arguments(self, t): +# first = True +# nonDef = len(t.args)-len(t.defaults) +# for a in t.args[0:nonDef]: +# if first:first = False +# else: self._write(", ") +# self._dispatch(a) +# for a,d in zip(t.args[nonDef:], t.defaults): +# if first:first = False +# else: self._write(", ") +# self._dispatch(a), +# self._write("=") +# self._dispatch(d) +# if t.vararg: +# if first:first = False +# else: self._write(", ") +# self._write("*"+t.vararg) +# if t.kwarg: +# if first:first = False +# else: self._write(", ") +# self._write("**"+t.kwarg) +# +## def _keyword(self, t): +## self._write(t.arg) +## self._write("=") +## self._dispatch(t.value) +# +# def _Lambda(self, t): +# self._write("lambda ") +# self._dispatch(t.args) +# self._write(": ") +# self._dispatch(t.body) diff --git a/doc/numpydoc-0.5/numpydoc/docscrape.py b/doc/numpydoc-0.5/numpydoc/docscrape.py new file mode 100644 index 0000000..36328a9 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/docscrape.py @@ -0,0 +1,567 @@ +"""Extract reference documentation from the NumPy source tree. + +""" +from __future__ import division, absolute_import, print_function + +import inspect +import textwrap +import re +import pydoc +from warnings import warn +import collections +import sys + + +class Reader(object): + """A line-based string reader. + + """ + + def __init__(self, data): + """ + Parameters + ---------- + data : str + String with lines separated by '\n'. + + """ + if isinstance(data, list): + self._str = data + else: + self._str = data.split("\n") # store string as list of lines + + self.reset() + + def __getitem__(self, n): + return self._str[n] + + def reset(self): + self._l = 0 # current line nr + + def read(self): + if not self.eof(): + out = self[self._l] + self._l += 1 + return out + else: + return "" + + def seek_next_non_empty_line(self): + for l in self[self._l :]: + if l.strip(): + break + else: + self._l += 1 + + def eof(self): + return self._l >= len(self._str) + + def read_to_condition(self, condition_func): + start = self._l + for line in self[start:]: + if condition_func(line): + return self[start : self._l] + self._l += 1 + if self.eof(): + return self[start : self._l + 1] + return [] + + def read_to_next_empty_line(self): + self.seek_next_non_empty_line() + + def is_empty(line): + return not line.strip() + + return self.read_to_condition(is_empty) + + def read_to_next_unindented_line(self): + def is_unindented(line): + return line.strip() and (len(line.lstrip()) == len(line)) + + return self.read_to_condition(is_unindented) + + def peek(self, n=0): + if self._l + n < len(self._str): + return self[self._l + n] + else: + return "" + + def is_empty(self): + return not "".join(self._str).strip() + + +class NumpyDocString(object): + def __init__(self, docstring, config={}): + docstring = textwrap.dedent(docstring).split("\n") + + self._doc = Reader(docstring) + self._parsed_data = { + "Signature": "", + "Summary": [""], + "Extended Summary": [], + "Parameters": [], + "Returns": [], + "Raises": [], + "Warns": [], + "Other Parameters": [], + "Attributes": [], + "Methods": [], + "See Also": [], + "Notes": [], + "Warnings": [], + "References": "", + "Examples": "", + "index": {}, + } + + self._parse() + + def __getitem__(self, key): + return self._parsed_data[key] + + def __setitem__(self, key, val): + if key not in self._parsed_data: + warn("Unknown section %s" % key) + else: + self._parsed_data[key] = val + + def _is_at_section(self): + self._doc.seek_next_non_empty_line() + + if self._doc.eof(): + return False + + l1 = self._doc.peek().strip() # e.g. Parameters + + if l1.startswith(".. index::"): + return True + + l2 = self._doc.peek(1).strip() # ---------- or ========== + return l2.startswith("-" * len(l1)) or l2.startswith("=" * len(l1)) + + def _strip(self, doc): + i = 0 + j = 0 + for i, line in enumerate(doc): + if line.strip(): + break + + for j, line in enumerate(doc[::-1]): + if line.strip(): + break + + return doc[i : len(doc) - j] + + def _read_to_next_section(self): + section = self._doc.read_to_next_empty_line() + + while not self._is_at_section() and not self._doc.eof(): + if not self._doc.peek(-1).strip(): # previous line was empty + section += [""] + + section += self._doc.read_to_next_empty_line() + + return section + + def _read_sections(self): + while not self._doc.eof(): + data = self._read_to_next_section() + name = data[0].strip() + + if name.startswith(".."): # index section + yield name, data[1:] + elif len(data) < 2: + yield StopIteration + else: + yield name, self._strip(data[2:]) + + def _parse_param_list(self, content): + r = Reader(content) + params = [] + while not r.eof(): + header = r.read().strip() + if " : " in header: + arg_name, arg_type = header.split(" : ")[:2] + else: + arg_name, arg_type = header, "" + + desc = r.read_to_next_unindented_line() + desc = dedent_lines(desc) + + params.append((arg_name, arg_type, desc)) + + return params + + _name_rgx = re.compile( + r"^\s*(:(?P\w+):`(?P[a-zA-Z0-9_.-]+)`|" + r" (?P[a-zA-Z0-9_.-]+))\s*", + re.X, + ) + + def _parse_see_also(self, content): + """ + func_name : Descriptive text + continued text + another_func_name : Descriptive text + func_name1, func_name2, :meth:`func_name`, func_name3 + + """ + items = [] + + def parse_item_name(text): + """Match ':role:`name`' or 'name'""" + m = self._name_rgx.match(text) + if m: + g = m.groups() + if g[1] is None: + return g[3], None + else: + return g[2], g[1] + raise ValueError("%s is not a item name" % text) + + def push_item(name, rest): + if not name: + return + name, role = parse_item_name(name) + items.append((name, list(rest), role)) + del rest[:] + + current_func = None + rest = [] + + for line in content: + if not line.strip(): + continue + + m = self._name_rgx.match(line) + if m and line[m.end() :].strip().startswith(":"): + push_item(current_func, rest) + current_func, line = line[: m.end()], line[m.end() :] + rest = [line.split(":", 1)[1].strip()] + if not rest[0]: + rest = [] + elif not line.startswith(" "): + push_item(current_func, rest) + current_func = None + if "," in line: + for func in line.split(","): + if func.strip(): + push_item(func, []) + elif line.strip(): + current_func = line + elif current_func is not None: + rest.append(line.strip()) + push_item(current_func, rest) + return items + + def _parse_index(self, section, content): + """ + .. index: default + :refguide: something, else, and more + + """ + + def strip_each_in(lst): + return [s.strip() for s in lst] + + out = {} + section = section.split("::") + if len(section) > 1: + out["default"] = strip_each_in(section[1].split(","))[0] + for line in content: + line = line.split(":") + if len(line) > 2: + out[line[1]] = strip_each_in(line[2].split(",")) + return out + + def _parse_summary(self): + """Grab signature (if given) and summary""" + if self._is_at_section(): + return + + # If several signatures present, take the last one + while True: + summary = self._doc.read_to_next_empty_line() + summary_str = " ".join([s.strip() for s in summary]).strip() + if re.compile("^([\w., ]+=)?\s*[\w\.]+\(.*\)$").match(summary_str): + self["Signature"] = summary_str + if not self._is_at_section(): + continue + break + + if summary is not None: + self["Summary"] = summary + + if not self._is_at_section(): + self["Extended Summary"] = self._read_to_next_section() + + def _parse(self): + self._doc.reset() + self._parse_summary() + + for (section, content) in self._read_sections(): + if not section.startswith(".."): + section = " ".join([s.capitalize() for s in section.split(" ")]) + if section in ( + "Parameters", + "Returns", + "Raises", + "Warns", + "Other Parameters", + "Attributes", + "Methods", + ): + self[section] = self._parse_param_list(content) + elif section.startswith(".. index::"): + self["index"] = self._parse_index(section, content) + elif section == "See Also": + self["See Also"] = self._parse_see_also(content) + else: + self[section] = content + + # string conversion routines + + def _str_header(self, name, symbol="-"): + return [name, len(name) * symbol] + + def _str_indent(self, doc, indent=4): + out = [] + for line in doc: + out += [" " * indent + line] + return out + + def _str_signature(self): + if self["Signature"]: + return [self["Signature"].replace("*", "\*")] + [""] + else: + return [""] + + def _str_summary(self): + if self["Summary"]: + return self["Summary"] + [""] + else: + return [] + + def _str_extended_summary(self): + if self["Extended Summary"]: + return self["Extended Summary"] + [""] + else: + return [] + + def _str_param_list(self, name): + out = [] + if self[name]: + out += self._str_header(name) + for param, param_type, desc in self[name]: + if param_type: + out += ["%s : %s" % (param, param_type)] + else: + out += [param] + out += self._str_indent(desc) + out += [""] + return out + + def _str_section(self, name): + out = [] + if self[name]: + out += self._str_header(name) + out += self[name] + out += [""] + return out + + def _str_see_also(self, func_role): + if not self["See Also"]: + return [] + out = [] + out += self._str_header("See Also") + last_had_desc = True + for func, desc, role in self["See Also"]: + if role: + link = ":%s:`%s`" % (role, func) + elif func_role: + link = ":%s:`%s`" % (func_role, func) + else: + link = "`%s`_" % func + if desc or last_had_desc: + out += [""] + out += [link] + else: + out[-1] += ", %s" % link + if desc: + out += self._str_indent([" ".join(desc)]) + last_had_desc = True + else: + last_had_desc = False + out += [""] + return out + + def _str_index(self): + idx = self["index"] + out = [] + out += [".. index:: %s" % idx.get("default", "")] + for section, references in idx.items(): + if section == "default": + continue + out += [" :%s: %s" % (section, ", ".join(references))] + return out + + def __str__(self, func_role=""): + out = [] + out += self._str_signature() + out += self._str_summary() + out += self._str_extended_summary() + for param_list in ( + "Parameters", + "Returns", + "Other Parameters", + "Raises", + "Warns", + ): + out += self._str_param_list(param_list) + out += self._str_section("Warnings") + out += self._str_see_also(func_role) + for s in ("Notes", "References", "Examples"): + out += self._str_section(s) + for param_list in ("Attributes", "Methods"): + out += self._str_param_list(param_list) + out += self._str_index() + return "\n".join(out) + + +def indent(str, indent=4): + indent_str = " " * indent + if str is None: + return indent_str + lines = str.split("\n") + return "\n".join(indent_str + l for l in lines) + + +def dedent_lines(lines): + """Deindent a list of lines maximally""" + return textwrap.dedent("\n".join(lines)).split("\n") + + +def header(text, style="-"): + return text + "\n" + style * len(text) + "\n" + + +class FunctionDoc(NumpyDocString): + def __init__(self, func, role="func", doc=None, config={}): + self._f = func + self._role = role # e.g. "func" or "meth" + + if doc is None: + if func is None: + raise ValueError("No function or docstring given") + doc = inspect.getdoc(func) or "" + NumpyDocString.__init__(self, doc) + + if not self["Signature"] and func is not None: + func, func_name = self.get_func() + try: + # try to read signature + if sys.version_info[0] >= 3: + argspec = inspect.getfullargspec(func) + else: + argspec = inspect.getargspec(func) + argspec = inspect.formatargspec(*argspec) + argspec = argspec.replace("*", "\*") + signature = "%s%s" % (func_name, argspec) + except TypeError as e: + signature = "%s()" % func_name + self["Signature"] = signature + + def get_func(self): + func_name = getattr(self._f, "__name__", self.__class__.__name__) + if inspect.isclass(self._f): + func = getattr(self._f, "__call__", self._f.__init__) + else: + func = self._f + return func, func_name + + def __str__(self): + out = "" + + func, func_name = self.get_func() + signature = self["Signature"].replace("*", "\*") + + roles = {"func": "function", "meth": "method"} + + if self._role: + if self._role not in roles: + print("Warning: invalid role %s" % self._role) + out += ".. %s:: %s\n \n\n" % (roles.get(self._role, ""), func_name) + + out += super(FunctionDoc, self).__str__(func_role=self._role) + return out + + +class ClassDoc(NumpyDocString): + + extra_public_methods = ["__call__"] + + def __init__(self, cls, doc=None, modulename="", func_doc=FunctionDoc, config={}): + if not inspect.isclass(cls) and cls is not None: + raise ValueError("Expected a class or None, but got %r" % cls) + self._cls = cls + + if modulename and not modulename.endswith("."): + modulename += "." + self._mod = modulename + + if doc is None: + if cls is None: + raise ValueError("No class or documentation string given") + doc = pydoc.getdoc(cls) + + NumpyDocString.__init__(self, doc) + + if config.get("show_class_members", True): + + def splitlines_x(s): + if not s: + return [] + else: + return s.splitlines() + + for field, items in [ + ("Methods", self.methods), + ("Attributes", self.properties), + ]: + if not self[field]: + doc_list = [] + for name in sorted(items): + try: + doc_item = pydoc.getdoc(getattr(self._cls, name)) + doc_list.append((name, "", splitlines_x(doc_item))) + except AttributeError: + pass # method doesn't exist + self[field] = doc_list + + @property + def methods(self): + if self._cls is None: + return [] + return [ + name + for name, func in inspect.getmembers(self._cls) + if ( + (not name.startswith("_") or name in self.extra_public_methods) + and isinstance(func, collections.Callable) + ) + ] + + @property + def properties(self): + if self._cls is None: + return [] + return [ + name + for name, func in inspect.getmembers(self._cls) + if not name.startswith("_") + and ( + func is None + or isinstance(func, property) + or inspect.isgetsetdescriptor(func) + ) + ] diff --git a/doc/numpydoc-0.5/numpydoc/docscrape_sphinx.py b/doc/numpydoc-0.5/numpydoc/docscrape_sphinx.py new file mode 100644 index 0000000..9e2317e --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/docscrape_sphinx.py @@ -0,0 +1,284 @@ +from __future__ import division, absolute_import, print_function + +import sys, re, inspect, textwrap, pydoc +import sphinx +import collections +from .docscrape import NumpyDocString, FunctionDoc, ClassDoc + +if sys.version_info[0] >= 3: + sixu = lambda s: s +else: + sixu = lambda s: unicode(s, "unicode_escape") + + +class SphinxDocString(NumpyDocString): + def __init__(self, docstring, config={}): + NumpyDocString.__init__(self, docstring, config=config) + self.load_config(config) + + def load_config(self, config): + self.use_plots = config.get("use_plots", False) + self.class_members_toctree = config.get("class_members_toctree", True) + + # string conversion routines + def _str_header(self, name, symbol="`"): + return [".. rubric:: " + name, ""] + + def _str_field_list(self, name): + return [":" + name + ":"] + + def _str_indent(self, doc, indent=4): + out = [] + for line in doc: + out += [" " * indent + line] + return out + + def _str_signature(self): + return [""] + if self["Signature"]: + return ["``%s``" % self["Signature"]] + [""] + else: + return [""] + + def _str_summary(self): + return self["Summary"] + [""] + + def _str_extended_summary(self): + return self["Extended Summary"] + [""] + + def _str_returns(self): + out = [] + if self["Returns"]: + out += self._str_field_list("Returns") + out += [""] + for param, param_type, desc in self["Returns"]: + if param_type: + out += self._str_indent( + ["**%s** : %s" % (param.strip(), param_type)] + ) + else: + out += self._str_indent([param.strip()]) + if desc: + out += [""] + out += self._str_indent(desc, 8) + out += [""] + return out + + def _str_param_list(self, name): + out = [] + if self[name]: + out += self._str_field_list(name) + out += [""] + for param, param_type, desc in self[name]: + if param_type: + out += self._str_indent( + ["**%s** : %s" % (param.strip(), param_type)] + ) + else: + out += self._str_indent(["**%s**" % param.strip()]) + if desc: + out += [""] + out += self._str_indent(desc, 8) + out += [""] + return out + + @property + def _obj(self): + if hasattr(self, "_cls"): + return self._cls + elif hasattr(self, "_f"): + return self._f + return None + + def _str_member_list(self, name): + """ + Generate a member listing, autosummary:: table where possible, + and a table where not. + + """ + out = [] + if self[name]: + out += [".. rubric:: %s" % name, ""] + prefix = getattr(self, "_name", "") + + if prefix: + prefix = "~%s." % prefix + + autosum = [] + others = [] + for param, param_type, desc in self[name]: + param = param.strip() + + # Check if the referenced member can have a docstring or not + param_obj = getattr(self._obj, param, None) + if not ( + callable(param_obj) + or isinstance(param_obj, property) + or inspect.isgetsetdescriptor(param_obj) + ): + param_obj = None + + if param_obj and (pydoc.getdoc(param_obj) or not desc): + # Referenced object has a docstring + autosum += [" %s%s" % (prefix, param)] + else: + others.append((param, param_type, desc)) + + if autosum: + out += [".. autosummary::"] + if self.class_members_toctree: + out += [" :toctree:"] + out += [""] + autosum + + if others: + maxlen_0 = max(3, max([len(x[0]) for x in others])) + hdr = sixu("=") * maxlen_0 + sixu(" ") + sixu("=") * 10 + fmt = sixu("%%%ds %%s ") % (maxlen_0,) + out += ["", hdr] + for param, param_type, desc in others: + desc = sixu(" ").join(x.strip() for x in desc).strip() + if param_type: + desc = "(%s) %s" % (param_type, desc) + out += [fmt % (param.strip(), desc)] + out += [hdr] + out += [""] + return out + + def _str_section(self, name): + out = [] + if self[name]: + out += self._str_header(name) + out += [""] + content = textwrap.dedent("\n".join(self[name])).split("\n") + out += content + out += [""] + return out + + def _str_see_also(self, func_role): + out = [] + if self["See Also"]: + see_also = super(SphinxDocString, self)._str_see_also(func_role) + out = [".. seealso::", ""] + out += self._str_indent(see_also[2:]) + return out + + def _str_warnings(self): + out = [] + if self["Warnings"]: + out = [".. warning::", ""] + out += self._str_indent(self["Warnings"]) + return out + + def _str_index(self): + idx = self["index"] + out = [] + if len(idx) == 0: + return out + + out += [".. index:: %s" % idx.get("default", "")] + for section, references in idx.items(): + if section == "default": + continue + elif section == "refguide": + out += [" single: %s" % (", ".join(references))] + else: + out += [" %s: %s" % (section, ",".join(references))] + return out + + def _str_references(self): + out = [] + if self["References"]: + out += self._str_header("References") + if isinstance(self["References"], str): + self["References"] = [self["References"]] + out.extend(self["References"]) + out += [""] + # Latex collects all references to a separate bibliography, + # so we need to insert links to it + if sphinx.__version__ >= "0.6": + out += [".. only:: latex", ""] + else: + out += [".. latexonly::", ""] + items = [] + for line in self["References"]: + m = re.match(r".. \[([a-z0-9._-]+)\]", line, re.I) + if m: + items.append(m.group(1)) + out += [" " + ", ".join(["[%s]_" % item for item in items]), ""] + return out + + def _str_examples(self): + examples_str = "\n".join(self["Examples"]) + + if ( + self.use_plots + and "import matplotlib" in examples_str + and "plot::" not in examples_str + ): + out = [] + out += self._str_header("Examples") + out += [".. plot::", ""] + out += self._str_indent(self["Examples"]) + out += [""] + return out + else: + return self._str_section("Examples") + + def __str__(self, indent=0, func_role="obj"): + out = [] + out += self._str_signature() + out += self._str_index() + [""] + out += self._str_summary() + out += self._str_extended_summary() + out += self._str_param_list("Parameters") + out += self._str_returns() + for param_list in ("Other Parameters", "Raises", "Warns"): + out += self._str_param_list(param_list) + out += self._str_warnings() + out += self._str_see_also(func_role) + out += self._str_section("Notes") + out += self._str_references() + out += self._str_examples() + for param_list in ("Attributes", "Methods"): + out += self._str_member_list(param_list) + out = self._str_indent(out, indent) + return "\n".join(out) + + +class SphinxFunctionDoc(SphinxDocString, FunctionDoc): + def __init__(self, obj, doc=None, config={}): + self.load_config(config) + FunctionDoc.__init__(self, obj, doc=doc, config=config) + + +class SphinxClassDoc(SphinxDocString, ClassDoc): + def __init__(self, obj, doc=None, func_doc=None, config={}): + self.load_config(config) + ClassDoc.__init__(self, obj, doc=doc, func_doc=None, config=config) + + +class SphinxObjDoc(SphinxDocString): + def __init__(self, obj, doc=None, config={}): + self._f = obj + self.load_config(config) + SphinxDocString.__init__(self, doc, config=config) + + +def get_doc_object(obj, what=None, doc=None, config={}): + if what is None: + if inspect.isclass(obj): + what = "class" + elif inspect.ismodule(obj): + what = "module" + elif isinstance(obj, collections.Callable): + what = "function" + else: + what = "object" + if what == "class": + return SphinxClassDoc(obj, func_doc=SphinxFunctionDoc, doc=doc, config=config) + elif what in ("function", "method"): + return SphinxFunctionDoc(obj, doc=doc, config=config) + else: + if doc is None: + doc = pydoc.getdoc(obj) + return SphinxObjDoc(obj, doc, config=config) diff --git a/doc/numpydoc-0.5/numpydoc/linkcode.py b/doc/numpydoc-0.5/numpydoc/linkcode.py new file mode 100644 index 0000000..6845720 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/linkcode.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +""" + linkcode + ~~~~~~~~ + + Add external links to module code in Python object descriptions. + + :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. + +""" +from __future__ import division, absolute_import, print_function + +import warnings +import collections + +warnings.warn( + "This extension has been accepted to Sphinx upstream. " + "Use the version from there (Sphinx >= 1.2) " + "https://bitbucket.org/birkenfeld/sphinx/pull-request/47/sphinxextlinkcode", + FutureWarning, + stacklevel=1, +) + + +from docutils import nodes + +from sphinx import addnodes +from sphinx.locale import _ +from sphinx.errors import SphinxError + + +class LinkcodeError(SphinxError): + category = "linkcode error" + + +def doctree_read(app, doctree): + env = app.builder.env + + resolve_target = getattr(env.config, "linkcode_resolve", None) + if not isinstance(env.config.linkcode_resolve, collections.Callable): + raise LinkcodeError("Function `linkcode_resolve` is not given in conf.py") + + domain_keys = dict( + py=["module", "fullname"], c=["names"], cpp=["names"], js=["object", "fullname"] + ) + + for objnode in doctree.traverse(addnodes.desc): + domain = objnode.get("domain") + uris = set() + for signode in objnode: + if not isinstance(signode, addnodes.desc_signature): + continue + + # Convert signode to a specified format + info = {} + for key in domain_keys.get(domain, []): + value = signode.get(key) + if not value: + value = "" + info[key] = value + if not info: + continue + + # Call user code to resolve the link + uri = resolve_target(domain, info) + if not uri: + # no source + continue + + if uri in uris or not uri: + # only one link per name, please + continue + uris.add(uri) + + onlynode = addnodes.only(expr="html") + onlynode += nodes.reference("", "", internal=False, refuri=uri) + onlynode[0] += nodes.inline("", _("[source]"), classes=["viewcode-link"]) + signode += onlynode + + +def setup(app): + app.connect("doctree-read", doctree_read) + app.add_config_value("linkcode_resolve", None, "") diff --git a/doc/numpydoc-0.5/numpydoc/numpydoc.py b/doc/numpydoc-0.5/numpydoc/numpydoc.py new file mode 100644 index 0000000..81a4328 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/numpydoc.py @@ -0,0 +1,201 @@ +""" +======== +numpydoc +======== + +Sphinx extension that handles docstrings in the Numpy standard format. [1] + +It will: + +- Convert Parameters etc. sections to field lists. +- Convert See Also section to a See also entry. +- Renumber references. +- Extract the signature from the docstring, if it can't be determined otherwise. + +.. [1] https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt + +""" +from __future__ import division, absolute_import, print_function + +import os, sys, re, pydoc +import sphinx +import inspect +import collections + +if sphinx.__version__ < "1.0.1": + raise RuntimeError("Sphinx 1.0.1 or newer is required") + +from .docscrape_sphinx import get_doc_object, SphinxDocString +from sphinx.util.compat import Directive + +if sys.version_info[0] >= 3: + sixu = lambda s: s +else: + sixu = lambda s: unicode(s, "unicode_escape") + + +def mangle_docstrings(app, what, name, obj, options, lines, reference_offset=[0]): + + cfg = dict( + use_plots=app.config.numpydoc_use_plots, + show_class_members=app.config.numpydoc_show_class_members, + class_members_toctree=app.config.numpydoc_class_members_toctree, + ) + + if what == "module": + # Strip top title + title_re = re.compile( + sixu("^\\s*[#*=]{4,}\\n[a-z0-9 -]+\\n[#*=]{4,}\\s*"), re.I | re.S + ) + lines[:] = title_re.sub(sixu(""), sixu("\n").join(lines)).split(sixu("\n")) + else: + doc = get_doc_object(obj, what, sixu("\n").join(lines), config=cfg) + if sys.version_info[0] >= 3: + doc = str(doc) + else: + doc = unicode(doc) + lines[:] = doc.split(sixu("\n")) + + if app.config.numpydoc_edit_link and hasattr(obj, "__name__") and obj.__name__: + if hasattr(obj, "__module__"): + v = dict(full_name=sixu("%s.%s") % (obj.__module__, obj.__name__)) + else: + v = dict(full_name=obj.__name__) + lines += [sixu(""), sixu(".. htmlonly::"), sixu("")] + lines += [ + sixu(" %s") % x for x in (app.config.numpydoc_edit_link % v).split("\n") + ] + + # replace reference numbers so that there are no duplicates + references = [] + for line in lines: + line = line.strip() + m = re.match(sixu("^.. \\[([a-z0-9_.-])\\]"), line, re.I) + if m: + references.append(m.group(1)) + + # start renaming from the longest string, to avoid overwriting parts + references.sort(key=lambda x: -len(x)) + if references: + for i, line in enumerate(lines): + for r in references: + if re.match(sixu("^\\d+$"), r): + new_r = sixu("R%d") % (reference_offset[0] + int(r)) + else: + new_r = sixu("%s%d") % (r, reference_offset[0]) + lines[i] = lines[i].replace(sixu("[%s]_") % r, sixu("[%s]_") % new_r) + lines[i] = lines[i].replace( + sixu(".. [%s]") % r, sixu(".. [%s]") % new_r + ) + + reference_offset[0] += len(references) + + +def mangle_signature(app, what, name, obj, options, sig, retann): + # Do not try to inspect classes that don't define `__init__` + if inspect.isclass(obj) and ( + not hasattr(obj, "__init__") + or "initializes x; see " in pydoc.getdoc(obj.__init__) + ): + return "", "" + + if not ( + isinstance(obj, collections.Callable) or hasattr(obj, "__argspec_is_invalid_") + ): + return + if not hasattr(obj, "__doc__"): + return + + doc = SphinxDocString(pydoc.getdoc(obj)) + if doc["Signature"]: + sig = re.sub(sixu("^[^(]*"), sixu(""), doc["Signature"]) + return sig, sixu("") + + +def setup(app, get_doc_object_=get_doc_object): + if not hasattr(app, "add_config_value"): + return # probably called by nose, better bail out + + global get_doc_object + get_doc_object = get_doc_object_ + + app.connect("autodoc-process-docstring", mangle_docstrings) + app.connect("autodoc-process-signature", mangle_signature) + app.add_config_value("numpydoc_edit_link", None, False) + app.add_config_value("numpydoc_use_plots", None, False) + app.add_config_value("numpydoc_show_class_members", True, True) + app.add_config_value("numpydoc_class_members_toctree", True, True) + + # Extra mangling domains + app.add_domain(NumpyPythonDomain) + app.add_domain(NumpyCDomain) + + +# ------------------------------------------------------------------------------ +# Docstring-mangling domains +# ------------------------------------------------------------------------------ + +from docutils.statemachine import ViewList +from sphinx.domains.c import CDomain +from sphinx.domains.python import PythonDomain + + +class ManglingDomainBase(object): + directive_mangling_map = {} + + def __init__(self, *a, **kw): + super(ManglingDomainBase, self).__init__(*a, **kw) + self.wrap_mangling_directives() + + def wrap_mangling_directives(self): + for name, objtype in list(self.directive_mangling_map.items()): + self.directives[name] = wrap_mangling_directive( + self.directives[name], objtype + ) + + +class NumpyPythonDomain(ManglingDomainBase, PythonDomain): + name = "np" + directive_mangling_map = { + "function": "function", + "class": "class", + "exception": "class", + "method": "function", + "classmethod": "function", + "staticmethod": "function", + "attribute": "attribute", + } + indices = [] + + +class NumpyCDomain(ManglingDomainBase, CDomain): + name = "np-c" + directive_mangling_map = { + "function": "function", + "member": "attribute", + "macro": "function", + "type": "class", + "var": "object", + } + + +def wrap_mangling_directive(base_directive, objtype): + class directive(base_directive): + def run(self): + env = self.state.document.settings.env + + name = None + if self.arguments: + m = re.match(r"^(.*\s+)?(.*?)(\(.*)?", self.arguments[0]) + name = m.group(2).strip() + + if not name: + name = self.arguments[0] + + lines = list(self.content) + mangle_docstrings(env.app, objtype, name, None, None, lines) + self.content = ViewList(lines, self.content.parent) + + return base_directive.run(self) + + return directive diff --git a/doc/numpydoc-0.5/numpydoc/phantom_import.py b/doc/numpydoc-0.5/numpydoc/phantom_import.py new file mode 100644 index 0000000..7ef99c3 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/phantom_import.py @@ -0,0 +1,181 @@ +""" +============== +phantom_import +============== + +Sphinx extension to make directives from ``sphinx.ext.autodoc`` and similar +extensions to use docstrings loaded from an XML file. + +This extension loads an XML file in the Pydocweb format [1] and +creates a dummy module that contains the specified docstrings. This +can be used to get the current docstrings from a Pydocweb instance +without needing to rebuild the documented module. + +.. [1] http://code.google.com/p/pydocweb + +""" +from __future__ import division, absolute_import, print_function + +import imp, sys, compiler, types, os, inspect, re + + +def setup(app): + app.connect("builder-inited", initialize) + app.add_config_value("phantom_import_file", None, True) + + +def initialize(app): + fn = app.config.phantom_import_file + if fn and os.path.isfile(fn): + print("[numpydoc] Phantom importing modules from", fn, "...") + import_phantom_module(fn) + + +# ------------------------------------------------------------------------------ +# Creating 'phantom' modules from an XML description +# ------------------------------------------------------------------------------ +def import_phantom_module(xml_file): + """ + Insert a fake Python module to sys.modules, based on a XML file. + + The XML file is expected to conform to Pydocweb DTD. The fake + module will contain dummy objects, which guarantee the following: + + - Docstrings are correct. + - Class inheritance relationships are correct (if present in XML). + - Function argspec is *NOT* correct (even if present in XML). + Instead, the function signature is prepended to the function docstring. + - Class attributes are *NOT* correct; instead, they are dummy objects. + + Parameters + ---------- + xml_file : str + Name of an XML file to read + + """ + import lxml.etree as etree + + object_cache = {} + + tree = etree.parse(xml_file) + root = tree.getroot() + + # Sort items so that + # - Base classes come before classes inherited from them + # - Modules come before their contents + all_nodes = dict([(n.attrib["id"], n) for n in root]) + + def _get_bases(node, recurse=False): + bases = [x.attrib["ref"] for x in node.findall("base")] + if recurse: + j = 0 + while True: + try: + b = bases[j] + except IndexError: + break + if b in all_nodes: + bases.extend(_get_bases(all_nodes[b])) + j += 1 + return bases + + type_index = ["module", "class", "callable", "object"] + + def base_cmp(a, b): + x = cmp(type_index.index(a.tag), type_index.index(b.tag)) + if x != 0: + return x + + if a.tag == "class" and b.tag == "class": + a_bases = _get_bases(a, recurse=True) + b_bases = _get_bases(b, recurse=True) + x = cmp(len(a_bases), len(b_bases)) + if x != 0: + return x + if a.attrib["id"] in b_bases: + return -1 + if b.attrib["id"] in a_bases: + return 1 + + return cmp(a.attrib["id"].count("."), b.attrib["id"].count(".")) + + nodes = root.getchildren() + nodes.sort(base_cmp) + + # Create phantom items + for node in nodes: + name = node.attrib["id"] + doc = (node.text or "").decode("string-escape") + "\n" + if doc == "\n": + doc = "" + + # create parent, if missing + parent = name + while True: + parent = ".".join(parent.split(".")[:-1]) + if not parent: + break + if parent in object_cache: + break + obj = imp.new_module(parent) + object_cache[parent] = obj + sys.modules[parent] = obj + + # create object + if node.tag == "module": + obj = imp.new_module(name) + obj.__doc__ = doc + sys.modules[name] = obj + elif node.tag == "class": + bases = [object_cache[b] for b in _get_bases(node) if b in object_cache] + bases.append(object) + init = lambda self: None + init.__doc__ = doc + obj = type(name, tuple(bases), {"__doc__": doc, "__init__": init}) + obj.__name__ = name.split(".")[-1] + elif node.tag == "callable": + funcname = node.attrib["id"].split(".")[-1] + argspec = node.attrib.get("argspec") + if argspec: + argspec = re.sub("^[^(]*", "", argspec) + doc = "%s%s\n\n%s" % (funcname, argspec, doc) + obj = lambda: 0 + obj.__argspec_is_invalid_ = True + if sys.version_info[0] >= 3: + obj.__name__ = funcname + else: + obj.func_name = funcname + obj.__name__ = name + obj.__doc__ = doc + if inspect.isclass(object_cache[parent]): + obj.__objclass__ = object_cache[parent] + else: + + class Dummy(object): + pass + + obj = Dummy() + obj.__name__ = name + obj.__doc__ = doc + if inspect.isclass(object_cache[parent]): + obj.__get__ = lambda: None + object_cache[name] = obj + + if parent: + if inspect.ismodule(object_cache[parent]): + obj.__module__ = parent + setattr(object_cache[parent], name.split(".")[-1], obj) + + # Populate items + for node in root: + obj = object_cache.get(node.attrib["id"]) + if obj is None: + continue + for ref in node.findall("ref"): + if node.tag == "class": + if ref.attrib["ref"].startswith(node.attrib["id"] + "."): + setattr( + obj, ref.attrib["name"], object_cache.get(ref.attrib["ref"]) + ) + else: + setattr(obj, ref.attrib["name"], object_cache.get(ref.attrib["ref"])) diff --git a/doc/numpydoc-0.5/numpydoc/plot_directive.py b/doc/numpydoc-0.5/numpydoc/plot_directive.py new file mode 100644 index 0000000..2bb8797 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/plot_directive.py @@ -0,0 +1,697 @@ +""" +A special directive for generating a matplotlib plot. + +.. warning:: + + This is a hacked version of plot_directive.py from Matplotlib. + It's very much subject to change! + + +Usage +----- + +Can be used like this:: + + .. plot:: examples/example.py + + .. plot:: + + import matplotlib.pyplot as plt + plt.plot([1,2,3], [4,5,6]) + + .. plot:: + + A plotting example: + + >>> import matplotlib.pyplot as plt + >>> plt.plot([1,2,3], [4,5,6]) + +The content is interpreted as doctest formatted if it has a line starting +with ``>>>``. + +The ``plot`` directive supports the options + + format : {'python', 'doctest'} + Specify the format of the input + + include-source : bool + Whether to display the source code. Default can be changed in conf.py + +and the ``image`` directive options ``alt``, ``height``, ``width``, +``scale``, ``align``, ``class``. + +Configuration options +--------------------- + +The plot directive has the following configuration options: + + plot_include_source + Default value for the include-source option + + plot_pre_code + Code that should be executed before each plot. + + plot_basedir + Base directory, to which plot:: file names are relative to. + (If None or empty, file names are relative to the directoly where + the file containing the directive is.) + + plot_formats + File formats to generate. List of tuples or strings:: + + [(suffix, dpi), suffix, ...] + + that determine the file format and the DPI. For entries whose + DPI was omitted, sensible defaults are chosen. + + plot_html_show_formats + Whether to show links to the files in HTML. + +TODO +---- + +* Refactor Latex output; now it's plain images, but it would be nice + to make them appear side-by-side, or in floats. + +""" +from __future__ import division, absolute_import, print_function + +import sys, os, glob, shutil, imp, warnings, re, textwrap, traceback +import sphinx + +if sys.version_info[0] >= 3: + from io import StringIO +else: + from io import StringIO + +import warnings + +warnings.warn( + "A plot_directive module is also available under " + "matplotlib.sphinxext; expect this numpydoc.plot_directive " + "module to be deprecated after relevant features have been " + "integrated there.", + FutureWarning, + stacklevel=2, +) + + +# ------------------------------------------------------------------------------ +# Registration hook +# ------------------------------------------------------------------------------ + + +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir + + app.add_config_value("plot_pre_code", "", True) + app.add_config_value("plot_include_source", False, True) + app.add_config_value("plot_formats", ["png", "hires.png", "pdf"], True) + app.add_config_value("plot_basedir", None, True) + app.add_config_value("plot_html_show_formats", True, True) + + app.add_directive( + "plot", plot_directive, True, (0, 1, False), **plot_directive_options + ) + + +# ------------------------------------------------------------------------------ +# plot:: directive +# ------------------------------------------------------------------------------ +from docutils.parsers.rst import directives +from docutils import nodes + + +def plot_directive( + name, + arguments, + options, + content, + lineno, + content_offset, + block_text, + state, + state_machine, +): + return run(arguments, content, options, state_machine, state, lineno) + + +plot_directive.__doc__ = __doc__ + + +def _option_boolean(arg): + if not arg or not arg.strip(): + # no argument given, assume used as a flag + return True + elif arg.strip().lower() in ("no", "0", "false"): + return False + elif arg.strip().lower() in ("yes", "1", "true"): + return True + else: + raise ValueError('"%s" unknown boolean' % arg) + + +def _option_format(arg): + return directives.choice(arg, ("python", "lisp")) + + +def _option_align(arg): + return directives.choice( + arg, ("top", "middle", "bottom", "left", "center", "right") + ) + + +plot_directive_options = { + "alt": directives.unchanged, + "height": directives.length_or_unitless, + "width": directives.length_or_percentage_or_unitless, + "scale": directives.nonnegative_int, + "align": _option_align, + "class": directives.class_option, + "include-source": _option_boolean, + "format": _option_format, +} + +# ------------------------------------------------------------------------------ +# Generating output +# ------------------------------------------------------------------------------ + +from docutils import nodes, utils + +try: + # Sphinx depends on either Jinja or Jinja2 + import jinja2 + + def format_template(template, **kw): + return jinja2.Template(template).render(**kw) + + +except ImportError: + import jinja + + def format_template(template, **kw): + return jinja.from_string(template, **kw) + + +TEMPLATE = """ +{{ source_code }} + +{{ only_html }} + + {% if source_link or (html_show_formats and not multi_image) %} + ( + {%- if source_link -%} + `Source code <{{ source_link }}>`__ + {%- endif -%} + {%- if html_show_formats and not multi_image -%} + {%- for img in images -%} + {%- for fmt in img.formats -%} + {%- if source_link or not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + {%- endfor -%} + {%- endif -%} + ) + {% endif %} + + {% for img in images %} + .. figure:: {{ build_dir }}/{{ img.basename }}.png + {%- for option in options %} + {{ option }} + {% endfor %} + + {% if html_show_formats and multi_image -%} + ( + {%- for fmt in img.formats -%} + {%- if not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + ) + {%- endif -%} + {% endfor %} + +{{ only_latex }} + + {% for img in images %} + .. image:: {{ build_dir }}/{{ img.basename }}.pdf + {% endfor %} + +""" + + +class ImageFile(object): + def __init__(self, basename, dirname): + self.basename = basename + self.dirname = dirname + self.formats = [] + + def filename(self, format): + return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) + + def filenames(self): + return [self.filename(fmt) for fmt in self.formats] + + +def run(arguments, content, options, state_machine, state, lineno): + if arguments and content: + raise RuntimeError("plot:: directive can't have both args and content") + + document = state_machine.document + config = document.settings.env.config + + options.setdefault("include-source", config.plot_include_source) + + # determine input + rst_file = document.attributes["source"] + rst_dir = os.path.dirname(rst_file) + + if arguments: + if not config.plot_basedir: + source_file_name = os.path.join(rst_dir, directives.uri(arguments[0])) + else: + source_file_name = os.path.join( + setup.confdir, config.plot_basedir, directives.uri(arguments[0]) + ) + code = open(source_file_name, "r").read() + output_base = os.path.basename(source_file_name) + else: + source_file_name = rst_file + code = textwrap.dedent("\n".join(map(str, content))) + counter = document.attributes.get("_plot_counter", 0) + 1 + document.attributes["_plot_counter"] = counter + base, ext = os.path.splitext(os.path.basename(source_file_name)) + output_base = "%s-%d.py" % (base, counter) + + base, source_ext = os.path.splitext(output_base) + if source_ext in (".py", ".rst", ".txt"): + output_base = base + else: + source_ext = "" + + # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames + output_base = output_base.replace(".", "-") + + # is it in doctest format? + is_doctest = contains_doctest(code) + if "format" in options: + if options["format"] == "python": + is_doctest = False + else: + is_doctest = True + + # determine output directory name fragment + source_rel_name = relpath(source_file_name, setup.confdir) + source_rel_dir = os.path.dirname(source_rel_name) + while source_rel_dir.startswith(os.path.sep): + source_rel_dir = source_rel_dir[1:] + + # build_dir: where to place output files (temporarily) + build_dir = os.path.join( + os.path.dirname(setup.app.doctreedir), "plot_directive", source_rel_dir + ) + if not os.path.exists(build_dir): + os.makedirs(build_dir) + + # output_dir: final location in the builder's directory + dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, source_rel_dir)) + + # how to link to files from the RST file + dest_dir_link = os.path.join( + relpath(setup.confdir, rst_dir), source_rel_dir + ).replace(os.path.sep, "/") + build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, "/") + source_link = dest_dir_link + "/" + output_base + source_ext + + # make figures + try: + results = makefig(code, source_file_name, build_dir, output_base, config) + errors = [] + except PlotError as err: + reporter = state.memo.reporter + sm = reporter.system_message( + 2, "Exception occurred in plotting %s: %s" % (output_base, err), line=lineno + ) + results = [(code, [])] + errors = [sm] + + # generate output restructuredtext + total_lines = [] + for j, (code_piece, images) in enumerate(results): + if options["include-source"]: + if is_doctest: + lines = [""] + lines += [row.rstrip() for row in code_piece.split("\n")] + else: + lines = [".. code-block:: python", ""] + lines += [" %s" % row.rstrip() for row in code_piece.split("\n")] + source_code = "\n".join(lines) + else: + source_code = "" + + opts = [ + ":%s: %s" % (key, val) + for key, val in list(options.items()) + if key in ("alt", "height", "width", "scale", "align", "class") + ] + + only_html = ".. only:: html" + only_latex = ".. only:: latex" + + if j == 0: + src_link = source_link + else: + src_link = None + + result = format_template( + TEMPLATE, + dest_dir=dest_dir_link, + build_dir=build_dir_link, + source_link=src_link, + multi_image=len(images) > 1, + only_html=only_html, + only_latex=only_latex, + options=opts, + images=images, + source_code=source_code, + html_show_formats=config.plot_html_show_formats, + ) + + total_lines.extend(result.split("\n")) + total_lines.extend("\n") + + if total_lines: + state_machine.insert_input(total_lines, source=source_file_name) + + # copy image files to builder's output directory + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + for code_piece, images in results: + for img in images: + for fn in img.filenames(): + shutil.copyfile(fn, os.path.join(dest_dir, os.path.basename(fn))) + + # copy script (if necessary) + if source_file_name == rst_file: + target_name = os.path.join(dest_dir, output_base + source_ext) + f = open(target_name, "w") + f.write(unescape_doctest(code)) + f.close() + + return errors + + +# ------------------------------------------------------------------------------ +# Run code and capture figures +# ------------------------------------------------------------------------------ + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import matplotlib.image as image +from matplotlib import _pylab_helpers + +import exceptions + + +def contains_doctest(text): + try: + # check if it's valid Python as-is + compile(text, "", "exec") + return False + except SyntaxError: + pass + r = re.compile(r"^\s*>>>", re.M) + m = r.search(text) + return bool(m) + + +def unescape_doctest(text): + """ + Extract code from a piece of text, which contains either Python code + or doctests. + + """ + if not contains_doctest(text): + return text + + code = "" + for line in text.split("\n"): + m = re.match(r"^\s*(>>>|\.\.\.) (.*)$", line) + if m: + code += m.group(2) + "\n" + elif line.strip(): + code += "# " + line.strip() + "\n" + else: + code += "\n" + return code + + +def split_code_at_show(text): + """ + Split code at plt.show() + + """ + + parts = [] + is_doctest = contains_doctest(text) + + part = [] + for line in text.split("\n"): + if (not is_doctest and line.strip() == "plt.show()") or ( + is_doctest and line.strip() == ">>> plt.show()" + ): + part.append(line) + parts.append("\n".join(part)) + part = [] + else: + part.append(line) + if "\n".join(part).strip(): + parts.append("\n".join(part)) + return parts + + +class PlotError(RuntimeError): + pass + + +def run_code(code, code_path, ns=None): + # Change the working directory to the directory of the example, so + # it can get at its data files, if any. + pwd = os.getcwd() + old_sys_path = list(sys.path) + if code_path is not None: + dirname = os.path.abspath(os.path.dirname(code_path)) + os.chdir(dirname) + sys.path.insert(0, dirname) + + # Redirect stdout + stdout = sys.stdout + sys.stdout = StringIO() + + # Reset sys.argv + old_sys_argv = sys.argv + sys.argv = [code_path] + + try: + try: + code = unescape_doctest(code) + if ns is None: + ns = {} + if not ns: + exec(setup.config.plot_pre_code, ns) + exec(code, ns) + except (Exception, SystemExit) as err: + raise PlotError(traceback.format_exc()) + finally: + os.chdir(pwd) + sys.argv = old_sys_argv + sys.path[:] = old_sys_path + sys.stdout = stdout + return ns + + +# ------------------------------------------------------------------------------ +# Generating figures +# ------------------------------------------------------------------------------ + + +def out_of_date(original, derived): + """ + Returns True if derivative is out-of-date wrt original, + both of which are full file paths. + """ + return ( + not os.path.exists(derived) + or os.stat(derived).st_mtime < os.stat(original).st_mtime + ) + + +def makefig(code, code_path, output_dir, output_base, config): + """ + Run a pyplot script *code* and save the images under *output_dir* + with file names derived from *output_base* + + """ + + # -- Parse format list + default_dpi = {"png": 80, "hires.png": 200, "pdf": 50} + formats = [] + for fmt in config.plot_formats: + if isinstance(fmt, str): + formats.append((fmt, default_dpi.get(fmt, 80))) + elif type(fmt) in (tuple, list) and len(fmt) == 2: + formats.append((str(fmt[0]), int(fmt[1]))) + else: + raise PlotError('invalid image format "%r" in plot_formats' % fmt) + + # -- Try to determine if all images already exist + + code_pieces = split_code_at_show(code) + + # Look for single-figure output files first + all_exists = True + img = ImageFile(output_base, output_dir) + for format, dpi in formats: + if out_of_date(code_path, img.filename(format)): + all_exists = False + break + img.formats.append(format) + + if all_exists: + return [(code, [img])] + + # Then look for multi-figure output files + results = [] + all_exists = True + for i, code_piece in enumerate(code_pieces): + images = [] + for j in range(1000): + img = ImageFile("%s_%02d_%02d" % (output_base, i, j), output_dir) + for format, dpi in formats: + if out_of_date(code_path, img.filename(format)): + all_exists = False + break + img.formats.append(format) + + # assume that if we have one, we have them all + if not all_exists: + all_exists = j > 0 + break + images.append(img) + if not all_exists: + break + results.append((code_piece, images)) + + if all_exists: + return results + + # -- We didn't find the files, so build them + + results = [] + ns = {} + + for i, code_piece in enumerate(code_pieces): + # Clear between runs + plt.close("all") + + # Run code + run_code(code_piece, code_path, ns) + + # Collect images + images = [] + fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() + for j, figman in enumerate(fig_managers): + if len(fig_managers) == 1 and len(code_pieces) == 1: + img = ImageFile(output_base, output_dir) + else: + img = ImageFile("%s_%02d_%02d" % (output_base, i, j), output_dir) + images.append(img) + for format, dpi in formats: + try: + figman.canvas.figure.savefig(img.filename(format), dpi=dpi) + except exceptions.BaseException as err: + raise PlotError(traceback.format_exc()) + img.formats.append(format) + + # Results + results.append((code_piece, images)) + + return results + + +# ------------------------------------------------------------------------------ +# Relative pathnames +# ------------------------------------------------------------------------------ + +try: + from os.path import relpath +except ImportError: + # Copied from Python 2.7 + if "posix" in sys.builtin_module_names: + + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + from os.path import sep, curdir, join, abspath, commonprefix, pardir + + if not path: + raise ValueError("no path specified") + + start_list = abspath(start).split(sep) + path_list = abspath(path).split(sep) + + # Work out how much of the filepath is shared by start and path. + i = len(commonprefix([start_list, path_list])) + + rel_list = [pardir] * (len(start_list) - i) + path_list[i:] + if not rel_list: + return curdir + return join(*rel_list) + + elif "nt" in sys.builtin_module_names: + + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + from os.path import ( + sep, + curdir, + join, + abspath, + commonprefix, + pardir, + splitunc, + ) + + if not path: + raise ValueError("no path specified") + start_list = abspath(start).split(sep) + path_list = abspath(path).split(sep) + if start_list[0].lower() != path_list[0].lower(): + unc_path, rest = splitunc(path) + unc_start, rest = splitunc(start) + if bool(unc_path) ^ bool(unc_start): + raise ValueError( + "Cannot mix UNC and non-UNC paths (%s and %s)" % (path, start) + ) + else: + raise ValueError( + "path is on drive %s, start on drive %s" + % (path_list[0], start_list[0]) + ) + # Work out how much of the filepath is shared by start and path. + for i in range(min(len(start_list), len(path_list))): + if start_list[i].lower() != path_list[i].lower(): + break + else: + i += 1 + + rel_list = [pardir] * (len(start_list) - i) + path_list[i:] + if not rel_list: + return curdir + return join(*rel_list) + + else: + raise RuntimeError("Unsupported platform (no relpath available!)") diff --git a/doc/numpydoc-0.5/numpydoc/tests/test_docscrape.py b/doc/numpydoc-0.5/numpydoc/tests/test_docscrape.py new file mode 100644 index 0000000..5976957 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/tests/test_docscrape.py @@ -0,0 +1,841 @@ +# -*- encoding:utf-8 -*- +from __future__ import division, absolute_import, print_function + +import sys, textwrap + +from numpydoc.docscrape import NumpyDocString, FunctionDoc, ClassDoc +from numpydoc.docscrape_sphinx import SphinxDocString, SphinxClassDoc +from nose.tools import * + +if sys.version_info[0] >= 3: + sixu = lambda s: s +else: + sixu = lambda s: unicode(s, "unicode_escape") + + +doc_txt = """\ + numpy.multivariate_normal(mean, cov, shape=None, spam=None) + + Draw values from a multivariate normal distribution with specified + mean and covariance. + + The multivariate normal or Gaussian distribution is a generalisation + of the one-dimensional normal distribution to higher dimensions. + + Parameters + ---------- + mean : (N,) ndarray + Mean of the N-dimensional distribution. + + .. math:: + + (1+2+3)/3 + + cov : (N, N) ndarray + Covariance matrix of the distribution. + shape : tuple of ints + Given a shape of, for example, (m,n,k), m*n*k samples are + generated, and packed in an m-by-n-by-k arrangement. Because + each sample is N-dimensional, the output shape is (m,n,k,N). + + Returns + ------- + out : ndarray + The drawn samples, arranged according to `shape`. If the + shape given is (m,n,...), then the shape of `out` is is + (m,n,...,N). + + In other words, each entry ``out[i,j,...,:]`` is an N-dimensional + value drawn from the distribution. + list of str + This is not a real return value. It exists to test + anonymous return values. + + Other Parameters + ---------------- + spam : parrot + A parrot off its mortal coil. + + Raises + ------ + RuntimeError + Some error + + Warns + ----- + RuntimeWarning + Some warning + + Warnings + -------- + Certain warnings apply. + + Notes + ----- + Instead of specifying the full covariance matrix, popular + approximations include: + + - Spherical covariance (`cov` is a multiple of the identity matrix) + - Diagonal covariance (`cov` has non-negative elements only on the diagonal) + + This geometrical property can be seen in two dimensions by plotting + generated data-points: + + >>> mean = [0,0] + >>> cov = [[1,0],[0,100]] # diagonal covariance, points lie on x or y-axis + + >>> x,y = multivariate_normal(mean,cov,5000).T + >>> plt.plot(x,y,'x'); plt.axis('equal'); plt.show() + + Note that the covariance matrix must be symmetric and non-negative + definite. + + References + ---------- + .. [1] A. Papoulis, "Probability, Random Variables, and Stochastic + Processes," 3rd ed., McGraw-Hill Companies, 1991 + .. [2] R.O. Duda, P.E. Hart, and D.G. Stork, "Pattern Classification," + 2nd ed., Wiley, 2001. + + See Also + -------- + some, other, funcs + otherfunc : relationship + + Examples + -------- + >>> mean = (1,2) + >>> cov = [[1,0],[1,0]] + >>> x = multivariate_normal(mean,cov,(3,3)) + >>> print x.shape + (3, 3, 2) + + The following is probably true, given that 0.6 is roughly twice the + standard deviation: + + >>> print list( (x[0,0,:] - mean) < 0.6 ) + [True, True] + + .. index:: random + :refguide: random;distributions, random;gauss + + """ +doc = NumpyDocString(doc_txt) + + +def test_signature(): + assert doc["Signature"].startswith("numpy.multivariate_normal(") + assert doc["Signature"].endswith("spam=None)") + + +def test_summary(): + assert doc["Summary"][0].startswith("Draw values") + assert doc["Summary"][-1].endswith("covariance.") + + +def test_extended_summary(): + assert doc["Extended Summary"][0].startswith("The multivariate normal") + + +def test_parameters(): + assert_equal(len(doc["Parameters"]), 3) + assert_equal([n for n, _, _ in doc["Parameters"]], ["mean", "cov", "shape"]) + + arg, arg_type, desc = doc["Parameters"][1] + assert_equal(arg_type, "(N, N) ndarray") + assert desc[0].startswith("Covariance matrix") + assert doc["Parameters"][0][-1][-2] == " (1+2+3)/3" + + +def test_other_parameters(): + assert_equal(len(doc["Other Parameters"]), 1) + assert_equal([n for n, _, _ in doc["Other Parameters"]], ["spam"]) + arg, arg_type, desc = doc["Other Parameters"][0] + assert_equal(arg_type, "parrot") + assert desc[0].startswith("A parrot off its mortal coil") + + +def test_returns(): + assert_equal(len(doc["Returns"]), 2) + arg, arg_type, desc = doc["Returns"][0] + assert_equal(arg, "out") + assert_equal(arg_type, "ndarray") + assert desc[0].startswith("The drawn samples") + assert desc[-1].endswith("distribution.") + + arg, arg_type, desc = doc["Returns"][1] + assert_equal(arg, "list of str") + assert_equal(arg_type, "") + assert desc[0].startswith("This is not a real") + assert desc[-1].endswith("anonymous return values.") + + +def test_notes(): + assert doc["Notes"][0].startswith("Instead") + assert doc["Notes"][-1].endswith("definite.") + assert_equal(len(doc["Notes"]), 17) + + +def test_references(): + assert doc["References"][0].startswith("..") + assert doc["References"][-1].endswith("2001.") + + +def test_examples(): + assert doc["Examples"][0].startswith(">>>") + assert doc["Examples"][-1].endswith("True]") + + +def test_index(): + assert_equal(doc["index"]["default"], "random") + assert_equal(len(doc["index"]), 2) + assert_equal(len(doc["index"]["refguide"]), 2) + + +def non_blank_line_by_line_compare(a, b): + a = textwrap.dedent(a) + b = textwrap.dedent(b) + a = [l.rstrip() for l in a.split("\n") if l.strip()] + b = [l.rstrip() for l in b.split("\n") if l.strip()] + for n, line in enumerate(a): + if not line == b[n]: + raise AssertionError( + "Lines %s of a and b differ: " "\n>>> %s\n<<< %s\n" % (n, line, b[n]) + ) + + +def test_str(): + non_blank_line_by_line_compare( + str(doc), + """numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +Draw values from a multivariate normal distribution with specified +mean and covariance. + +The multivariate normal or Gaussian distribution is a generalisation +of the one-dimensional normal distribution to higher dimensions. + +Parameters +---------- +mean : (N,) ndarray + Mean of the N-dimensional distribution. + + .. math:: + + (1+2+3)/3 + +cov : (N, N) ndarray + Covariance matrix of the distribution. +shape : tuple of ints + Given a shape of, for example, (m,n,k), m*n*k samples are + generated, and packed in an m-by-n-by-k arrangement. Because + each sample is N-dimensional, the output shape is (m,n,k,N). + +Returns +------- +out : ndarray + The drawn samples, arranged according to `shape`. If the + shape given is (m,n,...), then the shape of `out` is is + (m,n,...,N). + + In other words, each entry ``out[i,j,...,:]`` is an N-dimensional + value drawn from the distribution. +list of str + This is not a real return value. It exists to test + anonymous return values. + +Other Parameters +---------------- +spam : parrot + A parrot off its mortal coil. + +Raises +------ +RuntimeError + Some error + +Warns +----- +RuntimeWarning + Some warning + +Warnings +-------- +Certain warnings apply. + +See Also +-------- +`some`_, `other`_, `funcs`_ + +`otherfunc`_ + relationship + +Notes +----- +Instead of specifying the full covariance matrix, popular +approximations include: + + - Spherical covariance (`cov` is a multiple of the identity matrix) + - Diagonal covariance (`cov` has non-negative elements only on the diagonal) + +This geometrical property can be seen in two dimensions by plotting +generated data-points: + +>>> mean = [0,0] +>>> cov = [[1,0],[0,100]] # diagonal covariance, points lie on x or y-axis + +>>> x,y = multivariate_normal(mean,cov,5000).T +>>> plt.plot(x,y,'x'); plt.axis('equal'); plt.show() + +Note that the covariance matrix must be symmetric and non-negative +definite. + +References +---------- +.. [1] A. Papoulis, "Probability, Random Variables, and Stochastic + Processes," 3rd ed., McGraw-Hill Companies, 1991 +.. [2] R.O. Duda, P.E. Hart, and D.G. Stork, "Pattern Classification," + 2nd ed., Wiley, 2001. + +Examples +-------- +>>> mean = (1,2) +>>> cov = [[1,0],[1,0]] +>>> x = multivariate_normal(mean,cov,(3,3)) +>>> print x.shape +(3, 3, 2) + +The following is probably true, given that 0.6 is roughly twice the +standard deviation: + +>>> print list( (x[0,0,:] - mean) < 0.6 ) +[True, True] + +.. index:: random + :refguide: random;distributions, random;gauss""", + ) + + +def test_sphinx_str(): + sphinx_doc = SphinxDocString(doc_txt) + non_blank_line_by_line_compare( + str(sphinx_doc), + """ +.. index:: random + single: random;distributions, random;gauss + +Draw values from a multivariate normal distribution with specified +mean and covariance. + +The multivariate normal or Gaussian distribution is a generalisation +of the one-dimensional normal distribution to higher dimensions. + +:Parameters: + + **mean** : (N,) ndarray + + Mean of the N-dimensional distribution. + + .. math:: + + (1+2+3)/3 + + **cov** : (N, N) ndarray + + Covariance matrix of the distribution. + + **shape** : tuple of ints + + Given a shape of, for example, (m,n,k), m*n*k samples are + generated, and packed in an m-by-n-by-k arrangement. Because + each sample is N-dimensional, the output shape is (m,n,k,N). + +:Returns: + + **out** : ndarray + + The drawn samples, arranged according to `shape`. If the + shape given is (m,n,...), then the shape of `out` is is + (m,n,...,N). + + In other words, each entry ``out[i,j,...,:]`` is an N-dimensional + value drawn from the distribution. + + list of str + + This is not a real return value. It exists to test + anonymous return values. + +:Other Parameters: + + **spam** : parrot + + A parrot off its mortal coil. + +:Raises: + + **RuntimeError** + + Some error + +:Warns: + + **RuntimeWarning** + + Some warning + +.. warning:: + + Certain warnings apply. + +.. seealso:: + + :obj:`some`, :obj:`other`, :obj:`funcs` + + :obj:`otherfunc` + relationship + +.. rubric:: Notes + +Instead of specifying the full covariance matrix, popular +approximations include: + + - Spherical covariance (`cov` is a multiple of the identity matrix) + - Diagonal covariance (`cov` has non-negative elements only on the diagonal) + +This geometrical property can be seen in two dimensions by plotting +generated data-points: + +>>> mean = [0,0] +>>> cov = [[1,0],[0,100]] # diagonal covariance, points lie on x or y-axis + +>>> x,y = multivariate_normal(mean,cov,5000).T +>>> plt.plot(x,y,'x'); plt.axis('equal'); plt.show() + +Note that the covariance matrix must be symmetric and non-negative +definite. + +.. rubric:: References + +.. [1] A. Papoulis, "Probability, Random Variables, and Stochastic + Processes," 3rd ed., McGraw-Hill Companies, 1991 +.. [2] R.O. Duda, P.E. Hart, and D.G. Stork, "Pattern Classification," + 2nd ed., Wiley, 2001. + +.. only:: latex + + [1]_, [2]_ + +.. rubric:: Examples + +>>> mean = (1,2) +>>> cov = [[1,0],[1,0]] +>>> x = multivariate_normal(mean,cov,(3,3)) +>>> print x.shape +(3, 3, 2) + +The following is probably true, given that 0.6 is roughly twice the +standard deviation: + +>>> print list( (x[0,0,:] - mean) < 0.6 ) +[True, True] +""", + ) + + +doc2 = NumpyDocString( + """ + Returns array of indices of the maximum values of along the given axis. + + Parameters + ---------- + a : {array_like} + Array to look in. + axis : {None, integer} + If None, the index is into the flattened array, otherwise along + the specified axis""" +) + + +def test_parameters_without_extended_description(): + assert_equal(len(doc2["Parameters"]), 2) + + +doc3 = NumpyDocString( + """ + my_signature(*params, **kwds) + + Return this and that. + """ +) + + +def test_escape_stars(): + signature = str(doc3).split("\n")[0] + assert_equal(signature, "my_signature(\*params, \*\*kwds)") + + +doc4 = NumpyDocString( + """a.conj() + + Return an array with all complex-valued elements conjugated.""" +) + + +def test_empty_extended_summary(): + assert_equal(doc4["Extended Summary"], []) + + +doc5 = NumpyDocString( + """ + a.something() + + Raises + ------ + LinAlgException + If array is singular. + + Warns + ----- + SomeWarning + If needed + """ +) + + +def test_raises(): + assert_equal(len(doc5["Raises"]), 1) + name, _, desc = doc5["Raises"][0] + assert_equal(name, "LinAlgException") + assert_equal(desc, ["If array is singular."]) + + +def test_warns(): + assert_equal(len(doc5["Warns"]), 1) + name, _, desc = doc5["Warns"][0] + assert_equal(name, "SomeWarning") + assert_equal(desc, ["If needed"]) + + +def test_see_also(): + doc6 = NumpyDocString( + """ + z(x,theta) + + See Also + -------- + func_a, func_b, func_c + func_d : some equivalent func + foo.func_e : some other func over + multiple lines + func_f, func_g, :meth:`func_h`, func_j, + func_k + :obj:`baz.obj_q` + :class:`class_j`: fubar + foobar + """ + ) + + assert len(doc6["See Also"]) == 12 + for func, desc, role in doc6["See Also"]: + if func in ( + "func_a", + "func_b", + "func_c", + "func_f", + "func_g", + "func_h", + "func_j", + "func_k", + "baz.obj_q", + ): + assert not desc + else: + assert desc + + if func == "func_h": + assert role == "meth" + elif func == "baz.obj_q": + assert role == "obj" + elif func == "class_j": + assert role == "class" + else: + assert role is None + + if func == "func_d": + assert desc == ["some equivalent func"] + elif func == "foo.func_e": + assert desc == ["some other func over", "multiple lines"] + elif func == "class_j": + assert desc == ["fubar", "foobar"] + + +def test_see_also_print(): + class Dummy(object): + """ + See Also + -------- + func_a, func_b + func_c : some relationship + goes here + func_d + """ + + pass + + obj = Dummy() + s = str(FunctionDoc(obj, role="func")) + assert ":func:`func_a`, :func:`func_b`" in s + assert " some relationship" in s + assert ":func:`func_d`" in s + + +doc7 = NumpyDocString( + """ + + Doc starts on second line. + + """ +) + + +def test_empty_first_line(): + assert doc7["Summary"][0].startswith("Doc starts") + + +def test_no_summary(): + str( + SphinxDocString( + """ + Parameters + ----------""" + ) + ) + + +def test_unicode(): + doc = SphinxDocString( + """ + öäöäöäöäöåååå + + öäöäöäööäååå + + Parameters + ---------- + ååå : äää + ööö + + Returns + ------- + ååå : ööö + äää + + """ + ) + assert isinstance(doc["Summary"][0], str) + assert doc["Summary"][0] == "öäöäöäöäöåååå" + + +def test_plot_examples(): + cfg = dict(use_plots=True) + + doc = SphinxDocString( + """ + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> plt.plot([1,2,3],[4,5,6]) + >>> plt.show() + """, + config=cfg, + ) + assert "plot::" in str(doc), str(doc) + + doc = SphinxDocString( + """ + Examples + -------- + .. plot:: + + import matplotlib.pyplot as plt + plt.plot([1,2,3],[4,5,6]) + plt.show() + """, + config=cfg, + ) + assert str(doc).count("plot::") == 1, str(doc) + + +def test_class_members(): + class Dummy(object): + """ + Dummy class. + + """ + + def spam(self, a, b): + """Spam\n\nSpam spam.""" + pass + + def ham(self, c, d): + """Cheese\n\nNo cheese.""" + pass + + @property + def spammity(self): + """Spammity index""" + return 0.95 + + class Ignorable(object): + """local class, to be ignored""" + + pass + + for cls in (ClassDoc, SphinxClassDoc): + doc = cls(Dummy, config=dict(show_class_members=False)) + assert "Methods" not in str(doc), (cls, str(doc)) + assert "spam" not in str(doc), (cls, str(doc)) + assert "ham" not in str(doc), (cls, str(doc)) + assert "spammity" not in str(doc), (cls, str(doc)) + assert "Spammity index" not in str(doc), (cls, str(doc)) + + doc = cls(Dummy, config=dict(show_class_members=True)) + assert "Methods" in str(doc), (cls, str(doc)) + assert "spam" in str(doc), (cls, str(doc)) + assert "ham" in str(doc), (cls, str(doc)) + assert "spammity" in str(doc), (cls, str(doc)) + + if cls is SphinxClassDoc: + assert ".. autosummary::" in str(doc), str(doc) + else: + assert "Spammity index" in str(doc), str(doc) + + +def test_duplicate_signature(): + # Duplicate function signatures occur e.g. in ufuncs, when the + # automatic mechanism adds one, and a more detailed comes from the + # docstring itself. + + doc = NumpyDocString( + """ + z(x1, x2) + + z(a, theta) + """ + ) + + assert doc["Signature"].strip() == "z(a, theta)" + + +class_doc_txt = """ + Foo + + Parameters + ---------- + f : callable ``f(t, y, *f_args)`` + Aaa. + jac : callable ``jac(t, y, *jac_args)`` + Bbb. + + Attributes + ---------- + t : float + Current time. + y : ndarray + Current variable values. + + Methods + ------- + a + b + c + + Examples + -------- + For usage examples, see `ode`. +""" + + +def test_class_members_doc(): + doc = ClassDoc(None, class_doc_txt) + non_blank_line_by_line_compare( + str(doc), + """ + Foo + + Parameters + ---------- + f : callable ``f(t, y, *f_args)`` + Aaa. + jac : callable ``jac(t, y, *jac_args)`` + Bbb. + + Examples + -------- + For usage examples, see `ode`. + + Attributes + ---------- + t : float + Current time. + y : ndarray + Current variable values. + + Methods + ------- + a + + b + + c + + .. index:: + + """, + ) + + +def test_class_members_doc_sphinx(): + doc = SphinxClassDoc(None, class_doc_txt) + non_blank_line_by_line_compare( + str(doc), + """ + Foo + + :Parameters: + + **f** : callable ``f(t, y, *f_args)`` + + Aaa. + + **jac** : callable ``jac(t, y, *jac_args)`` + + Bbb. + + .. rubric:: Examples + + For usage examples, see `ode`. + + .. rubric:: Attributes + + === ========== + t (float) Current time. + y (ndarray) Current variable values. + === ========== + + .. rubric:: Methods + + === ========== + a + b + c + === ========== + + """, + ) + + +if __name__ == "__main__": + import nose + + nose.run() diff --git a/doc/numpydoc-0.5/numpydoc/tests/test_linkcode.py b/doc/numpydoc-0.5/numpydoc/tests/test_linkcode.py new file mode 100644 index 0000000..340166a --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/tests/test_linkcode.py @@ -0,0 +1,5 @@ +from __future__ import division, absolute_import, print_function + +import numpydoc.linkcode + +# No tests at the moment... diff --git a/doc/numpydoc-0.5/numpydoc/tests/test_phantom_import.py b/doc/numpydoc-0.5/numpydoc/tests/test_phantom_import.py new file mode 100644 index 0000000..75de7cc --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/tests/test_phantom_import.py @@ -0,0 +1,14 @@ +from __future__ import division, absolute_import, print_function + +import sys +from nose import SkipTest + + +def test_import(): + if sys.version_info[0] >= 3: + raise SkipTest("phantom_import not ported to Py3") + + import numpydoc.phantom_import + + +# No tests at the moment... diff --git a/doc/numpydoc-0.5/numpydoc/tests/test_plot_directive.py b/doc/numpydoc-0.5/numpydoc/tests/test_plot_directive.py new file mode 100644 index 0000000..e6ed81c --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/tests/test_plot_directive.py @@ -0,0 +1,15 @@ +from __future__ import division, absolute_import, print_function + +import sys +from nose import SkipTest + + +def test_import(): + if sys.version_info[0] >= 3: + raise SkipTest( + "plot_directive not ported to Python 3 (use the one from Matplotlib instead)" + ) + import numpydoc.plot_directive + + +# No tests at the moment... diff --git a/doc/numpydoc-0.5/numpydoc/tests/test_traitsdoc.py b/doc/numpydoc-0.5/numpydoc/tests/test_traitsdoc.py new file mode 100644 index 0000000..a4b8bd6 --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/tests/test_traitsdoc.py @@ -0,0 +1,13 @@ +from __future__ import division, absolute_import, print_function + +import sys +from nose import SkipTest + + +def test_import(): + if sys.version_info[0] >= 3: + raise SkipTest("traitsdoc not ported to Python3") + import numpydoc.traitsdoc + + +# No tests at the moment... diff --git a/doc/numpydoc-0.5/numpydoc/traitsdoc.py b/doc/numpydoc-0.5/numpydoc/traitsdoc.py new file mode 100644 index 0000000..b17a89f --- /dev/null +++ b/doc/numpydoc-0.5/numpydoc/traitsdoc.py @@ -0,0 +1,143 @@ +""" +========= +traitsdoc +========= + +Sphinx extension that handles docstrings in the Numpy standard format, [1] +and support Traits [2]. + +This extension can be used as a replacement for ``numpydoc`` when support +for Traits is required. + +.. [1] http://projects.scipy.org/numpy/wiki/CodingStyleGuidelines#docstring-standard +.. [2] http://code.enthought.com/projects/traits/ + +""" +from __future__ import division, absolute_import, print_function + +import inspect +import os +import pydoc +import collections + +from . import docscrape +from . import docscrape_sphinx +from .docscrape_sphinx import SphinxClassDoc, SphinxFunctionDoc, SphinxDocString + +from . import numpydoc + +from . import comment_eater + + +class SphinxTraitsDoc(SphinxClassDoc): + def __init__(self, cls, modulename="", func_doc=SphinxFunctionDoc): + if not inspect.isclass(cls): + raise ValueError("Initialise using a class. Got %r" % cls) + self._cls = cls + + if modulename and not modulename.endswith("."): + modulename += "." + self._mod = modulename + self._name = cls.__name__ + self._func_doc = func_doc + + docstring = pydoc.getdoc(cls) + docstring = docstring.split("\n") + + # De-indent paragraph + try: + indent = min(len(s) - len(s.lstrip()) for s in docstring if s.strip()) + except ValueError: + indent = 0 + + for n, line in enumerate(docstring): + docstring[n] = docstring[n][indent:] + + self._doc = docscrape.Reader(docstring) + self._parsed_data = { + "Signature": "", + "Summary": "", + "Description": [], + "Extended Summary": [], + "Parameters": [], + "Returns": [], + "Raises": [], + "Warns": [], + "Other Parameters": [], + "Traits": [], + "Methods": [], + "See Also": [], + "Notes": [], + "References": "", + "Example": "", + "Examples": "", + "index": {}, + } + + self._parse() + + def _str_summary(self): + return self["Summary"] + [""] + + def _str_extended_summary(self): + return self["Description"] + self["Extended Summary"] + [""] + + def __str__(self, indent=0, func_role="func"): + out = [] + out += self._str_signature() + out += self._str_index() + [""] + out += self._str_summary() + out += self._str_extended_summary() + for param_list in ("Parameters", "Traits", "Methods", "Returns", "Raises"): + out += self._str_param_list(param_list) + out += self._str_see_also("obj") + out += self._str_section("Notes") + out += self._str_references() + out += self._str_section("Example") + out += self._str_section("Examples") + out = self._str_indent(out, indent) + return "\n".join(out) + + +def looks_like_issubclass(obj, classname): + """ Return True if the object has a class or superclass with the given class + name. + + Ignores old-style classes. + """ + t = obj + if t.__name__ == classname: + return True + for klass in t.__mro__: + if klass.__name__ == classname: + return True + return False + + +def get_doc_object(obj, what=None, config=None): + if what is None: + if inspect.isclass(obj): + what = "class" + elif inspect.ismodule(obj): + what = "module" + elif isinstance(obj, collections.Callable): + what = "function" + else: + what = "object" + if what == "class": + doc = SphinxTraitsDoc(obj, "", func_doc=SphinxFunctionDoc, config=config) + if looks_like_issubclass(obj, "HasTraits"): + for name, trait, comment in comment_eater.get_class_traits(obj): + # Exclude private traits. + if not name.startswith("_"): + doc["Traits"].append((name, trait, comment.splitlines())) + return doc + elif what in ("function", "method"): + return SphinxFunctionDoc(obj, "", config=config) + else: + return SphinxDocString(pydoc.getdoc(obj), config=config) + + +def setup(app): + # init numpydoc + numpydoc.setup(app, get_doc_object) diff --git a/doc/numpydoc-0.5/setup.cfg b/doc/numpydoc-0.5/setup.cfg new file mode 100644 index 0000000..861a9f5 --- /dev/null +++ b/doc/numpydoc-0.5/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/doc/numpydoc-0.5/setup.py b/doc/numpydoc-0.5/setup.py new file mode 100644 index 0000000..0f12aed --- /dev/null +++ b/doc/numpydoc-0.5/setup.py @@ -0,0 +1,32 @@ +from __future__ import division, print_function + +import sys +import setuptools +from distutils.core import setup + +if sys.version_info[:2] < (2, 6) or (3, 0) <= sys.version_info[0:2] < (3, 3): + raise RuntimeError("Python version 2.6, 2.7 or >= 3.3 required.") + +version = "0.5" + +setup( + name="numpydoc", + packages=["numpydoc"], + version=version, + description="Sphinx extension to support docstrings in Numpy format", + # classifiers from http://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Plugins", + "License :: OSI Approved :: BSD License", + "Topic :: Documentation", + ], + keywords="sphinx numpy", + author="Pauli Virtanen and others", + author_email="pav@iki.fi", + url="https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt", + license="BSD", + requires=["sphinx (>= 1.0.1)"], + package_data={"numpydoc": ["tests/test_*.py"]}, + test_suite="nose.collector", +) diff --git a/doc/source/an_model.rst b/doc/source/an_model.rst new file mode 100644 index 0000000..6539d07 --- /dev/null +++ b/doc/source/an_model.rst @@ -0,0 +1,28 @@ +an_model +-------- + +.. automodule:: cnmodel.an_model + :members: + :undoc-members: + :show-inheritance: + + +cnmodel.an_model.cache +====================== + +.. automodule:: cnmodel.an_model.cache + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.an_model.wrapper +======================== + +.. automodule:: cnmodel.an_model.wrapper + :members: + :undoc-members: + :show-inheritance: + :noindex: + + diff --git a/doc/source/architecture.rst b/doc/source/architecture.rst new file mode 100644 index 0000000..f6b7a61 --- /dev/null +++ b/doc/source/architecture.rst @@ -0,0 +1,180 @@ +Architecture of CNModel +======================= + +CNModel is built in a layered architecture that provides both high-level tools +for constructing and simulating networks, as well as low-level access to the +individual components. The model consists of three major pieces: *cell types*, +which describe the morphology and membrane physiology of the various cell types +in the cochlear nucleus; *synapse types*, which describe the strength, kinetics, +and short-term plasticity of the many synaptic connections; and *populations*, +which describe the aggregate organization of cell types, including their +distribution within the nucleus and their patterns of connectivity. + + .. figure:: architecture.svg + + +Cells +----- + +The cell types found in the cochlear nucleus (bushy, t-stellate, etc.) are each +represented in CNModel as classes that inherit from the base `Cell` class. One +instance of any of these classes represents exactly one neuron, and internally +manages any number of NEURON sections and mechanisms. + +Each class is responsible for determining the morphology and intrinsic membrane +properties of the cell it represents. Additionally, cell classes define the +properties of their synapses in a cooperative manner: the pre- snd postsynaptic +cells each create half of the synapse. + +Morphology can be procedurally generated (usually a single somatic section, +perhaps with another for a dendrite and/or axon), or it can be loaded from an +SWC file. After the cell morphology is established, the membrane is "decorated" +with channels using a `ChannelDecorator` object. + +Cell classes may be further divided into a class hierarchy. For example, the +base `DStellate` class is further inherited by `DStellateRothman` and +`DStellateEager` classes that each implement different D-stellate models that +have been published previously. To simplify (and in many cases to automate) the +creation of cells each base cell class (`Bushy`, `SGC`, `TStellate`, etc.) +implements a `create` method that can be used to generate instances from any of +the subclasses. + + +Synapses +-------- + +Every synapse in CNModel is represented by an instance of the `Synapse` class. +This class contains two objects: a `Terminal`, and a `PSD`. Synapses are created +by calling the `connect` method of the presynaptic cell with the postsynaptic +cell as an argument:: + + pre_cell.connect(post_cell) + +When `connect` is called, the presynaptic terminal is created by calling +``pre_cell.make_terminal()``, and the postsynaptic receptors are created by +calling ``post_cell.make_psd()``. In this way, both pre- and postsynaptic +cells are given a chance to influence the creation of the synapse. + +CNModel implements most synapses in two different ways. The first is a simple +synapse that implements variable amplitude, short-term plasticity, and double- +exponential rise/fall kinetics. This synapse is relatively efficient to compute +and simple to configure. The second synapse implementation is a much more +physiologically detailed model that includes stochastic release from multiple +release zones per terminal, synaptic cleft diffusion, and state models of +postsynaptic receptors. This synapse is computationally expensive but may capture +important behaviors that are not possible with the simpler implementation. + + +Populations +----------- + +Populations are objects that encapsulate a group of cells sharing a common cell +class. Each base `Cell` class has a corresponding `Population` class that +implements the organization of many cells within the nucleus and the patterns +of connectivity between populations. + +Conceptually, a `Population` represents *all* cells of a particular type within +the nucleus. When a population is created, it initially decides how many cells +is will represent and how to distribute properties across those cells. For +example, an `SGC` population describes 10k cells distributed uniformly across +the tonotopic axis. Initially, none of these 10k cells are created; rather, +each cell is simply represented as a virtual placeholder, and only instantiated +when it is explicitly requested or when it is required to satisfy the input +requirements for another cell. + +Populations are connected to each other in much the same way cells are:: + + pre_pop.connect(post_pop) + +Like the virtual cells, however, this connection does not create any synapses, +but instead merely records the fact that one population of cells projects to +another. Once the populations of interest are created and connected, the user +then manually instantiates only the cells that they wish to record from, and +finally the entire network of presynaptic inputs is automatically instantiated. + +Because populations manage the creation of synaptic connections between large +groups of neurons, they are also responsible for ensuring that the appropriate +patterns of connectivity are followed. For example, this allows us to ensure +that bushy cells are automatically connected to auditory nerve fibers coming +from a relatively narrow window across the tonotopic axis, whereas D-stellate +cells integrate the same inputs across a broader window. + + +Physiological parameters +------------------------ + +Throughout the model we use physiological parameters (channel kinetics, +synaptic strengths, convergence ratios, etc.) that are often derived +from published reports. In an attempt to make the provenance of these +parameters clear, we have separated them from the source code and embedded +them in annotated tables. These tables are found in `cnmodel/data`, and are +automatically parsed by the model as they are needed. + +Morphology +---------- + +The model implements the ability to use morphological reconstructions of +cells, as rendered in hoc files, the native NEURON format. These reconstructions +can be decorated with ion channels and synapses according to pre-specified +tables or pre-defined rules. + +Unit testing +------------ + +CNModel attempts to reproduce several specific published observations. +The complexity of the model makes it quite fragile--small modifications to one +physiological parameter may have unexpected (and sometimes unnoticed) +consequences in the output of the model. + +To combat this unavoidable fragility, CNModel includes a large battery of unit +tests that are used to ensure the model output is stable across modifications +and platforms, and that it does reproduce our target observations within +reasonable limits. As such, any modification to the model should usually be +followed soon after by running the unit tests (these depend on the py.test +package and may be invoked by running the included `test.py` script). + + +Auditory nerve input +-------------------- + +CNModel builds from the auditory periphery model developed by Zilany et al. +(2014). The periphery model converts auditory stimuli into +spike trains for auditory nerve fibers of a specific CF and SR group. + +CNModel uses the Python version of the auditory periphery model +as implemented by Rudnicki and Hemmert (available from https://github.com/mrkrd/cochlea). +This version does not require MATLAB, and in some simulations may run slightly faster +because there is no delay associated with loading (and unloading) MATLAB. The +interface is otherwise exactly the same, and the model type can be selected +at runtime. + +An alternative approach is to use the original auditory periphery model. Because +this model was developed in MATLAB, CNModel uses a Python-to-MATLAB bridge +that transparently invokes the periphery model in a background process. +When using CNModel, is is generally not necessary to manually interact with +MATLAB in any way; this interaction is wrapped within functions in the +`cnmodel/an_model` subpackage. + +At present, there is no mechanism for feedback from the cochlear nucleus model +back into the auditory periphery model. As such, the output of the periphery +model is a convenient place to do some caching--we can precompute auditory +nerve spike trains in response to various sound stimuli and reuse those spike +trains to improve the computational performance of the nucleus model. This +caching is performed automatically, but relies on the use of `Sound` objects +(described below) as a mechanism for storing and retrieving cached spike trains. + + +Generating sound stimuli +------------------------ + +Sound stimuli are defined as subclasses of `cnmodel.util.sound.Sound`. Each +subclass (for example, `TonePip` and `NoisePip`) defines the function for +generating a sound waveform, but also provides a unique key that is used to +store and retrieve auditory nerve spike trains that were generated with +a particular stimulus. + +`Sound` objects may be passed directly to `SGC` cells or populations, and the +necessary spike trains will be automatically computed (or read from cache). + + + diff --git a/doc/source/architecture.svg b/doc/source/architecture.svg new file mode 100644 index 0000000..f097372 --- /dev/null +++ b/doc/source/architecture.svg @@ -0,0 +1,1740 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Populations + + Cells + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Channels and + Receptors + + + + + + + + + + + + + + AMPA + + + + + + + + + + + + + NMDA + + + + + + + + + + + + + Gly + + + + + + + + + + + + + GABA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Na + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KLT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KHT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ih + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KA + + + + NEURON + + + Python + + + + + + Bushy + + + + TStellate + + + + SGC + + + + Pyramidal + + + + Tuberculov. + + + + DStellate + + + + + + + + + + + + + + + + + + + + + + Octopus + + + + AuditoryPeripheryModel + + + + + diff --git a/doc/source/cells.rst b/doc/source/cells.rst new file mode 100644 index 0000000..dffda1e --- /dev/null +++ b/doc/source/cells.rst @@ -0,0 +1,106 @@ +Cell classes +------------ + +.. automodule:: cnmodel.cells + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: Cell + :members: + +Individual cell subclasses +-------------------------- + +cnmodel.cells.bushy +=================== + +.. automodule:: cnmodel.cells.bushy + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.cells.cartwheel +======================= + +.. automodule:: cnmodel.cells.cartwheel + :members: + :undoc-members: + :show-inheritance: + :noindex: + +.. cnmodel.cells.cell class +.. ------------------------- +.. +.. .. automodule:: cnmodel.cells.cell +.. :members: +.. :undoc-members: +.. :show-inheritance: +.. :noindex: + +cnmodel.cells.dstellate +======================= + +.. automodule:: cnmodel.cells.dstellate + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.cells.hh +================ + +.. automodule:: cnmodel.cells.hh + :members: + :undoc-members: + :noindex: + :show-inheritance: + +cnmodel.cells.octopus +===================== + +.. automodule:: cnmodel.cells.octopus + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.cells.pyramidal +======================= + +.. automodule:: cnmodel.cells.pyramidal + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.cells.sgc +================= + +.. automodule:: cnmodel.cells.sgc + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.cells.tstellate +======================= + +.. automodule:: cnmodel.cells.tstellate + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.cells.tuberculoventral +============================== + +.. automodule:: cnmodel.cells.tuberculoventral + :members: + :undoc-members: + :show-inheritance: + :noindex: + + + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..6af3c00 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# +# CNModel documentation build configuration file, created by +# sphinx-quickstart on Sun Apr 2 15:20:59 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("../..")) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" +autoclass_content = "both" + +# General information about the project. +project = u"CNModel" +copyright = u"2017, 2018, Paul B. Manis and Luke Campagnola" +author = u"Paul B. Manis and Luke Campagnola" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u"0" +# The full version, including alpha/beta/rc tags. +release = u"0.2" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' +html_theme = "classic" +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = "CNModeldoc" + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "CNModel.tex", + u"CNModel Documentation", + u"Paul B. Manis and Luke Campagnola", + "manual", + ) +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "cnmodel", u"CNModel Documentation", [author], 1)] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "CNModel", + u"CNModel Documentation", + author, + "CNModel", + "One line description of project.", + "Miscellaneous", + ) +] diff --git a/doc/source/data.rst b/doc/source/data.rst new file mode 100644 index 0000000..3c852a7 --- /dev/null +++ b/doc/source/data.rst @@ -0,0 +1,37 @@ +cnmodel.data package +-------------------- + +.. autofunction:: cnmodel.data.get + +.. autofunction:: cnmodel.data.get_source + +.. autofunction:: cnmodel.data.add_table_data + +cnmodel.data.connectivity +========================= + +.. literalinclude:: ../../cnmodel/data/connectivity.py + :language: python + :linenos: + +cnmodel.data.synapses +===================== + +.. literalinclude:: ../../cnmodel/data/synapses.py + :language: python + :linenos: + +cnmodel.data.populations +======================== + +.. literalinclude:: ../../cnmodel/data/populations.py + :language: python + :linenos: + +cnmodel.data.ionchannels +======================== + +.. literalinclude:: ../../cnmodel/data/ionchannels.py + :language: python + :linenos: + \ No newline at end of file diff --git a/doc/source/decorator.rst b/doc/source/decorator.rst new file mode 100644 index 0000000..873b2f1 --- /dev/null +++ b/doc/source/decorator.rst @@ -0,0 +1,16 @@ +cnmodel.decorator package +------------------------- + +.. automodule:: cnmodel.decorator + :members: + :undoc-members: + :show-inheritance: + +cnmodel.decorator.decorator +============================ + +.. automodule:: cnmodel.decorator.decorator + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/doc/source/examples.rst b/doc/source/examples.rst new file mode 100644 index 0000000..aadd332 --- /dev/null +++ b/doc/source/examples.rst @@ -0,0 +1,31 @@ + +Examples +======== + + +Contents: + +.. toctree:: + :maxdepth: 0 + + examples/test_circuit + examples/test_mechanisms + examples/test_an_model + examples/test_bushy_variation + examples/test_simple_synapses + examples/test_mso_inputs + examples/test_sgc_input_phaselocking + examples/test_populations + examples/test_sgc_input + examples/test_cells + examples/test_physiology + examples/figures + examples/test_ccstim + examples/test_sounds + examples/toy_model + examples/test_sound_stim + examples/test_sgc_input_PSTH + examples/test_synapses + examples/test_decorator + examples/plot_hcno_kinetics + examples/play_test_sounds diff --git a/doc/source/examples/figures.rst b/doc/source/examples/figures.rst new file mode 100644 index 0000000..9c20471 --- /dev/null +++ b/doc/source/examples/figures.rst @@ -0,0 +1,22 @@ + +examples.figures +---------------- + +Plot selected figures from paper, Manis and Campagnola, Hearing Research, 2018. +To obtain the original results, this should be run after checking out the +"hearing-research-2018" tag available in the repository on github. + +From the main cnmodel directory:: + + $ ./examples figures.sh fignum + +where fignum is one of 2a, 2b, 2c, 3, 4, 5, 6a, 6b, or 7. + +Note that Figure 7 may take several **hours** to generate. + + + + +.. literalinclude:: ../../../examples/figures.py + :language: python + :linenos: diff --git a/doc/source/examples/play_test_sounds.rst b/doc/source/examples/play_test_sounds.rst new file mode 100644 index 0000000..23e57a6 --- /dev/null +++ b/doc/source/examples/play_test_sounds.rst @@ -0,0 +1,10 @@ + +examples.play_test_sounds +------------------------- + +.. automodule:: examples.play_test_sounds + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/plot_hcno_kinetics.rst b/doc/source/examples/plot_hcno_kinetics.rst new file mode 100644 index 0000000..8c6118b --- /dev/null +++ b/doc/source/examples/plot_hcno_kinetics.rst @@ -0,0 +1,10 @@ + +examples.plot_hcno_kinetics +--------------------------- + +.. automodule:: examples.plot_hcno_kinetics + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_an_model.rst b/doc/source/examples/test_an_model.rst new file mode 100644 index 0000000..0bbf84c --- /dev/null +++ b/doc/source/examples/test_an_model.rst @@ -0,0 +1,10 @@ + +examples.test_an_model +---------------------- + +.. automodule:: examples.test_an_model + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_bushy_variation.rst b/doc/source/examples/test_bushy_variation.rst new file mode 100644 index 0000000..1c17940 --- /dev/null +++ b/doc/source/examples/test_bushy_variation.rst @@ -0,0 +1,10 @@ + +examples.test_bushy_variation +----------------------------- + +.. automodule:: examples.test_bushy_variation + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_ccstim.rst b/doc/source/examples/test_ccstim.rst new file mode 100644 index 0000000..33d7512 --- /dev/null +++ b/doc/source/examples/test_ccstim.rst @@ -0,0 +1,10 @@ + +examples.test_ccstim +-------------------- + +.. automodule:: examples.test_ccstim + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_cells.rst b/doc/source/examples/test_cells.rst new file mode 100644 index 0000000..7ee35aa --- /dev/null +++ b/doc/source/examples/test_cells.rst @@ -0,0 +1,10 @@ + +examples.test_cells +------------------- + +.. automodule:: examples.test_cells + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_circuit.rst b/doc/source/examples/test_circuit.rst new file mode 100644 index 0000000..2e03092 --- /dev/null +++ b/doc/source/examples/test_circuit.rst @@ -0,0 +1,10 @@ + +examples.test_circuit +--------------------- + +.. automodule:: examples.test_circuit + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_decorator.rst b/doc/source/examples/test_decorator.rst new file mode 100644 index 0000000..48bb356 --- /dev/null +++ b/doc/source/examples/test_decorator.rst @@ -0,0 +1,10 @@ + +examples.test_decorator +----------------------- + +.. automodule:: examples.test_decorator + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_mechanisms.rst b/doc/source/examples/test_mechanisms.rst new file mode 100644 index 0000000..834d1ce --- /dev/null +++ b/doc/source/examples/test_mechanisms.rst @@ -0,0 +1,10 @@ + +examples.test_mechanisms +------------------------ + +.. automodule:: examples.test_mechanisms + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_mso_inputs.rst b/doc/source/examples/test_mso_inputs.rst new file mode 100644 index 0000000..6d27b5b --- /dev/null +++ b/doc/source/examples/test_mso_inputs.rst @@ -0,0 +1,10 @@ + +examples.test_mso_inputs +------------------------ + +.. automodule:: examples.test_mso_inputs + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_physiology.rst b/doc/source/examples/test_physiology.rst new file mode 100644 index 0000000..4ea046e --- /dev/null +++ b/doc/source/examples/test_physiology.rst @@ -0,0 +1,10 @@ + +examples.test_physiology +------------------------ + +.. automodule:: examples.test_physiology + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_populations.rst b/doc/source/examples/test_populations.rst new file mode 100644 index 0000000..e79b210 --- /dev/null +++ b/doc/source/examples/test_populations.rst @@ -0,0 +1,10 @@ + +examples.test_populations +------------------------- + +.. automodule:: examples.test_populations + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_sgc_input.rst b/doc/source/examples/test_sgc_input.rst new file mode 100644 index 0000000..b98447b --- /dev/null +++ b/doc/source/examples/test_sgc_input.rst @@ -0,0 +1,10 @@ + +examples.test_sgc_input +----------------------- + +.. automodule:: examples.test_sgc_input + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_sgc_input_PSTH.rst b/doc/source/examples/test_sgc_input_PSTH.rst new file mode 100644 index 0000000..9033450 --- /dev/null +++ b/doc/source/examples/test_sgc_input_PSTH.rst @@ -0,0 +1,10 @@ + +examples.test_sgc_input_PSTH +---------------------------- + +.. automodule:: examples.test_sgc_input_PSTH + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_sgc_input_phaselocking.rst b/doc/source/examples/test_sgc_input_phaselocking.rst new file mode 100644 index 0000000..2177ba6 --- /dev/null +++ b/doc/source/examples/test_sgc_input_phaselocking.rst @@ -0,0 +1,10 @@ + +examples.test_sgc_input_phaselocking +------------------------------------ + +.. automodule:: examples.test_sgc_input_phaselocking + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_simple_synapses.rst b/doc/source/examples/test_simple_synapses.rst new file mode 100644 index 0000000..e868225 --- /dev/null +++ b/doc/source/examples/test_simple_synapses.rst @@ -0,0 +1,10 @@ + +examples.test_simple_synapses +----------------------------- + +.. automodule:: examples.test_simple_synapses + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_sound_stim.rst b/doc/source/examples/test_sound_stim.rst new file mode 100644 index 0000000..549a16e --- /dev/null +++ b/doc/source/examples/test_sound_stim.rst @@ -0,0 +1,10 @@ + +examples.test_sound_stim +------------------------ + +.. automodule:: examples.test_sound_stim + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_sounds.rst b/doc/source/examples/test_sounds.rst new file mode 100644 index 0000000..fbbc88c --- /dev/null +++ b/doc/source/examples/test_sounds.rst @@ -0,0 +1,10 @@ + +examples.test_sounds +-------------------- + +.. automodule:: examples.test_sounds + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/test_synapses.rst b/doc/source/examples/test_synapses.rst new file mode 100644 index 0000000..623c2a8 --- /dev/null +++ b/doc/source/examples/test_synapses.rst @@ -0,0 +1,10 @@ + +examples.test_synapses +---------------------- + +.. automodule:: examples.test_synapses + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/examples/toy_model.rst b/doc/source/examples/toy_model.rst new file mode 100644 index 0000000..88cab37 --- /dev/null +++ b/doc/source/examples/toy_model.rst @@ -0,0 +1,10 @@ + +examples.toy_model +------------------ + +.. automodule:: examples.toy_model + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..71df961 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,32 @@ +.. CNModel documentation master file, created by + sphinx-quickstart on Sun Apr 2 15:20:59 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to CNModel's documentation! +=================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + readme + architecture + +.. toctree:: + :maxdepth: 1 + + examples + modules + +Indices and tables +================== + +* :ref:`modindex` +* :ref:`genindex` +* :ref:`search` + +.. automodule:: cnmodel + :members: + :show-inheritance: + :undoc-members: \ No newline at end of file diff --git a/doc/source/modules.rst b/doc/source/modules.rst new file mode 100644 index 0000000..5859ebf --- /dev/null +++ b/doc/source/modules.rst @@ -0,0 +1,18 @@ +API Reference +============= + +Contents: + +.. toctree:: + :maxdepth: 0 + + an_model + cells + data + decorator + morphology + synapses + populations + protocols + util + diff --git a/doc/source/morphology.rst b/doc/source/morphology.rst new file mode 100644 index 0000000..d5da5ff --- /dev/null +++ b/doc/source/morphology.rst @@ -0,0 +1,26 @@ +cnmodel.morphology package +-------------------------- + +.. automodule:: cnmodel.morphology + :members: + :undoc-members: + :show-inheritance: + +cnmodel.morphology.hoc_reader +============================= + +.. automodule:: cnmodel.morphology.hoc_reader + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.morphology.morphology +============================= + +.. automodule:: cnmodel.morphology.morphology + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/doc/source/populations.rst b/doc/source/populations.rst new file mode 100644 index 0000000..258c70f --- /dev/null +++ b/doc/source/populations.rst @@ -0,0 +1,54 @@ +cnmodel.populations package +--------------------------- + + +.. automodule:: cnmodel.populations + :members: + :undoc-members: + :show-inheritance: + + +cnmodel.populations.population +=============================== + +.. automodule:: cnmodel.populations.population + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.populations.bushy +========================= + +.. automodule:: cnmodel.populations.bushy + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.populations.dstellate +============================= + +.. automodule:: cnmodel.populations.dstellate + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.populations.sgc +======================= + +.. automodule:: cnmodel.populations.sgc + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.populations.tstellate +============================= + +.. automodule:: cnmodel.populations.tstellate + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/doc/source/protocols.rst b/doc/source/protocols.rst new file mode 100644 index 0000000..4dd47eb --- /dev/null +++ b/doc/source/protocols.rst @@ -0,0 +1,72 @@ +cnmodel.protocols package +========================= + +.. automodule:: cnmodel.protocols + :members: + :undoc-members: + :show-inheritance: + +cnmodel.protocols.cc +-------------------- + +.. automodule:: cnmodel.protocols.cc + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.protocols.iv_curve +-------------------------- + +.. automodule:: cnmodel.protocols.iv_curve + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.protocols.population_test +--------------------------------- + +.. automodule:: cnmodel.protocols.population_test + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.protocols.protocol +-------------------------- + +.. automodule:: cnmodel.protocols.protocol + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.protocols.simple_synapse_test +------------------------------------- + +.. automodule:: cnmodel.protocols.simple_synapse_test + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.protocols.synapse_test +------------------------------ + +.. automodule:: cnmodel.protocols.synapse_test + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.protocols.vc_curve +-------------------------- + +.. automodule:: cnmodel.protocols.vc_curve + :members: + :undoc-members: + :show-inheritance: + :noindex: + + diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..38ba804 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst \ No newline at end of file diff --git a/doc/source/synapses.rst b/doc/source/synapses.rst new file mode 100644 index 0000000..1481f04 --- /dev/null +++ b/doc/source/synapses.rst @@ -0,0 +1,83 @@ +cnmodel.synapses package +======================== + +.. automodule:: cnmodel.synapses + :members: + :undoc-members: + :show-inheritance: + + +cnmodel.synapses.exp2_psd +------------------------- + +.. automodule:: cnmodel.synapses.exp2_psd + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.synapses.glu_psd +------------------------ + +.. automodule:: cnmodel.synapses.glu_psd + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.synapses.gly_psd +------------------------- + +.. automodule:: cnmodel.synapses.gly_psd + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.synapses.psd +-------------------- + +.. automodule:: cnmodel.synapses.psd + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.synapses.simple_terminal +-------------------------------- + +.. automodule:: cnmodel.synapses.simple_terminal + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.synapses.stochastic_terminal +------------------------------------ + +.. automodule:: cnmodel.synapses.stochastic_terminal + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.synapses.synapse +------------------------ + +.. automodule:: cnmodel.synapses.synapse + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.synapses.terminal +------------------------- + +.. automodule:: cnmodel.synapses.terminal + :members: + :undoc-members: + :show-inheritance: + :noindex: + + + diff --git a/doc/source/util.rst b/doc/source/util.rst new file mode 100644 index 0000000..02a5d30 --- /dev/null +++ b/doc/source/util.rst @@ -0,0 +1,147 @@ +cnmodel.util package +-------------------- + +Utility routines used within cnmodel + +.. automodule:: cnmodel.util + :members: + :undoc-members: + :show-inheritance: + + +cnmodel.util.Params +==================== + +.. automodule:: cnmodel.util.Params + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.ccstim +=================== + +.. automodule:: cnmodel.util.ccstim + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.expfitting +======================= + +.. automodule:: cnmodel.util.expfitting + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.filelock +===================== + +.. automodule:: cnmodel.util.filelock + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.find_point +======================= + +.. automodule:: cnmodel.util.find_point + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.fitting +==================== + +.. automodule:: cnmodel.util.fitting + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.get_anspikes +========================= + +.. automodule:: cnmodel.util.get_anspikes + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.matlab_proc +======================== + +.. automodule:: cnmodel.util.matlab_proc + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.nrnutils +===================== + +.. automodule:: cnmodel.util.nrnutils + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.process +==================== + +.. automodule:: cnmodel.util.process + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.pynrnutilities +=========================== + +.. automodule:: cnmodel.util.pynrnutilities + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.random_seed +======================== + +.. automodule:: cnmodel.util.random_seed + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.sound +================== + +.. automodule:: cnmodel.util.sound + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.stim +================= + +.. automodule:: cnmodel.util.stim + :members: + :undoc-members: + :show-inheritance: + :noindex: + +cnmodel.util.user_tester +======================== + +.. automodule:: cnmodel.util.user_tester + :members: + :undoc-members: + :show-inheritance: + :noindex: + + diff --git a/examples/LC_bushy.hoc b/examples/LC_bushy.hoc new file mode 100644 index 0000000..8ac0584 --- /dev/null +++ b/examples/LC_bushy.hoc @@ -0,0 +1,955 @@ +proc celldef() { + topol() +} + +create hillock[1], initialsegment[1], unmyelinatedaxon[1], soma[1], primarydendrite[100] + +proc topol() { local i + connect soma(0), hillock(0) + connect initialsegment(0), hillock(1) + connect unmyelinatedaxon(0), initialsegment(1) + connect primarydendrite(0), soma(1) + for i = 1, 7 connect primarydendrite[i](0), primarydendrite[i-1](1) + for i = 8, 9 connect primarydendrite[i](0), primarydendrite[i-8](1) + for i = 10, 13 connect primarydendrite[i](0), primarydendrite[i-1](1) + connect primarydendrite[14](0), primarydendrite[12](1) + connect primarydendrite[15](0), primarydendrite[11](1) + connect primarydendrite[16](0), primarydendrite[4](1) + for i = 17, 18 connect primarydendrite[i](0), primarydendrite[i-1](1) + for i = 19, 22 connect primarydendrite[i](0), primarydendrite[16](1) + connect primarydendrite[23](0), primarydendrite[2](1) + for i = 24, 25 connect primarydendrite[i](0), primarydendrite[23](1) + connect primarydendrite[26](0), primarydendrite[17](1) + for i = 27, 28 connect primarydendrite[i](0), primarydendrite[i-1](1) + for i = 29, 30 connect primarydendrite[i](0), primarydendrite[27](1) + connect primarydendrite[31](0), primarydendrite[26](1) + connect primarydendrite[32](0), primarydendrite[2](1) + for i = 33, 35 connect primarydendrite[i](0), primarydendrite[i-1](1) + for i = 36, 37 connect primarydendrite[i](0), primarydendrite[i-3](1) + for i = 38, 39 connect primarydendrite[i](0), primarydendrite[i-1](1) + connect primarydendrite[40](0), primarydendrite[38](1) + for i = 41, 42 connect primarydendrite[i](0), primarydendrite[40](1) + for i = 43, 44 connect primarydendrite[i](0), primarydendrite[42](1) + connect primarydendrite[45](0), primarydendrite[37](1) + for i = 46, 47 connect primarydendrite[i](0), primarydendrite[i-1](1) + connect primarydendrite[48](0), primarydendrite[46](1) + connect primarydendrite[49](0), primarydendrite[45](1) + connect primarydendrite[50](0), primarydendrite[32](1) + for i = 51, 53 connect primarydendrite[i](0), primarydendrite[i-1](1) + connect primarydendrite[54](0), primarydendrite[52](1) + connect primarydendrite[55](0), primarydendrite[51](1) + for i = 56, 57 connect primarydendrite[i](0), primarydendrite[55](1) + connect primarydendrite[58](0), primarydendrite[50](1) + for i = 59, 60 connect primarydendrite[i](0), primarydendrite[58](1) + connect primarydendrite[61](0), primarydendrite[51](1) + for i = 62, 63 connect primarydendrite[i](0), primarydendrite[61](1) + connect primarydendrite[64](0), primarydendrite[3](1) + for i = 65, 69 connect primarydendrite[i](0), primarydendrite[i-1](1) + connect primarydendrite[70](0), primarydendrite[68](1) + connect primarydendrite[71](0), primarydendrite[67](1) + connect primarydendrite[72](0), primarydendrite[65](1) + connect primarydendrite[73](0), primarydendrite[64](1) + connect primarydendrite[74](0), primarydendrite[66](1) + for i = 75, 76 connect primarydendrite[i](0), primarydendrite[74](1) + for i = 77, 78 connect primarydendrite[i](0), primarydendrite[76](1) + connect primarydendrite[79](0), primarydendrite[64](1) + for i = 80, 81 connect primarydendrite[i](0), primarydendrite[5](1) + for i = 82, 84 connect primarydendrite[i](0), primarydendrite[81](1) + for i = 85, 87 connect primarydendrite[i](0), primarydendrite[i-1](1) + connect primarydendrite[88](0), primarydendrite[86](1) + for i = 89, 90 connect primarydendrite[i](0), primarydendrite[88](1) + for i = 91, 92 connect primarydendrite[i](0), primarydendrite[i-7](1) + for i = 93, 94 connect primarydendrite[i](0), primarydendrite[92](1) + connect primarydendrite[95](0), primarydendrite[6](1) + for i = 96, 97 connect primarydendrite[i](0), primarydendrite[95](1) + for i = 98, 99 connect primarydendrite[i](0), primarydendrite[i-89](1) + basic_shape() +} +proc shape3d_1() { + hillock[0] {pt3dclear() + pt3dadd(61.38639625, 55.43400875, 0.0, 3.51044933) + pt3dadd(63.01466225, 55.28039875, 0.0, 1.23072332) + pt3dadd(63.963204, 55.2919195, 0.0, 0.67265819) + } + initialsegment[0] {pt3dclear() + pt3dadd(63.963204, 55.2919195, 0.0, 0.67265819) + pt3dadd(64.65060875, 55.35720375, 0.0, 0.30722) + } + unmyelinatedaxon[0] {pt3dclear() + pt3dadd(64.65060875, 55.35720375, 0.0, 0.30722) + pt3dadd(65.18824375, 55.5069735, 0.0, 0.370461237) + pt3dadd(66.24047225, 55.5991395, 0.0, 0.480123416) + pt3dadd(67.9493835, 55.75658975, 0.0, 0.489724041) + pt3dadd(69.13602075, 55.81803375, 0.0, 0.4211141345) + pt3dadd(69.953994, 55.714347, 0.0, 0.6103002105) + pt3dadd(71.83187625, 55.83339475, 0.0, 0.5677348795) + pt3dadd(73.901771, 55.51081375, 0.0, 0.615115884) + pt3dadd(75.50315525, 55.4301685, 0.0, 0.4713138825) + pt3dadd(76.15599775, 55.1843925, 0.0, 0.5377194855) + pt3dadd(77.03157475, 55.69514575, 0.0, 0.4454766805) + pt3dadd(77.49240475, 56.41711275, 0.0, 0.5473201105) + pt3dadd(77.7650625, 57.36949475, 0.0, 0.6737565015) + pt3dadd(77.910992, 57.8111235, 0.0, 0.296375134) + pt3dadd(78.54463325, 58.75198475, 0.0, 0.3572891795) + pt3dadd(79.0707475, 59.8080535, 0.0, 0.3138943545) + pt3dadd(79.50469575, 61.01773225, 0.0, 0.340922034) + pt3dadd(80.0077685, 62.10836325, 0.0, 0.3778575585) + pt3dadd(80.5300425, 63.10682825, 0.0, 0.3418667355) + pt3dadd(81.167524, 64.25890325, 0.0, 0.3259988225) + pt3dadd(81.67827725, 64.97318975, 0.0, 0.4713138825) + pt3dadd(82.043101, 65.5684285, 0.0, 0.487465974) + pt3dadd(82.442487, 66.37104075, 0.0, 0.489539709) + pt3dadd(82.57689575, 67.296541, 0.0, 0.5703308885) + pt3dadd(82.1583085, 67.6268025, 0.0, 0.4573200115) + pt3dadd(82.165989, 68.12987525, 0.0, 0.62411743) + pt3dadd(82.3119185, 68.64446875, 0.0, 0.7671513815) + pt3dadd(82.31575875, 69.04385475, 0.0, 0.251275238) + pt3dadd(82.5423335, 69.44324075, 0.0, 0.428172514) + pt3dadd(83.2028565, 70.04231975, 0.0, 0.4665980555) + pt3dadd(83.42175075, 70.983181, 0.0, 0.480891466) + pt3dadd(83.179815, 71.981646, 0.0, 0.56251982) + pt3dadd(82.396404, 72.75353625, 0.0, 0.5373815435) + pt3dadd(81.3748975, 72.949389, 0.0, 0.413272344) + pt3dadd(80.276586, 72.81498025, 0.0, 0.489724041) + pt3dadd(79.4394115, 73.7097585, 0.0, 0.556667279) + pt3dadd(78.8403325, 75.00392275, 0.0, 0.639509152) + } + soma[0] {pt3dclear() + pt3dadd(61.38639625, 55.43400875, 0.0, 3.51044933) + pt3dadd(59.3318625, 55.60682, 0.0, 8.66437205) + pt3dadd(57.1813225, 55.81803375, 0.0, 13.06222635) + pt3dadd(55.7220275, 56.144455, 0.0, 13.7619199) + pt3dadd(53.3410725, 56.81649875, 0.0, 13.8325805) + pt3dadd(50.84491, 58.16058625, 0.0, 13.90708135) + pt3dadd(47.44628875, 58.448605, 0.0, 11.5038529) + pt3dadd(44.56610125, 58.26811325, 0.0, 6.441097715) + pt3dadd(42.37715875, 57.6421525, 0.0, 2.400693885) + } + primarydendrite[0] {pt3dclear() + pt3dadd(42.37715875, 57.6421525, 0.0, 1.46344247) + pt3dadd(40.8525795, 57.23892625, 0.0, 1.46344247) + pt3dadd(39.68130325, 56.7204925, 0.0, 1.48648397) + pt3dadd(37.864865, 55.5991395, 0.0, 2.855533095) + pt3dadd(36.38636875, 54.62755625, 0.0, 3.01428903) + } + primarydendrite[1] {pt3dclear() + pt3dadd(36.38636875, 54.62755625, 0.0, 3.01428903) + pt3dadd(35.02308, 53.2949895, 0.0, 2.006223405) + pt3dadd(32.57991295, 48.5177185, 0.0, 1.446929395) + pt3dadd(32.286901875, 47.93784075, 0.0, 1.488557705) + } + primarydendrite[2] {pt3dclear() + pt3dadd(32.286901875, 47.93784075, 0.0, 1.488557705) + pt3dadd(31.634059375, 46.48622625, 0.0, 0.72381032) + pt3dadd(30.827606875, 44.8925225, 0.0, 0.704194323) + pt3dadd(30.405179375, 43.82877325, 0.0, 1.114670965) + pt3dadd(29.886745625, 42.76118375, 0.0, 1.479955545) + pt3dadd(29.445116875, 41.37101325, 0.0, 0.6679193215) + } + primarydendrite[3] {pt3dclear() + pt3dadd(29.445116875, 41.37101325, 0.0, 0.6679193215) + pt3dadd(28.6866675, 40.3610275, 0.0, 0.7336797625) + pt3dadd(27.601796875, 39.76578875, 0.0, 0.5964753105) + pt3dadd(26.57453, 38.970857, 0.0, 2.474273075) + } + primarydendrite[4] {pt3dclear() + pt3dadd(26.57453, 38.970857, 0.0, 2.474273075) + pt3dadd(26.17130375, 38.6713175, 0.0, 1.76528612) + } + primarydendrite[5] {pt3dclear() + pt3dadd(26.17130375, 38.6713175, 0.0, 1.76528612) + pt3dadd(25.912086875, 38.008874375, 0.0, 1.27742076) + pt3dadd(24.8464175, 37.394434375, 0.0, 0.4675811595) + } + primarydendrite[6] {pt3dclear() + pt3dadd(24.8464175, 37.394434375, 0.0, 0.4675811595) + pt3dadd(24.0015625, 36.799195625, 0.0, 0.3461524545) + pt3dadd(23.339119375, 36.472774375, 0.0, 1.634948035) + pt3dadd(22.79188375, 35.819931875, 0.0, 0.686160509) + pt3dadd(21.87022375, 35.243894375, 0.0, 0.3958452895) + pt3dadd(21.21738125, 34.552649375, 0.0, 1.3794178) + } + primarydendrite[7] {pt3dclear() + pt3dadd(21.21738125, 34.552649375, 0.0, 1.3794178) + pt3dadd(20.266919375, 33.938209375, 0.0, 0.480415275) + pt3dadd(19.095643125, 34.187825625, 0.0, 0.317834451) + pt3dadd(17.81876, 34.629454375, 0.0, 0.5170128575) + pt3dadd(17.05071, 34.96547625, 0.0, 0.4241709735) + pt3dadd(15.370600625, 35.407105, 0.0, 0.771199005) + pt3dadd(14.80416375, 35.282296875, 0.0, 1.237405355) + pt3dadd(14.13212, 34.437441875, 0.0, 0.638280272) + pt3dadd(13.2488625, 34.015014375, 0.0, 0.3688406515) + pt3dadd(12.519215, 33.592586875, 0.0, 0.298080205) + pt3dadd(11.61675625, 33.458178125, 0.0, 0.558157296) + pt3dadd(11.15592625, 33.563785, 0.0, 0.1920125) + } + primarydendrite[8] {pt3dclear() + pt3dadd(36.38636875, 54.62755625, 0.0, 3.01428903) + pt3dadd(37.754649825, 52.4501345, 0.0, 0.96451719) + pt3dadd(38.2058792, 50.7604245, 0.0, 0.30737361) + pt3dadd(38.61371375, 49.3472125, 0.0, 0.4036793995) + pt3dadd(39.1935915, 48.7634945, 0.0, 0.407435164) + pt3dadd(41.62831, 45.65673225, 0.0, 0.6537564795) + pt3dadd(43.0645635, 45.5376845, 0.0, 0.480123416) + } + primarydendrite[9] {pt3dclear() + pt3dadd(32.286901875, 47.93784075, 0.0, 1.488557705) + pt3dadd(33.64059, 46.5054275, 0.0, 0.384025) + pt3dadd(35.02308, 45.7373775, 0.0, 0.537635) + pt3dadd(36.09835, 45.8909875, 0.0, 0.7081036975) + } + primarydendrite[10] {pt3dclear() + pt3dadd(36.09835, 45.8909875, 0.0, 0.7081036975) + pt3dadd(37.096815, 46.467025, 0.0, 0.61444) + pt3dadd(38.4409025, 46.4286225, 0.0, 0.584931519) + pt3dadd(39.63138, 46.7358425, 0.0, 0.6192249515) + pt3dadd(40.629845, 48.54076, 0.0, 0.89569991) + pt3dadd(41.21740325, 48.9017435, 0.0, 0.6418056215) + } + primarydendrite[11] {pt3dclear() + pt3dadd(41.21740325, 48.9017435, 0.0, 0.6418056215) + pt3dadd(41.659032, 49.5008225, 0.0, 0.695761134) + } + primarydendrite[12] {pt3dclear() + pt3dadd(41.659032, 49.5008225, 0.0, 0.695761134) + pt3dadd(42.28499275, 50.092221, 0.0, 0.597558261) + } + primarydendrite[13] {pt3dclear() + pt3dadd(42.28499275, 50.092221, 0.0, 0.597558261) + pt3dadd(43.241215, 50.17286625, 0.0, 0.489539709) + pt3dadd(44.7389125, 50.57225225, 0.0, 0.355223125) + } + primarydendrite[14] {pt3dclear() + pt3dadd(42.28499275, 50.092221, 0.0, 0.597558261) + pt3dadd(42.580692, 50.937076, 0.0, 0.347880567) + pt3dadd(43.17593075, 51.41710725, 0.0, 0.401966648) + pt3dadd(43.70588525, 51.96242275, 0.0, 0.4129420825) + pt3dadd(44.070709, 52.626786, 0.0, 0.428710149) + pt3dadd(44.38944975, 53.19130275, 0.0, 0.418264669) + } + primarydendrite[15] {pt3dclear() + pt3dadd(41.659032, 49.5008225, 0.0, 0.695761134) + pt3dadd(42.765024, 49.48162125, 0.0, 0.441736277) + pt3dadd(45.222784, 49.477781, 0.0, 0.489724041) + pt3dadd(45.83338375, 49.18976225, 0.0, 0.355223125) + pt3dadd(46.77808525, 49.40481625, 0.0, 0.3085640875) + pt3dadd(47.5038925, 48.94398625, 0.0, 0.399554971) + pt3dadd(48.2719425, 49.0707145, 0.0, 0.5240942785) + pt3dadd(49.861806, 49.324171, 0.0, 0.3553536935) + } + primarydendrite[16] {pt3dclear() + pt3dadd(26.17130375, 38.6713175, 0.0, 1.76528612) + pt3dadd(26.78574375, 37.548044375, 0.0, 0.6454615395) + pt3dadd(27.467388125, 36.261560625, 0.0, 0.618326333) + pt3dadd(28.1828267, 35.527304825, 0.0, 0.652980749) + pt3dadd(28.57146, 34.792665, 0.0, 1.174886085) + } + primarydendrite[17] {pt3dclear() + pt3dadd(28.57146, 34.792665, 0.0, 1.174886085) + pt3dadd(28.72507, 33.65979125, 0.0, 0.4241709735) + pt3dadd(29.03229, 32.41171, 0.0, 0.462427544) + pt3dadd(29.3011075, 31.47084875, 0.0, 0.32811096) + pt3dadd(29.51232125, 31.077223125, 0.0, 0.5240942785) + pt3dadd(30.193965625, 29.560324375, 0.0, 0.631598237) + pt3dadd(30.616393125, 29.03229, 0.0, 0.3102077145) + pt3dadd(31.000418125, 28.427450625, 0.0, 0.455192513) + } + primarydendrite[18] {pt3dclear() + pt3dadd(31.000418125, 28.427450625, 0.0, 0.455192513) + pt3dadd(32.027685, 26.776143125, 0.0, 0.3418667355) + } + primarydendrite[19] {pt3dclear() + pt3dadd(28.57146, 34.792665, 0.0, 1.174886085) + pt3dadd(28.97468625, 35.915938125, 0.0, 0.618326333) + pt3dadd(29.233903125, 36.338365625, 0.0, 0.518787053) + pt3dadd(29.6851325, 37.2120225, 0.0, 0.485760903) + } + primarydendrite[20] {pt3dclear() + pt3dadd(28.57146, 34.792665, 0.0, 1.174886085) + pt3dadd(29.099494375, 34.25503, 0.0, 0.602627391) + pt3dadd(29.32030875, 33.73659625, 0.0, 0.4640174075) + pt3dadd(29.387513125, 33.266165625, 0.0, 0.2886562315) + } + primarydendrite[21] {pt3dclear() + pt3dadd(28.57146, 34.792665, 0.0, 1.174886085) + pt3dadd(27.928218125, 34.187825625, 0.0, 0.678864034) + pt3dadd(27.870614375, 33.39097375, 0.0, 0.3728191505) + pt3dadd(28.043425625, 32.651725625, 0.0, 0.256175397) + pt3dadd(28.081828125, 31.806870625, 0.0, 0.3314673385) + pt3dadd(28.033825, 30.942814375, 0.0, 0.4432954185) + pt3dadd(27.524991875, 30.030755, 0.0, 0.5607917075) + pt3dadd(27.313778125, 29.6083275, 0.0, 1.345854015) + pt3dadd(26.968155625, 28.87868, 0.0, 1.073349875) + pt3dadd(26.67053625, 28.16823375, 0.0, 0.570891565) + pt3dadd(26.3825175, 27.74580625, 0.0, 0.4640174075) + pt3dadd(26.161703125, 27.332979375, 0.0, 0.34024615) + pt3dadd(25.969690625, 26.958555, 0.0, 0.261167722) + pt3dadd(25.729675, 26.488124375, 0.0, 0.3125733085) + pt3dadd(25.624068125, 26.28651125, 0.0, 0.1770278445) + } + primarydendrite[22] {pt3dclear() + pt3dadd(28.57146, 34.792665, 0.0, 1.174886085) + pt3dadd(27.55379375, 34.859869375, 0.0, 0.51843375) + pt3dadd(27.025759375, 35.00387875, 0.0, 0.815745905) + pt3dadd(26.6897375, 35.11908625, 0.0, 0.78325739) + pt3dadd(26.57453, 34.667856875, 0.0, 0.406866807) + pt3dadd(25.9984925, 34.2166275, 0.0, 0.899770575) + pt3dadd(25.854483125, 33.62138875, 0.0, 0.40322625) + pt3dadd(25.624068125, 32.632524375, 0.0, 0.498495172) + pt3dadd(26.036895, 31.557254375, 0.0, 1.00015471) + pt3dadd(26.05609625, 30.635594375, 0.0, 1.121737025) + pt3dadd(25.662470625, 30.385978125, 0.0, 0.5101925735) + pt3dadd(25.422455, 29.541123125, 0.0, 0.5240942785) + pt3dadd(25.144036875, 29.291506875, 0.0, 0.81689798) + pt3dadd(24.913621875, 28.465853125, 0.0, 0.335883626) + pt3dadd(24.500795, 28.101029375, 0.0, 0.4753999085) + pt3dadd(24.4193817, 27.81800295, 0.0, 0.5671665225) + pt3dadd(24.42399, 25.638661075, 0.0, 0.3457530685) + pt3dadd(25.0768325, 24.606401875, 0.0, 0.4648161795) + pt3dadd(25.067231875, 23.051100625, 0.0, 0.3461524545) + } + primarydendrite[23] {pt3dclear() + pt3dadd(29.445116875, 41.37101325, 0.0, 0.6679193215) + pt3dadd(29.848343125, 40.48775575, 0.0, 0.515223301) + pt3dadd(30.347575625, 39.3164795, 0.0, 0.489539709) + pt3dadd(30.31877375, 38.311486075, 0.0, 0.45530004) + pt3dadd(30.2995725, 38.085679375, 0.0, 0.542074329) + } + primarydendrite[24] {pt3dclear() + pt3dadd(30.2995725, 38.085679375, 0.0, 0.542074329) + pt3dadd(29.51232125, 38.87485075, 0.0, 0.4353230595) + } + primarydendrite[25] {pt3dclear() + pt3dadd(30.2995725, 38.085679375, 0.0, 0.542074329) + pt3dadd(30.405179375, 37.759258125, 0.0, 0.549171111) + pt3dadd(30.789204375, 37.140209825, 0.0, 0.5485797125) + pt3dadd(31.168621075, 36.525769825, 0.0, 0.476759357) + pt3dadd(31.1444275, 35.253495, 0.0, 0.5198546425) + } +} +proc shape3d_2() { + primarydendrite[25] {pt3dclear() + pt3dadd(30.962015625, 34.5960442, 0.0, 0.529078923) + pt3dadd(30.904411875, 33.27115795, 0.0, 0.601475316) + pt3dadd(30.50617795, 32.128683575, 0.0, 0.6625122495) + pt3dadd(30.746193575, 31.730065625, 0.0, 0.528034375) + pt3dadd(31.4516475, 31.518851875, 0.0, 0.558157296) + pt3dadd(32.455104825, 30.707791075, 0.0, 0.5432648065) + pt3dadd(32.939744375, 30.213166875, 0.0, 0.638280272) + pt3dadd(33.73659625, 29.632521075, 0.0, 0.688372493) + pt3dadd(34.029607325, 29.1474975, 0.0, 0.903149995) + pt3dadd(34.164016075, 28.773073125, 0.0, 0.5725889555) + } + primarydendrite[26] {pt3dclear() + pt3dadd(31.000418125, 28.427450625, 0.0, 0.455192513) + pt3dadd(31.77806875, 28.41785, 0.0, 0.4687639565) + } + primarydendrite[27] {pt3dclear() + pt3dadd(31.77806875, 28.41785, 0.0, 0.4687639565) + pt3dadd(32.31570375, 27.78420875, 0.0, 0.5430958355) + pt3dadd(32.786134375, 27.236973125, 0.0, 0.6567825965) + pt3dadd(33.410175, 26.603331875, 0.0, 0.81029275) + pt3dadd(33.6789925, 25.48005875, 0.0, 0.884102355) + pt3dadd(33.81340125, 24.28958125, 0.0, 0.76805) + pt3dadd(33.919008125, 23.569534375, 0.0, 0.63625262) + pt3dadd(34.10142, 23.13750625, 0.0, 0.80737416) + pt3dadd(34.85026875, 22.494264375, 0.0, 0.5647241235) + pt3dadd(35.051881875, 21.98543125, 0.0, 0.5324660235) + pt3dadd(35.301498125, 21.361390625, 0.0, 0.42503887) + pt3dadd(35.762328125, 20.324523125, 0.0, 0.61981635) + pt3dadd(36.04074625, 19.7772875, 0.0, 0.276928108) + pt3dadd(36.77039375, 18.961234375, 0.0, 0.55683625) + pt3dadd(37.240824375, 18.37559625, 0.0, 0.975500305) + } + primarydendrite[28] {pt3dclear() + pt3dadd(37.240824375, 18.37559625, 0.0, 0.975500305) + pt3dadd(37.336830625, 17.72275375, 0.0, 0.4936641375) + pt3dadd(37.65365125, 16.7818925, 0.0, 0.5498393145) + pt3dadd(38.277691875, 15.57221375, 0.0, 0.399554971) + pt3dadd(38.72124075, 14.621751875, 0.0, 0.598019091) + pt3dadd(39.0860645, 13.469676875, 0.0, 0.498495172) + pt3dadd(39.247355, 12.13519, 0.0, 0.5760375) + pt3dadd(39.60449825, 10.61829125, 0.0, 1.00599189) + pt3dadd(39.80419125, 9.50461875, 0.0, 0.5051080825) + pt3dadd(40.476235, 8.496553125, 0.0, 0.506928361) + pt3dadd(41.4670195, 7.574893125, 0.0, 0.598019091) + pt3dadd(42.734302, 6.3364125, 0.0, 0.6451696805) + pt3dadd(44.009265, 4.91552, 0.0, 0.5457993715) + pt3dadd(44.9232445, 3.273813125, 0.0, 0.5598086035) + } + primarydendrite[29] {pt3dclear() + pt3dadd(37.240824375, 18.37559625, 0.0, 0.975500305) + pt3dadd(37.92246875, 18.500404375, 0.0, 0.44162875) + pt3dadd(38.52922825, 18.558008125, 0.0, 0.6102234055) + pt3dadd(39.4700895, 18.817225, 0.0, 0.4263368745) + pt3dadd(40.25734075, 18.730819375, 0.0, 0.6102234055) + pt3dadd(40.68744875, 18.60601125, 0.0, 0.82903317) + pt3dadd(41.3594925, 18.385196875, 0.0, 0.410476642) + } + primarydendrite[30] {pt3dclear() + pt3dadd(37.240824375, 18.37559625, 0.0, 0.975500305) + pt3dadd(36.626384375, 17.962769375, 0.0, 0.4353230595) + pt3dadd(36.069548125, 18.154781875, 0.0, 0.535576626) + pt3dadd(35.08068375, 18.7020175, 0.0, 0.500707156) + pt3dadd(34.303033125, 19.690881875, 0.0, 0.862596955) + pt3dadd(33.976611875, 21.37099125, 0.0, 0.3728191505) + } + primarydendrite[31] {pt3dclear() + pt3dadd(31.77806875, 28.41785, 0.0, 0.4687639565) + pt3dadd(32.632524375, 28.36024625, 0.0, 0.4228652885) + pt3dadd(33.12215625, 28.465853125, 0.0, 0.4193629805) + pt3dadd(33.6021875, 28.033825, 0.0, 0.462427544) + pt3dadd(34.437441875, 27.13136625, 0.0, 0.4228652885) + pt3dadd(35.109485625, 26.28651125, 0.0, 0.7258456525) + pt3dadd(35.762328125, 25.278445625, 0.0, 1.11336528) + pt3dadd(36.280761875, 24.63520375, 0.0, 0.2930955605) + } + primarydendrite[32] {pt3dclear() + pt3dadd(29.445116875, 41.37101325, 0.0, 0.6679193215) + pt3dadd(30.24196875, 40.890982, 0.0, 0.32642125) + pt3dadd(31.365241875, 40.0538075, 0.0, 0.3958452895) + pt3dadd(31.874075, 39.093745, 0.0, 0.5498393145) + pt3dadd(33.400574375, 38.11448125, 0.0, 0.5170128575) + pt3dadd(34.5238475, 37.548044375, 0.0, 0.506928361) + pt3dadd(35.531913125, 36.55918, 0.0, 0.686160509) + pt3dadd(36.357566875, 36.55918, 0.0, 0.931414235) + } + primarydendrite[33] {pt3dclear() + pt3dadd(36.357566875, 36.55918, 0.0, 0.931414235) + pt3dadd(37.9800725, 37.02001, 0.0, 0.5760375) + pt3dadd(39.201272, 37.404035, 0.0, 0.6440329665) + pt3dadd(40.41095075, 37.548044375, 0.0, 0.442043497) + } + primarydendrite[34] {pt3dclear() + pt3dadd(40.41095075, 37.548044375, 0.0, 0.442043497) + pt3dadd(41.63983075, 37.125616875, 0.0, 0.647742648) + } + primarydendrite[35] {pt3dclear() + pt3dadd(41.63983075, 37.125616875, 0.0, 0.647742648) + pt3dadd(42.4654845, 38.008874375, 0.0, 0.718956244) + } + primarydendrite[36] {pt3dclear() + pt3dadd(40.41095075, 37.548044375, 0.0, 0.442043497) + pt3dadd(40.4685545, 37.864865, 0.0, 0.3286716365) + pt3dadd(40.41863125, 38.510027, 0.0, 0.499601164) + pt3dadd(40.4685545, 39.14366825, 0.0, 0.751060734) + pt3dadd(41.01387, 39.95780125, 0.0, 0.5223277635) + pt3dadd(41.48622075, 40.67976825, 0.0, 0.6875045965) + } + primarydendrite[37] {pt3dclear() + pt3dadd(41.63983075, 37.125616875, 0.0, 0.647742648) + pt3dadd(42.6190945, 36.856799375, 0.0, 0.442043497) + pt3dadd(43.02232075, 36.568780625, 0.0, 0.4519436615) + } + primarydendrite[38] {pt3dclear() + pt3dadd(43.02232075, 36.568780625, 0.0, 0.4519436615) + pt3dadd(43.702045, 36.607183125, 0.0, 0.5607917075) + pt3dadd(44.577622, 36.607183125, 0.0, 0.572827051) + } + primarydendrite[39] {pt3dclear() + pt3dadd(44.577622, 36.607183125, 0.0, 0.572827051) + pt3dadd(45.82570325, 35.92553875, 0.0, 0.3418667355) + pt3dadd(46.54383, 35.2918975, 0.0, 0.5430958355) + pt3dadd(46.93937575, 34.77346375, 0.0, 0.99201338) + pt3dadd(47.6882245, 34.264630625, 0.0, 0.489539709) + } + primarydendrite[40] {pt3dclear() + pt3dadd(44.577622, 36.607183125, 0.0, 0.572827051) + pt3dadd(45.1536595, 38.239289375, 0.0, 0.6102234055) + pt3dadd(45.51848325, 39.093745, 0.0, 0.864901105) + } + primarydendrite[41] {pt3dclear() + pt3dadd(45.51848325, 39.093745, 0.0, 0.864901105) + pt3dadd(45.97931325, 39.42016625, 0.0, 0.4753999085) + pt3dadd(46.5054275, 39.95780125, 0.0, 0.7603310975) + pt3dadd(46.85105, 40.30342375, 0.0, 0.4344782045) + } + primarydendrite[42] {pt3dclear() + pt3dadd(45.51848325, 39.093745, 0.0, 0.864901105) + pt3dadd(44.777315, 39.41248575, 0.0, 0.3958452895) + pt3dadd(44.22047875, 40.14981375, 0.0, 0.787942495) + } + primarydendrite[43] {pt3dclear() + pt3dadd(44.22047875, 40.14981375, 0.0, 0.787942495) + pt3dadd(43.62524, 40.87946125, 0.0, 0.4082262555) + } + primarydendrite[44] {pt3dclear() + pt3dadd(44.22047875, 40.14981375, 0.0, 0.787942495) + pt3dadd(43.963182, 39.56609575, 0.0, 0.4073199565) + pt3dadd(43.5100325, 39.01694, 0.0, 0.896544765) + pt3dadd(43.16441, 38.258490625, 0.0, 0.3418667355) + } + primarydendrite[45] {pt3dclear() + pt3dadd(43.02232075, 36.568780625, 0.0, 0.4519436615) + pt3dadd(43.3103395, 36.146353125, 0.0, 0.678864034) + pt3dadd(43.36794325, 35.560715, 0.0, 0.7114831175) + pt3dadd(42.933995, 35.253495, 0.0, 0.364316837) + pt3dadd(43.13752825, 34.792665, 0.0, 0.78817291) + pt3dadd(43.49083125, 34.6006525, 0.0, 1.08417938) + pt3dadd(44.039987, 34.35103625, 0.0, 0.3418667355) + pt3dadd(44.41249125, 33.938209375, 0.0, 0.6989331805) + pt3dadd(45.4301575, 33.4485775, 0.0, 0.384025) + pt3dadd(46.159805, 33.00694875, 0.0, 0.4992325) + pt3dadd(47.3041995, 32.901341875, 0.0, 0.640876281) + } + primarydendrite[46] {pt3dclear() + pt3dadd(47.3041995, 32.901341875, 0.0, 0.640876281) + pt3dadd(48.01464575, 33.16055875, 0.0, 0.4332032415) + } + primarydendrite[47] {pt3dclear() + pt3dadd(48.01464575, 33.16055875, 0.0, 0.4332032415) + pt3dadd(48.16825575, 33.2181625, 0.0, 0.3102077145) + pt3dadd(48.71357125, 33.362171875, 0.0, 0.4432954185) + pt3dadd(49.44321875, 33.42937625, 0.0, 0.462427544) + pt3dadd(50.184387, 33.256565, 0.0, 0.528295512) + pt3dadd(51.30574, 32.96854625, 0.0, 0.3456225) + } + primarydendrite[48] {pt3dclear() + pt3dadd(48.01464575, 33.16055875, 0.0, 0.4332032415) + pt3dadd(48.28346325, 32.9109425, 0.0, 0.261167722) + pt3dadd(48.47547575, 32.344505625, 0.0, 0.442043497) + } + primarydendrite[49] {pt3dclear() + pt3dadd(47.3041995, 32.901341875, 0.0, 0.640876281) + pt3dadd(47.5038925, 32.267700625, 0.0, 0.4432954185) + pt3dadd(47.6882245, 31.720465, 0.0, 0.5324660235) + pt3dadd(48.48315625, 31.12522625, 0.0, 0.6869669615) + pt3dadd(49.6083495, 30.39557875, 0.0, 0.352488867) + pt3dadd(50.31879575, 29.85794375, 0.0, 0.686160509) + pt3dadd(51.44014875, 29.55072375, 0.0, 0.577312463) + pt3dadd(52.24660125, 29.416315, 0.0, 0.2688175) + } + primarydendrite[50] {pt3dclear() + pt3dadd(36.357566875, 36.55918, 0.0, 0.931414235) + pt3dadd(36.453573125, 35.781529375, 0.0, 0.5216211575) + pt3dadd(36.69358875, 35.147888125, 0.0, 0.7145860395) + } + primarydendrite[51] {pt3dclear() + pt3dadd(36.69358875, 35.147888125, 0.0, 0.7145860395) + pt3dadd(37.250425, 34.86947, 0.0, 0.4687639565) + pt3dadd(38.6713175, 34.226228125, 0.0, 0.498126508) + pt3dadd(39.30495875, 33.611788125, 0.0, 0.6230805625) + pt3dadd(39.87331575, 33.33337, 0.0, 1.143242425) + } + primarydendrite[52] {pt3dclear() + pt3dadd(39.87331575, 33.33337, 0.0, 1.143242425) + pt3dadd(40.44935325, 33.02615, 0.0, 0.7031420945) + pt3dadd(40.94858575, 32.478914375, 0.0, 0.381137132) + } + primarydendrite[53] {pt3dclear() + pt3dadd(40.94858575, 32.478914375, 0.0, 0.381137132) + pt3dadd(41.41709625, 32.2965025, 0.0, 0.2924657595) + pt3dadd(42.22354875, 31.806870625, 0.0, 0.4228652885) + pt3dadd(43.22201375, 30.731600625, 0.0, 0.4482570215) + pt3dadd(43.502352, 29.569925, 0.0, 0.3353383105) + pt3dadd(43.502352, 28.811475625, 0.0, 0.4036793995) + } + primarydendrite[54] {pt3dclear() + pt3dadd(40.94858575, 32.478914375, 0.0, 0.381137132) + pt3dadd(40.7757745, 31.81647125, 0.0, 0.3728191505) + pt3dadd(41.18668125, 31.096424375, 0.0, 0.405054209) + pt3dadd(41.4362975, 30.47238375, 0.0, 0.395376779) + pt3dadd(41.9278495, 29.6083275, 0.0, 0.48156735) + pt3dadd(42.06993875, 29.022689375, 0.0, 0.808526235) + pt3dadd(42.196667, 28.3794475, 0.0, 0.32642125) + } + primarydendrite[55] {pt3dclear() + pt3dadd(39.87331575, 33.33337, 0.0, 1.143242425) + pt3dadd(39.56609575, 32.690128125, 0.0, 0.4742324725) + pt3dadd(39.3932845, 32.2196975, 0.0, 0.9416293) + pt3dadd(38.7481225, 32.056486875, 0.0, 0.5915137075) + pt3dadd(38.7481225, 31.33644, 0.0, 0.46083) + } + primarydendrite[56] {pt3dclear() + pt3dadd(38.7481225, 31.33644, 0.0, 0.46083) + pt3dadd(38.92093375, 30.654795625, 0.0, 0.3958452895) + pt3dadd(39.26655625, 30.059556875, 0.0, 0.4211141345) + pt3dadd(39.57377625, 29.12829625, 0.0, 0.2924657595) + pt3dadd(40.34182625, 28.39864875, 0.0, 0.4378576245) + pt3dadd(40.7450525, 28.043425625, 0.0, 0.89339576) + pt3dadd(41.0522725, 27.55379375, 0.0, 0.395376779) + } + primarydendrite[57] {pt3dclear() + pt3dadd(38.7481225, 31.33644, 0.0, 0.46083) + pt3dadd(38.2872925, 31.39404375, 0.0, 0.6528425) + pt3dadd(37.80726125, 31.566855, 0.0, 0.581137352) + } + primarydendrite[58] {pt3dclear() + pt3dadd(36.69358875, 35.147888125, 0.0, 0.7145860395) + pt3dadd(36.511176875, 34.6774575, 0.0, 0.456406032) + pt3dadd(36.280761875, 34.053416875, 0.0, 0.678864034) + pt3dadd(35.9831425, 33.381373125, 0.0, 0.659862477) + pt3dadd(36.04074625, 32.85333875, 0.0, 0.6539715335) + pt3dadd(36.146353125, 32.2965025, 0.0, 0.32642125) + } + primarydendrite[59] {pt3dclear() + pt3dadd(36.146353125, 32.2965025, 0.0, 0.32642125) + pt3dadd(36.175155, 31.9124775, 0.0, 0.4241709735) + pt3dadd(36.069548125, 31.346040625, 0.0, 1.01781986) + pt3dadd(35.88713625, 30.81800625, 0.0, 0.3859374445) + } + primarydendrite[60] {pt3dclear() + pt3dadd(36.146353125, 32.2965025, 0.0, 0.32642125) + pt3dadd(35.608718125, 32.23889875, 0.0, 0.3125733085) + pt3dadd(35.23429375, 32.2196975, 0.0, 0.5430958355) + pt3dadd(34.821466875, 32.2196975, 0.0, 0.92027751) + pt3dadd(34.648655625, 32.9877475, 0.0, 0.78817291) + } + primarydendrite[61] {pt3dclear() + pt3dadd(39.87331575, 33.33337, 0.0, 1.143242425) + pt3dadd(40.37254825, 33.92860875, 0.0, 0.399554971) + pt3dadd(40.5914425, 34.2934325, 0.0, 0.4750158835) + pt3dadd(41.10219575, 33.7942, 0.0, 0.697880952) + pt3dadd(41.3594925, 33.48698, 0.0, 0.4153844815) + } +} +proc shape3d_3() { + primarydendrite[61] { } + primarydendrite[62] {pt3dclear() + pt3dadd(41.3594925, 33.48698, 0.0, 0.4153844815) + pt3dadd(42.319555, 34.19742625, 0.0, 0.4241709735) + } + primarydendrite[63] {pt3dclear() + pt3dadd(41.3594925, 33.48698, 0.0, 0.4153844815) + pt3dadd(41.858725, 32.93014375, 0.0, 0.6629269965) + pt3dadd(42.3579575, 33.1413575, 0.0, 0.79170594) + pt3dadd(42.9263145, 32.728530625, 0.0, 0.489539709) + pt3dadd(44.02846625, 32.334905, 0.0, 0.384025) + pt3dadd(44.90404325, 31.8356725, 0.0, 0.3688406515) + pt3dadd(45.62217, 31.2980375, 0.0, 0.4344782045) + pt3dadd(46.5822325, 30.750801875, 0.0, 0.4228652885) + pt3dadd(47.36948375, 30.04995625, 0.0, 0.3801617085) + pt3dadd(48.20665825, 29.464318125, 0.0, 0.4036793995) + pt3dadd(48.63676625, 29.176299375, 0.0, 0.82657541) + pt3dadd(49.078395, 29.58912625, 0.0, 0.2999312055) + } + primarydendrite[64] {pt3dclear() + pt3dadd(26.57453, 38.970857, 0.0, 2.474273075) + pt3dadd(25.28804625, 40.09221, 0.0, 0.6606996515) + pt3dadd(24.039965, 40.967787, 0.0, 0.831874955) + pt3dadd(23.71354375, 41.0522725, 0.0, 1.306222635) + } + primarydendrite[65] {pt3dclear() + pt3dadd(23.71354375, 41.0522725, 0.0, 1.306222635) + pt3dadd(22.98389625, 41.07147375, 0.0, 1.3963149) + pt3dadd(22.0814375, 41.79344075, 0.0, 1.167512805) + pt3dadd(21.207780625, 42.64597625, 0.0, 0.470722484) + } + primarydendrite[66] {pt3dclear() + pt3dadd(21.207780625, 42.64597625, 0.0, 0.470722484) + pt3dadd(19.498869375, 43.33722125, 0.0, 0.6135413815) + } + primarydendrite[67] {pt3dclear() + pt3dadd(19.498869375, 43.33722125, 0.0, 0.6135413815) + pt3dadd(18.3179925, 43.10680625, 0.0, 0.46083) + } + primarydendrite[68] {pt3dclear() + pt3dadd(18.3179925, 43.10680625, 0.0, 0.46083) + pt3dadd(17.45393625, 43.0108, 0.0, 0.325852893) + pt3dadd(16.0138425, 42.3579575, 0.0, 0.4750158835) + } + primarydendrite[69] {pt3dclear() + pt3dadd(16.0138425, 42.3579575, 0.0, 0.4750158835) + pt3dadd(15.101783125, 42.9263145, 0.0, 0.489539709) + pt3dadd(14.0169125, 43.83645375, 0.0, 0.462427544) + pt3dadd(12.663224375, 44.70051, 0.0, 0.719724294) + pt3dadd(11.443945, 45.5837675, 0.0, 0.577312463) + pt3dadd(10.92551125, 45.499282, 0.0, 1.05606875) + pt3dadd(10.42627875, 45.545365, 0.0, 0.3456225) + } + primarydendrite[70] {pt3dclear() + pt3dadd(16.0138425, 42.3579575, 0.0, 0.4750158835) + pt3dadd(15.6298175, 41.3134095, 0.0, 0.456406032) + pt3dadd(15.1689875, 40.8218575, 0.0, 0.429355311) + pt3dadd(15.274594375, 39.354882, 0.0, 0.365330663) + } + primarydendrite[71] {pt3dclear() + pt3dadd(18.3179925, 43.10680625, 0.0, 0.46083) + pt3dadd(17.97237, 43.59835825, 0.0, 0.81029275) + pt3dadd(16.964304375, 44.3088045, 0.0, 0.442043497) + pt3dadd(15.601015625, 45.56456625, 0.0, 0.4846165085) + pt3dadd(14.468141875, 46.98545875, 0.0, 0.5170128575) + pt3dadd(14.372135625, 47.9954445, 0.0, 0.438694799) + } + primarydendrite[72] {pt3dclear() + pt3dadd(21.207780625, 42.64597625, 0.0, 0.470722484) + pt3dadd(21.361390625, 43.48315075, 0.0, 0.63625262) + pt3dadd(20.727749375, 44.28960325, 0.0, 0.3314673385) + pt3dadd(20.209315625, 44.52769875, 0.0, 0.3708375815) + pt3dadd(19.806089375, 44.316485, 0.0, 1.05253572) + pt3dadd(19.83489125, 43.72124625, 0.0, 0.4378576245) + } + primarydendrite[73] {pt3dclear() + pt3dadd(23.71354375, 41.0522725, 0.0, 1.306222635) + pt3dadd(23.886355, 41.8971275, 0.0, 0.4241709735) + pt3dadd(23.895955625, 44.116792, 0.0, 0.5216211575) + pt3dadd(23.454326875, 44.623705, 0.0, 0.5839868175) + pt3dadd(22.705478125, 44.6928295, 0.0, 0.518787053) + } + primarydendrite[74] {pt3dclear() + pt3dadd(19.498869375, 43.33722125, 0.0, 0.6135413815) + pt3dadd(18.385196875, 45.24966575, 0.0, 0.317834451) + pt3dadd(17.9339675, 45.74889825, 0.0, 0.399554971) + } + primarydendrite[75] {pt3dclear() + pt3dadd(17.9339675, 45.74889825, 0.0, 0.399554971) + pt3dadd(18.173983125, 46.190527, 0.0, 0.4955228185) + pt3dadd(18.922831875, 46.25581125, 0.0, 0.554178797) + pt3dadd(19.729284375, 46.13292325, 0.0, 0.442043497) + pt3dadd(20.132510625, 45.78730075, 0.0, 0.751060734) + pt3dadd(20.353325, 45.74889825, 0.0, 0.32642125) + } + primarydendrite[76] {pt3dclear() + pt3dadd(17.9339675, 45.74889825, 0.0, 0.399554971) + pt3dadd(17.137115625, 46.37101875, 0.0, 0.48003125) + pt3dadd(16.657084375, 46.927855, 0.0, 0.91781975) + pt3dadd(15.9370375, 47.2734775, 0.0, 0.4378576245) + } + primarydendrite[77] {pt3dclear() + pt3dadd(15.9370375, 47.2734775, 0.0, 0.4378576245) + pt3dadd(15.03457875, 47.7650295, 0.0, 0.3149235415) + pt3dadd(14.1705225, 48.44475375, 0.0, 0.384025) + pt3dadd(13.51768, 48.82109825, 0.0, 0.51843375) + pt3dadd(12.490413125, 49.28960875, 0.0, 0.384501191) + pt3dadd(11.96237875, 49.13599875, 0.0, 0.820968645) + pt3dadd(11.36714, 49.6851545, 0.0, 0.4644167935) + pt3dadd(9.9462475, 50.17286625, 0.0, 0.2924657595) + pt3dadd(8.707766875, 50.53769, 0.0, 0.2749234975) + pt3dadd(7.5652925, 50.7604245, 0.0, 1.17695982) + pt3dadd(6.691635625, 50.89483325, 0.0, 0.5598086035) + } + primarydendrite[78] {pt3dclear() + pt3dadd(15.9370375, 47.2734775, 0.0, 0.4378576245) + pt3dadd(16.0906475, 48.0722495, 0.0, 0.307819079) + pt3dadd(15.581814375, 48.63676625, 0.0, 0.321874394) + pt3dadd(15.389801875, 49.74275825, 0.0, 0.5072893445) + pt3dadd(15.101783125, 50.12678325, 0.0, 0.918818215) + pt3dadd(14.76576125, 50.51848875, 0.0, 0.378218542) + pt3dadd(14.516145, 51.09452625, 0.0, 0.77757382) + pt3dadd(14.160921875, 51.6513625, 0.0, 0.3607607655) + pt3dadd(13.757695625, 52.6037445, 0.0, 0.82765068) + pt3dadd(13.872903125, 53.5714875, 0.0, 0.3728191505) + pt3dadd(13.53688125, 54.27425325, 0.0, 0.3125733085) + pt3dadd(13.047249375, 55.81803375, 0.0, 0.3688406515) + } + primarydendrite[79] {pt3dclear() + pt3dadd(23.71354375, 41.0522725, 0.0, 1.306222635) + pt3dadd(23.1567075, 40.26502125, 0.0, 0.4640174075) + pt3dadd(23.02229875, 39.585297, 0.0, 0.3005456455) + pt3dadd(21.841421875, 38.2872925, 0.0, 0.319570244) + pt3dadd(21.37099125, 37.07761375, 0.0, 0.625146617) + pt3dadd(21.0829725, 36.760793125, 0.0, 0.841552385) + pt3dadd(20.766151875, 36.511176875, 0.0, 0.455192513) + pt3dadd(20.2381175, 35.992743125, 0.0, 0.399554971) + pt3dadd(20.48773375, 37.15441875, 0.0, 0.4687639565) + pt3dadd(20.44933125, 38.354496875, 0.0, 0.4211141345) + pt3dadd(20.247718125, 39.3625625, 0.0, 0.635960761) + pt3dadd(20.074906875, 39.95780125, 0.0, 0.96175221) + pt3dadd(19.738885, 41.0829945, 0.0, 0.87004704) + pt3dadd(19.710083125, 41.41709625, 0.0, 0.4675811595) + } + primarydendrite[80] {pt3dclear() + pt3dadd(24.8464175, 37.394434375, 0.0, 0.4675811595) + pt3dadd(24.529596875, 37.567245625, 0.0, 0.5437717195) + pt3dadd(24.395188125, 37.96087125, 0.0, 0.845085415) + pt3dadd(24.414389375, 38.335295625, 0.0, 0.5216211575) + } + primarydendrite[81] {pt3dclear() + pt3dadd(24.8464175, 37.394434375, 0.0, 0.4675811595) + pt3dadd(24.90402125, 36.549579375, 0.0, 0.6989331805) + pt3dadd(24.500795, 35.96394125, 0.0, 0.876575465) + pt3dadd(24.02076375, 35.147888125, 0.0, 0.5434337775) + pt3dadd(23.473528125, 34.46624375, 0.0, 0.5501772565) + pt3dadd(22.897490625, 33.5253825, 0.0, 0.98602259) + pt3dadd(23.13750625, 32.2581, 0.0, 0.998465) + pt3dadd(22.2350475, 31.35564125, 0.0, 0.6517134665) + pt3dadd(21.5054, 30.0691575, 0.0, 0.512350794) + pt3dadd(21.35179, 28.59066125, 0.0, 0.4224275) + pt3dadd(21.169378125, 26.372916875, 0.0, 0.787481665) + pt3dadd(21.073371875, 25.182439375, 0.0, 0.365330663) + pt3dadd(21.169378125, 23.550333125, 0.0, 0.442043497) + } + primarydendrite[82] {pt3dclear() + pt3dadd(21.169378125, 23.550333125, 0.0, 0.442043497) + pt3dadd(21.111774375, 22.033434375, 0.0, 0.4036793995) + pt3dadd(21.0061675, 20.60294125, 0.0, 0.3166746955) + pt3dadd(20.036504375, 18.817225, 0.0, 0.3461524545) + pt3dadd(19.60447625, 17.425134375, 0.0, 1.241552825) + pt3dadd(17.905165625, 16.8586975, 0.0, 1.007451185) + pt3dadd(16.119449375, 17.26192375, 0.0, 0.95330366) + pt3dadd(15.898635, 17.99157125, 0.0, 0.4992325) + pt3dadd(15.898635, 19.825290625, 0.0, 0.456406032) + pt3dadd(17.002706875, 21.246183125, 0.0, 0.598019091) + pt3dadd(17.91476625, 22.455861875, 0.0, 0.6654308395) + pt3dadd(19.767686875, 22.878289375, 0.0, 0.602934611) + } + primarydendrite[83] {pt3dclear() + pt3dadd(21.169378125, 23.550333125, 0.0, 0.442043497) + pt3dadd(20.64134375, 23.67514125, 0.0, 0.516651874) + pt3dadd(20.33412375, 23.09910375, 0.0, 0.5760375) + pt3dadd(20.58374, 22.398258125, 0.0, 0.5338485135) + } + primarydendrite[84] {pt3dclear() + pt3dadd(21.169378125, 23.550333125, 0.0, 0.442043497) + pt3dadd(21.60140625, 23.0030975, 0.0, 0.570891565) + pt3dadd(22.263849375, 22.86868875, 0.0, 0.7299009565) + } + primarydendrite[85] {pt3dclear() + pt3dadd(22.263849375, 22.86868875, 0.0, 0.7299009565) + pt3dadd(22.974295625, 22.043035, 0.0, 0.5310835335) + } + primarydendrite[86] {pt3dclear() + pt3dadd(22.974295625, 22.043035, 0.0, 0.5310835335) + pt3dadd(24.0015625, 20.670145625, 0.0, 0.386421316) + pt3dadd(24.241578125, 19.134045625, 0.0, 0.2937253615) + pt3dadd(24.433590625, 17.9339675, 0.0, 0.290568676) + pt3dadd(24.98082625, 16.983505625, 0.0, 0.456406032) + pt3dadd(25.278445625, 16.196254375, 0.0, 1.118664825) + pt3dadd(25.604866875, 15.409003125, 0.0, 0.7530192615) + pt3dadd(26.44012125, 13.488878125, 0.0, 0.5338485135) + pt3dadd(26.756941875, 12.80723375, 0.0, 1.157988985) + pt3dadd(26.929753125, 12.03918375, 0.0, 0.4466133945) + pt3dadd(27.86101375, 9.437414375, 0.0, 0.5927579485) + pt3dadd(28.446651875, 8.92858125, 0.0, 1.24454822) + pt3dadd(28.93628375, 8.054924375, 0.0, 0.6497318975) + pt3dadd(29.310708125, 6.7204375, 0.0, 0.638280272) + pt3dadd(30.385978125, 5.28034375, 0.0, 0.4769513695) + pt3dadd(31.250034375, 4.5698975, 0.0, 1.26451752) + } + primarydendrite[87] {pt3dclear() + pt3dadd(31.250034375, 4.5698975, 0.0, 1.26451752) + pt3dadd(32.363706875, 4.329881875, 0.0, 0.489539709) + pt3dadd(33.630989375, 3.888253125, 0.0, 0.5754000185) + pt3dadd(35.474309375, 2.160140625, 0.0, 0.5804998705) + pt3dadd(36.328765, 1.814518125, 0.0, 0.3353383105) + } + primarydendrite[88] {pt3dclear() + pt3dadd(31.250034375, 4.5698975, 0.0, 1.26451752) + pt3dadd(31.12522625, 3.6482375, 0.0, 0.4224275) + pt3dadd(31.259635, 3.0337975, 0.0, 0.5760375) + } + primarydendrite[89] {pt3dclear() + pt3dadd(31.259635, 3.0337975, 0.0, 0.5760375) + pt3dadd(31.49005, 2.236945625, 0.0, 0.768280415) + pt3dadd(31.614858125, -0.067204375, 0.0, 0.7230499505) + } + primarydendrite[90] {pt3dclear() + pt3dadd(31.259635, 3.0337975, 0.0, 0.5760375) + pt3dadd(32.16209375, 2.947391875, 0.0, 0.3286716365) + pt3dadd(33.227763125, 2.4193575, 0.0, 0.528295512) + pt3dadd(34.226228125, 2.09293625, 0.0, 0.4906687425) + pt3dadd(35.704724375, 1.008065625, 0.0, 0.5437717195) + } + primarydendrite[91] {pt3dclear() + pt3dadd(22.263849375, 22.86868875, 0.0, 0.7299009565) + pt3dadd(22.33105375, 23.425525, 0.0, 0.384025) + pt3dadd(22.3118525, 24.087968125, 0.0, 0.352488867) + pt3dadd(21.96623, 24.63520375, 0.0, 0.462427544) + } + primarydendrite[92] {pt3dclear() + pt3dadd(22.974295625, 22.043035, 0.0, 0.5310835335) + pt3dadd(23.435125625, 22.475063125, 0.0, 0.81505466) + pt3dadd(24.241578125, 22.17744375, 0.0, 0.712519985) + pt3dadd(24.808015, 22.014233125, 0.0, 0.7145860395) + pt3dadd(25.44165625, 21.553403125, 0.0, 1.29447147) + pt3dadd(25.78727875, 21.14057625, 0.0, 0.5223277635) + pt3dadd(27.006558125, 20.190114375, 0.0, 0.4036793995) + pt3dadd(27.448186875, 19.498869375, 0.0, 0.461628772) + } + primarydendrite[93] {pt3dclear() + pt3dadd(27.448186875, 19.498869375, 0.0, 0.461628772) + pt3dadd(27.659400625, 18.9324325, 0.0, 0.298080205) + pt3dadd(28.648265, 17.386731875, 0.0, 0.406866807) + pt3dadd(29.1474975, 16.580279375, 0.0, 0.4332032415) + pt3dadd(29.445116875, 14.76576125, 0.0, 0.36482375) + pt3dadd(29.790739375, 13.49847875, 0.0, 0.558157296) + } + primarydendrite[94] {pt3dclear() + pt3dadd(27.448186875, 19.498869375, 0.0, 0.461628772) + pt3dadd(28.09142875, 19.498869375, 0.0, 0.48156735) + pt3dadd(28.792274375, 18.942033125, 0.0, 0.4036793995) + pt3dadd(29.33951, 18.500404375, 0.0, 0.224746791) + pt3dadd(29.74273625, 18.3179925, 0.0, 0.2999312055) + } +} +proc shape3d_4() { + primarydendrite[94] {pt3dclear() + pt3dadd(31.077223125, 17.3963325, 0.0, 0.4753999085) + } + primarydendrite[95] {pt3dclear() + pt3dadd(21.21738125, 34.552649375, 0.0, 1.3794178) + pt3dadd(21.0061675, 33.6789925, 0.0, 1.38464054) + pt3dadd(21.04457, 32.50771625, 0.0, 0.5430958355) + pt3dadd(21.207780625, 31.326839375, 0.0, 0.34024615) + pt3dadd(20.727749375, 30.70279875, 0.0, 0.85975517) + pt3dadd(20.506935, 29.675531875, 0.0, 0.5170128575) + pt3dadd(20.420529375, 28.715469375, 0.0, 0.256175397) + pt3dadd(19.81569, 27.851413125, 0.0, 0.921890415) + pt3dadd(19.364460625, 27.24657375, 0.0, 0.393978928) + pt3dadd(18.961234375, 26.958555, 0.0, 0.352488867) + pt3dadd(18.1643825, 26.651335, 0.0, 0.81013914) + pt3dadd(17.72275375, 27.20817125, 0.0, 0.500707156) + pt3dadd(17.14671625, 27.544193125, 0.0, 0.4122661985) + } + primarydendrite[96] {pt3dclear() + pt3dadd(17.14671625, 27.544193125, 0.0, 0.4122661985) + pt3dadd(16.513075, 27.9186175, 0.0, 0.447849955) + pt3dadd(15.946638125, 28.235438125, 0.0, 0.480415275) + pt3dadd(13.88250375, 28.16823375, 0.0, 0.625146617) + } + primarydendrite[97] {pt3dclear() + pt3dadd(17.14671625, 27.544193125, 0.0, 0.4122661985) + pt3dadd(16.868298125, 27.18897, 0.0, 0.4122661985) + pt3dadd(16.68588625, 26.814545625, 0.0, 0.7145860395) + pt3dadd(16.465071875, 26.47852375, 0.0, 0.366836041) + } + primarydendrite[98] {pt3dclear() + pt3dadd(36.09835, 45.8909875, 0.0, 0.7081036975) + pt3dadd(36.482375, 45.41095625, 0.0, 0.3461524545) + pt3dadd(36.876000625, 45.1536595, 0.0, 0.353011141) + pt3dadd(37.413635625, 44.8003565, 0.0, 0.985484955) + pt3dadd(37.620241075, 44.63522575, 0.0, 0.631383183) + pt3dadd(37.92246875, 44.14367375, 0.0, 0.5457993715) + pt3dadd(38.463944, 43.6021985, 0.0, 0.3490710445) + } + primarydendrite[99] {pt3dclear() + pt3dadd(41.21740325, 48.9017435, 0.0, 0.6418056215) + pt3dadd(41.54766475, 48.3794695, 0.0, 0.373441271) + pt3dadd(42.227389, 47.8418345, 0.0, 0.340922034) + pt3dadd(42.8802315, 47.53845475, 0.0, 0.4261218205) + pt3dadd(43.2642565, 47.3041995, 0.0, 0.607573633) + pt3dadd(43.963182, 46.989299, 0.0, 0.586897727) + pt3dadd(44.32032525, 46.90865375, 0.0, 0.4060757155) + pt3dadd(44.78115525, 46.9048135, 0.0, 0.3918667905) + pt3dadd(44.946286, 47.5653365, 0.0, 0.3405149675) + pt3dadd(45.21894375, 47.79191125, 0.0, 0.499601164) + pt3dadd(45.54920525, 47.6191, 0.0, 0.468863803) + pt3dadd(45.8295435, 47.3041995, 0.0, 0.3629266665) + pt3dadd(46.1214025, 46.943216, 0.0, 0.48433233) + pt3dadd(46.7972865, 46.3825395, 0.0, 0.458925236) + pt3dadd(47.4885315, 46.05611825, 0.0, 0.518525916) + pt3dadd(48.294984, 45.4147965, 0.0, 0.4488714615) + pt3dadd(48.709731, 44.82723825, 0.0, 0.3870127145) + pt3dadd(49.2704075, 43.42938725, 0.0, 0.452857641) + pt3dadd(50.000055, 42.503887, 0.0, 0.6517134665) + pt3dadd(50.952437, 41.62446975, 0.0, 0.3462907035) + pt3dadd(51.720487, 39.17055, 0.0, 0.4466133945) + pt3dadd(52.97624875, 37.048811875, 0.0, 0.6654308395) + pt3dadd(54.6467575, 35.647120625, 0.0, 0.85806546) + pt3dadd(56.605285, 33.074153125, 0.0, 0.97096881) + pt3dadd(57.12371875, 32.16209375, 0.0, 0.876575465) + pt3dadd(57.96857375, 31.4516475, 0.0, 0.7143249025) + pt3dadd(58.479327, 30.16055545, 0.0, 0.4540788405) + pt3dadd(58.75198475, 29.464318125, 0.0, 1.239248675) + pt3dadd(59.02080225, 28.77806545, 0.0, 0.7408072665) + } +} +proc basic_shape() { + shape3d_1() + shape3d_2() + shape3d_3() + shape3d_4() +} +access soma + +celldef() diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..79fb492 --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1 @@ +# empty init to make examples importable (for sphinx) diff --git a/examples/figures.py b/examples/figures.py new file mode 100644 index 0000000..f9e14f3 --- /dev/null +++ b/examples/figures.py @@ -0,0 +1,64 @@ +from __future__ import print_function + +""" + +""" +import sys +import subprocess + +if len(sys.argv) < 2: # if no argument, print helpful message + print( + "Plot selected figures from paper, Manis and Campagnola, Hearing Research. 2018" + ) + print("Usage: figures.py [2a | 2b | 2c | 3 | 4 | 5 | 6a | 6d | 7]") + exit(1) + +arg = sys.argv[1] # get argument, check that it is valid +if arg not in ["2a", "2b", "2c", "3", "4", "5", "6a", "6d", "7"]: + print("Usage: figures.py [2a | 2b | 2c | 3 | 4 | 5 | 6a | 6d | 7]") + exit(1) + +if arg == "2a": + proc = subprocess.Popen( + ["python", "examples/test_mechanisms.py", "klt"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + proc.wait() + print(proc.stdout.read()) + # ;; + # 2b) + # python examples/test_mechanisms.py kht + # ;; + # 2c) + # python examples/test_mechanisms.py ka + # ;; + # 3) + # python examples/toy_model.py + # ;; + # 4) + # python examples/test_synapses.py sgc bushy + # ;; + # 5) + # python examples/test_decorator.py + # ;; + # 6a) + # python examples/test_bushy_variation.py a + # ;; + # 6d) + # python examples/test_bushy_variation.py d + # ;; + # + # 7) + # while true; do + # echo "This figure may take hours to generate!" + # read -p "Are you sure you want to run the script?" yn + # case $yn in + # [Yy]* ) python examples/test_physiology.py; break;; + # [Nn]* ) exit;; + # * ) echo "Please answer yes or no.";; + # esac + # done + # ;; + # diff --git a/examples/figures.sh b/examples/figures.sh new file mode 100755 index 0000000..f213934 --- /dev/null +++ b/examples/figures.sh @@ -0,0 +1,44 @@ +case $1 in + 2a) + python examples/test_mechanisms.py klt + ;; + 2b) + python examples/test_mechanisms.py kht + ;; + 2c) + python examples/test_mechanisms.py ka + ;; + 3) + python examples/toy_model.py + ;; + 4) + python examples/test_synapses.py sgc bushy + ;; + 5) + python examples/test_decorator.py + ;; + 6a) + python examples/test_bushy_variation.py a + ;; + 6d) + python examples/test_bushy_variation.py d + ;; + + 7) + while true; do + echo "This figure may take hours to generate!" + read -p "Are you sure you want to run the script?" yn + case $yn in + [Yy]* ) python examples/test_physiology.py; break;; + [Nn]* ) exit;; + * ) echo "Please answer yes or no.";; + esac + done + ;; + + *) + echo $"Plot selected figures from paper" + echo $"Usage: $0 {2a | 2b | 2c | 3 | 4 | 5 | 6a | 6d | 7}" + exit 1 +esac + diff --git a/examples/gif b/examples/gif new file mode 100755 index 0000000..45822f2 --- /dev/null +++ b/examples/gif @@ -0,0 +1 @@ +/Users/pbmanis/Desktop/Python/GIFFittingToolbox/src \ No newline at end of file diff --git a/examples/play_test_sounds.py b/examples/play_test_sounds.py new file mode 100644 index 0000000..70a64cc --- /dev/null +++ b/examples/play_test_sounds.py @@ -0,0 +1,146 @@ +""" +Test sounds and plot waveforms. + +This script tests the sound waveform generator for a variety of sounds + +""" +import numpy as np +import pyqtgraph as pg +from cnmodel.util import sound +from collections import OrderedDict +import scipy.signal +import sys + +try: + import PySounds + + HAVE_PYSOUNDS = True +except ImportError: + HAVE_PYSOUNDS = False + + +def play(): + if len(sys.argv) >= 2: + stimarg = sys.argv[1] + else: + exit() + + plots = True + + if HAVE_PYSOUNDS: + PS = PySounds.PySounds() + + cf = 2e3 + Fs = 44100 # sample frequency + level = 80.0 + seed = 34978 + fmod = 20.0 + dmod = 20.0 + + if plots: + # waveforms + win = pg.GraphicsWindow() + pipwin = win.addPlot(title="sound pip", row=0, col=0) + pipmodwin = win.addPlot(title="100 \% SAM modulated pip", row=1, col=0) + noisewin = win.addPlot(title="WB noise", row=2, col=0) + noisemodwin = win.addPlot(title="100 \% SAM Modulated WB Noise", row=3, col=0) + clickwin = win.addPlot(title="clicks", row=4, col=0) + fmwin = win.addPlot(title="fmsweep", row=5, col=0) + # spectra + pipwins = win.addPlot(title="sound pip Spec", row=0, col=1) + pipmodwins = win.addPlot(title="100 \% SAM modulated pip", row=1, col=1) + noisewins = win.addPlot(title="WB noise", row=2, col=1) + noisemodwins = win.addPlot(title="100 \% SAM Modulated WB Noise", row=3, col=1) + clickwins = win.addPlot(title="click spec", row=4, col=1) + fmwins = win.addPlot(title="fmsweep spec", row=5, col=1) + else: + pipwin = None + pipmodwin = None + noisewin = None + noisemodwin = None + clickwin = None + pipwins = None + pipmodwins = None + noisewins = None + noisemodwins = None + clickwins = None + fmwins = None + + stims = OrderedDict( + [ + ("pip", (pipwin, sound.TonePip)), + ("pipmod", (pipmodwin, sound.SAMTone)), + ("noise", (noisewin, sound.NoisePip)), + ("noisemod", (noisemodwin, sound.SAMNoise)), + ("clicks", (clickwin, sound.ClickTrain)), + ("fmsweep", (fmwins, sound.FMSweep)), + ] + ) + + specs = OrderedDict( + [ + ("pip", (pipwins, sound.TonePip)), + ("pipmod", (pipmodwins, sound.SAMTone)), + ("noise", (noisewins, sound.NoisePip)), + ("noisemod", (noisemodwins, sound.SAMNoise)), + ("clicks", (clickwins, sound.ClickTrain)), + ("fmsweep", (fmwins, sound.FMSweep)), + ] + ) + if stimarg == "all": + stimlist = list(stims.keys()) + else: + stimlist = [stimarg] + for stim in stimlist: + print(stim) + if stim in ["clicks"]: + wave = stims[stim][1]( + rate=Fs, + duration=1.0, + dbspl=level, + click_duration=1e-4, + click_starts=1e-3 * np.linspace(10, 500, 10), + ) + elif stim in ["fmsweep"]: + wave = stims[stim][1]( + rate=Fs, + duration=0.5, + dbspl=level, + start=0.0, + ramp="linear", + freqs=[16000, 200], + ) + elif stim in ["pip", "pipmod", "noise", "noisemod"]: + wave = stims[stim][1]( + rate=Fs, + duration=2.0, + f0=cf, + dbspl=level, + pip_duration=1.8, + pip_start=[10e-3], + ramp_duration=2.5e-3, + fmod=fmod, + dmod=dmod, + seed=seed, + ) + if plots: + stims[stim][0].plot(wave.time, wave.sound) + f, Pxx_spec = scipy.signal.periodogram( + wave.sound, Fs + ) # , window='flattop', nperseg=8192, + # noverlap=512, scaling='spectrum') + specs[stim][0].plot(f, np.sqrt(Pxx_spec)) + if HAVE_PYSOUNDS: + + print("Playing %s" % stim) + + PS.playSound(wave.sound, wave.sound, Fs) + + if plots and sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() + + +if __name__ == "__main__": + if not HAVE_PYSOUNDS: + print("Could not import PySounds; will not play audio.") + play() diff --git a/examples/plot_hcno_kinetics.py b/examples/plot_hcno_kinetics.py new file mode 100755 index 0000000..e15a7d7 --- /dev/null +++ b/examples/plot_hcno_kinetics.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +""" +test and plot hcno stuff + +""" +import numpy as np +import matplotlib.pyplot as plt + +# Parameters# +gbar = 0.0005 # (mho/cm2) + +vhalf1 = -50 # (mV) # v 1/2 for forward +vhalf2 = -84 # (mV) # v 1/2 for backward +gm1 = 0.3 ## (mV) # slope for forward +gm2 = 0.6 # # (mV) # slope for backward +zeta1 = 3 # (/ms) +# zeta2 = 3 # (/ms) +a01 = 0.008 # (/ms) +a02 = 0.0029 # (/ms) +frac = 0.0 +c0 = 273.16 # (degC) +thinf = -66 # (mV) # inact inf slope +qinf = 7 # (mV) # inact inf slope +q10tau = 4.5 # from Magee (1998) +# v # (mV) +q10g = 4.5 # Cao and Oertel +celsius = 35.0 + +F = 9.648e4 +R = 8.314 + +ct = 1e-3 * zeta1 * F / (R * (c0 + celsius)) + +q10 = q10tau ** ( + (celsius - 33.0) / 10.0 +) # (degC)) : if you don't like room temp, it can be changed! + + +def rates(v): # (mV)) { + tau1 = bet1(v) / (q10 * a01 * (1.0 + alp1(v))) + tau2 = bet2(v) / (q10 * a02 * (1.0 + alp2(v))) + hinf = 1.0 / (1.0 + np.exp((v - thinf) / qinf)) + return tau1, tau2, hinf + + +def alp1(v): # (mV)) { + alp1 = np.exp((v - vhalf1) * ct) + return alp1 + + +def bet1(v): # (mV)) { + bet1 = np.exp(gm1 * (v - vhalf1) * ct) + return bet1 + + +def alp2(v): # (mV)) { + alp2 = np.exp((v - vhalf2) * ct) + return alp2 + + +def bet2(v): # (mV)) { + bet2 = np.exp(gm2 * (v - vhalf2) * ct) + return bet2 + + +def plots(): + v = np.linspace(-120.0, 0.0, 120) + + t1, t2, hi = rates(v) + # print(v) + plt.figure(1) + plt.plot(v, t1) + plt.plot(v, t2) + plt.figure(2) + plt.plot(v, hi) + plt.show() + + +# prevent sphinx from running this script at first build +if __name__ == "__main__": + plots() diff --git a/examples/stim172_geese.wav b/examples/stim172_geese.wav new file mode 100755 index 0000000000000000000000000000000000000000..4fd9af99373650fe00690ae258927a0854653970 GIT binary patch literal 176444 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 H|Nr*CaU-AU literal 0 HcmV?d00001 diff --git a/examples/test_adex.py b/examples/test_adex.py new file mode 100755 index 0000000..9e19f8b --- /dev/null +++ b/examples/test_adex.py @@ -0,0 +1,340 @@ +#!/usr/bin/python + + +""" +Test adex model + +""" + +import numpy as np +from neuron import h +import neuron +import matplotlib.pyplot as plt +from collections import OrderedDict +import weave +from .gif.Filter_Rect_LogSpaced import * + + +class AdEx: + def __init__(self): + pass + self.dt = 0.025 + self.tstop = 250.0 + self.eta = ( + Filter_Rect_LogSpaced() + ) # nA, spike-triggered current (must be instance of class Filter) + self.gamma = ( + Filter_Rect_LogSpaced() + ) # mV, spike-triggered movement of the firing threshold (must be instance of class Filter) + + def runone(self, pars, dt=0.025, tstop=250): + + self.cell = self.create_Adex_model(pars) + self.dt = 0.025 + self.tstop = tstop + + # for inj in np.linspace(pars['I']*0.5, pars['I']*2.0, 3): + inj = pars["I"] + stim = h.Vector() + self.Vm = h.Vector() + self.Vm.record(self.cell(0.5)._ref_Vm_AdEx, sec=self.cell) + istim = h.iStim(0.5, sec=self.cell) + istim.delay = 5.0 + istim.dur = 1e9 # these actually do not matter... + istim.iMax = 0.0 + stim = np.zeros(int(tstop / h.dt)) + stim[int(20 / h.dt) : int(220 / h.dt)] = inj + cmd = h.Vector(stim) + # cmd.play(istim._ref_i, h.dt, 0, sec=cell) + cmd.play(self.cell(0.5)._ref_is_AdEx, h.dt, 0, sec=self.cell) + + rtime = h.Vector() + rtime.record(h._ref_t) + h.finitialize() + h.t = 0.0 + h.dt = self.dt + while h.t < self.tstop: + h.fadvance() + + tb = np.array(rtime) + vcell = self.Vm.to_python() + + return (tb, vcell) + + def run_gifAI(self, pars, dt=0.025, tstep=250.0): + # Model parameters + self.simulate(pars["I"], -65.0, pars) + + def plot_trace(self, i, panel, tb, vcell): + axi = self.ax[i] + axi.plot(tb, vcell) + axi.set_ylim([-100, 10]) + axi.set_title(panel) + + def create_Adex_model(self, pars): + cell = h.Section() + cell.L = 50 + + cell.insert("AdEx") + + cell(0.5).cm_AdEx = pars["cm"] + cell(0.5).gl_AdEx = pars["gl"] + cell(0.5).el_AdEx = pars["el"] + cell(0.5).vt_AdEx = pars["Vt"] + cell(0.5).delt_AdEx = pars["dt"] + cell(0.5).a_AdEx = pars["a"] + cell(0.5).tauw_AdEx = pars["tauw"] + cell(0.5).b_AdEx = pars["b"] + cell(0.5).vr_AdEx = pars["Vr"] + cell(0.5).refract_AdEx = 0.025 + return cell + + def create_GIF_model(self, pars): + cell = h.Section() + cell.L = 50 + cell.insert("GIF") + cell(0.5).cm_GIF = pars["cm"] + cell(0.5).gl_GIF = pars["gl"] + cell(0.5).el_GIF = pars["el"] + cell(0.5).vt_GIF = pars["Vt_star"] + cell(0.5).vr_GIF = pars["Vr"] + cell(0.5).refract_GIF = pars["Tref"] + cell(0.5).DV_GIF = pars["DV"] + cell(0.5).lambda0_GIF = pars["lambda0"] + cell(0.5).b_GIF = pars["b"] + return cell + + # + # The following are from Gif.py... + def simulate(self, I, V0, pars): + + """ + Simulate the spiking response of the GIF model to an input current I (nA) with time step dt. + V0 indicate the initial condition V(0)=V0. + The function returns: + - time : ms, support for V, eta_sum, V_T, spks + - V : mV, membrane potential + - eta_sum : nA, adaptation current + - V_T : mV, firing threshold + - spks : ms, list of spike times + """ + + # Input parameters + p_T = len(I) + p_dt = self.dt + + # Model parameters + p_gl = pars["gl"] + p_C = pars["cm"] + p_El = pars["el"] + p_Vr = pars["Vr"] + p_Tref = pars["Tref"] + p_Vt_star = pars["Vt_star"] + p_DV = pars["DV"] + p_lambda0 = pars["lambda0"] + + # Model kernels + (p_eta_support, p_eta) = self.eta.getInterpolatedFilter(self.dt) + p_eta = p_eta.astype("double") + p_eta_l = len(p_eta) + + (p_gamma_support, p_gamma) = self.gamma.getInterpolatedFilter(self.dt) + p_gamma = p_gamma.astype("double") + p_gamma_l = len(p_gamma) + + # Define arrays + V = np.array(np.zeros(p_T), dtype="double") + I = np.array(I, dtype="double") + spks = np.array(np.zeros(p_T), dtype="double") + eta_sum = np.array(np.zeros(p_T + 2 * p_eta_l), dtype="double") + gamma_sum = np.array(np.zeros(p_T + 2 * p_gamma_l), dtype="double") + + # Set initial condition + V[0] = V0 + + code = """ + #include + + int T_ind = int(p_T); + float dt = float(p_dt); + + float gl = float(p_gl); + float C = float(p_C); + float El = float(p_El); + float Vr = float(p_Vr); + int Tref_ind = int(float(p_Tref)/dt); + float Vt_star = float(p_Vt_star); + float DeltaV = float(p_DV); + float lambda0 = float(p_lambda0); + + int eta_l = int(p_eta_l); + int gamma_l = int(p_gamma_l); + + + float rand_max = float(RAND_MAX); + float p_dontspike = 0.0 ; + float lambda = 0.0 ; + float r = 0.0; + + + for (int t=0; t p_dontspike) { + + if (t+1 < T_ind-1) + spks[t+1] = 1.0; + + t = t + Tref_ind; + + if (t+1 < T_ind-1) + V[t+1] = Vr; + + + // UPDATE ADAPTATION PROCESSES + for(int j=0; j 1: + dst = np.diff(spikeTimes) + st = np.array(spikeTimes[0]) # get first spike + sok = np.where(dst > mindT) + st = np.append(st, [spikeTimes[s + 1] for s in sok]) + # print st + spikeTimes = st + return spikeTimes + + +def showplots(name): + """ + Show traces from sound stimulation - without current injection + """ + f = open(name, "rb") + d = pickle.load(f) + f.close() + ncells = len(d["results"]) + stiminfo = d["stim"] + dur = stiminfo["rundur"] * 1000.0 + print("dur: ", dur) + print("stim info: ") + print(" fmod: ", stiminfo["fmod"]) + print(" dmod: ", stiminfo["dmod"]) + print(" f0: ", stiminfo["f0"]) + print(" cf: ", stiminfo["cf"]) + varsg = np.linspace(0.25, 2.0, int((2.0 - 0.25) / 0.25) + 1) # was not stored... + fig, ax = mpl.subplots(ncells + 1, 2, figsize=(8.5, 11.0)) + spikelists = [[]] * ncells + prespikes = [[]] * ncells + xmin = 50.0 + for i in range(ncells): + vdat = d["results"][i]["v"] + idat = d["results"][i]["i"] + tdat = d["results"][i]["t"] + pdat = d["results"][i]["pre"] + PH.noaxes(ax[i, 0]) + # if i == 0: + # PH.calbar(ax[0, 0], calbar=[120., -120., 25., 20.], axesoff=True, orient='left', + # unitNames={'x': 'ms', 'y': 'mV'}, fontsize=9, weight='normal', font='Arial') + for j in range(len(vdat)): + if j == 2: + ax[i, 0].plot(tdat - xmin, vdat[j], "k", linewidth=0.5) + if j == 0: + ax[i, 0].annotate("%.2f" % varsg[i], (180.0, 20.0)) + + ax[i, 0].set_xlim([0, dur - xmin]) + ax[i, 0].set_ylim([-75, 50]) + PH.referenceline( + ax[i, 0], + reference=-62.0, + limits=None, + color="0.33", + linestyle="--", + linewidth=0.5, + dashes=[3, 3], + ) + + for j in range(len(vdat)): + detected = PU.findspikes(tdat, vdat[j], -20.0) + detected = clean_spiketimes(detected) + spikelists[i].extend(detected) + if j == 0: + n, bins = np.histogram( + detected, np.linspace(0.0, dur, 201), density=False + ) + else: + m, bins = np.histogram( + detected, np.linspace(0.0, dur, 201), density=False + ) + n += m + prespikes[i].extend(pdat) + if j == 0: + n, bins = np.histogram(pdat, np.linspace(0.0, dur, 201), density=False) + else: + m, bins = np.histogram(pdat, np.linspace(0.0, dur, 201), density=False) + n += m + + ax[i, 1].bar(bins[:-1] - xmin, n, width=bins[1], facecolor="k", alpha=0.75) + ax[i, 1].set_xlim([0, dur - xmin]) + ax[i, 1].set_ylim([0, 30]) + vs = PU.vector_strength(spikelists[i], stiminfo["fmod"]) + pre_vs = PU.vector_strength(prespikes[i], stiminfo["fmod"]) + # print 'pre: ', pre_vs + # print 'post: ', vs + # apos = ax[i,1].get_position() + # ax[i, 1].set_title('VS = %4.3f' % pre_vs['r']) + # # vector_plot(fig, vs['ph'], np.ones(len(vs['ph'])), yp = apos) + # phase_hist(fig, vs['ph'], yp=apos) + # phase_hist(fig, pre_vs['ph'], yp=apos) + prot = Variations(runtype, runname, "cochlea") + # stim_info = {'nreps': nrep, 'cf': cf, 'f0': f0, 'rundur': rundur, 'pipdur': pipdur, 'dbspl': dbspl, 'fmod': fmod, 'dmod': dmod} + if stiminfo["dmod"] > 0: + stimulus = "SAM" + else: + stimulus = "tone" + prot.make_stimulus( + stimulus=stimulus, + cf=stiminfo["cf"], + f0=stiminfo["f0"], + simulator=None, + rundur=stiminfo["rundur"], + pipdur=stiminfo["pipdur"], + dbspl=stiminfo["dbspl"], + fmod=stiminfo["fmod"], + dmod=stiminfo["dmod"], + ) + ax[-1, 1].plot( + prot.stim.time * 1000.0 - xmin, prot.stim.sound, "k-", linewidth=0.75 + ) + ax[-1, 1].set_xlim([0, (dur - xmin)]) + + PH.noaxes(ax[-1, 0]) + ax[-1, 0].set_xlim([0, dur - xmin]) + ax[-1, 0].set_ylim([-75, 50]) + # PH.referenceline(ax[-1, 0], reference=-62.0, limits=None, color='0.33', linestyle='--' ,linewidth=0.5, dashes=[3, 3]) + PH.calbar( + ax[-1, 0], + calbar=[20.0, 0.0, 25.0, 20.0], + axesoff=True, + orient="left", + unitNames={"x": "ms", "y": "mV"}, + fontsize=9, + weight="normal", + font="Arial", + ) + + PH.cleanAxes(ax.ravel().tolist()) + + mpl.show() + + +if __name__ == "__main__": + runname = None + panel = None + if len(sys.argv) == 2: + panel = sys.argv[1] + if panel == "a": + runtype = "IV" + runname = "Figure6_IV" + elif panel == "d": + runtype = "sound" + runname = "Figure6_AN" + else: + runtype = panel + if panel is None: + raise ValueError("Must specify figure panel to generate: 'a', 'b'") + if runtype in ["sound", "IV"]: + prot = Variations(runtype, runname, "cochlea") + if runtype == "IV": + start_time = timeit.default_timer() + prot.runIV(parallelize=True) + elapsed = timeit.default_timer() - start_time + print(("Elapsed time for IV simulations: %f" % (elapsed))) + showpicklediv(runname) + if runtype == "sound": + start_time = timeit.default_timer() + prot.runSound(parallelize=True) + elapsed = timeit.default_timer() - start_time + print(("Elapsed time for AN simulations: %f" % (elapsed))) + showplots(runname) + # pg.show() + # if sys.flags.interactive == 0: + # pg.QtGui.QApplication.exec_() + + elif runtype in ["showiv"]: + showpicklediv(runname) + + elif runtype in ["plots"]: + showplots(runname) + else: + print("run type should be one of sound, IV, showiv, plots") diff --git a/examples/test_ccstim.py b/examples/test_ccstim.py new file mode 100644 index 0000000..63cf595 --- /dev/null +++ b/examples/test_ccstim.py @@ -0,0 +1,77 @@ +""" +Test the ccstim generator. + +Usage: python test_ccstim.py + +This script runs ccstim for each of it's potential inputs, and plots the resulting waveforms. +""" + +import sys +import numpy as np +from cnmodel.util import ccstim +import pyqtgraph as pg + +pulsetypes = ["square", "hyp", "timedSpikes", "exp"] + + +def test_cc_stim(): + """ + stim: a dictionary with keys [required] + delay (delay to start of pulse train, msec [all] + duration: duration of pulses in train, msec [all] + Sfreq: stimulus train frequency (Hz) [timedSpikes] + PT: post-train test delay [all] + NP: number of pulses in the train [timedSpikes, exp] + amp: amplitude of the pulses in the train [all] + hypamp: amplitude of prehyperpolarizing pulse [hyp] + hypdur: duration of prehyperpolarizing pulse [hyp] + spikeTimes" times of spikes [timedSpikes] + """ + stim = { + "square": { + "delay": 10, + "duration": 100, + "Sfreq": 10, + "PT": 0, + "NP": 1, + "amp": 100.0, + }, + "hyp": { + "delay": 10, + "duration": 100, + "Sfreq": 10, + "PT": 0, + "NP": 1, + "amp": 100.0, + "hypamp": -50, + "hypdur": 50, + }, + "timedSpikes": { + "delay": 10, + "duration": 1, + "PT": 0, + "amp": 100.0, + "spikeTimes": [10.0, 20, 30, 40.0, 50.0], + }, + "exp": { + "delay": 10, + "duration": 3, + "Sfreq": 20, + "PT": 0, + "NP": 4, + "amp": 100.0, + }, + } + + dt = 0.1 + + for p in pulsetypes: + (w, tmax, ts) = ccstim.ccstim(stim[p], dt, pulsetype=p) + tb = np.arange(0, w.shape[0] * dt, dt) + pg.plot(tb, w, title=p) + + +if __name__ == "__main__": + test_cc_stim() + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_cells.py b/examples/test_cells.py new file mode 100755 index 0000000..e66d5a1 --- /dev/null +++ b/examples/test_cells.py @@ -0,0 +1,515 @@ +#!/usr/bin/python + +""" +Test the basic membrane physiology of cell types. + +Basic Usage: python test_cells.py celltype species [--cc | --vc] + +This script generates a cell of the specified type and species, then tests the +cell with a series of current/voltage pulses to produce I/V, F/I, and spike +latency analyses. + +""" + +import argparse +import os, sys +from neuron import h +import pyqtgraph as pg +import cnmodel +import cnmodel.cells as cells +from cnmodel.protocols import IVCurve, VCCurve + +debugFlag = True +ax = None +h.celsius = 22 +default_durs = [10.0, 100.0, 20.0] +cclamp = False + +cellinfo = { + "types": [ + "bushy", + "bushycoop", + "tstellate", + "tstellatenav11", + "dstellate", + "dstellateeager", + "sgc", + "cartwheel", + "pyramidal", + "octopus", + "tuberculoventral", + "mso", + ], + "morphology": ["point", "waxon", "stick"], + "nav": ["std", "jsrna", "nav11", "nacncoop"], + "species": ["guineapig", "cat", "rat", "mouse"], + "pulse": ["step", "pulse"], +} + +# Format for ivranges is list of tuples. This allows finer increments in selected ranges, such as close to rest +ccivrange = { + "mouse": { + "bushy": {"pulse": [(-1, 1.2, 0.05)]}, + "bushycoop": {"pulse": [(-0.5, 0.7, 0.02)]}, + "tstellate": {"pulse": [(-1.0, 1.01, 0.05), (-0.015, 0, 0.005)]}, + "tstellatenav11": {"pulse": [(-1, 1.0, 0.1)]}, + "dstellate": {"pulse": [(-0.3, 0.301, 0.015)]}, + "octopus": {"pulse": [(-1.0, 1.0, 0.05)]}, + "sgc": {"pulse": [(-0.3, 0.6, 0.02)]}, + "cartwheel": {"pulse": [(-0.5, 0.5, 0.05)]}, + "pyramidal": { + "pulse": [(-0.3, 0.3, 0.025), (-0.040, 0.025, 0.005)] + }, # , 'prepulse': [(-0.25, -0.25, 0.25)]}, + "tuberculoventral": {"pulse": [(-0.35, 1.0, 0.05), (-0.040, 0.01, 0.005)]}, + }, + "guineapig": { + "bushy": {"pulse": [(-1, 1.2, 0.05)]}, + "tstellate": {"pulse": [(-0.15, 0.15, 0.01)]}, + "dstellate": {"pulse": [(-0.25, 0.25, 0.025)]}, + "dstellateeager": {"pulse": [(-0.6, 1.0, 0.025)]}, + "octopus": {"pulse": [(-2.0, 6.0, 0.2)]}, + "sgc": {"pulse": [(-0.3, 0.6, 0.02)]}, + "mso": {"pulse": [(-1, 1.2, 0.05)]}, + }, + "rat": { + "pyramidal": { + "pulse": [(-0.3, 0.3, 0.025), (-0.040, 0.025, 0.005)] + } # 'prepulse': [(-0.25, -0.25, 0.25)]}, + }, +} + +# scales holds some default scaling to use in the cciv plots +# argument is {cellname: (xmin, xmax, IVymin, IVymax, FIspikemax, +# offset(for spikes), crossing (for IV) )} +## the "offset" refers to setting the axes back a bit +scale = { + "bushy": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "bushycoop": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "tstellate": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "tstellatenav11": ( + -1.0, + -160.0, + 1.0, + -40, + 0, + 40, + "offset", + 5, + "crossing", + [0, -60], + ), + "tstellatedend": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "dstellate": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "dstellateeager": ( + -1.0, + -160.0, + 1.0, + -40, + 0, + 40, + "offset", + 5, + "crossing", + [0, -60], + ), + "sgc:": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "cartwheel": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "pyramidal": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "tuberculoventral": ( + -1.0, + -160.0, + 1.0, + -40, + 0, + 40, + "offset", + 5, + "crossing", + [0, -60], + ), + "octopus": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), + "mso": (-1.0, -160.0, 1.0, -40, 0, 40, "offset", 5, "crossing", [0, -60]), +} + + +class Tests: + """ + Class to select cells for tests + """ + + def __init__(self): + pass + + def selectCell(self, args): + """ + Parameters + ---------- + args : argparse args from command line + + Returns + ------- + cell + Instantiated cell of the selected celltype + """ + h.celsius = float(args.temp) + # + # Spiral Ganglion cell tests + # + if args.celltype == "sgc": # morphology is always "point" for SGCs + cell = cells.SGC.create( + debug=debugFlag, + species=args.species, + nach=args.nav, + ttx=args.ttx, + modelType=args.type, + ) + + # + # Bushy tests + # + elif args.celltype == "bushy" and args.morphology == "point": + cell = cells.Bushy.create( + model="RM03", + species=args.species, + modelType=args.type, + ttx=args.ttx, + nach=args.nav, + debug=debugFlag, + ) + # cell.soma().klt.gbar = 0.0003 + + elif args.celltype == "bushy" and args.morphology == "waxon": + cell = cells.Bushy.create( + model="RM03", + species=args.species, + modelType=args.type, + nach=args.nav, + ttx=args.ttx, + debug=debugFlag, + ) + cell.add_axon() + + elif args.celltype == "bushy" and args.morphology == "stick": + cell = cells.Bushy.create( + model="RM03", + species=args.species, + modelType=args.type, + morphology="cnmodel/morphology/bushy_stick.hoc", + decorator=True, + nach=args.nav, + ttx=args.ttx, + debug=debugFlag, + ) + h.topology() + + elif args.celltype == "bushycoop" and args.morphology == "point": + cell = cells.Bushy.create( + model="RM03", + species=args.species, + modelType=args.type, + ttx=args.ttx, + nach=args.nav, + debug=debugFlag, + ) + + # + # T-stellate tests + # + elif args.celltype == "tstellate" and args.morphology == "point": + cell = cells.TStellate.create( + model="RM03", + species=args.species, + modelType=args.type, + nach=args.nav, + ttx=args.ttx, + debug=debugFlag, + ) + + elif args.celltype == "tstellate" and args.morphology == "stick": + cell = cells.TStellate.create( + model="RM03", + species=args.species, + modelType=args.type, + nach=args.nav, + ttx=args.ttx, + debug=debugFlag, + morphology="cnmodel/morphology/tstellate_stick.hoc", + decorator=True, + ) + + elif ( + args.celltype == "tstellatenav11" and args.morphology == "point" + ): # note this uses a different model... + print("test_cells: Stellate NAV11") + cell = cells.TStellateNav11.create( + model="Nav11", + species=args.species, + modelType=None, + ttx=args.ttx, + debug=debugFlag, + ) + + elif ( + args.celltype == "tstellatenav11" and args.morphology == "stick" + ): # note this uses a different model... + cell = cells.TStellateNav11.create( + model="Nav11", + species=args.species, + modelType=None, + morphology="cnmodel/morphology/tstellate_stick.hoc", + decorator=True, + ttx=args.ttx, + debug=debugFlag, + ) + h.topology() + + # + # Octopus cell tests + # + elif args.celltype == "octopus" and args.morphology == "point": + cell = cells.Octopus.create( + species=args.species, + modelType="RM03", # args.type, + nach=args.nav, + ttx=args.ttx, + debug=debugFlag, + ) + + elif ( + args.celltype == "octopus" and args.morphology == "stick" + ): # Go to spencer et al. model + cell = cells.Octopus.create( + modelType="Spencer", + species=args.species, + morphology="cnmodel/morphology/octopus_spencer_stick.hoc", + decorator=True, + nach=args.nav, + ttx=args.ttx, + debug=debugFlag, + ) + h.topology() + + # + # D-stellate tests + # + elif args.celltype == "dstellate": + cell = cells.DStellate.create( + debug=debugFlag, species=args.species, ttx=args.ttx, modelType=args.type + ) + + elif args.celltype == "dstellateeager": + cell = cells.DStellateEager.create( + debug=debugFlag, ttx=args.ttx, modelType=args.type + ) + + # + # DCN pyramidal cell tests + # + elif args.celltype == "pyramidal": + cell = cells.Pyramidal.create( + modelType=args.type, ttx=args.ttx, debug=debugFlag + ) + + # + # DCN tuberculoventral cell tests + # + elif args.celltype == "tuberculoventral" and args.morphology == "point": + cell = cells.Tuberculoventral.create( + species="mouse", + modelType="TVmouse", + ttx=args.ttx, + nach=args.nav, + debug=debugFlag, + ) + + elif args.celltype == "tuberculoventral" and args.morphology == "stick": + cell = cells.Tuberculoventral.create( + species="mouse", + modelType="TVmouse", + morphology="cnmodel/morphology/tv_stick.hoc", + decorator=True, + ttx=args.ttx, + debug=debugFlag, + ) + h.topology() + + # + # DCN cartwheel cell tests + # + elif args.celltype == "cartwheel": + cell = cells.Cartwheel.create( + modelType=args.type, ttx=args.ttx, debug=debugFlag + ) + + # + # MSO principal neuron tests + # + elif args.celltype == "mso" and args.morphology == "point": + cell = cells.MSO.create( + model="RM03", + species=args.species, + modelType=args.type, + ttx=args.ttx, + nach=args.nav, + debug=debugFlag, + ) + + else: + raise ValueError( + "Cell Type %s and configurations nav=%s or config=%s are not available" + % (args.celltype, args.nav, args.morphology) + ) + + print(cell.__doc__) + self.cell = cell + + def run_test(self, sites, ptype, args): + """ + Run either vc or cc test, and plot the result + + Parameters + ---------- + args : argparse args from command line + + """ + self.cell.set_temperature(float(args.temp)) + print(self.cell.status) + V0 = self.cell.find_i0(showinfo=True) + # self.cell.cell_initialize() + print( + "Currents at nominal Vrest= %.2f I = 0: I = %g " + % (V0, self.cell.i_currents(V=V0)) + ) + self.cell.print_mechs(self.cell.soma) + instant = self.cell.compute_rmrintau(auto_initialize=False, vrange=None) + print( + " From Inst: Rin = {:7.1f} Tau = {:7.1f} Vm = {:7.1f}".format( + instant["Rin"], instant["tau"], instant["v"] + ) + ) + if args.cc is True: + # define the current clamp electrode and default settings + self.iv = IVCurve() + self.iv.run( + ccivrange[args.species][args.celltype], + self.cell, + durs=default_durs, + sites=sites, + reppulse=ptype, + temp=float(args.temp), + ) + ret = self.iv.input_resistance_tau() + print( + " From IV: Rin = {:7.1f} Tau = {:7.1f} Vm = {:7.1f}".format( + ret["slope"], ret["tau"], ret["intercept"] + ) + ) + self.iv.show(cell=self.cell) + + elif args.rmp is True: + print("temperature: ", self.cell.status["temperature"]) + self.iv = IVCurve() + self.iv.run( + {"pulse": (0, 0, 1)}, + self.cell, + durs=default_durs, + sites=sites, + reppulse=ptype, + temp=float(args.temp), + ) + self.iv.show(cell=self.cell, rmponly=True) + + elif args.vc is True: + # define the voltage clamp electrode and default settings + self.vc = VCCurve() + self.vc.run((-120, 40, 5), self.cell) + self.vc.show(cell=self.cell) + + else: + raise ValueError("Nothing to run. Specify one of --cc, --vc, --rmp.") + + +def main(): + parser = argparse.ArgumentParser( + description=( + "test_cells.py:", + " Biophysical representations of neurons (mostly auditory), test file", + ) + ) + parser.add_argument("celltype", action="store") + parser.add_argument("species", action="store", default="guineapig") + parser.add_argument("--type", action="store", default=None) + parser.add_argument( + "--temp", action="store", default=22.0, help=("Temp DegC (22 default)") + ) + # species is an optional option.... + parser.add_argument( + "-m", + action="store", + dest="morphology", + default="point", + help=("Set morphology: %s " % [morph for morph in cellinfo["morphology"]]), + ) + parser.add_argument( + "--nav", + action="store", + dest="nav", + default=None, + help=("Choose sodium channel: %s " % [ch for ch in cellinfo["nav"]]), + ) + parser.add_argument( + "--ttx", + action="store_true", + dest="ttx", + default=False, + help=("Use TTX (no sodium current"), + ) + parser.add_argument( + "-p", + action="store", + dest="pulsetype", + default="step", + help=("Set CCIV pulse to step or repeated pulse"), + ) + clampgroup = parser.add_mutually_exclusive_group() + clampgroup.add_argument( + "--vc", action="store_true", help="Run in voltage clamp mode" + ) + clampgroup.add_argument( + "--cc", action="store_true", help="Run in current clamp mode" + ) + clampgroup.add_argument( + "--rmp", action="store_true", help="Run to get RMP in current clamp mode" + ) + + args = parser.parse_args() + if args.celltype not in cellinfo["types"]: + print("cell: %s is not in our list of cell types" % (args.celltype)) + print("celltypes: ", cellinfo["types"]) + sys.exit(1) + + path = os.path.dirname(cnmodel.__file__) + # h.load_file("stdrun.hoc") + # h.load_file(os.path.join(path, "custom_init.hoc")) # replace init with one that gets closer to steady state + + # print 'Species: ', args.species + # print 'Morphology configuration: ', args.morphology + sites = None + if args.pulsetype == "step": + ptype = None + else: + ptype = "pulses" + if args.morphology in cellinfo["morphology"]: + print("Morphological configuration %s is ok" % args.morphology) + + t = Tests() + t.selectCell(args) + app = pg.mkQApp() + t.run_test(sites, ptype, args) + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() + + +if __name__ == "__main__": + main() diff --git a/examples/test_circuit.py b/examples/test_circuit.py new file mode 100644 index 0000000..c915e85 --- /dev/null +++ b/examples/test_circuit.py @@ -0,0 +1,60 @@ +""" +Test construction of a complete circuit build from cell populations. + +This script: + +1. Creates populations of sgc, bushy, and stellate cells. +2. Connects the populations together. +3. Instantiates 10 bushy and 10 t-stellate cells near 16kHz. +4. Resolves all required synaptic dependencies. + +No simulation is executed; this is meant to be run interactively to allow +introspection of the circuit that was generated. +""" + +from cnmodel import populations + + +def testcircuit(): + # Create cell populations. + # This creates a complete set of _virtual_ cells for each population. No + # cells are instantiated at this point. + sgc = populations.SGC() + bushy = populations.Bushy() + dstellate = populations.DStellate() + tstellate = populations.TStellate() + + # Connect populations. + # This only records the connections between populations; no synapses are + # created at this stage. + sgc.connect(bushy, tstellate, dstellate) + dstellate.connect(tstellate, bushy) + # tstellate.connect(bushy) # this will fail - we don't know about this connection yet. + + # Select cells to record from. + # At this time, we actually instantiate the selected cells. + # select 10 bushy cells closest to 16kHz + bushy_cell_ids = bushy.select(10, cf=16e3, create=True) + # select 10 stellate cells closest to 16kHz + tstel_cell_ids = tstellate.select(10, cf=16e3, create=True) + + # Now create the supporting circuitry needed to drive the cells we selected. + # At this time, cells are created in all populations and automatically + # connected with synapses. + bushy.resolve_inputs(depth=2) + tstellate.resolve_inputs(depth=2) + # Note that using depth=2 indicates the level of recursion to use when + # resolving inputs. For example, resolving inputs for the bushy cell population + # (level 1) creates presynaptic cells in the dstellate population, and resolving + # inputs for the dstellate population (level 2) creates presynaptic cells in the + # sgc population. + + # TODO: + # - specify which parameters to record (Vm, spike times, per-synapse currents, etc) + # - run simulation and display / analyze results + # - add examples of modifying circuitry to search parameter spaces, test + # hypotheses, etc. + + +if __name__ == "__main__": + testcircuit() diff --git a/examples/test_decorator.py b/examples/test_decorator.py new file mode 100755 index 0000000..a5ddaea --- /dev/null +++ b/examples/test_decorator.py @@ -0,0 +1,151 @@ +#!/usr/bin/python +""" +Basic test of cnmodel decorator on simple cell +Users LC_bushy.hoc and XM13 mechanisms. + +""" +from __future__ import print_function + + +__author__ = "pbmanis" + + +__author__ = "pbmanis" + + +import sys +import numpy as np +import cnmodel.cells as cells +import cnmodel.decorator as Decorator +from cnmodel.util import pyqtgraphPlotHelpers as PH +from cnmodel.protocols import IVCurve +import pyqtgraph as pg +import timeit + + +class F5: + def __init__(self, filename): + # build plotting area + # + self.filename = filename + self.iv = IVCurve() # use standard IVCurve here... + self.temperature = 34 + self.initdelay = 150.0 + + def run(self): + self.post_cell = cells.Bushy.create( + morphology=self.filename, + decorator=Decorator, + species="mouse", + modelName="XM13", + modelType="II", + ) + self.post_cell.set_temperature(float(self.temperature)) + self.post_cell.set_d_lambda( + freq=2000.0 + ) # necessary to ensure appropriate spatial + self.iv.reset() + irange = self.post_cell.i_test_range + # irange = {'pulse': (-0.6, 1.1, 0.2)} for Figure 5 of paper + irange = {"pulse": (-0.5, 1.5, 0.25)} + self.durs = (self.initdelay + 20.0, 100.0, 50.0) + self.iv.run( + irange, + self.post_cell, + durs=self.durs, + temp=float(self.temperature), + initdelay=self.initdelay, + ) + + def plot(self): + pg.setConfigOption("background", "w") # set background to white + pg.setConfigOption("foreground", "k") + self.app = pg.mkQApp() + + wintitle = "test_decorator" + self.view = pg.GraphicsView() + self.layout = pg.GraphicsLayout() # (border=(100,100,100)) + self.view.setCentralItem(self.layout) + self.view.resize(800, 600) + self.view.show() + self.plots = {} + nr1 = 6 + nc = 10 + for i in range(1, nr1): + self.plots["p%d" % i] = None + for i in range(1, nr1 + 1): + self.layout.addLayout(row=i, col=nc) + for i in range(1, nc + 1): + self.layout.addLayout(row=nr1 + 2, col=i) + + self.plots["p1"] = self.layout.addPlot( + row=1, + col=1, + rowspan=6, + colspan=9, + labels={"left": "V (mV)", "bottom": "Time (ms)"}, + ) + self.plots["p2"] = self.layout.addPlot( + row=7, + col=1, + rowspan=1, + colspan=9, + labels={"left": "I (nA)", "bottom": "Time (ms)"}, + ) + + for k in range(len(self.iv.voltage_traces)): + self.plots["p1"].plot( + self.iv.time_values, + np.array(self.iv.voltage_traces[k]), + pen=pg.mkPen("k", width=0.75), + ) + self.plots["p2"].plot( + self.iv.time_values, + np.array(self.iv.current_traces[k]), + pen=pg.mkPen("k", width=0.75), + ) + self.plots["p1"].setRange( + xRange=(0.0, np.sum(self.durs) - self.initdelay), yRange=(-160.0, 40.0) + ) + self.plots["p2"].setRange( + xRange=(0.0, np.sum(self.durs) - self.initdelay), yRange=(-1, 1) + ) + PH.noaxes(self.plots["p1"]) + PH.calbar( + self.plots["p1"], + calbar=[125.0, -120.0, 10.0, 20.0], + unitNames={"x": "ms", "y": "mV"}, + ) + PH.noaxes(self.plots["p2"]) + PH.calbar( + self.plots["p2"], + calbar=[125, 0.1, 0.0, 0.5], + unitNames={"x": "ms", "y": "nA"}, + ) + + text = "{0:2d}\u00b0C {1:.2f}-{2:.2f} nA".format( + int(self.temperature), + np.min(self.iv.current_cmd), + np.max(self.iv.current_cmd), + ) + ti = pg.TextItem(text, anchor=(1, 0)) + ti.setFont(pg.QtGui.QFont("Arial", 9)) + ti.setPos(120.0, -120.0) + self.plots["p1"].addItem(ti) + + +if __name__ == "__main__": + if len(sys.argv) == 1: + fig5 = F5("examples/LC_bushy.hoc") + else: + fn = "/Users/pbmanis/Desktop/Python/VCNModel/VCN_Cells/VCN_c{0:02d}/Morphology/VCN_c{0:02d}.hoc".format( + int(sys.argv[1]) + ) + fig5 = F5(fn) + start_time = timeit.default_timer() + fig5.run() + elapsed = timeit.default_timer() - start_time + print("Elapsed time for simulation: %f" % (elapsed)) + fig5.plot() + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_mechanisms.py b/examples/test_mechanisms.py new file mode 100644 index 0000000..9b23ffe --- /dev/null +++ b/examples/test_mechanisms.py @@ -0,0 +1,317 @@ +""" +test_mechanisms.py + +This program displays the results of inserting NEURON mechaanisms from .mod files +into a point cell to voltage steps in voltage clamp. +This code is primarily for visual verification of model function. + +Usage: python test_mechanisms.py + +Available mechanisms:: + + CaPCalyx bkpkj hcno hcnobo hpkj + ihpyr ihsgcApical ihsgcBasalMiddle ihvcn jsrna + ka kcnq kdpyr kht kif + kis klt kpkj kpkj2 kpkjslow + kpksk leak lkpkj na naRsg + nacn nacncoop nap napyr nav11 + +Note: only modfiles that implement voltage-dependent ion channel models make sense to run +with this routine. the list "nottestablemechs" in the file defines mechanisms provided +with cnmodel that cannot be run with this program. + +""" +import sys +from neuron import h +from neuron import nrn + +import gc +import numpy as np + +# import scipy as sp +import cnmodel.util +import pyqtgraph as pg +import pyqtgraph.exporters +from pyqtgraph.Qt import QtCore, QtGui +import cnmodel.util.pynrnutilities as Util + +nottestablemechs = [ + "cadyn", + "ca_ion", + "cadiff", + "cadifpmp", + "Mechanism", + "capmp", + "capump", + "cl_ion", + "extracellular", + "fastpas", + "k_ion", + "KIR", + "hh", + "na_ion", + "narsg", + "pas", + "cap", +] # cap uses "pcabar" + + +class ChannelKinetics: + def __init__(self, args, export=False): + modfile = [] + if isinstance(args, list): + for arg in args: + modfile.append(arg) # must be string, not list... + else: + modfile.append(args) # 'CaPCalyx' + print("modfile: ", modfile) + colors = ["b", "r", "g", "y", "c", "m", "w"] + if len(modfile) > len(colors): + print("Too many modfiles... keep it simple!") + exit() + # if isinstance(args, list) and len(args) > 1: + # modfile2 = args[1] + doKinetics = False + self.app = pg.mkQApp() + self.win = pg.GraphicsWindow() + self.win.setWindowTitle("VC Plots") + self.win.resize(600, 800) + # cw = QtGui.QWidget() + # self.win.setCentralWidget(cw) + # self.gridLayout = QtGui.QGridLayout() + # cw.setLayout(self.gridLayout) + # self.gridLayout.setContentsMargins(9, 9, 4, 4) + # self.gridLayout.setSpacing(1) + self.p1 = self.win.addPlot(title="I (VC)") + # self.gridLayout.addWidget(self.p1, 0, 0, 1, 1) + self.p2 = self.win.addPlot(title="I_ss, I_max") + # self.gridLayout.addWidget(self.p2, 0, 1, 1, 1) + self.win.nextRow() + self.p3 = self.win.addPlot(title="V command") + # self.gridLayout.addWidget(self.p3, 1, 0, 1, 1) + self.p5 = self.win.addPlot(title="I_min") + # self.gridLayout.addWidget(self.p5, 1, 1, 1, 1) + self.win.show() + QtGui.QApplication.processEvents() + # + # self.tdur is a table of durations for the pulse and post-pulse for each channel type (best to highlight features + # on appropriate time scales) + # + self.tdur = { + "CaPCalyx": [20.0, 10.0], + "nav11": [10.0, 5.0], + "jsrna": [10.0, 5.0], + "ichanWT2005": [10.0, 5.0], + "nacn": [10.0, 5.0], + "nacncoop": [10.0, 5.0], + "nabu": [10.0, 5.0], + "kht": [200.0, 20.0], + "klt": [200.0, 20.0], + "ka": [25.0, 5.0], + "hcno": [1000.0, 200.0], + "ih": [1000.0, 200.0], + "ihvcn": [1000.0, 200.0], + "hcnobo": [1000.0, 200.0], + "ihsgcBasalMiddle": [1000.0, 200.0], + "ihsgcApical": [1000.0, 200.0], + "kif": [100.0, 100.0], + "kis": [100.0, 10.0], + "napyr": [10, 5.0], + "ihpyr": [1000.0, 200.0], + "kdpyr": [200.0, 20.0], + "kcnq": [200, 20], + "nap": [200.0, 100.0], + } + for i, mfile in enumerate(modfile): + self.run(modfile=mfile, color=colors[i], export=export) + s = "" + # self.win.setWindowTitle('VC Plots: ' + [s+sn+'; ' for sn in modfile]) + gc.collect() + + if doKinetics: + self.win2 = pg.GraphicsWindow(title="KineticPlots") + self.win2.resize(800, 600) + self.kp1 = self.win.addPlot(title="htau") + self.computeKinetics("nav11") + + # self.win.show() + + def run(self, modfile="CaPCalyx", color="r", export=False): + if isinstance(modfile, list): + modfile = modfile[0] + + if modfile in self.tdur: + tstep = self.tdur[modfile] + else: + tstep = [200.0, 50.0] + tdelay = 5.0 + Channel = cnmodel.util.Mechanism(modfile) + leak = cnmodel.util.Mechanism("leak") + Channel.set_parameters({"gbar": 1}) + leak.set_parameters({"gbar": 1e-12}) + # if modfile == 'nacncoop': + # self.soma().nacncoop.p = 0. + # self.soma().nacncoop.KJ = 0. + # # Channel.set_parameters({'p': 0., 'KJ': 000.}) + + self.soma = cnmodel.util.Section(L=10, diam=10, mechanisms=[Channel, leak]) + if modfile == "bkpjk": + ca_init = 100e-6 + self.soma().cai = ca_init + else: + ca_init = 70e-6 + if modfile == "nacncoop": + self.soma().nacncoop.p = 0.1 + self.soma().nacncoop.KJ = 1000.0 + # Channel.set_parameters({'p': 0., 'KJ': 000.}) + h.celsius = 37.0 # set the temperature. + self.vec = {} + for var in ["time", "V", "IChan", "Vcmd"]: + self.vec[var] = h.Vector() + + h.dt = 0.025 + v_init = -65.0 + + clampV = v_init + self.vcPost = h.SEClamp(0.5, sec=self.soma) + self.vcPost.dur1 = tdelay + self.vcPost.amp1 = clampV + self.vcPost.dur2 = tstep[0] + self.vcPost.amp2 = clampV - 0.0 # just a tiny step to keep the system honest + self.vcPost.dur3 = tstep[1] + self.vcPost.amp3 = clampV + self.vcPost.rs = 1e-9 + print("soma: ", self.soma, end=" ") + # print(dir(self.vcPost)) + # print(' vcpost sec: ', self.vcPost.Section()) + + if modfile[0:2] == "ih": + stimamp = np.linspace(-140, -40, num=21, endpoint=True) + else: + stimamp = np.linspace(-100, 60, num=35, endpoint=True) + self.ivss = np.zeros((2, stimamp.shape[0])) + self.ivmin = np.zeros((2, stimamp.shape[0])) + self.ivmax = np.zeros((2, stimamp.shape[0])) + print( + ( + "I range = %6.1f-%6.1f, T = %4.1f" + % (np.min(stimamp), np.max(stimamp), h.celsius) + ) + ) + + for i, V in enumerate(stimamp): + stim = {} + stim["NP"] = 1 + stim["Sfreq"] = 1 # stimulus frequency + stim["delay"] = 5 + stim["dur"] = 100 + stim["amp"] = V + stim["PT"] = 0.0 + self.vcPost.amp2 = V + self.vec["IChan"].record(self.vcPost._ref_i, sec=self.soma) + self.vec["V"].record(self.soma()._ref_v, sec=self.soma) + self.vec["time"].record(h._ref_t) + # print + h.tstop = self.vcPost.dur1 + self.vcPost.dur2 + self.vcPost.dur3 + h.finitialize(v_init) + h.run() + self.t = np.array(self.vec["time"]) + self.ichan = np.array(self.vec["IChan"]) + self.v = np.array(self.vec["V"]) + self.p1.plot(self.t, self.ichan, pen=pg.mkPen((i, len(stimamp) * 1.5))) + self.p3.plot(self.t, self.v, pen=pg.mkPen((i, len(stimamp) * 1.5))) + (self.ivss[1, i], r2) = Util.measure( + "mean", self.t, self.ichan, tdelay + tstep[0] - 10.0, tdelay + tstep[0] + ) + (self.ivmin[1, i], r2) = Util.measure( + "min", self.t, self.ichan, tdelay + 0.1, tdelay + tstep[0] / 5.0 + ) + (self.ivmax[1, i], r2) = Util.measure( + "max", self.t, self.ichan, tdelay + 0.1, tdelay + tstep[0] / 5.0 + ) + self.ivss[0, i] = V + self.ivmin[0, i] = V + self.ivmax[0, i] = V + self.p2.plot( + self.ivss[0, :], + self.ivss[1, :], + symbol="o", + symbolSize=4.0, + pen=pg.mkPen(color), + ) + self.p2.plot( + self.ivmax[0, :], + self.ivmax[1, :], + symbol="t", + symbolSize=4.0, + pen=pg.mkPen(color), + ) + self.p5.plot( + self.ivmin[0, :], + self.ivmin[1, :], + symbol="s", + symbolSize=4.0, + pen=pg.mkPen(color), + ) + + print(export) + if export: + exporter = pg.exporters.MatplotlibExporter(self.p1) + print("exporting: " + "%s_traces.svg" % modfile) + exporter.export(fileName="%s_traces.pdf" % modfile) + exporter = pg.exporters.MatplotlibExporter(self.p3) + exporter.export("%s_command.pdf" % modfile) + exporter = pg.exporters.MatplotlibExporter(self.p2) + exporter.export("%s_IV.pdf" % modfile) + + def computeKinetics(self, ch): + pass + + +def getmechs(): + mechs = [] + for n in dir(nrn): + o = getattr(nrn, n) + if str(o) == str(nrn.Mechanism): + mechs.append(n) + return mechs + + +if __name__ == "__main__": + mechs = getmechs() + if len(sys.argv) < 2: + + print("\n\nUsage: python test_mechanisms.py ") + print(" Available mechanisms:") + + linelen = 0 + for i, n in enumerate(mechs): + if n in nottestablemechs: # 'Mechanism': + continue + print("%20s" % n, end=" ") + linelen += 20 + if linelen > 80: + print("") + + linelen = 0 + sys.exit(1) + + if sys.argv[1:] in nottestablemechs: + exit() + export = False + if len(sys.argv) > 2: + if sys.argv[2] == "export": + export = True + + if sys.argv[1] == "all": + for n in mechs: + if n in nottestablemechs: + print(("Skipping %s" % n)) + continue + else: + ck = ChannelKinetics(n) + else: + ck = ChannelKinetics(sys.argv[1], export=export) + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_mso_inputs.py b/examples/test_mso_inputs.py new file mode 100644 index 0000000..d7a683e --- /dev/null +++ b/examples/test_mso_inputs.py @@ -0,0 +1,208 @@ +""" +Test generating a series of EPSPs in a MSO cell in response to tone pip. This +test demonstrates how a binaural circuit can be constructed. + +This script: + +1. Creates multiple SGCs (base instance has 3) converging onto two bushy cells. + The 2 bushy cells then converge onto one MSO cell +2. Connects the group of sgc cells from one ear to one bushy cell, and the + other sgcs from the other ear to the + other bushy cell. +3. Specifies the CFs of the SGC cells individually. Also specifies the frequency of + and stimuli by ear allowing for "binaural beats" +4. Records the bushy and MSO cell membrane voltages, sgc spike time, and calculates + vector strengths. + +The auditory nerve spike train is generated automatically by the DummySGC class +using the tone pip. For lower-level access to the auditory nerve model, see the +test_an_model.py and test_sound_stim.py examples. + + +Usage: + python examples/test_mso_inputs.py [cochlea | matlab] + + The AN simulator that is run depends on what is available and how the script is called. + python examples/test_mso_inputs.py [cochlea | matlab] will try to use the specified + simulator. If no simulator is specified, it will try to use cochlea or matlab, in + that order. + +""" +import sys +import numpy as np +import pyqtgraph as pg +from neuron import h +from cnmodel.protocols import Protocol +from cnmodel import cells +from cnmodel.util import sound +from cnmodel.util import custom_init +import cnmodel.util.pynrnutilities as PU + + +class MSOBinauralTest(Protocol): + def run(self, temp=38.0, dt=0.025, seed=575982035, simulator=None): + ears = {"left": [500.0, 502, 498], "right": [500.0, 502.0, 498]} + + self.beatfreq = 0.0 + self.f0 = 500.0 + f0 = {"left": self.f0, "right": self.f0 + self.beatfreq} + nsgc = len(list(ears.keys())) + sgcCell = {} + bushyCell = {} + msoCell = {} + synapse = {} + self.stim = {} + self.ears = ears + self.stimdur = 0.2 + self.stimdelay = 0.02 + self.rundur = self.stimdelay + self.stimdur + 0.02 + + for i, ear in enumerate(ears.keys()): + nsgc = len(ears[ear]) # how many sgcs are specified for this ear + sgcCell[ear] = [cells.DummySGC(cf=ears[ear][k], sr=2) for k in range(nsgc)] + bushyCell[ear] = [cells.Bushy.create(temperature=temp)] + synapse[ear] = [ + sgcCell[ear][k].connect(bushyCell[ear][0]) for k in range(nsgc) + ] + self.stim[ear] = [ + sound.TonePip( + rate=100e3, + duration=self.stimdur + 0.1, + f0=f0[ear], + dbspl=80, + ramp_duration=2.5e-3, + pip_duration=self.stimdur, + pip_start=[self.stimdelay], + ) + for k in range(nsgc) + ] + for k in range(len(self.stim[ear])): + sgcCell[ear][k].set_sound_stim( + self.stim[ear][k], seed=seed + i * seed + k, simulator=simulator + ) + self["vm_bu_%s" % ear] = bushyCell[ear][0].soma(0.5)._ref_v + for k in range(30): + self["xmtr%d_%s" % (k, ear)] = synapse[ear][ + 0 + ].terminal.relsite._ref_XMTR[k] + for k in range(len(synapse[ear])): + synapse[ear][k].terminal.relsite.Dep_Flag = False # turn off depression + + msoCell = cells.MSO.create(temperature=temp) # one target MSO cell + msosyn = {} + for ear in ears: + msosyn[ear] = bushyCell[ear][0].connect(msoCell) + self.sgc_cells = sgcCell + self.bushy_cells = bushyCell + self.synapses = synapse + self.msyns = msosyn + self.msoCell = msoCell + self.all_cells = [] # hold all "real" cells (DummySGC does not have mechanisms) + for ear in list(ears.keys()): + self.all_cells.append([c for c in self.bushy_cells[ear]]) + self.all_cells.append([self.msoCell]) + + self["vm_mso"] = self.msoCell.soma(0.5)._ref_v + for k, ear in enumerate(ears.keys()): + for i in range(30): + self["mso_xmtr%d_%s" % (i, ear)] = msosyn[ + ear + ].terminal.relsite._ref_XMTR[i] + msosyn[ear].terminal.relsite.Dep_Flag = False # turn off depression + + self["t"] = h._ref_t + + h.tstop = self.rundur * 1e3 # duration of a run + h.celsius = temp + h.dt = dt + + custom_init() + # confirm that all cells are ok + for cg in self.all_cells: + for c in cg: + c.check_all_mechs() + while h.t < h.tstop: + h.fadvance() + + def show(self): + self.win = pg.GraphicsWindow() + + p5 = self.win.addPlot(title="stim") + p5.plot(self.stim["left"][0].time * 1000.0, self.stim["left"][0].sound) + + p1 = self.win.addPlot(title="Bushy Vm", row=1, col=0) + for k, ear in enumerate(self.ears.keys()): + p1.plot(self["t"], self["vm_bu_%s" % ear], pen=(k, 15)) + + p2 = self.win.addPlot(title="SGC-BU xmtr left", row=0, col=1) + for i in range(30): + p2.plot(self["t"], self["xmtr%d_left" % i], pen=(i, 15)) + p2.setXLink(p1) + p2r = self.win.addPlot(title="SGC-BU xmtr right", row=1, col=1) + for i in range(30): + p2r.plot(self["t"], self["xmtr%d_right" % i], pen=(i, 15)) + p2r.setXLink(p1) + + p3 = self.win.addPlot(title="MSO Vm", row=2, col=0) + p3.plot(self["t"], self["vm_mso"]) + p3.setXLink(p1) + + p4 = self.win.addPlot(title="BU-MSO xmtr", row=2, col=1) + for k, ear in enumerate(self.ears.keys()): + for i in range(30): + p2.plot(self["t"], self["mso_xmtr%d_%s" % (i, ear)], pen=(i, 15)) + p4.setXLink(p1) + + p4 = self.win.addPlot(title="AN spikes", row=3, col=0) + ntrain = len(self.sgc_cells["left"]) + for k in range(ntrain): + yr = [k / float(ntrain), (k + 0.8) / float(ntrain)] + vt = pg.VTickGroup( + self.sgc_cells["left"][k]._spiketrain, yrange=yr, pen=(k, 15) + ) + p4.addItem(vt) + p4.setXLink(p1) + p5.setXLink(p1) + + # phaselocking calculations + phasewin = [self.stimdelay + 0.2 * self.stimdur, self.stimdelay + self.stimdur] + msospk = PU.findspikes(self["t"], self["vm_mso"], -30.0) + + spkin = msospk[np.where(msospk > phasewin[0] * 1e3)] + spikesinwin = spkin[np.where(spkin <= phasewin[1] * 1e3)[0]] + + # set freq for VS calculation + f0 = self.f0 + fb = self.beatfreq + vs = PU.vector_strength(spikesinwin, f0) + + print( + "MSO Vector Strength at %.1f: %7.3f, d=%.2f (us) Rayleigh: %7.3f p = %.3e n = %d" + % (f0, vs["r"], vs["d"] * 1e6, vs["R"], vs["p"], vs["n"]) + ) + if fb > 0: + vsb = PU.vector_strength(spikesinwin, fb) + print( + "MSO Vector Strength to beat at %.1f: %7.3f, d=%.2f (us) Rayleigh: %7.3f p = %.3e n = %d" + % (fb, vsb["r"], vsb["d"] * 1e6, vsb["R"], vsb["p"], vsb["n"]) + ) + (hist, binedges) = np.histogram(vs["ph"]) + p6 = self.win.addPlot(title="VS", row=3, col=1) + p6.plot( + binedges, hist, stepMode=True, fillBrush=(100, 100, 255, 150), fillLevel=0 + ) + p6.setXRange(0.0, 2 * np.pi) + + self.win.show() + + +if __name__ == "__main__": + simulator = None + if len(sys.argv) > 1: + simulator = sys.argv[1] + prot = MSOBinauralTest() + prot.run(simulator=simulator) + prot.show() + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_physiology.py b/examples/test_physiology.py new file mode 100644 index 0000000..c315aba --- /dev/null +++ b/examples/test_physiology.py @@ -0,0 +1,704 @@ +""" +Test principal cell responses to tone pips of varying frequency and intensity. + +This is an example of model construction from a very high level--we specify +only which populations of cells are present and which ones should be connected. +The population and cell classes take care of all the details of generating the +network needed to support a small number of output cells. + +Note: run time for this example can be very long. To speed things up, reduce +n_frequencies or n_levels, or reduce the number of selected output cells (see +cells_per_band). + +""" + +import os, sys, time +from collections import OrderedDict +import numpy as np +import scipy.stats +from neuron import h +import pyqtgraph as pg +import pyqtgraph.multiprocess as mp +from pyqtgraph.Qt import QtGui, QtCore +from cnmodel import populations +from cnmodel.util import sound, random_seed +from cnmodel.protocols import Protocol +import timeit + + +class CNSoundStim(Protocol): + def __init__(self, seed, temp=34.0, dt=0.025, synapsetype="simple"): + Protocol.__init__(self) + + self.seed = seed + self.temp = temp + self.dt = dt + # self.synapsetype = synapsetype # simple or multisite + + # Seed now to ensure network generation is stable + random_seed.set_seed(seed) + # Create cell populations. + # This creates a complete set of _virtual_ cells for each population. No + # cells are instantiated at this point. + self.sgc = populations.SGC(model="dummy") + self.bushy = populations.Bushy() + self.dstellate = populations.DStellate() + self.tstellate = populations.TStellate() + self.tuberculoventral = populations.Tuberculoventral() + + pops = [ + self.sgc, + self.dstellate, + self.tuberculoventral, + self.tstellate, + self.bushy, + ] + self.populations = OrderedDict([(pop.type, pop) for pop in pops]) + + # set synapse type to use in the sgc population - simple is fast, multisite is slower + # (eventually, we could do this for all synapse types..) + self.sgc._synapsetype = synapsetype + + # Connect populations. + # This only defines the connections between populations; no synapses are + # created at this stage. + self.sgc.connect( + self.bushy, self.dstellate, self.tuberculoventral, self.tstellate + ) + self.dstellate.connect( + self.bushy, self.tstellate + ) # should connect to dstellate as well? + self.tuberculoventral.connect(self.bushy, self.tstellate) + self.tstellate.connect(self.bushy) + + # Select cells to record from. + # At this time, we actually instantiate the selected cells. + + # Pick a single bushy cell near 16kHz, with medium-SR inputs + bc = self.bushy.cells + msr_cells = bc[bc["sgc_sr"] == 1] # filter for msr cells + ind = np.argmin(np.abs(msr_cells["cf"] - 16e3)) # find the one closest to 16kHz + cell_id = msr_cells[ind]["id"] + self.bushy.create_cells([cell_id]) # instantiate just one cell + + # Now create the supporting circuitry needed to drive the cells we selected. + # At this time, cells are created in all populations and automatically + # connected with synapses. + self.bushy.resolve_inputs(depth=2) + # self.tstellate.resolve_inputs(depth=2) + # Note that using depth=2 indicates the level of recursion to use when + # resolving inputs. For example, resolving inputs for the bushy cell population + # (level 1) creates presynaptic cells in the dstellate population, and resolving + # inputs for the dstellate population (level 2) creates presynaptic cells in the + # sgc population. + + def run(self, stim, seed): + """Run the network simulation with *stim* as the sound source and a unique + *seed* used to configure the random number generators. + """ + self.reset() + + # Generate 2 new seeds for the SGC spike generator and for the NEURON simulation + rs = np.random.RandomState() + rs.seed(self.seed ^ seed) + seed1, seed2 = rs.randint(0, 2 ** 32, 2) + random_seed.set_seed(seed1) + self.sgc.set_seed(seed2) + + self.sgc.set_sound_stim(stim, parallel=False) + + # set up recording vectors + for pop in self.bushy, self.dstellate, self.tstellate, self.tuberculoventral: + for ind in pop.real_cells(): + cell = pop.get_cell(ind) + self[cell] = cell.soma(0.5)._ref_v + self["t"] = h._ref_t + + h.tstop = stim.duration * 1000 + h.celsius = self.temp + h.dt = self.dt + + print("init..") + self.custom_init() + print("start..") + last_update = time.time() + while h.t < h.tstop: + h.fadvance() + now = time.time() + if now - last_update > 1.0: + print("%0.2f / %0.2f" % (h.t, h.tstop)) + last_update = now + + # record vsoma and spike times for all cells + vec = {} + for k in self._vectors: + v = self[k].copy() + if k == "t": + vec[k] = v + continue + spike_inds = np.argwhere((v[1:] > -20) & (v[:-1] <= -20))[:, 0] + spikes = self["t"][spike_inds] + pop = k.type + cell_ind = getattr(self, pop).get_cell_index(k) + vec[(pop, cell_ind)] = [v, spikes] + + # record SGC spike trains + for ind in self.sgc.real_cells(): + cell = self.sgc.get_cell(ind) + vec[("sgc", ind)] = [None, cell._spiketrain] + + return vec + + +class NetworkSimDisplay(pg.QtGui.QSplitter): + def __init__(self, prot, results, baseline, response): + pg.QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal) + self.selected_cell = None + + self.prot = prot + self.baseline = baseline # (start, stop) + self.response = response # (start, stop) + + self.ctrl = QtGui.QWidget() + self.layout = pg.QtGui.QVBoxLayout() + self.layout.setContentsMargins(0, 0, 0, 0) + self.ctrl.setLayout(self.layout) + self.addWidget(self.ctrl) + + self.nv = NetworkVisualizer(prot.populations) + self.layout.addWidget(self.nv) + self.nv.cell_selected.connect(self.nv_cell_selected) + + self.stim_combo = pg.QtGui.QComboBox() + self.layout.addWidget(self.stim_combo) + self.trial_combo = pg.QtGui.QComboBox() + self.layout.addWidget(self.trial_combo) + self.results = OrderedDict() + self.stim_order = [] + freqs = set() + levels = set() + max_iter = 0 + for k, v in list(results.items()): + f0, dbspl, iteration = k + max_iter = max(max_iter, iteration) + stim, result = v + key = "f0: %0.0f dBspl: %0.0f" % (f0, dbspl) + self.results.setdefault(key, [stim, {}]) + self.results[key][1][iteration] = result + self.stim_order.append((f0, dbspl)) + freqs.add(f0) + levels.add(dbspl) + self.stim_combo.addItem(key) + self.freqs = sorted(list(freqs)) + self.levels = sorted(list(levels)) + self.iterations = max_iter + 1 + self.trial_combo.addItem("all trials") + for i in range(self.iterations): + self.trial_combo.addItem(str(i)) + + self.stim_combo.currentIndexChanged.connect(self.stim_selected) + self.trial_combo.currentIndexChanged.connect(self.trial_selected) + + self.tuning_plot = pg.PlotWidget() + self.tuning_plot.setLogMode(x=True, y=False) + self.tuning_plot.scene().sigMouseClicked.connect(self.tuning_plot_clicked) + self.layout.addWidget(self.tuning_plot) + + self.tuning_img = pg.ImageItem() + self.tuning_plot.addItem(self.tuning_img) + + df = np.log10(self.freqs[1]) - np.log10(self.freqs[0]) + dl = self.levels[1] - self.levels[0] + self.stim_rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, df, dl)) + self.stim_rect.setPen(pg.mkPen("c")) + self.stim_rect.setZValue(20) + self.tuning_plot.addItem(self.stim_rect) + + # self.network_tree = NetworkTree(self.prot) + # self.layout.addWidget(self.network_tree) + + self.pw = pg.GraphicsLayoutWidget() + self.addWidget(self.pw) + + self.stim_plot = self.pw.addPlot() + self.pw.ci.layout.setRowFixedHeight(0, 100) + + self.pw.nextRow() + self.cell_plot = self.pw.addPlot(labels={"left": "Vm"}) + + self.pw.nextRow() + self.input_plot = self.pw.addPlot( + labels={"left": "input #", "bottom": "time"}, title="Input spike times" + ) + self.input_plot.setXLink(self.cell_plot) + self.stim_plot.setXLink(self.cell_plot) + + self.stim_selected() + + def update_stim_plot(self): + stim = self.selected_stim + self.stim_plot.plot(stim.time * 1000, stim.sound, clear=True, antialias=True) + + def update_raster_plot(self): + self.input_plot.clear() + if self.selected_cell is None: + return + pop, ind = self.selected_cell + + rec = pop._cells[ind] + i = 0 + plots = [] + # plot spike times for all presynaptic cells + labels = [] + if rec["connections"] == 0: + return + + pop_colors = { + "dstellate": "y", + "tuberculoventral": "r", + "sgc": "g", + "tstellate": "b", + } + pop_symbols = { + "dstellate": "x", + "tuberculoventral": "+", + "sgc": "t", + "tstellate": "o", + } + pop_order = [self.prot.sgc, self.prot.dstellate, self.prot.tuberculoventral] + trials = self.selected_trials() + for pop in pop_order: + pre_inds = rec["connections"].get(pop, []) + for preind in pre_inds: + # iterate over all trials + for j in trials: + result = self.selected_results[j] + spikes = result[(pop.type, preind)][1] + y = np.ones(len(spikes)) * i + j / ( + 2.0 * len(self.selected_results) + ) + self.input_plot.plot( + spikes, + y, + pen=None, + symbolBrush=pop_colors[pop.type], + symbol="+", + symbolPen=None, + ) + i += 1 + labels.append(pop.type + " " + str(preind)) + self.input_plot.getAxis("left").setTicks([list(enumerate(labels))]) + + def update_cell_plot(self): + self.cell_plot.clear() + if self.selected_cell is None: + return + pop, cell_ind = self.selected_cell + + self.cell_plot.setTitle( + "%s %d %s" % (pop.type, cell_ind, str(self.stim_combo.currentText())) + ) + trials = self.selected_trials() + for i in trials: + result = self.selected_results[i] + y = result[(pop.type, cell_ind)][0] + if y is not None: + p = self.cell_plot.plot( + self.selected_results[0]["t"], + y, + name="%s-%d" % self.selected_cell, + antialias=True, + pen=(i, len(self.selected_results) * 1.5), + ) + # p.curve.setClickable(True) + # p.sigClicked.connect(self.cell_curve_clicked) + # p.cell_ind = ind + + def tuning_plot_clicked(self, event): + spos = event.scenePos() + stimpos = self.tuning_plot.plotItem.vb.mapSceneToView(spos) + x = 10 ** stimpos.x() + y = stimpos.y() + + best = None + for stim, result in list(self.results.values()): + f0 = stim.opts["f0"] + dbspl = stim.opts["dbspl"] + if x < f0 or y < dbspl: + continue + if best is None: + best = stim + continue + if f0 > best.opts["f0"] or dbspl > best.opts["dbspl"]: + best = stim + continue + + if best is None: + return + self.select_stim(best.opts["f0"], best.opts["dbspl"]) + + def nv_cell_selected(self, nv, cell): + self.select_cell(*cell) + + def stim_selected(self): + key = str(self.stim_combo.currentText()) + results = self.results[key] + self.selected_results = results[1] + self.selected_stim = results[0] + self.update_stim_plot() + self.update_raster_plot() + self.update_cell_plot() + + self.stim_rect.setPos(np.log10(results[0].opts["f0"]), results[0].opts["dbspl"]) + + def trial_selected(self): + self.update_raster_plot() + self.update_cell_plot() + self.update_tuning() + + def selected_trials(self): + if self.trial_combo.currentIndex() == 0: + return list(range(self.iterations)) + else: + return [self.trial_combo.currentIndex() - 1] + + def select_stim(self, f0, dbspl): + i = self.stim_order.index((f0, dbspl)) + self.stim_combo.setCurrentIndex(i) + + def select_cell(self, pop, cell_id): + self.selected_cell = pop, cell_id + self.update_tuning() + self.update_cell_plot() + self.update_raster_plot() + + # def cell_curve_clicked(self, c): + # if self.selected is not None: + # pen = self.selected.curve.opts['pen'] + # pen.setWidth(1) + # self.selected.setPen(pen) + + # pen = c.curve.opts['pen'] + # pen.setWidth(3) + # c.setPen(pen) + # self.selected = c + + # self.show_cell(c.cell_ind) + + def update_tuning(self): + # update matrix image + if self.selected_cell is None: + return + + pop, ind = self.selected_cell + fvals = set() + lvals = set() + + # first get lists of all frequencies and levels in the matrix + for stim, vec in list(self.results.values()): + fvals.add(stim.key()["f0"]) + lvals.add(stim.key()["dbspl"]) + fvals = sorted(list(fvals)) + lvals = sorted(list(lvals)) + + # Get spontaneous rate statistics + spont_spikes = 0 + spont_time = 0 + for stim, iterations in list(self.results.values()): + for vec in list(iterations.values()): + spikes = vec[(pop.type, ind)][1] + spont_spikes += ( + (spikes >= self.baseline[0]) & (spikes < self.baseline[1]) + ).sum() + spont_time += self.baseline[1] - self.baseline[0] + spont_rate = spont_spikes / spont_time + + # next count the number of spikes for the selected cell at each point in the matrix + matrix = np.zeros((len(fvals), len(lvals))) + trials = self.selected_trials() + for stim, iteration in list(self.results.values()): + for i in trials: + vec = iteration[i] + spikes = vec[(pop.type, ind)][1] + n_spikes = ( + (spikes >= self.response[0]) & (spikes < self.response[1]) + ).sum() + i = fvals.index(stim.key()["f0"]) + j = lvals.index(stim.key()["dbspl"]) + matrix[i, j] += n_spikes - spont_rate * ( + self.response[1] - self.response[0] + ) + matrix /= self.iterations + + # plot and scale the matrix image + # note that the origin (lower left) of each image pixel indicates its actual test freq/level. + self.tuning_img.setImage(matrix) + self.tuning_img.resetTransform() + self.tuning_img.setPos(np.log10(min(fvals)), min(lvals)) + self.tuning_img.scale( + (np.log10(max(fvals)) - np.log10(min(fvals))) / (len(fvals) - 1), + (max(lvals) - min(lvals)) / (len(lvals) - 1), + ) + + +class NetworkTree(QtGui.QTreeWidget): + def __init__(self, prot): + self.prot = prot + QtGui.QTreeWidget.__init__(self) + self.setColumnCount(2) + + self.update_tree() + + def update_tree(self): + for pop_name in ["bushy", "tstellate", "dstellate", "tuberculoventral", "sgc"]: + if not hasattr(self.prot, pop_name): + continue + pop = getattr(self.prot, pop_name) + grp = QtGui.QTreeWidgetItem([pop_name]) + self.addTopLevelItem(grp) + for cell in pop.real_cells(): + self.add_cell(grp, pop, cell) + + def add_cell(self, grp_item, pop, cell): + item = QtGui.QTreeWidgetItem([str(cell)]) + grp_item.addChild(item) + all_conns = pop.cell_connections(cell) + if all_conns == 0: + return + for cpop, conns in list(all_conns.items()): + pop_grp = QtGui.QTreeWidgetItem([cpop.type, str(conns)]) + item.addChild(pop_grp) + + +class NetworkVisualizer(pg.PlotWidget): + + cell_selected = pg.QtCore.Signal(object, object) + + def __init__(self, populations): + self.pops = populations + pg.PlotWidget.__init__(self) + self.setLogMode(x=True, y=False) + + self.cells = pg.ScatterPlotItem(clickable=True) + self.cells.setZValue(10) + self.addItem(self.cells) + self.cells.sigClicked.connect(self.cells_clicked) + + self.selected = pg.ScatterPlotItem() + self.selected.setZValue(20) + self.addItem(self.selected) + + self.connections = pg.PlotCurveItem() + self.addItem(self.connections) + + # first assign positions of all cells + cells = [] + for y, pop in enumerate(self.pops.values()): + pop.cell_spots = [] + pop.fwd_connections = {} + for i, cell in enumerate(pop._cells): + pos = (np.log10(cell["cf"]), y) + real = cell["cell"] != 0 + if not real: + pop.cell_spots.append(None) + continue + brush = pg.mkBrush("b") if real else pg.mkBrush(255, 255, 255, 30) + spot = { + "x": pos[0], + "y": pos[1], + "symbol": "o" if real else "x", + "brush": brush, + "pen": None, + "data": (pop, i), + } + cells.append(spot) + pop.cell_spots.append(spot) + + self.cells.setData(cells) + + self.getAxis("left").setTicks([list(enumerate(self.pops.keys()))]) + + # now assign connection lines and record forward connectivity + con_x = [] + con_y = [] + for pop in list(self.pops.values()): + for i, cell in enumerate(pop._cells): + conns = cell["connections"] + if conns == 0: + continue + for prepop, precells in list(conns.items()): + spot = pop.cell_spots[i] + if spot is None: + continue + p1 = spot["x"], spot["y"] + for j in precells: + prepop.fwd_connections.setdefault(j, []) + prepop.fwd_connections[j].append((pop, i)) + spot2 = prepop.cell_spots[j] + if spot2 is None: + return + p2 = spot2["x"], spot2["y"] + con_x.extend([p1[0], p2[0]]) + con_y.extend([p1[1], p2[1]]) + self.connections.setData( + x=con_x, y=con_y, connect="pairs", pen=(255, 255, 255, 60) + ) + + def cells_clicked(self, *args): + selected = None + for spot in args[1]: + # find the first real cell + pop, i = spot.data() + if pop._cells[i]["cell"] != 0: + selected = spot + break + if selected is None: + self.selected.hide() + return + + rec = pop._cells[i] + pos = selected.pos() + spots = [ + { + "x": pos.x(), + "y": pos.y(), + "size": 15, + "symbol": "o", + "pen": "y", + "brush": "b", + } + ] + + # display presynaptic cells + if rec["connections"] != 0: + for prepop, preinds in list(rec["connections"].items()): + for preind in preinds: + spot = prepop.cell_spots[preind].copy() + spot["size"] = 15 + spot["brush"] = "r" + spots.append(spot) + + # display postsynaptic cells + for postpop, postind in pop.fwd_connections.get(i, []): + spot = postpop.cell_spots[postind].copy() + spot["size"] = 15 + spot["brush"] = "g" + spots.append(spot) + + self.selected.setData(spots) + self.selected.show() + + self.cell_selected.emit(self, selected.data()) + + +if __name__ == "__main__": + import pickle, os, sys + + app = pg.mkQApp() + pg.dbg() + + # Create a sound stimulus and use it to generate spike trains for the SGC + # population + stims = [] + parallel = True + + nreps = 5 + fmin = 4e3 + fmax = 32e3 + octavespacing = 1 / 8.0 + # octavespacing = 1. + n_frequencies = int(np.log2(fmax / fmin) / octavespacing) + 1 + fvals = ( + np.logspace( + np.log2(fmin / 1000.0), + np.log2(fmax / 1000.0), + num=n_frequencies, + endpoint=True, + base=2, + ) + * 1000.0 + ) + + n_levels = 11 + # n_levels = 3 + levels = np.linspace(20, 100, n_levels) + + print(("Frequencies:", fvals / 1000.0)) + print(("Levels:", levels)) + + syntype = "multisite" + path = os.path.dirname(__file__) + cachepath = os.path.join(path, "cache") + if not os.path.isdir(cachepath): + os.mkdir(cachepath) + + seed = 34657845 + prot = CNSoundStim(seed=seed, synapsetype=syntype) + i = 0 + + start_time = timeit.default_timer() + + # stimpar = {'dur': 0.06, 'pip': 0.025, 'start': [0.02], 'baseline': [10, 20], 'response': [20, 45]} + stimpar = { + "dur": 0.2, + "pip": 0.04, + "start": [0.1], + "baseline": [50, 100], + "response": [100, 140], + } + tasks = [] + for f in fvals: + for db in levels: + for i in range(nreps): + tasks.append((f, db, i)) + + results = {} + workers = 1 if not parallel else None + tot_runs = len(fvals) * len(levels) * nreps + with mp.Parallelize( + enumerate(tasks), + results=results, + progressDialog="Running parallel simulation..", + workers=workers, + ) as tasker: + for i, task in tasker: + f, db, iteration = task + stim = sound.TonePip( + rate=100e3, + duration=stimpar["dur"], + f0=f, + dbspl=db, # dura 0.2, pip_start 0.1 pipdur 0.04 + ramp_duration=2.5e-3, + pip_duration=stimpar["pip"], + pip_start=stimpar["start"], + ) + + print(("=== Start run %d/%d ===" % (i + 1, tot_runs))) + cachefile = os.path.join( + cachepath, + "seed=%d_f0=%f_dbspl=%f_syntype=%s_iter=%d.pk" + % (seed, f, db, syntype, iteration), + ) + if "--ignore-cache" in sys.argv or not os.path.isfile(cachefile): + result = prot.run(stim, seed=i) + pickle.dump(result, open(cachefile, "wb")) + else: + print(" (Loading cached results)") + result = pickle.load(open(cachefile, "rb")) + tasker.results[(f, db, iteration)] = (stim, result) + print(("--- finished run %d/%d ---" % (i + 1, tot_runs))) + + # get time of run before display + elapsed = timeit.default_timer() - start_time + print( + "Elapsed time for %d stimuli: %f (%f sec per stim), synapses: %s" + % (len(tasks), elapsed, elapsed / len(tasks), prot.bushy._synapsetype) + ) + + nd = NetworkSimDisplay( + prot, results, baseline=stimpar["baseline"], response=stimpar["response"] + ) + nd.show() + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_populations.py b/examples/test_populations.py new file mode 100644 index 0000000..6103c82 --- /dev/null +++ b/examples/test_populations.py @@ -0,0 +1,59 @@ +""" +Test connection between two cell populations. + +Usage: python test_populations.py + +This script: + +1. Creates two cell populations (pop1 and pop2) +2. Connects pop1 => pop2 +3. Instantiates a single cell in pop2 +4. Automatically generates presynaptic cells and synapses from pop1 +5. Stimulates presynaptic cells and records postsynaptically + +This is a high-level approach to generating networks in that the supporting +cells (those in pop1) are created automatically based on expected patterns +of connectivity in the cochlear nucleus. A lower-level approach is demonstrated +in test_synapses.py, in which the individual pre- and postsynaptic cells are +manually created and connected. +""" +from cnmodel import populations +from cnmodel.protocols import PopulationTest +import pyqtgraph as pg +import sys + + +def testpopulation(): + if len(sys.argv) < 3: + print("Usage: python test_populations.py ") + sys.exit(1) + + pop_types = { + "sgc": populations.SGC, + "bushy": populations.Bushy, + "tstellate": populations.TStellate, + "dstellate": populations.DStellate, + "pyramidal": populations.Pyramidal, + "tuberculoventral": populations.Tuberculoventral, + } + + pops = [] + for cell_type in sys.argv[1:3]: + if cell_type not in pop_types: + print( + '\nUnsupported cell type: "%s". Options are %s' + % (cell_type, list(pop_types.keys())) + ) + sys.exit(-1) + pops.append(pop_types[cell_type]()) + + pt = PopulationTest() + pt.run(pops) + pt.show() + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() + + +if __name__ == "__main__": + testpopulation() diff --git a/examples/test_sgc_input.py b/examples/test_sgc_input.py new file mode 100644 index 0000000..5c72e2f --- /dev/null +++ b/examples/test_sgc_input.py @@ -0,0 +1,96 @@ +""" +Test generating a series of EPSPs in a bushy cell in response to tone pip. + +This script: + +1. Creates one SGC and one bushy cell. +2. Connects the two cells with a synapse. +3. Specifies a tone pip input to the SGC cell. +4. Records the bushy cell Vm. + +The auditory nerve spike train is generated automatically by the DummySGC class +using the tone pip. For lower-level access to the auditory nerve model, see the +test_an_model.py and test_sound_stim.py examples. + +The simulator that is run depends on what is available and how the script is called. +python examples/test_sgc_input.py [cochlea | matlab] will try to use the specified +simulator. If no simulator is specified, it will try to use cochlea or matlab, in +that order. + +""" +import sys +import numpy as np +import pyqtgraph as pg +from neuron import h +from cnmodel.protocols import Protocol +from cnmodel import cells +from cnmodel.util import sound +from cnmodel.util import custom_init + + +class SGCInputTest(Protocol): + def run(self, temp=34.0, dt=0.025, seed=575982035, simulator=None): + preCell = cells.DummySGC(cf=4000, sr=2) + postCell = cells.Bushy.create() + synapse = preCell.connect(postCell) + self.pre_cell = preCell + self.post_cell = postCell + self.synapse = synapse + + self.stim = sound.TonePip( + rate=100e3, + duration=0.1, + f0=4000, + dbspl=80, + ramp_duration=2.5e-3, + pip_duration=0.04, + pip_start=[0.02], + ) + + preCell.set_sound_stim(self.stim, seed=seed, simulator=simulator) + + self["vm"] = postCell.soma(0.5)._ref_v + # self['prevm'] = preCell.soma(0.5)._ref_v + for i in range(30): + self["xmtr%d" % i] = synapse.terminal.relsite._ref_XMTR[i] + synapse.terminal.relsite.Dep_Flag = False + self["t"] = h._ref_t + + h.tstop = 100.0 # duration of a run + h.celsius = temp + h.dt = dt + + custom_init() + h.run() + + def show(self): + self.win = pg.GraphicsWindow() + + p1 = self.win.addPlot(title="Bushy Vm") + p1.plot(self["t"], self["vm"]) + p2 = self.win.addPlot(title="xmtr", row=1, col=0) + for i in range(30): + p2.plot(self["t"], self["xmtr%d" % i], pen=(i, 15)) + p2.setXLink(p1) + + p3 = self.win.addPlot(title="AN spikes", row=2, col=0) + vt = pg.VTickGroup(self.pre_cell._spiketrain) + p3.addItem(vt) + p3.setXLink(p1) + + p4 = self.win.addPlot(title="stim", row=3, col=0) + p4.plot(self.stim.time * 1000, self.stim.sound) + p4.setXLink(p1) + self.win.show() + + +if __name__ == "__main__": + simulator = None + if len(sys.argv) > 1: + simulator = sys.argv[1] + prot = SGCInputTest() + prot.run(simulator=simulator) + prot.show() + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_sgc_input_PSTH.py b/examples/test_sgc_input_PSTH.py new file mode 100644 index 0000000..b7173f6 --- /dev/null +++ b/examples/test_sgc_input_PSTH.py @@ -0,0 +1,431 @@ +import sys +import argparse +import numpy as np +import pyqtgraph as pg +from neuron import h +from cnmodel.protocols import Protocol +from cnmodel import cells +from cnmodel.util import sound +from cnmodel.util import custom_init +import cnmodel.util.pynrnutilities as PU +from cnmodel import data + +species = "mouse" # tables for other species do not yet exist + + +def runTrial(cell, info): + """ + info is a dict + """ + if cell == "bushy": + post_cell = cells.Bushy.create(species=species) + elif cell == "tstellate": + post_cell = cells.TStellate.create(species=species) + elif cell == "octopus": + post_cell = cells.Octopus.create(species=species) + elif cell == "dstellate": + post_cell = cells.DStellate.create(species=species) + elif cell == "tuberculoventral": + post_cell = cells.DStellate.create(species=species) + elif cell == "pyramidal": + post_cell = cells.DStellate.create(species=species) + else: + raise ValueError("cell %s is not yet implemented for PSTH testing" % self.cell) + pre_cells = [] + synapses = [] + j = 0 + xmtr = {} + for nsgc, sgc in enumerate(range(info["n_sgc"])): + pre_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + if synapseType == "simple": + synapses.append(pre_cells[-1].connect(post_cell, type=synapseType)) + synapses[-1].terminal.netcon.weight[0] = info["gmax"] + elif synapseType == "multisite": + synapses.append( + pre_cells[-1].connect( + post_cell, + post_opts={"AMPAScale": 1.0, "NMDAScale": 1.0}, + type=synapseType, + ) + ) + for i in range(synapses[-1].terminal.n_rzones): + xmtr["xmtr%04d" % j] = h.Vector() + xmtr["xmtr%04d" % j].record(synapses[-1].terminal.relsite._ref_XMTR[i]) + j = j + 1 + synapses[ + -1 + ].terminal.relsite.Dep_Flag = False # no depression in these simulations + pre_cells[-1].set_sound_stim( + info["stim"], seed=info["seed"] + nsgc, simulator=info["simulator"] + ) + Vm = h.Vector() + Vm.record(post_cell.soma(0.5)._ref_v) + rtime = h.Vector() + rtime.record(h._ref_t) + h.tstop = 1e3 * info["run_duration"] # duration of a run + h.celsius = info["temp"] + h.dt = info["dt"] + post_cell.cell_initialize() + info["init"]() + h.t = 0.0 + h.run() + return { + "time": np.array(rtime), + "vm": Vm.to_python(), + "xmtr": xmtr, + "pre_cells": pre_cells, + "post_cell": post_cell, + "synapses": synapses, + } + + +class SGCInputTestPSTH(Protocol): + def set_cell(self, cell="bushy"): + self.cell = cell + + def run( + self, + temp=34.0, + dt=0.025, + seed=575982035, + reps=10, + stimulus="tone", + simulator="cochlea", + parallelize=False, + ): + assert stimulus in ["tone", "SAM", "clicks"] # cases available + assert self.cell in [ + "bushy", + "tstellate", + "octopus", + "dstellate", + "tuberculoventral", + "pyramidal", + ] + self.nrep = reps + self.stimulus = stimulus + self.run_duration = 0.20 # in seconds + self.pip_duration = 0.05 # in seconds + self.pip_start = [0.1] # in seconds + self.Fs = 100e3 # in Hz + self.f0 = 4000.0 # stimulus in Hz + self.cf = 4000.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 0.0 # % mod depth, Hz + self.dbspl = 50.0 + self.simulator = simulator + self.sr = 2 # set SR group + if self.stimulus == "SAM": + self.dMod = 100.0 + self.stim = sound.SAMTone( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + fmod=self.fMod, + dmod=self.dMod, + dbspl=self.dbspl, + ramp_duration=2.5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + if self.stimulus == "tone": + self.f0 = 4000.0 + self.cf = 4000.0 + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=2.5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + + if self.stimulus == "clicks": + self.click_rate = 0.020 # msec + self.stim = sound.ClickTrain( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + click_start=0.010, + click_duration=100.0e-6, + click_interval=self.click_rate, + nclicks=int((self.run_duration - 0.01) / self.click_rate), + ramp_duration=2.5e-3, + ) + + n_sgc = data.get( + "convergence", species=species, post_type=self.cell, pre_type="sgc" + )[0] + self.n_sgc = int(np.round(n_sgc)) + # for simple synapses, need this value: + self.AMPA_gmax = ( + data.get( + "sgc_synapse", species=species, post_type=self.cell, field="AMPA_gmax" + )[0] + / 1e3 + ) # convert nS to uS for NEURON + self.vms = [None for n in range(self.nrep)] + self.synapses = [None for n in range(self.nrep)] + self.xmtrs = [None for n in range(self.nrep)] + self.pre_cells = [None for n in range(self.nrep)] + self.time = [None for n in range(self.nrep)] + info = { + "n_sgc": self.n_sgc, + "gmax": self.AMPA_gmax, + "stim": self.stim, + "simulator": self.simulator, + "cf": self.cf, + "sr": self.sr, + "seed": seed, + "run_duration": self.run_duration, + "temp": temp, + "dt": dt, + "init": custom_init, + } + if not parallelize: + for nr in range(self.nrep): + info["seed"] = seed + 3 * self.n_sgc * nr + res = runTrial(self.cell, info) + # res contains: {'time': time, 'vm': Vm, 'xmtr': xmtr, 'pre_cells': pre_cells, 'post_cell': post_cell} + self.pre_cells[nr] = res["pre_cells"] + self.time[nr] = res["time"] + self.xmtr = {k: v.to_python() for k, v in list(res["xmtr"].items())} + self.vms[nr] = res["vm"] + self.synapses[nr] = res["synapses"] + self.xmtrs[nr] = self.xmtr + + if parallelize: + pass + + def show(self): + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + Fs = self.Fs + p1 = self.win.addPlot( + title="Stimulus", row=0, col=0, labels={"bottom": "T (ms)", "left": "V"} + ) + p1.plot(self.stim.time * 1000, self.stim.sound, pen=pg.mkPen("k", width=0.75)) + p1.setXLink(p1) + + p2 = self.win.addPlot( + title="AN spikes", + row=1, + col=0, + labels={"bottom": "T (ms)", "left": "AN spikes (first trial)"}, + ) + for nr in range(self.nrep): + xan = [] + yan = [] + for k in range(len(self.pre_cells[nr])): + r = self.pre_cells[nr][k]._spiketrain + xan.extend(r) + yr = k + np.zeros_like(r) + 0.2 + yan.extend(yr) + c = pg.PlotCurveItem() + xp = np.repeat(np.array(xan), 2) + yp = np.repeat(np.array(yan), 2) + yp[1::2] = yp[::2] + 0.6 + c.setData( + xp.flatten(), + yp.flatten(), + connect="pairs", + pen=pg.mkPen(pg.intColor(nr, self.nrep), hues=self.nrep, width=1.0), + ) + p2.addItem(c) + p2.setXLink(p1) + + p3 = self.win.addPlot( + title="%s Spikes" % self.cell, + row=2, + col=0, + labels={"bottom": "T (ms)", "left": "Trial #"}, + ) + xcn = [] + ycn = [] + xspks = [] + for k in range(self.nrep): + bspk = PU.findspikes(self.time[k], self.vms[k], -35.0) + xcn.extend(bspk) + yr = k + np.zeros_like(bspk) + 0.2 + ycn.extend(yr) + d = pg.PlotCurveItem() + xp = np.repeat(np.array(xcn), 2) + yp = np.repeat(np.array(ycn), 2) + yp[1::2] = yp[::2] + 0.6 + d.setData( + xp.flatten(), yp.flatten(), connect="pairs", pen=pg.mkPen("k", width=1.5) + ) + p3.addItem(d) + p3.setXLink(p1) + + p4 = self.win.addPlot( + title="%s Vm" % self.cell, + row=3, + col=0, + labels={"bottom": "T (ms)", "left": "Vm (mV)"}, + ) + for nr in range(self.nrep): + p4.plot( + self.time[nr], + self.vms[nr], + pen=pg.mkPen(pg.intColor(nr, self.nrep), hues=self.nrep, width=1.0), + ) + p4.setXLink(p1) + + p5 = self.win.addPlot( + title="xmtr", row=0, col=1, labels={"bottom": "T (ms)", "left": "gSyn"} + ) + if synapseType == "multisite": + for nr in [0]: + syn = self.synapses[nr] + j = 0 + for k in range(self.n_sgc): + synapse = syn[k] + for i in range(synapse.terminal.n_rzones): + p5.plot( + self.time[nr], + self.xmtrs[nr]["xmtr%04d" % j], + pen=pg.mkPen( + pg.intColor(nr, self.nrep), hues=self.nrep, width=1.0 + ), + ) + j = j + 1 + p5.setXLink(p1) + + p6 = self.win.addPlot( + title="AN PSTH", + row=1, + col=1, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + bins = np.arange(0, 200, 1) + (hist, binedges) = np.histogram(xan, bins) + curve6 = p6.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + + p7 = self.win.addPlot( + title="%s PSTH" % self.cell, + row=2, + col=1, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + bins = np.arange(0, 200, 1) + (hist, binedges) = np.histogram(xcn, bins) + curve7 = p7.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + + # p6 = self.win.addPlot(title='AN phase', row=1, col=1) + # phasewin = [self.pip_start[0] + 0.25*self.pip_duration, self.pip_start[0] + self.pip_duration] + # prespk = self.pre_cells[0]._spiketrain # just sample one... + # spkin = prespk[np.where(prespk > phasewin[0]*1e3)] + # spikesinwin = spkin[np.where(spkin <= phasewin[1]*1e3)] + # print "\nCell type: %s" % self.cell + # print "Stimulus: " + # + # # set freq for VS calculation + # if self.stimulus == 'tone': + # f0 = self.f0 + # print "Tone: f0=%.3f at %3.1f dbSPL, cell CF=%.3f" % (self.f0, self.dbspl, self.cf) + # if self.stimulus == 'SAM': + # f0 = self.fMod + # print ("SAM Tone: f0=%.3f at %3.1f dbSPL, fMod=%3.1f dMod=%5.2f, cell CF=%.3f" % + # (self.f0, self.dbspl, self.fMod, self.dMod, self.cf)) + # if self.stimulus == 'clicks': + # f0 = 1./self.click_rate + # print "Clicks: interval %.3f at %3.1f dbSPL, cell CF=%.3f " % (self.click_rate, self.dbspl, self.cf) + # vs = PU.vector_strength(spikesinwin, f0) + # + # print 'AN Vector Strength: %7.3f, d=%.2f (us) Rayleigh: %7.3f p = %.3e n = %d' % (vs['r'], vs['d']*1e6, vs['R'], vs['p'], vs['n']) + # (hist, binedges) = np.histogram(vs['ph']) + # curve = p6.plot(binedges, hist, stepMode=True, fillBrush=(100, 100, 255, 150), fillLevel=0) + # p6.setXRange(0., 2*np.pi) + # + # p7 = self.win.addPlot(title='%s phase' % self.cell, row=2, col=1) + # spkin = bspk[np.where(bspk > phasewin[0]*1e3)] + # spikesinwin = spkin[np.where(spkin <= phasewin[1]*1e3)] + # vs = PU.vector_strength(spikesinwin, f0) + # print '%s Vector Strength: %7.3f, d=%.2f (us) Rayleigh: %7.3f p = %.3e n = %d' % (self.cell, vs['r'], vs['d']*1e6, vs['R'], vs['p'], vs['n']) + # (hist, binedges) = np.histogram(vs['ph']) + # curve = p7.plot(binedges, hist, stepMode=True, fillBrush=(100, 100, 255, 150), fillLevel=0) + # p7.setXRange(0., 2*np.pi) + # p7.setXLink(p6) + + self.win.show() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Compute AN only PSTH in postsynaptic cell" + ) + parser.add_argument( + type=str, + dest="cell", + default="bushy", + choices=[ + "bushy", + "tstellate", + "dstellate", + "octopus", + "tuberculoventral", + "pyramida", + ], + help="Select target cell", + ) + parser.add_argument( + "-s", + "--stimulus", + type=str, + dest="stimulus", + default="tone", + choices=["tone", "SAM", "clicks"], + help="Select stimulus from ['tone', 'SAM', 'clicks']", + ) + parser.add_argument( + "-t", + "--type", + type=str, + dest="syntype", + default="simple", + choices=["simple", "multisite"], + help="Set synapse type (simple, multisite)", + ) + parser.add_argument( + "-n", + "--nrep", + type=int, + dest="nrep", + default=10, + help="Set number of repetitions", + ) + + args = parser.parse_args() + + cell = args.cell + stimulus = args.stimulus + nrep = args.nrep + synapseType = args.syntype + + print("cell type: ", cell) + prot = SGCInputTestPSTH() + prot.set_cell(cell) + prot.run(stimulus=stimulus, reps=nrep) + prot.show() + + import sys + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_sgc_input_phaselocking.py b/examples/test_sgc_input_phaselocking.py new file mode 100644 index 0000000..2e81dbf --- /dev/null +++ b/examples/test_sgc_input_phaselocking.py @@ -0,0 +1,248 @@ +""" +test_sgc_input_phaselocking.py + +Test phase locking from an input sgc to a target cell type. Runs simulations +with AN input, and plots the results, including PSTH and phase histogram. + +Usage: python test_sgc_input_phaselocking.py celltype stimulus species + where: + celltype is one of [bushy, tstellate, octopus, dstellate] (default: bushy) + stimulus is one of [tone, SAM, clicks] (default: tone) + species is one of [guineapig, mouse] (default: guineapig) + +""" + +import sys +import numpy as np +import pyqtgraph as pg +from neuron import h +from cnmodel.protocols import Protocol +from cnmodel import cells +from cnmodel.util import sound +import cnmodel.util.pynrnutilities as PU +from cnmodel import data + + +class SGCInputTestPL(Protocol): + def set_cell(self, cell="bushy"): + self.cell = cell + + def run( + self, temp=34.0, dt=0.025, seed=575982035, stimulus="tone", species="mouse" + ): + assert stimulus in ["tone", "SAM", "clicks"] # cases available + if self.cell == "bushy": + postCell = cells.Bushy.create(species=species) + elif self.cell == "tstellate": + postCell = cells.TStellate.create(species=species) + elif self.cell == "octopus": + postCell = cells.Octopus.create(species=species) + elif self.cell == "dstellate": + postCell = cells.DStellate.create(species=species) + else: + raise ValueError( + "cell %s is not yet implemented for phaselocking" % self.cell + ) + self.post_cell = postCell + self.stimulus = stimulus + self.run_duration = 1.0 # in seconds + self.pip_duration = 0.8 # in seconds + self.pip_start = [0.02] # in seconds + self.Fs = 100e3 # in Hz + self.f0 = 4000.0 # stimulus in Hz + self.cf = 4000.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 50.0 # % mod depth, Hz + self.dbspl = 60.0 + if self.stimulus == "SAM": + self.stim = sound.SAMTone( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + fmod=self.fMod, + dmod=self.dMod, + dbspl=self.dbspl, + ramp_duration=2.5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + if self.stimulus == "tone": + self.f0 = 1000.0 + self.cf = 1000.0 + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=2.5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + + if self.stimulus == "clicks": + self.click_rate = 0.020 # msec + self.stim = sound.ClickTrain( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + click_start=0.010, + click_duration=100.0e-6, + click_interval=self.click_rate, + nclicks=int((self.run_duration - 0.01) / self.click_rate), + ramp_duration=2.5e-3, + ) + + n_sgc = data.get( + "convergence", species=species, post_type=postCell.type, pre_type="sgc" + )[0] + self.n_sgc = int(np.round(n_sgc)) + + self.pre_cells = [] + self.synapses = [] + j = 0 + for k in range(self.n_sgc): + seed = seed + k + preCell = cells.DummySGC(cf=self.cf, sr=2) + synapse = preCell.connect(postCell) + for i in range(synapse.terminal.n_rzones): + self["xmtr%03d" % j] = synapse.terminal.relsite._ref_XMTR[i] + j = j + 1 + synapse.terminal.relsite.Dep_Flag = False + preCell.set_sound_stim(self.stim, seed=seed) + self.pre_cells.append(preCell) + self.synapses.append(synapse) + + self["vm"] = postCell.soma(0.5)._ref_v + # self['prevm'] = preCell.soma(0.5)._ref_v + self["t"] = h._ref_t + postCell.cell_initialize() + h.tstop = 1e3 * self.run_duration # duration of a run + h.celsius = temp + h.dt = dt + + self.custom_init() + h.run() + + def show(self): + self.win = pg.GraphicsWindow() + p1 = self.win.addPlot(title="stim", row=0, col=0) + p1.plot(self.stim.time * 1000, self.stim.sound) + p1.setXLink(p1) + + p2 = self.win.addPlot(title="AN spikes", row=1, col=0) + vt = pg.VTickGroup(self.pre_cells[0]._spiketrain) + p2.addItem(vt) + p2.setXLink(p1) + + p3 = self.win.addPlot(title="%s Spikes" % self.cell, row=2, col=0) + bspk = PU.findspikes(self["t"], self["vm"], -30.0) + bspktick = pg.VTickGroup(bspk) + p3.addItem(bspktick) + p3.setXLink(p1) + + p4 = self.win.addPlot(title="%s Vm" % self.cell, row=3, col=0) + p4.plot(self["t"], self["vm"]) + p4.setXLink(p1) + + p5 = self.win.addPlot(title="xmtr", row=0, col=1) + j = 0 + for k in range(self.n_sgc): + synapse = self.synapses[k] + for i in range(synapse.terminal.n_rzones): + p5.plot(self["t"], self["xmtr%03d" % j], pen=(i, 15)) + j = j + 1 + p5.setXLink(p1) + + p6 = self.win.addPlot(title="AN phase", row=1, col=1) + phasewin = [ + self.pip_start[0] + 0.25 * self.pip_duration, + self.pip_start[0] + self.pip_duration, + ] + prespk = self.pre_cells[0]._spiketrain # just sample one... + spkin = prespk[np.where(prespk > phasewin[0] * 1e3)] + spikesinwin = spkin[np.where(spkin <= phasewin[1] * 1e3)] + print("\nCell type: %s" % self.cell) + print("Stimulus: ") + + # set freq for VS calculation + if self.stimulus == "tone": + f0 = self.f0 + print( + "Tone: f0=%.3f at %3.1f dbSPL, cell CF=%.3f" + % (self.f0, self.dbspl, self.cf) + ) + if self.stimulus == "SAM": + f0 = self.fMod + print( + ( + "SAM Tone: f0=%.3f at %3.1f dbSPL, fMod=%3.1f dMod=%5.2f, cell CF=%.3f" + % (self.f0, self.dbspl, self.fMod, self.dMod, self.cf) + ) + ) + if self.stimulus == "clicks": + f0 = 1.0 / self.click_rate + print( + "Clicks: interval %.3f at %3.1f dbSPL, cell CF=%.3f " + % (self.click_rate, self.dbspl, self.cf) + ) + vs = PU.vector_strength(spikesinwin, f0) + + print( + "AN Vector Strength at %.1f: %7.3f, d=%.2f (us) Rayleigh: %7.3f p = %.3e n = %d" + % (f0, vs["r"], vs["d"] * 1e6, vs["R"], vs["p"], vs["n"]) + ) + (hist, binedges) = np.histogram(vs["ph"]) + p6.plot( + binedges, hist, stepMode=True, fillBrush=(100, 100, 255, 150), fillLevel=0 + ) + p6.setXRange(0.0, 2 * np.pi) + + p7 = self.win.addPlot(title="%s phase" % self.cell, row=2, col=1) + spkin = bspk[np.where(bspk > phasewin[0] * 1e3)] + spikesinwin = spkin[np.where(spkin <= phasewin[1] * 1e3)] + vs = PU.vector_strength(spikesinwin, f0) + print( + "%s Vector Strength: %7.3f, d=%.2f (us) Rayleigh: %7.3f p = %.3e n = %d" + % (self.cell, vs["r"], vs["d"] * 1e6, vs["R"], vs["p"], vs["n"]) + ) + (hist, binedges) = np.histogram(vs["ph"]) + p7.plot( + binedges, hist, stepMode=True, fillBrush=(100, 100, 255, 150), fillLevel=0 + ) + p7.setXRange(0.0, 2 * np.pi) + p7.setXLink(p6) + + self.win.show() + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] in ["help", "-h", "--help"]: + print( + "\nUsage: python test_sgc_input_phaselocking.py\n celltype [bushy, tstellate, octopus, dstellate] (default: bushy)" + ) + print(" stimulus [tone, SAM, clicks] (default: tone)") + print(" species [guineapig mouse] (default: guineapig)\n") + exit(0) + if len(sys.argv) > 1: + cell = sys.argv[1] + else: + cell = "bushy" + if len(sys.argv) > 2: + stimulus = sys.argv[2] + else: + stimulus = "tone" + if len(sys.argv) > 3: + species = sys.argv[3] + else: + species = "guineapig" + print("cell type: ", cell) + prot = SGCInputTestPL() + prot.set_cell(cell) + prot.run(stimulus=stimulus, species=species) + prot.show() + + import sys + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_simple_synapses.py b/examples/test_simple_synapses.py new file mode 100644 index 0000000..e69d85a --- /dev/null +++ b/examples/test_simple_synapses.py @@ -0,0 +1,97 @@ +""" +test_simple_synapses + +Test synapses between cell types. + +Usage: + python test_synapses.py + Supported cell types: sgc, bushy, tstellate, dstellate, tuberculoventral, pyramidal +""" +import sys +import pyqtgraph as pg +from cnmodel.protocols.simple_synapse_test import SimpleSynapseTest +from cnmodel import cells +from cnmodel.synapses import Synapse + + +def runtest(): + if len(sys.argv) < 3: + print("Usage: python test_synapses.py ") + print( + " Supported cell types: sgc, bushy, tstellate, dstellate, tuberculoventral, pyramidal" + ) + sys.exit(1) + + convergence = { + "sgc": { + "bushy": 1, + "tstellate": 6, + "dstellate": 10, + "dstellate_eager": 10, + "tuberculoventral": 6, + "pyramidal": 5, + }, + "dstellate": { + "bushy": 10, + "tstellate": 15, + "dstellate": 5, + "tuberculoventral": 10, + "pyramidal": 10, + }, + "tuberculoventral": { + "bushy": 6, + "tstellate": 3, + "dstellate": 0, + "tuberculoventral": 2, + "pyramidal": 10, + }, + } + + c = [] + for cellType in sys.argv[1:3]: + if cellType == "sgc": + cell = cells.SGC.create() + elif cellType == "tstellate": + cell = cells.TStellate.create(debug=True, ttx=False) + elif ( + cellType == "dstellate" + ): # Type I-II Rothman model, similiar excitability (Xie/Manis, unpublished) + cell = cells.DStellate.create(model="RM03", debug=True, ttx=False) + elif cellType == "dstellate_eager": # From Eager et al. + cell = cells.DStellate.create(model="Eager", debug=True, ttx=False) + elif cellType == "bushy": + cell = cells.Bushy.create(debug=True, ttx=True) + elif cellType == "tuberculoventral": + cell = cells.Tuberculoventral.create(debug=True, ttx=False) + elif cellType == "pyramidal": + cell = cells.Pyramidal.create(debug=True, ttx=False) + else: + raise ValueError("Unknown cell type '%s'" % cellType) + c.append(cell) + + preCell, postCell = c + + nTerminals = convergence.get(sys.argv[1], {}).get(sys.argv[2], None) + if nTerminals is None: + nTerminals = 1 + print( + "Warning: Unknown convergence for %s => %s, assuming %d" + % (sys.argv[1], sys.argv[2], nTerminals) + ) + + if sys.argv[1:3] == ["sgc", "bushy"]: + niter = 1 + else: + niter = 20 + + st = SimpleSynapseTest() + st.run(preCell.soma, postCell.soma, iterations=niter) + st.show() + return st # keep in memory + + +if __name__ == "__main__": + r = runtest() + print("runtest done") + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_sound_stim.py b/examples/test_sound_stim.py new file mode 100644 index 0000000..6cbca0b --- /dev/null +++ b/examples/test_sound_stim.py @@ -0,0 +1,116 @@ +""" +Test using sound stimulation to generate SGC spike trains. + +This script uses an_model.get_spiketrain(), which internally calls MATLAB to +generate spike trains and caches the output. A higher-level approach is to use +DummySGC, which will automatically feed the spike train into a synapse for +input to downstream neurons (see test_sgc_input.py). Lower-level access to the +auditory nerve model is demonstrated in test_an_model.py. + +This script also can measure the time required to generate the spike trains, or +to retrieve them from the cache. In addition, it can access a second interface +to the Zilany et al. mode that is in pure python ("cochlea") for comparison. +In general the speed to compute the spike trains for the same sets of stimuli +is faster with the pure Python interface than with the Matlab interface, and +fastest for retrieval of pre-computed trains. Note that changing the value +of "seed" will force a recomputation of the spike trains. +""" +import numpy as np +import pyqtgraph as pg +from cnmodel import an_model +from cnmodel.util import sound +import cochlea + +import time + +seed = 34986 + + +def time_usage(func): + def wrapper(*args, **kwargs): + beg_ts = time.time() + res = func(*args, **kwargs) + end_ts = time.time() + print(("** Elapsed time: %f" % (end_ts - beg_ts))) + return res + + return wrapper + + +def set_dbspl(signal, dbspl): + """Scale the level of `signal` to the given dB_SPL.""" + p0 = 20e-6 + rms = np.sqrt(np.sum(signal ** 2) / signal.size) + scaled = signal * 10 ** (dbspl / 20.0) * p0 / rms + return scaled + + +@time_usage +def sound_stim(seed, useMatlab=True): + cf = 1.5e3 + levels = list(range(-10, 101, 10)) + + result = {} + if useMatlab: + simulator = "matlab" + else: + simulator = "cochlea" + for sr in 1, 2, 3: + spikes = [] + for level in levels: + stim = sound.TonePip( + rate=100e3, + duration=0.5, + f0=cf, + dbspl=level, + pip_duration=0.5, + pip_start=[0], + ramp_duration=2.5e-3, + ) + if simulator == "cochlea": + stim._sound = set_dbspl(stim.sound, level) # adjust scaling here + spikes.append( + an_model.get_spiketrain( + cf=cf, sr=sr, seed=seed, stim=stim, simulator=simulator + ) + ) + seed += 1 + result[sr] = {"levels": levels, "spikes": spikes} + return result + + +import sys + + +def runtest(): + usematlab = True + if len(sys.argv) > 0: + if len(sys.argv) == 1: + print( + 'Call requires argument, must be either "matlab" or "cochlea"; default is "matlab"' + ) + exit() + flag = sys.argv[1] + if flag not in ["matlab", "cochlea"]: + print('Flag must be either "matlab" or "cochlea"; default is "matlab"') + exit() + if flag == "cochlea": + usematlab = False + if usematlab: + print("Running with matlab simulator") + else: + print("Running with MR cochlea simulator") + + result = sound_stim(seed, useMatlab=usematlab) + + win = pg.GraphicsWindow() + p1 = win.addPlot(title="Rate-level function") + for i, x in enumerate(result.keys()): + p1.plot(result[x]["levels"], [s.size for s in result[x]["spikes"]], pen=(x, 6)) + return win + + +if __name__ == "__main__": + win = runtest() + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/test_sounds.py b/examples/test_sounds.py new file mode 100644 index 0000000..3d18aea --- /dev/null +++ b/examples/test_sounds.py @@ -0,0 +1,70 @@ +""" +Test sounds and plot waveforms. + +This script tests the sound waveform generator for a variety of sounds + +""" +import sys +import numpy as np +import pyqtgraph as pg +from cnmodel.util import sound +from collections import OrderedDict +from scipy import signal + +class test_sounds(): + def __init__(self): + cf = 2e3 + Fs = 100e3 # sample frequency + level = 80. + seed = 34978 + fmod = 10. + dmod = 100. + win = pg.GraphicsWindow() + pipwin = win.addPlot(title='sound pip', row=0, col=0) + pipmodwin = win.addPlot(title='100 % SAM modulated pip', row=1, col=0) + noisewin = win.addPlot(title='WB noise', row=2, col=0) + noisemodwin = win.addPlot(title='100 % SAM Modulated WB Noise', row=3, col=0) + clickwin = win.addPlot(title='clicks', row=4, col=0) + wavewin = win.addPlot(title='wavefile', row=5, col=0) + + pipwins = win.addPlot(title='sound pip Spec', row=0, col=1) + pipmodwins = win.addPlot(title='100 % SAM modulated pip', row=1, col=1) + noisewins = win.addPlot(title='WB noise', row=2, col=1) + noisemodwins = win.addPlot(title='100 % SAM Modulated WB Noise', row=3, col=1) + clickwins = win.addPlot(title='click spec', row=4, col=1) + wavewins = win.addPlot(title='Wavefile spec', row=5, col=1) + + stims = OrderedDict([('pip', (pipwin, sound.TonePip)), ('pipmod', (pipmodwin, sound.SAMTone)), + ('noise', (noisewin, sound.NoisePip)), ('noisemod', (noisemodwin, sound.SAMNoise)), + ('clicks', (clickwin, sound.ClickTrain)), + ('wavefile', (wavewin, sound.ReadWavefile))]) + + specs = OrderedDict([('pip', (pipwins, sound.TonePip)), ('pipmod', (pipmodwins, sound.SAMTone)), + ('noise', (noisewins, sound.NoisePip)), ('noisemod', (noisemodwins, sound.SAMNoise)), + ('clicks', (clickwins, sound.ClickTrain)), + ('wavefile', (wavewins, sound.ReadWavefile))]) + + + for stim in stims: + print(stim) + if stim in ['clicks']: + wave = stims[stim][1](rate=Fs, duration=1.0, dbspl=level, + click_duration=1e-4, click_starts=1e-3*np.linspace(10, 500, 50)) + # wave = stims[stim][1](rate=Fs, dbspl=level, click_interval=10., nclicks=10, + # click_duration=1e-4, click_start=10.) + elif stim in ['wavefile']: + wave = stims[stim][1](wavefile='cnmodel/examples/stim172_geese.wav', rate=Fs, dbspl=level) + wave.sound # force generation here + print('time shape test: ', wave.time.shape) + else: + wave = stims[stim][1](rate=Fs, duration=1.0, f0=cf, dbspl=level, + pip_duration=0.8, pip_start=[10e-3], ramp_duration=2.5e-3, + fmod=fmod, dmod=dmod, seed=seed) + + stims[stim][0].plot(wave.time, wave.sound) + f, Pxx_spec = signal.periodogram(wave.sound, Fs) # window='flattop', nperseg=8192, noverlap=512, scaling='spectrum') + specs[stim][0].plot(f, np.sqrt(Pxx_spec)) + + +if __name__ == '__main__': + test_sounds() diff --git a/examples/test_synapses.py b/examples/test_synapses.py new file mode 100644 index 0000000..fe4f0c2 --- /dev/null +++ b/examples/test_synapses.py @@ -0,0 +1,210 @@ +""" +Test synaptic connections between two different cell types. + +usage: test_synapses.py [-h] [-t {simple,multisite}] [-c] + {sgc,tstellate,dstellate,tuberculoventral} + {bushy,tstellate,dstellate,octopus,tuberculoventral,pyramidal} + +Compute AN only PSTH in postsynaptic cell + +positional arguments: + {sgc,tstellate,dstellate,tuberculoventral} + Select presynaptic cell type + {bushy,tstellate,dstellate,octopus,tuberculoventral,pyramidal} + Select postsynaptic cell type + +optional arguments: + -h, --help show this help message and exit + -t {simple,multisite}, --type {simple,multisite} + Set synapse type (simple, multisite) + -c, --convergence Use convergence = 1 for comparision between simple and + multi, instead of default table + + +This script: + +1. Creates single pre- and postsynaptic cells +2. Creates multiple synaptic terminals between the two cells. + (the convergence is hard-coded below). +3. Stimulates the presynaptic cell by current injection. +4. Records and analyzes the resulting post-synaptic events. + +This is used mainly to check that the strength, kinetics, and dynamics of +each synapse type is working as expected. A higher-level approach is +demonstrated in test_populations.py, in which the presynaptic cells are +automatically generated using expected patterns of connectivity. +""" + +import argparse +import pyqtgraph as pg +from cnmodel.protocols import SynapseTest +from cnmodel import cells +from cnmodel.synapses import Synapse + + +import sys + + +def runtest(): + parser = argparse.ArgumentParser( + description="Compute AN only PSTH in postsynaptic cell" + ) + parser.add_argument( + type=str, + dest="precell", + default="sgc", + choices=["sgc", "tstellate", "dstellate", "tuberculoventral"], + help="Select presynaptic cell type", + ) + parser.add_argument( + type=str, + dest="postcell", + default="bushy", + choices=[ + "bushy", + "tstellate", + "dstellate", + "octopus", + "tuberculoventral", + "pyramidal", + ], + help="Select postsynaptic cell type", + ) + parser.add_argument( + "-t", + "--type", + type=str, + dest="syntype", + default="multisite", + choices=["simple", "multisite"], + help="Set synapse type (simple, multisite)", + ) + parser.add_argument( + "-c", + "--convergence", + action="store_true", + dest="convergence", + help="Use convergence = 1 for comparision between simple and multi, instead of default table", + ) + + args = parser.parse_args() + + precell = args.precell + postcell = args.postcell + synapseType = args.syntype + use_conv_table = args.convergence + + # These must be se3t to 1 to match data in original tables. Otherwise, it would be better + # to use the original tables. + + if not use_conv_table: + convergence = { + "sgc": { + "bushy": 1, + "tstellate": 1, + "dstellate": 1, + "dstellate_eager": 10, + "octopus": 10, + "tuberculoventral": 1, + "pyramidal": 1, + "cartwheel": 0, + }, + "dstellate": {"bushy": 10, "tstellate": 15, "dstellate": 5}, + } + else: + convergence = { + "sgc": { + "bushy": 1, + "tstellate": 1, + "dstellate": 1, + "dstellate_eager": 1, + "octopus": 1, + "tuberculoventral": 1, + "pyramidal": 1, + "cartwheel": 0, + }, + "dstellate": { + "bushy": 1, + "tstellate": 1, + "dstellate": 0, + "tuberculoventral": 1, + "pyramidal": 1, + "cartwheel": 0, + }, + "tuberculoventral": { + "bushy": 1, + "tstellate": 1, + "dstellate": 0, + "tuberculoventral": 1, + "pyramidal": 1, + "cartwheel": 0, + }, + } + + c = [] + for cellType in [precell, postcell]: + if cellType == "sgc": + cell = cells.SGC.create() + elif cellType == "tstellate": + cell = cells.TStellate.create(debug=True, ttx=False) + elif ( + cellType == "dstellate" + ): # Type I-II Rothman model, similiar excitability (Xie/Manis, unpublished) + cell = cells.DStellate.create(model="RM03", debug=True, ttx=False) + elif cellType == "dstellate_eager": # From Eager et al. + cell = cells.DStellate.create(model="Eager", debug=True, ttx=False) + elif cellType == "bushy": + cell = cells.Bushy.create(debug=True, ttx=True) + elif cellType == "tuberculoventral": + cell = cells.Tuberculoventral.create(debug=True, ttx=True) + elif cellType == "pyramidal": + cell = cells.Pyramidal.create(debug=True, ttx=True) + elif cellType == "octopus": + cell = cells.Octopus.create(debug=True, ttx=True) + else: + raise ValueError("Unknown cell type '%s'" % cellType) + c.append(cell) + + preCell, postCell = c + + if not use_conv_table: + nTerminals = convergence.get(precell, {}).get(postcell, None) + else: + nTerminals = 1 + # print("Warning: Unknown convergence for %s => %s, assuming %d" % (precell, postcell, nTerminals)) + + if [precell, postcell] == ["sgc", "bushy"]: + niter = 5 + else: + niter = 20 + # syntype = 'multisite' + # if len(sys.argv) > 3: + # syntype = sys.argv[3] + # assert(syntype in ['simple', 'multisite']) + if synapseType == "simple": + niter = 1 + + st = SynapseTest() + st.run( + preCell.soma, + postCell.soma, + nTerminals, + vclamp=-65.0, + iterations=niter, + synapsetype=synapseType, + ) + st.show_result() + st.plots["VPre"].setYRange(-70.0, 10.0) + st.plots["EPSC"].setYRange(-2.0, 0.5) + st.plots["latency2080"].setYRange(0.0, 1.0) + st.plots["halfwidth"].setYRange(0.0, 1.0) + st.plots["RT"].setYRange(0.0, 0.2) + st.plots["latency"].setYRange(0.0, 1.0) + st.plots["latency_distribution"].setYRange(0.0, 1.0) + return st # need to keep st alive in memory + + +if __name__ == "__main__": + st = runtest() + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/examples/toy_model.py b/examples/toy_model.py new file mode 100755 index 0000000..ffe725d --- /dev/null +++ b/examples/toy_model.py @@ -0,0 +1,312 @@ +#!/usr/bin/python +""" +Basic test of initialization of multiple cells in the model, and running multiple cells at one time. +Plots the resposnes to a series of current injections for most implemented baseic cell types in +in cnmodel. + +Usage: + python examples/toy_model.py (no arguments) + +""" + +from __future__ import print_function + + +import sys +from neuron import h +import numpy as np +import cnmodel.cells as cells +from cnmodel.protocols import Protocol +from cnmodel.util import custom_init +from collections import OrderedDict +import re +import pyqtgraph.exporters +from cnmodel.util import pyqtgraphPlotHelpers as PH +from cnmodel.protocols import IVCurve + + +try: # check for pyqtgraph install + import pyqtgraph as pg +except ImportError: + raise ImportError("This model requires pyqtgraph") + +from cnmodel.util.stim import make_pulse + + +def autorowcol(n): + """ + return a reasonable layout (cols, rows) for n plots on a page + up to 16. + Otherwise return floor(sqrt(n)) + 1 for both r and c. + """ + nmap = { + 1: (1, 1), + 2: (2, 1), + 3: (3, 1), + 4: (2, 2), + 5: (3, 2), + 6: (3, 2), + 7: (3, 3), + 8: (3, 3), + 9: (3, 3), + 10: (3, 4), + 11: (3, 4), + 12: (3, 4), + 13: (4, 4), + 14: (4, 4), + 15: (4, 4), + 16: (4, 4), + } + if n <= 16: + return nmap[n][0], nmap[n][1] + else: + nx = np.floor(np.sqrt(n)) + 1 + return nx, nx + + +def makeLayout(cols=1, rows=1, letters=True, margins=4, spacing=4, nmax=None): + """ + Create a multipanel plot, returning the various pyptgraph elements. + The layout is always a rectangular grid with shape (cols, rows) + if letters is true, then the plot is labeled "A, B, C..." + margins sets the margins around the outside of the plot + spacing sets the spacing between the elements of the grid + """ + import string + + letters = string.ascii_uppercase + widget = pg.QtGui.QWidget() + gridLayout = pg.QtGui.QGridLayout() + widget.setLayout(gridLayout) + gridLayout.setContentsMargins(margins, margins, margins, margins) + gridLayout.setSpacing(spacing) + plots = [[0 for x in range(cols)] for x in range(rows)] + i = 0 + for c in range(cols): + for r in range(rows): + plots[r][c] = pg.PlotWidget() + gridLayout.addWidget(plots[r][c], r, c) + # labelUp(plots[r][c], 'T(s)', 'Y', title = letters[i]) + i += 1 + if i > 25: + i = 0 + if nmax is not None and i >= nmax: + break # that's all - leave out empty plots + + return (plots, widget, gridLayout) + + +def getnextrowcol(plx, row, col, cols): + col += 1 + if col >= cols: + col = 0 + row += 1 + return (plx[row][col], row, col) + + +class Toy(Protocol): + """ + Calls to encapsulate the model runs + Run a set of cells with defined parameters to show excitability patterns. + Note that cells from Rothman and Manis are run at 22C; others at various + temperatures depending on how they were initially measured and defined. + + """ + + def __init__(self): + super(Toy, self).__init__() + + def current_name(self, name, n): + """ + From the name of the current model, get the current injection information + + Parameters + --------- + name : str (no default) + name of the cell type + n : int (no default) + """ + if len(self.celltypes[name][3]) > 2: + injs = self.celltypes[name][3] + injarr = np.linspace(injs[0], injs[1], injs[2], endpoint=True) + return "%.3f" % injarr[n] + else: + return "%.3f" % self.celltypes[name][3][n] + + def getname(self, cell, ninj): + name = self.make_name(cell) + iname = self.current_name(name, ninj) + nname = name + " " + iname + return name, nname + + def make_name(self, cell): + return cell + ", " + self.celltypes[cell][1] + ":" + + def run(self): + sre = re.compile( + "(?P\w+)(?:[, ]*)(?P[\w-]*)(?:[, ]*)(?P[\w-]*)" + ) # regex for keys in cell types + self.celltypes = OrderedDict( + [ + ("Bushy, II", (cells.Bushy, "II", "guineapig", (-0.5, 0.5, 11), 22)), + ( + "Bushy, II-I", + (cells.Bushy, "II-I", "guineapig", (-0.5, 0.5, 11), 22), + ), + ( + "DStellate, I-II", + (cells.DStellate, "I-II", "guineapig", (-0.3, 0.3, 9), 22), + ), + ( + "TStellate, I-c", + (cells.TStellate, "I-c", "guineapig", (-0.15, 0.15, 9), 22), + ), + ( + "TStellate, I-t", + (cells.TStellate, "I-t", "guineapig", (-0.15, 0.15, 9), 22), + ), + ( + "Octopus, II-o", + (cells.Octopus, "II-o", "guineapig", (-2.5, 2.5, 11), 22), + ), + ("Bushy, II, Mouse", (cells.Bushy, "II", "mouse", (-1, 1.2, 13), 34)), + ( + "TStellate, I-c, Mouse", + (cells.TStellate, "I-c", "mouse", (-1, 1, 9), 34), + ), + ( + "DStellate, I-II, Mouse", + (cells.DStellate, "I-II", "mouse", (-0.5, 0.5, 9), 34), + ), + ( + "Pyramidal, I, Rat", + (cells.Pyramidal, "I", "rat", (-0.3, 0.4, 11), 34), + ), + ( + "Cartwheel, I, Mouse", + (cells.Cartwheel, "I", "mouse", (-0.5, 0.5, 9), 34), + ), + ( + "Tuberculoventral, I, Mouse", + (cells.Tuberculoventral, "I", "mouse", (-0.35, 1, 11), 34), + ), + ("SGC, bm, Mouse", (cells.SGC, "bm", "mouse", (-0.2, 0.6, 9), 34)), + ("SGC, a, Mouse", (cells.SGC, "a", "mouse", (-0.2, 0.6, 9), 34)), + ] + ) + + dt = 0.025 + h.dt = dt + h.celsius = 22 + + stim = {"NP": 1, "delay": 10, "dur": 100, "amp": 0.0, "dt": h.dt} + tend = stim["delay"] + stim["dur"] + 20.0 + + netcells = {} + for c in list(self.celltypes.keys()): + g = sre.match(c) + cellname = g.group("cell") + modelType = g.group("type") + species = self.celltypes[c][2] + if g.group("type") == "": + netcells[c] = self.celltypes[c][0].create() + else: + netcells[c] = self.celltypes[c][0].create( + modelType=modelType, species=species, debug=False + ) + # dicts to hold data + pl = OrderedDict([]) + pl2 = OrderedDict([]) + rvec = OrderedDict([]) + vec = OrderedDict([]) + istim = OrderedDict([]) + ncells = len(list(self.celltypes.keys())) + # + # build plotting area + # + app = pg.mkQApp() + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + self.win.resize(800, 600) + cols, rows = autorowcol(ncells) + + row = 0 + col = 0 + labelStyle = {"color": "#000", "font-size": "9pt", "weight": "normal"} + tickStyle = pg.QtGui.QFont("Arial", 9, pg.QtGui.QFont.Light) + self.iv = IVCurve() # use standard IVCurve here... + for n, name in enumerate(self.celltypes.keys()): + nrn_cell = netcells[ + name + ] # get the Neuron object we are using for this cell class + injcmds = list(self.celltypes[name][3]) # list of injections + injcmds[2] = (injcmds[1] - injcmds[0]) / ( + float(injcmds[2] - 1) + ) # convert to pulse format for IVCurve + temperature = self.celltypes[name][4] + nrn_cell.set_temperature(float(temperature)) + ninjs = len(injcmds) + print("cell: ", name) + # print( 'injs: ', injcmds) + pl[name] = self.win.addPlot( + labels={"left": "V (mV)", "bottom": "Time (ms)"} + ) + PH.nice_plot(pl[name]) + pl[name].setTitle(title=name, font=pg.QtGui.QFont("Arial", 10)) + col += 1 + if col >= cols: + col = 0 + self.win.nextRow() + row += 1 + self.iv.reset() + self.iv.run( + {"pulse": [injcmds]}, + nrn_cell, + durs=(stim["delay"], stim["dur"], 20.0), + sites=None, + reppulse=None, + temp=float(temperature), + ) + for k in range(len(self.iv.voltage_traces)): + pl[name].plot( + self.iv.time_values, + self.iv.voltage_traces[k], + pen=pg.mkPen("k", width=0.75), + ) + pl[name].setRange(xRange=(0.0, 130.0), yRange=(-160.0, 40.0)) + PH.noaxes(pl[name]) + PH.calbar( + pl[list(self.celltypes.keys())[0]], + calbar=[0, -120.0, 10.0, 20.0], + unitNames={"x": "ms", "y": "mV"}, + ) + + text = u"{0:2d}\u00b0C {1:.2f}-{2:.2f} nA".format( + int(temperature), + np.min(self.iv.current_cmd), + np.max(self.iv.current_cmd), + ) + ti = pg.TextItem(text, anchor=(1, 0)) + ti.setFont(pg.QtGui.QFont("Arial", 9)) + ti.setPos(120.0, -120.0) + pl[name].addItem(ti) + # get overall Rin, etc; need to initialize all cells + nrn_cell.cell_initialize() + for n, name in enumerate(self.celltypes.keys()): + nrn_cell = netcells[name] + nrn_cell.vm0 = nrn_cell.soma.v + pars = nrn_cell.compute_rmrintau(auto_initialize=False) + print( + "{0:>14s} [{1:>24s}] *** Rin = {2:6.1f} M\ohm Tau = {3:6.1f} ms Vm = {4:6.1f} mV".format( + nrn_cell.status["name"], name, pars["Rin"], pars["tau"], pars["v"] + ) + ) + + +if __name__ == "__main__": + t = Toy() + t.run() + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() + # exporter = pg.exporters.ImageExporter(t.win.scene()) + # exporter.export('~/Desktop/Model_Figure2.svg') diff --git a/modified_dend/hoc_trial_run_dendrites.py b/modified_dend/hoc_trial_run_dendrites.py new file mode 100644 index 0000000..5345ea1 --- /dev/null +++ b/modified_dend/hoc_trial_run_dendrites.py @@ -0,0 +1,178 @@ +from multiprocessing import Pool +from random import randint +from cnmodel import cells +from neuron import h, gui +from tqdm import tqdm +# from pprint import pprint + + +def run(run_input, processes): + results = [] + if processes == 1: + for input in run_input: + results.append(_run_trial(input)) + else: + p = Pool(processes) + for res in tqdm(p.imap_unordered(_run_trial, run_input)): + results.append(res) + return results + + +def add_pyramidal_cell(): + pyramidal = cells.Pyramidal.create(species="rat") + + return pyramidal + +def add_pyram_dend(pyramidal): + pyramidal.add_dendrites() + apical_dend = pyramidal.maindend[0] + basal_dend = pyramidal.maindend[1] + apical_dend.L = 1 + basal_dend.L = 179 + return apical_dend,basal_dend + + +def add_tuberculoventral_cell(): + tuberculoventral_1 = cells.Tuberculoventral.create() + tuberculoventral_2 = cells.Tuberculoventral.create() + return tuberculoventral_1, tuberculoventral_2 + + +def add_dstellate_cell(): + dstellate = cells.DStellateEager.create() + return dstellate + +def add_dstel_dend(dstellate): + dstellate.add_dendrites() + d_apic_dend = dstellate.maindend[0] + d_basal_dend = dstellate.maindend[1] + + return d_apic_dend,d_basal_dend + +def add_dstel_axon(dstellate): + dstellate.add_axon() + d_axon = dstellate.axon + # d_axon.L = 2 + + return d_axon + + +def _run_trial(run_input): + seed, info, run_number = run_input + """ + info is a dict + """ + pyramidal = add_pyramidal_cell() + tuberculoventral_1, tuberculoventral_2 = add_tuberculoventral_cell() + dstellate = add_dstellate_cell() + + d_apic_dend,d_basal_dend = add_dstel_dend(dstellate) + pyram_apical,pyram_basal = add_pyram_dend(pyramidal) + + d_axon = add_dstel_axon(dstellate) + + + auditory_nerve_cells = [] + synapses = [] + inhib_synapses = [] + # auditory nerve attachments + # attach to pyramidal cell + for nsgc in range(48): + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(pyramidal, post_opts={"postsite":[pyram_basal, 1]}, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + attach to tuberculoventral 1 + for nsgc in range(18): + # attach to tb cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(tuberculoventral_1, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + # attach to tuberculoventral 2 + for nsgc in range(18): + # attach to tb cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(tuberculoventral_2, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + for nsgc in range(24): + # attach to dstellate cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(dstellate, post_opts={"postsite":[d_apic_dend, 1]}, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + Connections between network cells + for _ in range(5): + inhib_synapses.append(cartwheel.connect(pyramidal, type="simple")) + for _ in range(21): + inhib_synapses.append(tuberculoventral_1.connect(pyramidal, post_opts={"postsite":[pyram_basal, 1]}, type="multisite")) + inhib_synapses.append(tuberculoventral_2.connect(pyramidal, post_opts={"postsite":[pyram_basal, 1]}, type="multisite")) + for _ in range(15): + inhib_synapses.append(dstellate.connect(pyramidal, post_opts={"postsite":[pyram_basal, 1], "presite":[d_axon, 1]}, type="multisite")) + inhib_synapses.append(dstellate.connect(tuberculoventral_1, type='simple')) + inhib_synapses.append(dstellate.connect(tuberculoventral_2, type='simple')) + for _ in range(3): + inhib_synapses.append(dstellate.connect(dstellate, post_opts={"postsite":[d_basal_dend, 1], "presite":[d_axon, 1]}, type="simple")) + stim = insert_current_clamp(pyramidal.soma) + + # set up our recording vectors for each cell + Vm = h.Vector() + Vm.record(pyramidal.soma(0.5)._ref_v) + Vmtb = h.Vector() + Vmtb.record(tuberculoventral_1.soma(0.5)._ref_v) + Vmds = h.Vector() + Vmds.record(dstellate.soma(0.5)._ref_v) + rtime = h.Vector() + rtime.record(h._ref_t) + # hoc trial run + h.tstop = 1e3 * info["run_duration"] # duration of a run + h.celsius = info["temp"] + h.dt = info["dt"] + init_cells([pyramidal, tuberculoventral_1, tuberculoventral_2, dstellate]) + info["init"]() + h.t = 0.0 + # h.run() + + # dtime = time.time() - start + # print(f"Trial {run_number} completed after {dtime} secs") + + return { + "time": list(rtime), + "vm": list(Vm), + "auditory_nerve_cells": [x._spiketrain.tolist() for x in auditory_nerve_cells], + "vmtb": list(Vmtb), + "vmds": list(Vmds), + } + + +def insert_current_clamp(sec): + """ + :param sec: to attach too + dur: ms + amp: nA + delay: ms + :return: stim needs to be put in a variable to stay alive + """ + stim = h.IClamp(0.5, sec=sec) + stim.dur = 30 + stim.amp = -0.2 + stim.delay = 120 + return stim + + +def init_cells(cell: list): + for x in cell: + x.cell_initialize() diff --git a/modified_dend/network_pauser_prototype_dendrites.py b/modified_dend/network_pauser_prototype_dendrites.py new file mode 100644 index 0000000..09b5a6b --- /dev/null +++ b/modified_dend/network_pauser_prototype_dendrites.py @@ -0,0 +1,298 @@ +""" +Layout: + + if __name__==__main__ handles cmd args, instantiates test, runs it, displays results + + run_trial(): defines a model and then runs the test using preset params in hoc and return the info to the class + SGCInputTest: + __init__ : defines many static variables + run(): calls delivers information to the run_trial() function and the recieved information of a single run + back and stores it as an extensible list in the class + show(): displays graphs depending on the graph options selected below it and displayed in a printout + +""" +import os +import argparse +import time +import json +import sys +from random import randint +from cnmodel.protocols import Protocol +from cnmodel.util import sound +from cnmodel.util import custom_init +from graphs import * +from hoc_trial_run_dendrites import run + + +class NetworkSimulation(Protocol): + """ + Tests a Single cell with input recieved from the SGC + + __init__: almost all parameters can be modified + run(): simply loops over the run_trial() function and stores the results just + show(): constructs the graphs using other functions + + """ + + def __init__( + self, + temp=34.0, + seed=randint(2500, 400000), + nrep=10, + stimulus="tone", + simulator="cochlea", + debug=True, + dt=0.025 + ): + """ + :param temp: (float) must be at 34 for default pyramidal cells + :param dt: (float) determine hoc resolution + :param seed: (int) contributes to randomization, needs to be changed to see different results (reduce this + number if you keep getting a timeout error + :param nrep: (int) number of presentations !!must be changed in the __name__ function if not calling from cmd line!! + :param stimulus: (str) must be 'tone' + :param simulator: (str) currently using cochlea instead of matlab + :param n_sgc:(int) This is the number of SGC fibers that connect to the post synaptic cell + :param debug: (bool) controls most of the terminal printouts is on by default + :param cell: (str) cell type !!must be changed in the __name__ function if not calling from cmd line!! + """ + super().__init__() + # + self.debug = debug + self.nrep = nrep + self.stimulus = stimulus + self.run_duration = 0.3 # in seconds + # stim parameters + self.pip_duration = 0.05 # in seconds + self.pip_start = [0.14] # in seconds + self.Fs = 100e3 # in Hz + self.f0 = 14013.0 # stimulus in Hz + self.cf = 14013.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 0.0 # % mod depth, Hz + self.dbspl = 55.0 + # usually cochlea + self.simulator = simulator + self.sr = 2 # set SR group + self.seed = seed + # physiological parameters + self.temp = temp + self.dt = dt + self.synapse_type = "multisite" + self.species = "rat" + + if self.stimulus == "tone": + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + # result containers + self.vms = [] + self.vmtbs = [] + self.vmdss = [] + self.vmcar = [] + self.synapses = [] + self.auditory_nerve_cells = [] + self.time = [] + # debug function reports a print out of various information about the run + if self.debug: + print("Small Network Test Created") + print("#" * 70) + print(f"Running Test of Network with Simulated SGC fibers") + print() + print(f"Run Conditions: Run Time: {self.run_duration}s,") + print(f" Run Temp: {self.temp} ") + print(f" Number of Presentations: {nrep}") + print() + print(f"Stimulus Conditions: Type: {stimulus}") + print(f" Stim Duration: {self.pip_duration}s") + print(f" Characteristic F: {self.cf}hz") + print(f" Stim Start:{str(self.pip_start)}s") + + def run(self, processes=1, **kwargs): + """ + Runs the trials with the number of nreps set for the trial, calls a multiprocess run of however many trials + are needed. + If processes set to one then the it will run without the multiprocessing library(more reliable) + """ + super().run() + info = { + "stim": self.stim, + "simulator": self.simulator, + "cf": self.cf, + "sr": self.sr, + "run_duration": self.run_duration, + "synapse_type": self.synapse_type, + "temp": self.temp, + "dt": self.dt, + "init": custom_init, + } + # Generate inputs + b = [(self.seed + randint(0, 80000)) for _ in range(self.nrep)] + c = [info for _ in range(self.nrep)] + d = range(1, self.nrep+1) + + run_input = zip(b, c, d) + self.all_results = run(run_input, processes=processes) + self.unpack_data(self.all_results) + + def unpack_data(self, run_data): + len_of_data = 0 + for nr, res in enumerate(run_data): + try: + len_of_data += 1 + # res contains: {'time': time, 'vm': list(Vm), 'auditory_nerve_cells': auditory_nerve_cells._spiketrain,'vmtb': list(Vmtb)} + self.auditory_nerve_cells.append(res["auditory_nerve_cells"]) + self.time.append(res["time"]) + self.vms.append(res["vm"]) + self.vmtbs.append(res["vmtb"]) + self.vmdss.append(res["vmds"]) + self.vmcar.append(res["vmcar"]) + except(KeyError): + continue + self.nrep = len_of_data + + def show(self): + """ + Creates a single page graph that contains all of the graphs based on the graphical functions in the class + """ + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + p2 = an_spike_graph(self.win, self.auditory_nerve_cells, 0, 0) + p3 = spike_graph(self.win, self.time, self.vms, 1, 0, title="Pyramidal") + p5 = spike_graph( + self.win, self.time, self.vmtbs, 2, 0, title="Tuberculoventral" + ) + p6 = spike_graph(self.win, self.time, self.vmdss, 3, 0, title="D-Stellate") + p1 = stimulus_graph(self.win, self.stim, 0, 1) + p4 = voltage_graph(self.win, self.time, self.vms, 1, 1) + p7 = an_psth_graph( + self.win, self.auditory_nerve_cells, 2, 1 + ) + p8 = cell_psth_graph( + self.win, self.time, self.vms, 3, 1, title="Pyramidal" + ) + + # links x axis + p1.setXLink(p1) + p2.setXLink(p1) + p3.setXLink(p1) + p4.setXLink(p1) + p5.setXLink(p1) + p6.setXLink(p1) + p7.setXLink(p1) + p8.setXLink(p1) + + self.win.show() + if self.debug: + print("finished") + + def export(self): + if self.debug: + print("Exporting File Binary") + t = time.gmtime() + destination_name = f"{os.path.basename(__file__).strip('.py')}_{t.tm_mday}-{t.tm_mon}-{t.tm_year}_{t.tm_hour}_{t.tm_min}.json" + os.scandir() + dirname = os.path.join(os.path.dirname(__file__), "run_data") + if not os.path.isdir(dirname): + os.mkdir(dirname) + filepath = os.path.join(dirname, destination_name) + with open(filepath, "w") as f: + json.dump(self.all_results, f) + if self.debug: + print(f"Run saved to {filepath}") + + def load(self, load_file=None): + with open(load_file, "r") as file_in: + data_list = json.load(file_in) + self.unpack_data(data_list) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Compute Neuron response for small network of DCN" + ) + # parser.add_argument(type=str, dest='cell', default='pyramidal', + # choices=['bushy', 'tstellate', 'dstellate', 'octopus', + # 'tuberculoventral', 'pyramidal'], + # help='Select target cell') + parser.add_argument( + "-n", + "--nrep", + type=int, + dest="nrep", + default=4, + help="Set number of repetitions", + ) + parser.add_argument( + "-l", + "--load", + type=str, + dest="load_file", + default=None, + help="Load data from a previous run pickle", + ) + parser.add_argument( + "-s", + "--save", + type=str, + dest="save_flag", + default=True, + help="If you do not want to export file set flag to False", + ) + parser.add_argument( + "-d", + "--debug", + type=str, + dest="debug_flag", + default=True, + help="If you do not want to see debug text or GUI set to False", + ) + parser.add_argument( + "-p", + "--process", + type=int, + dest="processes", + default=1, + help="Set the number of processes that the run will be run across", + ) + args = parser.parse_args() + start = time.time() + nrep = args.nrep + processes = args.processes + load_file = args.load_file + + # manages how options affect the class that is created to manage the trial run + if args.save_flag == "False": + save = False + else: + save = True + if args.debug_flag == "False": + debug_flag = False + else: + debug_flag = True + + if not load_file: + test = NetworkSimulation(nrep=nrep, debug=debug_flag) + test.run(processes=processes) + if debug_flag: + test.show() + if save: + test.export() + else: + if os.path.exists(load_file): + test = NetworkSimulation(nrep=nrep, debug=False) + test.load(load_file) + test.show() + else: + raise FileNotFoundError(f"{load_file} does not exist") + dtime = time.time() - start + print("#" * 70) + print(f"Total Elapsed Time {dtime/60} mins") + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/project/Figures/3 presentation pyramidal cell.png b/project/Figures/3 presentation pyramidal cell.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d5e5268252f1a5f734311ecde81fc498de4617 GIT binary patch literal 55192 zcmd3NRajh0(=P5B2yVd%?(XiE;I1LKJA=Cq1RW$ng1fs*U~qQ}?(S#y{{HX$m*@6e z%rk5H>7KQ^R#kP?TW>|FsmP+E5TQUpL7~gbNohbq!BRj$!RR0%Kt?h>f}J2=$WC&) zu24`I{r`NRQyDRcp`a+B2rYeF!5R)GPuu~{&Fndi?tMr|4qQ&5l_pao3EKD~C3Lq+%lQZUW(L|J}xBCh(Pz7vR87d!);}nhCa%+* z7rf#>`gepyG~$hEtrO5ImTQ@qZU9{_o<;iDww_3SF*W^4s&-THC%C&42D?7@#cz zHI*m%AM2?5#h~+_@&C7>jHR+lEU@ULO*p>aIbK)2-*ye^#jqn__Yj0M{NRgs2AjtA zTnojrOR1;43C5d4WxIOiq`e8I{lS*T9wf+i)v>La-_C}Wo0xiDCV&lWz{6f@@^><} zW$t>2=^C!kZvvl5LkyPjs1I}N&Ym6@{H&1D+FI!c^;3Lk?|T3JC0AAoY8s0An1`45 zvBCJc;kpwIqT@GjW4S@bZ{^ryB!?B+P zFY(Skfb8vTL->yWWtz1IGr;M@;VAiGZ6mDf0w4VRStSjD;z*+f@c!m)+#AJ36nEVT zSNLVH!G`LjrR0hZ0Au_(I)(T)*EQPZ^?rUS_<{ii5o~qKV=T)R?n^}%JnYg`F?d&E zUN09@IViIG{RO~!X)8)DjthNpw{mf?;TNXT`JVYc@7VMHhnN=;~%A@lsi!pK>+@^mRRHU*L#}|ALPDEh*)q^0w9$;<>)$`4ZtbTy?>S1?O~( z*Y#9(5dnDMstRtE6M4Pa47?_PA5^{WdN~6G+~K>SKy*57ly2fm6JjYUe>^*30nd&J z?!)7Mx!$#(-=Q|^CN{G+;2Ikr+5l(%0nxbvFWr1a(^%2C&<_#sSY#qXhhd_EUE_|| zX*=)XfG7Rlw)1%Ji}Tm@VfC-8qd}b2bwCGH$%m#2^BoYMf7rDFam?mTxmoA_2~}^N?}C7=sw>x*Rk?^=B2u%Z zP%{pXxBGXp3xBfJkxQxow}&9{ZWV@JYgDVlaOazJ-p z?jzyD_=XaSQ2bW*!0~SWoI=w8yWA${BQ;_Z@P5aGIFp!LtMV4-ys1fC>#vhIBG1v` z9X(9>@r1<<7g4`{o80b96p18adDRR{5TBD<7cblBOUFjYf^mfCvo-Miyf(|=8*WXz zklW)CZkVY6@()5VDM?`S>Oh=C6~sslt69=~@6SN@G})1m(^t5Icf-%sc=B?P*pa05 z)gbIs*$j4;-8RWfFDo@Cj%-w%NwSQ;myt^y$^I}eOz9U)(Mf`w#yxX<-oczJ!B<9S!!sL?o zBspn91U9q;iS0Z%O4Onc8H5TnpO9G86${V^seTA^ibMVRgJ^eee)Q<{&dUs$Bbjt)Cl!SpV&)LKj{@@rP0Nany(ewo^~E+ULZ>fy4$ObG zJAG~8j7Lpv@oTl{aC8R~3V0Yj1)iawW^fO8U>IU2Af8FTNER27C)wVovSYHroaIB% zbl5_4_7o@pw4-*sa#L_}My8$Y55P_pBJs&Kpah{l5AmmYY@zhsCt7Ah|LlM`6~vi1(NfB*NaR0W#3Th(pn3`wnRt3E|i|4|M7l%fzp zKK)sI@?^sz${Y(V>>xeCWr7M!ctp*O3TwWqRHIXRN`kJE7-IZ6&Mz?rtl8Re###j22Fin{VT!I>05*Tg#szc2_a2ol_ZvlkSsmy0-e0t%y;O= zyp6ST;`HuG3nT>K2^CDIDuE69aNP&#rSc?!gMGtDrlj#jxA900yd8#Hw_c`z(s+9C za(0ylz8dY3pvxmC&UjJE*RO0sGzu!by6ca zgrzvMQ0vtP(ww8)t?WYchooGvo7lbDF_K?aCtnWYV-)SS_o2ByYSH2+eDtnBN-XZ^ z`bAEK8~^M}ENkRnLCbhqvx%WeVjc6CVnVDPbA}an06H|}K^ED$HI9%_qdLc-rhXSTRDP$$N6P;4^b zynObJjs7DwgTy(41m#lpKr34IpY@H1eHuWMuP!@oNKD3<8+(;riKM}&j~}} zAjhHuesb@P5nXwY{XSSvf?TiO2M2cuqcw&!r2kdjx&AOM)H(5CZuv1#@ z08S`#tm7a8q8_pSNm6wI#CnB3>4UX1y%0-g>1d|f^fOHsd0IetT2O-|&^&{pw#r(f zRJ}Pp=^zzTtRB5eL%|S379KAHN#Kr6C3!XA&_i3``y^Fz`S4oGdRo06R#XB(GR$6I ziUOCSM9WGzeloP{JONt%zLU=28J(GZm&rCk>Tw%a8G3q-O4F^Cn8){5{2~K{u32#< zfI^WTykJ*6zQFG<;baQ~;eoQ8q5aod zoJy}lj}5C%l%y;_i%G)h?=v^2H>bqp0lTj;maV#*>~!h*8R-Q4bS0MsGBsa=YG0Fb* zZ(y|pK{$EXM5+*&qyi-2lcp_D&e`8G9JRlZce<2V_c4=Op$sY$=@jMK%LJTP7BU;A zj|_;pk$$_F(){3{n&bj4cU{zh5*dz%a|DWcm`64nglg;2(T3|oJ;o4B$^Z;ady1*< zn04ibQeH8h;%5e(&fN8*_@7&IRirG2k(1dLH8R}E?6foBG1IzV4fYY9;?D~f9tak* zervz(+SK5qjz&3kv#!4?2n!Bw%7(*)8ClPnYgU_K67d|?;!2NmNZ-pRz_^T6RfP36}|&njMa zmTtbdff$PJ4AbhD|R$C7D%tJCHrV}9s zTUHI!u(NU20epW^r}U0t>?Nd(l&urJUZG;FL(NEOzet!%vU08Xo3{|&Kcb=ks) zafqnBxt#sR(B#e?Ub~^Va#e{O7j@|6u`${rK1u9Kcq}QM-DKHe7{TWg__*=+tQ^C3o|qksDn7rEc$>Z8EGD(BMGExJvc6J3rg`5fOz3O^yP5Hqyh;;a#-f%R)?_Qmw!Gh>5Zy`U0hv!Z8AE;Yw#Mu_wL zR%<{TKdr>k9wo?6-)H(&un#1`afMgpz{ANc~t*P6PT}n8ScAo5kadc6>bK zw(cD70&BOZ+EenN%XGo`{c)W_3#IH!W@L zgbjHHcYcBp_X6pIL6VK|CS6)X_O>PrxyS_ea5}5K3-cK1M%pl&q8V39=h{URELh4r z3(ve)?DG<$eD;36I+|ZwEf&?HHWEWgt0qo!A#`~g`P=0MkqTw zidzJN{9Z9|irfcxqKh2~QTRXiz*E>ZaSd3GMHz2Pso~j=z za=TFR1HU=9KoVTk2|X)bVZ9q`kp$a5)XiApm*i#~bz5@I5B5zx{5cmE^{ybC7)w*$ zLnZFJTkrK@Z_Bwb+SxLB7QHkiitte@mN+=#I1Bs4xyLt&&~aL|s_ocr;M=hcnp7Lj z(TQ$(4?#bTX!)rVg|t$Eeil{eqg5A@Fw*q3eiln~d=E8gW9u5C5A<&zo?0GQ7|AXN zhJF1XbSuwY&d?Sd@W*ky>jWj}=jzY5jkb*IWxx2%-W;`Th>Z#>f6OfrL>wdH47nB6 zVw9d12+S|fvyff$5O|3+0f!TIp54;V}&x3qIZ&Vzr4!BXQAGI zB_b9zvsRcsotN+qV3dz1LPEK^Uil)}0;}&%`XzMTm!pO_-9zE&je~tF?<=*1>!$eA zEv>%(>$tw%meV4A??W;I-H{ui>qY6#e#!H3t!n2^EkB4qSP6l`$!*d(`E_=$$+d^^ zib%K2M-$?6DRtPt6efFU315+gB7s9r{ak+0oXv|3u_ykSVjFVn8T~jdVU=|9aY@1@ zZ9^ai$F-L75j!_!l38MdGmN4p0xs;vIXmR<6`JR~k^n3v3~0}$QL$%|D~?^Qy>kG@ z_sT*lZ)UKqGkScWZzV2Yo%f=b78_PmWKGV^qmg_EDYyp<$Gk$?qP+os?jSp4ZSA@< z`t^=U5LWB}`EM{@kU=vpQz1rOON;l53Snc`fTvL5Y~3dbyD;XZBX=!QgZH!USNKT7 zE`)+F3J9IFNEtDhI)!$dI>CXo;{o^OXqB)U`4V=dgTtFdLB9xHEjz!L3Ayd_F-Syw z7tWOl9t_k*@^a>Nxb+;2Z$zSxHdN zl(!E{zdRQs?)zO)7aR0+gJ94lQa`S>Y`5?e5+6BANYie+cx_x3~iLGW$=4mW?u#D#x1 zZ#jev$fVWWuhIHfPHO>LceSLt7y0Zg0jNz&AqcFlPjPU;FsKu=7slj#{W>u~NM?-K zh-f9wQyf{*-4`KQoUr1ZIW5qPCySPK8kACXgGjL{`%39%K5YzV3*3mYU^NYnvgkvQ}XenG2XeM&PDfvAS+Pdc@_6Y0)&-*M<+wKLp$A$XlVa z=Ao(des<@$KU)(#S5#9dNz!FkQb13_{!xtu9TX?Q`O1Mr;erR8s5`PJ2`Fb*qu<^s z=A_eQ;^!oYB{)Nj+1Xe-FBFJtF0)fTE8G6D_yMudjdpL2fig=?1}E(*}VdnHG$NvjiCWwnw^S>3pcf<;Ax%L*s#bnAB(Z`S>v=*miUl z?3u)!*PYsJ!ft88F~@;O!JNU4e7Z49>{pGSPtS1GnxKk#tN*wQr7QVV6TuE-kRE*v zpZ@9e!zG2B2ptb)QZHvS$agqV=9i#LJ>0l3WB$b0W_FM)FT*tHrCt{Gv*)cDws^WF zPYLv41@bTHg9)Yi=*ds=sDcOQjfTzXoD@WXzZE4l(^E85kH<9XS~-aZGxC*(PE|db zql~b9un5^M=$rf=YU=97?DuW14D>E?1}q~&3%G08+TR9-WyTme#X45yqK84S6N*Oo z6WF*CifRU+lrx0RN!sFR3f|XX;zCo z?gDQ_-D_SiB1DDG6}>(ai7~i0tom%@WU?6|_Fu>IcZ>0{v2h@nWrdg}QEdqG@aR-E zwoJTo4Il2MSVvK;uBymrn0Jvz$C5N4g(`z$_o<9412B*ZdEuEOUp zW6T$&(lw;BcE8Od#M*jmh@4^}*Yq%say>P_cbg zrNpuYUcul??Jh&pX!BkD_zC?tU9k(Smt;Qy)PwDb-Jo5J&1tek=i1kHn%Gb<=D@CQ ziM$=au%i);^dY^P<5@_1skcpKH*paDN$})!{Wr%Ji&|u}Hx8O{cGUuqg~XLL9Ezo| zm!sZICUu6W>QR$|uS$3-0#<|DQP=rXR-{Ox8LGjho zA|OB;s4C=$4&1Tw<(`ORAzT}dO{t|IHR+t36L#9ZuT4uCVMyJxC7LbtcV@9^!tcxm zyq$foUZ9)Rrx?eyV77(Nb)(43a0%?N9FN&AY%sv%29$p|X=9O0iQ;B1c961z&lxViu8mt=PS>RooDL z`r9kURf}uh_h>cBFg7&qLgOBhh+gy6PX`4~8`C_HAaVQYM23CJ-CTD6DM_3cpqP=C zwp(od)O$*i#(@RFui22;p>M@Lszu45khd$h$-{St}di3(;F(4eT7z zhjga4E9giB(lpt`In`PANF>@A^ruvTUX#iEp5e$V?|ve^B4u0}d(nh3LEaG`@|q0* zpt{kQbG2K_fbtHLL53S4Q<>}%$$WFZ!VVk=HO4#Y$3jN}6|c^3fPvBvvhpbtjQ^yQ zRWLnT7!-??V&zey9a?QdnB7I+gi`%)zW>L* z=mBcZ^SrxRR+f)WyarvsW%z$;$b?rHTast1Ew1Zn`Lj`nXv2}MA%7LJ-84q`;*n9P zOD-0F--V!=Q+Obh4@48PSgiYc6^|qOB?fe~by3;T8mus1J3nKn^9|xEvl98fJRH3G zw88FqU^mtAr^L{L)qD+zKZ}i|E|-t#Z0`lIhlh%&@Ag>d7C|G4H5-~S=n;x6&3@td zv&}?I>}%E3h8^m6M>3;y-z2&*QGCeE{Q;YOgqo5C1U<41d0%EWhY$W>jk45o#$$!Afp9*np%ya`W zBoJwUFX9?wVuXEu|D!j}Ypa%E9G&x%RvB|YK_bpBe%w@BlcCKA!(3I(YAFc*tG|$S zv82$-jB>T=lP-A#%SnN1D-}+yZN2GAXB^E?U5sIAi|QPFNix$A^hvv_wE1&-hO!m` zk^#bZP0{D_F7Gr$37jC;$nM_WVMqXPW5WKVge7mb6cWx~w?f9@dO0WxA$FvGxiawBr2ok}qmvhv^A!g7cz|g_ym?90 zA9!n%tsUq~PCJB;^5rwuP_Ilg;JbWp!SmwLlbG6^y}4EF=8r1&fiHmPZj|GN*3+-> z`AURro6oB`71swzEMadJ5i97hYu1CMUH1MTna7fCC0Jp2s(ruNUqP{}n7^SK*A3BVG1Y%F#rk%H!%Wf=4PR99gkCI{;EYAW9B9JyJFg3hM}YXZ8Cq(n{Rg|c z-i13R{+}wU^KT+j7)FRvXuFh>*-PN#6+n?JJbXC` zXDILPwk(E-yKp3zt!%EBk=QCnQPvFy={LZ4vUPOUFXNRP|Hmh zS4nm#k_zG$=jVspo*7Y_|H}9%>Qa|LXSDF5&WzAkX&izBm56^@eGsy92xelg-I4!5 zkH%yXDMms_`SY!nY>mAAkCERBfYAKF7nzv^q=BB z<>pz2(d7-zd-e;!M;r9dh5zjw~*v#%(k@&g8YI+Ua zCnQ3_(7}w{UJ{=DE8o$-iisY*`5*=NVK3727xOu6kR2YD;t>4GJ{TieEcmH6AduXB z6WsEz?`4t1etIJCKS>?UEAx@ zbaI_7RkSkhguEGYmL)pg#bLpRm+So0**Nl@metOlyz+8Nm_J#t$S<%nMzx$%=fRd} zN_6jUz4%Y)EpR<%z}%X1s==!%St?;ecNpDgdOUe6q5Jg$9WMfnN6O}C1d5D&L)}^F z8@8BKM};^!&FVoaJ`onboOtO3gOGZ`;eai9YzI1HJQTGtX)NinCR?XPiSV^}edA~& z`6Haz5j(^}84|usk>B$hZehlLVhc$4I8et*#DJpnY`Q0jFQEZh zk&VqDd1yh0F-*R6w)>I?f2VD_lMw5Am)#4S(bl)9x!lsfIMQxE!`uXWAn2vpsBL#0 zEs}Hkw9C%x{WU44k_aewrzN3EM0o;8z=FkT3vVV0^1P_gb!ULZa=-OE_uYWx^L!uU zWOg1vDWlcbN<9K6e>FWE6_CkO$Tq#@+vccNEb%y7)~fgVB8vnviENH64+ASE?>kG1O|%#h1B7d(LVb5lG$}tt9YGX zyK#&>VPD&dOrIsljmOa`)nARE4R;GrQeq)IakKOb_VTDe5fAZT?S zg99VcieYq5y3yon(V~2K+GElGwh5zRynJ;07&h6!3V=#(X_Heu%)?Y9cx`zdt$IPx zge@y7y}l&wx+I3Me2OXK%>!N+0I%(=l7=5T&#zrY?_I4uIRVPb_9hET$|+4Tisq61<@!i(>kif&;({8_C8Rw+m*c&lzgl`WH~ z215;QY)6{@gtj6~ZW{|=3YzZ7>gRnDMKTY&Emb|hpEkF&jGyz%9t(7v=}acVl2y5S zMh#B;QN3N)&f(<0=Q6GBj<`BXvT!&LqnpL@W;%9(7TH5B+GT)I17SPubc%idLiGa& z#IS$25Qx+}TNyE1DTZIJq-vAfJ!5ol{C*4oP5)e`80!Yo~LZj0N5@b(LX4kA1iJ~tLW1G-4<*6o6^=s$WqV0H=7W1=bDc2RVXPJx5hcijLU zlCS5^)7KzfTL>ctjdj7XfcMPPBYsgdUOe>PfzET^my=>;oMhYA?&ESq73{R|run-_ zlB?enLx%m99;)A&(rY53qG$X@OK}?5&4PTV=M~+`oy@T&2OM}4J*=3!-rX^&8gUf^ zEyyiPcaR`w=^qZt4jD>_n8#+X7KMV|-J*i?s^wxUn;})tL|V0;a1J3J@h}gMUhzfU z{@D5+6Pf&Nu3PXt_Rek2YaIaadQD|YPnXl+!i=t=JHlg#PpVDFR{a=g5Kf5Y&cE93 z&Hp15l$9kIKv&37bpHmJza@`=7sB7Wmcv@FSc}-R2;ON13z}`FB1-F=Y+y6*nKy#W zOAY&HF&I|y#IN`}vDzbXM(ofdDY%4&I!;-ypH|N<5l`GLh-*x1$fq(Z3c`3P1NV^} z%5g;67dBWgO6=1TtI0_-OYGr*V}WRuR2q@=3f|2KyJf~NBbivia77q_`cpBlUQGk7vBTvnutpHzw99zl!6 z!gA=Pl^Bt-g_2n#psvBITnRSl-uF}*MI^C+^sMYw-=fr)iKf4{b zOQdzlKJ0Qmrl?5t1J`|EJRE_fm8}@0F3Yz?4&0 zTnar>8{+~}K=;{8)doL*NNJtC1NTBGy~IoH409c>Zz`uEwJ8>JB)qV-{Cf{iZSd*m z7+(JLyClrm-%o`&yrt0P^1Ii-u6!he1@7Pspj%IHiF5S#xS%S3^v|j0p2~x_5;|4W znPmm1@yYIDByY1sTP|I)&a)$$OQle-+!T9wg3{p{g{5;ARt09C)7OL1)~3ppRC5Fl6^VK309Ipw!5@GGgN1@uV?3Nx?FP5YY1ZT6|Txx0odRR z3-?$S9dOTm7v{#p;BW-){``ffzRhvXx~y!q5AdKxuhrP}XAq+l98XDcW)^I@xkCq< z;(4+BV_O^6TnyK;a~HAhxyRGdXBm@Y;M)^f2X^6!&d>YK?&~eX1MW{=r`(Z1kC6`3 zC<ylX|vk1@3Y+_3y4NTJMmR>+5kX=3k_MADhRUsNl zn=?}9^Er5OI3h4#hhWD_!!d88Y>~`#Vg8ps*L%#-qyPm${`MR&XIQKRPQ6U5@+;*q z4L3tsnb>aK7i3dYQ!mmag@_D4cL1@`hAbMcgDBxTSvZ#9cjNa+%$>9G$(5$9djR=1 z!TzSK=+QYD_Ei0;16jHx-I%KJ`8}1b^a14v+{QW~76abJz|eqpIg%j*gu_gsg}>&_ zBIT7I8OYtFZldQX_6Ax4s7@!jn!62u zC^agYSY*4zzx{T$vDr1WW%Y|L;f-iB`NX9^1GnZe7>BT1s;uy#um^;c9!2Wlm}0t9 zm%p>45`XHmdLBHd*b{9Qi`|=GL_tk0%kpSc5~|a8*2d~2tYFpK3=qwL#@@=>IGGqj zl8E{+z^8jJ&9)4GOnC4b;3oLVTE_9?&6;orVbQ$#u8h)dr5JXr#?ewui04j9s|m{& z><8~-t(-2-M+(v#siD<=+|9o@n5~b8gJN{`yFxMSe=m62DdlRqmAJ&2Dx2Ws|LQO? zi+W>Jn8e~TC0WpJ&sUgxawD`$^n($rre7x7w}`M9Q}hu=_NVPlf63AP{_rL1^_A$W zNX`*8CkrfVfw4Wi=5LIJKq#)PNM|SWsL0>sfpE|t1YJb$sK_934aIo}qIfriA;5cw zg0y%^Bo7Olq@GhLkiXBVWi*ooaeE-!IwB3^D>?QMz~s>6R6GQcD6M2FoK5{Scwy#G zHlg?(HD0l&x7(Vx_eOCdq&;oSCq+QkFWb{v?5Ieb;?tmoV?MW>M(lTHa@esigTprG z=r@Sp1u%C=`h#R|8P83A@|^}lvW9%aD5T@8!?(vMfmFWUjnfIY=Etu_MmciP9=&f9 zR1SMftLV=Se7+ieSNFz*DnQ26=9X&3n>R(JFw|FbBVXngH4`?*CaI{OjBX3i?7xHz zxr^2lPJSgonA~^AZWxs~w$Go$Urz8r!uA3XW-Pibv$K%QehM0g`Av=XXWKiEETYxQ zJ1p_oi>-c~pY*zyty6q&)yphuisZp-9`U6Oq5K%E66fGR+(I!XdHnK<3Qb?y;?ksq zZX`uwbEe}}V(!kH>EVczfO}%N_Y!5qryRt+C4pLwu*Z1_a8#(9KUXry)#Bp)?q0zJ!JR^{6{XxzdF@=zB2>RW7(av#ITQlMp5>j}TrcPV(`&I< zA8Pkb3);q&65c+POx*-n=$2Js2f26ye({Q@qbV%D3=j)KnoL;Y{;+xF66Qy0l$1~7 zmb2r=n4s|zYaZGSCdK6H#t`UaoiR(Lk#MS`MapYL2w`MMKvLXDS3ctF?U*H7PLvUo z(q;K+po3CVAiH*m?|H}0#FTxXzW=Fj7g!wQijBwi{XPlYPC}DtLHE#c6p|~3J{B8j$igI`l%REZ7Ls{hWE$fA%u>zhy zROfBQn`A1zdQ^{lWR}4kUY9rRb2Dhuf7hy=!i4xO0 z+_C0o8xawhnCrLg4X*L}6CFbxsXGfda<>lp2}!-J&+`De10Vtgn1-8TJ;c_AISNUt zLMX0T!aqO70eFhMo`DXGta-l1g07mkDEe{)F()M@iu*I1MILc`|7LYD+)qh;zL%~Z zh3fzoa(_7ISfrOMz>Gfu?ZxHN7_W&lyCJ>=h-H5h}P zo+89L6>#(3b>Uw&Z%|Q(G=C`f5UDp+oW=dM5C{;dWswYB0Hp6f8^`+?Z$GOs!;O3& zRt`t(^bPyeviP9|LOzR8#6XrP%-EUwrTsWsLHioUE|d6&rYxS^Gu{Zl+_7C*jbJ?< z+u{=Wv`}@%zC|G`Gis#D;r>3$cqishs>Kh=gN4#vJsdmx%z)3?u4+|ORqNq~pPGDr z8N*IPD(XJXvI~h63mb1?tOf5cQMqzFjx}L@?AW-#5=`^o3(pg>p!oG;{@$wTjOAqo zD^ZM0XqzWey|eA{f0;anWREWdPQ@(31h zfckLr9x+U2QCE}Oi4wCJgm`+~^RYC^1h#bxR`tcFf^1Fz_gF%#{p9z--_T2MKu7F% z>FIg%4yb-1FN|yRiNclbM%7=2KSk1%cppTnHFsoz?9L4-v>lHk{W!!*+QKjEn8Pgv z=;y`vo0(Y3l{ixs*x2$~f(hpBD+$x^l-NPO_sJN#Fn}A`HC=ovDXH>DeG@cM_Dji5 z$IfuqAud)mb7z(jC*R9~>&|4;waW3?@4k&xUwUD7S&VQ?AxN!4NihfCalZKQLFcmq zh_#Bj$j~C;B!7#h&qOV}9Ptpv-&+CW)HS&YIm{7VEP{ z9}yylOt_b4o@o8|4b%Mt1R~(Z3x@Ml1SkN?Oei92t)R{gQx3tmWkkRdN6kP!x+?@~ z>;}(Rj{!%PwA}$kyBOBFtotrp7HIWuF;A&EV*=OK$d(Y=0cX5o;=BrBS3jMYsN(OELn~(8QUiCITdfOwKeX9d1;$Wwaa^)) z0{N^e>%pUp35zakI&`TGd$-N%zez%NE1`~Uf#O}&D*NK06 z)}<1(=xnU%T0gXE>=!qBI%qJs|HVL&;lkqY?uRo&e;=`Cox~tvWE?`vixWrl^An_| z?B(xT7;!Iv>L2(-IzvfGnM?kCJ%!M=+|(>Y+=)X*LEaI?CY0{uP_?h)kE7G)-Q@zt z41HA1iHbV!DzUnnOcA?6EcH>|L=TsUCH`-F9?cC|vcVYjPb-GI>KmiCSb@S3G zMmj~WTZY?4{&E!voSfzDf|55Sdtna_@9FzFiUI2|I5;ER|I`UZ%_sUyA20eKMGn*0 z_<rWw8#!A-MM&MS#M2+$ai8ZId`pEWJx|ftapz8Nf`BW^N;Xo3u&N& zh=4z$ZyBcSkEi!4*-m(}>j?ep5Wug%FzzFDxWY`gOmgE2Jr$Np07tKD0nOSTNLqI6-|*%M&VTg9-Y2vk>hV+Ix&$w zEE}6zPIOBmoWQZtes<4u+8Q~MJ`6!PxX|rBAl(aM!~G-Jvon3D zcE482%)Wp3YGcIDK)%97fz$&91|jm8-E-atgZ|;)R~EREk6|9b<0Ce}f>R9Q5mD{W z9KeCtm4<>(2mwm5`)D~jW@ee=1@Y$Kdhahw5(1YOpnLILzuxBw-jSB5di3)K+ljw6 z!f~I!(k`Im$({_#^&EZ7f1e^JZ7V}Ec)y(Zwy?0sBwL-?ZZ$9A-WZ?d9r?_}AYrTz zi%Bo7=AE*;47h{(juXGKf?%!equ6SMi0@TTOa?9fZZT+b){eB0cFBYHbo;3jbM7CPyt)NdBdmd!b@G*cSSLhy{um|gbVc75gAuBeY= zW~vKFOMh`$p;eEx6+m)Tyit0QO76`^;+!38>Z!+nRU<1RwEfygV6bj2P-@!GAq*Pl zX(zUu9F`qhJ*nzr|6rcn|B8(fX|D4IW^P=6{anM~_IyOUR&lffzf2AY^PAk@t!1}Y zuuB6vXxHEzxi{xd;EbyjeC=r145`2tENO=+Gh_&t;*%g3(Ug-O(VmVF=2YoKw zth`k?fsJ#o+FBo44@}8yT36snWdKqQ0efj(c4%4ura=-&L^QO~Z-Te{7rNm^9Z{hcPd5C;f z9`1V9BNla%uaJ_`3pJ`js!SC0Lnv4aUsUHs7jV`H%^48f*Hlvfw)@+0VvA6gWW~tO zsa^$f-5=k`_jJ)vdUM(K4->#T{(D;@UJ@}Zke7GSpi{aov7N7l1oxWMj~Hv;&gUgo zR31y$pe_dw)y}Tlqgb*+ouZ(P-T$b%YhVCj2B|54@;iE@{~ig&W6d=^Y)`}8;|cxQ8-+Ulqq}}ovmhcUh@xQ)c(U}mP0Hxk9{U#7#RxC?pX#_C@Cwg*XJ6ScBWkT^C zT-J0AKBdV(0fA~PSzDAheBlaVciNru?dC>V> zTeil>O;!%m1#(GY@vHlEjY)<6@X`=_ciOpn|@UhF-j zAv|RP-X_NlNHgF+^f)Q9F9Q7KAzJ#wdnTq^k*_qn%MM{_BJv`QArvEIdTR_=yJ3D7 zSQvzH(O5r(iu#BKA6J{9MPIf!y);xAsHx-gJYHS(OdW1?GwBtgn`F~5kYMP+Jc}Lg zFxUYc17aT-cNLk5$lGN^jvTKm`b?L8(=h1dStPLD{uVLD&-u#L&NYbgSc7l1thKdg|2SuIV=gqLa+o$G zt!jEDJIbx55NGSC?t^C~eN+BtqLg6pX>g65`*VE4%0RbVAkt)_EV%z(} zd!rG>CSjkFqL6K->|~jkEI8fiXa{*df3ecmm6Yjf+pz0hCGk7fA37rnXS?i00MRY{ zM%Z&3XkiQe&YTB7i#pOs5hx)-krv|=@ZrH7MpncTsef~uO}8OCz_a38lay4GM)Opl z?ZUT5($a;a^4AqdO1G5+UDQ1eR3KxyaORF;&w?EJ@bx!Orqt03v1X|#F%oh6aKEXY zh&6Yy*+Uy1V8(#xYjNEzuNm5xGQ^5UN&{4DOvfIOsdhmBFGvsb<SbC&e(Oh`t7285QMigC&*Y+e!(f`VqkBlfLd z2|u(M-VSwEQnIX*shI60BJ_u9u7jPKxj1#afBsGOb1Ag<1*6WhNw-SokiKp$+^?nQ zhLL=;wdE5y$eN^O5DsT$1e1GdeN#IOv|1x-z92>d+?Rd3_;Vp-6k}h~Ka1)0cu?=ID_1OpI9c-AtOy~_`?3tM#aYBB4 z8lY!W4?1IT=t{%Zm_3RsKdHG|SEFf2RP@I5r=JUto)G(H5j}@Merb@7rACpFDP_qn zVTf#3CpljKqM0=&o0e_>vPixZ5jmjEy;iCT@RRk8@p4GDVmu+!%~HmfmwkCVUUT(Q zR$1=30AHuiN@imUA%;NwfGQ|RQ0WZ_p3?!TS+`e3faVqv7jHmwjTLB!R(*9=8tt@n z_#Xu$uF=uq;3Xx_{u&>8zqOc3Fn*+j;??7SkT_j++*|*f^|b0V?xYX?{*{(bM5Jdh z+L9~7V$jcD$OsSgC?WI>StbqDZ7BoA6${JKNHY)AYiX-W!r1`N$w#d15v)*ByAt}z z|EoS!+~}wbc*4I5=iGfG*!&9}opoO1=b-aMKEYii7pj6Q%Rfu`+xOd~Jncs}k=YOZ zM;J>PzsBzrHOgYzZ(BOpg7M&(#g*i#Fv2;rXJI^RIFXePj!H|F_qpJTGRv_WbG{;d zfmgQ%DSu1%)EnF~l#mLzz9v5Vb9Cre*NxsHz|yvIMJL=7+GaYF@6%F+VJFxX>*vx|E=GYuSu@}~$C6sQ?Prwa20_bC)J!>T%d zU6tguyDD+Y$Oo+j%p-mqzz#tU6bwXJTj4>~>Uto6A8~TPjWvpEd|qJd=v;L>(d2xx z29fJ1O_m5OKLr z(FdBA@^U4knfdg~rW9ZK0GXJMPQ)|Z+gV-#& zeGxKJht>^X3|BY%If7>tFbDg?6%p6@X>rWTDYQV5DxpY)|2SrVbm+>?Kwnv#-pKpf6+s97=GpeoW<;Kag^T zIgaI#Ywf)RIzkdYs6VApbIwuWP)ygjnUH7(izCHKT(=)4hR%=c52l4{J3?J8B%b0BR_<3HHvF3Y1w{pdb&E)aqYC-cU4HxRhJjV~UGMdMZ_bAu zVsR53W;a&rr|wm(aogJ}wqK`Y^3#CpqB>^|}Z z-o?cki5GHg+SmqU;Lle<&m__J(_T=!&zZ~RLIQP-<^3Q)gC>JprSK*lu`JbSfk8o) zsD>8x-Q*LpCpl-6JByUy_oAY6liiD~tY4HZF=A`t!2z>n9 zNp$8wEtA9BItDCqFgf4oh0^&*+E40u68J_z{YP@SbNBgZ3>Nn%Uhl6CYRaq@1n2>d za(642y{%rbRqEoUHP?=1*R)>SWSAj+Y>Rna#Q7(&kZJ6+43SSu8h!u-=b(@ zjqY4aSWg3ZA&TWrj9`9t@uJvT@04M53Ai8Gv>Z^{bKcv}xzyEdoVIIK-py;FqV+a+ zBa$X7t5*0Kl*x+B6HIQMD0Gc^TBdtyVY#lu%k!4mU!*Kdbzk6_`CJjcy_(^IGksv1 zGflc*7t-svslso6kcF`SFtUZojv?X^Ud`5 z(NKa?{uk3lN*PTko(>*|WUSu%i{tZ{ogIh6e#hmDOi5$Z5_<%}m0SJcTtS&4SZPJu zl3cv0_#r6il!(FV{*(COD-Y;!LRkqbv))m%R~0nONL)GhNbMgl>cj+KAe9hTCE&L6Ze?(ZK+ zpD$w1XTM}FW!AuQ*dm2AY1AN4Z#-@xy89fXH^tn>NqvQQq?eT86wLM{6Hg2*rz39A z9368W4Ny@OtS`^`Ey74O8AI%(zxJC^(m(=$be&P zqv=!eZw&eKo0EmB*_Gs^vF@B%W|Vbz*l)tKm%H}6$<~G#MyuNz9k_B`28ciNWz~^R z*+W+V=V0JdHPxQo#767Gn>yD&N3qJzbTw?Tgzl9d_Y93yEEmKxcmAC~5)MajAiiqS z*|9>?uoTbVb&o^Z0wx|h<%J^}%U_f^MUl9WfxaV&zfI%{ngHOxGarydBbkvAAdIi=g=Sqm0rdLC)cb=~-im1|W zVI&N#vyD)^Q_I$$#7BW3hnt!%=+M&VF+aX#Yc8ns8shXMbBgQ4?Katg%GGdSm}u3Z zXUO*@=U6WDm(QMA*Gl4F>9L6`bVg9Cw`4fJUxRI#qNqZIA8ea550^iWXFGh| z-ri16L;WOR+!9(t$*0>vjc{xY>gM$YgE~0DQimHe53k}pzB{oy$J1L8mO^H#cBZ%Z zh?7Q;oy79y3PW`2z+pGds=4ylIYw?fnQVj4w|Z=Orrw7Yu}@;`TlVf64RE&Y$izPC zI_u6)+OOQr>Av)wfjInGC@A8oXE#?*^9r1EzeQ7_oepovTercUX?B^tM(y(#&)e)i zEytRwl>xsPRRyFF5D;2->Ml3&o_87b=hK)xuUq&sQn)@qw(PFaG2B*cUqop#=xB^7 zwHfd}wk^x=g%E5E({6@Dk9Bx}Inh(sMD|3foW`~vR$r`%XKb~`7zYmjtrdTTEt|qF z2whOLYr5+0q%j|~HZn9oT3$3qm!s}c>PWprfYzDzfJqMs4|7IK(UUc)%rvrcMPsK7 zPs`Ik_gGTK4YrUW4*VtT;Mt(8A~N~swY{bC0i=#`i&h-8Wx>AnYRU}M*z1*xLIym{ zdI zqPRqkPye)R{wYopJVauL18cqLciF%F?EL`U@o>Lj-F3}+nyJ+z>xz}Y-4J%CY8rRB z$<5GpVm9uaGybv)-gP)UGQcXoZ*noo+_mG=>|eiGN%vh%C2D#opiV^Wg4;S+$$(#u z?+f)>Jm{jOH8|I2!kx7+%;MCrTOVJVmxiqI=!3g}xwxLO9NE)*LudxCyYzTO*0F%x zreE&L(;GFP&wMTSs!{nVXAu6RPk8WH*|;7x(%Jn@`XZ9YiXY8aO%cR-Os8|pnlE@8 z!+C6(m`@9W3ztnLUKy%@s_#=ENr4EZvhIEA~bWh?vXd;C3{HDI>` z{RXXee>TBLLMuap6yruzHM03za~glA+Gz7a*T|a=e^f|sx5<%j?oOy~&e7$W9sbsqok5kDpBEIS2JQ-M?Wt1x$SrRk|N$$_S-GvsNsfPhu72 z2^vn&M-Ug;W}b~xH!7x}4v6R^&N2#IvWX>kUd5-hnJMTW%2PhNl^21t{#s#R-Op~{ z9?oNdxO3{CSzP~O6V~PlPsO_~ar(IwrK*>eGI<~u2aSg<6Va)p*I|Y^Q?RTwC;?rH z4JV&@J}#~(5Q`Sq@oN$-mVk4plLP1DneS$%-A3zvTefta{Ct%PYdB=ba~*yKaiT&m z5r@!gefp}KC`;-Wz5dLpDZ0*kw(EcJ7ywsq&rr3=)B zQCh8Y98dRHzAK}}e4vk^Y%|I@K~M8GSx%$o85tXi)lT6L&o(iCy`t=@yh7{OJCOWt z#^=mAY)pr5%6kuX+`jFjIuX?sHnAe%1 zA=-=6zsi&Eu58BeY&>7hUEP)N_Xa>)ZcJ}=-fdjZY(}xpuE$q*S24WpV0@##DDks_ zJou%hYS!)AnB#neE(FIs>9eFN;p|3L2@?@8zuOy$Q>l7_4_WhVpPz!plJ+mZ?!5gS z5vA#<->2_>gNaQWm)sleXVlVc3LP}~QDTIWwMmM5E14D>pmfRh?u2d&O(1rMy{4b zyl4|P?VQ*PI6fP-7?f5yx|jT&)87Fl^ugNw=pSbl-0HpHs>B*Rn{QEKDecJq zwyNw`{Yk|}iDYHXiY)7(PyMBr2{#ZjN|#l(k-^fcBZ;^^0bGIfo9R!;zz1wPzh#}` zit%yIh41$5G+N0K-uI}EjsozO{lxP^p4sj?QwKc=LP&?81&L zt^DGKcl1X54}CzCvNOmxSl;jtk;qByp4yruTGn`gC0vk<#?q=_NHS$-iYwv z5noj&f}%-S-kw!P*|o=*Ghch!Mx9f-J?P>^&_lRM7940)_gG1yr;2Rvr=_)Ds+kC_ zeD`D~kZgBV2|3+fN=|k#PqzXMBHdb_&3wWiQh0J0N>9fZQ-@|d$XoBnGd?F-mXS!ua@iSJN&vCq6zQ~h1_NVvKrEul3d5_e?w z+GNf`(|mkZhgB6@;I*b4!zK38`NhriA8E@GLrd(FTV(S+$(zxgZLm=)pFFVr`grLu zFwkaYR%3p9`_pp=Nthq9z_ZChl};Cl!#z({yFNT=jEuBeYptlubaTHU=}&GLWc=&L z7Y_O08!fy7l3rjyr2FijIVk4c9hn-cf~XccT_>px(n@Dig^dWXnD`FzyQ;?Kzd=mF zAOMuOcUf@bA`jH!1vOV97aYr%>$np5?PGfsg|3~krZq!v#^={w{+20sKHg`hL8tG# zw>WcoySFQIC1vufeE8nyABjEpNW7jZxk~3r8@d*BWVc z?(@H;N8uo;zozxEGP_=#zo;f8Y`AtS>9HG8j@s&*E2LLPF5YFjqFR^-^!K02U+<0S z6bDb|8@;ZQYS#}T2g2mLeM?H}-w3xOQzZbLV)H>_>8W27geu{)BKu72j33niKV!gP zc3x}?nStuN`A5x4kF>1AUfk^?ILfs1=dVfkACGT5^w{fUGyQ_l6Gnu3wn=?vu^Zqe zk5}Qb95T7eJU;B~=sV%mQq{&}W!n<=szi~?be;=1vY6ju0}jg1_C5(I8;I-}*VvWd zfudb*6NW&%(Hm`f*0HtRT!^GZC_KFLi*9YU0TX@1YD1%Y@{Sjmv!;oW75*H91%{Rj zG4(o;mkl~Cm_Qg$d&|3AoUz{=X$~5^iKn@LgBEl*m$VYyj%#CdVlOY3y^|U=(>P)Y zeDTGXW$iBOuFkY4ZoIM@lP$h2MLqYuM0sxOa$d=#e4*P+I^sCrb3JM?j$7WnfY$A~ zSo(14%@9-VbN8u9T;+1RWFax9ic+S14<9c54p)uN{PRpbe~a6V3a-~_qPBT^NRmb~ z>Q08}{fm4lT3>Yr@6C?2{LcOMN>^wR1vUS!(eYc3C{u3*&2!FKUAOT~^cVXP`YhJU zjAc!?J-o)qWmqo(Na#+U3s^w`m)^^got+(jh5*hml>Jt=g2LD+Q)kkCA4LA=EXwK- zyWFn2?MPb=&wvq!4Rh8WwxR5?v&Pi(f05^R-Di5AL3~^7C<>P4yryuRN$0yy^+Y;P-nq%;MV###ftBzpG3C z8PC?Xvv*p{!cq^3-Q!|7RJa!XlsiVF#$b4H*UhvSd-Y)M!1YrQw938(1Lhu@5@nc4 z)ic#C4;6K7zz3J-DG=}FwQxoyyf)uC_Dnv?jFq?97?0 zCo}r}D8H_b@HE+z#xkSLVX)#etO6VkWt|L2*%nR$!P*jksX&}146uFK6|x*gW6}L5 zAsVBGUzPrT4qO`DbCx@M(1!M#{^53Qz~M`9`B4vtO2K1@eXrPOf~NJ(VNG7o{;+J{ zg~woz{|#QN+5OMLX>a+3A&unL7EkW>1y$j_yc{G?SGL46a%2mS{N^VDG1i;rSV0%| zhi-lpn=RKU!E85i@ZW6vA3t!m6>YoH=gK>d`HsHiJntF!G=un3DPxrj?fo$1J{RZv zKdjF^ua)h_7<5y{3QT|t_pkc5)Z89B9dP+QgQY{n99|y|O=G|RifQ+_?e(qoI(V?o za6q{IW_0gy$3e*NgGCRfp7ayfufQpzPNzG=PI*l1ibG(I67W(d$}z!OPTOo ztsU{3a)Ylp0tb80=}M*9VP&Lx<6GL~){b=7&hKm^(w`-?uJ(jCkYT9)WVIzFoQCJg_4Q z*8k!^St_Ts%MXbBikMB5YB2&w-ybrG(q(lU-^f_9H+Gm61O==Q&3`Tdey{i8u{ z!frnQ1JV)&BJU4#T)xF_uGI3Trsw^;R0%GY)#IW1}Dvu?VQ%8AUp zeXKeul^q>dkgx5oeNQBocvqnWZXR2{lJ*0v2D<7i$)=+lTfc?m)*I1Y&(*+&1njoa zZ3(5B1%Flur;AOPfCCTO;P&3uhJh1bcjiGHbvvz@tyDI!-Tbz`D4W?t811!(zDi*k zH3(@3T1`&*UNW#oyS1NI;>f~G$fZj z88gh za%~;gb>eb4{&!Q;EI?X*&bQu55n5gyLST2;@|v?qNjUcfQ|siJ!40!q6_OHTI8VgI zj`eEp86+~8J6>kCa&J4y@S{a@ZPI$M92!A3(|Tx01^Rx!(P9Ha4Bo=1YLxY~ZLg_BL5-Zvh6Q~fl4?zW3`;m! z#L_5bde8Aee0`DMc1XK}EN=>MP#Meg-SY7Wzd`*4+xYX$mDk-~Z3PWovAq*+{REQ1 z7{1#ZU+^iPWa7ZJKg{HBVM*Q^ydJ~MWRT(8-$<2N4e5}tflRh`+x60nRH=*h~z zAoX#UIg0(4{ceGHI<0V_}* zo%mxfF<>;Ku+1*Dt}l*j+{ApFumyCUFWjgiA5a}cm@~)N9$DjW1TnF(=dYxM!fZh) z4>d0=z9!<&x1k`B5SJN7u=QX;+Qmk%c~H@gtTw6>{Sx4NK8@!{_T#uaSR&NITHzVj zl+jao@CgRr9Q$1V=#uon)AfpXl;)D2L>G=~GmwB|W}pQH^=R2CYWZSGj-gfPR<18L zobi8;iwOLMXG3qAT@pm7YX2<1Z-1=k8UMxp$>nCy`$Q9@?1j?{3t`JQ{>XIAoJ_Gp z*^@K{frPImOV5miLK9eO#GKwk_%ZsM;-Zk0*OPUVu&6NZg;6uBu@yN5r6GHujXDOm zv)99yqVe^Bc#6rn-t|s%Bx1{Bl&|3ZUM`oWQ>9JVN5;+ld zwfQKgV`E3(5DQ<2bFTJ3gG#uxh4n%fhFBqZof7J(B z!X={&l{0IFpvpK)a(?q;B?`irjuP!HyKN-AU8tY9Kkr4fTR86-N>YEm%1z`lm`#t$ zowgEc`Cn@R9$^%PiA2)2Hz#~UzFEO#vkWxbWQsNEWhxLyc)@TS3Y1e9w9#uF4YDZ= z1wEhGS-u9Q4A4I+g_1PnyR?pL*Qcj>Cm-`<6igy7-$N;Or@ zi3EW_J1P}>Ra}{QCUW@E#lmX!2QpNDF#D4*eEIA!URIE`|05H)+_$lqLWD#g=uaL8 zSHEs?6L_uvHaAu`!Nq68wHjGcBN?sO$$Z$_j*;iq60t0#aXcf{#t4xM&A5@QHai() zt0=JvxKyZ7SMR$+GFp5^#)3}mMDcXO;)?kov_*)!ioO1V%g}kP-*xoLY(VxKFKT{i zwD~l*rTrKNn|&w2@p2=FW8QMEeGqh{zw^gdO=ZX?u%O;v#w7 z_mNjju8Y>s2zi{hKB-w_c{z)NM^@HLOJC?e>sE93f?-LnEoXc(*(8bTW^?39T#&75 zO{14>Sm{drDQ_ouTMXB(kZO@^SV>LQiYMEzT&N`=$YQ7yprykKwS3z(8^^;I!4zMh zO>{#BU9K85%0(Tx?KihlPa^G=^}+?2>r4={yd3v4aT_QjbUee^MGRP8JQ9tW;ubjJ zV>F;mZ^S&Pc<_cNn}!@YOX-)#FEQvuD(93`i^rLnY_+@B_XJHIZ@O9PQOZa9C#pkP zPSU*NlGd*V$;C?PKYQNxK;t;KTbjeQ+@|4Km#RH=K6_Lmiy_}mif!Gs8T?a_p)|K; zD}b-0`dx|g=Y1&ZlPGIl+&_;(d_QeR_(2%^6_BwnM^brgXd|XQ!@khuTV4f{5;dk> zxIk`)zp@ZU*VSC5?R|69<*S$V3V}m&caht8#bq_^?ae5`_=WOxLdtbP$8v$z z_wbl7bIh_&H_~Ke_JQ~7vBAgITUdhb79T_9S!4dBI^UQ_h#h@ZcEgRo#y&!XeB6la zk1KPB`twi&SRUiwYma>P(qzonh*w*d)U!n;{+VzR*0|a&=oC0dr2MdfCy!2KN}tW7 z7%(m_Fs0s9!uESbx>>|@k{W?CmU5w@LM1Q+Y@3%8>_-G5?p2S{@KU1$ z)?}Nrirx7MpM|TCJQw#1w^=`Z$2$&v(q4RCV*0+XR-YZ;woMCl5jSrrc3btAfkUr* z743vl6=Z|7h~Vtl1B+>JZM5;?kts_ngLjOhR(kUme$3F{ZHdu4U-_?KNI|L&Zaz%{&{}v+)omIVyP#B4 zlGNcrf24-<18iK}>dtk#r$1@VeZSU(!RwV|^m z3n{6fdfzwWxL%nPnXc#VsURxsJiZqlRUeYUv=QC?ptR`D_NGd6HccO%-OU`eKf$ z8v3EB$vruMDs(US8Pt3z0}2erqjH()&}TzGX5Xe}G*((97AsP}-wb_`ui`OsAow$* z>ZqBQDsvyDqt?|&$jK^h|+6X=I)(#g_M8Sw@F9YEg`7QyMco>*gycmEz zdm%<54ccIGs})y4@1{$d4l_WKKQLL)?9?TCi;tV!HuC5k2{UwLGOzwnTidm(sHpoj zbcfYClt3)%vopuy5wiKVbl9Z`1S&R+R3LN+7Fzev=E}M?O!SCfJ9n6OVM=^NMnH1j zXM1k=Ityx=DOQID4IN^91maJEir5{kz0~2sNPEqBT7@v5p`mdxjHqdqhH5jJ3k3u5 zSjD6{=TSMT&bmtWUOe3nh}Bza?Dp{$hG2q1F<1={%w*Djg)qNC5CVZiDb1VhSby&O~_G0VwwiYgprTnfS>)oEsjrC!8k#Dgf?)o{5$eSY~#$< zH2Im``d^m{S=#4e*`HC8KkTrCg*Qv)JO+n$YTDM3x5Y0}svXHWn%0;of;Kw2@hwEG zqAA!{`)^P9*fH<3;W4-?A=S*Gkfq7P2-ZRy@<>S!M_tPJz+M9|q3Fa1##^R*qf5C4 zM}@8d_pgUu9ZbIkDw2UMXv?4uDK?%QSwfW~h zq}OvjEpV88$V8S73n=_RIrz-o8Y~BUaS;?-v$>Wh&d;lN5`cwX@O`~$h4~ZWPs?)d zTfQ`X?Upb~;LwAQIHucSc>R&p87*58>$$~H`BUcb5n*N$BD}&dcRs6rS`PV2*T4W` z86iTEu*sLfM~|pPDC#Rg9?C>~KTK>Gxd51Y4f=(IFNfkqyPm&|;lz%DN%$XWwMv36 zh9>J_oANp-Aj3Ed(#H1u8Gb_^ILAPcisX)F^E8jxYP`zzc}`dbuR2+L(lDXJrW?a= zqFqTt`g+62?zfbqNJQveQ5A^Lz%}9)7&216_6jd?o0f)w_PlG_QZwsO}0iV zQDscF67wvEVPfByYj ze+cC!17`S2uw3QkZ9%UOv~Z$FY#Y@Zm?~c(-C}aB2Bp*j_4%ErQOBfLl6eVS!0G_b zHUx&}Ii2#>)^r-*`v6!**^L53`4gYrb9Gq)$Rn|?B%Nuxk>K_qBGC=#7U20$LtsyCHfheJ_SIxD`nP(5E~!k>%%IW*Ix9iJko%%)}- zi95r7A>x+^AwsA)&U+=phL0>Ya$OS|UYk88^*)C=>ibGQ@9m~ojbYf&j#~!T%(~?E zp**x)UKgZ8_0jN{gxIWt@$PHKWkd#^xP}{aa-g0O9Cys_VhjNa!Jx>3x|jw5SE|z# zG>C89IIk14!7m_Wpu?ExcY|%Q#=ngurP=m|+nFKPl5*86V$Gkz@G4rIx)v+^`@2JA zt%?NWgnQ15rf|Y7TCU=uyLl)~dkY$e>FmgZQT~Vw8`tRet+&m}d?WRT{0!Gjl=FX| z^zPEmr3wAgaMjx|8Cs3=q^IXg7465lY*LZG-_l)o<34Qv5r-*OfR!jDv~{(FRWvoF zWHmd@ZPSG*BEcK~{$zc5dFkaZF(UtDD_6c{(f$4AkSHTEuUMcG4#)*hcr?35a_c!JiwKLNm+QU$0u47+v_sPv;^hlS?ZapfU2=UTa*YpR3rA>l+}oEpI&d8L&$nHegCgpG~o5Bv5S zRMFv3e>eYm`3O^u>yAC4NdWE;#bkXc9BI(`r!tHn)ZhHZJLRZCz;D#wcEFg)f1ZTy zqd$KB?-N{iJjBjt^K3m`I8tAXt$&oczN8+MzEt8I@B;m4MkmXS@5`D#L_EvF`Wiw`VRk3E{20K+|I;t{vtSk zdA}F)-@ZcC-}vrs;YN91u0txaq*i_)l6bFVZebU+$b0RC;5PZiUqpg{)HWA3*-jrh^)Z*e951OKG-^rcO0R}j(8BO$jp z-@YR~3UVuozv%c`Pr@T<&7V7^4fs;4f-tDd%MnJ`r*_VJU7v?t;t9`@Ge>Wf?5-TA zR_xFa?a&^weOGPOkf5Pu(!FF~j}@g8L7A2qW{S@Z7#i=wWo z{iH}-F^`U=D|VlU8_yXZGU4bPePCmr=|5tZQGHPQ{E`hsI{!C!2@V1P68~W|0ldI} zpHO|sf$`h_aPj|BBK}v8aG?ABw{J+F#Xg|^rAQwJE;R z&;UEcIm*cU<0WA75z#h`1@cja!m0r3DB{x}$V4ElUK+}8&fZxvi?(I|1?VtBn4%wm zu1sM8Uu}wM3Iv@OcHJ(WSlb(nIF{K5p>F*l0|jwv{^9*Bbf3QC_Fk{|tWyGQ`tX}( zl}a?kaAoT6*}eeT@Ffuchv7y4BkliZdHdgP+WbFDG^|g;)O1e(NAR6`f!;Ng4g~23 z#gXV&;Os&kI=k))#_GEg-cZACfOU`i3bVjIeCtE&wcDe{ikZw%6+qUBYPem zBi@$%yZ#XRt+VO47Ps$R-eRf9%eS>@uFrjML!GlmO|I>zig7H+}!Dz23Xd@5f%HAn1=LIFSvHYzDfLeAIZaa}vGZ*%#A$FkW-+Zs_}$u=SYsu?zL;eQkpPtcmhpU6di=i-3K#ePC)jZPrqgLlii$$QCm{Iy z=g&qciO;{g?>Y=PczAfgn}#6}D=X_0Tlzx%#P<>Yw}**W%adbBt7j%zm)~u>K2N4-Ndg@F@{`~pVW81uK&$*n{2RfRMkB{%|@_t&* zicN9q^m=dv?!!cWeZA|hH~x#x!oqUy#@l7opxb0Jf(`tl{`&Q@GbcU$Z;@o8+h#Bh z%JRd%YM`QBPp9p|9#I9rZKNzLi*+WEb{e|6#6WeAS6l1*9_bf;Rcq>%bfpaGX|B7P znwo}A$Xn5>RZ)OD_R~RMFAVP&$~TC{d1>9uK>zux)UmqZsnM z`4Cd>?yVoF<@sNnyYDQTSFi-!x5BABv|S&s4~DM~W;{F3F1Ckg_j7%H_@x$9-P~Fn zN9ddHeY$U#t*|o3_MW^CXUm51%(A8o+F8drh10n~zb;puCTRSA=;&0=E-W;2g`Ra> z#eAU7padbWLa%%M4k&#F zO8_8tD9A(d0!@9jH^x&mq~7h*`GGpu=dsLgDVG1W6k>R5Ad;s7>{$8u>jrda<^KG0 z*SpyZThrG^b7XW>8NvkIVu~J&z|i}PoArvhRzoKMTV71Z zvksp4xVYRQq@C=)v9V1~P6}9C)0vo<1cij46B4dgdw-1(vap~bB_$0C3UY3@RZ+o$ z03@Yz*;P~&nmaotEG*0?xmFa&f*l!LQ^N#dX=UZWN!UL)_(4HKBV4TTK%wi`udkS* zz1HkXdU|KTO_5v;D<`w_^XOPuq9!Ik&%E3^b-^BTB^a?oflJoK`T3BUVH4tkE_7tKpso(Rd@f?i8m8yK z`Efk>ud!vl&q(9p5tpTRcH2|tzY5;h>C3KYF><+*`mz0z#^Pei!^6X{@NmJBnTN(G z@w(B`QNQZ$APDs?FD}N8^AEs%4U8C+GPwV?;HJ#H4+G{dcUR|doJXv@CHuG)5)u== z`9qgo!QMkzTu!Cc)n_i9-LE&*bm}#EdP4~C@T3e3D3z+x+S=Ng1eMRvUVEeIeanqj zPYv?XV!}XbDo1|1>lwg)Ee zWSTH`UZXR1-IF=(jc%4#caib(uHwkhX}83|l6b}a`6KaO0C0op){3~XF@>U{V#m$A z7VpdTjCrGGgC>0@yUo_JRrSxVF5<`mA#H8K-Q8VdW8)h4D^mgFNkCQAJKUH_e4m*= z@m-?=eZA_wpY3%$B@VF)oT$4p|ve%O{2o zj_nN3);mYjSxd(Dr-8hLgrjq6dftDOP@5MnyU5AOy>pZ8x?fVboc0 zO0%8a)$-w7`NREoETlS+0&w%NCiY#a$9T!Ib=m>sdMEGW^F0sdGeFyLB!#|0voQjI zC`>;(Bv09wAF6|@M7~5Y@x|}gk<9b7)(MhZ@Ipef*Ymc6vpSipBhj0G$nw+ zT|fGz($%a}Q)G)8iYVDfi{i?H;Q^rm=+D8y!Oq@Zlg8^wGdLk9M@~}muU`@^2S@7l z^)-q{CQ5&Y`xST5IJ6rusM-2_IpW>Dzq=csle13gq-?k9^K=62JxRa5cCM?hhveMP za!TWLAS=ms!H1AeW%$C!mp#G%mIb_iH&D|vGu{@UnVA`P;2P+3E+ot2tbsOm#5<6TjNNLFhA=1#81J; zN7TNyUVhX4ct{I;O~c5@-~Z*P6X?k#phN)Mcsg93PMV!&WI2!p%3=uVD=#Xw_5ok# zV-6HC)bu0bt9`m3)1->Ba$Iuq9O;0$q$IqCh6Wo4$0vzae0=;r!@~u@ANXmx4MfWW zOx)Latu)zKFV^(lc^Vm!16jSh+yQVWKbo(EIv;DV0}L@xs<#tmQgU*9a`M2JRBa(u zRcxwkXLKNQSLW}E3cB}ZHd*7Lga%GePooT3SXz?N(ni+RvD_Xn&5$b8t(+K}n4l36 ztsQ)`IV;Kaxg;7oaK1g@BcZ=NU6Tf=5yzIO9i6Q-$5vES5YOm6DJd$V0o1dLXV+RB zw*6kB0oM!lb+%gYA3eo6ARlO@%b%oDxd9=9QO$*l%VCN7gVPR(g#UaOLqeM*9O(z~ zTqx87t@lZ_cYCGqA_d3?L*n0{F;n4cI|3X{I&5q9Ogp5&?t>c-Yjy(4#4_+xoo1zo zs3=ICn3!13+1a@}77q>0f63a`iP*@<$omP(tGYRBAubLT$PW_;nKxFKaPqUw~scF*# zmo^0z)sqiOFaTA_v$L}dtmmuteaNFZeC;DAjNQ+UdYa2l6MU(ec3rItL%J>Yq-AAg zZ0zi}mM_&^j}f!8$}VT?V@mRT1Az0yty5iF8xN#mGiWa!9O3ABF#}{l)gwy_-p|)F z@-AR(2PFEUItck^%g#9dohkyuk*_ zQ&v+;%*a^Ya{`<+NlVMGw$1Hh?C;3CpW|uUdJC{ED4 zvbO2ftfs6i0(=4hoJC1>vvxgT=I#`*X3PL_dT(gxGbg~B{>>_|g`4R9uV26ZjE@(W z!xRHf-@Cx=-aXo}Tr+vBMk&)UZYhyDr*NluC^MzNh=6%Oj zprWVFy2w4QW13Z0`Lk=_`gi%fj3=->@{{GZH!z4(5#Grv8^gJbaLwYvYx zGU^%+0F@$lTU%Qa($YsPCgzTgEP#mV*z!B>NPxSCqVW3SNQFJTtux2OG&MD=XJW-f zMB-9XLYAyeC-VelJdZz?{;#zFS)8>qH-LGzu&^NdG2SF0EBl3wtzlvJ_~^TkurT0l zl2cF=jhqa{l1yy$2DYy?FIiJ^ai#0DyP#oUKnZAS;%{zl7XAE*1}y6^ZC_ke2lRjP z=;l3zgyhVV{`>cwhK2@|A^h)NZm|}LRI(W&fP(?JZ$J*BB(@>(jNAQozQQR9prR#Z z*^uCGR;}Zq)BmOl+v)!^w5ALwu4hYvawNc}<-|5YLIw)tYR=WaxTySX*VEEmzzsG$ zbd1#0J(i7^zy(*3_AfUJthTEy!aqVr7`wAD8e?Eq-(e5VoL} z&ke8Gb+4J~(XXVWq&&`pn`{UmV!uC911^u(&74}-OLG$dnZO4lsf@52qpTA<@7AE{ zxKm?a;^Jb61SoRFSo`SktS?vB>p~RBh>VP^Ua6Ynbtwt9qR*{-HD5Xd{QX7K2cR2U z0B!MTjLo!d3!f?!zbhw%ReDlB&VG6Kxhsk6@pY$MfiMM#8C=y*Z#>E>D+dmoKUlUc zwY9YYEGRB6mK&MWk}FAEUS8HMxj#V`Rb1|PyCnp+Ls_VD3mXWVwan;=e=q_KGC!%Yg*^BQXGxlz1H|s~Bd(4|XQsZcy@$V0;@W_Lk`F@$ znIX;P;ixNWoIj=j1enS%=N34G0mi)OLT&KZQ>1q7S==v;6A}`*>6+E)fQTtlFc1dP z^6S!i@#IN)Lj#+kksE5HD1E>a(kW)MJpp9H z?0B&jiI{h>c|xP!^gjZA?hQDu^`>J5`T73aF+3c?se2$1V_x}NMV-i z&CZ@V*8DaP7}>OMRw;~p$Kg~tVZHkQ{wo6UadABr8uf4}D7W*A=1lB=Hc%I+JKFO|h~bW=rLvZy9aoiG%=LX!h<&{`@PdDwUq ze}o~3kVE7pA|^~_Xb3tLt(!XXbE&eZ0jj8Wo(W9@CPMI!lxag(sf8lhnZ4K7jZB#Y zQ*i3tk^7^7?J3{>&U&V&<0;p^Q{#So*>?4m`Q-XKO1jKmQzrW9R;5R;^7`#8vhRH# ziF5V#*4C=_53(U9B*q7x)H(Cld2-U!5#-m7&D~TzF(8Itx&y9(M-B=Nbq!B5Dpx62 zCFSKML3eu^ zZ*6anRlUtB$>-~J`{<P00d~^ zK=1$*yS*LDKlpM7wmUx`y1aA*QjGdSklRdH&Bps}n%9awUlzJwv9<@J`+z~3FrYrO zb94E?9q(>{fo)HsrJ;d$T?3__-T)FB0)%UT_feqLYzyNKTMk4}+6##P7zo&{fuO3T z&!DIsbQFR`tJ=~a133P_OBD-cy55h%c`p+f9bg!n!2mqI&Uy0uv6Xmk8U_ZrJ8=<_ z)6HuN=a`>g$PjEFo#5$oBTIVLSXE|(9j-WOFRUk7_ZmE z0)Fak;&Mc&1MZM}Ofvz*-XX+1vWQ66z|T))*A!0f^@gNmmncf4cAbhao+%h;@%vnS zA0u%R#T&|Y#jopPve(&DjWOdEv5H$#+W!=r0@uyaCojEfz z6HizjnNKZ(lPpe)IK@co&9E>e!-rLaQ(bnYpZKr68S8k33>X!?LtRirggz-Lsc5od zr9@SaJOD_`P{Zi^h-#a@eficmEqC6;%&X))IB0$%*14sHg@uKRr6JO>oB!f%_*Mf7Wl^!gaXrmztWZtAvcKthmV=($!lwN=y{@Vq%_Rj94UV#(iyV z4FOLwJKoi1c38|Bgx_gn<0t`@5T%_g$}i46O--d*TUjWSJ#6dgDShQ&oapPE`8c8c z{QPV7_O)-WlandM*5a=7-~FI-;R1Ea8Oxba6I?ie@bT{S!^S zy|?|=mXit!*zx3tThcj3Mn?g^t`wYZ`}XabW?Ff zt@quh%gxQj8P!4lfba@hUiJb}^z>As0x6)iRTrQ$857HH^B?O`*EgN+?cFOiLkEia z>|aJ#ns6G;cpqh2{vO0=@UR!PmhkLIOT(`@Z+J1}R;S**1=AozVnVTF+dChlZ%5pF z-o2?8t@1CC_%jTOzh;f+<>gV3-hxIOKDQ1e{gR|~&DM4+g))=dSi1m;={AqFnfoPL zTH3d(OMik-C#%uA_j;z;EfK%nxIxh~YTH#nOS%CzMwyAi^us^9j$$^QOx?!HxIR@sTi?U@Gi|5nm~RU)Pb*=xVWqeB9?y-PpYU#1&QU( zci5Wx*a#N`D%>b4I)Y>OW8iKg2^>0fHY_}xb)tBPug6PqeKDD&g!0enrAJa^%Si4bBP`#svG=1M8 z9P9mY&)RD^u47VPynKON)!#?Bww{HV`HdPCGjm66xxm%1usu(vm$+0_uZVpBl39G3 zx&PF|!?fU))a>j;N&He$g$;=>n4VSCwEP?!8w01plX@!Au6dc4m-m5;>ppNdGP9#? zjIXXX7I2NLD`w+X7QbE%Bf_w4X> zV$*&5=pH|Q92OO&e&WQf4~NrE#)m8}ddyDtZO4U#XzYCZ+Oqk8>01gzuCu9HRbk9K zzI1flOi~Fo18A^N%$9tNao(da)|4EoQd;x!T5g23>J zb{P~okj>4_iTb9#i92-l$(a{S+qT`TsZk3WGCeYYF>~ic3a7Z)jZa;~JbLzwf>lWS zb+O~m2=GgfWFaf!DwtcG z9tgX2>!`EyAxxrDxyeDq!{;wvTvruTK6-Q$Lhki&4zZ`0(8=miH6Y80SP5=L0FPjL zdfH+^6Om)z{{7MS?hP(K9#97BTI4Y;h+{cU|2VdXjV@G0b;LjWX>#&uP`qEio~irk z^r~_5>GYPmhK6?-9lS5^S%U^%U-i^_a3D#OX82twW3qi;g?2Ef=5Kt=lTb~9mDI&`X;#H2BqW56m}o6JPh;Lz$c*4C!8vLoxrGpXY00S>TrQR% zfxHzd>^5tY#Gqy;(clq@?hoA+7Z8dDc;12-OrFoUo3<|h?B5Pu-v>p+SPFcY0%-Kf0k4$enQ?zY;eZ9`<(_|zCsc-iT8v75F zr6!QSy@mYq`n6Q1o)lBGgoucUUcS{OQeoS86 z#0LjV0~_j@w{8voa93!DwDdNNIG2l^;^N{&8~`-7`y+`J^ErX60qW&gC^dKAHC>$Q z2V`3h=-XId(A^P#OjXt7?G4&H4L=F!)t-A<93_AuoA6E5Koq3i9s`jN6ntf{dx>h* zty{OSDXYV1W}#WuIw6PJ z$~DO@GTCHqq-FCVi;r(mO!F?wrQ%`DxOUztTh5k0uE>_-*ZV7G^fJ;E$2xmRVSfHr z{9sB_xh9@SIf|^SD#-jWK0eod(g;(ncveQUV(+zgQze%@lt7o^qdC;_x)<#%2Rh`D zcA}_U>6sk!{rbUSOBN~y9-8k~MJ>O6SrC*Bm8Efs3$3T8r-qi6Dg?Pyt++r`+e#^C zekXXJK7T$Kqr~*%1Mwq{KPXbQAK$XhDo)juUKsD9#$&W>PW>J87y&3aIQYS#tG7G0 zx4TGmL+(m*?6NIX7A`1fEd)HFX5e{)KQ^Ub5R;1e0&I7}VRg~5+oky=p3bYHiKRb7 zjz8Yhc0-Lbqf}M04;wk$caka(?q(T3TBCotAjO zyq;}#xQUz)K!{lob(IMDrT=#=HJOID8XGm;5BkZI(ksR?c4Ly#D=Ji49@Jnoai${8?t5FCJa^tvdeD* z$nd5rCI(ml7;|nOzq^~L#fUvn)ki;i6oG=nkn5Df5>_;fFMTHOyX?|4kMzN*=qH!p z`D@$rbNhN^5Fm*?z<@JxbBBvxu6ZClRDXZJK(xk|m>#duOime>=PlRNt0q3nPQC{o z8M_tmuObM>I#eh9ss9xBMoRUDL1%TD?ClxBH=sIg1_J`jO+2)raRI%WC2RY8zr_~P zN^aHhezTHbqoZWtlcOm%m}sf<%u3ZwcbpwQt+A8kZyGh!tf=az3iI<&d9%SKkY(jE z%#FaTcALktO%if}%F&~N%5}v6#&lo>i7Y~r8Udi+3{7a8%*VCz0p@_Z*0?5 zKu@sGRE&IE?w<(8#?U;-%Dv`@!0NZL(zioFflno53mIw4wu2NzDQsZyNYo&Eehyku zj$Mz80pb_%sa}qWa;%e;3akId+QKzEyPCxHSs? znr%FEHYq$mt8(4Tz<+Iys=-D&ab`CpBR_O(JIC%iubx=oyu!Ftp0 z66C!P_^+ydIqJ~k+9rLT;i~7#vi)9v``F03Sa5U$ z*QxmkQw0;Cl!EWFN2!Kr^S5s|-o8DG9K^Bje*LJFdEH?J+Mb34j~}$q5x9iCi4asp zy}kYZXTbC@1f9+@JQHQ`6mH|4=B_=+#{3YcY>s?Co2D%QX=>N5U6MnS&k0=-4v&nE zg+%WOX`h)7LR2fsSy>FwxTuoc<{*n>H{LYu#|P^!|NffdG4$B|PC|m+K`yK1#a{w`=}U zBPqw?6Bkj7P54A?OC-Nt|LM~)T8cNabW+;y?@z}O>JjX`5XVwA%U zD1Z-3NOY9-K+6q5dW4jfl$`u7a+k}aGcQ!HUQH_uS7xWE$D^YGV|MjP2##QUnf#{?_sB-N`E-cb(3e4K-zEKJ9RD!NpN?k~P}< zqSp~iP0h^;8>`L`$eB>L6c=v;e7N)BibbKc`vgycUC*t80?F0ir~IANqxaGrX(2Z= zh+AytVG6&5!+_Et3?q=1Cwo>u&j6 zgCgvABW-^f(+rz3Q<)(};y2VJ2#~95lm3P6teXtzdGcIGb=4vTqa3_wVk(bS&PZU2 z9Sq~4BoT5=kHEB~eQyNn&G@!hjh!eD-y)l2X34saoP%ZuCBABK<@oXAcq31C(}Nue zkYwbh-jG}LH6p-iX>CFHa$O;6ZftRoTj1JYT{K>O4W?@S1-<^?^~K%9HKds+u2AqUrP zXKU;9o+Zvh3e-HV@93r4&-;D18Z3n`BkhmduCuOWqts4?7_Uk%&cXTfH6>|k?Kt)j z)jM(K%s%m{9Oq)FznaXxlwW_vC5b@JcZE(#mi7S7P5o(9+yQ!^Q1R{Cw|9Wly1Na^ z5Q8v{wom;C7Za3~<;GR2U%Q4%;tGvp0xUQA*~5uTkLO~?XP4it{U{8xjy=14{$bCOXqu3TT7{oowRAao`@RLEJHEh#KRE`9ox!Y(@$8KwDd&GD7Fo>BX-Sl!p z9$v(6Xv+k!EAm;&YnhXkk#T4F0iiZ@I{DGHC+o_+iB9vUGm!;q38sY~GC=;;1`>_$)k z!>uy>wpGRqwyO3b$K;L$#nmYlWR*`7E{afy2nBu68zjpP!WBRy1ds9jHizLa0@zUr zj`bJ_PpCD>F_ZDUmDJRzK(U=nEktSwQv13;yDYHWd*LZ+cw9#b6&H!9)rm_I6DwRf zwDbyua|aJs+r?1E?P_XjAyq*Fned5o=w@E}S#zdiowp@6I{Lcr%6xI|IaRhjpA!|M zpoYeKadsce)ZGDmuO7u`P99)m*2W|}@500!y(`sDRs4)#^vb#O=LK+CmFvs<71n0Y zX5UMC@q$+9RKk03dP!gi`1mI7JKfpH@n#5{X8jVG8z>-1r(L$r>RF4CXmBu_zDVX- zepwk03gd4g`*F20a=J-6?XxCF^Djj{e8@EmMfP`|uCDGS+cC^MC1Lht_sL%EA@!`w z?&PZaRXI-u1O=arsyZvI1l1|NXt9!8UmPGzE)c?l$jGUl;Mg!0`X6u`)7!!;^X<1B zA=x5FpknLD`Yv6A;Gc`y!@zqy-$E!s3YidBj!^qR{BlSmlamvFY=ijK&pd!!_=goex+jDZ++AD{NsbCjKC%-Al>PpO zOzYv{55MygWZfBn5^{^QeSOP6HNCELCKzt-qH0)?M%V2{Zbga|*!S6;9X0?IL_u|F z%&Uw@*(ku`1qJE(PA@$wSZ%I)&r^Y3}xyRU;Ih6R&tq0e8DY71uoPG&|h zV6dtn%xo!1ngiST$;6+1>((SZe8iO+kX(+RB-)01<53WQPLK~reO#(|S5eku>d9I6 zqA^F{EmJtc+P_L+|{em zU}a^MbGeoQOAq?zT6dL|$(=iQuANQbUSJde{2IPO;L$|mRsgSv^|h6O7ikFxxr1qo zI%lGyq6!xW2!0-%PYHWz%Hi!^!vc?FTqf5Ts_nijprFv3l=bm~ zktZd>i6qL)YtyxhP)K>`j;{d!eus+j9wRcxW7>@1sz_6n8!MFr@;Cqi1lIx-k>t9WOZY*a)S?qF5x>P zfE8S-rY$ee6C5EXaBHSMH7P720tiBgFscrK<{*4*e-pn^87C19*u{(@#=8m$7*hj3 z7y0f-gYdyxe_?Wd51ApfI-EnFUZkVk+a0yDieU8c{4|(3VLN9$r}ouHEb1Sw9P|Ow z#P{XI;QBM~Yi!*C~ko={~{NR_09Qrq1(%0W+`+*!(<207J zBDlHuo-Q{Rm+u5Rf;+88=irRHau>-lmaynvy0bm_zq|luVTKpdirvO9@yL69OIa#z zo*(a;22EZhvIY)!6JV^2(vTOhp^x9i?ndSd51$rP>zZplVyoWky2_ zYQ5(#Ummr!6`|X_*|s<9`Ewe`)F3_FG%xEgQSiulFoiPk(mjZGnl%=EO#Za;W7mBq zLU}5-u1Fi*BzYigthuxE7Tj4K<%URo79Ss6?;F*Wd<>#(Yc>LYr{mcsYoX6Ti`t<2}F{1W+`EG`mobw-iwL(G{1v+vWhYMffeY@LuFsG%U3WHIQWsGKOdkN z8S~;5Jcc5d-z-pZ05JGjZcZU@U-HeWg)#ZM>AR35eCE-%>;)am54uFHn>IdgvExQq zSfC-Y8cYCRpdpbqe2d~sU0qzBl$3DEd(DEsyV3Q>nQ~lZ0*q?8AzK=7$8QV?O5}F5Qx$UV*`hol|aidmyZ>8$OK4APQnqE>{N~wML zfXhU;bnK0ie2GuE{P>F53c_b8dh@;cXAEzIUIF&-rq0fnr6l|AqNYL0{X#z!XfS0y}q7U`9U? zrMF#oX(1|%_RCQn`KM+V^2ORqT+>f(O?^ec5aLRX2HstHU06tv-NbBJpG{Mg-kR5K z1d^k@(0)6(_ZwiFJcW(bLFx7bv@!zh6EyVrk0TjK&cKVI=l0Q;Q1_;!rCmmfA_{b{ zg&(sn=;?)F^b7`lJ^#$qV7rd9<(ojw}A+PNc2E)c>{?br&+I((n9 z{M8E&vDa>Y9UcWAPT&g_c=kvpO;3+`z_>ELKPOs5zrX}>*`m+5$RX*&z2%4zEh-hi#J9l1g_KQmUYfDy`10v84 z^aC;hk|Yf+Z50s(5ni5xaD=f@E&(N-Ox2VP5<45sayd~UPmDe(zZD)n-K4%9Ah&9y1hvZ7 zV-(%5t}>!x8)?mpo^%>%EuKF1NM7K=yK*>N9{O$oY>0Y0oB<<85T&rsF_Q>M4rCZZ}J zd{6|A*V%WU7}t<&a${63QG2d+K(F7981naTyZj*Kzq(KxraP13!1h>@Z2{}Y0 zN90me99r>`^av{s1?_`EL@KO93PaJ~D@u2o!-s(eG69E1${FHM?1?MMP1kZjV`5Iq z-V0Srz7Cn~Iyp6ucF<}w=$asm)RM9b59_wBUY#LW(}ip*ML zf0ThEU%ciHNGBkov{P(P!mZI4dXS!Ke0;pFv9VBw%rHGAg%V|tw6ruS0M;i{Fp(B* z7t&jxQ&)rR-@@m0Z8RzmesiMEgOFk|6hRPM5E^tF%#NFy3Scm~D9gV_vPGqg%eBMA@T7k=ioqRw^%E*76sJdDoHK*EqD^VVOi^X4STQ;} ziZEZwBR1d)cl&`Wbxdh9FJHV+hPH>&=hLsVy1F4K5P}-VJ~y7qGIu9`Z$SR8PtdG* z)UpuVJ3I89l}W>xwv$plR}Q?$fAZwb@571UMhV-fMPH>uf8d9o)~y+iGWHj~R33o% z{&i#>?r+anb4Ix-5}(I+GEzwGC@go>4icRYTA|=&#=IyhDr(4s5xIhA0TSC_#{vo*8%&_dKUa6Z1V1cRY8fGft z8UVtm2CSThtKiaVQy*qD`XQK5Fjs>^=GoVW;pT{AJhTk}>GYc~V_1V+HM!{tQVC%i zbaHl{e!E$0xHmmJyB3?mRT;#*L#_RBLAoIv6VMX~_Vw*HX$(xAYkmZHR zDD#FG*QHP3SiXSeL*ZwNX<2f$cJU!xZiL(8$14f+?VQHa1s5)a%8WFhQmh6r+mhgy zHHq6=i>u36|0US>6Q-TDoRIZzD-h_?R-9wO^Iu+FoQ9mnF0}38Opkk?vS@7l+D!2Y zAbrFUW)_wo1sdC8#f(D?MBP^y58OXNS!{5do?DVsiyB(?d zyvM{#zxE*Y;5%FIok;nxaR0@_f1jCN%^m zB+uUkM9TrT-nVZbzTDxIS^wK$27kAa`rb($pi#U58Ozk|v4nN2l4z`~K`N>b>^b=J zr}QH#%8*;P=vJZLmbB3YZ-V#NV#hXicJ?25UM1XI_yh$H*1_(5?3FJ#hCvyS-c|^5 z@NT&~YIOEyR0K+B^y;uqcUn*-9_lkivcPnj%GrUTePYqJpR1Z>5N=UG{5%F1L!nTTNvEr1vE zOXI|eK(L=UK^0>l2oBt{r1Em!p~l2o1Xtdb?^pgQ?@K}wCEm!&`aC9sD4{LE2gWZt zHumbDFWhu|Fb3Cx&=>cfw+S*mTZ3YH3b%UziF-e0N&mcnJ5GeqRQv74S#if7s-*=Y z!osRB{O{I!@I60NupmFbA5Y^se5-^|VN~wb@!HVbd{@QBy7yR5DF;HVUZEW?+#wUc zzLvD1fSs=2pZx?Y}Rh34EYq_;-^`Jw9%8mxFnV4trU@KdKBuDA3RE^ zG6KP)!(9gZlf~3J4E4cCsskXzTs7DK%>bP}%Zkr+8`Nvz;D9z^qPV?X@%HfWwaUqU z{uv?B_#4`2HA`p)p$m%!?E4RSE_n^sF=Ixz*)HwBe**0IWr&1+vh2nwXG|f$tlFv! znu>Zdg!$RiRKDMk@L6;K?G1SRenNB#2j01JC+Ce&*HADA1}HFL$WR8N?)fM5uPr06 z>uVd!O;rsI4JFiDWF*+NBxz{l@7xK1`ny9?vNg-adw%RDj7+a;brky`j}so9PoF*! zbq@&BrnFPrZv+Pul<{{4Q>4TRPDY+1U#_3hifLcrMP-d6=WtRLgZ;rPhaxScbK-@J zf#gysdS$>-@+&I-^{fC5$GU1imI+3BIH#*S;`e^&*RP{sY%??IaYD(HyC*R@-P|Oh zSV891%1YKvKMkD}PZC-cZWUo_D_UP3Eq$~A``(-T`!8T(1R(8Ux)N0cE_vi&)YF_C zCQuJV1&<^QG=oq|0ZSi|9$Q*WQLzB!UbC@LJ$H_+<~FbOsNL%iIFw8DGomO+Ok@NR ziF*lnko4pU31CVE$~NIAz)0^|&`ECkj$(v~f(#}XN$%ysq9RV{VmdF*;UDN2HIT6o zC!Uq!C^b>rqi}POVI2o*whcN1LgF>yD}??@ z7kqTVq#SSb!}dUMq54B%e2RWN=vmMur!NXTnxtfC$V(tIlw*bX78qb9uER8dFK|>m z)1Tp&rDY)ea9h?_Z&u!q4GZgTJntF={NT5~bd@Xsh@0ZYxqSd@%EIiMF{oJxn@>N| z2M39U^!KfuECLb|=K(sOfx_Rlh3AM4ZrM={jW9d;wMvvNgfr$Xc#>dbI}H@)EQC`I z2fRCKy|EKq@%ivQ8qCq8~Gg5YEb&qK*-e%I=RdnGGE$_+eLQvi=mB-o2+ z;l|*YpX?Jut)xd7tVMlE#QBsH2tf*776Ft#qxG)1PloGYwwWf%7mWI03ox7?2YnEX zZn#s6h*6ZmM{y(h$NDObIpw`x<3fT@#!J$|Y4jGja|bZ1^6tB|X1RJysCL_+0W*PT zq*P|QfwG8pq1!o%KU(7kVakgYHDrMU5bYkpwS3+mP5N$Dn;CM!RbGUODHzN=x2gr>^fqSlHJ&#uSFaEb4~E#Pdj!!B6OG}756S>^ z;Qj!8bhva}cCsu8RAbNMcPJkSJLygqSK8c+#Kyk$d8uxnKc99t`{1*?GM@grQC8qH zAIj%N1A$l-jle`JC+Q3C25gDM@epQ8=Emiv>bwhVItY6YqJx`fW4FW`7R{k2B6bx^ z7Fe;;e>ATQkpfq;xKOzebmoF~XYw;O9ykp5AoL#gcP!^IBQ{Qd?{5Q|Q{T-Ie?VY^NHH>*DClBVtBI z#z%gh%hCVacs1AENS%n)jqh-FL;gjif33IH&+wM4bC-Wm(yyB)_1#TU+-Zu=vP{61Ar5 z7%dIrmI!2-FFe2cbg;B19 zPL(fb+hIi@kkCP^*n<9qyLT_2Jr&7MV=+Ws@cOl$yeP{}3N+rb58dQb@M*U#R~zgD zo+Bi{^gcsnrY*lh;^KvV9cNew7&d;B|1K{g)?0=D!a>+s6~}W-owI{FP)ammb2M9% zU}wwi%mK$(0~RDL-ep|ml-x80x^Zv7wB1oYj;3re+{k`}9KyZn($JQvA8OZAA|NHz zmYSrFJXKvfSrHCuy{8!~7X6C1Co~|?OoRzl#Zt#%pO{!}mSHJT(09%D%t>Gf{M^ss zq>JkrJQ>KZl%%6qmYk`$rCAR;_Q}(%)n?-7_T6t*lz|EMFIfYE2@gE?VFKJ1U}a8L z0Q-DY7;E&9Kg;|GeOFr)yg`|ul|oreMPHvAG9@?=)Xxm)M`ql zVj1dcz3^&&_3MOYu3pBd<5%Nak7763XpJjQ_|MwUU&EF5FAM_thhqxC&|#`0FdxPR z;<4J)pWW#Gw*7}_v<3tzy9QAg+RtQ;*I}Z8r}t^Y4BBSEpX2=H4dhRSx5>EdLE}V+ zxhEVcC;K@+-3$+R9#+=uY$PKkt3@8(7Zs$dmZ1931Jueumu{y^yY z=9@%V014OV%a?R;>>Y@2gr4;AwlAR-xULv){OO$OP`}MX3%CF$yXo+kBvQalRw1IN zf>0T7XiKJvGb?;c6%aiLqL3=-asEd`a^YMThE733NA2n!ryDCPliO#rd)@No%R`0fXs zetOb>BR-OO|LdV^s09!Sn7O!K#y{f2%|IYVXD+BhEsiGw!ophswgdU`>lTQ_q$ewt za9#q+fwgw2CBPzPI`Am)76B zb$&j%{3Sd{`LNH(P{Bls@LP_%BVY3vl75g=^8HuDxDc>pRvnB*#8cHSk?uuGrtVpOf1-N=!>%Qi*26!?KK(i zxD6Fsh#H@IWZm1|rf}AtX7COO-8Vn1oyv*-j`+1z{F-*$?!6!#73@8AWRd;Ugt76F znwqxDrv`2JowOeuomd$3@9idHG}oqn4Bx;CJ8$D+sRgV7D8nJe2O zYE!&NLmhJb+R9q~{ES>BUo-d;7*!p996BFg8@!$jwUqQix!E+Q%{<&mF2L)>G-(K&@ z!JO%_)*KzqZlH^RWj!b1iAfa|O*BVb$&S=D#r=$ef@bo{*Te39x#`0x#L5CoLxxb@ z_FU>4b?U$Set-2pi8zy!z`s$=$0WGDmJXGgu&KQAdWbx~DxcY;hv8Y)&G`)%JD8X$ z`C3`ogco7HVl?w{7Zl99F-Jj3&QK*d!@qXm)@_rNqpED}dZ%;_m=Kehm=9aL|Bq?) z|M%S`o?P93&*K06bY)lY2t*AVw1kEuE78Cy7KS-{ychT2FRwrMk&$V*f1nRJBfg=d@3&ET1k| z)=IJ7XYAY{oMQXZ=huwey;m#s21~-_S4Z&3?M-}@2jdZ$Dz9i3=E`?tk1wor`8yhnM)XjfRRaPBr&@*O|j-qFxqq+U@Y|gHe z7Y@t?b@$Q;&1VeN?R54{ts%uX)F$MyFfRp~WQt`tK2s2IVhc5iHg5b98<_6pJ>uX~UGBJB@9^G6!(;~XVi8`8}Ca`d`@)bt!i8_jvo_Ldu zRaLfboOo$BL)FuO>UvCdo2uo&x>x^+`~Mvp<^v#lLxri$Ap>FcpFnS%5Yk2>acnxS zs(kM6Apfs~AWpWxe{>*p;a_`AA_p;wju0%CqqW8SY&e)}8t zF5B2JF)y)8D#>NQ!bDK;Ag*kCEeP7az$UK8)3zgzl4#rO>GCAt)s*7KA4R+4rC9I@ z8z0lwzFjo~NV^rk1fNLxD&h@wD95%-GQGP>i9> zgXaufq2uY_G(}z!gzWOqKP)tX-^YKnX8;zk(ZzSZ>K}XbyC}DV!5-_CvJ)E-gl5Uc(?gMQjYwo2tPXk^_DLn>HHb zJ5@zZv^ZqgbwNY`5y6V{?fzqcejl_tmI(J@6|{=wk#x}pk;l{^l{~P<4R|)YrJ#R4 zY4Lj7cVV5FinV!tdyXB?+Yip}gI5?I(4fL+8~BGkA|l7Uy-VNyh7pADAHrAqJU!jn zl=e4_wMSfrnM4hWzMqvkoFVNx!bThgcg3@G4`KBq><|AcAqB+fI;k`W2T>GeX?*3! z6&T{sy^@A?SN<9pIw^MvUY3xM09!xhCO#&}a&xR?`nNX95P;V|>3ln#%AeR`z@K)Q ziiKPDbPi3?9z6`gnusCC9Oa{`gYroA%%_zuUk(bZD|GnxF!bE=pTVyzOQET_D!Ky} zU;KEgeUXH}0bRGqVsgk4e}AHA0srvoGEagg7+~Uv%xl|;2C42Sl^kM-|F5xo_qCw_ zho}D<>qC^b|rCu3!z704VdAW*V)Ct zxQGrS7~RD6{6GK^9gb+OY7^;#6izfuyLJ&)E?5YN7K9IS3zL0O=!15epEGK6CskM>xIpMNr3KrLIPepuE_Wkz zwZHP_KDF)eH6}Q)24h}hiG~lp-Py*9;F}vwIxysIJ0%Yv{`SLBQ%mb`@hS{PQpMe0 zJzh>R@{x95v%&$6_(~^Ec-;h{yiF_AOlRvWqJ=0qK zo%?*u(caTC=u(-ddrnn{)wbyOsSLDX2cuLkRS(|X=(8r1puTaRhdFv-xNr72a%qG0 z%-`9Fjra&)-*{;UC$Iz$$F|-2U&~b4n328h*56Rzv7}NSsSUp4A^Dz%XYt% zar0ku55mGCD9_>9N=ZgsH57uDhnTb`4B)W36&ABKjefaPt|)oPv}Ou{^2ToLkCH z6${OPTBssgiFaTDI`m>zi~d%?5-fk4+l8|;GKfuJgRsmk!x@AznyxwW()ko5h*i{o zJwQNHk@T_sj;F4!+K!@cbS&f&_!J z=$*N1A>Jk|zjmLJ;j6f!LbSxf^H@-n0Z|lsHxosbNjV{74Gunm5lpk>>gPv9<7n(p z)QET=8dJVrFZOu){CV9E8MZ|md1IgZ1=`;Yl!|%kO7dZMFfqfO$!><#8NrKZ`ixajaOk&` zjMgXeE6EtG5&sno+77m~Po?m&_=ndWQ)PQTW);xR^79r4K!88H3CwVS{qQgbwpL$B?n@jY*yx}(r{ZqSUBxePlOd%H z_f;yw!~W~sr9l4gpj^;eAF8pF&;_xIA2g3}co5RN5|21;0+k2)6m>MA8=l)&4Ih`o z(u_G_Ti>C6+vR0Mg)r0`l+)GK)ts_!8Jb@+va)L6-$1+i$W(EvygCYb5Eq1R6?8l` zxD_eAQ%O366pm~`v~R*m0ZkWz4rCGRMF$Hl7IZYc_zz&Z2VggkgQqK*1HY}6}B>|nB z;N%_9;Dikb`!FYB5U2wOh-_iiW`x%r+9?PS!c%*jPackcInRS&(^XdD4c_%rGml)@ zx6HV6r{L4?zw9F!d~svHH+B&nny|k^K$3n8ffjk`4dK?6ohTwFm==P!#-auB1@<;J z?=MHmhrn%^gyuyIQWDX{gvA9oVOrFVZHnE?Mu(1LQ#cYtHD7_H#uWUvR_Cq?Z##xZ0qyq&y}!7Cn%=z z?uJ1tWm#HaI)m_ckGfS>E;pUeGAMq!QhDaZYwIqE>cnahf&84p7xu6j#!x=R$$J=$ zFPz?YUwcw^+>tB*B-5)=NaF66{GgVG3+NdcC-WlxF4uGy>){^4QJ~i?Rq&{Gc@*~o z2TResoP?Or;8ru_sIDGrmpCG&3!=RtYPP=M?s*t)aO_Z z&%Y`BU@iW#{$)QA@|>m8fXtbr6+%NzmYgog0!8qRQX&!rzk^m?bnNdAd9i2Z`Ik4W zrYATM&u10{^djzPAj)Wm_7gqGo05%8l7Udxcy0&&oRss8)f9|Y5-#w4AFnD1&y>?1 zUgxf5I`4(x9q-M=A*`MBMl$!zg8z9^;V+=v%z zf%C_Q6Dk_=fgj`B#(PSoKQnJ_}<;+Dxe2d(v0pP`n zyk4{di=#>H?r*!kik`t4G|qO;dcq-P<@)b{uUgsEKlv^jr#Zx96`=Rf=W{&~S>sC9 zUxgAOPWl_kbQkK60u+IOGn<@fbf0`Yi>M_P5urh3x7D34#?@EvqOb^k5wBP#K!BcZ z?pt-6^_u>|oTaP)o+P^CdHV3LYBw?A1Z>vOKa_J3yqgNL6=xc?r>Dpjdx#k9YAa@3QtTfmI z9ZsEQd}xO)TAS}G74E}ok=PXV$>p}I9%v2#O)K*2_;na}sc|-RGtRr69!q`v$J7cU zO?1VR@=x_T;l%AyP2hM<05ALudj3VmMCnN}{;!g4`OhZlbFpD{@_*~p{|RK*Hs&WR z3L{_Alcq**a+mIP{CZ1wRf9$#$?k*{^xo>RVv6-=d2bEK3abJwt{2hS1;sztag4rm z?X=;>w`renMs6X;N6#V~wd5LioR6$ckggrN`nfFO&>@o2!A(psy~lNGR~>BRAd$YN z+mZx_Nu+>Tt*T8V(m%`rBogTZjS~KHg@T`i?eoiNcuBXNLsbLjSOZ9JDk*qLqZhhJ z@3Rd^q&r5oSku*{Z+5$*A+o`Fk|5TRt*xi&SQzC78 zqb7Ug$PpG79)EhJOSqDvoyShAR`x}yB{SoNs%nfm)=6eN=4iI$A`^)}V$EQ+cnog_S?99G?oLSN!Gp)MLz}%`Zl%B`e{8T>dU^P2qO`pcd*KGLi`H)Sg36CS zR%-vadU$wPbbRWp%bTpMM{moiPmK;o3_23Dy~PB7OifK`7{%Ui$oLr3((ohiA|FSC zqM+HqIG&64a(s;|?}b14RVq%3y?Twy-4gna)z_+(!=iyYNcnL5rzNl04mI8Uw&o10 z9UL7|k79mZT-zmRW^KJc3r&Ppd}8{xC2NvN?PHwTPo|v$c^O)k7d8{qxx4Nim)H&`}hNT}E7#UC9 zYp^sIZ)nIcVE^L;E@6r6=luNqs?wj=PFU<*JWea-*H5{*gAENh1kvZl%q56hAuO;M-+dOwBCMMLf+v3ywu~He)uZVj! z>>@Lbo7`A9hvm&`36NyZDl03iK9kpOWIV6il3wqwlhuvr^rb98le zsn?t5Mk0@Uz;`@0Iav#9$Ph6V;3$yFEiL87t%J}sN_ftbJ8Wt&j4#VPt}!_fM%dgj z-1sVCGsDEfx^@K1aZNIQ8bFeB3zZ`pI&u4_v1-jCdVYzWadK8v!bZ3NG;+83#A6t< zuV}yOh-kVt+i5%abS2u{_6ioAu{2^|(Tp)C`l=sUE4pvvs=g>NOXal8`$XZhEM-GG z=}t(g3E6)FjN}9tQ{g2FlhcJ=5|X5Ib!v~v;y(2~UXvX+d+Yja^QwB{6D#)G5ZC7Q z>*@Pi)~%B_F^ynVW54~fn|1asQ`qpWJv^z?MH>A6-Q;&=o=7jBM{}Wjhi9qH`Sq;x zeZA%{m)$d+WRr~QZ4}K@3~MJ1QcGH1&F3p{yVKWs#*uF!k$$(#Zy`cfubO<=lptgL zrtdbcGWs?)BoE)AZBml^NH-n0jYy>Dd6AomkT~K*Pi(DaI~nM@O>5zVeTS=jT1(LK z#l-NK;u1Z^IUSL`4TW^BWn(1`u4(d`$r&zZQw)FUUC`}JGb}6}D`)$idd=GWo~wCH zWZ;Dv)ExJ-i+vckE6Ba-zM9?AxNPwHV@tKy^H8hf7c0&`9~f8}V6znKve@=JtF)|v zXSDu=_r;R5@>szhHrvdIysWQ9rgZEVLX2(FmF3R`c|Fh08}(}uYl**Saj||iq2a4` zbmE_cC5>pimj$|N@)uq(#syNA<;n1}oomUn-1JR-L_yCYQOugo6c506+Ia7%vA({2 zr+HN0CZ_NA@0I`gv0>r*L}4T?L&jK6=|o)UAGslG-p?x$xhF5Q#$_M;+BaLE8!RbI zA`{J^lNEdZDsTA-`Lx%*8R%Rox8?uozJ}x#jTcj z!1_?v+>ZBxa|449M;aNgaeQ5Ec$hMM#fjHr9Jw8RRE#7XT96}2CAjzj zQ^5HMR)b@o1C)-5tbfg4w@Za_e~Ysdm1 z>Sjnzzzvu$63P+~5RFO5Z)ULIHlmB1o;w5t+VH<0WI8h%F$BclQh6x}Eg$3aJopfj z&+lWwU2Scv2A6D^JIFMJPZ8$D)ddU{6>p9bFsQM*>KGU}T47WQjiFS^5*ScnQ~&@f zYiNX_pK9Gxm*ZXVE+3b-d(L*xHnactoU1RK9a(%_&el6QdC!3+&mu{M7ytyy2=Jdg zwDH-e{~S?9#Pj1&{OhBP05wZM{_hir|9^IZewe>cH;o@8BWLQJ(fAW4bbIaHwS4@R z-oG(qkT<~!rjEeKlmtJUD=goqTLx<8oo+cNW9N%wyA|2(F^qcu^~l8brS4;EPdJ!z z)A&RpCRR4?dOiV-?O+&jzu8_bZQbfutn@x;-9J-o9_RLlMpYDv>zSi{MS%p@WC)a& z_k|FY5$$2LQ>YwdCN&v&lMDJS4)@b7iuhlJLiH3aJCM3AJnXl5`uTgrf2lrtu1+pQ zGXGjw567g;DD)pa{jj~EPexAT@E^TcadNVLw{ZJwXD>i#evyCKE&9&OVV7A&&rbH8 zWVUku(f;7*;w9%$pE(~-tS(WnSBf-GuzWY_NbCdmVkqQE9D%YJ9TtId_~D1XTMi3h z{6mIH8YHU@i??Apr}keMNtej2zq)ovmhaQ}>TU)FmaZq`cko}GrhT9$c|i}!spNi4BU8CN}hQYVpKUS4N%2#y}?&R@NEy7I`H*5)crOYZh(w$iy7aI5Fl>wJhZ4sGdv`zR zX3i>VKQj^s8fkYwQVPyuU69k)WA8feww%2+Ke~UL+M!?Zv;lolKiYzCnKdtoYpLfV zg{V_6&)K>3`GyOu-=$f3cr5Md5ZQCj`XPb=7-sRyP`S|wVO zLVw55Zy+q4p+Tz_&tl_R8<{jkZ6CzF66a5t8Y`wi(Ighyj?Q~u2F9NUF8mNI^Ue=k ztohrKqO=$ozu#-mS>O06@M)b~YIm37s?B5z+EBLJ{a*5-7&ZowU!6ZLzM+33oyTmv z^MVbt!{v}dKA81!AJuMD~n zY63WAa{1+SRIH!AlLpp(W$98Q6BVjLm&Gbs+A%RX=nX{q!LJ>Y2lM@i(cJ@s#Y4t{ zhhFh?$D;7H{_9ETZ)rwfuO|81ARgr3!}~LolF8+htK))e>|K8 z;>CPlHtzdkUNK7x-mU*#y;-bx=6uE8A)=U*Fx`JSs#RrcyTjRVBrl$k@z~P!H=)xl zj733J;=AF|vt+qXW$Slo!0gzNSkCTH-001QS03R2uh?&SFC6!;q~FdO)WYBbCA1Cw zidiK8rrPUFElgos1?-=h^synXnuQ0lAyN+0s)b+x41cibHL1>C24=PVLeg{aHhs*c zZ2J_nWfni3qQ!*k7pmUAA0$5?n$9TtGhww-G-I1&*T|kbLn3ER?ihtl40DuYGr~J(1yuK4|k$ zKm8_?t(_{URtIaT`jh^`sUspubXjr>S>J%#6rL? zSWm!X0ruz{H4+^7-0zmJ{z-xzhKCD1!@&h+>k;9SK-#)Us8Pc>`b z)vBXrQ()h^tQl>vm(bFBw-ak_a9Q%9aept*ugxLd36e1p%wCPR)`|ONO*rf>k3i`p z8CZizX}L6?m%Yqf>7BS7zH(}4e`RrL?iu41k#pBe8t3_EuyphwQKiS4exG1W_kQuT!*b=K*9-4Yc0;01`PYI)tpfWzz@U{P!LLtb7zbG% zB_>5@d?&%A*D@HE@BP>~_*t*qzwQjy-hSc}zu?3tfbtGKu z!$aDoTTi_otu&cHg59cFv~mLiW!5x!PB(ldot>oq`PzGN@IIfgnG+2fDaDuaTr&p4 zFoTJ=|+I!;YnRyla4|fr8LOH!$YjxD20J~_Dz3o*79D!XMrUCLB)Ul zIk7{S^#4Bm|LM8^Uw8v&%I@25z7;G&`L8oH+gsh^)MTo;zVp*fI~z%$_jJqMseoW? zz5@=N6;8?_nfth_qBypNU`(e^=#hoPsXr8b`RiO6h86pCQFO9L&1nwkXxOIMaApas zt!yvc^f;%G41&M2WCvE(fJScHU2}4sV6jtJ8h75E0{T$zca$G{a&7tsoTx3=LLewD z`8ys~3~Eb7%_C7G_u`$~>m4gl(t=L9joV65*HwK28Y!FRboK3P4VKu9VxYJzZVt2| zC>h!3x_C6@9<#A8_|_aFQ1HQTKPugamYzjf9<7X=@+OPtOK6MK_;rXk^G??*@y%sU z3ptIES_r7wyze?UIM1DXe$(Ot8g4@rXMe=8UZ_p>3L48%5myN9OYV%1g<~5KA206s z+VXLz$(_#9M~@Ewn`_rgm+p-9D>En5u)p*A-b~;8>xS7)>w0i;+ZeL@{uaF-F@epi zTEjscZg34dAZ4@)(G|T#F(I zuqBuq`XZG&zQFh?4#56M>6UQp>EkSz}@x z{_i*Bq9@1+aL4q7xGQuGJ+X`&Q+pkp<$A)gC4<}R(9{oz6xX%SUC`K?t13Yj#SMU;e1!HjeI{&^f}@;Cu7 zV6@0^;&$hr3%o>%Zc%(3ud|{&ZY*R44TT2gWx3SSXlBtWW{9!HEiYVrF&w{wCd7I4 zpZs=nNrYB(piE!v>jZ#{tA-hW$*E7@#Ls6{oDtT+o4goQ0G*qbB-m`KC4Ji#uQ?mc zMOqWX7G-+uxuKRraAp2CMsee=8APzf1$IJDzPk$kzoDYS;$X)8e9wcDOEdG)bm*Z> z=pCY@<$jw5!!QEIubYgWj6I?Xh&x$Ca}GiF$S0BF9T7Zmo4+zHrqjQD@8xe8tas*YLn%zSMmU)itZcx*|&H$s8~==zdQXIdTCV zzvS{iDLxVa-~dSxbv;J4AjL02vVeTn%sLFc_Gaz)O$M>?akIWI_}3lMx+k+5=W%h@ zJ0m4d*i{6O75Xk)T)i&UV?kSa4pPK)11HLBv$61y?~F}KPn1uIpX0v$W3Gj(-s3-cu1WVDh=lUOr;%t+QN>d) zIac!yi?7R~EJ3-YMkV;YreHve=YCtk;B0Cyg0t+od{ggIm0vw@h-xtjM-Rac!T`hD ze^0oL4?>Npd-sv8V|UtY(=0UN8q^29zVd1&aq-OWNMjueTn&>D2Nl?`yLF>|7o4$x7$$w{)?t^UjIUk|H0M&159bEfnF!d z;E2Iv!Ul3M3F*I=XTjQoY5RXhvJHp&?}!*Fyqv}%4r>jC-gN)%Y0rtrr{MZBbDsAP z;=y;gjQZyn{;$gz{?D-pXFENqL_}7QD2JDQP$FA$T}>$lHB|&g^tpseQh!)&wmRN+ z?Y^sYc6E&$-O1BnVb-r!AGz??JPjPgdO9G(h5LymjV%%b~mJtK>46eB5R}cmZOB`rG!w*xR%s*UIUbJ&wv3lj~oH z`?I9m@K>I<6rIvs^@W15fyU? zh`;|_&3ZUUb`%*SNUJHj(PB)&$SAJ`FUKYs#Bw5tMm3Db5P{(hYT1pgV|&heI!ejZ zp)>is_@N>po%G}FFrn#X?M}*iwiO_KsgruueRo*QSA(A=`1#`&Ke?Lxz5bWUO^~m9 z=yPw-F=g_XHiu+#l0I#wr>X|7nYq0JV`T}R1RYIofe|COiBXgBX^TGR$gd>+HRGGI zeZEqs)%s8kHNgBP>wTWK8_Y|CrSW}#Sx4GSxJ}&eEztJk zXE;?~PnXfh<4U?ho}pu8?3P>H_z%g=LnxjRss(ilk>rAwvOlxIQi(z;f`U?l01+&W z)Ze9;^LMIG|$WRX&8Ph8*)wG5{RXD{HF9>uUETNGKuqj`dTvb zz>JpjNB8X{^9mNFWuL{NLh{t7HtiG(^DmKGJMW4;-Bxkq$+A_EzpbC2v+NJGct&VR zGK$b7Z%dkrjKy?TzBJ_^h3hK?q7$+;~b?3mg0J|5~zK7Vd!)?tK7lC!VIrNYwM zFrB8&l#A|zpp_D=X~1ZqCDm9j(BT;wv2(YLgLyyw(=E)b1w^2wf%2d7K$R0KQ^B=Z z$KJn*q~Y6dgTnQHiapyQ@fHnfv~ z!o|Zwl_Y9c+YWO)GoxZZGGK9FoZ9ze!pR^EDxy7VU`3-Hbtw!35QWcO-1LS1W>7YP zZwD0K=kk54w#;(k;yBr*+SYQ&E9l*`<>%eR3Smc+jIyixilV)QT=p`IY57orD{=MG zy~C@8MxW4EKYG_CYZ|=@Y@iQqxhycWj*VH$o|^+olyS3m82FG7TD!ZhOXY9>LbTJ? zTrurmO@pO|Ul9?KP{(~E^J2&8kj9C~*9OGAl~*0VnHhsbMSUTOLQ1TVKWN}AtUT9n zc5v2A?N#$U|0eRzmXAV>QLIh-)jp&HLor0k?Yl{&2H0&h*srqLGv4U|vD5(L4Un#( zq9l(FaS1+q#j#Cpr%Zr_5;_9>{O5SwZy^n17)e&;Mfb#!hx}{1m>_fT3>;tvk5fn5 zK^bQT&6`=myUnedV?-)dB*-%t-`S?f*KZRcRaWL?E=H?+PMjAFltF??88Orr}n0S`>JY;5VPkiAXQom(bELm8reCROm%y%BeysA;LW zGA|&GuM0St&;cG-gw6Q@5WkY}JV=gM!A$?@tmz|SKu<>;M-EO@mB%?FQ4@(#3N>;7 z@s`>x5Q9_h#{&$Kp4~3&6 zA_^Q}9MoVhqCx8z`FWBZ^@R#cE!EKm&GH_H!GHBjy4Rq~2ue5)J;~6uXVJ`Z9&xfE z4`i+^`5EV2a8YA}fgn|6C2@$gXJkvtSJP-L3|Nu(*fdwsBt90>QYNx$L_Cj}S#m6A z-C6@PzrI1X8`!0=Iy4RmADXYBitSnaeyp*M#yI_;Rhn?nVQ6!Y!~V5ffxXD zF5Mf*1#R6@l-~{JCRId9z7F7SYv;S_!dH6Fubo>6NN?B)gqO?Pu4Ac?+8XcQt-A^QTbiXoN|8x zlY0Mi(ZJ1&tklSewXVN>KEyyrO_9@5;@T|Cg3PMho}gBlzsP+Uj|PSP*5hl8w}??M zC?c(W+S)J!*D{zk=jFOtNMRO%Qj*?c7+#Hu0zV9(+`wq21W=-Z;&snlWyy{li0hqX zccKo;Z?0lwV{)q>TV>WzP}tHNtfo!DBH#E*E*^gYjYh-($x6{5rsj@@Ocw1QV8#LH z=mCZwL$OM0%vW%Zt3!uzh;-g9K^_x2Ktgl0bq}4F?@5v3pJ?AyzD_>KFh;fVN|<;} zpx*Ng49c^5%c`g&g%$}F8UPSh&qnoY93bS+TX*5fKEF8?-xTd?gn-Epp=-t`6DL+H zQ4+*F&>lS1umE%n)|ao$Qn5|C;TSk}3o?%R3B;LiWJv^jqSe?3TK&=0ZS?K+duLKT z_OTmOJu+-B#s*$B)pMn9n2TEkS|>p%S6T#Dkv6lx^|2n`nPtLmY4Muv(Ul%7-zS*uGB1V_(SHkxtk~h3v0ZuR-!=M07&YwN`8RLmwO*xpGcjN4{BR+Q}e_ zY+om$yy&^CUqPE|;qyyD4j4y$DCsGUB7>TJc`2zCiYJw+)xZ+$fUIM(E|%c)0fHSe3=E3SaRW8CaD&l{LmL zv490|!#5%le&)hxG2~tEs4!&0)+Z&cSR@<$YnqO~!!3S!64dN;7yMaXLQkLLUpfHF zWtw$Lto(#%DU?IZcG1%`A;*yIVkXFIJu2RLleSc_iA-B-^gtrCzCQ1$^(Af+(XPIT z5tGeE()@guCH+4ZfC+U9gp-e%&tQAVe5x8wMV<%!eZjMUOsE*rwJV9jEV4A=(oH|& zU|`-HWV}IJXtNE&=}N0_N;+ZSuzfxjaym(nM}=zz35#F%DG~{_B6Y&ul#tQ^xvfnk zP|QXUx?#|PYWriSQQJ=)o}kz82FE+bOaNn!0y;LXlr*fY60rJV^V49J>2&n&C&|11 z5&29dZ$CXAAt7jtQ7o8RX{mTcQC?B155I}5V;}9+m-qX)|9M2RWKCYF20uKy=qGC(rtkTnA8-sGnyLBdNk(X?=tR zN2=-lbm>b8e0WuV!dKV@*mscP00OO=9_rM_H=EiWfm~E7kdFg;ibG9!~^zxBjm2WxxxIqdVP4&@B-i`ve z{d=EacHk?=N^E!{lqAwz7E2r|~5jNynJkGT<#O z^Gwqh(4lW?qsC?Kc^QJ%WM8F*tXWAH81h-z*@%u1G9@r=K_1NAe?q*c>LaZoiqKM! zf*qW;~Z|uOT53aHSi^pJmj8+Gqbr2|c(F7-tACRF<_f z*pvNcwIdL3;jGwRgF)^uPMXD5=s^|55pU{gTCNj$-u``@gvZX`B3Ge?XiuBC0%b*7 z6z0#W5#d`!G@OKaLfRAd8ZptEQH_~+d_i^;vs@8o=Ws4k*9Hf3)EK)<8mFJTTrK~l zTcfH%0C)__ucSDA!$#k(*e>6`-sk7X_OQTaS%OOQ zc%>znNU(LFh1rGs5)Fi*a4Ht+Gokk|jR;U+VNgVet}!*bz_($_ppvo=^uLDBo9lWr z1Me3e1~3Mt0GT9}w!VW4va>K5YQt~2``Q`9d+oUO!sN(ytj_B*EHG+vJKQ?=2wACy z0J#qlU6dKwS3SEGig%6sD90S7I}rOmcFiE7vqlUD!WT)1)F>|t($b1aHYrHFQE1Q@8T`)3$S>MURnpFWIJh zWEf6nN7!~#xlU@TZAdB1RtezbKI5iTMYw3p3=g!il_>1720- z^@vS7@sI=8Gur*T$!|>ztFwAC`mhK>nW;XOo5y<|%u3@Qibhtt-ukU3_Rzj%fCPEh zwsj%AQAUMhR|o$fxm#3O@=%-WZ|XuDM}t+winH@cR;rbpqv@{s4puUskGO^gXHb{t+v{!i6%hGl4Mqu@$0Z%*&6jRS?n{{b@<>UKQeR2ajGbzkTtT} za{=S%Td7yG?dMF85?DL}z2jJM_lGMts+U~AugxHuz}wM#0Gw}fN{<`b=Hd0@@H}0j z$J9H?5;)UDI&99ZVsUb36N+t$({_n(s?P3E>QVzz_+Z_Yy#Yy80QNxva~Lfmy&G`| zk4r-82D#+c*82lx>%1mwZV$?VD}J<9MN2=gIw6$+L(}AFZ z-EPC0{0;xUgD?f6G{O@P8l9hJIGPld=cR6uXuHc5!t&I4xR;ktQ_>;z__W{b777@% zmib_fBys^Y`pTVfQhE5S{WY;zoKWo-Poc7?ooj`@%1iF}w+|xG>*Fy97=TCbkGuT8 zx{BawK_y5wzvqc_Tl@YBRjjEj;HQS@O2{~d%l*!y}#C~9Lw>k+UCHx zr9Y4dc8jeUOf4aEB7OdB!+GfH0gz4C(M;{Axom5@LSWey@&92JscC6S0*W!y8=cr? zlgIp3N}R*6vUJWYs2(R+mz6hXG#!OX?Dr+5!_t5p#P&!2S8B_>@dB z(aNmZMO4c);Ckf(<{0rb^hljuNfkOQJX9vXit_IIZKFH2DU~^7oId9xh6!!kN9uNDNL+aSwj5+z5y$xR%iCNsdXOn5+ol$Z9~$ zfZ}!5(=oYnNDHy=Y8PU_HJj2%2B$_Q?WId_3cu6xH=^b`DAO_Oz)2o zA?<3=S2&!UNM@p&UG*whPCZ$aP3N8q8_jT&xJzAXWJfMb?-kfRSs=$SX{Z5D&|xQ* zG#^akL7W$i#@F31h<`gt#*9fesHTWDsSeLzwg34h-8=w`^SpmCYf~nxZ46;U=c~U` zlEXi04bGw?*#AOU50yEjR8=B6g2g`i78~#;VXD{jeW3we2F#$W=`TJF(JRt%0Q0 ze{K^%zAh?36x;Q5GHlG=t4Ak8XJC)#QVtWhQaxZvvJ-U*`L(Kb%<6cd% zlvXMK1gWvQk3(uEVS%UIhz_;fAYGbevVyMx%@1mgCC0x=a{fMf3EGAmUxpe($&N zGvI%J#X}z}t=O!BYU*WvJfdxrrENM4$6ZYpEBlO2Sfqo~c@;m|J_zc&qJU*G>3mF^ zT#+;R&Vo7|-%8Sh<>CbxYqS`xP}IJ;{L1ibx9}6j=G@uL91b+R!La{4@T)Mv8$EriNk=SZQK{k zP7iXklAuqiXKRRg7%5ce(17uesgi9`M8B|&_K<<_#4<$n4EH%R0tu;z9wrJg6XFY3 zTZbj(DBfzsBPg#NH$F&6&c>0Nmxg*STNm+^D%g)lAif7 zl@;uNH+U%YLtX+pPe=p5#-sS8r96BAsa&^T8lc&a|IR)S!T_g0=8(02fB&ZvG5U{> z`cp;Fj6L2^TFJpR4?qj~P=sRzDo<@H!$SEJutrjaf_}BamQbX$L-U>=GV!dp*QVA^ zU#6h3VzBs&`4)_(IJ6yp?()*UN*_;?*N(CE@N<`3U^iw&VWyrd9j=eqjL7RXuK(Qm zXtRJefnL=B#P-zv#1uZ4WJ&Gq?tN~s1P!Nd*dWkSRX4ZQ)4YkiBkxgM? z5bVSO1RNq*>Wr2Ju=s?KAfwhN4n9anqV4mUVq+-UJm_>oOI|S9JBDSxTkrI&Zb=hq zreC*B0Vz=!1=4H4Dp%yLa#JZ+X~4o&a8gfEmoZa&0919MiyryX%(P4#h{hNYrE^@C zEF)sy7xt73jMI3oZVo4%cup3X;FgDNBSMpnA|au08M_ci6KH)ktrl~nPOE5}y}vkr z#y}gBJ1*@?i)plz#=twhpK2wtf)Zd6v$)f;okIRuY#I6W^(EGi<`dR3ijpHcfJwBa zhq2jtHaNXh0o99%dW!SAM@xop&@p3qIrBO8ZH42hS*V;l3O*nT^B1k~NC%=Jb^xK& zYDgM4Oi9ODfN@hgI9o+?x87(G4Ysqv1bG53H!ASn!I{`Gcl8$;imeOSwlcVqnqt+` z0yvmQCck>sGs1ci+7TG~W5nYNU8##OV)(|z(zj=8DwHZ1BEC!*Wn$)GgLxVmu=~rC zo)n1U)~@z$8lu&kh=9BCuI6qNhB+nzRc@9vNg^gxh|=#}8lE6rot-FDBRq_e0~I_x z--d6Pc2-h8oP28UiaCdQBa(SBC|pY8qhWa{4iA6xa=w+E!wD)&iS1v&p7JGmi{pV) zc`|tp_XH!M2=ZkIIAo0tZ5b-)`kr5QXB!;Ow zSfzUuFw98(nQ16G6w2?dR4fm))-|hcDXZXwf*sR|Q1^2h4E){_i7=$< zq^1+|I`#?s%KLmg0`MIIVIaf6X?4VmNmWS^lyd+-Cs^}axif8`#~(6r+ECo~n+ z7VorI}W#>xblpd@*>L{ zatVlDm&a+K?1q@TpY@yKVhCaZ*vuMi+heG-n@)<`!G&TJ$s{d5ZJewsn$M&7AgVth z4#tl0#NZ!<>+Y0;yuQU+S32kbuN*QW7x}>8{F9A&7~mL__F2wHCn3Tm9y>6VX|+{F z8*A*AEd;w?D6{3*Aw#E*E^my#K6X`IT{0@mMl!g0;11S^$Ve_!e_kYgeLDr5kBJwQ z;OlfGq%u1@kd>pe;QMaQDr?v86SApCkY(eR_Jf)2_=1afj;TVZ0!=&Pbfz1QmEF9k z)gKATGB?|FyuX3e=11~8RZQqJPB_!Hs+Wxmfjd|{V7fZHf`t64vt7hnc_$?EM-o-e z*P>3(z}$>GK6(3ak|_QZHjxN4F^Xe94WMmR-=+9QBKa1X_$G13>ankm4w>y?mgbJi zIeuFgDScj)xeOCpc1Pe^#jahDM6#mBURP9A_2ha1dzC~eWVH6pjbH*CMt#!FQsqSg z#camvArQiNaWQvQ;*Q8+{dqU{5m`7qSt-qSdCh9r7SIZR!GEm$%VNrXHt2$@MZ|Wz z8$X7acyXRaYB#uFB46fuCQ~Jpc358Ls0L?>Bod7b?TdAJa?W}hWrknzDRrxE%a}5p zn~7kN?m{{W=I$j&BGQQIdy$rgswB_>?LJ|za6TC)rch;Z{g@qRzLDbt+i zfitl4Bm6CL-j+*7nVsL0t|~e-KPlO4g~gj!-N9}{CnUsEv*?BNKlzG(K&RK`RSO0H z%Y)-f%fECP1e6@&?vxL&Nd(rF{8Dm*UPqG~QVH_%(a!>{CZxyEP~pvs#O$x|!vi@v z^oz?d#ke2BX&Ld8Ho!ZDA%LD!wut6(ZKz=V7BSN6?^;+lkg?c%`H4O?O$(4Rm z-WcR6bZAUoK^TMvoQOuNCasFfOvb?J_VYrSx%(<5WwuzmZ6*6#MBlqo^uL`2*~Y6+XDLRXaa?ds0Kv+DV*A)?_e$vHP|bs9Bs_ZY zFy1ax{k;h)59H|#fh!Tj&u7M1F?NDS7rv*0 zjiIMAHcRfz0kYy<`^?V#Ukp5?KURZ}h8#7gj6J(+euxs#1+|F>g72_qjGr~dvUAcK zI6N*Cq=Ch&{E6Y8D?j{3xhITy9`b%gPGqOX>-ulSu6yDTWfk(LHE)t|@V> zHghtAY;4fRR9}oiPT_f9*PyT{vY`mvW&5 zYv4qiVLv@qnMsU+XW%S9VNw4j+L=>@cC_dhH@mi}I zH(eAdf>lUqJaFNkOrBcvUjabOnafJl8W%9M$<5mNdh(Ivd$QOt%nL0&Osj36i4ZII z=IwyJ7C(yx)X0s!_-ImQ65t{=hUH=s{*I_oz;{VU_0Z0Bt&lTAk5d&m&%Xm4 z30MU6X^~DdmSOs_QH-@cq@PS<%)1kAgx;`DxFcbkf!>o|6K7h)q=Did&jig9{Hk%0 zK^14W6#-Q&vA3KBq}2rbjE)y3(EAmS4e$oCx!ZO~u}$L3LrtZH<8d1&bJZW zgOCD5Z6^lTyQ%-KH4i<-n6A(@utg2pG#o8ls96LR3!~CCGo|lpLIZ1S{H;Ss?8v)9 zl0lUSIPzWSqqey!$3or;lBJZ@BF+Yofq6OGqhD3P1^E{@^tPZ77wVNQ2Ur@ouGh!J zbUAkm=VZ>oIe1GHJ8`BT9|RT4b37EdWQe%g`;ebPD09@_jpa6tN{lZuzsS@naNm@j+J@{jB06?77^ zA?KJI9FaOYCTvVxA@RqB^4d#08=4v8cVYzFI*yL&XIR~oQ8d%Q;RZGcQ z1eI$&;+$#R`t;A`VQ1Bn^3v<;RqOk#>1(|cn0DMja&RPq;N#+NGpLw@G+7#xQ^G}H}QDqu?SS@0$K zy0cVU{o+qa8n$66vmot+igHZM=s4!CxayTIIAnX^pg-T95`l=o)WN*8*SMs3j6OQ5vEx!1h~DF#(z_o=w0 zc@`NjJ3^0#-Tmm1?KB{q**I@ZA&`iy-V0w*CJHAiO&;0DPz{dQNRGqHUoCdYLKR%u zg^*GURT4L}p27lYPTl#8YmG&mW?MhIxndtD*hzgJ_wbR^y2&Cv#stUqWA?K)ut}+^ z3%k8VL&zc<+}8s!y3j*95puTZU^?*@b{Q(N3id&7I9`OYBXGlHBM?E=jrd| ztmMmmVmGWZv;7+FOm_mkcQN-`3K8eAfyZnNmX>n-495(iNWI6R6#h(pC1dpwz&(sM zy(nf_1jRKu-Ui=0Yj7f+Q)@pEe?KybXcFR~LiMcK2|P28HUQNFJNUKsGrIeZ@W`I+ zjjZY(Q=Z?RauB3x2Ziz5p1r7(Y8(k3e16XoL=WAI2)EMPvPrT@i>BIeP zUcLC%l^~##R5z#fwl3n3NPQ&cMDmM9JxuJ2op`(vkA_Ks6dYWf3(3_qs-kg;Y5s!E ztP?Gp8=$F8v?^AyawcjefJ1+>L_xWSe`9R@GUb8@qxos|;z}#XdT*_}4x*2>Euk^Y zKK3C|yw=7xM|Lpz*RVI;z93{+Qq1>=X&!Q1fX1)qqx`lEgo%LMs9uqB0p)3schSfSIM6~2pak&?4M;J(ds z@02_0h>EugnK>q^x96N&f=kTZFM7iF_15@H1p2LFmCj&a$=JsQE@a#wq%CdfRqmM% z&xPLVfK9u_fBLihmu$Vs5d+UOWf!dXbfrN_j=8ntX^2Pv8ReBbDFb4CGezw!6hFF& zGS7#Jo8}eZUs*Gt^$engQiC?^j?y?31NIgLgxU27E6eCkM;U;l@q`X>{ zBX&tm(fMu5*DTb5{A^7vFF2e$qf}l(trc`k0$W-#jFhrCrRzFRnd}ol_nPYJlwa}# zTU#=SfIB@ZDcjENF5f=AY~9qs?v>}d#kN=rej?;p>X;OD2P&bfCW|?2(U8Ajz@&{o z^*K8xS+BA&vgOl|2e>zF`JY?+-pVJ}2P#jG_0er6K8Fb3%mX@#PbmcylwYscXD6e@ z?n2ImZCkfiKWYNb-*IohJ;iK!q_%9+8kUy{J$)#(##d*mep{yt4&s*Ak+R8z%I_HO z8YBA-bpWld?lS0p`)dpNFhX^g&TSvq#tL?(WXf%M{s~t?~_Qf$zQ9s z_^w;14nBS(w65#6CZERfD`R6?`L$-Vz-}DyUDKkFG&us&%V}w^1NfWT*6rpL_gV=7>IVBaQccXyRDfOeUj13 zJ`PFDW zeLPW#eKb~hx8PR%SxD9VH9yok^*2^~GMDP%+>#oD!fAiSz_}$usc1ECcyncW*@_jj zZEomjZH39rZ%|i852H|KtVbslmYPVIunfK?>r4j%@SWtXTqb zKh9YOk>uk2 zE{G!Gdo5VRoKwt%pG(mE&sc}X^VPnj1>W)&Uv`-+nC-sTnZ;*)A0hDm3m=dF5dcJc zP1*RB)x7=n@hZ}~tk$@8DEQ4{h#{+Ik>P!;Jb~5jR65ULd+&%);NX#{S5?>)mdUAa zoBj7JAD+^#MGT*LfUVkwz{zsK^)$R^Z}oppqz5>c7Y1FW48MF6^HdKZ(H&n z?CcmKy)H8wWYW|CJn?AYlO80VNq~71hreWEW9y+6GtKMxB%SW_v$|*p(EjH2eP1Hb z0}s5k==BvNEa@tg`{Risj@Ng1vDRFL4%%k35DT7%Pq7veVfA-Ve)+;rSNwLkxC@IE zrGT|T!U4+p@(DVR`N~%1?Nxa@nRZGoaT=F8okNG+ACCjaM>C!|>+CS{>@@$#Ladv7CK>}2D>s!r=KOMDotRBcgPLv|F$Sk2SnEuWY7{Q z2u$JiW4HL()m}k5Y0!PU65pFhbsV>~mCP)YsI&17=~yyKi<6SjVNg1TU;{l>8z?^6 znO>#w)M+J)C30Kb_~K3zu8Iid?EmHO=m%z!Pg_!^OTq~(vhV(<%e2I>1{g(>K=F+@ z8hmI>^LB@`T!g1cciJ^<9;_DwXItDtUd4B7TAy5o5#SEh-UcBj<2k`+myZB0K%^$a z%Tk2I(T~JCW8VW*Ql%(N8lfO^r~9zT-7fbvq7VRZkga~9Nj*=p31JlxBfzpqaUEeo zimn(Z0fUo;zYqCqD03fvMrcQs<#*$V)x1zU@$5-zPH)A0%J{+8ZbPf&s`L~mTB+tN zVtCx=AKxZA!?{sds`I~}vG3A*8{ekZ}JEN@WMR|i}Yz{9*xsB>wYGoDrh7n1#k8JJgFm`hV% z?9}ghJxr4bQvBDLDp3u1C=g8ARKTcSq{%>a@VyVzHqoIL z=uDIaY%uF<=DZEY&L(j=IhR%qklCP;^zYk8eAogcgxOZOG9xVmuxn6>dGBXvxNZdB zTCf3fGQ}u!-?kdM^{iJyem-QloI|!?XB+(3f`Z;A!Gl;ccqamA^jdTChC=Rt>mU3-pd$rMGk>Zia%8R`tQyEc| z`?#|=hK!U}aXg7#TrEx*{9EqWPjj4JC~nEqoZolsMnGDGbCldd7>||!E;pxEP)N+;vu7RPOENfe|ZuOY3 z0vS4f&D;8`bO8hE@{lf!I}ONQZalnh)-{{85}RNkjO>7(eHC((4JJ)uWN9Me zS5orJiS&#|7iXePdM-f4G&_y1Q0-sdQWum5l;+xRbO!)y<*yDq4l5xQ7qVowUht6o z!J*l=L%-WU9mDk5rFSHtkZWg5wDvYK7(v!eeM}26IwMp&njuPqvn3$Re;;CaLpSa@ zoc?O1Q>}x-i_F$Zcf8>7tKqoVFY-zScD+`~yC|v7Rp*!%O8VEk5+?{kDkzLaD9}1c zc@&Dwoba+Qc;j!RGIT;Qf@`Cuf0ohd$pk=wtOa~2Z3CYmPFOx4xjaJN5NWKH!i4XV zG?JJNBnF%y;wbjl_;5a*F=RX(PX3fkh@++q95Ldgt8Wh`$Cz8M9JY|M9GvMzJ6$Vd zQJ^MC-U?qC*B-LNj&@lJsc@|CWxCz4w`Lb#tXm-|ja?EYO*+bMv!k8ABxRg&Yb<3frFB9*Nmiv)eS? z2f%c}@<~?^iB`Nb4uIw0ykOREvcV?32l}(|Vkbzo?8)h`TDfjJMwCsox5w7U-2(gi zvQL2lbmT$3bN;rttERQv47Av!Y4*xUgCEN_v+XjdEAi#`)0GJ?jM;NmhKnZi39 zO3JfpY0(ae3xTiEeIM>jw?U667L2yU#G$ZU3f*-F#P~}%%6i-}|Db_84Yc+_(bMTP zSPdxPNm?csq@+!xBk-YZ^<<{qdr(k&2W}#g9!<`s22iW6hV=I<6!v-PpIlcDtDQ7` zcZJ9_RdJ|xU14E1HQg{40zf@aHaDUKuH8qbU!=6P84B|pcn}ov2*#CEmrDswz1m5a zbZD(;F-btJ>YxQkZ)<$kwW(!1KilY%?N%6vsV3#2sAyC_+Uf0*3UKEeuNyb!A{nbO zsm5P1{=FQ2Oop-$t$CyHW;-IIZ3;@m`@nrDio?TZ&~^a<3BqI z`DWf2z*%C%VVVD0YNJ@lk_RL`1n2o6b6u{vtQrPKE9%+ph0iS%?w~gi7|%7%>|%i% zggg{cO*Yg!4NCc!y(+B`PwHF$i@CRoiX-UWy%Qu@fCLDE;O-FI-QC@T1$TFMXK;6S z*8suYoxxoP9rR4z_xsNO;@qEg?q+q*n(3~p-d(lp*}tdYv$t~>I`t>T{K#TyXg{aG zKU92s$Q$o{)xCc3XI9j4)2*h*o0e>$echXA6`u-67$u}l3UF&%{?I14d>;RDaWAdh zFyqUE2^fk;NE}$wqjz63yGQtv`IB9Jzf8RkrzC>kc;RZd@aEuH?r3Y-=#Bu{LLj2= zgvj4b@vMmj30qI$W0OZy(+=8|#+IVxNk!&qQU4MNZyu=>0}}?~7p9Xv4hTX3t2MnH$HR}pW@GIXSONJ&{$;1=OUPb2XmZEY05(Atbt@mo+qqYF3X)CgefXgn}7~;3MSJw)A*TAY4^Q z(dX(Do#k7^fQM(+k}^>(;zEg-8AS~`OmZ4c4G8GwBg}17iIUAA+d5AckQDY6|2RCS zAmUJLo5gY96Nn89Kf?Ck%!>jhsRhfRYqGf!T1`OEE7QDOXxDj) z%@{9TjKB2cPN!c?Nv-n0Hj7t*Vu*{)G7XW5Vcv(0=x?JxizB!kO@?Bijei9j1%ZqO zcE}iihGZ=kiFZtKGaSH%q`)8K92EqD_8}sZa0=QtE@ixv*YeZ-KfI%8B6P4i z=$d;j04c}Bk{T-9%9VQ;#$uTT`@==q>4#E55h+2eP2BLa(yt^2CVYK|ljpiT7aU!b z6BYrUz*#>fywWEHcK`UBySq3;=;QFieru!3+!0a>e#mmz>B*yz2BPy^5`rY0rXH_% ztBTJ2v%4hNzE=Fp&_J6Ul7nmUw=`wl3HegB{fScxyi&2 z9QLx5E~32pkZy8~Fr9EA;qcVVGHwsJu=(wb`M0aMBFr4$&CdaLy>)kdCgJ5Au{7S= zkl7Q}@Rk4V-%IETj18FVkB{3_$2cU)1)`n`ROY3{w83g<*v{lKsRnc=sAn4T^&b~q zKF2H_G>*M^G;ktE;a9BqS|ynTrN>Nq&FuvBMv`N5a)tys&R>5_ElIY=k@fC6qc;C; ztASlNIvrfKGkCzgn#x3&G|yWMvT6MM>Qit(AShlDB(@SyLzY>CHbW%9!}&E_g-vow zj_d=*&wGcl)uL(ixR)-|Lt7O$H|lhg%}pVMP!Ep`yF727RvLM&AC85}vOj2yq8*Ab z<~h6WjGozhLC*tMPo!oKq*U-pX@+nFv<_STPs=1RkOI-^s83Aj(v zkc2Bj4t4_56Q@G#Y`e3XpY$K+ZN*5)^U2U76Of&ID{=%I8TXY0Cxd3c;VZ)kRAv(% z28m=oEuxHV)JEO_47D42H%e`f2khjA@%?@+ccF{3Cs}^?w4O=eB{NhDa?IVKlxAyb zOAvc9_tZ?WkZin-#$yPK5LGJPy)4;BK9VBl)1v(a5&)L+P%cFe)Titxed8s!5jvKE z4^MkA+(?BDB_gX*6PKuRfF(7uQ>mmRN_E5m`%ZR#!XnYl?qa(|XVLrlaB-73v(Ler2-*X-CI(GcK@=H`lGgTDsuu8Vc0^BAUHo_N?djO17O0^m_% zRjvi11=otDsIuEN9cIjcKSppvQ{-*paw){QxKAK2DY{W4$-ExnD4T{L?Bhki8nw~d z%P*LDaQjmqNgl4b?c}-RCj-rfO$iCa;`8|F_B3zEyGjq6S?t~=Dt>x!go)0I6?KR) zksMI_IpBh|I~ zZ~p?gjQ!d)WO-SQBMwbs49{UHU`53ALj;N7(77$3nA}>W`=@;cOj2;TR-E}9BwYX> zyqC&;vtO73YWj+31QUE|B+b?If$I2HGB5pCzRMsVIu}h6LJZLY zy2XB}c`>{MBIzgELDE$c^X;N}t;5OiS*`pS|KJ)wB4B-m3)qhc59GM(JvlD{dZHfA$*$N5#~b6XDlk--xZUN$R6KXnor`V zy%Sx=JYrcvc*Lt+9WAKvEZ;yC60%&rioaRy^EX3KPHZp|(mvPY<4td<35N|~;Me98 z*uPW@pKN?}{hUinxC;y41qqqoG&xR`B%NZJgqXgR*k4wD#HPy^6}xp7wI%qLw9k=8 zB=3;Kh%}{AOM@QW9rYth9pxnXJId)|6BcDhX(h(Wj2gKS^gbH(t#oAiFDB>m@l8ca zXZNaE>)WH*H_~?LLOl2{HSF>y_v1<5a4agPN&UW|(7dJ^Rr);|sJE`cie^#IREgdF zJYxK5EWd_@Fv)_{F**hq9z&w6U{ELiX4+60j*fi&7`UUVrr+98&?2F=ME$KvS`hz) ziZ&5pafhZx!fj6No1%!8zGD=00FolMVd=}mMH*ou6!W-UR62@DSxoR0uBD>Z0)~R} zw8ByW%%~!nrOCdOE8DkG<15^)mauMi8z^W16i=cYaWI&PT!Gp>`^l!LZua+ri zrNK;FdPSJc9P$rP_LX8U)Un43ps-YpHg)y+%3uA`ozR- zT!_lrzeL*wgEewyuBhLr-Y~E-Yql)Ie8tAip07o*vbg?}NgNFkn%YA;io$|c z2Lpo`ZqvvpPNyQ4Z`7w7)JkIM80yW^GxN@9E5BTXYHHM>1YYR`kpH4IRJ~#rkx5CB zFf|O2eEn?v8!r}RJ6<5h$#w5{UT$5b=2X=2Nuj68Civ+oGP{WiKBwsZ3;V*f%Bt?8R ziuss%ic1HoB4z@lRgC!tPE4|_G5*xx6B%n@pniF!ItqitZe3nmkT1bYI zy4hb?scKj2yAmlZmokQwyBd9l%FgX{`rT5Q^$n7y4?&YUH;ilij*T`l7Of8*5VN=) z*67JqNMYq76sma2HoB0s6kib}0N5*Kk$n0wqxkF2Vg?iZ;ZJQPr*Z5oZ3u=d&BtHP z#GxTn8OxMJ{UllL2jMg0bU+{ppRGV&TQPRf?$G1tUv!XEp)%E1%PUNlcyq<2uP4K& zH%_ps1=UFQp+mNb*{sY@Ab4-Jh)5IDJJV~cG;%l?RJQ(^CB^W3yPx?P%i9{64YIPO zun3{hXjZ=gczJc8`!x*0e&p;nVbpJ$jl+m`2*0VqEFR%D;Cgyw0NQRyGZ2Fq!wvERVfB*B?C`I_U#Bi zK5rMqvIR0W!w-JMu85jYu<_4Sc(kbDAnFg@7&jk4d^6PQ3 z{tA(GMu{XmNpgRL`j*aH0R6Gs!(+oJA3lT=>bvP-zy10Kl;Zw@m;d zs3_12{F_n%BV(vP;>j}l1;xD&GU1-Qh{NQvv1$)IeQ?2$R5Z!@%?Ypqe@++7Ltd*J z`p-b1%vz-6{2lO(lBLR%rn8ka_cU(xX8b2sf%}`E)LNW~S26Z{0ciB{Aq2z zJQPLTqpw4)v?{kI2d;TxULu7-KEiUfsKdn}M?&PetxpJByII}CMP;P(Po60UvC^Wc zYpnXx9}eVgDAv3x-(2G;G%ikDIAfSU1wrWpiE<80wlKx0?Y$SL9CLB$21UpDoYD}n zRYG-4a8O~Nz1^N1Jhn@|=%>Z=6=Wx=miyl@3>vywAucs(!&^28i_)sODxLTZ=GH2uwM1>AY^Sr*oIUt8bui_xg4j+7qENo_y$;bGaF z1b;`P$cOr5s^?sxGO0s>JRn7>K_SSF?Q0mSGVZP(TlJw4^GbxYr9eeWd)zG$EiR!> zR|#5m3(FIRi=2X=aMR8PLRTw1Y1cX+$OW-iW|-ZzAE}-UQqsqF=A(?tIME(5rcAXPm7Z4n_d4{S7{d+r}z6G|(}q<3KJ+3Br?YJM{fVZp!ooVDts z#6=Fn8I(6Hyo`B3LG2W@Z^8IwHuuM-T#Sl~b@3@Qb6n-5%UZbulVWo-Nfm$KceW3d z7|SzEtnAG|tb|xXn=3}#AAbv9(p)4T$TE|isUMplyGP%S)0qPnT~U%dZRW~DORq2+ zrI&XT2Y38XP7y=?5{RHzdLgKm1&QP!!#0Y^X_o9!GcK=vFDa8`N3K}YqzEn|qSC0% zt3gOcBb4o?zOHzwNn0Qni>Y^+nZrtJuuReVZ21vXJZh^0H~UGiw)y!%s=_uZ$!*n{ zAii)ej1>0CHlc zD{3G@wap$`0z8~?|HfoQ(PSjs73^JP6T-4q%`T_qys%!Zmpy9@du}r4idKua(pxJZ z+enzV{#C(`u#U!4;wsh@3Q^~hl3nU7F8!UD(zg0&iFz;LZXV+nAobgAzO7QP zmrnO16*Y212}REoGQ;3)X<2-GA2AhGjKokz!_#L`vh*J}DsSJLB!`lz)0Vtb+xTI^ zmS{3uz8`5(Ybz7kTFzsjxJY|%l2S1oEynb$r^cp19_XD1e_%nzUpx!bX?ED1;bQAG z?u5h)2K4^jTP&`XQAn+eS;o2u>8fq65rRGQ?v)r((3w7Ef$PFwE>5C)3F^07SkCKt zw(~=NMaT52yl5ux^_iXPG+D2oYQ3HA)a99eGPl&i&DC{Nt;Jevfel2pqQqe)k_syC~j(*%QJHNntHaOCjT3vEET?S+_ z3w_J?Rcl=)&nK-i9)kiAp^w&Auq1}p;I#puGX4G?k*?)lnN1P`SP+}yE91{EdG$(p zhUxuPN}9(r?xmKTgyeR9qlLP=W|Gd%8nM0kGg8I6E|@atm#LUk!`EXcS3l;SL}g<} zb6Z-HkNh_j$7Z(8mzt>27g&5^)te7xQxJGFJO&W3`I@c1YS#YoDqJVMAk55@b-iqc z!mwT9B|#T5D&m}lNzNp^Ph{aQhWtKx@ zA?&DtIlj%JE(1Kb+$Ld`)1|RwkSS;B2?nRJiA1&sskkfY1M68lo3988A}-|sUiNZP zZU);(^KqLJ9(Ce%mdR{)feH$@3_^MpJ(HgAtEIcwI1=cs4|VKf%Xown#G0lzlR1Cq z476*4>3(re$(aYhHD!mXoG0(l1=kNzCfTJ)UFH*OwkeUip2(GrlqKNmOZ1_>%kF*e z`RQ5L^Zr`ju@GmD-lyagw*Z3#CzDCZeS!qoTBdI(t~ga2!q#RxP0vKOgqoeP8CDce z>(8i!?Nu;< zm%`c^je3r4PWbk@=|%}E25UmZNW z5qaPB+l*(FLD2K4LduF5wc9H&oz&6Urr+}+JTmhh=&Ml>DP8^T0g?;vvdi$ zo5)6gD|KTwYs%y^3y->v1}GU3JZNYJ(!fekcb0E{xtD>^stOa4d>`ScXg?2s1y&?J zzdu5APBTmEt*cOl(t9Z)#cAM2Z?Sz3Il}?+>5?anfBXD0^ECa7b03}8_bxfUNAe@HLf(=@{;AJwjicAX4C~V}> zKlhV?KV?)!oP3Ox(Qnfxb{Cyh&a}*uby{a)q;!-nAAs+NOXm8t$$`oxVR71}#QRD& za&s`cI?RP|v5_b4sE?Bd=`kx5+CyGrfJ~C4Lw-6UaD~=gTQ2#8(U5AnE?ciQ0Y>Zb zxTFdIQ>jS;Q4?5f$BTOm>G#^+u(3vCOqH59q@($-O5IBL-dO={LUrky%pe4`;T9Nn-!QB8zhZjdr+$(&kLZNPM|e-dt`} z!1E;{K2PDQ_11l+yf!fD2Ao|6)~a*rF4-jP1h&q|JY&8x*^_)zy zO`(tXBz_p_n(CqgzUPT2FI6?fTomNxNt=qT7FKmDfjkil%O)texiXAFd-T`A$mVOB z4koN&5d%vSXj!2TFzv;>Rr~dRsR}>`nwVv z7}vWa8jng8Wc(#hK_XoXabK9CM;=8n`L7{*5^EloGG_9uTuybHUtP5SzE$<=oe6kB zb>HDn66!GXl=Dc*yh=weP4B4O$OwCMn)O@NP9l^pGa5bB`V!J&KkC?0#$-bIM8kf% ziTLnK>^j4_1ytQ?Yfo8mwt>h$wwtY&@ZL$q#MR^&eS$Q@N%FHTx2lgbP_kW&1V7g6 z{o9IM8maNDf>_j%j#76?p zlGl2CYh_-5>ig`6*@TfbYs5hC0J-LEbjp5Hmg(ijgeDX;jF&)(A zsved54f{x8F`Dy)o_S?W91817XIGKxD8<>Vta0Cl4+5Pswe-djNo<%5$FX%9etIQ0 zmO86y7^83YMQpw4Oa~K{2Fa!JTxurM+)?K)%60awp1HeaMCT<8;7y#MN_x=H$`rj{ zw*XSroH~(Q-KDnIewy#}2gf$d{?)nu792ZDP~)kimi3MbeiPN+uK;e+!^x}>J#=2l zra1zoI4^M+M$G+Dhrd}wz-8ujl@?QI>D3C!yg8kttlU5SXgfLb)gtodY2RfB&4?rI z5866OZ?9^#$;;OHX{0iVIuXH!gd>z5-@ULph=IW#p6-Pz5hT>vI1}j;(hT3)&g$Rr z7Ol5%K5isdE3`m~BeA`|wH$+ys(m)Zw5s@A=qau#8@;L_yWPcCIqzMhWWg~tUD5UA z?LX7o(iPe{t<$kz)E<$`+`KC=fIhKN4DD*gFE_uyZx0jX#ONlG=0#K$PU7xa<2t#? zfON{0jXY_Kto3_?U>MZ1xD=lk#Wd{=!W$Gf?O3vp)7yA6FM6`l^S7z#KHWQsh&)n(ruCByH4qVY=~&1&}Gm^Ye%Y6rR>+P5|eR|58uDb zrd&LYD5)4oB$mjPcPbYdy2e|@sz`FQXp*xXO3NDt&$9=UDJYWSbPP&9ZmI8Wxpwa0 z=+lW&iUR{DwAV8B38%Q*U&mW_)gvo`m7yzQdLHr~S~1V^q4yGtD)Y44V7OsrOPAqg z^UG1rQ`Gpx`5;57&yR9yi+eUx@jKUWCVj?twY2)x8@G$truy+#TO$;*+cp# zBc5z-`TCmjCR|)~oWMPQYsBhYK5`I{QAiX2*p%PwNP(J*8&6`l8|l(Vc<8g5|utT5?=3_gwL zg7I8?mFVWO8eK3D5>~yn1N{zF6gqv*@K3J-xJ7LbsrAhL8j+=*Xu6fowuj` z`(-!$6h$VVa5*IF9{B4|f*to6dLYaQ+Z-a_4)gZ$n#ascSFpSj&xfKaER5@V%f+#k z6d(og#!G!u4SjVI=qerk@+j_eec&b{$0;gz#-%m5_ol8H?3>ud4kV@MbcnBf+OY2u z%fCO`odUGrP~0Y86oim=cBo)_X_S$xY~;3+V1^QG)@UZk%l2@Cp)^&z!a7~Xh&glb zNWqhE*~yFmX0n=}wB7hQXX{xs*~0oM?hMV67eqJ4>)YHW;>PtNqd0}dGZbVCMM`|l z+N<0{;d>YH2yMzvl8yWc3|Vx7=H@ZO*((ggp8AwY>yoVP1$G}TiZDvsme*Fv;NV=% zKh{4CKo?pLs$@-x8HPFmn#X##VIfcV_}wn4!yjDtf@WR0QYCH;Yik803?C!FMPv6{ z5u?&ibp(YR`DaBR_a(MAibp@TRA#*S7oK~Al%yO`*4t~4(qB!>Pt$ORW?htKhdzp?J$I6t;Eadin-+#RMhj|HbPf->Hk~5!SpF%Vt7PrH$cGfE zsR~H!-A>#;wGeV_=T@(bUp>d!+dTG-*FM!tSSrtn8?&uUY_E24za8OP@i`iEFk1kc zjG_9FJuKjpPPp=yGk|>GU;S}ej;+8??2daowwMxbBkX&gzF9OqRwoRViqCa;sx;w^E0H1K_kXm?F9f7vn6VzqT9 z4)(o6Ga{OePVJ&qxRDt;sy@b4&&F7!k}nLw(*@0vchje?Px3y&Hd;#>0z>cf)%z?t zk=p$uP%o*8tE5Z|XQP(Nde_`@ZH;MxUD|U%Xab&R%OHxk(IltEpiSa$+}&DsVnDqE z=PhAVAR3cLi2OTxMq4k?qFN_ijSqi%B(xSGk&x-YZ_)Q-lL=$kq^_sf`$a z@@%zEnp$g13+YRmswLp}@qD!k!HAGAlo^* z5%Vg|MT$|^=!F#%OQ+E?51gsB+NKwDi^u$Le-?JKermEeRS>jD#n>v9OP9r`Pou2x z)Tvr51s9f-Dx!pZjfzsI+#tfg4a;E`zqg_VjKDRXTKn>O1Y+#D?~e_NGKtFSE24`@ zPq7dVc7tW_Xg3LxChdVq>Gfjn4QD^vPU$T}KQ1?x)$l`$+>xc^_P2qzUM_v9D@uYO z0vhS>Qae(GD-K7cpGc2 zN(mU4?X8W;+C$^W?;ec%(s3D{>5~3p+c)R1qrMr(|7Ka_^($dz?>fce1UzI7>dF`# zF(P4ST-_Ns`BD8+r(>tx@j|{%L2uKeYdjk2Lwq=23Y<-oPWr&9 zJX+nlsqz^!Yn!c1msycL&mjP)x)JW1SPeHhKXpX2mJ{#W^(^lWPS|)d0riT?VxcPg zyg4?6uXb4o0hwDEiJ?fdtQ6#nA?nGZLQ+rNiLXNSu|VKMm=m?p0|WaYWs`b@Q3I2O zV#3LpffkO3I?9cYfX^LGJdVxJ{tqJkn*lBeL0rb}GZjo7TZ9`UniT=pyRtQ!glO@r z-+vSx28pGWkw<&D9vtjFAjYNWp$<2i;0!pf3&l@)%PkUkc8}XCVXk#ONL?@V#MQj; zkm|qPI=CMOHtfy4Io>hKy*b=rgZ9cDz$0`uRJ-7Q(3bM|k>tKChm*?4DqfKoHDwu_ zF4bzC$5I(J^wt-Uo~JguemTu~eAb4M2l5|P+wgAWozF?F-b&61|hsnC7-kn2Nlk@7aqbW*Vaz@FX|+WiVvkv)nRxFH>3 z0Mdy9l%2R=cD!=VHr1#%RB=q2(+leqUz7(WHAM?uc`#YtsW|KcWiA!_Buy@|S2<_3 zl#AdxEYlvVGZ&jA6intCn`Dd6vx;vM^?4FqhCiNkdSJQ0K1sORu9AT^A4V_ed=PMN zIwvpHfr$+J@r+uH*=5BQ3%9bYyj8_AhEw<^k9v3swuky8bFQ0YZHGN1-|5#@^x6jCg$|oG%4~enc}79<_oGjz9;r=*RRUPyZk;hp>pL(4U@C{ zvUDG&U(N~{fDF;^?gHGVZpVOHp6=@$rtT-vG|#gV=kwJ)Y*Tb8ip^<}<8?;tSPA~E zbdR@{ri6zholhLa2t+GWHE)aL{8~IDE;ouMBa`O%72oTyvrI16eTp)>O}crmp2YVv z9q;MVvN~FXw8d~DABHpQLVsuASkr^ptd>G9kr={gxt~+&-9gt0&)roXAGo8Le&4JK zw>QJ8L{|XH(B-~xzB=j7eK4}!#>-(xzHgXH1_N6~yBT-Y9TL-bu6y{i8HRk1_}Z=t zYer+`JOzFD9u!BE=ln?B9=0F*vT@Ut8w*@pCbZj)J;I9=(EQ6`J2o)a!MX9=Tn)L5 zd=c~8hzJ8+Qs+)=2a)r!DU)ecqg(2jr6vM4zUKKXV;BgrK+>C*B#G8sje8p^fJcE= z1QJf;w6KSB>jfy2+0LZ} zGk2zkW$&2z*{cV{$8c4ceQU1aW;UYej<<_%PD(vh+5y=WA-!bP46@VM*Wj zBqLj7R|Dyos)}=4l1rszuL^_sV3x|vGt$>l>UW1%p9`NdQKbf8EsdW(mYiOJzrSeE zYd^wqKAhSPV1A7FMx!#@wL;BHz@s6@^kWR^>jb&J-WYG1^uH+?@w@G*F;i)WUD5JvvjrqMIkbiLR@zh(-{Aq8%Vb{=h zG$>_gEH5Y9*1CxUY|Lrv5&SaIqPdrt(+IV2sV%Et^TH24RF_ZA@qLj1cRsbP)dFk=!8b$a6jehc>UYi)b57-XeJE5AOou6%z0%1S} z8N;!xLPVm557LW>%vwkETRD6(}YGQ{dODJ@2U{W>p|S!emog>ycP zOf3Z-A5S(&S^ptAYvd$Y%&6{E+UrB2)BT{XqQLq^{JsvM^}a7>v=?1|aZHpIg}*Nl zhTh820c7l`9_`W`thCbJ#m8CKM{Owg_pA7=DoLIe><)CmZUB-@iXN80U$h_Sht1MP z9pXYoID39AfN!>}mmoLf<7H*)Z>(#Dxdu`6&h%8wr1b?d<%kbQyEDL|VlMHM2MlTB z(NiX~z|qb+qo%MZCva1kkP#Am@Cbz20}YNF-yO7aR*|ZOD&H_7}pE~z-=~&x}A80G`3%}0?_=$X|JDLTNx-CPwKm1MCQ=i*9n|jlg<3BugSUprfy;E<#ObShIvg{3xsO;G^ z8gB3BBT^^0Y19D?#)ATjYQ|!DfN@CtkEyu2w|d~`K%~7A>6ovVX#{9sP|Ch|!#j1X zK8K`@CR##~#Zwss-5w?EWb#Adhmz~27;_DS52HA}_l^p91(_Q9Z1TGt-vejbRbY-5 zjkk;{IwIl|t8}9PIyHyS<7N+#!O^BmkUou7p3iyZTY|QU%j6#;L+;WWj4|#=A|2V@-*^B*4cB z8TZRJ=ac?(fCa1he9S_xgY}EShd;^9_DpS_&1?q-%dN)??G3e(2Y|~Yz&5*aTY8MH zeJw2qarm3B9W+coo^?(x+F+V&#V<6FWNwv;*8(z=p?B!G&$8I{7@1pj>AB!{32>nD@J_1t5EZzn=C?Je3+aCl|faTDBy z;z$v)7U{QxHfgNP^|mGnTsAY4#||4Yk^5KeE1;oa0w>?hjs=*Z4=(6D?6nQevduZo z%8|t$tlk>3az0n#zGIR@-twECy@+j(pc0egIqCiRo9tcj`o$<0f+EgKKTicRf zIWh!%OBXqZMuXL9wtv8`A949t^MN}<3;Bi^zvOv*Vq55;rSN%jnWMv0KtQv05Gc*g zdQfb$+U{36E<@uy`)L{30U?7_Z$V!Xq>w%0Wer1k+e4T?Y_=!vqM2->8YnC=PVjzq z<*Beof?DQBs+);99S@d_?@qj>xvU4>DvZT{HzbKlHR)D#@llQKEU?GMfnaUZkVgRy z?xM1D3-ghN615eo?d>>4&4<=h_)8>hc~Iarpa|cipe4a~ztzA9fp|CQ4Uou@Irwoi zl5812iIal#3p(3R7DLu*9V#Z*>Tg&$!U%B&X5CQ?ii3PyB;$;ho(3(SA_JJ3s8Bt`%o)1zKL{_o>^DpIF$XMI|)??xa31%x46A)l0EMGr5zaHUDNSn#RV1nrZg? zV|YwoBD*Kqvh}UIcL3_z7=2=Og%7o_MyHUE+hzx7YHm-CvxC{!AoS4 zfBOLh(#=US^Ysb#+J;CM=blmHSFc@18AUG}?v-#&+97($+X0DBpHa-0{I(cAJqCJe z-}e77N8w+utc?6LMVub8(g})Kv1(|n?aI;K+NZHiF5{eYvR)CuEh2A$fTMJJ-CCZ) zabU#K^&f-IPz8dPZw#>HLdmor#Ee&$VRGb6e&YD!Ba|OEvHHem7gLY0zG(SDEJT{* zG1@kHRO`8c45h|c?Dd}A69Ry+q4P*SN06_Dr`?=`2r z>Q(+a*Xa@OBg13z$*4aAAd38Ob$uLAo+bZA)F6yPO&`^ac0E5YshYtAA{027nDZc> z2asI8S*BMj1}Jc7$Oc?jsQfylvTk*B4t_MXVa&kU_N%5JVVD49;{kTCThs?DAVm6K zQoQg3MlVk;)YM5I%gNoC13LE8;?2=Mi;Oc8&GhU~u9~V!+Y9a3H{)j~ z%Z(|Jz>Kgb{)bw+#uA9)tQtGABSq=jfsC~9BvrE(^<(sR^EW6RQ3Zc;Q%xaB!K|FY{O*J66z3DJAwvg3%3l6;G zv&x5&fR#~)sL*(p0;WIJ#aZr=Np%Y8enNGKy(WwBCe~`ssQSo)hTm60nTxJC4D5(U-W_XepOa1H6}PyL))2 zh2UjlIlRQEd@uDp;Q5RbY@3wg;xMeQ z!t{|k;)dL&U1Ndk`#iChthxN)OBwxwmmps{>W#X{sTnCZPP9g8*}7J8w!Pl zC|kA07vDp1x*G>!}0Y0wOEZzN}YcTp_*x4Ij>j}bKEzm=i9p+`pxpQa&eQ2A_z6t6epw(XO#y_zL&-Q*3g zG9KTdl>LQ{9?BXZWM$HNPU?R3oq7M%%z0!8xJE5bFiTGM7CoEYpuBm!OgDA0O5E~x zD%y7N_IwWqaJkXT|dYHHzbvEg#r|Q(lMk# zB{B*huA{8#&zCe7^Moc5NW~Vu?!7)0!%2eqHJ9TDlhgya&U*7P=AEkYg!BX<_uc5f zO=zM@5Q)AulP{>u1CQ-2BE{Q5!nq2gk*dJ`LT6+95sme(pj9KH-O;J6v&Abaif$}X zx8qYKr35!b8NE9JFp%tMd>^9NJt*43#NY)n)@P|AK@g9kWJ+x0grQYR$;JEFNIY(? zdFwnEs%f)^+20jIxV?AM>XirgL14jA>6n*7Ob zjPTEQt9QB1Xd&{a{fJtz9omT;0#Sd+#@T50d3wCtHpz(Vaf$6^^x`uUpC#RRAXuk$4W}siZl8O5rgvpiadQ#h2^x8+!*$pFVF2|X>Sd9k z{R@aAQD)3jNtZ)qIt%|x-Z1ZjgdvB1-@rsWrxL?h^2q$dL|g!I4K8QX;OD?T|9x)f z@@}%mtAIf&MP&h5^o1Z~QBSs!>^et?%Z)Za1)qGiTPOZ?shM&G<@iy~u0w3H#6y$i z1dns%2!v|!=KW&dkd`Hz)}z&j7T=S?Pp6;$z)Zv937Oa$r*cvzlh{EFiTqk?{~2P1 zW?Wp@a2}DM4x{q33QqD5;cC#)o0urg?x$G4ed2WM!`AwIwj95VbMR~&uk+%P!!FRM z97>TH5rZ4P2i_;Jc#2BD7o-Bz>*6V?n-2?G^eq#(Zkk&=el?n(&&Elx=asEzHk%dj z9(jhgWDJJv!6pbbjHo%`WIz&~dpq4~cAOx6rE0g+vRwE*$9zbvWrDR zaiH)mhN2z@;*uSLJ%0c(%#N(E@ljCQbWP5DW(3p4+e85_B*C*MW#w^V;IH zvk*6x+%mk1H%O}XmGAyoNjl~;jq^V#Hh4?b=juw+ZUs|+ERCLK#U1`Itm2qc%M#i;M(@v^wiMKy();U(7d(N@t zw6!yy(4W?+6GIBHtNCg(L)q-m~M7;l7Xb?o%8&dzr&6>U1e%Zm)8*n2CiVJGJc7>F^GM!vOri#P8z!cAwTEoC57M@mKzMp9JC=}!954}7i zauEG7Vd9y4A$FN#lt^cqQx(a;TXuC?lDZ!lpb3UtQqbpQNIDklxj*~=_l6{=8Ttqz z@Te#VaiR&Ck?u?sT8g5_lf*)%r#O7~4^DVxmJ@}*?>ClvKKf}e&k$Yn@HFk&1VU0YUMF|<5pFE@ zBw5eXC`jbV4@t04> zE8-~@8e_l9g~Y1N@##G(2=Wjfw-O4}`)k{FEod=?>roWIfqvz_)GfiS#zxVW9Y}ZD zCEzPfq_}O9^2s#JnC0@+E~Tj-o8>l|Wf$hmOePxd8WDV9+W8z>Pk!w04$%DB15Z>i zUgWyRi|S56R`p7G!C6o#^p#Q34Kdo~yRpFQi6<>0;-;-D`~>&w7o;~pB^ zu(iGZ2jLkNQw+GTkfe#Rx+a;Lfc{E3F*@!5Bdz{g+bqb zL~KdN8sY#k?1`J9Nd6gir$-R;z}tgOsttK1kbZi}ga=%i{6)6I0$LA7FcVhe#U{=Qf+dM#=|zQDtH!AqSabg4 zf5J;L|GSvsw_x4JBw822Z>ERrBpz9DG7vz0oM7L-?|#Wp+1#DL?QkJM z`;T2193*(dukd4yK4$|F6J#NSfJx+0@IRiblKB75{rf1fYC!P6KSFN(zmG~G^8cLw zf9askxUJ6(&OgsRh}{qO6&9jp8pQo}ZMx?uKk`-|CrCV$6Z`kGLESE1*&`&F6i!Nb z(VTH){at1E;P=eY7!nWk4t0nQ`pgbdoEU#daBo9pe}On%NSHDv*}`suCpo$3wQ>Kh zQgHF;VG-Ft0L^C)9QtQsH+m`NNIXB7hJ)2~njL|##u4LAK5UEaB|$z45(BK9J-k(+Y=i?y$g zs`BgBr9pBd-6^PaNOzYYph$;+Al)gQN`rt(Nhv7Z-6^>N0qJzp-J7^;`}@9g&l&gL zaqbxR{s%kWvETL1HRm&*XRbMw{_vWzj?Uts+vARqP=TzMx*l&H?D0VXAv z1-M`5q**rOzkWkbp%=c&6ewhuK0vQ|jTN^bsiT`CBz1BdYQb2Ua;-cS+P6FPbLavI zvcibrL%=F~0-ui^&&0&}*X5J~iv(xUv&tI+lCQJmIG@R^Mb8$`V<`uDab0@hR?WX_ z-hU=RN`FO}F?}d%m9{J*$*^ahlJ3n(+=uP`Z4#Hkg1Oyy;*yqj!+?AA%zDSOTD;c> z#x$6z{Vok6n8<@-aWh2Lm(Mmz>s8PG$r3$SPt~ISuA1^KL#=3 z*{dXdlbEhKoimk-WZ5$kH~hKDbx3=xZy8;5HZ@MTkH5^^&tPF5N$gNu(KFu(5O_jZ z`Ox|*BG5UCNHcxEcEKkG^<50^J`>7dAoX|^#bI?|@J=NCW_@8Uxo|oym&Ld6WZJAS zGb?`3tXa(C4*S$IdoayD9Kx&Bk`(;v;ETM2u2<|*UJ z3fwG6VF{oI9$|(ev&$aysE1~F>Pm;wSPT@Zud(9wRM)WU>Q$DG{ceV$Ld~@ZWt9Hp zD$4BB+=%(a9)&7_Ew zvk`f{jnU>OhjafQ#}X;pFZAzG(*1_K7XL9}S^%7L{p)(Ll&OgS9h$4kO|T)jD2l*$ z-UaRZ$zVAX!0qw+{*H3_m!tBTc0}(R{KV&L_3sz{pFLMRnqk1rsmqA?xvnkaOGD?d z>s7kDvPq0yjesldOHd|iSKJLN@1;%Je+v^m8Zf!l+N)c|XM3JkU$574Ipp|Lm8+Q< zOrRbgkc9spIcX_80Idc$%{_N#EWbv!$ zhH~R19xuWd&WoNT++D*YuP8&>zYLSD5$M*ti{Ctzc5I&-|Y-GUL(Z;^JZ*CA)Y`MOyDXv6E3*oxa=NCsT$@=ZgVgvayH~%uU=? z@(LQa){aS6HJR9XYcwMi5Qj>5W8e|YFFS*m@-53>-$YS}x&}-hdyCsSYVIx{@C}PW zFYqd8-*e7qyLeLT)$8H(H|YVlmy@5(J|rhosHU)&g9Vzaca1q779G)#8Ub+Co{5WV zMCi{)^Jr}i@n6_aa36@^TBPw>;95T1UcWqsZTo322Vv5V_Lk^Z|HS4I-Y%+)mId9) zxXj_nFO3UR6+fSYTkOe^k1DF3HuU&ysi3KuQ2m-*g?`KSxr_`9=LmDo?|fdQsisD! zG|O#F*(dgEVWINA=lD~>&C{o80-Np+I!+p5?sJJ4`k@uaPty~WEq0Tc*oEx`sHF=* zFFL@G_2PW|F)pJK!>3PUoR`DM^lkgi?l|q!TYG~dZ#hJzKQruNMBV4Q68S)0E2G5U z-*W!W*V@rhFw<-0p84g`x(?V52Bvs_E!R+)9y0m_1nC_RB0%`iFHe&fu}af5qyOau zjB^i!W<+GI2T|#22#K+S;&s&f=ba!R)ul<}K$|4eEHY;A2Z3ktscIWTZ~l_~C<_vMR}imK}Am1$%wAFaK;{TLLQ zeSYdC#`}7ZoCcpuFcNht&>jyDZ{_%snHamj&Uy7jqJMOLzF7R?xsJ}3m)+;jpHGCp zfB$ZAzBjLBXjlYGz@RNSc(~%Tg3R^&!0?5(cJ9>d?Cfc@dtPcYUVwqV{)srfJw&i< ze3a$>yS{~H-~7Bhx%cni`;6BV6{*20pITXclkjwRuU=bQ)6IqlpU7mIwPuSkeZn2% zAo{@8Z;(GPk(W9aXJrk|#b{&b7Dr=WCk7*j`XV z?Mq|t{8;kFzeRf0nBVEA=M+@rdMI6D_P0 zV>8B$4l8;UBJL1@Cx=qP(~#&m{`IK{^@E`otuQi2|>OWmZm}p}&ba^jCQAhK&if(r@{nPv}vHz;>!2 zYl21S_jNgZmDrMZDG=vZChHyyBnEO6bd4HVWv#D4vrw-Y{vK}Q>j4=?2# z+_m`WeqScipzq|IWfA@fbN=9dVl!^xJ)N2$vU%TyV_L)%9FqH4o(tZJ`|zUyE&~R? zHtqX0;%C$5GyB)OwTlk+Jo62&W^uThOv7+#)2W=7^%Iee>&xyyBKs=rf&W2>%K%61h9weN zp;d-^N_C2G8xHv%&Kt|)cN%YVveMSn7&LG3!ZsG&#qNublt(6?^#{YqNBBTwU-7`; zklIOPzLxLT68OMrW{6eyJj`G+sIARyS}^5@QQcm5jZve=X8D8k)gV)PKD*8@7P9Ct z1;e@&i$Ld?8_&JDH(?*!gsAYP`H7AFPG9V{kiH4Gm7`QZJujaA;}#?eagxQJYJd<4 zc?_g*Tyc$|6zNrhD*DX_93q6V?e}<=m{WMZFdW?*=&QBnzF?}cUu-cd?Myzn0%I;lwvzw205csw%t>yU8qfXR0Ul|HMDM3zs?B7Xh0ErrV_gw`g~bhI!{XwQOe~ zZY%L|rxt23f%exZ0$^WsSXfv`>8*>a>uHOB>mv4M=l1EAJvuu2m7}wBd9{2O z?0B=RsY#5@;$Uem5@#wAcX-=HOJD!-uV23=XEvVc={;IsUvCCW6N7PifBT9^Q&Tfr zynvXLbjv*I{rjtK6CE9v7cX8kFSlJ%(`S`Mzm4iyJ>0O5q>82y)lpGVIpJ;N?cgPx zBOB|&7{MHSJg)R1Ko;3qBw*@L=|rrTNn|$(edJK-k7m)o4IOs|k1G(Xzl#7;LS<}c zhY0b2L&0*sf0OA+Dq%SWm(*^33FmX`R8>0TU896tyAOXtk&6%~2=m=V=J7Jxz8Cr7S-SH6TSVM78$|(Ww#zV_YG`O^ zCWhnsJPyN&HR_lkB2BX{L*B5zjzK;A>gp=5*CY_fqOLgRHE>ij_D-s*QL${~xcUZK zs3d+u3m$1rpdmfS>v#vO+W8{|7;Dc}n-8PlcLdUNc)Q#|1{ZkpWYekc*u7!>fkj?f zmNctZ%VBzBhBcu%*=xKfe~~za3tl9VKN2t_X4Mlx)a_vu4qxBzfBnx8@IL@N+@O-5 zL3^6*uL)AJX*Gbaa#hv~6q1xXs}MgnNBF)lqejya0a@?GxXJUCzf)p?`;Ml4gad&^ zJoH(h3-TRfiWPb_J^z*k$FOJR+DTCK`H~K?v#?P=e%L)CmW_)5sgCDc!PZ&#d9paLZIbEHv5dh%|+-(&qN*+~5LM*+AM#H$B^T?S4x>PA0$A+-?b2S<{_@|6<>n14NRru^oFx`oypa zCcdBW7yNFU|N8#oswqzV>>~*;x5?erlzVY_(mNtngfBNM&Zi^auYtxPoC^gzF}-Hn z{Nt|PXOF{ogzT0?NVlEFMTj9095!0Wd+rg81vx=c`dygFNZhyGriSMttL;x1X<40O zqIQRy+8V|mIGp$td8IFjXgljg-xsP|d5STISz=XX$onm7cq0K)t{7N($ zGc!qd%Hi9~$DDcQ;=PgP+^u)ZubIOl`HTm3>C}XPWAo&*Mh;H(<1g<3hG;>@eHPKV z6c^hW>sj#EKvfl{0=tN$5dqY~dbA7#Ro=NgcB%B9=JDy?^kITzKRopv+cKCg{2xCl zqkwRf<85)7y7@%MT2zF&xoAP!kwnabm$5(-)*XpXI)XCUAcf#GW9&2z6|1bA8@sYX*Mmo z6u}enx+qfM~HXY3ziazxM~nMWv>P9U?kFDIY4S# z>Ze!!Dr^Dv$tts4CUF&0@*>Ein_}CmZc?5cSC4Aq;oGyytm;YLq%p3wClGsqjVu2Le|8>~TB0bGA<0WXM zo=6aI(W$es5xgjTgw+byrQa{3@fOzqg0VN}I#NBE2wP7znVhnQ^)=ez69vF!E=w)3)!wfdf$CMP(-Pq%`B#i~w#pI-p zUkicXhHSI(2ao-QV$qzq52|Bsqmz?g#j*SjLg=VRiJlu7Wq5qUdHK~8ezuEfpl?%~ z>oLQSsasWBYwIZK2oP*N(oj_-vexKK)rA_hJn|rkHJ>qL|w)0!WE&$_A z5Wmc4I;S>dmpSKNJp%;-ueuIbBp9aCB{g0qh1*)szDt#u^6u|SMaclA#^-Im*u!)O znH1j}E#O9R=dVxFmC?oP7SyhdcSqV}m%lzuJK5)+bo>BGtjF!gb7bdT4*8%wRC%^H z|HGt%T$P#jUMYhESqiUa#}Sp*X99=*S$#)p-2b>-6>w~O)7g7ji&*)vQA1#3#~OBqtB zX54e`?t_yK$Mup&t3bhT|N zzBZi`8#50gO2q&j&z3_NUw93&T2wh)0TQ9}2zd0LiUH2?sqo9}2->lL(xxL7KY!m@ zkjIvuSC2T4DYI@K0BA{*i6tt|&81h7=XnXzN8Xb9Ka|Z)cE5RVrqnAN_gf4~rzR)2 z+!MVK02{Up0@X`4W(kne!tpGr^=wf?tcY3@66Eet-!$PTTP!9QwrJ;`*G@&SiIKjm zA@m$uSa1L{-kExiM%K`v3&6gf}Xyg1sip>y;EhB=#W3fbj z6(DpVIEl!>e9IPhYo?usW&QR=SCuiT1nf34{=qR|p1?yuP=lRS0i3)LEc;b&QptY* zzd63#-Y>wH0SO}@k~8oX12JMhCni!Ee-l+$a_hAo`Ly(hTf2m)4VAi%F~p_NFc zwWSb(8kwsQq2(5m5|<2m>dbm|@zdvieh2KKtx2pJlK}F)!`ZH*g;O8HMyB?BB##o} zHtgR?`Lq#{j-7#LHI`YjgnyJvcT=E_<3%V-ePCnpjUu-2`}eW!!#QCh&k!rx%=P%% z1D6ymu@Foew|M`WMC1{M%%KGKuwLP%C)UI1!a9AU&MkuFc2kD9e~c*o(rG+T8okT@ zGgP9eqFPYHpp(4ltyS)Uv_(0s|4g3w5=JO8)|Ro;6GOFL)``DOGs{T2Jyklz>0VCf zb#ppZEgj4AgcqHBhpY9vf)uYD4L|x3IRS*w$s0S!0!A^%4;VQWD*bZ9*p7LSTDJV? z)e1DwqM}A6tN*yndvVZ7FtuD=T`e*!OCbKd)Le{kP<GszuEH;ziemUco*wu5 z;gpI3CISOS1tBS?Sj>|*%+D{ImS|8A&Tzm=!MQOu3k&7R{5>!<$?>82oIc+PNOInl zlyLf$)-=vpe~28NUB1$J2LWdcILm6t!|xmVP#GTdtUwj%e$ASAuamHKtwl1$au16g ztF7XgTm)y!#gR0WHk#C(bRViGIyYYJItGY75^QB|PWRa=48I>XIdc%-7shY6-0{Gn zCeZWS@y+I$XLFKTgU&0^+BwvM>(ME)^d_N7Liz)B^&NY5OQ6{7pzeFX;2HZqAVP$g zJ#+BlnSIqMtQe4vz*PiijSI$Lhsa=berY!S4c;IEM63Fa3=0*zHX_nc;DQwEZ)gEr z_@nyeAySOhSm^4Z_5MqzEa7;A;bkOa_a%R_h14Z{e%gZ_x3vVtp9~SyIaABwqzf_% zW8E+%q|`Rn47WtJkKFCpo2YN%_!VcEo+4n*Cm!?gg_NYE+<87m+;dvICc}27>5ed* z{DOi$Y?568GP48!u-|8l7zD4BG-heI=QbGa<0P)EoAH;za2AS}J`2(c;b&HDoQ)gN z@5iUU6Z@EtZOBFEIp@q{w6OB2vog6T%2?36RWM_JM9sxMO?gU14a{YY-?40U?@Y5v zD$mTM)vK}lB%o%$Do6ohj^-x`KOZdz$>jwS2UDH}Io`a6J0;|)IozT8AhF}I7tdX$k^xb1kr$v&4q6++Q_15|=)8Tj03M>5y#EszgJ zmz~;iUN%AH8u@gB<><}rUMol-VtvjbwKFiA9Mjo_0{k0dNgE1!e#OY?iriPj0rR z3D}PkAO5KF}@0bPvpT1;pU?`4Dvtf(XAXY5#le%}x6-3*iyl+TM2mh&Q*h z;5s7iLs0@c9Gn|B+l7>iFL9Etim7Fa=|!}YfFta16B<@SMke(-O#hwo-YQ?qx*5NV z<%m;%fvwu?dE`>#X8lE?p18I{z!>?6W}+6Y)(29Vu%qtaw4VNsJkCr)mbgx!b!<8e zCrKpFqXM}GE@_>K0yVJ4lEDnomy({I^X{qDPb9n?)Fm!0Y>Nr3-u4GCTJSp5M*IX+ zCOya_zEumjOz*olrZD)>;udkR0xBcT>^L}?qsX9EJ%IYqlR2n7?AVk&F8c^!v32?e z2F3AE)TzUffZK6>)lAl`w_0@D#8g1q62bLfvcElukn96sU$*(Waq&hyrx$AH;4sRk!#^1-Tu;Xy-)y~XO{8+Dla4SKJ*%ugoitbnT)H_ zg!?!Q@`L5B@wDUv4=Catrs+0?bSWt*-Ke>T+ffO`=oc?~1W%5DfHLy*8iar(u-hl} zsf3;G&K7l#p9Muy#CjznhcW$4eoy`uKO_=&YQmS9GgeT))QlsFl%i=nSI`G;$@7n8 z!6sn#o8I?=*#)3tpte$kLMrZf*^SG>SIq(YVR09@5&{Zx9K$E>&EJ!J2)0xr`XVk2 zcPE(Lx2E+`VK~@%&Vsac9Th3sJ^ss9Gf|Sz)0(QPzV9!7tbGZ=PXy^Po7;tS#9{4% zkE*Zj_kw~l8O+`iqehF)a6Fe2sAAOOeyxmR1S~7S_^aBd9N6=~-a-z88f!;6VlY$y8wetq2C-K}G7vMZhHB(OG`$V$svq>c!_yyhvV4C` zOiaA7TgT#M9L$rkRj;a|vWAd?94z8MoTLd)6=;$J?8E-}_9hZ-?k(`b{SlJa%f^HW zk*Ase*|vK8y--ZlZzNr1E{$<7);~;IQ_*p6eQ%OU8S8^(4^tAI#X6_5+200sL&= zZrHJ2c45$}le$5h)ZFaeM@-a}kktBQwwE?RSq}6oOQ(2m?lHhD3eHm=do8klp2uG( z|KyaBM?j1sZM`b`$tIhWp=&;G0uAS{dmkY-y^wJWL`1%s3p!;WL9X?J33~!+=0S)RkKmIf16ENW~MQqxHF(F5fTTRqQv=%c2 zw2%^`IFWkzUgIh5Veo4DUJ{+p27`Eb2+@ZMbII8o9+kfG7V}>GW_&Z7#+_!()MIy1PT#pQpoZmJ+qPM{;Enu*EI@J8|OYOE&E_Y)9^}Z191*{fPRs>@xad8p8P-*K4x^<@soo3dK}X0UnxWmQc-SXg zTI_25`b+7%A#8S%{(7`}O zuC}gj!FfbfHzKb+0!YpX2NK$T`e=`8=!8EP<3MEavOQk}%b0&N^&G|9jE4VyQNi{~alv zlX#4)j2i0oUit0CYTULvc-~z@JGCfiZGk;?c;r%?8K&`~>0~I@^xP%k<0STCOD&|% z9UC?6A4xwZAx{ObI@|Z7Ox1$1P9%qc@X=P_H751nI>pt!q>7~MilK~_yxG(^Ldb#! zSB@bCfoac?R!LWt#4eUXd}tnQO^+-rZlX|3p=*XX;RZ$`HmA#lZr`bmo(RA(N`qDvQya;j%*tQU0 zt)vZR0MtWw@WSi_OvlVVmlJ0r1zbcnJ01$g zX%>(r8*7lYn{sa+pPihL&(F_;g?QEd&YXu8HMa9Gl2xx`xl96MlRQp}{>wQ9)?Dtf ze#Uu7Yh-Uv{m>@~EN!~<1$X$>&P~R7_=Sk5sJl?fB83$@Sc?Wa_9(;Por6Prgb10@V!FOP{qULeTcK zoIyGwmb;HB0jU(#~5Wqe+LpyLj7Z`*yDx)Ojlp8Fq0nQYh})P>n|H5~*m zoE1Czsrnx!Wo_6C5g2|kcK`Kart>$TW(()Pb+DxzFT|4}D!Dy=N7*VEMiLDSi`Ada z*q*wg>{Pj-#HkD6n@3FdE`RE%M^yW4wZi#8rV!`PnZD$$^v9c)-HX+4OFOG#Qf(<_ zhl%bovRc4v<;Edn8JN_v3+0%Cp7+54~tt(JbdcE3%OTr{EJH`eF1uFSkmZ zKRG$+O9PV&!L!v-vS~UQQ>}P=d1U-@s_;H`*ct#B9{{@7W9U?6z7IWDQu@l9$D->| zT^r;2d$;XdfJmDv)}Fy3W9MSf_t3+NV4SJ;K(YtJgg08jKlG!ixU~?dEz7g4hq!IP zzxzFj52$U|>&5J(6)L6Ud{#b08wlBR;|Y(z>q2{ti-MKf6ls2Qbvmg-w1(bAxzbKf zPhK`^>=9i4;=VX5L{^AXM1$ueZO5T!0HV4h}LzQ8ZtUOWycd!G{o)N6l( zfdfWjot@}t5jVrNFf}3ua`K5x<+C6Z$+wchlU};-0+CHjOiEkp5B(!duEtWUY_~TY zQ5SY00-)&X%76&RuJ;@ho`>C{cR|ZS$%r%+5@1lrbx@|0ozvjVX`e_Hh2oO_Q?P46 z458&t0ku6p@cj857XZar@=2D@0xKS&ruYQvN8+)IV&=-LBJ=OdADdud<9bBRBulA!xp?ji8pbzdJW-=q&f%q zT{rxoLs-)w#Mh`93_N?{O)rw^v-JoO=8GYGF-|5Pl@7RDQO8S^NN5=xflfCN|NaRH zLX^Mt;JA_VYd?jV?8>HxYIj=cuc$q)L23C8rUEyvJr-(YAkh4hj({LAIb#!i05pi7 zpk#zgaNaEmKV;dF>L+(*t=(@%0D4;%@KI^`KBIL`*GeZTlP_3%;LtPDgTVl85F~4@ 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%0A@ZN&*^j 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 g&ixNQO)LF5AMMOK@%K;4G?T_cMS|O=-@7o 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 A&Hw-a literal 0 HcmV?d00001 diff --git a/project/Figures/test 1 pres.png b/project/Figures/test 1 pres.png new file mode 100644 index 0000000000000000000000000000000000000000..bbf762110c488ec699c179940ff09f156611892c GIT binary patch literal 79704 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 literal 0 HcmV?d00001 diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 0000000..2237cc8 --- /dev/null +++ b/project/__init__.py @@ -0,0 +1 @@ +from network_pauser_prototype import NetworkSimulation \ No newline at end of file diff --git a/project/graphs.py b/project/graphs.py new file mode 100644 index 0000000..2118af9 --- /dev/null +++ b/project/graphs.py @@ -0,0 +1,167 @@ +""" +These are the graphs that can be used to present certain graphs through the pyqtgraph window +""" + +import pyqtgraph as pg +import numpy as np +import cnmodel.util.pynrnutilities as PU + + +def stimulus_graph(win, stim, row, col): + """ + Presents a graph of + win: pg.GraphicsWindow() + stim:(list) dummy sgc stim results + """ + p1 = win.addPlot( + title="Stimulus", row=row, col=col, labels={"bottom": "T (ms)", "left": "V"} + ) + data = zip(stim.time, stim.sound) + time = [] + stim = [] + for x, y in data: + if 0.1 < x < 0.25: + time.append(x * 1000) + stim.append(y) + p1.plot(time, stim, pen=pg.mkPen("k", width=0.75)) + return p1 + + +def an_spike_graph(win, pre_cells, row, col): + """ + Displays the spikes over time as lines with each cell representing a single row a single trial presentation + :param win: pg.GraphicsWindow() + :param pre_cells: (list) spike train time values + :return: exists to allow for further modifications on the pg.GraphicsWindow object + """ + p2 = win.addPlot( + title="AN spikes", + row=row, + col=col, + labels={"bottom": "T (ms)", "left": "AN spikes (first trial)"}, + ) + xan = [] + yan = [] + for k in range(len(pre_cells[0])): + r = pre_cells[0][k] + xan.extend(r) + yr = k + np.zeros_like(r) + 0.2 + yan.extend(yr) + c = pg.PlotCurveItem() + xp = np.repeat(np.array(xan), 2) + yp = np.repeat(np.array(yan), 2) + yp[1::2] = yp[::2] + 0.6 + c.setData( + xp.flatten(), + yp.flatten(), + connect="pairs", + width=1.0, + pen=pg.mkPen("k", width=1.5), + ) + p2.addItem(c) + return p2 + + +def spike_graph(win, time, vms, row, col, title="cell"): + """ + Displays the spikes over time as lines with each cell representing a single row a single trial presentation + :param win: pg.GraphicsWindow() + :param time: (list) of time that lines up with Vms + :param vms: voltage trace data as a list + :return: exists to allow for further modifications on the pg.GraphicsWindow object + """ + p3 = win.addPlot( + title=f"{title} Spikes", + row=row, + col=col, + labels={"bottom": "T (ms)", "left": "Trial #"}, + ) + xcn = [] + ycn = [] + for k in range(len(time)): + bspk = PU.findspikes(time[k], vms[k], -35.0) + xcn.extend(bspk) + yr = k + np.zeros_like(bspk) + 0.2 + ycn.extend(yr) + d = pg.PlotCurveItem() + xp = np.repeat(np.array(xcn), 2) + yp = np.repeat(np.array(ycn), 2) + yp[1::2] = yp[::2] + 0.6 + d.setData(xp.flatten(), yp.flatten(), connect="pairs", pen=pg.mkPen("k", width=1.5)) + p3.addItem(d) + + return p3 + + +def voltage_graph(win, time, vms, row, col): + """ + Actually displays the voltage trace + :param win: pg.GraphicsWindow() + :param time: (list) of time that lines up with Vms + :param vms: voltage trace data as a list + :return: exists to allow for further modifications on the pg.GraphicsWindow object + """ + p4 = win.addPlot( + title="Pyramidal Vm", + row=row, + col=col, + labels={"bottom": "T (ms)", "left": "Vm (mV)"}, + ) + nrep = len(time) + if nrep > 3: + display = 3 + else: + display = nrep + for nr in range(display): + p4.plot( + time[nr], vms[nr], pen=pg.mkPen(pg.intColor(nr, nrep), hues=nrep, width=1.0) + ) + return p4 + + +def an_psth_graph(win, pre_cells, row, col): + p6 = win.addPlot( + title="AN PSTH", + row=row, + col=col, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + bins = np.arange(100, 250, 1) + all_xan = [] + for x in range(len(pre_cells)): + for y in range(len(pre_cells[x])): + all_xan.extend(pre_cells[x][y]) + (hist, binedges) = np.histogram(all_xan, bins) + curve6 = p6.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p6 + + +def cell_psth_graph(win, time, vms, row, col, title="cell"): + p7 = win.addPlot( + title=f"{title} PSTH", + row=row, + col=col, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + spike_times = [] + for k in range(len(time)): + bspk = PU.findspikes(time[k], vms[k], -35.0) + spike_times.extend(bspk) + bins = np.arange(100, 250, 0.1) + (hist, binedges) = np.histogram(spike_times, bins) + curve7 = p7.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p7 diff --git a/project/hoc_trial_run_cartwheel.py b/project/hoc_trial_run_cartwheel.py new file mode 100644 index 0000000..cbf27a5 --- /dev/null +++ b/project/hoc_trial_run_cartwheel.py @@ -0,0 +1,160 @@ +from multiprocessing import Pool +from random import randint + +from neuron import h + +from cnmodel import cells + + +def run(run_input, processes): + results = [] + if processes == 1: + for r_input in run_input: + results.append(_run_trial(r_input)) + else: + p = Pool(processes) + for res in p.imap_unordered(_run_trial, run_input): + results.append(res) + return results + + +def add_pyramidal_cell(): + pyramidal = cells.Pyramidal.create(species="rat") + pyramidal.add_dendrites() + apical = pyramidal.maindend[0] + basal = pyramidal.maindend[1] + return pyramidal + + +def add_tuberculoventral_cell(): + tuberculoventral_1 = cells.Tuberculoventral.create() + tuberculoventral_2 = cells.Tuberculoventral.create() + return tuberculoventral_1, tuberculoventral_2 + + +def add_dstellate_cell(): + dstel1ate = cells.DStellateEager.create() + return dstel1ate + + +def add_cartwheel_cell(): + cartwheel = cells.Cartwheel.create() + return cartwheel + + +def _run_trial(run_input): + seed, info, run_number = run_input + """ + info is a dict + """ + pyramidal = add_pyramidal_cell() + tuberculoventral_1, tuberculoventral_2 = add_tuberculoventral_cell() + dstellate = add_dstellate_cell() + cartwheel = add_cartwheel_cell() + auditory_nerve_cells = [] + synapses = [] + inhib_synapses = [] + # auditory nerve attachments + # attach to pyramidal cell + for nsgc in range(48): + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(pyramidal, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + # attach to tuberculoventral 1 + for nsgc in range(18): + # attach to tb cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(tuberculoventral_1, type=info["synapse_type"])) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + # attach to tuberculoventral 2 + for nsgc in range(18): + # attach to tb cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(tuberculoventral_2, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + for nsgc in range(24): + # attach to dstellate cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(dstellate, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + # Connections between network cells + for _ in range(5): + inhib_synapses.append(cartwheel.connect(pyramidal, type='simple')) + for _ in range(21): + inhib_synapses.append(tuberculoventral_1.connect(pyramidal, type="simple")) + inhib_synapses.append(tuberculoventral_2.connect(pyramidal, type="simple")) + for _ in range(15): + inhib_synapses.append(dstellate.connect(pyramidal, type="simple")) + inhib_synapses.append(dstellate.connect(tuberculoventral_1, type='simple')) + inhib_synapses.append(dstellate.connect(tuberculoventral_2, type='simple')) + for _ in range(3): + inhib_synapses.append(dstellate.connect(dstellate, type="simple")) + stim = insert_current_clamp(cartwheel.soma) + + # set up our recording vectors for each cell + Vm = h.Vector() + Vm.record(pyramidal.soma(0.5)._ref_v) + Vmtb = h.Vector() + Vmtb.record(tuberculoventral_1.soma(0.5)._ref_v) + Vmds = h.Vector() + Vmds.record(dstellate.soma(0.5)._ref_v) + Vmcar = h.Vector() + Vmcar.record(cartwheel.soma(0.5)._ref_v) + rtime = h.Vector() + rtime.record(h._ref_t) + # initialize + init_cells([pyramidal, cartwheel, tuberculoventral_1, tuberculoventral_2, dstellate]) + info["init"]() + # hoc trial run + h.tstop = 1e3 * info["run_duration"] # duration of a run + h.celsius = info["temp"] + h.dt = info["dt"] + h.t = 0.0 + h.run() + # dtime = time.time() - start + # print(f"Trial {run_number} completed after {dtime} secs") + + return { + "time": list(rtime), + "vm": list(Vm), + "auditory_nerve_cells": [x._spiketrain.tolist() for x in auditory_nerve_cells], + "vmtb": list(Vmtb), + "vmds": list(Vmds), + "vmcar": list(Vmcar), + } + + +def insert_current_clamp(sec): + """ + :param sec: to attach too + dur: ms + amp: nA + delay: ms + :return: stim needs to be put in a variable to stay alive + """ + stim = h.IClamp(0.5, sec=sec) + stim.dur = 1 + stim.amp = 0.5 + stim.delay = 100 + return stim + + +def init_cells(cells: list): + for x in cells: + x.cell_initialize() diff --git a/project/hoc_trial_run_dendrites.py b/project/hoc_trial_run_dendrites.py new file mode 100644 index 0000000..8522cfc --- /dev/null +++ b/project/hoc_trial_run_dendrites.py @@ -0,0 +1,150 @@ +from multiprocessing import Pool +from random import randint +from cnmodel import cells +from neuron import h +from tqdm import tqdm + + +def run(run_input, processes): + results = [] + if processes == 1: + for input in run_input: + results.append(_run_trial(input)) + else: + p = Pool(processes) + for res in tqdm(p.imap_unordered(_run_trial, run_input)): + results.append(res) + return results + + +def add_pyramidal_cell(): + pyramidal = cells.Pyramidal.create(species="rat") + pyramidal.add_dendrites() + apical_dend = pyramidal.maindend[0] + basal_dend = pyramidal.maindend[1] + return pyramidal + + +def add_tuberculoventral_cell(): + tuberculoventral_1 = cells.Tuberculoventral.create() + tuberculoventral_2 = cells.Tuberculoventral.create() + return tuberculoventral_1, tuberculoventral_2 + + +def add_dstellate_cell(): + dstel1ate = cells.DStellateEager.create() + return dstel1ate + + +def _run_trial(run_input): + seed, info, run_number = run_input + """ + info is a dict + """ + pyramidal = add_pyramidal_cell() + tuberculoventral_1, tuberculoventral_2 = add_tuberculoventral_cell() + dstellate = add_dstellate_cell() + + auditory_nerve_cells = [] + synapses = [] + inhib_synapses = [] + # auditory nerve attachments + # attach to pyramidal cell + for nsgc in range(48): + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(pyramidal, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + # attach to tuberculoventral 1 + for nsgc in range(18): + # attach to tb cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(tuberculoventral_1, type=info["synapse_type"])) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + # attach to tuberculoventral 2 + for nsgc in range(18): + # attach to tb cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(tuberculoventral_2, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + for nsgc in range(24): + # attach to dstellate cell + auditory_nerve_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(auditory_nerve_cells[-1].connect(dstellate, type="multisite")) + auditory_nerve_cells[-1].set_sound_stim( + info["stim"], + seed=seed + nsgc + randint(0, 80000), + simulator=info["simulator"], + ) + # Connections between network cells + # for _ in range(5): + # inhib_synapses.append(cartwheel.connect(pyramidal, type="simple")) + for _ in range(21): + inhib_synapses.append(tuberculoventral_1.connect(pyramidal, type="simple")) + inhib_synapses.append(tuberculoventral_2.connect(pyramidal, type="simple")) + for _ in range(15): + inhib_synapses.append(dstellate.connect(pyramidal, type="simple")) + inhib_synapses.append(dstellate.connect(tuberculoventral_1, type='simple')) + inhib_synapses.append(dstellate.connect(tuberculoventral_2, type='simple')) + for _ in range(3): + inhib_synapses.append(dstellate.connect(dstellate, type="simple")) + stim = insert_current_clamp(pyramidal.soma) + + # set up our recording vectors for each cell + Vm = h.Vector() + Vm.record(pyramidal.soma(0.5)._ref_v) + Vmtb = h.Vector() + Vmtb.record(tuberculoventral_1.soma(0.5)._ref_v) + Vmds = h.Vector() + Vmds.record(dstellate.soma(0.5)._ref_v) + rtime = h.Vector() + rtime.record(h._ref_t) + # hoc trial run + h.tstop = 1e3 * info["run_duration"] # duration of a run + h.celsius = info["temp"] + h.dt = info["dt"] + init_cells([pyramidal, tuberculoventral_1, tuberculoventral_2, dstellate]) + info["init"]() + h.t = 0.0 + h.run() + # dtime = time.time() - start + # print(f"Trial {run_number} completed after {dtime} secs") + + return { + "time": list(rtime), + "vm": list(Vm), + "auditory_nerve_cells": [x._spiketrain.tolist() for x in auditory_nerve_cells], + "vmtb": list(Vmtb), + "vmds": list(Vmds), + } + + +def insert_current_clamp(sec): + """ + :param sec: to attach too + dur: ms + amp: nA + delay: ms + :return: stim needs to be put in a variable to stay alive + """ + stim = h.IClamp(0.5, sec=sec) + stim.dur = 30 + stim.amp = -0.2 + stim.delay = 120 + return stim + + +def init_cells(cell: list): + for x in cell: + x.cell_initialize() diff --git a/project/network_pauser_prototype_cartwheel.py b/project/network_pauser_prototype_cartwheel.py new file mode 100644 index 0000000..2103f44 --- /dev/null +++ b/project/network_pauser_prototype_cartwheel.py @@ -0,0 +1,311 @@ +""" +Layout: + + if __name__==__main__ handles cmd args, instantiates test, runs it, displays results + + run_trial(): defines a model and then runs the test using preset params in hoc and return the info to the class + SGCInputTest: + __init__ : defines many static variables + run(): calls delivers information to the run_trial() function and the recieved information of a single run + back and stores it as an extensible list in the class + show(): displays graphs depending on the graph options selected below it and displayed in a printout + +""" + +import argparse +import json +import os +import sys +import time +from random import randint + +import gooey + +from cnmodel.protocols import Protocol +from cnmodel.util import custom_init +from cnmodel.util import sound +from graphs import * +from hoc_trial_run_cartwheel import run + + +class NetworkSimulation(Protocol): + """ + Tests a Single cell with input recieved from the SGC + + __init__: almost all parameters can be modified + run(): simply loops over the run_trial() function and stores the results just + show(): constructs the graphs using other functions + + """ + + def __init__( + self, + temp=34.0, + seed=randint(2500, 400000), + nrep=10, + stimulus="tone", + simulator="cochlea", + debug=True, + dt=0.025 + ): + """ + :param temp: (float) must be at 34 for default pyramidal cells + :param dt: (float) determine hoc resolution + :param seed: (int) contributes to randomization, needs to be changed to see different results (reduce this + number if you keep getting a timeout error + :param nrep: (int) number of presentations !!must be changed in the __name__ function if not calling from cmd line!! + :param stimulus: (str) must be 'tone' + :param simulator: (str) currently using cochlea instead of matlab + :param n_sgc:(int) This is the number of SGC fibers that connect to the post synaptic cell + :param debug: (bool) controls most of the terminal printouts is on by default + :param cell: (str) cell type !!must be changed in the __name__ function if not calling from cmd line!! + """ + super().__init__() + # + self.debug = debug + self.nrep = nrep + self.stimulus = stimulus + self.run_duration = 0.3 # in seconds + # stim parameters + self.pip_duration = 0.05 # in seconds + self.pip_start = [0.14] # in seconds + self.Fs = 100e3 # in Hz + self.f0 = 14013.0 # stimulus in Hz + self.cf = 14013.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 0.0 # % mod depth, Hz + self.dbspl = 55.0 + # usually cochlea + self.simulator = simulator + self.sr = 2 # set SR group + self.seed = seed + # physiological parameters + self.temp = temp + self.dt = dt + self.synapse_type = "multisite" + self.species = "rat" + + if self.stimulus == "tone": + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + # result containers + self.vms = [] + self.vmtbs = [] + self.vmdss = [] + self.vmcar = [] + self.synapses = [] + self.auditory_nerve_cells = [] + self.time = [] + # debug function reports a print out of various information about the run + if self.debug: + print("Small Network Test Created") + print("#" * 70) + print(f"Running Test of Network with Simulated SGC fibers") + print() + print(f"Run Conditions: Run Time: {self.run_duration}s,") + print(f" Run Temp: {self.temp} ") + print(f" Number of Presentations: {nrep}") + print() + print(f"Stimulus Conditions: Type: {stimulus}") + print(f" Stim Duration: {self.pip_duration}s") + print(f" Characteristic F: {self.cf}hz") + print(f" Stim Start:{str(self.pip_start)}s") + + def run(self, processes=1, **kwargs): + """ + Runs the trials with the number of nreps set for the trial, calls a multiprocess run of however many trials + are needed. + If processes set to one then the it will run without the multiprocessing library(more reliable) + """ + super().run() + info = { + "stim": self.stim, + "simulator": self.simulator, + "cf": self.cf, + "sr": self.sr, + "run_duration": self.run_duration, + "synapse_type": self.synapse_type, + "temp": self.temp, + "dt": self.dt, + "init": custom_init, + } + # Generate inputs + b = [(self.seed + randint(0, 80000)) for _ in range(self.nrep)] + c = [info for _ in range(self.nrep)] + d = range(1, self.nrep+1) + + run_input = zip(b, c, d) + self.all_results = run(run_input, processes=processes) + self.unpack_data(self.all_results) + + def unpack_data(self, run_data): + len_of_data = 0 + for nr, res in enumerate(run_data): + try: + len_of_data += 1 + # res contains: {'time': time, 'vm': list(Vm), 'auditory_nerve_cells': auditory_nerve_cells._spiketrain,'vmtb': list(Vmtb)} + self.auditory_nerve_cells.append(res["auditory_nerve_cells"]) + self.time.append(res["time"]) + self.vms.append(res["vm"]) + self.vmtbs.append(res["vmtb"]) + self.vmdss.append(res["vmds"]) + self.vmcar.append(res["vmcar"]) + except(KeyError): + continue + self.nrep = len_of_data + + def show(self): + """ + Creates a single page graph that contains all of the graphs based on the graphical functions in the class + """ + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + p2 = an_spike_graph(self.win, self.auditory_nerve_cells, 0, 0) + p3 = spike_graph(self.win, self.time, self.vms, 1, 0, title="Pyramidal") + p5 = spike_graph( + self.win, self.time, self.vmtbs, 2, 0, title="Tuberculoventral" + ) + p6 = spike_graph(self.win, self.time, self.vmdss, 3, 0, title="D-Stellate") + p9 = spike_graph(self.win, self.time, self.vmcar, 4, 0, title="Cartwheel") + p1 = stimulus_graph(self.win, self.stim, 0, 1) + p4 = voltage_graph(self.win, self.time, self.vms, 1, 1) + p7 = an_psth_graph( + self.win, self.auditory_nerve_cells, 2, 1 + ) + p8 = cell_psth_graph( + self.win, self.time, self.vms, 3, 1, title="Pyramidal" + ) + + # links x axis + p1.setXLink(p1) + p2.setXLink(p1) + p3.setXLink(p1) + p4.setXLink(p1) + p5.setXLink(p1) + p6.setXLink(p1) + p7.setXLink(p1) + p8.setXLink(p1) + p9.setXLink(p1) + + self.win.show() + if self.debug: + print("finished") + + def export(self): + if self.debug: + print("Exporting File Binary") + t = time.gmtime() + destination_name = f"{os.path.basename(__file__).strip('.py')}_{t.tm_mday}-{t.tm_mon}-{t.tm_year}_{t.tm_hour}_{t.tm_min}.json" + os.scandir() + dirname = os.path.join(os.path.dirname(__file__), "run_data") + if not os.path.isdir(dirname): + os.mkdir(dirname) + filepath = os.path.join(dirname, destination_name) + with open(filepath, "w") as f: + json.dump(self.all_results, f) + if self.debug: + print(f"Run saved to {filepath}") + + def load(self, load_file=None): + with open(load_file, "r") as file_in: + data_list = json.load(file_in) + self.unpack_data(data_list) + +@gooey.Gooey +def interface(): + parser = argparse.ArgumentParser( + description="Compute Neuron response for small network of DCN" + ) + # parser.add_argument(type=str, dest='cell', default='pyramidal', + # choices=['bushy', 'tstellate', 'dstellate', 'octopus', + # 'tuberculoventral', 'pyramidal'], + # help='Select target cell') + parser.add_argument( + "-n", + "--nrep", + type=int, + dest="nrep", + default=4, + help="Set number of repetitions", + ) + parser.add_argument( + "-l", + "--load", + type=str, + dest="load_file", + default=None, + help="Load data from a previous run json", + ) + parser.add_argument( + "-s", + "--save", + type=str, + dest="save_flag", + default=True, + help="If you do not want to export file set flag to False", + ) + parser.add_argument( + "-d", + "--debug", + type=str, + dest="debug_flag", + default=True, + help="If you do not want to see debug text or GUI set to False", + ) + parser.add_argument( + "-p", + "--process", + type=int, + dest="processes", + default=1, + help="Set the number of processes that the run will be run across", + ) + args = parser.parse_args() + start = time.time() + nrep = args.nrep + processes = args.processes + load_file = args.load_file + + # manages how options affect the class that is created to manage the trial run + if args.save_flag == "False": + save = False + else: + save = True + if args.debug_flag == "False": + debug_flag = False + else: + debug_flag = True + + if load_file: + if os.path.exists(load_file): + test = NetworkSimulation(nrep=nrep, debug=False) + test.load(load_file) + test.show() + else: + raise FileNotFoundError(f"{load_file} does not exist") + else: + test = NetworkSimulation(nrep=nrep, debug=debug_flag) + test.run(processes=processes) + if debug_flag: + test.show() + if save: + test.export() + + dtime = time.time() - start + print("#" * 70) + print(f"Total Elapsed Time {dtime/60} mins") + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() + + + +if __name__ == "__main__": + interface() + diff --git a/project/network_pauser_prototype_dendrites.py b/project/network_pauser_prototype_dendrites.py new file mode 100644 index 0000000..09b5a6b --- /dev/null +++ b/project/network_pauser_prototype_dendrites.py @@ -0,0 +1,298 @@ +""" +Layout: + + if __name__==__main__ handles cmd args, instantiates test, runs it, displays results + + run_trial(): defines a model and then runs the test using preset params in hoc and return the info to the class + SGCInputTest: + __init__ : defines many static variables + run(): calls delivers information to the run_trial() function and the recieved information of a single run + back and stores it as an extensible list in the class + show(): displays graphs depending on the graph options selected below it and displayed in a printout + +""" +import os +import argparse +import time +import json +import sys +from random import randint +from cnmodel.protocols import Protocol +from cnmodel.util import sound +from cnmodel.util import custom_init +from graphs import * +from hoc_trial_run_dendrites import run + + +class NetworkSimulation(Protocol): + """ + Tests a Single cell with input recieved from the SGC + + __init__: almost all parameters can be modified + run(): simply loops over the run_trial() function and stores the results just + show(): constructs the graphs using other functions + + """ + + def __init__( + self, + temp=34.0, + seed=randint(2500, 400000), + nrep=10, + stimulus="tone", + simulator="cochlea", + debug=True, + dt=0.025 + ): + """ + :param temp: (float) must be at 34 for default pyramidal cells + :param dt: (float) determine hoc resolution + :param seed: (int) contributes to randomization, needs to be changed to see different results (reduce this + number if you keep getting a timeout error + :param nrep: (int) number of presentations !!must be changed in the __name__ function if not calling from cmd line!! + :param stimulus: (str) must be 'tone' + :param simulator: (str) currently using cochlea instead of matlab + :param n_sgc:(int) This is the number of SGC fibers that connect to the post synaptic cell + :param debug: (bool) controls most of the terminal printouts is on by default + :param cell: (str) cell type !!must be changed in the __name__ function if not calling from cmd line!! + """ + super().__init__() + # + self.debug = debug + self.nrep = nrep + self.stimulus = stimulus + self.run_duration = 0.3 # in seconds + # stim parameters + self.pip_duration = 0.05 # in seconds + self.pip_start = [0.14] # in seconds + self.Fs = 100e3 # in Hz + self.f0 = 14013.0 # stimulus in Hz + self.cf = 14013.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 0.0 # % mod depth, Hz + self.dbspl = 55.0 + # usually cochlea + self.simulator = simulator + self.sr = 2 # set SR group + self.seed = seed + # physiological parameters + self.temp = temp + self.dt = dt + self.synapse_type = "multisite" + self.species = "rat" + + if self.stimulus == "tone": + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + # result containers + self.vms = [] + self.vmtbs = [] + self.vmdss = [] + self.vmcar = [] + self.synapses = [] + self.auditory_nerve_cells = [] + self.time = [] + # debug function reports a print out of various information about the run + if self.debug: + print("Small Network Test Created") + print("#" * 70) + print(f"Running Test of Network with Simulated SGC fibers") + print() + print(f"Run Conditions: Run Time: {self.run_duration}s,") + print(f" Run Temp: {self.temp} ") + print(f" Number of Presentations: {nrep}") + print() + print(f"Stimulus Conditions: Type: {stimulus}") + print(f" Stim Duration: {self.pip_duration}s") + print(f" Characteristic F: {self.cf}hz") + print(f" Stim Start:{str(self.pip_start)}s") + + def run(self, processes=1, **kwargs): + """ + Runs the trials with the number of nreps set for the trial, calls a multiprocess run of however many trials + are needed. + If processes set to one then the it will run without the multiprocessing library(more reliable) + """ + super().run() + info = { + "stim": self.stim, + "simulator": self.simulator, + "cf": self.cf, + "sr": self.sr, + "run_duration": self.run_duration, + "synapse_type": self.synapse_type, + "temp": self.temp, + "dt": self.dt, + "init": custom_init, + } + # Generate inputs + b = [(self.seed + randint(0, 80000)) for _ in range(self.nrep)] + c = [info for _ in range(self.nrep)] + d = range(1, self.nrep+1) + + run_input = zip(b, c, d) + self.all_results = run(run_input, processes=processes) + self.unpack_data(self.all_results) + + def unpack_data(self, run_data): + len_of_data = 0 + for nr, res in enumerate(run_data): + try: + len_of_data += 1 + # res contains: {'time': time, 'vm': list(Vm), 'auditory_nerve_cells': auditory_nerve_cells._spiketrain,'vmtb': list(Vmtb)} + self.auditory_nerve_cells.append(res["auditory_nerve_cells"]) + self.time.append(res["time"]) + self.vms.append(res["vm"]) + self.vmtbs.append(res["vmtb"]) + self.vmdss.append(res["vmds"]) + self.vmcar.append(res["vmcar"]) + except(KeyError): + continue + self.nrep = len_of_data + + def show(self): + """ + Creates a single page graph that contains all of the graphs based on the graphical functions in the class + """ + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + p2 = an_spike_graph(self.win, self.auditory_nerve_cells, 0, 0) + p3 = spike_graph(self.win, self.time, self.vms, 1, 0, title="Pyramidal") + p5 = spike_graph( + self.win, self.time, self.vmtbs, 2, 0, title="Tuberculoventral" + ) + p6 = spike_graph(self.win, self.time, self.vmdss, 3, 0, title="D-Stellate") + p1 = stimulus_graph(self.win, self.stim, 0, 1) + p4 = voltage_graph(self.win, self.time, self.vms, 1, 1) + p7 = an_psth_graph( + self.win, self.auditory_nerve_cells, 2, 1 + ) + p8 = cell_psth_graph( + self.win, self.time, self.vms, 3, 1, title="Pyramidal" + ) + + # links x axis + p1.setXLink(p1) + p2.setXLink(p1) + p3.setXLink(p1) + p4.setXLink(p1) + p5.setXLink(p1) + p6.setXLink(p1) + p7.setXLink(p1) + p8.setXLink(p1) + + self.win.show() + if self.debug: + print("finished") + + def export(self): + if self.debug: + print("Exporting File Binary") + t = time.gmtime() + destination_name = f"{os.path.basename(__file__).strip('.py')}_{t.tm_mday}-{t.tm_mon}-{t.tm_year}_{t.tm_hour}_{t.tm_min}.json" + os.scandir() + dirname = os.path.join(os.path.dirname(__file__), "run_data") + if not os.path.isdir(dirname): + os.mkdir(dirname) + filepath = os.path.join(dirname, destination_name) + with open(filepath, "w") as f: + json.dump(self.all_results, f) + if self.debug: + print(f"Run saved to {filepath}") + + def load(self, load_file=None): + with open(load_file, "r") as file_in: + data_list = json.load(file_in) + self.unpack_data(data_list) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Compute Neuron response for small network of DCN" + ) + # parser.add_argument(type=str, dest='cell', default='pyramidal', + # choices=['bushy', 'tstellate', 'dstellate', 'octopus', + # 'tuberculoventral', 'pyramidal'], + # help='Select target cell') + parser.add_argument( + "-n", + "--nrep", + type=int, + dest="nrep", + default=4, + help="Set number of repetitions", + ) + parser.add_argument( + "-l", + "--load", + type=str, + dest="load_file", + default=None, + help="Load data from a previous run pickle", + ) + parser.add_argument( + "-s", + "--save", + type=str, + dest="save_flag", + default=True, + help="If you do not want to export file set flag to False", + ) + parser.add_argument( + "-d", + "--debug", + type=str, + dest="debug_flag", + default=True, + help="If you do not want to see debug text or GUI set to False", + ) + parser.add_argument( + "-p", + "--process", + type=int, + dest="processes", + default=1, + help="Set the number of processes that the run will be run across", + ) + args = parser.parse_args() + start = time.time() + nrep = args.nrep + processes = args.processes + load_file = args.load_file + + # manages how options affect the class that is created to manage the trial run + if args.save_flag == "False": + save = False + else: + save = True + if args.debug_flag == "False": + debug_flag = False + else: + debug_flag = True + + if not load_file: + test = NetworkSimulation(nrep=nrep, debug=debug_flag) + test.run(processes=processes) + if debug_flag: + test.show() + if save: + test.export() + else: + if os.path.exists(load_file): + test = NetworkSimulation(nrep=nrep, debug=False) + test.load(load_file) + test.show() + else: + raise FileNotFoundError(f"{load_file} does not exist") + dtime = time.time() - start + print("#" * 70) + print(f"Total Elapsed Time {dtime/60} mins") + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/project/previous_version/Ad041599_062_Buildup_psth.txt b/project/previous_version/Ad041599_062_Buildup_psth.txt new file mode 100644 index 0000000..8fb58d8 --- /dev/null +++ b/project/previous_version/Ad041599_062_Buildup_psth.txt @@ -0,0 +1,2107 @@ +0 +316 +17354 +24673 +24937 +35490 +42614 +45361 +50019 +54607 +58369 +162871 +186891 +197586 +0 +23921 +26970 +36409 +50506 +56071 +137945 +158010 +189398 +0 +24260 +34341 +41425 +47368 +52041 +55868 +57008 +166449 +170316 +190255 +196830 +0 +19064 +25623 +28980 +36193 +45750 +54414 +56731 +58102 +171252 +184789 +189973 +0 +5744 +12518 +19551 +25934 +36306 +39450 +41602 +49968 +55025 +143639 +172529 +0 +877 +18033 +24231 +30434 +39337 +46196 +52451 +56359 +58745 +161196 +192589 +0 +15550 +22777 +30249 +37309 +40367 +45911 +53628 +57859 +167936 +0 +11800 +18027 +28231 +31038 +34109 +41507 +45491 +51369 +54672 +55205 +60045 +141871 +173310 +194310 +198683 +0 +18413 +29235 +37152 +39313 +42422 +49612 +56413 +147726 +168101 +183216 +0 +11614 +15788 +27985 +34316 +43063 +48885 +51619 +54287 +55138 +83662 +149746 +171225 +179907 +0 +14115 +24775 +32023 +36098 +38570 +45091 +47539 +56106 +63756 +157765 +171647 +176995 +186151 +0 +21849 +31876 +39169 +45818 +53919 +58139 +101834 +155215 +156729 +185581 +193191 +0 +25232 +28380 +31706 +38412 +41741 +46647 +54186 +58067 +169788 +0 +3421 +20999 +27180 +34030 +42133 +47772 +53403 +58848 +139727 +182482 +196192 +0 +5551 +14039 +24914 +34737 +41194 +48521 +55078 +154631 +176259 +194829 +0 +365 +17151 +25538 +36507 +46748 +50955 +55465 +58057 +145291 +180075 +0 +16316 +25983 +30319 +37057 +42781 +49424 +57250 +151356 +199540 +0 +12721 +18885 +19522 +25779 +33150 +38071 +41348 +48095 +51168 +52281 +52741 +55000 +57713 +85450 +0 +13036 +15799 +20656 +24515 +27258 +31888 +35896 +38398 +42811 +43582 +47856 +48795 +54726 +56640 +57748 +122547 +163639 +0 +447 +12561 +13496 +25838 +30506 +34632 +39159 +42597 +43679 +47775 +48715 +56767 +56960 +58706 +64267 +122941 +135237 +166980 +186635 +192382 +197849 +0 +368 +4683 +11161 +13602 +14626 +18789 +22019 +25381 +26132 +29200 +30761 +32161 +35314 +38267 +39387 +42108 +46020 +47163 +48953 +52967 +54108 +56619 +59917 +70151 +94919 +99960 +130476 +135156 +137469 +139041 +142244 +145449 +149470 +150287 +155453 +157607 +158815 +170122 +174207 +182983 +184399 +188019 +198504 +199645 +0 +3283 +4363 +5904 +19439 +20035 +21118 +26432 +28812 +30835 +33289 +33914 +35261 +36871 +40056 +44939 +47666 +49867 +53289 +55416 +56456 +59649 +62490 +64601 +70428 +73386 +79873 +88019 +91608 +92459 +96072 +97793 +100190 +105415 +106866 +108322 +130351 +133097 +134188 +134868 +137184 +141141 +141873 +142379 +146449 +147316 +152449 +154042 +155352 +157773 +161903 +163524 +169550 +175015 +179307 +187476 +190910 +192562 +196203 +198494 +0 +2292 +4017 +4640 +5403 +11779 +12375 +17682 +21458 +24335 +28004 +29852 +30346 +31956 +36872 +38906 +41332 +43960 +45155 +47024 +49692 +51135 +52255 +54239 +55191 +55881 +58964 +64913 +68912 +76792 +81745 +83075 +91160 +93674 +94121 +97656 +100813 +102977 +104369 +105958 +107436 +108255 +108662 +110115 +117137 +118342 +124261 +126854 +138385 +141021 +142050 +152018 +154007 +155785 +157341 +174275 +189149 +193864 +0 +13946 +16363 +19569 +27665 +31635 +35451 +43426 +47616 +48091 +54094 +57195 +171624 +0 +17232 +28687 +34597 +45360 +51671 +58377 +120482 +172065 +194023 +0 +18028 +25635 +30367 +32079 +40546 +46742 +54365 +57223 +58758 +170718 +0 +3416 +18754 +34797 +39342 +42212 +51245 +55807 +174232 +189032 +0 +22897 +30709 +37382 +43127 +51656 +56634 +58220 +190972 +0 +16558 +21853 +31338 +39885 +45284 +55987 +183635 +192575 +0 +15639 +30723 +38934 +46194 +51243 +55601 +149393 +170158 +188337 +0 +1396 +14558 +26871 +35563 +45425 +49458 +54293 +170050 +0 +23838 +31122 +37171 +44937 +48954 +54530 +58574 +168894 +191481 +0 +15580 +27702 +33259 +44747 +48887 +54641 +147305 +182009 +196671 +0 +26806 +33832 +48722 +55015 +59004 +176816 +0 +19616 +29250 +38850 +43347 +47754 +53226 +181183 +0 +5790 +22918 +30060 +40337 +44489 +50312 +54786 +180608 +0 +18640 +30040 +37915 +43894 +48843 +55579 +172571 +199446 +0 +25199 +38338 +44428 +52780 +58524 +150523 +165961 +190346 +0 +2690 +28434 +34058 +39366 +47200 +52636 +57634 +156276 +177823 +0 +28480 +35001 +39848 +47376 +54159 +59125 +148299 +174459 +195818 +0 +15038 +30675 +38027 +44266 +48100 +54945 +153234 +169923 +191086 +0 +15277 +23969 +29702 +38978 +51092 +54999 +59929 +174737 +199497 +0 +18318 +27279 +31902 +40733 +51272 +57342 +142384 +168866 +194210 +0 +13044 +23620 +32868 +42924 +49680 +56786 +151622 +168423 +183219 +0 +1753 +20221 +26678 +36348 +43262 +49214 +55099 +59223 +138843 +168785 +188882 +199847 +0 +22028 +28608 +35151 +43587 +50045 +55151 +176076 +193930 +0 +12776 +16975 +28600 +34612 +42063 +48660 +53705 +154438 +181122 +0 +1804 +19969 +26813 +37839 +45203 +51842 +57603 +159605 +186697 +0 +22275 +30594 +37708 +46590 +52132 +56565 +168228 +193096 +0 +15259 +28192 +34267 +41953 +48051 +53174 +58347 +163489 +198935 +0 +17974 +24456 +29898 +38491 +47154 +51180 +56322 +165784 +190804 +0 +15041 +22484 +31144 +38783 +44788 +55094 +171701 +186582 +0 +17817 +34665 +42034 +47975 +54862 +58265 +157693 +183854 +198208 +0 +19561 +26275 +33212 +40155 +48727 +56793 +187208 +0 +23583 +29046 +36817 +47244 +53274 +57862 +142380 +189729 +0 +2615 +25424 +35720 +45090 +53373 +57665 +58410 +121827 +152172 +180851 +0 +2087 +18244 +27663 +39101 +46353 +50763 +55978 +160511 +193729 +0 +16477 +24818 +30083 +36983 +43575 +53383 +56979 +148281 +159346 +172086 +190855 +0 +13684 +32463 +38789 +42986 +49353 +54303 +58219 +164845 +184447 +0 +3441 +13681 +15267 +24093 +31374 +38001 +43623 +49295 +55814 +172774 +0 +3240 +18365 +26099 +32369 +38982 +46662 +54948 +59595 +150360 +174242 +0 +3730 +24800 +30266 +37331 +42059 +49692 +57989 +158079 +192326 +0 +19375 +30597 +37253 +42820 +49751 +54462 +149421 +180762 +0 +24651 +31784 +37787 +45245 +49907 +55483 +196471 +0 +13051 +20810 +33639 +43025 +50720 +56087 +146887 +168654 +196893 +0 +15374 +28326 +36940 +41629 +49754 +55664 +152511 +174691 +194825 +0 +16302 +28527 +34866 +41655 +49753 +55810 +139154 +168554 +190879 +0 +14861 +21304 +30319 +42692 +48043 +51984 +55997 +153069 +178025 +199384 +0 +21081 +27385 +34566 +41396 +49139 +54636 +149472 +168482 +196992 +0 +16169 +25474 +33587 +39426 +43699 +54596 +58784 +193773 +0 +20413 +30494 +35821 +41768 +47934 +54976 +58568 +174838 +199111 +0 +16746 +31491 +39281 +44522 +53005 +56592 +175812 +194817 +0 +14056 +19807 +29052 +37238 +44690 +53487 +57818 +177616 +191038 +0 +16211 +25002 +41884 +46864 +53682 +58395 +149947 +180011 +194905 +0 +17062 +27432 +38360 +43250 +50998 +57016 +165634 +186925 +0 +3773 +20649 +28092 +35334 +40863 +46204 +53645 +58313 +149732 +176009 +190357 +0 +16093 +25375 +30220 +35727 +42432 +46942 +54035 +154447 +183937 +0 +24790 +35308 +40552 +48417 +53667 +57986 +164856 +189616 +0 +3205 +19789 +34792 +41293 +48438 +53278 +57536 +157955 +0 +14045 +25169 +30843 +43234 +50196 +56013 +158631 +198813 +0 +25225 +31299 +41249 +47001 +52427 +59162 +153776 +180490 +199382 +0 +17683 +27674 +37143 +42958 +50967 +56347 +172235 +199289 +0 +18188 +24845 +33279 +42339 +54008 +56871 +60081 +165121 +192083 +0 +15232 +22432 +27211 +36006 +41429 +46205 +51443 +58132 +174274 +0 +1548 +13832 +23780 +32814 +38509 +43987 +51414 +57918 +161346 +182512 +0 +5851 +15333 +22651 +30917 +39557 +44911 +55631 +157265 +181771 +191295 +0 +14584 +33227 +42152 +47607 +55023 +58188 +146056 +173970 +193301 +0 +22914 +31831 +37567 +43552 +52980 +57507 +164777 +0 +804 +26307 +36048 +40610 +53780 +163689 +0 +14412 +26633 +34868 +41729 +47340 +51648 +57975 +179433 +194133 +0 +26735 +35961 +45091 +53002 +58310 +190115 +0 +13838 +25907 +32901 +40194 +47086 +53222 +57646 +159684 +179869 +0 +14599 +20214 +27440 +34655 +44146 +47853 +55466 +150314 +188159 +0 +20410 +26682 +34223 +40562 +47419 +53657 +59305 +171434 +195155 +0 +20638 +30305 +36073 +43518 +49795 +54945 +161340 +180163 +0 +1743 +15130 +28392 +36888 +45571 +50536 +56455 +156700 +183047 +0 +24144 +29837 +36239 +45661 +52598 +56112 +192297 +0 +24005 +33658 +41623 +50538 +56817 +188162 +0 +1342 +21296 +29008 +39875 +54094 +180862 +0 +2806 +18316 +31045 +37485 +49902 +159108 +182757 +0 +14748 +25619 +32021 +36266 +45751 +50621 +55773 +176300 +192147 +0 +26162 +34273 +43136 +49426 +55349 +168956 +192792 +0 +13599 +19576 +29497 +35209 +40656 +51290 +56179 +169713 +187379 +0 +25152 +32045 +43425 +47686 +51940 +56094 +160547 +183970 +199742 +0 +15378 +26142 +32686 +37182 +41479 +50979 +57432 +184048 +0 +14051 +25582 +32246 +38652 +46265 +57219 +159970 +191527 +0 +17341 +24341 +29364 +35964 +40350 +47383 +56739 +0 +656 +26625 +33562 +38950 +48176 +52044 +58958 +169323 +0 +1104 +22028 +30400 +34051 +36208 +42836 +53074 +56984 +142579 +184785 +0 +4282 +25398 +35243 +42666 +50064 +56165 +160846 +198316 +0 +21697 +30823 +38198 +44132 +51302 +56587 +164547 +0 +2645 +14564 +25405 +32648 +41364 +46909 +53767 +58830 +179341 +0 +5642 +14539 +25327 +31227 +43216 +48378 +51211 +54570 +58697 +159848 +174614 +197497 +0 +27877 +36926 +45072 +51260 +55964 +161078 +183733 +0 +6052 +16571 +24120 +29338 +37289 +44517 +50342 +57435 +151997 +175473 +0 +13820 +24083 +28577 +35672 +39802 +46037 +53128 +60257 +131217 +164356 +178766 +0 +5709 +25880 +32169 +39787 +46586 +51972 +56217 +169577 +193913 +0 +26969 +34355 +40308 +45681 +50136 +55628 +170401 +183676 +0 +5478 +23891 +33304 +39262 +45564 +54611 +62618 +185831 +0 +18682 +34896 +44669 +51242 +154007 +186577 +0 +21799 +34779 +37945 +42896 +49322 +54783 +152315 +189107 +0 +24636 +33071 +37812 +45972 +51766 +57737 +0 +5390 +27574 +34411 +41561 +48337 +52735 +56920 +153444 +174519 +0 +316 +23633 +33439 +38727 +44872 +52480 +57910 +160788 +193750 +0 +15453 +25015 +38952 +45027 +51701 +56353 +156966 +187165 +0 +1396 +19299 +24993 +33872 +40035 +44292 +50198 +55790 +189908 +0 +13325 +20270 +36115 +44373 +50646 +57070 +182963 +0 +14926 +25648 +32116 +39353 +44988 +52071 +57861 +146462 +177558 +0 +5184 +17869 +25689 +37156 +43698 +49286 +54800 +151671 +192860 +0 +15718 +27109 +34738 +44498 +53346 +58430 +144599 +193424 +0 +14741 +25336 +31700 +37500 +46317 +50846 +57057 +164500 +182244 +0 +6077 +25471 +30712 +40771 +45341 +51461 +55396 +171170 +189222 +0 +13905 +26411 +33453 +39032 +44291 +49021 +55722 +161639 +0 +3014 +16563 +31543 +36785 +45557 +51820 +55860 +172442 +191023 +0 +14464 +21557 +30993 +37248 +46755 +53005 +57396 +177348 +196104 +0 +16584 +26899 +33647 +38743 +44852 +50467 +55754 +151705 +186369 +0 +4468 +17024 +28052 +36963 +43973 +49306 +54891 +154970 +175774 +199676 +0 +26052 +34964 +41727 +46324 +55777 +149873 +181829 +0 +19841 +30368 +40293 +51152 +55612 +171298 +0 +15800 +22882 +29912 +41300 +50796 +56727 +181114 +0 +5596 +21677 +30472 +39987 +49585 +54485 +165650 +187845 +0 +112 +18133 +26585 +36026 +43216 +51150 +57235 +124416 +145993 +183496 +194896 +0 +16234 +29348 +34602 +39834 +45875 +53021 +58736 +166666 +186898 +0 +15567 +26426 +33938 +41536 +47367 +54146 +164889 +180880 +199218 +0 +27185 +35051 +44856 +50319 +55789 +174767 +198720 +0 +16261 +25055 +31004 +37044 +44477 +50983 +55662 +126646 +172819 +194229 +0 +20906 +28647 +38935 +45877 +50955 +56777 +160642 +0 +4982 +25609 +33407 +37809 +44723 +53071 +58363 +163739 +0 +296 +21976 +27447 +37297 +45285 +53382 +59367 +170248 +199266 +0 +15307 +27715 +37618 +44323 +51584 +57037 +185421 +0 +5912 +21087 +30496 +39376 +45177 +52531 +57421 +151460 +165331 +185339 +199436 +0 +20490 +28300 +34380 +45423 +50908 +55682 +161030 +183848 +0 +2256 +16089 +28326 +36906 +43166 +50392 +57050 +151124 +181005 +0 +16449 +25017 +35664 +41580 +47376 +55021 +59048 +169635 +190956 +0 +22313 +29068 +34975 +41529 +46304 +51662 +160894 +190772 +0 +24996 +32905 +39566 +44135 +51970 +56692 +143088 +177988 +198831 +0 +26070 +33429 +40968 +46994 +55760 +154997 +176295 +0 +4718 +16039 +28374 +34655 +42838 +51128 +56515 +160778 +191144 +0 +26250 +32775 +39950 +47593 +56685 +149190 +187114 +0 +17867 +34084 +40231 +45079 +50594 +57719 +151386 +162817 +197811 +0 +26307 +33259 +39656 +48089 +53594 +57384 +148018 +182171 +0 +14806 +27422 +42112 +48238 +53910 +58368 +162917 +180470 +193595 +0 +16222 +25838 +37780 +41897 +46398 +53784 +180969 +0 +5361 +17057 +25015 +33550 +41044 +47188 +53818 +174463 +193118 +0 +15835 +27086 +35251 +41591 +47953 +54812 +164357 +186965 +0 +5216 +14189 +26441 +33151 +41144 +49331 +57417 +148847 +179031 +0 +23825 +33217 +41851 +46816 +51098 +57995 +151638 +186609 +0 +13035 +22905 +33371 +41126 +47732 +53201 +59442 +160507 +175075 +0 +3186 +20717 +28902 +34190 +38837 +46245 +53624 +58061 +148590 +176374 +0 +14819 +24679 +36240 +42641 +46985 +52735 +58789 +152858 +184846 +0 +16735 +26655 +33015 +40614 +46612 +55166 +167912 +190735 +0 +25960 +36818 +44478 +52372 +58164 +166741 +0 +3703 +20383 +28758 +35292 +41839 +50632 +56539 +148528 +172988 +198825 +0 +13886 +31183 +38187 +43209 +53009 +188431 +0 +2339 +17439 +28495 +33083 +47062 +52561 +58240 +176683 +199771 +0 +22212 +29056 +34532 +46904 +51239 +56997 +180194 +0 +4865 +25486 +32018 +42172 +48059 +52187 +59861 +164346 +0 +14758 +26901 +34773 +46353 +53227 +57708 +169975 +186061 +0 +4265 +16550 +28024 +36675 +42582 +48874 +55848 +158896 +190259 +0 +2660 +24784 +32416 +39467 +48524 +53230 +142473 +161846 +191619 +0 +16661 +22012 +28342 +33051 +39040 +46120 +53251 +57843 +163027 +186098 +0 +16698 +29606 +34313 +41189 +50915 +55018 +178936 +199484 +0 +15844 +23682 +32143 +40698 +50409 +55831 +140749 +173455 +0 +4595 +20743 +26634 +32667 +49482 +55418 +144467 +180160 +194810 +0 +28683 +35033 +39841 +51166 +56968 +189475 +0 +15619 +29048 +35958 +50410 +54533 +146286 +175277 +194331 +0 +24140 +33397 +39275 +43524 +49664 +55542 +146512 +167761 +189419 +0 +4379 +25959 +35897 +48760 +54646 +153301 +173174 +191421 +0 +24982 +34688 +42346 +49464 +55865 +150760 +171131 +187996 +0 +14611 +26574 +34542 +40266 +45379 +51399 +56076 +144330 +166285 +188811 +199627 +0 +17499 +28710 +34420 +39083 +48816 +51451 +57710 +177148 +0 +4867 +21832 +27718 +36801 +41464 +51456 +57863 +141311 +160698 +174741 +196197 +0 +20332 +26379 +31878 +37758 +43228 +55907 +146818 +178139 +197747 +0 +1497 +13106 +28772 +35558 +40636 +53882 +57671 +140253 +162919 +185948 +0 +18079 +33973 +41787 +46187 +51808 +182959 +199057 +0 +20960 +27091 +35121 +41733 +48040 +49415 +55286 +59574 +167392 +0 +3441 +31642 +38493 +49404 +54055 +159318 +180068 +0 +1162 +18511 +27913 +35038 +42999 +48156 +55006 +61242 +174833 +0 +3122 +20814 +30369 +36059 +43378 +49705 +56168 +133191 +163852 +192749 +0 +16243 +33487 +39113 +45881 +50297 +55956 +160909 +194420 +0 diff --git a/project/previous_version/Ad081098_065_PauserBuildup_psth.txt b/project/previous_version/Ad081098_065_PauserBuildup_psth.txt new file mode 100644 index 0000000..4e55cd1 --- /dev/null +++ b/project/previous_version/Ad081098_065_PauserBuildup_psth.txt @@ -0,0 +1,1810 @@ +0 +6860 +9579 +23476 +35612 +44797 +50129 +56359 +59533 +196488 +0 +8142 +24717 +31341 +54424 +57340 +0 +6713 +20229 +28539 +36478 +44111 +48031 +53183 +56483 +0 +7735 +18829 +27932 +31472 +41760 +49541 +58044 +198141 +0 +8049 +29782 +37773 +44974 +54619 +57981 +0 +8887 +16896 +23712 +30131 +38160 +52927 +58574 +63117 +171730 +0 +7258 +20306 +28040 +35964 +45434 +54629 +57911 +0 +7683 +18695 +22989 +30100 +51679 +55146 +58538 +0 +7373 +26311 +30359 +37092 +43450 +53576 +57237 +0 +7846 +16873 +23830 +27160 +42087 +46741 +54819 +59374 +0 +8281 +19767 +23176 +30977 +40819 +47052 +56092 +59130 +0 +8039 +20037 +23590 +29470 +33139 +44605 +49252 +55888 +185689 +0 +8255 +23430 +28587 +32681 +37627 +43988 +49025 +57572 +196351 +0 +7614 +23214 +27464 +35737 +40894 +47889 +53604 +56386 +0 +2574 +8049 +16616 +25509 +32553 +37384 +44949 +51784 +55663 +58250 +0 +8223 +17090 +29771 +39088 +45030 +54353 +57788 +0 +7931 +19593 +26073 +37623 +46321 +50819 +56008 +59053 +0 +8628 +15936 +20502 +31802 +41890 +49229 +56296 +61879 +0 +3063 +7973 +19289 +31701 +36270 +49672 +55156 +57923 +0 +8900 +17913 +25790 +30057 +38128 +45798 +51492 +57256 +177345 +0 +6870 +15451 +21206 +27541 +32558 +42778 +54808 +58312 +0 +7373 +15159 +23352 +35400 +39321 +42744 +55157 +58807 +0 +7955 +17650 +21264 +27527 +31573 +45424 +49435 +56405 +184942 +0 +7932 +20051 +26385 +34872 +38671 +49247 +54583 +58111 +0 +8083 +17113 +23115 +32421 +36544 +41340 +52893 +56176 +59208 +0 +6986 +22592 +25789 +30400 +36874 +49257 +54926 +57778 +0 +9036 +23537 +28486 +34589 +38746 +53561 +56856 +0 +2618 +8206 +17845 +26230 +31938 +44760 +52454 +55286 +58693 +0 +7314 +16558 +29045 +41930 +53564 +56637 +59440 +0 +8317 +16320 +26816 +32798 +39668 +46871 +51524 +55766 +58698 +0 +8214 +20591 +28755 +41594 +47450 +51653 +55837 +198102 +0 +6692 +24069 +30503 +36062 +40163 +47505 +55257 +60392 +0 +8821 +17177 +23165 +28328 +40220 +55320 +58020 +0 +7581 +15409 +26124 +30373 +36307 +42383 +53488 +56079 +0 +8301 +16654 +24264 +32935 +40100 +47578 +52879 +61070 +0 +8314 +17349 +25755 +31799 +39248 +45738 +54413 +57143 +0 +7981 +15900 +20388 +26297 +31735 +40611 +45480 +53819 +57350 +0 +8334 +18695 +33047 +37512 +49699 +54238 +0 +9315 +16569 +21466 +27220 +30937 +37320 +45175 +56547 +59276 +0 +8326 +22994 +26524 +35542 +45826 +54864 +58928 +0 +8649 +16870 +24666 +32378 +35682 +46587 +50441 +54782 +61123 +188714 +0 +8785 +22981 +26187 +31726 +47831 +55506 +58690 +0 +7901 +16304 +20543 +27261 +41660 +50413 +55431 +58470 +0 +8368 +17630 +21182 +33087 +39225 +48088 +54961 +58896 +0 +8237 +19755 +23300 +27875 +34807 +39389 +56783 +0 +8179 +20551 +26448 +31395 +38484 +45100 +55852 +58561 +0 +8379 +16106 +20943 +27284 +37475 +50961 +54498 +57271 +0 +8067 +16963 +20539 +32507 +40392 +49950 +53002 +59602 +62975 +0 +7964 +16724 +29175 +38673 +46599 +52854 +56626 +0 +8198 +17419 +30427 +35374 +41876 +48962 +53673 +57112 +0 +8506 +16494 +22764 +26569 +40457 +50498 +53783 +58110 +0 +7375 +20636 +24184 +31136 +41255 +44325 +54760 +60890 +189099 +0 +7651 +18648 +24591 +32631 +45486 +49439 +53412 +58467 +61607 +0 +8718 +16073 +23051 +29601 +36655 +43912 +50283 +56940 +59594 +0 +8155 +16891 +26707 +30693 +45390 +49300 +53738 +58450 +0 +8687 +17758 +22650 +33610 +40096 +44677 +57404 +0 +8380 +25691 +32080 +39916 +50022 +56310 +59616 +0 +8254 +16638 +29201 +36697 +49325 +53265 +60594 +0 +7869 +18748 +27547 +30555 +34406 +41400 +49693 +57363 +60465 +0 +8372 +17933 +26722 +31525 +41862 +49943 +56877 +60839 +0 +7712 +17930 +27082 +31740 +35598 +44824 +49494 +53373 +57093 +0 +8390 +21504 +27742 +37150 +45037 +48277 +57164 +59942 +0 +8474 +22727 +30522 +35200 +41001 +50151 +54354 +57940 +0 +15713 +21875 +27484 +39447 +43514 +51511 +55849 +0 +8557 +17351 +28276 +32214 +39277 +45477 +54961 +60858 +0 +8722 +16203 +22702 +27591 +31427 +35111 +42973 +50035 +56340 +0 +8935 +16922 +21140 +24786 +32261 +39876 +46845 +55439 +59061 +0 +8136 +23450 +26769 +36367 +40325 +46964 +54345 +58217 +0 +8290 +17954 +26547 +37935 +43969 +55568 +63466 +0 +9601 +19950 +27295 +31815 +41175 +45394 +56855 +0 +8438 +21668 +27360 +31107 +35009 +39567 +48377 +55158 +58512 +0 +8685 +21341 +30095 +37136 +43976 +53050 +55890 +0 +7459 +16537 +26746 +31343 +37703 +41355 +56595 +59282 +0 +8651 +16472 +21816 +29084 +36478 +46416 +55548 +58484 +0 +8830 +17807 +21958 +28101 +32325 +38200 +47667 +50997 +0 +8983 +23303 +27790 +36329 +42088 +48140 +52653 +56746 +0 +8221 +18086 +28500 +33472 +42320 +50042 +55745 +0 +8399 +17177 +23690 +29052 +33963 +42650 +46185 +51353 +56709 +0 +8616 +16767 +23287 +29085 +36915 +45585 +49402 +56633 +0 +8765 +15383 +26950 +35250 +38989 +46940 +53786 +57200 +0 +8720 +17402 +26542 +30289 +42888 +56438 +59051 +0 +8338 +17400 +30127 +37519 +40797 +51578 +56371 +59105 +0 +9285 +17514 +25297 +31826 +35471 +48944 +56472 +59270 +0 +8964 +15337 +30683 +42105 +46397 +53246 +56703 +60139 +0 +9066 +16898 +23194 +33426 +39604 +43796 +56421 +59101 +0 +8584 +18100 +21870 +32483 +36294 +44231 +51661 +56817 +0 +7563 +18633 +25541 +29847 +42548 +46538 +55043 +57648 +0 +8436 +20297 +23368 +30641 +42150 +50904 +55973 +0 +9022 +23143 +29177 +35407 +45481 +51852 +57209 +0 +8684 +15082 +26156 +31250 +46331 +55516 +58096 +0 +7733 +15785 +21242 +32288 +40200 +47357 +51915 +56273 +59148 +0 +8263 +15754 +24319 +28446 +41032 +50990 +55777 +0 +8669 +16903 +25447 +31193 +40359 +48391 +53987 +57625 +0 +8554 +17201 +21942 +28115 +32779 +41621 +46391 +52671 +57734 +0 +8229 +17282 +25110 +30238 +35913 +40205 +49695 +53770 +56477 +0 +8605 +20608 +27351 +39984 +43305 +50230 +55338 +58335 +0 +7788 +17195 +22299 +37933 +49333 +53284 +56809 +0 +8275 +23207 +29721 +38021 +47128 +52349 +56291 +0 +8693 +16694 +20621 +28137 +32460 +41061 +50921 +55648 +58639 +0 +8030 +17512 +28649 +32395 +38178 +47271 +53202 +56406 +59997 +0 +9006 +20590 +26062 +35229 +39331 +46020 +54717 +57513 +0 +7942 +16810 +24125 +30315 +44566 +50863 +57519 +60055 +0 +8306 +23446 +32355 +46032 +51135 +53915 +58283 +0 +8935 +22454 +29033 +39960 +45846 +53497 +56570 +0 +7261 +17046 +21056 +28987 +37542 +45069 +51108 +57490 +62186 +0 +8151 +20782 +27128 +38035 +42195 +48234 +51966 +57983 +0 +9028 +15814 +20413 +24811 +30084 +36718 +42472 +54549 +58059 +0 +8936 +16799 +23251 +27568 +39171 +47205 +54087 +58781 +0 +7975 +17445 +27620 +31019 +37179 +41273 +49893 +53980 +58405 +0 +7941 +16721 +21999 +30214 +39343 +47792 +51382 +55390 +0 +7616 +16431 +27951 +37866 +47578 +52697 +56609 +60136 +0 +8235 +20207 +25486 +34153 +43645 +52540 +55561 +58614 +0 +8386 +20567 +28279 +33197 +38291 +42509 +48787 +56064 +58590 +0 +7947 +17535 +22411 +26719 +34049 +39580 +47634 +51116 +54927 +58260 +0 +7915 +18719 +23884 +27128 +35267 +41311 +44629 +52589 +58218 +0 +7729 +16500 +26052 +32075 +43573 +52077 +57814 +61121 +0 +7334 +21317 +35379 +40613 +45499 +55071 +58598 +0 +7581 +16953 +24308 +29182 +35299 +45995 +49575 +57536 +0 +503 +8372 +16177 +25665 +34995 +41302 +55812 +58258 +0 +8292 +16828 +24946 +30200 +34465 +46619 +50136 +55388 +59099 +0 +8673 +17467 +27177 +30346 +40639 +46557 +55162 +57854 +0 +8741 +20402 +27496 +34986 +45230 +52058 +55375 +58343 +0 +8260 +17849 +24834 +33037 +43284 +47522 +54044 +57446 +0 +9433 +17275 +23785 +32822 +41779 +47691 +53292 +57403 +0 +8324 +17520 +20719 +24945 +32169 +38912 +44061 +50748 +57312 +0 +8176 +17852 +26871 +31412 +36762 +47118 +51587 +56115 +59355 +0 +8351 +15726 +28216 +31771 +39913 +44423 +52105 +58494 +0 +8141 +16216 +24331 +29459 +36653 +41384 +48141 +56108 +58885 +0 +7965 +23297 +26747 +37817 +46512 +50450 +55516 +0 +9278 +24178 +31106 +37225 +44512 +50675 +56077 +59250 +0 +8647 +17024 +23417 +27586 +40984 +45903 +54612 +57073 +0 +8856 +19513 +22807 +27798 +40274 +46414 +53266 +57433 +0 +8859 +17543 +27998 +33296 +38968 +46442 +54721 +57485 +0 +3122 +8409 +22554 +26388 +32432 +47294 +52130 +57589 +0 +8612 +21610 +28872 +35431 +42969 +51450 +56446 +59308 +0 +7967 +19068 +33122 +39218 +45502 +54049 +58475 +0 +7909 +17061 +23652 +29666 +42303 +48116 +57004 +59679 +0 +8608 +16996 +29321 +37125 +42390 +55486 +58215 +0 +8132 +21266 +26671 +30998 +40314 +47057 +55259 +58784 +0 +8582 +17393 +24365 +36856 +40585 +51820 +56863 +0 +7588 +18304 +27448 +39232 +44247 +51912 +55468 +58678 +0 +8813 +17711 +22289 +34643 +40621 +50442 +55675 +58498 +0 +8923 +16993 +24029 +29942 +35775 +45678 +54558 +57206 +0 +8251 +21105 +24529 +31790 +39205 +47224 +54331 +57061 +195353 +0 +8257 +25854 +30406 +40398 +49017 +52786 +56330 +59427 +0 +8651 +18334 +28107 +32524 +38146 +49607 +55083 +57906 +0 +8982 +17558 +27675 +37098 +49251 +53286 +57184 +0 +7816 +20886 +27556 +32925 +37850 +43364 +53279 +56391 +59631 +0 +8230 +16309 +20482 +26375 +30164 +35701 +44440 +53908 +57709 +61647 +0 +8902 +19558 +27710 +35691 +47215 +51897 +56579 +59781 +0 +8220 +16420 +23378 +28430 +44737 +49067 +56022 +58814 +0 +8937 +18795 +26572 +36873 +42459 +53277 +57387 +0 +8092 +15836 +24966 +28771 +38015 +52334 +55661 +59781 +0 +8880 +26335 +35187 +46767 +55454 +58250 +0 +8412 +15944 +22209 +30241 +36585 +43753 +52036 +55556 +58454 +0 +8765 +19895 +26822 +31119 +36242 +45022 +53285 +56230 +0 +7602 +16954 +27092 +39982 +43274 +49260 +55688 +0 +8704 +17446 +24867 +35306 +42059 +45157 +52978 +57607 +0 +8006 +16466 +23803 +34197 +39633 +52283 +55984 +59673 +0 +9812 +17393 +25333 +29863 +35122 +47275 +52459 +55964 +60124 +0 +8540 +16202 +28980 +38752 +47194 +50941 +54426 +57376 +0 +7866 +17396 +21020 +30779 +40925 +46596 +55960 +58899 +0 +8846 +16513 +26637 +32155 +45387 +49917 +56387 +0 +8952 +17333 +21667 +31218 +34928 +43643 +47519 +52181 +56125 +0 +8139 +22117 +26011 +39450 +53387 +57632 +0 +8772 +16280 +26894 +33997 +40036 +44553 +52221 +57336 +60393 +0 +8432 +20795 +30076 +34940 +39256 +53274 +58507 +0 +8410 +17294 +22351 +27324 +32394 +38684 +45086 +50449 +55758 +58504 +0 +17450 +27537 +32102 +38813 +50652 +53716 +57405 +0 +8418 +17720 +29121 +33879 +37632 +45350 +49763 +54377 +57856 +0 +7975 +16528 +24275 +29740 +34934 +42131 +51205 +55094 +58278 +0 +7702 +17295 +27102 +32444 +36975 +41562 +53969 +57944 +0 +8044 +17107 +20655 +27354 +36997 +40874 +49444 +56487 +0 +8657 +20390 +24958 +31460 +37020 +41527 +52903 +55748 +0 +7762 +17129 +26752 +30872 +36321 +42420 +48069 +52887 +56017 +60302 +0 +8788 +17246 +23896 +30574 +41121 +47696 +54162 +57335 +60861 +0 +7968 +17684 +21664 +29922 +37176 +42023 +54979 +57934 +0 +9751 +17503 +24563 +29547 +33569 +45574 +48700 +54140 +59582 +0 +7953 +17144 +22030 +29933 +37190 +46000 +50327 +54841 +58332 +0 +8210 +17263 +24273 +31352 +41686 +45991 +53308 +58000 +0 +8047 +22785 +28627 +33531 +42576 +54148 +57444 +60380 +0 +7825 +20381 +26886 +33772 +38175 +54810 +57331 +0 +8557 +17166 +27394 +33489 +37380 +46695 +50489 +55261 +58558 +0 +7939 +18479 +26085 +38040 +47163 +53445 +57185 +0 +7882 +19570 +23724 +32141 +37233 +50331 +57857 +61315 +0 +8148 +17962 +23330 +27280 +36254 +41578 +45530 +50038 +56036 +0 +8607 +18551 +22818 +32010 +40599 +44144 +53853 +57014 +0 +8194 +17696 +22733 +27527 +34757 +42987 +56097 +59120 +0 +8756 +17643 +27758 +32782 +39212 +50996 +56051 +58627 +0 +7991 +16747 +24547 +32418 +47891 +56164 +60348 +0 +8770 +17464 +21750 +31458 +39552 +49558 +56435 +0 +8726 +20137 +24035 +31382 +36786 +40759 +48610 +52296 +56629 +59269 +0 +8537 +16813 +23268 +31124 +39475 +43360 +56394 +59155 +0 +7381 +25447 +30975 +34993 +38524 +54206 +57870 +0 +8058 +17359 +22383 +25915 +34642 +38471 +45686 +55152 +57404 +0 +8888 +16040 +23154 +28042 +40078 +43787 +51117 +55593 +59617 +0 +9185 +22785 +27445 +34065 +42570 +48932 +55411 +63636 +0 +8583 +17610 +25538 +32546 +40167 +44287 +54133 +57870 +0 +8994 +25939 +32018 +38867 +42830 +51600 +55524 +60408 +0 +8969 +16757 +24143 +27709 +35838 +41775 +52624 +56263 +0 diff --git a/project/previous_version/Ad081998_199_WideChopper_psth.txt b/project/previous_version/Ad081998_199_WideChopper_psth.txt new file mode 100644 index 0000000..067dad9 --- /dev/null +++ b/project/previous_version/Ad081998_199_WideChopper_psth.txt @@ -0,0 +1,1392 @@ +0 +11357 +17494 +24137 +30135 +36265 +43783 +49168 +0 +11557 +18522 +26267 +32527 +39541 +46701 +52353 +0 +11035 +19292 +26906 +33080 +39379 +48711 +0 +10344 +19099 +26135 +32604 +40261 +48825 +0 +12672 +20790 +28128 +35671 +42167 +50489 +0 +11721 +19182 +26396 +35512 +44323 +51207 +0 +11617 +19463 +25808 +32472 +40526 +47455 +0 +11495 +18685 +26657 +35143 +41961 +51589 +0 +12765 +20813 +29803 +39294 +47704 +56361 +0 +12261 +20261 +27853 +35546 +42904 +50776 +0 +13111 +20919 +29150 +36280 +43882 +50949 +0 +10022 +17908 +25241 +32060 +39596 +46368 +52966 +0 +13165 +20951 +28519 +35982 +44014 +55462 +0 +12103 +19213 +26908 +34039 +41594 +48407 +58302 +0 +12320 +19676 +29581 +37926 +44876 +53122 +0 +12006 +19479 +28269 +36074 +43610 +51916 +0 +12670 +21992 +29518 +36851 +45615 +54356 +0 +13416 +21453 +29308 +36724 +43478 +53118 +0 +13296 +21221 +28857 +37531 +46702 +55056 +0 +12273 +19923 +27770 +36717 +42983 +50613 +0 +12775 +21302 +28353 +37076 +43796 +53855 +0 +13580 +21557 +29876 +39478 +47145 +54814 +0 +12551 +19722 +27688 +35380 +43562 +52747 +0 +11942 +19271 +27183 +35966 +45935 +54076 +0 +13326 +21325 +28453 +36120 +42557 +53644 +0 +12932 +20618 +28133 +37703 +45106 +52842 +0 +11888 +19484 +26766 +34300 +41937 +50409 +0 +12999 +20926 +28826 +36282 +46296 +0 +13357 +24478 +31665 +39987 +48944 +0 +13288 +21575 +28383 +36159 +45087 +52043 +0 +12493 +20394 +27797 +35398 +46716 +53921 +0 +11947 +21219 +30175 +36880 +47029 +54860 +0 +13614 +23242 +30128 +38641 +46658 +56043 +0 +12022 +19928 +27540 +34275 +45059 +54404 +0 +13554 +22857 +30676 +39458 +47717 +54361 +0 +13118 +21376 +28161 +36167 +44368 +52487 +0 +13510 +21423 +31232 +40615 +47877 +55510 +0 +13513 +21393 +29243 +38755 +46807 +55664 +0 +12913 +20993 +28164 +37619 +44095 +54991 +0 +14176 +21228 +28865 +37207 +44766 +53915 +0 +12637 +20393 +28551 +37696 +48360 +0 +11683 +19555 +26765 +35169 +42350 +54508 +0 +11412 +19588 +26635 +33910 +41142 +49834 +0 +13149 +20735 +27807 +39537 +46080 +0 +11377 +20557 +28225 +34617 +43459 +52026 +0 +12998 +21461 +30856 +38998 +47143 +58026 +0 +12001 +19650 +26968 +34492 +42781 +49495 +0 +11321 +18289 +25462 +35749 +43461 +53603 +0 +11695 +18652 +26881 +35833 +43377 +52097 +0 +12895 +21051 +27173 +35106 +45627 +53097 +0 +9849 +17703 +24783 +33674 +41990 +48771 +0 +11562 +18579 +26343 +34645 +41863 +47789 +0 +12367 +21423 +28534 +35365 +46833 +0 +12971 +22652 +30019 +36988 +45402 +55157 +0 +11431 +18541 +25609 +32824 +39185 +47799 +0 +12651 +20889 +28286 +36361 +44724 +54153 +0 +13360 +22220 +29221 +37090 +44031 +0 +11690 +19473 +27472 +35665 +44108 +51142 +0 +10738 +17783 +26004 +33037 +40683 +49125 +0 +12087 +19553 +26566 +35890 +43738 +56136 +0 +13046 +20850 +28915 +36844 +47424 +55867 +0 +12280 +19963 +27585 +35781 +45689 +54049 +0 +11947 +19556 +27693 +34381 +42550 +51702 +0 +11794 +21435 +28156 +36090 +43281 +53070 +0 +13483 +21465 +28165 +37087 +47494 +54936 +0 +10926 +17736 +24543 +33391 +41556 +50632 +0 +12760 +19664 +26342 +34640 +44285 +51818 +0 +11875 +19002 +26558 +37703 +44683 +52634 +0 +12507 +20230 +26975 +35509 +43792 +51761 +0 +13980 +22630 +32886 +41843 +48742 +0 +12358 +20134 +26794 +34575 +42054 +52507 +0 +13791 +23222 +32056 +41114 +50170 +0 +13578 +22035 +30723 +39155 +47673 +54548 +0 +11692 +18900 +26488 +33950 +42776 +51879 +0 +11461 +19006 +25928 +34694 +43538 +56126 +0 +12570 +21593 +28661 +37003 +46992 +54728 +0 +11192 +20041 +26904 +35799 +45748 +52990 +0 +12525 +20979 +29578 +35997 +44342 +53093 +0 +12741 +20451 +28553 +40466 +50406 +0 +11403 +19051 +26705 +36342 +44305 +53893 +0 +12119 +19720 +28035 +36141 +44188 +53710 +0 +12263 +22319 +31422 +39421 +48093 +0 +11727 +19262 +26137 +33722 +41378 +49686 +0 +12242 +20265 +27349 +34952 +42330 +51148 +0 +13094 +20996 +28831 +37081 +44732 +53625 +0 +10062 +17326 +24944 +33169 +42760 +50357 +0 +13929 +22614 +30748 +37959 +46534 +53893 +0 +10656 +17958 +25138 +32566 +42583 +50193 +0 +14551 +22028 +29027 +38238 +47072 +56590 +0 +9830 +16828 +24480 +30732 +38935 +46619 +56043 +0 +12221 +21384 +28189 +34872 +44580 +53395 +0 +10997 +19339 +27601 +34495 +42349 +51731 +0 +13122 +21387 +30215 +38750 +46790 +57677 +0 +10953 +18575 +26375 +33108 +41262 +50850 +0 +12927 +21311 +28496 +35800 +45299 +0 +11233 +19087 +28364 +35284 +42695 +50647 +0 +12932 +21369 +29720 +37015 +42963 +53571 +0 +13099 +21207 +28774 +37389 +43933 +53939 +0 +11866 +19655 +25979 +32734 +39342 +46408 +53731 +0 +11876 +20715 +27337 +35503 +41532 +49478 +0 +11601 +20027 +27538 +35891 +42830 +54275 +0 +11505 +19660 +26570 +33554 +42338 +49683 +0 +13209 +20618 +28898 +35745 +43289 +51504 +0 +12331 +20062 +27098 +34661 +44310 +51360 +0 +12543 +19692 +28065 +35370 +43753 +52498 +0 +10625 +18852 +27105 +34237 +42112 +50662 +0 +11792 +19555 +26965 +35900 +43890 +52785 +0 +12526 +20255 +27899 +35075 +42715 +49730 +0 +11074 +19259 +26367 +34437 +40801 +48978 +0 +12547 +19924 +26363 +34124 +45018 +51768 +0 +12540 +21034 +28014 +35509 +42169 +51854 +0 +11381 +18663 +27604 +35463 +45397 +54266 +0 +11151 +18818 +26320 +34028 +41619 +50841 +0 +11951 +19678 +26912 +33645 +42423 +50510 +0 +10955 +18526 +25609 +33549 +43683 +55399 +0 +12882 +21342 +28373 +36695 +44563 +52378 +0 +12571 +20504 +28859 +36734 +45217 +52742 +0 +12289 +19576 +27834 +35277 +42689 +49320 +0 +12432 +21774 +28787 +37916 +46918 +0 +10593 +18560 +25928 +36097 +42764 +50149 +0 +12666 +20449 +27256 +34428 +43483 +51843 +0 +13424 +20960 +29008 +36833 +45225 +52513 +0 +12000 +19343 +28483 +38651 +44899 +53632 +0 +11020 +18831 +26776 +34336 +40937 +50163 +0 +12060 +19014 +26598 +36364 +45196 +0 +12336 +20306 +27731 +34993 +43310 +51930 +0 +11428 +19433 +26115 +34760 +42675 +50631 +0 +9856 +18195 +25270 +33698 +40672 +47023 +54653 +0 +12355 +19828 +27058 +37100 +44454 +54744 +0 +12173 +20018 +27843 +35117 +43500 +50558 +0 +12005 +19823 +27013 +34430 +42844 +51344 +0 +11850 +21051 +28564 +36079 +42147 +52961 +0 +11195 +20412 +29146 +36073 +43651 +51823 +0 +11641 +19772 +26780 +34139 +42809 +50716 +0 +10792 +18071 +25980 +33400 +40877 +50676 +0 +10880 +17659 +24619 +32623 +41509 +49806 +0 +12459 +21186 +28289 +35969 +46862 +54142 +0 +11003 +18244 +26411 +35604 +44897 +52931 +0 +12566 +21339 +29256 +38353 +46735 +53689 +0 +11430 +19194 +29117 +37116 +46740 +54604 +0 +12102 +19672 +26420 +36892 +45111 +52557 +0 +11604 +19476 +26790 +37738 +47407 +57368 +0 +10859 +17931 +25978 +35213 +43642 +50823 +0 +11908 +19826 +28385 +35691 +43774 +51844 +0 +10341 +18327 +25710 +34369 +43106 +50355 +0 +9674 +17027 +24009 +31883 +40622 +47975 +0 +11133 +19101 +26925 +33955 +43021 +50384 +0 +12775 +22833 +29544 +37424 +46412 +53805 +0 +12104 +19934 +27007 +34748 +42967 +0 +11337 +18225 +26546 +34556 +43760 +50511 +0 +12926 +19878 +26826 +35001 +43578 +53542 +0 +11218 +18269 +26578 +34043 +42875 +50825 +0 +10882 +18478 +25598 +32941 +40755 +47314 +0 +13373 +21668 +28113 +36972 +46667 +54321 +0 +10666 +18394 +25944 +35413 +42697 +48934 +0 +11741 +18560 +26937 +34409 +43216 +50403 +0 +10426 +17884 +25664 +33524 +43379 +52053 +0 +11400 +18528 +25453 +33245 +42044 +49855 +0 +11131 +18735 +26254 +32858 +45474 +53535 +0 +10924 +18027 +26354 +34212 +40776 +47961 +0 +11325 +19210 +26980 +34904 +44840 +52053 +0 +11068 +20844 +27187 +36192 +44009 +54244 +0 +12135 +20595 +27931 +35167 +41780 +51230 +0 +12623 +20657 +28733 +35980 +43981 +51962 +0 +12236 +19370 +28274 +35391 +41800 +51373 +0 +11764 +19740 +28369 +36237 +44372 +50678 +0 +11538 +19176 +26723 +35214 +43892 +52925 +0 +11964 +20351 +26348 +34365 +42872 +50230 +0 +10385 +17768 +26220 +34370 +42049 +50729 +0 +11801 +18279 +25698 +33417 +41393 +48798 +0 +11244 +19056 +26933 +34714 +42653 +49495 +0 +11999 +20364 +27976 +35164 +43801 +53080 +0 +12018 +20529 +27212 +34153 +41183 +49101 +0 +11888 +21685 +28963 +35811 +42485 +52995 +0 +11128 +18699 +26662 +34341 +45277 +53201 +0 +12337 +21046 +28089 +36343 +47782 +54711 +0 +11610 +18506 +29387 +36476 +45051 +0 +12153 +20093 +28096 +34384 +40972 +50511 +0 +11481 +19675 +27218 +35265 +41953 +50355 +0 +13025 +20698 +29213 +36097 +45889 +52850 +0 +11466 +19201 +26364 +36929 +46212 +54186 +0 +13611 +21908 +29923 +40018 +47951 +0 +12267 +20930 +28632 +36884 +49476 +0 +11931 +20196 +28307 +37447 +44769 +53125 +0 +11022 +18102 +24568 +30757 +38061 +46737 +53625 +0 +11641 +20692 +27868 +35116 +44433 +52890 +0 +11744 +20046 +27153 +34450 +42217 +48697 +0 +9637 +17380 +25405 +33274 +41364 +47810 +0 +10343 +18416 +25818 +32883 +40037 +51732 +0 +9899 +17554 +23620 +30358 +38004 +47837 +53619 +0 +11735 +19329 +27802 +35144 +42641 +50684 +0 +12892 +20236 +28478 +36441 +44333 +51170 +0 +11738 +19748 +27782 +35353 +44494 +52140 +0 +12490 +21405 +28990 +37491 +47080 +55268 +0 +11782 +20080 +27422 +36561 +45447 +53287 +0 +12635 +21098 +30133 +39174 +47828 +55953 +0 +12334 +19636 +27894 +34812 +44545 +52583 +0 +11288 +20065 +27109 +34629 +42454 +52779 +0 +13323 +24855 +33551 +42334 +49586 +0 +10842 +18996 +26626 +33784 +42577 +50864 +0 diff --git a/project/previous_version/histgen.py b/project/previous_version/histgen.py new file mode 100644 index 0000000..fe120db --- /dev/null +++ b/project/previous_version/histgen.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +# In[1]: + + +import os +import sys +import pyqtgraph as pg +import numpy as np + + +# In[7]: + + +dir_path = os.path.abspath("") + +win = pg.GraphicsWindow() +win.setBackground("w") +p1 = win.addPlot( + title="Pauser PSTH", + row=0, + col=0, + labels={"bottom": "T (ms)", "left": "# of spikes"}, +) +p2 = win.addPlot( + title="Buildup PSTH", + row=1, + col=0, + labels={"bottom": "T (ms)", "left": "# of spikes"}, +) +p3 = win.addPlot( + title="Wide Chopper PSTH", + row=2, + col=0, + labels={"bottom": "T (ms)", "left": "# of spikes"}, +) + +# In[ ]: + +bins = np.arange(0, 80, 0.5) + +PB_spike_data = [] +with open("Ad081098_065_PauserBuildup_psth.txt", "r+") as df: + for x in df: + x = x.strip("\n").strip() + x = float(x) * 1e-3 + if x: + PB_spike_data.append(x) +histogram, binedges = np.histogram(PB_spike_data, bins) +p1.plot( + binedges, + histogram, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, +) +B_spike_data = [] +with open("Ad041599_062_Buildup_psth.txt", "r+") as df: + for x in df: + x = x.strip("\n").strip() + x = float(x) * 1e-3 + if x: + B_spike_data.append(x) +histogram, binedges = np.histogram(B_spike_data, bins) +p2.plot( + binedges, + histogram, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, +) +C_spike_data = [] +with open("Ad081998_199_WideChopper_psth.txt", "r+") as df: + for x in df: + x = x.strip("\n").strip() + x = float(x) * 1e-3 + if x: + C_spike_data.append(x) +histogram, binedges = np.histogram(C_spike_data, bins) +p3.plot( + binedges, + histogram, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, +) +# In[ ]: + + +win.show() +print("finished") +if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/project/previous_version/test_network_PSTH.py b/project/previous_version/test_network_PSTH.py new file mode 100644 index 0000000..073df27 --- /dev/null +++ b/project/previous_version/test_network_PSTH.py @@ -0,0 +1,451 @@ +""" +Layout: + + if __name__==__main__ handles cmd args, instantiates test, runs it, displays results + + run_trial(): defines a model and then runs the test using preset params in hoc and return the info to the class + SGCInputTest: + __init__ : defines many static variables + run(): calls delivers information to the run_trial() function and the recieved information of a single run + back and stores it as an exstensible list in the class + show(): displays graphs depending on the graph options selected below it and displayed in a printout + +""" +import argparse +import numpy as np +import pyqtgraph as pg +from neuron import h +from cnmodel.protocols import Protocol +from cnmodel import cells +from cnmodel.util import sound +from cnmodel.util import custom_init +import cnmodel.util.pynrnutilities as PU +from cnmodel import data + +try: + from tqdm import trange +except ImportError: + raise ImportError("Please 'pip install tqdm' to allow for progress bar") + +species = "rat" # tables for other species do not yet exist + + +def run_trial(cell, info): + """ + info is a dict + """ + assert cell == "pyramidal" + post_cell = cells.Pyramidal.create(species=species) + inhib_cell = cells.Tuberculoventral.create() + inhib_cell2 = cells.Tuberculoventral.create() + # dstell = cells.DStellate.create() + pre_cells = [] + synapses = [] + inhib_synapses = [] + for nsgc in range(48): + # attach to pyramidal cell + pre_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(pre_cells[-1].connect(post_cell, type=info["synapse_type"])) + pre_cells[-1].set_sound_stim( + info["stim"], seed=info["seed"] + nsgc, simulator=info["simulator"] + ) + synapses[ + -1 + ].terminal.relsite.Dep_Flag = False # no depression in these simulations + for nsgc in range(16): + pre_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + inhib_synapses.append( + pre_cells[-1].connect(inhib_cell, type=info["synapse_type"]) + ) + inhib_synapses.append( + pre_cells[-1].connect(inhib_cell2, type=info["synapse_type"]) + ) + pre_cells[-1].set_sound_stim( + info["stim"], seed=info["seed"] + nsgc + 48, simulator=info["simulator"] + ) + synapses[ + -1 + ].terminal.relsite.Dep_Flag = False # no depression in these simulations + # for nsgc in range(20): + # pre_cells.append(cells.DummySGC(cf=info['cf'], sr=info['sr'])) + # inhib_synapses.append(pre_cells[-1].connect(dstell, type=info['synapse_type'])) + # pre_cells[-1].set_sound_stim(info['stim'], seed=info['seed'] + nsgc + 16 + 48, simulator=info['simulator']) + # synapses[-1].terminal.relsite.Dep_Flag = False # no depression in these simulations + for _ in range(21): + inhib_synapses.append(inhib_cell.connect(post_cell, type="simple")) + inhib_synapses.append(inhib_cell2.connect(post_cell, type="simple")) + # for _ in range(15): + # inhib_synapses.append(inhib_cell.connect(dstell, type='simple')) + + Vm = h.Vector() + Vm.record(post_cell.soma(0.5)._ref_v) + Vmtb = h.Vector() + Vmtb.record(inhib_cell.soma(0.5)._ref_v) + rtime = h.Vector() + rtime.record(h._ref_t) + h.tstop = 1e3 * info["run_duration"] # duration of a run + h.celsius = info["temp"] + h.dt = info["dt"] + post_cell.cell_initialize() + info["init"]() + h.t = 0.0 + h.run() + # package data + pre_cells_data = [x._spiketrain for x in pre_cells] + Vm_list = np.array(Vm) + Vmtb_list = np.array(Vmtb) + time_list = np.array(rtime) + # clean up + del ( + pre_cells, + synapses, + inhib_cell, + inhib_synapses, + Vm, + Vmtb, + post_cell, + inhib_cell2, + info, + ) + + return { + "time": time_list, + "vm": Vm_list, + "pre_cells": pre_cells_data, + "vmtb": Vmtb_list, + } + + +class SGCTestPSTH(Protocol): + """ + Tests a Single cell with input recieved from the SGC + + __init__: almost all parameters can be modified + run(): simply loops over the run_trial() function and stores the results just + show(): constructs the graphs using other functions + + """ + + def __init__( + self, + temp=34.0, + seed=2918, + nrep=10, + stimulus="tone", + simulator="cochlea", + n_sgc=12, + debug=True, + cell="pyramidal", + ): + """ + :param temp: (float) must be at 34 for default pyramidal cells + :param dt: (float) determine hoc resolution + :param seed: (int) contributes to randomization, needs to be changed to see different results (reduce this + number if you keep getting a timeout error + :param nrep: (int) number of presentations !!must be changed in the __name__ function if not calling from cmd line!! + :param stimulus: (str) must be 'tone' + :param simulator: (str) currently using cochlea instead of matlab + :param n_sgc:(int) This is the number of SGC fibers that connect to the post synaptic cell + :param debug: (bool) controls most of the terminal printouts is on by default + :param cell: (str) cell type !!must be changed in the __name__ function if not calling from cmd line!! + """ + super().__init__() + assert stimulus == "tone" + assert cell in [ + "bushy", + "tstellate", + "octopus", + "dstellate", + "tuberculoventral", + "pyramidal", + ] + self.debug = debug + self.nrep = nrep + self.stimulus = stimulus + self.run_duration = 0.30 # in seconds + self.pip_duration = 0.05 # in seconds + self.pip_start = [0.1] # in seconds + self.Fs = 100e3 # in Hz + self.f0 = 13000.0 # stimulus in Hz + self.cf = 13000.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 0.0 # % mod depth, Hz + self.dbspl = 40.0 + self.simulator = simulator + self.sr = 2 # set SR group + self.seed = seed + self.temp = temp + self.dt = 0.025 + self.cell = cell + self.synapse_type = "multisite" + + if self.stimulus == "tone": + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + + if not n_sgc: + n_sgc = data.get( + "convergence", species="mouse", post_type=self.cell, pre_type="sgc" + )[0] + self.n_sgc = int(np.round(n_sgc)) + # convert nS to uS for NEURON + + self.vms = [None for n in range(self.nrep)] + self.vmtbs = [None for n in range(self.nrep)] + self.synapses = [None for n in range(self.nrep)] + self.pre_cells = [None for n in range(self.nrep)] + self.time = [None for n in range(self.nrep)] + # debug function reports a print out of various information about the run + if self.debug: + print("SGCInputTest Created") + print() + print("Test parameters") + print("#" * 70) + print(f"Running test of {cell} cell synapse with Simulated SGC fibers") + print() + print(f"Run Conditions: Run Time: {self.run_duration}s,") + print(f" Run Temp: {self.temp} ") + print(f" Sgc Connections {n_sgc}") + print(f" Number of Presentations: {nrep}") + print() + print(f"Stimulus Conditions: Type: {stimulus}") + print(f" Stim Duration: {self.pip_duration}s") + print(f" Characteristic F: {self.cf}hz") + print(f" Stim Start:{str(self.pip_start)}s") + + def run(self): + super().run() + info = { + "n_sgc": self.n_sgc, + "stim": self.stim, + "simulator": self.simulator, + "cf": self.cf, + "sr": self.sr, + "seed": self.seed, + "run_duration": self.run_duration, + "synapse_type": self.synapse_type, + "temp": self.temp, + "dt": self.dt, + "init": custom_init, + } + for nr in trange(self.nrep): + info["seed"] = self.seed + self.n_sgc + (nr * (48 + 16 + 20)) + res = run_trial(self.cell, info) + # res contains: {'time': time, 'vm': list(Vm), 'pre_cells': pre_cells._spiketrain,'vmtb': list(Vmtb)} + self.pre_cells[nr] = res["pre_cells"] + self.time[nr] = res["time"] + self.vms[nr] = res["vm"] + self.vmtbs[nr] = res["vmtb"] + + def show(self): + """ + Creates a single page graph that contains all of the graphs based on the graphical functions in the class + + """ + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + p1 = self.stimulus_graph() + p2 = self.an_spike_graph() + p3 = self.pyram_spike_graph() + p4 = self.voltage_graph() + p5 = self.tb_cell_spike_graph() + p6 = ( + self.an_psth_graph() + ) # requires that an_spikes_graph() has been called before + p7 = ( + self.cell_psth_graph() + ) # requires that cell_spikes_graph() has been called before + + # links x axis + p1.setXLink(p1) + p2.setXLink(p1) + p3.setXLink(p1) + p4.setXLink(p1) + p5.setXLink(p1) + p6.setXLink(p1) + p7.setXLink(p1) + self.win.show() + if self.debug: + print("finished") + + ############# Graph options to be included in the show() method ################### + def stimulus_graph(self): + p1 = self.win.addPlot( + title="Stimulus", row=0, col=0, labels={"bottom": "T (ms)", "left": "V"} + ) + p1.plot(self.stim.time * 1000, self.stim.sound, pen=pg.mkPen("k", width=0.75)) + return p1 + + def an_spike_graph(self): + p2 = self.win.addPlot( + title="AN spikes", + row=1, + col=0, + labels={"bottom": "T (ms)", "left": "AN spikes (first trial)"}, + ) + self.all_xan = [] + for nr in range(self.nrep): + xan = [] + yan = [] + for k in range(len(self.pre_cells[nr])): + r = self.pre_cells[nr][k] + xan.extend(r) + self.all_xan.extend(r) + yr = k + np.zeros_like(r) + 0.2 + yan.extend(yr) + c = pg.PlotCurveItem() + xp = np.repeat(np.array(xan), 2) + yp = np.repeat(np.array(yan), 2) + yp[1::2] = yp[::2] + 0.6 + c.setData( + xp.flatten(), + yp.flatten(), + connect="pairs", + width=1.0, + pen=pg.mkPen("k", width=1.5), + ) + p2.addItem(c) + + return p2 + + def pyram_spike_graph(self): + p3 = self.win.addPlot( + title="Pyramidal Spikes", + row=2, + col=0, + labels={"bottom": "T (ms)", "left": "Trial #"}, + ) + xcn = [] + ycn = [] + for k in range(self.nrep): + bspk = PU.findspikes(self.time[k], self.vms[k], -35.0) + xcn.extend(bspk) + yr = k + np.zeros_like(bspk) + 0.2 + ycn.extend(yr) + d = pg.PlotCurveItem() + xp = np.repeat(np.array(xcn), 2) + yp = np.repeat(np.array(ycn), 2) + yp[1::2] = yp[::2] + 0.6 + d.setData( + xp.flatten(), yp.flatten(), connect="pairs", pen=pg.mkPen("k", width=1.5) + ) + self.xcn = xcn + self.ycn = ycn + p3.addItem(d) + + return p3 + + def voltage_graph(self): + p4 = self.win.addPlot( + title="%s Vm" % self.cell, + row=0, + col=1, + labels={"bottom": "T (ms)", "left": "Vm (mV)"}, + ) + if self.nrep > 3: + display = 3 + else: + display = self.nrep + for nr in range(display): + p4.plot( + self.time[nr], + self.vms[nr], + pen=pg.mkPen(pg.intColor(nr, self.nrep), hues=self.nrep, width=1.0), + ) + return p4 + + def tb_cell_spike_graph(self): + p5 = self.win.addPlot( + title="Tuberculoventral Spikes", + row=3, + col=0, + labels={"bottom": "T (ms)", "left": "Trial #"}, + ) + xtcn = [] + ytcn = [] + for k in range(self.nrep): + bspk = PU.findspikes(self.time[k], self.vmtbs[k], -35.0) + xtcn.extend(bspk) + yr = k + np.zeros_like(bspk) + 0.2 + ytcn.extend(yr) + d = pg.PlotCurveItem() + xp = np.repeat(np.array(xtcn), 2) + yp = np.repeat(np.array(ytcn), 2) + yp[1::2] = yp[::2] + 0.6 + d.setData( + xp.flatten(), yp.flatten(), connect="pairs", pen=pg.mkPen("k", width=1.5) + ) + p5.addItem(d) + + return p5 + + def an_psth_graph(self): + p6 = self.win.addPlot( + title="AN PSTH", + row=1, + col=1, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + bins = np.arange(50, 200, 1) + (hist, binedges) = np.histogram(self.all_xan, bins) + curve6 = p6.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p6 + + def cell_psth_graph(self): + p7 = self.win.addPlot( + title="Pyramidal PSTH", + row=2, + col=1, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + bins = np.arange(50, 200, 1) + (hist, binedges) = np.histogram(self.xcn, bins) + curve7 = p7.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p7 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Compute AN only PSTH in postsynaptic cell" + ) + parser.add_argument( + "-n", + "--nrep", + type=int, + dest="nrep", + default=10, + help="Set number of repetitions", + ) + + args = parser.parse_args() + + nrep = args.nrep + prot = SGCTestPSTH(nrep=50) + prot.run() + prot.show() + + import sys + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/project/previous_version/test_single_cell_psth.py b/project/previous_version/test_single_cell_psth.py new file mode 100644 index 0000000..4bf60f4 --- /dev/null +++ b/project/previous_version/test_single_cell_psth.py @@ -0,0 +1,412 @@ +""" +Layout: + + if __name__==__main__ handles cmd args, instantiates test, runs it, displays results + + run_trial(): defines a model and then runs the test using preset params in hoc and return the info to the class + SGCInputTest: + __init__ : defines many static variables + run(): calls delivers information to the run_trial() function and the recieved information of a single run + back and stores it as an exstensible list in the class + show(): displays graphs depending on the graph options selected below it and displayed in a printout + +""" +import argparse +import numpy as np +import pyqtgraph as pg +from neuron import h +from cnmodel.protocols import Protocol +from cnmodel import cells +from cnmodel.util import sound +from cnmodel.util import custom_init +import cnmodel.util.pynrnutilities as PU +from cnmodel import data + +try: + from tqdm import trange +except ImportError as err: + raise ImportError("Please 'pip install tqdm' to allow for progress bar") + +species = "rat" # tables for other species do not yet exist + + +def run_trial(cell, info): + """ + info is a dict + """ + if cell == "bushy": + post_cell = cells.Bushy.create(species=species) + elif cell == "tstellate": + post_cell = cells.TStellate.create(species=species) + elif cell == "octopus": + post_cell = cells.Octopus.create(species=species) + elif cell == "dstellate": + post_cell = cells.DStellate.create(species=species) + elif cell == "tuberculoventral": + post_cell = cells.DStellate.create(species=species) + elif cell == "pyramidal": + post_cell = cells.Pyramidal.create(species=species) + else: + raise ValueError("cell %s is not yet implemented for PSTH testing" % cell) + pre_cells = [] + synapses = [] + for nsgc, sgc in enumerate(range(info["n_sgc"])): + pre_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(pre_cells[-1].connect(post_cell, type=info["synapse_type"])) + synapses[ + -1 + ].terminal.relsite.Dep_Flag = False # no depression in these simulations + pre_cells[-1].set_sound_stim( + info["stim"], seed=info["seed"] + nsgc, simulator=info["simulator"] + ) + + # stim = insert_current_clamp(post_cell.soma) + Vm = h.Vector() + Vm.record(post_cell.soma(0.5)._ref_v) + rtime = h.Vector() + rtime.record(h._ref_t) + h.tstop = 1e3 * info["run_duration"] # duration of a run + h.celsius = info["temp"] + h.dt = info["dt"] + post_cell.cell_initialize() + info["init"](v_init=-60) + h.t = 0.0 + h.run() + pre_cells_data = [x._spiketrain for x in pre_cells] + + return {"time": np.array(rtime), "vm": list(Vm), "pre_cells": pre_cells_data} + + +# stim = insert_current_clamp(post_cell.soma) +def insert_current_clamp(sec): + """ + :param sec: to attach too + dur: ms + amp: nA + delay: ms + :return: stim needs to be put in a variable to stay alive + """ + stim = h.IClamp(0.5, sec=sec) + stim.dur = 90 + stim.amp = -0.2 + stim.delay = 2 + return stim + + +class SGCTestPSTH(Protocol): + """ + Tests a Single cell with input recieved from the SGC + + __init__: almost all parameters can be modified + run(): simply loops over the run_trial() function and stores the results just + show(): constructs the graphs using other functions + + """ + + def __init__( + self, + temp=34.0, + seed=5908035, + nrep=10, + stimulus="tone", + simulator="cochlea", + n_sgc=12, + debug=True, + cell="bushy", + ): + """ + :param temp: (float) must be at 34 for default pyramidal cells + :param dt: (float) determine hoc resolution + :param seed: (int) contributes to randomization, needs to be changed to see different results (reduce this + number if you keep getting a timeout error + :param nrep: (int) number of presentations !!must be changed in the __name__ function if not calling from cmd line!! + :param stimulus: (str) must be 'tone' + :param simulator: (str) currently using cochlea instead of matlab + :param n_sgc:(int) This is the number of SGC fibers that connect to the post synaptic cell + :param debug: (bool) controls most of the terminal printouts is on by default + :param cell: (str) cell type !!must be changed in the __name__ function if not calling from cmd line!! + """ + super().__init__() + assert stimulus == "tone" + assert cell in [ + "bushy", + "tstellate", + "octopus", + "dstellate", + "tuberculoventral", + "pyramidal", + ] + self.debug = debug + self.nrep = nrep + self.stimulus = stimulus + self.run_duration = 0.30 # in seconds + self.pip_duration = 0.05 # in seconds + self.pip_start = [0.1] # in seconds + self.Fs = 100e3 # in Hz + self.f0 = 13000.0 # stimulus in Hz + self.cf = 13000.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 0.0 # % mod depth, Hz + self.dbspl = 55.0 + self.simulator = simulator + self.sr = 2 # set SR group + self.seed = seed + self.temp = temp + self.dt = 0.025 + self.cell = cell + self.synapse_type = "multisite" + + if self.stimulus == "tone": + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=2.5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + + if not n_sgc: + n_sgc = data.get( + "convergence", species="mouse", post_type=self.cell, pre_type="sgc" + )[0] + self.n_sgc = int(np.round(n_sgc)) + # convert nS to uS for NEURON + + self.vms = [None for n in range(self.nrep)] + self.synapses = [None for n in range(self.nrep)] + self.pre_cells = [None for n in range(self.nrep)] + self.time = [None for n in range(self.nrep)] + # debug function reports a print out of various information about the run + if self.debug: + print("SGCInputTest Created") + print() + print("Test parameters") + print("#" * 70) + print(f"Running test of {cell} cell synapse with Simulated SGC fibers") + print() + print(f"Run Conditions: Run Time: {self.run_duration}s,") + print(f" Run Temp: {self.temp} ") + print(f" Sgc Connections {n_sgc}") + print(f" Number of Presentations: {nrep}") + print() + print(f"Stimulus Conditions: Type: {stimulus}") + print(f" Stim Duration: {self.pip_duration}s") + print(f" Characteristic F: {self.cf}hz") + print(f" Stim Start:{str(self.pip_start)}s") + + def run(self): + super().run() + info = { + "n_sgc": self.n_sgc, + "stim": self.stim, + "simulator": self.simulator, + "cf": self.cf, + "sr": self.sr, + "seed": self.seed, + "run_duration": self.run_duration, + "synapse_type": self.synapse_type, + "temp": self.temp, + "dt": self.dt, + "init": custom_init, + } + for nr in trange(self.nrep): + info["seed"] = self.seed + self.n_sgc + nr + 3 + res = run_trial(self.cell, info) + # res contains: {'time': time, 'vm': list(Vm), 'pre_cells': pre_cells._spiketrain,} + self.pre_cells[nr] = res["pre_cells"] + self.time[nr] = res["time"] + self.vms[nr] = res["vm"] + + def show(self): + """ + Creates a single page graph that contains all of the graphs based on the graphical functions in the class + + """ + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + p1 = self.stimulus_graph() + p2 = self.an_spikes_graph() + p3 = self.cell_spikes_graph() + p4 = self.voltage_graph() + p6 = ( + self.an_psth_graph() + ) # requires that an_spikes_graph() has been called before + p7 = ( + self.cell_psth_graph() + ) # requires that cell_spikes_graph() has been called before + + # links x axis + p1.setXLink(p1) + p2.setXLink(p1) + p3.setXLink(p1) + p4.setXLink(p1) + self.win.show() + if self.debug: + print("finished") + + ############# Graph options to be included in the show() method ################### + def stimulus_graph(self): + p1 = self.win.addPlot( + title="Single Tone Stimulus", row=0, col=0, labels={"bottom": "T (ms)"} + ) + p1.plot(self.stim.time * 1000, self.stim.sound, pen=pg.mkPen("k", width=0.75)) + return p1 + + def an_spikes_graph(self): + p2 = self.win.addPlot( + title="AN spikes", + row=1, + col=0, + labels={"bottom": "T (ms)", "left": "AN spikes (first trial)"}, + ) + self.all_xan = [] + for nr in range(self.nrep): + xan = [] + yan = [] + for k in range(len(self.pre_cells[nr])): + r = self.pre_cells[nr][k] + xan.extend(r) + self.all_xan.extend(r) + yr = k + np.zeros_like(r) + 0.2 + yan.extend(yr) + c = pg.PlotCurveItem() + xp = np.repeat(np.array(xan), 2) + yp = np.repeat(np.array(yan), 2) + yp[1::2] = yp[::2] + 0.6 + c.setData( + xp.flatten(), + yp.flatten(), + connect="pairs", + width=1.0, + pen=pg.mkPen("k", width=1.5), + ) + p2.addItem(c) + + return p2 + + def cell_spikes_graph(self): + p3 = self.win.addPlot( + title="Pyramidal Cell Spikes", + row=2, + col=0, + labels={"bottom": "T (ms)", "left": "Trial #"}, + ) + xcn = [] + ycn = [] + for k in range(self.nrep): + bspk = PU.findspikes(self.time[k], self.vms[k], -35.0) + xcn.extend(bspk) + yr = k + np.zeros_like(bspk) + 0.2 + ycn.extend(yr) + d = pg.PlotCurveItem() + xp = np.repeat(np.array(xcn), 2) + yp = np.repeat(np.array(ycn), 2) + yp[1::2] = yp[::2] + 0.6 + d.setData( + xp.flatten(), yp.flatten(), connect="pairs", pen=pg.mkPen("k", width=1.5) + ) + self.xcn = xcn + self.ycn = ycn + p3.addItem(d) + + return p3 + + def voltage_graph(self): + p4 = self.win.addPlot( + title="Pyramidal Vm", + row=0, + col=1, + labels={"bottom": "T (ms)", "left": "Vm (mV)"}, + ) + if self.nrep > 5: + display = 3 + else: + display = self.nrep + for nr in range(display): + p4.plot( + self.time[nr], + self.vms[nr], + pen=pg.mkPen(pg.intColor(nr, self.nrep), hues=self.nrep, width=1.0), + ) + return p4 + + def an_psth_graph(self): + p6 = self.win.addPlot( + title="AN PSTH", + row=1, + col=1, + labels={"bottom": "T (ms)", "left": "# of Spikes(0.75ms"}, + ) + bins = np.arange(60, 200, 0.5) + (hist, binedges) = np.histogram(self.all_xan, bins) + curve6 = p6.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p6 + + def cell_psth_graph(self): + p7 = self.win.addPlot( + title="Pyramidal PSTH", + row=2, + col=1, + labels={"bottom": "T (ms)", "left": "# of Spikes(0.75ms)"}, + ) + bins = np.arange(60, 200, 0.5) + (hist, binedges) = np.histogram(self.xcn, bins) + curve7 = p7.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p7 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Compute AN only PSTH in postsynaptic cell" + ) + parser.add_argument( + type=str, + dest="cell", + default="pyramidal", + choices=[ + "bushy", + "tstellate", + "dstellate", + "octopus", + "tuberculoventral", + "pyramidal", + ], + help="Select target cell", + ) + parser.add_argument( + "-n", + "--nrep", + type=int, + dest="nrep", + default=1, + help="Set number of repetitions", + ) + + args = parser.parse_args() + + cell = args.cell + nrep = args.nrep + prot = SGCTestPSTH(nrep=100, cell=cell) + prot.run() + prot.show() + + import sys + + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() diff --git a/project/previous_version/tone_pyram_sgc_input_PSTH2.7.py b/project/previous_version/tone_pyram_sgc_input_PSTH2.7.py new file mode 100644 index 0000000..1b15768 --- /dev/null +++ b/project/previous_version/tone_pyram_sgc_input_PSTH2.7.py @@ -0,0 +1,397 @@ +""" +The Assumptions of this cell are modified to the test_sgc_input_PSTH.py but should be identical in function + +synapse type = simple +synapse type = single +species = mouse +sgc = dummy SGC lacks cell body +cell connection = pyramidal + + + +""" + +import sys +import numpy as np +import pyqtgraph as pg +from neuron import h +from cnmodel.protocols import Protocol +from cnmodel import cells +from cnmodel.util import sound +from cnmodel.util import custom_init +import cnmodel.util.pynrnutilities as PU +from cnmodel import data + +synapseType = "simple" # no other options exist +species = "mouse" # tables for other species do not yet exist + + +def main(): + prot = SGCInputTestPSTH(seed=1233451898) + prot.run() + prot.show() + if sys.flags.interactive == 0: + pg.QtGui.QApplication.exec_() + + +class SGCInputTestPSTH(Protocol): + def __init__(self, temp=34.0, dt=0.025, seed=575982035): + """ + :param temp: Celcius + :param dt: resolution in ms + :param seed: seed should be generated randomly + + Pre-determined variables that can be changed by editing the value next to them + + AMPA_gmax and n_sgc = loaded from data that is kept in cnmodel/data dir + + Defines a single tone parameters with which to test the neuron complex + """ + # super only defines reset function + super(SGCInputTestPSTH, self).__init__() + # + self.temp = temp + self.dt = dt + self.seed = seed + self.simulator = "cochlea" + # predetermined variables + self.cell = "pyramidal" + self.nrep = 50 # number of repetitions + self.stimulus = "tone" + self.Fs = 100e3 # in Hz + self.f0 = 4000.0 # stimulus in Hz + self.cf = 4000.0 # SGCs in Hz + self.fMod = 100.0 # mod freq, Hz + self.dMod = 0.0 # % mod depth, Hz + self.dbspl = 50.0 + self.sr = 2 # set SR group + + # variables loaded from data + AMPA_gmax, n_sgc = self.load_variables_from_data() + self.n_sgc = int(np.round(n_sgc)) + + # value needed for simple synapses + self.AMPA_gmax = AMPA_gmax + + # the stimulation delivered is a Tone + self.run_duration = 0.20 # in seconds + self.pip_duration = 0.05 # in seconds + self.pip_start = [0.1] # in seconds + self.f0 = 4000.0 + self.cf = 4000.0 + self.stim = sound.TonePip( + rate=self.Fs, + duration=self.run_duration, + f0=self.f0, + dbspl=self.dbspl, + ramp_duration=2.5e-3, + pip_duration=self.pip_duration, + pip_start=self.pip_start, + ) + + # creates empty lists that will contain the results of each rep + self.vms = [None for n in range(self.nrep)] + self.synapses = [None for n in range(self.nrep)] + self.xmtrs = [None for n in range(self.nrep)] + self.pre_cells = [None for n in range(self.nrep)] + self.time = [None for n in range(self.nrep)] + + def load_variables_from_data(self): + AMPA_gmax = ( + data.get( + "sgc_synapse", species=species, post_type=self.cell, field="AMPA_gmax" + )[0] + / 1e3 + ) + n_sgc = data.get( + "convergence", species=species, post_type=self.cell, pre_type="sgc" + )[0] + print(n_sgc) + return AMPA_gmax, n_sgc + + def check_assertations(self): + assert self.cell in [ + "bushy", + "tstellate", + "octopus", + "dstellate", + "tuberculoventral", + "pyramidal", + ] + assert self.stimulus == "tone" # cases available + assert synapseType == "simple" + + def run(self): + self.check_assertations() + # info based on the parameters set in the __init__ statement + info = { + "n_sgc": self.n_sgc, + "gmax": self.AMPA_gmax, + "stim": self.stim, + "simulator": self.simulator, + "cf": self.cf, + "sr": self.sr, + "seed": self.seed, + "run_duration": self.run_duration, + "temp": self.temp, + "dt": self.dt, + "init": custom_init, + } + + # run number of trials based on nrep defined in __init__ statement + for nr in range(self.nrep): + info["seed"] = self.seed + 3 * self.n_sgc * nr + res = run_trial(info) + # res contains: {'time': time, 'vm': Vm, 'xmtr': xmtr, 'pre_cells': pre_cells, 'post_cell': post_cell} + + # unpacks the res dict returned from the run_trial() into refrenceable variables + self.pre_cells[nr] = res["pre_cells"] + self.time[nr] = res["time"] + self.xmtr = {k: v.to_python() for k, v in res["xmtr"].items()} + self.vms[nr] = res["vm"] + self.synapses[nr] = res["synapses"] + self.xmtrs[nr] = self.xmtr + + def show(self): + """ + Creates a single page graph that contains all of the graphs based on the graphical functions in the class + + """ + self.win = pg.GraphicsWindow() + self.win.setBackground("w") + p1 = self.stimulus_graph() + p2 = self.an_spikes_graph() + p3 = self.cell_spikes_graph() + p4 = self.voltage_graph() + p5 = self.xmtr_graph() + p6 = ( + self.an_psth_graph() + ) # requires that an_spikes_graph() has been called before + p7 = ( + self.cell_psth_graph() + ) # requires that cell_spikes_graph() has been called before + + # links x axis + p1.setXLink(p1) + p2.setXLink(p1) + p3.setXLink(p1) + p4.setXLink(p1) + p5.setXLink(p1) + self.win.show() + + ############# Graph options to be included in the show() method ################### + def stimulus_graph(self): + p1 = self.win.addPlot( + title="Stimulus", row=0, col=0, labels={"bottom": "T (ms)", "left": "V"} + ) + p1.plot( + self.stim.time * 1000, self.stim.sound, pen=pg.mkPen("k", width=0.75) + ) + return p1 + + def an_spikes_graph(self): + p2 = self.win.addPlot( + title="AN spikes", + row=1, + col=0, + labels={"bottom": "T (ms)", "left": "AN spikes (first trial)"}, + ) + for nr in range(self.nrep): + xan = [] + yan = [] + for k in range(len(self.pre_cells[nr])): + r = self.pre_cells[nr][k]._spiketrain + xan.extend(r) + yr = k + np.zeros_like(r) + 0.2 + yan.extend(yr) + c = pg.PlotCurveItem() + xp = np.repeat(np.array(xan), 2) + yp = np.repeat(np.array(yan), 2) + yp[1::2] = yp[::2] + 0.6 + c.setData( + xp.flatten(), + yp.flatten(), + connect="pairs", + pen=pg.mkPen(pg.intColor(nr, self.nrep), hues=self.nrep, width=1.0), + ) + self.xan = xan + self.yan = yan + p2.addItem(c) + return p2 + + def cell_spikes_graph(self): + p3 = self.win.addPlot( + title="%s Spikes" % self.cell, + row=2, + col=0, + labels={"bottom": "T (ms)", "left": "Trial #"}, + ) + xcn = [] + ycn = [] + xspks = [] + for k in range(self.nrep): + bspk = PU.findspikes(self.time[k], self.vms[k], -35.0) + xcn.extend(bspk) + yr = k + np.zeros_like(bspk) + 0.2 + ycn.extend(yr) + d = pg.PlotCurveItem() + xp = np.repeat(np.array(xcn), 2) + yp = np.repeat(np.array(ycn), 2) + yp[1::2] = yp[::2] + 0.6 + d.setData( + xp.flatten(), + yp.flatten(), + connect="pairs", + pen=pg.mkPen("k", width=1.5), + ) + self.xcn = xcn + self.ycn = ycn + p3.addItem(d) + + return p3 + + def voltage_graph(self): + p4 = self.win.addPlot( + title="%s Vm" % self.cell, + row=3, + col=0, + labels={"bottom": "T (ms)", "left": "Vm (mV)"}, + ) + for nr in range(self.nrep): + p4.plot( + self.time[nr], + self.vms[nr], + pen=pg.mkPen(pg.intColor(nr, self.nrep), hues=self.nrep, width=1.0), + ) + return p4 + + def xmtr_graph(self): + p5 = self.win.addPlot( + title="xmtr", row=0, col=1, labels={"bottom": "T (ms)", "left": "gSyn"} + ) + if synapseType == "multisite": + for nr in [0]: + syn = self.synapses[nr] + j = 0 + for k in range(self.n_sgc): + synapse = syn[k] + for i in range(synapse.terminal.n_rzones): + p5.plot( + self.time[nr], + self.xmtrs[nr]["xmtr%04d" % j], + pen=pg.mkPen( + pg.intColor(nr, self.nrep), + hues=self.nrep, + width=1.0, + ), + ) + j = j + 1 + return p5 + + def an_psth_graph(self): + p6 = self.win.addPlot( + title="AN PSTH", + row=1, + col=1, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + bins = np.arange(0, 200, 1) + (hist, binedges) = np.histogram(self.xan, bins) + curve6 = p6.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p6 + + def cell_psth_graph(self): + p7 = self.win.addPlot( + title="%s PSTH" % self.cell, + row=2, + col=1, + labels={"bottom": "T (ms)", "left": "Sp/ms/trial"}, + ) + bins = np.arange(0, 200, 1) + (hist, binedges) = np.histogram(self.xcn, bins) + curve7 = p7.plot( + binedges, + hist, + stepMode=True, + fillBrush=(0, 0, 0, 255), + brush=pg.mkBrush("k"), + fillLevel=0, + ) + return p7 + + +def run_trial(info): + """ + This function is really the bread + and butter of the run and determines the layout + of the model and the stimulus is + created and delivered by the SGC. + + Runs a single trial and returns it + into another dictionary that is stored + before the next trial is run. + + :param info: dict containing :'n_sgc': self.n_sgc, + 'gmax': self.AMPA_gmax, + 'stim': self.stim, + 'simulator': self.simulator, + 'cf': self.cf, 'sr': self.sr, + 'seed': self.seed, + 'run_duration': self.run_duration, + 'temp': self.temp, + 'dt': self.dt, + 'init': custom_init + + post cell allowed to use defaults in the case of things like pyramidal cells are guinea pigs instead of a mouse + """ + # for the model to change the post_cell needs to be changed here otherwise the model will not be changed. + # special consideration needs to be taken because not all cell parameters are universal + # ex: Pyramidal does not have a species='mouse', nothing just assumes a default and not all defaults are the same + post_cell = cells.Pyramidal.create() + pre_cells = [] + synapses = [] + xmtr = {} + + # connects all of the SGC fibers to the post_cell using simple synapses and then generates a sound stim based on a + # seed the number of repetitions desired + for nsgc, sgc in enumerate(range(info["n_sgc"])): + pre_cells.append(cells.DummySGC(cf=info["cf"], sr=info["sr"])) + synapses.append(pre_cells[-1].connect(post_cell, type="simple")) + synapses[-1].terminal.netcon.weight[0] = info["gmax"] + + # sets sounds stim for each of the SGC fibers independently + pre_cells[-1].set_sound_stim( + info["stim"], seed=info["seed"] + nsgc, simulator=info["simulator"] + ) + + # Recording each trial and returning it as a dictionary + Vm = h.Vector() + Vm.record(post_cell.soma(0.5)._ref_v) + rtime = h.Vector() + rtime.record(h._ref_t) + h.tstop = 1e3 * info["run_duration"] # duration of a run + h.celsius = info["temp"] + h.dt = info["dt"] + post_cell.cell_initialize() + info["init"]() + h.t = 0.0 + h.run() + return { + "time": np.array(rtime), + "vm": Vm.to_python(), + "xmtr": xmtr, + "pre_cells": pre_cells, + "post_cell": post_cell, + "synapses": synapses, + } + + +if __name__ == "__main__": + main() diff --git a/project/util/__init__.py b/project/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/util/tools.py b/project/util/tools.py new file mode 100644 index 0000000..146c4b4 --- /dev/null +++ b/project/util/tools.py @@ -0,0 +1,35 @@ +import functools +import time + + +class Clock(object): + """ + This is a decorator for the purpose of timing + functions and classes, it can be added to any function + and during runtime will spit out a formatted str + that displays function(arguments) and results with + a time delta. Uses pref_counter() not time() + """ + + def __init__(self, func): + self.func = func + functools.update_wrapper(self, func) + + def __call__(self, *args, **kwargs): + t0 = time.perf_counter() + result = self.func(*args, **kwargs) + elapsed = time.perf_counter() - t0 + name = self.func.__name__ + arg_lst = [] + if args: + arg_lst.extend(repr(arg) for arg in args) + if kwargs: + arg_lst.extend("{}={}".format(k, w) for k, w in kwargs.items()) + arg_str = ", ".join(arg_lst) + print( + "TIME TRIAL: {:s}({:.30s}~) -> {!r:.30}~ dt=[{:.8}]".format( + name, arg_str, result, elapsed + ) + ) + print() + return result diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ca44e8d --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup, find_packages +import os + +path = os.path.join(os.path.dirname(__file__), "cnmodel") +version = None +for line in open(os.path.join(path, "__init__.py"), "r").readlines(): + if line.startswith("__version__"): + version = line.partition("=")[2].strip("\"' \n") + break +if version is None: + raise Exception("Could not read __version__ from cnmodel/__init__.py") + + +setup( + name="cnmodel", + version=version, + description="A biophysically realistic model of the cochlear nucleus", + url="http://github.com/pbmanis/cnmodel", + author="Paul B. Manis and Luke Campagnola", + author_email="pmanis@med.unc.edu", + license="MIT", + packages=find_packages(include=["cnmodel*"]), + zip_safe=False, + install_requires=['cochlea', 'lmfit', 'tqdm', 'resampy'] +) diff --git a/test.py b/test.py new file mode 100644 index 0000000..a87bb4a --- /dev/null +++ b/test.py @@ -0,0 +1,48 @@ +from __future__ import print_function + +""" +Run unit tests for cnmodel +""" + +import os, sys +import pytest + + +def runtests(): + # Make sure we look for cnmodel here first. + path = os.path.dirname(__file__) + sys.path.insert(0, path) + + # Allow user to audit tests with --audit flag + import cnmodel + + if "--audit" in sys.argv: + sys.argv.remove("--audit") + sys.argv.append("-s") # needed for cli-based user interaction + cnmodel.AUDIT_TESTS = True + + # generate test flags + flags = sys.argv[1:] + flags.append("-v") + tb = [flag for flag in flags if flag.startswith("--tb")] + if len(tb) == 0: + flags.append("--tb=short") + + add_path = True + for flag in flags: + if os.path.isdir(flag) or os.path.isfile(flag): + add_path = False + break + if add_path: + flags.append("cnmodel/") + + # ignore the an cache + flags.append("--ignore=cnmodel/an_model/cache/") + + # Start tests. + print("Testing with flags: %s" % " ".join(flags)) + pytest.main(flags) + + +if __name__ == "__main__": + runtests() diff --git a/todo.rst b/todo.rst new file mode 100644 index 0000000..48acb70 --- /dev/null +++ b/todo.rst @@ -0,0 +1,44 @@ +Things tested and to do: + +ccstim: called by test_stim, but does ot appear to exist. +stim does not provide all the protocols tested in test_stim + +test_circuit.py: + File "test_circuit.py", line 42, in + bushy.resolve_inputs(depth=2) + File "/Users/pbmanis/Desktop/Python/cnmodel/cnmodel/populations/population.py", line 133, in resolve_inputs + pre_cells = self.connect_pop_to_cell(pop, i) + File "/Users/pbmanis/Desktop/Python/cnmodel/cnmodel/populations/population.py", line 166, in connect_pop_to_cell + pre_cell.connect(cell) + File "/Users/pbmanis/Desktop/Python/cnmodel/cnmodel/cells/cell.py", line 217, in connect + synapse = synapses.Synapse(self, pre_opts, post_cell, post_opts, **kwds) + File "/Users/pbmanis/Desktop/Python/cnmodel/cnmodel/synapses/synapse.py", line 13, in __init__ + self.terminal = pre_cell.make_terminal(post_cell, **pre_opts) + File "/Users/pbmanis/Desktop/Python/cnmodel/cnmodel/cells/cell.py", line 246, in make_terminal + post_cell.__class__.__name__)) + NotImplementedError: Cannot make Terminal connecting TStellateRothman => BushyRothman + +test_mechanisms: + some mechanisms do not make sense to be tested in this routine; need to exclude (done) + + +test_physiology: ok + +test_populations : not sure what this is supposed to do. + +test_sgc_input.py : works. + +test_sgc_input_phaselocking: no bushy cell spikes. Is inputs strength scaled correctly? + +test_simple_synapses works. + +test_sound_stim works + +test_synapses (more detailed test) works + +toy_model : works. + + + + +

      cSOdQ5v1*F0yP{ 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 Z=+=G{ELR#dJwvb4*DG*;y=T;=e*>(|d#?Zh literal 0 HcmV?d00001 diff --git a/project/Figures/examples.png b/project/Figures/examples.png new file mode 100644 index 0000000000000000000000000000000000000000..2be7f5b020e63e529b1da66c60330dd122f43951 GIT binary patch literal 17913 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 literal 0 HcmV?d00001 diff --git a/project/Figures/examples2.png b/project/Figures/examples2.png new file mode 100644 index 0000000000000000000000000000000000000000..bd6d0f0c9fcb6900449ed27ef0113008b31cd7ed GIT binary patch literal 19972 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