Coverage for src/thunderfish/fakefish.py: 98%

379 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-29 16:21 +0000

1"""Simulate EOD waveforms. 

2 

3 

4## Species names 

5 

6- `species_name`: translate species ids to full species names. 

7- `abbrv_genus()`: abbreviate genus in a species name. 

8 

9 

10## Wavefish 

11 

12- `wavefish_spectrum()`: amplitudes and phases of a wavefish EOD. 

13- `wavefish_eods()`: simulate EOD waveform of a wave-type fish. 

14- `normalize_wavefish()`: normalize amplitudes and phases of EOD wave-type waveform. 

15- `export_wavefish()`: serialize wavefish parameter to file. 

16- `chirps()`: simulate frequency trace with chirps. 

17- `rises()`: simulate frequency trace with rises. 

18 

19 

20## Pulsefish 

21 

22- `pulsefish_eods()`: simulate EOD waveform of a pulse-type fish. 

23- `normalize_pulsefish()`: normalize times and stdevs of pulse-type EOD waveform. 

24- `export_pulsefish()`: serialize pulsefish parameter to file. 

25 

26 

27## Interactive waveform generation 

28 

29- `generate_waveform()`: interactively generate audio file with simulated EOD waveforms. 

30""" 

31 

32import sys 

33import numpy as np 

34 

35 

36species_name = dict(Sine='Sinewave', 

37 Alepto='Apteronotus leptorhynchus', 

38 Arostratus='Apteronotus rostratus', 

39 Eigenmannia='Eigenmannia spec.', 

40 Sternarchella='Sternarchella terminalis', 

41 Sternopygus='Sternopygus dariensis') 

42"""Translate species ids used by wavefish_harmonics and pulsefish_eodpeaks to full species names. 

43""" 

44 

45 

46def abbrv_genus(name): 

47 """Abbreviate genus in a species name. 

48 

49 Parameters 

50 ---------- 

51 name: string 

52 Full species name of the form 'Genus species subspecies' 

53 

54 Returns 

55 ------- 

56 name: string 

57 The species name with abbreviated genus, i.e. 'G. species subspecies' 

58 """ 

59 ns = name.split() 

60 return ns[0][0] + '. ' + ' '.join(ns[1:]) 

61 

62 

63# Amplitudes and phases of various wavefish species: 

64 

65Sine_harmonics = dict(amplitudes=(1.0,), phases=(0.5*np.pi,)) 

66 

67Apteronotus_leptorhynchus_harmonics = \ 

68 dict(amplitudes=(0.90062, 0.15311, 0.072049, 0.012609, 0.011708), 

69 phases=(1.3623, 2.3246, 0.9869, 2.6492, -2.6885)) 

70 

71Apteronotus_rostratus_harmonics = \ 

72 dict(amplitudes=(0.64707, 0.43874, 0.063592, 0.07379, 0.040199, 0.023073, 

73 0.0097678), 

74 phases=(2.2988, 0.78876, -1.316, 2.2416, 2.0413, 1.1022, 

75 -2.0513)) 

76 

77Eigenmannia_harmonics = \ 

78 dict(amplitudes=(1.0087, 0.23201, 0.060524, 0.020175, 0.010087, 0.0080699), 

79 phases=(1.3414, 1.3228, 2.9242, 2.8157, 2.6871, -2.8415)) 

80 

81Sternarchella_terminalis_harmonics = \ 

82 dict(amplitudes=(0.11457, 0.4401, 0.41055, 0.20132, 0.061364, 0.011389, 

83 0.0057985), 

84 phases=(-2.7106, 2.4472, 1.6829, 0.79085, 0.119, -0.82355, 

85 -1.9956)) 

86 

87Sternopygus_dariensis_harmonics = \ 

88 dict(amplitudes=(0.98843, 0.41228, 0.047848, 0.11048, 0.022801, 0.030706, 

89 0.019018), 

90 phases=(1.4153, 1.3141, 3.1062, -2.3961, -1.9524, 0.54321, 

91 1.6844)) 

92 

93wavefish_harmonics = dict(Sine=Sine_harmonics, 

94 Alepto=Apteronotus_leptorhynchus_harmonics, 

95 Arostratus=Apteronotus_rostratus_harmonics, 

96 Eigenmannia=Eigenmannia_harmonics, 

97 Sternarchella=Sternarchella_terminalis_harmonics, 

98 Sternopygus=Sternopygus_dariensis_harmonics) 

99"""Amplitudes and phases of EOD waveforms of various species of wave-type electric fish.""" 

100 

101 

102def wavefish_spectrum(fish): 

103 """Amplitudes and phases of a wavefish EOD. 

104 

105 Parameters 

106 ---------- 

107 fish: string, dict or tuple of lists/arrays 

108 Specify relative amplitudes and phases of the fundamental and its harmonics. 

109 If string then take amplitudes and phases from the `wavefish_harmonics` dictionary. 

110 If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys. 

111 If tuple then the first element is the list of amplitudes and 

112 the second one the list of relative phases in radians. 

113 

114 Returns 

115 ------- 

116 amplitudes: array of floats 

117 Amplitudes of the fundamental and its harmonics. 

118 phases: array of floats 

119 Phases in radians of the fundamental and its harmonics. 

120 

121 Raises 

122 ------ 

123 KeyError: 

124 Unknown fish. 

125 IndexError: 

126 Amplitudes and phases differ in length. 

127 """ 

