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();
}