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

483 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-15 17:50 +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## Muscial intervals 

11 

12- `musical_intervals`: names and frequency ratios of musical intervals 

13 

14 

15## Wavefish 

16 

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

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

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

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

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

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

23 

24 

25## Pulsefish 

26 

27- `pulsefish_phases()`: position, amplitudes and standard deviations of EOD phases in pulsefish waveforms. 

28- `pulsefish_spectrum()`: analytically computed single-pulse spectrum. 

29- `pulsefish_waveform()`: compute a single pulsefish EOD. 

30- `pulsefish_eods()`: simulate EODs of a pulse-type fish. 

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

32- `export_pulsefish()`: serialize pulsefish parameter to python code. 

33 

34 

35## Waveform generation 

36 

37- `generate_testfiles()`: generate recordings of various pulse EODs and their spectrum. 

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

39""" 

40 

41import sys 

42import numpy as np 

43 

44 

45species_name = dict(Sine='Sinewave', 

46 Alepto='Apteronotus leptorhynchus', 

47 Arostratus='Apteronotus rostratus', 

48 Eigenmannia='Eigenmannia spec.', 

49 Sternarchella='Sternarchella terminalis', 

50 Sternopygus='Sternopygus dariensis') 

51"""Translate species ids used by wavefish_harmonics and pulsefish_eodphases to full species names. 

52""" 

53 

54 

55def abbrv_genus(name): 

56 """Abbreviate genus in a species name. 

57 

58 Parameters 

59 ---------- 

60 name: string 

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

62 

63 Returns 

64 ------- 

65 name: string 

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

67 """ 

68 ns = name.split() 

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

70 

71 

72musical_intervals = { 

73 'unison': (1/1, 1, 1, 0), 

74 'minor second': (16/15, 16, 15, 1), 

75 'major second': (9/8, 9, 8, 2), 

76 'minor third': (6/5, 6, 5, 3), 

77 'major third': (5/4, 5, 4, 4), 

78 'forth': (4/3, 4, 3, 5), 

79 'tritone': (45/32, 45, 32, 6), # =1.406, half way between forth and fifth: 17/6/2=1.4167, sqrt(2)=1.4142 

80 'fifth': (3/2, 3, 2, 7), 

81 'minor sixth': (8/5, 8, 5, 8), 

82 'major sixth': (5/3, 5, 3, 9), 

83 'subminor seventh': (7/4, 7, 4, 9.5), 

84 'minor seventh': (9/5, 9, 5, 10), 

85 'major seventh': (15/8, 15, 8, 11), 

86 'octave': (2/1, 2, 1, 12), 

87} 

88"""Name, frequency ratio, nominator, denominator, and index of musical intervals 

89""" 

90 

91# Amplitudes and phases of various wavefish species: 

92 

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

94 

95Apteronotus_leptorhynchus_harmonics = \ 

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

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

98 

99Apteronotus_rostratus_harmonics = \ 

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

101 0.0097678), 

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

103 -2.0513)) 

104 

105Eigenmannia_harmonics = \ 

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

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

108 

109Sternarchella_terminalis_harmonics = \ 

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

111 0.0057985), 

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

113 -1.9956)) 

114 

115Sternopygus_dariensis_harmonics = \ 

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

117 0.019018), 

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

119 1.6844)) 

120 

121wavefish_harmonics = dict(Sine=Sine_harmonics, 

122 Alepto=Apteronotus_leptorhynchus_harmonics, 

123 Arostratus=Apteronotus_rostratus_harmonics, 

124 Eigenmannia=Eigenmannia_harmonics, 

125 Sternarchella=Sternarchella_terminalis_harmonics, 

126 Sternopygus=Sternopygus_dariensis_harmonics) 

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

128 

129 

130def wavefish_spectrum(fish): 

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

132 

133 Parameters 

134 ---------- 

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

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

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

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

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

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

141 

142 Returns 

143 ------- 

144 amplitudes: array of floats 

145 Amplitudes of the fundamental and its harmonics. 

146 phases: array of floats 

147 Phases in radians of the fundamental and its harmonics. 

148 

149 Raises 

150 ------ 

151 KeyError: 

152 Unknown fish. 

153 IndexError: 

154 Amplitudes and phases differ in length. 

155 """ 

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

157 amplitudes = fish[0] 

158 phases = fish[1] 

159 elif isinstance(fish, dict): 

160 amplitudes = fish['amplitudes'] 

161 phases = fish['phases'] 

162 else: 

163 if not fish in wavefish_harmonics: 

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

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

166 amplitudes = wavefish_harmonics[fish]['amplitudes'] 

167 phases = wavefish_harmonics[fish]['phases'] 

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

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

170 # remove NaNs: 

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

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

