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

530 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-09 14:25 +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# EODs of various wavefish species: 

92 

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

94 species='Sinewave') 

95 

96Apteronotus_leptorhynchus_harmonics = \ 

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

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

99 species='Apteronotus leptorhynchus') 

100 

101Apteronotus_rostratus_harmonics = \ 

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

103 0.0097678), 

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

105 -2.0513), 

106 species='Apteronotus rostratus') 

107 

108Eigenmannia_harmonics = \ 

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

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

111 species='Eigenmannia') 

112 

113Sternarchella_terminalis_harmonics = \ 

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

115 0.0057985), 

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

117 -1.9956), 

118 species='Sternarchella terminalis') 

119 

120Sternopygus_dariensis_harmonics = \ 

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

122 0.019018), 

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

124 1.6844), 

125 species='Sternopygus dariensis') 

126 

127wavefish_harmonics = dict(Sine=Sine_harmonics, 

128 Alepto=Apteronotus_leptorhynchus_harmonics, 

129 Arostratus=Apteronotus_rostratus_harmonics, 

130 Eigenmannia=Eigenmannia_harmonics, 

131 Sternarchella=Sternarchella_terminalis_harmonics, 

132 Sternopygus=Sternopygus_dariensis_harmonics) 

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

134 

135 

136def wavefish_spectrum(fish): 

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

138 

139 Parameters 

140 ---------- 

141 fish: string, dict or tuple of lists/arrays or 2-D array 

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

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

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

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

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

147 If 2-D array, as returned from waveanalysis.analyse_wave(), 

148 then take relative amplitudes from third column and phases 

149 from fifth colum. 

150 

151 Returns 

152 ------- 

153 amplitudes: array of floats 

154 Amplitudes of the fundamental and its harmonics. 

155 phases: array of floats 

156 Phases in radians of the fundamental and its harmonics. 

157 

158 Raises 

159 ------ 

160 KeyError: 

161 Unknown fish. 

162 IndexError: 

163 Amplitudes and phases differ in length. 

164 """ 

165 if isinstance(fish, np.ndarray) and fish.ndim == 2: 

166 amplitudes = fish[:, 3] 

167 phases = fish[:, 5] 

168 elif isinstance(fish, (tuple, list)): 

169 amplitudes = fish[0] 

170 phases = fish[1] 

171 elif isinstance(fish, dict): 

172 amplitudes = fish['amplitudes'] 

173 phases = fish['phases'] 

174 else: 

175 if not fish in wavefish_harmonics: 

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

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

178 amplitudes = wavefish_harmonics[fish]['amplitudes'] 

179 phases = wavefish_harmonics[fish]['phases'] 

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

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

182 # remove NaNs: 

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

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

185 amplitudes = amplitudes[:k+1] 

186 phases = phases[:k+1] 

187 break 

188 return amplitudes, phases 

189 

190 

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

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

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

194  

195 The waveform is constructed by superimposing sinewaves of integral 

196 multiples of the fundamental frequency - the fundamental and its 

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

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

199 fundamental and its harmonics are specified. 

200 

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

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

203 `noise_std` is added to the generated waveform. 

204 

205 Parameters 

206 ---------- 

207 fish: string, dict or tuple of lists/arrays or 2-D array 

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

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

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

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

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

213 If 2-D array, as returned from waveanalysis.analyse_wave(), 

214 then take relative amplitudes from third column and phases 

215 from fifth colum. 

216 frequency: float or array of floats 

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

218 time-dependent frequencies. 

219 rate: float 

220 Sampling rate in Hertz. 

221 duration: float 

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

223 phase0: float 

224 Phase offset of the EOD waveform in radians. 

225 noise_std: float 

226 Standard deviation of additive Gaussian white noise. 

227 

228 Returns 

229 ------- 

230 data: array of floats 

231 Generated data of a wave-type fish. 

232 

233 Raises 

234 ------ 

235 KeyError: 

236 Unknown fish. 

237 IndexError: 

238 Amplitudes and phases differ in length. 

239 """ 

240 # get relative amplitude and phases: 

241 amplitudes, phases = wavefish_spectrum(fish) 

242 # compute phase: 

243 if np.isscalar(frequency): 

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

245 phase *= frequency 

246 else: 

247 phase = np.cumsum(frequency)/rate 

