如何在 C++ 中进行数值积分的基本示例

Basic example of how to do numerical integration in C++

我想大多数人都知道如何在计算机编程中进行数值推导,(作为极限 --> 0;阅读:“作为极限接近零”)。

//example code for derivation of position over time to obtain velocity

float currPosition, prevPosition, currTime, lastTime, velocity;

while (true)
{
    prevPosition = currPosition;
    currPosition = getNewPosition();

    lastTime = currTime;
    currTime = getTimestamp();

    // Numerical derivation of position over time to obtain velocity
    velocity = (currPosition - prevPosition)/(currTime - lastTime);
}

// since the while loop runs at the shortest period of time, we've already
// achieved limit --> 0;

这是大多数推导程序的基本构建块。

我如何使用积分来做到这一点?我是使用 for 循环并添加还是什么?

物理、制图、机器人、游戏、dead-reckoning 和控制的代码中的数值推导和集成

请注意我在下面使用“估计”和“测量”这两个词的地方。区别很重要。

  1. 测量值 是来自传感器的直接读数。
    1. 例如:GPS 直接测量 position(米),速度计直接测量 speed(m/s)。
  2. Estimates 计算出的预测 您可以通过积分和推导(导出)测量值来获得。 例如:您可以 推导 相对于时间的位置测量值 (m) 以获得速度或速度 (m/s) 估计值,并且您可以相对于时间积分速度或速度测量值(m/s)以获得位置或位移(m)估计.

例如下面的table是正确的。例如,阅读第二行:“如果你对速度测量值求导数,你会得到一个加速度估计值,如果你求其积分,你会得到一个位置估计值。”

Derivatives and integrals of position

Measurement, y              Derivative                  Integral
                            Estimate (dy/dt)            Estimate (dy*dt)
-----------------------     -----------------------     -----------------------
position        [m]         velocity        [m/s]       -               [m*s]
velocity        [m/s]       acceleration    [m/s^2]     position        [m]      
acceleration    [m/s^2]     jerk            [m/s^3]     velocity        [m/s]
jerk            [m/s^3]     snap            [m/s^4]     acceleration    [m/s^2]
snap            [m/s^4]     crackle         [m/s^5]     jerk            [m/s^3]
crackle         [m/s^5]     pop             [m/s^6]     snap            [m/s^4]
pop             [m/s^6]     -               [m/s^7]     crackle         [m/s^5]

对于急动、啪嗒声或颠簸、噼啪声和砰砰声,请参见:https://en.wikipedia.org/wiki/Fourth,_fifth,_and_sixth_derivatives_of_position

1。数值推导

请记住,推导会在 x-y 图上获得直线 dy/dx 斜率 。一般形式为(y_new - y_old)/(x_new - x_old).

为了从您获得重复位置测量的系统中获得速度估计(例如:您正在获取 GPS 读数周期性地),您必须随着时间的推移以数字方式推导您的位置测量值。你的y-axis是位置,你的x-axis是时间,所以dy/dx就是(position_new - position_old)/(time_new - time_old).单位检查显示这可能是 meters/sec,这确实是速度单位。

在代码中,对于仅在一维中测量位置的系统,它看起来像这样:

double position_new_m = getPosition(); // m = meters
double position_old_m;
// `getNanoseconds()` should return a `uint64_t timestamp in nanoseconds, for
// instance
double time_new_sec = NS_TO_SEC((double)getNanoseconds());
double time_old_sec;

while (true)
{
    position_old_m = position_new_m;
    position_new_m = getPosition();

    time_old_sec = time_new_sec;
    time_new_sec = NS_TO_SEC((double)getNanoseconds());

    // Numerical derivation of position measurements over time to obtain
    // velocity in meters per second (mps)
    double velocity_mps = 
        (position_new_m - position_old_m)/(time_new_sec - time_old_sec);
}

2。数值积分

数值积分在 x-y 图上获得 曲线下的面积 dy*dx。执行此操作的最佳方法之一称为 梯形积分 ,其中您取平均 dy 读数并乘以 dx。这看起来像这样:(y_old + y_new)/2 * (x_new - x_old).

为了从您正在获得重复速度测量值的系统中获得位置估计(例如:您正在尝试估计仅读取汽车上的速度表时行驶的距离),您必须随时间对速度测量值进行数值积分。你的y-axis是速度,你的x-axis是时间,所以(y_old + y_new)/2 * (x_new - x_old)就是velocity_old + velocity_new)/2 * (time_new - time_old).单位检查显示这可能是 meters/sec * sec = meters,这确实是距离单位。

在代码中,它看起来像这样。请注意,数值积分获得了在那个微小的时间间隔内行进的距离。要获得 行进距离的估计值,您必须将所有单独的行进距离估计值相加。

double velocity_new_mps = getVelocity(); // mps = meters per second
double velocity_old_mps;
// `getNanoseconds()` should return a `uint64_t timestamp in nanoseconds, for
// instance
double time_new_sec = NS_TO_SEC((double)getNanoseconds());
double time_old_sec;

// Total meters traveled
double distance_traveled_m_total = 0;

while (true)
{
    velocity_old_mps = velocity_new_mps;
    velocity_new_mps = getVelocity();

    time_old_sec = time_new_sec;
    time_new_sec = NS_TO_SEC((double)getNanoseconds());

    // Numerical integration of velocity measurements over time to obtain 
    // a distance estimate (in meters) over this time interval
    double distance_traveled_m = 
        (velocity_old_mps + velocity_new_mps)/2 * (time_new_sec - time_old_sec);
    distance_traveled_m_total += distance_traveled_m;
}