173 amplitudes = amplitudes[:k+1] 

174 phases = phases[:k+1] 

175 break 

176 return amplitudes, phases 

177 

178 

179def wavefish_eods(fish='Eigenmannia', frequency=100.0, rate=44100.0, 

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

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

182  

183 The waveform is constructed by superimposing sinewaves of integral 

184 multiples of the fundamental frequency - the fundamental and its 

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

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

187 fundamental and its harmonics are specified. 

188 

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

190 `rate` Hertz. Gaussian white noise with a standard deviation of 

191 `noise_std` is added to the generated waveform. 

192 

193 Parameters 

194 ---------- 

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

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

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

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

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

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

201 frequency: float or array of floats 

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

203 time-dependent frequencies. 

204 rate: float 

205 Sampling rate in Hertz. 

206 duration: float 

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

208 phase0: float 

209 Phase offset of the EOD waveform in radians. 

210 noise_std: float 

211 Standard deviation of additive Gaussian white noise. 

212 

213 Returns 

214 ------- 

215 data: array of floats 

216 Generated data of a wave-type fish. 

217 

218 Raises 

219 ------ 

220 KeyError: 

221 Unknown fish. 

222 IndexError: 

223 Amplitudes and phases differ in length. 

224 """ 

225 # get relative amplitude and phases: 

226 amplitudes, phases = wavefish_spectrum(fish) 

227 # compute phase: 

228 if np.isscalar(frequency): 

229 phase = np.arange(0, duration, 1.0/rate) 

230 phase *= frequency 

231 else: 

232 phase = np.cumsum(frequency)/rate 

233 # generate EOD: 

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

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

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

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

238 # add noise: 

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

240 return data 

241 

242 

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

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

245 

246 The amplitudes and phases of the Fourier components are adjusted 

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

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

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

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

251 

252 Parameters 

253 ---------- 

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

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

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

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

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

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

260 mode: 'peak' or 'zero' 

261 How to normalize amplitude and phases: 

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

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

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

265 

266 Returns 

267 ------- 

268 amplitudes: array of floats 

269 Adjusted amplitudes of the fundamental and its harmonics. 

270 phases: array of floats 

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

272 

273 """ 

274 # get relative amplitude and phases: 

275 amplitudes, phases = wavefish_spectrum(fish) 

276 if mode == 'zero': 

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

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

279 newphases %= 2.0*np.pi 

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

281 return newamplitudes, newphases 

282 else: 

283 # generate waveform: 

284 eodf = 100.0 

285 rate = 100000.0 

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

287 # normalize amplitudes: 

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

289 newamplitudes = np.array(amplitudes)/ampl 

290 # shift phases: 

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

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

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

294 newphases %= 2.0*np.pi 

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

296 # return: 

297 return newamplitudes, newphases 

298 

299 

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

301 """Serialize wavefish parameter to python code. 

302 

303 Add output to the wavefish_harmonics dictionary! 

304 

305 Parameters 

306 ---------- 

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

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

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

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

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

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

313 name: string 

314 Name of the dictionary to be written. 

315 file: string or file or None 

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

317 

318 Returns 

319 ------- 

320 fish: dict 

321 Dictionary with amplitudes and phases. 

322 """ 

323 # get relative amplitude and phases: 

324 amplitudes, phases = wavefish_spectrum(fish) 

325 # write out dictionary: 

326 if file is None: 

327 file = sys.stdout 

328 try: 

329 file.write('') 

330 closeit = False 

331 except AttributeError: 

332 file = open(file, 'w') 

333 closeit = True 

334 n = 6 

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

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

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

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

339 file.write(',\n') 

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

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

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

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

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

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

346 file.write(',\n') 

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

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

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

350 if closeit: 

351 file.close() 

352 # return dictionary: 

353 harmonics = dict(amplitudes=amplitudes, 

354 phases=phases) 

355 return harmonics 

356 

357 

358def chirps(eodf=100.0, rate=44100.0, duration=1.0, chirp_freq=5.0, 

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

360 """Simulate frequency trace with chirps. 

361 

362 A chirp is modeled as a Gaussian frequency modulation. 

363 The first chirp is placed at 0.5/chirp_freq. 

364 

365 Parameters 

366 ---------- 

367 eodf: float 

368 EOD frequency of the fish in Hertz. 

369 rate: float 

370 Sampling rate in Hertz. 

371 duration: float 

372 Duration of the generated data in seconds. 

373 chirp_freq: float 

374 Frequency of occurance of chirps in Hertz. 

375 chirp_size: float 

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

377 chirp_width: float 

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

379 chirp_kurtosis: float: 

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

381 chirp_contrast: float 

382 Maximum amplitude reduction of EOD during chirp. 

383 

384 Returns 

385 ------- 

386 frequency: array of floats 

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

388 amplitude: array of floats 

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

390 wavefish_eods(). 

391 """ 

392 # baseline eod frequency and amplitude modulation: 

393 n = len(np.arange(0, duration, 1.0/rate)) 

394 frequency = eodf * np.ones(n) 

395 am = np.ones(n) 

396 # time points for chirps: 

397 chirp_period = 1.0/chirp_freq 

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

399 # chirp frequency waveform: 

400 chirp_t = np.arange(-2.0*chirp_width, 2.0*chirp_width, 1./rate) 

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

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

403 # add chirps on baseline eodf: 

404 for ct in chirp_times: 

405 index = int(ct*rate) 

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

407 i1 = i0 + len(gauss) 

408 gi0 = 0 

409 gi1 = len(gauss) 

410 if i0 < 0: 

411 gi0 -= i0 

412 i0 = 0 

413 if i1 >= len(frequency): 

414 gi1 -= i1 - len(frequency) 

415 i1 = len(frequency) 

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

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

418 return frequency, am 

419 

420 

421def rises(eodf=100.0, rate=44100.0, duration=1.0, rise_freq=0.1, 

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

423 """Simulate frequency trace with rises. 

424 

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

426 

427 Parameters 

428 ---------- 

429 eodf: float 

430 EOD frequency of the fish in Hertz. 

431 rate: float 

432 Sampling rate in Hertz. 

433 duration: float 

434 Duration of the generated data in seconds. 

435 rise_freq: float 

436 Frequency of occurance of rises in Hertz. 

437 rise_size: float 

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

439 rise_tau: float 

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

441 decay_tau: float 

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

443 

444 Returns 

445 ------- 

446 data: array of floats 

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

448 """ 

449 n = len(np.arange(0, duration, 1.0/rate)) 

450 # baseline eod frequency: 

451 frequency = eodf * np.ones(n) 

452 # time points for rises: 

453 rise_period = 1.0/rise_freq 

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

455 # rise frequency waveform: 

456 rise_t = np.arange(0.0, 5.0*decay_tau, 1./rate) 

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

458 # add rises on baseline eodf: 

459 for r in rise_times: 

460 index = int(r*rate) 

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

462 rise_index = len(frequency)-index 

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

464 break 

465 else: 

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

467 return frequency 

468 

469 

470# Positions, amplitudes and standard deviations of EOD phases of various pulsefish species: 

471 

472Monophasic_phases = \ 

473 dict(times=(0,), 

474 amplitudes=(1,), 

475 stdevs=(0.0003,)) 

476 

477Biphasic_phases = \ 

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

479 amplitudes=(1.1922, -0.95374), 

480 stdevs=(0.0003, 0.00025)) 

481 

482Triphasic_phases = \ 

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

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

485 stdevs=(0.0001, 0.0001, 0.0002)) 

486 

487pulsefish_eodphases = dict(Monophasic=Monophasic_phases, 

488 Biphasic=Biphasic_phases, 

489 Triphasic=Triphasic_phases) 

490"""Positions, amplitudes, and standard deviations of Gaussians that 

491 make up the EOD phases of pulse-type electric fish. 

492""" 

493 

494 

495def pulsefish_phases(fish): 

496 """Position, amplitudes and standard deviations of EOD phases in pulsefish waveforms. 

497 

498 Parameters 

499 ---------- 

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

501 Specify positions, amplitudes, and standard deviations 

502 of Gaussians phases that are superimposed to simulate EOD waveforms 

503 of pulse-type electric fishes.  

504 If string then take positions, amplitudes and standard deviations  

505 from the `pulsefish_eodphases` dictionary. 

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

507 and 'stdevs' keys. 

508 If tuple then the first element is the list of phase positions, 

509 the second is the list of corresponding amplitudes, and 

510 the third one the list of corresponding standard deviations. 

511 

512 Returns 

513 ------- 

514 times : array of floats 

515 Positions of the phases. 

516 amplitudes : array of floats 

517 Amplitudes of the phases. 

518 stdevs : array of floats 

519 Standard deviations of the phases. 

520 

521 Raises 

522 ------ 

523 KeyError: 

524 Unknown fish. 

525 IndexError: 

526 Phase positions, amplitudes, or standard deviations differ in length. 

527 """ 

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

529 times = fish[0] 

530 amplitudes = fish[1] 

531 stdevs = fish[2] 

532 elif isinstance(fish, dict): 

533 times = fish['times'] 

534 amplitudes = fish['amplitudes'] 

535 stdevs = fish['stdevs'] 

536 else: 

537 if not fish in pulsefish_eodphases: 

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

539 ', '.join(pulsefish_eodphases.keys())) 

