rustfftで窓をかける

実際のFFTでは無限に継続する波形というのはないので、実用的なアプリケーションでは信号の切り出しが必要です

ところでrustfftには窓機能のメソッドはないので、個別に実装が必要ですが、一番ポピュラーと思うhanning窓を適用してみました

hanning窓は(1-cos(x))/2で表現されますが、前回の正弦波の合成波形にこの窓を適用します

<窓適用後の入力波形>

<FFTの結果>

比較用の窓処理なしのスペクトル

窓をかけるとピークレベルはほぼ半分になります(対称波形のエンベロープの面積から)、また多少スペクトルの根元が広がっています、予測ではもっと広がると思ってた

P.S. 2024/10/28

スペクトルの分散の考え方は、このケースでは正弦波を窓関数でAM変調していると捉えれば、キャリア周波数の両サイドに山がいくつか見えるはずで、周波数分解能が足りないからダンゴになって見えているだけです

<コード>

正弦波の合成のところで窓処理も同時に行ってます

use plotters::prelude::*;
use rustfft::{num_complex::Complex, FftPlanner};
use std::f32::consts::PI;
use std::time::Instant;

const N: usize = 256; // FFT size
const SAMPLE_RATE: f32 = 256.0; // sampling rate
const INPUT_FREQ: f32 = 20.0; // input frequency

fn complex_vector_with_hanning(length: usize, dt: f32, frequency: f32) -> Vec<Complex<f32>> {
    (0..length)
        .map(|i| {
            let t = i as f32 * dt;
            let phase = frequency * 2.0 * PI * t;
            let phase2 = 3.0 * frequency * 2.0 * PI * t;
            Complex::new(
                (f32::sin(phase) + f32::sin(phase2)) * 0.5 * (1.0 - f32::cos(2.0 * PI * t)),
                0.0,
            )
        })
        .collect()
}

fn re_vector(complex_numbers: Vec<Complex<f32>>) -> Vec<f32> {
    complex_numbers.iter().map(|z| z.re).collect()
}

fn draw(
    x: Vec<usize>,
    y: Vec<f32>,
    f_name: &str,
    cap: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let image_width = 1080;
    let image_height = 720;

    let root = BitMapBackend::new(f_name, (image_width, image_height)).into_drawing_area();

    root.fill(&WHITE)?;

    //   https://qiita.com/lo48576/items/343ca40a03c3b86b67cb
    let (y_min, y_max) = y
        .iter()
        .fold((0.0 / 0.0, 0.0 / 0.0), |(m, n), v| (v.min(m), v.max(n)));

    let caption = cap;
    let font = (“sans-serif”, 20);

    let mut chart = ChartBuilder::on(&root)
        .caption(caption, font.into_font())
        .margin(10)
        .x_label_area_size(16)
        .y_label_area_size(42)
        .build_cartesian_2d(*x.first().unwrap()..*x.last().unwrap(), y_min..y_max)?;

    chart.configure_mesh().draw()?;

    let line_series = LineSeries::new(x.iter().zip(y.iter()).map(|(x, y)| (*x, *y)), &RED);
    chart.draw_series(line_series)?;

    Ok(())
}

fn main() {
    // fft data preparation with window
    let sr = 1.0 / SAMPLE_RATE;

    let mut buffer = complex_vector_with_hanning(N, sr, INPUT_FREQ);
 
    // draw input signal
    let buf_re = re_vector(buffer.clone());

    let n = N;
    let f_input = “plot_input.png”;
    let cap = “fft input”;
    let x: Vec<usize> = (1..=n).collect();

    let _ = draw(x, buf_re, &f_input, &cap);

    // fft mode setting
    let mut planner: FftPlanner<f32> = FftPlanner::new();
    let fft = planner.plan_fft_forward(N);

    let start_time = Instant::now();
    
    // fft exec
    fft.process(&mut buffer);

    let end_time = Instant::now();
    println!(“Elapsed time for FFT: {:?}”, end_time.duration_since(start_time));

    // absolute value calc
    let y: Vec<f32> = buffer.iter().map(|z| z.norm()).collect();

    // draw fft graph output
    let n = N;
    let file_fft = “plot_fft_hanning.png”;
    let cap = “fft hanning result”;
    let x: Vec<usize> = (1..=n).collect();
    let _ = draw(x, y, &file_fft, &cap);

}

 

