RustでPID制御を書いてみる。比例微分先行型PID制御を書いてみる

「RustでPID制御を書いてみる」です。

このシリーズでは、南裕樹様が書かれた『Pythonによる制御工学入門』を参考&題材にして作成させていただいております。

www.amazon.co.jp

前回は、微分先行型PID制御(P-ID制御)にすることによって入力値がどうなるかを確認してみました。

今回は、もう一つの応用形である、比例微分先行型PID制御(I-PD制御)を実際に書いてみたいと思います。

具体的には『Pythonによる制御工学入門』内の図5.20右(161ページ)と、図5.20右(161ページ)のグラフを描いていきます。(当初の目標であった所に今回で到達することになります)

さて、微分先行型PID制御(P-ID制御)は、1自由度のPID制御の「操作量が急変する」ことの課題を改善したもので、具体的にはPID制御の微分項の計算方法を少し変えたものでした。

今回ご紹介する比例微分先行型PID制御(I-PD制御)は、微分項に加え比例項の計算方法も変えることで特性が良くなることを狙ったものです。

さて、実際に比例微分先行型PID制御を実装したコードは下記です。

use plotters::prelude::*;

struct PIDController {
    sv: f64,         // 目標値
    kp: f64,         // 比例項のゲイン
    kd: f64,         // 微分項のゲイン
    ki: f64,         // 積分項のゲイン
    integrator: f64, // 積算器
    dt: f64,         // 時間の刻み幅
    err_prev: f64,   // 1時点前の"目標値-測定値""
    pv_prev: f64,    // 1時点前の"目標値-測定値""
}
impl PIDController {
    fn new(dt: f64) -> PIDController {
        return PIDController {
            sv: 30.0,
            kp: 2.0,
            kd: 0.1,
            ki: 10.0,
            dt: dt,
            err_prev: 0.0,
            integrator: 0.0,
            pv_prev: 0.0,
        };
    }
    /// 1自由度PIDにより次の入力値を求める
    fn calc_1pid(&mut self, pv: f64) -> f64 {
        let term_p = self.kp * (self.sv - pv);
        let term_d = (self.kd / self.dt) * ((self.sv - pv) - self.err_prev);
        let term_i = self.ki * self.dt * self.integrator;
        let mv = term_p + term_i + term_d;
        self.integrator += self.sv - pv;
        self.err_prev = self.sv - pv;
        return mv;
    }
    /// 比例微分先行型PIDにより次の入力値を求める
    fn calc_ipd(&mut self, pv: f64) -> f64 {
        let term_p = self.kp * (-1.0 * pv);
        let term_d = (self.kd / self.dt) * (-1.0 * (pv - self.pv_prev));
        let term_i = self.ki * self.dt * self.integrator;
        let mv = term_p + term_i + term_d;
        self.integrator += self.sv - pv;
        self.err_prev = self.sv - pv;
        self.pv_prev = pv;
        return mv;
    }
}
struct Plant {
    h: f64,
    u: f64,
    y: f64,
    v: f64,
}
impl Plant {
    fn new(h: f64) -> Plant {
        return Plant {
            h,
            u: 0.0,
            y: 0.0,
            v: 0.0,
        };
    }
    fn get_state(&self) -> (f64, f64) {
        return (self.y, self.v);
    }
    fn set_input(&mut self, val: f64) {
        self.u = val;
        return;
    }
    fn step(&mut self) {
        // 「Pythonによる制御工学入門」p.60, 式(3.6)より
        // dx/dt = v
        // dv/dt = (1/J) * (u - (mu * v) - (M * g * l * y))
        // となる。これらを用いてオイラー法により微小時間後の状態を求める
        // なお、数値は上記本に記載されているものを用いている
        let dv_dt = (self.u - (0.015 * self.v) - (0.5 * 9.81 * 0.2 * self.y)) / 0.01;
        self.y = self.y + (self.v * self.h);
        self.v = self.v + (dv_dt * self.h);
        return;
    }
}

