diff --git a/applications/lofar2/model/pfs_coeff_final.m b/applications/lofar2/model/pfs_coeff_final.m index 7cc4798a14518588eb688864ce57a6dbe8f54791..3c3bb8947306f96fc3b736bdbed837ae719ad32c 100644 --- a/applications/lofar2/model/pfs_coeff_final.m +++ b/applications/lofar2/model/pfs_coeff_final.m @@ -1,128 +1,410 @@ +%------------------------------------------------------------------------------ +% +% Copyright 2021 +% ASTRON (Netherlands Institute for Radio Astronomy) <http://www.astron.nl/> +% P.O.Box 2, 7990 AA Dwingeloo, The Netherlands +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +%------------------------------------------------------------------------------ +% +% Authors : +% Created by FilterTaskForce team 2004 for LOFAR1 [1, 2] +% Updated in 2022 for LOFAR20 by E. Kooistra +% Purpose: +% +% References: +% [1] hdl/applications/lofar1/FilterTaskForce/readme_FilterTaskForce.txt +% [2] hdl/applications/lofar1/FilterTaskForce/pfs_coeff_final.m (Original +% file from LOFAR1) +% [3] LOFAR_MEM_162_digfilter_stopband3.pdf +% [4] data/Coeffs16384Kaiser-quant.dat original LOFAR1 FIR coefficients +% [5] data/Coefficient_16KKaiser.gold FTF reference FIR coefficients +% [6] run_pfir_coeff.m, similar to this FIR filter generation script, used +% for Apertif. +% +% Description: +% +% The LOFAR1 coefficients [4] were probably generated with this script, but +% the exact same values cannot be reproduced, probably due to differences +% in parameter settings. Nevertheless the FIR filter design method in this +% scriptis still valid, so if we should need to generate new coefficients, +% then we can do so with this script. +% +% Using reproduce_ftf = true does reproduce the FTF coefficients from [5]. +% However, these contain an error in r_pass, which could also be present the +% LOFAR1 coefficients [4], which means that the r_pass ripple could be +% improved. +% +% The half power subband bandwidth for the LOFAR1 [4] FIR filter is HP_BW = +% 0.9954 * f_sub and for the FTF [5] FIR filter HP_BW = 0.9947 * f_sub. +% Both FIR filters have the same maximum coefficient value of 2^15 - 1 = +% 32767. +% +% With a plain FFT one expects a processing gain of N_point for the signal +% to noise ration (SNR) of a bin, because the wanted signal is in that bin, +% while the noise is still in all N_point bins. However, in poly phase +% filterbank (PFB) simulations I (Eric) noticed that the PFB does not achieve +% a processing gain of N_point in SNR. I think this is due to that the poly +% phase FIR filter part in the PFB has a DC response that is not equal for +% all N_point polyphases, that form the N_point input to the FFT part of the +% PFB. There is a small variation in DC reponse that then causes leakage into +% other bins and thus reduces the processing gain of the PFB to less than +% N_points. This script shows that a more flat DC response over the poly +% phases can be achieved by using a stronger window function for the FIR +% coefficients calculation. However, more windowing would also increases the +% half power bandwidth of the subbands. For LOFAR the reduced PFB processing +% gain is no problem, because the PFB still meets the subband stop band +% attenuation requirement of [3]. +% +% The LOFAR1 FIR coefficients [4] have nof_bits = 16 bits. This is enough to +% meet the specification of [3]. Therefore it is not necessary to regenerate +% the FIR coefficients to 18 bit, even though the multipliers in the FPGA +% could support 18b nearly for free. +% +% For comparision between LOFAR1 and LOFAR20 stations it is preferred to +% keep using the same subband FIR coefficients. This is fine, because they +% meet the specification of [3]. +% +% Conclusion: +% a) We can and should keep the original LOFAR1 subband FIR filter coefficients +% also for LOFAR20. +% b) We cannot exactly reproduce the LOFAR1 subband FIR filter coefficients, but +% with this script we could generate an equivalent or improved set of subband +% FIR coefficients if we would have to. +% +% Modifications: +% a) Corrected missing -1 in r_pass=10^(r_pass_dB/20) - 1 +% b) Change r_stop_dB = -89 dB into -91 dB conform [3] +% c) Clarified plots and corrected the f1_axis center +% d) Support round() instead of uencode() for coefficient quantization +% e) Compare created coefficients with LOFAR1 coefficients +% f) Added plot of DC response per poly phase +% g) Determine half power bandwidth of the pass band +% h) Support loading and analysing LOFAR1 coefficients for comparison +% i) Support changing the half power subband band width via relative_bw = 1 + close all; clear all; +fig = 0; + +reproduce_ftf = true; +%reproduce_ftf = false; + +%% User settings +if not(reproduce_ftf) + % passband ripple (in dB); + r_pass_dB = 0.5; + r_pass = 10^(r_pass_dB/20) - 1; + % stopband ripple (in dB); + r_stop_dB = -89; + r_stop = 10^(r_stop_dB/20); + % relative subband band width + relative_bw = 1; + % word size for coefficients + nof_bits = 16; + % window function for coefficients + kaiser_beta = 1; + use_window = 'kaiser'; + % quantization method + use_uencode = false; + % support analysing LOFAR1 coefficients + use_lofar1 = false; +else + disp(sprintf('NOTE: recreate FTF coefficients result for reference')); + r_pass = 10^(0.5/20); % note missing -1 + r_stop = 10^(-89/20); % -89 instead of -91 in [3] + relative_bw = 1; + nof_bits = 16; + kaiser_beta = 1; + use_window = 'kaiser'; + use_uencode = true; + use_lofar1 = false; +end -fs =200*10^6; +coeff_max = 2^(nof_bits - 1) - 1; + + +%% Filter specification % fft_size; -N=1024; +N = 1024; % taps per frequency channel -L=16; -% passband ripple (in dB); -r_pass=10^(0.5/20); -% stopband ripple (in dB); -r_stop=-89; +L = 16; % upsampling factor -Q=16; -% word size for coefficients -W=16; - +Q = 16; +% pass bandwidth +subband_bw = relative_bw / N; +comp_bw = Q * subband_bw; % computed filter length -M1=N*L/Q; +M1 = N*L/Q; % interpolated filter length -M2=N*L; -Nfft = M2; -% compute filter -c=fircls1(M1-1,Q/N,10^(r_pass/20),10^(r_stop/20),'trace'); - -figure(30) -Nfft =length(c); -hold on -spectrum_Fircls1= abs(fftshift(fft(c/max(c),Nfft)))/max(abs(fftshift(fft(c/max(c),Nfft)))); -plot([-Nfft/2+1:Nfft/2]/Nfft*fs,20*log10(spectrum_Fircls1),'r') -hold off -legend('Other candidate','filter given by fircls1') -xlabel('f(Hz)') -ylabel('Gain(dB)') -title('Compirason of the prototype filter window before FFT interpolation'); - - - -% use fourier interpolation to create final filter -f1=fft(kaiser(length(c),1).'.*c);%hanning(length(c)).'.* -f2=zeros(1,M2); -% copy the lower frequency half. -n=0:M1/2; - -figure(31) -subplot(2,1,1) -plot(-length(f1)/2+1:length(f1)/2,20*log10(fftshift(abs(f1)))) -xlabel('f(Hz)') -title('Gain of the filter after FFT') -ylabel('Gain(dB)') -subplot(2,1,2) -plot(-length(f1)/2+1:length(f1)/2,unwrap(angle(fftshift(f1)))) -title('Phase of the filter after FFT') -xlabel('f(Hz)') -ylabel('Gain(dB)') - -f2(1+n)=f1(1+n); -% to make the impulse response exactly symmetric, we need to do a phase -% correction -% to make the impulse response exactly symmetric, we need to do a phase correction -f2(1+n)=f2(1+n).*exp(-sqrt(-1)*(Q-1)*pi*n*1/M2); +M2 = N*L; +% fine frequency resolution +P = 10; +M3 = M2 * P; +% frequency scale +fs1 = N/Q; +fs2 = N; +f1_axis = [-M1/2:M1/2-1]/M1 * fs1; % for compute filter +f2_axis = [-M2/2:M2/2-1]/M2 * fs2; % for interpolated filter +f3_axis = [-M3/2:M3/2-1]/M3 * fs2; % for fine resolution spectrum + +%% Plot possible window functions +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +plot([kaiser(M1, 0.5), kaiser(M1, 1), kaiser(M1, 2), kaiser(M1, 4), hanning(M1), blackman(M1)]) +legend('kaiser beta = 0.5', 'kaiser beta = 1', 'kaiser beta = 2', 'kaiser beta = 4', 'hanning', 'blackman') +title('Window functions'); +xlabel('Sample') +ylabel('Gain') +grid on; + + +%% Compute filter +h_comp = fircls1(M1-1, comp_bw, r_pass, r_stop, 'trace'); + +fft_hcomp = fft(h_comp); +fftshift_comp = fftshift(fft_hcomp); +spectrum_comp = abs(fftshift_comp) / max(abs(fftshift_comp)); +spectrum_comp_dB = 20*log10(spectrum_comp); +phase_comp = unwrap(angle(fftshift_comp)); + +%% Compute windowed filter +if strcmp(use_window, 'blackman') + disp(sprintf('NOTE: use blackman window')); + h_window = blackman(M1); +elseif strcmp(use_window, 'hanning') + disp(sprintf('NOTE: use hanning window')); + h_window = hanning(M1); +elseif strcmp(use_window, 'kaiser') + disp(sprintf('NOTE: use kaiser window (beta = %4.2f)', kaiser_beta)); + h_window = kaiser(M1, kaiser_beta); +else % default 'none' + disp(sprintf('NOTE: use no window')); + h_window = ones(M1, 1); % no window is rectangular window +end +h_wcomp = h_window' .* h_comp; + +%% Plot frequency response of compute filter +fft_wcomp = fft(h_wcomp); +fftshift_wcomp = fftshift(fft_wcomp); +spectrum_wcomp = abs(fftshift_wcomp) / max(abs(fftshift_wcomp)); +spectrum_wcomp_dB = 20*log10(spectrum_wcomp); +phase_wcomp = unwrap(angle(fftshift_wcomp)); + +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +plot(f1_axis, spectrum_comp_dB, 'r', f1_axis, spectrum_wcomp_dB, 'g') +legend('computed','windowed') +title('Power spectrum of computed and windowed filter'); +xlabel('f [Hz]') +ylabel('Gain [dB]') +grid on; + +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +plot(f1_axis, phase_comp, 'r', f1_axis, phase_wcomp, 'g') +legend('computed','windowed') +title('Phase spectrum of computed and windowed filter'); % no difference +xlabel('f [Hz]') +ylabel('Phase [rad]') +grid on; + + +%% Use fourier interpolation to create final filter +f2 = zeros(1, M2); +% copy the lower frequency half of the computed filter into f2. +n = 0:M1/2; +f2(1+n) = fft_wcomp(1+n); + +% apply phase correction (= time shift) to make the impulse response exactly symmetric +f2(1+n) = f2(1+n) .* exp(-sqrt(-1)*(Q-1)*pi*n*1/M2); % create the upper part of the spectrum from the lower part -f2(M2-n)=conj(f2(2+n)); - -figure(32) -subplot(2,1,1) -plot(-length(f2)/2+1:length(f2)/2,20*log10(fftshift(abs(f2)))) -xlabel('f(Hz)') -title('Gain of the filter after FFT') -ylabel('Gain(dB)') -subplot(2,1,2) -hold on -plot(-length(f2)/2+1:length(f2)/2,unwrap(fftshift(angle(f2)))) -plot(-length(f2)/2+1:length(f2)/2,unwrap(fftshift(angle(f2))),'r') -hold off -title('Phase of the filter after FFT') -xlabel('f(Hz)') -ylabel('Gain(dB)') +f2(M2-n) = conj(f2(2+n)); % back to time domain -c2=real(ifft(f2)); +h_fourier = real(ifft(f2)); + +% use same scale +h_fourier = h_fourier * coeff_max / max(h_fourier); % quantize the coefficients -c3=c2/max(c2); -coeffs_quant = double(uencode(c3,16,max(c3),'signed')); -% q=1/(2^(W-1)); -% c3=q*round(c3/q); -% c3=c3*max(c2)/max(c3); +if use_uencode + % uencode is equivalent to floor() + disp(sprintf('NOTE: use uencode() quantization')); + h_quant = double(uencode(h_fourier, nof_bits, coeff_max, 'signed')); +else + % round is equivalent to floor(x + 0.5) provided that exactly 0.5 does not + % because round(0.5) = floor(x + 0.5) = 1, but round(-0.5) = -1. + disp(sprintf('NOTE: use round() quantization')); + h_quant = round(h_fourier); +end + +%% Load LOFAR1 coefficients +if use_lofar1 + h_quant = load('data/Coeffs16384Kaiser-quant.dat'); + h_quant = h_quant'; +end + +%% Check symmetry of FIR coefficients +n=0:M2-1; +h_reverse = h_quant(M2-n); +d = h_quant == h_reverse; +if sum(d) == length(d) + disp(sprintf('NOTE: quantized FIR coefficients are symmetrical.')); +else + disp(sprintf('WARNING: quantized FIR coefficients are NOT symmetrical.')); +end + +% save the coefficients fid = fopen('data/Coefficient_16KKaiser.dat','w'); -fprintf(fid,'%i\n', coeffs_quant); +fprintf(fid,'%i\n', h_quant); fclose(fid); -fprintf('\n\nWARNING !\n') -fprintf('This coefficient recordered in Coefficient_16K.dat are scaled to one. It is necessary then to multiply them with the number of bits needed for the quanization: 2^N_bits \n\n') +%% Compare with reference FTF coefficients +h_ftf = load('data/Coefficient_16KKaiser.gold'); +d = h_ftf' == h_quant; +if sum(d) == length(d) + disp(sprintf('NOTE: quantized FIR coefficients are identical to FTF coefficients.')); +else + if reproduce_ftf + disp(sprintf('ERROR: quantized FIR coefficients are NOT identical to FTF coefficients.')); + else + disp(sprintf('NOTE: quantized FIR coefficients are NOT identical to FTF coefficients.')); + end +end + +%% Compare with LOFAR1 coefficients +h_lofar1 = load('data/Coeffs16384Kaiser-quant.dat'); +d = h_lofar1' - h_quant; +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +plot(d) +title('Difference between recreated coefficients and LOFAR1 coefficients'); +xlabel('Sample') +ylabel('Coefficient') +grid on; -% freqz(c3,1,M2*32,200*10^6); -% q=1/(2^(W-1)); -% c3=q*round(c3/q); -% c3=c3*max(c2)/max(c3); +d = h_lofar1' == h_quant; +if sum(d) == length(d) + disp(sprintf('NOTE: quantized FIR coefficients are identical to LOFAR1 coefficients.')); +else + disp(sprintf('NOTE: quantized FIR coefficients are NOT identical to LOFAR1 coefficients.')); +end -figure(2); -n=0:M2-1; -plot([c2',c2(M2-n)',c3']); -title('impulse response'); -legend('unquantized','reversed','quantized'); + +%% Plot (un)quantized impulse response +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +plot([h_fourier', h_quant']); +legend('unquantized','quantized'); +title('Impulse response'); +xlabel('Sample') +ylabel('Coefficient') +grid on; + +%% Plot quantization error histogram to see effect of use_uencode +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +hist(h_fourier - h_quant,100); +title('Quantization error histogram'); +xlabel('Quantization error'); +ylabel('Relative probability'); +grid on; + + +%% Plot DC response per polyphase +% . the DC response over the polyphases gets more flat with more windowing +dc_fourier = sum(reshape(h_fourier, N, L)'); +dc_quant = sum(reshape(h_quant, N, L)'); + +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +plot([dc_fourier', dc_quant']); +legend('unquantized','quantized'); +title('DC response per polyphase'); +xlabel('Polyphase index') +ylabel('DC response') grid on; -figure(2); -hist(c2-c3,100); -title('quantization error histogram'); -xlabel('quantization error'); -ylabel('relative probability'); - -figure(10); -freqz(c3,1,M2*4); -title('frequency response'); -Nfft = 16384*8; -c3 = c3/max(c3); -%spectrum_fft = abs(fftshift(fft(c3/max(c3),Nfft)))/max(abs(fftshift(fft(c3/max(c3),Nfft)))); -spectrum = abs(fftshift(fft(c3/max(c3),Nfft)))/max(abs(fftshift(fft(c3/max(c3),Nfft)))); -frequency = [-Nfft/2+1:Nfft/2]*200*10^6/Nfft; -plot(frequency(length(frequency)/2:end),20*log10(spectrum(length(frequency)/2:end))) -grid on -title('Gain of the filter window with quantized coefficients (16bits) using Fircls1 window with the FFT interpolation method: blackman window') -xlabel('f(Hz)') -ylabel('Gain(dB)') -legend('FFT interpolation') + +%% Plot frequency response of original and quantized filter + +%freqz(h_fourier, 1, M3); +%freqz(h_quant, 1, M3); + +fft_org = fft(h_fourier, M3); +fftshift_org = fftshift(fft_org); +spectrum_org = abs(fftshift_org) / max(abs(fftshift_org)); +spectrum_org_dB = 20*log10(spectrum_org); +phase_org = unwrap(angle(fftshift_org)); + +fft_quant = fft(h_quant, M3); +fftshift_quant = fftshift(fft_quant); +spectrum_quant = abs(fftshift_quant) / max(abs(fftshift_quant)); +spectrum_quant_dB = 20*log10(spectrum_quant); +phase_quant = unwrap(angle(fftshift_quant)); + +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +plot(f3_axis, spectrum_org_dB, 'r', f3_axis, spectrum_quant_dB, 'g') +legend('original','quantized') +title('Power spectrum of original and quantized filter'); +xlabel('f [Hz]') +ylabel('Gain [dB]') +grid on; + +fig=fig+1; +figure('position', [300+fig*20 200-fig*20 1000 800]); +figure(fig); +% There is a large absolute phase offset between phase_org and phase_quant, +% due to the quantization noise. Remove this absolute phase offset to be +% able to compare the phase within the pass band, to see that it is the +% same and is linear from -0.5:0.5. Use the phase offset at DC so the center +% at index M3/2. Manually zoom in to range -1:1 to see the linear phase from +% -0.5:0.5 in the subband. +phase_offset = phase_org(M3/2) - phase_quant(M3/2); +phase_quant = phase_quant + phase_offset; +plot(f3_axis, phase_org, 'r', f3_axis, phase_quant, 'g') +legend('original','quantized') +title('Phase spectrum of original and quantized filter'); +xlabel('f [Hz]') +ylabel('Phase [rad]') +grid on; + + +%% Determine half power bandwidth of the subband +pass_band = spectrum_quant >= 0.5; +pass_indices = find(pass_band); +pass_index_lo = pass_indices(1); +pass_index_hi = pass_indices(end); +% linear interpolate to relative index at 0.5 power +a = spectrum_quant(pass_index_lo); +b = spectrum_quant(pass_index_lo - 1); +pass_index_lo = pass_index_lo + (0.5 - b) / (a - b); +a = spectrum_quant(pass_index_hi); +b = spectrum_quant(pass_index_hi + 1); +pass_index_hi = pass_index_hi + 1 - (0.5 - b) / (a - b); +pass_bw = (pass_index_hi - pass_index_lo + 1) / M3 * fs2; +%disp(sprintf('%f', pass_index_lo)); +%disp(sprintf('%f', pass_index_hi)); +disp(sprintf('NOTE: quantized FIR filter normalized subband bandwidth is %6.4f * f_sub', pass_bw));