qualia_core_db/solvers/quantum_optimizers/
mod.rs1use crate::solvers::{SolverConfig, SolverResult, SolverState};
8use core::f64::consts;
9
10#[repr(C)]
12pub struct QAOAAngleOptimizer {
13 pub angles: QAOAAngles,
15 pub angle_updates: QAOAAngles,
17 pub cost_history: [f64; 50],
19 pub gradients: QAOAAngles,
21 pub depth: u8,
23 pub config: SolverConfig,
25 pub solver_state: SolverState,
27}
28
29#[repr(C)]
31pub struct SpsaOptimizer {
32 pub parameters: [f64; 20], pub perturbation: [f64; 20],
36 pub gradient: [f64; 20],
38 pub cost_plus: f64,
40 pub cost_minus: f64,
41 pub ck: f64, pub ak: f64, pub a: f64, pub c: f64, pub alpha: f64, pub gamma: f64, pub num_params: u8,
50 pub config: SolverConfig,
52 pub solver_state: SolverState,
54}
55
56#[repr(C)]
58#[derive(Clone, Copy)]
59pub struct QAOAAngles {
60 pub beta: [f64; 10],
62 pub gamma: [f64; 10],
64}
65
66#[repr(C)]
68#[derive(Clone, Copy)]
69pub struct SpsaGradient {
70 pub components: [f64; 20],
72 pub norm: f64,
74 pub perturbation_norm: f64,
76}
77
78#[repr(C)]
80#[derive(Clone, Copy)]
81pub struct QuantumOptimizerState {
82 pub iteration: u32,
84 pub cost_value: f64,
86 pub converged: bool,
88 pub quantum_calls: u32,
90}
91
92pub trait QuantumCostFunction {
94 fn evaluate_quantum(&self, angles: &QAOAAngles) -> SolverResult<f64>;
96
97 fn evaluate_perturbed(
99 &self,
100 angles: &QAOAAngles,
101 perturbation: &QAOAAngles,
102 ) -> SolverResult<(f64, f64)>;
103
104 fn problem_size(&self) -> u8;
106}
107
108pub trait SpsaCostFunction {
110 fn evaluate(&self, params: &[f64; 20], num_params: u8) -> SolverResult<f64>;
112
113 fn valid_parameters(&self, params: &[f64; 20], num_params: u8) -> bool;
115}
116
117impl QAOAAngleOptimizer {
118 pub fn new(depth: u8, config: SolverConfig) -> Self {
120 Self {
121 angles: QAOAAngles::default(),
122 angle_updates: QAOAAngles::default(),
123 cost_history: [f64::MAX; 50],
124 gradients: QAOAAngles::default(),
125 depth,
126 config,
127 solver_state: SolverState::default(),
128 }
129 }
130
131 pub fn optimize<F>(
133 &mut self,
134 f: &F,
135 initial_angles: QAOAAngles,
136 ) -> SolverResult<QuantumOptimizerState>
137 where
138 F: QuantumCostFunction,
139 {
140 self.angles = initial_angles;
141 self.solver_state.iteration = 0;
142 self.solver_state.set_quantum_calls(0);
143 self.solver_state.converged = false;
144
145 let initial_cost = f.evaluate_quantum(&self.angles)?;
147 self.cost_history[0] = initial_cost;
148 self.solver_state.set_cost_value(initial_cost);
149
150 while self.solver_state.iteration < self.config.max_iterations {
151 self.compute_gradient(f)?;
153
154 self.update_angles()?;
156
157 let new_cost = f.evaluate_quantum(&self.angles)?;
159 self.solver_state.set_cost_value(new_cost);
160 self.solver_state.add_quantum_calls(1);
161
162 let history_idx = (self.solver_state.iteration % 50) as usize;
164 self.cost_history[history_idx] = new_cost;
165
166 if self.check_convergence() {
168 self.solver_state.converged = true;
169 break;
170 }
171
172 self.solver_state.iteration += 1;
173 }
174
175 Ok(QuantumOptimizerState {
176 iteration: self.solver_state.iteration,
177 cost_value: self.solver_state.cost_value(),
178 converged: self.solver_state.converged,
179 quantum_calls: self.solver_state.quantum_calls(),
180 })
181 }
182
183 fn compute_gradient<F>(&mut self, f: &F) -> SolverResult<()>
185 where
186 F: QuantumCostFunction,
187 {
188 let epsilon = 1e-3; for i in 0..self.depth as usize {
192 let mut perturbed_angles = self.angles;
194 perturbed_angles.beta[i] += epsilon;
195
196 let cost_plus = f.evaluate_quantum(&perturbed_angles)?;
197 self.solver_state.add_quantum_calls(1);
198
199 perturbed_angles.beta[i] = self.angles.beta[i] - epsilon;
201
202 let cost_minus = f.evaluate_quantum(&perturbed_angles)?;
203 self.solver_state.add_quantum_calls(1);
204
205 self.gradients.beta[i] = (cost_plus - cost_minus) / (2.0 * epsilon);
207 }
208
209 for i in 0..self.depth as usize {
211 let mut perturbed_angles = self.angles;
213 perturbed_angles.gamma[i] += epsilon;
214
215 let cost_plus = f.evaluate_quantum(&perturbed_angles)?;
216 self.solver_state.add_quantum_calls(1);
217
218 perturbed_angles.gamma[i] = self.angles.gamma[i] - epsilon;
220
221 let cost_minus = f.evaluate_quantum(&perturbed_angles)?;
222 self.solver_state.add_quantum_calls(1);
223
224 self.gradients.gamma[i] = (cost_plus - cost_minus) / (2.0 * epsilon);
226 }
227
228 Ok(())
229 }
230
231 fn update_angles(&mut self) -> SolverResult<()> {
233 let learning_rate = 0.01; for i in 0..self.depth as usize {
237 self.angle_updates.beta[i] = -learning_rate * self.gradients.beta[i];
238 self.angles.beta[i] += self.angle_updates.beta[i];
239
240 self.angles.beta[i] = self.angles.beta[i] % (2.0 * consts::PI);
242 if self.angles.beta[i] < 0.0 {
243 self.angles.beta[i] += 2.0 * consts::PI;
244 }
245 }
246
247 for i in 0..self.depth as usize {
249 self.angle_updates.gamma[i] = -learning_rate * self.gradients.gamma[i];
250 self.angles.gamma[i] += self.angle_updates.gamma[i];
251
252 self.angles.gamma[i] = self.angles.gamma[i] % (2.0 * consts::PI);
254 if self.angles.gamma[i] < 0.0 {
255 self.angles.gamma[i] += 2.0 * consts::PI;
256 }
257 }
258
259 Ok(())
260 }
261
262 fn check_convergence(&self) -> bool {
264 if self.solver_state.iteration < 10 {
265 return false;
266 }
267
268 let recent_window = 5;
270 let start_idx = ((self.solver_state.iteration - recent_window) % 50) as usize;
271 let end_idx = (self.solver_state.iteration % 50) as usize;
272
273 let mut cost_change = 0.0;
274 let mut count = 0;
275
276 for i in 0..recent_window {
277 let idx = (start_idx + i as usize) % 50;
278 cost_change += (self.cost_history[idx] - self.cost_history[end_idx]).abs();
279 count += 1;
280 }
281
282 if count > 0 {
283 cost_change / (count as f64) < self.config.tolerance
284 } else {
285 false
286 }
287 }
288
289 pub fn get_angles(&self) -> QAOAAngles {
291 self.angles
292 }
293
294 pub fn get_gradients(&self) -> QAOAAngles {
296 self.gradients
297 }
298}
299
300impl SpsaOptimizer {
301 pub fn new(num_params: u8, config: SolverConfig) -> Self {
303 Self {
304 parameters: [0.0; 20],
305 perturbation: [0.0; 20],
306 gradient: [0.0; 20],
307 cost_plus: 0.0,
308 cost_minus: 0.0,
309 ck: 0.1,
310 ak: 0.1,
311 a: 0.1,
312 c: 0.1,
313 alpha: 0.602, gamma: 0.101,
315 num_params,
316 config,
317 solver_state: SolverState::default(),
318 }
319 }
320
321 pub fn optimize<F>(
323 &mut self,
324 f: &F,
325 initial_params: &[f64; 20],
326 ) -> SolverResult<QuantumOptimizerState>
327 where
328 F: SpsaCostFunction,
329 {
330 for i in 0..self.num_params as usize {
332 self.parameters[i] = initial_params[i];
333 }
334
335 self.solver_state.iteration = 0;
336 self.solver_state.set_quantum_calls(0);
337 self.solver_state.converged = false;
338
339 let initial_cost = f.evaluate(&self.parameters, self.num_params)?;
341 self.solver_state.set_cost_value(initial_cost);
342
343 while self.solver_state.iteration < self.config.max_iterations {
344 self.update_step_sizes();
346
347 self.generate_perturbation()?;
349
350 self.evaluate_perturbed_costs(f)?;
352 self.solver_state.add_quantum_calls(2);
353
354 self.estimate_gradient()?;
356
357 self.update_parameters()?;
359
360 let new_cost = f.evaluate(&self.parameters, self.num_params)?;
362 self.solver_state.set_cost_value(new_cost);
363 self.solver_state.add_quantum_calls(1);
364
365 if self.check_convergence() {
367 self.solver_state.converged = true;
368 break;
369 }
370
371 self.solver_state.iteration += 1;
372 }
373
374 Ok(QuantumOptimizerState {
375 iteration: self.solver_state.iteration,
376 cost_value: self.solver_state.cost_value(),
377 converged: self.solver_state.converged,
378 quantum_calls: self.solver_state.quantum_calls(),
379 })
380 }
381
382 fn update_step_sizes(&mut self) {
384 let k = self.solver_state.iteration as f64 + 1.0;
385
386 self.ak = self.a / (k + 1.0).powf(self.alpha);
388
389 self.ck = self.c / (k + 1.0).powf(self.gamma);
391 }
392
393 fn generate_perturbation(&mut self) -> SolverResult<()> {
395 for i in 0..self.num_params as usize {
396 let random_bit = self.generate_random_bit();
398 self.perturbation[i] = if random_bit { 1.0 } else { -1.0 };
399 }
400
401 Ok(())
402 }
403
404 fn evaluate_perturbed_costs<F>(&mut self, f: &F) -> SolverResult<()>
406 where
407 F: SpsaCostFunction,
408 {
409 let mut params_plus = self.parameters;
411 let mut params_minus = self.parameters;
412
413 for i in 0..self.num_params as usize {
414 let delta = self.ck * self.perturbation[i];
415 params_plus[i] += delta;
416 params_minus[i] -= delta;
417 }
418
419 self.cost_plus = f.evaluate(¶ms_plus, self.num_params)?;
421 self.cost_minus = f.evaluate(¶ms_minus, self.num_params)?;
422
423 Ok(())
424 }
425
426 fn estimate_gradient(&mut self) -> SolverResult<()> {
428 for i in 0..self.num_params as usize {
429 let denominator = 2.0 * self.ck * self.perturbation[i];
431 if denominator.abs() > 1e-10 {
432 self.gradient[i] = (self.cost_plus - self.cost_minus) / denominator;
433 } else {
434 self.gradient[i] = 0.0;
435 }
436 }
437
438 Ok(())
439 }
440
441 fn update_parameters(&mut self) -> SolverResult<()> {
443 for i in 0..self.num_params as usize {
444 self.parameters[i] -= self.ak * self.gradient[i];
446
447 if !self.valid_parameter_range(self.parameters[i]) {
449 self.parameters[i] = self.clip_parameter(self.parameters[i]);
451 }
452 }
453
454 Ok(())
455 }
456
457 fn valid_parameter_range(&self, param: f64) -> bool {
459 param >= -consts::PI && param <= consts::PI
461 }
462
463 fn clip_parameter(&self, param: f64) -> f64 {
465 param.clamp(-consts::PI, consts::PI)
466 }
467
468 fn check_convergence(&self) -> bool {
470 if self.solver_state.iteration < 10 {
471 return false;
472 }
473
474 self.ak < self.config.tolerance
476 }
477
478 fn generate_random_bit(&self) -> bool {
480 static mut SEED: u64 = 12345;
482 unsafe {
483 SEED = SEED.wrapping_mul(1103515245).wrapping_add(12345);
484 (SEED & 1) == 1
485 }
486 }
487
488 pub fn get_parameters(&self) -> [f64; 20] {
490 let mut result = [0.0; 20];
491 for i in 0..self.num_params as usize {
492 result[i] = self.parameters[i];
493 }
494 result
495 }
496
497 pub fn get_gradient(&self) -> SpsaGradient {
499 let mut gradient = SpsaGradient::default();
500
501 for i in 0..self.num_params as usize {
502 gradient.components[i] = self.gradient[i];
503 }
504
505 let mut norm = 0.0;
507 for i in 0..self.num_params as usize {
508 norm += self.gradient[i] * self.gradient[i];
509 }
510 gradient.norm = norm.sqrt();
511
512 let mut pert_norm = 0.0;
514 for i in 0..self.num_params as usize {
515 pert_norm += self.perturbation[i] * self.perturbation[i];
516 }
517 gradient.perturbation_norm = pert_norm.sqrt();
518
519 gradient
520 }
521}
522
523impl Default for QAOAAngles {
524 fn default() -> Self {
525 Self {
526 beta: [0.0; 10],
527 gamma: [0.0; 10],
528 }
529 }
530}
531
532impl Default for SpsaGradient {
533 fn default() -> Self {
534 Self {
535 components: [0.0; 20],
536 norm: 0.0,
537 perturbation_norm: 0.0,
538 }
539 }
540}
541
542impl Default for QuantumOptimizerState {
543 fn default() -> Self {
544 Self {
545 iteration: 0,
546 cost_value: f64::MAX,
547 converged: false,
548 quantum_calls: 0,
549 }
550 }
551}
552
553impl Default for QAOAAngleOptimizer {
554 fn default() -> Self {
555 Self::new(1, SolverConfig::default())
556 }
557}
558
559impl Default for SpsaOptimizer {
560 fn default() -> Self {
561 Self::new(4, SolverConfig::default())
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 struct MockQuantumCost;
571
572 impl QuantumCostFunction for MockQuantumCost {
573 fn evaluate_quantum(&self, angles: &QAOAAngles) -> SolverResult<f64> {
574 let mut cost = 0.0;
576 for i in 0..10 {
577 cost += (angles.beta[i] - 1.0).powi(2);
578 cost += (angles.gamma[i] - 0.5).powi(2);
579 }
580 Ok(cost)
581 }
582
583 fn evaluate_perturbed(
584 &self,
585 angles: &QAOAAngles,
586 _perturbation: &QAOAAngles,
587 ) -> SolverResult<(f64, f64)> {
588 let cost1 = self.evaluate_quantum(angles)?;
589 let cost2 = self.evaluate_quantum(angles)?;
590 Ok((cost1, cost2))
591 }
592
593 fn problem_size(&self) -> u8 {
594 10
595 }
596 }
597
598 #[test]
599 fn test_qaoa_angle_optimizer() {
600 let mut optimizer = QAOAAngleOptimizer::new(2, SolverConfig::default());
601
602 let initial_angles = QAOAAngles {
603 beta: [0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
604 gamma: [0.25, 0.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
605 };
606
607 let cost_func = MockQuantumCost;
608 let result = optimizer.optimize(&cost_func, initial_angles);
609 assert!(result.is_ok());
610
611 let state = result.unwrap();
612 assert!(state.quantum_calls > 0);
613
614 let optimized_angles = optimizer.get_angles();
615 assert!((optimized_angles.beta[0] - 1.0).abs() < 0.1);
616 assert!((optimized_angles.gamma[0] - 0.5).abs() < 0.1);
617 }
618
619 struct MockSpsaCost;
621
622 impl SpsaCostFunction for MockSpsaCost {
623 fn evaluate(&self, params: &[f64; 20], num_params: u8) -> SolverResult<f64> {
624 let mut cost = 0.0;
626 for i in 0..num_params as usize {
627 let target = (i as f64 + 1.0) * 0.5; cost += (params[i] - target).powi(2);
629 }
630 Ok(cost)
631 }
632
633 fn valid_parameters(&self, params: &[f64; 20], num_params: u8) -> bool {
634 for i in 0..num_params as usize {
635 if params[i] < -consts::PI || params[i] > consts::PI {
636 return false;
637 }
638 }
639 true
640 }
641 }
642
643 #[test]
644 fn test_spsa_optimizer() {
645 let mut optimizer = SpsaOptimizer::new(4, SolverConfig::default());
646
647 let initial_params = [0.1; 20];
648
649 let cost_func = MockSpsaCost;
650 let result = optimizer.optimize(&cost_func, &initial_params);
651 assert!(result.is_ok());
652
653 let state = result.unwrap();
654 assert!(state.quantum_calls > 0);
655
656 let optimized_params = optimizer.get_parameters();
657 assert!(!optimized_params.is_empty());
658 }
663
664 #[test]
665 fn test_zero_allocation_guarantee() {
666 }
671}