admin

RustのFFT crateで逆FFTを行う

rustfftには逆FFTの機能もあるので、FFT結果を逆FFTして元の波形と比較してみます

<やってること>

① 二つの三角関数の合成のVec作成してreに代入、imは全部ゼロ

② 作成したComplex Vecからre部分抽出のVecを作成してグラフ化

③  FFT実行

④ FFT実行結果VecのnormのVec作成してグラフ化

⑤ FFT実行結果Vecをそのまま使用して、iFFT実行

⑥ iFFT実行結果のComplex Vecからre部分抽出のVecを作成してグラフ化

 

<最終的なコード>

// Computes a forward FFT
//
use plotters::prelude::*;
use rustfft::{num_complex::Complex, FftPlanner};
use std::f32::consts::PI;
use std::time::Instant;

const N: usize = 512; // FFT size
const SAMPLE_RATE: f32 = 256.0; // sampling rate
const INPUT_FREQ: f32 = 20.0; // input frequency

// prepare target data
fn complex_vector(length: usize, dt: f32, frequency: f32) -> Vec<Complex<f32>> {
    (0..length)
        .map(|i| {
            let t = i as f32 * dt;
            let phase = frequency * 2.0 * PI * t;
            let phase2 = 3.0 * frequency * 2.0 * PI * t;
            Complex::new(phase.sin() + phase2.sin(), 0.0)
        })
        .collect()
}

fn re_vector(complex_numbers: Vec<Complex<f32>>) -> Vec<f32> {
    complex_numbers.iter().map(|z| z.re).collect()
}

fn draw(x: Vec<usize>, y: Vec<f32>, f_name: &str, cap: &str) -> Result<(), Box<dyn std::error::Error>> {
    let image_width = 1080;
    let image_height = 720;

    let root = BitMapBackend::new(f_name, (image_width, image_height)).into_drawing_area();

    root.fill(&WHITE)?;

    //   https://qiita.com/lo48576/items/343ca40a03c3b86b67cb
    let (y_min, y_max) = y
        .iter()
        .fold((0.0 / 0.0, 0.0 / 0.0), |(m, n), v| (v.min(m), v.max(n)));

    let caption = cap;
    let font = ("sans-serif", 20);

    let mut chart = ChartBuilder::on(&root)
        .caption(caption, font.into_font())
        .margin(10)
        .x_label_area_size(16)
        .y_label_area_size(42)
        .build_cartesian_2d(*x.first().unwrap()..*x.last().unwrap(), y_min..y_max)?;

    chart.configure_mesh().draw()?;

    let line_series = LineSeries::new(x.iter().zip(y.iter()).map(|(x, y)| (*x, *y)), &RED);
    chart.draw_series(line_series)?;

    Ok(())
}

fn main() {
    // fft data preparation
    let sr = 1.0 / SAMPLE_RATE;
    let mut buffer = complex_vector(N, sr, INPUT_FREQ);

    // draw input signal
    let buf_re = re_vector(buffer.clone());

    let n = N;
    let f_input = "plot_input.png";
    let cap = "fft input";
    let x: Vec<usize> = (1..=n).collect();
    let _ = draw(x, buf_re, &f_input, &cap);

    // fft mode setting
    let mut planner = FftPlanner::new();
    let fft = planner.plan_fft_forward(N);

    let start_time = Instant::now();
    // fft exec
    fft.process(&mut buffer);

    let end_time = Instant::now();
    println!("Elapsed time: {:?}", end_time.duration_since(start_time));

    // absolute value calc
    let y: Vec<f32> = buffer.iter().map(|z| z.norm()).collect();

    // drwa fft graph output
    let n = N;
    let file_fft = "plot_fft.png";
    let cap = "fft result";
    let x: Vec<usize> = (1..=n).collect();
    let _ = draw(x, y, &file_fft, &cap);

    // ifft exec
    let ifft = planner.plan_fft_inverse(N);
    ifft.process(&mut buffer);
    // get complex:re
    let y = re_vector(buffer);

    // ifft graph output
    let n = N;
    let file_ifft = "plot_ifft.png";
    let cap = "ifft result";
    let x: Vec<usize> = (1..=n).collect();
    let _ = draw(x, y, &file_ifft, &cap);

}