128 if isinstance(fish, (tuple, list)): 

129 amplitudes = fish[0] 

130 phases = fish[1] 

131 elif isinstance(fish, dict): 

132 amplitudes = fish['amplitudes'] 

133 phases = fish['phases'] 

134 else: 

135 if not fish in wavefish_harmonics: 

136 raise KeyError('unknown wavefish. Choose one of ' + 

137 ', '.join(wavefish_harmonics.keys())) 

138 amplitudes = wavefish_harmonics[fish]['amplitudes'] 

139 phases = wavefish_harmonics[fish]['phases'] 

140 if len(amplitudes) != len(phases): 

141 raise IndexError('need exactly as many phases as amplitudes') 

142 # remove NaNs: 

143 for k in reversed(range(len(amplitudes))): 

144 if np.isfinite(amplitudes[k]) or np.isfinite(phases[k]): 

145 amplitudes = amplitudes[:k+1] 

146 phases = phases[:k+1] 

147 break 

148 return amplitudes, phases 

149 

150 

151def wavefish_eods(fish='Eigenmannia', frequency=100.0, samplerate=44100.0, 

152 duration=1.0, phase0=0.0, noise_std=0.05): 

153 """Simulate EOD waveform of a wave-type fish. 

154  

155 The waveform is constructed by superimposing sinewaves of integral 

156 multiples of the fundamental frequency - the fundamental and its 

157 harmonics. The fundamental frequency of the EOD (EODf) is given by 

158 `frequency`. With `fish` relative amplitudes and phases of the 

159 fundamental and its harmonics are specified. 

160 

161 The generated waveform is `duration` seconds long and is sampled with 

162 `samplerate` Hertz. Gaussian white noise with a standard deviation of 

163 `noise_std` is added to the generated waveform. 

164 

165 Parameters 

166 ---------- 

167 fish: string, dict or tuple of lists/arrays 

168 Specify relative amplitudes and phases of the fundamental and its harmonics. 

169 If string then take amplitudes and phases from the `wavefish_harmonics` dictionary. 

170 If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys. 

171 If tuple then the first element is the list of amplitudes and 

172 the second one the list of relative phases in radians. 

173 frequency: float or array of floats 

174 EOD frequency of the fish in Hertz. Either fixed number or array for 

175 time-dependent frequencies. 

176 samplerate: float 

177 Sampling rate in Hertz. 

178 duration: float 

179 Duration of the generated data in seconds. Only used if frequency is scalar. 

180 phase0: float 

181 Phase offset of the EOD waveform in radians. 

182 noise_std: float 

183 Standard deviation of additive Gaussian white noise. 

184 

185 Returns 

186 ------- 

187 data: array of floats 

188 Generated data of a wave-type fish. 

189 

190 Raises 

191 ------ 

192 KeyError: 

193 Unknown fish. 

194 IndexError: 

195 Amplitudes and phases differ in length. 

196 """ 

197 # get relative amplitude and phases: 

198 amplitudes, phases = wavefish_spectrum(fish) 

199 # compute phase: 

200 if np.isscalar(frequency): 

201 phase = np.arange(0, duration, 1.0/samplerate) 

202 phase *= frequency 

203 else: 

204 phase = np.cumsum(frequency)/samplerate 

205 # generate EOD: 

206 data = np.zeros(len(phase)) 

207 for har, (ampl, phi) in enumerate(zip(amplitudes, phases)): 

208 if np.isfinite(ampl) and np.isfinite(phi): 

209 data += ampl * np.sin(2*np.pi*(har+1)*phase + phi - (har+1)*phase0) 

210 # add noise: 

211 data += noise_std * np.random.randn(len(data)) 

212 return data 

213 

214 

215def normalize_wavefish(fish, mode='peak'): 

216 """Normalize amplitudes and phases of wave-type EOD waveform. 

217 

218 The amplitudes and phases of the Fourier components are adjusted 

219 such that the resulting EOD waveform has a peak-to-peak amplitude 

220 of two and the peak of the waveform is at time zero (mode is set 

221 to 'peak') or that the fundamental has an amplitude of one and a 

222 phase of 0 (mode is set to 'zero'). 

223 

224 Parameters 

225 ---------- 

226 fish: string, dict or tuple of lists/arrays 

227 Specify relative amplitudes and phases of the fundamental and its harmonics. 

228 If string then take amplitudes and phases from the `wavefish_harmonics` dictionary. 

229 If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys. 

230 If tuple then the first element is the list of amplitudes and 

231 the second one the list of relative phases in radians. 

232 mode: 'peak' or 'zero' 

233 How to normalize amplitude and phases: 

234 - 'peak': normalize waveform to a peak-to-peak amplitude of two 

235 and shift it such that its peak is at time zero. 

236 - 'zero': scale amplitude of fundamental to one and its phase to zero. 

237 

238 Returns 

239 ------- 

240 amplitudes: array of floats 

241 Adjusted amplitudes of the fundamental and its harmonics. 

242 phases: array of floats 

243 Adjusted phases in radians of the fundamental and its harmonics. 

244 

245 """ 

