键盘快捷键

使用 跳转章节

使用 S/ 在本书内搜索

使用 ? 显示帮助页面

使用 Esc 隐藏帮助页面

Phyrs - 0x03

archive time: 2025-07-12

模块与函数的组织是一门技术

前面两节大体介绍了动力学的相关概念,并且通过例子说明分析流程

这一节将介绍如何让之前的代码通用

随时间、位置和速度变化的力

之前提到了可以通过欧拉法来分析系统状态变化, 对于一个基本的三维物体,状态可以由质量、位置、速度以及时间来描述:

#[derive(Clone)]
pub struct Particle<R: RealFloat, const E: usize> {
    pub mass: R,
    pub charge: R,
    pub position: VectorC<R, E>,
    pub velocity: VectorC<R, E>,
    pub time: R,
}

此时牛顿第二定律可以表示为:

此时状态转移方程可以表示为:

基于这个状态转移方程,我们可以得到一系列的状态:

根据研究的问题判断迭代终止条件,最后的状态就是我们要的结果

使用 Rust 实现可以得到:

impl<'a, R: RealFloat + 'a, const E: usize> Particle<R, E> {
    pub fn default_transition(
        acceleration: Rc<dyn Fn(Self) -> VectorC<R, E>>,
        dt: R,
    ) -> Rc<dyn Fn(Self) -> Self + 'a> {
        Rc::new(move |p: Self| -> Self {
            let velocity = p.velocity.clone() + acceleration(p.clone()) * dt.clone();
            let position = p.position.clone() + velocity.clone() * dt.clone();
            Self {
                mass: p.mass,
                charge: p.charge,
                velocity,
                position,
                time: p.time.clone() + dt.clone(),
            }
        })
    }

    pub fn simulate(self, transition: Rc<dyn Fn(Self) -> Self>) -> Iterate<'a, Self> {
        Iterate::of(transition, self)
    }
}

阻尼谐振子

作为受力随位置和速度相关的例子,可以考虑有一个阻尼谐振子, 即一个竖直悬挂的弹簧,末端有一个小球

假设向上为正方向,并且零点选择在弹簧平衡时的末端位置, 小球质量为 ,半径为

假设空气阻力系数为 ,空气密度为 , 重力加速度为 ,则此时系统受力为:

其中 表示弹簧距离平衡点的相对位移

若劲度系数 ,初始位置为 ,初速度为 ,则:

use std::rc::Rc;

use phyrs::newton::particle::Particle;
use plotters::{
    chart::ChartBuilder,
    prelude::{BitMapBackend, IntoDrawingArea},
    series::LineSeries,
    style::full_palette::{BLUE_400, YELLOW_50},
};
use rmatrix_ks::{
    matrix::vector::{VectorC, basis_vector},
    number::{
        instances::double::Double,
        traits::{floating::Floating, fractional::Fractional, zero::Zero},
    },
};

fn main() {
    run().unwrap()
}

fn run() -> Option<()> {
    // 初始状态
    let state = Particle::<Double, 1>::without_charge(
        Double::of(2.7e-3),
        VectorC::of(&[0.1].map(Double::of))?,
        VectorC::default(),
        Double::zero(),
    );
    // 加速度
    let total_acc = Rc::new(|s: Particle<Double, 1>| -> VectorC<Double, 1> {
        let g = -basis_vector(1) * Double::PI.power(Double::of(2.0));
        g   // 重力
            - (s.position * Double::of(0.8) // 弹力
            + s.velocity // 空气阻力
                * Double::of(2.0) // 空气阻力系数
                * Double::of(1.2) // 空气密度
                * Double::PI
                * Double::of(2e-2).power(Double::of(2.0)) // A = pi r^2
                * Double::half()) / s.mass
    });
    // 模拟计算
    let one_step = Particle::default_transition(total_acc, Double::of(0.001));
    // 计算 0 到 3 秒内的运动轨迹
    let states = state
        .simulate(one_step)
        .take_while(|s| s.time <= Double::of(3.0));
    // 绘图
    let draw_area = BitMapBackend::new("result/draw_damp.png", (960, 512)).into_drawing_area();
    draw_area.fill(&YELLOW_50).ok()?;
    let mut chart = ChartBuilder::on(&draw_area)
        .margin(10)
        .caption("阻尼谐振子", ("Zhuque Fangsong (technical preview)", 48))
        .x_label_area_size(48)
        .y_label_area_size(64)
        .build_cartesian_2d(0.0..3.0, -0.2..0.2)
        .ok()?;
    chart
        .configure_mesh()
        .axis_desc_style(("Zhuque Fangsong (technical preview)", 24))
        .x_desc("时间 (s)")
        .y_desc("位置 (m)")
        .draw()
        .ok()?;
    chart
        .draw_series(LineSeries::new(
            states.map(|s| (s.time.raw(), s.position[(1, 1)].raw())),
            &BLUE_400,
        ))
        .ok()?;
    draw_area.present().ok()?;

    Some(())
}

