An electrophysiological model of GABAergic double bouquet cells (Chrysanthidis et al. 2019)

 Download zip file 
Help downloading and running models
Accession:257610
We present an electrophysiological model of double bouquet cells (DBCs) and integrate them into an established cortical columnar microcircuit model that implements a BCPNN (Bayesian Confidence Propagation Neural Network) learning rule. The proposed architecture effectively solves the problem of duplexed learning of inhibition and excitation by replacing recurrent inhibition between pyramidal cells in functional columns of different stimulus selectivity with a plastic disynaptic pathway. The introduction of DBCs improves the biological plausibility of our model, without affecting the model's spiking activity, basic operation, and learning abilities.
Reference:
1 . Chrysanthidis N, Fiebig F, Lansner A (2019) Introducing double bouquet cells into a modular cortical associative memory model Journal of Computational Neuroscience
Citations  Citation Browser
Model Information (Click on a link to find other models with that property)
Model Type: Realistic Network;
Brain Region(s)/Organism:
Cell Type(s): Neocortex U1 interneuron basket PV GABA cell; Neocortex U1 L2/6 pyramidal intratelencephalic GLU cell; Abstract integrate-and-fire adaptive exponential (AdEx) neuron; Neocortex layer 2-3 interneuron; Neocortex bitufted interneuron;
Channel(s):
Gap Junctions:
Receptor(s):
Gene(s):
Transmitter(s):
Simulation Environment: NEST;
Model Concept(s): Learning;
Implementer(s): Chrysanthidis, Nikolaos [nchr at kth.se]; Fiebig, Florian [fiebig at kth.se]; Lansner, Anders [ala at kth.se];
Search NeuronDB for information about:  Neocortex U1 L2/6 pyramidal intratelencephalic GLU cell; Neocortex U1 interneuron basket PV GABA cell;
/
ChrysanthidisEtAl2019
BCPNN_NEST_Module
bootstrap-module-100725
module-100725
autom4te.cache
libltdl
sli
aclocal.m4 *
aeif_cond_exp_multisynapse.cpp *
aeif_cond_exp_multisynapse.h *
bcpnn_connection.cpp *
bcpnn_connection.h *
bcpnn_connection_backup.cpp *
bcpnn_connection_backup.h *
bootstrap.sh *
compile *
config.guess *
config.sub *
configure *
configure.ac *
depcomp *
iaf_cond_alpha_bias.cpp *
iaf_cond_alpha_bias.h *
iaf_cond_exp_bias.cpp *
iaf_cond_exp_bias.h *
install-sh *
ltmain.sh *
Makefile.am *
Makefile.in *
missing *
pt_module.cpp *
pt_module.h *
pt_module_config.h.in *
pt_module_names.cpp *
pt_module_names.h *
                            
/*
 *  bcpnn_connection.h
 *
 *  Written by Philip Tully
 *
 */

#ifndef BCPNN_CONNECTION_H
#define BCPNN_CONNECTION_H

/* BeginDocumentation
  Name: bcpnn_synapse - Synapse type for incremental, Bayesian spike-timing 
   dependent plasticity.

  Description:
   bcpnn_synapse is a connector to create synapses with incremental, Bayesian 
   spike timing dependent plasticity.

   tau_i	double - Primary trace presynaptic time constant
   tau_j	double - Primary trace postsynaptic time constant
   tau_e	double - Secondary trace time constant
   tau_p	double - Tertiarty trace time constant
   p_i		double - \
   p_j		double -  >- these 3 initial conditions determine weight, i.e. log(p_ij/(p_i * p_j)).
   p_ij		double - /
   K_		double - Print-now signal // Neuromodulation. Turn off learning, K = 0.
   fmax_        double - Frequency assumed as maximum firing, for match with abstract rule
   epsilon_     double - lowest possible probability of spiking, e.g. lowest assumed firing rate
   bias_        double - ANN interpretation. Only calculated here to demonstrate match to rule. 
                         Will be eliminated in future versions, where bias will be calculated postsynaptically
   gain_    double - Coefficient to scale weight as conductance, can be zero-ed out

  Transmits: SpikeEvent
   
  References:
   [1] Wahlgren and Lansner (2001) Biological Evaluation of a Hebbian-Bayesian
       learning rule. Neurocomputing, 38-40, 433-438

   [2] Bergel, Transforming the BCPNN Learning Rule for Spiking Units to a
       Learning Rule for Non-Spiking Units (2010). KTH Masters Thesis.

  FirstVersion: November 2011
  CurrentVersion: March 2012
  Author: Philip Tully
          tully@csc.kth.se
  SeeAlso: synapsedict, stdp_synapse, tsodyks_synapse, static_synapse
*/