另请参阅:https://en.wikipedia.org/wiki/Numerical_integration

更进一步:

high-resolution 时间戳

要执行上述操作,您需要一种获取时间戳的好方法。以下是我使用的各种技术:

在 C++ 中,使用 my uint64_t nanos() function here

如果在 C C++ 中使用 Linux,请在此处使用 my uint64_t nanos() function which uses clock_gettime() here. Even better, I have wrapped it up into a nice timinglib library for Linux, in my eRCaGuy_hello_world 存储库:

  1. timinglib.h
  2. timinglib.c

这是来自 timing.h 的 NS_TO_SEC() 宏:

#define NS_PER_SEC (1000000000L)
/// Convert nanoseconds to seconds
#define NS_TO_SEC(ns)   ((ns)/NS_PER_SEC)

如果使用微控制器,您需要从定时器或计数器寄存器中读取递增周期计数器,您已将其配置为以稳定、固定的速率递增。例如:在 Arduino 上:使用 micros() 获取具有 4 微秒分辨率的微秒时间戳(默认情况下,它可以更改)。在 STM32 或其他平台上,您需要配置自己的 timer/counter.

使用数据采样率

在样本循环中尽可能快地获取数据样本是个好主意,因为这样您可以对许多样本进行平均以实现:

  1. 降低噪声:对许多原始样本进行平均可降低传感器的噪声。
  2. Higher-resolution:平均许多原始样本实际上会增加测量系统的分辨率。这就是所谓的 过采样。
    1. 我把它写在我的个人网站上:ElectricRCAircraftGuy.com: Using the Arduino Uno’s built-in 10-bit to 16+-bit ADC (Analog to Digital Converter)
    2. 并且 Atmel/Microchip 在他们的 white-paper 中写道:Application Note AN8003: AVR121: Enhancing ADC resolution by oversampling
    3. 采用 4^n 样本可将样本分辨率提高 n 位分辨率。例如:
      4^0 = 1    sample  at 10-bits resolution --> 1 10-bit sample
      4^1 = 4    samples at 10-bits resolution --> 1 11-bit sample
      4^2 = 16   samples at 10-bits resolution --> 1 12-bit sample
      4^3 = 64   samples at 10-bits resolution --> 1 13-bit sample
      4^4 = 256  samples at 10-bits resolution --> 1 14-bit sample
      4^5 = 1024 samples at 10-bits resolution --> 1 15-bit sample
      4^6 = 4096 samples at 10-bits resolution --> 1 16-bit sample
      
      参见:

因此,以 高采样率 进行采样是好的。您可以对这些示例进行基本过滤。

如果您以高速率处理原始样本,对 high-sample-rate 个原始样本进行 数值推导 最终会推导很多噪声,它会产生噪声导数估计。这不太好。最好对过滤后的样本进行推导:例如:100 或 1000 个快速样本的平均值。然而,对 high-sample-rate 原始样本进行 数值积分 很好,因为 ,“当积分时,你得到的样本越多,噪声平均效果越好”这与我上面的笔记一致。

然而,仅使用过滤后的样本进行数值积分和数值推导就可以了。

使用合理控制循环速率

控制循环速率不应太快。 采样率越高越好,因为您可以过滤它们以减少噪音。然而,控制循环率越高,不一定越好,因为控制循环率有一个最佳点。如果你的控制回路速率太慢,系统的频率响应就会很慢,并且对环境的响应速度不够快,如果控制回路速率太快,它最终只会响应样本 噪音而不是测量数据的真实变化。

因此,例如,即使您的 采样率 为 1 kHz,要过采样和过滤数据, 控制循环 不需要这么快,因为在非常短的时间间隔内,真实传感器读数的噪声会太大。在 10 Hz ~ 100 Hz 之间的任何位置使用控制回路,对于具有干净数据的简单系统,可能高达 400+ Hz。在某些情况下,您可以走得更快,但 50 Hz 在控制系统中非常常见。 more-complicated 系统 and/or more-noisy 传感器测量,通常, 较慢 控制回路必须下降到大约 1~10 Hz左右。 Self-driving 汽车,例如,非常复杂,经常在 control loops of only 10 Hz.

运行

循环计时和multi-tasking

为了完成上述任务,独立的测量和过滤循环,以及控制循环,您需要一种执行方法精确高效的循环计时和multi-tasking.

如果需要在 C 或 C++Linux 中执行精确的重复循环 Linux,请使用我上面 timinglib 中的 sleep_until_ns() 函数.我在 Linux 中有一个 sleep_until_us() 函数 in-use 的演示,以获得 repetitive loops as fast as 1 KHz to 100 kHz here.

如果在微控制器上使用bare-metal(无操作系统)作为您的计算平台,请使用timestamp-based协同多任务处理 根据需要执行控制循环和其他循环,例如测量循环。在这里查看我的详细答案:.

完整的数值积分和 multi-tasking 示例

我在 bare-metal 系统上使用我的 CREATE_TASK_TIMER() 我的 Full coulomb counter example in code 中的宏。 在我看来,这是一个很好的学习演示。

卡尔曼滤波器

为了进行稳健的测量,您可能需要卡尔曼滤波器,也许是“无味卡尔曼滤波器”或 UKF,因为显然它们是“无味的”,因为它们“不臭”。