540 phases = pulsefish_eodphases[fish] 

541 times = phases['times'] 

542 amplitudes = phases['amplitudes'] 

543 stdevs = phases['stdevs'] 

544 if len(stdevs) != len(amplitudes) or len(stdevs) != len(times): 

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

546 return times, amplitudes, stdevs 

547 

548 

549def pulsefish_spectrum(fish, freqs): 

550 """Analytically computed single-pulse spectrum. 

551 

552 The spectrum is the sum of the spectra of all of the phases. Each 

553 phase is the spectrum of a Gaussian (which again is a Gaussian 

554 centered at f=0) multiplied with a shift term to account for its 

555 position relative to the first phase. 

556 

557 You get the amplitude spectral density by taking the absolute value. 

558 The energy spectral density is the amplitude spectral density squared: 

559 ``` 

560 spec = pulsefish_spectrum('Biphasic', freqs) 

561 ampl = np.abs(spec) 

562 energy = np.abs(spec)**2 

563 ``` 

564 

565 Parameters 

566 ---------- 

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

568 Specify positions, amplitudes, and standard deviations 

569 of Gaussians phases that are superimposed to simulate EOD waveforms 

570 of pulse-type electric fishes.  

571 If string then take positions, amplitudes and standard deviations  

572 from the `pulsefish_eodphases` dictionary. 

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

574 and 'stdevs' keys. 

575 If tuple then the first element is the list of phase positions, 

576 the second is the list of corresponding amplitudes, and 

577 the third one the list of corresponding standard deviations. 

578 freqs: 1-D array of float 

579 Frequencies at which the spectrum is computed. 

580  

581 Returns 

582 ------- 

583 spectrum: 1-D array of complex 

584 The one-sided complex-valued spectrum of the single pulse EOD. 

585 The squared magnitude (the energy spectrum) has 

586 the unit (x s)^2 = x^2 s/Hz. 

587 

588 """ 

