<jupyter_start><jupyter_text>Tutorial 3: Synapses and Networks Neuronal connectivityNeurons are connected at specific sites called **synapses**. Usually, the axon of a **presynaptic** neuron will make contact with the dendrite (or soma) of a **postsynaptic** in what's called a **chemical synapse**: **neurotransmitters** are transferred via the tiny gap between the two neurons (**synaptic cleft**) thanks to biochemical processes. This generates a change in membrane potential at the postsynaptic site, called **postsynaptic potential**. **Electrical synapses**, also known as **gap junctions**, can also be present -- in this case, specialized proteins make a direct electrical connection between neurons. [1]from [2]In this tutorial we will connect two or more neurons in NEST to create our first neuronal networks. First, we will learn the basic commands following [3] and [4], and then apply these to a well-known balanced network known as the **Brunel network**. By the end of this tutorial, you should be able to understand that a network's stability and activity behavior is deeply influenced by its parametrization. Sources:>[1] <NAME> and <NAME>, "Spiking Neuron Models". Cambridge University Press, 2002>[2] <NAME> and <NAME>, "Theoretical Neuroscience: Computational and Mathematical Modeling of Neural Systems". The MIT Press, 2001>[3] [PyNEST tutorial: Part 2, Populations of Neurons](https://nest-simulator.readthedocs.io/en/latest/tutorials/pynest_tutorial/part_2_populations_of_neurons.html)>[4] [PyNEST tutorial: Part 3, Connecting Networks with Synapses](https://nest-simulator.readthedocs.io/en/latest/tutorials/pynest_tutorial/part_3_connecting_networks_with_synapses.html)  Introduction: synapses in NESTAll synapse types included in NEST can be found in the following link:>[5] [NEST Docs: All synapse models](https://nest-simulator.readthedocs.io/en/latest/models/synapses.html)Alternatively, you can verify all synapse models present in NEST using the following command:<jupyter_code>import nest
nest.Models('synapses')<jupyter_output><empty_output><jupyter_text>Synapse typesThe simplest synapse type is the `static_synapse`. In this case, the **synaptic weight**, a measure of how strong is the influence of the presynaptic neuron on the postsynaptic neuron, is static, i.e. does not change over time. The synaptic transmission, however, is not instantaneous, and hence a **synaptic delay** is defined as the time between the presynaptic neuron activation (**action potential**) and the moment the postsynaptic potential is generated.In NEST, we can check the default values for all parameters using the `GetDefaults` function:<jupyter_code>nest.GetDefaults('static_synapse')<jupyter_output><empty_output><jupyter_text>Biological synapses, however, are constantly being created, destroyed and modified. Many models have been created to describe all of these processes. On synaptic modification, one of the most common models is the **spike-time dependent plasticity**. In this example, the synaptic weight changes according to the temporal order between pre and postsynaptic spike times.In NEST, the most common STDP mechanisms are implemented in `stdp_synapse`. The change for **normalized** synaptic weights is described by\begin{equation}    \Delta w = \begin{cases} - \lambda f_{-}(w) \times K_-(\Delta t) & \text{if $\Delta t \leq 0$,} \\    \lambda f_{+}(w) \times K_+(\Delta t) & \text{if $\Delta t > 0$,} \end{cases}\end{equation}where $\Delta t \equiv t_{post} - t_{pre}$ and the temporal filter is defined as $K_{(+,-)}(\Delta t) = \exp(-|\Delta t| / \tau_{(+,-)})$. The update functions\begin{equation}    f_{+}(w) = (1-w)^{\mu_{+}} \text{ and } f_{-}(w) = \alpha w^{\mu_{-}}\end{equation}create **synaptic potentiation** (stronger weights) when **causal spiking** is detected ($\Delta t > 0$), otherwise generating **synaptic depression** (weaker weights). This rule is also known as **temporally asymmetric Hebbian plasticity** and has been thoroughly studied under different parametrizations:|  STDP Type          |  Parametrization                 | Ref. ||---------------------|----------------------------------|------|| multiplicative STDP | $\mu_{+}=\mu_{-}=1.0$            | [7]  || additive STDP       | $\mu_{+}=\mu_{-}=0.0$            | [8]  || Guetig STDP         | $\mu_{+}=\mu_{-}=[0.0, 1.0]$     | [6]  || van Rossum STDP     | $\mu_{+}=0.0, \mu_{-} = 1.0$     | [9]  | Sources:> [6] Guetig et al. (2003). Learning input correlations through nonlinear temporally asymmetric hebbian plasticity. Journal of Neuroscience, 23:3697-3714 DOI: https://doi.org/10.1523/JNEUROSCI.23-09-03697.2003> [7] <NAME>, <NAME>, <NAME> (2001). Equilibrium properties of temporally asymmetric Hebbian plasticity. Physical Review Letters, 86:364-367. DOI: https://doi.org/10.1103/PhysRevLett.86.364> [8] <NAME>, <NAME>, Abbott LF (2000). Competitive Hebbian learning through spike-timing-dependent synaptic plasticity. Nature Neuroscience 3(9):919-926. DOI: https://doi.org/10.1038/78829> [9] van Rossum MCW, Bi G-Q, Turrigiano GG (2000). Stable Hebbian learning from spike timing-dependent plasticity. Journal of Neuroscience, 20(23):8812-8821. DOI: https://doi.org/10.1523/JNEUROSCI.20-23-08812.2000 Again, we can check the default values for all parameters of `stdp_synapse` using the `GetDefaults` function:<jupyter_code>nest.GetDefaults('stdp_synapse')<jupyter_output><empty_output><jupyter_text>You may have noticed that `tau_minus` is absent from the above list. For STDP synaptic models, the time constant of the depressing window of STDP is exceptionally a parameter of the **post-synaptic neuron**.<jupyter_code>nest.Create("iaf_psc_alpha", params={"tau_minus": 30.0})<jupyter_output><empty_output><jupyter_text>To change the default value of an **accessible** parameter, we can use the function `SetDefaults`. Note that *only some parameters listed by `GetDefaults` are changeable*. Please verify the details of the synapse type you want to use beforehand. [[5]](https://nest-simulator.readthedocs.io/en/latest/models/synapses.html)<jupyter_code>nest.SetDefaults("stdp_synapse",{"tau_plus": 15.0})<jupyter_output><empty_output><jupyter_text>Customized variants of a synapse model can be created using `CopyModel()`, and can be used anywhere that a built-in model name can be used.<jupyter_code>nest.CopyModel("stdp_synapse","layer1_stdp_synapse",{"Wmax": 90.0})<jupyter_output><empty_output><jupyter_text>When connecting multiple neurons, connectivity rules can be defined. Besides simple set-ups like `one_to_one` and `all_to_all`, sparse methods like `fixed_indegree`, `fixed_outdegree`, `fixed_total_number` and `pairwise_bernoulli` are also available. Please check [[3]](https://nest-simulator.readthedocs.io/en/latest/tutorials/pynest_tutorial/part_2_populations_of_neurons.html) to learn more details about creating and connecting neuron populations.<jupyter_code>epop1 = nest.Create("iaf_psc_delta", 10, params={"tau_m": 30.0})
epop2 = nest.Create("iaf_psc_delta", 10)
K = 5

conn_dict = {"rule": "fixed_indegree", "indegree": K}
syn_dict = {"model": "stdp_synapse", "alpha": 1.0}
nest.Connect(epop1, epop2, conn_dict, syn_dict)<jupyter_output><empty_output><jupyter_text>Synaptic parameters can also be randomly distributed by assigning a dictionary to the parameter. This should contain the target distribution and its optional parameters, as listed below:| Distributions | Keys             ||---------------|------------------|| `normal`      | `mu`, `sigma`    || `lognormal`   | `mu`, `sigma`    || `uniform`     | `low`, `high`    || `uniform_int` | `low`, `high`    || `binomial`    | `n`, `p`         || `exponential` | `lambda`         || `gamma`       | `order`, `scale` || `poisson`     | `lambda`         |<jupyter_code>neuron = nest.Create("iaf_psc_alpha")

alpha_min = 0.1
alpha_max = 2.
w_min = 0.5
w_max = 5.

syn_dict = {"model": "stdp_synapse",
            "alpha": {"distribution": "uniform", "low": alpha_min, "high": alpha_max},
            "weight": {"distribution": "uniform", "low": w_min, "high": w_max},
            "delay": 1.0}
nest.Connect(epop1, neuron, "all_to_all", syn_dict)<jupyter_output><empty_output><jupyter_text>Synapse information can be retrieved from its origin, target and synapse model using `GetConnections()`:<jupyter_code>nest.GetConnections(epop1, target=epop2, synapse_model="stdp_synapse")<jupyter_output><empty_output><jupyter_text>We can then extract the data using `GetStatus()`. Specific information can be retrieved by providing a list of desired parameters.<jupyter_code>conns = nest.GetConnections(epop1, synapse_model="stdp_synapse")
conn_vals = nest.GetStatus(conns, ["target","weight"])<jupyter_output><empty_output><jupyter_text>--- Example: connecting two neuronsIn this example, we will create and connect two neurons, A and B. Neuron A is of type `iaf_psc_delta` and receives external current $I_e = 376.0 pA$. This current is sufficient to elicit a spike every $\approx$ 50ms. Neuron B is solely connected to neuron A and hence can only spike if the input from A is strong enough. Let's observe how B can be influenced by A.First, verify the activity of neuron A running the block of code below.<jupyter_code>%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
import numpy as np

# reset kernel for new example

nest.ResetKernel()

neuron_A = nest.Create("iaf_psc_delta", 1, {"I_e": 376.0})

multimeter_A = nest.Create("multimeter", params={"withtime": True, "record_from":["V_m"]})
spikedetector_A = nest.Create("spike_detector", params={"withgid": True, "withtime": True})

nest.Connect(multimeter_A, neuron_A)
nest.Connect(neuron_A, spikedetector_A)

nest.Simulate(300.0)

# plot membrane potential and spiking activity

plt.rcParams['figure.dpi'] = 300
fig, ax = plt.subplots(2, 1, sharex=True, sharey=False)

multimeter_A_readout = nest.GetStatus(multimeter_A)[0]
V_A = multimeter_A_readout["events"]["V_m"]
t_A = multimeter_A_readout["events"]["times"]

spikedetector_A_readout = nest.GetStatus(spikedetector_A, keys="events")[0]
event_A = spikedetector_A_readout["senders"]
te_A = spikedetector_A_readout["times"]

ax[0].set_ylabel('V (mV)')

ax[0].plot(t_A, V_A, color="tab:blue", label="A")
ax[0].legend()   

ax[1].set_xlabel('time (ms)')
ax[1].set_ylabel('Spike times')
ax[1].plot(te_A, event_A, ".")

ax[1].set_yticks([1])
ax[1].set_yticklabels(["A"])

plt.show()<jupyter_output><empty_output><jupyter_text>Now let's create neuron B as `iaf_psc_delta` with no external current:<jupyter_code>neuron_B = nest.Create("iaf_psc_delta", 1, {"I_e": 0.0})

multimeter_B = nest.Create("multimeter", params={"withtime": True, "record_from":["V_m"]})
spikedetector_B = nest.Create("spike_detector", params={"withgid": True, "withtime": True})

nest.Connect(multimeter_B, neuron_B)
nest.Connect(neuron_B, spikedetector_B)<jupyter_output><empty_output><jupyter_text>We'll use the function below to easily plot the activity of the two neurons at the same time:<jupyter_code>def plot2neurons(multimeter_A, multimeter_B, spikedetector_A, spikedetector_B):
    multimeter_A_readout = nest.GetStatus(multimeter_A)[0]
    V_A = multimeter_A_readout["events"]["V_m"]
    t_A = multimeter_A_readout["events"]["times"]

    multimeter_B_readout = nest.GetStatus(multimeter_B)[0]
    V_B = multimeter_B_readout["events"]["V_m"]
    t_B = multimeter_B_readout["events"]["times"]
    
    spikedetector_A_readout = nest.GetStatus(spikedetector_A, keys="events")[0]
    event_A = spikedetector_A_readout["senders"]
    te_A = spikedetector_A_readout["times"]

    spikedetector_B_readout = nest.GetStatus(spikedetector_B, keys="events")[0]
    event_B = spikedetector_B_readout["senders"]
    te_B = spikedetector_B_readout["times"]        

    plt.rcParams['figure.dpi'] = 300
    fig, ax = plt.subplots(2, 1, sharex=True, sharey=False)    
    
    ax[0].set_ylabel('V (mV)')
    
    ax[0].plot(t_A, V_A, color="tab:blue", label="A")
    ax[0].plot(t_B, V_B, color="tab:orange", label="B")
    ax[0].legend()
    
    ax[1].set_ylim(0,5)
    ax[1].set_yticks([1,4])
    ax[1].set_yticklabels(["A","B"])
    ax[1].set_xlabel('time (ms)')
    ax[1].set_ylabel('Spike times')

    ax[1].plot(te_A, event_A, ".", color="tab:blue")
    ax[1].plot(te_B, event_B, ".", color="tab:orange")

    plt.show()
nest.Simulate(300.0)
plot2neurons(multimeter_A, multimeter_B, spikedetector_A, spikedetector_B)<jupyter_output><empty_output><jupyter_text>Neuron B is still inactive. We need to connect both neurons:<jupyter_code>nest.Connect(neuron_A, neuron_B, {"rule": "one_to_one"}, {"model": "static_synapse"})

nest.Simulate(300.0)
plot2neurons(multimeter_A, multimeter_B, spikedetector_A, spikedetector_B)<jupyter_output><empty_output><jupyter_text>Suggest at least 3 alterations we can make in this example to elicit spiking activity from B.Run the code below and check if B spikes using the spike time raster (bottommost graph). Additionally, check what happens if the neuron model is different: for example, change `iaf_psc_delta` to `hh_psc_alpha`.Finally, connect B to A reciprocally and check their dynamics. How can you describe their behavior?<jupyter_code>nest.ResetKernel()

neuron_A = nest.Create("iaf_psc_delta", 1, {"I_e": 376.0})

multimeter_A = nest.Create("multimeter", params={"withtime": True, "record_from":["V_m"]})
spikedetector_A = nest.Create("spike_detector", params={"withgid": True, "withtime": True})

nest.Connect(multimeter_A, neuron_A)
nest.Connect(neuron_A, spikedetector_A)

neuron_B = nest.Create("iaf_psc_delta", 1, {"I_e": 0.0})

multimeter_B = nest.Create("multimeter", params={"withtime": True, "record_from":["V_m"]})
spikedetector_B = nest.Create("spike_detector", params={"withgid": True, "withtime": True})

nest.Connect(multimeter_B, neuron_B)
nest.Connect(neuron_B, spikedetector_B)

nest.Connect(neuron_A, neuron_B, {"rule": "one_to_one"}, {"model": "static_synapse", "weight": 1.})

nest.Simulate(300.0)
plot2neurons(multimeter_A, multimeter_B, spikedetector_A, spikedetector_B)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1375755421258443, 'fraction_numerical': 0.02012086740135087, 'mean_word_length': 5.46118511713367, 'pattern_counts': {'":': 43, '<': 70, '<?xml version=': 0, '>': 81, 'https://': 9, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '488255', 'license': 'MIT', 'n_tokens_mistral': 4989, 'n_tokens_neox': 4540, 'n_words': 1291, 'path': 'Tutorial-3_Synapses-and-Networks/Synapses.ipynb', 'repo_name': 'oist-ncbc/skill-pill-plus', 'size': '14106'}