From da3b29af6f8a82d516fa99676015f364c4e2b491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20B=C3=B6ttcher?= Date: Tue, 6 Feb 2024 16:31:00 +0100 Subject: [PATCH 01/12] added first draft of ou_noise_gererator model --- models/ou_noise_generator.cpp | 389 ++++++++++++++++++++++++++++++++++ models/ou_noise_generator.h | 360 +++++++++++++++++++++++++++++++ modelsets/full | 1 + 3 files changed, 750 insertions(+) create mode 100644 models/ou_noise_generator.cpp create mode 100644 models/ou_noise_generator.h diff --git a/models/ou_noise_generator.cpp b/models/ou_noise_generator.cpp new file mode 100644 index 0000000000..492182e32c --- /dev/null +++ b/models/ou_noise_generator.cpp @@ -0,0 +1,389 @@ +/* + * ou_noise_generator.cpp + * + * 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 . + * + */ + +#include "ou_noise_generator.h" + +// Includes from libnestutil: +#include "compose.hpp" +#include "dict_util.h" +#include "logging.h" +#include "numerics.h" + +// Includes from nestkernel: +#include "event_delivery_manager_impl.h" +#include "kernel_manager.h" +#include "nest_impl.h" +#include "universal_data_logger_impl.h" + +// Includes from sli: +#include "dict.h" +#include "dictutils.h" +#include "doubledatum.h" + +namespace nest +{ +void +register_ou_noise_generator( const std::string& name ) +{ + register_node_model< ou_noise_generator >( name ); +} + +RecordablesMap< ou_noise_generator > ou_noise_generator::recordablesMap_; + +template <> +void +RecordablesMap< ou_noise_generator >::create() +{ + insert_( Name( names::I ), &ou_noise_generator::get_I_avg_ ); +} +} + +/* ---------------------------------------------------------------- + * Default constructors defining default parameter + * ---------------------------------------------------------------- */ + +nest::ou_noise_generator::Parameters_::Parameters_() + : mean_( 0.0 ) // pA + , std_( 0.0 ) // pA / sqrt(s) + , tau_( 0.0 ) // ms + , dt_( get_default_dt() ) + , num_targets_( 0 ) +{ +} + +nest::ou_noise_generator::Parameters_::Parameters_( const Parameters_& p ) + : mean_( p.mean_ ) + , std_( p.std_ ) + , tau_( p.tau_ ) + , dt_( p.dt_ ) + , num_targets_( 0 ) // we do not copy connections +{ + if ( dt_.is_step() ) + { + dt_.calibrate(); + } + else + { + dt_ = get_default_dt(); + } +} + +nest::ou_noise_generator::Parameters_& +nest::ou_noise_generator::Parameters_::operator=( const Parameters_& p ) +{ + if ( this == &p ) + { + return *this; + } + + mean_ = p.mean_; + std_ = p.std_; + tau_ = p.tau_; + dt_ = p.dt_; + + return *this; +} + +nest::ou_noise_generator::State_::State_() + : I_avg_( 0.0 ) // pA +{ +} + +nest::ou_noise_generator::Buffers_::Buffers_( ou_noise_generator& n ) + : next_step_( 0 ) + , logger_( n ) +{ +} + +nest::ou_noise_generator::Buffers_::Buffers_( const Buffers_& b, ou_noise_generator& n ) + : next_step_( b.next_step_ ) + , logger_( n ) +{ +} + +/* ---------------------------------------------------------------- + * Parameter extraction and manipulation functions + * ---------------------------------------------------------------- */ + +void +nest::ou_noise_generator::Parameters_::get( DictionaryDatum& d ) const +{ + ( *d )[ names::mean ] = mean_; + ( *d )[ names::std ] = std_; + ( *d )[ names::dt ] = dt_.get_ms(); + ( *d )[ names::tau ] = tau_; +} + +void +nest::ou_noise_generator::State_::get( DictionaryDatum& d ) const +{ +} + +void +nest::ou_noise_generator::Parameters_::set( const DictionaryDatum& d, const ou_noise_generator& n, Node* node ) +{ + updateValueParam< double >( d, names::mean, mean_, node ); + updateValueParam< double >( d, names::std, std_, node ); + updateValueParam< double >( d, names::tau, tau_, node ); + double dt; + if ( updateValueParam< double >( d, names::dt, dt, node ) ) + { + dt_ = Time::ms( dt ); + } + if ( std_ < 0 ) + { + throw BadProperty( "The standard deviation cannot be negative." ); + } + + if ( not dt_.is_step() ) + { + throw StepMultipleRequired( n.get_name(), names::dt, dt_ ); + } +} + + +/* ---------------------------------------------------------------- + * Default and copy constructor for node + * ---------------------------------------------------------------- */ + +nest::ou_noise_generator::ou_noise_generator() + : StimulationDevice() + , P_() + , S_() + , B_( *this ) +{ + recordablesMap_.create(); +} + +nest::ou_noise_generator::ou_noise_generator( const ou_noise_generator& n ) + : StimulationDevice( n ) + , P_( n.P_ ) + , S_( n.S_ ) + , B_( n.B_, *this ) +{ +} + + +/* ---------------------------------------------------------------- + * Node initialization functions + * ---------------------------------------------------------------- */ + +void +nest::ou_noise_generator::init_state_() +{ + StimulationDevice::init_state(); +} + +void +nest::ou_noise_generator::init_buffers_() +{ + StimulationDevice::init_buffers(); + B_.logger_.reset(); + + B_.next_step_ = 0; + B_.amps_.clear(); + B_.amps_.resize( P_.num_targets_, 0.0 ); +} + +void +nest::ou_noise_generator::pre_run_hook() +{ + B_.logger_.init(); + + StimulationDevice::pre_run_hook(); + if ( P_.num_targets_ != B_.amps_.size() ) + { + LOG( M_INFO, "ou_noise_generator::pre_run_hook()", "The number of targets has changed, drawing new amplitudes." ); + init_buffers_(); + } + + V_.dt_steps_ = P_.dt_.get_steps(); + + const double h = Time::get_resolution().get_ms(); + const double t = kernel().simulation_manager.get_time().get_ms(); + + // scale Hz to ms + const double noise_amp = P_.std_ * std::sqrt(-1 * std::expm1(-2 * h / P_.tau_)); + const double prop = std::exp(-1 * h / P_.tau_); + const double tau_inv = h / P_.tau_; + std::cout << "I am in pre run" << std::endl; + std::cout << P_.tau_ << std::endl; + std::cout << h << std::endl; + + + V_.noise_amp_ = noise_amp; + V_.prop_ = prop; + V_.tau_inv_ = tau_inv; + +} + + +/* ---------------------------------------------------------------- + * Update function and event hook + * ---------------------------------------------------------------- */ + +size_t +nest::ou_noise_generator::send_test_event( Node& target, size_t receptor_type, synindex syn_id, bool dummy_target ) +{ + StimulationDevice::enforce_single_syn_type( syn_id ); + + if ( dummy_target ) + { + DSCurrentEvent e; + e.set_sender( *this ); + return target.handles_test_event( e, receptor_type ); + } + else + { + CurrentEvent e; + e.set_sender( *this ); + const size_t p = target.handles_test_event( e, receptor_type ); + if ( p != invalid_port and not is_model_prototype() ) + { + ++P_.num_targets_; + } + return p; + } +} + +// +// Time Evolution Operator +// +void +nest::ou_noise_generator::update( Time const& origin, const long from, const long to ) +{ + const long start = origin.get_steps(); + + for ( long offs = from; offs < to; ++offs ) + { + + const long now = start + offs; + + if ( not StimulationDevice::is_active( Time::step( now ) ) ) + { + B_.logger_.record_data( origin.get_steps() + offs ); + continue; + } + + // >= in case we woke from inactivity + if ( now >= B_.next_step_ ) + { + // std::cout << "I am in update" << std::endl; + // compute new currents + for ( double& amp : B_.amps_ ) + { + amp = P_.mean_ * V_.tau_inv_ + S_.I_avg_ * V_.prop_ + V_.noise_amp_ * V_.normal_dist_( get_vp_specific_rng( get_thread() ) ); + std::cout << amp << std::endl; + std::cout << S_.I_avg_ - P_.mean_ << std::endl; + std::cout << S_.I_avg_ << std::endl; + std::cout << P_.mean_ << std::endl; + std::cout << V_.prop_ << std::endl; + std::cout << V_.noise_amp_ << std::endl; + } + // use now as reference, in case we woke up from inactive period + B_.next_step_ = now + V_.dt_steps_; + } + + // record values + int i = 0; + for ( double& amp : B_.amps_ ) + { + i++; + std::cout << "record values" << std::endl; + std::cout << amp << std::endl; + S_.I_avg_ = amp; + std::cout << S_.I_avg_ << std::endl; + } + std::cout << i << std::endl; + + // S_.I_avg_ /= std::max( 1, int( B_.amps_.size() ) ); + B_.logger_.record_data( origin.get_steps() + offs ); + + DSCurrentEvent ce; + kernel().event_delivery_manager.send( *this, ce, offs ); + } +} + +void +nest::ou_noise_generator::event_hook( DSCurrentEvent& e ) +{ + // get port number + const size_t prt = e.get_port(); + + // we handle only one port here, get reference to vector elem + assert( prt < B_.amps_.size() ); + + e.set_current( B_.amps_[ prt ] ); + e.get_receiver().handle( e ); +} + +void +nest::ou_noise_generator::handle( DataLoggingRequest& e ) +{ + B_.logger_.handle( e ); +} + +/* ---------------------------------------------------------------- + * Other functions + * ---------------------------------------------------------------- */ + +void +nest::ou_noise_generator::set_data_from_stimulation_backend( std::vector< double >& input_param ) +{ + Parameters_ ptmp = P_; // temporary copy in case of errors + ptmp.num_targets_ = P_.num_targets_; + + // For the input backend + if ( not input_param.empty() ) + { + if ( input_param.size() != 3 ) + { + throw BadParameterValue( + "The size of the data for the ou_noise_generator needs to be 3 [mean, std, tau]." ); + } + DictionaryDatum d = DictionaryDatum( new Dictionary ); + ( *d )[ names::mean ] = DoubleDatum( input_param[ 0 ] ); + ( *d )[ names::std ] = DoubleDatum( input_param[ 1 ] ); + ( *d )[ names::tau ] = DoubleDatum( input_param[ 2 ] ); + ptmp.set( d, *this, this ); + } + + // if we get here, temporary contains consistent set of properties + P_ = ptmp; + P_.num_targets_ = ptmp.num_targets_; +} + +void +nest::ou_noise_generator::calibrate_time( const TimeConverter& tc ) +{ + if ( P_.dt_.is_step() ) + { + P_.dt_ = tc.from_old_tics( P_.dt_.get_tics() ); + } + else + { + const double old = P_.dt_.get_ms(); + P_.dt_ = P_.get_default_dt(); + std::string msg = String::compose( "Default for dt changed from %1 to %2 ms", old, P_.dt_.get_ms() ); + LOG( M_INFO, get_name(), msg ); + } +} diff --git a/models/ou_noise_generator.h b/models/ou_noise_generator.h new file mode 100644 index 0000000000..87338c23fb --- /dev/null +++ b/models/ou_noise_generator.h @@ -0,0 +1,360 @@ +/* + * ou_noise_generator.h + * + * 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 . + * + */ + +#ifndef OU_NOISE_GENERATOR_H +#define OU_NOISE_GENERATOR_H + +// C++ includes: +#include + +// Includes from nestkernel: +#include "connection.h" +#include "device_node.h" +#include "event.h" +#include "nest_timeconverter.h" +#include "nest_types.h" +#include "random_generators.h" +#include "stimulation_device.h" +#include "universal_data_logger.h" + +namespace nest +{ + +/* BeginUserDocs: device, generator + +Short description ++++++++++++++++++ + +Generate a!!!!!!!j Gaussian white noise current + +Description ++++++++++++ + +The `ou_noise_generator` can be used to inject a Gaussian "white" noise current into a node. + +The current is not truly white, but a piecewise constant current with a Gaussian distributed +amplitude with mean :math:`\mu` and standard deviation :math:`\sigma`. The current changes at +a user-defined interval :math:`\delta` and is given by + +.. math:: + + I(t) = \mu + N_j \sigma \quad \text{for} \quad t_0 + j \delta < t \leq t_0 + (j+1) \delta \;, + +where :math:`N_j` are Gaussian random numbers with unit standard deviation and :math:`t_0` is +the device onset time. + +Additionally a sinusodially modulated term can be added to the standard +deviation of the noise: + +.. math:: + + I(t) = \mu + N_j \sqrt{\sigma^2 + \sigma_{\text{mod}}^2 \sin(\omega t + \phi)} + \quad \text{for} \quad t_0 + j \delta < t \leq t_0 + (j+1) \delta \;. + +The effect of the noise current on a neuron depends on the switching interval :math:`\delta`. +For a leaky integrate-and-fire neuron with time constant :math:`\tau_m` and capacitance +:math:`C_m`, the variance of the membrane potential is given by + +.. math:: + + \Sigma^2 = \frac{\delta \tau_m \sigma^2}{2 C_m^2} + +for :math:`\delta \ll \tau_m`. For details, see the `noise generator notebook +<../model_details/ou_noise_generator.ipynb>`_. + +All targets of a noise generator receive different currents, but the currents for all +targets change at the same points in time. The interval :math:`\delta` between +changes must be a multiple of the time step. + +.. admonition:: Recording the generated current + + You can use a :doc:`multimeter ` to record the average current sent to all targets for each time step + if simulating on a single thread; multiple MPI processes with one thread each also work. In this case, + the recording interval of the multimeter should be equal to the simulation resolution to avoid confusing effects + due to offset or drift between the recording times of the multimeter and the switching times of the + noise generator. In multi-threaded mode, recording of noise currents is prohibited for technical reasons. + + +.. include:: ../models/stimulation_device.rst + +mean + The mean value :math:`\mu` of the noise current (pA) + +std + The standard deviation :math:`\sigma` of the noise current (pA) + +dt + The interval :math:`\delta` between changes in current (ms; default: 10 * resolution) + +std_mod + The modulation :math:`\sigma_{\text{mod}}` of the standard deviation of the noise current (pA) + +frequency + The frequency of the sine modulation (Hz) + +phase + The phase of sine modulation (0–360 deg) + + +Setting parameters from a stimulation backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The parameters in this stimulation device can be updated with input +coming from a stimulation backend. The data structure used for the +update holds one value for each of the parameters mentioned above. +The indexing is as follows: + + 0. mean + 1. std + 2. std_mod + 3. frequency + 4. phase + +Sends ++++++ + +CurrentEvent + +See also +++++++++ + +step_current_generator + +Examples using this model ++++++++++++++++++++++++++ + +.. listexamples:: ou_noise_generator + +EndUserDocs */ + +void register_ou_noise_generator( const std::string& name ); + +class ou_noise_generator : public StimulationDevice +{ + +public: + ou_noise_generator(); + ou_noise_generator( const ou_noise_generator& ); + + + //! Allow multimeter to connect to local instances + bool local_receiver() const override; + + /** + * Import sets of overloaded virtual functions. + * @see Technical Issues / Virtual Functions: Overriding, Overloading, and + * Hiding + */ + using Node::event_hook; + using Node::handle; + using Node::handles_test_event; + using Node::sends_signal; + + size_t send_test_event( Node&, size_t, synindex, bool ) override; + + SignalType sends_signal() const override; + + void handle( DataLoggingRequest& ) override; + + size_t handles_test_event( DataLoggingRequest&, size_t ) override; + + void get_status( DictionaryDatum& ) const override; + void set_status( const DictionaryDatum& ) override; + + void calibrate_time( const TimeConverter& tc ) override; + + StimulationDevice::Type get_type() const override; + void set_data_from_stimulation_backend( std::vector< double >& input_param ) override; + +private: + void init_state_() override; + void init_buffers_() override; + + /** + * Recalculates parameters and forces reinitialization + * of amplitudes if number of targets has changed. + */ + void pre_run_hook() override; + + void update( Time const&, const long, const long ) override; + void event_hook( DSCurrentEvent& ) override; + + // ------------------------------------------------------------ + + typedef std::vector< double > AmpVec_; + + /** + * Store independent parameters of the model. + */ + struct Parameters_ + { + double mean_; //!< mean current, in pA + double std_; //!< standard deviation of current, in pA + double tau_; //!< Standard frequency in Hz + Time dt_; //!< time interval between updates + + /** + * Number of targets. + * This is a hidden parameter; must be placed in parameters, + * even though it is an implementation detail, since it + * concerns the connections and must not be affected by resets. + */ + size_t num_targets_; + + Parameters_(); //!< Sets default parameter values + Parameters_( const Parameters_& ); + Parameters_& operator=( const Parameters_& p ); + + void get( DictionaryDatum& ) const; //!< Store current values in dictionary + //! Set values from dictionary + void set( const DictionaryDatum&, const ou_noise_generator&, Node* node ); + + Time get_default_dt(); + }; + + // ------------------------------------------------------------ + + struct State_ + { + double I_avg_; //!< Average of instantaneous currents computed + //!< Used for recording current + + State_(); //!< Sets default parameter values + + void get( DictionaryDatum& ) const; //!< Store current values in dictionary + }; + + // ------------------------------------------------------------ + + // The next two classes need to be friends to access the State_ class/member + friend class RecordablesMap< ou_noise_generator >; + friend class UniversalDataLogger< ou_noise_generator >; + + // ------------------------------------------------------------ + + struct Buffers_ + { + long next_step_; //!< time step of next change in current + AmpVec_ amps_; //!< amplitudes, one per target + explicit Buffers_( ou_noise_generator& ); + Buffers_( const Buffers_&, ou_noise_generator& ); + UniversalDataLogger< ou_noise_generator > logger_; + }; + + // ------------------------------------------------------------ + + struct Variables_ + { + normal_distribution normal_dist_; //!< normal distribution + + long dt_steps_; //!< update interval in steps + double prop_; //!< frequency [radian/s] + double noise_amp_; //!< + double tau_inv_; //!< + + }; + + double + get_I_avg_() const + { + return S_.I_avg_; + } + + // ------------------------------------------------------------ + + static RecordablesMap< ou_noise_generator > recordablesMap_; + + Parameters_ P_; + State_ S_; + Buffers_ B_; + Variables_ V_; +}; + +inline size_t +ou_noise_generator::handles_test_event( DataLoggingRequest& dlr, size_t receptor_type ) +{ + if ( kernel().vp_manager.get_num_threads() > 1 ) + { + throw KernelException( "Recording from a ou_noise_generator is only possible in single-threaded mode." ); + } + + if ( receptor_type != 0 ) + { + throw UnknownReceptorType( receptor_type, get_name() ); + } + return B_.logger_.connect_logging_device( dlr, recordablesMap_ ); +} + +inline void +ou_noise_generator::get_status( DictionaryDatum& d ) const +{ + P_.get( d ); + S_.get( d ); + StimulationDevice::get_status( d ); + + ( *d )[ names::recordables ] = recordablesMap_.get_list(); +} + +inline void +ou_noise_generator::set_status( const DictionaryDatum& d ) +{ + Parameters_ ptmp = P_; // temporary copy in case of errors + ptmp.num_targets_ = P_.num_targets_; // Copy Constr. does not copy connections + ptmp.set( d, *this, this ); // throws if BadProperty + + // We now know that ptmp is consistent. We do not write it back + // to P_ before we are also sure that the properties to be set + // in the parent class are internally consistent. + StimulationDevice::set_status( d ); + + // if we get here, temporaries contain consistent set of properties + P_ = ptmp; + P_.num_targets_ = ptmp.num_targets_; +} + +inline SignalType +ou_noise_generator::sends_signal() const +{ + return ALL; +} + +inline bool +ou_noise_generator::local_receiver() const +{ + return true; +} + +inline StimulationDevice::Type +ou_noise_generator::get_type() const +{ + return StimulationDevice::Type::CURRENT_GENERATOR; +} + +inline Time +ou_noise_generator::Parameters_::get_default_dt() +{ + return 10 * Time::get_resolution(); +} + +} // namespace + +#endif // OU_NOISE_GENERATOR_H diff --git a/modelsets/full b/modelsets/full index 46d6c6ee5a..9d2821603d 100644 --- a/modelsets/full +++ b/modelsets/full @@ -89,6 +89,7 @@ music_rate_in_proxy music_rate_out_proxy music_message_in_proxy noise_generator +ou_noise_generator parrot_neuron parrot_neuron_ps inhomogeneous_poisson_generator From 82b48c057bc756e95a80f9ce69ffc5139f57763c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20B=C3=B6ttcher?= Date: Fri, 23 Feb 2024 14:32:11 +0100 Subject: [PATCH 02/12] add unfinished ou_noise_generator test --- .../test_ou_noise_generator.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py diff --git a/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py new file mode 100644 index 0000000000..cfc612924b --- /dev/null +++ b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# +# test_noise_generator.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 . + +""" +Tests parameter setting and statistical correctness for one application. +""" + +import nest +import numpy as np +import pytest + + +@pytest.fixture +def prepare_kernel(): + nest.ResetKernel() + nest.resolution = 0.1 + + +def test_ou_noise_generator_set_parameters(prepare_kernel): + params = {"mean": 210.0, "std": 60.0, "dt": 0.1} + + oung1 = nest.Create("ou_noise_generator") + oung1.set(params) + + nest.SetDefaults("ou_noise_generator", params) + + oung2 = nest.Create("ou_noise_generator") + assert oung1.get(params) == oung2.get(params) + + +def test_ou_noise_generator_incorrect_noise_dt(prepare_kernel): + with pytest.raises(nest.kernel.NESTError, match="StepMultipleRequired"): + nest.Create("ou_noise_generator", {"dt": 0.25}) + + +def test_ou_noise_generator_(prepare_kernel): + # run for resolution dt=0.1 project to iaf_psc_alpha. + # create 100 repetitions of 1000ms simulations + # collect membrane potential at end + ng = nest.Create("ou_noise_generator") + neuron = nest.Create("iaf_psc_alpha") + nest.Connect(ng, neuron) + + ng.set({"mean": 0.0, "std": 1.0, "dt": 0.1}) + + # no spiking, all parameters 1, 0 leak potential + neuron.set({"V_th": 1e10, "C_m": 1.0, "tau_m": 1.0, "E_L": 0.0}) + + # Simulate for 100 times + n_sims = 100 + v_m_arr = np.empty(n_sims) + for i in range(n_sims): + nest.Simulate(1000.0) + v_m_arr[i] = neuron.get("V_m") + + # Mean and std of membrane potentials + vm_mean = np.mean(v_m_arr) + vm_std = np.std(v_m_arr) + + # Expected mean and std values + expected_vm_mean = 0.0 + exp_dt = np.exp(-ng.get("dt") / neuron.get("tau_m")) + expected_vm_std = np.sqrt((1 + exp_dt) / (1 - exp_dt)) * ng.get("std") + expected_vm_std_std = expected_vm_std / np.sqrt(2) + + # require mean within 3 std dev, std dev within three std dev of std dev + assert np.abs(vm_mean - expected_vm_mean) < 3 * expected_vm_std + assert np.abs(vm_std - expected_vm_std) < 3 * expected_vm_std_std + + +def test_ou_noise_generator_variance(prepare_kernel): + # run for resolution dt=0.1 project to iaf_psc_alpha. + # create 100 repetitions of 1000ms simulations + # collect membrane potential at end + oung = nest.Create("ou_noise_generator") + neuron = nest.Create("iaf_psc_alpha") + # we need to connect to a neuron otherwise the generator does not generate + nest.Connect(oung, neuron) + + oung.set({"mean": 0.0, "std": 1.0, "dt": 0.1}) + + # no spiking, all parameters 1, 0 leak potential + # neuron.set({"V_th": 1e10, "C_m": 1.0, "tau_m": 1.0, "E_L": 0.0}) + + # Simulate for 100 times + n_sims = 100 + v_m_arr = np.empty(n_sims) + for i in range(n_sims): + mm = nest.Create('multimeter', 1, {'record_from':['I']}) + nest.Connect(mm, oung) + nest.Simulate(1000.0) + + ou_current = mm.get('events')['I'] + var_actual = np.var(ou_current) + var_expected = oung.std**2 + + # change this to check if the expected variance is close to the actual one + + ''' + # require mean within 3 std dev, std dev within three std dev of std dev + assert np.abs(vm_mean - expected_vm_mean) < 3 * expected_vm_std + assert np.abs(vm_std - expected_vm_std) < 3 * expected_vm_std_std + ''' From 500c33285d5b512823451d27f23a28501fe5382a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20B=C3=B6ttcher?= Date: Tue, 27 Feb 2024 11:21:45 +0100 Subject: [PATCH 03/12] bug fix in ou_noise_generator --- models/ou_noise_generator.cpp | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/models/ou_noise_generator.cpp b/models/ou_noise_generator.cpp index 492182e32c..1cf60023d5 100644 --- a/models/ou_noise_generator.cpp +++ b/models/ou_noise_generator.cpp @@ -225,10 +225,6 @@ nest::ou_noise_generator::pre_run_hook() const double noise_amp = P_.std_ * std::sqrt(-1 * std::expm1(-2 * h / P_.tau_)); const double prop = std::exp(-1 * h / P_.tau_); const double tau_inv = h / P_.tau_; - std::cout << "I am in pre run" << std::endl; - std::cout << P_.tau_ << std::endl; - std::cout << h << std::endl; - V_.noise_amp_ = noise_amp; V_.prop_ = prop; @@ -291,31 +287,21 @@ nest::ou_noise_generator::update( Time const& origin, const long from, const lon // compute new currents for ( double& amp : B_.amps_ ) { - amp = P_.mean_ * V_.tau_inv_ + S_.I_avg_ * V_.prop_ + V_.noise_amp_ * V_.normal_dist_( get_vp_specific_rng( get_thread() ) ); - std::cout << amp << std::endl; - std::cout << S_.I_avg_ - P_.mean_ << std::endl; - std::cout << S_.I_avg_ << std::endl; - std::cout << P_.mean_ << std::endl; - std::cout << V_.prop_ << std::endl; - std::cout << V_.noise_amp_ << std::endl; + amp = P_.mean_ * V_.tau_inv_ + + amp * V_.prop_ + V_.noise_amp_ + * V_.normal_dist_( get_vp_specific_rng( get_thread() ) ); } // use now as reference, in case we woke up from inactive period B_.next_step_ = now + V_.dt_steps_; } // record values - int i = 0; for ( double& amp : B_.amps_ ) { - i++; - std::cout << "record values" << std::endl; - std::cout << amp << std::endl; - S_.I_avg_ = amp; - std::cout << S_.I_avg_ << std::endl; + S_.I_avg_ += amp; } - std::cout << i << std::endl; - // S_.I_avg_ /= std::max( 1, int( B_.amps_.size() ) ); + S_.I_avg_ /= std::max( 1, int( B_.amps_.size() ) ); B_.logger_.record_data( origin.get_steps() + offs ); DSCurrentEvent ce; From 1f02f0bfe98d0dc9ee6d1dac06b73591f9cc830c Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Thu, 31 Jul 2025 13:07:54 +0200 Subject: [PATCH 04/12] Fix minor error --- models/ou_noise_generator.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/models/ou_noise_generator.cpp b/models/ou_noise_generator.cpp index 1cf60023d5..9fe7fe7b42 100644 --- a/models/ou_noise_generator.cpp +++ b/models/ou_noise_generator.cpp @@ -224,7 +224,7 @@ nest::ou_noise_generator::pre_run_hook() // scale Hz to ms const double noise_amp = P_.std_ * std::sqrt(-1 * std::expm1(-2 * h / P_.tau_)); const double prop = std::exp(-1 * h / P_.tau_); - const double tau_inv = h / P_.tau_; + const double tau_inv =-std::expm1(-h / P_.tau_); V_.noise_amp_ = noise_amp; V_.prop_ = prop; @@ -296,6 +296,8 @@ nest::ou_noise_generator::update( Time const& origin, const long from, const lon } // record values + S_.I_avg_ = 0.0; + for ( double& amp : B_.amps_ ) { S_.I_avg_ += amp; From 8be0909f4cd8b33113c34cc6cfd7ee7ddcedc636 Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Thu, 31 Jul 2025 14:55:20 +0200 Subject: [PATCH 05/12] fix and add tests --- .../test_ou_noise_generator.py | 143 +++++++++++------- 1 file changed, 92 insertions(+), 51 deletions(-) diff --git a/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py index cfc612924b..ce2e406b2b 100644 --- a/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py +++ b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py @@ -26,6 +26,7 @@ import nest import numpy as np import pytest +from scipy.signal import fftconvolve @pytest.fixture @@ -33,6 +34,17 @@ def prepare_kernel(): nest.ResetKernel() nest.resolution = 0.1 +def burn_in_start(dt, tau, k=10.0): + """ + Compute the array index corresponding to k*tau of warm-up. + dt : simulation time step (ms) + tau : OU time constant (ms) + k : how many taus to discard (default 10) + """ + return int(round((k * tau) / dt)) + + + def test_ou_noise_generator_set_parameters(prepare_kernel): params = {"mean": 210.0, "std": 60.0, "dt": 0.1} @@ -50,72 +62,101 @@ def test_ou_noise_generator_incorrect_noise_dt(prepare_kernel): with pytest.raises(nest.kernel.NESTError, match="StepMultipleRequired"): nest.Create("ou_noise_generator", {"dt": 0.25}) - -def test_ou_noise_generator_(prepare_kernel): +def test_ou_noise_generator_statistics(prepare_kernel): # run for resolution dt=0.1 project to iaf_psc_alpha. # create 100 repetitions of 1000ms simulations # collect membrane potential at end - ng = nest.Create("ou_noise_generator") - neuron = nest.Create("iaf_psc_alpha") - nest.Connect(ng, neuron) + #nest.rng_seed = 202 - ng.set({"mean": 0.0, "std": 1.0, "dt": 0.1}) + #oung = nest.Create("ou_noise_generator") + oung = nest.Create('ou_noise_generator', 1, {'mean':200.0, 'std': 60.0, 'tau':10, 'dt':0.1}) + neuron = nest.Create("iaf_psc_alpha") + # we need to connect to a neuron otherwise the generator does not generate + nest.Connect(oung, neuron) - # no spiking, all parameters 1, 0 leak potential - neuron.set({"V_th": 1e10, "C_m": 1.0, "tau_m": 1.0, "E_L": 0.0}) + mm = nest.Create('multimeter', 1, {'record_from':['I']}) + nest.Connect(mm, oung, syn_spec={'weight': 1}) # Simulate for 100 times n_sims = 100 - v_m_arr = np.empty(n_sims) + ou_current = np.empty(n_sims) for i in range(n_sims): nest.Simulate(1000.0) - v_m_arr[i] = neuron.get("V_m") - - # Mean and std of membrane potentials - vm_mean = np.mean(v_m_arr) - vm_std = np.std(v_m_arr) - # Expected mean and std values - expected_vm_mean = 0.0 - exp_dt = np.exp(-ng.get("dt") / neuron.get("tau_m")) - expected_vm_std = np.sqrt((1 + exp_dt) / (1 - exp_dt)) * ng.get("std") - expected_vm_std_std = expected_vm_std / np.sqrt(2) + ou_current[i] = mm.get('events')['I'][-1] - # require mean within 3 std dev, std dev within three std dev of std dev - assert np.abs(vm_mean - expected_vm_mean) < 3 * expected_vm_std - assert np.abs(vm_std - expected_vm_std) < 3 * expected_vm_std_std - - -def test_ou_noise_generator_variance(prepare_kernel): - # run for resolution dt=0.1 project to iaf_psc_alpha. - # create 100 repetitions of 1000ms simulations - # collect membrane potential at end - oung = nest.Create("ou_noise_generator") - neuron = nest.Create("iaf_psc_alpha") - # we need to connect to a neuron otherwise the generator does not generate - nest.Connect(oung, neuron) - oung.set({"mean": 0.0, "std": 1.0, "dt": 0.1}) + curr_mean = np.mean(ou_current) + curr_var = np.var(ou_current) + expected_curr_mean = oung.mean + expected_curr_var = oung.std**2 - # no spiking, all parameters 1, 0 leak potential - # neuron.set({"V_th": 1e10, "C_m": 1.0, "tau_m": 1.0, "E_L": 0.0}) + # require mean within 3 std dev, std dev within 0.2 std dev + assert np.abs(curr_mean - expected_curr_mean) < 3 * oung.std + assert np.abs(curr_var - expected_curr_var) < 0.2 * curr_var - # Simulate for 100 times - n_sims = 100 - v_m_arr = np.empty(n_sims) - for i in range(n_sims): - mm = nest.Create('multimeter', 1, {'record_from':['I']}) - nest.Connect(mm, oung) - nest.Simulate(1000.0) - ou_current = mm.get('events')['I'] - var_actual = np.var(ou_current) - var_expected = oung.std**2 +def test_ou_noise_generator_autocorrelation(): + nest.rng_seed = 6789 - # change this to check if the expected variance is close to the actual one + dt = 1.0 + tau = 1.0 - ''' - # require mean within 3 std dev, std dev within three std dev of std dev - assert np.abs(vm_mean - expected_vm_mean) < 3 * expected_vm_std - assert np.abs(vm_std - expected_vm_std) < 3 * expected_vm_std_std - ''' + oung = nest.Create('ou_noise_generator', 1, {'mean':0.0, 'std': 60.0, 'tau':tau,'dt': nest.resolution}) + neuron = nest.Create("iaf_psc_alpha") + # we need to connect to a neuron otherwise the generator does not generate + nest.Connect(oung, neuron) + mm = nest.Create('multimeter', 1, {'record_from':['I']}) + nest.Connect(mm, oung, syn_spec={'weight': 1}) + nest.Simulate(10000.0) + + + ou_current = mm.get('events')['I'] + + ## drop first k*tau of data + cutoff = burn_in_start(dt=dt, tau=tau, k=10.0) + x = ou_current[cutoff:] + x -= x.mean() + + # empirical lag-1 autocorrelation + emp_ac1 = np.dot(x[:-1], x[1:]) / ((len(x)-1)*x.var()) + # theoretical lag-1: exp(-dt/tau) + theor_ac1 = np.exp(-dt / tau) + + + assert abs(emp_ac1 - theor_ac1) < 0.05, \ + f"autocorr {emp_ac1:.3f} vs theoretical {theor_ac1:.3f}" + +def _crosscorr_between_two_neurons(V_mean, V_std, dt, tau_m, t_max): + nest.ResetKernel(); nest.resolution = dt + ou = nest.Create("ou_noise_generator", 1, + {"mean": V_mean, "std": V_std, "tau": tau_m, "dt": dt}) + neurons = nest.Create("iaf_psc_alpha", 2, + {"E_L": 0.0, "V_th": 1e9, "C_m": 1.0, "tau_m": tau_m}) + nest.Connect(ou, neurons) + mm = nest.Create("multimeter", 1, {"record_from": ["V_m"], "interval": dt}) + nest.Connect(mm, neurons) + nest.Simulate(t_max) + ev = mm.get("events") + senders = np.asarray(ev["senders"], int) + times = np.asarray(ev["times"]) + vms = np.asarray(ev["V_m"]) + t_steps = int(round(t_max / dt)) + 1 + V = np.zeros((t_steps, 2)) + for t, s, v in zip(times, senders, vms): + V[int(round(t / dt)), neurons.index(s)] = v + start = burn_in_start(dt, tau_m) + X1 = V[start:, 0] - V[start:, 0].mean() + X2 = V[start:, 1] - V[start:, 1].mean() + corr = fftconvolve(X1, X2[::-1], mode="full") + corr /= np.sqrt(np.dot(X1, X1) * np.dot(X2, X2)) + mid = corr.size // 2 + return corr[mid:] + + +def test_ou_noise_generator_cross_correlation(prepare_kernel): + nest.rng_seed = 2321 + crosscorr = _crosscorr_between_two_neurons( + V_mean=0.0, V_std=60.0, dt=nest.resolution, tau_m=1.0, t_max=30_000.0 + ) + assert float(np.max(np.abs(crosscorr))) < 0.05 \ No newline at end of file From d58385288ae0c3d4f580da5a30fc4f0383a715c7 Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Thu, 31 Jul 2025 17:45:57 +0200 Subject: [PATCH 06/12] cleanup tests --- .../test_ou_noise_generator.py | 102 +++++++++--------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py index ce2e406b2b..0f3bb0fa97 100644 --- a/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py +++ b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py @@ -34,18 +34,10 @@ def prepare_kernel(): nest.ResetKernel() nest.resolution = 0.1 -def burn_in_start(dt, tau, k=10.0): - """ - Compute the array index corresponding to k*tau of warm-up. - dt : simulation time step (ms) - tau : OU time constant (ms) - k : how many taus to discard (default 10) - """ +def burn_in_start(dt, tau, k=10): + """Return number of steps to discard for a k*tau burn-in.""" return int(round((k * tau) / dt)) - - - def test_ou_noise_generator_set_parameters(prepare_kernel): params = {"mean": 210.0, "std": 60.0, "dt": 0.1} @@ -62,19 +54,16 @@ def test_ou_noise_generator_incorrect_noise_dt(prepare_kernel): with pytest.raises(nest.kernel.NESTError, match="StepMultipleRequired"): nest.Create("ou_noise_generator", {"dt": 0.25}) -def test_ou_noise_generator_statistics(prepare_kernel): +def test_ou_noise_mean_and_variance(prepare_kernel): # run for resolution dt=0.1 project to iaf_psc_alpha. # create 100 repetitions of 1000ms simulations - # collect membrane potential at end - #nest.rng_seed = 202 - #oung = nest.Create("ou_noise_generator") - oung = nest.Create('ou_noise_generator', 1, {'mean':200.0, 'std': 60.0, 'tau':10, 'dt':0.1}) + oung = nest.Create('ou_noise_generator', {'mean':0.0, 'std': 60.0, 'tau':1., 'dt':0.1}) neuron = nest.Create("iaf_psc_alpha") + # we need to connect to a neuron otherwise the generator does not generate nest.Connect(oung, neuron) - - mm = nest.Create('multimeter', 1, {'record_from':['I']}) + mm = nest.Create('multimeter', 1, {'record_from':['I'], "interval": 0.1}) nest.Connect(mm, oung, syn_spec={'weight': 1}) # Simulate for 100 times @@ -82,10 +71,8 @@ def test_ou_noise_generator_statistics(prepare_kernel): ou_current = np.empty(n_sims) for i in range(n_sims): nest.Simulate(1000.0) - ou_current[i] = mm.get('events')['I'][-1] - curr_mean = np.mean(ou_current) curr_var = np.var(ou_current) expected_curr_mean = oung.mean @@ -96,67 +83,74 @@ def test_ou_noise_generator_statistics(prepare_kernel): assert np.abs(curr_var - expected_curr_var) < 0.2 * curr_var -def test_ou_noise_generator_autocorrelation(): - nest.rng_seed = 6789 - - dt = 1.0 +def test_ou_noise_generator_autocorrelation(prepare_kernel): + # verify lag-1 autocorr = exp(-dt/tau) + dt = 0.1 tau = 1.0 - oung = nest.Create('ou_noise_generator', 1, {'mean':0.0, 'std': 60.0, 'tau':tau,'dt': nest.resolution}) + oung = nest.Create('ou_noise_generator', {'mean':0.0, 'std': 60.0, 'tau':tau,'dt': nest.resolution}) neuron = nest.Create("iaf_psc_alpha") + # we need to connect to a neuron otherwise the generator does not generate nest.Connect(oung, neuron) - mm = nest.Create('multimeter', 1, {'record_from':['I']}) - nest.Connect(mm, oung, syn_spec={'weight': 1}) + mm = nest.Create('multimeter', 1, {'record_from':['I'], "interval": dt}) + nest.Connect(mm, oung, syn_spec={'weight': 1 }) nest.Simulate(10000.0) - - ou_current = mm.get('events')['I'] - ## drop first k*tau of data - cutoff = burn_in_start(dt=dt, tau=tau, k=10.0) + ## drop first k*tau + cutoff = burn_in_start(dt=dt, tau=tau) x = ou_current[cutoff:] x -= x.mean() - + # empirical lag-1 autocorrelation emp_ac1 = np.dot(x[:-1], x[1:]) / ((len(x)-1)*x.var()) # theoretical lag-1: exp(-dt/tau) theor_ac1 = np.exp(-dt / tau) - assert abs(emp_ac1 - theor_ac1) < 0.05, \ f"autocorr {emp_ac1:.3f} vs theoretical {theor_ac1:.3f}" -def _crosscorr_between_two_neurons(V_mean, V_std, dt, tau_m, t_max): - nest.ResetKernel(); nest.resolution = dt - ou = nest.Create("ou_noise_generator", 1, - {"mean": V_mean, "std": V_std, "tau": tau_m, "dt": dt}) + +def test_cross_correlation(prepare_kernel): + # two neurons driven by the same OU noise should remain uncorrelated + dt, tau = 0.1, 1.0 + + ou = nest.Create("ou_noise_generator", + {"mean": 0.0, "std": 50.0, "tau": tau, "dt": dt}) neurons = nest.Create("iaf_psc_alpha", 2, - {"E_L": 0.0, "V_th": 1e9, "C_m": 1.0, "tau_m": tau_m}) + {"E_L": 0.0, "V_th": 1e9, "C_m": 1.0, "tau_m": tau}) nest.Connect(ou, neurons) - mm = nest.Create("multimeter", 1, {"record_from": ["V_m"], "interval": dt}) + + mm = nest.Create("multimeter", + params={"record_from": ["V_m"], "interval": dt}) nest.Connect(mm, neurons) - nest.Simulate(t_max) + + simtime = 50000.0 + nest.Simulate(simtime) + ev = mm.get("events") senders = np.asarray(ev["senders"], int) - times = np.asarray(ev["times"]) - vms = np.asarray(ev["V_m"]) - t_steps = int(round(t_max / dt)) + 1 - V = np.zeros((t_steps, 2)) + times = np.asarray(ev["times"]) + vms = np.asarray(ev["V_m"]) + + # build time array + n_steps = int(round(simtime / dt)) + 1 + V = np.zeros((n_steps, 2)) for t, s, v in zip(times, senders, vms): - V[int(round(t / dt)), neurons.index(s)] = v - start = burn_in_start(dt, tau_m) + idx = int(round(t / dt)) + col = neurons.index(s) + V[idx, col] = v + + # discard burn-in + start = burn_in_start(dt, tau) X1 = V[start:, 0] - V[start:, 0].mean() X2 = V[start:, 1] - V[start:, 1].mean() + + # cross-corr via FFT corr = fftconvolve(X1, X2[::-1], mode="full") corr /= np.sqrt(np.dot(X1, X1) * np.dot(X2, X2)) - mid = corr.size // 2 - return corr[mid:] - + mid = corr.size // 2 -def test_ou_noise_generator_cross_correlation(prepare_kernel): - nest.rng_seed = 2321 - crosscorr = _crosscorr_between_two_neurons( - V_mean=0.0, V_std=60.0, dt=nest.resolution, tau_m=1.0, t_max=30_000.0 - ) - assert float(np.max(np.abs(crosscorr))) < 0.05 \ No newline at end of file + # maximum absolute cross-corr should be near zero + assert np.max(np.abs(corr[mid:])) < 0.05 \ No newline at end of file From d33e09c644036937ac0087f6e0fdb40046abd79c Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Thu, 31 Jul 2025 17:48:19 +0200 Subject: [PATCH 07/12] formatting --- models/ou_noise_generator.cpp | 21 +++++++++------------ models/ou_noise_generator.h | 15 +++++++-------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/models/ou_noise_generator.cpp b/models/ou_noise_generator.cpp index 9fe7fe7b42..27edee4f86 100644 --- a/models/ou_noise_generator.cpp +++ b/models/ou_noise_generator.cpp @@ -62,9 +62,9 @@ RecordablesMap< ou_noise_generator >::create() * ---------------------------------------------------------------- */ nest::ou_noise_generator::Parameters_::Parameters_() - : mean_( 0.0 ) // pA - , std_( 0.0 ) // pA / sqrt(s) - , tau_( 0.0 ) // ms + : mean_( 0.0 ) // pA + , std_( 0.0 ) // pA / sqrt(s) + , tau_( 0.0 ) // ms , dt_( get_default_dt() ) , num_targets_( 0 ) { @@ -222,14 +222,13 @@ nest::ou_noise_generator::pre_run_hook() const double t = kernel().simulation_manager.get_time().get_ms(); // scale Hz to ms - const double noise_amp = P_.std_ * std::sqrt(-1 * std::expm1(-2 * h / P_.tau_)); - const double prop = std::exp(-1 * h / P_.tau_); - const double tau_inv =-std::expm1(-h / P_.tau_); + const double noise_amp = P_.std_ * std::sqrt( -1 * std::expm1( -2 * h / P_.tau_ ) ); + const double prop = std::exp( -1 * h / P_.tau_ ); + const double tau_inv = -std::expm1( -h / P_.tau_ ); V_.noise_amp_ = noise_amp; V_.prop_ = prop; V_.tau_inv_ = tau_inv; - } @@ -287,9 +286,8 @@ nest::ou_noise_generator::update( Time const& origin, const long from, const lon // compute new currents for ( double& amp : B_.amps_ ) { - amp = P_.mean_ * V_.tau_inv_ - + amp * V_.prop_ + V_.noise_amp_ - * V_.normal_dist_( get_vp_specific_rng( get_thread() ) ); + amp = P_.mean_ * V_.tau_inv_ + amp * V_.prop_ + + V_.noise_amp_ * V_.normal_dist_( get_vp_specific_rng( get_thread() ) ); } // use now as reference, in case we woke up from inactive period B_.next_step_ = now + V_.dt_steps_; @@ -345,8 +343,7 @@ nest::ou_noise_generator::set_data_from_stimulation_backend( std::vector< double { if ( input_param.size() != 3 ) { - throw BadParameterValue( - "The size of the data for the ou_noise_generator needs to be 3 [mean, std, tau]." ); + throw BadParameterValue( "The size of the data for the ou_noise_generator needs to be 3 [mean, std, tau]." ); } DictionaryDatum d = DictionaryDatum( new Dictionary ); ( *d )[ names::mean ] = DoubleDatum( input_param[ 0 ] ); diff --git a/models/ou_noise_generator.h b/models/ou_noise_generator.h index 87338c23fb..be0ba262a0 100644 --- a/models/ou_noise_generator.h +++ b/models/ou_noise_generator.h @@ -207,10 +207,10 @@ class ou_noise_generator : public StimulationDevice */ struct Parameters_ { - double mean_; //!< mean current, in pA - double std_; //!< standard deviation of current, in pA - double tau_; //!< Standard frequency in Hz - Time dt_; //!< time interval between updates + double mean_; //!< mean current, in pA + double std_; //!< standard deviation of current, in pA + double tau_; //!< Standard frequency in Hz + Time dt_; //!< time interval between updates /** * Number of targets. @@ -266,11 +266,10 @@ class ou_noise_generator : public StimulationDevice { normal_distribution normal_dist_; //!< normal distribution - long dt_steps_; //!< update interval in steps - double prop_; //!< frequency [radian/s] + long dt_steps_; //!< update interval in steps + double prop_; //!< frequency [radian/s] double noise_amp_; //!< - double tau_inv_; //!< - + double tau_inv_; //!< }; double From f8f05113b219bab666997c81a80c05254e870a41 Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Fri, 1 Aug 2025 10:41:07 +0200 Subject: [PATCH 08/12] rewrite UserDocs --- models/ou_noise_generator.h | 83 +++++++++---------------------------- 1 file changed, 19 insertions(+), 64 deletions(-) diff --git a/models/ou_noise_generator.h b/models/ou_noise_generator.h index be0ba262a0..03a9163a02 100644 --- a/models/ou_noise_generator.h +++ b/models/ou_noise_generator.h @@ -44,75 +44,46 @@ namespace nest Short description +++++++++++++++++ -Generate a!!!!!!!j Gaussian white noise current +Generates a temporally correlated noise current based on an Ornstein-Uhlenbeck process. Description +++++++++++ -The `ou_noise_generator` can be used to inject a Gaussian "white" noise current into a node. - -The current is not truly white, but a piecewise constant current with a Gaussian distributed -amplitude with mean :math:`\mu` and standard deviation :math:`\sigma`. The current changes at -a user-defined interval :math:`\delta` and is given by +The `ou_noise_generator` can be used to inject a temporally correlated noise current into a node. +The current I(t) follows an Ornstein-Uhlenbeck (OU) process, which is described by the following stochastic differential equation: .. math:: - I(t) = \mu + N_j \sigma \quad \text{for} \quad t_0 + j \delta < t \leq t_0 + (j+1) \delta \;, - -where :math:`N_j` are Gaussian random numbers with unit standard deviation and :math:`t_0` is -the device onset time. - -Additionally a sinusodially modulated term can be added to the standard -deviation of the noise: - -.. math:: - - I(t) = \mu + N_j \sqrt{\sigma^2 + \sigma_{\text{mod}}^2 \sin(\omega t + \phi)} - \quad \text{for} \quad t_0 + j \delta < t \leq t_0 + (j+1) \delta \;. - -The effect of the noise current on a neuron depends on the switching interval :math:`\delta`. -For a leaky integrate-and-fire neuron with time constant :math:`\tau_m` and capacitance -:math:`C_m`, the variance of the membrane potential is given by - -.. math:: + dI = \frac{1}{\tau}(\mu - I)dt + \sigma_{stat} \sqrt{\frac{2}{\tau}} dW - \Sigma^2 = \frac{\delta \tau_m \sigma^2}{2 C_m^2} +where: + - :math:`\mu` is the long-term mean of the process (`mean` parameter). + - :math:`\tau` is the time constant of the correlation (`tau` parameter). + - :math:`\sigma_{stat}` is the stationary standard deviation of the process (`std` parameter). + - :math:`dW` is a Wiener process (Gaussian white noise). -for :math:`\delta \ll \tau_m`. For details, see the `noise generator notebook -<../model_details/ou_noise_generator.ipynb>`_. +The generator integrates this process at a user-defined interval `dt` and delivers the resulting current to its targets. A larger time constant :math:`\tau` results in a more slowly varying noise signal. -All targets of a noise generator receive different currents, but the currents for all -targets change at the same points in time. The interval :math:`\delta` between -changes must be a multiple of the time step. +All targets of a noise generator receive different, independent noise currents, but the currents for all targets are updated at the same points in time. The interval `dt` between updates must be a multiple of the simulation time step. .. admonition:: Recording the generated current - You can use a :doc:`multimeter ` to record the average current sent to all targets for each time step - if simulating on a single thread; multiple MPI processes with one thread each also work. In this case, - the recording interval of the multimeter should be equal to the simulation resolution to avoid confusing effects - due to offset or drift between the recording times of the multimeter and the switching times of the - noise generator. In multi-threaded mode, recording of noise currents is prohibited for technical reasons. + You can use a :doc:`multimeter ` to record the average current sent to all targets for each time step if simulating on a single thread; multiple MPI processes with one thread each also work. In this case, the recording interval of the multimeter should be equal to the `dt` of the generator to avoid aliasing effects. In multi-threaded mode, recording of noise currents is prohibited for technical reasons. .. include:: ../models/stimulation_device.rst mean - The mean value :math:`\mu` of the noise current (pA) + The mean value :math:`\mu` to which the process reverts (pA). std - The standard deviation :math:`\sigma` of the noise current (pA) + The stationary standard deviation :math:`\sigma_{stat}` of the process (pA). -dt - The interval :math:`\delta` between changes in current (ms; default: 10 * resolution) - -std_mod - The modulation :math:`\sigma_{\text{mod}}` of the standard deviation of the noise current (pA) - -frequency - The frequency of the sine modulation (Hz) +tau + The correlation time constant :math:`\tau` of the process (ms). -phase - The phase of sine modulation (0–360 deg) +dt + The interval :math:`\delta` between updates of the noise current (ms). Setting parameters from a stimulation backend @@ -125,24 +96,8 @@ The indexing is as follows: 0. mean 1. std - 2. std_mod - 3. frequency - 4. phase - -Sends -+++++ - -CurrentEvent - -See also -++++++++ - -step_current_generator - -Examples using this model -+++++++++++++++++++++++++ + 2. tau -.. listexamples:: ou_noise_generator EndUserDocs */ From fc8fa5409a3851226463b4b4aa556d816d26f9a3 Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Sun, 10 Aug 2025 17:37:58 +0200 Subject: [PATCH 09/12] Improve code style and correct copyright header --- models/ou_noise_generator.h | 14 +++-- .../test_ou_noise_generator.py | 53 +++++++++---------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/models/ou_noise_generator.h b/models/ou_noise_generator.h index 03a9163a02..e2b6dc48e2 100644 --- a/models/ou_noise_generator.h +++ b/models/ou_noise_generator.h @@ -50,7 +50,8 @@ Description +++++++++++ The `ou_noise_generator` can be used to inject a temporally correlated noise current into a node. -The current I(t) follows an Ornstein-Uhlenbeck (OU) process, which is described by the following stochastic differential equation: +The current I(t) follows an Ornstein-Uhlenbeck (OU) process, which is described by the following stochastic differential +equation: .. math:: @@ -62,13 +63,18 @@ The current I(t) follows an Ornstein-Uhlenbeck (OU) process, which is described - :math:`\sigma_{stat}` is the stationary standard deviation of the process (`std` parameter). - :math:`dW` is a Wiener process (Gaussian white noise). -The generator integrates this process at a user-defined interval `dt` and delivers the resulting current to its targets. A larger time constant :math:`\tau` results in a more slowly varying noise signal. +The generator integrates this process at a user-defined interval `dt` and delivers the resulting current to its targets. +A larger time constant :math:`\tau` results in a more slowly varying noise signal. -All targets of a noise generator receive different, independent noise currents, but the currents for all targets are updated at the same points in time. The interval `dt` between updates must be a multiple of the simulation time step. +All targets of a noise generator receive different, independent noise currents, but the currents for all targets are +updated at the same points in time. The interval `dt` between updates must be a multiple of the simulation time step. .. admonition:: Recording the generated current - You can use a :doc:`multimeter ` to record the average current sent to all targets for each time step if simulating on a single thread; multiple MPI processes with one thread each also work. In this case, the recording interval of the multimeter should be equal to the `dt` of the generator to avoid aliasing effects. In multi-threaded mode, recording of noise currents is prohibited for technical reasons. + You can use a :doc:`multimeter ` to record the average current sent to all targets for each time step if +simulating on a single thread; multiple MPI processes with one thread each also work. In this case, the recording +interval of the multimeter should be equal to the `dt` of the generator to avoid aliasing effects. In multi-threaded +mode, recording of noise currents is prohibited for technical reasons. .. include:: ../models/stimulation_device.rst diff --git a/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py index 0f3bb0fa97..2ac0731ab8 100644 --- a/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py +++ b/testsuite/pytests/sli2py_stimulating/test_ou_noise_generator.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# test_noise_generator.py +# test_ou_noise_generator.py # # This file is part of NEST. # @@ -34,10 +34,12 @@ def prepare_kernel(): nest.ResetKernel() nest.resolution = 0.1 + def burn_in_start(dt, tau, k=10): """Return number of steps to discard for a k*tau burn-in.""" return int(round((k * tau) / dt)) + def test_ou_noise_generator_set_parameters(prepare_kernel): params = {"mean": 210.0, "std": 60.0, "dt": 0.1} @@ -54,24 +56,25 @@ def test_ou_noise_generator_incorrect_noise_dt(prepare_kernel): with pytest.raises(nest.kernel.NESTError, match="StepMultipleRequired"): nest.Create("ou_noise_generator", {"dt": 0.25}) + def test_ou_noise_mean_and_variance(prepare_kernel): # run for resolution dt=0.1 project to iaf_psc_alpha. # create 100 repetitions of 1000ms simulations - oung = nest.Create('ou_noise_generator', {'mean':0.0, 'std': 60.0, 'tau':1., 'dt':0.1}) + oung = nest.Create("ou_noise_generator", {"mean": 0.0, "std": 60.0, "tau": 1.0, "dt": 0.1}) neuron = nest.Create("iaf_psc_alpha") - + # we need to connect to a neuron otherwise the generator does not generate nest.Connect(oung, neuron) - mm = nest.Create('multimeter', 1, {'record_from':['I'], "interval": 0.1}) - nest.Connect(mm, oung, syn_spec={'weight': 1}) + mm = nest.Create("multimeter", 1, {"record_from": ["I"], "interval": 0.1}) + nest.Connect(mm, oung, syn_spec={"weight": 1}) # Simulate for 100 times n_sims = 100 ou_current = np.empty(n_sims) for i in range(n_sims): nest.Simulate(1000.0) - ou_current[i] = mm.get('events')['I'][-1] + ou_current[i] = mm.get("events")["I"][-1] curr_mean = np.mean(ou_current) curr_var = np.var(ou_current) @@ -88,42 +91,38 @@ def test_ou_noise_generator_autocorrelation(prepare_kernel): dt = 0.1 tau = 1.0 - oung = nest.Create('ou_noise_generator', {'mean':0.0, 'std': 60.0, 'tau':tau,'dt': nest.resolution}) + oung = nest.Create("ou_noise_generator", {"mean": 0.0, "std": 60.0, "tau": tau, "dt": nest.resolution}) neuron = nest.Create("iaf_psc_alpha") - + # we need to connect to a neuron otherwise the generator does not generate nest.Connect(oung, neuron) - mm = nest.Create('multimeter', 1, {'record_from':['I'], "interval": dt}) - nest.Connect(mm, oung, syn_spec={'weight': 1 }) + mm = nest.Create("multimeter", 1, {"record_from": ["I"], "interval": dt}) + nest.Connect(mm, oung, syn_spec={"weight": 1}) nest.Simulate(10000.0) - ou_current = mm.get('events')['I'] + ou_current = mm.get("events")["I"] - ## drop first k*tau + # drop first k*tau cutoff = burn_in_start(dt=dt, tau=tau) x = ou_current[cutoff:] x -= x.mean() - + # empirical lag-1 autocorrelation - emp_ac1 = np.dot(x[:-1], x[1:]) / ((len(x)-1)*x.var()) + emp_ac1 = np.dot(x[:-1], x[1:]) / ((len(x) - 1) * x.var()) # theoretical lag-1: exp(-dt/tau) theor_ac1 = np.exp(-dt / tau) - assert abs(emp_ac1 - theor_ac1) < 0.05, \ - f"autocorr {emp_ac1:.3f} vs theoretical {theor_ac1:.3f}" - + assert abs(emp_ac1 - theor_ac1) < 0.05, f"autocorr {emp_ac1:.3f} vs theoretical {theor_ac1:.3f}" + def test_cross_correlation(prepare_kernel): # two neurons driven by the same OU noise should remain uncorrelated dt, tau = 0.1, 1.0 - ou = nest.Create("ou_noise_generator", - {"mean": 0.0, "std": 50.0, "tau": tau, "dt": dt}) - neurons = nest.Create("iaf_psc_alpha", 2, - {"E_L": 0.0, "V_th": 1e9, "C_m": 1.0, "tau_m": tau}) + ou = nest.Create("ou_noise_generator", {"mean": 0.0, "std": 50.0, "tau": tau, "dt": dt}) + neurons = nest.Create("iaf_psc_alpha", 2, {"E_L": 0.0, "V_th": 1e9, "C_m": 1.0, "tau_m": tau}) nest.Connect(ou, neurons) - mm = nest.Create("multimeter", - params={"record_from": ["V_m"], "interval": dt}) + mm = nest.Create("multimeter", params={"record_from": ["V_m"], "interval": dt}) nest.Connect(mm, neurons) simtime = 50000.0 @@ -131,8 +130,8 @@ def test_cross_correlation(prepare_kernel): ev = mm.get("events") senders = np.asarray(ev["senders"], int) - times = np.asarray(ev["times"]) - vms = np.asarray(ev["V_m"]) + times = np.asarray(ev["times"]) + vms = np.asarray(ev["V_m"]) # build time array n_steps = int(round(simtime / dt)) + 1 @@ -150,7 +149,7 @@ def test_cross_correlation(prepare_kernel): # cross-corr via FFT corr = fftconvolve(X1, X2[::-1], mode="full") corr /= np.sqrt(np.dot(X1, X1) * np.dot(X2, X2)) - mid = corr.size // 2 + mid = corr.size // 2 # maximum absolute cross-corr should be near zero - assert np.max(np.abs(corr[mid:])) < 0.05 \ No newline at end of file + assert np.max(np.abs(corr[mid:])) < 0.05 From 8fe02620f3fad42c2f6d7c3dc755bbb2f210515e Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Sun, 10 Aug 2025 18:54:56 +0200 Subject: [PATCH 10/12] Skip ou_noise_generator in multimeter stepping test --- testsuite/pytests/sli2py_recording/test_multimeter_stepping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testsuite/pytests/sli2py_recording/test_multimeter_stepping.py b/testsuite/pytests/sli2py_recording/test_multimeter_stepping.py index c800c5ae61..1cf7c80de6 100644 --- a/testsuite/pytests/sli2py_recording/test_multimeter_stepping.py +++ b/testsuite/pytests/sli2py_recording/test_multimeter_stepping.py @@ -52,6 +52,7 @@ "ac_generator", # generator device, does not support spike input "dc_generator", # generator device, does not support spike input "noise_generator", # generator device, does not support spike input + "ou_noise_generator", # generator device, does not support spike input "step_current_generator", # generator device, does not support spike input "step_rate_generator", # generator device, does not support spike input "sinusoidal_poisson_generator", # generator device, does not support spike input From f3b27c35dc8fb0e092da552e8289614a1f24753c Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Sun, 10 Aug 2025 19:21:53 +0200 Subject: [PATCH 11/12] Skip ou_noise_generator in tic-base tests --- testsuite/pytests/test_changing_tic_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testsuite/pytests/test_changing_tic_base.py b/testsuite/pytests/test_changing_tic_base.py index 6caa62b353..535091785e 100644 --- a/testsuite/pytests/test_changing_tic_base.py +++ b/testsuite/pytests/test_changing_tic_base.py @@ -52,6 +52,7 @@ def test_models(self): "correlomatrix_detector": ["delta_tau"], "correlospinmatrix_detector": ["delta_tau"], "noise_generator": ["dt"], + "ou_noise_generator": ["dt"], } # Generate a dictionary of reference values for each model. From c0e77e7bc3f0c76b19b70eaff6876f7fc0fe106d Mon Sep 17 00:00:00 2001 From: Finn Burkhardt Date: Mon, 11 Aug 2025 12:41:48 +0200 Subject: [PATCH 12/12] Apply doc suggestions --- models/ou_noise_generator.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models/ou_noise_generator.h b/models/ou_noise_generator.h index e2b6dc48e2..583fc7192a 100644 --- a/models/ou_noise_generator.h +++ b/models/ou_noise_generator.h @@ -71,10 +71,10 @@ updated at the same points in time. The interval `dt` between updates must be a .. admonition:: Recording the generated current - You can use a :doc:`multimeter ` to record the average current sent to all targets for each time step if -simulating on a single thread; multiple MPI processes with one thread each also work. In this case, the recording -interval of the multimeter should be equal to the `dt` of the generator to avoid aliasing effects. In multi-threaded -mode, recording of noise currents is prohibited for technical reasons. + You can use a :doc:`multimeter ` to record the average current sent to all targets for each time step if + simulating on a single thread; multiple MPI processes with one thread each also work. In this case, the recording + interval of the multimeter should be equal to the `dt` of the generator to avoid aliasing effects. In multi-threaded + mode, recording of noise currents is prohibited for technical reasons. .. include:: ../models/stimulation_device.rst