589 times, ampls, stdevs = pulsefish_phases(fish) 

590 spec = np.zeros(len(freqs), dtype=complex) 

591 for dt, a, s in zip(times, ampls, stdevs): 

592 gauss = a*np.sqrt(2*np.pi)*s*np.exp(-0.5*(2*np.pi*s*freqs)**2) 

593 shift = np.exp(-2j*np.pi*freqs*dt) 

594 spec += gauss*shift 

595 spec *= np.sqrt(2) # because of one-sided spectrum 

596 return spec 

597 

598 

599def pulsefish_waveform(fish, t): 

600 """Compute a single pulsefish EOD. 

601 

602 You may use pulsefish_parameter() to pass the paramters returned 

603 by pulsefish_phases() to this function. 

604  

605 Parameters 

606 ---------- 

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

608 Specify positions, amplitudes, and standard deviations 

609 of Gaussians phases that are superimposed to simulate EOD waveforms 

610 of pulse-type electric fishes.  

611 If string then take positions, amplitudes and standard deviations  

612 from the `pulsefish_eodphases` dictionary. 

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

614 and 'stdevs' keys. 

615 If tuple then the first element is the list of phase positions, 

616 the second is the list of corresponding amplitudes, and 

617 the third one the list of corresponding standard deviations. 

618 t: array of float 

619 The time array over which the pulse waveform is evaluated. 

620  

621 Returns 

622 ------- 

623 pulse: array of float 

624 The pulse waveform for the times given in `t`. 

625 

626 """ 

627 times, ampls, stdevs = pulsefish_phases(fish) 

628 pulse = np.zeros(len(t)) 

629 for dt, a, s in zip(times, ampls, stdevs): 

630 pulse += a*np.exp(-0.5*((t - dt)/s)**2) 

631 return pulse 

632 

633 

634def pulsefish_eods(fish='Biphasic', frequency=100.0, rate=44100.0, 

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

636 first_pulse=None): 

