#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the
# Scikit-NeuroMSI Project (https://github.com/renatoparedes/scikit-neuromsi).
# Copyright (c) 2021-2025, Renato Paredes; Cabral, Juan
# License: BSD 3-Clause
# Full Text:
# https://github.com/renatoparedes/scikit-neuromsi/blob/main/LICENSE.txt
import copy
from dataclasses import dataclass
from brainpy import odeint
import numpy as np
from ..core import SKNMSIMethodABC
from ..utils.neural_tools import (
calculate_lateral_synapses,
calculate_stimuli_input,
compute_latency,
create_unimodal_stimuli_matrix,
)
[docs]
@dataclass
class Cuppini2014Integrator:
"""
Integrator function for the Cuppini2014 model.
This class represents the integrator function used to compute
the dynamics of the neural network according to the Cuppini (2014) model.
It handles the update rules for the neurons' activities based on
their net inputs.
Parameters
----------
tau : tuple of 3 float
Time constants for the auditory, visual,
and multisensory neurons, respectively.
s : float
Slope of the sigmoid activation function.
theta : float
Central position of the sigmoid activation function.
"""
#: Time constants for the auditory, visual, and multisensory neurons
tau: tuple
#: Slope of the sigmoid activation function
s: float
#: Central position of the sigmoid activation function
theta: float
#: Name of the integrator
name: str = "Cuppini2014Integrator"
@property
def __name__(self):
"""Return the name of the Integrator."""
return self.name
[docs]
def sigmoid(self, u):
"""Computes the sigmoid activation function."""
return 1 / (1 + np.exp(-self.s * (u - self.theta)))
def __call__(self, y_a, y_v, t, u_a, u_v):
"""Computes the activites of neurons."""
# Auditory
dy_a = (-y_a + self.sigmoid(u_a)) * (1 / self.tau[0])
# Visual
dy_v = (-y_v + self.sigmoid(u_v)) * (1 / self.tau[0])
return dy_a, dy_v
[docs]
@dataclass
class Cuppini2014TemporalFilter:
"""Temporal filter for the Cuppini2014 model."""
tau: tuple
name: str = "Cuppini2014TemporalFilter"
@property
def __name__(self):
"""Return the name of the Temporal Filter."""
return self.name
def __call__(
self,
a_outside_input,
v_outside_input,
auditoryfilter_input,
visualfilter_input,
t,
a_external_input,
v_external_input,
a_cross_modal_input,
v_cross_modal_input,
a_gain,
v_gain,
):
"""
Computes the temporal filtering for the neural inputs.
Parameters
----------
a_outside_input : np.ndarray
The outside input to the auditory layer after filtering.
v_outside_input : np.ndarray
The outside input to the visual layer after filtering.
auditoryfilter_input : np.ndarray
The current auditory filter input.
visualfilter_input : np.ndarray
The current visual filter input.
t : float
The current time in the simulation.
a_external_input : np.ndarray
The external input to the auditory layer neurons.
v_external_input : np.ndarray
The external input to the visual layer neurons.
a_cross_modal_input : np.ndarray
The cross-modal input to the auditory layer neurons.
v_cross_modal_input : np.ndarray
The cross-modal input to the visual layer neurons.
a_feedback_input : np.ndarray
The feedback input to the auditory layer neurons.
v_feedback_input : np.ndarray
The feedback input to the visual layer neurons.
a_gain : float
The gain factor for the auditory layer.
v_gain : float
The gain factor for the visual layer.
Returns
-------
tuple
A tuple containing the updated outside inputs and filtered inputs
for the auditory and visual layers.
"""
# Auditory
da_outside_input = auditoryfilter_input
dauditory_filter_input = (
(a_gain / self.tau[1]) * (a_external_input + a_cross_modal_input)
- ((2 * auditoryfilter_input) / self.tau[1])
- a_outside_input / np.square(self.tau[1])
)
# Visual
dv_outside_input = visualfilter_input
dvisual_filter_input = (
(v_gain / self.tau[2]) * (v_external_input + v_cross_modal_input)
- ((2 * visualfilter_input) / self.tau[2])
- v_outside_input / np.square(self.tau[2])
)
return (
da_outside_input,
dv_outside_input,
dauditory_filter_input,
dvisual_filter_input,
)
[docs]
class Cuppini2014(SKNMSIMethodABC):
"""Network model for multisensory integration of Cuppini et al. (2014).
This model simulates neural responses in a multisensory system,
consisting of auditory and visual areas. By default, each of these areas
consists of 180 neurons arranged topologically to encode a 180° space.
In this way, each neuron encodes 1° of space and neurons close to each
other encode close spatial positions. The model includes a temporal filter
to accurately reproduce temporal dynamics of multisensory integration.
References
----------
:cite:p:`cuppini2014neurocomputational`
"""
_model_name = "Cuppini2014"
_model_type = "Neural"
_run_input = [
{"target": "auditory_position", "template": "${mode0}_position"},
{"target": "visual_position", "template": "${mode1}_position"},
{"target": "auditory_intensity", "template": "${mode0}_intensity"},
{"target": "visual_intensity", "template": "${mode1}_intensity"},
{"target": "auditory_duration", "template": "${mode0}_duration"},
{"target": "visual_duration", "template": "${mode1}_duration"},
{"target": "auditory_gain", "template": "${mode0}_gain"},
{"target": "visual_gain", "template": "${mode1}_gain"},
]
_run_output = [
{"target": "auditory", "template": "${mode0}"},
{"target": "visual", "template": "${mode1}"},
]
_output_mode = "multi"
def __init__(
self,
*,
neurons=180,
tau=(1, 15, 25), # neuron, auditory and visual
s=2,
theta=16,
seed=None,
mode0="auditory",
mode1="visual",
position_range=(0, 180),
position_res=1,
time_range=(0, 100),
time_res=0.01,
**integrator_kws,
):
"""
Initializes the Cuppini2014 model.
Parameters
----------
neurons : int, optional
Number of neurons per layer. Default is 180.
tau : tuple of 3 float, optional
Time constants for the auditory, visual, and multisensory neurons,
respectively. Default is (3, 15, 1).
s : float, optional
Slope of the sigmoid activation function. Default is 0.3.
theta : float, optional
Central position of the sigmoid activation function. Default is 20.
seed : int or None, optional
Seed for the random number generator.
If None, the random number generator will not be seeded.
Default is None.
mode0 : str, optional
The name for the first sensory modality. Default is "auditory".
mode1 : str, optional
The name for the second sensory modality. Default is "visual".
position_range : tuple of 2 int, optional
Range of positions in degrees as (min, max). Default is (0, 180).
position_res : float, optional
Resolution of positions in degrees. Default is 1.
time_range : tuple of 2 float, optional
Time range for the simulation as (start, end) in miliseconds.
Default is (0, 100).
time_res : float, optional
Time resolution for the simulation in miliseconds. Default is 0.01.
**integrator_kws
Additional keyword arguments passed to the integrator.
These can include parameters such as the integration method and
time step size.
Raises
------
ValueError
If the length of `tau` is not equal to 3.
Attributes
----------
neurons : int
Number of neurons per layer.
tau : tuple of 3 float
Time constants for the auditory, visual, and multisensory neurons.
s : float
Slope of the sigmoid activation function.
theta : float
Central position of the sigmoid activation function.
random : np.random.Generator
Random number generator.
time_range : tuple of 2 float
Time range for the simulation.
time_res : float
Time resolution for the simulation.
position_range : tuple of 2 int
Range of positions in degrees.
position_res : float
Resolution of positions in degrees.
mode0 : str
Name of the auditory modality.
mode1 : str
Name of the visual modality.
dtype : np.dtype
Data type used for computations.
_integrator_function : Cuppini2014IntegratorFunction
The integrator function used for simulation.
_integrator_kws : dict
Keyword arguments for the integrator.
_integrator : callable
The integrator function.
"""
if len(tau) != 3:
raise ValueError()
self._neurons = neurons
self._position_range = position_range
self._position_res = float(position_res)
self._time_range = time_range
self._time_res = float(time_res)
integrator_kws.setdefault("method", "euler")
integrator_kws.setdefault("dt", self._time_res)
integrator_model = Cuppini2014Integrator(tau=tau, s=s, theta=theta)
self._integrator = odeint(f=integrator_model, **integrator_kws)
temporal_filter_model = Cuppini2014TemporalFilter(tau=tau)
self._temporal_filter = odeint(
f=temporal_filter_model, **integrator_kws
)
self._mode0 = mode0
self._mode1 = mode1
self.set_random(np.random.default_rng(seed=seed))
# PROPERTY ================================================================
@property
def neurons(self):
"""
Number of neurons in each layer.
Returns
-------
int
The number of neurons used in the simulation.
"""
return self._neurons
@property
def tau(self):
"""
Time constants for the neurons.
Returns
-------
tuple of 3 float
Time constants for the auditory, visual, and
multisensory neurons, respectively.
"""
return self._integrator.f.tau
@property
def s(self):
"""
Slope of the sigmoid activation function.
Returns
-------
float
The slope parameter of the sigmoid function used in the model.
"""
return self._integrator.f.s
@property
def theta(self):
"""
Central position of the sigmoid activation function.
Returns
-------
float
The central position parameter of the sigmoid function
used in the model.
"""
return self._integrator.f.theta
@property
def random(self):
"""
Random number generator.
Returns
-------
numpy.random.Generator
The random number generator used for initialization.
"""
return self._random
@property
def time_range(self):
"""
Time range for simulation.
Returns
-------
tuple of 2 float
The start and end times for the simulation in seconds.
"""
return self._time_range
@property
def time_res(self):
"""
Time resolution of the simulation.
Returns
-------
float
The time step size for the simulation in seconds.
"""
return self._time_res
@property
def position_range(self):
"""
Range of positions in degrees.
Returns
-------
tuple of 2 int
The minimum and maximum positions in degrees.
"""
return self._position_range
@property
def position_res(self):
"""
Resolution of position encoding.
Returns
-------
float
The resolution of position encoding in degrees.
"""
return self._position_res
@property
def mode0(self):
"""
Returns the name of the first sensory modality.
Returns
-------
str
The name of the first sensory modality.
"""
return self._mode0
@property
def mode1(self):
"""
Returns the name of the second sensory modality.
Returns
-------
str
The name of the second sensory modality.
"""
return self._mode1
# Model run
[docs]
def set_random(self, rng):
"""
Set the random number generator for the model.
This method allows for setting a custom random number generator,
which can be useful for ensuring reproducibility or
for using different random number generation strategies.
Parameters
----------
rng : numpy.random.Generator
The random number generator to be used.
It should be an instance of `numpy.random.Generator`.
"""
self._random = rng
[docs]
def run(
self,
*,
soa=56,
onset=25,
auditory_duration=10,
visual_duration=20,
auditory_position=None,
visual_position=None,
auditory_intensity=3,
visual_intensity=1,
noise=False,
lateral_excitation=2,
lateral_inhibition=1.8,
cross_modal_latency=16,
auditory_gain=None,
visual_gain=None,
auditory_stim_n=2,
visual_stim_n=1,
):
"""
Run the simulation of the Cuppini2014 model.
Parameters
----------
soa : float, optional
Stimulus-onset asynchrony for stimuli (default is 50).
onset : float, optional
Onset time for stimuli (default is 16).
auditory_duration : float, optional
Duration of auditory stimuli (default is 7).
visual_duration : float, optional
Duration of visual stimuli (default is 12).
auditory_position : float, optional
Position of auditory stimuli (default is middle of the range).
visual_position : float, optional
Position of visual stimuli (default is middle of the range).
auditory_intensity : float, optional
Intensity of auditory stimuli (default is 2.4).
visual_intensity : float, optional
Intensity of visual stimuli (default is 1.4).
noise : bool, optional
Whether to include noise in the simulation (default is False).
lateral_excitation : float, optional
Lateral excitation parameter (default is 2).
lateral_inhibition : float, optional
Lateral inhibition parameter (default is 1.8).
cross_modal_latency : float, optional
Latency for cross-modal inputs (default is 16).
auditory_gain : float, optional
Gain for auditory processing
(default is None, which sets to exp(1)).
visual_gain : float, optional
Gain for visual processing
(default is None, which sets to exp(1)).
auditory_stim_n : int, optional
Number of auditory stimuli (default is 2).
visual_stim_n : int, optional
Number of visual stimuli (default is 1).
Returns
-------
tuple
A tuple containing:
- response (dict): A dictionary with keys "auditory", "visual",
and "multi", containing the simulation results for each layer.
- extra (dict): A dictionary with additional information such as
inputs and filter inputs.
"""
auditory_position = (
int(self._position_range[1] / 2)
if auditory_position is None
else auditory_position
)
visual_position = (
int(self._position_range[1] / 2)
if visual_position is None
else visual_position
)
auditory_gain = np.exp(1) if auditory_gain is None else auditory_gain
visual_gain = np.exp(1) if visual_gain is None else visual_gain
hist_times = np.arange(
self._time_range[0], self._time_range[1], self._integrator.dt
)
sim_cross_modal_latency = int(
cross_modal_latency / self._integrator.dt
)
# Build synapses
auditory_latsynapses = calculate_lateral_synapses(
neurons=self.neurons,
excitation_loc=lateral_excitation,
inhibition_loc=lateral_inhibition,
excitation_scale=3,
inhibition_scale=24,
)
visual_latsynapses = calculate_lateral_synapses(
neurons=self.neurons,
excitation_loc=lateral_excitation,
inhibition_loc=lateral_inhibition,
excitation_scale=3,
inhibition_scale=24,
)
cross_modal_synapses_weight = 0.35
# Generate Stimuli
point_auditory_stimuli = calculate_stimuli_input(
neurons=self.neurons,
intensity=auditory_intensity,
scale=32,
loc=auditory_position,
)
point_visual_stimuli = calculate_stimuli_input(
neurons=self.neurons,
intensity=visual_intensity,
scale=4,
loc=visual_position,
)
auditory_stimuli = create_unimodal_stimuli_matrix(
neurons=self.neurons,
stimuli=point_auditory_stimuli,
stimuli_duration=auditory_duration,
onset=onset,
simulation_length=self._time_range[1],
time_res=self.time_res,
dt=self._integrator.dt,
stimuli_n=auditory_stim_n,
soa=soa,
)
visual_stimuli = create_unimodal_stimuli_matrix(
neurons=self.neurons,
stimuli=point_visual_stimuli,
stimuli_duration=visual_duration,
onset=onset,
simulation_length=self._time_range[1],
time_res=self.time_res,
dt=self._integrator.dt,
stimuli_n=visual_stim_n,
)
# Data holders
z_1d = np.zeros(self.neurons)
auditory_y, visual_y = copy.deepcopy(z_1d), copy.deepcopy(z_1d)
auditory_outside_input, visual_outside_input = copy.deepcopy(
z_1d
), copy.deepcopy(z_1d)
auditoryfilter_input, visualfilter_input = copy.deepcopy(
z_1d
), copy.deepcopy(z_1d)
# template for the next holders
z_2d = np.zeros(
(int(self._time_range[1] / self._integrator.dt), self.neurons)
)
auditory_res, visual_res, multi_res = (
copy.deepcopy(z_2d),
copy.deepcopy(z_2d),
copy.deepcopy(z_2d),
)
auditory_outside_inputs, visual_outside_inputs = copy.deepcopy(
z_2d
), copy.deepcopy(z_2d)
auditoryfilter_inputs, visualfilter_inputs = copy.deepcopy(
z_2d
), copy.deepcopy(z_2d)
auditory_lateral_inputs, visual_lateral_inputs = copy.deepcopy(
z_2d
), copy.deepcopy(z_2d)
auditory_total_inputs, visual_total_inputs = copy.deepcopy(
z_2d
), copy.deepcopy(z_2d)
del z_1d, z_2d
for i in range(hist_times.size):
time = int(hist_times[i] / self._integrator.dt)
# Compute cross-modal input
computed_cross_latency = compute_latency(
time, sim_cross_modal_latency
)
auditory_cm_input = (
cross_modal_synapses_weight
* visual_res[computed_cross_latency, :]
)
visual_cm_input = (
cross_modal_synapses_weight
* auditory_res[computed_cross_latency, :]
)
(
auditory_outside_input,
visual_outside_input,
auditoryfilter_input,
visualfilter_input,
) = self._temporal_filter(
a_outside_input=auditory_outside_input,
v_outside_input=visual_outside_input,
auditoryfilter_input=auditoryfilter_input,
visualfilter_input=visualfilter_input,
t=time,
a_external_input=auditory_stimuli[i],
v_external_input=visual_stimuli[i],
a_cross_modal_input=auditory_cm_input,
v_cross_modal_input=visual_cm_input,
a_gain=auditory_gain,
v_gain=visual_gain,
)
auditory_outside_inputs[i, :], visual_outside_inputs[i, :] = (
auditory_outside_input,
visual_outside_input,
)
auditoryfilter_inputs[i, :], visualfilter_inputs[i, :] = (
auditoryfilter_input,
visualfilter_input,
)
# Compute lateral input
la = np.sum(auditory_latsynapses.T * auditory_y, axis=1)
lv = np.sum(visual_latsynapses.T * visual_y, axis=1)
auditory_lateral_inputs[i, :], visual_lateral_inputs[i, :] = la, lv
# Compute unisensory total input
auditory_u = la + auditory_outside_input
visual_u = lv + visual_outside_input
auditory_total_inputs[i, :], visual_total_inputs[i, :] = (
auditory_u,
visual_u,
)
# Compute neurons activity
auditory_y, visual_y = self._integrator(
y_a=auditory_y,
y_v=visual_y,
t=time,
u_a=auditory_u,
u_v=visual_u,
)
auditory_res[i, :], visual_res[i, :] = (
auditory_y,
visual_y,
)
response = {
"auditory": auditory_res,
"visual": visual_res,
"multi": multi_res,
}
return response, {
"auditory_stimuli": auditory_stimuli,
"visual_stimuli": visual_stimuli,
"auditory_outside_input": auditory_outside_inputs,
"visual_outside_input": visual_outside_inputs,
"auditory_lateral_input": auditory_lateral_inputs,
"visual_lateral_input": visual_lateral_inputs,
"auditory_total_input": auditory_total_inputs,
"visual_total_input": visual_total_inputs,
"auditory_lateral_synapses": auditory_latsynapses,
"visual_lateral_synapses": visual_latsynapses,
"auditory_filter_input": auditoryfilter_inputs,
"visual_filter_input": visualfilter_inputs,
}