Coverage for src/thunderfish/harmonics.py: 84%

919 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-19 20:05 +0000

1""" 

2Extract and analyze harmonic frequencies from power spectra. 

3 

4## Harmonic group extraction 

5 

6- `harmonic_groups()`: detect peaks in power spectrum and group them 

7 according to their harmonic structure. 

8- `expand_group()`: add more harmonics to harmonic group.  

9- `extract_fundamentals()`: collect harmonic groups from 

10 lists of power spectrum peaks. 

11- `threshold_estimate()`: estimates thresholds for peak detection 

12 in a power spectrum. 

13  

14## Helper functions for harmonic group extraction 

15 

16- `build_harmonic_group()`: find all the harmonics belonging to the largest peak in the power spectrum. 

17- `retrieve_harmonic_group()`: find all the harmonics belonging to a given fundamental. 

18- `group_candidate()`: candidate harmonic frequencies belonging to a fundamental frequency. 

19- `update_group()`: update frequency lists and harmonic group. 

20 

21## Handling of lists of harmonic groups 

22 

23- `fundamental_freqs()`: extract fundamental frequencies from 

24 lists of harmonic groups. 

25- `fundamental_freqs_and_power()`: extract fundamental frequencies and their 

26 power in decibel from lists of harmonic groups. 

27 

28## Handling of lists of fundamental frequencies 

29 

30- `add_relative_power()`: add a column with relative power. 

31- `add_power_ranks()`: add a column with power ranks. 

32- `similar_indices()`: indices of similar frequencies. 

33- `unique_indices()`: for each unique fundamental frequency a list of indices indicating where it is found.  

34- `collapse()`: collapse harmonic groups with similar fundamental frequencies to a single one. 

35- `consistent()`: harmonic groups whose fundamental frequencies consistently appear everywhere.  

36- `closest()`: harmonic groups whose fundamental frequencies are closest to each other.  

37- `unique_mask()`: mark similar frequencies from different recordings as dublicate. 

38- `unique()`: remove similar frequencies from different recordings. 

39 

40## Visualization 

41 

42- `colors_markers()`: Generate a list of colors and markers for plotting. 

43- `plot_harmonic_groups()`: Mark decibel power of fundamentals and their 

44 harmonics. 

45- `plot_psd_harmonic_groups()`: Plot decibel power-spectrum with detected peaks, 

46 harmonic groups, and mains frequencies. 

47- `annotate_harmonic_group()`: annotate peaks of a harmonic group in a spectrum. 

48- `plot_unique_frequencies()`: plot all fundamental frequencies and mark unique frequencies. 

49- `plot_selected_groups()`: plot all fundamental frequencies and mark selected frequencies with a bar. 

50 

51## Configuration 

52 

53- `add_psd_peak_detection_config()`: add parameters for the detection of 

54 peaks in power spectra to configuration. 

55- `psd_peak_detection_args()`: retrieve parameters for the detection of peaks 

56 in power spectra from configuration. 

57- `add_harmonic_groups_config()`: add parameters for the detection of 

58 harmonic groups to configuration. 

59- `harmonic_groups_args()`: retrieve parameters for the detection of 

60 harmonic groups from configuration. 

61""" 

62 

63import math as m 

64import numpy as np 

65import scipy.signal as sig 

66try: 

67 import matplotlib.pyplot as plt 

68 import matplotlib.cm as cm 

69 import matplotlib.colors as mc 

70except ImportError: 

71 pass 

72 

73from thunderlab.eventdetection import detect_peaks, trim, hist_threshold 

74from thunderlab.powerspectrum import decibel, power, plot_decibel_psd 

75 

76 

77def group_candidate(good_freqs, all_freqs, freq, divisor, 

78 freq_tol, max_freq_tol, min_group_size, verbose): 

79 """Candidate harmonic frequencies belonging to a fundamental frequency. 

80 

81 Parameters 

82 ---------- 

83 good_freqs: 2-D array 

84 Frequency, power, and use count (columns) of strong peaks detected 

85 in a power spectrum. 

86 all_freqs: 2-D array 

87 Frequency, power, and use count (columns) of all peaks detected 

88 in a power spectrum. 

89 freq: float 

90 Fundamental frequency for which a harmonic group should be assembled. 

91 divisor: int 

92 Fundamental frequency was obtained from a frequency divided by divisor. 

93 0: Fundamental frequency is given as is. 

94 freq_tol: float 

95 Harmonics should fall within this frequency tolerance. 

96 This should be in the range of the frequency resolution 

97 and should not be smaller than half of the frequency resolution. 

98 max_freq_tol: float 

99 Maximum deviation of harmonics from their expected frequency. 

100 Peaks with frequencies between `freq_tol` and `max_freq_tol` 

101 get penalized the further away from their expected frequency. 

102 min_group_size: int 

103 Minimum number of harmonics of a harmonic group. 

104 The harmonics from min_group_size/3 to max(min_group_size, divisor) 

105 need to be in good_freqs. 

106 verbose: int 

107 Verbosity level. 

108  

109 Returns 

110 ------- 

111 new_group: 1-D aray of indices 

112 Frequencies in all_freqs belonging to the candidate group. 

113 fzero: float 

114 Adjusted fundamental frequency. If negative, no group was found. 

115 fzero_harmonics: int 

116 The highest harmonics that was used to recompute the fundamental frequency. 

117 """ 

118 if verbose > 0: 

119 print('') 

120 

121 dvs = True 

122 if divisor <= 0: 

123 divisor = 1 

124 dvs = False 

125 

126 # 1. find harmonics in good_freqs and adjust fzero accordingly: 

127 fzero = freq 

128 fzero_harmonics = 1 

129 if len(good_freqs[:,0]) > 0: 

130 """ 

131 ff = good_freqs[:,0] 

132 h = np.round(ff/fzero) 

133 h_indices = np.unique(h, return_index=True)[1] 

134 f_best = [hi+np.argmin(np.abs(fh/h - fzero)) for hi, fh in zip(h_indices, np.split(ff, h_indices))] 

135 fe = np.abs(ff/h - fzero) 

136 h = h[fe < freq_tol] 

137 fe = fe[fe < freq_tol] 

138 """ 

139 prev_freq = divisor * freq 

140 for h in range(divisor+1, 2*min_group_size+1): 

141 idx = np.argmin(np.abs(good_freqs[:,0]/h - fzero)) 

142 ff = good_freqs[idx,0] 

143 if m.fabs(ff/h - fzero) > freq_tol: 

144 continue 

145 df = ff - prev_freq 

146 dh = np.round(df/fzero) 

147 fe = m.fabs(df/dh - fzero) 

148 if fe > 2.0*freq_tol: 

149 if h > min_group_size: 

150 break 

151 continue 

152 # update fzero: 

153 prev_freq = ff 

154 fzero_harmonics = h 

155 fzero = ff/fzero_harmonics 

156 if verbose > 1: 

157 print('adjusted fzero to %.2fHz from %d-th harmonic' % (fzero, h)) 

158 

159 if verbose > 0: 

160 ds = 'divisor: %d, ' % divisor if dvs else '' 

161 print('# %sfzero=%7.2fHz adjusted from harmonics %d' 

162 % (ds, fzero, fzero_harmonics)) 

163 

164 # 2. check fzero: 

165 # freq might not be in our group anymore, because fzero was adjusted: 

166 if np.abs(freq - fzero) > freq_tol: 

167 if verbose > 0: 

168 print(' discarded: lost frequency') 

169 return [], -1.0, fzero_harmonics 

170 

171 # 3. collect harmonics from all_freqs: 

172 new_group = -np.ones(min_group_size, dtype=int) 

173 new_penalties = np.ones(min_group_size) 

174 freqs = [] 

175 prev_h = 0 

176 prev_fe = 0.0 

177 for h in range(1, min_group_size+1): 

178 penalty = 0 

179 i = np.argmin(np.abs(all_freqs[:,0]/h - fzero)) 

180 f = all_freqs[i,0] 

181 if verbose > 2: 

182 print(' check %7.2fHz as %d. harmonics' % (f, h)) 

183 fac = 1.0 if h >= divisor else 2.0 

184 fe = m.fabs(f/h - fzero) 

185 if fe > fac*max_freq_tol: 

186 if verbose > 1 and fe < 2*fac*max_freq_tol: 

187 print(' %d. harmonics at %7.2fHz is off by %7.2fHz (max between %5.2fHz and %5.2fHz) from %7.2fHz' 

188 % (h, f, h*fe, h*fac*freq_tol, h*fac*max_freq_tol, h*fzero)) 

189 continue 

190 if fe > fac*freq_tol: 

191 penalty = np.interp(fe, [fac*freq_tol, fac*max_freq_tol], [0.0, 1.0]) 

192 if len(freqs) > 0: 

193 pf = freqs[-1] 

194 df = f - pf 

195 if df < 0.5*fzero: 

196 if len(freqs)>1: 

197 pf = freqs[-2] 

198 df = f - pf 

199 else: 

200 pf = 0.0 

201 df = h*fzero 

202 dh = m.floor(df/fzero + 0.5) 

203 fe = m.fabs(df/dh - fzero) 

204 if fe > 2*dh*fac*max_freq_tol: 

205 if verbose > 1: 

206 print(' %d. harmonics at %7.2fHz is off by %7.2fHz (max between %5.2fHz and %5.2fHz) from previous harmonics at %7.2fHz' 

207 % (h, f, dh*fe, 2*fac*dh*freq_tol, 2*fac*dh*max_freq_tol, pf)) 

208 continue 

209 if fe > 2*dh*fac*freq_tol: 

210 penalty = np.interp(fe, [2*dh*fac*freq_tol, 2*dh*fac*max_freq_tol], [0.0, 1.0]) 

211 else: 

212 fe = 0.0 

213 if h > prev_h or fe < prev_fe: 

214 if prev_h > 0 and h - prev_h > 1: 

215 if verbose > 1: 

216 print(' previous harmonics %d more than 1 away from %d. harmonics at %7.2fHz' 

217 % (prev_h, h, f)) 

218 break 

219 if h == prev_h and len(freqs) > 0: 

220 freqs.pop() 

221 freqs.append(f) 

222 new_group[int(h)-1] = i 

223 new_penalties[int(h)-1] = penalty 

224 prev_h = h 

225 prev_fe = fe 

226 if verbose > 1: 

227 print(' %d. harmonics at %7.2fHz has been taken (peak %2d) with penalty %3.1f' % (h, f, i, penalty)) 

228 

229 # 4. check new group: 

230 

231 # almost all harmonics in min_group_size required: 

232 max_penalties = min_group_size/3 

233 if np.sum(new_penalties) > max_penalties: 

234 if verbose > 0: 

235 print(' discarded group because sum of penalties %3.1f is more than %3.1f: indices' % 

236 (np.sum(new_penalties), max_penalties), new_group, ' penalties', new_penalties) 

237 return [], -1.0, fzero_harmonics 

238 new_group = new_group[new_group>=0] 

239 

240 # check use count of frequencies: 

241 double_use = np.sum(all_freqs[new_group, 2]>0) 

242 if double_use >= 2: # XXX make this a parameter? 

243 if verbose > 0: 

244 print(' discarded group because of use count = %2d >= 2' % double_use) 

245 return [], -1.0, fzero_harmonics 

246 

247 # 5. return results: 

248 return new_group, fzero, fzero_harmonics 

249 

250 

251def update_group(good_freqs, all_freqs, new_group, fzero, 

252 freq_tol, verbose, group_str): 

253 """Update good frequencies and harmonic group. 

254 

255 Remove frequencies from good_freqs, add missing fundamental to group. 

256 

257 Parameters 

258 ---------- 

259 good_freqs: 2-D array 

260 Frequency, power, and use count (columns) of strong peaks detected 

261 in a power spectrum. 

262 all_freqs: 2-D array 

263 Frequency, power, and use count (columns) of all peaks detected 

264 in a power spectrum. 

265 new_group: 1-D aray of indices 

266 Frequencies in all_freqs of an harmonic group. 

267 fzero: float 

268 Fundamental frequency for which frequencies are collected in good_freqs. 

269 freq_tol: float 

270 Harmonics need to fall within this frequency tolerance. 

271 This should be in the range of the frequency resolution 

272 and not be smaller than half of the frequency resolution. 

273 verbose: int 

274 Verbosity level. 

275 group_str: string 

276 String for debug message. 

277  

278 Returns 

279 ------- 

280 good_freqs: 2-D array 

281 Frequency, power, and use count (columns) of strong peaks detected 

282 in a power spectrum with frequencies for harmonic group 

283 of fundamental frequency fzero removed. 

284 group: 2-D array 

285 Frequency, power, and use count (columns) of harmonic group 

286 for fundamental frequency fzero. 

287 """ 

