diff options
Diffstat (limited to 'python/dpd')
-rw-r--r-- | python/dpd/Adapt.py | 291 | ||||
-rw-r--r-- | python/dpd/ExtractStatistic.py | 37 | ||||
-rw-r--r-- | python/dpd/GlobalConfig.py | 11 | ||||
-rw-r--r-- | python/dpd/Measure.py | 36 | ||||
-rw-r--r-- | python/dpd/Model_AM.py | 122 | ||||
-rw-r--r-- | python/dpd/Model_PM.py | 124 | ||||
-rw-r--r-- | python/dpd/Model_Poly.py | 147 | ||||
-rw-r--r-- | python/dpd/RX_Agc.py | 39 |
8 files changed, 312 insertions, 495 deletions
diff --git a/python/dpd/Adapt.py b/python/dpd/Adapt.py index 840aee9..a30f0c8 100644 --- a/python/dpd/Adapt.py +++ b/python/dpd/Adapt.py @@ -9,263 +9,184 @@ This module is used to change settings of ODR-DabMod using the ZMQ remote control socket. """ -import zmq import logging import numpy as np -import os -import datetime +import os.path import pickle +from lib import zmqrc +from typing import List LUT_LEN = 32 FORMAT_POLY = 1 FORMAT_LUT = 2 -def _write_poly_coef_file(coefs_am, coefs_pm, path): - assert (len(coefs_am) == len(coefs_pm)) +def _write_poly_coef_file(coefs_am: List[float], coefs_pm: List[float], path: str) -> None: + assert len(coefs_am) == len(coefs_pm) - f = open(path, 'w') - f.write("{}\n{}\n".format(FORMAT_POLY, len(coefs_am))) - for coef in coefs_am: - f.write("{}\n".format(coef)) - for coef in coefs_pm: - f.write("{}\n".format(coef)) - f.close() + with open(path, 'w') as f: + f.write("{}\n{}\n".format(FORMAT_POLY, len(coefs_am))) + for coef in coefs_am: + f.write("{}\n".format(coef)) + for coef in coefs_pm: + f.write("{}\n".format(coef)) -def _write_lut_file(scalefactor, lut, path): - assert (len(lut) == LUT_LEN) +def _write_lut_file(scalefactor: float, lut: List[complex], path: str) -> None: + assert len(lut) == LUT_LEN - f = open(path, 'w') - f.write("{}\n{}\n".format(FORMAT_LUT, scalefactor)) - for coef in lut: - f.write("{}\n{}\n".format(coef.real, coef.imag)) - f.close() + with open(path, 'w') as f: + f.write("{}\n{}\n".format(FORMAT_LUT, scalefactor)) + for coef in lut: + f.write("{}\n{}\n".format(coef.real, coef.imag)) -def dpddata_to_str(dpddata): +def dpddata_to_str(dpddata) -> str: if dpddata[0] == "poly": coefs_am = dpddata[1] coefs_pm = dpddata[2] - return "dpd_coefs_am {}, dpd_coefs_pm {}".format( + return "Poly: AM/AM {}, AM/PM {}".format( coefs_am, coefs_pm) elif dpddata[0] == "lut": scalefactor = dpddata[1] lut = dpddata[2] - return "LUT scalefactor {}, LUT {}".format( + return "LUT: scalefactor {}, LUT {}".format( scalefactor, lut) else: raise ValueError("Unknown dpddata type {}".format(dpddata[0])) class Adapt: - """Uses the ZMQ remote control to change parameters of the DabMod + """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, plot_location): + def __init__(self, port: int, coef_path: str, plot_location: str): logging.debug("Instantiate Adapt object") - self.port = port - self.coef_path = coef_path - self.plot_location = plot_location - 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) - poller = zmq.Poller() - poller.register(sock, zmq.POLLIN) - - sock.connect("tcp://%s:%d" % (self.host, self.port)) - - sock.send(b"ping") - - socks = dict(poller.poll(1000)) - if socks: - if socks.get(sock) == zmq.POLLIN: - data = [el.decode() for el in sock.recv_multipart()] - - if data != ['ok']: - raise RuntimeError( - "Invalid ZMQ RC answer to 'ping' at %s %d: %s" % - (self.host, self.port, data)) - else: - sock.close(linger=10) - raise RuntimeError( - "ZMQ RC does not respond to 'ping' at %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. + self._port = port + self._coef_path = coef_path + self._plot_location = plot_location + self._host = "localhost" + self._mod_rc = zmqrc.ModRemoteControl(self._host, self._port) - An example message could be - "get sdr txgain" or "set sdr txgain 50" - - Parameter - --------- - message : str - The message string that will be sent to the receiver. - """ - sock = self._connect() - logging.debug("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 = [el.decode() for el in sock.recv_multipart()] - logging.debug("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 - """ + def set_txgain(self, gain : float) -> None: # 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 sdr txgain %.4f" % float(gain)) + self._mod_rc.set_param_value("sdr", "txgain", "%.4f" % float(gain)) - def get_txgain(self): - """Get the txgain value in dB for the ODR-DabMod.""" - # TODO handle failure - return float(self.send_receive("get sdr txgain")[0]) + def get_txgain(self) -> float: + """Get the txgain value in dB, or -1 in case of error""" + try: + return float(self._mod_rc.get_param_value("sdr", "txgain")) + except ValueError as e: + logging.warning(f"Adapt: get_txgain error: {e}") + return -1.0 - 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 - """ + def set_rxgain(self, gain: float) -> None: # 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 sdr rxgain %.4f" % float(gain)) - - def get_rxgain(self): - """Get the rxgain value in dB for the ODR-DabMod.""" - # TODO handle failure - return float(self.send_receive("get sdr rxgain")[0]) - - def set_digital_gain(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 - """ - msg = "set gain digital %.5f" % gain - return self.send_receive(msg) - - def get_digital_gain(self): - """Get the rxgain value in dB for the ODR-DabMod.""" - # TODO handle failure - return float(self.send_receive("get gain digital")[0]) + self._mod_rc.set_param_value("sdr", "rxgain", "%.4f" % float(gain)) + + def get_rxgain(self) -> float: + """Get the rxgain value in dB, or -1 in case of error""" + try: + return float(self._mod_rc.get_param_value("sdr", "rxgain")) + except ValueError as e: + logging.warning(f"Adapt: get_rxgain error: {e}") + return -1.0 + + def set_digital_gain(self, gain: float) -> None: + self._mod_rc.set_param_value("gain", "digital", "%.5f" % float(gain)) + + def get_digital_gain(self) -> float: + """Get the digital gain value in linear scale, or -1 in case + of error""" + try: + return float(self._mod_rc.get_param_value("gain", "digital")) + except ValueError as e: + logging.warning(f"Adapt: get_digital_gain error: {e}") + return -1.0 def get_predistorter(self): """Load the coefficients from the file in the format given in the README, return ("poly", [AM coef], [PM coef]) or ("lut", scalefactor, [LUT entries]) """ - f = open(self.coef_path, 'r') - lines = f.readlines() - predistorter_format = int(lines[0]) - if predistorter_format == FORMAT_POLY: - coefs_am_out = [] - coefs_pm_out = [] - n_coefs = int(lines[1]) - coefs = [float(l) for l in lines[2:]] - i = 0 - for c in coefs: - if i < n_coefs: - coefs_am_out.append(c) - elif i < 2 * n_coefs: - coefs_pm_out.append(c) - else: - raise ValueError( - 'Incorrect coef file format: too many' - ' coefficients in {}, should be {}, coefs are {}' - .format(self.coef_path, n_coefs, coefs)) - i += 1 - f.close() - return 'poly', coefs_am_out, coefs_pm_out - elif predistorter_format == FORMAT_LUT: - scalefactor = int(lines[1]) - coefs = np.array([float(l) for l in lines[2:]], dtype=np.float32) - coefs = coefs.reshape((-1, 2)) - lut = coefs[..., 0] + 1j * coefs[..., 1] - if len(lut) != LUT_LEN: - raise ValueError("Incorrect number of LUT entries ({} expected {})".format(len(lut), LUT_LEN)) - return 'lut', scalefactor, lut - else: - raise ValueError("Unknown predistorter format {}".format(predistorter_format)) + with open(self._coef_path, 'r') as f: + lines = f.readlines() + predistorter_format = int(lines[0]) + if predistorter_format == FORMAT_POLY: + coefs_am_out = [] + coefs_pm_out = [] + n_coefs = int(lines[1]) + coefs = [float(l) for l in lines[2:]] + for i, c in enumerate(coefs): + if i < n_coefs: + coefs_am_out.append(c) + elif i < 2 * n_coefs: + coefs_pm_out.append(c) + else: + raise ValueError( + 'Incorrect coef file format: too many' + ' coefficients in {}, should be {}, coefs are {}' + .format(self._coef_path, n_coefs, coefs)) + return 'poly', coefs_am_out, coefs_pm_out + elif predistorter_format == FORMAT_LUT: + scalefactor = int(lines[1]) + coefs = np.array([float(l) for l in lines[2:]], dtype=np.float32) + coefs = coefs.reshape((-1, 2)) + lut = coefs[..., 0] + 1j * coefs[..., 1] + if len(lut) != LUT_LEN: + raise ValueError("Incorrect number of LUT entries ({} expected {})".format(len(lut), LUT_LEN)) + return 'lut', scalefactor, lut + else: + raise ValueError("Unknown predistorter format {}".format(predistorter_format)) - def set_predistorter(self, dpddata): + def set_predistorter(self, dpddata) -> None: """Update the predistorter data in the modulator. Takes the same tuple format as argument than the one returned get_predistorter()""" if dpddata[0] == "poly": coefs_am = dpddata[1] coefs_pm = dpddata[2] - _write_poly_coef_file(coefs_am, coefs_pm, self.coef_path) + _write_poly_coef_file(coefs_am, coefs_pm, self._coef_path) elif dpddata[0] == "lut": scalefactor = dpddata[1] lut = dpddata[2] - _write_lut_file(scalefactor, lut, self.coef_path) + _write_lut_file(scalefactor, lut, self._coef_path) else: raise ValueError("Unknown predistorter '{}'".format(dpddata[0])) - self.send_receive("set memlesspoly coeffile {}".format(self.coef_path)) + self._mod_rc.set_param_value("memlesspoly", "coeffile", self._coef_path) - def dump(self, path=None): + def dump(self, path: str) -> None: """Backup current settings to a file""" - dt = datetime.datetime.now().isoformat() - if path is None: - if self.plot_location is not None: - path = self.plot_location + "/" + dt + "_adapt.pkl" - else: - raise Exception("Cannot dump Adapt without either plot_location or path set") + d = { "txgain": self.get_txgain(), "rxgain": self.get_rxgain(), "digital_gain": self.get_digital_gain(), - "predistorter": self.get_predistorter() + "dpddata": self.get_predistorter() } + with open(path, "wb") as f: pickle.dump(d, f) - return path - - def load(self, path): + def restore(self, path: str): """Restore settings from a file""" with open(path, "rb") as f: d = pickle.load(f) - self.set_txgain(d["txgain"]) + self.set_txgain(0) + + # If any of the following fail, we will be running + # with the safe value of txgain=0 self.set_digital_gain(d["digital_gain"]) self.set_rxgain(d["rxgain"]) - self.set_predistorter(d["predistorter"]) + self.set_predistorter(d["dpddata"]) + self.set_txgain(d["txgain"]) + + return d # The MIT License (MIT) # -# Copyright (c) 2017 Andreas Steger, Matthias P. Braendli +# Copyright (c) 2019 Matthias P. Braendli +# 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 diff --git a/python/dpd/ExtractStatistic.py b/python/dpd/ExtractStatistic.py index 639513a..3518dba 100644 --- a/python/dpd/ExtractStatistic.py +++ b/python/dpd/ExtractStatistic.py @@ -38,14 +38,16 @@ class ExtractStatistic: """Calculate a low variance RX value for equally spaced tx values of a predefined range""" - def __init__(self, c): + def __init__(self, c, peak_amplitude): self.c = c + self._plot_data = None + # Number of measurements used to extract the statistic self.n_meas = 0 # Boundaries for the bins - self.tx_boundaries = np.linspace(c.ES_start, c.ES_end, c.ES_n_bins + 1) + self.tx_boundaries = np.linspace(0.0, peak_amplitude, c.ES_n_bins + 1) self.n_per_bin = c.ES_n_per_bin # List of rx values for each bin @@ -58,12 +60,14 @@ class ExtractStatistic: for i in range(c.ES_n_bins): self.tx_values_lists.append([]) - self.plot = c.ES_plot + def get_bin_info(self): + return "Binning: {} bins used for amplitudes between {} and {}".format( + len(self.tx_boundaries), np.min(self.tx_boundaries), np.max(self.tx_boundaries)) + + def plot(self, plot_path, title): + if self._plot_data is not None: + tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists = self._plot_data - def _plot_and_log(self, tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists): - if self.plot and self.c.plot_location is not None: - dt = datetime.datetime.now().isoformat() - fig_path = self.c.plot_location + "/" + dt + "_ExtractStatistic.png" sub_rows = 3 sub_cols = 1 fig = plt.figure(figsize=(sub_cols * 6, sub_rows / 2. * 6)) @@ -72,7 +76,7 @@ class ExtractStatistic: i_sub += 1 ax = plt.subplot(sub_rows, sub_cols, i_sub) ax.plot(tx_values, rx_values, - label="Estimated Values", + label="Averaged measurements", color="red") for i, tx_value in enumerate(tx_values): rx_values_list = self.rx_values_lists[i] @@ -80,17 +84,17 @@ class ExtractStatistic: np.abs(rx_values_list), s=0.1, color="black") - ax.set_title("Extracted Statistic") + ax.set_title("Extracted Statistic {}".format(title)) ax.set_xlabel("TX Amplitude") ax.set_ylabel("RX Amplitude") - ax.set_ylim(0, 0.8) - ax.set_xlim(0, 1.1) + ax.set_ylim(0, np.max(self.tx_boundaries)) # we expect a rougly a 1:1 correspondence between x and y + ax.set_xlim(0, np.max(self.tx_boundaries)) ax.legend(loc=4) i_sub += 1 ax = plt.subplot(sub_rows, sub_cols, i_sub) ax.plot(tx_values, np.rad2deg(phase_diffs_values), - label="Estimated Values", + label="Averaged measurements", color="red") for i, tx_value in enumerate(tx_values): phase_diff = phase_diffs_values_lists[i] @@ -101,7 +105,7 @@ class ExtractStatistic: ax.set_xlabel("TX Amplitude") ax.set_ylabel("Phase Difference") ax.set_ylim(-60, 60) - ax.set_xlim(0, 1.1) + ax.set_xlim(0, np.max(self.tx_boundaries)) ax.legend(loc=4) num = [] @@ -111,12 +115,12 @@ class ExtractStatistic: i_sub += 1 ax = plt.subplot(sub_rows, sub_cols, i_sub) ax.plot(num) - ax.set_xlabel("TX Amplitude") + ax.set_xlabel("TX Amplitude bin") ax.set_ylabel("Number of Samples") ax.set_ylim(0, self.n_per_bin * 1.2) fig.tight_layout() - fig.savefig(fig_path) + fig.savefig(plot_path) plt.close(fig) def _rx_value_per_bin(self): @@ -166,7 +170,7 @@ class ExtractStatistic: phase_diffs_values_lists = self._phase_diff_list_per_bin() phase_diffs_values = _phase_diff_value_per_bin(phase_diffs_values_lists) - self._plot_and_log(tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists) + self._plot_data = (tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists) tx_values_crop = np.array(tx_values, dtype=np.float32)[:idx_end] rx_values_crop = np.array(rx_values, dtype=np.float32)[:idx_end] @@ -176,6 +180,7 @@ class ExtractStatistic: # The MIT License (MIT) # # Copyright (c) 2017 Andreas Steger +# Copyright (c) 2018 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 diff --git a/python/dpd/GlobalConfig.py b/python/dpd/GlobalConfig.py index 873b6ac..632a63b 100644 --- a/python/dpd/GlobalConfig.py +++ b/python/dpd/GlobalConfig.py @@ -10,7 +10,7 @@ import numpy as np class GlobalConfig: - def __init__(self, samplerate, plot_location: str): + def __init__(self, samplerate: int, plot_location: str): self.sample_rate = samplerate assert self.sample_rate == 8192000, "We only support constants for 8192000 sample rate: {}".format(self.sample_rate) @@ -26,6 +26,8 @@ class GlobalConfig: self.T_U = oversample * 2048 # Inverse of carrier spacing self.T_C = oversample * 504 # Duration of cyclic prefix + self.median_to_peak = 12 # Estimated value for a DAB OFDM signal + # Frequency Domain # example: np.delete(fft[3328:4865], 768) self.FFT_delta = 1536 # Number of carrier frequencies @@ -40,10 +42,8 @@ class GlobalConfig: self.phase_offset_per_sample = 1. / self.sample_rate * 2 * np.pi * 1000 # Constants for ExtractStatistic - self.ES_plot = plot - self.ES_start = 0.0 self.ES_end = 1.0 - self.ES_n_bins = 64 # Number of bins between ES_start and ES_end + self.ES_n_bins = 64 self.ES_n_per_bin = 128 # Number of measurements pre bin # Constants for Measure_Shoulder @@ -68,9 +68,6 @@ class GlobalConfig: # Constants for MER self.MER_plot = plot - # Constants for Model - self.MDL_plot = plot - # Constants for Model_PM # Set all phase offsets to zero for TX amplitude < MPM_tx_min self.MPM_tx_min = 0.1 diff --git a/python/dpd/Measure.py b/python/dpd/Measure.py index 489c4c0..e5a72c7 100644 --- a/python/dpd/Measure.py +++ b/python/dpd/Measure.py @@ -15,7 +15,7 @@ import logging class Measure: """Collect Measurement from DabMod""" - def __init__(self, config, samplerate, port, num_samples_to_request): + def __init__(self, config, samplerate : int, port : int, num_samples_to_request : int): logging.info("Instantiate Measure object") self.c = config self.samplerate = samplerate @@ -23,7 +23,7 @@ class Measure: self.port = port self.num_samples_to_request = num_samples_to_request - def _recv_exact(self, sock, num_bytes): + def _recv_exact(self, sock : socket.socket, num_bytes : int) -> 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. @@ -41,7 +41,7 @@ class Measure: bufs.append(b) return b''.join(bufs) - def receive_tcp(self): + def receive_tcp(self, num_samples_to_request : int): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(4) s.connect(('localhost', self.port)) @@ -49,8 +49,8 @@ class Measure: 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("Send request for {} samples".format(num_samples_to_request)) + s.sendall(struct.pack("=I", num_samples_to_request)) logging.debug("Wait for TX metadata") num_samps, tx_second, tx_pps = struct.unpack("=III", self._recv_exact(s, 12)) @@ -90,14 +90,35 @@ class Measure: return txframe, tx_ts, rxframe, rx_ts + def get_samples_unaligned(self, short=False): + """Connect to ODR-DabMod, retrieve TX and RX samples, load + into numpy arrays, and return a tuple + (txframe, tx_ts, rxframe, rx_ts, rx_median, tx_median) + """ + + n_samps = int(self.num_samples_to_request / 4) if short else self.num_samples_to_request + txframe, tx_ts, rxframe, rx_ts = self.receive_tcp(n_samps) + + # Normalize received signal with sent signal + rx_median = np.median(np.abs(rxframe)) + tx_median = np.median(np.abs(txframe)) + rxframe = rxframe / rx_median * tx_median + + + logging.info( + "Measurement done, tx %d %s, rx %d %s" % + (len(txframe), txframe.dtype, len(rxframe), rxframe.dtype)) + + return txframe, tx_ts, rxframe, rx_ts, rx_median, tx_median - def get_samples(self): + def get_samples(self, short=False): """Connect to ODR-DabMod, retrieve TX and RX samples, load into numpy arrays, and return a tuple (txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median) """ - txframe, tx_ts, rxframe, rx_ts = self.receive_tcp() + n_samps = int(self.num_samples_to_request / 4) if short else self.num_samples_to_request + txframe, tx_ts, rxframe, rx_ts = self.receive_tcp(n_samps) # Normalize received signal with sent signal rx_median = np.median(np.abs(rxframe)) @@ -116,6 +137,7 @@ class Measure: # The MIT License (MIT) # +# Copyright (c) 2018 Matthias P. Braendli # Copyright (c) 2017 Andreas Steger # # Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/python/dpd/Model_AM.py b/python/dpd/Model_AM.py deleted file mode 100644 index 75b226f..0000000 --- a/python/dpd/Model_AM.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- -# -# DPD Computation Engine, model implementation for Amplitude and not Phase -# -# http://www.opendigitalradio.org -# Licence: The MIT License, see notice at the end of this file - -import datetime -import os -import logging -import numpy as np -import matplotlib.pyplot as plt - - -def is_npfloat32(array): - assert isinstance(array, np.ndarray), type(array) - assert array.dtype == np.float32, array.dtype - assert array.flags.contiguous - assert not any(np.isnan(array)) - - -def check_input_get_next_coefs(tx_dpd, rx_received): - is_npfloat32(tx_dpd) - is_npfloat32(rx_received) - - -def poly(sig): - return np.array([sig ** i for i in range(1, 6)]).T - - -def fit_poly(tx_abs, rx_abs): - return np.linalg.lstsq(poly(rx_abs), tx_abs, rcond=None)[0] - - -def calc_line(coefs, min_amp, max_amp): - rx_range = np.linspace(min_amp, max_amp) - tx_est = np.sum(poly(rx_range) * coefs, axis=1) - return tx_est, rx_range - - -class Model_AM: - """Calculates new coefficients using the measurement and the previous - coefficients""" - - def __init__(self, - c, - learning_rate_am=1, - plot=False): - self.c = c - - self.learning_rate_am = learning_rate_am - self.plot = plot - - def _plot(self, tx_dpd, rx_received, coefs_am, coefs_am_new): - if self.plot and self.c.plot_location is not None: - tx_range, rx_est = calc_line(coefs_am, 0, 0.6) - tx_range_new, rx_est_new = calc_line(coefs_am_new, 0, 0.6) - - dt = datetime.datetime.now().isoformat() - fig_path = self.c.plot_location + "/" + dt + "_Model_AM.png" - sub_rows = 1 - sub_cols = 1 - fig = plt.figure(figsize=(sub_cols * 6, sub_rows / 2. * 6)) - i_sub = 0 - - i_sub += 1 - ax = plt.subplot(sub_rows, sub_cols, i_sub) - ax.plot(tx_range, rx_est, - label="Estimated TX", - alpha=0.3, - color="gray") - ax.plot(tx_range_new, rx_est_new, - label="New Estimated TX", - color="red") - ax.scatter(tx_dpd, rx_received, - label="Binned Data", - color="blue", - s=1) - ax.set_title("Model_AM") - ax.set_xlabel("TX Amplitude") - ax.set_ylabel("RX Amplitude") - ax.set_xlim(-0.5, 1.5) - ax.legend(loc=4) - - fig.tight_layout() - fig.savefig(fig_path) - plt.close(fig) - - def get_next_coefs(self, tx_dpd, rx_received, coefs_am): - """Calculate the next AM/AM coefficients using the extracted - statistic of TX and RX amplitude""" - check_input_get_next_coefs(tx_dpd, rx_received) - - coefs_am_new = fit_poly(tx_dpd, rx_received) - coefs_am_new = coefs_am + \ - self.learning_rate_am * (coefs_am_new - coefs_am) - - self._plot(tx_dpd, rx_received, coefs_am, coefs_am_new) - - return coefs_am_new - -# 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/python/dpd/Model_PM.py b/python/dpd/Model_PM.py deleted file mode 100644 index 7b80bf3..0000000 --- a/python/dpd/Model_PM.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -# -# DPD Computation Engine, model implementation for Amplitude and not Phase -# -# http://www.opendigitalradio.org -# Licence: The MIT License, see notice at the end of this file - -import datetime -import os -import logging -import numpy as np -import matplotlib.pyplot as plt - - -def is_npfloat32(array): - assert isinstance(array, np.ndarray), type(array) - assert array.dtype == np.float32, array.dtype - assert array.flags.contiguous - assert not any(np.isnan(array)) - - -def check_input_get_next_coefs(tx_dpd, phase_diff): - is_npfloat32(tx_dpd) - is_npfloat32(phase_diff) - - -class Model_PM: - """Calculates new coefficients using the measurement and the previous - coefficients""" - - def __init__(self, - c, - learning_rate_pm=1, - plot=False): - self.c = c - - self.learning_rate_pm = learning_rate_pm - self.plot = plot - - def _plot(self, tx_dpd, phase_diff, coefs_pm, coefs_pm_new): - if self.plot and self.c.plot_location is not None: - tx_range, phase_diff_est = self.calc_line(coefs_pm, 0, 0.6) - tx_range_new, phase_diff_est_new = self.calc_line(coefs_pm_new, 0, 0.6) - - dt = datetime.datetime.now().isoformat() - fig_path = self.c.plot_location + "/" + dt + "_Model_PM.png" - sub_rows = 1 - sub_cols = 1 - fig = plt.figure(figsize=(sub_cols * 6, sub_rows / 2. * 6)) - i_sub = 0 - - i_sub += 1 - ax = plt.subplot(sub_rows, sub_cols, i_sub) - ax.plot(tx_range, phase_diff_est, - label="Estimated Phase Diff", - alpha=0.3, - color="gray") - ax.plot(tx_range_new, phase_diff_est_new, - label="New Estimated Phase Diff", - color="red") - ax.scatter(tx_dpd, phase_diff, - label="Binned Data", - color="blue", - s=1) - ax.set_title("Model_PM") - ax.set_xlabel("TX Amplitude") - ax.set_ylabel("Phase DIff") - ax.legend(loc=4) - - fig.tight_layout() - fig.savefig(fig_path) - plt.close(fig) - - def _discard_small_values(self, tx_dpd, phase_diff): - """ Assumes that the phase for small tx amplitudes is zero""" - mask = tx_dpd < self.c.MPM_tx_min - phase_diff[mask] = 0 - return tx_dpd, phase_diff - - def poly(self, sig): - return np.array([sig ** i for i in range(0, 5)]).T - - def fit_poly(self, tx_abs, phase_diff): - return np.linalg.lstsq(self.poly(tx_abs), phase_diff, rcond=None)[0] - - def calc_line(self, coefs, min_amp, max_amp): - tx_range = np.linspace(min_amp, max_amp) - phase_diff = np.sum(self.poly(tx_range) * coefs, axis=1) - return tx_range, phase_diff - - def get_next_coefs(self, tx_dpd, phase_diff, coefs_pm): - """Calculate the next AM/PM coefficients using the extracted - statistic of TX amplitude and phase difference""" - tx_dpd, phase_diff = self._discard_small_values(tx_dpd, phase_diff) - check_input_get_next_coefs(tx_dpd, phase_diff) - - coefs_pm_new = self.fit_poly(tx_dpd, phase_diff) - - coefs_pm_new = coefs_pm + self.learning_rate_pm * (coefs_pm_new - coefs_pm) - self._plot(tx_dpd, phase_diff, coefs_pm, coefs_pm_new) - - return coefs_pm_new - -# 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/python/dpd/Model_Poly.py b/python/dpd/Model_Poly.py index c8f6135..ef3fed3 100644 --- a/python/dpd/Model_Poly.py +++ b/python/dpd/Model_Poly.py @@ -8,15 +8,13 @@ import os import logging import numpy as np +import matplotlib.pyplot as plt -import dpd.Model_AM as Model_AM -import dpd.Model_PM as Model_PM - - -def assert_np_float32(x): - assert isinstance(x, np.ndarray) - assert x.dtype == np.float32 - assert x.flags.contiguous +def assert_np_float32(array): + assert isinstance(array, np.ndarray), type(array) + assert array.dtype == np.float32, array.dtype + assert array.flags.contiguous + assert not any(np.isnan(array)) def _check_input_get_next_coefs(tx_abs, rx_abs, phase_diff): @@ -36,20 +34,73 @@ class Poly: """Calculates new coefficients using the measurement and the previous coefficients""" - def __init__(self, - c, - learning_rate_am=1.0, - learning_rate_pm=1.0): + def __init__(self, c, learning_rate_am=1.0, learning_rate_pm=1.0): self.c = c - self.plot = c.MDL_plot self.learning_rate_am = learning_rate_am self.learning_rate_pm = learning_rate_pm self.reset_coefs() - self.model_am = Model_AM.Model_AM(c, plot=self.plot) - self.model_pm = Model_PM.Model_PM(c, plot=self.plot) + def plot(self, plot_location, title): + if self._am_plot_data is not None and self._pm_plot_data is not None: + tx_dpd, rx_received, coefs_am, coefs_am_new = self._am_plot_data + + tx_range, rx_est = self._am_calc_line(coefs_am, 0, 0.6) + tx_range_new, rx_est_new = self._am_calc_line(coefs_am_new, 0, 0.6) + + sub_rows = 2 + sub_cols = 1 + fig = plt.figure(figsize=(sub_cols * 6, sub_rows / 2. * 6)) + i_sub = 0 + + # AM subplot + i_sub += 1 + ax = plt.subplot(sub_rows, sub_cols, i_sub) + ax.plot(tx_range, rx_est, + label="Estimated TX", + alpha=0.3, + color="gray") + ax.plot(tx_range_new, rx_est_new, + label="New Estimated TX", + color="red") + ax.scatter(tx_dpd, rx_received, + label="Binned Data", + color="blue", + s=1) + ax.set_title("Model AM and PM {}".format(title)) + ax.set_xlabel("TX Amplitude") + ax.set_ylabel("RX Amplitude") + ax.set_xlim(0, 1.0) + ax.legend(loc=4) + + # PM sub plot + tx_dpd, phase_diff, coefs_pm, coefs_pm_new = self._pm_plot_data + + tx_range, phase_diff_est = self._pm_calc_line(coefs_pm, 0, 0.6) + tx_range_new, phase_diff_est_new = self._pm_calc_line(coefs_pm_new, 0, 0.6) + + i_sub += 1 + ax = plt.subplot(sub_rows, sub_cols, i_sub) + ax.plot(tx_range, phase_diff_est, + label="Estimated Phase Diff", + alpha=0.3, + color="gray") + ax.plot(tx_range_new, phase_diff_est_new, + label="New Estimated Phase Diff", + color="red") + ax.scatter(tx_dpd, phase_diff, + label="Binned Data", + color="blue", + s=1) + ax.set_xlabel("TX Amplitude") + ax.set_ylabel("Phase DIff") + ax.set_xlim(0, 1.0) + ax.legend(loc=4) + + fig.tight_layout() + fig.savefig(plot_location) + plt.close(fig) def reset_coefs(self): self.coefs_am = np.zeros(5, dtype=np.float32) @@ -65,12 +116,8 @@ class Poly: """ _check_input_get_next_coefs(tx_abs, rx_abs, phase_diff) - if not lr is None: - self.model_am.learning_rate_am = lr - self.model_pm.learning_rate_pm = lr - - coefs_am_new = self.model_am.get_next_coefs(tx_abs, rx_abs, self.coefs_am) - coefs_pm_new = self.model_pm.get_next_coefs(tx_abs, phase_diff, self.coefs_pm) + coefs_am_new = self._am_get_next_coefs(tx_abs, rx_abs, self.coefs_am) + coefs_pm_new = self._pm_get_next_coefs(tx_abs, phase_diff, self.coefs_pm) self.coefs_am = self.coefs_am + (coefs_am_new - self.coefs_am) * self.learning_rate_am self.coefs_pm = self.coefs_pm + (coefs_pm_new - self.coefs_pm) * self.learning_rate_pm @@ -78,9 +125,67 @@ class Poly: def get_dpd_data(self): return "poly", self.coefs_am, self.coefs_pm + def set_dpd_data(self, dpddata): + if dpddata[0] != "poly" or len(dpddata) != 3: + raise ValueError("dpddata is not of 'poly' format") + _, self.coefs_am, self.coefs_pm = dpddata + + def _am_calc_line(self, coefs, min_amp, max_amp): + rx_range = np.linspace(min_amp, max_amp) + tx_est = np.sum(self._am_poly(rx_range) * coefs, axis=1) + return tx_est, rx_range + + def _am_poly(self, sig): + return np.array([sig ** i for i in range(1, 6)]).T + + def _am_fit_poly(self, tx_abs, rx_abs): + return np.linalg.lstsq(self._am_poly(rx_abs), tx_abs, rcond=None)[0] + + def _am_get_next_coefs(self, tx_dpd, rx_received, coefs_am): + """Calculate the next AM/AM coefficients using the extracted + statistic of TX and RX amplitude""" + + coefs_am_new = self._am_fit_poly(tx_dpd, rx_received) + coefs_am_new = coefs_am + \ + self.learning_rate_am * (coefs_am_new - coefs_am) + + self._am_plot_data = (tx_dpd, rx_received, coefs_am, coefs_am_new) + + return coefs_am_new + + def _pm_poly(self, sig): + return np.array([sig ** i for i in range(0, 5)]).T + + def _pm_calc_line(self, coefs, min_amp, max_amp): + tx_range = np.linspace(min_amp, max_amp) + phase_diff = np.sum(self._pm_poly(tx_range) * coefs, axis=1) + return tx_range, phase_diff + + def _discard_small_values(self, tx_dpd, phase_diff): + """ Assumes that the phase for small tx amplitudes is zero""" + mask = tx_dpd < self.c.MPM_tx_min + phase_diff[mask] = 0 + return tx_dpd, phase_diff + + def _pm_fit_poly(self, tx_abs, phase_diff): + return np.linalg.lstsq(self._pm_poly(tx_abs), phase_diff, rcond=None)[0] + + def _pm_get_next_coefs(self, tx_dpd, phase_diff, coefs_pm): + """Calculate the next AM/PM coefficients using the extracted + statistic of TX amplitude and phase difference""" + tx_dpd, phase_diff = self._discard_small_values(tx_dpd, phase_diff) + + coefs_pm_new = self._pm_fit_poly(tx_dpd, phase_diff) + + coefs_pm_new = coefs_pm + self.learning_rate_pm * (coefs_pm_new - coefs_pm) + self._pm_plot_data = (tx_dpd, phase_diff, coefs_pm, coefs_pm_new) + + return coefs_pm_new + # The MIT License (MIT) # # Copyright (c) 2017 Andreas Steger +# Copyright (c) 2018 Matthias P. Brandli # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/python/dpd/RX_Agc.py b/python/dpd/RX_Agc.py index 48ef7f3..bb940be 100644 --- a/python/dpd/RX_Agc.py +++ b/python/dpd/RX_Agc.py @@ -19,19 +19,19 @@ import dpd.Adapt as Adapt import dpd.Measure as Measure class Agc: - """The goal of the automatic gain control is to set the - RX gain to a value at which all received amplitudes can - be detected. This means that the maximum possible amplitude + """The goal of the automatic gain control is to set the + RX gain to a value at which all received amplitudes can + be detected. This means that the maximum possible amplitude should be quantized at the highest possible digital value. - A problem we have to face, is that the estimation of the - maximum amplitude by applying the max() function is very - unstable. This is due to the maximum’s rareness. Therefore - we estimate a far more robust value, such as the median, + A problem we have to face, is that the estimation of the + maximum amplitude by applying the max() function is very + unstable. This is due to the maximum’s rareness. Therefore + we estimate a far more robust value, such as the median, and then approximate the maximum amplitude from it. - Given this, we tune the RX gain in such a way, that the - received signal fulfills our desired property, of having + Given this, we tune the RX gain in such a way, that the + received signal fulfills our desired property, of having all amplitudes properly quantized.""" def __init__(self, measure, adapt, c): @@ -45,10 +45,15 @@ class Agc: self.peak_to_median = 1./c.RAGC_rx_median_target def run(self) -> Tuple[bool, str]: - self.adapt.set_rxgain(self.rxgain) + try: + self.adapt.set_rxgain(self.rxgain) + except ValueError as e: + return (False, "Setting RX gain to {} failed: {}".format(self.rxgain, e)) + time.sleep(0.5) + # Measure - txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median = self.measure.get_samples() + txframe, tx_ts, rxframe, rx_ts, rx_median, tx_median = self.measure.get_samples_unaligned(short=False) # Estimate Maximum rx_peak = self.peak_to_median * rx_median @@ -68,11 +73,19 @@ class Agc: w = "Warning: calculated RX Gain={} is higher than maximum={}. RX feedback power should be increased.".format( self.rxgain, self.max_rxgain) logging.warning(w) + try: + # Reset to a low value, as we expect the user to reduce external attenuation + self.adapt.set_rxgain(30) + except ValueError as e: + return (False, "\n".join([measurements, w, "Setting RX gain to {} failed: {}".format(self.rxgain, e)])) return (False, "\n".join([measurements, w])) else: - self.adapt.set_rxgain(self.rxgain) + try: + self.adapt.set_rxgain(self.rxgain) + except ValueError as e: + return (False, "Setting RX gain to {} failed: {}".format(self.rxgain, e)) time.sleep(0.5) - return (True, measurements) + return (True, measurements) def plot_estimates(self): """Plots the estimate of for Max, Median, Mean for different |