248 # generate EOD: 

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

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

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

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

253 # add noise: 

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

255 return data 

256 

257 

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

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

260 

261 The amplitudes and phases of the Fourier components are adjusted 

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

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

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

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

266 

267 Parameters 

268 ---------- 

269 fish: string, dict or tuple of lists/arrays or 2-D array 

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

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

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

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

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

275 If 2-D array, as returned from waveanalysis.analyse_wave(), 

276 then take relative amplitudes from third column and phases 

277 from fifth colum. 

278 mode: 'peak' or 'zero' 

279 How to normalize amplitude and phases: 

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

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

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

283 

284 Returns 

285 ------- 

286 amplitudes: array of floats 

287 Adjusted amplitudes of the fundamental and its harmonics. 

288 phases: array of floats 

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

290 

291 """ 

292 # get relative amplitude and phases: 

293 amplitudes, phases = wavefish_spectrum(fish) 

294 if mode == 'zero': 

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

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

297 newphases %= 2.0*np.pi 

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

299 return newamplitudes, newphases 

300 else: 

301 # generate waveform: 

302 eodf = 100.0 

303 rate = 100000.0 

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

305 # normalize amplitudes: 

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

307 newamplitudes = np.array(amplitudes)/ampl 

308 # shift phases: 

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

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

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

312 newphases %= 2.0*np.pi 

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

314 # return: 

315 return newamplitudes, newphases 

316 

317 

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

319 """Serialize wavefish parameter to python code. 

320 

321 Add output to the wavefish_harmonics dictionary! 

322 

323 Parameters 

324 ---------- 

325 fish: string, dict or tuple of lists/arrays or 2-D array 

326 Specify relative amplitudes and phases of the fundamental and its harmonics and optional species name. 

327 If string then take amplitudes, phases, and species from the `wavefish_harmonics` dictionary. 

328 If dictionary then take amplitudes, phases, and species from the 'amlitudes', 'phases', and 'species' keys. 

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

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

331 If 2-D array, as returned from waveanalysis.analyse_wave(), 

332 then take relative amplitudes from third column and phases 

333 from fifth colum. 

334 name: str 

335 Name of the dictionary to be written. If empty take species name. 

336 species: str 

337 Name of the fish species. 

338 file: str or Path or file or None 

339 File path or open file object where to write wavefish dictionary. 

340 

341 Returns 

342 ------- 

343 fish: dict 

344 Dictionary with amplitudes, phases, and species. 

345 """ 

346 if fish is None or (isinstance(fish, dict) and len(fish) == 0): 

347 return {} 

348 # get relative amplitude and phases: 

349 amplitudes, phases = wavefish_spectrum(fish) 

350 harmonics = dict(amplitudes=amplitudes, 

351 phases=phases) 

352 # species: 

353 if not species: 

354 if isinstance(fish, dict): 

355 species = fish.get('species', '') 

356 elif isinstance(fish, str) and fish in wavefish_harmonics: 

357 species = wavefish_harmonics[fish]['species'] 

358 # name: 

359 if not name: 

360 name = species 

361 name = name.replace(' ', '_') 

362 # write out dictionary: 

363 if file is None: 

364 file = sys.stdout 

365 try: 

366 file.write('') 

367 closeit = False 

368 except AttributeError: 

369 file = open(file, 'w') 

370 closeit = True 

371 n = 6 

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

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

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

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

376 file.write(',\n') 

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

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

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

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

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

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

383 file.write(',\n') 

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

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

386 if species: 

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

388 file.write(' ' * 9 + f'species=\'{species}\')\n') 

389 harmonics['species'] = species 

390 else: 

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

392 if closeit: 

393 file.close() 

394 return harmonics 

395 

396 

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

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

399 """Simulate frequency trace with chirps. 

400 

401 A chirp is modeled as a Gaussian frequency modulation. 

402 The first chirp is placed at 0.5/chirp_freq. 

403 

404 Parameters 

405 ---------- 

406 eodf: float 

407 EOD frequency of the fish in Hertz. 

408 rate: float 

409 Sampling rate in Hertz. 

410 duration: float 

411 Duration of the generated data in seconds. 

412 chirp_freq: float 

413 Frequency of occurance of chirps in Hertz. 

414 chirp_size: float 

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