/* for Debugging */
#include <iostream>
using namespace std;

#include "connection_het_wd.h"
#include "archiving_node.h"
#include "generic_connector.h"
#include <cmath>

namespace mynest
{
  class BCPNNConnection : public nest::ConnectionHetWD
  {
    public:
      /* Default Constructor. Sets default values for all parameters. Needed by GenericConnectorModel. */
      BCPNNConnection();

      /* Copy constructor. Needs to be defined properly in order for GenericConnector to work. */
      BCPNNConnection(const BCPNNConnection &);

      /* Default Destructor. */
      ~BCPNNConnection() {}

      void check_connection(nest::Node & s, nest::Node & r, nest::port receptor_type, nest::double_t t_lastspike);

      /* Get all properties of this connection and put them into a dictionary. */
      void get_status(DictionaryDatum & d) const;

      /* Set properties of this connection from the values given in dictionary. */
      void set_status(const DictionaryDatum & d, nest::ConnectorModel &cm);

      /* Set properties of this connection from position p in the properties array given in dictionary. */
      void set_status(const DictionaryDatum & d, nest::index p, nest::ConnectorModel &cm);

      /* Create new empty arrays for the properties of this connection in the given dictionary. It is assumed 
         that they do not exist before. */
      void initialize_property_arrays(DictionaryDatum & d) const;

      /* Append properties of this connection to the given dictionary. If the dictionary is empty, new arrays 
         are created first. */
      void append_properties(DictionaryDatum & d) const;

      /* Send an event to the receiver of this connection.  */
      void send(nest::Event& e, nest::double_t t_lastspike, const nest::CommonSynapseProperties &cp);

      /* Overloaded for all supported event types. */
      using nest::Connection::check_event;
      void check_event(nest::SpikeEvent&) {}

    private:
      /* data members of each connection */
      nest::double_t yi_;
      nest::double_t yj_;

      nest::double_t taui_;
      nest::double_t tauj_;
      nest::double_t taue_;
      nest::double_t taup_;

      nest::double_t epsilon_;
      nest::double_t K_;
      nest::double_t bias_;
      nest::double_t fmax_;
      nest::double_t gain_;

      nest::double_t zi_;
      nest::double_t zj_;
      nest::double_t ei_;
      nest::double_t ej_;
      nest::double_t eij_;
      nest::double_t pi_;
      nest::double_t pj_;
      nest::double_t pij_;
      nest::double_t t_k_;
	  std::vector<nest::double_t> times_k_changed;
	  std::vector<nest::double_t> post_spiketimes;
	  std::vector<nest::double_t> K_values_;
  }; /* of class BCPNNConnection */

  inline 
  void BCPNNConnection::check_connection(nest::Node & s, nest::Node & r, nest::port receptor_type, nest::double_t t_lastspike)
  {
    nest::ConnectionHetWD::check_connection(s, r, receptor_type, t_lastspike);

    // For a new synapse, t_lastspike contains the point in time of the last spike.
    // So we initially read the history(t_last_spike - dendritic_delay, ...,  T_spike-dendritic_delay]
    // which increases the access counter for these entries.
    // At registration, all entries' access counters of history[0, ..., t_last_spike - dendritic_delay] will be 
    // incremented by the following call to Archiving_Node::register_stdp_connection().
    // See bug #218 for details.
    r.register_stdp_connection(t_lastspike - nest::Time(nest::Time::step(delay_)).get_ms());
  }

  /* Send an event to the receiver of this connection.
   * \param e The event to send
   * \param p The port under which this connection is stored in the Connector.
   * \param t_lastspike Time point of last spike emitted 
  
   note: every time this method is called by an outside function, a presynaptic
       event has occured and is being transmitted to the postsynaptic side. */

