Coverage for src/driada/experiment/synthetic/manifold_spatial_2d.py: 100.00%

98 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-25 15:40 +0300

1""" 

22D spatial manifold generation for place cells. 

3 

4This module contains functions for generating synthetic neural data on 2D spatial 

5manifolds, typically used to model hippocampal place cells. 

6""" 

7 

8import numpy as np 

9from .core import validate_peak_rate, generate_pseudo_calcium_signal 

10from .utils import get_effective_decay_time 

11from ..exp_base import Experiment 

12from ...information.info_base import TimeSeries, MultiTimeSeries 

13 

14 

15def generate_2d_random_walk(length, bounds=(0, 1), step_size=0.02, momentum=0.8, seed=None): 

16 """ 

17 Generate a 2D random walk trajectory within bounded region. 

18  

19 Parameters 

20 ---------- 

21 length : int 

22 Number of time points. 

23 bounds : tuple 

24 (min, max) bounds for x and y coordinates. 

25 step_size : float 

26 Step size for random walk. 

27 momentum : float 

28 Momentum factor (0-1) for smoother trajectories. 

29 seed : int, optional 

30 Random seed. 

31  

32 Returns 

33 ------- 

34 positions : ndarray 

35 Shape (2, length) with x, y coordinates. 

36 """ 

37 if seed is not None: 

38 np.random.seed(seed) 

39 

40 positions = np.zeros((2, length)) 

41 velocity = np.zeros(2) 

42 

43 # Initialize at random position 

44 positions[:, 0] = np.random.uniform(bounds[0], bounds[1], 2) 

45 

46 for t in range(1, length): 

47 # Random walk with momentum 

48 velocity = momentum * velocity + (1 - momentum) * np.random.randn(2) * step_size 

49 

50 # Update position 

51 new_pos = positions[:, t-1] + velocity 

52 

53 # Bounce off walls 

54 for dim in range(2): 

55 if new_pos[dim] < bounds[0]: 

56 new_pos[dim] = bounds[0] 

57 velocity[dim] = -velocity[dim] 

58 elif new_pos[dim] > bounds[1]: 

59 new_pos[dim] = bounds[1] 

60 velocity[dim] = -velocity[dim] 

61 

62 positions[:, t] = new_pos 

63 

64 return positions 

65 

66 

67def gaussian_place_field(positions, center, sigma=0.1): 

68 """ 

69 Calculate neural response using 2D Gaussian place field. 

70  

71 Parameters 

72 ---------- 

73 positions : ndarray 

74 Shape (2, n_timepoints) with x, y coordinates. 

75 center : ndarray 

76 Shape (2,) with place field center coordinates. 

77 sigma : float 

78 Width of the place field. 

79  

80 Returns 

81 ------- 

82 response : ndarray 

83 Neural response (firing rate modulation). 

84 """ 

85 # Calculate squared distance from center 

86 dx = positions[0, :] - center[0] 

87 dy = positions[1, :] - center[1] 

88 dist_sq = dx**2 + dy**2 

89 

90 # Gaussian response 

91 response = np.exp(-dist_sq / (2 * sigma**2)) 

92 

93 return response 

94 

95 

96def generate_2d_manifold_neurons(n_neurons, positions, field_sigma=0.1, 

97 baseline_rate=0.1, peak_rate=1.0, 

98 noise_std=0.05, grid_arrangement=True, 

99 bounds=(0, 1), seed=None): 

100 """ 

101 Generate population of place cells with 2D Gaussian place fields. 

102  

103 Parameters 

104 ---------- 

105 n_neurons : int 

106 Number of neurons. 

107 positions : ndarray 

108 Shape (2, n_timepoints) with x, y positions. 

109 field_sigma : float 

110 Width of place fields. Default is 0.1. 

111 baseline_rate : float 

112 Baseline firing rate. Default is 0.1 Hz. 

113 peak_rate : float 

114 Peak firing rate at place field center. Default is 1.0 Hz. 

115 noise_std : float 

116 Noise in firing rates. 

117 grid_arrangement : bool 

118 If True, arrange place fields in a grid. Otherwise random. 

119 bounds : tuple 

120 (min, max) bounds for place field centers. 

121 seed : int, optional 

122 Random seed. 

123  

124 Returns 

125 ------- 

126 firing_rates : ndarray 

127 Shape (n_neurons, n_timepoints) with firing rates. 

128 place_field_centers : ndarray 

129 Shape (n_neurons, 2) with place field centers. 

130 """ 

131 # Validate firing rate 

132 validate_peak_rate(peak_rate, context="generate_2d_manifold_neurons") 

133 

134 if seed is not None: 

135 np.random.seed(seed) 

136 

137 n_timepoints = positions.shape[1] 

138 

139 # Generate place field centers 

140 if grid_arrangement: 

141 # Arrange in a grid 

142 n_per_side = int(np.ceil(np.sqrt(n_neurons))) 

143 x_centers = np.linspace(bounds[0] + 0.1, bounds[1] - 0.1, n_per_side) 

144 y_centers = np.linspace(bounds[0] + 0.1, bounds[1] - 0.1, n_per_side) 