<作成されたグラフ>

②のグラフ

④のグラフ

⑥のグラフ

確かに元の波形が逆FFTで再現できています、サンプリング周波数を2のn乗分の1にしたのでノイズなく綺麗なスペクトルが現れています。

スペクトル計算ではnorm計算が必要ですが、iFFTの結果の波形はVecのre部分だけを使います

 

admin

FFTライブラリの実行速度(Python numpy, Rust Crate)

FFTの実行速度をbumpy FFTとRustのクレートである、

https://docs.rs/rustfft/latest/rustfft/

と比較してみた

<条件>

Python numpy/rustfftでM1 Macとラズパイzero W

 

・実行条件

sampling_rate = 2000  # サンプリング周波数(Hz)

T = 1 / sampling_rate  # サンプリング間隔

t = np.arange(0, 1.0, T)  # 時間ベクトル

# 信号生成(50Hzと120Hzのサイン波を重ねたもの)

f1 = 100  # Hz

f2 = 300  # Hz

signal = np.sin(2*np.pi*f1*t) + 0.5*np.sin(2*np.pi*f2*t)

<実行速度>

M1 MacBook Air: np.fft.fft:     0.000027 [sec] 

ラズパイzero W: np.fft.fft: 0.001830 [sec] 

ふーむ、およそ60倍ぐらい違うか、想定範囲だけど

 

<rustfftで実行>

・実行条件

const N: usize = 1024;              // FFT size

const SAMPLE_RATE: f32 = 100.0;     // sampling rate

const INPUT_FREQ: f32 = 20.0;       // input frequency

どちらもreleaseモードでコンパイル、ほぼ条件は同じでnumpyfftとrustfftは同等の実行速度で、Cで記述されているだろうnumpyだからある意味当然の結果です

M1 MacBook Air
Elapsed time: 31.583µs

(参考値:debugモードでコンパイル)
Elapsed time: 1.878708ms


ラズパイzero W
$ ./fft
Elapsed time: 1.957008ms

・Rustのコード

fftは複素数で計算していますが、データはre部分だけ作成、結果は複素数になるので絶対値を求めてVecに格納、このクレートは入力データのVecに計算結果が格納されるという変則的なクレートのように思う

// Computes a forward FFT
//
use plotters::prelude::*;
use rustfft::{FftPlanner, num_complex::Complex};
use std::f32::consts::PI;
use std::time::Instant;

const N: usize = 1024;              // FFT size
const SAMPLE_RATE: f32 = 100.0;     // sampling rate
const INPUT_FREQ: f32 = 20.0;       // input frequency

// prepare target data
fn complex_vector(length: usize, dt: f32, frequency: f32) -> Vec<Complex<f32>> {
    (0..length)
        .map(|i| {
            let t = i as f32 * dt;
            let phase = frequency * 2.0 * PI * t;
            let phase2 = 3.0 * frequency * 2.0 * PI * t;
            Complex::new(phase.sin() + phase2.sin(), 0.0)
        })
        .collect()
}

fn draw(x: Vec<usize>, y: Vec<f32>) -> Result<(), Box<dyn std::error::error="">> {
    let image_width = 1080;
    let image_height = 720;

    let root = BitMapBackend::new("plot.png", (image_width, image_height)).into_drawing_area();

    root.fill(&WHITE)?;

    //   https://qiita.com/lo48576/items/343ca40a03c3b86b67cb
    let (y_min, y_max) = y
        .iter()
        .fold((0.0 / 0.0, 0.0 / 0.0), |(m, n), v| (v.min(m), v.max(n)));

    let caption = "DFT result";
    let font = ("sans-serif", 20);

    let mut chart = ChartBuilder::on(&root)
        .caption(caption, font.into_font())
        .margin(10) 
        .x_label_area_size(16) 
        .y_label_area_size(42) 
        .build_cartesian_2d(
            *x.first().unwrap()..*x.last().unwrap(), 
            y_min..y_max,                            
        )?;

    chart.configure_mesh().draw()?;

    let line_series = LineSeries::new(x.iter().zip(y.iter()).map(|(x, y)| (*x, *y)), &RED);
    chart.draw_series(line_series)?;

    Ok(())
}

