Bode Plot
Magnitude and phase on dual Y-axes with interactive parameter sliders.
Second Order System Response
Source Code
#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSlider>
#include <QLabel>
#include <cmath>
#include "AZPlottingClass.h"
// Generate transfer function response: H(s) = wn^2 / (s^2 + 2*zeta*wn*s + wn^2)
void generateBodeData(double wn, double zeta,
QVector<double> &freq,
QVector<double> &magnitude_dB,
QVector<double> &phase_deg)
{
freq.clear();
magnitude_dB.clear();
phase_deg.clear();
for (double f = 0.1; f <= 1000; f *= 1.05) {
double w = 2 * M_PI * f;
double w_ratio = w / wn;
double denom_real = 1 - w_ratio * w_ratio;
double denom_imag = 2 * zeta * w_ratio;
double mag = 1.0 / std::sqrt(denom_real * denom_real + denom_imag * denom_imag);
double phase = -std::atan2(denom_imag, denom_real) * 180.0 / M_PI;
freq.append(f);
magnitude_dB.append(20 * std::log10(mag));
phase_deg.append(phase);
}
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget window;
window.setWindowTitle("Bode Plot - Second Order System");
window.resize(850, 550);
QVBoxLayout *mainLayout = new QVBoxLayout(&window);
mainLayout->setContentsMargins(10, 10, 10, 10);
// Control bar
QHBoxLayout *controlLayout = new QHBoxLayout();
QLabel *wnLabel = new QLabel("wn = 100 rad/s");
QSlider *wnSlider = new QSlider(Qt::Horizontal);
wnSlider->setRange(10, 500);
wnSlider->setValue(100);
wnSlider->setFixedWidth(150);
QLabel *zetaLabel = new QLabel("zeta = 0.30");
QSlider *zetaSlider = new QSlider(Qt::Horizontal);
zetaSlider->setRange(1, 200);
zetaSlider->setValue(30);
zetaSlider->setFixedWidth(150);
controlLayout->addWidget(new QLabel("Natural Freq:"));
controlLayout->addWidget(wnSlider);
controlLayout->addWidget(wnLabel);
controlLayout->addSpacing(30);
controlLayout->addWidget(new QLabel("Damping:"));
controlLayout->addWidget(zetaSlider);
controlLayout->addWidget(zetaLabel);
controlLayout->addStretch();
mainLayout->addLayout(controlLayout);
// Plot
AZPlottingClass *plot = new AZPlottingClass(&window);
plot->setTitle("Second Order System Frequency Response");
plot->setXAxisLabel("Frequency (Hz)");
plot->setYAxisLabel("Magnitude (dB)");
plot->setY2AxisLabel("Phase (deg)");
plot->setXAxisScale(AZPlottingClass::ScaleType::Logarithmic);
plot->applyProfessionalStyle();
plot->setInteractive(true, true);
plot->setGridVisible(true, false);
mainLayout->addWidget(plot, 1);
auto updatePlot = [plot, wnSlider, zetaSlider, wnLabel, zetaLabel]() {
double wn = wnSlider->value();
double zeta = zetaSlider->value() / 100.0;
wnLabel->setText(QString("wn = %1 rad/s").arg(wn));
zetaLabel->setText(QString("zeta = %1").arg(zeta, 0, 'f', 2));
QVector<double> freq, mag, phase;
generateBodeData(wn, zeta, freq, mag, phase);
plot->clearData();
plot->clearMarkerLines();
// Magnitude on primary Y-axis
plot->addDataSeries(freq, mag, "Magnitude", Qt::blue,
AZPlottingClass::LineStyle::Solid, 2.0);
// Phase on secondary Y-axis
plot->addDataSeriesToY2(freq, phase, "Phase", Qt::red,
AZPlottingClass::LineStyle::Solid, 2.0);
// Reference lines
plot->addHorizontalLine(0, QColor(100, 100, 100), "0 dB", 0.5);
plot->addVerticalLine(wn / (2 * M_PI), QColor(50, 150, 50), "wn", 0.5);
plot->autoScaleAll();
plot->setY2AxisRange(-180, 0);
};
QObject::connect(wnSlider, &QSlider::valueChanged, updatePlot);
QObject::connect(zetaSlider, &QSlider::valueChanged, updatePlot);
updatePlot();
window.show();
return app.exec();
}