得到的图像为:

阻尼谐振子

可以看到,由于小球的重力,系统的平衡点并不是最初的 , 而是

欧拉-克罗默方法

实际上,我们使用的方法是欧拉-克罗默方法1

通常的欧拉法使用的是上一个状态的速度来计算下一个状态的位移, 而欧拉-克罗默方法则是使用下一个状态的速度来计算下一个状态的位移,即:

欧拉-克罗默方法的结果比欧拉法更符合能量守恒,精度和稳定性也比欧拉法高

求解微分方程

大部分动力学问题都可以转化为一个求解微分方程的问题

但是大部分微分方程很难得到一个精确解,所以求解微分方程更多的还是使用数值方法,例如欧拉法

由于数值方法多种多样,所以在 Particle 中, simulate 接受 transition 而不是合力,这是为了将模拟过程与数值方法解耦

比如我们还可以使用四阶龙格-库塔方法2

// Self: Particle<R, E>
pub fn general_rk_transition<const K: usize>(
    acceleration: Rc<dyn Fn(Self) -> VectorC<R, E>>,
    dt: R,
    c: VectorC<R, K>,
    a: Matrix<R, K, K>,
    b: VectorC<R, K>,
) -> Rc<dyn Fn(Self) -> Self + 'a> {
    Rc::new(move |p: Self| -> Self {
        // init
        let mut k_dv_dt = vec![VectorC::default(); K + 1];
        let mut k_dr_dt = vec![VectorC::default(); K + 1];
        let mut pk = p.clone();
        k_dv_dt[1] = acceleration(pk.clone());
        k_dr_dt[1] = pk.velocity.clone();
        // iter
        for i in 2..=K {
            let mut ki_dv_dt = VectorC::default();
            let mut ki_dr_dt = VectorC::default();
            for j in 1..i {
                ki_dv_dt = ki_dv_dt + k_dv_dt[j].clone() * a[(i, j)].clone();
                ki_dr_dt = ki_dr_dt + k_dr_dt[j].clone() * a[(i, j)].clone();
            }
            pk = Self {
                position: p.position.clone() + ki_dr_dt * dt.clone(),
                velocity: p.velocity.clone() + ki_dv_dt * dt.clone(),
                time: p.time.clone() + c[(i, 1)].clone() * dt.clone(),
                ..pk
            };
            k_dv_dt[i] = acceleration(pk.clone());
            k_dr_dt[i] = pk.velocity.clone();
        }
        // result
        let position = p.position
            + b.linear_iter()
                .zip(k_dr_dt.iter().skip(1))
                .map(|(bi, dr_dt_i)| dr_dt_i.clone() * bi.clone())
                .fold(VectorC::default(), |acc, e| acc + e)
                * dt.clone();
        let velocity = p.velocity
            + b.linear_iter()
                .zip(k_dv_dt.iter().skip(1))
                .map(|(bi, dv_dt_i)| dv_dt_i.clone() * bi.clone())
                .fold(VectorC::default(), |acc, e| acc + e)
                * dt.clone();

        Self {
            mass: pk.mass,
            charge: pk.charge,
            position,
            velocity,
            time: p.time + dt.clone(),
        }
    })
}

pub fn rk4_transition(
    acceleration: Rc<dyn Fn(Self) -> VectorC<R, E>>,
    dt: R,
) -> Rc<dyn Fn(Self) -> Self + 'a> {
    let one_over_three = R::one() / from_integral(Int::of(3));
    let one_over_six = R::one() / from_integral(Int::of(6));
    let mut c = VectorC::default();
    c[(2, 1)] = R::half();
    c[(3, 1)] = R::half();
    c[(4, 1)] = R::one();
    let mut a = Matrix::default();
    a[(2, 1)] = R::half();
    a[(3, 2)] = R::half();
    a[(4, 3)] = R::one();
    let mut b = VectorC::default();
    b[(1, 1)] = one_over_six.clone();
    b[(2, 1)] = one_over_three.clone();
    b[(3, 1)] = one_over_three;
    b[(4, 1)] = one_over_six;
    Self::general_rk_transition::<4>(acceleration, dt, c, a, b)
}

不过多数情况下,欧拉-克罗默方法的精度就已经足够了

步长参考值欧拉-克罗默四阶龙格-库塔
0.12980.95798704172832978.8692643199732980.9576765056436
0.012980.95798704172832980.97308751586022980.9579870104103

范德波尔振荡器

这里再展示一个求解微分方程的例子,即范德波尔振荡器:

类比牛顿第二定律,假设质量和劲度系数都为 ,此时合力可以看成:

则对于 有如下代码:

use std::rc::Rc;

