Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mth5 \ mth5 \ processing \ spectre \ frequency_band_helpers.py: 94%

71 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-10 00:01 -0800

1""" 

2Module for tools for create and manage frequency bands. 

3 

4Bands can be defined by explicitly specifying band edges for each band, but here are some convenience 

5functions for other ways to specify. 

6""" 

7 

8from typing import Optional, Union 

9 

10import numpy as np 

11import pandas as pd 

12from loguru import logger 

13from mt_metadata.common.band import Band as FrequencyBand 

14from mt_metadata.processing.aurora import FrequencyBands 

15 

16 

17def half_octave( 

18 target_frequency: float, fft_frequencies: Optional[np.ndarray] = None 

19) -> FrequencyBand: 

20 """ 

21 

22 Create a half-octave wide frequency band object centered at target frequency. 

23 

24 :type target_frequency: float 

25 :param target_frequency: The center frequency (geometric) of the band 

26 :type fft_frequencies: Optional[np.ndarray] 

27 :param fft_frequencies: (array-like) Frequencies associated with an instance of a spectrogram. 

28 If provided, the indices of the spectrogram associated with the band will be stored in the 

29 Band object. 

30 :rtype band: mt_metadata.common.band.Band 

31 :return band: FrequencyBand object with lower and upper bounds. 

32 

33 """ 

34 h = 2**0.25 

35 f1 = target_frequency / h 

36 f2 = target_frequency * h 

37 band = FrequencyBand(frequency_min=f1, frequency_max=f2) 

38 

39 if fft_frequencies is not None: 

40 band.set_indices_from_frequencies(fft_frequencies) 

41 

42 return band 

43 

44 

45def log_spaced_frequencies( 

46 f_lower_bound: float, 

47 f_upper_bound: float, 

48 num_bands: Optional[int] = None, 

49 num_bands_per_decade: Optional[float] = None, 

50 num_bands_per_octave: Optional[float] = None, 

51): 

52 """ 

53 Convenience function for generating logarithmically spaced fenceposts running 

54 from f_lower_bound Hz to f_upper_bound Hz. 

55 

56 These can be taken, two at a time as band edges, or used as band centers with 

57 a constant Q scheme. This is basically the same as np.logspace, but allows 

58 for specification of frequencies in Hz. 

59 

60 Note in passing, replacing np.exp with 10** and np.log with np.log10 yields same base. 

61 

62 Parameters 

63 ---------- 

64 f_lower_bound : float 

65 lowest frequency under consideration 

66 f_upper_bound : float 

67 highest frequency under consideration 

68 num_bands : int 

69 Total number of bands. Note that `num_bands` is one fewer than the number 

70 of frequencies returned (gates and fenceposts). 

71 num_bands_per_decade : int (TODO test, float maybe ok also.. need to test) 

72 number of bands per decade. 8 is a nice choice. 

73 num_bands : int 

74 total number of bands. This supercedes num_bands_per_decade if supplied 

75 

76 Returns 

77 ------- 

78 frequencies : array 

79 logarithmically spaced fence posts acoss lowest and highest 

80 frequencies. These partition the frequency domain between 

81 f_lower_bound and f_upper_bound 

82 """ 

83 band_spacing_method = None 

84 if num_bands: 

85 msg = ( 

86 f"generating {num_bands} log-spaced frequencies in range " 

87 f"{f_lower_bound}-{f_upper_bound} Hz" 

88 ) 

89 logger.info(msg) 

90 band_spacing_method = "geometric" 

91 

92 if num_bands_per_decade: 

93 if band_spacing_method is not None: 

94 msg = f"band_spacing_method already set to {band_spacing_method}" 

95 msg += "Please specify only one of num_bands_per_decade, num_bands_per_octave, num_bands" 

96 logger.error(msg) 

97 raise ValueError(msg) 

98 else: 

99 msg = ( 

100 f"generating {num_bands_per_decade} log-spaced frequency bands per decade in range " 

101 f"{f_lower_bound}-{f_upper_bound} Hz" 

102 ) 

103 logger.info(msg) 

104 number_of_decades = np.log10(f_upper_bound / f_lower_bound) 

105 num_bands = round(number_of_decades * num_bands_per_decade) 

106 band_spacing_method = "bands per decade" 

107 

108 if num_bands_per_octave: 

109 if band_spacing_method is not None: 

110 msg = f"band_spacing_method already set to {band_spacing_method}" 