637 """Simulate EODs of a pulse-type fish. 

638 

639 Pulses are spaced by 1/frequency, jittered as determined by 

640 jitter_cv. Each pulse is a combination of Gaussian phases, whose 

641 positions, amplitudes and widths are given by `fish`. 

642 

643 The generated waveform is duration seconds long and is sampled 

644 with rate Hertz. Gaussian white noise with a standard deviation 

645 of noise_std is added to the generated pulse train. 

646 

647 Parameters 

648 ---------- 

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

650 Specify positions, amplitudes, and standard deviations 

651 of Gaussians phases that are superimposed to simulate EOD waveforms 

652 of pulse-type electric fishes.  

653 If string then take positions, amplitudes and standard deviations  

654 from the `pulsefish_eodphases` dictionary. 

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

656 and 'stdevs' keys. 

657 If tuple then the first element is the list of phase positions, 

658 the second is the list of corresponding amplitudes, and 

659 the third one the list of corresponding standard deviations. 

660 frequency: float 

661 EOD frequency of the fish in Hz. 

662 rate: float 

663 Sampling Rate in Hz. 

664 duration: float 

665 Duration of the generated data in seconds. 

666 noise_std: float 

667 Standard deviation of additive Gaussian white noise. 

668 jitter_cv: float 

669 Gaussian distributed jitter of pulse times as coefficient of variation 

670 of inter-pulse intervals. 

671 first_pulse: float or None 

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

673 depending on pulse width, jitter, and frequency. 

674 

675 Returns 

676 ------- 

677 data: array of floats 

678 Generated data of a pulse-type fish. 

679 

680 Raises 

681 ------ 

682 KeyError: 

683 Unknown fish. 

684 IndexError: 

685 Phase positions, amplitudes, or standard deviations differ in length. 

686 

687 """ 

688 # get phase properties: 

689 phase_times, phase_amplitudes, phase_stdevs = pulsefish_phases(fish) 

690 # time axis for single pulse: 

691 min_time_inx = np.argmin(phase_times) 

692 max_time_inx = np.argmax(phase_times) 

693 tmax = max(np.abs(phase_times[min_time_inx]-4.0*phase_stdevs[min_time_inx]), 

694 np.abs(phase_times[max_time_inx]+4.0*phase_stdevs[max_time_inx])) 

695 x = np.arange(-tmax, tmax, 1.0/rate) 

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

697 

698 # generate a single pulse: 

699 pulse = pulsefish_waveform((phase_times, phase_amplitudes, phase_stdevs), x) 

700 #pulse = np.zeros(len(x)) 

701 #for time, ampl, std in zip(phase_times, phase_amplitudes, phase_stdevs): 

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

703 poffs = len(pulse)//2 

704 

705 # paste the pulse into the noise floor: 

706 time = np.arange(0, duration, 1.0/rate) 

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

708 period = 1.0/frequency 

709 jitter_std = period * jitter_cv 

710 if first_pulse is None: 

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

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

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

714 pulse_indices = np.round(pulse_times * rate).astype(int) 

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

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

717 return data 

718 

719 

720def normalize_pulsefish(fish): 

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

722 

723 The positions and amplitudes of Gaussian phases are adjusted such 

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

725 and has the largest phase at time zero. 

726 

727 Parameters 

728 ---------- 

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

730 Specify positions, amplitudes, and standard deviations 

731 of Gaussians phases that are superimposed to simulate EOD waveforms 

732 of pulse-type electric fishes.  

733 If string then take positions, amplitudes and standard deviations  

734 from the `pulsefish_eodphases` dictionary. 

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

736 and 'stdevs' keys. 

737 If tuple then the first element is the list of phase positions, 

738 the second is the list of corresponding amplitudes, and 

739 the third one the list of corresponding standard deviations. 

740 

741 Returns 

742 ------- 

743 fish: dict 

744 Dictionary with adjusted times and standard deviations. 

745 """ 

746 # get phase properties: 

747 phase_times, phase_amplitudes, phase_stdevs = pulsefish_phases(fish) 

748 # generate waveform: 

749 eodf = 10.0 

750 rate = 100000.0 

751 first_pulse = 0.5/eodf 

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

753 jitter_cv=0.0, first_pulse=first_pulse) 

754 # maximum peak: 

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

756 # normalize amplitudes: 

757 ampl = data[idx] 

758 newamplitudes = np.array(phase_amplitudes)/ampl 

759 # shift times: 

760 deltat = idx/rate - first_pulse 

761 newtimes = np.array(phase_times) - deltat 

762 # store and return: 

763 phases = dict(times=newtimes, 

764 amplitudes=newamplitudes, 

765 stdevs=phase_stdevs) 

766 return phases 

767 

768 

769def export_pulsefish(fish, name='Unknown_phases', file=None): 

770 """Serialize pulsefish parameter to python code. 

771 

772 Add output to the pulsefish_eodphases dictionary! 

773 

774 Parameters 

775 ---------- 

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

777 Specify positions, amplitudes, and standard deviations 

778 of Gaussians phases that are superimposed to simulate EOD waveforms 

779 of pulse-type electric fishes.  

780 If string then take positions, amplitudes and standard deviations  

781 from the `pulsefish_eodphases` dictionary. 

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

783 and 'stdevs' keys. 

784 If tuple then the first element is the list of phase positions, 

785 the second is the list of corresponding amplitudes, and 

786 the third one the list of corresponding standard deviations. 

787 name: string 

788 Name of the dictionary to be written. 

789 file: string or file or None 

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

791 

792 Returns 

