Coverage for src/thunderfish/freqanalysis.py: 14%

86 statements  

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

1""" 

2Analysis of EOD frequencies. 

3 

4 

5## Muscial intervals 

6 

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

8- `musical_intervals_short`: short names for musical intervals 

9 

10 

11## Frequency analysis 

12 

13- `freq_diffs()`: matrix with frequency differences. 

14- `freq_ratios()`: matrix with frequency ratios. 

15- `freq_intervals()`: matrices with musical intervals and deviations thereof. 

16 

17 

18## Visualization 

19 

20- `plot_freq_diffs()`: plot matrix with frequency differences. 

21- `plot_freq_ratios()`: plot matrix with frequency ratios. 

22- `plot_musical_intervals()`: plot matrix with musical intervals. 

23 

24""" 

25 

26import numpy as np 

27 

28from matplotlib import ticker 

29 

30 

31musical_intervals = { 

32 'unison': (1/1, 1, 1, 0), # 1 

33 'minor second': (16/15, 16, 15, 1), # 1.0667 

34 'major second': (9/8, 9, 8, 2), # 1.125 

35 'minor third': (6/5, 6, 5, 3), # 1.2 

36 'major third': (5/4, 5, 4, 4), # 1.25 

37 'forth': (4/3, 4, 3, 5), # 1.3333 

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

39 'fifth': (3/2, 3, 2, 7), # 1.5 

40 'minor sixth': (8/5, 8, 5, 8), # 1.6 

41 'major sixth': (5/3, 5, 3, 9), # 1.6667 

42 'subminor seventh': (7/4, 7, 4, 9.5), # 1.75 

43 'minor seventh': (9/5, 9, 5, 10), # 1.8 

44 'major seventh': (15/8, 15, 8, 11), # 1.875 

45 'octave': (2/1, 2, 1, 12) # 2 

46} 

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

48""" 

49 

50musical_intervals_short = { 

51 'unison': 'P1', 

52 'minor second': 'm2', 

53 'major second': 'M2', 

54 'minor third': 'm3', 

55 'major third': 'M3', 

56 'forth': 'P4', 

57 'tritone': 'd5', 

58 'fifth': 'P5', 

59 'minor sixth': 'm6', 

60 'major sixth': 'M6', 

61 'subminor seventh': 'd7', 

62 'minor seventh': 'm7', 

63 'major seventh': 'M7', 

64 'octave': 'P8' 

65} 

66"""Short names for musical intervals 

67""" 

68 

69 

70def freq_diffs(freqs): 

71 """Matrix with frequency differences. 

72  

73 Parameters 

74 ---------- 

75 freqs: 1-D array of float 

76 List of frequencies. 

77 

78 Returns 

79 ------- 

80 deltafs: 2-D array of float 

81 Matrix with frequency differences for each pair of frequencies. 

82 """ 

83 deltafs = freqs.reshape(-1, 1) - freqs.reshape(1, -1) 

84 return deltafs 

85 

86 

87def freq_ratios(freqs): 

88 """Matrix with frequency ratios. 

89  

90 Parameters 

91 ---------- 

92 freqs: 1-D array of float 

93 List of frequencies. 

94 

95 Returns 

96 ------- 

97 ratios: 2-D array of float 

98 Matrix with frequency ratios for each pair of frequencies. 

99 """ 

100 ratios = freqs.reshape(-1, 1)/freqs.reshape(1, -1) 

101 return ratios 

102 

103 

104def freq_intervals(freqs): 

105 """Matrices with musical intervals and deviations thereof. 

106  

107 Parameters 

108 ---------- 

109 freqs: 1-D array of float 

110 List of frequencies. 

111 

112 Returns 

113 ------- 

114 intervals: 2-D array of int 

115 Matrix with index of nearest musical interval. 

116 diffs: 2-D array of float 

117 Matrix with deviation of freqeuncy ratio to nearest 

118 musical interval ratio. 

119 diff_fracs: 2-D array of float 

120 Matrix with deviation of freqeuncy ratio to nearest 

121 musical interval ratio relative to musical interval ratio. 

122 """ 

123 all_intervals = np.array([musical_intervals[k][0] 

124 for k in musical_intervals]) 

125 ratios = freqs.reshape(-1, 1)/freqs.reshape(1, -1) 

126 intervals = np.zeros(ratios.shape, dtype=int) 

127 diffs = np.zeros(ratios.shape) 

128 diff_fracs = np.zeros(ratios.shape) 

129 for r in range(ratios.shape[0]): 

130 for c in range(ratios.shape[1]): 

131 if r != c and 0.5 <= ratios[r, c] < 2.05: 

132 if ratios[r, c] >= 0.98: 

133 ratio = ratios[r, c] 

134 else: 

135 ratio = 1/ratios[r, c] 

136 intervals[r, c] = np.argmin(np.abs(all_intervals - ratio)) 

137 diffs[r, c] = ratio - all_intervals[intervals[r, c]] 

138 diff_fracs[r, c] = diffs[r, c]/all_intervals[intervals[r, c]] 

139 else: 

140 intervals[r, c] = -1 

141 diffs[r, c] = np.nan 

142 diff_fracs[r, c] = np.nan 

143 return intervals, diffs, diff_fracs 

144 

145 

146def plot_freq_diffs(ax, freqs, cmap='seismic', fontsize='large'): 

147 """Plot matrix with frequency differences. 

148 

149 Parameters 

150 ---------- 

151 ax: matplotlib axes 

152 Axes used for plotting. 

153 freqs: 1-D array of float 

