From f3e02080d8e95c332198ace518d98c8d9e9d7009 Mon Sep 17 00:00:00 2001
From: Sepehr Madani <ssepehrmadani@gmail.com>
Date: Mon, 3 Aug 2020 00:07:19 -0400
Subject: [PATCH] Fix QSlider and QLineEdit functionallities

Now, the QWidgets in each control layout will behave as expected.
QSliders now move on bit_count ticks, as per genes in GeneticAlgorithm.
Algorithm re-run button now also functions properly.
---
 utils/visualizerUI.py | 257 ++++++++++++++++++++++--------------------
 1 file changed, 132 insertions(+), 125 deletions(-)

diff --git a/utils/visualizerUI.py b/utils/visualizerUI.py
index e7c4340..2b87695 100644
--- a/utils/visualizerUI.py
+++ b/utils/visualizerUI.py
@@ -1,68 +1,70 @@
-from cmath import exp
-from math import log10, pi
+from cmath import phase
+from math import log10, pi, sin, cos
 
-from PySide2.QtCore import QCoreApplication, QMetaObject, QRect, QSize, Qt
+from PySide2.QtCore import QSize, Qt
 from PySide2.QtWidgets import *
 from PySide2.QtGui import QKeySequence
-from matplotlib.backends.backend_qt5agg import FigureCanvas, NavigationToolbar2QT
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT
 from matplotlib.figure import Figure
+from matplotlib import style as ChartStyle
 
 from utils.pattern import compute_pattern, range_in_deg
 
 
-class Parameter:
-    def __init__(self, name, value, min=0.0, max=1.0):
-        self.name = name
-        self.value = value
-        self.min = min
-        self.max = max
-
-    def scaleTo100Ratio(self) -> int:
-        return int(100 * ((self.value - self.min) / (self.max - self.min)))
-
-    def scaleFrom100Ratio(self, ratio_value) -> float:
-        return self.min + (ratio_value / 100) * (self.max - self.min)
+# Define Size Policies
+prefSizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
+fixedSizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
+expandingSizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
 
 
 class NullingSlider(QSlider):
-    def __init__(self, orientation, parent, default_value):
+    def __init__(self, orientation, parent, value):
         super().__init__(orientation=orientation, parent=parent)
         self.parent = parent
-        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
+        self.setSizePolicy(expandingSizePolicy)
         self.setMinimumSize(QSize(100, 0))
-        self.setRange(0, 100)
+        self.setRange(0, 2**self.parent.options.bit_count - 1)
         self.setTickPosition(QSlider.TicksBelow)
-        self.setTickInterval(5)
-        self.connect_signal()
-        self.update_value(default_value)
-
-    def connect_signal(self):
+        self.setTickInterval(1)
+        self.update_value(value)
         self.valueChanged.connect(lambda: self.parent.update_parameters(source=QSlider))
 
-    def update_value(self, new_value):
-        self.valueChanged.disconnect()
+    def normal_value(self):
+        bit_c = self.parent.options.bit_count
+        bit_res = self.parent.options.bit_resolution
+        angle = (self.value() - (2**bit_c-1)/2) * (2*pi) / (2**bit_res)
+        weight = complex(cos(angle), sin(angle))
+        return weight
+
+    def update_value(self, weight):
+        bit_c = self.parent.options.bit_count
+        bit_res = self.parent.options.bit_resolution
+        angle = phase(weight)  # (2*pi * value) - pi
+        new_value = int((angle * 2**bit_res / (2*pi)) + 0.5*(2**bit_c-1))
+        self.blockSignals(True)
         self.setValue(new_value)
-        self.connect_signal()
+        self.blockSignals(False)
 
 
 class NullingLineEdit(QLineEdit):
-    def __init__(self, parent, default_value):
+    def __init__(self, parent, value):
         super().__init__(parent=parent)
         self.parent = parent
         self.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred))
