Skip to content

Commit

Permalink
proper support for complex16u (2xuint8) signals (#772)
Browse files Browse the repository at this point in the history
* proper support for complex16 (2xint8) signals
  * fix Modulator: give correct iq type
  * fix View: Use general min/max and not from signal
  * Remove Padding

* fix requirements.txt (skip PyQt5.14.2 on Windows)

* fix noise area drawing

* fix demod view range for FSK and PSK

* Autofit signal in ModulatorDialog.py

* optimize Unittests
  • Loading branch information
jopohl authored May 1, 2020
1 parent 733371f commit 889dd05
Show file tree
Hide file tree
Showing 33 changed files with 96 additions and 143 deletions.
2 changes: 2 additions & 0 deletions data/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ jobs:
- script: |
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8d748e26ccc9afc8ea0d0201ae234fda35de721e/Formula/boost.rb
brew reinstall https://raw.githubusercontent.com/Homebrew/homebrew-core/a806a621ed3722fb580a58000fb274a2f2d86a6d/Formula/icu4c.rb
brew link icu4c --force
cd /tmp
wget https://github.com/libusb/libusb/releases/download/v1.0.22/libusb-1.0.22.tar.bz2
tar xf libusb-1.0.22.tar.bz2
Expand Down
3 changes: 2 additions & 1 deletion data/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
numpy>=1.9; sys_platform != 'win32'
numpy>=1.9,!=1.16.0; sys_platform == 'win32'
pyqt5
pyqt5; sys_platform != 'win32'
pyqt5!=5.14.2; sys_platform == 'win32'
psutil
cython
19 changes: 4 additions & 15 deletions src/urh/controller/dialogs/ModulatorDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,6 @@ def __init__(self, modulators, tree_model=None, parent=None):

self.modulators = modulators

for graphic_view in (self.ui.gVCarrier, self.ui.gVData):
graphic_view.scene_y_min = -1
graphic_view.scene_y_max = 1
graphic_view.scene_x_zoom_stretch = 1.1

min_max_mod = IQArray.min_max_for_dtype(self.current_modulator.get_dtype())
self.ui.gVModulated.scene_y_min = min_max_mod[0]
self.ui.gVModulated.scene_y_max = min_max_mod[1]
self.ui.gVModulated.scene_x_zoom_stretch = 1.1

self.set_ui_for_current_modulator()

self.ui.cbShowDataBitsOnly.setText(self.tr("Show Only Data Sequence\n"))
Expand Down Expand Up @@ -132,8 +122,7 @@ def closeEvent(self, event: QCloseEvent):

@property
def current_modulator(self):
mod = self.modulators[self.ui.comboBoxCustomModulations.currentIndex()]
return mod
return self.modulators[self.ui.comboBoxCustomModulations.currentIndex()]

def set_ui_for_current_modulator(self):
index = self.ui.comboBoxModulationType.findText("*(" + self.current_modulator.modulation_type + ")",
Expand Down Expand Up @@ -188,7 +177,7 @@ def draw_data_bits(self):
self.ui.gVData.update()

def draw_modulated(self):
self.ui.gVModulated.plot_data(self.current_modulator.modulate(pause=0).imag.astype(numpy.float32))
self.ui.gVModulated.plot_data(self.current_modulator.modulate(pause=0).imag)
if self.lock_samples_in_view:
siv = self.ui.gVOriginalSignal.view_rect().width()
self.adjust_samples_in_view(siv)
Expand All @@ -203,8 +192,8 @@ def draw_original_signal(self, start=0, end=-1):
if end == -1:
end = scene_manager.signal.num_samples

y = scene_manager.scene.sceneRect().y()
h = scene_manager.scene.sceneRect().height()
y = self.ui.gVOriginalSignal.view_rect().y()
h = self.ui.gVOriginalSignal.view_rect().height()
self.ui.gVOriginalSignal.setSceneRect(start, y, end - start, h)
self.ui.gVOriginalSignal.fitInView(self.ui.gVOriginalSignal.sceneRect())
scene_manager.show_scene_section(start, end)
Expand Down
2 changes: 2 additions & 0 deletions src/urh/controller/widgets/SignalFrame.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,7 @@ def on_cb_signal_view_index_changed(self):
else:
self.ui.stackedWidget.setCurrentWidget(self.ui.pageSignal)
self.ui.gvSignal.scene_type = self.ui.cbSignalView.currentIndex()
self.scene_manager.mod_type = self.signal.modulation_type
self.ui.gvSignal.redraw_view(reinitialize=True)
self.ui.labelRSSI.show()

Expand Down Expand Up @@ -1062,6 +1063,7 @@ def on_combobox_modulation_type_text_changed(self, txt: str):

self.undo_stack.push(modulation_action)

self.scene_manager.mod_type = txt
if self.ui.cbSignalView.currentIndex() == 1:
self.scene_manager.init_scene()
self.on_slider_y_scale_value_changed()
Expand Down
29 changes: 27 additions & 2 deletions src/urh/cythonext/signal_functions.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ cdef get_numpy_dtype(iq cython_type):
raise ValueError("dtype {} not supported for modulation".format(cython.typeof(cython_type)))

cpdef modulate_c(uint8_t[:] bits, uint32_t samples_per_symbol, str modulation_type,
float[:] parameters, uint16_t bits_per_symbol,
float carrier_amplitude, float carrier_frequency, float carrier_phase, float sample_rate,
uint32_t pause, uint32_t start, dtype=np.float32,
float gauss_bt=0.5, float filter_width=1.0):

if dtype == np.int8:
return __modulate(
bits, samples_per_symbol, modulation_type, parameters, bits_per_symbol, carrier_amplitude,
carrier_frequency, carrier_phase, sample_rate, pause, start, <char>0, gauss_bt, filter_width
)
elif dtype == np.int16:
return __modulate(
bits, samples_per_symbol, modulation_type, parameters, bits_per_symbol, carrier_amplitude,
carrier_frequency, carrier_phase, sample_rate, pause, start, <short>0, gauss_bt, filter_width
)
elif dtype == np.float32:
return __modulate(
bits, samples_per_symbol, modulation_type, parameters, bits_per_symbol, carrier_amplitude,
carrier_frequency, carrier_phase, sample_rate, pause, start, <float>0.0, gauss_bt, filter_width
)
else:
raise ValueError("Unsupported dtype for modulation {}".format(dtype))


cpdef __modulate(uint8_t[:] bits, uint32_t samples_per_symbol, str modulation_type,
float[:] parameters, uint16_t bits_per_symbol,
float carrier_amplitude, float carrier_frequency, float carrier_phase, float sample_rate,
uint32_t pause, uint32_t start, iq iq_type,
Expand Down Expand Up @@ -316,8 +341,8 @@ cpdef np.ndarray[np.float32_t, ndim=1] afp_demod(IQ samples, float noise_mag, st
else:
raise ValueError("Unsupported dtype")

# Atan2 liefert Werte im Bereich von -Pi bis Pi
# Wir nutzen die Magic Constant NOISE_FSK_PSK um Rauschen abzuschneiden
# Atan2 yields values from -Pi to Pi
# We use the Magic Constant NOISE_FSK_PSK to cut off noise
noise_sqrd = noise_mag * noise_mag
NOISE = get_noise_for_mod_type(mod_type)
result[0] = NOISE
Expand Down
2 changes: 1 addition & 1 deletion src/urh/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def get_qt_settings_filename():
SELECTION_COLOR = QColor("darkblue") # overwritten by system color (bin/urh)
NOISE_COLOR = QColor("red")
SELECTION_OPACITY = 1
NOISE_OPACITY = 0.4
NOISE_OPACITY = 0.33

# SEPARATION COLORS
ONES_AREA_COLOR = Qt.darkGreen
Expand Down
4 changes: 2 additions & 2 deletions src/urh/signalprocessing/IQArray.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,13 @@ def convert_to(self, target_dtype) -> np.ndarray:
def from_file(filename: str):
if filename.endswith(".complex16u") or filename.endswith(".cu8"):
# two 8 bit unsigned integers
return IQArray(data=np.fromfile(filename, dtype=np.uint8))
return IQArray(IQArray(data=np.fromfile(filename, dtype=np.uint8)).convert_to(np.int8))
elif filename.endswith(".complex16s") or filename.endswith(".cs8"):
# two 8 bit signed integers
return IQArray(data=np.fromfile(filename, dtype=np.int8))
elif filename.endswith(".complex32u") or filename.endswith(".cu16"):
# two 16 bit unsigned integers
return IQArray(data=np.fromfile(filename, dtype=np.uint16))
return IQArray(IQArray(data=np.fromfile(filename, dtype=np.uint16)).convert_to(np.int16))
elif filename.endswith(".complex32s") or filename.endswith(".cs16"):
# two 16 bit signed integers
return IQArray(data=np.fromfile(filename, dtype=np.int16))
Expand Down
5 changes: 1 addition & 4 deletions src/urh/signalprocessing/Modulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,6 @@ def modulate(self, data=None, pause=0, start=0) -> IQArray:
dtype = self.get_dtype()
a = self.carrier_amplitude * IQArray.min_max_for_dtype(dtype)[1]

type_code = "b" if dtype == np.int8 else "h" if dtype == np.int16 else "f"
type_val = array.array(type_code, [0])[0]

parameters = self.parameters
if self.modulation_type == "ASK":
parameters = array.array("f", [a*p/100 for p in parameters])
Expand All @@ -233,7 +230,7 @@ def modulate(self, data=None, pause=0, start=0) -> IQArray:
self.modulation_type, parameters, self.bits_per_symbol,
a, self.carrier_freq_hz,
self.carrier_phase_deg * (np.pi / 180),
self.sample_rate, pause, start, type_val,
self.sample_rate, pause, start, dtype,
self.gauss_bt, self.gauss_filter_width)
return IQArray(result)

Expand Down
23 changes: 19 additions & 4 deletions src/urh/signalprocessing/Signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def __init__(self, filename: str, name="Signal", modulation: str = None, sample_
self.noise_max_plot = 0
self.block_protocol_update = False

self.iq_array = IQArray(None, np.int8, 1)

self.wav_mode = filename.endswith(".wav")
self.__changed = False
if modulation is None:
Expand Down Expand Up @@ -281,19 +283,32 @@ def noise_threshold(self, value):
self._qad = None
self.clear_parameter_cache()
self._noise_threshold = value
self.noise_min_plot = -value
self.noise_max_plot = value

middle = 0.5*sum(IQArray.min_max_for_dtype(self.iq_array.dtype))
a = self.max_amplitude * value / self.max_magnitude
self.noise_min_plot = middle - a
self.noise_max_plot = middle + a
self.noise_threshold_changed.emit()
if not self.block_protocol_update:
self.protocol_needs_update.emit()

@property
def max_magnitude(self):
mi, ma = IQArray.min_max_for_dtype(self.iq_array.dtype)
return (2 * max(mi**2, ma**2))**0.5

@property
def max_amplitude(self):
mi, ma = IQArray.min_max_for_dtype(self.iq_array.dtype)
return 0.5 * (ma - mi)

@property
def noise_threshold_relative(self):
return self.noise_threshold / (self.iq_array.maximum**2.0 + self.iq_array.minimum**2.0)**0.5
return self.noise_threshold / self.max_magnitude

@noise_threshold_relative.setter
def noise_threshold_relative(self, value: float):
self.noise_threshold = value * (self.iq_array.maximum**2.0 + self.iq_array.minimum**2.0)**0.5
self.noise_threshold = value * self.max_magnitude

@property
def qad(self):
Expand Down
2 changes: 0 additions & 2 deletions src/urh/ui/painting/ContinuousSceneManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ def __init__(self, ring_buffer: RingBuffer, parent):
self.__start = 0
self.__end = 0

self.minimum, self.maximum = IQArray.min_max_for_dtype(self.ring_buffer.dtype)

@property
def plot_data(self):
return self.ring_buffer.view_data.real
Expand Down
2 changes: 0 additions & 2 deletions src/urh/ui/painting/LiveSceneManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ def __init__(self, data_array, parent):
self.plot_data = data_array
self.end = 0

self.minimum, self.maximum = IQArray.min_max_for_dtype(data_array.dtype)

@property
def num_samples(self):
return self.end
22 changes: 3 additions & 19 deletions src/urh/ui/painting/SceneManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QPen, QColor
from PyQt5.QtWidgets import QGraphicsPathItem
from urh.signalprocessing.IQArray import IQArray

from urh import settings
from urh.cythonext import path_creator, util
Expand All @@ -16,10 +17,6 @@ def __init__(self, parent):
self.scene = ZoomableScene()
self.__plot_data = None # type: np.ndarray
self.line_item = self.scene.addLine(0, 0, 0, 0, QPen(settings.AXISCOLOR, 0))
self.minimum = float("nan") # NaN = AutoDetect
self.maximum = float("nan") # NaN = AutoDetect

self.padding = 1.25

@property
def plot_data(self):
Expand Down Expand Up @@ -66,24 +63,11 @@ def __limit_value(self, val: float) -> int:
def show_full_scene(self):
self.show_scene_section(0, self.num_samples)

def init_scene(self, apply_padding=True):
def init_scene(self):
if self.num_samples == 0:
return

if math.isnan(self.minimum) or math.isnan(self.maximum):
minimum, maximum = util.minmax(self.plot_data)
else:
minimum, maximum = self.minimum, self.maximum

padding = self.padding if apply_padding else 1

if abs(minimum) > abs(maximum):
minimum = -padding * abs(minimum)
maximum = -padding * minimum
else:
maximum = padding * abs(maximum)
minimum = -padding * maximum

minimum, maximum = IQArray.min_max_for_dtype(self.plot_data.dtype)
self.scene.setSceneRect(0, minimum, self.num_samples, maximum - minimum)
self.scene.setBackgroundBrush(settings.BGCOLOR)

Expand Down
7 changes: 6 additions & 1 deletion src/urh/ui/painting/SignalSceneManager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import math

from urh.signalprocessing.Signal import Signal
from urh.ui.painting.SceneManager import SceneManager

Expand All @@ -7,6 +9,7 @@ def __init__(self, signal: Signal, parent):
super().__init__(parent)
self.signal = signal
self.scene_type = 0 # 0 = Analog Signal, 1 = QuadDemodView
self.mod_type = "ASK"

def show_scene_section(self, x1: float, x2: float, subpath_ranges=None, colors=None):
self.plot_data = self.signal.real_plot_data if self.scene_type == 0 else self.signal.qad
Expand All @@ -19,7 +22,9 @@ def init_scene(self):
else:
self.plot_data = self.signal.qad

super().init_scene(apply_padding=self.scene_type == 0)
super().init_scene()
if self.scene_type == 1 and (self.mod_type == "FSK" or self.mod_type == "PSK"):
self.scene.setSceneRect(0, -4, self.num_samples, 8)

self.line_item.setLine(0, 0, 0, 0) # Hide Axis

Expand Down
2 changes: 0 additions & 2 deletions src/urh/ui/painting/SniffSceneManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ def __init__(self, data_array, parent, window_length=5 * 10**6):
self.__end = 0
self.window_length = window_length

self.minimum, self.maximum = IQArray.min_max_for_dtype(data_array.dtype)

@property
def plot_data(self):
return self.data_array[self.__start:self.end]
Expand Down
2 changes: 1 addition & 1 deletion src/urh/ui/painting/SpectrogramSceneManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def show_full_scene(self):
# after we know how wide the spectrogram actually is
self.scene.setSceneRect(0, 0, x_pos, self.spectrogram.freq_bins)

def init_scene(self, apply_padding=True):
def init_scene(self):
pass

def eliminate(self):
Expand Down
32 changes: 0 additions & 32 deletions src/urh/ui/views/LegendGraphicView.py

This file was deleted.

15 changes: 12 additions & 3 deletions src/urh/ui/views/ZoomAndDropableGraphicView.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import QDragEnterEvent, QDropEvent
from urh.signalprocessing.IQArray import IQArray

from urh.cythonext import util

from urh.signalprocessing.ProtocolAnalyzer import ProtocolAnalyzer
from urh.signalprocessing.Signal import Signal
Expand Down Expand Up @@ -44,9 +47,6 @@ def dropEvent(self, event: QDropEvent):
if signal is None:
return

self.draw_signal(signal, proto_analyzer)

def draw_signal(self, signal, proto_analyzer):
if signal is None:
return

Expand All @@ -60,6 +60,15 @@ def draw_signal(self, signal, proto_analyzer):

self.signal_loaded.emit(self.proto_analyzer)

def auto_fit_view(self):
super().auto_fit_view()

plot_min, plot_max = util.minmax(self.signal.real_plot_data)
data_min, data_max = IQArray.min_max_for_dtype(self.signal.real_plot_data.dtype)
self.scale(1, (data_max - data_min) / (plot_max-plot_min))

self.centerOn(self.view_rect().x() + self.view_rect().width() / 2, self.y_center)

def eliminate(self):
# Do _not_ call eliminate() for self.signal and self.proto_analyzer
# as these are references to the original data!
Expand Down
Loading

0 comments on commit 889dd05

Please sign in to comment.