@@ -742,10 +742,10 @@ def synthesize_poisson_spike_vector(
742742
743743 # Calculate the number of frames in the refractory period
744744 refractory_period_seconds = refractory_period_ms / 1000.0
745- refactory_period_frames = int (refractory_period_seconds * sampling_frequency )
745+ refractory_period_frames = int (refractory_period_seconds * sampling_frequency )
746746
747- is_refactory_period_too_long = np .any (refractory_period_seconds >= 1.0 / firing_rates )
748- if is_refactory_period_too_long :
747+ is_refractory_period_too_long = np .any (refractory_period_seconds >= 1.0 / firing_rates )
748+ if is_refractory_period_too_long :
749749 raise ValueError (
750750 f"The given refractory period { refractory_period_ms } is too long for the firing rates { firing_rates } "
751751 )
@@ -764,9 +764,9 @@ def synthesize_poisson_spike_vector(
764764 binomial_p_modified = modified_firing_rate / sampling_frequency
765765 binomial_p_modified = np .minimum (binomial_p_modified , 1.0 )
766766
767- # Generate inter spike frames, add the refactory samples and accumulate for sorted spike frames
767+ # Generate inter spike frames, add the refractory samples and accumulate for sorted spike frames
768768 inter_spike_frames = rng .geometric (p = binomial_p_modified [:, np .newaxis ], size = (num_units , num_spikes_max ))
769- inter_spike_frames [:, 1 :] += refactory_period_frames
769+ inter_spike_frames [:, 1 :] += refractory_period_frames
770770 spike_frames = np .cumsum (inter_spike_frames , axis = 1 , out = inter_spike_frames )
771771 spike_frames = spike_frames .ravel ()
772772
@@ -1054,6 +1054,176 @@ def synthetize_spike_train_bad_isi(duration, baseline_rate, num_violations, viol
10541054 return spike_train
10551055
10561056
1057+ from spikeinterface .core .basesorting import BaseSortingSegment , BaseSorting
1058+
1059+
1060+ class SortingGenerator (BaseSorting ):
1061+ def __init__ (
1062+ self ,
1063+ num_units : int = 20 ,
1064+ sampling_frequency : float = 30_000.0 , # in Hz
1065+ durations : List [float ] = [10.325 , 3.5 ], # in s for 2 segments
1066+ firing_rates : float | np .ndarray = 3.0 ,
1067+ refractory_period_ms : float | np .ndarray = 4.0 , # in ms
1068+ seed : int = 0 ,
1069+ ):
1070+ """
1071+ A class for lazily generate synthetic sorting objects with Poisson spike trains.
1072+
1073+ We have two ways of representing spike trains in SpikeInterface:
1074+
1075+ - Spike vector (sample_index, unit_index)
1076+ - Dictionary of unit_id to spike times
1077+
1078+ This class simulates a sorting object that uses a representation based on unit IDs to lists of spike times,
1079+ rather than pre-computed spike vectors. It is intended for testing performance differences and functionalities
1080+ in data handling and analysis frameworks. For the normal use case of sorting objects with spike_vectors use the
1081+ `generate_sorting` function.
1082+
1083+ Parameters
1084+ ----------
1085+ num_units : int, optional
1086+ The number of distinct units (neurons) to simulate. Default is 20.
1087+ sampling_frequency : float, optional
1088+ The sampling frequency of the spike data in Hz. Default is 30_000.0.
1089+ durations : list of float, optional
1090+ A list containing the duration in seconds for each segment of the sorting data. Default is [10.325, 3.5],
1091+ corresponding to 2 segments.
1092+ firing_rates : float or np.ndarray, optional
1093+ The firing rate(s) in Hz, which can be specified as a single value applicable to all units or as an array
1094+ with individual firing rates for each unit. Default is 3.0.
1095+ refractory_period_ms : float or np.ndarray, optional
1096+ The refractory period in milliseconds. Can be specified either as a single value for all units or as an
1097+ array with different values for each unit. Default is 4.0.
1098+ seed : int, default: 0
1099+ The seed for the random number generator to ensure reproducibility.
1100+
1101+ Raises
1102+ ------
1103+ ValueError
1104+ If the refractory period is too long for the given firing rates, which could result in unrealistic
1105+ physiological conditions.
1106+
1107+ Notes
1108+ -----
1109+ This generator simulates the spike trains using a Poisson process. It takes into account the refractory periods
1110+ by adjusting the firing rates accordingly. See the notes on `synthesize_poisson_spike_vector` for more details.
1111+
1112+ """
1113+
1114+ unit_ids = np .arange (num_units )
1115+ super ().__init__ (sampling_frequency , unit_ids )
1116+
1117+ self .num_units = num_units
1118+ self .num_segments = len (durations )
1119+ self .firing_rates = firing_rates
1120+ self .durations = durations
1121+ self .refractory_period_seconds = refractory_period_ms / 1000.0
1122+
1123+ is_refractory_period_too_long = np .any (self .refractory_period_seconds >= 1.0 / firing_rates )
1124+ if is_refractory_period_too_long :
1125+ raise ValueError (
1126+ f"The given refractory period { refractory_period_ms } is too long for the firing rates { firing_rates } "
1127+ )
1128+
1129+ seed = _ensure_seed (seed )
1130+ self .seed = seed
1131+
1132+ for segment_index in range (self .num_segments ):
1133+ segment_seed = self .seed + segment_index
1134+ segment = SortingGeneratorSegment (
1135+ num_units = num_units ,
1136+ sampling_frequency = sampling_frequency ,
1137+ duration = durations [segment_index ],
1138+ firing_rates = firing_rates ,
1139+ refractory_period_seconds = self .refractory_period_seconds ,
1140+ seed = segment_seed ,
1141+ t_start = None ,
1142+ )
1143+ self .add_sorting_segment (segment )
1144+
1145+ self ._kwargs = {
1146+ "num_units" : num_units ,
1147+ "sampling_frequency" : sampling_frequency ,
1148+ "durations" : durations ,
1149+ "firing_rates" : firing_rates ,
1150+ "refractory_period_ms" : refractory_period_ms ,
1151+ "seed" : seed ,
1152+ }
1153+
1154+
1155+ class SortingGeneratorSegment (BaseSortingSegment ):
1156+ def __init__ (
1157+ self ,
1158+ num_units : int ,
1159+ sampling_frequency : float ,
1160+ duration : float ,
1161+ firing_rates : float | np .ndarray ,
1162+ refractory_period_seconds : float | np .ndarray ,
1163+ seed : int ,
1164+ t_start : Optional [float ] = None ,
1165+ ):
1166+ self .num_units = num_units
1167+ self .duration = duration
1168+ self .sampling_frequency = sampling_frequency
1169+ self .refractory_period_seconds = refractory_period_seconds
1170+
1171+ if np .isscalar (firing_rates ):
1172+ firing_rates = np .full (num_units , firing_rates , dtype = "float64" )
1173+
1174+ self .firing_rates = firing_rates
1175+
1176+ if np .isscalar (self .refractory_period_seconds ):
1177+ self .refractory_period_seconds = np .full (num_units , self .refractory_period_seconds , dtype = "float64" )
1178+
1179+ self .segment_seed = seed
1180+ self .units_seed = {unit_id : self .segment_seed + hash (unit_id ) for unit_id in range (num_units )}
1181+ self .num_samples = math .ceil (sampling_frequency * duration )
1182+ super ().__init__ (t_start )
1183+
1184+ def get_unit_spike_train (self , unit_id , start_frame : int | None = None , end_frame : int | None = None ) -> np .ndarray :
1185+ unit_seed = self .units_seed [unit_id ]
1186+ unit_index = self .parent_extractor .id_to_index (unit_id )
1187+
1188+ rng = np .random .default_rng (seed = unit_seed )
1189+
1190+ firing_rate = self .firing_rates [unit_index ]
1191+ refractory_period = self .refractory_period_seconds [unit_index ]
1192+
1193+ # p is the probably of an spike per tick of the sampling frequency
1194+ binomial_p = firing_rate / self .sampling_frequency
1195+ # We estimate how many spikes we will have in the duration
1196+ max_frames = int (self .duration * self .sampling_frequency ) - 1
1197+ max_binomial_p = float (np .max (binomial_p ))
1198+ num_spikes_expected = ceil (max_frames * max_binomial_p )
1199+ num_spikes_std = int (np .sqrt (num_spikes_expected * (1 - max_binomial_p )))
1200+ num_spikes_max = num_spikes_expected + 4 * num_spikes_std
1201+
1202+ # Increase the firing rate to take into account the refractory period
1203+ modified_firing_rate = firing_rate / (1 - firing_rate * refractory_period )
1204+ binomial_p_modified = modified_firing_rate / self .sampling_frequency
1205+ binomial_p_modified = np .minimum (binomial_p_modified , 1.0 )
1206+
1207+ inter_spike_frames = rng .geometric (p = binomial_p_modified , size = num_spikes_max )
1208+ spike_frames = np .cumsum (inter_spike_frames )
1209+
1210+ refractory_period_frames = int (refractory_period * self .sampling_frequency )
1211+ spike_frames [1 :] += refractory_period_frames
1212+
1213+ if start_frame is not None :
1214+ start_index = np .searchsorted (spike_frames , start_frame , side = "left" )
1215+ else :
1216+ start_index = 0
1217+
1218+ if end_frame is not None :
1219+ end_index = np .searchsorted (spike_frames [start_index :], end_frame , side = "left" )
1220+ else :
1221+ end_index = int (self .duration * self .sampling_frequency )
1222+
1223+ spike_frames = spike_frames [start_index :end_index ]
1224+ return spike_frames
1225+
1226+
10571227## Noise generator zone ##
10581228class NoiseGeneratorRecording (BaseRecording ):
10591229 """
0 commit comments