111 msg += "Please specify only one of num_bands_per_decade, num_bands_per_octave, num_bands" 

112 logger.error(msg) 

113 raise ValueError(msg) 

114 else: 

115 msg = ( 

116 f"generating {num_bands_per_octave} log-spaced frequency bands per octave in range " 

117 f"{f_lower_bound}-{f_upper_bound} Hz" 

118 ) 

119 logger.info(msg) 

120 number_of_octaves = np.log2(f_upper_bound / f_lower_bound) 

121 num_bands = round(number_of_octaves * num_bands_per_octave) 

122 

123 base = np.exp((1.0 / num_bands) * np.log(f_upper_bound / f_lower_bound)) 

124 bases = base * np.ones(num_bands + 1) 

125 exponents = np.linspace(0, num_bands, num_bands + 1) 

126 frequencies = f_lower_bound * (bases**exponents) 

127 

128 return frequencies 

129 

130 

131def bands_of_constant_q( 

132 band_center_frequencies: np.ndarray, 

133 q: Optional[float] = None, 

134 fractional_bandwidth: Optional[float] = None, 

135) -> FrequencyBands: 

136 """ 

137 Generate frequency bands centered at band_center_frequencies. 

138 These bands have Q = f_center/delta_f = constant. 

139 Normally f_center is defined geometrically, i.e. sqrt(f2*f1) is the center freq between f1 and f2. 

140 

141 Parameters 

142 ---------- 

143 band_center_frequencies: np.ndarray 

144 The center frequencies for the bands 

145 q: float 

146 Q = f_center/delta_f = constant. 

147 Q is 1/fractional_bandwidth. 

148 Q is nonsene when less than 1, just as fractional bandwidth is nonsense when greater than 1. 

149 - Upper case Q is used in the literature 

150 See 

151 - https://en.wikipedia.org/wiki/Bandwidth_(signal_processing)#Fractional_bandwidth 

152 - https://en.wikipedia.org/wiki/Q_factor 

153 

154 Returns 

155 ------- 

156 frequency_bands: FrequencyBands 

157 Frequency bands object with bands packed inside. 

158 """ 

159 if fractional_bandwidth is None: 

160 if q is None: 

161 msg = "must specify one of Q or fractional_bandwidth" 

162 raise ValueError(msg) 

163 fractional_bandwidth = 1.0 / q 

164 

165 num_bands = len(band_center_frequencies) 

166 lower_bounds = np.full(num_bands, np.nan) 

167 upper_bounds = np.full(num_bands, np.nan) 

168 for i, frq in enumerate(band_center_frequencies): 

169 delta_f = ( 

170 frq * fractional_bandwidth 

171 ) / 2 # halved because 2*delta_f is bandwidth 

172 # delta_f = frq / Q 

173 lower_bounds[i] = frq - delta_f 

174 upper_bounds[i] = frq + delta_f 

175 

176 band_edges_df = pd.DataFrame( 

177 data={ 

178 "lower_bound": lower_bounds, 

179 "upper_bound": upper_bounds, 

180 } 

181 ) 

182 frequency_bands = FrequencyBands(band_edges=band_edges_df) 

183 return frequency_bands 

184 

185 

186def partitioned_bands(frequencies: Union[np.ndarray, list]) -> FrequencyBands: 

187 """ 

188 Takes ordered list of frequencies and returns 

189 a FrequencyBands object 

190 Returns 

191 ------- 

192 

193 """ 

194 num_bands = len(frequencies) - 1 # gates and fenceposts 

195 lower_bounds = num_bands * [None] 

196 upper_bounds = num_bands * [None] 

197 lower_bounds = frequencies[:-1] 

198 upper_bounds = frequencies[1:] 

199 band_edges_df = pd.DataFrame( 

200 data={ 

201 "lower_bound": lower_bounds, 

202 "upper_bound": upper_bounds, 

203 } 

204 ) 

205 frequency_bands = FrequencyBands(band_edges=band_edges_df) 

206 return frequency_bands 

207 

208 

209# class FrequencyBandsCreator(): 

210# """ 

211# This class can generate FrequencyBands objects based on parametric methods. 

212# 

213# The most common will be to make logarithmically spaced bands with constant Q. 

214# These may be half-octave 

215# """ 

216# def __init__(self): 

217# pass 

218# 

219# def main(): 

220# measure_q() 

221# tst_constant_q() 

222# 

223# 

224# 

225# if __name__ == "__main__": 

226# main()