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
« 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.
4Bands can be defined by explicitly specifying band edges for each band, but here are some convenience
5functions for other ways to specify.
6"""
8from typing import Optional, Union
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
17def half_octave(
18 target_frequency: float, fft_frequencies: Optional[np.ndarray] = None
19) -> FrequencyBand:
20 """
22 Create a half-octave wide frequency band object centered at target frequency.
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.
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)
39 if fft_frequencies is not None:
40 band.set_indices_from_frequencies(fft_frequencies)
42 return band
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.
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.
60 Note in passing, replacing np.exp with 10** and np.log with np.log10 yields same base.
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
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"
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"
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)
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)
128 return frequencies
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.
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
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
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
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
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 -------
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
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()