fn main() {
    // fft data preparation
    let sr = 1.0/SAMPLE_RATE;
    let mut buffer = complex_vector(N, sr, INPUT_FREQ);

    // fft mode setting
    let mut planner = FftPlanner::new();
    let fft = planner.plan_fft_forward(N);    

    let start_time = Instant::now();
    // fft exec
    fft.process(&mut buffer);

    let end_time = Instant::now();
    println!("Elapsed time: {:?}", end_time.duration_since(start_time));

    // absolute value calc
    let y: Vec<f32> = buffer.iter().map(|z| z.norm()).collect();
    // drwa graph
    let n = N;
    let x: Vec<usize> = (1..=n).collect();
    let _ = draw(x, y);
}

ビジュアル化した結果、x軸の512を境に対称形になっています、レベル(ピーク値)が違うのは今のところ謎

 

admin

Golangのモジュールのアップデート

Golangを 1.21にアップしたら、buildでgo.bug.st/serial関連のエラー出るので、関連部分のソースコードは触っていないのでモジュールを最新にアップデートした

% go list -m all
~~
go.bug.st/serial v1.6.2
~~

前後省略

これは更新後ですが、前の版数は1.5.0

更新履歴は、

https://pkg.go.dev/go.bug.st/serial@v1.6.2?tab=versions

% go get go.bug.st/serial 

で更新してやります、これでめでたくビルドは成功

 

admin

生成A.I(Gemini)でコード生成(@Rust)

昨日の継続ですが、素数計算のロジックをGeminiで作成してみた

<環境>

M1 Macbook Air

————————————————————–

GeminiにRustである範囲の素数計算するコード出してといって、多少は手を入れてますが、ほぼ生成されたそのまま

use std::time::Instant;

fn is_prime(num: u64) -> bool {
    if num <= 1 {
        return false;
    }
    if num <= 3 {
        return true;
    }
    if num % 2 == 0 || num % 3 == 0 {
        return false;
    }

    let mut i = 5;
    while i * i <= num {
        if num % i == 0 || num % (i + 2) == 0 {
            return false;
        }
        i += 6;
    }

    true
}

fn find_primes(start: u64, end: u64) {
    let mut pri: Vec = Vec::new();
    let start_time = Instant::now();
    for num in start..=end {
        if is_prime(num) {
            pri.push(num);
        }
    }
    let end_time = Instant::now();
    println!("Elapsed time: {:?}", end_time.duration_since(start_time));
    println!("number of primes: {}", pri.len());
}

fn main() {
    let start = 2;
    let end = 1000_000;

    println!("{}から{}までの素数:", start, end);
    find_primes(start, end);
}

最初に気づいたことは① 素数検出ロジックは2, 3で割り算できずかつ6の倍数の±1で割り切れないことが『標準』だということ、確かに計算量はそれだけでほぼ三分の一になるよね

次に、それをマルチスレッド化、これも自動生成

use rayon::prelude::*;
use std::time::Instant;

fn is_prime(num: u64) -> bool {
    if num <= 1 {
        return false;
    }
    if num <= 3 {
        return true;
    }
    if num % 2 == 0 || num % 3 == 0 {
        return false;
    }

    let mut i = 5;
    while i * i <= num { if num % i == 0 || num % (i + 2) == 0 { return false; } i += 6; } true } fn find_primes_multithreaded(start: u64, end: u64) -> Vec {
    (start..=end)
        .into_par_iter()
        .filter(|num| is_prime(*num))
        .collect()
}

fn main() {
    let start = 2;
    let end = 1000_000;

    println!("{}から{}までの素数:", start, end);

    let start_time = Instant::now();
    let primes = find_primes_multithreaded(start, end);
    let end_time = Instant::now();

    println!("Elapsed time: {:?}", end_time.duration_since(start_time));
    println!("number of primes: {}", primes.len());
}

② rayonとinto_par_iter()使えば、マルチスレッド化がプロセッサの数に応じて自動でiter処理を実行してくれること

ちなみに実行時間を測ってみると、

