Alternative Smoothing: transformed to matrix method, minor bug fixes, merged test scripts
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Casper Jansen 2023-02-23 16:33:41 +01:00
parent 1b46616607
commit 5caddec583

View File

@ -5,13 +5,10 @@ Author: C. Jansen, J.A. de Jong - ASCEE V.O.F.
Smooth data in the frequency domain. Smooth data in the frequency domain.
TODO: This function is rather slow as it TODO: This function is rather slow as it uses [for loops] in Python. Speed up.
used Python for loops. The implementations should be speed up in the near NOTE: function requires lin frequency spaced input data
future. TODO: accept input data that is not lin spaced in frequency
TODO: check if the smoothing is correct: depending on whether the data points TODO: it makes more sense to output data that is log spaced in frequency
are spaced lin of log in frequency, they should be given different weights.
TODO: accept data that is not equally spaced in frequency
TODO: output data that is log spaced in frequency
Cutoff frequencies of window taken from Cutoff frequencies of window taken from
http://www.huennebeck-online.de/software/download/src/index.html 15-10-2021 http://www.huennebeck-online.de/software/download/src/index.html 15-10-2021
@ -30,14 +27,13 @@ where b = 3 for 1/3rd octave, f = frequency, fm = mid-band frequency, g = gain.
Gain is related to magnitude; power is related to gain^2 Gain is related to magnitude; power is related to gain^2
""" """
__all__ = ['SmoothingType', 'smoothSpectralData', 'SmoothingWidth', 'smoothSpectralDataAlt', 'intHann'] __all__ = ['SmoothingType', 'smoothSpectralData', 'SmoothingWidth']
from enum import Enum, unique from enum import Enum, unique
import bisect import bisect
import copy import copy
import numpy as np import numpy as np
from math import ceil, floor, sin
from numpy import arange, log2, log10, pi
@unique @unique
class SmoothingWidth(Enum): class SmoothingWidth(Enum):
@ -95,14 +91,17 @@ def smoothCalcMatrix(freq, sw: SmoothingWidth):
tr = 2 # truncate window after 2x std; shorter is faster and less accurate tr = 2 # truncate window after 2x std; shorter is faster and less accurate
Noct = sw.value[0] Noct = sw.value[0]
assert Noct > 0, "'Noct' must be absolute positive" assert Noct > 0, "'Noct' must be absolute positive"
assert np.isclose(freq[-1]-freq[-2], freq[1]-freq[0]), "Input data must "\
"have a linear frequency spacing"
if Noct < 1: if Noct < 1:
raise Warning('Check if \'Noct\' is entered correctly') raise Warning('Check if \'Noct\' is entered correctly')
# Initialize # Initialize
L = len(freq) L = len(freq)
Q = np.zeros(shape=(L, L), dtype=np.float16) # float16: keep size small Q = np.zeros(shape=(L, L), dtype=np.float16) # float16: keep size small
Q[0, 0] = 1 # in case first point is skipped
x0 = 1 if freq[0] == 0 else 0 # Skip first data point if zero frequency x0 = 1 if freq[0] == 0 else 0 # Skip first data point if zero frequency
# Loop over indices of raw frequency vector # Loop over indices of raw frequency vector
for x in range(x0, L): for x in range(x0, L):
# Find indices of data points to calculate current (smoothed) magnitude # Find indices of data points to calculate current (smoothed) magnitude
@ -129,9 +128,9 @@ def smoothCalcMatrix(freq, sw: SmoothingWidth):
# Find indices corresponding to frequencies # Find indices corresponding to frequencies
xl = bisect.bisect_left(freq, fl) # index corresponding to fl xl = bisect.bisect_left(freq, fl) # index corresponding to fl
xu = bisect.bisect_left(freq, fu) xu = bisect.bisect_right(freq, fu)
xl = xu-1 if xu-xl <= 0 else xl # Guarantee window length of at least 1 xl = xu-1 if xu-xl <= 0 else xl # Guarantee window length >= 1
# Calculate window # Calculate window
xg = np.arange(xl, xu) # indices xg = np.arange(xl, xu) # indices
@ -140,6 +139,10 @@ def smoothCalcMatrix(freq, sw: SmoothingWidth):
gs /= np.sum(gs) # normalize: integral=1 gs /= np.sum(gs) # normalize: integral=1
Q[x, xl:xu] = gs # add to matrix Q[x, xl:xu] = gs # add to matrix
# Normalize to conserve input power
Qpower = np.sum(Q, axis=0)
Q = Q / Qpower[np.newaxis, :]
return Q return Q
@ -154,7 +157,7 @@ def smoothSpectralData(freq, M, sw: SmoothingWidth,
fractional octave smoothing is related to log spaced data. In this fractional octave smoothing is related to log spaced data. In this
implementation, the window extends with a fixed frequency step to either implementation, the window extends with a fixed frequency step to either
side. The deviation is largest when Noct is small (e.g. coarse smoothing). side. The deviation is largest when Noct is small (e.g. coarse smoothing).
Casper Jansen, 07-05-2021 07-05-2021
Update 16-01-2023: speed up algorithm Update 16-01-2023: speed up algorithm
- Smoothing is performed using matrix multiplication - Smoothing is performed using matrix multiplication
@ -227,63 +230,10 @@ def smoothSpectralData(freq, M, sw: SmoothingWidth,
return Psm return Psm
# %% Alternative algorithm
from numpy import arange, log2, log10, pi, ceil, floor, sin
def smoothSpectralDataAlt(freq, Mdb, sw: SmoothingWidth): # Integrated Hann window
Noct = 1/sw.value[0]
# M = Mdb
M = 10**(Mdb/20)
f0 = 0
if freq[0] == 0:
f0 += 1
Nfreq = len(freq)
test_smoothed = np.array(M)
ifreq = freq/(freq[1]-freq[0])
ifreq = np.array(ifreq.astype(int))
ifreqMin = ifreq[f0]
ifreqMax = ifreq[Nfreq-1]
sfact = 2**(Noct/2)
maxNkp = ifreqMax - floor((ifreqMax-1)/sfact**2)+1
W = np.zeros(maxNkp)
kpmin = np.floor(ifreq/sfact).astype(int)
kpmax = np.ceil(ifreq*sfact).astype(int)
for ff in range(f0, len(M)):
if kpmin[ff] < ifreqMin:
kpmin[ff] = ifreqMin
kpmax[ff] = ceil(ifreq[ff]**2/ifreqMin)
NoctAct = log2(kpmax[ff]/kpmin[ff])
elif kpmax[ff] > ifreqMax:
kpmin[ff] = floor(ifreq[ff]**2/ifreqMax)
kpmax[ff] = ifreqMax
NoctAct = log2(kpmax[ff]/kpmin[ff])
else:
NoctAct = Noct
kp = arange(kpmin[ff], kpmax[ff]+1)
if NoctAct:
Phi1 = log2((kp - 0.5)/ifreq[ff])/NoctAct
Phi2 = log2((kp + 0.5)/ifreq[ff])/NoctAct
for ii in range(len(kp)):
W[ii] = intHann(Phi1[ii], Phi2[ii])
test_smoothed[ff] = np.dot(M[kpmin[ff]-ifreq[0]:kpmax[ff]-ifreq[0]+1], W[:ii+1])
test_smoothed = 20*log10(test_smoothed)
return test_smoothed
# %% Integrated Hann window
def intHann(x1, x2): def intHann(x1, x2):
if (x2 <= -1/2) or (x1 >= 1/2): if (x2 <= -1/2) or (x1 >= 1/2):
return 0 return 0
@ -299,6 +249,242 @@ def intHann(x1, x2):
return (sin(2*pi*x2) - sin(2*pi*x1))/(2*pi) + (x2-x1) return (sin(2*pi*x2) - sin(2*pi*x1))/(2*pi) + (x2-x1)
def smoothSpectralDataAlt(freq, MdB, sw: SmoothingWidth,
st: SmoothingType = SmoothingType.levels):
"""
According to Tylka_JAES_SmoothingWeights.pdf
"A Generalized Method for Fractional-Octave Smoothing of Transfer Functions
that Preserves Log-Frequency Symmetry"
https://doi.org/10.17743/jaes.2016.0053
par 1.3
eq. 16
"""
Noct = 1/sw.value[0]
# M = MdB
M = 10**(MdB/20)
f0 = 0
if freq[0] == 0:
f0 += 1
Nfreq = len(freq) # Number of frequenties
test_smoothed = np.array(M) # Input [Power]
ifreq = freq/(freq[1]-freq[0]) # Frequency, normalized to step=1
ifreq = np.array(ifreq.astype(int))
ifreqMin = ifreq[f0] # Min. freq, normalized to step=1
ifreqMax = ifreq[Nfreq-1] # Max. freq, normalized to step=1
sfact = 2**(Noct/2) # bounds are this factor from the center freq
maxNkp = ifreqMax - floor((ifreqMax-1)/sfact**2)+1
# W = np.zeros(int(np.round(maxNkp)))
kpmin = np.floor(ifreq/sfact).astype(int) # min freq of window
kpmax = np.ceil(ifreq*sfact).astype(int) # max freq of window
for ff in range(f0, len(M)): # loop over input freq
if kpmin[ff] < ifreqMin:
kpmin[ff] = ifreqMin
kpmax[ff] = ceil(ifreq[ff]**2/ifreqMin) # achieved Noct
if np.isclose(kpmin[ff], kpmax[ff]):
kpmax[ff] += 1
NoctAct = log2(kpmax[ff]/kpmin[ff])
elif kpmax[ff] > ifreqMax:
kpmin[ff] = floor(ifreq[ff]**2/ifreqMax) # achieved Noct
kpmax[ff] = ifreqMax
if np.isclose(kpmin[ff], kpmax[ff]):
kpmin[ff] -= 1
NoctAct = log2(kpmax[ff]/kpmin[ff])
else:
NoctAct = Noct # Noct = smoothing width (Noct=6 --> 1/6th octave)
kp = arange(kpmin[ff], kpmax[ff]+1) # freqs of window
Phi1 = log2((kp - 0.5)/ifreq[ff])/NoctAct # integration bounds for hann window
Phi2 = log2((kp + 0.5)/ifreq[ff])/NoctAct
W = np.zeros(len(kp))
for ii in range(len(kp)):
W[ii] = intHann(Phi1[ii], Phi2[ii]) # weight = integration of hann window between Phi1 and Phi2
test_smoothed[ff] = np.dot( M[kpmin[ff]-ifreq[0]:kpmax[ff]-ifreq[0]+1], W[:ii+1] ) # eq 16
test_smoothed = 20*log10(test_smoothed)
return test_smoothed
def smoothCalcMatrixAlt(freq, sw: SmoothingWidth):
"""
Args:
freq: array of frequencies of data points [Hz] - equally spaced
sw: SmoothingWidth
Returns:
freq: array frequencies of data points [Hz]
Q: matrix to smooth power: {fsm} = [Q] * {fraw}
Warning: this method does not work on levels (dB)
According to Tylka_JAES_SmoothingWeights.pdf
"A Generalized Method for Fractional-Octave Smoothing of Transfer Functions
that Preserves Log-Frequency Symmetry"
https://doi.org/10.17743/jaes.2016.0053
par 1.3
eq. 16
"""
# Settings
tr = 2 # truncate window after 2x std; shorter is faster and less accurate
Noct = sw.value[0]
assert Noct > 0, "'Noct' must be absolute positive"
assert np.isclose(freq[-1]-freq[-2], freq[1]-freq[0]), "Input data must "\
"have a linear frequency spacing"
if Noct < 1:
raise Warning('Check if \'Noct\' is entered correctly')
# Initialize
L = len(freq)
Q = np.zeros(shape=(L, L), dtype=np.float16) # float16: keep size small
Q[0, 0] = 1 # in case first point is skipped
x0 = 1 if freq[0] == 0 else 0 # Skip first data point if zero frequency
Noct /= 1.5 # empirical correction factor: window @ -6 dB at Noct bounds
ifreq = freq/(freq[1]-freq[0]) # frequency, normalized to step=1
ifreq = np.array(ifreq.astype(int))
ifreqMin = ifreq[x0] # min. freq, normalized to step=1
ifreqMax = ifreq[L-1] # max. freq, normalized to step=1
sfact = 2**((1/Noct)/2) # bounds are this factor from the center freq
kpmin = np.floor(ifreq/sfact).astype(int) # min freq of window
kpmax = np.ceil(ifreq*sfact).astype(int) # max freq of window
for ff in range(x0, len(M)): # loop over input freq
if kpmin[ff] < ifreqMin:
kpmin[ff] = ifreqMin
kpmax[ff] = ceil(ifreq[ff]**2/ifreqMin) # decrease smooth. width
if np.isclose(kpmin[ff], kpmax[ff]):
kpmax[ff] += 1
NoctAct = 1/log2(kpmax[ff]/kpmin[ff])
elif kpmax[ff] > ifreqMax:
kpmin[ff] = floor(ifreq[ff]**2/ifreqMax) # decrease smooth. width
kpmax[ff] = ifreqMax
if np.isclose(kpmin[ff], kpmax[ff]):
kpmin[ff] -= 1
NoctAct = 1/log2(kpmax[ff]/kpmin[ff])
else:
NoctAct = Noct
kp = arange(kpmin[ff], kpmax[ff]+1) # freqs of window
# Integration bounds for Hann window
Phi1 = log2((kp - 0.5)/ifreq[ff]) * NoctAct
Phi2 = log2((kp + 0.5)/ifreq[ff]) * NoctAct
# Weights within window = integration of hann window between Phi1, Phi2
W = np.zeros(len(kp))
for ii in range(len(kp)):
W[ii] = intHann(Phi1[ii], Phi2[ii])
# Insert W at input freq ii, starting at index 'kpmin[ff]-ifreq[0]'
Q[ff, kpmin[ff]-ifreq[0]:kpmax[ff]-ifreq[0]+1] = W
# Normalize to conserve input power
Qpower = np.sum(Q, axis=0)
Q = Q / Qpower[np.newaxis, :]
return Q
def smoothSpectralDataAltMatrix(freq, M, sw: SmoothingWidth,
st: SmoothingType = SmoothingType.levels):
"""
Apply fractional octave smoothing to magnitude data in frequency domain.
Smoothing is performed to power, using a sliding Gaussian window with
variable length. The window is truncated after 2x std at either side.
The implementation is not exact, because f is linearly spaced and
fractional octave smoothing is related to log spaced data. In this
implementation, the window extends with a fixed frequency step to either
side. The deviation is largest when Noct is small (e.g. coarse smoothing).
07-05-2021
Update 16-01-2023: speed up algorithm
- Smoothing is performed using matrix multiplication
- The smoothing matrix is not calculated if it already exists
Args:
freq: array of frequencies of data points [Hz] - equally spaced
M: array of either power, transfer functin or dB points. Depending on
the smoothing type `st`, the smoothing is applied.
Returns:
freq : array frequencies of data points [Hz]
Msm : float smoothed magnitude of data points
"""
# TODO: Make this function multi-dimensional array aware.
# Safety
MM = copy.deepcopy(M)
Noct = sw.value[0]
assert len(M) > 0, "Smoothing function: input array is empty" # not sure if this works
assert Noct > 0, "'Noct' must be absolute positive"
if Noct < 1:
raise Warning('Check if \'Noct\' is entered correctly')
assert len(freq) == len(M), 'f and M should have equal length'
# if st == SmoothingType.ps:
# assert np.min(M) >= 0, 'absolute magnitude M cannot be negative'
if st == SmoothingType.levels and isinstance(M.dtype, complex):
raise RuntimeError('Decibel input should be real-valued')
# Initialize
L = M.shape[0] # number of data points
if st == SmoothingType.levels:
P = 10**(MM/10) # magnitude [dB] --> power
else:
P = MM # data already given as power
# TODO: This does not work due to complex numbers. Should be split up in
# magnitude and phase.
# elif st == SmoothingType.tf:
# P = P**2
# P is power while smoothing. x are indices of P.
Psm = np.zeros_like(P) # Smoothed power - to be calculated
if freq[0] == 0:
Psm[0] = P[0] # Reuse old value in case first data..
# ..point is skipped. Not plotted any way.
# # Re-use smoothing matrix Q if available. Otherwise, calculate.
# # Store in dict 'Qdict'
# nfft = int(2*(len(freq)-1))
# key = f"nfft{nfft}_Noct{Noct}" # matrix name
# if 'Qdict' not in globals(): # Guarantee Qdict exists
# global Qdict
# Qdict = {}
# if key in Qdict:
# Q = Qdict[key]
# else:
# Q = smoothCalcMatrixAlt(freq, sw)
# Qdict[key] = Q
Q = smoothCalcMatrixAlt(freq, sw)
# Apply smoothing
Psm = np.matmul(Q, P)
if st == SmoothingType.levels:
Psm = 10*np.log10(Psm)
return Psm
# %% Test # %% Test
if __name__ == "__main__": if __name__ == "__main__":
""" Test function for evaluation and debugging """ Test function for evaluation and debugging
@ -307,53 +493,76 @@ if __name__ == "__main__":
points. They should be treated and weighted differently. points. They should be treated and weighted differently.
""" """
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import time
plt.close('all')
# Initialize # Initialize
Noct = 2 # Noct = 6 for 1/6 oct. smoothing Noct = 3 # Noct = 6 for 1/6 oct. smoothing
# Create dummy data set 1: noise # # Create dummy data set 1: noise
fmin = 1e3 # [Hz] min freq # fmin = 1e3 # [Hz] min freq
fmax = 24e3 # [Hz] max freq
Ndata = 200 # number of data points
freq = np.linspace(fmin, fmax, Ndata) # frequency points
M = abs(0.4*np.random.normal(size=(Ndata,)))+0.01 #
M = 20*np.log10(M)
# # Create dummy data set 2: dirac delta
# fmin = 3e3 # [Hz] min freq
# fmax = 24e3 # [Hz] max freq # fmax = 24e3 # [Hz] max freq
# Ndata = 200 # number of data points # Ndata = 200 # number of data points
# freq = np.linspace(fmin, fmax, Ndata) # frequency points # freq = np.linspace(fmin, fmax, Ndata) # frequency points
# M = 0 * abs(1+0.4*np.random.normal(size=(Ndata,))) + 0.01 # # # freq = np.hstack((0, freq))
# M[int(100)] = 1 # M = abs(0.4*np.random.normal(size=(len(freq),)))+0.01 #
# M = 20*np.log10(M) # M = 20*np.log10(M)
# Apply function # Create dummy data set 2: single tone
fmin = 0 # [Hz] min freq
fmax = 5e3 # [Hz] max freq
Ndata = 2501 # number of data points
freq = np.linspace(fmin, fmax, Ndata) # frequency points
M = 1e-4*np.random.normal(size=(Ndata,))
M[500] = 1
MdB = 20*np.log10(abs(M))
class sw: class sw:
value = [Noct] value = [Noct]
st = SmoothingType.levels # so data is given in dB st = SmoothingType.levels # so data is given in dB
# st = SmoothingType.ps # so data is given in power # st = SmoothingType.ps # so data is given in power
# Smooth # Smooth
Msm = smoothSpectralData(freq, M, sw, st) if 'Qdict' in globals():
del Qdict
t0 = time.time()
Msm = smoothSpectralData(freq, MdB, sw, st) # current algorithm
t1 = time.time()
MsmAlt = smoothSpectralDataAlt(freq, MdB, sw, st) # alternative algorithm
t2 = time.time()
MsmAltMatrix = smoothSpectralDataAltMatrix(freq, MdB, sw, st) # alternative algorithm, matrix method
t3 = time.time()
fsm = freq fsm = freq
print(f"Smoothing time: {t1-t0} s")
print(f"Smoothing time: {t2-t1} s (Alt)")
print(f"Smoothing time: {t3-t2} s (Alt Matrix)")
# Plot - lin frequency # Plot - lin frequency
plt.figure() plt.figure()
plt.plot(freq, M, '.b') plt.plot(freq, MdB, '.b')
plt.plot(fsm, Msm, 'r') plt.plot(fsm, Msm, 'r')
plt.plot(fsm, MsmAlt, 'g')
plt.plot(fsm, MsmAltMatrix, '--k')
plt.xlabel('f (Hz)') plt.xlabel('f (Hz)')
plt.ylabel('magnitude') plt.ylabel('magnitude')
plt.xlim([100, fmax]) plt.xlim((0, fmax))
plt.ylim((-90, 1))
plt.grid('both')
plt.title('lin frequency') plt.title('lin frequency')
plt.legend(['Raw', 'Smooth']) plt.legend(['Raw', 'Smooth', 'SmoothAlt', 'SmoothAltMatrix'])
# Plot - log frequency # Plot - log frequency
plt.figure() plt.figure()
plt.semilogx(freq, M, '.b') plt.semilogx(freq, MdB, '.b')
plt.semilogx(fsm, Msm, 'r') plt.semilogx(fsm, Msm, 'r')
plt.semilogx(fsm, MsmAlt, 'g')
plt.semilogx(fsm, MsmAltMatrix, '--k')
plt.xlabel('f (Hz)') plt.xlabel('f (Hz)')
plt.ylabel('magnitude') plt.ylabel('magnitude')
plt.xlim([100, fmax]) plt.xlim((100, fmax))
plt.ylim((-90, 1))
plt.grid('both')
plt.title('log frequency') plt.title('log frequency')
plt.legend(['Raw', 'Smooth 1']) plt.legend(['Raw', 'Smooth', 'SmoothAlt', 'SmoothAltMatrix'])