145 

146 centers = [] 

147 for x in x_centers: 

148 for y in y_centers: 

149 centers.append([x, y]) 

150 if len(centers) >= n_neurons: 

151 break 

152 if len(centers) >= n_neurons: 

153 break 

154 

155 place_field_centers = np.array(centers[:n_neurons]) 

156 

157 # Add small jitter 

158 jitter = np.random.normal(0, 0.02, place_field_centers.shape) 

159 place_field_centers += jitter 

160 place_field_centers = np.clip(place_field_centers, bounds[0], bounds[1]) 

161 else: 

162 # Random placement 

163 place_field_centers = np.random.uniform(bounds[0], bounds[1], (n_neurons, 2)) 

164 

165 # Generate firing rates 

166 firing_rates = np.zeros((n_neurons, n_timepoints)) 

167 

168 for i in range(n_neurons): 

169 # Gaussian place field 

170 place_response = gaussian_place_field(positions, place_field_centers[i], field_sigma) 

171 

172 # Scale to desired firing rate range 

173 firing_rate = baseline_rate + (peak_rate - baseline_rate) * place_response 

174 

175 # Add noise 

176 noise = np.random.normal(0, noise_std, n_timepoints) 

177 firing_rate = np.maximum(0, firing_rate + noise) 

178 

179 firing_rates[i, :] = firing_rate 

180 

181 return firing_rates, place_field_centers 

182 

183 

184def generate_2d_manifold_data(n_neurons, duration=600, sampling_rate=20.0, 

185 field_sigma=0.1, step_size=0.02, momentum=0.8, 

186 baseline_rate=0.1, peak_rate=1.0, 

187 noise_std=0.05, grid_arrangement=True, 

188 decay_time=2.0, calcium_noise_std=0.1, 

189 bounds=(0, 1), seed=None, verbose=True): 

190 """ 

191 Generate synthetic data with neurons on 2D spatial manifold (place cells). 

192  

193 Parameters 

194 ---------- 

195 n_neurons : int 

196 Number of neurons. 

197 duration : float 

198 Duration in seconds. 

199 sampling_rate : float 

200 Sampling rate in Hz. 

201 field_sigma : float 

202 Width of place fields. 

203 step_size : float 

204 Step size for random walk. 

205 momentum : float 

206 Momentum for smoother trajectories. 

207 baseline_rate : float 

208 Baseline firing rate. Default is 0.1 Hz. 

209 peak_rate : float 

210 Peak firing rate. Default is 1.0 Hz. 

211 noise_std : float 

212 Firing rate noise. 

213 grid_arrangement : bool 

214 Arrange place fields in grid. 

215 decay_time : float 

216 Calcium decay time. 

217 calcium_noise_std : float 

218 Calcium signal noise. 

219 bounds : tuple 

220 Spatial bounds. 

221 seed : int, optional 

222 Random seed. 

223 verbose : bool 

224 Print progress. 

225  

226 Returns 

227 ------- 

228 calcium_signals : ndarray 

229 Calcium signals (n_neurons x n_timepoints). 

230 positions : ndarray 

231 Position trajectory (2 x n_timepoints). 

232 place_field_centers : ndarray 

233 Place field centers (n_neurons x 2). 

234 firing_rates : ndarray 

235 Underlying firing rates. 

236 """ 

237 if seed is not None: 

238 np.random.seed(seed) 

239 

240 n_timepoints = int(duration * sampling_rate) 

241 

242 if verbose: 

243 print(f'Generating 2D spatial manifold data: {n_neurons} neurons, {duration}s') 

244 

245 # Generate spatial trajectory 

246 if verbose: 

247 print(' Generating 2D random walk trajectory...') 

248 positions = generate_2d_random_walk(n_timepoints, bounds, step_size, momentum, seed) 

249 

250 # Generate neural responses 

251 if verbose: 

252 print(' Generating neural responses with place fields...') 

253 firing_rates, place_field_centers = generate_2d_manifold_neurons( 

254 n_neurons, positions, field_sigma, 

255 baseline_rate, peak_rate, noise_std, 

256 grid_arrangement, bounds, 

257 seed=(seed + 1) if seed else None 

258 ) 

259 

260 # Convert to calcium signals 

261 if verbose: 

262 print(' Converting to calcium signals...') 

263 calcium_signals = np.zeros((n_neurons, n_timepoints)) 

264 

265 for i in range(n_neurons): 

266 # Generate Poisson events 

267 prob_spike = firing_rates[i, :] / sampling_rate 

268 prob_spike = np.clip(prob_spike, 0, 1) 

269 events = np.random.binomial(1, prob_spike) 

270 

271 # Convert to calcium 

272 calcium_signal = generate_pseudo_calcium_signal( 

273 events=events, 

274 duration=duration, 

275 sampling_rate=sampling_rate, 

276 amplitude_range=(0.5, 2.0), 

277 decay_time=decay_time, 

278 noise_std=calcium_noise_std 

279 ) 

280 calcium_signals[i, :] = calcium_signal 

281 

282 if verbose: 

283 print(' Done!') 

284 

285 return calcium_signals, positions, place_field_centers, firing_rates 