fn main() {
    // グラフの基本的な設定をする
    let rt = BitMapBackend::new("fig.png", (640, 480)).into_drawing_area();
    rt.fill(&WHITE).unwrap(); // 背景を白にする
    let mut ch = ChartBuilder::on(&rt)
        .margin(25)
        .x_label_area_size(25)
        .y_label_area_size(35)
        .build_cartesian_2d(
            0.0..2.0,   // x軸の範囲
            -1.0..50.0, // y軸の範囲
        )
        .unwrap();
    ch.configure_mesh().draw().unwrap(); //  グラフ内にグリッド線を描画

    // グラフに描画するデータの生成
    let mut t_arr: Vec<f64> = Vec::new();
    let mut x_1pid_arr: Vec<f64> = Vec::new();
    let mut x_ipd_arr: Vec<f64> = Vec::new();
    let t_max: f64 = 2.0; // tの最大値
    let points: u64 = 1_000_000; // 計算する点数。精度を確保するために多くしている
    let mut t: f64 = 0.0;
    let h = t_max / (points as f64); // 刻み幅

    //積分ゲイン0で動かすモデルを作る
    let mut plant_1pid = Plant::new(h);
    let mut pid_1pid = PIDController::new(h);

    //積分ゲイン5で動かすモデルを作る
    let mut plant_ipd = Plant::new(h);
    let mut pid_ipd = PIDController::new(h);

    for _ in 0..points {
        // 制御対象の状態を取得する
        let state_1pid = plant_1pid.get_state();
        let state_ipd = plant_ipd.get_state();

        t_arr.push(t);
        x_1pid_arr.push(state_1pid.0);
        x_ipd_arr.push(state_ipd.0);

        // 次の入力値を計算し設定する
        plant_1pid.set_input(pid_1pid.calc_1pid(state_1pid.0));
        plant_ipd.set_input(pid_ipd.calc_ipd(state_ipd.0));

        // 次の状態を計算する
        plant_1pid.step();
        plant_ipd.step();
        t += h;
    }
    // データをグラフに描画する(1つ目)
    let line_1pid = LineSeries::new(
        t_arr.iter().zip(x_1pid_arr.iter()).map(|(x, y)| (*x, *y)),
        &RED,
    );
    ch.draw_series(line_1pid)
        .unwrap()
        .label("pid")
        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));

    // データをグラフに描画する(2つ目)
    let line_ipd = LineSeries::new(
        t_arr.iter().zip(x_ipd_arr.iter()).map(|(x, y)| (*x, *y)),
        &BLUE,
    );
    ch.draw_series(line_ipd)
        .unwrap()
        .label("pi-d")
        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));

    // 凡例を書く
    ch.configure_series_labels()
        .background_style(&WHITE.mix(0.8))
        .border_style(&BLACK)
        .draw()
        .unwrap();
    return;
}

コードのfn calc_ipd(&mut self, pv: f64) -> f64 {から始まる関数の中を見てみると

        let term_p = self.kp * (-1.0 * pv);

となっております。1自由度のPIDや微分先行型PIDの時は

        let term_p = self.kp * (self.sv - pv);

となっており、比較してみると微分先行型PID制御の微分項の時と同じく、測定値のみを用いて計算している(1自由度や微分先行型PIDでは目標値と測定値の2つの値を用いていた)のが処理の違いです。

さて、このコードを走らせてみると下記のようなグラフが作成されます。 f:id:fourteenth_letter:20220213103313p:plain

1自由度PID制御と比較すると、振動することなく目標値に収束していってる様子が分かるかと思います。

また、入力値はどのように変化しているでしょうか。(この部分のコードは省略いたします)

f:id:fourteenth_letter:20220213103319p:plain

微分先行型よりも更に入力の変化が緩やかになっていることが分かるかと思います。




さて、当初の目標であった所までRustで書くことができましたので、このシリーズはここで終わりとしたいと思います。

数値計算でPID制御のシミュレーションをしてみました。組込みなどで実際に制御を行いたい時にも参考に出来るかなぁと思います。