From 74066d066924cc3b3abe7e69405b13351bef910b Mon Sep 17 00:00:00 2001 From: Silas Oettinghaus Date: Fri, 21 Mar 2025 08:11:40 +0100 Subject: [PATCH] Changes from mwork PC. PDP 2025 MPI analysis new focus on database and SQL --- .gitignore | 2 + Classes/00_signals/Signal.m | 19 +- Classes/01_transmit/PAMmapper.m | 215 ++++-- Classes/01_transmit/Pulseformer.m | 43 +- Classes/04_DSP/Equalizer/FFE.m | 14 +- Classes/04_DSP/Equalizer/Postfilter.m | 4 +- Classes/04_DSP/Sequence Detection/MLSE.m | 52 +- Classes/04_DSP/TransmissionPerformance.m | 23 +- Classes/DataBaseHandler/DBHandler.m | 616 +++++++++++------- Classes/Moveit_wrapper.m | 23 +- Datatypes/equalizer_structure.m | 5 +- Functions/EQ_structures/duobinary_target.m | 141 ++-- Functions/EQ_structures/vnle.m | 30 +- .../EQ_structures/vnle_postfilter_mlse.m | 144 +++- Functions/EQ_visuals/showEQcoefficients.m | 77 ++- Functions/EQ_visuals/showEQfilter.m | 36 + Functions/EQ_visuals/showLevelHistogram.m | 2 + Functions/Metrics/calc_air_plain.m | 40 ++ Functions/Metrics/calc_snr.m | 36 + Functions/beautifyBERplot.m | 2 +- .../auswertung MPI/analyze_mpi.m | 92 +++ .../auswertung MPI/dsp_mpi.m | 349 ++++++++++ .../auswertung MPI/plot_from_database.m | 91 +++ .../analyze_burg.m | 0 .../analyze_equalizer_taps.m | 0 .../analyze_lambda.m | 0 .../analyze_postfilter_taps.m | 0 .../auswertung/App/auswertungs_app.mlapp | Bin 71157 -> 70397 bytes projects/IMDD_base_system/imdd_it.m | 17 +- projects/IMDD_base_system/imdd_model.m | 25 +- projects/Messung_Zürich/change_filenames.m | 85 +++ projects/Messung_Zürich/dsp_all_files.m | 431 +++++++++--- projects/Messung_Zürich/dsp_ief_file.m | 157 ++--- projects/Messung_Zürich/minimal_pam4_mlse.m | 19 + test/matched_filter_minimal.m | 40 +- test/test_mapping_eth.m | 24 +- 36 files changed, 2234 insertions(+), 620 deletions(-) create mode 100644 Functions/EQ_visuals/showEQfilter.m create mode 100644 Functions/Metrics/calc_air_plain.m create mode 100644 Functions/Metrics/calc_snr.m create mode 100644 projects/HighSpeedExperiment_2024/auswertung MPI/analyze_mpi.m create mode 100644 projects/HighSpeedExperiment_2024/auswertung MPI/dsp_mpi.m create mode 100644 projects/HighSpeedExperiment_2024/auswertung MPI/plot_from_database.m rename projects/{IMDD_base_system => HighSpeedExperiment_2024/auswertung deliv 06 feb 2025}/analyze_burg.m (100%) rename projects/{IMDD_base_system => HighSpeedExperiment_2024/auswertung deliv 06 feb 2025}/analyze_equalizer_taps.m (100%) rename projects/{IMDD_base_system => HighSpeedExperiment_2024/auswertung deliv 06 feb 2025}/analyze_lambda.m (100%) rename projects/{IMDD_base_system => HighSpeedExperiment_2024/auswertung deliv 06 feb 2025}/analyze_postfilter_taps.m (100%) create mode 100644 projects/Messung_Zürich/change_filenames.m create mode 100644 projects/Messung_Zürich/minimal_pam4_mlse.m diff --git a/.gitignore b/.gitignore index 43b6a18..de1fa6c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ sccprj/ # Matlab code generation folders codegen/ + +.mat diff --git a/Classes/00_signals/Signal.m b/Classes/00_signals/Signal.m index e5828a4..f75ddd4 100644 --- a/Classes/00_signals/Signal.m +++ b/Classes/00_signals/Signal.m @@ -656,7 +656,7 @@ classdef Signal end %% - function [obj,S,isFlipped] = tsynch(obj,options) + function [obj,S,isFlipped,sequenceFound] = tsynch(obj,options) % time sync and cut arguments obj Signal @@ -664,6 +664,14 @@ classdef Signal options.fs_ref = 0; options.debug_plots = 0; end + + + + S = {}; + isFlipped=0; + sequenceFound = 0; + + %normalize the signal a = obj.normalize("mode","oneone").signal; @@ -681,7 +689,11 @@ classdef Signal %estimate start pos of signal maxpeaknum = floor(length(a)/length(b)); - [pks,pkpos] = findpeaks(abs(co./max(co)),'MinPeakDistance',length(b)/2,'MinPeakHeight',0.2,'NPeaks',maxpeaknum,'SortStr','descend'); + [pks,pkpos,w,p] = findpeaks(abs(co./max(co)),'MinPeakDistance',length(b)/2,'MinPeakHeight',0.2,'NPeaks',maxpeaknum,'SortStr','descend'); + + if mean(w) > 10 || mean(p) > 10 + return + end if options.debug_plots figure() @@ -692,9 +704,6 @@ classdef Signal shifts = shifts(shifts>=0); - S = {}; - isFlipped=0; - if numel(shifts) > 0 %Cut occurences of ref signal from signal (only positive shifts) diff --git a/Classes/01_transmit/PAMmapper.m b/Classes/01_transmit/PAMmapper.m index 92d8c53..6ba8558 100644 --- a/Classes/01_transmit/PAMmapper.m +++ b/Classes/01_transmit/PAMmapper.m @@ -24,7 +24,7 @@ classdef PAMmapper obj.M = M; obj.unipolar = unipolar; - + obj.thresholds = obj.get_demodulation_thresholds(); obj.levels = obj.get_levels(); @@ -36,10 +36,10 @@ classdef PAMmapper end function out = map(obj,signal_in) - + if isa(signal_in,'Signal') - signal_in.signal = obj.map_(signal_in.signal); - + signal_in.signal = obj.map_(signal_in.signal); + % signal_in = signal_in.normalize("mode","rms"); lbdesc = ['Map bat stream to PAM ',num2str(obj.M),' symbols']; signal_in = signal_in.logbookentry(lbdesc,obj); @@ -47,19 +47,19 @@ classdef PAMmapper else out = signal_in; end - + end function signal_out = demap(obj,signal_in) issignalclass = 0; if isa(signal_in,'Signal') - signalclass = signal_in; - signal_in = signal_in.signal; - issignalclass = 1; + signalclass = signal_in; + signal_in = signal_in.signal; + issignalclass = 1; end - signal_out = obj.demap_(signal_in); + signal_out = obj.demap_(signal_in); if issignalclass lbdesc = ['Demap PAM ',num2str(obj.M),' symbols to bit stream']; @@ -68,7 +68,7 @@ classdef PAMmapper signal_in.signal = signal_out; signal_out = signal_in; end - + end function pam_sig = map_(obj,bitpattern) @@ -89,7 +89,7 @@ classdef PAMmapper % 4-ASK: if ~obj.eth_style pam_sig=2*bitpattern(:,1)+(bitpattern(:,1)==bitpattern(:,2)); - + if obj.unipolar==0 pam_sig=2*pam_sig-3; end @@ -100,15 +100,65 @@ classdef PAMmapper pam_sig = pam_sig/sqrt(5); case 6 - - m = 1; - if size(bitpattern,2)>size(bitpattern,1) - bitpattern = bitpattern'; %vector aufrecht stellen - end - % LUT based mapping - for k = 1:5:fix(length(bitpattern)/5)*5 - pam_sig(m:m+1,1) = obj.thresholds(bin2dec(int2str(bitpattern(k:k+4)'))+1,:); - m = m+2; + + if ~obj.eth_style + + m = 1; + if size(bitpattern,2)>size(bitpattern,1) + bitpattern = bitpattern'; %vector aufrecht stellen + end + + % LUT based mapping + for k = 1:5:fix(length(bitpattern)/5)*5 + pam_sig(m:m+1,1) = obj.thresholds(bin2dec(int2str(bitpattern(k:k+4)'))+1,:); + m = m+2; + end + + else + + bitsPerSymbol = reshape(bitpattern,5,[]).'; % reorder 5 bits per symbol + normFactor = 1; + %====================32 QAM===================% + %=============================================% + % Coding % + % 01000 01001 |11001 11000 % + % | % + % 01010 01110 01100 |11100 11110 11010 % + % | % + % 01011 01111 01101 |11101 11111 11011 % + % --------------------|------------------- % + % 00011 00111 00101 |10101 10111 10011 % + % | % + % 00010 00110 00100 |10100 10110 10010 % + % | % + % 00000 00001 |10001 10000 % + %=============================================% + % modulate three LSB first in first Quadrant + % first bit inverts real part if 0 + % second bit inverts imaginary part if 0 + + LSB_symbols = normFactor*(... + +(1+1i) *(bitsPerSymbol(:,3)==1 & bitsPerSymbol(:,4)==0 & bitsPerSymbol(:,5)==1)... + +(3+1i) *(bitsPerSymbol(:,3)==1 & bitsPerSymbol(:,4)==1 & bitsPerSymbol(:,5)==1)... + +(5+1i) *(bitsPerSymbol(:,3)==0 & bitsPerSymbol(:,4)==1 & bitsPerSymbol(:,5)==1)... + +(1+3i) *(bitsPerSymbol(:,3)==1 & bitsPerSymbol(:,4)==0 & bitsPerSymbol(:,5)==0)... + +(3+3i) *(bitsPerSymbol(:,3)==1 & bitsPerSymbol(:,4)==1 & bitsPerSymbol(:,5)==0)... + +(5+3i) *(bitsPerSymbol(:,3)==0 & bitsPerSymbol(:,4)==1 & bitsPerSymbol(:,5)==0)... + +(1+5i) *(bitsPerSymbol(:,3)==0 & bitsPerSymbol(:,4)==0 & bitsPerSymbol(:,5)==1)... + +(3+5i) *(bitsPerSymbol(:,3)==0 & bitsPerSymbol(:,4)==0 & bitsPerSymbol(:,5)==0)); + + Re = real(LSB_symbols); + Im = imag(LSB_symbols); + % if first bit== 0 => invert real part + % if second bit== 0 => invert imag part + modData2 = Re.*(bitsPerSymbol(:,1)*2-1) + 1i*Im.*(bitsPerSymbol(:,2)*2-1); + Re = real(modData2(1:end/2)); + Im = imag(modData2(1:end/2)); + pam_sig = zeros(length(Re)*2,1); + pam_sig(1:2:length(Re)*2) = Re; + pam_sig(2:2:length(Im)*2) = Im; + + end pam_sig = pam_sig/sqrt(10); @@ -120,9 +170,9 @@ classdef PAMmapper x1 = bitpattern(:,1); x2 = (bitpattern(:,1)==bitpattern(:,3)); x3 = x2~=bitpattern(:,2); - + pam_sig = 4*x1 + 2*x2 + x3; - + if obj.unipolar==0 pam_sig=2*pam_sig-7; end @@ -133,7 +183,7 @@ classdef PAMmapper end pam_sig = pam_sig/sqrt(21); - + case 16 % 16-ASK: x1 = bitpattern(:,1); @@ -167,7 +217,7 @@ classdef PAMmapper % case 16 % thres = [-10 -8 -6 -4 -2 0 2 4 6 8 10]; % end - + switch obj.M @@ -190,12 +240,12 @@ classdef PAMmapper thres = thres .* 1/sqrt(5); - case 6 %PAM 6 + case 6 %PAM 6 thres = [-3 5;-1 5;-3 -5;-1 -5;-5 3;-5 1;-5 -3;-5 -1;-1 3;-1 1;-1 -3;-1 -1;-3 3;-3 1;-3 -3;-3 -1;3 5;1 5;3 -5;1 -5;5 3;5 1;5 -3;5 -1;1 3;1 1;1 -3;1 -1;3 3;3 1;3 -3;3 -1]; % thres = [-4 -2 0 2 4]; % thres = thres ./ sqrt(10); - + case 8 % 8-ASK if obj.unipolar==0 @@ -203,7 +253,7 @@ classdef PAMmapper elseif obj.unipolar==1 thres=0.5:6.5; end - + thres=thres./sqrt(21); case 16 @@ -234,23 +284,26 @@ classdef PAMmapper end function [data_out] = demap_(obj,data_in) + data_in= data_in'; + if obj.M ~= 6 - + % create output if ~isempty(obj.thresholds) a = squeeze(repmat(real(data_in),[1 1 length(obj.thresholds)])); %Eingangssignal in 3 spalten b = squeeze(repmat(reshape(obj.thresholds(:).',[1 1 length(obj.thresholds)]),[1 length(data_in) 1])); %Threshold in 3 Spalten comp_real = a > b; %check for each symbol/ sampling if it exeeds the obj.thresholdseshold 1, 2 or 3 - + comp_real=repmat(real(data_in),[1 1 length(obj.thresholds)]) > repmat(reshape(obj.thresholds(:).',[1 1 length(obj.thresholds)]),[1 length(data_in) 1]); - + else comp_real=[]; end - + s1=size(comp_real,1); s2=size(comp_real,2); + end switch obj.M @@ -273,34 +326,70 @@ classdef PAMmapper case 6 - data_in = data_in/(sqrt(mean(abs(data_in).^2))); - data_in = data_in*sqrt(10); + if ~obj.eth_style + + data_in = data_in/(sqrt(mean(abs(data_in).^2))); + data_in = data_in*sqrt(10); + + if size(data_in,2) > 1 + data_in = data_in.'; + end + + if length(data_in)/2 ~= round(length(data_in)/2) + data_in = [data_in;0]; + end + + m = 1; + for n = 1:2:length(data_in) + dist = sqrt((data_in(n)-obj.thresholds(:,1)).^2+(data_in(n+1)-obj.thresholds(:,2)).^2); + [~,dd_idx] = min(dist); + % dec_out(n:n+1) = LUT(dd_idx,:); + data_out(m:m+4) = bitget(dd_idx-1,5:-1:1); + m = m+5; + end + + else + + data_in= data_in'; + rxSym = data_in(1:2:end) + 1i*data_in(2:2:end); % 16×1 + + % Decode the sign bits (bits 1 & 2). + rxBit1 = double(real(rxSym) > 0); + rxBit2 = double(imag(rxSym) > 0); + + % Undo the quadrant inversion and normalization. + rxSym_corr = abs(real(rxSym)) + 1i*abs(imag(rxSym)); + normFactor = 1/sqrt(10); + rxSym_unscaled = rxSym_corr / normFactor; + + + cand = [1+1i, 3+1i, 5+1i, 1+3i, 3+3i, 5+3i, 1+5i, 3+5i]; + candBits = [1 0 1; + 1 1 1; + 0 1 1; + 1 0 0; + 1 1 0; + 0 1 0; + 0 0 1; + 0 0 0]; + d = abs(rxSym_unscaled - cand).^2; % 32×8 distances + [~, idx] = min(d, [], 2); + rxLSB = candBits(idx,:); % 32×3 + + + decodedSymbols = [rxBit1, rxBit2, rxLSB]; + data_out = reshape(decodedSymbols.', [], 1).'; - if size(data_in,2) > 1 - data_in = data_in.'; end - - if length(data_in)/2 ~= round(length(data_in)/2) - data_in = [data_in;0]; - end - - m = 1; - for n = 1:2:length(data_in) - dist = sqrt((data_in(n)-obj.thresholds(:,1)).^2+(data_in(n+1)-obj.thresholds(:,2)).^2); - [~,dd_idx] = min(dist); - % dec_out(n:n+1) = LUT(dd_idx,:); - data_out(m:m+4) = bitget(dd_idx-1,5:-1:1); - m = m+5; - end - + case 8 % 8-ASK if ~obj.eth_style - data_out=[comp_real(:,:,4); - comp_real(:,:,1)-comp_real(:,:,3)+comp_real(:,:,5)-comp_real(:,:,7); - 1-comp_real(:,:,2)+comp_real(:,:,6)]; + data_out=[comp_real(:,:,4); + comp_real(:,:,1)-comp_real(:,:,3)+comp_real(:,:,5)-comp_real(:,:,7); + 1-comp_real(:,:,2)+comp_real(:,:,6)]; else data_out = [(data_in>=0); (abs(data_in)>(4/sqrt(21))); (abs(data_in)>=(2/sqrt(21)))&(abs(data_in)<=(6/sqrt(21)))]; end @@ -324,21 +413,21 @@ classdef PAMmapper data_in Signal options.symbol_levels = [] end - + %A) normally return the preproduct of the decision a = squeeze(repmat(real(data_in.signal),[1 1 length(obj.thresholds)])); %Eingangssignal in 3 spalten b = squeeze(repmat(reshape(obj.thresholds(:).',[1 1 length(obj.thresholds)]),[1 length(data_in.signal) 1])); %Threshold in 3 Spalten comp_real = a > b; %check for each symbol/ sampling if it exeeds the obj.thresholdseshold 1, 2 or 3 - + data_out = data_in; data_out.signal = sum(comp_real,2); - + %Option: return the actual level values/ just map onto given %symbol levels if ~isempty(options.symbol_levels) data_out.signal = options.symbol_levels(data_out.signal+1); end - + end function [out] = separate_pamlevels(obj,data_in) @@ -347,7 +436,7 @@ classdef PAMmapper a = squeeze(repmat(real(data_in.signal),[1 1 length(obj.thresholds)])); %Eingangssignal in 3 spalten b = squeeze(repmat(reshape(obj.thresholds(:).',[1 1 length(obj.thresholds)]),[1 length(data_in.signal) 1])); %Threshold in 3 Spalten comp_real = a > b; %check for each symbol/ sampling if it exeeds the obj.thresholdseshold 1, 2 or 3 - comp_real_sum = sum(comp_real,2); + comp_real_sum = sum(comp_real,2); out = NaN(length(data_in),length(obj.thresholds)+1); @@ -358,7 +447,7 @@ classdef PAMmapper end function [Signal_out] = quantize(obj,Signal_in) - + constellation = obj.get_levels(); constellation = constellation ./ rms(constellation); @@ -366,9 +455,9 @@ classdef PAMmapper if isa(Signal_in,'Signal') issignalclass = 1; Sig_class = Signal_in; - Signal_in = Signal_in.signal; + Signal_in = Signal_in.signal; end - + [~,high_dim_sig] = max(size(Signal_in)); [~,high_dim_const] = max(size(constellation)); @@ -381,7 +470,7 @@ classdef PAMmapper Signal_out = constellation(symbol_idx); Signal_out = reshape(Signal_out,size(Signal_in)); - if issignalclass + if issignalclass Sig_class.signal = Signal_out; Signal_out = Sig_class; end @@ -390,6 +479,10 @@ classdef PAMmapper end + function bitmap = showBitMapping(obj) + bitmap = obj.demap([obj.levels ./ obj.scaling]'); + end + end end diff --git a/Classes/01_transmit/Pulseformer.m b/Classes/01_transmit/Pulseformer.m index dc7d0d1..cbf62f3 100644 --- a/Classes/01_transmit/Pulseformer.m +++ b/Classes/01_transmit/Pulseformer.m @@ -6,8 +6,10 @@ classdef Pulseformer end properties(Access=public) - fdac + fs fsym + matched_sps + output_sps pulse pulselength alpha @@ -20,8 +22,10 @@ classdef Pulseformer % Detailed explanation goes here arguments - options.fdac double + options.fs double options.fsym double + options.matched_sps double + options.output_sps double options.pulse pulseform = pulseform.rc options.pulselength double {mustBeInteger} = 32 options.alpha double = 0.05 @@ -50,7 +54,11 @@ classdef Pulseformer signalclass_in = signalclass_in.logbookentry(lbdesc); % write fs to signal - signalclass_in.fs = obj.fdac;%.* (obj.fdac./obj.fsym); + if obj.matched + signalclass_in.fs = obj.fsym .* obj.output_sps;%.* (obj.fdac./obj.fsym); + else + signalclass_in.fs = obj.fs;%.* (obj.fdac./obj.fsym); + end % write to output signalclass_out = signalclass_in; @@ -77,15 +85,19 @@ classdef Pulseformer data_out end - if ~rem(obj.fdac,obj.fsym) + if ~isempty(obj.output_sps) + obj.fsym = obj.fsym.*obj.output_sps; + end + + if ~rem(obj.fs,obj.fsym) %ist ein Vielfaches - sps = obj.fdac / obj.fsym; + sps = obj.fs / obj.fsym; p = sps; q = 1; else %ist kein Vielfaches - p = obj.fsym / gcd(obj.fdac, obj.fsym); %upsampling p->->-> - q = obj.fdac/ gcd(obj.fdac, obj.fsym); %downsampling <-q + p = obj.fsym / gcd(obj.fs, obj.fsym); %upsampling p->->-> + q = obj.fs/ gcd(obj.fs, obj.fsym); %downsampling <-q sps= q; %sps während dem pulse shaping end @@ -105,6 +117,7 @@ classdef Pulseformer end manual_cyclic_convolution = 0; + upsample_filter = 0; upfirdn_convolution = 1; if manual_cyclic_convolution @@ -122,16 +135,28 @@ classdef Pulseformer data_out=ifft( fft(symbolov.') .* repmat( H,size(data_in,1),1 ).' ).'; data_out = circshift(data_out,[0 -(obj.pulselength*sps)]); - if rem(obj.fdac,obj.fsym) + if rem(obj.fs,obj.fsym) data_out = data_out(1:q:end); end end + if upsample_filter + data_out_ = upsample(data_in,p); + + mfOutput = filter(h, 1, data_out_); % Matched filter output + + figure() + hold on + stem(mfOutput(1:1000),'Marker','o','MarkerSize',1,'LineStyle','-','LineWidth',1); + stem(data_out_(1:1000),'Marker','o','MarkerSize',1,'LineStyle','-','LineWidth',1); + + + end + if upfirdn_convolution %Apply Filter using Matlab build in fctn. - data_out_ = upfirdn(data_in,h,p,q); %cut signal, which is longer due to fir filter diff --git a/Classes/04_DSP/Equalizer/FFE.m b/Classes/04_DSP/Equalizer/FFE.m index 2f414ec..54d0b57 100644 --- a/Classes/04_DSP/Equalizer/FFE.m +++ b/Classes/04_DSP/Equalizer/FFE.m @@ -101,6 +101,17 @@ classdef FFE < handle end x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)]; + + if training + mask = ones(obj.order,1); + else + mask = zeros(obj.order,1); + mask(900:end) = 1; + mask(ceil(length(obj.e)/2)) = 1; + end + + mask = ones(obj.order,1); + for epoch = 1 : epochs symbol = 0; @@ -110,7 +121,7 @@ classdef FFE < handle U = x(obj.order+sample-1:-1:sample); - y(symbol,1) = obj.e.' * U; % Calculating output of LMS __ * | + y(symbol,1) = (obj.e.*mask).' * U; % Calculating output of LMS __ * | if training d_hat(symbol,1) = d(symbol); @@ -129,7 +140,6 @@ classdef FFE < handle normalizationfactor = (U.' * U); obj.e = obj.e - err(symbol) * U / normalizationfactor; % Weight update rule of NLMS end - obj.error(epoch,symbol) = err(symbol) * err(symbol)'; % Instantaneous square error diff --git a/Classes/04_DSP/Equalizer/Postfilter.m b/Classes/04_DSP/Equalizer/Postfilter.m index 9a3119b..8d09ef1 100644 --- a/Classes/04_DSP/Equalizer/Postfilter.m +++ b/Classes/04_DSP/Equalizer/Postfilter.m @@ -43,12 +43,12 @@ classdef Postfilter < handle if ~isnan(options.useBurg) && options.useBurg - disp('using burg alg') + % disp('using burg alg') obj.coefficients = arburg(noiseclass_in.signal,obj.ncoeff); elseif ~isempty(options.coefficients) - disp('using given taps') + % disp('using given taps') obj.coefficients = options.coefficients; obj.useBurg = 0; diff --git a/Classes/04_DSP/Sequence Detection/MLSE.m b/Classes/04_DSP/Sequence Detection/MLSE.m index 59b9c33..9576524 100644 --- a/Classes/04_DSP/Sequence Detection/MLSE.m +++ b/Classes/04_DSP/Sequence Detection/MLSE.m @@ -70,7 +70,7 @@ classdef MLSE < handle %%%% Separate the equalized signal into the respective levels based on the actually transmitted level constellation = unique(data_ref); decisionLevels = (constellation(1:end-1) + constellation(2:end)) / 2; - tx_bits = PAMmapper(numel(constellation),0).demap(data_ref); + tx_bits = PAMmapper(numel(constellation),0,"eth_style",1).demap(data_ref); % impulse respnse i.e. [0.5, 1.0000] @@ -188,10 +188,50 @@ classdef MLSE < handle end + % Compute soft output PAM4 stream from the metric_sym + soft_output = zeros(length(data_in),1); % Expected symbol value per stage + symbol_prob = zeros(length(data_in), length(states)); % Store full probability distribution (optional) + llp = llp'; + for n = 1:length(data_in) + metrics = llp(n, :); % A posteriori metric for each PAM4 candidate + % For numerical stability, subtract the maximum metric before exponentiating + maxMetric = min(metrics); + expMetrics = exp(metrics - maxMetric); + probs = expMetrics / sum(expMetrics); % Normalize to get probabilities + symbol_prob(n, :) = probs; % (Optional) store distribution for analysis + % Compute the soft output as the expected value of the PAM4 symbols + soft_output(n) = sum(probs .* constellation'); + end + + % Number of symbols and bits per symbol + num_symbols = constellation; + num_bits = 2; % 2 bits per symbol + + bit_mapping = PAMmapper(4,0,"eth_style",1).showBitMapping; % Each row corresponds to the symbol above + + % Initialize LLR storage + llr = zeros(num_bits, length(data_in)); + + % Compute bit-wise LLRs + for bit_idx = 1:num_bits + % Find indices where bit is 0 and where it is 1 + idx_bit_0 = find(bit_mapping(:,bit_idx) == 0); + idx_bit_1 = find(bit_mapping(:,bit_idx) == 1); + + % Sum over log-probabilities (Max-Log approximation: using min instead of sum) + llr(:,bit_idx) = min(llp(:,idx_bit_1), [], 1) - min(llp(:,idx_bit_0), [], 1); + + end + + % Convert LLR values to a hard-decision bit stream + bit_stream = llr < 0; + [~,~,ber_llr,~] = calc_ber(bit_stream',tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); + fprintf('LLR BER : %.2e \n',ber_llr); + % directly decide based on lowest LLP index [~,llp_based_state_seq]=min(llp); LLP_EST(1:length(data_in)) = constellation(llp_based_state_seq); - rx_bits = PAMmapper(numel(constellation),0).demap(LLP_EST'); + rx_bits = PAMmapper(numel(constellation),0,"eth_style",1).demap(LLP_EST'); [~,~,ber_llp,~] = calc_ber(rx_bits,tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); fprintf('LLP BER : %.2e \n',ber_llp); % [~,~,ber_llp,~] = calc_ber(circshift(rx_bits,1),tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); @@ -200,7 +240,7 @@ classdef MLSE < handle % fprintf('LLP BER -1: %.2e \n',ber_llp); %%%% DECIDE based on Viterbi traceback - rx_bits = PAMmapper(numel(constellation),0).demap(VITERBI_ESTIMATION_SYMBOLS'); + rx_bits = PAMmapper(numel(constellation),0,"eth_style",1).demap(VITERBI_ESTIMATION_SYMBOLS'); [~,~,ber_viterbi,~] = calc_ber(rx_bits,tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); fprintf('Viterbi BER: %.2e \n',ber_viterbi); % [~,~,ber_viterbi,~] = calc_ber(circshift(rx_bits,1),tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); @@ -209,14 +249,14 @@ classdef MLSE < handle % directly decide based on the FW path metrics [~,fw_direct_state_seq]=min(pm_survivor_fw); FW_EST(1:length(data_in)) = constellation(fw_direct_state_seq); - rx_bits = PAMmapper(numel(constellation),0).demap(FW_EST'); + rx_bits = PAMmapper(numel(constellation),0,"eth_style",1).demap(FW_EST'); [~,~,ber_fw,~] = calc_ber(rx_bits,tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); fprintf('FW BER: %.2e \n',ber_fw); % directly decide based on the BW path metrics [~,bw_direct_state_seq]=min(pm_survivor_bw); BW_EST(1:length(data_in)) = constellation(bw_direct_state_seq(2:end)); - rx_bits = PAMmapper(numel(constellation),0).demap(BW_EST'); + rx_bits = PAMmapper(numel(constellation),0,"eth_style",1).demap(BW_EST'); [~,~,ber_bw,~] = calc_ber(rx_bits,tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); fprintf('BW BER: %.2e \n',ber_bw); % [~,~,ber_viterbi,~] = calc_ber(circshift(rx_bits,1),tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); @@ -224,6 +264,8 @@ classdef MLSE < handle % [~,~,ber_viterbi,~] = calc_ber(circshift(rx_bits,-1),tx_bits,"skip_front",100,"skip_end",150,"returnErrorLocation",1); % fprintf('BW BER: %.2e \n',ber_viterbi); + PAMmapper(4,0,"eth_style",1).showBitMapping + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% tx_symbolpos = zeros(numel(constellation),length(data_ref)); diff --git a/Classes/04_DSP/TransmissionPerformance.m b/Classes/04_DSP/TransmissionPerformance.m index 4e1c7ee..1550e40 100644 --- a/Classes/04_DSP/TransmissionPerformance.m +++ b/Classes/04_DSP/TransmissionPerformance.m @@ -70,7 +70,7 @@ classdef TransmissionPerformance CODE_RATE_HDFEC = [1/(1+0.067)]; %Beyond 300 Gbps Short-Reach Links Using TFLN MZMs With 500 mVpp and Linear Equalization BERTHRESHOLDS_HDFEC = 3.8e-3; - CODE_RATE_O_FEC = [1/(1+0.15)]; %Stefano im Meeting + CODE_RATE_O_FEC = [1/(1+0.153)]; %Stefano im Meeting BERTHRESHOLDS_O_FEC = 2e-2; @@ -162,6 +162,11 @@ classdef TransmissionPerformance netrates.KP4_hamming.NetRate = NaN(1, numMeasurements); netrates.KP4_hamming.CodeRate = NaN(1, numMeasurements); netrates.KP4_hamming.Threshold = NaN(1, numMeasurements); + + netrates.O_FEC.GrossRate = NaN(1, numMeasurements); + netrates.O_FEC.NetRate = NaN(1, numMeasurements); + netrates.O_FEC.CodeRate = NaN(1, numMeasurements); + netrates.O_FEC.Threshold = NaN(1, numMeasurements); end % Process each measurement individually. @@ -215,6 +220,22 @@ classdef TransmissionPerformance netrates.KP4_hamming.Threshold(i) = obj.BERTHRESHOLDS_KP4_AND_INNER(idxBER); end + idxBER = []; + for j = length(obj.BERTHRESHOLDS_O_FEC):-1:1 + if ber(i) <= obj.BERTHRESHOLDS_O_FEC(j) + idxBER = j; + break; + end + end + if ~isempty(idxBER) + codeRate = obj.CODE_RATE_O_FEC(idxBER); + netrates.O_FEC.NetRate(i) = grossRate(i) * codeRate; + netrates.O_FEC.GrossRate(i) = grossRate(i) ; + netrates.O_FEC.CodeRate(i) = codeRate; + netrates.O_FEC.Threshold(i) = obj.BERTHRESHOLDS_O_FEC(idxBER); + end + + end diff --git a/Classes/DataBaseHandler/DBHandler.m b/Classes/DataBaseHandler/DBHandler.m index 1495209..32de7c5 100644 --- a/Classes/DataBaseHandler/DBHandler.m +++ b/Classes/DataBaseHandler/DBHandler.m @@ -45,15 +45,15 @@ classdef DBHandler < handle else error('DB seems to be corrupt') end - + end function obj = refresh(obj) - % Get table names and the first rows of each table to understand the structure - obj.getTableNames(); - obj.getTables(); - obj.getDistinctValues(); + % Get table names and the first rows of each table to understand the structure + obj.getTableNames(); + obj.getTables(); + obj.getDistinctValues(); end function obj = getTableNames(obj) @@ -187,10 +187,11 @@ classdef DBHandler < handle if isstruct(newRow) fields = fieldnames(newRow); emptyFields = structfun(@isempty,newRow); - if sum(emptyFields)>0 - newRow.(fields{emptyFields==1}) = NaN; - disp(['In Table: ',tableName,': ',fields{emptyFields==1},' was empty, is now NaN ',newRow.(fields{emptyFields==1})]) - + if sum(emptyFields) > 0 + emptyFieldNames = fields(emptyFields); % use () to get a cell array + for idx = 1:numel(emptyFieldNames) + newRow.(emptyFieldNames{idx}) = NaN; + end end newRow = struct2table(newRow); end @@ -274,87 +275,168 @@ classdef DBHandler < handle end end - function addBEREntry(obj, berValue, occurrence, runID, ffe, dfe, mlse, pf, eqType, ffe_order, dfe_order, len_tr, mu_ffe, mu_dfe, mu_dc, comment) - % addBEREntry Adds a BER entry linked to an existing or new Equalizer entry. - % Usage: - % addBEREntry(runID, eq, ffe, dfe, mlse, pf, eqType, ffe_order, dfe_order, len_tr, mu_ffe, mu_dfe, mu_dc, berValue, comment) - if isempty(pf) - postfilter_taps = []; + function resultID = addProcessingResult(obj, run_id, resultData, eqParamsData) + % addProcessingResult Adds a processing result and links it to an EqualizerParameters entry. + % + % Inputs: + % run_id: A run_id from the main table to connect the BER with. + % + % resultData: A struct with fields corresponding to the ProcessingResults table. + % + % eqParamsData: A struct with fields corresponding to the EqualizerParameters table, + % except 'eq_id' and 'config_hash'. These fields are used to compute + % a hash and check for an existing configuration. + % + % Output: + % resultID: The result_id of the newly inserted ProcessingResults entry. + + % 1. Compute hash for equalizer parameters + jsonStr = jsonencode(eqParamsData); + md = java.security.MessageDigest.getInstance('MD5'); + md.update(uint8(jsonStr)); + hashBytes = typecast(md.digest, 'uint8'); + hashStr = lower(dec2hex(hashBytes)'); + hashStr = lower(strtrim(hashStr(:)')); % Convert to a lowercase string + + % Add hash to equalizer parameters + eqParamsData.config_hash = hashStr; + + % 2. Check if an equalizer configuration with the same hash exists + queryStr = sprintf('SELECT eq_id FROM EqualizerParameters WHERE config_hash = ''%s''', eqParamsData.config_hash); + existingEntry = obj.fetch(queryStr); + + if ~isempty(existingEntry) + % Use existing eq_id + eq_id = existingEntry{1,1}; else - postfilter_taps = pf.burg_coeff; + % Insert the new equalizer configuration and get its eq_id + eq_id = obj.appendToTable('EqualizerParameters', eqParamsData); end - % Create equalizer data struct for searching and adding if necessary - equalizerData = struct( ... - 'ffe', jsonencode(ffe), ... - 'dfe', jsonencode(dfe), ... - 'mlse', jsonencode(mlse), ... - 'pf', jsonencode(pf), ... - 'eq_type', string(eqType), ... - 'ffe_order', jsonencode(ffe_order), ... - 'dfe_order', jsonencode(dfe_order), ... - 'postfilter_taps',jsonencode(postfilter_taps),... - 'len_tr', len_tr, ... - 'mu_ffe', jsonencode(mu_ffe), ... - 'mu_dfe', mu_dfe, ... - 'mu_dc', mu_dc, ... - 'comment', comment ... - ); + % 3. Add the equalizer configuration reference and run_id to resultData + resultData.eqParam_id = eq_id; + resultData.run_id = run_id; - % Check if exact Equalizer and BER entries already exist in the DB ... - selectedFields = {'Runs.run_id','BERs.ber_id','Equalizer.eq_id','BERs.ber',['BERs.occurrence' ... - '']}; - filterParams = obj.tables; - filterParams.Equalizer = equalizerData; - [dataTable,sql_query] = obj.queryDB(filterParams, selectedFields); + % 4. Compute hash for the processing result + tempResultData = rmfield(resultData, 'date_of_processing'); + resultJsonStr = jsonencode(tempResultData); + md2 = java.security.MessageDigest.getInstance('MD5'); % Create a new MD5 instance + md2.update(uint8(resultJsonStr)); + resultHashBytes = typecast(md2.digest, 'uint8'); + resultHashStr = lower(dec2hex(resultHashBytes)'); + resultHashStr = lower(strtrim(resultHashStr(:)')); % Convert to a lowercase string - % get or insert Equalizer - if ~isempty(dataTable) - % Equalizer entry already exists, use the existing eq_id - cur_eq_id = dataTable.eq_id; - else - % Insert the new Equalizer entry - cur_eq_id = obj.appendToTable('Equalizer', equalizerData); + % Add the result hash to resultData + resultData.result_hash = resultHashStr; + + % 5. Check if an identical processing result already exists + queryStr2 = sprintf('SELECT result_id FROM Results WHERE result_hash = ''%s''', resultData.result_hash); + existingResult = obj.fetch(queryStr2); + + if ~isempty(existingResult) + % If the result exists, return its result_id without inserting a new row + resultID = existingResult{1,1}; + warning(['Result already exists: ResultID: ',num2str(resultID),'| EQ ID: ',num2str(eq_id),' Run ID: ' num2str(run_id)]) + return; end - % skip if already here or insert BER entry - if ~isempty(dataTable) + % 6. Insert the processing result + resultID = obj.appendToTable('Results', resultData); + end - % A BER entry with the same eq_id, run_id, and occurrence already exists - existingBERValue = dataTable.ber; + function recalcHashes(obj) + % recalcHashes Recalculate hashes for all rows in the Results and EqualizerParameters tables. + % + % For EqualizerParameters, the hash is computed from all fields except + % 'eq_id' and 'config_hash'. + % + % For Results, the hash is computed from all fields except 'result_id', + % 'result_hash', and 'date_of_processing'. - % Compare the existing BER value with the new BER value - if existingBERValue == berValue - fprintf('The BER entry %.2e || -- eq_id: %d -- run_id: %d -- occurrence: %d already exists. \n',berValue, cur_eq_id, runID, occurrence); - else - fprintf('Already found BER for EQ: %.2e ~= %.2e || -- eq_id: %d -- run_id: %d -- occurrence: %d already exists.\n', berValue, existingBERValue, cur_eq_id, runID, occurrence); + % Recalculate hashes for EqualizerParameters + eqParamsRows = obj.fetch('SELECT * FROM EqualizerParameters'); + for i = 1:height(eqParamsRows) + rowStruct = table2struct(eqParamsRows(i,:)); % Convert the table row to a struct + + % Remove fields not part of the hash computation + if isfield(rowStruct, 'eq_id') + rowStruct = rmfield(rowStruct, 'eq_id'); + end + if isfield(rowStruct, 'config_hash') + rowStruct = rmfield(rowStruct, 'config_hash'); end - else - - % No such BER entry exists, insert the new BER entry - berData = struct( ... - 'run_id', runID, ... - 'eq_id', cur_eq_id, ... - 'ber', berValue, ... - 'occurrence', occurrence ... - ); - - obj.appendToTable('BERs', berData); + % Compute MD5 hash from the JSON representation + jsonStr = jsonencode(rowStruct); + md = java.security.MessageDigest.getInstance('MD5'); + md.update(uint8(jsonStr)); + hashBytes = typecast(md.digest, 'uint8'); + hashStr = lower(dec2hex(hashBytes)'); + hashStr = lower(strtrim(hashStr(:)')); + % Update the config_hash field using the eq_id from the table row + eq_id = eqParamsRows.eq_id(i); + updateQuery = sprintf('UPDATE EqualizerParameters SET config_hash = ''%s'' WHERE eq_id = %d', hashStr, eq_id); + obj.executeSQL(updateQuery); end + + % Fetch all rows from the Results table using queryDB with no filters + Results = obj.tables.Results; + + if isstruct(Results) + newFields = {}; + fNames = fieldnames(Results); + for i = 1:numel(fNames) + newFields{end+1} = ['Results.' fNames{i}]; + end + Results = newFields; + end + [resultsRows, ~] = obj.queryDB(obj.tables, Results); + + for i = 1:height(resultsRows) + rowStruct = table2struct(resultsRows(i, :)); % Convert the row to a struct + + % Remove fields not used in the hash calculation + if isfield(rowStruct, 'result_id') + rowStruct = rmfield(rowStruct, 'result_id'); + end + if isfield(rowStruct, 'result_hash') + rowStruct = rmfield(rowStruct, 'result_hash'); + end + if isfield(rowStruct, 'date_of_processing') + rowStruct = rmfield(rowStruct, 'date_of_processing'); + end + + % Compute MD5 hash from the JSON representation + jsonStr = jsonencode(rowStruct); + md = java.security.MessageDigest.getInstance('MD5'); + md.update(uint8(jsonStr)); + hashBytes = typecast(md.digest, 'uint8'); + hashStr = lower(dec2hex(hashBytes)'); + hashStr = lower(strtrim(hashStr(:)')); + + % Access the result_id from the table row and update the hash + result_id = resultsRows.result_id(i); + updateQuery = sprintf('UPDATE Results SET result_hash = ''%s'' WHERE result_id = %d', ... + hashStr, result_id); + obj.executeSQL(updateQuery); + end + + + + end + function executeSQL(obj, query) % This method executes an SQL statement using MATLAB's execute function. execute(obj.conn, query); end - function answer = fetch(obj,query) answer = fetch(obj.conn,query); end @@ -405,15 +487,46 @@ classdef DBHandler < handle function query = constructSQLQuery(obj, filterParams, selectedFields) % constructSQLQuery Constructs the SQL query based on filter parameters and selected fields. + % + % If selectedFields is provided as a struct, it is converted to a cell array. + % The conversion takes the field names and creates entries like: + % {'selectedFields.fieldName'} for each field. - % Construct the SELECT clause dynamically based on user selection + % Input check for selectedFields: if it's a struct, convert it to a cell array. + if isstruct(selectedFields) + newFields = {}; + tableNames = fieldnames(selectedFields); + for t = 1:numel(tableNames) + tableStruct = selectedFields.(tableNames{t}); + fieldNames = fieldnames(tableStruct); + for f = 1:numel(fieldNames) + if isequal(tableStruct.(fieldNames{f}), 1) + newFields{end+1} = sprintf('%s.%s', tableNames{t}, fieldNames{f}); + end + end + end + selectedFields = newFields; + end + + % Construct the SELECT clause dynamically based on user selection. + % (Assuming that when provided as a cell array, each entry is of the form + % 'TableName.fieldName' or, in our conversion case, 'selectedFields.fieldName'.) selectClause = 'SELECT DISTINCT '; for i = 1:numel(selectedFields) fieldParts = strsplit(selectedFields{i}, '.'); - tableName = fieldParts{1}; - fieldName = fieldParts{2}; + % If the field comes from the struct conversion, its first part is 'selectedFields' + % and the actual field name is in the second part. + if strcmp(fieldParts{1}, 'selectedFields') + tableName = fieldParts{1}; % not used for type checking below + fieldName = fieldParts{2}; + else + tableName = fieldParts{1}; + fieldName = fieldParts{2}; + end - if isnumeric(obj.tables.(tableName).(fieldName)) + % Decide on COALESCE depending on the field type. + % If the table is known in obj.tables and the field is numeric, use 'NaN'. + if isfield(obj.tables, tableName) && isfield(obj.tables.(tableName), fieldName) && isnumeric(obj.tables.(tableName).(fieldName)) selectClause = [selectClause, 'COALESCE(', selectedFields{i}, ', ''NaN'') AS ', fieldName]; else selectClause = [selectClause, 'COALESCE(', selectedFields{i}, ', '''') AS ', fieldName]; @@ -426,235 +539,302 @@ classdef DBHandler < handle end end - % Construct the FROM and WHERE clause - baseQuery = [selectClause, 'FROM Runs ' ... - 'LEFT JOIN Configurations ON Runs.run_id = Configurations.run_id ' ... - 'LEFT JOIN Measurements ON Runs.run_id = Measurements.run_id ' ... - 'WHERE ']; - % 'LEFT JOIN BERs ON Runs.run_id = BERs.run_id ' ... - % 'LEFT JOIN Equalizer ON BERs.eq_id = Equalizer.eq_id ' ... - + % --- Adaptive FROM Clause --- + % Use "Runs" as the main table and add LEFT JOINs for every other table in obj.tables + % (except "sqlite_sequence") that has a run_id field. + mainTable = 'Runs'; + fromClause = ['FROM ', mainTable, ' ']; + tableNamesAll = fieldnames(obj.tables); + for t = 1:numel(tableNamesAll) + tableName = tableNamesAll{t}; + if strcmpi(tableName, mainTable) || strcmpi(tableName, 'sqlite_sequence') + continue; + end - % Loop through each table in filterParams + if isfield(obj.tables.(tableName), 'run_id') + % most tables are directly linked to runs table + fromClause = [fromClause, 'LEFT JOIN ', tableName, ' ON ', mainTable, '.run_id = ', tableName, '.run_id ']; + elseif isfield(obj.tables.(tableName), 'eq_id') + % equalizer is only linked to results table + fromClause = [fromClause, 'LEFT JOIN ', tableName, ' ON ', 'Results', '.eqParam_id = ', tableName, '.eq_id ']; + end + end + + % --- WHERE Clause Construction --- + baseQuery = [selectClause, ' ', fromClause, 'WHERE ']; filterClauses = []; tableNames_ = fieldnames(filterParams); - for t = 1:numel(tableNames_) tableName = tableNames_{t}; tableParams = filterParams.(tableName); - - % Loop through each parameter in the table fieldNames = fieldnames(tableParams); for i = 1:numel(fieldNames) fieldName = fieldNames{i}; value = tableParams.(fieldName); - - % Construct the full column name in the format "tableName.fieldName" fullName = sprintf('%s.%s', tableName, fieldName); - % Handle different types of values for SQL query construction + % Handle various types of values for SQL query construction if isempty(value) - % Skip this parameter if it is empty (include all values) continue; elseif isnumeric(value) && isnan(value) - % If value is NaN, use IS NULL in SQL filterClause = sprintf('%s IS NULL', fullName); elseif isnumeric(value) && ~isEnumeration(value) filterClause = sprintf('%s = %f', fullName, value); elseif islogical(value) || (isnumeric(value) && ismember(value, [0, 1])) && ~isEnumeration(value) filterClause = sprintf('%s = %d', fullName, value); elseif ischar(value) || isstring(value) - filterClause = sprintf('%s = ''%s''', fullName, char(value)); %nicht nach string suchen sondern nach chararray -> 'bla' statt "bla" + filterClause = sprintf('%s = ''%s''', fullName, char(value)); elseif isEnumeration(value) filterClause = sprintf('%s = ''%s''', fullName, value); else error('Unsupported data type for field "%s".', fullName); end - - % Add the constructed filter clause to the list filterClauses = [filterClauses, filterClause, ' AND ']; end end - % Remove trailing ' AND ' from the filter clauses if any filters were added + % Remove trailing ' AND ' if any filters were added. if ~isempty(filterClauses) filterClauses = filterClauses(1:end-5); - end - - % Construct the final SQL query - if isempty(filterClauses) - query = [selectClause, 'FROM Runs ' ... - 'LEFT JOIN Configurations ON Runs.run_id = Configurations.run_id ' ... - 'LEFT JOIN Measurements ON Runs.run_id = Measurements.run_id ' ... - 'LEFT JOIN BERs ON Runs.run_id = BERs.run_id']; + query = [selectClause, ' ', fromClause, 'WHERE ', filterClauses]; else - query = [baseQuery, filterClauses]; + query = [selectClause, ' ', fromClause]; end - end - - function selectedFields = promptSelectFields(obj) - % promptSelectFields Prompts the user to select fields from multiple tables to include in the SELECT statement using settingsdlg. + % promptSelectFields Prompts the user to select fields from multiple tables + % using a custom checkbox GUI with scrolling. + % + % The function builds a list of all fields (formatted as 'TableName.fieldName') + % and displays each as a checkbox inside an inner container panel. The container's + % height is set to accommodate all checkboxes, so the scrollable panel shows scrollbars. + % When the user clicks the "Select" button, the selected fields are returned. + % If none are selected, all fields are returned. - % Get all possible fields from all tables (excluding sqlite_sequence) + % Get all possible tables (excluding sqlite_sequence) tableNames = fieldnames(obj.tables); - tableNames = setdiff(tableNames, {'sqlite_sequence'}); % Remove sqlite_sequence - - % Prepare the inputs for settingsdlg - promptSettings = {}; - allFieldsFullName = {}; - convertedFieldNames = {}; + tableNames = setdiff(tableNames, {'sqlite_sequence'}); + % Build a single cell array of all field names with table prefix. + allFields = {}; for i = 1:numel(tableNames) - tableFields = fieldnames(obj.tables.(tableNames{i})); - for j = 1:numel(tableFields) - fieldName = tableFields{j}; - fullName = sprintf('%s.%s', tableNames{i}, fieldName); - convertedName = strrep(fullName, '.', '_'); % Replace '.' with '_' - - allFieldsFullName{end + 1} = fullName; % Add full name to the list - convertedFieldNames{end + 1} = convertedName; % Store the converted name - - % Add the field name and checkbox setting to the prompt - promptSettings{end + 1} = {sprintf('Include %s', fullName), convertedName}; - promptSettings{end + 1} = false; % Default: not selected + fields = fieldnames(obj.tables.(tableNames{i})); + for j = 1:numel(fields) + allFields{end+1} = sprintf('%s.%s', tableNames{i}, fields{j}); end end + numFields = numel(allFields); - % Create the settings dialog - [settings, button] = settingsdlg(... - 'title', 'Select Fields for the SQL Query', ... - 'description', 'Check the boxes for the fields you want to include in the SELECT statement.', ... - promptSettings{:} ... - ); + % Create the main figure. + fig = uifigure('Name', 'Select Fields', 'Position', [100, 100, 400, 600]); - % If the user cancels, default to selecting all fields - if strcmp(button, 'cancel') - selectedFields = allFieldsFullName; - return; + % Create a scrollable panel. + scrollPanel = uipanel(fig, 'Position', [10, 60, 380, 530], 'Scrollable', 'on'); + + % Define checkbox dimensions. + checkboxHeight = 30; + spacing = 5; + totalHeight = numFields * (checkboxHeight + spacing); + + % Create an inner container panel with height larger than the scrollPanel's height. + container = uipanel(scrollPanel, 'Position', [0, 0, scrollPanel.Position(3), totalHeight]); + + % Create checkboxes using absolute positioning in the container. + checkboxes = gobjects(numFields, 1); + for i = 1:numFields + % Calculate the vertical position. + % The origin (0,0) is at the bottom left of the container. + yPos = totalHeight - i*(checkboxHeight + spacing) + spacing; + checkboxes(i) = uicheckbox(container, ... + 'Text', allFields{i}, ... + 'Value', false, ... + 'Position', [10, yPos, container.Position(3)-20, checkboxHeight]); end - % Parse user input into selectedFields + % Create a "Select" button in the main figure. + btn = uibutton(fig, 'Text', 'Select', ... + 'Position', [150, 10, 100, 30], ... + 'ButtonPushedFcn', @(btn, event) uiresume(fig)); + + % Wait for the user to click the button. + uiwait(fig); + + % Retrieve the selected fields. selectedFields = {}; - for i = 1:numel(allFieldsFullName) - convertedName = convertedFieldNames{i}; - if isfield(settings, convertedName) && settings.(convertedName) % Add to selectedFields if the checkbox was selected - selectedFields{end + 1} = allFieldsFullName{i}; %#ok + for i = 1:numFields + if checkboxes(i).Value + selectedFields{end+1} = checkboxes(i).Text; end end - % If no fields are selected, default to selecting all fields + % If no fields are selected, default to all fields. if isempty(selectedFields) - selectedFields = allFieldsFullName; + selectedFields = allFields; end + + % Close the figure. + delete(fig); end + + function filterParams = promptFilterParameters(obj) - % promptFilterParameters Prompts the user to enter filter parameters using the settingsdlg framework. + % promptFilterParameters Prompts the user to enter filter parameters using a + % custom scrollable UI with dropdowns. + % + % For each table (excluding 'sqlite_sequence'), each field that has distinct + % values is displayed as a label and a dropdown. The dropdown items are built + % from the distinct values (with "All" prepended). The output is a struct where, + % for each table, each field is set to the chosen value (or [] if "All" is selected). - % Get all possible parameters from all tables (excluding sqlite_sequence) - tableNames_ = fieldnames(obj.tables); - tableNames_ = setdiff(tableNames_, {'sqlite_sequence'}); % Remove sqlite_sequence + % Get all tables except 'sqlite_sequence' + tableNames = fieldnames(obj.tables); + tableNames = setdiff(tableNames, {'sqlite_sequence'}); - % Prepare the inputs for settingsdlg with sections and separators - promptSettings = {}; - allFieldsFullName = {}; - convertedFieldNames = {}; + % Precompute layout constants. + heightPerTableLabel = 30; + heightPerField = 40; % vertical space for a field (label + dropdown) + spacing = 5; - for i = 1:numel(tableNames_) - % Add a separator for each table section - promptSettings{end + 1} = 'separator'; - promptSettings{end + 1} = tableNames_{i}; - - % Get all fields from the current table - tableFields = fieldnames(obj.tables.(tableNames_{i})); - - % Prepare each field to be added to the dialog + % Compute total required height. + totalHeight = 0; + for i = 1:numel(tableNames) + totalHeight = totalHeight + heightPerTableLabel; + tableName = tableNames{i}; + tableFields = fieldnames(obj.tables.(tableName)); for j = 1:numel(tableFields) fieldName = tableFields{j}; - fullName = sprintf('%s.%s', tableNames_{i}, fieldName); - convertedName = strrep(fullName, '.', '_'); % Replace '.' with '_' - - % Skip fields that do not have distinct values stored - if ~isfield(obj.distinctValues.(tableNames_{i}), fieldName) - continue; + % Only include fields that have distinct values stored. + if isfield(obj.distinctValues.(tableName), fieldName) + totalHeight = totalHeight + heightPerField; end - - % Get the distinct values for the field - distinctValues_ = obj.distinctValues.(tableNames_{i}).(fieldName); - - % Prepare distinct values for dropdown - if isempty(distinctValues_) - % If there are no distinct values, use only an "All" entry - distinctValues_ = {'All'}; - else - % Ensure distinctValues is a cell array of strings - if isnumeric(distinctValues_) - distinctValues_ = arrayfun(@(x) num2str(x), distinctValues_, 'UniformOutput', false); - elseif isstring(distinctValues_) - distinctValues_ = cellstr(distinctValues_); - elseif iscell(distinctValues_) && ~iscellstr(distinctValues_) - distinctValues_ = cellfun(@num2str, distinctValues_, 'UniformOutput', false); - end - - % Add an "All" option at the beginning of the distinct values list - distinctValues_ = [{'All'}; distinctValues_]; - end - - allFieldsFullName{end + 1} = fullName; % Add full name to the list - convertedFieldNames{end + 1} = convertedName; % Store the converted name - - % Add the field name and value setting to the prompt - promptSettings{end + 1} = {sprintf('%s', fullName), convertedName}; - promptSettings{end + 1} = distinctValues_; % Add distinct values as dropdown options end end - % Create the settings dialog - [settings, button] = settingsdlg(... - 'title', 'Input Parameters for Filtering', ... - 'description', 'Enter the values for each field to filter. Select "All" to include all values.', ... - promptSettings{:} ... - ); + % Create the main UI figure. + fig = uifigure('Name', 'Input Parameters for Filtering', 'Position', [100, 100, 500, 600]); - % If the user cancels, return an empty struct - if strcmp(button, 'cancel') + % Set a CloseRequestFcn so that closing the figure calls uiresume. + fig.CloseRequestFcn = @(src, event) uiresume(src); + + % Create a scrollable panel inside the figure. + scrollPanel = uipanel(fig, 'Position', [10, 60, 480, 530], 'Scrollable', 'on'); + + % Create an inner container panel with a height set to totalHeight. + container = uipanel(scrollPanel, 'Position', [0, 0, scrollPanel.Position(3), totalHeight]); + + % Prepare cell arrays to store dropdown handles and corresponding table/field names. + dropdownHandles = {}; + dropdownTableNames = {}; + dropdownFieldNames = {}; + + % Set the starting Y coordinate (filling from top to bottom). + currentY = totalHeight; + + % Maximum number of dropdown items. + maxItems = 100; + + for i = 1:numel(tableNames) + % Create a label for the table name. + uilabel(container, ... + 'Text', tableNames{i}, ... + 'FontWeight', 'bold', ... + 'Position', [10, currentY - heightPerTableLabel + spacing, 200, heightPerTableLabel - spacing]); + currentY = currentY - heightPerTableLabel; + + tableName = tableNames{i}; + tableFields = fieldnames(obj.tables.(tableName)); + for j = 1:numel(tableFields) + fieldName = tableFields{j}; + if ~isfield(obj.distinctValues.(tableName), fieldName) + continue; % Skip if no distinct values are stored. + end + + % Retrieve distinct values for the field. + distinctValues_ = obj.distinctValues.(tableName).(fieldName); + if ~isempty(distinctValues_) && numel(distinctValues_) > maxItems + distinctValues_ = distinctValues_(1:maxItems); + end + + if isempty(distinctValues_) + items = {'All'}; + else + if isnumeric(distinctValues_) + items = cellfun(@num2str, num2cell(distinctValues_), 'UniformOutput', false); + elseif isstring(distinctValues_) + items = cellstr(distinctValues_); + elseif iscell(distinctValues_) && ~iscellstr(distinctValues_) + items = cellfun(@num2str, distinctValues_, 'UniformOutput', false); + else + items = distinctValues_; + end + items = items(:)'; % Ensure row vector + items = [{'All'}, items]; + end + + % Create a label for the field. + uilabel(container, ... + 'Text', sprintf('%s:', fieldName), ... + 'HorizontalAlignment', 'right', ... + 'Position', [10, currentY - 25, 150, 25]); + + % Create a dropdown for the field. + dd = uidropdown(container, ... + 'Items', items, ... + 'Value', 'All', ... + 'Position', [170, currentY - 25, 200, 25]); + + % Store the dropdown handle and its associated table/field. + dropdownHandles{end+1} = dd; + dropdownTableNames{end+1} = tableName; + dropdownFieldNames{end+1} = fieldName; + + currentY = currentY - heightPerField; + end + end + + % Create a "Submit" button at the bottom of the figure. + btn = uibutton(fig, 'Text', 'Submit', ... + 'Position', [200, 10, 100, 30], ... + 'ButtonPushedFcn', @(btn, event) uiresume(fig)); + + % Wait until the user clicks "Submit" or closes the figure. + uiwait(fig); + + % If the figure was closed (and thus no longer valid), return an empty struct. + if ~isvalid(fig) filterParams = struct(); return; end - % Parse user input into filterParams structure + % Build the filterParams struct from the dropdown selections. filterParams = struct(); - - for i = 1:numel(allFieldsFullName) - value = settings.(convertedFieldNames{i}); - - % Split full name to get table and field names - fieldParts = strsplit(allFieldsFullName{i}, '.'); - tableName = fieldParts{1}; - fieldName = fieldParts{2}; - - % If the table does not exist in the filterParams struct, create it + for k = 1:numel(dropdownHandles) + tableName = dropdownTableNames{k}; + fieldName = dropdownFieldNames{k}; + value = dropdownHandles{k}.Value; if ~isfield(filterParams, tableName) filterParams.(tableName) = struct(); end - % Assign values to the respective fields under each table + % If "All" is selected, assign empty; otherwise, try converting to numeric. if strcmp(value, 'All') - filterParams.(tableName).(fieldName) = []; % Set to empty to include all values - elseif isnumeric(value) && isnan(value) - filterParams.(tableName).(fieldName) = NaN; % Use NaN to handle as NULL + filterParams.(tableName).(fieldName) = []; else - filterParams.(tableName).(fieldName) = value; % Use the entered value + numValue = str2double(value); + if ~isnan(numValue) + filterParams.(tableName).(fieldName) = numValue; + else + filterParams.(tableName).(fieldName) = value; + end end end + + % Close the figure. + delete(fig); end - - - end end diff --git a/Classes/Moveit_wrapper.m b/Classes/Moveit_wrapper.m index 025a464..96dce6a 100644 --- a/Classes/Moveit_wrapper.m +++ b/Classes/Moveit_wrapper.m @@ -39,17 +39,22 @@ classdef Moveit_wrapper < handle end - function signalclass_out = process(obj,signal_in) + function signal_out = process(obj,signal_in) + + arguments + obj + signal_in = [] + + end isSignalClass = 0; if isa(signal_in,"Signal") - signalclass_out = signal_in; + signal_out = signal_in; signal_in = signal_in.signal; isSignalClass = 1; end - input_len = length(signal_in); - signal_in = signal_in.'; + % signal_in = signal_in.'; % INIT MOVEIT global loop; @@ -59,18 +64,14 @@ classdef Moveit_wrapper < handle % RUN MOVEIT global loop; loop = 1; - signal_out = obj.moveit_init(signal_out); - % CHECK IF SIGNAL CHANGED - if input_len ~= length(signal_out) - warning(['Signal length changed in moveit function: ', obj.moveit_function_name]); - end + signal_out = obj.moveit_init(signal_out); if isSignalClass % append to logbook lbdesc = ['Logbookentry']; - signalclass_out = signalclass_out.logbookentry(lbdesc); - signalclass_out.signal = signal_out; + signal_out = signal_out.logbookentry(lbdesc); + signal_out.signal = signal_out; end end diff --git a/Datatypes/equalizer_structure.m b/Datatypes/equalizer_structure.m index 263260b..c5bdeb2 100644 --- a/Datatypes/equalizer_structure.m +++ b/Datatypes/equalizer_structure.m @@ -4,8 +4,9 @@ classdef equalizer_structure < int32 ffe (0) vnle (1) vnle_pf_mlse (2) - db_precoded (3) - db_encoded (4) + % db_precoded (3) + % db_encoded (4) + vnle_db_mlse (3) end end \ No newline at end of file diff --git a/Functions/EQ_structures/duobinary_target.m b/Functions/EQ_structures/duobinary_target.m index d2d3671..cbb880e 100644 --- a/Functions/EQ_structures/duobinary_target.m +++ b/Functions/EQ_structures/duobinary_target.m @@ -1,36 +1,42 @@ function [eq_package] = duobinary_target(eq_, mlse_,M, rx_signal, tx_symbols, tx_bits, options) arguments - eq_ - mlse_ - M - rx_signal - tx_symbols - tx_bits + eq_ + mlse_ + M + rx_signal + tx_symbols + tx_bits options.precode_mode db_mode options.showAnalysis = 0; options.eth_style_symbol_mapping = 0; + options.postFFE = []; end - - %Duobinary Targeting - [eq_signal, eq_noise] = eq_.process(rx_signal,Duobinary().encode(tx_symbols)); +%Duobinary Targeting +db_ref_sequence = Duobinary().encode(tx_symbols); +db_ref_constellation = unique(db_ref_sequence.signal); +[eq_signal, eq_noise] = eq_.process(rx_signal,db_ref_sequence); - % dir = [1,1]; - mlse_sig_sd = mlse_.process(eq_signal); +if ~isempty(options.postFFE) + [eq_signal,eq_noise] = options.postFFE.process(eq_signal,db_ref_sequence); +end - mlse_sig_hd = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).quantize(mlse_sig_sd); - - % precoding to mitigate error propagation, most prominently used in - % combination with duobinary signaling to avoid catastrophic error - % behavior (see J.W.M. Bergmans, Digital Baseband Transmission and Recording -> partial response signaling) +% dir = [1,1]; +mlse_sig_sd = mlse_.process(eq_signal); - % takes: - % -> eq_signal_hd: hard decision signal after eq - % -> tx_symbols: that where used as reference for eq +mlse_sig_hd = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).quantize(mlse_sig_sd); - switch options.precode_mode - case db_mode.db_emulate +% precoding to mitigate error propagation, most prominently used in +% combination with duobinary signaling to avoid catastrophic error +% behavior (see J.W.M. Bergmans, Digital Baseband Transmission and Recording -> partial response signaling) + +% takes: +% -> eq_signal_hd: hard decision signal after eq +% -> tx_symbols: that where used as reference for eq + +switch options.precode_mode + case db_mode.db_emulate mlse_sig_hd = Duobinary().encode(mlse_sig_hd,"M",M); mlse_sig_hd = Duobinary().decode(mlse_sig_hd,"M",M); @@ -40,43 +46,102 @@ end tx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols_precoded); - case db_mode.db_discard + case db_mode.db_discard % normal dsp for precoded sequence == discard/omit/ignore precode tx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols); - case db_mode.db_encoded + case db_mode.db_encoded % normal DB encoded data (only for 10KM) - case db_mode.db_precoded + case db_mode.db_precoded mlse_sig_hd = Duobinary().encode(mlse_sig_hd,"M",M); mlse_sig_hd = Duobinary().decode(mlse_sig_hd,"M",M); - end +end - % M = numel(unique(tx_symbols.signal)); - rx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd); +% M = numel(unique(tx_symbols.signal)); +rx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd); - [~,numErrors,ber,~] = calc_ber(rx_bits.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1); +[bits_db,errors_db,ber_db,errorIndice_db] = calc_ber(rx_bits.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1); +eq_package.ber = ber_db; - - eq_package.ber = ber; +resultsDBtgt = struct( ... + 'result_id', NaN, ... % + 'run_id', NaN, ... % Beispielhafte Run-ID + 'eqParam_id', NaN, ... % Beispielhafter Fremdschlüssel zur EqualizerParameters-Tabelle + 'date_of_processing', datetime('now'), ... % Aktuelles Datum und Uhrzeit + 'numBits', bits_db, ... % Beispiel: 1.000.000 Bits + 'numBitErr', errors_db, ... % Beispiel: 120 Bitfehler + 'BER', ber_db, ... % BER = 120 / 1.000.000 + 'SNR', [], ... % Beispielhafte SNR + 'SNR_level', jsonencode([]), ... % SNR-Level als JSON-codiertes Array + 'GMI', [], ... % Beispielhafter GMI-Wert + 'AIR', [], ... % Beispielhafter AIR-Wert + 'EVM', [], ... % Beispielhafte EVM + 'EVM_level', jsonencode([]), ... % EVM-Level als JSON-codiertes Array + 'Alpha', [] ... % Beispielhafter Alpha-Wert + ); - if options.showAnalysis - eq_noise = eq_noise - mean(eq_noise.signal); +if ~isempty(options.postFFE) + npostFFE = options.postFFE.order; +else + npostFFE = 0; +end - rx_signal.spectrum("normalizeTo0dB",1,"fignum",250,"displayname","Rx Spectrum"); +equalizerConfigDBtgt = struct( ... + 'eq_id', NaN, ... % Auto-Inkrement, wird in der DB gesetzt + 'equalizer_structure', int32(equalizer_structure.vnle_db_mlse), ... % Beispiel: 1 (z.B. für vnle) + 'M', M, ... % Ordnung der PAM-Konstellation + 'target_constellation', jsonencode(round(db_ref_constellation,5)), ... % Beispielhafter Target-String + 'db_target', 1, ... % 0 oder 1 + 'diff_precode', int32(options.precode_mode), ... % 0 oder 1 + 'postFFE', ~isempty(options.postFFE), ... % Beispielwert + 'NpostFFE', npostFFE, ... % Beispielwert + 'Ne1', eq_.Ne(1), ... % Feedforward Koeffizienten 1. Ordnung + 'Ne2', eq_.Ne(2), ... % Feedforward Koeffizienten 2. Ordnung + 'Ne3', eq_.Ne(3), ... % Feedforward Koeffizienten 3. Ordnung + 'Nb1', eq_.Nb(1), ... % Decision Feedback Koeffizienten 1. Ordnung + 'Nb2', eq_.Nb(2), ... % Decision Feedback Koeffizienten 2. Ordnung + 'Nb3', eq_.Nb(3), ... % Decision Feedback Koeffizienten 3. Ordnung + 'K', eq_.K, ... % Samples pro Symbol + 'DCmu', eq_.DCmu, ... % Anpassungsrate für DC-Tap + 'ideal_dfe', eq_.ideal_dfe, ... % Flag für ideal DFE (0 oder 1) + 'training_length', eq_.training_length, ... % Anzahl Trainingssymbole + 'training_loops', eq_.training_loops, ... % Anzahl Trainingsdurchläufe + 'TRmu1', eq_.FFEmu, ... % mu für DD-Modus (1. Ordnung) + 'TRmu2', eq_.FFEmu, ... % mu für DD-Modus (2. Ordnung) + 'TRmu3', eq_.FFEmu, ... % mu für DD-Modus (3. Ordnung) + 'TRmuDFE', eq_.DFEmu, ... % mu für DFE-Modus im DD + 'dd_loops', 5, ... % Anzahl Durchläufe im DD-Modus + 'DDmu1', eq_.DDmu(1), ... % mu für DD-Modus (1. Ordnung) + 'DDmu2', eq_.DDmu(2), ... % mu für DD-Modus (2. Ordnung) + 'DDmu3', eq_.DDmu(3), ... % mu für DD-Modus (3. Ordnung) + 'DDmuDFE', eq_.DDmu(4), ... % mu für DFE-Modus im DD + 'MLSE_mode', 'viterbi', ... % Beispiel: MLSE-Modus als String + 'MLSE_trellis_states', jsonencode(mlse_.trellis_states), ... % Trellis-States, z.B. als JSON-String oder kommasepariert + 'comment', 'function: duobinary_target.m', ... % Zusätzliche Kommentare + 'config_hash', NaN ... + ); - Duobinary().encode(tx_symbols).spectrum("normalizeTo0dB",1,"fignum",250,"displayname","DB encoded reference"); +eq_package.resultsDBtgt = resultsDBtgt; +eq_package.equalizerConfigDBtgt = equalizerConfigDBtgt; - showEQNoisePSD(eq_noise,"fignum",250,"displayname",'Duobinary Target Noise after Equalization'); +if options.showAnalysis + eq_noise = eq_noise - mean(eq_noise.signal); - fprintf('DB tgt BER: %.2e \n',ber); + rx_signal.spectrum("normalizeTo0dB",1,"fignum",250,"displayname","Rx Spectrum"); - - end + Duobinary().encode(tx_symbols).spectrum("normalizeTo0dB",1,"fignum",250,"displayname","DB encoded reference"); + + showEQNoisePSD(eq_noise,"fignum",250,"displayname",'Duobinary Target Noise after Equalization'); + + fprintf('DB tgt BER: %.2e \n',ber); + + +end end \ No newline at end of file diff --git a/Functions/EQ_structures/vnle.m b/Functions/EQ_structures/vnle.m index 3bcc18a..1447351 100644 --- a/Functions/EQ_structures/vnle.m +++ b/Functions/EQ_structures/vnle.m @@ -23,6 +23,7 @@ function [eq_package] = vnle(eq_,M,rx_signal,tx_symbols,tx_bits,options) options.precode_mode db_mode options.showAnalysis = 0 options.eth_style = 0; + options.postFFE = []; end %FFE or VNLE @@ -32,6 +33,10 @@ function [eq_package] = vnle(eq_,M,rx_signal,tx_symbols,tx_bits,options) end [eq_signal_sd,eq_noise] = eq_.process(rx_signal,tx_symbols); + if ~isempty(options.postFFE) + [eq_signal_sd,eq_noise] = options.postFFE.process(eq_signal_sd,tx_symbols); + end + eq_signal_hd = PAMmapper(M,0).quantize(eq_signal_sd); % precoding to mitigate error propagation, most prominently used in @@ -89,15 +94,34 @@ function [eq_package] = vnle(eq_,M,rx_signal,tx_symbols,tx_bits,options) fprintf('SNR: %d dB \n',snr(eq_signal_sd.signal,eq_noise.signal)); + if M == 6 + logm = 2.5; + else + logm = log2(M); + end + + fprintf('NGMI: %.4f \n', inf_rate/logm); + fprintf(['VNLE EVM lvl: ',repmat('%.3f ',1,numel(evm_lvl)),' \n'],evm_lvl); fprintf('VNLE BER: %.2e \n',ber); - showEQcoefficients(eq_.e,eq_.e2,eq_.e3,"displayname",'Coefficients'); + disp("%%%%%%%%%%%%%%%%%%%%%") - showEQNoiseSNR(tx_symbols,eq_noise,"displayname",'SNR after VNLE','fignum',301); + % showEQcoefficients('n1',eq_.e,'n2',eq_.e2,'n3',eq_.e3,"displayname",'Coefficients'); + % + % if ~isempty(options.postFFE) + % showEQcoefficients('n1',options.postFFE.e,"displayname",'Coefficients'); + % end + % + % showEQNoisePSD(eq_noise); + % + % showEQfilter(eq_.e,eq_signal_sd.fs.*2) + + % noiselessness(tx_symbols,eq_noise,"displayname",'SNR after VNLE','fignum',301); + + % showLevelHistogram(eq_signal_sd,tx_symbols,"fignum",302); - showLevelHistogram(eq_signal_sd,tx_symbols,"fignum",302); end diff --git a/Functions/EQ_structures/vnle_postfilter_mlse.m b/Functions/EQ_structures/vnle_postfilter_mlse.m index dc1501f..aa5e716 100644 --- a/Functions/EQ_structures/vnle_postfilter_mlse.m +++ b/Functions/EQ_structures/vnle_postfilter_mlse.m @@ -11,11 +11,17 @@ arguments options.precode_mode db_mode options.showAnalysis = 0; options.eth_style_symbol_mapping = 0; + options.postFFE = []; + options.database = []; end %FFE or VNLE [eq_signal_sd,eq_noise] = eq_.process(rx_signal,tx_symbols); + if ~isempty(options.postFFE) + [eq_signal_sd,eq_noise] = options.postFFE.process(eq_signal_sd,tx_symbols); + end + eq_signal_hd = PAMmapper(M,0).quantize(eq_signal_sd); mlse_sig_sd = pf_.process(eq_signal_sd,eq_noise); @@ -70,28 +76,152 @@ end % METRICS OF VNLE % rx_bits_vnle = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(eq_signal_hd); - [~,~,ber_vnle,~] = calc_ber(rx_bits_vnle.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1); - + [bits_vnle,errors_vnle,ber_vnle,errorIndice_vnle] = calc_ber(rx_bits_vnle.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1); + [snr_vnle,snr_vnle_lvl] = calc_snr(tx_symbols.signal,eq_noise.signal); + % correct TUM implementation of AIR - [inf_rate_vnle] = calc_air(eq_signal_sd,tx_symbols,"skip_front",10000,"skip_end",10000); + [gmi_vnle] = calc_air(eq_signal_sd,tx_symbols,"skip_front",10000,"skip_end",10000); + gmi_vnle = min(max(abs(gmi_vnle),0.1),0); %set to zero if no convergence of gmi below 0.1 to avoid negative or any other "dumb" value + air_vnle = tx_symbols.fs .* floor(log2(8)*10)/10 .* gmi_vnle; + [evm_vnle_total,evm_vnle_lvl] = calc_evm(eq_signal_sd,tx_symbols); % METRICS OF MLSE (HD-VITERBI) rx_bits_mlse = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd); - [~,~,ber_mlse,~] = calc_ber(rx_bits_mlse.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1); + [bits_mlse,errors_mlse,ber_mlse,errorIndice_mlse]= calc_ber(rx_bits_mlse.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1); + + pf_.ncoeff = 1; + pf_.process(eq_signal_sd,eq_noise); + alpha = pf_.coefficients(2); eq_package.ber_mlse = ber_mlse; eq_package.ber_vnle = ber_vnle; eq_package.evm_vnle_total = evm_vnle_total; eq_package.evm_vnle_lvl = evm_vnle_lvl; - eq_package.air = inf_rate_vnle; + eq_package.gmi = gmi_vnle; eq_package.eq = eq_; eq_package.pf = pf_; eq_package.mlse = mlse_; + resultsVNLE = struct( ... + 'result_id', NaN, ... % + 'run_id', NaN, ... % Beispielhafte Run-ID + 'eqParam_id', NaN, ... % Beispielhafter Fremdschlüssel zur EqualizerParameters-Tabelle + 'date_of_processing', datetime('now'), ... % Aktuelles Datum und Uhrzeit + 'numBits', bits_vnle, ... % Beispiel: 1.000.000 Bits + 'numBitErr', errors_vnle, ... % Beispiel: 120 Bitfehler + 'BER', ber_vnle, ... % BER = 120 / 1.000.000 + 'SNR', snr_vnle, ... % Beispielhafte SNR + 'SNR_level', jsonencode(snr_vnle_lvl), ... % SNR-Level als JSON-codiertes Array + 'GMI', gmi_vnle, ... % Beispielhafter GMI-Wert + 'AIR', air_vnle, ... % Beispielhafter AIR-Wert + 'EVM', evm_vnle_total, ... % Beispielhafte EVM + 'EVM_level', jsonencode(evm_vnle_lvl), ... % EVM-Level als JSON-codiertes Array + 'Alpha', [] ... % Beispielhafter Alpha-Wert +); + if ~isempty(options.postFFE) + npostFFE = options.postFFE.order; + else + npostFFE = 0; + end + + equalizerConfigVNLE = struct( ... + 'eq_id', NaN, ... % Auto-Inkrement, wird in der DB gesetzt + 'equalizer_structure', int32(equalizer_structure.vnle), ... % Beispiel: 1 (z.B. für vnle) + 'M', M, ... % Ordnung der PAM-Konstellation + 'target_constellation', jsonencode(round(unique(tx_symbols.signal),5)), ... % Beispielhafter Target-String + 'db_target', 0, ... % 0 oder 1 + 'diff_precode', int32(options.precode_mode), ... % 0 oder 1 + 'postFFE', ~isempty(options.postFFE), ... % Beispielwert + 'NpostFFE', npostFFE, ... % Beispielwert + 'Ne1', eq_.Ne(1), ... % Feedforward Koeffizienten 1. Ordnung + 'Ne2', eq_.Ne(2), ... % Feedforward Koeffizienten 2. Ordnung + 'Ne3', eq_.Ne(3), ... % Feedforward Koeffizienten 3. Ordnung + 'Nb1', eq_.Nb(1), ... % Decision Feedback Koeffizienten 1. Ordnung + 'Nb2', eq_.Nb(2), ... % Decision Feedback Koeffizienten 2. Ordnung + 'Nb3', eq_.Nb(3), ... % Decision Feedback Koeffizienten 3. Ordnung + 'K', eq_.K, ... % Samples pro Symbol + 'DCmu', eq_.DCmu, ... % Anpassungsrate für DC-Tap + 'ideal_dfe', eq_.ideal_dfe, ... % Flag für ideal DFE (0 oder 1) + 'training_length', eq_.training_length, ... % Anzahl Trainingssymbole + 'training_loops', eq_.training_loops, ... % Anzahl Trainingsdurchläufe + 'TRmu1', eq_.FFEmu, ... % mu für DD-Modus (1. Ordnung) + 'TRmu2', eq_.FFEmu, ... % mu für DD-Modus (2. Ordnung) + 'TRmu3', eq_.FFEmu, ... % mu für DD-Modus (3. Ordnung) + 'TRmuDFE', eq_.DFEmu, ... % mu für DFE-Modus im DD + 'dd_loops', 5, ... % Anzahl Durchläufe im DD-Modus + 'DDmu1', eq_.DDmu(1), ... % mu für DD-Modus (1. Ordnung) + 'DDmu2', eq_.DDmu(2), ... % mu für DD-Modus (2. Ordnung) + 'DDmu3', eq_.DDmu(3), ... % mu für DD-Modus (3. Ordnung) + 'DDmuDFE', eq_.DDmu(4), ... % mu für DFE-Modus im DD + 'comment', 'function: vnle_postfilter_mlse', ... % Zusätzliche Kommentare + 'config_hash', NaN ... +); + + + resultsMLSE = struct( ... + 'result_id', NaN, ... % + 'run_id', NaN, ... % Beispielhafte Run-ID + 'eqParam_id', NaN, ... % Beispielhafter Fremdschlüssel zur EqualizerParameters-Tabelle + 'date_of_processing', datetime('now'), ... % Aktuelles Datum und Uhrzeit + 'numBits', bits_mlse, ... % Beispiel: 1.000.000 Bits + 'numBitErr', errors_mlse, ... % Beispiel: 120 Bitfehler + 'BER', ber_mlse, ... % BER = 120 / 1.000.000 + 'SNR', [], ... % Beispielhafte SNR + 'SNR_level', jsonencode([]), ... % SNR-Level als JSON-codiertes Array + 'GMI', [], ... % Beispielhafter GMI-Wert + 'AIR', [], ... % Beispielhafter AIR-Wert + 'EVM', [], ... % Beispielhafte EVM + 'EVM_level', jsonencode([]), ... % EVM-Level als JSON-codiertes Array + 'Alpha', alpha, ... % Beispielhafter Alpha-Wert + 'MLSE_dir', jsonencode([mlse_.DIR])... +); + + + + equalizerConfigMLSE = struct( ... + 'eq_id', NaN, ... % Auto-Inkrement, wird in der DB gesetzt + 'equalizer_structure', int32(equalizer_structure.vnle_pf_mlse), ... % Beispiel: 1 (z.B. für vnle) + 'M', M, ... % Ordnung der PAM-Konstellation + 'target_constellation', jsonencode(round(unique(tx_symbols.signal),5)), ... % Beispielhafter Target-String + 'db_target', 0, ... % 0 oder 1 + 'diff_precode', int32(options.precode_mode), ... % 0 oder 1 + 'postFFE', ~isempty(options.postFFE), ... % Beispielwert + 'NpostFFE', npostFFE, ... % Beispielwert + 'Ne1', eq_.Ne(1), ... % Feedforward Koeffizienten 1. Ordnung + 'Ne2', eq_.Ne(2), ... % Feedforward Koeffizienten 2. Ordnung + 'Ne3', eq_.Ne(3), ... % Feedforward Koeffizienten 3. Ordnung + 'Nb1', eq_.Nb(1), ... % Decision Feedback Koeffizienten 1. Ordnung + 'Nb2', eq_.Nb(2), ... % Decision Feedback Koeffizienten 2. Ordnung + 'Nb3', eq_.Nb(3), ... % Decision Feedback Koeffizienten 3. Ordnung + 'K', eq_.K, ... % Samples pro Symbol + 'DCmu', eq_.DCmu, ... % Anpassungsrate für DC-Tap + 'ideal_dfe', eq_.ideal_dfe, ... % Flag für ideal DFE (0 oder 1) + 'training_length', eq_.training_length, ... % Anzahl Trainingssymbole + 'training_loops', eq_.training_loops, ... % Anzahl Trainingsdurchläufe + 'TRmu1', eq_.FFEmu, ... % mu für DD-Modus (1. Ordnung) + 'TRmu2', eq_.FFEmu, ... % mu für DD-Modus (2. Ordnung) + 'TRmu3', eq_.FFEmu, ... % mu für DD-Modus (3. Ordnung) + 'TRmuDFE', eq_.DFEmu, ... % mu für DFE-Modus im DD + 'dd_loops', 5, ... % Anzahl Durchläufe im DD-Modus + 'DDmu1', eq_.DDmu(1), ... % mu für DD-Modus (1. Ordnung) + 'DDmu2', eq_.DDmu(2), ... % mu für DD-Modus (2. Ordnung) + 'DDmu3', eq_.DDmu(3), ... % mu für DD-Modus (3. Ordnung) + 'DDmuDFE', eq_.DDmu(4), ... % mu für DFE-Modus im DD + 'MLSE_mode', 'viterbi', ... % Beispiel: MLSE-Modus als String + 'MLSE_trellis_states', jsonencode(mlse_.trellis_states), ... % Trellis-States, z.B. als JSON-String oder kommasepariert + 'comment', 'function: vnle_postfilter_mlse', ... % Zusätzliche Kommentare + 'config_hash', NaN ... +); + eq_package.resultsVNLE = resultsVNLE; + eq_package.resultsMLSE = resultsMLSE; + eq_package.equalizerConfigVNLE = equalizerConfigVNLE; + eq_package.equalizerConfigMLSE = equalizerConfigMLSE; + + % eq_package.vnle_out = eq_signal_sd; if options.showAnalysis % fprintf(['VNLE EVM lvl: ',repmat('%.3f ',1,numel(evm_lvl)),' \n'],evm_lvl); @@ -110,13 +240,13 @@ end showLevelHistogram(eq_signal_sd,tx_symbols) % showLevelHistogram(mlse_sig_sd,tx_symbols) - showEQcoefficients(eq_.e,eq_.e2,eq_.e3,"displayname",'Coefficients'); + showEQcoefficients('n1',eq_.e,'n2',eq_.e2,'n3',eq_.e3,"displayname",'Coefficients'); showEQNoiseSNR(tx_symbols,eq_noise,"displayname",'vnle snr','fignum',101); %%% EQ SNR Spectrum %230 %snr - snr_vnle = snr(tx_symbols.signal,eq_noise.signal); + % showErrorBurstCount(eq_signal_sd,tx_symbols) diff --git a/Functions/EQ_visuals/showEQcoefficients.m b/Functions/EQ_visuals/showEQcoefficients.m index 3794212..cb747b8 100644 --- a/Functions/EQ_visuals/showEQcoefficients.m +++ b/Functions/EQ_visuals/showEQcoefficients.m @@ -1,58 +1,67 @@ -function showEQcoefficients(n1, n2, n3, options) - % Show filter coefficients as stem plot - % n1, n2, and n3 in different subplots - % Scale all y-axis to -1 and 1 +function showEQcoefficients(options) + % Show filter coefficients as stem plots. + % Only the provided coefficient arrays (n1, n2, n3) are shown, + % each in its own subplot. The y-axis is scaled to [-1, 1]. arguments - n1 - n2 - n3 - options.fignum (1,1) double = NaN % Default to NaN if not provided - options.displayname (1,:) char = '' % Default to an empty string if not provided + options.n1 = []; + options.n2 = []; + options.n3 = []; + options.fignum (1,1) double = NaN; % Default: create new figure if NaN + options.displayname (1,:) char = ''; % Default: empty string options.color = [0.2157, 0.4941, 0.7216]; - options.clf = 0; % Clear figure before plotting new + options.clf = 0; % Clear figure before plotting if set to 1 end - % Determine the figure number to use or create a new figure + % Determine the figure number to use or create a new one. if isnan(options.fignum) - fig = figure; % Create a new figure and get its handle + fig = figure; else - fig = figure(options.fignum); % Use the specified figure number + fig = figure(options.fignum); end if options.clf - clf(fig); % Clear the figure if requested + clf(fig); end hold on ax = gca; N = numel(ax.Children); - % Set up a colormap for consistent coloring + % Set up a colormap for consistent coloring. cmap = linspecer(8); options.color = cmap(mod(N, size(cmap, 1)) + 1, :); - % Create subplots for n1, n2, n3 - for i = 1:3 - subplot(3, 1, i); - switch i - case 1 - stem(n1, 'Color', options.color, 'LineWidth', 1,'Marker','.','MarkerSize',10); - title(sprintf('1st order Filter Coefficients: %d',numel(n1))); - case 2 - stem(n2, 'Color', options.color, 'LineWidth', 1,'Marker','.','MarkerSize',10); - title(sprintf('2nd order Filter Coefficients: %d',numel(n2))); - case 3 - stem(n3, 'Color', options.color, 'LineWidth', 1,'Marker','.','MarkerSize',10); - title(sprintf('3rd order Filter Coefficients: %d',numel(n3))); - end - ylim([-1, 1]); % Scale y-axis to -1 and 1 + % Build cell arrays for coefficients and their corresponding titles. + coeffs = {}; + titles = {}; + + if ~isempty(options.n1) + coeffs{end+1} = options.n1; + titles{end+1} = sprintf('1st order Filter Coefficients: %d', numel(options.n1)); + end + if ~isempty(options.n2) + coeffs{end+1} = options.n2; + titles{end+1} = sprintf('2nd order Filter Coefficients: %d', numel(options.n2)); + end + if ~isempty(options.n3) + coeffs{end+1} = options.n3; + titles{end+1} = sprintf('3rd order Filter Coefficients: %d', numel(options.n3)); + end + + numSubplots = numel(coeffs); + + for i = 1:numSubplots + subplot(1, numSubplots, i); + stem(coeffs{i}, 'Color', options.color, 'LineWidth', 1, ... + 'Marker', '.', 'MarkerSize', 10); + title(titles{i}); + ylim([-1, 1]); % Set y-axis limits to [-1, 1] grid on; - grid minor + grid minor; xlabel('Coefficient Index'); ylabel('Amplitude'); end - % Ensure the layout is tight for better visibility - sgtitle('Filter Coefficients'); % Overall title -end \ No newline at end of file + sgtitle('Filter Coefficients'); % Overall title for the figure +end diff --git a/Functions/EQ_visuals/showEQfilter.m b/Functions/EQ_visuals/showEQfilter.m new file mode 100644 index 0000000..3ef6732 --- /dev/null +++ b/Functions/EQ_visuals/showEQfilter.m @@ -0,0 +1,36 @@ + +function showEQfilter(coefficients,fs) + + % Assuming that obj.e contains the final FFE filter coefficients. + % Set the number of frequency points and sampling frequency. + nfft = 1024; % Number of frequency points + + % Compute the frequency response of the FFE filter. + [H, f] = freqz(coefficients, 1, nfft, fs); + + % Keep only the first half of the frequency response (up to the Nyquist frequency). + half_nfft = floor(nfft/2) + 1; + f = f(1:half_nfft); + H = H(1:half_nfft); + + % Plot the magnitude and phase responses. + figure; + + % Magnitude response (in dB) + subplot(2,1,1); + hold on + plot(f.*1e-9, 20*log10(abs(1./H))); + title('(Inverted) Magnitude Response of FFE Filter'); + xlabel('Frequency (Hz)'); + ylabel('Magnitude (dB)'); + grid on; + + % Phase response + subplot(2,1,2); + plot(f.*1e-9, unwrap(angle(H))); + title('Phase Response of FFE Filter'); + xlabel('Frequency (Hz)'); + ylabel('Phase'); + grid on; + +end \ No newline at end of file diff --git a/Functions/EQ_visuals/showLevelHistogram.m b/Functions/EQ_visuals/showLevelHistogram.m index 40f31f4..66e5d08 100644 --- a/Functions/EQ_visuals/showLevelHistogram.m +++ b/Functions/EQ_visuals/showLevelHistogram.m @@ -39,7 +39,9 @@ end intermediate = received_sd(lvl,:); cnt(lvl) = round(numel(intermediate(~isnan(intermediate)))./length(eq_signal),3).*100; hold on + warning off histogram(received_sd(lvl,:),1000,"EdgeAlpha",0,'DisplayName',['Lvl ',num2str(lvl),' | ',num2str(cnt(lvl)),' %'],'FaceColor',lvlcol(lvl,:),'Normalization','pdf'); + warning on end legend grid on diff --git a/Functions/Metrics/calc_air_plain.m b/Functions/Metrics/calc_air_plain.m new file mode 100644 index 0000000..757c9c8 --- /dev/null +++ b/Functions/Metrics/calc_air_plain.m @@ -0,0 +1,40 @@ +function [airs] = calc_air_plain(noisy_signal,reference_signal,options) + % Calculation of AIR acc. to J. Kozesnik, „Numerically Computing Achievable Rates of Memoryless Channels“, Francisco Javier Garcıa-Gomez, doi: 10.1007/978-94-009-9857-5. + % Implementation is not accessible, I mailed TUM to get the code... + + arguments(Input) + noisy_signal; + reference_signal; + options.skip_front = 0; + options.skip_end = 0; + options.returnErrorLocation = 0; + end + + options.skip_end = abs(options.skip_end); + options.skip_front = abs(options.skip_front); + + assert((options.skip_end+options.skip_front)0; + +doub_mode = db_mode.no_db; + +%%% change specific parameter if given in varargin +% Parse optional input arguments +if ~isempty(varargin) + var_s = varargin{1}; + if isstruct(var_s) + fields = fieldnames(var_s); + for i = 1:numel(fields) + if isnumeric(fields{i}) + eval([fields{i}, ' = ', num2str( var_s.(fields{i}) ), ';']); + fprintf("%s <-- %.2f \n", fields{i}, var_s.(fields{i})); + else + eval([fields{i}, ' = ', 'var_s.(fields{',num2str(i),'})' , ';']); + end + + end + else + error('Optional variables should be passed as a struct.'); + end +end + + + +if doub_mode ~= db_mode.db_encoded + if precomp == 0 && db_precode == 1 + doub_mode = db_mode.db_precoded; + + db_precode = 1; % preceded data (in my measurement set, this corresponds to low precomp too!) + discard_precode = 0; % + emulate_precode = 0; + legendentry = 'low precomp; precoded'; + disp('low precomp; precoded') + + elseif precomp == 1 && db_precode == 1 + doub_mode = db_mode.db_emulate; + + db_precode = 0; % preceded data (in my measurement set, this corresponds to low precomp too!) + discard_precode = 0; % + emulate_precode = 1; + legendentry = 'high precomp; precoded'; + disp('high precomp; precoded') + + elseif precomp == 0 && db_precode == 0 + doub_mode = db_mode.db_discard; + + db_precode = 1; % preceded data (in my measurement set, this corresponds to low precomp too!) + discard_precode = 1; % + emulate_precode = 0; + legendentry = 'no precomp; not precoded'; + disp('no precomp; not precoded') + + elseif precomp == 1 && db_precode == 0 + doub_mode = db_mode.no_db; + + db_precode = 0; % preceded data (in my measurement set, this corresponds to low precomp too!) + discard_precode = 0; % + emulate_precode = 0; + legendentry = 'high precomp; not precoded'; + disp('high precomp; not precoded') + + end + +else + +end + +fsym_ = floor( bitrate*1e-9./log2(M) ).*1e9; + +if fsym_ ~= fsym + fsym = fsym_; + % fprintf('Adapted symbolrate to %d GBd, to match provided bitrate of %d GBit/s using PAM %d \n',fsym.*1e-9,bitrate.*1e-9, M); +end +f_nyquist = fsym/2; + + +basePath = 'C:\Users\Silas\Documents\MATLAB\Datensätze\sioe_labor\'; +database = DBHandler("pathToDB",[basePath,'silas_labor.db']); + +useGui = 0; +% db = DBHandler("pathToDB",[basePath,'silas_labor.db']); +filterParams = database.tables; +% filterParams.Runs.run_id = 2958; % no db +% filterParams.Runs.run_id = 2937; % no db +filterParams.Configurations = struct( ... + 'bitrate', bitrate, ... + 'db_mode', db_precode+db_encode, ... + 'fiber_length', link_length, ... + 'interference_attenuation', [], ... + 'interference_path_length', [], ... + 'is_mpi', is_mpi, ... + 'pam_level', M, ... + 'precomp_amp', [], ... + 'rop_attenuation', 0, ... + 'symbolrate', [], ... + 'v_awg', [], ... + 'v_bias', [], ... + 'wavelength', laser_wavelength ... + ); + +selectedFields = {'Runs.run_id','Runs.tx_bits_path', 'Runs.tx_symbols_path', 'Runs.rx_sync_path','Runs.rx_raw_path',... + 'Configurations.db_mode','Configurations.pam_level','Configurations.bitrate','Configurations.symbolrate','Configurations.fiber_length','Configurations.wavelength','Configurations.precomp_amp','Measurements.power_rop','Configurations.v_bias',... + 'Configurations.interference_attenuation'}; + +[dataTable,sql_query] = database.queryDB(filterParams, selectedFields); +[~, uniqueIdx] = unique(dataTable.run_id); % Get unique run_id indices +dataTable = dataTable(uniqueIdx,:); % Extract unique configurations for each run_id +fprintf('Found %d entries for requested Configuration. IDs are: %s \n \n',size(dataTable,1),jsonencode(dataTable.run_id(1:min(size(dataTable,1),100)))); + + +output = struct(); +vnle_pf_package = {}; +vnle_dfe_package = {}; +dbtgt_package = {}; + +disp(num2str(bitrate)) + +for iatt = 1:numel(dataTable.interference_attenuation) + + current_run_id = dataTable.run_id(iatt); + + Tx_bits = load([basePath, char(dataTable.tx_bits_path(iatt))]); + Tx_bits = Tx_bits.Bits; + Symbols_mapped = PAMmapper(M,0).map(Tx_bits); + Symbols_mapped.fs = fsym; + + Symbols = load([basePath, char(dataTable.tx_symbols_path(iatt))]); + Symbols = Symbols.Symbols; + + Scpe_load = load([basePath, char(dataTable.rx_sync_path(iatt))]); + Scpe_cell = Scpe_load.S; + [~,~,found]=Scpe_cell{2}.tsynch("reference",Symbols,"fs_ref",fsym,"debug_plots",1); + + if ~found + Raw_signal = load([basePath, char(dataTable.rx_raw_path(1))]); + Raw_signal = Raw_signal.Scpe_sig_raw; + [~,Scpe_cell,found] =Raw_signal.tsynch("reference",Symbols,"fs_ref",fsym,"debug_plots",0); + end + + if ~found + if length(Symbols_mapped.signal) == sum(Symbols_mapped.signal == Symbols.signal) + warning('Could not synchronize the received signal with the stored symbols!') + else + [~,Scpe_cell,found] =Raw_signal.tsynch("reference",Symbols_mapped,"fs_ref",fsym,"debug_plots",0); + end + if ~found + warning('Could not synchronize the received signal with the stored symbols!') + end + end + + + % + % Raw_signal = Filter('filtdegree',4,"f_cutoff",Symbols.fs.*0.55,"fs",Raw_signal.fs,"filterType",filtertypes.gaussian,"active",true).process(Raw_signal); + % + % Scpe_cell{1}.eye(fsym,M,"displayname",'eye','fignum',227); + % + % Raw_signal.spectrum("normalizeTo0dB",0,"fignum",11,"fft_length",2^12); + % Raw_signal.move_it_spectrum("fignum",334); + % Raw_signal.move_it_spectrum("fignum",334); + + fsym = Symbols.fs; + + if db_precode + Symbols_precoded = Symbols; + end + + + + proc_occ = min(15,length(Scpe_cell)); + for occ = 1:proc_occ + + Scpe_sig = Scpe_cell{occ}; + + %%%%%% Sample to 2x fsym %%%%%% + Scpe_sig = Scpe_sig.resample("fs_out",2*fsym); + + %%%%%% Sync Rx signal with reference %%%%%% + [Scpe_sig,~] = Scpe_sig.tsynch("reference",Symbols,"fs_ref",fsym,"debug_plots",0); + + Scpe_sig = Filter('filtdegree',4,"f_cutoff",Symbols.fs.*0.5,"fs",Scpe_sig.fs,"filterType",filtertypes.gaussian,"active",true).process(Scpe_sig); + + Scpe_sig = Scpe_sig - mean(Scpe_sig.signal); + % + % Pform = Pulseformer("fsym",Scpe_sig.fs,"fdac",2*fsym,"pulse","rrc","pulselength",16,"alpha",rcalpha,"matched",0); + % + % Scpe_sig_matched = Pform.process(Scpe_sig); + % + % Scpe_sig.spectrum("normalizeTo0dB",0,"fignum",336,"displayname","scope "); + % Scpe_sig_matched.spectrum("normalizeTo0dB",0,"fignum",336,"displayname","matched"); + + %%% EQUALIZING + + + % eq_mlse = FFE_DCremoval("epochs_tr",5,"epochs_dd",5,"len_tr",len_tr,"mu_dd",mu_ffe(1),"mu_tr",0,"order",ffe_order(1),"sps",2,"decide",0,"dc_buffer_len",1,"mu_dc",0.05); + % eq_mlse = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",len_tr,"mu_dd",mu_ffe(1),"mu_tr",0,"order",ffe_order(1),"sps",2,"decide",0); + % eq_mlse = FFE_DCremoval("epochs_tr",5,"epochs_dd",5,"len_tr",len_tr,"mu_dd",mu_ffe(1),"mu_tr",0,"order",ffe_order(1),"sps",2,"decide",0,"dc_buffer_len",512,"mu_dc",0.05); + + mu_ffe = [mu_ffe1 mu_ffe2 mu_ffe3]; + vnle_order=[vnle_order1,vnle_order2,vnle_order3]; + + % %%%%% VNLE + DFE %%%% + if 0 + + eq_vnle_dfe = EQ("Ne",vnle_order,"Nb",[0,0,0],"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",0); + eq_2 = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",2001,"sps",1,"decide",0); + + [result] = vnle(eq_vnle_dfe,M,Scpe_sig,Symbols,Tx_bits,"precode_mode",doub_mode,"showAnalysis",1,"postFFE",[]); + vnle_dfe_package{iatt,occ} = result; + + end + %%%%% VNLE + PF + MLSE %%%% + if 1 + + try + % len_tr = length(Symbols)-1000; + eq_vnle_ = EQ("Ne",vnle_order,"Nb",dfe_order,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",1); + % eq_vnle_ = VNLE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",[0.0004 0.0005 0.0006],"mu_tr",0,"order",vnle_order,"sps",2,"decide",0); + pf_ = Postfilter("ncoeff",pf_ncoeffs,"useBurg",1); + mlse_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels); + eq_2 = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",2001,"sps",1,"decide",0); + + [result] = vnle_postfilter_mlse(eq_vnle_,pf_,mlse_,M,Scpe_sig,Symbols,Tx_bits,"precode_mode",doub_mode,'showAnalysis',0,"postFFE",[]); + vnle_pf_package{iatt,occ} = result; + + database.addProcessingResult(current_run_id,result.resultsMLSE, result.equalizerConfigMLSE); + database.addProcessingResult(current_run_id,result.resultsVNLE, result.equalizerConfigVNLE); + catch + warning(['VNLE+MLSE fail: run id: ', num2str(current_run_id)],' occ:', num2str(occ), ' iatten: ',num2str(iatt)) + end + + end + + + %%%%% Duobinary Targeting %%%% + if 1 + + try + mlse_db = MLSE_viterbi("DIR",[1,1],"duobinary_output",0,"M",M,"trellis_states",PAMmapper(M,0).levels); + eq_db = EQ("Ne",vnle_order,"Nb",dfe_order,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",1); + eq_2 = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",2001,"sps",1,"decide",0); + + [result] = duobinary_target(eq_db, mlse_db, M, Scpe_sig, Symbols, Tx_bits, "precode_mode", doub_mode,'showAnalysis',0,"postFFE",[]); + dbtgt_package{iatt,occ} = result; + + database.addProcessingResult(current_run_id,result.resultsDBtgt, result.equalizerConfigDBtgt); + + catch + warning(['VNLE DB+MLSE fail: run id: ', num2str(current_run_id)],' occ:', num2str(occ), ' iatten: ',num2str(iatt)) + end + + end + + %%%%%% %db signaling => db encoded %%%%% + if 0 + mlse_db_enc = MLSE_viterbi("DIR",[1,1],"duobinary_output",0,"M",M,"trellis_states",PAMmapper(M,0).levels); + eq_db_enc = EQ("Ne",vnle_order,"Nb",dfe_order,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",1); + [result] = duobinary_signaling(eq_db_enc, mlse_db_enc,M, Scpe_sig ,Symbols, Tx_bits); + dbenc_package{iatt,occ} = result; + end + + + % autoArrangeFigures; + disp('- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ') + fprintf('\n') + + + end + + + + if ~isempty(curFolder) + cd(curFolder); + end + +end + +output.dataTable = dataTable; +output.vnle_dfe_package = vnle_dfe_package; +output.vnle_pf_package = vnle_pf_package; +output.dbtgt_package = dbtgt_package; \ No newline at end of file diff --git a/projects/HighSpeedExperiment_2024/auswertung MPI/plot_from_database.m b/projects/HighSpeedExperiment_2024/auswertung MPI/plot_from_database.m new file mode 100644 index 0000000..6eba0dd --- /dev/null +++ b/projects/HighSpeedExperiment_2024/auswertung MPI/plot_from_database.m @@ -0,0 +1,91 @@ + + +basePath = 'C:\Users\Silas\Documents\MATLAB\Datensätze\sioe_labor\'; +database = DBHandler("pathToDB",[basePath,'silas_labor.db']); + +filterParams = database.tables; +filterParams.Configurations = struct( ... + 'bitrate', 336e9, ... + 'db_mode', 0, ... + 'fiber_length', 1, ... + 'interference_attenuation', [], ... + 'interference_path_length', [], ... + 'is_mpi', 1, ... + 'pam_level', 4, ... + 'rop_attenuation', 0 ... + ); + +filterParams.EqualizerParameters.diff_precode = int32(db_mode.no_db); +filterParams.EqualizerParameters.equalizer_structure = int32(equalizer_structure.vnle); + +selectedFields = {'Configurations.run_id' 'Runs.rx_raw_path' 'Configurations.bitrate' 'Configurations.symbolrate' 'Configurations.pam_level' 'Configurations.db_mode' 'Configurations.rop_attenuation' 'Configurations.is_mpi' 'Configurations.interference_attenuation' 'EqualizerParameters.equalizer_structure' 'EqualizerParameters.diff_precode' 'EqualizerParameters.eq_id' 'Measurements.power_pd_in' 'Measurements.power_mpi_interference' 'Measurements.power_mpi_signal' 'Results.BER' 'Results.SNR' 'Results.GMI' 'Results.Alpha'}; + +[dataTable,sql_query] = database.queryDB(filterParams, selectedFields); + +fixedVars = {'run_id','eq_id','bitrate'}; +resultTable = groupIt(fixedVars,dataTable); + + + +% Create a new figure +figure(1); +hold on +unique_rates = unique(resultTable.bitrate); +for i = 1:numel(unique_rates) + % Plot BER vs. interference_attenuation + plot(resultTable.power_mpi_signal(resultTable.bitrate==unique_rates(i),:)-resultTable.power_mpi_interference(resultTable.bitrate==unique_rates(i),:), resultTable.BER(resultTable.bitrate==unique_rates(i),:), 'o-', 'LineWidth', 1.5); +end + +% Label the axes and add a title +xlabel('Interference Attenuation'); +ylabel('BER'); +title('BER vs. Interference Attenuation'); + +% Enable grid for better readability +grid on; + +beautifyBERplot; + + + + +function resultTable = groupIt(fixedVars,dataTable) + + % Group by run_id and eq_id (adjust grouping keys as needed) + + [G, groupKeys] = findgroups(dataTable(:, fixedVars)); + + % Preallocate a cell array for aggregated data. + varNames = dataTable.Properties.VariableNames; + nVars = numel(varNames); + aggData = cell(height(groupKeys), nVars); + groupCount = zeros(height(groupKeys), 1); % To store the size of each group + + % Loop over each group. + for i = 1:height(groupKeys) + idx = (G == i); % Logical index for group i + groupCount(i) = sum(idx); % Count number of rows in this group + % For each variable in the table: + for j = 1:nVars + colData = dataTable.(varNames{j}); + if isnumeric(colData) + % For numeric data, compute the mean. + aggData{i, j} = mean(colData(idx)); + else + % For non-numeric data, take the first entry. + if iscell(colData) + aggData{i, j} = colData{find(idx, 1)}; + else + aggData{i, j} = colData(find(idx, 1)); + end + end + end + end + + % Convert the aggregated cell array into a table. + resultTable = cell2table(aggData, 'VariableNames', varNames); + + % Append the group count as a new column. + resultTable.nRows = groupCount; + +end \ No newline at end of file diff --git a/projects/IMDD_base_system/analyze_burg.m b/projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_burg.m similarity index 100% rename from projects/IMDD_base_system/analyze_burg.m rename to projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_burg.m diff --git a/projects/IMDD_base_system/analyze_equalizer_taps.m b/projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_equalizer_taps.m similarity index 100% rename from projects/IMDD_base_system/analyze_equalizer_taps.m rename to projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_equalizer_taps.m diff --git a/projects/IMDD_base_system/analyze_lambda.m b/projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_lambda.m similarity index 100% rename from projects/IMDD_base_system/analyze_lambda.m rename to projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_lambda.m diff --git a/projects/IMDD_base_system/analyze_postfilter_taps.m b/projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_postfilter_taps.m similarity index 100% rename from projects/IMDD_base_system/analyze_postfilter_taps.m rename to projects/HighSpeedExperiment_2024/auswertung deliv 06 feb 2025/analyze_postfilter_taps.m diff --git a/projects/HighSpeedExperiment_2024/auswertung/App/auswertungs_app.mlapp b/projects/HighSpeedExperiment_2024/auswertung/App/auswertungs_app.mlapp index 375605d6234be5c1b157ada02284b4fd77bb504b..9fb4d4a8e63c8b0292262a7466d86ebc6f974695 100644 GIT binary patch delta 68635 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 jgk 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