286 

287 

288def generate_2d_manifold_exp(n_neurons=100, duration=600, fps=20.0, 

289 field_sigma=0.1, step_size=0.02, momentum=0.8, 

290 baseline_rate=0.1, peak_rate=1.0, 

291 noise_std=0.05, grid_arrangement=True, 

292 decay_time=2.0, calcium_noise_std=0.1, 

293 bounds=(0, 1), seed=None, verbose=True, return_info=False): 

294 """ 

295 Generate complete experiment with 2D spatial manifold (place cells). 

296  

297 Parameters 

298 ---------- 

299 n_neurons : int 

300 Number of neurons. 

301 duration : float 

302 Duration in seconds. 

303 fps : float 

304 Sampling rate. 

305 field_sigma : float 

306 Place field width. 

307 step_size : float 

308 Random walk step size. 

309 momentum : float 

310 Trajectory smoothness. 

311 baseline_rate : float 

312 Baseline firing rate. Default is 0.1 Hz. 

313 peak_rate : float 

314 Peak firing rate. Default is 1.0 Hz. 

315 noise_std : float 

316 Firing rate noise. 

317 grid_arrangement : bool 

318 Grid arrangement of place fields. 

319 decay_time : float 

320 Calcium decay time. 

321 calcium_noise_std : float 

322 Calcium noise. 

323 bounds : tuple 

324 Spatial bounds. 

325 seed : int, optional 

326 Random seed. 

327 verbose : bool 

328 Print progress. 

329 return_info : bool 

330 If True, return (exp, info) tuple with additional information. 

331  

332 Returns 

333 ------- 

334 exp : Experiment 

335 DRIADA Experiment object with 2D spatial manifold data. 

336 info : dict, optional 

337 If return_info=True, dictionary with manifold info. 

338 """ 

339 # Calculate effective decay time for shuffle mask 

340 effective_decay_time = get_effective_decay_time(decay_time, duration, verbose) 

341 

342 # Generate data 

343 calcium, positions, place_field_centers, firing_rates = generate_2d_manifold_data( 

344 n_neurons=n_neurons, 

345 duration=duration, 

346 sampling_rate=fps, 

347 field_sigma=field_sigma, 

348 step_size=step_size, 

349 momentum=momentum, 

350 baseline_rate=baseline_rate, 

351 peak_rate=peak_rate, 

352 noise_std=noise_std, 

353 grid_arrangement=grid_arrangement, 

354 decay_time=decay_time, 

355 calcium_noise_std=calcium_noise_std, 

356 bounds=bounds, 

357 seed=seed, 

358 verbose=verbose 

359 ) 

360 

361 # Create static features 

362 static_features = { 

363 'fps': fps, 

364 't_rise_sec': 0.04, 

365 't_off_sec': effective_decay_time, # Use effective decay time for shuffle mask 

366 'manifold_type': '2d_spatial', 

367 'field_sigma': field_sigma, 

368 'baseline_rate': baseline_rate, 

369 'peak_rate': peak_rate, 

370 'grid_arrangement': grid_arrangement, 

371 } 

372 

373 # Create dynamic features 

374 position_ts = MultiTimeSeries( 

375 [TimeSeries(positions[0, :], discrete=False), 

376 TimeSeries(positions[1, :], discrete=False)] 

377 ) 

378 

379 # Also create separate x, y features 

380 x_ts = TimeSeries( 

381 data=positions[0, :], 

382 discrete=False 

383 ) 

384 

385 y_ts = TimeSeries( 

386 data=positions[1, :], 

387 discrete=False 

388 ) 

389 

390 dynamic_features = { 

391 'position_2d': position_ts, 

392 'x': x_ts, 

393 'y': y_ts 

394 } 

395 

396 # Store additional information 

397 static_features['place_field_centers'] = place_field_centers 

398 

399 # Create experiment 

400 exp = Experiment( 

401 signature='2d_spatial_manifold_exp', 

402 calcium=calcium, 

403 spikes=None, 

404 static_features=static_features, 

405 dynamic_features=dynamic_features, 

406 exp_identificators={ 

407 'manifold': '2d_spatial', 

408 'n_neurons': n_neurons, 

409 'duration': duration 

410 } 

411 ) 

412 

413 # Store firing rates 

414 exp.firing_rates = firing_rates 

415 

416 # Create info dictionary if requested 

417 if return_info: 

418 info = { 

419 'manifold_type': '2d_spatial', 

420 'n_neurons': n_neurons, 

421 'positions': positions, 

422 'place_field_centers': place_field_centers, 

423 'firing_rates': firing_rates, 

424 'parameters': { 

425 'field_sigma': field_sigma, 

426 'step_size': step_size, 

427 'momentum': momentum, 

428 'baseline_rate': baseline_rate, 

429 'peak_rate': peak_rate, 

430 'noise_std': noise_std, 

431 'grid_arrangement': grid_arrangement, 

432 'decay_time': decay_time, 

433 'calcium_noise_std': calcium_noise_std, 

434 'bounds': bounds 

435 } 

436 } 

437 return exp, info 

438 

439 return exp