"""Constant calculation."""
import numpy as np
import pyfar as pf
from typing import Literal
import warnings
[docs]
def air_attenuation(
temperature, frequencies, relative_humidity,
atmospheric_pressure=None):
r"""Calculate the pure tone attenuation of sound in air according to
ISO 9613-1.
Calculation is in accordance with ISO 9613-1 [#]_. The shape of the
outputs is broadcasted from the shapes of the ``temperature``,
``relative_humidity``, and ``atmospheric_pressure``.
The frequency bins represents the last dimension.
Parameters
----------
temperature : float, array_like
Temperature in °C.
Must be in the range of -20 °C to 50 °C for accuracy of +/-10% or
must be greater than -70 °C for accuracy of +/-50%.
frequencies : float, array_like
Frequency in Hz. Must be greater than 50 Hz.
Just one dimensional array is allowed.
relative_humidity : float, array_like
Relative humidity in the range from 0 to 1.
atmospheric_pressure : float, array_like, optional
Atmospheric pressure in Pascal, by default
:py:attr:`reference_atmospheric_pressure`.
Returns
-------
alpha : np.ndarray[float]
Pure tone air attenuation coefficient in dB/m for atmospheric
absorption.
m : :py:class:`~pyfar.FrequencyData`
Pure tone energy attenuation coefficient in 1/m for atmospheric
absorption. The parameter ``m`` is calculated as
:math:`m = \alpha / (10 \log_{10}(e))`.
accuracy : :py:class:`~pyfar.FrequencyData`
accuracy of the results according to the standard:
``10``, +/- 10% accuracy
- molar concentration of water vapour: 0.05% to 5%.
- air temperature: 253.15 K to 323.15 K (-20 °C to +50 °C)
- atmospheric pressure: less than 200 000 Pa (2 atm)
- frequency-to-pressure ratio: 0.0004 Hz/Pa to 10 Hz/Pa.
``20``, +/- 20% accuracy
- molar concentration of water vapour: 0.005% to 0.05%,
and greater than 5%
- air temperature: 253.15 K to 323.15 K (-20 °C to +50 °C)
- atmospheric pressure: less than 200 000 Pa (2 atm)
- frequency-to-pressure ratio: 0.0004 Hz/Pa to 10 Hz/Pa.
``50``, +/- 50% accuracy
- molar concentration of water vapour: less than 0.005%
- air temperature: greater than 200 K (- 73 °C)
- atmospheric pressure: less than 200 000 Pa (2 atm)
- frequency-to-pressure ratio: 0.0004 Hz/Pa to 10 Hz/Pa.
``-1``, no valid result
else.
References
----------
.. [#] ISO 9613-1:1993, Acoustics -- Attenuation of sound during
propagation outdoors -- Part 1: Calculation of the absorption of
sound by the atmosphere.
"""
if atmospheric_pressure is None:
atmospheric_pressure = pf.constants.reference_atmospheric_pressure
# check inputs
if not isinstance(temperature, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'temperature must be a number or array of numbers')
if not isinstance(frequencies, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'frequencies must be a number or array of numbers')
if not isinstance(
relative_humidity, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'relative_humidity must be a number or array of numbers')
if np.array(frequencies).ndim > 1:
raise ValueError('frequencies must be one dimensional.')
if not isinstance(
atmospheric_pressure, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'atmospheric_pressure must be a number or array of numbers')
# check if broadcastable
try:
_ = np.broadcast_shapes(
np.atleast_1d(temperature).shape,
np.atleast_1d(relative_humidity).shape,
np.atleast_1d(atmospheric_pressure).shape)
except ValueError as e:
raise ValueError(
'temperature, relative_humidity, and atmospheric_pressure must '
'have the same shape or be broadcastable.') from e
# check limits
if np.any(np.array(temperature) < -73):
raise ValueError("Temperature must be greater than -73°C.")
if np.any(np.array(frequencies) < 50):
raise ValueError("frequencies must be greater than 50 Hz.")
if np.any(np.array(relative_humidity) < 0) or np.any(
np.array(relative_humidity) > 1):
raise ValueError("Relative humidity must be between 0 and 1.")
if np.any(np.array(atmospheric_pressure) > 200000):
raise ValueError("Atmospheric pressure must be less than 200 kPa.")
# convert arrays
temperature = np.array(
temperature, dtype=float)[..., np.newaxis]
relative_humidity = np.array(
relative_humidity, dtype=float)[..., np.newaxis]
atmospheric_pressure = np.array(
atmospheric_pressure, dtype=float)[..., np.newaxis]
frequencies = np.array(frequencies, dtype=float)
# calculate air attenuation
p_atmospheric_ref = pf.constants.reference_atmospheric_pressure
t_degree_ref = pf.constants.reference_air_temperature_celsius
h_r = relative_humidity*100
p_a = atmospheric_pressure
p_r = p_atmospheric_ref
f = frequencies
T = temperature - pf.constants.absolute_zero_celsius
T_0 = t_degree_ref - pf.constants.absolute_zero_celsius
# saturation vapour pressure
p_sat = _saturation_vapour_pressure_iso(temperature)
# molar concentration of water vapor as a percentage (Equation B.1)
h = h_r * (p_sat / p_r) * (p_a / p_r)
# Oxygen relaxation frequency (Eq. 3)
f_rO = (p_a/p_r)*(24+4.04e4*h*(0.02+h)/(0.391+h))
# Nitrogen relaxation frequency (Eq. 4)
f_rN = (p_a/p_r)*(T/T_0)**(-1/2)*(9+280*h*np.exp(
-4.17*((T/T_0)**(-1/3)-1)))
# air attenuation (Eq. 5)
alpha = 8.686*f**2*((1.84e-11*p_r/p_a*(T/T_0)**(1/2)) + \
(T/T_0)**(-5/2)*(0.01275*np.exp(-2239.1/T)*(f_rO + (f**2/f_rO))**(-1)
+0.1068*np.exp(-3352/T) * (f_rN + (f**2/f_rN))**(-1)))
# Equation 3: ISO 17497-1:2004
m = pf.FrequencyData(
alpha / (10*np.log10(np.exp(1))), frequencies=frequencies)
# calculate accuracy
accuracy = _air_attenuation_accuracy(
h, temperature, atmospheric_pressure, frequencies, m.freq.shape)
return alpha, m, accuracy
def _air_attenuation_accuracy(
concentration_water_vapour, temperature, atmospheric_pressure,
frequencies, shape):
"""Calculate the accuracy of the air attenuation calculation.
Calculation is in accordance with ISO 9613-1 [#]_. This method is used in
:py:func:`~pyfar.constants.air_attenuation` to calculate the accuracy of
the air attenuation calculation.
Parameters
----------
concentration_water_vapour : float, array_like
Molar concentration of water vapor as a percentage.
Must be between 0% and 100%.
temperature : float, array_like
Temperature in degree Celsius.
Must be above -273.15°C.
atmospheric_pressure : float, array_like
Atmospheric pressure in Pascal.
Must be above 0Pa.
frequencies : float, array_like
Frequency in Hz.
Must be larger than 0 Hz.
shape : tuple
Shape of the output.
Returns
-------
accuracy : :py:class:`~pyfar.classes.FrequencyData`
accuracy of the results according to the standard:
``10``, +/- 10% accuracy
- molar concentration of water vapour: 0.05% to 5%.
- air temperature: 253.15 K to 323.15 K (-20 °C to +50 °C)
- atmospheric pressure: less than 200 000 Pa (2 atm)
- frequency-to-pressure ratio: 4 x 10-4 Hz/Pa to 10 Hz/Pa.
``20``, +/- 20% accuracy
- molar concentration of water vapour: 0.005% to 0.05%,
and greater than 5%
- air temperature: 253.15 K to 323.15 K (-20 °C to +50 °C)
- atmospheric pressure: less than 200 000 Pa (2 atm)
- frequency-to-pressure ratio: 4 x 10-4 Hz/Pa to 10 Hz/Pa.
``50``, +/- 50% accuracy
- molar concentration of water vapour: less than 0.005%
- air temperature: greater than 200 K (- 73 °C)
- atmospheric pressure: less than 200 000 Pa (2 atm)
- frequency-to-pressure ratio: 4 x 10-4 Hz/Pa to 10 Hz/Pa.
``-1``, no valid result
else.
References
----------
.. [#] ISO 9613-1:1993, Acoustics -- Attenuation of sound during
propagation outdoors -- Part 1: Calculation of the absorption of
sound by the atmosphere.
"""
if np.any(np.array(concentration_water_vapour) < 0) or np.any(
np.array(concentration_water_vapour) > 100):
raise ValueError(
r"Concentration of water vapour must be between 0% and 100%.")
if np.any(np.array(temperature) < -273.15):
raise ValueError(
"Temperature must be greater than -273.15°C.")
if np.any(np.array(atmospheric_pressure) < 0):
raise ValueError(
"Atmospheric pressure must be greater than 0 Pa.")
if np.any(np.array(frequencies) < 0):
raise ValueError(
"Frequencies must be positive.")
# broadcast inputs
atmospheric_pressure = np.broadcast_to(atmospheric_pressure, shape)
h_water_vapor = np.broadcast_to(concentration_water_vapour, shape)
frequency_pressure_ratio = frequencies/atmospheric_pressure
accuracy = np.zeros(shape) - 1
# atmospheric pressure < 200 kPa
atm_mask = atmospheric_pressure < 200000
# frequency-to-pressure ratio: 4 x 10-4 Hz/Pa to 10 Hz/Pa
frequency_pressure_ratio_mask = (4e-4 <= frequency_pressure_ratio) & (
frequency_pressure_ratio <= 10)
common_mask = atm_mask & frequency_pressure_ratio_mask
# molar concentration of water vapour: 0.05% to 5%
vapor_10_mask = (0.05 <= h_water_vapor) & (h_water_vapor <= 5)
# molar concentration of water vapour: 0.005% to 0.05% and greater than 5%
vapor_20_mask = (5 < h_water_vapor) | (
(0.005 <= h_water_vapor) & (h_water_vapor < 0.05))
# molar concentration of water vapour: less than 0.005%
vapor_50_mask = (0.005 > h_water_vapor)
# air temperature: 253,15 K to 323,15 (-20 °C to +50°C)
temp_20_mask = (-20 <= temperature) & (temperature <= 50)
# air temperature: greater than 200 K (- 73 °C)
temp_50_mask = (-73 <= temperature)
# apply masks
accuracy_50 = common_mask & temp_50_mask & (
vapor_10_mask | vapor_20_mask | vapor_50_mask)
accuracy[accuracy_50] = 50
accuracy_20 = common_mask & temp_20_mask & (
vapor_10_mask | vapor_20_mask)
accuracy[accuracy_20] = 20
accuracy_10 = vapor_10_mask & common_mask & temp_20_mask
accuracy[accuracy_10] = 10
# return FrequencyData object
return pf.FrequencyData(accuracy, frequencies=frequencies)
def _saturation_vapour_pressure_iso(temperature):
"""Calculates the saturation vapour pressure after ISO 9613-1:1993.
This method is used in the :py:func:`air_attenuation` function to calculate
the saturation vapour pressure of water vapour in air as a close
approximation to
those calculated by the World Meteorological Organization.
The method is described in Equation B.2 and B.3 in [#]_.
Parameters
----------
temperature : float, array_like
Temperature in degree Celsius.
Must be in the range of -20°C to 50°C for accuracy of +/-10% or
must be greater than -70°C for accuracy of +/-50%.
Returns
-------
p_sat : float, array_like
Saturation vapour pressure in Pascal.
References
----------
.. [#] ISO 9613-1:1993, Acoustics -- Attenuation of sound during
propagation outdoors -- Part 1: Calculation of the absorption of
sound by the atmosphere.
"""
T = temperature - pf.constants.absolute_zero_celsius
p_r = pf.constants.reference_atmospheric_pressure
# triple-point isotherm temperature of 273.16 K
T_01 = 273.16
# saturation vapour pressure (Equation B.2 and B.3)
C = -6.8346*(T_01/T)**1.261+4.6151
return 10**C * p_r
[docs]
def saturation_vapor_pressure_magnus(temperature):
r"""
Calculate the saturation vapor pressure of water in Pascal using the
Magnus formula.
The Magnus formula is valid for temperatures between -45°C and 60°C [#]_.
.. math::
e_s = 610.94 \cdot \exp\left(\frac{17.625 \cdot T}{T + 243.04}\right)
Parameters
----------
temperature : float, array_like
Temperature in degrees Celsius (°C).
Returns
-------
p_sat : float, array_like
Saturation vapor pressure in Pa.
References
----------
.. [#] O. A. Alduchov and R. E. Eskridge, “Improved Magnus Form
Approximation of Saturation Vapor Pressure,” Journal of Applied
Meteorology and Climatology, vol. 35, no. 4, pp. 60-609, Apr. 1996
"""
if not isinstance(temperature, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'temperature must be a number or array of numbers')
if np.any(np.array(
temperature) < -45) or np.any(np.array(temperature) > 60):
raise ValueError("Temperature must be in the range of -45°C and 60°C.")
if isinstance(temperature, (np.ndarray, list, tuple)):
temperature = np.asarray(temperature, dtype=float)
# Eq. (21)
e_s = 6.1094 * np.exp((17.625 * temperature) / (temperature + 243.04))
return 100 * e_s
[docs]
def density_of_air(
temperature, relative_humidity, atmospheric_pressure=None,
saturation_vapor_pressure=None):
r"""
Calculate the density of air in kg/m³ based on the temperature,
relative humidity, and atmospheric pressure.
The density of air is calculated based on chapter 6.3 in [#]_.
All input parameters must be broadcastable to the same shape.
Parameters
----------
temperature : float, array_like
Temperature in degrees Celsius (°C).
relative_humidity : float, array_like
Relative humidity in the range from 0 to 1.
atmospheric_pressure : float, array_like, optional
Atmospheric pressure in Pascal (Pa), by default
:py:attr:`reference_atmospheric_pressure`.
saturation_vapor_pressure : float, array_like, optional
Saturation vapor pressure in Pascal (Pa).
The default uses the value and valid temperature range from
:py:func:`~pyfar.constants.saturation_vapor_pressure_magnus`.
Returns
-------
density : float, array_like
Density of air in kg/m³.
References
----------
.. [#] V. E. Ostashev and D. K. Wilson, Acoustics in Moving Inhomogeneous
Media, 2nd ed. London: CRC Press, 2015. doi: 10.1201/b18922.
"""
if atmospheric_pressure is None:
atmospheric_pressure = pf.constants.reference_atmospheric_pressure
# check inputs
if not isinstance(temperature, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'Temperature must be a number or array of numbers')
if not isinstance(
relative_humidity, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'Relative humidity must be a number or array of numbers')
if not isinstance(
atmospheric_pressure, (int, float, np.ndarray, list, tuple)):
raise TypeError(
'Atmospheric pressure must be a number or array of numbers')
temperature = np.array(temperature, dtype=float)
if np.any(np.array(relative_humidity) < 0) or np.any(
np.array(relative_humidity) > 1):
raise ValueError("Relative humidity must be between 0 and 1.")
if np.any(np.array(atmospheric_pressure) <= 0):
raise ValueError("Atmospheric pressure must be larger than 0 Pa.")
P = np.array(atmospheric_pressure, dtype=float) # Pa
relative_humidity = np.array(relative_humidity, dtype=float)
# Constants according to 6.3.2
R = 8.314 # J/(K mol)
mu_a = 28.97*1e-3 # kg/mol
mu_w = 18.02*1e-3 # kg/mol
# next to Equation 6.69
R_a = R / mu_a
alpha = mu_a / mu_w
# partial pressure of water vapor in Pa
if saturation_vapor_pressure is None:
p = saturation_vapor_pressure_magnus(temperature) # Pa
else:
p = np.array(saturation_vapor_pressure, dtype=float) # Pa
e = relative_humidity * p # Pa
# Equation 6.70
C = (e/P) / (alpha * (1 - e/P))
# Equation 6.71
return P / (R_a * (temperature + 273.15)) * (1+C)/(1+alpha*C)
[docs]
def fractional_octave_filter_tolerance(
exact_center_frequency: float,
num_fractions: Literal[1, 3],
tolerance_class : Literal[1, 2]):
r"""
Calculate the tolerance limits for fractional octave band filters.
Calculation is in accordance with IEC 61260-1:2014 [#]_ (Section 5.10 and
Table 1).
.. note ::
The standard defines some lower tolerance limits as :math:`-\infty`,
which is inconvenient for plotting. The returned tolerance is -60000 dB
in these cases, which is below the smallest possible value of
``20*np.log10(np.finfo(float).tiny``
:math:`\approx`-6000 dB.
Parameters
----------
exact_center_frequency : float
The exact center frequency of the band filter in Hz (see
:py:func:`~pyfar.dsp.filter.fractional_octave_frequencies`).
num_fractions : Literal[1, 3]
The number of bands an octave is divided into. ``1`` for octave bands
and ``3`` for third octave bands.
tolerance_class : Literal[1, 2]
The tolerance class as defined in the standard. Must be ``1`` or ``2``.
Returns
-------
lower_tolerance : numpy array
Lower tolerance limits in dB of shape (19, ).
upper_tolerance : numpy array
Upper tolerance limits in dB of shape (19, ).
frequencies : numpy array
The frequencies in Hz at which the tolerance is given of shape (19, ).
References
----------
.. [#] IEC61260-1:2014 Octave-band and fractional-octave-band filters.
Part 1: Specifications.
Examples
--------
Class 1 tolerance region and filter for the 1000 Hz octave band.
.. plot::
>>> import pyfar as pf
>>> import matplotlib.pyplot as plt
>>>
>>> lower, upper, frequencies = \
... pf.constants.fractional_octave_filter_tolerance(
... exact_center_frequency=1000, num_fractions=1,
... tolerance_class=1)
>>>
>>> octave_filter = pf.dsp.filter.fractional_octave_bands(
... pf.signals.impulse(2**12), num_fractions=1,
... frequency_range=(1000, 1000))
>>>
>>> ax = pf.plot.freq(octave_filter, color='k', label='Octave filter')
>>> plt.fill_between(
... frequencies, lower, upper,
... facecolor='g', alpha=.25, label='Class 1 Tolerance')
>>> ax.set_ylim(-70, 5)
>>> ax.set_xlim(63, 15_850)
>>> ax.legend()
"""
if not isinstance(exact_center_frequency, (int, float)):
raise ValueError('The exact center frequency must be a float '
'or integer number')
if num_fractions not in [1, 3]:
raise ValueError(
f"num_fractions is {num_fractions} but must be 1 or 3")
if tolerance_class not in [1, 2]:
raise ValueError(
f"tolerance_class is {tolerance_class} but must be 1 or 2")
# constants from DIN EN 61260-1:2014-10, Sec. 5.10 and Tab. 1
# (define only first half due to symmetry)
G = 10**(3/10) # octave ratio according to Eq. (1)
freq_offset = 1e-6 # small frequency offset to model discontinuities in
# tolerance curve
min_dB = -60e3 # dB value to be used instead of minus infinity
# (number required for plots)
relative_frequencies = [-4, -3, -2, -1, -0.5-freq_offset, -0.5+freq_offset,
-3/8, -1/4, -1/8, 0]
if tolerance_class == 1:
upper_tolerance = [
-70, -60, -40.5, -16.6, -1.2, 0.4, 0.4, 0.4, 0.4, 0.4]
lower_tolerance = [min_dB, min_dB, min_dB, min_dB, min_dB, -5.3,
-1.4, -0.7, -0.5, -0.4]
else:
upper_tolerance = [
-60, -54, -39.5, -15.6, -0.8, 0.6, 0.6, 0.6, 0.6, 0.6]
lower_tolerance = [min_dB, min_dB, min_dB, min_dB, min_dB, -5.8,
-1.7, -0.9, -0.7, -0.6]
relative_frequencies = np.hstack((
relative_frequencies, -np.flip(relative_frequencies[:-1])))
upper_tolerance = np.hstack((
upper_tolerance, np.flip(upper_tolerance[:-1])))
lower_tolerance = np.hstack((
lower_tolerance, np.flip(lower_tolerance[:-1])))
# scale frequencies to bandwidth
relative_frequencies /= num_fractions
frequencies = exact_center_frequency * G**relative_frequencies
return lower_tolerance, upper_tolerance, frequencies
[docs]
def fractional_octave_frequencies_nominal(num_fractions:Literal[1,3]=1,
frequency_range:tuple[float, float]=(20, 20e3)):
"""Return the nominal center frequencies for octave-band and
one-third-octave-band filters.
Nominal center frequencies, as specified in the IEC 61260-1:2014 standard
[#]_ (Section 5.5 and Annex E), are standardized values that approximate
the exact center frequencies. They are defined from 10 Hz to 20 kHz.
Parameters
----------
num_fractions : {1, 3}
The number of octave fractions. ``1`` returns octave center
frequencies, ``3`` returns third-octave center frequencies.
The default is ``1``.
frequency_range : array, tuple
The lower and upper frequency limits, the default is
``(20, 20e3)`` following IEC 61260-1.
E.g. ``(10, 20e3)`` would follow IEC 61672-1 [#]_.
Returns
-------
nominal : numpy.ndarray of float
The nominal center frequencies.
Notes
-----
The specified ``frequency_range`` is interpreted as frequencies lying
within (fractional) octave bands defined by their cutoff frequencies
(not their center frequencies). All bands that overlap with the
specified frequency range are returned.
References
----------
.. [#] International Electrotechnical Commission, "IEC 61260-1:2014 -
Electroacoustics - Octave-band and fractional-octave-band filters -
Part 1: Specifications", IEC, 2014.
.. [#] International Electrotechnical Commission,
"IEC 61672-1:2013 - Electroacoustics - Sound level meters - Part 1:
Specifications", IEC, 2013.
"""
# IEC 61260-1 Eq. (1)
G = 10**(3/10)
f_lims = np.asarray(frequency_range)
if f_lims.size != 2:
raise ValueError(
"You need to specify a lower and upper limit frequency.")
if f_lims[0] > f_lims[1]:
raise ValueError(
"The second frequency needs to be higher than the first.")
_, lower, upper = fractional_octave_frequencies_exact(num_fractions,
frequency_range)
if num_fractions == 1:
if (f_lims[0] < 15.8*G**(-1/2)) or (f_lims[1] >
15848.93192*G**(1/2)):
warnings.warn(
"The nominal center frequencies for octave-band " \
"are defined only from 11.2 Hz to 22387.2 Hz.", UserWarning,
stacklevel=2)
nominal = np.array([
16, 31.5, 63, 125, 250, 500, 1e3,
2e3, 4e3, 8e3, 16e3], dtype=float)
elif num_fractions == 3:
if (f_lims[0] < 10*G**(-1/6)) or (f_lims[1] > 19952.62315*G**(1/6)):
warnings.warn(
"The nominal center frequencies for one-third-octave-band " \
"are defined only from 8.91 Hz to 22387.2 Hz.", UserWarning,
stacklevel=2)
nominal = np.array([
10, 12.5, 16, 20, 25, 31.5, 40, 50, 63, 80,
100, 125, 160, 200, 250, 315, 400, 500, 630,
800, 1000, 1250, 1600, 2000, 2500, 3150, 4000,
5000, 6300, 8000, 10000, 12500, 16000, 20000], dtype=float)
else:
raise ValueError('num_fractions must be 1 or 3')
mask = (nominal >= lower[0]) & (nominal <= upper[-1])
nominal = nominal[mask]
return nominal
[docs]
def fractional_octave_frequencies_exact(
num_fractions:int=1, frequency_range: tuple[float, float]=(20, 20e3)):
r"""Return the exact center and cutoff frequencies for
fractional-octave-band filters.
The frequencies are calculated in accordance with the IEC 61260-1:2014
standard [#]_ (Sections 5.2, 5.3, 5.4 and 5.6).
The octave frequency ratio, :math:`G`, is given by the following
expression.
.. math::
G = 10^{\tfrac{3}{10}}
The center frequencies :math:`f_m` are calculated using formula
:eq:`eq_center_odd` for odd values of :math:`b` and formula
:eq:`eq_center_even` for even values of :math:`b`.
.. math::
:label: eq_center_odd
f_m = f_r \cdot G^{ \tfrac{x}{b}}
.. math::
:label: eq_center_even
f_m = f_r \cdot G^{ \tfrac{2x+1}{2b}}
where:
- :math:`b` is the number of octave fractions.
- :math:`f_r` is the reference frequency, set to 1000 Hz.
- :math:`x` is the index of the frequency band.
Parameters
----------
num_fractions : int, optional
The number of bands an octave is divided into. E.g., ``1`` refers to
octave bands and ``3`` to third octave bands. The default is ``1``.
All positive integers are allowed.
frequency_range : array, tuple
The lower and upper frequency limits, the default is
``(20, 20e3)``.
Returns
-------
center_frequencies : numpy.ndarray
The exact center frequencies in Hz of the bandpass filters
for each fractional octave band.
lower_cutoff_frequencies : numpy.ndarray
The lower cutoff frequencies in Hz of the bandpass filters
for each fractional octave band.
upper_cutoff_frequencies : numpy.ndarray
The upper cutoff frequencies in Hz of the bandpass filters
for each fractional octave band.
References
----------
.. [#] International Electrotechnical Commission, "IEC 61260-1:2014 -
Electroacoustics - Octave-band and fractional-octave-band filters -
Part 1: Specifications", IEC, 2014.
Notes
-----
The specified ``frequency_range`` is interpreted as frequencies lying
within (fractional) octave bands defined by their cutoff frequencies
(not their center frequencies). All bands that overlap with the
specified frequency range are returned.
"""
if not isinstance(frequency_range, (tuple, np.ndarray, list)):
raise TypeError("The frequency range must be a tuple, list or"
" np.ndarray of float or integer values.")
if not isinstance(num_fractions, (int)):
raise TypeError("Number of fractions must be an integer.")
if not all(isinstance(f, (int, float)) for f in frequency_range):
raise TypeError("The frequency range must contain only integer"
" or float values.")
f_lims = np.asarray(frequency_range)
if f_lims.size != 2:
raise ValueError(
"The frequency range must contain exactly two values.")
if f_lims[0] > f_lims[1]:
raise ValueError(
"The upper frequency must be greater than the lower frequency.")
if f_lims[0] <=0 or f_lims[1]<=0:
raise ValueError(
"The frequencies must be positive numbers.")
if num_fractions <= 0:
raise ValueError(
"Number of fractions must be a positive number.")
# IEC 61260-1 Eq. (1)
G = 10**(3/10)
ref_freq = 1e3
if num_fractions % 2 != 0:
Nmax = np.rint((10/3)*np.log10(frequency_range[1]/ref_freq)*
num_fractions)
Nmin = np.rint((10/3)*np.log10(frequency_range[0]/ref_freq)*
num_fractions)
indices = np.arange(Nmin, Nmax+1)
# IEC 61260-1 Eq. (2)
center_frequencies = ref_freq * (G)**(indices / num_fractions)
else:
Nmax = np.rint(((10/6)*np.log10(frequency_range[1]/ref_freq)*2*
num_fractions - 1/2))
Nmin = np.rint(((10/6)*np.log10(frequency_range[0]/ref_freq)*2*
num_fractions - 1/2))
indices = np.arange(Nmin, Nmax+1)
# IEC 61260-1 Eq. (3)
center_frequencies = ref_freq * (G)**((2*indices + 1)/
(2*num_fractions))
# IEC 61260-1 Eq. (5)
upper_cutoff_frequencies = center_frequencies * G**(1/2/num_fractions)
# IEC 61260-1 Eq. (4)
lower_cutoff_frequencies = center_frequencies * G**(-1/2/num_fractions)
return (center_frequencies,
lower_cutoff_frequencies, upper_cutoff_frequencies)