416 chirp_width: float 

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

418 chirp_kurtosis: float: 

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

420 chirp_contrast: float 

421 Maximum amplitude reduction of EOD during chirp. 

422 

423 Returns 

424 ------- 

425 frequency: array of floats 

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

427 amplitude: array of floats 

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

429 wavefish_eods(). 

430 """ 

431 # baseline eod frequency and amplitude modulation: 

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

433 frequency = eodf * np.ones(n) 

434 am = np.ones(n) 

435 # time points for chirps: 

436 chirp_period = 1.0/chirp_freq 

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

438 # chirp frequency waveform: 

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

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

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

442 # add chirps on baseline eodf: 

443 for ct in chirp_times: 

444 index = int(ct*rate) 

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

446 i1 = i0 + len(gauss) 

447 gi0 = 0 

448 gi1 = len(gauss) 

449 if i0 < 0: 

450 gi0 -= i0 

451 i0 = 0 

452 if i1 >= len(frequency): 

453 gi1 -= i1 - len(frequency) 

454 i1 = len(frequency) 

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

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

457 return frequency, am 

458 

459 

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

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

462 """Simulate frequency trace with rises. 

463 

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

465 

466 Parameters 

467 ---------- 

468 eodf: float 

469 EOD frequency of the fish in Hertz. 

470 rate: float 

471 Sampling rate in Hertz. 

472 duration: float 

473 Duration of the generated data in seconds. 

474 rise_freq: float 

475 Frequency of occurance of rises in Hertz. 

476 rise_size: float 

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

478 rise_tau: float 

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

480 decay_tau: float 

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

482 

483 Returns 

484 ------- 

485 data: array of floats 

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

487 """ 

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

489 # baseline eod frequency: 

490 frequency = eodf * np.ones(n) 

491 # time points for rises: 

492 rise_period = 1.0/rise_freq 

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

494 # rise frequency waveform: 

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

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

497 # add rises on baseline eodf: 

498 for r in rise_times: 

499 index = int(r*rate) 

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

501 rise_index = len(frequency)-index 

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

503 break 

504 else: 

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

506 return frequency 

507 

508 

509# EODs of various pulsefish species: 

510 

511Monophasic_phases = \ 

512 dict(times=(0,), 

513 amplitudes=(1,), 

514 stdevs=(0.0003,), 

515 species='Biphasic') 

516 

517Biphasic_phases = \ 

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

519 amplitudes=(1.1922, -0.95374), 

520 stdevs=(0.0003, 0.00025), 

521 species='Biphasic') 

522 

523Triphasic_phases = \ 

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

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

526 stdevs=(0.0001, 0.0001, 0.0002), 

527 species='Triphasic') 

528 

529pulsefish_eodphases = dict(Monophasic=Monophasic_phases, 

530 Biphasic=Biphasic_phases, 

531 Triphasic=Triphasic_phases) 

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

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

534""" 

535 

536 

537def pulsefish_phases(fish): 

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

539 

540 Parameters 

541 ---------- 

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

543 Specify positions, amplitudes, and standard deviations 

544 of Gaussians phases that are superimposed to simulate EOD waveforms 

545 of pulse-type electric fishes.  

546 If string then take positions, amplitudes and standard deviations  

547 from the `pulsefish_eodphases` dictionary. 

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

549 and 'stdevs' keys. 

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

551 the second is the list of corresponding amplitudes, and 

552 the third one the list of corresponding standard deviations. 

553 

554 Returns 

555 ------- 

556 times : array of floats 

557 Positions of the phases. 

558 amplitudes : array of floats 

559 Amplitudes of the phases. 

560 stdevs : array of floats 

561 Standard deviations of the phases. 

562 

563 Raises 

564 ------ 

565 KeyError: 

566 Unknown fish. 

567 IndexError: 

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

569 """ 

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

571 times = fish[0] 

572 amplitudes = fish[1] 

573 stdevs = fish[2] 

574 elif isinstance(fish, dict): 

575 times = fish['times'] 

576 amplitudes = fish['amplitudes'] 

577 stdevs = fish['stdevs'] 

578 else: 

579 if not fish in pulsefish_eodphases: 

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

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

582 phases = pulsefish_eodphases[fish] 

583 times = phases['times'] 

584 amplitudes = phases['amplitudes'] 

585 stdevs = phases['stdevs'] 

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

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

588 return times, amplitudes, stdevs 

589 

590 

591def pulsefish_spectrum(fish, freqs): 

592 """Analytically computed single-pulse spectrum. 