AlYxkdDwS;i+0-dF+`7Y zhxDl@a32Q4&2=SltWcn2dD^SlWD(!=) z8gXFiJwG}+Iw9L3M4KQM;h8lh2nQ_$bR^-vGk;sz^r$gU)il-B3xSz`2n8jVc8t^$ zZ|{>p;n6bh;{#ZwYgkyAi_aPC;yD&;BX&nU@}_JP#RW@JzJ-NJ9xfFDDMTyo`cn9e zBsdrs@_Lq^94xL@ZMFk?5EB!_qhB)OwTrWe$<4@^Tgc9f<1=j#wO1yX@a2&n;wKK! znSZ_OJkGPNqQj}K8k^HIGpTm(fms{1q!3`At}dVo4L$I%D1x&I(PpqeX>I@SL(6|Q z|1La$okNrYxL1lGd}azQSu<-2Wu2$XasxV9?@zC)P!$u0A3$?DyStrE_;**we1MFQ z-up>W+vm(7@NP?a$0IzSxx#n(>bZw$B7e&n(EvOadNVb?pQ+P1JKi$Vf6wN5d~F5c z8^8ZP9dY_?WrQ{0j(C2305EgU2uE_Wc$RCb6W}R<^q(k#C-uTc3EK4M!N(k*nVFe^ zgiKo?Y#7n$-hVs>K4}33iPSYT!P=1@T4(%1h&1q8b43a-gE2kAurINZwUms?yXG-oZXkjKiR7#o*Szh1Ny#==u$H-umJ&$!$VHV&6lm ze}Rmuh%;`B62xgJh%^Q>jenKc>YJKQ?YABsEapj{>`mq7<|by7;A0MpZ6uMw@JV1a z_t>>+a}A4q*Nf*8f)~@)Qhgc6cE2U>V>+)ofx@sIE`fE*1dRclXk_$Q+FPHR$Rfde z?pR@wD~vT;E*+eMRm{kqwDwux#W|pYw??K8m!zXs#oKO!Ajboc(|^)|xtu`03s`k0 z2TO@LjQ?=Z`rz=0V1iw6x+AXW8P&=s!}81`2aqQZxnJ&elLoC8?OS3%rd+)x;xJTf zRbO9^=XZ8@-)9b%P=EgX?~1IgicelcXvX5k9?Xg!YqslC$Y`$~8f4IIR-?Uc8!*6* zC1zFN9YO%eDVx0Nl7B^aPA)D$pT;D&Q?N2iJ*e#j$+#V0z8;zxxt`XZMKGV^d|ia0 zvuAvp+@@C|?KA>5W+CR6#OM`2PdbziTXgi02waTQDs+(BYF3W@0Fv*V$%VH;j7($L z-rlVd{@~`8mZBP86QJ$hT_jsT#sNXe$e^$#h%ZvD?04SXOMlJt&~BP#=!ynuVh>XK zGCk%Oa%J6%LM&<(*LK^hM|2m&b&Ij)7r`(LGqNqMed8=Uuz+K%B>pp>6nmBb0Ml46 zLpsAx8GXPZ(iDG-0pR)0d&l%(qr#0pn(o}W?t66;{!<1`S z@O+C$5hTw;xPK@U#H`2(v>)h2XyFS5Xv6$Ag0QjaVLZTBLh-!rDIdP}zm{_RzgJ7E z4rnx5ztm0=G1rNY_lX+~NxU?V%^L%NoUR%#<-Rff_Y}6je`TN`xu9UZs6&dBPipJ( zg^O43@$&L=aglCO{2%RoS6EYN7cM$u8BrLNVga#HM1P7R5_)VP0)hwwf)Z4EOXww( zSOyUg5K)j4qErDP(vcP$O{Die5Gery1PC>dv$oFvoZIs}=jOlob0K8A%ePl~*ZY2Z z?GO4f?|svcF0w!E;J^nZfZof=1t_IJfrbJw{sZ}>INY3Y`KbjVZ0`UV1Ipre3_+0` ztnf|07Jn}vz*_0=?_cENQmw6v5?iMC6+?PC8*(l-oq}8#%7-?rsIR zB{{C06(8Y5P{+>Rx-{#(Pcqi|(&YFpT$z7kKc)W88CBh0_CLRIR9x--90SCuTtLw4 zB7c4q?6q7PPx4=iEfSbGfkL6E0;HL3xQE-eJ;GJcmnf2DCqt6-ldr_+lnkTdG4<~H zs#pI#Pu#5MkSke)#Yq`0_}H zvH8X~!PpM|kEmx6Q?Rp6HfVSkVT171T!5=mHH|5)S-9Y?rjVsZdIE$1fS z;Kl4P9&&3^&0ri=K8HdPHm1x8^un$r@zRZ@KBSfN?w7}bH=8XYREzvp=i9%elLAUJ zGba~m8R<&b*dqdz68nzKshl$D%TCROa3fdr-S4nB#Y3dQ>^osJv+E(WPeVT z(LQcgd#@evz#p1i^FPY;+^60Gj;63iWqfyR;aH0+@TmM#UEj%xj%;0Rz)72xy0;{$ z1DKP`=f6QD{U(JSEcellkq(RDQ*W6WC~I1Mjz}sCOSd=Sf{L!o170#W-+y#DYl3Y` zR0ZD1tups97d$May!7!mZbKuZnSY*qvE~te8H}KeEpUk}W3sN4_&HVnAX#FX%N}42&A18W^kma}q72ZRnh=-^_O}vHuAksmN zvM;@Q?P_-8HmCEN$%PJG0e{Ra^J{@Zz>1K?7oj3U?J9+A6~K58o3okb3-j}c+QAJ~ zUJ>>$sHoUjA7XDB=&6GJq1-^#D^T@a@>?z&3QH0f6HABDLf$fL9Xj6y?@*z-hK7dL zgdK{}Jkdyrf(mt#T)Acs_QhZ@nJ29oBUQ-ydLp>(6Iy~L1w}>u%zq9_aMIgK?DB(+ zbwb-bVeI=x8ER<7-@&1NmHV81`&;DIl^=CcS0WByV;aiK$+dTO4*D%L9FlkRW^XL_ z_4RS?+BN7iPP}}~rAc*p_W@Z;YIPXn<@51|c$DXKcgY27khVf%+fBWIP^B%1I03S3 z-*d42OnYrp6Y>sBrGNi=qdfcZoh2aE?~)TJOURhvi0Sr2)a{bfSTjX{do+6i&7L%-0QWWW@bK{c`GI75anhi%Xrj8h8o1x{ z=g$|&MQLf)bN_tu@$u;^a#D8w=lroq%@cd!c81s7kF2%vVFvxE%aqO87-|rM4`X`k zR=`G<2fHa8C4X&O@Y@cq+a4W%*?jMZz_4zJV+P9j-CdgQ@#=S}5vZICT=_i}5Lz`P zNj;PwOpe=FV+LP77Ls)N*cmW?s&)(#tY9`%JC6I9OXJ$T1%-wFYqWHtDDrsGanDJ{ z18czyoj5sKF=fNa?}bklpYhikGNi&9RhaEP{{v*Q{(trB&TAK(CP!rTc?aNhHf7hf z#kw%Q^iq#0keiJ?v917L{Cs@G+-xKlOG>;U@c<&hvYP|50n0Gvqs(a^qpc4Lii_E^ z>`h#HFo;qcPVxBocqmo;UU4s?qX$9$247Bk`_>ldGK12rpks%_;q2`pCre67+IH5* z$mp3>kAFheYMb3oqb`PbdD0YAD-U0T;I=OSy)VrAXNN$=nzP!-sq(K!>&fi((TAV5 z>GSgO?yjt~{47Q)&}cCsFSY7VI*DVwRoJN{6`#DqLJzn}80_&Y;h-_pfJTC|r75iW z5{4V(KiE#7+=zptM{DJ|{Oal|Lcrv5rwkq-WPceTL1l9l6fV}mAv9^?+bQx>oXuIw z+GpY+;fGaAA&JipnkjUoZ@@0d`g*LX^3}0;2gt5(-%1nD{#8c<6m50+c;V6Z(a}-= z)q3;rrX(%m6Zb@s3wWfbwaT2Oo*oQVnIc3K4+Z#6j{j<8M<)IG`8D+A9J}8G6m@iU zp?~D$bP`oQSX|$$O%&lJRkk-b|4E8*FUk)Zp)tPm@$tFbM7la2KYpw*bJeU+;G($r zeiZ;&GNDj9pu4xXwYS%G%BdKN;+w+4)Yo^Mdt$Jr6_Y(~;ZaIw#UXjj%9c)}E9 zqN^pZI*zijq)wiGX^iP{2^)zffeE&8@=HR!0<3mHD_UO0U zg&S0XJJv<1Xc74oAYl>K6JXW<0)QFzz|htblD%8NGB(ORbBnPQW;SkPr3O6++D0W6 z>Mc|RFnnpL(m9^ioWeq@(I!GK&02#|k!noFdonX^ICpKYL0weRL#9KN+-+I%FIvx} z%_5A0speX}d=fHF<5$o?BIpuJDu2CZ2azK3{neR_scqGy8ZMx7Fn)3HX$VqI0VKpj z>>V7Co>twUY<=FagXboJg(y)KuLuo5SvSV`Soy`i1g-}WlbSP$905R65~?hMLV1Cz z0-JxLjbA0QzsbwXA6D_61FVDywy1sz4nX>dZ)7>E3A}#bY~7T&F~r^&LVxN;J~C|s zaJK0dG+3t-YXsgjymfx+1~aj0#FT*HodTrx#$+I zWRJs^1TblpY|c zv1e^`iAv1W7|dD&F5s-&*!PvQe+2|=Lfsr$a3AUWgP8apN>NHl$x4Foj}cBXAir8= zSj+d=vnXtTi94-TJY*zNkIidrXlRJR1hm++3-Iy%Wn`2#;|bprDHu_cR zLT3H7&-lCG#t@FQ$s{+$qhvyo2&@`{qmxtiyLYz>?K=wW+A7yN$bk2Q0qb-mPX!+b zj4KbVZZ1;@tD`Yj^cNS~^lH46Cl&qyi>Ccz>}^eoA)Bm&(xCz}y5}0*6AlwcB~vJC zSrq){>Nr9QhdjE>gnt|q*y|#nQkCBm*bg?stjtU8YT|LX&1K)bzBBjWxADp`Y>jpV zAiqoi`J{{t0iYd9{-lgQO`9RG+r?%AFnXqRDhJ}FVzE_wx!+9>=o1uFYW<-qau}tQ zgJa_5|5a)1&5br$-jfV}6xG+$x|CnKHNzl1TIyDGPD`|;$$tpf*8?s6{VYB9qFyM! z)bn7=bkOdSVeDqAz% zF8~y9J@m$6xzEB0nP(=xOVhE+UNfHqIYnd^?=d~frlvdvcz1^j4+24nJ-a;n1N4?% zWYJtIlyJ~NVt-;{X1~cofm>-miqEl>?ID{hk>APP`FI~Ptu)FIRR;H5gd0kvNhoB& zCa(R@hG+NU8{+Q(46!avZnh<)7yMLQQ3XOGyM@(M%xq3yAV=zm25}1~R&N0o)wH0i z)a*6gS4^JbJnHpma9I=qNt&moyA+);x8?re38&%uMSofwFoy1WF4f^1C!*?*nSPR* zYM4dz%a=&A4PiZv|qHdXe?>(wj7;T&u57c%Wp$WjCkuo07yQ zw`F%Zw}3zVj$lkp;|47lJjXTUP$>3{7o{E;6OdXx%15!=A#hG)J`8d8xvtv=`udSQ z_saiRGJhG~{H2IyJbAOSvj@Qaph}Rg3OO!LRc%VZ#$#?_HUG6mVD~}$DMdw|K-R;; z!WcCw6Y<-3@4tTg_H830{t4k;^*Pb*9f6yVxH%TN?6`g^{@;QBJMe!8{_nv59r*v< zI6*sT`}5%)(v0-FtM;mBRBX=A$B137z5m72&9>)x)wQD~>)eqRWR8lq`zwT@jTx`A zw~%M7=gB_`j{iTLqICOOOGLnnoHrcOk;)~hT{8n^L%>KM?cfer>amHzj)UbNa&vRb%l$wGgFudz zPA5Cq+4%r9<`zjhTUg8z^>zblud7qQoo$d(+s=qvOR%gA`Ma^G9gA z%{eBlJZA?7y1Nx@8U=H;qxXX@0V+;d?|&Pb8LU{p81-<7HHHEXctF;&7UX>&nY0kj zCXfi5tGO06MUFl5L1@-+=Pg`=yCTPVnIGMp_w(_TjdJ2U z;Fgy`sMxo^efV%|ownG<8uJuL-E;QsiK@OZ3?j`HO4Lu}@S-Q`HfNBTQu%&y-NZ+w;K@6wqnN7w<$6v2ZfcsJih)U+gAp z)yc`J&e9c5ZYIg75-?9t^<4s?rlhM=As?xUyryGptoQ2lT~J6FD}Q%@wKK@qWn=wD zg2gg3GfNxIrOnjUtG>NFrLCA8ig@Cf$#)8;G$ z4GM%TWZ{)?L6h8jRb8MfrRN1E-XO~a?buAQ_Fxg!j$on5Ctqzp{)} zS#w|DuJw7rSW>r3=6_xqW9;nhwfq&)plK_7mlNII8XDBm_V8qMzMPz#1gn7{mA?7@ z^@k5W5Gz7`PB|aO;<(YQ)5!E7Xr20R-$q=l8{+uX_07x%Xd7FF4q4u)ryJJwOhy=c zPIYDz<1XLm@J!ghe8JuE$&+=-5$p>Bdzlbn6DsB@cp~v=j(?ci+S9Hi>esZiv{}D# zW#q{M^=Zb`8Zrgn9yIs(@ni82Y&oO4J)iMn$6@aygTdqd4n((iqhVta$z#xvf&eFZKEm8tBuV^Xgn;S8kSkM3X|>V>zrq6RTo z#G>;x7jD$u$$u0U!xm)ZTW#evX`EU?vQ z=`TePSbtPxPCDl{GBX)|SXBv|)}3diZD3$9S+;>Zoe@R{eE$0n#+$p4J;=;Qbd7y{TwJuYSt-PerK}=;al&J&ljy{B zu(vM)sS97o{&a_~+iurx-RdiGp~^dU|Eb;*!hd&aq@y1+=p}AdUc&c5h|;GA&OWdE zk!MwJlx_NwL}EvgSU)P)XKq#CJ>bG!%!vJGMv^1cLM}xMDSIM+mTB9zZ5di4C&4-U z#B`tbJ(1S@)f`2pY_2KKmM=V|>jE5ub;47yLCyIN+v% z!GBqs#ssKus>}UuZe!m~;9})@fGtA#k>Fd!ShF%uCx8D97_8DpCgeNWv`~VJ zHu5?>DQV=xc;YRnLnBq80x}rE<$u~cItILk`~hL!lF8;pjwXoBFz;^WCRuExN03Y- zJF*bGfItrCOLQS6HXI;mCyOJGRK-Ys&wq79g#%WGh1n}LE|MN=c}e1Na&|YsgYVwy z6h7wZZ2)wD%7n~En^%@+W=;Z9;3`+gmgs%RE9)=@0NwRio5TZWt##6EsD`PPm77s| zYKpEuYKtiwbP}62P6}9~!Tw$EOpw140H^wzs|#U_Oj~`eKw3b_6-%bP>g**PTYnx7 z`}d6~-=X!y&Ui_r6rai|Nus`hfA1|#_rzwVJBaBfKoHdvy#|r%h=)j*P3L|4_AM?h zPET$1#AGwiLc4(;QNd;8Yw$ijRc1SqI(knZ)dKg}Tuev%PiCZ#1kYA1V^mzzyQ=pY zkp1Bc#4~$}vN;tjmiUax!J&HbV}DyX1f-FnVSCkXA-$bS>42k%53ClUz|v--RP zkTq8#4u_$aG5$;_XvQ~hmbi|7`~3McM7D;0dvo)%n3!VNf-D|DQmY8veCS=Yaw6{6 zq&YG7`9cOdQggSEk{mYo!}6f-GFYj(`MR+&%Ch!ZNhVm=%cZChqI7??uMdn_hF!Pv3^vr&)gelCPV8kUUJ-hf zA8vpJB-BmkB1Rw&wM0CW>PG9tNZ*^9n(FFKN=TG0G%8coG8PnhM8hv8iGOQq5=LQf z*QEFM^%Xnz?8TKrk?P5}sekd;Yatbl|NX~4Y{~HBaG9$RpoWIvv7>5R=y47?iqxd3 zsVQVpKN+*q0;rxf;X=sT3(B637|dY*2RG`SiN!SoYI+*-Vl&iI7ybjJFpnaCyu8i2 zl+c-bUn`ae52?36v;bW6Ha9!3<&7vBobq;Ae-SEg+x!}dRR&foHVi2hr`*~txhCIr0Eh%J?XvPU0!R(kopzB7QK^4q1ck=~nB=~xIzd+~W^?z}Uz zv$JQuvvdB~KX%WV{k{Zw`*`+vvHomS;5ny=dG(=KiV+Q3={Vzs{bIAo{;V6NmUk5E zxcD;2?xO8U;o^@n@6JFi{BpoIw=u4rA66w6P1nP{Hs)Um%6;~1?;HJ8ft_Mj z-*QGS9g*X?$r&ACE+>mDf}eWS>QeveF;&dD@$JRomFhCSXWCOybG;QjJpjqS(}BZc zaIew7tYS*#hiR3brNP$FXXXl$zRm;IbdacuUp+;{i`V-6;eP+~r3k^zz!|An5il?3 zT}!6vqN2m5Gtlm+<=VST82M3_6LhBmX`oq}zpUn{@3FOP@0P`X(l^tSHbVkLhc{Py z>*f^Ql1i#NJ1KT07FNcJ-9X&v;MyBoF8A!g48@Y|b@4O?qXWztPSO&T?Xo)Jt2~%( zeR|C}!Fk0qZqw}0YK#&}T3jsL=yE~<$1&gC9r>Hww&ccmQhAh% zdS-vnYnx~PQ-dEcQ%qXf2ueuwvA=ofS0Nig*Jl-J`O=#{>RS>Ss8BOeleYy7BJGiuc zU{*CN@;vRL%+aL1aL}u0gXTv|P$ITHP)*1Mpm&fA1qKE<=e;-D%2$|OW%9OymURzWi2M4pc+kF zs8lmeK9tN$2qj~Wv*lx4op=6!YYXk)*|@Uh0UYb@nUhjVzkH%lbE9>3ckNIB^GGO&D( zLHDA~waRpk^&zKY$A{g%GkJ*`0j>~&_;H*}+b97!QQiId%jjB}((cb6lF377wX_R0 z%87@!y_9#bl%IOd2k@=DoN~ZOY?p3+yhgDb5nfYGxCAB}#9$?rX=gleb;Ydl#0xP- zz{WV9t<-F@o5fY!&=BXb=v3ZxqEwg(G878j!xV^vh*?k3iFj>|y(yUB!P!ChjA>WyI)@2#|uHEAY7XKh6;kk#0@GJC0ZKXrPc z_o`t%&&Q4qS01jD`^#ZA5phA3AWE#k)_b|B?+STVIF++7Ma4hFEMw}=1fNoIHQ9Wf zR0Ucue^w*X4Vkp+=VagZh;>PN7av{RV0tkyxu@<$>16Hr#wihnay-uaK{GN`$HDhW z@`W77SCwHz)xxE}RDcyVi+xi7VX?$x`_j}yOydow1f@uU{CLoI;t`YYcX)F&NA8K4 z&(U$s&VLw-K1+lITw0ZqTwv>lGW+XpW#H|wuEQ+vo}M0Kq{CbC3nuT=rE?+(4+9&% zp;w8iR-cO+$A*UVbT4d` zI!LN&-vn0YKrcLYP$?zy?LziSq@uxz8Th7NrSp_TI6EuN+rtC=$=bjh&h7{B`?*? zmWNc$>6^3;lw&cKN*#U?|L8r=(bs=#;qsv#0mxB_&5%d4rwaVsWBYdNe1Tn$h)Y!af3-eK-{16nXLI58usk z5GGw*>1=?mlH=lTRX*Mye~-b`GKIc5q5Y)!W9yDJ9NywnntG?O(F&tsEvF+dpVlD& z5QD>|Nzb-_5*Oypy~*2SlGpZv-Iz1ayZZ!1`1sI*+Z@85q$~GxG0RT$%;2Y;+4ng6 zd*!|P8`5znFt8?761sqPdRq){6Zb)Wg54-CDpFNd)wzJ0G5=HGwxXh2>mSr;!!qX5 zRomhV@tzCs%g{KbESTcYlwc1r;l3sy3jIbRzPg|}TcIwL`#WX6s_fqOQsfn6qnfsXi62kW$lgr&c zQnpILhC-*2djJT_F91&Abb&z}T~a`xi4-hR0Bi)I(t_8c1VJD%4-iNI^xGIxSqyuZ zhT+W+QnR%JGi0+E2oO7C$bkM|OzRgA1o|BiRPak$OBM1zu_uO$z=#w74fqFfx(H)X zEP>q+1oL8QiM(9@#ugT1dl*$@a4EK15DdZU2!ZvPa!9{?;@Bi1uoUE9bt6Wu|7`ku VBD};jmq4&HLSSAtG2UOP{1^Vo<-7m@ delta 69403 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

DC+Vf1 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 N5&_Pmod_dBm_P_PD_dBm_GBd_PAM.mat + newTRname = sprintf('TR_SILAS_%s_Pmod_%.3fdBm_P_PD_%.3fdBm_%dGBd_%dPAM', ... + filecode{1}, config.P_laser, config.P_pd, config.fsym/1e9, config.M); + + newTRname = strrep(newTRname,'.','p'); + newTRname = [newTRname, '.mat']; + movefile(fullfile(pathToTimingRecov, trFileName), fullfile(pathToTimingRecov, newTRname)); + + % Append the file code to the corresponding PAM list based on config.M + switch config.M + case 2 + listPAM2{end+1} = filecode{1}; + case 4 + listPAM4{end+1} = filecode{1}; + case 6 + listPAM6{end+1} = filecode{1}; + case 8 + listPAM8{end+1} = filecode{1}; + otherwise + warning('Unexpected PAM value %d in file %s', config.M, measurementFilename); + end +end + +%% Display the lists of file codes for each PAM type +disp('List of file codes for PAM2:'); +disp(listPAM2); + +disp('List of file codes for PAM4:'); +disp(listPAM4); + +disp('List of file codes for PAM6:'); +disp(listPAM6); + +disp('List of file codes for PAM8:'); +disp(listPAM8); diff --git a/projects/Messung_Zürich/dsp_all_files.m b/projects/Messung_Zürich/dsp_all_files.m index cde817c..9ec98ab 100644 --- a/projects/Messung_Zürich/dsp_all_files.m +++ b/projects/Messung_Zürich/dsp_all_files.m @@ -1,113 +1,366 @@ -% 202502211_km_fiber_Pmod_15dBm_P_PD_9p2494dBm_W03C34-0603E03_160GBd_8PAM__20250221T024256.h5 - -M = 8; -if M == 4 - file_codes = {"20250221T032043",... - "20250221T032312",... - "20250221T032424",... - "20250221T032529",... - "20250221T032632",... - "20250221T032800",... - "20250221T032936",... - "20250221T033035",... - "20250221T033138",... - "20250221T033138",... - "20250221T033425",... - "20250221T033527",... - "20250221T033642",... - "20250221T033743",... - "20250221T033851",... - "20250221T034314",... - "20250221T034529",... - "20250221T034647",... - "20250221T034756",... - "20250221T034915",... - }; - baudrate = [96,120,128,136,144,152,160,168,172,176,180,184,188,192,196,200,204,212,220,224]; - pam4.result_vnle = result_vnle; - pam4.result_db = result_db; +M = 6; +if M == 2 + file_codes = {'20250221T035221' '20250221T035354' '20250221T035824' '20250221T035931' '20250221T040035' '20250221T040132' '20250221T040226' '20250221T040523' '20250221T040646' '20250221T040723' '20250221T040843' '20250221T041011' '20250221T041101' '20250221T041244'}; +elseif M == 4 + file_codes = {'20250221T030844' '20250221T032043' '20250221T032312' '20250221T032424' '20250221T032529' '20250221T032632' '20250221T032800' '20250221T033035' '20250221T033138' '20250221T033246' '20250221T033425' '20250221T033527' '20250221T033642' '20250221T033743' '20250221T033851' '20250221T034314' '20250221T034529' '20250221T034647' '20250221T034756' '20250221T034915'}; +elseif M == 6 + file_codes = {'20250221T041445' '20250221T041512' '20250221T041539' '20250221T041607' '20250221T041633' '20250221T041702' '20250221T041729' '20250221T041758' '20250221T041825' '20250221T041854' '20250221T041922' '20250221T041951' '20250221T042019' '20250221T042048' '20250221T042117' '20250221T042147' '20250221T042215'}; elseif M ==8 - file_codes = {"20250221T004651",... %96 - "20250221T004926",... - "20250221T005844",... - "20250221T010032",... - "20250221T010150",... - "20250221T025505",...%144 ohne balun - "20250221T024629",...%152 ohne balun - "20250221T024315",...%160 ohne balun - "20250221T023534",...%168 ohne balun - "20250221T024929",...%172 ohne balun - "20250221T025305",...%176 ohne balun - }; - file_codes = {"20250221T024256"}; - baudrate = [96,112,120,128,136,144,152,160,168,172,176]; + file_codes = {'20250221T004926' '20250221T023534' '20250221T024256' '20250221T024629' '20250221T024929' '20250221T025305' '20250221T025505' '20250221T025856' '20250221T030122' '20250221T030311' '20250221T030513'}; end -if 1 - +if 0 uloops = struct; uloops.filecode = file_codes; - uloops.mu_dc = [0]; + uloops.vnle_order1 = [50]; + uloops.vnle_order2 = [5]; + uloops.vnle_order3 = [5]; wh = DataStorage(uloops); wh.addStorage("ber"); - - wh = submit_handle(@dsp_ief_file,wh,"parallel",0); + + wh = submit_handle(@dsp_ief_file,wh,"parallel",1); +end + +if 0 + % Bring figure(5) to focus + fig = figure(2); + + % Get handles to all line objects in the figure + lines = findobj(fig, 'Type', 'line'); + + % Preallocate cell arrays to store the data for each line + xData = cell(numel(lines),1); + yData = cell(numel(lines),1); + + % Loop through each line and extract its data + + for k = 1:numel(lines) + xData{k} = get(lines(k), 'XData'); + yData{k} = get(lines(k), 'YData'); + end + + ief.M2.baudr_new = xData{4}; + ief.M2.ngmi_new = yData{4}; + + ief.M4.baudr_new = xData{3}; + ief.M4.ngmi_new = yData{3}; + + ief.M6.baudr_new = xData{2}; + ief.M6.ngmi_new = yData{2}; + + ief.M8.baudr_new = xData{1}; + ief.M8.ngmi_new = yData{1}; + + ief.M2.ber_new = yData{4}; + ief.M4.ber_new = yData{3}; + ief.M6.ber_new = yData{2}; + ief.M8.ber_new = yData{1}; + + pam2_baudr = xData{4}; + pam2_ber = yData{4}; + pam2_ngmi = yData{4}; + + pam4_baudr = xData{3}; + pam4_ber = yData{3}; + pam4_ngmi = yData{3}; + + pam6_baudr = xData{2}; + pam6_ber = yData{2}; + pam6_ngmi = yData{2}; + + pam8_baudr = xData{1}; + pam8_ber = yData{1}; + pam8_ngmi = yData{1}; + + pam2_baudr_lowdsp = xData{4}; + pam2_ber_lowdsp = yData{4}; + pam2_ngmi_lowdsp = yData{4}; + + pam4_baudr_lowdsp = xData{3}; + pam4_ber_lowdsp = yData{3}; + pam4_ngmi_lowdsp = yData{3}; + + pam6_baudr_lowdsp = xData{2}; + pam6_ber_lowdsp = yData{2}; + pam6_ngmi_lowdsp = yData{2}; + + pam8_baudr_lowdsp = xData{1}; + pam8_ber_lowdsp = yData{1}; + pam8_ngmi_lowdsp = yData{1}; + + + end -a = wh.getStoValue('ber',uloops.filecode, uloops.mu_ffe1); +close all +figure(1) +cnt = 1; +for M = [2,4,6,8] + % for i = 1:numel(uloops.vnle_order2) + % for j = 1:numel(uloops.vnle_order3) + wh = eval(sprintf('wh_pam%d',M)); + a =wh.getStoValue('ber',wh.parameter.filecode.values, wh.parameter.vnle_order1.values, wh.parameter.vnle_order2.values,wh.parameter.vnle_order3.values); + % a =wh_mit_1001.getStoValue('ber',uloops.filecode, wh_mit_1001.parameter.vnle_order1.values, wh_mit_1001.parameter.vnle_order2.values,wh_mit_1001.parameter.vnle_order3.values); + baudrate = cell2mat(cellfun(@(a) a.config.fsym, a, 'UniformOutput', false)); + [baudrate, idx] = sort(baudrate); -% get best results per baudrate -ber_vnle_values = cellfun(@(a) cellfun(@(y) y.ber_vnle, a.vnle_package, 'UniformOutput', false), a, 'UniformOutput', false); -best_vnle = cellfun(@(x) min(cell2mat(x)), ber_vnle_values); + % get best results per baudrate + vnle = 0; + try + ber_vnle_values = cellfun(@(a) cellfun(@(y) y.ber_vnle, a.vnle_package, 'UniformOutput', false), a, 'UniformOutput', false); + ber_vnle_infrate = cellfun(@(a) cellfun(@(y) y.inf_rate_vnle, a.vnle_package, 'UniformOutput', false), a, 'UniformOutput', false); -% ber_vnle_values = cellfun(@(a) cellfun(@(y) y.ber_vnle, a.vnle_pf_package, 'UniformOutput', false), a, 'UniformOutput', false); -% best_vnle = cellfun(@(x) min(cell2mat(x)), ber_vnle_values); -% -% ber_mlse_values = cellfun(@(a) cellfun(@(y) y.ber_mlse, a.vnle_pf_package, 'UniformOutput', false), a, 'UniformOutput', false); -% best_mlse = cellfun(@(x) min(cell2mat(x)), ber_mlse_values); + best_vnle = cellfun(@(x) min(cell2mat(x)), ber_vnle_values); + best_vnle = best_vnle(idx); + best_gmi_vnle = cellfun(@(x) min(cell2mat(x)), ber_vnle_infrate); + best_gmi_vnle = best_gmi_vnle(idx); -%%% PLOT PARAMETER OPTIMIZATION -figure(2) -cols = cbrewer2('Set1',6); -hold on -title(sprintf('%d km | %d nm | PAM %d',1,1313.5,M)); -xax = uloops.mu_ffe1; -plot(xax,best_vnle,'DisplayName',sprintf('VNLE 50,3,3'),'Color',cols(o,:),'LineStyle','-','HandleVisibility','on'); + % netRateVNLE = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'NGMI',best_gmi_vnle./log2(M), 'BER',best_vnle); -% plot(xax,best_mlse,'DisplayName',sprintf('VNLE + 1 tap post-filter + MLSE'),'Color',cols(2,:),'LineStyle','-','HandleVisibility','on'); + vnle = 1; + end -% plot(xax,best_db,'DisplayName',sprintf('DB tgt. + MLSE'),'Color',cols(3,:),'LineStyle','-','HandleVisibility','on'); + mlse = 0; + try + ber_vnle_values = cellfun(@(a) cellfun(@(y) y.ber_vnle, a.vnle_pf_package, 'UniformOutput', false), a, 'UniformOutput', false); + best_vnle_2 = cellfun(@(x) min(cell2mat(x)), ber_vnle_values); + best_vnle_2 = best_vnle_2(idx); -set(gca, 'YScale', 'log'); -ylim([5e-5 0.3]); -xlim([min(xax), max(xax) ]); -yline([3.8e-3, 2e-2],'HandleVisibility','off'); -legend -beautifyBERplot() -xlabel('Adaption speed'); -ylabel('BER'); + gmi_vnle_values = cellfun(@(a) cellfun(@(y) y.gmi, a.vnle_pf_package, 'UniformOutput', false), a, 'UniformOutput', false); + best_gmi_vnle = cellfun(@(x) min(cell2mat(x)), gmi_vnle_values); + best_gmi_vnle = best_gmi_vnle(idx); + + ber_mlse_values = cellfun(@(a) cellfun(@(y) y.ber_mlse, a.vnle_pf_package, 'UniformOutput', false), a, 'UniformOutput', false); + best_mlse = cellfun(@(x) min(cell2mat(x)), ber_mlse_values); + best_mlse = best_mlse(idx); + + if M == 8 + best_vnle_2(2) = []; + best_mlse(2) = []; + best_gmi_vnle(2) = []; + best_db(2) = []; + + baudrate(2) = []; + elseif M == 4 + + end + + mlse = 1; + end + + db = 0; + try + ber_db_values = cellfun(@(a) cellfun(@(y) y.ber, a.dbtgt_package, 'UniformOutput', false), a, 'UniformOutput', false); + best_db = cellfun(@(x) min(cell2mat(x)), ber_db_values); + best_db = best_db(idx); + if M == 4 + best_db(end-1:end) = []; + baudrate(end-1:end) = []; + end + + db = 1; + end + + %% BER PLOT + + % figure(1) + subplot(1,2,1) + cols = cbrewer2('Set1',6); + hold on + title(sprintf('BER')); + % title(sprintf('%d 1st order',uloops.vnle_order1(i))); + xax = baudrate.*1e-9; + + if M == 4 || M == 2 + plot(xax,best_db,'DisplayName',sprintf('PAM %d | DB+MLSE',M),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on','Marker','o','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + plot(ief.(sprintf('M%d', M)).baudr_new,ief.(sprintf('M%d', M)).ber_new,'DisplayName',sprintf('PAM %d |VNLE',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + elseif M == 6 + plot(xax,best_vnle_2,'DisplayName',sprintf('PAM %d |VNLE',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + % plot(ief.(sprintf('M%d', M)).baudr_new,ief.(sprintf('M%d', M)).ber_new,'DisplayName',sprintf('PAM %d |VNLE',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + else + % plot(xax,best_vnle_2,'DisplayName',sprintf('PAM %d |VNLE',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + plot(ief.(sprintf('M%d', M)).baudr_new,ief.(sprintf('M%d', M)).ber_new,'DisplayName',sprintf('PAM %d |VNLE',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + end + + if 0 + if vnle + plot(xax,best_vnle,'DisplayName',sprintf('VNLE'),'Color',cols(2,:),'LineStyle','-','HandleVisibility','on'); + end + if mlse + plot(xax,best_vnle_2,'DisplayName',sprintf('VNLE'),'Color',cols(2,:),'LineStyle','-','HandleVisibility','on'); + plot(xax,best_mlse,'DisplayName',sprintf('VNLE + 1 tap post-filter + MLSE'),'Color',cols(3,:),'LineStyle','-','HandleVisibility','on'); + end + if db + plot(xax,best_db,'DisplayName',sprintf('DB tgt. + MLSE'),'Color',cols(1,:),'LineStyle','-','HandleVisibility','on'); + end + + plot(ief.(sprintf('M%d', M)).baudr,ief.(sprintf('M%d', M)).ber,'DisplayName',sprintf('VNLE [100,15,15] (ETH)'),'Color',cols(4,:),'LineStyle','-','HandleVisibility','on'); + plot(ief.(sprintf('M%d', M)).baudr_lowdsp,ief.(sprintf('M%d', M)).ber_lowdsp,'DisplayName',sprintf('VNLE [100,15,15] (ETH)'),'Color',cols(4,:),'LineStyle','-','HandleVisibility','on'); + end + + set(gca, 'YScale', 'log'); + ylim([1e-6 0.3]); + xlim([92, 260 ]); + xticks([0:16:280]); + yline([4.85e-3, 2e-2],'HandleVisibility','off'); + % legend + % beautifyBERplot() + xlabel('baudrate in GBd'); + ylabel('BER'); + set(findall(gca, '-property', 'Interpreter'), 'Interpreter', 'latex'); + + set(gcf, 'Color', 'w'); + set(gca, 'Box', 'on', 'LineWidth', 0.8); % Thicker border + grid on; + set(gca, 'FontSize', 10, 'FontName', 'Times New Roman'); + % + + %% NGMI PLOT + + % figure(2) + % cols = cbrewer2('Set1',6); + % hold on + % title(sprintf('%d km ; %.1f nm ; PAM %d',1,1313,M)); + % xax = baudrate.*1e-9; + % + % plot(xax,best_gmi_vnle./log2(M),'DisplayName',sprintf('NGMI VNLE'),'Color',cols(2,:),'LineStyle','-','HandleVisibility','on'); + % + % plot(ief.(sprintf('M%d', M)).baudr,ief.(sprintf('M%d', M)).ngmi,'DisplayName',sprintf('VNLE [100,15,15] (ETH)'),'Color',cols(4,:),'LineStyle','-','HandleVisibility','on'); + % plot(ief.(sprintf('M%d', M)).baudr_lowdsp,ief.(sprintf('M%d', M)).ngmi_lowdsp,'DisplayName',sprintf('VNLE [100,15,15] (ETH)'),'Color',cols(4,:),'LineStyle','-','HandleVisibility','on'); + % + % set(gca, 'YScale', 'log'); + % ylim([0.7 1]); + % xlim([min(xax), max(xax) ]); + % xticks(xax); + % yline([0.8],'HandleVisibility','off'); + % legend + % beautifyBERplot() + % xlabel('Baudrate in GBd'); + % ylabel('NGMI'); + + %% AIR Plot + % subplot(1,3,2) + % + % netRateMLSE = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'BER',best_mlse); + % netRateVNLE = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'NGMI',best_gmi_vnle./log2(M), 'BER',best_vnle_2); + % netRateDB = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'BER',best_db); + % + % netRateIEF = TransmissionPerformance().calculateNetRate(log2(M)*ief.(sprintf('M%d', M)).baudr,'BER',ief.(sprintf('M%d', M)).ber,'NGMI',ief.(sprintf('M%d', M)).ngmi); + % netRateIEF_lowdsp = TransmissionPerformance().calculateNetRate(log2(M)*ief.(sprintf('M%d', M)).baudr_lowdsp,'BER',ief.(sprintf('M%d', M)).ber_lowdsp,'NGMI',ief.(sprintf('M%d', M)).ngmi_lowdsp); + % + % cols = cbrewer2('Set1',8); + % hold on + % title(sprintf('AIR')); + % xax = baudrate.*1e-9; + % + % if M == 2 || M == 4 + % + % % plot(xax,netRateMLSE.HD.NetRate.*1e-9,'DisplayName',sprintf('MLSE - HD'),'Color',cols(1,:),'LineStyle',':','HandleVisibility','on'); + % % plot(xax,netRateVNLE.SDHD.NetRate.*1e-9,'DisplayName',sprintf('VNLE SD+HD'),'Color',cols(4,:),'LineStyle','-','HandleVisibility','on'); + % plot(xax,netRateDB.HD.NetRate.*1e-9,'DisplayName',sprintf('PAM %d | DB+MLSE HD',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on'); + % plot(xax,netRateDB.O_FEC.NetRate.*1e-9,'DisplayName',sprintf('PAM %d | DB+MLSE O-FEC',M),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on'); + % % plot(xax,netRateDB.KP4_hamming.NetRate.*1e-9,'DisplayName',sprintf('PAM %d | DB+MLSE KP4-FEC',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on'); + % + % else + % + % % plot(xax,netRateMLSE.HD.NetRate.*1e-9,'DisplayName',sprintf('MLSE - HD'),'Color',cols(1,:),'LineStyle',':','HandleVisibility','on'); + % % plot(xax,netRateVNLE.SDHD.NetRate.*1e-9,'DisplayName',sprintf('VNLE SD+HD'),'Color',cols(4,:),'LineStyle','-','HandleVisibility','on'); + % % plot(ief.(sprintf('M%d', M)).baudr,netRateIEF.HD.NetRate,'DisplayName',sprintf('PAM %d | HD IEF',M),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on'); + % plot(ief.(sprintf('M%d', M)).baudr,netRateIEF.SDHD.NetRate,'DisplayName',sprintf('PAM %d | SD+HD IEF',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on'); + % + % end + % set(gca, 'YScale', 'log'); + % ylim([92 450]); + % xlim([min(xax), max(xax) ]); + % xlim([92, 260 ]); + % xticks([0:16:280]); + % yline([0.8],'HandleVisibility','off'); + % legend + % beautifyBERplot() + % xlabel('Baudrate in GBd'); + % ylabel('Net Rate in Gbps'); + % + % + %% NDR PLOT -%%% PLOT COMPLETE BAUDRATE -figure(20) -cols = cbrewer2('Set1',6); -hold on -title(sprintf('%d km | %d nm | PAM %d',1,1313.5,M)); + subplot(1,2,2) + netRateIEF = TransmissionPerformance().calculateNetRate(log2(M)*ief.(sprintf('M%d', M)).baudr,'BER',ief.(sprintf('M%d', M)).ber,'NGMI',ief.(sprintf('M%d', M)).ngmi); + netRateIEF_lowdsp = TransmissionPerformance().calculateNetRate(log2(M)*ief.(sprintf('M%d', M)).baudr_lowdsp,'BER',ief.(sprintf('M%d', M)).ber_lowdsp,'NGMI',ief.(sprintf('M%d', M)).ngmi_lowdsp); + netRateIEF_new = TransmissionPerformance().calculateNetRate(log2(M)*ief.(sprintf('M%d', M)).baudr_new,'BER',ief.(sprintf('M%d', M)).ber_new,'NGMI',ief.(sprintf('M%d', M)).ngmi_new); -plot(baudrate,best_vnle,'DisplayName',sprintf('VNLE'),'Color',cols(1,:),'LineStyle','-','HandleVisibility','on'); -plot(baudrate,best_mlse,'DisplayName',sprintf('VNLE + 1 tap post-filter + MLSE'),'Color',cols(2,:),'LineStyle','-','HandleVisibility','on'); + cols = cbrewer2('Set1',8); + hold on + title(sprintf('Net Bitrate')); + xax = baudrate.*1e-9; -plot(baudrate,best_db,'DisplayName',sprintf('DB tgt. + MLSE'),'Color',cols(3,:),'LineStyle','-','HandleVisibility','on'); + if M == 2 || M == 4 -set(gca, 'YScale', 'log'); -ylim([5e-5 0.3]); -xlim([min(baudrate), max(baudrate) ]); -yline([3.8e-3, 2e-2],'HandleVisibility','off'); -legend -beautifyBERplot() -xlabel('Bit Rate in Gbps'); -ylabel('BER'); + log2M = log2(M); + + netRateDB = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'BER',best_db); + + % plot(xax,netRateMLSE.HD.NetRate.*1e-9,'DisplayName',sprintf('MLSE - HD'),'Color',cols(1,:),'LineStyle',':','HandleVisibility','on'); + % plot(xax,netRateVNLE.SDHD.NetRate.*1e-9,'DisplayName',sprintf('VNLE SD+HD'),'Color',cols(4,:),'LineStyle','-','HandleVisibility','on'); + plot(xax,netRateDB.HD.NetRate.*1e-9,'DisplayName',sprintf('PAM %d | DB+MLSE HD',M),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + plot(xax,netRateDB.O_FEC.NetRate.*1e-9,'DisplayName',sprintf('PAM %d | DB+MLSE O-FEC',M),'Color',cols(M/2,:),'LineStyle','-.','HandleVisibility','on','Marker','x','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + % plot(xax,netRateDB.KP4_hamming.NetRate.*1e-9,'DisplayName',sprintf('PAM %d | DB+MLSE KP4-FEC',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','*','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + plot(ief.(sprintf('M%d', M)).baudr_new,netRateIEF_new.SDHD.NetRate,'DisplayName',sprintf('PAM %d | SD+HD',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','o','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + plot(ief.(sprintf('M%d', M)).baudr_new,ief.(sprintf('M%d', M)).ngmi_new.*ief.(sprintf('M%d', M)).baudr_new.*log2M ,'DisplayName',sprintf('PAM %d | AIR',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','o','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + + elseif M == 6 + + log2M = 2.5; + netRateMLSE = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'BER',best_mlse); + netRateVNLE = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'NGMI',best_gmi_vnle./log2(M), 'BER',best_vnle_2); + + plot(xax,netRateVNLE.HD.NetRate.*1e-9,'DisplayName',sprintf('MLSE - HD'),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + plot(xax,netRateVNLE.SDHD.NetRate.*1e-9,'DisplayName',sprintf('VNLE SD+HD'),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','o','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + % plot(ief.(sprintf('M%d', M)).baudr_new,netRateIEF_new.HD.NetRate,'DisplayName',sprintf('PAM %d | HD IEF',M),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + % plot(ief.(sprintf('M%d', M)).baudr_new,netRateIEF_new.SDHD.NetRate,'DisplayName',sprintf('PAM %d | SD+HD IEF',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','o','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + + else + + log2M = log2(M); + + netRateMLSE = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'BER',best_mlse); + netRateVNLE = TransmissionPerformance().calculateNetRate(log2(M)*baudrate,'NGMI',best_gmi_vnle./log2(M), 'BER',best_vnle_2); + + % plot(xax,netRateVNLE.HD.NetRate.*1e-9,'DisplayName',sprintf('MLSE - HD'),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + % plot(xax,netRateVNLE.SDHD.NetRate.*1e-9,'DisplayName',sprintf('VNLE SD+HD'),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','o','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + plot(ief.(sprintf('M%d', M)).baudr_new,netRateIEF_new.HD.NetRate,'DisplayName',sprintf('PAM %d | HD IEF',M),'Color',cols(M/2,:),'LineStyle',':','HandleVisibility','on','Marker','square','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + plot(ief.(sprintf('M%d', M)).baudr_new,netRateIEF_new.SDHD.NetRate,'DisplayName',sprintf('PAM %d | SD+HD IEF',M),'Color',cols(M/2,:),'LineStyle','-','HandleVisibility','on','Marker','o','MarkerFaceColor',[1,1,1],'MarkerEdgeColor',cols(M/2,:),'MarkerSize',4,'LineWidth',1.4); + + end + set(gca, 'YScale', 'log'); + ylim([150 450]); + xlim([min(xax), max(xax) ]); + xlim([92, 270 ]); + xticks([0:16:280]); + yline([0.8],'HandleVisibility','off'); + % legend + + xlabel('Baudrate in GBd'); + ylabel('Net Rate in Gbps'); + set(findall(gca, '-property', 'Interpreter'), 'Interpreter', 'latex'); + + set(gcf, 'Color', 'w'); + set(gca, 'Box', 'on', 'LineWidth', 0.8); % Thicker border + grid on; + set(gca, 'FontSize', 10, 'FontName', 'Times New Roman'); + +end \ No newline at end of file diff --git a/projects/Messung_Zürich/dsp_ief_file.m b/projects/Messung_Zürich/dsp_ief_file.m index e39dad4..3fd0754 100644 --- a/projects/Messung_Zürich/dsp_ief_file.m +++ b/projects/Messung_Zürich/dsp_ief_file.m @@ -4,12 +4,14 @@ function [results] = dsp_ief_file(varargin) mu_ffe1 = 0.0001; mu_ffe2 = 0.0008; mu_ffe3 = 0.001; - mu_dc = 0.00; + mu_dc = 0.0003; %0.0003; - mu_dfe = 0.0004; - vnle_order1 = 100; + mu_dfe_training = 0.0004; + vnle_order1 = 50; vnle_order2 = 3; vnle_order3 = 3; + + dfe_mu = 0.0005; tcorrect = 0; @@ -33,10 +35,11 @@ function [results] = dsp_ief_file(varargin) end end - ssdPath = "D:\36_IMDD_Kiel\Data"; - % code = "20250221T004802"; - % code = "20250221T032043"; - filename = findFileByCode(ssdPath, filecode{1}); + pathToMeasurement = "C:\Users\Silas\Documents\MATLAB\Datensätze\IEF_Polariton_2025\36_IMDD_Kiel\Data\20250221"; + filename = findFileByCode(pathToMeasurement, filecode{1}); + + pathToTimingRecov = "C:\Users\Silas\Documents\MATLAB\Datensätze\IEF_Polariton_2025\36_IMDD_Kiel\Data\TR_ZIP"; + filename_2 = findFileByCode(pathToTimingRecov, filecode{1}); % Extract parameters from the filename using an updated regex tokens = regexp(filename, 'Pmod_([-0-9p]+)dBm_P_PD_([-0-9p]+)dBm_.*?_(\d+)GBd_(\d+)PAM__\d+T\d+', 'tokens'); @@ -75,20 +78,45 @@ function [results] = dsp_ief_file(varargin) dataRx = double(h5read(filepath, '/Waveforms/Channel 2/Channel 2Data')); % rohdaten des CH4 bitsTx = h5read(filepath, '/Settings/dataTx'); %Binär - bitsTx = reshape(bitsTx,log2(config.M),[])'; - + + if config.M ~= 6 + bitsTx = reshape(bitsTx,log2(config.M),[])'; + else + %bitsTx = reshape(bitsTx,5,[])'; + end + %%% Build Tx Signal (Bits, Pam Map, Symbols) Tx_bits = Informationsignal(bitsTx); Tx_symbols = PAMmapper(config.M,0,"eth_style",1).map(Tx_bits); Tx_symbols.fs = config.fsym; - + + if 0 + Rx_bits = PAMmapper(config.M,0,"eth_style",1).demap(Tx_symbols); + [~,~,ber_bw,~] = calc_ber(Rx_bits.signal,Tx_bits.signal(1:length(Rx_bits.signal)),"skip_front",100,"skip_end",150,"returnErrorLocation",1); + end + %%% Build Rx Signal (Rx, normalize,remove mean) - loadAfterTR = 0; + loadAfterTR = 1; if loadAfterTR - - rx_sig = load("testSilas.mat","signal_TR3"); - rx_sig=rx_sig.signal_TR3; - Rx_Sig_resamp = Informationsignal(rx_sig,"fs",config.fsym*2); + if 1 + rx_sig = load(filename_2); + rx_sig=rx_sig.signal_TR; + if config.M == 6 + rx_sig_pam6 = zeros(length(rx_sig)*2,1); + rx_sig_pam6(1:2:end) = real(rx_sig); + rx_sig_pam6(2:2:end) = imag(rx_sig); + rx_sig = rx_sig_pam6; + Rx_Sig_resamp = Informationsignal(rx_sig,"fs",config.fsym); + % Tx_symbols.signal = Tx_symbols.signal(1:end/2); + else + Rx_Sig_resamp = Informationsignal(rx_sig,"fs",config.fsym*2); + end + + else + rx_sig = load(string(['testSilas_',char(filecode{1}),'.mat']),"signal_TR3"); + rx_sig=rx_sig.signal_TR3; + Rx_Sig_resamp = Informationsignal(rx_sig,"fs",config.fsym*2); + end else @@ -98,39 +126,17 @@ function [results] = dsp_ief_file(varargin) Rx_Sig.signal = Rx_Sig.signal - mean(Rx_Sig.signal); Rx_Sig = Rx_Sig.normalize("mode","rms"); - - Rx_Sig.spectrum("fignum",2,"displayname",'Rx Signal','normalizeTo0dB',1); - - Rx_Sig = Filter('filtdegree',4,"f_cutoff",Tx_symbols.fs.*0.9,"fs",Rx_Sig.fs,"filterType",filtertypes.gaussian,"active",true).process(Rx_Sig); - - Rx_Sig.spectrum("fignum",2,"displayname",'Rx Signal filt','normalizeTo0dB',1); - - mf = Pulseformer("alpha",config.ROF,"fsym",config.fsym,"fdac",Rx_Sig.fs,"matched",1,"pulse","rrc","pulselength",32); - Rx_matched = mf.process(Rx_Sig); - - Rx_matched.spectrum("fignum",2,"displayname",'Rx Signal m','normalizeTo0dB',1); %%%%%% Sample to 2x fsym %%%%%% - Rx_Sig_resamp = Rx_Sig.resample("fs_out",2*config.fsym); + Rx_Sig_resamp = Rx_Sig.resample("fs_out",config.fsym); end - + + %%%%%% Sync Rx signal with reference (S is a cell array with all occurences) %%%%%% - [Rx_Sig_sync,S,isFlipped] = Rx_Sig_resamp.tsynch("reference",Tx_symbols,"fs_ref",config.fsym,"debug_plots",1); - - % Rx_Sig_sync.eye(fsym,M,"fignum",4,"displayname",'eye diagram'); - % timing syncroization ?? + [~,S,isFlipped] = Rx_Sig_resamp.tsynch("reference",Tx_symbols,"fs_ref",config.fsym,"debug_plots",1); - % SYMSYNC = comm.SymbolSynchronizer("Modulation","PAM/PSK/QAM","SamplesPerSymbol",2); - % - % synd = SYMSYNC(Rx_Sig_sync.signal); - % Rx_Sig_sync.signal = synd; - % - % [Rx_Sig_sync,S,isFlipped] = Rx_Sig_sync.tsynch("reference",Tx_symbols,"fs_ref",config.fsym,"debug_plots",1); - % - - % filter Bw output = struct(); vnle_package = {}; vnle_pf_package = {}; @@ -139,78 +145,44 @@ function [results] = dsp_ief_file(varargin) for s = 1%:length(S) Rx_Sig_sync = S{s}; Rx_Sig_sync = Rx_Sig_sync.normalize("mode","rms"); - - if 1 - Tx_symbols.spectrum("fignum",3,"displayname",'Tx Symbols','normalizeTo0dB',1); - Rx_Sig_sync.spectrum("fignum",3,"displayname",'No Matched Filter','normalizeTo0dB',1); - - Nsym = 64; - sampsPerSym = 2; - rcrFilt = comm.RaisedCosineReceiveFilter(... - 'Shape', 'Square root', ... - 'RolloffFactor', config.ROF, ... - 'FilterSpanInSymbols', Nsym, ... - 'InputSamplesPerSymbol', sampsPerSym, ... - 'DecimationFactor', 1); - yr = rcrFilt([Rx_Sig_sync.signal;zeros(Nsym*sampsPerSym/2, 1)]); - fltDelay = Nsym / (2*config.fsym); - yr = yr(fltDelay*Rx_Sig_sync.fs+1:end); - Rx_matched = Rx_Sig_sync; - Rx_matched.signal = yr; - - % Rx_matched = Rx_matched.resample("fs_out",2*config.fsym); - % length(Rx_matched); - % symbolSync = comm.SymbolSynchronizer("Modulation","PAM/PSK/QAM","SamplesPerSymbol",2,"TimingErrorDetector","Mueller-Muller (decision-directed)"); - % Rx_syncd = Rx_matched; - % [Rx_syncd.signal,tError] = symbolSync(Rx_syncd.signal); - % Rx_syncd.fs = config.fsym; - % length(Rx_syncd); - - % figure();hold on; - % stem(Tx_symbols.normalize("mode","oneone").signal); - % stem(Rx_syncd.normalize("mode","oneone").signal); - % - % figure();hold on; - % plot(Rx_Sig_sync.normalize("mode","oneone").signal); - % plot(Rx_matched.normalize("mode","oneone").signal); - - - Rx_syncd = Rx_syncd.resample("fs_out",2*config.fsym); - - Rx_matched.spectrum("fignum",3,"displayname",'After Matched Filter','normalizeTo0dB',1); - - Rx_syncd.spectrum("fignum",3,"displayname",'Zero Crossing TR','normalizeTo0dB',1); - - end + Rx_Sig_sync = Rx_Sig_sync.resample("fs_out",2*config.fsym); mu_ffe = [mu_ffe1 mu_ffe3 mu_ffe3]; vnle_order=[vnle_order1,vnle_order2,vnle_order3]; - eq_ = EQ("Ne",vnle_order,"Nb",[0,0,0],"training_length",4096*2,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",1,"ideal_dfe",1); + eq_ = EQ("Ne",vnle_order,"Nb",[0,0,0],"training_length",4096*4,"training_loops",4,"dd_loops",3,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe_training],"DFEmu",dfe_mu,"FFEmu",0,"plotfinal",0,"ideal_dfe",0,"plottrain",0); %%%%% VNLE only (or DFE) %%%% - if 1 + if 0 - - [result] = vnle(eq_,config.M,Rx_syncd,Tx_symbols,Tx_bits,"precode_mode",db_mode.no_db,"showAnalysis",1,'eth_style',1); + eq_post = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",1001,"sps",1,"decide",0); + [result] = vnle(eq_,config.M,Rx_Sig_sync,Tx_symbols,Tx_bits,"precode_mode",db_mode.no_db,"showAnalysis",1,'eth_style',1,'postFFE',eq_post); + + netRate = TransmissionPerformance().calculateNetRate(log2(config.M)*config.fsym,'NGMI',result.inf_rate_vnle, 'BER',result.ber_vnle); vnle_package{s} = result; end %%%%% VNLE + PF + MLSE %%%% - if 0 + if 1 + eq_post = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",1001,"sps",1,"decide",0); pf_ = Postfilter("ncoeff",1,"useBurg",1); + mlse_ = MLSE("duobinary_output",0,'M',config.M,'trellis_states',PAMmapper(config.M,0).levels); mlse_ = MLSE_viterbi("duobinary_output",0,'M',config.M,'trellis_states',PAMmapper(config.M,0).levels); - doub_mode = db_mode.no_db; + doub_mode = db_mode.no_db; - [result] = vnle_postfilter_mlse(eq_,pf_,mlse_,config.M,Rx_Sig_sync,Tx_symbols,Tx_bits,"precode_mode",doub_mode,'showAnalysis',1,'eth_style_symbol_mapping',1); + [result] = vnle_postfilter_mlse(eq_,pf_,mlse_,config.M,Rx_Sig_sync,Tx_symbols,Tx_bits,"precode_mode",doub_mode,'showAnalysis',1,'eth_style_symbol_mapping',1,'postFFE',eq_post); + netRate = TransmissionPerformance().calculateNetRate(log2(config.M)*config.fsym,'NGMI',result.gmi./log2(config.M), 'BER',result.ber_mlse); + fprintf('VNLE SD: %.1f GBd \n',netRate.SDHD.NetRate.*1e-9); + fprintf('MLSE HD: %.1f GBd \n',netRate.HD.NetRate.*1e-9); + vnle_pf_package{s} = result; end %%%%% Duobinary Targeting %%%% - if 0 + if 1 mlse_db = MLSE_viterbi("DIR",[1,1],"duobinary_output",0,"M",config.M,"trellis_states",PAMmapper(config.M,0).levels); doub_mode = db_mode.db_emulate; @@ -225,6 +197,7 @@ function [results] = dsp_ief_file(varargin) results.vnle_package = vnle_package; results.vnle_pf_package = vnle_pf_package; results.dbtgt_package = dbtgt_package; + results.config = config; end diff --git a/projects/Messung_Zürich/minimal_pam4_mlse.m b/projects/Messung_Zürich/minimal_pam4_mlse.m new file mode 100644 index 0000000..1a2bb54 --- /dev/null +++ b/projects/Messung_Zürich/minimal_pam4_mlse.m @@ -0,0 +1,19 @@ + + +mlse_sig_sd=load("imdd_simulation\projects\Messung_Zürich\mlse_sig_sd.mat","mlse_sig_sd"); +mlse_sig_sd = mlse_sig_sd.mlse_sig_sd; +tx_symbols=load("imdd_simulation\projects\Messung_Zürich\tx_symbols.mat","tx_symbols"); +tx_symbols = tx_symbols.tx_symbols; + +mlse_ = MLSE_viterbi("duobinary_output",0,'M',4,'trellis_states',PAMmapper(4,0).levels); +mlse_.DIR = [1.0000 0.5452]; +mlse_sig_sd = mlse_.process(mlse_sig_sd); + + +% +% mlse_ = MLSE_viterbi("duobinary_output",0,'M',4,'trellis_states',PAMmapper(4,0).levels); +% mlse_.DIR = [1.0000 0.5452]; +% mlse_sig_sd = mlse_.process(mlse_sig_sd,tx_symbols); + +h = [1.0000 0.5452]; +chatgpt_answer(mlse_sig_sd.signal,tx_symbols.signal,h) diff --git a/test/matched_filter_minimal.m b/test/matched_filter_minimal.m index 9a5d413..b298fac 100644 --- a/test/matched_filter_minimal.m +++ b/test/matched_filter_minimal.m @@ -26,24 +26,34 @@ tx_bw_nyquist = 0.8; O = 18; %order of prbs N = 2^(O-1); %length of prbs -rcalpha = 1; -Pform = Pulseformer("fsym",fsym,"fdac",fdac,"pulse","rrc","pulselength",1024,"alpha",rcalpha); +%%%%% MOVE-IT PRMS %%%% +Mi_prms = Moveit_wrapper("prms"); +if M == 6 + Mi_prms.para.bl = 2^(O-2); + Mi_prms.para.dimension = 5; +else + Mi_prms.para.bl = 2^(O-1); + Mi_prms.para.dimension = log2(M); %2.5bits/sym -> 2 bit/sym +end +Mi_prms.para.rand = 0; +Mi_prms.para.order = floor(O / log2(M)); +Mi_prms.para.skip =0; +Mi_prms.para.bruijn = 0; +Mi_prms.para.reset_prms = 0; +Mi_prms.para.method = 1; +bitpattern = Mi_prms.process([]); +if M == 6 + bitpattern = reshape(bitpattern',[],1); + bitpattern = bitpattern(1:end-mod(length(bitpattern),5)); +end +bits = Informationsignal(bitpattern); +symbols = PAMmapper(M,0).map(bits); +symbols.fs = fsym; -[Digi_sig,Symbols,Tx_bits] = PAMsource(... - "fsym",fsym,"M",M,"order",18,"useprbs",0,... - "fs_out",fdac,... - "applyclipping",0,"clipfactor",1.5,... - "applypulseform",apply_pulsef,"pulseformer",Pform,... - "randkey",random_key,... - "db_precode",db_precode,"db_encode",db_encode,... - "mrds_code",0,"mrds_blocklength",512).process(); - -Digi_sig.spectrum("displayname",'Signal after shaping','fignum',1); -% Digi_sig.move_it_spectrum("displayname",'Signal after shaping','fignum',2); - -% Digi_sig = Digi_sig.resample("fs_out",fsym); +Pform = Pulseformer("fsym",fsym,"fs",fsym,"alpha",0.6,"pulse","rrc","pulselength",64,"matched_sps",4,"output_sps",2); +Pform.process(symbols); MF = Pulseformer("fsym",fsym,"fdac",fdac,"pulse","rrc","pulselength",1024,"alpha",rcalpha,"matched",0); Digi_sig = MF.process(Digi_sig); diff --git a/test/test_mapping_eth.m b/test/test_mapping_eth.m index c3e1cc9..592d9db 100644 --- a/test/test_mapping_eth.m +++ b/test/test_mapping_eth.m @@ -1,21 +1,32 @@ % Setup PRBS parameters -O = 12; -M = 2; +O = 6; +M = 6; N = 2^(O-1); % Length of PRBS randkey = 1; % Random key for random stream +use_eth_mapping =1; + +if M ~= 6 + dimension = log2(M); +else + dimension = 5; +end [~, seed] = prbs(O, 1); % Initialize first seed of PRBS bitpattern = []; s = RandStream('twister', 'Seed', randkey); -for i = 1:log2(M) +for i = 1:dimension bitpattern(:, i) = randi(s, [0 1], N, 1); end +if M == 6 + bitpattern = reshape(bitpattern',[],1); + bitpattern = bitpattern(1:end-mod(length(bitpattern),5)); +end Tx_bits = Informationsignal(bitpattern); -Digi_Mod = PAMmapper(M, 0,"eth_style",1); +Digi_Mod = PAMmapper(M, 0,"eth_style",use_eth_mapping); % Map bits to symbols Symbols = Digi_Mod.map(Tx_bits); @@ -23,7 +34,6 @@ Symbols = Digi_Mod.map(Tx_bits); % Demap symbols back to bits Rx_bits = Digi_Mod.demap(Symbols); -[~, error_num, ber, ~] = calc_ber(Tx_bits.signal, Rx_bits.signal, ... - "skip_front", 0, "skip_end", 0, "returnErrorLocation", 1); +[~, error_num, ber, ~] = calc_ber(Tx_bits.signal(1:length(Rx_bits.signal)), Rx_bits.signal,"skip_front", 0, "skip_end", 0, "returnErrorLocation", 1); -fprintf('BER VNLE: %.1E \n',ber); \ No newline at end of file +fprintf('BER: %.1E \n',ber); \ No newline at end of file