import os
import numpy as np
from qtpy.QtCore import Qt
from qtpy.QtGui import QFont, QIcon, QPageLayout, QPen, QPixmap
from qtpy.QtPrintSupport import QPrintDialog, QPrinter
from qtpy.QtWidgets import (
QFrame,
QHBoxLayout,
QLabel,
QMainWindow,
QToolBar,
QToolButton,
QWidget,
)
from qwt import (
QwtLegend,
QwtLogScaleEngine,
QwtPlot,
QwtPlotCurve,
QwtPlotGrid,
QwtPlotMarker,
QwtPlotRenderer,
QwtSymbol,
QwtText,
)
from qwt.tests import utils
print_xpm = [
"32 32 12 1",
"a c #ffffff",
"h c #ffff00",
"c c #ffffff",
"f c #dcdcdc",
"b c #c0c0c0",
"j c #a0a0a4",
"e c #808080",
"g c #808000",
"d c #585858",
"i c #00ff00",
"# c #000000",
". c None",
"................................",
"................................",
"...........###..................",
"..........#abb###...............",
".........#aabbbbb###............",
".........#ddaaabbbbb###.........",
"........#ddddddaaabbbbb###......",
".......#deffddddddaaabbbbb###...",
"......#deaaabbbddddddaaabbbbb###",
".....#deaaaaaaabbbddddddaaabbbb#",
"....#deaaabbbaaaa#ddedddfggaaad#",
"...#deaaaaaaaaaa#ddeeeeafgggfdd#",
"..#deaaabbbaaaa#ddeeeeabbbbgfdd#",
".#deeefaaaaaaa#ddeeeeabbhhbbadd#",
"#aabbbeeefaaa#ddeeeeabbbbbbaddd#",
"#bbaaabbbeee#ddeeeeabbiibbadddd#",
"#bbbbbaaabbbeeeeeeabbbbbbaddddd#",
"#bjbbbbbbaaabbbbeabbbbbbadddddd#",
"#bjjjjbbbbbbaaaeabbbbbbaddddddd#",
"#bjaaajjjbbbbbbaaabbbbadddddddd#",
"#bbbbbaaajjjbbbbbbaaaaddddddddd#",
"#bjbbbbbbaaajjjbbbbbbddddddddd#.",
"#bjjjjbbbbbbaaajjjbbbdddddddd#..",
"#bjaaajjjbbbbbbjaajjbddddddd#...",
"#bbbbbaaajjjbbbjbbaabdddddd#....",
"###bbbbbbaaajjjjbbbbbddddd#.....",
"...###bbbbbbaaajbbbbbdddd#......",
"......###bbbbbbjbbbbbddd#.......",
".........###bbbbbbbbbdd#........",
"............###bbbbbbd#.........",
"...............###bbb#..........",
"..................###...........",
]
class BodePlot(QwtPlot):
def __init__(self, *args):
QwtPlot.__init__(self, *args)
self.setTitle("Frequency Response of a 2<sup>nd</sup>-order System")
self.setCanvasBackground(Qt.darkBlue)
# legend
legend = QwtLegend()
legend.setFrameStyle(QFrame.Box | QFrame.Sunken)
self.insertLegend(legend, QwtPlot.BottomLegend)
# grid
QwtPlotGrid.make(plot=self, enableminor=(True, False), color=Qt.darkGray)
# axes
self.enableAxis(QwtPlot.yRight)
self.setAxisTitle(QwtPlot.xBottom, "\u03c9/\u03c9<sub>0</sub>")
self.setAxisTitle(QwtPlot.yLeft, "Amplitude [dB]")
self.setAxisTitle(QwtPlot.yRight, "Phase [\u00b0]")
self.setAxisMaxMajor(QwtPlot.xBottom, 6)
self.setAxisMaxMinor(QwtPlot.xBottom, 10)
self.setAxisScaleEngine(QwtPlot.xBottom, QwtLogScaleEngine())
# curves
self.curve1 = QwtPlotCurve.make(
title="Amplitude", linecolor=Qt.yellow, plot=self, antialiased=True
)
self.curve2 = QwtPlotCurve.make(
title="Phase", linecolor=Qt.cyan, plot=self, antialiased=True
)
self.dB3Marker = QwtPlotMarker.make(
label=QwtText.make(color=Qt.white, brush=Qt.red, weight=QFont.Light),
linestyle=QwtPlotMarker.VLine,
align=Qt.AlignRight | Qt.AlignBottom,
color=Qt.green,
width=2,
style=Qt.DashDotLine,
plot=self,
)
self.peakMarker = QwtPlotMarker.make(
label=QwtText.make(
color=Qt.red, brush=self.canvasBackground(), weight=QFont.Bold
),
symbol=QwtSymbol.make(QwtSymbol.Diamond, Qt.yellow, Qt.green, (7, 7)),
linestyle=QwtPlotMarker.HLine,
align=Qt.AlignRight | Qt.AlignBottom,
color=Qt.red,
width=2,
style=Qt.DashDotLine,
plot=self,
)
QwtPlotMarker.make(
xvalue=0.1,
yvalue=-20.0,
align=Qt.AlignRight | Qt.AlignBottom,
label=QwtText.make(
"[1-(\u03c9/\u03c9<sub>0</sub>)<sup>2</sup>+2j\u03c9/Q]"
"<sup>-1</sup>",
color=Qt.white,
borderradius=2,
borderpen=QPen(Qt.lightGray, 5),
brush=Qt.lightGray,
weight=QFont.Bold,
),
plot=self,
)
self.setDamp(0.01)
def showData(self, frequency, amplitude, phase):
self.curve1.setData(frequency, amplitude)
self.curve2.setData(frequency, phase)
def showPeak(self, frequency, amplitude):
self.peakMarker.setValue(frequency, amplitude)
label = self.peakMarker.label()
label.setText("Peak: %4g dB" % amplitude)
self.peakMarker.setLabel(label)
def show3dB(self, frequency):
self.dB3Marker.setValue(frequency, 0.0)
label = self.dB3Marker.label()
label.setText("-3dB at f = %4g" % frequency)
self.dB3Marker.setLabel(label)
def setDamp(self, d):
self.damping = d
# Numerical Python: f, g, a and p are NumPy arrays!
f = np.exp(np.log(10.0) * np.arange(-2, 2.02, 0.04))
g = 1.0 / (1.0 - f * f + 2j * self.damping * f)
a = 20.0 * np.log10(abs(g))
p = 180 * np.arctan2(g.imag, g.real) / np.pi
# for show3dB
i3 = np.argmax(np.where(np.less(a, -3.0), a, -100.0))
f3 = f[i3] - (a[i3] + 3.0) * (f[i3] - f[i3 - 1]) / (a[i3] - a[i3 - 1])
# for showPeak
imax = np.argmax(a)
self.showPeak(f[imax], a[imax])
self.show3dB(f3)
self.showData(f, a, p)
self.replot()
FNAME_PDF = "bode.pdf"
class BodeDemo(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
self.plot = BodePlot(self)
self.plot.setContentsMargins(5, 5, 5, 0)
self.setContextMenuPolicy(Qt.NoContextMenu)
self.setCentralWidget(self.plot)
toolBar = QToolBar(self)
self.addToolBar(toolBar)
btnPrint = QToolButton(toolBar)
btnPrint.setText("Print")
btnPrint.setIcon(QIcon(QPixmap(print_xpm)))
btnPrint.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
toolBar.addWidget(btnPrint)
btnPrint.clicked.connect(self.print_)
btnExport = QToolButton(toolBar)
btnExport.setText("Export")
btnExport.setIcon(QIcon(QPixmap(print_xpm)))
btnExport.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
toolBar.addWidget(btnExport)
btnExport.clicked.connect(self.exportDocument)
toolBar.addSeparator()
dampBox = QWidget(toolBar)
dampLayout = QHBoxLayout(dampBox)
dampLayout.setSpacing(0)
dampLayout.addWidget(QWidget(dampBox), 10) # spacer
dampLayout.addWidget(QLabel("Damping Factor", dampBox), 0)
dampLayout.addSpacing(10)
toolBar.addWidget(dampBox)
self.statusBar()
self.showInfo()
if utils.TestEnvironment().unattended:
self.print_(unattended=True)
def print_(self, unattended=False):
try:
mode = QPrinter.HighResolution
printer = QPrinter(mode)
except AttributeError:
# Some PySide6 / PyQt6 versions do not have this attribute on Linux
printer = QPrinter()
printer.setCreator("Bode example")
printer.setPageOrientation(QPageLayout.Landscape)
try:
printer.setColorMode(QPrinter.Color)
except AttributeError:
pass
docName = str(self.plot.title().text())
if not docName:
docName.replace("\n", " -- ")
printer.setDocName(docName)
dialog = QPrintDialog(printer)
if unattended:
# Configure QPrinter object to print to PDF file
printer.setPrinterName("")
printer.setOutputFileName(FNAME_PDF)
dialog.accept()
ok = True
else:
ok = dialog.exec_()
if ok:
renderer = QwtPlotRenderer()
renderer.renderTo(self.plot, printer)
def exportDocument(self):
renderer = QwtPlotRenderer(self.plot)
renderer.exportTo(self.plot, "bode")
def showInfo(self, text=""):
self.statusBar().showMessage(text)
def moved(self, point):
info = "Freq=%g, Ampl=%g, Phase=%g" % (
self.plot.invTransform(QwtPlot.xBottom, point.x()),
self.plot.invTransform(QwtPlot.yLeft, point.y()),
self.plot.invTransform(QwtPlot.yRight, point.y()),
)
self.showInfo(info)
def selected(self, _):
self.showInfo()
def test_bodedemo():
"""Bode demo"""
utils.test_widget(BodeDemo, (640, 480))
if os.path.isfile(FNAME_PDF):
os.remove(FNAME_PDF)
if __name__ == "__main__":
test_bodedemo()