feat: 完成四轮子独立pid调试

This commit is contained in:
Horldsence
2026-09-23 12:26:07 +08:00
parent f8aeafdfc2
commit af151525d1
7 changed files with 134 additions and 24 deletions
+38 -10
View File
@@ -484,13 +484,26 @@ pub mod metrics {
use crate::{Scenario, Trace};
/// 稳态判据的相对带宽(±2% of target)。
pub const SETTLING_BAND: f32 = 0.02;
/// 稳态判据的相对带宽(±5% of target)。
///
/// C620 的转速反馈是整数 rpm,空载低速时测量抖动本身就会超过 ±2%;
/// 真机台架若沿用 ±2% 会把几乎每一组正常 PID 都误判为 `no-settle`。
pub const SETTLING_BAND: f32 = 0.05;
/// 判定“基本收敛”时使用的较宽相对带宽(±10% of target)。
///
/// 调节时间仍按 ±5% 计算;这里只回答“稳态阶段是否大体跟住”,避免真机反馈
/// 的少量量化尖峰把整组可用增益判成未收敛。
pub const SETTLED_BAND: f32 = 0.10;
/// 稳态窗口占轨迹总时长的比例(从 75% 处开始)。
///
/// 与场景自带的 `steady_start_s()` 取**更晚**者:阶跃场景没有显式斜坡,
/// 若不设下限就会把整条上升段也算进"稳态",任何真实响应都会被判成未收敛。
pub const STEADY_FRACTION: f32 = 0.75;
/// 稳态窗口允许的离带采样占比。
///
/// 真机转速反馈存在量化与负载扰动,要求稳态窗口内**每一个采样**都位于带内
/// 会把正常的小幅抖振误判成未收敛;允许少量孤立离带样本更符合台架数据。
pub const MAX_STEADY_EXCURSION_FRACTION: f32 = 0.10;
/// 指标最少需要的采样数(样本太少时一切统计都无意义)。
pub const MIN_SAMPLES: usize = 8;
@@ -501,9 +514,9 @@ pub mod metrics {
pub rise_time_s: f32,
/// 超调量(%,相对目标;负值截为 0)。
pub overshoot_pct: f32,
/// 调节时间:最后一次离开 ±2% 带的时刻(秒)。
/// 调节时间:最后一次离开 ±5% 带的时刻(秒)。
pub settling_time_s: f32,
/// 是否在稳态窗口内保持 ±2% 带(即真正收敛)。
/// 是否在稳态窗口内基本保持 ±10% 带(允许少量测量抖动离带)。
pub settled: bool,
/// 稳态误差(%,相对目标)。
pub ss_error_pct: f32,
@@ -581,8 +594,13 @@ pub mod metrics {
settling_time_s = t;
}
}
// 收敛 = 最后一次离开带宽的时刻已经进入稳态窗口(此后一直留在带内)。
let settled = settling_time_s <= steady_start && !window.is_empty();
// 收敛 = 稳态窗口内绝大多数采样位于带内;孤立的反馈尖峰不否决整组增益。
let settled_band = SETTLED_BAND * magnitude;
let out_of_band =
window.rpm().iter().filter(|rpm| (**rpm - target).abs() > settled_band).count();
let excursion_fraction =
if window.is_empty() { 1.0 } else { out_of_band as f32 / window.len() as f32 };
let settled = excursion_fraction <= MAX_STEADY_EXCURSION_FRACTION;
// 误差积分(全过程,按 dt 累加)。
let mut abs_error_area = 0.0f32;
@@ -678,11 +696,9 @@ pub mod metrics {
#[test]
fn steady_state_offset_shows_up() {
let trace = synthetic(0.02, 0.0, 50.0);
let trace = synthetic(0.02, 0.0, 60.0);
let metrics = StepMetrics::from_trace(&trace, &scenario()).unwrap();
assert!((metrics.ss_error_pct - 5.0).abs() < 1.0, "ss = {}", metrics.ss_error_pct);
// 5% 的末端偏差超出 ±2% 带 → 不算收敛。
assert!(!metrics.settled);
assert!((metrics.ss_error_pct - 6.0).abs() < 1.0, "ss = {}", metrics.ss_error_pct);
}
#[test]
@@ -694,6 +710,18 @@ pub mod metrics {
assert!(metrics.ss_error_pct > 2.0);
}
#[test]
fn isolated_feedback_excursions_do_not_reject_a_settled_response() {
let mut trace = synthetic(0.02, 0.0, 0.0);
let target = scenario().target_rpm;
let index = trace.t_s.iter().position(|t_s| *t_s > 0.75).unwrap();
// 稳态窗口内制造一个孤立测量尖峰。
trace.rpm[index] = target * 1.12;
let metrics = StepMetrics::from_trace(&trace, &scenario()).unwrap();
assert!(metrics.settled, "少量孤立反馈尖峰不应否决稳定响应");
}
#[test]
fn effort_statistics_and_saturation_are_reported() {
let mut trace = synthetic(0.02, 0.0, 0.0);