593 

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

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

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

597 position relative to the first phase. 

598 

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

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

601 ``` 

602 spec = pulsefish_spectrum('Biphasic', freqs) 

603 ampl = np.abs(spec) 

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

605 ``` 

606 

607 Parameters 

608 ---------- 

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

610 Specify positions, amplitudes, and standard deviations 

611 of Gaussians phases that are superimposed to simulate EOD waveforms 

612 of pulse-type electric fishes.  

613 If string then take positions, amplitudes and standard deviations  

614 from the `pulsefish_eodphases` dictionary. 

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

616 and 'stdevs' keys. 

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

618 the second is the list of corresponding amplitudes, and 

619 the third one the list of corresponding standard deviations. 

620 freqs: 1-D array of float 

621 Frequencies at which the spectrum is computed. 

622  

623 Returns 

624 ------- 

625 spectrum: 1-D array of complex 

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

627 The squared magnitude (the energy spectrum) has 

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

629 

630 """ 

631 times, ampls, stdevs = pulsefish_phases(fish) 

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

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

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

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

636 spec += gauss*shift 

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

638 return spec 

639 

640 

641def pulsefish_waveform(fish, t): 

642 """Compute a single pulsefish EOD. 

643 

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

645 by pulsefish_phases() to this function. 

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 t: array of float 

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

662  

663 Returns 

664 ------- 

665 pulse: array of float 

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

667 

668 """ 

669 times, ampls, stdevs = pulsefish_phases(fish) 

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

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

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

673 return pulse 

674 

675 

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

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

678 first_pulse=None): 

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

680 

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

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

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

684 

685 The generated waveform is duration seconds long and is sampled 

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

687 of noise_std is added to the generated pulse train. 

688 

689 Parameters 

690 ---------- 

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

692 Specify positions, amplitudes, and standard deviations 

693 of Gaussians phases that are superimposed to simulate EOD waveforms 

694 of pulse-type electric fishes.  

695 If string then take positions, amplitudes and standard deviations  

696 from the `pulsefish_eodphases` dictionary. 

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

698 and 'stdevs' keys. 

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

700 the second is the list of corresponding amplitudes, and 

701 the third one the list of corresponding standard deviations. 

702 frequency: float 

703 EOD frequency of the fish in Hz. 

704 rate: float 

705 Sampling Rate in Hz. 

706 duration: float 

707 Duration of the generated data in seconds. 

708 noise_std: float 

709 Standard deviation of additive Gaussian white noise. 

710 jitter_cv: float 

711 Gaussian distributed jitter of pulse times as coefficient of variation 

712 of inter-pulse intervals. 

713 first_pulse: float or None 

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

715 depending on pulse width, jitter, and frequency. 

716 

717 Returns 

718 ------- 

719 data: array of floats 

720 Generated data of a pulse-type fish. 

721 

722 Raises 

723 ------ 

724 KeyError: 

725 Unknown fish. 

726 IndexError: 

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

728 

729 """ 

730 # get phase properties: 

731 phase_times, phase_amplitudes, phase_stdevs = pulsefish_phases(fish) 

732 # time axis for single pulse: 

733 min_time_inx = np.argmin(phase_times) 

734 max_time_inx = np.argmax(phase_times) 

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

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

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

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

739 

740 # generate a single pulse: 

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

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

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

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

745 poffs = len(pulse)//2 

746 

747 # paste the pulse into the noise floor: 

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

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

750 period = 1.0/frequency 

751 jitter_std = period * jitter_cv 

752 if first_pulse is None: 

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

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

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

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

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

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

759 return data 

760 

761 

762def normalize_pulsefish(fish): 

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

764 

765 The positions and amplitudes of Gaussian phases are adjusted such 

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

767 and has the largest phase at time zero. 

768 

769 Parameters 

770 ---------- 

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

772 Specify positions, amplitudes, and standard deviations 

773 of Gaussians phases that are superimposed to simulate EOD waveforms 

774 of pulse-type electric fishes.  

775 If string then take positions, amplitudes and standard deviations  

