Integração Numérica RK4 para Telemetria IoT e Controle de Baixa Latência
Português
Neste artigo, estabelecemos a formulação matemática para a resposta ao degrau de um sistema linear invariante no tempo (LTI) de segunda ordem, integrando sua dinâmica usando o método clássico de Runge-Kutta de 4ª Ordem (RK4).
A equação diferencial ordinária que descreve um sistema massa-mola-amortecedor é dada por:
\[ m \frac{d^2x}{dt^2} + c \frac{dx}{dt} + k x = F(t) \]
Convertendo para o espaço de estados, definimos \(\mathbf{y} = [x, \dot{x}]^T\):
\[ \frac{d}{dt} \begin{bmatrix} x \\ \dot{x} \end{bmatrix} = \begin{bmatrix} \dot{x} \\ -\frac{k}{m}x - \frac{c}{m}\dot{x} + \frac{1}{m}F(t) \end{bmatrix} \]
Nosso foco é implementar o algoritmo numérico em Rust garantindo Cache-Locality, utilizando arrays estáticos [f64; 2] alocados estritamente na stack.
Representação Visual (Manim)
O bloco a seguir renderiza uma abstração matemática em Python utilizando o Manim.
Implementação em Rust (Zero-Allocation)
A implementação a seguir demonstra o algoritmo RK4 com foco na localidade de cache da CPU (L1/L2), evitando a heap.
// Integrador RK4 determinístico focado em CPU Cache-Locality
type State = [f64; 2];
/// Função que descreve a dinâmica do sistema (Espaço de Estados)
fn system_dynamics(t: f64, y: &State, m: f64, c: f64, k: f64, force: f64) -> State {
[
y[1],
-(k / m) * y[0] - (c / m) * y[1] + (force / m),
]
}
/// Passo de integração Runge-Kutta de 4ª Ordem
fn rk4_step(t: f64, y: &State, dt: f64, m: f64, c: f64, k: f64, force: f64) -> State {
let k1 = system_dynamics(t, y, m, c, k, force);
let y_k2 = [y[0] + 0.5 * dt * k1[0], y[1] + 0.5 * dt * k1[1]];
let k2 = system_dynamics(t + 0.5 * dt, &y_k2, m, c, k, force);
let y_k3 = [y[0] + 0.5 * dt * k2[0], y[1] + 0.5 * dt * k2[1]];
let k3 = system_dynamics(t + 0.5 * dt, &y_k3, m, c, k, force);
let y_k4 = [y[0] + dt * k3[0], y[1] + dt * k3[1]];
let k4 = system_dynamics(t + dt, &y_k4, m, c, k, force);
[
y[0] + (dt / 6.0) * (k1[0] + 2.0 * k2[0] + 2.0 * k3[0] + k4[0]),
y[1] + (dt / 6.0) * (k1[1] + 2.0 * k2[1] + 2.0 * k3[1] + k4[1]),
]
}
fn main() {
let m = 1.0;
let c = 0.5;
let k = 2.0;
let force = 1.0; // Degrau unitário
let mut y: State = [0.0, 0.0]; // Estado inicial [posição, velocidade]
let dt = 0.01;
println!("t,x,v");
for i in 0..1000 {
let t = (i as f64) * dt;
println!("{:.3},{:.6},{:.6}", t, y[0], y[1]);
y = rk4_step(t, &y, dt, m, c, k, force);
}
}Como observado no código Rust, utilizamos primitivas de arrays de tamanho fixo [f64; 2] alocadas na stack. Essa abordagem garante a previsibilidade dos acessos em memória e evita a sobrecarga associada ao coletor ou ao alocador de memória na heap. O determinismo temporal e a latência de acesso à CPU tornam-se constantes, propriedades ideais para sistemas críticos de tempo real (Hard Real-Time Systems).