Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
da3b29a
added first draft of ou_noise_gererator model
JoshuaBoettcher Feb 6, 2024
82b48c0
add unfinished ou_noise_generator test
JoshuaBoettcher Feb 23, 2024
500c332
bug fix in ou_noise_generator
JoshuaBoettcher Feb 27, 2024
1f02f0b
Fix minor error
FinnBurkhardt Jul 31, 2025
8be0909
fix and add tests
FinnBurkhardt Jul 31, 2025
d583852
cleanup tests
FinnBurkhardt Jul 31, 2025
d33e09c
formatting
FinnBurkhardt Jul 31, 2025
f8f0511
rewrite UserDocs
FinnBurkhardt Aug 1, 2025
fc8fa54
Improve code style and correct copyright header
FinnBurkhardt Aug 10, 2025
8fe0262
Skip ou_noise_generator in multimeter stepping test
FinnBurkhardt Aug 10, 2025
f3b27c3
Skip ou_noise_generator in tic-base tests
FinnBurkhardt Aug 10, 2025
c0e77e7
Apply doc suggestions
FinnBurkhardt Aug 11, 2025
e7aa371
Tau validation and cleanup
FinnBurkhardt Sep 11, 2025
079b240
Merge remote-tracking branch 'upstream/master' into add_ou_generator
FinnBurkhardt Sep 11, 2025
7637ff0
cleanup
FinnBurkhardt Sep 11, 2025
a012822
Set tau default to 1
FinnBurkhardt Sep 11, 2025
791beed
Add example scripts
FinnBurkhardt Sep 23, 2025
19b4e8f
Improve formatting in example scripts
FinnBurkhardt Oct 16, 2025
b2915f6
Fix comments, rename tau_inv_ to mean_weight_, and simplify pre_run_hook
FinnBurkhardt Nov 11, 2025
76c4f32
Use generator-specific update interval instead of global resolution
FinnBurkhardt Nov 11, 2025
84d645f
Correct OU noise generator update during inactive periods
FinnBurkhardt Nov 12, 2025
c40f941
Relocate test_ou_noise_generator to correct folder
FinnBurkhardt Nov 12, 2025
7cc0493
Add comments and fix formatting
FinnBurkhardt Nov 12, 2025
638f9d7
Skip OU updates during inactive periods and add corresponding tests
FinnBurkhardt Nov 24, 2025
d0d36c7
format and lint OU noise tests
FinnBurkhardt Nov 25, 2025
df7abb4
Fix inconsistent variable names
FinnBurkhardt Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 374 additions & 0 deletions models/ou_noise_generator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
*/

#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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems to make no sense here.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would simplify this a bit

Suggested change
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;
V_.noise_amp_ = P_.std_ * std::sqrt( -1 * std::expm1( -2 * h / P_.tau_ ) );
V_.prop_ = std::exp( -1 * h / P_.tau_ );
V_.tau_inv_ = -std::expm1( -h / P_.tau_ );

}


/* ----------------------------------------------------------------
* 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_ )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the logic for handling inactivity is correct here. Consider a situation like the following:

ou = nest.Create('ou_noise_generator', params={'start': 200, 'stop': 500'})
for k in range(10):
    ou.offset = k * 1000
    nest.Simulate(1000)

This runs a simulation in sections of 1000 ms and in each section the generator is on from 200 to 500 ms and off in between.

With the code as it is now, we freeze the OU process while the generator is inactive. But would it not make more sense to assume that the process runs permanently and we only send its values out between start and stop? Then, amp should always be updated and only I_amp should be kept to 0 and no events sent out while the generator is inactive. Or does this not matter? Or could on be more efficient and correct in probability by updating according to the time that has passed since the generator was last active?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I hadn’t considered that case before. I’ve now implemented a version where the OU process keeps evolving even while the generator is inactive.
I tested it by repeatedly turning the generator on and off and checking the correlation between the last value before the pause and the first after resuming.
Would you consider this a useful test to add to the unit tests?

I also believe it’s possible to compute the update in a single large step, based on the time elapsed since the last update, with some bookkeeping overhead. I'll try to implement that version to assess how much more efficient it is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, "jumping" over that gap mathematically should help efficiency. And if you can include the correlation check across the gap as a test, that would be very nice. Then, to test the test, try to break it by going temporarily back to the old implementation in NEST and see if the test fails.

{
// std::cout << "I am in update" << std::endl;
// 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() ) );
}
// use now as reference, in case we woke up from inactive period
B_.next_step_ = now + V_.dt_steps_;
}

// record values
S_.I_avg_ = 0.0;

for ( double& amp : B_.amps_ )
{
S_.I_avg_ += amp;
}

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 );
}
}
Loading
Loading