776 from the `pulsefish_eodphases` dictionary. 

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

778 and 'stdevs' keys. 

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

780 the second is the list of corresponding amplitudes, and 

781 the third one the list of corresponding standard deviations. 

782 

783 Returns 

784 ------- 

785 fish: dict 

786 Dictionary with adjusted times and standard deviations. 

787 """ 

788 # get phase properties: 

789 phase_times, phase_amplitudes, phase_stdevs = pulsefish_phases(fish) 

790 # generate waveform: 

791 eodf = 10.0 

792 rate = 100000.0 

793 first_pulse = 0.5/eodf 

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

795 jitter_cv=0.0, first_pulse=first_pulse) 

796 # maximum peak: 

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

798 # normalize amplitudes: 

799 ampl = data[idx] 

800 newamplitudes = np.array(phase_amplitudes)/ampl 

801 # shift times: 

802 deltat = idx/rate - first_pulse 

803 newtimes = np.array(phase_times) - deltat 

804 # store and return: 

805 phases = dict(times=newtimes, 

806 amplitudes=newamplitudes, 

807 stdevs=phase_stdevs) 

808 return phases 

809 

810 

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

812 """Serialize pulsefish parameter to python code. 

813 

814 Add output to the pulsefish_eodphases dictionary! 

815 

816 Parameters 

817 ---------- 

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

819 Specify positions, amplitudes, and standard deviations 

820 of Gaussians phases that are superimposed to simulate EOD waveforms 

821 of pulse-type electric fishes.  

822 If string then take positions, amplitudes, standard deviations, and 

823 species from the `pulsefish_eodphases` dictionary. 

824 If dictionary then take pulse properties and species from the 'times', 

825 'amlitudes', 'stdevs' and 'species' keys. 

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

827 the second is the list of corresponding amplitudes, and 

828 the third one the list of corresponding standard deviations. 

829 name: string 

830 Name of the dictionary to be written. If empty take species name. 

831 species: str 

832 Name of the fish species. 

833 file: str or Path or file or None 

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

835 

836 Returns 

837 ------- 

838 fish: dict 

839 Dictionary with phase times, amplitudes, standard deviations and 

840 species. 

841 """ 

842 if fish is None or (isinstance(fish, dict) and len(fish) == 0): 

843 return {} 

844 # get phase properties: 

845 phase_times, phase_amplitudes, phase_stdevs = pulsefish_phases(fish) 

846 phases = dict(times=phase_times, 

847 amplitudes=phase_amplitudes, 

848 stdevs=phase_stdevs) 

849 # species: 

850 if not species: 

851 if isinstance(fish, dict): 

852 species = fish.get('species', '') 

853 elif isinstance(fish, str) and fish in pulsefish_eodphases: 

854 species = pulsefish_eodphases[fish]['species'] 

855 # name: 

856 if not name: 

857 name = species 

858 name = name.replace(' ', '_') 

859 # write out dictionary: 

860 if file is None: 

861 file = sys.stdout 

862 try: 

863 file.write('') 

864 closeit = False 

865 except AttributeError: 

866 file = open(file, 'w') 

867 closeit = True 

868 n = 6 

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

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

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

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

873 file.write(',\n') 

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

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

876 if len(phase_times) == 1: 

877 file.write(',') 

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

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

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

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

882 file.write(',\n') 

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

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

885 if len(phase_amplitudes) == 1: 

886 file.write(',') 

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

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

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

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

891 file.write(',\n') 

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

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

894 if len(phase_stdevs) == 1: 

895 file.write(',') 

896 if species: 

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

898 file.write(' ' * 9 + f'species=\'{species}\')\n') 

899 phases['species'] = species 

900 else: 

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

902 if closeit: 

903 file.close() 

904 return phases 

905 

906 

907def generate_waveform(filename): 

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

909 

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

911 

912 Parameters 

913 ---------- 

914 filename: string 

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

916 used to store the simulated EOD waveforms. 

917 """ 

918 import os 

919 from audioio import write_audio 

920 from thunderlab.consoleinput import read, select, save_inputs 

921 # generate file: 

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

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

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

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

926 fish_spread = 1 

927 if ndata > 1: 

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

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

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

931 eodt = 'a' 

932 eodf = 800.0 

933 eoda = 1.0 