246 # get relative amplitude and phases: 

247 amplitudes, phases = wavefish_spectrum(fish) 

248 if mode == 'zero': 

249 newamplitudes = np.array(amplitudes)/amplitudes[0] 

250 newphases = np.array([p+(k+1)*(-phases[0]) for k, p in enumerate(phases)]) 

251 newphases %= 2.0*np.pi 

252 newphases[newphases>np.pi] -= 2.0*np.pi 

253 return newamplitudes, newphases 

254 else: 

255 # generate waveform: 

256 eodf = 100.0 

257 rate = 100000.0 

258 data = wavefish_eods(fish, eodf, rate, 2.0/eodf, noise_std=0.0) 

259 # normalize amplitudes: 

260 ampl = 0.5*(np.max(data) - np.min(data)) 

261 newamplitudes = np.array(amplitudes)/ampl 

262 # shift phases: 

263 deltat = np.argmax(data[:int(rate/eodf)])/rate 

264 deltap = 2.0*np.pi*deltat*eodf 

265 newphases = np.array([p+(k+1)*deltap for k, p in enumerate(phases)]) 

266 newphases %= 2.0*np.pi 

267 newphases[newphases>np.pi] -= 2.0*np.pi 

268 # return: 

269 return newamplitudes, newphases 

270 

271 

272def export_wavefish(fish, name='Unknown_harmonics', file=None): 

273 """Serialize wavefish parameter to python code. 

274 

275 Add output to the wavefish_harmonics dictionary! 

276 

277 Parameters 

278 ---------- 

279 fish: string, dict or tuple of lists/arrays 

280 Specify relative amplitudes and phases of the fundamental and its harmonics. 

281 If string then take amplitudes and phases from the `wavefish_harmonics` dictionary. 

282 If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys. 

283 If tuple then the first element is the list of amplitudes and 

284 the second one the list of relative phases in radians. 

285 name: string 

286 Name of the dictionary to be written. 

287 file: string or file or None 

288 File name or open file object where to write wavefish dictionary. 

289 

290 Returns 

291 ------- 

292 fish: dict 

293 Dictionary with amplitudes and phases. 

294 """ 

295 # get relative amplitude and phases: 

296 amplitudes, phases = wavefish_spectrum(fish) 

297 # write out dictionary: 

298 if file is None: 

299 file = sys.stdout 

300 try: 

301 file.write('') 

302 closeit = False 

303 except AttributeError: 

304 file = open(file, 'w') 

305 closeit = True 

306 n = 6 

307 file.write(name + ' = \\\n') 

308 file.write(' dict(amplitudes=(') 

309 file.write(', '.join([f'{a:.5g}' for a in amplitudes[:n]])) 

310 for k in range(n, len(amplitudes), n): 

311 file.write(',\n') 

312 file.write(' ' * (9+12)) 

313 file.write(', '.join([f'{a:.5g}' for a in amplitudes[k:k+n]])) 

314 file.write('),\n') 

315 file.write(' ' * 9 + 'phases=(') 

316 file.write(', '.join(['{p:.5g}' for p in phases[:n]])) 

317 for k in range(n, len(phases), n): 

318 file.write(',\n') 

319 file.write(' ' * (9+8)) 

320 file.write(', '.join([f'{p:.5g}' for p in phases[k:k+n]])) 

321 file.write('))\n') 

322 if closeit: 

323 file.close() 

324 # return dictionary: 

325 harmonics = dict(amplitudes=amplitudes, 

326 phases=phases) 

327 return harmonics 

328 

329 

330def chirps(eodf=100.0, samplerate=44100.0, duration=1.0, chirp_freq=5.0, 

331 chirp_size=100.0, chirp_width=0.01, chirp_kurtosis=1.0, chirp_contrast=0.05): 

332 """Simulate frequency trace with chirps. 

333 

334 A chirp is modeled as a Gaussian frequency modulation. 

335 The first chirp is placed at 0.5/chirp_freq. 

336 

337 Parameters 

338 ---------- 

339 eodf: float 

340 EOD frequency of the fish in Hertz. 

341 samplerate: float 

342 Sampling rate in Hertz. 

343 duration: float 

344 Duration of the generated data in seconds. 

345 chirp_freq: float 

346 Frequency of occurance of chirps in Hertz. 

347 chirp_size: float 

348 Size of the chirp (maximum frequency increase above eodf) in Hertz. 

349 chirp_width: float 

350 Width of the chirp at 10% height in seconds. 

351 chirp_kurtosis: float: 

352 Shape of the chirp. =1: Gaussian, >1: more rectangular, <1: more peaked. 

353 chirp_contrast: float 

354 Maximum amplitude reduction of EOD during chirp. 

355 

356 Returns 

357 ------- 

358 frequency: array of floats 

359 Generated frequency trace that can be passed on to wavefish_eods(). 

360 amplitude: array of floats 

361 Generated amplitude modulation that can be used to multiply the trace generated by 

362 wavefish_eods(). 

363 """ 

364 # baseline eod frequency and amplitude modulation: 

365 n = len(np.arange(0, duration, 1.0/samplerate)) 

