diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..94143827ed065ca0d7d5be1b765d255c5c32cd9a --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/.gitignore b/.gitignore index 7f7adc5d269adf15ba7ae4e4de2930ae05ca0a02..3256d9fa27698c2d00976e1f05fc9a35060e9179 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /build -/.vscode \ No newline at end of file +/.vscode +__pycache__ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 683d5b4df68a7a3502f7bfdb252447ea07c01da0..a0ecd16e19b20f6ac993b5a0fe05c2ac63ed8a3f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,22 +1,36 @@ -stages: - - build - - run +# include: +# - project: 'vr-group/in-situ-pipeline/insite' +# ref: develop +# file: '/test-setup.yml' -docker:build: - stage: build - tags: - - centos - - docker - script: - - docker build -t insite-nest-module . +# variables: +# INSITE_NEST_MODULE_COMMIT: $CI_COMMIT_SHA -docker:run: - stage: run - tags: - - centos - - docker - dependencies: - - docker:build - script: - - docker run --publish 8000:8000 insite-nest-module 20 +# api_test: +# extends: .api_test +deploy:develop: + tags: + - docker + - centos + only: + - develop + script: + - docker build -t rwthvr/insite-nest-module:develop . + - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + - docker push rwthvr/insite-nest-module:develop + +deploy:master: + tags: + - docker + - centos + only: + - master + script: + - docker build -t rwthvr/insite-nest-module . + - VERSION=$(git tag --points-at $CI_COMMIT_SHA) + - > + [[ $VERSION =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] + - docker tag rwthvr/insite-nest-module rwthvr/insite-nest-module:$VERSION + - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + - docker push rwthvr/insite-nest-module diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f14d35c8b473383e7e56b098f2341d411658c85..3ee9ac482e30cca4d307d9c021d11224af35d421 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ project( ${MODULE_NAME} CXX ) # Add cpprestsdk find_package(cpprestsdk REQUIRED) +find_package(libpqxx REQUIRED) # Get the Python executable (for help generation). execute_process( @@ -283,7 +284,7 @@ if ( BUILD_SHARED_LIBS ) install( TARGETS ${MODULE_NAME}_module DESTINATION ${CMAKE_INSTALL_LIBDIR} ) - target_link_libraries(${MODULE_NAME}_module PRIVATE cpprestsdk::cpprest) + target_link_libraries(${MODULE_NAME}_module PRIVATE cpprestsdk::cpprest libpqxx::pqxx_shared) endif () # Build dynamic/static library for standard linking from NEST. @@ -298,6 +299,7 @@ set_target_properties( ${MODULE_NAME}_lib COMPILE_FLAGS "${NEST_CXXFLAGS}" LINK_FLAGS "${NEST_LIBS}" OUTPUT_NAME ${MODULE_NAME} ) +target_link_libraries(${MODULE_NAME}_lib PRIVATE cpprestsdk::cpprest libpqxx::pqxx_shared) # Install library, header and sli init files. install( TARGETS ${MODULE_NAME}_lib DESTINATION ${CMAKE_INSTALL_LIBDIR} ) @@ -340,6 +342,11 @@ if ( ( NOT CMAKE_CROSSCOMPILING ) endif () +configure_file( + ${CMAKE_SOURCE_DIR}/examples/run_brunel_simulation.sh.in + ${CMAKE_BINARY_DIR}/run_brunel_simulation.sh + @ONLY +) message( "" ) message( "-------------------------------------------------------" ) diff --git a/Dockerfile b/Dockerfile index 56ec745c03264cd7d86c9a5b2511ade2189a490d..a8db0618eca26ac302a71505a18f1ed1134c47c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,26 +2,44 @@ FROM ubuntu:latest ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ cmake g++ make ninja-build python3 python3-dev python3-pip python3-numpy python3-scipy python3-matplotlib \ - git gsl-bin libgsl0-dev libltdl-dev libtool \ + git gsl-bin libgsl0-dev libltdl-dev libtool netcat \ libboost-atomic-dev libboost-thread-dev libboost-system-dev libboost-date-time-dev libboost-regex-dev \ libboost-filesystem-dev libboost-random-dev libboost-chrono-dev libboost-serialization-dev \ - libwebsocketpp-dev openssl libssl-dev ninja-build + libwebsocketpp-dev openssl libssl-dev ninja-build \ + openmpi-bin libopenmpi-dev libpq-dev postgresql-server-dev-all RUN pip3 install Cython -RUN git clone --single-branch --branch nestio https://github.com/jougs/nest-simulator.git nest + +RUN git clone --single-branch --branch master https://github.com/nest/nest-simulator.git nest && \ + cd nest && \ + git checkout 5c0f41230dda9e4b99b8df89729ea43b340246ad && \ + cd / WORKDIR /nest-build RUN cmake \ -G Ninja \ + -Dwith-mpi=ON \ -DCMAKE_INSTALL_PREFIX=/nest-install \ -DCMAKE_BUILD_TYPE=Release \ /nest RUN ninja && ninja install + RUN git clone --single-branch --branch v2.10.14 --recurse-submodules https://github.com/microsoft/cpprestsdk.git /cpprestsdk WORKDIR /cpprestsdk-build RUN cmake \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ /cpprestsdk RUN ninja && ninja install + +RUN git clone --single-branch --branch 6.4.6 https://github.com/jtv/libpqxx.git /libpqxx +WORKDIR /libpqxx-build +RUN cmake \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + /libpqxx +RUN ninja && ninja install + COPY . /insite WORKDIR /insite-build RUN cmake \ @@ -30,4 +48,8 @@ RUN cmake \ -DCMAKE_BUILD_TYPE=Release \ /insite RUN ninja && ninja install -ENTRYPOINT [ "/insite/examples/run.sh" ] \ No newline at end of file +ENV PGPASSWORD=postgres + +EXPOSE 8000 +ENTRYPOINT "/insite-build/run_brunel_simulation.sh" +CMD 1000 2500 2 \ No newline at end of file diff --git a/data_storage.cpp b/data_storage.cpp index a6d4ddb65200d3cf4dfb95ab45cb9c5b3089453d..4ed7af7cdcedea4ff215bdef2f069af059474bb9 100644 --- a/data_storage.cpp +++ b/data_storage.cpp @@ -1,6 +1,7 @@ #include "data_storage.hpp" #include <algorithm> +#include <cstring> namespace insite { @@ -37,21 +38,40 @@ DataStorage::DataStorage( // spikes_neurons_dataset_ = // h5_file_->createDataSet("spikes/neurons", H5::PredType::NATIVE_UINT64, // spikes_data_space, spikes_set_properties_); + buffered_spikes_.reserve(32 * 1024 * 1024 / sizeof(Spike)); // Reserve 32mb + SetCurrentSimulationTime(0.0); } -void DataStorage::AddSpike(std::uint64_t simulation_step, std::uint64_t gid) { +void DataStorage::AddNeuronId(uint64_t neuron_id) { + std::unique_lock<std::mutex> lock(neuron_ids_mutex_); + const auto insert_position = + std::lower_bound(neuron_ids_.begin(), neuron_ids_.end(), neuron_id); + if (insert_position == neuron_ids_.end() || *insert_position != neuron_id) { + neuron_ids_.insert(insert_position, neuron_id); + } +} + +std::vector<uint64_t> DataStorage::GetNeuronIds() { + std::unique_lock<std::mutex> lock(neuron_ids_mutex_); + std::vector<uint64_t> temp_neuron_ids = neuron_ids_; + return temp_neuron_ids; +} + +void DataStorage::AddSpike(double simulation_time, std::uint64_t gid) { std::unique_lock<std::mutex> lock(spike_mutex_); constexpr auto spike_occured_before = [](const Spike& lhs, const Spike& rhs) { - return lhs.simulation_step < rhs.simulation_step; + return lhs.simulation_time < rhs.simulation_time; }; - const Spike spike {simulation_step, gid}; - const auto equal_range = std::equal_range(buffered_spikes_.begin(), buffered_spikes_.end(), spike, spike_occured_before); + const Spike spike{simulation_time, gid}; + const auto equal_range = + std::equal_range(buffered_spikes_.begin(), buffered_spikes_.end(), spike, + spike_occured_before); for (auto i = equal_range.first; i != equal_range.second; ++i) { if (i->gid == gid) { return; } } - buffered_spikes_.insert(equal_range.second,spike); + buffered_spikes_.insert(equal_range.second, spike); } std::vector<Spike> DataStorage::GetSpikes() { @@ -91,4 +111,71 @@ void DataStorage::Flush() { // buffered_spikes_.clear(); } -} // namespace insite \ No newline at end of file +void DataStorage::AddMultimeterMeasurement(std::uint64_t device_id, + const std::string& attribute_name, + const double simulation_time, + const std::uint64_t gid, + const double value) { + std::unique_lock<std::mutex> lock(measurement_mutex_); + auto& measurement = buffered_measurements_[device_id][attribute_name]; + auto& simulation_times = measurement.simulation_times; + auto& gids = measurement.gids; + auto& values = measurement.values; + + auto time_iterator = std::lower_bound( + simulation_times.begin(), simulation_times.end(), simulation_time); + auto time_index = std::distance(simulation_times.begin(), time_iterator); + if (time_iterator == simulation_times.end() || + *time_iterator != simulation_time) { + simulation_times.insert(time_iterator, simulation_time); + + auto new_values = + std::vector<double>(simulation_times.size() * gids.size(), 0.0); + for (std::size_t t = 0; t < simulation_times.size(); ++t) + for (std::size_t g = 0; g < gids.size(); ++g) + if (t != time_index) + new_values[t * gids.size() + g] = + values[(t > time_index ? t - 1 : t) * gids.size() + g]; + values = new_values; + } + + auto gid_iterator = std::lower_bound(gids.begin(), gids.end(), gid); + auto gid_index = std::distance(gids.begin(), gid_iterator); + if (gid_iterator == gids.end() || *gid_iterator != gid) { + gids.insert(gid_iterator, gid); + + auto new_values = + std::vector<double>(simulation_times.size() * gids.size(), 0.0); + for (std::size_t t = 0; t < simulation_times.size(); ++t) + for (std::size_t g = 0; g < gids.size(); ++g) + if (g != gid_index) + new_values[t * gids.size() + g] = + values[t * gids.size() + (g > gid_index ? g - 1 : g)]; + values = new_values; + } + + values[time_index * gids.size() + gid_index] = value; +} + +std::unordered_map<std::uint64_t, + std::unordered_map<std::string, MultimeterMeasurements>> +DataStorage::GetMultimeterMeasurements() { + std::unique_lock<std::mutex> lock(measurement_mutex_); + auto measurements = buffered_measurements_; + return measurements; +} + +void DataStorage::SetCurrentSimulationTime(double simulation_time) { + uint64_t simulation_time_int; + memcpy(&simulation_time_int, &simulation_time, sizeof(simulation_time_int)); + current_simulation_time_ = simulation_time_int; +} + +double DataStorage::GetCurrentSimulationTime() const { + const uint64_t simulation_time_int =current_simulation_time_; + double simulation_time; + memcpy(&simulation_time, &simulation_time_int, sizeof(simulation_time)); + return simulation_time; +} + +} // namespace insite diff --git a/data_storage.hpp b/data_storage.hpp index 89b27aef7c0d6fde243b114e13ee8911bd8a81b0..580a0c9348982197007202626aed2075394689f7 100644 --- a/data_storage.hpp +++ b/data_storage.hpp @@ -3,35 +3,74 @@ #include <cstdint> #include <memory> +#include <mutex> #include <string> +#include <unordered_map> #include <vector> -#include <mutex> +#include <atomic> namespace insite { struct Spike { - std::uint64_t simulation_step; + double simulation_time; std::uint64_t gid; }; static_assert(sizeof(Spike) == 2 * 8); +struct MultimeterInfo { + std::uint64_t device_id; + bool needs_update; + std::vector<std::string> double_attributes; + std::vector<std::string> long_attributes; + std::vector<std::uint64_t> gids; +}; + +struct MultimeterMeasurements { + std::vector<double> simulation_times; + std::vector<std::uint64_t> gids; + std::vector<double> values; +}; + class DataStorage { public: DataStorage(const std::string& filename /*, hsize_t time_chunk_size, hsize_t neuronids_chunk_size*/); - void AddSpike(std::uint64_t simulation_step, std::uint64_t gid); + void AddNeuronId(uint64_t neuron_ids); + std::vector<uint64_t> GetNeuronIds(); + + void AddSpike(double simulation_time, std::uint64_t gid); std::vector<Spike> GetSpikes(); void Flush(); + void AddMultimeterMeasurement(std::uint64_t device_id, + const std::string& attribute_name, const double simulation_time, + const std::uint64_t gid, const double value); + std::unordered_map<std::uint64_t, std::unordered_map<std::string, + MultimeterMeasurements>> GetMultimeterMeasurements(); + + void SetCurrentSimulationTime(double simulation_time); + double GetCurrentSimulationTime() const; + private: // std::unique_ptr<H5::H5File> h5_file_; + std::mutex neuron_ids_mutex_; + std::vector<uint64_t> neuron_ids_; + // uint64_t flushed_spikes_count = 0; std::vector<Spike> buffered_spikes_; // H5::DataSet spikes_times_dataset_; // H5::DataSet spikes_neurons_dataset_; std::mutex spike_mutex_; + + std::atomic_uint64_t current_simulation_time_; + + // Device ID to attribute index to measurement map. + std::unordered_map<std::uint64_t, std::unordered_map<std::string, + MultimeterMeasurements>> buffered_measurements_; + + std::mutex measurement_mutex_; }; } // namespace insite diff --git a/examples/sim2.py b/examples/brunel_simulation.py similarity index 87% rename from examples/sim2.py rename to examples/brunel_simulation.py index 5e72710f5627d8e167aed6df22fe89528717d2a4..6dc936c0ed5798313acec20fad053c3274c315ca 100644 --- a/examples/sim2.py +++ b/examples/brunel_simulation.py @@ -44,12 +44,21 @@ References # Import all necessary modules for simulation, analysis and plotting. import nest -import nest.raster_plot +import signal +import math import time import sys from numpy import exp + +def sigint_handler(sig, frame): + print('Exiting...') + sys.exit(0) + + +signal.signal(signal.SIGINT, sigint_handler) + nest.Install("insitemodule") nest.ResetKernel() @@ -81,11 +90,11 @@ epsilon = 0.1 # connection probability # Definition of the number of neurons in the network and the number of neuron # recorded from -order = 2500 +order = int(sys.argv[2]) if len( + sys.argv) > 2 else 2500 # Should be square, otherwise the position grid becomes invalid NE = 4 * order # number of excitatory neurons NI = 1 * order # number of inhibitory neurons N_neurons = NE + NI # number of neurons in total -N_rec = 4 # record from 50 neurons ############################################################################### # Definition of connectivity parameter @@ -127,7 +136,7 @@ p_rate = 1000.0 * nu_ex * CE # already processed simulation time as well as its percentage of the total # simulation time. -nest.SetKernelStatus({"resolution": dt, "print_time": True, +nest.SetKernelStatus({"resolution": dt, "overwrite_files": True}) print("Building network") @@ -148,21 +157,11 @@ nest.SetDefaults("poisson_generator", {"rate": p_rate}) # as the poisson generator and two spike detectors. The spike detectors will # later be used to record excitatory and inhibitory spikes. -nodes_ex = nest.Create("iaf_psc_delta", NE) -nodes_in = nest.Create("iaf_psc_delta", NI) +nodes_ex = nest.Create("iaf_psc_delta", positions=nest.spatial.grid([int(math.sqrt(NE)), int(math.sqrt(NE))])) +nodes_in = nest.Create("iaf_psc_delta", positions=nest.spatial.grid([int(math.sqrt(NI)), int(math.sqrt(NI))])) noise = nest.Create("poisson_generator") espikes = nest.Create("spike_detector") ispikes = nest.Create("spike_detector") -multimeter = nest.Create("multimeter") -multimeter2 = nest.Create("multimeter") -ascii_multimeter = nest.Create("multimeter") - -############################################################################### -# Configuration of the spike detectors recording excitatory and inhibitory -# spikes using ``SetStatus``, which expects a list of node handles and a list -# of parameter dictionaries. Setting the property `record_to` to *"ascii"* -# ensures that the spikes will be recorded to a file, whose name starts with -# the string assigned to label. nest.SetStatus(espikes, [{"label": "brunel-py-ex", "record_to": "insite"}]) @@ -170,11 +169,6 @@ nest.SetStatus(espikes, [{"label": "brunel-py-ex", nest.SetStatus(ispikes, [{"label": "brunel-py-in", "record_to": "insite"}]) -nest.SetStatus(multimeter, {"record_from": ["V_m"], "record_to": "insite", }) -nest.SetStatus(multimeter2, {"record_from": ["V_m"], "record_to": "insite", }) -nest.SetStatus(ascii_multimeter, {"record_from": [ - "V_m"], "record_to": "ascii", }) - print("Connecting devices") ############################################################################### @@ -207,12 +201,8 @@ nest.Connect(noise, nodes_in, syn_spec="excitatory") # Here the same shortcut for the specification of the synapse as defined # above is used. -nest.Connect(nodes_ex[:N_rec], espikes, syn_spec="excitatory") -nest.Connect(nodes_in[:N_rec], ispikes, syn_spec="excitatory") -nest.Connect(multimeter, nodes_ex[:N_rec], syn_spec="excitatory") -nest.Connect(multimeter, nodes_in[:N_rec], syn_spec="excitatory") -nest.Connect(ascii_multimeter, nodes_ex[:N_rec], syn_spec="excitatory") -nest.Connect(ascii_multimeter, nodes_in[:N_rec], syn_spec="excitatory") +nest.Connect(nodes_ex, espikes, syn_spec="excitatory") +nest.Connect(nodes_in, ispikes, syn_spec="excitatory") print("Connecting network") @@ -227,7 +217,8 @@ print("Excitatory connections") # suffices to insert a string. conn_params_ex = {'rule': 'fixed_indegree', 'indegree': CE} -nest.Connect(nodes_ex, nodes_ex + nodes_in, conn_params_ex, "excitatory") +nest.Connect(nodes_ex, nodes_ex, conn_params_ex, "excitatory") +nest.Connect(nodes_ex, nodes_in, conn_params_ex, "excitatory") print("Inhibitory connections") @@ -238,7 +229,8 @@ print("Inhibitory connections") # population defined above. conn_params_in = {'rule': 'fixed_indegree', 'indegree': CI} -nest.Connect(nodes_in, nodes_ex + nodes_in, conn_params_in, "inhibitory") +nest.Connect(nodes_in, nodes_ex, conn_params_in, "inhibitory") +nest.Connect(nodes_in, nodes_in, conn_params_in, "inhibitory") ############################################################################### # Storage of the time point after the buildup of the network in a variable. @@ -270,8 +262,8 @@ events_in = nest.GetStatus(ispikes, "n_events")[0] # neurons recorded from and the simulation time. The multiplication by 1000.0 # converts the unit 1/ms to 1/s=Hz. -rate_ex = events_ex / simtime * 1000.0 / N_rec -rate_in = events_in / simtime * 1000.0 / N_rec +rate_ex = events_ex / simtime * 1000.0 / NE +rate_in = events_in / simtime * 1000.0 / NI ############################################################################### # Reading out the number of connections established using the excitatory and @@ -300,3 +292,10 @@ print("Excitatory rate : %.2f Hz" % rate_ex) print("Inhibitory rate : %.2f Hz" % rate_in) print("Building time : %.2f s" % build_time) print("Simulation time : %.2f s" % sim_time) + +try: + input("Press Enter to quit...") +except EOFError: + print("Simulation finished, press ctrl+c to exit.") + while True: + time.sleep(1) diff --git a/examples/run.sh b/examples/run.sh deleted file mode 100755 index c730a500da1046cd24bdaaec2e901e3f703e7b2b..0000000000000000000000000000000000000000 --- a/examples/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -source /nest-install/bin/nest_vars.sh -export LD_LIBRARY_PATH=$NEST_MODULE_PATH:/usr/local/lib/:$LD_LIBRARY_PATH -python3 /insite/examples/sim2.py $1 \ No newline at end of file diff --git a/examples/run_brunel_simulation.sh.in b/examples/run_brunel_simulation.sh.in new file mode 100755 index 0000000000000000000000000000000000000000..be009df14365664f74531417bc418b9698628679 --- /dev/null +++ b/examples/run_brunel_simulation.sh.in @@ -0,0 +1,5 @@ +#!/bin/bash +source @NEST_INSTALL_PREFIX@/bin/nest_vars.sh +export LD_LIBRARY_PATH=$NEST_MODULE_PATH:/usr/local/lib/:$LD_LIBRARY_PATH +# python3 @CMAKE_SOURCE_DIR@/examples/brunel_simulation.py $1 $2 # Uncomment this to run normally +time mpirun -n $3 --mca btl_vader_single_copy_mechanism none --allow-run-as-root -x PYTHONPATH python3 @CMAKE_SOURCE_DIR@/examples/brunel_simulation.py $1 $2 \ No newline at end of file diff --git a/examples/simulation.py b/examples/simulation.py deleted file mode 100644 index 6d770c92d570bbd95d9d3af8ae0bfb16bc4ab272..0000000000000000000000000000000000000000 --- a/examples/simulation.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# -# soundclick_example.py -# -# This file is part of NEST. -# -# Copyright (C) 2004 The NEST Initiative -# -# NEST is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# NEST is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with NEST. If not, see <http://www.gnu.org/licenses/>. - -''' -soundclick recording backend example ------------------------------------- - -Example PyNest script to demonstrate the soundclick recording backend. - -Recorded spike events produce a clicking sound similar to that heard in -electrophysiological recordings. The recording backend makes use of the -SFML (Simple and Fast Multimedia Library) audio module. -This requires libsfml-dev to be installed. To create the illusion of a -realistic sound from an electrophysiological recording, the recording -backend slows down the simulation to biological real time. -''' - -import nest -nest.Install("insitemodule") - -population = nest.Create("izhikevich", 100) -multimeter = nest.Create("multimeter", 1) -ascii_multimeter = nest.Create("multimeter", 1) -nest.SetStatus(multimeter, {"record_from": ["V_m"], "record_to": "insite", }) -nest.SetStatus(ascii_multimeter, {"record_from": [ - "V_m"], "record_to": "ascii", }) -nest.Connect(multimeter, population) -nest.Connect(ascii_multimeter, population) - -# regular spiking -nest.SetStatus(population, {"a": 0.02, - "b": 0.2, - "c": -65.0, - "d": 8.0, - "U_m": 0.0, - "V_m": -75.0, - "I_e": 6.0}) -nest.Simulate(4000) -nest.SetStatus(population, {"I_e": 0.0}) -nest.Simulate(500) - -# fast spiking -nest.SetStatus(population, {"a": 0.1, - "b": 0.2, - "c": -65.0, - "d": 2.0, - "U_m": 0.0, - "V_m": -75.0, - "I_e": 6.0}) -nest.Simulate(4000) -nest.SetStatus(population, {"I_e": 0.0}) -nest.Simulate(500) - -# chattering -nest.SetStatus(population, {"a": 0.02, - "b": 0.2, - "c": -50.0, - "d": 2.0, - "U_m": 0.0, - "V_m": -75.0, - "I_e": 6.0}) -nest.Simulate(4000) diff --git a/http_server.cpp b/http_server.cpp index e5b37fb5185459734b01e18c62ecdf408800824e..e2d2d9c3d16511c5fece8fe4cb0e809ab98c6b58 100644 --- a/http_server.cpp +++ b/http_server.cpp @@ -1,14 +1,29 @@ #include "http_server.hpp" + +#include <algorithm> +#include <iostream> +#include <regex> +#include <unordered_set> + #include "data_storage.hpp" namespace insite { -HttpServer::HttpServer(web::http::uri address, DataStorage* storage) - : http_listener_{address}, storage_(storage) { +HttpServer::HttpServer(web::http::uri address, DataStorage* storage, + std::string database_uri) + : http_listener_{address}, + storage_(storage), + database_uri_(database_uri) { http_listener_.support([this](web::http::http_request request) { if (request.method() == "GET" && request.relative_uri().path() == "/spikes") { request.reply(GetSpikes(request)); + } else if (request.method() == "GET" && + request.relative_uri().path() == "/multimeter_measurement") { + request.reply(GetMultimeterMeasurement(request)); + } else if (request.method() == "GET" && + request.relative_uri().path() == "/current_simulation_time") { + request.reply(GetCurrentSimulationTime(request)); } else { std::cerr << "Invalid request: " << request.to_string() << "\n"; request.reply(web::http::status_codes::NotFound); @@ -19,19 +34,170 @@ HttpServer::HttpServer(web::http::uri address, DataStorage* storage) std::cout << "HTTP server is listening...\n"; } +web::http::http_response HttpServer::GetCurrentSimulationTime( + const web::http::http_request& request) { + web::http::http_response response(web::http::status_codes::OK); + web::json::value simulation_time = storage_->GetCurrentSimulationTime(); + response.set_body(simulation_time); + return response; +} + web::http::http_response HttpServer::GetSpikes( const web::http::http_request& request) { + const auto parameters = web::uri::split_query(request.request_uri().query()); + web::http::http_response response(web::http::status_codes::OK); - const auto spikes = storage_->GetSpikes(); - web::json::value gids = web::json::value::array(spikes.size()); - web::json::value simulation_steps = web::json::value::array(spikes.size()); - for (size_t i = 0; i < spikes.size(); ++i) { - gids[i] = spikes[i].gid; - simulation_steps[i] = spikes[i].simulation_step; + auto spikes = storage_->GetSpikes(); + + const auto from = parameters.find("from"); + const auto to = parameters.find("to"); + const auto population = parameters.find("population"); + + std::unordered_set<uint64_t> population_node_ids; + if (population != parameters.end()) { + pqxx::connection connection(database_uri_); + pqxx::work txn(connection); + const auto population_node_ids_result = txn.exec( + "SELECT id FROM nest_neuron WHERE nest_neuron.population_id = " + + population->second); + txn.commit(); + + population_node_ids.reserve(population_node_ids_result.size()); + + for (const auto& node_id : population_node_ids_result) { + population_node_ids.insert(node_id[0].as<uint64_t>()); + } + + spikes.erase(std::remove_if(spikes.begin(), spikes.end(), [&population_node_ids](const Spike& spike) { + return population_node_ids.count(spike.gid) == 0; + }), spikes.end()); + } + + const auto spike_happened_before = [](const Spike& spike, + double simulation_time) { + return spike.simulation_time < simulation_time; + }; + + auto spikes_begin = spikes.begin(); + auto spikes_end = spikes.end(); + if (from != parameters.end()) { + const auto from_number = std::stoll(from->second); + spikes_begin = std::lower_bound(spikes.begin(), spikes.end(), from_number, + spike_happened_before); + } + + if (to != parameters.end()) { + const auto to_number = std::stoll(to->second); + spikes_end = std::lower_bound(spikes.begin(), spikes.end(), to_number, + spike_happened_before); } + + + const auto element_count = spikes_end - spikes_begin; + web::json::value gids = web::json::value::array(element_count); + web::json::value simulation_times = web::json::value::array(element_count); + + { + size_t index = 0; + for (auto spike = spikes_begin; spike != spikes_end; ++spike, ++index) { + gids[index] = spike->gid; + simulation_times[index] = spike->simulation_time; + } + } + response.set_body(web::json::value::object( - {{"simulation_steps", simulation_steps}, {"neuron_ids", gids}})); + {{"simulation_times", simulation_times}, {"gids", gids}})); return response; } +web::http::http_response HttpServer::GetMultimeterMeasurement( + const web::http::http_request& request) { + web::http::http_response response(web::http::status_codes::OK); + web::json::value body = web::json::value::object(); + + const auto parameters = web::uri::split_query(request.request_uri().query()); + const auto parameter_multimeter_id = parameters.find("multimeter_id"); + const auto parameter_attribute = parameters.find("attribute"); + const auto parameter_from = parameters.find("from"); + const auto parameter_to = parameters.find("to"); + const auto parameter_gids = parameters.find("gids"); + const auto parameter_offset = parameters.find("offset"); + const auto parameter_limit = parameters.find("limit"); + + const auto multimeter_id = std::stoll(parameter_multimeter_id->second); + const auto attribute = parameter_attribute->second; + + auto filter_gids = std::vector<std::uint64_t>(); + if (parameter_gids != parameters.end()) { + std::regex regex{R"([\s,]+)"}; + std::sregex_token_iterator it{parameter_gids->second.begin(), + parameter_gids->second.end(), regex, -1}; + std::vector<std::string> filter_gid_strings{it, {}}; + std::transform(filter_gid_strings.begin(), filter_gid_strings.end(), + std::back_inserter(filter_gids), + [](const std::string& str) { return std::stoll(str); }); + } + std::cout << "Filter GID count: " << filter_gids.size() << "\n"; + + const auto measurements = storage_->GetMultimeterMeasurements(); + if (measurements.find(multimeter_id) != measurements.end() && + measurements.at(multimeter_id).find(attribute) != + measurements.at(multimeter_id).end()) { + auto& measurement = measurements.at(multimeter_id).at(attribute); + auto& simulation_times = measurement.simulation_times; + auto& gids = measurement.gids; + auto& values = measurement.values; + + auto simulation_times_begin = + parameter_from == parameters.end() + ? simulation_times.begin() + : std::lower_bound(simulation_times.begin(), simulation_times.end(), + std::stoll(parameter_from->second)); + auto simulation_times_end = + parameter_to == parameters.end() + ? simulation_times.end() + : std::lower_bound(simulation_times.begin(), simulation_times.end(), + std::stoll(parameter_to->second)); + if (parameter_offset != parameters.end()) + simulation_times_begin += std::stoll(parameter_offset->second); + if (parameter_limit != parameters.end()) + simulation_times_end = + simulation_times_begin + std::stoll(parameter_limit->second); + auto simulation_times_subset = std::vector<web::json::value>( + simulation_times_begin, simulation_times_end); + std::size_t simulation_start_index = + std::distance(simulation_times.begin(), simulation_times_begin); + std::size_t simulation_end_index = + std::distance(simulation_times.begin(), simulation_times_end); + + auto gids_begin = filter_gids.empty() ? gids.begin() : filter_gids.begin(); + auto gids_end = filter_gids.empty() ? gids.end() : filter_gids.end(); + auto gids_subset = std::vector<web::json::value>(gids_begin, gids_end); + auto gid_indices = std::vector<std::size_t>(); + if (!filter_gids.empty()) + for (auto& filter_gid : filter_gids) + gid_indices.push_back(std::distance( + gids.begin(), + std::lower_bound(gids.begin(), gids.end(), filter_gid))); + else { + gid_indices.resize(gids.size()); + std::iota(gid_indices.begin(), gid_indices.end(), 0); + } + + auto values_subset = std::vector<web::json::value>( + simulation_times_subset.size() * gids_subset.size()); + for (std::size_t t = 0, vt = simulation_start_index; + vt < simulation_end_index; ++t, ++vt) + for (std::size_t g = 0; g < gid_indices.size(); ++g) + values_subset[t * gids_subset.size() + g] = + values[vt * gids.size() + gid_indices[g]]; + + body["simulation_times"] = web::json::value::array(simulation_times_subset); + body["gids"] = web::json::value::array(gids_subset); + body["values"] = web::json::value::array(values_subset); + } + + response.set_body(body); + return response; +} } // namespace insite diff --git a/http_server.hpp b/http_server.hpp index bca17999f0c5048137e476a8ee16fa40e1b6250e..90f0bbd92201d251fb72b343fcd1963fd0c55bca 100644 --- a/http_server.hpp +++ b/http_server.hpp @@ -3,6 +3,7 @@ #include <cpprest/http_listener.h> #include <string> +#include <pqxx/pqxx> namespace insite { @@ -10,13 +11,19 @@ class DataStorage; class HttpServer { public: - HttpServer(web::http::uri address, DataStorage* storage); + HttpServer(web::http::uri address, DataStorage* storage, std::string database_uri); private: - DataStorage* storage_; web::http::experimental::listener::http_listener http_listener_; + DataStorage* storage_; + std::string database_uri_; + + web::http::http_response GetCurrentSimulationTime(const web::http::http_request& request); web::http::http_response GetSpikes(const web::http::http_request& request); + + web::http::http_response GetMultimeterMeasurement( + const web::http::http_request& request); }; } // namespace insite diff --git a/insite-nest-module.tar b/insite-nest-module.tar new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/neuron_info.hpp b/neuron_info.hpp new file mode 100644 index 0000000000000000000000000000000000000000..19bf4e3eda6e8de17159f44f177044b446e1118d --- /dev/null +++ b/neuron_info.hpp @@ -0,0 +1,21 @@ +#ifndef NEURON_INFO_HPP +#define NEURON_INFO_HPP + +#include <vector> + +// Includes from nest kernel: +#include <node_collection.h> + +namespace insite { + +struct NeuronInfo { + bool operator<(const NeuronInfo& that) const { return gid < that.gid; } + + nest::index gid; + nest::NodeCollectionPTR gid_collection; + std::vector<double> position; +}; + +} // namespace insite + +#endif diff --git a/recording_backend_insite.cpp b/recording_backend_insite.cpp index c94e87386d29d51fe3d74301645d17e4d90d023b..63a59929870df327f5d3d76b46f6bff45645ca28 100644 --- a/recording_backend_insite.cpp +++ b/recording_backend_insite.cpp @@ -1,20 +1,54 @@ +#include <stdexcept> +#include <string> + // Includes from libnestutil: #include "compose.hpp" // Includes from nestkernel: +#include "kernel_manager.h" #include "recording_device.h" #include "vp_manager_impl.h" +// Includes from topology: +#include "topology.h" + // Includes from sli: #include "dictutils.h" - #include "recording_backend_insite.h" namespace insite { +namespace { + +std::string ReadDatabaseHost() { + std::ifstream host_file("database_host.txt"); + if (host_file.is_open()) { + std::string database_host; + std::getline(host_file, database_host); + return database_host; + } else { + return "database"; + } +} + +} // namespace + RecordingBackendInsite::RecordingBackendInsite() : data_storage_("tgest"), - http_server_("http://0.0.0.0:8000", &data_storage_) {} + database_connection_("postgresql://postgres@" + ReadDatabaseHost()), + http_server_("http://0.0.0.0:" + get_port_string(), &data_storage_, "postgresql://postgres@" + ReadDatabaseHost()) { + pqxx::work txn(database_connection_); + simulation_node_id_ = txn.exec1( + "INSERT INTO nest_simulation_node (address) " + "VALUES ('http://insite-nest-module:" + + get_port_string() + + "') " + "RETURNING id;")[0] + .as<int>(); + std::cout << "Simulation node registered to database. Node ID: " + << simulation_node_id_ << std::endl; + txn.commit(); +} RecordingBackendInsite::~RecordingBackendInsite() throw() {} @@ -29,6 +63,11 @@ void RecordingBackendInsite::finalize() { void RecordingBackendInsite::enroll(const nest::RecordingDevice& device, const DictionaryDatum& params) { std::cout << "RecordingBackendInsite::enroll(" << device.get_label() << ")\n"; + + if (device.get_type() == nest::RecordingDevice::MULTIMETER) { + auto id = device.get_node_id(); + multimeter_infos_.emplace(std::make_pair(id, MultimeterInfo{id, true})); + } } void RecordingBackendInsite::disenroll(const nest::RecordingDevice& device) { @@ -41,6 +80,37 @@ void RecordingBackendInsite::set_value_names( const std::vector<Name>& double_value_names, const std::vector<Name>& long_value_names) { std::cout << "RecordingBackendInsite::set_value_names()\n"; + + if (device.get_type() == nest::RecordingDevice::MULTIMETER) { + auto& multimeter = multimeter_infos_.at(device.get_node_id()); + + std::stringstream multimeter_query; + multimeter_query << "INSERT INTO nest_multimeter (id, attributes) " + << "VALUES (" << device.get_node_id() << ",\'{"; + + bool first = true; + for (auto& name : double_value_names) { + const auto& name_string = name.toString(); + multimeter.double_attributes.push_back(name_string); + + multimeter_query << (first ? "" : ",") << '\"' << name_string << "\""; + first = false; + } + for (auto& name : long_value_names) { + const auto& name_string = name.toString(); + multimeter.long_attributes.push_back(name_string); + + multimeter_query << (first ? "" : ",") << '\"' << name_string << "\""; + first = false; + } + multimeter_query << "}\') ON CONFLICT DO NOTHING;"; + + multimeter.needs_update = true; + + pqxx::work txn(database_connection_); + txn.exec0(multimeter_query.str()); + txn.commit(); + } } void RecordingBackendInsite::prepare() {} @@ -57,15 +127,146 @@ void RecordingBackendInsite::post_run_hook() { std::cout << "RecordingBackendInsite::post_run_hook()\n"; } -void RecordingBackendInsite::post_step_hook() {} +void RecordingBackendInsite::post_step_hook() { + // // Send simulation time + // { + // pqxx::work txn(database_connection_); + // txn.exec0( + // "UPDATE nest_simulation_node " + // "SET current_simulation_time = " + + // std::to_string(latest_simulation_time_) + + // "" + // "WHERE id = " + + // std::to_string(simulation_node_id_)); + // txn.commit(); + // } + data_storage_.SetCurrentSimulationTime(latest_simulation_time_); + + if (new_neuron_infos_.size() > 0) { + std::stringstream neuron_query; + neuron_query << "INSERT INTO nest_neuron (id, simulation_node_id, " + "population_id, position) " + << "VALUES "; + for (auto& neuron_info : new_neuron_infos_) { + const bool first = neuron_info.gid == new_neuron_infos_[0].gid; + if (!first) { + neuron_query << ","; + } + + uint64_t population_id = 0; + for (const nest::NodeIDTriple& node_id_triple : + *neuron_info.gid_collection.get()) { + population_id ^= node_id_triple.node_id * 938831; + } + + neuron_query << "(" << neuron_info.gid << "," << simulation_node_id_ + << "," << population_id % 0x800000; + + const auto position_size = neuron_info.position.size(); + if (position_size > 0) { + assert(position_size <= 3); + neuron_query << ",\'{"; + for (size_t i = 0; i < position_size; ++i) { + if (i > 0) { + neuron_query << ","; + } + neuron_query << neuron_info.position[i]; + } + neuron_query << "}\'"; + } else { + neuron_query << ",NULL"; + } + neuron_query << ")"; + } + neuron_query << ";"; + + pqxx::work txn(database_connection_); + txn.exec0(neuron_query.str()); + txn.commit(); + + neuron_infos_.insert(neuron_infos_.end(), new_neuron_infos_.begin(), + new_neuron_infos_.end()); + std::sort(neuron_infos_.begin(), neuron_infos_.end()); + new_neuron_infos_.clear(); + } + + // Send multimeter info + // for (auto& kvp : multimeter_infos_) { + // auto& multimeter = kvp.second; + // if (!multimeter.needs_update) continue; + // multimeter.needs_update = false; + + // if (multimeter.gids.size() > 0) { + // std::stringstream neuron_multimeter_query; + // neuron_multimeter_query + // << "INSERT INTO nest_neuron_multimeter (neuron_id, multimeter_id) " + // << "VALUES "; + + // for (const auto& neuron_id : multimeter.gids) { + // const bool first = neuron_id == multimeter.gids[0]; + // neuron_multimeter_query << (first ? "" : ",") << "(" << neuron_id << "," + // << multimeter.device_id << ")"; + // } + + // neuron_multimeter_query << " ON CONFLICT DO NOTHING;"; + + // pqxx::work txn(database_connection_); + // txn.exec0(neuron_multimeter_query.str()); + // txn.commit(); + // } + // } +} void RecordingBackendInsite::write(const nest::RecordingDevice& device, const nest::Event& event, const std::vector<double>& double_values, const std::vector<long>& long_values) { + const auto sender_gid = event.get_sender_node_id(); + const auto time_stamp = event.get_stamp().get_ms(); if (device.get_type() == nest::RecordingDevice::SPIKE_DETECTOR) { - data_storage_.AddSpike(event.get_stamp().get_steps(), - event.get_sender_gid()); + data_storage_.AddSpike(time_stamp, sender_gid); + } + if (device.get_type() == nest::RecordingDevice::MULTIMETER) { + auto device_id = device.get_node_id(); + auto& multimeter = multimeter_infos_.at(device_id); + auto& gids = multimeter.gids; + + // If the measurement is from a GID we previously do not know, add. + if (!binary_search(gids.begin(), gids.end(), sender_gid)) { + gids.insert(std::lower_bound(gids.begin(), gids.end(), sender_gid), + sender_gid); + multimeter.needs_update = true; + } + + for (std::size_t i = 0; i < double_values.size(); ++i) + data_storage_.AddMultimeterMeasurement( + device_id, multimeter.double_attributes[i], time_stamp, sender_gid, + double_values[i]); + for (std::size_t i = 0; i < long_values.size(); ++i) + data_storage_.AddMultimeterMeasurement( + device_id, multimeter.long_attributes[i], time_stamp, sender_gid, + double(long_values[i])); + } + latest_simulation_time_ = std::max(latest_simulation_time_, time_stamp); + + NeuronInfo neuron_info; + neuron_info.gid = sender_gid; + if (!binary_search(neuron_infos_.begin(), neuron_infos_.end(), neuron_info) && + !binary_search(new_neuron_infos_.begin(), new_neuron_infos_.end(), + neuron_info)) { + neuron_info.gid_collection = event.get_sender().get_nc(); + + const auto layer = nest::get_layer(neuron_info.gid_collection); + if (layer.get()) { + neuron_info.position = layer->get_position_vector( + neuron_info.gid_collection->find(sender_gid)); + } + + new_neuron_infos_.insert( + std::lower_bound(new_neuron_infos_.begin(), new_neuron_infos_.end(), + neuron_info), + neuron_info); + data_storage_.AddNeuronId(neuron_info.gid); } } @@ -92,4 +293,7 @@ void RecordingBackendInsite::get_device_status( std::cout << "RecordingBackendInsite::get_device_status()\n"; } +std::string RecordingBackendInsite::get_port_string() const { + return std::to_string(8000 + nest::kernel().mpi_manager.get_rank()); +} } // namespace insite diff --git a/recording_backend_insite.h b/recording_backend_insite.h index 38985cf67628492c8805d7aef2102f70d553fe83..93c071e3d9e575e041af79e9cac5ac5c8026b6c9 100644 --- a/recording_backend_insite.h +++ b/recording_backend_insite.h @@ -1,10 +1,17 @@ #ifndef RECORDING_BACKEND_INSITE_H #define RECORDING_BACKEND_INSITE_H +#include <unordered_map> +#include <cpprest/http_client.h> +#include <pqxx/pqxx> + #include "data_storage.hpp" #include "http_server.hpp" - #include "recording_backend.h" +#include "nest_types.h" +#include "node_collection.h" + +#include "neuron_info.hpp" namespace insite { @@ -52,8 +59,16 @@ class RecordingBackendInsite : public nest::RecordingBackend { DictionaryDatum& params) const override; private: + std::string get_port_string() const; + DataStorage data_storage_; + pqxx::connection database_connection_; HttpServer http_server_; + int simulation_node_id_; + std::vector<NeuronInfo> neuron_infos_; + std::vector<NeuronInfo> new_neuron_infos_; + std::unordered_map<nest::index, MultimeterInfo> multimeter_infos_; + double latest_simulation_time_ = 0; }; } // namespace insite diff --git a/test/test_module.py b/test/test_module.py new file mode 100644 index 0000000000000000000000000000000000000000..e16fe6ce14c114a94c94894be652fb8dc449f962 --- /dev/null +++ b/test/test_module.py @@ -0,0 +1,11 @@ +import requests + +def test_spikes(nest_simulation): + r = requests.get("http://localhost:8000/spikes") + spikes = r.json() + assert(len(spikes['gids']) == len(spikes['simulation_times'])) + + previous_time = 0 + for time in spikes['simulation_times']: + assert(time >= previous_time) + previous_time = time diff --git a/wait-for.sh b/wait-for.sh new file mode 100755 index 0000000000000000000000000000000000000000..b1e34c1c0d17809376acfcf05302bd6b0b4897b2 --- /dev/null +++ b/wait-for.sh @@ -0,0 +1,79 @@ +#!/bin/sh + +TIMEOUT=15 +QUIET=0 + +echoerr() { + if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi +} + +usage() { + exitcode="$1" + cat << USAGE >&2 +Usage: + $cmdname host:port [-t timeout] [-- command args] + -q | --quiet Do not output any status messages + -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit "$exitcode" +} + +wait_for() { + for i in `seq $TIMEOUT` ; do + nc -z "$HOST" "$PORT" > /dev/null 2>&1 + + result=$? + if [ $result -eq 0 ] ; then + if [ $# -gt 0 ] ; then + exec "$@" + fi + exit 0 + fi + sleep 1 + done + echo "Operation timed out" >&2 + exit 1 +} + +while [ $# -gt 0 ] +do + case "$1" in + *:* ) + HOST=$(printf "%s\n" "$1"| cut -d : -f 1) + PORT=$(printf "%s\n" "$1"| cut -d : -f 2) + shift 1 + ;; + -q | --quiet) + QUIET=1 + shift 1 + ;; + -t) + TIMEOUT="$2" + if [ "$TIMEOUT" = "" ]; then break; fi + shift 2 + ;; + --timeout=*) + TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + break + ;; + --help) + usage 0 + ;; + *) + echoerr "Unknown argument: $1" + usage 1 + ;; + esac +done + +if [ "$HOST" = "" -o "$PORT" = "" ]; then + echoerr "Error: you need to provide a host and port to test." + usage 2 +fi + +wait_for "$@" \ No newline at end of file