<Geminiで生成したシングルスレッド版 : release mode>
2から1000000までの素数:
Elapsed time: 37.51125ms


rayon::tierクレートのinto_par_iter()メソッドでマルチスレッド対応のiteratorが作れるということ、

<Geminiで生成したマルチスレッド版 : release mode>
2から1000000までの素数:
Elapsed time: 7.397958ms

昨日のロジックではrelease modeのバイナリで90ms程度要していたから、やはり三分の一程度高速化されていて、さらにマルチスレッド版(8CPU)でオーバーヘッドあるにしてもさらに5倍ぐらい高速化

というわけで、仕様を明確に定義できる領域であれば、生成A.I使った方が人が記述するより時間短縮して効率の良いコードが作成できるだろうということで、githubのcopilotなどのその範疇(使ったことはないけど)でしょう

逆に他の言語からのそのまま置き換え(変換)は元のコードがスマートでないと無駄な作業が発生するし、もしかしたらそれほど向いていないかもしれない

 

admin

Rustのコンパイルオプション—debug vs —releaseで作成された実行ファイルの速度差

Rustのドキュメントによれば、–debugオプションと–releaseオプションで作成されたバイナリの実行速度差は10倍から最大100倍といっていますが、実際のサンプルでどうなるかみてみた

<環境>

M1 Macbook Airとラズパイzero W

百万までの素数を求めてベクターに格納、比較対象はGolnagでの実行速度、

Golangのコードはこちら、

https://github.com/chateight/golang/blob/master/concpara/waitGroup.go

Rustのコードは省力化のためにGeminiで作成、ただしマルチスレッド周りでたくさんエラー出たからシングルスレッド化して動かしてみた、マルチスレッド対応のコードは多少残存しているけど、比較の目的にはそれほど影響ないだろうからそのまま

use std::sync::{Arc, Mutex};
use std::time::Instant;

fn main() {
    let start_time = Instant::now();
    let num = find_primes(2, 10000 * 100);
    println!("{}",num.len());
    let end_time = Instant::now();
    println!("Elapsed time: {:?}", end_time.duration_since(start_time));
}

fn find_primes(start: i32, end: i32) -> Vec<i32>{
    let counter = Arc::new(Mutex::new(start)); // Shared counter for prime candidates

    let mut value = Vec::new();
    let counter_clone = counter.clone();

    loop {
        let c = counter_clone.lock().unwrap().clone();
        if c > end {
            break;
        }

        let mut flag = true;
        for j in 2..((c as f64).sqrt() as i32 + 1) {
            if c % j == 0 {
                flag = false;
                break;
            }
        }

        if flag {
            value.push(c);
        }

        *counter_clone.lock().unwrap() += 1;
    }
    value
}

結果:素数の数は重要ではないけれども、ロジック検証用

@MacBook Air M1

<Golangでの百万までの素数計算>
78498	; 素数の数
440.783125ms ; 実行時間

<rust build>
78498
Elapsed time: 508.7235ms

<rust release>
78498
Elapsed time: 91.830167ms


@Raspberry pi zero W
<rust build>
78498
Elapsed time: 18.59699031s

<rust release>
78498
Elapsed time: 7.10051562s

–debugバイナリと–releaseバイナリの実行速度差はM1 Macだと6倍弱、ラズパイzeroだと2倍程度で予想したほどの差はないというのが実感、M1 Macとラズパイzeroの速度差は安定の数十倍

Golangはマルチスレッドにしてマルチコアを使ったつもりだけれども、Rustに比較するとアドバンテージはなさそうに見える

 

admin

 

 

Rustのクロスコンパイル環境でCrossを使う

結構以前からあるツールのようですが、DockerあるいはオープンソースのコンテナであるPodmanなどのコンテナ使ってコンテナ内にターゲットのイメージを持ってきてその中でコンパイルしてくれるツールです

<環境>

・コンパイル環境:M1 Mac

・ターゲット:Raspberry pi zero W

Dockerの場合にはroot権限でなくユーザ権限でないと使えないというし、Padmanならば最初からユーザー権限ということでPodmanをインストして使ってみました

https://podman.io/docs/installation

Podmanを使うかDockerを使うかは~/.zshrcに環境変数の指定が必要で、

https://docs.rs/crate/cross/latest

