A biologically plausible decision-making model based on interacting neural populations.
The 6 matches
- [1] § Materials and methods › Model › Basic module. ↔ AdExMFForDecisionMakingPythonNb/MainNotebook.ipynb, lines 51–194 · score 0.80 · leakage potential, Dirac delta function, white Gaussian noise, partial derivatives, intensity, notation
- [2] § Materials and methods › Model › Basic module. ↔ CodePackage_A_ biologically_ plausible_ decision_making model/AdExMFForDecisionMakingPythonNb/MainNotebook.ipynb, lines 24–163 · score 0.80 · leakage potential, Dirac delta function, white Gaussian noise, partial derivatives, intensity, notation
- [3] § Materials and methods › Model › Basic module. ↔ CodePackage_A_ biologically_ plausible_ decision_making model/AdExMFForDecisionMakingPythonNb/MainNotebook.ipynb, lines 24–163 · score 0.77 · asynchronous irregular, awake state, AI state, AdEx, anesthesia, biased
- [4] § Materials and methods › Model › Basic module. ↔ AdExMFForDecisionMakingPythonNb/MainNotebook.ipynb, lines 51–194 · score 0.77 · asynchronous irregular, awake state, AI state, AdEx, anesthesia, biased
- [5] § Materials and methods › Noise sources › Exploratory behavior. ↔ AdExMFForDecisionMakingPythonNb/MainNotebook.ipynb, lines 693–785 · score 0.57 · deviation block, cluster deviations, initial episodes, cluster episode, Learning speed
- [6] § Materials and methods › Noise sources › Exploratory behavior. ↔ CodePackage_A_ biologically_ plausible_ decision_making model/AdExMFForDecisionMakingPythonNb/MainNotebook.ipynb, lines 643–735 · score 0.57 · deviation block, cluster deviations, initial episodes, cluster episode, Learning speed
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Jupyter notebook · 905 lines · 61 KB · CC-BY-4.0 · 3 matches
- # %% [markdown]
- # # Notebook of a biophysically plausible decision-making model based on interacting cortical columns
- # by Emre Baspinar, CNRS, NeuroPSI, Laboratory of Computational Neuroscience, Paris-Saclay
- #
- # This notebook contains the implementation of our biophysically realistic AdEx mean-field model proposed for a reward-driven consequential decision-making task. The implementation is in Python 3.8.5. The code writing and the preparation of the Jupyter notebook was done by Emre Baspinar. The model design was contributed by Emre Baspinar, Gloria Cecchini (U. of Barcelona), Rubén Moreno Bote (Pompeu Fabra University, Barcelona), Ignasi Cos (U. of Barcelona) and Alain Destexhe (CNRS, NeuroPSI, Paris-Saclay). The related manuscript [1] is a joint work of Emre Baspinar, Gloria Cecchini, Michael DePass (U. of Barcelona), Marta Andujar (U. of Rome), Pierpaolo Pani (U. of Rome), Stefano Ferraina (U. of Rome), Rubén Moreno Bote, Ignasi Cos and Alain Destexhe. See [1] for more information.
- #
- # This notebook is licensed by Creative Commons Attribution 4.0 International Public License (CC BY 4.0). Please cite [1] and [7] if you use this notebook in your work.
- #
- # Please feel free to contact for questions, comments and feedback.
- #
- # Contact: [email hidden]
- #
- #
- #
- # This notebook provides the implementation of the decision-making AdEx mean-field model which was presented in [1]. It provides the implementation, an example simulation of the behavioral experiments related to the decision-making paradigm considered in [1, 2], and three case studies. The results of the example simulation are saved automatically in the notebook folder in case of that the user wants to analyse the results. The default values for the simmulations are provided and they can be easily changed by the user. For more description regarding the model and the technical details, please see *ModelSummary.pdf* and *README.pdf*.
- # %% [markdown]
- # # Glossary
- #
- # <u>MainNotebook.ipnyb</u>: This is the main notebook to run the simulations and the case studies. It contains explanations related to the model and to the simulations. This file uses the .py files given below.
- #
- # <u>cell_library.py</u>: It contains the parameters of the biophysical cell properties of the neurons. Do not change unless you add new cell types.
- #
- # <u>DiffOperator.py</u>: It contains the stochastic differential equations of the AdEx mean-field equations corresponding to both cortical columns.
- #
- # <u>NeuronConnectivity.py</u>: It contains the functions which we use to load the transfer functions of Regular Spiking (RS) and Fast Spikin (FS) cells by using the method explained in~\cite{zerlaut2018modeling}. The transfer functions and their parameters are based on a fitting to experimental data, therefore the parameters should be kept fixed. Do not change this file.
- #
- # <u>SDEIntegrator.py</u>: This is the Euler-Maruyama integrator. It integrates the SDEs found in DiffOperator.py in time.
- #
- # <u>syn_and_connec_library.py</u>: It contains the connectivity and synaptic properties of the neurons.
- #
- # <u>theoretical\_tools.py</u>: It contains implementation of some analytical functions appearing in the AdEx mean-field equations.
- #
- # In addition to these .py files, there are two .npy files in *data* folder: FS-cell_CONFIG1_fit.npy and RS-cell_CONFIG1_fit.npy. They contain the fitted parameters of RS and FS cell transfer functions to the experimentally obtained RS and FS cell response vlaues. Do not change the location of these files. Finally, *showcaseData* folder contains the data which MainNotebook.ipnyb uses for the case studies.
- #
- # This notebook uses Matplotlib, Nump, Random, Os and Scipy.io libraries.
- # %% [markdown]
- # ## Initialization
- # %%
- import matplotlib.pyplot as plt
- import numpy as np
- import random
- import os
- import scipy.io
- # import operator
- from SDEIntegrator import RegulatoryPsi, TimeStepping
- from NeuronConnectivity import ReformatSynParameters, LoadTransferFunctions
- # %% [markdown]
- # ## Introduction
- # Our model contains two pools where each pool is composed of an excitatory and an inhibitory population. Each population is modeled as a mean-field adaptive exponential integrate-and-fire (AdEx) model, which was derived in [3] from the AdEx network model [4] by following the *master equation formalism* [5, 6], by including adaptive dynamics as well. This model was proposed for only one excitatory and one inhibitory population, that is, for only one pool. We extend this model to a double pool architecture, where now also cross connections and correlations between the cross-populations must be considered. The challenge, besides the increased dimension of the mean field model, is to implement the cross-connections between the pools, since it requires the derivatives of cross-population covariances. Yet, it allows to take into account biophysical mechanisms which involve in the decision making process and which were not taken into account previously.
- # %% [markdown]
- # ## Experimental task
- # Experimental task was applied on healthy human subjects. Each session consists of a number of episodes. Each episode is composed of a fixed number of trials. The number of trials are determined by the horizon number. If it is 0, then we have 1 trial per episode, and if it is 1, we have 2 trials per episode. In each trial, the subject is asked to choose between two stimuli, of which each one is a partially filled vertical bar. The amount of the filling is different for each bar. The total height of the bars are identical. Depending on the choice the subject makes and the implemented strategy for every episode, the subject has a reward or loss for the next trial. The reward is a fixed amount of gain added to the filled parts of both stimuli, and the loss is the same amount of gain subtracted from the filled parts of both stimuli. Each bar is located either on the right hand side or on the left hand side of the screen, and their positions might be randomly exchanged between two trials. The sum of the chosen stimuli with the reward or the loss obtained at the end of each trial of each episode is accepted as the total result of the episode. More precisely, if we denote the chosen stimulus plus the reward or loss at the end of the $i$th trial by $A_i$, then the total result of an episode with $N\in \mathbb{Z}$ trial is
- # \begin{equation}
- # \text{Total reward} = \sum\limits_{i=0}^N = A_i.
- # \end{equation}
- # The goal of the subject is to maximize the total result in each episode, that is, to be rewarded as many times as possible in each episode by making decisions in accordance with the implemented strategy. The behavioral performance of the subject is scored at the end of each episode as a scaled measure of the total reward between 0 and 1.
- # %% [markdown]
- # |  |
- # |:--:|
- # | <b>Fig. 1: Example episode with 2 trials. Implemented strategy is to choose small and large stimuli in the first and second trials, respectively. Subject makes the bad choice in the first trial and receives a loss. In the second she/he makes the good choice, thus receives a reward. The total result is the sum of the chosen stimuli (after their reward or loss), which are highlighted by the orange dashed rectangles. Corresponding performance results is the total result normalized by the sum of the stimuli corresponding to the maximum total result, i.e., by the double rewarded total result. The total result is not explicitly shown to the participant. </b> |
- # %% [markdown]
- # ## Double-column AdEx mean field model
- # Departure point of this study is the AdEx mean-field model which was proposed in [3]. We extend to a bicolumnar setting the model equations and the simulation framework provided in [3], which was for only a single cortical column.
- #
- # The extended model equations are as follows:
- # \begin{equation}
- # \begin{split}
- # T \partial_t v_\alpha & = (F_\alpha -v_\alpha) + \frac{1}{2} C_{\xi\eta}\partial_{\xi\eta}F_\alpha +\sigma \omega_\alpha\\
- # T\partial_t C_{\alpha\beta} & =\delta_{\alpha\beta}A_{\alpha\alpha} + (F_\alpha-v_\alpha)(F_\beta-v_\beta) + C_{\beta\xi}\partial_\xi F_\alpha + C_\alpha\xi\partial_\xi F_\eta-2 C_{\alpha\beta}\\
- # \partial_t W_\alpha & = -\frac{W_\alpha}{\tau_w} + (\delta_{\alpha e_A}+\delta_{\alpha e_B})(b v_\alpha + a(\mu_V(v_{e_A}, v_{e_B}, W_\alpha) - E_L)),\hspace{12cm}(1)
- # \end{split}
- # \end{equation}
- # where
- # \begin{equation}
- # \begin{split}
- # F_{e_A}=F_{e_A}(\tilde{v}_{e_A}, \tilde{v}_{i_A}, W_{e_A}), & \quad\quad
- # F_{i_A}=F_{e_A}(\tilde{v}_{e_A}, \tilde{v}_{i_A}, W_{i_A}),\\
- # F_{e_B}=F_{e_B}(\tilde{v}_{e_B}, \tilde{v}_{i_B}, W_{e_B}), &\quad\quad
- # F_{i_B}=F_{e_A}(\tilde{v}_{e_B}, \tilde{v}_{i_B}, W_{i_B}),
- # \end{split}
- # \end{equation}
- # are the population transfer functions with the corresponding population indicated as subindex and where $\omega_\alpha=w_\alpha(t)$ is a white Gaussian noise generated independently for each time instant and for each population. More precisely,
- # $$
- # \mathbb{E}[\omega_\alpha(t)]=0,\;\text{for all}\; t\geq 0,\quad\text{and}\quad \mathbb{E}[\omega_\alpha(t) \omega_\beta(t^\prime)]=\delta_{\alpha\beta}\delta_{tt^\prime}.
- # $$
- # Here $\sigma>0$ shows the noise intensity level. Function $A_{\alpha\beta}$ is defined as follows:
- # $$
- # A_{\alpha\beta}=\delta_{\alpha\beta}\frac{N_\alpha}{F_\alpha(1/T-F_\beta)},
- # $$
- # with $N_\alpha$ denoting the number of neurons in population $\alpha$. Here $T$ is the time scale parameter both for the firing rate and for the covariance variables appearing in (1). We refer to [Section 2.3.1, 3] for a detailed explanation of $\mu_V$ appearing in the last line of (1). The time dependency of the terms are tacitly understood from (3). We use the notation given by $\partial_\alpha = \frac{\partial}{\partial v_\alpha}$ and $\partial_{\alpha\beta} = \frac{\partial^2}{\partial {v_\alpha}\partial_{v_\beta}}$ for the partial derivatives. Here $\delta$ is the Dirac delta function, $E_L$ is a constant representing the reversal leakage potential, and finally $\tau_w$ is a time scale-like parameter for $W_\alpha$. The latter determines the evolution time scale of $W_\alpha$ approximately. We denote the total excitatory input firing rate for each transfer function by using the tilde notation. We express the probability of cross-connections with $p_c$. Cross-connections are always from excitatory populations, therefore, $p_c$ can be thought of as the ratio of exciatory neurons to the total number of neurons in the whole double pool network. Then we write the total excitatory input firing rates for the transfer functions as follows:
- # \begin{equation}
- # \begin{split}
- # \tilde{v}_{e_A}(t) & = v_{e_A}(t)+ v_{\text{AI}}+ \lambda^A_{w_r}(v_A(t), v_B(t))+w^e_c \Big ( v_{e_B}(t) + v_{\text{AI}}+\lambda^B_{w_r}(v_A(t), v_B(t), t) \Big ) p_c N_{e_B},\\
- # \tilde{v}_{i_A}(t) & = v_{i_A}(t)+ \lambda^A_{w_r}(v_A(t), v_B(t)) + w^i_c \Big ( v_{e_B}(t) + v_{\text{AI}}+\lambda^B_{w_r}(v_A(t), v_B(t), t) \Big ) p_c N_{e_B},\\
- # \tilde{v}_{e_B}(t) & = v_{e_B}(t) + v_{\text{AI}}+\lambda^B_{w_r}(v_A(t), v_B(t))+ w^e_c \Big ( v_{e_A}(t) + v_{\text{AI}}+\lambda^A_{w_r}(v_A(t), v_B(t)) \Big ) p_c N_{e_A},\\
- # \tilde{v}_{i_B}(t) & = v_{i_B}(t)+ \lambda^B_{w_r}(v_A(t), v_B(t)) + w^i_c \Big ( v_{e_A}t)+v_{\text{AI}}+\lambda^A_{w_r}(v_A(t), v_B(t), t) \Big ) p_c N_{e_B},\hspace{10cm} (2)
- # \end{split}
- # \end{equation}
- # where $\lambda$ represents the firing rate of the regulatory pool, which is explained in the next section. In (3), all terms for which the time dependency is not explicit are constants.
- # %% [markdown]
- # | |
- # |:--:|
- # | <b>Fig. 2: Mean-field model. Each cortical column is represented as one pool of excitatory ($e_A$, $e_B$) and inhibitory ($i_A$, $i_B$) neuronal populations. Excitatory (blue) and inhibitory (red) in-pool connection weights are denoted by $w_+=w_-=1$, respectively. Cross-pool connections model intercolumnar long range connections in the prefrontal cortex, thus they are only excitatory. The cross-pool connections towards excitatory and inhibitory populations are weighted by $0<w_c^e=w_c^i<1$. Here $V_{\text{ext}}=v_{\text{AI}}$ represents the based-drive potential keeping the model in the asynchronous irregular (awake) state. Finally, $\lambda$ represents the regulatory mechanism introducing the bias to the input stimuli as explained in the next section.</b> |
- # %% [markdown]
- # ## Regulatory mechanism
- #
- # We interpret $v_A$ and $v_B$ as the stimuli with large quantity sample and the small quantity sample, respectively. In the experiment task conducted on human subjects, one of them appears on the left hand side of the screen and the other one appears on the right hand side. Those positions of the stimuli might be shuffled at each trial. The decision is made for choosing either $v_A$ or $v_B$, and it depends mostly on the regulatory pool. The regulatory pool weights both stimuli in accordance with the decision to be made. In this way, it promotes one of the stimulus, making more likely that the pool sensitive to the promoted stimulus has a higher firing rate compared to the firing rate of the other pool. This results in that the pool with the higher firing rate dominates the other pool and the decision is made for the promoted stimulus. We find the time instant where the decision is made as the moment where the difference between the firing rates of the excitatory populations of two pools exceeds a prefixed value.
- #
- # We model the regulatory pool evolving during the $i$th trial of the $E$th episode in terms of the following stochastic differential system:
- # \begin{equation}
- # \begin{cases}
- # \tau_\psi \frac{d\psi^E_i(t)}{dt} = -4 \psi^E_i(t) \Big ( \psi^E_i(t)-1 \Big )\Big ( \psi^E_i(t)-1/2 \Big )+\frac{\sigma_r}{(c_0 t)^2}\zeta_i\\
- # \psi_i^E(0)= \phi^{E-1}_i,\hspace{15cm}(2)
- # \end{cases}
- # \end{equation}
- # where $\tau_\psi$ is the time scale parameter, $\psi$ is the firing rate of the regulatory pool evolving in time independently for each trial, $\zeta_i = \zeta_i(t)$ is white Gaussian noise whose intensity level is determined by a temporally scaled version of $\sigma>0$, and finally $c_0>0$ is a constant. Here $E$ and $i$ denote the episode number and the trial number within the corresponding episode, respectively. The time dependent denominator appearing in front of this noise term introduces a strong stochastic behavior to the regulatory pool at the beginning of its corresponding trial, and this allows us to take into account that the system learns the strategy by exploring it initially over the episodes. Moreover, $c_0$ determines how much the model has the flexibility to deviate from the captured strategy once it learns the strategy. This is important to model the deviations occurring due to either perceptual difficulty to distinguish two stimuli or the human factor which makes the subject sometimes not follow the strategy to explore what happens. The evolution process (2) is restarted at the beginning of each trial by setting the initial condition to the value determined by the reward $\phi^E_i$ (to be explained later) corresponding to the same trial but of the precedent episode, i.e., of the $(E-1)$th episode. In this way, the reward mechanism provides for each trial a feedback to the regulatory pool at the end of the $(E-1)$th episode. This feedback determines towards which decision the regulatory pool will be biased in each trial of the $(E-1)$th episode. The final time of the evolution given by (2) is the final time of each trial and it is the same for every trial.
- #
- #
- # The regulatory pool activation is transmitted to Pool $A$ and Pool $B$ populations via $\lambda$ function appearing in (2). Its explicit form is given by
- # \begin{equation}\label{eq:lambdaExpression}
- # \begin{split}
- # \lambda^A_{w_r}(v_A(t), v_B(t), t) = & \psi\,v_A(t) + w_r(1-\psi(t))\,v_B(t),\\
- # \lambda^B_{w_r}(v_A(t), v_B(t), t) = & \psi\,v_B(t) + w_r(1-\psi(t))\,v_A(t),
- # \end{split}
- # \end{equation}
- # where $w_r$ is a constant (which we choose to be 1 for now).
- # %% [markdown]
- # ## Parameters and initial conditions
- # Please refer to [1, 3] for more details about the parameters. Here we set the parameters in a way that the model is always in asynchronous irregular (AI) state, which is the awake state. We focus on rather AI state here, but it is possible to set the model to up-and-down state (sleep/anesthesia state) by changing the parameters; see [1] for proper parameters corresponding to up-and-down state.
- #
- # <b>Do not forget to choose if SI units are considered or not! Note that the default parameters given below are without SI units!</b>
- #
- # <b>The default AdEx parameters are for the simulations in this notebook, which are related to the decision-making tasks considered in [1]. Please do not change them unless you would like to use the notebook for other simulations than the ones included here.</b>
- # %%
- # AdEx system parameters
- siUnits=int(input("Biophysical parameters without --> 0 or with --> 1 SI units?"))
- if siUnits==0:
- ## without SI units (in the poster-paper, we use without SI units)
- aRS, bRS, aFS, bFS, tauwRS, tauwFS = 4, 40, 0, 0, 5000, 1e9 # a, b and tauw parameters of adaptation mechanism for RS and FS cells
- Ntot = 20000 # total number of neurons in the corresponding network
- pc = 0.80 # ratio: Ne/Ntot, where Ne is the number of excitatory cells (RS cells)
- Ne = Ntot * pc/2 # number of excitatory cells in one pool (RS cells)
- Ni = Ntot * (1-pc)/2 # number of inhibitory cells in one pool (FS cells)
- vAI = 5 # base potential keeping the population pairs in asynchronous irreguler (AI) state (awake state)
- wce = 2.5e-4 # cross-connection weight from excitatory to excitatory population
- wci = 2.5e-4 # cross-connection weight from excitatory to inhibitory population
- sigma = 0.01 # intrinsic noise level (for the noise appearing in firing rate equations in (1))
- El = -65 # reversal leakage potential
- Qe, Qi = 1.5, 5 # excitatory and inhibitory quantal conductances
- Te, Ti = 5, 5 # decay (refractory) time scale of excitatory and inhibitory synapses
- Gl = 10 # leak conductance
- Ee, Ei = 0, -80 # excitatory and inhibitory reversal potentials
- tF, dt = 15, 0.05 # final time and time step of the time integration via Euler-Maruyama
- T = 5 # time scale for the fast variables
- tauPsi = T # time scale of the regulatory mechanism
- sigma_r = 0.01 # Extrinsic noise level (noise in the regulatory mechanism)
- c0 = 1.0 # c0 constant determining the decay rate of the extrinsic noise
- elif siUnits==1:
- ## with SI units
- aRS, bRS, aFS, bFS, tauwRS, tauwFS = 4e-9, 40e-12, 0, 0, 5, 1e6 # a, b and tauw parameters of adaptation mechanism for RS and FS cells
- Ntot = 20000 # total number of neurons in the corresponding network
- pc = 0.80 # ratio: Ne/Ntot, where Ne is the number of excitatory cells (RS cells)
- Ne = Ntot * pc/2 # number of excitatory cells in one pool (RS cells)
- Ni = Ntot * (1-pc)/2 # number of inhibitory cells in one pool (FS cells)
- vAI = 5 # base potential keeping the population pairs in asynchronous irreguler (AI) state (awake state)
- wce = 2.5e-4 # cross-connection weight from excitatory to excitatory population
- wci = 2.5e-4 # cross-connection weight from excitatory to inhibitory population
- sigma = 0.01 # intrinsic noise level (for the noise appearing in firing rate equations in (1))
- El = -65e-3 # reversal leakage potential
- Qe, Qi = 1.5e-9, 5e-9 # excitatory and inhibitory quantal conductances
- Te, Ti = 5e-3, 5e-3 # decay (refractory) time scale of excitatory and inhibitory synapses
- Gl = 10e-9 # leak conductance
- Ee, Ei = 0, -80e-3 # excitatory and inhibitory reversal potentials
- tF, dt = 6, 0.05 # final time and time step of the time integration via Euler-Maruyama
- T = 5e-3 # time scale for the fast variables
- tauPsi = 10*T # time scale of the regulatory mechanism
- sigma_r = 0.01 # Extrinsic noise level (noise in the regulatory mechanism)
- c0 = 45 # c0 constant determining the decay rate of the extrinsic noise
- # We gather all parameters together in the vector "params"
- params = np.zeros(28)
- params[0] = aRS
- params[1] = bRS
- params[2] = aFS
- params[3] = bFS
- params[4] = tauwRS
- params[5] = tauwFS
- params[6] = Ntot
- params[7] = pc
- params[8] = Ne
- params[9] = Ni
- params[10] = vAI
- params[11] = wce
- params[12] = wci
- params[13] = sigma
- params[14] = El
- params[15] = Qe
- params[16] = Qi
- params[17] = Te
- params[18] = Ti
- params[19] = Gl
- params[20] = Ee
- params[21] = Ei
- params[22] = tF
- params[23] = dt
- params[24] = T
- params[25] = tauPsi
- params[26] = sigma_r
- params[27] = c0
- # %% [markdown]
- # We set below the initial conditions ad hoc to the decision-making tasks which we consider in [1].
- #
- # <b> These are the default initial conditions for the simulations in this notebook, which are related to the decision-making tasks considered in [1]. Do not change them unless you would like to use the notebook for other simulations than the ones included here.
- # %%
- # Initial conditions for state variables
- v0 = 1. # v_eA : V[0]
- v1 = 30. # v_iA : V[1]
- v2 = 0.5 # C_eAeA : V[2]
- v3 = 0.5 # C_eAiA = C_iAeA : V[3]
- v4 = 0.5 # C_iAiA : V[4]
- v5 = 1.e-10 # W_eA : V[5]
- v6 = 0. # W_iA : V[6]
- v7 = 1. # v_eB : V[7]
- v8 = 30. # v_iB : V[8]
- v9 = 0.5 # C_eBeB : V[9]
- v10 = 0.5 # C_eBiB : V[10]
- v11 = 0.5 # C_iBiB : V[11]
- v12 = 1.e-10 # W_eB : V[12]
- v13 = 0. # W_iB : V[13]
- v14 = 0.05 # C_eAeB : V[14]
- v15 = 0.05 # C_eAiB : V[15]
- v16 = 0.05 # C_iAeB : V[16]
- v17 = 0 # C_iAiB : V[17]
- psi0 = 0.5 # initial condition for the regulatory mechanism at the beginning of the first trial in each episode
- V0 = [v0, v1, v2, v3, v4, v5, v6, v7, v8,\
- v9, v10, v11, v12, v13, v14, v15, v16, v17]
- # %% [markdown]
- # ## Single-trial simulation setup
- # We first initialize the stimuli A and B, in a way that one of them is weaker in comparison to the other.
- # %%
- # Define the stimuli for one trial
- from scipy.special import comb
- def smoothstep(x, x_min=0, x_max=1, N=1):
- x = np.clip((x - x_min) / (x_max - x_min), 0, 1)
- result = 0
- for n in range(0, N + 1):
- result += comb(N + n, n) * comb(2 * N + 1, N - n) * (-x) ** n
- result *= x ** (N + 1)
- return result
- t = np.linspace(0, tF, int(tF/dt)+1) # define the whole time interval
- ampA = 1 # amplitude of stimulus A
- ampB = 10 # amplitude of stimulus B
- t0 = 2 # initial time instant of the stimuli
- stimulusA = ampA*(smoothstep(t-t0)-smoothstep(t-t0-tF))
- stimulusB = ampB*(smoothstep(t-t0)-smoothstep(t-t0-tF))
- # %% [markdown]
- # Here we load the transfer functions which were derived for both regularly spiking (RS, excitatory) and fast spiking (FS, inhibitory) neural populations via the semi-analytical approach given in [6]. Once they are loaded, we can run a one-trial simulation.
- # %%
- ## Build the simulation setup: load the transfer functions for both RS & FS populations
- TF1, TF2 = LoadTransferFunctions('RS-cell', 'FS-cell', 'CONFIG1')
- lambdaA, lambdaB, psi = RegulatoryPsi(psi0, stimulusA, stimulusB, params) # generate the regulated stimuli for the trial
- # Time integration for one single trial
- state = TimeStepping(V0, lambdaA, lambdaB, TF1, TF2, params)
- # %% [markdown]
- # Now we can plot the evolution of firing rates of excitatory populations in both A and B pools. Those plots are example of one trial simulations, in which the reward is not included yet. The population which wins the competition makes the decision in favor of the stimulus which it is selective to. We say that the population having the higher firing rate wins the competition when the difference betweenthe firing rates exceed a certain threshold. The threshold is arbitrary and fixed to a constant at the beginnning of the whole experiment session. In the next step we will consider multiple episodes, where each epsiode contains one trial. In those simulations, we will introduce also the reward mechanism.
- # %%
- # %matplotlib widget
- # Plot Pool A (red) and B (blue) excitatory population firing rates
- plt.figure(figsize=(6, 4))
- plt.plot(t, state[:, 0], 'r') # plotting v_{eA} (red)
- plt.plot(t, state[:, 7], 'b') # plotting v_{eB} (blue)
- # plt.margins(x=0.001, y=-0.001)
- # %% [markdown]
- # Now it is time to introduce the reward mechanism. This will allow us to extend our simulation framework to multiple episodes, where each episode contains one (Horizon 0) or two (Horizon 1) trials, depending on which horizon we would like to study.
- # %% [markdown]
- # ## Reward mechanism
- # Reward mechanism is the key module introducing an online learning to the model. It allows the model to capture the strategy maximizing the gain through each episode. Once the strategy is learned, the system makes the decisions in the way which maximizes the final gain, which is the total reward of one episode. This is the key mechanism endowing the model with working memory.
- #
- # Reward mechanism is updated through a discrete evolution where the temporal variable is the trial number. In other words, the reward function value remains constant during a trial and it is updated at the end of the trial. This updated value at the end of the trial is fed to the same trial corresponding to the episode coming after as the initial condition of the regulatory function $\psi$; see Eq. (2). We use the notation $M^E_i$ to denote the mean value of the stimuli corresponding to $i$the trial of the $E$th episode. We write $N$ with $N-1$ expressing the number of the last trial in one episode to refer to the total reward obtained at the end of the episode, although $N$th trial does not exist. Then we write the evolution for the reward function $\phi$ as follows:
- # \begin{equation}\label{eq:rewardMechanismEqn}
- # \begin{cases}
- # \phi^{E+1}_i = \phi^E_i + k ( M^E_{i+1} - M^E_i ) (2\psi_i^E(T)-1)(\phi^E_i -1 )^2(\phi_i^E)^2,\\
- # \phi^0_i = C,\quad \text{for all}\quad i\in \{0, 1,\dots, N-1 \},\quad\quad\quad\quad\quad\quad\quad\quad\quad (3)
- # \end{cases}
- # \end{equation}
- # with $C$ denoting a constant, which is fixed to $0.5$ for the first episode in our framework. This system initiates the reward mechanism for each trial series over the episodes separately, yet the trials are not independent due to the coupling effect arising from $( M^E_{i+1} - M^E_{i})$ factor.
- #
- # Reward mechanism is implemented as given below, in an integrated way to the simulation setup.
- # %% [markdown]
- # ## Model in action: simulations
- #
- # Each simulation session is composed of a certain number (nOfEpisodes) of episodes. Here we fix nOfEpisodes to 5 for simplicity although in our simulations we use higher values (e.g., 100). In Horizon 0, each episode has one single trial. In Horizon 1, each episode has two trials. See [1] for more details about the experiments.
- # %%
- # Parameters for episode initializations
- nOfEpisodes = 5 # number of episodes in the whole session. In [1], it was selected as 100.
- dSet = [0.01,0.05,0.1,0.15,0.2] # difficulty set of visual discriminization. Please do not change these values.
- # difficulty is proportional to the difference between the amplitudes of the two stimuli
- dMax = max(dSet) # max. value of the difficulty
- nH =int(input("Enter the horizon number (0 or 1): "))# horizon number: n. of trials per episode = nH + 1
- gain = 0.3 # reward gain. Please do not change this value.
- if nH==0:
- minStim = gain # min. value of the stimuli (for Horizon 0)
- maxStim = minStim + 6 # max. value of the stimuli (for Horizon 0)
- elif nH==1:
- minStim = 2 * gain # min. value of the stimuli (for Horizon 1)
- maxStim = minStim + 6 # max. value of the stimuli (for Horizon 1)
- else:
- print("Invalid horizon number! Choose either 0 or 1.")
- reward = np.zeros((nH+1, nOfEpisodes)) # initialize reward list
- decisionThreshold = 5 # the decision is made once the absolute value of the difference between excitatory population firing rates exceeds this value. Please do not change this value.
- # Initialize the vectors which store the recorded results
- performance = np.zeros(nOfEpisodes) # performance vector saving performance results of each episode
- decisionTimeList = np.zeros(nOfEpisodes * (nH+1)) # vector saving the decision times of each trial
- difficultyList = np.zeros(nOfEpisodes * (nH+1)) # vector saving the difficulty values used in each episodeNow it is time to introduce the reward mechanism. This will allow us to extend our simulation framework to multiple episodes with each episode containing one (Horizon 0) or two (Horizon 1) trials, depending on which horizon we would like to study.
- # %% [markdown]
- # We perform each simulation session for several times (nOfIterations) to be able to obtain statistics allowing us to quantify the behavioral results produced by the model framework and to compare them to the experimental data. We fit the model to the experimental data by tuning two parameters: learning speed ($k$ as given in Eq. (3)) and noise decay rate ($c_0$ as given in Eq. (2)). The user can chose one fixed value for either one of those two parameters and vary the other by assigning a vector of several value. <b>Please do not put comma between varied values.</b> Example--> Enter the k values: 0 0.025 0.050 0.075 1
- #
- # <b>DEFAULT VALUES:</b>
- #
- # I. For Horizon 0 with fixed $k$ and varied $c_0$: $k=0.3$ and $c_0$ is varied between 1.0 and 1.9.
- #
- # II. For Horizon 0 with fixed $c_0$ and varied $k$: $c_0=1.1$ and $k$ is varied between 0.05 and 0.5.
- #
- # III. For Horizon 1 with fixed $k$ and varied $c_0$: $k=0.05$ and $c_0$ is varied between 1.0 and 2.0.
- #
- # IV. For Horizon 1 with fixed $c_0$ and varied $k$: $c_0=2.0$ and $k$ is varied between 0 and 0.10.
- #
- # These simulations were used to produce the results found in Figure 8 and 14 in [1].
- # %%
- # Iteration parameters
- nOfIterations = int(input("Enter the number of iterations per each experiment session: ")) # number of how many times we perform each simulation session. In [1], it was selected 10.
- fixedPar = int(input("Enter 0 to fix k (learning speed), enter 1 to fix c0 (decay rate): ")) # determine which parameter will be fixed and the other will be varied
- if fixedPar==0:
- learningSpeed=float(input("Enter fixed k (learning speed) value: "))
- # nc0Val=int(input("Number of c0 values: ")) # enter for how many values of c0 will be the experiment performed
- c0List=list(map(float, input("Enter the c0 values: ").strip().split())) # Enter the values as, e.g., 1 2 5 7, without any comma in between
- lengthParameterList = len(c0List)
- nc0Val = lengthParameterList
- elif fixedPar==1:
- c0=float(input("Enter fixed c0 (decay rate) value: "))
- # nkVal=int(input("Number of k values: ")) # enter for how many values of k will be the experiment performed
- learningSpeedList=list(map(float, input("Enter the k values: ").strip().split())) # Enter the values as, e.g., 1 2 5 7, without any comma in between
- lengthParameterList = len(learningSpeedList)
- nkVal = lengthParameterList
- # %% [markdown]
- # Now we can run the simulation for the values entered in the above code box. <b>Results will be saved automatically to the notebook folder with the parameters noted in the title.</b> These results can be used for the statistical analysis of the simulation results. <b>You should comment out the three np.save lines at the end of the code block to turn this option off.</b>
- # %%
- # Run the simulations by varying the parameter chosen in the previous command box
- for parameterNo in range(lengthParameterList):
- iteration = 0
- if fixedPar==0:
- c0 = c0List[parameterNo] # to let the regulator have time to evolve, a bit ad hoc!
- params[27] = c0
- elif fixedPar==1:
- learningSpeed = learningSpeedList[parameterNo]
- params[27] = c0
- # Initiate the iteration
- for iteration in range(nOfIterations):
- # Keep the trace of which parameters are evaluated
- print("Processing for...")
- print("Parameter learning speed: ", learningSpeed)
- print("Parameter c0: ", c0)
- print("Iteration: ", iteration)
- # Set the initial reward for each horizon in the first episode
- for i in range(0, nH+1):
- reward[i][0] = 0.5 # set the initial reward to 0.5 (neutral value)
- # Simulate each episode
- for episodeNo in range(0, nOfEpisodes):
- d = dSet[random.randint(0,len(dSet)-1)] # assign randomly the difficulty for the current episode
- difficultyList[episodeNo] = d # save the difficulty of the current episode
- # Verify that the stimuli and rewards are compatible (to avoid negative stimuli etc.)
- lowest = minStim + d/2 + (nH+1)*gain # generated stimuli cannot be lower than this value
- highest = maxStim - d/2 - (nH+1)*gain # generated stimuli cannot be higher than this value
- mean0Val = np.random.uniform(lowest, highest) # initial mean of the stimuli
- minAchievable = mean0Val - d/2 - (nH+1)*gain # to check if this value is lower than "lowest" (Cond. A)
- maxAchievable = mean0Val + d/2 + (nH+1)*gain # to chech if this value is higher than "higher" (Cond. B)
- while (maxAchievable > maxStim) or (minAchievable < minStim): # if Cond. A or Cond. B, regenerate your stimuli
- print('Mean value is out of bounds! Regenerating the mean value...')
- mean0Val = np.random.uniform(lowest, highest)
- minAchievable = mean0Val - d/2 - (nH+1)*gain
- maxAchievable = mean0Val + d/2 + (nH+1)*gain
- saveDecision = np.zeros(nH+1) # initialize the decision vector, it will save which stimuli are chosen throughout the trials
- pointer = 0
- mean0ValInitial = mean0Val # store mean value for nH=1 simulations
- for trial in range(0,nH+1): # initiate the first trial of the current episode
- shuffle = np.random.randint(0,2) # to randomly shuffle the positions of two stimuli
- ampA = mean0Val - d/2 # weak stimulus amplitude
- ampB = mean0Val + d/2 # strong stimulus amplitude
- # Initialize the stimuli, and by shuffling them if "shuffle" is on
- if shuffle==1:
- ampA_0 = ampA
- ampA = ampB
- ampB = ampA_0
- stimulusA = ampA*(smoothstep(t-t0)-smoothstep(t-t0-tF)) # weak stimulus (appears at t=2)
- stimulusB = ampB*(smoothstep(t-t0)-smoothstep(t-t0-tF)) # strong stimulus (appears at t=2)
- # Activity evolution (SDE system is integrated in time for each trial separately)
- psi0=reward[trial][episodeNo] # set initial condition for the regulatory mechanism
- lambdaA, lambdaB, psi = RegulatoryPsi(psi0, stimulusA, stimulusB, params) # generate the regulated stimuli for the trial
- state = TimeStepping(V0, lambdaA, lambdaB, TF1, TF2, params)
- veA, veB = state[:, 0], state[:, 7]
- # Find the decision time
- diff = np.abs(veA - veB) # compute the difference between the excitatory populations
- diff2 = diff[int(2/dt):-1] # ignore the first instants of the trial since the activity is degenerate the beginning
- decisionIndex = np.where(diff2>decisionThreshold) # identify the time instant at which the decision is made (bicolumnar competition is assumed to end at this instant)
- # Save the results and update the reward: learning the strategy through episodes
- if len(decisionIndex[0])>0:
- decisionTime = (decisionIndex[0][0]) * dt # pick the first instant where the difference exceeds dec. threshold
- decisionTimeList[episodeNo] = decisionTime
- if episodeNo<nOfEpisodes-1:
- if nH == 1:
- if trial==0:
- if psi[-1]<0.5:
- ampA = ampA + gain
- ampB = ampB + gain
- saveDecision[trial] = min(ampA, ampB)
- mean1Val = (ampA + ampB)/2 # mean after the gain
- reward[trial][episodeNo+1] = reward[trial][episodeNo] +\
- learningSpeed*(mean1Val - mean0Val)*\
- (2*psi[-1]-1)*(reward[trial][episodeNo]-1)**2\
- *(reward[trial][episodeNo])**2
- mean0Val = mean1Val # update mean value for the next trial
- else:
- ampA = ampA - gain
- ampB = ampB - gain
- saveDecision[trial] = max(ampA, ampB)
- mean1Val = (ampA + ampB)/2 # mean after the gain
- reward[trial][episodeNo+1] = reward[trial][episodeNo] +\
- learningSpeed*(mean1Val - mean0Val)*(2*psi[-1]-1)*(reward[trial][episodeNo]-1)**2\
- *(reward[trial][episodeNo])**2
- mean0Val = mean1Val # update mean value for the next trial
- elif trial==1:
- if psi[-1]>0.5:
- ampA = ampA + gain
- ampB = ampB + gain
- saveDecision[trial] = max(ampA, ampB)
- mean1Val = (ampA + ampB)/2 # mean after the gain
- reward[trial][episodeNo+1] = reward[trial][episodeNo] +\
- learningSpeed*(mean1Val - mean0Val)*(2*psi[-1]-1)*(reward[trial][episodeNo]-1)**2\
- *(reward[trial][episodeNo])**2
- mean0Val = mean1Val # update mean value for the next trial
- else:
- ampA = ampA - gain
- ampB = ampB - gain
- saveDecision[trial] = min(ampA, ampB)
- mean1Val = (ampA + ampB)/2 # mean after the gain
- reward[trial][episodeNo+1] = reward[trial][episodeNo] +\
- learningSpeed*(mean1Val - mean0Val)*(2*psi[-1]-1)*(reward[trial][episodeNo]-1)**2\
- *(reward[trial][episodeNo])**2
- mean0Val = mean1Val # update mean value for the next trial
- elif nH == 0:
- if psi[-1]<0.5:
- ampA = ampA + gain
- ampB = ampB + gain
- saveDecision[trial] = min(ampA, ampB)
- performance[episodeNo] = 1
- mean1Val = (ampA + ampB)/2 # mean after the gain
- reward[trial][episodeNo+1] = reward[trial][episodeNo] +\
- learningSpeed*(mean1Val - mean0Val)*(2*psi[-1]-1)*(reward[trial][episodeNo]-1)**2\
- *(reward[trial][episodeNo])**2
- else:
- ampA = ampA - gain
- ampB = ampB - gain
- saveDecision[trial] = max(ampA, ampB)
- performance[episodeNo] = 0
- mean1Val = (ampA + ampB)/2 # mean after the gain
- reward[trial][episodeNo+1] = reward[trial][episodeNo] +\
- learningSpeed*(mean1Val - mean0Val)*(2*psi[-1]-1)*(reward[trial][episodeNo]-1)**2\
- *(reward[trial][episodeNo])**2
- else:
- pointer = 1
- decisionTimeList[episodeNo] = -1
- # Save the final performance of the episode
- if pointer == 0:
- if nH == 1:
- mxBound = 2*mean0ValInitial + 3*gain
- mnBound = 2*mean0ValInitial - 3*gain
- performance[episodeNo] = (sum(saveDecision)-mnBound)/(mxBound - mnBound)
- if performance[episodeNo]>0.99:
- performance[episodeNo]=1
- elif performance[episodeNo]<0.01:
- performance[episodeNo]=0
- elif nH == 0:
- '''DO NOTHING!'''
- elif episodeNo!=nOfEpisodes-1:
- print("Episode %1.0i is not valid!" % episodeNo)
- performance[episodeNo] = -1
- learningSpeedSave2 = learningSpeed*1000 # define again to comply with indentation of Python
- c0Save = c0*1000
- performanceArray = np.array(performance)
- # Comment out these three lines to turn of automatic saving of the simulation results
- np.save("performanceResults_Iter_%d_k_%d_c0_%d.npy" % (iteration, learningSpeedSave2, c0Save), performanceArray)
- np.save("decisionTimeList_Iter_%d_k_%d_c0_%d.npy" % (iteration, learningSpeedSave2, c0Save), decisionTimeList)
- np.save("difficultyList_Iter_%d_k_%d_c0_%d.npy" % (iteration, learningSpeedSave2, c0Save), difficultyList)
- print('Simulation is over.')
- # %% [markdown]
- # If an episode is not valid (Episode X is not valid!), it means that there was no decision made in that episode. <b>This can happen especially when the values of $k$ or $c_0$ are chosen out of the suggested ranges in the default values above.</b> We ignore such episode in the performance results. Such cases model the experimental episodes in which the subject makes no decision within in the given time duration or makes decision before the authorized period of time and so on. This is why we usually simulate more episodes than we need, in this way we can eliminate the invalid episodes and use the ones ending up with a decision for our analysis.
- # %% [markdown]
- # ## Case study 1: changing noise decay rate $c_0$
- # Noise decay rate $c_0$ appears in time evolution Eq. (2) of the regulatory mechanism, and it determines how much the model has the flexibility to deviate from the strategy which it captures. This flexibility models both the perceptual difficulties that the human subject undergoes in the experimental task and the exploratory nature of the subject which sometimes makes the subject to deviate from the strategy which the subject alread captured on purpose in order to explore, see what happens in such cases of deviation.
- #
- # Here we will have a look at how global measures of simulation performance results change with varying $c_0$. We fix $k=0.2$. Performance results are scaled between 0 and 1, with 0 denoting the worst case performance and 1 denoting the best case performance. In the best case, the decisions are made in complete agreement with the implemented strategy. In the worst case, there is no decision made in agreement with the implemented strategy. In the all cases in between, there is a partial overlap with the made decisions and the implemented strategy.
- #
- # We define the following global measures to quantify the performance results:
- #
- # **Performance deviation:** A performance deviation is a point with a performance lower than 1 in the set of performance samples.
- #
- # **Deviation cluster:** A deviation cluster is a set of <u>at least</u> 3 successive performance deviations with respective to the episode numbers.
- #
- # **Performance cluster:** Performance cluster is the set of performance samples in which there is no deviation cluster and whose last performance sample is also the last sample of the whole experiment.
- #
- # **In-cluster deviation:** A performance deviation is called in-cluster if it occurs in a performance cluster.
- # Two measures are particularly important to quantify the behavioral performance in terms of strategy learning and flexibility of the model. Those are performance cluster, since it gives at which episode the model/subject captures the implemented strategy; and in-cluster deviation, since it provides information about how much flexible the model/subject is after capturing the strategy.
- #
- # We will consider only Horizon 0 simulations for the sake of simplicity, i.e., we have only one trial for each episode. Let us first load the performance data which was produced by using the same routine given in the previous section.
- # %%
- notebook_path = os.path.abspath("MainNotebook.ipynb") # find the actual path of the Python notebook
- # Define "deviations" finding the first and the last episode numbers of all performance deviation ranges
- def deviations(nums):
- nums = sorted(set(nums))
- gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s+1 < e]
- edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
- return list(zip(edges, edges))
- # Set the total number of episodes in each simulation
- nOfEpisodes = 100 # since in human experiments it is 100.
- episodeAxis = np.linspace(1,nOfEpisodes,nOfEpisodes, dtype=int) # for the x axes of performance result plots
- ## Import the experimental data
- # experimentData = scipy.io.loadmat('workspace_scriptGeneralH210528.mat')
- ## Load and arrange the experimental human performance data
- # subject7 = experimentData['Performance'][0][6]
- # subject7 = subject7[~np.isnan(subject7)]
- # subject7 = subject7[(1 >= subject7) & (subject7 >= 0)]
- # subject7 = subject7.flatten()
- # subject7 = subject7[0:nOfEpisodes]
- # Load the simulation performance data
- c0Set = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9] # simulated c0 values
- inClusterSet = [] # initialize an array to save number of in-cluster performance deviations for future plots
- for c0Val in c0Set:
- c0Iter = c0Val*1000 # to avoid rounding in the title of the data file (only for Python convention)
- path = os.path.join(os.path.dirname(notebook_path), \
- "showcaseData/showcase_1_H0/varying_c0/performanceResults_Iter_0_k_200_c0_%d.npy" %c0Iter) # set the data file path
- simulationData = np.load(path) # load the simulation result of the corresponding c0
- simulationData = simulationData[0:nOfEpisodes] # take only the first 100 episodes!
- # Plot the performance results episode-by-episode
- print('SIMULATION FOR c0: %.1f' %c0Val)
- fig = plt.figure(figsize=(6,4))
- plt.scatter(episodeAxis, simulationData)
- plt.xlabel("episode no")
- plt.ylabel("performance")
- plt.show()
- # Episode numbers of the performance deviations...
- # print('Episode numbers of EXPERIMENT cluster gaps:', np.where(subject7 < 1)) #...of the human experiments
- print('Episode numbers SIMULATION cluster gaps:', np.where(simulationData < 1)) #...of the simulations
- print('Number of performance deviations:', len(np.where(simulationData < 1)[0])) # total number of performance deviations (simulations)
- deviationsAll = np.where(simulationData < 1) # performance deviation episode numbers
- devRanges = deviationsAll[0] # jsut for Python convention
- devRanges = deviations(devRanges) # performance deviation ranges to find deviation blocks (thus initial episode number of performance blocks)
- # print(devRanges) # the first and last episode numbers of deviation blocks
- # Arrange the performance deviation ranges
- firstEpisode = []
- lastEpisode = []
- for element in devRanges:
- firstEpisode.append(element[0]) # keep the first
- lastEpisode.append(element[1]) # keep the second
- # Find the last deviation block, the last episode number of this block is the initial episode number of the performance block
- devBlockIndices = list(np.where(np.subtract(lastEpisode, firstEpisode)>=2))
- # Check if there is any deviation block. If not, the whole experiment is actually a performance block! Save also the in-cluster performance deviations.
- if len(devBlockIndices[0]>0):
- initialPerformanceBlock = devBlockIndices[-1][0]
- devRanges = list(devRanges)
- initialPerformanceBlock = devRanges[initialPerformanceBlock][1]
- numberOfInClusterDeviations = len(np.where(deviationsAll>initialPerformanceBlock)[0]) # find the number of in-cluster performance deviations
- inClusterSet.append(numberOfInClusterDeviations) # save the number of in-cluster performance deviations for future plots
- # Print now also the initial episode number of the performance blocks
- print('Initial episode number of performance block:', initialPerformanceBlock)
- print('Number of in-cluster performance deviations:', numberOfInClusterDeviations)
- else:
- noDeviationBlock = len(np.where(simulationData < 1)[0][:])
- inClusterSet.append(noDeviationBlock)
- print('No deviation block! Performance block starts from the beginning.')
- print('Number of in-cluster performance deviations:', noDeviationBlock)
- print('')
- print('')
- print('')
- # %% [markdown]
- # As expected, increasing $c_0$ results in a decreasing trend in the number of in-cluster deviations as can be seen in the following plot:
- # %%
- fig = plt.figure(figsize=(6,4))
- plt.plot(c0Set, inClusterSet)
- plt.xlabel("$c_0$")
- plt.ylabel("number of in-cluster devations")
- plt.show()
- # %% [markdown]
- # ## Case study 2: changing learning speed $k$
- # Learning speed parameter $k$ appears in episode-based discrete evolution Eq. (3) of the reward mechanism. It determines (approximately) the initial episode number of the performance cluster, which was defined in Case study 1. In other words, it determines how fast the model captures the implemented strategy. Here we will see the effect of varying $k$, where we fix $c_0=2$.
- #
- # An important remark is that the learning speed parameter is related to in-cluster deviations, although this relation is not explicit. In the cases where the initial episode number of performance cluster is too large, since the number of episodes for in-cluster deviations is not large, the effect of $c_0$ on the number of in-cluster episodes is outweighted by the effect of learning speed $k$. Therefore, extreme values for $k$ might produce misleading results in the simulations.
- #
- # Here we perform the data obtained via the same type of simulations as in Case study 1, with the only difference of that now we fix $c_0$ and vary $k$. We consider only Horizon 0 case as before for the sake of simplicity.
- # %%
- notebook_path = os.path.abspath("MainNotebook.ipynb") # find the actual path of the Python notebook
- # Define "deviations" finding the first and the last episode numbers of all performance deviation ranges
- def deviations(nums):
- nums = sorted(set(nums))
- gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s+1 < e]
- edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
- return list(zip(edges, edges))
- # Set the total number of episodes in each simulation
- nOfEpisodes = 100 # since in human experiments it is 100.
- episodeAxis = np.linspace(1,nOfEpisodes,nOfEpisodes, dtype=int) # for the x axes of performance result plots
- ## Import the experimental data
- # experimentData = scipy.io.loadmat('workspace_scriptGeneralH210528.mat')
- ## Load and arrange the experimental human performance data
- # subject7 = experimentData['Performance'][0][6]
- # subject7 = subject7[~np.isnan(subject7)]
- # subject7 = subject7[(1 >= subject7) & (subject7 >= 0)]
- # subject7 = subject7.flatten()
- # subject7 = subject7[0:nOfEpisodes]
- # Load the simulation performance data
- kSet = [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50] # simulated k values
- inClusterSet = [] # initialize an array to save number of in-cluster performance deviations for future plots
- for kVal in kSet:
- kIter = kVal*1000 # to avoid rounding in the title of the data file (only for Python convention)
- path = os.path.join(os.path.dirname(notebook_path), \
- "showcaseData/showcase_2_H0/varying_k/performanceResults_Iter_0_k_%d.npy" %kIter) # set the data file path
- simulationData = np.load(path) # load the simulation result of the corresponding k
- simulationData = simulationData[0:nOfEpisodes] # take only the first 100 episodes!
- # Plot the performance results episode-by-episode
- print('SIMULATION FOR k: %.2f' %kVal)
- fig = plt.figure(figsize=(6,4))
- plt.scatter(episodeAxis, simulationData)
- plt.xlabel("episode no")
- plt.ylabel("performance")
- plt.show()
- # Episode numbers of the performance deviations...
- # print('Episode numbers of EXPERIMENT cluster gaps:', np.where(subject7 < 1)) #...of the human experiments
- print('Episode numbers SIMULATION cluster gaps:', np.where(simulationData < 1)) #...of the simulations
- print('Number of performance deviations:', len(np.where(simulationData < 1)[0])) # total number of performance deviations (simulations)
- deviationsAll = np.where(simulationData < 1) # performance deviation episode numbers
- devRanges = deviationsAll[0] # jsut for Python convention
- devRanges = deviations(devRanges) # performance deviation ranges to find deviation blocks (thus initial episode number of performance blocks)
- # print(devRanges) # the first and last episode numbers of deviation blocks
- # Arrange the performance deviation ranges
- firstEpisode = []
- lastEpisode = []
- for element in devRanges:
- firstEpisode.append(element[0]) # keep the first
- lastEpisode.append(element[1]) # keep the second
- # Find the last deviation block, the last episode number of this block is the initial episode number of the performance block
- devBlockIndices = list(np.where(np.subtract(lastEpisode, firstEpisode)>=2))
- # Check if there is any deviation block. If not, the whole experiment is actually a performance block! Save also the in-cluster performance deviations.
- if len(devBlockIndices[0]>0):
- initialPerformanceBlock = devBlockIndices[-1][0]
- devRanges = list(devRanges)
- initialPerformanceBlock = devRanges[initialPerformanceBlock][1]
- numberOfInClusterDeviations = len(np.where(deviationsAll>initialPerformanceBlock)[0]) # find the number of in-cluster performance deviations
- inClusterSet.append(numberOfInClusterDeviations) # save the number of in-cluster performance deviations for future plots
- # Print now also the initial episode number of the performance blocks
- print('Initial episode number of performance block:', initialPerformanceBlock)
- print('Number of in-cluster performance deviations:', numberOfInClusterDeviations)
- else:
- noDeviationBlock = len(np.where(simulationData < 1)[0][:])
- inClusterSet.append(noDeviationBlock)
- print('No deviation block! Performance block starts from the beginning.')
- print('Number of in-cluster performance deviations:', noDeviationBlock)
- print('')
- print('')
- print('')
- # %% [markdown]
- # As expected, increasing $k$ results in a decreasing trend in the initial number of performance block, that is, the model learns more and more quickly as $k$ increases; see the following plot:
- # %%
- plt.figure(figsize=(6, 4))
- plt.plot(kSet, inClusterSet)
- plt.xlabel("$k$")
- plt.ylabel("number of in-cluster devations")
- plt.show()
- # %% [markdown]
- # ## Case study 3: effect of decision threshold on reaction time
- # Each cortical column is composed of a pair of excitatory and inhibitory neuronal populations, and in our model framework, each column makes the decision in favor of the stimulus which it is sensitive to. A column is represented as pool of pair of excitatory and inhibitory populations of mean-field AdEx equations in the model. Decision is made through bicolumnar competition between those two pool. It is made by the winning pool at the instant where the difference between the excitatory population firing rates exceeds a certain threshold, which we call decision threshold and which we fix to a convenient value. The decision threshold is directly related to the reaction time of the model, which is the time duration between the moment where the stimuli are shown on the screen, and the decision is made by the model.
- #
- # Here we will see this relation between the decision threshold and the reaction time by focusing on single trial. We simulate a single trial via:
- # %%
- # Define the stimuli for one trial
- from scipy.special import comb
- def smoothstep(x, x_min=0, x_max=1, N=1):
- x = np.clip((x - x_min) / (x_max - x_min), 0, 1)
- result = 0
- for n in range(0, N + 1):
- result += comb(N + n, n) * comb(2 * N + 1, N - n) * (-x) ** n
- result *= x ** (N + 1)
- return result
- t = np.linspace(0, tF, int(tF/dt)+1) # define the whole time interval
- ampA = 1 # amplitude of stimulus A
- ampB = 10 # amplitude of stimulus B
- t0 = 2 # initial time instant of the stimuli
- stimulusA = ampA*(smoothstep(t-t0)-smoothstep(t-t0-tF))
- stimulusB = ampB*(smoothstep(t-t0)-smoothstep(t-t0-tF))
- ## Build the simulation setup: load the transfer functions for both RS & FS populations
- TF1, TF2 = LoadTransferFunctions('RS-cell', 'FS-cell', 'CONFIG1')
- lambdaA, lambdaB, psi = RegulatoryPsi(psi0, stimulusA, stimulusB, params) # generate the regulated stimuli for the trial
- # Time integration for one single trial
- state = TimeStepping(V0, lambdaA, lambdaB, TF1, TF2, params)
- # Plot Pool A (red) and B (blue) excitatory population firing rates
- fig = plt.figure(figsize=(6,4))
- plt.plot(t, state[:, 0], 'r') # plotting v_{eA} (red)
- plt.plot(t, state[:, 7], 'b') # plotting v_{eB} (blue)
- plt.xlabel("time (s)")
- plt.ylabel("exc. population firing rates (red-eA, blue-eB)")
- # plt.margins(x=0.001, y=-0.001)
- plt.show()
- # %%
- veA, veB = state[:, 0], state[:, 7]
- decisionThreshold = 3 # the decision is made once the absolute value of the difference between excitatory population firing rates exceeds this value.
- # Find the reaction time
- diff = np.abs(veA - veB) # compute the difference between the excitatory populations
- diff2 = diff[int(2/dt):-1] # ignore the first instants of the trial since the activity is degenerate the beginning
- decisionIndex = np.where(diff2>decisionThreshold) # identify the time instant at which the decision is made (bicolumnar competition is assumed to end at this instant)
- decisionTime = (decisionIndex[0][0]) * dt # pick the first instant where the difference exceeds dec. threshold
- # decisionTimeList[episodeNo] = decisionTime
- print('Decision is made at: %.2f s' % decisionTime)
- # %% [markdown]
- # Let us see now how the trend of decision time looks like with respect to varying decision threshold.
- # %%
- inThreshold = 2 # initial decision threshold value
- finThreshold = 10 # final decision threshold value
- nOfSamples = 20 # number of samples of threshold values between the initial and final values
- decisionThresholdList = np.linspace(inThreshold, finThreshold, nOfSamples) # create the list of threshold samples
- decisionTimeList = [] # initialize reaction time list
- # Find the reaction time for each threshold value
- diff = np.abs(veA - veB) # compute the difference between the excitatory populations
- diff2 = diff[int(2/dt):-1] # ignore the first instants of the trial since the activity is degenerate the beginning
- for decisionThreshold in decisionThresholdList:
- decisionIndex = np.where(diff2>decisionThreshold) # identify the time instant at which the decision is made (bicolumnar competition is assumed to end at this instant)
- decisionTime = (decisionIndex[0][0]) * dt # pick the first instant where the difference exceeds dec. threshold
- decisionTimeList.append(decisionTime)
- # Plot the reaction time with respect to increasing decision threshold values
- fig = plt.figure(figsize=(6,4))
- plt.plot(decisionTimeList, decisionTimeList)
- plt.xlabel("decision threshold")
- plt.ylabel("reaction time (s)")
- # plt.margins(x=0.001, y=-0.001)
- plt.show()
- # %% [markdown]
- # As expected, reaction time increases as the decision threshold increases. Moreover, the relation has a rather linear character.
- # %% [markdown]
- # ## Bibliography
- # [1]: E. Baspinar, G. Cecchini, M. DePass, M. Andujar, P. Pani, S. Ferraina, R. Moreno-Bote, I. Cos, A. Destexhe, "A biologically plausible decision-making model based on interacting cortical columns", bioRxiv, 2023.
- #
- # [2]: G. Cecchini, M. DePass, E. Baspinar, M. Andujar, S. Ramawat, P. Pani, A. Destexhe, R. Moreno-Bote, I. Cos, "A theoretical formalization of consequence-based decision-making", bioRxiv, 2023.
- #
- # [3]: M. di Volo, A. Romagnoni, C. Capone, A. Destexhe, "Biologically realistic mean-field models of conductance-based networks of spiking neurons with adaptation", Neural Computation, vol. 31, no. 4, pp. 653-680, 2019.
- #
- # [4]: R. Brette, W. Gerstner, "Adaptive exponential integrate-and-fire model as an effective description of neuronal activity", Journal of Neurophysiology, vol. 94, no. 5, pp. 3637-3642, 2005.
- #
- # [5]: S. El Boustani, A. Destexhe, "A master equation formalism for macroscopic modeling of asynchronous irregular activity states", Neural Computation, vol. 21, no. 1, pp.46-100, 2009.
- #
- # [6]: Y. Zerlaut, S. Chemla, F. Chavane, A. Destexhe, "Modeling mesoscopic cortical dynamics using a mean-field model of conductance-based networks of adaptive exponential integrate-and-fire neurons", Journal of Computational Neuroscience, vol. 44, no. 1, pp. 45-61, 2018.
- #
- # [7]: E. Baspinar, G. Cecchini, R. Moreno-Bote, I. Cos, A. Destexhe, "Jupyter notebook of a biophysically plausible decision-making model based on interacting cortical columns (1.0.0)", Zenodo, 2023, https://doi.org/10.5281/zenodo.7682309
MainNotebook.ipynb at commit 55fd876, under CC-BY-4.0 · at the source
Overview
- Paris-Saclay University, CNRS, NeuroPSI, Saclay, France
- Facultat de Matemàtiques i Informàtica, Universitat de Barcelona, Barcelona, Catalonia, Spain
- Center for Brain and Cognition, DTIC, Universitat Pompeu Fabra, Barcelona, Catalonia, Spain
- Department of Physiology and Pharmacology, Sapienza University of Rome, Rome, Italy
- Serra-Hunter Fellow Programme, Barcelona, Catalonia, Spain
Abstract
We present a novel decision-making model with two populations. Each population is composed of Regularly Spiking (excitatory) and Fast Spiking (inhibitory) cells in cortical layer 2/
Reproduced under the paper's license (CC BY), from the paper cited above.
Repositories
Its files are read in the Code ↔ Paper reader above, with 6 matches between paragraphs and lines of code.
emrebasp/Jupyter-notebook-A-biophysically-plausible-decision-making-model-based-on-interacting-cortical-co
55fd876b32f60b0e976bce7a72f30cf57a9484a9, 29 June 2023Availability: 1 check, the latest on 30 September 2026: the link answers
- 30 September 2026: the link answers
8 files
- AdExMFForDecisionMakingP
ythonNb/ , Python, 266 linesDiffOperator.py - AdExMFForDecisionMakingP
ythonNb/ , Jupyter, 905 lines, 3 matchesMainNotebook.ipynb - AdExMFForDecisionMakingP
ythonNb/ , Python, 92 linesNeuronConnectivity.py - AdExMFForDecisionMakingP
ythonNb/ , Python, 140 linesSDEIntegrator.py - AdExMFForDecisionMakingP
ythonNb/ , Python, 88 linescell_library.py - AdExMFForDecisionMakingP
ythonNb/ , Python, 122 linessyn_and_connec_library.p y - AdExMFForDecisionMakingP
ythonNb/ , Python, 378 linestheoretical_tools.py - README.md, Text, 45 lines
Zenodo 7682309
Availability: 1 check, the latest on 30 September 2026: the link answers (HTTP 200)
- 30 September 2026: the link answers (HTTP 200)
12 files
- CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Jupyter, 851 linesythonNb/ .ipynb_checkpoints/ MainNotebook-checkpoint. ipynb - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Python, 266 linesythonNb/ DiffOperator.py - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Jupyter, 851 lines, 3 matchesythonNb/ MainNotebook.ipynb - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Python, 92 linesythonNb/ NeuronConnectivity.py - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Python, 140 linesythonNb/ SDEIntegrator.py - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Python, 88 linesythonNb/ cell_library.py - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Jupyter, 470 linesythonNb/ ipynb/ DiffOperator.ipynb - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Jupyter, 86 linesythonNb/ ipynb/ NeuronConnectivity.ipynb - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Jupyter, 133 linesythonNb/ ipynb/ SDEIntegrator.ipynb - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Jupyter, 67 linesythonNb/ ipynb/ nb_finder.ipynb - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Python, 122 linesythonNb/ syn_and_connec_library.p y - CodePackage_A_ biologically_ plausible_ decision_making model/
AdExMFForDecisionMakingP , Python, 378 linesythonNb/ theoretical_tools.py
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 19 scripts, each with its path and the digest of its content;
- 6 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
Datasets cited
- search.kg.ebrains.eu/
instances/ , at search.kg.ebrains.eu; found in “Data Availability”0d145ebe-3ecd-4b3c-9400- 913a8cd21a6a - search.kg.ebrains.eu/
instances/ , at search.kg.ebrains.eu; found in “Data Availability”755b98d5-3212-4220-950e- 54d58e809e6e - search.kg.ebrains.eu/
instances/ , at search.kg.ebrains.eu; found in the referencesa0cb1207-b6a8-44e9-9cd0- 7f98acce2080
Data Availability
The data corresponding to the human experiments is found in https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 30 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 9 authors, 8 MeSH terms, 1 funder, 41 references.
Cite
This paper
Baspinar, E., Cecchini, G., DePass, M., Andujar, M., Pani, P., Ferraina, S., Moreno-Bote, R., Cos, I., & Destexhe, A. (2026). A biologically plausible decision-making model based on interacting neural populations. PloS one, 21(3), e0340393. https://
BibTeX
@article{baspinar2026bio
author = {Baspinar, Emre and Cecchini, Gloria and DePass, Michael and Andujar, Marta and Pani, Pierpaolo and Ferraina, Stefano and Moreno-Bote, Rubén and Cos, Ignasi and Destexhe, Alain},
title = {{A biologically plausible decision-making model based on interacting neural populations}},
journal = {PloS one},
year = {2026},
month = mar,
volume = {21},
number = {3},
pages = {e0340393},
publisher = {PLOS},
issn = {1932-6203},
doi = {10.1371/
url = {https://
pmid = {41774750},
pmcid = {PMC12956099}
}
RIS
TY - JOUR
AU - Baspinar, Emre
AU - Cecchini, Gloria
AU - DePass, Michael
AU - Andujar, Marta
AU - Pani, Pierpaolo
AU - Ferraina, Stefano
AU - Moreno-Bote, Rubén
AU - Cos, Ignasi
AU - Destexhe, Alain
TI - A biologically plausible decision-making model based on interacting neural populations
T2 - PloS one
J2 - PLoS One
PY - 2026
DA - 2026/
VL - 21
IS - 3
SP - e0340393
SN - 1932-6203
PB - PLOS
DO - 10.1371/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1371/
"type": "article-journal",
"title": "A biologically plausible decision-making model based on interacting neural populations",
"container-title": "PloS one",
"author": [
{
"family": "Baspinar",
"given": "Emre"
},
{
"family": "Cecchini",
"given": "Gloria"
},
{
"family": "DePass",
"given": "Michael"
},
{
"family": "Andujar",
"given": "Marta"
},
{
"family": "Pani",
"given": "Pierpaolo"
},
{
"family": "Ferraina",
"given": "Stefano"
},
{
"family": "Moreno-Bote",
"given": "Rubén"
},
{
"family": "Cos",
"given": "Ignasi"
},
{
"family": "Destexhe",
"given": "Alain"
}
],
"container-title-short":
"volume": "21",
"issue": "3",
"page": "e0340393",
"DOI": "10.1371/
"PMID": "41774750",
"PMCID": "PMC12956099",
"ISSN": "1932-6203",
"publisher": "PLOS",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
3
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1073/pnas.2532072123 [code]
- Spatially structured heterogeneity shapes large-scale cortical dynamics in a model of the human cortex.Journal: Proceedings of the National Academy of Sciences of the United States of AmericaIn common: SciPy, Matplotlib, NumPy, 3 references, author Alain Destexhe
- [2] doi:10.1038/s41467-026-75704-3 [code]
- A minimal model of working memory in neural systems and neuromorphic circuits.Journal: Nature communicationsIn common: Numba, SciPy, Matplotlib, 1 other tool, 3 references
- [3] doi:10.1371/journal.pcbi.1014378 [code]
- A mean-field model of neural networks with PV and SOM interneurons reveals connectivity-based mechanisms of gamma oscillations.Journal: PLoS computational biologyIn common: SciPy, Matplotlib, NumPy, 3 references
- [4] doi:10.1038/s41467-026-74818-y [code]
- Stable readout of visual representations mediates flexible generalization.Journal: Nature communicationsIn common: SciPy, Matplotlib, NumPy, non-human primate, cognitive, 2 references
- [5] doi:10.1371/journal.pcbi.1013463 [code]
- A multi-frequency whole-brain neural mass model with homeostatic feedback inhibition.Journal: PLoS computational biologyIn common: Numba, SciPy, Matplotlib, 1 other tool, 1 reference
- [6] doi:10.1162/imag.a.1147 [code]
- The Virtual Brain links transcranial magnetic stimulation evoked potentials and inhibitory neurotransmitter changes in major depressive disorder.Journal: Imaging neuroscience (Cambridge, Mass.)In common: Numba, SciPy, Matplotlib, 1 other tool, 1 reference
- [7] doi:10.1073/pnas.2616911123 [code]
- Cerebellar microcircuits enable robust evidence-based decisions through cortico-cerebellar coupling.Journal: Proceedings of the National Academy of Sciences of the United States of AmericaIn common: Matplotlib, NumPy, cognitive, 2 references
- [8] doi:10.1038/s41467-026-72146-9 [code]
- Modeling attention and binding in the brain through bidirectional recurrent gating.Journal: Nature communicationsIn common: SciPy, Matplotlib, NumPy, 2 references
- [9] doi:10.1371/journal.pcbi.1013487 [code]
- Flexible navigation with neuromodulated cognitive maps.Journal: PLoS computational biologyIn common: SciPy, Matplotlib, NumPy, 2 references
- [10] doi:10.1038/s42003-026-10427-1 [code]
- Phase-tuned modulation during reward expectancy in human anterior insular cortex.Journal: Communications biologyIn common: Numba, SciPy, NumPy, 1 reference
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 2 repositories of the authors' code, each at its verified commit and with its license, 19 scripts, and 6 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:a7895f6d0812b07d…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
