% esno_est.m
% David Rowe Mar 2017
%
% Functions for estimating Es/No from QPSK symbols, and supporting tests

1;

#{
  ----------------------------------------------------------------------------
  Estimate the energy and noise of received symbols.
  
  Signal power is distance from axis on complex
  plane. Noise power is the distance orthogonal to each symbol, to provide an
  estimate that is insensitive to fading that moves symbol towards he origin.
  
  For QAM we need to use pilots as they don't have modulation that affects 
  estimate, for QPSK Modes we can use all rx symbols.
  ----------------------------------------------------------------------------
#}

function EsNodB = esno_est_calc(rx_syms)
  sig_var = sum(abs(rx_syms) .^ 2)/length(rx_syms);
  sig_rms = sqrt(sig_var);
   
  sum_x = 0;
  sum_xx = 0;
  n = 0;
  for i=1:length(rx_syms)
    s = rx_syms(i);
    % only consider symbols a reasonable distance from the origin, as these are
    % more likely to be valid and not errors that will mess up the estimate
    if abs(s) > sig_rms
        % rough demodulation, determine if symbol is on real or imag axis
        if abs(real(s)) > abs(imag(s))
          % assume noise is orthogonal to real axis
          sum_x  += imag(s);
          sum_xx += imag(s)*imag(s);
        else
          % assume noise is orthogonal to imag axis
          sum_x  += real(s);
          sum_xx += real(s)*real(s);
        end
        n++;
    end
  end

  % trap corner case
  if n > 1
    noise_var = (n*sum_xx - sum_x*sum_x)/(n*(n-1));
  else
    noise_var = sig_var;
  end

  % Total noise power is twice estimate of single-axis noise.
  noise_var = 2*noise_var;
  
  EsNodB = 10*log10(sig_var/noise_var);  
endfunction


#{
  Plot curves of Es/No estimator in action.
  
  Plots indicate it works OK down to Es/No=3dB,
  where it is 1dB high.  That's acceptable as Es/No=3dB is the lower limit of 
  our operation (ie Eb/No=0dB, 10% raw BER).
#}

function [EsNo_est rx_symbols] = esno_est_curves(EsNodB=0:20, channel="awgn", plot_en=1)
    Nsym=1000; rand('seed',1); randn('seed',1);
    tx_symbols = 2*(rand(1,Nsym) > 0.5) -1 + j*(2*(rand(1,Nsym) > 0.5) - 1);
    tx_symbols *= exp(-j*pi/4)/sqrt(2);
    
    if strcmp(channel,"mpp")
        % for fading we assume perfect phase recovery, so just multiply by mag
        spread = doppler_spread(2.0, 50, length(tx_symbols));
        tx_symbols = tx_symbols .* abs(spread);
        % normalise power over the multipath channel run
        S = tx_symbols*tx_symbols';
        tx_symbols *= sqrt(Nsym/S);
    end
    
    for i = 1:length(EsNodB)
        aEsNodB = EsNodB(i);
        EsNo = 10 .^ (aEsNodB/10);
        N = 1/EsNo;
        noise = sqrt(N/2)*randn(1,Nsym) +  sqrt(N/2)*j*randn(1,Nsym);  
        S = tx_symbols*tx_symbols';
        N = noise*noise';
        EsNo_meas(i) = 10*log10(S/N);
        rx_symbols = tx_symbols + noise;  
        EsNo_est(i) = esno_est_calc(rx_symbols);
        printf("EsNo: %5.2f EsNo_meas: %5.2f EsNo_est: %5.2f\n", aEsNodB, EsNo_meas(i), EsNo_est(i));
    end
    if plot_en
        figure(1);
        plot(EsNodB, EsNo_meas, '+-;EsNo meas;');  hold on; plot(EsNodB, EsNo_est, 'o-;EsNo est;'); hold off;
        axis([0 max(EsNodB) 0 max(EsNodB)]); grid;
        figure(2); plot(tx_symbols,'+');
    end
endfunction

function esno_est_test(channel="awgn")
    test_point_dB = 5;
    [EsNo_est_awgn rx_syms] = esno_est_curves(test_point_dB, channel, plot_en=0);
    if abs(EsNo_est_awgn - test_point_dB) < 1.0
        printf("%s Pass\n",toupper(channel));
    else
        printf("%s Fail\n",toupper(channel));
    end
endfunction

function esno_est_tests_octave
    esno_est_test("awgn");
    esno_est_test("mpp");    
endfunction

function esno_est_test_c(channel="awgn")
    test_point_dB = 5;
    [EsNo_est rx_syms] = esno_est_curves(test_point_dB, channel, plot_en=0);
    rx_syms_float = zeros(1,2*length(rx_syms));
    rx_syms_float(1:2:end) = real(rx_syms);
    rx_syms_float(2:2:end) = imag(rx_syms);
    f = fopen("esno_est.iqfloat","wb"); fwrite(f, rx_syms_float, "float"); fclose(f);

    printf("\nRunning C version....\n");
    path_to_unittest = "../build_linux/unittest"
    if getenv("PATH_TO_UNITEST")
      path_to_unittest = getenv("PATH_TO_UNITEST")
      printf("setting path from env var to %s\n", path_to_unittest);
    end
    system(sprintf("%s/tesno_est %s %d > tesno_est_out.txt", path_to_unittest, "esno_est.iqfloat", length(rx_syms)));
    load tesno_est_out.txt;
    printf("test_point: %5.2f Octave: %5.2f C: %5.2f\n", test_point_dB, EsNo_est, tesno_est_out);
    if abs(EsNo_est - tesno_est_out) < 0.5
        printf("%s Pass\n",toupper(channel));
    else
        printf("%s Fail\n",toupper(channel));
    end
endfunction

function esno_est_tests_c
    esno_est_test_c("awgn");
    esno_est_test_c("mpp");    
endfunction