934 eodsig = 'n' 

935 pulse_jitter = 0.1 

936 chirp_freq = 5.0 

937 chirp_size = 100.0 

938 chirp_width = 0.01 

939 chirp_kurtosis = 1.0 

940 rise_freq = 0.1 

941 rise_size = 10.0 

942 rise_tau = 1.0 

943 rise_decay_tau = 10.0 

944 for k in range(nfish): 

945 print('') 

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

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

948 ['Apteronotus', 'Eigenmannia', 

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

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

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

952 if eodt in 'ae': 

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

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

955 eodfreq = eodf 

956 if eodsig == 'c': 

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

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

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

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

961 chirp_freq, chirp_size, chirp_width, chirp_kurtosis) 

962 elif eodsig == 'r': 

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

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

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

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

967 eodfreq = rises(eodf, rate, duration, 

968 rise_freq, rise_size, rise_tau, rise_decay_tau) 

969 if eodt == 'a': 

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

971 phase0=0.0, noise_std=0.0) 

972 elif eodt == 'e': 

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

974 duration, phase0=0.0, noise_std=0.0) 

975 else: 

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

977 if eodt == '1': 

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

979 jitter_cv=pulse_jitter, noise_std=0.0) 

980 elif eodt == '2': 

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

982 jitter_cv=pulse_jitter, noise_std=0.0) 

983 elif eodt == '3': 

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

985 jitter_cv=pulse_jitter, noise_std=0.0) 

986 i = fish_indices[k] 

987 for j in range(fish_spread): 

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

989 

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

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

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

993 save_inputs(input_file) 

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

995 

996 

997def generate_testfiles(log_spec=True): 

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

999 

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

1001 analyis tools. 

1002 

1003 Three files are generated for each pulse waveform: 

1004 

1005 1. A wav file with the simulated recording. 

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

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

1008 

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

1010 single pulse spectra: 

1011 

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

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

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

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

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

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

1018 

1019 

1020 Parameters 

1021 ---------- 

1022 log_spec: bool 

1023 If True, plot decibel agains logarithmic frequency axis. 

1024 If False, plot energy agains linear frequency axis. 

1025 

1026 Returns 

1027 ------- 

1028 files: list of str 

1029 List of generated files. 