288 # initialize group: 

289 group = all_freqs[new_group,:] 

290 

291 # indices of group in good_freqs: 

292 freq_tol *= 1.1 

293 indices = [] 

294 for f in group[:,0]: 

295 idx = np.argmin(np.abs(good_freqs[:,0]-f)) 

296 if np.abs(good_freqs[idx,0]-f) <= freq_tol: 

297 indices.append(idx) 

298 indices = np.asarray(indices, dtype=int) 

299 

300 # harmonics in good_freqs: 

301 nharm = np.round(good_freqs[:,0]/fzero) 

302 idxs = np.where(np.abs(good_freqs[:,0] - nharm*fzero) <= freq_tol)[0] 

303 indices = np.unique(np.concatenate((indices, idxs))) 

304 

305 # report: 

306 if verbose > 1: 

307 print('# good freqs: ', indices, 

308 '[', ', '.join(['%.2f' % f for f in good_freqs[indices,0]]), ']') 

309 print('# all freqs : ', new_group, 

310 '[', ', '.join(['%.2f' % f for f in all_freqs[new_group,0]]), ']') 

311 if verbose > 0: 

312 refi = np.argmax(group[:,1] > 0.0) 

313 print('') 

314 print(group_str) 

315 for i in range(len(group)): 

316 print('f=%8.2fHz n=%5.2f: power=%9.3g power/pmax=%6.4f=%5.1fdB' 

317 % (group[i,0], group[i,0]/fzero, 

318 group[i,1], group[i,1]/group[refi,1], decibel(group[i,1], group[refi,1]))) 

319 print('') 

320 

321 # erase group from good_freqs: 

322 good_freqs = np.delete(good_freqs, indices, axis=0) 

323 

324 # adjust frequencies to fzero: 

325 group[:,0] = np.round(group[:,0]/fzero)*fzero 

326 

327 # insert missing fzero: 

328 if np.round(group[0,0]/fzero) != 1.0: 

329 group = np.vstack(((fzero, group[0,1], -2.0), group)) 

330 

331 return good_freqs, group 

332 

333 

334def build_harmonic_group(good_freqs, all_freqs, freq_tol, max_freq_tol, 

335 verbose=0, min_group_size=3, max_divisor=4): 

336 """Find all the harmonics belonging to the largest peak in a list of frequency peaks. 

337 

338 Parameters 

339 ---------- 

340 good_freqs: 2-D array 

341 Frequency, power, and use count (columns) of strong peaks detected 

342 in a power spectrum. 

343 all_freqs: 2-D array 

344 Frequency, power, and use count (columns) of all peaks detected 

345 in a power spectrum. 

346 freq_tol: float 

347 Harmonics should fall within this frequency tolerance. 

348 This should be in the range of the frequency resolution 

349 and should not be smaller than half of the frequency resolution. 

350 max_freq_tol: float 

351 Maximum deviation of harmonics from their expected frequency. 

352 Peaks with frequencies between `freq_tol` and `max_freq_tol` 

353 get penalized the further away from their expected frequency. 

354 verbose: int 

355 Verbosity level. 

356 min_group_size: int 

357 Minimum number of harmonics of a harmonic group. 

358 The harmonics from min_group_size/3 to max(min_group_size, divisor) 

359 need to be in good_freqs. 

360 max_divisor: int 

361 Maximum divisor used for checking for sub-harmonics. 

362  

363 Returns 

364 ------- 

365 good_freqs: 2-D array 

366 Frequency, power, and use count (columns) of strong peaks detected 

367 in a power spectrum with frequencies of the returned harmonic group removed. 

368 group: 2-D array 

369 The detected harmonic group. Might be empty. 

370 indices: 1-D array of indices 

371 Indices of the harmonic group in all_freqs. 

372 best_fzero_harmonics: int 

373 The highest harmonics that was used to recompute 

374 the fundamental frequency. 

375 fmax: float 

376 The frequency of the largest peak in good_freqs 

377 for which the harmonic group was detected. 

378 """ 

379 # select strongest frequency for building the harmonic group: 

380 fmaxinx = np.argmax(good_freqs[:,1]) 

381 fmax = good_freqs[fmaxinx,0] 

382 if verbose > 0: 

383 print('') 

384 print('%s build_harmonic_group %s' % (10*'#', 38*'#')) 

385 print('%s fmax=%7.2fHz, power=%9.3g %s' 

386 % (10*'#', fmax, good_freqs[fmaxinx,1], 27*'#')) 

387 print('good_freqs: ', '[', 

388 ', '.join(['%.2f' % f for f in good_freqs[:,0]]), ']') 

389 

390 # container for harmonic groups: 

391 best_group = [] 

392 best_value = -1e6 

393 best_divisor = 0 

394 best_fzero = 0.0 

395 best_fzero_harmonics = 0 

396 

397 # check for integer fractions of the frequency: 

398 for divisor in range(1, max_divisor + 1): 

399 # 1. hypothesized fundamental: 

400 freq = fmax / divisor 

401 

402 # 2. find harmonics in good_freqs and adjust fzero accordingly: 

403 group_size = min_group_size if divisor <= min_group_size else divisor 

404 new_group, fzero, fzero_harmonics = group_candidate(good_freqs, all_freqs, 

405 freq, divisor, 

406 freq_tol, max_freq_tol, 

407 group_size, verbose) 

408 # no group found: 

409 if fzero < 0.0: 

410 continue 

411 

412 # 3. compare new group to best group: 

413 peaksum = decibel(np.sum(all_freqs[new_group, 1])*min_group_size/len(new_group)) 

414 diff = np.std(np.diff(decibel(all_freqs[new_group, 1]))) 

415 new_group_value = peaksum - diff 

416 counts = int(np.sum(all_freqs[new_group, 2])) 

417 if verbose > 0: 

418 print(f' new group: fzero={fzero:7.2f}Hz, nharmonics={len(new_group)}, value={new_group_value:6.1f}dB, peaksum={peaksum:5.1f}dB, diff={diff:6.1f}dB, use count={counts:2d}, peaks:', new_group) 

419 if verbose > 1 and len(best_group) > 0: 

420 print(f' best group: divisor={best_divisor}, fzero={best_fzero:7.2f}Hz, nharmonics={len(best_group)}, value={best_value:6.1f}dB, peaks:', best_group) 

421 # select new group if sum of peak power minus diff is larger: 

422 if (len(new_group) >= min_group_size and len(best_group) < min_group_size) or \ 

423 (len(new_group) >= len(best_group) and new_group_value >= best_value): 

424 best_value = new_group_value 

425 best_group = new_group 

426 best_divisor = divisor 

427 best_fzero = fzero 

428 best_fzero_harmonics = fzero_harmonics 

429 if verbose > 1: 

430 print(f' new best group: divisor={best_divisor}, fzero={best_fzero:7.2f}Hz, nharmonics={len(best_group)}, value={best_value:6.1f}dB, peaks:', best_group) 

431 elif verbose > 0: 

432 print(' took as new best group') 

433 elif verbose > 0: 

434 if not (len(new_group) >= min_group_size and len(best_group) < min_group_size): 

435 s = f', number of harmonics {len(new_group)} not greater than minimum group size {min_group_size}' 

436 if len(new_group) < len(best_group): 

437 print(f' keep best group: number of harmonics {len(new_group)} is SMALLER than in best group {len(best_group)}{s}') 

438 elif new_group_value < best_value: 

439 print(f' keep best group: value {new_group_value:6.1f}dB is SMALLER than in best group {best_value:6.1f}dB{s}') 

440 else: 

441 print(f' keep best group: {s[2:]}') 

442 

443 # no group found: 

444 if len(best_group) == 0: 

445 # erase freq: 

446 good_freqs = np.delete(good_freqs, fmaxinx, axis=0) 

447 group = np.zeros((0, 3)) 

448 return good_freqs, group, best_group, 1, fmax 

449 

450 # update frequencies and group: 

451 if verbose > 1: 

452 print('') 

453 print('# best group found for fmax=%.2fHz, fzero=%.2fHz, divisor=%d:' 

454 % (fmax, best_fzero, best_divisor)) 

455 group_str = '%s resulting harmonic group for fmax=%.2fHz' % (10*'#', fmax) 

456 good_freqs, group = update_group(good_freqs, all_freqs, best_group, best_fzero, freq_tol, verbose, group_str) 

457 

458 # good_freqs: removed all frequencies of bestgroup 

459 return good_freqs, group, best_group, best_fzero_harmonics, fmax 

460 

461 

462def retrieve_harmonic_group(freq, good_freqs, all_freqs, 

463 freq_tol, max_freq_tol, verbose=0, 

464 min_group_size=3): 

465 """Find all the harmonics belonging to a given fundamental. 

466 

467 Parameters 

468 ---------- 

469 freq: float 

470 Fundamental frequency for which harmonics are to be retrieved. 

471 good_freqs: 2-D array 

472 Frequency, power, and use count (columns) of strong peaks detected 

473 in a power spectrum. All harmonics of `freq` will be 

474 removed from `good_freqs`. 

475 all_freqs: 2-D array 

476 Frequency, power, and use count (columns) of all peaks detected 

477 in a power spectrum. 

478 freq_tol: float 

479 Harmonics should fall within this frequency tolerance. 

480 This should be in the range of the frequency resolution 

481 and should not be smaller than half of the frequency resolution. 

482 max_freq_tol: float 

483 Maximum deviation of harmonics from their expected frequency. 

484 Peaks with frequencies between `freq_tol` and `max_freq_tol` 

485 get penalized the further away from their expected frequency. 

486 verbose: int 

487 Verbosity level. 

488 min_group_size: int 

489 Minimum number of harmonics of a harmonic group. 

490 The harmonics from min_group_size/3 to max(min_group_size, divisor) 

491 need to be in good_freqs. 

492  

493 Returns 

494 ------- 

495 good_freqs: 2-D array 

496 Frequency, power, and use count (columns) of strong peaks detected 

497 in a power spectrum with frequencies of the returned harmonic group removed. 

498 group: 2-D array 

499 The detected harmonic group. Might be empty. 

500 indices: 1-D array of indices 

501 Indices of the harmonic group in all_freqs. 

502 fzero_harmonics: int 

503 The highest harmonics that was used to recompute 

504 the fundamental frequency. 

505 """ 

506 if verbose > 0: 

507 print('') 

508 print('%s retrieve harmonic group %s' % (10*'#', 35*'#')) 

509 print('%s freq=%7.2fHz %s' % (10*'#', freq, 44*'#')) 

510 print('good_freqs: ', '[', 

511 ', '.join(['%.2f' % f for f in good_freqs[:,0]]), ']') 

512 

513 # find harmonics in good_freqs and adjust fzero accordingly: 

514 new_group, fzero, fzero_harmonics = group_candidate(good_freqs, all_freqs, 

515 freq, 0, 

516 freq_tol, max_freq_tol, 

517 min_group_size, verbose) 

518 

519 # no group found: 

520 if fzero < 0.0: 

521 return good_freqs, np.zeros((0, 2)), np.zeros(0), fzero_harmonics 

522 

523 if verbose > 1: 

524 print('') 

525 print('# group found for freq=%.2fHz, fzero=%.2fHz:' 

526 % (freq, fzero)) 

527 group_str = '#### resulting harmonic group for freq=%.2fHz' % freq 

528 good_freqs, group = update_group(good_freqs, all_freqs, new_group, 

529 fzero, freq_tol, verbose, group_str) 

530 

531 # good_freqs: removed all frequencies of bestgroup 

532 return good_freqs, group, new_group, fzero_harmonics 

533 

534 

535def expand_group(group, indices, freqs, freq_tol, max_harmonics=0): 

536 """Add more harmonics to harmonic group. 

537  

538 Parameters 

539 ---------- 

540 group: 2-D array 

541 Group of fundamental frequency and harmonics 

542 as returned by build_harmonic_group. 

543 indices: 1-D array of indices 

544 Indices of the harmonics in group in `freqs`. 

545 freqs: 2-D array 

546 Frequency, power, and use count (columns) of all peaks detected 

547 in a power spectrum. 

548 freq_tol: float 

549 Harmonics need to fall within this frequency tolerance. 

550 This should be in the range of the frequency resolution 

551 and not be smaller than half of the frequency resolution. 

552 max_harmonics: int 

553 Maximum number of harmonics to be returned for each group. 

554 

555 Returns 

556 ------- 

557 group: 2-D array 

558 Expanded group of fundamental frequency and harmonics. 

559 indices: 1-D array of indices 

560 Indices of the harmonics in the expanded group in all_freqs. 

561 """ 

562 if len(group) == 0: 

563 return group, indices 

564 fzero = group[0,0] 

565 if max_harmonics <= 0: 