-        self.setFixedWidth(64)
-        self.connect_signal()
-        self.update_value(default_value)
-
-    def connect_signal(self):
-        self.textChanged.connect(
+        self.setFixedWidth(96)
+        self.update_value(value)
+        self.editingFinished.connect(
             lambda: self.parent.update_parameters(source=QLineEdit)
         )
 
-    def update_value(self, new_value):
-        self.textChanged.disconnect()
-        self.setText(str(new_value))
-        self.connect_signal()
+    def normal_value(self):
+        angle = float(self.text())
+        weight = complex(cos(angle), sin(angle))
+        return weight
+
+    def update_value(self, value):
+        self.setText(str(phase(value)))
+        self.home(False)
 
 
 class NullingLabel(QLabel):
@@ -70,20 +72,24 @@ class NullingLabel(QLabel):
         super().__init__(parent=parent)
         self.setText(str(label))
         self.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred))
-        self.setFixedWidth(32)
+        self.setFixedWidth(24)
 
 
 class MainWindow(QMainWindow):
-    def __init__(self, param_builder):
+    def __init__(self, algorithm, options):
         super().__init__()
 
-        self.centralWidget = QWidget()
+        self.algorithm = algorithm
+        self.options = options
+        self.final_weights = self.algorithm.solve()[0]
+        # self.theta_weights = [phase(x) for x in self.final_weights]
+        # self.normal_weights = [(angle + pi) / (2*pi) for angle in self.theta_weights]
+
+        self.centralWidget = QWidget(self)
         self.setCentralWidget(self.centralWidget)
-        self.setWindowTitle("Chart Display")
+        self.setWindowTitle('Chart Display')
         self.resize(1280, 720)
 
-        self.paramBuilder = param_builder
-        self.parameterSet = self.paramBuilder.build_params()
         self.create_actions()
         self.create_menus()
         self.create_status_bar()
@@ -94,42 +100,42 @@ class MainWindow(QMainWindow):
 
     def create_actions(self):
         self.refreshAct = QAction(
-            "&Refresh chart",
-            self,
+            parent=self,
+            text='&Refresh chart',
             shortcut=QKeySequence.Refresh,
-            statusTip="Updates the plot on the right",
+            statusTip='Updates the plot on the right',
             triggered=self.update_chart,
         )
 
         self.useLog10ForChartAct = QAction(
-            "Use &Log10 for Chart",
-            self,
-            shortcut="Ctrl+L",
-            statusTip="If checked, scales the y-axis of chart using log10",
+            parent=self,
+            text='Use &Log10 for Chart',
+            shortcut='Ctrl+L',
+            statusTip='If checked, scales the y-axis of chart using log10',
             triggered=self.update_chart,
             checkable=True,
         )
         self.useLog10ForChartAct.setChecked(True)
 
         self.aboutAct = QAction(
-            "&About",
-            self,
+            parent=self,
+            text='&About',
             shortcut=QKeySequence.HelpContents,
-            statusTip="Displays info about this software",
+            statusTip='Displays info about this software',
             triggered=self.about,
         )
 
         self.aboutQtAct = QAction(
-            "About &Qt",
-            self,
-            shortcut="Ctrl+F1",
-            statusTip="Show the Qt library's About box",
+            parent=self,
+            text='About &Qt',
+            shortcut='Ctrl+F1',
+            statusTip='Show the Qt library\'s About box',
             triggered=self.aboutQt,
         )
 
     def create_menus(self):
-        self.fileMenu = self.menuBar().addMenu("&Chart")
-        self.helpMenu = self.menuBar().addMenu("&Help")
+        self.fileMenu = self.menuBar().addMenu('&Chart')
+        self.helpMenu = self.menuBar().addMenu('&Help')
 
         self.fileMenu.addAction(self.refreshAct)
         self.fileMenu.addAction(self.useLog10ForChartAct)
@@ -137,45 +143,53 @@ class MainWindow(QMainWindow):
         self.helpMenu.addAction(self.aboutQtAct)
 
     def create_status_bar(self):
-        self.statusBar().showMessage("Ready")
+        self.statusBar().showMessage('Ready')
 
     def create_control_box(self):