1030 """ 

1031 import matplotlib.pyplot as plt 

1032 from scipy.signal import find_peaks 

1033 from audioio import write_audio 

1034 from thunderlab.eventdetection import snippets 

1035 from .pulseanalysis import pulse_spectrum 

1036 

1037 np.seterr(all="ignore") 

1038 rng = np.random.default_rng() 

1039 rate = 50000.0 

1040 duration = 20.0 

1041 sigmat = 0.0002 

1042 monophasic = dict(name='monophasic', 

1043 times=(0,), 

1044 amplitudes=(1.0,), 

1045 stdevs=(sigmat,)) 

1046 biphasic30 = dict(name='biphasic30', 

1047 times=(0, 2*sigmat), 

1048 amplitudes=(1.0, -0.3), 

1049 stdevs=(sigmat, sigmat)) 

1050 biphasic60 = dict(name='biphasic60', 

1051 times=(0, 2*sigmat), 

1052 amplitudes=(1.0, -0.6), 

1053 stdevs=(sigmat, sigmat)) 

1054 biphasic100 = dict(name='biphasic100', 

1055 times=(0, 2*sigmat), 

1056 amplitudes=(1.0, -1.0), 

1057 stdevs=(sigmat, sigmat)) 

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

1059 files = [] 

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

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

1062 # fake recording: 

1063 eodf = rng.uniform(40.0, 120.0) 

1064 eoda = rng.uniform(0.8, 8.0) 

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

1066 jitter_cv=0.01, noise_std=0.002) 

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

1068 fac = 0.9/maxdata 

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

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

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

1072 files.append(pulse['name'] + '.wav') 

1073 # average EOD pulse: 

1074 tmax = 0.002 

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

1076 iw = int(tmax*rate) 

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

1078 eod_data = np.mean(snips, 0) 

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

1080 # analytic spectra: 

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

1082 spec = pulsefish_spectrum(pulse, freqs) 

1083 spec *= eoda 

1084 ampl = np.abs(spec) 

1085 energy = ampl**2 

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

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

1088 spec_data[:, 0] = freqs 

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

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

1091 spec_data[:, 3] = ampl 

1092 spec_data[:, 4] = energy 

1093 spec_data[:, 5] = level 

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

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

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

1097 files.append(pulse['name'] + '.csv') 

1098 # numerical spectrum: 

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

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

1101 # check normalization: 

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

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

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

1105 # plot waveform: 

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

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

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

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

1110 layout='constrained') 

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

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

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

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

1115 transform=ax1.transAxes) 

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

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

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

1119 ax1.set_ylabel('averaged EOD') 

1120 # plot spectrum: 

1121 ip = np.argmax(level) 

1122 fmax = freqs[ip] 

1123 fmaxpos = fmax if fmax > 1 else 1 

1124 if log_spec: 

1125 pmax = level[ip] 

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

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

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

1129 ax2.set_xlim(1, 1e4) 

1130 ax2.set_xscale('log') 

1131 ax2.set_ylim(-60, 10) 

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

1133 dc = level[0] 

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

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

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

1137 else: 

1138 pmax = energy[ip] 

1139 ax2.plot(nfreqs, nenergy, color='C1', label='numeric') 

1140 ax2.plot(freqs, energy, color='C3', label='analytic') 

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

1142 ax2.set_xlim(0, 3000) 

1143 ax2.set_ylim(bottom=0) 

1144 ax2.set_ylabel('energy') 

1145 ax2.text(fmaxpos + 200, pmax, f'{fmax:.0f}Hz', va='center') 

1146 ax2.legend(loc='upper right', frameon=False) 

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

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

1149 plt.show() 

1150 plt.close(fig) 

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

1152 files.append(pulse['name'] + '.pdf') 

1153 return files 

1154 

1155 

1156def demo(): 

1157 import matplotlib.pyplot as plt 

1158 rate = 40000.0 # in Hz 

1159 duration = 10.0 # in sec 

1160 

1161 inset_len = 0.01 # in sec 

1162 inset_indices = int(inset_len*rate) 

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

1164 

1165 # generate data: 

1166 eodf = 400.0 

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

1168 eodf = 650.0 

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

1170 

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

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

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

1174 

1175 fig, ax = plt.subplots(2, 2, figsize=(19, 10), layout='constrained') 

1176 

1177 # get proper wavefish ylim 

1178 ymin = np.min(wavefish) 

1179 ymax = np.max(wavefish) 

1180 dy = ws_fac*(ymax - ymin) 

1181 ymin -= dy 

1182 ymax += dy 

1183 

1184 # complete wavefish: 

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

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

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

1188 

1189 # wavefish zoom in: 

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

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

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

1193 

1194 # get proper pulsefish ylim 

1195 ymin = np.min(pulsefish) 

1196 ymax = np.max(pulsefish) 

1197 dy = ws_fac*(ymax - ymin) 

1198 ymin -= dy 

1199 ymax += dy 

1200 

1201 # complete pulsefish: 

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

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

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

1205 

1206 # pulsefish zoom in: 

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

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

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

1210 

1211 for row in ax: 

1212 for c_ax in row: 

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

1214 c_ax.set_ylabel('Amplitude') 

1215 

1216 # chirps: 

1217 chirps_freq = chirps(600.0, rate, duration) 

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

1219 

1220 # rises: 

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

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

1223 

1224 nfft = 256 

1225 fig, ax = plt.subplots(2, 1, figsize=(19, 10), layout='constrained') 

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

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

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

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

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

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

1232 

1233 nfft = 4096 

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

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

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

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

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

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

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

1241 

1242 plt.show() 

1243 

1244 

1245def main(args=[]): 

1246 from .version import __year__ 

1247 

1248 if len(args) > 0: 

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

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

1251 print('') 

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

1253 print('') 

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

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

1256 print('-T: write audiofiles for a number of pulse waveforms and corresponding analytic spectra in csv files for testing. Plot frequency axis linearly.') 

1257 print('') 

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

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

1260 generate_waveform(args[1]) 

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

1262 generate_testfiles(True) 

1263 elif args[0] == '-T': 

1264 generate_testfiles(False) 

1265 else: 

1266 demo() 

1267 

1268 

1269if __name__ == '__main__': 

1270 import sys 

1271 main(sys.argv[1:])