154 List of frequencies. 

155 cmap: matplotlib color map 

156 Color map for coloring frequency differences. 

157 fontsize: str or float 

158 Font size for labels indicating frequency differences. 

159 """ 

160 ax.set_title('Differences $\\Delta f$') 

161 deltafs = freq_diffs(freqs) 

162 vmax = np.max(np.abs(deltafs)) 

163 cma = ax.pcolormesh(deltafs[::-1, :], cmap=cmap, 

164 vmin=-vmax, vmax=vmax) 

165 for r in range(deltafs.shape[0]): 

166 for c in range(deltafs.shape[1]): 

167 ax.text(c + 0.5, r + 0.5, f'{deltafs[-1 - r, c]:.1f}', 

168 ha='center', va='center', 

169 fontsize=fontsize, clip_on=True, 

170 bbox=dict(boxstyle='round,pad=0.1', ec='none', 

171 fc='white', alpha=0.8)) 

172 ax.set_aspect('equal') 

173 ax.xaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5)) 

174 ax.xaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in freqs])) 

175 ax.yaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5)) 

176 ax.yaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in reversed(freqs)])) 

177 ax.set_xlabel('EOD$f_i$ [Hz]') 

178 ax.set_ylabel('EOD$f_j$ [Hz]') 

179 ax.get_figure().colorbar(cma, ax=ax, label='$\\Delta f$ [Hz]') 

180 

181 

182def plot_freq_ratios(ax, freqs, cmap='seismic', fontsize='large'): 

183 """Plot matrix with frequency ratios. 

184 

185 Parameters 

186 ---------- 

187 ax: matplotlib axes 

188 Axes used for plotting. 

189 freqs: 1-D array of float 

190 List of frequencies. 

191 cmap: matplotlib color map 

192 Color map for coloring frequency ratios. 

193 fontsize: str or float 

194 Font size for labels indicating frequency ratios. 

195 """ 

196 ax.set_title('Ratios $f_1/f_0$') 

197 ratios = freq_ratios(freqs) 

198 cma = ax.pcolormesh(ratios[::-1, :], cmap=cmap, norm='log', 

199 vmin=1/5, vmax=5) 

200 for r in range(ratios.shape[0]): 

201 for c in range(ratios.shape[1]): 

202 ax.text(c + 0.5, r + 0.5, f'{ratios[-1 - r, c]:.3f}', 

203 ha='center', va='center', 

204 fontsize=fontsize, clip_on=True, 

205 bbox=dict(boxstyle='round,pad=0.1', ec='none', 

206 fc='white', alpha=0.8)) 

207 ax.set_aspect('equal') 

208 ax.xaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5)) 

209 ax.xaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in freqs])) 

210 ax.yaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5)) 

211 ax.yaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in reversed(freqs)])) 

212 ax.set_xlabel('EOD$f_i$ [Hz]') 

213 ax.set_ylabel('EOD$f_j$ [Hz]') 

214 ax.get_figure().colorbar(cma, ax=ax, label='Ratio', ticks=[1/5, 1/2, 1, 2, 5], format='%g') 

215 

216 

217def plot_musical_intervals(ax, freqs, cmap='YlOrRd_r', fontsize='large'): 

218 """Plot matrix with musical intervals. 

219 

220 Parameters 

221 ---------- 

222 ax: matplotlib axes 

223 Axes used for plotting. 

224 freqs: 1-D array of float 

225 List of frequencies. 

226 cmap: matplotlib color map 

227 Color map for coloring deviations from musical intervals. 

228 fontsize: str or float 

229 Font size for labels indicating musical intervals and 

230 deviations thereof. 

231 """ 

232 ax.set_title('Musical intervals') 

233 all_intervals = np.array([musical_intervals[k][0] for k in musical_intervals]) 

234 if len(freqs) < 6: 

235 all_names = list(musical_intervals.keys()) 

236 else: 

237 all_names = [musical_intervals_short[k] for k in musical_intervals] 

238 intervals, diffs, diff_fracs = freq_intervals(freqs) 

239 cma = ax.pcolormesh(100*np.abs(diff_fracs[::-1, :]), cmap=cmap, 

240 vmin=0, vmax=1) 

241 for r in range(intervals.shape[0]): 

242 for c in range(intervals.shape[1]): 

243 if -1 - r != c and intervals[-1 - r, c] >= 0: 

244 idx = intervals[-1 - r, c] 

245 if len(freqs) < 6: 

246 label = f'{all_intervals[idx]:.4f}\n{all_names[idx]}\n$\\Delta$={diffs[-1 - r, c]:.4f}\n{100*diff_fracs[-1 - r, c]:.1f}%' 

247 else: 

248 label = f'{all_names[idx]}\n{100*diff_fracs[-1 - r, c]:.1f}%' 

249 ax.text(c + 0.5, r + 0.5, label, 

250 ha='center', va='center', 

251 fontsize=fontsize, clip_on=True, 

252 bbox=dict(boxstyle='round,pad=0.1', ec='none', 

253 fc='white', alpha=0.8)) 

254 ax.set_aspect('equal') 

255 ax.xaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5)) 

256 ax.xaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in freqs])) 

257 ax.yaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5)) 

258 ax.yaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in reversed(freqs)])) 

259 ax.set_xlabel('EOD$f_i$ [Hz]') 

260 ax.set_ylabel('EOD$f_j$ [Hz]') 

261 ax.get_figure().colorbar(cma, ax=ax, extend='max', 

262 format=ticker.PercentFormatter(decimals=1), 

263 label='Deviation from musical interval')