-        self.parameterControlList = []
-        self.labelList = []
-        self.sliderList = []
-        self.lineEditList = []
-        self.toolboxLayout = QVBoxLayout()
-
-        for parameter in self.parameterSet:
-            # Create widgets for each parameter
-            self.labelList.append(NullingLabel(parent=self, label=parameter.name))
-            self.lineEditList.append(NullingLineEdit(self, parameter.value))
-            self.sliderList.append(NullingSlider(Qt.Horizontal, self, parameter.scaleTo100Ratio()))
-
-            # Bundle layout for label, line edit, and slider
-            self.parameterControlList.append(QHBoxLayout())
-            self.parameterControlList[-1].addWidget(self.labelList[-1])
-            self.parameterControlList[-1].addWidget(self.lineEditList[-1])
-            self.parameterControlList[-1].addWidget(self.sliderList[-1])
-
-            self.toolboxLayout.addLayout(self.parameterControlList[-1])
-
-        self.rerunButton = QPushButton("Re-run")
+        self.toolbox_layout = QVBoxLayout()
+        
+        # Create widgets for each parameter
+        self.weight_control_list = [
+            QHBoxLayout()
+            for _ in range(len(self.final_weights))
+        ] 
+        self.weight_label_list = [
+            NullingLabel(parent=self, label=f'a{n + 1}')
+            for n in range(len(self.final_weights))
+        ]
+        self.weight_editor_list = [
+            NullingLineEdit(parent=self, value=z)
+            for z in self.final_weights
+        ]
+        self.weight_slider_list = [
+            NullingSlider(orientation=Qt.Horizontal, parent=self, value=z)
+            for z in self.final_weights
+        ]
+
+        for ii in range(len(self.final_weights)):
+            self.weight_control_list[ii].addWidget(self.weight_label_list[ii])
+            self.weight_control_list[ii].addWidget(self.weight_editor_list[ii])
+            self.weight_control_list[ii].addWidget(self.weight_slider_list[ii])
+
+            self.toolbox_layout.addLayout(self.weight_control_list[ii])
+
+        self.rerunButton = QPushButton('Re-run')
         self.rerunButton.clicked.connect(self.call_algorithm)
-        self.toolboxLayout.addWidget(self.rerunButton)
+        self.toolbox_layout.addWidget(self.rerunButton)
 
         self.toolboxSpacer = QSpacerItem(
             20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding
         )
-        self.toolboxLayout.addItem(self.toolboxSpacer)
+        self.toolbox_layout.addItem(self.toolboxSpacer)
 
     def create_plot_box(self):
         self.plotWidget = QWidget(self.centralWidget)
-        self.plotWidget.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
+        self.plotWidget.setSizePolicy(expandingSizePolicy)
         self.plotWidget.setMinimumSize(QSize(720, 480))
         self.plotLayout = QVBoxLayout(self.plotWidget)
 
-        self.canvas = FigureCanvas(Figure(figsize=(5, 3)))
+        ChartStyle.use("ggplot")
+        self.canvas = FigureCanvasQTAgg(Figure(figsize=(5, 3)))
         self.addToolBar(NavigationToolbar2QT(self.canvas, self))
         self.chart = self.canvas.figure.subplots()
         self.canvas.figure.set_tight_layout(True)
@@ -183,86 +197,79 @@ class MainWindow(QMainWindow):
 
     def set_up_central_widget(self):
         self.centralLayout = QHBoxLayout(self.centralWidget)
-        self.centralLayout.addLayout(self.toolboxLayout)
+        self.centralLayout.addLayout(self.toolbox_layout)
         self.centralLayout.addWidget(self.plotWidget)
         self.centralLayout.setStretch(0, 2)
         self.centralLayout.setStretch(1, 5)
         self.centralWidget.setLayout(self.centralLayout)
 
     def update_parameters(self, source=None):
