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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-25 15:40 +0300
1"""
22D spatial manifold generation for place cells.
4This module contains functions for generating synthetic neural data on 2D spatial
5manifolds, typically used to model hippocampal place cells.
6"""
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
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.
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.
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)
40 positions = np.zeros((2, length))
41 velocity = np.zeros(2)
43 # Initialize at random position
44 positions[:, 0] = np.random.uniform(bounds[0], bounds[1], 2)
46 for t in range(1, length):
47 # Random walk with momentum
48 velocity = momentum * velocity + (1 - momentum) * np.random.randn(2) * step_size
50 # Update position
51 new_pos = positions[:, t-1] + velocity
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]
62 positions[:, t] = new_pos
64 return positions
67def gaussian_place_field(positions, center, sigma=0.1):
68 """
69 Calculate neural response using 2D Gaussian place field.
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.
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
90 # Gaussian response
91 response = np.exp(-dist_sq / (2 * sigma**2))
93 return response
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.
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.
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")
134 if seed is not None:
135 np.random.seed(seed)
137 n_timepoints = positions.shape[1]
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)
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
155 place_field_centers = np.array(centers[:n_neurons])
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))
165 # Generate firing rates
166 firing_rates = np.zeros((n_neurons, n_timepoints))
168 for i in range(n_neurons):
169 # Gaussian place field
170 place_response = gaussian_place_field(positions, place_field_centers[i], field_sigma)
172 # Scale to desired firing rate range
173 firing_rate = baseline_rate + (peak_rate - baseline_rate) * place_response
175 # Add noise
176 noise = np.random.normal(0, noise_std, n_timepoints)
177 firing_rate = np.maximum(0, firing_rate + noise)
179 firing_rates[i, :] = firing_rate
181 return firing_rates, place_field_centers
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).
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.
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)
240 n_timepoints = int(duration * sampling_rate)
242 if verbose:
243 print(f'Generating 2D spatial manifold data: {n_neurons} neurons, {duration}s')
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)
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 )
260 # Convert to calcium signals
261 if verbose:
262 print(' Converting to calcium signals...')
263 calcium_signals = np.zeros((n_neurons, n_timepoints))
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)
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
282 if verbose:
283 print(' Done!')
285 return calcium_signals, positions, place_field_centers, firing_rates
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).
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.
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)
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 )
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 }
373 # Create dynamic features
374 position_ts = MultiTimeSeries(
375 [TimeSeries(positions[0, :], discrete=False),
376 TimeSeries(positions[1, :], discrete=False)]
377 )
379 # Also create separate x, y features
380 x_ts = TimeSeries(
381 data=positions[0, :],
382 discrete=False
383 )
385 y_ts = TimeSeries(
386 data=positions[1, :],
387 discrete=False
388 )
390 dynamic_features = {
391 'position_2d': position_ts,
392 'x': x_ts,
393 'y': y_ts
394 }
396 # Store additional information
397 static_features['place_field_centers'] = place_field_centers
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 )
413 # Store firing rates
414 exp.firing_rates = firing_rates
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
439 return exp