366 frequency = eodf * np.ones(n) 

367 am = np.ones(n) 

368 # time points for chirps: 

369 chirp_period = 1.0/chirp_freq 

370 chirp_times = np.arange(0.5*chirp_period, duration, chirp_period) 

371 # chirp frequency waveform: 

372 chirp_t = np.arange(-2.0*chirp_width, 2.0*chirp_width, 1./samplerate) 

373 chirp_sig = 0.5*chirp_width / (2.0*np.log(10.0))**(0.5/chirp_kurtosis) 

374 gauss = np.exp(-0.5*((chirp_t/chirp_sig)**2.0)**chirp_kurtosis) 

375 # add chirps on baseline eodf: 

376 for ct in chirp_times: 

377 index = int(ct*samplerate) 

378 i0 = index - len(gauss)//2 

379 i1 = i0 + len(gauss) 

380 gi0 = 0 

381 gi1 = len(gauss) 

382 if i0 < 0: 

383 gi0 -= i0 

384 i0 = 0 

385 if i1 >= len(frequency): 

386 gi1 -= i1 - len(frequency) 

387 i1 = len(frequency) 

388 frequency[i0:i1] += chirp_size * gauss[gi0:gi1] 

389 am[i0:i1] -= chirp_contrast * gauss[gi0:gi1] 

390 return frequency, am 

391 

392 

393def rises(eodf=100.0, samplerate=44100.0, duration=1.0, rise_freq=0.1, 

394 rise_size=10.0, rise_tau=1.0, decay_tau=10.0): 

395 """Simulate frequency trace with rises. 

396 

397 A rise is modeled as a double exponential frequency modulation. 

398 

399 Parameters 

400 ---------- 

401 eodf: float 

402 EOD frequency of the fish in Hertz. 

403 samplerate: float 

404 Sampling rate in Hertz. 

405 duration: float 

406 Duration of the generated data in seconds. 

407 rise_freq: float 

408 Frequency of occurance of rises in Hertz. 

409 rise_size: float 

410 Size of the rise (frequency increase above eodf) in Hertz. 

411 rise_tau: float 

412 Time constant of the frequency increase of the rise in seconds. 

413 decay_tau: float 

414 Time constant of the frequency decay of the rise in seconds. 

415 

416 Returns 

417 ------- 

418 data: array of floats 

419 Generated frequency trace that can be passed on to wavefish_eods(). 

420 """ 

421 n = len(np.arange(0, duration, 1.0/samplerate)) 

422 # baseline eod frequency: 

423 frequency = eodf * np.ones(n) 

424 # time points for rises: 

425 rise_period = 1.0/rise_freq 

426 rise_times = np.arange(0.5*rise_period, duration, rise_period) 

427 # rise frequency waveform: 

428 rise_t = np.arange(0.0, 5.0*decay_tau, 1./samplerate) 

429 rise = rise_size * (1.0-np.exp(-rise_t/rise_tau)) * np.exp(-rise_t/decay_tau) 

430 # add rises on baseline eodf: 

431 for r in rise_times: 

432 index = int(r*samplerate) 

433 if index+len(rise) > len(frequency): 

434 rise_index = len(frequency)-index 

435 frequency[index:index+rise_index] += rise[:rise_index] 

436 break 

437 else: 

438 frequency[index:index+len(rise)] += rise 

439 return frequency 

440 

441 

442# Positions, amplitudes and standard deviations of peaks of various pulsefish species: 

443 

444Monophasic_peaks = \ 

445 dict(times=(0,), 

446 amplitudes=(1,), 

447 stdevs=(0.0003,)) 

448 

449Biphasic_peaks = \ 

450 dict(times=(9e-05, 0.00049), 

451 amplitudes=(1.1922, -0.95374), 

452 stdevs=(0.0003, 0.00025)) 

453 

454Triphasic_peaks = \ 

455 dict(times=(3e-05, 0.00018, 0.00043), 

456 amplitudes=(1.2382, -0.9906, 0.12382), 

457 stdevs=(0.0001, 0.0001, 0.0002)) 

458 

459pulsefish_eodpeaks = dict(Monophasic=Monophasic_peaks, 

460 Biphasic=Biphasic_peaks, 

461 Triphasic=Triphasic_peaks) 

462"""Standard deviations, amplitudes and positions of Gaussians that 

463 make up EOD waveforms of pulse-type electric fish. 

464""" 

465 

466 

467def pulsefish_peaks(fish): 

468 """Position, amplitudes and standard deviations of peaks in pulsefish EOD waveforms. 

469 

470 Parameters 

471 ---------- 

472 fish: string, dict or tuple of floats/lists/arrays 

473 Specify positions, amplitudes and standard deviations Gaussians peaks that are 

474 superimposed to simulate EOD waveforms of pulse-type electric fishes.  

475 If string then take positions, amplitudes and standard deviations  

476 from the `pulsefish_eodpeaks` dictionary. 

477 If dictionary then take pulse properties from the 'times', 'amlitudes' 

478 and 'stdevs' keys. 

479 If tuple then the first element is the list of peak positions, 

480 the second is the list of corresponding amplitudes, and 

481 the third one the list of corresponding standard deviations. 

482 

483 Returns 

484 ------- 

485 times : array of floats 

486 Positions of the peaks. 

487 amplitudes : array of floats 

488 Amplitudes of the peaks. 

489 stdevs : array of floats 

490 Standard deviations of the peaks. 

491 

492 Raises 

493 ------ 

494 KeyError: 

495 Unknown fish. 

496 IndexError: 

497 Peak positions, amplitudes, or standard deviations differ in length. 

498 """ 