566 max_harmonics = m.floor(freqs[-1,0]/fzero + 0.5) # round 

567 if max_harmonics <= len(group): 

568 return group, indices 

569 group_freqs = list(group[:,0]) 

570 indices = list(indices) 

571 last_h = m.floor(group_freqs[-1]/fzero + 0.5) # round 

572 for h in range(last_h+1, max_harmonics+1): 

573 i = np.argmin(np.abs(freqs[:,0]/h - fzero)) 

574 f = freqs[i,0] 

575 if m.fabs(f/h - fzero) > freq_tol: 

576 continue 

577 df = f - group_freqs[-1] 

578 dh = m.floor(df/fzero + 0.5) # round 

579 if dh < 1: 

580 continue 

581 fe = m.fabs(df/dh - fzero) 

582 if fe > 2*freq_tol: 

583 continue 

584 group_freqs.append(f) 

585 indices.append(i) 

586 # assemble group: 

587 new_group = freqs[indices,:group.shape[1]] 

588 # keep filled in fundamental: 

589 if group[0,2] == -2: 

590 new_group = np.vstack((group[0,:], new_group)) 

591 return new_group, np.array(indices, dtype=int) 

592 

593 

594def extract_fundamentals(good_freqs, all_freqs, freq_tol, max_freq_tol, 

595 verbose=0, check_freqs=[], 

596 mains_freq=60.0, mains_freq_tol=1.0, 

597 min_freq=0.0, max_freq=2000.0, max_db_diff=20.0, 

598 max_divisor=4, min_group_size=3, max_harmonics_db=-5.0, 

599 max_harmonics=0, max_groups=0, **kwargs): 

600 """Extract fundamental frequencies from power-spectrum peaks. 

601  

602 Parameters 

603 ---------- 

604 good_freqs: 2-D array 

605 Frequency, power, and use count (columns) of strong peaks detected 

606 in a power spectrum. 

607 all_freqs: 2-D array 

608 Frequency, power, and use count (columns) of all peaks detected 

609 in a power spectrum. 

610 freq_tol: float 

611 Harmonics should fall within this frequency tolerance. 

612 This should be in the range of the frequency resolution 

613 and should not be smaller than half of the frequency resolution. 

614 max_freq_tol: float 

615 Maximum deviation of harmonics from their expected frequency. 

616 Peaks with frequencies between `freq_tol` and `max_freq_tol` 

617 get penalized the further away from their expected frequency. 

618 verbose: int 

619 Verbosity level. 

620 check_freqs: list of float 

621 List of fundamental frequencies that will be checked 

622 first for being present and valid harmonic groups in the peak frequencies 

623 of a power spectrum. 

624 mains_freq: float 

625 Frequency of the mains power supply. 

626 mains_freq_tol: float 

627 Tolarerance around harmonics of the mains frequency, 

628 within which peaks are removed. 

629 min_freq: float 

630 Minimum frequency accepted as a fundamental frequency. 

631 max_freq: float 

632 Maximum frequency accepted as a fundamental frequency. 

633 max_db_diff: float 

634 If larger than zero, maximum standard deviation of differences between 

635 logarithmic powers of harmonics in decibel. 

636 Low values enforce smoother power spectra. 

637 max_divisor: int 

638 Maximum divisor used for checking for sub-harmonics. 

639 min_group_size: int 

640 Minimum number of harmonics of a harmonic group. 

641 The harmonics from min_group_size/3 to max(min_group_size, divisor) 

642 need to be in good_freqs. 

643 max_harmonics_db: float 

644 Maximum allowed power of the `min_group_size`-th and higher harmonics 

645 after the peak (in decibel relative to peak power withn the first 

646 `min_group_size` harmonics, i.e. if harmonics are required to be 

647 smaller than fundamental then this is a negative number). 

648 Make it a large positive number to effectively not check for relative power. 

649 max_harmonics: int 

650 Maximum number of harmonics to be returned for each group. 

651 max_groups: int 

652 If not zero the maximum number of most powerful harmonic groups. 

653 

654 Returns 

655 ------- 

656 group_list: list of 2-D arrays 

657 List of all harmonic groups found sorted by fundamental frequency. 

658 Each harmonic group is a 2-D array with the first dimension the harmonics 

659 and the second dimension containing frequency and power of each harmonic. 

660 If the power is zero, there was no corresponding peak in the power spectrum. 

661 fzero_harmonics_list: list of int 

662 The harmonics from which the fundamental frequencies were computed. 

663 mains_freqs: 2-d array 

664 Array of mains peaks found in all_freqs (frequency, power). 

665 """ 

666 if verbose > 0: 

667 print('') 

668 

669 # set use count to zero: 

670 all_freqs[:,2] = 0.0 

671 

672 # remove power line harmonics from good_freqs: 

673 if mains_freq > 0.0: 

674 indices = np.where(np.abs(good_freqs[:,0] - np.round(good_freqs[:,0]/mains_freq)*mains_freq) < mains_freq_tol)[0] 

675 if len(indices)>0: 

676 if verbose > 1: 

677 print('remove power line frequencies', 

678 ', '.join(['%.1f' % f for f in good_freqs[indices,0]])) 

679 good_freqs = np.delete(good_freqs, indices, axis=0) 

680 

681 if verbose > 1: 

682 print('all_freqs: ', '[', 

683 ', '.join(['%.2f' % f for f in all_freqs[:,0] if f < 3000.0]), ']') 

684 

685 group_list = [] 

686 fzero_harmonics_list = [] 

687 first = True 

688 # as long as there are frequencies left in good_freqs: 

689 fi = 0 

690 while len(good_freqs) > 0: 

691 if fi < len(check_freqs): 

692 # check for harmonic group of a given fundamental frequency: 

693 fmax = check_freqs[fi] 

694 f0s = 'freq' 

695 good_freqs, harm_group, harm_indices, fzero_harmonics = \ 

696 retrieve_harmonic_group(fmax, good_freqs, all_freqs, 

697 freq_tol, max_freq_tol, 

698 verbose-1, min_group_size) 

699 fi += 1 

700 else: 

701 # check for harmonic groups: 

702 f0s = 'fmax' 

703 good_freqs, harm_group, harm_indices, fzero_harmonics, fmax = \ 

704 build_harmonic_group(good_freqs, all_freqs, 

705 freq_tol, max_freq_tol, verbose-1, 

706 min_group_size, max_divisor) 

707 

708 # nothing found: 

709 if len(harm_group) == 0: 

710 if verbose > 1 - first: 

711 s = ' (largest peak)' if first else '' 

712 print('No harmonic group for %7.2fHz%s' % (fmax, s)) 

713 first = False 

714 continue 

715 first = False 

716 

717 # fill up harmonic group: 

718 harm_group, harm_indices = expand_group(harm_group, harm_indices, 

719 all_freqs, freq_tol, 

720 max_harmonics) 

721 

722 # check whether fundamental was filled in: 

723 first_h = 0 if harm_group[0,2] > -2 else 1 

724 

725 # increment use count: 

726 all_freqs[harm_indices,2] += 1 

727 if harm_group[0,2] == -1: 

728 harm_group[0,2] = -2 

729 

730 # check frequency range of fundamental: 

731 fundamental_ok = (harm_group[0, 0] >= min_freq and 

732 harm_group[0, 0] <= max_freq) 

733 # check power hum: 

734 mains_ok = ((mains_freq <= 0.0) or 

735 (m.fabs(harm_group[0,0] - mains_freq) > freq_tol)) 

736 # check smoothness: 

737 db_powers = decibel(harm_group[first_h:,1]) 

738 diff = np.std(np.diff(db_powers)) 

739 smooth_ok = max_db_diff <= 0.0 or diff < max_db_diff 

740 # check relative power of higher harmonics: 

741 p_max = np.argmax(db_powers[:min_group_size]) 

742 db_powers -= db_powers[p_max] 

743 amplitude_ok = len(db_powers[p_max+min_group_size:]) == 0 

744 if amplitude_ok: 

745 pi = len(db_powers) - 1 

746 else: 

747 amplitude_ok = np.all((db_powers[p_max+min_group_size:] < max_harmonics_db) | 

748 (harm_group[first_h+p_max+min_group_size:,2] > 1)) 

749 if amplitude_ok: 

750 pi = p_max+min_group_size-1 

751 else: 

752 pi = np.where((db_powers[p_max+min_group_size:] >= max_harmonics_db) & 

753 (harm_group[first_h+p_max+min_group_size:,2] <= 1))[0][0] 

754 

755 # check: 

756 if fundamental_ok and mains_ok and smooth_ok and amplitude_ok: 

757 if verbose > 0: 

758 print('accept harmonic group from %s=%7.2fHz: %7.2fHz power=%9.3g nharmonics=%2d, use count=%d' 

759 % (f0s, fmax, harm_group[0,0], np.sum(harm_group[first_h:,1]), 

760 len(harm_group), np.sum(harm_group[first_h:,2]))) 

761 group_list.append(harm_group[:,0:2]) 

762 fzero_harmonics_list.append(fzero_harmonics) 

763 else: 

764 if verbose > 0: 

765 fs = 'is ' if fundamental_ok else 'NOT' 

766 ms = 'not ' if mains_ok else 'IS' 

767 ss = 'smaller' if smooth_ok else 'LARGER ' 

768 ps = 'smaller' if amplitude_ok else 'LARGER ' 

769 print('discard harmonic group from %s=%7.2fHz: %7.2fHz power=%9.3g: %s in frequency range, %s mains frequency, smooth=%4.1fdB %s than %4.1fdB, relpower[%d]=%5.1fdB %s than %5.1fdB' 

770 % (f0s, fmax, harm_group[0,0], np.sum(harm_group[first_h:,1]), 

771 fs, ms, diff, ss, max_db_diff, 

772 pi, db_powers[pi], ps, max_harmonics_db)) 

773 

774 # select most powerful harmonic groups: 

775 if max_groups > 0 and len(group_list) > max_groups: 

776 n = len(group_list) 

777 powers = [np.sum(group[:,1]) for group in group_list] 

778 powers_inx = np.argsort(powers) 

779 group_list = [group_list[pi] for pi in powers_inx[-max_groups:]] 

780 fzero_harmonics_list = [fzero_harmonics_list[pi] for pi in powers_inx[-max_groups:]] 

781 if verbose > 0: 

782 print(f'selected from {n} groups the {max_groups} most powerful groups.') 

783 

784 # sort groups by fundamental frequency: 

785 freqs = [group[0,0] for group in group_list] 

786 freq_inx = np.argsort(freqs) 

787 group_list = [group_list[fi] for fi in freq_inx] 

788 fzero_harmonics_list = [fzero_harmonics_list[fi] for fi in freq_inx] 

789 

790 if verbose > 1: 

791 print('') 

792 if len(group_list) > 0: 

793 print('## FUNDAMENTALS FOUND: ##') 

794 for group, fzero_h in zip(group_list, fzero_harmonics_list): 

795 print('%7.2fHz: power=%9.3g fzero-h=%2d' 

796 % (group[0,0], np.sum(group[:,1]), fzero_h)) 

797 else: 

798 print('## NO FUNDAMENTALS FOUND ##') 

799 

800 # assemble mains frequencies from all_freqs: 

801 if mains_freq > 0.0: 

802 mains_freqs = all_freqs[np.abs(all_freqs[:,0] - np.round(all_freqs[:,0]/mains_freq)*mains_freq) < mains_freq_tol,:2] 

803 else: 

804 mains_freqs = np.zeros((0, 2)) 

805 

806 return group_list, fzero_harmonics_list, mains_freqs 

807 

808 

809def threshold_estimate(psd_data, low_thresh_factor=6.0, high_thresh_factor=10.0, 

810 nbins=100, hist_height=1.0/ np.sqrt(np.e)): 

811 """Estimate thresholds for peak detection from histogram of power spectrum. 

812 

813 The standard deviation of the noise floor without peaks is estimated from 

814 the width of the histogram of the power spectrum at `hist_height` relative height. 

815 The histogram is computed in the third quarter of the linearly detrended power spectrum. 

816 

817 Parameters 

818 ---------- 

819 psd_data: 1-D array 

820 The power spectrum from which to estimate the thresholds. 

821 low_thresh_factor: float 

822 Factor by which the estimated standard deviation of the noise floor 

823 is multiplied to set the `low_threshold`. 

824 high_thresh_factor: float 

825 Factor by which the estimated standard deviation of the noise floor 

826 is multiplied to set the `high_threshold`. 

827 nbins: int or list of floats 

828 Number of bins or the bins for computing the histogram. 

829 hist_height: float 

830 Height between 0 and 1 at which the standard deviation of the histogram is estimated. 

831 

832 Returns 

833 ------- 

834 low_threshold: float 

835 The threshold for peaks just above the noise floor. 

836 high_threshold: float 

837 The threshold for distinct peaks. 

838 center: float 

839 The baseline level of the power spectrum. 

840 """ 

