RustでPID制御を書いてみる。plottersを使ってグラフを描く

前回はオイラー法により制御モデルの数値計算をし、各時点での位置を出力しました。

今回は値を出力するのではなくグラフを作り、それを図として保存することをやってみたいと思います。

グラフを出力するには、そのためのcrate(Rustのライブラリのこと)を読み込む必要があり、ここではplotterというものを使用します。

tomlファイルの[dependencies]に以下のように記述します。

[dependencies]
plotters = "0.3.1"

そしてコードに、グラフを描くために必要なコードを足していきます。

数値計算した数値は、前回はprintfへの出力でしたが今回はグラフに描画したいので少し処理を変えていることに注意してください。

use plotters::prelude::*;

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軸の範囲
            -0.1..60.0, // y軸の範囲
        )
        .unwrap();
    ch.configure_mesh().draw().unwrap(); //  グラフ内にグリッド線を描画

    // グラフに描画するデータの生成
    let mut t_arr: Vec<f64> = Vec::new();  // グラフ用に値を格納するベクタを作る
    let mut x_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); // 刻み幅
    let mut plant = Plant::new(h);
    plant.set_input(30.0); // とりあえず適当な値を入力として設定する
    for _ in 0..points {
        // 制御対象の状態を取得する
        let state = plant.get_state();

        t_arr.push(t);  // 値をベクタに追加
        x_arr.push(state.0);  // 値をベクタに追加

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

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

上記コードを実行すると、"fig.png"という、下のような図が作成されるかと思います。 f:id:fourteenth_letter:20220205162923p:plain

これで制御モデルで数値計算を行って図にすることができ、制御シミュレーションをする準備が整ってきました。

今回はここで終了です。