kcan 0.1.4

CAN controller primitives for actuator and motor control.
Documentation
use kcan::{LowPassFilterConfig, PIDConfig, PIDController, SecondOrderLowPassFilter, SolverType};

fn assert_close(left: f64, right: f64, tolerance: f64) {
    assert!(
        (left - right).abs() <= tolerance,
        "left={left} right={right} tolerance={tolerance}"
    );
}

#[test]
fn pid_first_output_is_zero() {
    let mut pid = PIDController::new(PIDConfig::default(), LowPassFilterConfig::default());

    assert_eq!(pid.compute_output(1.0, 1.0, 0.0), 0.0);
}

#[test]
fn pid_proportional_term_only() {
    let mut pid = PIDController::new(
        PIDConfig {
            proportional_gain: 2.0,
            integral_gain: 0.0,
            derivative_gain: 0.0,
            output_limits: Some((-100.0, 100.0)),
        },
        LowPassFilterConfig::default(),
    );

    pid.compute_output(0.0, 0.0, 0.0);
    let output = pid.compute_output(1.0, 1.0, 0.5);

    assert_close(output, 1.0, f64::EPSILON);
}

#[test]
fn pid_clamps_large_error_to_sixty_degrees() {
    let mut pid = PIDController::new(
        PIDConfig {
            proportional_gain: 1.0,
            integral_gain: 0.0,
            derivative_gain: 0.0,
            output_limits: Some((-100.0, 100.0)),
        },
        LowPassFilterConfig::default(),
    );

    pid.compute_output(0.0, 0.0, 0.0);
    let output = pid.compute_output(1.0, 500_f64.to_radians(), 0.0);

    assert_close(output, 60_f64.to_radians(), 1e-12);
}

#[test]
fn pid_output_is_clamped() {
    let mut pid = PIDController::new(
        PIDConfig {
            proportional_gain: 1000.0,
            integral_gain: 0.0,
            derivative_gain: 0.0,
            output_limits: Some((-5.0, 5.0)),
        },
        LowPassFilterConfig::default(),
    );

    pid.compute_output(0.0, 0.0, 0.0);
    assert_eq!(pid.compute_output(1.0, 1.0, 0.0), 5.0);
}

#[test]
fn pid_reset_clears_internal_state() {
    let mut pid = PIDController::default();
    pid.compute_output(0.0, 1.0, 0.0);
    pid.compute_output(1.0, 1.0, 0.0);

    pid.reset();

    assert_eq!(pid.integral(), 0.0);
    assert_eq!(pid.previous_velocity(), 0.0);
}

#[test]
fn low_pass_filter_converges_to_constant_input() {
    let mut filter = SecondOrderLowPassFilter::new(LowPassFilterConfig {
        cut_off_frequency_rad_per_sec: 20.0,
        damping_ratio: 1.0,
        initial_condition: 0.0,
        solver_type: SolverType::RungeKutta,
    });

    let mut output = 0.0;
    for _ in 0..500 {
        (output, _) = filter.step(1.0, 0.01);
    }

    assert_close(output, 1.0, 1e-2);
}

#[test]
fn low_pass_filter_reset_restores_initial_condition() {
    let mut filter = SecondOrderLowPassFilter::new(LowPassFilterConfig {
        cut_off_frequency_rad_per_sec: 20.0,
        damping_ratio: 1.0,
        initial_condition: 2.5,
        solver_type: SolverType::ForwardEuler,
    });

    filter.step(10.0, 0.01);
    filter.reset();

    assert_close(filter.state().filtered_output, 2.5, f64::EPSILON);
    assert_close(filter.state().feedback_state, 2.5, f64::EPSILON);
}