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
@@ -704,10 +704,13 @@ pub mod cost {
}
impl Score {
/// 罚分结果。
/// 没有可用指标时的罚分结果。
///
/// 它必须高于 [`Objective::rejected_cost`] 产生的指标罚值,否则预算耗尽、
/// 安全停机等“未评估”占位结果会反过来击败真实测得的失败候选。
#[must_use]
pub fn penalized(verdict: Verdict) -> Self {
Self { cost: PENALTY, verdict, metrics: None }
Self { cost: PENALTY * 2.0, verdict, metrics: None }
}
}
@@ -757,20 +760,29 @@ pub mod cost {
pub fn score_metrics(&self, metrics: StepMetrics) -> Score {
if metrics.max_rpm > self.max_rpm {
return Score {
cost: PENALTY,
cost: self.rejected_cost(&metrics)
+ f64::from(metrics.max_rpm / self.max_rpm - 1.0).max(0.0) * 10.0,
verdict: Verdict::Overspeed,
metrics: Some(metrics),
};
}
if metrics.saturated_pct / 100.0 > self.max_saturated_fraction {
return Score {
cost: PENALTY,
cost: self.rejected_cost(&metrics)
+ (f64::from(metrics.saturated_pct) / 100.0
- f64::from(self.max_saturated_fraction))
.max(0.0)
* 10.0,
verdict: Verdict::Saturated,
metrics: Some(metrics),
};
}
if !metrics.settled {
return Score { cost: PENALTY, verdict: Verdict::NoSettle, metrics: Some(metrics) };
return Score {
cost: self.rejected_cost(&metrics),
verdict: Verdict::NoSettle,
metrics: Some(metrics),
};
}
Score {
cost: self.weighted_cost(&metrics),
@@ -779,6 +791,14 @@ pub mod cost {
}
}
/// 硬约束失败时的可比较罚值。
///
/// 所有失败解仍高于 [`PENALTY`],但保留指标残差,避免搜索面对一整片完全
/// 同值的“罚值平原”而无法排序、精修或选出最接近可用的候选。
fn rejected_cost(&self, metrics: &StepMetrics) -> f64 {
PENALTY + self.weighted_cost(metrics).max(0.0)
}
/// 归一化加权成本。
#[must_use]
pub fn weighted_cost(&self, metrics: &StepMetrics) -> f64 {
@@ -844,7 +864,15 @@ pub mod cost {
simulate(weak, &PlantParams::gm3508(), &scenario, LoopLimits::gm3508()).unwrap();
let score = objective(scenario).score(&trace);
assert_eq!(score.verdict, Verdict::NoSettle);
assert!((score.cost - PENALTY).abs() < 1.0);
assert!(
score.cost >= PENALTY && score.cost < PENALTY + 100.0,
"失败解应保留可比较的指标残差,cost = {}",
score.cost
);
assert!(
Score::penalized(Verdict::Rejected).cost > score.cost,
"未评估占位结果不得击败真实测得的失败候选"
);
}
#[test]
@@ -2841,8 +2869,8 @@ pub mod search {
//!
//! # 为什么是两阶段
//!
//! 成本函数有一大片"罚值平原"(不收敛 / 饱和的候选成本都等于 `PENALTY`),
//! 纯局部搜索会陷在平原上;而纯网格搜索在四个数量级跨度上需要天文数字的
//! 失败候选位于 `PENALTY` 以上的罚值区带,并携带连续的指标残差;纯局部搜索
//! 仍难跨过成功 / 失败边界,而纯网格搜索在四个数量级跨度上需要天文数字的
//! 评估次数。两阶段各取所长:
//!
//! 1. **粗扫**:Latin Hypercube 分层采样 + `rayon` 并行评估,用有限次数把
@@ -3133,12 +3161,12 @@ pub mod search {
fn cost(&self, param: &Self::Param) -> Result<f64, ArgminError> {
let coords: [f64; AXIS_COUNT] = match param.as_slice() {
[a, b, c, d] => [*a, *b, *c, *d],
_ => return Ok(crate::cost::PENALTY),
_ => return Ok(crate::cost::PENALTY * 2.0),
};
let gains = self.space.from_coords(coords);
let cost = match self.budget.take() {
Some(_) => self.evaluator.evaluate(&gains).cost,
None => crate::cost::PENALTY,
None => crate::cost::PENALTY * 2.0,
};
if cost.is_finite() {
self.tracker.record(cost, coords);