From a2c0230b83bb41b603367bb83181f50c98ad0678 Mon Sep 17 00:00:00 2001 From: Vyassa Baratham Date: Wed, 21 Jul 2021 08:39:06 -0700 Subject: [PATCH] first commit --- analysis/mars/io/common.py | 89 + analysis/mars/io/nsenwb.py | 499 + analysis/mars/preprocess.py | 365 + analysis/mars/preprocess_100um.sh | 19 + analysis/mars/preprocess_layer_ei.py | 69 + analysis/mars/preprocess_layers.sh | 19 + analysis/mars/preprocess_slices.py | 50 + analysis/mars/signal_processing/__init__.py | 6 + analysis/mars/signal_processing/bandpass.py | 23 + .../signal_processing/common_referencing.py | 39 + analysis/mars/signal_processing/fft.py | 14 + .../signal_processing/hilbert_transform.py | 84 + .../mars/signal_processing/linenoise_notch.py | 68 + analysis/mars/signal_processing/resample.py | 156 + analysis/mars/signal_processing/smooth.py | 62 + analysis/mars/utils/bands.py | 74 + analysis/mars/wn/wn_tokenize.py | 83 + analysis/simulation_analysis/ampl_vary.py | 60 + analysis/simulation_analysis/analysis.py | 184 + analysis/simulation_analysis/layer_reader.py | 97 + .../simulation_analysis/power_spectrum.py | 316 + .../power_spectrum_100um.py | 141 + .../power_spectrum_layers.py | 80 + analysis/simulation_analysis/raw_ecp.py | 77 + analysis/simulation_analysis/tone_avg_ecp.py | 86 + analysis/simulation_analysis/tone_figure.py | 205 + .../tone_power_spectrum.py | 169 + .../simulation_analysis/tone_spectrogram.py | 133 + analysis/simulation_analysis/utils.py | 91 + bmtk-vb/CONTRIBUTING.md | 26 + bmtk-vb/LICENSE.txt | 21 + bmtk-vb/README.md | 32 + bmtk-vb/bmtk.egg-info/PKG-INFO | 57 + bmtk-vb/bmtk.egg-info/SOURCES.txt | 215 + bmtk-vb/bmtk.egg-info/dependency_links.txt | 1 + bmtk-vb/bmtk.egg-info/requires.txt | 18 + bmtk-vb/bmtk.egg-info/top_level.txt | 1 + bmtk-vb/bmtk/__init__.py | 23 + bmtk-vb/bmtk/__init__.pyc | Bin 0 -> 161 bytes .../bmtk/__pycache__/__init__.cpython-35.pyc | Bin 0 -> 148 bytes .../bmtk/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 146 bytes .../bmtk/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 150 bytes bmtk-vb/bmtk/analyzer/__init__.py | 189 + bmtk-vb/bmtk/analyzer/__init__.pyc | Bin 0 -> 6691 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 5176 bytes bmtk-vb/bmtk/analyzer/cell_vars.py | 95 + bmtk-vb/bmtk/analyzer/firing_rates.py | 55 + bmtk-vb/bmtk/analyzer/io_tools.py | 11 + bmtk-vb/bmtk/analyzer/spike_trains.py | 16 + bmtk-vb/bmtk/analyzer/spikes_analyzer.py | 127 + bmtk-vb/bmtk/analyzer/spikes_loader.py | 112 + bmtk-vb/bmtk/analyzer/utils.py | 6 + .../bmtk/analyzer/visualization/__init__.py | 22 + .../bmtk/analyzer/visualization/__init__.pyc | Bin 0 -> 152 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 152 bytes .../__pycache__/spikes.cpython-37.pyc | Bin 0 -> 13247 bytes .../bmtk/analyzer/visualization/rasters.py | 60 + bmtk-vb/bmtk/analyzer/visualization/spikes.py | 499 + .../bmtk/analyzer/visualization/spikes.pyc | Bin 0 -> 15676 bytes .../bmtk/analyzer/visualization/widgets.py | 114 + bmtk-vb/bmtk/builder/__init__.py | 23 + bmtk-vb/bmtk/builder/__init__.pyc | Bin 0 -> 228 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 207 bytes .../__pycache__/connection_map.cpython-37.pyc | Bin 0 -> 5570 bytes .../__pycache__/connector.cpython-37.pyc | Bin 0 -> 585 bytes .../builder/__pycache__/edge.cpython-37.pyc | Bin 0 -> 1965 bytes .../__pycache__/functor_cache.cpython-37.pyc | Bin 0 -> 1170 bytes .../__pycache__/id_generator.cpython-37.pyc | Bin 0 -> 2156 bytes .../__pycache__/iterator.cpython-37.pyc | Bin 0 -> 4066 bytes .../__pycache__/network.cpython-37.pyc | Bin 0 -> 14169 bytes .../builder/__pycache__/node.cpython-37.pyc | Bin 0 -> 2045 bytes .../__pycache__/node_pool.cpython-37.pyc | Bin 0 -> 3153 bytes .../__pycache__/node_set.cpython-37.pyc | Bin 0 -> 2032 bytes bmtk-vb/bmtk/builder/aux/__init__.py | 22 + bmtk-vb/bmtk/builder/aux/__init__.pyc | Bin 0 -> 141 bytes bmtk-vb/bmtk/builder/aux/edge_connectors.py | 56 + bmtk-vb/bmtk/builder/aux/edge_connectors.pyc | Bin 0 -> 1099 bytes bmtk-vb/bmtk/builder/aux/node_params.py | 38 + bmtk-vb/bmtk/builder/aux/node_params.pyc | Bin 0 -> 1124 bytes bmtk-vb/bmtk/builder/bionet/__init__.py | 1 + bmtk-vb/bmtk/builder/bionet/swc_reader.py | 81 + bmtk-vb/bmtk/builder/connection_map.py | 153 + bmtk-vb/bmtk/builder/connection_map.pyc | Bin 0 -> 6696 bytes bmtk-vb/bmtk/builder/connector.py | 35 + bmtk-vb/bmtk/builder/connector.pyc | Bin 0 -> 798 bytes bmtk-vb/bmtk/builder/edge.py | 66 + bmtk-vb/bmtk/builder/edge.pyc | Bin 0 -> 2501 bytes bmtk-vb/bmtk/builder/formats/__init__.py | 246 + bmtk-vb/bmtk/builder/formats/hdf5_format.py | 423 + bmtk-vb/bmtk/builder/formats/iformats.py | 29 + bmtk-vb/bmtk/builder/functor_cache.py | 55 + bmtk-vb/bmtk/builder/functor_cache.pyc | Bin 0 -> 1544 bytes bmtk-vb/bmtk/builder/id_generator.py | 71 + bmtk-vb/bmtk/builder/id_generator.pyc | Bin 0 -> 2477 bytes bmtk-vb/bmtk/builder/io/__init__.py | 66 + bmtk-vb/bmtk/builder/iterator.py | 124 + bmtk-vb/bmtk/builder/iterator.pyc | Bin 0 -> 4588 bytes bmtk-vb/bmtk/builder/network.py | 478 + bmtk-vb/bmtk/builder/network.pyc | Bin 0 -> 15943 bytes bmtk-vb/bmtk/builder/networks/__init__.py | 30 + bmtk-vb/bmtk/builder/networks/__init__.pyc | Bin 0 -> 380 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 348 bytes .../__pycache__/dm_network.cpython-37.pyc | Bin 0 -> 14712 bytes .../__pycache__/mpi_network.cpython-37.pyc | Bin 0 -> 4317 bytes bmtk-vb/bmtk/builder/networks/dm_network.py | 487 + bmtk-vb/bmtk/builder/networks/dm_network.pyc | Bin 0 -> 16836 bytes .../bmtk/builder/networks/input_network.py | 22 + bmtk-vb/bmtk/builder/networks/mpi_network.py | 171 + bmtk-vb/bmtk/builder/networks/mpi_network.pyc | Bin 0 -> 5239 bytes bmtk-vb/bmtk/builder/networks/nxnetwork.py | 80 + .../bmtk/builder/networks/sparse_network.py | 26 + bmtk-vb/bmtk/builder/node.py | 76 + bmtk-vb/bmtk/builder/node.pyc | Bin 0 -> 2600 bytes bmtk-vb/bmtk/builder/node_pool.py | 106 + bmtk-vb/bmtk/builder/node_pool.pyc | Bin 0 -> 3921 bytes bmtk-vb/bmtk/builder/node_set.py | 71 + bmtk-vb/bmtk/builder/node_set.pyc | Bin 0 -> 2264 bytes bmtk-vb/bmtk/simulator/__init__.py | 22 + bmtk-vb/bmtk/simulator/__init__.pyc | Bin 0 -> 139 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 139 bytes .../bmtk/simulator/bionet/#biosimulator.py# | 361 + bmtk-vb/bmtk/simulator/bionet/README.md | 4 + bmtk-vb/bmtk/simulator/bionet/__init__.py | 31 + bmtk-vb/bmtk/simulator/bionet/__init__.pyc | Bin 0 -> 531 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 466 bytes .../bionet/__pycache__/biocell.cpython-37.pyc | Bin 0 -> 8864 bytes .../__pycache__/bionetwork.cpython-37.pyc | Bin 0 -> 7772 bytes .../__pycache__/biosimulator.cpython-37.pyc | Bin 0 -> 9756 bytes .../bionet/__pycache__/cell.cpython-37.pyc | Bin 0 -> 3004 bytes .../bionet/__pycache__/config.cpython-37.pyc | Bin 0 -> 2222 bytes .../bionet/__pycache__/iclamp.cpython-37.pyc | Bin 0 -> 811 bytes .../__pycache__/io_tools.cpython-37.pyc | Bin 0 -> 728 bytes .../__pycache__/morphology.cpython-37.pyc | Bin 0 -> 4843 bytes .../__pycache__/nml_reader.cpython-37.pyc | Bin 0 -> 5896 bytes .../bionet/__pycache__/nrn.cpython-37.pyc | Bin 0 -> 1535 bytes .../pointprocesscell.cpython-37.pyc | Bin 0 -> 2543 bytes .../__pycache__/pointsomacell.cpython-37.pyc | Bin 0 -> 708 bytes .../pyfunction_cache.cpython-37.pyc | Bin 0 -> 9309 bytes .../sonata_adaptors.cpython-37.pyc | Bin 0 -> 4137 bytes .../bionet/__pycache__/utils.cpython-37.pyc | Bin 0 -> 2485 bytes .../__pycache__/virtualcell.cpython-37.pyc | Bin 0 -> 1209 bytes bmtk-vb/bmtk/simulator/bionet/biocell.py | 323 + bmtk-vb/bmtk/simulator/bionet/biocell.pyc | Bin 0 -> 11737 bytes bmtk-vb/bmtk/simulator/bionet/bionetwork.py | 262 + bmtk-vb/bmtk/simulator/bionet/bionetwork.pyc | Bin 0 -> 9157 bytes bmtk-vb/bmtk/simulator/bionet/biosimulator.py | 363 + .../bmtk/simulator/bionet/biosimulator.pyc | Bin 0 -> 12602 bytes bmtk-vb/bmtk/simulator/bionet/cell.py | 104 + bmtk-vb/bmtk/simulator/bionet/cell.pyc | Bin 0 -> 3946 bytes bmtk-vb/bmtk/simulator/bionet/config.py | 88 + bmtk-vb/bmtk/simulator/bionet/config.pyc | Bin 0 -> 2843 bytes .../bionet/default_setters/__init__.py | 25 + .../bionet/default_setters/__init__.pyc | Bin 0 -> 310 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 274 bytes .../__pycache__/cell_models.cpython-37.pyc | Bin 0 -> 11141 bytes .../__pycache__/synapse_models.cpython-37.pyc | Bin 0 -> 4594 bytes .../synaptic_weights.cpython-37.pyc | Bin 0 -> 972 bytes .../bionet/default_setters/cell_models.py | 460 + .../bionet/default_setters/cell_models.pyc | Bin 0 -> 13764 bytes .../bionet/default_setters/synapse_models.py | 206 + .../bionet/default_setters/synapse_models.pyc | Bin 0 -> 6133 bytes .../default_setters/synaptic_weights.py | 51 + .../default_setters/synaptic_weights.pyc | Bin 0 -> 1361 bytes .../bionet/default_templates/BioAxonStub.hoc | 61 + .../bionet/default_templates/Biophys1.hoc | 32 + .../bionet/default_templates/advance.hoc | 10 + bmtk-vb/bmtk/simulator/bionet/gids.pyc | Bin 0 -> 1751 bytes bmtk-vb/bmtk/simulator/bionet/iclamp.py | 38 + bmtk-vb/bmtk/simulator/bionet/iclamp.pyc | Bin 0 -> 1058 bytes bmtk-vb/bmtk/simulator/bionet/import3d.hoc | 12 + .../bionet/import3d/import3d_gui.hoc | 1174 ++ .../bionet/import3d/import3d_sec.hoc | 392 + .../bionet/import3d/read_morphml.hoc | 78 + .../simulator/bionet/import3d/read_nlcda.hoc | 550 + .../simulator/bionet/import3d/read_nlcda3.hoc | 1194 ++ .../simulator/bionet/import3d/read_nts.hoc | 331 + .../simulator/bionet/import3d/read_swc.hoc | 428 + bmtk-vb/bmtk/simulator/bionet/io_tools.py | 38 + bmtk-vb/bmtk/simulator/bionet/io_tools.pyc | Bin 0 -> 887 bytes .../bmtk/simulator/bionet/modules/__init__.py | 27 + .../simulator/bionet/modules/__init__.pyc | Bin 0 -> 500 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 422 bytes .../modules/__pycache__/ecp.cpython-37.pyc | Bin 0 -> 7825 bytes .../record_cellvars.cpython-37.pyc | Bin 0 -> 6202 bytes .../__pycache__/record_spikes.cpython-37.pyc | Bin 0 -> 2174 bytes .../__pycache__/save_synapses.cpython-37.pyc | Bin 0 -> 8109 bytes .../__pycache__/sim_module.cpython-37.pyc | Bin 0 -> 2404 bytes .../modules/__pycache__/xstim.cpython-37.pyc | Bin 0 -> 5658 bytes .../xstim_waveforms.cpython-37.pyc | Bin 0 -> 4791 bytes bmtk-vb/bmtk/simulator/bionet/modules/ecp.py | 275 + bmtk-vb/bmtk/simulator/bionet/modules/ecp.pyc | Bin 0 -> 10006 bytes .../bionet/modules/record_cellvars.py | 212 + .../bionet/modules/record_cellvars.pyc | Bin 0 -> 7944 bytes .../bionet/modules/record_netcons.pyc | Bin 0 -> 6179 bytes .../simulator/bionet/modules/record_spikes.py | 94 + .../bionet/modules/record_spikes.pyc | Bin 0 -> 2930 bytes .../simulator/bionet/modules/save_synapses.py | 235 + .../bionet/modules/save_synapses.pyc | Bin 0 -> 10371 bytes .../simulator/bionet/modules/sim_module.py | 72 + .../simulator/bionet/modules/sim_module.pyc | Bin 0 -> 2708 bytes .../bmtk/simulator/bionet/modules/xstim.py | 163 + .../bmtk/simulator/bionet/modules/xstim.pyc | Bin 0 -> 7209 bytes .../bionet/modules/xstim_waveforms.py | 127 + .../bionet/modules/xstim_waveforms.pyc | Bin 0 -> 6392 bytes bmtk-vb/bmtk/simulator/bionet/morphology.py | 245 + bmtk-vb/bmtk/simulator/bionet/morphology.pyc | Bin 0 -> 6182 bytes bmtk-vb/bmtk/simulator/bionet/nml_reader.py | 168 + bmtk-vb/bmtk/simulator/bionet/nml_reader.pyc | Bin 0 -> 7715 bytes bmtk-vb/bmtk/simulator/bionet/nrn.py | 82 + bmtk-vb/bmtk/simulator/bionet/nrn.pyc | Bin 0 -> 2009 bytes .../bmtk/simulator/bionet/pointprocesscell.py | 85 + .../simulator/bionet/pointprocesscell.pyc | Bin 0 -> 3020 bytes .../bmtk/simulator/bionet/pointsomacell.py | 12 + .../bmtk/simulator/bionet/pointsomacell.pyc | Bin 0 -> 874 bytes .../bmtk/simulator/bionet/pyfunction_cache.py | 252 + .../simulator/bionet/pyfunction_cache.pyc | Bin 0 -> 11524 bytes .../bionet/schemas/config_schema.json | 131 + .../bionet/schemas/csv_edge_types.json | 20 + .../schemas/csv_node_types_external.json | 11 + .../schemas/csv_node_types_internal.json | 15 + .../bionet/schemas/csv_nodes_external.json | 11 + .../bionet/schemas/csv_nodes_internal.json | 19 + .../bmtk/simulator/bionet/sonata_adaptors.py | 142 + .../bmtk/simulator/bionet/sonata_adaptors.pyc | Bin 0 -> 5721 bytes bmtk-vb/bmtk/simulator/bionet/utils.py | 84 + bmtk-vb/bmtk/simulator/bionet/utils.pyc | Bin 0 -> 3121 bytes bmtk-vb/bmtk/simulator/bionet/virtualcell.py | 51 + bmtk-vb/bmtk/simulator/bionet/virtualcell.pyc | Bin 0 -> 1594 bytes bmtk-vb/bmtk/simulator/core/__init__.py | 0 bmtk-vb/bmtk/simulator/core/__init__.pyc | Bin 0 -> 144 bytes .../core/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 144 bytes .../core/__pycache__/io_tools.cpython-37.pyc | Bin 0 -> 3851 bytes .../__pycache__/network_reader.cpython-37.pyc | Bin 0 -> 2854 bytes .../core/__pycache__/node_sets.cpython-37.pyc | Bin 0 -> 2283 bytes .../core/__pycache__/simulator.cpython-37.pyc | Bin 0 -> 705 bytes .../simulator_network.cpython-37.pyc | Bin 0 -> 6953 bytes bmtk-vb/bmtk/simulator/core/config.py | 436 + .../bmtk/simulator/core/edge_population.py | 21 + bmtk-vb/bmtk/simulator/core/graph.py | 435 + bmtk-vb/bmtk/simulator/core/io_tools.py | 117 + bmtk-vb/bmtk/simulator/core/io_tools.pyc | Bin 0 -> 4958 bytes bmtk-vb/bmtk/simulator/core/network_reader.py | 73 + .../bmtk/simulator/core/network_reader.pyc | Bin 0 -> 3807 bytes .../bmtk/simulator/core/node_population.py | 37 + bmtk-vb/bmtk/simulator/core/node_sets.py | 57 + bmtk-vb/bmtk/simulator/core/node_sets.pyc | Bin 0 -> 2850 bytes bmtk-vb/bmtk/simulator/core/simulator.py | 9 + bmtk-vb/bmtk/simulator/core/simulator.pyc | Bin 0 -> 934 bytes .../bmtk/simulator/core/simulator_network.py | 200 + .../bmtk/simulator/core/simulator_network.pyc | Bin 0 -> 8766 bytes .../simulator/core/sonata_reader/__init__.py | 3 + .../simulator/core/sonata_reader/__init__.pyc | Bin 0 -> 431 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 374 bytes .../__pycache__/edge_adaptor.cpython-37.pyc | Bin 0 -> 6420 bytes .../__pycache__/network_reader.cpython-37.pyc | Bin 0 -> 5765 bytes .../__pycache__/node_adaptor.cpython-37.pyc | Bin 0 -> 5918 bytes .../core/sonata_reader/edge_adaptor.py | 208 + .../core/sonata_reader/edge_adaptor.pyc | Bin 0 -> 9168 bytes .../core/sonata_reader/network_reader.py | 244 + .../core/sonata_reader/network_reader.pyc | Bin 0 -> 8184 bytes .../core/sonata_reader/node_adaptor.py | 207 + .../core/sonata_reader/node_adaptor.pyc | Bin 0 -> 8268 bytes bmtk-vb/bmtk/simulator/filternet/__init__.py | 5 + bmtk-vb/bmtk/simulator/filternet/cell.py | 28 + .../bmtk/simulator/filternet/cell_models.py | 1 + bmtk-vb/bmtk/simulator/filternet/config.py | 8 + .../filternet/default_setters/__init__.py | 1 + .../filternet/default_setters/cell_loaders.py | 9 + .../bmtk/simulator/filternet/filternetwork.py | 28 + bmtk-vb/bmtk/simulator/filternet/filters.py | 3 + .../simulator/filternet/filtersimulator.py | 193 + bmtk-vb/bmtk/simulator/filternet/io_tools.py | 1 + .../simulator/filternet/lgnmodel/__init__.py | 7 + .../lgnmodel/cell_metrics/sOFF_cell_data.csv | 29 + .../lgnmodel/cell_metrics/sON_cell_data.csv | 23 + .../cell_metrics/sus_sus_cells_v3.csv | 11 + .../lgnmodel/cell_metrics/tOFF_cell_data.csv | 18 + .../lgnmodel/cell_metrics/tON_cell_data.csv | 2 + .../cell_metrics/trans_sus_cells_v3.csv | 8 + .../simulator/filternet/lgnmodel/cellmodel.py | 358 + .../simulator/filternet/lgnmodel/cursor.py | 266 + .../simulator/filternet/lgnmodel/fitfuns.py | 190 + .../simulator/filternet/lgnmodel/kernel.py | 475 + .../lgnmodel/lattice_unit_constructor.py | 254 + .../simulator/filternet/lgnmodel/lgnmodel1.py | 87 + .../filternet/lgnmodel/linearfilter.py | 128 + .../simulator/filternet/lgnmodel/lnunit.py | 380 + .../filternet/lgnmodel/make_cell_list.py | 294 + .../simulator/filternet/lgnmodel/movie.py | 196 + .../filternet/lgnmodel/poissongeneration.py | 104 + .../filternet/lgnmodel/singleunitcell.py | 8 + .../filternet/lgnmodel/spatialfilter.py | 215 + .../filternet/lgnmodel/temporalfilter.py | 114 + .../filternet/lgnmodel/transferfunction.py | 58 + .../simulator/filternet/lgnmodel/util_fns.py | 190 + .../simulator/filternet/lgnmodel/utilities.py | 123 + .../simulator/filternet/modules/__init__.py | 2 + .../bmtk/simulator/filternet/modules/base.py | 9 + .../filternet/modules/create_spikes.py | 99 + .../filternet/modules/record_rates.py | 29 + .../simulator/filternet/pyfunction_cache.py | 98 + .../simulator/filternet/transfer_functions.py | 1 + bmtk-vb/bmtk/simulator/filternet/utils.py | 1 + .../bmtk/simulator/mintnet/Image_Library.py | 105 + .../mintnet/Image_Library_Supervised.py | 93 + bmtk-vb/bmtk/simulator/mintnet/__init__.py | 22 + .../mintnet/analysis/LocallySparseNoise.py | 105 + .../mintnet/analysis/StaticGratings.py | 101 + .../simulator/mintnet/analysis/__init__.py | 22 + .../bmtk/simulator/mintnet/hmax/C_Layer.py | 260 + .../simulator/mintnet/hmax/Readout_Layer.py | 243 + .../bmtk/simulator/mintnet/hmax/S1_Layer.py | 273 + .../bmtk/simulator/mintnet/hmax/S_Layer.py | 404 + .../bmtk/simulator/mintnet/hmax/Sb_Layer.py | 242 + .../simulator/mintnet/hmax/ViewTunedLayer.py | 219 + .../bmtk/simulator/mintnet/hmax/__init__.py | 28 + bmtk-vb/bmtk/simulator/mintnet/hmax/hmax.py | 432 + bmtk-vb/bmtk/simulator/pointnet/__init__.py | 26 + bmtk-vb/bmtk/simulator/pointnet/config.py | 48 + .../pointnet/default_setters/__init__.py | 2 + .../default_setters/synapse_models.py | 16 + .../default_setters/synaptic_weights.py | 8 + bmtk-vb/bmtk/simulator/pointnet/io_tools.py | 122 + .../simulator/pointnet/modules/__init__.py | 2 + .../pointnet/modules/multimeter_reporter.py | 110 + .../pointnet/modules/record_spikes.py | 90 + .../bmtk/simulator/pointnet/pointnetwork.py | 176 + .../bmtk/simulator/pointnet/pointsimulator.py | 266 + .../bmtk/simulator/pointnet/property_map.py | 213 + .../simulator/pointnet/pyfunction_cache.py | 246 + .../simulator/pointnet/sonata_adaptors.py | 295 + bmtk-vb/bmtk/simulator/pointnet/utils.py | 188 + bmtk-vb/bmtk/simulator/popnet/__init__.py | 25 + bmtk-vb/bmtk/simulator/popnet/config.py | 34 + bmtk-vb/bmtk/simulator/popnet/popedge.py | 82 + bmtk-vb/bmtk/simulator/popnet/popnetwork.py | 695 + .../bmtk/simulator/popnet/popnetwork_OLD.py | 327 + bmtk-vb/bmtk/simulator/popnet/popnode.py | 158 + bmtk-vb/bmtk/simulator/popnet/popsimulator.py | 451 + .../popnet/property_schemas/__init__.py | 28 + .../popnet/property_schemas/base_schema.py | 50 + .../property_schemas/property_schema_ver0.py | 28 + .../property_schemas/property_schema_ver1.py | 28 + .../bmtk/simulator/popnet/sonata_adaptors.py | 12 + bmtk-vb/bmtk/simulator/popnet/utils.py | 287 + bmtk-vb/bmtk/simulator/utils/__init__.py | 22 + bmtk-vb/bmtk/simulator/utils/__init__.pyc | Bin 0 -> 145 bytes .../utils/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 145 bytes .../utils/__pycache__/config.cpython-37.pyc | Bin 0 -> 12881 bytes .../__pycache__/sim_validator.cpython-37.pyc | Bin 0 -> 4479 bytes .../simulation_inputs.cpython-37.pyc | Bin 0 -> 2076 bytes .../simulation_reports.cpython-37.pyc | Bin 0 -> 8945 bytes bmtk-vb/bmtk/simulator/utils/config.py | 438 + bmtk-vb/bmtk/simulator/utils/config.pyc | Bin 0 -> 16248 bytes bmtk-vb/bmtk/simulator/utils/graph.py | 408 + bmtk-vb/bmtk/simulator/utils/io.py | 54 + bmtk-vb/bmtk/simulator/utils/load_spikes.py | 91 + bmtk-vb/bmtk/simulator/utils/nwb.py | 530 + bmtk-vb/bmtk/simulator/utils/property_maps.py | 7 + .../utils/scripts/convert_filters.py | 71 + bmtk-vb/bmtk/simulator/utils/sim_validator.py | 126 + .../bmtk/simulator/utils/sim_validator.pyc | Bin 0 -> 5149 bytes .../bmtk/simulator/utils/simulation_inputs.py | 77 + .../simulator/utils/simulation_inputs.pyc | Bin 0 -> 2813 bytes .../simulator/utils/simulation_reports.py | 284 + .../simulator/utils/simulation_reports.pyc | Bin 0 -> 11855 bytes .../utils/stimulus/LocallySparseNoise.py | 137 + .../simulator/utils/stimulus/NaturalScenes.py | 337 + .../utils/stimulus/StaticGratings.py | 100 + .../bmtk/simulator/utils/stimulus/__init__.py | 22 + bmtk-vb/bmtk/simulator/utils/stimulus/lsn.npy | Bin 0 -> 4032080 bytes .../bmtk/simulator/utils/tools/__init__.py | 22 + .../simulator/utils/tools/process_spikes.py | 207 + bmtk-vb/bmtk/simulator/utils/tools/spatial.py | 26 + bmtk-vb/bmtk/test.py~ | 3 + .../bmtk/tests/builder/test_connection_map.py | 116 + bmtk-vb/bmtk/tests/builder/test_connector.py | 44 + .../bmtk/tests/builder/test_densenetwork.py | 307 + .../bmtk/tests/builder/test_edge_iterator.py | 72 + .../bmtk/tests/builder/test_id_generator.py | 36 + bmtk-vb/bmtk/tests/builder/test_iterator.py | 134 + bmtk-vb/bmtk/tests/builder/test_node_pool.py | 81 + bmtk-vb/bmtk/tests/builder/test_node_set.py | 52 + .../simulator/bionet/bionet_virtual_files.py | 172 + .../tests/simulator/bionet/set_cell_params.py | 108 + .../tests/simulator/bionet/set_syn_params.py | 31 + .../tests/simulator/bionet/set_weights.py | 19 + .../tests/simulator/bionet/test_biograph.py | 70 + .../bmtk/tests/simulator/bionet/test_nrn.py | 148 + .../pointnet/pointnet_virtual_files.py | 158 + .../simulator/pointnet/test_pointgraph.py | 77 + .../simulator/popnet/popnet_virtual_files.py | 159 + .../tests/simulator/popnet/test_popgraph.py | 39 + .../simulator/utils/files/circuit_config.json | 43 + .../tests/simulator/utils/files/config.json | 9 + .../utils/files/simulator_config.json | 50 + .../bmtk/tests/simulator/utils/test_config.py | 148 + .../bmtk/tests/simulator/utils/test_nwb.py | 341 + bmtk-vb/bmtk/utils/__init__.py | 24 + bmtk-vb/bmtk/utils/__init__.pyc | Bin 0 -> 164 bytes .../utils/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 150 bytes .../utils/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 154 bytes bmtk-vb/bmtk/utils/cell_vars/__init__.py | 6 + bmtk-vb/bmtk/utils/cell_vars/__init__.pyc | Bin 0 -> 208 bytes bmtk-vb/bmtk/utils/cell_vars/var_reader.py | 134 + bmtk-vb/bmtk/utils/cell_vars/var_reader.pyc | Bin 0 -> 5717 bytes bmtk-vb/bmtk/utils/converters/__init__.py | 22 + .../bmtk/utils/converters/hoc_converter.py | 299 + .../bmtk/utils/converters/sonata/__init__.py | 2 + .../converters/sonata/edge_converters.py | 278 + .../converters/sonata/node_converters.py | 399 + .../bmtk/utils/io/Final_Sp2019_20190512.docx | Bin 0 -> 566550 bytes bmtk-vb/bmtk/utils/io/__init__.py | 27 + bmtk-vb/bmtk/utils/io/__init__.pyc | Bin 0 -> 299 bytes .../io/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 237 bytes .../io/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 241 bytes .../io/__pycache__/cell_vars.cpython-36.pyc | Bin 0 -> 14372 bytes .../io/__pycache__/cell_vars.cpython-37.pyc | Bin 0 -> 11148 bytes .../__pycache__/spike_trains.cpython-37.pyc | Bin 0 -> 11147 bytes bmtk-vb/bmtk/utils/io/_test_cell_vars.py~ | 3 + bmtk-vb/bmtk/utils/io/cell_vars.py | 361 + bmtk-vb/bmtk/utils/io/cell_vars.pyc | Bin 0 -> 13424 bytes bmtk-vb/bmtk/utils/io/firing_rates.py | 35 + bmtk-vb/bmtk/utils/io/orig_cell_vars.py | 356 + bmtk-vb/bmtk/utils/io/spike_trains.py | 312 + bmtk-vb/bmtk/utils/io/spike_trains.pyc | Bin 0 -> 13781 bytes bmtk-vb/bmtk/utils/io/tabular_network.py | 350 + bmtk-vb/bmtk/utils/io/tabular_network_v0.py | 160 + bmtk-vb/bmtk/utils/io/tabular_network_v1.py | 256 + bmtk-vb/bmtk/utils/property_schema.py | 35 + bmtk-vb/bmtk/utils/reports/__init__.pyc | Bin 0 -> 143 bytes .../utils/reports/spike_trains/__init__.pyc | Bin 0 -> 550 bytes .../spike_trains/adaptors/__init__.pyc | Bin 0 -> 931 bytes .../spike_trains/adaptors/csv_adaptors.pyc | Bin 0 -> 6416 bytes .../spike_trains/adaptors/nwb_adaptors.pyc | Bin 0 -> 5342 bytes .../spike_trains/adaptors/sonata_adaptors.pyc | Bin 0 -> 16592 bytes .../bmtk/utils/reports/spike_trains/core.pyc | Bin 0 -> 6383 bytes .../utils/reports/spike_trains/plotting.pyc | Bin 0 -> 9480 bytes .../spike_trains/spike_train_buffer.pyc | Bin 0 -> 17151 bytes .../reports/spike_trains/spike_trains.pyc | Bin 0 -> 11879 bytes .../utils/scripts/bionet/default_config.json | 47 + .../bionet/hoc_templates/BioAllen_old.hoc | 21 + .../bionet/hoc_templates/BioAxonStub.hoc | 61 + .../scripts/bionet/hoc_templates/Biophys1.hoc | 34 + .../bionet/intfire/IntFire1_exc_1.json | 5 + .../bionet/intfire/IntFire1_inh_1.json | 5 + .../bionet/mechanisms/modfiles/CaDynamics.mod | 40 + .../bionet/mechanisms/modfiles/Ca_HVA.mod | 82 + .../bionet/mechanisms/modfiles/Ca_LVA.mod | 69 + .../scripts/bionet/mechanisms/modfiles/Ih.mod | 71 + .../scripts/bionet/mechanisms/modfiles/Im.mod | 62 + .../bionet/mechanisms/modfiles/Im_v2.mod | 59 + .../bionet/mechanisms/modfiles/K_P.mod | 71 + .../bionet/mechanisms/modfiles/K_T.mod | 68 + .../scripts/bionet/mechanisms/modfiles/Kd.mod | 62 + .../bionet/mechanisms/modfiles/Kv2like.mod | 86 + .../bionet/mechanisms/modfiles/Kv3_1.mod | 54 + .../bionet/mechanisms/modfiles/NaTa.mod | 95 + .../bionet/mechanisms/modfiles/NaTs.mod | 95 + .../bionet/mechanisms/modfiles/NaV.mod | 186 + .../bionet/mechanisms/modfiles/Nap.mod | 77 + .../scripts/bionet/mechanisms/modfiles/SK.mod | 56 + .../bionet/mechanisms/modfiles/vecevent.mod | 71 + .../bmtk/utils/scripts/bionet/run_bionet.py | 23 + .../bionet/synaptic_models/AMPA_ExcToExc.json | 6 + .../bionet/synaptic_models/AMPA_ExcToInh.json | 6 + .../bionet/synaptic_models/GABA_InhToExc.json | 7 + .../bionet/synaptic_models/GABA_InhToInh.json | 7 + .../synaptic_models/instanteneousExc.json | 5 + .../synaptic_models/instanteneousInh.json | 5 + .../472363762_point.json | 9 + .../472912177_point.json | 9 + .../473862421_point.json | 9 + .../473863035_point.json | 9 + .../473863510_point.json | 9 + .../IntFire1_exc_point.json | 9 + .../IntFire1_inh_point.json | 9 + .../point_neuron_templates/filter_point.json | 3 + .../utils/scripts/pointnet/run_pointnet.py | 14 + .../pointnet/synaptic_models/ExcToExc.json | 2 + .../pointnet/synaptic_models/ExcToInh.json | 2 + .../pointnet/synaptic_models/InhToExc.json | 3 + .../pointnet/synaptic_models/InhToInh.json | 3 + .../synaptic_models/instanteneousExc.json | 3 + .../synaptic_models/instanteneousInh.json | 3 + .../popnet/population_models/exc_model.json | 9 + .../popnet/population_models/inh_model.json | 9 + .../bmtk/utils/scripts/popnet/run_popnet.py | 24 + .../popnet/synaptic_models/ExcToExc.json | 2 + .../popnet/synaptic_models/ExcToInh.json | 2 + .../popnet/synaptic_models/InhToExc.json | 3 + .../popnet/synaptic_models/InhToInh.json | 3 + .../synaptic_models/input_ExcToExc.json | 2 + .../synaptic_models/input_ExcToInh.json | 2 + .../utils/scripts/sonata.circuit_config.json | 21 + .../scripts/sonata.simulation_config.json | 31 + bmtk-vb/bmtk/utils/sim_setup.py | 443 + bmtk-vb/bmtk/utils/sonata/__init__.py | 25 + bmtk-vb/bmtk/utils/sonata/__init__.pyc | Bin 0 -> 329 bytes .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 273 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 277 bytes .../column_property.cpython-36.pyc | Bin 0 -> 2988 bytes .../column_property.cpython-37.pyc | Bin 0 -> 2992 bytes .../sonata/__pycache__/edge.cpython-36.pyc | Bin 0 -> 2688 bytes .../sonata/__pycache__/edge.cpython-37.pyc | Bin 0 -> 2692 bytes .../sonata/__pycache__/file.cpython-36.pyc | Bin 0 -> 3286 bytes .../sonata/__pycache__/file.cpython-37.pyc | Bin 0 -> 3290 bytes .../__pycache__/file_root.cpython-36.pyc | Bin 0 -> 10425 bytes .../__pycache__/file_root.cpython-37.pyc | Bin 0 -> 10423 bytes .../sonata/__pycache__/group.cpython-36.pyc | Bin 0 -> 12296 bytes .../sonata/__pycache__/group.cpython-37.pyc | Bin 0 -> 12294 bytes .../sonata/__pycache__/node.cpython-36.pyc | Bin 0 -> 3459 bytes .../sonata/__pycache__/node.cpython-37.pyc | Bin 0 -> 3463 bytes .../__pycache__/population.cpython-36.pyc | Bin 0 -> 17770 bytes .../__pycache__/population.cpython-37.pyc | Bin 0 -> 17769 bytes .../__pycache__/types_table.cpython-36.pyc | Bin 0 -> 6099 bytes .../__pycache__/types_table.cpython-37.pyc | Bin 0 -> 6101 bytes .../sonata/__pycache__/utils.cpython-36.pyc | Bin 0 -> 2623 bytes .../sonata/__pycache__/utils.cpython-37.pyc | Bin 0 -> 2412 bytes bmtk-vb/bmtk/utils/sonata/column_property.py | 103 + bmtk-vb/bmtk/utils/sonata/column_property.pyc | Bin 0 -> 3734 bytes bmtk-vb/bmtk/utils/sonata/config.py | 341 + bmtk-vb/bmtk/utils/sonata/edge.py | 90 + bmtk-vb/bmtk/utils/sonata/edge.pyc | Bin 0 -> 3516 bytes bmtk-vb/bmtk/utils/sonata/file.py | 124 + bmtk-vb/bmtk/utils/sonata/file.pyc | Bin 0 -> 3940 bytes bmtk-vb/bmtk/utils/sonata/file_root.py | 301 + bmtk-vb/bmtk/utils/sonata/file_root.pyc | Bin 0 -> 11825 bytes bmtk-vb/bmtk/utils/sonata/group.py | 416 + bmtk-vb/bmtk/utils/sonata/group.pyc | Bin 0 -> 15118 bytes bmtk-vb/bmtk/utils/sonata/node.py | 126 + bmtk-vb/bmtk/utils/sonata/node.pyc | Bin 0 -> 4592 bytes bmtk-vb/bmtk/utils/sonata/population.py | 608 + bmtk-vb/bmtk/utils/sonata/population.pyc | Bin 0 -> 22301 bytes bmtk-vb/bmtk/utils/sonata/types_table.py | 220 + bmtk-vb/bmtk/utils/sonata/types_table.pyc | Bin 0 -> 7681 bytes bmtk-vb/bmtk/utils/sonata/utils.py | 116 + bmtk-vb/bmtk/utils/sonata/utils.pyc | Bin 0 -> 3271 bytes bmtk-vb/bmtk/utils/spike_trains/__init__.py | 24 + bmtk-vb/bmtk/utils/spike_trains/spikes_csv.py | 94 + .../bmtk/utils/spike_trains/spikes_file.py | 174 + bmtk-vb/build/lib/bmtk/__init__.py | 23 + bmtk-vb/build/lib/bmtk/analyzer/__init__.py | 189 + bmtk-vb/build/lib/bmtk/analyzer/cell_vars.py | 95 + .../build/lib/bmtk/analyzer/firing_rates.py | 55 + bmtk-vb/build/lib/bmtk/analyzer/io_tools.py | 11 + .../build/lib/bmtk/analyzer/spike_trains.py | 16 + .../lib/bmtk/analyzer/spikes_analyzer.py | 127 + .../build/lib/bmtk/analyzer/spikes_loader.py | 112 + bmtk-vb/build/lib/bmtk/analyzer/utils.py | 6 + .../bmtk/analyzer/visualization/__init__.py | 22 + .../bmtk/analyzer/visualization/rasters.py | 60 + .../lib/bmtk/analyzer/visualization/spikes.py | 499 + .../bmtk/analyzer/visualization/widgets.py | 114 + bmtk-vb/build/lib/bmtk/builder/__init__.py | 23 + .../build/lib/bmtk/builder/aux/__init__.py | 22 + .../lib/bmtk/builder/aux/edge_connectors.py | 56 + .../build/lib/bmtk/builder/aux/node_params.py | 38 + .../build/lib/bmtk/builder/bionet/__init__.py | 1 + .../lib/bmtk/builder/bionet/swc_reader.py | 81 + .../build/lib/bmtk/builder/connection_map.py | 153 + bmtk-vb/build/lib/bmtk/builder/connector.py | 35 + bmtk-vb/build/lib/bmtk/builder/edge.py | 66 + .../lib/bmtk/builder/formats/__init__.py | 246 + .../lib/bmtk/builder/formats/hdf5_format.py | 423 + .../lib/bmtk/builder/formats/iformats.py | 29 + .../build/lib/bmtk/builder/functor_cache.py | 55 + .../build/lib/bmtk/builder/id_generator.py | 71 + bmtk-vb/build/lib/bmtk/builder/io/__init__.py | 66 + bmtk-vb/build/lib/bmtk/builder/iterator.py | 124 + bmtk-vb/build/lib/bmtk/builder/network.py | 478 + .../lib/bmtk/builder/networks/__init__.py | 30 + .../lib/bmtk/builder/networks/dm_network.py | 487 + .../bmtk/builder/networks/input_network.py | 22 + .../lib/bmtk/builder/networks/mpi_network.py | 171 + .../lib/bmtk/builder/networks/nxnetwork.py | 80 + .../bmtk/builder/networks/sparse_network.py | 26 + bmtk-vb/build/lib/bmtk/builder/node.py | 76 + bmtk-vb/build/lib/bmtk/builder/node_pool.py | 106 + bmtk-vb/build/lib/bmtk/builder/node_set.py | 71 + bmtk-vb/build/lib/bmtk/simulator/__init__.py | 22 + .../build/lib/bmtk/simulator/bionet/README.md | 4 + .../lib/bmtk/simulator/bionet/__init__.py | 31 + .../lib/bmtk/simulator/bionet/biocell.py | 323 + .../lib/bmtk/simulator/bionet/bionetwork.py | 262 + .../lib/bmtk/simulator/bionet/biosimulator.py | 357 + .../build/lib/bmtk/simulator/bionet/cell.py | 104 + .../build/lib/bmtk/simulator/bionet/config.py | 84 + .../bionet/default_setters/__init__.py | 25 + .../bionet/default_setters/cell_models.py | 460 + .../bionet/default_setters/synapse_models.py | 206 + .../default_setters/synaptic_weights.py | 51 + .../bionet/default_templates/BioAxonStub.hoc | 61 + .../bionet/default_templates/Biophys1.hoc | 32 + .../bionet/default_templates/advance.hoc | 10 + .../build/lib/bmtk/simulator/bionet/iclamp.py | 38 + .../lib/bmtk/simulator/bionet/import3d.hoc | 12 + .../bionet/import3d/import3d_gui.hoc | 1174 ++ .../bionet/import3d/import3d_sec.hoc | 392 + .../bionet/import3d/read_morphml.hoc | 78 + .../simulator/bionet/import3d/read_nlcda.hoc | 550 + .../simulator/bionet/import3d/read_nlcda3.hoc | 1194 ++ .../simulator/bionet/import3d/read_nts.hoc | 331 + .../simulator/bionet/import3d/read_swc.hoc | 428 + .../lib/bmtk/simulator/bionet/io_tools.py | 38 + .../bmtk/simulator/bionet/modules/__init__.py | 27 + .../lib/bmtk/simulator/bionet/modules/ecp.py | 275 + .../bionet/modules/record_cellvars.py | 204 + .../simulator/bionet/modules/record_spikes.py | 94 + .../simulator/bionet/modules/save_synapses.py | 235 + .../simulator/bionet/modules/sim_module.py | 72 + .../bmtk/simulator/bionet/modules/xstim.py | 163 + .../bionet/modules/xstim_waveforms.py | 127 + .../lib/bmtk/simulator/bionet/morphology.py | 245 + .../lib/bmtk/simulator/bionet/nml_reader.py | 168 + .../build/lib/bmtk/simulator/bionet/nrn.py | 82 + .../bmtk/simulator/bionet/pointprocesscell.py | 85 + .../bmtk/simulator/bionet/pointsomacell.py | 12 + .../bmtk/simulator/bionet/pyfunction_cache.py | 252 + .../bionet/schemas/config_schema.json | 130 + .../bionet/schemas/csv_edge_types.json | 20 + .../schemas/csv_node_types_external.json | 11 + .../schemas/csv_node_types_internal.json | 15 + .../bionet/schemas/csv_nodes_external.json | 11 + .../bionet/schemas/csv_nodes_internal.json | 19 + .../bmtk/simulator/bionet/sonata_adaptors.py | 142 + .../build/lib/bmtk/simulator/bionet/utils.py | 84 + .../lib/bmtk/simulator/bionet/virtualcell.py | 51 + .../build/lib/bmtk/simulator/core/__init__.py | 0 .../build/lib/bmtk/simulator/core/config.py | 436 + .../bmtk/simulator/core/edge_population.py | 21 + .../build/lib/bmtk/simulator/core/graph.py | 435 + .../build/lib/bmtk/simulator/core/io_tools.py | 111 + .../lib/bmtk/simulator/core/network_reader.py | 73 + .../bmtk/simulator/core/node_population.py | 37 + .../lib/bmtk/simulator/core/node_sets.py | 57 + .../lib/bmtk/simulator/core/simulator.py | 9 + .../bmtk/simulator/core/simulator_network.py | 200 + .../simulator/core/sonata_reader/__init__.py | 3 + .../core/sonata_reader/edge_adaptor.py | 206 + .../core/sonata_reader/network_reader.py | 241 + .../core/sonata_reader/node_adaptor.py | 207 + .../lib/bmtk/simulator/filternet/__init__.py | 5 + .../lib/bmtk/simulator/filternet/cell.py | 28 + .../bmtk/simulator/filternet/cell_models.py | 1 + .../lib/bmtk/simulator/filternet/config.py | 8 + .../filternet/default_setters/__init__.py | 1 + .../filternet/default_setters/cell_loaders.py | 9 + .../bmtk/simulator/filternet/filternetwork.py | 28 + .../lib/bmtk/simulator/filternet/filters.py | 3 + .../simulator/filternet/filtersimulator.py | 193 + .../lib/bmtk/simulator/filternet/io_tools.py | 1 + .../simulator/filternet/lgnmodel/__init__.py | 7 + .../simulator/filternet/lgnmodel/cellmodel.py | 358 + .../simulator/filternet/lgnmodel/cursor.py | 266 + .../simulator/filternet/lgnmodel/fitfuns.py | 190 + .../simulator/filternet/lgnmodel/kernel.py | 475 + .../lgnmodel/lattice_unit_constructor.py | 254 + .../simulator/filternet/lgnmodel/lgnmodel1.py | 87 + .../filternet/lgnmodel/linearfilter.py | 128 + .../simulator/filternet/lgnmodel/lnunit.py | 380 + .../filternet/lgnmodel/make_cell_list.py | 294 + .../simulator/filternet/lgnmodel/movie.py | 196 + .../filternet/lgnmodel/poissongeneration.py | 104 + .../filternet/lgnmodel/singleunitcell.py | 8 + .../filternet/lgnmodel/spatialfilter.py | 215 + .../filternet/lgnmodel/temporalfilter.py | 114 + .../filternet/lgnmodel/transferfunction.py | 58 + .../simulator/filternet/lgnmodel/util_fns.py | 190 + .../simulator/filternet/lgnmodel/utilities.py | 123 + .../simulator/filternet/modules/__init__.py | 2 + .../bmtk/simulator/filternet/modules/base.py | 9 + .../filternet/modules/create_spikes.py | 99 + .../filternet/modules/record_rates.py | 29 + .../simulator/filternet/pyfunction_cache.py | 98 + .../simulator/filternet/transfer_functions.py | 1 + .../lib/bmtk/simulator/filternet/utils.py | 1 + .../bmtk/simulator/mintnet/Image_Library.py | 105 + .../mintnet/Image_Library_Supervised.py | 93 + .../lib/bmtk/simulator/mintnet/__init__.py | 22 + .../mintnet/analysis/LocallySparseNoise.py | 105 + .../mintnet/analysis/StaticGratings.py | 101 + .../simulator/mintnet/analysis/__init__.py | 22 + .../bmtk/simulator/mintnet/hmax/C_Layer.py | 260 + .../simulator/mintnet/hmax/Readout_Layer.py | 243 + .../bmtk/simulator/mintnet/hmax/S1_Layer.py | 273 + .../bmtk/simulator/mintnet/hmax/S_Layer.py | 404 + .../bmtk/simulator/mintnet/hmax/Sb_Layer.py | 242 + .../simulator/mintnet/hmax/ViewTunedLayer.py | 219 + .../bmtk/simulator/mintnet/hmax/__init__.py | 28 + .../lib/bmtk/simulator/mintnet/hmax/hmax.py | 432 + .../lib/bmtk/simulator/pointnet/__init__.py | 26 + .../lib/bmtk/simulator/pointnet/config.py | 48 + .../pointnet/default_setters/__init__.py | 2 + .../default_setters/synapse_models.py | 16 + .../default_setters/synaptic_weights.py | 8 + .../lib/bmtk/simulator/pointnet/io_tools.py | 122 + .../simulator/pointnet/modules/__init__.py | 2 + .../pointnet/modules/multimeter_reporter.py | 110 + .../pointnet/modules/record_spikes.py | 90 + .../bmtk/simulator/pointnet/pointnetwork.py | 176 + .../bmtk/simulator/pointnet/pointsimulator.py | 266 + .../bmtk/simulator/pointnet/property_map.py | 213 + .../simulator/pointnet/pyfunction_cache.py | 246 + .../simulator/pointnet/sonata_adaptors.py | 295 + .../lib/bmtk/simulator/pointnet/utils.py | 188 + .../lib/bmtk/simulator/popnet/__init__.py | 25 + .../build/lib/bmtk/simulator/popnet/config.py | 34 + .../lib/bmtk/simulator/popnet/popedge.py | 82 + .../lib/bmtk/simulator/popnet/popnetwork.py | 695 + .../bmtk/simulator/popnet/popnetwork_OLD.py | 327 + .../lib/bmtk/simulator/popnet/popnode.py | 158 + .../lib/bmtk/simulator/popnet/popsimulator.py | 451 + .../popnet/property_schemas/__init__.py | 28 + .../popnet/property_schemas/base_schema.py | 50 + .../property_schemas/property_schema_ver0.py | 28 + .../property_schemas/property_schema_ver1.py | 28 + .../bmtk/simulator/popnet/sonata_adaptors.py | 12 + .../build/lib/bmtk/simulator/popnet/utils.py | 287 + .../lib/bmtk/simulator/utils/__init__.py | 22 + .../build/lib/bmtk/simulator/utils/config.py | 438 + .../build/lib/bmtk/simulator/utils/graph.py | 408 + bmtk-vb/build/lib/bmtk/simulator/utils/io.py | 54 + .../lib/bmtk/simulator/utils/load_spikes.py | 91 + bmtk-vb/build/lib/bmtk/simulator/utils/nwb.py | 530 + .../lib/bmtk/simulator/utils/property_maps.py | 7 + .../lib/bmtk/simulator/utils/sim_validator.py | 126 + .../bmtk/simulator/utils/simulation_inputs.py | 77 + .../simulator/utils/simulation_reports.py | 284 + .../utils/stimulus/LocallySparseNoise.py | 137 + .../simulator/utils/stimulus/NaturalScenes.py | 337 + .../utils/stimulus/StaticGratings.py | 100 + .../bmtk/simulator/utils/stimulus/__init__.py | 22 + .../bmtk/simulator/utils/tools/__init__.py | 22 + .../simulator/utils/tools/process_spikes.py | 207 + .../lib/bmtk/simulator/utils/tools/spatial.py | 26 + bmtk-vb/build/lib/bmtk/utils/__init__.py | 24 + .../lib/bmtk/utils/cell_vars/__init__.py | 6 + .../lib/bmtk/utils/cell_vars/var_reader.py | 134 + .../lib/bmtk/utils/converters/__init__.py | 22 + .../bmtk/utils/converters/hoc_converter.py | 299 + .../bmtk/utils/converters/sonata/__init__.py | 2 + .../converters/sonata/edge_converters.py | 278 + .../converters/sonata/node_converters.py | 399 + bmtk-vb/build/lib/bmtk/utils/io/__init__.py | 27 + bmtk-vb/build/lib/bmtk/utils/io/cell_vars.py | 361 + .../build/lib/bmtk/utils/io/firing_rates.py | 35 + .../build/lib/bmtk/utils/io/spike_trains.py | 312 + .../lib/bmtk/utils/io/tabular_network.py | 350 + .../lib/bmtk/utils/io/tabular_network_v0.py | 160 + .../lib/bmtk/utils/io/tabular_network_v1.py | 256 + .../build/lib/bmtk/utils/property_schema.py | 35 + .../utils/scripts/sonata.circuit_config.json | 21 + .../scripts/sonata.simulation_config.json | 31 + bmtk-vb/build/lib/bmtk/utils/sim_setup.py | 443 + .../build/lib/bmtk/utils/sonata/__init__.py | 25 + .../lib/bmtk/utils/sonata/column_property.py | 103 + bmtk-vb/build/lib/bmtk/utils/sonata/config.py | 341 + bmtk-vb/build/lib/bmtk/utils/sonata/edge.py | 90 + bmtk-vb/build/lib/bmtk/utils/sonata/file.py | 124 + .../build/lib/bmtk/utils/sonata/file_root.py | 301 + bmtk-vb/build/lib/bmtk/utils/sonata/group.py | 416 + bmtk-vb/build/lib/bmtk/utils/sonata/node.py | 126 + .../build/lib/bmtk/utils/sonata/population.py | 608 + .../lib/bmtk/utils/sonata/types_table.py | 220 + bmtk-vb/build/lib/bmtk/utils/sonata/utils.py | 116 + .../lib/bmtk/utils/spike_trains/__init__.py | 24 + .../lib/bmtk/utils/spike_trains/spikes_csv.py | 94 + .../bmtk/utils/spike_trains/spikes_file.py | 174 + bmtk-vb/docker/Dockerfile | 84 + bmtk-vb/docker/README.md | 56 + bmtk-vb/docker/entry_script.sh | 16 + bmtk-vb/docs/README.md | 5 + bmtk-vb/docs/autodocs/.nojekyll | 0 bmtk-vb/docs/autodocs/Makefile | 25 + .../_static/images/all_network_cropped.png | Bin 0 -> 677518 bytes .../source/_static/images/dipde_icon.png | Bin 0 -> 9172 bytes .../source/_static/images/edge_types.png | Bin 0 -> 765677 bytes .../_static/images/edges_h5_structure.png | Bin 0 -> 11765 bytes .../_static/images/ext_inputs_raster.png | Bin 0 -> 266646 bytes .../source/_static/images/graph_structure.png | Bin 0 -> 194693 bytes .../_static/images/levels_of_resolution.png | Bin 0 -> 305790 bytes .../source/_static/images/nest_icon.png | Bin 0 -> 5837 bytes .../source/_static/images/neuron_icon.png | Bin 0 -> 11847 bytes .../source/_static/images/node_types.png | Bin 0 -> 441160 bytes .../_static/images/nodes_h5_structure.png | Bin 0 -> 10478 bytes .../source/_static/images/tensorflow_icon.png | Bin 0 -> 5644 bytes .../source/_static/images/v1_raster.png | Bin 0 -> 18318 bytes .../source/_static/images/workflow.png | Bin 0 -> 63162 bytes .../aibs_sphinx/static/aibs_sphinx.css_t | 833 ++ .../external_assets/images/AIBS_Logo.png | Bin 0 -> 6628 bytes .../images/Brain_Atlas_Logotype_SDK.png | Bin 0 -> 18954 bytes .../external_assets/images/arrow_off.gif | Bin 0 -> 58 bytes .../external_assets/images/arrow_on.gif | Bin 0 -> 58 bytes .../external_assets/images/arrow_over.gif | Bin 0 -> 58 bytes .../static/external_assets/images/close_x.png | Bin 0 -> 475 bytes .../external_assets/images/logo_AIBS.gif | Bin 0 -> 17447 bytes .../images/logo_aibs_footer.png | Bin 0 -> 4654 bytes .../images/progress_indicator.gif | Bin 0 -> 8238 bytes .../external_assets/images/tab_blue.gif | Bin 0 -> 351 bytes .../external_assets/images/workflow.png | Bin 0 -> 63162 bytes .../javascript/AC_RunActiveContent.js | 292 + .../external_assets/javascript/appConfig.js | 14 + .../javascript/browserVersions.js | 28 + .../external_assets/javascript/portal.js | 843 ++ .../javascript/portalFooter.js | 557 + .../javascript/portalHeader.js | 670 + .../external_assets/javascript/relatedData.js | 5 + .../external_assets/stylesheets/animation.css | 85 + .../stylesheets/bebasneue-webfont.eot | 0 .../stylesheets/bebasneue-webfont.svg | 1936 +++ .../stylesheets/bebasneue-webfont.ttf | 0 .../stylesheets/bebasneue-webfont.woff | 0 .../stylesheets/common_layout.css | 224 + .../external_assets/stylesheets/portal.css | 89 + .../stylesheets/portal_icon_font.css | 74 + .../stylesheets/portal_icon_font.eot | 0 .../stylesheets/portal_icon_font.svg | 33 + .../stylesheets/portal_icon_font.ttf | 0 .../stylesheets/portal_icon_font.woff | 0 .../aibs_sphinx/static/style/comment.png | Bin 0 -> 401 bytes .../aibs_sphinx/static/style/nocomment.png | Bin 0 -> 415 bytes .../aibs_sphinx/templates/globaltoc.html | 11 + .../source/aibs_sphinx/templates/layout.html | 20 + .../aibs_sphinx/templates/portalFooter.html | 457 + .../aibs_sphinx/templates/portalHeader.html | 591 + .../autodocs/source/aibs_sphinx/theme.conf | 4 + bmtk-vb/docs/autodocs/source/bionet.rst | 63 + .../docs/autodocs/source/bionet_config.rst | 170 + .../docs/autodocs/source/bionet_tutorial.rst | 153 + bmtk-vb/docs/autodocs/source/builder.rst | 49 + bmtk-vb/docs/autodocs/source/conf.py | 193 + bmtk-vb/docs/autodocs/source/filternet.rst | 38 + bmtk-vb/docs/autodocs/source/index.rst | 121 + bmtk-vb/docs/autodocs/source/installation.rst | 143 + bmtk-vb/docs/autodocs/source/mintnet.rst | 7 + .../autodocs/source/network_file_formats.rst | 108 + bmtk-vb/docs/autodocs/source/pointnet.rst | 51 + bmtk-vb/docs/autodocs/source/popnet.rst | 49 + bmtk-vb/docs/autodocs/source/simulators.rst | 15 + .../environments/bionet/default_config.json | 47 + .../bionet/hoc_templates/BioAllen_old.hoc | 21 + .../bionet/hoc_templates/BioAxonStub.hoc | 61 + .../bionet/hoc_templates/Biophys1.hoc | 34 + .../bionet/intfire/IntFire1_exc_1.json | 5 + .../bionet/intfire/IntFire1_inh_1.json | 5 + .../bionet/mechanisms/modfiles/CaDynamics.mod | 40 + .../bionet/mechanisms/modfiles/Ca_HVA.mod | 82 + .../bionet/mechanisms/modfiles/Ca_LVA.mod | 69 + .../bionet/mechanisms/modfiles/Ih.mod | 71 + .../bionet/mechanisms/modfiles/Im.mod | 62 + .../bionet/mechanisms/modfiles/Im_v2.mod | 59 + .../bionet/mechanisms/modfiles/K_P.mod | 71 + .../bionet/mechanisms/modfiles/K_T.mod | 68 + .../bionet/mechanisms/modfiles/Kd.mod | 62 + .../bionet/mechanisms/modfiles/Kv2like.mod | 86 + .../bionet/mechanisms/modfiles/Kv3_1.mod | 54 + .../bionet/mechanisms/modfiles/NaTa.mod | 95 + .../bionet/mechanisms/modfiles/NaTs.mod | 95 + .../bionet/mechanisms/modfiles/NaV.mod | 186 + .../bionet/mechanisms/modfiles/Nap.mod | 77 + .../bionet/mechanisms/modfiles/SK.mod | 56 + .../bionet/mechanisms/modfiles/vecevent.mod | 71 + .../docs/environments/bionet/run_bionet.py | 23 + .../bionet/synaptic_models/AMPA_ExcToExc.json | 6 + .../bionet/synaptic_models/AMPA_ExcToInh.json | 6 + .../bionet/synaptic_models/GABA_InhToExc.json | 7 + .../bionet/synaptic_models/GABA_InhToInh.json | 7 + .../synaptic_models/instanteneousExc.json | 5 + .../synaptic_models/instanteneousInh.json | 5 + .../docs/examples/NWB_files/lgn_spikes.nwb | Bin 0 -> 17454352 bytes bmtk-vb/docs/examples/NWB_files/tw_spikes.nwb | Bin 0 -> 5318992 bytes .../examples/bio_14cells/build_network.py | 369 + bmtk-vb/docs/examples/bio_14cells/config.json | 109 + .../bio_14cells/network/lgn_node_types.csv | 4 + .../examples/bio_14cells/network/lgn_nodes.h5 | Bin 0 -> 12952 bytes .../bio_14cells/network/lgn_v1_edge_types.csv | 8 + .../bio_14cells/network/lgn_v1_edges.h5 | Bin 0 -> 47120 bytes .../bio_14cells/network/tw_node_types.csv | 2 + .../examples/bio_14cells/network/tw_nodes.h5 | Bin 0 -> 7392 bytes .../bio_14cells/network/tw_v1_edge_types.csv | 8 + .../bio_14cells/network/tw_v1_edges.h5 | Bin 0 -> 34152 bytes .../bio_14cells/network/v1_node_types.csv | 8 + .../examples/bio_14cells/network/v1_nodes.h5 | Bin 0 -> 12912 bytes .../bio_14cells/network/v1_v1_edge_types.csv | 12 + .../bio_14cells/network/v1_v1_edges.h5 | Bin 0 -> 24832 bytes .../docs/examples/bio_14cells/run_bionet.py | 20 + .../examples/bio_450cells/build_network.py | 190 + .../docs/examples/bio_450cells/build_nodes.py | 7 + .../docs/examples/bio_450cells/config.json | 102 + .../network/external_internal_edge_types.csv | 4 + .../network/external_internal_edges.h5 | Bin 0 -> 1614288 bytes .../network/external_node_types.csv | 2 + .../bio_450cells/network/external_nodes.h5 | Bin 0 -> 8216 bytes .../network/internal_internal_edge_types.csv | 7 + .../network/internal_internal_edges.h5 | Bin 0 -> 1296092 bytes .../network/internal_node_types.csv | 8 + .../bio_450cells/network/internal_nodes.h5 | Bin 0 -> 37664 bytes .../docs/examples/bio_450cells/plot_spikes.py | 3 + .../bio_450cells/rebuild_edge_index.py | 18 + .../docs/examples/bio_450cells/run_bionet.py | 23 + .../bio_450cells_exact/build_network.py | 215 + .../examples/bio_450cells_exact/config.json | 104 + .../network/external_internal_edge_types.csv | 4 + .../network/external_internal_edges.h5 | Bin 0 -> 6386641 bytes .../network/external_node_types.csv | 2 + .../network/external_nodes.h5 | Bin 0 -> 8216 bytes .../network/internal_internal_edge_types.csv | 7 + .../network/internal_internal_edges.h5 | Bin 0 -> 5325445 bytes .../network/internal_node_types.csv | 8 + .../network/internal_nodes.h5 | Bin 0 -> 37664 bytes .../bio_450cells_exact/plot_spikes.py | 3 + .../bio_450cells_exact/rebuild_edge_index.py | 18 + .../examples/bio_450cells_exact/run_bionet.py | 21 + .../examples/bio_basic_features/README.md | 38 + .../bio_basic_features/build_network.py | 71 + .../bio_basic_features/config_iclamp.json | 94 + .../config_spikes_input.json | 96 + .../bio_basic_features/config_xstim.json | 125 + .../inputs/exc_spike_trains.h5 | Bin 0 -> 14736 bytes .../network/bio_node_types.csv | 6 + .../bio_basic_features/network/bio_nodes.h5 | Bin 0 -> 7992 bytes .../network/virt_bio_edge_types.csv | 2 + .../network/virt_bio_edges.h5 | Bin 0 -> 18536 bytes .../network/virt_node_types.csv | 2 + .../bio_basic_features/network/virt_nodes.h5 | Bin 0 -> 7392 bytes .../bio_basic_features/network_config.json | 33 + .../examples/bio_basic_features/run_bionet.py | 48 + .../examples/bio_stp_models/build_network.py | 142 + .../docs/examples/bio_stp_models/config.json | 98 + .../bio_stp_models/inputs/stim_12_pulses.nwb | Bin 0 -> 42920 bytes .../bio_stp_models/network/ext_node_types.csv | 2 + .../bio_stp_models/network/ext_nodes.h5 | Bin 0 -> 7992 bytes .../network/ext_to_slice_edge_types.csv | 11 + .../network/ext_to_slice_edges.h5 | Bin 0 -> 16488 bytes .../network/slice_node_types.csv | 3 + .../bio_stp_models/network/slice_nodes.h5 | Bin 0 -> 7992 bytes .../examples/bio_stp_models/run_bionet.py | 20 + .../1606013050101.json | 295 + .../318331342_fit.json | 297 + .../472363762_fit.json | 173 + .../472912177_fit.json | 163 + .../473862421_fit.json | 163 + .../473863035_fit.json | 173 + .../473863510_fit.json | 173 + .../485184849_fit.json | 135 + .../nml/Cell_472363762.cell.nml | 40 + .../nml/Cell_472912177.cell.nml | 39 + .../nml/Cell_473862421.cell.nml | 39 + .../nml/Cell_473863035.cell.nml | 41 + .../nml/Cell_473863510.cell.nml | 43 + .../mechanisms/modfiles/CaDynamics.mod | 40 + .../mechanisms/modfiles/Ca_HVA.mod | 82 + .../mechanisms/modfiles/Ca_LVA.mod | 69 + .../mechanisms/modfiles/Ih.mod | 71 + .../mechanisms/modfiles/Im.mod | 62 + .../mechanisms/modfiles/Im_v2.mod | 59 + .../mechanisms/modfiles/K_P.mod | 71 + .../mechanisms/modfiles/K_T.mod | 68 + .../mechanisms/modfiles/Kd.mod | 62 + .../mechanisms/modfiles/Kv2like.mod | 86 + .../mechanisms/modfiles/Kv3_1.mod | 54 + .../mechanisms/modfiles/NaTa.mod | 95 + .../mechanisms/modfiles/NaTs.mod | 95 + .../mechanisms/modfiles/NaV.mod | 186 + .../mechanisms/modfiles/Nap.mod | 77 + .../mechanisms/modfiles/SK.mod | 56 + .../mechanisms/modfiles/exp1isyn.mod | 52 + .../mechanisms/modfiles/exp1syn.mod | 42 + .../mechanisms/modfiles/stp1syn.mod | 75 + .../mechanisms/modfiles/stp2syn.mod | 95 + .../mechanisms/modfiles/stp3syn.mod | 102 + .../mechanisms/modfiles/stp4syn.mod | 83 + .../mechanisms/modfiles/stp5isyn.mod | 113 + .../mechanisms/modfiles/stp5syn.mod | 131 + .../mechanisms/modfiles/vecevent.mod | 71 + .../morphologies/1606013050101.swc | 3437 +++++ .../morphologies/485184849_reconstruction.swc | 10674 ++++++++++++++++ ...i14_IVSCC_-169250.03.02.01_471087815_m.swc | 1534 +++ .../morphologies/Nr5a1_471087815_m.swc | 1534 +++ ...S-Cre_Ai14-169125.03.01.01_491119617_m.swc | 1239 ++ ...i14_IVSCC_-169125.03.01.01_469628681_m.swc | 1250 ++ ...i14_IVSCC_-176847.04.02.01_470522102_m.swc | 1966 +++ .../morphologies/Pvalb_469628681_m.swc | 1250 ++ .../morphologies/Pvalb_470522102_m.swc | 1966 +++ ...i14_IVSCC_-168053.05.01.01_325404214_m.swc | 2194 ++++ .../morphologies/Rorb_325404214_m.swc | 2194 ++++ ...i14_IVSCC_-177300.01.02.01_473845048_m.swc | 3786 ++++++ .../morphologies/Scnn1a_473845048_m.swc | 3786 ++++++ .../IntFire1_exc_1.json | 5 + .../IntFire1_inh_1.json | 5 + .../recXelectrodes/linear_electrode.csv | 87 + .../stimulations/485058595_0000.csv | 2 + .../stimulations/stimxmesh.csv | 2 + .../synaptic_models/AMPA_ExcToExc.json | 6 + .../synaptic_models/AMPA_ExcToInh.json | 6 + .../synaptic_models/GABA_InhToExc.json | 7 + .../synaptic_models/GABA_InhToInh.json | 7 + .../synaptic_models/instanteneousExc.json | 5 + .../synaptic_models/instanteneousInh.json | 5 + .../synaptic_models/pvalb_pvalb.json | 10 + .../examples/filter_graitings/build_cells.py | 251 + .../examples/filter_graitings/cell_loaders.py | 167 + .../examples/filter_graitings/config.json | 95 + .../network/lgn_node_types.csv | 4 + .../filter_graitings/network/lgn_nodes.h5 | Bin 0 -> 26016 bytes ...1017_-2.1430682_4.59332686_20.0_5.0_ic.pkl | 139 + .../sOFF_TF1_3.5_-2.0_50.0_140.0_5.0_ic.pkl | 139 + .../sOFF_TF2_3.5_-2.0_10.0_100.0_25.0_ic.pkl | 139 + .../sOFF_TF4_3.5_-2.0_10.0_60.0_15.0_ic.pkl | 139 + .../sOFF_TF8_3.5_-2.0_10.0_60.0_15.0_ic.pkl | 139 + .../sON_TF1_3.5_-2.0_50.0_140.0_15.0_ic.pkl | 139 + .../sON_TF2_3.5_-2.0_50.0_140.0_5.0_ic.pkl | 139 + .../sON_TF4_3.5_-2.0_30.0_60.0_25.0_ic.pkl | 139 + .../sON_TF8_3.5_-2.0_10.0_20.0_25.0_ic.pkl | 139 + ...357_-2.11509939_8.27421573_20.0_0.0_ic.pkl | 139 + .../tOFF_TF4_3.5_-2.0_30.0_60.0_15.0_ic.pkl | 139 + ...F_TF8_4.222_-2.404_8.545_23.019_0.0_ic.pkl | 139 + .../filter_graitings/run_filternet.py | 21 + .../examples/point_120cells/build_gids.py | 4 + .../examples/point_120cells/build_network.py | 90 + .../docs/examples/point_120cells/config.json | 75 + .../examples/point_120cells/create_inputs.py | 5 + .../network/cortex_cortex_edge_types.csv | 3 + .../network/cortex_cortex_edges.h5 | Bin 0 -> 81560 bytes .../network/cortex_node_types.csv | 3 + .../point_120cells/network/cortex_nodes.h5 | Bin 0 -> 14392 bytes .../examples/point_120cells/network/gids.h5 | Bin 0 -> 8672 bytes .../network/thalamus_cortex_edge_types.csv | 2 + .../network/thalamus_cortex_edges.h5 | Bin 0 -> 74596 bytes .../network/thalamus_node_types.csv | 2 + .../point_120cells/network/thalamus_nodes.h5 | Bin 0 -> 8216 bytes .../examples/point_120cells/plot_spikes.py | 3 + .../examples/point_120cells/run_pointnet.py | 16 + .../point_120cells/thalamus_spikes.csv | 101 + .../examples/point_450cells/build_network.py | 178 + .../docs/examples/point_450cells/config.json | 67 + .../network/external_internal_edge_types.csv | 4 + .../network/external_internal_edges.h5 | Bin 0 -> 1619744 bytes .../network/external_node_types.csv | 2 + .../point_450cells/network/external_nodes.h5 | Bin 0 -> 8216 bytes .../network/internal_internal_edge_types.csv | 7 + .../network/internal_internal_edges.h5 | Bin 0 -> 1293804 bytes .../network/internal_node_types.csv | 8 + .../point_450cells/network/internal_nodes.h5 | Bin 0 -> 37664 bytes .../examples/point_450cells/plot_spikes.py | 3 + .../examples/point_450cells/run_pointnet.py | 18 + .../cell_models/472363762_point.json | 9 + .../cell_models/472912177_point.json | 9 + .../cell_models/473862421_point.json | 9 + .../cell_models/473863035_point.json | 9 + .../cell_models/473863510_point.json | 9 + .../cell_models/IntFire1_exc_point.json | 9 + .../cell_models/IntFire1_inh_point.json | 9 + .../cell_models/filter_point.json | 3 + .../cell_models/iaf_psc_delta_exc.json | 3 + .../cell_models/iaf_psc_delta_inh.json | 3 + .../synaptic_models/ExcToExc.json | 2 + .../synaptic_models/ExcToInh.json | 2 + .../synaptic_models/InhToExc.json | 3 + .../synaptic_models/InhToInh.json | 3 + .../synaptic_models/instanteneousExc.json | 3 + .../synaptic_models/instanteneousInh.json | 3 + .../docs/examples/point_iclamp/config.json | 85 + .../network/recurrent_network/edge_types.csv | 9 + .../network/recurrent_network/edges.h5 | Bin 0 -> 16688 bytes .../network/recurrent_network/node_types.csv | 5 + .../network/recurrent_network/nodes.csv | 9 + .../network/recurrent_network/nodes.h5 | Bin 0 -> 8880 bytes .../examples/point_iclamp/run_pointnet.py | 14 + .../docs/examples/pop_2pops/build_network.py | 48 + .../components/pop_models/exc_model.json | 9 + .../components/pop_models/inh_model.json | 9 + .../components/synaptic_models/ExcToExc.json | 2 + .../components/synaptic_models/ExcToInh.json | 2 + .../components/synaptic_models/InhToExc.json | 3 + .../components/synaptic_models/InhToInh.json | 3 + .../synaptic_models/input_ExcToExc.json | 2 + .../synaptic_models/input_ExcToInh.json | 2 + bmtk-vb/docs/examples/pop_2pops/config.json | 64 + bmtk-vb/docs/examples/pop_2pops/lgn_rates.csv | 2 + .../pop_2pops/network/brunel_edge_types.csv | 3 + .../pop_2pops/network/brunel_edges.h5 | Bin 0 -> 16488 bytes .../pop_2pops/network/brunel_node_types.csv | 3 + .../pop_2pops/network/brunel_nodes.h5 | Bin 0 -> 7392 bytes .../pop_2pops/network/input_edge_types.csv | 2 + .../examples/pop_2pops/network/input_edges.h5 | Bin 0 -> 16488 bytes .../pop_2pops/network/input_node_types.csv | 2 + .../examples/pop_2pops/network/input_nodes.h5 | Bin 0 -> 7392 bytes bmtk-vb/docs/examples/pop_2pops/run_popnet.py | 24 + .../examples/pop_7pops_converted/config.json | 81 + .../network/lgn_node_types.csv | 4 + .../pop_7pops_converted/network/lgn_nodes.h5 | Bin 0 -> 402656 bytes .../network/lgn_v1_edge_types.csv | 8 + .../network/lgn_v1_edges.h5 | Bin 0 -> 681112 bytes .../network/tw_node_types.csv | 2 + .../pop_7pops_converted/network/tw_nodes.h5 | Bin 0 -> 138656 bytes .../network/tw_v1_edge_types.csv | 8 + .../network/tw_v1_edges.h5 | Bin 0 -> 594944 bytes .../network/v1_node_types.csv | 8 + .../pop_7pops_converted/network/v1_nodes.h5 | Bin 0 -> 40004 bytes .../network/v1_v1_edge_types.csv | 12 + .../network/v1_v1_edges.h5 | Bin 0 -> 2742632 bytes .../pop_7pops_converted/run_popnet.py | 17 + .../pop_models/472363762_pop.json | 9 + .../pop_models/472912177_pop.json | 9 + .../pop_models/473862421_pop.json | 9 + .../pop_models/473863035_pop.json | 9 + .../pop_models/473863510_pop.json | 9 + .../pop_models/IntFire1_exc_pop.json | 4 + .../pop_models/IntFire1_inh_pop.json | 4 + .../pop_models/excitatory_pop.json | 9 + .../pop_models/filter_pop.json | 3 + .../pop_models/inhibitory_pop.json | 9 + .../synaptic_models/ExcToExc.json | 2 + .../synaptic_models/ExcToInh.json | 2 + .../synaptic_models/InhToExc.json | 3 + .../synaptic_models/InhToInh.json | 3 + .../synaptic_models/instanteneousExc.json | 3 + .../synaptic_models/instanteneousInh.json | 3 + bmtk-vb/docs/tutorial/00_introduction.ipynb | 108 + .../tutorial/01_single_cell_clamped.ipynb | 671 + .../docs/tutorial/02_single_cell_syn.ipynb | 736 ++ bmtk-vb/docs/tutorial/03_single_pop.ipynb | 478 + bmtk-vb/docs/tutorial/04_multi_pop.ipynb | 735 ++ .../docs/tutorial/05_pointnet_modeling.ipynb | 997 ++ .../tutorial/06_population_modeling.ipynb | 558 + bmtk-vb/docs/tutorial/07_filter_models.ipynb | 44 + .../docs/tutorial/NetworkBuilder_Intro.ipynb | 837 ++ bmtk-vb/docs/tutorial/Simulation_Intro.ipynb | 246 + .../tutorial/images/levels_of_resolution.png | Bin 0 -> 305790 bytes .../electrophysiology/472363762_fit.json | 173 + .../electrophysiology/472912177_fit.json | 163 + .../electrophysiology/473862421_fit.json | 163 + .../electrophysiology/473863035_fit.json | 173 + .../electrophysiology/473863510_fit.json | 173 + ...i14_IVSCC_-169250.03.02.01_471087815_m.swc | 1534 +++ .../morphology/Nr5a1_471087815_m.swc | 1534 +++ ...i14_IVSCC_-169125.03.01.01_469628681_m.swc | 1250 ++ ...i14_IVSCC_-176847.04.02.01_470522102_m.swc | 1966 +++ .../morphology/Pvalb_469628681_m.swc | 1250 ++ .../morphology/Pvalb_470522102_m.swc | 1966 +++ ...i14_IVSCC_-168053.05.01.01_325404214_m.swc | 2194 ++++ .../morphology/Rorb_325404214_m.swc | 2194 ++++ ...i14_IVSCC_-177300.01.02.01_473845048_m.swc | 3786 ++++++ .../morphology/Scnn1a_473845048_m.swc | 3786 ++++++ .../components/hoc_templates/BioAllen_old.hoc | 21 + .../components/hoc_templates/BioAxonStub.hoc | 61 + .../components/hoc_templates/Biophys1.hoc | 34 + .../components/intfire/IntFire1_exc_1.json | 5 + .../components/intfire/IntFire1_inh_1.json | 5 + .../mechanisms/modfiles/CaDynamics.mod | 40 + .../components/mechanisms/modfiles/Ca_HVA.mod | 82 + .../components/mechanisms/modfiles/Ca_LVA.mod | 69 + .../components/mechanisms/modfiles/Ih.mod | 71 + .../components/mechanisms/modfiles/Im.mod | 62 + .../components/mechanisms/modfiles/Im_v2.mod | 59 + .../components/mechanisms/modfiles/K_P.mod | 71 + .../components/mechanisms/modfiles/K_T.mod | 68 + .../components/mechanisms/modfiles/Kd.mod | 62 + .../mechanisms/modfiles/Kv2like.mod | 86 + .../components/mechanisms/modfiles/Kv3_1.mod | 54 + .../components/mechanisms/modfiles/NaTa.mod | 95 + .../components/mechanisms/modfiles/NaTs.mod | 95 + .../components/mechanisms/modfiles/NaV.mod | 186 + .../components/mechanisms/modfiles/Nap.mod | 77 + .../components/mechanisms/modfiles/SK.mod | 56 + .../mechanisms/modfiles/vecevent.mod | 71 + .../recXelectrodes/linear_electrode.csv | 87 + .../recXelectrodes/mesh_electrode.csv | 388 + .../recXelectrodes/mesh_electrode_half.csv | 216 + .../synaptic_models/AMPA_ExcToExc.json | 6 + .../synaptic_models/AMPA_ExcToInh.json | 6 + .../synaptic_models/GABA_InhToExc.json | 7 + .../synaptic_models/GABA_InhToInh.json | 7 + .../synaptic_models/instanteneousExc.json | 5 + .../synaptic_models/instanteneousInh.json | 5 + .../sources/chapter01/analyze_simulation.py | 6 + .../sources/chapter01/build_network.py | 15 + .../sources/chapter01/circuit_config.json | 23 + .../electrophysiology/472363762_fit.json | 173 + .../electrophysiology/472912177_fit.json | 163 + .../electrophysiology/473862421_fit.json | 163 + .../electrophysiology/473863035_fit.json | 173 + .../electrophysiology/473863510_fit.json | 173 + ...i14_IVSCC_-169250.03.02.01_471087815_m.swc | 1534 +++ .../morphology/Nr5a1_471087815_m.swc | 1534 +++ ...i14_IVSCC_-169125.03.01.01_469628681_m.swc | 1250 ++ ...i14_IVSCC_-176847.04.02.01_470522102_m.swc | 1966 +++ .../morphology/Pvalb_469628681_m.swc | 1250 ++ .../morphology/Pvalb_470522102_m.swc | 1966 +++ ...i14_IVSCC_-168053.05.01.01_325404214_m.swc | 2194 ++++ .../morphology/Rorb_325404214_m.swc | 2194 ++++ ...i14_IVSCC_-177300.01.02.01_473845048_m.swc | 3786 ++++++ .../biophysical/morphology/Scnn1a.swc | 3786 ++++++ .../components/hoc_templates/BioAllen_old.hoc | 21 + .../components/hoc_templates/BioAxonStub.hoc | 61 + .../components/hoc_templates/Biophys1.hoc | 34 + .../components/intfire/IntFire1_exc_1.json | 5 + .../components/intfire/IntFire1_inh_1.json | 5 + .../mechanisms/modfiles/CaDynamics.mod | 40 + .../components/mechanisms/modfiles/Ca_HVA.mod | 82 + .../components/mechanisms/modfiles/Ca_LVA.mod | 69 + .../components/mechanisms/modfiles/Ih.mod | 71 + .../components/mechanisms/modfiles/Im.mod | 62 + .../components/mechanisms/modfiles/Im_v2.mod | 59 + .../components/mechanisms/modfiles/K_P.mod | 71 + .../components/mechanisms/modfiles/K_T.mod | 68 + .../components/mechanisms/modfiles/Kd.mod | 62 + .../mechanisms/modfiles/Kv2like.mod | 86 + .../components/mechanisms/modfiles/Kv3_1.mod | 54 + .../components/mechanisms/modfiles/NaTa.mod | 95 + .../components/mechanisms/modfiles/NaTs.mod | 95 + .../components/mechanisms/modfiles/NaV.mod | 186 + .../components/mechanisms/modfiles/Nap.mod | 77 + .../components/mechanisms/modfiles/SK.mod | 56 + .../mechanisms/modfiles/vecevent.mod | 71 + .../recXelectrodes/linear_electrode.csv | 87 + .../recXelectrodes/mesh_electrode.csv | 388 + .../recXelectrodes/mesh_electrode_half.csv | 216 + .../synaptic_models/AMPA_ExcToExc.json | 6 + .../synaptic_models/AMPA_ExcToInh.json | 6 + .../synaptic_models/GABA_InhToExc.json | 7 + .../synaptic_models/GABA_InhToInh.json | 7 + .../synaptic_models/instanteneousExc.json | 5 + .../synaptic_models/instanteneousInh.json | 5 + .../tutorial/sources/chapter01/run_bionet.py | 23 + .../sources/chapter01/simulation_config.json | 49 + .../472363762_fit.json | 145 + .../morphologies/Scnn1a_473845048_m.swc | 2595 ++++ .../synaptic_models/AMPA_ExcToExc.json | 6 + .../sources/chapter02/build_bionet.py | 35 + .../sources/chapter02/circuit_config.json | 32 + .../sources/chapter02/create_spikes.py | 5 + .../tutorial/sources/chapter02/run_bionet.py | 23 + .../sources/chapter02/simulation_config.json | 46 + .../sources/chapter02/thalamus_spikes.csv | 11 + .../sources/chapter03/analyze_results.py | 3 + .../472363762_fit.json | 145 + .../morphologies/Scnn1a_473845048_m.swc | 2595 ++++ .../synaptic_models/AMPA_ExcToExc.json | 6 + .../sources/chapter03/build_cortex.py | 35 + .../sources/chapter03/build_thalamus.py | 36 + .../sources/chapter03/circuit_config.json | 36 + .../sources/chapter03/create_spikes.py | 5 + .../tutorial/sources/chapter03/run_bionet.py | 23 + .../sources/chapter03/simulation_config.json | 46 + .../sources/chapter03/thalamus_spikes.csv | 101 + .../sources/chapter04/analyze_results.py | 5 + .../472363762_fit.json | 145 + .../472912177_fit.json | 137 + .../biophys_components/morphologies/Pvalb.swc | 1645 +++ .../morphologies/Scnn1a.swc | 2595 ++++ .../IntFire1_exc_1.json | 5 + .../IntFire1_inh_1.json | 5 + .../synaptic_models/AMPA_ExcToExc.json | 6 + .../synaptic_models/AMPA_ExcToInh.json | 6 + .../synaptic_models/GABA_InhToExc.json | 7 + .../synaptic_models/GABA_InhToInh.json | 7 + .../synaptic_models/instanteneousExc.json | 5 + .../synaptic_models/instanteneousInh.json | 5 + .../sources/chapter04/build_cortex.py | 189 + .../sources/chapter04/build_thalamus.py | 69 + .../sources/chapter04/circuit_config.json | 36 + .../tutorial/sources/chapter04/run_bionet.py | 23 + .../sources/chapter04/simulation_config.json | 50 + .../sources/chapter05/build_network.py | 168 + .../tutorial/sources/chapter05/config.json | 77 + .../converted_network/V1_V1_edge_types.csv | 9 + .../converted_network/V1_node_types.csv | 5 + .../V1_node_types_bionet.csv | 5 + .../sources/chapter05/run_pointnet.py | 20 + .../sources/chapter06/circuit_config.json | 37 + .../V1_node_types_bionet.csv | 5 + .../V1_node_types_popnet.csv | 5 + .../tutorial/sources/chapter06/lgn_rates.csv | 2 + .../tutorial/sources/chapter06/run_popnet.py | 24 + .../sources/chapter06/simulation_config.json | 30 + bmtk-vb/requirements.txt | 1 + bmtk-vb/setup.py | 95 + bmtk-vb/test.h5 | Bin 0 -> 57520 bytes cortical-column/BBP_build_network.py | 571 + cortical-column/_run.sh | 98 + cortical-column/_run_calc_ecp.sh | 66 + cortical-column/_run_calc_ecp_100um.sh | 69 + cortical-column/base_config.json | 103 + cortical-column/build_network.py | 666 + cortical-column/calc_ecp.py | 370 + cortical-column/calc_ecp_100um.py | 314 + cortical-column/cells.json | 2596 ++++ cortical-column/configure.py | 190 + cortical-column/count_layer_segments.py | 43 + cortical-column/layers.py | 23 + cortical-column/myplotters.py | 468 + cortical-column/plot_combined.py | 489 + cortical-column/run.py | 247 + cortical-column/sbatch.sh | 98 + cortical-column/stimulus.py | 252 + cortical-column/utils.py | 232 + 1299 files changed, 206437 insertions(+) create mode 100644 analysis/mars/io/common.py create mode 100644 analysis/mars/io/nsenwb.py create mode 100755 analysis/mars/preprocess.py create mode 100755 analysis/mars/preprocess_100um.sh create mode 100644 analysis/mars/preprocess_layer_ei.py create mode 100644 analysis/mars/preprocess_layers.sh create mode 100644 analysis/mars/preprocess_slices.py create mode 100644 analysis/mars/signal_processing/__init__.py create mode 100644 analysis/mars/signal_processing/bandpass.py create mode 100644 analysis/mars/signal_processing/common_referencing.py create mode 100644 analysis/mars/signal_processing/fft.py create mode 100644 analysis/mars/signal_processing/hilbert_transform.py create mode 100644 analysis/mars/signal_processing/linenoise_notch.py create mode 100644 analysis/mars/signal_processing/resample.py create mode 100644 analysis/mars/signal_processing/smooth.py create mode 100644 analysis/mars/utils/bands.py create mode 100644 analysis/mars/wn/wn_tokenize.py create mode 100644 analysis/simulation_analysis/ampl_vary.py create mode 100644 analysis/simulation_analysis/analysis.py create mode 100644 analysis/simulation_analysis/layer_reader.py create mode 100644 analysis/simulation_analysis/power_spectrum.py create mode 100644 analysis/simulation_analysis/power_spectrum_100um.py create mode 100644 analysis/simulation_analysis/power_spectrum_layers.py create mode 100644 analysis/simulation_analysis/raw_ecp.py create mode 100644 analysis/simulation_analysis/tone_avg_ecp.py create mode 100644 analysis/simulation_analysis/tone_figure.py create mode 100644 analysis/simulation_analysis/tone_power_spectrum.py create mode 100644 analysis/simulation_analysis/tone_spectrogram.py create mode 100644 analysis/simulation_analysis/utils.py create mode 100644 bmtk-vb/CONTRIBUTING.md create mode 100644 bmtk-vb/LICENSE.txt create mode 100644 bmtk-vb/README.md create mode 100644 bmtk-vb/bmtk.egg-info/PKG-INFO create mode 100644 bmtk-vb/bmtk.egg-info/SOURCES.txt create mode 100644 bmtk-vb/bmtk.egg-info/dependency_links.txt create mode 100644 bmtk-vb/bmtk.egg-info/requires.txt create mode 100644 bmtk-vb/bmtk.egg-info/top_level.txt create mode 100644 bmtk-vb/bmtk/__init__.py create mode 100644 bmtk-vb/bmtk/__init__.pyc create mode 100644 bmtk-vb/bmtk/__pycache__/__init__.cpython-35.pyc create mode 100644 bmtk-vb/bmtk/__pycache__/__init__.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/analyzer/__init__.py create mode 100644 bmtk-vb/bmtk/analyzer/__init__.pyc create mode 100644 bmtk-vb/bmtk/analyzer/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/analyzer/cell_vars.py create mode 100644 bmtk-vb/bmtk/analyzer/firing_rates.py create mode 100644 bmtk-vb/bmtk/analyzer/io_tools.py create mode 100644 bmtk-vb/bmtk/analyzer/spike_trains.py create mode 100644 bmtk-vb/bmtk/analyzer/spikes_analyzer.py create mode 100644 bmtk-vb/bmtk/analyzer/spikes_loader.py create mode 100644 bmtk-vb/bmtk/analyzer/utils.py create mode 100644 bmtk-vb/bmtk/analyzer/visualization/__init__.py create mode 100644 bmtk-vb/bmtk/analyzer/visualization/__init__.pyc create mode 100644 bmtk-vb/bmtk/analyzer/visualization/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/analyzer/visualization/__pycache__/spikes.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/analyzer/visualization/rasters.py create mode 100644 bmtk-vb/bmtk/analyzer/visualization/spikes.py create mode 100644 bmtk-vb/bmtk/analyzer/visualization/spikes.pyc create mode 100644 bmtk-vb/bmtk/analyzer/visualization/widgets.py create mode 100644 bmtk-vb/bmtk/builder/__init__.py create mode 100644 bmtk-vb/bmtk/builder/__init__.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/connection_map.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/connector.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/edge.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/functor_cache.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/id_generator.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/iterator.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/network.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/node.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/node_pool.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/__pycache__/node_set.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/aux/__init__.py create mode 100644 bmtk-vb/bmtk/builder/aux/__init__.pyc create mode 100644 bmtk-vb/bmtk/builder/aux/edge_connectors.py create mode 100644 bmtk-vb/bmtk/builder/aux/edge_connectors.pyc create mode 100644 bmtk-vb/bmtk/builder/aux/node_params.py create mode 100644 bmtk-vb/bmtk/builder/aux/node_params.pyc create mode 100644 bmtk-vb/bmtk/builder/bionet/__init__.py create mode 100644 bmtk-vb/bmtk/builder/bionet/swc_reader.py create mode 100644 bmtk-vb/bmtk/builder/connection_map.py create mode 100644 bmtk-vb/bmtk/builder/connection_map.pyc create mode 100644 bmtk-vb/bmtk/builder/connector.py create mode 100644 bmtk-vb/bmtk/builder/connector.pyc create mode 100644 bmtk-vb/bmtk/builder/edge.py create mode 100644 bmtk-vb/bmtk/builder/edge.pyc create mode 100644 bmtk-vb/bmtk/builder/formats/__init__.py create mode 100644 bmtk-vb/bmtk/builder/formats/hdf5_format.py create mode 100644 bmtk-vb/bmtk/builder/formats/iformats.py create mode 100644 bmtk-vb/bmtk/builder/functor_cache.py create mode 100644 bmtk-vb/bmtk/builder/functor_cache.pyc create mode 100644 bmtk-vb/bmtk/builder/id_generator.py create mode 100644 bmtk-vb/bmtk/builder/id_generator.pyc create mode 100644 bmtk-vb/bmtk/builder/io/__init__.py create mode 100644 bmtk-vb/bmtk/builder/iterator.py create mode 100644 bmtk-vb/bmtk/builder/iterator.pyc create mode 100644 bmtk-vb/bmtk/builder/network.py create mode 100644 bmtk-vb/bmtk/builder/network.pyc create mode 100644 bmtk-vb/bmtk/builder/networks/__init__.py create mode 100644 bmtk-vb/bmtk/builder/networks/__init__.pyc create mode 100644 bmtk-vb/bmtk/builder/networks/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/networks/__pycache__/dm_network.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/networks/__pycache__/mpi_network.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/builder/networks/dm_network.py create mode 100644 bmtk-vb/bmtk/builder/networks/dm_network.pyc create mode 100644 bmtk-vb/bmtk/builder/networks/input_network.py create mode 100644 bmtk-vb/bmtk/builder/networks/mpi_network.py create mode 100644 bmtk-vb/bmtk/builder/networks/mpi_network.pyc create mode 100644 bmtk-vb/bmtk/builder/networks/nxnetwork.py create mode 100644 bmtk-vb/bmtk/builder/networks/sparse_network.py create mode 100644 bmtk-vb/bmtk/builder/node.py create mode 100644 bmtk-vb/bmtk/builder/node.pyc create mode 100644 bmtk-vb/bmtk/builder/node_pool.py create mode 100644 bmtk-vb/bmtk/builder/node_pool.pyc create mode 100644 bmtk-vb/bmtk/builder/node_set.py create mode 100644 bmtk-vb/bmtk/builder/node_set.pyc create mode 100644 bmtk-vb/bmtk/simulator/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/__init__.pyc create mode 100644 bmtk-vb/bmtk/simulator/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/#biosimulator.py# create mode 100644 bmtk-vb/bmtk/simulator/bionet/README.md create mode 100644 bmtk-vb/bmtk/simulator/bionet/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/__init__.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/biocell.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/bionetwork.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/biosimulator.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/cell.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/config.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/iclamp.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/io_tools.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/morphology.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/nml_reader.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/nrn.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/pointprocesscell.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/pointsomacell.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/pyfunction_cache.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/sonata_adaptors.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/utils.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/__pycache__/virtualcell.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/biocell.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/biocell.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/bionetwork.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/bionetwork.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/biosimulator.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/biosimulator.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/cell.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/cell.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/config.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/config.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/__init__.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/cell_models.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/synapse_models.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/synaptic_weights.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/cell_models.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/cell_models.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/synapse_models.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/synapse_models.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/synaptic_weights.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_setters/synaptic_weights.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_templates/BioAxonStub.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_templates/Biophys1.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/default_templates/advance.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/gids.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/iclamp.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/iclamp.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d/import3d_gui.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d/import3d_sec.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d/read_morphml.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda3.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d/read_nts.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/import3d/read_swc.hoc create mode 100644 bmtk-vb/bmtk/simulator/bionet/io_tools.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/io_tools.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__init__.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/ecp.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/record_cellvars.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/record_spikes.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/save_synapses.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/sim_module.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/xstim.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/xstim_waveforms.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/ecp.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/ecp.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/record_cellvars.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/record_cellvars.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/record_netcons.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/record_spikes.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/record_spikes.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/save_synapses.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/save_synapses.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/sim_module.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/sim_module.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/xstim.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/xstim.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/xstim_waveforms.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/modules/xstim_waveforms.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/morphology.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/morphology.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/nml_reader.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/nml_reader.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/nrn.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/nrn.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/pointprocesscell.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/pointprocesscell.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/pointsomacell.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/pointsomacell.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/pyfunction_cache.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/pyfunction_cache.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/schemas/config_schema.json create mode 100644 bmtk-vb/bmtk/simulator/bionet/schemas/csv_edge_types.json create mode 100644 bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_external.json create mode 100644 bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_internal.json create mode 100644 bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_external.json create mode 100644 bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_internal.json create mode 100644 bmtk-vb/bmtk/simulator/bionet/sonata_adaptors.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/sonata_adaptors.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/utils.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/utils.pyc create mode 100644 bmtk-vb/bmtk/simulator/bionet/virtualcell.py create mode 100644 bmtk-vb/bmtk/simulator/bionet/virtualcell.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/core/__init__.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/__pycache__/io_tools.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/__pycache__/network_reader.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/__pycache__/node_sets.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/__pycache__/simulator.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/__pycache__/simulator_network.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/config.py create mode 100644 bmtk-vb/bmtk/simulator/core/edge_population.py create mode 100644 bmtk-vb/bmtk/simulator/core/graph.py create mode 100644 bmtk-vb/bmtk/simulator/core/io_tools.py create mode 100644 bmtk-vb/bmtk/simulator/core/io_tools.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/network_reader.py create mode 100644 bmtk-vb/bmtk/simulator/core/network_reader.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/node_population.py create mode 100644 bmtk-vb/bmtk/simulator/core/node_sets.py create mode 100644 bmtk-vb/bmtk/simulator/core/node_sets.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/simulator.py create mode 100644 bmtk-vb/bmtk/simulator/core/simulator.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/simulator_network.py create mode 100644 bmtk-vb/bmtk/simulator/core/simulator_network.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/__init__.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/edge_adaptor.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/network_reader.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/node_adaptor.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/edge_adaptor.py create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/edge_adaptor.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/network_reader.py create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/network_reader.pyc create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/node_adaptor.py create mode 100644 bmtk-vb/bmtk/simulator/core/sonata_reader/node_adaptor.pyc create mode 100644 bmtk-vb/bmtk/simulator/filternet/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/cell.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/cell_models.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/config.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/default_setters/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/default_setters/cell_loaders.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/filternetwork.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/filters.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/filtersimulator.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/io_tools.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/__init__.py create mode 100755 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sOFF_cell_data.csv create mode 100755 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sON_cell_data.csv create mode 100755 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sus_sus_cells_v3.csv create mode 100755 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tOFF_cell_data.csv create mode 100755 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tON_cell_data.csv create mode 100755 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/trans_sus_cells_v3.csv create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cellmodel.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/cursor.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/fitfuns.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/kernel.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/lattice_unit_constructor.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/lgnmodel1.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/linearfilter.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/lnunit.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/make_cell_list.py create mode 100755 bmtk-vb/bmtk/simulator/filternet/lgnmodel/movie.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/poissongeneration.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/singleunitcell.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/spatialfilter.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/temporalfilter.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/transferfunction.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/util_fns.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/lgnmodel/utilities.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/modules/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/modules/base.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/modules/create_spikes.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/modules/record_rates.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/pyfunction_cache.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/transfer_functions.py create mode 100644 bmtk-vb/bmtk/simulator/filternet/utils.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/Image_Library.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/Image_Library_Supervised.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/analysis/LocallySparseNoise.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/analysis/StaticGratings.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/analysis/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/C_Layer.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/Readout_Layer.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/S1_Layer.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/S_Layer.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/Sb_Layer.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/ViewTunedLayer.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/mintnet/hmax/hmax.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/config.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/default_setters/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/default_setters/synapse_models.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/default_setters/synaptic_weights.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/io_tools.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/modules/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/modules/multimeter_reporter.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/modules/record_spikes.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/pointnetwork.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/pointsimulator.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/property_map.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/pyfunction_cache.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/sonata_adaptors.py create mode 100644 bmtk-vb/bmtk/simulator/pointnet/utils.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/config.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/popedge.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/popnetwork.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/popnetwork_OLD.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/popnode.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/popsimulator.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/property_schemas/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/property_schemas/base_schema.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver0.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver1.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/sonata_adaptors.py create mode 100644 bmtk-vb/bmtk/simulator/popnet/utils.py create mode 100644 bmtk-vb/bmtk/simulator/utils/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/utils/__init__.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/__pycache__/config.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/__pycache__/sim_validator.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/__pycache__/simulation_inputs.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/__pycache__/simulation_reports.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/config.py create mode 100644 bmtk-vb/bmtk/simulator/utils/config.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/graph.py create mode 100644 bmtk-vb/bmtk/simulator/utils/io.py create mode 100644 bmtk-vb/bmtk/simulator/utils/load_spikes.py create mode 100644 bmtk-vb/bmtk/simulator/utils/nwb.py create mode 100644 bmtk-vb/bmtk/simulator/utils/property_maps.py create mode 100644 bmtk-vb/bmtk/simulator/utils/scripts/convert_filters.py create mode 100644 bmtk-vb/bmtk/simulator/utils/sim_validator.py create mode 100644 bmtk-vb/bmtk/simulator/utils/sim_validator.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/simulation_inputs.py create mode 100644 bmtk-vb/bmtk/simulator/utils/simulation_inputs.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/simulation_reports.py create mode 100644 bmtk-vb/bmtk/simulator/utils/simulation_reports.pyc create mode 100644 bmtk-vb/bmtk/simulator/utils/stimulus/LocallySparseNoise.py create mode 100644 bmtk-vb/bmtk/simulator/utils/stimulus/NaturalScenes.py create mode 100644 bmtk-vb/bmtk/simulator/utils/stimulus/StaticGratings.py create mode 100644 bmtk-vb/bmtk/simulator/utils/stimulus/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/utils/stimulus/lsn.npy create mode 100644 bmtk-vb/bmtk/simulator/utils/tools/__init__.py create mode 100644 bmtk-vb/bmtk/simulator/utils/tools/process_spikes.py create mode 100644 bmtk-vb/bmtk/simulator/utils/tools/spatial.py create mode 100644 bmtk-vb/bmtk/test.py~ create mode 100644 bmtk-vb/bmtk/tests/builder/test_connection_map.py create mode 100644 bmtk-vb/bmtk/tests/builder/test_connector.py create mode 100644 bmtk-vb/bmtk/tests/builder/test_densenetwork.py create mode 100644 bmtk-vb/bmtk/tests/builder/test_edge_iterator.py create mode 100644 bmtk-vb/bmtk/tests/builder/test_id_generator.py create mode 100644 bmtk-vb/bmtk/tests/builder/test_iterator.py create mode 100644 bmtk-vb/bmtk/tests/builder/test_node_pool.py create mode 100644 bmtk-vb/bmtk/tests/builder/test_node_set.py create mode 100644 bmtk-vb/bmtk/tests/simulator/bionet/bionet_virtual_files.py create mode 100644 bmtk-vb/bmtk/tests/simulator/bionet/set_cell_params.py create mode 100644 bmtk-vb/bmtk/tests/simulator/bionet/set_syn_params.py create mode 100644 bmtk-vb/bmtk/tests/simulator/bionet/set_weights.py create mode 100644 bmtk-vb/bmtk/tests/simulator/bionet/test_biograph.py create mode 100644 bmtk-vb/bmtk/tests/simulator/bionet/test_nrn.py create mode 100644 bmtk-vb/bmtk/tests/simulator/pointnet/pointnet_virtual_files.py create mode 100644 bmtk-vb/bmtk/tests/simulator/pointnet/test_pointgraph.py create mode 100644 bmtk-vb/bmtk/tests/simulator/popnet/popnet_virtual_files.py create mode 100644 bmtk-vb/bmtk/tests/simulator/popnet/test_popgraph.py create mode 100755 bmtk-vb/bmtk/tests/simulator/utils/files/circuit_config.json create mode 100644 bmtk-vb/bmtk/tests/simulator/utils/files/config.json create mode 100644 bmtk-vb/bmtk/tests/simulator/utils/files/simulator_config.json create mode 100644 bmtk-vb/bmtk/tests/simulator/utils/test_config.py create mode 100644 bmtk-vb/bmtk/tests/simulator/utils/test_nwb.py create mode 100644 bmtk-vb/bmtk/utils/__init__.py create mode 100644 bmtk-vb/bmtk/utils/__init__.pyc create mode 100644 bmtk-vb/bmtk/utils/__pycache__/__init__.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/cell_vars/__init__.py create mode 100644 bmtk-vb/bmtk/utils/cell_vars/__init__.pyc create mode 100644 bmtk-vb/bmtk/utils/cell_vars/var_reader.py create mode 100644 bmtk-vb/bmtk/utils/cell_vars/var_reader.pyc create mode 100644 bmtk-vb/bmtk/utils/converters/__init__.py create mode 100644 bmtk-vb/bmtk/utils/converters/hoc_converter.py create mode 100644 bmtk-vb/bmtk/utils/converters/sonata/__init__.py create mode 100644 bmtk-vb/bmtk/utils/converters/sonata/edge_converters.py create mode 100644 bmtk-vb/bmtk/utils/converters/sonata/node_converters.py create mode 100644 bmtk-vb/bmtk/utils/io/Final_Sp2019_20190512.docx create mode 100644 bmtk-vb/bmtk/utils/io/__init__.py create mode 100644 bmtk-vb/bmtk/utils/io/__init__.pyc create mode 100644 bmtk-vb/bmtk/utils/io/__pycache__/__init__.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/io/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/io/__pycache__/cell_vars.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/io/__pycache__/cell_vars.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/io/__pycache__/spike_trains.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/io/_test_cell_vars.py~ create mode 100644 bmtk-vb/bmtk/utils/io/cell_vars.py create mode 100644 bmtk-vb/bmtk/utils/io/cell_vars.pyc create mode 100644 bmtk-vb/bmtk/utils/io/firing_rates.py create mode 100644 bmtk-vb/bmtk/utils/io/orig_cell_vars.py create mode 100644 bmtk-vb/bmtk/utils/io/spike_trains.py create mode 100644 bmtk-vb/bmtk/utils/io/spike_trains.pyc create mode 100644 bmtk-vb/bmtk/utils/io/tabular_network.py create mode 100644 bmtk-vb/bmtk/utils/io/tabular_network_v0.py create mode 100644 bmtk-vb/bmtk/utils/io/tabular_network_v1.py create mode 100644 bmtk-vb/bmtk/utils/property_schema.py create mode 100644 bmtk-vb/bmtk/utils/reports/__init__.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/__init__.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/adaptors/__init__.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/adaptors/csv_adaptors.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/adaptors/nwb_adaptors.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/adaptors/sonata_adaptors.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/core.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/plotting.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/spike_train_buffer.pyc create mode 100644 bmtk-vb/bmtk/utils/reports/spike_trains/spike_trains.pyc create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/default_config.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/hoc_templates/BioAllen_old.hoc create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/hoc_templates/BioAxonStub.hoc create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/hoc_templates/Biophys1.hoc create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/intfire/IntFire1_exc_1.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/intfire/IntFire1_inh_1.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/CaDynamics.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Ca_HVA.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Ca_LVA.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Ih.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Im.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Im_v2.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/K_P.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/K_T.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Kd.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Kv2like.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Kv3_1.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/NaTa.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/NaTs.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/NaV.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/Nap.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/SK.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/mechanisms/modfiles/vecevent.mod create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/run_bionet.py create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/synaptic_models/AMPA_ExcToInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/synaptic_models/GABA_InhToExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/synaptic_models/GABA_InhToInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/bionet/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/472363762_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/472912177_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/473862421_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/473863035_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/473863510_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/IntFire1_exc_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/IntFire1_inh_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/point_neuron_templates/filter_point.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/run_pointnet.py create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/synaptic_models/ExcToExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/synaptic_models/ExcToInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/synaptic_models/InhToExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/synaptic_models/InhToInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/pointnet/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/population_models/exc_model.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/population_models/inh_model.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/run_popnet.py create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/synaptic_models/ExcToExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/synaptic_models/ExcToInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/synaptic_models/InhToExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/synaptic_models/InhToInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/synaptic_models/input_ExcToExc.json create mode 100644 bmtk-vb/bmtk/utils/scripts/popnet/synaptic_models/input_ExcToInh.json create mode 100644 bmtk-vb/bmtk/utils/scripts/sonata.circuit_config.json create mode 100644 bmtk-vb/bmtk/utils/scripts/sonata.simulation_config.json create mode 100644 bmtk-vb/bmtk/utils/sim_setup.py create mode 100644 bmtk-vb/bmtk/utils/sonata/__init__.py create mode 100644 bmtk-vb/bmtk/utils/sonata/__init__.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/__init__.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/__init__.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/column_property.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/column_property.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/edge.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/edge.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/file.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/file.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/file_root.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/file_root.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/group.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/group.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/node.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/node.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/population.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/population.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/types_table.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/types_table.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/utils.cpython-36.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/__pycache__/utils.cpython-37.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/column_property.py create mode 100644 bmtk-vb/bmtk/utils/sonata/column_property.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/config.py create mode 100644 bmtk-vb/bmtk/utils/sonata/edge.py create mode 100644 bmtk-vb/bmtk/utils/sonata/edge.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/file.py create mode 100644 bmtk-vb/bmtk/utils/sonata/file.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/file_root.py create mode 100644 bmtk-vb/bmtk/utils/sonata/file_root.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/group.py create mode 100644 bmtk-vb/bmtk/utils/sonata/group.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/node.py create mode 100644 bmtk-vb/bmtk/utils/sonata/node.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/population.py create mode 100644 bmtk-vb/bmtk/utils/sonata/population.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/types_table.py create mode 100644 bmtk-vb/bmtk/utils/sonata/types_table.pyc create mode 100644 bmtk-vb/bmtk/utils/sonata/utils.py create mode 100644 bmtk-vb/bmtk/utils/sonata/utils.pyc create mode 100644 bmtk-vb/bmtk/utils/spike_trains/__init__.py create mode 100644 bmtk-vb/bmtk/utils/spike_trains/spikes_csv.py create mode 100644 bmtk-vb/bmtk/utils/spike_trains/spikes_file.py create mode 100644 bmtk-vb/build/lib/bmtk/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/cell_vars.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/firing_rates.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/io_tools.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/spike_trains.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/spikes_analyzer.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/spikes_loader.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/utils.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/visualization/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/visualization/rasters.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/visualization/spikes.py create mode 100644 bmtk-vb/build/lib/bmtk/analyzer/visualization/widgets.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/aux/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/aux/edge_connectors.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/aux/node_params.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/bionet/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/bionet/swc_reader.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/connection_map.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/connector.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/edge.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/formats/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/formats/hdf5_format.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/formats/iformats.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/functor_cache.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/id_generator.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/io/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/iterator.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/network.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/networks/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/networks/dm_network.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/networks/input_network.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/networks/mpi_network.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/networks/nxnetwork.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/networks/sparse_network.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/node.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/node_pool.py create mode 100644 bmtk-vb/build/lib/bmtk/builder/node_set.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/README.md create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/biocell.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/bionetwork.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/biosimulator.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/cell.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/config.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/default_setters/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/default_setters/cell_models.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/default_setters/synapse_models.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/default_setters/synaptic_weights.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/default_templates/BioAxonStub.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/default_templates/Biophys1.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/default_templates/advance.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/iclamp.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d/import3d_gui.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d/import3d_sec.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d/read_morphml.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d/read_nlcda.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d/read_nlcda3.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d/read_nts.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/import3d/read_swc.hoc create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/io_tools.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/ecp.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/record_cellvars.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/record_spikes.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/save_synapses.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/sim_module.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/xstim.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/modules/xstim_waveforms.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/morphology.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/nml_reader.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/nrn.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/pointprocesscell.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/pointsomacell.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/pyfunction_cache.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/schemas/config_schema.json create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/schemas/csv_edge_types.json create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/schemas/csv_node_types_external.json create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/schemas/csv_node_types_internal.json create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/schemas/csv_nodes_external.json create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/schemas/csv_nodes_internal.json create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/sonata_adaptors.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/utils.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/bionet/virtualcell.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/config.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/edge_population.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/graph.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/io_tools.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/network_reader.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/node_population.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/node_sets.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/simulator.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/simulator_network.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/sonata_reader/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/sonata_reader/edge_adaptor.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/sonata_reader/network_reader.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/core/sonata_reader/node_adaptor.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/cell.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/cell_models.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/config.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/default_setters/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/default_setters/cell_loaders.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/filternetwork.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/filters.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/filtersimulator.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/io_tools.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/cellmodel.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/cursor.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/fitfuns.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/kernel.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/lattice_unit_constructor.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/lgnmodel1.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/linearfilter.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/lnunit.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/make_cell_list.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/movie.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/poissongeneration.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/singleunitcell.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/spatialfilter.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/temporalfilter.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/transferfunction.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/util_fns.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/lgnmodel/utilities.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/modules/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/modules/base.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/modules/create_spikes.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/modules/record_rates.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/pyfunction_cache.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/transfer_functions.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/filternet/utils.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/Image_Library.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/Image_Library_Supervised.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/analysis/LocallySparseNoise.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/analysis/StaticGratings.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/analysis/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/C_Layer.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/Readout_Layer.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/S1_Layer.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/S_Layer.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/Sb_Layer.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/ViewTunedLayer.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/mintnet/hmax/hmax.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/config.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/default_setters/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/default_setters/synapse_models.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/default_setters/synaptic_weights.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/io_tools.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/modules/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/modules/multimeter_reporter.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/modules/record_spikes.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/pointnetwork.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/pointsimulator.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/property_map.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/pyfunction_cache.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/sonata_adaptors.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/pointnet/utils.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/config.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/popedge.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/popnetwork.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/popnetwork_OLD.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/popnode.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/popsimulator.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/property_schemas/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/property_schemas/base_schema.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/property_schemas/property_schema_ver0.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/property_schemas/property_schema_ver1.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/sonata_adaptors.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/popnet/utils.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/config.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/graph.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/io.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/load_spikes.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/nwb.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/property_maps.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/sim_validator.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/simulation_inputs.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/simulation_reports.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/stimulus/LocallySparseNoise.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/stimulus/NaturalScenes.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/stimulus/StaticGratings.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/stimulus/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/tools/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/tools/process_spikes.py create mode 100644 bmtk-vb/build/lib/bmtk/simulator/utils/tools/spatial.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/cell_vars/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/cell_vars/var_reader.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/converters/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/converters/hoc_converter.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/converters/sonata/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/converters/sonata/edge_converters.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/converters/sonata/node_converters.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/io/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/io/cell_vars.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/io/firing_rates.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/io/spike_trains.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/io/tabular_network.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/io/tabular_network_v0.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/io/tabular_network_v1.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/property_schema.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/scripts/sonata.circuit_config.json create mode 100644 bmtk-vb/build/lib/bmtk/utils/scripts/sonata.simulation_config.json create mode 100644 bmtk-vb/build/lib/bmtk/utils/sim_setup.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/column_property.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/config.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/edge.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/file.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/file_root.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/group.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/node.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/population.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/types_table.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/sonata/utils.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/spike_trains/__init__.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/spike_trains/spikes_csv.py create mode 100644 bmtk-vb/build/lib/bmtk/utils/spike_trains/spikes_file.py create mode 100644 bmtk-vb/docker/Dockerfile create mode 100644 bmtk-vb/docker/README.md create mode 100644 bmtk-vb/docker/entry_script.sh create mode 100644 bmtk-vb/docs/README.md create mode 100644 bmtk-vb/docs/autodocs/.nojekyll create mode 100644 bmtk-vb/docs/autodocs/Makefile create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/all_network_cropped.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/dipde_icon.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/edge_types.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/edges_h5_structure.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/ext_inputs_raster.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/graph_structure.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/levels_of_resolution.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/nest_icon.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/neuron_icon.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/node_types.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/nodes_h5_structure.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/tensorflow_icon.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/v1_raster.png create mode 100644 bmtk-vb/docs/autodocs/source/_static/images/workflow.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/aibs_sphinx.css_t create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/AIBS_Logo.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/Brain_Atlas_Logotype_SDK.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/arrow_off.gif create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/arrow_on.gif create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/arrow_over.gif create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/close_x.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/logo_AIBS.gif create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/logo_aibs_footer.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/progress_indicator.gif create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/tab_blue.gif create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/images/workflow.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/javascript/AC_RunActiveContent.js create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/javascript/appConfig.js create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/javascript/browserVersions.js create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/javascript/portal.js create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/javascript/portalFooter.js create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/javascript/portalHeader.js create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/javascript/relatedData.js create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/animation.css create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/bebasneue-webfont.eot create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/bebasneue-webfont.svg create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/bebasneue-webfont.ttf create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/bebasneue-webfont.woff create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/common_layout.css create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/portal.css create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/portal_icon_font.css create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/portal_icon_font.eot create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/portal_icon_font.svg create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/portal_icon_font.ttf create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/external_assets/stylesheets/portal_icon_font.woff create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/style/comment.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/static/style/nocomment.png create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/templates/globaltoc.html create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/templates/layout.html create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/templates/portalFooter.html create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/templates/portalHeader.html create mode 100644 bmtk-vb/docs/autodocs/source/aibs_sphinx/theme.conf create mode 100644 bmtk-vb/docs/autodocs/source/bionet.rst create mode 100644 bmtk-vb/docs/autodocs/source/bionet_config.rst create mode 100644 bmtk-vb/docs/autodocs/source/bionet_tutorial.rst create mode 100644 bmtk-vb/docs/autodocs/source/builder.rst create mode 100644 bmtk-vb/docs/autodocs/source/conf.py create mode 100644 bmtk-vb/docs/autodocs/source/filternet.rst create mode 100644 bmtk-vb/docs/autodocs/source/index.rst create mode 100644 bmtk-vb/docs/autodocs/source/installation.rst create mode 100644 bmtk-vb/docs/autodocs/source/mintnet.rst create mode 100644 bmtk-vb/docs/autodocs/source/network_file_formats.rst create mode 100644 bmtk-vb/docs/autodocs/source/pointnet.rst create mode 100644 bmtk-vb/docs/autodocs/source/popnet.rst create mode 100644 bmtk-vb/docs/autodocs/source/simulators.rst create mode 100644 bmtk-vb/docs/environments/bionet/default_config.json create mode 100644 bmtk-vb/docs/environments/bionet/hoc_templates/BioAllen_old.hoc create mode 100644 bmtk-vb/docs/environments/bionet/hoc_templates/BioAxonStub.hoc create mode 100644 bmtk-vb/docs/environments/bionet/hoc_templates/Biophys1.hoc create mode 100644 bmtk-vb/docs/environments/bionet/intfire/IntFire1_exc_1.json create mode 100644 bmtk-vb/docs/environments/bionet/intfire/IntFire1_inh_1.json create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/CaDynamics.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Ca_HVA.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Ca_LVA.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Ih.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Im.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Im_v2.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/K_P.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/K_T.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Kd.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Kv2like.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Kv3_1.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/NaTa.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/NaTs.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/NaV.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/Nap.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/SK.mod create mode 100644 bmtk-vb/docs/environments/bionet/mechanisms/modfiles/vecevent.mod create mode 100644 bmtk-vb/docs/environments/bionet/run_bionet.py create mode 100644 bmtk-vb/docs/environments/bionet/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/docs/environments/bionet/synaptic_models/AMPA_ExcToInh.json create mode 100644 bmtk-vb/docs/environments/bionet/synaptic_models/GABA_InhToExc.json create mode 100644 bmtk-vb/docs/environments/bionet/synaptic_models/GABA_InhToInh.json create mode 100644 bmtk-vb/docs/environments/bionet/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/docs/environments/bionet/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/docs/examples/NWB_files/lgn_spikes.nwb create mode 100644 bmtk-vb/docs/examples/NWB_files/tw_spikes.nwb create mode 100644 bmtk-vb/docs/examples/bio_14cells/build_network.py create mode 100755 bmtk-vb/docs/examples/bio_14cells/config.json create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/lgn_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/lgn_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/lgn_v1_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/lgn_v1_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/tw_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/tw_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/tw_v1_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/tw_v1_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/v1_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/v1_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/v1_v1_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_14cells/network/v1_v1_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_14cells/run_bionet.py create mode 100644 bmtk-vb/docs/examples/bio_450cells/build_network.py create mode 100644 bmtk-vb/docs/examples/bio_450cells/build_nodes.py create mode 100755 bmtk-vb/docs/examples/bio_450cells/config.json create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/external_internal_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/external_internal_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/external_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/external_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/internal_internal_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/internal_internal_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/internal_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells/network/internal_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells/plot_spikes.py create mode 100644 bmtk-vb/docs/examples/bio_450cells/rebuild_edge_index.py create mode 100644 bmtk-vb/docs/examples/bio_450cells/run_bionet.py create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/build_network.py create mode 100755 bmtk-vb/docs/examples/bio_450cells_exact/config.json create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/external_internal_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/external_internal_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/external_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/external_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/internal_internal_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/internal_internal_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/internal_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/network/internal_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/plot_spikes.py create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/rebuild_edge_index.py create mode 100644 bmtk-vb/docs/examples/bio_450cells_exact/run_bionet.py create mode 100644 bmtk-vb/docs/examples/bio_basic_features/README.md create mode 100644 bmtk-vb/docs/examples/bio_basic_features/build_network.py create mode 100644 bmtk-vb/docs/examples/bio_basic_features/config_iclamp.json create mode 100644 bmtk-vb/docs/examples/bio_basic_features/config_spikes_input.json create mode 100644 bmtk-vb/docs/examples/bio_basic_features/config_xstim.json create mode 100644 bmtk-vb/docs/examples/bio_basic_features/inputs/exc_spike_trains.h5 create mode 100644 bmtk-vb/docs/examples/bio_basic_features/network/bio_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_basic_features/network/bio_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_basic_features/network/virt_bio_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_basic_features/network/virt_bio_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_basic_features/network/virt_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_basic_features/network/virt_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_basic_features/network_config.json create mode 100644 bmtk-vb/docs/examples/bio_basic_features/run_bionet.py create mode 100644 bmtk-vb/docs/examples/bio_stp_models/build_network.py create mode 100644 bmtk-vb/docs/examples/bio_stp_models/config.json create mode 100644 bmtk-vb/docs/examples/bio_stp_models/inputs/stim_12_pulses.nwb create mode 100644 bmtk-vb/docs/examples/bio_stp_models/network/ext_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_stp_models/network/ext_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_stp_models/network/ext_to_slice_edge_types.csv create mode 100644 bmtk-vb/docs/examples/bio_stp_models/network/ext_to_slice_edges.h5 create mode 100644 bmtk-vb/docs/examples/bio_stp_models/network/slice_node_types.csv create mode 100644 bmtk-vb/docs/examples/bio_stp_models/network/slice_nodes.h5 create mode 100644 bmtk-vb/docs/examples/bio_stp_models/run_bionet.py create mode 100644 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/1606013050101.json create mode 100755 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/318331342_fit.json create mode 100644 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/472363762_fit.json create mode 100644 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/472912177_fit.json create mode 100644 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/473862421_fit.json create mode 100644 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/473863035_fit.json create mode 100644 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/473863510_fit.json create mode 100644 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/485184849_fit.json create mode 100755 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/nml/Cell_472363762.cell.nml create mode 100755 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/nml/Cell_472912177.cell.nml create mode 100755 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/nml/Cell_473862421.cell.nml create mode 100755 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/nml/Cell_473863035.cell.nml create mode 100755 bmtk-vb/docs/examples/biophys_components/biophysical_neuron_templates/nml/Cell_473863510.cell.nml create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/CaDynamics.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Ca_HVA.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Ca_LVA.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Ih.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Im.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Im_v2.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/K_P.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/K_T.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Kd.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Kv2like.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Kv3_1.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/NaTa.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/NaTs.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/NaV.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/Nap.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/SK.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/exp1isyn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/exp1syn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/stp1syn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/stp2syn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/stp3syn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/stp4syn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/stp5isyn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/stp5syn.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/mechanisms/modfiles/vecevent.mod create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/1606013050101.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/485184849_reconstruction.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Nr5a1-Cre_Ai14_IVSCC_-169250.03.02.01_471087815_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Nr5a1_471087815_m.swc create mode 100755 bmtk-vb/docs/examples/biophys_components/morphologies/Pvalb-IRES-Cre_Ai14-169125.03.01.01_491119617_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Pvalb-IRES-Cre_Ai14_IVSCC_-169125.03.01.01_469628681_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Pvalb-IRES-Cre_Ai14_IVSCC_-176847.04.02.01_470522102_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Pvalb_469628681_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Pvalb_470522102_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Rorb-IRES2-Cre-D_Ai14_IVSCC_-168053.05.01.01_325404214_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Rorb_325404214_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Scnn1a-Tg3-Cre_Ai14_IVSCC_-177300.01.02.01_473845048_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/morphologies/Scnn1a_473845048_m.swc create mode 100644 bmtk-vb/docs/examples/biophys_components/point_neuron_templates/IntFire1_exc_1.json create mode 100644 bmtk-vb/docs/examples/biophys_components/point_neuron_templates/IntFire1_inh_1.json create mode 100644 bmtk-vb/docs/examples/biophys_components/recXelectrodes/linear_electrode.csv create mode 100644 bmtk-vb/docs/examples/biophys_components/stimulations/485058595_0000.csv create mode 100644 bmtk-vb/docs/examples/biophys_components/stimulations/stimxmesh.csv create mode 100644 bmtk-vb/docs/examples/biophys_components/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/docs/examples/biophys_components/synaptic_models/AMPA_ExcToInh.json create mode 100644 bmtk-vb/docs/examples/biophys_components/synaptic_models/GABA_InhToExc.json create mode 100644 bmtk-vb/docs/examples/biophys_components/synaptic_models/GABA_InhToInh.json create mode 100644 bmtk-vb/docs/examples/biophys_components/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/docs/examples/biophys_components/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/docs/examples/biophys_components/synaptic_models/pvalb_pvalb.json create mode 100644 bmtk-vb/docs/examples/filter_graitings/build_cells.py create mode 100644 bmtk-vb/docs/examples/filter_graitings/cell_loaders.py create mode 100755 bmtk-vb/docs/examples/filter_graitings/config.json create mode 100644 bmtk-vb/docs/examples/filter_graitings/network/lgn_node_types.csv create mode 100644 bmtk-vb/docs/examples/filter_graitings/network/lgn_nodes.h5 create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sOFF_TF15_6.23741017_-2.1430682_4.59332686_20.0_5.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sOFF_TF1_3.5_-2.0_50.0_140.0_5.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sOFF_TF2_3.5_-2.0_10.0_100.0_25.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sOFF_TF4_3.5_-2.0_10.0_60.0_15.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sOFF_TF8_3.5_-2.0_10.0_60.0_15.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sON_TF1_3.5_-2.0_50.0_140.0_15.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sON_TF2_3.5_-2.0_50.0_140.0_5.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sON_TF4_3.5_-2.0_30.0_60.0_25.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/sON_TF8_3.5_-2.0_10.0_20.0_25.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/tOFF_TF15_3.44215357_-2.11509939_8.27421573_20.0_0.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/tOFF_TF4_3.5_-2.0_30.0_60.0_15.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/optimized_params/tOFF_TF8_4.222_-2.404_8.545_23.019_0.0_ic.pkl create mode 100644 bmtk-vb/docs/examples/filter_graitings/run_filternet.py create mode 100644 bmtk-vb/docs/examples/point_120cells/build_gids.py create mode 100644 bmtk-vb/docs/examples/point_120cells/build_network.py create mode 100644 bmtk-vb/docs/examples/point_120cells/config.json create mode 100644 bmtk-vb/docs/examples/point_120cells/create_inputs.py create mode 100644 bmtk-vb/docs/examples/point_120cells/network/cortex_cortex_edge_types.csv create mode 100644 bmtk-vb/docs/examples/point_120cells/network/cortex_cortex_edges.h5 create mode 100644 bmtk-vb/docs/examples/point_120cells/network/cortex_node_types.csv create mode 100644 bmtk-vb/docs/examples/point_120cells/network/cortex_nodes.h5 create mode 100644 bmtk-vb/docs/examples/point_120cells/network/gids.h5 create mode 100644 bmtk-vb/docs/examples/point_120cells/network/thalamus_cortex_edge_types.csv create mode 100644 bmtk-vb/docs/examples/point_120cells/network/thalamus_cortex_edges.h5 create mode 100644 bmtk-vb/docs/examples/point_120cells/network/thalamus_node_types.csv create mode 100644 bmtk-vb/docs/examples/point_120cells/network/thalamus_nodes.h5 create mode 100644 bmtk-vb/docs/examples/point_120cells/plot_spikes.py create mode 100644 bmtk-vb/docs/examples/point_120cells/run_pointnet.py create mode 100644 bmtk-vb/docs/examples/point_120cells/thalamus_spikes.csv create mode 100644 bmtk-vb/docs/examples/point_450cells/build_network.py create mode 100644 bmtk-vb/docs/examples/point_450cells/config.json create mode 100644 bmtk-vb/docs/examples/point_450cells/network/external_internal_edge_types.csv create mode 100644 bmtk-vb/docs/examples/point_450cells/network/external_internal_edges.h5 create mode 100644 bmtk-vb/docs/examples/point_450cells/network/external_node_types.csv create mode 100644 bmtk-vb/docs/examples/point_450cells/network/external_nodes.h5 create mode 100644 bmtk-vb/docs/examples/point_450cells/network/internal_internal_edge_types.csv create mode 100644 bmtk-vb/docs/examples/point_450cells/network/internal_internal_edges.h5 create mode 100644 bmtk-vb/docs/examples/point_450cells/network/internal_node_types.csv create mode 100644 bmtk-vb/docs/examples/point_450cells/network/internal_nodes.h5 create mode 100644 bmtk-vb/docs/examples/point_450cells/plot_spikes.py create mode 100644 bmtk-vb/docs/examples/point_450cells/run_pointnet.py create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/472363762_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/472912177_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/473862421_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/473863035_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/473863510_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/IntFire1_exc_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/IntFire1_inh_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/filter_point.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/iaf_psc_delta_exc.json create mode 100644 bmtk-vb/docs/examples/point_components/cell_models/iaf_psc_delta_inh.json create mode 100644 bmtk-vb/docs/examples/point_components/synaptic_models/ExcToExc.json create mode 100644 bmtk-vb/docs/examples/point_components/synaptic_models/ExcToInh.json create mode 100644 bmtk-vb/docs/examples/point_components/synaptic_models/InhToExc.json create mode 100644 bmtk-vb/docs/examples/point_components/synaptic_models/InhToInh.json create mode 100644 bmtk-vb/docs/examples/point_components/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/docs/examples/point_components/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/docs/examples/point_iclamp/config.json create mode 100644 bmtk-vb/docs/examples/point_iclamp/network/recurrent_network/edge_types.csv create mode 100644 bmtk-vb/docs/examples/point_iclamp/network/recurrent_network/edges.h5 create mode 100644 bmtk-vb/docs/examples/point_iclamp/network/recurrent_network/node_types.csv create mode 100644 bmtk-vb/docs/examples/point_iclamp/network/recurrent_network/nodes.csv create mode 100644 bmtk-vb/docs/examples/point_iclamp/network/recurrent_network/nodes.h5 create mode 100644 bmtk-vb/docs/examples/point_iclamp/run_pointnet.py create mode 100644 bmtk-vb/docs/examples/pop_2pops/build_network.py create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/pop_models/exc_model.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/pop_models/inh_model.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/synaptic_models/ExcToExc.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/synaptic_models/ExcToInh.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/synaptic_models/InhToExc.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/synaptic_models/InhToInh.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/synaptic_models/input_ExcToExc.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/components/synaptic_models/input_ExcToInh.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/config.json create mode 100644 bmtk-vb/docs/examples/pop_2pops/lgn_rates.csv create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/brunel_edge_types.csv create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/brunel_edges.h5 create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/brunel_node_types.csv create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/brunel_nodes.h5 create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/input_edge_types.csv create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/input_edges.h5 create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/input_node_types.csv create mode 100644 bmtk-vb/docs/examples/pop_2pops/network/input_nodes.h5 create mode 100644 bmtk-vb/docs/examples/pop_2pops/run_popnet.py create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/config.json create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/lgn_node_types.csv create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/lgn_nodes.h5 create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/lgn_v1_edge_types.csv create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/lgn_v1_edges.h5 create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/tw_node_types.csv create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/tw_nodes.h5 create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/tw_v1_edge_types.csv create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/tw_v1_edges.h5 create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/v1_node_types.csv create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/v1_nodes.h5 create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/v1_v1_edge_types.csv create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/network/v1_v1_edges.h5 create mode 100644 bmtk-vb/docs/examples/pop_7pops_converted/run_popnet.py create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/472363762_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/472912177_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/473862421_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/473863035_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/473863510_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/IntFire1_exc_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/IntFire1_inh_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/excitatory_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/filter_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/pop_models/inhibitory_pop.json create mode 100644 bmtk-vb/docs/examples/population_components/synaptic_models/ExcToExc.json create mode 100644 bmtk-vb/docs/examples/population_components/synaptic_models/ExcToInh.json create mode 100644 bmtk-vb/docs/examples/population_components/synaptic_models/InhToExc.json create mode 100644 bmtk-vb/docs/examples/population_components/synaptic_models/InhToInh.json create mode 100644 bmtk-vb/docs/examples/population_components/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/docs/examples/population_components/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/docs/tutorial/00_introduction.ipynb create mode 100644 bmtk-vb/docs/tutorial/01_single_cell_clamped.ipynb create mode 100644 bmtk-vb/docs/tutorial/02_single_cell_syn.ipynb create mode 100644 bmtk-vb/docs/tutorial/03_single_pop.ipynb create mode 100644 bmtk-vb/docs/tutorial/04_multi_pop.ipynb create mode 100644 bmtk-vb/docs/tutorial/05_pointnet_modeling.ipynb create mode 100644 bmtk-vb/docs/tutorial/06_population_modeling.ipynb create mode 100644 bmtk-vb/docs/tutorial/07_filter_models.ipynb create mode 100644 bmtk-vb/docs/tutorial/NetworkBuilder_Intro.ipynb create mode 100644 bmtk-vb/docs/tutorial/Simulation_Intro.ipynb create mode 100644 bmtk-vb/docs/tutorial/images/levels_of_resolution.png create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/electrophysiology/472363762_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/electrophysiology/472912177_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/electrophysiology/473862421_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/electrophysiology/473863035_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/electrophysiology/473863510_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Nr5a1-Cre_Ai14_IVSCC_-169250.03.02.01_471087815_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Nr5a1_471087815_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Pvalb-IRES-Cre_Ai14_IVSCC_-169125.03.01.01_469628681_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Pvalb-IRES-Cre_Ai14_IVSCC_-176847.04.02.01_470522102_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Pvalb_469628681_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Pvalb_470522102_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Rorb-IRES2-Cre-D_Ai14_IVSCC_-168053.05.01.01_325404214_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Rorb_325404214_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Scnn1a-Tg3-Cre_Ai14_IVSCC_-177300.01.02.01_473845048_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/biophysical/morphology/Scnn1a_473845048_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/hoc_templates/BioAllen_old.hoc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/hoc_templates/BioAxonStub.hoc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/hoc_templates/Biophys1.hoc create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/intfire/IntFire1_exc_1.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/intfire/IntFire1_inh_1.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/CaDynamics.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Ca_HVA.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Ca_LVA.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Ih.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Im.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Im_v2.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/K_P.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/K_T.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Kd.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Kv2like.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Kv3_1.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/NaTa.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/NaTs.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/NaV.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/Nap.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/SK.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/mechanisms/modfiles/vecevent.mod create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/recXelectrodes/linear_electrode.csv create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/recXelectrodes/mesh_electrode.csv create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/recXelectrodes/mesh_electrode_half.csv create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/synaptic_models/AMPA_ExcToInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/synaptic_models/GABA_InhToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/synaptic_models/GABA_InhToInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/bionet_files/components/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/analyze_simulation.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/build_network.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/circuit_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/electrophysiology/472363762_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/electrophysiology/472912177_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/electrophysiology/473862421_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/electrophysiology/473863035_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/electrophysiology/473863510_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Nr5a1-Cre_Ai14_IVSCC_-169250.03.02.01_471087815_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Nr5a1_471087815_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Pvalb-IRES-Cre_Ai14_IVSCC_-169125.03.01.01_469628681_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Pvalb-IRES-Cre_Ai14_IVSCC_-176847.04.02.01_470522102_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Pvalb_469628681_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Pvalb_470522102_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Rorb-IRES2-Cre-D_Ai14_IVSCC_-168053.05.01.01_325404214_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Rorb_325404214_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Scnn1a-Tg3-Cre_Ai14_IVSCC_-177300.01.02.01_473845048_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/biophysical/morphology/Scnn1a.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/hoc_templates/BioAllen_old.hoc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/hoc_templates/BioAxonStub.hoc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/hoc_templates/Biophys1.hoc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/intfire/IntFire1_exc_1.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/intfire/IntFire1_inh_1.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/CaDynamics.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Ca_HVA.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Ca_LVA.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Ih.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Im.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Im_v2.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/K_P.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/K_T.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Kd.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Kv2like.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Kv3_1.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/NaTa.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/NaTs.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/NaV.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/Nap.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/SK.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/mechanisms/modfiles/vecevent.mod create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/recXelectrodes/linear_electrode.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/recXelectrodes/mesh_electrode.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/recXelectrodes/mesh_electrode_half.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/synaptic_models/AMPA_ExcToInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/synaptic_models/GABA_InhToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/synaptic_models/GABA_InhToInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/components/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/run_bionet.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter01/simulation_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/biophys_components/biophysical_neuron_templates/472363762_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/biophys_components/morphologies/Scnn1a_473845048_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/biophys_components/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/build_bionet.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/circuit_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/create_spikes.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/run_bionet.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/simulation_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter02/thalamus_spikes.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/analyze_results.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/biophys_components/biophysical_neuron_templates/472363762_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/biophys_components/morphologies/Scnn1a_473845048_m.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/biophys_components/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/build_cortex.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/build_thalamus.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/circuit_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/create_spikes.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/run_bionet.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/simulation_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter03/thalamus_spikes.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/analyze_results.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/biophysical_neuron_templates/472363762_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/biophysical_neuron_templates/472912177_fit.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/morphologies/Pvalb.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/morphologies/Scnn1a.swc create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/point_neuron_templates/IntFire1_exc_1.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/point_neuron_templates/IntFire1_inh_1.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/synaptic_models/AMPA_ExcToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/synaptic_models/AMPA_ExcToInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/synaptic_models/GABA_InhToExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/synaptic_models/GABA_InhToInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/synaptic_models/instanteneousExc.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/biophys_components/synaptic_models/instanteneousInh.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/build_cortex.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/build_thalamus.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/circuit_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/run_bionet.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter04/simulation_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter05/build_network.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter05/config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter05/converted_network/V1_V1_edge_types.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter05/converted_network/V1_node_types.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter05/converted_network/V1_node_types_bionet.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter05/run_pointnet.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter06/circuit_config.json create mode 100644 bmtk-vb/docs/tutorial/sources/chapter06/converted_network/V1_node_types_bionet.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter06/converted_network/V1_node_types_popnet.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter06/lgn_rates.csv create mode 100644 bmtk-vb/docs/tutorial/sources/chapter06/run_popnet.py create mode 100644 bmtk-vb/docs/tutorial/sources/chapter06/simulation_config.json create mode 100644 bmtk-vb/requirements.txt create mode 100644 bmtk-vb/setup.py create mode 100644 bmtk-vb/test.h5 create mode 100644 cortical-column/BBP_build_network.py create mode 100644 cortical-column/_run.sh create mode 100644 cortical-column/_run_calc_ecp.sh create mode 100644 cortical-column/_run_calc_ecp_100um.sh create mode 100644 cortical-column/base_config.json create mode 100644 cortical-column/build_network.py create mode 100644 cortical-column/calc_ecp.py create mode 100644 cortical-column/calc_ecp_100um.py create mode 100644 cortical-column/cells.json create mode 100644 cortical-column/configure.py create mode 100644 cortical-column/count_layer_segments.py create mode 100644 cortical-column/layers.py create mode 100644 cortical-column/myplotters.py create mode 100644 cortical-column/plot_combined.py create mode 100644 cortical-column/run.py create mode 100644 cortical-column/sbatch.sh create mode 100644 cortical-column/stimulus.py create mode 100644 cortical-column/utils.py diff --git a/analysis/mars/io/common.py b/analysis/mars/io/common.py new file mode 100644 index 0000000..032f914 --- /dev/null +++ b/analysis/mars/io/common.py @@ -0,0 +1,89 @@ +""" +Class for performing command line arg parsing, tokenizing, etc. +""" + +__author__ = 'Vyassa Baratham ' + +import argparse +import os + +from mars import NSE_DATAROOT +from mars.io.tokenizer import Tokenizer +from mars.configs.block_directory import bl +from mars.io import NSENWB + +class MarsBaseArgParser(argparse.ArgumentParser): + def __init__(self, *args, **kwargs): + super(MarsBaseArgParser, self).__init__(*args, **kwargs) + + self.add_argument('--block', '--blockname', type=str, required=True, + help="Block whose configuration to use " + \ + "(see block_directory.py)") + self.add_argument('--nwb', type=str, required=False, default=None, + help="use this .nwb file instead of looking for one " + \ + "within the block directory. Required if not passing" + \ + "--droot") + self.add_argument('--droot', type=str, required=False, default=NSE_DATAROOT, + help="root data directory. Required if not passing --nwb") + + self._args = None + + @property + def args(self): + if not self._args: + self.parse_args() + return self._args + + def parse_args(self): + self._args = super(MarsBaseArgParser, self).parse_args() + return self._args + + def nwb_filename(self): + if self.args.nwb: + return self.args.nwb + + return os.path.join( + self.args.droot, + self.args.block.split('_')[0], + self.args.block, + '{}.nwb'.format(self.args.block) + ) + + def block_info(self): + return bl[self.args.block] + + def reader(self): + # return NWBReader(self.nwb_filename(), block_name=self.args.block) + return NSENWB.from_existing_nwb(self.args.block, self.nwb_filename()) + + def tokenizer(self): + # TODO: Load the right one based on block directory (when we put that info there) + return Tokenizer(self.reader()) + + +class MarsArgParser(MarsBaseArgParser): + def __init__(self, *args, **kwargs): + super(MarsArgParser, self).__init__(*args, **kwargs) + + self.add_argument('--device', type=str, required=True, + help="eg 'Poly' or 'ECoG'") + + +class MarsProcessedArgParser(MarsArgParser): + def __init__(self, *args, **kwargs): + super(MarsProcessedArgParser, self).__init__(*args, **kwargs) + + self.add_argument('--processed', type=str, required=False, default='Hilb_54bands', + help="which preprocessed data to use, " + \ + "eg. 'Wvlt_4to1200_54band_CAR1' (must be a key " + \ + "in processing/preprocessed/ in the .nwb file)") + +# class MarsRawArgParser(MarsArgParser): +# def __init__(self, *args, **kwargs): +# super(MarsArgParser, self).__init__(*args, **kwargs) + +# self.add_argument('--raw', type=str, required=True, +# help="which raw data to use, " + \ +# "eg. 'Wvlt_4to1200_54band_CAR1' (must be a key " + \ +# "in acquisition/Raw/ in the .nwb file)") + diff --git a/analysis/mars/io/nsenwb.py b/analysis/mars/io/nsenwb.py new file mode 100644 index 0000000..4f85de1 --- /dev/null +++ b/analysis/mars/io/nsenwb.py @@ -0,0 +1,499 @@ +""" +Write NSE Lab rodent electrophysiological response recordings to NWB +""" + +__author__ = 'Max Dougherty 1: + raise ValueError("Choose one time index method only") + + if time_idx: + time_idx = time_idx + elif time_range: + time_idx = self._index_for_time_range(time_range, dset.rate, dset.starting_time) + elif trial_query: + time_idx = self._index_for_trials(dset.rate, trial_query, pre_dur, post_dur) + else: + time_idx = slice(None) + + + # INDEX BY CHANNELS: + if dset_channels and device_channels: + raise ValueError("Choose one channel index method only") + + if dset_channels is not None: + ch_idx = dset_channels + elif device_channels is not None: + ch_idx = self._index_for_device_channels(dset, device_channels) + else: + ch_idx = slice(None) + + if dset.data.ndim < 2: + return dset.data[time_idx] + + # Prepare to zscore: + if zscore: + bl_data = np.concatenate( + list(self.index_dset(dset, dset_channels=ch_idx, trial_query="sb == 'b'")), + axis=0 + ) + bl_mean = np.mean(bl_data, axis=0) + bl_std = np.std(bl_data, axis=0) + maybe_zscore = lambda x: (x - bl_mean) / bl_std + else: + maybe_zscore = lambda x: x + + if isinstance(time_idx, types.GeneratorType): + def _iter(): + for t_idx in time_idx: + yield maybe_zscore(np.atleast_2d(dset.data[t_idx, ch_idx, ...])) + return _iter() + else: + return maybe_zscore(np.atleast_2d(dset.data[time_idx, ch_idx, ...])) + + @classmethod + def _index_for_time_range(cls, time_range, rate, starting_time=0.0): + # TODO: Allow for selecting multiple timeranges + start = int(np.round((time_range[0]-starting_time) * rate)) + stop = int(np.round((time_range[1]-starting_time) * rate)) + return slice(start, stop) + + @classmethod + def _index_for_device_channels(cls, dset, channels): + device = dset.name # TODO: is dset.name always the device name? + try: + # processed dset + electrodes_df = dset.source_timeseries.electrodes.table.to_dataframe().query('group_name == @device') + except AttributeError: + # raw dset + electrodes_df = dset.electrodes.table.to_dataframe().query('group_name == @device') + chs = {elec.location: i for i, elec in enumerate(electrodes_df.itertuples())} + return [chs[str(chnum)] for chnum in channels] + + def _index_for_trials(self, rate, trial_query=None, pre_dur=0.0, post_dur=0.0): + # Returns a generator + table = self.nwb.trials.to_dataframe() + if trial_query: + table = table.query(trial_query) + + for s in table.itertuples(): + yield self._index_for_time_range((s.start_time-pre_dur, s.stop_time+post_dur), rate) + + # def electrode_order(self, device_name, device_channels, axis='z'): + # # Get the channel order + # device_raw = self.read_raw(device_name) #TODO: Can we read electrodes without needing to go through a dataset? + # channel_positions = [] + # for ch in device_channels: + # query = 'group_name == "%s" & location == "%s"'%(device_name,ch) + # channel_positions.append(float(device_raw.electrodes.table.to_dataframe().query(query)[axis])) + # channel_positions = np.array(channel_positions) + # channel_order = np.arange(len(device_channels))[np.argsort(channel_positions)] + # return channel_order, np.sort(channel_positions) + + def has_analysis_dataset(self,device_path,device_name,dataset_name): + # Check if NWB analysis dataset exists + carr_path = path.join(self.nwb_directory,self.block_name+'.h5') + dset_path = device_path + '/' + device_name + '/' + dataset_name + if not path.exists(carr_path): + return False + with h5py.File(carr_path,'r') as f: + if not dset_path in f: + return False + return True + + def read_analysis_dataset(self,device_path,device_name,dataset_name): + # Read an NWB analysis dataset + carr_path = path.join(self.nwb_directory,self.block_name+'.h5') + dset_path = device_path + '/' + device_name + '/' + dataset_name + if not path.exists(carr_path): + return False + with h5py.File(carr_path,'r') as f: + if not dset_path in f: + return False + data = np.array(f[dset_path]) + return data + + # These functions are much less capable than index_dset() but are here for backwards compatibility + def read_trials(self, dset, pre_dur=0.0, post_dur=0.0, trial_query=None): + """ + Read data associated with a particular stimulus + """ + return self.index_dset(dset, trial_query=trial_query, pre_dur=pre_dur, post_dur=post_dur) + + def index_by_device_channels(self, dset, channels, timerange=None): + """ + dset - nwb Timeseries object + channels - device-defined channel numbers + """ + return self.index_dset(dset, device_channels=channels, time_range=timerange) + + ################### + ## OTHER METHODS ## + ################### + def device_channels(self, device, remove_bad=False): + """ + Return the device channel IDs. + """ + elec = self.nwb.electrodes + device_idx = elec['group_name'].data[:] == device + device_chs = elec['location'].data[device_idx] + if remove_bad: + bad_chs = np.array(self.block_params['bad_chs'][device]).astype('str') + device_chs = np.array([c for c in device_chs if not c in bad_chs]) + return device_chs + + def channel_positions(self, device, remove_bad=False): + """ + Return an 3 column array containing the x,y,z positions of each electrode in device + """ + elec = self.nwb.electrodes + device_idx = elec['group_name'].data[:] == device + device_chs = elec['location'].data[device_idx] + return np.array([elec['x'].data[device_idx], + elec['y'].data[device_idx], + elec['z'].data[device_idx]]) + + def ordered_channels(self, device='Poly', reverse=False): + """ + Return a list of device channel IDs (starting from 1) and dset indexes (starting from 0), + sorted by z coordinate. + Also return the corresponding z coordinates + """ + elec = self.nwb.electrodes + device_idx = elec['group_name'].data[:] == device + z = elec['z'].data[device_idx] + ch_ids = np.array([int(ch) for ch in elec['location'].data[device_idx]]) + + sort_idx = np.argsort(z) # in mars, z coordinates are positive + if reverse: + return ch_ids[sort_idx][::-1], sort_idx[::-1], np.sort(z)[::-1] + else: + return ch_ids[sort_idx], sort_idx, np.sort(z) + + def write(self, save_path=None, time=False): + tstart = datetime.now() + self.io = NWBHDF5IO(save_path, 'w') if save_path else self.io + self.io.write(self.nwb) + if time: + print('Write time for {}: {}s'.format(self.block_name,datetime.now()-tstart)) + + def close(self): + # check for self.io without throwing error + if getattr(self, 'io'): + self.io.close() + + diff --git a/analysis/mars/preprocess.py b/analysis/mars/preprocess.py new file mode 100755 index 0000000..8dd2f33 --- /dev/null +++ b/analysis/mars/preprocess.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python +from __future__ import print_function + +import argparse +import h5py +import time +import sys +import os +import logging + +import numpy as np + +from hdmf.data_utils import AbstractDataChunkIterator, DataChunk + +try: + from tqdm import tqdm +except: + def tqdm(x, *args, **kwargs): + return x + +from mars.signal_processing import resample +from mars.signal_processing import subtract_CAR +from mars.signal_processing import linenoise_notch +from mars.signal_processing import hilbert_transform +from mars.signal_processing import gaussian +from mars.utils import bands +from mars.wn import mua_signal, mua_rate +from mars.io import NSENWB + +log = logging.getLogger('mars_preprocess') +log.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') +ch = logging.StreamHandler(stream=sys.stdout) +ch.setFormatter(formatter) +ch.setLevel(logging.DEBUG) +log.addHandler(ch) + + +def _get_cfs(_cfs): + # Default: use precomputed wavelet cfs + if _cfs is None: + return bands.wavelet['cfs'] + + # Case 1: use precomputed cfs for Chang Lab or wavelet + if _cfs[0].lower() in ('chang', 'changlab'): + return bands.chang_lab['cfs'] + elif _cfs[0].lower() in ('wavelet', 'wave', 'wvlt'): + return bands.wavelet['cfs'] + + # Case 2: call to a function in bands.py + elif _cfs[0].lower() in ('log', 'logspace', 'logspaced'): + return bands.log_spaced_cfs(*[float(arg) for arg in _cfs[1:]]) + + # Case 3: list of numbers + else: + return np.array([float(cf) for cf in _cfs]) + + +def _get_sds(cfs, _sds): + # Default: use precomputed wavelet cfs + if _sds is None: + return bands.wavelet['sds'] + + # Case 1: use precomputed sds for Chang Lab or wavelet + if _sds[0].lower() in ('chang', 'changlab'): + return bands.chang_lab['sds'] + elif _sds[0].lower() in ('wavelet', 'wave', 'wvlt'): + return bands.wavelet['sds'] + + # Case 2: Call to a function in bands.py + elif _sds[0] in ('q', 'constq', 'cqt'): + return bands.const_Q_sds(cfs, *[float(arg) for arg in _sds[1:]]) + elif _sds[0] in ('sqrt', 'scaledsqrt'): + return bands.scaled_sqrt_sds(cfs, *[float(arg) for arg in _sds[1:]]) + + # Case 3: list of numbers + else: + return np.array([float(sd) for sd in _sds]) + + +def __resample(X, new_freq, old_freq, axis=-1): + assert new_freq < old_freq + n_timepts, n_ch = X.shape + if not np.allclose(new_freq, old_freq): + for ch in range(n_ch): + ch_X = X[:, ch] + yield resample(ch_X, new_freq, old_freq, axis=axis) + log.info("resampled channel {} of {}".format(ch+1, n_ch)) + +def _resample(X, new_freq, old_freq, axis=-1): + return np.stack(__resample(X, new_freq, old_freq, axis=-1)).T + +# def _resample_iterator(X, new_freq, old_freq, axis=-1): +# assert new_freq < old_freq +# n_timepts, n_ch = X.shape +# if not np.allclose(new_freq, old_freq): +# for ch in range(n_ch): +# ch_X = X[:, ch] +# yield DataChunk(data=resample(ch_x, new_freq, old_freq, axis=axis), +# selection=np.s_[:, ch]) + +class MyDataChunkIterator(AbstractDataChunkIterator): + def __init__(self, it, dtype, n_ch, n_bands, approx_timepts=200000): + self.it = it + self._dtype = dtype + self._maxshape = (None, n_ch, n_bands) + self._approx_timepts = approx_timepts + self._chunkshape = self._approx_timepts, 1, 1 + self._n_bands = n_bands + self._i = 0 + + def __iter__(self): + return self + + def __next__(self): + data = next(self.it) + ch = self._i / self._n_bands + band = self._i % self._n_bands + self._i += 1 + + return DataChunk(data=data, selection=np.s_[:data.shape[0], ch, band]) + + next = __next__ + + @property + def dtype(self): + return self._dtype + + @property + def maxshape(self): + return self._maxshape + + def recommended_chunk_shape(self): + return self._chunkshape + + def recommended_data_shape(self): + return (self._approx_timepts, self.maxshape[1], self.maxshape[2]) + +def _notch_filter(X, rate): + return linenoise_notch(X, rate) + +def _subtract_CAR(X): + subtract_CAR(X) + return X + +def _hilbert_onech(ch_X, rate, cfs, sds, final_resample): + """ + Hilbert transform one channel, band by band + First resample already performed. Rate == first_resample + """ + for i, (cf, sd) in enumerate(zip(cfs, sds)): + kernel = gaussian(ch_X, rate, cf, sd) + transform = np.abs(hilbert_transform(ch_X, rate, kernel)) + final_data = resample(transform, final_resample, rate) + log.info("done band {}".format(i)) + yield np.squeeze(final_data) + # yield DataChunk(data=final_data, selection=np.s_[:, ch, i]) + +def _hilbert_iterator(X, rate, cfs, sds, first_resample, final_resample): + n_timepts, n_ch = X.shape + for ch in range(n_ch): + # ch_X = resample(np.atleast_2d(X[:, ch]).T, first_resample, rate).T + ch_X = np.atleast_2d(resample(np.squeeze(X[:, ch]), first_resample, rate)) # HACK + yield np.stack(_hilbert_onech(ch_X, first_resample, cfs, sds, final_resample), axis=-1) + log.info("done Hilbert on channel {} of {}".format(ch+1, n_ch)) + +def _hilbert_one_by_one(X, rate, cfs, sds, first_resample, final_resample): + n_timepts, n_ch = X.shape + for ch in range(n_ch): + ch_X = _resample(np.atleast_2d(X[:, ch]).T, first_resample, rate).T + for one_band_done in _hilbert_onech(ch_X, first_resample, cfs, sds, final_resample): + yield one_band_done + log.info("done Hilbert on channel {} of {}".format(ch+1, n_ch)) + +def _hilbert_transform(X, rate, cfs, sds, first_resample, final_resample): + n_timepts, n_ch = X.shape + approx_timepts_final = float(n_timepts) * final_resample / rate + # final = None #np.zeros(shape=(n_timepts, n_ch, len(cfs)), dtype=np.float32) + # for datachunk in _hilbert_iterator(X, rate, cfs, sds, final_resample=final_resample): + # import ipdb; ipdb.set_trace() + # if final is None: + # pass + # final[datachunk.selection] = datachunk.data + + + # return np.stack(_hilbert_iterator(X, rate, cfs, sds, first_resample, final_resample), axis=1) + it = _hilbert_one_by_one(X, rate, cfs, sds, first_resample, final_resample) + return MyDataChunkIterator(it, X.dtype, n_ch, 54, approx_timepts=approx_timepts_final) + # return DataChunkIterator(data=it, maxshape=(None, n_ch, 54), dtype=np.dtype(float)) + + # x = np.stack(_hilbert_iterator(X, rate, cfs, sds, first_resample, final_resample), axis=-1) + # return x + +def _mua(X_raw, fs, lowcut, highcut, order): + mua = mua_signal(X_raw[:], fs, lowcut, highcut, order) + return mua, mua_rate(mua, fs) + +def _write_data(nsenwb, outfile, device, rate, raw_rate, X, Y, mua, mua_rate, decomp_type, cfs, sds, postfix): + def postfixed(s): + return '{}_{}'.format(s, postfix) if postfix else s + + nsenwb.add_proc(X, device, postfixed(device), rate) + nsenwb.add_proc(Y, device, postfixed('Hilb_54bands'), rate, cfs=cfs, sds=sds) + nsenwb.add_proc(mua, device, postfixed('tMUA'), raw_rate) + nsenwb.add_proc(mua_rate, device, postfixed('tMUA_rate'), rate) + + if outfile and os.path.exists(outfile): + os.remove(outfile) + nsenwb.write(save_path=outfile) + nsenwb.close() + + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Preprocessing ecog data from nwb.') + parser.add_argument('datafile', type=str, help="Input .nwb file") + parser.add_argument('--outfile', type=str, default=None, + help="Output file. Default = write to input file") + parser.add_argument('--block', type=str, required=True) + parser.add_argument('--device', '--device-name', type=str, default='ECoG') + parser.add_argument('--acquisition', '--acq', type=str, default='Raw') + parser.add_argument('--first-resample', type=float, default=None, + help='Resample data to this rate before processing. ' + + 'Omit to skip resampling before processing.') + parser.add_argument('--final-resample', type=float, default=400., + help='Resample data to this rate after processing. ' + + 'Omit to skip resampling after processing.') + parser.add_argument('--cfs', type=str, nargs='+', default=None, + help="Center frequency of the Gaussian filter. " + +""" +Must be one of the following: +1.) The name of a precomputed set of filters (choices: 'changlab', 'wavelet') +2.) The name of a function (choices: 'logspaced') followed by + args to that function (usually fmin, fmax, but see bands.py) +3.) A list of floats specifying the center frequencies +Default = precomputed wavelet 4-1200hz cfs +eg. to use the precomputed Chang lab filters, use `--cfs changlab` +eg. to use log spaced frequencies from 10-200hz, use `--cfs logspaced 10 200` +eg. to use your own list of center frequencies, use `--cfs 2 4 8 16 [...]` +""" + ) + parser.add_argument('--sds', type=str, nargs='+', default=None, + help="Standard deviation of the Gaussian filter. " + +""" +Must be one of the following: +1.) The name of a precomputed set of filters (choices: 'changlab', 'wavelet') +2.) The name of a function (choices: 'constq', 'scaledsqrt') followed by + args to that function (q-factor, or scale, etc. See bands.py) +3.) A list of floats specifying the center frequencies +Default = precomputed wavelet 4-1200hz sds +eg. to use the precomputed Chang lab filters, use `--sds changlab` +eg. to use constant Q filters with Q=4, use `--sds constq 4` +eg. to use constant filter widths of 10hz, use `--sds 10 10 10 10 [...]` +""" + ) + parser.add_argument('--no-notch', default=False, action='store_true', + help="Do not perform notch filtering") + parser.add_argument('--no-car', default=False, action='store_true', + help="Do not perform common avg reference subtraction") + parser.add_argument('--decomp-type', type=str, default='hilbert', + choices=['hilbert', 'hil'], + help="frequency decomposition method") + parser.add_argument('--no-magnitude', default=False, action='store_true', + help="Do not take the magnitude of the frequency decomp") + parser.add_argument('--no-mua', default=False, action='store_true', + help="Do not compute MUA") + parser.add_argument('--mua-range', type=float, nargs=2, default=(500, 5000), + help="critical frequencies for MUA bandpass filter") + parser.add_argument('--mua-order', type=int, default=8, + help="order for butterworth bandpass filter for MUA") + parser.add_argument('--dset-postfix', default=None, required=False, + help="String to append to nwb dset names") + # parser.add_argument('--luigi', action='store_true', required=False, default=False, + # help="use luigi logger, which doesn't go to console") + parser.add_argument('--logfile', type=str, default=None, required=False) + + args = parser.parse_args() + + if args.logfile: + fh = logging.FileHandler(args.logfile) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + fh.setFormatter(formatter) + fh.setLevel(logging.DEBUG) + log.addHandler(fh) + + # PARSE ARGS + if args.decomp_type in ('hilbert', 'hil'): + decomp_type = 'hilbert' + else: + raise NotImplementedError() + + cfs = _get_cfs(args.cfs) + sds = _get_sds(cfs, args.sds) + + if args.outfile and (args.outfile != args.infile): + raise NotImplementedError("Cannot write to different outfile until pynwb issue #668 is addressed") + + # LOAD DATA + start = time.time() + nsenwb = NSENWB.from_existing_nwb(args.block, args.datafile) + raw_dset = nsenwb.read_raw(args.device, acq_name=args.acquisition) + raw_freq = raw_dset.rate + + X = raw_dset.data + + log.info("Time to load: {} sec".format(time.time()-start)) + + # TODO: remove bad electrodes. Or maybe keep them in the file but mark them bad? + + # CAR REMOVAL + if not args.no_car: + start = time.time() + X = _subtract_CAR(X) + log.info("Time to subtract CAR: {} sec".format(time.time()-start)) + + # NOTCH FILTER + if not args.no_notch: + start = time.time() + X = _notch_filter(X, raw_dset.rate) + log.info("Time to notch filter: {} sec".format(time.time()-start)) + + # MUA RATE + if not args.no_mua: + start = time.time() + mua, mua_rate = _mua(X, raw_freq, args.mua_range[0], args.mua_range[1], args.mua_order) + log.info("Time to compute MUA rate: {} sec".format(time.time()-start)) + else: + mua, mua_rate = None, None + + # FREQUENCY DECOMPOSITION + if decomp_type == 'hilbert': + start = time.time() + Y = _hilbert_transform(X, raw_freq, cfs, sds, args.first_resample, args.final_resample) + log.info("Time to Hilbert transform: {} sec".format(time.time()-start)) + else: + raise NotImplementedError() + + # FINAL RESAMPLE + if args.final_resample: + start = time.time() + # Y = _resample(Y, args.final_resample, rate, axis=0) # Done in Hilbert + # X = _resample(X, args.final_resample, rate, axis=0) # TODO: uncomment + if mua_rate is not None: + mua_rate = _resample(mua_rate, args.final_resample, raw_freq, axis=0) + log.info("Time to resample: {} sec".format(time.time()-start)) + + # TOKENIZE + # TODO: store tokenizer class in block directory and load it here. + # For now, just assume white noise tokenizer, which may become some sort of default + import mars.tokenizers + try: + tokenizer_name = nsenwb.stim['tokenizer'] + tokenize = getattr(mars.tokenizers, tokenizer_name) + except KeyError: + log.error('no tokenizer specified in block directory') + except AttributeError: + log.error('tokenizer {} not found'.format(tokenizer_name)) + else: + tokenize(nsenwb) + + # WRITE DATA + start = time.time() + _write_data(nsenwb, args.outfile, args.device, args.final_resample, raw_freq, X, Y, mua, mua_rate, + decomp_type, cfs, sds, args.dset_postfix) + log.info("Time to write {}: {} sec".format(args.datafile, time.time()-start)) diff --git a/analysis/mars/preprocess_100um.sh b/analysis/mars/preprocess_100um.sh new file mode 100755 index 0000000..b805b91 --- /dev/null +++ b/analysis/mars/preprocess_100um.sh @@ -0,0 +1,19 @@ +infile=$1 +block=Simulation_v0 + +if [ $# -eq 0 ] + then + echo "pass input ECP nwb file as argument to this script" + exit +fi + +python scripts/preprocess.py $infile --block $block --device ECoG --acquisition Raw --first-resample 3200 --final-resample 400 --no-notch --no-car +python scripts/preprocess.py $infile --block $block --device Poly --acquisition Raw --first-resample 3200 --final-resample 400 --no-notch --no-car + +for i in `seq 0 20`; +do + python scripts/preprocess.py $infile --block $block --device ECoG --acquisition $i --first-resample 3200 \ + --final-resample 400 --no-notch --no-car --dset-postfix $i + python scripts/preprocess.py $infile --block $block --device Poly --acquisition $i --first-resample 3200 \ + --final-resample 400 --no-notch --no-car --dset-postfix $i +done diff --git a/analysis/mars/preprocess_layer_ei.py b/analysis/mars/preprocess_layer_ei.py new file mode 100644 index 0000000..e194b21 --- /dev/null +++ b/analysis/mars/preprocess_layer_ei.py @@ -0,0 +1,69 @@ +"""Script to preprocess a layer_ei contributions file so it has +processed contributions and lesions """ +import sys + +from pynwb import NWBHDF5IO + +from mars.io import NSENWB +from mars.wn import tokenize +from preprocess import _hilbert_transform, _get_cfs, _get_sds +from layer_reader import LayerReader + +FIRST_RESAMPLE = 3200.0 +FINAL_RESAMPLE = 400.0 +BLOCK = 'Simulation_v1' + +if __name__ == '__main__': + nwbfile = sys.argv[1] + cfs = _get_cfs(None) + sds = _get_sds(cfs, None) + + nsenwb = NSENWB.from_existing_nwb(BLOCK, nwbfile) + reader = LayerReader(nsenwb.nwb) + + # full CSEP: + X = reader.raw_full() + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands', + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + for layer in [1, 2, 3, 4, 5, 6]: + for ei in 'ei': + if layer == 1 and ei == 'e': + continue + + # contribution: + X = reader.raw_contrib(layer, ei) + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands_{}{}'.format(layer, ei), + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + # lesion: + X = reader.raw_lesion(layer, ei) + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands_l{}{}'.format(layer, ei), + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + # combined e/i contribution: + X = reader.raw_contrib(layer) + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands_{}'.format(layer), + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + # combined e/i lesion: + X = reader.raw_lesion(layer) + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands_l{}'.format(layer), + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + + tokenize(nsenwb) + nsenwb.write() + + + diff --git a/analysis/mars/preprocess_layers.sh b/analysis/mars/preprocess_layers.sh new file mode 100644 index 0000000..d2dda91 --- /dev/null +++ b/analysis/mars/preprocess_layers.sh @@ -0,0 +1,19 @@ +infile=$1 +block=Simulation_v0 + +if [ $# -eq 0 ] + then + echo "pass input ECP nwb file as argument to this script" + exit +fi + +python scripts/preprocess.py $infile --block $block --device ECoG --acquisition Raw --first-resample 3200 --final-resample 400 --no-notch --no-car +python scripts/preprocess.py $infile --block $block --device Poly --acquisition Raw --first-resample 3200 --final-resample 400 --no-notch --no-car + +for i in `seq 1 6`; +do + python scripts/preprocess.py $infile --block $block --device ECoG --acquisition L$i --first-resample 3200 \ + --final-resample 400 --no-notch --no-car --dset-postfix L$i + python scripts/preprocess.py $infile --block $block --device Poly --acquisition L$i --first-resample 3200 \ + --final-resample 400 --no-notch --no-car --dset-postfix L$i +done diff --git a/analysis/mars/preprocess_slices.py b/analysis/mars/preprocess_slices.py new file mode 100644 index 0000000..27a5d70 --- /dev/null +++ b/analysis/mars/preprocess_slices.py @@ -0,0 +1,50 @@ +"""Script to preprocess a 100um slice contributions file so it has +processed contributions and lesions """ +import sys + +from pynwb import NWBHDF5IO + +from mars.io import NSENWB +from mars.wn import tokenize +from preprocess import _hilbert_transform, _get_cfs, _get_sds +from layer_reader import LayerReader + +FIRST_RESAMPLE = 3200.0 +FINAL_RESAMPLE = 400.0 +BLOCK = 'Simulation_v1' +THICKNESS = 200 +NSLICES = 11 + +if __name__ == '__main__': + nwbfile = sys.argv[1] + cfs = _get_cfs(None) + sds = _get_sds(cfs, None) + + nsenwb = NSENWB.from_existing_nwb(BLOCK, nwbfile) + reader = LayerReader(nsenwb.nwb) + + # full CSEP: + X = reader.raw_full() + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands', + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + for slice_i in range(NSLICES): + # contrirbution: + X = reader.raw_slice(slice_i, thickness=THICKNESS) + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands_{}'.format(slice_i), + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + # lesion: + X = reader.raw_slice_lesion(slice_i, thickness=THICKNESS) + X = _hilbert_transform(X, reader.raw_rate(), cfs, sds, + FIRST_RESAMPLE, FINAL_RESAMPLE) + nsenwb.add_proc(X, 'ECoG', 'Hilb_54bands_l{}'.format(slice_i), + FINAL_RESAMPLE, cfs=cfs, sds=sds) + + tokenize(nsenwb) + nsenwb.write() + diff --git a/analysis/mars/signal_processing/__init__.py b/analysis/mars/signal_processing/__init__.py new file mode 100644 index 0000000..1a1052d --- /dev/null +++ b/analysis/mars/signal_processing/__init__.py @@ -0,0 +1,6 @@ +from .hilbert_transform import * +from .resample import * +from .linenoise_notch import * +from .common_referencing import * +from .bandpass import * +from .smooth import * diff --git a/analysis/mars/signal_processing/bandpass.py b/analysis/mars/signal_processing/bandpass.py new file mode 100644 index 0000000..506aef2 --- /dev/null +++ b/analysis/mars/signal_processing/bandpass.py @@ -0,0 +1,23 @@ +# Taken from https://scipy-cookbook.readthedocs.io/items/ButterworthBandpass.html + +from __future__ import print_function + +from scipy.signal import butter, filtfilt, sosfilt, sosfiltfilt + +__all__ = ['butter_bandpass'] + + + +def butter_bandpass(data, fs, lowcut, highcut, order=8, filter_fcn=sosfiltfilt): + nyq = 0.5 * fs + low = lowcut / nyq + + if nyq > highcut: + high = highcut / nyq + sos = butter(order, [low, high], btype='bandpass', output='sos') + else: + print("WARNING: Requested filter abovve nyquist frequency") + sos = butter(order, [low], btype='highpass', output='sos') + + y = filter_fcn(sos, data) + return y \ No newline at end of file diff --git a/analysis/mars/signal_processing/common_referencing.py b/analysis/mars/signal_processing/common_referencing.py new file mode 100644 index 0000000..bf3bacd --- /dev/null +++ b/analysis/mars/signal_processing/common_referencing.py @@ -0,0 +1,39 @@ +from __future__ import division +import numpy as np + + +__all__ = ['subtract_CAR', + 'subtract_common_median_reference'] + +def subtract_CAR(X, mean_frac=0.95, round_fcn=np.ceil): + """ + Compute and subtract common average reference + mean_frac - average is calculated over the middle X percent. This is X. + """ + timepts, channels = X.shape + nchs_excl = int(round_fcn(channels*(1-mean_frac)/2.0)) + avg = np.mean(np.sort(X)[:, nchs_excl:-nchs_excl], axis=1) + + return X - np.tile(avg, (channels, 1)).T + + +def subtract_common_median_reference(X, channel_axis=-2): + """ + Compute and subtract common median reference + for the entire grid. + + Parameters + ---------- + X : ndarray (..., n_channels, n_time) + Data to common median reference. + + Returns + ------- + Xp : ndarray (..., n_channels, n_time) + Common median referenced data. + """ + + median = np.nanmedian(X, axis=channel_axis, keepdims=True) + Xp = X - median + + return Xp diff --git a/analysis/mars/signal_processing/fft.py b/analysis/mars/signal_processing/fft.py new file mode 100644 index 0000000..8fc9d3d --- /dev/null +++ b/analysis/mars/signal_processing/fft.py @@ -0,0 +1,14 @@ +import numpy as np + +from numpy.fft import rfftfreq, fftfreq + +try: + from mkl_fft._numpy_fft import rfft, irfft, fft, ifft +except ImportError: + try: + from accelerate.mkl.fftpack import rfft, irfft, fft, ifft + except ImportError: + try: + from pyfftw.interfaces.numpy_fft import rfft, irfft, fft, ifft + except ImportError: + from numpy.fft import rfft, irfft, fft, ifft diff --git a/analysis/mars/signal_processing/hilbert_transform.py b/analysis/mars/signal_processing/hilbert_transform.py new file mode 100644 index 0000000..a2450b4 --- /dev/null +++ b/analysis/mars/signal_processing/hilbert_transform.py @@ -0,0 +1,84 @@ +from __future__ import division +import numpy as np +from numpy.fft import fftfreq + +from .fft import fft, ifft + +__authors__ = "Alex Bujan, Jesse Livezey" + + +__all__ = ['gaussian', 'hamming', 'hilbert_transform'] + + +def gaussian(X, rate, center, sd): + n_channels, time = X.shape + freq = fftfreq(time, 1./rate) + + k = np.exp((-(np.abs(freq) - center)**2)/(2 * (sd**2))) + + return k / k.sum() + + +def hamming(X, rate, min_freq, max_freq): + n_channels, time = X.shape + freq = fftfreq(time, 1./rate) + + pos_in_window = np.logical_and(freq >= min_freq, freq <= max_freq) + neg_in_window = np.logical_and(freq <= -min_freq, freq >= -max_freq) + + k = np.zeros(len(freq)) + window_size = np.count_nonzero(pos_in_window) + window = np.hamming(window_size) + k[pos_in_window] = window + window_size = np.count_nonzero(neg_in_window) + window = np.hamming(window_size) + k[neg_in_window] = window + + return k / k.sum() + + +def hilbert_transform(X, rate, filters=None, normalize_filters=True): + """ + Apply bandpass filtering with Hilbert transform using + a prespecified set of filters. + + Parameters + ---------- + X : ndarray (n_channels, n_time) + Input data, dimensions + rate : float + Number of samples per second. + filters : filter or list of filters (optional) + One or more bandpass filters + normalize_filters : bool + If true, normalize each filter so that its entries sum to 1 + + Returns + ------- + Xc : array + Bandpassed analytical signal (dtype: complex) + """ + if not isinstance(filters, list): + filters = [filters] + time = X.shape[-1] + freq = fftfreq(time, 1. / rate) + + # Heavyside filter + h = np.zeros(len(freq)) + h[freq > 0] = 2. + h[0] = 1. + h = h[np.newaxis, :] + + Xh = np.zeros((len(filters),) + X.shape, dtype=np.complex) + X_fft_h = fft(X) * h + for ii, f in enumerate(filters): + if f is None: + Xh[ii] = ifft(X_fft_h) + else: + if normalize_filters: + f = f / f.sum() + Xh[ii] = ifft(X_fft_h * f) + if Xh.shape[0] == 1: + return Xh[0] + + return Xh diff --git a/analysis/mars/signal_processing/linenoise_notch.py b/analysis/mars/signal_processing/linenoise_notch.py new file mode 100644 index 0000000..d811084 --- /dev/null +++ b/analysis/mars/signal_processing/linenoise_notch.py @@ -0,0 +1,68 @@ +from __future__ import division +import numpy as np +from scipy.signal import firwin2, filtfilt +from numpy.fft import rfftfreq + +try: + from accelerate.mkl.fftpack import rfft, irfft +except ImportError: + try: + from pyfftw.interfaces.numpy_fft import rfft, irfft + except ImportError: + from numpy.fft import rfft, irfft + + +__all__ = ['linenoise_notch'] + + +__authors__ = "Alex Bujan" + + +def apply_notches(X, notches, rate, fft=True): + if fft: + fs = rfftfreq(X.shape[-1], 1./rate) + delta = 1. + fd = rfft(X) + else: + nyquist = rate/2. + n_taps = 1001 + gain = [1, 1, 0, 0, 1, 1] + for notch in notches: + if fft: + window_mask = np.logical_and(fs > notch-delta, fs < notch+delta) + window_size = window_mask.sum() + window = np.hamming(window_size) + fd[:, window_mask] = (fd[:, window_mask] * + (1.-window)[np.newaxis, :]) + else: + freq = np.array([0, notch-1, notch-.5, + notch+.5, notch+1, nyquist]) / nyquist + filt = firwin2(n_taps, freq, gain) + X = filtfilt(filt, np.array([1]), X) + if fft: + X = irfft(fd) + return X + + +def linenoise_notch(X, rate): + """ + Apply Notch filter at 60 Hz and its harmonics + + Parameters + ---------- + X : array + Input data, dimensions (n_channels, n_timePoints) + rate : float + Number of samples per second + + Returns + ------- + X : array + Denoised data, dimensions (n_channels, n_timePoints) + """ + + nyquist = rate / 2 + noise_hz = 60. + notches = np.arange(noise_hz, nyquist, noise_hz) + + return apply_notches(X, notches, rate) diff --git a/analysis/mars/signal_processing/resample.py b/analysis/mars/signal_processing/resample.py new file mode 100644 index 0000000..4bfa799 --- /dev/null +++ b/analysis/mars/signal_processing/resample.py @@ -0,0 +1,156 @@ +# Clone of scipy resample w/ various fft bindings +""" +Copyright (c) 2001, 2002 Enthought, Inc. +All rights reserved. + +Copyright (c) 2003-2016 SciPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. 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. + c. Neither the name of Enthought nor the names of the SciPy Developers + 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 HOLDERS 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. +""" +import numpy as np +from scipy.fftpack import ifftshift, fftfreq +from scipy.signal import get_window +from scipy._lib.six import callable + +from .fft import fft, ifft + +__all__ = ['resample'] + + +def _resample(x, num, t=None, axis=0, window=None): + """ + Resample `x` to `num` samples using Fourier method along the given axis. + The resampled signal starts at the same value as `x` but is sampled + with a spacing of ``len(x) / num * (spacing of x)``. Because a + Fourier method is used, the signal is assumed to be periodic. + Parameters + ---------- + x : array_like + The data to be resampled. + num : int + The number of samples in the resampled signal. + t : array_like, optional + If `t` is given, it is assumed to be the sample positions + associated with the signal data in `x`. + axis : int, optional + The axis of `x` that is resampled. Default is 0. + window : array_like, callable, string, float, or tuple, optional + Specifies the window applied to the signal in the Fourier + domain. See below for details. + Returns + ------- + resampled_x or (resampled_x, resampled_t) + Either the resampled array, or, if `t` was given, a tuple + containing the resampled array and the corresponding resampled + positions. + Notes + ----- + The argument `window` controls a Fourier-domain window that tapers + the Fourier spectrum before zero-padding to alleviate ringing in + the resampled values for sampled signals you didn't intend to be + interpreted as band-limited. + If `window` is a function, then it is called with a vector of inputs + indicating the frequency bins (i.e. fftfreq(x.shape[axis]) ). + If `window` is an array of the same length as `x.shape[axis]` it is + assumed to be the window to be applied directly in the Fourier + domain (with dc and low-frequency first). + For any other type of `window`, the function `scipy.signal.get_window` + is called to generate the window. + The first sample of the returned vector is the same as the first + sample of the input vector. The spacing between samples is changed + from ``dx`` to ``dx * len(x) / num``. + If `t` is not None, then it represents the old sample positions, + and the new sample positions will be returned as well as the new + samples. + As noted, `resample` uses FFT transformations, which can be very + slow if the number of input samples is large and prime, see + `scipy.fftpack.fft`. + """ + x = np.asarray(x) + X = fft(x, axis=axis) + Nx = x.shape[axis] + if window is not None: + if callable(window): + W = window(fftfreq(Nx)) + elif isinstance(window, np.ndarray): + if window.shape != (Nx,): + raise ValueError('window must have the same length as data') + W = window + else: + W = ifftshift(get_window(window, Nx)) + newshape = [1] * x.ndim + newshape[axis] = len(W) + W.shape = newshape + X = X * W + sl = [slice(None)] * len(x.shape) + newshape = list(x.shape) + newshape[axis] = num + N = int(np.minimum(num, Nx)) + Y = np.zeros(newshape, 'D') + sl[axis] = slice(0, (N + 1) // 2) + Y[sl] = X[sl] + sl[axis] = slice(-(N - 1) // 2, None) + Y[sl] = X[sl] + y = ifft(Y, axis=axis) * (float(num) / float(Nx)) + + if x.dtype.char not in ['F', 'D']: + y = y.real + + if t is None: + return y + else: + new_t = np.arange(0, num) * (t[1] - t[0]) * Nx / float(num) + t[0] + return y, new_t + +def resample(X, new_freq, old_freq, axis=-1): + """ + Resamples the ECoG signal from the original + sampling frequency to a new frequency. + + Parameters + ---------- + X : array + Input data, dimensions (n_channels, ..., n_timePoints) + new_freq : float + New sampling frequency + old_freq : float + Original sampling frequency + axis : int (optional) + Axis along which to resample the data + + Returns + ------- + Xds : array + Downsampled data, dimensions (n_channels, ..., n_timePoints_new) + """ + time = X.shape[axis] + new_time = int(np.ceil(time * new_freq / old_freq)) + + Xds = _resample(X, new_time, axis=axis) + + return Xds + diff --git a/analysis/mars/signal_processing/smooth.py b/analysis/mars/signal_processing/smooth.py new file mode 100644 index 0000000..853d52c --- /dev/null +++ b/analysis/mars/signal_processing/smooth.py @@ -0,0 +1,62 @@ +# Adapted from https://scipy-cookbook.readthedocs.io/items/SignalSmooth.html + +import scipy +import numpy + +__all__ = ['smooth'] + +def smooth(x,window_len=11,window='hanning', mode='full'): + """smooth the data using a window with requested size. + + This method is based on the convolution of a scaled window with the signal. + The signal is prepared by introducing reflected copies of the signal + (with the window size) in both ends so that transient parts are minimized + in the begining and end part of the output signal. + + input: + x: the input signal + window_len: the dimension of the smoothing window; should be an odd integer + window: the type of window from 'flat', 'hanning', 'hamming', 'bartlett', 'blackman' + flat window will produce a moving average smoothing. + mode: see argument to fftconvolve + output: + the smoothed signal + + example: + t=linspace(-2,2,0.1) + x=sin(t)+randn(len(t))*0.1 + y=smooth(x) + + see also: + + numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, numpy.convolve + scipy.signal.lfilter + + TODO: the window parameter could be the window itself if an array instead of a string + NOTE: length(output) != length(input), to correct this: return y[(window_len/2-1):-(window_len/2)] instead of just y. + """ + + if x.ndim != 1: + raise ValueError("smooth only accepts 1 dimension arrays.") + + if x.size < window_len: + raise ValueError("Input vector needs to be bigger than window size.") + + + if window_len<3: + return x + + + if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']: + raise ValueError("Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'") + + + s=numpy.r_[x[window_len-1:0:-1],x,x[-2:-window_len-1:-1]] + #print(len(s)) + if window == 'flat': #moving average + w=numpy.ones(window_len,'d') + else: + w=eval('numpy.'+window+'(window_len)') + + y=scipy.signal.fftconvolve(w/w.sum(),s,mode=mode) + return y diff --git a/analysis/mars/utils/bands.py b/analysis/mars/utils/bands.py new file mode 100644 index 0000000..b96bb8f --- /dev/null +++ b/analysis/mars/utils/bands.py @@ -0,0 +1,74 @@ +""" +Frequency band information for different types of data processing. +""" +import os + +from scipy.io import loadmat +import numpy as np + +class DataFormat(object): + def write_preprocessed(self): + raise NotImplementedError + def read_preprocessed(self): + raise NotImplementedError + +def log_spaced_cfs(fmin, fmax, nbin=6): + """ + Center frequencies that are uniform in log space + """ + noct = np.ceil(np.log2(fmax/fmin)) + return fmin * 2**(np.arange(noct*nbin)/nbin) + +def const_Q_sds(cfs, Q=8): + return cfs/Q + +def scaled_sqrt_sds(cfs, scale=0.39): + # equivalent to: + # return scale * np.sqrt(cfs) + return 10 ** ( np.log10(scale) + .5 * (np.log10(cfs))) * np.sqrt(2.) + + +# Chang lab frequencies +fq_min = 4.0749286538265 +fq_max = 200. +scale = 7. +cfs = 2 ** (np.arange(np.log2(fq_min) * scale, np.log2(fq_max) * scale) / scale) +sds = scaled_sqrt_sds(cfs) +chang_lab = {'fq_min': fq_min, + 'fq_max': fq_max, + 'scale': scale, + 'cfs': cfs, + 'sds': sds, + 'block_path': '{}_Hilb.h5'} + +# Standard neuro bands +bands = ['theta', 'alpha', 'beta', 'high beta', 'gamma', 'high gamma', 'ultra high gamma', 'multiunit activity range'] +abrev = ['T','A','B','HB','G','HG','UHG','MUAR'] +min_freqs = [4., 9., 15., 21., 30., 70.,180.,500] +max_freqs = [8., 14., 20., 29., 59., 170.,450.,1200] +HG_freq = 200. +neuro = {'bands': bands, + 'abrev': abrev, + 'min_freqs': min_freqs, + 'max_freqs': max_freqs, + 'HG_freq': HG_freq, + 'block_path': '{}_neuro_Hilb.h5'} + +def frequency_range(abrev): + frq_ind = neuro['abrev'].index(abrev) + return [neuro['min_freqs'][frq_ind],neuro['max_freqs'][frq_ind]] + +# Wavelet 4-1200hz 54 bands +# which actually start at 2.6308 hz +wavelet_cfs = log_spaced_cfs(2.6308, 1200.0) +wavelet_sds = const_Q_sds(wavelet_cfs) +wavelet = {'cfs': wavelet_cfs, 'sds': wavelet_sds} + +if __name__ == '__main__': + # with open(os.path.join(os.path.dirname(__file__), 'cfs.4_1200.54Wvl.mat'), 'r') as matfile: + # mat = loadmat(matfile) + # cfs = np.squeeze(mat['cfs']) + # sds = 10 ** ( np.log10(.39) + .5 * (np.log10(cfs))) + # sds = np.array(sds) + # print cfs + pass diff --git a/analysis/mars/wn/wn_tokenize.py b/analysis/mars/wn/wn_tokenize.py new file mode 100644 index 0000000..7bffae2 --- /dev/null +++ b/analysis/mars/wn/wn_tokenize.py @@ -0,0 +1,83 @@ +""" +Tokenize white noise stimulus data +""" + +__author__ = 'Vyassa Baratham ' + +import numpy as np +from mars.io import NSENWB +from mars.signal_processing import smooth + +def get_stim_onsets(nsenwb, mark_name): + if 'Simulation' in nsenwb.block_name: + raw_dset = nsenwb.read_raw('ECoG') + end_time = raw_dset.data.shape[0] / raw_dset.rate + return np.arange(0.5, end_time, 0.3) + + mark_dset = nsenwb.read_mark(mark_name) + mark_fs = mark_dset.rate + mark_offset = nsenwb.stim['mark_offset'] + stim_dur = nsenwb.stim['duration'] + stim_dur_samp = stim_dur*mark_fs + + mark_threshold = 0.25 if nsenwb.stim.get('mark_is_stim') else nsenwb.stim['mark_threshold'] + thresh_crossings = np.diff( (mark_dset.data[:] > mark_threshold).astype('int'), axis=0 ) + stim_onsets = np.where(thresh_crossings > 0.5)[0] + 1 # +1 b/c diff gets rid of 1st datapoint + + real_stim_onsets = [stim_onsets[0]] + for stim_onset in stim_onsets[1:]: + # Check that each stim onset is more than 2x the stimulus duration since the previous + if stim_onset > real_stim_onsets[-1] + 2*stim_dur_samp: + real_stim_onsets.append(stim_onset) + + if len(real_stim_onsets) != nsenwb.stim['nsamples']: + print("WARNING: found {} stim onsets in block {}, but supposed to have {} samples".format( + len(real_stim_onsets), nsenwb.block_name, nsenwb.stim['nsamples'])) + + return (real_stim_onsets / mark_fs) + mark_offset + +def get_end_time(nsenwb, mark_name): + mark_dset = nsenwb.read_mark(mark_name) + end_time = mark_dset.num_samples/mark_dset.rate + return end_time + +def already_tokenized(nsenwb): + return nsenwb.nwb.trials and 'sb' in nsenwb.nwb.trials.colnames + +def tokenize(nsenwb, mark_name='recorded_mark'): + """ + Required: mark track + + Output: stim on/off as "wn" + baseline as "baseline" + """ + if already_tokenized(nsenwb): + return + + stim_onsets = get_stim_onsets(nsenwb, mark_name) + stim_dur = nsenwb.stim['duration'] + bl_start = nsenwb.stim['baseline_start'] + bl_end = nsenwb.stim['baseline_end'] + + nsenwb.add_trial_column('sb', 'Stimulus (s) or baseline (b) period') + + # Add the pre-stimulus period to baseline + # nsenwb.add_trial(start_time=0.0, stop_time=stim_onsets[0]-stim_dur, sb='b') + + for onset in stim_onsets: + nsenwb.add_trial(start_time=onset, stop_time=onset+stim_dur, sb='s') + if bl_start==bl_end: + continue + nsenwb.add_trial(start_time=onset+bl_start, stop_time=onset+bl_end, sb='b') + + # Add the period after the last stimulus to baseline + # rec_end_time = get_end_time(nsenwb,mark_name) + # nsenwb.add_trial(start_time=stim_onsets[-1]+bl_end, stop_time=rec_end_time, sb='b') + + +if __name__ == '__main__': + fn = '/data/ECoGData/R32_B6_tokenizetest.nwb' + + nsenwb = NSENWB.from_existing_nwb('R32_B6', fn) + + tokenize(nsenwb) diff --git a/analysis/simulation_analysis/ampl_vary.py b/analysis/simulation_analysis/ampl_vary.py new file mode 100644 index 0000000..467911d --- /dev/null +++ b/analysis/simulation_analysis/ampl_vary.py @@ -0,0 +1,60 @@ +""" +Plots of thalamic amplitude variations +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as colors +import matplotlib.cm as cmx + +from utils import find_layer_ei_ecp_file +from power_spectrum import PowerSpectrum, PowerSpectrumRatio + +JOBNUMS = ( + (20, '30103194'), + (23, '30103187'), + (26, '30034900'), + (29, '30034918'), + (32, '30034921'), + (35, '29812500'), + (38, '30034922'), + (44, '30034925'), +) + +def get_colors(cmap='Reds', n=len(JOBNUMS)): + color_norm = colors.Normalize(vmin=0, vmax=n+1) + scalar_map = cmx.ScalarMappable(norm=color_norm, cmap=cmap) + cmap = [scalar_map.to_rgba(i+1) for i in range(n)] + return cmap + +def plot(ps_ax, peak_ax, PS_cls=PowerSpectrum): + for color, (thal_freq, jobnum) in zip(get_colors(), JOBNUMS): + nwbfile = find_layer_ei_ecp_file(jobnum) + plt.sca(ps_ax) + plotter = PS_cls(nwbfile, '', nosave=True, color=color) + f, spectrum, errs = plotter.plot_one(0) + max_f = f[np.argmax(spectrum)] + max_resp = np.max(spectrum) + alpha = (thal_freq-18.0)/26.0 + peak_ax.scatter(max_resp, max_f, c=color, #alpha=alpha, + marker='s', s=48, edgecolors='none') + if thal_freq == 35: + peak_ax.plot(max_resp, max_f, 'ro', fillstyle='none', markersize=16, alpha=alpha) + +if __name__ == '__main__': + fig, axs = plt.subplots(2, 2, figsize=(6, 6)) + + axs[0, 0].set_xlabel('Neural freq (Hz)') + axs[0, 0].set_ylabel('Z-score') + axs[0, 1].set_xlabel('Resp. magnitude (Z-score)') + axs[0, 1].set_ylabel('Resp. peak freq (Hz)') + plot(axs[0, 0], axs[0, 1], PS_cls=PowerSpectrum) + + axs[1, 0].set_xlabel('Neural freq (Hz)') + axs[1, 0].set_ylabel('Stim/bl ratio') + axs[1, 1].set_xlabel('Resp. magnitude (ratio)') + axs[1, 1].set_ylabel('Resp. peak freq (Hz)') + plot(axs[1, 0], axs[1, 1], PS_cls=PowerSpectrumRatio) + + plt.tight_layout() + plt.savefig("ampl_vary.pdf") diff --git a/analysis/simulation_analysis/analysis.py b/analysis/simulation_analysis/analysis.py new file mode 100644 index 0000000..6430309 --- /dev/null +++ b/analysis/simulation_analysis/analysis.py @@ -0,0 +1,184 @@ +""" +Base classes for analysis objects +""" +import os +import h5py +from pynwb import NWBHDF5IO +from argparse import ArgumentParser + +import numpy as np +import matplotlib.pyplot as plt + +class BasePlotter(object): + def __init__( + # TODO: remove outdir as an arg (saving should happen manually in the calling script) + self, nwbfile, outdir, mode='r', device='ECoG', auxfile=None, + raw_dset_name='Raw', proc_dset_name='Hilb_54bands', block=None, + filetype='pdf', identifier='', no_baseline_stats=False, + channel=None, stim_i=None, tstart=None, tstop=None, + figsize=None, ax=None, + color=None, linewidth=None, label=None, nosave=False, show=False, + is_expt=False, nwb=None, + ): + """ + identifier - appended to filename + """ + self.nwbfile = nwbfile + self.auxfile = auxfile + self.device = device + self.channel = channel + self.block = block + self.raw_dset_name = raw_dset_name + self.proc_dset_name = proc_dset_name + self.outdir = outdir + self.filetype = filetype + self.identifier = identifier + self.stim_i = stim_i if stim_i is not None else 'avg' + self.tstart = tstart + self.tstop = tstop + self.is_expt = is_expt + + # TODO: remove? + self.nosave = nosave + self.show = show + + if figsize and not ax: + plt.figure(figsize=figsize) + + self.linewidth = linewidth + self.color = color + self.label = label + + if nwb: + self.nwb = nwb + else: + self.io = NWBHDF5IO(self.nwbfile, mode) + self.nwb = self.io.read() + + + if not no_baseline_stats: + self.raw_bl_stats = self._compute_raw_baseline_stats() + self.proc_bl_stats = self._compute_proc_baseline_stats() + + @property + def proc_dset(self): + try: + return self.nwb.modules[self.proc_dset_name].data_interfaces[self.device] + except KeyError: + raise KeyError("Unable to read processed data from {} (probably needs to be preprocessed)".format(self.nwbfile)) + + @property + def raw_dset(self): + return self.nwb.acquisition[self.raw_dset_name].electrical_series[self.device] + + @property + def n_ch(self): + return self.raw_dset.data.shape[1] + + def do_plots(self): + """subclasses override""" + dset = self.proc_dset + n_timepts, n_ch = dset.data.shape[:2] + for i in range(n_ch): + self.plot_one(i) + plt.clf() + + def fix_len_off_by_one(self, x, y): + """ + In case a timeseries doesn't line up with its t-axis, fix by cutting off one timepoint + from the end of whichever series is shorter + """ + if len(x) == len(y) + 1: + x = x[:-1] + elif len(x) == len(y) - 1: + y = y[:-1] + return x, y + + def get_stim_periods(self, rate=None, pre_dur=0.1, post_dur=0.1): + """ + Return stim-on times in seconds, unless rate is passed, in which case + in samples at the given sampling rate + """ + trials = self.nwb.trials + idxs = trials['sb'][:] == 's' + times = zip(trials['start_time'][idxs]-pre_dur, trials['stop_time'][idxs]+post_dur) + + if rate: + return [(int(t[0]*rate), int(t[1]*rate)) for t in times] + else: + return times + + def get_baseline_periods(self, rate=None): + """ + Return baseline period times in seconds, unless rate is passed, in which case + in samples at the given sampling rate + """ + trials = self.nwb.trials + bl_idxs = trials['sb'][:] == 'b' + times = zip(trials['start_time'][bl_idxs], trials['stop_time'][bl_idxs]) + + if rate: + return [(int(t[0]*rate), int(t[1]*rate)) for t in times] + else: + return times + + def _compute_raw_baseline_stats(self): + # TODO + return None, None + + def _compute_proc_baseline_stats(self): + """ + Compute baseline stats per frequency band for the given channel data + """ + if self.auxfile and os.path.exists(self.auxfile): + print("Using saved baseline stats") + with h5py.File(self.auxfile) as infile: + return infile['/bl_mu'][:], infile['/bl_std'][:] + else: + print("Computing baseline stats") + full_data = self.proc_dset.data + rate = self.proc_dset.rate + bl_periods = self.get_baseline_periods(rate=rate) + idx = np.zeros(full_data.shape[0], dtype=bool) + for t1, t2 in bl_periods: + idx[t1:t2] = True + bl_data = full_data[idx, ...] + bl_mean = np.average(bl_data, axis=0) + bl_std = np.std(bl_data, axis=0) + return (bl_mean, bl_std) + + def get_bfs(self): + with h5py.File(self.auxfile) as infile: + return infile['/bf'][:] + + + def run(self): + self.do_plots() + + +class PlotterArgParser(ArgumentParser): + kwarg_fields = [ + 'tstart', 'tstop', 'stim_i', 'identifier', 'filetype', 'nosave', 'show', + 'proc_dset_name', 'auxfile', + ] + def __init__(self, *args, **kwargs): + super(PlotterArgParser, self).__init__(*args, **kwargs) + self.add_argument('--nwbfile', '--nwb', type=str, required=True) + self.add_argument('--auxfile', '--aux', type=str, required=False, default=None) + self.add_argument('--outdir', type=str, required=False, default='.') + self.add_argument('--proc-dset-name', '--proc-dset', '--proc', type=str, required=False, + default='Hilb_54bands') + self.add_argument('--tstart', type=float, required=False, default=None) + self.add_argument('--tstop', type=float, required=False, default=None) + self.add_argument('--stim-i', type=int, required=False, default=None) + self.add_argument('--channel', type=int, required=False, default=None) + self.add_argument('--identifier', type=str, required=False, default='', + help='append this string to filename') + self.add_argument('--filetype', '--extension', '--ext', required=False, default='pdf') + self.add_argument('--nosave', default=False, action='store_true') + self.add_argument('--show', default=False, action='store_true') + + @property + def kwargs(self): + args = self.parse_args() + return {f: getattr(args, f) for f in PlotterArgParser.kwarg_fields} diff --git a/analysis/simulation_analysis/layer_reader.py b/analysis/simulation_analysis/layer_reader.py new file mode 100644 index 0000000..26b2a80 --- /dev/null +++ b/analysis/simulation_analysis/layer_reader.py @@ -0,0 +1,97 @@ +from pynwb import NWBHDF5IO + +class LayerReader(object): + def __init__(self, nwb=None, nwbfile=None, device='ECoG', + raw_dset_name='Raw', proc_dset_name='Hilb_54bands'): + """ + nwb = an ecp_layer_ei nwb file object + nwbfile = pointer to an nwb file + """ + + if nwb: + self.nwb = nwb + elif nwbfile: + self.nwbfile = nwbfile + self.io = NWBHDF5IO(nwbfile, 'r') + self.nwb = self.io.read() + else: + raise ValueError('Must specify either nwb or nwbfile') + + self.device = device + self.raw_dset_name = raw_dset_name + self.proc_dset_name = proc_dset_name + + self.raw_dset = self.nwb.acquisition[raw_dset_name].electrical_series[device] + + def raw_rate(self): + return self.raw_dset.rate + + def raw_full(self): + return self.raw_dset.data[:] + + def raw_contrib_dset_name(self, layer, ei): + return '{}{}'.format(layer, ei) + + def raw_contrib(self, layer=None, ei=None): + if layer and ei: + return self.nwb.acquisition[self.raw_contrib_dset_name(layer, ei)] \ + .electrical_series[self.device].data[:] + elif layer and not ei: + if layer == 1: + return self.raw_contrib(1, 'i') + return self.raw_contrib(layer, 'e') + self.raw_contrib(layer, 'i') + elif ei and not layer: + raise NotImplementedError("Total e/i across all layers not implemented yet") + else: + raise ValueError("Must specify layer or ei") + + def raw_lesion(self, layer=None, ei=None): + return self.raw_full() - self.raw_contrib(layer, ei) + + def raw_slice(self, slice_i, thickness=100): + if thickness == 100: + return self.nwb.acquisition[str(slice_i)].electrical_series[self.device].data[:] + elif thickness == 200: + if slice_i == 10: + return self.raw_slice(2*slice_i) # slice_i = 21 does not exist + else: + return self.raw_slice(2*slice_i) + self.raw_slice(2*slice_i + 1) + else: + raise ValueError("Can only take 100 or 200um slices") + + def raw_slice_lesion(self, slice_i, thickness=100): + return self.raw_full() - self.raw_slice(slice_i, thickness=thickness) + + ############################################################### + """Hilbert (processed) data, which must be preprocessed into + contributions, not calculated on the fly. Most scripts that need + this data will just grab it directly from the nwb, but here's a + convenient way to do that""" + ############################################################### + + @property + def proc_dset(self): + return self.nwb.modules[self.proc_dset_name].data_interfaces[self.device] + + def proc_rate(self): + return self.proc_dset.rate + + def proc_contrib(self, layer=None, ei=None): + if layer and ei: + return self.get_proc_dset('{}_{}{}'.format(self.proc_dset_name, layer, ei)) + elif layer and not ei: + return self.get_proc_dset('{}_{}'.format(self.proc_dset_name, layer)) + elif ei and not layer: + raise NotImplementedError("Total e/i across all layers not implemented yet") + else: + raise ValueError("Must specify layer or ei") + + def proc_lesion(self, layer=None, ei=None): + if layer and ei: + return self.get_proc_dset('{}_l{}{}'.format(self.proc_dset_name, layer, ei)) + elif layer and not ei: + return self.get_proc_dset('{}_l{}'.format(self.proc_dset_name, layer)) + elif ei and not layer: + raise NotImplementedError("Total e/i across all layers not implemented yet") + else: + raise ValueError("Must specify layer or ei") diff --git a/analysis/simulation_analysis/power_spectrum.py b/analysis/simulation_analysis/power_spectrum.py new file mode 100644 index 0000000..5da99e0 --- /dev/null +++ b/analysis/simulation_analysis/power_spectrum.py @@ -0,0 +1,316 @@ +""" +z-scored power spectrum during peak Hg response +""" + +import os +import numpy as np +import matplotlib.pyplot as plt + +from analysis import BasePlotter, PlotterArgParser + +def find_peak_signal(stim_data, rate, search_window=.005, avg_window=1, time_shift_samp=0): + """ + Average the signal on each channel/band around the peak, + where peak is defined as the maximum within some time (search_window) + of the peak response (across ALL times) of the high gamma signal + + stim_data: data during the stimulus period + shape (n_stims, n_timepts, n_channels, n_bands) + search_window: number of SECONDS to search around the high gamma peak + avg_window: number of SAMPLES around the high gamma peak to avg over + """ + n_stims, n_timepts, n_ch, n_bands = stim_data.shape + + # Average across stimulus presentations + stim_spectra = np.mean(stim_data, axis=0) + + # Compute the max in the high gamma range across all timepoints + search = int(search_window * rate) # samples + hg_signal = np.mean(stim_spectra[search+avg_window:-search-avg_window, :, 30:37], axis=-1) # 29:36? + hg_maxes = np.argmax(hg_signal, axis=0) + search + avg_window # Max of the hg signal on each electrode + print("hg maxes", hg_maxes) + + # Compute the average within the search window of the hg peaks + avg_ampl_during_peak = np.zeros(shape=stim_spectra.shape[1:]) + for i in range(n_ch): + hg_max = hg_maxes[i] + time_shift_samp + for j in range(n_bands): + maxidx = np.argmax(stim_spectra[hg_max-search:hg_max+search+1, i, j]) + hg_max - search + x = np.mean(stim_spectra[maxidx-avg_window:maxidx+avg_window+1, i, j]) + avg_ampl_during_peak[i, j] = x + + return avg_ampl_during_peak + + +class PowerSpectrum(BasePlotter): + ylabel = 'Z-score' + + def __init__(self, nwbfile, outdir, **kwargs): + self.half_width = kwargs.pop('half_width', .0025) + self.normalize = kwargs.pop('normalize', False) + self.time_shift_samp = kwargs.pop('time_shift_samp', 0) + self.errors = kwargs.pop('errors', False) + self.elinewidth = kwargs.pop('elinewidth', 0.5) + + super(PowerSpectrum, self).__init__(nwbfile, outdir, **kwargs) + + def set_normalize(self, normalize): + self.normalize = normalize + + def transform_data(self, data, bl_mean, bl_std): + """Z-score""" + return (data - bl_mean) / bl_std + + def get_spectrum(self, channel): + """ + Return the z-scored power spectrum + """ + ch_data = self.proc_dset.data[:, channel, :] + n_timepts, n_bands = ch_data.shape + rate = self.proc_dset.rate + + hw = int(self.half_width * rate) + + # Rescale to baseline mean/stdev + bl_mean, bl_std = self.proc_bl_stats + bl_mean, bl_std = bl_mean[channel, :], bl_std[channel, :] + ch_data = self.transform_data(ch_data, bl_mean, bl_std) + + # Grab center frequencies from bands table + # def log_spaced_cfs(fmin, fmax, nbin=6): + # noct = np.ceil(np.log2(fmax/fmin)) + # return fmin * 2**(np.arange(noct*nbin)/nbin) + # band_means = log_spaced_cfs(2.6308, 1200.0) + band_means = self.proc_dset.bands['band_mean'][:] + hg_band_idx = np.logical_and(band_means > 65, band_means < 170) + + # Grab stim-on data, trial average if requested + stim_periods = self.get_stim_periods(rate=rate, pre_dur=0, post_dur=0) + if self.stim_i == 'avg': + n_stim_timepts = stim_periods[0][1] - stim_periods[0][0] + stim_data = np.zeros(shape=(len(stim_periods), n_stim_timepts, n_bands)) + for i, (t1, t2) in enumerate(stim_periods): + stim_data[i, :, :] = ch_data[t1:t1+n_stim_timepts, :] + + # Calculate max of average high gamma response + # Average over stims, bands in hg range: time axis remains + hg_data = np.average(stim_data[:, :, hg_band_idx], axis=(0,2)) + max_i = np.argmax(hg_data) + self.max_i = max_i + print(max_i) + max_i += self.time_shift_samp + assert max_i - hw > 0 + + # Average over stims, time: freq (bands) axis remainds + spectrum = np.average(stim_data[:, max_i-hw:max_i+hw, :], axis=(0,1)) + errors = np.std(stim_data[:, max_i-hw:max_i+hw, :], axis=(0,1)) + else: # self.stim_i is an integer index + tstart, tstop = stim_periods[self.stim_i] + stim_data = ch_data[tstart:tstop, :] + hg_data = np.average(ch_data[tstart:tstop, hg_band_idx], axis=1) + max_i = np.argmax(hg_data) + self.max_i = max_i + spectrum = np.average(stim_data[max_i-hw:max_i+hw+1, :], axis=0) + errors = np.zeros(len(spectrum)) + + return band_means, spectrum, errors + + def get_data(self): + """ + Return all data during the stimulus period + shape (n_stims, n_timepts, n_channels, n_bands) + """ + n_timepts, n_ch, n_bands = self.proc_dset.data.shape + rate = self.proc_dset.rate + stim_periods = self.get_stim_periods(rate=rate, pre_dur=0.0, post_dur=0.0) + n_stim_timepts = stim_periods[0][1] - stim_periods[0][0] + stim_data = np.zeros(shape=(len(stim_periods), n_stim_timepts, n_ch, n_bands)) + for i, (t1, t2) in enumerate(stim_periods): + stim_data[i, ...] = self.proc_dset.data[t1:t1+n_stim_timepts, ...] + return stim_data + + def save_spectra(self): + all_ch_spectra = np.stack([self.get_spectrum(ch)[1] for ch in range(n_ch)]) + self.nwb.add_scratch(all_ch_spectra, name='power_spectrum', notes='power spectrum') + + def get_avg_spectrum(self): + """ + Channel average z-score. + This should really only be used for experimental blocks. + Does not support errorbars + """ + bl_mean, bl_std = self.proc_bl_stats + def log_spaced_cfs(fmin, fmax, nbin=6): + noct = np.ceil(np.log2(fmax/fmin)) + return fmin * 2**(np.arange(noct*nbin)/nbin) + band_means = log_spaced_cfs(2.6308, 1200.0) if self.is_expt else self.proc_dset.bands['band_mean'][:] + stim_data = self.get_data() + n_stims, n_timepts, n_ch, n_bands = stim_data.shape + bl_mean = np.tile(bl_mean, (n_stims, n_timepts, 1, 1)) + bl_std = np.tile(bl_std, (n_stims, n_timepts, 1, 1)) + stim_data = self.transform_data(stim_data, bl_mean, bl_std) + stim_peak_spectra = find_peak_signal(stim_data, self.proc_dset.rate, time_shift_samp=self.time_shift_samp) + avg_spectrum = np.average(stim_peak_spectra, axis=0) + return band_means, avg_spectrum + + def prepare_axes(self): + plt.gca().set_xscale('log') + plt.xlim([10, 1200]) + plt.xscale('log') + plt.xlabel('Frequency (Hz)') + ylabel = self.ylabel + '/max' if self.normalize else self.ylabel + plt.ylabel(ylabel) + + def plot_avg(self, **plot_args): + """channel average""" + f, avg_spectrum = self.get_avg_spectrum() + if self.normalize: + avg_spectrum = avg_spectrum / np.max(avg_spectrum) + self.prepare_axes() + plt.gca().plot(f, avg_spectrum, color=self.color, **plot_args) + + + def plot_one(self, channel, **plot_args): + """ + Make one channel's power spectrum and save it to file + """ + + band_means, spectrum, errors = self.get_spectrum(channel) + + if self.normalize: + errors = errors / max(spectrum) + spectrum = spectrum / max(spectrum) + + final_plot_args = { + 'label': self.label, + 'color': self.color, + 'linewidth': self.linewidth, + 'elinewidth': self.elinewidth, + 'capsize': 1, + } + final_plot_args.update(plot_args) + + self.prepare_axes() + plt.errorbar( + band_means, spectrum, yerr=(errors if self.errors else None), + **final_plot_args + ) + self.label = None # Prevent the label from being applied multiple times + plt.tight_layout() + + if not self.nosave: + fn = 'power_spectrum_{}_ch{:02d}_{}.{}'.format( + self.device, channel, self.identifier, self.filetype + ) + full_fn = os.path.join(self.outdir, fn) + plt.savefig(full_fn) + + if self.show: + plt.show() + + return band_means, spectrum, errors + + + def plot_one_layer_ei(self, layer, ei, contrib_or_lesion, **plot_args): + """Only valid for simulation, so channel must be 0""" + old_proc_dset_name = self.proc_dset_name # "Hilb_54bands" + old_proc_bl_stats = self.proc_bl_stats + lesion = 'l' if contrib_or_lesion in ('lesion', 'removal') else '' + self.proc_dset_name = '{}_{}{}{}'.format(old_proc_dset_name, lesion, layer, ei) + self.proc_bl_stats = self._compute_proc_baseline_stats() + + self.plot_one(0, **plot_args) + + self.proc_dset_name = old_proc_dset_name + self.proc_bl_stats = old_proc_bl_stats + + def plot_one_slice(self, slice_i, contrib_or_lesion, **plot_args): + old_proc_dset_name = self.proc_dset_name # "Hilb_54bands" + old_proc_bl_stats = self.proc_bl_stats + lesion = 'l' if contrib_or_lesion in ('lesion', 'removal') else '' + self.proc_dset_name = '{}_{}{}'.format(old_proc_dset_name, lesion, slice_i) + self.proc_bl_stats = self._compute_proc_baseline_stats() + + self.plot_one(0, **plot_args) + + self.proc_dset_name = old_proc_dset_name + self.proc_bl_stats = old_proc_bl_stats + + def plot_diff_layer_ei(self, layer, ei, contrib_or_lesion, **plot_args): + """ + Plot the difference between layer contribution/lesion and full power spectrum + """ + f, full_spectrum, full_errors = self.get_spectrum(0) + + old_proc_dset_name = self.proc_dset_name # "Hilb_54bands" + old_proc_bl_stats = self.proc_bl_stats + lesion = 'l' if contrib_or_lesion in ('lesion', 'removal') else '' + self.proc_dset_name = '{}_{}{}{}'.format(old_proc_dset_name, lesion, layer, ei) + self.proc_bl_stats = self._compute_proc_baseline_stats() + + f, layer_spectrum, layer_errors = self.get_spectrum(0) + + self.proc_dset_name = old_proc_dset_name + self.proc_bl_stats = old_proc_bl_stats + + diff = full_spectrum - layer_spectrum + + self.prepare_axes() + final_plot_args = { + 'label': self.label, + 'color': self.color, + 'linewidth': self.linewidth, + } + final_plot_args.update(plot_args) + plt.plot(f, diff, **final_plot_args) + + + +class PowerSpectrumRatio(PowerSpectrum): + ylabel = 'Stim/baseline ratio' + + def transform_data(self, data, bl_mean, bl_std): + """Ratio""" + return data / bl_mean + + def plot_one_layer_ei(self, layer, ei, contrib_or_lesion, **plot_args): + """Only valid for simulation, so channel must be 0""" + old_proc_dset_name = self.proc_dset_name # "Hilb_54bands" + lesion = 'l' if contrib_or_lesion in ('lesion', 'removal') else '' + self.proc_dset_name = '{}_{}{}{}'.format(old_proc_dset_name, lesion, layer, ei) + self.plot_one(0, **plot_args) + self.proc_dset_name = old_proc_dset_name + + def plot_one_slice(self, slice_i, contrib_or_lesion, **plot_args): + old_proc_dset_name = self.proc_dset_name # "Hilb_54bands" + lesion = 'l' if contrib_or_lesion in ('lesion', 'removal') else '' + self.proc_dset_name = '{}_{}{}'.format(old_proc_dset_name, lesion, slice_i) + self.plot_one(0, **plot_args) + self.proc_dset_name = old_proc_dset_name + + def plot_diff_layer_ei(self, layer, ei, contrib_or_lesion, **plot_args): + f, full_spectrum, full_errors = self.get_spectrum(0) + + old_proc_dset_name = self.proc_dset_name # "Hilb_54bands" + lesion = 'l' if contrib_or_lesion in ('lesion', 'removal') else '' + self.proc_dset_name = '{}_{}{}{}'.format(old_proc_dset_name, lesion, layer, ei) + f, layer_spectrum, layer_errors = self.get_spectrum(0) + self.proc_dset_name = old_proc_dset_name + + diff = full_spectrum - layer_spectrum + + self.prepare_axes() + final_plot_args = { + 'label': self.label, + 'color': self.color, + 'linewidth': self.linewidth, + } + final_plot_args.update(plot_args) + plt.plot(f, diff, **final_plot_args) + +if __name__ == '__main__': + parser = PlotterArgParser() + args = parser.parse_args() + + analysis = PowerSpectrumRatio(args.nwbfile, args.outdir, **parser.kwargs) + analysis.run() diff --git a/analysis/simulation_analysis/power_spectrum_100um.py b/analysis/simulation_analysis/power_spectrum_100um.py new file mode 100644 index 0000000..0aaafc3 --- /dev/null +++ b/analysis/simulation_analysis/power_spectrum_100um.py @@ -0,0 +1,141 @@ +"""Plot of power spectrum in each 100um slice, along with 6 graphs +showing the # of segments in layer i in each slice + +""" +import numpy as np +import json +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec, GridSpecFromSubplotSpec +import matplotlib.colors as colors +import matplotlib.cm as cmx + +from layer_reader import LayerReader +from power_spectrum import PowerSpectrum, PowerSpectrumRatio +from utils import find_slice_ecp_file, get_layer_slice_counts, numerals + +TSTART, TSTOP, TSTIM = 2400, 2700, 2500 +JOBNUM = '29812500' +TYPE = 'zscore' +# COLOR = 'Greys' +# COLOR = 'gist_rainbow' +COLOR = 'gist_heat' +THICKNESS = 200 +NSLICES = 11 +nwbfile = find_slice_ecp_file(JOBNUM, thickness=THICKNESS) + +if TYPE == 'zscore': + PS_cls = PowerSpectrum +else: + PS_cls = PowerSpectrumRatio + +def plot_contribs(reader, ax): + rate = reader.raw_rate() + istart, istop = int(round(TSTART/1000.0*rate)), int(round(TSTOP/1000.0*rate)) + offset = 0 + for slice_i, color in zip(range(NSLICES), get_colors()): + offset += .1 + ecp = reader.raw_slice(slice_i, thickness=THICKNESS)[istart:istop] + t = np.linspace(TSTART, TSTOP, len(ecp)) + ax.plot(t-TSTIM, ecp-offset, linewidth=0.5, color=color) + +def get_colors(cmap=COLOR, n=NSLICES, skipfirst=4 if COLOR=='Greys' else 1): + color_norm = colors.Normalize(vmin=0, vmax=n+skipfirst) + scalar_map = cmx.ScalarMappable(norm=color_norm, cmap=cmap) + cmap = [scalar_map.to_rgba(n-(i+skipfirst)) for i in range(n)] + return cmap + +def plot_counts(ax, slice_counts, halfgap=8): + for (slice_i, count), color in zip(slice_counts.items(), get_colors()): + top = slice_i * THICKNESS + halfgap + bottom = (slice_i + 1) * THICKNESS - halfgap + mid = (top + bottom) / 2.0 + ax.plot([count, count], [top, bottom], color=color) + # if count > 0: + # ax.text(count, mid, str(count), rotation=90, fontsize=4, + # horizontalalignment='right', verticalalignment='center') + +fig = plt.figure(figsize=(11, 4)) +gs = GridSpec(1, 20, figure=fig) + +# Raw traces +ax = plt.subplot(gs[0, 0:3]) +ax.get_yaxis().set_visible(False) +ax.set_xticks([-100, 0, 100, 200]) +ax.set_xlabel("Time (ms)") +reader = LayerReader(nwbfile=nwbfile) +plot_contribs(reader, ax) + +# Power spectra +plotter = PS_cls(nwbfile, '', device='ECoG', nosave=True) +ax = plt.subplot(gs[0, 4:10]) +plt.sca(ax) +# plt.title("Power spectra, {}um slices".format(THICKNESS)) +for slice_i, color in zip(range(NSLICES), get_colors()): + plotter.plot_one_slice(slice_i, 'contrib', color=color) + +layer_slice_counts = get_layer_slice_counts(JOBNUM, thickness=THICKNESS) +print(json.dumps(layer_slice_counts, indent=4, sort_keys=True)) +layers = [1, 2, 3, 4, 5, 6] + +# Num layer segments in each slice +for layer, slice_counts in layer_slice_counts.items(): + ax = plt.subplot(gs[0, 11+layer-1:11+layer]) + ax.set_title(numerals[layer]) + plt.sca(ax) + ax.set_ylim([2100, 0]) + if layer in (1, 2): + xlim = 14000 if THICKNESS == 100 else 25000 + ax.set_xlim([0, xlim]) + plt.xticks([xlim], rotation='vertical') + else: + xlim = 135000 if THICKNESS == 100 else 265000 + ax.set_xlim([0, xlim]) + plt.xticks([xlim], rotation='vertical') + + if layer == 1: + ax.set_ylabel("Depth (um)") + else: + ax.get_yaxis().set_visible(False) + plot_counts(ax, slice_counts) +# depth axis on L1 + +# Num total segments in each slice +ax = plt.subplot(gs[0, 17]) +plt.sca(ax) +ax.set_title("Total") +ax.set_ylim([NSLICES-0.5, -0.5]) +ax.get_yaxis().set_visible(False) +# ax.get_xaxis().set_visible(False) +xlim = 200000 if THICKNESS == 100 else 350000 +ax.set_xlim([0, xlim]) +plt.xticks([xlim], rotation='vertical') +# for spine in ['left', 'right', 'top', 'bottom']: +# ax.spines[spine].set_visible(False) +total_counts = [sum(layer_slice_counts[layer][slice_i] for layer in layers) for slice_i in range(NSLICES)] +print(total_counts) +ax.barh( + range(NSLICES), + total_counts, + color=get_colors(), + height=0.8 +) + +# Fraction of segments in each layer +subgs = GridSpecFromSubplotSpec(NSLICES, 1, subplot_spec=gs[0, 18:20]) +for slice_i, color in zip(range(NSLICES), get_colors()): + ax = plt.subplot(subgs[slice_i, 0]) + ax.axis('off') + plt.bar(layers, [layer_slice_counts[l][slice_i] for l in layers], width=0.7, color=color) +# Label layers on bottom subplot only +ax = plt.subplot(subgs[-1, 0]) +ax.axis('on') +ax.get_yaxis().set_visible(False) +for spine in ['left', 'right', 'top', 'bottom']: + ax.spines[spine].set_visible(False) +ax.set_xticks(layers) +ax.set_xticklabels([numerals[l] for l in layers]) + +col = '' if COLOR == 'Greys' else ('_'+COLOR) +plt.tight_layout() +plt.savefig('power_spectrum_{}um_{}_{}{}.pdf'.format(THICKNESS, JOBNUM, TYPE, col)) +plt.savefig('power_spectrum_100um_latest.pdf'.format(THICKNESS, JOBNUM, TYPE, col)) diff --git a/analysis/simulation_analysis/power_spectrum_layers.py b/analysis/simulation_analysis/power_spectrum_layers.py new file mode 100644 index 0000000..6ffff51 --- /dev/null +++ b/analysis/simulation_analysis/power_spectrum_layers.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec +import matplotlib.colors as colors +import matplotlib.cm as cmx + +from layer_reader import LayerReader +from power_spectrum import PowerSpectrum, PowerSpectrumRatio +from utils import find_layer_ei_ecp_file + +TSTART, TSTOP, TSTIM = 2400, 2700, 2500 +JOBNUM = '29812500' +TYPE = 'ratio' +nwbfile = find_layer_ei_ecp_file(JOBNUM) +expt_nwbfile = "/Users/vbaratham/src/simulation_analysis/R32_B6_notch_filtered.nwb" + +def get_colors(n=6): + color_norm_e = colors.Normalize(vmin=0, vmax=n+1) + scalar_map_e = cmx.ScalarMappable(norm=color_norm_e, cmap='Greys') + cmap = [scalar_map_e.to_rgba(i+1) for i in range(0, n+1)][1:] + + return cmap + +def plot_ecp(reader, ax, layer, offset=0, color='red'): + data = reader.raw_contrib(layer=layer) + rate = reader.raw_rate() + istart, istop = int(round(TSTART/1000.0*rate)), int(round(TSTOP/1000.0*rate)) + ecp = data[istart:istop] + t = np.linspace(TSTART, TSTOP, len(ecp)) + ax.plot(t-TSTIM, ecp-offset, linewidth=0.5, color=color) + +def plot_contribs(nwb, ax): + layers = [1, 2, 3, 4, 5, 6] + colors = get_colors() + offset_per_layer = .15 + reader = LayerReader(nwb=nwb) + for layer, color in zip(layers, colors): + offset = layer * offset_per_layer + plot_ecp(reader, ax, layer, offset=offset, color=color) + +def plot_power_spectra_contrib(plotter, ax, _type='contrib'): + layers = [1, 2, 3, 4, 5, 6] + colors = get_colors() + plt.sca(ax) + y = .95 + label = 'Contribution' if _type == 'contrib' else 'Lesion' + for layer, color in zip(layers, colors): + plotter.plot_one_layer_ei(layer, '', _type, color=color) + ax.text(.95, y, "L{} {}".format(layer, label), fontsize=7, color=color, + horizontalalignment='right', verticalalignment='top', transform=ax.transAxes) + y -= 0.07 + print("done layer {}".format(layer)) + +fig = plt.figure(figsize=(9, 3)) +gs = GridSpec(1, 3, figure=fig) + +PS_cls = PowerSpectrum if TYPE == 'zscore' else PowerSpectrumRatio +sim_plotter = PS_cls(nwbfile, '', device='ECoG', stim_i='avg', nosave=True, + color='red', label='In silico') + +# Raw contribs +ax = plt.subplot(gs[0, 0]) +plot_contribs(sim_plotter.nwb, ax) +ax.get_yaxis().set_visible(False) +ax.set_xlabel("Time (ms)") +ax.set_title("a", loc='left') + +ax = plt.subplot(gs[0, 1]) +plot_power_spectra_contrib(sim_plotter, ax, _type='contrib') +ax.set_title("b", loc='left') + +ax = plt.subplot(gs[0, 2]) +plot_power_spectra_contrib(sim_plotter, ax, _type='lesion') +ax.set_title("c", loc='left') + +plt.tight_layout() +plt.savefig('power_spectrum_layers_{}_{}.png'.format(TYPE, JOBNUM)) +plt.savefig('power_spectrum_layers_{}_latest.pdf'.format(TYPE)) diff --git a/analysis/simulation_analysis/raw_ecp.py b/analysis/simulation_analysis/raw_ecp.py new file mode 100644 index 0000000..732a691 --- /dev/null +++ b/analysis/simulation_analysis/raw_ecp.py @@ -0,0 +1,77 @@ +import os +import numpy as np +import matplotlib.pyplot as plt + +from utils import bandpass +from analysis import BasePlotter, PlotterArgParser + +class RawECP(BasePlotter): + def __init__(self, nwbfile, outdir, **kwargs): + self.bandpass = kwargs.pop('bandpass', None) + + super(RawECP, self).__init__(nwbfile, outdir, **kwargs) + + def add_stim_line(self, where='top'): + tr = self.nwb.trials + stim_idx = tr['sb'][:] == 's' + start_times = tr['start_time'][stim_idx] + stop_times = tr['stop_time'][stim_idx] + top = plt.ylim()[1 if where=='top' else 0] + + for (start, stop) in zip(start_times, stop_times): + if start > self.tstop or stop < self.tstart: + continue + plt.plot([start, stop], [top, top], color='blue') + + + def plot_one(self, channel): + if args.tstart is None or args.tstop is None: + raise ValueError('Must specify --tstart and --tstop for RawECP') + + fig = plt.figure(figsize=(8, 2)) + rate = self.raw_dset.rate + istart, istop = int(self.tstart*rate), int(self.tstop*rate) + ch_data = self.raw_dset.data[istart:istop, channel] + + # TODO: axis labels + t = np.arange(self.tstart, self.tstop, 1.0/rate) + + # fix off-by-one time round errors + t, ch_data = self.fix_len_off_by_one(t, ch_data) + + if self.bandpass: + print('bandpassing from {} to {}'.format(self.bandpass[0], self.bandpass[1])) + ch_data = bandpass(ch_data, rate, lowcut=self.bandpass[0], highcut=self.bandpass[1]) + + plt.plot(t, ch_data, linewidth=0.5, color='red') + self.add_stim_line() + plt.xlabel('Time (s)') + plt.tight_layout() + + if not self.nosave: + fn = 'rawECP_{}_ch{:02d}_{}.{}'.format( + self.device, channel, self.identifier, self.filetype + ) + full_fn = os.path.join(self.outdir, fn) + plt.savefig(full_fn) + + if self.show: + plt.show() + + fig.clear() + + def do_plots(self): + dset = self.raw_dset + n_timepts, n_ch = dset.data.shape[:2] + for i in range(n_ch): + self.plot_one(i) + + +if __name__ == '__main__': + parser = PlotterArgParser() + parser.add_argument('--bandpass', nargs=2, type=float, required=False, default=None, + help='low/high cutoffs to bandpass before running') + args = parser.parse_args() + + analysis = RawECP(args.nwbfile, args.outdir, tstart=args.tstart, tstop=args.tstop, filetype=args.filetype, bandpass=args.bandpass) + analysis.run() diff --git a/analysis/simulation_analysis/tone_avg_ecp.py b/analysis/simulation_analysis/tone_avg_ecp.py new file mode 100644 index 0000000..de701e0 --- /dev/null +++ b/analysis/simulation_analysis/tone_avg_ecp.py @@ -0,0 +1,86 @@ +""" +Plot the avg raw response to the BF on a given electrode +""" + +import numpy as np +import matplotlib.pyplot as plt +import h5py + +from analysis import BasePlotter, PlotterArgParser +from utils import bandpass, highpass + +class ToneAvgECP(BasePlotter): + def plot(self, channel): + rate = self.raw_dset.rate + bf = self.get_bfs()[channel] + trials = self.nwb.trials + trial_idxs = np.logical_and(np.logical_and(trials['sb'][:] == 's', trials['frq'][:] == str(bf)), trials['amp'][:] == '7') + # trial_idxs = trials['sb'][:] == 's' + start_times = trials['start_time'][trial_idxs]-0.05 + window_len_samp = int(0.15 * rate) + stim_periods = [(int(t*rate), int(t*rate) + window_len_samp) for t in start_times] + ch_data = self.raw_dset.data[:, channel] * 1000 + # ch_data = bandpass(ch_data, rate, 2, 3000) + # ch_data = highpass(ch_data, rate, 800) + all_stim_data = [ch_data[istart:istop] for istart, istop in stim_periods] + stim_data = np.stack(all_stim_data) + avg_waveform = np.average(stim_data, axis=0) + std_waveform = np.std(stim_data, axis=0) + t = np.linspace(-50, 100, len(avg_waveform)) + + # DEBUG + # for i in range(len(start_times)): + # plt.plot(t, stim_data[i, :], color='k', linewidth=0.3, alpha=0.3) + # END DEBUG + plt.fill_between(t, avg_waveform+std_waveform, avg_waveform-std_waveform, color='grey') + plt.plot(t, avg_waveform, color='black') + + # Draw stim bars + ymin, ymax = plt.ylim() + plt.plot([0, 0], [ymin, ymax], linestyle='--', linewidth=0.5, color='k') + plt.plot([50, 50], [ymin, ymax], linestyle='--', linewidth=0.5, color='k') + + # Draw peak bars + center_samp = 10 + center_time = center_samp / self.proc_dset.rate + t1, t2 = (center_time - .005) * 1000, (center_time + .005) * 1000 + plt.plot([t1, t1], [ymin, ymax], linewidth=0.3, color='red') + plt.plot([t2, t2], [ymin, ymax], linewidth=0.3, color='red') + + plt.xlim([-50, 100]) + plt.ylim([ymin, ymax]) + + plt.xlabel("Time (ms)") + plt.ylabel("Voltage (mV)") + plt.tight_layout() + + +if __name__ == '__main__': + # TONE150 (not used) + # rat = 'R72' + # block = 'R72_B6' + # rat = 'R73' + # block = 'R73_B2' + rat = 'R70' + block = 'R70_B8' + # rat = 'R75' + # block = 'R75_B8' + my_preproc = ['R70', 'R67'] + + rat = 'R32' + block = 'R32_B7' + + nwbfile = '/data/{}/{}.nwb'.format(rat, block) + auxfile = '/data/{}/{}_aux.h5'.format(rat, block) + + # Not used - all tone blocks are preprocessed by me + # proc_dset_name = 'Hilb_54bands' if rat in my_preproc else 'Wvlt_4to1200_54band_CAR0' + + for channel in range(128): + plotter = ToneAvgECP(nwbfile, '.', no_baseline_stats=True, auxfile=auxfile) + plt.figure(figsize=(4.2, 4)) + plotter.plot(channel) + plt.savefig('plots/tone_raw_{}_ch{}.pdf'.format(block, channel)) + plt.close() + print("done channel {}".format(channel)) + diff --git a/analysis/simulation_analysis/tone_figure.py b/analysis/simulation_analysis/tone_figure.py new file mode 100644 index 0000000..5b46e6e --- /dev/null +++ b/analysis/simulation_analysis/tone_figure.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +""" +Figure 1 of the High gamma ECoG paper +""" +import os, sys +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec +import matplotlib.patches as patches +import matplotlib.cm as cmx +import matplotlib.colors as colors + +from tone_avg_ecp import ToneAvgECP +from tone_spectrogram import ToneSpectrogram +from tone_power_spectrum import TonePowerSpectrum +from tone_spectrogram import ToneSpectrogram + +rat = 'R18' +block = 'R18_B12' +tstart, tstop = 40, 42.5 +# channel = 109 # for single channel plots +# channels = [10, 20, 30, 40, 50, 60, 70, 80, 124, 100, 109] # for Hg plot +if len(sys.argv) > 1: + channel = int(sys.argv[-1]) + channels = [] +else: + channel = np.random.randint(128) + channels = list(np.random.randint(128, size=10)) +channels.append(channel) +print("Channel = {}".format(channel)) +print("Channels = {}".format(channels)) +if os.path.exists("fig1_ch{}.pdf".format(channel)): + exit("Already done channel") + +nwbfile = '/data/{}/{}.nwb'.format(rat, block) +auxfile = '/data/{}/{}_aux.h5'.format(rat, block) +specfile = '/data/{}/{}_spectra.h5'.format(rat, block) + +fig = plt.figure(figsize=(7, 7)) +# gs = GridSpec(4, 3, height_ratios=(1, 2, 2, 3.8)) + +######## +# AXES +######## + +# ECoG micrograph (not produced here) + +CBAR_WD = .015 +CBAR_GAP = .005 +LEN = .5 + +# Stimulus +# stim_ax = plt.subplot(gs[0, 1:]) +stim_ax = fig.add_axes([.4, 1-.08, LEN, .08]) +stim_ax.get_xaxis().set_visible(False) +stim_ax.get_yaxis().set_visible(False) + +# Z-scored High gamma response +# hg_ax = plt.subplot(gs[1, 1:], sharex=stim_ax) +hg_ax = fig.add_axes([.4, 1-.08-.16, LEN, .16], sharex=stim_ax) +hg_ax.get_xaxis().set_visible(False) +hg_ax.set_ylabel("Hγ (Z-score)") + +freq_colorbar_ax = fig.add_axes([.4+LEN+CBAR_GAP, 1-.08-.16+.005, CBAR_WD, .23]) + +# Spectrogram +# spect_ax = plt.subplot(gs[2, 1:], sharex=stim_ax) +bottom = 1-.08-.16-.16 +spect_ax = fig.add_axes([.4, bottom, LEN, .16], sharex=stim_ax) +spect_colorbar_ax = fig.add_axes([.4+LEN+CBAR_GAP, bottom, CBAR_WD, .16]) + +SQ = .25 + +# Trial-avg raw trace +# raw_ax = plt.subplot(gs[3, 0]) +raw_ax = fig.add_axes([.08, 1-.08-.16-.16-SQ, SQ, SQ]) +raw_ax.get_xaxis().set_visible(False) + +# Trial-avg spectrogram +# avg_spect_ax = plt.subplot(gs[3, 1]) +bottom = 1-.08-.16-.16-SQ-.02-SQ +avg_spect_ax = fig.add_axes([.08, bottom, SQ, SQ]) +avg_colorbar_ax = fig.add_axes([.08+SQ+CBAR_GAP, bottom, CBAR_WD, SQ]) + +# Power spectrum +# ps_ax = plt.subplot(gs[3, 2]) +ps_ax = fig.add_axes([.5, .08, .45, .45]) + + +######## +# PLOTS +######## + + +# Trial-avg raw trace +plt.sca(raw_ax) +plotter = ToneAvgECP(nwbfile, '.', no_baseline_stats=True, auxfile=auxfile, nosave=True) +plotter.plot(channel) + +# Trial-avg spectrogram +plt.sca(avg_spect_ax) +plotter = ToneSpectrogram(nwbfile, '.', auxfile=auxfile, nosave=True) +im = plotter.plot_one(channel) +cbar = fig.colorbar(im, cax=avg_colorbar_ax) +avg_colorbar_ax.tick_params(labelsize=6) +cbar.set_label("Z-score", size=8) + +# Power spectrum +plt.sca(ps_ax) +plotter = TonePowerSpectrum(nwbfile, '.', auxfile=auxfile, half_width=.005, nosave=True) +plotter.prepare_axes() +plotter.plot_all_and_avg(specfile=specfile) + +# Stimulus +nwb = plotter.nwb +bfs = plotter.get_bfs() +trial_idxs = np.logical_and(nwb.trials['start_time'][:] > tstart, + nwb.trials['stop_time'][:] < tstop) +trial_idxs = np.logical_and(trial_idxs, nwb.trials['sb'][:] == 's') +start_times = nwb.trials['start_time'][trial_idxs] +freqs = np.array([float(f) for f in nwb.trials['frq'][trial_idxs]]) +ampls = np.array([float(f) for f in nwb.trials['amp'][trial_idxs]]) +all_freqs = np.array([float(f) for f in nwb.trials['frq'][::2]]) +fmin, fmax = np.min(all_freqs), np.max(all_freqs) +nfreqs = len(np.unique(all_freqs)) +print(bfs) +print([bfs[ch] for ch in channels]) +print(freqs) +print(ampls) + +color_norm = colors.LogNorm(vmin=fmin, vmax=fmax) +cmap = cmx.ScalarMappable(norm=color_norm, cmap='jet') +# use scalar_map.to_rgba(freq) for each freq's color +cbar = fig.colorbar(cmap, cax=freq_colorbar_ax) +cmin, cmax = freq_colorbar_ax.get_xlim() +cbar_mid = np.exp((np.log(cmin) + np.log(cmax)) / 2.0) +freq_colorbar_ax.plot(cbar_mid, bfs[channel], marker='.', color='k') +freq_colorbar_ax.tick_params(labelsize=6) +cbar.set_label("Freq (Hz)", size=8) + +for start_time, freq, ampl in zip(start_times, freqs, ampls): + stim_ax.add_patch(patches.Rectangle((start_time, 0), .05, ampl, color=cmap.to_rgba(freq))) + +stim_ax.set_xlim([tstart, tstop]) +stim_ax.set_ylim([0, 8]) + + +# Hg +bands = plotter.proc_dset.bands['band_mean'][:] +f_idx = np.logical_and(bands > 65, bands < 170) +rate = plotter.proc_dset.rate +istart, istop = int(tstart*rate), int(tstop*rate) +t = np.linspace(tstart, tstop, istop-istart) +bl_mu, bl_std = plotter.proc_bl_stats +for ch in channels: + mu = np.average(bl_mu[ch, f_idx]) + std = np.average(bl_std[ch, f_idx]) + ch_hg = plotter.proc_dset.data[istart:istop, ch, f_idx] + ch_hg = np.average(ch_hg, axis=-1) + ch_hg = (ch_hg - mu) / std + hg_ax.plot(t, ch_hg, linewidth=0.5, color=cmap.to_rgba(bfs[ch])) + +ymin, ymax = hg_ax.get_ylim() +for start_time in start_times: + hg_ax.plot([start_time, start_time], (ymin, ymax), + linestyle='--', color='grey', linewidth=0.5) + hg_ax.plot([start_time+.05, start_time+.05], (ymin, ymax), + linestyle='--', color='grey', linewidth=0.5) +hg_ax.set_ylim([ymin, ymax]) +hg_ax.set_xlim([tstart, tstop]) + + +# Spectrogram +class ToneSpectrogramLong(ToneSpectrogram): + def get_t_extent(self): + t = np.arange(tstart, tstop, 1.0/self.proc_dset.rate) + extent = [tstart, tstop, 0, 1] + return t, extent + + def draw_stim_bars(self): + pass + def draw_peak_bars(self): + pass + +plt.sca(spect_ax) +plotter = ToneSpectrogramLong(nwbfile, '.', tstart=tstart, tstop=tstop, stim_i='', auxfile=auxfile) +im = plotter.plot_one(channel, vmin=0, vmax=7) +cbar = fig.colorbar(im, cax=spect_colorbar_ax) +spect_colorbar_ax.tick_params(labelsize=6) +cbar.set_label("Z-score", size=8) + +ymin, ymax = spect_ax.get_ylim() +for start_time in start_times: + spect_ax.plot([start_time, start_time], (ymin, ymax), + linestyle='--', color='grey', linewidth=0.5) + spect_ax.plot([start_time+.05, start_time+.05], (ymin, ymax), + linestyle='--', color='grey', linewidth=0.5) +spect_ax.set_ylim([ymin, ymax]) +spect_ax.set_xlim([tstart, tstop]) + + + + +# plt.show() +plt.savefig("fig1_ch{}.pdf".format(channel)) diff --git a/analysis/simulation_analysis/tone_power_spectrum.py b/analysis/simulation_analysis/tone_power_spectrum.py new file mode 100644 index 0000000..3f79e2b --- /dev/null +++ b/analysis/simulation_analysis/tone_power_spectrum.py @@ -0,0 +1,169 @@ +"""Generate power spectrum from tone150 experimental block. Overlays +channels. Uses best frequency at each channel only. """ +import os + +import numpy as np +import h5py +import matplotlib.pyplot as plt + +from power_spectrum import PowerSpectrum +from utils import wavelet_cfs + +def bf_tone(plotter, auxfile, hg_min=65, hg_max=170): + """ + Compute and store the baseline stats and best frequencies on each channel + """ + nwb = plotter.nwb + bl_mu, bl_std = plotter.proc_bl_stats + + # Grab list of all frequencies presented + all_stim_freq = [int(x) for x in np.unique(nwb.trials['frq'][:])] + n_stim_freq = len(all_stim_freq) + + # Grab Z-scored high gamma data for each stim freq individually, + # compute max within each trial, and average across trials + proc_dset = plotter.proc_dset + _, n_ch, _ = proc_dset.data.shape + trials = plotter.nwb.trials + f_idx = np.logical_and(wavelet_cfs > hg_min, wavelet_cfs < hg_max) + + freq_maxes = np.empty(shape=(n_stim_freq, n_ch)) # trial-avg of max Hg amplitude per ch + for stim_freq_i, stim_freq in enumerate(all_stim_freq): + trial_idxs = np.logical_and(trials['sb'][:] == 's', trials['frq'][:] == str(stim_freq)) + times = zip(trials['start_time'][trial_idxs], trials['stop_time'][trial_idxs]) + time_idxs = [(int(t[0]*proc_dset.rate), int(t[1]*proc_dset.rate)) for t in times] + ch_maxes = np.empty(shape=(len(time_idxs), n_ch)) + for trial_i, (istart, istop) in enumerate(time_idxs): + trial_data = proc_dset.data[istart:istop, :, f_idx] + trial_data = (trial_data - bl_mu[:, f_idx]) / bl_std[:, f_idx] + trial_data = np.average(trial_data, axis=-1) + ch_maxes[trial_i, :] = np.max(trial_data, axis=0) + freq_maxes[stim_freq_i, :] = np.average(ch_maxes, axis=0) + bf_idxs = np.argmax(freq_maxes, axis=0) + bf = np.array([all_stim_freq[bf_i] for bf_i in bf_idxs]) + + with h5py.File(auxfile) as h5file: + h5file.create_dataset('/bl_mu', data=bl_mu) + h5file.create_dataset('/bl_std', data=bl_std) + h5file.create_dataset('/freq_maxes', data=freq_maxes) + h5file.create_dataset('/bf', data=bf) + + +class TonePowerSpectrum(PowerSpectrum): + def get_bfs(self): + with h5py.File(self.auxfile) as infile: + return infile['/bf'][:] + + + def get_spectrum(self, channel): + ch_data = self.proc_dset.data[:, channel, :] + n_timepts, n_bands = ch_data.shape + rate = self.proc_dset.rate + + hw = int(self.half_width * rate) + + # Rescale to baseline mean/stdev + bl_mean, bl_std = self.proc_bl_stats + bl_mean, bl_std = bl_mean[channel, :], bl_std[channel, :] + ch_data = (ch_data - bl_mean) / bl_std + + # Grab center frequencies from bands table + # def log_spaced_cfs(fmin, fmax, nbin=6): + # noct = np.ceil(np.log2(fmax/fmin)) + # return fmin * 2**(np.arange(noct*nbin)/nbin) + # band_means = log_spaced_cfs(2.6308, 1200.0) + # band_means = self.proc_dset.bands['band_mean'][:] + band_means = wavelet_cfs + hg_band_idx = np.logical_and(band_means > 65, band_means < 170) + + # Grab stim-on data for best freq + bf = self.get_bfs()[channel] + trials = self.nwb.trials + trial_idxs = np.logical_and(np.logical_and(trials['sb'][:] == 's', trials['frq'][:] == str(bf)), trials['amp'][:] == '7') + times = zip(trials['start_time'][trial_idxs], trials['stop_time'][trial_idxs]) + stim_periods = [(int(t[0]*self.proc_dset.rate), int(t[1]*self.proc_dset.rate)) for t in times] + + n_stim_timepts = stim_periods[0][1] - stim_periods[0][0] + stim_data = np.zeros(shape=(len(stim_periods), n_stim_timepts, n_bands)) + for i, (t1, t2) in enumerate(stim_periods): + stim_data[i, :, :] = ch_data[t1:t1+n_stim_timepts, :] + + # Calculate max of average high gamma response + # Average over stims, bands in hg range: time axis remains + hg_data = np.average(stim_data[:, :, hg_band_idx], axis=(0,2)) + max_i = np.argmax(hg_data) + self.max_i = max_i + print(channel, max_i) + max_i += self.time_shift_samp + if max_i - hw <= 0: + spectrum = np.zeros(shape=(54,)) + errors = np.zeros(shape=(54,)) + else: + # Average over stims, time: freq (bands) axis remainds + spectrum = np.average(stim_data[:, max_i-hw:max_i+hw, :], axis=(0,1)) + errors = np.std(stim_data[:, max_i-hw:max_i+hw, :], axis=(0,1)) + + return band_means, spectrum, errors + + def plot_all_and_avg(self, specfile=None): + if specfile and os.path.exists(specfile): + print("using saved spectra") + with h5py.File(specfile) as infile: + all_spectra = infile['power_spectra'][:] + else: + print("Computing spectra") + all_spectra = [self.get_spectrum(ch)[1] for ch in range(self.n_ch)] + + # if specfile and os.path.exists(specfile): + # os.remove(specfile) + if specfile and not os.path.exists(specfile): + with h5py.File(specfile) as outfile: + outfile.create_dataset('f', data=wavelet_cfs) + outfile.create_dataset('power_spectra', data=np.stack(all_spectra)) + + ch_spectra = [] + for ch in range(self.n_ch): + # f, spectrum, errors = self.get_spectrum(ch) + spectrum = all_spectra[ch] + if np.any(spectrum > 3.0): + ch_spectra.append(spectrum) + plt.plot(wavelet_cfs, spectrum, color='k', alpha=0.3, linewidth=0.3) + avg_spectrum = np.average(np.stack(ch_spectra), axis=0) + plt.plot(wavelet_cfs, avg_spectrum, color='red', alpha=1, linewidth=2) + print("plotted {} spectra".format(len(ch_spectra))) + + + +if __name__ == '__main__': + # TONE150: + # rat = 'R72' + # block = 'R72_B6' + # rat = 'R73' + # block = 'R73_B2' + # rat = 'R75' + # block = 'R75_B8' + # rat = 'R70' + # block = 'R70_B8' + + # rat = 'R32' + # block = 'R32_B7' + rat = 'R18' + block = 'R18_B12' + + nwbfile = '/data/{}/{}.nwb'.format(rat, block) + auxfile = '/data/{}/{}_aux.h5'.format(rat, block) + specfile = '/data/{}/{}_spectra.h5'.format(rat, block) + + plotter = TonePowerSpectrum(nwbfile, '.', + # proc_dset_name='Wvlt_4to1200_54band_CAR0', + auxfile=auxfile, half_width=0.005) + + if not os.path.exists(auxfile): + bf_tone(plotter, auxfile) + + plt.figure(figsize=(4, 4)) + plotter.prepare_axes() + plotter.plot_all_and_avg(specfile=specfile) + + plt.savefig("plots/tone_ps_{}.pdf".format(block)) + diff --git a/analysis/simulation_analysis/tone_spectrogram.py b/analysis/simulation_analysis/tone_spectrogram.py new file mode 100644 index 0000000..16c410d --- /dev/null +++ b/analysis/simulation_analysis/tone_spectrogram.py @@ -0,0 +1,133 @@ +import os +import numpy as np +import matplotlib.pyplot as plt + +from analysis import BasePlotter, PlotterArgParser + +class ToneSpectrogram(BasePlotter): + + def get_t_extent(self): + t = np.arange(-100, 150, 1/self.proc_dset.rate) + extent = [-100, 150, 0, 1] + return t, extent + + def draw_stim_bars(self): + ymax, ymin = plt.ylim() + plt.plot([0, 0], [ymin, ymax], linestyle='--', linewidth=0.5, color='k') + plt.plot([50, 50], [ymin, ymax], linestyle='--', linewidth=0.5, color='k') + plt.ylim([ymax, ymin]) + + def draw_peak_bars(self): + ymax, ymin = plt.ylim() + center_samp = 10 + center_time = center_samp / self.proc_dset.rate + t1, t2 = (center_time - .005) * 1000, (center_time + .005) * 1000 + plt.plot([t1, t1], [ymin, ymax], linewidth=0.3, color='red') + plt.plot([t2, t2], [ymin, ymax], linewidth=0.3, color='red') + plt.ylim([ymax, ymin]) + + def plot_one(self, channel, **plot_kwargs): + """ + Make one spectrogram and save it to file + """ + ch_data = self.proc_dset.data[:, channel, :] + rate = self.proc_dset.rate + + # Grab stim-on data, trial average if requested + bf = self.get_bfs()[channel] + trials = self.nwb.trials + trial_idxs = np.logical_and(np.logical_and(trials['sb'][:] == 's', trials['frq'][:] == str(bf)), trials['amp'][:] == '7') + times = zip(trials['start_time'][trial_idxs]-.1, trials['stop_time'][trial_idxs]+.1) + stim_periods = [(int(t[0]*self.proc_dset.rate), int(t[1]*self.proc_dset.rate)) for t in times] + if self.stim_i == 'avg': + print("doing stim avg") + n_stim_timepts = stim_periods[0][1] - stim_periods[0][0] + stim_data = np.average( + np.stack([ch_data[t[0]:t[0]+n_stim_timepts] for t in stim_periods]), + axis=0 + ) + elif self.tstart is not None and self.tstop is not None: + print("using tstart, tstop") + istart, istop = int(self.tstart*rate), int(self.tstop*rate) + stim_data = ch_data[istart:istop, :] + else: # self.stim_i is an integer index + print("doing stim {}".format(self.stim_i)) + tstart, tstop = stim_periods[self.stim_i] + stim_data = ch_data[tstart:tstop, :] + + # Rescale to baseline mean/stdev + bl_mean, bl_std = self.proc_bl_stats + bl_mean, bl_std = bl_mean[channel, :], bl_std[channel, :] + stim_data = (stim_data - bl_mean) / bl_std + # stim_data = stim_data / bl_mean + + # Get band info for axis labels + bands = self.proc_dset.bands['band_mean'][:] + + # Make plot + t, extent = self.get_t_extent() + ax = plt.gca() + im = ax.imshow(stim_data.T, origin='lower', cmap='Greys', aspect='auto', + extent=extent, **plot_kwargs) # , vmin=0, vmax=5) + + + plt.xlabel('Time (ms)') + plt.ylabel("Frequency (Hz)") + ticks, ticklabels = [], [] + for i in range(0, len(bands), 8): + ticks.append(float(i)/len(bands)) + ticklabels.append(int(bands[i])) + ax.set_yticks(ticks) + ax.set_yticklabels(ticklabels) + # plt.colorbar(label="Stim/baseline ratio") + # plt.colorbar().set_label(label="Z-score Amplitude", size=8) + plt.tight_layout() + + # Draw stim bars + self.draw_stim_bars() + + # Draw peak bars + self.draw_peak_bars() + + if not self.nosave: + fn = 'spectrogram_{}_ch{:02d}_{}.{}'.format( + self.device, channel, self.identifier, self.filetype + ) + full_fn = os.path.join(self.outdir, fn) + plt.savefig(full_fn) + + if self.show: + plt.show() + + return im + + +if __name__ == '__main__': + # TONE150 (not used) + # rat = 'R72' + # block = 'R72_B6' + # rat = 'R73' + # block = 'R73_B2' + # rat = 'R70' + # block = 'R70_B8' + # rat = 'R75' + # block = 'R75_B8' + my_preproc = ['R70', 'R67'] + + rat = 'R32' + block = 'R32_B7' + + nwbfile = '/data/{}/{}.nwb'.format(rat, block) + auxfile = '/data/{}/{}_aux.h5'.format(rat, block) + + # Not used - all tone blocks are preprocessed by me + # proc_dset_name = 'Hilb_54bands' if rat in my_preproc else 'Wvlt_4to1200_54band_CAR0' + + plotter = ToneSpectrogram(nwbfile, '.', auxfile=auxfile) + for channel in range(128): + plt.figure(figsize=(5, 4)) + plotter.plot_one(channel) + plt.savefig('plots/tone_spect_{}_ch{}.pdf'.format(block, channel)) + plt.close() + print("done channel {}".format(channel)) + diff --git a/analysis/simulation_analysis/utils.py b/analysis/simulation_analysis/utils.py new file mode 100644 index 0000000..945df77 --- /dev/null +++ b/analysis/simulation_analysis/utils.py @@ -0,0 +1,91 @@ +import glob +import os +import json + +import numpy as np +from scipy.signal import butter, lfilter + +def butter_bandpass(lowcut, highcut, fs, order=5): + nyq = 0.5 * fs + low = lowcut / nyq + high = highcut / nyq + b, a = butter(order, [low, high], btype='band') + return b, a + + +def bandpass(data, fs, lowcut=20, highcut=5000, order=5): + b, a = butter_bandpass(lowcut, highcut, fs, order=order) + y = lfilter(b, a, data) + return y + +def butter_highpass(lowcut, fs, order=5): + nyq = 0.5 * fs + low = lowcut / nyq + b, a = butter(order, low, btype='highpass') + return b, a + +def highpass(data, fs, lowcut, order=5): + b, a = butter_highpass(lowcut, fs, order=order) + y = lfilter(b, a, data) + return y + +def log_spaced_cfs(fmin, fmax, nbin=6): + """ + Center frequencies that are uniform in log space + """ + noct = np.ceil(np.log2(fmax/fmin)) + return fmin * 2**(np.arange(noct*nbin)/nbin) + +wavelet_cfs = log_spaced_cfs(2.6308, 1200.0) + +CCROOT = '/Users/vbaratham/src/cortical-column' + +def find_layer_ei_ecp_file(jobnum): + output_dir = os.path.join(CCROOT, 'runs', jobnum, '1', 'output') + ecp_files = glob.glob(os.path.join(output_dir, 'ecp*layer_ei*.nwb')) + if len(ecp_files) == 0: + raise ValueError('No layer_ei ECP file found') + elif len(ecp_files) == 1: + return ecp_files[0] + else: + log.info( + 'Found multiple layer_ei ECP files: \n{}\n'.format('\n'.join(ecp_files)) + + '\nUsing {}\n'.format(ecp_files[-1]) + ) + return ecp_files[-1] + +def find_slice_ecp_file(jobnum, thickness=100): + output_dir = os.path.join(CCROOT, 'runs', jobnum, '1', 'output') + ecp_files = glob.glob(os.path.join(output_dir, 'ecp*{}um*.nwb'.format(thickness))) + if len(ecp_files) == 0: + raise ValueError('No 100um slice ECP file found') + elif len(ecp_files) == 1: + return ecp_files[0] + else: + log.info( + 'Found multiple 100um slice ECP files: \n{}\n'.format('\n'.join(ecp_files)) + + '\nUsing {}\n'.format(ecp_files[-1]) + ) + return ecp_files[-1] + +def get_layer_slice_counts(jobnum, thickness=100): + fn = os.path.join(CCROOT, 'runs', jobnum, '1', 'output', 'layer_slice_counts.json') + with open(fn, 'r') as infile: + orig = json.load(infile) + counts = { + int(layer): {int(slice_i): count for slice_i, count in slice_counts.items()} + for layer, slice_counts in orig.items() + } + if thickness == 100: + return counts + elif thickness == 200: + def convert(layercounts): + return {slice_i: layercounts[slice_i*2] + layercounts.get(slice_i*2 + 1, 0) + for slice_i in range(11)} + for layer in counts.keys(): + counts[layer] = convert(counts[layer]) + return counts + else: + raise ValueError("Can only do 100 or 200um slices") + +numerals = {1: 'I', 2: 'II', 3: 'III', 4: 'IV', 5: 'V', 6: 'VI'} diff --git a/bmtk-vb/CONTRIBUTING.md b/bmtk-vb/CONTRIBUTING.md new file mode 100644 index 0000000..e55867f --- /dev/null +++ b/bmtk-vb/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Allen Institute Contribution Agreement + +This document describes the terms under which you may make “Contributions” — +which may include without limitation, software additions, revisions, bug fixes, configuration changes, +documentation, or any other materials — to any of the projects owned or managed by the Allen Institute. +If you have questions about these terms, please contact us at terms@alleninstitute.org. + +You certify that: + +• Your Contributions are either: + +1. Created in whole or in part by you and you have the right to submit them under the designated license +(described below); or +2. Based upon previous work that, to the best of your knowledge, is covered under an appropriate +open source license and you have the right under that license to submit that work with modifications, +whether created in whole or in part by you, under the designated license; or + +3. Provided directly to you by some other person who certified (1) or (2) and you have not modified them. + +• You are granting your Contributions to the Allen Institute under the terms of the [2-Clause BSD license](https://opensource.org/licenses/BSD-2-Clause) +(the “designated license”). + +• You understand and agree that the Allen Institute projects and your Contributions are public and that +a record of the Contributions (including all metadata and personal information you submit with them) is +maintained indefinitely and may be redistributed consistent with the Allen Institute’s mission and the +2-Clause BSD license. diff --git a/bmtk-vb/LICENSE.txt b/bmtk-vb/LICENSE.txt new file mode 100644 index 0000000..280f59d --- /dev/null +++ b/bmtk-vb/LICENSE.txt @@ -0,0 +1,21 @@ +Copyright 2017. Allen Institute. 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. + +3. 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/bmtk-vb/README.md b/bmtk-vb/README.md new file mode 100644 index 0000000..29d5b8b --- /dev/null +++ b/bmtk-vb/README.md @@ -0,0 +1,32 @@ +# The Brain Modeling Toolkit + +A software development package for building, simulating and analyzing large-scale networks of different levels of resolution. + +## Level of Support +We are releasing this code to the public as a tool we expect others to use. Questions concerning bugs and related issues are welcomed. We expect to address them promptly, pull requests will vetted by our staff before inclusion. + + +## Quickstart +bmtk requires Python 2.7 plus [additional python dependicies](https://alleninstitute.github.io/bmtk/index.html#base-installation). To install with +base requirements from a command-line: + +```bash + $ git clone https://github.com/AllenInstitute/bmtk.git + $ cd bmtk + $ python setup.py install +``` + +There are examples of building models and running simulations located in docs/examples/. Some of the simulation engines may require additonal requirements to run. + + +## Documentation + +[User Guide](https://alleninstitute.github.io/bmtk/) +* [Building network models](https://alleninstitute.github.io/bmtk/builder.html) +* [Running biophysical simulations](https://alleninstitute.github.io/bmtk/bionet.html) +* [Running point-neuron simulations](https://alleninstitute.github.io/bmtk/pointnet.html) +* [Running population-level simulations](https://alleninstitute.github.io/bmtk/popnet.html) + + + +Copyright 2017 Allen Institute diff --git a/bmtk-vb/bmtk.egg-info/PKG-INFO b/bmtk-vb/bmtk.egg-info/PKG-INFO new file mode 100644 index 0000000..ad0b5ba --- /dev/null +++ b/bmtk-vb/bmtk.egg-info/PKG-INFO @@ -0,0 +1,57 @@ +Metadata-Version: 2.1 +Name: bmtk +Version: 0.0.6 +Summary: Brain Modeling Toolkit +Home-page: https://github.com/AllenInstitute/bmtk +Author: Kael Dai +Author-email: kaeld@alleninstitute.org +License: UNKNOWN +Description: # The Brain Modeling Toolkit + + A software development package for building, simulating and analyzing large-scale networks of different levels of resolution. + + ## Level of Support + We are releasing this code to the public as a tool we expect others to use. Questions concerning bugs and related issues are welcomed. We expect to address them promptly, pull requests will vetted by our staff before inclusion. + + + ## Quickstart + bmtk requires Python 2.7 plus [additional python dependicies](https://alleninstitute.github.io/bmtk/index.html#base-installation). To install with + base requirements from a command-line: + + ```bash + $ git clone https://github.com/AllenInstitute/bmtk.git + $ cd bmtk + $ python setup.py install + ``` + + There are examples of building models and running simulations located in docs/examples/. Some of the simulation engines may require additonal requirements to run. + + + ## Documentation + + [User Guide](https://alleninstitute.github.io/bmtk/) + * [Building network models](https://alleninstitute.github.io/bmtk/builder.html) + * [Running biophysical simulations](https://alleninstitute.github.io/bmtk/bionet.html) + * [Running point-neuron simulations](https://alleninstitute.github.io/bmtk/pointnet.html) + * [Running population-level simulations](https://alleninstitute.github.io/bmtk/popnet.html) + + + + Copyright 2017 Allen Institute + +Keywords: neuroscience,scientific,modeling,simulation +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: BSD License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Topic :: Scientific/Engineering :: Bio-Informatics +Description-Content-Type: text/markdown +Provides-Extra: mintnet +Provides-Extra: popnet +Provides-Extra: pointnet +Provides-Extra: bionet diff --git a/bmtk-vb/bmtk.egg-info/SOURCES.txt b/bmtk-vb/bmtk.egg-info/SOURCES.txt new file mode 100644 index 0000000..9b37b58 --- /dev/null +++ b/bmtk-vb/bmtk.egg-info/SOURCES.txt @@ -0,0 +1,215 @@ +README.md +setup.py +bmtk/__init__.py +bmtk.egg-info/PKG-INFO +bmtk.egg-info/SOURCES.txt +bmtk.egg-info/dependency_links.txt +bmtk.egg-info/requires.txt +bmtk.egg-info/top_level.txt +bmtk/analyzer/__init__.py +bmtk/analyzer/cell_vars.py +bmtk/analyzer/firing_rates.py +bmtk/analyzer/io_tools.py +bmtk/analyzer/spike_trains.py +bmtk/analyzer/spikes_analyzer.py +bmtk/analyzer/spikes_loader.py +bmtk/analyzer/utils.py +bmtk/analyzer/visualization/__init__.py +bmtk/analyzer/visualization/rasters.py +bmtk/analyzer/visualization/spikes.py +bmtk/analyzer/visualization/widgets.py +bmtk/builder/__init__.py +bmtk/builder/connection_map.py +bmtk/builder/connector.py +bmtk/builder/edge.py +bmtk/builder/functor_cache.py +bmtk/builder/id_generator.py +bmtk/builder/iterator.py +bmtk/builder/network.py +bmtk/builder/node.py +bmtk/builder/node_pool.py +bmtk/builder/node_set.py +bmtk/builder/aux/__init__.py +bmtk/builder/aux/edge_connectors.py +bmtk/builder/aux/node_params.py +bmtk/builder/bionet/__init__.py +bmtk/builder/bionet/swc_reader.py +bmtk/builder/formats/__init__.py +bmtk/builder/formats/hdf5_format.py +bmtk/builder/formats/iformats.py +bmtk/builder/io/__init__.py +bmtk/builder/networks/__init__.py +bmtk/builder/networks/dm_network.py +bmtk/builder/networks/input_network.py +bmtk/builder/networks/mpi_network.py +bmtk/builder/networks/nxnetwork.py +bmtk/builder/networks/sparse_network.py +bmtk/simulator/__init__.py +bmtk/simulator/bionet/__init__.py +bmtk/simulator/bionet/biocell.py +bmtk/simulator/bionet/bionetwork.py +bmtk/simulator/bionet/biosimulator.py +bmtk/simulator/bionet/cell.py +bmtk/simulator/bionet/config.py +bmtk/simulator/bionet/iclamp.py +bmtk/simulator/bionet/io_tools.py +bmtk/simulator/bionet/morphology.py +bmtk/simulator/bionet/nml_reader.py +bmtk/simulator/bionet/nrn.py +bmtk/simulator/bionet/pointprocesscell.py +bmtk/simulator/bionet/pointsomacell.py +bmtk/simulator/bionet/pyfunction_cache.py +bmtk/simulator/bionet/sonata_adaptors.py +bmtk/simulator/bionet/utils.py +bmtk/simulator/bionet/virtualcell.py +bmtk/simulator/bionet/default_setters/__init__.py +bmtk/simulator/bionet/default_setters/cell_models.py +bmtk/simulator/bionet/default_setters/synapse_models.py +bmtk/simulator/bionet/default_setters/synaptic_weights.py +bmtk/simulator/bionet/modules/__init__.py +bmtk/simulator/bionet/modules/ecp.py +bmtk/simulator/bionet/modules/record_cellvars.py +bmtk/simulator/bionet/modules/record_spikes.py +bmtk/simulator/bionet/modules/save_synapses.py +bmtk/simulator/bionet/modules/sim_module.py +bmtk/simulator/bionet/modules/xstim.py +bmtk/simulator/bionet/modules/xstim_waveforms.py +bmtk/simulator/core/__init__.py +bmtk/simulator/core/config.py +bmtk/simulator/core/edge_population.py +bmtk/simulator/core/graph.py +bmtk/simulator/core/io_tools.py +bmtk/simulator/core/network_reader.py +bmtk/simulator/core/node_population.py +bmtk/simulator/core/node_sets.py +bmtk/simulator/core/simulator.py +bmtk/simulator/core/simulator_network.py +bmtk/simulator/core/sonata_reader/__init__.py +bmtk/simulator/core/sonata_reader/edge_adaptor.py +bmtk/simulator/core/sonata_reader/network_reader.py +bmtk/simulator/core/sonata_reader/node_adaptor.py +bmtk/simulator/filternet/__init__.py +bmtk/simulator/filternet/cell.py +bmtk/simulator/filternet/cell_models.py +bmtk/simulator/filternet/config.py +bmtk/simulator/filternet/filternetwork.py +bmtk/simulator/filternet/filters.py +bmtk/simulator/filternet/filtersimulator.py +bmtk/simulator/filternet/io_tools.py +bmtk/simulator/filternet/pyfunction_cache.py +bmtk/simulator/filternet/transfer_functions.py +bmtk/simulator/filternet/utils.py +bmtk/simulator/filternet/default_setters/__init__.py +bmtk/simulator/filternet/default_setters/cell_loaders.py +bmtk/simulator/filternet/lgnmodel/__init__.py +bmtk/simulator/filternet/lgnmodel/cellmodel.py +bmtk/simulator/filternet/lgnmodel/cursor.py +bmtk/simulator/filternet/lgnmodel/fitfuns.py +bmtk/simulator/filternet/lgnmodel/kernel.py +bmtk/simulator/filternet/lgnmodel/lattice_unit_constructor.py +bmtk/simulator/filternet/lgnmodel/lgnmodel1.py +bmtk/simulator/filternet/lgnmodel/linearfilter.py +bmtk/simulator/filternet/lgnmodel/lnunit.py +bmtk/simulator/filternet/lgnmodel/make_cell_list.py +bmtk/simulator/filternet/lgnmodel/movie.py +bmtk/simulator/filternet/lgnmodel/poissongeneration.py +bmtk/simulator/filternet/lgnmodel/singleunitcell.py +bmtk/simulator/filternet/lgnmodel/spatialfilter.py +bmtk/simulator/filternet/lgnmodel/temporalfilter.py +bmtk/simulator/filternet/lgnmodel/transferfunction.py +bmtk/simulator/filternet/lgnmodel/util_fns.py +bmtk/simulator/filternet/lgnmodel/utilities.py +bmtk/simulator/filternet/modules/__init__.py +bmtk/simulator/filternet/modules/base.py +bmtk/simulator/filternet/modules/create_spikes.py +bmtk/simulator/filternet/modules/record_rates.py +bmtk/simulator/mintnet/Image_Library.py +bmtk/simulator/mintnet/Image_Library_Supervised.py +bmtk/simulator/mintnet/__init__.py +bmtk/simulator/mintnet/analysis/LocallySparseNoise.py +bmtk/simulator/mintnet/analysis/StaticGratings.py +bmtk/simulator/mintnet/analysis/__init__.py +bmtk/simulator/mintnet/hmax/C_Layer.py +bmtk/simulator/mintnet/hmax/Readout_Layer.py +bmtk/simulator/mintnet/hmax/S1_Layer.py +bmtk/simulator/mintnet/hmax/S_Layer.py +bmtk/simulator/mintnet/hmax/Sb_Layer.py +bmtk/simulator/mintnet/hmax/ViewTunedLayer.py +bmtk/simulator/mintnet/hmax/__init__.py +bmtk/simulator/mintnet/hmax/hmax.py +bmtk/simulator/pointnet/__init__.py +bmtk/simulator/pointnet/config.py +bmtk/simulator/pointnet/io_tools.py +bmtk/simulator/pointnet/pointnetwork.py +bmtk/simulator/pointnet/pointsimulator.py +bmtk/simulator/pointnet/property_map.py +bmtk/simulator/pointnet/pyfunction_cache.py +bmtk/simulator/pointnet/sonata_adaptors.py +bmtk/simulator/pointnet/utils.py +bmtk/simulator/pointnet/default_setters/__init__.py +bmtk/simulator/pointnet/default_setters/synapse_models.py +bmtk/simulator/pointnet/default_setters/synaptic_weights.py +bmtk/simulator/pointnet/modules/__init__.py +bmtk/simulator/pointnet/modules/multimeter_reporter.py +bmtk/simulator/pointnet/modules/record_spikes.py +bmtk/simulator/popnet/__init__.py +bmtk/simulator/popnet/config.py +bmtk/simulator/popnet/popedge.py +bmtk/simulator/popnet/popnetwork.py +bmtk/simulator/popnet/popnetwork_OLD.py +bmtk/simulator/popnet/popnode.py +bmtk/simulator/popnet/popsimulator.py +bmtk/simulator/popnet/sonata_adaptors.py +bmtk/simulator/popnet/utils.py +bmtk/simulator/popnet/property_schemas/__init__.py +bmtk/simulator/popnet/property_schemas/base_schema.py +bmtk/simulator/popnet/property_schemas/property_schema_ver0.py +bmtk/simulator/popnet/property_schemas/property_schema_ver1.py +bmtk/simulator/utils/__init__.py +bmtk/simulator/utils/config.py +bmtk/simulator/utils/graph.py +bmtk/simulator/utils/io.py +bmtk/simulator/utils/load_spikes.py +bmtk/simulator/utils/nwb.py +bmtk/simulator/utils/property_maps.py +bmtk/simulator/utils/sim_validator.py +bmtk/simulator/utils/simulation_inputs.py +bmtk/simulator/utils/simulation_reports.py +bmtk/simulator/utils/stimulus/LocallySparseNoise.py +bmtk/simulator/utils/stimulus/NaturalScenes.py +bmtk/simulator/utils/stimulus/StaticGratings.py +bmtk/simulator/utils/stimulus/__init__.py +bmtk/simulator/utils/tools/__init__.py +bmtk/simulator/utils/tools/process_spikes.py +bmtk/simulator/utils/tools/spatial.py +bmtk/utils/__init__.py +bmtk/utils/property_schema.py +bmtk/utils/sim_setup.py +bmtk/utils/cell_vars/__init__.py +bmtk/utils/cell_vars/var_reader.py +bmtk/utils/converters/__init__.py +bmtk/utils/converters/hoc_converter.py +bmtk/utils/converters/sonata/__init__.py +bmtk/utils/converters/sonata/edge_converters.py +bmtk/utils/converters/sonata/node_converters.py +bmtk/utils/io/__init__.py +bmtk/utils/io/cell_vars.py +bmtk/utils/io/firing_rates.py +bmtk/utils/io/spike_trains.py +bmtk/utils/io/tabular_network.py +bmtk/utils/io/tabular_network_v0.py +bmtk/utils/io/tabular_network_v1.py +bmtk/utils/sonata/__init__.py +bmtk/utils/sonata/column_property.py +bmtk/utils/sonata/config.py +bmtk/utils/sonata/edge.py +bmtk/utils/sonata/file.py +bmtk/utils/sonata/file_root.py +bmtk/utils/sonata/group.py +bmtk/utils/sonata/node.py +bmtk/utils/sonata/population.py +bmtk/utils/sonata/types_table.py +bmtk/utils/sonata/utils.py +bmtk/utils/spike_trains/__init__.py +bmtk/utils/spike_trains/spikes_csv.py +bmtk/utils/spike_trains/spikes_file.py \ No newline at end of file diff --git a/bmtk-vb/bmtk.egg-info/dependency_links.txt b/bmtk-vb/bmtk.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bmtk-vb/bmtk.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/bmtk-vb/bmtk.egg-info/requires.txt b/bmtk-vb/bmtk.egg-info/requires.txt new file mode 100644 index 0000000..582dcee --- /dev/null +++ b/bmtk-vb/bmtk.egg-info/requires.txt @@ -0,0 +1,18 @@ +jsonschema +pandas +numpy +six +h5py +matplotlib + +[bionet] +NEURON + +[mintnet] +tensorflow + +[pointnet] +NEST + +[popnet] +DiPDE diff --git a/bmtk-vb/bmtk.egg-info/top_level.txt b/bmtk-vb/bmtk.egg-info/top_level.txt new file mode 100644 index 0000000..8ea5840 --- /dev/null +++ b/bmtk-vb/bmtk.egg-info/top_level.txt @@ -0,0 +1 @@ +bmtk diff --git a/bmtk-vb/bmtk/__init__.py b/bmtk-vb/bmtk/__init__.py new file mode 100644 index 0000000..f4f772b --- /dev/null +++ b/bmtk-vb/bmtk/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +__version__ = '0.0.6' diff --git a/bmtk-vb/bmtk/__init__.pyc b/bmtk-vb/bmtk/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98acadec267e7915198fc1d8aab227b139cc8d0b GIT binary patch literal 161 zcmZSn%*)m7+8dk900oRd+5w1*xqw6p149&$WMl}|U;>G;0to{>13fc84UoDLZXgjK zUzS=_oSB~&AFl!AG9ZXzEg)Avv=}I@UzU_ulvt9Hn5$o0l&qhWTapbS;^Q;(GE3s) a^$IFWIDpD+a`RJ4b5iZZf$U-+W&i-}9U)Ty literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/__pycache__/__init__.cpython-35.pyc b/bmtk-vb/bmtk/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf5af1874bb00c34e6a08688110de703e40ae04f GIT binary patch literal 148 zcmWgV<>j*Mb&O$WV_iTv=}I%UzU_ulvt9Hn5$o0l&qhWTapbS;^Q;(GE3s) X^$IF)aoFVMre~gbu)&Yje+4Y0}@~avK@f9m;*?pFhnt=Fa|SdGF7n}=o#o)_-QiU;*O6m zOD!tS%+HIDU&&C!1XKqmerf5476T>p%aRg{5=$}?bM=dhlJ%2vOR^zEe0*kJW=VX! XUP0w84x8Nkl+v73J8_U*K+FID;Tt4J literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3311eab1241aa2abab54bfbbaac2efb1931237d GIT binary patch literal 150 zcmZ?b<>g`k0?S^<7?B^imi`o%@b`boJZ*$^T=J~J<~ aBtBlRpz;=nO>TZlX-=x0ILIy_W&i+Qcq3H+ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/analyzer/__init__.py b/bmtk-vb/bmtk/analyzer/__init__.py new file mode 100644 index 0000000..7b04c40 --- /dev/null +++ b/bmtk-vb/bmtk/analyzer/__init__.py @@ -0,0 +1,189 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +from six import string_types +import h5py +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +import bmtk.simulator.utils.config as cfg + + +def _get_config(config): + if isinstance(config, string_types): + return cfg.from_json(config) + elif isinstance(config, dict): + return config + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(config, type(config))) + +def plot_potential(cell_vars_h5=None, config_file=None, gids=None, show_plot=True, save=False): + if (cell_vars_h5 or config_file) is None: + raise Exception('Please specify a cell_vars hdf5 file or a simulation config.') + + if cell_vars_h5 is not None: + plot_potential_hdf5(cell_vars_h5, gids=gids, show_plot=show_plot, + save_as='sim_potential.jpg' if save else None) + + else: + # load the json file or object + if isinstance(config_file, string_types): + config = cfg.from_json(config_file) + elif isinstance(config_file, dict): + config = config_file + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(config_file, type(config_file))) + + gid_list = gids or config['node_id_selections']['save_cell_vars'] + for gid in gid_list: + save_as = '{}_v.jpg'.format(gid) if save else None + title = 'cell gid {}'.format(gid) + var_h5 = os.path.join(config['output']['cell_vars_dir'], '{}.h5'.format(gid)) + plot_potential_hdf5(var_h5, title, show_plot, save_as) + + +def plot_potential_hdf5(cell_vars_h5, gids, title='membrane potential', show_plot=True, save_as=None): + data_h5 = h5py.File(cell_vars_h5, 'r') + membrane_trace = data_h5['data'] + + time_ds = data_h5['/mapping/time'] + tstart = time_ds[0] + tstop = time_ds[1] + x_axis = np.linspace(tstart, tstop, len(membrane_trace), endpoint=True) + + gids_ds = data_h5['/mapping/gids'] + index_ds = data_h5['/mapping/index_pointer'] + index_lookup = {gids_ds[i]: (index_ds[i], index_ds[i+1]) for i in range(len(gids_ds))} + gids = gids_ds.keys() if gids_ds is None else gids + for gid in gids: + var_indx = index_lookup[gid][0] + plt.plot(x_axis, membrane_trace[:, var_indx], label=gid) + + plt.xlabel('time (ms)') + plt.ylabel('membrane (mV)') + plt.title(title) + plt.legend(markerscale=2, scatterpoints=1) + + if save_as is not None: + plt.savefig(save_as) + + if show_plot: + plt.show() + + +def plot_calcium(cell_vars_h5=None, config_file=None, gids=None, show_plot=True, save=False): + if (cell_vars_h5 or config_file) is None: + raise Exception('Please specify a cell_vars hdf5 file or a simulation config.') + + if cell_vars_h5 is not None: + plot_calcium_hdf5(cell_vars_h5, gids, show_plot=show_plot, save_as='sim_ca.jpg' if save else None) + + else: + # load the json file or object + if isinstance(config_file, string_types): + config = cfg.from_json(config_file) + elif isinstance(config_file, dict): + config = config_file + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(config_file, type(config_file))) + + gid_list = gids or config['node_id_selections']['save_cell_vars'] + for gid in gid_list: + save_as = '{}_v.jpg'.format(gid) if save else None + title = 'cell gid {}'.format(gid) + var_h5 = os.path.join(config['output']['cell_vars_dir'], '{}.h5'.format(gid)) + plot_calcium_hdf5(var_h5, title, show_plot, save_as) + + +def plot_calcium_hdf5(cell_vars_h5, gids, title='Ca2+ influx', show_plot=True, save_as=None): + data_h5 = h5py.File(cell_vars_h5, 'r') + cai_trace = data_h5['cai/data'] + + time_ds = data_h5['/mapping/time'] + tstart = time_ds[0] + tstop = time_ds[1] + x_axis = np.linspace(tstart, tstop, len(cai_trace), endpoint=True) + + gids_ds = data_h5['/mapping/gids'] + index_ds = data_h5['/mapping/index_pointer'] + index_lookup = {gids_ds[i]: (index_ds[i], index_ds[i+1]) for i in range(len(gids_ds))} + gids = gids_ds.keys() if gids_ds is None else gids + for gid in gids: + var_indx = index_lookup[gid][0] + plt.plot(x_axis, cai_trace[:, var_indx], label=gid) + + #plt.plot(x_axis, cai_trace) + plt.xlabel('time (ms)') + plt.ylabel('calcium [Ca2+]') + plt.title(title) + plt.legend(markerscale=2, scatterpoints=1) + + if save_as is not None: + plt.savefig(save_as) + + if show_plot: + plt.show() + + +def spikes_table(config_file, spikes_file=None): + config = _get_config(config_file) + spikes_file = config['output']['spikes_file'] + spikes_h5 = h5py.File(spikes_file, 'r') + gids = np.array(spikes_h5['/spikes/gids'], dtype=np.uint) + times = np.array(spikes_h5['/spikes/timestamps'], dtype=np.float) + return pd.DataFrame(data={'gid': gids, 'spike time (ms)': times}) + #return pd.read_csv(spikes_ascii, names=['time (ms)', 'cell gid'], sep=' ') + + +def nodes_table(nodes_file, population): + # TODO: Integrate into sonata api + nodes_h5 = h5py.File(nodes_file, 'r') + nodes_pop = nodes_h5['/nodes'][population] + root_df = pd.DataFrame(data={'node_id': nodes_pop['node_id'], 'node_type_id': nodes_pop['node_type_id'], + 'node_group_id': nodes_pop['node_group_id'], + 'node_group_index': nodes_pop['node_group_index']}) #, + #index=[nodes_pop['node_group_id'], nodes_pop['node_group_index']]) + root_df = root_df.set_index(['node_group_id', 'node_group_index']) + + node_grps = np.unique(nodes_pop['node_group_id']) + for grp_id in node_grps: + sub_group = nodes_pop[str(grp_id)] + grp_df = pd.DataFrame() + for hf_key in sub_group: + hf_obj = sub_group[hf_key] + if isinstance(hf_obj, h5py.Dataset): + grp_df[hf_key] = hf_obj + + subgrp_len = len(grp_df) + if subgrp_len > 0: + grp_df['node_group_id'] = [grp_id]*subgrp_len + grp_df['node_group_index'] = range(subgrp_len) + grp_df = grp_df.set_index(['node_group_id', 'node_group_index']) + root_df = root_df.join(other=grp_df, how='left') + + return root_df.reset_index(drop=True) + + +def node_types_table(node_types_file, population): + return pd.read_csv(node_types_file, sep=' ') \ No newline at end of file diff --git a/bmtk-vb/bmtk/analyzer/__init__.pyc b/bmtk-vb/bmtk/analyzer/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf540e667f6b28460bbaf3f7394945dcd5ca1575 GIT binary patch literal 6691 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/analyzer/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/analyzer/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5e45c9309f2477a0c51de0f8672b0d2fbd979ed GIT binary patch literal 5176 zcmeHL&2!{N6<13djYjj~kM(|dznClu*&&M^k~k1BOOo9zRa6*4vanS{7Db-c*s?Vv zZL7zzXEl3>191+nN)<&F2P)V{{s;aLs;`{(#Eqi%#P7AdV|&R$a^aMj((9+!-Rjos z-}};=^}1)^_uAeY@BibnVf>wL9{);UUdNSv4#Ex2VxvQ?8MD6GFd=u4A7B2aEKg)xQ{`iusRTUi@ViifnOnB@lsJ z3;K(h=q|4GHO%+r6{6m`gM-|Et# z(~lM^1HX8Is6@zT7(oq1#IY`vR1H;ubL z9hb4nA9)$$%Lk0s8oS&r?#2~aS@-sl%WMKjgV z&zbCR0O0| z9*t9B6Gwm$WIDnnQUOtBk|{d{^pw4xM1xjU)sB`~TT<0RH9o0LG-%osO|xn1f^er? zY}1dUOezbUm?l|CMlueS1GW2WOB(70sZnp^NH3njb-@qp6SM)uaS2c|34UlTv;ZBq(Yj~> zO#seZ2cYtbv~y>Nafg@p?6HT@?!57-Q2_B%_KEQc>zUfWxw0hfU3ioob`x}5-6bP( z#e-dA-#IV_FqkphdL>8i-D~4oUd?NOF|Apb<#iVjc3@t{Z_T{k!_M*=ukBfY^X?sZ zs{IPDA23Fo#~X)SH8%@xy#{Yu;?sNZ-MOtz_n#P9&u!|@tX#%BUX-a;Oi9+=hR0!vRkL1bJ)};eT{yGASDHa0^hwpioTQGPy%i z{V818w?HEJnY)+AY?tM1-XO}2v&Ov9Gj|P={NB80!)BRy7K1f9TBb4=UQ{dsQ>I0I z*qvJ}$Fm~JwO9?f@W=Z-m>KveFn)K>H78ORuh+ZQTk)YePA4xC-4D&IQg0CI*MoYKVDK zkG4D-r_HZ0vN9be%&XIQymem+TD(keMY?k&VwI3E$4n-va<=?nE6P-@P-&bb8^csw zqSalpREW0@4?asK1UN5paSZIwk%_0ajAoWa#>3I|D z{5lYQ=|JRBAnFD>0+~l={uLsQn887bS&*m16+xCN=4imlP%@*<2#D`S!~T~IB;pJ` z?JSXVAgyYF+w&xtgbi_lZYgSri$n-|8q3-Q#5d@cKyqa2s&-;C=*!3o9wYGl7srXW zFyc}5M)1-L&1lezhg%Z}ejOm59w5XOsBPqSgJ|(-VM63eTMY5T!+591_F}m~=cKG!8AG`D+5wU2&~g^E+t70`XH~ zk@y#he3!^eL|!KH3XvruuM+tlk?#|EjfnQ+RJn*B5?KcMFMeM92>qQ3j-_z#;yO+G zF_G7ayg`I~b74+5h$D5J(2rKlO-Mh)^}n3B^@J0rd0i8PeiY*=o1}HJ4hWT6NPf)R zvoP3aozxjDqznX+lMk#1K=DjE9sQTMw(UrlgZsZDg`TT z?W9ySOQVf2E0R!+(%NDnS|sFAJm*wjjbDbQgeH)3Rvx-C#=TJgftjAF`xAj?GLaMrp6xl+lj3gM(EmKi_ zfHZmQYFtGu#diw|;VmF3kr zBX@ae*^rR$F!0VYB*%8+=Jt9Ga(p@BTGAa0L>mp`%3@<*fC5 zB%_MjWM#eMVF-;zvic-UtHmN3rGLgY3!h;UMyHv>rp<=g&?z4hy#l*B5p1Bp!Ddat zF=v8d@*NYOLAC+Z=(3EPdnWAMYCmF4DA>xv6!uD5RjvpF?sv0IoWm4truYn0_<-MF z#FINN4w_Jq`Fh)b#ULF83??G*tx~()zJ04@iDz(kyM1(jtGH+F);XQWU?b61Q8?3C zD<=&GJjjZT&ERtm#gsT&!N(Zl(63wV>{V$n?5EoW3NFx>oQ1;=4T)G7$|%kjijo=! zRD6I)gevKdA~PaI6+Wp+9A(~u5*l%n2>AxBT>VPNbTEm4%3ba!d>Dt 1: + raise Exception('Found more than one membrane_report, please specify report_name') + + else: + report_name = cell_var_reports[0][0] + report = cell_var_reports[0][1] + report_fname = report['file_name'] if 'file_name' in report else '{}.h5'.format(report_name) + return report_name, os.path.join(cfg.output_dir, report_fname) + + +def plot_report(config_file=None, report_file=None, report_name=None, variables=None, gids=None): + if report_file is None: + report_name, report_file = _get_cell_report(config_file, report_name) + + var_report = CellVarsFile(report_file) + variables = listify(variables) if variables is not None else var_report.variables + gids = listify(gids) if gids is not None else var_report.gids + time_steps = var_report.time_trace + + def __units_str(var): + units = var_report.units(var) + if units == CellVarsFile.UNITS_UNKNOWN: + units = missing_units.get(var, '') + return '({})'.format(units) if units else '' + + n_plots = len(variables) + if n_plots > 1: + # If more than one variale to plot do so in different subplots + f, axarr = plt.subplots(n_plots, 1) + for i, var in enumerate(variables): + for gid in gids: + axarr[i].plot(time_steps, var_report.data(gid=gid, var_name=var), label='gid {}'.format(gid)) + + axarr[i].legend() + axarr[i].set_ylabel('{} {}'.format(var, __units_str(var))) + if i < n_plots - 1: + axarr[i].set_xticklabels([]) + + axarr[i].set_xlabel('time (ms)') + + elif n_plots == 1: + # For plotting a single variable + plt.figure() + for gid in gids: + plt.plot(time_steps, var_report.data(gid=0, var_name=variables[0]), label='gid {}'.format(gid)) + plt.ylabel('{} {}'.format(variables[0], __units_str(variables[0]))) + plt.xlabel('time (ms)') + + else: + return + + plt.show() + + #for gid in gids: + # plt.plot(times, var_report.data(gid=0, var_name='v'), label='gid {}'.format(gid)) + + + ''' + + + + plt.ylabel('{} {}'.format('v', units_str)) + plt.xlabel('time (ms)') + plt.legend() + plt.show() + ''' + + + diff --git a/bmtk-vb/bmtk/analyzer/firing_rates.py b/bmtk-vb/bmtk/analyzer/firing_rates.py new file mode 100644 index 0000000..bca785c --- /dev/null +++ b/bmtk-vb/bmtk/analyzer/firing_rates.py @@ -0,0 +1,55 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import pandas as pd +import numpy as np + +def convert_rates(rates_file): + rates_df = pd.read_csv(rates_file, sep=' ', names=['gid', 'time', 'rate']) + rates_sorted_df = rates_df.sort_values(['gid', 'time']) + rates_dict = {} + for gid, rates in rates_sorted_df.groupby('gid'): + start = rates['time'].iloc[0] + #start = rates['rate'][0] + end = rates['time'].iloc[-1] + dt = float(end - start)/len(rates) + rates_dict[gid] = {'start': start, 'end': end, 'dt': dt, 'rates': np.array(rates['rate'])} + + return rates_dict + + +def firing_rates_equal(rates_file1, rates_file2, err=0.0001): + trial_1 = convert_rates(rates_file1) + trial_2 = convert_rates(rates_file2) + if set(trial_1.keys()) != set(trial_2.keys()): + return False + + for gid, rates_data1 in trial_1.items(): + rates_data2 = trial_2[gid] + if rates_data1['dt'] != rates_data2['dt'] or rates_data1['start'] != rates_data2['start'] or rates_data1['end'] != rates_data2['end']: + return False + + for r1, r2 in zip(rates_data1['rates'], rates_data2['rates']): + if abs(r1 - r2) > err: + return False + + return True \ No newline at end of file diff --git a/bmtk-vb/bmtk/analyzer/io_tools.py b/bmtk-vb/bmtk/analyzer/io_tools.py new file mode 100644 index 0000000..326389b --- /dev/null +++ b/bmtk-vb/bmtk/analyzer/io_tools.py @@ -0,0 +1,11 @@ +from six import string_types +from bmtk.simulator.utils.config import ConfigDict + + +def load_config(config): + if isinstance(config, string_types): + return ConfigDict.from_json(config) + elif isinstance(config, dict): + return ConfigDict.from_dict(config) + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(config, type(config))) \ No newline at end of file diff --git a/bmtk-vb/bmtk/analyzer/spike_trains.py b/bmtk-vb/bmtk/analyzer/spike_trains.py new file mode 100644 index 0000000..a7f6c8d --- /dev/null +++ b/bmtk-vb/bmtk/analyzer/spike_trains.py @@ -0,0 +1,16 @@ +import numpy as np +import pandas as pd +import h5py + + +from bmtk.analyzer.visualization.spikes import plot_spikes as raster_plot +from bmtk.analyzer.visualization.spikes import plot_rates as rates_plot +from .io_tools import load_config +from bmtk.utils.spike_trains import SpikesFile + + +def to_dataframe(config_file, spikes_file=None): + config = load_config(config_file) + spikes_file = SpikesFile(config.spikes_file) + return spikes_file.to_dataframe() + diff --git a/bmtk-vb/bmtk/analyzer/spikes_analyzer.py b/bmtk-vb/bmtk/analyzer/spikes_analyzer.py new file mode 100644 index 0000000..af77187 --- /dev/null +++ b/bmtk-vb/bmtk/analyzer/spikes_analyzer.py @@ -0,0 +1,127 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import pandas as pd +import numpy as np + +try: + from distutils.version import LooseVersion + use_sort_values = LooseVersion(pd.__version__) >= LooseVersion('0.19.0') + +except: + use_sort_values = False + + +def spikes2dict(spikes_file): + spikes_df = pd.read_csv(spikes_file, sep=' ', names=['time', 'gid']) + + if use_sort_values: + spikes_sorted = spikes_df.sort_values(['gid', 'time']) + else: + spikes_sorted = spikes_df.sort(['gid', 'time']) + + spike_dict = {} + for gid, spike_train in spikes_sorted.groupby('gid'): + spike_dict[gid] = np.array(spike_train['time']) + return spike_dict + + +def spike_files_equal(spikes_txt_1, spikes_txt_2, err=0.0001): + trial_1 = spikes2dict(spikes_txt_1) + trial_2 = spikes2dict(spikes_txt_2) + if set(trial_1.keys()) != set(trial_2.keys()): + return False + + for gid, spike_train1 in trial_1.items(): + spike_train2 = trial_2[gid] + if len(spike_train1) != len(spike_train2): + return False + + for s1, s2 in zip(spike_train1, spike_train2): + if abs(s1 - s2) > err: + return False + + return True + + +def get_mean_firing_rates(spike_gids, node_ids, tstop_msec): + + """ + Compute mean firing rate over the duration of the simulation + + :param spike_gids: gids of cells which spiked + :param node_ids: np.array of node_ids + + :return mean_firing_rate: np.array mean firing rates + + """ + + min_gid = np.min(node_ids) + max_gid = np.max(node_ids) + + gid_bins = np.arange(min_gid-0.5,max_gid+1.5,1) + hist,bins = np.histogram(spike_gids, bins=gid_bins) + + tstop_sec = tstop_msec*1E-3 + mean_firing_rates = hist/tstop_sec + + return mean_firing_rates + + + +def spikes_equal_in_window(spikes1,spikes2,twindow): + """ + Compare spikes within a time window + :param spikes1: dict with "time" and "gid" arrays for raster 1 + :param spikes2: dict with "time" and "gid" arrays for raster 2 + :param twindow: [tstart,tend] time window + + :return boolean: True if equal, False if different + """ + + ix1_window0=np.where(spikes1["time"]>twindow[0]) + ix1_window1=np.where(spikes1["time"]twindow[0]) + ix2_window1=np.where(spikes2["time"]ll;F00oRd+5w1*S%5?e14FO|NW@PANHCxg#g0HR{m|mnqGJ8Bq{O1c zl8nS${og`k0?S^Qb z`ejLpMTsRDiMjg4MalX}xh2^UA~7#9r?M)wNWUzzxHK^*vnsJ9Ge1v1K0Y%qvm`!V Xub}c4hfQvNN@-529mwcnAZ7pnPvIs& literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/analyzer/visualization/__pycache__/spikes.cpython-37.pyc b/bmtk-vb/bmtk/analyzer/visualization/__pycache__/spikes.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53f716bddc8295abb854aa62d25f2edad025d12f GIT binary patch literal 13247 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/analyzer/visualization/rasters.py b/bmtk-vb/bmtk/analyzer/visualization/rasters.py new file mode 100644 index 0000000..b177452 --- /dev/null +++ b/bmtk-vb/bmtk/analyzer/visualization/rasters.py @@ -0,0 +1,60 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np + + +def plot_raster_query(ax, spikes, nodes_df, cmap, twindow=[0, 3], marker=".", lw=0, s=10): + """Plot raster colored according to a query. + + Query's key defines node selection and the corresponding values defines color + :param ax: matplotlib axes object, axes to use + :param spikes: tuple of numpy arrays, includes [times, gids] + :param nodes_df: pandas DataFrame, nodes table + :param cmap: dict, key: query string, value:color + :param twindow: tuple [start_time,end_time] + :param marker: + :param lw: + :param s: + """ + tstart = twindow[0] + tend = twindow[1] + + ix_t = np.where((spikes[0] > tstart) & (spikes[0] < tend)) + + spike_times = spikes[0][ix_t] + spike_gids = spikes[1][ix_t] + + for query, col in cmap.items(): + query_df = nodes_df.query(query) + gids_query = query_df.index + print("{} ncells: {} {}".format(query, len(gids_query), col)) + + ix_g = np.in1d(spike_gids, gids_query) + ax.scatter(spike_times[ix_g], spike_gids[ix_g], + marker=marker, + # facecolors='none', + facecolors=col, + # edgecolors=col, + s=s, + label=query, + lw=lw) diff --git a/bmtk-vb/bmtk/analyzer/visualization/spikes.py b/bmtk-vb/bmtk/analyzer/visualization/spikes.py new file mode 100644 index 0000000..e7b34e9 --- /dev/null +++ b/bmtk-vb/bmtk/analyzer/visualization/spikes.py @@ -0,0 +1,499 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import csv +import h5py +from six import string_types +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.cm as cmx +import matplotlib.colors as colors +import matplotlib.gridspec as gridspec + +import bmtk.simulator.utils.config as config + +from mpl_toolkits.axes_grid1 import make_axes_locatable + +def _create_node_table(node_file, node_type_file, group_key=None, exclude=[]): + """Creates a merged nodes.csv and node_types.csv dataframe with excluded items removed. Returns a dataframe.""" + node_types_df = pd.read_csv(node_type_file, sep=' ', index_col='node_type_id') + nodes_h5 = h5py.File(node_file) + # TODO: Use utils.spikesReader + node_pop_name = nodes_h5['/nodes'].keys()[0] + + nodes_grp = nodes_h5['/nodes'][node_pop_name] + # TODO: Need to be able to handle gid or node_id + nodes_df = pd.DataFrame({'node_id': nodes_grp['node_id'], 'node_type_id': nodes_grp['node_type_id']}) + #nodes_df = pd.DataFrame({'node_id': nodes_h5['/nodes/node_gid'], 'node_type_id': nodes_h5['/nodes/node_type_id']}) + nodes_df.set_index('node_id', inplace=True) + + # nodes_df = pd.read_csv(node_file, sep=' ', index_col='node_id') + full_df = pd.merge(left=nodes_df, right=node_types_df, how='left', left_on='node_type_id', right_index=True) + + if group_key is not None and len(exclude) > 0: + # Make sure sure we group-key exists as column + if group_key not in full_df: + raise Exception('Could not find column {}'.format(group_key)) + + group_keys = set(nodes_df[group_key].unique()) - set(exclude) + groupings = nodes_df.groupby(group_key) + # remove any rows with matching column value + for cond in exclude: + full_df = full_df[full_df[group_key] != cond] + + return full_df + +def _count_spikes(spikes_file, max_gid, interval=None): + def parse_line(line): + ts, gid = line.strip().split(' ') + return float(ts), int(gid) + + if interval is None: + t_max = t_bounds_low = -1.0 + t_min = t_bounds_high = 1e16 + elif hasattr(interval, "__getitem__") and len(interval) == 2: + t_min = t_bounds_low = interval[0] + t_max = t_bounds_high = interval[1] + elif isinstance(interval, float): + t_max = t_min = t_bounds_low = interval[0] + t_bounds_high = 1e16 + else: + raise Exception("Unable to determine interval.") + + max_gid = int(max_gid) # strange bug where max_gid was being returned as a float. + spikes = [[] for _ in xrange(max_gid+1)] + spike_sums = np.zeros(max_gid+1) + # TODO: Use utils.spikesReader + spikes_h5 = h5py.File(spikes_file, 'r') + #print spikes_h5['/spikes'].keys() + gid_ds = spikes_h5['/spikes/gids'] + ts_ds = spikes_h5['/spikes/timestamps'] + + for i in range(len(gid_ds)): + ts = ts_ds[i] + gid = gid_ds[i] + + if gid <= max_gid and t_bounds_low <= ts <= t_bounds_high: + spikes[gid].append(ts) + spike_sums[gid] += 1 + t_min = ts if ts < t_min else t_min + t_max = ts if ts > t_max else t_max + + """ + with open(spikes_file, 'r') as fspikes: + for line in fspikes: + ts, gid = parse_line(line) + if gid <= max_gid and t_bounds_low <= ts <= t_bounds_high: + spikes[gid].append(ts) + spike_sums[gid] += 1 + t_min = ts if ts < t_min else t_min + t_max = ts if ts > t_max else t_max + """ + return spikes, spike_sums/(float(t_max-t_min)*1e-3) + + + +def plot_spikes_config(configure, group_key=None, exclude=[], save_as=None, show_plot=True): + if isinstance(configure, string_types): + conf = config.from_json(configure) + elif isinstance(configure, dict): + conf = configure + else: + raise Exception("configure variable must be either a json dictionary or json file name.") + + cells_file_name = conf['internal']['nodes'] + cell_models_file_name = conf['internal']['node_types'] + spikes_file = conf['output']['spikes_ascii'] + + plot_spikes(cells_file_name, cell_models_file_name, spikes_file, group_key, exclude, save_as, show_plot) + + +def plot_spikes(cells_file, cell_models_file, spikes_file, population=None, group_key=None, exclude=[], save_as=None, + show=True, title=None): + # check if can be shown and/or saved + #if save_as is not None: + # if os.path.exists(save_as): + # raise Exception('file {} already exists. Cannot save.'.format(save_as)) + + cm_df = pd.read_csv(cell_models_file, sep=' ') + cm_df.set_index('node_type_id', inplace=True) + + cells_h5 = h5py.File(cells_file, 'r') + # TODO: Use sonata api + if population is None: + if len(cells_h5['/nodes']) > 1: + raise Exception('Multiple populations in nodes file. Please specify one to plot using population param') + else: + population = cells_h5['/nodes'].keys()[0] + + nodes_grp = cells_h5['/nodes'][population] + c_df = pd.DataFrame({'node_id': nodes_grp['node_id'], 'node_type_id': nodes_grp['node_type_id']}) + # c_df = pd.read_csv(cells_file, sep=' ') + c_df.set_index('node_id', inplace=True) + nodes_df = pd.merge(left=c_df, + right=cm_df, + how='left', + left_on='node_type_id', + right_index=True) # use 'model_id' key to merge, for right table the "model_id" is an index + + # TODO: Uses utils.SpikesReader to open + spikes_h5 = h5py.File(spikes_file, 'r') + spike_gids = np.array(spikes_h5['/spikes/gids'], dtype=np.uint) + spike_times = np.array(spikes_h5['/spikes/timestamps'], dtype=np.float) + # spike_times, spike_gids = np.loadtxt(spikes_file, dtype='float32,int', unpack=True) + # spike_gids, spike_times = np.loadtxt(spikes_file, dtype='int,float32', unpack=True) + + spike_times = spike_times * 1.0e-3 + + if group_key is not None: + if group_key not in nodes_df: + raise Exception('Could not find column {}'.format(group_key)) + groupings = nodes_df.groupby(group_key) + + n_colors = nodes_df[group_key].nunique() + color_norm = colors.Normalize(vmin=0, vmax=(n_colors-1)) + scalar_map = cmx.ScalarMappable(norm=color_norm, cmap='hsv') + color_map = [scalar_map.to_rgba(i) for i in range(0, n_colors)] + else: + groupings = [(None, nodes_df)] + color_map = ['blue'] + + #marker = '.' if len(nodes_df) > 1000 else 'o' + marker = 'o' + + # Create plot + gs = gridspec.GridSpec(2, 1, height_ratios=[7, 1]) + ax1 = plt.subplot(gs[0]) + gid_min = 10**10 + gid_max = -1 + for color, (group_name, group_df) in zip(color_map, groupings): + if group_name in exclude: + continue + group_min_gid = min(group_df.index.tolist()) + group_max_gid = max(group_df.index.tolist()) + gid_min = group_min_gid if group_min_gid <= gid_min else gid_min + gid_max = group_max_gid if group_max_gid > gid_max else gid_max + + gids_group = group_df.index + indexes = np.in1d(spike_gids, gids_group) + ax1.scatter(spike_times[indexes], spike_gids[indexes], marker=marker, facecolors=color, label=group_name, lw=0, s=5) + + #ax1.set_xlabel('time (s)') + ax1.axes.get_xaxis().set_visible(False) + ax1.set_ylabel('cell_id') + ax1.set_xlim([0, max(spike_times)]) + ax1.set_ylim([gid_min, gid_max]) + plt.legend(markerscale=2, scatterpoints=1) + + ax2 = plt.subplot(gs[1]) + plt.hist(spike_times, 100) + ax2.set_xlabel('time (s)') + ax2.set_xlim([0, max(spike_times)]) + ax2.axes.get_yaxis().set_visible(False) + if title is not None: + ax1.set_title(title) + + if save_as is not None: + plt.savefig(save_as) + + if show: + plt.show() + + +def plot_ratess(cells_file, cell_models_file, spikes_file, group_key='pop_name', exclude=['LIF_inh', 'LIF_exc'], save_as=None, show_plot=True): + #if save_as is not None: + # if os.path.exists(save_as): + # raise Exception('file {} already exists. Cannot save.'.format(save_as)) + + cm_df = pd.read_csv(cell_models_file, sep=' ') + cm_df.set_index('node_type_id', inplace=True) + + c_df = pd.read_csv(cells_file, sep=' ') + c_df.set_index('node_id', inplace=True) + nodes_df = pd.merge(left=c_df, + right=cm_df, + how='left', + left_on='node_type_id', + right_index=True) # use 'model_id' key to merge, for right table the "model_id" is an index + + for cond in exclude: + nodes_df = nodes_df[nodes_df[group_key] != cond] + + groupings = nodes_df.groupby(group_key) + n_colors = nodes_df[group_key].nunique() + color_norm = colors.Normalize(vmin=0, vmax=(n_colors - 1)) + scalar_map = cmx.ScalarMappable(norm=color_norm, cmap='hsv') + color_map = [scalar_map.to_rgba(i) for i in range(0, n_colors)] + + + spike_times, spike_gids = np.loadtxt(spikes_file, dtype='float32,int', unpack=True) + rates = np.zeros(max(spike_gids) + 1) + for ts, gid in zip(spike_times, spike_gids): + if ts < 500.0: + continue + rates[gid] += 1 + + for color, (group_name, group_df) in zip(color_map, groupings): + print(group_name) + print(group_df.index) + print(rates[group_df.index]) + plt.plot(group_df.index, rates[group_df.index], '.', color=color) + + plt.show() + + print(n_colors) + exit() + + + + group_keys = set(nodes_df[group_key].unique()) - set(exclude) + groupings = nodes_df.groupby(group_key) + + n_colors = len(group_keys) + color_norm = colors.Normalize(vmin=0, vmax=(n_colors - 1)) + scalar_map = cmx.ScalarMappable(norm=color_norm, cmap='hsv') + color_map = [scalar_map.to_rgba(i) for i in range(0, n_colors)] + + for color, (group_name, group_df) in zip(color_map, groupings): + print(group_name) + print(group_df.index) + + exit() + + + """ + print color_map + exit() + + n_colors = nodes_df[group_key].nunique() + color_norm = colors.Normalize(vmin=0, vmax=(n_colors - 1)) + scalar_map = cmx.ScalarMappable(norm=color_norm, cmap='hsv') + color_map = [scalar_map.to_rgba(i) for i in range(0, n_colors)] + """ + + spike_times, spike_gids = np.loadtxt(spikes_file, dtype='float32,int', unpack=True) + rates = np.zeros(max(spike_gids)+1) + + for ts, gid in zip(spike_times, spike_gids): + if ts < 500.0: + continue + + rates[gid] += 1 + + rates = rates / 3.0 + + plt.plot(xrange(max(spike_gids)+1), rates, '.') + plt.show() + + +def plot_rates(cells_file, cell_models_file, spikes_file, group_key=None, exclude=[], interval=None, show=True, + title=None, save_as=None, smoothed=False): + def smooth(data, window=100): + h = int(window/2) + x_max = len(data) + return [np.mean(data[max(0, x-h):min(x_max, x+h)]) for x in xrange(0, x_max)] + + nodes_df = _create_node_table(cells_file, cell_models_file, group_key, exclude) + _, spike_rates = _count_spikes(spikes_file, max(nodes_df.index), interval) + + if group_key is not None: + groupings = nodes_df.groupby(group_key) + group_order = {k: i for i, k in enumerate(nodes_df[group_key].unique())} + + n_colors = len(group_order) + color_norm = colors.Normalize(vmin=0, vmax=(n_colors-1)) + scalar_map = cmx.ScalarMappable(norm=color_norm, cmap='hsv') + color_map = [scalar_map.to_rgba(i) for i in range(0, n_colors)] + ordered_groupings = [(group_order[name], c, name, df) for c, (name, df) in zip(color_map, groupings)] + + else: + ordered_groupings = [(0, 'blue', None, nodes_df)] + + keys = ['' for _ in xrange(len(group_order))] + means = [0 for _ in xrange(len(group_order))] + stds = [0 for _ in xrange(len(group_order))] + fig = plt.figure() + ax1 = fig.add_subplot(111) + for indx, color, group_name, group_df in ordered_groupings: + keys[indx] = group_name + means[indx] = np.mean(spike_rates[group_df.index]) + stds[indx] = np.std(spike_rates[group_df.index]) + y = smooth(spike_rates[group_df.index]) if smoothed else spike_rates[group_df.index] + ax1.plot(group_df.index, y, '.', color=color, label=group_name) + + max_rate = np.max(spike_rates) + ax1.set_ylim(0, 50)#max_rate*1.3) + ax1.set_ylabel('Hz') + ax1.set_xlabel('gid') + ax1.legend(fontsize='x-small') + if title is not None: + ax1.set_title(title) + if save_as is not None: + plt.savefig(save_as) + + plt.figure() + plt.errorbar(xrange(len(means)), means, stds, linestyle='None', marker='o') + plt.xlim(-0.5, len(color_map)-0.5) # len(color_map) == last_index + 1 + plt.ylim(0, 50.0)# max_rate*1.3) + plt.xticks(xrange(len(means)), keys) + if title is not None: + plt.title(title) + if save_as is not None: + if save_as.endswith('.jpg'): + base = save_as[0:-4] + elif save_as.endswith('.jpeg'): + base = save_as[0:-5] + else: + base = save_as + + plt.savefig('{}.summary.jpg'.format(base)) + with open('{}.summary.csv'.format(base), 'w') as f: + f.write('population mean stddev\n') + for i, key in enumerate(keys): + f.write('{} {} {}\n'.format(key, means[i], stds[i])) + + if show: + plt.show() + +def plot_rates_popnet(cell_models_file, rates_file, model_keys=None, save_as=None, show_plot=True): + """Initial method for plotting popnet output + + :param cell_models_file: + :param rates_file: + :param model_keys: + :param save_as: + :param show_plot: + :return: + """ + + pops_df = pd.read_csv(cell_models_file, sep=' ') + lookup_col = model_keys if model_keys is not None else 'node_type_id' + pop_keys = {str(r['node_type_id']): r[lookup_col] for _, r in pops_df.iterrows()} + + # organize the rates file by population + # rates = {pop_name: ([], []) for pop_name in pop_keys.keys()} + rates_df = pd.read_csv(rates_file, sep=' ', names=['id', 'times', 'rates']) + for grp_key, grp_df in rates_df.groupby('id'): + grp_label = pop_keys[str(grp_key)] + plt.plot(grp_df['times'], grp_df['rates'], label=grp_label) + + plt.legend(fontsize='x-small') + plt.xlabel('time (s)') + plt.ylabel('firing rates (Hz)') + + if save_as is not None: + plt.savefig(save_as) + + if show_plot: + plt.show() + +def plot_avg_rates(cell_models_file, rates_file, model_keys=None, save_as=None, show_plot=True): + pops_df = pd.read_csv(cell_models_file, sep=' ') + lookup_col = model_keys if model_keys is not None else 'node_type_id' + pop_keys = {str(r['node_type_id']): r[lookup_col] for _, r in pops_df.iterrows()} + + # organize the rates file by population + rates = {pop_name: [] for pop_name in pop_keys.keys()} + with open(rates_file, 'r') as f: + reader = csv.reader(f, delimiter=' ') + for row in reader: + if row[0] in rates: + #rates[row[0]][0].append(row[1]) + rates[row[0]].append(float(row[2])) + + labels = [] + means = [] + stds = [] + #print rates + for pop_name in pops_df['node_type_id'].unique(): + r = rates[str(pop_name)] + if len(r) == 0: + continue + + labels.append(pop_keys.get(str(pop_name), str(pop_name))) + means.append(np.mean(r)) + stds.append(np.std(r)) + + plt.figure() + plt.errorbar(xrange(len(means)), means, stds, linestyle='None', marker='o') + plt.xlim(-0.5, len(means) - 0.5) + plt.xticks(xrange(len(means)), labels) + plt.ylabel('firing rates (Hz)') + + if save_as is not None: + plt.savefig(save_as) + + if show_plot: + plt.show() + + +def plot_tuning(sg_analysis, node, band, Freq=0, show=True, save_as=None): + def index_for_node(node, band): + if node == 's4': + mask = sg_analysis.node_table.node == node + else: + mask = (sg_analysis.node_table.node == node) & (sg_analysis.node_table.band == band) + return str(sg_analysis.node_table[mask].index[0]) + + index = index_for_node(node, band) + + key = index + '/sg/tuning' + analysis_file = sg_analysis.get_tunings_file() + + tuning_matrix = analysis_file[key].value[:, :, :, Freq] + + n_or, n_sf, n_ph = tuning_matrix.shape + + vmax = np.max(tuning_matrix[:, :, :]) + vmin = np.min(tuning_matrix[:, :, :]) + + #fig, ax = plt.subplots(1, n_ph, figsize=(12, 16), sharex=True, sharey=True) + fig, ax = plt.subplots(1, n_ph, figsize=(13.9, 4.3), sharex=False, sharey=True) + + print(sg_analysis.orientations) + for phase in range(n_ph): + tuning_to_plot = tuning_matrix[:, :, phase] + + im = ax[phase].imshow(tuning_to_plot, interpolation='nearest', vmax=vmax, vmin=vmin) + ax[phase].set_xticklabels([0] + list(sg_analysis.spatial_frequencies)) + ax[phase].set_yticklabels([0] + list(sg_analysis.orientations)) + + ax[phase].set_title('phase = {}'.format(sg_analysis.phases[phase])) + ax[phase].set_xlabel('spatial_frequency') + if phase == 0: + ax[phase].set_ylabel('orientation') + + fig.subplots_adjust(right=0.90) + cbar_ax = fig.add_axes([0.92, 0.10, 0.02, 0.75]) + cbar = fig.colorbar(im, cax=cbar_ax, ticks=[vmin, 0.0, vmax]) + + if save_as is not None: + plt.savefig(save_as) + + if show: + plt.show() + + + #config_file = +# plot_spikes('../../examples/pointnet/example2/config.json', 'pop_name') diff --git a/bmtk-vb/bmtk/analyzer/visualization/spikes.pyc b/bmtk-vb/bmtk/analyzer/visualization/spikes.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d07cf0c09ead5ad6d4af8dfd35fd086b751a7193 GIT binary patch literal 15676 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/builder/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3807c1db415a9974088e2140493acc378baafaa GIT binary patch literal 207 zcmZ?b<>g`k0?S^<7!@`KhQ}Zd3@`y14nSNi0whuxQW$d>av7r-bD5%;7#UKSf*CZK zUorxvG?{MkxTNM4r~0Lql;;;^-{ONZoJuouQc{cjG?}9~@*r}>MIcj)n1RGfh9VXa z8$|pv(hn_8Eh^S8OG+$CEXhdB)h{ke)=$bU$%YaTqx9qBGxIV_;^XxSDsOSv-dy)+I0 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/__pycache__/connection_map.cpython-37.pyc b/bmtk-vb/bmtk/builder/__pycache__/connection_map.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b21f72cf777626d151534287a5e09a34083b89c3 GIT binary patch literal 5570 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/__pycache__/id_generator.cpython-37.pyc b/bmtk-vb/bmtk/builder/__pycache__/id_generator.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2162f3cf31a7882af624b0f8a03419daf55b3f9b GIT binary patch literal 2156 zcmZWpTW=gS6t?HKlkKK0y>XZPLe+(m^r=7#J9-Hx$X$^Hg=Bfbt!2H^u%2}JKK+QwcVZhK`js6 zt@dp;8K``~?4Ya)!Mc*UfstH%YI#p?uwB<=(6$R(vH`b%X5^?mkeL#<`bxB!(sO{E zTghgaY@&(+^aE*NyQ_=_P%uCVunLqV1nX5gcdFD(SyovoJR3x^{@TevYM4i6K)Eyy zc4+A)r5S=LTCj;^6RsU|C6k33Y3?K&8+oA0$`(f~2PSw1E^d0jr;m3Y-}4&#nC+q$ zM-q;6)>R;l;YSEHlyc0R;rWo2J;n?4#}Si<$~y04aPJbsv61Qz96CI{W z_4RBeGqNl%EA6DILn~c<_57OSL#gYvSzv)d9erEo!}`kkT?NdwgF3RZ=yg)1mCFuz z@i#G8G_US%{%EDKn+IJExj5jXO>6Sa?#K=2qEdyBW>bl*zsT2(@ll;-nTN`5BDDkr zp$Sdt@wFB6Hx`@DIRs16JLC>1j+{~#Oz8=A%$}!y7Fv@t1C)NQ438%}C+Gg1nY_JGN=O~-XE>k!JJ zj*3$E&07EmPl!6KV|?Xp9UHFuvK`GTAwpi}Lo^8=DR0-pMqd_C?QkR9{Q#D23k0Dp z$dr_(!Id{^eztWF5PX`h;bWosNM>2x%(77_sseQg|9-`b`AQ?pM44xqL3D%e^gc81 zqt-@&DYhceFoS+VyS+_m8qsE)#%U`J{R&hFzD#wjtI_z#tmBD~a3U$Y&t&fWrWHtk U!;J9O%*^|5BRGNpRhmZs0e)2*1^@s6 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/__pycache__/iterator.cpython-37.pyc b/bmtk-vb/bmtk/builder/__pycache__/iterator.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48cf404e84fce57f306f642b73e919b16f22be07 GIT binary patch literal 4066 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/__pycache__/network.cpython-37.pyc b/bmtk-vb/bmtk/builder/__pycache__/network.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25dbaffb8b7e981090a9a9d9b469197f4baf7bcb GIT binary patch literal 14169 zcmb_jOK=>=d7jtq&Mp=p2tuStOVo&>Nv=$Slp;UGka|&~ELnnOleV0Vl=WhJ04%uJ z1$zc0v0B)UNn4a-$yVY@;*?VbBq=*iC6$yb#~gCWJ*i4AQ@JITDpmQAOAhfNmGAp| z9t)6^6;~EDJw5&W`|tnN|KE6Sas6FIuXi z{9Bgt!>OBYbC0qwS&ixkn9-X5z((4z6Muf7!q*0a9`chH2K|26iMxaTtL-%uR9JZ_ zjFFvq>G>DKeyH2=KzBGo_VbZ>22b=jMhvZ{tt_m-R?aP}>4u(im3Pa!X*WyAmsDBu zW#k1ilvj|is+yGh$WN%cLqnZeGDag)XVAt^&oQ7>T~L_dI-6_Do_uj)r>l#9>IH`I;uX7_pCamj^llwdPOzV zqiDHbeL|f?$pQ7Mnp3Aya=)5akEzFzJE(q5J%PGI>XYhIcz;ZNT73rZ2h``)ljIHQX??==u_Tx2RIgGCl^p%d2 z;?%{P`Ogup-$4>vBm1VU>?@9*je**m_BZX3J+fLrG*VZ&NIl7yBwt>#P*Rc9m-1?t zCAD}$c}o}z`MT6j##1+0vnQTXB`KfIb9>_nshg3$`;-N6%k!F)GrTVaI_S4o!(g== zMcw|=+{L8c2g$VJjkU1VRUarcZOkTaFYG7P7jAUIH4;}c+3K4mx7a(WwEFB7CH0oP zTMO&mUd-Z9ElIJw)8d*mYDHljH6LiDV6cnRadn-gY`bzCHqY? z!Wp&J^k6O2aTim$7&h5AUfCJ+)>r#cGL60&ceGa9YmqKvaVf}HYikK>+*|Z4iU@p$ z5otA*FP}q3m@geVR9Q{ba1$>Idy9!DD4NU=Mp&btTKXYO{)@*>e<=!ebo$yt8;rT! zUOgS@&gq5K_)3}pk1Eus)6R2i8;Rd)b^G18)jEm+A|{qyw|%GVlx_JlZ=`j%?D}@i zu9bXyIGy5oF6-VQ74XM5p9^>*qGV*PkWlRThOO)P#_IvR1Y(l}{Cp6}^LQemdS_VA zX5e~1+UQg0e)rLni7`U@?AR>(@>xi5hN3>&WYZrq*%*1ga<|FG>hC@og=lPj6S6bK zT(WO|5Wlp#)(cld5UhGZ>w&(6)Ru9HaB-6z>i z5&Q2w#h#@wΝ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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/__pycache__/node.cpython-37.pyc b/bmtk-vb/bmtk/builder/__pycache__/node.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2b4559c4002feb87d93a712da207bb71f1488b1 GIT binary patch literal 2045 zcma(S%Wm67aF?VgN}_Dpv6DDK3p7B1>d>e?q(Ff*NE4t%FS-=H2n7}lXI(RvWOkP- zU_m`uKu-M!=h$D`Yfd@j2YP8|h7?T;c9A7^c6N7m-ZOmB?{@``Z~yxK*AG1*-qB_= z0z{tUvB1iDuf?PXci;fm7!Khsu6?)% z_i^2MBLXqmMg1PSV0;Gp{l7TU7Aq$Nt$sD_H;5FXkSlTFU3!^#E2UU@E1~=qp1t-# zJ{2n;_rWSa*rHPvf~jHnJuiy_yxL6Ras2g$H6LBkGC*KtY#q(N&PqOWkn1td;Gb3nmbaj^JI z;@KB?CaW{-BwSFKDxM*~mhtmBUz~rc&s`hzak7{eTiN6M-*oW=Us n41B;T&GNYQe zH!Ja|p~N*G)t2Ex3Yj_X(b zIn~oC8d7VFUfv-hZjaP|=&_}MM{=hV2b-@OMSc`UKL2y5bRN-E!-6u@C$~|$qp7b8FR_!TP zuof?yNt`PkC80Wu66%{=$LZrRKOQR6Bc<8npz%Il^EU{^x?HlZki6?iA)P0z>q=L8 z`1fQ@*709cb?M7XPuL0X`e--gifvy)yD3*~yP;O(n!Ie=O}Q?wVBM;`3J0&1ekYUa z^DIlEO~Kg1Jk!bqQP9hhMD_AGOM|Q*q|h@cQX z>`6g;y15CU7eu|DGS)pA_xnn#G`HK@IHpm5@!5x;y=O=71|JWGi5fub9F}hxg@NR; z+R;%Er@dq(DdK{n0LXce4JoWln|2VZwzWlgR%mSzP;J|OlZdnRb_+`{j6G;Q=fT`) zfYU^n*dvdX`5Eg((qKDy`~LmslrRR*mv$vxtJMR4?BhUE`xCt821LON7P3M>g3gda zGLVmVT26U2q_ZUghT)V4MBVKbAB1gTD;t8Vu5Gi`X5x3Kss}oH0nwfgRzG+MV(wDGwfV_vg zcuGb)HjtJmMf-lqL))uF6SL1QZBj}c4fXbYGDS9BHhz7{(L|+T*xpRCUX+;a_FNy; z$T6cqi)$Z?F4!&WVQ3d_%&am#UcYE{9?mR!LcqKN@h{frHh%wmw#QESnML7w3_R$* zLyH0kVDQR>f6q=q94zB!LW_b=5t$N4l(&{hVlz6-cJ@%R!o}%pINe&^_8XU_phAX@s6 z_XS|9Qia=HXV!_;i{saq(ETmmY74P+MAM{r@eXw2w8I|p4Cy$H%uS<#dht$Wb80Xo zop+bEd})r|vc`O0x!TdyVNaZZDEmSOlF9^2zm^XDAN_{rP8}l7 zH@6>&37-h*p1LS0;!I4S1Pf_eiCX9X$BT#&n7+_>Ai#iVqa5{Zrh;ILc6k zXuYEe=#t+p+1USXvbA;RyUCqr{Pxoy@w95XPO}ckZ&_4&IOl;WYt^Mu`YM&?+c@8C zxi)V~zF+dga;2B0GNwBvN-m~SV^OoVVB1`WV4}g-xG!q_I&bnDc-O`F+8oKniXNpD zJ>(W)X>rWa$Wd5xej?0TF8;xDrwtQlkFPJ_U8v9TrKFql zbc_yAB$hU*79Z^bIcB-|2xvLLj-LV*$-qUg@T}(VByi3};Y#O3=uY7l-U&CFMh|Y_ zeB9i{c)#!pcfYQu~ zC4I1Vd}|KnPnznq)+hw37GYR6!f=quQ9|t|t%;K93@#d(^}f3qCU*^)YVxM@fBVb~1ONa4 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/__pycache__/node_set.cpython-37.pyc b/bmtk-vb/bmtk/builder/__pycache__/node_set.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb8035916bdd565ffa509eb3022b87c0ef049a92 GIT binary patch literal 2032 zcma(S%WfP+upjfb0!R``3(_T$NnX%C>oQ8dBhr)pIgx(G$Gq!Fx=G^THQXl2YYI4~)=Ns6^^jFB z4#Wn4{vJ?9x>S-blT@;E(&d>ax%AG-n09@jeHl1906LVBqeGw@vgznZwqzULHDm`= zFW3;ZZDyv2w&U^vP~q1A`UgN2t%x8M157Ip7*tpMAkfQ({|uC*O9Z%{@hh@VFMP8h(v$?0vc$FrpjDXDVJ31Jw`@zy%n%~6?_O=(wPC8#zP8IZ z*zWaEmBUP#JkxgJxzW^TZUOnChZ_HlWU)8F& zy4^PiGjx;}GE=Kgn9j_~@YF^^+%tCWElp2t;M|Fa>u_;#8in5m$dL4-R_J7-vlYiQwu#YSi zH_TAWz}^6u>Aw_?4q}nr!?TY8IVT@N2q6Im2P1=EPt)Sfsf)S7rwY@nT+MK&T73u% z8%)dYS{=F0Lo>bwn3_T3hO2PPcQfO<5++oBV*P%34Aah6>0mdrer__Djg4$@)X!kB zWR{=h!#HxSZ95Qiv6~I>1SVoM%cAav=}_zxsj(cI!}>!v5@~Mvaay?O)YhS4)^~Y$ zhNj_kAPh&(B7Z;^>0S6X=~?Gm#*(;+vulv{Ga+nKh<+)L3Z&aY93G{`^h7;C;n8ep zo;n{Dwu4$i^bpYsA{?M|$OQ}kuHFaGH~=CVb=uzar|=~973Rx&=&?(N4a)8PtY_Tu WoG>Tbc^R!u&0NQwMkx_(^S=QYy25w> literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/aux/__init__.py b/bmtk-vb/bmtk/builder/aux/__init__.py new file mode 100644 index 0000000..2d56a26 --- /dev/null +++ b/bmtk-vb/bmtk/builder/aux/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# \ No newline at end of file diff --git a/bmtk-vb/bmtk/builder/aux/__init__.pyc b/bmtk-vb/bmtk/builder/aux/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..581d2e28f0cf2895184a85af2a2fc0934a0c63e6 GIT binary patch literal 141 zcmZSn%*)m7+8dk900oRd+5w1*S%5?e14FO|NW@PANHCxg#b!V;{m|mnqGJ8Bq{O1c zl8nS${o d_max: + dw = 0.0 + else: + t = r / d_max + dw = d_weight_max * (1.0 - t) + d_weight_min * t + + # drop the connection if the weight is too low + if dw <= 0: + return None + + # filter out nodes by treating the weight as a probability of connection + if random.random() > dw: + return None + + # Add the number of synapses for every connection. + tmp_nsyn = random.randint(nsyn_min, nsyn_max) + return tmp_nsyn + + +def connect_random(source, target, nsyn_min=0, nsyn_max=10, distribution=None): + return np.random.randint(nsyn_min, nsyn_max) \ No newline at end of file diff --git a/bmtk-vb/bmtk/builder/aux/edge_connectors.pyc b/bmtk-vb/bmtk/builder/aux/edge_connectors.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71f9849b2c9f54d9492c5374f0384efc804959d4 GIT binary patch literal 1099 zcmb_b&2G~`5FY;|ja?U{QraL8LY#8Ky>a1Bs<`Ihh)X17IbO?k60f7(HEks4)ZP$h zo`R>~g^+jv_-2#Ra_?&A+xgqs*_mDM_d)pe!>-yYhhd7rM3UuZ-9_@_m7NCGy5j;M=pN50V51i)3JD^oR6mk{jO zF~8ndERF^M!-bHIDAGz8F7mjM1uh5W%5c4wmCTl%d5fa zU{y9{abX2xH=BBua)1%cWI?|L7k|=)((#vBrZRJ$)v;E2Jgd!O2W_^BY?s4!^6Wv!1TDOlM!ETJYu)R0=FySa@a;E+~Mxi*rvHn zAwGw~2J59MMu`MS1kIB6?b;Q(w$(CFH#)3kf4Ti_AnD77`b*b#o z%3;qlkCU6!1)#CAAu164U8?VwUK=XI1&DpKK^jDvk}b&$yU6_{Ok+8rKjg+Z&;_ZM zU8}_OMBWu~ve3p4t~@TN;dV^qeh?)l$^~vBwcZ%RU5iz+$o)_ix)S~UNo0Sa(k55S z!l*Qnhq8?lm$~r0@N3}*59djkehxHxtD{_xJ}&(n`S|fj=fP+ho2!}?Y8OWN$S?Mz zBn_iQhI6rgnjK6Zx3g3$epD}l6e+31&(ERO928lPtY_9aeiwGYRbb+2lF4}tP>szN zvuf2%MdQ$QjL!<=L(a_OLQf>6UHVCiy5X9zTC3~GW1mxDtm3~7Rk{w4X@YU(_7y_W z_IH*(r?^*W6&k@EzksZH9LVzcU3oGo;_Ohx#~q#ym^}o$4dX6a(T2wtaT*r8=;aVt M^C0%Dp7W&p8#fx?Q~&?~ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/bionet/__init__.py b/bmtk-vb/bmtk/builder/bionet/__init__.py new file mode 100644 index 0000000..324aace --- /dev/null +++ b/bmtk-vb/bmtk/builder/bionet/__init__.py @@ -0,0 +1 @@ +from swc_reader import SWCReader \ No newline at end of file diff --git a/bmtk-vb/bmtk/builder/bionet/swc_reader.py b/bmtk-vb/bmtk/builder/bionet/swc_reader.py new file mode 100644 index 0000000..4833a1d --- /dev/null +++ b/bmtk-vb/bmtk/builder/bionet/swc_reader.py @@ -0,0 +1,81 @@ +import numpy as np +from neuron import h + +from bmtk.simulator.bionet import nrn +from bmtk.simulator.bionet.morphology import Morphology + + +class SWCReader(object): + def __init__(self, swc_file, random_seed=10, fix_axon=True): + nrn.load_neuron_modules(None, None) + self._swc_file = swc_file + self._hobj = h.Biophys1(swc_file) + if fix_axon: + self._fix_axon() + + self._morphology = Morphology(self._hobj) + self._morphology.set_seg_props() + self._morphology.calc_seg_coords() + self._prng = np.random.RandomState(random_seed) + + self._secs = [] + self._save_sections() + + def _save_sections(self): + for sec in self._hobj.all: + for _ in sec: + self._secs.append(sec) + + def _fix_axon(self): + """Removes and refixes axon""" + axon_diams = [self._hobj.axon[0].diam, self._hobj.axon[0].diam] + for sec in self._hobj.all: + section_name = sec.name().split(".")[1][:4] + if section_name == 'axon': + axon_diams[1] = sec.diam + + for sec in self._hobj.axon: + h.delete_section(sec=sec) + + h.execute('create axon[2]', self._hobj) + for index, sec in enumerate(self._hobj.axon): + sec.L = 30 + sec.diam = 1 + + self._hobj.axonal.append(sec=sec) + self._hobj.all.append(sec=sec) # need to remove this comment + + self._hobj.axon[0].connect(self._hobj.soma[0], 1.0, 0) + self._hobj.axon[1].connect(self._hobj.axon[0], 1.0, 0) + + h.define_shape() + + def find_sections(self, section_names, distance_range): + return self._morphology.find_sections(section_names, distance_range) + + def choose_sections(self, section_names, distance_range, n_sections=1): + secs, probs = self.find_sections(section_names, distance_range) + secs_ix = self._prng.choice(secs, n_sections, p=probs) + return secs_ix, self._morphology.seg_prop['x'][secs_ix] + + def get_coord(self, sec_ids, sec_xs, soma_center=(0.0, 0.0, 0.0), rotations=None): + adjusted = self._morphology.get_soma_pos() - np.array(soma_center) + absolute_coords = [] + for sec_id, sec_x in zip(sec_ids, sec_xs): + sec = self._secs[sec_id] + n_coords = int(h.n3d(sec=sec)) + coord_indx = int(sec_x*(n_coords - 1)) + swc_coords = np.array([h.x3d(coord_indx, sec=sec), h.y3d(coord_indx, sec=sec), h.x3d(coord_indx, sec=sec)]) + absolute_coords.append(swc_coords - adjusted) + + if rotations is not None: + raise NotImplementedError + + return absolute_coords + + def get_dist(self, sec_ids): + return [self._morphology.seg_prop['dist'][sec_id] for sec_id in sec_ids] + + def get_type(self, sec_ids): + return [self._morphology.seg_prop['type'][sec_id] for sec_id in sec_ids] + diff --git a/bmtk-vb/bmtk/builder/connection_map.py b/bmtk-vb/bmtk/builder/connection_map.py new file mode 100644 index 0000000..863cf26 --- /dev/null +++ b/bmtk-vb/bmtk/builder/connection_map.py @@ -0,0 +1,153 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from . import connector +from . import iterator + + +class ConnectionMap(object): + """Class for keeping track of connection rules. + + For every connection from source --> target this keeps track of rules (functions, literals, lists) for + 1. the number of synapses between source and target + 2. Used defined parameters (syn-weight, synaptic-location) for every synapse. + + The number of synapses rule (1) is stored as a connector. Individual synaptic parameters, if they exists, are stored + as ParamsRules. + """ + + class ParamsRules(object): + """A subclass to store indvidiual synpatic parameter rules""" + def __init__(self, names, rule, rule_params, dtypes): + self._names = names + self._rule = rule + self._rule_params = rule_params + self._dtypes = self.__create_dtype_dict(names, dtypes) + + def __create_dtype_dict(self, names, dtypes): + if isinstance(names, list): + # TODO: compare size of names and dtypes + return {n: dt for n, dt in zip(names, dtypes)} + else: + return {names: dtypes} + + @property + def names(self): + return self._names + + @property + def rule(self): + return connector.create(self._rule, **(self._rule_params or {})) + + @property + def dtypes(self): + return self._dtypes + + def get_prop_dtype(self, prop_name): + return self._dtypes[prop_name] + + def __init__(self, sources=None, targets=None, connector=None, connector_params=None, iterator='one_to_one', + edge_type_properties=None): + self._source_nodes = sources # source nodes + self._target_nodes = targets # target nodes + self._connector = connector # function, list or value that determines connection between sources and targets + self._connector_params = connector_params # parameters passed into connector + self._iterator = iterator # rule for iterating between sources and targets + self._edge_type_properties = edge_type_properties + + self._params = [] + self._param_keys = [] + + @property + def params(self): + return self._params + + @property + def source_nodes(self): + return self._source_nodes + + @property + def source_network_name(self): + return self._source_nodes.network_name + + @property + def target_nodes(self): + return self._target_nodes + + @property + def target_network_name(self): + return self._target_nodes.network_name + + @property + def connector(self): + return self._connector + + @property + def connector_params(self): + return self._connector_params + + @property + def iterator(self): + return self._iterator + + @property + def edge_type_properties(self): + return self._edge_type_properties or {} + + @property + def edge_type_id(self): + # TODO: properly implement edge_type + return self._edge_type_properties['edge_type_id'] + + @property + def property_names(self): + if len(self._param_keys) == 0: + return ['nsyns'] + else: + return self._param_keys + + def properties_keys(self): + ordered_keys = sorted(self.property_names) + return str(ordered_keys) + + + def max_connections(self): + return len(self._source_nodes) * len(self._target_nodes) + + def add_properties(self, names, rule, rule_params=None, dtypes=None): + """A a synaptic property + + :param names: list, or single string, of the property + :param rule: function, list or value of property + :param rule_params: when rule is a function, rule_params will be passed into function when called. + :param dtypes: expected property type + """ + self._params.append(self.ParamsRules(names, rule, rule_params, dtypes)) + self._param_keys += names + + def connection_itr(self): + """Returns a generator that will iterate through the source/target pairs (as specified by the iterator function, + and create a connection rule based on the connector. + """ + conr = connector.create(self.connector, **(self.connector_params or {})) + itr = iterator.create(self.iterator, conr, **({})) + return itr(self.source_nodes, self.target_nodes, conr) diff --git a/bmtk-vb/bmtk/builder/connection_map.pyc b/bmtk-vb/bmtk/builder/connection_map.pyc new file mode 100644 index 0000000000000000000000000000000000000000..222a2ff29114574727cd96cba3e312a8ae12400b GIT binary patch literal 6696 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 A(f|Me literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/connector.py b/bmtk-vb/bmtk/builder/connector.py new file mode 100644 index 0000000..0d2cfd6 --- /dev/null +++ b/bmtk-vb/bmtk/builder/connector.py @@ -0,0 +1,35 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from . import functor_cache + + +def create(connector, **params): + return CONNECTOR_CACHE.create(connector, **params) + + +def register(name, func): + CONNECTOR_CACHE.register(name, func) + + +CONNECTOR_CACHE = functor_cache.FunctorCache() +register('passthrough', lambda *_: {}) diff --git a/bmtk-vb/bmtk/builder/connector.pyc b/bmtk-vb/bmtk/builder/connector.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28b99325ede715ba2938ed74718ae4d8025c4249 GIT binary patch literal 798 zcmb_a!AiqG5S?vOwc272g5bf6m!8bQlOV+!s)Eo$)JqY{cC$9EOXJ?0*w=?T~)OOETttUmr=V84B>=h`+FK9xP3hD_Gaf#lEdL@dz-0(;X z8vC?_vrO8f*r&M6N6QkTx9GmHjWiPHfixV|X>gp|2Zh~EryV|Yqkh;5Z`*DQK4r9YdL2{Y{cJkbTqr-COCwX;fS+=* zXRv}ttBnm_2RL+zOap6FFi71?LD^^$YZH8BI-S2otl-gAFj#*G*aOga{dp_s z#~q@)7dr?vdN{I9o3(5oo~cZ=V}slD4|>Z;bqo}G-5%_@wALkNmJJhyEwLQN4Pd$U zXrmXqVs;$S|GOf`_r93ObP&tt32X~sxI9DjjVQ{Adm6ELR|Q?K-%Q9)Chy)xM{fOg YoNF=ZEY2pnd5A)m)z!Ug$8$_Dfanbm8#4Ln(N7}9j4up}fiNsqgkg-t z-g&It3M)+AB_;|^DrHTqx@j^!n@i|c9i9lA)rb4tlryk z^DGs{!53DSm_Ka}-j#M(Ab3W0jY;!^)y6gtwmYWdzIOUeHrm|oyWI)dPxlgSnlQOG z9Jv~AQ7X++rxdQ+VuHN7{9?qQ7!pAsTO2C{M(E?krgMAZgM0Euc zE|C2HMg#TpMB_T9s~ylhI0G6~$$6o<&bCe_gElVJlGvxCbe#2zyeYpkF5%^Nh=rEB zaKcoY^XsrP&KPOP6I=-{41GIa_5A>UBmgKtTS|JEM=F+>&X->grgV&>kd!k zU4qkmw@Sm?h3G*nIs^7Zh*yLXm8eSgOG&3r_h%~u+>cW6Aak8Gvr4VAoJZ1&3%K|S z?u7?#gIt&SSOB#CVk~q>DWV-Y%LL-~aFJz-qTu%s* zQcqd5D0%XhyhV(>&fyvoLEL-K4CgLi2l7}Ncu%@_FQB7<#^Mb>Nxcds@_;~Xj(Sx* zS8tCz;{T@;tqR?2OY3df`)u}byz=<*<_dqpMp;>D(s<5ut1HqD568J64JeoZv;4IU zCVbYHyY$%yjIocm12`FyPe(p|^uEcPmzSNGYaEWndP}12s%&FtVEP@GKdA70?TJH` z>E?}w?kjYjoC68 zM8jBTX2Lglfs7s+rC#tU76Bpep7+oTfo7tRb^84LI;4tH{Xw$7$6^Fg6!6Y+ZI7nB zklI8O5)Yy);Ls1EJ4{N building tables with %d nodes and %d edges" % (self._network.nnodes, self._network.nedges)) + indptr_table = [0] + nsyns_table = [] + src_gids_table = [] + edge_types_table = [] + for trg in self._network.nodes(): + tid = trg[1]['id'] + for edges in self._network.edges([tid], rank=1): + src_gids_table.append(edges[0]) + nsyns_table.append(edges[2]) + edge_types_table.append(edges[3]) + + #if len(src_gids_table) == indptr_table[-1]: + # print "node %d doesn't have any edges" % (tid) + indptr_table.append(len(src_gids_table)) + + + print("> saving tables to %s" % (filename)) + + with h5py.File(filename, 'w') as hf: + hf.create_dataset('edge_ptr', data=indptr_table) + if include_nsyns: + hf.create_dataset('num_syns', data=nsyns_table) + hf.create_dataset('src_gids', data=src_gids_table) + hf.create_dataset('edge_types', data=edge_types_table) + hf.attrs["shape"] = (n_nodes, n_nodes) + + + """ + temp = np.empty([n_edges, 3]) + for i, edge in enumerate(self._network.edges()): + temp[i, 0] = edge[0] + temp[i, 1] = edge[1] + temp[i, 2] = edge[2] + + src_gids_new = np.array([]) + nsyns_new = np.array([]) + indptr_new = [] + counter = 0 + indptr_new.append(counter) + print "Building database" + for i in range(n_nodes): + indicies = np.where(temp[:, 1] == i) + + src_gids_new = np.concatenate([src_gids_new, np.array(temp[indicies[0], 0])]) + nsyns_new = np.concatenate([nsyns_new, np.array(temp[indicies[0], 2])]) + + counter += np.size(indicies[0]) + indptr_new.append(counter) + + print "Writing to h5" + + indptr_new = np.array(indptr_new) + + src_gids_new = src_gids_new.astype(int) + print src_gids_new + exit() + + nsyns_new = nsyns_new.astype(int) + indptr_new = indptr_new.astype(int) + + with h5py.File(filename, 'w') as hf: + hf.create_dataset('indptr', data=indptr_new) + if include_nsyns: + hf.create_dataset('nsyns', data=nsyns_new) + hf.create_dataset('src_gids', data=src_gids_new) + hf.attrs["shape"] = (n_nodes, n_nodes) + """ + + def save(self, cells_fname, cell_models_fname, connections_fname, include_nsyns=True): + """Saves node (cells) and connection information to files. + + :param cells_fname: name of csv file where cell information will be saved. + :param cell_models_fname: name of csv file where cell model information will be saved. + :param connections_fname: Name of h5 file where connection information will be stored. + :param include_nsyns: set to False to build h5 without nsyn table. + """ + #self.save_nodes(cells_fname, cell_models_fname) + self.save_edges(connections_fname, include_nsyns) + + def load(self, nodes, edge_types=None, node_types=None, edges=None, positions=None): + # TODO: check imported ids + + df = pd.read_csv(nodes, sep=' ') + if node_types is not None: + types_df = pd.read_csv(node_types, sep=' ', index_col='node_type_id') + df = pd.merge(left=df, right=types_df, how='left', left_on='node_type_id', right_index=True) + + gids_df = df['node_id'] if 'node_id' in df.columns else df['id'] + #df = df.drop(['id'], axis=1) + + positions_df = None + if positions: + positions_df = df[positions] + df = df.drop(positions, axis=1) + + node_params = df.to_dict(orient='records') + node_tuples = [Node(gids_df[i], gids_df[i], None, array_params=node_params[i]) + for i in xrange(df.shape[0])] + + + if positions: + self._network.positions = position_set.PositionSet() + posr = positioner.create('points', location=positions_df.as_matrix()) + #self._network.positions.add(posr(df.shape[0]), gids_df.tolist()) + self._network.positions.add(positions_df.values, gids_df.tolist()) + + for i in xrange(df.shape[0]): + node_tuples[i]['position'] = np.array(positions_df.loc[i]) + + self._network.positions.finalize() + + self._network._initialize() + self._network._add_nodes(node_tuples) + self._network.nodes_built = True + diff --git a/bmtk-vb/bmtk/builder/formats/hdf5_format.py b/bmtk-vb/bmtk/builder/formats/hdf5_format.py new file mode 100644 index 0000000..a0227ca --- /dev/null +++ b/bmtk-vb/bmtk/builder/formats/hdf5_format.py @@ -0,0 +1,423 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import csv +import json +import math +import h5py +import pandas as pd +from ast import literal_eval + +import bmtk +from .iformats import IFormat +from bmtk.builder.node_pool import NodePool +from time import gmtime, strftime + + +class HDF5Format(IFormat): + """ + Format prior to Blue-brain project collaboration. + Saves as: + nodes (csv) + node_types (csv) + edge_types (csv) + edges (h5) + """ + + CSV_DELIMITER = ' ' + COL_NODE_TYPE_ID = 'node_type_id' + COL_EDGE_TYPE_ID = 'edge_type_id' + COL_TARGET_QUERY = 'target_query' + COL_SOURCE_QUERY = 'source_query' + COL_NODE_ID = 'node_id' + BASE_DIR = 'network' + + @property + def format(self): + return 'msdk.HDF5Format' + + def save(self, directory, **kwargs): + """ saves nodes.csv, node_types.csv, edges.h5, edge_types.csv and .metadata.json. Will overwrite existing files. + + :param directory: Directory where all the files will be saved, creating dir if it doesn't exists. + :param kwargs: + """ + if directory is None: + base_path = os.path.join(self.BASE_DIR, self._network.name) + else: + base_path = directory + + metadata = { + 'version': bmtk.__version__, + 'name': self._network.name, + 'date_created': strftime("%Y-%m-%d %H:%M:%S", gmtime()), + 'file_format': self.format, + 'network_class': self._network.__class__.__name__ + } + + # save node-types. + node_types_path = os.path.join(base_path, 'node_types.csv') + self.save_node_types(node_types_path, **kwargs) + metadata['node_types_file'] = 'node_types.csv' + + # save individual nodes. + if self._network.nodes_built: + # make sure nodes have been built + nodes_path = os.path.join(base_path, 'nodes.csv') + self.save_nodes(nodes_path, **kwargs) + metadata['nodes_file'] = 'nodes.csv' + else: + print('Nodes not built. Unable to save to nodes.csv.') + + # save edge-types. + edge_types_path = os.path.join(base_path, 'edge_types.csv') + self.save_edge_types(edge_types_path, **kwargs) + metadata['edge_types_file'] = 'edge_types.csv' + + # save edges if they have been built + if self._network.edges_built: + edges_path = os.path.join(base_path, 'edges.h5') + self.save_edges(edges_path, **kwargs) + metadata['edges_file'] = 'edges.h5' + else: + print('Edges not built. Unable to save to edges.h5.') + + # save the metadata file + metadata_path = os.path.join(base_path, '.metadata.json') + with open(metadata_path, 'w') as mdfile: + json.dump(metadata, mdfile, indent=2) + + def save_node_types(self, file_name, columns=None, **kwargs): + """Write node_types to csv. + + :param file_name: path to csv file. Will be overwritten if it exists + :param columns: optional columns (not incl. manditory ones). If None then will use all node properties. + :param kwargs: optional + """ + self.__checkpath(file_name, **kwargs) + + # csv should always start with node_type_id + manditory_cols = [self.COL_NODE_TYPE_ID] + + # Determine which columns are in the node_types file and their order + nt_properties = self._network.node_type_properties + opt_cols = [] + if columns is None: + # use all node type properties + opt_cols = list(nt_properties) + else: + # check that columns specified by user exists + for col_name in columns: + if col_name not in nt_properties: + raise Exception('No node property {} found in network, cannot save {}.'.format(col_name, file_name)) + else: + opt_cols.append(col_name) + + # write to csv iteratively + cols = manditory_cols + opt_cols + with open(file_name, 'w') as csvfile: + csvw = csv.writer(csvfile, delimiter=self.CSV_DELIMITER) + csvw.writerow(cols) + for node_set in self._network._node_sets: + props = node_set.properties + row = [] + for cname in cols: + # TODO: determine dtype of parameters so we can use the appropiate none value + row.append(props.get(cname, 'NA')) # get column name or NA if it doesn't exists for this node + csvw.writerow(row) + + def save_nodes(self, file_name, columns=None, **kwargs): + """Write nodes to csv. + + :param file_name: path to csv file. Will be overwritten if it exists + :param columns: optional columns (not incl. manditory ones). If None then will use all node properties. + :param kwargs: optional + """ + self.__checkpath(file_name, **kwargs) + + # csv will start with node_id and node_type_id + manditory_columns = [self.COL_NODE_ID, self.COL_NODE_TYPE_ID] + + # optional columns from either node params or node-type properties + opt_columns = [] + if columns is None: + opt_columns = list(self._network.node_params) + else: + all_cols = self._network.node_params | self._network.node_type_properties + for col_name in columns: + if col_name not in all_cols: + # verify params/properties exist + raise Exception('No edge property {} found in network, cannot save {}.'.format(col_name, file_name)) + else: + opt_columns.append(col_name) + + # write to csv + with open(file_name, 'w') as csvfile: + csvw = csv.writer(csvfile, delimiter=self.CSV_DELIMITER) + csvw.writerow(manditory_columns + opt_columns) + for nid, node in self._network.nodes(): + row = [node.node_id, node.node_type_id] + for cname in opt_columns: + row.append(node.get(cname, 'NA')) + csvw.writerow(row) + + def save_edge_types(self, file_name, columns=None, **kwargs): + """Write edge-types to csv. + + :param file_name: path to csv file. Will be overwritten if it exists + :param columns: optional columns (not incl. manditory ones). If None then will use all node properties. + :param kwargs: optional + """ + self.__checkpath(file_name, **kwargs) + + # start with edge_type_id, target_query and source_query + manditory_cols = [self.COL_EDGE_TYPE_ID, self.COL_TARGET_QUERY, self.COL_SOURCE_QUERY] + + # optional columns + edge_props = self._network.edge_type_properties + opt_cols = [] + if columns is None: + opt_cols = list(edge_props) + else: + for col_name in columns: + if col_name not in edge_props: + raise Exception('No edge property {} found in network, cannot save {}.'.format(col_name, file_name)) + else: + opt_cols.append(col_name) + + # write to csv by iteratively going through all edge-types + with open(file_name, 'w') as csvfile: + csvw = csv.writer(csvfile, delimiter=self.CSV_DELIMITER) + csvw.writerow(manditory_cols + opt_cols) + for et in self._network._edge_sets: + edge = et['edge'] + targetnodes = edge.targets # get source as NodePools to get the source_query strings + sourcenodes = edge.sources # same with target + row_array = [edge.id, targetnodes.filter_str, sourcenodes.filter_str] + edge_params = edge.parameters + for col in opt_cols: + row_array.append(edge_params.get(col, 'NA')) + csvw.writerow(row_array) + + def save_edges(self, file_name, **kwargs): + """Saves edges to edges.h5 + + :param file_name: path to hdf5 file. Will be overwritten if it exists + :param kwargs: optional + """ + self.__checkpath(file_name, **kwargs) + + # Get sources, targets, nsyns and edge_type_id for all edges. + print("> building tables with %d nodes and %d edges" % (self._network.nnodes, self._network.nedges)) + indptr_table = [0] + nsyns_table = [] + src_gids_table = [] + edge_types_table = [] + for trg in self._network.nodes(): + # the targets have to be ordered. + tid = trg[1].node_id + for edges in self._network.edges([tid], rank=1): + src_gids_table.append(edges[0]) + nsyns_table.append(edges[2]) + edge_types_table.append(edges[3]) + + indptr_table.append(len(src_gids_table)) + + # save to h5 + print("> saving tables to %s" % (file_name)) + with h5py.File(file_name, 'w') as hf: + hf.create_dataset('edge_ptr', data=indptr_table) + hf.create_dataset('num_syns', data=nsyns_table) + hf.create_dataset('src_gids', data=src_gids_table) + hf.create_dataset('edge_types', data=edge_types_table) + + def __checkpath(self, file_name, **kwargs): + """Makes sure file_name is a valid file path and can be written.""" + dir_path = os.path.dirname(file_name) + if not os.path.exists(dir_path): + # create file's directory if it doesn't exist + os.makedirs(dir_path) + + def __load_nodes(self, nodes_file, node_types_file): + """Loads nodes and node_types from exists files + + :param nodes_file: path to nodes csv + :param node_types_file: path to node_types csv + """ + def eval(val): + # Helper function that can convert csv to an appropiate type. Helpful for cells of lists (positions, etc) + # TODO: keep column dtypes in metadata and use that for converting each column + if isinstance(val, float) and math.isnan(val): + return None + elif isinstance(val, basestring): + try: + # this will be helpful for turning strings into lists where appropiate "(0, 1, 2)" --> (0, 1, 2) + return literal_eval(val) + except ValueError: + return val + return val + + if nodes_file is None and node_types_file is None: + return None + + elif nodes_file is not None and node_types_file is not None: + # Get the array_params from nodes_file and properties from nodes_types_file, combine them to call + # add_nodes() function and rebuilt the nodes. + nt_df = pd.read_csv(node_types_file, self.CSV_DELIMITER) #, index_col=self.COL_NODE_TYPE_ID) + n_df = pd.read_csv(nodes_file, self.CSV_DELIMITER) + + for _, row in nt_df.iterrows(): + # iterate through the node_types, find all nodes with matching node_type_id and get those node's + # parameters as a dictionary of lists + node_type_props = {l: eval(row[l]) for l in nt_df.columns if eval(row[l]) is not None} + selected_nodes = n_df[n_df[self.COL_NODE_TYPE_ID] == row[self.COL_NODE_TYPE_ID]] + N = len(selected_nodes.axes[0]) + array_params = {l: list(selected_nodes[l]) for l in selected_nodes.columns + if l not in ['node_type_id', 'position']} + + # Special function for position_params + position = None + position_params = None + if 'position' in selected_nodes.columns: + position_params = {'location': [eval(p) for p in selected_nodes['position']]} + position = 'points' + + self._network.add_nodes(N, position=position, position_params=position_params, + array_params=array_params, **node_type_props) + + self._network._build_nodes() + + elif node_types_file is not None: + # nodes_types exists but nodes doesn't. We convert each row (node_type) in the csv to a collection + # of nodes with N=1, no array_params. + nt_df = pd.read_csv(node_types_file, self.CSV_DELIMITER) + for _, row in nt_df.iterrows(): + node_type_props = {l: eval(row[l]) for l in nt_df.columns if eval(row[l]) is not None} + self._network.add_nodes(N=1, **node_type_props) + self._network._build_nodes() + + elif nodes_file is not None: + # nodes exists but node_types doesn't. In this case group together all nodes by node_type_id and add them + # as a single population (with no node_params) + n_df = pd.read_csv(nodes_file, self.CSV_DELIMITER) + for nt_id, df in n_df.groupby(self.COL_NODE_TYPE_ID): + N = len(df.axes[0]) + array_params = {l: list(df[l]) for l in df.columns + if l not in ['node_type_id', 'position']} + + position = None + position_params = None + if 'position' in df.columns: + position_params = {'location': [eval(p) for p in df['position']]} + position = 'points' + + self._network.add_nodes(N, position=position, position_params=position_params, + array_params=array_params, node_type_id=nt_id) + self._network._build_nodes() + + def __load_edge_types(self, edges_file, edge_types_file): + """Loads edges and edge_types + + :param edges_file: path to edges hdf5 + :param edge_types_file: path to edge_types csv + """ + if edge_types_file is None and edges_file is None: + return + + if edge_types_file is not None: + # load in the edge-types. iterate through all the rows of edge_types.csv and call connect() function. + et_pd = pd.read_csv(edge_types_file, self.CSV_DELIMITER) + prop_cols = [label for label in et_pd.columns + if label not in [self.COL_SOURCE_QUERY, self.COL_TARGET_QUERY]] + + for _, row in et_pd.iterrows(): + # the connect function requires a Pool of nodes (like net.nodes()) or a dictionary filter. + source_nodes = NodePool.from_filter(self._network, row[self.COL_SOURCE_QUERY]) + target_nodes = NodePool.from_filter(self._network, row[self.COL_TARGET_QUERY]) + # TODO: evaluate edge-properties and exclude any that are None. + edge_params = {label: row[label] for label in prop_cols} + + # don't try to guess connection rule + self._network.connect(source=source_nodes, target=target_nodes, edge_params=edge_params) + + if edges_file is not None: + # Create edges from h5. + if not self._network.nodes_built: + print('The nodes have not been built. Cannot load edges file.') + return + + # load h5 tables + edges_h5 = h5py.File(edges_file, 'r') + edge_types_ds = edges_h5['edge_types'] + num_syns_ds = edges_h5['num_syns'] + src_gids_ds = edges_h5['src_gids'] + edge_ptr_ds = edges_h5['edge_ptr'] + n_edge_ptr = len(edge_ptr_ds) + + # the network needs edge-types objects while building the edges. If the edge_types_file exists then they + # would have been added in the previous section of code. If edge_types_file is missing we will create + # filler edge types based on the edge_type_id's found in edge_ptr dataset + if edge_types_file is None: + for et_id in set(edges_h5['edge_types'][:]): + self._network.connect(edge_params={self.COL_NODE_TYPE_ID: et_id}) + + # TODO: if edge_types.csv does exists we should check it has matching edge_type_ids with edges.h5/edge_ptr + + def itr_fnc(et): + # Creates a generator that will iteratively go through h5 file and return (source_gid, target_gid, + # nsyn) values for connections with matching edge_type.edge_type_id + edge_type_id = et.id + for ep_indx in xrange(n_edge_ptr - 1): + trg_gid = ep_indx + for syn_indx in xrange(edge_ptr_ds[ep_indx], edge_ptr_ds[ep_indx + 1]): + if edge_types_ds[syn_indx] == edge_type_id: + src_gid = src_gids_ds[syn_indx] + n_syn = num_syns_ds[syn_indx] + yield (src_gid, trg_gid, n_syn) + + for edge in self._network.edge_types(): + # create iterator and directly add edges + itr = itr_fnc(edge) + self._network._add_edges(edge, itr) + + self.edges_built = True + + def load_dir(self, directory, metadata): + def get_path(f): + if f not in metadata: + return None + file_name = metadata[f] + if directory is None or os.path.isabs(file_name): + return file + return os.path.join(directory, file_name) + + nodes_file = get_path('nodes_file') + node_types_file = get_path('node_types_file') + self.__load_nodes(nodes_file, node_types_file) + + edge_types_file = get_path('edge_types_file') + edges_file = get_path('edges_file') + self.__load_edge_types(edges_file, edge_types_file) + + def load(self, nodes_file=None, node_types_file=None, edges_file=None, edge_types_file=None): + self.__load_nodes(nodes_file, node_types_file) diff --git a/bmtk-vb/bmtk/builder/formats/iformats.py b/bmtk-vb/bmtk/builder/formats/iformats.py new file mode 100644 index 0000000..a29261e --- /dev/null +++ b/bmtk-vb/bmtk/builder/formats/iformats.py @@ -0,0 +1,29 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +class IFormat(object): + def __init__(self, network): + self._network = network + + @property + def format(self): + raise NotImplementedError() \ No newline at end of file diff --git a/bmtk-vb/bmtk/builder/functor_cache.py b/bmtk-vb/bmtk/builder/functor_cache.py new file mode 100644 index 0000000..0da8fc1 --- /dev/null +++ b/bmtk-vb/bmtk/builder/functor_cache.py @@ -0,0 +1,55 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from six import string_types +import functools + + +class FunctorCache(object): + def __init__(self): + self.cache = {} + + def create(self, connector, **params): + if params is None: + params = {} + + if isinstance(connector, string_types): + # TODO: don't do this, a user may want to return a string in connection_map params + func = self.cache[connector] + return functools.partial(func, **params) + + elif isinstance(connector, dict): + return lambda *args: connector + + elif isinstance(connector, list): + # for the iterator we want to pass backs lists as they are + return connector + + elif callable(connector): + return functools.partial(connector, **params) + + else: + # should include all numericals, non-callable objects and tuples + return lambda *args: connector + + def register(self, name, func): + self.cache[name] = func diff --git a/bmtk-vb/bmtk/builder/functor_cache.pyc b/bmtk-vb/bmtk/builder/functor_cache.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69e7056b9de3a32f13abcd358e15d02ef168cee3 GIT binary patch literal 1544 zcmb_cO>fgc5S_K%kCa3WLZ~2+IB+o+azRB%RYj@9F$YH+s6f_cy=~pvPPDs0RBBH} zz=8e*ekDHu=8a8yMG8@NJiDHc_hx1t{~Bz)x;gk@sQbnIe9g=6vMBhks3NKr9Vt@S zM^r_06j2eAjpN|B|)QJ8zvIeVS^g!>O)E-kH&~4(FX%lvQEf=(uH(OTg#zi?GSQ zd0sYU$n(dndw?Qk@IWd6zQfD^WO2!?h|V|3-3Q*Qh#Ib!qVFstx{k;lfW%phlZRI^ zg@n%5=rV@qjiO0syDqjL=^IHy6n&-TyA63aM}!HZmC~1u54tfn;~iz_e}Gv#F|*1CgqUj=N?nO> zQJNr1Rr(-e`DvLJFf>f~#=RDpAFv?RSG{OE z8mM8ktp=Og#P(DoE4Moa4msaI@Ju4O!Z!@Vg7-aWY+@e{1UQmEXfP&twt6tpO mY2l!FzWm%y2_{F%KbxNM#mV6HA5jSRy_W6%@-h!qKmG&p#X8Uc literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/id_generator.py b/bmtk-vb/bmtk/builder/id_generator.py new file mode 100644 index 0000000..9d7b798 --- /dev/null +++ b/bmtk-vb/bmtk/builder/id_generator.py @@ -0,0 +1,71 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import threading +import numpy as np +import six + + + +class IDGenerator(object): + """ A simple class for fetching global ids. To get a unqiue global ID class next(), which should be thread-safe. It + Also has a remove_id(gid) in which case next() will never return the gid. The remove_id function is used for cases + when using imported networks and we want to elimnate previously created id. + + TODO: + * Implement a bit array to keep track of already existing gids + * It might be necessary to implement with MPI support? + """ + def __init__(self, init_val=0): + self.__counter = init_val + self.__taken = set() + self.__lock = threading.Lock() + + def remove_id(self, gid): + assert(np.issubdtype(type(gid), np.integer)) + if gid >= self.__counter: + self.__taken.add(gid) + + def next(self): + self.__lock.acquire() + while self.__counter in self.__taken: + self.__taken.remove(self.__counter) + self.__counter += 1 + + nid = self.__counter + self.__counter += 1 + self.__lock.release() + + return nid + + def __contains__(self, gid): + return gid < self.__counter + + def __call__(self, *args, **kwargs): + if len(args) == 1: + N = args[0] + elif 'N' in 'kwargs': + N = args['N'] + + assert(isinstance(N, (int, long))) + return [self.next() for _ in six.moves.range(N)] + diff --git a/bmtk-vb/bmtk/builder/id_generator.pyc b/bmtk-vb/bmtk/builder/id_generator.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5114175396bb674b4b0b4db84e19f59770cb3aab GIT binary patch literal 2477 zcmb_d+io015UtspFRURr3J@Wnqey7OCFY5nq68Bo%Mv7L9E7ldQSVIeZhL2Y*4;f` zTZ&(BUimIQh!22MwU=1r5wCV?s&A*NPF3|Z|K8gC>d)O@b6WhGcz=T@JcKClPpBk1 zTImTLHJ0;DI%-nUAk(5jLPe8a5M2`ea@6KyhrJdc>_7NDiLXID*xNVGcpWRhGS|TT zw|K;yfg#{P!UU8Cy?{BG5agp46)iTw@NH2#RCGjHqx7)1#yEc^L~2(BJDHY7<)scm z4Jxk&CgvmShH6+=eO)SBg!}4QrG_Rdt!j5-YqMNC*jpTQ<}CK^eWGR~n~zi&RdrdY zzEN@HjV?Y7dSLFWgSe^i+bu(-Mmhk^n@M$QGF$Y9wz#LPTL9!b7&XUKGh3F>PmRa+ zSbGOhIA9BS1!V=H2DQtht(>w!)xi`J4I>3HI~y5?d5#PrS3Y8gGjUe=F*u8`&Pm^0jic%IO6qKqgeTyd;7C^qT1wH%^G2qlPWpeMZ!WV{8DU8%t=%d6T& z<8cnKCdQku?1*}d;5YcVq#UtDuIlfrd>pr5*9#7Xlnx#WO= zMdYS&6E7P=-7n(#)JV{xa3@+&I|R9}L*EUEl#BQ~?<=2jQc`wNnAb%ioTYq9X3z!K zLlq}()kz;?u;A)W@jj6Gr8H=0DoJJRkbmTk9pS?#G;sp#xMI%5-uU~ zR!MZqh}VV;8{|LWA-*=ZFPh|T z^XCR18qPNIFW)s;z&WI|$;FQ<9U%2+p~FQB4YAr@whg)~p}pU>>0(|b>zwW1Q5XZ{ zo4uLmTkbbtVjMZO9LzBLRIk7o!FXn-*q_?4@??8wH+4W?C?*?Q-mt~ecf*v?Elx(ALx+s`;lOFF_ z++a>mOqNN6SvIMPy5u1jc9s=Yo@FUBk-o`-qe$OoaTnsHv!eZv%QOWVy@y9U?W}i_ zZhND>(cNgyn~~`fGF?5Ho~N9R^nIS=d{zBplShHLK97W)2Xi_ea>R0Fb-`b^^)K2Y BFUSA@ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/io/__init__.py b/bmtk-vb/bmtk/builder/io/__init__.py new file mode 100644 index 0000000..00a458f --- /dev/null +++ b/bmtk-vb/bmtk/builder/io/__init__.py @@ -0,0 +1,66 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import h5py +from ..network import Network + +def write_edges_to_h5(network, filename, synapse_key=None, verbose=True): + assert(isinstance(network, Network)) + + # The network edges may either be a raw value, dictionary or list + if synapse_key == None: + lookup = lambda x: x + + elif isinstance(synapse_key, str): + lookup = lambda x: x[synapse_key] + + elif isinstance(synapse_key, int): + lookup = lambda x: x[synapse_key] + + else: + raise Exception("Unable to resolve the synapse_key type.") + + # Create the tables for indptr, nsyns and src_gids + if verbose: + print("> building tables with {} nodes and {} edges.".format(network.nnodes, network.nedges)) + indptr_table = [0] + nsyns_table = [] + src_gids_table = [] + for trg in network.nodes(): + # TODO: check the order of the node list + tid = trg[1]['id'] + for edges in network.edges([tid], rank=1): + src_gids_table.append(edges[0]) + nsyns_table.append(lookup(edges[2])) + + if len(src_gids_table) == indptr_table[-1]: + print("node %d doesn't have any edges {}".format(tid)) + indptr_table.append(len(src_gids_table)) + + # Save the tables in h5 format + if verbose: + print("> Saving table to {}.".format(filename)) + with h5py.File(filename, 'w') as hf: + hf.create_dataset('indptr', data=indptr_table) + hf.create_dataset('nsyns', data=nsyns_table) + hf.create_dataset('src_gids', data=src_gids_table, dtype=int32) + hf.attrs["shape"] = (network.nnodes, network.nnodes) diff --git a/bmtk-vb/bmtk/builder/iterator.py b/bmtk-vb/bmtk/builder/iterator.py new file mode 100644 index 0000000..1469cfa --- /dev/null +++ b/bmtk-vb/bmtk/builder/iterator.py @@ -0,0 +1,124 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import itertools +import functools +import types + + +class IteratorCache(object): + def __init__(self): + self.cache = {} + + def create(self, itr_name, itr_type, **params): + if params is None: + params = {} + + if (itr_name, itr_type) in self.cache: + func = self.cache[(itr_name, itr_type)] + return functools.partial(func, **params) + + else: + raise Exception("Couldn't find iterator for ({}, {}).".format(itr_name, itr_type)) + + def register(self, name, itr_type, func): + self.cache[(name, itr_type)] = func + + +def create(iterator, connector, **params): + return ITERATOR_CACHE.create(iterator, type(connector), **params) + + +def register(name, dtype, func): + ITERATOR_CACHE.register(name, dtype, func) + + +######################################################################## +# Pre-defined iterators +######################################################################## +def one_to_all_iterator(source_nodes, target_nodes, connector): + """Calls the connector function with (1 source, all targets), iterated for each source""" + target_list = list(target_nodes) # list of all targets + target_node_ids = [t.node_id for t in target_list] # slight improvement than calling node_id S*T times + for source in source_nodes: + source_node_id = source.node_id + edge_vals = connector(source, target_list) + for i, target in enumerate(target_list): + yield (source_node_id, target_node_ids[i], edge_vals[i]) + + +def all_to_one_iterator(source_nodes, target_nodes, connector): + """Iterate through all the target nodes and return target node + list of all sources""" + source_list = list(source_nodes) + for target in target_nodes: + val = connector(source_list, target) + for i, source in enumerate(source_list): + yield (source.node_id, target.node_id, val[i]) + + +def one_to_one_iterator(source_nodes, target_nodes, connector): + # TODO: may be faster to pull out the node_ids, don't user itertools + for source, target in itertools.product(source_nodes, target_nodes): + val = connector(source, target) + yield (source.node_id, target.node_id, val) + + +def one_to_one_list_iterator(source_nodes, target_nodes, vals): + assert(len(vals) == len(source_nodes)*len(target_nodes)) + for i, (source, target) in enumerate(itertools.product(source_nodes, target_nodes)): + yield (source.node_id, target.node_id, vals[i]) + + +def one_to_all_list_iterator(source_nodes, target_nodes, vals): + assert(len(vals) == len(target_nodes)) + source_ids = [s.node_id for s in list(source_nodes)] + target_ids = [t.node_id for t in list(target_nodes)] + for src_id in source_ids: + for i, trg_id in enumerate(target_ids): + yield (src_id, trg_id, vals[i]) + + +def all_to_one_list_iterator(source_nodes, target_nodes, vals): + assert(len(vals) == len(source_nodes)) + source_ids = [s.node_id for s in list(source_nodes)] + target_ids = [t.node_id for t in list(target_nodes)] + for trg_id in target_ids: + for i, src_id in enumerate(source_ids): + yield (src_id, trg_id, vals[i]) + + +def lambda_iterator(source_nodes, target_nodes, lambda_val): + for source, target in itertools.product(source_nodes, target_nodes): + yield (source.node_id, target.node_id, lambda_val()) + + +ITERATOR_CACHE = IteratorCache() +register('one_to_one', functools.partial, one_to_one_iterator) +register('all_to_one', functools.partial, all_to_one_iterator) +register('one_to_all', functools.partial, one_to_all_iterator) + +register('one_to_one', list, one_to_one_list_iterator) +register('one_to_all', list, one_to_all_list_iterator) +register('all_to_one', list, all_to_one_list_iterator) + + +register('one_to_one', types.FunctionType, lambda_iterator) diff --git a/bmtk-vb/bmtk/builder/iterator.pyc b/bmtk-vb/bmtk/builder/iterator.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b642e6263cedcbb9c4b570ef35d3d031eeec20bb GIT binary patch literal 4588 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 A=>Px# literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/networks/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/builder/networks/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b5d19fcf3f9c795fd8f5bdc6030c3e78f719065 GIT binary patch literal 348 zcmX|7!AiqG5S`hLO-WnvQ1I?87xyfRXi=~SgCNqn3L$kDjL9ZpHdX3B_#yt%UOo8> zp4`yrz&v;}Z{E!AY%+w zpF0Q(s15?qgCe3U1Q{Z{2MbVKaTz{@OSp%MEj_=5IQT|y8lxLu=*sFuw{JBSF>}%N z&AeyCy(Ec;GEye3whCgt=5~9Av@~-G_~F Kz!;n29{vJ~B3Y0C literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/networks/__pycache__/dm_network.cpython-37.pyc b/bmtk-vb/bmtk/builder/networks/__pycache__/dm_network.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f50aa6d905770388470f12896addda71b03c216 GIT binary patch literal 14712 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 A6951J literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/networks/__pycache__/mpi_network.cpython-37.pyc b/bmtk-vb/bmtk/builder/networks/__pycache__/mpi_network.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68baa18462130fdcf0e0c758c64b72ce1d25fa1a GIT binary patch literal 4317 zcma)9+j1kt8SXi1Bv}W`ULTf)ObAJ#t&~jyflA_R5!ix2z$8$KsVT_RSUr}-L-D2!N)S3TmG+sr?0#uxFW59++U>J;>+!~leE3kCi9@s-CaE5N+>b^6m z1eMr2@q%hxy{uiappNefzKu5-cX{Oj6e89Ti*9H#g8fxWkoM!QUoPRPB zr(c6cMC;w(ywx?8cN|CK@gzG|72O(*_j@#B{v)vT(_mv37@P&>XY9z}1~(sgfu-9P zx6!tB+u<(S4sYw8$Ey#jfy-;W{(uD)zQP;$dc4V7_*Oq-HVbM1yQ*pcRATIz#i7i? zmr9)wJc}x4xgpto^Td?Kks;0TdcyuhtYeae7H8PW*zelP$tL4ihzgb{FAS43$-_{! z!v;=z%!(`Xp0Mq2PF9E;I71Cy#@*c)Ja82*!eb7%K=SuC>M zdxw#T^5bZ@n~C1;;V?fft4T88vDhs)$##e1BrH2S<8x637`j8Id1`DK7i-Ha@618= z=)Ux$FU_QH=A)AAl1cN*5Vvw8H;D_EmIM#3OuGO4=B@I2VLl2)l%5I?OAAKS!iaOt zf~;!^;;Cx(Mrj)N@??~T!)UCGq-%&Kt!PozrmBPL2Ph@(nsIUCI_|}u-;SJC<`D8z zQ`~$KcWz%gSFFTu?hCqX*H!l8$g}8P9Ht{4XUgu4hC@|(EfOM$#rz^IJs2J(1I(kh zTqi=9pO52gNqzEi8#wSbUCu?t4A1C1dcEbr`@Oj?&_j}BeGeteQB6~6++`mh&1{m- z#oO3@ypFW4toz2y$?X$II;1rZ&6%6Knj?RhJ}+IY+?rLU9a%xEHuI$SdvN6J?E`l9 z>H$k#&ZrMgRftn(?m_X|R>>^`+!~GYuBYrllI7Rmo1{GplN_3`37CP~huorb10Au2 zs#`6@z{^EH>?b@^b;$HwXjPrar39-=IY&8q+o07O4F(XMxD+W^B~mWJploVB$?T_b zaSVX*k9VNSjjnFm-dU3*}waqS#D|VNG z*LI)H926I&g5sf66E>@HYgWg11*L(~oO0-tNvl5oaMqHoOY4fw+OjQcr>;1bHQC-~ zv(0fer!NfvnJ zp@q}_I_u;WZb2u$lFp?kJ15mESTX79c1^b{x^3(B3TD`ZMS7sFY8uW2WldH=S({TF zw#^8i#rD|rHdfWiLEyw&ADOfD*$qOq&Ez_`<;feg>T~1lHYDw<16CmV+!SAeUmM`n zCd$_Ij@-%{(z$P(Vy^hRbmSIx*gQ4S`=i{_+}KJxa#Oc9-F>HjOr<_8)rG#AxG@(Wdlx zt6WR7+p@}6ul1ouguxg zghoCsZ}A%sVRJt>&ptY!8gEQG`CIlr`;g^pm+K3Qwz1zMA51@TX=L3#K+*Ewhu}ip z4QZUPznEwC2Zhar*QsdJm2kLcbLRzRq=hkFSmbYNd!%KsgX24I({{AC%PwwTH_O^^ zX(ig(8;p9@1g0MXTkSyE+U{V4EP~(dwKnX{Oy3WIzIy{9M~QnL6)2~0V=cPO()(trML|a_@&qRPG(OMNipEr)shZ{Rxpr@v zAX};pxpEP}#VJ>{YaFUA@2=Le^E4XgNl%A0#R8pNrxkAE^gSN0q5od^g- z9OZFHe9huqIZ>XAOu0uRF^qEUMhegIOyN0N`qoPiyL`H!zQmKEQ+k+?qeD)H+zoZ~ z4%$(JwM1y>d+S%Cu4Z=eXvNbMbNXPO_CE++0ypksaRwLE4+o>s>0}&uXy(!3AP#CY zm_ob|Jea*W?jN*b91|JT3C%9t|~1G#5P$mR_^u^9DoHD*cbnO zjc%U#2(cZgMZlAi)0U$iHR_{3P>v+)H@O{ljy;)i9bS!^zMe zkC41Ts(r0@{`1LByyK^n;bAOjlS1wNDCK^keZICf-5vj}ba5&{gTe5Zd61tSk0t{g z77e1b7wZ$3czn|QWa?+hMV$F*T%d}h-my<{`p#nGrB~TUIO<@qA1gP7^X($pHTU 0: + node_id_to_range[node_index, 0] = range_index + for r in trg_ranges: + range_to_edge_id[range_index, :] = r + range_index += 1 + node_id_to_range[node_index, 1] = range_index + + output_grp.create_dataset('range_to_edge_id', data=range_to_edge_id, dtype='uint64') + output_grp.create_dataset('node_id_to_range', data=node_id_to_range, dtype='uint64') + + def _clear(self): + self._nedges = 0 + self._nnodes = 0 + + def edges_iter(self, trg_gids, src_network=None, trg_network=None): + matching_edge_tables = self.__edges_tables + if trg_network is not None: + matching_edge_tables = [et for et in self.__edges_tables if et['target_network'] == trg_network] + + if src_network is not None: + matching_edge_tables = [et for et in matching_edge_tables if et['source_network'] == src_network] + + for trg_gid in trg_gids: + for ets in matching_edge_tables: + syn_table = ets['syn_table'] + if syn_table.has_target(trg_gid): + for src_id, nsyns in syn_table.trg_itr(trg_gid): + if ets['params']: + synapses = [{} for _ in range(nsyns)] + for param_name, param_table in ets['params'].items(): + for i, val in enumerate(param_table[src_id, trg_gid]): + synapses[i][param_name] = val + for syn_prop in synapses: + yield Edge(src_gid=src_id, trg_gid=trg_gid, edge_type_props=ets['edge_types'], + syn_props=syn_prop) + else: + yield Edge(src_gid=src_id, trg_gid=trg_gid, edge_type_props=ets['edge_types'], + syn_props={'nsyns': nsyns}) + + @property + def nnodes(self): + if not self.nodes_built: + return 0 + return self._nnodes + + @property + def nedges(self): + return self._nedges + + class EdgeTable(object): + def __init__(self, connection_map): + # TODO: save column and row lengths + # Create maps between source_node gids and their row in the matrix. + self.__idx2src = [n.node_id for n in connection_map.source_nodes] + self.__src2idx = {node_id: i for i, node_id in enumerate(self.__idx2src)} + + # Create maps betwee target_node gids and their column in the matrix + self.__idx2trg = [n.node_id for n in connection_map.target_nodes] + self.__trg2idx = {node_id: i for i, node_id in enumerate(self.__idx2trg)} + + self._nsyn_table = np.zeros((len(self.__idx2src), len(self.__idx2trg)), dtype=np.uint8) + + def __getitem__(self, item): + # TODO: make sure matrix is column oriented, or swithc trg and srcs. + indexed_pair = (self.__src2idx[item[0]], self.__trg2idx[item[1]]) + return self._nsyn_table[indexed_pair] + + def __setitem__(self, key, value): + assert(len(key) == 2) + indexed_pair = (self.__src2idx[key[0]], self.__trg2idx[key[1]]) + self._nsyn_table[indexed_pair] = value + + def has_target(self, node_id): + return node_id in self.__trg2idx + + @property + def nsyn_table(self): + return self._nsyn_table + + @property + def target_ids(self): + return self.__idx2trg + + @property + def source_ids(self): + return self.__idx2src + + def trg_itr(self, trg_id): + trg_i = self.__trg2idx[trg_id] + for src_j, src_id in enumerate(self.__idx2src): + nsyns = self._nsyn_table[src_j, trg_i] + if nsyns: + yield src_id, nsyns + + class PropertyTable(object): + # TODO: add support for strings + def __init__(self, nvalues): + self._prop_array = np.zeros(nvalues) + # self._prop_table = np.zeros((nvalues, 1)) # TODO: set dtype + self._index = np.zeros((nvalues, 2), dtype=np.uint32) + self._itr_index = 0 + + def itr_vals(self, src_id, trg_id): + indicies = np.where((self._index[:, 0] == src_id) & (self._index[:, 1] == trg_id)) + for val in self._prop_array[indicies]: + yield val + + def __setitem__(self, key, value): + self._index[self._itr_index, 0] = key[0] # src_node_id + self._index[self._itr_index, 1] = key[1] # trg_node_id + self._prop_array[self._itr_index] = value + self._itr_index += 1 + + def __getitem__(self, item): + indicies = np.where((self._index[:, 0] == item[0]) & (self._index[:, 1] == item[1])) + return self._prop_array[indicies] + + +def add_hdf5_attrs(hdf5_handle): + # TODO: move this as a utility function + hdf5_handle['/'].attrs['magic'] = np.uint32(0x0A7A) + hdf5_handle['/'].attrs['version'] = [np.uint32(0), np.uint32(1)] diff --git a/bmtk-vb/bmtk/builder/networks/dm_network.pyc b/bmtk-vb/bmtk/builder/networks/dm_network.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8c42c7e4e05b0ec9b9cb3ddbbf1ffa36d2436c0 GIT binary patch literal 16836 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 W!G4)NfhW-Zxzx1(+<|Yq&;BoER?|TM literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/networks/input_network.py b/bmtk-vb/bmtk/builder/networks/input_network.py new file mode 100644 index 0000000..04c8f88 --- /dev/null +++ b/bmtk-vb/bmtk/builder/networks/input_network.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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/bmtk-vb/bmtk/builder/networks/mpi_network.py b/bmtk-vb/bmtk/builder/networks/mpi_network.py new file mode 100644 index 0000000..aa6a51e --- /dev/null +++ b/bmtk-vb/bmtk/builder/networks/mpi_network.py @@ -0,0 +1,171 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from .dm_network import DenseNetwork +from mpi4py import MPI +from heapq import heappush, heappop +import h5py + +comm = MPI.COMM_WORLD +rank = comm.Get_rank() +nprocs = comm.Get_size() + + +class MPINetwork(DenseNetwork): + def __init__(self, name, **network_props): + super(MPINetwork, self).__init__(name, **network_props or {}) + self._edge_assignment = None + + def _add_edges(self, connection_map, i): + if self._assign_to_rank(i): + super(MPINetwork, self)._add_edges(connection_map, i) + + def save_nodes(self, nodes_file_name, node_types_file_name): + if rank == 0: + super(MPINetwork, self).save_nodes(nodes_file_name, node_types_file_name) + comm.Barrier() + + """ + def save_edges(self, edges_file_name=None, edge_types_file_name=None, output_dir='.', src_network=None, + trg_network=None, force_build=True, force_overwrite=False): + + if rank == 0: + # print rank, len(self.edges_table()) + super(MPINetwork, self).save_edges(edges_file_name, edge_types_file_name, output_dir, src_network, + trg_network, force_build, force_overwrite) + + comm.Barrier() + """ + + def edges_iter(self, trg_gids, src_network=None, trg_network=None): + for trg_gid in trg_gids: + edges = list(super(MPINetwork, self).edges_iter([trg_gid], src_network, trg_network)) + collected_edges = comm.gather(edges, root=0) + if rank == 0: + for edge_list in collected_edges: + for edge in edge_list: + # print 'b' + yield edge + else: + yield None + + comm.Barrier() + + def _save_edges(self, edges_file_name, src_network, trg_network): + target_gids = [n.node_id for n in self._target_networks[trg_network].nodes()] + # TODO: make sure target_gids are sorted + + trg_gids_ds = [] + src_gids_ds = [] + edge_type_id_ds = [] + edge_group_ds = [] + edge_group_index_ds = [] + + eg_collection = {} + eg_ids = 0 + eg_lookup = {} + eg_table = {} + eg_indices = {} + for cm in self.get_connections(): + col_key = cm.properties_keys() + if col_key in eg_collection: + group_id = eg_collection[col_key] + else: + group_id = eg_ids + eg_collection[col_key] = group_id + eg_ids += 1 + eg_lookup[cm.edge_type_id] = group_id + eg_indices[group_id] = 0 + eg_table[group_id] = {k: [] for k in cm.property_names} + + for e in self.edges_iter(target_gids, src_network=src_network, trg_network=trg_network): + if rank == 0: + trg_gids_ds.append(e.target_gid) + src_gids_ds.append(e.source_gid) + edge_type_id_ds.append(e.edge_type_id) + + group_id = eg_lookup[e.edge_type_id] + edge_group_ds.append(group_id) + group_id_index = eg_indices[group_id] + edge_group_index_ds.append(group_id_index) + eg_indices[group_id] += 1 + + for k, v in e.synaptic_properties.items(): + eg_table[group_id][k].append(v) + + if rank == 0: + # Create index from target_gids dataset + index_pointer_ds = [] + cur_gid = 0 + index = 0 + while index < len(trg_gids_ds): + if trg_gids_ds[index] == cur_gid: + index += 1 + else: + cur_gid += 1 + index_pointer_ds.append(index) + index_pointer_ds.append(len(trg_gids_ds)+1) + + + with h5py.File(edges_file_name, 'w') as hf: + hf.create_dataset('edges/target_gid', data=trg_gids_ds, dtype='uint64') + hf['edges/target_gid'].attrs['network'] = trg_network + hf.create_dataset('edges/source_gid', data=src_gids_ds, dtype='uint64') + hf['edges/source_gid'].attrs['network'] = src_network + + hf.create_dataset('edges/edge_group', data=edge_group_ds, dtype='uint16') + hf.create_dataset('edges/edge_group_index', data=edge_group_index_ds, dtype='uint32') + hf.create_dataset('edges/edge_type_id', data=edge_type_id_ds, dtype='uint32') + hf.create_dataset('edges/index_pointer', data=index_pointer_ds, dtype='uint32') + + for gid, group in eg_table.items(): + for col_key, col_ds in group.items(): + ds_loc = 'edges/{}/{}'.format(gid, col_key) + hf.create_dataset(ds_loc, data=col_ds) + + comm.Barrier() + + def _assign_to_rank(self, i): + if self._edge_assignment is None: + self._build_rank_assignments() + + return rank == self._edge_assignment[i] + + def _build_rank_assignments(self): + """Builds the _edge_assignment array. + + Division of connections is decided by the maximum possible edges (i.e. number of source and target nodes). In + the end assignment should balance the connection matrix sizes need by each rank. + """ + rank_heap = [] # A heap of tuples (weight, rank #) + for a in range(nprocs): + heappush(rank_heap, (0, a)) + + # find the rank with the lowest weight, assign that rank to build the i'th connection matrix, update the rank's + # weight and re-add to the heap. + # TODO: sort connection_maps in descending order to get better balance + self._edge_assignment = [] + for cm in self.get_connections(): + r = heappop(rank_heap) + self._edge_assignment.append(r[1]) + heappush(rank_heap, (r[0] + cm.max_connections(), r[1])) + diff --git a/bmtk-vb/bmtk/builder/networks/mpi_network.pyc b/bmtk-vb/bmtk/builder/networks/mpi_network.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdd6730916cba074cf765108b5b7cc4545ce1e3d GIT binary patch literal 5239 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 X^Cp4~@=me7Rq_8Y3(0e_&bsLrW`pZ~ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/node_pool.py b/bmtk-vb/bmtk/builder/node_pool.py new file mode 100644 index 0000000..2e1bb18 --- /dev/null +++ b/bmtk-vb/bmtk/builder/node_pool.py @@ -0,0 +1,106 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from ast import literal_eval +from six import string_types + + +class NodePool(object): + """Stores a collection of nodes based off some query of the network. + + Returns the results of a query of nodes from a network using the nodes() method. Nodes are still generated and + saved by the network, this just stores the query information and provides iterator methods for accessing different + nodes. + + TODO: + * Implement a collection-set algebra including | and not operators. ie. + nodes = net.nodes(type=1) | net.nodes(type=2) + * Implement operators on properties + nodes = net.nodes(val) > 100 + nodes = 100 in net.nodes(val) + """ + + def __init__(self, network, **properties): + self.__network = network + self.__properties = properties + self.__filter_str = None + + def __len__(self): + return sum(1 for _ in self) + + def __iter__(self): + return (n for n in self.__network.nodes_iter() if self.__query_object_properties(n, self.__properties)) + + @property + def network(self): + return self.__network + + @property + def network_name(self): + return self.__network.name + + @property + def filter_str(self): + if self.__filter_str is None: + if len(self.__properties) == 0: + self.__filter_str = '*' + else: + self.__filter_str = '' + for k, v in self.__properties.items(): + conditional = "{}=='{}'".format(k, v) + self.__filter_str += conditional + '&' + if self.__filter_str.endswith('&'): + self.__filter_str = self.__filter_str[0:-1] + + return self.__filter_str + + @classmethod + def from_filter(cls, network, filter_str): + assert(isinstance(filter_str, string_types)) + if len(filter_str) == 0 or filter_str == '*': + return cls(network, position=None) + + properties = {} + for condtional in filter_str.split('&'): + var, val = condtional.split('==') + properties[var] = literal_eval(val) + return cls(network, position=None, **properties) + + def __query_object_properties(self, obj, props): + if props is None: + return True + + for k, v in props.items(): + ov = obj.get(k, None) + if ov is None: + return False + + if hasattr(v, '__call__'): + if not v(ov): + return False + elif isinstance(v, list): + if ov not in v: + return False + elif ov != v: + return False + + return True diff --git a/bmtk-vb/bmtk/builder/node_pool.pyc b/bmtk-vb/bmtk/builder/node_pool.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24483783302113703ce72906f4a7b7f05e727d27 GIT binary patch literal 3921 zcmb_fOK%)S5bl|M_z?$NQ9_6aFyR2}0Jal^5Rn~05|B`kL1SKt61B!Vz4q9%GwbPI zV=MM0LBJ(~I|q)48~=?T0N+==561`&vDZ7jT|L!ZkFTn_T>IT=tq1}GZST_l_)iO0-lx03rDa7I}Jhs4K6g7b&_e1Ii zsZaz;aZ*=Mh?9mg&7n@vd_u^Es0eaW$mWooQf6AE6GQThN+;!z85OU!rYLtA8mzgp zGB!%0cA4j+Uc8WpKsl!i3;PsE355=>FEfc*tmXG zSb2l<`nj`YOvZcsiJhwKVs)@N>Rb58A4h?$`KW8$W|=NT^cK{rOk`b_=h22K;A97N zk|Gt6HrWNWzJFAkw?Sk!+Ui?}ePS7P^@?Xjr>we(n21B7URCa9lp^?ndm!9K;ER%W z+gMRA%{m=ZnZk)!v14HMm!IGH{D!PvjXv%6a?=IZGroMs8Z6`+X1z+HtZ3)`lolV* z3OLzCWshc+m0gH3vmj^DpWa(5bku^(TMVR?YxCH8Z0-8|zcd&f8Uan12^lUk_J2-C z=*~wsqiah`|6;-#G(RGhyu#IH1lREJ_bwyqH4F!VI#5bu63RrPnk$HB#G7QuYO4-T zcMgLl(##932T|cJJi9zLz;V}V3BF@2oh(NVYUEst`UsyjdB;tF8Xh~1`S`3BC?$h^ z7qi8$tf}nc?s@_^Hk0n6t=fz0UAH|L`dOZuYLQsz9<*KP?Mqf`ofVnW`YO(_EDkD| z!e5)!fe+QWjmMtH;H2&#li`j#PXdMST8qwG6@Je6SU$86Y!A<r6? zRc`~gI1UR-j?4Pwrdig9W#-&oue!Me!css}`T9nqY`-f+Oc$SAq9CPpZVIgzhw{|e zj}$LrU%})@CB->VKPdH+Qd{`KnckJg4 zJ`QLt7DqrL^FRVif_j_&M8N@^!2U@Ptih$gX>XZK01psi=beXdsl&QD5Ij9b4}fPw z9X1ubcF-gfKs{)v<(+$i=jc!@KgGGyvUI&B$ zIezAAFl|q$YQ!;Es;LUxI1`(`p<2FU7}Vo-lB08KG5tIowCKyKZ{kash-){DlT`a8 z$5I9J72$ifYIW}U=|Pd37b`%QN7g0t~uti-%}rO_yNPa)^1pVt3znSP6x z_EiijmPt+XSnK~EiD_>aCkg8%>k literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/builder/node_set.py b/bmtk-vb/bmtk/builder/node_set.py new file mode 100644 index 0000000..59c1918 --- /dev/null +++ b/bmtk-vb/bmtk/builder/node_set.py @@ -0,0 +1,71 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import six +from .node import Node + + +class NodeSet(object): + def __init__(self, N, node_params, node_type_properties): + self.__N = N + self.__node_params = node_params + self.__node_type_properties = node_type_properties + + assert('node_type_id' in node_type_properties) + self.__node_type_id = node_type_properties['node_type_id'] + + # Used for determining which node_sets share the same params columns + columns = list(self.__node_params.keys()) + columns.sort() + self.__params_col_hash = hash(str(columns)) + + @property + def N(self): + return self.__N + + @property + def node_type_id(self): + return self.__node_type_id + + @property + def params_keys(self): + return self.__node_params.keys() + + @property + def params_hash(self): + return self.__params_col_hash + + def build(self, nid_generator): + # fetch existing node ids or create new ones + node_ids = self.__node_params.get('node_id', None) + if node_ids is None: + node_ids = [nid for nid in nid_generator(self.N)] + + # turn node_params from dictionary of lists to a list of dictionaries. + ap_flat = [{} for _ in six.moves.range(self.N)] + for key, plist in self.__node_params.items(): + for i, val in enumerate(plist): + ap_flat[i][key] = val + + # create node objects + return [Node(nid, params, self.__node_type_properties, self.__params_col_hash) + for (nid, params) in zip(node_ids, ap_flat)] diff --git a/bmtk-vb/bmtk/builder/node_set.pyc b/bmtk-vb/bmtk/builder/node_set.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9692157a2a2f77f2b45a2230a6ecbec45764cdb5 GIT binary patch literal 2264 zcmcIlQEwYX5T3m|JC5tnswG8%iZ{gdp;$$Ns#1ko2#6;?Tm%(S!s+7MB8~%ghBE(z_I--z1?E&jr1P#CB&g^wTxlid2H zxw2vXMb-e%zQ%ArLRjR(vchGN30bfzQy4)8n!&`2m}8WKed zgWLQD!_^RHM$SZ1D1j<4LshV4)eqX@9*Ga-jhcA4#(+qCi+&>`qc$r5gtJJ_A{i~w zG`=H#nKmZyV@#JD+laa0f$u?9s7dNir#3C}_!b^`ij<{7h0=83SKqSLf11@<>HNq4 zoY9r0^<-*mUs&hggUbu&Xichb>v~fA``1JjxgOHEa9))Id+Jni2vTJ~bNi?Q%I0go z_SP~DqI5VJr}Tfiz|>=2i{yF(FXbx%0d2XgqB-j&b^S83ED%W%a@*GLLc*DaZ~ z_?-$(0I_Nb7t??MNN0dDFY{MplNoBSF8%ilo zz@{SQqNqpMe0G3I7HPVbW&+ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/__init__.py b/bmtk-vb/bmtk/simulator/__init__.py new file mode 100644 index 0000000..04c8f88 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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/bmtk-vb/bmtk/simulator/__init__.pyc b/bmtk-vb/bmtk/simulator/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abfe6a55be59074a9bdf0d8f57271b6afd1f6d01 GIT binary patch literal 139 zcmZSn%*$oj>ll;F00oRd+5w1*S%5?e14FO|NW@PANHCxg#U?;8{m|mnqGJ8Bq{O1c zl8nS${og`k0?S^Qb z`ejLpMTsRDiMjg4MalX}xh2^UqBt|RG$*knzeqnmJ~J<~BtBlRpz;=nO>TZlX-=vg K$h2Z0W&i+7q#*17 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/#biosimulator.py# b/bmtk-vb/bmtk/simulator/bionet/#biosimulator.py# new file mode 100644 index 0000000..773d796 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/#biosimulator.py# @@ -0,0 +1,361 @@ + + + + +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import time +from six import string_types +from neuron import h +from bmtk.simulator.core.simulator import Simulator +from bmtk.simulator.bionet.io_tools import io +from bmtk.simulator.bionet.iclamp import IClamp +from bmtk.simulator.bionet import modules as mods +from bmtk.simulator.core.node_sets import NodeSet +import bmtk.simulator.utils.simulation_reports as reports +import bmtk.simulator.utils.simulation_inputs as inputs +from bmtk.utils.io import spike_trains + + +pc = h.ParallelContext() # object to access MPI methods + + +class BioSimulator(Simulator): + """Includes methods to run and control the simulation""" + + def __init__(self, network, dt, tstop, v_init, celsius, cao0, nsteps_block, start_from_state=False): + self.net = network + + self._start_from_state = start_from_state + self.dt = dt + self.tstop = tstop + + self._v_init = v_init + self._celsius = celsius + self._cao0 = cao0 + self._h = h + + self.tstep = int(round(h.t / h.dt)) + self.tstep_start_block = self.tstep + self.nsteps = int(round(h.tstop/h.dt)) + + # make sure the block size isn't small than the total number of steps + # TODO: should we send a warning that block-step size is being reset? + self._nsteps_block = nsteps_block if self.nsteps > nsteps_block else self.nsteps + + self.__tstep_end_block = 0 + self.__tstep_start_block = 0 + + h.runStopAt = h.tstop + h.steps_per_ms = 1/h.dt + + self._set_init_conditions() # call to save state + h.cvode.cache_efficient(1) + + h.pysim = self # use this objref to be able to call postFadvance from proc advance in advance.hoc + self._iclamps = [] + + self._output_dir = 'output' + self._log_file = 'output/log.txt' + + self._spikes = {} # for keeping track of different spike times, key of cell gids + + self._cell_variables = [] # location of saved cell variables + self._cell_vars_dir = 'output/cellvars' + + self._sim_mods = [] # list of modules.SimulatorMod's + + @property + def dt(self): + return h.dt + + @dt.setter + def dt(self, ms): + h.dt = ms + + @property + def tstop(self): + return h.tstop + + @tstop.setter + def tstop(self, ms): + h.tstop = ms + + @property + def v_init(self): + return self._v_init + + @v_init.setter + def v_init(self, voltage): + self._v_init = voltage + + @property + def celsius(self): + return self._celsius + + @celsius.setter + def celsius(self, c): + self._celsius = c + + @property + def cao0(self): + return self._cao0 + + @cao0.setter + def cao0(self, cao): + self._cao0 = cao + + @property + def n_steps(self): + return int(round(self.tstop/self.dt)) + + @property + def cell_variables(self): + return self._cell_variables + + @property + def cell_var_output(self): + return self._cell_vars_dir + + @property + def spikes_table(self): + return self._spikes + + @property + def nsteps_block(self): + return self._nsteps_block + + @property + def h(self): + return self._h + + @property + def biophysical_gids(self): + return self.net.cell_type_maps('biophysical').keys() + + @property + def local_gids(self): + # return self.net.get + return self.net.local_gids + + def __elapsed_time(self, time_s): + if time_s < 120: + return '{:.4} seconds'.format(time_s) + elif time_s < 7200: + mins, secs = divmod(time_s, 60) + return '{} minutes, {:.4} seconds'.format(mins, secs) + else: + mins, secs = divmod(time_s, 60) + hours, mins = divmod(mins, 60) + return '{} hours, {} minutes and {:.4} seconds'.format(hours, mins, secs) + + def _set_init_conditions(self): + """Set up the initial conditions: either read from the h.SaveState or from config["condidtions"]""" + pc.set_maxstep(10) + h.stdinit() + self.tstep = int(round(h.t/h.dt)) + self.tstep_start_block = self.tstep + + if self._start_from_state: + # io.read_state() + io.log_info('Read the initial state saved at t_sim: {} ms'.format(h.t)) + else: + h.v_init = self.v_init + + h.celsius = self.celsius + h.cao0_ca_ion = self.cao0 + + def set_spikes_recording(self): + for gid, _ in self.net.get_local_cells().items(): + tvec = self.h.Vector() + gidvec = self.h.Vector() + pc.spike_record(gid, tvec, gidvec) + self._spikes[gid] = tvec + + def attach_current_clamp(self, amplitude, delay, duration, gids=None): + # TODO: verify current clamp works with MPI + # TODO: Create appropiate module + if gids is None: + gids = self.gids['biophysical'] + if isinstance(gids, int): + gids = [gids] + elif isinstance(gids, string_types): + gids = [int(gids)] + elif isinstance(gids, NodeSet): + gids = gids.gids() + + + gids = list(set(self.local_gids) & set(gids)) + for gid in gids: + cell = self.net.get_cell_gid(gid) + Ic = IClamp(amplitude, delay, duration) + Ic.attach_current(cell) + self._iclamps.append(Ic) + + def add_mod(self, module): + self._sim_mods.append(module) + + def run(self): + """Run the simulation: + if beginning from a blank state, then will use h.run(), + if continuing from the saved state, then will use h.continuerun() + """ + for mod in self._sim_mods: + mod.initialize(self) + + self.start_time = h.startsw() + s_time = time.time() + pc.timeout(0) + + pc.barrier() # wait for all hosts to get to this point + io.log_info('Running simulation for {:.3f} ms with the time step {:.3f} ms'.format(self.tstop, self.dt)) + io.log_info('Starting timestep: {} at t_sim: {:.3f} ms'.format(self.tstep, h.t)) + io.log_info('Block save every {} steps'.format(self.nsteps_block)) + + if self._start_from_state: + h.continuerun(h.tstop) + else: + h.run(h.tstop) # <- runs simuation: works in parallel + + pc.barrier() + + for mod in self._sim_mods: + mod.finalize(self) + pc.barrier() + + end_time = time.time() + + sim_time = self.__elapsed_time(end_time - s_time) + io.log_info('Simulation completed in {} '.format(sim_time)) + + def report_load_balance(self): + comptime = pc.step_time() + avgcomp = pc.allreduce(comptime, 1)/pc.nhost() + maxcomp = pc.allreduce(comptime, 2) + io.log_info('Maximum compute time is {} seconds.'.format(maxcomp)) + io.log_info('Approximate exchange time is {} seconds.'.format(comptime - maxcomp)) + if maxcomp != 0.0: + io.log_info('Load balance is {}.'.format(avgcomp/maxcomp)) + + def post_fadvance(self): + """ + Runs after every execution of fadvance (see advance.hoc) + Called after every time step to perform computation and save data to memory block or to disk. + The initial condition tstep=0 is not being saved + """ + for mod in self._sim_mods: + mod.step(self, self.tstep) + + self.tstep += 1 + + if (self.tstep % self.nsteps_block == 0) or self.tstep == self.nsteps: + io.log_info(' step:{} t_sim:{:.2f} ms'.format(self.tstep, h.t)) + self.__tstep_end_block = self.tstep + time_step_interval = (self.__tstep_start_block, self.__tstep_end_block) + + for mod in self._sim_mods: + mod.block(self, time_step_interval) + + self.__tstep_start_block = self.tstep # starting point for the next block + + @classmethod + def from_config(cls, config, network, set_recordings=True): + # TODO: convert from json to sonata config if necessary + + sim = cls(network=network, + dt=config.dt, + tstop=config.tstop, + v_init=config.v_init, + celsius=config.celsius, + cao0=config.cao0, + nsteps_block=config.block_step) + + network.io.log_info('Building cells.') + network.build_nodes() + + network.io.log_info('Building recurrent connections') + network.build_recurrent_edges() + + # TODO: Need to create a gid selector + for sim_input in inputs.from_config(config): + node_set = network.get_node_set(sim_input.node_set) + if sim_input.input_type == 'spikes': + spikes = spike_trains.SpikesInput.load(name=sim_input.name, module=sim_input.module, + input_type=sim_input.input_type, params=sim_input.params) + io.log_info('Build virtual cell stimulations for {}'.format(sim_input.name)) + network.add_spike_trains(spikes, node_set) + + elif sim_input.module == 'IClamp': + # TODO: Parse from csv file + amplitude = sim_input.params['amp'] + delay = sim_input.params['delay'] + duration = sim_input.params['duration'] + gids = sim_input.params['node_set'] + sim.attach_current_clamp(amplitude, delay, duration, node_set) + + elif sim_input.module == 'xstim': + sim.add_mod(mods.XStimMod(**sim_input.params)) + + else: + io.log_exception('Can not parse input format {}'.format(sim_input.name)) + + if config.calc_ecp: + for gid, cell in network.cell_type_maps('biophysical').items(): + cell.setup_ecp() + sim.h.cvode.use_fast_imem(1) + + # Parse the "reports" section of the config and load an associated output module for each report + sim_reports = reports.from_config(config) + for report in sim_reports: + if isinstance(report, reports.SpikesReport): + mod = mods.SpikesMod(**report.params) + + elif isinstance(report, reports.SectionReport): + mod = mods.SectionReport(**report.params) + + elif isinstance(report, reports.MembraneReport): + if report.params['sections'] == 'soma': + mod = mods.SomaReport(**report.params) + + else: + mod = mods.MembraneReport(**report.params) + + elif isinstance(report, reports.ECPReport): + assert config.calc_ecp + mod = mods.EcpMod(**report.params) + # Set up the ability for ecp on all relevant cells + # TODO: According to spec we need to allow a different subset other than only biophysical cells + # for gid, cell in network.cell_type_maps('biophysical').items(): + # cell.setup_ecp() + + elif report.module == 'save_synapses': + mod = mods.SaveSynapses(**report.params) + + else: + # TODO: Allow users to register customized modules using pymodules + io.log_warning('Unrecognized module {}, skipping.'.format(report.module)) + continue + + sim.add_mod(mod) + + return sim diff --git a/bmtk-vb/bmtk/simulator/bionet/README.md b/bmtk-vb/bmtk/simulator/bionet/README.md new file mode 100644 index 0000000..5448a66 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/README.md @@ -0,0 +1,4 @@ +## BioNet source code + +For instruction on how to install BioNet please consult the [BioNet tutorial](https://alleninstitute.github.io/bmtk/bionet.html) + diff --git a/bmtk-vb/bmtk/simulator/bionet/__init__.py b/bmtk-vb/bmtk/simulator/bionet/__init__.py new file mode 100644 index 0000000..7c86d80 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from bmtk.simulator.bionet.pyfunction_cache import synapse_model, synaptic_weight, cell_model +from bmtk.simulator.bionet.config import Config +from bmtk.simulator.bionet.bionetwork import BioNetwork +from bmtk.simulator.bionet.biosimulator import BioSimulator +#from bmtk.simulator.bionet.io_tools import io + +#io = NEURONIOUtils() + + diff --git a/bmtk-vb/bmtk/simulator/bionet/__init__.pyc b/bmtk-vb/bmtk/simulator/bionet/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c8584ad897b3da57acfa46a7876ced0d417c7f4 GIT binary patch literal 531 zcmaJ-$xg#C5S?t@L_Xz2{r){4J=ClIP044wyfC+(xU?N}<1nw;a9fL`LC7ws1 zdtmy&`ku$22VjQ4hQt$?r9BF&K53*8-Eiy@?V56JB{%I{YCJtyPzk=3YFQ(W$R?!L zUXljR(*=5Jn}u3>?lh08ol4xc-OAfeyltk;Mwi%j)6}W(il8#M%Ix7Gt5n-a%+|Zb zrV&Vli&E6GASDzoU*Xz<6N2A8#lDnc|EY^VRX&X?u!vo}%m0+e>WYM9DeWI%Z;0kE zR(3XjtIDp#x-_}%MDFVIa14<9ygBEpQOG&{69?pfOD*4Md3R2C>l!)<&S-uC5io~G literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96ea6be83b0a240dae7054b0a267fabbf7b814fe GIT binary patch literal 466 zcmZ`#yH3L}6m{Nd)QSa(g#j@jWl3h#3PIVsR4f*fl{ju=)lMqgq3TEQA^cKSCVqj5 z>qPBTZ29<{>-+Lu&SnXM_4xVnK93Okw&Jv0AkT300-!>bBan2kBVFuD4||Taxx$wL z4hHUtP)0Z!xG!Qk!4u%Y8l~Y6WI(Cc&-Ae=_L>pdR!sC)gEX9y1LND;^a*7`jE||) zhp%n3;agzIlD8{n4z1dOnu1z$*@?omYLzDVp_FEq>60lddD}3P?T?$Tp#}t@1+AH~ zKT@6r9{}H84x>X=6HMQCRAK5u n=33F*W*XrfnvdmjLU_XsA@H$2Ucd-D!Jcj)pSDe0J5$(yPoIdQ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/biocell.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/biocell.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31641c6f39bc647b7986a01fb86c8574d947c155 GIT binary patch literal 8864 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/bionetwork.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/bionetwork.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e43da044d51504d9ba5074e876cb2a4e02d9d0c GIT binary patch literal 7772 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 U#@|M4oWJRep8eNIbEwAr9pox{X#fBK literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/biosimulator.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/biosimulator.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76f720b2fddb146ee8baf81d19daf9873cd92c40 GIT binary patch literal 9756 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 HOxyfF7}SQt literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/cell.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/cell.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0c045237175f849f010e19ae987a3354d3a809b GIT binary patch literal 3004 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/io_tools.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/io_tools.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..928891f2478f4971005cbec41a2c8b6d2c651596 GIT binary patch literal 728 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 WISYHMT+F9*M^bvy+Dc8cwf_Ny;*Bo= literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/nml_reader.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/nml_reader.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bc9bf47eab98544e8e39d1ba579431c8fcb5c3d GIT binary patch literal 5896 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/pointprocesscell.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/pointprocesscell.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c43d61e4f05dee97180a6d588f70e1fbdbf4844d GIT binary patch literal 2543 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 ORXSN3D~*+l`tQF%Q|l7| literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/sonata_adaptors.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/sonata_adaptors.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea939246570bcc68470469659622191a2d8f7b0f GIT binary patch literal 4137 zcmb_f-EJgD74GW4>G}28vDa&~f*rj3Nbu)B?5IU2S{3J=2q_ zc34|IT(B#U5V_$A)|cFI%`@->b<0&=fh)dqrhD8oc;iJ%x2o!#s#9N`sy;u}$NhfC z!t>teAN+QI&$9kaO#QeZo}lP|ppuppnH4Tu5!&GG%$d5OJM}_u>WBWc6}AM8J6SMo zhizefWJyw+(^}+8-@rgD1+85B;8joDl9p^IrK*sYT zS7z`@K8uU^IMyk3o9@r#*YR%oY=6w-cSJr|)1 z+L12P9%xVcO#7f)GGMv|x-C0Q2eK=B*i>8gq4dDCjwd-OmI&a}LoF@(A&MTLDy<9a zr9f@p&_2a@NGq3yQO+S_|NgtuXU>Q(Pt;I zii`7jdZblybTTcTE-O8mUS=_l>*xd^X>p|a&}w{rIKNVD0DXgWvQb&~^{`U>uwHv@ z2PDKhAakP~NO536dwXm;(_GEZ^DIBR8VN-?$M&f@c|zAWRIbB z!+xzz1e8=EtXpBg@~5*z9grsOd46(Xb~j-oU8rzyyG%qAC}K3cu$|?xWX->a0sRn_ zC43Q}?1)`^$M!|f?unil+Sl8)C#>d-M;#MHtXLG8PE?%-=w1|kdKqV=GPr=^S*nU< zoKah(1NoUE+g4;x>g!b9D#sMX3DrVHgPOVm(R2CrdM=M7=v+M%5uTvvH&NBmd>P9_ zCv|Tlx%BYSeKV*wRArNdV;`ncyNWIp!V0TIh<3l}9#zB-5>;DMjYS1~|qA zXKBG9`{w{z1~+kC2e)d~!&)Yx3{4Mt$;wmy1*V`3Az<0gjk9nis0;h4uih!(D}`5z z8@qHPfA)^FtC3?K@mC`Q=36x{ol5S4`-9SDg!XJH*Qt5wR&sx1xo*|p+mP!?uaf&G z%k?X{!G_$Rbm`c~rd=l$&3R|T6h>Q$K7i@Z<03h)vy|z^@UA4!E~hhXyaH~hP3M?Je#dZlWl=U7%DR!u;BrWDg&b0HyS0#(*W+k@Wj6!Jjd?);12=!hXy?HuLsXr;Xe2y!2PiSPke&1dgySTplHga zn<+s$up*Z)>GXq;IFO--z7rMxXpcY?xSYJCD+-K!;gk+2mjd#00zgLa_(q@}K-Ba# z(*0bg%bNh-9XLymT+SC^Tc-)%N1uhAnw&_ak!so)P_ z{%}nOKT4ZX;C)PMcm|!UV)u9t)Sn2G&MFX!`2YJ1DU~+l8op(<%NNM&qrk(IF*wS8FB=wjoih#En xOp%YPcc~(OQDoKpSy;Kdc430|msimr??YCT%_4f+4@J)zj)so|xA_kJ{{jFFW3>PP literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/utils.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/utils.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44a2b8075e91ce62f8a2a745446efe8e22bdb91a GIT binary patch literal 2485 zcma)8%Z?kz6|L$A*?bI#9?{k_ar=?P1cDhGkYL9c2##U|UI~7{kkJ@{Xm-_bdWP)g zR8?z+RQF0o;EiAb`2o%{o2(4O3x7a9A*e+*Ug;OK%BgBHBL!KcIeqKasav=1tvYpk z?rm+g37$VZ{o}!peL{XQoz3Gw`2?^09D)%>rzEOd8acM_My_qW$g`~Qxj&?u9wlZe^B@Eg;QbR`8B#(NS<#YQ(5lub zH1w6TaxvnS1Y^zxy-6_cto)KP=&*50(lJP5gAB-lTe+22`K41j15&!B3+a^}q+j|2 za_Aj+RikQFLDj;j!5xD`*1EBGE#g`G#zCWMSDmU`ZB@N$8}T&DM%lbd%HSF)Ti37w zWxMQLB?F`N%I-C26Sqc<4(kBc&Hq?$ms`f#+Q5E26X@EywYG-;i92yG%r?5q`IlT3 zA`8_K4@ItGmF8JEixibD~zG|uF1JH#`=;h5)>=jPakfp)UFZpJbeB3@_@OCM=Zo(iR1mO~`D z)UHf3?OTgsUwg*8_N{MCV@=1JCYmzs#Bt<|$J$8}?J)GmXk&w$7WkM&PQw01#0@O{ z;-kHZBGjofXqqs;T@{Lp6_sRjVY-U$w!-1{m!y$Y=C>RRv{m++oeq zW5IR8T2CDcegSC%>AZq;fo#2k^nh${5Y}g}eGA4PcUM~%6YzMKo z&u^>O>N!sk_rrG)E%;rw%IKOfZb49a#L-TBX@hwQga z<`4f454as=u-aj-BfG5&bA!QX_QnbF6W^8F$mMF;W7pvGI=k`IMScdm3GCJ;c8mN9 z$gf{P-URaI2FdQi>lN$ur~k{or>?NDj_C=dIHl7Y56A<;-ui-kNxmVcq0w=}-d_Ht z5Q1lFx(LTS%_d+h3{uKbTv&@>glLAX(8 zZ9K*2Eo#YPT$4<@6EXiUhG*l3c&F4%QO6y>Mma}5Y?_Nbu+JA@;;`--w-Jt((XV0$K literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/__pycache__/virtualcell.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/__pycache__/virtualcell.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d51b9d31bec15295bb51683a38d922e9a316c0d GIT binary patch literal 1209 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 Q%p&?+#WU+JxU-f21q&5-WB>pF literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/bionetwork.py b/bmtk-vb/bmtk/simulator/bionet/bionetwork.py new file mode 100644 index 0000000..78ec0ae --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/bionetwork.py @@ -0,0 +1,262 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +from neuron import h + +from bmtk.simulator.core.simulator_network import SimNetwork +from bmtk.simulator.bionet.biocell import BioCell +from bmtk.simulator.bionet.pointprocesscell import PointProcessCell +from bmtk.simulator.bionet.pointsomacell import PointSomaCell +from bmtk.simulator.bionet.virtualcell import VirtualCell +from bmtk.simulator.bionet.morphology import Morphology +from bmtk.simulator.bionet.io_tools import io +from bmtk.simulator.bionet import nrn +from bmtk.simulator.bionet.sonata_adaptors import BioNodeAdaptor, BioEdgeAdaptor + +# TODO: leave this import, it will initialize some of the default functions for building neurons/synapses/weights. +import bmtk.simulator.bionet.default_setters + + +pc = h.ParallelContext() # object to access MPI methods +MPI_size = int(pc.nhost()) +MPI_rank = int(pc.id()) + + +class BioNetwork(SimNetwork): + model_type_col = 'model_type' + + def __init__(self): + # property_schema = property_schema if property_schema is not None else DefaultPropertySchema + super(BioNetwork, self).__init__() + self._io = io + + # TODO: Find a better way that will allow users to register their own class + self._model_type_map = { + 'biophysical': BioCell, + 'point_process': PointProcessCell, + 'point_soma': PointSomaCell, + 'virtual': VirtualCell + } + + self._morphologies_cache = {} + self._morphology_lookup = {} + + self._rank_node_gids = {} + self._rank_node_ids = {} + self._rank_nodes_by_model = {m_type: {} for m_type in self._model_type_map.keys()} + self._remote_node_cache = {} + self._virtual_nodes = {} + + self._cells_built = False + self._connections_initialized = False + + @property + def py_function_caches(self): + return nrn + + def get_node_id(self, population, node_id): + if node_id in self._rank_node_ids[population]: + return self._rank_node_ids[population][node_id].node + + elif node_id in self._remote_node_cache[population]: + return self._remote_node_cache[population][node_id] + + else: + node_pop = self.get_node_population(population) + node = node_pop.get_node(node_id) + self._remote_node_cache[population][node_id] = node + return node + + def cell_type_maps(self, model_type): + return self._rank_nodes_by_model[model_type] + + def get_cell_node_id(self, population, node_id): + return self._rank_node_ids[population].get(node_id, None) + + def get_cell_gid(self, gid): + return self._rank_node_gids[gid] + + def get_local_cells(self): + return self._rank_node_gids + + @property + def local_gids(self): + return list(self._rank_node_gids.keys()) + + def get_virtual_cells(self, population, node_id, spike_trains): + if node_id in self._virtual_nodes[population]: + return self._virtual_nodes[population][node_id] + else: + node = self.get_node_id(population, node_id) + virt_cell = VirtualCell(node, spike_trains) + self._virtual_nodes[population][node_id] = virt_cell + return virt_cell + + def _build_cell(self, bionode): + if bionode.model_type in self._model_type_map: + cell = self._model_type_map[bionode.model_type](bionode, self) + self._rank_nodes_by_model[bionode.model_type][cell.gid] = cell + return cell + else: + self.io.log_exception('Unrecognized model_type {}.'.format(bionode.model_type)) + + def _register_adaptors(self): + super(BioNetwork, self)._register_adaptors() + self._node_adaptors['sonata'] = BioNodeAdaptor + self._edge_adaptors['sonata'] = BioEdgeAdaptor + + def build_nodes(self): + for node_pop in self.node_populations: + self._remote_node_cache[node_pop.name] = {} + node_ids_map = {} + if node_pop.internal_nodes_only: + for node in node_pop[MPI_rank::MPI_size]: + cell = self._build_cell(node) + node_ids_map[node.node_id] = cell + self._rank_node_gids[cell.gid] = cell + + elif node_pop.mixed_nodes: + # node population contains both internal and virtual (external) nodes and the virtual nodes must be + # filtered out + self._virtual_nodes[node_pop.name] = {} + for node in node_pop[MPI_rank::MPI_size]: + if node.model_type == 'virtual': + continue + else: + cell = self._build_cell(node) + node_ids_map[node.node_id] = cell + self._rank_node_gids[cell.gid] = cell + + elif node_pop.virtual_nodes_only: + self._virtual_nodes[node_pop.name] = {} + + self._rank_node_ids[node_pop.name] = node_ids_map + + self.make_morphologies() + self.set_seg_props() # set segment properties by creating Morphologies + self.calc_seg_coords() # use for computing the ECP + self._cells_built = True + + def set_seg_props(self): + """Set morphological properties for biophysically (morphologically) detailed cells""" + for _, morphology in self._morphologies_cache.items(): + morphology.set_seg_props() + + def calc_seg_coords(self): + """Needed for the ECP calculations""" + # TODO: Is there any reason this function can't be moved to make_morphologies() + for morphology_file, morphology in self._morphologies_cache.items(): + morph_seg_coords = morphology.calc_seg_coords() # needed for ECP calculations + + for gid in self._morphology_lookup[morphology_file]: + self.get_cell_gid(gid).calc_seg_coords(morph_seg_coords) + + def make_morphologies(self): + """Creating a Morphology object for each biophysical model""" + # TODO: Let Morphology take care of the cache + # TODO: Let other types have morphologies + # TODO: Get all available morphologies from TypesTable or group + for gid, cell in self._rank_node_gids.items(): + if not isinstance(cell, BioCell): + continue + + morphology_file = cell.morphology_file + if morphology_file in self._morphologies_cache: + # create a single morphology object for each model_group which share that morphology + morph = self._morphologies_cache[morphology_file] + + # associate morphology with a cell + cell.set_morphology(morph) + self._morphology_lookup[morphology_file].append(cell.gid) + + else: + hobj = cell.hobj # get hoc object (hobj) from the first cell with a new morphologys + morph = Morphology(hobj) + + # associate morphology with a cell + cell.set_morphology(morph) + + # create a single morphology object for each model_group which share that morphology + self._morphologies_cache[morphology_file] = morph + self._morphology_lookup[morphology_file] = [cell.gid] + + self.io.barrier() + + def _init_connections(self): + if not self._connections_initialized: + for gid, cell in self._rank_node_gids.items(): + cell.init_connections() + self._connections_initialized = True + + def build_recurrent_edges(self): + recurrent_edge_pops = [ep for ep in self._edge_populations if not ep.virtual_connections] + if not recurrent_edge_pops: + return + + self._init_connections() + for edge_pop in recurrent_edge_pops: + if edge_pop.recurrent_connections: + source_population = edge_pop.source_nodes + for trg_nid, trg_cell in self._rank_node_ids[edge_pop.target_nodes].items(): + for edge in edge_pop.get_target(trg_nid): + src_node = self.get_node_id(source_population, edge.source_node_id) + trg_cell.set_syn_connection(edge, src_node) + + elif edge_pop.mixed_connections: + # When dealing with edges that contain both virtual and recurrent edges we have to check every source + # node to see if is virtual (bc virtual nodes can't be built yet). This conditional can significantly + # slow down build time so we use a special loop that can be ignored. + source_population = edge_pop.source_nodes + for trg_nid, trg_cell in self._rank_node_ids[edge_pop.target_nodes].items(): + for edge in edge_pop.get_target(trg_nid): + src_node = self.get_node_id(source_population, edge.source_node_id) + if src_node.model_type == 'virtual': + continue + trg_cell.set_syn_connection(edge, src_node) + + def find_edges(self, source_nodes=None, target_nodes=None): + selected_edges = self._edge_populations[:] + + if source_nodes is not None: + selected_edges = [edge_pop for edge_pop in selected_edges if edge_pop.source_nodes == source_nodes] + + if target_nodes is not None: + selected_edges = [edge_pop for edge_pop in selected_edges if edge_pop.target_nodes == target_nodes] + + return selected_edges + + def add_spike_trains(self, spike_trains, node_set): + self._init_connections() + + src_nodes = [node_pop for node_pop in self.node_populations if node_pop.name in node_set.population_names()] + for src_node_pop in src_nodes: + source_population = src_node_pop.name + for edge_pop in self.find_edges(source_nodes=source_population): + if edge_pop.virtual_connections: + for trg_nid, trg_cell in self._rank_node_ids[edge_pop.target_nodes].items(): + for edge in edge_pop.get_target(trg_nid): + src_cell = self.get_virtual_cells(source_population, edge.source_node_id, spike_trains) + trg_cell.set_syn_connection(edge, src_cell, src_cell) + + elif edge_pop.mixed_connections: + raise NotImplementedError() diff --git a/bmtk-vb/bmtk/simulator/bionet/bionetwork.pyc b/bmtk-vb/bmtk/simulator/bionet/bionetwork.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca52c8d652afe2136c8e680fd956960898244e93 GIT binary patch literal 9157 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/biosimulator.py b/bmtk-vb/bmtk/simulator/bionet/biosimulator.py new file mode 100644 index 0000000..4082adc --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/biosimulator.py @@ -0,0 +1,363 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import time +from six import string_types +from neuron import h +from bmtk.simulator.core.simulator import Simulator +from bmtk.simulator.bionet.io_tools import io +from bmtk.simulator.bionet.iclamp import IClamp +from bmtk.simulator.bionet import modules as mods +from bmtk.simulator.core.node_sets import NodeSet +import bmtk.simulator.utils.simulation_reports as reports +import bmtk.simulator.utils.simulation_inputs as inputs +from bmtk.utils.io import spike_trains + + +pc = h.ParallelContext() # object to access MPI methods + + +class BioSimulator(Simulator): + """Includes methods to run and control the simulation""" + # Add extra argument "optocell" + def __init__(self, network, dt, tstop, v_init, celsius, cao0, optocell, nsteps_block, start_from_state=False): + self.net = network + + self._start_from_state = start_from_state + self.dt = dt + self.tstop = tstop + + self._v_init = v_init + self._celsius = celsius + self._cao0 = cao0 + self._optocell = optocell # Set instance var to optocell + self._h = h + + self.tstep = int(round(h.t / h.dt)) + self.tstep_start_block = self.tstep + self.nsteps = int(round(h.tstop/h.dt)) + + # make sure the block size isn't small than the total number of steps + # TODO: should we send a warning that block-step size is being reset? + self._nsteps_block = nsteps_block if self.nsteps > nsteps_block else self.nsteps + + self.__tstep_end_block = 0 + self.__tstep_start_block = 0 + + h.runStopAt = h.tstop + h.steps_per_ms = 1/h.dt + + self._set_init_conditions() # call to save state + h.cvode.cache_efficient(1) + + h.pysim = self # use this objref to be able to call postFadvance from proc advance in advance.hoc + self._iclamps = [] + + self._output_dir = 'output' + self._log_file = 'output/log.txt' + + self._spikes = {} # for keeping track of different spike times, key of cell gids + + self._cell_variables = [] # location of saved cell variables + self._cell_vars_dir = 'output/cellvars' + + self._sim_mods = [] # list of modules.SimulatorMod's + + @property + def optocell(self): + return self._optocell + + @property + def dt(self): + return h.dt + + @dt.setter + def dt(self, ms): + h.dt = ms + + @property + def tstop(self): + return h.tstop + + @tstop.setter + def tstop(self, ms): + h.tstop = ms + + @property + def v_init(self): + return self._v_init + + @v_init.setter + def v_init(self, voltage): + self._v_init = voltage + + @property + def celsius(self): + return self._celsius + + @celsius.setter + def celsius(self, c): + self._celsius = c + + @property + def cao0(self): + return self._cao0 + + @cao0.setter + def cao0(self, cao): + self._cao0 = cao + + @property + def n_steps(self): + return int(round(self.tstop/self.dt)) + + @property + def cell_variables(self): + return self._cell_variables + + @property + def cell_var_output(self): + return self._cell_vars_dir + + @property + def spikes_table(self): + return self._spikes + + @property + def nsteps_block(self): + return self._nsteps_block + + @property + def h(self): + return self._h + + @property + def biophysical_gids(self): + return self.net.cell_type_maps('biophysical').keys() + + @property + def local_gids(self): + # return self.net.get + return self.net.local_gids + + def __elapsed_time(self, time_s): + if time_s < 120: + return '{:.4} seconds'.format(time_s) + elif time_s < 7200: + mins, secs = divmod(time_s, 60) + return '{} minutes, {:.4} seconds'.format(mins, secs) + else: + mins, secs = divmod(time_s, 60) + hours, mins = divmod(mins, 60) + return '{} hours, {} minutes and {:.4} seconds'.format(hours, mins, secs) + + def _set_init_conditions(self): + """Set up the initial conditions: either read from the h.SaveState or from config["condidtions"]""" + pc.set_maxstep(10) + h.stdinit() + self.tstep = int(round(h.t/h.dt)) + self.tstep_start_block = self.tstep + + if self._start_from_state: + # io.read_state() + io.log_info('Read the initial state saved at t_sim: {} ms'.format(h.t)) + else: + h.v_init = self.v_init + + h.celsius = self.celsius + # h.cao0_ca_ion = self.cao0 + + def set_spikes_recording(self): + for gid, _ in self.net.get_local_cells().items(): + tvec = self.h.Vector() + gidvec = self.h.Vector() + pc.spike_record(gid, tvec, gidvec) + self._spikes[gid] = tvec + + def attach_current_clamp(self, amplitude, delay, duration, gids=None): + # TODO: verify current clamp works with MPI + # TODO: Create appropiate module + if gids is None: + gids = self.gids['biophysical'] + if isinstance(gids, int): + gids = [gids] + elif isinstance(gids, string_types): + gids = [int(gids)] + elif isinstance(gids, NodeSet): + gids = gids.gids() + + + gids = list(set(self.local_gids) & set(gids)) + for gid in gids: + cell = self.net.get_cell_gid(gid) + Ic = IClamp(amplitude, delay, duration) + Ic.attach_current(cell) + self._iclamps.append(Ic) + + def add_mod(self, module): + self._sim_mods.append(module) + + def run(self): + """Run the simulation: + if beginning from a blank state, then will use h.run(), + if continuing from the saved state, then will use h.continuerun() + """ + for mod in self._sim_mods: + mod.initialize(self) + + self.start_time = h.startsw() + s_time = time.time() + pc.timeout(0) + + pc.barrier() # wait for all hosts to get to this point + io.log_info('Running simulation for {:.3f} ms with the time step {:.3f} ms'.format(self.tstop, self.dt)) + io.log_info('Starting timestep: {} at t_sim: {:.3f} ms'.format(self.tstep, h.t)) + io.log_info('Block save every {} steps'.format(self.nsteps_block)) + + if self._start_from_state: + h.continuerun(h.tstop) + else: + h.run(h.tstop) # <- runs simuation: works in parallel + + pc.barrier() + + for mod in self._sim_mods: + mod.finalize(self) + pc.barrier() + + end_time = time.time() + + sim_time = self.__elapsed_time(end_time - s_time) + io.log_info('Simulation completed in {} '.format(sim_time)) + + def report_load_balance(self): + comptime = pc.step_time() + avgcomp = pc.allreduce(comptime, 1)/pc.nhost() + maxcomp = pc.allreduce(comptime, 2) + io.log_info('Maximum compute time is {} seconds.'.format(maxcomp)) + io.log_info('Approximate exchange time is {} seconds.'.format(comptime - maxcomp)) + if maxcomp != 0.0: + io.log_info('Load balance is {}.'.format(avgcomp/maxcomp)) + + def post_fadvance(self): + """ + Runs after every execution of fadvance (see advance.hoc) + Called after every time step to perform computation and save data to memory block or to disk. + The initial condition tstep=0 is not being saved + """ + for mod in self._sim_mods: + mod.step(self, self.tstep) + + self.tstep += 1 + + if (self.tstep % self.nsteps_block == 0) or self.tstep == self.nsteps: + io.log_info(' step:{} t_sim:{:.2f} ms'.format(self.tstep, h.t)) + self.__tstep_end_block = self.tstep + time_step_interval = (self.__tstep_start_block, self.__tstep_end_block) + + for mod in self._sim_mods: + mod.block(self, time_step_interval) + + self.__tstep_start_block = self.tstep # starting point for the next block + + @classmethod + def from_config(cls, config, network, set_recordings=True): + # TODO: convert from json to sonata config if necessary + + sim = cls(network=network, + dt=config.dt, + tstop=config.tstop, + v_init=config.v_init, + celsius=config.celsius, + cao0=config.cao0, + optocell=config.optocell, + nsteps_block=config.block_step) + + network.io.log_info('Building cells.') + network.build_nodes() + + network.io.log_info('Building recurrent connections') + network.build_recurrent_edges() + + # TODO: Need to create a gid selector + for sim_input in inputs.from_config(config): + node_set = network.get_node_set(sim_input.node_set) + if sim_input.input_type == 'spikes': + spikes = spike_trains.SpikesInput.load(name=sim_input.name, module=sim_input.module, + input_type=sim_input.input_type, params=sim_input.params) + io.log_info('Build virtual cell stimulations for {}'.format(sim_input.name)) + network.add_spike_trains(spikes, node_set) + + elif sim_input.module == 'IClamp': + # TODO: Parse from csv file + amplitude = sim_input.params['amp'] + delay = sim_input.params['delay'] + duration = sim_input.params['duration'] + gids = sim_input.params['node_set'] + sim.attach_current_clamp(amplitude, delay, duration, node_set) + + elif sim_input.module == 'xstim': + sim.add_mod(mods.XStimMod(**sim_input.params)) + + else: + io.log_exception('Can not parse input format {}'.format(sim_input.name)) + + if config.calc_ecp: + for gid, cell in network.cell_type_maps('biophysical').items(): + cell.setup_ecp() + sim.h.cvode.use_fast_imem(1) + + # Parse the "reports" section of the config and load an associated output module for each report + sim_reports = reports.from_config(config) + for report in sim_reports: + if isinstance(report, reports.SpikesReport): + mod = mods.SpikesMod(**report.params) + + elif isinstance(report, reports.SectionReport): + mod = mods.SectionReport(**report.params) + + elif isinstance(report, reports.MembraneReport): + if report.params['sections'] == 'soma': + mod = mods.SomaReport(**report.params) + + else: + mod = mods.MembraneReport(**report.params) + + elif isinstance(report, reports.ECPReport): + assert config.calc_ecp + mod = mods.EcpMod(**report.params) + # Set up the ability for ecp on all relevant cells + # TODO: According to spec we need to allow a different subset other than only biophysical cells + # for gid, cell in network.cell_type_maps('biophysical').items(): + # cell.setup_ecp() + + elif report.module == 'save_synapses': + mod = mods.SaveSynapses(**report.params) + + else: + # TODO: Allow users to register customized modules using pymodules + io.log_warning('Unrecognized module {}, skipping.'.format(report.module)) + continue + + sim.add_mod(mod) + + return sim diff --git a/bmtk-vb/bmtk/simulator/bionet/biosimulator.pyc b/bmtk-vb/bmtk/simulator/bionet/biosimulator.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b47d1d9f950f9b25b5fed3974a8c66e169cc2cf GIT binary patch literal 12602 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/cell.py b/bmtk-vb/bmtk/simulator/bionet/cell.py new file mode 100644 index 0000000..190836a --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/cell.py @@ -0,0 +1,104 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from neuron import h +import numpy as np + + +pc = h.ParallelContext() # object to access MPI methods +MPI_RANK = int(pc.id()) + + +class Cell(object): + """A abstract base class for any cell object. + + A base class for implementation of a cell-type objects like biophysical cells, LIF cells, etc. Do not instantiate + a Cell object directly. Cell classes act as wrapper around HOC cell object with extra functionality for setting + positions, synapses, and other parameters depending on the desired cell class. + """ + def __init__(self, node): + self._node = node + self._gid = node.gid + self._node_id = node.node_id + self._props = node + self._netcons = [] # list of NEURON network connection object attached to this cell + + self._pos_soma = [] + self.set_soma_position() + + # register the cell + pc.set_gid2node(self.gid, MPI_RANK) + + # Load the NEURON HOC object + self._hobj = node.load_cell() + + @property + def node(self): + return self._node + + @property + def hobj(self): + return self._hobj + + @property + def gid(self): + return self._gid + + @property + def node_id(self): + return self._node_id + + @property + def group_id(self): + return self._node.group_id + + @property + def network_name(self): + return self._node.network + + @property + def netcons(self): + return self._netcons + + @property + def soma_position(self): + return self._pos_soma + + def set_soma_position(self): + positions = self._node.position + if positions is not None: + self._pos_soma = positions.reshape(3, 1) + + def init_connections(self): + self.rand_streams = [] + self.prng = np.random.RandomState(self.gid) # generate random stream based on gid + + def scale_weights(self, factor): + for nc in self.netcons: + weight = nc.weight[0] + nc.weight[0] = weight*factor + + def get_connection_info(self): + return [] + + def set_syn_connections(self, edge_prop, src_node, stim=None): + raise NotImplementedError diff --git a/bmtk-vb/bmtk/simulator/bionet/cell.pyc b/bmtk-vb/bmtk/simulator/bionet/cell.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd5671d434e770667aaa774b01cd62b6f88282ca GIT binary patch literal 3946 zcmcInTW=dx5I*Z$V%KRxTXB1U6_?_N5+j5_psFHGsVEn>+Hes8wA$<*+w1I&Jtw4= z+85eaeiJ{69{}Gs>vcjUD%37^&Uofr=FIKPOxk~L4L<&T@260$K5hJdiebJ1i16pA zM5)wKhmLYcw^Y(nhi#=DwdklvN*#8U?kU|>iw%;Guys>K*w9x~M@1d=7?Vo(xymdo|e?a-${(LT=XN1tI%2IaJYr_~Dj{E(mf_MMFWhRkS6@j*5U^ zRF84>CFLEAcBlx9sP^38$k;Lnt$P$0?S@HUj62OsH^`Q52m`qJ(L#rIcQA1A*?V$Z zoEC{rb!Gz_=b4*N-9V1LYnO#yoi%O}A8Yq0&Wrid#9@%g7IV%0>dx&N)i&I9Z{=>5 zTQ|;(4Kf=CR?Dq{OINJ8xlvp~pk%pQ&5Fp{xYQvq?r9kmg@*iPev(D*=XdwdtKpu; zcJAu?P}!ZHWFdtPlGrXqXQQo+vzgpkF2E_lkJC!ZKtGakMe^=>mkZdf=!BO8J5;D!j$XMO)@O^@gsRVGnlp zYGi}tVw2JA*uGi}gm#p@pdPi9Lmope@(`j!A=nw+lhH*$%eeQmJkplp%;HF5-Y2qw z39k6SH|uRAdL)W6FU)ElUW18nIp=fZn>-Ed3!q?1Avh~`v{bwZ?Qpe-d)}AZdBNBB z?)cu`~_Th7|q3{KIR zg2CB!S_Reo+q=v=(BYhU0`0>zhv8QB*$F8I$678%s#WSYd3d#ST(#sj$?cuGmDbT` zWto@fOw5O*qnVc8OX>q!o2q%^9md9KL-wXD(z`9QbJ`ggwAO4g;+|F0#4PRmmb&?V znnx#alrK5J_oF=YeUH7AY-Zc2OS_a##v>1TFH$W%RweI60>+v5G69)-ZxCE1c!%Is zf@=V0>I(Ke{hvoCypCaB1L*a7PJd^+^UrU;+wWksM||=U@tHm;^NcySYNfN2v{-uN zVA*orLl2rHI@!-Ni+?vfIoHNQoLPA$apWl1Np} literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/config.py b/bmtk-vb/bmtk/simulator/bionet/config.py new file mode 100644 index 0000000..e81a0ba --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/config.py @@ -0,0 +1,88 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json + +from neuron import h + +#import bmtk.simulator.utils.config as msdk_config +#from bmtk.utils.sonata.config import SonataConfig +#from bmtk.simulator.core.config import ConfigDict +from bmtk.simulator.utils.config import ConfigDict +from bmtk.simulator.utils.sim_validator import SimConfigValidator +from bmtk.simulator.bionet.io_tools import io +from . import nrn + +pc = h.ParallelContext() # object to access MPI methods +MPI_Rank = int(pc.id()) + + +# load the configuration schema +schema_folder = os.path.join(os.path.dirname(__file__), 'schemas') +config_schema_file = os.path.join(schema_folder, 'config_schema.json') + +# json schemas (but not real jsonschema) to describe the various input file formats +file_formats = [ + ("csv:nodes_internal", os.path.join(schema_folder, 'csv_nodes_internal.json')), + ("csv:node_types_internal", os.path.join(schema_folder, 'csv_node_types_internal.json')), + ("csv:edge_types", os.path.join(schema_folder, 'csv_edge_types.json')), + ("csv:nodes_external", os.path.join(schema_folder, 'csv_nodes_external.json')), + ("csv:node_types_external", os.path.join(schema_folder, 'csv_node_types_external.json')) +] + +# Create a config and input file validator for Bionet +with open(config_schema_file, 'r') as f: + config_schema = json.load(f) +bionet_validator = SimConfigValidator(config_schema, file_formats=file_formats) + + +class Config(ConfigDict): + @property + def cao0(self): + return self.conditions['cao0'] + + @property + def optocell(self): + return self.run['optocell'] + + @staticmethod + def get_validator(): + return bionet_validator + + def create_output_dir(self): + io.setup_output_dir(self.output_dir, self.log_file) + + def load_nrn_modules(self): + nrn.load_neuron_modules(self.mechanisms_dir, self.templates_dir) + + def build_env(self): + if MPI_Rank == 0: + self.create_output_dir() + self.copy_to_output() + + if io.mpi_size > 1: + # A friendly message requested by fb + io.log_info('Running NEURON with mpi ({} cores).'.format(io.mpi_size)) + + pc.barrier() + self.load_nrn_modules() diff --git a/bmtk-vb/bmtk/simulator/bionet/config.pyc b/bmtk-vb/bmtk/simulator/bionet/config.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2394fbef7be2cce5bfc6e96ed3efc0abded5fb3 GIT binary patch literal 2843 zcmcImTW=dh6h5|9Wh%)>Xb3m5N%<@lbJUx|e1@_XPf6%Fo^ z-WH>F=vz4Uo9}ds^bTb(VH3FO?NYWux=DRlw@JUl14Q5V?r{_jJ6i~g`5T{u4q`)& zhZ9fEgM3nzeRKTOq|Rfjzz$8};g?Bnvcy%jUkk8iD#H>y5P~Y}(zWn!(-U1J*0O+9 zgvK7~4s2C=*q%Kst4!P2lup-WlG{z@wrBBm*l!uH$(Goi53laqy|VAMHNW|eXLzWy zW9=F2+IY_;bdzfOryEt9z_+S4VX!x-Hd&L+5`o&ap-tc9I__6>kvQvS0C=~jpyb02 z{|t})6a(@MMWm67Br!&ohol=EWez!}P09kMdnlAph!$lHA!aE$>@*=I<>>LlvY+6w z3mASy4olT;A9i@*z%?+YN%i4nrXZ-9aR#yU7;{*i_dEC}8SF=(_Fq_C+x@enq)yyP zQtaD0-9IYasUNH%d?j>9Y0#sAyD{raay|c=jUt`e5%z<4*R9q1o&F^4nfo)!Kg0?Qq68&Xcd6 ze-TGXc^Yw4#lvYeJda&9j;+r+=7fmYnxD07i4N`42uNH%)U^kdo9cB_;Wuu05HPp59 zy+m59b%`@+q1{Q9MVwyZ8L=d~hv{V*3H3i)80+vc9?JolQA>@b##}fTcJ5;;4y~+o zR=yI&*S;hoER);8z>Tsy?UCJ)Gr8^h78!A`aqr#6giV(%D&pWdKJ_($zEyTjg_Njr z>(c>j6iKNrfqIU{o##5oUF2|aMXVNb8!1^jvB6}~24lQqh#v1Ps<6!O0VrCI&p1?a z!^P4={o4SpyF5dP{`Hz5?JZ&nA`fva<)-Y*lnYj*cwdZPEh4@|BX;^54WsD($hWCa k_$|b~kD;yF!E$q1$v0@L)yBJOy|JbI_kLqlt*h1WA7)dGasU7T literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/__init__.py b/bmtk-vb/bmtk/simulator/bionet/default_setters/__init__.py new file mode 100644 index 0000000..4ad0b56 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/default_setters/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from . import cell_models +from . import synapse_models +from . import synaptic_weights \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/__init__.pyc b/bmtk-vb/bmtk/simulator/bionet/default_setters/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5107fd010afee03dd116999117df8e741898b307 GIT binary patch literal 310 zcmY+8!D_=W42I>TgVC~)+g>53`O-_Fl=WQLp=FmaFixVjrgl;yWup(#r|k<=PBzAj z_1WJK5=w7R)5UfDS;5aql*jA_iPB6W1}&yacs=%`NEGfZz*f{9#=2& Kr?^O_GIa+9H$s&F literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c648a69a1ff45897973f50c809d63c28ec97dd3 GIT binary patch literal 274 zcmYL@&1%Ci41nz<8>K&Ex4lAhsn3Hl)*f~l>@s!<1>+=YYi1|KQa1V!dyu_St~>P= zdYar)D$vI#AzNBbCPTsT__J8O+zRm*i%TRd&zv}A)S{M7cv(mpJz-CZ=NV{y=M1$$m9}kwR1Is>InoG3@uAkXv845Ex9hZf literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/cell_models.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/cell_models.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4ac9ff31488bcf169e17942abda9716c7e049d4 GIT binary patch literal 11141 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 Z5t`Ns>jmqyp<0)$d?itTfc2yQe**&hD60Sf literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/synapse_models.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/default_setters/__pycache__/synapse_models.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08a45bb54b248626ac214715c4e78f55ae61c770 GIT binary patch literal 4594 zcmd5=NpBoQ6z+X`7LS*#&f09j6To9RGxcPGUQN=uy3XU0YXu^}W~c&83l%f`aS) z2Or*kd_Ym2&_nMsp?rW6K7wEhQ|n4YT~JlhwYt{O7j&uXb>oJ@3})U}7EETb9PSp+ zF`MP@EBDj|8+w5irJjdgVk1&7KrgdVsTZM-v2m%F*aX{yIYv;om+g~vW$63a0jZBd zKgcGfJ_h{|J1q5au5*i->mXkG|I^oKp5CR{egts7;l}&x^YNT}3j&@I*Q@*@#TiY}?%}8CH}@l z>HwQbrDM4|G+k;2G(&1;M}KH_tRI2lH)EeG9UZGOH~#oYa4+JH=hXc$a+*sHUu&Jh z(wy${!a~8pb*|+JuR)cr)WUO)yz_{QkcN4^0qO2*=+>JzXo`kc_cu6m7+&QCH6A)% zy%{V!(F%7$UW@!@kRFXHjE_9A%%fg=m~;?4YKven`TXUZ*FK%6CA7SdJI%#aOqcWp zvN~pp4KDbd*oeILtdyrJxme%uTS5cAaqcpnL8?nO;e zUBs?=RAqe0Yu6(;g8Y?Lu@7~94jFi$ zA~`ezB~n&Z;*aDCF@(6HyIPkkx?~M;MR$!ZJAVIH*#QXMwR6rE#CRz$T%QdyBTI|{ zAqd4TA!L_|Do;yTfMjsg6;tv+kVg{5Y?C2IP!XGQy|G0%pNR?TL}#6dy`<6TDoQ9F zo83DaS>HdHjB(#UZ)x10q>TIgi;bJEIEXTh8YMv_6yqdl!YvUwgocYK zA;G0+lCWAqwdAj?32N^nlcDS!T6RQPg_)APCJ1dw4Y_kY8B{6AIfbrO%2!VpbPKL?ow z$(T6_(C+#28eaJYO1K}Q%NGEa@P#BeLw&Xe@4vF93}6XN!`5^r3)2y-`4ypg9aH!y zX;bP$16ZiN4@<_B>S%doQ&Lz?8dVPFWlPP5mX}%{T0v?BX>!F3zmk3TO(-@!GPzjy z+&L0HiKRH#T8#WUXO31!PIKqHhb zw+Ndq4cYVyF&EDTY$RpgHXPqsf2Wx5-q@CwvJOQ}M>D;1pgXutJ-0rM2rcq7 zR#T^O`yW4zm-$f1@#J$N5Nzh@yRzif{Cce3jJ5Vm#*JE0V>yfRN$f(rL*iW$?~&N` zH+LGNe?S@Xp#w+(DD~+BnsF9nOWSZ3^kp@+P9LzfUNW%b?7^90el4n`(}3-Uh|bhf zz3FAgB32uY6P>A*X000#cHh&>Ca~~SwAf1@hQdX-7>W`WEMyGVdl8e4^WK*4i7iC& zc+XFCJ~o7sPu!275WRmQ8HaNfEt2AwQWQV4ZHns;5hG^OKI2*UT(MGqfHIAKG$<&b z-8~s+G5QyjZWJ`IapWu1_CMLOO2>QYs;p z>;4Bg@|S$&w10sUW2f0&al)48;n#fg&3x$hBLw5k&HFFsU4(vHryV{R6Idz%!4by= zN~|UYE=fW#;*@*eQ9`-T1JEAt@DQ{wi12v8yF9v~7=h>W@PP-fT94yP6rbF}|7aRx zGi034b|TM~dbZe9sm@nbO6@5Os|l=+uv7@521w5kd_blqQrFe-jVedL_qmz$5gSiv`;h zdZvWd_94V(Yn`V@0BW?pHjmm%Z8}x@!UDG$AS{7v^k11to3_IlQ)w$A11mc1Zj*>0}D%J_?7#kBECsFk7OY8`j~Ats4z#hY+Rq?tV0hV0Ia z?}8;gE(*=sgP;L&-xSg@8f9#wRL-i4i&ua^4J<+&;*i*NhEItc!p=FgZg;g6awl^w z;y~Ja=C$ph(OvV5n&yGtXghnBvUDk=&CT_LwDocyKEUDH)PV`#mMgv~#Ji_(%|QuC MkA@!1{s0f~UoT7ECIA2c literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/cell_models.py b/bmtk-vb/bmtk/simulator/bionet/default_setters/cell_models.py new file mode 100644 index 0000000..16d5bfb --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/default_setters/cell_models.py @@ -0,0 +1,460 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import numpy as np +from neuron import h +try: + from sklearn.decomposition import PCA +except Exception as e: + pass + +from bmtk.simulator.bionet.pyfunction_cache import add_cell_model, add_cell_processor +from bmtk.simulator.bionet.io_tools import io +from bmtk.simulator.bionet.nml_reader import NMLTree + +""" +Functions for loading NEURON cell objects. + +Functions will be loaded by bionetwork and called when a new cell object is created. These are for standard models +loaded with Cell-Types json files or their NeuroML equivelent, but may be overridden by the users. +""" + + +def IntFire1(cell, template_name, dynamics_params): + """Loads a point integrate and fire neuron""" + hobj = h.IntFire1() + hobj.tau = dynamics_params['tau']*1000.0 # Convert from seconds to ms. + hobj.refrac = dynamics_params['refrac']*1000.0 # Convert from seconds to ms. + return hobj + + +def Biophys1(cell, template_name, dynamic_params): + """Loads a biophysical NEURON hoc object using Cell-Types database objects.""" + morphology_file = cell.morphology_file + hobj = h.Biophys1(str(morphology_file)) + #fix_axon(hobj) + #set_params_peri(hobj, dynamic_params) + return hobj + + +def Biophys1_nml(json_file): + # TODO: look at examples to see how to convert .nml files + raise NotImplementedError() + + +def Biophys1_dict(cell): + """ Set parameters for cells from the Allen Cell Types database Prior to setting parameters will replace the + axon with the stub + """ + morphology_file = cell['morphology'] + hobj = h.Biophys1(str(morphology_file)) + return hobj + + +def aibs_perisomatic(hobj, cell, dynamics_params): + if dynamics_params is not None: + fix_axon_peri(hobj) + set_params_peri(hobj, dynamics_params) + + return hobj + + +def fix_axon_peri(hobj): + """Replace reconstructed axon with a stub + + :param hobj: hoc object + """ + for sec in hobj.axon: + h.delete_section(sec=sec) + + h.execute('create axon[2]', hobj) + + for sec in hobj.axon: + sec.L = 30 + sec.diam = 1 + hobj.axonal.append(sec=sec) + hobj.all.append(sec=sec) # need to remove this comment + + hobj.axon[0].connect(hobj.soma[0], 0.5, 0) + hobj.axon[1].connect(hobj.axon[0], 1, 0) + + h.define_shape() + + +def set_params_peri(hobj, biophys_params): + """Set biophysical parameters for the cell + + :param hobj: NEURON's cell object + :param biophys_params: name of json file with biophys params for cell's model which determine spiking behavior + :return: + """ + passive = biophys_params['passive'][0] + conditions = biophys_params['conditions'][0] + genome = biophys_params['genome'] + + # Set passive properties + cm_dict = dict([(c['section'], c['cm']) for c in passive['cm']]) + for sec in hobj.all: + sec.Ra = passive['ra'] + sec.cm = cm_dict[sec.name().split(".")[1][:4]] + sec.insert('pas') + + for seg in sec: + seg.pas.e = passive["e_pas"] + + # Insert channels and set parameters + for p in genome: + sections = [s for s in hobj.all if s.name().split(".")[1][:4] == p["section"]] + + for sec in sections: + if p["mechanism"] != "": + sec.insert(p["mechanism"]) + setattr(sec, p["name"], p["value"]) + + # Set reversal potentials + for erev in conditions['erev']: + sections = [s for s in hobj.all if s.name().split(".")[1][:4] == erev["section"]] + for sec in sections: + sec.ena = erev["ena"] + sec.ek = erev["ek"] + + +def aibs_allactive(hobj, cell, dynamics_params): + fix_axon_allactive(hobj) + set_params_allactive(hobj, dynamics_params) + return hobj + + +def fix_axon_allactive(hobj): + """Replace reconstructed axon with a stub + + Parameters + ---------- + hobj: instance of a Biophysical template + NEURON's cell object + """ + # find the start and end diameter of the original axon, this is different from the perisomatic cell model + # where diameter == 1. + axon_diams = [hobj.axon[0].diam, hobj.axon[0].diam] + for sec in hobj.all: + section_name = sec.name().split(".")[1][:4] + if section_name == 'axon': + axon_diams[1] = sec.diam + + for sec in hobj.axon: + h.delete_section(sec=sec) + + h.execute('create axon[2]', hobj) + for index, sec in enumerate(hobj.axon): + sec.L = 30 + sec.diam = axon_diams[index] # 1 + + hobj.axonal.append(sec=sec) + hobj.all.append(sec=sec) # need to remove this comment + + hobj.axon[0].connect(hobj.soma[0], 1.0, 0) + hobj.axon[1].connect(hobj.axon[0], 1.0, 0) + + h.define_shape() + + +def set_params_allactive(hobj, params_dict): + # params_dict = json.load(open(params_file_name, 'r')) + passive = params_dict['passive'][0] + genome = params_dict['genome'] + conditions = params_dict['conditions'][0] + + section_map = {} + for sec in hobj.all: + section_name = sec.name().split(".")[1][:4] + if section_name in section_map: + section_map[section_name].append(sec) + else: + section_map[section_name] = [sec] + + for sec in hobj.all: + sec.insert('pas') + # sec.insert('extracellular') + + if 'e_pas' in passive: + e_pas_val = passive['e_pas'] + for sec in hobj.all: + for seg in sec: + seg.pas.e = e_pas_val + + if 'ra' in passive: + ra_val = passive['ra'] + for sec in hobj.all: + sec.Ra = ra_val + + if 'cm' in passive: + # print('Setting cm') + for cm_dict in passive['cm']: + cm = cm_dict['cm'] + for sec in section_map.get(cm_dict['section'], []): + sec.cm = cm + + for genome_dict in genome: + g_section = genome_dict['section'] + if genome_dict['section'] == 'glob': + io.log_warning("There is a section called glob, probably old json file") + continue + + g_value = float(genome_dict['value']) + g_name = genome_dict['name'] + g_mechanism = genome_dict.get("mechanism", "") + for sec in section_map.get(g_section, []): + if g_mechanism != "": + sec.insert(g_mechanism) + setattr(sec, g_name, g_value) + + for erev in conditions['erev']: + erev_section = erev['section'] + erev_ena = erev['ena'] + erev_ek = erev['ek'] + + if erev_section in section_map: + for sec in section_map.get(erev_section, []): + if h.ismembrane('k_ion', sec=sec) == 1: + setattr(sec, 'ek', erev_ek) + if h.ismembrane('na_ion', sec=sec) == 1: + setattr(sec, 'ena', erev_ena) + else: + io.log_warning("Can't set erev for {}, section array doesn't exist".format(erev_section)) + + +def aibs_perisomatic_directed(hobj, cell, dynamics_params): + fix_axon_perisomatic_directed(hobj) + set_params_peri(hobj, dynamics_params) + return hobj + + +def aibs_allactive_directed(hobj, cell, dynamics_params): + fix_axon_allactive_directed(hobj) + set_params_allactive(hobj, dynamics_params) + return hobj + + +def fix_axon_perisomatic_directed(hobj): + # io.log_info('Fixing Axon like perisomatic') + all_sec_names = [] + for sec in hobj.all: + all_sec_names.append(sec.name().split(".")[1][:4]) + + if 'axon' not in all_sec_names: + io.log_exception('There is no axonal recostruction in swc file.') + else: + beg1, end1, beg2, end2 = get_axon_direction(hobj) + + for sec in hobj.axon: + h.delete_section(sec=sec) + h.execute('create axon[2]', hobj) + + h.pt3dadd(beg1[0], beg1[1], beg1[2], 1, sec=hobj.axon[0]) + h.pt3dadd(end1[0], end1[1], end1[2], 1, sec=hobj.axon[0]) + hobj.all.append(sec=hobj.axon[0]) + h.pt3dadd(beg2[0], beg2[1], beg2[2], 1, sec=hobj.axon[1]) + h.pt3dadd(end2[0], end2[1], end2[2], 1, sec=hobj.axon[1]) + hobj.all.append(sec=hobj.axon[1]) + + hobj.axon[0].connect(hobj.soma[0], 0.5, 0) + hobj.axon[1].connect(hobj.axon[0], 1.0, 0) + + hobj.axon[0].L = 30.0 + hobj.axon[1].L = 30.0 + + h.define_shape() + + for sec in hobj.axon: + # print "sec.L:", sec.L + if np.abs(30-sec.L) > 0.0001: + io.log_exception('Axon stub L is less than 30') + + +def fix_axon_allactive_directed(hobj): + all_sec_names = [] + for sec in hobj.all: + all_sec_names.append(sec.name().split(".")[1][:4]) + + if 'axon' not in all_sec_names: + io.log_exception('There is no axonal recostruction in swc file.') + else: + beg1, end1, beg2, end2 = get_axon_direction(hobj) + + axon_diams = [hobj.axon[0].diam, hobj.axon[0].diam] + for sec in hobj.all: + section_name = sec.name().split(".")[1][:4] + if section_name == 'axon': + axon_diams[1] = sec.diam + + for sec in hobj.axon: + h.delete_section(sec=sec) + h.execute('create axon[2]', hobj) + hobj.axon[0].connect(hobj.soma[0], 1.0, 0) + hobj.axon[1].connect(hobj.axon[0], 1.0, 0) + + h.pt3dadd(beg1[0], beg1[1], beg1[2], axon_diams[0], sec=hobj.axon[0]) + h.pt3dadd(end1[0], end1[1], end1[2], axon_diams[0], sec=hobj.axon[0]) + hobj.all.append(sec=hobj.axon[0]) + h.pt3dadd(beg2[0], beg2[1], beg2[2], axon_diams[1], sec=hobj.axon[1]) + h.pt3dadd(end2[0], end2[1], end2[2], axon_diams[1], sec=hobj.axon[1]) + hobj.all.append(sec=hobj.axon[1]) + + hobj.axon[0].L = 30.0 + hobj.axon[1].L = 30.0 + + h.define_shape() + + for sec in hobj.axon: + # io.log_info('sec.L: {}'.format(sec.L)) + if np.abs(30 - sec.L) > 0.0001: + io.log_exception('Axon stub L is less than 30') + + +def get_axon_direction(hobj): + for sec in hobj.somatic: + n3d = int(h.n3d()) # get number of n3d points in each section + soma_end = np.asarray([h.x3d(n3d - 1), h.y3d(n3d - 1), h.z3d(n3d - 1)]) + mid_point = int(n3d / 2) + soma_mid = np.asarray([h.x3d(mid_point), h.y3d(mid_point), h.z3d(mid_point)]) + + for sec in hobj.all: + section_name = sec.name().split(".")[1][:4] + if section_name == 'axon': + n3d = int(h.n3d()) # get number of n3d points in each section + axon_p3d = np.zeros((n3d, 3)) # to hold locations of 3D morphology for the current section + for i in range(n3d): + axon_p3d[i, 0] = h.x3d(i) + axon_p3d[i, 1] = h.y3d(i) # shift coordinates such to place soma at the origin. + axon_p3d[i, 2] = h.z3d(i) + + # Add soma coordinates to the list + p3d = np.concatenate(([soma_mid], axon_p3d), axis=0) + + # Compute PCA + pca = PCA(n_components=3) + pca.fit(p3d) + unit_v = pca.components_[0] + + mag_v = np.sqrt(pow(unit_v[0], 2) + pow(unit_v[1], 2) + pow(unit_v[2], 2)) + unit_v[0] = unit_v[0] / mag_v + unit_v[1] = unit_v[1] / mag_v + unit_v[2] = unit_v[2] / mag_v + + # Find the direction + axon_end = axon_p3d[-1] - soma_mid + if np.dot(unit_v, axon_end) < 0: + unit_v *= -1 + + axon_seg_coor = np.zeros((4, 3)) + # unit_v = np.asarray([0,1,0]) + axon_seg_coor[0] = soma_end + axon_seg_coor[1] = soma_end + (unit_v * 30.) + axon_seg_coor[2] = soma_end + (unit_v * 30.) + axon_seg_coor[3] = soma_end + (unit_v * 60.) + + return axon_seg_coor + + +nml_files = {} # For caching neuroml file trees +def NMLLoad(cell, template_name, dynamic_params): + """Convert a NEUROML file to a NEURON hoc cell object. + + Current limitations: + * Ignores nml morphology section. You must pass in a swc file + * Only for biophysically detailed cell biophysical components. All properties must be assigned to a segment group. + + :param cell: + :param template_name: + :param dynamic_params: + :return: + """ + # Last I checked there is no built in way to load a NML file directly into NEURON through the API, instead we have + # to manually parse the nml file and build the NEUROM cell object section-by-section. + morphology_file = cell.morphology_file + hobj = h.Biophys1(str(morphology_file)) + # Depending on if the axon is cut before or after setting cell channels and mechanism can create drastically + # different results. Currently NML files doesn't produce the same results if you use model_processing directives. + # TODO: Find a way to specify model_processing directive with NML file + fix_axon_peri(hobj) + + # Load the hoc template containing a swc initialized NEURON cell + if template_name in nml_files: + nml_params = nml_files[template_name] + else: + # Parse the NML parameters file xml tree and cache. + biophys_dirs = cell.network.get_component('biophysical_neuron_models_dir') + nml_path = os.path.join(biophys_dirs, template_name) + nml_params = NMLTree(nml_path) + nml_files[template_name] = nml_params + + # Iterate through the NML tree by section and use the properties to manually create cell mechanisms + section_lists = [(sec, sec.name().split(".")[1][:4]) for sec in hobj.all] + for sec, sec_name in section_lists: + for prop_name, prop_obj in nml_params[sec_name].items(): + if prop_obj.element_tag() == 'resistivity': + sec.Ra = prop_obj.value + + elif prop_obj.element_tag() == 'specificCapacitance': + sec.cm = prop_obj.value + + elif prop_obj.element_tag() == 'channelDensity' and prop_obj.ion_channel == 'pas': + sec.insert('pas') + setattr(sec, 'g_pas', prop_obj.cond_density) + for seg in sec: + seg.pas.e = prop_obj.erev + + elif prop_obj.element_tag() == 'channelDensity' or prop_obj.element_tag() == 'channelDensityNernst': + sec.insert(prop_obj.ion_channel) + setattr(sec, prop_obj.id, prop_obj.cond_density) + if prop_obj.ion == 'na' and prop_obj: + sec.ena = prop_obj.erev + elif prop_obj.ion == 'k': + sec.ek = prop_obj.erev + + elif prop_obj.element_tag() == 'concentrationModel': + sec.insert(prop_obj.id) + setattr(sec, 'gamma_' + prop_obj.type, prop_obj.gamma) + setattr(sec, 'decay_' + prop_obj.type, prop_obj.decay) + + return hobj + +def set_extracellular(hobj, cell, dynamics_params): + for sec in hobj.all: + sec.insert('extracellular') + + return hobj + + +add_cell_model(NMLLoad, directive='nml', model_type='biophysical') +add_cell_model(Biophys1, directive='ctdb:Biophys1', model_type='biophysical', overwrite=False) +add_cell_model(Biophys1, directive='ctdb:Biophys1.hoc', model_type='biophysical', overwrite=False) +add_cell_model(IntFire1, directive='nrn:IntFire1', model_type='point_process', overwrite=False) + + +add_cell_processor(aibs_perisomatic, overwrite=False) +add_cell_processor(aibs_allactive, overwrite=False) +add_cell_processor(aibs_perisomatic_directed, overwrite=False) +add_cell_processor(aibs_allactive_directed, overwrite=False) +add_cell_processor(set_extracellular, overwrite=False) +add_cell_processor(set_extracellular, 'extracellular', overwrite=False) \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/cell_models.pyc b/bmtk-vb/bmtk/simulator/bionet/default_setters/cell_models.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4bc74f0e26a604bc8d583fc9029f383207d75ce0 GIT binary patch literal 13764 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/synapse_models.py b/bmtk-vb/bmtk/simulator/bionet/default_setters/synapse_models.py new file mode 100644 index 0000000..013cbcb --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/default_setters/synapse_models.py @@ -0,0 +1,206 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from neuron import h + +from bmtk.simulator.bionet.pyfunction_cache import add_synapse_model +from bmtk.simulator.bionet.nrn import * + + +def exp2syn(syn_params, xs, secs): + """Create a list of exp2syn synapses + + :param syn_params: parameters of a synapse + :param xs: list of normalized distances along the section + :param secs: target sections + :return: list of NEURON synpase objects + """ + syns = [] + for x, sec in zip(xs, secs): + syn = h.Exp2Syn(x, sec=sec) + syn.e = syn_params['erev'] + syn.tau1 = syn_params['tau1'] + syn.tau2 = syn_params['tau2'] + syns.append(syn) + return syns + + +def Exp2Syn(syn_params, sec_x, sec_id): + """Create a list of exp2syn synapses + + :param syn_params: parameters of a synapse + :param sec_x: normalized distance along the section + :param sec_id: target section + :return: NEURON synapse object + """ + syn = h.Exp2Syn(sec_x, sec=sec_id) + syn.e = syn_params['erev'] + syn.tau1 = syn_params['tau1'] + syn.tau2 = syn_params['tau2'] + return syn + + + +@synapse_model +def stp1syn(syn_params, xs, secs): + syns = [] + for x, sec in zip(xs, secs): + syn = h.stp1syn(x, sec=sec) + + syn.e = syn_params["erev"] + syn.p0 = 0.5 + syn.tau_r = 200 + syn.tau_1 = 5 + syns.append(syn) + + return syns + + +@synapse_model +def stp2syn(syn_params, x, sec): + syn = h.stp2syn(x, sec=sec) + syn.e = syn_params["erev"] + syn.p0 = syn_params["p0"] + syn.tau_r0 = syn_params["tau_r0"] + syn.tau_FDR = syn_params["tau_FDR"] + syn.tau_1 = syn_params["tau_1"] + return syn + + +@synapse_model +def stp3syn(syn_params, xs, secs): + syns = [] + for x, sec in zip(xs, secs): + syn = h.stp3syn(x, sec=sec) # temporary + syn.e = syn_params["erev"] + syn.p0 = 0.6 + syn.tau_r0 = 200 + syn.tau_FDR = 2000 + syn.tau_D = 500 + syn.tau_1 = 5 + syns.append(syn) + + return syns + + +@synapse_model +def stp4syn(syn_params, xs, secs): + syns = [] + for x, sec in zip(xs, secs): + syn = h.stp4syn(x, sec=sec) + syn.e = syn_params["erev"] + syn.p0 = 0.6 + syn.tau_r = 200 + syn.tau_1 = 5 + syns.append(syn) + + return syns + + +@synapse_model +def stp5syn(syn_params, x, sec): # temporary + syn = h.stp5syn(x, sec=sec) + syn.e = syn_params["erev"] + syn.tau_1 = syn_params["tau_1"] + syn.tau_r0 = syn_params["tau_r0"] + syn.tau_FDR = syn_params["tau_FDR"] + syn.a_FDR = syn_params["a_FDR"] + syn.a_D = syn_params["a_D"] + syn.a_i = syn_params["a_i"] + syn.a_f = syn_params["a_f"] + syn.pbtilde = syn_params["pbtilde"] + return syn + + +def stp5isyn(syn_params, xs, secs): # temporary + syns = [] + for x, sec in zip(xs, secs): + syn = h.stp5isyn(x, sec=sec) + syn.e = syn_params["erev"] + syn.tau_1 = syn_params["tau_1"] + syn.tau_r0 = syn_params["tau_r0"] + syn.tau_FDR = syn_params["tau_FDR"] + syn.a_FDR = syn_params["a_FDR"] + syn.a_D = syn_params["a_D"] + syn.a_i = syn_params["a_i"] + syn.a_f = syn_params["a_f"] + syn.pbtilde = syn_params["pbtilde"] + syns.append(syn) + + return syns + + +@synapse_model +def tmgsyn(syn_params, xs, secs): + syns = [] + for x, sec in zip(xs, secs): + syn = h.tmgsyn(x, sec=sec) + syn.e = syn_params["erev"] + syn.tau_1 = syn_params["tau_1"] + syn.tau_rec = syn_params["tau_rec"] + syn.tau_facil = syn_params["tau_facil"] + syn.U = syn_params["U"] + syn.u0 = syn_params["u0"] + syns.append(syn) + + return syns + + +@synapse_model +def expsyn(syn_params, x, sec): + """Create a list of expsyn synapses + + :param syn_params: parameters of a synapse (dict) + :param x: normalized distance along the section (float) + :param sec: target section (hoc object) + :return: synapse objects + """ + syn = h.ExpSyn(x, sec=sec) + syn.e = syn_params['erev'] + syn.tau = syn_params["tau1"] + return syn + + +@synapse_model +def exp1syn(syn_params, xs, secs): + syns = [] + for x, sec in zip(xs, secs): + syn = h.exp1syn(x, sec=sec) + syn.e = syn_params['erev'] + syn.tau = syn_params["tau_1"] + syns.append(syn) + return syns + + +@synapse_model +def exp1isyn(syn_params, xs, secs): + syns = [] + for x, sec in zip(xs, secs): + syn = h.exp1isyn(x, sec=sec) + syn.e = syn_params['erev'] + syn.tau = syn_params["tau_1"] + syns.append(syn) + return syns + + +add_synapse_model(Exp2Syn, 'exp2syn', overwrite=False) +add_synapse_model(Exp2Syn, overwrite=False) diff --git a/bmtk-vb/bmtk/simulator/bionet/default_setters/synapse_models.pyc b/bmtk-vb/bmtk/simulator/bionet/default_setters/synapse_models.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e3c6a0e50ed60e6cbed34c40671ba95fed702b4 GIT binary patch literal 6133 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 am|{VXyFhr#%x>i38GU*Bx@tC)jr2DP4QKiQ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/iclamp.py b/bmtk-vb/bmtk/simulator/bionet/iclamp.py new file mode 100644 index 0000000..fe823ef --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/iclamp.py @@ -0,0 +1,38 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from neuron import h + + +class IClamp(object): + def __init__(self, amplitude, delay, duration): + self._iclamp_amp = amplitude + self._iclamp_del = delay + self._iclamp_dur = duration + self._stim = None + + def attach_current(self, cell): + self._stim = h.IClamp(cell.hobj.soma[0](0.5)) + self._stim.delay = self._iclamp_del + self._stim.dur = self._iclamp_dur + self._stim.amp = self._iclamp_amp + return self._stim diff --git a/bmtk-vb/bmtk/simulator/bionet/iclamp.pyc b/bmtk-vb/bmtk/simulator/bionet/iclamp.pyc new file mode 100644 index 0000000000000000000000000000000000000000..312c9761db5d02d51d856fd7b2ce06b2c1127106 GIT binary patch literal 1058 zcmb_bO>fjN5FIDmZFdo0r>by*Q^bLsDE{_^M4vY)RWn*=dzc&xJ!#dqg_Wz!kn;9T2RV*}UfSsx?zuP|l**I*S+{)Z^%o}0ydHI*XD ib$%TBP6gKg3DWwKb;+F;p`Y%n>7=g@r5NeWqx1^J#o6Kj literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d.hoc new file mode 100644 index 0000000..3bcad33 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d.hoc @@ -0,0 +1,12 @@ +{xopen("import3d/import3d_sec.hoc")} +{xopen("import3d/read_swc.hoc")} +{xopen("import3d/read_nlcda.hoc")} +{xopen("import3d/read_nlcda3.hoc")} +{xopen("import3d/read_nts.hoc")} +{xopen("import3d/read_morphml.hoc")} +{xopen("import3d/import3d_gui.hoc")} +objref tobj, nil +proc makeimport3dtool() { + tobj = new Import3d_GUI(nil) + tobj = nil +} diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d/import3d_gui.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d/import3d_gui.hoc new file mode 100644 index 0000000..81d6935 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d/import3d_gui.hoc @@ -0,0 +1,1174 @@ +{load_file("celbild.hoc")} +{load_file("stdlib.hoc")} + +begintemplate Import3d_GUI +public swc, diam_glyph, box, plrot, readfile, redraw, name +public origin, rotmatold, raworigin, g, rotraw, instantiate +external hoc_sf_ +objref swc, g, box, this, rotmat, m2, origin, tobj, rotmatold +objref raworigin, rotsav, viewsec, rawsel, deck +objref file, nil, problist, types, editbox +strdef tstr, tstr1, typelabel_, filename +public quiet + + +proc init() { + + quiet = 0 + + if (numarg() == 2) if ($2 == 0) { + swc = $o1 + return + } + if ($o1 == nil) { + file = new File() + filename = "choose a file " + }else{ + file = $o1.file + hoc_sf_.head(file.getname(), "[^/]*$", tstr) + file.chooser("r", "Import 3-D Reconstruction File", "*", "Read", "Cancel", tstr) + filename =file.getname() + } + ztrans_ = 0 + dummy_ = 0 + undo_type_ = 0 + show_point_ = 1 + show_diam_ = 1 + if ($o1 == nil) { build() map() return } + init1($o1) + build() + map() + init2() + +} + +proc map() { + sprint(tstr, "%s", this) + if (numarg() == 0) { + box.map(tstr) + }else{ + box.map(tstr, $2, $3, $4, $5) + } +} + +proc init1() { + i=0 j=0 + swc = $o1 + selpoint_ = -1 + selid_ = swc.pt2id(selpoint_) + viewsec = new List() + showtype(-10000) + rotated_ = 0 + rotmat = new Matrix(3,3) + rotmatold = rotmat.c.ident + rotsav = rotmat.c.ident + origin = new Vector(3) + raworigin = new Vector(3) + rawsel = new Vector(3) + m2 = new Matrix(3,3) +} +proc init2() { + rot(0,0) + pl() + g.exec_menu("View = plot") + g.exec_menu("Zoom") +} + +proc build() {local i + box = new HBox(3) + box.full_request(1) + box.save("") + box.ref(this) + box.intercept(1) + box.adjuster(400) + g = new Graph(0) + g.view(2) + g.xaxis(3) + deck = new Deck(3) + build_panel() + deck.map + box.intercept(0) +} + +proc build_panel() {local i + deck.intercept(1) + xpanel("") + xcheckbox(filename, &readfile_, "readfile()") + if (swc == nil) { + xlabel(" accepted file formats:") + xlabel(" SWC") + xlabel(" Neurolucida (v1 and v3)") + xlabel(" Eutectic") + if (nrnpython("")) xlabel(" MorphML") + for i = 0, 15 { xlabel("") } + xpanel(0) + deck.intercept(0) + deck.flip_to(0) + return + } + sprint(tstr, "File format: %s", swc.filetype) + xlabel(tstr) + xlabel("-------------------------------") + g.menu_remove("Zoom") + g.menu_tool("Zoom", "zoom") + g.menu_remove("Translate ") + g.menu_tool("Translate ", "translate") + g.menu_remove("Rotate") + g.menu_tool("Rotate (about axis in plane)", "rotate") + xcheckbox("Rotate 45deg about y axis", &dummy_, "rot45()") + xcheckbox("Rotated (vs Raw view)", &rotated_, "rotraw()") + xcheckbox("Show Points", &show_point_, "pl()") + xcheckbox("Show Diam", &show_diam_, "pl()") + xvarlabel(typelabel_) + xmenu("View type") + xradiobutton("All", "showtype(-10000) pl()", 1) + xradiobutton("Section containing selected point", "showsec() pl()") + xradiobutton("Distal (tree) from selected point", "showdistal() pl()") + xradiobutton("Proximal (path to root) from selected point", "showprox() pl()") + xradiobutton("Root sections", "showroot() pl()") + if (swc.type.min != swc.type.max) { + for i = swc.type.min, swc.type.max { + if (swc.type.indwhere("==", i) != -1) { + sprint(tstr, "type %d", i) + sprint(tstr1, "showtype(%d) pl()", i) + xradiobutton(tstr, tstr1) + } + } + } + xmenu() + g.menu_remove("Select point") + g.menu_tool("Select point", "selpoint", "selpoint1(1)") + if (strcmp(swc.filetype, "Neurolucida") == 0) { + xpvalue("Line#", &selid_, 1, "selid(1)") + if (swc.err) { + xbutton("Problem points", "probpointpanel()") + } + }else if (strcmp(swc.filetype, "Neurolucida V3") == 0) { + xpvalue("Line#", &selid_, 1, "selid(1)") + }else{ + xpvalue("Select id", &selid_, 1, "selid(1)") + } + xlabel("-------------------------------") + xbutton("Edit", "map_edit()") + xmenu("Export") + xbutton("CellBuilder", "cbexport()") + xbutton("Instantiate", "instantiate(nil)") + xmenu() + sprint(tstr, "%s filter facts", swc.filetype) + xbutton(tstr, "swc.helptxt()") + xpanel(0) + deck.intercept(0) + deck.flip_to(0) +} + + +proc map_edit() { + if (editbox == nil) { + build_edit() + } + if (editbox.ismapped) { return } + sprint(tstr, "Edit %s", this) + editbox.map(tstr) +} +proc build_edit() { + editbox = new VBox() + editbox.intercept(1) + editbox.save("") + xpanel("") + ztransitem() + xlabel("Select point:") + xcheckbox("Largest z change", &dummy_, "sel_largest_dz()") + xlabel("then action:") + xcheckbox("z-translate rest of tree to parent point", &dummy_, "edit2()") + xcheckbox("z-translate to average of adjacent points", &dummy_, "edit1()") + xcheckbox("undo last", &dummy_, "edit0()") + xlabel("-------------------") + xcheckbox("3 point filter of all z values (no undo)", &dummy_, "edit3()") + xpanel() + editbox.intercept(0) +} + +proc sel_largest_dz() {local i, j, dz, dzmax, imax, jmax localobj sec, tobj + dummy_ = 0 + dzmax = -1 + for i = 0, swc.sections.count-1 { + sec = swc.sections.object(i) + tobj = sec.raw.getrow(2).deriv(1,1).abs + j = tobj.max_ind + dz = tobj.x[j] + if (dz > dzmax) { + jmax = j+1 + imax = i + dzmax = dz + } + } + if (dzmax > 0) { + selpoint_ = swc.sec2pt(imax, jmax) + selpoint_dependent_show() + swc.sections.object(imax).raw.getcol(jmax, rawsel) + selid_ = swc.pt2id(selpoint_) + pl() + } +} + +proc ztransitem() {local i, n localobj raw + n = 0 + for i = 0, swc.sections.count-1 { + raw = swc.sections.object(i).raw + if (abs(raw.x[2][0] - raw.x[2][1]) > 10) { + n += 1 + } + } + if (n > 0) { + sprint(tstr, "z translation for %d abrupt branch backlash", n) + xcheckbox(tstr, &ztrans_, "ztrans()") + } +} + +proc ztrans() { local i, zd, pn localobj sec + if (ztrans_) { + for i = 0, swc.sections.count-1 { + sec = swc.sections.object(i) + if (object_id(sec.parentsec) == 0) { continue } + if (object_id(sec.parentsec.parentsec) == 0) { continue } + zd = sec.raw.x[2][1] - sec.raw.x[2][0] + if (abs(zd) > 5) { + zd += sec.parentsec.ztrans + }else{ + zd = sec.parentsec.ztrans + } + sec.ztrans = zd + sec.raw.setrow(2, sec.raw.getrow(2).sub(sec.ztrans)) + pn = sec.parentsec.raw.ncol + sec.raw.x[2][0] = sec.parentsec.raw.x[2][pn-1] + } + }else{ + for i = 0, swc.sections.count-1 { + sec = swc.sections.object(i) + if (sec.ztrans) { +sec.raw.setrow(2, sec.raw.getrow(2).add(sec.ztrans)) + pn = sec.parentsec.raw.ncol + sec.raw.x[2][0] = sec.parentsec.raw.x[2][pn-1] + sec.ztrans = 0 + } + } + } + redraw() +} + +proc edit0() {local i, n localobj sec + dummy_ = 0 + if (undo_type_ == 1) { + i = swc.pt2sec(undo_selpoint_, sec) + sec.raw.x[2][i] = undo_z_ + sec.raw.getcol(i, rawsel) + }else if (undo_type_ == 2) { + i = swc.pt2sec(undo_selpoint_, sec) + n = sec.raw.ncol + for i=i, n-1 { + sec.raw.x[2][i] += undo_z_ + } + sec.raw.getcol(i, rawsel) + for i=0, swc.sections.count-1 { swc.sections.object(i).volatile = 0 } + sec.volatile = 1 + for i=0, swc.sections.count-1 { + sec = swc.sections.object(i) + if (object_id(sec.parentsec)) if (sec.parentsec.volatile) { + sec.volatile = 1 + sec.raw.setrow(2, sec.raw.getrow(2).add(undo_z_)) + } + } + } + undo_type_ = 0 + redraw() +} + +proc edit1() {local i, z1, z2 localobj sec + // z translate to average of adjacent points + dummy_ = 0 + if (selpoint_ >= 0) { + i = swc.pt2sec(selpoint_, sec) + if (i > 0) { + z1 = sec.raw.x[2][i-1] + }else{ + return + } + if (i < sec.raw.ncol-1) { + z2 = sec.raw.x[2][i+1] + }else{ + return + } + undo_selpoint_ = selpoint_ + undo_type_ = 1 + undo_z_ = sec.raw.x[2][i] + sec.raw.x[2][i] = (z1 + z2)/2 + sec.raw.getcol(i, rawsel) + } + redraw() +} + +proc edit2() {local i, ip, z1, n localobj sec + // z-translate rest of tree to parent point + dummy_ = 0 + if (selpoint_ >= 0) { + ip = swc.pt2sec(selpoint_, sec) + if (ip > 0) { + z1 = sec.raw.x[2][ip] - sec.raw.x[2][ip-1] + }else{ + return + } + undo_selpoint_ = selpoint_ + undo_type_ = 2 + undo_z_ = z1 + n = sec.raw.ncol + for i=ip, n-1 { + sec.raw.x[2][i] -= z1 + } + sec.raw.getcol(ip, rawsel) + for i=0, swc.sections.count-1 { swc.sections.object(i).volatile = 0 } + sec.volatile = 1 + for i=0, swc.sections.count-1 { + sec = swc.sections.object(i) + if (object_id(sec.parentsec)) if (sec.parentsec.volatile) { + sec.volatile = 1 + sec.raw.setrow(2, sec.raw.getrow(2).sub(z1)) + } + } + } + redraw() +} + +proc edit3() {local i localobj sec + dummy_ = 0 + for i=0, swc.sections.count-1 { + sec = swc.sections.object(i) + sec.raw.setrow(2, sec.raw.getrow(2).medfltr) + } + if (selpoint_ >= 0) { + i = swc.pt2sec(selpoint_, sec) + sec.raw.getcol(i, rawsel) + } + redraw() +} + +proc probpointpanel() { + problist = new List() + problist.browser("Problem points", "s") + problist.select_action("probpoint(hoc_ac_)") + swc.fillproblist(problist) + problist.select(-1) +} + +proc probpoint() {local i + if ($1 < 0) {return} + sscanf(problist.object($1).s, "%d:", &i) + selid_ = i + selid(0) +} + +proc readfile() { + readfile_ = 0 + if (numarg() == 0) { + file.chooser("r", "Import 3-D Reconstruction File", "*", "Read", "Cancel") + if (file.chooser()) { + if (!some_format()) { + return + } + }else{ + return + } + }else{ + file = new File($s1) + if (!some_format()) { + return + } + } + // if new file + problist = nil + deck.flip_to(-1) + build_panel() + deck.move_last(0) + deck.flip_to(0) + init1(swc) + init2() + doNotify() + if (swc.err) { + printf("\n") + sprint(tstr, "%s: File translation problems. See the messages on the terminal", file.getname) + continue_dialog(tstr) + if (strcmp(swc.filetype, "Neurolucida V3") == 0) { + swc.b2spanel(this) + } + } + deck.remove_last() +} + +func some_format() {local i, a,b,c,d,e,f,g, n + if (!file.ropen()) { + sprint(tstr, "Can't read %s", file.getname) + continue_dialog(tstr) + return 0 + } + while (1) { + if (file.eof) { + file.close + sprint(tstr, "Can't figure out file format for %s", file.getname) + continue_dialog(tstr) + return 0 + } + file.gets(tstr) + if (hoc_sf_.head(tstr, "^\\<\\?xml", tstr1) != -1) { + if (nrnpython("")) { + swc = new Import3d_MorphML() break + }else{ + file.close + sprint(tstr, "Can't read MorphML: Python not available.") + continue_dialog(tstr) + return 0 + } + } + n = sscanf(tstr, "%f %f %f %f %f %f %f", &a, &b, &c, &d, &e, &f, &g) + if (n == 7) { swc = new Import3d_SWC_read() break } + n = sscanf(tstr, "[%d,%d] (%f,%f,%f) %f", &a, &b, &c, &d, &e, &f) + if (n == 6) { swc = new Import3d_Neurolucida_read() break } + n = sscanf(tstr, "%d %s %d %f %f %f %f", &a, tstr, &b, &c, &d, &e, &f) + if (n == 7) { swc = new Import3d_Eutectic_read() break } + if (hoc_sf_.tail(tstr, "^[ \t]*", tstr1) != -1) { + //unfortunately regexp does not allow an explicit "(" + hoc_sf_.left(tstr1, 1) + if (strcmp(tstr1, "(") == 0) { + swc = new Import3d_Neurolucida3() break + } + } + if (hoc_sf_.head(tstr, "^;[ \t]*V3", tstr1) != -1) { + swc = new Import3d_Neurolucida3() break + } + } + file.close + filename = file.getname + swc.input(filename) + return 1 +} + +proc pl_point() { local i, j, i1 localobj m, m0 + if (viewsec.count) {m0 = swc.sections.object(0).xyz} + for i=0, viewsec.count-1 { + viewsec.object(i).pl_point(g) + } +} + +proc pl_centroid() {local i + for i=0, swc.sections.count-1 { + swc.sections.object(i).pl_centroid(g) + } +} +proc pl_diam() {local i localobj sec + for i=0, viewsec.count-1 { + viewsec.object(i).pl_diam(g) + } +} +proc pl() { localobj tobj + g.erase_all + if (show_diam_) {pl_diam()} + pl_centroid() + if (show_point_) {pl_point()} + if (selpoint_ >= 0) { + tobj = m2.mulv(rawsel) + g.mark(tobj.x[0], tobj.x[1], "O", 12, 2, 1) + swc.label(selpoint_, tstr) + g.label(.1, .05, tstr, 2, 1, 0, 0, 1) + } +} + +proc redraw() { local i localobj sec + if (selpoint_ >= 0) { + i = swc.pt2sec(selpoint_, sec) + sec.raw.getcol(i, rawsel) + } + showtype(viewtype_) + rot(0,0) + pl() +} + +proc showtype() { + viewtype_ = $1 + viewsec.remove_all + if ($1 == -10000) { + typelabel_ = "View all types" + for i=0, swc.sections.count - 1 { + viewsec.append(swc.sections.object(i)) + swc.sections.object(i).centroid_color = 2 + } + }else{ + sprint(typelabel_, "View type %d", viewtype_) + for i=0, swc.sections.count - 1 { + if (swc.sections.object(i).type == viewtype_) { + viewsec.append(swc.sections.object(i)) + swc.sections.object(i).centroid_color = 2 + }else{ + swc.sections.object(i).centroid_color = 9 + } + } + } +} + +proc selpoint_dependent_show() { + if (viewtype_ == -20000) { + showdistal() + }else if (viewtype_ == -30000) { + showprox() + }else if (viewtype_ == -40000) { + showsec() + }else if (viewtype_ == -50000) { + showroot() + } +} + +proc showdistal() {local i localobj sec + viewtype_ = -20000 + typelabel_ = "Show distal (tree) from selected point" + viewsec.remove_all + for i=0, swc.sections.count - 1 { + swc.sections.object(i).centroid_color = 9 + } + if (selpoint_ < 0) { return } + swc.pt2sec(selpoint_, sec) + // recursion is trivial but I want to avoid the depth so use the + // fact that children are after the parent in the sections list + sec.centroid_color = 2 + viewsec.append(sec) + for i=0, swc.sections.count - 1 { + if (swc.sections.object(i).centroid_color == 2) { + break + } + } + for i=i+1, swc.sections.count - 1 { + sec = swc.sections.object(i) + if (sec.parentsec != nil) if (sec.parentsec.centroid_color == 2) { + sec.centroid_color = 2 + viewsec.append(sec) + } + } +} + +proc showprox() {localobj sec + viewtype_ = -30000 + typelabel_ = "Show proximal (path to root) from selected point" + viewsec.remove_all + for i=0, swc.sections.count - 1 { + swc.sections.object(i).centroid_color = 9 + } + if (selpoint_ < 0) { return } + for (swc.pt2sec(selpoint_, sec); sec != nil; sec = sec.parentsec) { + viewsec.append(sec) + sec.centroid_color = 2 + } +} + +proc showsec() {localobj sec + viewtype_ = -40000 + typelabel_ = "Show section containing selected point" + viewsec.remove_all + for i=0, swc.sections.count - 1 { + swc.sections.object(i).centroid_color = 9 + } + if (selpoint_ < 0) { return } + swc.pt2sec(selpoint_, sec) + if (sec != nil) { + viewsec.append(sec) + sec.centroid_color = 2 + } +} + +proc showroot() {localobj sec + viewtype_ = -50000 + typelabel_ = "Show root sections" + viewsec.remove_all + for i=0, swc.sections.count - 1 { + sec = swc.sections.object(i) + sec.centroid_color = 9 + if (sec.parentsec == nil) { + sec.centroid_color = 2 + viewsec.append(sec) + } + } +} + +proc selpoint1() { // deselection not supported by menu_tool + if ($1 == 0) { + selpoint_ = -1 + } +} +proc selpoint() {local i, j + if ($1 == 2) { + nearest_point($2, $3, &i, &j) + selpoint_ = swc.sec2pt(i, j) + selpoint_dependent_show() + swc.sections.object(i).raw.getcol(j, rawsel) + selid_ = swc.pt2id(selpoint_) + pl() + } +} + +proc selid() {local i, j localobj sec + selpoint_ = swc.id2pt(selid_) + selid_ = swc.pt2id(selpoint_) + if (selpoint_ >= 0) { + i = swc.pt2sec(selpoint_, sec) + sec.raw.getcol(i, rawsel) + } + selpoint_dependent_show() + pl() + if ($1 == 1) { + swc.label(selpoint_, tstr) + print tstr + } +} + +proc zoom() {local x1,y1,scale,w,h,x0,y0 + if ($1 == 2) { + i = g.view_info() + x = $2 + y = $3 + xrel=g.view_info(i, 11, $2) + yrel=g.view_info(i, 12, $3) + width=g.view_info(i,1) + height=g.view_info(i,2) + } + if ($1 == 1) { + x1 = g.view_info(i, 11, $2) + y1 = g.view_info(i, 12, $3) + y1 = (y1 - yrel) + (x1 - xrel) + if(y1 > 2) { y1 = 2 } else if (y1 < -2) { y1 = -2 } + scale = 10^(y1) + w = width/scale + h = height/scale + x0 = x - w*xrel + y0 = y - h*yrel + g.view_size(i, x0, x0+w, y0, y0+h) + } +} + +proc translate() {local x0,y0 + if ($1 == 2) { + i = g.view_info() + x = g.view_info(i, 5) + y = g.view_info(i, 7) + xrel=g.view_info(i, 11, $2) + yrel=g.view_info(i, 12, $3) + width=g.view_info(i,1) + height=g.view_info(i,2) + } + if ($1 == 1) { + x1 = g.view_info(i, 11, $2) + y1 = g.view_info(i, 12, $3) + x0 = x - width*(x1 - xrel) + y0 = y - height*(y1 - yrel) + g.view_size(i, x0, x0 + width, y0, y0 + height) + } +} + +func nearest_point() { local i, j, xmin localobj m, v1 + // return section index and sectionpoint index in $3 and $4 + xmin = 1e9 + for i=0, swc.sections.count-1 { + m = swc.sections.object(i).xyz + v1 = m.getrow(0).sub($1).pow(2).add(m.getrow(1).sub($2).pow(2)) + j = v1.min_ind + if (v1.x[j] < xmin) { + xmin = v1.x[j] + $&3 = i + $&4 = j + } + } + return xmin +} + +proc rotate() {local x, y, x0, y0, len, a + if ($1 == 2) { + rotated_ = 1 + nearest_point($2, $3, &i, &j) + swc.sections.object(i).xyz.getcol(j, origin) + swc.sections.object(i).raw.getcol(j, raworigin) +//print i, j origin.printf + i = g.view_info() + xpix = g.view_info(i,13, $2) + ypix = g.view_info(i, 14, $3) // from top + left = g.view_info(i, 5) + bottom = g.view_info(i, 7) + width=g.view_info(i,1) + height=g.view_info(i,2) + }else{ + x = g.view_info(i,13, $2) - xpix + y = ypix - g.view_info(i, 14, $3) + // rotation axis is normal to the line, rotation magnitude + // proportional to length of line + len = sqrt(x*x + y*y) + // rotation axis angle + if (len > 0) { + a = atan2(x, y) + b = len/50 + }else{ + a = 0 + b = 0 + } + rot(a, b) + pl() + tobj = rotmat.mulv(origin) + //tobj.x[0] should be at same place as origin.x[0] + x0 = left - origin.x[0] + tobj.x[0] + y0 = bottom - origin.x[1] + tobj.x[1] + g.view_size(i, x0, x0 + width, y0, y0 + height) + + } + if ($1 == 3) { + m2.c(rotmatold) +//rotmatold.printf + } +} + +proc rotraw() {local x0, y0 + width = g.view_info(0, 1) + height = g.view_info(0, 2) + left = g.view_info(0,5) + bottom = g.view_info(0,7) + if (rotated_ == 0) { //turn off + rotmatold.c(rotsav) + tobj = rotmatold.mulv(raworigin) + //tobj.x[0] should be at same place as origin.x[0] + x0 = left + raworigin.x[0] - tobj.x[0] + y0 = bottom + raworigin.x[1] - tobj.x[1] + rotmatold.ident + }else{ // back to previous rotation + rotsav.c(rotmatold) + tobj = rotmatold.mulv(raworigin) + //tobj.x[0] should be at same place as origin.x[0] + x0 = left - raworigin.x[0] + tobj.x[0] + y0 = bottom - raworigin.x[1] + tobj.x[1] + } + rot(0,0) + pl() + g.view_size(0, x0, x0 + width, y0, y0 + height) +} + +proc rot45() { + rot(PI/2, PI/4) + rotated_=1 + m2.c(rotmatold) + pl() + dummy_ = 0 +} + +proc rot() {local s, c, i localobj sec + s = sin($1) c = cos($1) + m2.zero + m2.x[2][2] = 1 + m2.x[1][1] = m2.x[0][0] = c + m2.x[1][0] = -s + m2.x[0][1] = s +//m2.printf + s = sin($2) c = cos($2) + rotmat.zero + rotmat.x[0][0] = 1 + rotmat.x[1][1] = rotmat.x[2][2] = c + rotmat.x[1][2] = s + rotmat.x[2][1] = -s +//rotmat.printf + + m2.mulm(rotmat).mulm(m2.transpose(m2), rotmat) + rotmat.mulm(rotmatold, m2) +//rotmat.printf + for i=0, swc.sections.count-1 { + sec = swc.sections.object(i) + sec.rotate(m2) + } +} + +proc cbexport() {local i, j, k localobj sec, cell + chk_valid() + j = 0 + for i=0, swc.sections.count-1 { + sec = swc.sections.object(i) + if (sec.is_subsidiary) { continue } + if (sec.parentsec == nil) { + sec.volatile2 = j + j += 1 + }else{ + sec.volatile2 = sec.parentsec.volatile2 + } + } + cell = new List() + for k=0, j-1 { + cell.remove_all() + for i=0, swc.sections.count-1 { + sec = swc.sections.object(i) + if (sec.is_subsidiary) { continue } + if (sec.volatile2 == k) { + cell.append(sec) + } + } + cbexport1(cell) + } +} + +proc sphere_rep() { local i localobj x, y, z, d + x = new Vector(3) y = x.c z = x.c d = x.c + x.fill($o1.x[0]) + y.fill($o2.x[0]) + z.fill($o3.x[0]) + d.fill($o4.x[0]) + x.x[0] -= $o4.x[0]/2 + x.x[2] += $o4.x[0]/2 + $o1 = x $o2 = y $o3 = z $o4 = d +} + +proc cbexport1() {local i, j, k, min localobj cb, sec, psec, cbsec, slist, m, subsetindex, xx, yy, zz, dd + for i=0, $o1.count-1 { + sec = $o1.object(i) + sec.volatile = i + } + min = set_nameindex($o1) + cb = new CellBuild() + cb.topol.names_off = 1 + cb.topol.circles_off = 1 + slist = cb.topol.slist + slist.remove_all() + for i=0, $o1.count-1 { + sec = $o1.object(i) + psec = nil + if (sec.parentsec != nil) { + psec = slist.object(sec.parentsec.volatile) + } + type2name(sec.type, tstr) + cbsec = new CellBuildSection(tstr, sec.nameindex, 0, psec, sec.parentx) + slist.append(cbsec) + m = sec.raw + j = sec.first + xx = m.getrow(0).c(j) + yy = m.getrow(1).c(j) + zz = m.getrow(2).c(j) + dd = sec.d.c(j) + if (sec.iscontour_) { + contour2centroid(xx, yy, zz, dd, sec) + } + if (sec.parentsec == nil && dd.size == 1) { + // represent spherical soma as 3 point cylinder + // with L=diam + sphere_rep(xx, yy, zz, dd) + } + k = dd.size-1 + cbsec.position(xx.x[0], yy.x[0], xx.x[k], yy.x[k]) + cbsec.i3d = k+1 + cbsec.p3d = new P3D(k + 1) + cbsec.p3d.x = xx + cbsec.p3d.y = yy + cbsec.p3d.z = zz + cbsec.p3d.d = dd + if (sec.first == 1) { + cbsec.logstyle(m.x[0][0], m.x[1][0], m.x[2][0]) + } + cb.all.add(cbsec) + } + cb.topol.consist() + cb.topol.update() + cb.subsets.update() + subsetindex = types.c.fill(0) + k = 0 + for i=0, types.size-1 { + if (types.x[i] > 0) { + k += 1 // after all + subsetindex.x[i] = k + j = i + min + if (j == 1) { + tstr = "somatic" + }else if (j == 2) { + tstr = "axonal" + }else if (j == 3) { + tstr = "basal" + }else if (j == 4) { + tstr = "apical" + }else if (j < 0) { + sprint(tstr, "minus_%dset", -j) + }else{ + sprint(tstr, "dendritic_%d", j) + } + m = new SNList(tstr) + cb.subsets.snlist.append(m) + } + } + for i=0, slist.count-1 { + sec = $o1.object(i) + cbsec = slist.object(i) + cb.subsets.snlist.object(subsetindex.x[sec.type-min]).add(cbsec) + } + //cb.page(2) //unfortunately not able to blacken the radiobutton +} + +func set_nameindex() {local i, min localobj sec + min = swc.type.min + types = new Vector(swc.type.max - min + 1) + for i = 0, $o1.count-1 { + sec = $o1.object(i) + if (sec.is_subsidiary) { continue } + sec.nameindex = types.x[sec.type - min] + types.x[sec.type-min] += 1 + } + return min +} + +proc instantiate() {local i, j, min, haspy localobj sec, xx, yy, zz, dd, pyobj + chk_valid() + haspy = nrnpython("import neuron") + if (haspy) { + pyobj = new PythonObject() + } + min = set_nameindex(swc.sections) + // create + for i = 0, types.size-1 { + type2name(i+min, tstr) + if (types.x[i] == 1) { + sprint(tstr1, "~create %s[1]\n", tstr) + execute(tstr1, $o1) + }else if (types.x[i] > 1) { + sprint(tstr1, "~create %s[%d]\n", tstr, types.x[i]) + execute(tstr1, $o1) + } + if ($o1 != nil) { mksubset($o1, i+min, tstr) } + } + if ($o1 != nil) {execute("forall all.append", $o1) } + // connect + for i = 0, swc.sections.count-1 { + sec = swc.sections.object(i) + if (sec.is_subsidiary) { continue } + name(sec, tstr) + if (i == 0) { + sprint(tstr1, "access %s", tstr) + if ($o1 == nil) { + execute(tstr1, $o1) + } + } + if (sec.parentsec != nil) { + name(sec.parentsec, tstr1) + sprint(tstr1, "%s connect %s(0), %g", tstr1, tstr, sec.parentx) + execute(tstr1, $o1) + } + // 3-d point info + if (sec.first == 1) { + sprint(tstr1, "%s { pt3dstyle(1, %g, %g, %g) }", tstr, sec.raw.x[0][0], sec.raw.x[1][0], sec.raw.x[2][0]) + execute(tstr1, $o1) + } + j = sec.first + xx = sec.raw.getrow(0).c(j) + yy = sec.raw.getrow(1).c(j) + zz = sec.raw.getrow(2).c(j) + dd = sec.d.c(j) + if (sec.iscontour_) { + if (haspy) { + pyobj.neuron._declare_contour(sec, tstr) + } + contour2centroid(xx, yy, zz, dd, sec) + } + if (dd.size == 1) { sphere_rep(xx, yy, zz, dd) } + for j = 0, dd.size-1 { + sprint(tstr1, "%s { pt3dadd(%g, %g, %g, %g) }",\ + tstr,xx.x[j], yy.x[j], zz.x[j], dd.x[j]) + execute(tstr1, $o1) + } + } +} + +proc chk_valid() {local i, x, replot localobj sec + replot = 0 + // some validity checks added in response to experienced file errors + // sometimes we can work around them + + // two point sections with 0 length, remove, unless root + for (i=swc.sections.count-1; i >= 0; i -= 1) { + sec = swc.sections.object(i) + if (sec.parentsec == nil) { continue } + if ((sec.raw.ncol - sec.first) <= 1) { + if (!quiet) {// addded by Sergey to suppress the warning output + printf("One point section %s ending at line %d has been removed\n", sec, swc.iline.x[swc.id2line(sec.id)]) + } + rm0len(i, sec) + replot = 1 + }else if ((sec.raw.ncol - sec.first) <= 2) { + if (sec.raw.getcol(sec.first).eq(sec.raw.getcol(sec.first + 1))) { + printf("Two point section ending at line %d with 0 length has been removed\n", swc.iline.x[swc.id2line(sec.id)]) + rm0len(i, sec) + replot = 1 + } + } + } + if (replot && g != nil) { + redraw() + } +} + +proc rm0len() {local i localobj sec + swc.sections.remove($1) + for i=$1, swc.sections.count-1 { + sec = swc.sections.object(i) + if (sec.parentsec == $o2) { + sec.parentsec = $o2.parentsec + sec.parentx = $o2.parentx + if (!quiet) {// addded by Sergey to suppress the warning output + printf("\tand child %s reattached\n", sec) + } + } + } +} + +proc mksubset() { + if ($2 == 1) { + tstr1 = "somatic" + }else if ($2 == 2) { + tstr1 = "axonal" + }else if ($2 == 3) { + tstr1 = "basal" + }else if ($2 == 4) { + tstr1 = "apical" + }else if ($2 < 0) { + sprint(tstr1, "minus_%dset", -$2) + }else{ + sprint(tstr1, "dendritic_%d", $2) + } + sprint(tstr1, "forsec \"%s\" %s.append", $s3, tstr1) + execute(tstr1, $o1) +} + +proc contour2centroid() {local i, j, imax, imin, ok localobj mean, pts, d, max, min, tobj, rad, rad2, side2, pt, major, m, minor + if (object_id($o5.contour_list)) { + contourstack2centroid($o1, $o2, $o3, $o4, $o5) + return + } + mean = swc.sections.object(0).contourcenter($o1, $o2, $o3) + if (g != nil) { + g.beginline(6,1) + for i=0, $o1.size-1 { + g.line($o1.x[i], $o2.x[i]) + } + g.flush() + } + pts = new Matrix(3, $o1.size) + for i=1,3 { pts.setrow(i-1, $oi.c.sub(mean.x[i-1])) } + // find the major axis of the ellipsoid that best fits the shape + // assuming (falsely in general) that the center is the mean + + m = new Matrix(3,3) + for i=0, 2 { + for j=i, 2 { + m.x[i][j] = pts.getrow(i).mul(pts.getrow(j)).sum + m.x[j][i] = m.x[i][j] + } + } + tobj = m.symmeig(m) + // major axis is the one with largest eigenvalue + major = m.getcol(tobj.max_ind) + // minor is normal and in xy plane + minor = m.getcol(3-tobj.min_ind-tobj.max_ind) + minor.x[2] = 0 + minor.div(minor.mag) +if (g != nil) { +g.beginline(4, 3) g.line(mean.x[0], mean.x[1]) +g.line(mean.x[0] + 20*major.x[0], mean.x[1] + 20*major.x[1]) g.flush +} + d = new Vector(pts.ncol) + rad = new Vector(pts.ncol) + for i=0, pts.ncol-1 { + pt = pts.getcol(i) + d.x[i] = pt.dot(major) // position on the line + tobj = major.c.mul(d.x[i]) + rad.x[i] = pt.dot(minor) + } + imax = d.max_ind + d.rotate(-imax) + rad.rotate(-imax) + imin = d.min_ind + side2 = d.c(imin) + rad2 = rad.c(imin) + d.resize(imin).reverse + rad.resize(imin).reverse + // now we have the two sides without the min and max points (rad=0) + // we hope both sides now monotonically increase, i.e. convex + // make it convex + for (j = d.size-1; j > 0; j -= 1) { + if (d.x[j] <= d.x[j-1]) { +//printf("removed d %d %g\n", j, d.x[j]) + d.remove(j) + rad.remove(j) + if (j != d.size()) { j += 1 } + } + } + for (j = side2.size-1; j > 0; j -= 1) { + if (side2.x[j] <= side2.x[j-1]) { +//printf("removed side2 %d %g\n", j, side2.x[j]) + side2.remove(j) + rad2.remove(j) + if (j != side2.size()) { j += 1 } + } + } + // can interpolate so diams on either side of major have same d + tobj = d.c.append(side2) + tobj.sort + i = tobj.x[1] j = tobj.x[tobj.size-2] + tobj.indgen(i, j, (j-i)/20) + rad.interpolate(tobj, d) + rad2.interpolate(tobj,side2) + d = tobj + pts.resize(3, d.size) + $o4.resize(d.size) + for i = 0, d.size-1 { + pt = major.c.mul(d.x[i]).add(mean) + $o4.x[i] = abs(rad.x[i] - rad2.x[i]) + tobj = pt.c.add(minor.c.mul(rad.x[i])) +if (g != nil) g.beginline(5,3) g.line(tobj.x[0], tobj.x[1]) + tobj = pt.c.add(minor.c.mul(rad2.x[i])) +if (g != nil) g.line(tobj.x[0], tobj.x[1]) g.flush +// pt.add(minor.c.mul(rad2.x[i])).add(minor.c.mul(rad.x[i])) + pts.setcol(i, pt) + } + // avoid 0 diameter ends + $o4.x[0] = ($o4.x[0]+$o4.x[1])/2 + i = $o4.size-1 + $o4.x[i] = ($o4.x[i]+$o4.x[i-1])/2 + for i=1,3 { $oi = pts.getrow(i-1) } +// print d d.printf print rad rad.printf +// print side2 side2.printf print rad2 rad2.printf +} + +proc contourstack2centroid() {local i, j, area, d localobj c + area = $o5.stk_triang_area() + printf("stk_triang_area = %g\n", area) + for i=1,4 { $oi.resize(0) } + c = $o5.approximate_contour_by_circle(&d) + $o4.append(d) for i=1,3 { $oi.append(c.x[i-1]) } + for j=0, $o5.contour_list.count-1 { + c = $o5.contour_list.object(j).approximate_contour_by_circle(&d) + $o4.append(d) for i=1,3 { $oi.append(c.x[i-1]) } + } +} + +proc name() { + type2name($o1.type, $s2) + if ($o1.nameindex > 0) { + sprint($s2, "%s[%d]", $s2, $o1.nameindex) + } +} + +proc type2name() { + if ($1 == 1) { + $s2 = "soma" + }else if ($1 == 2) { + $s2 = "axon" + }else if ($1 == 3) { + $s2 = "dend" + }else if ($1 == 4) { + $s2 = "apic" + }else if ($1 < 0) { + sprint($s2, "minus_%d", -$1) + }else{ + sprint($s2, "dend_%d", $1) + } +} +endtemplate Import3d_GUI diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d/import3d_sec.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d/import3d_sec.hoc new file mode 100644 index 0000000..01b0b2d --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d/import3d_sec.hoc @@ -0,0 +1,392 @@ +begintemplate Import3d_Section +// primarily for display. Allows gui without instantiating sections +// fid refers to the raw index of the point that id refers to. +// For a root section fid is normally 0. For sections that have +// parents, fid is normally 1 since the first point is often a copy of +// the last point of the parent. +// The variable first=0 means that when diam is shown, there +// should be a glyph drawn defined by raw indices 0 and 1. +// if this is a contour it may also contain a list of contours that +// define a 3-d object +public raw, xyz, d, id, append, g, mkglyph, rotate, type, centroid_color +public iscontour_, pid, parentsec, parentx, volatile, nameindex, first, fid +public contour_list, pl_centroid, pl_diam +public stk_triang_vec, stk_triang_area, is_subsidiary +public volatile2, contourcenter, ztrans, approximate_contour_by_circle +public pl_point, insrt, set_pt, stk_center, accurate_triangle_area +objref raw, xyz, d, g, parentsec, contour_list, this, stk_triang_vec +proc init() { + is_subsidiary = 0 + ztrans = 0 + first = 0 + fid = 0 + nameindex=0 + parentx = 1 + volatile = 0 + volatile2 = 0 + pid = -1 + iscontour_ = 0 + type = 0 + centroid_color = 2 + id = $1 + raw = new Matrix(3, $2) + xyz = new Matrix(3, $2) + d = new Vector($2) +} +proc set_pt() { + raw.x[0][$1] = $2 + raw.x[1][$1] = $3 + raw.x[2][$1] = $4 + d.x[$1] = $5 +} + +proc append() {local i, j + for i=0, $3-1 { + j = $1 + i + k = $2 + i + set_pt(j, $o4.x[k], $o5.x[k], $o6.x[k], $o7.x[k]) + } +} + +proc insrt() {local i, nr, nc + nr = raw.nrow nc = raw.ncol + d.resize(nc+1) + raw.resize(nr, nc+1) + xyz.resize(nr, nc+1) + for (i=nc-1; i >= $1; i -= 1) { + raw.setcol(i+1, raw.getcol(i)) + d.x[i+1] = d.x[i] + } + set_pt($1, $2, $3, $4, $5) +} + +proc pl_centroid() {local i, n + xyz.getrow(1).line($o1, xyz.getrow(0), centroid_color, 1) + if (iscontour_) { + n = xyz.ncol - 1 + $o1.beginline(centroid_color, 1) + $o1.line(xyz.x[0][0], xyz.x[1][0]) + $o1.line(xyz.x[0][n], xyz.x[1][n]) + } + if (0) { + if (object_id(contour_list)) { + for i=0, contour_list.count-1 { + contour_list.object(i).pl_centroid($o1) + } + } + } +} + +proc pl_diam() {local i + if (!iscontour_) { + mkglyph() + $o1.glyph(g, 0, 0) + }else{ + if (object_id(contour_list)) { + if (!object_id(contour_list.object(0).stk_triang_vec)) { + mk_stk_triang_vec(this, contour_list.object(0)) + for i=1, contour_list.count-1 { + mk_stk_triang_vec(contour_list.object(i-1), contour_list.object(i)) + } + } + pl_stk_triang($o1, this, contour_list.object(0)) + for i=1, contour_list.count-1 { + pl_stk_triang($o1, contour_list.object(i-1), contour_list.object(i)) + } + } + } +} + +proc pl_point() {local i + for i=first, xyz.ncol-1 { + $o1.mark(xyz.x[0][i], xyz.x[1][i], "s", 5, 3, 1) + } + if (object_id(parentsec) == 0) { + $o1.mark(xyz.x[0][0], xyz.x[1][0], "S", 8, 3, 1) + } + if (0) { + if (object_id(contour_list)) { + for i=0, contour_list.count-1 { + contour_list.object(i).pl_point($o1) + } + } + } +} + +proc mkglyph() {local i, d1, d2 localobj x, y, norm, x1, y1, i1 + g = new Glyph() + if (xyz.ncol - first < 1) { return } + // normal + x1 = xyz.getrow(0) + y1 = xyz.getrow(1) + if (xyz.ncol - first == 1) { + // render as spherical + g.circle(x1.x[0], y1.x[0], d.x[0]/2) + g.fill(1) + return + } + // may or may not want to include parent point in glyph + x = x1.c(first).deriv(1,1) + y = y1.c(first).deriv(1,1) + // point separations + norm = x.c.mul(x).add(y.c.mul(y)).sqrt.mul(2) // d is diam, need radius + // only want frustra for the non-zero separations + i1=norm.c.indvwhere("!=", 0) + if (i1.size == 0) { +// printf("Section with id=%d has 0 length in this projection\n", id) + return + } + norm.index(norm, i1) + x.index(x, i1).div(norm) + y.index(y, i1).div(norm) + + // but take care of the possible index offset due to missing parent point + if (first) { i1.add(first) } + i1.append(x1.size-1) + x1.index(x1, i1) + y1.index(y1, i1) + + for i = 0, x.size-1 { + d1 = d.x[i1.x[i]] d2=d.x[i1.x[i]+1] + g.path() + g.m(x1.x[i]+y.x[i]*d1, y1.x[i]-x.x[i]*d1) + g.l(x1.x[i+1]+y.x[i]*d2, y1.x[i+1]-x.x[i]*d2) + g.l(x1.x[i+1]-y.x[i]*d2, y1.x[i+1]+x.x[i]*d2) + g.l(x1.x[i]-y.x[i]*d1, y1.x[i]+x.x[i]*d1) + g.close() + g.fill(1) + } +} + +proc rotate() { + $o1.mulm(raw, xyz) + if (1) { + if (object_id(contour_list)) { + for i=0, contour_list.count-1 { + contour_list.object(i).rotate($o1) + } + } + } +} + + +// a utility function +obfunc contourcenter() {local i localobj mean, pts, perim, d + // convert contour defined by $o1, $o2, $o3 vectors to + // 100 uniform points around perimeter + // and return the center coordinates as well as the uniform contour + // vectors (in $o1, $o2, $o3) + pts = new Matrix(3, $o1.size) + for i=1,2 { pts.setrow(i-1, $oi) } + for i=0,2 {pts.setrow(i, pts.getrow(i).append(pts.x[i][0]).deriv(1,1)) } + perim = new Vector(pts.ncol) + for i=1, pts.ncol-1 { perim.x[i] = perim.x[i-1] + pts.getcol(i-1).mag } + d = new Vector(101) + d.indgen(perim.x(perim.size-1)/100) + for i=1,3 $oi.interpolate(d, perim) + mean = new Vector(3) + for i=1, 3 { mean.x[i-1] = $oi.mean } + return mean +} + +// return center (Vector.size=3) and average diameter in $&1 +obfunc approximate_contour_by_circle() {local i,n, perim localobj center, x, y, z + x=raw.getrow(0) + y=raw.getrow(1) + z=raw.getrow(2) + perim = 0 + n = x.size + for i = 0, n-1 { + perim += edgelen(raw.getcol(i), raw.getcol((i+1)%n)) + } + center = contourcenter(x, y, z) + if (0) { + $&1 = perim/PI + }else{ + x.sub(center.x[0]).mul(x) + y.sub(center.x[1]).mul(y) + z.sub(center.x[2]).mul(z) +// $&1 = 2*x.add(y).add(z).sqrt.mean + // average of radius based on perim and mean radius of all points + $&1 = x.add(y).add(z).sqrt.mean + perim/(2*PI) + } +// printf("%g %g %g %g\n", center.x[0], center.x[1], center.x[2], $&1) +// printf("perimeter approx = %g actual = %g\n", PI*$&1, perim) + return center +} + +proc mk_stk_triang_vec() {local i, j, n1, n2, d1, d2 localobj i1, i2, trv + trv = new Vector() + $o2.stk_triang_vec = trv + // contour indices are chosen so points 0 cross 1 of a contour from center + // are in +z direction and points 0 between the two contours are + // guaranteed to be an edge. An extra index added to end to close the polygon + // I suppose this could fail if angle does not increase monotonically + stk_contour_indices($o1, i1, $o1.raw.getcol(0)) + stk_contour_indices($o2, i2, $o1.raw.getcol(0)) + i = 0 j = 0 + n1 = i1.size-1 + n2 = i2.size-1 + while(i < n1 || j < n2) { + trv.append(i1.x[i], i2.x[j]) + if (i < n1 && j < n2) { + // which next one is shorter + d1 = ($o1.raw.x[0][i1.x[i]] - $o2.raw.x[0][i2.x[j+1]])^2 + ($o1.raw.x[1][i1.x[i]] - $o2.raw.x[1][i2.x[j+1]])^2 + d2 = ($o1.raw.x[0][i1.x[i+1]] - $o2.raw.x[0][i2.x[j]])^2 + ($o1.raw.x[1][i1.x[i+1]] - $o2.raw.x[1][i2.x[j]])^2 + if (d2 < d1) { + i += 1 + }else{ + j += 1 + } + }else{ + if (i < n1) { + i += 1 + }else{ + j += 1 + } + } + } + trv.append(i1.x[i], i2.x[j]) +} + +proc stk_contour_indices() {local i, d, dmin, imin localobj c, x, y, z + $o2 = new Vector($o1.raw.ncol) + $o2.indgen() + // order the points counterclockwise. ie 0 cross 1 in -z direction + x = $o1.raw.getrow(0) + y = $o1.raw.getrow(1) + z = $o1.raw.getrow(2) + c = contourcenter(x, y, z) + x = $o1.raw.getcol(0).sub(c) + y = $o1.raw.getcol(1).sub(c) + if (x.x[0]*y.x[1] - x.x[1]*y.x[0] > 0) { + $o2.reverse() + } + + // which point is closest to $o3 + imin = -1 + dmin = 1e9 + for i=0, $o2.size - 1 { + d = edgelen($o1.raw.getcol($o2.x[i]), $o3) + if (d < dmin) { + dmin = d + imin = i + } + } + $o2.rotate(-imin) + + $o2.append($o2.x[0]) +} + +proc pl_stk_triang() {local i, j localobj g, m1, m2, trv + g = $o1 + m1 = $o2.xyz + m2 = $o3.xyz + trv = $o3.stk_triang_vec + for i=0, trv.size-1 { + g.beginline(centroid_color, 1) + j = trv.x[i] + g.line(m1.x[0][j], m1.x[1][j]) + i += 1 + j = trv.x[i] + g.line(m2.x[0][j], m2.x[1][j]) + } +} + +func edgelen() { + return sqrt($o1.c.sub($o2).sumsq) +} + +func stk_triang_area1() {local area, i, i1, i2, j1, j2, a, b, c, na localobj m1, m2, trv + area = 0 + m1 = $o1.raw + m2 = $o2.raw + trv = $o2.stk_triang_vec + i1 = trv.x[0] + i2 = trv.x[1] + a = edgelen(m1.getcol(i1), m2.getcol(i2)) + na = 0 + for i=2, trv.size-1 { + j1 = trv.x[i] + i += 1 + j2 = trv.x[i] + b = edgelen(m1.getcol(j1), m2.getcol(j2)) + + // which contour for side c + if (i1 == j1) { + c = edgelen(m2.getcol(i2), m2.getcol(j2)) + }else{ + c = edgelen(m1.getcol(i1), m1.getcol(j1)) + } + + area += accurate_triangle_area(a, b, c) + na += 1 + i1 = j1 + i2 = j2 + a = b + } +//printf("stk_triang_area1 na=%d npoints=%d\n", na, m1.ncol+m2.ncol) + // missing one triangle + return area +} + +func stk_triang_area() {local area, i + area = stk_triang_area1(this, contour_list.object(0)) + for i=1, contour_list.count-1 { + area += stk_triang_area1(contour_list.object(i-1), contour_list.object(i)) + } + return area +} + +// the center of the centroid of the contour stack +obfunc stk_center() {local i, len, th localobj c, centroid, x, y, z, r, lenvec + centroid = new Matrix(3, 1 + contour_list.count) + lenvec = new Vector(centroid.ncol) lenvec.resize(1) + x = raw.getrow(0) + y = raw.getrow(1) + z = raw.getrow(2) + c = contourcenter(x, y, z) + centroid.setcol(0, c) + len = 0 + for i=0, contour_list.count-1 { + r = contour_list.object(i).raw + x = r.getrow(0) + y = r.getrow(1) + z = r.getrow(2) + c = contourcenter(x, y, z) + centroid.setcol(i+1, c) + + len += sqrt(c.sub(centroid.getcol(i)).sumsq) + lenvec.append(len) + } + len = len/2 + if (len == 0) { + c = centroid.getcol(0) + return c + } + i = lenvec.indwhere(">", len) + th = (len - lenvec.x[i-1])/(lenvec.x[i] - lenvec.x[i-1]) + for j=0, 2 { + c.x[j] = th*centroid.x[j][i] + (1 - th)*centroid.x[j][i-1] + } + return c +} + +func accurate_triangle_area() {local x localobj a + // from http://http.cs.berkeley.edu/~wkahan/Triangle.pdf + // W. Kahan + x = float_epsilon + float_epsilon = 0 + a = new Vector(3) a.resize(0) + a.append($1, $2, $3).sort + if ((a.x[0] - (a.x[2] - a.x[1])) < 0) { + float_epsilon = x + execerror("accurate_triangle_area:","not a triangle") + } + float_epsilon = x + x = .25*sqrt((a.x[2]+(a.x[1]+a.x[0])) * (a.x[0]-(a.x[2]-a.x[1])) \ + * (a.x[0]+(a.x[2]-a.x[1])) * (a.x[2]+(a.x[1]-a.x[0]))) + return x +} + +endtemplate Import3d_Section diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d/read_morphml.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d/read_morphml.hoc new file mode 100644 index 0000000..c6801b4 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d/read_morphml.hoc @@ -0,0 +1,78 @@ + +begintemplate Import3d_MorphML +public input, filetype, type, sections, err, parsed +public pt2id, id2pt, pt2sec, sec2pt, label, id2line +objref type, sections, this, p, nil +objref cables, points, cableid2index +strdef filetype, tstr +proc init() { + nrnpython("from neuron.neuroml.rdxml import rdxml") + //print "Import3d_MorphML" + filetype = "MorphML" + p = new PythonObject() +} +proc input() { + //print "Import3d_MorphML.input" + type = new Vector() + sections = new List(1000) + err = 0 + p.rdxml($s1, this) +} +proc parsed() {local i, j, ip, jp localobj cab, sec, pt + cables = $o1.cables_ + points = $o1.points_ + cableid2index = $o1.cableid2index_ + // ptid2pt = $o1.ptid2pt_ + //print $o1, cables.__len__() + for i=0, cables.__len__() - 1 { + cab = cables._[i] + sec = new Import3d_Section(cab.first_, cab.pcnt_) + sections.append(sec) + if (cab.parent_cable_id_ >= 0) { + ip = $o1.cableid2index_[cab.parent_cable_id_] + sec.parentsec = sections.object(ip) + sec.parentx = cab.px_ + } + //print i, cab.id_, cab.name_ + for j=0, cab.pcnt_ - 1 { + jp = cab.first_ + j + pt = points._[jp] + sec.set_pt(j, pt.x_, pt.y_, pt.z_, pt.d_) + } + } +} +func pt2id() { + //print "pt2id ", $1 + if ($1 < 0) { return 0 } + if ($1 >= points.__len__()) { return points.__len__() - 1 } + return $1 +} +func id2pt() { + //print "id2pt ", $1 + return $1 +} +func pt2sec() {local cid, cindex + //print "pt2sec ", $1, " cid=", points._[$1].cid_ + cid = points._[$1].cid_ + cindex = cableid2index._[cid] + //print " cindex=", cindex, " first=", cables._[cindex].first_ + $o2 = sections.object(cindex) + //printf("pt2sec %s\n", $o2) + return $1 - cables._[cindex].first_ +} +func sec2pt() {local i localobj sec + sec = sections.object($1) + //print "sec2pnt ", $1, $2, " secid=", sec.id, " cabid=", cables._[$1].id_ + i = sec.id + $2 - sec.fid + return i +} +func id2line() { + //print "id2line ", $1 + return $1 +} +proc label() {localobj pt + pt = points._[$1] + sprint($s2, "pt[%d] Line %d x=%g y=%g z=%g d=%g", $1, pt.lineno_, pt.x_, pt.y_, pt.z_, pt.d_) +} +endtemplate Import3d_MorphML + diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda.hoc new file mode 100644 index 0000000..9a8e450 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda.hoc @@ -0,0 +1,550 @@ +// Assume that except for soma, the move and line items form a tree +// where, generally, a move is at the same point of the line to which +// it is connected. Under this assumption, all major codes except 1 and 2 +// can be ignored. +// An exception is the [10,5] code for branch point. The next point +// is generally a line (not a move) with the same x,y,z of the branch point. + +begintemplate Import3d_Neurolucida_read +public input, pheader +public type, x, y, z, d, iline, header, point2sec, sections, lines +public label, id2pt, id2line, pt2id, pt2sec, sec2pt, file, filetype, err +public points, pointtype, branchpoints, firstpoints +public helptxt, iline2pt, mark, fillproblist +external hoc_sf_ +objref major, minor, x, y, z, d, iline, header, lines, iline2sec +objref type, pointtype, points, iline2pt +objref file, vectors, sec2point, point2sec, sections +objref firstpoints, branchpoints +objref cursec, diam, nil, gm +objref line_branch_err, parse_err, xyparent_err, xynotnearest_err, noparent_err +objref line_coincide_err, line_branch_err_pt, somabbox_err +strdef tstr, line, filetype +double a[7] + +proc init() { + filetype = "Neurolucida" + vectors = new List() + header = new List() + lines = new List() + gm = new GUIMath() +} + +proc input() { + err = 0 + line_branch_err = new List() + parse_err = new List() + xyparent_err = new List() + xynotnearest_err = new List() + noparent_err = new List() + line_coincide_err = new List() + somabbox_err = new List() + line_branch_err_pt = new Vector() + + rdfile($s1) + find_parents() + repair_diam() + connect2soma() + if (err) { errout() } +} + +proc repair_diam() {local i localobj sec + // I am told, and it seems the case, that + // the first point incorrectly always has the diameter of + // the last point of the previous branch. For this reason + // we set the diameter of the first point to the diameter + // of the second point in the section + for i=0, sections.count-1 { + sec = sections.object(i) + if (sec.parentsec != nil) { + if (sec.first < sec.d.size-1){ + sec.d.x[sec.first] = sec.d.x[sec.first + 1] + } + } + } +} + +proc rdfile() {local i, j + file = new File($s1) + // count lines for vector allocation space (not really necessary) + if (!file.ropen()) { + err = 1 + printf("could not open %s\n", $s1) + } + for (i = 0; !file.eof(); i += 1) { + file.gets(line) + } + file.close() +// printf("%s has %d lines\n", $s1, i) + alloc(i, major, minor, x, y, z, d, iline, pointtype, points) + diam = d + file.ropen() + for (i = 1; !file.eof(); i += 1) { + file.gets(line) + parse(i, line) + } + file.close() + iline2pt = new Vector(iline.x[iline.size-1]) + j = 0 + for i=0, points.size-2 { + while(j <= iline.x[points.x[i]]) { + iline2pt.x[j] = i + j += 1 + } + } + for j=j, iline2pt.size-1 { + iline2pt.x[j] = points.size-1 + } +} + +proc alloc() { local i // $oi.size = 0 but enough space for $1 elements + for i = 2, numarg() { + $oi = new Vector($1) + $oi.resize(0) + vectors.append($oi) + } +} + +func dist() {local x1, y1, z1 + x1 = ($1 - x.x[$4]) + y1 = ($2 - y.x[$4]) + z1 = ($3 - z.x[$4]) + return sqrt(x1*x1 + y1*y1 + z1*z1) +} + +func xydist() {local x1, y1 + x1 = (x.x[$1] - x.x[$2]) + y1 = (y.x[$1] - y.x[$2]) + return sqrt(x1*x1 + y1*y1) +} + +func xysame() { + if ($1 == x.x[$3]) { + if ($2 == y.x[$3]) { + return 1 + } + } + return 0 +} + +proc parse() {local i, n, m + n = sscanf($s2, "[%d,%d] (%f,%f,%f) %f", &a[0], &a[1], &a[2],\ + &a[3], &a[4], &a[5]) + hoc_sf_.left($s2, hoc_sf_.len($s2)-1) + if (n == 6) { + a[5] *= 2 + iline_ = major.size + if (a[0] == 1) { // line + m = major.x[iline_ - 1] + if (m == 10 && minor.x[iline_-1] == 5) { + pointtype.append(0) + points.append(iline_) + if (!xysame(a[2], a[3], iline_-1)) { + err = 1 + line_branch_err_pt.append(points.size-1) +sprint(tstr, "%d: %s separated by %g from branch",\ +$1, $s2, dist(a[2], a[3], a[4], iline_-1)) +line_branch_err.append(new String(tstr)) + } + }else if (m == 1 || m == 2) { + pointtype.append(1) + points.append(iline_) + }else{ + pointtype.append(1) + points.append(iline_) + } + }else if (a[0] == 2) { // move + pointtype.append(0) + points.append(iline_) + }else if (a[0] == 10 && a[1] == 5) { // branch + pointtype.append(2) + points.append(iline_) + }else{ + } + for i=0, 5 { + vectors.object(i).append(a[i]) + } + iline.append($1) // for error messages + lines.append(new String($s2)) + } else if (n == 0) { // comment + header.append(new String($s2)) + } else { + err = 1 + sprint(tstr, "%d: %s parse failure after item %d", $1, $s2, n) + parse_err.append(new String(tstr)) + } +} + +proc mark() {local i, a,b,c,d,e,f + print $o1, $2, iline, lines + i = iline.indwhere("==",$2) + printf("%d,%d: %s\n", i, iline.x[i], lines.object(i).s) + n = sscanf(lines.object(i).s, "[%d,%d] (%f,%f,%f) %f", &a,&b,&c,\ + &d,&e,&f) + if (n == 6) { + print a,b,c,d,e,f + $o1.mark(c,d,"S",12,4,1) + } +} + +proc pheader() {local i + for i=0, header.count-1 { + printf("%s", header.object(i).s) + } +} + +proc find_parents() {local i, j, m, ip, jp, jpmin, d, dmin, xi,yi,zi, bp, ip1 + // we need to associate all pointtype=0 with a branch point (except the + // ones conceptually connected to the soma + // assume the pid is earlier than the pointtype=0 + point2sec = points.c.fill(-1) + branchpoints = pointtype.c.indvwhere("==", 2) + firstpoints = pointtype.c.indvwhere("==", 0) + sections = new List() + type = firstpoints.c.fill(0) + for i=0, firstpoints.size-1 { + ip = points.x[firstpoints.x[i]] + newsec(i) + type.x[i] = cursec.type + xi = x.x[ip] yi = y.x[ip] zi = z.x[ip] + dmin = 1e9 + jpmin = -1 + m = minor.x[ip] + if (m == 41) { // soma start (contour + continue +/* some files use these as branch beginnings so check this after seeing if +there are coincident points. + }else if (m == 1) { // dendrite start + continue + }else if (m == 21) { // axon start + continue + }else if (m == 61) { // apical dendrite start + continue +*/ + } + if (line_branch_err_pt.size) { + j = line_branch_err_pt.x[0] + if (ip == points.x[j]) { + physcon(i, ip, ip-1, j-1) + line_branch_err_pt.remove(0) + continue + } + } + for j=0, branchpoints.size-1 { + jp = points.x[branchpoints.x[j]] + if (ip <= jp) { break } + d = dist(xi, yi, zi, jp) + if (d < dmin) { + bp = branchpoints.x[j] + dmin = d + jpmin = jp + } + } + if (dmin <= 0) { + cursec.parentsec = sections.object(point2sec.x[bp]) + }else if (m == 1) { // dendrite start + continue + }else if (m == 21) { // axon start + continue + }else if (m == 61) { // apical dendrite start + continue + }else{ + err = 1 +sprint(tstr, "%d: %s branch at line %d is %.4g away",\ +iline.x[ip], lines.object(ip).s, iline.x[jpmin], dmin) + d = xydist(ip, jpmin) + if (d <= 0) { // overlay branch point in xy plane? + xyparent_err.append(new String(tstr)) + physcon(i, ip, jpmin, bp) + }else if (ip > 0) { + // sometime it coincides with a previous LineTo + ip1 = firstpoints.x[i]-1 + d = dist(xi, yi, zi, points.x[ip1]) + if (d <= 0) { +sprint(tstr, "%s\n but coincides with line %d", tstr, iline.x[points.x[ip1]]) + line_coincide_err.append(new String(tstr)) + cursec.parentsec = sections.object(point2sec.x[ip1]) + }else if (try_xy_coincide(i, ip)){ + xynotnearest_err.append(new String(tstr)) + }else{ + noparent_err.append(new String(tstr)) + } + } + } + } +} + +func try_xy_coincide() {local j, jp, d + // sometimes it coincides in the xy plane with a branch point + // even though it is not the nearest point and therefore we + // assume that is the parent point + for j=0, branchpoints.size-1 { + jp = points.x[branchpoints.x[j]] + if ($2 <= jp) { break } + d = xydist($2, jp) + if (d <= 0) { +sprint(tstr, "%s\n but coincides with branch point at line %d", tstr, iline.x[jp]) + bp = branchpoints.x[j] + physcon($1, $2, jp, bp) + return 1 + } + } + return 0 +} + +proc physcon() { + cursec.parentsec = sections.object(point2sec.x[$4]) + cursec.insrt(0, x.x[$3], y.x[$3], z.x[$3], d.x[$2]) + cursec.id -= 1 +} + +proc newsec() {local i, ip, n, m, first, isec + first = firstpoints.x[$1] + ip = points.x[first] + if ($1 < firstpoints.size-1) { + n = firstpoints.x[$1+1] - first + }else{ + n = points.size - first + } + cursec = new Import3d_Section(first, n) + isec = sections.count + sections.append(cursec) + for i = 0, n-1 { + cursec.append(i, points.x[i+first], 1, x, y, z, d) + point2sec.x[i+first] = isec + } + m = minor.x[ip] + if (m == 1 || m == 2) { // dendrite + cursec.type = 3 + }else if (m == 21 || m == 22) { //axon + cursec.type = 2 + }else if (m == 41 || m == 42) { // soma + cursec.type = 1 + cursec.iscontour_ = 1 + }else if (m == 61 || m == 62) { // apdendrite + cursec.type = 4 + }else{ + err = 1 +printf("%s line %d: don't know section type: %s\n",\ + file.getname, iline.x[ip], lines.object(ip).s) + } +} + +proc connect2soma() {local i, ip, j, jp, bp, jpmin, dmin, d, xmin, xmax, ymin, ymax localobj soma, sec, xc, yc, zc, c, psec, r + // find centroid of soma if outline and connect all dangling + // dendrites to that if inside the contour + for i=0, sections.count-1 { + sec = sections.object(i) + if (sec.type == 1 && sec.iscontour_ == 1) { + soma = sec + sections.remove(i) + sections.insrt(0, soma) + break + } + } + if (soma == nil) { return } + xc = soma.raw.getrow(0) + yc = soma.raw.getrow(1) + zc = soma.raw.getrow(2) + xmin = xc.min-.5 xmax = xc.max + .5 + ymin = yc.min-.5 ymax = yc.max + .5 + c = soma.contourcenter(xc, yc, zc) + for i=0, sections.count-1 { + sec = sections.object(i) + if (sec.parentsec == nil && sec != soma) { + if (gm.inside(sec.raw.x[0][0], sec.raw.x[1][0], xmin, ymin, xmax, ymax)) { + sec.parentsec = soma + sec.parentx = .5 + sec.insrt(0, c.x[0], c.x[1], c.x[2], .01) + sec.id -= 1 + sec.first = 1 + }else{ + // is same as end point of earlier section? + ip = points.x[sec2pt(i, 0)] + d = 1e9 + for j=0, i-1 { + psec = sections.object(j) + jp = psec.d.size-1 + r = psec.raw + d = dist(r.x[0][jp], r.x[1][jp], r.x[2][jp], ip) + if (d == 0) { + sec.parentsec = psec + break + } + } + if (d == 0) { continue } + ip = points.x[sec2pt(i, 0)] + dmin = dist(c.x[0], c.x[1], c.x[2], ip) + jpmin = -1 + for j=0, branchpoints.size-1 { + jp = points.x[branchpoints.x[j]] + if (ip <= jp) { break } + d = dist(x.x[ip], y.x[ip], z.x[ip], jp) + if (d < dmin) { + bp = branchpoints.x[j] + dmin = d + jpmin = jp + } + } + err = 1 +sprint(tstr, "%d: %s is outside soma, logically connect to", iline.x[ip], lines.object(ip).s) + if (jpmin == -1) { + sprint(tstr, "%s soma", tstr) + sec.parentsec = soma + sec.insrt(0, c.x[0], c.x[1], c.x[2], .01) + sec.id -= 1 + }else{ + jp = jpmin + sprint(tstr, "%s %d", tstr, iline.x[jp]) + sec.parentsec = sections.object(point2sec.x[bp]) + sec.insrt(0, x.x[jp], y.x[jp], z.x[jp], .01) + sec.id -= 1 + } + sec.first = 1 + somabbox_err.append(new String(tstr)) + } + } + } +} + +// note selpoint defined in swc_gui.hoc as sec.id + j +// selpoint is the points index +// ie. the first points of the sections are firstpoints +proc label() {local i + i = points.x[$1] + sprint($s2, "Line %d: %s", iline.x[i], lines.object(i).s) +} +func id2pt() { + if ($1 < 0) { return -1 } + if ($1 >= iline2pt.size) { return iline2pt.x[iline2pt.size-1]} + return iline2pt.x[$1] +} +func id2line() { return points.x[$1] } +func pt2id() { + if ($1 < 0) {return -1} + return iline.x[points.x[$1]] +} +func pt2sec() {local i + i = firstpoints.indwhere(">", $1) + if (i == -1) { + i = firstpoints.size + } + $o2 = sections.object(i-1) + j = $1 - $o2.id + return j +} +func sec2pt() { +//print "sec2pt ", $1, $2, sections.object($1).id + return sections.object($1).id + $2 +} + +proc helptxt() { + xpanel("Neurolucida file filter characteristics") +xlabel(" The only lines utilized are [1,x], [2,x], and [5,10]. i.e , LineTo,") +xlabel("MoveTo, and Branch lines. ") +xlabel(" Sections generally consist of MoveTo followed by sequence of LineTo,") +xlabel("and possibly ending with Branch. Intervening lines of other major types") +xlabel("are ignored. ") +xlabel(" The type of the section (dendrite, axon, soma outline, or apical) is") +xlabel("determined by the minor code of the first point in the branch. ") +xlabel(" Coincidence of the first x,y,z point of a section with the last") +xlabel("(branch) point of some section defines a connection between child and") +xlabel("parent section. However most files contain errors and the following") +xlabel("heuristics are applied to the first points of problem sections when the") +xlabel("parent is not obvious. EACH PROBLEM POINT SHOULD BE EXAMINED to") +xlabel("determine if the correction is suitable. ") +xlabel(" 1) The first point after a Branch point is a MoveTo which is") +xlabel("coincident in the xy plane but not in the z axis. A physical connection") +xlabel("is made with the diam of the MoveTo. ") +xlabel(" 2) The nearest branch point is coincident in the xy plane. A physical") +xlabel("connection is made with the diam of the MoveTo.") +xlabel(" 3) There is no coincident branchpoint in the xy plane but the MoveTo") +xlabel("is 3-d coincident with the preceding LineTo point. A logical connection") +xlabel("is made to the section containing the LineTo point.") +xlabel(" 4) There is an xy plane coincident branch point but it is not the") +xlabel("nearest in a 3-d sense. A physical connection is made to the section") +xlabel("containing the xy plane coincident point. ") +xlabel(" 5) The first point of the branch is not a soma, dendrite, axon, or") +xlabel("apical start point and there is no xy plane coincident branch point. ") +xlabel("The branch remains unattached (but see heuristic 6). ") +xlabel(" 6) All unattached branches within 0.5 microns of the soma contour") +xlabel("bounding box are logically connected to the soma contour section. ") +xlabel("I am told, and it seems to be the case, that the first point in a") +xlabel("branch always has a diameter value of the last point in the previous") +xlabel("branch. For this reason we set the first point to the diameter of") +xlabel("of the second point in each section that has a parent branch.") +xlabel("If this is not the right thing to do then comment out the call to") +xlabel("repair_diam() in the input() procedure of read_nlcda.hoc") + xpanel(1) +} + +proc errout() {local i + printf("\n%s problems and default fixes\n\n", file.getname) + if (parse_err.count) { + printf(" Following lines could not be parsed\n") + for i=0, parse_err.count-1 { + printf(" %s\n", parse_err.object(i).s) + } + printf("\n") + } + if (line_branch_err.count) { +printf(" LINETO follows branch and does not coincide in the xy plane.\n") +printf(" Make a physical connection using the LINETO diameter.\n") + for i = 0, line_branch_err.count-1 { + printf(" %s\n", line_branch_err.object(i).s) + } + printf("\n") + } + if (xyparent_err.count) { + printf(" Nearest branch point is coincident in xy plane.\n Make a physical connection with diam of the MOVETO\n") + for i=0, xyparent_err.count-1 { + printf(" %s\n", xyparent_err.object(i).s) + } + printf("\n") + } + if (line_coincide_err.count) { + printf(" No coincident branchpoint in xy plane but 3-d coincident to previous LINETO.\n") + printf(" point. Make a logical connection to the section containing that LINETO\n") + for i=0, line_coincide_err.count-1 { + printf(" %s\n", line_coincide_err.object(i).s) + } + printf("\n") + } + if (xynotnearest_err.count) { + printf(" The xy plane coincident branch point is not the nearest in the 3-d sense.\n") + printf(" However we connect physically to the indicated xy coincident branch point\n") + for i=0, xynotnearest_err.count-1 { + printf(" %s\n", xynotnearest_err.object(i).s) + } + printf("\n") + } + if (noparent_err.count) { + printf(" Cannot figure out which is the parent\n") + printf(" No coincident (even in xy plane) branch point.\n") + for i=0, noparent_err.count-1 { + printf(" %s\n", noparent_err.object(i).s) + } + printf("\n") + } + if (somabbox_err.count) { + printf(" Unconnected branch is more than .5 microns outside the soma bounding box.\n") + printf(" Connect logically to nearest branch point\n") + for i=0, somabbox_err.count-1 { + printf(" %s\n", somabbox_err.object(i).s) + } + printf("\n") + } +} + +proc fillproblist() { + fillproblist1($o1, parse_err, line_branch_err, xyparent_err, line_coincide_err, xynotnearest_err, noparent_err, somabbox_err) +} +proc fillproblist1() { local i, j + for i=2, numarg() { + for j=0, $oi.count-1 { + $o1.append($oi.object(j)) + } + } +} + +endtemplate Import3d_Neurolucida_read diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda3.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda3.hoc new file mode 100644 index 0000000..0402dbb --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d/read_nlcda3.hoc @@ -0,0 +1,1194 @@ +// Read neurolucida +// ; V3 text file written for MicroBrightField products. +// file. +// The format is given by a context free grammar that would be easy +// to handle with lex/yacc but we can do reasonably well using recursive descent +// that more or less matches each production rules for the grammar. +// Presently we only handle contours and trees but with spines ignored. + +begintemplate Branch2SomaInfo +// info to carry out decision about which to connect to for +// possible root branch mistakes +// may have to split the parent +public sec, sindex, pbranch, ipoint, d2p, d2s, connected2p +objref sec, pbranch +proc init() { + sec = $o1 + pbranch = $o2 + sindex = $3 + d2p = $4 + d2s = $5 + ipoint = $6 + connected2p = 0 +} +endtemplate Branch2SomaInfo + +begintemplate Import3d_LexToken +public token, x, s, itok, iline, clone +strdef s +token = 0 +x = 0 +itok = 0 +iline = 0 +obfunc clone() { localobj r + r = new Import3d_LexToken() + r.s = s + r.token = token + r.x = x + r.itok = itok + r.iline = iline + return r +} +endtemplate Import3d_LexToken + +begintemplate Import3d_Neurolucida3 +public type +public filetype, input, file, sections +public label, id2pt, id2line, pt2id, pt2sec, sec2pt, helptxt, mark, err, b2spanel +public x, y, z, d, iline, lines, quiet +external hoc_sf_ +objref type, firstpoints, gm, plist +objref current, look_ahead, look_ahead2 +objref file, tokens, sections, cursec, parentsec, nil +objref x, y, z, d, iline, lines +objref somas, centers, b2serr, b2sinfo +strdef line, tstr, tstr2, filetype, fline + +proc init() { + quiet = 0 + debug_on = 0 + gm = new GUIMath() + filetype = "Neurolucida V3" + current = new Import3d_LexToken() + look_ahead = new Import3d_LexToken() + look_ahead2 = new Import3d_LexToken() + eof=0 + number=1 leftpar=2 rightpar=3 comma=4 bar=5 + set=6 rgb=7 string=8 label_=9 err_=10 + leftsp=11 rightsp=12 + tokens = new List() + tokensappend("eof", "number", "leftpar", "rightpar", "comma", "bar") + tokensappend("set", "rgb", "string", "label", "err") + tokensappend("leftsp", "rightsp") + plist = new List() +} +proc tokensappend() {local i + for i=1, numarg() { + tokens.append(new String($si)) + } +} + +proc input() { + b2serr = new List() + b2sinfo = new List() + nspine = 0 + err = 0 + type = new Vector() + sections = new List(1000) + alloc(25000, x, y, z, d, iline) + lines = new List(25000) + itoken = 0 + depth = 0 + rdfile($s1) + firstpoints = new Vector(sections.count) + set_firstpoints() + connect2soma() + if (err) { errout() } +} + +proc set_firstpoints() {local i + firstpoints.resize(sections.count) + for i=0, sections.count-1 { + firstpoints.x[i] = sections.object(i).id + } +} + +proc alloc() {local i + for i=2, numarg() { + $oi = new Vector($1) + $oi.resize(0) + } +} +proc connect2soma() {local i, j, d, dmin localobj sec, roots, xx + // first make sure all somas are at the beginning + centers = new List() + j = 0 // next soma index + somas = new List() + roots = new List() + for i=0, sections.count-1 { + sec = sections.object(i) + if (sec.iscontour_) { + if (i > j) { + sections.remove(i) + sections.insrt(j, sec) + } + somas.append(sec) + j += 1 + } + } + // mark the soma contours that are part of a + // contour stack and link them into a list + // that is in the main contour section. + // we do not remove them from the sections since + // we want to be able to select their points + soma_contour_stack() + for i=0, sections.count-1 { + sec = sections.object(i) + if (!sec.iscontour_) if (sec.parentsec == nil) { + roots.append(sec) + } + } + if (somas.count == 0) { return } + // note that j is the number of soma's + for i = 0, somas.count-1 { + connect2soma_2(somas.object(i), roots) + } + for i=0, roots.count-1 { + sec = roots.object(i) + xx = sec.raw.getcol(0) + dmin = 1e9 + for j=0, centers.count-1 { + d = xx.c.sub(centers.object(j)).mag + if (d < dmin) { + imin = j + dmin = d + } + } + err = 1 + xx = centers.object(imin) + sprint(tstr, "\nMain branch starting at line %d is outside the soma bounding boxes", pt2id(sec.id)) + b2serr.append(new String(tstr)) + sprint(tstr, " Making a logical connection to center of nearest soma") + b2serr.append(new String(tstr)) + sec.parentsec = somas.object(imin) + sec.parentx = .5 + sec.insrt(0, xx.x[0], xx.x[1], xx.x[2], .01) + sec.first = 1 + sec.fid = 1 + opt_connect(sec, imin, dmin) + } +} + +proc soma_contour_stack() {local i, j localobj bb1, bb2, first, next + // if soma contour bounding boxes overlap, treat as single soma + if (somas.count == 0) return + first = somas.object(0) + bb1 = bounding_box(first) + j = 0 + for i = 1, somas.count-1 { + j += 1 + next = somas.object(j) + bb2 = bounding_box(next) + if (xy_intersect(bb1, bb2)) { + if (!object_id(first.contour_list)) { + first.contour_list = new List() + } + first.contour_list.append(next) + next.is_subsidiary = 1 + somas.remove(j) + j -= 1 + }else{ + first = next + } + bb1 = bb2 + } + for i=0, somas.count-1 { + somastack_makes_sense(somas.object(i)) + somastack_process(somas.object(i)) + } +} + +obfunc bounding_box() {localobj bb + bb = new Vector(6) + bb.x[0] = $o1.raw.getrow(0).min + bb.x[1] = $o1.raw.getrow(1).min + bb.x[2] = $o1.raw.getrow(2).min + bb.x[3] = $o1.raw.getrow(0).max + bb.x[4] = $o1.raw.getrow(1).max + bb.x[5] = $o1.raw.getrow(2).max + return bb +} + +func xy_intersect() {local i + for i = 0, 1 { +if ($o1.x[i] > $o2.x[3+i] || $o2.x[i] > $o1.x[3+i]) { return 0 } + } + return 1 +} + +proc somastack_makes_sense() {local i, j, z, z2, dz, dz2 localobj sec + if (!object_id($o1.contour_list)) { return } + // the soma stack must be monotonic in the z axis and all points + // on a contour must have same z value. + z = $o1.raw.x[2][0] + for i = 1, $o1.raw.ncol-1 if (z != $o1.raw.x[2][i]) { + sprint(tstr, "Soma stack contour %s does not have constant z value.", $o1) + b2serr.append(new String(tstr)) + b2serr.append(new String(" Soma area calculation may be serious in error.")) + return + } + dz = 0 + for j=0, $o1.contour_list.count-1 { + sec = $o1.contour_list.object(j) + z2 = sec.raw.x[2][0] + dz2 = z2 - z + if (dz2 == 0) { + sprint(tstr, "Adjacent contour %d of soma stack %s has same z coordinate and previous.", j, $o1) + b2serr.append(new String(tstr)) + return + }else if (dz2 > 0) { + dz2 = 1 + }else{ + dz2 = -1 + } + if (dz == 0) { + dz = dz2 + }else if (dz != dz2) { + sprint(tstr, "Contour %d of the Soma stack %s is not monotonic in z.", j, $o1) + b2serr.append(new String(tstr)) + b2serr.append(new String(" Manually edit the neurolucida file and reorder or eliminate some contours.")) + b2serr.append(new String(" Presently the soma surface is nonsense.")) + return + } + z = z2 + for i = 1, sec.raw.ncol-1 if (z != sec.raw.x[2][i]) { + sprint(tstr, "contour %d of the Soma stack %s does not have constant z value.", j, $o1) + b2serr.append(new String(tstr)) + b2serr.append(new String(" Soma area calculation may be serious in error.")) + return + } + } +} + +proc somastack_process() {local i, n, n1 localobj pts, m, center, pv + if (!object_id($o1.contour_list)) { return } + printf("somastack_process %d\n", $o1.contour_list.count + 1) + // The stack defines a volume. Determine the principle axes + // and slice the volume along the major axis, approximating + // each slice by a circle and shifting the circle to be + // along the major axis. So the set of soma contours ends + // up being one straight cylindrically symetric soma centroid + // note then that curved carrots don't look quite right but + // straight carrots do. + + // for each contour use 100 points equally spaced. + // we should, but do not, make the stack equally spaced. + // then all the points are used to find the principle axes + // this pretty much follows the corresponding analysis in + // Import3d_GUI + // Heck. Let's just use all the contour points and approximate + // the thing as an ellipsoid + + // copy all the centroids into one matrix + // size of matrix + n = $o1.raw.nrow + for i=0, $o1.contour_list.count-1 { n += $o1.contour_list.object(i).raw.nrow} + pts = new Matrix(3, n) + n = 0 + n1 = $o1.raw.nrow + $o1.raw.bcopy(0, 0, 3, n1, 0, n, pts) + n = n1 + for i=0, $o1.contour_list.count-1 { + n1 = $o1.contour_list.object(i).raw.nrow + $o1.contour_list.object(i).raw.bcopy(0, 0, 3, n1, 0, n, pts) + n += n1 + } + center = new Vector(3) + for i=0, 2 { center.x[i] = pts.getrow(i).mean } + printf("center\n") center.printf + + //principle axes + m = new Matrix(3,3) + for i=0, 2 { pts.setrow(i, pts.getrow(i).sub(center.x[i])) } + for i=0, 2 { + for j=i, 2 { + m.x[i][j] = pts.getrow(i).mul(pts.getrow(j)).sum + m.x[j][i] = m.x[i][j] + } + } + pv = m.symmeig(m) + printf("Principle values\n") pv.printf() + printf("Principle axes\n") m.printf() +} + +proc stk_bbox() {local i, j localobj bbs, bbc + bbs = bounding_box($o1) + for i=0, $o1.contour_list.count-1 { + bbc = bounding_box($o1.contour_list.o(i)) + for j=0, 2 { + if (bbs.x[j] > bbc.x[j]) bbs.x[j] = bbc.x[j] + if (bbs.x[j+3] < bbc.x[j+3]) bbs.x[j+3] = bbc.x[j+3] + } + } + $&2 = bbs.x[0] $&3 = bbs.x[3] $&4 = bbs.x[1] $&5 = bbs.x[4] +} + +proc connect2soma_2() {local i, xmin, xmax, ymin, ymax localobj sec, xc, yc, zc, center + // find centroid of soma if outline and connect all dangling + // dendrites to that if inside the contour + if (object_id($o1.contour_list)) { + center = $o1.stk_center() + stk_bbox($o1, &xmin, &xmax, &ymin, &ymax) + }else{ + xc = $o1.raw.getrow(0) + yc = $o1.raw.getrow(1) + zc = $o1.raw.getrow(2) + xmin = xc.min-.5 xmax = xc.max + .5 + ymin = yc.min-.5 ymax = yc.max + .5 + center = $o1.contourcenter(xc, yc, zc) + } + centers.append(center) + + for (i=$o2.count-1; i >= 0; i -= 1) { + sec = $o2.object(i) + if (gm.inside(sec.raw.x[0][0], sec.raw.x[1][0], xmin, ymin, xmax, ymax)) { + sec.parentsec = $o1 + sec.parentx = .5 + sec.insrt(0, center.x[0], center.x[1], center.x[2], .01) + sec.first = 1 + sec.fid = 1 + $o2.remove(i) + } + } +} + +proc opt_connect() {local i, j, d, dmin, imin, n, ip localobj psec, xx + dmin = 1e9 + xx = $o1.raw.getcol(1) + for i=0, sections.count - 1 { + psec = sections.object(i) + if (psec == $o1) { break } + n = psec.raw.ncol + for j=0, n-1 { + d = xx.c.sub(psec.raw.getcol(j)).set(2,0).mag + if (d < dmin) { + dmin = d + imin = i + ip = j + } + } + } + if (dmin == 1e9) { return } + psec = sections.object(imin) +// if (dmin < psec.d.x[psec.d.size-1]) { + if (dmin < $3) { + b2sinfo.append(new Branch2SomaInfo($o1, psec, $2, dmin, $3, ip)) + } +} + +proc b2spanel() {local i localobj b2s + if (b2sinfo.count == 0) { return } + xpanel("Possible root branch errors") + xlabel("Default logical connection to nearest soma.") + xlabel("Check to physically connect to closest parent") + xlabel(" in the xy plane.") + xlabel(" (Note: may split the parent into two sections)") + for i=0, b2sinfo.count -1 { + b2s = b2sinfo.object(i) +sprint(tstr, "Line #%d connect to #%d %g (um) away", pt2id(sec2pto(b2s.sec, 1)), \ +pt2id(sec2pto(b2s.pbranch, b2s.ipoint)), b2s.d2p) +sprint(tstr2, "b2soption_act(%d, \"%s\")", i, $o1) + xcheckbox(tstr, &b2s.connected2p(), tstr2) + } + xpanel() +} + +proc b2soption_act() {local i localobj b2s, sec, parent, soma, xx + b2s = b2sinfo.object($1) + sec = b2s.sec + soma = somas.object(b2s.sindex) + parent = b2s.pbranch + if (sec.parentsec == soma) { // connect to parent + if (b2s.ipoint != parent.raw.ncol-1) { // need to split + b2soption_split(b2s) + parent = b2s.pbranch + set_firstpoints() + } + xx = parent.raw.getcol(b2s.ipoint) + sec.parentsec = parent + sec.parentx = 1 + sec.raw.setcol(0, xx) + sec.d.x[0] = sec.d.x[1] + sec.first = 0 + sec.fid = 1 + }else{ // connect to soma + xx = centers.object(b2s.sindex) + sec.parentsec = soma + sec.parentx = .5 + sec.raw.setcol(0, xx) + sec.d.x[0] = .01 + sec.first = 1 + sec.fid = 1 + } + sprint(tstr, "%s.redraw()", $s2) + execute(tstr) +} + +proc b2soption_split() {local i, n, id, ip localobj p, newsec, tobj + p = $o1.pbranch + ip = $o1.ipoint + id = sec2pto(p, ip) + n = p.raw.ncol + newsec = new Import3d_Section(p.id, ip+1) + p.id = id + + tobj = p.raw.c + tobj.bcopy(0,0,3,ip+1,newsec.raw) + p.raw.resize(3, n - ip) + p.xyz.resize(3, n - ip) + tobj.bcopy(0, ip, 3, n - ip, p.raw) + + tobj = p.d.c + newsec.d.copy(tobj, 0, ip) + p.d.resize(n - ip) + p.d.copy(tobj, ip, n-1) + + newsec.parentsec = p.parentsec + p.parentsec = newsec + newsec.parentx = p.parentx + p.parentx = 1 + newsec.type = p.type + newsec.first = p.first + newsec.fid = p.fid + p.first = 0 + p.fid = 0 + newsec.type = p.type + $o1.pbranch = newsec + $o1.ipoint = newsec.d.size-1 + // now adjust any screwed up b2sinfo items that also reference p + for i=0, b2sinfo.count-1 { + tobj = b2sinfo.object(i) + if (tobj == $o1) { continue } + if (tobj.pbranch == p) { + if (tobj.ipoint <= ip) { // on newsec + tobj.pbranch = newsec + }else{ // still on p + tobj.ipoint -= ip + } + } + } + sections.insrt(sections.index(p), newsec) +} + +func lex() {local n + $o1.x = 0 + $o1.s = "" + while (hoc_sf_.len(line) <= 1 || sscanf(line, " ;%[^@]", line) == 1) { + if (!getline(fline)) { + $o1.token = eof + itoken += 1 + $o1.itok = itoken + $o1.iline = iline_ + return eof + } + line = fline + hoc_sf_.left(fline, hoc_sf_.len(fline)-1) + } + if (sscanf(line, " %lf%[^@]", &$o1.x, line) == 2) { + $o1.token = number + }else if (sscanf(line, " (%[^@]", line) == 1) { + $o1.token = leftpar + }else if (sscanf(line, " )%[^@]", line) == 1) { + $o1.token = rightpar + }else if (sscanf(line, " ,%[^@]", line) == 1) { + $o1.token = comma + }else if (sscanf(line, " |%[^@]", line) == 1) { + $o1.token = bar + }else if (sscanf(line, " <%[^@]", line) == 1) { + $o1.token = leftsp + }else if (sscanf(line, " >%[^@]", line) == 1) { + $o1.token = rightsp + }else if (sscanf(line, " set %[^@]", line) == 1) { + $o1.token = set + }else if (sscanf(line, " Set %[^@]", line) == 1) { + $o1.token = set + }else if (sscanf(line, " SET %[^@]", line) == 1) { + $o1.token = set + }else if (sscanf(line, " RGB %[^@]", line) == 1) { + $o1.token = rgb + }else if ((n = sscanf(line, " \"%[^\"]\"%[^@]", $o1.s, line)) > 0) { + // not allowing quotes in quote + $o1.token = string + if (n == 1) { + printf("Lexical error: no closing '\"' in string. The entire line %d in ||is\n", iline_) + printf("|%s|\n", fline) + line = "" + $o1.token = err_ + } + }else if (sscanf(line, " %[A-Za-z0-9_]%[^@]", $o1.s, line) == 2) { + $o1.token = label_ + }else{ + $o1.token = err_ + } + itoken += 1 + $o1.itok = itoken + $o1.iline = iline_ + return $o1.token +} + +func getline() { + if (file.eof) { + if (!quiet) { + printf("\r%d lines read\n", iline_) + } + return 0 + } + file.gets($s1) + iline_ += 1 +// printf("%d: %s", iline_, $s1) + if ((iline_%1000) == 0) { + if (!quiet) { + printf("\r%d lines read", iline_) + } + } + return 1 +} + +proc rdfile() {local i + iline_ = 0 + file = new File($s1) + if (!file.ropen()) { + err = 1 + printf("could not open %s\n", $s1) + } + for (i=0; !file.eof(); i += 1) { + file.gets(line) + } + alloc(i, x, y, z, d, iline) + file.close + lines = new List(25000) + line="" + if (!quiet) { + printf("\n") + } + file.ropen() + p_file() + file.close +} + +objref rollback + +proc save_for_rollback() { + if (object_id(rollback)) { + printf("rollback in use\n") + p_err() + } + rollback = new List() + rollback.append(current.clone()) + rollback.append(look_ahead.clone()) + rollback.append(look_ahead2.clone()) + use_rollback_ = 0 +} +proc use_rollback() { + use_rollback_ = 1 + current = rollback.o(0) rollback.remove(0) + look_ahead = rollback.o(0) rollback.remove(0) + look_ahead2 = rollback.o(0) rollback.remove(0) + if (rollback.count == 0) {clear_rollback()} +} +proc clear_rollback() {localobj nil + rollback = nil + use_rollback_ = 0 +} + +proc read_next_token() { + if (use_rollback_) { + current = look_ahead + look_ahead = look_ahead2 + look_ahead2 = rollback.o(0) + rollback.remove(0) + if (rollback.count == 0) { + clear_rollback() + } + }else{ + read_next_token_lex() + if (object_id(rollback)){ + rollback.append(look_ahead2.clone()) + } + } +} +proc read_next_token_lex() {localobj tobj + tobj = current + current = look_ahead + look_ahead = look_ahead2 + look_ahead2 = tobj + if (look_ahead.token != eof) { + lex(look_ahead2) + }else{ + look_ahead2.token = eof + } +// printf("current token=%s x=%g s=%s\n", tokens.object(current.token).s, current.x, current.s) +} + +func need_extra() {local i, n localobj m + if (parentsec == nil) { return 0 } + m = parentsec.raw + n = m.ncol-1 + if ( m.x[0][n] == x.x[$1]) { + if ( m.x[1][n] == y.x[$1]) { + if ( m.x[2][n] == z.x[$1]) { + return 0 + } + } + } + return 1 +} +proc newsec() {local i, n, first, n1 localobj m + first = 0 + n = $2 - $1 + if (need_extra($1)) { + cursec = new Import3d_Section($1, n+1) + first = 1 + cursec.fid = 1 + m = parentsec.raw + n1 = m.ncol-1 + cursec.set_pt(0, m.x[0][n1], m.x[1][n1], m.x[2][n1], d.x[$1]) + }else{ + cursec = new Import3d_Section($1, n) + } + cursec.type = sectype + type.append(sectype) + sections.append(cursec) + cursec.append(first, $1, n, x, y, z, d) +} +proc set_sectype() {localobj tobj + sectype = 0 + if (plist.count) { + tobj = plist.object(plist.count-1) + if (strcmp(tobj.s, "Axon") == 0) { + sectype = 2 + }else if (strcmp(tobj.s, "Dendrite") == 0) { + sectype = 3 + }else if (strcmp(tobj.s, "Apical") == 0) { + sectype = 4 + } + } +} + +proc label() { + sprint($s2, "Line %d: %s", iline.x[$1], lines.object($1).s) +} +func id2pt() {local i + i = iline.indwhere(">=", $1) + if (i < 0) { i = iline.size-1 } + return i +} +func id2line() { return $1 } +func pt2id() {local i + i = $1 + if (i < 0) { i == 0 } + if (i >= iline.size) { i = iline.size-1 } + return iline.x[i] +} +func pt2sec() {local i, j + i = firstpoints.indwhere(">", $1) + if (i == -1) { + i = firstpoints.size + } + $o2 = sections.object(i-1) + j = $1 - $o2.id + $o2.fid + return j +} +func sec2pt() {local i localobj sec + sec = sections.object($1) + i = sec.id + $2 - sec.fid + return i +} +func sec2pto() {local i localobj sec + sec = $o1 + i = sec.id + $2 - sec.fid + return i +} +proc mark() {local i + print $o1, $2, iline, lines + i = iline.indwhere("==", $2) + if (i != -1) { + printf("%d,%d,%d (%g,%g): %s\n", $2, iline.x[i], i, x.x[i], y.x[i], lines.object(i).s) + $o1.mark(x.x[i], y.x[i], "S",12,4,1) + } +} + +proc helptxt() { + xpanel("Neurolucida V3 file filter characteristics") +xlabel("The elaborate file format is handled by a reasonably complete") +xlabel("recursive descent parser that more or less matches the production") +xlabel("rules for the grammar. However, at present, only contours and trees") +xlabel("are given any semantic actions (in particular, spines are ignored).") + xpanel(1) +} + +proc chk() { + if (current.token != $1) { p_err() } +} +proc demand() { + read_next_token() + chk($1) +} +proc pcur() { + printf("itok=%d on line %d token=%s x=%g s=%s\n", current.itok, current.iline, tokens.object(current.token).s, current.x, current.s) +} +proc plook() { +// printf("lookahead: itok=%d token=%s x=%g s=%s\n", look_ahead.itok, tokens.object(look_ahead.token).s, look_ahead.x, look_ahead.s) +} +proc enter() {local i + if (debug_on == 0) {return} + for i=1, depth {printf(" ")} + printf("enter %s: ", $s1) + pcur() + depth += 1 +} +proc leave() {local i + if (debug_on == 0) {return} + depth -= 1 + for i=1, depth {printf(" ")} + printf("leave %s: ", $s1) + pcur() +} +// p stands for production if needed to avoid conflict with variable +proc p_file() { + look_ahead2.token = eof + look_ahead.token = eof + if (lex(current) != eof) { + if (lex(look_ahead) != eof) { + lex(look_ahead2) + } + } + enter("p_file") + objects() + leave("p_file") +} +proc objects() { + enter("objects") + object() + while(1) { + optionalcomma() + if (current.token != leftpar) { + break + } + object() + } + leave("objects") +} +proc object() {local i + i = current.itok + enter("object") + if (current.token == leftpar) { + plook() + if (look_ahead.token == string) { + contour() + }else if (look_ahead.token == label_) { + marker_or_property() + }else if (look_ahead.token == leftpar) { + tree_or_text() + }else if (look_ahead.token == set) { + p_set() + }else{ + p_err() + } + }else{ + p_err() + } + leave("object") + if (i == current.itok) { + print "internal error: ", "object consumed no tokens" + stop + } +} +proc marker_or_property() { + enter("marker_or_property") + if (look_ahead2.token == leftpar) { + marker() + }else{ + property() + } + leave("marker_or_property") +} +proc tree_or_text() { + // the tree and text productions are poorly conceived since they + // match each other for arbitrarily long sequences of Properties tokens. + // And after the properties they both have a Point. + // For now just assume it is a tree. + // It will be painful to consume the [ '(' Properties Point ] here + // and then disambiguate between Tree or Text and then more + // often than not, start the tree production after having already + // read the first point (Branch currently assumes it is supposed + // to read the first point of the tree.) + enter("tree_or_text") + save_for_rollback() + if (text()) { + clear_rollback() + }else{ + use_rollback() + tree() + } + leave("tree_or_text") +} +proc properties() { + enter("properties") + plist.remove_all() + if (current.token == leftpar) { + if(look_ahead.token == label_ || look_ahead.token == set) { + property_or_set() + while (1) { + optionalcomma() +if (current.token != leftpar || (look_ahead.token != label_ && look_ahead.token != set)) { + break + } + property_or_set() + } + } + } + leave("properties") +} +proc property_or_set() { + if (look_ahead.token == label_) { + property() + }else{ + p_set() + } +} +proc property() { + enter("property") + chk(leftpar) + demand(label_) + plist.append(new String(current.s)) + read_next_token() + optionalvalues() + chk(rightpar) + read_next_token() + leave("property") +} +proc optionalvalues() {local c + enter("optionalvalues") + c = current.token + if (c == number || c == string || c == label_ || c == rgb) { + values() + } + leave("optionalvalues") +} +proc values() {local c + enter("values") + value() + while (1) { + c = current.token + if (c != number && c != string && c != label_ && c != rgb) { + break + } + value() + } + leave("values") +} +proc value() {local c + enter("value") + c = current.token + if (c == number) { + }else if (c == string) { + }else if (c == label_) { + }else if (c == rgb) { + demand(leftpar) + demand(number) + read_next_token() + optionalcomma() + chk(number) + read_next_token() + optionalcomma() + chk(number) + demand(rightpar) + }else{ + p_err() + } + read_next_token() + leave("value") +} +proc p_set() { + // presently, I am imagining that we ignore sets + // and I hope we never see objects() in them. + enter("p_set") + chk(leftpar) + demand(set) + demand(string) + read_next_token() + if (current.token != rightpar) { + objects() + } + chk(rightpar) + read_next_token() + leave("p_set") +} +proc contour() {local begin, end, keep, il + enter("contour") + chk(leftpar) + begin = x.size + keep = 0 + demand(string) + if (strcmp(current.s, "CellBody") == 0) { keep = 1 } + if (strcmp(current.s, "Cell Body") == 0) { keep = 1 } + il = current.iline + read_next_token() + contourinfo() + if (keep) { + end = x.size + if (end - begin > 2) { + sectype = 1 + newsec(begin, end) + cursec.iscontour_ = 1 + }else{ +sprint(tstr, "CellBody contour has less than three points at line %d. Ignoring.", il) + b2serr.append(new String(tstr)) + } + } + chk(rightpar) + read_next_token() + leave("contour") +} +proc contourinfo() { + enter("contourinfo") + properties() + points() + morepoints() + leave("contourinfo") +} +proc morepoints() { + enter("morepoints") + optmarkerlist() + leave("morepoints") +} +proc optmarkerlist() { + enter("optmarkerlist") + leave("optmarkerlist") +} +proc markerlist() {local pcnt + enter("markerlist") + chk(leftpar) + pcnt = 1 + // not handling markers. when pcnt goes to 0 then leave + while (pcnt != 0) { + read_next_token() + if (current.token == rightpar) { + pcnt -= 1 + }else if (current.token == leftpar) { + pcnt += 1 + } + } + read_next_token() + leave("markerlist") +} +proc tree() { + enter("tree") + parentsec = nil + chk(leftpar) + read_next_token() + properties() + set_sectype() + branch() + chk(rightpar) + read_next_token() + parentsec = nil + leave("tree") +} +proc branch() {local begin, end localobj psav + enter("branch") + psav = parentsec + begin = x.size + treepoints() + end = x.size + newsec(begin, end) + cursec.parentsec = parentsec + parentsec = cursec + branchend() + parentsec = psav + leave("branch") +} +proc treepoints() { + enter("treepoints") + treepoint() + while (1) { + optionalcomma() + if (current.token != leftpar || look_ahead.token != number) { + break + } + treepoint() + } + leave("treepoints") +} +proc treepoint() { + enter("treepoint") + point() + if (current.token == leftsp) { + spines() + } + leave("treepoint") +} +proc spines() { + enter("spines") + spine() + while(current.token == leftsp) { + spine() + } + leave("spines") +} +proc spine() { + enter("spine") + chk(leftsp) read_next_token() + nspine += 1 err = 1 +// properties() points() + while (current.token != rightsp) { + read_next_token() + } + chk(rightsp) read_next_token() + leave("spine") +} +proc branchend() { + enter("branchend") + optionalcomma() + if (current.token == leftpar) { + while (look_ahead.token == label_) { + markerlist() + } + } + optionalcomma() + if (current.token == leftpar || current.token == label_) { + node() + } + leave("branchend") +} +proc node() { + enter("node") + if (current.token == leftpar) { + read_next_token() split() + chk(rightpar) read_next_token() + }else if (current.token == label_) { + read_next_token() + }else{ + p_err() + } + leave("node") +} +proc split() { + enter("split") + branch() + while (current.token == bar) { + read_next_token() + branch() + } + leave("split") +} +proc marker() { + enter("marker") + chk(leftpar) + demand(label_) + read_next_token() + properties() points() + chk(rightpar) read_next_token() + leave("marker") +} +func text() { + // if text fails then it may be a tree + enter("text") + chk(leftpar) read_next_token() + properties() point() + if (current.token != string) { + leave("text invalid --- expect string") + return 0 + } + chk(string) +// demand(rightpar) + read_next_token() + if (current.token != rightpar) { + leave("text invalid --- expect rightpar") + return 0 + } + chk(rightpar) + read_next_token() + leave("text") + return 1 +} +proc points() { + enter("points") + point() + while (1) { + optionalcomma() + if (current.token != leftpar) { + break + } + point() + } + leave("points") +} +proc point() { + enter("point") + chk(leftpar) + demand(number) + xval = current.x + iline.append(iline_) lines.append(new String(fline)) + read_next_token() optionalcomma() + chk(number) + yval = current.x + zval = dval = 0 + read_next_token() optz() + x.append(xval) y.append(yval) z.append(zval) d.append(dval) + chk(rightpar) read_next_token() +//printf("%g %g %g %g\n", xval, yval, zval, dval) + leave("point") +} +proc optz() { + enter("optz") + optionalcomma() + if (current.token == number) { + zval = current.x + read_next_token() + optmodifier() + } + leave("optz") +} +proc optmodifier() { + enter("optmodifier") + optionalcomma() + if (current.token == number) { + dval = current.x + read_next_token() + optionalcomma() + if (current.token == label_) { + read_next_token() + } + optbezier() + } + leave("optmodifier") +} +proc optbezier() { + enter("optbezier") + optionalcomma() + if (current.token == leftpar) { + demand(number) + read_next_token() + optionalcomma() chk(number) read_next_token() + optionalcomma() chk(number) read_next_token() + optionalcomma() chk(number) demand(rightpar) + read_next_token() + } + leave("optbezier") +} +proc optionalcomma() { + enter("optionalcomma") + if (current.token == comma) { + read_next_token() + } + leave("optionalcomma") +} +proc p_err() { + printf("\nparse error\n") + pcur() + printf("line %d: %s\n", iline_, fline) + stop +} +proc errout() {local i + if (quiet) { return } + printf("\n%s problems\n\n", file.getname) + if (nspine) { + printf("Ignored %d spines\n", nspine) + } + for i=0, b2serr.count-1 { + printf("%s\n", b2serr.object(i).s) + } +} +endtemplate Import3d_Neurolucida3 diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d/read_nts.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d/read_nts.hoc new file mode 100644 index 0000000..c58e9d6 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d/read_nts.hoc @@ -0,0 +1,331 @@ +// translation of ntscable's read_nts.c file for importing +// eutectic files. After reading and parsing lines, the logic +// follows that in nlcda_read.hoc + +begintemplate Import3d_Eutectic_read +public filetype, sections, input, type, file, err +public label, id2pt, id2line, pt2id, pt2sec, sec2pt, mark +external hoc_sf_ +public id, ptype, tag, x, y, z, d, iline, pointtype, points, type +public firstpoints, lastpoints +objref sections, file, stack, cursec, firstpoints, lastpoints, gm +objref id, ptype, tag, x, y, z, d, iline, pointtype, points, type +objref iline2pt, vectors, header, lines, diam, parse_err, nil, soma +strdef tstr, tstr1, point_type_names, filetype, line + +proc init() { + filetype = "Eutectic" + vectors = new List() + header = new List() + lines = new List() + gm = new GUIMath() + MTO = 0 + TTO = 3 + BTO = 6 + CP = 9+1 + FS = 12+1 + SB = 15+1 + BP = 18+1 + NE = 21+1 + ES = 24+1 + MAE = 27 + TAE = 30 + BAE = 33 + SOS = 36 + SCP = 39 + SOE = 42 + OS = 45+1 + OCP = 48 + OE = 51+1 + DS = 54+1 + DCP = 57 + DE = 60+1 + point_type_names = \ +"MTOTTOBTO CP FS SB BP NE ESMAETAEBAESOSSCPSOE OSOCP OE DSDCP DE" +// note numbering for two char item is 1 more than in read_nts.c +// since space is not included in first char +} + +proc input() {local i + nspine = 0 + err = 0 + parse_err = new List() + sections = new List() + stack = new List() + lastpoints = new Vector() + firstpoints = new Vector() + + rdfile($s1) + parse2() + type = new Vector(sections.count) + for i=0, sections.count-1 { + type.x[i] = tag.x[sections.object(i).id] + } + connect2soma() + if (err) { errout() } +} + +proc rdfile() {local i, j + file = new File($s1) + // count lines for vector allocation space (not really necessary) + if (!file.ropen()) { + err = 1 + printf("could not open %s\n", $s1) + } + for (i = 0; !file.eof(); i += 1) { + file.gets(line) + } + file.close() +// printf("%s has %d lines\n", $s1, i) + alloc(i, id, ptype, tag, x, y, z, d, iline, pointtype, points) + tag + diam = d + file.ropen() + for (i = 1; !file.eof(); i += 1) { + file.gets(line) + parse(i, line) + } + file.close() +} + +proc alloc() { local i // $oi.size = 0 but enough space for $1 elements + for i = 2, numarg() { + $oi = new Vector($1) + $oi.resize(0) + vectors.append($oi) + } +} + +proc parse() {local a1 ,a2, a3, a4, a5, a6, a7 + n = sscanf($s2, "%d %s %d %f %f %f %f", &a1, tstr, &a3, &a4, &a5, &a6, &a7) + hoc_sf_.left($s2, hoc_sf_.len($s2)-1) + if (n <= 0) { + header.append(new String($s2)) + return + } + if (n != 7) { + err = 1 + sprint(tstr, "%d: %s parse failure after item %d", $1, $s2, n) + parse_err.append(new String(tstr)) + return + } + a2 = hoc_sf_.head(point_type_names, tstr, tstr1) +// print tstr, " ", a2 + // first points of branches (before physical connection) is 1 + // continuation points are 2 + // branch are 3 + // ends are 4 + // a branch point can also be a first point + // so easiest to accumulate them here + if (a2 == MTO) { + last = 1 + firstpoints.append(id.size) + }else if (a2 == BP ){ + if (last == 3 || last == 4){ + firstpoints.append(id.size) + } + last = 3 + }else if (a2 == FS || a2 == SB || a2 == CP){ + if (a2 == SB) { err = 1 nspine += 1 } + if (last == 3 || last == 4){ + firstpoints.append(id.size) + last = 1 + }else{ + last = 2 + } + }else if (a2 == NE || a2 == ES || a2 == MAE || a2 == TAE || a2 == BAE){ + if (last == 3 || last == 4){ + firstpoints.append(id.size) + } + last = 4 + }else if (a2 == SOS){ + last = 10 + }else if (a2 == SCP){ + last = 10 + }else if (a2 == SOE){ + last = 10 + }else if (a2 == OS){ + return + }else if (a2 == DS){ + return + }else if (a2 == DCP || OCP){ + return + }else if (a2 == DE || a2 == OE){ + return + }else{ + return + } + pointtype.append(last) + points.append(a1) + id.append(a1) + ptype.append(a2) + tag.append(a3) + x.append(a4) + y.append(a5) + z.append(a6) + d.append(a7) + iline.append($1) + lines.append(new String($s2)) +} +proc parse2() {local i, j, k localobj parent + i = ptype.indwhere("==", SOS) + j = ptype.indwhere("==", SOE) + if (i > -1 && j > i) { + mksec(i, j, nil) + cursec.iscontour_ = 1 +// cursec.type=1 + soma = cursec + } + for i=0, firstpoints.size-1 { + j = firstpoints.x[i] + for (k=j; pointtype.x[k] <= 2; k += 1) { + } + parent = pop() + if (parent != nil) { + if (parent.volatile < 1) { + push(parent) + parent.volatile += 1 + } + } + mksec(j, k, parent) +//printf("%s %d %d: %s | %s\n", cursec, j, k, lines.object(j).s, lines.object(k).s) + cursec.parentsec = parent +// logic_connect(cursec, parent) + if (pointtype.x[k] == 3) { + push(cursec) + } + } + if (stack.count > 0) { + err = 1 + } +} + +proc push() { + stack.append($o1) +} +obfunc pop() {localobj p + if (stack.count > 0) { + p = stack.object(stack.count-1) + stack.remove(stack.count-1) + }else{ + p = nil + } + return p +} + +proc mksec() {local i, x1, y1, z1, d1 + if ($o3 == nil) { + cursec = new Import3d_Section($1, $2-$1+1) + cursec.append(0, $1, $2-$1+1, x, y, z, d) + }else{ + cursec = new Import3d_Section($1, $2-$1+2) + cursec.append(1, $1, $2-$1+1, x, y, z, d) + cursec.first = 0 // physical connection + i = $o3.raw.ncol-1 + x1 = $o3.raw.x[0][i] + y1 = $o3.raw.x[1][i] + z1 = $o3.raw.x[2][i] + //d1 = $o3.d.x[i] + cursec.set_pt(0, x1, y1, z1, cursec.d.x[1]) + cursec.fid = 1 + } + cursec.volatile = 0 + cursec.type = tag.x[$1] + sections.append(cursec) + lastpoints.append($2) +} + +proc logic_connect() {local i, x1, y1, z1, d1 + if ($o2 == nil) { return } + i = $o2.raw.ncol-1 + x1 = $o2.raw.x[0][i] + y1 = $o2.raw.x[1][i] + z1 = $o2.raw.x[2][i] + d1 = $o2.d.x[i] + $o1.insrt(0, x1, y1, z1, $o1.d.x[0]) + $o1.first = 1 +} + +proc connect2soma() {local i, ip, j, jp, bp, jpmin, dmin, d, xmin, xmax, ymin, ymax localobj sec, xc, yc, zc, c + // find centroid of soma if outline and connect all dangling + // dendrites to that if inside the contour + if (soma == nil) { return } + xc = soma.raw.getrow(0) + yc = soma.raw.getrow(1) + zc = soma.raw.getrow(2) + xmin = xc.min-.5 xmax = xc.max + .5 + ymin = yc.min-.5 ymax = yc.max + .5 + c = soma.contourcenter(xc, yc, zc) + for i=0, sections.count-1 { + sec = sections.object(i) + if (sec.parentsec == nil && sec != soma) { + if (gm.inside(sec.raw.x[0][0], sec.raw.x[1][0], xmin, ymin, xmax, ymax)) { + sec.parentsec = soma + sec.parentx = .5 + sec.insrt(0, c.x[0], c.x[1], c.x[2], .01) + sec.first = 1 + sec.fid = 1 + } + } + } +} + +proc label(){ + sprint($s2, "Line %d: %s", iline.x[$1], lines.object($1).s) +} +func id2pt() { + i = id.indwhere(">=", $1) +//print "id2pt ", $1, i, id.x[i] + return i +} +func id2line() { return points.x[$1] } +func pt2id() {local i +//print "pt2id ", $1, id.x[$1] + return id.x[$1] +} +func pt2sec(){local i, j + i = lastpoints.indwhere(">=", $1) + if (i == -1) { + i = lastpoints.size-1 + } + $o2 = sections.object(i) + j = $1 - $o2.id + $o2.fid +//print "pt2sec ", $1, $o2, $o2.id, j + return j +} +func sec2pt(){local i localobj sec + sec = sections.object($1) + i = sec.id + $2 - sec.fid +//print "sec2pt ", $1, $2, sec.id, sec.first, i + return i +} + +proc mark() {local i, a,b,c,d,e,f + print $o1, $2, iline, lines + i = id.indwhere("==",$2) + printf("%d,%d,%d: %s\n", i, id.x[i], iline.x[i], lines.object(i).s) + n = sscanf(lines.object(i).s, "%d %s %d %f %f %f %f", &a, tstr, &b, &c, &d, &e, &f) + if (n == 7) { + print a," ",tstr," ",b,c,d,e,f + $o1.mark(c,d,"S",12,4,1) + } +} + +proc errout() { + printf("\n%s problems and default fixes\n\n", file.getname) + if (parse_err.count) { + printf(" Following lines could not be parsed\n") + for i=0, parse_err.count-1 { + printf(" %s\n", parse_err.object(i).s) + } + printf("\n") + } + if (stack.count > 0) { + printf(" stack.count = %d\n", stack.count) + } + if (nspine > 0) { + printf(" Ignore %d spines\n", nspine) + } +} + +endtemplate Import3d_Eutectic_read diff --git a/bmtk-vb/bmtk/simulator/bionet/import3d/read_swc.hoc b/bmtk-vb/bmtk/simulator/bionet/import3d/read_swc.hoc new file mode 100644 index 0000000..2dddd72 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/import3d/read_swc.hoc @@ -0,0 +1,428 @@ +// read swc file, create and verify that it is a single tree, +// and identify the lists of unbranched points. + +begintemplate Import3d_SWC_read +public input, pheader, instantiate +public id, type, x, y, z, d, pid, iline, header, point2sec, sections, lines +public idoffset, label, id2pt, id2line, pt2id, pt2sec, sec2pt, file, mark +public filetype, err, helptxt +public quiet +external hoc_sf_ +objref id, type, x, y, z, d, pid, iline, header, lines +objref file, vectors, sec2point, point2sec, sections +objref connect2prox +strdef tstr, line, filetype +double a[7] +objref id2index_ + +// id and pid contain the raw id values (1st and 7th values on each line) +// from the file. After the file is read id2index(id.x[i]) == i +// Note that the only requireement for a valid swc file is the tree +// topology condition pid.x[i] < id.x[i] + + +proc init() { + quiet = 0 + filetype = "SWC" + vectors = new List() + header = new List() + lines = new List() +} + +func id2index() { + return id2index_.x[$1] +} +func pix2ix() {local pid_ + pid_ = pid.x[$1] + if (pid_ < 0) { return -1 } + return id2index_.x[pid_] +} + +proc input() { + err = 0 + rdfile($s1) + check_pid() // and also creates id2index_ + sectionify() // create point2sec index map + mksections() // Import3dSection list +// instantiate() +} + +proc rdfile() {local i + file = new File($s1) + // count lines for vector allocation space (not really necessary) + if (!file.ropen()) { + err = 1 + printf("could not open %s\n", $s1) + } + for (i = 0; !file.eof(); i += 1) { + file.gets(line) + } + file.close() +// printf("%s has %d lines\n", $s1, i) + alloc(i, id, type, x, y, z, d, pid, iline) + file.ropen() + for (i = 1; !file.eof(); i += 1) { + file.gets(line) + parse(i, line) + } + file.close() +} + +proc alloc() { local i // $oi.size = 0 but enough space for $1 elements + for i = 2, numarg() { + $oi = new Vector($1) + $oi.resize(0) + vectors.append($oi) + } +} + +proc parse() {local i, n + n = sscanf($s2, "%f %f %f %f %f %f %f", &a[0], &a[1], &a[2],\ + &a[3], &a[4], &a[5], &a[6]) + if (n == 7) { + a[5] *= 2 // radius to diameter + for i=0, 6 { + vectors.object(i).append(a[i]) + } + iline.append($1) // for error messages + hoc_sf_.left($s2, hoc_sf_.len($s2)-1) + lines.append(new String($s2)) + } else if (hoc_sf_.head($s2, "#", tstr) == 0) { // comment + header.append(new String($s2)) + } else { + err = 1 + printf("error %s line %d: could not parse: %s", file.getname, $1, $s2) +// Note: only swcdata/n120.swc and swcdata/n423.swc last lines are invalid + } +} + +proc pheader() {local i + for i=0, header.count-1 { + printf("%s", header.object(i).s) + } +} + +proc shift_id() { local i, ierr, imin + // Note: swcdata/*.swc have sequential id's + // shift id and pid so that id.x[0] == 0. Then verify that + // id.x[i] == i + if (id.size > 0) { + imin = id.min_ind + idoffset = id.x[imin] + // is the first one the smallest? + if (id.x[0] != idoffset) { + err = 1 +printf("error %s lines %d and %d: id's %d and %d are not sequential\n", \ + file.getname, iline.x[0], iline.x[imin], \ + id.x[0], idoffset) + } + id.sub(idoffset) + pid.sub(idoffset) + } + ierr = 0 + for i=0, id.size-1 { + if (id.x[i] != i ) { + err = 1 +printf("error %s line %d: id's shifted by %d are not sequential: id.x[%d] != %g\n", \ + file.getname, iline.x[i], idoffset, i, id.x[i]) + ierr += 1 + } + if (ierr > 5) { break } + } +} + +proc check_pid() {local i, ierr, needsort localobj tobj + // if all pid.x[i] < id.x[i] then we must be 1 or more trees with no loops + // Note: swcdata/*.swc conforms. + needsort = 0 + ierr = 0 + for i=0, id.size-1 { + if (i > 0) if (id.x[i] <= id.x[i-1]) { needsort = 1 } + if (pid.x[i] >= id.x[i]) { + err = 1 +printf("error %s line %d: index %d pid=%d is not less than id=%d\n",\ + file.getname, iline.x[i], i, pid.x[i], id.x[i]) + } + } + if (needsort) { // sort in id order + tobj = id.sortindex() + id.sortindex(id, tobj) + pid.sortindex(pid, tobj) + x.sortindex(x, tobj) + y.sortindex(y, tobj) + z.sortindex(z, tobj) + d.sortindex(diam, tobj) + iline.sortindex(iline, tobj) + } + // the number of trees is just the number of pid's < 0 + // Note: swcdata/*.swc have only one tree + tobj = new Vector() + tobj.indvwhere(pid, "<", 0) + if (tobj.size > 1) { + err = 1 + + if (!quiet) {// added by Sergey to suppress the warning output + +printf("warning %s: more than one tree:\n", file.getname) + printf(" root at line:") + for i=0, tobj.size-1 { + printf(" %d,", iline.x[tobj.x[i]]) + } + printf(" \n") + }// end of quiet + } + // check for duplicate id + for i=1, id.size-1 if (id.x[i] == id.x[i-1]) { + err = 1 +printf("error %s: duplicate id:\n", file.getname) +printf(" %d: %s\n", iline.x[i-1], lines.o(iline.x[i-1]).s) +printf(" %d: %s\n", iline.x[i], lines.o(iline.x[i]).s) + } + // create the id2index_ map + id2index_ = new Vector(id.max()+1) + id2index_.fill(-1) + for i=0, id.size-1 { + id2index_.x[id.x[i]] = i + } +} + +proc sectionify() {local i, si localobj tobj + // create point2sec map and sections list + // point2sec gives immediate knowledge of the section a point is in + // sections list is for display purposes + if (id.size < 2) { return } + + // tobj stores the number of child nodes with pid equal to i + // actually every non-contiguous child adds 1.01 and a contiguous + // child adds 1 + mark_branch(tobj) + + point2sec = new Vector(id.size) + // first point in the root and if only one point it will be interpreted + // as spherical. + point2sec.x[0] = 0 + si = 0 + for i=1, id.size-1 { + if (tobj.x[pix2ix(i)] > 1 || connect2prox.x[i]) { + si += 1 + } + point2sec.x[i] = si + } + sec2point = new Vector(si) + tobj.x[0] = 1 + sec2point.indvwhere(tobj, "!=", 1) + // sec2point.x[i] is the last point of section i + // 0 is the first point of section 0 + // sec2point.x[i-1]+1 is the first point of section i +} + +proc mark_branch() { local i, p + //$o1 is used to store the number of child nodes with pid equal to i + // actually add a bit more than 1 + // if noncontiguous child and 1 if contiguous child + // this is the basic computation that defines sections, i.e. + // contiguous 1's with perhaps a final 0 (a leaf) + // As usual, the only ambiguity will be how to treat the soma + + // Another wrinkle is that we do not want any sections that + // have multiple point types. E.g. point type 1 is often + // associated with the soma. Therefore we identify + // point type changes with branch points. + + // however warn if the first two points do not have the same type + // if single point soma set the spherical_soma flag + if ( type.x[0] != type.x[1]) { + err = 1 + if (0 && !quiet) { +printf("\nNotice: %s:\nThe first two points have different types (%d and %d) but\n a single point NEURON section is not allowed.\n Interpreting the point as the center of a sphere of\n radius %g at location (%g, %g, %g)\n Will represent as 3-point cylinder with L=diam and with all\n children kept at their 1st point positions and connected\n with wire to middle point.\n If this is an incorrect guess, then change the file.\n"\ +, file.getname, type.x[0], type.x[1], d.x[0]/2, x.x[0], y.x[0], z.x[0]) + } + } + + // another wrinkle is that when a dendrite connects to the soma + // by a wire, + // another branch may connect to the first point of that dendrite + // In this case (to avoid single point sections) + // that branch should be connected to position 0 + // of the dendrite (and the first point of that branch should be + // the same position as the first point of the dendrite. + // use connect2prox to indicate the parent point is not + // the distal end but the proximal end of the parent section + connect2prox = new Vector(id.size) + + $o1 = new Vector(id.size) + for i=0, id.size-1 { + p = pix2ix(i) + if (p >= 0) { + $o1.x[p] += 1 + if ( p != i-1) { + $o1.x[p] += .01 +//i noncontiguous with parent and +// if parent is not soma and parent of parent is soma +// then i appended to connect2prox +if (p > 1) if (type.x[p] != 1 && type.x[pix2ix(p)] == 1) { + connect2prox.x[i] = 1 + $o1.x[p] = 1 // p not treated as a 1pt section + err = 1 + if (0 && !quiet) { +printf("\nNotice: %s:\n %d parent is %d which is the proximal point of a section\n connected by a wire to the soma.\n The dendrite is being connected to\n the proximal end of the parent dendrite.\n If this is an incorrect guess, then change the file.\n"\ +, file.getname, id.x[i], id.x[p]) + } +} + + } + if (type.x[p] != type.x[i]) { + // increment enough to get past 1 + // so force end of section but + // not really a branch + $o1.x[p] += .01 + } + } + } +} + +proc mksections() {local i, j, isec, first localobj sec, psec, pts + sections = new List() + isec = 0 + first = 0 + for i=0, id.size-1 { + if (point2sec.x[i] > isec) { + mksection(isec, first, i) + isec += 1 + first = i + } + } + mksection(isec, first, i) +} + +proc mksection() { local i, isec, first localobj sec + isec = $1 first=$2 i=$3 + if (isec > 0) {// branches have pid as first point + sec = new Import3d_Section(first, i-first+1) + pt2sec(pix2ix(first), sec.parentsec) + // but if the parent is the root and the branch has more than + // one point, then connect to center of root with wire + if (point2sec.x[pix2ix(first)] == 0 && i > 1) { + sec.parentx = 0.5 + sec.first = 1 + }else{ + if (pix2ix(first) == 0) { sec.parentx = 0 } + } + sec.append(0, pix2ix(first), 1, x, y, z, d) + sec.append(1, first, i-first, x, y, z, d) + }else{// pid not first point in root section + sec = new Import3d_Section(first, i-first) + sec.append(0, first, i-first, x, y, z, d) + } + sec.type = type.x[first] + sections.append(sec) + if (object_id(sec.parentsec)) { + if (sec.parentsec.type == 1 && sec.type != 1) { + sec.d.x[0] = sec.d.x[1] + } + } + if (connect2prox.x[first]) { + sec.pid = sec.parentsec.id + sec.parentx = 0 + } +} + +func same() { + if ($2 < 0) return 0 + if (x.x[$1] == x.x[$2]) { + if (y.x[$1] == y.x[$2]) { +// if (z.x[$1] == z.x[$2]) { + return 1 +// } + } + } + return 0 +} + +proc instantiate() {local i, isec, psec, pp, si, px + if (id.size < 2) { return } + + sprint(tstr, "~create K[%d]", sec2point.size) + execute(tstr) + + // connect + for i = 2, id.size-1 { + if (point2sec.x[pix2ix(i)] == point2sec.x[i]) { continue } + if (pix2ix(i) == 0) { px = 0 } else { px = 1 } + sprint(tstr, "K[%d] connect K[%d](0), (%g)", \ + point2sec.x[pix2ix(i)], point2sec.x[i], px) + execute(tstr) + } + + // 3-d point info + // needs some thought with regard to interior duplicate + // points, and whether it is appropriate to make the first + // point in the section the same location and diam as the + // pid point + isec = 0 + for i=0, id.size-1 { + if (point2sec.x[i] > isec ) { // in next section + ptadd(pix2ix(i), point2sec.x[i]) + } + isec = point2sec.x[i] + ptadd(i, isec) + } +} + +proc ptadd() { + sprint(tstr, "K[%d] { pt3dadd(%g, %g, %g, %g) }", \ + $2, x.x[$1], y.x[$1], z.x[$1], d.x[$1]) + execute(tstr) +} + +proc label() { + sprint($s2, "Line %d: %s", iline.x[$1], lines.object($1).s) +} +func id2pt() {local i + if ($1 < 0) { + $1 = 0 + }else if ( $1 > id2index_.size-1) { + $1 = id2index_.size-1 + } + return id2index($1) +} +func id2line() { return $1 } +func pt2id() { return id.x[$1] } +func pt2sec() { local i,j //from selpoint + i = point2sec.x[$1] + $o2 = sections.object(i) + j = $1 - $o2.id + if (i > 0) { j += 1 } + return j +} +func sec2pt() {local i + i = sections.object($1).id + $2 + if ($1 > 0) { + i -= 1 + } + return i +} +proc mark() {local i + print $o1, $2, iline, lines + i = id2index($2) + printf("%d %d %g %g: %s\n", i, iline.x[i], x.x[i], y.x[i], lines.object(i).s) + $o1.mark(x.x[i], y.x[i], "S", 12, 4, 1) +} + +proc helptxt() { + xpanel("SWC file filter characteristics") +xlabel(" Sections consist of unbranched sequences of points having") +xlabel("the same type. All sections connect from 0 to 1") +xlabel("(except those connecting to the first point") +xlabel("of the root section connect from 0 to 0).") +xlabel("With one exception, all child sections have as their first pt3d") +xlabel("point a copy of the parent point and the diameter of that first") +xlabel("point is the diameter of the parent point") +xlabel(" The exception, so that the error in area is not so") +xlabel("egregious, is that dendrite branches that connect to the soma") +xlabel("get a copy of the parent point as their first pt3d point but") +xlabel("the diameter of that point is the diameter of the second point") +xlabel(" The root section does not contain an extra parent point.") + xpanel(0) +} +endtemplate Import3d_SWC_read diff --git a/bmtk-vb/bmtk/simulator/bionet/io_tools.py b/bmtk-vb/bmtk/simulator/bionet/io_tools.py new file mode 100644 index 0000000..2ae6289 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/io_tools.py @@ -0,0 +1,38 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from neuron import h + +from bmtk.simulator.core.io_tools import IOUtils + +pc = h.ParallelContext() +MPI_Rank = int(pc.id()) +MPI_Size = int(pc.nhost()) + + +class NEURONIOUtils(IOUtils): + def __init__(self): + super(NEURONIOUtils, self).__init__() + self.mpi_rank = MPI_Rank + self.mpi_size = MPI_Size + +io = NEURONIOUtils() diff --git a/bmtk-vb/bmtk/simulator/bionet/io_tools.pyc b/bmtk-vb/bmtk/simulator/bionet/io_tools.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33f4fcdbb39d649654eee41e15e1a0f681ca6088 GIT binary patch literal 887 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/record_cellvars.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/record_cellvars.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ff25418b89c9ef8a54c371dbe022ff7883503da GIT binary patch literal 6202 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/xstim.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/xstim.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..273b5a397ef5eefedba3505598a666a3f0d0dec8 GIT binary patch literal 5658 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/xstim_waveforms.cpython-37.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/__pycache__/xstim_waveforms.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22d74a6768a5c9be6d43fd2f66010083a91fd1df GIT binary patch literal 4791 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 0: + # just in case the simulation doesn't end on a block step + self.block(sim, (sim.n_steps - self._block_step, sim.n_steps)) + + self._save_ecp(sim) + self._delete_tmp_files() + pc.barrier() + + +class RecXElectrode(object): + """Extracellular electrode + + """ + + def __init__(self, positions): + """Create an array""" + # self.conf = conf + electrode_file = positions # self.conf["recXelectrode"]["positions"] + + # convert coordinates to ndarray, The first index is xyz and the second is the channel number + el_df = pd.read_csv(electrode_file, sep=' ') + self.pos = el_df[['x_pos', 'y_pos', 'z_pos']].T.values + #self.pos = el_df.as_matrix(columns=['x_pos', 'y_pos', 'z_pos']).T + self.nsites = self.pos.shape[1] + # self.conf['run']['nsites'] = self.nsites # add to the config + self.transfer_resistances = {} # V_e = transfer_resistance*Im + + def drift(self): + # will include function to model electrode drift + pass + + def get_transfer_resistance(self, gid): + return self.transfer_resistances[gid] + + def calc_transfer_resistance(self, gid, seg_coords): + """Precompute mapping from segment to electrode locations""" + sigma = 0.3 # mS/mm + + r05 = (seg_coords['p0'] + seg_coords['p1']) / 2 + dl = seg_coords['p1'] - seg_coords['p0'] + + nseg = r05.shape[1] + + tr = np.zeros((self.nsites, nseg)) + + for j in range(self.nsites): # calculate mapping for each site on the electrode + rel = np.expand_dims(self.pos[:, j], axis=1) # coordinates of a j-th site on the electrode + rel_05 = rel - r05 # distance between electrode and segment centers + + # compute dot product column-wise, the resulting array has as many columns as original + r2 = np.einsum('ij,ij->j', rel_05, rel_05) + + # compute dot product column-wise, the resulting array has as many columns as original + rlldl = np.einsum('ij,ij->j', rel_05, dl) + dlmag = np.linalg.norm(dl, axis=0) # length of each segment + rll = abs(rlldl / dlmag) # component of r parallel to the segment axis it must be always positive + rT2 = r2 - rll ** 2 # square of perpendicular component + up = rll + dlmag / 2 + low = rll - dlmag / 2 + num = up + np.sqrt(up ** 2 + rT2) + den = low + np.sqrt(low ** 2 + rT2) + tr[j, :] = np.log(num / den) / dlmag # units of (um) use with im_ (total seg current) + np.copyto(tr[j, :], 0, where=(dlmag == 0)) # zero out stub segments + + tr *= 1 / (4 * math.pi * sigma) + self.transfer_resistances[gid] = tr diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/ecp.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/ecp.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3fd9d9fecfb20602aa1217129819b3f72d0bad2 GIT binary patch literal 10006 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 OCJvi8fz+=2*?$7%Le#bZ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/record_cellvars.py b/bmtk-vb/bmtk/simulator/bionet/modules/record_cellvars.py new file mode 100644 index 0000000..91a0ace --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/modules/record_cellvars.py @@ -0,0 +1,212 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import h5py +import numpy as np +from neuron import h + +from bmtk.simulator.bionet.modules.sim_module import SimulatorMod +from bmtk.simulator.bionet.io_tools import io + +from bmtk.utils.io import cell_vars +try: + # Check to see if h5py is built to run in parallel + if h5py.get_config().mpi: + MembraneRecorder = cell_vars.CellVarRecorderParallel + else: + MembraneRecorder = cell_vars.CellVarRecorder + +except Exception as e: + MembraneRecorder = cell_vars.CellVarRecorder + +MembraneRecorder._io = io + +pc = h.ParallelContext() +MPI_RANK = int(pc.id()) +N_HOSTS = int(pc.nhost()) + + +def first_element(lst): + return lst[0] + + +transforms_table = { + 'first_element': first_element, +} + + +class MembraneReport(SimulatorMod): + def __init__(self, tmp_dir, file_name, variable_name, cells, sections='all', buffer_data=True, transform={}): + """Module used for saving NEURON cell properities at each given step of the simulation. + + :param tmp_dir: + :param file_name: name of h5 file to save variable. + :param variables: list of cell variables to record + :param gids: list of gids to to record + :param sections: + :param buffer_data: Set to true then data will be saved to memory until written to disk during each block, reqs. + more memory but faster. Set to false and data will be written to disk on each step (default: True) + """ + self._all_variables = list(variable_name) + self._variables = list(variable_name) + self._transforms = {} + # self._special_variables = [] + for var_name, fnc_name in transform.items(): + if fnc_name is None or len(fnc_name) == 0: + del self._transforms[var_name] + continue + + fnc = transforms_table[fnc_name] + self._transforms[var_name] = fnc + self._variables.remove(var_name) + + self._tmp_dir = tmp_dir + + self._file_name = file_name if os.path.isabs(file_name) else os.path.join(tmp_dir, file_name) + self._all_gids = cells + self._local_gids = [] + self._sections = sections + + self._var_recorder = MembraneRecorder(self._file_name, self._tmp_dir, self._all_variables, + buffer_data=buffer_data, mpi_rank=MPI_RANK, mpi_size=N_HOSTS) + + self._gid_list = [] # list of all gids that will have their variables saved + self._data_block = {} # table of variable data indexed by [gid][variable] + self._block_step = 0 # time step within a given block + + def _get_gids(self, sim): + # get list of gids to save. Will only work for biophysical cells saved on the current MPI rank + selected_gids = set(sim.net.get_node_set(self._all_gids).gids()) + self._local_gids = list(set(sim.biophysical_gids) & selected_gids) + + def _save_sim_data(self, sim): + self._var_recorder.tstart = 0.0 + self._var_recorder.tstop = sim.tstop + self._var_recorder.dt = sim.dt + + def initialize(self, sim): + self._get_gids(sim) + self._save_sim_data(sim) + + # TODO: get section by name and/or list of section ids + # Build segment/section list + sec_list = [] + seg_list = [] + for gid in self._local_gids: + cell = sim.net.get_cell_gid(gid) + cell.store_segments() + for sec_id, sec in enumerate(cell.get_sections()): + for seg in sec: + # TODO: Make sure the seg has the recorded variable(s) + sec_list.append(sec_id) + seg_list.append(seg.x) + + # sec_list = [cell.get_sections_id().index(sec) for sec in cell.get_sections()] + # seg_list = [seg.x for seg in cell.get_segments()] + + self._var_recorder.add_cell(gid, sec_list, seg_list) + + self._var_recorder.initialize(sim.n_steps, sim.nsteps_block) + + def step(self, sim, tstep): + # save all necessary cells/variables at the current time-step into memory + for gid in self._local_gids: + cell = sim.net.get_cell_gid(gid) + + for var_name in self._variables: + seg_vals = [getattr(seg, var_name) for seg in cell.get_segments()] + self._var_recorder.record_cell(gid, var_name, seg_vals, tstep) + + for var_name, fnc in self._transforms.items(): + seg_vals = [fnc(getattr(seg, var_name)) for seg in cell.get_segments()] + self._var_recorder.record_cell(gid, var_name, seg_vals, tstep) + + self._block_step += 1 + + def block(self, sim, block_interval): + # write variables in memory to file + self._var_recorder.flush() + + def finalize(self, sim): + # TODO: Build in mpi signaling into var_recorder + pc.barrier() + self._var_recorder.close() + + pc.barrier() + self._var_recorder.merge() + + +class SomaReport(MembraneReport): + """Special case for when only needing to save the soma variable""" + def __init__(self, tmp_dir, file_name, variable_name, cells, sections='soma', buffer_data=True, transform={}): + super(SomaReport, self).__init__(tmp_dir=tmp_dir, file_name=file_name, variable_name=variable_name, cells=cells, + sections=sections, buffer_data=buffer_data, transform=transform) + + def initialize(self, sim): + self._get_gids(sim) + self._save_sim_data(sim) + + for gid in self._local_gids: + self._var_recorder.add_cell(gid, [0], [0.5]) + self._var_recorder.initialize(sim.n_steps, sim.nsteps_block) + + def step(self, sim, tstep, rel_time=0.0): + # save all necessary cells/variables at the current time-step into memory + for gid in self._local_gids: + cell = sim.net.get_cell_gid(gid) + for var_name in self._variables: + var_val = getattr(cell.hobj.soma[0](0.5), var_name) + self._var_recorder.record_cell(gid, var_name, [var_val], tstep) + + for var_name, fnc in self._transforms.items(): + var_val = getattr(cell.hobj.soma[0](0.5), var_name) + new_val = fnc(var_val) + self._var_recorder.record_cell(gid, var_name, [new_val], tstep) + + self._block_step += 1 + +class SectionReport(MembraneReport): + """For variables like im which have one value per section, not segment""" + + def initialize(self, sim): + self._get_gids(sim) + self._save_sim_data(sim) + + for gid in self._local_gids: + cell = sim.net.get_cell_gid(gid) + sec_list = range(len(cell.get_sections())) + self._var_recorder.add_cell(gid, sec_list, sec_list) + + self._var_recorder.initialize(sim.n_steps, sim.nsteps_block) + + def step(self, sim, tstep): + for gid in self._local_gids: + for var in self._variables: + cell = sim.net.get_cell_gid(gid) + if var == 'im': + vals = cell.get_im() + elif var =='v': + vals = np.array([sec.v for sec in cell.get_sections()]) + self._var_recorder.record_cell(gid, var, vals, tstep) + + self._block_step += 1 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/record_cellvars.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/record_cellvars.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21c71d952185080c67b21d8886532403b7e80cc0 GIT binary patch literal 7944 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/record_netcons.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/record_netcons.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d724a656e6298ac66e28f8c77c1575272aa27338 GIT binary patch literal 6179 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 CKZt<< literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/record_spikes.py b/bmtk-vb/bmtk/simulator/bionet/modules/record_spikes.py new file mode 100644 index 0000000..4c8751b --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/modules/record_spikes.py @@ -0,0 +1,94 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import csv +import h5py +import numpy as np +from bmtk.simulator.bionet.modules.sim_module import SimulatorMod +from bmtk.utils.io.spike_trains import SpikeTrainWriter + +from neuron import h + + +pc = h.ParallelContext() +MPI_RANK = int(pc.id()) +N_HOSTS = int(pc.nhost()) + + +class SpikesMod(SimulatorMod): + """Module use for saving spikes + + """ + + def __init__(self, tmp_dir, spikes_file_csv=None, spikes_file=None, spikes_file_nwb=None, spikes_sort_order=None): + # TODO: Have option to turn off caching spikes to csv. + def _file_path(file_name): + if file_name is None: + return None + return file_name if os.path.isabs(file_name) else os.path.join(tmp_dir, file_name) + + self._csv_fname = _file_path(spikes_file_csv) + self._save_csv = spikes_file_csv is not None + + self._h5_fname = _file_path(spikes_file) + self._save_h5 = spikes_file is not None + + self._nwb_fname = _file_path(spikes_file_nwb) + self._save_nwb = spikes_file_nwb is not None + + self._tmpdir = tmp_dir + self._sort_order = spikes_sort_order + + self._spike_writer = SpikeTrainWriter(tmp_dir=tmp_dir, mpi_rank=MPI_RANK, mpi_size=N_HOSTS) + + def initialize(self, sim): + # TODO: since it's possible that other modules may need to access spikes, set_spikes_recordings() should + # probably be called in the simulator itself. + sim.set_spikes_recording() + + def block(self, sim, block_interval): + # take spikes from Simulator spikes vector and save to the tmp file + for gid, tVec in sim.spikes_table.items(): + for t in tVec: + self._spike_writer.add_spike(time=t, gid=gid) + + pc.barrier() # wait until all ranks have been saved + sim.set_spikes_recording() # reset recording vector + + def finalize(self, sim): + self._spike_writer.flush() + pc.barrier() + + if self._save_csv: + self._spike_writer.to_csv(self._csv_fname, sort_order=self._sort_order) + pc.barrier() + + if self._save_h5: + self._spike_writer.to_hdf5(self._h5_fname, sort_order=self._sort_order) + pc.barrier() + + if self._save_nwb: + self._spike_writer.to_nwb(self._nwb_fname, sort_order=self._sort_order) + pc.barrier() + + self._spike_writer.close() diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/record_spikes.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/record_spikes.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcd53a861aa7847c11b0d42efd98b37810a4c7c8 GIT binary patch literal 2930 zcmcIm?`|7K5TCuXo!E66HxT{_(UeN9B_RHQf`o*sLR--a)l}y~ghg~ZpV##{_s6|m zrvZ{rsQ?M_2)qCiufR+305HE&=G$G9NSGQ&4S{ z%8s11ie?_oSSty5c>~B`Nv$oPKk-;7MD<0qtpP1^Z9PT`Bl~$|rxp({IQ>9XGl;HVZ zCgC%^QpZI(4qe=~=ijkk*cP0(;as}Bsku5$6d~!jg~x^?|4kJusmeb*>a8?I7Mkg)?b$a zoGhXqK6(%apZ33y)BW)NmxIRx3&os~;WJk?;zj6YbUJZ%tz#z%>>L19@yt1z{qGs5 zb>-~LIfZaj;v-R$7hZ0p5QZ>J^2CPWdr;w32IQ^R*VVdLt)^;vF_H}nKDxupTxM{@ z`71pLZLUgE@gn3HZI?+Ax)kB+?m|ZJCIc(woMg^ECR{)g!muPtIjwF(KHerj#=})# z5mQD?IQ)ER%}k-G-rf6%zV+zXr{fwO-b7=#yLXk2r4P88<(|uJfx7{>loRsQ)9bYR zv_rdGkh3~uD7dE~n79Y;_N82Hl4-I3NivqUExme~BQ_eP+7+|anF-$DYm)BKcv)mp z`SVz^lZ&-XICrn|De0`@++Yir5Ta39#QPj!?(zpw8e9im%IRwzbEDziMFjxuNM66k zh(Se@_qJM7*SwZGO(&>w2mC)7aP#AIasg0mBsWHJBhzYBKB)LE20T-w(K><`rqnFTX$*Dt(riON5!8*M?>{^tfOCd1mLKhmo&-RG=?yC`-SLil5nF3dEz z2^!oUfNPP9wm?@45V2qr01ZI`%$(_XvJ+r#2bd()d2SoQ2KEpfr-jjH#U=5-sKBoB zdy^!Wy6Q7-kfFqpZFx<_5&jqKcX8Y`Xc<6Z=o)nB#s_buC5Pi84nyhm!8?2-xCZb( zz=sUZY!U3GcEJFJd4rcEX~S=Lo14vgv)P5W19(?z6|EfUI^E$B2Wq~^7(>+axl|Em z<@V{A+;(GfdpRb-SlS%$6|!$JkQ)n|q-HxQwxt1t7Qf4k8%9WtTrbKZ5AcqRjz>J6 z(^RK-3NGqHE7>Fpa*>NR$vAiohegLJ|3U~}=dD`ucJC% L$8W0*wc-B*j$nK6 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/save_synapses.py b/bmtk-vb/bmtk/simulator/bionet/modules/save_synapses.py new file mode 100644 index 0000000..396aa7d --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/modules/save_synapses.py @@ -0,0 +1,235 @@ +import os +import csv +import h5py +import numpy as np +from neuron import h + +from .sim_module import SimulatorMod +from bmtk.simulator.bionet.biocell import BioCell +from bmtk.simulator.bionet.io_tools import io +from bmtk.simulator.bionet.pointprocesscell import PointProcessCell + + +pc = h.ParallelContext() +MPI_RANK = int(pc.id()) +N_HOSTS = int(pc.nhost()) + + +class SaveSynapses(SimulatorMod): + def __init__(self, network_dir, single_file=False, **params): + self._network_dir = network_dir + self._virt_lookup = {} + self._gid_lookup = {} + self._sec_lookup = {} + if not os.path.exists(network_dir): + os.makedirs(network_dir) + + if N_HOSTS > 1: + io.log_exception('save_synapses module is not current supported with mpi') + + self._syn_writer = ConnectionWriter(network_dir) + + def _print_nc(self, nc, src_nid, trg_nid, cell, src_pop, trg_pop, edge_type_id): + if isinstance(cell, BioCell): + sec_x = nc.postloc() + sec = h.cas() + sec_id = self._sec_lookup[cell.gid][sec] #cell.get_section_id(sec) + h.pop_section() + self._syn_writer.add_bio_conn(edge_type_id, src_nid, src_pop, trg_nid, trg_pop, nc.weight[0], sec_id, sec_x) + # print '{} ({}) <-- {} ({}), {}, {}, {}, {}'.format(trg_nid, trg_pop, src_nid, src_pop, nc.weight[0], nc.delay, sec_id, sec_x) + + else: + self._syn_writer.add_point_conn(edge_type_id, src_nid, src_pop, trg_nid, trg_pop, nc.weight[0]) + #print '{} ({}) <-- {} ({}), {}, {}'.format(trg_nid, trg_pop, src_nid, src_pop, nc.weight[0], nc.delay) + + + def initialize(self, sim): + io.log_info('Saving network connections. This may take a while.') + + # Need a way to look up virtual nodes from nc.pre() + for pop_name, nodes_table in sim.net._virtual_nodes.items(): + for node_id, virt_node in nodes_table.items(): + self._virt_lookup[virt_node.hobj] = (pop_name, node_id) + + # Need to figure out node_id and pop_name from nc.srcgid() + for node_pop in sim.net.node_populations: + pop_name = node_pop.name + for node in node_pop[0::1]: + if node.model_type != 'virtual': + self._gid_lookup[node.gid] = (pop_name, node.node_id) + + for gid, cell in sim.net.get_local_cells().items(): + trg_pop, trg_id = self._gid_lookup[gid] + if isinstance(cell, BioCell): + #from pprint import pprint + #pprint({i: s_name for i, s_name in enumerate(cell.get_sections())}) + #exit() + # sections = cell._syn_seg_ix + self._sec_lookup[gid] = {sec_name: sec_id for sec_id, sec_name in enumerate(cell.get_sections_id())} + + else: + sections = [-1]*len(cell.netcons) + + for nc, edge_type_id in zip(cell.netcons, cell._edge_type_ids): + src_gid = int(nc.srcgid()) + if src_gid == -1: + # source is a virtual node + src_pop, src_id = self._virt_lookup[nc.pre()] + else: + src_pop, src_id = self._gid_lookup[src_gid] + + self._print_nc(nc, src_id, trg_id, cell, src_pop, trg_pop, edge_type_id) + + self._syn_writer.close() + io.log_info(' Done saving network connections.') + + +class ConnectionWriter(object): + class H5Index(object): + def __init__(self, network_dir, src_pop, trg_pop): + # TODO: Merge with NetworkBuilder code for building SONATA files + self._nsyns = 0 + self._n_biosyns = 0 + self._n_pointsyns = 0 + self._block_size = 5 + + self._pop_name = '{}_{}'.format(src_pop, trg_pop) + self._h5_file = h5py.File(os.path.join(network_dir, '{}_edges.h5'.format(self._pop_name)), 'w') + self._pop_root = self._h5_file.create_group('/edges/{}'.format(self._pop_name)) + self._pop_root.create_dataset('edge_group_id', (self._block_size, ), dtype=np.uint16, + chunks=(self._block_size, ), maxshape=(None, )) + self._pop_root.create_dataset('source_node_id', (self._block_size, ), dtype=np.uint64, + chunks=(self._block_size, ), maxshape=(None, )) + self._pop_root['source_node_id'].attrs['node_population'] = src_pop + self._pop_root.create_dataset('target_node_id', (self._block_size, ), dtype=np.uint64, + chunks=(self._block_size, ), maxshape=(None, )) + self._pop_root['target_node_id'].attrs['node_population'] = trg_pop + self._pop_root.create_dataset('edge_type_id', (self._block_size, ), dtype=np.uint32, + chunks=(self._block_size, ), maxshape=(None, )) + self._pop_root.create_dataset('0/syn_weight', (self._block_size, ), dtype=np.float, + chunks=(self._block_size, ), maxshape=(None, )) + self._pop_root.create_dataset('0/sec_id', (self._block_size, ), dtype=np.uint64, + chunks=(self._block_size, ), maxshape=(None, )) + self._pop_root.create_dataset('0/sec_x', (self._block_size, ), chunks=(self._block_size, ), + maxshape=(None, ), dtype=np.float) + self._pop_root.create_dataset('1/syn_weight', (self._block_size, ), dtype=np.float, + chunks=(self._block_size, ), maxshape=(None, )) + + def _add_conn(self, edge_type_id, src_id, trg_id, grp_id): + self._pop_root['edge_type_id'][self._nsyns] = edge_type_id + self._pop_root['source_node_id'][self._nsyns] = src_id + self._pop_root['target_node_id'][self._nsyns] = trg_id + self._pop_root['edge_group_id'][self._nsyns] = grp_id + + self._nsyns += 1 + if self._nsyns % self._block_size == 0: + self._pop_root['edge_type_id'].resize((self._nsyns + self._block_size,)) + self._pop_root['source_node_id'].resize((self._nsyns + self._block_size, )) + self._pop_root['target_node_id'].resize((self._nsyns + self._block_size, )) + self._pop_root['edge_group_id'].resize((self._nsyns + self._block_size, )) + + def add_bio_conn(self, edge_type_id, src_id, trg_id, syn_weight, sec_id, sec_x): + self._add_conn(edge_type_id, src_id, trg_id, 0) + self._pop_root['0/syn_weight'][self._n_biosyns] = syn_weight + self._pop_root['0/sec_id'][self._n_biosyns] = sec_id + self._pop_root['0/sec_x'][self._n_biosyns] = sec_x + + self._n_biosyns += 1 + if self._n_biosyns % self._block_size == 0: + self._pop_root['0/syn_weight'].resize((self._n_biosyns + self._block_size, )) + self._pop_root['0/sec_id'].resize((self._n_biosyns + self._block_size, )) + self._pop_root['0/sec_x'].resize((self._n_biosyns + self._block_size, )) + + def add_point_conn(self, edge_type_id, src_id, trg_id, syn_weight): + self._add_conn(edge_type_id, src_id, trg_id, 1) + self._pop_root['1/syn_weight'][self._n_pointsyns] = syn_weight + + self._n_pointsyns += 1 + if self._n_pointsyns % self._block_size == 0: + self._pop_root['1/syn_weight'].resize((self._n_pointsyns + self._block_size, )) + + def clean_ends(self): + self._pop_root['source_node_id'].resize((self._nsyns,)) + self._pop_root['target_node_id'].resize((self._nsyns,)) + self._pop_root['edge_group_id'].resize((self._nsyns,)) + self._pop_root['edge_type_id'].resize((self._nsyns,)) + + self._pop_root['0/syn_weight'].resize((self._n_biosyns,)) + self._pop_root['0/sec_id'].resize((self._n_biosyns,)) + self._pop_root['0/sec_x'].resize((self._n_biosyns,)) + + self._pop_root['1/syn_weight'].resize((self._n_pointsyns,)) + + eg_ds = self._pop_root.create_dataset('edge_group_index', (self._nsyns, ), dtype=np.uint64) + bio_count, point_count = 0, 0 + for idx, grp_id in enumerate(self._pop_root['edge_group_id']): + if grp_id == 0: + eg_ds[idx] = bio_count + bio_count += 1 + elif grp_id == 1: + eg_ds[idx] = point_count + point_count += 1 + + self._create_index('target') + + def _create_index(self, index_type='target'): + if index_type == 'target': + edge_nodes = np.array(self._pop_root['target_node_id'], dtype=np.int64) + output_grp = self._pop_root.create_group('indicies/target_to_source') + elif index_type == 'source': + edge_nodes = np.array(self._pop_root['source_node_id'], dtype=np.int64) + output_grp = self._pop_root.create_group('indicies/source_to_target') + + edge_nodes = np.append(edge_nodes, [-1]) + n_targets = np.max(edge_nodes) + ranges_list = [[] for _ in xrange(n_targets + 1)] + + n_ranges = 0 + begin_index = 0 + cur_trg = edge_nodes[begin_index] + for end_index, trg_gid in enumerate(edge_nodes): + if cur_trg != trg_gid: + ranges_list[cur_trg].append((begin_index, end_index)) + cur_trg = int(trg_gid) + begin_index = end_index + n_ranges += 1 + + node_id_to_range = np.zeros((n_targets + 1, 2)) + range_to_edge_id = np.zeros((n_ranges, 2)) + range_index = 0 + for node_index, trg_ranges in enumerate(ranges_list): + if len(trg_ranges) > 0: + node_id_to_range[node_index, 0] = range_index + for r in trg_ranges: + range_to_edge_id[range_index, :] = r + range_index += 1 + node_id_to_range[node_index, 1] = range_index + + output_grp.create_dataset('range_to_edge_id', data=range_to_edge_id, dtype='uint64') + output_grp.create_dataset('node_id_to_range', data=node_id_to_range, dtype='uint64') + + def __init__(self, network_dir): + self._network_dir = network_dir + self._pop_groups = {} + + def _group_key(self, src_pop, trg_pop): + return (src_pop, trg_pop) + + def _get_edge_group(self, src_pop, trg_pop): + grp_key = self._group_key(src_pop, trg_pop) + if grp_key not in self._pop_groups: + self._pop_groups[grp_key] = self.H5Index(self._network_dir, src_pop, trg_pop) + + return self._pop_groups[grp_key] + + def add_bio_conn(self, edge_type_id, src_id, src_pop, trg_id, trg_pop, syn_weight, sec_id, sec_x): + h5_grp = self._get_edge_group(src_pop, trg_pop) + h5_grp.add_bio_conn(edge_type_id, src_id, trg_id, syn_weight, sec_id, sec_x) + + def add_point_conn(self, edge_type_id, src_id, src_pop, trg_id, trg_pop, syn_weight): + h5_grp = self._get_edge_group(src_pop, trg_pop) + h5_grp.add_point_conn(edge_type_id, src_id, trg_id, syn_weight) + + def close(self): + for _, h5index in self._pop_groups.items(): + h5index.clean_ends() diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/save_synapses.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/save_synapses.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e7d394e3e62166d1125e923a7eeddef654c608d GIT binary patch literal 10371 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/xstim.py b/bmtk-vb/bmtk/simulator/bionet/modules/xstim.py new file mode 100644 index 0000000..f2192ff --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/modules/xstim.py @@ -0,0 +1,163 @@ +import os +import math +import pandas as pd +import numpy as np +import six +from neuron import h + +from bmtk.simulator.bionet.modules.sim_module import SimulatorMod +from bmtk.simulator.bionet.modules.xstim_waveforms import stimx_waveform_factory +from bmtk.simulator.bionet.utils import rotation_matrix +from bmtk.simulator.bionet.io_tools import io + + +class XStimMod(SimulatorMod): + def __init__(self, positions_file, waveform, mesh_files_dir=None, cells=None, set_nrn_mechanisms=True, + node_set=None): + self._positions_file = positions_file + self._mesh_files_dir = mesh_files_dir if mesh_files_dir is not None \ + else os.path.dirname(os.path.realpath(self._positions_file)) + + self._waveform = waveform # TODO: Check if waveform is a file or dict and load it appropiately + + self._set_nrn_mechanisms = set_nrn_mechanisms + self._electrode = None + self._cells = cells + self._local_gids = [] + self._fih = None + + #def __set_extracellular_mechanism(self): + # for gid in self._local_gids: + + def initialize(self, sim): + if self._cells is None: + # if specific gids not listed just get all biophysically detailed cells on this rank + self._local_gids = sim.biophysical_gids + else: + # get subset of selected gids only on this rank + self._local_gids = list(set(sim.local_gids) & set(self._all_gids)) + + self._electrode = StimXElectrode(self._positions_file, self._waveform, self._mesh_files_dir, sim.dt) + for gid in self._local_gids: + # cell = sim.net.get_local_cell(gid) + cell = sim.net.get_cell_gid(gid) + cell.setup_xstim(self._set_nrn_mechanisms) + self._electrode.set_transfer_resistance(gid, cell.get_seg_coords()) + + def set_pointers(): + for gid in self._local_gids: + cell = sim.net.get_cell_gid(gid) + #cell = sim.net.get_local_cell(gid) + cell.set_ptr2e_extracellular() + + self._fih = sim.h.FInitializeHandler(0, set_pointers) + + def step(self, sim, tstep): + for gid in self._local_gids: + cell = sim.net.get_cell_gid(gid) + # Use tstep +1 to match isee-engine existing results. This will make it so that it begins a step earlier + # than if using just tstep. + self._electrode.calculate_waveforms(tstep+1) + vext_vec = self._electrode.get_vext(gid) + cell.set_e_extracellular(vext_vec) + + +class StimXElectrode(object): + """ + Extracellular Stimulating electrode + """ + def __init__(self, positions_file, waveform, mesh_files_dir, dt): + self._dt = dt + self._mesh_files_dir = mesh_files_dir + + stimelectrode_position_df = pd.read_csv(positions_file, sep=' ') + + self.elmesh_files = stimelectrode_position_df['electrode_mesh_file'] + self.elpos = stimelectrode_position_df[['pos_x', 'pos_y', 'pos_z']].T.values + self.elrot = stimelectrode_position_df[['rotation_x', 'rotation_y', 'rotation_z']].values + self.elnsites = self.elpos.shape[1] # Number of electrodes in electrode file + self.waveform = stimx_waveform_factory(waveform) + + self.trans_X = {} # mapping segment coordinates + self.waveform_amplitude = [] + self.el_mesh = {} + self.el_mesh_size = [] + + self.read_electrode_mesh() + self.rotate_the_electrodes() + self.place_the_electrodes() + + def read_electrode_mesh(self): + el_counter = 0 + for mesh_file in self.elmesh_files: + file_path = mesh_file if os.path.isabs(mesh_file) else os.path.join(self._mesh_files_dir, mesh_file) + mesh = pd.read_csv(file_path, sep=" ") + mesh_size = mesh.shape[0] + self.el_mesh_size.append(mesh_size) + + self.el_mesh[el_counter] = np.zeros((3, mesh_size)) + self.el_mesh[el_counter][0] = mesh['x_pos'] + self.el_mesh[el_counter][1] = mesh['y_pos'] + self.el_mesh[el_counter][2] = mesh['z_pos'] + el_counter += 1 + + def place_the_electrodes(self): + + transfer_vector = np.zeros((self.elnsites, 3)) + + for el in range(self.elnsites): + mesh_mean = np.mean(self.el_mesh[el], axis=1) + transfer_vector[el] = self.elpos[:, el] - mesh_mean[:] + + for el in range(self.elnsites): + new_mesh = self.el_mesh[el].T + transfer_vector[el] + self.el_mesh[el] = new_mesh.T + + def rotate_the_electrodes(self): + for el in range(self.elnsites): + phi_x = self.elrot[el][0] + phi_y = self.elrot[el][1] + phi_z = self.elrot[el][2] + + rot_x = rotation_matrix([1, 0, 0], phi_x) + rot_y = rotation_matrix([0, 1, 0], phi_y) + rot_z = rotation_matrix([0, 0, 1], phi_z) + rot_xy = rot_x.dot(rot_y) + rot_xyz = rot_xy.dot(rot_z) + new_mesh = np.dot(rot_xyz, self.el_mesh[el]) + self.el_mesh[el] = new_mesh + + def set_transfer_resistance(self, gid, seg_coords): + + rho = 300.0 # ohm cm + r05 = seg_coords['p05'] + nseg = r05.shape[1] + cell_map = np.zeros((self.elnsites, nseg)) + for el in six.moves.range(self.elnsites): + + mesh_size = self.el_mesh_size[el] + + for k in range(mesh_size): + + rel = np.expand_dims(self.el_mesh[el][:, k], axis=1) + rel_05 = rel - r05 + r2 = np.einsum('ij,ij->j', rel_05, rel_05) + r = np.sqrt(r2) + if not all(i >= 10 for i in r): + io.log_exception('External electrode is too close') + cell_map[el, :] += 1. / r + + cell_map *= (rho / (4 * math.pi)) * 0.01 + self.trans_X[gid] = cell_map + + def calculate_waveforms(self, tstep): + simulation_time = self._dt * tstep + # copies waveform elnsites times (homogeneous) + self.waveform_amplitude = np.zeros(self.elnsites) + self.waveform.calculate(simulation_time) + + def get_vext(self, gid): + waveform_per_mesh = np.divide(self.waveform_amplitude, self.el_mesh_size) + v_extracellular = np.dot(waveform_per_mesh, self.trans_X[gid]) * 1E6 + vext_vec = h.Vector(v_extracellular) + + return vext_vec diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/xstim.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/xstim.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72dd0f70249ec1b5d3c2c1952894eccc868596cf GIT binary patch literal 7209 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/xstim_waveforms.py b/bmtk-vb/bmtk/simulator/bionet/modules/xstim_waveforms.py new file mode 100644 index 0000000..86e204d --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/modules/xstim_waveforms.py @@ -0,0 +1,127 @@ +import os +import numpy as np +import pandas as pd +import json +from six import string_types + +from bmtk.simulator.bionet.io_tools import io + +class BaseWaveform(object): + """Abstraction of waveform class to ensure calculate method is implemented""" + def calculate(self, simulation_time): + raise NotImplementedError("Implement specific waveform calculation") + + +class BaseWaveformType(object): + """Specific waveform type""" + def __init__(self, waveform_config): + self.amp = float(waveform_config["amp"]) # units? mA? + self.delay = float(waveform_config["del"]) # ms + self.duration = float(waveform_config["dur"]) # ms + + def is_active(self, simulation_time): + stop_time = self.delay + self.duration + return self.delay < simulation_time < stop_time + + +class WaveformTypeDC(BaseWaveformType, BaseWaveform): + """DC (step) waveform""" + def __init__(self, waveform_config): + super(WaveformTypeDC, self).__init__(waveform_config) + + def calculate(self, t): # TODO better name + if self.is_active(t): + return self.amp + else: + return 0 + + +class WaveformTypeSin(BaseWaveformType, BaseWaveform): + """Sinusoidal waveform""" + def __init__(self, waveform_config): + super(WaveformTypeSin, self).__init__(waveform_config) + self.freq = float(waveform_config["freq"]) # Hz + self.phase_offset = float(waveform_config.get("phase", np.pi)) # radians, optional + self.amp_offset = float(waveform_config.get("offset", 0)) # units? mA? optional + + def calculate(self, t): # TODO better name + if self.is_active(t): + f = self.freq / 1000. # Hz to mHz + a = self.amp + return a * np.sin(2 * np.pi * f * t + self.phase_offset) + self.amp_offset + else: + return 0 + + +class WaveformCustom(BaseWaveform): + """Custom waveform defined by csv file""" + def __init__(self, waveform_file): + self.definition = pd.read_csv(waveform_file, sep='\t') + + def calculate(self, t): + return np.interp(t, self.definition["time"], self.definition["amplitude"]) + + +class ComplexWaveform(BaseWaveform): + """Superposition of simple waveforms""" + def __init__(self, el_collection): + self.electrodes = el_collection + + def calculate(self, t): + val = 0 + for el in self.electrodes: + val += el.calculate(t) + + return val + + +# mapping from 'shape' code to subclass, always lowercase +shape_classes = { + 'dc': WaveformTypeDC, + 'sin': WaveformTypeSin, +} + + +def stimx_waveform_factory(waveform): + """ + Factory to create correct waveform class based on conf. + Supports json config in conf as well as string pointer to a file. + :rtype: BaseWaveformType + """ + if isinstance(waveform, string_types): + # if waveform_conf is str or unicode assume to be name of file in stim_dir + # waveform_conf = str(waveform_conf) # make consistent + file_ext = os.path.splitext(waveform) + if file_ext == 'csv': + return WaveformCustom(waveform) + + elif file_ext == 'json': + with open(waveform, 'r') as f: + waveform = json.load(f) + else: + io.log_warning('Unknwon filetype for waveform') + + shape_key = waveform["shape"].lower() + + if shape_key not in shape_classes: + io.log_warning("Waveform shape not known") # throw error? + + Constructor = shape_classes[shape_key] + return Constructor(waveform) + + +def iclamp_waveform_factory(conf): + """ + Factory to create correct waveform class based on conf. + Supports json config in conf as well as string pointer to a file. + :rtype: BaseWaveformType + """ + iclamp_waveform_conf = conf["iclamp"] + + shape_key = iclamp_waveform_conf["shape"].lower() + + if shape_key not in shape_classes: + io.log_warning('iclamp waveform shape not known') # throw error? + + Constructor = shape_classes[shape_key] + return Constructor(iclamp_waveform_conf) \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/modules/xstim_waveforms.pyc b/bmtk-vb/bmtk/simulator/bionet/modules/xstim_waveforms.pyc new file mode 100644 index 0000000000000000000000000000000000000000..560ade81178c29b63e2416c49ac1161e9669e4b2 GIT binary patch literal 6392 zcmdT|U2h{v6|J@%Cw81kCX-Dzuq+L%z$-)|Bs2>nSav4EF6=`j+Kd(g$m+PunReRU zJzeE|NRtTo%hsHqpEMJqgIt)Qb$XYZ>XfLrpxMRS$Q1aRLP2(cGOV^ z<(AT2HTG1rr2eYZE2aK$w5pM*MGl4*L*^ zvOP-27IAtqw3oRyuC~EmnKV8Ljs7A$)8nj|)^g?0CmgwhL}M9j2r16y$zn|v4EmhJ ze1_!lF;)miHqKH%8~f+wZ2l+-jqz>f>(tB&?T^A_G)qFO{i(Lkv&fH)A5Zf{PjzZ_ zG~$HS_kfdcAhB|*uRQe^*@Ij5(EI>Rzp2LhCfB2Q9FG>(F4w}I+>sw(z+qi-W%VZS=!_zCT&+XV1B*S4EPW5nTyHap- zp|~u?C>srjK^yt`9`9nK0k_4?8g8uZwLQz5Swzn7fUWE3@zLu>^h6DTQHwBYqLbN1 zGPrh$L~M-;_7hb60vVvT_EEn1m#s#jk>B)RW<9XCSg1f`5szM7*2EfjdbFE zXN8mg5*o)z7TSOpV_8I>@KUZNn&Iw=ED9!&+ahqP%N>rgbR3_|FXgs@ZzTpMpgDyt9hza)>khZ3YkH}8wgtt-N|^UfLAS- zBnJ&B2|=#0S^gFX5}P4a%b6xb1c1uh@cP_FKpF%TMnH51n@p$x=aQG)EsH-O8AVZ0 z?4(0y$=*Y@Qllip(}NLe2`{|7W*Gjc#SxNk~rYa zJEfj)s4HASlP8|Q0fxwtSyEe2`CGV|Esk%i9mrp@%W7u;H`uX`3mu*yft&s1CvZdI zHnPy?wnWv6FolQ#DK26)aT<`)!MBLq{C~w_c^E%$% z*UMQVk*=%g5aDe5HPdl0gIT@75BD+Ve={B5Lr*tLO;oqNrDJ+MyGK291!0J8X<}8EYi*~ z6690;#CXRPdB7trs0u>d=@M6u2cGUm&xjr8+mPICTlx>M#80^wuC-v24l+K~ zE-K6PRZJ4^Y(PgQ&x~M;PfonLFn+S)v1#b3_;mST#6di!8zunQG2? z^&F)W`11){>G7k&3Guc?N3aSgxw^VR*Z&yRQ0vg@7ax>L|LLE1MY+T4gH9LXLXCff zE-yCJ7f|r!Yk9$M+QTgF$w=ZasN^P!XJH~_*2&$SlgRiK-_o+R;mq%Ry*l|v*v(_^ zXaTgmglH5HY$HO1q++w&&_9uZ+5nbd!r;0TR|;aAho8L1gTfg?UM%8CMj zuL)I#a*p9&%N%-qxP=20?isVwE*daH| zqk>QUNB)~31zAFT3fdGRmdl9aP#hYn-Dl}(dXAN8NjQg(I99!F_v(2bBJM@QTd5!4 zu*y#}>z}6CdDy0Q0 zUK~X!?D-rS;vTV^k|r>;6N#_VV+9L5f%3zWR3u0VWv~i;9pJ$vRJ&Nt{|}U@Lt@`nat+#R=%n<5kD2_E2?ZL^;JZK-TS-T|g^ZhOeot%&o^Sv~e|Hd}hx6vz zB}QJL$uJ77+T9uz5~dF5DF`Xj*)+d&?*|Gf57Q_#!S|7Mv}z(};tR(~zAPAT-K#^b zdoI+v7iUA8Wr=a@<4oD{1Zqs;mlCyc!4K*JVQ)ay@1**axnCoh3rTXfZ03d4KX-B5 jk9m{EN74gz_($4ox3_z{z1{oUo$Y%~Z`td%I`91l0!N$A literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/morphology.py b/bmtk-vb/bmtk/simulator/bionet/morphology.py new file mode 100644 index 0000000..b0085fc --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/morphology.py @@ -0,0 +1,245 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +from neuron import h + + +pc = h.ParallelContext() # object to access MPI methods + + +class Morphology(object): + """Methods for processing morphological data""" + def __init__(self, hobj): + """reuse hoc object from one of the cells which share the same morphology/model""" + self.hobj = hobj + self.sec_type_swc = {'soma': 1, 'somatic': 1, # convert section name and section list names + 'axon': 2, 'axonal': 2, # into a consistent swc notation + 'dend': 3, 'basal': 3, + 'apic': 4, 'apical': 4} + self.nseg = self.get_nseg() + self._segments = {} + + def get_nseg(self): + nseg = 0 + for sec in self.hobj.all: + nseg += sec.nseg # get the total # of segments in the cell + return nseg + + def get_soma_pos(self): + n3dsoma = 0 + r3dsoma = np.zeros(3) + for sec in self.hobj.somatic: + n3d = int(h.n3d()) # get number of n3d points in each section + r3d = np.zeros((3, n3d)) # to hold locations of 3D morphology for the current section + n3dsoma += n3d + + for i in range(n3d): + r3dsoma[0] += h.x3d(i) + r3dsoma[1] += h.y3d(i) + r3dsoma[2] += h.z3d(i) + + r3dsoma /= n3dsoma + return r3dsoma + + def calc_seg_coords(self): + """Calculate segment coordinates from 3d point coordinates""" + ix = 0 # segment index + + p3dsoma = self.get_soma_pos() + self.psoma = p3dsoma + + p0 = np.zeros((3, self.nseg)) # hold the coordinates of segment starting points + p1 = np.zeros((3, self.nseg)) # hold the coordinates of segment end points + p05 = np.zeros((3, self.nseg)) + d0 = np.zeros(self.nseg) + d1 = np.zeros(self.nseg) + + for sec in self.hobj.all: + n3d = int(h.n3d()) # get number of n3d points in each section + p3d = np.zeros((3, n3d)) # to hold locations of 3D morphology for the current section + l3d = np.zeros(n3d) # to hold locations of 3D morphology for the current section + diam3d = np.zeros(n3d) # to diameters + + for i in range(n3d): + p3d[0, i] = h.x3d(i) - p3dsoma[0] + p3d[1, i] = h.y3d(i) - p3dsoma[1] # shift coordinates such to place soma at the origin. + p3d[2, i] = h.z3d(i) - p3dsoma[2] + diam3d[i] = h.diam3d(i) + l3d[i] = h.arc3d(i) + + l3d /= sec.L # normalize + nseg = sec.nseg + + l0 = np.zeros(nseg) # keep range of segment starting point + l1 = np.zeros(nseg) # keep range of segment ending point + l05 = np.zeros(nseg) + + for iseg, seg in enumerate(sec): + l0[iseg] = seg.x - 0.5*1/nseg # x (normalized distance along the section) for the beginning of the segment + l1[iseg] = seg.x + 0.5*1/nseg # x for the end of the segment + l05[iseg] = seg.x + + if n3d != 0: + p0[0, ix:ix+nseg] = np.interp(l0, l3d, p3d[0, :]) + p0[1, ix:ix+nseg] = np.interp(l0, l3d, p3d[1, :]) + p0[2, ix:ix+nseg] = np.interp(l0, l3d, p3d[2, :]) + d0[ix:ix+nseg] = np.interp(l0, l3d, diam3d[:]) + + p1[0, ix:ix+nseg] = np.interp(l1, l3d, p3d[0, :]) + p1[1, ix:ix+nseg] = np.interp(l1, l3d, p3d[1, :]) + p1[2, ix:ix+nseg] = np.interp(l1, l3d, p3d[2, :]) + d1[ix:ix+nseg] = np.interp(l1, l3d, diam3d[:]) + + p05[0,ix:ix+nseg] = np.interp(l05, l3d, p3d[0,:]) + p05[1,ix:ix+nseg] = np.interp(l05, l3d, p3d[1,:]) + p05[2,ix:ix+nseg] = np.interp(l05, l3d, p3d[2,:]) + else: + # If we are dealing with a stub axon, this compartment + # will be zero'd out in the calculation of transfer + # resistance in modules/ecp.py + + if sec not in self.hobj.axonal: + raise Exception("Non-axonal section with 0 3d points (stub)") + + if nseg != 1: + raise Exception("in calc_seg_coords(), n3d = 0, but nseg != 1") + + ix += nseg + + self.seg_coords = {} + + self.seg_coords['p0'] = p0 + self.seg_coords['p1'] = p1 + self.seg_coords['p05'] = p05 + + self.seg_coords['d0'] = d0 + self.seg_coords['d1'] = d1 + + return self.seg_coords + + def set_seg_props(self): + """Set segment properties which are invariant for all cell using this morphology""" + seg_type = [] + seg_area = [] + seg_x = [] + seg_dist = [] + seg_length = [] + + h.distance(sec=self.hobj.soma[0]) # measure distance relative to the soma + + for sec in self.hobj.all: + fullsecname = sec.name() + sec_type = fullsecname.split(".")[1][:4] # get sec name type without the cell name + sec_type_swc = self.sec_type_swc[sec_type] # convert to swc code + + for seg in sec: + + seg_area.append(h.area(seg.x)) + seg_x.append(seg.x) + seg_length.append(sec.L/sec.nseg) + seg_type.append(sec_type_swc) # record section type in a list + seg_dist.append(h.distance(seg.x)) # distance to the center of the segment + + self.seg_prop = {} + self.seg_prop['type'] = np.array(seg_type) + self.seg_prop['area'] = np.array(seg_area) + self.seg_prop['x'] = np.array(seg_x) + self.seg_prop['dist'] = np.array(seg_dist) + self.seg_prop['length'] = np.array(seg_length) + self.seg_prop['dist0'] = self.seg_prop['dist'] - self.seg_prop['length']/2 + self.seg_prop['dist1'] = self.seg_prop['dist'] + self.seg_prop['length']/2 + + def get_target_segments(self, edge_type): + # Determine the target segments and their probabilities of connections for each new edge-type. Save the + # information for each additional time a given edge-type is used on this morphology + # TODO: Don't rely on edge-type-table, just use the edge? + if edge_type in self._segments: + return self._segments[edge_type] + + else: + tar_seg_ix, tar_seg_prob = self.find_sections(edge_type.target_sections, edge_type.target_distance) + self._segments[edge_type] = (tar_seg_ix, tar_seg_prob) + return tar_seg_ix, tar_seg_prob + + """ + tar_sec_labels = edge_type.target_sections + drange = edge_type.target_distance + dmin, dmax = drange[0], drange[1] + + seg_d0 = self.seg_prop['dist0'] # use a more compact variables + seg_d1 = self.seg_prop['dist1'] + seg_length = self.seg_prop['length'] + seg_area = self.seg_prop['area'] + seg_type = self.seg_prop['type'] + + # Find the fractional overlap between the segment and the distance range: + # this is done by finding the overlap between [d0,d1] and [dmin,dmax] + # np.minimum(seg_d1,dmax) find the smaller of the two end locations + # np.maximum(seg_d0,dmin) find the larger of the two start locations + # np.maximum(0,overlap) is used to return zero when segments do not overlap + # and then dividing by the segment length + frac_overlap = np.maximum(0, (np.minimum(seg_d1, dmax) - np.maximum(seg_d0, dmin))) / seg_length + ix_drange = np.where(frac_overlap > 0) # find indexes with non-zero overlap + ix_labels = np.array([], dtype=np.int) + + for tar_sec_label in tar_sec_labels: # find indexes within sec_labels + sec_type = self.sec_type_swc[tar_sec_label] # get swc code for the section label + ix_label = np.where(seg_type == sec_type) + ix_labels = np.append(ix_labels, ix_label) # target segment indexes + + tar_seg_ix = np.intersect1d(ix_drange, ix_labels) # find intersection between indexes for range and labels + tar_seg_length = seg_length[tar_seg_ix] * frac_overlap[tar_seg_ix] # weighted length of targeted segments + tar_seg_prob = tar_seg_length / np.sum(tar_seg_length) # probability of targeting segments + + self._segments[edge_type] = (tar_seg_ix, tar_seg_prob) + return tar_seg_ix, tar_seg_prob + """ + + def find_sections(self, target_sections, distance_range): + dmin, dmax = distance_range[0], distance_range[1] + + seg_d0 = self.seg_prop['dist0'] # use a more compact variables + seg_d1 = self.seg_prop['dist1'] + seg_length = self.seg_prop['length'] + seg_area = self.seg_prop['area'] + seg_type = self.seg_prop['type'] + + # Find the fractional overlap between the segment and the distance range: + # this is done by finding the overlap between [d0,d1] and [dmin,dmax] + # np.minimum(seg_d1,dmax) find the smaller of the two end locations + # np.maximum(seg_d0,dmin) find the larger of the two start locations + # np.maximum(0,overlap) is used to return zero when segments do not overlap + # and then dividing by the segment length + frac_overlap = np.maximum(0, (np.minimum(seg_d1, dmax) - np.maximum(seg_d0, dmin))) / seg_length + ix_drange = np.where(frac_overlap > 0) # find indexes with non-zero overlap + ix_labels = np.array([], dtype=np.int) + + for tar_sec_label in target_sections: # find indexes within sec_labels + sec_type = self.sec_type_swc[tar_sec_label] # get swc code for the section label + ix_label = np.where(seg_type == sec_type) + ix_labels = np.append(ix_labels, ix_label) # target segment indexes + + tar_seg_ix = np.intersect1d(ix_drange, ix_labels) # find intersection between indexes for range and labels + tar_seg_length = seg_length[tar_seg_ix] * frac_overlap[tar_seg_ix] # weighted length of targeted segments + tar_seg_prob = tar_seg_length / np.sum(tar_seg_length) # probability of targeting segments + return tar_seg_ix, tar_seg_prob diff --git a/bmtk-vb/bmtk/simulator/bionet/morphology.pyc b/bmtk-vb/bmtk/simulator/bionet/morphology.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe2f023078a038542737854960e7ac1d52f3dfbb GIT binary patch literal 6182 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 Q{FI5j=HZ&wY^7QG4<0?dP5=M^ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/nml_reader.py b/bmtk-vb/bmtk/simulator/bionet/nml_reader.py new file mode 100644 index 0000000..c64b9cd --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/nml_reader.py @@ -0,0 +1,168 @@ +class NMLTree(object): + nml_ns = '{http://www.neuroml.org/schema/neuroml2}' + element_registry = {} + + def __init__(self, nml_path): + from xml.etree import ElementTree + self._nml_path = nml_path + self._nml_root = ElementTree.parse(nml_path).getroot() + #self._relevant_elements = { + # NMLTree.ns_name('channelDensity'): ChannelDensity, + # NMLTree.ns_name('resistivity'): Resistivity + #} + + # For each section store a list of all the NML elements include + self._soma_props = {} + self._axon_props = {} + self._dend_props = {} + self._apic_props = {} + # For lookup by segmentGroup attribute, include common synonyms for diff sections + self._section_maps = { + 'soma': self._soma_props, 'somatic': self._soma_props, + 'axon': self._axon_props, 'axonal': self._axon_props, + 'dend': self._dend_props, 'basal': self._dend_props, 'dendritic': self._dend_props, + 'apic': self._apic_props, 'apical': self._apic_props + } + + self._parse_root(self._nml_root) + + @classmethod + def ns_name(cls, name): + return '{}{}'.format(cls.nml_ns, name) + + @staticmethod + def common_name(elem): + if '}' in elem: + return elem.split('}')[-1] + else: + return elem + + @staticmethod + def parse_value(value): + val_list = value.split(' ') + if len(val_list) == 2: + return float(val_list[0]), val_list[1] + elif len(val_list) == 1: + return float(val_list[0]), 'NONE' + else: + raise Exception('Cannot parse value {}'.format(value)) + + @classmethod + def register_module(cls, element_cls): + cls.element_registry[cls.ns_name(element_cls.element_tag())] = element_cls + return element_cls + + def _parse_root(self, root): + for elem in root.iter(): + if elem.tag in NMLTree.element_registry: + nml_element = NMLTree.element_registry[elem.tag](elem) + self._add_param(nml_element) + + def _add_param(self, nml_element): + seggroup_str = nml_element.section + if seggroup_str is None: + raise Exception('Error: tag {} in {} is missing segmentGroup'.format(nml_element.id, self._nml_path)) + elif seggroup_str.lower() == 'all': + sections = ['soma', 'axon', 'apic', 'dend'] + else: + sections = [seggroup_str.lower()] + + for sec_name in sections: + param_table = self._section_maps[sec_name] + if sec_name in param_table: + raise Exception('Error: {} already has a {} element in {}.'.format(nml_element.id, sec_name, + self._nml_path)) + + self._section_maps[sec_name][nml_element.id] = nml_element + + def __getitem__(self, section_name): + return self._section_maps[section_name] + + +class NMLElement(object): + def __init__(self, nml_element): + self._elem = nml_element + self._attribs = nml_element.attrib + + self.tag_name = NMLTree.common_name(self._elem.tag) + self.section = self._attribs.get('segmentGroup', None) + self.id = self._attribs.get('id', self.tag_name) + + @staticmethod + def element_tag(): + raise NotImplementedError() + + +@NMLTree.register_module +class ChannelDensity(NMLElement): + def __init__(self, nml_element): + super(ChannelDensity, self).__init__(nml_element) + self.ion = self._attribs['ion'] + self.ion_channel = self._attribs['ionChannel'] + + if 'erev' in self._attribs: + v_list = NMLTree.parse_value(self._attribs['erev']) + self.erev = v_list[0] + self.erev_units = v_list[1] + else: + self.erev = None + + v_list = NMLTree.parse_value(self._attribs['condDensity']) + self.cond_density = v_list[0] + self.cond_density_units = v_list[1] + + @staticmethod + def element_tag(): + return 'channelDensity' + + +@NMLTree.register_module +class ChannelDensityNernst(ChannelDensity): + + @staticmethod + def element_tag(): + return 'channelDensityNernst' + + +@NMLTree.register_module +class Resistivity(NMLElement): + def __init__(self, nml_element): + super(Resistivity, self).__init__(nml_element) + v_list = NMLTree.parse_value(self._attribs['value']) + self.value = v_list[0] + self.value_units = v_list[1] + + @staticmethod + def element_tag(): + return 'resistivity' + + +@NMLTree.register_module +class SpecificCapacitance(NMLElement): + def __init__(self, nml_element): + super(SpecificCapacitance, self).__init__(nml_element) + v_list = NMLTree.parse_value(self._attribs['value']) + self.value = v_list[0] + self.value_units = v_list[1] + + @staticmethod + def element_tag(): + return 'specificCapacitance' + + +@NMLTree.register_module +class ConcentrationModel(NMLElement): + def __init__(self, nml_element): + super(ConcentrationModel, self).__init__(nml_element) + self.type = self._attribs['type'] + v_list = NMLTree.parse_value(self._attribs['decay']) + self.decay = v_list[0] + self.decay_units = v_list[1] + + v_list = NMLTree.parse_value(self._attribs['gamma']) + self.gamma = v_list[0] + self.gamma_units = v_list[1] + + @staticmethod + def element_tag(): + return 'concentrationModel' diff --git a/bmtk-vb/bmtk/simulator/bionet/nml_reader.pyc b/bmtk-vb/bmtk/simulator/bionet/nml_reader.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5db337ae0b65885528472664b1de271d237ff689 GIT binary patch literal 7715 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 Q&~r96@Zg_08!Ptt57js`-~a#s literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/nrn.py b/bmtk-vb/bmtk/simulator/bionet/nrn.py new file mode 100644 index 0000000..c5f8419 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/nrn.py @@ -0,0 +1,82 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import sys +import os +import glob +import neuron +from neuron import h + +from bmtk.simulator.bionet.pyfunction_cache import py_modules +from bmtk.simulator.bionet.pyfunction_cache import load_py_modules +from bmtk.simulator.bionet.pyfunction_cache import synapse_model, synaptic_weight, cell_model + + +pc = h.ParallelContext() + + +def quit_execution(): # quit the execution with a message + pc.done() + sys.exit() + return + + +def clear_gids(): + pc.gid_clear() + pc.barrier() + + +def load_neuron_modules(mechanisms_dir, templates_dir, default_templates=True): + """ + + :param mechanisms_dir: + :param templates_dir: + :param default_templates: + """ + h.load_file('stdgui.hoc') + + bionet_dir = os.path.dirname(__file__) + # h.load_file(os.path.join(bionet_dir, 'import3d.hoc')) # customized import3d.hoc to supress warnings + # h.load_file('import3d.hoc') + h.load_file(os.path.join(bionet_dir,'default_templates', 'advance.hoc')) + + if mechanisms_dir is not None: + neuron.load_mechanisms(str(mechanisms_dir)) + + # if default_templates: + # load_templates(os.path.join(bionet_dir, 'default_templates')) + + # if templates_dir: + # load_templates(templates_dir) + + +def load_templates(template_dir): + """Load all templates to be available in the hoc namespace for instantiating cells""" + cwd = os.getcwd() + os.chdir(template_dir) + + hoc_templates = glob.glob("*.hoc") + + for hoc_template in hoc_templates: + h.load_file(str(hoc_template)) + + os.chdir(cwd) diff --git a/bmtk-vb/bmtk/simulator/bionet/nrn.pyc b/bmtk-vb/bmtk/simulator/bionet/nrn.pyc new file mode 100644 index 0000000000000000000000000000000000000000..826481f8473e1f161c0e6dcd51f54def8dffd2c2 GIT binary patch literal 2009 zcmb_d-*4kY5FXq4(d3eA?vS`cI_ZoAC<3YBiH8$HNCk-pRFDfoSgOp%-X_ki?VNYh zYo+$-?hWzR@Tc(yz&Eo^QgIJF6el~Gnf>9J`M#Y*e~pK~fBVN-PPb2p->)(3pAZGU z0U4rnrw5b>;p;4Bz;K50lh^u6DkJce8jPyPKSuQ{2R~7G=S<@ zEWOH8>#MA4ijC3M`wrnVjViJ`6A|wlL)fcYt*z!A+BjY+9#`hsYh5mu&he4CHpVa6 zRgqtM%*&rbIHFa+nFKDRRU{fJb1DIxOgp@U%f8MX>qS#*$067&D-L~Gx+!eh3;PAk z+0RzDc6KpWt#V6M&1{>`=9PQtg)OU%QLbrc^OBF8)op#WzH$$-_0^_ynZDHd#$oPb zcx(@Xf+0TtnFaC*|D6Q~obVZL0I?{GEH_%Ut`7?Vwxw?O;fr>k9Hi`geug*M&+rb* zrQ`T>j)aXa#xaI1AUp;j$eUmb!>U8B%l1ulJMH0F_2?Q@2D1SbzSwXLR66u>K<#hj z4rnzX01qeTc)FJ9(<#5v{t7W1#&}+=0ZbKFI$x@~w3W?@vVC!P%IRtii1fQNg+5ms zdLE8~oUbiYhlfz-49f2M-UOnDx5x)b z61UlN>L#Si59vCjZ_YjtLZN$g2~`4>ZdvkbM!)b`*zairik(iGt@a0q@6m)}WlX#) zOYE9>u48qfN~7jR$7LP6rH%n<%n;eN%5{9+w3x6?)vi>otQRr&BWszApL+QD4ki-_ zL2aR3{<;A5(Ycn41!uc3&0H9gaF+;YVl_Y(rR3RZ+N~H>^a?zryYE)Qlyc1`g_3pAW z0ixJX@Y-M7*Gld8owZGPC${(a%w^BzJC~W(KO1ZJ|9Q0AW&{(r>dB9Ih6Be}@L z$i5N7aUgjh`+=^tByY*2E&FXT*2uUcZ$a%_qVzdA7 z=^JpK_FZSaq6p8HKOPs^r!Sg3;*P#9#WYrjQ3WZIIp zl<7zcT?6bv2aeS$~xRAA&rYRCzYBg-?7|7QOPYm-K$M$?<9LN#X6N zPQ2Yoigf2N%Wbc)Cp9+t)5`XSuyJ?Tf{4ZQm@kYaeot ztB3I@OWpfXVZHO0xn|Rmjp;*-4Sibg+bSwQRHi5)YYj68K*ajjWqsVoNR&8wx%a!X zwcC4jkN}^fWU>c!dj}JL+z^*dPVxk{>@^bhD#o^IVyn1Srx8_tiN{fsn44zJg!m5W zJbk5I)aCAh0MW4_Xn+$C0oR$K)kr{2b`|45^YuJ@Olk4Vz=8FT%R=2dDi6lW4CozI zRYM)qGEcPtuFv_1Q0k6icmW77wiN3Kg_s+S@ARLK`py;jyYLAk(dT*-ZtIP3Kj$y-M{hF@Bx2uip)wrT8>dV~^=Si+ju(K@*e5WpnDmr&3V6?;YsSsV|`qQL7~0&@+kSc;MOSaLd_ zRFy5#h*c6%P{bXc!p}E~K1DVK!K%o^V5Mizcd9xkl^f-8@EGT8(v+IZZNX z5Hv`hN%g@oajqRwTXq4lV!p%hb8*5scUV5bbkiV%RxqS;y2cdMP#l8+mH_?Di}@)5 z(BXcA`64WjWTv4&NiznEt^{U*Iyi=*^}z%hLR-Fv@RxVw55!_o^iV2}Wqvk$*n6t5>`e(Cy_`kc>GyxWgM!{uoG%l0!m30w;i|&%J-n9M>%X*Ud=o?P54m1RMEH~xG znyQ_zqa83806ZH?RpWq;(VCLVS@#=e=Eri^+%aF9Tfv}7FybC-`r!V-l#Am@nV#ep zL-vk1PRn5&>){^pe2W;?rf0F@qFs{jK;Ao|+()}s@VNQ{kE2^%lytZfw!_x??Xatt zK83uw$2DE_TkP|1*NioX;gw3GVCWhBf2uq;W?43XajDd z*Szy!h)=(znV%CFk|G7df}N2L*=RRa!3Qo0uCwVSMw!fzOf~^!w&AlgvWtGu2|aIh zlt0Vw7WXokL()!q)9rPz+2@Y+cyh`W&AcSQu@Scdevays;5xsg(CAvS?J=*!<6yGX z$;c2%>W~six}&syaE9egUr)t9qp#NyKb!*ZQi|!+nQYumMTcO7d_k36Tv3~xGj0{B?>`VZ|Im#x|HJ>-z}HjD-mrYDh#hPY3wB5>V1tlggIFODtXQF}yWkIi-|su~xV~{pTJDXgYI}~) z`+VPbzV}SyUq>dN`~Ib`1*-U&!2jp*=wG1-@n@;7QY%&6QY)79+p24;t%h1@C@XBJ zsHxU4&{VgSdP}KSRwh(5!P-hog%j*uIi$jtR1T}~kW|{-KAKYDVRg$^E7MYMt6Mna zNI5hqLr2Tzlr(49tpBFft)^O;WzTZw2mp`&ji05Cg=$WWZf1T@=hLWpU)j0RPl7y7 z6I~%~W8e#T^j}d#098<2vAH23Z>Z3gN|Q(d+gpIJr6OQt5JnS788m^EL36T1b4peL zM@$2bSOpwGCW9sr#1TLcsG%|=lYtRNXJzt{a`G`19+f$B(mzx7KcK={>3>kx9+R~X zsc=p@A6D*i=K*5(H_&4_&-y{$&mw0n&76(28^+1H6ZCbSZaIA&Wlx3CTAW0o)7!~6 z(!^OSsWm@2>EP$(xZ8Cw8$0-pj&dhmtLHdD7Wp|AukJYcMkE`~JIfnszZ*J9nmc|F zL|S7k)=n5_QIL0coYj8rWIpC+vVbe)s4xmr^rxA204vVQF{6U#Nykq@E|8OYJKk0r z_PddGg07DvZunX3uXZt-mZ!!^7~hD)zTd5PnRmWONYEXHa%jKj#7Uk?UGF4*FOLK7 zW)!b)`ZgQ(jjR8jYWqYVwR2((X}r5CTcFD*HofIC>- zH1lN>;B(*-zg$$#!ZMJrIEkFjhn)NZ@4AB`R%#aiY_NpL$x^3@CHZj_ zgEHcIIIqB8A23?usPj_?t%cx4h6p-;?2ZYtyqBfq3f!}iSOTJ>?iz7WQfT6|a1HFK z7jCTjWEOvGL1)3j>Q;W;6guAOcgZaas}SxeU+C?Wyy69Zuo2CJqYq;r&x?~d_q?;H zYj!BBEx$oYMgA8meijvwa1(zPm6^qcJ=karHkxH)fT~nKF0VxtQP7S2%x$obSh^e) zq}>*4ct(q`OZ=Y1Qy_Z8;%~sS`4A96^c*U?4W%DayHu?Nc? zqN9a;)B~E_--8?kkPp$UA|31TjVN&ve=9PISco7@Cb;H2pQBQdn`)ycy%A+MvpA1N zPm7#N&<{g$^q^QjLu?3*vLCYUcNgj83%VW6Q8EiX**dn_GFZ)isTsHoEK=AQSBPNF6=G4me5W@K zu_8-(UTp|GFtNZYq_+aX15Qn3^;dCOLN6Kc1q^y6w5716d-Kj*2FOcVX0 zerj{Y-48H1`{58(+Y!E#;usdWb1X;}m&~0LE`U4+o;-@3sc>qTuAoN~5@pR<_pWW0 zjQkK%myA13laZ4sHN^T$XqHk$0QgW>MF)S zcwTOe!b^m-V8#*y{ud1B@C-3y!GN_+;98g98+PJaqZr1C1DS`~p~k~SPl*R_fKP?T zy<6~FghbbIm)b0guB(M(40l7CRaUt44X>fo*x>gr6+~eCQo-vG`%;3eUty21)dA+^ zAhTKcK|*k0DN#vdvWHX?``~@G0{5PsoI-lVg&n)vp#c|Oufu`YE0EtCg~h8LJB7&+ z`vDFsc;APM4WnMT*tLKP4pIn8!s!*$j8#f$BP-KY-N-r-BowL?Qv$~h(WNvMNSSP+ zG+_gRCfIhttmrrPTx|aNVgOMS78twV;DTacS?uacLXz+a^unmGZyM$)`U!tvPSdf zo?t<$xu;k#40k`m;-f51qo|1?wNb^}E-eht)Mrt&+AXV%VtQg`qTOsa+6~mq|0e(Q znQjJ{S(!HR>VJ%mT&y%wX5gdiw#r^pxj64&wBzVS8&;c*?_e$>^oDvH4oQKxugrCH&RuZk=Zj=rdbjl*a^UA57e0&=FACQq2@mw+=k9Uqmy*C_-fFbr ztvIU@f_^{GnQ#duLl2&oe}c z$w@IDCHz5gZr~vUYnmq9E*JnB^ar)}M7Ie%MTY!rUE`kN>)&M6xX^SIJDfKvj+Z1e zvGka8hM0bnn-g4R2gIR-T(h4t39w&1Rw_wbU2mzcTH8p>F_%pg#Um?RhuT-*HsEum z=AVWj04c#@q^M|~wzwW8(Hp(&;&;&yJX&fW9+Fx96b2!oNRmN-0QqrThlriMU+nBs z?8GD({FVg4_i?K(v31X(7}H(4PhjsK;wkP=u0xb|&7Go^>6jNO4lLp8V}Z@4j*bdTF>qimrD^2U^l5w2dH@JE8>g+A z!@@UoKqx973~TWmAbmG$G5B(zoBHA4OON^gNe|v1iTMfe8!ct3WT6mUl{+b95)_~fXuzlyS+C|kN)Yv$619~Zz`c6)AOsja$}VmGpYas=zByZ7 zMiMeUP|}?iuP2;9uRn`xy@48Eqghxv{@v*TMU}zY;J^0V<69Ndcbsk0A>1 z?#bO14^Xv&!x8!;?gwKt><3SWMg#naB9A8WZ<{TS>1P#!VrYpsUQ_GnKg>}i+AhEP zsM?dMn{Bl@$+yAiea%+emoYZaG46yCJ56z?>~)1?%jOYwZXT5dIC}eag};{*$psZa zoUphQ&{aVE<_;hv+gAdCB;XJbf4mzIw*`oqAs}w6?b`+jd_!m`@lyf99suH^HB1Rm zucic&m_tA;S$CntrbSB34gs-gZEspa3Hi)R6tYc9Ko3`x`28I~KpE9Q;Pa*-ApUYU zAl?xmjtv3vj0HORhRly#DL#IbB}1`>m~Tmxz6ZVd2+q+2 aY8u$jHXgo)7Y}FJC)y`wTGOo={Qd_zq`TDs literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/schemas/config_schema.json b/bmtk-vb/bmtk/simulator/bionet/schemas/config_schema.json new file mode 100644 index 0000000..780e7bd --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/schemas/config_schema.json @@ -0,0 +1,131 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + + "properties": { + "target_simulator": {"$ref": "#/definitions/target_simulator"}, + "components": {"$ref": "#/definitions/components"}, + "networks": { + "type": "object", + "properties": { + "node_files": {"$ref": "#/definitions/nodes_files"}, + "edge_files": {"$ref": "#/definitions/edges_files"} + } + }, + "run": {"$ref": "#/definitions/run"}, + "groups": {"$ref": "#/definitions/groups"}, + "output": {"$ref": "#/definitions/output"}, + "conditions": {"$ref": "#/definitions/conditions"}, + "input": { + "type": "array", + "items": { + "oneOf": [ + {"$ref": "#/definitions/input_file"} + ] + } + } + }, + + "definitions": { + "target_simulator": { + "type": "string" + }, + + "components": { + "type": "object", + "properties": { + "synaptic_models_dir": {"type": "directory", "exists": true}, + "mechanisms_dir": {"type": "directory", "exists": true}, + "morphologies_dir": {"type": "directory", "exists": true}, + "biophysical_neuron_models_dir": {"type": "directory", "exists": true}, + "point_neuron_models_dir": {"type": "directory", "exists": true}, + "templates_dir": {"type": "directory", "exists": true} + } + }, + + "edges": { + "type": "array", + "items": { + "type": "object", + "properites": { + "edges_file": {"type": "file", "exists": true}, + "edge_types_file": {"type": "file", "exists": true} + } + } + }, + + "nodes": { + "type": "array", + "items": { + "type": "object", + + "properties": { + "nodes_file": {"type": "file", "exists": true}, + "node_types_file": {"type": "file", "exists": true} + } + } + }, + + "run": { + "type": "object", + "properties": { + "tstop": {"type": "number", "minimum": 0}, + "dt": {"type": "number", "minimum": 0}, + "dL": {"type": "number", "minimum": 0}, + "overwrite_output_dir": {"type": "boolean"}, + "spike_threshold": {"type": "number"}, + "save_state": {"type": "boolean"}, + "start_from_state": {"type": "boolean"}, + "nsteps_block": {"type": "number", "minimum": 0}, + "save_cell_vars": {"type": "array"}, + "calc_ecp": {"type": "boolean"}, + "optocell": {"type": "array", "items": {"type": "string"}} + } + }, + + "node_id_selections": { + "type": "object", + "properties": { + "save_cell_vars": {"type": "array", "items": {"type": "number"}} + } + }, + + "output": { + "type": "object", + "properties": { + "log_file": {"type": "file"}, + "spikes_ascii": {"type": "file"}, + "spikes_h5": {"type": "file"}, + "cell_vars_dir": {"type": "file"}, + "extra_cell_vars": {"type": "file"}, + "ecp_file": {"type": "file"}, + "state_dir": {"type": "directory"}, + "output_dir": {"type": "directory"} + } + }, + + "conditions": { + "type": "object", + "properties": { + "celsius": {"type": "number"}, + "v_init": {"type": "number"}, + "cao0": {"type": "number"} + } + }, + + "extracellular_electrode": { + "type": "object", + "properties": { + "positions": {"type": "file"} + } + }, + + "input_file": { + "type": "object", + "properties": { + "format": {"type": "string", "enum": ["nwb", "csv"]}, + "file": {"type": "file", "exists": true} + } + } + } +} diff --git a/bmtk-vb/bmtk/simulator/bionet/schemas/csv_edge_types.json b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_edge_types.json new file mode 100644 index 0000000..b3e6f59 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_edge_types.json @@ -0,0 +1,20 @@ +{ + "file_type": "csv", + "file_properties": { + "sep": " " + }, + + "columns": { + "edge_type_id": {"required": true}, + "target_query": {"required": false}, + "source_query": {"required": false}, + "weight_max": {"required": true}, + "weight_function": {"required": false}, + "weight_sigma": {"required": false}, + "distance_range": {"required": true}, + "target_sections": {"required": true}, + "delay": {"required": true}, + "params_file": {"required": true}, + "set_params_function": {"required": true} + } +} \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_external.json b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_external.json new file mode 100644 index 0000000..5dd3a08 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_external.json @@ -0,0 +1,11 @@ +{ + "file_type": "csv", + "file_properties": { + "sep": " " + }, + + "columns": { + "node_type_id": {"required": true}, + "level_of_detail": {"required": true} + } +} \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_internal.json b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_internal.json new file mode 100644 index 0000000..6dc2188 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_node_types_internal.json @@ -0,0 +1,15 @@ +{ + "file_type": "csv", + "file_properties": { + "sep": " " + }, + + "columns": { + "node_type_id": {"required": true}, + "params_file": {"required": true}, + "level_of_detail": {"required": true}, + "morphology_file": {"required": true}, + "rotation_angle_zaxis": {"required": true}, + "set_params_function": {"required": true} + } +} \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_external.json b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_external.json new file mode 100644 index 0000000..e7240b0 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_external.json @@ -0,0 +1,11 @@ +{ + "file_type": "csv", + "file_properties": { + "sep": " " + }, + + "columns": { + "node_id": {"required": true}, + "node_type_id": {"required": true} + } +} \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_internal.json b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_internal.json new file mode 100644 index 0000000..f2287b0 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/schemas/csv_nodes_internal.json @@ -0,0 +1,19 @@ +{ + "file_type": "csv", + "file_properties": { + "sep": " " + }, + + "columns": { + "node_id": {"required": true}, + "node_type_id": {"required": true}, + "x_soma": {"required": true}, + "y_soma": {"required": true}, + "z_soma": {"required": true}, + "rotation_angle_yaxis": {"required": true}, + "pop_name": {"required": true}, + "ei": {"required": true}, + "location": {"required": false}, + "tuning_angle": {"required": false} + } +} \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/bionet/sonata_adaptors.py b/bmtk-vb/bmtk/simulator/bionet/sonata_adaptors.py new file mode 100644 index 0000000..91982c6 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/sonata_adaptors.py @@ -0,0 +1,142 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import types +import numpy as np + +from bmtk.simulator.core.sonata_reader import NodeAdaptor, SonataBaseNode, EdgeAdaptor, SonataBaseEdge +from bmtk.simulator.bionet import nrn + + +class BioNode(SonataBaseNode): + @property + def position(self): + return self._prop_adaptor.position(self._node) + + @property + def morphology_file(self): + return self._node['morphology'] + + @property + def rotation_angle_xaxis(self): + return self._prop_adaptor.rotation_angle_xaxis(self._node) + + @property + def rotation_angle_yaxis(self): + # TODO: Combine rotation alnges into a single property + return self._prop_adaptor.rotation_angle_yaxis(self._node) + + @property + def rotation_angle_zaxis(self): + return self._prop_adaptor.rotation_angle_zaxis(self._node) + + def load_cell(self): + model_template = self.model_template + template_name = model_template[1] + model_type = self.model_type + if nrn.py_modules.has_cell_model(self['model_template'], model_type): + cell_fnc = nrn.py_modules.cell_model(self['model_template'], model_type) + else: + cell_fnc = nrn.py_modules.cell_model(model_template[0], model_type) + + dynamics_params = self.dynamics_params + hobj = cell_fnc(self, template_name, dynamics_params) + + for model_processing_str in self.model_processing: + processing_fnc = nrn.py_modules.cell_processor(model_processing_str) + hobj = processing_fnc(hobj, self, dynamics_params) + + return hobj + + +class BioNodeAdaptor(NodeAdaptor): + def get_node(self, sonata_node): + return BioNode(sonata_node, self) + + @classmethod + def patch_adaptor(cls, adaptor, node_group, network): + node_adaptor = NodeAdaptor.patch_adaptor(adaptor, node_group, network) + + # Position + if 'positions' in node_group.all_columns: + node_adaptor.position = types.MethodType(positions, adaptor) + elif 'position' in node_group.all_columns: + node_adaptor.position = types.MethodType(position, adaptor) + else: + node_adaptor.position = types.MethodType(positions_default, adaptor) + + # Rotation angles + if 'rotation_angle_xaxis' in node_group.all_columns: + node_adaptor.rotation_angle_xaxis = types.MethodType(rotation_angle_x, node_adaptor) + else: + node_adaptor.rotation_angle_xaxis = types.MethodType(rotation_angle_default, node_adaptor) + + if 'rotation_angle_yaxis' in node_group.all_columns: + node_adaptor.rotation_angle_yaxis = types.MethodType(rotation_angle_y, node_adaptor) + else: + node_adaptor.rotation_angle_yaxis = types.MethodType(rotation_angle_default, node_adaptor) + + if 'rotation_angle_zaxis' in node_group.all_columns: + node_adaptor.rotation_angle_zaxis = types.MethodType(rotation_angle_z, node_adaptor) + else: + node_adaptor.rotation_angle_zaxis = types.MethodType(rotation_angle_default, node_adaptor) + + return node_adaptor + + +def positions_default(self, node): + return np.array([0.0, 0.0, 0.0]) + + +def positions(self, node): + return node['positions'] + + +def position(self, node): + return node['position'] + + +def rotation_angle_default(self, node): + return 0.0 + + +def rotation_angle_x(self, node): + return node['rotation_angle_xaxis'] + + +def rotation_angle_y(self, node): + return node['rotation_angle_yaxis'] + + +def rotation_angle_z(self, node): + return node['rotation_angle_zaxis'] + + +class BioEdge(SonataBaseEdge): + def load_synapses(self, section_x, section_id): + synapse_fnc = nrn.py_modules.synapse_model(self.model_template) + return synapse_fnc(self.dynamics_params, section_x, section_id) + + +class BioEdgeAdaptor(EdgeAdaptor): + def get_edge(self, sonata_edge): + return BioEdge(sonata_edge, self) diff --git a/bmtk-vb/bmtk/simulator/bionet/sonata_adaptors.pyc b/bmtk-vb/bmtk/simulator/bionet/sonata_adaptors.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea79c888f2dce0219a3cfe0a7efd2a593fc4428a GIT binary patch literal 5721 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 N!2ja!td?r4e*<79uucE~ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/bionet/virtualcell.py b/bmtk-vb/bmtk/simulator/bionet/virtualcell.py new file mode 100644 index 0000000..64b3929 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/bionet/virtualcell.py @@ -0,0 +1,51 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from neuron import h + + +class VirtualCell(object): + """Representation of a Virtual/External node""" + + def __init__(self, node, spike_train_dataset): + # VirtualCell is currently not a subclass of bionet.Cell class b/c the parent has a bunch of properties that + # just don't apply to a virtual cell. May want to make bionet.Cell more generic in the future. + self._node_id = node.node_id + self._hobj = None + self._spike_train_dataset = spike_train_dataset + self._train_vec = [] + self.set_stim(node, self._spike_train_dataset) + + @property + def node_id(self): + return self._node_id + + @property + def hobj(self): + return self._hobj + + def set_stim(self, stim_prop, spike_train): + """Gets the spike trains for each individual cell.""" + self._train_vec = h.Vector(spike_train.get_spikes(self.node_id)) + vecstim = h.VecStim() + vecstim.play(self._train_vec) + self._hobj = vecstim diff --git a/bmtk-vb/bmtk/simulator/bionet/virtualcell.pyc b/bmtk-vb/bmtk/simulator/bionet/virtualcell.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d11300f1f81dc2513df76086b9aa50fec541d8b0 GIT binary patch literal 1594 zcmcIkO>5gg5FLHksndi6S|}7+bSi2N(H|(KG=$u8P-vimvaF?bEJv1%c5E6*PwB1n z*Y*dr^G0zJ=%okO){JI%$2&7`-Z=WTyZz(%=a+)+K9RgXi1-6hr91)EMDu_y0t(8I zTu76E%80%b-4cDih^UO|EFFk@^;ng*IV``ZuAJe{R*fkwTkn{+anNtAT%q^Kw((reQ{>LL z%)5G`1|sk??EHOID%+$=;m$sJ*ZFKUww>jvooBu)vhkd+dgZJ6vbNlI*;r6?oUQu) z71F=M#dTkhshWz7d4+&Tl%kzrq~8KKtVOW}8o=rSBpm0cK|#w$utYYNV-9+~5%Dl6 z{O6p|mj9HKb_pFnc$60~KpXK^tWLyW!AjT_(oDHGi!cK6fRM4C4lsD%gq)F5vFD*o zr>x<$g(*5qQM=DXojCRhPhFziNR(4PnY3NvY%xu$rmR*~DI=O_k55x@HWwitiaHne zwVm?u5(EUnyh5Dcvt4}7gqT|7qPEw$B8k$pyNnitxn|L|3;kDk#8aIY;kZ&;95=@U z6mp2rFerQ->;(IPmUlll;F00oRd+5w1*S%5?e14FO|NW@PANHCxg#g;%Z{m|mnqGJ8Bq{O1c zl8nS${og`k0?S^<7!ds!M8E(ekl_Ht#VkM~g&~+hlhJP_LlHfPs%OHh7iS>xurRYCHY1A$@xX8`tk9Zd6^~g@p=W7w>WHa^HWN5 MQtd!S76UN@0EH(Z(*OVf literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/__pycache__/io_tools.cpython-37.pyc b/bmtk-vb/bmtk/simulator/core/__pycache__/io_tools.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0144fff5ed6ddfb0320d22960747056bc18d971b GIT binary patch literal 3851 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/__pycache__/simulator.cpython-37.pyc b/bmtk-vb/bmtk/simulator/core/__pycache__/simulator.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06c3afe152c4fb33ffda48ad21bfb4f5f79f92f8 GIT binary patch literal 705 zcmZ8eJ5Iwu5S_Ih#|cE@cLW+YaScK!fDocd6VXISE62M)fBX}JTGT!kaJ zF+14^{6WTz~+cA$ zsZ51$De5R(BtMI#Azea)J~u-l=v@?447OJcezSiLMaQZSmBRvF@aol}cKca=nM`Au z$TXMYT&Ya?%ux8h+E%mFTU*;%t;I~=cj-Q9m`KixHYbwEAN@_vA7)`(W|WH)REuDP ufd0uE(6bZ~Ce>8yG0*mF^R4e&g`15gGRjT7)@R0;19?Z~;$t%b2fz<+w3P7x literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/__pycache__/simulator_network.cpython-37.pyc b/bmtk-vb/bmtk/simulator/core/__pycache__/simulator_network.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50dbed5beb40deaa99e1fa006c69ae6111241ee1 GIT binary patch literal 6953 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 0 + + @property + def networks(self): + return self['networks'] + + @property + def nodes(self): + return self.networks.get('nodes', []) + + @property + def edges(self): + return self.networks.get('edges', []) + + @property + def reports(self): + return self.get('reports', {}) + + @property + def inputs(self): + return self.get('inputs', {}) + + @property + def node_sets(self): + return self._node_set + + def _load_node_set(self): + if 'node_sets_file' in self.keys(): + node_set_val = self['node_sets_file'] + elif 'node_sets' in self.keys(): + node_set_val = self['node_sets'] + else: + self._node_set = {} + return + + if isinstance(node_set_val, dict): + self._node_set = node_set_val + else: + try: + self._node_set = json.load(open(node_set_val, 'r')) + except Exception as e: + io.log_exception('Unable to load node_sets_file {}'.format(node_set_val)) + + def copy_to_output(self): + copy_config(self) + + def get_modules(self, module_name): + return [report for report in self.reports.values() if report['module'] == module_name] + + def _set_logging(self): + """Check if log-level and/or log-format string is being changed through the config""" + output_sec = self.output + if 'log_format' in output_sec: + self._io.set_log_format(output_sec['log_format']) + + if 'log_level' in output_sec: + self._io.set_log_level(output_sec['log_level']) + + if 'log_to_console' in output_sec: + self._io.log_to_console = output_sec['log_to_console'] + + if 'quiet_simulator' in output_sec and output_sec['quiet_simulator']: + self._io.quiet_simulator() + + def build_env(self): + if self._env_built: + return + + self._set_logging() + self.io.setup_output_dir(self.output_dir, self.log_file, self.overwrite_output) + self.copy_to_output() + self._env_built = True + + @staticmethod + def get_validator(): + raise NotImplementedError + + @classmethod + def from_json(cls, config_file, validate=False): + validator = cls.get_validator() if validate else None + return cls(from_json(config_file, validator)) + + @classmethod + def from_dict(cls, config_dict, validate=False): + validator = cls.get_validator() if validate else None + return cls(from_dict(config_dict, validator)) + + @classmethod + def from_yaml(cls, config_file, validate=False): + raise NotImplementedError + + @classmethod + def load(cls, config_file, validate=False): + # Implement factory method that can resolve the format/type of input configuration. + if isinstance(config_file, dict): + return cls.from_dict(config_file, validate) + elif isinstance(config_file, string_types): + if config_file.endswith('yml') or config_file.endswith('yaml'): + return cls.from_yaml(config_file, validate) + else: + return cls.from_json(config_file, validate) + else: + raise Exception +''' + diff --git a/bmtk-vb/bmtk/simulator/core/edge_population.py b/bmtk-vb/bmtk/simulator/core/edge_population.py new file mode 100644 index 0000000..5dfa06c --- /dev/null +++ b/bmtk-vb/bmtk/simulator/core/edge_population.py @@ -0,0 +1,21 @@ +class SimEdge(object): + @property + def node_id(self): + raise NotImplementedError() + + @property + def gid(self): + raise NotImplementedError() + + +class EdgePopulation(object): + @property + def source_nodes(self): + raise NotImplementedError() + + @property + def target_nodes(self): + raise NotImplementedError() + + def initialize(self, network): + raise NotImplementedError() \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/core/graph.py b/bmtk-vb/bmtk/simulator/core/graph.py new file mode 100644 index 0000000..1e56ef1 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/core/graph.py @@ -0,0 +1,435 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json +import ast +import numpy as np + +from bmtk.simulator.core.config import ConfigDict +#import config as cfg +from bmtk.simulator.utils.property_maps import NodePropertyMap, EdgePropertyMap +from bmtk.utils import sonata +from bmtk.simulator.core.io_tools import io + +from bmtk.simulator.core.node_sets import NodeSet, NodeSetAll + + +"""Creates a graph of nodes and edges from multiple network files for all simulators. + +Consists of edges and nodes. All classes are abstract and should be reimplemented by a specific simulator. Also +contains base factor methods for building a network from a config file (or other). +""" + + +class SimEdge(object): + def __init__(self, original_params, dynamics_params): + self._orig_params = original_params + self._dynamics_params = dynamics_params + self._updated_params = {'dynamics_params': self._dynamics_params} + + @property + def edge_type_id(self): + return self._orig_params['edge_type_id'] + + def __getitem__(self, item): + if item in self._updated_params: + return self._updated_params[item] + else: + return self._orig_params[item] + + +class SimNode(object): + def __init__(self, node_id, graph, network, params): + self._node_id = node_id + self._graph = graph + self._graph_params = params + self._node_type_id = params['node_type_id'] + self._network = network + self._updated_params = {} + + self._model_params = {} + + @property + def node_id(self): + return self._node_id + + @property + def node_type_id(self): + return self._node_type_id + + @property + def network(self): + """Name of network node belongs too.""" + return self._network + + @property + def model_params(self): + """Parameters (json file, nml, dictionary) that describe a specific node""" + return self._model_params + + @model_params.setter + def model_params(self, value): + self._model_params = value + + def __contains__(self, item): + return item in self._updated_params or item in self._graph_params + + def __getitem__(self, item): + if item in self._updated_params: + return self._updated_params[item] + else: + return self._graph_params[item] + + +class SimGraph(object): + model_type_col = 'model_type' + + def __init__(self): + self._components = {} # components table, i.e. paths to model files. + self._io = io + + self._node_property_maps = {} + self._edge_property_maps = {} + + self._node_populations = {} + self._internal_populations_map = {} + self._virtual_populations_map = {} + + self._virtual_cells_nid = {} + + self._recurrent_edges = {} + self._external_edges = {} + + self._node_sets = {} + self._using_gids = False + + @property + def io(self): + return self._io + + ''' + @property + def internal_pop_names(self): + return self + ''' + + @property + def node_populations(self): + return list(self._node_populations.keys()) + + def get_node_set(self, node_set): + if node_set in self._node_sets.keys(): + return self._node_sets[node_set] + + elif isinstance(node_set, (dict, list)): + return NodeSet(node_set, self) + + else: + self.io.log_exception('Unable to load or find node_set "{}"'.format(node_set)) + + def get_node_populations(self): + return self._node_populations.values() + + def get_node_population(self, population_name): + return self._node_populations[population_name] + + def get_component(self, key): + """Get the value of item in the components dictionary. + + :param key: name of component + :return: value assigned to component + """ + return self._components[key] + + def add_component(self, key, value): + """Add a component key-value pair + + :param key: name of component + :param value: value + """ + self._components[key] = value + + ''' + def _from_json(self, file_name): + return cfg.from_json(file_name) + ''' + + def _validate_components(self): + """Make sure various components (i.e. paths) exists before attempting to build the graph.""" + return True + + def _create_nodes_prop_map(self, grp): + return NodePropertyMap() + + def _create_edges_prop_map(self, grp): + return EdgePropertyMap() + + def __avail_model_types(self, population): + model_types = set() + for grp in population.groups: + if self.model_type_col not in grp.all_columns: + self.io.log_exception('model_type is missing from nodes.') + + model_types.update(set(np.unique(grp.get_values(self.model_type_col)))) + return model_types + + def _preprocess_node_types(self, node_population): + # TODO: The following figures out the actually used node-type-ids. For mem and speed may be better to just + # process them all + node_type_ids = node_population.type_ids + # TODO: Verify all the node_type_ids are in the table + node_types_table = node_population.types_table + + # TODO: Convert model_type to a enum + morph_dir = self.get_component('morphologies_dir') + if morph_dir is not None and 'morphology' in node_types_table.columns: + for nt_id in node_type_ids: + node_type = node_types_table[nt_id] + if node_type['morphology'] is None: + continue + # TODO: Check the file exits + # TODO: See if absolute path is stored in csv + node_type['morphology'] = os.path.join(morph_dir, node_type['morphology']) + + if 'dynamics_params' in node_types_table.columns and 'model_type' in node_types_table.columns: + for nt_id in node_type_ids: + node_type = node_types_table[nt_id] + dynamics_params = node_type['dynamics_params'] + if isinstance(dynamics_params, dict): + continue + + model_type = node_type['model_type'] + if model_type == 'biophysical': + params_dir = self.get_component('biophysical_neuron_models_dir') + elif model_type == 'point_process': + params_dir = self.get_component('point_neuron_models_dir') + elif model_type == 'point_soma': + params_dir = self.get_component('point_neuron_models_dir') + else: + # Not sure what to do in this case, throw Exception? + params_dir = self.get_component('custom_neuron_models') + + params_path = os.path.join(params_dir, dynamics_params) + + # see if we can load the dynamics_params as a dictionary. Otherwise just save the file path and let the + # cell_model loader function handle the extension. + try: + params_val = json.load(open(params_path, 'r')) + node_type['dynamics_params'] = params_val + except Exception: + # TODO: Check dynamics_params before + self.io.log_exception('Could not find node dynamics_params file {}.'.format(params_path)) + + def _preprocess_edge_types(self, edge_pop): + edge_types_table = edge_pop.types_table + edge_type_ids = np.unique(edge_pop.type_ids) + + for et_id in edge_type_ids: + edge_type = edge_types_table[et_id] + if 'dynamics_params' in edge_types_table.columns: + dynamics_params = edge_type['dynamics_params'] + params_dir = self.get_component('synaptic_models_dir') + + params_path = os.path.join(params_dir, dynamics_params) + + # see if we can load the dynamics_params as a dictionary. Otherwise just save the file path and let the + # cell_model loader function handle the extension. + try: + params_val = json.load(open(params_path, 'r')) + edge_type['dynamics_params'] = params_val + except Exception: + # TODO: Check dynamics_params before + self.io.log_exception('Could not find edge dynamics_params file {}.'.format(params_path)) + + # Split target_sections + if 'target_sections' in edge_type: + trg_sec = edge_type['target_sections'] + if trg_sec is not None: + try: + edge_type['target_sections'] = ast.literal_eval(trg_sec) + except Exception as exc: + self.io.log_warning('Unable to split target_sections list {}'.format(trg_sec)) + edge_type['target_sections'] = None + + # Split target distances + if 'distance_range' in edge_type: + dist_range = edge_type['distance_range'] + if dist_range is not None: + try: + # TODO: Make the distance range has at most two values + edge_type['distance_range'] = json.loads(dist_range) + except Exception as e: + try: + edge_type['distance_range'] = [0.0, float(dist_range)] + except Exception as e: + self.io.log_warning('Unable to parse distance_range {}'.format(dist_range)) + edge_type['distance_range'] = None + + def external_edge_populations(self, src_pop, trg_pop): + return self._external_edges.get((src_pop, trg_pop), []) + + def add_nodes(self, sonata_file, populations=None): + """Add nodes from a network to the graph. + + :param sonata_file: A NodesFormat type object containing list of nodes. + :param populations: name/identifier of network. If none will attempt to retrieve from nodes object + """ + nodes = sonata_file.nodes + + selected_populations = nodes.population_names if populations is None else populations + for pop_name in selected_populations: + if pop_name not in nodes: + # when user wants to simulation only a few populations in the file + continue + + if pop_name in self.node_populations: + # Make sure their aren't any collisions + self.io.log_exception('There are multiple node populations with name {}.'.format(pop_name)) + + node_pop = nodes[pop_name] + self._preprocess_node_types(node_pop) + self._node_populations[pop_name] = node_pop + + # Segregate into virtual populations and non-virtual populations + model_types = self.__avail_model_types(node_pop) + if 'virtual' in model_types: + self._virtual_populations_map[pop_name] = node_pop + self._virtual_cells_nid[pop_name] = {} + model_types -= set(['virtual']) + if model_types: + # We'll allow a population to have virtual and non-virtual nodes but it is not ideal + self.io.log_warning('Node population {} contains both virtual and non-virtual nodes which can ' + + 'cause memory and build-time inefficency. Consider separating virtual nodes ' + + 'into their own population'.format(pop_name)) + + if model_types: + self._internal_populations_map[pop_name] = node_pop + + self._node_sets[pop_name] = NodeSet({'population': pop_name}, self) + self._node_property_maps[pop_name] = {grp.group_id: self._create_nodes_prop_map(grp) + for grp in node_pop.groups} + + def build_nodes(self): + raise NotImplementedError + + def build_recurrent_edges(self): + raise NotImplementedError + + def add_edges(self, sonata_file, populations=None, source_pop=None, target_pop=None): + """ + + :param sonata_file: + :param populations: + :param source_pop: + :param target_pop: + :return: + """ + edges = sonata_file.edges + selected_populations = edges.population_names if populations is None else populations + + for pop_name in selected_populations: + if pop_name not in edges: + continue + + edge_pop = edges[pop_name] + self._preprocess_edge_types(edge_pop) + + # Check the source nodes exists + src_pop = source_pop if source_pop is not None else edge_pop.source_population + is_internal_src = src_pop in self._internal_populations_map.keys() + is_external_src = src_pop in self._virtual_populations_map.keys() + + trg_pop = target_pop if target_pop is not None else edge_pop.target_population + is_internal_trg = trg_pop in self._internal_populations_map.keys() + + if not is_internal_trg: + self.io.log_exception(('Node population {} does not exists (or consists of only virtual nodes). ' + + '{} edges cannot create connections.').format(trg_pop, pop_name)) + + if not (is_internal_src or is_external_src): + self.io.log_exception('Source node population {} not found. Please update {} edges'.format(src_pop, + pop_name)) + if is_internal_src: + if trg_pop not in self._recurrent_edges: + self._recurrent_edges[trg_pop] = [] + self._recurrent_edges[trg_pop].append(edge_pop) + + if is_external_src: + if trg_pop not in self._external_edges: + self._external_edges[(src_pop, trg_pop)] = [] + self._external_edges[(src_pop, trg_pop)].append(edge_pop) + + self._edge_property_maps[pop_name] = {grp.group_id: self._create_edges_prop_map(grp) + for grp in edge_pop.groups} + + @classmethod + def from_config(cls, conf, **properties): + """Generates a graph structure from a json config file or dictionary. + + :param conf: name of json config file, or a dictionary with config parameters + :param properties: optional properties. + :return: A graph object of type cls + """ + graph = cls(**properties) + + # The simulation run script should create a config-dict since it's likely to vary based on the simulator engine, + # however in the case the user doesn't we will try a generic conversion from dict/json to ConfigDict + if isinstance(conf, ConfigDict): + config = conf + else: + try: + config = ConfigDict.load(conf) + except Exception as e: + graph.io.log_exception('Could not convert {} (type "{}") to json.'.format(conf, type(conf))) + + if not config.with_networks: + graph.io.log_exception('Could not find any network files. Unable to build network.') + + # TODO: These are simulator specific + graph.spike_threshold = config.spike_threshold + graph.dL = config.dL + + # load components + for name, value in config.components.items(): + graph.add_component(name, value) + graph._validate_components() + + # load nodes + gid_map = config.gid_mappings + for node_dict in config.nodes: + nodes_net = sonata.File(data_files=node_dict['nodes_file'], data_type_files=node_dict['node_types_file'], + gid_table=gid_map) + graph.add_nodes(nodes_net) + + # load edges + for edge_dict in config.edges: + target_network = edge_dict['target'] if 'target' in edge_dict else None + source_network = edge_dict['source'] if 'source' in edge_dict else None + edge_net = sonata.File(data_files=edge_dict['edges_file'], data_type_files=edge_dict['edge_types_file']) + graph.add_edges(edge_net, source_pop=target_network, target_pop=source_network) + + graph._node_sets['all'] = NodeSetAll(graph) + for ns_name, ns_filter in conf.node_sets.items(): + graph._node_sets[ns_name] = NodeSet(ns_filter, graph) + + return graph diff --git a/bmtk-vb/bmtk/simulator/core/io_tools.py b/bmtk-vb/bmtk/simulator/core/io_tools.py new file mode 100644 index 0000000..8d80c9f --- /dev/null +++ b/bmtk-vb/bmtk/simulator/core/io_tools.py @@ -0,0 +1,117 @@ +import os +import sys +import shutil +import logging + +from mpi4py import MPI + +comm = MPI.COMM_WORLD +rank = comm.Get_rank() +size = comm.Get_size() + +class IOUtils(object): + """ + For logging/mkdir commands we sometimes need to use different MPI classes depending on the simulator being used + (NEST and NEURON have their own barrier functions that don't work well with mpi). We also need to be able to + adjust the logging levels/format at run-time depending on the simulator/configuration options. + + Thus the bulk of the io and logging functions are put into their own class and can be overwritten by specific + simulator modules + """ + def __init__(self): + self.mpi_rank = rank + self.mpi_size = size + + self._log_format = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") + self._log_level = logging.DEBUG + self._log_to_console = True + self._logger = None + + @property + def log_to_console(self): + return self._log_to_console + + @log_to_console.setter + def log_to_console(self, flag): + assert(isinstance(flag, bool)) + self._log_to_console = flag + + @property + def logger(self): + if self._logger is None: + # Create the logger the first time it is accessed + self._logger = logging.getLogger(self.__class__.__name__) + self._logger.setLevel(self._log_level) + self._set_console_logging() + + return self._logger + + def _set_console_logging(self): + if not self._log_to_console: + return + + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(self._log_format) + self._logger.addHandler(console_handler) + + def set_log_format(self, format_str): + self._log_format = logging.Formatter(format_str) + + def set_log_level(self, loglevel): + if isinstance(loglevel, int): + self._log_level = loglevel + + elif isinstance(loglevel, (str, unicode)): + self._log_level = logging.getLevelName(loglevel) + + else: + raise Exception('Error: cannot set logging levels to {}'.format(loglevel)) + + def barrier(self): + pass + + def quiet_simulator(self): + pass + + def setup_output_dir(self, output_dir, log_file, overwrite=True): + if self.mpi_rank == 0: + print("mpi rank 0") + # Create output directory + if os.path.exists(output_dir): + if overwrite: + shutil.rmtree(output_dir) + else: + self.log_exception('Directory already exists (remove or set to overwrite).') + os.makedirs(output_dir) + + # Create log file + if log_file is not None: + log_path = log_file if os.path.isabs(log_file) else os.path.join(output_dir, log_file) + file_logger = logging.FileHandler(log_path) + file_logger.setFormatter(self._log_format) + self.logger.addHandler(file_logger) + self.log_info('Created log file') + + self.barrier() + + def log_info(self, message, all_ranks=False): + if all_ranks is False and self.mpi_rank != 0: + return + + self.logger.info(message) + + def log_warning(self, message, all_ranks=False): + if all_ranks is False and self.mpi_rank != 0: + return + + self.logger.warning(message) + + def log_exception(self, message): + if self.mpi_rank == 0: + self.logger.error(message) + + self.barrier() + raise Exception(message) + + +io = IOUtils() diff --git a/bmtk-vb/bmtk/simulator/core/io_tools.pyc b/bmtk-vb/bmtk/simulator/core/io_tools.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a353f9eef1646af38c63ded1c45303e205567c0 GIT binary patch literal 4958 zcmcgvU31)489vh5YwuidNxK1RGs8y*=(MDX6FLK1MoxyooubjM&aSoA zN*kTCaSX`~?H$99;1~6dJAMT}0G{VPlGfaC!v@#19cWO`=L4u8@i*;Iw}p7*-}#+=%}xi`bw$apKU9%P3o*m zUZ=w?@Xh{(pOM=E`Pt`BT$bBbeu%wKaoN9NF?da+QBFXX5CRF1L-x=`xX;1b3-miU zu+<#cQR%k4xU15x5IvRd2+>#Rt`Ij=+7n`+%$`d7>TB@1sm#7gZwP%L^g!s>RJtdJ z4^?zJyh&C69w^n<_<2}XIxlClte71yF4C;hlX9_0iqz^$qiwk`E?XF@3u994O1-j1 zr`dFBDpNT9o8Nw>CwXEmPNZgOiWGcwS!j2Tm$StxPaFh{4Y%=XDl&(olhel8hBxtSl^^B~GVh@xIfSWpx47@?2kL?p!aH+1(@k zJEN1_maUd!+>dkcEJa|F&R5onQnjr*H_uFNkEdm|0BihJtKtKi`d{ogo|MHjo2{yZ zTIq5r${qDZ#^du9^)!0C$}e;|6*?=$%tq+iprkT-xpF!y;M29`;!AmDlE4o}m(NUf zS!K={++JzBG?Q$aO+@0_zl$5TOu(a-o_7{vBeH zkG5oITj;I=(76c&Dj@(oRcFE|13#5fgnb{&onc}TriHm{^%r-B;@BeL?U(w_5ZJNF z3^&6YIO}$?AY^fs6c_D{&HiX4vuZ!N9!>$6h>SCpEd^*I9lEwZE)bSH*Dtr74XiR{= z#j&j>$K!>&@Js7`prSIzSsA;s%7;kLv8;=9ZeH88qqRkD+>#*x`OaYM1RIC>l0R3>m(N z@QsMnhz=EO&y4%E08sAYSkgX@Yv3?>>KluLEL0Kk;aglC7IOa}Xp^gT-5WTBJP z-xKeR?}hV++>jIu_JaXR*zL}QO`^FZ1dN3^%C`^Py^7AkI#iEm_dqs)Xf)yf|D!gH z?$cEUoVMNg!8NVu5|#*tT(tTJ2w$cEvJ0T_8+J4lDojEih$#IpI;#4RNM*V5wdpB# zrEp<-Vy!}<=X;ou{PP!*uot~+40{TjI9oj@Q5-gc`h16vLiG|O*9|ovs29FQLL5?J zPek2pYvjI~AMgR?QV9(6GP4CwzfpC=Ih5`r!KcZ7K(jvu`5>#z1QYlb#$y!F^h%rO znRQkVE3?3CsWF^0fsirHq-pLRSq?4tao%C(2fXz(%gyi>HX{ZYyOCG?LWt ze0@beU*Y>m$ZCPQZ5$EJN@Ch6_%A71J`hi=83E?u$J01`Ptkb_`+dQJN$f-rC(h5}PQNXPQY7w;P!a zWgawcwT#zFJpyn09ZCrH(IvXUL3j}Cbq2u)$oB*M-U=tgFt4_6{0KL|5vndHmUjd- zD3!o9@cWLG0>b4Qwtb70I-22SCEvB;_lNj+G7{T}PL;^gmqSYQZe-%uv%I)1$#WUv z?WW3J^b<(<0+(g%RPYu$<}0&0`UoPf8BFUgld9k_)!H0=%tN1G*_4g)DMO&vvJ&vV zZZ2cfT=)5mYBGxQ0mp*%ZXIXDwXQvN4M0tR(-1ko{U2S^M_UFW=C6-2Z;edgOql2n zmUUwj(>G;x^h5BT;j+wnU>tbv2fm)%#)kili~UC|244mvj?;1y#}O-sB<8Xzmzeyn zWS;kB#_;Pu`I&-`nkVdVO4-!Rv$WpgZKL==)rl^wAL)qE5^z$8(fy;Utg9IP)X# T>yL>B87v0)EX8l0zyc8=oJT6NTb=A&hBJqrmgOF zl0|#UB5wRzegM>0Gd*^AL@d|{TV3w%>8bvzx~jhR;FrDeou9w>KBw+8;NQ==xsO;B zM4ISPL`6hD5?v5|cgTY{%nth5n2I4COt<(Ye}%=qDYR2ME41xZ$2@nAE{^n|I117HHW20XKJp zg(o^EN~6$W1Bea4y8S7f;9<*`S>?2Um3!$)=F+n9+BR90HZad$5A#n->la~Kboe1Z zbGmvo%V)Z1)9JMpv^Z$AzZJ}C#4D!~8wfLx zr0+6k%~3pN&B(nntI8krn>=cRqRqYl_#4c>Ec=##JwhX|oQLMQHFIr!sok1k!z-1^ zigy4EC@G%8`CT0AXX!S^LMDiPl$PNXk} z7(C!60mQTmk`f|5kOk6tYyyG6jqO|`dwB$zC2!c z`(J}cN=w1r7<4drThdp6fBYxFFHzXBbQ4z+;dHaGxo-LAlm9PFpV?#WTbOrNV4@hJ z^)ZYRc^=^gWpB}W96FMas3b8pw`IYA?ESESJG@{hrG^*m%5bY2a+wU*0?=$*|8aqq zgl?GtM5x|oAs1&kDO=50? z96mz3MC4()Sffy}*9%>k^D9={71Sw6Q=y0|B$4=R(VRBsX`{s5y_2OBE8O_h8xU_n zfZEl&5H}&-gFt{UL9dU7aZ$qt4v~7rr4R44f^Z)<= literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/node_population.py b/bmtk-vb/bmtk/simulator/core/node_population.py new file mode 100644 index 0000000..353bd7a --- /dev/null +++ b/bmtk-vb/bmtk/simulator/core/node_population.py @@ -0,0 +1,37 @@ +class SimNode(object): + @property + def node_id(self): + raise NotImplementedError() + + @property + def gid(self): + raise NotImplementedError() + + +class NodePopulation(object): + def __init__(self): + self._has_internal_nodes = False + self._has_virtual_nodes = False + + @property + def name(self): + raise NotImplementedError() + + @property + def internal_nodes_only(self): + return self._has_internal_nodes and not self._has_virtual_nodes + + @property + def virtual_nodes_only(self): + return self._has_virtual_nodes and not self._has_internal_nodes + + @property + def mixed_nodes(self): + return self._has_internal_nodes and self._has_virtual_nodes + + def initialize(self, network): + raise NotImplementedError() + + @classmethod + def load(cls, **properties): + raise NotImplementedError() diff --git a/bmtk-vb/bmtk/simulator/core/node_sets.py b/bmtk-vb/bmtk/simulator/core/node_sets.py new file mode 100644 index 0000000..5a67f95 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/core/node_sets.py @@ -0,0 +1,57 @@ +from .io_tools import io + + +class NodeSet(object): + def __init__(self, filter_params, network): + self._network = network + self._populations = [] + self._preselected_gids = None + + if isinstance(filter_params, list): + self._preselected_gids = filter_params + elif isinstance(filter_params, dict): + self._filter = filter_params.copy() + self._populations = self._find_populations() + else: + io.log_exception('Unknown node set params type {}'.format(type(filter_params))) + + def _find_populations(self): + for k in ['population', 'populations']: + if k in self._filter: + node_pops = [] + for pop_name in to_list(self._filter[k]): + node_pops.append(self._network.get_node_population(pop_name)) + del self._filter[k] + return node_pops + + return self._network.get_node_populations() + + def populations(self): + return self._populations + + def population_names(self): + return [p.name for p in self._populations] + + def gids(self): + if self._preselected_gids is not None: + for gid in self._preselected_gids: + yield gid + else: + for pop in self._populations: + for node in pop.filter(self._filter): + yield node.node_id + + def nodes(self): + return None + + +class NodeSetAll(NodeSet): + def __init__(self, network): + super(NodeSetAll, self).__init__({}, network) + + +def to_list(val): + if isinstance(val, list): + return val + else: + return [val] diff --git a/bmtk-vb/bmtk/simulator/core/node_sets.pyc b/bmtk-vb/bmtk/simulator/core/node_sets.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5618942d8c1f35ab718e1b63011b6d51577e205e GIT binary patch literal 2850 zcmcIm-)|d55S~3dPMpL|XrNLMr4~OewZb4#q@q&MHZMrX4`-FA5#ey|*6um>opZZs zQ<3lz#2bGt{{Y`NXZKRUTa4|U+uI+rGv9nO>)PL~^#^}F{yCQUr-tYEXzmAy1V1Aa zk*SdbBPI#N1~N2~)Z`D5OOfXXHA(7XYdX5}X0$FzgD(dwlB`OvC?yugGN}#W(Di^RF`W$g@bw4L?s*`OFqUE9B&+4gT2 zof}x=iDHyl|0*wz{U%>#`RsHu@M)eo*iAGs{f^EQ*4c@Ty-lJ~nmE`3%w39wgDkeX zdXhS?og|IRPMptPYgnRTI`OtBbq&)jxq4IxRdh&Xl8+*L7TXz3@WgwV7t=whjJ9_O zy`BMUhPtQJ9F-30VRM%{SSl*qx9D}BIa|2y%OfD+Uks*QSH#_;sXs1-OQ&=_FS>DF z*e-#K0G#^*9w<;znx#I9R-nq=q-xXTH`p?pW((h&K}@m{Hm*@_{Ep69#*l$xqd@i! zf0YX`KuItbIc`YNB55uHIs01tVjK&05!=1PJ91GYyT(eeaZSdcQ*9o1bMjF3NfS`U z$ah%QKWUXCSO~_E69lZM(W-*$H!xVeI%Rgqh$?~8ppb%0|h4yS|MtdSzg>X~* zefr6q?8<)jK+aGW!D6PzUCo4#l=2`xM(ZT|-r)^R1f@0h^<@9#zU)K$0uOREWZs2J zI+{|}53M^Dx0QZra)p{)ii+m>gqJ0r$o}QwM~hf9KgQFs&I(AmZ8n2#vs+T&ElP6v z70)=QPW=yj#I;mrq`QaCJf3>H&TAft6-8P;qG*~YrxUz}%r8x}kbDk5r1*s5Q-}&T z?On6>hIF5ZC~ytaY&AEUHGG?DRIj}5A5SJR*64>Do}fA2TlVc6AD1|%W2VZ#(#~t0 z0_Sj3gN&(GRrJfdmHT?-^+V9y>C6@(JybNxd}OT%d2EHjb=@b_U!g5xLzaf0QG8Cp z>FThh08S-eqUni^il03BDaRRZUhyhy6a<$VmSRbG0C8VzSCA@|tBY2b0R? z>jLy1RNtprJcx-^4|;l^DTC8I@_9aSAt$vU(K Mwr$sL-Mh8&4{zK{ng9R* literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/simulator.py b/bmtk-vb/bmtk/simulator/core/simulator.py new file mode 100644 index 0000000..4a84174 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/core/simulator.py @@ -0,0 +1,9 @@ +class Simulator(object): + def __init__(self): + self._sim_mods = [] + + def add_mod(self, module): + self._sim_mods.append(module) + + def run(self): + raise NotImplementedError() \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/core/simulator.pyc b/bmtk-vb/bmtk/simulator/core/simulator.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8844715401804768966ce392a3d3ba16ccfae4bd GIT binary patch literal 934 zcmb`Fy-ve05Xa9=DYQ~2m{?gdkr{*#A7WtX;EI6?$x34vQsN}CT_mJVl$EFL3vhQy z$^+0S_qTKPoqu=NXgeNV{M^1|&^{5aH)#G8MFSXsg#az!8(;(QJj0J3Ok+A1hA7kA zu8K5NZkIx!_YTdUpw zIKptH@-h!f5tb(bkf4o`(xqVKW8s7BmW&eT`&;CXc$d+W<0KH WzBF0jL;Jm#3%+Vfyrv;|0{I27iN=Zm literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/simulator_network.py b/bmtk-vb/bmtk/simulator/core/simulator_network.py new file mode 100644 index 0000000..e1da1b3 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/core/simulator_network.py @@ -0,0 +1,200 @@ +from six import string_types + +from bmtk.simulator.core.io_tools import io +#from bmtk.simulator.core.config import ConfigDict +from bmtk.simulator.utils.config import ConfigDict +from bmtk.simulator.core.node_sets import NodeSet, NodeSetAll +from bmtk.simulator.core import sonata_reader + + +class SimNetwork(object): + def __init__(self): + self._components = {} + self._io = io + + self._node_adaptors = {} + self._edge_adaptors = {} + self._register_adaptors() + + self._node_populations = {} + self._node_sets = {} + + self._edge_populations = [] + + @property + def io(self): + return self._io + + @property + def node_populations(self): + return self._node_populations.values() + + @property + def recurrent_edges(self): + return [ep for ep in self._edge_populations if ep.recurrent_connections] + + @property + def py_function_caches(self): + return None + + def _register_adaptors(self): + self._node_adaptors['sonata'] = sonata_reader.NodeAdaptor + self._edge_adaptors['sonata'] = sonata_reader.EdgeAdaptor + + def get_node_adaptor(self, name): + return self._node_adaptors[name] + + def get_edge_adaptor(self, name): + return self._edge_adaptors[name] + + def add_component(self, name, path): + self._components[name] = path + + def get_component(self, name): + if name not in self._components: + self.io.log_exception('No network component set with name {}'.format(name)) + else: + return self._components[name] + + def has_component(self, name): + return name in self._components + + def get_node_population(self, name): + return self._node_populations[name] + + def get_node_populations(self): + return self._node_populations.values() + + def add_node_set(self, name, node_set): + self._node_sets[name] = node_set + + def get_node_set(self, node_set): + if isinstance(node_set, string_types) and node_set in self._node_sets: + return self._node_sets[node_set] + + elif isinstance(node_set, (dict, list)): + return NodeSet(node_set, self) + + else: + self.io.log_exception('Unable to load or find node_set "{}"'.format(node_set)) + + def add_nodes(self, node_population): + pop_name = node_population.name + if pop_name in self._node_populations: + # Make sure their aren't any collisions + self.io.log_exception('There are multiple node populations with name {}.'.format(pop_name)) + + node_population.initialize(self) + self._node_populations[pop_name] = node_population + if node_population.mixed_nodes: + # We'll allow a population to have virtual and non-virtual nodes but it is not ideal + self.io.log_warning(('Node population {} contains both virtual and non-virtual nodes which can cause ' + + 'memory and build-time inefficency. Consider separating virtual nodes into their ' + + 'own population').format(pop_name)) + + # Used in inputs/reports when needed to get all gids belonging to a node population + self._node_sets[pop_name] = NodeSet({'population': pop_name}, self) + + def add_edges(self, edge_population): + edge_population.initialize(self) + pop_name = edge_population.name + + # Check that source_population exists + src_pop_name = edge_population.source_nodes + if src_pop_name not in self._node_populations: + self.io.log_exception('Source node population {} not found. Please update {} edges'.format(src_pop_name, + pop_name)) + + # Check that the target population exists and contains non-virtual nodes (we cannot synapse onto virt nodes) + trg_pop_name = edge_population.target_nodes + if trg_pop_name not in self._node_populations or self._node_populations[trg_pop_name].virtual_nodes_only: + self.io.log_exception(('Node population {} does not exists (or consists of only virtual nodes). ' + + '{} edges cannot create connections.').format(trg_pop_name, pop_name)) + + edge_population.set_connection_type(src_pop=self._node_populations[src_pop_name], + trg_pop = self._node_populations[trg_pop_name]) + self._edge_populations.append(edge_population) + + def build(self): + self.build_nodes() + self.build_recurrent_edges() + + def build_nodes(self): + raise NotImplementedError() + + def build_recurrent_edges(self): + raise NotImplementedError() + + def build_virtual_connections(self): + raise NotImplementedError() + + @classmethod + def from_config(cls, conf, **properties): + """Generates a graph structure from a json config file or dictionary. + + :param conf: name of json config file, or a dictionary with config parameters + :param properties: optional properties. + :return: A graph object of type cls + """ + network = cls(**properties) + + # The simulation run script should create a config-dict since it's likely to vary based on the simulator engine, + # however in the case the user doesn't we will try a generic conversion from dict/json to ConfigDict + if isinstance(conf, ConfigDict): + config = conf + else: + try: + config = ConfigDict.load(conf) + except Exception as e: + network.io.log_exception('Could not convert {} (type "{}") to json.'.format(conf, type(conf))) + + if not config.with_networks: + network.io.log_exception('Could not find any network files. Unable to build network.') + + # TODO: These are simulator specific + network.spike_threshold = config.spike_threshold + network.dL = config.dL + + # load components + for name, value in config.components.items(): + network.add_component(name, value) + + # load nodes + gid_map = config.gid_mappings + node_adaptor = network.get_node_adaptor('sonata') + for node_dict in config.nodes: + nodes = sonata_reader.load_nodes(node_dict['nodes_file'], node_dict['node_types_file'], gid_map, + adaptor=node_adaptor) + for node_pop in nodes: + network.add_nodes(node_pop) + + # TODO: Raise a warning if more than one internal population and no gids (node_id collision) + + # load edges + edge_adaptor = network.get_edge_adaptor('sonata') + for edge_dict in config.edges: + if not edge_dict.get('enabled', True): + continue + + edges = sonata_reader.load_edges(edge_dict['edges_file'], edge_dict['edge_types_file'], + adaptor=edge_adaptor) + for edge_pop in edges: + network.add_edges(edge_pop) + + # Add nodeset section + network.add_node_set('all', NodeSetAll(network)) + for ns_name, ns_filter in config.node_sets.items(): + network.add_node_set(ns_name, NodeSet(ns_filter, network)) + + return network + + @classmethod + def from_manifest(cls, manifest_json): + # TODO: Add adaptors to build a simulation network from model files downloaded celltypes.brain-map.org + raise NotImplementedError() + + @classmethod + def from_builder(cls, network): + # TODO: Add adaptors to build a simulation network from a bmtk.builder Network object + raise NotImplementedError() + diff --git a/bmtk-vb/bmtk/simulator/core/simulator_network.pyc b/bmtk-vb/bmtk/simulator/core/simulator_network.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3aa722570856f9cc7737dd0610e20693ce1005e GIT binary patch literal 8766 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c4c1f96ab2df4700a955e0b3bfba94bb781964d GIT binary patch literal 374 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/network_reader.cpython-37.pyc b/bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/network_reader.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..673b99b617719f5917ee579cf78f3d31c371716c GIT binary patch literal 5765 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 RhnYJ4otE2lTfesK{{fQDCK~_% literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/node_adaptor.cpython-37.pyc b/bmtk-vb/bmtk/simulator/core/sonata_reader/__pycache__/node_adaptor.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fabdabbd2f10dc51182da09524c591acb5a92a6d GIT binary patch literal 5918 zcmbVQ%WoV>8SmH3^z=MD89x$-gf(a-c2Ka<9$0~*C{FARVdF`(4%$r%)U>5<-*?foD;vVy5||ki1etczj}0i??=tO zTFp}M{P=h4zxeJ=Mfo=khK~W@E?UgcF@>pJ#m?TUt;(@xYx1qz#sh_EOn;>?-7lQ# zb`fKP6)-NyafuaK>6K!e%w%P}%gkaGye(E`HM}cK-Bw!luYpNvd(;mf9l%|*I7R0x zgc(>>rr|5yH<-?hSIVhs7XTMnQNl&QC1y&v1Q^&PYyz&Zs)WmcYpgC|3-BCkNVo#{ z3TsNZ%I4Vu@YL8Ms9O^C?I?5;_nsU3?TGmv(MAuwHMIL^aRXhdri!Da8hX%+Z`F27 z69p&4SEAV5>9*l$=c6jpT}#@cUs9Xmv}+CLJe;{;)lIF3owl9Cm*s2*RP z=52Mj%Ts9w9(og_lF2%NROu*ZV6>y1lk&i#&gnQ&k|+blX?siM1-v99Sx0y|nOzkv zCb=ov@zM?%COwe)jVmk7Z z-hq}zMhkq)^W!)O_b!vl94J|tpys^`sTrR&MGtw2^boRg@{msM8`{t}{NzQ%58mPy zWSH+y;C{m`vakOil$_4I7@e^CQJ*455QQI1V4byt@h(rgE4I>m6!OTKiPuzJ&V!wr z30RZ9Z<{ix9r#BxQ9x82XU|W9#P2!Itq}q0@%0&zPi$+q>J-A@Y`)_N%WQH%?u)&NLf|%Pug(F0z6>N{)UxOOHrrmTz?`NRmjFG|JMJ zjbuqF3kgZ4E4!YhDtpdvWT^^!ulP;2$d=@H^MJ3iYZ6{y%j`P7U1Z-vw7wxKZN!N6 zED2@u7>j2lnTz$k1b(s<7eMchDtnXyc@}_v!pu&^BB_TWQSQ5r=X(3TeKnsRJ5jdK z$_ysc$5;ze0i@86NSZ}-l(LYt7AAPOIAe|ckusAkGPL%`Qnzn$iQ0~vc>9#1DOB7h zgrr!csO3pyQ@f_5G>tKnNro_EP8hpgcW>6bt^>tgv>e&RClcqW+8Oy6`Q1=tv^IMv zt#6=ZhpNZ(lN^Lp7a-S3?y!m8)MWIM9!afUJvfpJ*xU>cHenWL;EHb<~3*|31A_jc7}?Gn9kp z>WQAhua*_``Z5?fF-{7pkrsC4H>q}8$wu2KVPU66`?dNLPP+Pu&jc5}w6%=ME`AK%~H z-hSAAz*le#sySJ>Nro3Ph9~?LEYEU7q4YDfnEX`%AK*qmzVRjx<6}k^Ko&zDE-m2i zP)S0yrAoCWY(VYk(64f45C{AVKn1CDBQ;%F>^{mE>vpVfQ zS5+k~(K)x3!#khhs*}<12bKSo702+)ZzM(knWBg&Gn2X0Od6*e|K{+k)RYvy22^B4 ztd`R-TK zAPMrYD{X*e)C`|L2qhn(=e&HQ|?|wAsvXwAOR(6As0OqfZcVV*Y zul)LCb)2_FWhaRG`$uu$xm|H{Vt}+W;8B<*mkeR89|d7D4Cmr%Hhgm*Dk0hzM?F^- z0`}s#=$SarUhoES67{Ar36&q;x`>aNN`AylYf)%nUla#na5(U7lL~bwV6j~xD0UKe zr;Cf?z8gDU)ayr~A10!PQfXpX@}lmb7sf)1Vqx@gFBC=xOo*cYGKdj?Oh07tivaUl z5Q!SJ;P@{+zfaYkDDFnQ=O)4mP!7k58+yJlSl}f%B*wbYja(*-sPBiOygB@=Rg;h7j@sYV|_Qqdy3!_@H1aqnoUrkhe}LXDMgMScur#xt>)| zs;GfYR3O9u2MnQm-B?&sTzfnk^ra5+qPpkyXQfd0aPv<_cm6Zr&93=iYC5A#Z}f;t zdgx6}QcJZfQAmGhbkz7pqMhl9(J7?rIpVTY#B7OwC}+)lt{i^Az){CSJbolsDx;OU z99M^9#BJJF;(Thp#`Jvtw{pIo@1INQHf6LkI~i4}twt^qav0p-V^NTPB&;nz*^k&$ zx(8ncBjam;Jx_aQ{8KDS`OZ#DY<9)%cHw@R&n;$tI{Q7`AoQKxL5RE`h4O+SEF|pY z$mzl-OJtW)#WqVeXG;F%^djOt+{usKF!~S{*e))}{;7DdQQX zS1J~CG^tW5?h;uhoNBqz$;(S#(b*LQ0Q}7*U1)|TVNwtxQ~Vna8J2fbW%zJIyS2pU zv5C`#i7!$|cYM1<6~}{z8@4RCHlDTDw;tZ#cAl(%y1vEVqiyd~N5udqSK-9HD8_z* zKM^toPT$@{74a(xyq?rGU#IOAuj|HfKC?-ukTCfcb>n-5gyiLdjN^xB@d`RB!t$SH z7OFxx5r%okPkhBuxDQZGl;cK~1kw`8h`XSc;i8dsf##6_SL_u}q_0BZFn+`TUGucUYxFB{9|;Y5Dt3%BmM(|i_uO86}Gh9%)Lp3oH*u07pEgGQnxT~ bqn^dT26u?s;f)@7z|;hxZ?teKfwh^NbmztRdLY^#4Q(8s?-}KuJ{4)JnxyAU2mPFMc%ddoIT&~ z$9d2DW!L|+vh zg*6q;t352Pt6wVhM5%Xo7gX3NN9U^1MHSAov)feRf;5&?xG0Tf6*i^OQsI&`R#dnw zja3!4q_L*L6=|F!vOAqsARhl0Kih`QohLIO=&WJt4vPLestCY^Dq;b= zL5ag7pMY8*jBc&$&PmsjgxPuNHpN+Rzi+a-~p* z?wbR@x25x7>tW9v+Dga0LE;0-R*>bJTO#NOQXs-{6=xMo+nJ~Wz;Cu{| z%&(}RWLqkr39jzQ!`POW5k`qWv`A8XW`Zsy_}a73{VGg<9L4(w#@idDfr+zpc1$Nv zUpj>fvhg_&RV0Q)_VWmuCkjjydRF@NOm2XhZQ~T&pCd37^5T(dAPsgG=*~=ZsKT#* zb;PNe!dM$Wg<8!0bU&J{$eS_Fm3X{*3K?HQcDTe;mdOnJGgI;B0D1Eiq${EMvd@+l zIn~2-h8prbK)&`Y+)vk#l$#!`XyTcu*M~KqvD~<{O|OsPO@qTSk0MIuMKs3k6ZAV= zA~<;Phn_gK9dU8C#lL`iBDPgJCf}Eim;!6UsFranp4F(bQM?A1_v&6mHAVkn9mn4w z0+c4@1pF$C5IVXb&<7L7cd$M7in&D&l-LqZ@L@ z-3P(BdeEL1WU2AvRC}Ji7T&bnmN>14(QtYsxmN(|HcCN9hmu#grSNJZ&oh({&%<*V zB&iZyVQd&IpXp~ig>%cdiOFb;xHmnT4(KP zy8xkxo~G@dcbbj0Q-7QFW?inCVb=F;xDaqJe|T?qQ1k(+=R{i#)s^V6ioa|C$xut; z$6_3@72?Gz)IuC7*Pa`gtvGeRH+M+UL*m=X|Y_6L5V3k^+rr-!73 zk`H|o959{?BCX#f#*_=hKKx>-tU2)wNBE+r#@Nc)y~N+29=h%;0P#bVNes!QyBz1^ z1V?Q&{iJq+wen=f@a2;(qFF|m(?eUd3nM~C%ReSmVX!q0G|iP2{5Mz)+`CF0*OWm6 z_78QP%J#UfKKR5rs;f8N`^Y(NNTQ&jt{&r6;@Q+xx1kQ_RsOL$l98*&^P;Zh(Q^BO zGV`jtpt_6dXh9v#E3oHyQ4Oyv(^TCh1ub!eoDX10Xn5RINBGEDH{W^mv1;H+9WJT- zFUoNo8iEzoMZN=iVT~IB0v`tpAEc0yIc_CBw;Y}^2Sk_D5i&?h9X2^SysXT5)xE$$ zY_y92=!R1Qdfjo3msJVs1*#E(lH~-2ql#VF^x)Aghj2leg}Wh>3xL~FN61rIxStDL z(0h@C1>6M%xSv&U{|UG&${Ai`y34E=3-@sqAVk)FTCM$uU0bjNIO1sFC`XUsh+U69 z1&*JgeC|BjQ24w9ly*8aF56;f4BZp>Ey(D^0nJmCXfKR&h=EeM{wC_X*&qqqX=d7c zaT>NMPVI?c9WzPP{_uEH?3DfncHK|?hZr+iTlbULv?n5;c7lKcJ6uZQK8pP4m+>{) ziA;b7tP`OJ_H-Dni+P~~uMztJlA&~`eMp;9gEW3Lh~xnjF1|LR|FdM!OSR}9zKkI2 z^|Lfejcm@eI6r-)qomQz;?y?qUZs&_ekhHsk4?0L_dW^- zvb^UT*}4sYhML3IM$8466Xky5MeqBG5XJ3}{XC7+ebLK=s-?AurjeYcOGZgtB)PDD z)@Qbic+fqNv2or|K~;lDYT4t!fjAU0syQnY6c3w)5mBJZD!Rg)v1#H=&e9=fP)ou< zx_CRhHybXBFilg{(FsB}ps*&Q@jRh_2NU`>DphMb7iufclC#EATW{2zE6%F3Qft>+ z7->0m=Q8H5Iv2}+js3cFO-3bu+Q2!L(ihhO%E0Qadf}x)Q$lx6*Z`3Vunq>V%$@Qm z;Z~4n;g(#%ex42b_HGxkKpfYe-=Ct9;PVTRRy9?g_U9O}-b1Z5iXduRT&dW7Woz)r`3$Z?Uxue0RO5c=_3{!URAIfFW z?O)sr^HB|AZ;P6O_Qg@ZKxhXGR-N-NrUJ0oTPY7*9oxI6(rvLQ)%t4=Ue}5BP`VW? ztbge_2V(I~hx%aKdiE4sO%a_?v7wbz>XtK zVJ0eq@NdzuW`aROLp40ZRE!Fjm9~VJ>gi^{X(PKHOs`zI~j&8>s zpD~I*1<1dmOrrQKPBQ$MKvV4Gbh8TXPXP9lQ&6qA$#B5F{P7HvxCkbOB|nc(CZJ~SJ|Zw%N};&L`mMprO9(?L`Dvq y%ST`nu)H>5h`<}YJ87M@EfcrrrqwUsBeyjjhZde7e&K6ex^Zc**^u8=`~5#PL+ebL%br zdv@ya^$Qwd9blJ!zCF3WmZ$G$Wx(yGdORaz6W zJ|P~zQj+GRv}&?mbMvw^r=&G4>(g#tk>-rFW@UXAbC0LSHEC4kA9jN^!G}vg^sd$Ub^s_Dix~ zmct4N<3U3-t?5UUvW$vocEU!TpUWoEwMlWX^LBP3p9I;1E>+# zgN>VUl*NreU&|)2AiTHXy!3^S~Q2{&y$PO z5G7mZcT^@ZwN}W4lOdU$#ik=EbUR(Lk#ySDQ2omP*oC0vog^Jj2!_lb*%~KT`0)4W zVptgvEyONNEQV!yiep$@S*Lz6t5&+nMdP#Q6 zI;}_o%wxT(u*U#&%8`1B>5MY4y>6VUXjG>`khGI52o%(cqX61dgJ_4X*ezov&>4*( z=%$$F@V4$F1=vZxz6&U9WyD+#VcG>hoWmj`R)@bcPK1~ zb7yYoD{^l_(lvPmE~~N&Tf^4L!5;99e_k7MfZ^8Ej;CaIngMBd=E;5e?!9@r-9XIp zu28ufo*(qZtyt$XR-UF;84LIo@^nf zD9I9E&rKt{T~-E}MNWUm)%X0RP+!Tqq=i>^$7=VakYh9qwjL5u%Ckh!AxOoFp<);Og-*g#*A(jUE z)jhiV`D;cwXGlxJX7bDUt2o%cf=)^`IR10q1%KI}f%BjDYkt+6@#p=g<1S_3!T>W# zO(Ox;VK+8V1U zz>9M)uz&cWLn$3pWgGr(Li%5mefTl>suKL&c{!Mr{fRuc*vGg+d!8LL&u_6d>@-N8*%wEL&_jIunOd`+l||eI$u_EikjAt zD>KQC%ZA}x^Gn5Bo@tPdj9K6lDTm8cip8-ig-Yn5xqQQa)vE$}ulNylR5Vo%$s7h6 zRHSprN8}fVj@gVpF z<6*r~XVjnS?JF4cUmJnq?*wjOT9xNhM`uQK#5V`Q4JL;(2EI?t&n+UNwHrx>LQs`9 zZsnLCgRtMCSxQ|BeK5RnL7Amx_S*wyS2pf4H6cb_MpBXmVissEOS&rkOFFL5djhp8 zl>)gJ<@VmJ^w0Cz%EnnaKrM#%P=evk!K2)6>5gH1s}rMTn=i9t_*9Tnbix)C|asvw!+O^$MC@XicTlyc$z`&{xe*uyd?XqdzWOgavlQvQ%WPb&|mr zfp*qgtuy<`t4@}as5;3)5Fl0PH8IsnO*a&Yr(xcqKHhwZqqo^H=&5X?W@s&n)SXU4 z;kDXn$40S9gH&;uA9C~~cEpDH8M=WaboBg_5G}8%Vog1&UTtEoJXfxjYA@B^DS786 zF{+@I4HGJ$FN)UKp1YxPYMk zq7phbN{~>q6f}?x(+Dt7KOT6+J%ip%7G907xw`_r7vdEHOZ*~x4-maI?+x^Ead|N0 zGMG3NujZEoW4}65xcw0a=l;8P?4^7b#KwpeM*Q5SKa(TKOb2;#A zQnOQhx6@0b;j5SzL%X6OKbtga5_hRLEEsPDdcD7}F z8l6VYJJ{nmAhkoa7IiSr8dIatUvWt8H|G60Pm}vX@e!ZBhpzvvf$M-upxhN+gs3NF zwxK#-;0><&;-~T~jrHXCsGxV)GvwW64U-tr5-6g9KQ&kQ5+jPPpj<4TJUK%2SF zE<`s}WYC!^?grVFPV?YpHn|;bT!PhbT4>h(9S1#?Q=RnmH!wngNV&>AH_&D3aE1&1 zL-7wPTIyG|)UOPe`V(6E>(2VRw_ArlLRR=wo)8ir(cYW_OF-hwLaV{rrG_9X)?ns`=v|aGYgkjXbl$JQJB_ zwN|dZSMuCtB;8@hy6cu+tNXxw9SiCkc5!E-mn)&ovI|({FOf?}KSD0?N64kopB*U} zF^ovr(&xM|@~h?2a7CiI`4@a|#MUTMb%9D?FH3M4MDOAR&P>xVxzW5^g)(;0Yn5`* c6)u-34$EJOW<34t&!g-vI-hmX!0;om`Nv;GYT<;qFhzBtKD7I zse3z#g}p$-4zYkGD>ev;9jw@}V8Ma~Tf~BY0gKte_nq5S)$U{@1h!+}@_pZP&OP@# zkJSFT(E91-&z^;9{I7<;AE4-;;1l83QGKPh%etesouc1W+ip>>sqMOoTouo#T}-Gc ztp7x*r`rt`)r;PlvUgTR4K}vtR5UAH+y?PBOQiv9&YF*X(|c>-vvaVHW4>Ol;eZqb~PrX?4n4QbYk(OGF)vN1X* zO(C3HHKi%!v)PiSFu^9sw|umfXSoO|Tow|02Ss1S=g3h4ao~rDXRK8sN2uUtzlE@=6L`$%-co&!Q&BB$3ArL>L~&g@rTW!{eb8nfoMzy{Ie4*PjoB@`71#FsyT zK(Tq5NG9b)5fM|BtcYRaRtg!D zBdZ4@ZGxqk5V2MV@!{DaydgopzZaXt!~@@d7fnmNvqt%iEg9f1_I+X0_o2a~KAK*m zm_Rz?+#HI%@Cdygk;|iCJf7fD_vAu6K4gy%p<+sEr)`5h9HD5xGy$~XH0zzIAK|?V zP`qXRhL9`xkC$*8MgJC`(r=<)2Ny;hCHi&n?q=x6(VG)*Xd9jpT(?PAsHu8z-AZq$ z^yeo0pr}R1YI{LN^HNz6En}o{}Ro-iQP3-lz!KQ=T*W7eKG^*dF)oQU);)X1$`Z#3vlp-?F1&=r~gWO zuz`+g>^MPhRMCs6s*Y+g2C3;Rwpr7f~h+XKk^?uyu104|M=;n&GVz1V0mwfZmKUIpS)Mtjq!Vh zfLD$Y8Ob9(m{a*%3^})Ku!I2OprP`cVrOL13P*Yp@FBD~uBjsdu@(NQsTKFQu8wEa zQC%G&P?dlSdtk{%*|jBl$BXM4Ug`Tl2}Gc*uCdoF;WHt)%?FP@y7hQdl(L(pvB#iJ z9+w{W6SGFXED)5dr@%y;TyCR87r5R=8=93U$>Y!@FJduH#e_4|C@RdiQ8c+!?t;^H z>Q2X*cRS7%=aM5D##<_p^*S0BS>JF|d-kRvVwe+#dLmn{wX%KJ#;>$ml|&zT5ASSl zZQbwQ^WMgQN9>k)6H?@gY46g99M4cPH0w`L^c+6Q5y{j<9)vdDvS{uLa0jGifD3ex z0GH~EdQH?3Lolc!jlAv9KZ0eICR7tj2niiD6^4+bfYSD6MZ??9tKU;i?>_sD=r8no z2JfPwj$n~pwZc_+8$VXhzo$|VFsFKAkEzvm#wV^io-GFE)zK{Pqn&@sTCdH=QMJ*y z`TAsyxgd8z^%m(1^g82Bu+Dk@DUZcg_OuO<@#J~iK_C2uijCZb%P`q|wdn{~?>l^z zC%7VibIM#$NBCY;pg;ei^ZW+)M9-MvyyyXw4J>G?BRC$A8D{^oknS8=J8r2okn?X}VOWV9`#|0+foinWl1y~)LKPjv$=l1u5&tHZAlJ$jdvxvY-h z7j@WA+S=*}zv5hNJOPh&I2{KqmG@l7k+ZDOFNo0RxkQA1!}%^IKcSRqhAu(}yAT+k zMn2rn`q^F*YabXQ-fY=U$5L; zaVB??Y`A}@lQ8I;x3J4Z8=liB&r<6%+4K2ChFOxD!upHvWP4tpP4i&KbT$aYjTSGg z3OpwFCKiMvZL-11g9W{FO*j8dEKVglN#@JA#6ZuBvpq{qv{bbGql)Y)*lU0ZFoke#c&LfhLcKZk7)MBR8EO*w!XU!&vr=lF-=Ak;#)vKMNwM zAeKqR&}T?Hm6Vc3vR3IXK5AykZPq$4 zkk?=kTFwQhQCq2f5w#`f9rv>Pw)D0z-f-I}Etu1#$x`dMSL!uq*;#ZJT*(jDu`7!h z6A4>@cNQf8j{q4oQGt0uXYHg%FRx5|>>@qPoeb@D*w-RB5&mADjfVD)sb36f{XsB1 zLs2f01SF76nL7RzdTgo~k_ij0Nvd%E4^%9l5H?taJ=O0iya1R{XfBK3kKtUo|i!&)`BUkt!Hv3uhn7K_tUzLb3%&&9N z6tsyV%bib;$oT?hLB9{Evbe;6*Mi&hMkFl-zWKYO6mDsjO2$K|LS$?X{XRfT5+K5# z$Has{-dG>KEp(i9K>Qt?@oN|TK*yDTLnnzEESCxVBzNxTHz znv9ku#Zm=GBD3-Ti||RE;nU#@r6J)le?+}VS@D)UntYG0x_1p95j7p@d-v~_Vana7 z-CGat-`VmX-STcd@Mx>OFY_aT301~puq!r1$Hv6D)hMEHmC!PDe_>Nh5)MTFyhmI_ z)eZZB*5l5Pxri6w1^84_k=m&T=~Vx(qv%z98czA+HfK8Zr8;iD)?8@Lb#8TLI?LsK zGm;aG;S-Fe;&(xbuIWl4*CnqX9|%%7s!O=EaXV}3ih4fRpY 0: + raise Exception('Cannot use more than one model_processing method per cell. Exiting.') + elif len(cell_loaders) == 1: + model_processing_fnc = py_modules.cell_processor(cell_loaders[0]) + else: + model_processing_fnc = py_modules.cell_processor('default') + + self._lgn_cell_obj = model_processing_fnc(self._node) diff --git a/bmtk-vb/bmtk/simulator/filternet/cell_models.py b/bmtk-vb/bmtk/simulator/filternet/cell_models.py new file mode 100644 index 0000000..a415e9f --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/cell_models.py @@ -0,0 +1 @@ +from bmtk.simulator.filternet.lgnmodel.cellmodel import * \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/config.py b/bmtk-vb/bmtk/simulator/filternet/config.py new file mode 100644 index 0000000..b10ee10 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/config.py @@ -0,0 +1,8 @@ +import os +import json + +from bmtk.simulator.core.config import ConfigDict + + +class Config(ConfigDict): + pass \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/default_setters/__init__.py b/bmtk-vb/bmtk/simulator/filternet/default_setters/__init__.py new file mode 100644 index 0000000..6ec46cc --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/default_setters/__init__.py @@ -0,0 +1 @@ +from cell_loaders import * diff --git a/bmtk-vb/bmtk/simulator/filternet/default_setters/cell_loaders.py b/bmtk-vb/bmtk/simulator/filternet/default_setters/cell_loaders.py new file mode 100644 index 0000000..c0c74ad --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/default_setters/cell_loaders.py @@ -0,0 +1,9 @@ +from bmtk.simulator.filternet.pyfunction_cache import add_cell_processor + + +def default_cell_loader(node): + print(node.model_template) + print('DEFAULT') + exit() + +add_cell_processor(default_cell_loader, 'default', overwrite=False) diff --git a/bmtk-vb/bmtk/simulator/filternet/filternetwork.py b/bmtk-vb/bmtk/simulator/filternet/filternetwork.py new file mode 100644 index 0000000..170a9e7 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/filternetwork.py @@ -0,0 +1,28 @@ +from bmtk.simulator.core.simulator_network import SimNetwork +from bmtk.simulator.filternet.cell import Cell +from bmtk.simulator.filternet.pyfunction_cache import py_modules + + +class FilterNetwork(SimNetwork): + def __init__(self): + super(FilterNetwork, self).__init__() + + self._local_cells = [] + + def cells(self): + return self._local_cells + + def build(self): + self.build_nodes() + + def set_default_processing(self, processing_fnc): + py_modules.add_cell_processor('default', processing_fnc) + + def build_nodes(self): + for node_pop in self.node_populations: + for node in node_pop.get_nodes(): + cell = Cell(node) + cell.build() + self._local_cells.append(cell) + + diff --git a/bmtk-vb/bmtk/simulator/filternet/filters.py b/bmtk-vb/bmtk/simulator/filternet/filters.py new file mode 100644 index 0000000..ae53df5 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/filters.py @@ -0,0 +1,3 @@ +from bmtk.simulator.filternet.lgnmodel.temporalfilter import * +from bmtk.simulator.filternet.lgnmodel.spatialfilter import * +from bmtk.simulator.filternet.lgnmodel.linearfilter import * \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/filtersimulator.py b/bmtk-vb/bmtk/simulator/filternet/filtersimulator.py new file mode 100644 index 0000000..7d6742a --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/filtersimulator.py @@ -0,0 +1,193 @@ +import csv + +from bmtk.simulator.core.simulator import Simulator +import bmtk.simulator.utils.simulation_inputs as inputs +from bmtk.simulator.filternet.config import Config +from bmtk.simulator.filternet.lgnmodel.movie import * +from bmtk.simulator.filternet import modules as mods +from bmtk.simulator.filternet.io_tools import io +from six import string_types + + +class FilterSimulator(Simulator): + def __init__(self, network, dt, tstop): + super(FilterSimulator, self).__init__() + self._network = network + self._dt = dt + self._tstop = tstop/1000.0 + + self.rates_csv = None + self._movies = [] + + def add_movie(self, movie_type, params): + # TODO: Move this into its own factory + movie_type = movie_type.lower() if isinstance(movie_type, string_types) else 'movie' + if movie_type == 'movie' or not movie_type: + raise NotImplementedError + + elif movie_type == 'full_field': + raise NotImplementedError + + elif movie_type == 'full_field_flash': + raise NotImplementedError + + elif movie_type == 'graiting': + init_params = FilterSimulator.find_params(['row_size', 'col_size', 'frame_rate'], **params) + create_params = FilterSimulator.find_params(['gray_screen_dur', 'cpd', 'temporal_f', 'theta', 'contrast'], + **params) + gm = GratingMovie(**init_params) + graiting_movie = gm.create_movie(t_min=0.0, t_max=self._tstop, **create_params) + self._movies.append(graiting_movie) + + else: + raise Exception('Unknown movie type {}'.format(movie_type)) + + def run(self): + for mod in self._sim_mods: + mod.initialize(self) + + io.log_info('Evaluating rates.') + for cell in self._network.cells(): + for movie in self._movies: + ts, f_rates = cell.lgn_cell_obj.evaluate(movie, downsample=1, separable=True) + + for mod in self._sim_mods: + mod.save(self, cell.gid, ts, f_rates) + + """ + if self.rates_csv is not None: + print 'saving {}'.format(cell.gid) + for t, f in zip(t, f_tot): + csv_writer.writerow([t, f, cell.gid]) + csv_fhandle.flush() + """ + io.log_info('Done.') + for mod in self._sim_mods: + mod.finalize(self) + + """ + def generate_spikes(LGN, trials, duration, output_file_name): + # f_tot = np.loadtxt(output_file_name + "_f_tot.csv", delimiter=" ") + # t = f_tot[0, :] + + f = h5.File(output_file_name + "_f_tot.h5", 'r') + f_tot = np.array(f.get('firing_rates_Hz')) + + t = np.array(f.get('time')) + # For h5 files that don't have time explicitly saved + t = np.linspace(0, duration, f_tot.shape[1]) + + + #create output file + f = nwb.create_blank_file(output_file_name + '_spikes.nwb', force=True) + + for trial in range(trials): + for counter in range(len(LGN.nodes())): + try: + spike_train = np.array(f_rate_to_spike_train(t*1000., f_tot[counter, :], np.random.randint(10000), 1000.*min(t), 1000.*max(t), 0.1)) + except: + spike_train = 1000.*np.array(pg.generate_inhomogenous_poisson(t, f_tot[counter, :], seed=np.random.randint(10000))) #convert to milliseconds and hence the multiplication by 1000 + + nwb.SpikeTrain(spike_train, unit='millisecond').add_to_processing(f, 'trial_%s' % trial) + f.close() + + """ + + + @staticmethod + def find_params(param_names, **kwargs): + ret_dict = {} + for pn in param_names: + if pn in kwargs: + ret_dict[pn] = kwargs[pn] + + return ret_dict + + @classmethod + def from_config(cls, config, network): + if not isinstance(config, Config): + try: + config = Config.load(config, False) + except Exception as e: + network.io.log_exception('Could not convert {} (type "{}") to json.'.format(config, type(config))) + + if not config.with_networks: + network.io.log_exception('Could not find any network files. Unable to build network.') + + sim = cls(network=network, dt=config.dt, tstop=config.tstop) + + network.io.log_info('Building cells.') + network.build_nodes() + + # TODO: Need to create a gid selector + for sim_input in inputs.from_config(config): + if sim_input.input_type == 'movie': + sim.add_movie(sim_input.module, sim_input.params) + else: + raise Exception('Unable to load input type {}'.format(sim_input.input_type)) + + + """ + node_set = network.get_node_set(sim_input.node_set) + if sim_input.input_type == 'spikes': + spikes = spike_trains.SpikesInput.load(name=sim_input.name, module=sim_input.module, + input_type=sim_input.input_type, params=sim_input.params) + io.log_info('Build virtual cell stimulations for {}'.format(sim_input.name)) + network.add_spike_trains(spikes, node_set) + + elif sim_input.module == 'IClamp': + # TODO: Parse from csv file + amplitude = sim_input.params['amp'] + delay = sim_input.params['delay'] + duration = sim_input.params['duration'] + gids = sim_input.params['node_set'] + sim.attach_current_clamp(amplitude, delay, duration, node_set) + + elif sim_input.module == 'xstim': + sim.add_mod(mods.XStimMod(**sim_input.params)) + + else: + io.log_exception('Can not parse input format {}'.format(sim_input.name)) + """ + + + rates_csv = config.output.get('rates_csv', None) + rates_h5 = config.output.get('rates_h5', None) + if rates_csv or rates_h5: + sim.add_mod(mods.RecordRates(rates_csv, rates_h5, config.output_dir)) + + spikes_csv = config.output.get('spikes_csv', None) + spikes_h5 = config.output.get('spikes_h5', None) + spikes_nwb = config.output.get('spikes_nwb', None) + if spikes_csv or spikes_h5 or spikes_nwb: + sim.add_mod(mods.SpikesGenerator(spikes_csv, spikes_h5, spikes_nwb, config.output_dir)) + + # Parse the "reports" section of the config and load an associated output module for each report + """ + sim_reports = reports.from_config(config) + for report in sim_reports: + if isinstance(report, reports.SpikesReport): + mod = mods.SpikesMod(**report.params) + + elif isinstance(report, reports.MembraneReport): + if report.params['sections'] == 'soma': + mod = mods.SomaReport(**report.params) + + else: + #print report.params + mod = mods.MembraneReport(**report.params) + + elif isinstance(report, reports.ECPReport): + mod = mods.EcpMod(**report.params) + # Set up the ability for ecp on all relevant cells + # TODO: According to spec we need to allow a different subset other than only biophysical cells + for gid, cell in network.cell_type_maps('biophysical').items(): + cell.setup_ecp() + else: + # TODO: Allow users to register customized modules using pymodules + io.log_warning('Unrecognized module {}, skipping.'.format(report.module)) + continue + + sim.add_mod(mod) + """ + return sim \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/io_tools.py b/bmtk-vb/bmtk/simulator/filternet/io_tools.py new file mode 100644 index 0000000..dfdcfaa --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/io_tools.py @@ -0,0 +1 @@ +from bmtk.simulator.core.io_tools import io diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/__init__.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/__init__.py new file mode 100644 index 0000000..72b9443 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/__init__.py @@ -0,0 +1,7 @@ +__version__ = '0.1.0' + +# from lgnmodel import lgnmodel +# from lgnmodel.dev import mask +# from lgnmodel.dev import movie +# from lgnmodel import cellmodel +# from lgnmodel.dev import boundcellmodel diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sOFF_cell_data.csv b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sOFF_cell_data.csv new file mode 100755 index 0000000..f6438be --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sOFF_cell_data.csv @@ -0,0 +1,29 @@ +Mouse_id,Shank,Clu,Area,Layer,WVF_ratio,WVF_duration,Bl_ctr_row,Bl_ctr_col,Wh_ctr_row,Wh_ctr_col,Bl_rad,Wh_rad,Bl_lat,Wh_lat,Bl_mfr,Wh_mfr,Bl_SI,Wh_SI,Area_Overlap,Area_Union,OverlapIdx,PSTH_correln,Subfield_sep,Bl_stimc,Wh_stimc,Bl_chk_num,Wh_chk_num,Spont,Spont_cyc,Pref_ori,Pref_SF,Pref_TF,Max_rate,Max_rate_cyc,F0_pref,Sharp,CV,OSI,OSI_wnull,DSI_sev,DSI_chen,f1f0b,f1f0,Pval,F0_TF1,F0_TF2,F0_TF4,F0_TF8,F0_TF15,F1_TF1,F1_TF2,F1_TF4,F1_TF8,F1_TF15 +60,3,12,1,NaN,0.227894433917728,0.275,8,13,10,9,3.31738342162619,0,94.5,NaN,21.0980965620245,9.81223461549439,0.312689578226931,NaN,0,56.309018948858,0,NaN,NaN,224,154,6,7,1.15555555555555,1.17037037037037,315,0.02,2,50,22.2222222222222,3.22222222222222,0.605603448275862,0.262695167743211,0.372781065088757,0.620071684587814,0.0265486725663716,0.0517241379310343,1.06592499221608,1.6739255292924,0.0435181045378624,2.16666666666667,3.22222222222222,2.22222222222222,1.61111111111111,0.833333333333333,2.71549870589566,3.4346471971407,3.7301484517374,2.33418898771623,1.50047031747581 +63,3,12,1,NaN,0.264513134151242,0.3,3,9,3,9,4.03491385399393,0,73.5,188.5,127.560438680997,48.7988511553626,0.342208990487044,0.322079894957013,0,51.146792844982,0,-0.436401558435213,0,147,147,5,11,9.35,9.16388888888889,135,0.04,1,75,54.1666666666667,17.625,0.700945626477541,0.110300729852412,0.254262416604892,0.464038359083644,0.231441048034935,0.375886524822695,1.108780641646,2.30965632673797,0.000512599358863601,17.625,11.2916666666667,15.4166666666667,8.875,2.875,19.5422588090107,11.9779602766624,14.173133697861,7.52200682626415,5.00527135935301 +72,2,25,1,NaN,0.23674463616793,0.375,6,14,15,19,11.6876336352714,0,144.5,NaN,23.6240071690253,18.4133737675155,0.356643544987323,NaN,0,454.751365334866,0,NaN,NaN,240,339,9,4,3.3,3.25,270,0.08,8,62.5,37.5,14.6666666666667,0.747422680412371,0.0658453670825114,0.193220338983051,0.422330097087379,0.20136518771331,0.335227272727273,0.352768488949166,0.453191635438344,0.000689821836055592,13.0833333333333,9.75,11.8333333333333,14.6666666666667,8.58333333333333,3.03189515980365,2.35162873257691,3.71804674030783,5.1739378379211,2.90346262297405 +72,3,2,1,NaN,0.17161401270352,0.45,7,14,5,32,3.2114115745907,0,106.5,NaN,10.7361652151892,6.0966299947273,0.357411817046969,NaN,0,37.6978353638313,0,NaN,NaN,241,563,7,6,2.3,0.25,0,0.16,8,37.5,33.3333333333333,6.08333333333333,0.415239726027397,0.345586939365797,0.654390934844193,0.759368836291913,0.309417040358744,0.472602739726027,0.459642275182134,0.479341229832797,0.0120917698906574,3.91666666666667,4.33333333333333,3.625,6.08333333333333,1.33333333333333,1.12042247030086,2.64044245685633,1.30729519345545,2.79615717402465,0.992882005175792 +72,3,15,1,NaN,0.191960207571084,0.525,7,14,14,2,3.8287025341613,0,128.5,NaN,25.8455956185529,12.3750239323341,0.464097667283985,NaN,0,50.2637804851085,0,NaN,NaN,241,32,8,5,0.1,0,180,0.16,8,25,12.5,2.16666666666667,0.432692307692308,0.461412217618803,0.664,0.816593886462882,0,0,0.527894845241589,0.527894845241589,0.0354251640054324,1.375,1.66666666666667,1.54166666666667,2.16666666666667,0.291666666666667,1.17481549663973,1.67067644404671,1.08148048724396,1.14377216469011,0.462933359250596 +72,3,19,1,NaN,0.193829748450602,0.45,7,12,17,18,3.80888656544766,0,95.5,NaN,15.6969845209821,9.88260583506573,0.397834985620538,NaN,0,61.6070931080991,0,NaN,NaN,205,323,6,2,1.9,1.75,45,0.64,15,75,16.6666666666667,3.875,0.75,0.0396701498587432,0.166144200626959,0.396825396825397,0.207792207792208,0.344086021505376,0.516975271992441,0.942719613633275,0.00462256555845095,2.79166666666667,3.20833333333333,3.29166666666667,2.91666666666667,3.875,1.12745038398484,0.965812716058466,1.85618045420095,1.42340203076708,2.00327917897071 +72,3,24,1,NaN,0.214114220695846,0.45,7,12,18,10,2.81304915787261,0,117.5,NaN,18.7368033776989,7.57736847420232,0.338538785120179,NaN,0,30.7695845402083,0,NaN,NaN,205,180,7,10,1.2,1.75,45,0.16,2,37.5,16.6666666666667,4.33333333333333,0.681490384615384,0.104771405575162,0.27217125382263,0.442622950819672,0.350649350649351,0.519230769230769,0.45560461795616,0.764240004313558,0.0375202720639944,3.54166666666667,4.33333333333333,3.08333333333333,2.29166666666667,2.20833333333333,2.00193125072158,1.97428667781003,1.89661235690541,1.16926096300871,1.4021824426304 +72,4,22,1,NaN,0.243258295337443,0.5,16,1,5,26,1.89448348117648,0,84.5,NaN,18.1616089127425,15.941831811114,0.313254251849423,NaN,0,32.7393813430031,0,NaN,NaN,16,455,6,5,0.7,0.5,135,0.02,4,62.5,25,6.33333333333333,0.689144736842105,0.168836493478692,0.208747514910537,0.496839443742099,0.027027027027027,0.0526315789473684,1.49136304384079,1.61919416188429,0.00797095394639537,3.875,4.33333333333333,6.33333333333333,3.70833333333333,5.04166666666667,5.18251916951976,6.44403480524721,9.44529927765837,5.83524176474689,7.47958040965291 +72,4,26,1,NaN,0.251147052076164,0.425,6,8,7,16,3.36590940295369,0,125.5,NaN,31.2988506309783,13.1607172188833,0.435950524637569,NaN,0,36.6789749485927,0,NaN,NaN,132,277,9,4,0,0,270,0.08,4,37.5,20.8333333333333,4.08333333333333,0.514030612244898,0.245707692206953,0.561752988047809,0.686609686609687,0.324324324324324,0.489795918367347,1.42520239084387,1.42520239084387,0.0153220356313431,2.91666666666667,2.54166666666667,4.08333333333333,3.25,0.541666666666667,3.15832004061644,3.12001867713679,5.81957642927912,4.76093191575991,0.932792515297761 +72,4,29,1,NaN,0.173264409742832,0.45,6,9,9,20,4.22853114375315,0,132.5,NaN,28.993229578405,10.4456145698005,0.445186921014291,NaN,0,60.180688526765,0,NaN,NaN,150,351,8,3,1.2,1.25,135,0.04,4,37.5,16.6666666666667,4,0.638020833333333,0.0560388988479427,0.207547169811321,0.405660377358491,0.288590604026846,0.447916666666667,1.43135854910831,2.08197607143027,0.00448418831120022,2.91666666666667,3.75,4,1.91666666666667,1.29166666666667,3.20708787843897,5.32832274286747,5.72543419643325,2.42846036962814,1.69737352881297 +72,5,16,1,NaN,0.32528169876946,0.45,12,17,2,20,9.76242188228036,0,10.5,NaN,16.2352767444367,16.1883204032263,0.318095563667322,NaN,0.203772083047737,434.645853140823,0.00046882325363338,NaN,NaN,300,344,2,5,3.2,2.75,135,0.04,4,87.5,58.3333333333333,16.4583333333333,0.715822784810127,0.0625111146961294,0.202435312024353,0.417130144605117,0.240188383045526,0.387341772151899,1.54763513865801,1.85810297802405,9.82286176845653e-05,13.8333333333333,15.75,16.4583333333333,10.2083333333333,15,14.5196957230697,19.9547738711671,25.4714949904131,17.8859216430096,23.7133703939669 +72,5,25,1,NaN,0.154176850826486,0.525,6,11,8,1,3.71986146157398,0,103.5,NaN,39.9487382339472,18.7840458760451,0.53388333550947,NaN,0,47.8864395162182,0,NaN,NaN,186,8,7,3,0.8,0,0,0.08,2,50,37.5,12.4166666666667,0.690855704697987,0.0835000262684289,0.337822671156005,0.490940465918896,0.37962962962963,0.550335570469799,0.91276594444903,0.91276594444903,0.00130098648313454,4.54166666666667,12.4166666666667,12.25,5.70833333333333,8.375,4.13731107678691,11.3335104769088,13.4014603108746,7.15836649799689,8.40963025038677 +72,6,18,1,NaN,0.192842487635676,0.425,6,9,7,24,2.86634576035391,0,92.5,NaN,46.4315953853779,13.4560643344102,0.321303060480483,NaN,0,25.81113051938,0,NaN,NaN,150,421,6,3,2,1,180,0.02,8,62.5,33.3333333333333,7.54166666666667,0.785714285714286,0.0543590713583877,-0.0189701897018971,0.234215885947047,0.194719471947195,0.325966850828729,1.72875777040663,1.99302647416306,0.0121928458017285,4.79166666666667,7.33333333333333,4.625,7.54166666666667,5.04166666666667,6.49902397742458,11.0959764435025,7.67992950266336,13.0377148518167,8.94178529614187 +72,6,27,1,NaN,0.163265955223293,0.425,6,9,11,31,5.26067852910692,0,187.5,NaN,28.1086462945887,18.259239443644,0.405490551796678,NaN,0,120.293453025847,0,NaN,NaN,150,551,11,10,0.9,1.75,135,0.02,1,62.5,33.3333333333333,6.875,0.756060606060606,0.0708240362608107,0.217712177121771,0.445026178010471,0.195652173913044,0.327272727272727,1.21203148773004,1.62589589817444,0.000917138003879289,6.875,5.54166666666667,3.33333333333333,3.875,2.83333333333333,8.332716478144,5.88975257891834,3.79454087673417,3.75877880958225,2.74849780724932 +73,5,9,1,NaN,0.268914895339528,0.375,6,10,4,21,5.3138386985443,0,94.5,NaN,13.3907614920431,8.32550972859402,0.351692126044824,NaN,0,104.874698741902,0,NaN,NaN,168,364,6,2,0.1,0.25,90,0.04,1,50,33.3333333333333,7.83333333333333,0.575581395348837,0.226393360499366,0.305555555555555,0.602385685884692,-0.0669975186104219,-0.143617021276596,1.35611352253308,1.40082056173747,0.000140317384885218,7.83333333333333,7.04166666666667,2.625,0.5,0.291666666666667,10.6228892598424,10.5808897672901,4.49326770686497,0.887518649793165,0.522100042786368 +73,6,14,1,NaN,0.242726417237974,0.3,9,11,3,6,9.14964940445724,0,65.5,NaN,45.3938924646263,25.8702040095581,0.334086340574054,NaN,0,278.556437526256,0,NaN,NaN,189,93,5,10,4.7,4.25,270,0.02,15,150,141.666666666667,24.0416666666667,0.868381240544629,0.0272134135049013,0.0382366171839857,0.370991468078847,-0.00944206008583688,-0.0190641247833622,1.76909268952665,2.14898206706711,1.04506321347993e-05,6.625,8.375,16.7083333333333,21.7083333333333,24.0416666666667,8.51928922805708,12.8457058640276,27.4766733503038,37.16245657985,42.5319367440365 +75,2,8,1,NaN,0.189102268383661,0.3,7,11,3,26,3.7946689474635,0,68.5,NaN,18.1486917869246,13.3610202594852,0.38722203250154,NaN,0,67.5844075441661,0,NaN,NaN,187,453,5,9,3.3,4,225,0.32,4,50,25,6.29166666666667,0.779801324503311,0.0309498508218965,0.100182149362477,0.352555701179554,0.170542635658915,0.291390728476821,0.425835896651261,1.16911309807892,0.000120970070208297,4.5,4.08333333333333,6.29166666666667,4.54166666666667,5.70833333333333,1.81889501379154,1.53151973185195,2.67921751643085,1.86448756331264,2.16687851095823 +75,3,6,1,NaN,0.156525539257234,0.3,6,10,1,16,4.3619242059316,0,71.5,NaN,31.3866228823021,18.946005773115,0.527174587558903,NaN,0,77.3654675304575,0,NaN,NaN,168,271,5,3,5.8,5.25,90,0.04,15,75,50,11.1666666666667,0.625932835820895,0.0860841200396548,0.271648873072361,0.466550825369244,0.270142180094787,0.425373134328358,1.6825021632129,3.17542661789476,0.000773426379641316,7.20833333333333,6.16666666666667,6.95833333333333,7.04166666666667,11.1666666666667,9.23802191648321,8.63768439604191,11.3629232423904,11.461549916428,18.787940822544 +75,3,10,1,NaN,0.205059107016681,0.3,7,11,13,4,3.35626033560117,0,102.5,NaN,27.7293416501643,9.88926872507762,0.5511360577852,NaN,0,37.8336834191965,0,NaN,NaN,187,67,7,4,7.9,8.75,45,0.02,15,125,91.6666666666667,20,0.761197916666666,0.0682387190512284,0.15872057936029,0.438582360048329,0.0750279955207168,0.139583333333334,1.63914777665678,2.91404049183427,8.49051217147285e-05,7.625,11.0833333333333,12.8333333333333,12.625,20,10.5593851547536,16.3644705428685,19.8691468326841,19.4162516634255,32.7829555331355 +75,3,14,1,NaN,0.193391587044465,0.275,7,8,17,27,5.13169011499309,0,72.5,NaN,22.85892380357,16.9770380143854,0.467112310488397,NaN,0,112.753885953081,0,NaN,NaN,133,485,5,2,6.6,6.25,225,0.08,4,87.5,62.5,17.4166666666667,0.634569377990431,0.0609248992044004,0.207220216606498,0.359019264448336,0.436426116838488,0.607655502392345,1.0870687813283,1.69550280072847,0.0313438418094254,11.75,12.875,17.4166666666667,13.0416666666667,7.08333333333333,9.36215604059938,9.84202467011974,18.9331146081346,16.3988326909231,8.89559308939555 +75,3,15,1,NaN,0.313001745093912,0.3,8,15,4,25,10.6510472403218,0,86.5,NaN,37.0214120367194,22.9181216449019,0.4520114522557,NaN,0,485.385101819709,0,NaN,NaN,260,436,6,7,5.2,5.75,180,0.32,1,87.5,54.1666666666667,24.1666666666667,0.500215517241379,0.348202850860347,0.688500727802038,0.810451727192205,0.135029354207436,0.237931034482759,0.126202913691368,0.165605633350663,9.89709604734052e-05,24.1666666666667,23.5416666666667,15.8333333333333,17,17.1666666666667,3.04990374754138,3.93465250177409,4.45322723594722,4.14212991396283,4.00548296424652 +77,2,25,1,NaN,0.258379026929274,0.375,10,22,7,7,8.9065730588013,0,187.5,NaN,24.7351110224677,22.4579227596474,0.50715649625158,NaN,0,322.503283436885,0,NaN,NaN,388,115,11,4,5.3,5,180,0.04,4,50,37.5,13.375,0.500389408099688,0.287783111281432,0.539568345323741,0.6751269035533,0.296969696969697,0.457943925233645,0.282369399924872,0.450948146148676,0.00118075407152943,12.4583333333333,13.25,13.375,10.5416666666667,12.6666666666667,4.24022664788561,5.37433560144109,3.77669072399516,3.62812819243134,3.8117929952908 +78,1,14,1,NaN,0.161806152197335,0.3,3,4,7,30,6.62335148075923,0,96.5,NaN,68.5583245747765,33.7420784958353,0.531495707189419,NaN,0,149.908329095452,0,NaN,NaN,57,529,6,11,13.2,16.25,225,0.02,2,87.5,58.3333333333333,20.5,0.715193089430894,0.0761481984225665,0.111236589497459,0.356763383735186,0.185542168674699,0.313008130081301,0.791461156384882,3.81763616609178,0.0017595645200145,13.1666666666667,20.5,14.9583333333333,14.2083333333333,11.75,13.4953037568487,16.2249537058901,14.9736415138084,14.2612878116158,12.0939263761713 +78,2,7,1,NaN,0.239215201127631,0.275,7,9,7,22,6.017889160438,0,59.5,NaN,56.0686653175444,22.7928361604366,0.366315406681982,NaN,0,123.350034271563,0,NaN,NaN,151,385,4,4,10.4,8.25,225,0.08,8,87.5,70.8333333333333,23.375,0.665552584670232,0.162892660368509,0.322333529758397,0.549902152641879,0.133333333333333,0.235294117647059,1.27533261529656,1.9709685872765,2.15623430262214e-05,9.375,11.8333333333333,22.2083333333333,23.375,11.5833333333333,8.63429439311284,13.7068058472998,25.8353763950164,29.810899882557,15.0089329977661 +78,2,10,1,NaN,0.255564925100401,0.3,3,31,16,24,3.96464206223105,0,187.5,NaN,14.4639652570043,12.2962025411232,0.406956185701321,NaN,0,179.794901275787,0,NaN,NaN,543,430,11,2,3,3.75,180,0.32,4,50,29.1666666666667,8.5,0.915441176470588,0.0360452798335642,0.0381679389312977,0.349397590361446,0.0408163265306121,0.0784313725490193,0.358570589289018,0.641652633464558,0.00353540111464831,5.58333333333333,6.08333333333333,8.5,4.33333333333333,2.41666666666667,3.03472779299531,2.19925771875221,3.04785000895665,2.34220359090903,1.89677282603666 +78,2,12,1,NaN,0.22823020436422,0.275,5,10,13,7,5.30569487966127,0,62.5,NaN,56.1729585863476,20.8519488568425,0.38673327032744,NaN,0,95.9087270878015,0,NaN,NaN,167,121,5,3,4.2,4.5,225,0.16,1,50,29.1666666666667,10.9583333333333,0.708650190114068,0.0776988864537472,0.190045248868778,0.450920245398773,0.112050739957717,0.201520912547529,0.746702318947234,1.2669852250524,0.000150046958143631,10.9583333333333,8.25,6.45833333333333,4.16666666666667,6.33333333333333,8.18261291179678,8.00310352210064,8.01003028585456,4.33601505186198,5.82912768418014 +63,2,2,1,NaN,0.119049915500518,0.35,5,7,5,7,5.36848635382528,0,63.5,NaN,61.5955705313946,41.2808571747207,0.366689375305713,NaN,0,90.5427289008778,0,NaN,NaN,113,113,5,11,6.30333333333333,6.41111111111111,270,0.08,2,62.5,41.6666666666667,16.2083333333333,0.686083123425693,0.0849905885078977,0.0843205574912892,0.318818040435459,0.223270440251572,0.365038560411311,0.754052323967631,1.24748945572757,0.00361592014864013,14.4166666666667,16.2083333333333,15.4583333333333,11.5833333333333,10.5833333333333,11.3388618807977,12.221931417642,16.030682212326,12.9797678284226,13.1982179165743 +63,2,6,1,NaN,0.246787403028116,0.275,4,9,4,9,6.01429531768067,0,63.5,NaN,47.5346848059286,54.4310078449639,0.437604558761872,NaN,0,113.636898312955,0,NaN,NaN,148,148,5,11,9.81166666666667,10.1472222222222,0,0.16,4,75,45.8333333333333,23.625,0.845017636684303,0.0868065796926466,0.163673678809646,0.463639355051004,0.0197841726618705,0.0388007054673722,0.158324117572715,0.277524035440218,0.000692696526636554,15.7083333333333,20.4583333333333,23.625,19.2916666666667,14.6666666666667,4.3983451939541,3.38407253347712,3.74040727765539,3.93974714619665,4.81246566442344 diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sON_cell_data.csv b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sON_cell_data.csv new file mode 100755 index 0000000..08ee244 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sON_cell_data.csv @@ -0,0 +1,23 @@ +Mouse_id,Shank,Clu,Area,Layer,WVF_ratio,WVF_duration,Bl_ctr_row,Bl_ctr_col,Wh_ctr_row,Wh_ctr_col,Bl_rad,Wh_rad,Bl_lat,Wh_lat,Bl_mfr,Wh_mfr,Bl_SI,Wh_SI,Area_Overlap,Area_Union,OverlapIdx,PSTH_correln,Subfield_sep,Bl_stimc,Wh_stimc,Bl_chk_num,Wh_chk_num,Spont,Spont_cyc,Pref_ori,Pref_SF,Pref_TF,Max_rate,Max_rate_cyc,F0_pref,Sharp,CV,OSI,OSI_wnull,DSI_sev,DSI_chen,f1f0b,f1f0,Pval,F0_TF1,F0_TF2,F0_TF4,F0_TF8,F0_TF15,F1_TF1,F1_TF2,F1_TF4,F1_TF8,F1_TF15 +60,2,10,1,NaN,0.195334635936007,0.3,10,17,7,10,0,8.66541108694164,NaN,61.5,14.3369503257721,23.3418634859177,NaN,0.369894205590993,0,795.186592079952,0,NaN,NaN,298,169,6,5,4.55999999999999,4.54814814814815,225,0.64,15,50,27.7777777777778,9.11111111111111,0.510670731707317,0.184133379263178,0.413793103448276,0.575,0.301587301587302,0.463414634146341,0.362847818857151,0.724517560380351,0.00303825454171586,5.55555555555556,4.61111111111111,5.55555555555556,5.5,9.11111111111111,2.18477310973599,2.21429766905465,1.28442570797043,1.66852860111851,3.30594679403182 +72,2,6,1,NaN,0.164650901025171,0.55,14,26,7,11,0,2.67920423277157,NaN,153.5,13.6987630666981,14.536343558531,NaN,0.508970866157443,0,39.1242399451655,0,NaN,NaN,464,187,7,11,2.3,1.75,270,0.04,8,37.5,25,2.66666666666667,0.46484375,0.103773857438895,0.326424870466321,0.46058091286307,0.454545454545455,0.625,1.68037570610877,4.88836569049824,0.0382100920720876,1.04166666666667,2.29166666666667,1.16666666666667,2.66666666666667,1.58333333333333,1.27921707995628,3.50166981622075,2.0656675945114,4.48100188295672,2.67678022646792 +72,2,26,1,NaN,0.3344343145235,0.55,14,13,8,11,0,22.338050703905,NaN,176.5,22.1850478435311,24.8512546002087,NaN,0.523543835583338,409.717734981316,3234.27050213368,0.126680107526882,NaN,NaN,230,188,10,10,5.8,3.5,180,0.32,1,62.5,41.6666666666667,18.8333333333333,0.673119469026549,0.0931761102566501,0.21098459477562,0.399286078531361,0.317784256559767,0.482300884955752,0.304905314300113,0.374503266477313,6.43124629642561e-05,18.8333333333333,14.5833333333333,11.125,17.125,14,5.7423834193188,3.92013733095239,1.51569285463246,2.65128842556021,2.54752025533966 +72,3,20,1,NaN,0.267229517066826,0.525,11,28,4,29,0,5.20905138663336,NaN,8.49999999999999,9.61684879280928,11.3614898494724,NaN,0.351299958425315,0,117.98403608464,0,NaN,NaN,497,508,5,2,1,0.25,180,0.04,8,62.5,50,8.75,0.726785714285714,0.0796959761336222,0.0383189122373301,0.288197621225984,0.193181818181818,0.323809523809524,1.55465310904619,1.60037820048872,0.000505355333596263,4.66666666666667,3.125,5.54166666666667,8.75,8.66666666666667,6.30360793581716,3.76268647580404,8.91959591843672,13.6032147041541,11.6831246427099 +72,3,25,1,NaN,0.167781666216991,0.45,7,3,7,14,0,2.91496304979481,NaN,98.5,11.5325019439142,26.7238773557078,NaN,0.434989832625236,0,33.7582417582418,0,NaN,NaN,43,241,9,6,6.4,4.5,0,0.08,2,75,45.8333333333333,20.3333333333333,0.61859631147541,0.143142512106825,0.369824561403509,0.539250897896357,0.301333333333333,0.463114754098361,1.13193139299252,1.45363820994829,0.000107554353819693,6.70833333333333,20.3333333333333,16.5833333333333,8.33333333333333,10.4166666666667,6.38270556956685,23.0159383241812,23.8107421763215,13.6121294525348,13.2136843916077 +72,3,28,1,NaN,0.285846367748608,0.475,6,23,8,13,0,3.20467198358483,NaN,163.5,11.5653476737154,27.6895556751833,NaN,0.54583551614614,0,41.2298848033254,0,NaN,NaN,402,224,2,10,3.3,1.25,270,0.04,8,100,66.6666666666667,13.9583333333333,0.786940298507462,0.023712659554386,0.112033195020747,0.341538461538462,0.229357798165138,0.373134328358209,1.82736605362479,2.00710697693215,1.97344460780919e-05,10.125,9.91666666666667,12.1666666666667,13.9583333333333,7.16666666666667,13.6431561330579,14.7953235714228,20.2497869884287,25.5069844985128,11.6064123684217 +72,4,6,1,NaN,0.196289462787722,0.45,8,31,7,11,0,3.0384403606745,NaN,76.5,9.73233924408585,13.2693675358585,NaN,0.480610176730089,0,54.4750702014284,0,NaN,NaN,548,187,4,5,1.1,1,225,0.08,1,37.5,20.8333333333333,4.16666666666667,0.555,0.170130788000192,0.36518771331058,0.566433566433566,0.19047619047619,0.32,0.996095214877805,1.31065159852343,0.000711129623582446,4.16666666666667,2.25,3.08333333333333,1.54166666666667,1.79166666666667,4.15039672865752,2.62884608494724,3.55879221873478,2.2213841981537,2.26591428588672 +72,4,9,1,NaN,0.214294572215156,0.475,18,7,6,9,0,4.27177138381067,NaN,135.5,9.52936353633489,16.4833587567275,NaN,0.427509241026204,0,64.1882061600371,0,NaN,NaN,126,150,3,6,0.1,0.25,315,0.02,8,50,20.8333333333333,3.5,0.388392857142857,0.258528435369155,0.615384615384615,0.68503937007874,0.570093457943925,0.726190476190476,1.74310505967557,1.877190064266,0.00505336318565361,1.04166666666667,1.875,2.16666666666667,3.5,1.04166666666667,1.24763858363152,2.38776711999282,3.05709788515879,6.10086770886451,1.7155806622439 +72,4,21,1,NaN,0.217844151005732,0.475,3,31,8,11,0,3.18776066773677,NaN,170.5,12.2025615058367,20.7168108660928,NaN,0.435841801543017,0,45.4411745196453,0,NaN,NaN,543,188,7,10,1.7,2.5,135,0.16,4,62.5,37.5,12.5,0.608333333333334,0.0958757327317662,0.160541586073501,0.369186046511628,0.273885350318471,0.43,0.2603860195393,0.325482524424125,0.00547165806238821,8.375,6.83333333333333,12.5,5.875,4.45833333333333,3.59489882839589,3.85625472720877,3.25482524424125,2.26673443467376,2.10461606793623 +72,4,24,1,NaN,0.10878335818925,0.525,18,29,7,12,0,2.53404897104881,NaN,145.5,10.3511882503664,12.4980165446304,NaN,0.438140238886373,0,30.973356623256,0,NaN,NaN,522,205,5,9,0.1,0,135,0.02,8,37.5,16.6666666666667,3.04166666666667,0.571917808219178,0.0984843365472965,0.247863247863248,0.460122699386503,0.226890756302521,0.36986301369863,1.77737719143541,1.77737719143541,0.00472255324225911,1.66666666666667,1.20833333333333,1.54166666666667,3.04166666666667,0.625,3.07106646108599,1.92539901200301,2.56057988567349,5.40618895728271,1.12497421428348 +72,5,15,1,NaN,0.208480053180709,0.45,8,5,9,12,0,3.84279423632037,NaN,152.5,16.6943191530683,32.5814332173706,NaN,0.40223320762938,0,55.3580825613019,0,NaN,NaN,80,207,7,9,7.9,6.75,180,0.64,1,87.5,50,22.0833333333333,0.75872641509434,0.0549956127678157,0.184357541899441,0.422924901185771,0.177777777777778,0.30188679245283,0.161878539451814,0.233140287797449,0.00279839051556877,22.0833333333333,11.8333333333333,21.0416666666667,15.0416666666667,13.2083333333333,3.57481774622756,3.11748344851772,3.57759609685268,3.40194551628711,3.06055137359331 +72,5,19,1,NaN,0.428997620088616,0.15,10,28,7,11,0,5.33819561166472,NaN,112.5,23.9938894732253,35.949116935107,NaN,0.49103810252798,0,113.229354146859,0,NaN,NaN,496,187,4,7,3,3.5,90,0.02,4,62.5,37.5,11.125,0.410112359550562,0.170485431073922,0.457025920873124,0.55431131019037,0.538904899135447,0.700374531835206,0.875806996894982,1.27781676596153,0.0117853881341222,4.70833333333333,4.79166666666667,11.125,3.91666666666667,0.958333333333333,2.50967638970477,2.53217635520425,9.74335284045668,5.73578914722165,0.959235740480376 +72,6,2,1,NaN,0.190965268808285,0.375,13,21,7,9,0,3.98910700374141,NaN,120.5,13.3659124777888,33.0006033745198,NaN,0.447981807753197,0,52.8448935370465,0,NaN,NaN,373,151,5,8,2.9,2.75,135,0.08,8,62.5,45.8333333333333,14.7916666666667,0.602816901408451,0.162490299501509,0.303948576675849,0.525953721075672,0.163934426229508,0.28169014084507,0.605162911628848,0.743366206326093,0.000537158073086421,3.66666666666667,4.75,5.16666666666667,14.7916666666667,11.25,2.23704014273044,4.48402994329032,6.85878410708804,8.95136806784337,10.3292341381655 +72,6,6,1,NaN,0.197515761108188,0.375,13,11,8,9,0,3.70821867413724,NaN,100.5,9.40118730320646,14.0110191514325,NaN,0.400352438170497,0,66.2259269905145,0,NaN,NaN,193,152,8,7,2,1.5,90,0.04,8,37.5,29.1666666666667,5,0.78125,0.0987026961243827,0.212121212121212,0.493506493506494,0.0434782608695652,0.0833333333333334,1.68961352013503,2.41373360019289,0.00198568320815603,2.20833333333333,2.79166666666667,3,5,2.33333333333333,3.09566678759879,4.06940890689401,5.40946818090603,8.44806760067513,3.39229267789208 +73,4,30,1,NaN,0.331675635535987,0.375,10,27,9,14,0,21.623444609417,NaN,126.5,17.1084521879945,20.9154487535644,NaN,0.525931485916512,323.454219824441,3480.35925442766,0.0929370206288179,NaN,NaN,478,243,4,8,1.4,1,45,0.02,2,62.5,41.6666666666667,11.5833333333333,0.602517985611511,0.164829106773475,0.300584795321637,0.481352992194276,0.302107728337236,0.464028776978417,1.28689372636422,1.40848998397344,3.78393090054118e-05,10.25,11.5833333333333,5.83333333333333,6.08333333333333,3.45833333333333,13.730833515557,14.9065189970522,8.154312506586,8.07544576276451,3.1756889386636 +73,5,15,1,NaN,0.269035257341049,0.425,9,2,8,11,0,21.5824105195833,NaN,76.5,25.5259179241373,34.2880657737409,NaN,0.644064874608546,57.2599553364141,1944.66491255224,0.0294446384910933,NaN,NaN,27,188,4,5,2.3,2,90,0.02,8,100,83.3333333333333,10.9583333333333,0.735245901639344,0.172830579196914,0.101570680628272,0.38139870223504,0.0981210855949896,0.178707224334601,1.84476433113939,2.25661869344028,3.26507399354889e-05,4.375,5.375,7.875,10.9583333333333,6.66666666666667,5.48434521400257,7.56330018968726,13.1400678542315,20.2155424620692,12.0456918874321 +73,5,20,1,NaN,0.35689874574847,0.375,7,22,9,14,0,48.3327076539571,NaN,52.5,33.2042213652416,40.0433630233324,NaN,0.517788682219814,5856.13797068123,8744.33555177217,0.669706455797479,NaN,NaN,385,243,4,4,17.2,16,90,0.02,15,187.5,195.833333333333,36.9166666666667,0.693707674943567,0.106838257011537,0.270250896057348,0.498027613412229,0.166556945358789,0.285553047404063,1.73169924739543,3.05634568365011,1.87970305511782e-06,18.0833333333333,19.0833333333333,24.9166666666667,29.625,36.9166666666667,24.3957816777468,25.610858029386,38.0768763948049,53.4565594288953,63.9285638830148 +73,6,10,1,NaN,0.253528460654241,0.4,1,26,7,11,0,26.5284743983064,NaN,182.5,27.0272474277681,43.0864002273731,NaN,0.433202727809693,76.6183032259491,2659.90492404979,0.0288049029622063,NaN,NaN,451,187,7,11,2.8,4.25,270,0.02,2,62.5,33.3333333333333,8.91666666666667,0.604771784232365,0.242878254862887,0.515044247787611,0.718974358974359,0.0214797136038186,0.0420560747663551,0.824283051631535,1.57496940222454,0.00271873587544628,8.875,8.91666666666667,8.41666666666667,6.875,4.25,8.14317431824067,7.34985721038119,7.45736442349215,8.44350960466603,3.32440853257175 +74,1,15,1,NaN,0.183727313954152,0.15,14,21,7,21,0,28.4981661351972,NaN,58.5,28.4534386038884,41.8461851356699,NaN,0.604351837481679,1143.02553784244,5694.27501271364,0.200732408478761,NaN,NaN,374,367,10,4,6,6.5,45,0.04,1,87.5,58.3333333333333,19.9166666666667,0.622907949790795,0.0651809699658942,0.325017325017325,0.450028232636928,0.489096573208723,0.656903765690377,1.10528441215473,1.64076381680112,0.000190181231333335,19.9166666666667,13.875,5.41666666666667,14.2916666666667,15.9583333333333,22.0135812087484,15.8633444518911,6.53086993544498,21.5396018940375,25.7521269169952 +74,2,14,1,NaN,0.231251511914634,0.325,2,18,7,10,0,5.46824379552042,NaN,153.5,16.1446156076403,18.2826899152246,NaN,0.340041370352017,0,119.750060804387,0,NaN,NaN,308,169,5,9,1.9,1.5,315,0.08,8,62.5,20.8333333333333,5.375,0.531976744186047,0.223628682474149,0.402173913043478,0.60431654676259,0.15695067264574,0.271317829457364,0.322173076149067,0.446885234658383,0.00149819575287729,3.04166666666667,2.125,3.125,5.375,3.375,1.62585277528586,1.79821781640358,1.39978294787706,1.73168028430123,1.53350083156145 +74,3,20,1,NaN,0.377663793477819,0.275,12,11,10,16,0,8.41605810757602,NaN,53.5,35.373197432754,40.4894314308,NaN,0.460420129411097,0.815088332190948,604.727618458001,0.00134786027181849,NaN,NaN,192,280,9,4,6.6,5,45,0.02,4,100,54.1666666666667,17.125,0.811739659367397,0.107705702431509,0.262672811059908,0.52286282306163,0.073107049608355,0.13625304136253,1.25678507377187,1.77504695986336,1.77457002212552e-05,9.5,11.5416666666667,17.125,13.375,13.2916666666667,10.8379556037253,12.8763796531722,21.5224443883432,23.1589205101968,21.360342369141 +78,1,17,1,NaN,0.322199590418021,0.3,2,23,4,3,0,37.4243183273526,NaN,29.5,32.3773236372484,33.0802094356025,NaN,0.608504560227227,2204.67809052115,6874.72668980918,0.320693198434968,NaN,NaN,398,40,8,3,21.7,21.25,225,0.08,4,75,50,18.8333333333333,0.768252212389381,0.0876143581084309,0.21505376344086,0.477638640429338,0.0944309927360775,0.172566371681416,1.09063082238159,-8.49939882269789,0.0312489529126974,14.625,14.1666666666667,18.8333333333333,16.5416666666667,18.75,13.0475087788804,17.0262378450584,20.5402138215199,21.0534416364737,24.8113490972229 diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sus_sus_cells_v3.csv b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sus_sus_cells_v3.csv new file mode 100755 index 0000000..240114e --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/sus_sus_cells_v3.csv @@ -0,0 +1,11 @@ +Mouse_id,Shank,Clu,Area,Layer,WVF_ratio,WVF_duration,Bl_ctr_row,Bl_ctr_col,Wh_ctr_row,Wh_ctr_col,Bl_rad,Wh_rad,Bl_lat,Wh_lat,Bl_mfr,Wh_mfr,Bl_SI,Wh_SI,Area_Overlap,Area_Union,OverlapIdx,PSTH_correln,Subfield_sep,Bl_stimc,Wh_stimc,Bl_chk_num,Wh_chk_num,Spont,Spont_cyc,Pref_ori,Pref_SF,Pref_TF,Max_rate,Max_rate_cyc,F0_pref,Sharp,CV,OSI,OSI_wnull,DSI_sev,DSI_chen,f1f0b,f1f0,Pval,F0_TF1,F0_TF2,F0_TF4,F0_TF8,F0_TF15,F1_TF1,F1_TF2,F1_TF4,F1_TF8,F1_TF15 +60,3,17,1,NaN,0.21988379,0.275,8,11,8,12,6.327865943,6.701237761,193.5,78.5,37.37712406,45.7133433,0.444857153,0.456371041,104.5350786,162.3384262,0.643933054,0.35553935,4,188,206,11,5,10.43555556,10.72222222,225,0.02,15,66.66666667,66.66666667,15.72222222,0.747699387,0.109007838,0.161025641,0.497234173,-0.070607553,-0.151943463,1.587591025,4.992091778,0.001231846,10.44444444,11.44444444,12.44444444,13.44444444,14.44444444,15.44444444,16.44444444,17.44444444,18.44444444,19.44444444 +61,3,2,1,NaN,0.217966289,0.275,10,9,9,11,16.52292262,8.516929279,120.5,87.5,31.04554547,33.76316153,0.510116736,0.418572618,222.5870387,862.9747717,0.257929949,0.668366904,8.94427191,154,189,7,6,2.620952381,2.650793651,180,0.04,8,100,80.95238095,15.23809524,0.428125,0.261312339,0.610062893,0.698736638,0.464530892,0.634375,0.803898246,0.973193337,0.006674439,5.666666667,3.619047619,8.19047619,15.23809524,3.904761905,2.677247676,1.879182797,6.742832439,12.24987804,3.509487327 +73,5,14,1,NaN,0.403757221,0.1,9,12,10,11,23.60903496,10.13939494,99.5,182.5,24.94225614,16.10278371,0.510265909,0.542734315,98.21814403,1975.842041,0.049709512,0.474228965,5.656854249,207,190,7,11,2.2,2,90,0.02,4,87.5,45.83333333,19.75,0.850140713,0.041302918,0.029315961,0.369089626,-0.022680412,-0.046413502,0.326612259,0.36341364,0.000343899,18.83333333,16.5,19.75,18.625,16.95833333,6.325000537,3.901611464,6.450592115,13.91496496,9.869628164 +73,5,24,1,NaN,0.314723274,0.4,9,10,9,10,4.541621803,1.727334002,98.5,131.5,29.1787525,10.99404131,0.451261624,0.411681223,9.37351582,64.79952241,0.144654088,0.382149377,0,171,171,6,8,0.6,0.5,45,0.16,4,37.5,16.66666667,5.291666667,0.431102362,0.486649606,0.687707641,0.828153565,0.016,0.031496063,0.630096291,0.695845469,0.029047724,2.125,5,5.291666667,4.75,0.625,1.586805628,2.579259347,3.334259537,2.30995181,0.564028931 +75,2,14,1,NaN,0.142546732,0.425,8,8,7,5,29.57267929,25.99540155,112.5,169.5,43.1770015,30.45673398,0.643575616,0.507588764,847.4201694,4023.004312,0.210643615,0.578750606,12.64911064,134,79,7,10,11.9,15,90,0.32,4,75,50,25.33333333,0.577919408,0.299900872,0.559974343,0.745643307,0.033135089,0.064144737,0.240499114,0.589610732,0.008356979,21.66666667,16.54166667,25.33333333,16,14.20833333,7.120547858,4.453865703,6.092644228,4.714501425,3.586799092 +75,3,12,1,NaN,0.045806832,0.35,7,9,8,9,3.652408635,1.029283045,74.5,105.5,50.48434961,19.19976844,0.57805952,0.496553402,0.067924028,45.16947841,0.001503759,0.463704924,4,151,152,5,7,3.9,2.75,90,0.16,4,50,45.83333333,17.58333333,0.441646919,0.181271486,0.464006938,0.547915143,0.595463138,0.746445498,0.836846909,0.991992685,0.001800185,11.04166667,11.5,17.58333333,11,8.041666667,6.885552531,6.495778681,14.71455815,11.77583318,5.816827922 +78,2,8,1,NaN,0.337036581,0.3,6,11,7,10,4.865683305,4.845646089,148.5,108.5,25.39909748,20.21502353,0.4358264,0.550653522,31.38090079,116.7614036,0.268760908,0.209763145,5.656854249,186,169,9,5,3,2.25,180,0.16,15,50,33.33333333,13.70833333,0.488981763,0.27877874,0.602923264,0.728106756,0.27027027,0.425531915,0.35278986,0.42206496,0.001579724,5.708333333,7.375,7.25,7.541666667,13.70833333,2.465575603,3.101341176,3.666361028,2.655445907,4.836161001 +78,3,13,1,NaN,0.146747351,0.3,5,7,5,8,5.36647229,1.800870186,79.5,109.5,40.72018458,16.63837297,0.497626777,0.479883721,10.18860415,90.47480487,0.112612613,0.398203514,4,113,131,5,7,4.1,2.25,315,0.08,4,62.5,37.5,13.5,0.602623457,0.21507263,0.367088608,0.613899614,0.033492823,0.064814815,1.364038002,1.636845602,6.12E-06,9.083333333,11.41666667,13.5,11.70833333,5.666666667,6.518626565,11.17129891,18.41451302,18.08773131,7.404301371 +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,36,18,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tOFF_cell_data.csv b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tOFF_cell_data.csv new file mode 100755 index 0000000..5a0c882 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tOFF_cell_data.csv @@ -0,0 +1,18 @@ +Mouse_id,Shank,Clu,Area,Layer,WVF_ratio,WVF_duration,Bl_ctr_row,Bl_ctr_col,Wh_ctr_row,Wh_ctr_col,Bl_rad,Wh_rad,Bl_lat,Wh_lat,Bl_mfr,Wh_mfr,Bl_SI,Wh_SI,Area_Overlap,Area_Union,OverlapIdx,PSTH_correln,Subfield_sep,Bl_stimc,Wh_stimc,Bl_chk_num,Wh_chk_num,Spont,Spont_cyc,Pref_ori,Pref_SF,Pref_TF,Max_rate,Max_rate_cyc,F0_pref,Sharp,CV,OSI,OSI_wnull,DSI_sev,DSI_chen,f1f0b,f1f0,Pval,F0_TF1,F0_TF2,F0_TF4,F0_TF8,F0_TF15,F1_TF1,F1_TF2,F1_TF4,F1_TF8,F1_TF15 +62,2,2,1,NaN,0.215549039041187,0.275,9,10,1,1,4.40139943774984,0,89.5,NaN,11.4607553064083,7.23264495463483,0.21704329596622,NaN,0,64.052358104672,0,NaN,NaN,171,1,6,3,0.834285714285718,0.742857142857142,135,0.32,4,42.8571428571429,9.52380952380952,2.38095238095238,0.565,0.22221025715911,0.428571428571429,0.63302752293578,0.123595505617978,0.22,0.981766325561374,1.42698593831595,0.0185094155784453,2,2.04761904761905,2.38095238095238,1.38095238095238,1,1.78402771385951,2.02612115745981,2.33753887038422,1.62320220368668,1.0868034839115 +62,3,3,1,NaN,0.218390570366483,0.275,6,9,12,2,5.60685057436199,0,66.5,NaN,21.0423061972472,10.8957804159064,0.265842714788767,NaN,0,107.319963738475,0,NaN,NaN,150,30,5,4,4.40571428571431,4.32063492063492,225,0.08,4,57.1428571428571,28.5714285714286,9,0.680555555555555,0.111063879629385,0.18125,0.427947598253275,0.155963302752294,0.26984126984127,0.921889826679877,1.7731056028748,0.00675805091247906,5.19047619047619,5.47619047619048,9,7.42857142857143,8.95238095238095,3.3031912443396,5.33219727104051,8.2970084401189,6.89913212674369,9.68056390590482 +62,3,5,1,NaN,0.197926357567497,0.275,4,8,2,4,5.57785442804055,0,75.5,NaN,27.7666141318318,13.313964158145,0.283153286968822,NaN,0,104.874698741902,0,NaN,NaN,130,56,5,10,2.79238095238097,2.64444444444444,45,0.08,2,71.4285714285714,23.8095238095238,5.57142857142857,0.757478632478632,0.0486345265149977,0.0758620689655171,0.354735152487961,0.109004739336493,0.196581196581197,0.846904980659619,1.61205882978051,0.00170523629049793,5.38095238095238,5.57142857142857,5.42857142857143,2.80952380952381,1.66666666666667,4.62218839714155,4.71847060653216,5.71702372992491,3.36081934119775,1.9838377906711 +72,2,3,1,NaN,0.219228375065107,0.45,9,20,13,6,3.18097096744501,0,171.5,NaN,12.0243263448861,10.7547248446559,0.22550229499408,NaN,0,53.9316779799677,0,NaN,NaN,351,103,10,6,0.8,0.25,315,0.64,15,37.5,12.5,2.95833333333333,0.60387323943662,0.0556996302464804,0.285067873303167,0.399239543726236,0.543478260869565,0.704225352112676,0.634369758786599,0.692926967289978,0.0188630922948254,2.29166666666667,0.375,1.33333333333333,2.83333333333333,2.95833333333333,1.18786881647464,0.352855348578216,1.28629593926641,1.88896647489622,1.87667720307702 +72,3,6,1,NaN,0.303651340054094,0.475,5,11,10,5,2.88514167165197,0,175.5,NaN,7.61894386746631,5.11392365433839,0.19984893684438,NaN,0,33.554469675194,0,NaN,NaN,185,82,10,4,0.1,0,0,0.02,15,25,8.33333333333333,0.791666666666667,0.592105263157895,0.191162783712058,0.310344827586207,0.5,0.266666666666667,0.421052631578947,1.10025474012538,1.10025474012538,0.00671478395873842,0.333333333333333,0.208333333333333,0.416666666666667,0.625,0.791666666666667,0.657769746816372,0.402959104280664,0.832019116885747,1.15589846012821,0.87103500259926 +72,3,8,1,NaN,0.204060705496308,0.45,9,14,7,7,2.94448241594942,0,91.5,NaN,23.6539725775758,5.40307877080598,0.227148188798464,NaN,0,27.2375351007142,0,NaN,NaN,243,115,6,2,0.2,0,225,0.02,15,50,37.5,5.875,0.554078014184397,0.131560480388299,0.247787610619469,0.492537313432836,0.128,0.226950354609929,1.63830360823029,1.63830360823029,0.00851226121028642,0.958333333333333,1.41666666666667,3,5.25,5.875,1.41866533560815,2.07280982427571,4.97146086971592,8.95908439080849,9.62503369835296 +72,4,11,1,NaN,0.121467735132855,0.425,2,13,8,3,2.53404897104881,0,51.5,NaN,8.72315355165845,7.84027106480733,0.250936754016357,NaN,0,37.6978353638313,0,NaN,NaN,218,44,4,9,0.1,0,90,0.02,4,37.5,12.5,1.75,0.476190476190476,0.212316096893288,0.541284403669725,0.6,0.68,0.80952380952381,1.42202602918566,1.42202602918566,0.00127938050810905,0.708333333333333,1.08333333333333,1.75,1.125,0.333333333333333,0.890715366202271,1.5105587568751,2.4885455510749,1.15101886396704,0.446888264581374 +72,4,13,1,NaN,0.165064781163654,0.4,5,9,14,1,2.72322696550062,0,102.5,NaN,38.6534392818895,6.76555110911782,0.246768639760459,NaN,0,23.2979414951246,0,NaN,NaN,149,14,7,2,0.2,0.25,45,0.04,8,37.5,16.6666666666667,3.79166666666667,0.581043956043956,0.271826494989175,0.510373443983402,0.708641975308642,0.0520231213872832,0.0989010989010988,1.53364475775656,1.64190203477467,0.00668181748704225,2.66666666666667,3.41666666666667,2.45833333333333,3.79166666666667,2.875,3.78601437983779,5.32693847854503,3.57133286263097,5.81506970649364,4.30216788976989 +74,4,26,1,NaN,0.213066995798835,0.375,5,16,7,5,7.32403078236779,0,73.5,NaN,10.6567039774559,8.21846108392401,0.226105620480015,NaN,0,187.945784597696,0,NaN,NaN,275,79,5,4,3.5,1.5,180,0.16,8,87.5,58.3333333333333,21.9583333333333,0.688242784380306,0.17555884492751,0.340966921119593,0.571192052980132,0.110642781875659,0.199240986717268,0.246462806198488,0.264533398913652,0.000186389460257988,11.1666666666667,15.625,10.4583333333333,21.9583333333333,6.54166666666667,3.93733712996556,3.61454481579941,4.59466872918316,5.41191245277514,2.77995569376241 +78,1,2,1,NaN,0.161143475730707,0.275,4,8,11,20,4.15372962981313,0,67.5,NaN,17.4880308364894,10.3810798326495,0.284779069546012,NaN,0,62.4221814402901,0,NaN,NaN,130,353,5,5,0.9,1.75,90,0.64,15,37.5,20.8333333333333,4.04166666666667,0.716494845360825,0.0814333195875668,0.371024734982332,0.484057971014493,0.515625,0.680412371134021,0.639572350375804,1.12797305429914,0.0248302824856542,2.625,3.29166666666667,2.04166666666667,3.58333333333333,4.04166666666667,1.86726284810314,1.71094863790475,1.19978031351727,2.27556720947382,2.58493824943554 +60,3,13,1,NaN,0.327845840428655,0.275,5,11,5,12,13.7959664558697,0,70.5,NaN,21.4829912811113,26.345156682018,0.271986804553519,NaN,0,597.935215689743,0,NaN,NaN,185,203,5,5,3.25333333333332,3.28148148148148,225,0.08,15,66.6666666666667,27.7777777777778,6.66666666666667,0.8125,0.106649406887618,0.126760563380282,0.413249211356467,0.0714285714285714,0.133333333333333,0.380618892553829,0.749577687742771,0.0225767012180496,3.44444444444445,3.22222222222222,4.66666666666667,3.83333333333333,6.66666666666667,1.4754221906457,2.22143660105183,2.05276157715755,2.03956046517732,2.5374592836922 +74,4,27,1,NaN,0.171967475142122,0.425,6,13,5,14,7.83326780213671,0,70.5,NaN,45.5289049512588,48.0564686452002,0.284859348176703,NaN,0,192.768390563159,0,NaN,NaN,222,239,5,7,4.4,5,180,0.04,4,50,33.3333333333333,13.375,0.470015576323987,0.229015558550861,0.42825361512792,0.610900832702498,0.206766917293233,0.342679127725857,0.37341006160471,0.596341441667224,0.0273395799205358,5.91666666666667,7.95833333333333,13.375,6.58333333333333,5.41666666666667,5.24514293869152,3.83526909785319,4.994359573963,3.69495923488874,1.75006614065008 +74,4,30,1,NaN,0.21589970754208,0.375,6,18,6,18,9.14964940445724,0,69.5,NaN,18.8875477576379,20.901651232031,0.23827178541323,NaN,0,263.001835186946,0,NaN,NaN,312,312,5,5,5.5,3.75,135,0.04,1,62.5,29.1666666666667,9.83333333333334,0.511122881355932,0.275602863630443,0.568106312292359,0.709821428571429,0.232375979112272,0.377118644067797,0.456952101507818,0.738634903807158,0.0336237550414378,9.83333333333334,4,4.58333333333333,5.20833333333333,1.45833333333333,4.49336233149354,1.84107264032154,1.91663363575812,3.67203571593219,1.5244219695287 +60,2,17,1,NaN,0.210579601832644,0.275,6,11,6,11,3.94276800806811,0,64.5,NaN,68.3907979469579,33.0741293106546,0.291222198592647,NaN,0,48.8373759037743,0,NaN,NaN,186,186,5,10,10.8777777777778,10.7888888888889,225,0.02,4,116.666666666667,66.6666666666667,16.6111111111111,0.70443143812709,0.110744129662319,0.202010050251256,0.46747149564051,0.0932358318098721,0.17056856187291,1.61675665077938,4.61269311625032,0.00359490460352649,12,10.3888888888889,16.6111111111111,13.6111111111111,14.3333333333333,17.0709984900058,15.2742718731787,26.8561243657241,21.8665174082843,23.9662722866475 +62,2,11,1,NaN,0.16565594104601,0.275,8,8,8,8,5.33414384970586,0,73.5,NaN,36.9172103761967,20.7529629579445,0.279188248021387,NaN,0,89.3880204302739,0,NaN,NaN,134,134,5,11,3.61333333333336,3.39365079365079,135,0.08,8,42.8571428571429,33.3333333333333,6.95238095238095,0.780821917808219,0.0658077460005423,0.17741935483871,0.441095890410959,0.110266159695818,0.198630136986302,1.55334081094115,3.03462656196353,7.45592685020831e-06,2.90476190476191,4.14285714285714,6.80952380952381,6.95238095238095,3.57142857142857,2.59084283971845,5.36811590918796,10.7597210527297,10.7994170665432,4.90613730007544 +62,3,4,1,NaN,0.137829754624094,0.275,6,10,6,10,4.15633140246399,0,70.5,NaN,39.7539863338688,27.282756259574,0.268876729571066,NaN,0,54.2712981183806,0,NaN,NaN,168,168,5,11,4.02666666666669,3.87936507936508,270,0.16,2,71.4285714285714,33.3333333333333,9.95238095238095,0.633771929824561,0.220457590731079,0.312401883830455,0.599268069533394,-0.0434782608695652,-0.0909090909090909,0.971607039042687,1.59225722289536,0.00223569037496728,5.28571428571429,9.95238095238095,7.38095238095238,9.76190476190476,6.61904761904762,3.26139269755511,9.6698033885677,10.6971171002793,12.7083262519171,5.85903230667237 +62,3,9,1,NaN,0.149611066777769,0.275,4,9,4,9,4.26417261458555,0,71.5,NaN,54.7282727298168,33.5884915562644,0.25687180983123,NaN,0,57.1241072810489,0,NaN,NaN,148,148,5,11,3.04952380952383,3.14285714285714,225,0.08,4,71.4285714285714,38.0952380952381,14.9047619047619,0.579472843450479,0.168516811168717,0.306889352818372,0.543956043956044,0.113879003558719,0.204472843450479,0.980894394884076,1.24299573116889,0.00103948076226042,12.1428571428571,10.3809523809524,14.9047619047619,12.2380952380952,5.71428571428572,9.28207380412442,13.8969989848769,14.6199974094627,13.1194669863453,7.60851795281775 diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tON_cell_data.csv b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tON_cell_data.csv new file mode 100755 index 0000000..c4713a6 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/tON_cell_data.csv @@ -0,0 +1,2 @@ +Mouse_id,Shank,Clu,Area,Layer,WVF_ratio,WVF_duration,Bl_ctr_row,Bl_ctr_col,Wh_ctr_row,Wh_ctr_col,Bl_rad,Wh_rad,Bl_lat,Wh_lat,Bl_mfr,Wh_mfr,Bl_SI,Wh_SI,Area_Overlap,Area_Union,OverlapIdx,PSTH_correln,Subfield_sep,Bl_stimc,Wh_stimc,Bl_chk_num,Wh_chk_num,Spont,Spont_cyc,Pref_ori,Pref_SF,Pref_TF,Max_rate,Max_rate_cyc,F0_pref,Sharp,CV,OSI,OSI_wnull,DSI_sev,DSI_chen,f1f0b,f1f0,Pval,F0_TF1,F0_TF2,F0_TF4,F0_TF8,F0_TF15,F1_TF1,F1_TF2,F1_TF4,F1_TF8,F1_TF15 +72,3,27,1,NaN,0.132502493887977,0.475,12,11,7,15,0,3.32064054899067,NaN,143.5,12.0701457420764,16.5396350519869,NaN,0.298956651109785,0,53.6599818692374,0,NaN,NaN,192,259,2,9,2.6,3.75,0,0.32,8,62.5,29.1666666666667,10.9166666666667,0.508587786259542,0.227164140602856,0.536656891495601,0.657266811279826,0.3717277486911,0.541984732824427,0.385321305601915,0.586942918998266,0.00329535217373217,9.25,8.83333333333334,10.7083333333333,10.9166666666667,5.45833333333333,3.86117629231897,4.97689599926721,7.40574622372637,4.20642425282091,2.24653624184082 diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/trans_sus_cells_v3.csv b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/trans_sus_cells_v3.csv new file mode 100755 index 0000000..a18e315 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cell_metrics/trans_sus_cells_v3.csv @@ -0,0 +1,8 @@ +Mouse_id,Shank,Clu,Area,Layer,WVF_ratio,WVF_duration,Bl_ctr_row,Bl_ctr_col,Wh_ctr_row,Wh_ctr_col,Bl_rad,Wh_rad,Bl_lat,Wh_lat,Bl_mfr,Wh_mfr,Bl_SI,Wh_SI,Area_Overlap,Area_Union,OverlapIdx,PSTH_correln,Subfield_sep,Bl_stimc,Wh_stimc,Bl_chk_num,Wh_chk_num,Spont,Spont_cyc,Pref_ori,Pref_SF,Pref_TF,Max_rate,Max_rate_cyc,F0_pref,Sharp,CV,OSI,OSI_wnull,DSI_sev,DSI_chen,f1f0b,f1f0,Pval,F0_TF1,F0_TF2,F0_TF4,F0_TF8,F0_TF15,F1_TF1,F1_TF2,F1_TF4,F1_TF8,F1_TF15 +74,1,16,1,NaN,0.180751985,0.175,8,17,8,18,3.49511124,4.859013414,80.5,95.5,34.18268004,35.83521367,0.345587999,0.460766164,37.83368342,74.71643045,0.506363636,0.607703791,4,296,314,5,6,5,6.25,0,0.16,1,125,79.16666667,36,0.50072338,0.258886811,0.56238698,0.691915977,0.300225734,0.461805556,0.245893542,0.297551849,6.79E-07,36,21.04166667,22.41666667,23.54166667,12.04166667,8.852167517,7.717443623,8.798522874,16.1074593,8.09474585 +74,4,27,1,NaN,0.171967475,0.425,6,13,5,14,7.833267802,6.991768818,70.5,109.5,45.52890495,48.05646865,0.284859348,0.552612747,142.8442302,203.5003869,0.701935915,0.00279346,5.656854249,222,239,5,7,4.4,5,180,0.04,4,50,33.33333333,13.375,0.470015576,0.229015559,0.428253615,0.610900833,0.206766917,0.342679128,0.373410062,0.596341442,0.02733958,5.916666667,7.958333333,13.375,6.583333333,5.416666667,5.245142939,3.835269098,4.994359574,3.694959235,1.750066141 +75,1,2,1,NaN,0.179020336,0.3,7,12,7,13,5.870579009,5.231830094,64.5,87.5,14.71608404,10.38016037,0.416686052,0.30332155,31.99221704,162.2705021,0.197153621,0.435460466,4,205,223,5,6,1,0.5,270,0.16,2,37.5,12.5,3.291666667,0.674157303,0.064616927,0.244094488,0.451428571,0.244094488,0.392405063,1.063534332,1.254018092,0.001120253,3.166666667,3.291666667,3.25,1.75,2.291666667,2.326214764,3.500800508,2.853026994,2.125669008,2.369890296 +78,3,7,1,NaN,0.25704279,0.3,6,6,8,5,8.514390322,3.457795816,69.5,68.5,24.97441569,17.92925808,0.255980641,0.343553303,14.87536206,250.4358901,0.059397884,0.774372866,8.94427191,96,80,5,5,3.3,2.5,225,0.02,15,50,25,5.708333333,0.682432432,0.051506564,0.033962264,0.37254902,-0.021428571,-0.04379562,1.492219197,2.654987403,0.005427301,3.208333333,3.458333333,3.791666667,5.125,5.708333333,4.43147768,4.621665853,5.537427652,8.081975,8.518084585 +78,3,11,1,NaN,0.2538379,0.25,6,9,6,10,5.969189721,2.135885692,59.5,83.5,78.43921026,33.31181738,0.268908037,0.485015497,1.290556526,124.9802109,0.010326087,0.398062208,4,150,168,4,6,11.2,13,315,0.04,8,75,50,21.83333333,0.654341603,0.112935538,0.195664575,0.454123113,0.116080937,0.208015267,1.650983868,4.080733712,0.000335046,10.29166667,13.125,19.16666667,21.83333333,17.29166667,10.79237938,19.16833868,29.71094753,36.04648112,25.37906864 +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,46,30,,,,,,,,,,,,5,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cellmodel.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cellmodel.py new file mode 100644 index 0000000..bc64495 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cellmodel.py @@ -0,0 +1,358 @@ +#import isee_engine +import os +import itertools +import matplotlib.pyplot as plt +import numpy as np +from . import utilities as util +import importlib +from .kernel import Kernel2D, Kernel3D +from .linearfilter import SpatioTemporalFilter +import json +from .spatialfilter import GaussianSpatialFilter +from .transferfunction import ScalarTransferFunction +from .temporalfilter import TemporalFilterCosineBump +from .cursor import LNUnitCursor, MultiLNUnitCursor +from .movie import Movie +from .lgnmodel1 import LGNModel, heat_plot +from .transferfunction import MultiTransferFunction, ScalarTransferFunction +from .lnunit import LNUnit, MultiLNUnit +from sympy.abc import x as symbolic_x +from sympy.abc import y as symbolic_y + + + +class OnUnit(LNUnit): + + def __init__(self, linear_filter, transfer_function): + assert linear_filter.amplitude > 0 + super(OnUnit, self).__init__(linear_filter, transfer_function) + +class OffUnit(LNUnit): + + def __init__(self, linear_filter, transfer_function): + assert linear_filter.amplitude < 0 + super(OffUnit, self).__init__(linear_filter, transfer_function) + +class LGNOnOffCell(MultiLNUnit): + """A cell model for a OnOff cell""" + def __init__(self, on_filter, off_filter, transfer_function=MultiTransferFunction((symbolic_x, symbolic_y), 'Heaviside(x)*(x)+Heaviside(y)*(y)')): + """Summary + + :param on_filter: + :param off_filter: + :param transfer_function: + """ + self.on_filter = on_filter + self.off_filter = off_filter + self.on_unit = OnUnit(self.on_filter, ScalarTransferFunction('s')) + self.off_unit = OffUnit(self.off_filter, ScalarTransferFunction('s')) + super(LGNOnOffCell, self).__init__([self.on_unit, self.off_unit], transfer_function) + +class TwoSubfieldLinearCell(MultiLNUnit): + + def __init__(self, dominant_filter, nondominant_filter,subfield_separation=10, onoff_axis_angle=45, dominant_subfield_location=(30,40), + transfer_function = MultiTransferFunction((symbolic_x, symbolic_y), 'Heaviside(x)*(x)+Heaviside(y)*(y)')): + + self.subfield_separation = subfield_separation + self.onoff_axis_angle = onoff_axis_angle + self.dominant_subfield_location = dominant_subfield_location + self.dominant_filter = dominant_filter + self.nondominant_filter = nondominant_filter + self.transfer_function= transfer_function + + self.dominant_unit = LNUnit(self.dominant_filter, ScalarTransferFunction('s'), amplitude=self.dominant_filter.amplitude) + self.nondominant_unit = LNUnit(self.nondominant_filter, ScalarTransferFunction('s'), amplitude=self.dominant_filter.amplitude) + + super(TwoSubfieldLinearCell, self).__init__([self.dominant_unit, self.nondominant_unit], self.transfer_function) + + self.dominant_filter.spatial_filter.translate = self.dominant_subfield_location + hor_offset = np.cos(self.onoff_axis_angle*np.pi/180.)*self.subfield_separation + self.dominant_subfield_location[0] + vert_offset = np.sin(self.onoff_axis_angle*np.pi/180.)*self.subfield_separation+ self.dominant_subfield_location[1] + rel_translation = (hor_offset,vert_offset) + self.nondominant_filter.spatial_filter.translate = rel_translation + + +class LGNOnCell(object): + + def __init__(self, **kwargs): + + self.position = kwargs.pop('position', None) + self.weights = kwargs.pop('weights', None) + self.kpeaks = kwargs.pop('kpeaks', None) + self.amplitude = kwargs.pop('amplitude', None) + self.sigma = kwargs.pop('sigma', None) + self.transfer_function_str = kwargs.pop('transfer_function_str', 's') # 'Heaviside(s)*s') + self.metadata = kwargs.pop('metadata', {}) + + temporal_filter = TemporalFilterCosineBump(self.weights, self.kpeaks) + spatial_filter = GaussianSpatialFilter(translate=self.position, sigma=self.sigma, origin=(0,0)) # all distances measured from BOTTOM LEFT + spatiotemporal_filter = SpatioTemporalFilter(spatial_filter, temporal_filter, amplitude=self.amplitude) + transfer_function = ScalarTransferFunction(self.transfer_function_str) + self.unit = OnUnit(spatiotemporal_filter, transfer_function) + +class LGNOffCell(OffUnit): + + def __init__(self, **kwargs): + + lattice_unit_center = kwargs.pop('lattice_unit_center', None) + weights = kwargs.pop('weights', None) + kpeaks = kwargs.pop('kpeaks', None) + amplitude = kwargs.pop('amplitude', None) + sigma = kwargs.pop('sigma', None) + width = kwargs.pop('width', 5) + transfer_function_str = kwargs.pop('transfer_function_str', 'Heaviside(s)*s') + + dxi = np.random.uniform(-width*1./2,width*1./2) + dyi = np.random.uniform(-width*1./2,width*1./2) + temporal_filter = TemporalFilterCosineBump(weights, kpeaks) + spatial_filter = GaussianSpatialFilter(translate=(dxi,dyi), sigma=sigma, origin=lattice_unit_center) # all distances measured from BOTTOM LEFT + spatiotemporal_filter = SpatioTemporalFilter(spatial_filter, temporal_filter, amplitude=amplitude) + transfer_function = ScalarTransferFunction(transfer_function_str) + super(LGNOnCell, self).__init__(spatiotemporal_filter, transfer_function) + +if __name__ == "__main__": + + movie_file = '/data/mat/iSee_temp_shared/movies/TouchOfEvil.npy' + m_data = np.load(movie_file, 'r') + m = Movie(m_data[1000:], frame_rate=30.) + + # Create second cell: + transfer_function = ScalarTransferFunction('s') + temporal_filter = TemporalFilterCosineBump((.4,-.3), (20,60)) + cell_list = [] + for xi in np.linspace(0,m.data.shape[2], 5): + for yi in np.linspace(0,m.data.shape[1], 5): + spatial_filter_on = GaussianSpatialFilter(sigma=(2,2), origin=(0,0), translate=(xi, yi)) + on_linear_filter = SpatioTemporalFilter(spatial_filter_on, temporal_filter, amplitude=20) + spatial_filter_off = GaussianSpatialFilter(sigma=(4,4), origin=(0,0), translate=(xi, yi)) + off_linear_filter = SpatioTemporalFilter(spatial_filter_off, temporal_filter, amplitude=-20) + on_off_cell = LGNOnOffCell(on_linear_filter, off_linear_filter) + cell_list.append(on_off_cell) + + lgn = LGNModel(cell_list) #Here include a list of all cells + y = lgn.evaluate(m, downsample=100) #Does the filtering + non-linearity on movie object m + heat_plot(y, interpolation='none', colorbar=True) + + + + + +# +# def imshow(self, ii, image_shape, fps, ax=None, show=True, relative_spatial_location=(0,0)): +# +# if ax is None: +# _, ax = plt.subplots(1,1) +# +# curr_kernel = self.get_spatio_temporal_kernel(image_shape, fps, relative_spatial_location=relative_spatial_location) +# +# cax = curr_kernel.imshow(ii, ax=ax, show=False) +# +# if show == True: +# plt.show() +# +# return ax +# +# +# class OnOffCellModel(CellModel): +# +# def __init__(self, dc_offset=0, on_subfield=None, off_subfield=None, on_weight = 1, off_weight = -1, t_max=None): +# +# super(self.__class__, self).__init__(dc_offset, t_max) +# +# if isinstance(on_subfield, dict): +# curr_module, curr_class = on_subfield.pop('class') +# self.on_subfield = getattr(importlib.import_module(curr_module), curr_class)(**on_subfield) +# else: +# self.on_subfield = on_subfield +# +# super(self.__class__, self).add_subfield(on_subfield, on_weight) +# +# if isinstance(off_subfield, dict): +# curr_module, curr_class = off_subfield.pop('class') +# self.off_subfield = getattr(importlib.import_module(curr_module), curr_class)(**off_subfield) +# else: +# self.off_subfield = off_subfield +# +# super(self.__class__, self).add_subfield(off_subfield, off_weight) +# +# +# def to_dict(self): +# +# return {'dc_offset':self.dc_offset, +# 'on_subfield':self.on_subfield.to_dict(), +# 'off_subfield':self.off_subfield.to_dict(), +# 't_max':self.t_max, +# 'class':(__name__, self.__class__.__name__)} +# +# class SingleSubfieldCellModel(CellModel): +# +# def __init__(self, subfield, weight = 1, dc_offset=0, t_max=None): +# +# super(SingleSubfieldCellModel, self).__init__(dc_offset, t_max) +# +# if isinstance(subfield, dict): +# curr_module, curr_class = subfield.pop('class') +# subfield = getattr(importlib.import_module(curr_module), curr_class)(**subfield) +# +# super(self.__class__, self).add_subfield(subfield, weight) +# +# def to_dict(self): +# +# assert len(self.subfield_list) == 1 +# subfield = self.subfield_list[0] +# weight = self.subfield_weight_dict[subfield] +# +# return {'dc_offset':self.dc_offset, +# 'subfield':subfield.to_dict(), +# 'weight':weight, +# 't_max':self.t_max, +# 'class':(__name__, self.__class__.__name__)} +# +# class OnCellModel(SingleSubfieldCellModel): +# +# def __init__(self, on_subfield, weight = 1, dc_offset=0 , t_max=None): +# assert weight > 0 +# super(OnCellModel, self).__init__(on_subfield, weight, dc_offset, t_max) +# +# def to_dict(self): +# data_dict = super(OnCellModel, self).to_dict() +# data_dict['on_subfield'] = data_dict.pop('subfield') +# return data_dict +# +# class OffCellModel(SingleSubfieldCellModel): +# +# def __init__(self, on_subfield, weight = -1, dc_offset=0 , t_max=None): +# assert weight < 0 +# super(OffCellModel, self).__init__(on_subfield, weight, dc_offset, t_max) +# +# def to_dict(self): +# data_dict = super(OffCellModel, self).to_dict() +# data_dict['off_subfield'] = data_dict.pop('subfield') +# return data_dict + + +# class OffCellModel(CellModel): +# +# def __init__(self, off_subfield, dc_offset=0, off_weight = 1, t_max=None): +# +# assert off_weight < 0. +# self.weight = off_weight +# +# +# +# +# super(self.__class__, self).__init__(dc_offset, t_max) +# +# if isinstance(on_subfield, dict): +# curr_module, curr_class = on_subfield.pop('class') +# self.subfield = getattr(importlib.import_module(curr_module), curr_class)(**on_subfield) +# else: +# self.subfield = on_subfield +# +# super(self.__class__, self).add_subfield(self.subfield, self.weight) +# +# def to_dict(self): +# +# return {'dc_offset':self.dc_offset, +# 'on_subfield':self.subfield.to_dict(), +# 'on_weight':self.weight, +# 't_max':self.t_max, +# 'class':(__name__, self.__class__.__name__)} + + + + + + +# if __name__ == "__main__": +# +# t = np.arange(0,.5,.001) +# example_movie = movie.Movie(file_name=os.path.join(isee_engine.movie_directory, 'TouchOfEvil.npy'), frame_rate=30.1, memmap=True) +# +# temporal_filter_on = TemporalFilterExponential(weight=1, tau=.05) +# on_subfield = Subfield(scale=(5,15), weight=.5, rotation=30, temporal_filter=temporal_filter_on, translation=(0,0)) +# +# temporal_filter_off = TemporalFilterExponential(weight=2, tau=.01) +# off_subfield = Subfield(scale=(5,15), weight=.5, rotation=-30, temporal_filter=temporal_filter_off) +# +# cell = OnOffCellModel(on_subfield=on_subfield, off_subfield=off_subfield, dc_offset=0., t_max=.5) +# curr_kernel = cell.get_spatio_temporal_kernel((100,150), 30.1) +# curr_kernel.imshow(0) +# +# print cell.to_dict() + + + +# f = cell.get_spatio_temporal_filter(example_movie.movie_data.shape[1:], t,threshold=.5) +# print len(f.t_ind_list) +# +# + +# for ii in range(example_movie.number_of_frames-curr_filter.t_max): +# print ii, example_movie.number_of_frames, curr_filter.map(example_movie, ii) + + +# off_subfield = Subfield(scale=(15,15), weight=.2, translation=(30,30)) + + +# +# curr_filter = cell.get_spatio_temporal_filter((100,150)) +# + +# +# # print touch_of_evil(40.41, mask=m) +# print curr_filter.t_max +# for ii in range(example_movie.number_of_frames-curr_filter.t_max): +# print ii, example_movie.number_of_frames, curr_filter.map(example_movie, ii) + +# cell.visualize_spatial_filter((100,150)) +# show_volume(spatio_temporal_filter, vmin=spatio_temporal_filter.min(), vmax=spatio_temporal_filter.max()) + + + +# def get_spatial_filter(self, image_shape, relative_spatial_location=(0,0), relative_threshold=default_relative_threshold): +# +# # Initialize: +# translation_matrix = util.get_translation_matrix(relative_spatial_location) +# +# # On-subunit: +# on_filter_pre_spatial = self.on_subfield.get_spatial_filter(image_shape) +# on_filter_spatial = util.apply_transformation_matrix(on_filter_pre_spatial, translation_matrix) +# +# # Off-subunit: +# off_filter_pre_spatial = self.off_subfield.get_spatial_filter(image_shape) +# off_filter_spatial = util.apply_transformation_matrix(off_filter_pre_spatial, translation_matrix) +# +# spatial_filter = on_filter_spatial - off_filter_spatial +# +# tmp = np.abs(spatial_filter) +# spatial_filter[np.where(tmp/tmp.max() < relative_threshold )] = 0 +# +# return spatial_filter + +# kernel = float(self.dc_offset)/len(nonzero_ind_tuple[0])+spatio_temporal_filter[nonzero_ind_tuple] + +# def rectifying_filter_factory(kernel, movie, dc_offset=0): +# +# def rectifying_filter(t): +# +# fi = movie.frame_rate*float(t) +# fim, fiM = np.floor(fi), np.ceil(fi) +# +# print t, fim, fiM +# +# try: +# s1 = (movie.movie_data[int(fim)+kernel.t_ind_list, kernel.row_ind_list, kernel.col_ind_list]*kernel.kernel).sum() +# s2 = (movie.movie_data[int(fiM)+kernel.t_ind_list, kernel.row_ind_list, kernel.col_ind_list]*kernel.kernel).sum() +# except IndexError: +# return None +# +# # Linear interpolation: +# s_pre = dc_offset + s1*((1-(fi-fim))*.5) + s2*((fi-fim)*.5) +# +# if s_pre < 0: +# return 0 +# else: +# return float(s_pre) +# +# return rectifying_filter diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cursor.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cursor.py new file mode 100644 index 0000000..8406fd1 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/cursor.py @@ -0,0 +1,266 @@ +from .movie import Movie +import numpy as np +from .linearfilter import SpatioTemporalFilter +from .spatialfilter import GaussianSpatialFilter +from .temporalfilter import TemporalFilterCosineBump +from .utilities import convert_tmin_tmax_framerate_to_trange +import matplotlib.pyplot as plt +from .kernel import Kernel3D +import scipy.signal as spsig +import time + +class KernelCursor(object): + + + def __init__(self, kernel, movie): + + self.movie = movie + self.kernel = kernel + self.cache = {} + + # print self.kernel.t_range.min(), self.kernel.t_range.max(), type(kernel), len(self.kernel) + + # This ensures that the kernel frame rate matches the movie frame rate: + np.testing.assert_almost_equal(np.diff(self.kernel.t_range), np.ones_like(self.kernel.t_range[1:])*(1./movie.frame_rate)) + + @property + def row_range(self): + return self.movie.row_range + + @property + def col_range(self): + return self.movie.col_range + + @property + def t_range(self): + return self.movie.t_range + + @property + def frame_rate(self): + return self.movie.frame_rate + + def evaluate(self, t_min=None, t_max=None, downsample=1):#:#, show=True, ax=None, plot=False, save_file_name=None, plotstyle='b-'): + + + # print 'EVALUATE' + if t_max is None: + t_max = self.t_range[-1] + + if t_min is None: + t_min = self.t_range[0] + + t_range = convert_tmin_tmax_framerate_to_trange(t_min, t_max, self.movie.frame_rate)[::int(downsample)] + y_vals = np.array([self(t) for t in t_range]) + + return t_range, y_vals + + def __call__(self, t): + + + + if t < self.t_range[0] or t > self.t_range[-1]: + curr_rate = 0 + else: +# print 'zero' + + ti = t*self.frame_rate + til, tir = int(np.floor(ti)), int(np.ceil(ti)) + + tl, tr = float(til)/self.frame_rate, float(tir)/self.frame_rate + if np.abs(tl-t)<1e-12: + curr_rate = self.apply_dot_product(til) + # print 'a' + + elif np.abs(tr-t)<1e-12: + curr_rate = self.apply_dot_product(tir) + # print 'b' + else: + wa, wb = (1-(t-tl)/(tr-tl)), (1-(tr-t)/(tr-tl)) + cl = self.apply_dot_product(til) + cr = self.apply_dot_product(tir) + curr_rate = cl*wa+cr*wb + # print 'c' + + if np.isnan(curr_rate): + assert RuntimeError + + return curr_rate + + def apply_dot_product(self, ti_offset): + + try: + return self.cache[ti_offset] + + except KeyError: + t_inds = self.kernel.t_inds + ti_offset + 1 # Offset by one nhc 14 Apr '17 + min_ind, max_ind = 0, self.movie.data.shape[0] + allowed_inds = np.where(np.logical_and(min_ind <= t_inds, t_inds < max_ind)) + t_inds = t_inds[allowed_inds] + row_inds = self.kernel.row_inds[allowed_inds] + col_inds = self.kernel.col_inds[allowed_inds] + kernel_vector = self.kernel.kernel[allowed_inds] + result = np.dot(self.movie[t_inds, row_inds, col_inds],kernel_vector) + self.cache[ti_offset ] = result + return result + +class FilterCursor(KernelCursor): + + def __init__(self, spatiotemporal_filter, movie, threshold=0): + + self.spatiotemporal_filter = spatiotemporal_filter + kernel = self.spatiotemporal_filter.get_spatiotemporal_kernel(movie.row_range, movie.col_range, t_range=movie.t_range, threshold=threshold, reverse=True) + + super(FilterCursor, self).__init__(kernel, movie) + +class LNUnitCursor(KernelCursor): + + def __init__(self, lnunit, movie, threshold=0): + + # print 'LNUnitCursor' + + self.lnunit = lnunit + + kernel = lnunit.get_spatiotemporal_kernel(movie.row_range, movie.col_range, movie.t_range, reverse=True, threshold=threshold) + + kernel.apply_threshold(threshold) + + super(LNUnitCursor, self).__init__(kernel, movie) + + def __call__(self, t): + return self.lnunit.transfer_function(super(LNUnitCursor, self).__call__(t)) + +class MultiLNUnitCursor(object): + + def __init__(self, multi_lnunit, movie, threshold=0): + + self.multi_lnunit = multi_lnunit + self.lnunit_cursor_list = [LNUnitCursor(lnunit, movie, threshold=threshold) for lnunit in multi_lnunit.lnunit_list] + self.movie = movie + + def evaluate(self, **kwargs): + +# print len(self.lnunit_cursor_list) +# for ii, x in enumerate(self.lnunit_cursor_list): +# +# print ii, self.multi_lnunit, self.multi_lnunit.transfer_function, x +# print ii, x.evaluate(**kwargs), kwargs +# print 'done' +# # print lnunit, movie, curr_cursor + + + + multi_e = [unit_cursor.evaluate(**kwargs) for unit_cursor in self.lnunit_cursor_list] + t_list, y_list = zip(*multi_e) + +# plt.figure() +# plt.plot(t_list[0],y_list[0]) +# plt.plot(t_list[0],y_list[1],'r') +# plt.show() + + #sys.exit() + +# print len(y_list) + + return t_list[0], self.multi_lnunit.transfer_function(*y_list) + +class MultiLNUnitMultiMovieCursor(MultiLNUnitCursor): + + def __init__(self, multi_lnunit, movie_list, threshold=0.): + + assert len(multi_lnunit.lnunit_list) == len(movie_list) + + self.multi_lnunit = multi_lnunit + self.lnunit_movie_list = movie_list + self.lnunit_cursor_list = [lnunit.get_cursor(movie, threshold=threshold) for lnunit, movie in zip(multi_lnunit.lnunit_list, movie_list)] +# for lnunit, movie, curr_cursor in zip(multi_lnunit.lnunit_list, movie_list, self.lnunit_cursor_list): +# print lnunit, movie, curr_cursor + +class SeparableKernelCursor(object): + + def __init__(self, spatial_kernel, temporal_kernel, movie): + '''Assumes temporal kernel is not reversed''' + + self.movie = movie + self.spatial_kernel = spatial_kernel + self.temporal_kernel = temporal_kernel + + def evaluate(self, threshold=0): + + full_spatial_kernel = np.array([self.spatial_kernel.full()]) + full_temporal_kernel = self.temporal_kernel.full() + + nonzero_inds = np.where(np.abs(full_spatial_kernel[0,:,:])>=threshold) + rm, rM = nonzero_inds[0].min(), nonzero_inds[0].max() + cm, cM = nonzero_inds[1].min(), nonzero_inds[1].max() + + convolution_answer_sep_spatial = (self.movie.data[:,rm:rM+1, cm:cM+1] * full_spatial_kernel[:,rm:rM+1, cm:cM+1]).sum(axis=1).sum(axis=1) + sig_tmp = np.zeros(len(full_temporal_kernel) + len(convolution_answer_sep_spatial) - 1) + sig_tmp[len(full_temporal_kernel)-1:] = convolution_answer_sep_spatial + convolution_answer_sep = spsig.convolve(sig_tmp, full_temporal_kernel[::-1], mode='valid') + t = np.arange(len(convolution_answer_sep))/self.movie.frame_rate + return t, convolution_answer_sep + + +class SeparableSpatioTemporalFilterCursor(SeparableKernelCursor): + + def __init__(self, spatiotemporal_filter, movie): + + self.spatial_filter = spatiotemporal_filter.spatial_filter + self.temporal_filter = spatiotemporal_filter.temporal_filter + + spatial_kernel = self.spatial_filter.get_kernel(movie.row_range, movie.col_range, threshold=-1) + temporal_kernel = self.temporal_filter.get_kernel(t_range=movie.t_range, threshold=0, reverse=True) + spatial_kernel.kernel *= spatiotemporal_filter.amplitude + + super(SeparableSpatioTemporalFilterCursor, self).__init__(spatial_kernel, + temporal_kernel, + movie) + + +class SeparableLNUnitCursor(SeparableSpatioTemporalFilterCursor): + def __init__(self, lnunit, movie): + self.lnunit = lnunit + + super(SeparableLNUnitCursor, self).__init__(self.lnunit.linear_filter, movie) + + def evaluate(self, downsample = 1): + + assert downsample == 1 + + t, y = super(SeparableLNUnitCursor, self).evaluate() + + return t, [self.lnunit.transfer_function(yi) for yi in y] + +class SeparableMultiLNUnitCursor(object): + + def __init__(self, multilnunit, movie): + + self.multilnunit = multilnunit + + self.lnunit_cursor_list = [] + for lnunit in self.multilnunit.lnunit_list: + self.lnunit_cursor_list.append(SeparableLNUnitCursor(lnunit, movie)) + + def evaluate(self, *args, **kwargs): + + assert kwargs.get('downsample', 1) == 1 + + y_list = [] + for cursor in self.lnunit_cursor_list: + t, y = cursor.evaluate(*args, **kwargs) + y_list.append(y) + + return t, self.multilnunit.transfer_function(*y_list) + +# if __name__ == "__main__": +# spatial_filter_1 = GaussianSpatialFilter(sigma=(2.,2.), amplitude=10) +# temporal_filter = TemporalFilterCosineBump((.4,-.3), (40,80)) +# curr_filter = SpatioTemporalFilter(spatial_filter_1, temporal_filter) +# +# movie_file = '/data/mat/iSee_temp_shared/movies/TouchOfEvil.npy' +# m_data = np.load(movie_file, 'r') +# movie = Movie(m_data[:,:,:], frame_rate=30.) +# cursor = FilterCursor(curr_filter, movie, threshold=-1) +# cursor.evaluate() + + \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/fitfuns.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/fitfuns.py new file mode 100644 index 0000000..5b67919 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/fitfuns.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 13 17:07:50 2014 + +@author: rami +""" +import os +from math import * +import numpy as np +import numpy.fft as npft +from random import * +import scipy.io as sio +#import statsmodels.api as sm +from scipy import stats +import matplotlib.pyplot as plt + +def makeFitStruct_GLM(dtsim,kbasprs,nkt,flag_exp): + + gg = {} + gg['k'] = [] + gg['dc'] = 0 + gg['kt'] = np.zeros((nkt,1)) + gg['ktbas'] = [] + gg['kbasprs'] = kbasprs + gg['dt'] = dtsim + + nkt = nkt + if flag_exp==0: + ktbas = makeBasis_StimKernel(kbasprs,nkt) + else: + ktbas = makeBasis_StimKernel_exp(kbasprs,nkt) + + gg['ktbas'] = ktbas + gg['k'] = gg['ktbas']*gg['kt'] + + return gg + +def makeBasis_StimKernel(kbasprs,nkt): + + neye = kbasprs['neye'] + ncos = kbasprs['ncos'] + kpeaks = kbasprs['kpeaks'] + kdt = 1 + b = kbasprs['b'] + delays_raw = kbasprs['delays'] + delays = delays_raw[0].astype(int) + + ylim = np.array([100.,200.]) # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!HARD-CODED FOR NOW +# yrnge = nlin(kpeaks + b*np.ones(np.shape(kpeaks))) + yrnge = nlin(ylim + b*np.ones(np.shape(kpeaks))) + db = (yrnge[-1]-yrnge[0])/(ncos-1) + ctrs = nlin(np.array(kpeaks))#yrnge + mxt = invnl(yrnge[ncos-1]+2*db)-b + kt0 = np.arange(0,mxt,kdt) #-delay + nt = len(kt0) + e1 = np.tile(nlin(kt0+b*np.ones(np.shape(kt0))),(ncos,1)) + e2 = np.transpose(e1) + e3 = np.tile(ctrs,(nt,1)) + + kbasis0 = [] + for kk in range(ncos): + kbasis0.append(ff(e2[:,kk],e3[:,kk],db)) + + + #Concatenate identity vectors + nkt0 = np.size(kt0,0) + a1 = np.concatenate((np.eye(neye), np.zeros((nkt0,neye))),axis=0) + a2 = np.concatenate((np.zeros((neye,ncos)),np.array(kbasis0).T),axis=0) + kbasis = np.concatenate((a1,a2),axis=1) + kbasis = np.flipud(kbasis) + nkt0 = np.size(kbasis,0) + + if nkt0 < nkt: + kbasis = np.concatenate((np.zeros((nkt-nkt0,ncos+neye)),kbasis),axis=0) + elif nkt0 > nkt: + kbasis = kbasis[-1-nkt:-1,:] + + + kbasis = normalizecols(kbasis) + +# plt.figure() +# plt.plot(kbasis[:,0],'b') +# plt.plot(kbasis[:,1],'r') +# plt.show() +# +# print kpeaks +# print nkt0, nkt +# print delays[0][0], delays[0][1] +# print sev + kbasis2_0 = np.concatenate((kbasis[:,0],np.zeros((delays[0],))),axis=0) + kbasis2_1 = np.concatenate((kbasis[:,1],np.zeros((delays[1],))),axis=0) + +# plt.figure() +# plt.plot(kbasis2_0,'b') +# plt.plot(kbasis2_1,'r') +# plt.show(block=False) + + len_diff = delays[1]-delays[0] + kbasis2_1 = kbasis2_1[len_diff:] + + kbasis2 = np.zeros((len(kbasis2_0),2)) + kbasis2[:,0] = kbasis2_0 + kbasis2[:,1] = kbasis2_1 + # print(np.shape(kbasis2_0)) + # print(len(kbasis2_0), len(kbasis2_1)) + + +# plt.figure() +# plt.plot(kbasis[:,0],'b') +# plt.plot(kbasis[:,1],'r') +# plt.plot(kbasis2_0,'m') +# plt.plot(kbasis2_1,'k') +# plt.show(block=False) + + kbasis2 = normalizecols(kbasis2) + + return kbasis2 + + +def makeBasis_StimKernel_exp(kbasprs,nkt): + ks = kbasprs['ks'] + b = kbasprs['b'] + x0 = np.arange(0,nkt) + kbasis = np.zeros((nkt,len(ks))) + for ii in range(len(ks)): + kbasis[:,ii] = invnl(-ks[ii]*x0) #(1.0/ks[ii])* + + kbasis = np.flipud(kbasis) + #kbasis = normalizecols(kbasis) + + return kbasis + +def nlin(x): + eps = 1e-20 + #x.clip(0.) + + return np.log(x+eps) + +def invnl(x): + eps = 1e-20 + return np.exp(x)-eps + +def ff(x,c,dc): + rowsize = np.size(x,0) + m = [] + for i in range(rowsize): + xi = x[i] + ci = c[i] + val=(np.cos(np.max([-pi,np.min([pi,(xi-ci)*pi/dc/2])]))+1)/2 + m.append(val) + + return np.array(m) + +def normalizecols(A): + + B = A/np.tile(np.sqrt(sum(A**2,0)),(np.size(A,0),1)) + + return B + +def sameconv(A,B): + + am = np.size(A) + bm = np.size(B) + nn = am+bm-1 + + q = npft.fft(A,nn)*npft.fft(np.flipud(B),nn) + p = q + G = npft.ifft(p) + G = G[range(am)] + + return G + +# kbasprs = {} +# kbasprs['neye'] = 0 +# kbasprs['ncos'] = 2 +# kbasprs['kpeaks'] = 40,80 +# kbasprs['b'] = .3 +# +# nkt = 400 +# +# filter_data = makeBasis_StimKernel(kbasprs, nkt) +# +# print filter_data +# +# print [x for x in filter_data.T] +# +# import matplotlib.pyplot as plt +# plt.plot(filter_data[:,0]+filter_data[:,1]) +# plt.show() + diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/kernel.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/kernel.py new file mode 100644 index 0000000..820b1a3 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/kernel.py @@ -0,0 +1,475 @@ +#from matplotlib import _cntr as cntr +import matplotlib as mpl +from mpl_toolkits.mplot3d import Axes3D +from matplotlib import cm +import scipy.interpolate as spinterp +import h5py +import numpy as np +import bisect +import matplotlib.pyplot as plt + +def find_l_r_in_t_range(t_range, t): + + for tl in range(len(t_range)-1): + tr = tl+1 + test_val = (t_range[tl]-t)*(t_range[tr]-t) + if np.abs(test_val) < 1e-16: + + if np.abs(t_range[tl]-t) < 1e-16: + return (tl,) + else: + return (tr,) + + elif test_val < 0: + t_range[tl], t_range[tr], t + return tl, tr + +def get_contour(X, Y, Z, c): + contour_obj = plt.contour(X, Y, Z) + #contour_obj = cntr.Cntr(X, Y, Z) + res = contour_obj.trace(c) + nseg = len(res) // 2 + if nseg > 0: + seg = res[:nseg][0] + return seg[:,0], seg[:,1] + else: + return [],[] + +def plot_single_contour(ax, x_contour, y_contour, t, color): + t_contour = t+np.zeros_like(x_contour) + ax.plot(x_contour, t_contour, y_contour, zdir='z', color=color) + + +class Kernel1D(object): + + def rescale(self): + #self.kernel /= np.abs(self.kernel).sum() + if np.abs(self.kernel.sum())!=0: + self.kernel /= np.abs(self.kernel.sum()) + + def normalize(self): +# self.kernel /= np.abs(self.kernel).sum() + self.kernel /= np.abs(self.kernel.sum()) +# self.kernel /= self.kernel.sum() + + + def __init__(self, t_range, kernel_array, threshold=0., reverse=False): + assert len(t_range) == len(kernel_array) + + kernel_array = np.array(kernel_array) + inds_to_keep = np.where(np.abs(kernel_array) > threshold) + + if reverse == True: + self.t_range = -np.array(t_range)[::-1] + + t_inds_tmp = inds_to_keep[0] + max_t_ind = t_inds_tmp.max() + reversed_t_inds = max_t_ind - t_inds_tmp + self.t_inds = reversed_t_inds - max_t_ind - 1 # Had an off by one error here should be "- 1" nhc 14 Apr '17 change made in cursor evalutiate too + + else: + self.t_range = np.array(t_range) + self.t_inds = inds_to_keep[0] + + self.kernel = kernel_array[inds_to_keep] + assert len(self.t_inds) == len(self.kernel) + + def __len__(self): + return len(self.kernel) + + def imshow(self, ax=None, show=True, save_file_name=None, ylim=None, xlim=None,color='b'): + + if ax is None: + _, ax = plt.subplots(1,1) + + t_vals = self.t_range[self.t_inds] + + ax.plot(t_vals, self.kernel, color) + ax.set_xlabel('Time (Seconds)') + + if not ylim is None: + ax.set_ylim(ylim) + + if not xlim is None: + ax.set_xlim(xlim) + else: + a,b=(t_vals[0], t_vals[-1]) + ax.set_xlim(min(a,b), max(a,b)) + + if not save_file_name is None: + ax.savefig(save_file_name, transparent=True) + + if show == True: + plt.show() + + return ax, (t_vals, self.kernel) + + def full(self, truncate_t=True): + data = np.zeros(len(self.t_range)) + data[self.t_inds] = self.kernel + + + if truncate_t == True: + ind_min = np.where(np.abs(data) > 0)[0].min() + return data[ind_min:] + else: + return data + + + + return data + +class Kernel2D(object): + + def rescale(self): + #self.kernel /= np.abs(self.kernel).sum() + if np.abs(self.kernel.sum())!=0: + self.kernel /= np.abs(self.kernel.sum()) + + def normalize(self): +# self.kernel /= np.abs(self.kernel).sum() + self.kernel /= np.abs(self.kernel.sum()) + + @classmethod + def from_dense(cls, row_range, col_range, kernel_array, threshold=0.): + col_range = np.array(col_range).copy() + row_range = np.array(row_range).copy() + kernel_array = np.array(kernel_array).copy() + inds_to_keep = np.where(np.abs(kernel_array) > threshold) + kernel = kernel_array[inds_to_keep] + if len(inds_to_keep) == 1: + col_inds, row_inds = np.array([]), np.array([]) + else: + col_inds, row_inds = inds_to_keep + + return cls(row_range, col_range, row_inds, col_inds, kernel) + + @classmethod + def copy(cls, instance): + return cls(instance.row_range.copy(), + instance.col_range.copy(), + instance.row_inds.copy(), + instance.col_inds.copy(), + instance.kernel.copy()) + + + def __init__(self, row_range, col_range, row_inds, col_inds, kernel): + + + self.col_range = np.array(col_range) + self.row_range = np.array(row_range) + self.row_inds = np.array(row_inds) + self.col_inds = np.array(col_inds) + + self.kernel = np.array(kernel) + + assert len(self.row_inds) == len(self.col_inds) + assert len(self.row_inds) == len(self.kernel) + + def __mul__(self, constant): + + new_copy = Kernel2D.copy(self) + new_copy.kernel *= constant + return new_copy + + def __add__(self, other): + + + if len(other) == 0: + return self + + try: + np.testing.assert_almost_equal(self.row_range, other.row_range) + np.testing.assert_almost_equal(self.col_range, other.col_range) + except: + raise Exception('Kernels must exist on same grid to be added') + + row_range = self.row_range.copy() + col_range = self.col_range.copy() + + kernel_dict = {} + for key, ker in zip(zip(self.row_inds, self.col_inds), self.kernel): + kernel_dict[key] = kernel_dict.setdefault(key, 0) + ker + for key, ker in zip(zip(other.row_inds, other.col_inds), other.kernel): + kernel_dict[key] = kernel_dict.setdefault(key, 0) + ker + + key_list, kernel_list = zip(*kernel_dict.items()) + row_inds_list, col_inds_list = zip(*key_list) + row_inds = np.array(row_inds_list) + col_inds = np.array(col_inds_list) + kernel = np.array(kernel_list) + + return Kernel2D(row_range, col_range, row_inds, col_inds, kernel) + + def apply_threshold(self, threshold): + + inds_to_keep = np.where(np.abs(self.kernel) > threshold) + self.row_inds = self.row_inds[inds_to_keep] + self.col_inds = self.col_inds[inds_to_keep] + self.kernel = self.kernel[inds_to_keep] + + def full(self): + data = np.zeros((len(self.row_range), len(self.col_range))) + data[self.row_inds, self.col_inds] = self.kernel + return data + + def imshow(self, ax=None, show=True, save_file_name=None, clim=None, colorbar=True): + + from mpl_toolkits.axes_grid1 import make_axes_locatable + + if ax is None: + _, ax = plt.subplots(1,1) + + if colorbar == True: + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size = "5%", pad = 0.05) + + data = self.full() + + if not clim is None: + im = ax.imshow(data, extent=(self.col_range[0], self.col_range[-1], self.row_range[0], self.row_range[-1]), origin='lower', clim=clim, interpolation='none') + else: + im = ax.imshow(data, extent=(self.col_range[0], self.col_range[-1], self.row_range[0], self.row_range[-1]), origin='lower', interpolation='none') + + if colorbar == True: + plt.colorbar(im,cax=cax) + + if not save_file_name is None: + plt.savefig(save_file_name, transparent=True) + + if show == True: + plt.show() + + return ax, data + + def __len__(self): + return len(self.kernel) + +class Kernel3D(object): + + def rescale(self): + #self.kernel /= np.abs(self.kernel).sum() + if np.abs(self.kernel.sum())!=0: + self.kernel /= np.abs(self.kernel.sum()) + + def normalize(self): + #self.kernel /= np.abs(self.kernel).sum() +# print self.kernel.sum() + self.kernel /= (self.kernel.sum())*np.sign(self.kernel.sum()) +# print self.kernel.sum() +# sys.exit() + + @classmethod + def copy(cls, instance): + return cls(instance.row_range.copy(), + instance.col_range.copy(), + instance.t_range.copy(), + instance.row_inds.copy(), + instance.col_inds.copy(), + instance.t_inds.copy(), + instance.kernel.copy()) + + def __len__(self): + return len(self.kernel) + + def __init__(self, row_range, col_range, t_range, row_inds, col_inds, t_inds, kernel): + + self.col_range = np.array(col_range) + self.row_range = np.array(row_range) + self.t_range = np.array(t_range) + self.col_inds = np.array(col_inds) + self.row_inds = np.array(row_inds) + self.t_inds = np.array(t_inds) + self.kernel = np.array(kernel) + + assert len(self.row_inds) == len(self.col_inds) + assert len(self.row_inds) == len(self.t_inds) + assert len(self.row_inds) == len(self.kernel) + + def apply_threshold(self, threshold): + + inds_to_keep = np.where(np.abs(self.kernel) > threshold) + self.row_inds = self.row_inds[inds_to_keep] + self.col_inds = self.col_inds[inds_to_keep] + self.t_inds = self.t_inds[inds_to_keep] + self.kernel = self.kernel[inds_to_keep] + + def __add__(self, other): + + + if len(other) == 0: + return self + + try: + if not (len(self.row_range) == 0 or len(other.row_range) == 0): + np.testing.assert_almost_equal(self.row_range, other.row_range) + if not (len(self.col_range) == 0 or len(other.col_range) == 0): + np.testing.assert_almost_equal(self.col_range, other.col_range) + if not (len(self.t_range) == 0 or len(other.t_range) == 0): + np.testing.assert_almost_equal(self.t_range, other.t_range) + except: + raise Exception('Kernels must exist on same grid to be added') + + if len(self.row_range) == 0: + row_range = other.row_range.copy() + else: + row_range = self.row_range.copy() + if len(self.col_range) == 0: + col_range = other.col_range.copy() + else: + col_range = self.col_range.copy() + if len(self.t_range) == 0: + t_range = other.t_range.copy() + else: + t_range = self.t_range.copy() + + kernel_dict = {} + for key, ker in zip(zip(self.row_inds, self.col_inds, self.t_inds), self.kernel): + kernel_dict[key] = kernel_dict.setdefault(key, 0) + ker + for key, ker in zip(zip(other.row_inds, other.col_inds, other.t_inds), other.kernel): + kernel_dict[key] = kernel_dict.setdefault(key, 0) + ker + + key_list, kernel_list = zip(*kernel_dict.items()) + row_inds_list, col_inds_list, t_inds_list = zip(*key_list) + row_inds = np.array(row_inds_list) + col_inds = np.array(col_inds_list) + t_inds = np.array(t_inds_list) + kernel = np.array(kernel_list) + + return Kernel3D(row_range, col_range, t_range, row_inds, col_inds, t_inds, kernel) + + def __mul__(self, constant): + + new_copy = Kernel3D.copy(self) + new_copy.kernel *= constant + return new_copy + + def t_slice(self, t): + + ind_list = find_l_r_in_t_range(self.t_range, t) + + if ind_list is None: + return None + + elif len(ind_list) == 1: + + t_ind_i = ind_list[0] + inds_i = np.where(self.t_range[self.t_inds] == self.t_range[t_ind_i]) + row_inds = self.row_inds[inds_i] + col_inds = self.col_inds[inds_i] + kernel = self.kernel[inds_i] + return Kernel2D(self.row_range, self.col_range, row_inds, col_inds, kernel) + + else: + t_ind_l, t_ind_r = ind_list + t_l, t_r = self.t_range[t_ind_l], self.t_range[t_ind_r] + + inds_l = np.where(self.t_range[self.t_inds] == self.t_range[t_ind_l]) + inds_r = np.where(self.t_range[self.t_inds] == self.t_range[t_ind_r]) + row_inds_l = self.row_inds[inds_l] + col_inds_l = self.col_inds[inds_l] + kernel_l = self.kernel[inds_l] + kl = Kernel2D(self.row_range, self.col_range, row_inds_l, col_inds_l, kernel_l) + row_inds_r = self.row_inds[inds_r] + col_inds_r = self.col_inds[inds_r] + kernel_r = self.kernel[inds_r] + kr = Kernel2D(self.row_range, self.col_range, row_inds_r, col_inds_r, kernel_r) + wa, wb = (1-(t-t_l)/(t_r-t_l)), (1-(t_r-t)/(t_r-t_l)) + + return kl*wa + kr*wb + + def full(self, truncate_t=True): + + data = np.zeros((len(self.t_range), len(self.row_range), len(self.col_range))) + data[self.t_inds, self.row_inds, self.col_inds] = self.kernel + + if truncate_t == True: + ind_max = np.where(np.abs(data) > 0)[0].min() + return data[ind_max:, :, :] + else: + return data + + + # if truncate_t == True: + # ind_min = np.where(np.abs(data) > 0)[0].min() + # return data[ind_min:] + # else: + # return data + + def imshow(self, ax=None, t_range=None, cmap=cm.bwr, N=10, show=True, save_file_name=None, kvals=None): + + if ax is None: + fig = plt.figure() + ax = fig.gca(projection='3d') + + if t_range is None: + t_range = self.t_range + + slice_list_sparse = [self.t_slice(t) for t in t_range] + slice_list = [] + slice_t_list = [] + for curr_slice, curr_t in zip(slice_list_sparse, t_range): + if not curr_slice is None: + slice_list.append(curr_slice.full()) + slice_t_list.append(curr_t) + all_slice_max = max(map(np.max, slice_list)) + all_slice_min = min(map(np.min, slice_list)) + upper_bound = max(np.abs(all_slice_max), np.abs(all_slice_min)) + lower_bound = -upper_bound + norm = mpl.colors.Normalize(vmin=lower_bound, vmax=upper_bound) + color_mapper = cm.ScalarMappable(norm=norm, cmap=cmap).to_rgba + + if kvals is None: + kvals = np.linspace(lower_bound, upper_bound, N) + + X, Y = np.meshgrid(self.row_range, self.col_range) + + contour_dict = {} + for kval in kvals: + for t_val, curr_slice in zip(slice_t_list, slice_list): + x_contour, y_contour = get_contour(Y, X, curr_slice.T, kval) + contour_dict[kval, t_val] = x_contour, y_contour + color = color_mapper(kval) + color = color[0], color[1], color[2], np.abs(kval)/upper_bound + plot_single_contour(ax, x_contour, y_contour, t_val, color) + + ax.set_zlim(self.row_range[0], self.row_range[-1]) + ax.set_ylim(self.t_range[0], self.t_range[-1]) + ax.set_xlim(self.col_range[0], self.col_range[-1]) + + if not save_file_name is None: + plt.savefig(save_file_name, transparent=True) + + if show == True: + plt.show() + + return ax, contour_dict + +def merge_spatial_temporal(spatial_kernel, temporal_kernel, threshold=0): + + t_range = temporal_kernel.t_range + + spatiotemporal_kernel = np.ones(( len(temporal_kernel), len(spatial_kernel))) + spatiotemporal_kernel *= spatial_kernel.kernel[None, :] + spatiotemporal_kernel *= temporal_kernel.kernel[:,None] + spatiotemporal_kernel = spatiotemporal_kernel.reshape((np.prod(spatiotemporal_kernel.shape))) + + spatial_coord_array = np.empty((len(spatial_kernel),2)) + spatial_coord_array[:,0] = spatial_kernel.col_inds + spatial_coord_array[:,1] = spatial_kernel.row_inds + + spatiiotemporal_coord_array = np.zeros((len(spatial_kernel)*len(temporal_kernel),3)) + spatiiotemporal_coord_array[:,0:2] = np.kron(np.ones((len(temporal_kernel),1)),spatial_coord_array) + spatiiotemporal_coord_array[:,2] = np.kron(temporal_kernel.t_inds, np.ones(len(spatial_kernel))) + + col_inds, row_inds, t_inds = map(lambda x:x.astype(np.int),spatiiotemporal_coord_array.T) + kernel = Kernel3D(spatial_kernel.row_range, spatial_kernel.col_range, t_range, row_inds, col_inds, t_inds, spatiotemporal_kernel) + kernel.apply_threshold(threshold) + + return kernel + + + +# Candidate for print +# for ri, ci, ti, k in zip(kernel.row_inds, kernel.col_inds, kernel.t_inds, kernel.kernel): +# print ri, ci, ti, k diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lattice_unit_constructor.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lattice_unit_constructor.py new file mode 100644 index 0000000..4de580d --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lattice_unit_constructor.py @@ -0,0 +1,254 @@ +import scipy.io as sio +import os +import matplotlib.pyplot as plt +import isee_engine.nwb as nwb +from linearfilter import SpatioTemporalFilter +import numpy as np +from spatialfilter import GaussianSpatialFilter +from transferfunction import ScalarTransferFunction +from temporalfilter import TemporalFilterCosineBump +from cursor import LNUnitCursor, MultiLNUnitCursor +from movie import Movie +from lgnmodel1 import LGNModel, heat_plot +from cellmodel import LGNOnCell, LGNOffCell,LGNOnOffCell,TwoSubfieldLinearCell +from transferfunction import MultiTransferFunction, ScalarTransferFunction +from lnunit import LNUnit, MultiLNUnit +from sympy.abc import x as symbolic_x +from sympy.abc import y as symbolic_y +from kernel import Kernel3D +from movie import Movie, FullFieldFlashMovie +import itertools +import scipy.stats as sps +from make_cell_list import multi_cell_random_generator, make_single_unit_cell_list, make_on_off_cell_list +#from lgnmodel.make_cell_list import two_unit_cell_config +#from make_cell_list import single_unit_cell_config + +def make_lattice_unit(lattice_unit_center=None): + cell_list = [] + tON_cell_list = make_tON_cell_list(lattice_unit_center) + tOFF_cell_list = make_tOFF_cell_list(lattice_unit_center) + sON_cell_list = make_sON_cell_list(lattice_unit_center) + sOFF_cell_list = make_sOFF_cell_list(lattice_unit_center) + overlap_onoff_cell_list = make_overlapping_onoff_cell_list(lattice_unit_center) + separate_onoff_cell_list = make_separate_onoff_cell_list(lattice_unit_center) + + cell_list = tON_cell_list + tOFF_cell_list + sON_cell_list + sOFF_cell_list + overlap_onoff_cell_list + separate_onoff_cell_list + + return cell_list + + +def make_tON_cell_list(lattice_unit_center): + tON_cell_list = [] + + single_unit_cell_config = {} + single_unit_cell_config['lattice_unit_center']=lattice_unit_center + single_unit_cell_config['width'] = 5. + sz = [3,6,9] + ncells = [5,3,2] + amp_dist = sps.rv_discrete(values=([20,25], [.5,.5])) +# kpeaks_dist = sps.multivariate_normal(mean=[40., 80.], cov=[[5.0, 0], [0, 5]]) +# wts = (.4,-.2) + kpeaks_dist = sps.multivariate_normal(mean=[15., 35.], cov=[[5.0, 0], [0, 5]]) + wts = (4.,-2.5) + delays = (0.,0.) + single_unit_cell_config['amplitude'] = amp_dist + single_unit_cell_config['kpeaks'] = kpeaks_dist + single_unit_cell_config['weights'] = wts + single_unit_cell_config['delays'] = delays + for num_cells, sig in zip(ncells,sz): + single_unit_cell_config['number_of_cells'] = num_cells + single_unit_cell_config['sigma'] = (sig,sig) +# print single_unit_cell_config + tON_cell_list += multi_cell_random_generator(make_single_unit_cell_list, **single_unit_cell_config) + + #print len(tON_cell_list) + return tON_cell_list + +def make_tOFF_cell_list(lattice_unit_center): + tOFF_cell_list = [] + + single_unit_cell_config = {} + single_unit_cell_config['lattice_unit_center']=lattice_unit_center + single_unit_cell_config['width'] = 5. + sz = [3,6,9] + ncells = [10,5,5] + amp_dist = sps.rv_discrete(values=([-20,-25], [.5,.5])) +# kpeaks_dist = sps.multivariate_normal(mean=[40., 80.], cov=[[5.0, 0], [0, 5]]) +# wts = (.4,-.2) + kpeaks_dist = sps.multivariate_normal(mean=[15., 35.], cov=[[5.0, 0], [0, 5]]) + wts = (4.,-2.5) + delays = (0.,0.) + single_unit_cell_config['amplitude'] = amp_dist + single_unit_cell_config['kpeaks'] = kpeaks_dist + single_unit_cell_config['weights'] = wts + single_unit_cell_config['delays'] = delays + for num_cells, sig in zip(ncells,sz): + single_unit_cell_config['number_of_cells'] = num_cells + single_unit_cell_config['sigma'] = (sig,sig) + tOFF_cell_list += multi_cell_random_generator(make_single_unit_cell_list, **single_unit_cell_config) + + #print len(tOFF_cell_list) + return tOFF_cell_list + +def make_sON_cell_list(lattice_unit_center): + sON_cell_list = [] + + single_unit_cell_config = {} + single_unit_cell_config['lattice_unit_center']=lattice_unit_center + single_unit_cell_config['width'] = 5. + sz = [3,6,9] + ncells = [5,3,2] + amp_dist = sps.rv_discrete(values=([20,25], [.5,.5])) +# kpeaks_dist = sps.multivariate_normal(mean=[100., 160.], cov=[[5.0, 0], [0, 5]]) +# wts = (.4,-.1) + kpeaks_dist = sps.multivariate_normal(mean=[80., 120.], cov=[[5.0, 0], [0, 5]]) + wts = (4.,-.85) + delays = (0.,0.) + single_unit_cell_config['amplitude'] = amp_dist + single_unit_cell_config['kpeaks'] = kpeaks_dist + single_unit_cell_config['weights'] = wts + single_unit_cell_config['delays'] = delays + for num_cells, sig in zip(ncells,sz): + single_unit_cell_config['number_of_cells'] = num_cells + single_unit_cell_config['sigma'] = (sig,sig) + sON_cell_list += multi_cell_random_generator(make_single_unit_cell_list, **single_unit_cell_config) + + #print len(sON_cell_list) + return sON_cell_list + +def make_sOFF_cell_list(lattice_unit_center): + sOFF_cell_list = [] + + single_unit_cell_config = {} + single_unit_cell_config['lattice_unit_center']=lattice_unit_center + single_unit_cell_config['width'] = 5. + sz = [3,6,9] + ncells = [10,5,5] + amp_dist = sps.rv_discrete(values=([-20,-25], [.5,.5])) +# kpeaks_dist = sps.multivariate_normal(mean=[100., 160.], cov=[[5.0, 0], [0, 5]]) + kpeaks_dist = sps.multivariate_normal(mean=[80., 120.], cov=[[5.0, 0], [0, 5]]) +# wts = (.4,-.1) + wts = (4.,-.85) + delays = (0.,0.) + single_unit_cell_config['amplitude'] = amp_dist + single_unit_cell_config['kpeaks'] = kpeaks_dist + single_unit_cell_config['weights'] = wts + single_unit_cell_config['delays'] = delays + for num_cells, sig in zip(ncells,sz): + single_unit_cell_config['number_of_cells'] = num_cells + single_unit_cell_config['sigma'] = (sig,sig) + sOFF_cell_list += multi_cell_random_generator(make_single_unit_cell_list, **single_unit_cell_config) + + #print len(sOFF_cell_list) + return sOFF_cell_list + +def make_overlapping_onoff_cell_list(lattice_unit_center): + overlap_onoff_cell_list = [] + + two_unit_cell_config = {} + two_unit_cell_config['lattice_unit_center']=lattice_unit_center + two_unit_cell_config['width']=5. + + ncells = 4 + sz = 9 + ang_dist = sps.rv_discrete(values=(np.arange(0,180,45), 1./ncells*np.ones(ncells))) + amp_on_dist = sps.rv_discrete(values=([20,25], [.5,.5])) + amp_off_dist = sps.rv_discrete(values=([-20,-25], [.5,.5])) +# kpeak_on_dist = sps.multivariate_normal(mean=[40., 80.], cov=[[5.0, 0], [0, 5]]) +# kpeak_off_dist = sps.multivariate_normal(mean=[50., 90.], cov=[[5.0, 0], [0, 5]]) +# wts_on = wts_off = (.4,-.2) + kpeak_on_dist = sps.multivariate_normal(mean=[15., 35.], cov=[[5.0, 0], [0, 5]]) + kpeak_off_dist = sps.multivariate_normal(mean=[20., 40.], cov=[[5.0, 0], [0, 5]]) + wts_on = wts_off = (4.,-2.5) + delays_on = delays_off = (0.,0.) + subfield_sep = 2. + + two_unit_cell_config['number_of_cells'] = ncells + two_unit_cell_config['ang'] = ang_dist + two_unit_cell_config['amplitude_on'] = amp_on_dist + two_unit_cell_config['amplitude_off'] = amp_off_dist + two_unit_cell_config['kpeaks_on'] = kpeak_on_dist + two_unit_cell_config['kpeaks_off'] = kpeak_off_dist + two_unit_cell_config['weights_on'] = wts_on + two_unit_cell_config['weights_off'] = wts_off + two_unit_cell_config['sigma_on'] = (sz,sz) + two_unit_cell_config['sigma_off'] = (sz,sz) + two_unit_cell_config['subfield_separation'] = subfield_sep + two_unit_cell_config['dominant_subunit']='on' + two_unit_cell_config['delays_on']=delays_on + two_unit_cell_config['delays_off']=delays_off + + overlap_onoff_cell_list += multi_cell_random_generator(make_on_off_cell_list, **two_unit_cell_config) + + #print len(overlap_onoff_cell_list) + return overlap_onoff_cell_list + +def make_separate_onoff_cell_list(lattice_unit_center): + separate_onoff_cell_list = [] + + two_unit_cell_config = {} + two_unit_cell_config['lattice_unit_center']=lattice_unit_center + two_unit_cell_config['width']=5. + + ncells = 8 + sz = 6 + ang_dist = np.arange(0,360,45) + subfield_sep = 4. + +# kpeak_dom_dist = sps.multivariate_normal(mean=[40., 80.], cov=[[5.0, 0], [0, 5]]) +# kpeak_nondom_dist = sps.multivariate_normal(mean=[100., 160.], cov=[[5.0, 0], [0, 5]]) +# wts_dom = (.4,-.2) +# wts_nondom = (.4,-.1) + + kpeak_dom_dist = sps.multivariate_normal(mean=[15., 35.], cov=[[5.0, 0], [0, 5]]) + kpeak_nondom_dist = sps.multivariate_normal(mean=[80., 120.], cov=[[5.0, 0], [0, 5]]) + wts_dom = (4.,-2.5) + wts_nondom = (4,-.85) + delays_dom = delays_nondom = (0.,0.) + + two_unit_cell_config['number_of_cells'] = ncells + two_unit_cell_config['ang'] = ang_dist + two_unit_cell_config['sigma_on'] = (sz,sz) + two_unit_cell_config['sigma_off'] = (sz,sz) + two_unit_cell_config['subfield_separation'] = subfield_sep + + #On-dominant + dom_subunit = 'on' + if dom_subunit=='on': + two_unit_cell_config['dominant_subunit'] = dom_subunit + amp_dom_dist = sps.rv_discrete(values=([20,25], [.5,.5])) + amp_nondom_dist = sps.rv_discrete(values=([-10,-15], [.5,.5])) + two_unit_cell_config['amplitude_on'] = amp_dom_dist + two_unit_cell_config['amplitude_off'] = amp_nondom_dist + two_unit_cell_config['kpeaks_on'] = kpeak_dom_dist + two_unit_cell_config['kpeaks_off'] = kpeak_nondom_dist + two_unit_cell_config['weights_on'] = wts_dom + two_unit_cell_config['weights_off'] = wts_nondom + two_unit_cell_config['delays_on'] = delays_dom + two_unit_cell_config['delays_off'] = delays_nondom + separate_onoff_cell_list += multi_cell_random_generator(make_on_off_cell_list, **two_unit_cell_config) + + #Off-dominant + dom_subunit = 'off' + if dom_subunit=='off': + two_unit_cell_config['dominant_subunit'] = dom_subunit + amp_dom_dist = sps.rv_discrete(values=([-20,-25], [.5,.5])) + amp_nondom_dist = sps.rv_discrete(values=([10,15], [.5,.5])) + two_unit_cell_config['amplitude_off'] = amp_dom_dist + two_unit_cell_config['amplitude_on'] = amp_nondom_dist + two_unit_cell_config['kpeaks_off'] = kpeak_dom_dist + two_unit_cell_config['kpeaks_on'] = kpeak_nondom_dist + two_unit_cell_config['weights_off'] = wts_dom + two_unit_cell_config['weights_on'] = wts_nondom + two_unit_cell_config['delays_off'] = delays_dom + two_unit_cell_config['delays_on'] = delays_nondom + separate_onoff_cell_list += multi_cell_random_generator(make_on_off_cell_list, **two_unit_cell_config) + + #print len(separate_onoff_cell_list) + return separate_onoff_cell_list + +if __name__ == "__main__": + lattice_unit_center = (40,30) + lattice_cell_list = make_lattice_unit(lattice_unit_center) + print(len(lattice_cell_list)) + \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lgnmodel1.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lgnmodel1.py new file mode 100644 index 0000000..1b04710 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lgnmodel1.py @@ -0,0 +1,87 @@ +import numpy as np +import matplotlib.pyplot as plt + +def line_plot(evaluate_result, ax=None, show=True, save_file_name=None, xlabel=None, plotstyle=None): + + if ax is None: + _, ax = plt.subplots(1,1) + + if not plotstyle is None: + for ((t_range, y_vals), curr_plotstyle) in zip(evaluate_result, plotstyle): + ax.plot(t_range, y_vals, curr_plotstyle) + else: + for t_range, y_vals in evaluate_result: + ax.plot(t_range, y_vals) + + if xlabel is None: + ax.set_xlabel('Time (Seconds)') + else: + ax.set_xlabel(xlabel) + + if xlabel is None: + ax.set_xlabel('Firing Rate (Hz)') + else: + ax.set_xlabel(xlabel) + + if not save_file_name is None: + plt.savefig(save_file_name, transparent=True) + + + + + if show == True: + plt.show() + +def heat_plot(evaluate_result, ax=None, show=True, save_file_name=None, colorbar=True, **kwargs): + + if ax is None: + _, ax = plt.subplots(1,1) + + data = np.empty((len(evaluate_result), len(evaluate_result[0][0]))) + for ii, (t_vals, y_vals) in enumerate(evaluate_result): + data[ii,:] = y_vals + + cax = ax.pcolor(t_vals, np.arange(len(evaluate_result)), data, **kwargs) + ax.set_ylim([0,len(evaluate_result)-1]) + ax.set_xlim([t_vals[0], t_vals[-1]]) + ax.set_ylabel('Neuron id') + ax.set_xlabel('Time (Seconds)') + + if colorbar == True: + plt.colorbar(cax) + + if not save_file_name is None: + plt.savefig(save_file_name, transparent=True) + + if show == True: + plt.show() + + + + +class LGNModel(object): + + def __init__(self, cell_list): + self.cell_list = cell_list + + def evaluate(self, movie, **kwargs): + return [cell.evaluate(movie, **kwargs) for cell in self.cell_list] + +# def plot(self): +# if show == True: +# plt.show() + + +# show = kwargs.pop('show', False) +# data = [cell.evaluate_movie(movie, **kwargs) for cell in self.cell_list] +# t_list, y_list, kernel_list = zip(*data) + +# if show == True: +# for y in y_list: +# plt.plot(t_list[0], y) +# plt.show() +# +# return t_list[0], y_list, kernel_list + + def __len__(self): + return len(self.cell_list) \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/linearfilter.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/linearfilter.py new file mode 100644 index 0000000..af7fef2 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/linearfilter.py @@ -0,0 +1,128 @@ +import numpy as np +from .kernel import Kernel3D +import matplotlib.pyplot as plt + +class SpatioTemporalFilter(object): + + def __init__(self, spatial_filter, temporal_filter, amplitude=1.): + + self.spatial_filter = spatial_filter + self.temporal_filter = temporal_filter + self.amplitude = amplitude + + def get_spatiotemporal_kernel(self, row_range, col_range, t_range=None, threshold=0, reverse=False): + + spatial_kernel = self.spatial_filter.get_kernel(row_range, col_range, threshold=0) + temporal_kernel = self.temporal_filter.get_kernel(t_range=t_range, threshold=0, reverse=reverse) + + t_range = temporal_kernel.t_range + + spatiotemporal_kernel = np.ones(( len(temporal_kernel), len(spatial_kernel))) + spatiotemporal_kernel *= spatial_kernel.kernel[None, :] + + spatiotemporal_kernel *= temporal_kernel.kernel[:,None] + spatiotemporal_kernel = spatiotemporal_kernel.reshape((np.prod(spatiotemporal_kernel.shape))) + + spatial_coord_array = np.empty((len(spatial_kernel),2)) + spatial_coord_array[:,0] = spatial_kernel.col_inds + spatial_coord_array[:,1] = spatial_kernel.row_inds + + spatiiotemporal_coord_array = np.zeros((len(spatial_kernel)*len(temporal_kernel),3)) + spatiiotemporal_coord_array[:,0:2] = np.kron(np.ones((len(temporal_kernel),1)),spatial_coord_array) + spatiiotemporal_coord_array[:,2] = np.kron(temporal_kernel.t_inds, np.ones(len(spatial_kernel))) + + col_inds, row_inds, t_inds = map(lambda x:x.astype(np.int),spatiiotemporal_coord_array.T) + kernel = Kernel3D(spatial_kernel.row_range, spatial_kernel.col_range, t_range, row_inds, col_inds, t_inds, spatiotemporal_kernel) + kernel.apply_threshold(threshold) + + + kernel.kernel *= self.amplitude + + + return kernel + + def t_slice(self, t, *args, **kwargs): + + k = self.get_spatiotemporal_kernel(*args, **kwargs) + return k.t_slice(t) + + def show_temporal_filter(self, *args, **kwargs): + + self.temporal_filter.imshow(*args, **kwargs) + + def show_spatial_filter(self, *args, **kwargs): + + self.spatial_filter.imshow(*args, **kwargs) + + def to_dict(self): + + return {'class':(__name__, self.__class__.__name__), + 'spatial_filter':self.spatial_filter.to_dict(), + 'temporal_filter':self.temporal_filter.to_dict(), + 'amplitude':self.amplitude} + +# class OnOffSpatioTemporalFilter(SpatioTemporalFilter): +# +# def __init__(self, on_spatiotemporal_filter, off_spatiotemporal_filter): +# +# self.on_spatiotemporal_filter = on_spatiotemporal_filter +# self.off_spatiotemporal_filter = off_spatiotemporal_filter +# +# def get_spatiotemporal_kernel(self, col_range, row_range, t_range=None, threshold=0, reverse=False): +# +# on_kernel = self.on_spatiotemporal_filter.get_spatiotemporal_kernel(col_range, row_range, t_range, threshold, reverse) +# off_kernel = self.off_spatiotemporal_filter.get_spatiotemporal_kernel(col_range, row_range, t_range, threshold, reverse) +# +# return on_kernel + off_kernel*(-1) +# +# def to_dict(self): +# +# return {'class':(__name__, self.__class__.__name__), +# 'on_filter':self.on_spatiotemporal_filter.to_dict(), +# 'off_filter':self.off_spatiotemporal_filter.to_dict()} +# +# class TwoSubfieldLinearFilter(OnOffSpatioTemporalFilter): +# +# def __init__(self, dominant_spatiotemporal_filter, nondominant_spatiotemporal_filter, subfield_separation=10, onoff_axis_angle=45, dominant_subfield_location=(30,40)): +# +# self.subfield_separation = subfield_separation +# self.onoff_axis_angle = onoff_axis_angle +# self.dominant_subfield_location = dominant_subfield_location +# self.dominant_spatiotemporal_filter = dominant_spatiotemporal_filter +# self.nondominant_spatiotemporal_filter = nondominant_spatiotemporal_filter +# +# dom_amp = dominant_spatiotemporal_filter.spatial_filter.amplitude +# nondom_amp = nondominant_spatiotemporal_filter.spatial_filter.amplitude +# if dom_amp < 0 and nondom_amp > 0: +# super(TwoSubfieldLinearFilter, self).__init__(self.nondominant_spatiotemporal_filter, self.dominant_spatiotemporal_filter) +# elif dom_amp > 0 and nondom_amp < 0: +# super(TwoSubfieldLinearFilter, self).__init__(self.dominant_spatiotemporal_filter, self.nondominant_spatiotemporal_filter) +# else: +# raise ValueError('Subfields are not of opposite polarity') +# +# self.dominant_spatiotemporal_filter.spatial_filter.translate = self.dominant_subfield_location +# hor_offset = np.cos(self.onoff_axis_angle*np.pi/180.)*self.subfield_separation + self.dominant_subfield_location[0] +# vert_offset = np.sin(self.onoff_axis_angle*np.pi/180.)*self.subfield_separation+ self.dominant_subfield_location[1] +# rel_translation = (hor_offset,vert_offset) +# self.nondominant_spatiotemporal_filter.spatial_filter.translate = rel_translation +# self.nondominant_spatiotemporal_filter.spatial_filter.origin=self.dominant_spatiotemporal_filter.spatial_filter.origin +# +# +# def to_dict(self): +# +# raise NotImplementedError +# + + + + + + + + + + + + + + diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lnunit.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lnunit.py new file mode 100644 index 0000000..ebc9952 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/lnunit.py @@ -0,0 +1,380 @@ +import os +import itertools +import matplotlib.pyplot as plt +import numpy as np +from . import utilities as util +import importlib +from .kernel import Kernel2D, Kernel3D +from .linearfilter import SpatioTemporalFilter +import json +from .spatialfilter import GaussianSpatialFilter +from .transferfunction import ScalarTransferFunction +from .temporalfilter import TemporalFilterCosineBump +from .cursor import LNUnitCursor, MultiLNUnitCursor, MultiLNUnitMultiMovieCursor, SeparableLNUnitCursor, SeparableMultiLNUnitCursor +from .movie import Movie +from .lgnmodel1 import LGNModel, heat_plot +from .transferfunction import MultiTransferFunction, ScalarTransferFunction + + +class LNUnit(object): + + def __init__(self, linear_filter, transfer_function, amplitude=1.): + + self.linear_filter = linear_filter + self.transfer_function = transfer_function + self.amplitude = amplitude + + def evaluate(self, movie, **kwargs): + return self.get_cursor(movie, separable=kwargs.pop('separable', False)).evaluate(**kwargs) + + def get_spatiotemporal_kernel(self, *args, **kwargs): + return self.linear_filter.get_spatiotemporal_kernel(*args, **kwargs) + + def get_cursor(self, movie, threshold=0, separable = False): + if separable: + return SeparableLNUnitCursor(self, movie) + else: + return LNUnitCursor(self, movie, threshold=threshold) + + def show_temporal_filter(self, *args, **kwargs): + self.linear_filter.show_temporal_filter(*args, **kwargs) + + def show_spatial_filter(self, *args, **kwargs): + self.linear_filter.show_spatial_filter(*args, **kwargs) + + def to_dict(self): + return {'class':(__name__, self.__class__.__name__), + 'linear_filter':self.linear_filter.to_dict(), + 'transfer_function':self.transfer_function.to_dict()} + +class MultiLNUnit(object): + + def __init__(self, lnunit_list, transfer_function): + + self.lnunit_list = lnunit_list + self.transfer_function = transfer_function + + def get_spatiotemporal_kernel(self, *args, **kwargs): + + k = Kernel3D([],[],[],[],[],[],[]) + for unit in self.lnunit_list: + k = k+unit.get_spatiotemporal_kernel(*args, **kwargs) + + return k + + def show_temporal_filter(self, *args, **kwargs): + + ax = kwargs.pop('ax', None) + show = kwargs.pop('show', None) + save_file_name = kwargs.pop('save_file_name', None) + + + if ax is None: + _, ax = plt.subplots(1,1) + + kwargs.update({'ax':ax, 'show':False, 'save_file_name':None}) + for unit in self.lnunit_list: + if unit.linear_filter.amplitude < 0: + color='b' + else: + color='r' + unit.linear_filter.show_temporal_filter(color=color, **kwargs) + + if not save_file_name is None: + plt.savefig(save_file_name, transparent=True) + + if show == True: + plt.show() + + return ax + + def show_spatial_filter(self, *args, **kwargs): + + ax = kwargs.pop('ax', None) + show = kwargs.pop('show', True) + save_file_name = kwargs.pop('save_file_name', None) + colorbar = kwargs.pop('colorbar', True) + + k = Kernel2D(args[0],args[1],[],[],[]) + for lnunit in self.lnunit_list: + k = k + lnunit.linear_filter.spatial_filter.get_kernel(*args, **kwargs) + k.imshow(ax=ax, show=show, save_file_name=save_file_name, colorbar=colorbar) + + def get_cursor(self, *args, **kwargs): + + threshold = kwargs.get('threshold', 0.) + separable = kwargs.get('separable', False) + + if len(args) == 1: + movie = args[0] + if separable: + return SeparableMultiLNUnitCursor(self, movie) + else: + return MultiLNUnitCursor(self, movie, threshold=threshold) + elif len(args) > 1: + movie_list = args + if separable: + raise NotImplementedError + else: + return MultiLNUnitMultiMovieCursor(self, movie_list, threshold=threshold) + else: + assert ValueError + + + def evaluate(self, movie, **kwargs): + seperable = kwargs.pop('separable', False) + return self.get_cursor(movie, separable=seperable).evaluate(**kwargs) + +from sympy.abc import x, y + +if __name__ == "__main__": + + movie_file = '/data/mat/iSee_temp_shared/movies/TouchOfEvil.npy' + m_data = np.load(movie_file, 'r') + m = Movie(m_data[1000:], frame_rate=30.) + + # Create second cell: + transfer_function = ScalarTransferFunction('s') + temporal_filter = TemporalFilterCosineBump((.4,-.3), (20,60)) + cell_list = [] + for xi in np.linspace(0,m.data.shape[2], 5): + for yi in np.linspace(0,m.data.shape[1], 5): + spatial_filter_on = GaussianSpatialFilter(sigma=(2,2), origin=(0,0), translate=(xi, yi)) + on_linear_filter = SpatioTemporalFilter(spatial_filter_on, temporal_filter, amplitude=20) + on_lnunit = LNUnit(on_linear_filter, transfer_function) + spatial_filter_off = GaussianSpatialFilter(sigma=(4,4), origin=(0,0), translate=(xi, yi)) + off_linear_filter = SpatioTemporalFilter(spatial_filter_off, temporal_filter, amplitude=-20) + off_lnunit = LNUnit(off_linear_filter, transfer_function) + + multi_transfer_function = MultiTransferFunction((x, y), 'x+y') + + multi_unit = MultiLNUnit([on_lnunit, off_lnunit], multi_transfer_function) + cell_list.append(multi_unit) + + lgn = LGNModel(cell_list) #Here include a list of all cells + y = lgn.evaluate(m, downsample=10) #Does the filtering + non-linearity on movie object m + heat_plot(y, interpolation='none', colorbar=False) + + + + + +# +# def imshow(self, ii, image_shape, fps, ax=None, show=True, relative_spatial_location=(0,0)): +# +# if ax is None: +# _, ax = plt.subplots(1,1) +# +# curr_kernel = self.get_spatio_temporal_kernel(image_shape, fps, relative_spatial_location=relative_spatial_location) +# +# cax = curr_kernel.imshow(ii, ax=ax, show=False) +# +# if show == True: +# plt.show() +# +# return ax +# +# +# class OnOffCellModel(CellModel): +# +# def __init__(self, dc_offset=0, on_subfield=None, off_subfield=None, on_weight = 1, off_weight = -1, t_max=None): +# +# super(self.__class__, self).__init__(dc_offset, t_max) +# +# if isinstance(on_subfield, dict): +# curr_module, curr_class = on_subfield.pop('class') +# self.on_subfield = getattr(importlib.import_module(curr_module), curr_class)(**on_subfield) +# else: +# self.on_subfield = on_subfield +# +# super(self.__class__, self).add_subfield(on_subfield, on_weight) +# +# if isinstance(off_subfield, dict): +# curr_module, curr_class = off_subfield.pop('class') +# self.off_subfield = getattr(importlib.import_module(curr_module), curr_class)(**off_subfield) +# else: +# self.off_subfield = off_subfield +# +# super(self.__class__, self).add_subfield(off_subfield, off_weight) +# +# +# def to_dict(self): +# +# return {'dc_offset':self.dc_offset, +# 'on_subfield':self.on_subfield.to_dict(), +# 'off_subfield':self.off_subfield.to_dict(), +# 't_max':self.t_max, +# 'class':(__name__, self.__class__.__name__)} +# +# class SingleSubfieldCellModel(CellModel): +# +# def __init__(self, subfield, weight = 1, dc_offset=0, t_max=None): +# +# super(SingleSubfieldCellModel, self).__init__(dc_offset, t_max) +# +# if isinstance(subfield, dict): +# curr_module, curr_class = subfield.pop('class') +# subfield = getattr(importlib.import_module(curr_module), curr_class)(**subfield) +# +# super(self.__class__, self).add_subfield(subfield, weight) +# +# def to_dict(self): +# +# assert len(self.subfield_list) == 1 +# subfield = self.subfield_list[0] +# weight = self.subfield_weight_dict[subfield] +# +# return {'dc_offset':self.dc_offset, +# 'subfield':subfield.to_dict(), +# 'weight':weight, +# 't_max':self.t_max, +# 'class':(__name__, self.__class__.__name__)} +# +# class OnCellModel(SingleSubfieldCellModel): +# +# def __init__(self, on_subfield, weight = 1, dc_offset=0 , t_max=None): +# assert weight > 0 +# super(OnCellModel, self).__init__(on_subfield, weight, dc_offset, t_max) +# +# def to_dict(self): +# data_dict = super(OnCellModel, self).to_dict() +# data_dict['on_subfield'] = data_dict.pop('subfield') +# return data_dict +# +# class OffCellModel(SingleSubfieldCellModel): +# +# def __init__(self, on_subfield, weight = -1, dc_offset=0 , t_max=None): +# assert weight < 0 +# super(OffCellModel, self).__init__(on_subfield, weight, dc_offset, t_max) +# +# def to_dict(self): +# data_dict = super(OffCellModel, self).to_dict() +# data_dict['off_subfield'] = data_dict.pop('subfield') +# return data_dict + + +# class OffCellModel(CellModel): +# +# def __init__(self, off_subfield, dc_offset=0, off_weight = 1, t_max=None): +# +# assert off_weight < 0. +# self.weight = off_weight +# +# +# +# +# super(self.__class__, self).__init__(dc_offset, t_max) +# +# if isinstance(on_subfield, dict): +# curr_module, curr_class = on_subfield.pop('class') +# self.subfield = getattr(importlib.import_module(curr_module), curr_class)(**on_subfield) +# else: +# self.subfield = on_subfield +# +# super(self.__class__, self).add_subfield(self.subfield, self.weight) +# +# def to_dict(self): +# +# return {'dc_offset':self.dc_offset, +# 'on_subfield':self.subfield.to_dict(), +# 'on_weight':self.weight, +# 't_max':self.t_max, +# 'class':(__name__, self.__class__.__name__)} + + + + + + +# if __name__ == "__main__": +# +# t = np.arange(0,.5,.001) +# example_movie = movie.Movie(file_name=os.path.join(isee_engine.movie_directory, 'TouchOfEvil.npy'), frame_rate=30.1, memmap=True) +# +# temporal_filter_on = TemporalFilterExponential(weight=1, tau=.05) +# on_subfield = Subfield(scale=(5,15), weight=.5, rotation=30, temporal_filter=temporal_filter_on, translation=(0,0)) +# +# temporal_filter_off = TemporalFilterExponential(weight=2, tau=.01) +# off_subfield = Subfield(scale=(5,15), weight=.5, rotation=-30, temporal_filter=temporal_filter_off) +# +# cell = OnOffCellModel(on_subfield=on_subfield, off_subfield=off_subfield, dc_offset=0., t_max=.5) +# curr_kernel = cell.get_spatio_temporal_kernel((100,150), 30.1) +# curr_kernel.imshow(0) +# +# print cell.to_dict() + + + +# f = cell.get_spatio_temporal_filter(example_movie.movie_data.shape[1:], t,threshold=.5) +# print len(f.t_ind_list) +# +# + +# for ii in range(example_movie.number_of_frames-curr_filter.t_max): +# print ii, example_movie.number_of_frames, curr_filter.map(example_movie, ii) + + +# off_subfield = Subfield(scale=(15,15), weight=.2, translation=(30,30)) + + +# +# curr_filter = cell.get_spatio_temporal_filter((100,150)) +# + +# +# # print touch_of_evil(40.41, mask=m) +# print curr_filter.t_max +# for ii in range(example_movie.number_of_frames-curr_filter.t_max): +# print ii, example_movie.number_of_frames, curr_filter.map(example_movie, ii) + +# cell.visualize_spatial_filter((100,150)) +# show_volume(spatio_temporal_filter, vmin=spatio_temporal_filter.min(), vmax=spatio_temporal_filter.max()) + + + +# def get_spatial_filter(self, image_shape, relative_spatial_location=(0,0), relative_threshold=default_relative_threshold): +# +# # Initialize: +# translation_matrix = util.get_translation_matrix(relative_spatial_location) +# +# # On-subunit: +# on_filter_pre_spatial = self.on_subfield.get_spatial_filter(image_shape) +# on_filter_spatial = util.apply_transformation_matrix(on_filter_pre_spatial, translation_matrix) +# +# # Off-subunit: +# off_filter_pre_spatial = self.off_subfield.get_spatial_filter(image_shape) +# off_filter_spatial = util.apply_transformation_matrix(off_filter_pre_spatial, translation_matrix) +# +# spatial_filter = on_filter_spatial - off_filter_spatial +# +# tmp = np.abs(spatial_filter) +# spatial_filter[np.where(tmp/tmp.max() < relative_threshold )] = 0 +# +# return spatial_filter + +# kernel = float(self.dc_offset)/len(nonzero_ind_tuple[0])+spatio_temporal_filter[nonzero_ind_tuple] + +# def rectifying_filter_factory(kernel, movie, dc_offset=0): +# +# def rectifying_filter(t): +# +# fi = movie.frame_rate*float(t) +# fim, fiM = np.floor(fi), np.ceil(fi) +# +# print t, fim, fiM +# +# try: +# s1 = (movie.movie_data[int(fim)+kernel.t_ind_list, kernel.row_ind_list, kernel.col_ind_list]*kernel.kernel).sum() +# s2 = (movie.movie_data[int(fiM)+kernel.t_ind_list, kernel.row_ind_list, kernel.col_ind_list]*kernel.kernel).sum() +# except IndexError: +# return None +# +# # Linear interpolation: +# s_pre = dc_offset + s1*((1-(fi-fim))*.5) + s2*((fi-fim)*.5) +# +# if s_pre < 0: +# return 0 +# else: +# return float(s_pre) +# +# return rectifying_filter \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/make_cell_list.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/make_cell_list.py new file mode 100644 index 0000000..aa05481 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/make_cell_list.py @@ -0,0 +1,294 @@ +import scipy.io as sio +import os +import matplotlib.pyplot as plt +import isee_engine.nwb as nwb +from linearfilter import SpatioTemporalFilter +import numpy as np +from spatialfilter import GaussianSpatialFilter +from transferfunction import ScalarTransferFunction +from temporalfilter import TemporalFilterCosineBump +from cursor import LNUnitCursor, MultiLNUnitCursor +from movie import Movie +from lgnmodel1 import LGNModel, heat_plot +from cellmodel import LGNOnCell, LGNOffCell,LGNOnOffCell,TwoSubfieldLinearCell, OnUnit, OffUnit +from transferfunction import MultiTransferFunction, ScalarTransferFunction +from lnunit import LNUnit, MultiLNUnit +from sympy.abc import x as symbolic_x +from sympy.abc import y as symbolic_y +from kernel import Kernel3D +from movie import Movie, FullFieldFlashMovie +import itertools +import scipy.stats as sps + +# def multi_cell_tensor_generator(cell_creation_function, **kwargs): +# +# sew_param_dict = {} +# static_param_dict = {} +# for key, val in kwargs.items(): +# if isinstance(val, (list, np.ndarray)): +# sew_param_dict[key]=val +# else: +# static_param_dict[key]=val +# +# cell_list = [] +# loop_keys, loop_lists = zip(*sew_param_dict.items()) +# for param_tuple in itertools.product(*loop_lists): +# param_dict = dict(zip(loop_keys, param_tuple)) +# print param_dict +# param_dict.update(static_param_dict) +# cell_list += cell_creation_function(**param_dict) +# +# return cell_list + +def multi_cell_random_generator(cell_creation_function=None, **kwargs): + + sew_param_dict = {} + static_param_dict = {} + range_key_dict = {} + for key, val in kwargs.items(): + if isinstance(val, (sps.rv_continuous, sps.rv_discrete)) or type(val) == type(sps.multivariate_normal()): + sew_param_dict[key]=val + elif isinstance(val, np.ndarray): + range_key_dict[key] = val + else: + static_param_dict[key]=val + + number_of_cells = static_param_dict.pop('number_of_cells', 1) + + for key, val in range_key_dict.items(): + assert len(val) == number_of_cells + + cell_list = [] + loop_keys, loop_lists = zip(*sew_param_dict.items()) + value_instance_list = zip(*map(lambda x: x.rvs(size=number_of_cells), loop_lists)) + for ii, curr_value_instance in enumerate(value_instance_list): + param_dict = dict(zip(loop_keys, curr_value_instance)) + param_dict.update(static_param_dict) + param_dict['number_of_cells'] = 1 + for range_key in range_key_dict: + param_dict[range_key] = range_key_dict[range_key][ii] + + if cell_creation_function is None: + cell_list.append(param_dict) + else: + cell_list += cell_creation_function(**param_dict) + + return cell_list + + +def make_single_unit_cell_list(number_of_cells=None, + lattice_unit_center=None, + weights=None, + kpeaks=None, + delays=None, + amplitude=None, + sigma=None, + width=5, + transfer_function_str = 'Heaviside(s)*s'): + + cell_list = [] + for _ in range(number_of_cells): + dxi = np.random.uniform(-width*1./2,width*1./2) + dyi = np.random.uniform(-width*1./2,width*1./2) + temporal_filter = TemporalFilterCosineBump(weights, kpeaks,delays) + spatial_filter = GaussianSpatialFilter(translate=(dxi,dyi), sigma=sigma, origin=lattice_unit_center) # all distances measured from BOTTOM LEFT + spatiotemporal_filter = SpatioTemporalFilter(spatial_filter, temporal_filter, amplitude=amplitude) + transfer_function = ScalarTransferFunction(transfer_function_str) + if amplitude > 0.: + cell = OnUnit(spatiotemporal_filter, transfer_function) + elif amplitude < 0.: + cell = OffUnit(spatiotemporal_filter, transfer_function) + else: + raise Exception + + + cell_list.append(cell) + + return cell_list + +def make_on_off_cell_list(number_of_cells=None, + lattice_unit_center=None, + weights_on=None, + weights_off=None, + kpeaks_on=None, + kpeaks_off=None, + delays_on = None, + delays_off = None, + amplitude_on=None, + amplitude_off=None, + sigma_on=None, + sigma_off=None, + subfield_separation=None, + ang=None, + dominant_subunit=None, + width=5, + transfer_function_str = 'Heaviside(x)*x + Heaviside(y)*y'): + + cell_list = [] + for _ in range(number_of_cells): + + dxi = np.random.uniform(-width*1./2,width*1./2) + dyi = np.random.uniform(-width*1./2,width*1./2) + + dominant_subfield_location = (lattice_unit_center[0]+dxi, lattice_unit_center[1]+dyi) +# hor_offset = np.cos(ang*np.pi/180.)*subfield_separation +# vert_offset = np.sin(ang*np.pi/180.)*subfield_separation +# nondominant_subfield_translation = (hor_offset,vert_offset) + + if dominant_subunit == 'on': + on_translate = dominant_subfield_location#(0,0) + off_translate = dominant_subfield_location#nondominant_subfield_translation + + elif dominant_subunit == 'off': + + off_translate = dominant_subfield_location#(0,0) + on_translate = dominant_subfield_location#nondominant_subfield_translation + + else: + raise Exception + + on_origin = off_origin = (0,0)#dominant_subfield_location + + temporal_filter_on = TemporalFilterCosineBump(weights_on, kpeaks_on,delays_on) + spatial_filter_on = GaussianSpatialFilter(translate=on_translate,sigma=sigma_on, origin=on_origin) # all distances measured from BOTTOM LEFT + on_filter = SpatioTemporalFilter(spatial_filter_on, temporal_filter_on, amplitude=amplitude_on) + + temporal_filter_off = TemporalFilterCosineBump(weights_off, kpeaks_off,delays_off) + spatial_filter_off = GaussianSpatialFilter(translate=off_translate,sigma=sigma_off, origin=off_origin) # all distances measured from BOTTOM LEFT + off_filter = SpatioTemporalFilter(spatial_filter_off, temporal_filter_off, amplitude=amplitude_off) + +# cell = LGNOnOffCell(on_filter, off_filter, transfer_function=MultiTransferFunction((symbolic_x, symbolic_y), transfer_function_str)) + cell = TwoSubfieldLinearCell(on_filter,off_filter,subfield_separation=subfield_separation, onoff_axis_angle=ang, dominant_subfield_location=dominant_subfield_location) + cell_list.append(cell) + + return cell_list + +# amplitude_list = amplitude_dist.rvs(size=5) +# kpeak_list = kpeak_dist.rvs(size=5) +# cell_config = {'number_of_cells':5, +# 'lattice_unit_center':(40,30), +# 'weights':(.4,-.2), +# 'kpeaks':kpeak_list, +# 'amplitude':amplitude_list, +# 'sigma':(4,4), +# 'width':5} +# multi_cell_tensor_generator(make_single_unit_cell_list, **cell_config) + + +# amplitude_dist = sps.rv_discrete(values=([20,25], [.5,.5])) +# kpeak_dist = sps.multivariate_normal(mean=[40., 80.], cov=[[5.0, 0], [0, 5]]) +# +# single_unit_cell_config = {'number_of_cells':10, +# 'lattice_unit_center':(40,30), +# 'weights':(.4,-.2), +# 'kpeaks':kpeak_dist, +# 'amplitude':amplitude_dist, +# 'sigma':(4,4), +# 'width':5} +# +# +# amplitude_on_dist = sps.rv_discrete(values=([20,25], [.5,.5])) +# amplitude_off_dist = sps.rv_discrete(values=([-10,-15], [.5,.5])) +# kpeak_on_dist = sps.multivariate_normal(mean=[40., 80.], cov=[[5.0, 0], [0, 5]]) +# kpeak_off_dist = sps.multivariate_normal(mean=[100., 160.], cov=[[5.0, 0], [0, 5]]) +# #ang_dist = sps.rv_discrete(values=(np.arange(0,360,45), 1./8*np.ones((1,8)))) +# ang_dist = np.arange(0,360,45) +# +# two_unit_cell_config={'number_of_cells':8, +# 'lattice_unit_center':(40,30), +# 'weights_on':(.4,-.2), +# 'weights_off':(.4,-.1), +# 'kpeaks_on':kpeak_on_dist, +# 'kpeaks_off':kpeak_off_dist, +# 'amplitude_on':20., +# 'amplitude_off':-10., +# 'sigma_on':(4,4), +# 'sigma_off':(4,4), +# 'subfield_separation':2., +# 'ang':ang_dist, +# 'dominant_subunit':'on', +# 'width':5} + + +def evaluate_cell_and_plot(input_cell, input_movie, ax, show=False): + t, y = input_cell.evaluate(input_movie,downsample = 10) + ax.plot(t, y) + + if show == True: + plt.show() + + +# if __name__ == "__main__": +# +# # Create stimulus 0: +# frame_rate = 60 +# m1 = FullFieldFlashMovie(np.arange(60), np.arange(80), 1., 3., frame_rate=frame_rate).full(t_max=3) +# m2 = FullFieldFlashMovie(np.arange(60), np.arange(80), 0, 2, frame_rate=frame_rate, max_intensity=-1).full(t_max=2) +# m3 = FullFieldFlashMovie(np.arange(60), np.arange(80), 0, 2., frame_rate=frame_rate).full(t_max=2) +# m4 = FullFieldFlashMovie(np.arange(60), np.arange(80), 0, 2, frame_rate=frame_rate, max_intensity=0).full(t_max=2) +# m0 = m1+m2+m3+m4 +# +# # Create stimulus 1: +# movie_file = '/data/mat/RamIyer/for_Anton/grating_ori0_res2.mat' +# m_file = sio.loadmat(movie_file) +# m_data_raw = m_file['mov_fine'].T +# m_data = np.reshape(m_data_raw,(3000,64,128)) +# m1 = Movie(m_data, frame_rate=1000.) +# +# #Create stimulus 2: +# movie_file = '/data/mat/iSee_temp_shared/TouchOfEvil_norm.npy' +# m_data = np.load(movie_file, 'r') +# m = Movie(m_data[1000:], frame_rate=30.) +# +# movie_list = [m0, m1, m2] +# +# #==================================================== +# +# #Create cell list +# +# cell_list = [] +# +# #On cells +# params_tON = (5, (40,30), (.4,-.2),(40,80),20.,(4,4)) +# tON_list = make_single_unit_cell_list(*params_tON) +# cell_list.append(tON_list) +# +# params_sON = (5, (40,30), (.4,-.1),(100,160),20.,(4,4)) +# sON_list = make_single_unit_cell_list(*params_sON) +# cell_list.append(sON_list) +# +# #Off cells +# params_tOFF = (5, (40,30), (.4,-.2),(40,80),-20.,(4,4)) +# tOFF_list = make_single_unit_cell_list(*params_tOFF) +# cell_list.append(tOFF_list) +# +# params_sOFF = (5, (40,30), (.4,-.1),(100,160),-20.,(4,4)) +# sOFF_list = make_single_unit_cell_list(*params_sOFF) +# cell_list.append(sOFF_list) +# +# #ONOFF cells +# params_onoff = (5, (40,30),(.4, -.2),(.4,-.2),(40, 80),(50,100),20.,-20.,(4,4),(4,4),2.,0,'on') +# onoff_list = make_on_off_cell_list(*params_onoff) +# cell_list.append(onoff_list) +# +# #Two subunit cells +# params_twosub = (5, (40,30),(.4, -.2),(.4,-.1),(40, 80),(100,160),20.,-10.,(4,2),(3,4),10.,90,'on') +# twosub_list = make_on_off_cell_list(*params_twosub) +# cell_list.append(twosub_list) +# +# #===================================================== +# #Evaluate and plot responses +# nc = len(movie_list) +# nr = len(cell_list) +# fig, axes = plt.subplots(nr,nc+2) +# +# for curr_row, curr_cell in zip(axes, cell_list): +# curr_cell.show_spatial_filter(np.arange(60),np.arange(80), ax=curr_row[0], show=False, colorbar=False) +# curr_cell.show_temporal_filter(ax=curr_row[1], show=False) +# +# for curr_row, curr_cell in zip(axes, cell_list): +# for curr_ax, curr_movie in zip(curr_row[2:], movie_list): +# evaluate_cell_and_plot(curr_cell, curr_movie, curr_ax, show=False) +# +# plt.tight_layout() +# plt.show() diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/movie.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/movie.py new file mode 100755 index 0000000..a9d4e67 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/movie.py @@ -0,0 +1,196 @@ +import matplotlib.pyplot as plt +import numpy as np +from .utilities import convert_tmin_tmax_framerate_to_trange + + +class Movie(object): + def __init__(self, data, row_range=None, col_range=None, labels=('time', 'y', 'x'), + units=('second', 'pixel', 'pixel'), frame_rate=None, t_range=None): + self.data = data + self.labels = labels + self.units = units + assert units[0] == 'second' + + if t_range is None: + self.frame_rate = float(frame_rate) + self.t_range = np.arange(data.shape[0])*(1./self.frame_rate) + else: + self.t_range = np.array(t_range) + self.frame_rate = 1./np.mean(np.diff(t_range)) + + if row_range is None: + self.row_range = np.arange(data.shape[1]) + else: + self.row_range = np.array(row_range) + if col_range is None: + self.col_range = np.arange(data.shape[2]) + else: + self.col_range = np.array(col_range) + + def imshow_summary(self, ax=None, show=True, xlabel=None): + if ax is None: + _, ax = plt.subplots(1,1) + + t_vals = self.t_range.copy() + y_vals = self.data.mean(axis=2).mean(axis=1) + ax.plot(t_vals, y_vals) + ax.set_ylim(y_vals.min()-np.abs(y_vals.min())*.05, y_vals.max()+np.abs(y_vals.max())*.05) + + if not xlabel is None: + ax.set_xlabel(xlabel) + + ax.set_ylabel('Average frame intensity') + + if show == True: + plt.show() + + return ax, (t_vals, y_vals) + + def imshow(self, t, show=True, vmin=-1, vmax=1, cmap=plt.cm.gray): + ti = int(t*self.frame_rate) + data = self.data[ti,:,:] + plt.imshow(data, vmin=vmin, vmax=vmax, cmap=cmap) + plt.colorbar() + if show: + plt.show() + + def __add__(self, other): + + assert self.labels == other.labels + assert self.units == other.units + assert self.frame_rate == other.frame_rate + np.testing.assert_almost_equal(self.col_range, other.col_range) + np.testing.assert_almost_equal(self.row_range, other.row_range) + + + new_data = np.empty((len(self.t_range)+len(other.t_range)-1, len(self.row_range), len(self.col_range))) + new_data[:len(self.t_range), :,:] = self.data[:,:,:] + new_data[len(self.t_range):, :,:] = other.data[1:,:,:] + + return Movie(new_data, row_range=self.row_range.copy(), col_range=self.col_range.copy(), labels=self.labels, units=self.units, frame_rate=self.frame_rate) + + @property + def ranges(self): + return self.t_range, self.row_range, self.col_range + + def get_nwb_GrayScaleMovie(self): + + t_scale = nwb.Scale(self.t_range, 'time', self.units[0]) + row_scale = nwb.Scale(self.row_range, 'distance', self.units[1]) + col_scale = nwb.Scale(self.col_range, 'distance', self.units[2]) + + return nwb.GrayScaleMovie(self.data, scale=(t_scale, row_scale, col_scale)) + + def __getitem__(self, *args): + return self.data.__getitem__(*args) + + +class FullFieldMovie(Movie): + def __init__(self, f, row_range, col_range, frame_rate=24): + self.row_range = row_range + self.col_range = col_range + self.frame_size = (len(self.row_range), len(self.col_range)) + self._frame_rate = frame_rate + self.f = f + + @property + def frame_rate(self): + return self._frame_rate + + @property + def data(self): + return self + + def __getitem__(self, *args): + + t_inds, x_inds, y_inds = args[0] + + assert (len(x_inds) == len(y_inds)) and (len(y_inds) == len(t_inds)) + + # Convert frame indices to times: + t_vals = (1./self.frame_rate)*t_inds + + # Evaluate and return: + return self.f(t_vals) + + def full(self, t_min=0, t_max=None): + # Compute t_range + t_range = convert_tmin_tmax_framerate_to_trange(t_min, t_max, self.frame_rate) + + nt = len(t_range) + nr = len(self.row_range) + nc = len(self.col_range) + a,b,c = np.meshgrid(range(nt),range(nr),range(nc)) + af, bf, cf = map(lambda x: x.flatten(), [a,b,c]) + data = np.empty((nt, nr, nc)) + data[af, bf, cf] = self.f(t_range[af]) + + return Movie(data, row_range=self.row_range, col_range=self.col_range, labels=('time', 'y', 'x'), units=('second', 'pixel', 'pixel'), frame_rate=self.frame_rate) + + +class FullFieldFlashMovie(FullFieldMovie): + def __init__(self, row_range, col_range, t_on, t_off, max_intensity=1, frame_rate=24): + assert t_on < t_off + + def f(t): + return np.piecewise(t, *zip(*[(t < t_on, 0), (np.logical_and(t_on <= t, t < t_off), max_intensity), + (t_off <= t, 0)])) + + super(FullFieldFlashMovie, self).__init__(f, row_range, col_range, frame_rate=frame_rate) + + +class GratingMovie(Movie): + def __init__(self, row_size, col_size, frame_rate=1000.): + self.row_size = row_size #in degrees + self.col_size = col_size #in degrees + self.frame_rate = float(frame_rate) #in Hz + + def create_movie(self, t_min = 0, t_max = 1, gray_screen_dur = 0, cpd = 0.05, temporal_f = 4, theta = 45, phase = 0., contrast = 1.0, row_size_new = None, col_size_new = None): + """Create the grating movie with the desired parameters + :param t_min: start time in seconds + :param t_max: end time in seconds + :param gray_screen_dur: Duration of gray screen before grating stimulus starts + :param cpd: cycles per degree + :param temporal_f: in Hz + :param theta: orientation angle + :return: Movie object of grating with desired parameters + """ + assert contrast <= 1, "Contrast must be <= 1" + assert contrast > 0, "Contrast must be > 0" + + physical_spacing = 1. / (float(cpd) * 10) #To make sure no aliasing occurs + self.row_range = np.linspace(0, self.row_size, self.row_size / physical_spacing, endpoint = True) + self.col_range = np.linspace(0, self.col_size, self.col_size / physical_spacing, endpoint = True) + numberFramesNeeded = int(round(self.frame_rate * (t_max - gray_screen_dur))) + 1 + time_range = np.linspace(gray_screen_dur, t_max - gray_screen_dur, numberFramesNeeded, endpoint=True) + + tt, yy, xx = np.meshgrid(time_range, self.row_range, self.col_range, indexing='ij') + + thetaRad = -np.pi*(180-theta)/180. + phaseRad = np.pi*(180-phase)/180. + xy = xx * np.cos(thetaRad) + yy * np.sin(thetaRad) + data = contrast*np.sin(2*np.pi*(cpd * xy + temporal_f *tt) + phaseRad) + + if row_size_new != None: + self.row_range = np.linspace(0, row_size_new, data.shape[1], endpoint = True) + if col_size_new != None: + self.col_range = np.linspace(0, col_size_new, data.shape[2], endpoint = True) + + if gray_screen_dur > 0: + # just adding one or two seconds to gray screen so flash never "happens" + m_gray = FullFieldFlashMovie(self.row_range, self.col_range, gray_screen_dur + 1, gray_screen_dur + 2, + frame_rate=self.frame_rate).full(t_max=gray_screen_dur) + mov = m_gray + Movie(data, row_range=self.row_range, col_range=self.col_range, labels=('time', 'y', 'x'), + units=('second', 'pixel', 'pixel'), frame_rate=self.frame_rate) + else: + mov = Movie(data, row_range=self.row_range, col_range=self.col_range, labels=('time', 'y', 'x'), + units=('second', 'pixel', 'pixel'), frame_rate=self.frame_rate) + + return mov + + +if __name__ == "__main__": + m1 = FullFieldFlashMovie(range(60), range(80), 1, 2).full(t_max=2) + m2 = FullFieldFlashMovie(range(60), range(80), 1, 2).full(t_max=2) + m3 = m1+m2 + m3.imshow_summary() diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/poissongeneration.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/poissongeneration.py new file mode 100644 index 0000000..b2125b1 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/poissongeneration.py @@ -0,0 +1,104 @@ +import numpy as np +import scipy.interpolate as sinterp +import scipy.integrate as spi +import warnings +import scipy.optimize as sopt +import scipy.stats as sps + +def generate_renewal_process(t0, t1, renewal_distribution): + last_event_time = t0 + curr_interevent_time = float(renewal_distribution()) + event_time_list = [] + while last_event_time+curr_interevent_time <= t1: + event_time_list.append(last_event_time+curr_interevent_time) + curr_interevent_time = float(renewal_distribution()) + last_event_time = event_time_list[-1] + + return event_time_list + +def generate_poisson_process(t0, t1, rate): + + if rate is None: raise ValueError('Rate cannot be None') + if rate > 10000: warnings.warn('Very high rate encountered: %s' % rate) + + + try: + assert rate >= 0 + except AssertionError: + raise ValueError('Negative rate (%s) not allowed' % rate) + + try: + assert rate < np.inf + except AssertionError: + raise ValueError('Rate (%s) must be finite' % rate) + + + + + + + + if rate == 0: + return [] + else: + return generate_renewal_process(t0, t1, sps.expon(0,1./rate).rvs) + +def generate_inhomogenous_poisson(t_range, y_range, seed=None): + if not seed == None: np.random.seed(seed) + spike_list = [] + for tl, tr, y in zip(t_range[:-1], t_range[1:], y_range[:-1]): + spike_list += generate_poisson_process(tl, tr, y) + return spike_list + + + + +def generate_poisson_rescaling(t, y, seed=None): + y = np.array(y) + t = np.array(t) + assert not np.any(y<0) + f = sinterp.interp1d(t, y, fill_value=0, bounds_error=False) + return generate_poisson_rescaling_function(lambda y, t: f(t), t[0], t[-1], seed=seed) + + + +def generate_poisson_rescaling_function(f, t_min, t_max, seed=None): + + + + def integrator(t0, t1): + return spi.odeint(f, 0, [t0, t1])[1][0] + + if not seed == None: + np.random.seed(seed) + + spike_train = [] + while t_min < t_max: + e0 = np.random.exponential() + def root_function(t): + return e0 - integrator(t_min, t) + + try: + with warnings.catch_warnings(record=True) as w: + result = sopt.root(root_function, .1) + assert result.success + except AssertionError: + if not e0 < integrator(t_min, t_max): + assert Exception + else: + break + + + + + t_min = result.x[0] + spike_train.append(t_min) + + return np.array(spike_train) + + +def test_generate_poisson_function(): + + f = lambda y, t:10 + + assert len(generate_poisson_function(f,0,1,seed=5)) == 12 \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/singleunitcell.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/singleunitcell.py new file mode 100644 index 0000000..d3e0b24 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/singleunitcell.py @@ -0,0 +1,8 @@ +from temporalfilter import TemporalFilterCosineBump +from transferfunction import ScalarTransferFunction +from linearfilter import SpatioTemporalFilter +import numpy as np +from spatialfilter import GaussianSpatialFilter +from cellmodel import OnUnit, OffUnit + + diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/spatialfilter.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/spatialfilter.py new file mode 100644 index 0000000..466db94 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/spatialfilter.py @@ -0,0 +1,215 @@ +from scipy import ndimage +import numpy as np +import itertools +import importlib +import scipy.interpolate as spinterp +from . import utilities as util +import matplotlib.pyplot as plt +import scipy.misc as spmisc +import scipy.ndimage as spndimage +from .kernel import Kernel2D, Kernel3D + +class ArrayFilter(object): + + default_threshold = .01 + + def __init__(self, mask): + + self.mask = mask + + def imshow(self, row_range, col_range, threshold=0, **kwargs): + + return self.get_kernel(row_range, col_range,threshold).imshow(**kwargs) + + def get_kernel(self, row_range, col_range, threshold=0, amplitude=1.): + +# print np.where(self.mask>threshold) + row_vals, col_vals = np.where(self.mask>threshold) + + kernel_vals = self.mask[row_vals, col_vals] + kernel_vals = amplitude*kernel_vals/kernel_vals.sum() + + return Kernel2D(row_range, col_range, row_vals, col_vals, kernel_vals) # row_range, col_range, row_inds, col_inds, kernel): + + +class GaussianSpatialFilter(object): + + default_threshold = .01 + + def __init__(self, translate=(0, 0), sigma=(1.,1.), rotation=0, origin='center'): + '''When w=1 and rotation=0, half-height will be at y=1''' + + self.translate = translate + self.rotation = rotation + self.sigma = sigma + self.origin = origin + + def imshow(self, row_range, col_range, threshold=0, **kwargs): + return self.get_kernel(row_range, col_range,threshold).imshow(**kwargs) + + def to_dict(self): + + return {'class':(__name__, self.__class__.__name__), + 'translate':self.translate, + 'rotation':self.rotation, + 'sigma':self.sigma} + + def get_kernel(self, row_range, col_range, threshold=0, amplitude=1.): + + # Create symmetric initial point at center: + image_shape = len(col_range), len(row_range) + h, w = image_shape + on_filter_spatial = np.zeros(image_shape) + if h%2 == 0 and w%2 == 0: + for ii, jj in itertools.product(range(2), range(2)): + on_filter_spatial[int(h/2)+ii-1,int(w/2)+jj-1] = .25 + elif h%2 == 0 and w%2 != 0: + for ii in range(2): + on_filter_spatial[int(h/2)+ii-1,int(w/2)] = .25 + elif h%2 != 0 and w%2 == 0: + for jj in range(2): + on_filter_spatial[int(h/2),int(w/2)+jj-1] = .25 + else: + on_filter_spatial[int(h/2),int(w/2)] = .25 + + # Apply gaussian filter to create correct sigma: + scaled_sigma_x =float(self.sigma[0])/(col_range[1]-col_range[0]) + scaled_sigma_y = float(self.sigma[1])/(row_range[1]-row_range[0]) + on_filter_spatial = ndimage.gaussian_filter(on_filter_spatial, (scaled_sigma_x, scaled_sigma_y), mode='nearest', cval=0) +# on_filter_spatial = skf.gaussian_filter(on_filter_spatial, sigma=(scaled_sigma_x, scaled_sigma_y)) + + # Rotate and translate at center: + rotation_matrix = util.get_rotation_matrix(self.rotation, on_filter_spatial.shape) + translation_x = float(self.translate[1])/(row_range[1]-row_range[0]) + translation_y = -float(self.translate[0])/(col_range[1]-col_range[0]) + translation_matrix = util.get_translation_matrix((translation_x, translation_y)) + if self.origin != 'center': + center_y = -(self.origin[0]-(col_range[-1]+col_range[0])/2)/(col_range[1]-col_range[0]) + center_x = (self.origin[1]-(row_range[-1]+row_range[0])/2)/(row_range[1]-row_range[0]) + translation_matrix += util.get_translation_matrix((center_x, center_y)) + kernel_data = util.apply_transformation_matrix(on_filter_spatial, translation_matrix+rotation_matrix) + + kernel = Kernel2D.from_dense(row_range, col_range, kernel_data, threshold=0) + kernel.apply_threshold(threshold) + kernel.normalize() + + kernel.kernel *= amplitude + + + return kernel + + + +# spatial_model = GaussianSpatialFilterModel(height=21, aspect_ratio=1., rotation=0) +# spatial_filter = spatial_model(center=(30,40)) +# k = spatial_filter.get_spatial_kernel(range(60), range(80)) +# k.imshow(frame_size=(60,80)) + + + + + + + + + + + + + +# def evaluate_movie(self, movie, t, show=False): +# +# y = [] +# for ti in t: +# kernel_result = movie.evaluate_Kernel3D(ti, self) +# y.append(self.transfer_function(kernel_result)) +# +# if show == True: +# plt.plot(t, y) +# plt.show() +# +# return t, y + +# print mesh_range[0] +# +# ii = mesh_range[0][inds] +# jj = mesh_range[1][inds] +# print ii, jj +# print tmp[jj,ii] + +# plt.figure() +# plt.pcolor(mesh_range[0], mesh_range[1], tmp) +# plt.colorbar() +# plt.axis('equal') +# plt.show() + +# print self.xydata[0].shape +# +# t0 = spndimage.rotate(self.xydata[0],30,reshape=False, mode=mode) +# t1 = spndimage.rotate(self.xydata[1],30, reshape=False, mode=mode) + +# print t0.shape +# print t1.shape +# print on_filter_spatial.shape + +# plt.pcolor(t0,t1, on_filter_spatial) + + +# self.interpolation_function = spinterp.interp2d(self.w_values, self.h_values, on_filter_spatial, fill_value=0, bounds_error=False) +# +# print self.interpolation_function((t0,t1)) + +# translation_matrix = util.get_translation_matrix(self.translation) +# tmp = util.apply_transformation_matrix(on_filter_spatial, translation_matrix) +# +# plt.pcolor(self.xydata[0], self.xydata[1], tmp) +# plt.show() + +# # print self.xydata_trans[0][0], self.xydata_trans[0],[-1] +# # print self.xydata_trans[1][0], self.xydata_trans[1],[-1] +# print self.xydata_trans +# rotation_matrix = util.get_rotation_matrix(self.rotation, on_filter_spatial.shape) +# translation_matrix = util.get_translation_matrix(self.translation) +# on_filter_spatial = util.apply_transformation_matrix(on_filter_spatial, translation_matrix+rotation_matrix) + +# plt.imshow(on_filter_spatial, extent=(self.w_values[0], self.w_values[-1], self.h_values[0], self.h_values[-1]), aspect=1.) +# plt.show() + +# def to_dict(self): +# +# return {'scale':self.scale, +# 'translation':self.translation, +# 'rotation':self.rotation, +# 'weight':self.weight, +# 'temporal_filter':self.temporal_filter.to_dict(), +# 'class':(__name__, self.__class__.__name__)} + +# def get_kernel(self, xdata, ydata, threshold=default_threshold): +# +# +# # Rotate and translate at center: +# rotation_matrix = util.get_rotation_matrix(self.rotation, on_filter_spatial.shape) +# translation_matrix = util.get_translation_matrix(self.translation) +# on_filter_spatial = util.apply_transformation_matrix(on_filter_spatial, translation_matrix+rotation_matrix) +# +# # Now translate center of field in image: +# # translation_matrix = util.get_translation_matrix(relative_spatial_location) +# # on_filter_spatial = util.apply_transformation_matrix(on_filter_spatial, translation_matrix) +# +# # Create and return thresholded 2D mask: +# row_ind_list, col_ind_list = np.where(on_filter_spatial != 0) +# kernel = on_filter_spatial[row_ind_list, col_ind_list] +# +# +# +# +# # filter_mask = Kernel2D(row_ind_list, col_ind_list, kernel, threshold=threshold) +# +# return filter_mask + +# translation_matrix = util.get_translation_matrix((1.*translation[0]/fudge_factor,-1.*translation[1]/fudge_factor)) + +# plt.figure() +# plt.pcolor(self.mesh_support[0], self.mesh_support[1], self.kernel_data) +# plt.axis('equal') +# plt.show() \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/temporalfilter.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/temporalfilter.py new file mode 100644 index 0000000..1e604bf --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/temporalfilter.py @@ -0,0 +1,114 @@ +import numpy as np +from . import fitfuns +import scipy.interpolate as spinterp +import matplotlib.pyplot as plt +from .kernel import Kernel1D + +class TemporalFilter(object): + + def __init__(self, *args, **kwargs): pass + + def imshow(self, t_range=None, threshold=0, reverse=False, rescale=False, **kwargs): + return self.get_kernel(t_range, threshold, reverse, rescale).imshow(**kwargs) + + + def to_dict(self): + return {'class':(__name__, self.__class__.__name__)} + + def get_kernel(self, t_range=None, threshold=0, reverse=False, rescale=False): + + if t_range is None: + t_range = self.get_default_t_grid() + +# print self.t_support +# print self.kernel_data + + if len(self.t_support) == 1: + k = Kernel1D(self.t_support, self.kernel_data, threshold=threshold, reverse=reverse) + else: + interpolation_function = spinterp.interp1d(self.t_support, self.kernel_data, fill_value=0, bounds_error=False, assume_sorted=True) + k = Kernel1D(t_range, interpolation_function(t_range), threshold=threshold, reverse=reverse) + if rescale == True: + k.rescale() + + #assert np.abs(np.abs(k.kernel).sum() - 1) < 1e-14 + assert np.abs(np.abs(k.kernel.sum()) - 1) < 1e-14 + + return k + +class ArrayTemporalFilter(TemporalFilter): + + def __init__(self, mask,t_support): + + self.mask = mask + self.t_support = t_support + + assert len(self.mask) == len(self.t_support) + + self.nkt = 600 + + super(self.__class__, self).__init__() + + self.kernel_data = self.mask + #self.t_support = np.arange(0, len(self.kernel_data)*.001, .001) + #assert len(self.t_support) == len(self.kernel_data) + + def get_default_t_grid(self): + + return np.arange(self.nkt)*.001 + +class TemporalFilterCosineBump(TemporalFilter): + + def __init__(self, weights, kpeaks, delays): + + assert len(kpeaks) == 2 + assert kpeaks[0] 0 + assert delays[0] <= delays[1] + + self.ncos = len(weights) + + # Not likely to change defaults: + self.neye = 0 + self.b = .3 + self.nkt = 600 + + super(self.__class__, self).__init__() + + # Parameters + self.weights = np.array([weights]).T + self.kpeaks = kpeaks + self.delays = np.array([delays]).astype(int) + + # Adapter code to get filters from Ram's code: + kbasprs = {} + kbasprs['neye'] = self.neye + kbasprs['ncos'] = self.ncos + kbasprs['kpeaks'] = self.kpeaks + kbasprs['b'] = self.b + kbasprs['delays'] = self.delays + nkt = self.nkt + #kbasprs['bases'] = fitfuns.makeBasis_StimKernel(kbasprs, nkt) + self.kernel_data = np.dot(fitfuns.makeBasis_StimKernel(kbasprs, nkt), self.weights)[::-1].T[0] +# plt.figure() +# plt.plot(self.kernel_data) +# plt.show() +# sys.exit() + self.t_support = np.arange(0, len(self.kernel_data)*.001, .001) + self.kbasprs = kbasprs + assert len(self.t_support) == len(self.kernel_data) + + def __call__(self, t): + return self.interpolation_function(t) + + def get_default_t_grid(self): + return np.arange(self.nkt)*.001 + + def to_dict(self): + + param_dict = super(self.__class__, self).to_dict() + + param_dict.update({'weights':self.weights.tolist(), + 'kpeaks':self.kpeaks}) + + return param_dict diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/transferfunction.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/transferfunction.py new file mode 100644 index 0000000..03ff617 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/transferfunction.py @@ -0,0 +1,58 @@ +from sympy.utilities.lambdify import lambdify +import sympy.parsing.sympy_parser as symp +import sympy.abc +import numpy as np + + +class ScalarTransferFunction(object): + def __init__(self, transfer_function_string, symbol=sympy.abc.s): + self.symbol = symbol + self.transfer_function_string = transfer_function_string + self.closure = lambdify(self.symbol, symp.parse_expr(self.transfer_function_string), modules=['sympy']) + + def __call__(self, s): + return self.closure(s) + + def to_dict(self): + return {'class': (__name__, self.__class__.__name__), + 'function': self.transfer_function_string} + + def imshow(self, xlim, ax=None, show=True, save_file_name=None, ylim=None): + # TODO: This function should be removed (as Ram to see if/where it's used) since it will fail (no t_vals) + import matplotlib.pyplot as plt + if ax is None: + _, ax = plt.subplots(1, 1) + + plt.plot(self.t_vals, self.kernel) + ax.set_xlabel('Time (Seconds)') + + if ylim is not None: + ax.set_ylim(ylim) + + if xlim is not None: + ax.set_xlim((self.t_range[0], self.t_range[-1])) + + if save_file_name is not None: + plt.savefig(save_file_name, transparent=True) + + if show: + plt.show() + + return ax + + +class MultiTransferFunction(object): + def __init__(self, symbol_tuple, transfer_function_string): + self.symbol_tuple = symbol_tuple + self.transfer_function_string = transfer_function_string + self.closure = lambdify(self.symbol_tuple, symp.parse_expr(self.transfer_function_string), modules=['sympy']) + + def __call__(self, *s): + if isinstance(s[0], (float,)): + return self.closure(*s) + else: + return np.array(list(map(lambda x: self.closure(*x), zip(*s)))) + + def to_dict(self): + return {'class': (__name__, self.__class__.__name__), + 'function': self.transfer_function_string} diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/util_fns.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/util_fns.py new file mode 100644 index 0000000..af297a0 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/util_fns.py @@ -0,0 +1,190 @@ +import os +import re +import matplotlib.mlab as mlab +import numpy as np +import scipy.io as sio +from scipy.fftpack import fft +import pandas as pd +from .movie import Movie, FullFieldFlashMovie + + +pd.set_option('display.width', 1000) +pd.set_option('display.max_columns', 100) + + +################################################# +def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i:i + n] + + +################################################## +def compute_FFT_OneCycle(FR, TF, downsample): + one_cyc = np.int(((1000. / downsample) / TF)) + FR_cyc = list(chunks(FR, one_cyc)) + if (TF == 15. or TF == 8.): + FR_cyc = FR_cyc[:-1] + + FR_cyc_avg = np.mean(FR_cyc, axis=0) + y = FR_cyc_avg + AMP = 2 * np.abs(fft(y) / len(y)) + F0 = 0.5 * AMP[0] + assert (F0 - np.mean(y) < 1.e-4) + F1 = AMP[1] + + return F0, F1 + + +################################################## +def create_ff_mov(frame_rate, tst, tend, xrng, yrng): + ff_mov_on = FullFieldFlashMovie(np.arange(xrng), np.arange(yrng), tst, tend, frame_rate=frame_rate, + max_intensity=1).full(t_max=tend) # +0.5) + ff_mov_off = FullFieldFlashMovie(np.arange(xrng), np.arange(yrng), tst, tend, frame_rate=frame_rate, + max_intensity=-1).full(t_max=tend) # +0.5) + + return ff_mov_on, ff_mov_off + + +################################################## +def create_grating_movie_list(gr_dir_name): + gr_fnames = os.listdir(gr_dir_name) + gr_fnames_ord = sorted(gr_fnames, key=lambda x: (int(re.sub('\D', '', x)), x)) + + gr_mov_list = [] + for fname in gr_fnames_ord[:5]: + movie_file = os.path.join(gr_dir_name, fname) + m_file = sio.loadmat(movie_file) + m_data_raw = m_file['mov'].T + swid = np.shape(m_data_raw)[1] + res = int(np.sqrt(swid / (8 * 16))) + m_data = np.reshape(m_data_raw, (3000, 8 * res, 16 * res)) + m1 = Movie(m_data[:500, :, :], row_range=np.linspace(0, 120, m_data.shape[1], endpoint=True), col_range=np.linspace(0, 120, m_data.shape[2], endpoint=True), frame_rate=1000.) + gr_mov_list.append(m1) + + return gr_mov_list + + +################################################## +metrics_dir = os.path.join(os.path.dirname(__file__), 'cell_metrics') +def get_data_metrics_for_each_subclass(ctype): + # Load csv file into dataframe + if ctype.find('_sus') >= 0: + prs_fn = os.path.join(metrics_dir, '{}_cells_v3.csv'.format(ctype)) + else: + prs_fn = os.path.join(metrics_dir, '{}_cell_data.csv'.format(ctype)) + + prs_df = pd.read_csv(prs_fn) + N_class, nmet = np.shape(prs_df) + + # Group data by subclasses based on max F0 vals + exp_df = prs_df.iloc[:, [13, 14, 17, 18, 28, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54]].copy() # Bl_lat,Wh_lat,Bl_si, wh_si, spont, 5 F0s, 5 F1s + sub_df = exp_df.iloc[:, [5, 6, 7, 8, 9]] + exp_df['max_tf'] = sub_df.idxmax(axis=1).values # sub_df.idxmax(axis=1) + + exp_means = exp_df.groupby(['max_tf']).mean() + exp_std = exp_df.groupby(['max_tf']).std() + exp_nsub = exp_df.groupby(['max_tf']).size() + + max_ind_arr = np.where(exp_nsub == np.max(exp_nsub)) + max_nsub_ind = max_ind_arr[0][0] + + # Get means and std dev for subclasses + exp_prs_dict = {} + for scn in np.arange(len(exp_nsub)): + f0_exp = exp_means.iloc[scn, 5:10].values + f1_exp = exp_means.iloc[scn, 10:].values + spont_exp = exp_means.iloc[scn, 4:5].values + if ctype.find('OFF') >= 0: + si_exp = exp_means.iloc[scn, 2:3].values + ttp_exp = exp_means.iloc[scn, 0:1].values + elif ctype.find('ON') >= 0: + si_exp = exp_means.iloc[scn, 3:4].values + ttp_exp = exp_means.iloc[scn, 1:2].values + else: + si_exp = np.NaN * np.ones((1, 5)) + ttp_exp = np.NaN * np.ones((1, 2)) + + nsub = exp_nsub.iloc[scn] + if nsub == 1: + f0_std = np.mean(exp_std.iloc[max_nsub_ind, 5:10].values) * np.ones((1, 5)) + f1_std = np.mean(exp_std.iloc[max_nsub_ind, 10:].values) * np.ones((1, 5)) + spont_std = np.mean(exp_std.iloc[max_nsub_ind, 4:5].values) * np.ones((1, 5)) + if ctype.find('OFF') >= 0: + si_std = np.mean(exp_std.iloc[max_nsub_ind, 2:3].values) * np.ones((1, 5)) + elif ctype.find('ON') >= 0: + si_std = np.mean(exp_std.iloc[max_nsub_ind, 3:4].values) * np.ones((1, 5)) + else: + si_std = np.NaN * np.ones((1, 5)) + + else: + f0_std = exp_std.iloc[scn, 5:10].values + f1_std = exp_std.iloc[scn, 10:].values + spont_std = exp_std.iloc[scn, 4:5].values + if ctype.find('OFF') >= 0: + si_std = exp_std.iloc[scn, 2:3].values + elif ctype.find('ON') >= 0: + si_std = exp_std.iloc[scn, 3:4].values + else: + si_std = np.NaN * np.ones((1, 5)) + + if ctype.find('t') >= 0: + tcross = 40. + si_inf_exp = (si_exp - tcross / 200.) * (200. / (200. - tcross - 40.)) + elif ctype.find('s') >= 0: + tcross = 60. + si_inf_exp = (si_exp - tcross / 200.) * (200. / (200. - tcross - 40.)) + + dict_key = exp_means.iloc[scn].name[3:] + exp_prs_dict[dict_key] = {} + exp_prs_dict[dict_key]['f0_exp'] = f0_exp + exp_prs_dict[dict_key]['f1_exp'] = f1_exp + exp_prs_dict[dict_key]['spont_exp'] = spont_exp + exp_prs_dict[dict_key]['si_exp'] = si_exp + exp_prs_dict[dict_key]['si_inf_exp'] = si_inf_exp + exp_prs_dict[dict_key]['ttp_exp'] = ttp_exp + exp_prs_dict[dict_key]['f0_std'] = f0_std + exp_prs_dict[dict_key]['f1_std'] = f1_std + exp_prs_dict[dict_key]['spont_std'] = spont_std + exp_prs_dict[dict_key]['si_std'] = si_std + exp_prs_dict[dict_key]['nsub'] = nsub + exp_prs_dict[dict_key]['N_class'] = N_class + + return exp_prs_dict + + +################################################## +def check_optim_results_against_bounds(bounds, opt_wts, opt_kpeaks): + bds_wts0 = bounds[0] + bds_wts1 = bounds[1] + bds_kp0 = bounds[2] + bds_kp1 = bounds[3] + + opt_wts0 = opt_wts[0] + opt_wts1 = opt_wts[1] + opt_kp0 = opt_kpeaks[0] + opt_kp1 = opt_kpeaks[1] + + if (opt_wts0 == bds_wts0[0] or opt_wts0 == bds_wts0[1]): + prm_on_bds = 'w0' + elif (opt_wts1 == bds_wts1[0] or opt_wts1 == bds_wts1[1]): + prm_on_bds = 'w1' + elif (opt_kp0 == bds_kp0[0] or opt_kp0 == bds_kp0[1]): + prm_on_bds = 'kp0' + elif (opt_kp1 == bds_kp1[0] or opt_kp1 == bds_kp1[1]): + prm_on_bds = 'kp1' + else: + prm_on_bds = 'None' + + return prm_on_bds + + +####################################################### +def get_tcross_from_temporal_kernel(temporal_kernel): + max_ind = np.argmax(temporal_kernel) + min_ind = np.argmin(temporal_kernel) + + temp_tcross_ind = mlab.cross_from_above(temporal_kernel[max_ind:min_ind], 0.0) + tcross_ind = max_ind + temp_tcross_ind[0] + return tcross_ind diff --git a/bmtk-vb/bmtk/simulator/filternet/lgnmodel/utilities.py b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/utilities.py new file mode 100644 index 0000000..69e61d9 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/lgnmodel/utilities.py @@ -0,0 +1,123 @@ +import array +import matplotlib.pyplot as plt +import skimage.transform as transform +import numpy as np +import scipy.integrate as spi +import scipy.optimize as sopt +import warnings +import scipy.interpolate as sinterp + +def get_vanhateren(filename, src_dir): + with open(filename, 'rb') as handle: + s = handle.read() + arr = array.array('H', s) + arr.byteswap() + return np.array(arr, dtype='uint16').reshape(1024, 1536) + +def convert_tmin_tmax_framerate_to_trange(t_min,t_max,frame_rate): + duration = t_max-t_min + number_of_frames = duration*frame_rate # Assumes t_min/t_max in same time units as frame_rate + dt= 1./frame_rate + return t_min+np.arange(number_of_frames+1)*dt + +def get_rotation_matrix(rotation, shape): + '''Angle in degrees''' + + shift_y, shift_x = np.array(shape) / 2. + tf_rotate = transform.SimilarityTransform(rotation=np.deg2rad(rotation)) + tf_shift = transform.SimilarityTransform(translation=[-shift_x, -shift_y]) + tf_shift_inv = transform.SimilarityTransform(translation=[shift_x, shift_y]) + return (tf_shift + (tf_rotate + tf_shift_inv)) + +def get_translation_matrix(translation): + shift_x, shift_y = translation + tf_shift = transform.SimilarityTransform(translation=[-shift_x, shift_y]) + return tf_shift + + +def get_scale_matrix(scale, shape): + shift_y, shift_x = np.array(shape) / 2. + tf_rotate = transform.SimilarityTransform(scale=(1./scale[0], 1./scale[1])) + tf_shift = transform.SimilarityTransform(translation=[-shift_x, -shift_y]) + tf_shift_inv = transform.SimilarityTransform(translation=[shift_x, shift_y]) + return tf_shift + (tf_rotate + tf_shift_inv) + +def apply_transformation_matrix(image, matrix): + return transform.warp(image, matrix) + + +def get_convolution_ind(curr_fi, flipped_t_inds, kernel, data): + + flipped_and_offset_t_inds = flipped_t_inds + curr_fi + + if np.all( flipped_and_offset_t_inds >= 0): + + # No negative entries; still might be over the end though: + try: + return np.dot(data[flipped_and_offset_t_inds], kernel) + + except IndexError: + + # Requested some indices out of range of data: + indices_within_range = np.where(flipped_and_offset_t_inds < len(data)) + valid_t_inds = flipped_and_offset_t_inds[indices_within_range] + valid_kernel = kernel[indices_within_range] + return np.dot(data[valid_t_inds], valid_kernel) + + else: + +# # Some negative entries: +# if np.all( flipped_and_offset_t_inds < 0): +# +# # All are negative: +# return 0 +# +# else: + + # Only some are negative, so restrict: + indices_within_range = np.where(flipped_and_offset_t_inds >= 0) + valid_t_inds = flipped_and_offset_t_inds[indices_within_range] + valid_kernel = kernel[indices_within_range] + + return np.dot(data[valid_t_inds], valid_kernel) + +def get_convolution(t, frame_rate, flipped_t_inds, kernel, data): + + # Get frame indices: + fi = frame_rate*float(t) + fim = int(np.floor(fi)) + fiM = int(np.ceil(fi)) + + if fim != fiM: + + # Linear interpolation: + sm = get_convolution_ind(fim, flipped_t_inds, kernel, data) + sM = get_convolution_ind(fiM, flipped_t_inds, kernel, data) + return sm*(1-(fi-fim)) + sM*(fi-fim) + + else: + + # Requested time is exactly one piece of data: + return get_convolution_ind(fim, flipped_t_inds, kernel, data) + +if __name__ == "__main__": + pass +# print generate_poisson([0,1,2,3],[.5,1,2,3]) + + + +# test_generate_poisson_function() + +# image = np.zeros((101,151)) +# image[48:52+1]=1 +# +# mr = get_rotation_matrix(30, image.shape) +# mt = get_translation_matrix((20,0)) +# ms = get_scale_matrix((.5,1),image.shape) +# +# m = mr +# +# fig, ax = plt.subplots(2,1) +# ax[0].imshow(image) +# ax[1].imshow(apply_transformation_matrix(image, m)) +# plt.show() diff --git a/bmtk-vb/bmtk/simulator/filternet/modules/__init__.py b/bmtk-vb/bmtk/simulator/filternet/modules/__init__.py new file mode 100644 index 0000000..13185dd --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/modules/__init__.py @@ -0,0 +1,2 @@ +from .record_rates import RecordRates +from .create_spikes import SpikesGenerator diff --git a/bmtk-vb/bmtk/simulator/filternet/modules/base.py b/bmtk-vb/bmtk/simulator/filternet/modules/base.py new file mode 100644 index 0000000..1bf7865 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/modules/base.py @@ -0,0 +1,9 @@ +class SimModule(object): + def initialize(self, sim): + pass + + def save(self, sim, **kwargs): + pass + + def finalize(self, sim): + pass \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/modules/create_spikes.py b/bmtk-vb/bmtk/simulator/filternet/modules/create_spikes.py new file mode 100644 index 0000000..d2acf96 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/modules/create_spikes.py @@ -0,0 +1,99 @@ +import os +import numpy as np +import random +import six + +from .base import SimModule +from bmtk.utils.io.spike_trains import SpikeTrainWriter +from bmtk.simulator.filternet.lgnmodel import poissongeneration as pg + + +class SpikesGenerator(SimModule): + def __init__(self, spikes_file_csv=None, spikes_file=None, spikes_file_nwb=None, tmp_dir='output'): + def _get_file_path(file_name): + if file_name is None or os.path.isabs(file_name): + return file_name + + return os.path.join(tmp_dir, file_name) + + self._csv_fname = _get_file_path(spikes_file_csv) + self._save_csv = spikes_file_csv is not None + + self._h5_fname = _get_file_path(spikes_file) + self._save_h5 = spikes_file is not None + + self._nwb_fname = _get_file_path(spikes_file_nwb) + self._save_nwb = spikes_file_nwb is not None + + self._tmpdir = tmp_dir + + self._spike_writer = SpikeTrainWriter(tmp_dir=tmp_dir) + + def save(self, sim, gid, times, rates): + try: + spike_trains = np.array(f_rate_to_spike_train(times*1000.0, rates, np.random.randint(10000), + 1000.*min(times), 1000.*max(times), 0.1)) + except: + # convert to milliseconds and hence the multiplication by 1000 + spike_trains = 1000.0*np.array(pg.generate_inhomogenous_poisson(times, rates, + seed=np.random.randint(10000))) + + self._spike_writer.add_spikes(times=spike_trains, gid=gid) + + def finalize(self, sim): + self._spike_writer.flush() + + if self._save_csv: + self._spike_writer.to_csv(self._csv_fname) + + if self._save_h5: + self._spike_writer.to_hdf5(self._h5_fname) + + if self._save_nwb: + self._spike_writer.to_nwb(self._nwb_fname) + + self._spike_writer.close() + + +def f_rate_to_spike_train(t, f_rate, random_seed, t_window_start, t_window_end, p_spike_max): + # t and f_rate are lists containing time stamps and corresponding firing rate values; + # they are assumed to be of the same length and ordered with the time strictly increasing; + # p_spike_max is the maximal probability of spiking that we allow within the time bin; it is used to decide on the size of the time bin; should be less than 1! + + if np.max(f_rate) * np.max(np.diff(t))/1000. > 0.1: #Divide by 1000 to convert to seconds + print('Firing rate to high for time interval and will not estimate spike correctly. Spikes will ' \ + 'be calculated with the slower inhomogenous poisson generating fucntion') + raise Exception() + + spike_times = [] + + # Use seed(...) to instantiate the random number generator. Otherwise, current system time is used. + random.seed(random_seed) + + # Assume here for each pair (t[k], f_rate[k]) that the f_rate[k] value applies to the time interval [t[k], t[k+1]). + for k in six.moves.range(0, len(f_rate)-1): + t_k = t[k] + t_k_1 = t[k+1] + if ((t_k >= t_window_start) and (t_k_1 <= t_window_end)): + delta_t = t_k_1 - t_k + # Average number of spikes expected in this interval (note that firing rate is in Hz and time is in ms). + av_N_spikes = f_rate[k] / 1000.0 * delta_t + + if (av_N_spikes > 0): + if (av_N_spikes <= p_spike_max): + N_bins = 1 + else: + N_bins = int(np.ceil(av_N_spikes / p_spike_max)) + + t_base = t[k] + t_bin = 1.0 * delta_t / N_bins + p_spike_bin = 1.0 * av_N_spikes / N_bins + for i_bin in six.moves.range(0, N_bins): + rand_tmp = random() + if rand_tmp < p_spike_bin: + spike_t = t_base + random() * t_bin + spike_times.append(spike_t) + + t_base += t_bin + + return spike_times \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/modules/record_rates.py b/bmtk-vb/bmtk/simulator/filternet/modules/record_rates.py new file mode 100644 index 0000000..b2978a3 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/modules/record_rates.py @@ -0,0 +1,29 @@ +import os +import csv + +from .base import SimModule + + +class RecordRates(SimModule): + def __init__(self, csv_file=None, h5_file=None, tmp_dir='output'): + csv_file = csv_file if csv_file is None or os.path.isabs(csv_file) else os.path.join(tmp_dir, csv_file) + self._save_to_csv = csv_file is not None + self._tmp_csv_file = csv_file if self._save_to_csv else os.path.join(tmp_dir, '__tmp_rates.csv') + + self._tmp_csv_fhandle = open(self._tmp_csv_file, 'w') + self._tmp_csv_writer = csv.writer(self._tmp_csv_fhandle, delimiter=' ') + + self._save_to_h5 = h5_file is not None + + def save(self, sim, gid, times, rates): + for t, r in zip(times, rates): + self._tmp_csv_writer.writerow([gid, t, r]) + self._tmp_csv_fhandle.flush() + + def finalize(self, sim): + if self._save_to_h5: + raise NotImplementedError + + self._tmp_csv_fhandle.close() + if not self._save_to_csv: + os.remove(self._tmp_csv_file) diff --git a/bmtk-vb/bmtk/simulator/filternet/pyfunction_cache.py b/bmtk-vb/bmtk/simulator/filternet/pyfunction_cache.py new file mode 100644 index 0000000..9ac949a --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/pyfunction_cache.py @@ -0,0 +1,98 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import types +from functools import wraps + + +class _PyFunctions(object): + """Structure for holding custom user-defined python functions. + + Will store a set of functions created by the user. Should not access this directly but rather user the + decorators or setter functions, and use the py_modules class variable to access individual functions. Is divided + up into + synaptic_weight: functions for calcuating synaptic weight. + cell_model: should return NEURON cell hobj. + synapse model: should return a NEURON synapse object. + """ + def __init__(self): + self.__cell_processors = {} + + def clear(self): + self.__cell_processors.clear() + + @property + def cell_processors(self): + return self.__cell_processors.keys() + + def cell_processor(self, name): + return self.__cell_processors[name] + + def add_cell_processor(self, name, func, overwrite=True): + if overwrite or name not in self.__cell_processors: + self.__cell_processors[name] = func + + def __repr__(self): + return self.__cell_processors + + +py_modules = _PyFunctions() + + +def cell_processor(*wargs, **wkwargs): + """A decorator for registering NEURON cell loader functions.""" + if len(wargs) == 1 and callable(wargs[0]): + # for the case without decorator arguments, grab the function object in wargs and create a decorator + func = wargs[0] + py_modules.add_cell_processor(func.__name__, func) # add function assigned to its original name + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + else: + # for the case with decorator arguments + assert(all(k in ['name'] for k in wkwargs.keys())) + + def decorator(func): + # store the function in py_modules but under the name given in the decorator arguments + py_modules.add_cell_processor(wkwargs['name'], func) + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + return decorator + + +def add_cell_processor(func, name=None, overwrite=True): + assert(callable(func)) + func_name = name if name is not None else func.__name__ + py_modules.add_cell_processor(func_name, func, overwrite) + + +def load_py_modules(cell_processors): + # py_modules.clear() + assert (isinstance(cell_processors, types.ModuleType)) + for f in [cell_processors.__dict__.get(f) for f in dir(cell_processors)]: + if isinstance(f, types.FunctionType): + py_modules.add_cell_processor(f.__name__, f) diff --git a/bmtk-vb/bmtk/simulator/filternet/transfer_functions.py b/bmtk-vb/bmtk/simulator/filternet/transfer_functions.py new file mode 100644 index 0000000..6517719 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/transfer_functions.py @@ -0,0 +1 @@ +from bmtk.simulator.filternet.lgnmodel.transferfunction import * \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/filternet/utils.py b/bmtk-vb/bmtk/simulator/filternet/utils.py new file mode 100644 index 0000000..c01045c --- /dev/null +++ b/bmtk-vb/bmtk/simulator/filternet/utils.py @@ -0,0 +1 @@ +from bmtk.simulator.filternet.lgnmodel.util_fns import * diff --git a/bmtk-vb/bmtk/simulator/mintnet/Image_Library.py b/bmtk-vb/bmtk/simulator/mintnet/Image_Library.py new file mode 100644 index 0000000..506a040 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/Image_Library.py @@ -0,0 +1,105 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import os +from PIL import Image + +# Image_Batch +# .data (image_data) +# .image_dir, .new_size + + +# add seed for random +# call should return indices into im_list +class Image_Experiment(object): + + def __init__(self,stuff): + + self.image_dir + self.new_size + self.sample_indices + self.im_list + # creating of pandas table, template + + + + + +class Image_Library (object): + def __init__(self, image_dir,new_size=(128,192)): # NOTE: change this so that sequential is a class variable, not an argument to the call + self.image_dir = image_dir + self.new_size = new_size + + im_list = os.listdir(image_dir) + + remove_list = [] + for im in im_list: + if im[-5:]!='.tiff' and im[-5:]!='.JPEG' and im[-4:]!='.jpg': + remove_list.append(im) + + for im in remove_list: + im_list.remove(im) + + self.im_list = im_list + + self.current_location = 0 # used for sequential samples + self.lib_size = len(self.im_list) + + def __call__(self,num_samples, sequential=False): + + image_data = np.zeros([num_samples,self.new_size[0],self.new_size[1],1],dtype=np.float32) + + if sequential: + if self.lib_size-self.current_location > num_samples: + sample_indices = np.arange(self.current_location,self.current_location + num_samples) + self.current_location += num_samples + else: + sample_indices = np.arange(self.current_location,self.lib_size) + self.current_location = 0 + else: + sample_indices = np.random.randint(0,len(self.im_list),num_samples) + + for i,s in enumerate(sample_indices): + im = Image.open(os.path.join(self.image_dir,self.im_list[s])) + im = im.convert('L') + im = im.resize((self.new_size[1],self.new_size[0])) + image_data[i,:,:,0] = np.array(im,dtype=np.float32) + + return image_data + + def create_experiment(self): + + data = self() + return Image_Experiment(stuff) + + def experiment_from_table(self,table): + pass + + def to_h5(self,sample_indices=None): + pass + + def template(self): + pass + + def table(self,*params): + pass diff --git a/bmtk-vb/bmtk/simulator/mintnet/Image_Library_Supervised.py b/bmtk-vb/bmtk/simulator/mintnet/Image_Library_Supervised.py new file mode 100644 index 0000000..756b62b --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/Image_Library_Supervised.py @@ -0,0 +1,93 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from PIL import Image +import numpy as np +import os + +class Image_Library_Supervised (object): + + def __init__(self,image_dir,new_size=(256,256)): + + self.categories = os.listdir(image_dir) + + self.num_categories = len(self.categories) #len(image_dir_list) + self.image_dir_list = [os.path.join(image_dir,x) for x in self.categories] + self.new_size = new_size + + + # self.categories = [] + # for d in self.image_dir_list: + # self.categories += [os.path.basename(d)] + + self.im_lists = {} + for i,cat in enumerate(self.categories): + d = self.image_dir_list[i] + if os.path.basename(d[0])=='.': continue + self.im_lists[cat] = os.listdir(d) + + for cat in self.im_lists: + remove_list = [] + for im in self.im_lists[cat]: + if im[-4:]!='.jpg': + remove_list.append(im) + + for im in remove_list: + self.im_lists[cat].remove(im) + + + self.current_location = np.zeros(len(self.categories)) # used for sequential samples + self.lib_size = [len(self.im_lists[x]) for x in self.categories] + #self.lib_size = len(self.im_list) + + def __call__(self,num_samples,sequential=False): + + image_data = np.zeros([self.num_categories*num_samples,self.new_size[0],self.new_size[1],1],dtype=np.float32) + + # y_vals = np.tile(np.arange(self.num_categories),(num_samples,1)).T.flatten() + # y_vals = y_vals.astype(np.float32) + + y_vals = np.zeros([num_samples*self.num_categories,self.num_categories],np.float32) + + for i,cat in enumerate(self.categories): + + y_vals[num_samples*i:num_samples*i+num_samples].T[i] = 1 + + if sequential: + if self.lib_size[i]-self.current_location[i] > num_samples: + sample_indices = np.arange(self.current_location[i],self.current_location[i] + num_samples,dtype=np.int64) + self.current_location[i] += num_samples + else: + sample_indices = np.arange(self.current_location[i],self.lib_size[i],dtype=np.int64) + self.current_location[i] = 0 + else: + sample_indices = np.random.randint(0,len(self.im_lists[cat]),num_samples) + + for j,s in enumerate(sample_indices): + im = Image.open(os.path.join(self.image_dir_list[i],self.im_lists[cat][s])) + im = im.convert('L') + im = im.resize((self.new_size[1],self.new_size[0])) + index = j + num_samples*i + image_data[index,:,:,0] = np.array(im,dtype=np.float32) + + return y_vals, image_data + diff --git a/bmtk-vb/bmtk/simulator/mintnet/__init__.py b/bmtk-vb/bmtk/simulator/mintnet/__init__.py new file mode 100644 index 0000000..04c8f88 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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/bmtk-vb/bmtk/simulator/mintnet/analysis/LocallySparseNoise.py b/bmtk-vb/bmtk/simulator/mintnet/analysis/LocallySparseNoise.py new file mode 100644 index 0000000..60b9228 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/analysis/LocallySparseNoise.py @@ -0,0 +1,105 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import pandas as pd +import h5py + + +class LocallySparseNoise (object): + + def __init__(self,data_file_name): + + self.stim_table = pd.read_hdf(data_file_name,'stim_table') + self.node_table = pd.read_hdf(data_file_name,'node_table') + + + self.data_file_name = data_file_name + + data = h5py.File(self.data_file_name,'r') + + self.data_sets = data.keys() + self.data_sets.remove('stim_table') + self.data_sets.remove('node_table') + self.data_sets.remove('stim_template') + + self.stim_template = data['stim_template'].value + + data.close() + + @staticmethod + def rf(response, stim_template, stim_shape): + T = stim_template.shape[0] + rf_shape = tuple(stim_template.shape[1:]) + + unit_shape = tuple(response.shape[1:]) + + response.resize([T,np.prod(unit_shape)]) + + rf = np.dot(response.T,stim_template) + + rf_new_shape = tuple([rf.shape[0]] + list(rf_shape)) + rf.resize(rf_new_shape) + rf_final_shape = tuple(list(unit_shape) + list(stim_shape)) + rf.resize(rf_final_shape) + + return rf + + def compute_receptive_fields(self, dtype=np.float32): + + output = h5py.File(self.data_file_name[:-3]+'_analysis.ic','a') + data = h5py.File(self.data_file_name,'r') + + # convert to +/-1 or 0 + stim_template = data['stim_template'].value.astype(dtype) + stim_template = stim_template-127 + stim_template = np.sign(stim_template) + #print np.unique(stim_template) + + stim_shape = tuple(stim_template.shape[1:]) + T = stim_template.shape[0] + + stim_template.resize([T,np.prod(stim_shape)]) + + stim_template_on = stim_template.copy() + stim_template_off = stim_template.copy() + + stim_template_on[stim_template_on<0] = 0.0 + stim_template_off[stim_template_off>0] = 0.0 + + for data_set in self.data_sets: + + response = data[data_set].value + response = response - np.mean(response,axis=0) + + key_onoff = data_set+'/lsn/on_off' + key_on = data_set+'/lsn/on' + key_off = data_set+'/lsn/off' + for key in [key_onoff, key_on, key_off]: + if key in output: + del output[key] + + output[key_onoff] = self.rf(response, stim_template, stim_shape) + output[key_on] = self.rf(response, stim_template_on, stim_shape) + output[key_off] = self.rf(response, stim_template_off, stim_shape) + + data.close() diff --git a/bmtk-vb/bmtk/simulator/mintnet/analysis/StaticGratings.py b/bmtk-vb/bmtk/simulator/mintnet/analysis/StaticGratings.py new file mode 100644 index 0000000..10a019b --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/analysis/StaticGratings.py @@ -0,0 +1,101 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import pandas as pd +import h5py +import sys +import os + +class StaticGratings (object): + + def __init__(self,data_file_name): + + self.stim_table = pd.read_hdf(data_file_name,'stim_table') + self.node_table = pd.read_hdf(data_file_name,'node_table') + self.tunings_file = None + + f = lambda label: self.stim_table.dropna().drop_duplicates([label])[label].sort_values(inplace=False).values + + self.orientations = f('orientation') + self.spatial_frequencies = f('spatial_frequency') + self.phases = f('phase') + + self.data_file_name = data_file_name + + data = h5py.File(self.data_file_name,'r') + + self.data_sets = data.keys() + self.data_sets.remove('stim_table') + self.data_sets.remove('node_table') + self.data_sets.remove('stim_template') + + data.close() + + def tuning_matrix(self, response, dtype=np.float32): + + tuning_shape = tuple([len(self.orientations), len(self.spatial_frequencies), len(self.phases)] + list(response.shape[1:])) + + tuning_matrix = np.empty(tuning_shape, dtype=dtype) + + for i,ori in enumerate(self.orientations): + for j,sf in enumerate(self.spatial_frequencies): + for k,ph in enumerate(self.phases): + + index = self.stim_table[(self.stim_table.spatial_frequency==sf) & (self.stim_table.orientation==ori) & (self.stim_table.phase==ph)].index + + tuning_matrix[i,j,k] = np.mean(response[index],axis=0) + + return tuning_matrix + + def compute_all_tuning(self, dtype=np.float32, force=False): + self.tunings_file = self.data_file_name[:-3]+'_analysis.ic' + if os.path.exists(self.tunings_file) and not force: + print('Using existing tunings file {}.'.format(self.tunings_file)) + return + + output = h5py.File(self.tunings_file,'a') + data = h5py.File(self.data_file_name,'r') + + for i, data_set in enumerate(self.data_sets): + sys.stdout.write( '\r{0:.02f}'.format(float(i)*100/len(self.data_sets))+'% done') + sys.stdout.flush() + + response = data[data_set].value + + tuning = self.tuning_matrix(response, dtype=dtype) + + key = data_set+'/sg/tuning' + if key in output: + del output[key] + output[key] = tuning + + sys.stdout.write( '\r{0:.02f}'.format(float(100))+'% done') + sys.stdout.flush() + + data.close() + + def get_tunings_file(self): + if self.tunings_file is None: + self.compute_all_tuning() + + return h5py.File(self.tunings_file, 'r') \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/mintnet/analysis/__init__.py b/bmtk-vb/bmtk/simulator/mintnet/analysis/__init__.py new file mode 100644 index 0000000..2d56a26 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/analysis/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/C_Layer.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/C_Layer.py new file mode 100644 index 0000000..1489c89 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/C_Layer.py @@ -0,0 +1,260 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import tensorflow as tf +import os +import pandas as pd + +class C_Layer (object): + def __init__(self,node_name,S_Layer_input,bands): + ''' + :type S_Layer: S_Layer object + :param S_Layer: instance of S_Layer object that serves as input for this C_Layer + + :type bands: list + :param bands: bands[i] = [[list of frequency indices for S_layer over which to pool], grid_size, sample_step] + ''' + self.node_name = node_name + self.input = S_Layer_input.input + + self.tf_sess = S_Layer_input.tf_sess + + s_output = S_Layer_input.output + + self.K = S_Layer_input.K + + band_output = {} + + num_bands = len(bands) + + self.band_output = {} + + self.band_shape = {} + + with tf.name_scope(self.node_name): + for b in range(num_bands): + bands_to_pool, grid_size, sample_step = bands[b] + + sub_band_shape = [] + for sub_band in bands_to_pool: + sub_band_shape += [S_Layer_input.band_shape[sub_band]] + + max_band_shape = sub_band_shape[0] + for shape in sub_band_shape[1:]: + if shape[0] > max_band_shape[0]: max_band_shape[0] = shape[0] + if shape[1] > max_band_shape[1]: max_band_shape[1] = shape[1] + + # print "max_band_shape = ", max_band_shape + # for sub_band in bands_to_pool: + # print "\tsub_band_shape = ", S_Layer_input.band_shape[sub_band] + # print "\tinput band shape = ", s_output[sub_band].get_shape() + + #resize all inputs to highest resolution so that we can maxpool over equivalent scales + resize_ops = [] + for sub_band in bands_to_pool: + op = s_output[sub_band] + # resize_ops += [tf.image.resize_images(op,max_band_shape[0],max_band_shape[1],method=ResizeMethod.NEAREST_NEIGHBOR)] + resize_ops += [tf.image.resize_nearest_neighbor(op,max_band_shape)] + #print "\tresize op shape = ", resize_ops[-1].get_shape() + + #take the maximum for each input channel, element-wise + max_channel_op = resize_ops[0] + for op in resize_ops[1:]: + max_channel_op = tf.maximum(op,max_channel_op) + + #print "\tmax channel op shape = ", max_channel_op.get_shape() + + # new shape for mode 'SAME' + # new_band_shape = (max_band_shape[0]/sample_step, max_band_shape[1]/sample_step) + new_band_shape = np.ceil(np.array(max_band_shape)/float(sample_step)).astype(np.int64) + + # make sure the grid_size and sample_step aren't bigger than the image + if max_band_shape[0] < grid_size: + y_size = max_band_shape[0] + else: + y_size = grid_size + + if max_band_shape[1] < grid_size: + x_size = max_band_shape[1] + else: + x_size = grid_size + + if sample_step > max_band_shape[0]: + y_step = max_band_shape[0] + new_band_shape = (1,new_band_shape[1]) + else: + y_step = sample_step + if sample_step > max_band_shape[1]: + x_step = max_band_shape[1] + new_band_shape = (new_band_shape[0],1) + else: + x_step = sample_step + + # max pool + max_pool_op = tf.nn.max_pool(max_channel_op,[1,y_size,x_size,1],strides=[1,y_step,x_step,1],padding='SAME') + + self.band_shape[b] = new_band_shape + #print "max_band_shape: ", max_band_shape + + self.band_output[b]=max_pool_op + + self.num_units = 0 + for b in self.band_shape: + self.num_units += np.prod(self.band_shape[b])*self.K + + self.output = self.band_output + + def __repr__(self): + return "C_Layer" + + def compute_output(self,X,band): + return self.tf_sess.run(self.output[band],feed_dict={self.input:X}) + + # def get_compute_ops(self): + # + # node_table = pd.DataFrame(columns=['node','band']) + # compute_list = [] + # + # for band in self.band_output: + # node_table = node_table.append(pd.DataFrame([[self.node_name,band]],columns=['node','band']),ignore_index=True) + # + # compute_list.append(self.output[band]) + # + # return node_table, compute_list + + def get_compute_ops(self,unit_table=None): + + compute_list = [] + + if unit_table is not None: + + for i, row in unit_table.iterrows(): + + if 'y' in unit_table: + node, band, y, x = row['node'], int(row['band']), int(row['y']), int(row['x']) + compute_list.append(self.output[band][:,y,x,:]) + + elif 'band' in unit_table: + node, band = row['node'], int(row['band']) + compute_list.append(self.output[band]) + + else: + return self.get_all_compute_ops() + + else: + return self.get_all_compute_ops() + + return unit_table, compute_list + + def get_all_compute_ops(self): + + compute_list = [] + unit_table = pd.DataFrame(columns=['node','band']) + for band in self.band_output: + unit_table = unit_table.append(pd.DataFrame([[self.node_name,band]],columns=['node','band']),ignore_index=True) + + compute_list.append(self.output[band]) + + return unit_table, compute_list + + +def test_C1_Layer(): + + from S1_Layer import S1_Layer + import matplotlib.pyplot as plt + + fig_dir = 'Figures' + # First we need an S1 Layer + # these parameters are taken from Serre, et al PNAS for HMAX + freq_channel_params = [ [7,2.8,3.5], + [9,3.6,4.6], + [11,4.5,5.6], + [13,5.4,6.8], + [15,6.3,7.9], + [17,7.3,9.1], + [19,8.2,10.3], + [21,9.2,11.5], + [23,10.2,12.7], + [25,11.3,14.1], + [27,12.3,15.4], + [29,13.4,16.8], + [31,14.6,18.2], + [33,15.8,19.7], + [35,17.0,21.2], + [37,18.2,22.8], + [39,19.5,24.4]] + + orientations = np.arange(4)*np.pi/4 + + input_shape = (128,192) + s1 = S1_Layer(input_shape,freq_channel_params,orientations) + + # Now we need to define a C1 Layer + bands = [ [[0,1], 8, 3], + [[2,3], 10, 5], + [[4,5], 12, 7], + [[6,7], 14, 8], + [[8,9], 16, 10], + [[10,11], 18, 12], + [[12,13], 20, 13], + [[14,15,16], 22, 15]] + + c1 = C_Layer(s1,bands) + + # Test c1 on an image + from isee_engine.mintnet.Image_Library import Image_Library + + image_dir = '/Users/michaelbu/Code/HCOMP/SampleImages' + + im_lib = Image_Library(image_dir) + + image_data = im_lib(1) + + fig, ax = plt.subplots(1) + ax.imshow(image_data[0,:,:,0],cmap='gray') + + print(image_data.shape) + + fig, ax = plt.subplots(len(bands),len(orientations)*2) + result = {} + for b in range(len(bands)): + result[b] = c1.compute_output(image_data,b) + print(result[b].shape) + n, y,x,K = result[b].shape + + for k in range(K): + #print result[b][i].shape + # y = i/8 + # x = i%8 + # ax[y,x].imshow(result[b][0,i],interpolation='nearest',cmap='gray') + # ax[y,x].axis('off') + + ax[b,k].imshow(result[b][0,:,:,k],interpolation='nearest',cmap='gray') + ax[b,k].axis('off') + + fig.savefig(os.path.join(fig_dir,'c1_layer.tiff')) + plt.show() + +if __name__=='__main__': + + test_C1_Layer() diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/Readout_Layer.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/Readout_Layer.py new file mode 100644 index 0000000..9126ea1 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/Readout_Layer.py @@ -0,0 +1,243 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import tensorflow as tf +from bmtk.simulator.mintnet.Image_Library_Supervised import Image_Library_Supervised +import h5py + +class Readout_Layer (object): + + def __init__(self,node_name,input_layer,K,lam,alt_image_dir='',file_name=None): + + self.node_name = node_name + self.K = K + self.input_layer = input_layer + self.weight_file = file_name + self.lam = lam + + self.alt_image_dir = alt_image_dir + + if file_name==None: + new_weights=True + self.train_state = False + else: + + weight_h5 = h5py.File(self.weight_file,'a') + file_open=True + + if self.node_name in weight_h5.keys(): + + new_weights = False + weight_data = weight_h5[self.node_name]['weights'].value + self.train_state = weight_h5[self.node_name]['train_state'].value + + else: + + new_weights = True + self.train_state =False + weight_h5.create_group(self.node_name) + weight_h5[self.node_name]['train_state']=self.train_state + + self.input = self.input_layer.input + #self.tf_sess = self.input_layer.tf_sess + self.tf_sess = tf.Session() + + self.w_shape = (self.input_layer.K,self.K) + + if new_weights: + #weights=1.0*np.ones(self.w_shape).astype(np.float32) + weights=100000*np.random.normal(size=self.w_shape).astype(np.float32) + if file_name!=None: + weight_h5[self.node_name].create_dataset('weights',shape=weights.shape,dtype=np.float32,compression='gzip',compression_opts=9) + weight_h5[self.node_name]['weights'][...]=weights + else: + weights=weight_data + + self.weights = tf.Variable(weights.astype(np.float32),trainable=True,name='weights') + self.weights.initializer.run(session=self.tf_sess) + self.bias = tf.Variable(np.zeros(self.K,dtype=np.float32),trainable=True,name='bias') + self.bias.initializer.run(session=self.tf_sess) + + # sigmoid doesn't seem to work well, and is slow + #self.output = tf.sigmoid(tf.matmul(self.input_layer.output,W)+self.bias) + + self.input_placeholder = tf.placeholder(tf.float32,shape=(None,self.input_layer.K)) + #self.output = tf.nn.softmax(tf.matmul(self.input_placeholder,self.weights) + self.bias) + self.linear = tf.matmul(self.input_placeholder,self.weights) #+ self.bias + + self.output = tf.sign(self.linear) + #self.output = tf.nn.softmax(self.linear) + #self.output = tf.nn.softmax(tf.matmul(self.input_layer.output,self.weights) + self.bias) + + self.y = tf.placeholder(tf.float32,shape=(None,self.K)) + + + #self.cost = -tf.reduce_mean(self.y*tf.log(self.output)) + self.cost = tf.reduce_mean((self.y - self.output)**2) + self.lam*(tf.reduce_sum(self.weights))**2 + + # not gonna do much with current cost function :) + self.train_step = tf.train.GradientDescentOptimizer(0.1).minimize(self.cost) + + self.num_units = self.K + + if file_open: + weight_h5.close() + + def compute_output(self,X): + + #return self.tf_sess.run(self.output,feed_dict={self.input:X}) + + rep = self.input_layer.tf_sess.run(self.input_layer.output,feed_dict={self.input:X}) + + return self.tf_sess.run(self.output,feed_dict={self.input_placeholder:rep}) + + def predict(self,X): + + y_vals = self.compute_output(X) + + return np.argmax(y_vals,axis=1) + + def train(self,image_dir,batch_size=10,image_shape=(256,256),max_iter=200): + + print("Training") + + im_lib = Image_Library_Supervised(image_dir,new_size=image_shape) + + # let's use the linear regression version for now + training_lib_size = 225 + y_vals, image_data = im_lib(training_lib_size,sequential=True) + + y_vals = y_vals.T[0].T + y_vals = 2*y_vals - 1.0 + + print(y_vals) + # print y_vals + # print image_data.shape + + # import matplotlib.pyplot as plt + # plt.imshow(image_data[0,:,:,0]) + # plt.figure() + # plt.imshow(image_data[1,:,:,0]) + # plt.figure() + # plt.imshow(image_data[9,:,:,0]) + + # plt.show() + + num_batches = int(np.ceil(2*training_lib_size/float(batch_size))) + rep_list = [] + for i in range(num_batches): + print(i) + # if i==num_batches-1: + # rep = self.input_layer.tf_sess.run(self.input_layer.output,feed_dict={self.input:image_data[i*batch_size:i*batch_size + training_lib_size%batch_size]}) + # else: + rep = self.input_layer.tf_sess.run(self.input_layer.output,feed_dict={self.input:image_data[i*batch_size:(i+1)*batch_size]}) + rep_list += [rep] + + rep = np.vstack(rep_list) + + + C = np.dot(rep.T,rep) + self.lam*np.eye(self.input_layer.K) + W = np.dot(np.linalg.inv(C),np.dot(rep.T,y_vals)).astype(np.float32) + + self.tf_sess.run(self.weights.assign(tf.expand_dims(W,1))) + + train_result = self.tf_sess.run(self.output,feed_dict={self.input_placeholder:rep}) + + print(W) + print(train_result.flatten()) + print(y_vals.flatten()) + #print (train_result.flatten() - y_vals.flatten()) + print("train error = ", np.mean((train_result.flatten() != y_vals.flatten()))) + + from scipy.stats import norm + target_mask = y_vals==1 + dist_mask = np.logical_not(target_mask) + hit_rate = np.mean(train_result.flatten()[target_mask] == y_vals.flatten()[target_mask]) + false_alarm = np.mean(train_result.flatten()[dist_mask] != y_vals.flatten()[dist_mask]) + dprime = norm.ppf(hit_rate) - norm.ppf(false_alarm) + print("dprime = ", dprime) + + # Test error + im_lib = Image_Library_Supervised('/Users/michaelbu/Data/SerreOlivaPoggioPNAS07/Train_Test_Set/Test',new_size=image_shape) + + testing_lib_size = 300 + y_vals_test, image_data_test = im_lib(testing_lib_size,sequential=True) + + y_vals_test = y_vals_test.T[0].T + y_vals_test = 2*y_vals_test - 1.0 + + num_batches = int(np.ceil(2*testing_lib_size/float(batch_size))) + rep_list = [] + for i in range(num_batches): + print(i) + # if i==num_batches-1: + # rep = self.input_layer.tf_sess.run(self.input_layer.output,feed_dict={self.input:image_data[i*batch_size:i*batch_size + training_lib_size%batch_size]}) + # else: + rep = self.input_layer.tf_sess.run(self.input_layer.output,feed_dict={self.input:image_data_test[i*batch_size:(i+1)*batch_size]}) + rep_list += [rep] + + rep_test = np.vstack(rep_list) + + test_result = self.tf_sess.run(self.output,feed_dict={self.input_placeholder:rep_test}) + + #print test_result + print("test error = ", np.mean((test_result.flatten() != y_vals_test.flatten()))) + target_mask = y_vals_test==1 + dist_mask = np.logical_not(target_mask) + hit_rate = np.mean(test_result.flatten()[target_mask] == y_vals_test.flatten()[target_mask]) + false_alarm = np.mean(test_result.flatten()[dist_mask] != y_vals_test.flatten()[dist_mask]) + dprime = norm.ppf(hit_rate) - norm.ppf(false_alarm) + print("dprime = ", dprime) + + print(rep_test.shape) + + + # logistic regression unit + # import time + # for n in range(max_iter): + # start = time.time() + # print "\tIteration ", n + + # y_vals, image_data = im_lib(batch_size,sequential=True) + + # print "\tComputing representation" + # rep = self.input_layer.tf_sess.run(self.input_layer.output,feed_dict={self.input:image_data}) + + # print "\tGradient descent step" + # #print "rep shape = ", rep.shape + # self.tf_sess.run(self.train_step,feed_dict={self.input_placeholder:rep,self.y:y_vals}) + + + # #self.tf_sess.run(self.train_step,feed_dict={self.input:image_data,self.y:y_vals}) + + # #print "\t\ttraining batch cost = ", self.tf_sess.run(self.cost,feed_dict={self.input:image_data,self.y:y_vals}) + + # print "\t\tTraining error = ", np.mean(np.abs(np.argmax(y_vals,axis=1) - self.predict(image_data))) + # print y_vals + # print + # print self.predict(image_data) + # print "\t\ttraining batch cost = ", self.tf_sess.run(self.cost,feed_dict={self.input_placeholder:rep,self.y:y_vals}) + # print "\t\ttraining linear model = ", self.tf_sess.run(self.linear,feed_dict={self.input_placeholder:rep,self.y:y_vals}) + + # print "\t\ttotal time = ", time.time() - start + diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/S1_Layer.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/S1_Layer.py new file mode 100644 index 0000000..44bed67 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/S1_Layer.py @@ -0,0 +1,273 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import tensorflow as tf +import os +import pandas as pd + +def gabor(X,Y,lamb,sigma,theta,gamma,phase): + + X_hat = X*np.cos(theta) + Y*np.sin(theta) + Y_hat = -X*np.sin(theta) + Y*np.cos(theta) + + arg1 = (0.5/sigma**2)*(X_hat**2 + (gamma**2)*Y_hat**2) + arg2 = (2.0*np.pi/lamb)*X_hat + + return np.exp(-arg1)*np.cos(arg2 + phase) + +class S1_Layer (object): + def __init__(self,node_name,input_shape,freq_channel_params,orientations): #,num_cores=8): + ''' + freq_channel_params is a dictionary of features for each S1 channel + len(freq_channel_params) ==num_bands freq_channel_params[i] = [pixels,sigma,lambda,stride] + orientations is a list of angles in radians for each filter + ''' + #self.tf_sess = tf.Session() + + self.node_name = node_name +# NUM_CORES = num_cores # Choose how many cores to use. +# NUM_CORES = 1 +# self.tf_sess = tf.Session(config=tf.ConfigProto(inter_op_parallelism_threads=NUM_CORES, +# intra_op_parallelism_threads=NUM_CORES)) + self.tf_sess = tf.Session() +# print "Warning: Using hard-coded number of CPU Cores. This should be changed to auto-configure when TensorFlow has been updated." + + self.input_shape = (None,input_shape[0],input_shape[1],1) + self.input = tf.placeholder(tf.float32,shape=self.input_shape,name="input") + + #phases = np.array([0, np.pi/2]) + phases = np.array([0.0]) # HMAX uses dense tiling in lieu of phases (make this explicit later) + + num_bands = len(freq_channel_params) + num_orientations = len(orientations) + num_phases = len(phases) + self.K = num_orientations*num_phases #number of features per band + + #n_output = num_frequency_channels*num_orientations*num_phases + + n_input = 1 + + self.band_filters = {} + self.filter_params = {} + self.band_output = {} + self.output = self.band_output + self.band_shape = {} + + with tf.name_scope(self.node_name): + for band in range(num_bands): + pixels, sigma, lamb, stride = freq_channel_params[band] + self.band_shape[band] = input_shape + + w_shape = np.array([pixels,pixels,n_input,self.K]) + + W = np.zeros(w_shape,dtype=np.float32) + + #compute w values from parameters + gamma = 0.3 # value taken from Serre et al giant HMAX manuscript from 2005 + X,Y = np.meshgrid(np.arange(pixels),np.arange(pixels)) + X = X - pixels/2 + Y = Y - pixels/2 + + #self.filter_params[band] = freq_channel_params[band] + self.filter_params[band] = {'pixels':pixels,'sigma':sigma,'lambda':lamb, 'stride':stride} #should I add orientations and phases to this? + + for i in range(self.K): + + ori_i = i%num_orientations + phase_i = i/num_orientations + + theta = orientations[ori_i] + phase = phases[phase_i] + + zero_mask = np.zeros([pixels,pixels],dtype='bool') + zero_mask = (X*X + Y*Y > pixels*pixels/4) + + W[:,:,0,i] = gabor(X,Y,lamb,sigma,theta,gamma,phase) + W[:,:,0,i][zero_mask] = 0.0 + W[:,:,0,i] = W[:,:,0,i]/np.sqrt(np.sum(W[:,:,0,i]**2)) + + W = tf.Variable(W,trainable=False,name='W_'+str(band)) + W.initializer.run(session=self.tf_sess) + + self.band_filters[band] = W + + input_norm = tf.reshape(tf.reduce_sum(self.input*self.input,[1,2,3]),[-1,1,1,1]) + normalized_input = tf.div(self.input,tf.sqrt(input_norm)) + self.band_output[band] = tf.nn.conv2d(normalized_input,W,strides=[1,stride,stride,1],padding='SAME') + self.band_shape[band] = tuple([int(x) for x in self.band_output[band].get_shape()[1:3]]) + + + self.num_units = 0 + for b in self.band_shape: + self.num_units += np.prod(self.band_shape[band])*self.K + + def __del__(self): + self.tf_sess.close() + + def __repr__(self): + return "S1_Layer" + + def compute_output(self,X,band): + + return self.tf_sess.run(self.output[band],feed_dict={self.input:X}) + + def get_compute_ops(self,unit_table=None): + + compute_list = [] + + if unit_table is not None: + + for i, row in unit_table.iterrows(): + + if 'y' in unit_table: + node, band, y, x = row['node'], int(row['band']), int(row['y']), int(row['x']) + compute_list.append(self.output[band][:,y,x,:]) + + elif 'band' in unit_table: + node, band = row['node'], int(row['band']) + compute_list.append(self.output[band]) + + else: + return self.get_all_compute_ops() + + else: + return self.get_all_compute_ops() + + return unit_table, compute_list + + def get_all_compute_ops(self): + + compute_list = [] + unit_table = pd.DataFrame(columns=['node','band']) + for band in self.band_output: + unit_table = unit_table.append(pd.DataFrame([[self.node_name,band]],columns=['node','band']),ignore_index=True) + + compute_list.append(self.output[band]) + + return unit_table, compute_list + +def S1_Layer_test(): + + import matplotlib.pyplot as plt + + fig_dir = 'Figures' + + # these parameters are taken from Serre, et al PNAS for HMAX + freq_channel_params = [ [7,2.8,3.5], + [9,3.6,4.6], + [11,4.5,5.6], + [13,5.4,6.8], + [15,6.3,7.9], + [17,7.3,9.1], + [19,8.2,10.3], + [21,9.2,11.5], + [23,10.2,12.7], + [25,11.3,14.1], + [27,12.3,15.4], + [29,13.4,16.8], + [31,14.6,18.2], + [33,15.8,19.7], + [35,17.0,21.2], + [37,18.2,22.8], + [39,19.5,24.4]] + + orientations = np.arange(4)*np.pi/4 + + input_shape = (128,192) + s1 = S1_Layer(input_shape,freq_channel_params,orientations) + + #plot filters, make sure they are correct + fig, ax = plt.subplots(len(orientations),len(freq_channel_params)) + fig2,ax2 = plt.subplots(len(orientations),len(freq_channel_params)) + for i,theta in enumerate(orientations): + for j,params in enumerate(freq_channel_params): + + #index = j*len(orientations)*2 + i*2 + + fil = s1.tf_sess.run(s1.band_filters[j])[:,:,0,i] + + ax[i,j].imshow(fil,interpolation='nearest',cmap='gray') + ax[i,j].axis('off') + + fil = s1.tf_sess.run(s1.band_filters[j])[:,:,0,i+4] + + ax2[i,j].imshow(fil,interpolation='nearest',cmap='gray') + ax2[i,j].axis('off') + + + from Image_Library import Image_Library + + image_dir = '/Users/michaelbu/Code/HCOMP/SampleImages' + + im_lib = Image_Library(image_dir) + + image_data = im_lib(1) + + fig, ax = plt.subplots(1) + ax.imshow(image_data[0,:,:,0],cmap='gray') + + import timeit + #print timeit.timeit('result = s1.compute_output(image_data)','from __main__ import s1',number=10) + + def f(): + for band in range(len(freq_channel_params)): + s1.compute_output(image_data,band) + + number = 10 + runs = timeit.Timer(f).repeat(repeat=10,number=number) + print("Average time (s) for output evaluation for ", number, " runs: ", np.mean(runs)/number, '+/-', np.std(runs)/np.sqrt(number)) + + + + print("Image shape = ", image_data.shape) + + + fig_r, ax_r = plt.subplots(len(orientations),len(freq_channel_params)) + fig_r2,ax_r2 = plt.subplots(len(orientations),len(freq_channel_params)) + + for j,params in enumerate(freq_channel_params): + + result = s1.compute_output(image_data,j) + print("result shape = ", result.shape) + + for i,theta in enumerate(orientations): + + #fil = np.zeros([39,39]) + #index = j*len(orientations)*2 + i*2 + #print s1.params[0] + + ax_r[i,j].imshow(result[0,:,:,i],interpolation='nearest',cmap='gray') + ax_r[i,j].axis('off') + + ax_r2[i,j].imshow(result[0,:,:,i+4],interpolation='nearest',cmap='gray') + ax_r2[i,j].axis('off') + + fig_r.savefig(os.path.join(fig_dir,'s1_layer_0.tiff')) + fig_r2.savefig(os.path.join(fig_dir,'s1_layer_1.tiff')) + plt.show() + + #sess.close() + +if __name__=='__main__': + + S1_Layer_test() diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/S_Layer.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/S_Layer.py new file mode 100644 index 0000000..df10f08 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/S_Layer.py @@ -0,0 +1,404 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import tensorflow as tf +from bmtk.simulator.mintnet.Image_Library import Image_Library +import os +import h5py +import pandas as pd + +class S_Layer (object): + def __init__(self, node_name, C_Layer_input, grid_size, pool_size, K, file_name=None, randomize=False): + self.node_name = node_name + + self.input = C_Layer_input.input + + self.tf_sess = C_Layer_input.tf_sess + #self.input_layer = C_Layer_input + # c_output should be a dictionary indexed over bands + + c_output = C_Layer_input.output + self.C_Layer_input = C_Layer_input + + self.K = K + self.input_K = C_Layer_input.K + self.grid_size = grid_size + self.pool_size = pool_size + + self.band_output = {} + #self.band_filters = {} + self.band_shape = C_Layer_input.band_shape + #print self.band_shape + + file_open = False + if file_name==None: + self.train_state=False + new_weights = True + else: + + self.weight_file = file_name + + weight_h5 = h5py.File(self.weight_file, 'a') + file_open = True + + if self.node_name in weight_h5.keys(): + + new_weights=False + weight_data = weight_h5[self.node_name]['weights'] + self.train_state = weight_h5[self.node_name]['train_state'].value + + else: + + new_weights=True + self.train_state = False + weight_h5.create_group(self.node_name) + #weight_h5[self.node_name].create_group('weights') + weight_h5[self.node_name]['train_state']=self.train_state + + + + # perform checks to make sure weight_file is consistent with the Layer parameters + # check input bands + # check grid_size, pool_size, K + + with tf.name_scope(self.node_name): + #for band in c_output.keys(): + + if new_weights: + + # if self.grid_size >= self.band_shape[band][0]: + # size_y = self.band_shape[band][0] + # else: + # size_y = grid_size + # if self.grid_size >= self.band_shape[band][1]: + # size_x = self.band_shape[band][1] + # else: + # size_x = grid_size + + w_shape = np.array([self.grid_size,self.grid_size,self.input_K,self.K]) + + self.w_shape = w_shape + + w_bound = np.sqrt(np.prod(w_shape[1:])) + if randomize: + W = np.random.uniform(low= -1.0/w_bound, high=1.0/w_bound, size=w_shape).astype(np.float32) + else: + W = np.zeros(w_shape).astype(np.float32) + + if file_name!=None: + weight_h5[self.node_name].create_dataset('weights',shape=w_shape,dtype=np.float32) + + else: + # Need to check that c_output.keys() has the same set of keys that weight_dict is expecting + W = weight_data.value + self.w_shape = W.shape + + + + + W = tf.Variable(W,trainable=False,name='W') + W.initializer.run(session=self.tf_sess) + + #self.band_filters[band]= W + self.weights = W + + for band in c_output.keys(): + W_slice = W[:self.band_shape[band][0],:self.band_shape[band][1]] + + input_norm = tf.expand_dims(tf.reduce_sum(c_output[band]*c_output[band],[1,2]),1) #,[-1,1,1,self.input_K]) + input_norm = tf.expand_dims(input_norm,1) + normalized_input = tf.div(c_output[band],tf.maximum(tf.sqrt(input_norm),1e-12)) + self.band_output[band] = tf.nn.conv2d(normalized_input,W_slice,strides=[1,1,1,1],padding='SAME') + + self.output = self.band_output + + self.num_units = 0 + for b in self.band_shape: + self.num_units += np.prod(self.band_shape[b])*self.K + + if file_open: + weight_h5.close() + + def __repr__(self): + return "S_Layer" + + def compute_output(self,X,band): + + return self.tf_sess.run(self.output[band],feed_dict={self.input:X}) + + def find_band_and_coords_for_imprinting_unit(self, imprinting_unit_index): + + cumulative_units = 0 + for band in self.C_Layer_input.output: + + units_in_next_band = int(np.prod(self.C_Layer_input.output[band].get_shape()[1:3])) + + if imprinting_unit_index < cumulative_units + units_in_next_band: + # found the right band! + yb, xb = self.C_Layer_input.band_shape[band] + + band_index = imprinting_unit_index - cumulative_units + + y = band_index/xb + x = band_index%xb + break + else: + cumulative_units += units_in_next_band + + return band, y, x + + + + def get_total_pixels_in_C_Layer_input(self): + + total = 0 + + band_shape = self.C_Layer_input.band_shape + band_ids = band_shape.keys() + band_ids.sort() + + for band in band_ids: + total += np.prod(band_shape[band]) + + return total + + + def get_patch_bounding_box_and_shift(self,band,y,x): + y_lower = y - self.grid_size/2 + y_upper = y_lower + self.grid_size + + x_lower = x - self.grid_size/2 + x_upper = x_lower + self.grid_size + + yb, xb = self.C_Layer_input.band_shape[band] + + # compute shifts in lower bound to deal with overlap with the edges + y_shift_lower = np.max([-y_lower,0]) + x_shift_lower = np.max([-x_lower,0]) + + + y_lower = np.max([y_lower,0]) + y_upper = np.min([y_upper,yb]) + + x_lower = np.max([x_lower,0]) + x_upper = np.min([x_upper,xb]) + + y_shift_upper = y_shift_lower + y_upper - y_lower + x_shift_upper = x_shift_lower + x_upper - x_lower + + return y_lower, y_upper, x_lower, x_upper, y_shift_lower, y_shift_upper, x_shift_lower, x_shift_upper + + def train(self,image_dir,batch_size=100,image_shape=(256,256)): #,save_file='weights.pkl'): + + print("Training") + + im_lib = Image_Library(image_dir,new_size=image_shape) + + new_weights = np.zeros(self.w_shape).astype(np.float32) + + + for k in range(self.K): + + if k%10==0: + print("Imprinting feature ", k) + # how to handle the randomly picked neuron; rejection sampling? + imprinting_unit_index = np.random.randint(self.get_total_pixels_in_C_Layer_input()) + + #print "Imprinting unit index ", imprinting_unit_index + band, y, x = self.find_band_and_coords_for_imprinting_unit(imprinting_unit_index) + #print "Imprinting unit in band ", band, " at ", (y, x) + + im_data = im_lib(1) + + output = self.C_Layer_input.compute_output(im_data,band) + + # grab weights from chosen unit, save them to new_weights + y_lower, y_upper, x_lower, x_upper, y_shift_lower, y_shift_upper, x_shift_lower, x_shift_upper = self.get_patch_bounding_box_and_shift(band,y,x) + + w_patch = output[0,y_lower:y_upper,x_lower:x_upper,:].copy() + + #print "(y_lower, y_upper), (x_lower, x_upper) = ", (y_lower, y_upper), (x_lower, x_upper) + #print "Patch shape = ", w_patch.shape + + patch_size = np.prod(w_patch.shape) + # print "self.w_shape = ", self.w_shape, " patch_size = ", patch_size, " pool_size = ", self.pool_size + # print "band, y, x = ", band,y,x + + pool_size = np.min([self.pool_size,patch_size]) + pool_mask_indices = np.random.choice(np.arange(patch_size), size=pool_size, replace=False) + pool_mask = np.zeros(patch_size,dtype=np.bool) + pool_mask[pool_mask_indices] = True + pool_mask.resize(w_patch.shape) + pool_mask = np.logical_not(pool_mask) # we want a mask for the indices to zero out + + w_patch[pool_mask] = 0.0 + + # will need to enlarge w_patch if the edges got truncated + + new_weights[y_shift_lower:y_shift_upper,x_shift_lower:x_shift_upper,:,k] = w_patch + + + # old code starts here + # num_batches = self.K/batch_size + # if self.K%batch_size!=0: + # num_batches = num_batches+1 + + self.tf_sess.run(self.weights.assign(new_weights)) + print() + print("Saving weights to file in ", self.weight_file) + + weight_h5 = h5py.File(self.weight_file,'a') + #for band in new_weights: + weight_h5[self.node_name]['weights'][...] = new_weights + weight_h5[self.node_name]['train_state'][...]=True + + weight_h5.close() + + # def get_compute_ops(self): + # + # node_table = pd.DataFrame(columns=['node','band']) + # compute_list = [] + # + # for band in self.band_output: + # node_table = node_table.append(pd.DataFrame([[self.node_name,band]],columns=['node','band']),ignore_index=True) + # + # compute_list.append(self.output[band]) + # + # return node_table, compute_list + + def get_compute_ops(self,unit_table=None): + + compute_list = [] + + if unit_table is not None: + + for i, row in unit_table.iterrows(): + + if 'y' in unit_table: + node, band, y, x = row['node'], int(row['band']), int(row['y']), int(row['x']) + compute_list.append(self.output[band][:,y,x,:]) + + elif 'band' in unit_table: + node, band = row['node'], int(row['band']) + compute_list.append(self.output[band]) + + else: + return self.get_all_compute_ops() + + else: + return self.get_all_compute_ops() + + return unit_table, compute_list + + def get_all_compute_ops(self): + + compute_list = [] + unit_table = pd.DataFrame(columns=['node','band']) + for band in self.band_output: + unit_table = unit_table.append(pd.DataFrame([[self.node_name,band]],columns=['node','band']),ignore_index=True) + + compute_list.append(self.output[band]) + + return unit_table, compute_list + + +def test_S_Layer_ouput(): + + from S1_Layer import S1_Layer + import matplotlib.pyplot as plt + from C_Layer import C_Layer + + fig_dir = 'Figures' + # First we need an S1 Layer + # these parameters are taken from Serre, et al PNAS for HMAX + freq_channel_params = [ [7,2.8,3.5], + [9,3.6,4.6], + [11,4.5,5.6], + [13,5.4,6.8], + [15,6.3,7.9], + [17,7.3,9.1], + [19,8.2,10.3], + [21,9.2,11.5], + [23,10.2,12.7], + [25,11.3,14.1], + [27,12.3,15.4], + [29,13.4,16.8], + [31,14.6,18.2], + [33,15.8,19.7], + [35,17.0,21.2], + [37,18.2,22.8], + [39,19.5,24.4]] + + orientations = np.arange(4)*np.pi/4 + + input_shape = (128,192) + s1 = S1_Layer(input_shape,freq_channel_params,orientations) + + # Now we need to define a C1 Layer + bands = [ [[0,1], 8, 3], + [[2,3], 10, 5], + [[4,5], 12, 7], + [[6,7], 14, 8], + [[8,9], 16, 10], + [[10,11], 18, 12], + [[12,13], 20, 13], + [[14,15,16], 22, 15]] + + c1 = C_Layer(s1,bands) + + grid_size = 3 + pool_size = 10 + K = 10 + + s2 = S_Layer('s2',c1,grid_size,pool_size,K,file_name='S_test_file.h5',randomize=False) + + # Test s2 on an image + image_dir = '/Users/michaelbu/Code/HCOMP/SampleImages' + + im_lib = Image_Library(image_dir,new_size=input_shape) + + image_data = im_lib(1) + + fig, ax = plt.subplots(1) + ax.imshow(image_data[0,:,:,0],cmap='gray') + + fig,ax = plt.subplots(8,10) + + result = {} + for b in range(len(bands)): + result[b] = s2.compute_output(image_data,b) + + for k in range(K): + ax[b,k].imshow(result[b][0,:,:,k],interpolation='nearest',cmap='gray') + ax[b,k].axis('off') + + fig.savefig(os.path.join(fig_dir,'s2_layer.tiff')) + plt.show() + + s2.train(image_dir,batch_size=10,image_shape=input_shape) #,save_file='test_weights.pkl') + + + + +if __name__=='__main__': + test_S_Layer_ouput() diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/Sb_Layer.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/Sb_Layer.py new file mode 100644 index 0000000..4731323 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/Sb_Layer.py @@ -0,0 +1,242 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import tensorflow as tf +from S_Layer import S_Layer +import pandas as pd + +class Sb_Layer (object): + def __init__(self,node_name,C_Layer_input,grid_size,pool_size,K_per_subband,file_name=None): + '''grid_size is a list, unlike the standard S_Layer, as is file_names''' + + self.node_name = node_name + self.tf_sess = C_Layer_input.tf_sess + + self.input = C_Layer_input.input + + self.num_sublayers = len(grid_size) + self.K = K_per_subband*self.num_sublayers #number of features will be number of sub bands times the K per subband + self.pool_size = pool_size + self.grid_size = grid_size + + c_output = C_Layer_input.output + + self.sublayers = {} + with tf.name_scope(self.node_name): + for i in range(self.num_sublayers): + subnode_name = node_name+'_'+str(i) + self.sublayers[i] = S_Layer(subnode_name,C_Layer_input,grid_size[i],pool_size,K_per_subband,file_name) + + self.band_output = {} + self.band_shape = C_Layer_input.band_shape + + for band in c_output.keys(): + + sub_band_list = [] + for i in range(self.num_sublayers): + sub_band_list += [self.sublayers[i].band_output[band]] + + + + #gather sub_layer outputs and stack them for each band + self.band_output[band] = tf.concat(sub_band_list, 3) + + self.output = self.band_output + + self.num_units = 0 + for b in self.band_shape: + self.num_units += np.prod(self.band_shape[b])*self.K + + def __repr__(self): + return "Sb_Layer" + + def compute_output(self,X,band): + + return self.tf_sess.run(self.output[band],feed_dict={self.input:X}) + + def train(self,image_dir,batch_size=100,image_shape=(256,256)): #,save_file_prefix='weights'): + + for i in range(self.num_sublayers): + #save_file = save_file_prefix + '_'+str(i)+'.pkl' + + #try: + self.sublayers[i].train(image_dir,batch_size,image_shape) #,save_file) + #except Exception as e: + # print i + # raise e + + # def get_compute_ops(self): + # + # node_table = pd.DataFrame(columns=['node','band']) + # compute_list = [] + # + # for band in self.band_output: + # node_table = node_table.append(pd.DataFrame([[self.node_name,band]],columns=['node','band']),ignore_index=True) + # + # compute_list.append(self.output[band]) + # + # return node_table, compute_list + + def get_compute_ops(self,unit_table=None): + + compute_list = [] + + if unit_table is not None: + + for i, row in unit_table.iterrows(): + + if 'y' in unit_table: + node, band, y, x = row['node'], int(row['band']), int(row['y']), int(row['x']) + compute_list.append(self.output[band][:,y,x,:]) + + elif 'band' in unit_table: + node, band = row['node'], int(row['band']) + compute_list.append(self.output[band]) + + else: + return self.get_all_compute_ops() + + else: + return self.get_all_compute_ops() + + return unit_table, compute_list + + def get_all_compute_ops(self): + + compute_list = [] + unit_table = pd.DataFrame(columns=['node','band']) + for band in self.band_output: + unit_table = unit_table.append(pd.DataFrame([[self.node_name,band]],columns=['node','band']),ignore_index=True) + + compute_list.append(self.output[band]) + + return unit_table, compute_list + + +def test_S2b_Layer(): + + from S1_Layer import S1_Layer + import matplotlib.pyplot as plt + from C_Layer import C_Layer + + fig_dir = 'Figures' + # First we need an S1 Layer + # these parameters are taken from Serre, et al PNAS for HMAX + freq_channel_params = [ [7,2.8,3.5], + [9,3.6,4.6], + [11,4.5,5.6], + [13,5.4,6.8], + [15,6.3,7.9], + [17,7.3,9.1], + [19,8.2,10.3], + [21,9.2,11.5], + [23,10.2,12.7], + [25,11.3,14.1], + [27,12.3,15.4], + [29,13.4,16.8], + [31,14.6,18.2], + [33,15.8,19.7], + [35,17.0,21.2], + [37,18.2,22.8], + [39,19.5,24.4]] + + orientations = np.arange(4)*np.pi/4 + + input_shape = (128,192) + s1 = S1_Layer(input_shape,freq_channel_params,orientations) + + # Now we need to define a C1 Layer + bands = [ [[0,1], 8, 3], + [[2,3], 10, 5], + [[4,5], 12, 7], + [[6,7], 14, 8], + [[8,9], 16, 10], + [[10,11], 18, 12], + [[12,13], 20, 13], + [[14,15,16], 22, 15]] + + c1 = C_Layer(s1,bands) + + print("s1 shape: ", s1.band_shape) + print("c1 shape: ", c1.band_shape) + + grid_size = [6,9,12,15] + pool_size = 10 + K = 10 + + s2b = Sb_Layer(c1,grid_size,pool_size,K) + + print("s2b shape: ", s2b.band_shape) + + c2b_bands = [ [[0,1,2,3,4,5,6,7],40,40]] + + c2b = C_Layer(s2b,c2b_bands) + + + print("c2b shape: ", c2b.band_shape) + #print c2b.band_output.keys() + # Test s2 on an image + from Image_Library import Image_Library + + image_dir = '/Users/michaelbu/Code/HCOMP/SampleImages' + + im_lib = Image_Library(image_dir,new_size=input_shape) + + image_data = im_lib(1) + + fig, ax = plt.subplots(1) + ax.imshow(image_data[0,:,:,0],cmap='gray') + + fig,ax = plt.subplots(8,10) + + result = {} + for b in range(len(bands)): + result[b] = s2b.compute_output(image_data,b) + + for k in range(K): + ax[b,k].imshow(result[b][0,:,:,k],interpolation='nearest',cmap='gray') + ax[b,k].axis('off') + + fig.savefig(os.path.join(fig_dir,'s2b_layer.tiff')) + + fig,ax = plt.subplots(8,10) + + result = {} + + #only one band for c2b + result[0] = c2b.compute_output(image_data,0) + + for k in range(K): + ax[b,k].imshow(result[0][0,:,:,k],interpolation='nearest',cmap='gray') + ax[b,k].axis('off') + + fig.savefig(os.path.join(fig_dir,'c2b_layer.tiff')) + + + #plt.show() + + s2b.train(image_dir,batch_size=10,image_shape=input_shape,save_file_prefix='test_S2b_weights') + +if __name__=='__main__': + + test_S2b_Layer() diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/ViewTunedLayer.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/ViewTunedLayer.py new file mode 100644 index 0000000..1ae95e1 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/ViewTunedLayer.py @@ -0,0 +1,219 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import tensorflow as tf +from bmtk.simulator.mintnet.Image_Library import Image_Library +#from bmtk.mintnet.Stimulus.NaturalScenes import NaturalScenes +import h5py +import pandas as pd + +class ViewTunedLayer (object): + def __init__(self,node_name,K,alt_image_dir='',*inputs,**keyword_args): + + self.node_name=node_name + + file_name = keyword_args.get('file_name',None) + + self.alt_image_dir = alt_image_dir + + if file_name==None: + print("No filename given. Generating new (random) weights for layer ", node_name) + self.train_state = False + new_weights=True + else: + + self.weight_file = file_name + weight_h5 = h5py.File(self.weight_file,'a') + file_open=True + + if self.node_name in weight_h5.keys(): + + #print "Loading weights for layer ", node_name, " from ", self.weight_file + new_weights = False + weight_data = weight_h5[self.node_name]['weights'].value + self.train_state = weight_h5[self.node_name]['train_state'] + + else: + + new_weights=True + self.train_state=False + weight_h5.create_group(self.node_name) + weight_h5[self.node_name]['train_state']=self.train_state + + self.input = inputs[0].input + self.tf_sess = inputs[0].tf_sess + #should add a check that all inputs have the same value of inputs[i].input + + self.K = K + + concat_list = [] + total_K = 0 + + with tf.name_scope(self.node_name): + for i, node in enumerate(inputs): + + output_i = node.output + + for b in output_i: + shape = node.band_shape[b] + + num_K = np.prod(shape)*node.K + total_K = total_K + num_K + #print "shape = ", shape, " total_K = ", num_K + reshape_op = tf.reshape(output_i[b],[-1,num_K]) + concat_list += [reshape_op] + + self.input_unit_vector = tf.concat(concat_list, 1) #shape [batch_size, total_K] + + self.w_shape = (total_K,K) + #weight = np.random.normal(size=self.w_shape).astype(np.float32) + if new_weights: + weight = np.zeros(self.w_shape).astype(np.float32) + weight_h5[self.node_name].create_dataset('weights',shape=weight.shape,dtype=np.float32,compression='gzip',compression_opts=9) + else: + weight = weight_data #ict['ViewTunedWeight'] + assert weight.shape[0]==total_K, "weights from file are not equal to total input size for layer "+self.node_name + + + self.weights = tf.Variable(weight,trainable=False,name='weights') + self.weights.initializer.run(session=self.tf_sess) + + #print self.input_unit_vector.get_shape(), total_K + #should this be a dictionary for consistency? + #print "input unit vector shape = ", self.input_unit_vector.get_shape() + #print "total_K = ", total_K + + input_norm = tf.expand_dims(tf.reduce_sum(self.input_unit_vector*self.input_unit_vector,[1]),1) #,[-1,total_K]) + normalized_input = tf.div(self.input_unit_vector,tf.sqrt(input_norm)) + self.output = tf.matmul(normalized_input,self.weights) #/0.01 + + # try gaussian tuning curve centered on preferred feature + # self.output = tf.exp(-0.5*tf.reduce_sum(self.weights - self.input_unit_vector)) + + self.num_units = K + + if file_open: + weight_h5.close() + + def __repr__(self): + return "ViewTunedLayer" + + def compute_output(self,X): + + return self.tf_sess.run(self.output,feed_dict={self.input:X}) + + def train(self,image_dir,batch_size=10,image_shape=(256,256)): #,save_file=None): + + print("Training") + + im_lib = Image_Library(image_dir,new_size=image_shape) + + #ns_lib = NaturalScenes.with_new_stimulus_from_folder(image_dir, new_size=image_shape, add_channels=True) + + new_weights = np.zeros(self.w_shape,dtype=np.float32) + + num_batches = self.K/batch_size + + for n in range(num_batches): + #for k in range(self.K): + print("\t\tbatch: ", n, " Total features: ", n*batch_size) + print("\t\t\tImporting images for batch") + image_data = im_lib(batch_size,sequential=True) + print("\t\t\tDone") + + print("\t\t\tComputing responses for batch") + batch_output = self.tf_sess.run(self.input_unit_vector,feed_dict={self.input:image_data}) + new_weights[:,n*batch_size:(n+1)*batch_size] = batch_output.T + + print("\t\t\tDone") + + if self.K%batch_size!=0: + last_batch_size = self.K%batch_size + print("\t\tbatch: ", n+1, " Total features: ", (n+1)*batch_size) + print("\t\t\tImporting images for batch") + image_data = im_lib(last_batch_size,sequential=True) + print("\t\t\tDone") + + print("\t\t\tComputing responses for batch") + batch_output = self.tf_sess.run(self.input_unit_vector,feed_dict={self.input:image_data}) + new_weights[:,-last_batch_size:] = batch_output.T + + new_weights = new_weights/np.sqrt(np.maximum(np.sum(new_weights**2,axis=0),1e-12)) + + self.tf_sess.run(self.weights.assign(new_weights)) + + print("") + print("Saving weights to file ", self.weight_file) + weight_h5 = h5py.File(self.weight_file,'a') + weight_h5[self.node_name]['weights'][...] = new_weights + weight_h5[self.node_name]['train_state'][...] = True + weight_h5.close() + + def get_compute_ops(self,unit_table=None): + + compute_list = [] + + if unit_table is not None: + for i, row in unit_table.iterrows(): + compute_list = [self.output] + + else: + unit_table = pd.DataFrame([[self.node_name]], columns=['node']) + compute_list = [self.output] + + return unit_table, compute_list + + + +def test_ViewTunedLayer(): + + from hmouse_test import hmouse + + image_dir = '/Users/michaelbu/Code/H-MOUSE/ILSVRC2015/Data/DET/test' + image_shape = (256,256) + weight_file_prefix = 'S2b_weights_500' + + print("Configuring HMAX network") + hm = hmouse('config/nodes.csv','config/node_types.csv') + + for node in hm.nodes: + print(node, " num_units = ", hm.nodes[node].num_units) + + s4 = ViewTunedLayer(10,hm.nodes['c1'],hm.nodes['c2'],hm.nodes['c2b']) #,hm.nodes['c3']) + + im_lib = Image_Library(image_dir,new_size=image_shape) + image_data = im_lib(1) + + print(s4.tf_sess.run(tf.shape(s4.input_unit_vector),feed_dict={s4.input:image_data})) + print(s4.tf_sess.run(tf.shape(s4.weights))) + + print(s4.compute_output(image_data).shape) + + #s4.train(image_dir,batch_size=10,image_shape=image_shape,save_file='s4_test_weights.pkl') + + + + +if __name__=='__main__': + + test_ViewTunedLayer() diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/__init__.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/__init__.py new file mode 100644 index 0000000..44200f2 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import S_Layer +import S1_Layer +import Sb_Layer +import C_Layer +import ViewTunedLayer +import hmax diff --git a/bmtk-vb/bmtk/simulator/mintnet/hmax/hmax.py b/bmtk-vb/bmtk/simulator/mintnet/hmax/hmax.py new file mode 100644 index 0000000..c770ec6 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/mintnet/hmax/hmax.py @@ -0,0 +1,432 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import sys +import json +from S1_Layer import S1_Layer +from C_Layer import C_Layer +from S_Layer import S_Layer +from Sb_Layer import Sb_Layer +from ViewTunedLayer import ViewTunedLayer +from Readout_Layer import Readout_Layer +import tensorflow as tf +import os +import h5py +import pandas as pd + +from bmtk.simulator.mintnet.Image_Library import Image_Library +import matplotlib.pyplot as plt + +class hmax (object): + + def __init__(self, configuration, name=None): #,num_cores=8): + self.name = name + + if os.path.isdir(configuration): + # If configuration is a directory look for a config-file inside it. + self.config_file = os.path.join(configuration, 'config_' + configuration + '.json') + if self.name is None: + self.name = os.path.basename(configuration) + + elif os.path.isfile(configuration): + # If configuration is a json file + if self.name is None: + raise Exception("A name is required for configuration parameters") + self.config_file = configuration + + with open(self.config_file,'r') as f: + self.config_data = json.loads(f.read()) + + self.config_dir = os.path.dirname(os.path.abspath(configuration)) + self.train_state_file = self.__get_config_file(self.config_data['train_state_file']) + self.image_dir = self.__get_config_file(self.config_data['image_dir']) + + # Find, and create if necessary, the output directory + if 'output_dir' in self.config_data: + self.output_dir = self.__get_config_file(self.config_data['output_dir']) + else: + self.output_dir = os.path.join(self.config_dir, 'output') + + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) + + with open(self.train_state_file, 'r') as f: + self.train_state = json.loads(f.read()) + + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) + + # get the nodes + models_file = self.__get_config_file(self.config_data['network']['node_types']) + nodes_file = self.__get_config_file(self.config_data['network']['nodes']) + self.__nodes_table = self.__build_nodes_table(nodes_file, models_file, self.config_data) + + # Read the connections + self.nodes = {} + self.train_order = [] + + edges_file = self.__get_config_file(self.config_data['network']['edges']) + for (node_name, input_node, node_dict) in self.__get_edges(edges_file, self.config_data): + model_class = self.__nodes_table[node_name]['model_id'] + + print("Constructing node: ", node_name) + if model_class=='S1_Layer': + node_type = S1_Layer + freq_channel_params = node_dict['freq_channel_params'] + input_shape = node_dict['input_shape'] + self.input_shape = input_shape + orientations = node_dict['orientations'] + + self.nodes[node_name] = node_type(node_name,input_shape,freq_channel_params,orientations) #,num_cores=num_cores) + #writer = tf.train.SummaryWriter('tmp/hmax', self.nodes['s1'].tf_sess.graph_def) + #merged = tf.merge_all_summaries() + + #writer.add_summary(self.nodes[node_name].tf_sess.run(merged),0) + + elif model_class=='C_Layer': + node_type = C_Layer + bands = node_dict['bands'] + + + self.nodes[node_name] = node_type(node_name,self.nodes[input_node],bands) + #writer = tf.train.SummaryWriter('tmp/hmax', self.nodes['s1'].tf_sess.graph_def) + + elif model_class=='S_Layer': + node_type = S_Layer + K = node_dict['K'] + weight_file = self.__get_config_file(node_dict['weight_file']) if 'weight_file' in node_dict else None + pool_size = node_dict['pool_size'] + grid_size = node_dict['grid_size'] + self.train_order += [node_name] + + self.nodes[node_name] = node_type(node_name, self.nodes[input_node], grid_size, pool_size,K, + file_name=weight_file) + + elif model_class=='Sb_Layer': + node_type = Sb_Layer + K = node_dict['K'] + weight_file = self.__get_config_file(node_dict['weight_file']) if 'weight_file' in node_dict else None + pool_size = node_dict['pool_size'] + grid_size = node_dict['grid_size'] + + self.train_order += [node_name] + + self.nodes[node_name] = node_type(node_name,self.nodes[input_node],grid_size,pool_size,K,file_name=weight_file) + + elif model_class=='ViewTunedLayer': + node_type = ViewTunedLayer + K = node_dict['K'] + input_nodes = node_dict['inputs'] + input_nodes = [self.nodes[node] for node in input_nodes] + weight_file = self.__get_config_file(node_dict['weight_file']) if 'weight_file' in node_dict else None + alt_image_dir = node_dict['alt_image_dir'] + + self.train_order += [node_name] + + #print "alt_image_dir=",alt_image_dir + self.nodes[node_name] = node_type(node_name,K,alt_image_dir,*input_nodes,file_name=weight_file) + + elif model_class=='Readout_Layer': + node_type = Readout_Layer + K = node_dict['K'] + input_nodes = self.nodes[input_node] + weight_file = os.path.join(config_dir,node_dict['weight_file']) + if weight_file=='': weight_file=None + alt_image_dir = node_dict['alt_image_dir'] + lam = node_dict['lam'] + + self.train_order += [node_name] + + self.nodes[node_name] = node_type(node_name,self.nodes[input_node],K,lam,alt_image_dir,file_name=weight_file) + + else: + raise Exception("Unknown model class {}".format(model_class)) + + # print "Done" + # print + + #nfhandle.close() + + + + self.node_names = self.nodes.keys() + + self.input_shape = (self.nodes['s1'].input_shape[1], self.nodes['s1'].input_shape[2]) + + print("Done") + #writer = tf.train.SummaryWriter('tmp/hmax', self.nodes['s1'].tf_sess.graph_def) + + + def __build_nodes_table(self, nodes_csv, models_csv, config): + models_df = pd.read_csv(models_csv, sep=' ') + nodes_df = pd.read_csv(nodes_csv, sep=' ') + nodes_df.set_index('id') + nodes_full = pd.merge(left=nodes_df, right=models_df, on='model_id') + nodes_table = {r['id']: {'model_id': r['model_id'], 'python_object': r['python_object']} + for _, r in nodes_full.iterrows() } + + return nodes_table + + def __get_edges(self, edges_csv, config): + def parse_query(query_str): + if query_str == '*' or query_str == 'None': + return None + elif query_str.startswith('id=='): + return query_str[5:-1] + else: + raise Exception('Unknown query string {}'.format(query_str)) + + # location where config files are located + params_dir = self.__get_config_file(config.get('node_config_dir', '')) + + edges_df = pd.read_csv(edges_csv, sep=' ') + edges = [] + for _, row in edges_df.iterrows(): + # find source and target + source = parse_query(row['source_query']) + target = parse_query(row['target_query']) + + # load the parameters from the file + params_file = os.path.join(params_dir, row['params_file']) + params = json.load(open(params_file, 'r')) + + # Add to list + edges.append((target, source, params)) + + # TODO: check list and reorder to make sure the layers are in a valid order + + # return the edges. Should we use a generator? + return edges + + def __get_config_file(self, fpath): + if os.path.isabs(fpath): + return fpath + else: + return os.path.join(self.config_dir, fpath) + + + + @classmethod + def load(cls, config_dir, name=None): + return cls(config_dir, name) + + def train(self): #,alt_image_dict=None): + + for node in self.train_order: + if not self.train_state.get(node, False): + print("Training Node: ", node) + + if hasattr(self.nodes[node],'alt_image_dir') and self.nodes[node].alt_image_dir!='': + print("\tUsing alternate image directory: ", self.nodes[node].alt_image_dir) # alt_image_dict[node] + self.nodes[node].train(self.nodes[node].alt_image_dir,batch_size=self.config_data['batch_size'],image_shape=self.input_shape) + self.train_state[node]=True + else: + print("\tUsing default image directory: ", self.image_dir) + self.nodes[node].train(self.image_dir,batch_size=self.config_data['batch_size'],image_shape=self.input_shape) + self.train_state[node]=True + + + # if node not in alt_image_dict: + # print "\tUsing default image directory: ", image_dir + # self.nodes[node].train(image_dir,batch_size=self.config_data['batch_size'],image_shape=self.input_shape) + # self.train_state[node]=True + # else: + # print "\tUsing alternate image directory: ", alt_image_dict[node] + # self.nodes[node].train(alt_image_dict[node],batch_size=self.config_data['batch_size'],image_shape=self.input_shape) + # self.train_state[node]=True + + print("Done") + + with open(self.config_data['train_state_file'], 'w') as f: + f.write(json.dumps(self.train_state)) + + + def run_stimulus(self,stimulus, node_table=None, output_file='output'): + '''stimulus is an instance of one of the mintnet.Stimulus objects, i.e. LocallySparseNoise''' + + if output_file[-3:]!=".ic": + output_file = output_file+".ic" # add *.ic suffix if not already there + + stim_template = stimulus.get_image_input(new_size=self.input_shape, add_channels=True) + + print("Creating new output file: ", output_file, " (and removing any previous one)") + if os.path.exists(output_file): + os.remove(output_file) + output_h5 = h5py.File(output_file,'w') + + T, y, x, K = stim_template.shape + all_nodes = self.nodes.keys() + + if node_table is None: # just compute everything and return it all; good luck! + + new_node_table = pd.DataFrame(columns=['node','band']) + + compute_list = [] + for node in all_nodes: + + add_to_node_table, new_compute_list = self.nodes[node].get_compute_ops() + new_node_table = new_node_table.append(add_to_node_table,ignore_index=True) + compute_list += new_compute_list + else: + compute_list = [] + + new_node_table = node_table.sort_values('node') + new_node_table = new_node_table.reindex(np.arange(len(new_node_table))) + + for node in all_nodes: + unit_table = new_node_table[node_table['node']==node] + if (new_node_table['node']==node).any(): + _, new_compute_list = self.nodes[node].get_compute_ops(unit_table=unit_table) + + compute_list += new_compute_list + + + # create datasets in hdf5 file from node_table, with data indexed by table index + for i, row in new_node_table.iterrows(): + + output_shape = tuple([T] + [ int(x) for x in compute_list[i].get_shape()[1:]]) + output_h5.create_dataset(str(i), output_shape, dtype=np.float32) + + + + batch_size = self.config_data['batch_size'] + num_batches = T/batch_size + if T%self.config_data['batch_size']!=0: + num_batches += 1 + + for i in range(num_batches): + sys.stdout.write( '\r{0:.02f}'.format(float(i)*100/num_batches)+'% done') + sys.stdout.flush() + output_list = self.nodes[all_nodes[0]].tf_sess.run(compute_list,feed_dict={self.nodes[all_nodes[0]].input: stim_template[i*batch_size:(i+1)*batch_size]}) + + for io, output in enumerate(output_list): + # dataset_string = node_table['node'].loc[io] + "/" + str(int(node_table['band'].loc[io])) + # output_h5[dataset_string][i*batch_size:(i+1)*batch_size] = output + + output_h5[str(io)][i*batch_size:(i+1)*batch_size] = output + sys.stdout.write( '\r{0:.02f}'.format(float(100))+'% done') + sys.stdout.flush() + + output_h5['stim_template'] = stimulus.stim_template + output_h5.close() + new_node_table.to_hdf(output_file,'node_table') + if hasattr(stimulus,'label_dataframe') and stimulus.label_dataframe is not None: + stimulus.label_dataframe.to_hdf(output_file,'labels') + stimulus.stim_table.to_hdf(output_file,'stim_table') + + + def get_exemplar_node_table(self): + + node_table = pd.DataFrame(columns=['node','band','y','x']) + for node in self.nodes: + node_output = self.nodes[node].output + if hasattr(self.nodes[node],'band_shape'): + for band in node_output: + y,x = [int(x) for x in node_output[band].get_shape()[1:3]] + y /= 2 + x /= 2 + new_row = pd.DataFrame([[self.nodes[node].node_name, band, y, x]], columns=['node','band','y','x']) + node_table = node_table.append(new_row, ignore_index=True) + else: + new_row = pd.DataFrame([[self.nodes[node].node_name]], columns=['node']) + node_table = node_table.append(new_row, ignore_index=True) + + return node_table + + + def generate_output(self): + try: + im_lib = Image_Library(self.image_dir,new_size=self.input_shape) + except OSError as e: + print('''A repository of images (such as a collection from ImageNet - http://www.image-net.org) is required for input. + An example would be too large to include in the isee_engine itself. + Set the path for this image repository in hmax/config_hmax.json''') + raise e + + image_data = im_lib(1) + + fig, ax = plt.subplots(1) + ax.imshow(image_data[0,:,:,0],cmap='gray') + + fig.savefig(os.path.join(self.output_dir,'input_image')) + plt.close(fig) + + nodes = self.nodes + + for node_to_plot in nodes: + print("Generating output for node ", node_to_plot) + node_output_dir = os.path.join(self.output_dir,node_to_plot) + + if not os.path.exists(node_output_dir): + os.makedirs(node_output_dir) + + if type(self.nodes[node_to_plot])==ViewTunedLayer: + print("ViewTunedLayer") + self.nodes[node_to_plot].compute_output(image_data) + continue + + if type(self.nodes[node_to_plot])==Readout_Layer: + print("Readout_Layer") + self.nodes[node_to_plot].compute_output(image_data) + continue + + num_bands = len(nodes[node_to_plot].output) + + if type(self.nodes[node_to_plot])==S1_Layer or node_to_plot=='c1': + #print "Yes, this is an S1_Layer" + num_filters_to_plot = 4 + fig, ax = plt.subplots(num_filters_to_plot,num_bands,figsize=(20,8)) + #fig2,ax2 = plt.subplots(num_filters_to_plot,num_bands,figsize=(20,8)) + else: + num_filters_to_plot = 8 + fig, ax = plt.subplots(num_filters_to_plot,num_bands,figsize=(20,8)) + + for band in range(num_bands): + result = nodes[node_to_plot].compute_output(image_data,band) + #print result[band].shape + n, y,x,K = result.shape + + for k in range(num_filters_to_plot): + + if num_bands!=1: + ax[k,band].imshow(result[0,:,:,k],interpolation='nearest',cmap='gray') + ax[k,band].axis('off') + else: + ax[k].imshow(result[0,:,:,k],interpolation='nearest',cmap='gray') + ax[k].axis('off') + + # if type(self.nodes[node_to_plot])==S1_Layer: + # for k in range(num_filters_to_plot): + + # ki = 4+k + # ax2[k,band].imshow(result[0,:,:,ki],interpolation='nearest',cmap='gray') + # ax2[k,band].axis('off') + + if type(self.nodes[node_to_plot])==S1_Layer: + fig.savefig(os.path.join(node_output_dir,'output_phase0.pdf')) + #fig2.savefig(os.path.join(node_output_dir,'output_phase1.pdf')) + #plt.close(fig2) + else: + fig.savefig(os.path.join(node_output_dir,'output.pdf')) + + plt.close(fig) diff --git a/bmtk-vb/bmtk/simulator/pointnet/__init__.py b/bmtk-vb/bmtk/simulator/pointnet/__init__.py new file mode 100644 index 0000000..2ad957d --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from . import default_setters +from .config import Config +from .pointnetwork import PointNetwork +from .pointsimulator import PointSimulator \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/pointnet/config.py b/bmtk-vb/bmtk/simulator/pointnet/config.py new file mode 100644 index 0000000..a6644d5 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/config.py @@ -0,0 +1,48 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json + +from bmtk.simulator.core.config import ConfigDict +from bmtk.simulator.pointnet.io_tools import io + + +# TODO: Implement pointnet validator and create json schema for pointnet +def from_json(config_file, validate=False): + conf_dict = ConfigDict.from_json(config_file) + conf_dict.io = io + return conf_dict + +def from_dict(config_file, validate=False): + conf_dict = ConfigDict.from_dict(config_file) + conf_dict.io = io + return conf_dict + +class Config(ConfigDict): + def __init__(self, dict_obj): + super(Config, self).__init__(dict_obj) + self._io = io + + @property + def io(self): + return io \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/pointnet/default_setters/__init__.py b/bmtk-vb/bmtk/simulator/pointnet/default_setters/__init__.py new file mode 100644 index 0000000..b07cc2d --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/default_setters/__init__.py @@ -0,0 +1,2 @@ +from . import synaptic_weights +from . import synapse_models \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/pointnet/default_setters/synapse_models.py b/bmtk-vb/bmtk/simulator/pointnet/default_setters/synapse_models.py new file mode 100644 index 0000000..8e94328 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/default_setters/synapse_models.py @@ -0,0 +1,16 @@ +from bmtk.simulator.pointnet.pyfunction_cache import add_synapse_model + + +def static_synapse(edge): + model_params = { + 'model': 'static_synapse', + 'delay': edge.delay, + 'weight': edge.syn_weight(None, None) + } + + model_params.update(edge.dynamics_params) + return model_params + + +add_synapse_model(static_synapse, 'default', overwrite=False) +add_synapse_model(static_synapse, overwrite=False) \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/pointnet/default_setters/synaptic_weights.py b/bmtk-vb/bmtk/simulator/pointnet/default_setters/synaptic_weights.py new file mode 100644 index 0000000..4c66ae1 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/default_setters/synaptic_weights.py @@ -0,0 +1,8 @@ +from bmtk.simulator.pointnet.pyfunction_cache import add_weight_function + + +def default_weight_fnc(edge_props, source_node, target_node): + return edge_props['syn_weight']*edge_props.nsyns + + +add_weight_function(default_weight_fnc, 'default_weight_fnc', overwrite=False) diff --git a/bmtk-vb/bmtk/simulator/pointnet/io_tools.py b/bmtk-vb/bmtk/simulator/pointnet/io_tools.py new file mode 100644 index 0000000..b5ea9ea --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/io_tools.py @@ -0,0 +1,122 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +""" +Functions for logging, writing and reading from file. + +""" +import nest + +from bmtk.simulator.core.io_tools import IOUtils + +# Want users to be able to use NEST whether or not it is compiled in parallel mode or not, which means checking if +# the method nest.SyncPRocesses (aka MPI Barrier) exists. If it doesn't try getting barrier from mpi4py +rank = nest.Rank() +n_nodes = nest.NumProcesses() +try: + barrier = nest.SyncProcesses +except AttributeError as exc: + try: + from mpi4py import MPI + barrier = MPI.COMM_WORLD.Barrier + except: + # Barrier is just an empty function, no problem if running on one core. + barrier = lambda: None + + +class NestIOUtils(IOUtils): + def __init__(self): + super(NestIOUtils, self).__init__() + self.mpi_rank = rank + self.mpi_size = n_nodes + + def barrier(self): + barrier() + + def quiet_simulator(self): + nest.set_verbosity('M_QUIET') + + def setup_output_dir(self, config_dir, log_file, overwrite=True): + super(NestIOUtils, self).setup_output_dir(config_dir, log_file, overwrite=True) + if n_nodes > 1 and rank == 0: + io.log_info('Running NEST with MPI ({} cores)'.format(n_nodes)) + + +io = NestIOUtils() + + +''' +log_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') +pointnet_logger = logging.getLogger() +pointnet_logger.setLevel(logging.DEBUG) + +console_handler = logging.StreamHandler(sys.stdout) +console_handler.setFormatter(log_format) +pointnet_logger.addHandler(console_handler) + + +def collect_gdf_files(gdf_dir, output_file, nest_id_map, overwrite=False): + + if n_nodes > 0: + # Wait until all nodes are finished + barrier() + + if rank != 0: + return + + log("Saving spikes to file...") + spikes_out = output_file + if os.path.exists(spikes_out) and not overwrite: + return + + gdf_files_globs = '{}/*.gdf'.format(gdf_dir) + gdf_files = glob.glob(gdf_files_globs) + with open(spikes_out, 'w') as spikes_file: + csv_writer = csv.writer(spikes_file, delimiter=' ') + for gdffile in gdf_files: + spikes_df = pd.read_csv(gdffile, names=['gid', 'time', 'nan'], sep='\t') + for _, row in spikes_df.iterrows(): + csv_writer.writerow([row['time'], nest_id_map[int(row['gid'])]]) + os.remove(gdffile) + log("done.") + + +def setup_output_dir(config): + if rank == 0: + try: + output_dir = config['output']['output_dir'] + if os.path.exists(output_dir): + shutil.rmtree(output_dir) + os.makedirs(output_dir) + + if 'log_file' in config['output']: + file_logger = logging.FileHandler(config['output']['log_file']) + file_logger.setFormatter(log_format) + pointnet_logger.addHandler(file_logger) + log('Created a log file') + + except Exception as exc: + print(exc) + + barrier() +''' + diff --git a/bmtk-vb/bmtk/simulator/pointnet/modules/__init__.py b/bmtk-vb/bmtk/simulator/pointnet/modules/__init__.py new file mode 100644 index 0000000..962ea78 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/modules/__init__.py @@ -0,0 +1,2 @@ +from .record_spikes import SpikesMod +from .multimeter_reporter import MultimeterMod \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/pointnet/modules/multimeter_reporter.py b/bmtk-vb/bmtk/simulator/pointnet/modules/multimeter_reporter.py new file mode 100644 index 0000000..12d86ac --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/modules/multimeter_reporter.py @@ -0,0 +1,110 @@ +import os +import glob +import pandas as pd +from bmtk.utils.io.cell_vars import CellVarRecorder +from bmtk.simulator.pointnet.io_tools import io + +import nest + + +try: + MPI_RANK = nest.Rank() + N_HOSTS = nest.NumProcesses() + +except Exception as e: + MPI_RANK = 0 + N_HOSTS = 1 + + +class MultimeterMod(object): + def __init__(self, tmp_dir, file_name, variable_name, cells, tstart=None, tstop=None, interval=None, to_h5=True, + delete_dat=True, **opt_params): + """For recording neuron properties using a NEST multimeter object + + :param tmp_dir: ouput directory + :param file_name: Name of (SONATA hdf5) file that will be saved to + :param variable_name: A list of the variable(s) being recorded. Must be valid according to the cells + :param cells: A node-set or list of gids to record from + :param tstart: Start time of the recording (if None will default to sim.tstart) + :param tstop: Stop time of recording (if None will default to sim.tstop) + :param interval: Recording time step (if None will default to sim.dt) + :param to_h5: True to save to sonata .h5 format (default: True) + :param delete_dat: True to delete the .dat files created by NEST (default True) + :param opt_params: + """ + + self._output_dir = tmp_dir + self._file_name = file_name if os.path.isabs(file_name) else os.path.join(self._output_dir, file_name) + self._variable_name = variable_name + self._node_set = cells + self._tstart = tstart + self._tstop = tstop + self._interval = interval + self._to_h5 = to_h5 + self._delete_dat = delete_dat + + self._gids = None + self._nest_ids = None + self._multimeter = None + + self._min_delay = 1.0 # Required for calculating steps recorded + + self.__output_label = os.path.join(self._output_dir, '__bmtk_nest_{}'.format(os.path.basename(self._file_name))) + self._var_recorder = CellVarRecorder(self._file_name, self._output_dir, self._variable_name, buffer_data=False) + + def initialize(self, sim): + self._gids = list(sim.net.get_node_set(self._node_set).gids()) + self._nest_ids = [sim.net._gid2nestid[gid] for gid in self._gids] + + self._tstart = self._tstart or sim.tstart + self._tstop = self._tstop or sim.tstop + self._interval = self._interval or sim.dt + self._multimeter = nest.Create('multimeter', + params={'interval': self._interval, 'start': self._tstart, 'stop': self._tstop, + 'to_file': True, 'to_memory': False, + 'withtime': True, + 'record_from': self._variable_name, + 'label': self.__output_label}) + + nest.Connect(self._multimeter, self._nest_ids) + + def finalize(self, sim): + io.barrier() # Makes sure all nodes finish, but not sure if actually required by nest + + # min_delay needs to be fetched after simulation otherwise the value will be off. There also seems to be some + # MPI barrier inside GetKernelStatus + self._min_delay = nest.GetKernelStatus('min_delay') + # print self._min_delay + if self._to_h5 and MPI_RANK == 0: + for gid in self._gids: + self._var_recorder.add_cell(gid, sec_list=[0], seg_list=[0.0]) + + # Initialize hdf5 file including preallocated data block of recorded variables + # Unfortantely with NEST the final time-step recorded can't be calculated in advanced, and even with the + # same min/max_delay can be different. We need to read the output-file to get n_steps + def get_var_recorder(node_recording_df): + if not self._var_recorder.is_initialized: + self._var_recorder.tstart = node_recording_df['time'].min() + self._var_recorder.tstop = node_recording_df['time'].max() + self._var_recorder.dt = self._interval + self._var_recorder.initialize(len(node_recording_df)) + + return self._var_recorder + + gid_map = sim.net._nestid2gid + for nest_file in glob.glob('{}*'.format(self.__output_label)): + report_df = pd.read_csv(nest_file, index_col=False, names=['nest_id', 'time']+self._variable_name, + sep='\t') + for grp_id, grp_df in report_df.groupby(by='nest_id'): + gid = gid_map[grp_id] + vr = get_var_recorder(grp_df) + for var_name in self._variable_name: + vr.record_cell_block(gid, var_name, grp_df[var_name]) + + if self._delete_dat: + # remove csv file created by nest + os.remove(nest_file) + + self._var_recorder.close() + + io.barrier() diff --git a/bmtk-vb/bmtk/simulator/pointnet/modules/record_spikes.py b/bmtk-vb/bmtk/simulator/pointnet/modules/record_spikes.py new file mode 100644 index 0000000..9791fdc --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/modules/record_spikes.py @@ -0,0 +1,90 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import glob +from bmtk.utils.io.spike_trains import SpikeTrainWriter +from bmtk.simulator.pointnet.io_tools import io + +import nest + + +MPI_RANK = nest.Rank() +N_HOSTS = nest.NumProcesses() + + +class SpikesMod(object): + """Module use for saving spikes + + """ + + def __init__(self, tmp_dir, spikes_file_csv=None, spikes_file=None, spikes_file_nwb=None, spikes_sort_order=None): + def _get_path(file_name): + # Unless file-name is an absolute path then it should be placed in the $OUTPUT_DIR + if file_name is None: + return None + return file_name if os.path.isabs(file_name) else os.path.join(tmp_dir, file_name) + + self._csv_fname = _get_path(spikes_file_csv) + self._h5_fname = _get_path(spikes_file) + self._nwb_fname = _get_path(spikes_file_nwb) + + self._tmp_dir = tmp_dir + self._tmp_file_base = 'tmp_spike_times' + self._spike_labels = os.path.join(self._tmp_dir, self._tmp_file_base) + + self._spike_writer = SpikeTrainWriter(tmp_dir=tmp_dir, mpi_rank=MPI_RANK, mpi_size=N_HOSTS) + self._spike_writer.delimiter = '\t' + self._spike_writer.gid_col = 0 + self._spike_writer.time_col = 1 + self._sort_order = spikes_sort_order + + self._spike_detector = None + + def initialize(self, sim): + self._spike_detector = nest.Create("spike_detector", 1, {'label': self._spike_labels, 'withtime': True, + 'withgid': True, 'to_file': True}) + + for pop_name, pop in sim._graph._nestid2nodeid_map.items(): + nest.Connect(list(pop.keys()), self._spike_detector) + + def finalize(self, sim): + if MPI_RANK == 0: + for gdf_file in glob.glob(self._spike_labels + '*.gdf'): + self._spike_writer.add_spikes_file(gdf_file) + io.barrier() + + gid_map = sim._graph._nestid2gid + + if self._csv_fname is not None: + self._spike_writer.to_csv(self._csv_fname, sort_order=self._sort_order, gid_map=gid_map) + io.barrier() + + if self._h5_fname is not None: + self._spike_writer.to_hdf5(self._h5_fname, sort_order=self._sort_order, gid_map=gid_map) + io.barrier() + + if self._nwb_fname is not None: + self._spike_writer.to_nwb(self._nwb_fname, sort_order=self._sort_order, gid_map=gid_map) + io.barrier() + + self._spike_writer.close() diff --git a/bmtk-vb/bmtk/simulator/pointnet/pointnetwork.py b/bmtk-vb/bmtk/simulator/pointnet/pointnetwork.py new file mode 100644 index 0000000..0cc781f --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/pointnetwork.py @@ -0,0 +1,176 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json +import functools +import nest + +from bmtk.simulator.core.simulator_network import SimNetwork +from bmtk.simulator.pointnet.sonata_adaptors import PointNodeAdaptor, PointEdgeAdaptor +from bmtk.simulator.pointnet import pyfunction_cache +from bmtk.simulator.pointnet.io_tools import io + + +class PointNetwork(SimNetwork): + def __init__(self, **properties): + super(PointNetwork, self).__init__(**properties) + self._io = io + + self.__weight_functions = {} + self._params_cache = {} + + self._virtual_ids_map = {} + + self._batch_nodes = True + + self._nest_id_map = {} + self._nestid2nodeid_map = {} + + self._nestid2gid = {} + + self._nodes_table = {} + self._gid2nestid = {} + + @property + def py_function_caches(self): + return pyfunction_cache + + def __get_params(self, node_params): + if node_params.with_dynamics_params: + # TODO: use property, not name + return node_params['dynamics_params'] + + params_file = node_params[self._params_column] + # params_file = self._MT.params_column(node_params) #node_params['dynamics_params'] + if params_file in self._params_cache: + return self._params_cache[params_file] + else: + params_dir = self.get_component('models_dir') + params_path = os.path.join(params_dir, params_file) + params_dict = json.load(open(params_path, 'r')) + self._params_cache[params_file] = params_dict + return params_dict + + def _register_adaptors(self): + super(PointNetwork, self)._register_adaptors() + self._node_adaptors['sonata'] = PointNodeAdaptor + self._edge_adaptors['sonata'] = PointEdgeAdaptor + + # TODO: reimplement with py_modules like in bionet + def add_weight_function(self, function, name=None): + fnc_name = name if name is not None else function.__name__ + self.__weight_functions[fnc_name] = functools.partial(function) + + def set_default_weight_function(self, function): + self.add_weight_function(function, 'default_weight_fnc', overwrite=True) + + def get_weight_function(self, name): + return self.__weight_functions[name] + + def build_nodes(self): + for node_pop in self.node_populations: + nid2nest_map = {} + nest2nid_map = {} + if node_pop.internal_nodes_only: + for node in node_pop.get_nodes(): + node.build() + for nid, gid, nest_id in zip(node.node_ids, node.gids, node.nest_ids): + self._nestid2gid[nest_id] = gid + self._gid2nestid[gid] = nest_id + nid2nest_map[nid] = nest_id + nest2nid_map[nest_id] = nid + + elif node_pop.mixed_nodes: + for node in node_pop.get_nodes(): + if node.model_type != 'virtual': + node.build() + for nid, gid, nest_id in zip(node.node_ids, node.gids, node.nest_ids): + self._nestid2gid[nest_id] = gid + self._gid2nestid[gid] = nest_id + nid2nest_map[nid] = nest_id + nest2nid_map[nest_id] = nid + + self._nest_id_map[node_pop.name] = nid2nest_map + self._nestid2nodeid_map[node_pop.name] = nest2nid_map + + def build_recurrent_edges(self): + recurrent_edge_pops = [ep for ep in self._edge_populations if not ep.virtual_connections] + if not recurrent_edge_pops: + return + + for edge_pop in recurrent_edge_pops: + src_nest_ids = self._nest_id_map[edge_pop.source_nodes] + trg_nest_ids = self._nest_id_map[edge_pop.target_nodes] + for edge in edge_pop.get_edges(): + nest_srcs = [src_nest_ids[nid] for nid in edge.source_node_ids] + nest_trgs = [trg_nest_ids[nid] for nid in edge.target_node_ids] + nest.Connect(nest_srcs, nest_trgs, conn_spec='one_to_one', syn_spec=edge.nest_params) + + def find_edges(self, source_nodes=None, target_nodes=None): + # TODO: Move to parent + selected_edges = self._edge_populations[:] + + if source_nodes is not None: + selected_edges = [edge_pop for edge_pop in selected_edges if edge_pop.source_nodes == source_nodes] + + if target_nodes is not None: + selected_edges = [edge_pop for edge_pop in selected_edges if edge_pop.target_nodes == target_nodes] + + return selected_edges + + def add_spike_trains(self, spike_trains, node_set): + # Build the virtual nodes + src_nodes = [node_pop for node_pop in self.node_populations if node_pop.name in node_set.population_names()] + for node_pop in src_nodes: + if node_pop.name in self._virtual_ids_map: + continue + + virt_node_map = {} + if node_pop.virtual_nodes_only: + for node in node_pop.get_nodes(): + nest_ids = nest.Create('spike_generator', node.n_nodes, {}) + for node_id, nest_id in zip(node.node_ids, nest_ids): + virt_node_map[node_id] = nest_id + nest.SetStatus([nest_id], {'spike_times': spike_trains.get_spikes(node_id)}) + + elif node_pop.mixed_nodes: + for node in node_pop.get_nodes(): + if node.model_type != 'virtual': + continue + + nest_ids = nest.Create('spike_generator', node.n_nodes, {}) + for node_id, nest_id in zip(node.node_ids, nest_ids): + virt_node_map[node_id] = nest_id + nest.SetStatus([nest_id], {'spike_times': spike_trains.get_spikes(node_id)}) + + self._virtual_ids_map[node_pop.name] = virt_node_map + + # Create virtual synaptic connections + for source_reader in src_nodes: + for edge_pop in self.find_edges(source_nodes=source_reader.name): + src_nest_ids = self._virtual_ids_map[edge_pop.source_nodes] + trg_nest_ids = self._nest_id_map[edge_pop.target_nodes] + for edge in edge_pop.get_edges(): + nest_srcs = [src_nest_ids[nid] for nid in edge.source_node_ids] + nest_trgs = [trg_nest_ids[nid] for nid in edge.target_node_ids] + nest.Connect(nest_srcs, nest_trgs, conn_spec='one_to_one', syn_spec=edge.nest_params) diff --git a/bmtk-vb/bmtk/simulator/pointnet/pointsimulator.py b/bmtk-vb/bmtk/simulator/pointnet/pointsimulator.py new file mode 100644 index 0000000..a434da6 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/pointsimulator.py @@ -0,0 +1,266 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import glob +import nest +from six import string_types +from six import moves + +from bmtk.simulator.core.simulator import Simulator +from bmtk.simulator.pointnet.config import Config +#import bmtk.simulator.pointnet.config as cfg +from bmtk.simulator.pointnet.io_tools import io +import bmtk.simulator.utils.simulation_reports as reports +import bmtk.simulator.utils.simulation_inputs as inputs +from bmtk.utils.io import spike_trains +from . import modules as mods +from bmtk.simulator.core.node_sets import NodeSet + + +class PointSimulator(Simulator): + def __init__(self, graph, dt=0.001, overwrite=True, print_time=False): + self._tstop = 0.0 # simulation time + self._dt = dt # time step + self._output_dir = './output/' # directory where log and temporary output will be stored + self._overwrite = overwrite + self._block_run = False + self._block_size = -1 + + self._cells_built = False + self._internal_connections_built = False + + self._graph = graph + self._external_cells = {} # dict-of-dict of external pointnet cells with keys [network_name][cell_id] + self._internal_cells = {} # dictionary of internal pointnet cells with cell_id as key + self._nest_id_map = {} # a map between NEST IDs and Node-IDs + + self._spikedetector = None + self._spikes_file = None # File where all output spikes will be collected and saved + self._tmp_spikes_file = None # temporary gdf files of spike-trains + self._spike_trains_ds = {} # used to temporary store NWB datasets containing spike trains + + self._spike_detector = None + + self._mods = [] + + self._inputs = {} # Used to hold references to nest input objects (current_generators, etc) + + # Reset the NEST kernel for a new simualtion + # TODO: move this into it's own function and make sure it is called before network is built + nest.ResetKernel() + nest.SetKernelStatus({"resolution": self._dt, "overwrite_files": self._overwrite, "print_time": print_time}) + + @property + def tstart(self): + return 0.0 + + @property + def dt(self): + return self._dt + + @property + def tstop(self): + return self._tstop + + @tstop.setter + def tstop(self, val): + self._tstop = val + + @property + def n_steps(self): + return long((self.tstop-self.tstart)/self.dt) + + @property + def net(self): + return self._graph + + @property + def gid_map(self): + return self._graph._nestid2gid + + def _get_block_trial(self, duration): + """ + Compute necessary number of block trials, the length of block simulation and the simulation length of the last + block run, if necessary. + """ + if self._block_run: + data_res = self._block_size * self._dt + fn = duration / data_res + n = int(fn) + res = fn - n + else: + n = -1 + res = -1 + data_res = -1 + return n, res, data_res + + ''' + def set_spikes_recordings(self): + # TODO: Pass in output-dir and file name to save to + # TODO: Allow for sorting - overwrite bionet module + self._spike_detector = nest.Create("spike_detector", 1, {'label': os.path.join(self.output_dir, 'tmp_spike_times'), + 'withtime': True, 'withgid': True, 'to_file': True}) + # print self._spike_detector + + for pop_name, pop in self._graph._nestid2nodeid_map.items(): + # print pop.keys() + + nest.Connect(pop.keys(), self._spike_detector) + # exit() + ''' + + def add_step_currents(self, amp_times, amp_values, node_set, input_name): + scg = nest.Create("step_current_generator", + params={'amplitude_times': amp_times, 'amplitude_values': amp_values}) + + if not isinstance(node_set, NodeSet): + node_set = self.net.get_node_set(node_set) + + # Convert node set into list of gids and then look-up the nest-ids + nest_ids = [self.net._gid2nestid[gid] for gid in node_set.gids()] + + # Attach current clamp to nodes + nest.Connect(scg, nest_ids, syn_spec={'delay': self.dt}) + + self._inputs[input_name] = nest_ids + + def run(self, tstop=None): + if tstop is None: + tstop = self._tstop + + for mod in self._mods: + mod.initialize(self) + + io.barrier() + + io.log_info('Starting Simulation') + n, res, data_res = self._get_block_trial(tstop) + if n > 0: + for r in moves.range(n): + nest.Simulate(data_res) + if res > 0: + nest.Simulate(res * self.dt) + if n < 0: + nest.Simulate(tstop) + + io.barrier() + io.log_info('Simulation finished, finalizing results.') + for mod in self._mods: + mod.finalize(self) + io.barrier() + io.log_info('Done.') + + def add_mod(self, mod): + self._mods.append(mod) + + @classmethod + def from_config(cls, configure, graph): + # load the json file or object + if isinstance(configure, string_types): + config = Config.from_json(configure, validate=True) + elif isinstance(configure, dict): + config = configure + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(configure, type(configure))) + + if 'run' not in config: + raise Exception('Json file is missing "run" entry. Unable to build Bionetwork.') + run_dict = config['run'] + + # Get network parameters + # step time (dt) is set in the kernel and should be passed + overwrite = run_dict['overwrite_output_dir'] if 'overwrite_output_dir' in run_dict else True + print_time = run_dict['print_time'] if 'print_time' in run_dict else False + dt = run_dict['dt'] # TODO: make sure dt exists + network = cls(graph, dt=dt, overwrite=overwrite) + + if 'output_dir' in config['output']: + network.output_dir = config['output']['output_dir'] + + if 'block_run' in run_dict and run_dict['block_run']: + if 'block_size' not in run_dict: + raise Exception('"block_run" is set to True but "block_size" not found.') + network._block_size = run_dict['block_size'] + + if 'duration' in run_dict: + network.tstop = run_dict['duration'] + elif 'tstop' in run_dict: + network.tstop = run_dict['tstop'] + + # Create the output-directory, or delete existing files if it already exists + graph.io.log_info('Setting up output directory') + if not os.path.exists(config['output']['output_dir']): + os.mkdir(config['output']['output_dir']) + elif overwrite: + for gfile in glob.glob(os.path.join(config['output']['output_dir'], '*.gdf')): + os.remove(gfile) + + graph.io.log_info('Building cells.') + graph.build_nodes() + + graph.io.log_info('Building recurrent connections') + graph.build_recurrent_edges() + + for sim_input in inputs.from_config(config): + node_set = graph.get_node_set(sim_input.node_set) + if sim_input.input_type == 'spikes': + spikes = spike_trains.SpikesInput.load(name=sim_input.name, module=sim_input.module, + input_type=sim_input.input_type, params=sim_input.params) + io.log_info('Build virtual cell stimulations for {}'.format(sim_input.name)) + graph.add_spike_trains(spikes, node_set) + + elif sim_input.input_type == 'current_clamp': + # TODO: Need to make this more robust + amp_times = sim_input.params.get('amplitude_times', []) + amp_values = sim_input.params.get('amplitude_values', []) + + if 'delay' in sim_input.params: + amp_times.append(sim_input.params['delay']) + amp_values.append(sim_input.params['amp']) + + if 'duration' in sim_input.params: + amp_times.append(sim_input.params['delay'] + sim_input.params['duration']) + amp_values.append(0.0) + + network.add_step_currents(amp_times, amp_values, node_set, sim_input.name) + + else: + graph.io.log_warning('Unknown input type {}'.format(sim_input.input_type)) + + sim_reports = reports.from_config(config) + for report in sim_reports: + if report.module == 'spikes_report': + mod = mods.SpikesMod(**report.params) + + elif isinstance(report, reports.MembraneReport): + # For convience and for compliance with SONATA format. "membrane_report" and "multimeter_report is the + # same in pointnet. + mod = mods.MultimeterMod(**report.params) + + else: + graph.io.log_exception('Unknown report type {}'.format(report.module)) + + network.add_mod(mod) + + io.log_info('Network created.') + return network diff --git a/bmtk-vb/bmtk/simulator/pointnet/property_map.py b/bmtk-vb/bmtk/simulator/pointnet/property_map.py new file mode 100644 index 0000000..dd1ecc4 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/property_map.py @@ -0,0 +1,213 @@ +import types +import numpy as np + +import nest + +from bmtk.simulator.pointnet.pyfunction_cache import py_modules +from bmtk.simulator.pointnet.io_tools import io + +class NodePropertyMap(object): + def __init__(self, graph): + self._graph = graph + # TODO: Move template_cache to parent graph so it can be shared across diff populations. + self._template_cache = {} + self.node_types_table = None + + self.batch = True + + + def _parse_model_template(self, model_template): + if model_template in self._template_cache: + return self._template_cache[model_template] + else: + template_parts = model_template.split(':') + assert(len(template_parts) == 2) + directive, template = template_parts[0], template_parts[1] + self._template_cache[model_template] = (directive, template) + return directive, template + + def load_cell(self, node): + model_type = self._parse_model_template(node['model_template'])[1] + dynamics_params = self.dynamics_params(node) + fnc_name = node['model_processing'] + if fnc_name is None: + return nest.Create(model_type, 1, dynamics_params) + else: + cell_fnc = py_modules.cell_processor(fnc_name) + return cell_fnc(model_type, node, dynamics_params) + + @classmethod + def build_map(cls, node_group, graph): + prop_map = cls(graph) + + node_types_table = node_group.parent.node_types_table + prop_map.node_types_table = node_types_table + + if 'model_processing' in node_group.columns: + prop_map.batch = False + elif 'model_processing' in node_group.all_columns: + model_fncs = [node_types_table[ntid]['model_processing'] for ntid in np.unique(node_group.node_type_ids) + if node_types_table[ntid]['model_processing'] is not None] + + if model_fncs: + prop_map.batch = False + + if node_group.has_dynamics_params: + prop_map.batch = False + prop_map.dynamics_params = types.MethodType(group_dynamics_params, prop_map) + else: # 'dynamics_params' in node_group.all_columns: + prop_map.dynamics_params = types.MethodType(types_dynamics_params, prop_map) + + if prop_map.batch: + prop_map.model_type = types.MethodType(model_type_batched, prop_map) + prop_map.model_params = types.MethodType(model_params_batched, prop_map) + else: + prop_map.model_type = types.MethodType(model_type, prop_map) + prop_map.model_params = types.MethodType(model_params, prop_map) + + if node_group.has_gids: + prop_map.gid = types.MethodType(gid, prop_map) + else: + prop_map.gid = types.MethodType(node_id, prop_map) + + return prop_map + + +def gid(self, node): + return node['gid'] + + +def node_id(self, node): + return node.node_id + + +def model_type(self, node): + return self._parse_model_template(node['model_template']) + + +def model_type_batched(self, node_type_id): + return self._parse_model_template(self.node_types_table[node_type_id]['model_template']) + + +def model_params(self, node): + return {} + + +def model_params_batched(self, node_type_id): + return self.node_types_table[node_type_id]['dynamics_params'] + + +def types_dynamics_params(self, node): + return node['dynamics_params'] + + +def group_dynamics_params(self, node): + return node.dynamics_params + + +class EdgePropertyMap(object): + def __init__(self, graph, source_population, target_population): + self._graph = graph + self._source_population = source_population + self._target_population = target_population + + self.batch = True + self.synpatic_models = [] + + + def synaptic_model(self, edge): + return edge['model_template'] + + + def synpatic_params(self, edge): + params_dict = {'weight': self.syn_weight(edge), 'delay': edge['delay']} + params_dict.update(edge['dynamics_params']) + return params_dict + + @classmethod + def build_map(cls, edge_group, biograph): + prop_map = cls(biograph, edge_group.parent.source_population, edge_group.parent.source_population) + if 'model_template' in edge_group.columns: + prop_map.batch = False + elif 'model_template' in edge_group.all_columns: + edge_types_table = edge_group.parent.edge_types_table + syn_models = set(edge_types_table[etid]['model_template'] + for etid in np.unique(edge_types_table.edge_type_ids)) + prop_map.synpatic_models = list(syn_models) + else: + prop_map.synpatic_models = ['static_synapse'] + #s = [edge_types_table[ntid]['model_template'] for ntid in np.unique(edge_types_table.node_type_ids) + # if edge_types_table[ntid]['model_template'] is not None] + + + # For fetching/calculating synaptic weights + edge_types_weight_fncs = set() + edge_types_table = edge_group.parent.edge_types_table + for etid in edge_types_table.edge_type_ids: + weight_fnc = edge_types_table[etid].get('weight_function', None) + if weight_fnc is not None: + edge_types_weight_fncs.add(weight_fnc) + + if 'weight_function' in edge_group.group_columns or edge_types_weight_fncs: + # Customized function for user to calculate the synaptic weight + prop_map.syn_weight = types.MethodType(weight_function, prop_map) + + elif 'syn_weight' in edge_group.all_columns: + # Just return the synaptic weight + prop_map.syn_weight = types.MethodType(syn_weight, prop_map) + else: + io.log_exception('Could not find syn_weight or weight_function properties. Cannot create connections.') + + # For determining the synapse placement + if 'nsyns' in edge_group.all_columns: + prop_map.nsyns = types.MethodType(nsyns, prop_map) + else: + # It will get here for connections onto point neurons + prop_map.nsyns = types.MethodType(no_syns, prop_map) + + # For target sections + ''' + if 'syn_weight' not in edge_group.all_columns: + io.log_exception('Edges {} missing syn_weight property for connections.'.format(edge_group.parent.name)) + else: + prop_map.syn_weight = types.MethodType(syn_weight, prop_map) + + + + if 'syn_weight' in edge_group.columns: + prop_map.weight = types.MethodType(syn_weight, prop_map) + prop_map.preselected_targets = True + prop_map.nsyns = types.MethodType(no_nsyns, prop_map) + else: + prop_map.preselected_targets = False + ''' + return prop_map + + +def syn_weight(self, edge): + return edge['syn_weight']*self.nsyns(edge) + + +def weight_function(self, edge): + weight_fnc_name = edge['weight_function'] + src_node = self._graph.get_node(self._source_population, edge.source_node_id) + trg_node = self._graph.get_node(self._target_population, edge.target_node_id) + + if weight_fnc_name is None: + weight_fnc = py_modules.synaptic_weight('default_weight_fnc') + return weight_fnc(edge, src_node, trg_node)# *self.nsyns(edge) + + elif py_modules.has_synaptic_weight(weight_fnc_name): + weight_fnc = py_modules.synaptic_weight(weight_fnc_name) + return weight_fnc(edge, src_node, trg_node) + + else: + io.log_exception('weight_function {} is not defined.'.format(weight_fnc_name)) + + +def nsyns(self, edge): + return edge['nsyns'] + + +def no_syns(self, edge): + return 1 \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/pointnet/pyfunction_cache.py b/bmtk-vb/bmtk/simulator/pointnet/pyfunction_cache.py new file mode 100644 index 0000000..9e50616 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/pyfunction_cache.py @@ -0,0 +1,246 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import types +from functools import wraps + + +class _PyFunctions(object): + """Structure for holding custom user-defined python functions. + + Will store a set of functions created by the user. Should not access this directly but rather user the + decorators or setter functions, and use the py_modules class variable to access individual functions. Is divided + up into + synaptic_weight: functions for calcuating synaptic weight. + cell_model: should return NEURON cell hobj. + synapse model: should return a NEURON synapse object. + """ + def __init__(self): + self.__syn_weights = {} + self.__cell_models = {} + self.__synapse_models = {} + self.__cell_processors = {} + + def clear(self): + self.__syn_weights.clear() + self.__cell_models.clear() + self.__synapse_models.clear() + self.__cell_processors.clear() + + def add_synaptic_weight(self, name, func, overwrite=True): + """stores synpatic fuction for given name""" + if overwrite or name not in self.__syn_weights: + self.__syn_weights[name] = func + + @property + def synaptic_weight(self): + """return list of the names of all available synaptic weight functions""" + return self.__syn_weights.keys() + + def synaptic_weight(self, name): + """return the synpatic weight function""" + return self.__syn_weights[name] + + def has_synaptic_weight(self, name): + return name in self.__syn_weights + + def __cell_model_key(self, directive, model_type): + return (directive, model_type) + + def add_cell_model(self, directive, model_type, func, overwrite=True): + key = self.__cell_model_key(directive, model_type) + if overwrite or key not in self.__cell_models: + self.__cell_models[key] = func + + @property + def cell_models(self): + return self.__cell_models.keys() + + def cell_model(self, directive, model_type): + return self.__cell_models[self.__cell_model_key(directive, model_type)] + + def has_cell_model(self, directive, model_type): + return self.__cell_model_key(directive, model_type) in self.__cell_models + + def add_synapse_model(self, name, func, overwrite=True): + if overwrite or name not in self.__synapse_models: + self.__synapse_models[name] = func + + @property + def synapse_models(self): + return self.__synapse_models.keys() + + def synapse_model(self, name): + return self.__synapse_models[name] + + + @property + def cell_processors(self): + return self.__cell_processors.keys() + + def cell_processor(self, name): + return self.__cell_processors[name] + + def add_cell_processor(self, name, func, overwrite=True): + if overwrite or name not in self.__syn_weights: + self.__cell_processors[name] = func + + def __repr__(self): + rstr = '{}: {}\n'.format('cell_models', self.cell_models) + rstr += '{}: {}\n'.format('synapse_models', self.synapse_models) + rstr += '{}: {}'.format('synaptic_weights', self.synaptic_weights) + return rstr + +py_modules = _PyFunctions() + + +def synaptic_weight(*wargs, **wkwargs): + """A decorator for registering a function as a synaptic weight function. + To use either + @synaptic_weight + def weight_function(): ... + + or + @synaptic_weight(name='name_in_edge_types') + def weight_function(): ... + + Once the decorator has been attached and imported the functions will automatically be added to py_modules. + """ + if len(wargs) == 1 and callable(wargs[0]): + # for the case without decorator arguments, grab the function object in wargs and create a decorator + func = wargs[0] + py_modules.add_synaptic_weight(func.__name__, func) # add function assigned to its original name + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + else: + # for the case with decorator arguments + assert(all(k in ['name'] for k in wkwargs.keys())) + def decorator(func): + # store the function in py_modules but under the name given in the decorator arguments + py_modules.add_synaptic_weight(wkwargs['name'], func) + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + return decorator + + +def cell_model(*wargs, **wkwargs): + """A decorator for registering NEURON cell loader functions.""" + if len(wargs) == 1 and callable(wargs[0]): + # for the case without decorator arguments, grab the function object in wargs and create a decorator + func = wargs[0] + py_modules.add_cell_model(func.__name__, func) # add function assigned to its original name + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + else: + # for the case with decorator arguments + assert(all(k in ['name'] for k in wkwargs.keys())) + + def decorator(func): + # store the function in py_modules but under the name given in the decorator arguments + py_modules.add_cell_model(wkwargs['name'], func) + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + return decorator + + +def synapse_model(*wargs, **wkwargs): + """A decorator for registering NEURON synapse loader functions.""" + if len(wargs) == 1 and callable(wargs[0]): + # for the case without decorator arguments, grab the function object in wargs and create a decorator + func = wargs[0] + py_modules.add_synapse_model(func.__name__, func) # add function assigned to its original name + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + else: + # for the case with decorator arguments + assert(all(k in ['name'] for k in wkwargs.keys())) + + def decorator(func): + # store the function in py_modules but under the name given in the decorator arguments + py_modules.add_synapse_model(wkwargs['name'], func) + + @wraps(func) + def func_wrapper(*args, **kwargs): + return func(*args, **kwargs) + return func_wrapper + return decorator + + +def add_weight_function(func, name=None, overwrite=True): + assert(callable(func)) + func_name = name if name is not None else func.__name__ + py_modules.add_synaptic_weight(func_name, func, overwrite) + + +def add_cell_model(func, directive, model_type, overwrite=True): + assert(callable(func)) + # func_name = name if name is not None else func.__name__ + py_modules.add_cell_model(directive, model_type, func, overwrite) + + +def add_cell_processor(func, name=None, overwrite=True): + assert(callable(func)) + func_name = name if name is not None else func.__name__ + py_modules.add_cell_processor(func_name, func, overwrite) + + +def add_synapse_model(func, name=None, overwrite=True): + assert (callable(func)) + func_name = name if name is not None else func.__name__ + py_modules.add_synapse_model(func_name, func, overwrite) + + +def load_py_modules(cell_models=None, syn_models=None, syn_weights=None): + # py_modules.clear() + + if cell_models is not None: + assert(isinstance(cell_models, types.ModuleType)) + for f in [cell_models.__dict__.get(f) for f in dir(cell_models)]: + if isinstance(f, types.FunctionType): + py_modules.add_cell_model(f.__name__, f) + + if syn_models is not None: + assert(isinstance(syn_models, types.ModuleType)) + for f in [syn_models.__dict__.get(f) for f in dir(syn_models)]: + if isinstance(f, types.FunctionType): + py_modules.add_synapse_model(f.__name__, f) + + if syn_weights is not None: + assert(isinstance(syn_weights, types.ModuleType)) + for f in [syn_weights.__dict__.get(f) for f in dir(syn_weights)]: + if isinstance(f, types.FunctionType): + py_modules.add_synaptic_weight(f.__name__, f) diff --git a/bmtk-vb/bmtk/simulator/pointnet/sonata_adaptors.py b/bmtk-vb/bmtk/simulator/pointnet/sonata_adaptors.py new file mode 100644 index 0000000..b528dba --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/sonata_adaptors.py @@ -0,0 +1,295 @@ +import numpy as np +from collections import Counter +import numbers +import nest +import types +import pandas as pd + +from bmtk.simulator.core.sonata_reader import NodeAdaptor, SonataBaseNode, EdgeAdaptor, SonataBaseEdge +from bmtk.simulator.pointnet.io_tools import io +from bmtk.simulator.pointnet.pyfunction_cache import py_modules + + +def all_null(node_group, column_name): + """Helper function to determine if a column has any non-NULL values""" + types_table = node_group.parent.types_table + non_null_vals = [types_table[ntid][column_name] for ntid in np.unique(node_group.node_type_ids) + if types_table[ntid][column_name] is not None] + return len(non_null_vals) == 0 + + +class PointNodeBatched(object): + def __init__(self, node_ids, gids, node_types_table, node_type_id): + self._n_nodes = len(node_ids) + self._node_ids = node_ids + self._gids = gids + self._nt_table = node_types_table + self._nt_id = node_type_id + self._nest_ids = [] + + @property + def n_nodes(self): + return self._n_nodes + + @property + def node_ids(self): + return self._node_ids + + @property + def gids(self): + return self._gids + + @property + def nest_ids(self): + return self._nest_ids + + @property + def nest_model(self): + return self._nt_table[self._nt_id]['model_template'].split(':')[1] + + @property + def nest_params(self): + return self._nt_table[self._nt_id]['dynamics_params'] + + @property + def model_type(self): + return self._nt_table[self._nt_id]['model_type'] + + def build(self): + self._nest_ids = nest.Create(self.nest_model, self.n_nodes, self.nest_params) + + +class PointNode(SonataBaseNode): + def __init__(self, node, prop_adaptor): + super(PointNode, self).__init__(node, prop_adaptor) + self._nest_ids = [] + + @property + def n_nodes(self): + return 1 + + @property + def node_ids(self): + return [self._prop_adaptor.node_id(self._node)] + + @property + def gids(self): + return [self._prop_adaptor.gid(self._node)] + + @property + def nest_ids(self): + return self._nest_ids + + @property + def nest_model(self): + return self._prop_adaptor.model_template(self._node)[1] + + @property + def nest_params(self): + return self.dynamics_params + + def build(self): + nest_model = self.nest_model + dynamics_params = self.dynamics_params + fnc_name = self._node['model_processing'] + if fnc_name is None: + self._nest_ids = nest.Create(nest_model, 1, dynamics_params) + else: + cell_fnc = py_modules.cell_processor(fnc_name) + self._nest_ids = cell_fnc(nest_model, self._node, dynamics_params) + + +class PointNodeAdaptor(NodeAdaptor): + def __init__(self, network): + super(PointNodeAdaptor, self).__init__(network) + + # Flag for determining if we can build multiple NEST nodes at once. If each individual node has unique + # NEST params or a model_processing function is being called then we must nest.Create for each individual cell. + # Otherwise we can try to call nest.Create for a batch of nodes that share the same properties + self._can_batch = True + + @property + def batch_process(self): + return self._can_batch + + @batch_process.setter + def batch_process(self, flag): + self._can_batch = flag + + def get_node(self, sonata_node): + return PointNode(sonata_node, self) + + def get_batches(self, node_group): + node_ids = node_group.node_ids + node_type_ids = node_group.node_type_ids + node_gids = node_group.gids + if node_gids is None: + node_gids = node_ids + + ntids_counter = Counter(node_type_ids) + + nid_groups = {nt_id: np.zeros(ntids_counter[nt_id], dtype=np.uint32) for nt_id in ntids_counter} + gid_groups = {nt_id: np.zeros(ntids_counter[nt_id], dtype=np.uint32) for nt_id in ntids_counter} + node_groups_counter = {nt_id: 0 for nt_id in ntids_counter} + + for node_id, gid, node_type_id in zip(node_ids, node_gids, node_type_ids): + grp_indx = node_groups_counter[node_type_id] + nid_groups[node_type_id][grp_indx] = node_id + gid_groups[node_type_id][grp_indx] = gid + node_groups_counter[node_type_id] += 1 + + return [PointNodeBatched(nid_groups[nt_id], gid_groups[nt_id], node_group.parent.node_types_table, nt_id) + for nt_id in ntids_counter] + + @staticmethod + def patch_adaptor(adaptor, node_group, network): + node_adaptor = NodeAdaptor.patch_adaptor(adaptor, node_group, network) + + # If dynamics params is stored in the nodes.h5 then we have to build each node separate + if node_group.has_dynamics_params: + node_adaptor.batch_process = False + + # If there is a non-null value in the model_processing column then it potentially means that every cell is + # uniquly built (currently model_processing is applied to each individ. cell) and nodes can't be batched + if 'model_processing' in node_group.columns: + node_adaptor.batch_process = False + elif 'model_processing' in node_group.all_columns and not all_null(node_group, 'model_processing'): + node_adaptor.batch_process = False + + if node_adaptor.batch_process: + io.log_info('Batch processing nodes for {}/{}.'.format(node_group.parent.name, node_group.group_id)) + + return node_adaptor + + +class PointEdge(SonataBaseEdge): + @property + def source_node_ids(self): + return [self._edge.source_node_id] + + @property + def target_node_ids(self): + return [self._edge.target_node_id] + + @property + def nest_params(self): + if self.model_template in py_modules.synapse_models: + syn_model_fnc = py_modules.synapse_model(self.model_template) + else: + syn_model_fnc = py_modules.synapse_models('default') + + return syn_model_fnc(self) + + +class PointEdgeBatched(object): + def __init__(self, source_nids, target_nids, nest_params): + self._src_nids = source_nids + self._trg_nids = target_nids + self._nest_params = nest_params + + @property + def source_node_ids(self): + return self._src_nids + + @property + def target_node_ids(self): + return self._trg_nids + + @property + def nest_params(self): + return self._nest_params + + +class PointEdgeAdaptor(EdgeAdaptor): + def __init__(self, network): + super(PointEdgeAdaptor, self).__init__(network) + self._can_batch = True + + @property + def batch_process(self): + return self._can_batch + + @batch_process.setter + def batch_process(self, flag): + self._can_batch = flag + + def synaptic_params(self, edge): + # TODO: THIS NEEDS to be replaced with call to synapse_models + params_dict = {'weight': self.syn_weight(edge, None, None), 'delay': edge.delay} + params_dict.update(edge.dynamics_params) + return params_dict + + def get_edge(self, sonata_node): + return PointEdge(sonata_node, self) + + + def get_batches(self, edge_group): + src_ids = {} + trg_ids = {} + edge_types_table = edge_group.parent.edge_types_table + + edge_type_ids = edge_group.node_type_ids() + et_id_counter = Counter(edge_type_ids) + tmp_df = pd.DataFrame({'etid': edge_type_ids, 'src_nids': edge_group.src_node_ids(), + 'trg_nids': edge_group.trg_node_ids()}) + + for et_id, grp_vals in tmp_df.groupby('etid'): + src_ids[et_id] = np.array(grp_vals['src_nids']) + trg_ids[et_id] = np.array(grp_vals['trg_nids']) + + # selected_etids = np.unique(edge_type_ids) + type_params = {et_id: {} for et_id in et_id_counter.keys()} + for et_id, p_dict in type_params.items(): + p_dict.update(edge_types_table[et_id]['dynamics_params']) + if 'model_template' in edge_types_table[et_id]: + p_dict['model'] = edge_types_table[et_id]['model_template'] + + if 'delay' in edge_group.columns: + raise NotImplementedError + elif 'delay' in edge_types_table.columns: + for et_id, p_dict in type_params.items(): + p_dict['delay'] = edge_types_table[et_id]['delay'] + + scalar_syn_weight = 'syn_weight' not in edge_group.columns + scalar_nsyns = 'nsyns' not in edge_group.columns + + if scalar_syn_weight and scalar_nsyns: + for et_id, p_dict in type_params.items(): + et_dict = edge_types_table[et_id] + p_dict['weight'] = et_dict['nsyns']*et_dict['syn_weight'] + + else: + if not scalar_nsyns and not scalar_syn_weight: + tmp_df['nsyns'] = edge_group.get_dataset('nsyns') + tmp_df['syn_weight'] = edge_group.get_dataset('syn_weight') + for et_id, grp_vals in tmp_df.groupby('etid'): + type_params[et_id]['weight'] = np.array(grp_vals['nsyns'])*np.array(grp_vals['syn_weight']) + + elif scalar_nsyns: + tmp_df['syn_weight'] = edge_group.get_dataset('syn_weight') + for et_id, grp_vals in tmp_df.groupby('etid'): + type_params[et_id]['weight'] = edge_types_table[et_id].get('nsyns', 1) * np.array(grp_vals['syn_weight']) + + elif scalar_syn_weight: + tmp_df['nsyns'] = edge_group.get_dataset('nsyns') + for et_id, grp_vals in tmp_df.groupby('etid'): + type_params[et_id]['weight'] = np.array(grp_vals['nsyns']) * edge_types_table[et_id]['syn_weight'] + + batched_edges = [] + for et_id in et_id_counter.keys(): + batched_edges.append(PointEdgeBatched(src_ids[et_id], trg_ids[et_id], type_params[et_id])) + + return batched_edges + + @staticmethod + def patch_adaptor(adaptor, edge_group): + edge_adaptor = EdgeAdaptor.patch_adaptor(adaptor, edge_group) + + if 'weight_function' not in edge_group.all_columns and 'syn_weight' in edge_group.all_columns: + adaptor.syn_weight = types.MethodType(point_syn_weight, adaptor) + + return edge_adaptor + + +def point_syn_weight(self, edge, src_node, trg_node): + return edge['syn_weight']*edge.nsyns diff --git a/bmtk-vb/bmtk/simulator/pointnet/utils.py b/bmtk-vb/bmtk/simulator/pointnet/utils.py new file mode 100644 index 0000000..d71716a --- /dev/null +++ b/bmtk-vb/bmtk/simulator/pointnet/utils.py @@ -0,0 +1,188 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import h5py +from collections import defaultdict +import pandas as pd +import numpy as np +import six +""" +Most of these functions were collected from previous version of pointnet and are no longer tested and tested. However +some functions may still be used by some people internally at AI for running their own simulations. I have marked all +such functions as UNUSED. + +I will leave them alone for now but in the future they should be purged or updated. +""" + + +def read_LGN_activity(trial_num, file_name): + # UNUSED. + spike_train_dict = {} + f5 = h5py.File(file_name, 'r') + trial_group = f5['processing/trial_{}/spike_train'.format(trial_num)] + for cid in trial_group.keys(): + spike_train_dict[int(cid)] = trial_group[cid]['data'][...] + + return spike_train_dict + + +def read_conns(file_name): + # UNUSED. + fc = h5py.File(file_name) + indptr = fc['indptr'] + cell_size = len(indptr) - 1 + print(cell_size) + conns = {} + source = fc['src_gids'] + for xin in six.moves.range(cell_size): + conns[str(xin)] = list(source[indptr[xin]:indptr[xin+1]]) + + return conns + + +def gen_recurrent_csv(num, offset, csv_file): + # UNUSED. + conn_data = np.loadtxt(csv_file) + target_ids = conn_data[:, 0] + source_ids = conn_data[:, 1] + weight_scale = conn_data[:, 2] + + pre = [] + cell_num = num + params = [] + for xin in six.moves.range(cell_num): + pre.append(xin+offset) + ind = np.where(source_ids == xin) + + temp_param = {} + targets = target_ids[ind] + offset + weights = weight_scale[ind] + delays = np.ones(len(ind[0]))*1.5 + targets.astype(float) + weights.astype(float) + temp_param['target'] = targets + temp_param['weight'] = weights*1 + temp_param['delay'] = delays + params.append(temp_param) + + return pre, params + + +def gen_recurrent_h5(num, offset, h5_file): + # UNUSED. + fc = h5py.File(h5_file) + indptr = fc['indptr'] + cell_size = len(indptr) - 1 + src_gids = fc['src_gids'] + nsyns = fc['nsyns'] + source_ids = [] + weight_scale = [] + target_ids = [] + delay_v = 1.5 # arbitrary value + + for xin in six.moves.range(cell_size): + target_ids.append(xin) + source_ids.append(list(src_gids[indptr[xin]:indptr[xin+1]])) + weight_scale.append(list(nsyns[indptr[xin]:indptr[xin+1]])) + targets = defaultdict(list) + weights = defaultdict(list) + delays = defaultdict(list) + + for xi, xin in enumerate(target_ids): + for yi, yin in enumerate(source_ids[xi]): + targets[yin].append(xin) + weights[yin].append(weight_scale[xi][yi]) + delays[yin].append(delay_v) + + presynaptic = [] + params = [] + for xin in targets: + presynaptic.append(xin+offset) + temp_param = {} + temp_array = np.array(targets[xin])*1.0 + offset + temp_array.astype(float) + temp_param['target'] = temp_array + temp_array = np.array(weights[xin]) + temp_array.astype(float) + temp_param['weight'] = temp_array + temp_array = np.array(delays[xin]) + temp_array.astype(float) + temp_param['delay'] = temp_array + params.append(temp_param) + + return presynaptic, params + + +def load_params(node_name, model_name): + """ + load information regarding nodes and cell_models from csv files + + Parameters + ---------- + node_name: json file name for node information + model_name: json file name for neuron model information + + Returns + ------- + node_info: 2d array of node info read out from the json file + mode_info: 2d array of model info read out from the json file + dict_coordinates: dictionary of coordinates. keyword is the node_id and entries are the x,y and z coordinates. + """ + # UNUSED. + node = pd.read_csv(node_name, sep=' ', quotechar='"', quoting=0) + model = pd.read_csv(model_name, sep=' ', quotechar='"', quoting=0) + node_info = node.values + model_info = model.values + # In NEST, cells do not have intrinsic coordinates. So we have to make some virutial links between cells and + # coordinates + dict_coordinates = defaultdict(list) + + for xin in six.moves.range(len(node_info)): + dict_coordinates[str(node_info[xin, 0])] = [node_info[xin, 2], node_info[xin, 3], node_info[xin, 4]] + return node_info, model_info, dict_coordinates + + +def load_conns(cnn_fn): + """ + load information regarding connectivity from csv files + + Parameters + ---------- + cnn_fn: json file name for connection information + + Returns + ------- + connection dictionary + """ + # UNUSED. + conns = pd.read_csv(cnn_fn, sep=' ', quotechar='"', quoting=0) + targets = conns.target_label + sources = conns.source_label + weights = conns.weight + delays = conns.delay + + conns_mapping = {} + for xin in six.moves.range(len(targets)): + keys = sources[xin] + '-' + targets[xin] + conns_mapping[keys] = [weights[xin], delays[xin]] + + return conns_mapping diff --git a/bmtk-vb/bmtk/simulator/popnet/__init__.py b/bmtk-vb/bmtk/simulator/popnet/__init__.py new file mode 100644 index 0000000..7b591ca --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from .popnetwork import PopNetwork +from .popsimulator import PopSimulator +from .config import Config diff --git a/bmtk-vb/bmtk/simulator/popnet/config.py b/bmtk-vb/bmtk/simulator/popnet/config.py new file mode 100644 index 0000000..567e5b6 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/config.py @@ -0,0 +1,34 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +# import bmtk.simulator.utils.config as msdk_config +from bmtk.simulator.core.config import ConfigDict +from bmtk.simulator.core.io_tools import io + +def from_json(config_file, validate=False): + conf_dict = ConfigDict.from_json(config_file) + conf_dict.io = io + return conf_dict + + +class Config(ConfigDict): + pass \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/popnet/popedge.py b/bmtk-vb/bmtk/simulator/popnet/popedge.py new file mode 100644 index 0000000..1e4e98e --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/popedge.py @@ -0,0 +1,82 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from bmtk.simulator.utils.graph import SimEdge + + +class PopEdge(SimEdge): + def __init__(self, source_pop, target_pop, edge_params, dynamics_params): + super(PopEdge, self).__init__(edge_params, dynamics_params) + self.__source_pop = source_pop + self.__target_pop = target_pop + self._weight = self.__get_prop('weight', 0.0) + self._nsyns = self.__get_prop('nsyns', 0) + self._delay = self.__get_prop('delay', 0.0) + + @property + def source(self): + return self.__source_pop + + @property + def target(self): + return self.__target_pop + + @property + def params(self): + return self._orig_params + + @property + def weight(self): + return self._weight + + @weight.setter + def weight(self, value): + self._weight = value + + @property + def nsyns(self): + return self._nsyns + + @nsyns.setter + def nsyns(self, value): + self._nsyns = value + + @property + def delay(self): + return self._delay + + @delay.setter + def delay(self, value): + self._delay = value + + def __get_prop(self, name, default=None): + if name in self._orig_params: + return self._orig_params[name] + elif name in self._dynamics_params: + return self._dynamics_params[name] + else: + return default + + def __repr__(self): + relevant_params = "weight: {}, delay: {}, nsyns: {}".format(self.weight, self.delay, self.nsyns) + rstr = "{} --> {} {{{}}}".format(self.source.pop_id, self.target.pop_id, relevant_params) + return rstr diff --git a/bmtk-vb/bmtk/simulator/popnet/popnetwork.py b/bmtk-vb/bmtk/simulator/popnet/popnetwork.py new file mode 100644 index 0000000..46b7928 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/popnetwork.py @@ -0,0 +1,695 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json +import numpy as np + +from bmtk.simulator.core.simulator_network import SimNetwork +#from bmtk.simulator.core.graph import SimGraph +#from property_schemas import PopTypes, DefaultPropertySchema +#from popnode import InternalNode, ExternalPopulation +#from popedge import PopEdge +from bmtk.simulator.popnet import utils as poputils +from bmtk.simulator.popnet.sonata_adaptors import PopEdgeAdaptor + +from dipde.internals.internalpopulation import InternalPopulation +from dipde.internals.externalpopulation import ExternalPopulation +from dipde.internals.connection import Connection + +''' +class PopNode(object): + def __init__(self, node, property_map, graph): + self._node = node + self._property_map = property_map + self._graph = graph + + @property + def dynamics_params(self): + # TODO: Use propert map + return self._node['dynamics_params'] + + @property + def node_id(self): + # TODO: Use property map + return self._node.node_id +''' + + +class Population(object): + def __init__(self, pop_id): + self._pop_id = pop_id + self._nodes = [] + self._params = None + + self._dipde_obj = None + + def add_node(self, pnode): + self._nodes.append(pnode) + if self._params is None and pnode.dynamics_params is not None: + self._params = pnode.dynamics_params.copy() + + @property + def pop_id(self): + return self._pop_id + + @property + def dipde_obj(self): + return self._dipde_obj + + @property + def record(self): + return True + + def build(self): + params = self._nodes[0].dynamics_params + self._dipde_obj = InternalPopulation(**params) + + def get_gids(self): + for node in self._nodes: + yield node.node_id + + def __getitem__(self, item): + return self._params[item] + + def __setitem__(self, key, value): + self._params[key] = value + + def __repr__(self): + return str(self._pop_id) + + +class ExtPopulation(Population): + def __init__(self, pop_id): + super(ExtPopulation, self).__init__(pop_id) + self._firing_rate = None + + @property + def record(self): + return False + + @property + def firing_rate(self): + return self._firing_rate + + @firing_rate.setter + def firing_rate(self, value): + self.build(value) + + def build(self, firing_rate): + if firing_rate is not None: + self._firing_rate = firing_rate + + self._dipde_obj = ExternalPopulation(firing_rate) + + +class PopEdge(object): + def __init__(self, edge, property_map, graph): + self._edge = edge + self._prop_map = property_map + self._graph = graph + + @property + def nsyns(self): + # TODO: Use property map + return self._edge['nsyns'] + + @property + def delay(self): + return self._edge['delay'] + + @property + def weight(self): + return self._edge['syn_weight'] + + +class PopConnection(object): + def __init__(self, src_pop, trg_pop): + self._src_pop = src_pop + self._trg_pop = trg_pop + self._edges = [] + + self._dipde_conn = None + + def add_edge(self, edge): + self._edges.append(edge) + + def build(self): + edge = self._edges[0] + self._dipde_conn = Connection(self._src_pop._dipde_obj, self._trg_pop._dipde_obj, edge.nsyns, edge.delay, + edge.syn_weight) + + @property + def dipde_obj(self): + return self._dipde_conn + + +class PopNetwork(SimNetwork): + def __init__(self, group_by='node_type_id', **properties): + super(PopNetwork, self).__init__() + + self.__all_edges = [] + self._group_key = group_by + self._gid_table = {} + self._edges = {} + self._target_edges = {} + self._source_edges = {} + + self._params_cache = {} + #self._params_column = property_schema.get_params_column() + self._dipde_pops = {} + self._external_pop = {} + self._all_populations = [] + # self._loaded_external_pops = {} + + self._nodeid2pop_map = {} + + self._connections = {} + self._external_connections = {} + self._all_connections = [] + + @property + def populations(self): + return self._all_populations + + @property + def connections(self): + return self._all_connections + + @property + def internal_populations(self): + return self._dipde_pops.values() + + def _register_adaptors(self): + super(PopNetwork, self)._register_adaptors() + self._edge_adaptors['sonata'] = PopEdgeAdaptor + + def build_nodes(self): + if self._group_key == 'node_id' or self._group_key is None: + self._build_nodes() + else: + self._build_nodes_grouped() + + def _build_nodes(self): + for node_pop in self.node_populations: + if node_pop.internal_nodes_only: + nid2pop_map = {} + for node in node_pop.get_nodes(): + #pnode = PopNode(node, prop_maps[node.group_id], self) + pop = Population(node.node_id) + pop.add_node(node) + pop.build() + + self._dipde_pops[node.node_id] = pop + self._all_populations.append(pop) + nid2pop_map[node.node_id] = pop + + self._nodeid2pop_map[node_pop.name] = nid2pop_map + + """ + for node_pop in self._internal_populations_map.values(): + prop_maps = self._node_property_maps[node_pop.name] + nid2pop_map = {} + for node in node_pop: + pnode = PopNode(node, prop_maps[node.group_id], self) + pop = Population(node.node_id) + pop.add_node(pnode) + pop.build() + + self._dipde_pops[node.node_id] = pop + self._all_populations.append(pop) + nid2pop_map[node.node_id] = pop + + self._nodeid2pop_map[node_pop.name] = nid2pop_map + """ + + def _build_nodes_grouped(self): + # Organize every single sonata-node into a given population. + for node_pop in self.node_populations: + nid2pop_map = {} + if node_pop.internal_nodes_only: + for node in node_pop.get_nodes(): + pop_key = node[self._group_key] + if pop_key not in self._dipde_pops: + pop = Population(pop_key) + self._dipde_pops[pop_key] = pop + self._all_populations.append(pop) + + pop = self._dipde_pops[pop_key] + pop.add_node(node) + nid2pop_map[node.node_id] = pop + + self._nodeid2pop_map[node_pop.name] = nid2pop_map + + for dpop in self._dipde_pops.values(): + dpop.build() + + """ + for node_pop in self._internal_populations_map.values(): + prop_maps = self._node_property_maps[node_pop.name] + nid2pop_map = {} + for node in node_pop: + pop_key = node[self._group_key] + pnode = PopNode(node, prop_maps[node.group_id], self) + if pop_key not in self._dipde_pops: + pop = Population(pop_key) + self._dipde_pops[pop_key] = pop + self._all_populations.append(pop) + + pop = self._dipde_pops[pop_key] + pop.add_node(pnode) + nid2pop_map[node.node_id] = pop + + self._nodeid2pop_map[node_pop.name] = nid2pop_map + + for dpop in self._dipde_pops.values(): + dpop.build() + """ + + def build_recurrent_edges(self): + recurrent_edge_pops = [ep for ep in self._edge_populations if not ep.virtual_connections] + + for edge_pop in recurrent_edge_pops: + if edge_pop.recurrent_connections: + src_pop_maps = self._nodeid2pop_map[edge_pop.source_nodes] + trg_pop_maps = self._nodeid2pop_map[edge_pop.target_nodes] + for edge in edge_pop.get_edges(): + src_pop = src_pop_maps[edge.source_node_id] + trg_pop = trg_pop_maps[edge.target_node_id] + conn_key = (src_pop, trg_pop) + if conn_key not in self._connections: + conn = PopConnection(src_pop, trg_pop) + self._connections[conn_key] = conn + self._all_connections.append(conn) + + self._connections[conn_key].add_edge(edge) + + elif edge_pop.mixed_connections: + raise NotImplementedError() + + for conn in self._connections.values(): + conn.build() + + """ + recurrent_edges = [edge_pop for _, edge_list in self._recurrent_edges.items() for edge_pop in edge_list] + for edge_pop in recurrent_edges: + prop_maps = self._edge_property_maps[edge_pop.name] + src_pop_maps = self._nodeid2pop_map[edge_pop.source_population] + trg_pop_maps = self._nodeid2pop_map[edge_pop.target_population] + for edge in edge_pop: + src_pop = src_pop_maps[edge.source_node_id] + trg_pop = trg_pop_maps[edge.target_node_id] + conn_key = (src_pop, trg_pop) + if conn_key not in self._connections: + conn = PopConnection(src_pop, trg_pop) + self._connections[conn_key] = conn + self._all_connections.append(conn) + + pop_edge = PopEdge(edge, prop_maps[edge.group_id], self) + self._connections[conn_key].add_edge(pop_edge) + + for conn in self._connections.values(): + conn.build() + # print len(self._connections) + """ + + def find_edges(self, source_nodes=None, target_nodes=None): + # TODO: Move to parent + selected_edges = self._edge_populations[:] + + if source_nodes is not None: + selected_edges = [edge_pop for edge_pop in selected_edges if edge_pop.source_nodes == source_nodes] + + if target_nodes is not None: + selected_edges = [edge_pop for edge_pop in selected_edges if edge_pop.target_nodes == target_nodes] + + return selected_edges + + def add_spike_trains(self, spike_trains, node_set): + # Build external node populations + src_nodes = [node_pop for node_pop in self.node_populations if node_pop.name in node_set.population_names()] + for node_pop in src_nodes: + pop_name = node_pop.name + if node_pop.name not in self._external_pop: + external_pop_map = {} + src_pop_map = {} + for node in node_pop.get_nodes(): + pop_key = node[self._group_key] + if pop_key not in external_pop_map: + pop = ExtPopulation(pop_key) + external_pop_map[pop_key] = pop + self._all_populations.append(pop) + + pop = external_pop_map[pop_key] + pop.add_node(node) + src_pop_map[node.node_id] = pop + + self._nodeid2pop_map[pop_name] = src_pop_map + + firing_rates = poputils.get_firing_rates(external_pop_map.values(), spike_trains) + self._external_pop[pop_name] = external_pop_map + for dpop in external_pop_map.values(): + dpop.build(firing_rates[dpop.pop_id]) + + else: + # TODO: Throw error spike trains should only be called once per source population + # external_pop_map = self._external_pop[pop_name] + src_pop_map = self._nodeid2pop_map[pop_name] + + unbuilt_connections = [] + for source_reader in src_nodes: + for edge_pop in self.find_edges(source_nodes=source_reader.name): + trg_pop_map = self._nodeid2pop_map[edge_pop.target_nodes] + for edge in edge_pop.get_edges(): + src_pop = src_pop_map[edge.source_node_id] + trg_pop = trg_pop_map[edge.target_node_id] + conn_key = (src_pop, trg_pop) + if conn_key not in self._external_connections: + pconn = PopConnection(src_pop, trg_pop) + self._external_connections[conn_key] = pconn + unbuilt_connections.append(pconn) + self._all_connections.append(pconn) + + #pop_edge = PopEdge(edge, prop_maps[edge.group_id], self) + self._external_connections[conn_key].add_edge(edge) + + for pedge in unbuilt_connections: + pedge.build() + #exit() + + """ + print node_pop.name + + + exit() + if node_pop.name in self._virtual_ids_map: + continue + + virt_node_map = {} + if node_pop.virtual_nodes_only: + print 'HERE' + exit() + + + for pop_name, node_pop in self._virtual_populations_map.items(): + if pop_name not in spike_trains.populations: + continue + + # Build external population if it already hasn't been built + if pop_name not in self._external_pop: + prop_maps = self._node_property_maps[pop_name] + external_pop_map = {} + src_pop_map = {} + for node in node_pop: + pop_key = node[self._group_key] + pnode = PopNode(node, prop_maps[node.group_id], self) + if pop_key not in external_pop_map: + pop = ExtPopulation(pop_key) + external_pop_map[pop_key] = pop + self._all_populations.append(pop) + + pop = external_pop_map[pop_key] + pop.add_node(pnode) + src_pop_map[node.node_id] = pop + + self._nodeid2pop_map[pop_name] = src_pop_map + + firing_rates = poputils.get_firing_rates(external_pop_map.values(), spike_trains) + self._external_pop[pop_name] = external_pop_map + for dpop in external_pop_map.values(): + dpop.build(firing_rates[dpop.pop_id]) + + else: + # TODO: Throw error spike trains should only be called once per source population + # external_pop_map = self._external_pop[pop_name] + src_pop_map = self._nodeid2pop_map[pop_name] + + unbuilt_connections = [] + for node_pop in self._internal_populations_map.values(): + trg_pop_map = self._nodeid2pop_map[node_pop.name] + for edge_pop in self.external_edge_populations(src_pop=pop_name, trg_pop=node_pop.name): + for edge in edge_pop: + src_pop = src_pop_map[edge.source_node_id] + trg_pop = trg_pop_map[edge.target_node_id] + conn_key = (src_pop, trg_pop) + if conn_key not in self._external_connections: + pconn = PopConnection(src_pop, trg_pop) + self._external_connections[conn_key] = pconn + unbuilt_connections.append(pconn) + self._all_connections.append(pconn) + + pop_edge = PopEdge(edge, prop_maps[edge.group_id], self) + self._external_connections[conn_key].add_edge(pop_edge) + + for pedge in unbuilt_connections: + pedge.build() + """ + + + def add_rates(self, rates, node_set): + if self._group_key == 'node_id': + id_lookup = lambda n: n.node_id + else: + id_lookup = lambda n: n[self._group_key] + + src_nodes = [node_pop for node_pop in self.node_populations if node_pop.name in node_set.population_names()] + for node_pop in src_nodes: + pop_name = node_pop.name + if node_pop.name not in self._external_pop: + external_pop_map = {} + src_pop_map = {} + for node in node_pop.get_nodes(): + pop_key = id_lookup(node) + if pop_key not in external_pop_map: + pop = ExtPopulation(pop_key) + external_pop_map[pop_key] = pop + self._all_populations.append(pop) + + pop = external_pop_map[pop_key] + pop.add_node(node) + src_pop_map[node.node_id] = pop + + self._nodeid2pop_map[pop_name] = src_pop_map + + self._external_pop[pop_name] = external_pop_map + for dpop in external_pop_map.values(): + firing_rates = rates.get_rate(dpop.pop_id) + dpop.build(firing_rates) + + else: + # TODO: Throw error spike trains should only be called once per source population + # external_pop_map = self._external_pop[pop_name] + src_pop_map = self._nodeid2pop_map[pop_name] + + unbuilt_connections = [] + for source_reader in src_nodes: + for edge_pop in self.find_edges(source_nodes=source_reader.name): + trg_pop_map = self._nodeid2pop_map[edge_pop.target_nodes] + for edge in edge_pop.get_edges(): + src_pop = src_pop_map[edge.source_node_id] + trg_pop = trg_pop_map[edge.target_node_id] + conn_key = (src_pop, trg_pop) + if conn_key not in self._external_connections: + pconn = PopConnection(src_pop, trg_pop) + self._external_connections[conn_key] = pconn + unbuilt_connections.append(pconn) + self._all_connections.append(pconn) + + #pop_edge = PopEdge(edge, prop_maps[edge.group_id], self) + self._external_connections[conn_key].add_edge(edge) + + for pedge in unbuilt_connections: + pedge.build() + + """ + for pop_name, node_pop in self._virtual_populations_map.items(): + if pop_name not in rates.populations: + continue + + # Build external population if it already hasn't been built + if pop_name not in self._external_pop: + prop_maps = self._node_property_maps[pop_name] + external_pop_map = {} + src_pop_map = {} + for node in node_pop: + pop_key = id_lookup(node) + #pop_key = node[self._group_key] + pnode = PopNode(node, prop_maps[node.group_id], self) + if pop_key not in external_pop_map: + pop = ExtPopulation(pop_key) + external_pop_map[pop_key] = pop + self._all_populations.append(pop) + + pop = external_pop_map[pop_key] + pop.add_node(pnode) + src_pop_map[node.node_id] = pop + + self._nodeid2pop_map[pop_name] = src_pop_map + + firing_rate = rates.get_rate(pop_key) + self._external_pop[pop_name] = external_pop_map + for dpop in external_pop_map.values(): + dpop.build(firing_rate) + + else: + # TODO: Throw error spike trains should only be called once per source population + # external_pop_map = self._external_pop[pop_name] + src_pop_map = self._nodeid2pop_map[pop_name] + """ + + ''' + def _add_node(self, node, network): + pops = self._networks[network] + pop_key = node[self._group_key] + if pop_key in pops: + pop = pops[pop_key] + pop.add_gid(node.gid) + self._gid_table[network][node.gid] = pop + else: + model_class = self.property_schema.get_pop_type(node) + if model_class == PopTypes.Internal: + pop = InternalNode(pop_key, self, network, node) + pop.add_gid(node.gid) + pop.model_params = self.__get_params(node) + self._add_internal_node(pop, network) + + elif model_class == PopTypes.External: + # TODO: See if we can get firing rate from dynamics_params + pop = ExternalPopulation(pop_key, self, network, node) + pop.add_gid(node.gid) + self._add_external_node(pop, network) + + else: + raise Exception('Unknown model type') + + if network not in self._gid_table: + self._gid_table[network] = {} + self._gid_table[network][node.gid] = pop + ''' + + def __get_params(self, node_params): + if node_params.with_dynamics_params: + return node_params['dynamics_params'] + + params_file = node_params[self._params_column] + if params_file in self._params_cache: + return self._params_cache[params_file] + else: + params_dir = self.get_component('models_dir') + params_path = os.path.join(params_dir, params_file) + params_dict = json.load(open(params_path, 'r')) + self._params_cache[params_file] = params_dict + return params_dict + + def _preprocess_node_types(self, node_population): + node_type_ids = np.unique(node_population.type_ids) + # TODO: Verify all the node_type_ids are in the table + node_types_table = node_population.types_table + + if 'dynamics_params' in node_types_table.columns and 'model_type' in node_types_table.columns: + for nt_id in node_type_ids: + node_type = node_types_table[nt_id] + dynamics_params = node_type['dynamics_params'] + model_type = node_type['model_type'] + + if model_type == 'biophysical': + params_dir = self.get_component('biophysical_neuron_models_dir') + elif model_type == 'point_process': + params_dir = self.get_component('point_neuron_models_dir') + elif model_type == 'point_soma': + params_dir = self.get_component('point_neuron_models_dir') + elif model_type == 'population': + params_dir = self.get_component('population_models_dir') + else: + # Not sure what to do in this case, throw Exception? + params_dir = self.get_component('custom_neuron_models') + + params_path = os.path.join(params_dir, dynamics_params) + + # see if we can load the dynamics_params as a dictionary. Otherwise just save the file path and let the + # cell_model loader function handle the extension. + try: + params_val = json.load(open(params_path, 'r')) + node_type['dynamics_params'] = params_val + except Exception: + # TODO: Check dynamics_params before + self.io.log_exception('Could not find node dynamics_params file {}.'.format(params_path)) + + + ''' + def add_edges(self, edges, target_network=None, source_network=None): + # super(PopGraph, self).add_edges(edges) + + target_network = target_network if target_network is not None else edges.target_network + if target_network not in self._target_edges: + self._target_edges[target_network] = [] + + source_network = source_network if source_network is not None else edges.source_network + if source_network not in self._source_edges: + self._source_edges[source_network] = [] + + target_pops = self.get_populations(target_network) + source_pops = self.get_populations(source_network) + source_gid_table = self._gid_table[source_network] + + for target_pop in target_pops: + for target_gid in target_pop.get_gids(): + for edge in edges.edges_itr(target_gid): + source_pop = source_gid_table[edge.source_gid] + self._add_edge(source_pop, target_pop, edge) + ''' + + def _add_edge(self, source_pop, target_pop, edge): + src_id = source_pop.node_id + trg_id = target_pop.node_id + edge_type_id = edge['edge_type_id'] + edge_key = (src_id, source_pop.network, trg_id, target_pop.network, edge_type_id) + + if edge_key in self._edges: + return + else: + # TODO: implement dynamics params + dynamics_params = self._get_edge_params(edge) + pop_edge = PopEdge(source_pop, target_pop, edge, dynamics_params) + self._edges[edge_key] = pop_edge + self._source_edges[source_pop.network].append(pop_edge) + self._target_edges[target_pop.network].append(pop_edge) + + def get_edges(self, source_network): + return self._source_edges[source_network] + + def edges_table(self, target_network, source_network): + return self._edges_table[(target_network, source_network)] + + def get_populations(self, network): + return super(PopNetwork, self).get_nodes(network) + + def get_population(self, node_set, gid): + return self._nodeid2pop_map[node_set][gid] + + def rebuild(self): + for _, ns in self._nodeid2pop_map.items(): + for _, pop in ns.items(): + pop.build() + + for pc in self._all_connections: + pc.build() \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/popnet/popnetwork_OLD.py b/bmtk-vb/bmtk/simulator/popnet/popnetwork_OLD.py new file mode 100644 index 0000000..cfdeddb --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/popnetwork_OLD.py @@ -0,0 +1,327 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import logging + +from dipde.internals.internalpopulation import InternalPopulation +from dipde.internals.externalpopulation import ExternalPopulation +from dipde.internals.connection import Connection +import dipde + +import bmtk.simulator.popnet.config as cfg +import bmtk.simulator.popnet.utils as poputils + + +class PopNetwork (object): + def __init__(self, graph): + self._graph = graph + + self._duration = 0.0 + self._dt = 0.0001 + self._rates_file = None # name of file where the output is saved + + self.__population_list = [] # list of all populations, internal and external + self.__population_table = {graph: {} for graph in self._graph.networks} # population lookup by [network][id] + self.__connection_list = [] # list of all connections + self._dipde_network = None # reference to dipde.Network object + + # diction of rates for every external network/pop_id. Prepopulate dictionary with populations whose rates + # have already been manually set, otherwise they should use one of the add_rates_* function. + self._rates = {network: {pop.pop_id: pop.firing_rate for pop in self._graph.get_populations(network) + if not pop.is_internal and pop.is_firing_rate_set} + for network in self._graph.networks} + + """ + for network in self._graph.networks: + for pop in self._graph.get_populations(network): + + if pop.is_internal: + dipde_pop = self.__create_internal_pop(pop) + + else: + if pop.is_firing_rate_set: + rates = pop.firing_rate + """ + + @property + def duration(self): + return self._duration + + @duration.setter + def duration(self, value): + self._duration = value + + @property + def dt(self): + return self._dt + + @dt.setter + def dt(self, value): + self._dt = value + + @property + def rates_file(self): + return self._rates_file + + @rates_file.setter + def rates_file(self, value): + self._rates_file = value + + @property + def populations(self): + return self.__population_list + + @property + def connections(self): + return self.__connection_list + + def add_rates_nwb(self, network, nwb_file, trial, force=False): + """Creates external population firing rates from an NWB file. + + Will iterate through a processing trial of an NWB file by assigning gids the population it belongs too and + taking the average firing rate. + + This should be done before calling build_cells(). If a population has already been assigned a firing rate an + error will occur unless force=True. + + :param network: Name of network with external populations. + :param nwb_file: NWB file with spike rates. + :param trial: trial id in NWB file + :param force: will overwrite existing firing rates + """ + existing_rates = self._rates[network] # TODO: validate network exists + # Get all unset, external populations in a network. + network_pops = self._graph.get_populations(network) + selected_pops = [] + for pop in network_pops: + if pop.is_internal: + continue + elif not force and pop.pop_id in existing_rates: + print('Firing rate for {}/{} has already been set, skipping.'.format(network, pop.pop_id)) + else: + selected_pops.append(pop) + + if selected_pops: + # assign firing rates from NWB file + # TODO: + rates_dict = poputils.get_firing_rate_from_nwb(selected_pops, nwb_file, trial) + self._rates[network].update(rates_dict) + + def add_rate_hz(self, network, pop_id, rate, force=False): + """Set the firing rate of an external population. + + This should be done before calling build_cells(). If a population has already been assigned a firing rate an + error will occur unless force=True. + + :param network: name of network with wanted exteranl population + :param pop_id: name/id of external population + :param rate: firing rate in Hz. + :param force: will overwrite existing firing rates + """ + self.__add_rates_validator(network, pop_id, force) + self._rates[network][pop_id] = rate + + def __add_rates_validator(self, network, pop_id, force): + if network not in self._graph.networks: + raise Exception('No network {} found in PopGraph.'.format(network)) + + pop = self._graph.get_population(network, pop_id) + if pop is None: + raise Exception('No population with id {} found in {}.'.format(pop_id, network)) + if pop.is_internal: + raise Exception('Population {} in {} is not an external population.'.format(pop_id, network)) + if not force and pop_id in self._rates[network]: + raise Exception('The firing rate for {}/{} already set and force=False.'.format(network, pop_id)) + + def _get_rate(self, network, pop): + """Gets the firing rate for a given population""" + return self._rates[network][pop.pop_id] + + def build_populations(self): + """Build dipde Population objects from graph nodes. + + To calculate external populations firing rates, it first see if a population's firing rate has been manually + set in the graph. Otherwise it attempts to calulate the firing rate from the call to add_rate_hz, add_rates_NWB, + etc. (which should be called first). + """ + for network in self._graph.networks: + for pop in self._graph.get_populations(network): + if pop.is_internal: + dipde_pop = self.__create_internal_pop(pop) + + else: + dipde_pop = self.__create_external_pop(pop, self._get_rate(network, pop)) + + self.__population_list.append(dipde_pop) + self.__population_table[network][pop.pop_id] = dipde_pop + + def set_logging(self, log_file): + # TODO: move this out of the function, put in io class + if os.path.exists(log_file): + os.remove(log_file) + + # get root logger + logger = logging.getLogger() + for h in list(logger.handlers): + # remove existing handlers that will write to console. + logger.removeHandler(h) + + # creates handler that write to log_file + logging.basicConfig(filename=log_file, filemode='w', level=logging.DEBUG) + + def set_external_connections(self, network_name): + """Sets the external connections for populations in a given network. + + :param network_name: name of external network with External Populations to connect to internal pops. + """ + for edge in self._graph.get_edges(network_name): + # Get source and target populations + src = edge.source + source_pop = self.__population_table[src.network][src.pop_id] + trg = edge.target + target_pop = self.__population_table[trg.network][trg.pop_id] + + # build a connection. + self.__connection_list.append(self.__create_connection(source_pop, target_pop, edge)) + + def set_recurrent_connections(self): + """Initialize internal connections.""" + for network in self._graph.internal_networks(): + for edge in self._graph.get_edges(network): + src = edge.source + source_pop = self.__population_table[src.network][src.pop_id] + trg = edge.target + target_pop = self.__population_table[trg.network][trg.pop_id] + self.__connection_list.append(self.__create_connection(source_pop, target_pop, edge)) + + def run(self, duration=None): + # TODO: Check if cells/connections need to be rebuilt. + + # Create the networ + self._dipde_network = dipde.Network(population_list=self.populations, connection_list=self.__connection_list) + + if duration is None: + duration = self.duration + + print("running simulation...") + self._dipde_network.run(t0=0.0, tf=duration, dt=self.dt) + # TODO: make record_rates optional? + self.__record_rates() + print("done simulation.") + + def __create_internal_pop(self, params): + # TODO: use getter methods directly in case arguments are not stored in dynamics params + # pop = InternalPopulation(**params.dynamics_params) + pop = InternalPopulation(**params.model_params) + return pop + + def __create_external_pop(self, params, rates): + pop = ExternalPopulation(rates, record=False) + return pop + + def __create_connection(self, source, target, params): + return Connection(source, target, nsyn=params.nsyns, delays=params.delay, weights=params.weight) + + def __record_rates(self): + with open(self._rates_file, 'w') as f: + # TODO: store internal populations separately, unless there is a reason to save external populations + # (there isn't and it will be problematic) + for network, pop_list in self.__population_table.items(): + for pop_id, pop in pop_list.items(): + if pop.record: + for time, rate in zip(pop.t_record, pop.firing_rate_record): + f.write('{} {} {}\n'.format(pop_id, time, rate)) + + @classmethod + def from_config(cls, configure, graph): + # load the json file or object + if isinstance(configure, basestring): + config = cfg.from_json(configure, validate=True) + elif isinstance(configure, dict): + config = configure + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(configure, type(configure))) + network = cls(graph) + + if 'run' not in config: + raise Exception('Json file is missing "run" entry. Unable to build Bionetwork.') + run_dict = config['run'] + + # Create the output file + if 'output' in config: + out_dict = config['output'] + + rates_file = out_dict.get('rates_file', None) + if rates_file is not None: + # create directory if required + network.rates_file = rates_file + parent_dir = os.path.dirname(rates_file) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir) + + if 'log_file' in out_dict: + log_file = out_dict['log_file'] + network.set_logging(log_file) + + # get network parameters + if 'duration' in run_dict: + network.duration = run_dict['duration'] + + if 'dt' in run_dict: + network.dt = run_dict['dt'] + + # TODO: need to get firing rates before building populations + if 'input' in config: + for netinput in config['input']: + if netinput['type'] == 'external_spikes' and netinput['format'] == 'nwb' and netinput['active']: + # Load external network spike trains from an NWB file. + print('Setting firing rates for {} from {}.'.format(netinput['source_nodes'], netinput['file'])) + network.add_rates_nwb(netinput['source_nodes'], netinput['file'], netinput['trial']) + + if netinput['type'] == 'pop_rate': + print('Setting {}/{} to fire at {} Hz.'.format(netinput['source_nodes'], netinput['pop_id'], netinput['rate'])) + network.add_rate_hz(netinput['source_nodes'], netinput['pop_id'], netinput['rate']) + + # TODO: take input as function with Population argument + + # Build populations + print('Building Populations') + network.build_populations() + + # Build recurrent connections + if run_dict['connect_internal']: + print('Building recurrention connections') + network.set_recurrent_connections() + + # Build external connections. Set connection to default True and turn off only if explicitly stated. + # NOTE: It might be better to set to default off?!?! Need to dicuss what would be more intuitive for the users. + # TODO: ignore case of network name + external_network_settings = {name: True for name in graph.external_networks()} + if 'connect_external' in run_dict: + external_network_settings.update(run_dict['connect_external']) + for netname, connect in external_network_settings.items(): + if connect: + print('Setting external connections for {}'.format(netname)) + network.set_external_connections(netname) + + return network diff --git a/bmtk-vb/bmtk/simulator/popnet/popnode.py b/bmtk-vb/bmtk/simulator/popnet/popnode.py new file mode 100644 index 0000000..6288762 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/popnode.py @@ -0,0 +1,158 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from bmtk.simulator.utils.graph import SimNode + +class PopNode(SimNode): + def __init__(self, node_id, graph, network, params): + self._graph = graph + self._node_id = node_id + self._network = network + self._graph_params = params + + self._dynamics_params = {} + self._updated_params = {'dynamics_params': self._dynamics_params} + + self._gids = set() + + @property + def node_id(self): + return self._node_id + + @property + def pop_id(self): + return self._node_id + + @property + def network(self): + return self._network + + @property + def dynamics_params(self): + return self._dynamics_params + + @dynamics_params.setter + def dynamics_params(self, value): + self._dynamics_params = value + + @property + def is_internal(self): + return False + + def __getitem__(self, item): + if item in self._updated_params: + return self._updated_params[item] + elif item in self._graph_params: + return self._graph_params[item] + elif self._model_params is not None: + return self._model_params[item] + + def add_gid(self, gid): + self._gids.add(gid) + + def get_gids(self): + return list(self._gids) + + +class InternalNode(PopNode): + """ + def __init__(self, node_id, graph, network, params): + super(InternalNode, self).__init__(node_id, graph, network, params) + #self._pop_id = node_id + #self._graph = graph + #self._network = network + #self._graph_params = params + #self._dynamics_params = {} + #self._update_params = {'dynamics_params': self._dynamics_params} + """ + @property + def tau_m(self): + return self['tau_m'] + #return self._dynamics_params.get('tau_m', None) + + @tau_m.setter + def tau_m(self, value): + #return self['tau_m'] + self._dynamics_params['tau_m'] = value + + @property + def v_max(self): + return self._dynamics_params.get('v_max', None) + + @v_max.setter + def v_max(self, value): + self._dynamics_params['v_max'] = value + + @property + def dv(self): + return self._dynamics_params.get('dv', None) + + @dv.setter + def dv(self, value): + self._dynamics_params['dv'] = value + + @property + def v_min(self): + return self._dynamics_params.get('v_min', None) + + @v_min.setter + def v_min(self, value): + self._dynamics_params['v_min'] = value + + @property + def is_internal(self): + return True + + def __repr__(self): + props = 'pop_id={}, tau_m={}, v_max={}, v_min={}, dv={}'.format(self.pop_id, self.tau_m, self.v_max, self.v_min, + self.dv) + return 'InternalPopulation({})'.format(props) + + +class ExternalPopulation(PopNode): + def __init__(self, node_id, graph, network, params): + super(ExternalPopulation, self).__init__(node_id, graph, network, params) + self._firing_rate = -1 + if 'firing_rate' in params: + self._firing_rate = params['firing_rate'] + + @property + def firing_rate(self): + return self._firing_rate + + @property + def is_firing_rate_set(self): + return self._firing_rate >= 0 + + @firing_rate.setter + def firing_rate(self, rate): + assert(isinstance(rate, float) and rate >= 0) + self._firing_rate = rate + + @property + def is_internal(self): + return False + + def __repr__(self): + props = 'pop_id={}, firing_rate={}'.format(self.pop_id, self.firing_rate) + return 'ExternalPopulation({})'.format(props) + diff --git a/bmtk-vb/bmtk/simulator/popnet/popsimulator.py b/bmtk-vb/bmtk/simulator/popnet/popsimulator.py new file mode 100644 index 0000000..38c660a --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/popsimulator.py @@ -0,0 +1,451 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import logging +from six import string_types + +from dipde.internals.internalpopulation import InternalPopulation +from dipde.internals.externalpopulation import ExternalPopulation +from dipde.internals.connection import Connection +import dipde + +from bmtk.simulator.core.simulator import Simulator +from . import config as cfg +from . import utils as poputils +import bmtk.simulator.utils.simulation_inputs as inputs +from bmtk.utils.io import spike_trains, firing_rates + + +class PopSimulator(Simulator): + def __init__(self, graph, dt=0.0001, tstop=0.0, overwrite=True): + self._graph = graph + + self._tstop = tstop + self._dt = dt + self._rates_file = None # name of file where the output is saved + + self.__population_list = [] # list of all populations, internal and external + #self.__population_table = {graph: {} for graph in self._graph.networks} # population lookup by [network][id] + self.__connection_list = [] # list of all connections + self._dipde_network = None # reference to dipde.Network object + + # diction of rates for every external network/pop_id. Prepopulate dictionary with populations whose rates + # have already been manually set, otherwise they should use one of the add_rates_* function. + #self._rates = {network: {pop.pop_id: pop.firing_rate for pop in self._graph.get_populations(network) + # if not pop.is_internal and pop.is_firing_rate_set} + # for network in self._graph.networks} + + """ + for network in self._graph.networks: + for pop in self._graph.get_populations(network): + + if pop.is_internal: + dipde_pop = self.__create_internal_pop(pop) + + else: + if pop.is_firing_rate_set: + rates = pop.firing_rate + """ + + @property + def tstop(self): + return self._tstop + + @tstop.setter + def tstop(self, value): + self._tstop = value + + @property + def dt(self): + return self._dt + + @dt.setter + def dt(self, value): + self._dt = value + + @property + def rates_file(self): + return self._rates_file + + @rates_file.setter + def rates_file(self, value): + self._rates_file = value + + @property + def populations(self): + return self.__population_list + + @property + def connections(self): + return self.__connection_list + + def add_rates_nwb(self, network, nwb_file, trial, force=False): + """Creates external population firing rates from an NWB file. + + Will iterate through a processing trial of an NWB file by assigning gids the population it belongs too and + taking the average firing rate. + + This should be done before calling build_cells(). If a population has already been assigned a firing rate an + error will occur unless force=True. + + :param network: Name of network with external populations. + :param nwb_file: NWB file with spike rates. + :param trial: trial id in NWB file + :param force: will overwrite existing firing rates + """ + existing_rates = self._rates[network] # TODO: validate network exists + # Get all unset, external populations in a network. + network_pops = self._graph.get_populations(network) + selected_pops = [] + for pop in network_pops: + if pop.is_internal: + continue + elif not force and pop.pop_id in existing_rates: + print('Firing rate for {}/{} has already been set, skipping.'.format(network, pop.pop_id)) + else: + selected_pops.append(pop) + + if selected_pops: + # assign firing rates from NWB file + # TODO: + rates_dict = poputils.get_firing_rate_from_nwb(selected_pops, nwb_file, trial) + self._rates[network].update(rates_dict) + + def add_rate_hz(self, network, pop_id, rate, force=False): + """Set the firing rate of an external population. + + This should be done before calling build_cells(). If a population has already been assigned a firing rate an + error will occur unless force=True. + + :param network: name of network with wanted exteranl population + :param pop_id: name/id of external population + :param rate: firing rate in Hz. + :param force: will overwrite existing firing rates + """ + self.__add_rates_validator(network, pop_id, force) + self._rates[network][pop_id] = rate + + def __add_rates_validator(self, network, pop_id, force): + if network not in self._graph.networks: + raise Exception('No network {} found in PopGraph.'.format(network)) + + pop = self._graph.get_population(network, pop_id) + if pop is None: + raise Exception('No population with id {} found in {}.'.format(pop_id, network)) + if pop.is_internal: + raise Exception('Population {} in {} is not an external population.'.format(pop_id, network)) + if not force and pop_id in self._rates[network]: + raise Exception('The firing rate for {}/{} already set and force=False.'.format(network, pop_id)) + + def _get_rate(self, network, pop): + """Gets the firing rate for a given population""" + return self._rates[network][pop.pop_id] + + def build_populations(self): + """Build dipde Population objects from graph nodes. + + To calculate external populations firing rates, it first see if a population's firing rate has been manually + set in the graph. Otherwise it attempts to calulate the firing rate from the call to add_rate_hz, add_rates_NWB, + etc. (which should be called first). + """ + for network in self._graph.networks: + for pop in self._graph.get_populations(network): + if pop.is_internal: + dipde_pop = self.__create_internal_pop(pop) + + else: + dipde_pop = self.__create_external_pop(pop, self._get_rate(network, pop)) + + self.__population_list.append(dipde_pop) + self.__population_table[network][pop.pop_id] = dipde_pop + + def set_logging(self, log_file): + # TODO: move this out of the function, put in io class + if os.path.exists(log_file): + os.remove(log_file) + + # get root logger + logger = logging.getLogger() + for h in list(logger.handlers): + # remove existing handlers that will write to console. + logger.removeHandler(h) + + # creates handler that write to log_file + logging.basicConfig(filename=log_file, filemode='w', level=logging.DEBUG) + + def set_external_connections(self, network_name): + """Sets the external connections for populations in a given network. + + :param network_name: name of external network with External Populations to connect to internal pops. + """ + for edge in self._graph.get_edges(network_name): + # Get source and target populations + src = edge.source + source_pop = self.__population_table[src.network][src.pop_id] + trg = edge.target + target_pop = self.__population_table[trg.network][trg.pop_id] + + # build a connection. + self.__connection_list.append(self.__create_connection(source_pop, target_pop, edge)) + + def set_recurrent_connections(self): + """Initialize internal connections.""" + for network in self._graph.internal_networks(): + for edge in self._graph.get_edges(network): + src = edge.source + source_pop = self.__population_table[src.network][src.pop_id] + trg = edge.target + target_pop = self.__population_table[trg.network][trg.pop_id] + self.__connection_list.append(self.__create_connection(source_pop, target_pop, edge)) + + def run(self, tstop=None): + # TODO: Check if cells/connections need to be rebuilt. + + # Create the networ + dipde_pops = [p.dipde_obj for p in self._graph.populations] + dipde_conns = [c.dipde_obj for c in self._graph.connections] + #print dipde_pops + #print dipde_conns + #exit() + + self._dipde_network = dipde.Network(population_list=dipde_pops, connection_list=dipde_conns) + + #self._dipde_network = dipde.Network(population_list=self._graph.populations, + # connection_list=self._graph.connections) + + if tstop is None: + tstop = self.tstop + + #print tstop, self.dt + #print self._graph.populations + #exit() + print("running simulation...") + self._dipde_network.run(t0=0.0, tf=tstop, dt=self.dt) + # TODO: make record_rates optional? + self.__record_rates() + print("done simulation.") + + def __create_internal_pop(self, params): + # TODO: use getter methods directly in case arguments are not stored in dynamics params + # pop = InternalPopulation(**params.dynamics_params) + pop = InternalPopulation(**params.model_params) + return pop + + def __create_external_pop(self, params, rates): + pop = ExternalPopulation(rates, record=False) + return pop + + def __create_connection(self, source, target, params): + return Connection(source, target, nsyn=params.nsyns, delays=params.delay, weights=params.weight) + + def __record_rates(self): + with open(self._rates_file, 'w') as f: + for pop in self._graph.internal_populations: + if pop.record: + for time, rate in zip(pop.dipde_obj.t_record, pop.dipde_obj.firing_rate_record): + f.write('{} {} {}\n'.format(pop.pop_id, time, rate)) + + ''' + @classmethod + def from_config(cls, configure, graph): + # load the json file or object + if isinstance(configure, basestring): + config = cfg.from_json(configure, validate=True) + elif isinstance(configure, dict): + config = configure + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(configure, type(configure))) + network = cls(graph) + + if 'run' not in config: + raise Exception('Json file is missing "run" entry. Unable to build Bionetwork.') + run_dict = config['run'] + + # Create the output file + if 'output' in config: + out_dict = config['output'] + + rates_file = out_dict.get('rates_file', None) + if rates_file is not None: + # create directory if required + network.rates_file = rates_file + parent_dir = os.path.dirname(rates_file) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir) + + if 'log_file' in out_dict: + log_file = out_dict['log_file'] + network.set_logging(log_file) + + # get network parameters + if 'duration' in run_dict: + network.duration = run_dict['duration'] + + if 'dt' in run_dict: + network.dt = run_dict['dt'] + + # TODO: need to get firing rates before building populations + if 'input' in config: + for netinput in config['input']: + if netinput['type'] == 'external_spikes' and netinput['format'] == 'nwb' and netinput['active']: + # Load external network spike trains from an NWB file. + print('Setting firing rates for {} from {}.'.format(netinput['source_nodes'], netinput['file'])) + network.add_rates_nwb(netinput['source_nodes'], netinput['file'], netinput['trial']) + + if netinput['type'] == 'pop_rate': + print('Setting {}/{} to fire at {} Hz.'.format(netinput['source_nodes'], netinput['pop_id'], netinput['rate'])) + network.add_rate_hz(netinput['source_nodes'], netinput['pop_id'], netinput['rate']) + + # TODO: take input as function with Population argument + + # Build populations + print('Building Populations') + network.build_populations() + + # Build recurrent connections + if run_dict['connect_internal']: + print('Building recurrention connections') + network.set_recurrent_connections() + + # Build external connections. Set connection to default True and turn off only if explicitly stated. + # NOTE: It might be better to set to default off?!?! Need to dicuss what would be more intuitive for the users. + # TODO: ignore case of network name + external_network_settings = {name: True for name in graph.external_networks()} + if 'connect_external' in run_dict: + external_network_settings.update(run_dict['connect_external']) + for netname, connect in external_network_settings.items(): + if connect: + print('Setting external connections for {}'.format(netname)) + network.set_external_connections(netname) + + return network + ''' + + @classmethod + def from_config(cls, configure, graph): + # load the json file or object + if isinstance(configure, string_types): + config = cfg.from_json(configure, validate=True) + elif isinstance(configure, dict): + config = configure + else: + raise Exception('Could not convert {} (type "{}") to json.'.format(configure, type(configure))) + + if 'run' not in config: + raise Exception('Json file is missing "run" entry. Unable to build Bionetwork.') + run_dict = config['run'] + + # Get network parameters + # step time (dt) is set in the kernel and should be passed + overwrite = run_dict['overwrite_output_dir'] if 'overwrite_output_dir' in run_dict else True + print_time = run_dict['print_time'] if 'print_time' in run_dict else False + dt = run_dict['dt'] # TODO: make sure dt exists + tstop = float(config.tstop) / 1000.0 + network = cls(graph, dt=config.dt, tstop=tstop, overwrite=overwrite) + + if 'output_dir' in config['output']: + network.output_dir = config['output']['output_dir'] + + # network.spikes_file = config['output']['spikes_ascii'] + + if 'block_run' in run_dict and run_dict['block_run']: + if 'block_size' not in run_dict: + raise Exception('"block_run" is set to True but "block_size" not found.') + network._block_size = run_dict['block_size'] + + if 'duration' in run_dict: + network.duration = run_dict['duration'] + + graph.io.log_info('Building cells.') + graph.build_nodes() + + graph.io.log_info('Building recurrent connections') + graph.build_recurrent_edges() + + for sim_input in inputs.from_config(config): + node_set = graph.get_node_set(sim_input.node_set) + if sim_input.input_type == 'spikes': + spikes = spike_trains.SpikesInput.load(name=sim_input.name, module=sim_input.module, + input_type=sim_input.input_type, params=sim_input.params) + graph.io.log_info('Build virtual cell stimulations for {}'.format(sim_input.name)) + graph.add_spike_trains(spikes, node_set) + else: + graph.io.log_info('Build virtual cell stimulations for {}'.format(sim_input.name)) + rates = firing_rates.RatesInput(sim_input.params) + graph.add_rates(rates, node_set) + + # Create the output file + if 'output' in config: + out_dict = config['output'] + + rates_file = out_dict.get('rates_file', None) + if rates_file is not None: + rates_file = rates_file if os.path.isabs(rates_file) else os.path.join(config.output_dir, rates_file) + # create directory if required + network.rates_file = rates_file + parent_dir = os.path.dirname(rates_file) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir) + + if 'log_file' in out_dict: + log_file = out_dict['log_file'] + network.set_logging(log_file) + + + # exit() + + + # build the cells + #io.log('Building cells') + #network.build_cells() + + # Build internal connections + #if run_dict['connect_internal']: + # io.log('Creating recurrent connections') + # network.set_recurrent_connections() + + # Build external connections. Set connection to default True and turn off only if explicitly stated. + # NOTE: It might be better to set to default off?!?! Need to dicuss what would be more intuitive for the users. + # TODO: ignore case of network name + + ''' + external_network_settings = {name: True for name in graph.external_networks()} + if 'connect_external' in run_dict: + external_network_settings.update(run_dict['connect_external']) + for netname, connect in external_network_settings.items(): + if connect: + io.log('Setting external connections for {}'.format(netname)) + network.set_external_connections(netname) + + # Build inputs + if 'input' in config: + for netinput in config['input']: + if netinput['type'] == 'external_spikes' and netinput['format'] == 'nwb' and netinput['active']: + network.add_spikes_nwb(netinput['source_nodes'], netinput['file'], netinput['trial']) + + io.log_info('Adding stimulations') + network.make_stims() + ''' + + graph.io.log_info('Network created.') + return network \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/popnet/property_schemas/__init__.py b/bmtk-vb/bmtk/simulator/popnet/property_schemas/__init__.py new file mode 100644 index 0000000..4d7c64c --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/property_schemas/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from base_schema import PopTypes +import property_schema_ver0 as v0 +import property_schema_ver1 as v1 + +DefaultPropertySchema = v1.PropertySchema() +AIPropertySchema = v0.PropertySchema() \ No newline at end of file diff --git a/bmtk-vb/bmtk/simulator/popnet/property_schemas/base_schema.py b/bmtk-vb/bmtk/simulator/popnet/property_schemas/base_schema.py new file mode 100644 index 0000000..cc880a6 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/property_schemas/base_schema.py @@ -0,0 +1,50 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +class PopTypes: + """Essentially an enum to store the type/group of each cell. It's faster and more robust than doing multiple string + comparisons. + """ + Internal = 0 + External = 1 + Other = 2 # should never really get here + + @staticmethod + def len(): + return 3 + + +class PropertySchema(object): + ####################################### + # For nodes/cells properties + ####################################### + def get_pop_type(self, pop_params): + model_type = pop_params['model_type'].lower() + if model_type == 'virtual' or model_type == 'external': + return PopTypes.External + elif model_type == 'internal': + return PopTypes.Internal + else: + return PopTypes.Unknown + + def get_params_column(self): + raise NotImplementedError() diff --git a/bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver0.py b/bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver0.py new file mode 100644 index 0000000..6c5c542 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver0.py @@ -0,0 +1,28 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from base_schema import PopTypes, PropertySchema as BaseSchema + + +class PropertySchema(BaseSchema): + def get_params_column(self): + return 'params_file' diff --git a/bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver1.py b/bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver1.py new file mode 100644 index 0000000..8794525 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/property_schemas/property_schema_ver1.py @@ -0,0 +1,28 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +from base_schema import PropertySchema as BaseSchema + + +class PropertySchema(BaseSchema): + def get_params_column(self): + return 'dynamics_params' diff --git a/bmtk-vb/bmtk/simulator/popnet/sonata_adaptors.py b/bmtk-vb/bmtk/simulator/popnet/sonata_adaptors.py new file mode 100644 index 0000000..dcc1300 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/sonata_adaptors.py @@ -0,0 +1,12 @@ +from bmtk.simulator.core.sonata_reader import NodeAdaptor, SonataBaseNode, EdgeAdaptor, SonataBaseEdge + + +class PopNetEdge(SonataBaseEdge): + @property + def syn_weight(self): + return self._edge['syn_weight'] + + +class PopEdgeAdaptor(EdgeAdaptor): + def get_edge(self, sonata_edge): + return PopNetEdge(sonata_edge, self) diff --git a/bmtk-vb/bmtk/simulator/popnet/utils.py b/bmtk-vb/bmtk/simulator/popnet/utils.py new file mode 100644 index 0000000..ceeeaa3 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/popnet/utils.py @@ -0,0 +1,287 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import math +import warnings +import numpy as np +import pandas as pd +import scipy.interpolate as spinterp +import collections +import h5py +import itertools +import scipy.io as sio +import json +import importlib + +""" +Most of these functions are not being used directly by popnet, but may still be used in some other capcity. These have +been marked as depreciated, and should be removed soon. + + +""" + + +def get_firing_rate_from_nwb(populations, nwb_file, trial): + """Calculates firing rates for an external population""" + h5_file = h5py.File(nwb_file, 'r') + spike_trains_ds = h5_file['processing'][trial]['spike_train'] + + # TODO: look into adding a time window rather than searching for min/max t. + firing_rates = {} + for pop in populations: + spike_counts = [] + spike_min_t = 1.0e30 + spike_max_t = 0.0 + for gid in pop.get_gids(): + spike_train_ds = spike_trains_ds[str(gid)]['data'] + if spike_train_ds is not None and len(spike_train_ds[...]) > 0: + spike_times = spike_train_ds[...] + tmp_min = min(spike_times) + spike_min_t = tmp_min if tmp_min < spike_min_t else spike_min_t + tmp_max = max(spike_times) + spike_max_t = tmp_max if tmp_max > spike_max_t else spike_max_t + spike_counts.append(len(spike_times)) + + # TODO make sure t_diffs is not null and spike_counts has some values + firing_rates[pop.pop_id] = 1.0e03 * np.mean(spike_counts) / (spike_max_t - spike_min_t) + return firing_rates + + +def get_firing_rates(populations, spike_trains): + """Calculates firing rates for an external population""" + #h5_file = h5py.File(nwb_file, 'r') + #spike_trains_ds = h5_file['processing'][trial]['spike_train'] + + # TODO: look into adding a time window rather than searching for min/max t. + firing_rates = {} + for pop in populations: + spike_counts = [] + spike_min_t = 1.0e30 + spike_max_t = 0.0 + for gid in pop.get_gids(): + spike_times = spike_trains.get_spikes(gid) + if spike_times is not None and len(spike_times) > 0: + tmp_min = min(spike_times) + spike_min_t = tmp_min if tmp_min < spike_min_t else spike_min_t + tmp_max = max(spike_times) + spike_max_t = tmp_max if tmp_max > spike_max_t else spike_max_t + spike_counts.append(len(spike_times)) + + # TODO make sure t_diffs is not null and spike_counts has some values + firing_rates[pop.pop_id] = 1.0e03 * np.mean(spike_counts) / (spike_max_t - spike_min_t) + return firing_rates + +############################################# +# Depreciated +############################################# +def list_of_dicts_to_dict_of_lists(list_of_dicts, default=None): + new_dict = {} + for curr_dict in list_of_dicts: + print(curr_dict.keys()) + + +############################################# +# Depreciated +############################################# +class KeyDefaultDict(collections.defaultdict): + def __missing__(self, key): + if self.default_factory is None: + raise KeyError + else: + ret = self[key] = self.default_factory(key) + return ret + + +############################################# +# Depreciated +############################################# +def create_firing_rate_server(t, y): + + warnings.warn('Hard coded bug fix for mindscope council 4/27/15') + t = t/.001/200 + interpolation_callable = spinterp.interp1d(t, y, bounds_error=False, fill_value=0) + return lambda t: interpolation_callable(t) + + +############################################# +# Depreciated +############################################# +def create_nwb_server_file_path(nwb_file_name, nwb_path): + f = h5py.File(nwb_file_name, 'r') + y = f['%s/data' % nwb_path][:] + dt = f['%s/data' % nwb_path].dims[0][0].value + t = np.arange(len(y))*dt + f.close() + return create_firing_rate_server(t, y) + + +############################################# +# Depreciated +############################################# +def get_mesoscale_connectivity_dict(): + + # Extract data into a dictionary: + mesoscale_data_dir = '/data/mat/iSee_temp_shared/packages/mesoscale_connectivity' + nature_data = {} + for mat, side in itertools.product(['W', 'PValue'],['ipsi', 'contra']): + data, row_labels, col_labels = [sio.loadmat(os.path.join(mesoscale_data_dir, '%s_%s.mat' % (mat, side)))[key] + for key in ['data', 'row_labels', 'col_labels']] + for _, (row_label, row) in enumerate(zip(row_labels, data)): + for _, (col_label, val) in enumerate(zip(col_labels, row)): + nature_data[mat, side, str(row_label.strip()), str(col_label.strip())] = val + + return nature_data + + +############################################# +# Depreciated +############################################# +def reorder_columns_in_frame(frame, var): + varlist = [w for w in frame.columns if w not in var] + return frame[var+varlist] + + +############################################# +# Depreciated +############################################# +def population_to_dict_for_dataframe(p): + + black_list = ['firing_rate_record', + 'initial_firing_rate', + 'metadata', + 't_record'] + + json_list = ['p0', 'tau_m'] + + return_dict = {} + p_dict = p.to_dict() + + for key, val in p_dict['metadata'].items(): + return_dict[key] = val + + for key, val in p_dict.items(): + if key not in black_list: + if key in json_list: + val = json.dumps(val) + return_dict[key] = val + + return return_dict + + +############################################# +# Depreciated +############################################# +def network_dict_to_target_adjacency_dict(network_dict): + print(network_dict) + + +############################################# +# Depreciated +############################################# +def population_list_to_dataframe(population_list): + df = pd.DataFrame({'_tmp': [None]}) + for p in population_list: + model_dict = {'_tmp': [None]} + for key, val in population_to_dict_for_dataframe(p).items(): + model_dict.setdefault(key, []).append(val) + df_tmp = pd.DataFrame(model_dict) + + df = pd.merge(df, df_tmp, how='outer') + df.drop('_tmp', inplace=True, axis=1) + return df + + +############################################# +# Depreciated +############################################# +def df_to_csv(df, save_file_name, index=False, sep=' ', na_rep='None'): + df.to_csv(save_file_name, index=index, sep=sep, na_rep=na_rep) + + +############################################# +# Depreciated +############################################# +def population_list_to_csv(population_list, save_file_name): + df = population_list_to_dataframe(population_list) + df_to_csv(df, save_file_name) + + +############################################# +# Depreciated +############################################# +def create_instance(data_dict): + '''Helper function to create an object from a dictionary containing: + + "module": The name of the module containing the class + "class": The name of the class to be used to create the object + ''' + + curr_module, curr_class = data_dict.pop('module'), data_dict.pop('class') + curr_instance = getattr(importlib.import_module(curr_module), curr_class)(**data_dict) + + return curr_instance + + +############################################# +# Depreciated +############################################# +def assert_model_known(model, model_dict): + """Test if a model in in the model_dict; if not, raise UnknownModelError""" + + try: + assert model in model_dict + except: + raise Exception('model {} does not exist.'.format(model)) + + +############################################# +# Depreciated +############################################# +def create_population_list(node_table, model_table): + """Create a population list from the node and model pandas tables""" + + model_dict = {} + for row in model_table.iterrows(): + model = row[1].to_dict() + model_dict[model.pop('model')] = model + + population_list = [] + for row in node_table.iterrows(): + node = row[1].to_dict() + model = node.pop('model') + + # Check if model type in model dict: + assert_model_known(model, model_dict) + + # Clean up: + curr_model = {} + for key, val in model_dict[model].items(): + if not (isinstance(val, float) and math.isnan(val)): + curr_model[key] = val + curr_model.setdefault('metadata', {})['model'] = model + + curr_module, curr_class = curr_model['module'], curr_model['class'] + curr_instance = getattr(importlib.import_module(curr_module), curr_class)(**curr_model) + population_list.append(curr_instance) + + return population_list diff --git a/bmtk-vb/bmtk/simulator/utils/__init__.py b/bmtk-vb/bmtk/simulator/utils/__init__.py new file mode 100644 index 0000000..04c8f88 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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/bmtk-vb/bmtk/simulator/utils/__init__.pyc b/bmtk-vb/bmtk/simulator/utils/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d08f5a6ef6acf68447b2b7296fdad9f8bf952911 GIT binary patch literal 145 zcmZSn%*$oj>ll;F00oRd+5w1*S%5?e14FO|NW@PANHCxg#a2Ku{m|mnqGJ8Bq{O1c zl8nS${o-5!pP83g5+AQuP+7tO)NYfTpHiBW LY6r5U7>F4FhhiWz literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/__pycache__/__init__.cpython-37.pyc b/bmtk-vb/bmtk/simulator/utils/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9640cf6a07bfad075f114701158f993bcd315fb GIT binary patch literal 145 zcmZ?b<>g`k0?S^Qb z`ejLpMTsRDiMjg4MalX}xh2^UqBt|RG$*knzevBdBr~U2KR!M)FS8^*Uaz3?7Kcr4 QeoARhsvXG8VjyM!056~;QUCw| literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/__pycache__/config.cpython-37.pyc b/bmtk-vb/bmtk/simulator/utils/__pycache__/config.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef4a035d1615f963344d8deb8d34097afacd10cc GIT binary patch literal 12881 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/__pycache__/sim_validator.cpython-37.pyc b/bmtk-vb/bmtk/simulator/utils/__pycache__/sim_validator.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2aa7e21a08ba71e07e45e8de430a402e2a5b6644 GIT binary patch literal 4479 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 Mn+a literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/config.py b/bmtk-vb/bmtk/simulator/utils/config.py new file mode 100644 index 0000000..aa5ee5e --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/config.py @@ -0,0 +1,438 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json +import re +import copy +import datetime +from six import string_types + + +from bmtk.simulator.core.io_tools import io + + +def from_json(config_file, validator=None): + """Builds and validates a configuration json file. + + :param config_file: File object or path to a json file. + :param validator: A SimConfigValidator object to validate json file. Won't validate if set to None + :return: A dictionary, verified against json validator and with manifest variables resolved. + """ + #print(config_file) + #if os.path.isfile(config_file): + #if isinstance(config_file, file): + # conf = json.load(config_file) + if isinstance(config_file, string_types): + conf = json.load(open(config_file, 'r')) + elif isinstance(config_file, dict): + conf = config_file.copy() + else: + raise Exception('{} is not a file or file path.'.format(config_file)) + + # insert file path into dictionary + if 'config_path' not in conf: + conf['config_path'] = os.path.abspath(config_file) + conf['config_dir'] = os.path.dirname(conf['config_path']) + + # Will resolve manifest variables and validate + return from_dict(conf, validator) + + +def from_dict(config_dict, validator=None): + """Builds and validates a configuration json dictionary object. Best to directly use from_json when possible. + + :param config_dict: Dictionary object + :param validator: A SimConfigValidator object to validate json file. Won't validate if set to None + :return: A dictionary, verified against json validator and with manifest variables resolved. + """ + assert(isinstance(config_dict, dict)) + conf = copy.deepcopy(config_dict) # Since the functions will mutate the dictionary we will copy just-in-case. + + if 'config_path' not in conf: + conf['config_path'] = os.path.join(os.getcwd(), 'tmp_cfg.dict') + conf['config_dir'] = os.path.dirname(conf['config_path']) + + # Build the manifest and resolve variables. + # TODO: Check that manifest exists + manifest = __build_manifest(conf) + conf['manifest'] = manifest + __recursive_insert(conf, manifest) + + # In our work with Blue-Brain it was agreed that 'network' and 'simulator' parts of config may be split up into + # separate files. If this is the case we build each sub-file separately and merge into this one + for childconfig in ['network', 'simulation']: + if childconfig in conf and isinstance(conf[childconfig], string_types): + # Try to resolve the path of the network/simulation config files. If an absolute path isn't used find + # the file relative to the current config file. TODO: test if this will work on windows? + conf_str = conf[childconfig] + conf_path = conf_str if conf_str.startswith('/') else os.path.join(conf['config_dir'], conf_str) + + # Build individual json file and merge into parent. + child_json = from_json(conf_path) + del child_json['config_path'] # we don't want 'config_path' of parent being overwritten. + conf.update(child_json) + + # Run the validator + if validator is not None: + validator.validate(conf) + + return conf + + +def copy_config(conf): + """Copy configuration file to different directory, with manifest variables resolved. + + :param conf: configuration dictionary + """ + output_dir = conf.output_dir + config_name = os.path.basename(conf['config_path']) + output_path = os.path.join(output_dir, config_name) + with open(output_path, 'w') as fp: + out_cfg = conf.copy() + if 'manifest' in out_cfg: + del out_cfg['manifest'] + json.dump(out_cfg, fp, indent=2) + + +def __special_variables(conf): + """A list of preloaded variables to insert into the manifest, containing things like path to run-time directory, + configuration directory, etc. + """ + pre_manifest = dict() + pre_manifest['$workingdir'] = os.path.dirname(os.getcwd()) + if 'config_path' in conf: + pre_manifest['$configdir'] = os.path.dirname(conf['config_path']) # path of configuration file + pre_manifest['$configfname'] = conf['config_path'] + + dt_now = datetime.datetime.now() + pre_manifest['$time'] = dt_now.strftime('%H-%M-%S') + pre_manifest['$date'] = dt_now.strftime('%Y-%m-%d') + pre_manifest['$datetime'] = dt_now.strftime('%Y-%m-%d_%H-%M-%S') + + return pre_manifest + + +def __build_manifest(conf): + """Resolves the manifest section and resolve any internal variables""" + if 'manifest' not in conf: + return __special_variables(conf) + + manifest = conf["manifest"] + resolved_manifest = __special_variables(conf) + resolved_keys = set() + unresolved_keys = set(manifest.keys()) + + # No longer using recursion since that can lead to an infinite loop if the person who writes the config file isn't + # careful. Also added code to allow for ${VAR} format in-case user wants to user "$.../some_${MODEl}_here/..." + while unresolved_keys: + for key in unresolved_keys: + # Find all variables in manifest and see if they can be replaced by the value in resolved_manifest + value = __find_variables(manifest[key], resolved_manifest) + + # If value no longer has variables, and key-value pair to resolved_manifest and remove from unresolved-keys + if value.find('$') < 0: + resolved_manifest[key] = value + resolved_keys.add(key) + + # remove resolved key-value pairs from set, and make sure at every iteration unresolved_keys shrinks to prevent + # infinite loops + n_unresolved = len(unresolved_keys) + unresolved_keys -= resolved_keys + if n_unresolved == len(unresolved_keys): + msg = "Unable to resolve manifest variables: {}".format(unresolved_keys) + raise Exception(msg) + + return resolved_manifest + + +def __recursive_insert(json_obj, manifest): + """Loop through the config and substitute the path variables (e.g.: $MY_DIR) with the values from the manifest + + :param json_obj: A json dictionary object that may contain variables needing to be resolved. + :param manifest: A dictionary of variable values + :return: A new json dictionar config file with variables resolved + """ + if isinstance(json_obj, string_types): + return __find_variables(json_obj, manifest) + + elif isinstance(json_obj, list): + new_list = [] + for itm in json_obj: + new_list.append(__recursive_insert(itm, manifest)) + return new_list + + elif isinstance(json_obj, dict): + for key, val in json_obj.items(): + if key == 'manifest': + continue + json_obj[key] = __recursive_insert(val, manifest) + + return json_obj + + else: + return json_obj + + +def __find_variables(json_str, manifest): + """Replaces variables (i.e. $VAR, ${VAR}) with their values from the manifest. + + :param json_str: a json string that may contain none, one or multiple variable + :param manifest: dictionary of variable lookup values + :return: json_str with resolved variables. Won't resolve variables that don't exist in manifest. + """ + variables = [m for m in re.finditer('\$\{?[\w]+\}?', json_str)] + for var in variables: + var_lookup = var.group() + if var_lookup.startswith('${') and var_lookup.endswith('}'): + # replace ${VAR} with $VAR + var_lookup = "$" + var_lookup[2:-1] + if var_lookup in manifest: + json_str = json_str.replace(var.group(), manifest[var_lookup]) + + return json_str + + +class ConfigDict(dict): + def __init__(self, *args, **kwargs): + self.update(*args, **kwargs) + self._env_built = False + self._io = None + + self._node_set = {} + self._load_node_set() + + @property + def io(self): + if self._io is None: + self._io = io + return self._io + + @io.setter + def io(self, io): + self._io = io + + @property + def run(self): + return self['run'] + + @property + def tstart(self): + return self.run.get('tstart', 0.0) + + @property + def tstop(self): + return self.run['tstop'] + + @property + def dt(self): + return self.run.get('dt', 0.1) + + @property + def spike_threshold(self): + return self.run.get('spike_threshold', -15.0) + + @property + def dL(self): + return self.run.get('dL', 20.0) + + @property + def gid_mappings(self): + return self.get('gid_mapping_file', None) + + @property + def block_step(self): + return self.run.get('nsteps_block', 5000) + + @property + def calc_ecp(self): + return self.run.get('calc_ecp', False) + + @property + def conditions(self): + return self['conditions'] + + @property + def celsius(self): + return self.conditions['celsius'] + + @property + def v_init(self): + return self.conditions['v_init'] + + @property + def path(self): + return self['config_path'] + + @property + def output(self): + return self['output'] + + @property + def output_dir(self): + return self.output['output_dir'] + + @property + def overwrite_output(self): + return self.output.get('overwrite_output_dir', False) + + @property + def log_file(self): + return self.output['log_file'] + + @property + def components(self): + return self.get('components', {}) + + @property + def morphologies_dir(self): + return self.components['morphologies_dir'] + + @property + def synaptic_models_dir(self): + return self.components['synaptic_models_dir'] + + @property + def point_neuron_models_dir(self): + return self.components['point_neuron_models_dir'] + + @property + def mechanisms_dir(self): + return self.components['mechanisms_dir'] + + @property + def biophysical_neuron_models_dir(self): + return self.components['biophysical_neuron_models_dir'] + + @property + def templates_dir(self): + return self.components.get('templates_dir', None) + + @property + def with_networks(self): + return 'networks' in self and len(self.nodes) > 0 + + @property + def networks(self): + return self['networks'] + + @property + def nodes(self): + return self.networks.get('nodes', []) + + @property + def edges(self): + return self.networks.get('edges', []) + + @property + def reports(self): + return self.get('reports', {}) + + @property + def inputs(self): + return self.get('inputs', {}) + + @property + def node_sets(self): + return self._node_set + + @property + def spikes_file(self): + return os.path.join(self.output_dir, self.output['spikes_file']) + + def _load_node_set(self): + if 'node_sets_file' in self.keys(): + node_set_val = self['node_sets_file'] + elif 'node_sets' in self.keys(): + node_set_val = self['node_sets'] + else: + self._node_set = {} + return + + if isinstance(node_set_val, dict): + self._node_set = node_set_val + else: + try: + self._node_set = json.load(open(node_set_val, 'r')) + except Exception as e: + io.log_exception('Unable to load node_sets_file {}'.format(node_set_val)) + + def copy_to_output(self): + copy_config(self) + + def get_modules(self, module_name): + return [report for report in self.reports.values() if report['module'] == module_name] + + def _set_logging(self): + """Check if log-level and/or log-format string is being changed through the config""" + output_sec = self.output + if 'log_format' in output_sec: + self._io.set_log_format(output_sec['log_format']) + + if 'log_level' in output_sec: + self._io.set_log_level(output_sec['log_level']) + + if 'log_to_console' in output_sec: + self._io.log_to_console = output_sec['log_to_console'] + + if 'quiet_simulator' in output_sec and output_sec['quiet_simulator']: + self._io.quiet_simulator() + + def build_env(self): + if self._env_built: + return + + self._set_logging() + self.io.setup_output_dir(self.output_dir, self.log_file, self.overwrite_output) + self.copy_to_output() + self._env_built = True + + @staticmethod + def get_validator(): + raise NotImplementedError + + @classmethod + def from_json(cls, config_file, validate=False): + validator = cls.get_validator() if validate else None + return cls(from_json(config_file, validator)) + + @classmethod + def from_dict(cls, config_dict, validate=False): + validator = cls.get_validator() if validate else None + return cls(from_dict(config_dict, validator)) + + @classmethod + def from_yaml(cls, config_file, validate=False): + raise NotImplementedError + + @classmethod + def load(cls, config_file, validate=False): + # Implement factory method that can resolve the format/type of input configuration. + if isinstance(config_file, dict): + return cls.from_dict(config_file, validate) + elif isinstance(config_file, string_types): + if config_file.endswith('yml') or config_file.endswith('yaml'): + return cls.from_yaml(config_file, validate) + else: + return cls.from_json(config_file, validate) + else: + raise Exception diff --git a/bmtk-vb/bmtk/simulator/utils/config.pyc b/bmtk-vb/bmtk/simulator/utils/config.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba09d6d7e22cf971dcd8b1f3998d2540171a44e3 GIT binary patch literal 16248 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/graph.py b/bmtk-vb/bmtk/simulator/utils/graph.py new file mode 100644 index 0000000..629ea1d --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/graph.py @@ -0,0 +1,408 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json +import ast +import numpy as np + +import config as cfg +from property_maps import NodePropertyMap, EdgePropertyMap +from bmtk.utils import sonata + + +"""Creates a graph of nodes and edges from multiple network files for all simulators. + +Consists of edges and nodes. All classes are abstract and should be reimplemented by a specific simulator. Also +contains base factor methods for building a network from a config file (or other). +""" + + +class SimEdge(object): + def __init__(self, original_params, dynamics_params): + self._orig_params = original_params + self._dynamics_params = dynamics_params + self._updated_params = {'dynamics_params': self._dynamics_params} + + @property + def edge_type_id(self): + return self._orig_params['edge_type_id'] + + def __getitem__(self, item): + if item in self._updated_params: + return self._updated_params[item] + else: + return self._orig_params[item] + + +class SimNode(object): + def __init__(self, node_id, graph, network, params): + self._node_id = node_id + self._graph = graph + self._graph_params = params + self._node_type_id = params['node_type_id'] + self._network = network + self._updated_params = {} + + self._model_params = {} + + @property + def node_id(self): + return self._node_id + + @property + def node_type_id(self): + return self._node_type_id + + @property + def network(self): + """Name of network node belongs too.""" + return self._network + + @property + def model_params(self): + """Parameters (json file, nml, dictionary) that describe a specific node""" + return self._model_params + + @model_params.setter + def model_params(self, value): + self._model_params = value + + def __contains__(self, item): + return item in self._updated_params or item in self._graph_params + + def __getitem__(self, item): + if item in self._updated_params: + return self._updated_params[item] + else: + return self._graph_params[item] + + +class SimGraph(object): + model_type_col = 'model_type' + + def __init__(self): + self._components = {} # components table, i.e. paths to model files. + self._io = None # TODO: create default io module (without mpi) + + self._node_property_maps = {} + self._edge_property_maps = {} + + self._node_populations = {} + self._internal_populations_map = {} + self._virtual_populations_map = {} + + self._virtual_cells_nid = {} + + self._recurrent_edges = {} + self._external_edges = {} + + @property + def io(self): + return self._io + + @property + def internal_pop_names(self): + return self + + @property + def node_populations(self): + return list(self._node_populations.keys()) + + def get_component(self, key): + """Get the value of item in the components dictionary. + + :param key: name of component + :return: value assigned to component + """ + return self._components[key] + + def add_component(self, key, value): + """Add a component key-value pair + + :param key: name of component + :param value: value + """ + self._components[key] = value + + def _from_json(self, file_name): + return cfg.from_json(file_name) + + def _validate_components(self): + """Make sure various components (i.e. paths) exists before attempting to build the graph.""" + return True + + def _create_nodes_prop_map(self, grp): + return NodePropertyMap() + + def _create_edges_prop_map(self, grp): + return EdgePropertyMap() + + def __avail_model_types(self, population): + model_types = set() + for grp in population.groups: + if self.model_type_col not in grp.all_columns: + self.io.log_exception('model_type is missing from nodes.') + + model_types.update(set(np.unique(grp.get_values(self.model_type_col)))) + return model_types + + def _preprocess_node_types(self, node_population): + # TODO: The following figures out the actually used node-type-ids. For mem and speed may be better to just + # process them all + node_type_ids = node_population.type_ids + # TODO: Verify all the node_type_ids are in the table + node_types_table = node_population.types_table + + # TODO: Convert model_type to a enum + morph_dir = self.get_component('morphologies_dir') + if morph_dir is not None and 'morphology' in node_types_table.columns: + for nt_id in node_type_ids: + node_type = node_types_table[nt_id] + if node_type['morphology'] is None: + continue + # TODO: Check the file exits + # TODO: See if absolute path is stored in csv + node_type['morphology'] = os.path.join(morph_dir, node_type['morphology']) + + if 'dynamics_params' in node_types_table.columns and 'model_type' in node_types_table.columns: + for nt_id in node_type_ids: + node_type = node_types_table[nt_id] + dynamics_params = node_type['dynamics_params'] + if isinstance(dynamics_params, dict): + continue + + model_type = node_type['model_type'] + if model_type == 'biophysical': + params_dir = self.get_component('biophysical_neuron_models_dir') + elif model_type == 'point_process': + params_dir = self.get_component('point_neuron_models_dir') + elif model_type == 'point_soma': + params_dir = self.get_component('point_neuron_models_dir') + else: + # Not sure what to do in this case, throw Exception? + params_dir = self.get_component('custom_neuron_models') + + params_path = os.path.join(params_dir, dynamics_params) + + # see if we can load the dynamics_params as a dictionary. Otherwise just save the file path and let the + # cell_model loader function handle the extension. + try: + params_val = json.load(open(params_path, 'r')) + node_type['dynamics_params'] = params_val + except Exception: + # TODO: Check dynamics_params before + self.io.log_exception('Could not find node dynamics_params file {}.'.format(params_path)) + + def _preprocess_edge_types(self, edge_pop): + edge_types_table = edge_pop.types_table + edge_type_ids = np.unique(edge_pop.type_ids) + + for et_id in edge_type_ids: + if 'dynamics_params' in edge_types_table.columns: + edge_type = edge_types_table[et_id] + dynamics_params = edge_type['dynamics_params'] + params_dir = self.get_component('synaptic_models_dir') + + params_path = os.path.join(params_dir, dynamics_params) + + # see if we can load the dynamics_params as a dictionary. Otherwise just save the file path and let the + # cell_model loader function handle the extension. + try: + params_val = json.load(open(params_path, 'r')) + edge_type['dynamics_params'] = params_val + except Exception: + # TODO: Check dynamics_params before + self.io.log_exception('Could not find edge dynamics_params file {}.'.format(params_path)) + + # Split target_sections + if 'target_sections' in edge_type: + trg_sec = edge_type['target_sections'] + if trg_sec is not None: + try: + edge_type['target_sections'] = ast.literal_eval(trg_sec) + except Exception as exc: + self.io.log_warning('Unable to split target_sections list {}'.format(trg_sec)) + edge_type['target_sections'] = None + + # Split target distances + if 'distance_range' in edge_type: + dist_range = edge_type['distance_range'] + if dist_range is not None: + try: + # TODO: Make the distance range has at most two values + edge_type['distance_range'] = json.loads(dist_range) + except Exception as e: + try: + edge_type['distance_range'] = [0.0, float(dist_range)] + except Exception as e: + self.io.log_warning('Unable to parse distance_range {}'.format(dist_range)) + edge_type['distance_range'] = None + + def external_edge_populations(self, src_pop, trg_pop): + return self._external_edges.get((src_pop, trg_pop), []) + + def add_nodes(self, sonata_file, populations=None): + """Add nodes from a network to the graph. + + :param sonata_file: A NodesFormat type object containing list of nodes. + :param populations: name/identifier of network. If none will attempt to retrieve from nodes object + """ + nodes = sonata_file.nodes + + selected_populations = nodes.population_names if populations is None else populations + for pop_name in selected_populations: + if pop_name not in nodes: + # when user wants to simulation only a few populations in the file + continue + + if pop_name in self.node_populations: + # Make sure their aren't any collisions + self.io.log_exception('There are multiple node populations with name {}.'.format(pop_name)) + + node_pop = nodes[pop_name] + self._preprocess_node_types(node_pop) + self._node_populations[pop_name] = node_pop + + # Segregate into virtual populations and non-virtual populations + model_types = self.__avail_model_types(node_pop) + if 'virtual' in model_types: + self._virtual_populations_map[pop_name] = node_pop + self._virtual_cells_nid[pop_name] = {} + model_types -= set(['virtual']) + if model_types: + # We'll allow a population to have virtual and non-virtual nodes but it is not ideal + self.io.log_warning('Node population {} contains both virtual and non-virtual nodes which can ' + + 'cause memory and build-time inefficency. Consider separating virtual nodes ' + + 'into their own population'.format(pop_name)) + + if model_types: + self._internal_populations_map[pop_name] = node_pop + + self._node_property_maps[pop_name] = {grp.group_id: self._create_nodes_prop_map(grp) + for grp in node_pop.groups} + + def build_nodes(self): + raise NotImplementedError + + def build_recurrent_edges(self): + raise NotImplementedError + + def add_edges(self, sonata_file, populations=None, source_pop=None, target_pop=None): + """ + + :param sonata_file: + :param populations: + :param source_pop: + :param target_pop: + :return: + """ + edges = sonata_file.edges + selected_populations = edges.population_names if populations is None else populations + + for pop_name in selected_populations: + if pop_name not in edges: + continue + + edge_pop = edges[pop_name] + self._preprocess_edge_types(edge_pop) + + # Check the source nodes exists + src_pop = source_pop if source_pop is not None else edge_pop.source_population + is_internal_src = src_pop in self._internal_populations_map.keys() + is_external_src = src_pop in self._virtual_populations_map.keys() + + trg_pop = target_pop if target_pop is not None else edge_pop.target_population + is_internal_trg = trg_pop in self._internal_populations_map.keys() + + if not is_internal_trg: + self.io.log_exception(('Node population {} does not exists (or consists of only virtual nodes). ' + + '{} edges cannot create connections.').format(trg_pop, pop_name)) + + if not (is_internal_src or is_external_src): + self.io.log_exception('Source node population {} not found. Please update {} edges'.format(src_pop, + pop_name)) + if is_internal_src: + if trg_pop not in self._recurrent_edges: + self._recurrent_edges[trg_pop] = [] + self._recurrent_edges[trg_pop].append(edge_pop) + + if is_external_src: + if trg_pop not in self._external_edges: + self._external_edges[(src_pop, trg_pop)] = [] + self._external_edges[(src_pop, trg_pop)].append(edge_pop) + + self._edge_property_maps[pop_name] = {grp.group_id: self._create_edges_prop_map(grp) + for grp in edge_pop.groups} + + @classmethod + def from_config(cls, conf, **properties): + """Generates a graph structure from a json config file or dictionary. + + :param conf: name of json config file, or a dictionary with config parameters + :param properties: optional properties. + :return: A graph object of type cls + """ + graph = cls(**properties) + if isinstance(conf, basestring): + config = graph._from_json(conf) + elif isinstance(conf, dict): + config = conf + else: + graph.io.log_exception('Could not convert {} (type "{}") to json.'.format(conf, type(conf))) + + run_dict = config['run'] + if 'spike_threshold' in run_dict: + # TODO: FIX, spike-thresholds should be set by simulation code, allow for diff. values based on node-group + graph.spike_threshold = run_dict['spike_threshold'] + if 'dL' in run_dict: + graph.dL = run_dict['dL'] + + if not config.with_networks: + graph.io.log_exception('Could not find any network files. Unable to build network.') + + # load components + for name, value in config.components.items(): + graph.add_component(name, value) + graph._validate_components() + + # load nodes + for node_dict in config.nodes: + nodes_net = sonata.File(data_files=node_dict['nodes_file'], data_type_files=node_dict['node_types_file']) + graph.add_nodes(nodes_net) + + # load edges + for edge_dict in config.edges: + target_network = edge_dict['target'] if 'target' in edge_dict else None + source_network = edge_dict['source'] if 'source' in edge_dict else None + edge_net = sonata.File(data_files=edge_dict['edges_file'], data_type_files=edge_dict['edge_types_file']) + graph.add_edges(edge_net, source_pop=target_network, target_pop=source_network) + + ''' + graph.io.log_info('Building cells.') + graph.build_nodes() + + graph.io.log_info('Building recurrent connections') + graph.build_recurrent_edges() + ''' + + return graph diff --git a/bmtk-vb/bmtk/simulator/utils/io.py b/bmtk-vb/bmtk/simulator/utils/io.py new file mode 100644 index 0000000..b6e5e5c --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/io.py @@ -0,0 +1,54 @@ +import os +import shutil +import logging + + +class IOUtils(object): + def __init__(self): + self.mpi_rank = 0 + self.mpi_size = 1 + + self._log_format = '%(asctime)s [%(levelname)s] %(message)s' + self._logger = logging.getLogger() + self.set_console_logging() + + @property + def logger(self): + return None + + def set_console_logging(self): + pass + + def barrier(self): + pass + + def quit(self): + exit(1) + + def setup_output_dir(self, config_dir, log_file, overwrite=True): + if self.mpi_rank == 0: + # Create output directory + if os.path.exists(config_dir): + if overwrite: + shutil.rmtree(config_dir) + else: + self.log_exception('ERROR: Directory already exists (remove or set to overwrite).') + os.makedirs(config_dir) + + # Create log file + if log_file is not None: + file_logger = logging.FileHandler(log_file) + file_logger.setFormatter(self._log_format) + self.logger.addHandler(file_logger) + self.log_info('Created log file') + + self.barrier() + + def log_info(self, message, all_ranks=False): + print(message) + + def log_warning(self, message, all_ranks=False): + print(message) + + def log_exception(self, message): + raise Exception(message) diff --git a/bmtk-vb/bmtk/simulator/utils/load_spikes.py b/bmtk-vb/bmtk/simulator/utils/load_spikes.py new file mode 100644 index 0000000..8c16caf --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/load_spikes.py @@ -0,0 +1,91 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import h5py +import numpy as np +import os +import datetime + + +def load_spikes_ascii(file_name): + ''' + Load ascii spike file + ''' + t = os.path.getmtime(file_name) + print(file_name, "modified on:", datetime.datetime.fromtimestamp(t)) + spk_ts,spk_gids = np.loadtxt(file_name, + dtype='float32,int', + unpack=True) + + spk_ts=spk_ts*1E-3 + + print('loaded spikes from ascii') + + return [spk_ts,spk_gids] + + +def load_spikes_h5(file_name): + ''' + Load ascii spike file + ''' + + t = os.path.getmtime(file_name) + print(file_name, "modified on:", datetime.datetime.fromtimestamp(t)) + + with h5py.File(file_name,'r') as h5: + + spk_ts=h5["time"][...]*1E-3 + spk_gids=h5["gid"][...] + + + print('loaded spikes from hdf5') + + return [spk_ts,spk_gids] + + +def load_spikes_nwb(file_name,trial_name): + + ''' + Load spikes from the nwb file + + Returns: + ------- + + spike_times: list + spike_gids: list + ''' + f5 = h5py.File(file_name, 'r') + + + spike_trains_handle = f5['processing/%s/spike_train' % trial_name] # nwb.SpikeTrain.get_processing(f5,'trial_0') + + spike_times = [] + spike_gids = [] + + for gid in spike_trains_handle.keys(): + + times_gid = spike_trains_handle['%d/data' %int(gid)][:] + spike_times.extend(times_gid) + spike_gids.extend([int(gid)]*len(times_gid)) + + return [np.array(spike_times)*1E-3,np.array(spike_gids)] + diff --git a/bmtk-vb/bmtk/simulator/utils/nwb.py b/bmtk-vb/bmtk/simulator/utils/nwb.py new file mode 100644 index 0000000..4d18d16 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/nwb.py @@ -0,0 +1,530 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import copy +import numpy as np +import os +import h5py +import time +import uuid +import tempfile +from bmtk.analyzer.visualization.widgets import PlotWidget, MovieWidget + +__version__ = '0.1.0' + +allowed_dimensions = {'firing_rate': ('hertz',), + 'time': ('second', 'millisecond'), + 'brightness': ('intensity',), + 'distance': ('pixel',), + 'index': ('gid',), + 'intensity': ('bit',None), + 'voltage': ('volt',), + 'current': ('ampere',), + None: (None,), + 'dev': ('dev',)} + +allowed_groups = {'firing_rate': ('firing_rate',), + 'spike_train': ('index', 'time'), + 'grayscale_movie': ('intensity',), + 'time_series': ('voltage', 'current'), + 'dev': ('dev',)} + +top_level_data = ['file_create_date', + 'stimulus', + 'acquisition', + 'analysis', + 'processing', + 'epochs', + 'general', + 'session_description', + 'nwb_version', + 'identifier'] + + +def open_file(file_name): + return h5py.File(file_name) + + +class Scale(object): + def __init__(self, scale_range, dimension, unit): + assert dimension in allowed_dimensions + assert unit in allowed_dimensions[dimension] + + self.scale_range = scale_range + self.dimension = dimension + self.unit = unit + self._hdf5_location = None + + def __eq__(self, other): + d = self.dimension == other.dimension + u = self.unit == other.unit + s = np.allclose(self.scale_range, other.scale_range) + return d and u and s + + @ property + def data(self): + return self.scale_range + + +class DtScale(object): + def __init__(self, dt, dimension, unit): + assert dimension in allowed_dimensions + assert unit in allowed_dimensions[dimension] + + self.dt = dt + self.dimension = dimension + self.unit = unit + self._hdf5_location = None + + def __eq__(self, other): + d = self.dimension == other.dimension + u = self.unit == other.unit + s = np.allclose(self.scale_range, other.scale_range) + return d and u and s + + @ property + def data(self): + return self.dt + + +class NullScale(object): + + def __init__(self): + self._hdf5_location = None + self.data = None + self.dimension = None + self.unit = None + + +class Data(object): + def __init__(self, data, dimension, unit, scales, metadata): + assert dimension in allowed_dimensions + assert unit in allowed_dimensions[dimension] + if isinstance(scales, (Scale, DtScale)): + assert len(data.shape) == 1 + scales = (scales,) + + for key in metadata.iterkeys(): + assert isinstance(key, (str, unicode)) + for ii, scale in enumerate(scales): + if isinstance(scale, Scale): + assert len(scale.scale_range) == data.shape[ii] + elif isinstance(scale, DtScale): + assert isinstance(scale.dt, (float, np.float)) and scale.dt > 0 + else: + raise Exception + + if len(scales) == 0: + scales = [NullScale()] + + metadata = copy.copy(metadata) + self.data = data + self.scales = scales + self.dimension = dimension + self.unit = unit + self.metadata = metadata + self._hdf5_location = None + + def __eq__(self, other): + da = np.allclose(self.data, other.data) + d = self.dimension == other.dimension + u = self.unit == other.unit + s = [s1 == s2 for s1, s2 in zip(self.scales, other.scales)].count(True) == len(self.scales) + if len(self.metadata) != len(other.metadata): + m = False + else: + try: + sum = 0 + for key in self.metadata.keys(): + sum += other.metadata[key] == self.metadata[key] + assert sum == len(self.metadata) + m = True + except: + m = False + return da and d and u and s and m + + @staticmethod + def _get_from_group(object_class, parent_group, group_name, ii=0): + + data_group = parent_group['%s/%s' % (group_name, ii)] + data, scales, dimension, unit, metadata = _get_data(data_group) + + assert dimension in allowed_groups[object_class.group] + + if unit == "None": + unit = None + scale_list = [] + for scale in scales: + if scale.attrs['type'] == 'Scale': + curr_scale = Scale(scale, scale.attrs['dimension'], scale.attrs['unit']) + elif scale.attrs['type'] == 'DtScale': + curr_scale = DtScale(float(scale.value), scale.attrs['dimension'], scale.attrs['unit']) + elif scale.attrs['type'] == 'NullScale': + curr_scale = None + else: + raise Exception + if curr_scale is not None: + scale_list.append(curr_scale) + + if len(scale_list) == 1: + scale_list = scale_list[0] + + return object_class(data, dimension=dimension, unit=unit, scale=scale_list, metadata=metadata) + + def add_to_stimulus(self, f, compression='gzip', compression_opts=4): + self._add_to_group(f, 'stimulus', self.__class__.group, compression=compression, + compression_opts=compression_opts) + + @classmethod + def get_stimulus(cls, f, ii=None): + if ii is None: + return_data = [cls.get_stimulus(f, ii) for ii in range(len(f['stimulus/%s' % cls.group]))] + if len(return_data) == 1: + return_data = return_data[0] + return return_data + else: + return Data._get_from_group(cls, f['stimulus'], cls.group, ii=ii) + + def add_to_acquisition(self, f, compression='gzip', compression_opts=4): + self._add_to_group(f, 'acquisition', self.__class__.group, compression=compression, + compression_opts=compression_opts) + + @classmethod + def get_acquisition(cls, f, ii=None): + if ii is None: + return_data = [cls.get_acquisition(f, ii) for ii in range(len(f['acquisition/%s' % cls.group]))] + if len(return_data) == 1: + return_data = return_data[0] + return return_data + + else: + return Data._get_from_group(cls, f['acquisition'], cls.group, ii=ii) + + def add_to_processing(self, f, processing_submodule_name): + if processing_submodule_name not in f['processing']: + f['processing'].create_group(processing_submodule_name) + return self._add_to_group(f, 'processing/%s' % processing_submodule_name, self.__class__.group) + + @classmethod + def get_processing(cls, f, subgroup_name, ii=None): + if ii is None: + return_data = {} + for ii in range(len(f['processing/%s/%s' % (subgroup_name, cls.group)])): + return_data[ii] = cls.get_processing(f, subgroup_name, ii) + return return_data + + else: + return Data._get_from_group(cls, f['processing/%s' % subgroup_name], cls.group, ii=ii) + + def add_to_analysis(self, f, analysis_submodule_name): + if analysis_submodule_name not in f['analysis']: + f['analysis'].create_group(analysis_submodule_name) + return self._add_to_group(f, 'analysis/%s' % analysis_submodule_name, self.__class__.group) + + @classmethod + def get_analysis(cls, f, subgroup_name, ii=None): + if ii is None: + return [cls.get_analysis(f, ii, subgroup_name) + for ii in range(len(f['analysis/%s/%s' % (subgroup_name, cls.group)]))] + else: + return Data._get_from_group(cls, f['analysis/%s' % subgroup_name], cls.group, ii=ii) + + def _add_to_group(self, f, parent_name, group_name, compression='gzip', compression_opts=4): + assert group_name in allowed_groups + assert self.dimension in allowed_groups[group_name] + try: + parent_group = f[parent_name] + except ValueError: + try: + file_name = f.filename + raise Exception('Parent group:%s not found in file %s' % parent_name, file_name) + except ValueError: + raise Exception('File not valid: %s' % f) + + if self.__class__.group in parent_group: + subgroup = parent_group[self.__class__.group] + int_group_name = str(len(subgroup)) + else: + subgroup = parent_group.create_group(self.__class__.group) + int_group_name = '0' + + # Create external link: + if isinstance(self.data, h5py.Dataset): + if subgroup.file == self.data.file: + raise NotImplementedError + else: + return _set_data_external_link(subgroup, int_group_name, self.data.parent) + else: + dataset_group = subgroup.create_group(int_group_name) + + # All this to allow do shared scale management: + scale_group = None + scale_list = [] + for ii, scale in enumerate(self.scales): + if isinstance(scale, (Scale, DtScale, NullScale)): + if scale._hdf5_location is None: + if scale_group is None: + scale_group = dataset_group.create_group('scale') + curr_scale = _set_scale(scale_group, 'dimension_%s' % ii, scale.data, scale.dimension, + scale.unit, scale.__class__.__name__) + scale._hdf5_location = curr_scale + else: + curr_scale = _set_scale(scale_group, 'dimension_%s' % ii, scale.data, scale.dimension, + scale.unit, scale.__class__.__name__) + scale._hdf5_location = curr_scale + else: + curr_scale = scale._hdf5_location + elif isinstance(scale, h5py.Dataset): + curr_scale = scale + else: + raise Exception + + scale_list.append(curr_scale) + + _set_data(subgroup, dataset_group.name, self.data, scale_list, self.dimension, self.unit, + metadata=self.metadata, compression=compression, compression_opts=compression_opts) + + +class FiringRate(Data): + group = 'firing_rate' + + def __init__(self, data, **kwargs): + dimension = 'firing_rate' + unit = 'hertz' + scale = kwargs.get('scale') + metadata = kwargs.get('metadata', {}) + assert isinstance(scale, (Scale, DtScale)) + super(FiringRate, self).__init__(data, dimension, unit, scale, metadata) + + def get_widget(self, **kwargs): + rate_data = self.data[:] + t_range = self.scales[0].data[:] + return PlotWidget(t_range, rate_data, metadata=self.metadata, **kwargs) + + +class Dev(Data): + group = 'dev' + + def __init__(self, data, **kwargs): + dimension = kwargs.get('dimension') + unit = kwargs.get('unit') + scale = kwargs.get('scale') + metadata = kwargs.get('metadata', {}) + + super(Dev, self).__init__(data, dimension, unit, scale, metadata) + + +class TimeSeries(Data): + group = 'time_series' + + def __init__(self, data, **kwargs): + dimension = kwargs.get('dimension') + unit = kwargs.get('unit') + scale = kwargs.get('scale') + metadata = kwargs.get('metadata', {}) + + assert isinstance(scale, (Scale, DtScale)) + assert scale.dimension == 'time' + super(TimeSeries, self).__init__(data, dimension, unit, scale, metadata) + + +class SpikeTrain(Data): + group = 'spike_train' + + def __init__(self, data, **kwargs): + scales = kwargs.get('scale',[]) + unit = kwargs.get('unit', 'gid') + metadata = kwargs.get('metadata',{}) + + if isinstance(scales, Scale): + super(SpikeTrain, self).__init__(data, 'index', unit, scales, metadata) + elif len(scales) == 0: + assert unit in allowed_dimensions['time'] + scales = [] + super(SpikeTrain, self).__init__(data, 'time', unit, scales, metadata) + else: + assert len(scales) == 1 and isinstance(scales[0], Scale) + super(SpikeTrain, self).__init__(data, 'index', unit, scales, metadata) + + +class GrayScaleMovie(Data): + group = 'grayscale_movie' + + def __init__(self, data, **kwargs): + dimension = 'intensity' + unit = kwargs.get('unit', None) + scale = kwargs.get('scale') + metadata = kwargs.get('metadata', {}) + + super(GrayScaleMovie, self).__init__(data, dimension, unit, scale, metadata) + + def get_widget(self, ax=None): + data = self.data[:] + t_range = self.scales[0].data[:] + return MovieWidget(t_range=t_range, data=data, ax=ax, metadata=self.metadata) + + +def get_temp_file_name(): + f = tempfile.NamedTemporaryFile(delete=False) + temp_file_name = f.name + f.close() + os.remove(f.name) + return temp_file_name + + +def create_blank_file(save_file_name=None, force=False, session_description='', close=False): + + if save_file_name is None: + save_file_name = get_temp_file_name() + + if not force: + f = h5py.File(save_file_name, 'w-') + else: + if os.path.exists(save_file_name): + os.remove(save_file_name) + f = h5py.File(save_file_name, 'w') + + f.create_group('acquisition') + f.create_group('analysis') + f.create_group('epochs') + f.create_group('general') + f.create_group('processing') + f.create_group('stimulus') + + f.create_dataset("file_create_date", data=np.string_(time.ctime())) + f.create_dataset("session_description", data=session_description) + f.create_dataset("nwb_version", data='iSee_%s' % __version__) + f.create_dataset("identifier", data=str(uuid.uuid4())) + + if close: + f.close() + else: + return f + + +def assert_subgroup_exists(child_name, parent): + try: + assert child_name in parent + except: + raise RuntimeError('Group: %s has no subgroup %s' % (parent.name, child_name)) + + +def _set_data_external_link(parent_group, dataset_name, data): + parent_group[dataset_name] = h5py.ExternalLink(data.file.filename, data.name) + + +def _set_scale_external_link(parent_group, name, scale): + print(parent_group, name, scale) + print(scale.file.filename, scale.name) + parent_group[name] = h5py.ExternalLink(scale.file.filename, scale.name) + return parent_group[name] + + +def _set_data(parent_group, dataset_name, data, scales, dimension, unit, force=False, metadata={}, compression='gzip', + compression_opts=4): + # Check inputs: + if isinstance(scales, h5py.Dataset): + scales = (scales,) + else: + assert isinstance(scales, (list, tuple)) + + assert data.ndim == len(scales) + assert dimension in allowed_dimensions + assert unit in allowed_dimensions[dimension] + for ii, scale in enumerate(scales): + assert len(scale.shape) in (0, 1) + check_dimension = str(scale.attrs['dimension']) + if check_dimension == 'None': + check_dimension = None + check_unit = scale.attrs['unit'] + if check_unit == 'None': + check_unit = None + assert check_dimension in allowed_dimensions + assert check_unit in allowed_dimensions[check_dimension] + if len(scale.shape) == 1: + assert len(scale) == data.shape[ii] or len(scale) == 0 + + if dataset_name not in parent_group: + dataset_group = parent_group.create_group(dataset_name) + else: + dataset_group = parent_group[dataset_name] + + for key, val in metadata.iteritems(): + assert key not in dataset_group.attrs + dataset_group.attrs[key] = val + + if 'data' in dataset_group: + if not force: + raise IOError('Field "stimulus" of %s is not empty; override with force=True' % parent_group.name) + else: + del dataset_group['data'] + + dataset = dataset_group.create_dataset(name='data', data=data, compression=compression, + compression_opts=compression_opts) + + for ii, scale in enumerate(scales): + dataset.dims[ii].label = scale.attrs['dimension'] + dataset.dims[ii].attach_scale(scale) + + dataset.attrs.create('dimension', str(dimension)) + dataset.attrs.create('unit', str(unit)) + + return dataset + + +def _set_scale(parent_group, name, scale, dimension, unit, scale_class_name): + assert dimension in allowed_dimensions + assert unit in allowed_dimensions[dimension] + + if scale is None: + scale = parent_group.create_dataset(name=name, shape=(0,)) + else: + scale = np.array(scale) + assert scale.ndim in (0, 1) + scale = parent_group.create_dataset(name=name, data=scale) + scale.attrs['dimension'] = str(dimension) + scale.attrs['unit'] = str(unit) + scale.attrs['type'] = scale_class_name + + return scale + + +def _get_data(dataset_group): + data = dataset_group['data'] + dimension = dataset_group['data'].attrs['dimension'] + unit = dataset_group['data'].attrs['unit'] + scales = tuple([dim[0] for dim in dataset_group['data'].dims]) + metadata = dict(dataset_group.attrs) + + return data, scales, dimension, unit, metadata + + +def get_stimulus(f): + category = 'stimulus' + for parent_group in f[category]: + for data_group in f[category][parent_group]: + print(f[category][parent_group][data_group]) + + +def add_external_links(parent_group, external_file_name, external_group_name_list=top_level_data): + for subgroup in external_group_name_list: + parent_group[subgroup] = h5py.ExternalLink(external_file_name, subgroup) diff --git a/bmtk-vb/bmtk/simulator/utils/property_maps.py b/bmtk-vb/bmtk/simulator/utils/property_maps.py new file mode 100644 index 0000000..9a22515 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/property_maps.py @@ -0,0 +1,7 @@ +class NodePropertyMap(object): + pass + + +class EdgePropertyMap(object): + pass + diff --git a/bmtk-vb/bmtk/simulator/utils/scripts/convert_filters.py b/bmtk-vb/bmtk/simulator/utils/scripts/convert_filters.py new file mode 100644 index 0000000..298c101 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/scripts/convert_filters.py @@ -0,0 +1,71 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import numpy as np +from bmtk.simulator.utils import nwb +import pickle +import re + +pickle_regex = re.compile('.*\.pkl') + +def convert_filters(src_dir, tgt_dir): + + for file_name in os.listdir(src_dir): + if not pickle_regex.match(file_name) is None: + + print 'Converting: %s' % file_name + + full_path_to_src_file = os.path.join(src_dir, file_name) + full_path_to_tgt_file = os.path.join(tgt_dir, file_name).replace('.pkl', '.nwb') + + try: + f = nwb.NWB(file_name=full_path_to_tgt_file, + identifier='iSee example filter dataset', + description='Convering an example inhomogenous Poisson rate collection from a filter to drive simulations') + + # Load data from file: + data = pickle.load(open(full_path_to_src_file, 'r')) + timestamps = data['t'] + + # Load first cell into file: + ts0 = f.create_timeseries('TimeSeries', "Cell_0", "acquisition") + ts0.set_data(data['cells'][0], unit='Hz', resolution=float('nan'), conversion=1.) + ts0.set_time_by_rate(0.,1000.) + ts0.set_value('num_samples', len(timestamps)) + ts0.finalize() + + # Load remaining cells into file, linking timestamps: + for ii in np.arange(1,len(data['cells'])): + ts = f.create_timeseries('TimeSeries', "Cell_%s" % ii, "acquisition") + ts.set_data(data['cells'][ii], unit='Hz', resolution=float('nan'), conversion=1.) + ts.set_time_by_rate(0.,1000.) + ts.set_value('num_samples', len(timestamps)) + ts.finalize() + + # Close out: + f.close() + + except: + print ' Conversion failed: %s' % file_name + + diff --git a/bmtk-vb/bmtk/simulator/utils/sim_validator.py b/bmtk-vb/bmtk/simulator/utils/sim_validator.py new file mode 100644 index 0000000..447dda1 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/sim_validator.py @@ -0,0 +1,126 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import os +import json +from jsonschema import Draft4Validator +from jsonschema.exceptions import ValidationError +import pandas as pd + + +class SimConfigValidator(Draft4Validator): + """ + A JSON Schema validator class that will store a schema (passed into the constructor) and validate a json file. + It has all the functionality of the JSONSchema format, plus includes special types and parameters like making + sure a value is a file or directory type, checking csv files, etc. + + To Use: + validator = SimConfigValidator(json_schema.json) + validator.validate(file.json) + """ + + def __init__(self, schema, types=(), resolver=None, format_checker=None, file_formats=()): + super(SimConfigValidator, self).__init__(schema, types, resolver, format_checker) + + # custom parameter + self.VALIDATORS["exists"] = self._check_path + + self._file_formats = {} # the "file_format" property the validity of a (non-json) file. + for (name, schema) in file_formats: + self._file_formats[name] = self._parse_file_formats(schema) + self.VALIDATORS["file_format"] = self._validate_file + + def is_type(self, instance, dtype): + # override type since checking for file and directory type is potentially more complicated. + if dtype == "directory": + return self._is_directory_type(instance) + + elif dtype == "file": + return self._is_file_type(instance) + + else: + return super(SimConfigValidator, self).is_type(instance, dtype) + + def _is_directory_type(self, instance): + """Check if instance value is a valid directory file path name + + :param instance: string that represents a directory path + :return: True if instance is a valid dir path (even if it doesn't exists). + """ + # Always return true for now, rely on the "exists" property (_check_path) to actual determine if file exists. + # TODO: check that instance string is a valid path string, even if path doesn't yet exists. + return True + + def _is_file_type(self, instance): + """Check if instance value is a valid file path. + + :param instance: string of file path + :return: True if instance is a valid file path (but doesn't necessary exists), false otherwise. + """ + # Same issue as with _is_directory_type + return True + + def _parse_file_formats(self, schema_file): + # Open the schema file and based on "file_type" property create a Format validator + schema = json.load(open(schema_file, 'r')) + if schema['file_type'] == 'csv': + return self._CSVFormat(schema) + else: + return Exception("No format found") + + @staticmethod + def _check_path(validator, schema_bool, path, schema): + """Makes sure a file/directory exists or doesn't based on the "exists" property in the schema + + :param validator: + :param schema_bool: True means file must exists, False means file should not exists + :param path: path of the file + :param schema: + :return: True if schema is satisfied. + """ + assert(schema['type'] == 'directory' or schema['type'] == 'file') + path_exists = os.path.exists(path) + if path_exists != schema_bool: + raise ValidationError("{} {} exists.".format(path, "already" if path_exists else "does not")) + + def _validate_file(self, validator, file_format, file_path, schema): + file_validator = self._file_formats.get(file_format, None) + if file_validator is None: + raise ValidationError("Could not find file validator {}".format(file_format)) + + if not file_validator.check(file_path): + raise ValidationError("File {} could not be validated against {}.".format(file_path, file_format)) + + # A series of validators for indivdiual types of files. All of them should have a check(file) function that returns + # true only when it is formated correctly. + class _CSVFormat(object): + def __init__(self, schema): + self._properties = schema['file_properties'] + self._required_columns = [header for header, props in schema['columns'].items() if props['required']] + + def check(self, file_name): + csv_headers = set(pd.read_csv(file_name, nrows=0, **self._properties).columns) + for col in self._required_columns: + if col not in csv_headers: + return False + + return True diff --git a/bmtk-vb/bmtk/simulator/utils/sim_validator.pyc b/bmtk-vb/bmtk/simulator/utils/sim_validator.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a844c845fdc711c80d80147db8cd58184390df2 GIT binary patch literal 5149 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 Cq-uQt literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/simulation_reports.py b/bmtk-vb/bmtk/simulator/utils/simulation_reports.py new file mode 100644 index 0000000..a7bffd9 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/simulation_reports.py @@ -0,0 +1,284 @@ +import os + + +class SimReport(object): + default_dir = '.' + registry = {} # Used by factory to keep track of subclasses + + def __init__(self, name, module, params): + self.report_name = name + self.module = module + self.params = params + + # Not part of standard, just want a quick way to turn off modules + if 'enabled' in params: + self.enabled = params['enabled'] + del params['enabled'] + else: + self.enabled = True + + # Set default parameter values (when not explicity stated). Should occur on a module-by-module basis + self._set_defaults() + + @property + def node_set(self): + return self.params.get('cells', 'all') + + def _set_defaults(self): + for var_name, default_val in self._get_defaults(): + if var_name not in self.params: + self.params[var_name] = default_val + + def _get_defaults(self): + """Should be overwritten by subclass with list of (var_name, default_val) tuples.""" + return [] + + @staticmethod + def avail_modules(): + # Return a string (or list of strings) to identify module name for each subclass + raise NotImplementedError + + @classmethod + def build(cls, report_name, params): + """Factory method to get the module subclass, using the params (particularlly the 'module' value, which is + required). If there is no registered subclass a generic SimReport object will be returned + + :param report_name: name of report + :param params: parameters of report + :return: A SimReport (or subclass) object with report parameters parsed out. + """ + params = params.copy() + if 'module' not in params: + raise Exception('report {} does not specify the module.'.format(report_name)) + + module_name = params['module'] + del params['module'] + module_cls = SimReport.registry.get(module_name, SimReport) + return module_cls(report_name, module_name, params) + + @classmethod + def register_module(cls, subclass): + # For factory, register subclass based on the module name(s) + assert(issubclass(subclass, cls)) + mod_registry = cls.registry + mod_list = subclass.avail_modules() + modules = mod_list if isinstance(mod_list, list) else [mod_list] + for mod_name in modules: + if mod_name in mod_registry: + raise Exception('Multiple modules named {}'.format(mod_name)) + mod_registry[mod_name] = subclass + + return subclass + + +@SimReport.register_module +class MembraneReport(SimReport, object): + def __init__(self, report_name, module, params): + super(MembraneReport, self).__init__(report_name, module, params) + # Want variable_name option to allow for singular of list of params + variables = params['variable_name'] + if isinstance(variables, list): + self.params['variable_name'] = variables + else: + self.params['variable_name'] = [variables] + self.variables = self.params['variable_name'] + + self.params['buffer_data'] = self.params.pop('buffer') + + if self.params['transform'] and not isinstance(self.params['transform'], dict): + self.params['transform'] = {var_name: self.params['transform'] for var_name in self.variables} + + def _get_defaults(self): + # directory for saving temporary files created during simulation + tmp_dir = self.default_dir + + # Find the report file name. Either look for "file_name" parameter, or else it is .h5 + if 'file_name' in self.params: + file_name = self.params['file_name'] + elif self.report_name.endswith('.h5') or self.report_name.endswith('.hdf') \ + or self.report_name.endswith('.hdf5'): + file_name = self.report_name # Check for case report.h5.h5 + else: + file_name = '{}.h5'.format(self.report_name) + + return [('cells', 'biophysical'), ('sections', 'all'), ('tmp_dir', tmp_dir), ('file_name', file_name), + ('buffer', True), ('transform', {})] + + def add_variables(self, var_name, transform): + self.params['variable_name'].extend(var_name) + self.params['transform'].update(transform) + + def can_combine(self, other): + def param_eq(key): + return self.params.get(key, None) == other.params.get(key, None) + + return param_eq('cells') and param_eq('sections') and param_eq('file_name') and param_eq('buffer') + + @staticmethod + def avail_modules(): + return 'membrane_report' + + @classmethod + def build(cls, name, params): + report = cls(name) + report.cells = params.get('cells', 'biophysical') + report.sections = params.get('sections', 'all') + + if 'file_name' in params: + report.file_name = params['file_name'] + report.tmp_dir = os.path.dirname(os.path.realpath(report.file_name)) + else: + report.file_name = os.path.join(cls.default_dir, 'cell_vars.h5') + report.tmp_dir = cls.default_dir + + variables = params['variable_name'] + if isinstance(variables, list): + report.variables = variables + else: + report.variables = [variables] + + return report + + +@SimReport.register_module +class SpikesReport(SimReport): + def __init__(self, report_name, module, params): + super(SpikesReport, self).__init__(report_name, module, params) + + @classmethod + def build(cls, name, params): + return None + + @staticmethod + def avail_modules(): + return 'spikes_report' + + @classmethod + def from_output_dict(cls, output_dict): + params = { + 'spikes_file': output_dict.get('spikes_file', None), + 'spikes_file_csv': output_dict.get('spikes_file_csv', None), + 'spikes_file_nwb': output_dict.get('spikes_file_nwb', None), + 'spikes_sort_order': output_dict.get('spikes_sort_order', None), + 'tmp_dir': output_dict.get('output_dir', cls.default_dir) + } + if not (params['spikes_file'] or params['spikes_file_csv'] or params['spikes_file_nwb']): + # User hasn't specified any spikes file + params['enabled'] = False + + return cls('spikes_report', 'spikes_report', params) + + +@SimReport.register_module +class SEClampReport(SimReport): + def __init__(self, report_name, module, params): + super(SEClampReport, self).__init__(report_name, module, params) + + @staticmethod + def avail_modules(): + return 'SEClamp' + + +@SimReport.register_module +class ECPReport(SimReport): + def __init__(self, report_name, module, params): + super(ECPReport, self).__init__(report_name, module, params) + self.tmp_dir = self.default_dir + self.positions_file = None + self.file_name = None + + @staticmethod + def avail_modules(): + return 'extracellular' + + def _get_defaults(self): + if 'file_name' in self.params: + file_name = self.params['file_name'] + elif self.report_name.endswith('.h5') or self.report_name.endswith('.hdf') \ + or self.report_name.endswith('.hdf5'): + file_name = self.report_name # Check for case report.h5.h5 + else: + file_name = '{}.h5'.format(self.report_name) + + return [('tmp_dir', self.default_dir), ('file_name', file_name), + ('contributions_dir', os.path.join(self.default_dir, 'ecp_contributions'))] + + @classmethod + def build(cls, name, params): + report = cls(name) + + if 'file_name' in params: + report.file_name = params['file_name'] + report.tmp_dir = os.path.dirname(os.path.realpath(report.file_name)) + else: + report.file_name = os.path.join(cls.default_dir, 'ecp.h5') + report.tmp_dir = cls.default_dir + + report.contributions_dir = params.get('contributions_dir', cls.default_dir) + report.positions_file = params['electrode_positions'] + return report + + +@SimReport.register_module +class SaveSynapses(SimReport): + def __init__(self, report_name, module, params): + super(SaveSynapses, self).__init__(report_name, module, params) + + @staticmethod + def avail_modules(): + return 'SaveSynapses' + + +@SimReport.register_module +class MultimeterReport(MembraneReport): + + @staticmethod + def avail_modules(): + return ['multimeter', 'multimeter_report'] + + +@SimReport.register_module +class SectionReport(MembraneReport): + + @staticmethod + def avail_modules(): + return ['section_report'] + + +def from_config(cfg): + SimReport.default_dir = cfg.output_dir + + reports_list = [] + membrane_reports = [] + has_spikes_report = False + for report_name, report_params in cfg.reports.items(): + # Get the Report class from the module_name parameter + if not report_params.get('enabled', True): + # not a part of the standard but will help skip modules + continue + + report = SimReport.build(report_name, report_params) + + if isinstance(report, MembraneReport): + # When possible for membrane reports combine multiple reports into one module if all the parameters + # except for the variable name differs. + for existing_report in membrane_reports: + if existing_report.can_combine(report): + existing_report.add_variables(report.variables, report.params['transform']) + break + else: + reports_list.append(report) + membrane_reports.append(report) + + else: + reports_list.append(report) + + if not has_spikes_report: + report = SpikesReport.from_output_dict(cfg.output) + if report is None: + # TODO: Log exception or possibly warning + pass + else: + reports_list.append(report) + + return reports_list diff --git a/bmtk-vb/bmtk/simulator/utils/simulation_reports.pyc b/bmtk-vb/bmtk/simulator/utils/simulation_reports.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0704f779a7509922b5711579b10ada277d28695a GIT binary patch literal 11855 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 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/stimulus/LocallySparseNoise.py b/bmtk-vb/bmtk/simulator/utils/stimulus/LocallySparseNoise.py new file mode 100644 index 0000000..ee43e9f --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/stimulus/LocallySparseNoise.py @@ -0,0 +1,137 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +from scipy.misc import imresize +import os +import pandas as pd + +stimulus_folder = os.path.dirname(os.path.abspath(__file__)) +bob_stimlus = os.path.join(stimulus_folder,'lsn.npy') + +class LocallySparseNoise (object): + + def __init__(self,stim_template=None, stim_table=None): + + if stim_template is None or stim_table is None: + raise Exception("stim_template or stim_table not provided. Please provide them or call the class methods .with_new_stimulus or .with_bob_stimulus.") + else: + self.stim_template = stim_template + self.stim_table = stim_table + + T,y,x = stim_template.shape + + self.T = T + self.y = y + self.x = x + + + def get_image_input(self, new_size=None, add_channels=False): + + if new_size is not None: + y,x = new_size + data_new_size = np.empty((self.T,y,x),dtype=np.float32) + + for t in range(self.stim_template.shape[0]): + data_new_size[t] = imresize(self.stim_template[t].astype(np.float32),new_size,interp='nearest') + + if add_channels: + return data_new_size[:,:,:,np.newaxis] + else: + return data_new_size + + @staticmethod + def exclude(av,y_x,exclusion=0): + y, x = y_x + X,Y = np.meshgrid(np.arange(av.shape[1]), np.arange(av.shape[0])) + + mask = ((X-x)**2 + (Y-y)**2) <= exclusion**2 + av[mask] = False + + @classmethod + def create_sparse_noise_matrix(cls,Y=16,X=28,exclusion=5,T=9000, buffer_x=6, buffer_y=6): + + Xp = X+2*buffer_x + Yp = Y+2*buffer_y + + # 127 is mean luminance value + sn = 127*np.ones([T,Yp,Xp],dtype=np.uint8) + + for t in range(T): + available = np.ones([Yp,Xp]).astype(np.bool) + + while np.any(available): + y_available, x_available = np.where(available) + + pairs = zip(y_available,x_available) + pair_index = np.random.choice(range(len(pairs))) + y,x = pairs[pair_index] + + p = np.random.random() + if p < 0.5: + sn[t,y,x] = 255 + else: + sn[t,y,x] = 0 + + cls.exclude(available,(y,x),exclusion=exclusion) + + return sn[:,buffer_y:(Y+buffer_y), buffer_x:(X+buffer_x)] + + def save_to_hdf(self): + + pass + + @staticmethod + def generate_stim_table(T,start_time=0,trial_length=250): + '''frame_length is in milliseconds''' + + start_time_array = trial_length*np.arange(T) + start_time + column_list = [np.arange(T),start_time_array, start_time_array+trial_length-1] # -1 is because the tables in BOb use inclusive intervals, so we'll stick to that convention + cols = np.vstack(column_list).T + stim_table = pd.DataFrame(cols,columns=['frame','start','end']) + + return stim_table + + + @classmethod + def with_new_stimulus(cls,Y=16,X=28,exclusion=5,T=9000, buffer_x=6, buffer_y=6): + + stim_template = cls.create_sparse_noise_matrix(Y=Y,X=X,exclusion=exclusion,T=T, buffer_x=buffer_x, buffer_y=buffer_y) + T,y,x = stim_template.shape + + stim_table = cls.generate_stim_table(T) + + new_locally_sparse_noise = cls(stim_template=stim_template, stim_table=stim_table) + + return new_locally_sparse_noise + + @classmethod + def with_brain_observatory_stimulus(cls): + + stim_template = np.load(bob_stimlus) + T,y,x = stim_template.shape + + stim_table = cls.generate_stim_table(T) + + new_locally_sparse_noise = cls(stim_template=stim_template, stim_table=stim_table) + + return new_locally_sparse_noise diff --git a/bmtk-vb/bmtk/simulator/utils/stimulus/NaturalScenes.py b/bmtk-vb/bmtk/simulator/utils/stimulus/NaturalScenes.py new file mode 100644 index 0000000..b04056b --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/stimulus/NaturalScenes.py @@ -0,0 +1,337 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import os +from PIL import Image +import pandas as pd + +class NaturalScenes (object): + def __init__(self, new_size=(64,112), mode='L', dtype=np.float32, start_time=0, trial_length=250, add_channels=False): + + self.new_size = new_size + self.mode = mode + self.dtype = dtype + self.add_channels = add_channels + + + + def random_sample(self, n): + sample_indices = np.random.randint(0, self.num_images, n) + return self.stim_template[sample_indices] + + # also a method random_sample_with_labels ? + def random_sample_with_labels(self, n): + pass + + def get_image_input(self,**kwargs): + return self.stim_template + + def add_gray_screen(self): + + gray_screen = np.ones(self.new_size,dtype=self.dtype)*127 # using 127 as "gray" value + if self.add_channels: + gray_screen = gray_screen[:,:,np.newaxis] + self.stim_template = np.vstack([self.stim_template, gray_screen[np.newaxis,:,:]]) + + start = int(self.stim_table.tail(1)['end']) + 1 + end = start+self.trial_length-1 #make trial_length an argument of this function? + frame = int(self.stim_table.tail(1)['frame']) + 1 + + self.stim_table = self.stim_table.append(pd.DataFrame([[frame,start,end]],columns=['frame','start','end']),ignore_index=True) + + self.label_dataframe = self.label_dataframe.append(pd.DataFrame([['gray_screen']],columns=['image_name']),ignore_index=True) + self.num_images = self.num_images + 1 + + @classmethod + def with_brain_observatory_stimulus(cls, new_size=(64,112), mode='L', dtype=np.float32, start_time=0, trial_length=250, add_channels=False): + + from sys import platform + + if platform=='linux2': + image_dir = '/data/mat/iSee_temp_shared/CAM_Images.icns' + elif platform=='darwin': + + image_dir = '/Users/michaelbu/Data/Images/CAM_Images.icns' + if not os.path.exists(image_dir): + print("Detected platform: OS X. I'm assuming you've mounted \\\\aibsdata\\mat at /Volumes/mat/") + image_dir = '/Volumes/mat/iSee_temp_shared/CAM_Images.icns' + + + elif platform=='win32': + image_dir = r'\\aibsdata\mat\iSee_temp_shared\CAM_Images.icns' + + #image_dir = '/Users/michaelbu/Data/Images/CAM_Images' # change this to temp directory on aibsdata + new_ns = cls.with_new_stimulus_from_dataframe(image_dir=image_dir, new_size=new_size, mode=mode, dtype=dtype, start_time=start_time, trial_length=trial_length, add_channels=add_channels) + + new_ns.add_gray_screen() + + return new_ns + + @staticmethod + def generate_stim_table(T,start_time=0,trial_length=250): + '''frame_length is in milliseconds''' + + start_time_array = trial_length*np.arange(T) + start_time + column_list = [np.arange(T),start_time_array, start_time_array+trial_length-1] # -1 is because the tables in BOb use inclusive intervals, so we'll stick to that convention + cols = np.vstack(column_list).T + stim_table = pd.DataFrame(cols,columns=['frame','start','end']) + + return stim_table + + def to_h5(self,sample_indices=None): + pass + + @classmethod + def with_new_stimulus_from_folder(cls, image_dir, new_size=(64,112), mode='L', dtype=np.float32, start_time=0, trial_length=250, add_channels=False): + + new_ns = cls(new_size=new_size, mode=mode, dtype=dtype, start_time=start_time, trial_length=trial_length, add_channels=add_channels) + + new_ns.im_list = os.listdir(image_dir) + new_ns.image_dir = image_dir + + stim_list = [] + for im in new_ns.im_list: + try: + im_data = Image.open(os.path.join(new_ns.image_dir,im)) + except IOError: + print("Skipping file: ", im) + new_ns.im_list.remove(im) + + im_data = im_data.convert(new_ns.mode) + if new_size is not None: + im_data = im_data.resize((new_ns.new_size[1], new_ns.new_size[0])) + im_data = np.array(im_data,dtype=new_ns.dtype) + if add_channels: + im_data = im_data[:,:,np.newaxis] + stim_list.append(im_data) + + new_ns.stim_template = np.stack(stim_list) + new_ns.num_images = new_ns.stim_template.shape[0] + + t,y,x = new_ns.stim_template.shape + new_ns.new_size = (y,x) + + new_ns.trial_length = trial_length + new_ns.start_time = start_time + new_ns.stim_table = new_ns.generate_stim_table(new_ns.num_images,start_time=new_ns.start_time,trial_length=new_ns.trial_length) + + new_ns.label_dataframe = pd.DataFrame(columns=['image_name']) + new_ns.label_dataframe['image_name'] = new_ns.im_list + + return new_ns + + @classmethod + def with_new_stimulus_from_dataframe(cls, image_dir, new_size=(64,112), mode='L', dtype=np.float32, start_time=0, trial_length=250, add_channels=False): + '''image_dir should contain a folder of images called 'images' and an hdf5 file with a + dataframe called 'label_dataframe.h5' with the frame stored in the key 'labels'. + dataframe should have columns ['relative_image_path','label_1', 'label_2', ...]''' + + new_ns = cls(new_size=new_size, mode=mode, dtype=dtype, start_time=start_time, trial_length=trial_length, add_channels=add_channels) + + image_path = os.path.join(image_dir,'images') + label_dataframe = pd.read_hdf(os.path.join(image_dir,'label_dataframe.h5'),'labels') + new_ns.label_dataframe = label_dataframe + + new_ns.image_dir = image_path + new_ns.im_list = list(label_dataframe.image_name) + + stim_list = [] + for im in new_ns.im_list: + try: + im_data = Image.open(os.path.join(image_path,im)) + except IOError: + print("Skipping file: ", im) + new_ns.im_list.remove(im) + + im_data = im_data.convert(new_ns.mode) + if new_size is not None: + im_data = im_data.resize((new_ns.new_size[1], new_ns.new_size[0])) + im_data = np.array(im_data,dtype=new_ns.dtype) + if add_channels: + im_data = im_data[:,:,np.newaxis] + stim_list.append(im_data) + + new_ns.stim_template = np.stack(stim_list) + new_ns.num_images = new_ns.stim_template.shape[0] + + if add_channels: + t,y,x,_ = new_ns.stim_template.shape + else: + t,y,x = new_new.stim_template.shape + new_ns.new_size = (y,x) + + new_ns.trial_length = trial_length + new_ns.start_time = start_time + new_ns.stim_table = new_ns.generate_stim_table(new_ns.num_images,start_time=new_ns.start_time,trial_length=new_ns.trial_length) + + return new_ns + + @staticmethod + def create_image_dir_from_hierarchy(folder, new_path, label_names=None): + + import shutil + + image_dataframe = pd.DataFrame(columns=["image_name"]) + + if os.path.exists(new_path): + raise Exception("path "+new_path+" already exists!") + + os.mkdir(new_path) + os.mkdir(os.path.join(new_path,'images')) + for path, sub_folders, file_list in os.walk(folder): + + for f in file_list: + try: + im_data = Image.open(os.path.join(path,f)) + except IOError: + print("Skipping file: ", f) + im_data = None + + if im_data is not None: + shutil.copy(os.path.join(path,f), os.path.join(new_path,'images',f)) + image_name = f + label_vals = os.path.split(os.path.relpath(path,folder)) + if label_names is not None: + current_label_names = label_names[:] + else: + current_label_names = [] + + if len(label_vals) > current_label_names: + labels_to_add = ["label_"+str(i) for i in range(len(current_label_names), len(label_vals))] + current_label_names += labels_to_add + elif len(label_vals) < current_label_names: + current_label_names = current_label_names[:len(label_vals)] + + vals = [f] + list(label_vals) + cols = ['image_name']+current_label_names + new_frame = pd.DataFrame([vals],columns=cols) + + image_dataframe = image_dataframe.append(new_frame,ignore_index=True) + + image_dataframe.to_hdf(os.path.join(new_path,'label_dataframe.h5'),'labels') + + # @staticmethod + # def add_object_to_image(image, object_image): + # + # new_image = image.copy() + # new_image[np.isfinite(object_image)] = object_image[np.isfinite(object_image)] + # return new_image + + @staticmethod + def add_object_to_template(template, object_image): + + if template.ndim==3: + T,y,x = template.shape + elif template.ndim==4: + T,y,x,K = template.shape + else: + raise Exception("template.ndim must be 3 or 4") + + if object_image.ndim < template.ndim-1: + object_image=object_image[:,:,np.newaxis] + + new_template = template.copy() + new_template[:,np.isfinite(object_image)] = object_image[np.isfinite(object_image)] + + return new_template + + def add_objects_to_foreground(self, object_dict): + + template_list = [] + + if self.label_dataframe is None: + self.label_dataframe = pd.DataFrame(columns=['object']) + + new_label_dataframe_list = [] + + for obj in object_dict: + template_list.append(self.add_object_to_template(self.stim_template,object_dict[obj])) + obj_dataframe = self.label_dataframe.copy() + obj_dataframe['object'] = [ obj for i in range(self.num_images) ] + new_label_dataframe_list.append(obj_dataframe) + + self.stim_template = np.vstack(template_list) + self.label_dataframe = pd.concat(new_label_dataframe_list,ignore_index=True) + + self.num_images = self.stim_template.shape[0] + + self.stim_table = self.generate_stim_table(self.num_images,start_time=self.start_time,trial_length=self.trial_length) + + + @staticmethod + def create_object_dict(folder, background_shape=(64,112), dtype=np.float32, rotations=False): + + from scipy.misc import imresize + + # resize function to preserve the nans in the background + def resize_im(im,new_shape): + def mask_for_nans(): + mask = np.ones(im.shape) + mask[np.isfinite(im)] = 0 + mask = imresize(mask,new_shape,interp='nearest') + + return mask.astype(np.bool) + + new_im = im.copy() + new_im = new_im.astype(dtype) + new_im[np.isnan(new_im)] = -1. + new_im = imresize(new_im,new_shape,interp='nearest') + + new_im = new_im.astype(dtype) + new_im[mask_for_nans()] = np.nan + + return new_im + + def im_on_background(im, shift=None): + bg = np.empty(background_shape) + bg[:] = np.nan + + buffer_x = (background_shape[1] - im.shape[1])/2 + buffer_y = (background_shape[0] - im.shape[0])/2 + + bg[buffer_y:im.shape[0]+buffer_y, buffer_x:im.shape[1]+buffer_x] = im + + return bg + + im_list = os.listdir(folder) + + obj_dict = {} + + for im_file in im_list: + try: + im = np.load(os.path.join(folder,im_file)) + except IOError: + print("skipping file: ", im_file) + im = None + + if im is not None: + new_shape = (np.min(background_shape), np.min(background_shape)) + im = resize_im(im,new_shape) + obj_dict[im_file[:-4]] = im_on_background(im) + if rotations: + im_rot=im.copy() + for i in range(3): + im_rot = np.rot90(im_rot) + obj_dict[im_file[:-4]+'_'+str(90*(i+1))] = im_on_background(im_rot) + + return obj_dict diff --git a/bmtk-vb/bmtk/simulator/utils/stimulus/StaticGratings.py b/bmtk-vb/bmtk/simulator/utils/stimulus/StaticGratings.py new file mode 100644 index 0000000..c7bf9cb --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/stimulus/StaticGratings.py @@ -0,0 +1,100 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import numpy as np +import pandas as pd + + +class StaticGratings (object): + + def __init__(self,orientations=30.0*np.arange(6),spatial_frequencies=0.01*(2.0**np.arange(1,6)),phases=0.25*np.arange(4),num_trials=50, start_time=0, trial_length=250): + + self.orientations = orientations + self.spatial_frequencies = spatial_frequencies + self.phases = phases + self.num_trials = num_trials + self.start_time = start_time + self.trial_length = trial_length + + trial_stims = np.array([ [orientation, spat_freq, phase] for orientation in self.orientations for spat_freq in self.spatial_frequencies for phase in self.phases ]) + + trial_stims = np.tile(trial_stims,(num_trials,1)) + + indices = np.random.permutation(trial_stims.shape[0]) + trial_stims = trial_stims[indices] + + self.stim_table = pd.DataFrame(trial_stims,columns=['orientation','spatial_frequency','phase']) + + T = self.stim_table.shape[0] + self.T = T + start_time_array = trial_length*np.arange(self.T) + start_time + end_time_array = start_time_array + trial_length + + self.stim_table['start'] = start_time_array + self.stim_table['end'] = end_time_array + + def get_image_input(self,new_size=(64,112),pix_per_degree=1.0, dtype=np.float32, add_channels=False): + + y, x = new_size + stim_template = np.empty([self.T, y, x],dtype=dtype) + + for t, row in self.stim_table.iterrows(): + ori, sf, ph = row[0], row[1], row[2] + + theta = ori*np.pi/180.0 #convert to radians + + k = (sf/pix_per_degree) # radians per pixel + ph = ph*np.pi*2.0 + + X,Y = np.meshgrid(np.arange(x),np.arange(y)) + X = X - x/2 + Y = Y - y/2 + Xp, Yp = self.rotate(X,Y,theta) + + stim_template[t] = np.cos(2.0*np.pi*Xp*k + ph) + + self.stim_template = stim_template + + if add_channels: + return stim_template[:,:,:,np.newaxis] + else: + return stim_template + + @staticmethod + def rotate(X,Y, theta): + + Xp = X*np.cos(theta) - Y*np.sin(theta) + Yp = X*np.sin(theta) + Y*np.cos(theta) + + return Xp, Yp + + @classmethod + def with_brain_observatory_stimulus(cls, num_trials=50): + + orientations = 30.0*np.arange(6) + spatial_frequencies = 0.01*(2.0**np.arange(1,6)) + phases = 0.25*np.arange(4) + + start_time = 0 + trial_length = 250 + + return cls(orientations=orientations,spatial_frequencies=spatial_frequencies,phases=phases,num_trials=num_trials,start_time=start_time,trial_length=trial_length) diff --git a/bmtk-vb/bmtk/simulator/utils/stimulus/__init__.py b/bmtk-vb/bmtk/simulator/utils/stimulus/__init__.py new file mode 100644 index 0000000..04c8f88 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/stimulus/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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/bmtk-vb/bmtk/simulator/utils/stimulus/lsn.npy b/bmtk-vb/bmtk/simulator/utils/stimulus/lsn.npy new file mode 100644 index 0000000000000000000000000000000000000000..f4358aeb37ef4128c566ebccaa66019223b275e3 GIT binary patch literal 4032080 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
LrbYy+AWU5#N)C>cz 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%0AqN_#PHe1$AHKA(NN7x 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 hDg&=0rKeta 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 z z-MAnC9uX9}JGW!LF{6{iDCW5tk&VK#jW#Lx=r*2NpF*YyjtS8vUy~yW3778ERh--l z2GKYPqe$dHIc&1x|zEBOeYohCjCw1}zTMj29AZ;qIebB?7+ItbyQ(7XvGDI-eoA z0>;sg+oG*09n;dxkO-_sp>Yz{0}f$onP6M!Sj(`8A6wx97gpw@6EI&8i}C(a&*Is* zEdKwR=n6Er{U>9*qq(~LpB;$g(dG(6Q_x{a0%G!5G&4Uy#k2z5QXfRs`B(o~a(Ul(0!#qmB5(f8)8==F^sjO) zeze&yEh2JvK1aH7UXnOu2TnG4XbB5Rky|C|3pbIvC~HbbrX?}4$a)7n)_% zpke2V&pt@IVYV!eztPVXgoERzVZHnHyp!e~So}qpa9_)sY)TU07JS~wQ~gtRmtU;DX){UlnD9eTV z?b1l-u;rE#VF+j+X-i z3((K9l6Fa&^^Xx&lZ}qf^oS+O$vCLK=@U2N+?J&cB z<3?HDp!*M}#}b5ui<;oH<>*fdA74CAu!%<7q`S|U;Yt$+(_Qys(n(>^t8o)C{1}6c z$uOa4Ns&v-8Hh~ROB`s4-UazynnB(G1(!}0g0SS9N{&Wmi5f#8F}V%Ou?Ke7Xx}9& z@A2=&NBZpzD%_pZ@L_h}@8RXK5Ram9M`GcywI)~#oo)8Lq@V6N#!>^ir9MyX$fRwR z`_rv-9S#$nuJzr+l4J1j3%2rqW!Z;^9@hVOHwz)wk3s5`RY+J$Kn2j^*QUnlUwwrcH#T!BDQ{=|JJ3@?8(xOb%QduE)ZK*5>xxxJ zIErQGI?u|2DXcsjAS3MTuUCFGn8sS!bFRLLmT{w>Kkc(a&fxQPepZy4(eSgV?W?M! zX1I()HxmUILEvh^VnIq!1m7&vpqZ0bz_ia?U>yygfCRy4MJdgWRlH^%>YcUh$NQ_J*99th|P>;*^ zeU+~4U%6ws>?d)Q=9mK*KrhWBnLbC~1JMQ%w28lG)siPS-+i1k$xy32G@H)SyAd1! z+U*esEyg3bMe?%<60dJ9l*Ek8mBN^daOzgZr(Yp+kxb3~pElB=XPQEi#J4Ws$h%qk z$Srm(6n4!L9uXy1JI8W+_JdU=Ww+>oZ|<1c@pwMxj4Z2Lvw(-9cdJVy&11R;Owc(8 z+c58YEIG&IUUy~0iduu^>`XS$BDWNx%`vUR&>n{(b1dh~#+TjfSUIqDN(Wh~MrP)G zR{osOUeQh&5%D>%vHgEewNzRsg_%dqoMM%XZow4c4V=(7@XT8AYZ88wD8Sm{I17`Q zbD+pbv{`SxnU0P8Vm6DP&e;*q<4@01-q@#o>NdsQ8z)Ro`PsPQjW^Q>FYcS0Kg#bS zy$q|AOGd0>8z<9&RbY-a8!h≪K(vltr#i-MkQisI8iMmJce)Mzqmzpg`;edV$=> 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 Ltxi6JOM?FgLJ~b@ literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/simulator/utils/tools/__init__.py b/bmtk-vb/bmtk/simulator/utils/tools/__init__.py new file mode 100644 index 0000000..04c8f88 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/tools/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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/bmtk-vb/bmtk/simulator/utils/tools/process_spikes.py b/bmtk-vb/bmtk/simulator/utils/tools/process_spikes.py new file mode 100644 index 0000000..0f5519a --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/tools/process_spikes.py @@ -0,0 +1,207 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import h5py +import numpy as np +import pandas as pd +import os + + +def read_spk_txt(f_name): + + ''' + + Parameters + ---------- + f_name: string + Full path to a file containing cell IDs and spike times. + + Returns + ------- + A dataframe containing two columns: spike times and cell IDs. + + Usage: + x = read_spk_txt('output/spk.dat') + + ''' + + df = pd.read_csv(f_name, header=None, sep=' ') + df.columns = ['t', 'gid'] + + return df + + +def read_spk_h5(f_name): + + ''' + + Parameters + ---------- + f_name: string + Full path to a file containing cell IDs and spike times. + + Returns + ------- + A dataframe containing two columns: spike times and cell IDs. + + Usage: + x = read_spk_h5('output/spk.h5') + + ''' + + f = h5py.File(f_name, 'r' , libver='latest') + spikes = {} + + t = np.array([]) + gids = np.array([]) + for i, gid in enumerate(f.keys()): # save spikes of all gids + if (i % 1000 == 0): + print(i) + spike_times = f[gid][...] + t = np.append(t, spike_times) + gids = np.append(gids, np.ones(spike_times.size)*int(gid)) + + f.close() + + df = pd.DataFrame(columns=['t', 'gid']) + df['t'] = t + df['gid'] = gids + + return df + + +def spikes_to_mean_f_rate(cells_f, spk_f, t_window, **kwargs): + + ''' + + Parameters + ---------- + cells_f: string + Full path to a file containing information about all cells (in particular, all cell IDs, + and not just those that fired spikes in a simulation). + spk_f: string + Full path to a file containing cell IDs and spike times. + t_window: a tuple of two floats + Start and stop time for the window within which the firing rate is computed. + **kwargs + spk_f_type: string with accepted values 'txt' or 'h5' + Type of the file from which spike times should be extracted. + + + Assumptions + ----------- + It is assumed here that TIME IS in ms and the RATES ARE RETURNED in Hz. + + + Returns + ------- + A dataframe containing a column of cell IDs and a column of corresponding + average firing rates. + + Usage: + x = spikes_to_mean_f_rate('../network_model/cells.csv', 'output/spk.dat', (500.0, 3000.0)) + + ''' + + # Make sure the time window's start and stop times are reasonable. + t_start = t_window[0] + t_stop = t_window[1] + delta_t = t_stop - t_start + if (delta_t <= 0.0): + print('spikes_to_mean_f_rate: stop time %f is <= start time %f; exiting.' % (t_stop, t_start)) + quit() + + # Read information about all cells. + cells_df = pd.read_csv(cells_f, sep=' ') + gids = cells_df['id'].values + + # By default, the spk file type is "None", in which case it should be chosen + # based on the extension of the supplied spk file name. + spk_f_type = kwargs.get('spk_f_type', None) + if (spk_f_type == None): + spk_f_ext = spk_f.split('.')[-1] + if (spk_f_ext in ['txt', 'dat']): + spk_f_type = 'txt' # Assume this is an ASCII file. + elif (spk_f_ext in ['h5']): + spk_f_type = 'h5' # Assume this is an HDF5 file. + else: + print('spikes_to_mean_f_rate: unrecognized file extension. Use the flag spk_f_type=\'txt\' or \'h5\' to override this message. Exiting.') + quit() + + # In case the spk_f_type was provided directly, check that the value is among those the code recognizes. + if (spk_f_type not in ['txt', 'h5']): + print('spikes_to_mean_f_rate: unrecognized value of spk_f_type. The recognized values are \'txt\' or \'h5\'. Exiting.') + quit() + + # Read spikes. + # If the spike file has zero size, create a dataframe with all rates equal to zero. + # Otherwise, use spike times from the file to fill the dataframe. + if (os.stat(spk_f).st_size == 0): + f_rate_df = pd.DataFrame(columns=['gid', 'f_rate']) + f_rate_df['gid'] = gids + f_rate_df['f_rate'] = np.zeros(gids.size) + else: + # Use the appropriate function to read the spikes. + if (spk_f_type == 'txt'): + df = read_spk_txt(spk_f) + elif(spk_f_type == 'h5'): + df = read_spk_h5(spk_f) + + # Keep only those entries that have spike times within the time window. + df = df[(df['t'] >= t_start) & (df['t'] <= t_stop)] + + # Compute rates. + f_rate_df = df.groupby('gid').count() * 1000.0 / delta_t # Time is in ms and rate is in Hz. + f_rate_df.columns = ['f_rate'] + # The 'gid' label is now used as index (after the groupby operation). + # Convert it to a column; then change the index name to none, as in default. + f_rate_df['gid'] = f_rate_df.index + f_rate_df.index.names = [''] + + # Find cell IDs from the spk file that are not in the cell file. + # Remove them from the dataframe with rates. + gids_not_in_cells_f = f_rate_df['gid'].values[~np.in1d(f_rate_df['gid'].values, gids)] + f_rate_df = f_rate_df[~f_rate_df['gid'].isin(gids_not_in_cells_f)] + + # Find cell IDs from the cell file that do not have counterparts in the spk file + # (for example, because those cells did not fire). + # Add these cell IDs to the dataframe; fill rates with zeros. + gids_not_in_spk = gids[~np.in1d(gids, f_rate_df['gid'].values)] + f_rate_df = f_rate_df.append(pd.DataFrame(np.array([gids_not_in_spk, np.zeros(gids_not_in_spk.size)]).T, columns=['gid', 'f_rate'])) + + # Sort the rows according to the cell IDs. + f_rate_df = f_rate_df.sort('gid', ascending=True) + + return f_rate_df + + +# Tests. + +#x = spikes_to_mean_f_rate('/data/mat/yazan/corticalCol/ice/sims/column/build/net_structure/cells.csv', '/data/mat/yazan/corticalCol/ice/sims/column/full_preliminary_runs/output008/spikes.txt', (500.0, 2500.0)) +#print x + +#x = spikes_to_mean_f_rate('/data/mat/yazan/corticalCol/ice/sims/column/build/net_structure/cells.csv', '/data/mat/yazan/corticalCol/ice/sims/column/full_preliminary_runs/output008/spikes.h5', (500.0, 2500.0)) +#print x + +#x = spikes_to_mean_f_rate('/data/mat/yazan/corticalCol/ice/sims/column/build/net_structure/cells.csv', '/data/mat/yazan/corticalCol/ice/sims/column/full_preliminary_runs/output008/spikes.txt', (500.0, 2500.0), spk_f_type='txt') +#print x + diff --git a/bmtk-vb/bmtk/simulator/utils/tools/spatial.py b/bmtk-vb/bmtk/simulator/utils/tools/spatial.py new file mode 100644 index 0000000..9b331d0 --- /dev/null +++ b/bmtk-vb/bmtk/simulator/utils/tools/spatial.py @@ -0,0 +1,26 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# + + +def example(): + print('OK') diff --git a/bmtk-vb/bmtk/test.py~ b/bmtk-vb/bmtk/test.py~ new file mode 100644 index 0000000..2d77926 --- /dev/null +++ b/bmtk-vb/bmtk/test.py~ @@ -0,0 +1,3 @@ +from bmtk.builder.networks import NetworkBuilder + +net = NetworkBuilder("cortical-column") diff --git a/bmtk-vb/bmtk/tests/builder/test_connection_map.py b/bmtk-vb/bmtk/tests/builder/test_connection_map.py new file mode 100644 index 0000000..9043d7b --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_connection_map.py @@ -0,0 +1,116 @@ +import pytest +from itertools import product + +from bmtk.builder.connection_map import ConnectionMap +from bmtk.builder import NetworkBuilder + + +@pytest.fixture +def net(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, x=range(100), ei='i') + net.add_nodes(N=50, x=range(50), y='y', ei='e') + return net + + +def test_connection_map_fnc(net): + cm = ConnectionMap(sources=net.nodes(ei='i'), targets=net.nodes(ei='e'), + connector=lambda s, t, a, b: s['node_id']*t['node_id'], + connector_params={'a': 1, 'b': 0}, iterator='one_to_one', + edge_type_properties={'prop1': 'prop1', 'edge_type_id': 101}) + assert(len(cm.source_nodes) == 100) + assert(len(cm.target_nodes) == 50) + assert(cm.params == []) + assert(cm.iterator == 'one_to_one') + assert(len(cm.edge_type_properties.keys()) == 2) + assert(cm.edge_type_id == 101) + for v in cm.connection_itr(): + src_id, trg_id, val = v + assert(val == src_id*trg_id) + + +def test_connection_map_num(net): + cm = ConnectionMap(sources=net.nodes(ei='i'), targets=net.nodes(ei='e'), connector=10) + count = 0 + for v in cm.connection_itr(): + src_id, trg_id, val = v + assert(val == 10) + count += 1 + assert(count == 5000) + + +def test_connection_map_list(net): + cm = ConnectionMap(sources=net.nodes(ei='i'), targets=net.nodes(ei='e'), + connector=[s.node_id*t.node_id for s, t in product(net.nodes(ei='i'), net.nodes(ei='e'))]) + count = 0 + for v in cm.connection_itr(): + src_id, trg_id, val = v + assert(val == src_id*trg_id) + count += 1 + assert(count == 5000) + + +def test_connection_map_dict(net): + cm = ConnectionMap(sources=net.nodes(ei='i'), targets=net.nodes(ei='e'), connector={'nsyn': 10}) + for v in cm.connection_itr(): + src_id, trg_id, val = v + assert('nsyn' in val and val['nsyn'] == 10) + + +def test_cm_params1(net): + cm = ConnectionMap(sources=net.nodes(ei='i'), targets=net.nodes(ei='e'), + connector=lambda s, t: 3, + edge_type_properties={'prop1': 'prop1', 'edge_type_id': 101}) + cm.add_properties(names='syn_weight', rule=lambda a: a+0.15, rule_params={'a': 0.20}, dtypes=float) + + assert(len(cm.params) == 1) + edge_props_1 = cm.params[0] + assert(edge_props_1.names == 'syn_weight') + assert(edge_props_1.get_prop_dtype('syn_weight') == float) + for v in cm.connection_itr(): + src_id, trg_id, nsyn = v + assert(nsyn == 3) + assert(edge_props_1.rule() == 0.35) + + +def test_cm_params2(net): + cm = ConnectionMap(sources=net.nodes(ei='i'), targets=net.nodes(ei='e'), + connector=lambda s, t: 3, + edge_type_properties={'prop1': 'prop1', 'edge_type_id': 101}) + cm.add_properties(names=['w', 'c'], rule=0.15, dtypes=[float, str]) + + assert(len(cm.params) == 1) + edge_props_1 = cm.params[0] + assert(edge_props_1.names == ['w', 'c']) + assert(edge_props_1.get_prop_dtype('w')) + assert (edge_props_1.get_prop_dtype('c')) + for v in cm.connection_itr(): + src_id, trg_id, nsyn = v + assert(nsyn == 3) + assert(edge_props_1.rule() == 0.15) + + +def test_cm_params3(net): + cm = ConnectionMap(sources=net.nodes(ei='i'), targets=net.nodes(ei='e'), + connector=lambda s, t: 3, + edge_type_properties={'prop1': 'prop1', 'edge_type_id': 101}) + cm.add_properties(names=['w', 'c'], rule=0.15, dtypes=[float, str]) + cm.add_properties(names='a', rule=(1, 2, 3), dtypes=dict) + + assert(len(cm.params) == 2) + edge_props_1 = cm.params[0] + assert(edge_props_1.names == ['w', 'c']) + assert(edge_props_1.get_prop_dtype('w')) + assert(edge_props_1.get_prop_dtype('c')) + for v in cm.connection_itr(): + src_id, trg_id, nsyn = v + assert(nsyn == 3) + assert(edge_props_1.rule() == 0.15) + + edge_props_2 = cm.params[1] + assert(edge_props_2.names == 'a') + assert(edge_props_2.get_prop_dtype('a')) + assert(edge_props_2.rule() == (1, 2, 3)) + + +test_connection_map_fnc(net()) \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/builder/test_connector.py b/bmtk-vb/bmtk/tests/builder/test_connector.py new file mode 100644 index 0000000..99aef6f --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_connector.py @@ -0,0 +1,44 @@ +from bmtk.builder import connector + + +def test_fnc_params(): + con_fnc = connector.create(connector=lambda x, p: x**p) + assert(con_fnc(2, 3) == 2**3) + + +def test_fnc_noparams(): + con_fnc = connector.create(connector=lambda x, p, a:x**p+a, a=10) + assert(con_fnc(2, 3) == 2**3+10) + + +def test_literal(): + con_fnc = connector.create(connector=100.0) + assert(con_fnc() == 100.0) + + con_fnc1 = connector.create(connector=101.0, a=10, b='10') # parameters in literals should be ignored + assert(con_fnc1() == 101.0) + + +def test_list(): + con_fnc = connector.create(connector=['a', 'b', 'c']) + assert(con_fnc == ['a', 'b', 'c']) + + con_fnc1 = connector.create(connector=[100, 200, 300], p1=1, p2='2', p34=(3,4)) + assert(con_fnc1 == [100, 200, 300]) + + +def test_dict(): + con_fnc = connector.create(connector={'a': 1, 'b': 'b', 'c': [5, 6]}) + assert('a' in con_fnc()) + assert('b' in con_fnc()) + assert('c' in con_fnc()) + + con_fnc = connector.create(connector={'a': 1, 'b': 'b', 'c': [5, 6]}, p1='p1', p2=2) + assert('a' in con_fnc()) + assert('b' in con_fnc()) + assert('c' in con_fnc()) + + +#test_dict() +#test_connector_fnc_params() +#test_connector_fnc_noparams() \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/builder/test_densenetwork.py b/bmtk-vb/bmtk/tests/builder/test_densenetwork.py new file mode 100644 index 0000000..81dfdc7 --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_densenetwork.py @@ -0,0 +1,307 @@ +import os +import shutil +import pytest +import numpy as np +import pandas as pd +import h5py +import tempfile + +from bmtk.builder import NetworkBuilder + + +def test_create_network(): + net = NetworkBuilder('NET1') + assert(net.name == 'NET1') + assert(net.nnodes == 0) + assert(net.nedges == 0) + assert(net.nodes_built is False) + assert(net.edges_built is False) + + +def test_no_name(): + with pytest.raises(Exception): + NetworkBuilder('') + + +def test_build_nodes(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, + position=[(100.0, -50.0, 50.0)]*100, + tunning_angle=np.linspace(0, 365.0, 100, endpoint=False), + cell_type='Scnna1', + model_type='Biophys1', + location='V1', + ei='e') + + net.add_nodes(N=25, + position=np.random.rand(25, 3)*[100.0, 50.0, 100.0], + model_type='intfire1', + location='V1', + ei='e') + + net.add_nodes(N=150, + position=np.random.rand(150, 3)*[100.0, 50.0, 100.0], + tunning_angle=np.linspace(0, 365.0, 150, endpoint=False), + cell_type='SST', + model_type='Biophys1', + location='V1', + ei='i') + + net.build() + assert(net.nodes_built is True) + assert(net.nnodes == 275) + assert(net.nedges == 0) + assert(len(net.nodes()) == 275) + assert(len(net.nodes(ei='e')) == 125) + assert(len(net.nodes(model_type='Biophys1')) == 250) + assert(len(net.nodes(location='V1', model_type='Biophys1'))) + + intfire_nodes = list(net.nodes(model_type='intfire1')) + assert(len(intfire_nodes) == 25) + node1 = intfire_nodes[0] + assert(node1['model_type'] == 'intfire1' and 'cell_type' not in node1) + + +def test_build_nodes1(): + net = NetworkBuilder('NET1') + net.add_nodes(N=3, node_id=[100, 200, 300], node_type_id=101, name=['one', 'two', 'three']) + node_one = list(net.nodes(name='one'))[0] + assert(node_one['name'] == 'one') + assert(node_one['node_id'] == 100) + assert(node_one['node_type_id'] == 101) + + node_three = list(net.nodes(name='three'))[0] + assert(node_three['name'] == 'three') + assert(node_three['node_id'] == 300) + assert(node_three['node_type_id'] == 101) + + +def test_build_nodes_fail1(): + net = NetworkBuilder('NET1') + with pytest.raises(Exception): + net.add_nodes(N=100, list1=[100]*99) + + +def test_build_nodes_fail2(): + net = NetworkBuilder('NET1') + with pytest.raises(Exception): + net.add_nodes(N=2, node_type_id=0) + net.add_nodes(N=2, node_type_id=0) + + +def test_nsyn_edges(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, cell_type='Scnna1', ei='e') + net.add_nodes(N=100, cell_type='PV1', ei='i') + net.add_nodes(N=100, cell_type='PV2', ei='i') + net.add_edges(source={'ei': 'i'}, target={'ei': 'e'}, connection_rule=lambda s, t: 1) # 200*100 = 20000 edges + net.add_edges(source=net.nodes(cell_type='Scnna1'), target=net.nodes(cell_type='PV1'), + connection_rule=lambda s, t: 2) # 100*100*2 = 20000 + net.build() + assert(net.nedges == 20000 + 20000) + assert(net.edges_built is True) + #print list(net.edges()) + + +def test_save_nsyn_table(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, cell_type='Scnna1', ei='e') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, cell_type='PV1', ei='i') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, tags=np.linspace(0, 100, 100), cell_type='PV2', ei='i') + net.add_edges(source={'ei': 'i'}, target={'ei': 'e'}, connection_rule=lambda s, t: 1, + p1='e2i', p2='e2i') # 200*100 = 20000 edges + net.add_edges(source=net.nodes(cell_type='Scnna1'), target=net.nodes(cell_type='PV1'), + connection_rule=lambda s, t: 2, p1='s2p') # 100*100*2 = 20000 + net.build() + nodes_h5 = tempfile.NamedTemporaryFile(suffix='.h5') + nodes_csv = tempfile.NamedTemporaryFile(suffix='.csv') + edges_h5 = tempfile.NamedTemporaryFile(suffix='.h5') + edges_csv = tempfile.NamedTemporaryFile(suffix='.csv') + + net.save_nodes(nodes_h5.name, nodes_csv.name) + net.save_edges(edges_h5.name, edges_csv.name) + + assert(os.path.exists(nodes_h5.name) and os.path.exists(nodes_csv.name)) + node_types_df = pd.read_csv(nodes_csv.name, sep=' ') + assert(len(node_types_df) == 3) + assert('cell_type' in node_types_df.columns) + assert('ei' in node_types_df.columns) + assert('positions' not in node_types_df.columns) + + nodes_h5 = h5py.File(nodes_h5.name, 'r') + assert ('node_id' in nodes_h5['/nodes/NET1']) + assert (len(nodes_h5['/nodes/NET1/node_id']) == 300) + assert (len(nodes_h5['/nodes/NET1/node_type_id']) == 300) + assert (len(nodes_h5['/nodes/NET1/node_group_id']) == 300) + assert (len(nodes_h5['/nodes/NET1/node_group_index']) == 300) + + node_groups = {nid: grp for nid, grp in nodes_h5['/nodes/NET1'].items() if isinstance(grp, h5py.Group)} + for grp in node_groups.values(): + if len(grp) == 1: + assert ('position' in grp and len(grp['position']) == 200) + + elif len(grp) == 2: + assert ('position' in grp and len(grp['position']) == 100) + assert ('tags' in grp and len(grp['tags']) == 100) + + else: + assert False + + assert(os.path.exists(edges_h5.name) and os.path.exists(edges_csv.name)) + edge_types_df = pd.read_csv(edges_csv.name, sep=' ') + assert (len(edge_types_df) == 2) + assert ('p1' in edge_types_df.columns) + assert ('p2' in edge_types_df.columns) + + edges_h5 = h5py.File(edges_h5.name, 'r') + assert('source_to_target' in edges_h5['/edges/NET1_to_NET1/indicies']) + assert ('target_to_source' in edges_h5['/edges/NET1_to_NET1/indicies']) + assert (len(edges_h5['/edges/NET1_to_NET1/target_node_id']) == 30000) + assert (len(edges_h5['/edges/NET1_to_NET1/source_node_id']) == 30000) + + assert (edges_h5['/edges/NET1_to_NET1/target_node_id'][0] == 0) + assert (edges_h5['/edges/NET1_to_NET1/source_node_id'][0] == 100) + assert (edges_h5['/edges/NET1_to_NET1/edge_group_index'][0] == 0) + assert (edges_h5['/edges/NET1_to_NET1/edge_type_id'][0] == 100) + assert (edges_h5['/edges/NET1_to_NET1/0/nsyns'][0] == 1) + + assert (edges_h5['/edges/NET1_to_NET1/target_node_id'][29999] == 199) + assert (edges_h5['/edges/NET1_to_NET1/source_node_id'][29999] == 99) + assert (edges_h5['/edges/NET1_to_NET1/edge_group_id'][29999] == 0) + assert (edges_h5['/edges/NET1_to_NET1/edge_type_id'][29999] == 101) + assert (edges_h5['/edges/NET1_to_NET1/0/nsyns'][29999] == 2) + + #try: + # os.remove('tmp_nodes.h5') + # os.remove('tmp_node_types.csv') + # os.remove('tmp_edges.h5') + # os.remove('tmp_edge_types.csv') + #except: + # pass + + +def test_save_weights(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, cell_type='Scnna1', ei='e') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, cell_type='PV1', ei='i') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, tags=np.linspace(0, 100, 100), cell_type='PV2', ei='i') + cm = net.add_edges(source={'ei': 'i'}, target={'ei': 'e'}, connection_rule=lambda s, t: 3, + p1='e2i', p2='e2i') # 200*100 = 60000 edges + cm.add_properties(names=['segment', 'distance'], rule=lambda s, t: [1, 0.5], dtypes=[np.int, np.float]) + + net.add_edges(source=net.nodes(cell_type='Scnna1'), target=net.nodes(cell_type='PV1'), + connection_rule=lambda s, t: 2, p1='s2p') # 100*100 = 20000' + + net.build() + net_dir = tempfile.mkdtemp() + net.save_nodes('tmp_nodes.h5', 'tmp_node_types.csv', output_dir=net_dir) + net.save_edges('tmp_edges.h5', 'tmp_edge_types.csv', output_dir=net_dir) + + edges_h5 = h5py.File('{}/tmp_edges.h5'.format(net_dir), 'r') + assert(net.nedges == 80000) + assert(len(edges_h5['/edges/NET1_to_NET1/0/distance']) == 60000) + assert(len(edges_h5['/edges/NET1_to_NET1/0/segment']) == 60000) + assert(len(edges_h5['/edges/NET1_to_NET1/1/nsyns']) == 10000) + assert(edges_h5['/edges/NET1_to_NET1/0/distance'][0] == 0.5) + assert(edges_h5['/edges/NET1_to_NET1/0/segment'][0] == 1) + assert(edges_h5['/edges/NET1_to_NET1/1/nsyns'][0] == 2) + + #try: + # os.remove('tmp_nodes.h5') + # os.remove('tmp_node_types.csv') + # os.remove('tmp_edges.h5') + # os.remove('tmp_edge_types.csv') + #except: + # pass + + +def test_save_multinetwork(): + net1 = NetworkBuilder('NET1') + net1.add_nodes(N=100, position=[(0.0, 1.0, -1.0)] * 100, cell_type='Scnna1', ei='e') + net1.add_edges(source={'ei': 'e'}, target={'ei': 'e'}, connection_rule=5, ctype_1='n1_rec') + net1.build() + + net2 = NetworkBuilder('NET2') + net2.add_nodes(N=10, position=[(0.0, 1.0, -1.0)] * 10, cell_type='PV1', ei='i') + net2.add_edges(connection_rule=10, ctype_1='n2_rec') + net2.add_edges(source=net1.nodes(), target={'ei': 'i'}, connection_rule=1, ctype_2='n1_n2') + net2.add_edges(target=net1.nodes(cell_type='Scnna1'), source={'cell_type': 'PV1'}, connection_rule=2, + ctype_2='n2_n1') + net2.build() + + net_dir = tempfile.mkdtemp() + net1.save_edges(output_dir=net_dir) + net2.save_edges(output_dir=net_dir) + + n1_n1_fname = '{}/{}_{}'.format(net_dir, 'NET1', 'NET1') + edges_h5 = h5py.File(n1_n1_fname + '_edges.h5', 'r') + assert(len(edges_h5['/edges/NET1_to_NET1/target_node_id']) == 100*100) + assert(len(edges_h5['/edges/NET1_to_NET1/0/nsyns']) == 100*100) + assert(edges_h5['/edges/NET1_to_NET1/0/nsyns'][0] == 5) + edge_types_csv = pd.read_csv(n1_n1_fname + '_edge_types.csv', sep=' ') + assert(len(edge_types_csv) == 1) + assert('ctype_2' not in edge_types_csv.columns.values) + assert(edge_types_csv['ctype_1'].iloc[0] == 'n1_rec') + + n1_n2_fname = '{}/{}_{}'.format(net_dir, 'NET1', 'NET2') + edges_h5 = h5py.File(n1_n2_fname + '_edges.h5', 'r') + assert(len(edges_h5['/edges/NET1_to_NET2/target_node_id']) == 100*10) + assert(len(edges_h5['/edges/NET1_to_NET2/0/nsyns']) == 100*10) + assert(edges_h5['/edges/NET1_to_NET2/0/nsyns'][0] == 1) + edge_types_csv = pd.read_csv(n1_n2_fname + '_edge_types.csv', sep=' ') + assert(len(edge_types_csv) == 1) + assert('ctype_1' not in edge_types_csv.columns.values) + assert(edge_types_csv['ctype_2'].iloc[0] == 'n1_n2') + + n2_n1_fname = '{}/{}_{}'.format(net_dir, 'NET2', 'NET1') + edges_h5 = h5py.File(n2_n1_fname + '_edges.h5', 'r') + assert(len(edges_h5['/edges/NET2_to_NET1/target_node_id']) == 100*10) + assert(len(edges_h5['/edges/NET2_to_NET1/0/nsyns']) == 100*10) + assert(edges_h5['/edges/NET2_to_NET1/0/nsyns'][0] == 2) + edge_types_csv = pd.read_csv(n2_n1_fname + '_edge_types.csv', sep=' ') + assert(len(edge_types_csv) == 1) + assert('ctype_1' not in edge_types_csv.columns.values) + assert(edge_types_csv['ctype_2'].iloc[0] == 'n2_n1') + + n2_n2_fname = '{}/{}_{}'.format(net_dir, 'NET2', 'NET2') + edges_h5 = h5py.File(n2_n2_fname + '_edges.h5', 'r') + assert(len(edges_h5['/edges/NET2_to_NET2/target_node_id']) == 10*10) + assert(len(edges_h5['/edges/NET2_to_NET2/0/nsyns']) == 10*10) + assert(edges_h5['/edges/NET2_to_NET2/0/nsyns'][0] == 10) + edge_types_csv = pd.read_csv(n2_n2_fname + '_edge_types.csv', sep=' ') + assert(len(edge_types_csv) == 1) + assert('ctype_2' not in edge_types_csv.columns.values) + assert(edge_types_csv['ctype_1'].iloc[0] == 'n2_rec') + + +def test_save_multinetwork_1(): + net1 = NetworkBuilder('NET1') + net1.add_nodes(N=100, position=[(0.0, 1.0, -1.0)] * 100, cell_type='Scnna1', ei='e') + net1.add_edges(source={'ei': 'e'}, target={'ei': 'e'}, connection_rule=5, ctype_1='n1_rec') + net1.build() + + net2 = NetworkBuilder('NET2') + net2.add_nodes(N=10, position=[(0.0, 1.0, -1.0)] * 10, cell_type='PV1', ei='i') + net2.add_edges(connection_rule=10, ctype_1='n2_rec') + net2.add_edges(source=net1.nodes(), target={'ei': 'i'}, connection_rule=1, ctype_2='n1_n2') + net2.add_edges(target=net1.nodes(cell_type='Scnna1'), source={'cell_type': 'PV1'}, connection_rule=2, + ctype_2='n2_n1') + net2.build() + net_dir = tempfile.mkdtemp() + net2.save_edges(edges_file_name='NET2_NET1_edges.h5', edge_types_file_name='NET2_NET1_edge_types.csv', + output_dir=net_dir, src_network='NET2') + + n1_n2_fname = '{}/{}_{}'.format(net_dir, 'NET2', 'NET1') + edges_h5 = h5py.File(n1_n2_fname + '_edges.h5', 'r') + assert(len(edges_h5['/edges/NET2_to_NET1/target_node_id']) == 100*10) + assert(len(edges_h5['/edges/NET2_to_NET1/0/nsyns']) == 100*10) + assert(edges_h5['/edges/NET2_to_NET1/0/nsyns'][0] == 2) + edge_types_csv = pd.read_csv(n1_n2_fname + '_edge_types.csv', sep=' ') + assert(len(edge_types_csv) == 1) + assert('ctype_1' not in edge_types_csv.columns.values) + assert(edge_types_csv['ctype_2'].iloc[0] == 'n2_n1') + + +if __name__ == '__main__': + test_save_weights() + # test_save_multinetwork_1() diff --git a/bmtk-vb/bmtk/tests/builder/test_edge_iterator.py b/bmtk-vb/bmtk/tests/builder/test_edge_iterator.py new file mode 100644 index 0000000..8d968fc --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_edge_iterator.py @@ -0,0 +1,72 @@ +import pytest + +from bmtk.builder import NetworkBuilder + +def test_itr_basic(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, cell_type='Scnna1', ei='e') + net.add_nodes(N=100, position=[(0.0, 1.0, -1.0)]*100, cell_type='PV1', ei='i') + net.add_edges(source={'ei': 'e'}, target={'ei': 'i'}, connection_rule=5, syn_type='e2i') + net.add_edges(source={'cell_type': 'PV1'}, target={'cell_type': 'Scnna1'}, connection_rule=5, syn_type='i2e') + net.build() + + edges = net.edges() + assert(len(edges) == 100*100*2) + assert(edges[0]['nsyns'] == 5) + + +def test_itr_advanced_search(): + net = NetworkBuilder('NET1') + net.add_nodes(N=1, cell_type='Scnna1', ei='e') + net.add_nodes(N=50, cell_type='PV1', ei='i') + net.add_nodes(N=100, cell_type='PV2', ei='i') + net.add_edges(source={'ei': 'e'}, target={'ei': 'i'}, connection_rule=5, syn_type='e2i', nm='A') + net.add_edges(source={'cell_type': 'PV1'}, target={'cell_type': 'PV2'}, connection_rule=5, syn_type='i2i', nm='B') + net.add_edges(source={'cell_type': 'PV2'}, target={'ei': 'i'}, connection_rule=5, syn_type='i2i', nm='C') + net.build() + + edges = net.edges(target_nodes=net.nodes(cell_type='Scnna1')) + assert(len(edges) == 0) + + edges = net.edges(source_nodes={'ei': 'e'}, target_nodes={'ei': 'i'}) + assert(len(edges) == 50 + 100) + + edges = net.edges(source_nodes=[n.node_id for n in net.nodes(ei='e')]) + assert(len(edges) == 50 + 100) + + edges = net.edges(source_nodes={'ei': 'i'}) + assert(len(edges) == 100 * 100 * 2) + for e in edges: + assert(e['syn_type'] == 'i2i') + + edges = net.edges(syn_type='i2i') + print(len(edges) == 100 * 100 * 2) + for e in edges: + assert(e['nm'] != 'A') + + edges = net.edges(syn_type='i2i', nm='C') + assert(len(edges) == 100 * 150) + + +def test_mulitnet_iterator(): + net1 = NetworkBuilder('NET1') + net1.add_nodes(N=50, cell_type='Rorb', ei='e') + net1.build() + + net2 = NetworkBuilder('NET2') + net2.add_nodes(N=100, cell_type='Scnna1', ei='e') + net2.add_nodes(N=100, cell_type='PV1', ei='i') + net2.add_edges(source={'ei': 'e'}, target={'ei': 'i'}, connection_rule=5, syn_type='e2i', net_type='rec') + net2.add_edges(source=net1.nodes(), target={'ei': 'e'}, connection_rule=1, syn_type='e2e', net_type='fwd') + net2.build() + + assert(len(net2.edges()) == 50*100 + 100*100) + assert(len(net2.edges(source_network='NET2', target_network='NET1')) == 0) + assert(len(net2.edges(source_network='NET1', target_network='NET2')) == 50*100) + assert(len(net2.edges(target_network='NET2', net_type='rec')) == 100*100) + + edges = net2.edges(source_network='NET1') + assert(len(edges) == 50*100) + for e in edges: + assert(e['net_type'] == 'fwd') + diff --git a/bmtk-vb/bmtk/tests/builder/test_id_generator.py b/bmtk-vb/bmtk/tests/builder/test_id_generator.py new file mode 100644 index 0000000..ba9dc5e --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_id_generator.py @@ -0,0 +1,36 @@ +import pytest + +from bmtk.builder.id_generator import IDGenerator + +def test_generator(): + generator = IDGenerator() + assert(generator.next() == 0) + assert(generator.next() == 1) + assert(generator.next() == 2) + + +def test_generator_initval(): + generator = IDGenerator(101) + assert(generator.next() == 101) + assert(generator.next() == 102) + assert(generator.next() == 103) + + +def test_contains(): + generator = IDGenerator(init_val=10) + gids = [generator.next() for _ in range(10)] + assert(len(gids) == 10) + assert(10 in generator) + assert(19 in generator) + assert(20 not in generator) + + +def test_remove(): + generator = IDGenerator(init_val=101) + assert(generator.next() == 101) + generator.remove_id(102) + generator.remove_id(104) + generator.remove_id(106) + assert(generator.next() == 103) + assert(generator.next() == 105) + assert(generator.next() == 107) diff --git a/bmtk-vb/bmtk/tests/builder/test_iterator.py b/bmtk-vb/bmtk/tests/builder/test_iterator.py new file mode 100644 index 0000000..82692d5 --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_iterator.py @@ -0,0 +1,134 @@ +import pytest +import itertools + +from bmtk.builder import connector, iterator +from bmtk.builder import NetworkBuilder +from bmtk.builder.node import Node + + +@pytest.fixture +def network(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, x=range(100), ei='i') + net.add_nodes(N=50, x=range(50), y='y', ei='e') + return net + + +def test_one2one_fnc(): + def connector_fnc(s, t): + assert(s['ei'] == 'i') + assert(t['ei'] == 'e') + return '100' + + net = network() + conr = connector.create(connector_fnc) + itr = iterator.create('one_to_one', conr) + count = 0 + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(src_id < 100) + assert(trg_id >= 100) + assert(val == '100') + count += 1 + assert(count == 100*50) + + +def test_one2all_fnc(): + def connector_fnc(s, ts): + assert(isinstance(s, Node)) + assert(s['ei'] == 'i') + assert(len(ts) == 50) + return [100]*50 + + net = network() + conr = connector.create(connector_fnc) + itr = iterator.create('one_to_all', conr) + count = 0 + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(src_id < 100) + assert(trg_id >= 100) + assert(val == 100) + count += 1 + assert(count == 5000) + + +def test_all2one_fnc(): + def connector_fnc(ss, t): + assert(isinstance(t, Node)) + assert(t['ei'] == 'e') + assert(len(ss) == 100) + return [100]*100 + + net = network() + conr = connector.create(connector_fnc) + itr = iterator.create('all_to_one', conr) + count = 0 + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(src_id < 100) + assert(trg_id >= 100) + assert(val == 100) + count += 1 + assert(count == 5000) + + +def test_literal(): + net = network() + conr = connector.create(100) + itr = iterator.create('one_to_one', conr) + count = 0 + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(src_id < 100) + assert(trg_id >= 100) + assert(val == 100) + count += 1 + + assert(count == 5000) + + +def test_dict(): + net = network() + conr = connector.create({'nsyn': 10, 'target': 'axon'}) + itr = iterator.create('one_to_one', conr) + count = 0 + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(src_id < 100) + assert(trg_id >= 100) + assert(val['nsyn'] == 10) + assert(val['target'] == 'axon') + count += 1 + + assert (count == 5000) + + +def test_one2one_list(): + net = network() + vals = [s.node_id*t.node_id for s,t in itertools.product(net.nodes(ei='i'), net.nodes(ei='e'))] + conr = connector.create(vals) + itr = iterator.create('one_to_one', conr) + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(src_id*trg_id == val) + + +def test_one2all_list(): + net = network() + vals = [v.node_id for v in net.nodes(ei='e')] + conr = connector.create(vals) + itr = iterator.create('one_to_all', conr) + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(trg_id == val) + + +def test_all2one_list(): + net = network() + vals = [v.node_id for v in net.nodes(ei='i')] + conr = connector.create(vals) + itr = iterator.create('all_to_one', conr) + for v in itr(net.nodes(ei='i'), net.nodes(ei='e'), conr): + src_id, trg_id, val = v + assert(src_id == val) diff --git a/bmtk-vb/bmtk/tests/builder/test_node_pool.py b/bmtk-vb/bmtk/tests/builder/test_node_pool.py new file mode 100644 index 0000000..65cd6b1 --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_node_pool.py @@ -0,0 +1,81 @@ +import pytest + +from bmtk.builder import NetworkBuilder + + +def test_single_node(): + net = NetworkBuilder('NET1') + net.add_nodes(prop1='prop1', prop2='prop2', param1=['param1']) + nodes = list(net.nodes()) + assert(len(nodes) == 1) + assert(nodes[0]['param1'] == 'param1') + assert(nodes[0]['prop1'] == 'prop1') + assert(nodes[0]['prop2'] == 'prop2') + + +def test_node_set(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, prop1='prop1', param1=range(100)) + node_pool = net.nodes() + assert(node_pool.filter_str == '*') + + nodes = list(node_pool) + assert(len(nodes) == 100) + assert(nodes[0]['prop1'] == 'prop1') + assert(nodes[0]['param1'] == 0) + assert(nodes[99]['prop1'] == 'prop1') + assert(nodes[99]['param1'] == 99) + assert(nodes[0]['node_type_id'] == nodes[99]['node_type_id']) + assert(nodes[0]['node_id'] != nodes[99]['node_id']) + + +def test_node_sets(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, prop_n='prop1', pool1='p1', sp='sp', param1=range(100)) + net.add_nodes(N=100, prop_n='prop2', pool2='p2', sp='sp', param1=range(100)) + net.add_nodes(N=100, prop_n='prop3', pool3='p3', sp='sp', param1=range(100)) + node_pool_1 = net.nodes(prop_n='prop1') + assert(len(node_pool_1) == 100) + assert(node_pool_1.filter_str == "prop_n=='prop1'") + for n in node_pool_1: + assert('pool1' in n and n['prop_n'] == 'prop1') + + node_pool_2 = net.nodes(sp='sp') + assert(node_pool_2.filter_str == "sp=='sp'") + assert(len(node_pool_2) == 300) + for n in node_pool_2: + assert(n['sp'] == 'sp') + + node_pool_3 = net.nodes(param1=10) + assert(len(node_pool_3) == 3) + assert(node_pool_3.filter_str == "param1=='10'") + nodes = list(node_pool_3) + assert(nodes[0]['node_id'] == 10) + assert(nodes[1]['node_id'] == 110) + assert(nodes[2]['node_id'] == 210) + assert(nodes[0]['node_type_id'] != nodes[1]['node_type_id'] != nodes[2]['node_type_id']) + + +def test_multi_search(): + net = NetworkBuilder('NET1') + net.add_nodes(N=10, prop_n='prop1', sp='sp1', param1=range(0, 10)) + net.add_nodes(N=10, prop_n='prop1', sp='sp2', param1=range(5, 15)) + net.add_nodes(N=20, prop_n='prop2', sp='sp2', param1=range(20)) + node_pool = net.nodes(prop_n='prop1', param1=5) + assert(len(node_pool) == 2) + nodes = list(node_pool) + assert(nodes[0]['node_id'] == 5) + assert(nodes[1]['node_id'] == 10) + + +def test_failed_search(): + net = NetworkBuilder('NET1') + net.add_nodes(N=100, p1='p1', q1=range(100)) + node_pool = net.nodes(p1='p2') + assert(len(node_pool) == 0) + + node_pool = net.nodes(q2=10) + assert(len(node_pool) == 0) + + +test_failed_search() \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/builder/test_node_set.py b/bmtk-vb/bmtk/tests/builder/test_node_set.py new file mode 100644 index 0000000..34b7afa --- /dev/null +++ b/bmtk-vb/bmtk/tests/builder/test_node_set.py @@ -0,0 +1,52 @@ +import pytest +from bmtk.builder.node_set import NodeSet +from bmtk.builder.node import Node +from bmtk.builder.id_generator import IDGenerator + +def test_node_set(): + generator = IDGenerator() + node_set = NodeSet(N=100, + node_params={'p1': range(100), 'p2': range(0, 1000, 100)}, + node_type_properties={'prop1': 'prop1', 'node_type_id': 1}) + print(node_set.N) + print(node_set.node_type_id) + print(node_set.params_keys) + + nodes = node_set.build(generator) + print(len(nodes) == 100) + print(nodes[1]['p1'] == 1) + print(nodes[1]['p2'] == 100) + print(nodes[1]['prop1'] == 'prop1') + print(nodes[1]['node_type_id'] == 1) + + +def test_set_hash(): + node_set1 = NodeSet(N=100, + node_params={'param1': range(100)}, + node_type_properties={'prop1': 'prop1', 'node_type_id': 1}) + node_set2 = NodeSet(N=100, + node_params = {'p1': range(100)}, + node_type_properties={'prop1': 'prop2', 'node_type_id': 2}) + node_set3 = NodeSet(N=10, + node_params={'p1': ['hello']*10}, + node_type_properties={'prop1': 'prop3', 'node_type_id': 3}) + + assert(node_set1.params_hash != node_set2.params_hash) + assert(node_set2.params_hash == node_set3.params_hash) + + +def test_node(): + node_set1 = NodeSet(N=100, + node_params={'param1': range(100)}, + node_type_properties={'prop1': 'prop1', 'node_type_id': 1}) + nodes = node_set1.build(IDGenerator()) + node_1 = nodes[0] + assert(node_1.node_id == 0) + assert(node_1['node_id'] == 0) + assert(node_1.node_type_id == 1) + assert(node_1['node_type_id'] == 1) + assert('prop1' in node_1.node_type_properties) + assert('param1' in node_1.params) + assert('node_id' in node_1.params) + assert('param1' in node_set1.params_keys) + assert(node_1.params_hash == node_set1.params_hash) diff --git a/bmtk-vb/bmtk/tests/simulator/bionet/bionet_virtual_files.py b/bmtk-vb/bmtk/tests/simulator/bionet/bionet_virtual_files.py new file mode 100644 index 0000000..9444329 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/bionet/bionet_virtual_files.py @@ -0,0 +1,172 @@ +import numpy as np + +from bmtk.utils.io import tabular_network as tn + + +class NodeRow(tn.NodeRow): + @property + def with_dynamics_params(self): + return False + + +class EdgeRow(tn.EdgeRow): + @property + def with_dynamics_params(self): + return False + + +class NodesFile(object): + def __init__(self, N): + self._network_name = 'test_bionet' + self._version = None + self._iter_index = 0 + self._nrows = 0 + self._node_types_table = None + + self._N = N + self._rot_delta = 360.0/float(N) + self._node_types_table = { + 101: { + 'pop_name': 'bio_exc', 'node_type_id': 101, 'model_type': 'biophysical', + 'morphology': 'be_morphology.swc', + 'dynamics_params': 'be_dynamics.json', + 'ei': 'e' + }, + 102: { + 'pop_name': 'point_exc', 'node_type_id': 102, 'model_type': 'point_IntFire1', + 'dynamics_params': 'pe_dynamics.json', + 'ei': 'e' + }, + 103: { + 'pop_name': 'bio_inh', 'node_type_id': 103, 'model_type': 'biophysical', + 'morphology': 'bi_morphology.swc', + 'dynamics_params': 'bi_dynamics.json', + 'ei': 'i' + }, + 104: { + 'pop_name': 'point_inh', 'node_type_id': 104, 'model_type': 'point_IntFire1', + 'dynamics_params': 'bi_dynamics.json', + 'ei': 'i' + } + + } + + + @property + def name(self): + """name of network containing these nodes""" + return self._network_name + + @property + def version(self): + return self._version + + @property + def gids(self): + raise NotImplementedError() + + @property + def node_types_table(self): + return self._node_types_table + + def load(self, nodes_file, node_types_file): + raise NotImplementedError() + + def get_node(self, gid, cache=False): + return self[gid] + + def __len__(self): + return self._N + + def __iter__(self): + self._iter_index = 0 + return self + + def next(self): + if self._iter_index >= len(self): + raise StopIteration + + node_row = self[self._iter_index] + self._iter_index += 1 + return node_row + + def __getitem__(self, gid): + node_props = {'positions': np.random.rand(3), 'rotation': self._rot_delta*gid, 'weight': 0.0001*gid} + return NodeRow(gid, node_props, self.__get_node_type_props(gid)) + + + def __get_node_type_props(self, gid): + if gid <= self._N/4: + return self._node_types_table[101] + elif gid <= self._N/2: + return self._node_types_table[102] + elif gid <= self._N*3/4: + return self._node_types_table[103] + else: + return self._node_types_table[104] + + +class EdgesFile(tn.EdgesFile): + def __init__(self, target_nodes, source_nodes): + self._target_nodes = target_nodes + self._source_nodes = source_nodes + self._edge_type_props = [ + { + 'node_type_id': 1, + 'target_query': 'model_type="biophysical"', 'source_query': 'ei="e"', + 'syn_weight': .10, + 'syn_targets': ['dend', 'apical'], + 'dynamics_params': 'biophys_exc.json' + }, + { + 'node_type_id': 2, + 'target_query': 'model_type="point_IntFire1"', 'source_query': 'ei="e"', + 'syn_weight': .20, + 'dynamics_params': 'point_exc.json' + }, + { + 'node_type_id': 3, + 'target_query': 'model_type="biophysical"', 'source_query': 'ei="i"', + 'syn_weight': -.10, + 'syn_targets': ['soma', 'dend'], + 'dynamics_params': 'biophys_inh.json' + }, + { + 'node_type_id': 4, + 'target_query': 'model_type="point_IntFire1"', 'source_query': 'ei="i"', + 'syn_weight': -.20, + 'dynamics_params': 'point_inh.json' + } + ] + + + @property + def source_network(self): + """Name of network containing the source gids""" + return self._source_nodes.name + + @property + def target_network(self): + """Name of network containing the target gids""" + return self._target_nodes.name + + def load(self, edges_file, edge_types_file): + raise NotImplementedError() + + def edges_itr(self, target_gid): + trg_node = self._target_nodes[target_gid] + for src_node in self._source_nodes: + edge_props = {'syn_weight': trg_node['weight']} + #edge_type_props = {'edge_type_id': 1} + yield EdgeRow(trg_node.gid, src_node.gid, edge_props, self.__get_edge_type_prop(src_node, trg_node)) + + #def __init__(self, trg_gid, src_gid, edge_props={}, edge_type_props={}): + #raise NotImplementedError() + + def __len__(self): + return len(self._source_nodes)*len(self._target_nodes) + + def __get_edge_type_prop(self, source_node, target_node): + indx = 0 if source_node['ei'] == 'e' else 2 + indx += 0 if target_node['model_type'] == 'biophysical' else 1 + return self._edge_type_props[indx] diff --git a/bmtk-vb/bmtk/tests/simulator/bionet/set_cell_params.py b/bmtk-vb/bmtk/tests/simulator/bionet/set_cell_params.py new file mode 100644 index 0000000..2a244b0 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/bionet/set_cell_params.py @@ -0,0 +1,108 @@ +import json +from neuron import h + + +def IntFire1(cell_prop): + """Set parameters for the IntFire1 cell models.""" + params_file = cell_prop['params_file'] + + with open(params_file) as params_file: + params = json.load(params_file) + + hobj = h.IntFire1() + hobj.tau = params['tau'] * 1000.0 # Convert from seconds to ms. + hobj.refrac = params['refrac'] * 1000.0 # Convert from seconds to ms. + + return hobj + + +def Biophys1(cell_prop): + """ + Set parameters for cells from the Allen Cell Types database + Prior to setting parameters will replace the axon with the stub + """ + morphology_file_name = str(cell_prop['morphology']) + params_file_name = str(cell_prop['params_file']) + + hobj = h.Biophys1(morphology_file_name) + fix_axon(hobj) + set_params_peri(hobj, params_file_name) + + return hobj + + +def set_params_peri(hobj, params_file_name): + """Set biophysical parameters for the cell + + Parameters + ---------- + hobj: instance of a Biophysical template + NEURON's cell object + params_file_name: string + name of json file containing biophysical parameters for cell's model which determine spiking behavior + """ + + with open(params_file_name) as biophys_params_file: + biophys_params = json.load(biophys_params_file) + + passive = biophys_params['passive'][0] + conditions = biophys_params['conditions'][0] + genome = biophys_params['genome'] + + # Set passive properties + cm_dict = dict([(c['section'], c['cm']) for c in passive['cm']]) + for sec in hobj.all: + sec.Ra = passive['ra'] + sec.cm = cm_dict[sec.name().split(".")[1][:4]] + sec.insert('pas') + + for seg in sec: + seg.pas.e = passive["e_pas"] + + # Insert channels and set parameters + + for p in genome: + sections = [s for s in hobj.all if s.name().split(".")[1][:4] == p["section"]] + + for sec in sections: + if p["mechanism"] != "": + sec.insert(p["mechanism"]) + setattr(sec, p["name"], p["value"]) + + # Set reversal potentials + for erev in conditions['erev']: + sections = [s for s in hobj.all if s.name().split(".")[1][:4] == erev["section"]] + for sec in sections: + sec.ena = erev["ena"] + sec.ek = erev["ek"] + + +def fix_axon(hobj): + ''' + Replace reconstructed axon with a stub + + Parameters + ---------- + hobj: instance of a Biophysical template + NEURON's cell object + ''' + + for sec in hobj.axon: + h.delete_section(sec=sec) + + h.execute('create axon[2]', hobj) + + for sec in hobj.axon: + sec.L = 30 + sec.diam = 1 + hobj.axonal.append(sec=sec) + hobj.all.append(sec=sec) # need to remove this comment + + hobj.axon[0].connect(hobj.soma[0], 0.5, 0) + hobj.axon[1].connect(hobj.axon[0], 1, 0) + + h.define_shape() + + + + diff --git a/bmtk-vb/bmtk/tests/simulator/bionet/set_syn_params.py b/bmtk-vb/bmtk/tests/simulator/bionet/set_syn_params.py new file mode 100644 index 0000000..b2b4732 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/bionet/set_syn_params.py @@ -0,0 +1,31 @@ +from neuron import h + + +def exp2syn(syn_params, xs, secs): + ''' + Create a list of exp2syn synapses + + Parameters + ---------- + syn_params: dict + parameters of a synapse + xs: float + normalized distance along the section + + secs: hoc object + target section + + Returns + ------- + syns: synapse objects + + ''' + syns = [] + + for x, sec in zip(xs, secs): + syn = h.Exp2Syn(x, sec=sec) + syn.e = syn_params['erev'] + syn.tau1 = syn_params['tau1'] + syn.tau2 = syn_params['tau2'] + syns.append(syn) + return syns \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/simulator/bionet/set_weights.py b/bmtk-vb/bmtk/tests/simulator/bionet/set_weights.py new file mode 100644 index 0000000..cdaa454 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/bionet/set_weights.py @@ -0,0 +1,19 @@ +import math + + +def gaussianLL(tar_prop,src_prop,con_prop): + src_tuning = src_prop['tuning_angle'] + tar_tuning = tar_prop['tuning_angle'] + + w0 = con_prop["weight_max"] + sigma = con_prop["weight_sigma"] + + delta_tuning = abs(abs(abs(180.0 - abs(float(tar_tuning) - float(src_tuning)) % 360.0) - 90.0) - 90.0) + weight = w0*math.exp(-(delta_tuning / sigma) ** 2) + + return weight + + +def wmax(tar_prop,src_prop,con_prop): + w0 = con_prop["weight_max"] + return w0 diff --git a/bmtk-vb/bmtk/tests/simulator/bionet/test_biograph.py b/bmtk-vb/bmtk/tests/simulator/bionet/test_biograph.py new file mode 100644 index 0000000..68ee18f --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/bionet/test_biograph.py @@ -0,0 +1,70 @@ +import pytest +import os +import json +import tempfile + +import bionet_virtual_files as bvf +from bmtk.simulator import bionet + + +@pytest.mark.skip() +def test_add_nodes(): + nodes = bvf.NodesFile(N=100) + + net = bionet.BioNetwork() + net.add_component('morphologies_dir', '.') + net.add_component('biophysical_neuron_models_dir', '.') + net.add_component('point_neuron_models_dir', '.') + net.add_nodes(nodes) + + assert(net.networks == [nodes.name]) + assert(net.get_internal_nodes() == net.get_nodes(nodes.name)) + for bionode in net.get_internal_nodes(): + node_id = bionode.node_id + orig_node = nodes[node_id] + assert(node_id == orig_node.gid) + assert(len(bionode.positions) == 3) + assert(bionode['ei'] == orig_node['ei']) + assert(bionode['model_type'] == orig_node['model_type']) + assert(bionode['rotation'] == orig_node['rotation']) + assert(os.path.basename(bionode.model_params) == orig_node['dynamics_params']) + + +@pytest.mark.skip() +def test_add_edges(): + nodes = bvf.NodesFile(N=100) + edges = bvf.EdgesFile(nodes, nodes) + + net = bionet.BioNetwork() + net.add_component('morphologies_dir', '.') + net.add_component('biophysical_neuron_models_dir', '.') + net.add_component('point_neuron_models_dir', '.') + net.add_component('synaptic_models_dir', '.') + + with open('biophys_exc.json', 'w') as fp: + json.dump({}, fp) + + with open('biophys_inh.json', 'w') as fp: + json.dump({}, fp) + + with open('point_exc.json', 'w') as fp: + json.dump({}, fp) + + with open('point_inh.json', 'w') as fp: + json.dump({}, fp) + + net.add_nodes(nodes) + net.add_edges(edges) + + count = 0 + for trg_node in net.get_internal_nodes(): + #print bionode.node_id + for e in net.edges_iterator(trg_node.node_id, nodes.name): + _, src_node, edge = e + assert(edge['syn_weight'] == trg_node['weight']) + count += 1 + assert(count == 10000) + + +if __name__ == '__main__': + test_add_nodes() \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/simulator/bionet/test_nrn.py b/bmtk-vb/bmtk/tests/simulator/bionet/test_nrn.py new file mode 100644 index 0000000..a65c333 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/bionet/test_nrn.py @@ -0,0 +1,148 @@ +import pytest +from bmtk.simulator.bionet.pyfunction_cache import * + + +def test_weight(): + def wmax(v1, v2): + return max(v1, v2) + + def wmin(v1, v2): + return min(v1, v2) + + add_weight_function(wmax) + add_weight_function(wmin, 'minimum') + + assert('wmax' in py_modules.synaptic_weights) + assert('minimum' in py_modules.synaptic_weights) + assert('wmin' not in py_modules.synaptic_weights) + wmax_fnc = py_modules.synaptic_weight('wmax') + assert(wmax_fnc(1, 2) == 2) + + wmin_fnc = py_modules.synaptic_weight('minimum') + assert(wmin_fnc(1, 2) == 1) + py_modules.clear() + + +def test_weight_decorator(): + @synaptic_weight + def wmax(v1, v2): + return max(v1, v2) + + @synaptic_weight(name='minimum') + def wmin(v1, v2): + return min(v1, v2) + + assert('wmax' in py_modules.synaptic_weights) + assert('minimum' in py_modules.synaptic_weights) + assert('wmin' not in py_modules.synaptic_weights) + wmax_fnc = py_modules.synaptic_weight('wmax') + assert(wmax_fnc(1, 2) == 2) + + wmin_fnc = py_modules.synaptic_weight('minimum') + assert(wmin_fnc(1, 2) == 1) + py_modules.clear() + + +def test_synapse_model(): + def syn1(): + return 'Syn1' + + def syn2(p1, p2): + return p1, p2 + + add_synapse_model(syn1) + add_synapse_model(syn2, 'synapse_2') + + assert('syn1' in py_modules.synapse_models) + assert('synapse_2' in py_modules.synapse_models) + assert('syn2' not in py_modules.synapse_models) + + syn_fnc = py_modules.synapse_model('syn1') + assert(syn_fnc() == 'Syn1') + + syn_fnc = py_modules.synapse_model('synapse_2') + assert(syn_fnc(1, 2) == (1, 2)) + py_modules.clear() + + +def test_synapse_model_decorator(): + @synapse_model + def syn1(): + return 'Syn1' + + @synapse_model(name='synapse_2') + def syn2(p1, p2): + return p1, p2 + + assert('syn1' in py_modules.synapse_models) + assert('synapse_2' in py_modules.synapse_models) + assert('syn2' not in py_modules.synapse_models) + + syn_fnc = py_modules.synapse_model('syn1') + assert(syn_fnc() == 'Syn1') + + syn_fnc = py_modules.synapse_model('synapse_2') + assert(syn_fnc(1, 2) == (1, 2)) + py_modules.clear() + + +@pytest.mark.skip() +def test_cell_model(): + def hoc1(): + return "hoc" + + def hoc2(p1): + return p1 + + add_cell_model(hoc1) + add_cell_model(hoc2, name='hoc_function') + + assert('hoc1' in py_modules.cell_models) + assert('hoc_function' in py_modules.cell_models) + assert('hoc2' not in py_modules.cell_models) + + hoc_fnc = py_modules.cell_model('hoc1') + assert(hoc_fnc() == 'hoc') + + hoc_fnc = py_modules.cell_model('hoc_function') + assert(hoc_fnc(1.0) == 1.0) + + +@pytest.mark.skip() +def test_cell_model_decorator(): + @cell_model + def hoc1(): + return "hoc" + + @cell_model(name='hoc_function') + def hoc2(p1): + return p1 + + assert('hoc1' in py_modules.cell_models) + assert('hoc_function' in py_modules.cell_models) + assert('hoc2' not in py_modules.cell_models) + + hoc_fnc = py_modules.cell_model('hoc1') + assert(hoc_fnc() == 'hoc') + + hoc_fnc = py_modules.cell_model('hoc_function') + assert(hoc_fnc(1.0) == 1.0) + + +@pytest.mark.skip() +def test_load_py_modules(): + import set_weights + import set_syn_params + import set_cell_params + + load_py_modules(cell_models=set_cell_params, syn_models=set_syn_params, syn_weights=set_weights) + assert(all(n in py_modules.cell_models for n in ['Biophys1', 'IntFire1'])) + assert(isinstance(py_modules.cell_model('Biophys1'), types.FunctionType)) + assert (isinstance(py_modules.cell_model('IntFire1'), types.FunctionType)) + + assert (all(n in py_modules.synapse_models for n in ['exp2syn'])) + assert (isinstance(py_modules.synapse_model('exp2syn'), types.FunctionType)) + + assert (all(n in py_modules.synaptic_weights for n in ['wmax', 'gaussianLL'])) + assert (isinstance(py_modules.synaptic_weight('wmax'), types.FunctionType)) + assert (isinstance(py_modules.synaptic_weight('gaussianLL'), types.FunctionType)) diff --git a/bmtk-vb/bmtk/tests/simulator/pointnet/pointnet_virtual_files.py b/bmtk-vb/bmtk/tests/simulator/pointnet/pointnet_virtual_files.py new file mode 100644 index 0000000..46e3c26 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/pointnet/pointnet_virtual_files.py @@ -0,0 +1,158 @@ +import numpy as np + +from bmtk.utils.io import tabular_network as tn + + +class NodeRow(tn.NodeRow): + @property + def with_dynamics_params(self): + return False + + +class NodesFile(tn.NodesFile): + def __init__(self, N): + self._network_name = 'test_bionet' + self._version = None + self._iter_index = 0 + self._nrows = 0 + self._node_types_table = None + + self._N = N + self._rot_delta = 360.0/float(N) + self._node_types_table = { + 101: { + 'pop_name': 'Rorb', 'node_type_id': 101, 'model_type': 'iaf_psc_alpha', + 'dynamics_params': 'iaf_dynamics.json', + 'ei': 'e' + }, + + 102: { + 'pop_name': 'PV1', 'node_type_id': 102, 'model_type': 'izhikevich', + 'dynamics_params': 'iz_dynamics.json', + 'ei': 'i' + } + } + + @property + def name(self): + """name of network containing these nodes""" + return self._network_name + + @property + def version(self): + return self._version + + @property + def gids(self): + raise NotImplementedError() + + @property + def node_types_table(self): + return self._node_types_table + + def load(self, nodes_file, node_types_file): + raise NotImplementedError() + + def get_node(self, gid, cache=False): + return self[gid] + + def __len__(self): + return self._N + + def __iter__(self): + self._iter_index = 0 + return self + + def next(self): + if self._iter_index >= len(self): + raise StopIteration + + node_row = self[self._iter_index] + self._iter_index += 1 + return node_row + + def __getitem__(self, gid): + node_props = {'positions': np.random.rand(3), 'rotation': self._rot_delta*gid, 'weight': 0.0001*gid} + return NodeRow(gid, node_props, self.__get_node_type_props(gid)) + + + def __get_node_type_props(self, gid): + if gid <= self._N/2: + return self._node_types_table[101] + else: + return self._node_types_table[102] + + +class EdgeRow(tn.EdgeRow): + @property + def with_dynamics_params(self): + return False + + +class EdgesFile(tn.EdgesFile): + def __init__(self, target_nodes, source_nodes): + self._target_nodes = target_nodes + self._source_nodes = source_nodes + self._edge_type_props = [ + { + 'node_type_id': 1, + 'target_query': 'model_type="iaf_psc_alpha"', 'source_query': 'ei="e"', + 'syn_weight': .10, + 'delay': 2.0, + 'dynamics_params': 'iaf_exc.json' + }, + { + 'node_type_id': 2, + 'target_query': 'model_type="iaf_psc_alpha"', 'source_query': 'ei="i"', + 'syn_weight': -.10, + 'delay': 2.0, + 'dynamics_params': 'iaf_inh.json' + }, + { + 'node_type_id': 3, + 'target_query': 'model_type="izhikevich"', 'source_query': 'ei="e"', + 'syn_weight': .20, + 'delay': 2.0, + 'dynamics_params': 'izh_exc.json' + }, + { + 'node_type_id': 4, + 'target_query': 'model_type="izhikevich"', 'source_query': 'ei="i"', + 'syn_weight': -.20, + 'delay': 2.0, + 'dynamics_params': 'izh_inh.json' + } + ] + + + + @property + def source_network(self): + """Name of network containing the source gids""" + return self._source_nodes.name + + @property + def target_network(self): + """Name of network containing the target gids""" + return self._target_nodes.name + + def load(self, edges_file, edge_types_file): + raise NotImplementedError() + + def edges_itr(self, target_gid): + trg_node = self._target_nodes[target_gid] + for src_node in self._source_nodes: + edge_props = {'syn_weight': trg_node['weight']} + #edge_type_props = {'edge_type_id': 1} + yield EdgeRow(trg_node.gid, src_node.gid, edge_props, self.__get_edge_type_prop(src_node, trg_node)) + + #def __init__(self, trg_gid, src_gid, edge_props={}, edge_type_props={}): + #raise NotImplementedError() + + def __len__(self): + return len(self._source_nodes)*len(self._target_nodes) + + def __get_edge_type_prop(self, source_node, target_node): + indx = 0 if source_node['model_type'] == 'iaf_psc_alpha' else 2 + indx += 0 if target_node['ei'] == 'e' else 1 + return self._edge_type_props[indx] diff --git a/bmtk-vb/bmtk/tests/simulator/pointnet/test_pointgraph.py b/bmtk-vb/bmtk/tests/simulator/pointnet/test_pointgraph.py new file mode 100644 index 0000000..a1c18c9 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/pointnet/test_pointgraph.py @@ -0,0 +1,77 @@ +import pytest +import os +import json +import tempfile + +from pointnet_virtual_files import NodesFile, EdgesFile +from bmtk.simulator import pointnet + + +@pytest.mark.skip() +def test_add_nodes(): + nodes = NodesFile(N=100) + + net = pointnet.PointNetwork() + if not os.path.exists('tmp/'): + os.mkdir('tmp/') + net.add_component('models_dir', '.') + with open('iaf_dynamics.json', 'w') as fp: + json.dump({}, fp) + + with open('iz_dynamics.json', 'w') as fp: + json.dump({}, fp) + + net.add_nodes(nodes) + assert(net.networks == [nodes.name]) + assert(net.get_internal_nodes() == net.get_nodes(nodes.name)) + count = 0 + for pointnode in net.get_internal_nodes(): + node_id = pointnode.node_id + orig_node = nodes[node_id] + assert(node_id == orig_node.gid) + assert(pointnode['ei'] == orig_node['ei']) + assert(pointnode['model_type'] == orig_node['model_type']) + assert(pointnode['rotation'] == orig_node['rotation']) + assert(pointnode.model_params == {}) + count += 1 + assert(count == 100) + + +@pytest.mark.skip() +def test_add_edges(): + nodes = NodesFile(N=100) + edges = EdgesFile(nodes, nodes) + + net = pointnet.PointNetwork() + net.add_component('models_dir', '.') + net.add_component('synaptic_models_dir', '.') + + with open('iaf_dynamics.json', 'w') as fp: + json.dump({}, fp) + + with open('iz_dynamics.json', 'w') as fp: + json.dump({}, fp) + + with open('iaf_exc.json', 'w') as fp: + json.dump({}, fp) + + with open('iaf_inh.json', 'w') as fp: + json.dump({}, fp) + + with open('izh_exc.json', 'w') as fp: + json.dump({}, fp) + + with open('izh_inh.json', 'w') as fp: + json.dump({}, fp) + + net.add_nodes(nodes) + net.add_edges(edges) + + count = 0 + for trg_node in net.get_internal_nodes(): + for e in net.edges_iterator(trg_node.node_id, nodes.name): + _, src_node, edge = e + assert(edge['syn_weight'] == trg_node['weight']) + count += 1 + assert(count == 10000) + diff --git a/bmtk-vb/bmtk/tests/simulator/popnet/popnet_virtual_files.py b/bmtk-vb/bmtk/tests/simulator/popnet/popnet_virtual_files.py new file mode 100644 index 0000000..eb95216 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/popnet/popnet_virtual_files.py @@ -0,0 +1,159 @@ +import numpy as np + +from bmtk.utils.io import tabular_network as tn + + +class NodeRow(tn.NodeRow): + @property + def with_dynamics_params(self): + return False + + +class NodesFile(tn.NodesFile): + def __init__(self, N): + self._network_name = 'test_bionet' + self._version = None + self._iter_index = 0 + self._nrows = 0 + self._node_types_table = None + + self._N = N + self._rot_delta = 360.0/float(N) + self._node_types_table = { + 101: { + 'pop_name': 'internal_exc', 'node_type_id': 101, 'model_type': 'internal', + 'dynamics_params': 'exc_dynamics.json', + 'ei': 'e' + }, + + 102: { + 'pop_name': 'internal_inh', 'node_type_id': 102, 'model_type': 'internal', + 'dynamics_params': 'inh_dynamics.json', + 'ei': 'i' + }, + 103: { + 'pop_name': 'external_exc', 'node_type_id': 103, 'model_type': 'external', + 'dynamics_params': 'external_dynamics.json', + 'ei': 'external_exc.json' + } + } + + @property + def name(self): + """name of network containing these nodes""" + return self._network_name + + @property + def version(self): + return self._version + + @property + def gids(self): + raise NotImplementedError() + + @property + def node_types_table(self): + return self._node_types_table + + def load(self, nodes_file, node_types_file): + raise NotImplementedError() + + def get_node(self, gid, cache=False): + return self[gid] + + def __len__(self): + return self._N + + def __iter__(self): + self._iter_index = 0 + return self + + def next(self): + if self._iter_index >= len(self): + raise StopIteration + + node_row = self[self._iter_index] + self._iter_index += 1 + return node_row + + def __getitem__(self, gid): + node_props = {'positions': np.random.rand(3), 'rotation': self._rot_delta*gid, 'weight': 0.0001*gid} + return NodeRow(gid, node_props, self.__get_node_type_props(gid)) + + def __get_node_type_props(self, gid): + if gid <= self._N/3: + return self._node_types_table[101] + elif gid <= self._N*2/3: + return self._node_types_table[102] + else: + return self._node_types_table[103] + + +class EdgeRow(tn.EdgeRow): + @property + def with_dynamics_params(self): + return False + + +class EdgesFile(tn.EdgesFile): + def __init__(self, target_nodes, source_nodes): + self._target_nodes = target_nodes + self._source_nodes = source_nodes + self._edge_type_props = [ + { + 'node_type_id': 1, + 'target_query': 'model_type="iaf_psc_alpha"', 'source_query': 'ei="e"', + 'syn_weight': .10, + 'delay': 2.0, + 'dynamics_params': 'iaf_exc.json' + }, + { + 'node_type_id': 2, + 'target_query': 'model_type="iaf_psc_alpha"', 'source_query': 'ei="i"', + 'syn_weight': -.10, + 'delay': 2.0, + 'dynamics_params': 'iaf_inh.json' + }, + { + 'node_type_id': 3, + 'target_query': 'model_type="izhikevich"', 'source_query': 'ei="e"', + 'syn_weight': .20, + 'delay': 2.0, + 'dynamics_params': 'izh_exc.json' + }, + { + 'node_type_id': 4, + 'target_query': 'model_type="izhikevich"', 'source_query': 'ei="i"', + 'syn_weight': -.20, + 'delay': 2.0, + 'dynamics_params': 'izh_inh.json' + } + ] + + @property + def source_network(self): + """Name of network containing the source gids""" + return self._source_nodes.name + + @property + def target_network(self): + """Name of network containing the target gids""" + return self._target_nodes.name + + def load(self, edges_file, edge_types_file): + raise NotImplementedError() + + def edges_itr(self, target_gid): + trg_node = self._target_nodes[target_gid] + for src_node in self._source_nodes: + edge_props = {'syn_weight': trg_node['weight']} + yield EdgeRow(trg_node.gid, src_node.gid, edge_props, self.__get_edge_type_prop(src_node, trg_node)) + + + def __len__(self): + return len(self._source_nodes)*len(self._target_nodes) + + def __get_edge_type_prop(self, source_node, target_node): + indx = 0 if source_node['model_type'] == 'iaf_psc_alpha' else 2 + indx += 0 if target_node['ei'] == 'e' else 1 + return self._edge_type_props[indx] diff --git a/bmtk-vb/bmtk/tests/simulator/popnet/test_popgraph.py b/bmtk-vb/bmtk/tests/simulator/popnet/test_popgraph.py new file mode 100644 index 0000000..318422c --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/popnet/test_popgraph.py @@ -0,0 +1,39 @@ +import pytest +import os +import json + +import popnet_virtual_files as pvf +from bmtk.simulator import popnet + + +@pytest.mark.skip() +def test_add_nodes(): + nodes = pvf.NodesFile(N=100) + + net = popnet.PopNetwork() + net.add_component('models_dir', '.') + with open('exc_dynamics.json', 'w') as fp: + json.dump({'tau_m': 0.1}, fp) + + with open('inh_dynamics.json', 'w') as fp: + json.dump({'tau_m': 0.2}, fp) + + net.add_nodes(nodes) + assert(net.networks == [nodes.name]) + assert(len(net.get_internal_nodes()) == 2) + assert(len(net.get_populations(nodes.name)) == 3) + assert(net.get_populations(nodes.name)) + + pop_e = net.get_population(nodes.name, 101) + assert (pop_e['ei'] == 'e') + assert (pop_e.is_internal == True) + assert (pop_e.pop_id == 101) + assert (pop_e.tau_m == 0.1) + + pop_i = net.get_population(nodes.name, 102) + assert (pop_i['ei'] == 'i') + assert (pop_i.is_internal == True) + assert (pop_i.tau_m == 0.2) + + +#test_add_nodes() \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/simulator/utils/files/circuit_config.json b/bmtk-vb/bmtk/tests/simulator/utils/files/circuit_config.json new file mode 100755 index 0000000..3da4ff1 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/utils/files/circuit_config.json @@ -0,0 +1,43 @@ +{ + "target_simulator":"NEURON", + + "components": { + "morphologies": "$COMPONENT_DIR/morphologies", + "synaptic_models": "$COMPONENT_DIR/synapse_dynamics", + "mechanisms":"$COMPONENT_DIR/mechanisms", + "biophysical_neuron_models": "$COMPONENT_DIR/biophysical_neuron_dynamics", + "point_neuron_models": "$COMPONENT_DIR/point_neuron_dynamics", + "templates": "$COMPONENT_DIR/hoc_templates" + + }, + + "networks": { + "node_files": [ + { + "nodes": "$NETWORK_DIR/V1/v1_nodes.h5", + "node_types": "$NETWORK_DIR/V1/v1_node_types.csv" + }, + { + "nodes": "$NETWORK_DIR/LGN/lgn_nodes.h5", + "node_types": "$NETWORK_DIR/LGN/lgn_node_types.csv" + } + ], + + "edge_files": [ + { + "edges": "$NETWORK_DIR/V1/v1_edges.h5", + "edge_types": "$NETWORK_DIR/V1/v1_edge_types.csv" + }, + { + "edges": "$NETWORK_DIR/LGN/lgn_v1_edges.h5", + "edge_types": "$NETWORK_DIR/LGN/lgn_v1_edge_types.csv" + } + ] + }, + + "manifest": { + "$BASE_DIR": "${configdir}", + "$NETWORK_DIR": "$BASE_DIR/networks", + "$COMPONENT_DIR": "$BASE_DIR/components" + } +} diff --git a/bmtk-vb/bmtk/tests/simulator/utils/files/config.json b/bmtk-vb/bmtk/tests/simulator/utils/files/config.json new file mode 100644 index 0000000..27539c1 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/utils/files/config.json @@ -0,0 +1,9 @@ +{ + "manifest": { + "$BASE_DIR": "${configdir}" + }, + + "simulation": "${BASE_DIR}/simulator_config.json", + "network": "$BASE_DIR/circuit_config.json" + +} \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/simulator/utils/files/simulator_config.json b/bmtk-vb/bmtk/tests/simulator/utils/files/simulator_config.json new file mode 100644 index 0000000..1606d3d --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/utils/files/simulator_config.json @@ -0,0 +1,50 @@ +{ + "run": { + "tstop": 3000.0, + "dt": 0.025, + "dL": 20, + "overwrite_output_dir": true, + "spike_threshold": -15, + "save_state":false, + "start_from_state": false, + "nsteps_block":5000, + "save_cell_vars": ["v", "cai"], + "calc_ecp": false, + "connect_internal": true, + "connect_external": {"lgn": true, "tw": true} + }, + + "conditions": { + "celsius": 34.0, + "v_init": -80 + }, + + "groups": { + "save_vars": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + }, + + "input": [ + { + "type": "external_spikes", + "format": "nwb", + "file": "lgn_spike_trains.txt", + "network": "lgn", + "trial": "trial_0" + } + ], + + "output": { + "log": "$OUTPUT_DIR/log.txt", + "spikes_ascii": "$OUTPUT_DIR/spikes.txt", + "spikes_h5": "$OUTPUT_DIR/spikes.h5", + "cell_vars_dir": "$OUTPUT_DIR/cellvars", + "extra_cell_vars": "$OUTPUT_DIR/extra_cell_vars.h5", + "ecp_file": "$OUTPUT_DIR/ecp.h5", + "state_dir": "$OUTPUT_DIR/state", + "output_dir": "$OUTPUT_DIR" + }, + + "manifest": { + "$OUTPUT_DIR": "./output" + } +} \ No newline at end of file diff --git a/bmtk-vb/bmtk/tests/simulator/utils/test_config.py b/bmtk-vb/bmtk/tests/simulator/utils/test_config.py new file mode 100644 index 0000000..414666d --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/utils/test_config.py @@ -0,0 +1,148 @@ +import os +import pytest + +import bmtk.simulator.utils.config as cfg + + +def config_path(rel_path): + c_path = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(c_path, rel_path) + + +def test_load_parent_config(): + """Test a parent config file can pull in children configs""" + cfg_full_path = config_path('files/config.json') + config = cfg.from_json(cfg_full_path) + assert(config['config_path'] == cfg_full_path) + assert('components' in config) + assert('networks' in config) + assert('run' in config) + + +def test_load_network_config(): + cfg_full_path = config_path('files/circuit_config.json') + config = cfg.from_json(cfg_full_path) + manifest = config['manifest'] + assert(config['config_path'] == cfg_full_path) + assert(config['components']['morphologies'] == os.path.join(manifest['$COMPONENT_DIR'], 'morphologies')) + assert(config['networks']['node_files'][0]['nodes'] == os.path.join(manifest['$NETWORK_DIR'], 'V1/v1_nodes.h5')) + + +def test_load_simulator_config(): + cfg_full_path = config_path('files/simulator_config.json') + config = cfg.from_json(cfg_full_path) + manifest = config['manifest'] + assert('run' in config) + assert(config['output']['log'] == os.path.join(manifest['$OUTPUT_DIR'], 'log.txt')) + + +def test_build_manifest1(): + """Test simple manifest""" + config_file = {'manifest': { + '$BASE_DIR': '/base', + '$TMP_DIR': '$BASE_DIR/tmp', + '$SHARE_DIR': '${TMP_DIR}_1/share' + }} + + manifest = cfg.__build_manifest(config_file) + assert(manifest['$BASE_DIR'] == '/base') + assert(manifest['$TMP_DIR'] == '/base/tmp') + assert(manifest['$SHARE_DIR'] == '/base/tmp_1/share') + + +def test_build_manifest2(): + config_file = {'manifest': { + '$DIR_DATA': 'data', + '$DIR_MAT': 'mat', + '$APPS': '/${DIR_DATA}/$DIR_MAT/apps' + }} + + manifest = cfg.__build_manifest(config_file) + assert(manifest['$APPS'] == '/data/mat/apps') + + +def test_build_manifest_fail1(): + """Test exception occurs when variable is missing""" + config_file = {'manifest': { + '$BASE': '/base', + '$TMP': '$VAR/Smat', + }} + with pytest.raises(Exception): + cfg.__build_manifest(config_file) + + +def test_build_manifest_fail2(): + """Test recursive definition""" + config_file = {'manifest': { + '$BASE': '$TMP/share', + '$TMP': '$BASE/share', + }} + with pytest.raises(Exception): + cfg.__build_manifest(config_file) + + +def test_resolve_var_str(): + """Check that a variable can be resolved in a string""" + config_file = { + 'manifest': { + '$BASE': 'path' + }, + 's1': '$BASE/test', + 'i1': 9 + } + conf = cfg.from_dict(config_file) + assert(conf['s1'] == 'path/test') + assert(conf['i1'] == 9) + + +def test_resolve_var_list(): + """Check variables can be resolved in list""" + config_file = { + 'manifest': { + '$p1': 'a', + '$p2': 'b' + }, + 'l1': ['$p1/test', '${p2}/test', 9] + } + conf = cfg.from_dict(config_file) + assert(conf['l1'][0] == 'a/test') + assert(conf['l1'][1] == 'b/test') + assert(conf['l1'][2] == 9) + + +def test_resolve_var_dict(): + """Check variables can be resolved in dictionary""" + config_file = { + 'manifest': { + '$v1': 'a', + '$v2': 'c' + }, + 'd1': { + 'k1': '$v1', + 'k2': 'B', + 'k3': ['${v2}'], + 'k4': 4 + } + } + conf = cfg.from_dict(config_file) + assert(conf['d1']['k1'] == 'a') + assert(conf['d1']['k2'] == 'B') + assert(conf['d1']['k3'] == ['c']) + assert(conf['d1']['k4'] == 4) + + +def test_time_vars(): + config_file = { + 'd1': { + 'k1': 'k1_${date}', + 'k2': 'k2/$time', + 'k3': ['${datetime}'], + 'k4': 4 + } + } + + conf = cfg.from_dict(config_file) + + + +#test_time_vars() diff --git a/bmtk-vb/bmtk/tests/simulator/utils/test_nwb.py b/bmtk-vb/bmtk/tests/simulator/utils/test_nwb.py new file mode 100644 index 0000000..c92f058 --- /dev/null +++ b/bmtk-vb/bmtk/tests/simulator/utils/test_nwb.py @@ -0,0 +1,341 @@ +import pytest +import numpy as np +from bmtk.simulator.utils import nwb +import os +import h5py + + +def test_create_blank_file(): + nwb.create_blank_file() + f = nwb.create_blank_file(close=False) + file_name = f.filename + f.close() + + nwb.create_blank_file(file_name, force=True) + os.remove(file_name) + + +def test_create_blank_file_force(): + temp_file_name = nwb.get_temp_file_name() + nwb.create_blank_file(temp_file_name, force=True) + try: + nwb.create_blank_file(temp_file_name) + except IOError: + exception_caught = True + assert exception_caught + os.remove(temp_file_name) + + +def test_different_scales(): + y_values = 10*np.ones(10) + f = nwb.create_blank_file(close=False) + scale = nwb.DtScale(.1, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_stimulus(f) + + spike_train = nwb.FiringRate.get_stimulus(f, 0) + y_values_new = spike_train.data[:] + np.testing.assert_almost_equal(y_values_new, y_values) + f.close() + + +def test_set_data_file_handle(): + f = nwb.create_blank_file(close=False) + s0 = nwb._set_scale(f, '0', np.arange(3), 'time', 'second', "Scale") + s1 = nwb._set_scale(f, '1', np.arange(4), 'time', 'second', "Scale") + s2 = nwb._set_scale(f, '2', np.arange(5), 'time', 'second', "Scale") + nwb._set_data(f, '1D', np.zeros(3), s0, 'firing_rate', 'hertz') + nwb._set_data(f, '2D', np.zeros((3, 4)), (s0, s1), 'firing_rate', 'hertz') + nwb._set_data(f, '3D', np.zeros((3, 4, 5)), (s0, s1, s2), 'firing_rate', 'hertz') + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_set_data_force(): + f = nwb.create_blank_file(close=False) + s0 = nwb._set_scale(f, '0', np.arange(3), 'time', 'second', "Scale") + nwb._set_data(f, 'test_force', np.zeros(3), s0, 'firing_rate', 'hertz') + nwb._set_data(f, 'test_force', np.zeros(3), s0, 'firing_rate', 'hertz', force=True) + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_get_data(): + s0_tuple = '0', np.arange(3), 'distance', 'pixel', "Scale" + s1_tuple = '1', np.arange(4), 'distance', 'pixel', "Scale" + data, dimension, unit = np.ones((3, 4)), 'brightness', 'intensity' + + f = nwb.create_blank_file(close=False) + s0 = nwb._set_scale(f, *s0_tuple) + s1 = nwb._set_scale(f, *s1_tuple) + scales = (s0, s1) + nwb._set_data(f, 'test', data, scales, dimension, unit) + data_new, scales_new, dimension_new, unit_new, metadata = nwb._get_data(f['test']) + np.testing.assert_almost_equal(data, data_new) + assert len(metadata) == 0 + assert dimension == dimension_new + assert unit == unit_new + for scale_tuple, scale_new in zip((s0_tuple, s1_tuple), scales_new): + np.testing.assert_almost_equal(scale_tuple[1], scale_new[:]) + assert scale_tuple[2] == scale_new.attrs['dimension'] + assert scale_tuple[3] == scale_new.attrs['unit'] + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_metadata(): + f = nwb.create_blank_file(close=False) + s0 = nwb._set_scale(f, '0', np.arange(3), 'time', 'second', "Scale") + nwb._set_data(f, 'test_metadata', np.zeros(3), s0, 'firing_rate', 'hertz', metadata={'name':'foo'}) + _, _, _, _, metadata = nwb._get_data(f['test_metadata']) + assert metadata['name'] == 'foo' + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_add_shared_scale(): + f = nwb.create_blank_file(close=False, force=True) + t_values = np.arange(10) + shared_scale = nwb.Scale(t_values, 'time', 'second') + data_0 = nwb.FiringRate(10*np.ones(10), scale=shared_scale) + data_0.add_to_stimulus(f) + data_1 = nwb.FiringRate(20*np.ones(10), scale=shared_scale) + data_1.add_to_stimulus(f) + + round_trip_0 = nwb.FiringRate.get_stimulus(f, 0) + assert data_0 == round_trip_0 + + round_trip_1 = nwb.FiringRate.get_stimulus(f, 1) + assert data_1 == round_trip_1 + + rt0, rt1 = nwb.FiringRate.get_stimulus(f) + assert data_0 == rt0 + assert data_1 == rt1 + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_firing_rate(): + t_values = np.arange(10) + y_values = 10*np.ones(10) + + f = nwb.create_blank_file(close=False, force=True) + + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_stimulus(f) + data.add_to_acquisition(f) + data.add_to_processing(f, 'step_0') + data.add_to_analysis(f, 'step_0') + + round_trip = nwb.FiringRate.get_stimulus(f, 0) + assert round_trip == data + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_spike_train(): + t_values = np.arange(5)*.1 + y_values = np.array([0, 1, 2, 2, 1]) + + f = nwb.create_blank_file(close=False, force=True) + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.SpikeTrain(y_values, scale=scale) + data.add_to_stimulus(f) + data.add_to_acquisition(f) + data.add_to_processing(f, 'step_0') + data.add_to_analysis(f, 'step_0') + + round_trip = nwb.SpikeTrain.get_stimulus(f, 0) + assert round_trip == data + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_grayscale_movie(): + t_values = np.arange(20)*.1 + row_values = np.arange(5) + col_values = np.arange(10) + data_values = np.empty((20, 5, 10)) + + f = nwb.create_blank_file(close=False, force=True) + t_scale = nwb.Scale(t_values, 'time', 'second') + row_scale = nwb.Scale(row_values, 'distance', 'pixel') + col_scale = nwb.Scale(col_values, 'distance', 'pixel') + + data = nwb.GrayScaleMovie(data_values, scale=(t_scale, row_scale, col_scale), metadata={'foo': 5}) + data.add_to_stimulus(f) + data.add_to_acquisition(f) + data.add_to_processing(f, 'step_0') + data.add_to_analysis(f, 'step_0') + + round_trip = nwb.GrayScaleMovie.get_stimulus(f, 0) + np.testing.assert_almost_equal(round_trip.data[:], data.data[:], 12) + + round_trip = nwb.GrayScaleMovie.get_acquisition(f, 0) + np.testing.assert_almost_equal(round_trip.data[:], data.data[:], 12) + + round_trip = nwb.GrayScaleMovie.get_processing(f, 'step_0', 0) + np.testing.assert_almost_equal(round_trip.data[:], data.data[:], 12) + + round_trip = nwb.GrayScaleMovie.get_analysis(f, 'step_0', 0) + np.testing.assert_almost_equal(round_trip.data[:], data.data[:], 12) + f.close() + + +def test_processing(): + t_values = np.arange(10) + y_values = 10*np.ones(10) + + f = nwb.create_blank_file(close=False) + + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_processing(f, 'step_0') + + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_processing(f, 'step_0') + + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_processing(f, 'step_1') + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_analysis(): + t_values = np.arange(10) + y_values = 10*np.ones(10) + + f = nwb.create_blank_file(close=False) + + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_analysis(f, 'step_0') + + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_analysis(f, 'step_0') + + scale = nwb.Scale(t_values, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + data.add_to_analysis(f, 'step_1') + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_writable(): + y_values = 10*np.ones(10) + scale = nwb.DtScale(.1, 'time', 'second') + data = nwb.FiringRate(y_values, scale=scale) + + f = nwb.create_blank_file(close=True) + try: + data.add_to_stimulus(f) + except TypeError as e: + assert str(e).replace('\'', '') == "NoneType object has no attribute __getitem__" + + f = nwb.create_blank_file(close=False) + f.close() + try: + data.add_to_stimulus(f) + except Exception as e: + assert str(e) == 'File not valid: ' + + +@pytest.mark.skip(reason='Ability to add 0-lenght datasetset has been removed in newer version of h5py') +def test_nullscale(): + y_values = np.array([.1, .5, .51]) + + f = nwb.create_blank_file(force=True) + data = nwb.SpikeTrain(y_values, unit='second') + data.add_to_stimulus(f) + + spike_train = nwb.SpikeTrain.get_stimulus(f) + y_values_new = spike_train.data[:] + np.testing.assert_almost_equal(y_values, y_values_new) + assert isinstance(spike_train.scales[0], nwb.NullScale) + f.close() + + +def test_timeseries(): + y_values = np.array([.1, .2, .1]) + f = nwb.create_blank_file() + scale = nwb.DtScale(.1, 'time', 'second') + nwb.TimeSeries(y_values, scale=scale, dimension='voltage', unit='volt').add_to_acquisition(f) + + data = nwb.TimeSeries.get_acquisition(f) + assert data.scales[0].dt == .1 + assert data.scales[0].unit == 'second' + np.testing.assert_almost_equal(data.data[:], y_values) + assert data.unit == 'volt' + + file_name = f.filename + f.close() + os.remove(file_name) + + +def test_external_link(): + data_original = np.zeros(10) + f = nwb.create_blank_file(force=True) + scale = nwb.Scale(np.zeros(10), 'time', 'second') + nwb.TimeSeries(data_original, scale=scale, dimension='voltage', unit='volt', + metadata={'foo': 1}).add_to_acquisition(f) + temp_file_name = f.filename + f.close() + + f = h5py.File(temp_file_name, 'r') + f2 = nwb.create_blank_file(force=True) + data = nwb.TimeSeries.get_acquisition(f, 0) + data.add_to_acquisition(f2) + f.close() + temp_file_name_2 = f2.filename + f2.close() + + f = h5py.File(temp_file_name_2) + data = nwb.TimeSeries.get_acquisition(f, 0) + np.testing.assert_almost_equal(data.data, data_original) + assert data.data.file.filename == temp_file_name + + f.close() + os.remove(temp_file_name) + os.remove(temp_file_name_2) + + +if __name__ == "__main__": + test_create_blank_file() # pragma: no cover + test_create_blank_file_force() # pragma: no cover + test_set_data_file_handle() # pragma: no cover + test_set_data_force() # pragma: no cover + test_get_data() # pragma: no cover + test_metadata() # pragma: no cover + test_add_shared_scale() # pragma: no cover + test_firing_rate() # pragma: no cover + test_processing() # pragma: no cover + test_analysis() # pragma: no cover + test_spike_train() # pragma: no cover + test_grayscale_movie() # pragma: no cover +# test_get_stimulus() # pragma: no cover + test_different_scales() + test_writable() + #test_nullscale() + test_timeseries() + test_external_link() diff --git a/bmtk-vb/bmtk/utils/__init__.py b/bmtk-vb/bmtk/utils/__init__.py new file mode 100644 index 0000000..1c9c088 --- /dev/null +++ b/bmtk-vb/bmtk/utils/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +import logging + + diff --git a/bmtk-vb/bmtk/utils/__init__.pyc b/bmtk-vb/bmtk/utils/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccd017257a2838eef70828975ba8bb973fda1036 GIT binary patch literal 164 zcmZSn%*$oj>ll;F00m4y+5w1*1%N~f5HT|3Ffc@c8NnJL+06ey;HSX|WR|c4iJbiO z^vt|;4Iq;NK@=MRx%#2SsYS*5Wl4!ei6t3{x%$OL$@)pTCD{<7v?Md9SU)~KGcU6w bK3=b&vV;Su$tE{Hr8FniP7KH{24V&PmQW>? literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/utils/__pycache__/__init__.cpython-36.pyc b/bmtk-vb/bmtk/utils/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90d6bc10b00e79a35e0fa614f93eeebe1ba7a629 GIT binary patch literal 150 zcmXr!<>j*Mb&O$PV_g`k0?S^<7zQ>5hQ}Zd3@`y14nSPY10+%yQW$d>qJU&DgC^5Upa_GXCgUyk zoc#3k%)IoK3`NX9Au#dFKtHrNwWwIXEGe-lu_PlgSHHL@SwAVaBpX7MmSpA>>&M4u d=4F<|$LkeT-r}&y%}*)KNwpIL8d3~09013;BxC>p literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/utils/cell_vars/__init__.py b/bmtk-vb/bmtk/utils/cell_vars/__init__.py new file mode 100644 index 0000000..021345f --- /dev/null +++ b/bmtk-vb/bmtk/utils/cell_vars/__init__.py @@ -0,0 +1,6 @@ +from .var_reader import CellVarsFile + + + + + diff --git a/bmtk-vb/bmtk/utils/cell_vars/__init__.pyc b/bmtk-vb/bmtk/utils/cell_vars/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0eba0b8c5f0d0246012d63d6ec0d68547f49c56c GIT binary patch literal 208 zcmY+8u?hk)3_#PnMGoEkh)#A^L=?f%LD4~k(rXJATa~1D;79ni{(vdE7?Kx4$RqW8 z)J$KivxQ$&+?QN5W;jj(5g-XT2@tM=y{vf=1Va|&A!F@hJQ}1;h<+=JA~%fQphj<; z#~n`~|3NdJ)qy;c>RW>*UrbVjR<$W#zvyy^q%7+-%SdS*ZXs(uc|7xg&*$mxy66|> H6Ds--9Um;d literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/utils/cell_vars/var_reader.py b/bmtk-vb/bmtk/utils/cell_vars/var_reader.py new file mode 100644 index 0000000..21da36d --- /dev/null +++ b/bmtk-vb/bmtk/utils/cell_vars/var_reader.py @@ -0,0 +1,134 @@ +import h5py +import numpy as np + + +class CellVarsFile(object): + VAR_UNKNOWN = 'Unknown' + UNITS_UNKNOWN = 'NA' + + def __init__(self, filename, mode='r', **params): + self._h5_handle = h5py.File(filename, 'r') + self._h5_root = self._h5_handle[params['h5_root']] if 'h5_root' in params else self._h5_handle['/'] + self._var_data = {} + self._var_units = {} + + self._mapping = None + + # Look for variabl and mapping groups + for var_name in self._h5_root.keys(): + hf_grp = self._h5_root[var_name] + + if var_name == 'data': + # According to the sonata format the /data table should be located at the root + var_name = self._h5_root['data'].attrs.get('variable_name', CellVarsFile.VAR_UNKNOWN) + self._var_data[var_name] = self._h5_root['data'] + self._var_units[var_name] = self._find_units(self._h5_root['data']) + + if not isinstance(hf_grp, h5py.Group): + continue + + if var_name == 'mapping': + # Check for /mapping group + self._mapping = hf_grp + else: + # In the bmtk we can support multiple variables in the same file (not sonata compliant but should be) + # where each variable table is separated into its own group //data + if 'data' not in hf_grp: + print('Warning: could not find "data" dataset in {}. Skipping!'.format(var_name)) + else: + self._var_data[var_name] = hf_grp['data'] + self._var_units[var_name] = self._find_units(hf_grp['data']) + + # create map between gids and tables + self._gid2data_table = {} + if self._mapping is None: + raise Exception('could not find /mapping group') + else: + gids_ds = self._mapping['gids'] + index_pointer_ds = self._mapping['index_pointer'] + for indx, gid in enumerate(gids_ds): + self._gid2data_table[gid] = (index_pointer_ds[indx], index_pointer_ds[indx+1]) # slice(index_pointer_ds[indx], index_pointer_ds[indx+1]) + + time_ds = self._mapping['time'] + self._t_start = time_ds[0] + self._t_stop = time_ds[1] + self._dt = time_ds[2] + self._n_steps = int((self._t_stop - self._t_start) / self._dt) + + @property + def variables(self): + return list(self._var_data.keys()) + + @property + def gids(self): + return list(self._gid2data_table.keys()) + + @property + def t_start(self): + return self._t_start + + @property + def t_stop(self): + return self._t_stop + + @property + def dt(self): + return self._dt + + @property + def time_trace(self): + return np.linspace(self.t_start, self.t_stop, num=self._n_steps, endpoint=True) + + @property + def h5(self): + return self._h5_root + + def _find_units(self, data_set): + return data_set.attrs.get('units', CellVarsFile.UNITS_UNKNOWN) + + def units(self, var_name=VAR_UNKNOWN): + return self._var_units[var_name] + + def n_compartments(self, gid): + bounds = self._gid2data_table[gid] + return bounds[1] - bounds[0] + + def compartment_ids(self, gid): + bounds = self._gid2data_table[gid] + return self._mapping['element_id'][bounds[0]:bounds[1]] + + def compartment_positions(self, gid): + bounds = self._gid2data_table[gid] + return self._mapping['element_pos'][bounds[0]:bounds[1]] + + def data(self, gid, var_name=VAR_UNKNOWN,time_window=None, compartments='origin'): + if var_name not in self.variables: + raise Exception('Unknown variable {}'.format(var_name)) + + if time_window is None: + time_slice = slice(0, self._n_steps) + else: + if len(time_window) != 2: + raise Exception('Invalid time_window, expecting tuple [being, end].') + + window_beg = max(int((time_window[0] - self.t_start)/self.dt), 0) + window_end = min(int((time_window[1] - self.t_start)/self.dt), self._n_steps/self.dt) + time_slice = slice(window_beg, window_end) + + multi_compartments = True + if compartments == 'origin' or self.n_compartments(gid) == 1: + # Return the first (and possibly only) compartment for said gid + gid_slice = self._gid2data_table[gid][0] + multi_compartments = False + elif compartments == 'all': + # Return all compartments + gid_slice = slice(self._gid2data_table[gid][0], self._gid2data_table[gid][1]) + else: + # return all compartments with corresponding element id + compartment_list = list(compartments) if isinstance(compartments, (long, int)) else compartments + begin = self._gid2data_table[gid][0] + end = self._gid2data_table[gid][1] + gid_slice = [i for i in range(begin, end) if self._mapping[i] in compartment_list] + + data = np.array(self._var_data[var_name][time_slice, gid_slice]) + return data.T if multi_compartments else data diff --git a/bmtk-vb/bmtk/utils/cell_vars/var_reader.pyc b/bmtk-vb/bmtk/utils/cell_vars/var_reader.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a307d7374b7ebb23a34906b2cb9f185954dda91f GIT binary patch literal 5717 zcmc&&&2JmW6@Rm&NKv9BOO|XAb!*ygVWLf}*sW0{Ra3{YoumO+VC4@)vAbPySJYDE zF1_vUTs zUvo3rU;g&ABjdjk-goe1x6lOmH_{i`Jn9?SbR=*jEXlT!pd_D*yb}3jv&^X*tumHH zU*oTB>*&58_WO_hG81_);$1L6r3u+ zs)CgQoK~<}fHeiD3vfokS^>@~I8%Ui31->foRbi{*LC-K3FZ{JAi=zL&PlMKokg8? zUIJhi<@1tkqQ9g=%i?admLZs}qIneW#>varI_S6Gvj(rULmkiJnZolip6o3QTaliT z1E|8tD@P7QJXQ2TNp>re-Xf+4yy~aXh-bzKsH(xqC5c2=m5UBkmt(53_m$2rNpG6g zy7%lKa#$Xt4$9IvoD#f(vg|sN{(U?Eu+ghYZ$^SCv9r>vld+?9941eoQAzzhJW?`^T?<)Fg3 z_n!Sko3Z0!ChO1tXr53D*s~5oFd1|?m)mPuf!Q2b zQD8m?E{MIT`vZ%`%d*PX1zE`j^HwRrMr#3{({|}g>S;T-y);Rzy4ad610DF*w{z&c z@YBfO>W5zJ4?>+h@Q1@F?q)y7=o3GU(SN7eNk;vk87H>69mPTOD%W3avdu!3 zY83dL+rxdGz{p`|fc7BajvogAr^lcQTq6JxJ(X8y5*o#k&6GkriN{OE>?w9P+|Mje zpl@xOS?aMHS{E<+y2#`AT<=l)FxV<>&Pd|HoQ?eJGYs03Z97J!yu%VxPT5Wg*Z+y1`L3kWUs^< z)JBW*+M0Yc*Xh;UT>;YNqCAr^{Xc4d^l+odWx7hknA${>6Vm%B#x!>odp^dK%Vau* zOnj?MOj>z}fMksmVPX+O$1J*J>oi$g2fa`5OtMB*AE}#{nq$@u;rUvXH289)u{w5m zy}v}5J9dmq?2(9S)_oH_O}kskC_dc;ISLc7^j0p~}9F z&Pj<1S1N>}Y$zhGM54SzEa4eNS%%9u*2RC;rB$7E-;nl+i4zdmBLg*w{~)*kV@JD4)=|v? zpKh~M%{yh5O*goxWY&+6;ja;7JVY&`?}5MPUIm=z{+|`YgD7_2Ci)JWOGJ~% z1HYez?oln+PvWj#ZgnvA<8G)C$YuGeI6qDOeZ8w|{rgah>*glfin)Tisb=ad1v zGCdOiICvn1=uu>Cx|YkNq+)gYNp?2(IQLWR@dci7>Tb6fGxZMRX*uh8Y9Y@<%{#)k z4$r-a3GOf1tQAv-X)+8`yPvynjbpdi=w*~%>9PiMf5C>=77tNZFu>b%7 literal 0 HcmV?d00001 diff --git a/bmtk-vb/bmtk/utils/converters/__init__.py b/bmtk-vb/bmtk/utils/converters/__init__.py new file mode 100644 index 0000000..04c8f88 --- /dev/null +++ b/bmtk-vb/bmtk/utils/converters/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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/bmtk-vb/bmtk/utils/converters/hoc_converter.py b/bmtk-vb/bmtk/utils/converters/hoc_converter.py new file mode 100644 index 0000000..c500945 --- /dev/null +++ b/bmtk-vb/bmtk/utils/converters/hoc_converter.py @@ -0,0 +1,299 @@ +# Copyright 2017. Allen Institute. 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. +# +# 3. 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. +# +import json +import os.path +import re +from collections import defaultdict +from itertools import groupby +from lxml import etree +import bluepyopt.ephys as ephys +from tqdm import tqdm +import utils + +XML_NS = '{http://www.neuroml.org/schema/neuroml2}' +MECHANISMS = [ + 'channelDensity', 'channelDensityNernst', 'specificCapacitance', 'species', + 'resistivity', 'concentrationModel' +] + +LOCATION_MAP = { + 'apic': 'apical', + 'soma': 'somatic', + 'dend': 'basal', + 'axon': 'axonal', + 'all': 'all' +} + + +def map_location_name(name): + return LOCATION_MAP[name] + + +def load_json(json_path): + params = json.load(open(json_path)) + + scalar = ephys.parameterscalers.NrnSegmentLinearScaler() + mechanisms = {} + sections_lookup = {'soma': 'somatic', 'dend': 'basal', 'axon': 'axonal', 'apic': 'apical'} + def getNrnSeclist(loc_name): + return ephys.locations.NrnSeclistLocation(loc_name, seclist_name=loc_name) + + parameters = [] + for d in params['genome']: + section = sections_lookup[d['section']] + value = d['value'] + name = d['name'] + mech = 'pas' if name == 'g_pass' else d['mechanism'] + mech_name = 'CaDynamics' if mech == 'CaDynamics' else '{}.{}'.format(name, d['section']) + p_name = '{}_{}'.format(name, section) if name == 'g_pass' else name + + if mech_name not in mechanisms: + nrn_mech = ephys.mechanisms.NrnMODMechanism(name=mech_name, mod_path=None, suffix=mech, + locations=[getNrnSeclist(section)]) + mechanisms[mech_name] = nrn_mech + + parameters.append(ephys.parameters.NrnSectionParameter(name=p_name, param_name=name, value_scaler=scalar, + value=value, locations=[getNrnSeclist(section)])) + + parameters.append(ephys.parameters.NrnSectionParameter(name='erev_na', param_name='ena', value_scaler=scalar, + value=params['conditions'][0]['erev'][0]['ena'], + locations=[getNrnSeclist('somatic')])) + parameters.append(ephys.parameters.NrnSectionParameter(name='erev_k', param_name='ek', value_scaler=scalar, + value=params['conditions'][0]['erev'][0]['ek'], + locations=[getNrnSeclist('somatic')])) + parameters.append(ephys.parameters.NrnSectionParameter(name='erev_pas', param_name='e_pas', value_scaler=scalar, + value=params['conditions'][0]['v_init'], + locations=[getNrnSeclist('somatic'), getNrnSeclist('axonal'), + getNrnSeclist('basal'), getNrnSeclist('apical')])) + + parameters.append(ephys.parameters.NrnSectionParameter(name='erev_Ih', param_name='ehcn', value_scaler=scalar, + value=-45.0, + locations=[getNrnSeclist('somatic')])) + + parameters.append(ephys.parameters.NrnSectionParameter(name='res_all', param_name='Ra', value_scaler=scalar, + value=params['passive'][0]['ra'], + locations=[getNrnSeclist('somatic')])) + for sec in params['passive'][0]['cm']: + parameters.append( + ephys.parameters.NrnSectionParameter(name='{}_cap'.format(sec['section']), param_name='cm', + value_scaler=scalar, + value=sec['cm'], + locations=[getNrnSeclist(sec['section'])])) + + parameters.append( + ephys.parameters.NrnSectionParameter(name='ca', param_name='depth_CaDynamics', value_scaler=scalar, + value=0.1, locations=[getNrnSeclist('somatic')])) + parameters.append( + ephys.parameters.NrnSectionParameter(name='ca', param_name='minCai_CaDynamics', value_scaler=scalar, + value=0.0001, locations=[getNrnSeclist('somatic')])) + + return mechanisms.values(), parameters + + +def load_neuroml(neuroml_path): + root = etree.parse(neuroml_path).getroot() + biophysics = defaultdict(list) + for mechanism in MECHANISMS: + xml_mechanisms = root.findall('.//' + XML_NS + mechanism) + for xml_mechanism in xml_mechanisms: + biophysics[mechanism].append(xml_mechanism.attrib) + + return biophysics + + +def define_mechanisms(biophysics): + def keyfn(x): + return x['segmentGroup'] + + channels = biophysics['channelDensity'] + biophysics[ + 'channelDensityNernst'] + segment_groups = [(k, list(g)) + for k, g in groupby( + sorted( + channels, key=keyfn), keyfn)] + mechanisms = [] + for sectionlist, channels in segment_groups: + loc_name = map_location_name(sectionlist) + seclist_loc = ephys.locations.NrnSeclistLocation( + loc_name, seclist_name=loc_name) + for channel in channels: + # print 'mechanisms.append(ephys.mechanisms.NrnMODMechanism(name={}.{}, mod_path=None, suffix={}, locations=[{}]))'.format(channel['ionChannel'], loc_name, channel['ionChannel'], seclist_loc) + mechanisms.append( + ephys.mechanisms.NrnMODMechanism( + name='%s.%s' % (channel['ionChannel'], loc_name), + mod_path=None, + suffix=channel['ionChannel'], + locations=[seclist_loc], )) + for elem in biophysics['species']: + section = map_location_name(elem['segmentGroup']) + section_loc = ephys.locations.NrnSeclistLocation( + section, seclist_name=section) + # print 'mechanisms.append(ephys.mechanisms.NrnMODMechanism(name={}, mod_path=None, suffix={}, location=[{}]))'.format(elem['concentrationModel'], elem['concentrationModel'], section_loc) + mechanisms.append( + ephys.mechanisms.NrnMODMechanism( + name=elem['concentrationModel'], + mod_path=None, + suffix=elem['concentrationModel'], + locations=[section_loc])) + + return mechanisms + + +def define_parameters(biophysics): + ''' for the time being all AIBS distribution are uniform ''' + parameters = [] + + def keyfn(x): + return x['ionChannel'] + + NUMERIC_CONST_PATTERN = r'''[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?''' + rx = re.compile(NUMERIC_CONST_PATTERN, re.VERBOSE) + + def get_cond_density(density_string): + m = re.match(rx, density_string) + return float(m.group()) + + scaler = ephys.parameterscalers.NrnSegmentLinearScaler() + MAP_EREV = { + 'Im': 'ek', + 'Ih': 'ehcn', # I am not sure of that one + 'Nap': 'ena', + 'K_P': 'ek', + 'K_T': 'ek', + 'SK': 'ek', + 'SKv3_1': 'ek', + 'NaTs': 'ena', + 'Kv3_1': 'ek', + 'NaV': 'ena', + 'Kd': 'ek', + 'Kv2like': 'ek', + 'Im_v2': 'ek', + 'pas': 'e_pas' + } + for mech_type in ['channelDensity', 'channelDensityNernst']: + mechanisms = biophysics[mech_type] + for mech in mechanisms: + section_list = map_location_name(mech['segmentGroup']) + seclist_loc = ephys.locations.NrnSeclistLocation( + section_list, seclist_name=section_list) + + def map_name(name): + ''' this name has to match the name in the mod file ''' + reg_name = re.compile('gbar\_(?P[\w]+)') + m = re.match(reg_name, name) + if m: + channel = m.group('channel') + return 'gbar' + '_' + channel + if name[:len('g_pas')] == 'g_pas': + ''' special case ''' + return 'g_pas' + assert False, "name %s" % name + + param_name = map_name(mech['id']) + # print 'parameters.append(ephys.parameters.NrnSectionParameter(name={}, param_name={}, value_scalar={}, value={}, locations=[{}]))'.format(mech['id'], param_name, scaler, get_cond_density(mech['condDensity']), seclist_loc) + parameters.append( + ephys.parameters.NrnSectionParameter( + name=mech['id'], + param_name=param_name, + value_scaler=scaler, + value=get_cond_density(mech['condDensity']), + locations=[seclist_loc])) + if mech_type != 'channelDensityNernst': + # print 'parameters.append(ephys.parameters.NrnSectionParameter(name={}, param_name={}, value_scalar={}, value={}, locations=[{}]))'.format('erev' + mech['id'], MAP_EREV[mech['ionChannel']], scaler, get_cond_density(mech['erev']), seclist_loc) + parameters.append( + ephys.parameters.NrnSectionParameter( + name='erev' + mech['id'], + param_name=MAP_EREV[mech['ionChannel']], + value_scaler=scaler, + value=get_cond_density(mech['erev']), + locations=[seclist_loc])) + + # print '' + PARAM_NAME = {'specificCapacitance': 'cm', 'resistivity': 'Ra'} + for b_type in ['specificCapacitance', 'resistivity']: + for elem in biophysics[b_type]: + section = map_location_name(elem['segmentGroup']) + section_loc = ephys.locations.NrnSeclistLocation( + section, seclist_name=section) + + # print 'parameters.append(ephys.parameters.NrnSectionParameter(name={}, param_name={}, value_scalar={}, value={}, locations=[{}]))'.format(elem['id'], PARAM_NAME[b_type], scaler, get_cond_density(elem['value']), seclist_loc) + parameters.append( + ephys.parameters.NrnSectionParameter( + name=elem['id'], + param_name=PARAM_NAME[b_type], + value_scaler=scaler, + value=get_cond_density(elem['value']), + locations=[section_loc])) + concentrationModel = biophysics['concentrationModel'][0] + + # print '' + for elem in biophysics['species']: + section = map_location_name(elem['segmentGroup']) + section_loc = ephys.locations.NrnSeclistLocation( + section, seclist_name=section) + for attribute in ['gamma', 'decay', 'depth', 'minCai']: + # print 'parameters.append(ephys.parameters.NrnSectionParameter(name={}, param_name={}, value_scalar={}, value={}, locations=[{}]))'.format(elem['id'], attribute + '_' + elem['concentrationModel'], scaler, get_cond_density(concentrationModel[attribute]), seclist_loc) + parameters.append( + ephys.parameters.NrnSectionParameter( + name=elem['id'], + param_name=attribute + '_' + elem['concentrationModel'], + value_scaler=scaler, + value=get_cond_density(concentrationModel[attribute]), + locations=[section_loc])) + + return parameters + + +def create_hoc(neuroml_path, neuroml, morphologies, incr, output_dir): + if neuroml_path.endswith('json'): + mechanisms, parameters = load_json(neuroml_path) + + else: + biophysics = load_neuroml(neuroml_path) + mechanisms = define_mechanisms(biophysics) + parameters = define_parameters(biophysics) + + for morphology in morphologies: + ccell_name = utils.name_ccell(neuroml, morphology) + hoc = ephys.create_hoc.create_hoc( + mechs=mechanisms, + parameters=parameters, + template_name='ccell' + str(incr), + template_filename='cell_template_compatible.jinja2', + template_dir='.', + morphology=morphology + '.swc', ) + with open(os.path.join(output_dir, ccell_name + '.hoc'), 'w') as f: + f.write(hoc) + + +def convert_to_hoc(config, cells, output_dir): + to_convert = cells[['dynamics_params', 'morphology', 'neuroml']] + to_convert = to_convert.drop_duplicates() + neuroml_config_path = config['components']['biophysical_neuron_models_dir'] + incr = 0 + for name, g in tqdm(to_convert.groupby('dynamics_params'), 'creating hoc files'): + neuroml_path = os.path.join(neuroml_config_path, name) + create_hoc(neuroml_path, + list(g['neuroml'])[0], + set(g['morphology']), incr, output_dir) + incr += 1 \ No newline at end of file diff --git a/bmtk-vb/bmtk/utils/converters/sonata/__init__.py b/bmtk-vb/bmtk/utils/converters/sonata/__init__.py new file mode 100644 index 0000000..c473e5d --- /dev/null +++ b/bmtk-vb/bmtk/utils/converters/sonata/__init__.py @@ -0,0 +1,2 @@ +from edge_converters import convert_edges +from node_converters import convert_nodes diff --git a/bmtk-vb/bmtk/utils/converters/sonata/edge_converters.py b/bmtk-vb/bmtk/utils/converters/sonata/edge_converters.py new file mode 100644 index 0000000..335d4f5 --- /dev/null +++ b/bmtk-vb/bmtk/utils/converters/sonata/edge_converters.py @@ -0,0 +1,278 @@ +import os +from functools import partial + +import numpy as np +import pandas as pd +import h5py + +column_renames = { + 'params_file': 'dynamics_params', + 'level_of_detail': 'model_type', + 'morphology': 'morphology', + 'x_soma': 'x', + 'y_soma': 'y', + 'z_soma': 'z', + 'weight_max': 'syn_weight', + 'set_params_function': 'model_template' +} + + +def convert_edges(edges_file, edge_types_file, **params): + is_flat_h5 = False + is_new_h5 = False + try: + h5file = h5py.File(edges_file, 'r') + print + if 'edges' in h5file: + is_new_h5 = True + elif 'num_syns' in h5file: + is_flat_h5 = True + except Exception as e: + pass + + if is_flat_h5: + update_aibs_edges(edges_file, edge_types_file, **params) + return + elif is_new_h5: + update_h5_edges(edges_file, edge_types_file, **params) + return + + try: + edges_csv2h5(edges_file, **params) + return + except Exception as exc: + raise exc + + raise Exception('Could not parse edges file') + + +def update_edge_types_file(edge_types_file, src_network=None, trg_network=None, output_dir='network'): + edge_types_csv = pd.read_csv(edge_types_file, sep=' ') + + # rename required columns + edge_types_csv = edge_types_csv.rename(index=str, columns=column_renames) + + edge_types_output_fn = os.path.join(output_dir, '{}_{}_edge_types.csv'.format(src_network, trg_network)) + edge_types_csv.to_csv(edge_types_output_fn, sep=' ', index=False, na_rep='NONE') + + +def update_h5_edges(edges_file, edge_types_file, src_network=None, population_name=None, trg_network=None, + output_dir='network'): + population_name = population_name if population_name is not None else '{}_to_{}'.format(src_network, trg_network) + input_h5 = h5py.File(edges_file, 'r') + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + edges_output_fn = os.path.join(output_dir, '{}_{}_edges.h5'.format(src_network, trg_network)) + with h5py.File(edges_output_fn, 'w') as h5: + edges_path = '/edges/{}'.format(population_name) + h5.copy(input_h5['/edges'], edges_path) + grp = h5[edges_path] + grp.move('source_gid', 'source_node_id') + grp.move('target_gid', 'target_node_id') + grp.move('edge_group', 'edge_group_id') + + if 'network' in grp['source_node_id'].attrs: + del grp['source_node_id'].attrs['network'] + grp['source_node_id'].attrs['node_population'] = src_network + + if 'network' in grp['target_node_id'].attrs: + del grp['target_node_id'].attrs['network'] + grp['target_node_id'].attrs['node_population'] = trg_network + + create_index(input_h5['edges/target_gid'], grp, index_type=INDEX_TARGET) + create_index(input_h5['edges/source_gid'], grp, index_type=INDEX_SOURCE) + + update_edge_types_file(edge_types_file, src_network, trg_network, output_dir) + + +def update_aibs_edges(edges_file, edge_types_file, trg_network, src_network, population_name=None, output_dir='output'): + population_name = population_name if population_name is not None else '{}_to_{}'.format(src_network, trg_network) + + edges_h5 = h5py.File(edges_file, 'r') + src_gids = edges_h5['/src_gids'] + n_edges = len(src_gids) + trg_gids = np.zeros(n_edges, dtype=np.uint64) + start = edges_h5['/edge_ptr'][0] + for trg_gid, end in enumerate(edges_h5['/edge_ptr'][1:]): + trg_gids[start:end] = [trg_gid]*(end-start) + start = end + + edges_output_fn = os.path.join(output_dir, '{}_{}_edges.h5'.format(src_network, trg_network)) + if not os.path.exists(output_dir): + os.mkdir(output_dir) + + with h5py.File(edges_output_fn, 'w') as hf: + grp = hf.create_group('/edges/{}'.format(population_name)) + + grp.create_dataset('target_node_id', data=trg_gids, dtype='uint64') + grp['target_node_id'].attrs['node_population'] = trg_network + grp.create_dataset('source_node_id', data=edges_h5['src_gids'], dtype='uint64') + grp['source_node_id'].attrs['node_population'] = src_network + grp.create_dataset('edge_group_id', data=np.zeros(n_edges), dtype='uint32') + grp.create_dataset('edge_group_index', data=np.arange(0, n_edges)) + grp.create_dataset('edge_type_id', data=edges_h5['edge_types']) + grp.create_dataset('0/nsyns', data=edges_h5['num_syns'], dtype='uint32') + grp.create_group('0/dynamics_params') + + create_index(trg_gids, grp, index_type=INDEX_TARGET) + create_index(src_gids, grp, index_type=INDEX_SOURCE) + + update_edge_types_file(edge_types_file, src_network, trg_network, output_dir) + + +def edges_csv2h5(edges_file, edge_types_file, src_network, src_nodes, src_node_types, trg_network, trg_nodes, + trg_node_types, output_dir='network', src_label='location', trg_label='pop_name'): + """Used to convert oldest (isee engine) edges files + + :param edges_file: + :param edge_types_file: + :param src_network: + :param src_nodes: + :param src_node_types: + :param trg_network: + :param trg_nodes: + :param trg_node_types: + :param output_dir: + :param src_label: + :param trg_label: + """ + column_renames = { + 'target_model_id': 'node_type_id', + 'weight': 'weight_max', + 'weight_function': 'weight_func', + } + + columns_order = ['edge_type_id', 'target_query', 'source_query'] + + edges_h5 = h5py.File(edges_file, 'r') + edge_types_df = pd.read_csv(edge_types_file, sep=' ') + n_edges = len(edges_h5['src_gids']) + n_targets = len(edges_h5['indptr']) - 1 + + # rename specified columns in edge-types + edge_types_df = edge_types_df.rename(columns=column_renames) + + # Add a "target_query" and "source_query" columns from target_label and source_label + def query_col(row, labels, search_col): + return '&'.join("{}=='{}'".format(l, row[search_col]) for l in labels) + trg_query_fnc = partial(query_col, labels=['node_type_id', trg_label], search_col='target_label') + src_query_fnc = partial(query_col, labels=[src_label], search_col='source_label') + + edge_types_df['target_query'] = edge_types_df.apply(trg_query_fnc, axis=1) + edge_types_df['source_query'] = edge_types_df.apply(src_query_fnc, axis=1) + + # Add an edge_type_id column + edge_types_df['edge_type_id'] = np.arange(100, 100 + len(edge_types_df.index), dtype='uint32') + + nodes_tmp = pd.read_csv(src_nodes, sep=' ', index_col=['id']) + node_types_tmp = pd.read_csv(src_node_types, sep=' ') + src_nodes_df = pd.merge(nodes_tmp, node_types_tmp, on='model_id') + + nodes_tmp = pd.read_csv(trg_nodes, sep=' ', index_col=['id']) + node_types_tmp = pd.read_csv(trg_node_types, sep=' ') + trg_nodes_df = pd.merge(nodes_tmp, node_types_tmp, on='model_id') + + # For assigning edge types to each edge. For a given src --> trg pair we need to lookup source_label and + # target_label values of the nodes, then use it to find the corresponding edge_types row. + print('Processing edge_type_id dataset') + edge_types_ids = np.zeros(n_edges, dtype='uint32') + edge_types_df = edge_types_df.set_index(['node_type_id', 'target_label', 'source_label']) + ten_percent = int(n_targets*.1) # for keepting track of progress + index = 0 # keeping track of row index + for trg_gid in xrange(n_targets): + # for the target find value node_type_id and target_label + nodes_row = trg_nodes_df.loc[trg_gid] + model_id = nodes_row['model_id'] + trg_label_val = nodes_row[trg_label] + + # iterate through all the sources + idx_begin = edges_h5['indptr'][trg_gid] + idx_end = edges_h5['indptr'][trg_gid+1] + for src_gid in edges_h5['src_gids'][idx_begin:idx_end]: + # find each source_label, use value to find edge_type_id + # TODO: may be faster to filter by model_id, trg_label_val before iterating through the sources + src_label_val = src_nodes_df.loc[src_gid][src_label] + edge_type_id = edge_types_df.loc[model_id, trg_label_val, src_label_val]['edge_type_id'] + edge_types_ids[index] = edge_type_id + index += 1 + + if trg_gid % ten_percent == 0 and trg_gid != 0: + print(' processed {} out of {} targets'.format(trg_gid, n_targets)) + + # Create the target_gid table + print('Creating target_gid dataset') + trg_gids = np.zeros(n_edges) + for trg_gid in xrange(n_targets): + idx_begin = edges_h5['indptr'][trg_gid] + idx_end = edges_h5['indptr'][trg_gid+1] + trg_gids[idx_begin:idx_end] = [trg_gid]*(idx_end - idx_begin) + + # Save edges.h5 + edges_output_fn = '{}/{}_{}_edges.h5'.format(output_dir, src_network, trg_network) + print('Saving edges to {}.'.format(edges_output_fn)) + with h5py.File(edges_output_fn, 'w') as hf: + hf.create_dataset('edges/target_gid', data=trg_gids, dtype='uint64') + hf['edges/target_gid'].attrs['node_population'] = trg_network + hf.create_dataset('edges/source_gid', data=edges_h5['src_gids'], dtype='uint64') + hf['edges/source_gid'].attrs['node_population'] = trg_network + hf.create_dataset('edges/index_pointer', data=edges_h5['indptr']) + hf.create_dataset('edges/edge_group', data=np.zeros(n_edges), dtype='uint32') + hf.create_dataset('edges/edge_group_index', data=np.arange(0, n_edges)) + hf.create_dataset('edges/edge_type_id', data=edge_types_ids) + + hf.create_dataset('edges/0/nsyns', data=edges_h5['nsyns'], dtype='uint32') + + # Save edge_types.csv + update_edge_types_file(edge_types_file, src_network, trg_network, output_dir) + ''' + edges_types_output_fn = '{}/{}_{}_edge_types.csv'.format(output_dir, src_network, trg_network) + print('Saving edge-types to {}'.format(edges_types_output_fn)) + edge_types_df = edge_types_df[edge_types_df['edge_type_id'].isin(np.unique(edge_types_ids))] + # reorder columns + reorderd_cols = columns_order + [cn for cn in edge_types_df.columns.tolist() if cn not in columns_order] + edge_types_df = edge_types_df[reorderd_cols] + edge_types_df.to_csv(edges_types_output_fn, sep=' ', index=False, na_rep='NONE') + ''' + + +INDEX_TARGET = 0 +INDEX_SOURCE = 1 + + +def create_index(node_ids_ds, output_grp, index_type=INDEX_TARGET): + if index_type == INDEX_TARGET: + edge_nodes = np.array(node_ids_ds, dtype=np.int64) + output_grp = output_grp.create_group('indicies/target_to_source') + elif index_type == INDEX_SOURCE: + edge_nodes = np.array(node_ids_ds, dtype=np.int64) + output_grp = output_grp.create_group('indicies/source_to_target') + + edge_nodes = np.append(edge_nodes, [-1]) + n_targets = np.max(edge_nodes) + ranges_list = [[] for _ in xrange(n_targets + 1)] + + n_ranges = 0 + begin_index = 0 + cur_trg = edge_nodes[begin_index] + for end_index, trg_gid in enumerate(edge_nodes): + if cur_trg != trg_gid: + ranges_list[cur_trg].append((begin_index, end_index)) + cur_trg = int(trg_gid) + begin_index = end_index + n_ranges += 1 + + node_id_to_range = np.zeros((n_targets+1, 2)) + range_to_edge_id = np.zeros((n_ranges, 2)) + range_index = 0 + for node_index, trg_ranges in enumerate(ranges_list): + if len(trg_ranges) > 0: + node_id_to_range[node_index, 0] = range_index + for r in trg_ranges: + range_to_edge_id[range_index, :] = r + range_index += 1 + node_id_to_range[node_index, 1] = range_index + + output_grp.create_dataset('range_to_edge_id', data=range_to_edge_id, dtype='uint64') + output_grp.create_dataset('node_id_to_range', data=node_id_to_range, dtype='uint64') \ No newline at end of file diff --git a/bmtk-vb/bmtk/utils/converters/sonata/node_converters.py b/bmtk-vb/bmtk/utils/converters/sonata/node_converters.py new file mode 100644 index 0000000..befab51 --- /dev/null +++ b/bmtk-vb/bmtk/utils/converters/sonata/node_converters.py @@ -0,0 +1,399 @@ +import os + +import h5py +import pandas as pd +import numpy as np + + +def convert_nodes(nodes_file, node_types_file, **params): + is_h5 = False + try: + h5file = h5py.File(nodes_file, 'r') + is_h5 = True + except Exception as e: + pass + + if is_h5: + update_h5_nodes(nodes_file, node_types_file, **params) + return + + update_csv_nodes(nodes_file, node_types_file, **params) + + +# columns which need to be renamed, key is original name and value is the updated name +column_renames = { + 'id': 'node_id', + 'model_id': 'node_type_id', + 'electrophysiology': 'dynamics_params', + 'level_of_detail': 'model_type', + 'morphology': 'morphology', + 'params_file': 'dynamics_params', + 'x_soma': 'x', + 'y_soma': 'y', + 'z_soma': 'z' +} + + +def update_h5_nodes(nodes_file, node_types_file, network_name, output_dir='output', + column_order=('node_type_id', 'model_type', 'model_template', 'model_processing', 'dynamics_params', + 'morphology')): + # open nodes and node-types into a single table + input_h5 = h5py.File(nodes_file, 'r') + + output_name = '{}_nodes.h5'.format(network_name) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + nodes_output_fn = os.path.join(output_dir, output_name) + + # save nodes hdf5 + with h5py.File(nodes_output_fn, 'w') as h5: + #h5.copy() + #grp = h5.create_group('/nodes/{}'.format(network_name)) + #input_grp = input_h5['/nodes/'] + nodes_path = '/nodes/{}'.format(network_name) + h5.copy(input_h5['/nodes/'], nodes_path) + grp = h5[nodes_path] + grp.move('node_gid', 'node_id') + grp.move('node_group', 'node_group_id') + + node_types_csv = pd.read_csv(node_types_file, sep=' ') + + node_types_csv = node_types_csv.rename(index=str, columns=column_renames) + + # Change values for model type + model_type_map = { + 'biophysical': 'biophysical', + 'point_IntFire1': 'point_process', + 'intfire': 'point_process', + 'virtual': 'virtual', + 'iaf_psc_alpha': 'nest:iaf_psc_alpha', + 'filter': 'virtual' + } + node_types_csv['model_type'] = node_types_csv.apply(lambda row: model_type_map[row['model_type']], axis=1) + + # Add model_template column + def model_template(row): + model_type = row['model_type'] + if model_type == 'biophysical': + return 'ctdb:Biophys1.hoc' + elif model_type == 'point_process': + return 'nrn:IntFire1' + else: + return 'NONE' + node_types_csv['model_template'] = node_types_csv.apply(model_template, axis=1) + + # Add model_processing column + def model_processing(row): + model_type = row['model_type'] + if model_type == 'biophysical': + return 'aibs_perisomatic' + else: + return 'NONE' + node_types_csv['model_processing'] = node_types_csv.apply(model_processing, axis=1) + + # Reorder columns + orig_columns = node_types_csv.columns + col_order = [cn for cn in column_order if cn in orig_columns] + col_order += [cn for cn in node_types_csv.columns if cn not in column_order] + node_types_csv = node_types_csv[col_order] + + # Save node-types csv + node_types_output_fn = os.path.join(output_dir, '{}_node_types.csv'.format(network_name)) + node_types_csv.to_csv(node_types_output_fn, sep=' ', index=False, na_rep='NONE') + # open nodes and node-types into a single table + + ''' + print('loading csv files') + nodes_tmp = pd.read_csv(nodes_file, sep=' ') + node_types_tmp = pd.read_csv(node_types_file, sep=' ') + nodes_df = pd.merge(nodes_tmp, node_types_tmp, on='node_type_id') + n_nodes = len(nodes_df.index) + + # rename required columns + nodes_df = nodes_df.rename(index=str, columns=column_renames) + + # Old versions of node_type_id may be set to strings/floats, convert to integers + dtype_ntid = nodes_df['node_type_id'].dtype + if dtype_ntid == 'object': + # if string, move model_id to pop_name and create an integer node_type_id column + if 'pop_name' in nodes_df.columns: + nodes_df = nodes_df.drop('pop_name', axis=1) + nodes_df = nodes_df.rename(index=str, columns={'node_type_id': 'pop_name'}) + ntid_map = {pop_name: indx for indx, pop_name in enumerate(nodes_df['pop_name'].unique())} + nodes_df['node_type_id'] = nodes_df.apply(lambda row: ntid_map[row['pop_name']], axis=1) + + elif dtype_ntid == 'float64': + nodes_df['node_type_id'] = nodes_df['node_type_id'].astype('uint64') + + # divide columns up into nodes and node-types columns, and for nodes determine which columns are valid for every + # node-type. The rules are + # 1. If all values are the same for a node-type-id, column belongs in node_types csv. If there's any intra + # node-type heterogenity then the column belongs in the nodes h5. + # 2. For nodes h5 columns, a column belongs to a node-type-id if it contains at least one non-null value + print('parsing input') + opt_columns = [n for n in nodes_df.columns if n not in ['node_id', 'node_type_id']] + heterogeneous_cols = {cn: False for cn in opt_columns} + nonnull_cols = {} # for each node-type, a list of columns that contains at least one non-null value + for node_type_id, nt_group in nodes_df.groupby(['node_type_id']): + nonnull_cols[node_type_id] = set(nt_group.columns[nt_group.isnull().any() == False].tolist()) + for col_name in opt_columns: + heterogeneous_cols[col_name] |= len(nt_group[col_name].unique()) > 1 + + nodes_columns = set(cn for cn, val in heterogeneous_cols.items() if val) + nodes_types_columns = [cn for cn, val in heterogeneous_cols.items() if not val] + + # Check for nodes columns that has non-numeric values, these will require some special processing to save to hdf5 + string_nodes_columns = set() + for col_name in nodes_columns: + if nodes_df[col_name].dtype == 'object': + string_nodes_columns.add(col_name) + if len(string_nodes_columns) > 0: + print('Warning: column(s) {} have non-numeric values that vary within a node-type and will be stored in h5 format'.format(list(string_nodes_columns))) + + # Divide the nodes columns into groups and create neccessary lookup tables. If two node-types share the same + # non-null columns then they belong to the same group + grp_idx2cols = {} # group-id --> group-columns + grp_cols2idx = {} # group-columns --> group-id + grp_id2idx = {} # node-type-id --> group-id + group_index = -1 + for nt_id, cols in nonnull_cols.items(): + group_columns = sorted(list(nodes_columns & cols)) + col_key = tuple(group_columns) + if col_key in grp_cols2idx: + grp_id2idx[nt_id] = grp_cols2idx[col_key] + else: + group_index += 1 + grp_cols2idx[col_key] = group_index + grp_idx2cols[group_index] = group_columns + grp_id2idx[nt_id] = group_index + + # merge x,y,z columns, if they exists, into 'positions' dataset + grp_pos_cols = {} + for grp_idx, cols in grp_idx2cols.items(): + pos_list = [] + for coord in ['x', 'y', 'z']: + if coord in cols: + pos_list += coord + grp_idx2cols[grp_idx].remove(coord) + if len(pos_list) > 0: + grp_pos_cols[grp_idx] = pos_list + + # Create the node_group and node_group_index columns + nodes_df['__bmtk_node_group'] = nodes_df.apply(lambda row: grp_id2idx[row['node_type_id']], axis=1) + nodes_df['__bmtk_node_group_index'] = [0]*n_nodes + for grpid in grp_idx2cols.keys(): + group_size = len(nodes_df[nodes_df['__bmtk_node_group'] == grpid]) + nodes_df.loc[nodes_df['__bmtk_node_group'] == grpid, '__bmtk_node_group_index'] = range(group_size) + + # Save nodes.h5 file + nodes_output_fn = os.path.join(output_dir, '{}_nodes.h5'.format(network_name)) + node_types_output_fn = os.path.join(output_dir, '{}_node_types.csv'.format(network_name)) + if not os.path.exists(output_dir): + os.mkdir(output_dir) + + print('Creating {}'.format(nodes_output_fn)) + with h5py.File(nodes_output_fn, 'w') as hf: + hf.create_dataset('nodes/node_gid', data=nodes_df['node_id'], dtype='uint64') + hf['nodes/node_gid'].attrs['network'] = network_name + hf.create_dataset('nodes/node_type_id', data=nodes_df['node_type_id'], dtype='uint64') + hf.create_dataset('nodes/node_group', data=nodes_df['__bmtk_node_group'], dtype='uint32') + hf.create_dataset('nodes/node_group_index', data=nodes_df['__bmtk_node_group_index'], dtype='uint64') + + for grpid, cols in grp_idx2cols.items(): + group_slice = nodes_df[nodes_df['__bmtk_node_group'] == grpid] + for col_name in cols: + dataset_name = 'nodes/{}/{}'.format(grpid, col_name) + if col_name in string_nodes_columns: + # for columns with non-numeric values + dt = h5py.special_dtype(vlen=bytes) + hf.create_dataset(dataset_name, data=group_slice[col_name], dtype=dt) + else: + hf.create_dataset(dataset_name, data=group_slice[col_name]) + + # special case for positions + if grpid in grp_pos_cols: + hf.create_dataset('nodes/{}/positions'.format(grpid), + data=group_slice.as_matrix(columns=grp_pos_cols[grpid])) + + # Save the node_types.csv file + print('Creating {}'.format(node_types_output_fn)) + node_types_table = nodes_df[['node_type_id'] + nodes_types_columns] + node_types_table = node_types_table.drop_duplicates() + if len(sort_order) > 0: + node_types_table = node_types_table.sort_values(by=sort_order) + + node_types_table.to_csv(node_types_output_fn, sep=' ', index=False) # , na_rep='NONE') + ''' + + +def update_csv_nodes(nodes_file, node_types_file, network_name, output_dir='network', + column_order=('node_type_id', 'model_type', 'model_template', 'model_processing', + 'dynamics_params', 'morphology')): + # open nodes and node-types into a single table + print('loading csv files') + nodes_tmp = pd.read_csv(nodes_file, sep=' ') + node_types_tmp = pd.read_csv(node_types_file, sep=' ') + if 'model_id' in nodes_tmp: + nodes_df = pd.merge(nodes_tmp, node_types_tmp, on='model_id') + elif 'node_type_id' in nodes_tmp: + nodes_df = pd.merge(nodes_tmp, node_types_tmp, on='node_type_id') + else: + raise Exception('Could not find column to merge nodes and node_types') + + n_nodes = len(nodes_df.index) + + # rename required columns + nodes_df = nodes_df.rename(index=str, columns=column_renames) + + # Old versions of node_type_id may be set to strings/floats, convert to integers + dtype_ntid = nodes_df['node_type_id'].dtype + if dtype_ntid == 'object': + # if string, move model_id to pop_name and create an integer node_type_id column + if 'pop_name' in nodes_df: + nodes_df = nodes_df.drop('pop_name', axis=1) + + nodes_df = nodes_df.rename(index=str, columns={'node_type_id': 'pop_name'}) + + ntid_map = {pop_name: indx for indx, pop_name in enumerate(nodes_df['pop_name'].unique())} + nodes_df['node_type_id'] = nodes_df.apply(lambda row: ntid_map[row['pop_name']], axis=1) + + elif dtype_ntid == 'float64': + nodes_df['node_type_id'] = nodes_df['node_type_id'].astype('uint64') + + # divide columns up into nodes and node-types columns, and for nodes determine which columns are valid for every + # node-type. The rules are + # 1. If all values are the same for a node-type-id, column belongs in node_types csv. If there's any intra + # node-type heterogenity then the column belongs in the nodes h5. + # 2. For nodes h5 columns, a column belongs to a node-type-id if it contains at least one non-null value + print('parsing input') + opt_columns = [n for n in nodes_df.columns if n not in ['node_id', 'node_type_id']] + heterogeneous_cols = {cn: False for cn in opt_columns} + nonnull_cols = {} # for each node-type, a list of columns that contains at least one non-null value + for node_type_id, nt_group in nodes_df.groupby(['node_type_id']): + nonnull_cols[node_type_id] = set(nt_group.columns[nt_group.isnull().any() == False].tolist()) + for col_name in opt_columns: + heterogeneous_cols[col_name] |= len(nt_group[col_name].unique()) > 1 + + nodes_columns = set(cn for cn, val in heterogeneous_cols.items() if val) + nodes_types_columns = [cn for cn, val in heterogeneous_cols.items() if not val] + + # Check for nodes columns that has non-numeric values, these will require some special processing to save to hdf5 + string_nodes_columns = set() + for col_name in nodes_columns: + if nodes_df[col_name].dtype == 'object': + string_nodes_columns.add(col_name) + if len(string_nodes_columns) > 0: + print('Warning: column(s) {} have non-numeric values that vary within a node-type and will be stored in h5 format'.format(list(string_nodes_columns))) + + # Divide the nodes columns into groups and create neccessary lookup tables. If two node-types share the same + # non-null columns then they belong to the same group + grp_idx2cols = {} # group-id --> group-columns + grp_cols2idx = {} # group-columns --> group-id + grp_id2idx = {} # node-type-id --> group-id + group_index = -1 + for nt_id, cols in nonnull_cols.items(): + group_columns = sorted(list(nodes_columns & cols)) + col_key = tuple(group_columns) + if col_key in grp_cols2idx: + grp_id2idx[nt_id] = grp_cols2idx[col_key] + else: + group_index += 1 + grp_cols2idx[col_key] = group_index + grp_idx2cols[group_index] = group_columns + grp_id2idx[nt_id] = group_index + + # merge x,y,z columns, if they exists, into 'positions' dataset + grp_pos_cols = {} + for grp_idx, cols in grp_idx2cols.items(): + pos_list = [] + for coord in ['x', 'y', 'z']: + if coord in cols: + pos_list += coord + grp_idx2cols[grp_idx].remove(coord) + if len(pos_list) > 0: + grp_pos_cols[grp_idx] = pos_list + + # Create the node_group and node_group_index columns + nodes_df['__bmtk_node_group'] = nodes_df.apply(lambda row: grp_id2idx[row['node_type_id']], axis=1) + nodes_df['__bmtk_node_group_index'] = [0]*n_nodes + for grpid in grp_idx2cols.keys(): + group_size = len(nodes_df[nodes_df['__bmtk_node_group'] == grpid]) + nodes_df.loc[nodes_df['__bmtk_node_group'] == grpid, '__bmtk_node_group_index'] = range(group_size) + + # Save nodes.h5 file + nodes_output_fn = os.path.join(output_dir, '{}_nodes.h5'.format(network_name)) + node_types_output_fn = os.path.join(output_dir, '{}_node_types.csv'.format(network_name)) + if not os.path.exists(output_dir): + os.mkdir(output_dir) + + print('Creating {}'.format(nodes_output_fn)) + with h5py.File(nodes_output_fn, 'w') as hf: + grp = hf.create_group('/nodes/{}'.format(network_name)) + grp.create_dataset('node_id', data=nodes_df['node_id'], dtype='uint64') + grp.create_dataset('node_type_id', data=nodes_df['node_type_id'], dtype='uint64') + grp.create_dataset('node_group_id', data=nodes_df['__bmtk_node_group'], dtype='uint32') + grp.create_dataset('node_group_index', data=nodes_df['__bmtk_node_group_index'], dtype='uint64') + + for grpid, cols in grp_idx2cols.items(): + group_slice = nodes_df[nodes_df['__bmtk_node_group'] == grpid] + for col_name in cols: + dataset_name = '{}/{}'.format(grpid, col_name) + if col_name in string_nodes_columns: + # for columns with non-numeric values + dt = h5py.special_dtype(vlen=bytes) + grp.create_dataset(dataset_name, data=group_slice[col_name], dtype=dt) + else: + grp.create_dataset(dataset_name, data=group_slice[col_name]) + + # special case for positions + if grpid in grp_pos_cols: + grp.create_dataset('{}/positions'.format(grpid), + data=group_slice.as_matrix(columns=grp_pos_cols[grpid])) + + # Create empty dynamics_params + grp.create_group('{}/dynamics_params'.format(grpid)) + + # Save the node_types.csv file + print('Creating {}'.format(node_types_output_fn)) + node_types_table = nodes_df[['node_type_id'] + nodes_types_columns] + node_types_table = node_types_table.drop_duplicates() + + # Change values for model type + model_type_map = { + 'biophysical': 'biophysical', + 'point_IntFire1': 'point_process', + 'virtual': 'virtual', + 'intfire': 'point_process', + 'filter': 'virtual' + } + node_types_table['model_type'] = node_types_table.apply(lambda row: model_type_map[row['model_type']], axis=1) + if 'set_params_function' in node_types_table: + node_types_table = node_types_table.drop('set_params_function', axis=1) + + # Add model_template column + def model_template(row): + model_type = row['model_type'] + if model_type == 'biophysical': + return 'ctdb:Biophys1.hoc' + elif model_type == 'point_process': + return 'nrn:IntFire1' + else: + return 'NONE' + node_types_table['model_template'] = node_types_table.apply(model_template, axis=1) + + # Add model_processing column + def model_processing(row): + model_type = row['model_type'] + if model_type == 'biophysical': + return 'aibs_perisomatic' + else: + return 'NONE' + node_types_table['model_processing'] = node_types_table.apply(model_processing, axis=1) + + # Reorder columns + orig_columns = node_types_table.columns + col_order = [cn for cn in column_order if cn in orig_columns] + col_order += [cn for cn in node_types_table.columns if cn not in column_order] + node_types_table = node_types_table[col_order] + + node_types_table.to_csv(node_types_output_fn, sep=' ', index=False, na_rep='NONE') diff --git a/bmtk-vb/bmtk/utils/io/Final_Sp2019_20190512.docx b/bmtk-vb/bmtk/utils/io/Final_Sp2019_20190512.docx new file mode 100644 index 0000000000000000000000000000000000000000..a65f351ed9311bdfc0b3d25b5e3883021ff16870 GIT binary patch literal 566550 zcmeF1QlZripw?{_v+HUD5H2dPRbCt0(q;`#NrGk+3fGH zE@%{GE%JIm4vM_Y&t|?G_%RNfoPQvz_@DI1hs-)u`>7}Uo>e03>3+?x?bUX z4Z~N1=1V%3!yAT9?S*4>P-|gm9CikARheyC{ZuZYuhv02FV+u_eqvzB`ck5h;xd7% zrwa1D*fO025@M5j3fkuhvz7aj!gaU^YnOhDAfmMe9;iqJA*J*`SH%aDQ>|oX&wX+T z@+pFs9va*cl{xq&mIq;)X<(% zCXW@z1K8}77xV8)5-F<5;Akzhs5xx##J7lynE{kzYSEYDau_ROs<~wo(&pkkC$-hr z3_q5FOs0P;){;8j5LJ9)ePMWAvsix6<^qlBEOZI+e|MVnhE*?;JD6jwsh(-EZ5bKJ zauSVept0|I7B0Ms`^!fnLusK3p&}VReav-{wSP{dPOt>nZ;7;84E7yxa`c2A|F3+^ zF^F+g8vp^#P=Ep<{TCE>2WL|TQwI}QyZ?moe-!b`cPj2sx@m1E;5V=?74GTWxG%%h zvkTlO&wj7Rr^^>yA5IgwdUzALlBiOB6%*Wiue;ORq32G(!@c)zl7R6M;gM{1oRlod zD`iKykqu&rtdE(6_%JmQ39J9}dv!ta6WPo&hy2!lUgujpI!S*cKDpeT*V$I_ht9X-fLCCJ6S0OHOrEC;xMllrF})kf=*V#8?%Eu5nvi8ziSu zN5-R(tWQVAw2{oEJq>-_szq||oKt#uXp9vnG_+$n&tAgl#gq9*o|P9q`i%&Gbfno* zg`>*+FXwyDP{goCGrD+5zbnj_Tgpo@(YZ!#$*n!Pma~T6@Aj29=11;F-c&H7SWu=wqC_upC^cr3kPxTL1<&qt|^Hsa|Vg1Jxdq z!OF%&@pYDw8q52mMkAj=SxL__ZJy{ zcl#*>HL@vNrz5*E9O2-gt*0y3_aUJyUns?&HUu8g0MzqhUJf}9tjFI+;44OUsVSlq zyL#s}`RB^+-<=1N^9d*B`LLDH&0C(;r`$N!!P*@H(7OnAi8VEpa%Q1F>F<2DcaBt zLfkt1F{%@K0#PO4amL*y|F2_zD+9KWy<5$*AXym{JMIW?&}*%|MhtQrC3WeRLs&r8 zh!_A{c3T0$2r06)nRFLL&?i4X8N|@K+=(BEovzaiZ+dMoW+`(=nS|D1MZ6F(-duE_z$;&DU!-;8K-TH>5Ahqd{ z6;g{yPFO9BuTo=3e=UBPTlNwL3+70Vmu515bg1wJ(Mk**%9VtZX!SwLVZNkx^LPpt zh9z31zI9r&Z9uzs+LE8^nAwC&5z-_Z8xozLt$=YX@b;|)IOWhWeM(G658^$di0jTG z5a7j8X+IF=)DL?9byHSf(7nLiNjk&- zB=_(4?VKdWh5BB~Yzp)~J5!ZjXFrrc1XDOMZp61rf8wA#P!NsT)su=*v84F@(CA6i zyK0p53~p*_OjT3rajIO3h+*o+>m1Qh)fuvf#Nl;m=M~EX`6kQ>`5PI7k4f5CRmLW? zf@@@X?E_Qzx_p9i(zwvUf*Zpp%fg^iTKG6dy9EBsp3o1w=JHsPn@^u6hRz&(Ok4R_ z`CSGM^>ie9iYFX9%_X2z7~h`{XMBTGh9GQ#QsaXZdW73<+Yn&uF$ydxMcMc`h#SqY zSc7SqVRgtV5ADcM6Z#=om!`1v1m@EPaHoqyaVSA$9&iDorn`h;i}5Rb4y z_(hD47|a@v{9LNoR4fU3f=heu8t^808rCk*2YnZq3d&D#@Sqw%fQ}4dl%xO*yf@|% zFapv8p-&x%6qvc7TD

^x`uh26Bngs2mwOixeax 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 z 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

Q(lPkIdge5cocV5TiA>xQ`G7lOU4sQAi(ni56 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%0DZF6t!d-e zyErSGHaq;{=x|8ocvR(dK;?ME!SOej+(|#|Go-&rkr;CZV<8*X@tLf$jcAr~{#T-& z5mu4WjE>5{9MT&0SE}Mxt@^^y z0SZoDv@ppG946v3DMuJ0(7x2P#Mro7iiC-``^$l!0)zZ{f#q1>ok`iA#G)K&KncH> zXnjEp!z3psf6Qa<{}U2TU3B|p4I-GM|H|Ybfo+F>%(q%X6!0=(SuZVznXU(4W%Yh#eHk-w7x^{#{ zqr3j)eU%D<>cabdk$;SUdBn0KPg(i6ClBsE4$L%XTt2_UV#zQXUt9Z?6ZK#<5gQQ> zqmk1YS-@P^Z^ulYwoZ`EtjVkgJegU=CVTt%U|Ua1hH)q)nLUN?#jtulIb}z`fj4I_ zxjuQ~wW(u##|-fuIyhwfYCsKP!B;S{a{O<%4^+3+&xBYH(-24$ZE9)g+>A3$KPXuhpnBwMyl0 zixikuv1&a>ho1|T``%I2zHpw|rAq9~Uu;Ri0_#+cKRCJkL(x#T*oaygtk|L?B3PZcb@r&wVo$k&+mRN&G?Hp8OV z%AD;Bd1Sg>!DzdF{@YQ{-QE4oH`2&FH8e;x8?@5TKc1-jMaM~1ng)H=#Itr;-^O(U z8#VE6(tL7(g1+6`M|{^Qbm+Gc69?R$KQ4aRwAe*cqE}82AJx~pPF0Tz#e(YBbo;zU z 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