diff options
-rw-r--r-- | .gitignore | 119 | ||||
-rw-r--r-- | dpd/README.md | 48 | ||||
-rw-r--r-- | dpd/dpd.ini | 27 | ||||
-rwxr-xr-x | dpd/iq_file_server.py | 120 | ||||
-rwxr-xr-x | dpd/main.py | 80 | ||||
-rw-r--r-- | dpd/poly.coef | 11 | ||||
-rwxr-xr-x | dpd/show_spectrum.py | 132 | ||||
-rw-r--r-- | dpd/src/Adapt.py | 164 | ||||
-rw-r--r-- | dpd/src/Dab_Util.py | 122 | ||||
-rw-r--r-- | dpd/src/Measure.py | 141 | ||||
-rw-r--r-- | dpd/src/Model.py | 66 | ||||
-rw-r--r-- | dpd/src/__init__.py | 0 | ||||
-rwxr-xr-x | dpd/src/subsample_align.py | 74 | ||||
-rw-r--r-- | dpd/src/test_dab_Util.py | 62 | ||||
-rw-r--r-- | dpd/src/test_measure.py | 33 | ||||
-rw-r--r-- | src/ConfigParser.cpp | 5 | ||||
-rw-r--r-- | src/ConfigParser.h | 2 | ||||
-rw-r--r-- | src/DabModulator.cpp | 9 | ||||
-rw-r--r-- | src/MemlessPoly.cpp | 133 | ||||
-rw-r--r-- | src/MemlessPoly.h | 11 |
20 files changed, 1273 insertions, 86 deletions
@@ -19,3 +19,122 @@ odr-dabmod .*.swp *~ + + +# Created by https://www.gitignore.io/api/vim,python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +# End of https://www.gitignore.io/api/vim,python diff --git a/dpd/README.md b/dpd/README.md index ec7cec2..b5a6b81 100644 --- a/dpd/README.md +++ b/dpd/README.md @@ -1,20 +1,56 @@ -Digital Predistortion for ODR-DabMod -==================================== +Digital Predistortion Calculation Engine for ODR-DabMod +======================================================= -This folder contains work in progress for digital predistortion. It requires: +This folder contains work in progress for digital predistortion. + +Concept +------- + +ODR-DabMod makes outgoing TX samples and feedback RX samples available for an external tool. This +external tool can request a buffer of samples for analysis, can calculate coefficients for the +polynomial predistorter in ODR-DabMod and load the new coefficients using the remote control. + +The *dpd/main.py* script is the entry point for the *DPD Calculation Engine* into which these +features will be implemented. The tool uses modules from the *dpd/src/* folder: + +- Sample transfer and time alignment with subsample accuracy is done by *Measure.py* +- Estimating the effects of the PA using some model and calculation of the updated + polynomial coefficients is done in *Model.py* +- Finally, *Adapt.py* loads them into ODR-DabMod. + +These modules themselves use additional helper scripts in the *dpd/src/* folder. + +Requirements +------------ - USRP B200. - Power amplifier. - A feedback connection from the power amplifier output, at an appropriate power level for the B200. - Usually this is done with a directional coupler. -- ODR-DabMod with enabled dpd_port, and with a samplerate of 8192000 samples per second. + Usually this is done with a directional coupler and additional attenuators. +- ODR-DabMod with enabled *dpd_port*, and with a samplerate of 8192000 samples per second. - Synchronous=1 so that the USRP has the timestamping set properly, internal refclk and pps are sufficient for this example. - A live mux source with TIST enabled. See dpd/dpd.ini for an example. +The DPD server port can be tested with the *dpd/show_spectrum.py* helper tool, which can also display +a constellation diagram. + +File format for coefficients +---------------------------- +The coef file contains the polynomial coefficients used in the predistorter. The file format is +very similar to the filtertaps file used in the FIR filter. It is a text-based format that can +easily be inspected and edited in a text editor. + +The first line contains the number of coefficients as an integer. The second and third lines contain +the real, respectively the imaginary parts of the first coefficient. Fourth and fifth lines give the +second coefficient, and so on. The file therefore contains 2xN + 1 lines if it contains N +coefficients. + TODO ---- -Implement a PA model that updates the predistorter. +Implement a PA model. +Implement cases for different oversampling for FFT bin choice. +Fix loads of missing and buggy aspects of the implementation. diff --git a/dpd/dpd.ini b/dpd/dpd.ini index 5e809e5..eb221e3 100644 --- a/dpd/dpd.ini +++ b/dpd/dpd.ini @@ -6,35 +6,48 @@ zmqctrlendpoint=tcp://127.0.0.1:9400 [log] syslog=0 -filelog=0 -filename=/dev/stderr +filelog=1 +filename=/tmp/dabmod.log [input] transport=tcp source=localhost:9200 [modulator] -digital_gain=0.8 -rate=8192000 +gainmode=var +rate=8196000 [firfilter] -enabled=0 +enabled=1 + +[poly] +enabled=1 +polycoeffile=dpd/poly.coef + +# How many threads to use for the predistorter. +# If not set, detect automatically. +#num_threads=2 [output] +# to prepare a file for the dpd/iq_file_server.py script, +# use output=file output=uhd +[fileoutput] +filename=dpd.iq + [uhdoutput] device= master_clock_rate=32768000 type=b200 -txgain=75 +txgain=70 channel=13C refclk_source=internal pps_source=none behaviour_refclk_lock_lost=ignore max_gps_holdover_time=600 dpd_port=50055 -rxgain=0 +rxgain=15 [delaymanagement] ; Use synchronous=1 so that the USRP time is set. This works diff --git a/dpd/iq_file_server.py b/dpd/iq_file_server.py new file mode 100755 index 0000000..7a4e570 --- /dev/null +++ b/dpd/iq_file_server.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# This example server simulates the ODR-DabMod's +# DPD server, taking samples from an IQ file +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file + +import sys +import socket +import struct +import argparse +import numpy as np +from datetime import datetime + +SIZEOF_SAMPLE = 8 # complex floats +# Constants for TM 1 +NbSymbols = 76 +NbCarriers = 1536 +Spacing = 2048 +NullSize = 2656 +SymSize = 2552 +FicSizeOut = 288 +FrameSize = NullSize + NbSymbols*SymSize + +def main(): + parser = argparse.ArgumentParser(description="Simulate ODR-DabMod DPD server") + parser.add_argument('--port', default='50055', + help='port to listen on (default: 50055)', + required=False) + parser.add_argument('--file', help='I/Q File to read from (complex floats)', + required=True) + parser.add_argument('--samplerate', default='8192000', help='Sample rate', + required=False) + + cli_args = parser.parse_args() + + serve_file(cli_args) + +def recv_exact(sock, num_bytes): + bufs = [] + while num_bytes > 0: + b = sock.recv(num_bytes) + if len(b) == 0: + break + num_bytes -= len(b) + bufs.append(b) + return b''.join(bufs) + +def serve_file(options): + oversampling = int(int(options.samplerate) / 2048000) + consumesamples = 8*FrameSize * oversampling + iq_data = np.fromfile(options.file, count=consumesamples, dtype=np.complex64) + + print("Loaded {} samples of IQ data".format(len(iq_data))) + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('localhost', int(options.port))) + s.listen() + + try: + while True: + sock, addr_info = s.accept() + print("Got a connection from {}".format(addr_info)) + + ver = recv_exact(sock, 1) + (num_samps,) = struct.unpack("=I", recv_exact(sock, 4)) + num_bytes = num_samps * SIZEOF_SAMPLE + + if num_bytes > len(iq_data): + print("Truncating length to {} samples".format(len(iq_data))) + num_samps = len(iq_data) + num_bytes = num_samps * 4 + + tx_sec = datetime.now().timestamp() + tx_pps = int(16384000 * (tx_sec - int(tx_sec))) + tx_second = int(tx_sec) + + # TX metadata and data + sock.sendall(struct.pack("=III", num_samps, tx_second, tx_pps)) + sock.sendall(iq_data[-num_samps:].tobytes()) + + # RX metadata and data + rx_second = tx_second + 1 + rx_pps = tx_pps + sock.sendall(struct.pack("=III", num_samps, rx_second, rx_pps)) + sock.sendall(iq_data[-num_samps:].tobytes()) + + print("Sent {} samples".format(num_samps)) + + sock.close() + finally: + s.close() + raise + +main() + + +# The MIT License (MIT) +# +# Copyright (c) 2017 Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/main.py b/dpd/main.py new file mode 100755 index 0000000..d7e2706 --- /dev/null +++ b/dpd/main.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# DPD Calculation Engine main file. +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file + +"""This Python script is the main file for ODR-DabMod's DPD Computation Engine. +This engine calculates and updates the parameter of the digital +predistortion module of ODR-DabMod.""" + +import logging +logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + filename='/tmp/dpd.log', + filemode='w', + level=logging.DEBUG) + +import src.Measure as Measure +import src.Model as Model +import src.Adapt as Adapt +import argparse + +parser = argparse.ArgumentParser(description="DPD Computation Engine for ODR-DabMod") +parser.add_argument('--port', default='50055', + help='port of DPD server to connect to (default: 50055)', + required=False) +parser.add_argument('--rc-port', default='9400', + help='port of ODR-DabMod ZMQ Remote Control to connect to (default: 9400)', + required=False) +parser.add_argument('--samplerate', default='8192000', + help='Sample rate', + required=False) +parser.add_argument('--coefs', default='dpdpoly.coef', + help='File with DPD coefficients, which will be read by ODR-DabMod', + required=False) +parser.add_argument('--samps', default='10240', + help='Number of samples to request from ODR-DabMod', + required=False) + +cli_args = parser.parse_args() + +port = int(cli_args.port) +port_rc = int(cli_args.rc_port) +coef_path = cli_args.coefs +num_req = int(cli_args.samps) +samplerate = int(cli_args.samplerate) + +meas = Measure.Measure(samplerate, port, num_req) +adapt = Adapt.Adapt(port_rc, coef_path) +coefs = adapt.get_coefs() +model = Model.Model(coefs) + +for i in range(10): + txframe_aligned, _, rxframe_aligned, _ = meas.get_samples() + coefs = model.get_next_coefs(txframe_aligned, rxframe_aligned) + adapt.set_coefs(coefs) + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger, Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/poly.coef b/dpd/poly.coef new file mode 100644 index 0000000..b29fa26 --- /dev/null +++ b/dpd/poly.coef @@ -0,0 +1,11 @@ +5 +0 +0 +0.8 +0 +0 +0 +0 +0 +0 +0 diff --git a/dpd/show_spectrum.py b/dpd/show_spectrum.py index e92c1d0..f23dba2 100755 --- a/dpd/show_spectrum.py +++ b/dpd/show_spectrum.py @@ -1,16 +1,15 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# This is an example tool that shows how to connect to ODR-DabMod's dpd TCP server -# and get samples from there. +# This is an example tool that shows how to connect to ODR-DabMod's dpd TCP +# server and get samples from there. # -# Since the TX and RX samples are not perfectly aligned, the tool has to align them properly, -# which is done in two steps: First on sample-level using a correlation, then with subsample -# accuracy using a FFT approach. +# Since the TX and RX samples are not perfectly aligned, the tool has to align +# them properly, which is done in two steps: First on sample-level using a +# correlation, then with subsample accuracy using a FFT approach. # # It requires SciPy and matplotlib. # -# Copyright (C) 2017 Matthias P. Braendli # http://www.opendigitalradio.org # Licence: The MIT License, see notice at the end of this file @@ -21,9 +20,18 @@ import numpy as np import matplotlib.pyplot as pp from matplotlib.animation import FuncAnimation import argparse +from scipy.misc import imsave SIZEOF_SAMPLE = 8 # complex floats +# Constants for TM 1 +NbSymbols = 76 +NbCarriers = 1536 +Spacing = 2048 +NullSize = 2656 +SymSize = 2552 +FicSizeOut = 288 + def main(): parser = argparse.ArgumentParser(description="Plot the spectrum of ODR-DabMod's DPD feedback") parser.add_argument('--samps', default='10240', help='Number of samples to request at once', @@ -31,18 +39,19 @@ def main(): parser.add_argument('--port', default='50055', help='port to connect to ODR-DabMod DPD (default: 50055)', required=False) - parser.add_argument('--animated', action='store_true', help='Enable real-time animation') + parser.add_argument('--constellation', action='store_true', help='Draw constellaton plot') + parser.add_argument('--samplerate', default='8192000', help='Sample rate', + required=False) cli_args = parser.parse_args() - port = int(cli_args.port) - num_samps_to_request = int(cli_args.samps) - - if cli_args.animated: - plot_spectrum_animated(port, num_samps_to_request) + if cli_args.constellation: + plot_constellation_once(cli_args) + elif cli_args.animated: + plot_spectrum_animated(cli_args) else: - plot_spectrum_once(port, num_samps_to_request) + plot_spectrum_once(cli_args) def recv_exact(sock, num_bytes): bufs = [] @@ -82,6 +91,7 @@ def get_samples(port, num_samps_to_request): else: txframe = np.array([], dtype=np.complex64) + print("Wait for RX metadata") rx_second, rx_pps = struct.unpack("=II", recv_exact(s, 8)) rx_ts = rx_second + rx_pps / 16384000.0 @@ -98,7 +108,7 @@ def get_samples(port, num_samps_to_request): return (tx_ts, txframe, rx_ts, rxframe) -def get_spectrum(port, num_samps_to_request): +def recv_rxtx(port, num_samps_to_request): tx_ts, txframe, rx_ts, rxframe = get_samples(port, num_samps_to_request) # convert to complex doubles for more dynamic range @@ -107,6 +117,10 @@ def get_spectrum(port, num_samps_to_request): print("Received {} & {} frames at {} and {}".format( len(txframe), len(rxframe), tx_ts, rx_ts)) + return tx_ts, txframe, rx_ts, rxframe + +def get_spectrum(port, num_samps_to_request): + tx_ts, txframe, rx_ts, rxframe = recv_rxtx(port, num_samps_to_request) print("Calculate TX and RX spectrum assuming 8192000 samples per second") tx_spectrum = np.fft.fftshift(np.fft.fft(txframe, fft_size)) @@ -116,12 +130,90 @@ def get_spectrum(port, num_samps_to_request): rx_power = 20*np.log10(np.abs(rx_spectrum)) return tx_power, rx_power +def remove_guard_intervals(frame, options): + """Remove the cyclic prefix. The frame needs to be aligned to the + end of the transmission frame. Transmission Mode 1 is assumed""" + oversample = int(int(options.samplerate) / 2048000) + + # From the end, take 2048 samples, then skip 504 samples + frame = frame[::-1] + + stride_len = Spacing * oversample + stride_advance = SymSize * oversample + + # Truncate the frame to an integer length of strides + newlen = len(frame) - (len(frame) % stride_advance) + print("Truncating frame from {} to {}".format(len(frame), newlen)) + frame = frame[:newlen] + + # Remove the cyclic prefix + frame = frame.reshape(-1, stride_advance)[:,:stride_len].reshape(-1) + + # Reverse again + return frame[::-1] + + +def plot_constellation_once(options): + port = int(options.port) + num_samps_to_request = int(options.samps) + + tx_ts, txframe, rx_ts, rxframe = recv_rxtx(port, num_samps_to_request) + + frame = remove_guard_intervals(txframe, options) + + oversample = int(int(options.samplerate) / 2048000) + + n = Spacing * oversample # is also number of samples per symbol + if len(frame) % n != 0: + raise ValueError("Frame length doesn't contain exact number of symbols") + num_syms = int(len(frame) / n) + print("frame {} has {} symbols".format(len(frame), num_syms)) + spectrums = np.array([np.fft.fftshift(np.fft.fft(frame[n*i:n*(i+1)], n)) for i in range(num_syms)]) + + def normalise(x): + """Normalise a real-valued array x to the range [0,1]""" + y = x + np.min(x) + return x / np.max(x) + + imsave("spectrums.png", np.concatenate([ + normalise(np.abs(spectrums)), + normalise(np.angle(spectrums))])) + + # Only take bins that are supposed to contain energy + # i.e. the middle 1536 bins, excluding the bin at n/2 + assert(n % 2 == 0) + n_half = int(n/2) + spectrums = np.concatenate( + [spectrums[...,n_half-768:n_half], + spectrums[...,n_half + 1:n_half + 769]], axis=1) + + sym_indices = (np.tile(np.arange(num_syms-1).reshape(num_syms-1,1), (1,NbCarriers)) + + np.tile(np.linspace(-0.4, 0.4, NbCarriers), (num_syms-1, 1) ) ) + sym_indices = sym_indices.reshape(-1) + diff_angles = np.mod(np.diff(np.angle(spectrums, deg=1), axis=0), 360) + #sym_points = spectrums[:-1].reshape(-1) + # Set amplitude and phase of low power points to zero, avoid cluttering diagram + #sym_points[np.abs(sym_points) < np.mean(np.abs(sym_points)) * 0.1] = 0 + + print("ix {} spec {} da {}".format( + sym_indices.shape, spectrums.shape, diff_angles.shape)) + + fig = pp.figure() + + fig.suptitle("Constellation") + ax1 = fig.add_subplot(111) + ax1.set_title("TX") + ax1.scatter(sym_indices, diff_angles.reshape(-1), alpha=0.1) + + pp.show() -sampling_rate = 8192000 fft_size = 4096 -freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./sampling_rate)) -def plot_spectrum_once(port, num_samps_to_request): +def plot_spectrum_once(options): + port = int(options.port) + num_samps_to_request = int(options.samps) + freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./int(options.samplerate))) + tx_power, rx_power = get_spectrum(port, num_samps_to_request) fig = pp.figure() @@ -134,7 +226,11 @@ def plot_spectrum_once(port, num_samps_to_request): ax2.plot(freqs, rx_power, 'b') pp.show() -def plot_spectrum_animated(port, num_samps_to_request): +def plot_spectrum_animated(options): + port = int(options.port) + num_samps_to_request = int(options.samps) + freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./int(options.samplerate))) + fig, axes = pp.subplots(2, sharex=True) line1, = axes[0].plot(freqs, np.ones(len(freqs)), 'r', animated=True) axes[0].set_title("TX") diff --git a/dpd/src/Adapt.py b/dpd/src/Adapt.py new file mode 100644 index 0000000..fec6aa5 --- /dev/null +++ b/dpd/src/Adapt.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# +# DPD Calculation Engine: updates ODR-DabMod's predistortion block. +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file +""" +This module is used to change settings of ODR-DabMod using +the ZMQ remote control socket. +""" + +import zmq +import logging +import numpy as np + +class Adapt: + """Uses the ZMQ remote control to change parameters of the DabMod + + Parameters + ---------- + port : int + Port at which the ODR-DabMod is listening to connect the + ZMQ remote control. + """ + + def __init__(self, port, coef_path): + logging.info("Instantiate Adapt object") + self.port = port + self.coef_path = coef_path + self.host = "localhost" + self._context = zmq.Context() + + def _connect(self): + """Establish the connection to ODR-DabMod using + a ZMQ socket that is in request mode (Client). + Returns a socket""" + sock = self._context.socket(zmq.REQ) + sock.connect("tcp://%s:%d" % (self.host, self.port)) + + sock.send(b"ping") + data = sock.recv_multipart() + + if data != ['ok']: + raise RuntimeError( + "Could not connect to server %s %d." % + (self.host, self.port)) + + return sock + + def send_receive(self, message): + """Send a message to ODR-DabMod. It always + returns the answer ODR-DabMod sends back. + + An example message could be + "get uhd txgain" or "set uhd txgain 50" + + Parameter + --------- + message : str + The message string that will be sent to the receiver. + """ + sock = self._connect() + logging.info("Send message: %s" % message) + msg_parts = message.split(" ") + for i, part in enumerate(msg_parts): + if i == len(msg_parts) - 1: + f = 0 + else: + f = zmq.SNDMORE + + sock.send(part.encode(), flags=f) + + data = sock.recv_multipart() + logging.info("Received message: %s" % message) + return data + + def set_txgain(self, gain): + """Set a new txgain for the ODR-DabMod. + + Parameters + ---------- + gain : int + new TX gain, in the same format as ODR-DabMod's config file + """ + # TODO this is specific to the B200 + if gain < 0 or gain > 89: + raise ValueError("Gain has to be in [0,89]") + return self.send_receive("set uhd txgain %d" % gain) + + def get_txgain(self): + """Get the txgain value in dB for the ODR-DabMod.""" + # TODO handle failure + return self.send_receive("get uhd txgain") + + def set_rxgain(self, gain): + """Set a new rxgain for the ODR-DabMod. + + Parameters + ---------- + gain : int + new RX gain, in the same format as ODR-DabMod's config file + """ + # TODO this is specific to the B200 + if gain < 0 or gain > 89: + raise ValueError("Gain has to be in [0,89]") + return self.send_receive("set uhd rxgain %d" % gain) + + def get_rxgain(self): + """Get the rxgain value in dB for the ODR-DabMod.""" + # TODO handle failure + return self.send_receive("get uhd rxgain") + + def _read_coef_file(self): + """Load the coefficients from the file in the format given in the README""" + coefs_complex = [] + f = open(self.coef_path, 'r') + lines = f.readlines() + n_coefs = int(lines[0]) + coefs = [float(l) for l in lines[1:]] + i = 0 + for r, c in zip(coefs[0::2], coefs[1::2]): + if i < n_coefs: + coefs_complex.append(np.complex64(r + 1j * c)) + else: + raise ValueError("Incorrect coef file format: too many coefficients") + i += 1 + f.close() + return coefs_complex + + def get_coefs(self): + return self._read_coef_file() + + def _write_coef_file(self, coefs_complex): + f = open(self.coef_path, 'w') + f.write("{}\n".format(len(coefs_complex)).encode()) + for coef in coefs_complex: + f.write("{}\n{}\n".format(coef.real, coef.imag).encode()) + f.close() + + def set_coefs(self, coefs_complex): + self._write_coef_file(coefs_complex) + self.send_receive("set memlesspoly coeffile {}".format(self.coef_path)) + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger, Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Dab_Util.py b/dpd/src/Dab_Util.py new file mode 100644 index 0000000..aee66d2 --- /dev/null +++ b/dpd/src/Dab_Util.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import scipy +import matplotlib.pyplot as plt +import src.subsample_align as sa +from scipy import signal +import logging + +class Dab_Util: + """Collection of methods that can be applied to an array + complex IQ samples of a DAB signal + """ + def __init__(self, sample_rate): + """ + :param sample_rate: sample rate [sample/sec] to use for calculations + """ + self.sample_rate = sample_rate + self.dab_bandwidth = 1536000 #Bandwidth of a dab signal + self.frame_ms = 96 #Duration of a Dab frame + + def lag(self, sig_orig, sig_rec): + """ + Find lag between two signals + Args: + sig_orig: The signal that has been sent + sig_rec: The signal that has been recored + """ + off = sig_rec.shape[0] + c = np.abs(signal.correlate(sig_orig, sig_rec)) + return np.argmax(c) - off + 1 + + def lag_upsampling(self, sig_orig, sig_rec, n_up): + if n_up != 1: + sig_orig_up = signal.resample(sig_orig, sig_orig.shape[0] * n_up) + sig_rec_up = signal.resample(sig_rec, sig_rec.shape[0] * n_up) + else: + sig_orig_up = sig_orig + sig_rec_up = sig_rec + l = self.lag(sig_orig_up, sig_rec_up) + l_orig = float(l) / n_up + return l_orig + + def subsample_align_upsampling(self, sig1, sig2, n_up=32): + """ + Returns an aligned version of sig1 and sig2 by cropping and subsample alignment + Using upsampling + """ + assert(sig1.shape[0] == sig2.shape[0]) + + if sig1.shape[0] % 2 == 1: + sig1 = sig1[:-1] + sig2 = sig2[:-1] + + sig1_up = signal.resample(sig1, sig1.shape[0] * n_up) + sig2_up = signal.resample(sig2, sig2.shape[0] * n_up) + + off_meas = self.lag_upsampling(sig2_up, sig1_up, n_up=1) + off = int(abs(off_meas)) + + if off_meas > 0: + sig1_up = sig1_up[:-off] + sig2_up = sig2_up[off:] + elif off_meas < 0: + sig1_up = sig1_up[off:] + sig2_up = sig2_up[:-off] + + sig1 = signal.resample(sig1_up, sig1_up.shape[0] / n_up).astype(np.complex64) + sig2 = signal.resample(sig2_up, sig2_up.shape[0] / n_up).astype(np.complex64) + return sig1, sig2 + + def subsample_align(self, sig1, sig2): + """ + Returns an aligned version of sig1 and sig2 by cropping and subsample alignment + """ + logging.debug("Sig1_orig: %d %s, Sig2_orig: %d %s" % (len(sig1), sig1.dtype, len(sig2), sig2.dtype)) + off_meas = self.lag_upsampling(sig2, sig1, n_up=1) + off = int(abs(off_meas)) + + if off_meas > 0: + sig1 = sig1[:-off] + sig2 = sig2[off:] + elif off_meas < 0: + sig1 = sig1[off:] + sig2 = sig2[:-off] + + if off % 2 == 1: + sig1 = sig1[:-1] + sig2 = sig2[:-1] + + sig2 = sa.subsample_align(sig2, sig1) + logging.debug("Sig1_cut: %d %s, Sig2_cut: %d %s, off: %d" % (len(sig1), sig1.dtype, len(sig2), sig2.dtype, off)) + return sig1, sig2 + + def fromfile(self, filename, offset=0, length=None): + if length is None: + return np.memmap(filename, dtype=np.complex64, mode='r', offset=64/8*offset) + else: + return np.memmap(filename, dtype=np.complex64, mode='r', offset=64/8*offset, shape=length) + + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Measure.py b/dpd/src/Measure.py new file mode 100644 index 0000000..7b5379d --- /dev/null +++ b/dpd/src/Measure.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- + +import sys +import socket +import struct +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +import argparse +import os +import time +import logging +import src.Dab_Util as DU +import datetime + +class Measure: + """Collect Measurement from DabMod""" + def __init__(self, samplerate, port, num_samples_to_request): + logging.info("Instantiate Measure object") + self.samplerate = samplerate + self.sizeof_sample = 8 # complex floats + self.port = port + self.num_samples_to_request = num_samples_to_request + + def _recv_exact(self, sock, num_bytes): + """Receive an exact number of bytes from a socket. This is + a wrapper around sock.recv() that can return less than the number + of requested bytes. + + Args: + sock (socket): Socket to receive data from. + num_bytes (int): Number of bytes that will be returned. + """ + bufs = [] + while num_bytes > 0: + b = sock.recv(num_bytes) + if len(b) == 0: + break + num_bytes -= len(b) + bufs.append(b) + return b''.join(bufs) + + def get_samples(self): + """Connect to ODR-DabMod, retrieve TX and RX samples, load + into numpy arrays, and return a tuple + (tx_timestamp, tx_samples, rx_timestamp, rx_samples) + where the timestamps are doubles, and the samples are numpy + arrays of complex floats, both having the same size + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(('localhost', self.port)) + + logging.debug("Send version") + s.sendall(b"\x01") + + logging.debug("Send request for {} samples".format(self.num_samples_to_request)) + s.sendall(struct.pack("=I", self.num_samples_to_request)) + + logging.debug("Wait for TX metadata") + num_samps, tx_second, tx_pps = struct.unpack("=III", self._recv_exact(s, 12)) + tx_ts = tx_second + tx_pps / 16384000.0 + + if num_samps > 0: + logging.debug("Receiving {} TX samples".format(num_samps)) + txframe_bytes = self._recv_exact(s, num_samps * self.sizeof_sample) + txframe = np.fromstring(txframe_bytes, dtype=np.complex64) + else: + txframe = np.array([], dtype=np.complex64) + + logging.debug("Wait for RX metadata") + rx_second, rx_pps = struct.unpack("=II", self._recv_exact(s, 8)) + rx_ts = rx_second + rx_pps / 16384000.0 + + if num_samps > 0: + logging.debug("Receiving {} RX samples".format(num_samps)) + rxframe_bytes = self._recv_exact(s, num_samps * self.sizeof_sample) + rxframe = np.fromstring(rxframe_bytes, dtype=np.complex64) + else: + rxframe = np.array([], dtype=np.complex64) + + if logging.getLogger().getEffectiveLevel() == logging.DEBUG: + txframe_path = ('/tmp/txframe_fft_' + + datetime.datetime.now().isoformat() + + '.pdf') + plt.plot(np.abs(np.fft.fftshift(np.fft.fft(txframe[2048:])))) + plt.savefig(txframe_path) + plt.clf() + + rxframe_path = ('/tmp/rxframe_fft_' + + datetime.datetime.now().isoformat() + + '.pdf') + plt.plot(np.abs(np.fft.fftshift(np.fft.fft(rxframe[2048:])))) + plt.savefig(rxframe_path) + plt.clf() + + logging.debug("txframe: min %f, max %f, median %f, spectrum %s" % + (np.min(np.abs(txframe)), + np.max(np.abs(txframe)), + np.median(np.abs(txframe)), + txframe_path)) + + logging.debug("rxframe: min %f, max %f, median %f, spectrum %s" % + (np.min(np.abs(rxframe)), + np.max(np.abs(rxframe)), + np.median(np.abs(rxframe)), + rxframe_path)) + + logging.debug("Disconnecting") + s.close() + + du = DU.Dab_Util(self.samplerate) + txframe_aligned, rxframe_aligned = du.subsample_align(txframe, rxframe) + + logging.info( + "Measurement done, tx %d %s, rx %d %s, tx aligned %d %s, rx aligned %d %s" + % (len(txframe), txframe.dtype, len(rxframe), rxframe.dtype, + len(txframe_aligned), txframe_aligned.dtype, len(rxframe_aligned), rxframe_aligned.dtype) ) + + return txframe_aligned, rxframe_aligned + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Model.py b/dpd/src/Model.py new file mode 100644 index 0000000..9ad7532 --- /dev/null +++ b/dpd/src/Model.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import datetime +import logging +import matplotlib.pyplot as plt + +class Model: + """Calculates new coefficients using the measurement and the old + coefficients""" + + def __init__(self, coefs): + self.coefs = coefs + + def get_next_coefs(self, txframe_aligned, rxframe_aligned): + if logging.getLogger().getEffectiveLevel() == logging.DEBUG: + logging.debug("txframe: min %f, max %f, median %f" % + (np.min(np.abs(txframe_aligned)), + np.max(np.abs(txframe_aligned)), + np.median(np.abs(txframe_aligned)) + )) + + logging.debug("rxframe: min %f, max %f, median %f" % + (np.min(np.abs(rxframe_aligned)), + np.max(np.abs(rxframe_aligned)), + np.median(np.abs(rxframe_aligned)) + )) + + tx_rx_frame_path = ('/tmp/tx_rx_sync_' + + datetime.datetime.now().isoformat() + + '.pdf') + plt.plot(np.real(rxframe_aligned[:1024]), label="rxframe") + plt.plot(np.real(txframe_aligned[:1024]), label="txframe") + plt.xlabel("Samples") + plt.ylabel("Real Part") + plt.legend() + plt.savefig(tx_rx_frame_path) + plt.clf() + logging.debug("Tx, Rx synchronized %s" % tx_rx_frame_path) + + mse = np.mean(np.abs(np.square(txframe_aligned - rxframe_aligned))) + logging.debug("MSE: {}".format(mse)) + + return self.coefs + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/__init__.py b/dpd/src/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/dpd/src/__init__.py diff --git a/dpd/src/subsample_align.py b/dpd/src/subsample_align.py new file mode 100755 index 0000000..c30af7c --- /dev/null +++ b/dpd/src/subsample_align.py @@ -0,0 +1,74 @@ +import numpy as np +from scipy import signal, optimize +import sys +import matplotlib.pyplot as plt + +def gen_omega(length): + if (length % 2) == 1: + raise ValueError("Needs an even length array.") + + halflength = int(length/2) + factor = 2.0 * np.pi / length + + omega = np.zeros(length, dtype=np.float) + for i in range(halflength): + omega[i] = factor * i + + for i in range(halflength, length): + omega[i] = factor * (i - length) + + return omega + +def subsample_align(sig, ref_sig): + """Do subsample alignment for sig relative to the reference signal + ref_sig. The delay between the two must be less than sample + + Returns the aligned signal""" + + n = len(sig) + if (n % 2) == 1: + raise ValueError("Needs an even length signal.") + halflen = int(n/2) + + fft_sig = np.fft.fft(sig) + + omega = gen_omega(n) + + def correlate_for_delay(tau): + # A subsample offset between two signals corresponds, in the frequency + # domain, to a linearly increasing phase shift, whose slope + # corresponds to the delay. + # + # Here, we build this phase shift in rotate_vec, and multiply it with + # our signal. + + rotate_vec = np.exp(1j * tau * omega) + # zero-frequency is rotate_vec[0], so rotate_vec[N/2] is the + # bin corresponding to the [-1, 1, -1, 1, ...] time signal, which + # is both the maximum positive and negative frequency. + # I don't remember why we handle it differently. + rotate_vec[halflen] = np.cos(np.pi * tau) + + corr_sig = np.fft.ifft(rotate_vec * fft_sig) + + # TODO why do we only look at the real part? Because it's faster than + # a complex cross-correlation? Clarify! + return -np.sum(np.real(corr_sig) * np.real(ref_sig.real)) + + optim_result = optimize.minimize_scalar(correlate_for_delay, bounds=(-1,1), method='bounded', options={'disp': True}) + + if optim_result.success: + #print("x:") + #print(optim_result.x) + + best_tau = optim_result.x + + #print("Found subsample delay = {}".format(best_tau)) + + # Prepare rotate_vec = fft_sig with rotated phase + rotate_vec = np.exp(1j * best_tau * omega) + rotate_vec[halflen] = np.cos(np.pi * best_tau) + return np.fft.ifft(rotate_vec * fft_sig).astype(np.complex64) + else: + #print("Could not optimize: " + optim_result.message) + return np.zeros(0, dtype=np.complex64) diff --git a/dpd/src/test_dab_Util.py b/dpd/src/test_dab_Util.py new file mode 100644 index 0000000..0b2fa4f --- /dev/null +++ b/dpd/src/test_dab_Util.py @@ -0,0 +1,62 @@ +from unittest import TestCase + +import numpy as np +import pandas as pd +import src.Dab_Util as DU + +class TestDab_Util(TestCase): + + def test_subsample_align(self, sample_orig=r'../test_data/orig_rough_aligned.dat', + sample_rec =r'../test_data/recored_rough_aligned.dat', + length = 10240, max_size = 1000000): + du = DU.Dab_Util(8196000) + res1 = [] + res2 = [] + for i in range(10): + start = np.random.randint(50, max_size) + r = np.random.randint(-50, 50) + + s1 = du.fromfile(sample_orig, offset=start+r, length=length) + s2 = du.fromfile(sample_rec, offset=start, length=length) + + res1.append(du.lag_upsampling(s2, s1, 32)) + + s1_aligned, s2_aligned = du.subsample_align(s1, s2) + + res2.append(du.lag_upsampling(s2_aligned, s1_aligned, 32)) + + error_rate = np.mean(np.array(res2) != 0) + self.assertEqual(error_rate, 0.0, "The error rate for aligning was %.2f%%" + % error_rate * 100) + +#def test_using_aligned_pair(sample_orig=r'../data/orig_rough_aligned.dat', sample_rec =r'../data/recored_rough_aligned.dat', length = 10240, max_size = 1000000): +# res = [] +# for i in tqdm(range(100)): +# start = np.random.randint(50, max_size) +# r = np.random.randint(-50, 50) +# +# s1 = du.fromfile(sample_orig, offset=start+r, length=length) +# s2 = du.fromfile(sample_rec, offset=start, length=length) +# +# res.append({'offset':r, +# '1':r - du.lag_upsampling(s2, s1, n_up=1), +# '2':r - du.lag_upsampling(s2, s1, n_up=2), +# '3':r - du.lag_upsampling(s2, s1, n_up=3), +# '4':r - du.lag_upsampling(s2, s1, n_up=4), +# '8':r - du.lag_upsampling(s2, s1, n_up=8), +# '16':r - du.lag_upsampling(s2, s1, n_up=16), +# '32':r - du.lag_upsampling(s2, s1, n_up=32), +# }) +# df = pd.DataFrame(res) +# df = df.reindex_axis(sorted(df.columns), axis=1) +# print(df.describe()) +# +# +#print("Align using upsampling") +#for n_up in [1, 2, 3, 4, 7, 8, 16]: +# correct_ratio = test_phase_offset(lambda x,y: du.lag_upsampling(x,y,n_up), tol=1./n_up) +# print("%.1f%% of the tested offsets were measured within tolerance %.4f for n_up = %d" % (correct_ratio * 100, 1./n_up, n_up)) +#test_using_aligned_pair() +# +#print("Phase alignment") +#test_subsample_alignment() diff --git a/dpd/src/test_measure.py b/dpd/src/test_measure.py new file mode 100644 index 0000000..b695721 --- /dev/null +++ b/dpd/src/test_measure.py @@ -0,0 +1,33 @@ +from unittest import TestCase +from Measure import Measure +import socket + + +class TestMeasure(TestCase): + + def _open_socks(self): + sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock_server.bind(('localhost', 1234)) + sock_server.listen(1) + + sock_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_client.connect(('localhost', 1234)) + + conn_server, addr_server = sock_server.accept() + return conn_server, sock_client + + def test__recv_exact(self): + m = Measure(1234, 1) + payload = b"test payload" + + conn_server, sock_client = self._open_socks() + conn_server.send(payload) + rec = m._recv_exact(sock_client, len(payload)) + + self.assertEqual(rec, payload, + "Did not receive the same message as sended. (%s, %s)" % + (rec, payload)) + + def test_get_samples(self): + self.fail() diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp index 459811f..9ac1280 100644 --- a/src/ConfigParser.cpp +++ b/src/ConfigParser.cpp @@ -171,7 +171,10 @@ static void parse_configfile( // Poly coefficients: if (pt.get("poly.enabled", 0) == 1) { mod_settings.polyCoefFilename = - pt.get<std::string>("poly.polycoeffile", "default"); + pt.get<std::string>("poly.polycoeffile", "dpd/poly.coef"); + + mod_settings.polyNumThreads = + pt.get<int>("poly.num_threads", 0); } // Output options diff --git a/src/ConfigParser.h b/src/ConfigParser.h index 22a4fc5..89f0fb7 100644 --- a/src/ConfigParser.h +++ b/src/ConfigParser.h @@ -75,7 +75,7 @@ struct mod_settings_t { std::string filterTapsFilename = ""; std::string polyCoefFilename = ""; - + unsigned polyNumThreads = 0; #if defined(HAVE_OUTPUT_UHD) OutputUHDConfig outputuhd_conf; diff --git a/src/DabModulator.cpp b/src/DabModulator.cpp index 34d8e66..cc2642a 100644 --- a/src/DabModulator.cpp +++ b/src/DabModulator.cpp @@ -205,13 +205,8 @@ int DabModulator::process(Buffer* dataOut) shared_ptr<MemlessPoly> cifPoly; if (not m_settings.polyCoefFilename.empty()) { - cifPoly = make_shared<MemlessPoly>(m_settings.polyCoefFilename); - etiLog.level(debug) << m_settings.polyCoefFilename << "\n"; - etiLog.level(debug) << cifPoly->m_coefs[0] << " " << - cifPoly->m_coefs[1] << " "<< cifPoly->m_coefs[2] << " "<< - cifPoly->m_coefs[3] << " "<< cifPoly->m_coefs[4] << " "<< - cifPoly->m_coefs[5] << " "<< cifPoly->m_coefs[6] << " "<< - cifPoly->m_coefs[7] << "\n"; + cifPoly = make_shared<MemlessPoly>(m_settings.polyCoefFilename, + m_settings.polyNumThreads); rcs.enrol(cifPoly.get()); } diff --git a/src/MemlessPoly.cpp b/src/MemlessPoly.cpp index 7e074eb..b0d950c 100644 --- a/src/MemlessPoly.cpp +++ b/src/MemlessPoly.cpp @@ -36,6 +36,7 @@ #include <stdio.h> #include <stdexcept> +#include <future> #include <array> #include <iostream> #include <fstream> @@ -43,22 +44,36 @@ using namespace std; +#define NUM_COEFS 5 // By default the signal is unchanged -static const std::array<float, 8> default_coefficients({ +static const std::array<complexf, 8> default_coefficients({{ 1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 - }); + }}); -MemlessPoly::MemlessPoly(const std::string& coefs_file) : +MemlessPoly::MemlessPoly(const std::string& coefs_file, unsigned int num_threads) : PipelinedModCodec(), RemoteControllable("memlesspoly"), - m_coefs_file(coefs_file) + m_num_threads(num_threads), + m_coefs(), + m_coefs_file(coefs_file), + m_coefs_mutex() { PDEBUG("MemlessPoly::MemlessPoly(%s) @ %p\n", coefs_file.c_str(), this); + if (m_num_threads == 0) { + const unsigned int hw_concurrency = std::thread::hardware_concurrency(); + etiLog.level(info) << "Polynomial Predistorter will use " << + hw_concurrency << " threads (auto detected)"; + } + else { + etiLog.level(info) << "Polynomial Predistorter will use " << + m_num_threads << " threads (set in config file)"; + } + RC_ADD_PARAMETER(ncoefs, "(Read-only) number of coefficients."); RC_ADD_PARAMETER(coeffile, "Filename containing coefficients. When written to, the new file gets automatically loaded."); @@ -69,41 +84,42 @@ MemlessPoly::MemlessPoly(const std::string& coefs_file) : void MemlessPoly::load_coefficients(const std::string &coefFile) { - std::vector<float> coefs; + std::vector<complexf> coefs; if (coefFile == "default") { std::copy(default_coefficients.begin(), default_coefficients.end(), std::back_inserter(coefs)); } else { std::ifstream coef_fstream(coefFile.c_str()); - if(!coef_fstream) { - fprintf(stderr, "MemlessPoly: file %s could not be opened !\n", coefFile.c_str()); - throw std::runtime_error("MemlessPoly: Could not open file with coefs! "); + if (!coef_fstream) { + throw std::runtime_error("MemlessPoly: Could not open file with coefs!"); } int n_coefs; coef_fstream >> n_coefs; if (n_coefs <= 0) { - fprintf(stderr, "MemlessPoly: warning: coefs file has invalid format\n"); throw std::runtime_error("MemlessPoly: coefs file has invalid format."); } - - if (n_coefs != 8) { - throw std::runtime_error( "MemlessPoly: error: coefs file does not have 8 coefs\n"); + else if (n_coefs != NUM_COEFS) { + throw std::runtime_error("MemlessPoly: invalid number of coefs: " + + std::to_string(coefs.size())); } - fprintf(stderr, "MemlessPoly: Reading %d coefs...\n", n_coefs); + etiLog.log(debug, "MemlessPoly: Reading %d coefs...", n_coefs); coefs.resize(n_coefs); - int n; - for (n = 0; n < n_coefs; n++) { - coef_fstream >> coefs[n]; - PDEBUG("MemlessPoly: coef: %f\n", coefs[n] ); + for (int n = 0; n < n_coefs; n++) { + float a, b; + coef_fstream >> a; + coef_fstream >> b; + coefs[n] = complexf(a, b); + if (coef_fstream.eof()) { - fprintf(stderr, "MemlessPoly: file %s should contains %d coefs, but EOF reached "\ - "after %d coefs !\n", coefFile.c_str(), n_coefs, n); - throw std::runtime_error("MemlessPoly: coefs file invalid ! "); + etiLog.log(error, "MemlessPoly: file %s should contains %d coefs, " + "but EOF reached after %d coefs !", + coefFile.c_str(), n_coefs, n); + throw std::runtime_error("MemlessPoly: coefs file invalid !"); } } } @@ -115,30 +131,69 @@ void MemlessPoly::load_coefficients(const std::string &coefFile) } } +static void apply_coeff( + const vector<complexf> &coefs, + const complexf* in, size_t start, size_t stop, + complexf* out) +{ + for (size_t i = start; i < stop; i++) { + + /* Implement + a0 + a1*x + a2*x^2 + a3*x^3 + a4*x^4 + a5*x^5; + with less multiplications: + a0 + x*(a1 + x*(a2 + x*(a3 + x*(a3 + x*(a4 + a5*x))))); + */ + + /* Make sure to adapt NUM_COEFS when you change this */ + out[i] = + coefs[0] + in[i] * + ( coefs[1] + in[i] * + ( coefs[2] + in[i] * + ( coefs[3] + in[i] * + ( coefs[4] + in[i] * + ( coefs[5] + in[i] ))))); + } +} int MemlessPoly::internal_process(Buffer* const dataIn, Buffer* dataOut) { - const float* in = reinterpret_cast<const float*>(dataIn->getData()); - float* out = reinterpret_cast<float*>(dataOut->getData()); - size_t sizeIn = dataIn->getLength() / sizeof(float); - - { - std::lock_guard<std::mutex> lock(m_coefs_mutex); - for (size_t i = 0; i < sizeIn; i += 1) { - float mag = std::abs(in[i]); - //out[i] = in[i]; - out[i] = in[i] * ( - m_coefs[0] + - m_coefs[1] * mag + - m_coefs[2] * mag*mag + - m_coefs[3] * mag*mag*mag + - m_coefs[4] * mag*mag*mag*mag + - m_coefs[5] * mag*mag*mag*mag*mag + - m_coefs[6] * mag*mag*mag*mag*mag*mag + - m_coefs[7] * mag*mag*mag*mag*mag*mag*mag - ); + dataOut->setLength(dataIn->getLength()); + + const complexf* in = reinterpret_cast<const complexf*>(dataIn->getData()); + complexf* out = reinterpret_cast<complexf*>(dataOut->getData()); + size_t sizeOut = dataOut->getLength() / sizeof(complexf); + + { + std::lock_guard<std::mutex> lock(m_coefs_mutex); + const unsigned int hw_concurrency = std::thread::hardware_concurrency(); + + const unsigned int num_threads = + (m_num_threads > 0) ? m_num_threads : hw_concurrency; + + if (num_threads) { + const size_t step = sizeOut / num_threads; + vector<future<void> > flags; + + size_t start = 0; + for (size_t i = 0; i < num_threads - 1; i++) { + flags.push_back(async(launch::async, apply_coeff, + m_coefs, in, start, start + step, out)); + + start += step; + } + + // Do the last in this thread + apply_coeff(m_coefs, in, start, sizeOut, out); + + // Wait for completion of the tasks + for (auto& f : flags) { + f.get(); } } + else { + apply_coeff(m_coefs, in, 0, sizeOut, out); + } + } return dataOut->getLength(); } diff --git a/src/MemlessPoly.h b/src/MemlessPoly.h index 210b4b4..b7fd81e 100644 --- a/src/MemlessPoly.h +++ b/src/MemlessPoly.h @@ -52,7 +52,7 @@ typedef std::complex<float> complexf; class MemlessPoly : public PipelinedModCodec, public RemoteControllable { public: - MemlessPoly(const std::string& coefs_file); + MemlessPoly(const std::string& coefs_file, unsigned int num_threads); virtual const char* name() { return "MemlessPoly"; } @@ -63,16 +63,13 @@ public: virtual const std::string get_parameter( const std::string& parameter) const; -//TODO to protected - std::vector<float> m_coefs; - - -protected: +private: int internal_process(Buffer* const dataIn, Buffer* dataOut); void load_coefficients(const std::string &coefFile); + unsigned int m_num_threads; + std::vector<complexf> m_coefs; std::string m_coefs_file; - mutable std::mutex m_coefs_mutex; }; |