1use std::f64::consts::PI;
26
27#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct C {
33 pub re: f64,
34 pub im: f64,
35}
36
37impl C {
38 pub const ZERO: C = C { re: 0.0, im: 0.0 };
39 pub const ONE: C = C { re: 1.0, im: 0.0 };
40 pub const I: C = C { re: 0.0, im: 1.0 };
42
43 pub fn new(re: f64, im: f64) -> C {
44 C { re, im }
45 }
46 pub fn real(re: f64) -> C {
47 C { re, im: 0.0 }
48 }
49 pub fn conj(self) -> C {
50 C { re: self.re, im: -self.im }
51 }
52 pub fn norm_sqr(self) -> f64 {
54 self.re * self.re + self.im * self.im
55 }
56}
57
58impl std::ops::Add for C {
59 type Output = C;
60 fn add(self, o: C) -> C {
61 C { re: self.re + o.re, im: self.im + o.im }
62 }
63}
64impl std::ops::Mul for C {
65 type Output = C;
66 fn mul(self, o: C) -> C {
67 C {
69 re: self.re * o.re - self.im * o.im,
70 im: self.re * o.im + self.im * o.re,
71 }
72 }
73}
74impl std::ops::Neg for C {
75 type Output = C;
76 fn neg(self) -> C {
77 C { re: -self.re, im: -self.im }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum EncodingScheme {
87 Amplitude,
90 Angle,
93}
94
95#[derive(Debug, Clone, PartialEq)]
97pub struct StateVector {
98 pub n: usize,
99 pub amps: Vec<C>,
100}
101
102impl StateVector {
103 pub fn norm_sqr(&self) -> f64 {
105 self.amps.iter().map(|a| a.norm_sqr()).sum()
106 }
107}
108
109#[derive(Debug, Clone)]
113pub struct RotationLayer {
114 pub ry: Vec<f64>,
115 pub rz: Vec<f64>,
116}
117
118#[derive(Debug, Clone, Default)]
120pub struct VariationalCircuit {
121 pub layers: Vec<RotationLayer>,
122}
123
124#[derive(Debug, Clone, Default)]
127pub struct PauliSum {
128 pub terms: Vec<(f64, String)>,
131}
132
133#[derive(Debug, Clone, PartialEq)]
136pub enum QuantError {
137 CapacityExceeded { requested: usize, cap: usize },
139 NotNormalized { norm: f64 },
141 DimensionMismatch { detail: String },
144 BadPauli { pauli: String, bad: char },
146}
147
148impl QuantError {
149 pub fn code(&self) -> &'static str {
150 match self {
151 QuantError::CapacityExceeded { .. } => "axon-E0783",
152 QuantError::NotNormalized { .. } => "axon-E0788",
153 QuantError::DimensionMismatch { .. } => "axon-E0789",
154 QuantError::BadPauli { .. } => "axon-E0785",
155 }
156 }
157}
158
159impl std::fmt::Display for QuantError {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 match self {
162 QuantError::CapacityExceeded { requested, cap } => write!(
163 f,
164 "axon-E0783 quant: capacity exceeded — requested {requested} qubits (D = 2^{requested}), \
165 the OSS reference simulator caps n ≤ {cap}; use an enterprise QuantBackend for larger registers."
166 ),
167 QuantError::NotNormalized { norm } => write!(
168 f,
169 "axon-E0788 quant: amplitude encoding requires a unit-norm input (‖x‖₂ = 1), got ‖x‖₂ = {norm:.6}."
170 ),
171 QuantError::DimensionMismatch { detail } => {
172 write!(f, "axon-E0789 quant: dimension mismatch — {detail}")
173 }
174 QuantError::BadPauli { pauli, bad } => write!(
175 f,
176 "axon-E0785 quant: Pauli string '{pauli}' contains '{bad}' — the closed alphabet is {{I, X, Y, Z}}."
177 ),
178 }
179 }
180}
181
182pub trait QuantBackend {
189 fn capacity(&self) -> usize;
191 fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError>;
193 fn evolve(&self, state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError>;
195 fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError>;
198 fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError>;
200}
201
202pub const OSS_QUBIT_CAP: usize = 10;
208
209const NORM_TOL: f64 = 1e-9;
211
212#[derive(Debug, Clone)]
215pub struct ReferenceSimulator {
216 cap: usize,
217}
218
219impl Default for ReferenceSimulator {
220 fn default() -> Self {
221 ReferenceSimulator { cap: OSS_QUBIT_CAP }
222 }
223}
224
225impl ReferenceSimulator {
226 pub fn new() -> Self {
227 Self::default()
228 }
229
230 fn amplitude_qubits(d: usize) -> usize {
232 let mut n = 0usize;
233 while (1usize << n) < d {
234 n += 1;
235 }
236 n
237 }
238
239 fn apply_1q(amps: &mut [C], q: usize, g: [[C; 2]; 2]) {
241 let bit = 1usize << q;
242 for i in 0..amps.len() {
243 if i & bit == 0 {
244 let j = i | bit;
245 let a0 = amps[i];
246 let a1 = amps[j];
247 amps[i] = g[0][0] * a0 + g[0][1] * a1;
248 amps[j] = g[1][0] * a0 + g[1][1] * a1;
249 }
250 }
251 }
252
253 pub fn reupload_encode(&self, x: &[f64], layers: usize) -> Result<StateVector, QuantError> {
263 let n = x.len();
264 if n == 0 {
265 return Err(QuantError::DimensionMismatch {
266 detail: "empty input vector".to_string(),
267 });
268 }
269 if n > self.cap {
270 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
271 }
272 let mut amps = vec![C::ZERO; 1usize << n];
274 amps[0] = C::real(1.0);
275 for _ in 0..layers.max(1) {
276 for (q, &angle) in x.iter().enumerate() {
278 let ry = [
279 [C::real((angle / 2.0).cos()), C::real(-(angle / 2.0).sin())],
280 [C::real((angle / 2.0).sin()), C::real((angle / 2.0).cos())],
281 ];
282 Self::apply_1q(&mut amps, q, ry);
283 }
284 for q in 0..n.saturating_sub(1) {
287 Self::apply_cnot(&mut amps, q, q + 1);
288 }
289 }
290 Ok(StateVector { n, amps })
291 }
292
293 fn apply_cnot(amps: &mut [C], c: usize, t: usize) {
295 let cb = 1usize << c;
296 let tb = 1usize << t;
297 for i in 0..amps.len() {
298 if i & cb != 0 && i & tb == 0 {
299 amps.swap(i, i | tb);
300 }
301 }
302 }
303
304 fn apply_pauli(amps: &mut [C], q: usize, p: char) -> Result<(), char> {
306 let bit = 1usize << q;
307 match p {
308 'I' => {}
309 'X' => {
310 for i in 0..amps.len() {
311 if i & bit == 0 {
312 amps.swap(i, i | bit);
313 }
314 }
315 }
316 'Z' => {
317 for amp in amps.iter_mut().enumerate().filter(|(i, _)| i & bit != 0).map(|(_, a)| a) {
318 *amp = -*amp;
319 }
320 }
321 'Y' => {
322 for i in 0..amps.len() {
324 if i & bit == 0 {
325 let j = i | bit;
326 let a0 = amps[i];
327 let a1 = amps[j];
328 amps[i] = (-C::I) * a1;
329 amps[j] = C::I * a0;
330 }
331 }
332 }
333 other => return Err(other),
334 }
335 Ok(())
336 }
337
338 fn inner(a: &[C], b: &[C]) -> C {
340 a.iter()
341 .zip(b.iter())
342 .fold(C::ZERO, |acc, (x, y)| acc + x.conj() * *y)
343 }
344}
345
346impl QuantBackend for ReferenceSimulator {
347 fn capacity(&self) -> usize {
348 self.cap
349 }
350
351 fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError> {
352 if x.is_empty() {
353 return Err(QuantError::DimensionMismatch {
354 detail: "empty input vector".to_string(),
355 });
356 }
357 match scheme {
358 EncodingScheme::Amplitude => {
359 let n = Self::amplitude_qubits(x.len());
360 if n > self.cap {
361 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
362 }
363 let norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
365 if (norm - 1.0).abs() > NORM_TOL {
366 return Err(QuantError::NotNormalized { norm });
367 }
368 let mut amps = vec![C::ZERO; 1usize << n];
369 for (i, &v) in x.iter().enumerate() {
370 amps[i] = C::real(v);
371 }
372 Ok(StateVector { n, amps })
373 }
374 EncodingScheme::Angle => {
375 let n = x.len();
376 if n > self.cap {
377 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
378 }
379 let mut amps = vec![C::ZERO; 1usize << n];
381 for (idx, amp) in amps.iter_mut().enumerate() {
382 let mut coeff = 1.0f64;
383 for (q, &angle) in x.iter().enumerate() {
384 let bit = (idx >> q) & 1;
385 coeff *= if bit == 0 { (angle / 2.0).cos() } else { (angle / 2.0).sin() };
386 }
387 *amp = C::real(coeff);
388 }
389 Ok(StateVector { n, amps })
390 }
391 }
392 }
393
394 fn evolve(&self, mut state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError> {
395 let n = state.n;
396 for (li, layer) in circuit.layers.iter().enumerate() {
397 if layer.ry.len() != n || layer.rz.len() != n {
398 return Err(QuantError::DimensionMismatch {
399 detail: format!(
400 "layer {li} has {}/{} rotation angles but the register has {n} qubits",
401 layer.ry.len(),
402 layer.rz.len()
403 ),
404 });
405 }
406 for q in 0..n {
408 let ty = layer.ry[q];
409 let ry = [
410 [C::real((ty / 2.0).cos()), C::real(-(ty / 2.0).sin())],
411 [C::real((ty / 2.0).sin()), C::real((ty / 2.0).cos())],
412 ];
413 Self::apply_1q(&mut state.amps, q, ry);
414 let tz = layer.rz[q];
415 let rz = [
416 [C::new((tz / 2.0).cos(), -(tz / 2.0).sin()), C::ZERO],
417 [C::ZERO, C::new((tz / 2.0).cos(), (tz / 2.0).sin())],
418 ];
419 Self::apply_1q(&mut state.amps, q, rz);
420 }
421 for q in 0..n.saturating_sub(1) {
423 Self::apply_cnot(&mut state.amps, q, q + 1);
424 }
425 }
426 Ok(state)
427 }
428
429 fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError> {
430 let n = state.n;
431 let mut expectation = 0.0f64;
432 for (coeff, pauli) in &observable.terms {
433 if pauli.chars().count() != n {
434 return Err(QuantError::DimensionMismatch {
435 detail: format!(
436 "Pauli string '{pauli}' spans {} qubit(s) but the state has {n}",
437 pauli.chars().count()
438 ),
439 });
440 }
441 let mut phi = state.amps.clone();
443 for (q, p) in pauli.chars().enumerate() {
444 Self::apply_pauli(&mut phi, q, p)
445 .map_err(|bad| QuantError::BadPauli { pauli: pauli.clone(), bad })?;
446 }
447 expectation += coeff * Self::inner(&state.amps, &phi).re;
448 }
449 Ok(expectation)
450 }
451
452 fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError> {
453 if a.n != b.n {
454 return Err(QuantError::DimensionMismatch {
455 detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
456 });
457 }
458 Ok(Self::inner(&a.amps, &b.amps).norm_sqr())
459 }
460}
461
462impl ReferenceSimulator {
463 pub fn polynomial_kernel(a: &StateVector, b: &StateVector, degree: u32) -> Result<f64, QuantError> {
470 if a.n != b.n {
471 return Err(QuantError::DimensionMismatch {
472 detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
473 });
474 }
475 Ok(Self::inner(&a.amps, &b.amps).re.powi(degree as i32))
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 fn approx(a: f64, b: f64) -> bool {
485 (a - b).abs() < 1e-9
486 }
487
488 #[test]
489 fn amplitude_qubits_is_ceil_log2() {
490 assert_eq!(ReferenceSimulator::amplitude_qubits(1), 0);
491 assert_eq!(ReferenceSimulator::amplitude_qubits(2), 1);
492 assert_eq!(ReferenceSimulator::amplitude_qubits(3), 2);
493 assert_eq!(ReferenceSimulator::amplitude_qubits(4), 2);
494 assert_eq!(ReferenceSimulator::amplitude_qubits(1024), 10);
495 assert_eq!(ReferenceSimulator::amplitude_qubits(1025), 11);
496 }
497
498 #[test]
499 fn capacity_cap_is_enforced_with_e0783() {
500 let sim = ReferenceSimulator::new();
501 let x = vec![0.0; 1025];
503 let err = sim.encode(&x, EncodingScheme::Amplitude).unwrap_err();
504 assert!(matches!(err, QuantError::CapacityExceeded { requested: 11, cap: 10 }));
505 assert_eq!(err.code(), "axon-E0783");
506 }
507
508 #[test]
509 fn amplitude_encode_requires_unit_norm() {
510 let sim = ReferenceSimulator::new();
511 let ok = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
513 assert_eq!(ok.n, 1);
514 assert!(approx(ok.norm_sqr(), 1.0));
515 let err = sim.encode(&[1.0, 1.0], EncodingScheme::Amplitude).unwrap_err();
517 assert!(matches!(err, QuantError::NotNormalized { .. }));
518 assert_eq!(err.code(), "axon-E0788");
519 }
520
521 #[test]
522 fn angle_encode_is_unit_norm_product_state() {
523 let sim = ReferenceSimulator::new();
524 let s0 = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
526 assert!(approx(s0.amps[0].re, 1.0) && approx(s0.amps[1].re, 0.0));
527 let s1 = sim.encode(&[PI], EncodingScheme::Angle).unwrap();
529 assert!(approx(s1.amps[0].re, 0.0) && approx(s1.amps[1].re, 1.0));
530 assert!(approx(s1.norm_sqr(), 1.0));
531 }
532
533 #[test]
534 fn ry_pi_flips_zero_to_one() {
535 let sim = ReferenceSimulator::new();
536 let s = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
538 let circuit = VariationalCircuit {
539 layers: vec![RotationLayer { ry: vec![PI], rz: vec![0.0] }],
540 };
541 let out = sim.evolve(s, &circuit).unwrap();
542 assert!(approx(out.amps[1].norm_sqr(), 1.0));
544 assert!(approx(out.amps[0].norm_sqr(), 0.0));
545 }
546
547 #[test]
548 fn measure_pauli_z_eigenvalues() {
549 let sim = ReferenceSimulator::new();
550 let z = PauliSum { terms: vec![(1.0, "Z".to_string())] };
551 let s0 = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
553 assert!(approx(sim.measure(&s0, &z).unwrap(), 1.0));
554 let s1 = sim.encode(&[PI], EncodingScheme::Angle).unwrap();
556 assert!(approx(sim.measure(&s1, &z).unwrap(), -1.0));
557 }
558
559 #[test]
560 fn measure_zz_on_two_qubits() {
561 let sim = ReferenceSimulator::new();
562 let zz = PauliSum { terms: vec![(1.0, "ZZ".to_string())] };
563 let s00 = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap();
565 assert!(approx(sim.measure(&s00, &zz).unwrap(), 1.0));
566 let s01 = sim.encode(&[PI, 0.0], EncodingScheme::Angle).unwrap();
568 assert!(approx(sim.measure(&s01, &zz).unwrap(), -1.0));
569 }
570
571 #[test]
572 fn measure_rejects_wrong_length_pauli() {
573 let sim = ReferenceSimulator::new();
574 let s = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap(); let bad = PauliSum { terms: vec![(1.0, "Z".to_string())] }; assert!(matches!(sim.measure(&s, &bad), Err(QuantError::DimensionMismatch { .. })));
577 }
578
579 #[test]
580 fn measure_rejects_bad_pauli_alphabet() {
581 let sim = ReferenceSimulator::new();
582 let s = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
583 let bad = PauliSum { terms: vec![(1.0, "K".to_string())] };
584 let err = sim.measure(&s, &bad).unwrap_err();
585 assert!(matches!(err, QuantError::BadPauli { bad: 'K', .. }));
586 }
587
588 #[test]
589 fn kernel_fidelity_identical_and_orthogonal() {
590 let sim = ReferenceSimulator::new();
591 let a = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
592 assert!(approx(sim.kernel(&a, &a).unwrap(), 1.0));
594 let e0 = sim.encode(&[1.0, 0.0], EncodingScheme::Amplitude).unwrap();
596 let e1 = sim.encode(&[0.0, 1.0], EncodingScheme::Amplitude).unwrap();
597 assert!(approx(sim.kernel(&e0, &e1).unwrap(), 0.0));
598 }
599
600 #[test]
601 fn reupload_changes_the_feature_map_and_stays_a_valid_kernel() {
602 let sim = ReferenceSimulator::new();
606 let x = [0.5, 1.2, 0.3];
607 let y = [1.0, 0.2, 0.9];
608 let k1 = sim
609 .kernel(
610 &sim.reupload_encode(&x, 1).unwrap(),
611 &sim.reupload_encode(&y, 1).unwrap(),
612 )
613 .unwrap();
614 let k2 = sim
615 .kernel(
616 &sim.reupload_encode(&x, 2).unwrap(),
617 &sim.reupload_encode(&y, 2).unwrap(),
618 )
619 .unwrap();
620 assert!(
621 (k1 - k2).abs() > 1e-3,
622 "re-uploading must change the kernel (escape the single-layer bound): k1={k1}, k2={k2}"
623 );
624 let sx = sim.reupload_encode(&x, 2).unwrap();
626 assert!(approx(sim.kernel(&sx, &sx).unwrap(), 1.0));
627 assert!(k2 >= -1e-9 && k2 <= 1.0 + 1e-9);
628 }
629
630 #[test]
631 fn polynomial_kernel_is_dot_to_the_degree() {
632 let sim = ReferenceSimulator::new();
634 let a = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
635 let b = sim.encode(&[1.0, 0.0], EncodingScheme::Amplitude).unwrap();
636 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 0).unwrap(), 1.0));
637 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 1).unwrap(), 0.6));
638 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 2).unwrap(), 0.36));
639 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 3).unwrap(), 0.216));
640 }
641
642 #[test]
643 fn cnot_entangles_for_bell_correlation() {
644 let sim = ReferenceSimulator::new();
646 let s = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap();
647 let circuit = VariationalCircuit {
648 layers: vec![RotationLayer { ry: vec![PI, 0.0], rz: vec![0.0, 0.0] }],
649 };
650 let out = sim.evolve(s, &circuit).unwrap();
651 let zz = PauliSum { terms: vec![(1.0, "ZZ".to_string())] };
652 assert!(approx(sim.measure(&out, &zz).unwrap(), 1.0), "post-CNOT |11⟩ ⇒ ⟨ZZ⟩ = +1");
653 }
654}