/*
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
*/
/*
This file is part of ODR-DabMod.
ODR-DabMod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
ODR-DabMod is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ODR-DabMod. If not, see .
*/
#include "OfdmGenerator.h"
#include "PcDebug.h"
#define FFT_TYPE fftwf_complex
#include
#include
#include
#include
#include
static const size_t MAX_CLIP_STATS = 10;
OfdmGenerator::OfdmGenerator(size_t nbSymbols,
size_t nbCarriers,
size_t spacing,
bool& enableCfr,
float& cfrClip,
float& cfrErrorClip,
bool inverse) :
ModCodec(), RemoteControllable("ofdm"),
myFftPlan(nullptr),
myFftIn(nullptr), myFftOut(nullptr),
myNbSymbols(nbSymbols),
myNbCarriers(nbCarriers),
mySpacing(spacing),
myCfr(enableCfr),
myCfrClip(cfrClip),
myCfrErrorClip(cfrErrorClip),
myCfrFft(nullptr),
// Initialise the PAPRStats to a few seconds worth of samples
myPaprBeforeCFR(nbSymbols * 50),
myPaprAfterCFR(nbSymbols * 50)
{
PDEBUG("OfdmGenerator::OfdmGenerator(%zu, %zu, %zu, %s) @ %p\n",
nbSymbols, nbCarriers, spacing, inverse ? "true" : "false", this);
if (nbCarriers > spacing) {
throw std::runtime_error(
"OfdmGenerator::OfdmGenerator nbCarriers > spacing!");
}
/* register the parameters that can be remote controlled */
RC_ADD_PARAMETER(cfr, "Enable crest factor reduction");
RC_ADD_PARAMETER(clip, "CFR: Clip to amplitude");
RC_ADD_PARAMETER(errorclip, "CFR: Limit error");
RC_ADD_PARAMETER(clip_stats, "CFR: statistics (clip ratio, errorclip ratio)");
RC_ADD_PARAMETER(papr, "PAPR measurements (before CFR, after CFR)");
if (inverse) {
myPosDst = (nbCarriers & 1 ? 0 : 1);
myPosSrc = 0;
myPosSize = (nbCarriers + 1) / 2;
myNegDst = spacing - (nbCarriers / 2);
myNegSrc = (nbCarriers + 1) / 2;
myNegSize = nbCarriers / 2;
}
else {
myPosDst = (nbCarriers & 1 ? 0 : 1);
myPosSrc = nbCarriers / 2;
myPosSize = (nbCarriers + 1) / 2;
myNegDst = spacing - (nbCarriers / 2);
myNegSrc = 0;
myNegSize = nbCarriers / 2;
}
myZeroDst = myPosDst + myPosSize;
myZeroSize = myNegDst - myZeroDst;
PDEBUG(" myPosDst: %u\n", myPosDst);
PDEBUG(" myPosSrc: %u\n", myPosSrc);
PDEBUG(" myPosSize: %u\n", myPosSize);
PDEBUG(" myNegDst: %u\n", myNegDst);
PDEBUG(" myNegSrc: %u\n", myNegSrc);
PDEBUG(" myNegSize: %u\n", myNegSize);
PDEBUG(" myZeroDst: %u\n", myZeroDst);
PDEBUG(" myZeroSize: %u\n", myZeroSize);
const int N = mySpacing; // The size of the FFT
myFftIn = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
myFftOut = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
fftwf_set_timelimit(2);
myFftPlan = fftwf_plan_dft_1d(N,
myFftIn, myFftOut,
FFTW_BACKWARD, FFTW_MEASURE);
myCfrPostClip = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
myCfrPostFft = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
myCfrFft = fftwf_plan_dft_1d(N,
myCfrPostClip, myCfrPostFft,
FFTW_FORWARD, FFTW_MEASURE);
if (sizeof(complexf) != sizeof(FFT_TYPE)) {
printf("sizeof(complexf) %zu\n", sizeof(complexf));
printf("sizeof(FFT_TYPE) %zu\n", sizeof(FFT_TYPE));
throw std::runtime_error(
"OfdmGenerator::process complexf size is not FFT_TYPE size!");
}
}
OfdmGenerator::~OfdmGenerator()
{
PDEBUG("OfdmGenerator::~OfdmGenerator() @ %p\n", this);
if (myFftIn) {
fftwf_free(myFftIn);
}
if (myFftOut) {
fftwf_free(myFftOut);
}
if (myFftPlan) {
fftwf_destroy_plan(myFftPlan);
}
if (myCfrPostClip) {
fftwf_free(myCfrPostClip);
}
if (myCfrPostFft) {
fftwf_free(myCfrPostFft);
}
if (myCfrFft) {
fftwf_destroy_plan(myCfrFft);
}
}
int OfdmGenerator::process(Buffer* const dataIn, Buffer* dataOut)
{
PDEBUG("OfdmGenerator::process(dataIn: %p, dataOut: %p)\n",
dataIn, dataOut);
dataOut->setLength(myNbSymbols * mySpacing * sizeof(complexf));
FFT_TYPE* in = reinterpret_cast(dataIn->getData());
FFT_TYPE* out = reinterpret_cast(dataOut->getData());
size_t sizeIn = dataIn->getLength() / sizeof(complexf);
size_t sizeOut = dataOut->getLength() / sizeof(complexf);
if (sizeIn != myNbSymbols * myNbCarriers) {
PDEBUG("Nb symbols: %zu\n", myNbSymbols);
PDEBUG("Nb carriers: %zu\n", myNbCarriers);
PDEBUG("Spacing: %zu\n", mySpacing);
PDEBUG("\n%zu != %zu\n", sizeIn, myNbSymbols * myNbCarriers);
throw std::runtime_error(
"OfdmGenerator::process input size not valid!");
}
if (sizeOut != myNbSymbols * mySpacing) {
PDEBUG("Nb symbols: %zu\n", myNbSymbols);
PDEBUG("Nb carriers: %zu\n", myNbCarriers);
PDEBUG("Spacing: %zu\n", mySpacing);
PDEBUG("\n%zu != %zu\n", sizeIn, myNbSymbols * mySpacing);
throw std::runtime_error(
"OfdmGenerator::process output size not valid!");
}
// It is not guaranteed that fftw keeps the FFT input vector intact.
// That's why we copy it to the reference.
std::vector reference;
// IFFT output before CFR applied, for MER calc
std::vector before_cfr;
size_t num_clip = 0;
size_t num_error_clip = 0;
// For performance reasons, do not calculate MER for every symbol.
myMERCalcIndex = (myMERCalcIndex + 1) % myNbSymbols;
// The PAPRStats' clear() is not threadsafe, do not access it
// from the RC functions.
if (myPaprClearRequest.exchange(false)) {
myPaprBeforeCFR.clear();
myPaprAfterCFR.clear();
}
for (size_t i = 0; i < myNbSymbols; ++i) {
myFftIn[0][0] = 0;
myFftIn[0][1] = 0;
/* For TM I this is:
* ZeroDst=769 ZeroSize=511
* PosSrc=0 PosDst=1 PosSize=768
* NegSrc=768 NegDst=1280 NegSize=768
*/
memset(&myFftIn[myZeroDst], 0, myZeroSize * sizeof(FFT_TYPE));
memcpy(&myFftIn[myPosDst], &in[myPosSrc],
myPosSize * sizeof(FFT_TYPE));
memcpy(&myFftIn[myNegDst], &in[myNegSrc],
myNegSize * sizeof(FFT_TYPE));
if (myCfr) {
reference.resize(mySpacing);
memcpy(reinterpret_cast(reference.data()),
myFftIn, mySpacing * sizeof(FFT_TYPE));
}
fftwf_execute(myFftPlan); // IFFT from myFftIn to myFftOut
if (myCfr) {
complexf *symbol = reinterpret_cast(myFftOut);
myPaprBeforeCFR.process_block(symbol, mySpacing);
if (myMERCalcIndex == i) {
before_cfr.resize(mySpacing);
memcpy(reinterpret_cast(before_cfr.data()),
myFftOut, mySpacing * sizeof(FFT_TYPE));
}
/* cfr_one_iteration runs the myFftPlan again at the end, and
* therefore writes the output data to myFftOut.
*/
const auto stat = cfr_one_iteration(symbol, reference.data());
// i == 0 always zero power, so the MER ends up being NaN
if (i > 0) {
myPaprAfterCFR.process_block(symbol, mySpacing);
}
if (i > 0 and myMERCalcIndex == i) {
/* MER definition, ETSI ETR 290, Annex C
*
* \sum I^2 + Q^2
* MER[dB] = 10 log_10( ---------------- )
* \sum dI^2 + dQ^2
* Where I and Q are the ideal coordinates, and dI and dQ are
* the errors in the received datapoints.
*
* In our case, we consider the constellation points given to the
* OfdmGenerator as "ideal", and we compare the CFR output to it.
*/
double sum_iq = 0;
double sum_delta = 0;
for (size_t j = 0; j < mySpacing; j++) {
sum_iq += (double)std::norm(before_cfr[j]);
sum_delta += (double)std::norm(symbol[j] - before_cfr[j]);
}
// Clamp to 90dB, otherwise the MER average is going to be inf
const double mer = sum_delta > 0 ?
10.0 * std::log10(sum_iq / sum_delta) : 90;
myMERs.push_back(mer);
}
num_clip += stat.clip_count;
num_error_clip += stat.errclip_count;
}
memcpy(out, myFftOut, mySpacing * sizeof(FFT_TYPE));
in += myNbCarriers;
out += mySpacing;
}
if (myCfr) {
std::lock_guard lock(myCfrRcMutex);
const double num_samps = myNbSymbols * mySpacing;
const double clip_ratio = (double)num_clip / num_samps;
myClipRatios.push_back(clip_ratio);
while (myClipRatios.size() > MAX_CLIP_STATS) {
myClipRatios.pop_front();
}
const double errclip_ratio = (double)num_error_clip / num_samps;
myErrorClipRatios.push_back(errclip_ratio);
while (myErrorClipRatios.size() > MAX_CLIP_STATS) {
myErrorClipRatios.pop_front();
}
while (myMERs.size() > MAX_CLIP_STATS) {
myMERs.pop_front();
}
}
return sizeOut;
}
OfdmGenerator::cfr_iter_stat_t OfdmGenerator::cfr_one_iteration(
complexf *symbol, const complexf *reference)
{
// use std::norm instead of std::abs to avoid calculating the
// square roots
const float clip_squared = myCfrClip * myCfrClip;
OfdmGenerator::cfr_iter_stat_t ret;
// Clip
for (size_t i = 0; i < mySpacing; i++) {
const float mag_squared = std::norm(symbol[i]);
if (mag_squared > clip_squared) {
// normalise absolute value to myCfrClip:
// x_clipped = x * clip / |x|
// = x * sqrt(clip_squared) / sqrt(mag_squared)
// = x * sqrt(clip_squared / mag_squared)
symbol[i] *= std::sqrt(clip_squared / mag_squared);
ret.clip_count++;
}
}
// Take FFT of our clipped signal
memcpy(myCfrPostClip, symbol, mySpacing * sizeof(FFT_TYPE));
fftwf_execute(myCfrFft); // FFT from myCfrPostClip to myCfrPostFft
// Calculate the error in frequency domain by subtracting our reference
// and clip it to myCfrErrorClip. By adding this clipped error signal
// to our FFT output, we compensate the introduced error to some
// extent.
const float err_clip_squared = myCfrErrorClip * myCfrErrorClip;
std::vector error_norm(mySpacing);
for (size_t i = 0; i < mySpacing; i++) {
// FFTW computes an unnormalised transform, i.e. a FFT-IFFT pair
// or vice-versa gives back the original vector scaled by a factor
// FFT-size. Because we're comparing our constellation point
// (calculated with IFFT-clip-FFT) against reference (input to
// the IFFT), we need to divide by our FFT size.
const complexf constellation_point =
reinterpret_cast(myCfrPostFft)[i] / (float)mySpacing;
complexf error = reference[i] - constellation_point;
const float mag_squared = std::norm(error);
error_norm[i] = mag_squared;
if (mag_squared > err_clip_squared) {
error *= std::sqrt(err_clip_squared / mag_squared);
ret.errclip_count++;
}
// Update the input to the FFT directly to avoid another copy for the
// subsequence IFFT
complexf *fft_in = reinterpret_cast(myFftIn);
fft_in[i] = constellation_point + error;
}
// Run our error-compensated symbol through the IFFT again
fftwf_execute(myFftPlan); // IFFT from myFftIn to myFftOut
return ret;
}
void OfdmGenerator::set_parameter(const std::string& parameter,
const std::string& value)
{
using namespace std;
stringstream ss(value);
ss.exceptions ( stringstream::failbit | stringstream::badbit );
if (parameter == "cfr") {
ss >> myCfr;
myPaprClearRequest.store(true);
}
else if (parameter == "clip") {
ss >> myCfrClip;
myPaprClearRequest.store(true);
}
else if (parameter == "errorclip") {
ss >> myCfrErrorClip;
myPaprClearRequest.store(true);
}
else if (parameter == "clip_stats" or parameter == "papr") {
throw ParameterError("Parameter '" + parameter + "' is read-only");
}
else {
stringstream ss_err;
ss_err << "Parameter '" << parameter
<< "' is not exported by controllable " << get_rc_name();
throw ParameterError(ss_err.str());
}
}
const std::string OfdmGenerator::get_parameter(const std::string& parameter) const
{
using namespace std;
stringstream ss;
if (parameter == "cfr") {
ss << myCfr;
}
else if (parameter == "clip") {
ss << std::fixed << myCfrClip;
}
else if (parameter == "errorclip") {
ss << std::fixed << myCfrErrorClip;
}
else if (parameter == "clip_stats") {
std::lock_guard lock(myCfrRcMutex);
if (myClipRatios.empty() or myErrorClipRatios.empty() or myMERs.empty()) {
ss << "No stats available";
}
else {
const double avg_clip_ratio =
std::accumulate(myClipRatios.begin(), myClipRatios.end(), 0.0) /
myClipRatios.size();
const double avg_errclip_ratio =
std::accumulate(myErrorClipRatios.begin(), myErrorClipRatios.end(), 0.0) /
myErrorClipRatios.size();
const double avg_mer =
std::accumulate(myMERs.begin(), myMERs.end(), 0.0) /
myMERs.size();
ss << "Statistics : " << std::fixed <<
avg_clip_ratio * 100 << "%"" samples clipped, " <<
avg_errclip_ratio * 100 << "%"" errors clipped. " <<
"MER after CFR: " << avg_mer << " dB";
}
}
else if (parameter == "papr") {
const double papr_before = myPaprBeforeCFR.calculate_papr();
const double papr_after = myPaprAfterCFR.calculate_papr();
ss << "PAPR [dB]: " << std::fixed <<
(papr_before == 0 ? string("N/A") : to_string(papr_before)) <<
", " <<
(papr_after == 0 ? string("N/A") : to_string(papr_after));
}
else {
ss << "Parameter '" << parameter <<
"' is not exported by controllable " << get_rc_name();
throw ParameterError(ss.str());
}
return ss.str();
}
const RemoteControllable::map_t OfdmGenerator::get_all_values() const
{
map_t map;
// TODO needs rework of the values
return map;
}