1use serde::{Deserialize, Serialize};
9
10use super::domain_types::BranchMeasurement;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct BranchCandidate {
15 pub measurement: BranchMeasurement,
16 pub targeted_improvement: f64,
19}
20
21pub fn select_branch(eligible: &[BranchCandidate]) -> Option<&BranchCandidate> {
27 let profile = &eligible.first()?.measurement.sensor_profile;
28 let comparable: Vec<&BranchCandidate> = eligible
29 .iter()
30 .filter(|candidate| candidate.measurement.sensor_profile == *profile)
31 .collect();
32 comparable.into_iter().min_by(|a, b| {
33 b.measurement
34 .hard_pass
35 .cmp(&a.measurement.hard_pass)
36 .then_with(|| a.measurement.energy.total_cmp(&b.measurement.energy))
37 .then_with(|| b.targeted_improvement.total_cmp(&a.targeted_improvement))
38 .then_with(|| a.measurement.cost.total_cmp(&b.measurement.cost))
39 .then_with(|| a.measurement.branch_id.cmp(&b.measurement.branch_id))
40 })
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct FrontierEntry {
46 pub branch_id: String,
47 pub barrier_blocked: bool,
49 pub unresolved_obligations: u32,
50 pub dominant_residual: f64,
52 pub cumulative_cost: f64,
53 pub checkpoint_depth: u32,
55}
56
57pub fn frontier_order(a: &FrontierEntry, b: &FrontierEntry) -> std::cmp::Ordering {
61 a.barrier_blocked
62 .cmp(&b.barrier_blocked)
63 .then_with(|| a.unresolved_obligations.cmp(&b.unresolved_obligations))
64 .then_with(|| a.dominant_residual.total_cmp(&b.dominant_residual))
65 .then_with(|| a.cumulative_cost.total_cmp(&b.cumulative_cost))
66 .then_with(|| a.checkpoint_depth.cmp(&b.checkpoint_depth))
67 .then_with(|| a.branch_id.cmp(&b.branch_id))
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 fn candidate(
75 branch: &str,
76 hard: bool,
77 energy: f64,
78 improve: f64,
79 cost: f64,
80 ) -> BranchCandidate {
81 BranchCandidate {
82 measurement: BranchMeasurement {
83 branch_id: branch.into(),
84 candidate_id: format!("{branch}/c1"),
85 energy,
86 hard_pass: hard,
87 residuals: vec![],
88 sensor_profile: "profile-1".into(),
89 cost,
90 },
91 targeted_improvement: improve,
92 }
93 }
94
95 fn winner(candidates: Vec<BranchCandidate>) -> String {
96 select_branch(&candidates)
97 .unwrap()
98 .measurement
99 .branch_id
100 .clone()
101 }
102
103 #[test]
104 fn the_five_keys_apply_in_order() {
105 assert_eq!(
107 winner(vec![
108 candidate("a", false, 0.1, 9.0, 0.0),
109 candidate("b", true, 5.0, 0.0, 9.0),
110 ]),
111 "b"
112 );
113 assert_eq!(
115 winner(vec![
116 candidate("a", false, 2.0, 9.0, 0.0),
117 candidate("b", false, 1.0, 0.0, 9.0),
118 ]),
119 "b"
120 );
121 assert_eq!(
123 winner(vec![
124 candidate("a", false, 1.0, 0.5, 0.0),
125 candidate("b", false, 1.0, 0.9, 9.0),
126 ]),
127 "b"
128 );
129 assert_eq!(
131 winner(vec![
132 candidate("a", false, 1.0, 0.5, 3.0),
133 candidate("b", false, 1.0, 0.5, 2.0),
134 ]),
135 "b"
136 );
137 assert_eq!(
139 winner(vec![
140 candidate("b", false, 1.0, 0.5, 2.0),
141 candidate("a", false, 1.0, 0.5, 2.0),
142 ]),
143 "a"
144 );
145 }
146
147 #[test]
148 fn profile_mismatches_are_excluded_not_normalized() {
149 let mut other = candidate("z", false, 0.0, 9.0, 0.0);
150 other.measurement.sensor_profile = "profile-2".into();
151 let candidates = vec![candidate("a", false, 1.0, 0.0, 0.0), other];
152 let picked = select_branch(&candidates).unwrap();
153 assert_eq!(picked.measurement.branch_id, "a");
154 }
155
156 #[test]
157 fn frontier_order_is_total_and_fair() {
158 let entry = |id: &str, blocked, obligations, residual, cost, depth| FrontierEntry {
159 branch_id: id.into(),
160 barrier_blocked: blocked,
161 unresolved_obligations: obligations,
162 dominant_residual: residual,
163 cumulative_cost: cost,
164 checkpoint_depth: depth,
165 };
166 let mut entries = [
167 entry("c", false, 1, 2.0, 1.0, 1),
168 entry("a", true, 0, 0.0, 0.0, 0),
169 entry("b", false, 1, 2.0, 1.0, 1),
170 entry("d", false, 0, 5.0, 9.0, 3),
171 ];
172 entries.sort_by(frontier_order);
173 let order: Vec<&str> = entries.iter().map(|e| e.branch_id.as_str()).collect();
174 assert_eq!(order, ["d", "b", "c", "a"]);
176 }
177}