793 ------- 

794 fish: dict 

795 Dictionary with phase times, amplitudes and standard deviations. 

796 """ 

797 # get phase properties: 

798 phase_times, phase_amplitudes, phase_stdevs = pulsefish_phases(fish) 

799 # write out dictionary: 

800 if file is None: 

801 file = sys.stdout 

802 try: 

803 file.write('') 

804 closeit = False 

805 except AttributeError: 

806 file = open(file, 'w') 

807 closeit = True 

808 n = 6 

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

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

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

812 for k in range(n, len(phase_times), n): 

813 file.write(',\n') 

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

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

816 if len(phase_times) == 1: 

817 file.write(',') 

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

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

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

821 for k in range(n, len(phase_amplitudes), n): 

822 file.write(',\n') 

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

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

825 if len(phase_amplitudes) == 1: 

826 file.write(',') 

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

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

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

830 for k in range(n, len(phase_stdevs), n): 

831 file.write(',\n') 

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

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

834 if len(phase_stdevs) == 1: 

835 file.write(',') 

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

837 if closeit: 

838 file.close() 

839 # return dictionary: 

840 phases = dict(times=phase_times, 

841 amplitudes=phase_amplitudes, 

842 stdevs=phase_stdevs) 

843 return phases 

844 

845 

846def generate_waveform(filename): 

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

848 

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

850 

851 Parameters 

852 ---------- 

853 filename: string 

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

855 used to store the simulated EOD waveforms. 

856 """ 

857 import os 

858 from audioio import write_audio 

859 from thunderlab.consoleinput import read, select, save_inputs 

860 # generate file: 

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

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

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

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

865 fish_spread = 1 

866 if ndata > 1: 

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

868 data = np.random.randn(int(duration*rate), ndata)*0.01 

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

870 eodt = 'a' 

871 eodf = 800.0 

872 eoda = 1.0 

873 eodsig = 'n' 

874 pulse_jitter = 0.1 

875 chirp_freq = 5.0 

876 chirp_size = 100.0 

877 chirp_width = 0.01 

878 chirp_kurtosis = 1.0 

879 rise_freq = 0.1 

880 rise_size = 10.0 

881 rise_tau = 1.0 

882 rise_decay_tau = 10.0 

883 for k in range(nfish): 

884 print('') 

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

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