  inline
  void BCPNNConnection::send(nest::Event& e, nest::double_t t_lastspike, const nest::CommonSynapseProperties &)
  {
    nest::double_t t_spike = e.get_stamp().get_ms();  /* time stamp of current spike event */
    nest::double_t dendritic_delay = nest::Time(nest::Time::step(delay_)).get_ms();    /* delay from dendrite -> soma */
    nest::double_t resolution = nest::Time::get_resolution().get_ms();               /* nest.GetKernelStatus('resolution') simulation timestep */
    nest::int_t spike_width = 10;                     /* assume spike width of 1ms, resolution is 0.1 so mult by 10 */    
    nest::double_t spike_height = 1000.0 / fmax_;     /* normalizing to match this spiking rule to abstract = 1000/FMAX (Hz)*/
    nest::int_t counter = 0;                          /* ensuring traces reverberate for duration of the spike width */
    nest::double_t min_weight = epsilon_/std::pow(0.5 ,2);         /* theoretical minimum weight = epsilon/(0.5*0.5) */

//      cout << "t_lastspike: " << t_lastspike << " t_spike: " << t_spike << " K_values.size(): " << K_values_.size() << endl;

    /*STEP ONE: Get all timings of pre and postsynaptic spikes. Post store in dynamically allocated array */

    /* get spike history in relevant range (t1, t2] from post-synaptic neuron */
    std::deque<nest::histentry>::iterator start;
    std::deque<nest::histentry>::iterator finish;
  
    /* Initially read the history(t_last_spike - dendritic_delay, ...,  T_spike-dendritic_delay] which increases the 
       access counter for these entries. At registration, all entries' access counters of history[0, ..., 
       t_last_spike - dendritic_delay] have been incremented by Archiving_Node::register_stdp_connection(). 
       See bug #218 for details. */
    target_->get_history(t_lastspike - dendritic_delay, t_spike - dendritic_delay, &start, &finish);

    /* For spike order pre-post, if dopamine present facilitate else depress.
       Pre  spikes: |       |  t_lastpike is the last pre spike and t_spike is the current pre spike
       Post spikes    | ||		 start is a pointer to the first post spike in the interval between the
       two pre spikes. It is then iterated until the last post spike in the interval */


    while (start != finish)  {/* loop until you get to last post spike */
		post_spiketimes.push_back(start->t_);
		start++;
    } /* of while */
    /* STEP TWO: Consider the presynaptic firing window, delta t resolution, and update the traces */
    
    /* nest stores with ms precision the timing of the spike. */
    /* the following loop iterates through the presynaptic spike difference window */
    counter = 0;
	nest::int_t number_iterations = (nest::int_t)((t_spike - t_lastspike)/resolution);
	// This if+else is to account for the case when K_ is initialized with 1 and K_ is changed via set_status /before/ the simulation starts
	std::vector<nest::double_t> K_vec (number_iterations, K_values_.front());
	if((nest::int_t)t_lastspike == 0) {
		K_vec = std::vector<nest::double_t> (number_iterations, K_values_.back());
	}

	std::vector<nest::double_t>::iterator post_it = post_spiketimes.begin(); 
	std::vector<nest::double_t>::iterator time_it = times_k_changed.end();
	std::vector<nest::double_t>::iterator K_it = K_values_.end();
	if (K_values_.size() > 1) { // only if a set_status has ever been called --> TODO
		if (times_k_changed.back() >= t_lastspike){ // if K has changed since the last pre-synaptic spike has occured
			// --> find out when changes occured and set the K_vec values right
			K_it--; // move iterator one element to the left, because .end() returns iterator just past the last element
			time_it--;
			nest::int_t idx_first = (nest::int_t) ((t_spike - t_lastspike) / resolution);
			nest::int_t idx_second;
			while (*time_it > t_lastspike){
				idx_second = (nest::int_t) ((*time_it - t_lastspike)/ resolution);
//                n_constant_K = (nest::int_t) ((idx_first - idx_second) / resolution);
//                cout << "New spike at *time_it: " << *time_it << " t_lastspike: " << t_lastspike << endl;
				for (nest::int_t i_k=idx_first-1; i_k >= idx_second; --i_k) { 
					// alternative implementation would be to create a temporary vector with the correct K-values and assign this to K_vec
//                    cout << "debug size K_vec = " << K_vec.size() << " i_k = " << i_k << " idx_first: " << idx_first << " idx_second: " << idx_second << " times_k_changed.size=" << times_k_changed.size() << " t_lastspike=" << t_lastspike << endl;
					K_vec.at(i_k) = *K_it;
				}
				idx_first = idx_second;
				time_it--;
				K_it--;
			} // end of while
//            K_ = *(K_values_.end()); // update the private K_ value
		}
	}
	K_values_.clear();
	K_values_.push_back(K_);
	times_k_changed.clear();
	times_k_changed.push_back(*time_it);


	nest::int_t idx_spike;

	/* Create a vector to represent the post spikes as a trace */
	std::vector<nest::double_t> post_active (number_iterations + spike_width, 0.);

//    std::vector<nest::double_t double> bar (5,0);
//    cout << "weight at send t_spike " << t_spike << " number_iterations: " << number_iterations << " t_lastspike: " << t_lastspike << endl;
	cout << "START PRINTOUT" << endl;
//    cout << t_spike << "\t" << number_iterations << "\t" << t_lastspike << endl;
    for (nest::int_t timestep = 0; timestep < number_iterations; timestep++)
    {
		/* CASE: Default. Neither Pre nor Post spike. */
		yi_ = 0.0; 
		yj_ = 0.0;

		/* CASE: Pre without (*OR WITH post) spike - synchronous events handled automatically. */
//        if(timestep < spike_width && (nest::int_t)t_lastspike != 0) {
		if(timestep == 0 && t_lastspike != 0.) {
			yi_ = spike_height * spike_width;
		}

		// if you have any post spike at all
		if (post_spiketimes.size() > 0) { 
			if (post_it != post_spiketimes.end()) { 
				if (timestep == (nest::int_t)((*post_it) - t_lastspike) / resolution){
					yj_ = spike_height * spike_width;
					post_it++;
				}
			}
		}

		/* Primary synaptic traces. Noise - commented out*/
		zi_ += (yi_ - zi_ + epsilon_ /*+ (0.01 + (double)rand() / RAND_MAX * (0.05 - 0.01))*/ ) * resolution / taui_;
//        zj_ += (spike_height * post_active.at(timestep) - zj_ + epsilon_ /*+ (0.01 + (double)rand() / RAND_MAX * (0.05 - 0.01))*/ ) * resolution / tauj_;
		zj_ += (yj_ - zj_ + epsilon_ /*+ (0.01 + (double)rand() / RAND_MAX * (0.05 - 0.01))*/ ) * resolution / tauj_;

		/* Secondary synaptic traces */
		ei_  += (zi_ - ei_) * resolution / taue_;
		ej_  += (zj_ - ej_) * resolution / taue_;
		eij_ += (zi_ * zj_ - eij_) * resolution / taue_;

		/* Tertiary synaptic traces. Commented is from Wahlgren paper. */
		//    pi_  += K_ * (ei_ - pi_) * resolution / taup_/* * eij_*/;
		//    pj_  += K_ * (ej_ - pj_) * resolution / taup_/* * eij_*/;
		//    pij_ += K_ * (eij_ - pij_) * resolution / taup_/* * eij_*/;
		pi_  += K_vec[timestep] * (ei_ - pi_) * resolution / taup_;
		pj_  += K_vec[timestep] * (ej_ - pj_) * resolution / taup_;
		pij_ += K_vec[timestep] * (eij_ - pij_) * resolution / taup_;

		weight_ = gain_ * std::log(pij_ / (pi_ * pj_)) /*- std::log(min_weight)*/;
		cout << timestep << "\t" << zi_ << "\t" << zj_ << "\t" << ei_ << "\t" << "\t" << ej_ << "\t" << eij_ << "\t" << pi_ << "\t" << pj_ << "\t" << pij_ << "\t" << weight_ << "\t" << t_spike << "\t" << yj_ << "\t" << yi_ << endl;
    } /* of for */
	cout << "END PRINTOUT" << endl;

    /* Update the weight & bias before event is sent. Use commented normalization to 
       implement soft weight bounds, this way the weight will never go below 0 because
       you push all weights up by the most negative weight possible. */
    bias_ = std::log(pj_);
    weight_ = gain_ * (std::log(pij_ / (pi_ * pj_)) /*- std::log(min_weight) */);

    /* STEP THREE. Implement hard weight bounds. NOTE if using above normalization, weights
                   are soft-bounded above zero already. */
    /*weight_ = (weight_ < 0) ? weight_ : 0.0;
      nest::double_t Wmax = ...;
      weight_ = (weight_ > Wmax) ? weight_ : Wmax;*/

    /* Send the spike to the target */
    e.set_receiver(*target_);
    e.set_weight(weight_);
    e.set_delay(delay_);
    e.set_rport(rport_);
    e();

    /* final clean up */
	// before clearing it, remember the last spike, for the case that the post synaptic spike is just before the next presynaptic spike
//    nest::double_t t_temp = post_spiketimes.back();
	post_spiketimes.clear();
//    post_spiketimes.push_back(0.);
//    cout << "after send, post_spiketimes holds: " << post_spiketimes.front() << endl;

//    K_vec.clear();

  } /* of BCPNNConnection::send */

} /* of namespace mynest */
#endif /* of #ifndef BCPNN_CONNECTION_H */