841 n = len(psd_data) 

842 psd_data_seg = psd_data[n//2:n*3//4] 

843 psd_data_seg = psd_data_seg[~np.isinf(psd_data_seg)] 

844 psd_data_seg = np.mean(psd_data_seg) + \ 

845 sig.detrend(psd_data_seg, type='linear') 

846 noise_std, center = hist_threshold(psd_data_seg, thresh_fac=1.0, nbins=nbins) 

847 low_threshold = noise_std * low_thresh_factor 

848 high_threshold = noise_std * high_thresh_factor 

849 return low_threshold, high_threshold, center 

850 

851 

852def harmonic_groups(psd_freqs, psd, verbose=0, check_freqs=[], 

853 low_threshold=0.0, high_threshold=0.0, thresh_bins=100, 

854 low_thresh_factor=6.0, high_thresh_factor=10.0, 

855 freq_tol_fac=1.0, max_freq_tol=1.0, 

856 mains_freq=60.0, mains_freq_tol=1.0, 

857 min_freq=0.0, max_freq=2000.0, max_db_diff=20.0, max_divisor=4, 

858 min_group_size=3, max_harmonics_db=-5.0, 

859 max_harmonics=0, max_groups=0, **kwargs): 

860 """Detect peaks in power spectrum and group them according to their harmonic structure. 

861 

862 Parameters 

863 ---------- 

864 psd_freqs: 1-D array 

865 Frequencies of the power spectrum. 

866 psd: 1-D array 

867 Power spectrum (linear, not decible). 

868 verbose: int 

869 Verbosity level. 

870 check_freqs: list of float 

871 List of fundamental frequencies that will be checked first for being 

872 present and valid harmonic groups in the peak frequencies 

873 of the power spectrum. 

874 low_threshold: float 

875 The relative threshold for detecting all peaks in the decibel spectrum. 

876 high_threshold: float 

877 The relative threshold for detecting good peaks in the decibel spectrum. 

878 thresh_bins: int or list of floats 

879 Number of bins or the bins for computing the histogram from 

880 which the standard deviation of the noise level in the `psd` is estimated. 

881 low_thresh_factor: float 

882 Factor by which the estimated standard deviation of the noise floor 

883 is multiplied to set the `low_threshold`. 

884 high_thresh_factor: float 

885 Factor by which the estimated standard deviation of the noise floor 

886 is multiplied to set the `high_threshold`. 

887 freq_tol_fac: float 

888 Harmonics should fall within `deltaf*freq_tol_fac`. 

889 max_freq_tol: float 

890 Maximum absolute frequency deviation of harmonics in Hertz.. 

891 mains_freq: float 

892 Frequency of the mains power supply. 

893 mains_freq_tol: float 

894 Tolarerance around harmonics of the mains frequency, 

895 within which peaks are removed. 

896 min_freq: float 

897 Minimum frequency accepted as a fundamental frequency. 

898 max_freq: float 

899 Maximum frequency accepted as a fundamental frequency. 

900 max_db_diff: float 

901 If larger than zero, maximum standard deviation of differences between 

902 logarithmic powers of harmonics in decibel (larger than zero). 

903 Low values enforce smoother power spectra. 

904 max_divisor: int 

905 Maximum divisor used for checking for sub-harmonics. 

906 min_group_size: int 

907 Minimum number of harmonics of a harmonic group. 

908 The harmonics from min_group_size/3 to max(min_group_size, divisor) 

909 need to be in good_freqs. 

910 max_harmonics_db: float 

911 Maximum allowed power of the `min_group_size`-th and higher harmonics 

912 after the peak (in decibel relative to peak power withn the first 

913 `min_group_size` harmonics, i.e. if harmonics are required to be 

914 smaller than fundamental then this is a negative number). 

915 Make it a large positive number to effectively not check for relative power. 

916 max_harmonics: int 

917 Maximum number of harmonics to be returned for each group. 

918 max_groups: int 

919 If not zero the maximum number of most powerful harmonic groups. 

920 

921 Returns 

922 ------- 

923 groups: list of 2-D arrays 

924 List of all extracted harmonic groups, sorted by fundamental frequency. 

925 Each harmonic group is a 2-D array with the harmonics in the rows, 

926 frequency of each harmonic in the first column and corresponding 

927 power in the second column. If the power is zero, there was no 

928 corresponding peak in the power spectrum. 

929 fzero_harmonics: list of ints 

930 The harmonics from which the fundamental frequencies were computed. 

931 mains: 2-d array 

932 Frequencies and power of multiples of the mains frequency found in the power spectrum. 

933 all_freqs: 2-D array 

934 Frequency, power, and use count (columns) of all peaks detected 

935 in the power spectrum. 

936 good_freqs: 1-D array 

937 Frequencies of strong peaks detected in the power spectrum. 

938 low_threshold: float 

939 The relative threshold for detecting all peaks in the decibel spectrum. 

940 high_threshold: float 

941 The relative threshold for detecting good peaks in the decibel spectrum. 

942 center: float 

943 The baseline level of the power spectrum. 

944 """ 

945 if verbose > 0: 

946 print('') 

947 if verbose > 1: 

948 print(70*'#') 

949 print('##### harmonic_groups', 48*'#') 

950 

951 # decibel power spectrum: 

952 log_psd = decibel(psd) 

953 max_idx = np.argmax(~np.isfinite(log_psd)) 

954 if max_idx > 0: 

955 log_psd = log_psd[:max_idx] 

956 delta_f = psd_freqs[1] - psd_freqs[0] 

957 

958 # thresholds: 

959 center = np.nan 

960 if low_threshold <= 0.0 or high_threshold <= 0.0: 

961 low_th, high_th, center = threshold_estimate(log_psd, low_thresh_factor, 

962 high_thresh_factor, 

963 thresh_bins) 

964 if low_threshold <= 0.0: 

965 low_threshold = low_th 

966 if high_threshold <= 0.0: 

967 high_threshold = high_th 

968 if verbose > 1: 

969 print('') 

970 print(f'low_threshold ={low_threshold:4.1f}dB, center+low_threshold ={center + low_threshold:6.1f}dB') 

971 print(f'high_threshold={high_threshold:4.1f}dB, center+high_threshold={center + high_threshold:6.1f}dB') 

972 print(f' center={center:6.1f}dB') 

973 

974 # detect peaks in decibel power spectrum: 

975 peaks, troughs = detect_peaks(log_psd, low_threshold) 

976 peaks, troughs = trim(peaks, troughs) 

977 all_freqs = np.zeros((len(peaks), 3)) 

978 all_freqs[:,0] = psd_freqs[peaks] 

979 all_freqs[:,1] = psd[peaks] 

980 

981 if len(all_freqs) == 0: 

982 return [], [], [], np.zeros((0, 3)), [], low_threshold, high_threshold, center 

983 

984 # select good peaks: 

985 good_freqs = all_freqs[(log_psd[peaks] - log_psd[troughs] > high_threshold) & 

986 (all_freqs[:,0] >= min_freq) & 

987 (all_freqs[:,0] < max_freq*max_divisor),:] 

988 

989 # detect harmonic groups: 

990 freq_tol = delta_f*freq_tol_fac 

991 if max_freq_tol < 1.1*freq_tol: 

992 max_freq_tol = 1.1*freq_tol 

993 groups, fzero_harmonics, mains = \ 

994 extract_fundamentals(good_freqs, all_freqs, 

995 freq_tol, max_freq_tol, 

996 verbose, check_freqs, mains_freq, mains_freq_tol, 

997 min_freq, max_freq, max_db_diff, max_divisor, min_group_size, 

998 max_harmonics_db, max_harmonics, max_groups) 

999 

1000 return (groups, fzero_harmonics, mains, all_freqs, good_freqs[:,0], 

1001 low_threshold, high_threshold, center) 

1002 

1003 

1004def fundamental_freqs(group_list): 

1005 """ 

1006 Extract fundamental frequencies from lists of harmonic groups. 

1007 

1008 The inner list of 2-D arrays of the input argument is transformed into 

1009 a 1-D array containig the fundamental frequencies extracted from 

1010 the 2-D arrays. 

1011 

1012 Parameters 

1013 ---------- 

1014 group_list: (list of (list of ...)) list of 2-D arrays 

1015 Arbitrarily nested lists of harmonic groups as returned by 

1016 extract_fundamentals() and harmonic_groups() with the element 

1017 [0, 0] being the fundamental frequency. 

1018 

1019 Returns 

1020 ------- 

1021 fundamentals: (list of (list of ...)) 1-D array 

1022 Nested list (corresponding to `group_list`) of 1-D arrays 

1023 with the fundamental frequencies extracted from the harmonic groups. 

1024 """ 

1025 if len(group_list) == 0: 

1026 return np.zeros(0) 

1027 

1028 # check whether group_list is list of harmonic groups: 

1029 list_of_groups = True 

1030 for group in group_list: 

1031 if not ( hasattr(group, 'shape') and len(group.shape) == 2 ): 

1032 list_of_groups = False 

1033 break 

1034 

1035 if list_of_groups: 

1036 fundamentals = np.array([group[0, 0] for group in group_list if len(group) > 0]) 

1037 else: 

1038 fundamentals = [] 

1039 for groups in group_list: 

1040 f = fundamental_freqs(groups) 

1041 fundamentals.append(f) 

1042 return fundamentals 

1043 

1044 

1045def fundamental_freqs_and_power(group_list, power=False, 

1046 ref_power=1.0, min_power=1e-20): 

1047 """Extract fundamental frequencies and their power in decibel from lists 

1048 of harmonic groups. 

1049 

1050 The inner list of 2-D arrays of the input argument is transformed 

1051 into a 2-D array containig for each fish (1st dimension) the 

1052 fundamental frequencies and powers (summed over all harmonics) 

1053 extracted from the 2-D arrays. 

1054  

1055 Parameters 

1056 ---------- 

1057 group_list: (list of (list of ...)) list of 2-D arrays 

1058 Arbitrarily nested lists of harmonic groups as returned by 

1059 extract_fundamentals() and harmonic_groups() with the element 

1060 [0, 0] being the fundamental frequency and the elements [:,1] being 

1061 the powers of each harmonics. 

1062 power: boolean 

1063 If `False` convert the power into decibel using the 

1064 `thunderlab.powerspectrum.decibel()` function. 

1065 ref_power: float 

1066 Reference power for computing decibel. 

1067 If set to `None` the maximum power is used. 

1068 min_power: float 

1069 Power values smaller than `min_power` are set to `-np.inf`. 

1070 

1071 Returns 

1072 ------- 

1073 fundamentals: (list of (list of ...)) 2-D array 

1074 Nested list (corresponding to `group_list`) of 2-D arrays 

1075 with fundamental frequencies in first column and 

1076 corresponding power in second column. 

1077 """ 

1078 if len(group_list) == 0: 

1079 return np.zeros((0, 2)) 

1080 

1081 # check whether group_list is list of harmonic groups: 

1082 list_of_groups = True 

1083 for group in group_list: 

1084 if not ( hasattr(group, 'shape') and len(group.shape) == 2 ): 

1085 list_of_groups = False 

1086 break 

1087 

1088 if list_of_groups: 

1089 fundamentals = np.array([[group[0, 0], np.sum(group[:, 1])] 

1090 for group in group_list if len(group) > 0]) 

1091 if not power: 

1092 fundamentals[:, 1] = decibel(fundamentals[:, 1], 

1093 ref_power, min_power) 

1094 else: 

1095 fundamentals = [] 

1096 for groups in group_list: 

1097 f = fundamental_freqs_and_power(groups, power, 

1098 ref_power, min_power) 

1099 fundamentals.append(f) 

1100 return fundamentals 

1101 

1102 

1103def add_relative_power(freqs): 

1104 """Add a column with relative power. 

1105 

1106 For each element in `freqs`, its maximum power is subtracted 

1107 from all powers. 

1108 

1109 Parameters 

1110 ---------- 

1111 freqs: list of 2-D arrays 

1112 First column in the ndarrays is fundamental frequency and 

1113 second column the corresponding power. 

1114 Further columns are optional and kept in the returned list. 

1115 fundamental_freqs_and_power() returns such a list. 

1116 

1117 Returns 

1118 ------- 

1119 power_freqs: list of 2-D arrays 

1120 Same as freqs, but with an added column containing the relative power. 

1121 """ 

1122 return [np.column_stack((f, f[:, 1] - np.max(f[:, 1]))) for f in freqs] 

1123 

1124 

1125def add_power_ranks(freqs): 

1126 """Add a column with power ranks. 

1127 

1128 Parameters 

1129 ---------- 

1130 freqs: list of 2-D arrays 

1131 First column in the arrays is fundamental frequency and 

1132 second column the corresponding power. 

1133 Further columns are optional and kept in the returned list. 

1134 fundamental_freqs_and_power() returns such a list. 

1135 

1136 Returns 

1137 ------- 

1138 rank_freqs: list of 2-D arrays 

1139 Same as freqs, but with an added column containing the ranks. 

1140 The highest power is assinged to zero, 

1141 lower powers are assigned negative integers. 

1142 """ 

1143 rank_freqs = [] 

1144 for f in freqs: 

1145 i = np.argsort(f[:, 1])[::-1] 

1146 ranks = np.empty_like(i) 

1147 ranks[i] = -np.arange(len(i)) 

1148 rank_freqs.append(np.column_stack((f, ranks))) 

1149 return rank_freqs 

1150 

1151 

1152def similar_indices(freqs, df_thresh, nextfs=0): 

1153 """Indices of similar frequencies. 

1154 

1155 If two frequencies from different elements in the inner lists of `freqs` are 

1156 reciprocally the closest to each other and closer than `df_thresh`, 

1157 then two indices (element, frequency) of the respective other frequency 

1158 are appended. 

1159 

1160 Parameters 

1161 ---------- 

1162 freqs: (list of (list of ...)) list of 1-D or 2-D arrays 

1163 First column in the inner arrays is fundamental frequency. 

1164 df_thresh: float 

1165 Fundamental frequencies reciprocally being the closest to each other 

1166 and closer than this threshold are considered equal. 

1167 nextfs: int 

1168 If zero, compare all elements in freqs with each other. Otherwise, 

1169 only compare with the `nextfs` next elements in freqs. 

1170 

1171 Returns 

1172 ------- 

1173 indices: (list of (list of ...)) list of list of two-tuples of int 

1174 For each frequency of each element in `freqs` a list of two tuples 

1175 containing the indices of elements and frequencies that are similar. 

1176 """ 

1177 if len(freqs) == 0: 

1178 return [] 

1179 

1180 # check whether freqs is list of array of fundamental frequencies: 

1181 list_of_freqs = True 

1182 for group in freqs: 

1183 if not (hasattr(group, 'shape') and 1 <= len(group.shape) <= 2): 

1184 list_of_freqs = False 

1185 break 

1186 

1187 if list_of_freqs: 

1188 indices = [ [[] for j in range(len(freqs[i]))] for i in range(len(freqs))] 

1189 for j in range(len(freqs) - 1): 

1190 freqsj = freqs[j] 

1191 nn = len(freqs) if nextfs == 0 else j + nextfs + 1 

1192 if nn > len(freqs): 

1193 nn = len(freqs) 

1194 for m in range(len(freqsj)): 

1195 freq1 = freqsj[m] 

1196 for k in range(j + 1, nn): 

1197 freqsk = freqs[k] 

1198 if len(freqsk) == 0: 

1199 continue 

1200 n = np.argmin(np.abs(freqsk[:, 0] - freq1[0])) 

1201 freq2 = freqsk[n] 

1202 if np.argmin(np.abs(freqsj[:, 0] - freq2[0])) != m: 

1203 continue 

1204 if np.abs(freq1[0] - freq2[0]) < df_thresh: 

1205 indices[j][m].append((k, n)) 

1206 indices[k][n].append((j, m)) 

1207 else: 

1208 indices = [] 

1209 for groups in freqs: 

1210 indices.append(similar_indices(groups, df_thresh, nextfs)) 

1211 return indices 

1212 

1213 

1214def unique_indices(freqs, df_thresh, nextfs=0): 

1215 """For each unique fundamental frequency a list of indices indicating where it is found.  

1216 

1217 Parameters 

1218 ---------- 

1219 freqs: (list of (list of ...)) list of 1-D or 2-D arrays 

1220 First column in the inner arrays is fundamental frequency. 

1221 df_thresh: float 

1222 Fundamental frequencies reciprocally being the closest to each other 

1223 and closer than this threshold are considered equal. 

1224 nextfs: int 

1225 If zero, compare all elements in `freqs` with each other. Otherwise, 

1226 only compare with the `nextfs` next elements in `freqs`. 

1227 

1228 Returns 

1229 ------- 

1230 freq_indices: (list of (list of ...)) list of list of 2-tuple of int 

1231 For each unique frequency in `freqs`, a list of two-tuples of indices 

1232 into `freqs`, indicating where in `freqs` these frequencies are. 

1233 """ 

1234 if len(freqs) == 0: 

1235 return [] 

1236 

1237 # check whether freqs is list of array of fundamental frequencies: 

1238 list_of_freqs = True 

1239 for group in freqs: 

1240 if not (hasattr(group, 'shape') and 1 <= len(group.shape) <= 2): 

1241 list_of_freqs = False 

1242 break 

1243 

1244 if list_of_freqs: 

1245 freq_indices = [] 

1246 indices = similar_indices(freqs, df_thresh, 0) 

1247 if len(indices) == 0: 

1248 return freq_indices 

1249 for j in range(len(indices)): 

1250 for m in range(len(indices[j])): 

1251 if indices[j][m] is not None: 

1252 # store: 

1253 freq_indices.append([(j, m)]) 

1254 freq_indices[-1].extend(indices[j][m]) 

1255 # remove from indices: 

1256 for inx in indices[j][m]: 

1257 indices[inx[0]][inx[1]] = None 

1258 else: 

1259 indices = [] 

1260 for groups in freqs: 

1261 indices.append(unique_indices(groups, df_thresh, nextfs)) 

1262 return freq_indices 

1263 

1264 

1265def collapse(group_list): 

1266 """Collapse harmonic groups with similar fundamental frequencies to a single one. 

1267  

1268 The group with the highest power (second column summed over all harmonics) is returned. 

1269 

1270 Parameters 

1271 ---------- 

1272 group_list: list of 2-D arrays of float 

1273 List of harmonic groups that are supposed to have similar fundamental frequencies. 

1274  

1275 Returns 

1276 ------- 

1277 group: 2-D array of float 

1278 Harmonic group with first column frequency of harmonics, second column corresponding powers. 

1279 Further columns are optional. 

1280 index: int 

1281 Index of the returned harmonic group. 

1282 """ 

1283 if len(group_list) == 0: 

1284 return np.zeros((0, 2)), 0 

1285 rgroup = group_list[0] 

1286 rindex = 0 

1287 for k, group in enumerate(group_list[1:]): 

1288 if np.sum(group[:, 1]) > np.sum(rgroup[:, 1]): 

1289 rgroup = group 

1290 rindex = 1 + k 

1291 return rgroup, rindex 

1292 

1293 

1294def consistent(group_list, df_thresh): 

1295 """Harmonic groups whose fundamental frequencies consistently appear everywhere.  

1296 

1297 Parameters 

1298 ---------- 

1299 group_list: (list of (list of ...)) list of list of 2-D arrays of float 

1300 Harmonic groups as returned by several calls to extract_fundamentals() 

1301 or harmonic_groups() with the element [0, 0] of the harmonic groups 

1302 being the fundamental frequency, and element[0, 1] the corresponding 

1303 power. 

1304 df_thresh: float 

1305 Fundamental frequencies reciprocally being the closest to each other 

1306 and closer than this threshold are considered equal. 

1307 

1308 Returns 

1309 ------- 

1310 consistent_groups: (list of (list of ...)) list of 2-D arrays of float 

1311 List of harmonic groups that consistently occur in all elements of 

1312 `group_list`. Each element has frequency of the harmonics in the 

1313 first column and corresponding power in the second column. 

1314 """ 

1315 if len(group_list) == 0: 

1316 return [] 

1317 

1318 # check whether group_list is list of harmonic groups: 

1319 list_of_groups = True 

1320 for groups in group_list: 

1321 for group in groups: 

1322 if not ( hasattr(group, 'shape') and len(group.shape) == 2 ): 

1323 list_of_groups = False 

1324 break 

1325 if not list_of_groups: 

1326 break 

1327 

1328 if list_of_groups: 

1329 consistent_groups = [] 

1330 freqs = fundamental_freqs_and_power(group_list) 

1331 indices = unique_indices(freqs, df_thresh, 0) 

1332 for idx in indices: 

1333 # frequency appears everywhere: 

1334 if len(idx) == len(freqs): 

1335 cgroup, _ = collapse([group_list[j][k] for j, k in idx]) 

1336 consistent_groups.append(cgroup) 

1337 else: 

1338 consistent_groups = [] 

1339 for groups in group_list: 

1340 consistent_groups.append(consistent(groups, df_thresh)) 

1341 return consistent_groups 

1342 

1343 

1344def closest(group_list, df_thresh, close_thresh=0, min_closest=1): 

1345 """Harmonic groups whose fundamental frequencies are closest to each other.  

1346 

1347 Parameters 

1348 ---------- 

1349 group_list: (list of (list of ...)) list of list of 2-D arrays of float 

1350 Harmonic groups as returned by several calls to extract_fundamentals() 

1351 or harmonic_groups() with the element [0, 0] of the harmonic groups 

1352 being the fundamental frequency, and element[0, 1] the corresponding 

1353 power. 

1354 df_thresh: float 

1355 Fundamental frequencies reciprocally being the closest to each other 

1356 and closer than this threshold are considered equal and form a unique group. 

1357 close_thresh: float 

1358 Subsequent fundamental frequencies within a unique group that are closer 

1359 than this threshold are considered close. 

1360 min_closest: int 

1361 Only return harmonic groups that have at minimum that many 

1362 harmonic groups whose fundamental frequencies are closest to 

1363 each other. 

1364 

1365 Returns 

1366 ------- 

1367 closest_groups: (list of (list of ...)) list of 2-D arrays of float 

1368 For each unique fundamental frequency in `group_list` the 

1369 harmonic group with the largest power among subsequent ones whose 

1370 fundamental frequencies are closest to each other. Each element 

1371 has frequency of the harmonics in the first column and 

1372 corresponding power in the second column. 

1373 closest_indices: (list of (list of ...)) 2-D array of int 

1374 For each unique fundamental frequency in `group_list` (rows) 

1375 the first and last index (two columns) into `group_list` 

1376 where the closest frequencies have been found. 

1377 

1378 """ 

1379 if len(group_list) == 0: 

1380 return [], np.zeros((0, 2)) 

1381 

1382 # check whether group_list is list of harmonic groups: 

1383 list_of_groups = True 

1384 for groups in group_list: 

1385 for group in groups: 

1386 if not ( hasattr(group, 'shape') and len(group.shape) == 2 ): 

1387 list_of_groups = False 

1388 break 

1389 if not list_of_groups: 

1390 break 

1391 

1392 if list_of_groups: 

1393 if close_thresh < 1e-8: 

1394 close_thresh = 1e-8 

1395 closest_groups = [] 

1396 closest_indices = [] 

1397 freqs = fundamental_freqs_and_power(group_list) 

1398 freq_indices = unique_indices(freqs, df_thresh, 0) 

1399 for fidx in freq_indices: 

1400 if len(fidx) == 0: 

1401 continue 

1402 if len(fidx) < 2: 

1403 if min_closest < 2: 

1404 cgroup = group_list[fidx[0][0]][fidx[0][1]] 

1405 closest_groups.append(cgroup) 

1406 closest_indices.append((fidx[0][0], fidx[0][0])) 

1407 continue 

1408 # find minimum difference in subsequent fundamental frequencies: 

1409 fp = [] 

1410 fpi = [] 

1411 fi = 0 

1412 for i in range(len(freqs)): 

1413 if fi < len(fidx): 

1414 j, k = fidx[fi] 

1415 else: 

1416 j = len(freqs) 

1417 fpi. append(fi) 

1418 if j == i: 

1419 fp. append(freqs[j][k][0]) 

1420 fi += 1 

1421 else: 

1422 fp. append(np.nan) 

1423 fp = np.array(fp) 

1424 dfs = np.abs(np.diff(fp)) 

1425 if np.all(np.isnan(dfs)): 

1426 if min_closest <= 1: 

1427 cgroup, cinx = collapse([group_list[j][k] for j, k in fidx]) 

1428 closest_groups.append(cgroup) 

1429 closest_indices.append((fidx[cinx][0], fidx[cinx][0])) 

1430 continue 

1431 i0 = fpi[np.nanargmin(dfs)] 

1432 i1 = i0 + 1 

1433 dfs = np.abs(np.diff(fp[np.isfinite(fp)])) 

1434 # expand: 

1435 for k in range(1, len(fidx)): 

1436 go_left = i0 > 0 and fidx[i0 - 1][0] + 1 == fidx[i0][0] and abs(dfs[i0 - 1]) < close_thresh 

1437 go_right = i1 < len(dfs) and fidx[i1][0] + 1 == fidx[i1 + 1][0] and abs(dfs[i1]) < close_thresh 

1438 if go_left and go_right: 

1439 if dfs[i0 - 1] <= dfs[i1]: 

1440 i0 -= 1 

1441 else: 

1442 i1 += 1 

1443 elif go_left: 

1444 i0 -= 1 

1445 elif go_right: 

1446 i1 += 1 

1447 else: 

1448 break 

1449 if i1 + 1 - i0 >= min_closest: 

1450 cgroup, _ = collapse([group_list[j][k] for j, k in fidx[i0:i1 + 1]]) 

1451 closest_groups.append(cgroup) 

1452 closest_indices.append((fidx[i0][0], fidx[i1][0])) 

1453 closest_indices = np.array(closest_indices) 

1454 else: 

1455 closest_groups = [] 

1456 closest_indices = [] 

1457 for groups in group_list: 

1458 cg, ci = closest(groups, df_thresh, min_closest) 

1459 closest_groups.append(cg) 

1460 closest_indices.append(ci) 

1461 return closest_groups, closest_indices 

1462 

1463 

1464def unique_mask(freqs, df_thresh, nextfs=0): 

1465 """Mark similar frequencies from different recordings as dublicate. 

1466 

1467 If two frequencies from different elements in `freqs` are 

1468 reciprocally the closest to each other and closer than `df_thresh`, 

1469 then the one with the smaller power is marked for removal. 

1470 

1471 Parameters 

1472 ---------- 

1473 freqs: list of 2-D arrays 

1474 First column in the arrays is fundamental frequency and 

1475 second column the corresponding power or equivalent. 

1476 If values in the second column are equal (e.g. they are the same ranks), 

1477 and there is a third column (e.g. power), 

1478 the third column is used to decide, which element should be removed. 

1479 df_thresh: float 

1480 Fundamental frequencies closer than this threshold are considered 

1481 equal. 

1482 nextfs: int 

1483 If zero, compare all elements in freqs with each other. Otherwise, 

1484 only compare with the `nextfs` next elements in freqs. 

1485 

1486 Returns 

1487 ------- 

1488 mask: list of boolean arrays 

1489 For each element in `freqs` True if that frequency should be kept. 

1490 """ 

1491 mask = [np.ones(len(freqs[i]), dtype=bool) for i in range(len(freqs))] 

1492 for j in range(len(freqs)-1): 

1493 freqsj = np.asarray(freqs[j]) 

1494 for m in range(len(freqsj)): 

1495 freq1 = freqsj[m] 

1496 nn = len(freqs) if nextfs == 0 else j+1+nextfs 

1497 if nn > len(freqs): 

1498 nn = len(freqs) 

1499 for k in range(j+1, nn): 

1500 freqsk = np.asarray(freqs[k]) 

1501 if len(freqsk) == 0: 

1502 continue 

1503 n = np.argmin(np.abs(freqsk[:,0] - freq1[0])) 

1504 freq2 = freqsk[n] 

1505 if np.argmin(np.abs(freqsj[:,0] - freq2[0])) != m: 

1506 continue 

1507 if np.abs(freq1[0] - freq2[0]) < df_thresh: 

1508 if freq1[1] > freq2[1]: 

1509 mask[k][n] = False 

1510 elif freq1[1] < freq2[1]: 

1511 mask[j][m] = False 

1512 elif len(freq1) > 2: 

1513 if freq1[2] > freq2[2]: 

1514 mask[k][n] = False 

1515 else: 

1516 mask[j][m] = False 

1517 else: 

1518 mask[j][m] = False 

1519 return mask 

1520 

1521 

1522def unique(freqs, df_thresh, mode='power', nextfs=0): 

1523 """Remove similar frequencies from different recordings. 

1524 

1525 If two frequencies from different elements in the inner lists of `freqs` 

1526 are reciprocally the closest to each other and closer than `df_thresh`, 

1527 then the one with the smaller power is removed. As power, either the 

1528 absolute power as provided in the second column of the data elements 

1529 in `freqs` is taken (mode=='power'), or the relative power 

1530 (mode='relpower'), or the power rank (mode='rank'). 

1531 

1532 Parameters 

1533 ---------- 

1534 freqs: (list of (list of ...)) list of 2-D arrays 

1535 First column in the arrays is fundamental frequency and 

1536 second column the corresponding power, as returned by 

1537 fundamental_freqs_and_power(). 

1538 df_thresh: float 

1539 Fundamental frequencies closer than this threshold are considered 

1540 equal. 

1541 mode: string 

1542 - 'power': use second column of freqs elements as power. 

1543 - 'relpower': use relative power computed from the second column 

1544 of freqs elements for deciding which frequency to delete. 

1545 - 'rank': use rank of second column of freqs elements 

1546 for deciding which frequency to delete. 

1547 nextfs: int 

1548 If zero, compare all elements in freqs with each other. Otherwise, 

1549 only compare with the `nextfs` next elements in freqs. 

1550 

1551 Returns 

1552 ------- 

1553 unique_freqs: (list of (list of ...)) list of 2-D arrays 

1554 Same as `freqs` but elements with similar fundamental frequencies 

1555 removed. 

1556 

1557 Raises 

1558 ------ 

1559 ValueError: 

1560 Invalid mode string. 

1561 """ 

1562 if len(freqs) == 0: 

1563 return [] 

1564 

1565 # check whether freqs is array of fundamental frequencies and powers: 

1566 list_of_freq_power = True 

1567 for group in freqs: 

1568 if not (hasattr(group, 'shape') and len(group.shape) == 2): 

1569 list_of_freq_power = False 

1570 break 

1571 

1572 if list_of_freq_power: 

1573 if mode == 'power': 

1574 mask = unique_mask(freqs, df_thresh, nextfs) 

1575 elif mode == 'relpower': 

1576 power_freqs = [f[:, [0, 2, 1]] for f in add_relative_power(freqs)] 

1577 mask = unique_mask(power_freqs, df_thresh, nextfs) 

1578 elif mode == 'rank': 

1579 rank_freqs = [f[:, [0, 2, 1]] for f in add_power_ranks(freqs)] 

1580 mask = unique_mask(rank_freqs, df_thresh, nextfs) 

1581 else: 

1582 raise ValueError(f'{mode} is not a valid mode for unique(). Choose one of "power", "relpower", or "rank"') 

1583 unique_freqs = [] 

1584 for f, m in zip(freqs, mask): 

1585 unique_freqs.append(f[m]) 

1586 else: 

1587 unique_freqs = [] 

1588 for groups in freqs: 

1589 unique_freqs.append(unique(groups, df_thresh, mode, nextfs)) 

1590 return unique_freqs 

1591 

1592 

1593def colors_markers(): 

1594 """Generate a list of colors and markers for plotting. 

1595 

1596 Returns 

1597 ------- 

1598 colors: list 

1599 list of colors 

1600 markers: list 

1601 list of markers 

1602 """ 

1603 # color and marker range: 

1604 colors = [] 

1605 markers = [] 

1606 mr2 = [] 

1607 

1608 # first color range: 

1609 cc0 = cm.gist_rainbow(np.linspace(0.0, 1.0, 8)) 

1610 

1611 # shuffle it: 

1612 for k in range((len(cc0) + 1) // 2): 

1613 colors.extend(cc0[k::(len(cc0) + 1) // 2]) 

1614 markers.extend(len(cc0) * 'o') 

1615 mr2.extend(len(cc0) * 'v') 

1616 # second darker color range: 

1617 cc1 = cm.gist_rainbow(np.linspace(0.33 / 7.0, 1.0, 7)) 

1618 cc1 = mc.hsv_to_rgb(mc.rgb_to_hsv(np.array([cc1[:, :3]])) * np.array([1.0, 0.9, 0.7]))[0] 

1619 cc1 = np.hstack((cc1, np.ones((len(cc1),1)))) 

1620 # shuffle it: 

1621 for k in range((len(cc1) + 1) // 2): 

1622 colors.extend(cc1[k::(len(cc1) + 1) // 2]) 

1623 markers.extend(len(cc1) * '^') 

1624 mr2.extend(len(cc1) * '*') 

1625 # third lighter color range: 

1626 cc2 = cm.gist_rainbow(np.linspace(0.67 / 6.0, 1.0, 6)) 

1627 cc2 = mc.hsv_to_rgb(mc.rgb_to_hsv(np.array([cc1[:, :3]])) * np.array([1.0, 0.5, 1.0]))[0] 

1628 cc2 = np.hstack((cc2, np.ones((len(cc2),1)))) 

1629 # shuffle it: 

1630 for k in range((len(cc2) + 1) // 2): 

1631 colors.extend(cc2[k::(len(cc2) + 1) // 2]) 

1632 markers.extend(len(cc2) * 'D') 

1633 mr2.extend(len(cc2) * 'x') 

1634 markers.extend(mr2) 

1635 return colors, markers 

1636 

1637 

1638def plot_harmonic_groups(ax, group_list, indices=None, max_groups=0, 

1639 skip_bad=False, sort_by_freq=True, label_power=False, 

1640 colors=None, markers=None, marker_size=18, 

1641 legend_rows=8, **kwargs): 

1642 """Mark decibel power of fundamentals and their harmonics in a plot. 

1643 

1644 Parameters 

1645 ---------- 

1646 ax: axis for plot 

1647 Axis used for plotting. 

1648 group_list: list of 2-D arrays 

1649 Lists of harmonic groups as returned by extract_fundamentals() and 

1650 harmonic_groups() with the element [0, 0] of the harmonic groups being 

1651 the fundamental frequency, and element[0, 1] being the corresponding power. 

1652 indices: list of int or None 

1653 If smaller than zero then set the legend label of the corresponding group in brackets. 

1654 max_groups: int 

1655 If not zero plot only the max_groups most powerful groups. 

1656 skip_bad: bool 

1657 Skip harmonic groups without index (entry in indices is negative). 

1658 sort_by_freq: boolean 

1659 If True sort legend by frequency, otherwise by power. 

1660 label_power: boolean 

1661 If `True` put the power in decibel in addition to the frequency into the legend. 

1662 colors: list of colors or None 

1663 If not None list of colors for plotting each group 

1664 markers: list of markers or None 

1665 If not None list of markers for plotting each group 

1666 marker_size: float 

1667 Maximum size of markers used to mark the pulses. 

1668 legend_rows: int 

1669 Maximum number of rows to be used for the legend. 

1670 kwargs:  

1671 Key word arguments for the legend of the plot. 

1672 

1673 Returns 

1674 ------- 

1675 groups_dict: dict 

1676 Dictionary that maps artist handles marking the harmonic groups 

1677 with tuples containing the index and the 2D arrays from the 

1678 `group_list`. 

1679 groups_colors: list of matplotlib colors 

1680 For each group the color that was used to mark it in 

1681 the power spectrum. 

1682 Color can be None if not plotted. 

1683 groups_markers: list of string 

1684 For each group the marker symbol that was used to mark it in 

1685 the power spectrum. 

1686 Marker symbol can be None if not plotted. 

1687 groups_sizes: list of float 

1688 For each group the marker size that was used to mark it in 

1689 the power spectrum relative to the maximumpossible size. 

1690 Marker size can be None if not plotted. 

1691 """ 

1692 if len(group_list) == 0: 

1693 return {}, [], [], [] 

1694 

1695 # sort by power: 

1696 powers = np.array([np.sum(group[:, 1]) for group in group_list]) 

1697 max_power = np.max(powers) 

1698 idx = np.argsort(-powers) 

1699 if max_groups > 0 and len(idx > max_groups): 

1700 idx = idx[:max_groups] 

1701 

1702 # sort by frequency: 

1703 if sort_by_freq: 

1704 freqs = np.array([group_list[group][0, 0] for group in idx]) 

1705 idx = idx[np.argsort(freqs)] 

1706 

1707 # plot: 

1708 groups_dict = {} 

1709 artists = [] 

1710 labels = [] 

1711 groups_colors = [None]*len(group_list) 

1712 groups_markers = [None]*len(group_list) 

1713 groups_sizes = [None]*len(group_list) 

1714 k = 0 

1715 for i in idx: 

1716 if indices is not None and skip_bad and indices[i] < 0: 

1717 continue 

1718 group = group_list[i] 

1719 x = group[:, 0] 

1720 y = decibel(group[:, 1]) 

1721 msize = marker_size*(0.3 + 0.7*(powers[i]/max_power)**0.25) 

1722 color_kwargs = {} 

1723 if colors is not None: 

1724 color_kwargs = {'color': colors[k%len(colors)]} 

1725 label = f'{group[0, 0]:6.1f} Hz' 

1726 if label_power: 

1727 label += f' {decibel(np.array([np.sum(group[:,1])]))[0]:6.1f}dB' 

1728 if indices is not None: 

1729 if indices[i] < 0: 

1730 label = '(' + label + ')' 

1731 else: 

1732 label = ' ' + label + ' ' 

1733 if markers is None: 

1734 aa = ax.plot(x, y, 'o', label=label, ms=msize, **color_kwargs) 

1735 else: 

1736 if k >= len(markers): 

1737 break 

1738 aa = ax.plot(x, y, label=label, linestyle='none', marker=markers[k], 

1739 mec=None, mew=0.0, ms=msize, **color_kwargs) 

1740 groups_colors[i] = aa[0].get_markerfacecolor() 

1741 groups_markers[i] = aa[0].get_marker() 

1742 groups_sizes[i] = aa[0].get_markersize()/marker_size 

1743 for a in aa: 

1744 a.set_picker(8) 

1745 groups_dict[a] = [i, group] 

1746 k += 1 

1747 

1748 # legend: 

1749 ncol = (k - 1) // legend_rows + 1 if legend_rows > 0 else 1 

1750 if ncol < 1: 

1751 ncol = 1 

1752 try: 

1753 handles = ax.legend(numpoints=1, ncols=ncol, **kwargs) 

1754 except TypeError: 

1755 handles = ax.legend(numpoints=1, ncol=ncol, **kwargs) 

1756 lines = handles.get_lines() 

1757 for k in range(min(len(lines), len(idx))): 

1758 a = lines[k] 

1759 a.set_picker(8) 

1760 i = idx[k] 

1761 groups_dict[a] = [i, group_list[i]] 

1762 return groups_dict, groups_colors, groups_markers, groups_sizes 

1763 

1764 

1765def plot_psd_harmonic_groups(ax, psd_freqs, psd, group_list, 

1766 mains=None, all_freqs=None, 

1767 good_freqs=None, log_freq=False, 

1768 min_freq=0.0, max_freq=2000.0, ymarg=0.0, 

1769 sstyle=dict(color='tab:blue', lw=1)): 

1770 """Plot decibel power-spectrum with detected peaks, harmonic groups, 

1771 and mains frequencies. 

1772  

1773 Parameters 

1774 ---------- 

1775 ax: axis for plot 

1776 Axis used for plotting. 

1777 psd_freqs: array 

1778 Frequencies of the power spectrum. 

1779 psd: array 

1780 Power spectrum (linear, not decible). 

1781 group_list: list of 2-D arrays 

1782 Lists of harmonic groups as returned by extract_fundamentals() and 

1783 harmonic_groups() with the element [0, 0] of the harmonic groups being the fundamental frequency, 

1784 and element[0, 1] being the corresponding power. 

1785 mains: 2-D array 

1786 Frequencies and power of multiples of the mains frequency found in the power spectrum. 

1787 all_freqs: 2-D array 

1788 Peaks in the power spectrum detected with low threshold. 

1789 good_freqs: 1-D array 

1790 Frequencies of peaks detected with high threshold. 

1791 log_freq: boolean 

1792 Logarithmic (True) or linear (False) frequency axis. 

1793 min_freq: float 

1794 Limits of frequency axis are set to `(min_freq, max_freq)` 

1795 if `max_freq` is greater than zero 

1796 max_freq: float 

1797 Limits of frequency axis are set to `(min_freq, max_freq)` 

1798 and limits of power axis are computed from powers below max_freq 

1799 if `max_freq` is greater than zero 

1800 ymarg: float 

1801 Add this to the maximum decibel power for setting the ylim. 

1802 sstyle: dict 

1803 Arguments passed on to the plot command for the power spectrum. 

1804 """ 

1805 

1806 # mark all and good psd peaks: 

1807 pmin, pmax = ax.get_ylim() 

1808 doty = pmax - 5.0 

1809 if all_freqs is not None: 

1810 ax.plot(all_freqs[:, 0], np.zeros(len(all_freqs[:, 0])) + doty, 'o', color='#ffffff') 

1811 if good_freqs is not None: 

1812 ax.plot(good_freqs, np.zeros(len(good_freqs)) + doty, 'o', color='#888888') 

1813 # mark mains frequencies: 

1814 if mains is not None and len(mains) > 0: 

1815 fpeaks = mains[:, 0] 

1816 fpeakinx = [int(np.round(fp/(psd_freqs[1]-psd_freqs[0]))) for fp in fpeaks if fp < psd_freqs[-1]] 

1817 ax.plot(fpeaks[:len(fpeakinx)], decibel(psd[fpeakinx]), linestyle='None', 

1818 marker='.', color='k', ms=10, mec=None, mew=0.0, 

1819 label=f'{mains[0, 0]:3.0f} Hz mains') 

1820 # mark harmonic groups: 

1821 colors, markers = colors_markers() 

1822 plot_harmonic_groups(ax, group_list, max_groups=0, sort_by_freq=True, 

1823 colors=colors, markers=markers, legend_rows=8, 

1824 loc='upper right') 

1825 # plot power spectrum: 

1826 plot_decibel_psd(ax, psd_freqs, psd, log_freq=log_freq, min_freq=min_freq, 

1827 max_freq=max_freq, ymarg=ymarg, sstyle=sstyle) 

1828 

1829 

1830def annotate_harmonic_group(ax, group, index=None, all_peaks=None, 

1831 freq_thresh=None): 

1832 """Annotate peaks of a harmonic group in a spectrum. 

1833 

1834 Parameters 

1835 ---------- 

1836 ax: axis for plot 

1837 Axis used for plotting. 

1838 group: 2D array of float 

1839 All harmonics of a harmonic group first column their 

1840 frequencies and second comun their power. Element [0, 0] is 

1841 its fundamental frequency. 

1842 index: None or ont 

1843 Index of the group. 

1844 all_freqs: None or 2D array of float 

1845 All peaks in the power spectrum detected with low threshold. 

1846 freq_thresh: None or float 

1847 Threshold for matching peak frequencies. Something like 

1848 `0.8*delta_f`, where `delta_f` is the frequency resolution 

1849 of the power spectrum. 

1850 

1851 Returns 

1852 ------- 

1853 artists: list of matplotlib.Artist 

1854 List of the annotations. Use it to remove the annotations: 

1855 ``` 

1856 for a in artists: 

1857 a.remove() 

1858 artists = [] 

1859 ax.get_figure().canvas.draw() 

1860 ``` 

1861 

1862 """ 

1863 fmin, fmax = ax.get_xlim() 

1864 fwidth = fmax - fmin 

1865 artists = [] 

1866 f1 = group[0, 0] 

1867 for harmonic, (freq, power) in enumerate(group): 

1868 count = None 

1869 if all_peaks is not None and freq_thresh is not None: 

1870 peak = all_peaks[np.abs(all_peaks[:, 0] - freq) < freq_thresh, :] 

1871 if len(peak) == 0: 

1872 continue 

1873 count = peak[2] 

1874 pl = [] 

1875 if index is not None: 

1876 pl.append(f'{index}: {f1:.1f}Hz') 

1877 pl.append(f'$f{harmonic + 1} = ${freq:.1f}Hz') 

1878 pl.append(f'$p=${power:g}') 

1879 if count is not None: 

1880 pl.append(f'$c=${count}') 

1881 a = ax.annotate('\n'.join(pl), xy=(freq, decibel(power)), 

1882 xytext=(freq + 0.03*fwidth, decibel(power)), 

1883 bbox=dict(boxstyle='round', facecolor='white'), 

1884 arrowprops=dict(arrowstyle='-')) 

1885 artists.append(a) 

1886 return artists 

1887 

1888 

1889def plot_selected_groups(ax, group_list, selected_groups, indices=None, 

1890 label=None, 

1891 freq_style=dict(ls='none', marker='o', 

1892 color='k', ms=10), 

1893 bar_style=dict(ls='-', color='c', lw=12)): 

1894 """ 

1895 Plot all fundamental frequencies and mark selected frequencies with a bar. 

1896 

1897 Parameters 

1898 ---------- 

1899 ax: matplotlib.Axes 

1900 Axes for plotting. 

1901 group_list: list of list of 2-D arrays of float 

1902 Harmonic groups as returned by several calls to extract_fundamentals() 

1903 or harmonic_groups() with the element [0, 0] of the harmonic groups 

1904 being the fundamental frequency, and element[0, 1] the corresponding 

1905 power. 

1906 selected_groups: list of 2-D arrays of float 

1907 Frequencies and power of selected harmonic groups from `group_list`. 

1908 indices: None or list of two-tuple of int 

1909 Ranges of the selected groups. 

1910 label: str or None 

1911 Label for the selected groups that is added to the legend. 

1912 freq_style: dict 

1913 Plot style for marking fundamental frequencies of `group_list`. 

1914 bar_style: dict 

1915 Plot style for marking frequencies from `selected_groups`. 

1916 """ 

1917 # mark selected frequencies: 

1918 if indices is None: 

1919 for index in range(len(selected_groups)): 

1920 x = [-0.25, len(group_list) - 0.75] 

1921 y = [selected_groups[index][0, 0]]*2 

1922 if index == 0 and label is not None: 

1923 ax.plot(x, y, label=label, **bar_style) 

1924 else: 

1925 ax.plot(x, y, **bar_style) 

1926 else: 

1927 for index in range(len(selected_groups)): 

1928 x = [indices[index][0] - 0.25, 

1929 indices[index][1] + 0.25] 

1930 y = [selected_groups[index][0, 0]]*2 

1931 if index == 0 and label is not None: 

1932 ax.plot(x, y, label=label, **bar_style) 

1933 else: 

1934 ax.plot(x, y, **bar_style) 

1935 # mark all frequencies: 

1936 for index in range(len(group_list)): 

1937 for group in group_list[index]: 

1938 ax.plot(index, group[0, 0], **freq_style) 

1939 ax.set_xlim([-0.5, len(group_list) - 0.5]) 

1940 ax.xaxis.set_major_locator(plt.MultipleLocator(1)) 

1941 ax.set_ylabel('frequency [Hz]') 

1942 ax.set_xlabel('group index') 

1943 

1944 

1945def plot_unique_frequencies(ax, freqs_list, unique_freqs, 

1946 freq_style=dict(ls='none', marker='o', 

1947 color='k', ms=10), 

1948 unique_style=dict(ls='none', marker='o', 

1949 color='r', ms=16)): 

1950 """ 

1951 Plot all fundamental frequencies and mark unique frequencies. 

1952 

1953 Parameters 

1954 ---------- 

1955 ax: matplotlib.Axes 

1956 Axes for plotting. 

1957 freqs_list: list of 2-D arrays of float 

1958 Harmonic groups as returned by severla calls to extract_fundamentals() 

1959 or harmonic_groups() with the element [0, 0] of the harmonic groups 

1960 being the fundamental frequency, and element[0, 1] the corresponding 

1961 power. 

1962 unique_freqs: list of 2-D arrays of float 

1963 Frequencies and power of unique harmonic groups from `freqs_list`. 

1964 freq_style: dict 

1965 Plot style for marking fundamental frequencies of `freqs_list`. 

1966 unique_style: dict 

1967 Plot style for marking frequencies from `unique_freqs`. 

1968 """ 

1969 for index in range(len(freqs_list)): 

1970 freqs = freqs_list[index][:, 0] 

1971 for freq in freqs: 

1972 if len(unique_freqs[index][:, 0]) > 0 and \ 

1973 np.min(np.abs(unique_freqs[index][:, 0] - freq)) < 1e-8: 

1974 ax.plot(index, freq, **unique_style) 

1975 ax.plot(np.zeros(len(freqs)) + index, freqs, **freq_style) 

1976 ax.set_xlim([-0.5, len(freqs_list) - 0.5]) 

1977 ax.xaxis.set_major_locator(plt.MultipleLocator(1)) 

1978 ax.set_ylabel('frequency [Hz]') 

1979 ax.set_xlabel('group index') 

1980 

1981 

1982def add_psd_peak_detection_config(cfg, low_threshold=0.0, high_threshold=0.0, 

1983 thresh_bins=100, 

1984 low_thresh_factor=6.0, high_thresh_factor=10.0): 

1985 """Add parameter needed for detection of peaks in power spectrum used 

1986 by harmonic_groups() as a new section to a configuration. 

1987 

1988 Parameters 

1989 ---------- 

1990 cfg: ConfigFile 

1991 The configuration. 

1992 """ 

1993 cfg.add_section('Thresholds for peak detection in power spectra:') 

1994 cfg.add('lowThreshold', low_threshold, 'dB', 'Threshold for all peaks.\n If 0.0 estimate threshold from histogram.') 

1995 cfg.add('highThreshold', high_threshold, 'dB', 'Threshold for good peaks. If 0.0 estimate threshold from histogram.') 

1996 

1997 cfg.add_section('Threshold estimation:\nIf no thresholds are specified they are estimated from the histogram of the decibel power spectrum.') 

1998 cfg.add('thresholdBins', thresh_bins, '', 'Number of bins used to compute the histogram used for threshold estimation.') 

1999 cfg.add('lowThresholdFactor', low_thresh_factor, '', 'Factor for multiplying standard deviation of noise floor for lower threshold.') 

2000 cfg.add('highThresholdFactor', high_thresh_factor, '', 'Factor for multiplying standard deviation of noise floor for higher threshold.') 

2001 

2002 

2003def psd_peak_detection_args(cfg): 

2004 """Translates a configuration to the respective parameter names for 

2005 the detection of peaks in power spectrum used by 

2006 harmonic_groups(). The return value can then be passed as 

2007 key-word arguments to this function. 

2008 

2009 Parameters 

2010 ---------- 

2011 cfg: ConfigFile 

2012 The configuration. 

2013 

2014 Returns 

2015 ------- 

2016 a: dict 

2017 Dictionary with names of arguments of the `harmonic-group()` function 

2018 and their values as supplied by `cfg`. 

2019 """ 

2020 return cfg.map(low_threshold='lowThreshold', 

2021 high_threshold='highThreshold', 

2022 thresh_bins='thresholdBins', 

2023 low_thresh_factor='lowThresholdFactor', 

2024 high_thresh_factor='highThresholdFactor') 

2025 

2026 

2027def add_harmonic_groups_config(cfg, mains_freq=60.0, mains_freq_tol=1.0, 

2028 max_divisor=4, freq_tol_fac=1.0, 

2029 max_freq_tol=1.0, min_group_size=3, 

2030 min_freq=20.0, max_freq=2000.0, max_db_diff=20.0, 

2031 max_harmonics_db=-5.0, max_harmonics=0, max_groups=0): 

2032 """Add parameter needed for detection of harmonic groups as a new 

2033 section to a configuration. 

2034 

2035 Parameters 

2036 ---------- 

2037 cfg: ConfigFile 

2038 The configuration. 

2039 """ 

2040 cfg.add_section('Harmonic groups:') 

2041 cfg.add('mainsFreq', mains_freq, 'Hz', 'Mains frequency to be excluded.') 

2042 cfg.add('mainsFreqTolerance', mains_freq_tol, 'Hz', 'Exclude peaks within this tolerance around multiples of the mains frequency.') 

2043 cfg.add('minimumGroupSize', min_group_size, '', 

2044'Minimum number of harmonics (inclusively fundamental) that make up a harmonic group.') 

2045 cfg.add('maxDivisor', max_divisor, '', 'Maximum ratio between the frequency of the largest peak and its fundamental.') 

2046 cfg.add('freqTolerance', freq_tol_fac, '', 

2047 'Harmonics should be within this factor times the frequency resolution of the power spectrum. Needs to be higher than 0.5!') 

2048 cfg.add('maximumFreqTolerance', max_freq_tol, 'Hz', 

2049 'Maximum deviation of harmonics from their expected value.') 

2050 

2051 cfg.add_section('Acceptance of best harmonic groups:') 

2052 cfg.add('minimumFrequency', min_freq, 'Hz', 'Minimum frequency allowed for the fundamental.') 

2053 cfg.add('maximumFrequency', max_freq, 'Hz', 'Maximum frequency allowed for the fundamental.') 

2054 cfg.add('maximumPowerDifference', max_db_diff, 'dB', 'If larger than zero, maximum standard deviation allowed for difference in logarithmic power between successive harmonics. Smaller values enforce smoother spectra.') 

2055 cfg.add('maximumHarmonicsPower', max_harmonics_db, 'dB', 'Maximum allowed power of the minimumGroupSize-th and higher harmonics relative to peak power.') 

2056 cfg.add('maximumHarmonics', max_harmonics, '', 'Only keep the first # harmonics. If 0 keep all.') 

2057 cfg.add('maximumGroups', max_groups, '', 'Maximum number of harmonic groups. If 0 process all.') 

2058 

2059 

2060def harmonic_groups_args(cfg): 

2061 """Translates a configuration to the respective parameter names of 

2062 the harmonic-group detection functions. The return value can then 

2063 be passed as key-word arguments to this function. 

2064 

2065 Parameters 

2066 ---------- 

2067 cfg: ConfigFile 

2068 The configuration. 

2069 

2070 Returns 

2071 ------- 

2072 a: dict 

2073 Dictionary with names of arguments of the harmonic-group detection functions 

2074 and their values as supplied by `cfg`. 

2075 """ 

2076 return cfg.map(mains_freq='mainsFreq', 

2077 mains_freq_tol='mainsFreqTolerance', 

2078 freq_tol_fac='freqTolerance', 

2079 max_freq_tol='maximumFreqTolerance', 

2080 max_divisor='maxDivisor', 

2081 min_group_size='minimumGroupSize', 

2082 min_freq='minimumFrequency', 

2083 max_freq='maximumFrequency', 

2084 max_db_diff='maximumPowerDifference', 

2085 max_harmonics_db='maximumHarmonicsPower', 

2086 max_harmonics='maximumHarmonics', 

2087 max_groups='maximumGroups') 

2088 

2089 

2090def main(data_file=None): 

2091 import matplotlib.pyplot as plt 

2092 from thunderlab.powerspectrum import psd 

2093 from .fakefish import wavefish_eods 

2094 

2095 def mkg(f, p, n=5): 

2096 return np.array([(h*f, p/h) for h in range(1, n)]) 

2097 

2098 if data_file is None: 

2099 # generate data: 

2100 title = 'simulation' 

2101 rate = 44100.0 

2102 d = 20.0 

2103 noise = 0.01 

2104 eodfs = [123.0, 333.0, 666.0, 666.5, 792.8] 

2105 fish1 = 0.5*wavefish_eods('Eigenmannia', eodfs[0], rate, 

2106 duration=d, noise_std=noise) 

2107 fish2 = 1.0*wavefish_eods('Eigenmannia', eodfs[1], rate, 

2108 duration=d, noise_std=noise) 

2109 fish3 = 10.0*wavefish_eods('Alepto', eodfs[2], rate, 

2110 duration=d, noise_std=noise) 

2111 fish4 = 6.0*wavefish_eods('Alepto', eodfs[3], rate, 

2112 duration=d, noise_std=noise) 

2113 fish5 = 2.7*wavefish_eods('Alepto', eodfs[4], rate, 

2114 duration=d, noise_std=noise) 

2115 data = fish1 + fish2 + fish3 + fish4 + fish5 

2116 else: 

2117 from thunderlab.dataloader import load_data 

2118 print(f"load {data_file} ...") 

2119 data, rate, unit, amax = load_data(data_file) 

2120 data = data[:, 0] 

2121 title = data_file 

2122 

2123 # retrieve fundamentals from power spectrum: 

2124 psd_data = psd(data, rate, freq_resolution=0.1) 

2125 groups, _, mains, all_freqs, good_freqs, _, _, _ = harmonic_groups(psd_data[0], psd_data[1], check_freqs=[123.0, 666.0], max_db_diff=30.0, verbose=0) 

2126 fundamentals = fundamental_freqs(groups) 

2127 np.set_printoptions(formatter={'float': lambda x: f'{x:5.1f}'}) 

2128 print('fundamental frequencies extracted from power spectrum:') 

2129 print(fundamentals) 

2130 fig, ax = plt.subplots() 

2131 plot_psd_harmonic_groups(ax, psd_data[0], psd_data[1], groups, mains, 

2132 all_freqs, good_freqs, max_freq=3000.0) 

2133 annotate_harmonic_group(ax, groups[1]) 

2134 ax.set_title(title) 

2135 plt.show() 

2136 print() 

2137 

2138 # add groups: 

2139 group_list = [groups] 

2140 group_list.append([mkg(44.0, 2.0), mkg(123.4, 17.2), mkg(320.5, 294.3), mkg(665.5, 486.1), mkg(666.2, 782.3), mkg(793.1, 74.9)]) 

2141 group_list.append([mkg(123.3, 163.8), mkg(320.2, 82.5), mkg(666.1, 184.9), mkg(668.4, 205.1)]) 

2142 group_list.append([mkg(123.1, 44.5), mkg(666.1, 86.4)]) 

2143 fundamentals = fundamental_freqs(group_list) 

2144 print('fundamental frequencies of group_list:') 

2145 print('\n'.join((str(f) for f in fundamentals))) 

2146 print() 

2147 

2148 # unify fundamental frequencies: 

2149 freqs = fundamental_freqs_and_power(group_list) 

2150 rank_freqs = add_relative_power(freqs) 

2151 freqs = add_power_ranks(rank_freqs) 

2152 print('all frequencies (frequency, power, relpower, rank):') 

2153 print('\n'.join((str(f) for f in freqs))) 

2154 print() 

2155 indices = similar_indices(freqs, 1.0) 

2156 print('similar indices:') 

2157 for i in range(len(indices)): 

2158 for inx in indices[i]: 

2159 if len(inx) > 0: 

2160 print(f'{i} {freqs[inx[0][0]][inx[0][1]][0]:6.1f}Hz:', inx) 

2161 else: 

2162 print(f'{i} :', inx) 

2163 print() 

2164 indices = unique_indices(freqs, 1.0) 

2165 print('unique indices:') 

2166 for inx in indices: 

2167 print(f'{freqs[inx[0][0]][inx[0][1]][0]:6.1f}Hz:', inx) 

2168 print() 

2169 

2170 # consistent groups: 

2171 cgroups = consistent(group_list, 1.0) 

2172 print('fundamental frequencies of consistent groups:') 

2173 print(fundamental_freqs(cgroups)) 

2174 print() 

2175 fig, axs = plt.subplots(2, 2, layout='constrained', sharey=True) 

2176 axs[0, 0].set_title('consistent groups') 

2177 plot_selected_groups(axs[0, 0], group_list, cgroups) 

2178 

2179 # check almost empty group_list: 

2180 group2_list = [[], group_list[1], []] 

2181 cgroups = consistent(group2_list, 1.0) 

2182 axs[0, 1].set_title('consistent in mostly empty group list') 

2183 plot_selected_groups(axs[0, 1], group2_list, cgroups) 

2184 

2185 # check single group_list: 

2186 group3_list = [group_list[1]] 

2187 cgroups = consistent(group3_list, 1.0) 

2188 axs[1, 0].set_title('consistent in single group list') 

2189 plot_selected_groups(axs[1, 0], group3_list, cgroups) 

2190 

2191 # closest groups: 

2192 cgroups, cindices = closest(group_list, 1.0, 3, 2) 

2193 print('closest frequencies:') 

2194 print(fundamental_freqs(cgroups)) 

2195 print(cindices) 

2196 axs[1, 1].set_title('closest groups') 

2197 plot_selected_groups(axs[1, 1], group_list, cgroups, cindices) 

2198 plt.show() 

2199 print() 

2200 

2201 fig, axs = plt.subplots(2, 2, layout='constrained') 

2202 unique_freqs = unique(freqs, 1.0, 'power') 

2203 axs[0, 0].set_title('unique power') 

2204 plot_unique_frequencies(axs[0, 0], freqs, unique_freqs) 

2205 print('unique power:') 

2206 print('\n'.join((str(f) for f in unique_freqs))) 

2207 print() 

2208 

2209 unique_freqs = unique(freqs, 1.0, 'relpower') 

2210 axs[0, 1].set_title('unique relative power') 

2211 plot_unique_frequencies(axs[0, 1], freqs, unique_freqs) 

2212 print('unique relative power:') 

2213 print('\n'.join((str(f) for f in unique_freqs))) 

2214 print() 

2215 

2216 unique_freqs = unique(freqs, 1.0, 'rank') 

2217 axs[1, 0].set_title('unique rank') 

2218 plot_unique_frequencies(axs[1, 0], freqs, unique_freqs) 

2219 print('unique rank:') 

2220 print('\n'.join((str(f) for f in unique_freqs))) 

2221 print() 

2222 

2223 unique_freqs = unique(freqs, 1.0, 'rank', 1) 

2224 axs[1, 1].set_title('unique rank for next neighbor') 

2225 plot_unique_frequencies(axs[1, 1], freqs, unique_freqs) 

2226 print('unique rank for next neighour only:') 

2227 print('\n'.join((str(f) for f in unique_freqs))) 

2228 print() 

2229 plt.show() 

2230 

2231 

2232if __name__ == "__main__": 

2233 import sys 

2234 data_file = sys.argv[1] if len(sys.argv) > 1 else None 

2235 main(data_file) 

2236