887 ['Apteronotus', 'Eigenmannia', 

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

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

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

891 if eodt in 'ae': 

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

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

894 eodfreq = eodf 

895 if eodsig == 'c': 

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

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

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

899 eodfreq, _ = chirps(eodf, rate, duration, 

900 chirp_freq, chirp_size, chirp_width, chirp_kurtosis) 

901 elif eodsig == 'r': 

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

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

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

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

906 eodfreq = rises(eodf, rate, duration, 

907 rise_freq, rise_size, rise_tau, rise_decay_tau) 

908 if eodt == 'a': 

909 fishdata = eoda*wavefish_eods('Alepto', eodfreq, rate, duration, 

910 phase0=0.0, noise_std=0.0) 

911 elif eodt == 'e': 

912 fishdata = eoda*wavefish_eods('Eigenmannia', eodfreq, rate, 

913 duration, phase0=0.0, noise_std=0.0) 

914 else: 

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

916 if eodt == '1': 

917 fishdata = eoda*pulsefish_eods('Monophasic', eodf, rate, duration, 

918 jitter_cv=pulse_jitter, noise_std=0.0) 

919 elif eodt == '2': 

920 fishdata = eoda*pulsefish_eods('Biphasic', eodf, rate, duration, 

921 jitter_cv=pulse_jitter, noise_std=0.0) 

922 elif eodt == '3': 

923 fishdata = eoda*pulsefish_eods('Triphasic', eodf, rate, duration, 

924 jitter_cv=pulse_jitter, noise_std=0.0) 

925 i = fish_indices[k] 

926 for j in range(fish_spread): 

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

928 

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

930 write_audio(filename, 0.9*data/maxdata, rate) 

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

932 save_inputs(input_file) 

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

934 

935 

936def generate_testfiles(): 

937 """Generate recordings of various pulse EODs and their spectrum. 

938 

939 The spectrum is analytically computed and thus can be used to test 

940 analyis tools. 

941 

942 Three files are generated for each pulse waveform: 

943 

944 1. A wav file with the simulated recording. 

945 2. A csv file with the spectra (see below for details). 

946 3. A pdf file with a plot showing the averaged EOD pulse and the spectrum. 

947 

948 The csv file contains six columns separated by semicolons of the 

949 single pulse spectra: 

950 

951 1. `f`: the frequency components in Hertz 

952 2- `real`: real part of the Fourier spectrum 

953 3. `imag`: imaginary part of the Fourier spectrum 

954 4. `ampl`: amplitude spectrum, i.e. magnitude of Fourier spectrum 

955 5. `energy`: energy spectrum, i.e. squared amplitude 

956 6. `level`: energy sepctrum in decibel relative to maximum energy 

957 

958 """ 

959 import matplotlib.pyplot as plt 

960 from scipy.signal import find_peaks 

961 from audioio import write_audio 

962 from thunderlab.eventdetection import snippets 

963 from .eodanalysis import pulse_spectrum 

964 

965 np.seterr(all="ignore") 

966 rng = np.random.default_rng() 

967 rate = 50000.0 

968 duration = 20.0 

969 sigmat = 0.0002 

970 monophasic = dict(name='monophasic', 

971 times=(0,), 

972 amplitudes=(1.0,), 

973 stdevs=(sigmat,)) 

974 biphasic30 = dict(name='biphasic30', 

975 times=(0, 2*sigmat), 

976 amplitudes=(1.0, -0.3), 

977 stdevs=(sigmat, sigmat)) 

978 biphasic60 = dict(name='biphasic60', 

979 times=(0, 2*sigmat), 

980 amplitudes=(1.0, -0.6), 

981 stdevs=(sigmat, sigmat)) 

982 biphasic100 = dict(name='biphasic100', 

983 times=(0, 2*sigmat), 

984 amplitudes=(1.0, -1.0), 

985 stdevs=(sigmat, sigmat)) 

986 triphasic = dict(name='triphasic', **Triphasic_phases) 

987 for pulse in [monophasic, biphasic30, biphasic60, biphasic100, triphasic]: 

988 print(pulse['name'], '...') 

989 # fake recording: 

990 eodf = rng.uniform(40.0, 120.0) 

991 eoda = rng.uniform(0.8, 8.0) 

992 data = eoda*pulsefish_eods(pulse, eodf, rate, duration, 

993 jitter_cv=0.01, noise_std=0.002) 

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

995 fac = 0.9/maxdata 

996 metadata = dict(gain=f'{1/fac:.3f}mV') 

997 write_audio(pulse['name'] + '.wav', fac*data, rate, metadata) 

998 print(f' wrote {pulse['name']}.wav') 

999 # average EOD pulse: 

1000 tmax = 0.002 

1001 idxs, _ = find_peaks(data, prominence=0.9*eoda) 

1002 iw = int(tmax*rate) 

1003 snips = snippets(data, idxs, start=-iw, stop=iw) 

1004 eod_data = np.mean(snips, 0) 

1005 eod_time = (np.arange(len(eod_data)) - iw)/rate 

1006 # analytic spectra: 

1007 freqs = np.arange(0, rate/2, 1.0) 

1008 spec = pulsefish_spectrum(pulse, freqs) 

1009 spec *= eoda 

1010 ampl = np.abs(spec) 

1011 energy = ampl**2 

1012 level = 10*np.log10(energy/np.max(energy)) 

1013 spec_data = np.zeros((len(freqs), 6)) 

1014 spec_data[:, 0] = freqs 

1015 spec_data[:, 1] = np.real(spec) 

1016 spec_data[:, 2] = np.imag(spec) 

1017 spec_data[:, 3] = ampl 

1018 spec_data[:, 4] = energy 

1019 spec_data[:, 5] = level 

1020 np.savetxt(pulse['name'] + '.csv', spec_data, fmt='%g', delimiter=';', 

1021 header='f/Hz;real;imag;ampl;energy;level/dB') 

1022 print(f' wrote {pulse['name']}.csv') 

1023 # numerical spectrum: 

1024 nfreqs, nenergy = pulse_spectrum(eod_data, rate, 1.0, 0.05) 

1025 nlevel = 10*np.log10(nenergy/np.max(energy)) 

1026 # check normalization: 

1027 print(f' integral over analytic energy spectrum: {np.sum(energy)*freqs[1]:9.3e}') 

1028 print(f' integral over numeric energy spectrum : {np.sum(nenergy)*nfreqs[1]:9.3e}') 

1029 print(f' integral over squared signal : {np.sum(eod_data**2)/rate:9.3e}') 

1030 # plot waveform: 

1031 pa = np.sum(eod_data[eod_data >= 0])/rate 

1032 na = np.sum(-eod_data[eod_data <= 0])/rate 

1033 balance = (pa - na)/(pa + na) 

1034 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(6, 3), 

1035 layout='constrained') 

1036 fig.suptitle(pulse['name']) 

1037 ax1.axhline(0, color='gray') 

1038 ax1.plot(1000*eod_time, eod_data, color='C0', label='data') 

1039 ax1.text(0.1, 0.05, f'polarity balance = {100*balance:.0f}%', 

1040 transform=ax1.transAxes) 