499 if isinstance(fish, (tuple, list)): 

500 peak_times = fish[0] 

501 peak_amplitudes = fish[1] 

502 peak_stdevs = fish[2] 

503 elif isinstance(fish, dict): 

504 peak_times = fish['times'] 

505 peak_amplitudes = fish['amplitudes'] 

506 peak_stdevs = fish['stdevs'] 

507 else: 

508 if not fish in pulsefish_eodpeaks: 

509 raise KeyError('unknown pulse-type fish. Choose one of ' + 

510 ', '.join(pulsefish_eodpeaks.keys())) 

511 peak_times = pulsefish_eodpeaks[fish]['times'] 

512 peak_amplitudes = pulsefish_eodpeaks[fish]['amplitudes'] 

513 peak_stdevs = pulsefish_eodpeaks[fish]['stdevs'] 

514 if len(peak_stdevs) != len(peak_amplitudes) or len(peak_stdevs) != len(peak_times): 

515 raise IndexError('need exactly as many standard deviations as amplitudes and times') 

516 return peak_times, peak_amplitudes, peak_stdevs 

517 

518 

519def pulsefish_eods(fish='Biphasic', frequency=100.0, samplerate=44100.0, 

520 duration=1.0, noise_std=0.01, jitter_cv=0.1, 

521 first_pulse=None): 

522 """Simulate EOD waveform of a pulse-type fish. 

523 

524 Pulses are spaced by 1/frequency, jittered as determined by jitter_cv. Each pulse is 

525 a combination of Gaussian peaks, whose positions, amplitudes and widths are 

526 given by 'fish'. 

527 

528 The generated waveform is duration seconds long and is sampled with samplerate Hertz. 

529 Gaussian white noise with a standard deviation of noise_std is added to the generated 

530 pulse train. 

531 

532 Parameters 

533 ---------- 

534 fish: string, dict or tuple of floats/lists/arrays 

535 Specify positions, amplitudes and standard deviations Gaussians peaks that are 

536 superimposed to simulate EOD waveforms of pulse-type electric fishes.  

537 If string then take positions, amplitudes and standard deviations  

538 from the `pulsefish_eodpeaks` dictionary. 

539 If dictionary then take pulse properties from the 'times', 'amlitudes' 

540 and 'stdevs' keys. 

541 If tuple then the first element is the list of peak positions, 

542 the second is the list of corresponding amplitudes, and 

543 the third one the list of corresponding standard deviations. 

544 frequency: float 

545 EOD frequency of the fish in Hz. 

546 samplerate: float 

547 Sampling Rate in Hz. 

548 duration: float 

549 Duration of the generated data in seconds. 

550 noise_std: float 

551 Standard deviation of additive Gaussian white noise. 

552 jitter_cv: float 

553 Gaussian distributed jitter of pulse times as coefficient of variation 

554 of inter-pulse intervals. 

555 first_pulse: float or None 

556 The position of the first pulse. If None it is choosen automatically 

557 depending on pulse width, jitter, and frequency. 

558 

559 Returns 

560 ------- 

561 data: array of floats 

562 Generated data of a pulse-type fish. 

563 

564 Raises 

565 ------ 

566 KeyError: 

567 Unknown fish. 

568 IndexError: 

569 Peak positions, amplitudes, or standard deviations differ in length. 

570 """ 

571 # get peak properties: 

572 peak_times, peak_amplitudes, peak_stdevs = pulsefish_peaks(fish) 

573 # time axis for single pulse: 

574 min_time_inx = np.argmin(peak_times) 

575 max_time_inx = np.argmax(peak_times) 

576 tmax = max(np.abs(peak_times[min_time_inx]-4.0*peak_stdevs[min_time_inx]), 

577 np.abs(peak_times[max_time_inx]+4.0*peak_stdevs[max_time_inx])) 

578 x = np.arange(-tmax, tmax, 1.0/samplerate) 

579 pulse_duration = x[-1] - x[0] 

580 

581 # generate a single pulse: 

582 pulse = np.zeros(len(x)) 

583 for time, ampl, std in zip(peak_times, peak_amplitudes, peak_stdevs): 

584 pulse += ampl * np.exp(-0.5*((x-time)/std)**2) 

585 poffs = len(pulse)//2 

586 

587 # paste the pulse into the noise floor: 

588 time = np.arange(0, duration, 1.0/samplerate) 

589 data = np.random.randn(len(time)) * noise_std 

590 period = 1.0/frequency 

591 jitter_std = period * jitter_cv 

592 if first_pulse is None: 

593 first_pulse = np.max([pulse_duration, 3.0*jitter_std]) 

594 pulse_times = np.arange(first_pulse, duration, period ) 

595 pulse_times += jitter_std*np.random.randn(len(pulse_times)) 