-        for slider, lineEdit, parameter in zip(
-            self.sliderList, self.lineEditList, self.parameterSet
+        for slider, editor, ii in zip(
+            self.weight_slider_list, self.weight_editor_list, range(len(self.final_weights))
         ):
             if source == QSlider:
-                newValue = parameter.scaleFrom100Ratio(slider.value())
-                parameter.value = newValue
-                lineEdit.update_value(newValue)
+                newValue = slider.normal_value()
 
             if source == QLineEdit:
                 try:
-                    parameter.value = float(lineEdit.text())
-                    slider.update_value(parameter.scaleTo100Ratio())
+                    newValue = editor.normal_value()
                 except ValueError:
-                    pass
+                    newValue = self.final_weights[ii]
 
             if source == None:
-                slider.update_value(parameter.scaleTo100Ratio())
-                lineEdit.update_value(round(parameter.value, 4))
+                newValue = self.final_weights[ii]
+
+            self.final_weights[ii] = newValue
+            slider.update_value(newValue)
+            editor.update_value(newValue)
 
         self.update_chart()
 
     def update_chart(self):
-        self.statusBar().showMessage("Updating the chart...")
+        self.statusBar().showMessage('Updating the chart...')
         self.chart.clear()
         self.chart.set_xticks([10 * x for x in range(19)])
-        self.chart.set_xlabel("Degrees (°)")
-        self.chart.grid(True, linestyle="--")
+        self.chart.set_yticks([10 * y for y in range(-7, 3)])
+        self.chart.set_xlabel('Degrees (°)')
+        self.chart.grid(True, linestyle='--')
 
-        customWeights = [exp(2 * pi * 1j * x.value) for x in self.parameterSet] + (
-            [1] * (16 - len(self.parameterSet))
-        )
-        data_x = compute_pattern(weights=customWeights)
+        data_x = compute_pattern(weights=self.final_weights)
 
         for elem in data_x:
             if elem < 0:
-                raise ValueError("negative values in list 'data_x'")
+                raise ValueError('negative values in list "data_x"')
 
         if self.useLog10ForChartAct.isChecked():
-            data_x = list(map(lambda x: 10 * log10(x), data_x))
-            self.chart.set_ylim(-30, 10 * log10(16) + 1)
-            self.chart.set_ylabel("dB (scaled with log10)")
+            data_x = list(map(lambda x: 20 * log10(x), data_x))
+            self.chart.set_ylim(-70, 20 * log10(16) + 1)
+            self.chart.set_ylabel('dB (scaled with log10)')
         else:
             self.chart.set_ylim(0, 18)
-            self.chart.set_ylabel("dB")
+            self.chart.set_ylabel('dB')
 
         data_y = range_in_deg(0.1)
 
         if len(data_x) != len(data_y):
-            raise ValueError("resolution doesn't match with data_x's length")
+            raise ValueError('resolution doesn\'t match with data_x\'s length')
 
         self.chart.plot(data_y, data_x)
         self.chart.figure.canvas.draw()
-        self.statusBar().showMessage("Ready")
+        self.statusBar().showMessage('Ready')
 
     def call_algorithm(self):
-        A = [x.value for x in self.parameterSet]
-        self.parameterSet = self.paramBuilder.build_params()
-        B = [x.value for x in self.parameterSet]
-        diff = [int(100 * (a - b)) for a, b in zip(A, B)]
-        if sum(list(map(abs, diff))) > 0:
-            print(diff)
-            self.update_parameters()
+        self.final_weights = self.algorithm.solve()[0]
+        self.update_parameters()
 
     def about(self):
         QMessageBox.about(
             self,
-            "About Nulling-Python",
-            "<b>Nulling-Python</b> is a small tool for analyzing and testing"
-            " algorithms for nulling systems of mmWave WLANs. <br/>"
-            " It is developed by Sepehr and Sohrab Madani and available on"
-            "<a href='https://gitlab.engr.illinois.edu/smadani2/nulling-python'>"
-            " UIUC Engineering Department Gitlab</a>.",
+            'About Nulling-Python',
+            '<b>Nulling-Python</b> is a small tool for analyzing and testing'
+            ' algorithms for nulling systems of mmWave WLANs. <br/>'
+            ' It is developed by Sepehr and Sohrab Madani and available on'
+            '<a href="https://gitlab.engr.illinois.edu/smadani2/nulling-python">'
+            ' UIUC Engineering Department Gitlab</a>.',
         )
 
     def aboutQt(self):
-        QMessageBox.aboutQt(self, "About Qt")
+        QMessageBox.aboutQt(self, 'About Qt')
-- 
GitLab