1041 ax1.set_xlim(-1000*tmax, 1000*tmax) 

1042 ax1.set_ylim(-1.1*eoda, 1.1*eoda) 

1043 ax1.set_xlabel('time [ms]') 

1044 ax1.set_ylabel('averaged EOD') 

1045 # plot spectrum: 

1046 ip = np.argmax(level) 

1047 fmax = freqs[ip] 

1048 pmax = level[ip] 

1049 fmaxpos = fmax if fmax > 1 else 1 

1050 ax2.plot(nfreqs, nlevel + 0.5, color='C1', label='numeric') 

1051 ax2.plot(freqs, level, color='C3', label='analytic') 

1052 ax2.plot(fmaxpos, pmax, 'o', color='C3') 

1053 ax2.set_xlim(1, 1e4) 

1054 ax2.set_ylim(-60, 10) 

1055 ax2.set_xscale('log') 

1056 ax2.set_xlabel('frequency [Hz]') 

1057 ax2.set_ylabel('energy [dB]') 

1058 ax2.legend(loc='lower left', frameon=False) 

1059 dc = level[0] 

1060 ax2.text(2, dc - 4, f'{dc:.0f}dB') 

1061 ax2.text(fmaxpos*1.05, pmax + 1, f'{fmax:.0f}Hz') 

1062 fig.savefig(pulse['name'] + '.pdf') 

1063 plt.show() 

1064 plt.close(fig) 

1065 print(f' wrote {pulse['name']}.pdf') 

1066 

1067 

1068def demo(): 

1069 import matplotlib.pyplot as plt 

1070 rate = 40000.0 # in Hz 

1071 duration = 10.0 # in sec 

1072 

1073 inset_len = 0.01 # in sec 

1074 inset_indices = int(inset_len*rate) 

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

1076 

1077 # generate data: 

1078 eodf = 400.0 

1079 wavefish = wavefish_eods('Alepto', eodf, rate, duration, noise_std=0.02) 

1080 eodf = 650.0 

1081 wavefish += 0.5*wavefish_eods('Eigenmannia', eodf, rate, duration) 

1082 

1083 pulsefish = pulsefish_eods('Biphasic', 80.0, rate, duration, 

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

1085 time = np.arange(len(wavefish))/rate 

1086 

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

1088 

1089 # get proper wavefish ylim 

1090 ymin = np.min(wavefish) 

1091 ymax = np.max(wavefish) 

1092 dy = ws_fac*(ymax - ymin) 

1093 ymin -= dy 

1094 ymax += dy 

1095 

1096 # complete wavefish: 

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

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

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

1100 

1101 # wavefish zoom in: 

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

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

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

1105 

1106 # get proper pulsefish ylim 

1107 ymin = np.min(pulsefish) 

1108 ymax = np.max(pulsefish) 

1109 dy = ws_fac*(ymax - ymin) 

1110 ymin -= dy 

1111 ymax += dy 

1112 

1113 # complete pulsefish: 

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

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

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

1117 

1118 # pulsefish zoom in: 

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

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

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

1122 

1123 for row in ax: 

1124 for c_ax in row: 

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

1126 c_ax.set_ylabel('Amplitude') 

1127 

1128 plt.tight_layout() 

1129 

1130 # chirps: 

1131 chirps_freq = chirps(600.0, rate, duration) 

1132 chirps_data = wavefish_eods('Alepto', chirps_freq, rate) 

1133 

1134 # rises: 

1135 rises_freq = rises(600.0, rate, duration, rise_size=20.0) 

1136 rises_data = wavefish_eods('Alepto', rises_freq, rate) 

1137 

1138 nfft = 256 

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

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

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

1142 time = np.arange(len(chirps_freq))/rate 

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

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

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

1146 

1147 nfft = 4096 

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

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

1150 time = np.arange(len(rises_freq))/rate 

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

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

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

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

1155 plt.tight_layout() 

1156 

1157 plt.show() 

1158 

1159 

1160def main(args=[]): 

1161 from .version import __year__ 

1162 

1163 if len(args) > 0: 

1164 if (len(args) == 1 and args[0] != '-t') and args[0] != '-s': 

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

1166 print('') 

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

1168 print('') 

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

1170 print('-t: write audiofiles for a number of pulse waveforms and corresponding analytic spectra in csv files for testing.') 

1171 print('') 

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

1173 elif args[0] == '-s': 

1174 generate_waveform(args[1]) 

1175 elif args[0] == '-t': 

1176 generate_testfiles() 

1177 else: 

1178 demo() 

1179 

1180 

1181if __name__ == '__main__': 

1182 import sys 

1183 main(sys.argv[1:])