596 pulse_indices = np.round(pulse_times * samplerate).astype(int) 

597 for inx in pulse_indices[(pulse_indices>=poffs)&(pulse_indices-poffs+len(pulse)<len(data))]: 

598 data[inx-poffs:inx-poffs+len(pulse)] += pulse 

599 return data 

600 

601 

602def normalize_pulsefish(fish): 

603 """Normalize times and stdevs of pulse-type EOD waveform. 

604 

605 The positions and amplitudes of Gaussian peaks are adjusted such 

606 that the resulting EOD waveform has a maximum peak amplitude of one 

607 and has the largest peak at time zero. 

608 

609 Parameters 

610 ---------- 

611 fish: string, dict or tuple of floats/lists/arrays 

612 Specify positions, amplitudes and standard deviations Gaussians peaks that are 

613 superimposed to simulate EOD waveforms of pulse-type electric fishes.  

614 If string then take positions, amplitudes and standard deviations  

615 from the `pulsefish_eodpeaks` dictionary. 

616 If dictionary then take pulse properties from the 'times', 'amlitudes' 

617 and 'stdevs' keys. 

618 If tuple then the first element is the list of peak positions, 

619 the second is the list of corresponding amplitudes, and 

620 the third one the list of corresponding standard deviations. 

621 

622 Returns 

623 ------- 

624 fish: dict 

625 Dictionary with adjusted times and standard deviations. 

626 """ 

627 # get peak properties: 

628 peak_times, peak_amplitudes, peak_stdevs = pulsefish_peaks(fish) 

629 # generate waveform: 

630 eodf = 10.0 

631 rate = 100000.0 

632 first_pulse = 0.5/eodf 

633 data = pulsefish_eods(fish, eodf, rate, 1.0/eodf, noise_std=0.0, 

634 jitter_cv=0.0, first_pulse=first_pulse) 

635 # maximum peak: 

636 idx = np.argmax(np.abs(data)) 

637 # normalize amplitudes: 

638 ampl = data[idx] 

639 newamplitudes = np.array(peak_amplitudes)/ampl 

640 # shift times: 

641 deltat = idx/rate - first_pulse 

642 newtimes = np.array(peak_times) - deltat 

643 # store and return: 

644 peaks = dict(times=newtimes, 

645 amplitudes=newamplitudes, 

646 stdevs=peak_stdevs) 

647 return peaks 

648 

649 

650def export_pulsefish(fish, name='Unknown_peaks', file=None): 

651 """Serialize pulsefish parameter to python code. 

652 

653 Add output to the pulsefish_eodpeaks dictionary! 

654 

655 Parameters 

656 ---------- 

657 fish: string, dict or tuple of floats/lists/arrays 

658 Specify positions, amplitudes and standard deviations Gaussians peaks that are 

659 superimposed to simulate EOD waveforms of pulse-type electric fishes.  

660 If string then take positions, amplitudes and standard deviations  

661 from the `pulsefish_eodpeaks` dictionary. 

662 If dictionary then take pulse properties from the 'times', 'amlitudes' 

663 and 'stdevs' keys. 

664 If tuple then the first element is the list of peak positions, 

665 the second is the list of corresponding amplitudes, and 

666 the third one the list of corresponding standard deviations. 

667 name: string 

668 Name of the dictionary to be written. 

669 file: string or file or None 

670 File name or open file object where to write pulsefish dictionary. 

671 

672 Returns 

673 ------- 

674 fish: dict 

675 Dictionary with peak times, amplitudes and standard deviations. 

676 """ 

677 # get peak properties: 

678 peak_times, peak_amplitudes, peak_stdevs = pulsefish_peaks(fish) 

679 # write out dictionary: 

680 if file is None: 

681 file = sys.stdout 

682 try: 

683 file.write('') 

684 closeit = False 

685 except AttributeError: 

686 file = open(file, 'w') 

687 closeit = True 

688 n = 6 

689 file.write(name + ' = \\\n') 

690 file.write(' dict(times=(') 

691 file.write(', '.join([f'{a:.5g}' for a in peak_times[:n]])) 

692 for k in range(n, len(peak_times), n): 

693 file.write(',\n') 

694 file.write(' ' * (9+12)) 

695 file.write(', '.join([f'{a:.5g}' for a in peak_times[k:k+n]])) 

696 if len(peak_times) == 1: 

697 file.write(',') 

698 file.write('),\n') 

699 file.write(' ' * 9 + 'amplitudes=(') 

700 file.write(', '.join([f'{p:.5g}' for p in peak_amplitudes[:n]])) 

701 for k in range(n, len(peak_amplitudes), n): 

702 file.write(',\n') 

703 file.write(' ' * (9+8)) 

704 file.write(', '.join([f'{p:.5g}' for p in peak_amplitudes[k:k+n]])) 

705 if len(peak_amplitudes) == 1: 

706 file.write(',') 

707 file.write('),\n') 

708 file.write(' ' * 9 + 'stdevs=(') 

709 file.write(', '.join([f'{p:.5g}' for p in peak_stdevs[:n]])) 

710 for k in range(n, len(peak_stdevs), n): 

711 file.write(',\n') 

712 file.write(' ' * (9+8)) 