export CROSS_CONTAINER_ENGINE=podman

を指定します

 

Podmanを起動状態で、

% CROSS_CONTAINER_OPTS="--platform linux/amd64" cross build --target arm-unknown-linux-gnueabihf

を実行すると、imageをダウンロードしてその上でコンパイルします

% CROSS_CONTAINER_OPTS="--platform linux/amd64" cross build --target arm-unknown-linux-gnueabihf

Trying to pull ghcr.io/cross-rs/arm-unknown-linux-gnueabihf:0.2.5...
Getting image source signatures
Copying blob sha256:2c7e00e2a4a7dccfad0ec62cc82e2d3c05f3a4f1d4d0acc1733e78def0556d1e

~~~途中省略~~~

Writing manifest to image destination
   Compiling hello v0.1.0 (/project)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s

このようにコンパイルできて、定番のHello Worldソースからビルドした実行ファイルをラズパイ zero Wに転送すると実行できました

なぜCROSS_CONTAINER_OPTS="--platform linux/amd64"必要なのかは、

https://github.com/cross-rs/cross/issues/1214

からの情報ですがM1上のクロスだよと指定が必要のようで、指定しないとtargetで使うimage見つからないよと言われます

 

admin

micro:bitからのログの方法

Wi-Fi機能があれば簡単ですが、micro:bitには無いので普通はBLE使うのですが、BLEはいまいち使いづらい、

というわけで、もう一台micro:bitを用意してmicro:bit間は専用のradio機能で通信、受け側はUSB serialを使えば割と簡単にログが取れます。パソコンでのターミナルのシリアル動作はいまいち不安定(文字が抜ける)なのでMakeCodeのログ機能で表示させてみました、最終的にはProcessingで物体の状態を表示させるのが目的ですが、

micro:bitの送り側からはコンパス情報と加速度情報を送って、受け側にシリアル転送、MakeCodeのログ機能でビジュアル化しています。

<コンパス機能>

角度360から0は断絶してますが、micro:bitを徐々に回転させた時の数値の変化

<x軸の加速度>

micro:bitを手で左右(x軸方向)に振ってる状態

事前の処理は必要ですが、これらを使ってクラゲホバークラフトの安定化に使うつもり、

P.S. USB シリアルのデータ抜け、飛びはM1 Macで起こるけれどもIntel Macでは問題なさそうだ、MakeCodeではまともそうな理屈はつきませんが、

あと、MakeCode以外でUSBシリアル使う時にはパソコンのリブートでポート番号が変わるのは要注意

 

admin

 

 

 

 

 

 

 

シリカゲル再生

乾燥剤はものによっては再生可能らしいのでやってみた

要は熱を加えて水分追い出せば良いのだから、最初はフィラメント脱湿機でやってみたけど赤い色(吸湿状態)は数時間ではほぼ変化しない

で、世の中でよくやられているようにフライパン(弱火)で炒ってみる、これは顕著に変化して、徐々に赤玉が青玉に変化していく

写真は赤玉ほとんど見えなくなった状態なのでこの程度で完了、二、三回はこの方法で再生できそうだ、再生後は適度に空気を通す袋に入れて使用

 

admin

3Dフィラメント防湿

フィラメント保管用に密閉ケースを買ったつもりでしたが、半年ぐらい経過すると湿度上昇して、乾燥剤追加しても割とすぐの戻ってしまう、つまり密閉度が保証されなくなってしまった模様で、フィラメントもポキポキ折れるから完全に吸湿してます

全体をボックスに入れるのではなくて、フィラメント一巻を個別で密閉袋に入れるような仕組みに変更するために

3Dプリンターフィラメント収納バッグ 真空ハンドポンプ付き

というのをAmazonで購入、中の空気を抜くポンプ付きですが、なぜポンプで中の空気が抜けるのか、つまり一方向性をどうやって確保しているのかがおそらくポイントなんだろうけどメカニズムは不明

ともかくも、中心部に乾燥剤いれて封入してみました、これでなんとかなるものなのかは時間経過しないと効果の程は不明ですが

ともかくも、3Dフィラメント保管用には吸湿したフィラメント乾燥用のヒーターと吸湿防止手段の二つは必需品です

 

admin