1use crate::core::entropy::jaccard_similarity;
24
25const ALPHA: f64 = 0.5;
27const BETA: f64 = 1.0;
30const SA_ITERS: usize = 4000;
32
33pub fn is_enabled() -> bool {
36 matches!(
37 std::env::var("LEAN_CTX_EXPERIMENTAL_QUBO")
38 .ok()
39 .as_deref()
40 .map(str::trim),
41 Some("1" | "true" | "yes" | "on")
42 )
43}
44
45#[derive(Debug, Clone)]
47pub struct QuboItem {
48 pub id: String,
49 pub phi: f64,
50 pub tokens: usize,
51 pub sketch: String,
53}
54
55struct SplitMix64(u64);
58impl SplitMix64 {
59 fn new(seed: u64) -> Self {
60 Self(seed)
61 }
62 fn next_u64(&mut self) -> u64 {
63 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
64 let mut z = self.0;
65 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
66 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
67 z ^ (z >> 31)
68 }
69 fn next_f64(&mut self) -> f64 {
70 (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
71 }
72 fn next_below(&mut self, bound: usize) -> usize {
73 if bound == 0 {
74 0
75 } else {
76 (self.next_u64() % bound as u64) as usize
77 }
78 }
79}
80
81fn redundancy_matrix(items: &[QuboItem]) -> Vec<Vec<f64>> {
84 let n = items.len();
85 let mut sim = vec![vec![0.0; n]; n];
86 for i in 0..n {
87 for j in (i + 1)..n {
88 let s = jaccard_similarity(&items[i].sketch, &items[j].sketch);
89 sim[i][j] = s;
90 sim[j][i] = s;
91 }
92 }
93 sim
94}
95
96fn energy(items: &[QuboItem], sim: &[Vec<f64>], x: &[bool], budget: usize) -> f64 {
97 let mut e = 0.0;
98 let mut tokens = 0usize;
99 for i in 0..items.len() {
100 if !x[i] {
101 continue;
102 }
103 e -= items[i].phi;
104 tokens += items[i].tokens;
105 for j in (i + 1)..items.len() {
106 if x[j] {
107 e += ALPHA * sim[i][j];
108 }
109 }
110 }
111 let overflow = tokens.saturating_sub(budget) as f64;
112 e + BETA * overflow
113}
114
115pub fn select(items: &[QuboItem], budget: usize) -> Vec<usize> {
119 crate::core::introspect::tick("qubo_select");
120 let n = items.len();
121 if n == 0 {
122 return Vec::new();
123 }
124 let sim = redundancy_matrix(items);
125
126 let mut x = greedy_mask(items, budget);
128 let mut best = x.clone();
129 let mut best_e = energy(items, &sim, &x, budget);
130 let mut cur_e = best_e;
131
132 let mut rng = SplitMix64::new(problem_seed(items, budget));
133 for k in 0..SA_ITERS {
134 let t = (1.0 - (k as f64 / SA_ITERS as f64)).mul_add(0.99, 0.01);
136 let i = rng.next_below(n);
137 x[i] = !x[i];
138 let new_e = energy(items, &sim, &x, budget);
139 let delta = new_e - cur_e;
140 if delta <= 0.0 || rng.next_f64() < (-delta / t).exp() {
141 cur_e = new_e;
142 if new_e < best_e {
143 best_e = new_e;
144 best.clone_from(&x);
145 }
146 } else {
147 x[i] = !x[i]; }
149 }
150
151 best.iter()
152 .enumerate()
153 .filter_map(|(i, &on)| on.then_some(i))
154 .collect()
155}
156
157fn greedy_mask(items: &[QuboItem], budget: usize) -> Vec<bool> {
160 let mut order: Vec<usize> = (0..items.len()).collect();
161 order.sort_by(|&a, &b| {
162 let ea = items[a].phi / items[a].tokens.max(1) as f64;
163 let eb = items[b].phi / items[b].tokens.max(1) as f64;
164 eb.partial_cmp(&ea)
165 .unwrap_or(std::cmp::Ordering::Equal)
166 .then_with(|| items[a].id.cmp(&items[b].id))
167 });
168 let mut mask = vec![false; items.len()];
169 let mut used = 0usize;
170 for i in order {
171 if used + items[i].tokens <= budget {
172 mask[i] = true;
173 used += items[i].tokens;
174 }
175 }
176 mask
177}
178
179fn problem_seed(items: &[QuboItem], budget: usize) -> u64 {
181 let mut h = budget as u64;
182 for it in items {
183 h ^= it.id.bytes().fold(1469598103934665603u64, |acc, b| {
184 (acc ^ u64::from(b)).wrapping_mul(1099511628211)
185 });
186 h = h.wrapping_mul(0x100000001b3).wrapping_add(it.tokens as u64);
187 }
188 h
189}
190
191#[derive(Debug, Clone)]
193pub struct BenchReport {
194 pub items: usize,
195 pub budget: usize,
196 pub greedy_phi: f64,
197 pub greedy_tokens: usize,
198 pub qubo_phi: f64,
199 pub qubo_tokens: usize,
200}
201
202impl BenchReport {
203 pub fn phi_gain_pct(&self) -> f64 {
205 if self.greedy_phi <= 0.0 {
206 return 0.0;
207 }
208 (self.qubo_phi - self.greedy_phi) / self.greedy_phi * 100.0
209 }
210
211 pub fn format(&self) -> String {
212 format!(
213 "QUBO spike (experimental, greedy stays default)\n\
214 items={} budget={}\n\
215 greedy: phi={:.3} tokens={}\n\
216 qubo: phi={:.3} tokens={}\n\
217 phi gain: {:+.1}%",
218 self.items,
219 self.budget,
220 self.greedy_phi,
221 self.greedy_tokens,
222 self.qubo_phi,
223 self.qubo_tokens,
224 self.phi_gain_pct(),
225 )
226 }
227}
228
229fn captured(items: &[QuboItem], idx: &[usize]) -> (f64, usize) {
230 idx.iter().fold((0.0, 0usize), |(p, t), &i| {
231 (p + items[i].phi, t + items[i].tokens)
232 })
233}
234
235pub fn benchmark(items: &[QuboItem], budget: usize) -> BenchReport {
237 let greedy: Vec<usize> = greedy_mask(items, budget)
238 .iter()
239 .enumerate()
240 .filter_map(|(i, &on)| on.then_some(i))
241 .collect();
242 let qubo = select(items, budget);
243 let (greedy_phi, greedy_tokens) = captured(items, &greedy);
244 let (qubo_phi, qubo_tokens) = captured(items, &qubo);
245 BenchReport {
246 items: items.len(),
247 budget,
248 greedy_phi,
249 greedy_tokens,
250 qubo_phi,
251 qubo_tokens,
252 }
253}
254
255pub fn synthetic_problem() -> (Vec<QuboItem>, usize) {
258 let mut items = Vec::new();
259 for cluster in 0..3 {
261 for k in 0..3 {
262 items.push(QuboItem {
263 id: format!("dup{cluster}_{k}"),
264 phi: 0.6,
265 tokens: 300,
266 sketch: format!("cluster {cluster} shared redundant content body"),
267 });
268 }
269 }
270 let unique_sketches = [
273 "kepler orbital mechanics ellipse perihelion",
274 "ribosome translation codon peptide synthesis",
275 "byzantine consensus quorum fault tolerance",
276 "monsoon humidity evaporation precipitation cycle",
277 ];
278 for (u, sketch) in unique_sketches.iter().enumerate() {
279 items.push(QuboItem {
280 id: format!("uniq{u}"),
281 phi: 0.8,
282 tokens: 300,
283 sketch: (*sketch).to_string(),
284 });
285 }
286 (items, 1500)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 fn items() -> Vec<QuboItem> {
294 synthetic_problem().0
295 }
296
297 #[test]
298 fn disabled_by_default() {
299 let _lock = crate::core::data_dir::test_env_lock();
302 crate::test_env::remove_var("LEAN_CTX_EXPERIMENTAL_QUBO");
303 assert!(!is_enabled());
304 }
305
306 #[test]
307 fn selection_respects_budget() {
308 let it = items();
309 let budget = 1500;
310 let sel = select(&it, budget);
311 let (_, tokens) = captured(&it, &sel);
312 assert!(tokens <= budget, "QUBO must not exceed budget: {tokens}");
313 }
314
315 #[test]
316 fn selection_is_deterministic() {
317 let it = items();
319 let a = select(&it, 1500);
320 let b = select(&it, 1500);
321 assert_eq!(a, b, "seeded SA must be reproducible");
322 }
323
324 #[test]
325 fn benchmark_runs_and_reports() {
326 let (it, budget) = synthetic_problem();
327 let report = benchmark(&it, budget);
328 assert_eq!(report.items, it.len());
329 assert!(report.greedy_phi > 0.0);
330 assert!(report.qubo_phi > 0.0);
331 assert!(report.greedy_tokens <= budget);
333 assert!(report.qubo_tokens <= budget);
334 assert!(report.format().contains("QUBO spike"));
336 }
337
338 #[test]
339 fn empty_problem_selects_nothing() {
340 assert!(select(&[], 1000).is_empty());
341 }
342}