713 file.write(', '.join([f'{p:.5g}' for p in peak_stdevs[k:k+n]])) 

714 if len(peak_stdevs) == 1: 

715 file.write(',') 

716 file.write('))\n') 

717 if closeit: 

718 file.close() 

719 # return dictionary: 

720 peaks = dict(times=peak_times, 

721 amplitudes=peak_amplitudes, 

722 stdevs=peak_stdevs) 

723 return peaks 

724 

725 

726def generate_waveform(filename): 

727 """Interactively generate audio file with simulated EOD waveforms. 

728 

729 Parameters needed to generate EOD waveforms are take from console input. 

730 

731 Parameters 

732 ---------- 

733 filename: string 

734 Name of file inclusively extension (e.g. '.wav') 

735 used to store the simulated EOD waveforms. 

736 """ 

737 import os 

738 from audioio import write_audio 

739 from thunderlab.consoleinput import read, select, save_inputs 

740 # generate file: 

741 samplerate = read('Sampling rate in Hz', '44100', float, 1.0) 

742 duration = read('Duration in seconds', '10', float, 0.001) 

743 nfish = read('Number of fish', '1', int, 1) 

744 ndata = read('Number of electrodes', '1', int, 1) 

745 fish_spread = 1 

746 if ndata > 1: 

747 fish_spread = read('Number of electrodes fish are spread over', '2', int, 1) 

748 data = np.random.randn(int(duration*samplerate), ndata)*0.01 

749 fish_indices = np.random.randint(ndata, size=nfish) 

750 eodt = 'a' 

751 eodf = 800.0 

752 eoda = 1.0 

753 eodsig = 'n' 

754 pulse_jitter = 0.1 

755 chirp_freq = 5.0 

756 chirp_size = 100.0 

757 chirp_width = 0.01 

758 chirp_kurtosis = 1.0 

759 rise_freq = 0.1 

760 rise_size = 10.0 

761 rise_tau = 1.0 

762 rise_decay_tau = 10.0 

763 for k in range(nfish): 

764 print('') 

765 fish = 'Fish %d: ' % (k+1) 

766 eodt = select(fish + 'EOD type', eodt, ['a', 'e', '1', '2', '3'], 

767 ['Apteronotus', 'Eigenmannia', 

768 'monophasic pulse', 'biphasic pulse', 'triphasic pulse']) 

769 eodf = read(fish + 'EOD frequency in Hz', '%g'%eodf, float, 1.0, 3000.0) 

770 eoda = read(fish + 'EOD amplitude', '%g'%eoda, float, 0.0, 10.0) 

771 if eodt in 'ae': 

772 eodsig = select(fish + 'Add communication signals', eodsig, ['n', 'c', 'r'], 

773 ['fixed EOD', 'chirps', 'rises']) 

774 eodfreq = eodf 

775 if eodsig == 'c': 

776 chirp_freq = read('Number of chirps per second', '%g'%chirp_freq, float, 0.001) 

777 chirp_size = read('Size of chirp in Hz', '%g'%chirp_size, float, 1.0) 

778 chirp_width = 0.001*read('Width of chirp in ms', '%g'%(1000.0*chirp_width), float, 1.0) 

779 eodfreq, _ = chirps(eodf, samplerate, duration, 

780 chirp_freq, chirp_size, chirp_width, chirp_kurtosis) 

781 elif eodsig == 'r': 

782 rise_freq = read('Number of rises per second', '%g'%rise_freq, float, 0.00001) 

783 rise_size = read('Size of rise in Hz', '%g'%rise_size, float, 0.01) 

784 rise_tau = read('Time-constant of rise onset in seconds', '%g'%rise_tau, float, 0.01) 

785 rise_decay_tau = read('Time-constant of rise decay in seconds', '%g'%rise_decay_tau, float, 0.01) 

786 eodfreq = rises(eodf, samplerate, duration, 

787 rise_freq, rise_size, rise_tau, rise_decay_tau) 

788 if eodt == 'a': 

789 fishdata = eoda*wavefish_eods('Alepto', eodfreq, samplerate, duration, 

790 phase0=0.0, noise_std=0.0) 

791 elif eodt == 'e': 

792 fishdata = eoda*wavefish_eods('Eigenmannia', eodfreq, samplerate, 

793 duration, phase0=0.0, noise_std=0.0) 

794 else: 

795 pulse_jitter = read(fish + 'CV of pulse jitter', '%g'%pulse_jitter, float, 0.0, 2.0) 

796 if eodt == '1': 

797 fishdata = eoda*pulsefish_eods('Monophasic', eodf, samplerate, duration, 

798 jitter_cv=pulse_jitter, noise_std=0.0) 

799 elif eodt == '2': 

800 fishdata = eoda*pulsefish_eods('Biphasic', eodf, samplerate, duration, 

801 jitter_cv=pulse_jitter, noise_std=0.0) 

802 elif eodt == '3': 

803 fishdata = eoda*pulsefish_eods('Triphasic', eodf, samplerate, duration, 

804 jitter_cv=pulse_jitter, noise_std=0.0) 

805 i = fish_indices[k] 

806 for j in range(fish_spread): 

807 data[:, (i+j)%ndata] += fishdata*(0.2**j) 

808 