use phyrs::newton::particle::Particle;
use plotters::{
    chart::{ChartBuilder, SeriesLabelPosition},
    prelude::{BitMapBackend, IntoDrawingArea, PathElement},
    series::LineSeries,
    style::{
        Color,
        full_palette::{
            BLUE_400, BLUEGREY_50, BLUEGREY_400, BROWN_800, GREEN_400, RED_400, YELLOW_50,
        },
    },
};
use rmatrix_ks::{
    matrix::vector::{VectorC, basis_vector},
    number::{
        instances::double::Double,
        traits::{floating::Floating, one::One, zero::Zero},
    },
};

fn main() {
    run().unwrap()
}

fn run() -> Option<()> {
    // 初始状态
    let state = Particle::<Double, 1>::without_charge(
        Double::one(),
        VectorC::of(&[Double::of(2.0)])?,
        VectorC::default(),
        Double::zero(),
    );
    // 加速度
    let total_acc = |mu: f64| -> Rc<dyn Fn(Particle<Double, 1>) -> VectorC<Double, 1>> {
        Rc::new(move |s: Particle<Double, 1>| -> VectorC<Double, 1> {
            (basis_vector(1)
                * Double::of(mu)
                * (Double::one() - s.position[(1, 1)].clone().power(Double::of(2.0)))
                * s.velocity[(1, 1)].clone()
                - s.position)
                / s.mass
        })
    };
    // 模拟计算
    let states0 = state
        .clone()
        .simulate(Particle::rk45_transition(
            total_acc(0.0),
            Double::of(0.015625),
        ))
        .take_while(|s| s.time <= Double::of(512.0));
    let states2 = state
        .clone()
        .simulate(Particle::rk45_transition(
            total_acc(2.0),
            Double::of(0.015625),
        ))
        .take_while(|s| s.time <= Double::of(512.0));
    let states4 = state
        .clone()
        .simulate(Particle::rk45_transition(
            total_acc(4.0),
            Double::of(0.015625),
        ))
        .take_while(|s| s.time <= Double::of(512.0));
    let states6 = state
        .simulate(Particle::rk45_transition(
            total_acc(6.0),
            Double::of(0.015625),
        ))
        .take_while(|s| s.time <= Double::of(512.0));
    // 绘图
    let draw_area = BitMapBackend::new("result/draw_vanderpol.png", (960, 512)).into_drawing_area();
    draw_area.fill(&YELLOW_50).ok()?;
    let mut chart = ChartBuilder::on(&draw_area)
        .margin(10)
        .caption(
            "范德波尔振荡器",
            ("Zhuque Fangsong (technical preview)", 48),
        )
        .x_label_area_size(48)
        .y_label_area_size(64)
        .build_cartesian_2d(-2.4..2.4, -10.0..10.0)
        .ok()?;
    chart
        .configure_mesh()
        .axis_desc_style(("Zhuque Fangsong (technical preview)", 24))
        .x_desc("位置")
        .y_desc("速度")
        .draw()
        .ok()?;
    chart
        .draw_series(LineSeries::new(
            states0.map(|s| (s.position[(1, 1)].raw(), s.velocity[(1, 1)].raw())),
            &BLUE_400,
        ))
        .ok()?
        .label("mu = 0")
        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 32, y)], BLUE_400));

    chart
        .draw_series(LineSeries::new(
            states2.map(|s| (s.position[(1, 1)].raw(), s.velocity[(1, 1)].raw())),
            &RED_400,
        ))
        .ok()?
        .label("mu = 2")
        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 32, y)], RED_400));

    chart
        .draw_series(LineSeries::new(
            states4.map(|s| (s.position[(1, 1)].raw(), s.velocity[(1, 1)].raw())),
            &GREEN_400,
        ))
        .ok()?
        .label("mu = 4")
        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 32, y)], GREEN_400));

    chart
        .draw_series(LineSeries::new(
            states6.map(|s| (s.position[(1, 1)].raw(), s.velocity[(1, 1)].raw())),
            &BLUEGREY_400,
        ))
        .ok()?
        .label("mu = 6")
        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 32, y)], BLUEGREY_400));

    chart
        .configure_series_labels()
        .border_style(BROWN_800)
        .label_font(("Zhuque Fangsong (technical preview)", 16))
        .legend_area_size(48)
        .background_style(BLUEGREY_50.mix(0.8))
        .position(SeriesLabelPosition::UpperLeft)
        .draw()
        .ok()?;

    draw_area.present().ok()?;

    Some(())
}

对应位置和速度的相图如下:

范德波尔振荡器

有了龙格-库塔方法,大部分的常微分方程都可以尝试求解了

不过更重要的是知道如何转化物理问题到微分方程,包括如何确定状态转移函数


  1. Semi-implicit Euler method.Wikipedia [DB/OL].(2025-04-15)[2025-07-12]. https://en.wikipedia.org/wiki/Semi-implicit_Euler_method

  2. Runge–Kutta methods.Wikipedia [DB/OL].(2025-07-07)[2025-07-12]. https://en.wikipedia.org/wiki/Runge–Kutta_methods