809 maxdata = np.max(np.abs(data)) 

810 write_audio(filename, 0.9*data/maxdata, samplerate) 

811 input_file = os.path.splitext(filename)[0] + '.inp' 

812 save_inputs(input_file) 

813 print(f'\nWrote fakefish data to file "{filename}".') 

814 

815 

816def demo(): 

817 import matplotlib.pyplot as plt 

818 samplerate = 40000.0 # in Hz 

819 duration = 10.0 # in sec 

820 

821 inset_len = 0.01 # in sec 

822 inset_indices = int(inset_len*samplerate) 

823 ws_fac = 0.1 # whitespace factor or ylim (between 0. and 1.) 

824 

825 # generate data: 

826 eodf = 400.0 

827 wavefish = wavefish_eods('Alepto', eodf, samplerate, duration, noise_std=0.02) 

828 eodf = 650.0 

829 wavefish += 0.5*wavefish_eods('Eigenmannia', eodf, samplerate, duration) 

830 

831 pulsefish = pulsefish_eods('Biphasic', 80.0, samplerate, duration, 

832 noise_std=0.02, jitter_cv=0.1, first_pulse=inset_len/2) 

833 time = np.arange(len(wavefish))/samplerate 

834 

835 fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(19, 10)) 

836 

837 # get proper wavefish ylim 

838 ymin = np.min(wavefish) 

839 ymax = np.max(wavefish) 

840 dy = ws_fac*(ymax - ymin) 

841 ymin -= dy 

842 ymax += dy 

843 

844 # complete wavefish: 

845 ax[0][0].set_title('Wavefish') 

846 ax[0][0].set_ylim(ymin, ymax) 

847 ax[0][0].plot(time, wavefish) 

848 

849 # wavefish zoom in: 

850 ax[0][1].set_title('Wavefish ZOOM IN') 

851 ax[0][1].set_ylim(ymin, ymax) 

852 ax[0][1].plot(time[:inset_indices], wavefish[:inset_indices], '-o') 

853 

854 # get proper pulsefish ylim 

855 ymin = np.min(pulsefish) 

856 ymax = np.max(pulsefish) 

857 dy = ws_fac*(ymax - ymin) 

858 ymin -= dy 

859 ymax += dy 

860 

861 # complete pulsefish: 

862 ax[1][0].set_title('Pulsefish') 

863 ax[1][0].set_ylim(ymin, ymax) 

864 ax[1][0].plot(time, pulsefish) 

865 

866 # pulsefish zoom in: 

867 ax[1][1].set_title('Pulsefish ZOOM IN') 

868 ax[1][1].set_ylim(ymin, ymax) 

869 ax[1][1].plot(time[:inset_indices], pulsefish[:inset_indices], '-o') 

870 

871 for row in ax: 

872 for c_ax in row: 

873 c_ax.set_xlabel('Time [sec]') 

874 c_ax.set_ylabel('Amplitude') 

875 

876 plt.tight_layout() 

877 

878 # chirps: 

879 chirps_freq = chirps(600.0, samplerate, duration) 

880 chirps_data = wavefish_eods('Alepto', chirps_freq, samplerate) 

881 

882 # rises: 

883 rises_freq = rises(600.0, samplerate, duration, rise_size=20.0) 

884 rises_data = wavefish_eods('Alepto', rises_freq, samplerate) 

885 

886 nfft = 256 

887 fig, ax = plt.subplots(nrows=2, ncols=1, figsize=(19, 10)) 

888 ax[0].set_title('Chirps') 

889 ax[0].specgram(chirps_data, Fs=samplerate, NFFT=nfft, noverlap=nfft//16) 

890 time = np.arange(len(chirps_freq))/samplerate 

891 ax[0].plot(time[:-nfft//2], chirps_freq[nfft//2:], '-k', lw=2) 

892 ax[0].set_ylim(0.0, 3000.0) 

893 ax[0].set_ylabel('Frequency [Hz]') 

894 

895 nfft = 4096 

896 ax[1].set_title('Rises') 

897 ax[1].specgram(rises_data, Fs=samplerate, NFFT=nfft, noverlap=nfft//2) 

898 time = np.arange(len(rises_freq))/samplerate 

899 ax[1].plot(time[:-nfft//4], rises_freq[nfft//4:], '-k', lw=2) 

900 ax[1].set_ylim(500.0, 700.0) 

901 ax[1].set_ylabel('Frequency [Hz]') 

902 ax[1].set_xlabel('Time [s]') 

903 plt.tight_layout() 

904 

905 plt.show() 

906 

907 

908def main(args=[]): 

909 from .version import __year__ 

910 

911 if len(args) > 0: 

912 if len(args) == 1 or args[0] != '-s': 

913 print('usage: fakefish [-h|--help] [-s audiofile]') 

914 print('') 

915 print('Without arguments, run a demo for illustrating fakefish functionality.') 

916 print('') 

917 print('-s audiofile: writes audiofile with user defined simulated electric fishes.') 

918 print('') 

919 print(f'by bendalab ({__year__})') 

920 else: 

921 generate_waveform(args[1]) 

922 else: 

923 demo() 

924 

925 

926if __name__ == '__main__': 

927 import sys 

928 main(sys.argv[1:])