1#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct C {
32 pub re: f64,
33 pub im: f64,
34}
35
36impl C {
37 pub const ZERO: C = C { re: 0.0, im: 0.0 };
38 pub const ONE: C = C { re: 1.0, im: 0.0 };
39 pub const I: C = C { re: 0.0, im: 1.0 };
41
42 pub fn new(re: f64, im: f64) -> C {
43 C { re, im }
44 }
45 pub fn real(re: f64) -> C {
46 C { re, im: 0.0 }
47 }
48 pub fn conj(self) -> C {
49 C { re: self.re, im: -self.im }
50 }
51 pub fn norm_sqr(self) -> f64 {
53 self.re * self.re + self.im * self.im
54 }
55}
56
57impl std::ops::Add for C {
58 type Output = C;
59 fn add(self, o: C) -> C {
60 C { re: self.re + o.re, im: self.im + o.im }
61 }
62}
63impl std::ops::Mul for C {
64 type Output = C;
65 fn mul(self, o: C) -> C {
66 C {
68 re: self.re * o.re - self.im * o.im,
69 im: self.re * o.im + self.im * o.re,
70 }
71 }
72}
73impl std::ops::Neg for C {
74 type Output = C;
75 fn neg(self) -> C {
76 C { re: -self.re, im: -self.im }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum EncodingScheme {
86 Amplitude,
89 Angle,
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub struct StateVector {
97 pub n: usize,
98 pub amps: Vec<C>,
99}
100
101impl StateVector {
102 pub fn norm_sqr(&self) -> f64 {
104 self.amps.iter().map(|a| a.norm_sqr()).sum()
105 }
106}
107
108#[derive(Debug, Clone)]
112pub struct RotationLayer {
113 pub ry: Vec<f64>,
114 pub rz: Vec<f64>,
115}
116
117#[derive(Debug, Clone, Default)]
119pub struct VariationalCircuit {
120 pub layers: Vec<RotationLayer>,
121}
122
123#[derive(Debug, Clone, Default)]
126pub struct PauliSum {
127 pub terms: Vec<(f64, String)>,
130}
131
132#[derive(Debug, Clone, PartialEq)]
135pub enum QuantError {
136 CapacityExceeded { requested: usize, cap: usize },
138 NotNormalized { norm: f64 },
140 DimensionMismatch { detail: String },
143 BadPauli { pauli: String, bad: char },
145}
146
147impl QuantError {
148 pub fn code(&self) -> &'static str {
149 match self {
150 QuantError::CapacityExceeded { .. } => "axon-E0783",
151 QuantError::NotNormalized { .. } => "axon-E0788",
152 QuantError::DimensionMismatch { .. } => "axon-E0789",
153 QuantError::BadPauli { .. } => "axon-E0785",
154 }
155 }
156}
157
158impl std::fmt::Display for QuantError {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 QuantError::CapacityExceeded { requested, cap } => write!(
162 f,
163 "axon-E0783 quant: capacity exceeded — requested {requested} qubits (D = 2^{requested}), \
164 the OSS reference simulator caps n ≤ {cap}; use an enterprise QuantBackend for larger registers."
165 ),
166 QuantError::NotNormalized { norm } => write!(
167 f,
168 "axon-E0788 quant: amplitude encoding requires a unit-norm input (‖x‖₂ = 1), got ‖x‖₂ = {norm:.6}."
169 ),
170 QuantError::DimensionMismatch { detail } => {
171 write!(f, "axon-E0789 quant: dimension mismatch — {detail}")
172 }
173 QuantError::BadPauli { pauli, bad } => write!(
174 f,
175 "axon-E0785 quant: Pauli string '{pauli}' contains '{bad}' — the closed alphabet is {{I, X, Y, Z}}."
176 ),
177 }
178 }
179}
180
181pub trait QuantBackend {
188 fn capacity(&self) -> usize;
190 fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError>;
192 fn evolve(&self, state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError>;
194 fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError>;
197 fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError>;
199}
200
201pub const OSS_QUBIT_CAP: usize = 10;
207
208const NORM_TOL: f64 = 1e-9;
210
211#[derive(Debug, Clone)]
214pub struct ReferenceSimulator {
215 cap: usize,
216}
217
218impl Default for ReferenceSimulator {
219 fn default() -> Self {
220 ReferenceSimulator { cap: OSS_QUBIT_CAP }
221 }
222}
223
224impl ReferenceSimulator {
225 pub fn new() -> Self {
226 Self::default()
227 }
228
229 fn amplitude_qubits(d: usize) -> usize {
231 let mut n = 0usize;
232 while (1usize << n) < d {
233 n += 1;
234 }
235 n
236 }
237
238 fn apply_1q(amps: &mut [C], q: usize, g: [[C; 2]; 2]) {
240 let bit = 1usize << q;
241 for i in 0..amps.len() {
242 if i & bit == 0 {
243 let j = i | bit;
244 let a0 = amps[i];
245 let a1 = amps[j];
246 amps[i] = g[0][0] * a0 + g[0][1] * a1;
247 amps[j] = g[1][0] * a0 + g[1][1] * a1;
248 }
249 }
250 }
251
252 pub fn reupload_encode(&self, x: &[f64], layers: usize) -> Result<StateVector, QuantError> {
262 let n = x.len();
263 if n == 0 {
264 return Err(QuantError::DimensionMismatch {
265 detail: "empty input vector".to_string(),
266 });
267 }
268 if n > self.cap {
269 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
270 }
271 let mut amps = vec![C::ZERO; 1usize << n];
273 amps[0] = C::real(1.0);
274 for _ in 0..layers.max(1) {
275 for (q, &angle) in x.iter().enumerate() {
277 let ry = [
278 [C::real((angle / 2.0).cos()), C::real(-(angle / 2.0).sin())],
279 [C::real((angle / 2.0).sin()), C::real((angle / 2.0).cos())],
280 ];
281 Self::apply_1q(&mut amps, q, ry);
282 }
283 for q in 0..n.saturating_sub(1) {
286 Self::apply_cnot(&mut amps, q, q + 1);
287 }
288 }
289 Ok(StateVector { n, amps })
290 }
291
292 fn apply_cnot(amps: &mut [C], c: usize, t: usize) {
294 let cb = 1usize << c;
295 let tb = 1usize << t;
296 for i in 0..amps.len() {
297 if i & cb != 0 && i & tb == 0 {
298 amps.swap(i, i | tb);
299 }
300 }
301 }
302
303 fn apply_pauli(amps: &mut [C], q: usize, p: char) -> Result<(), char> {
305 let bit = 1usize << q;
306 match p {
307 'I' => {}
308 'X' => {
309 for i in 0..amps.len() {
310 if i & bit == 0 {
311 amps.swap(i, i | bit);
312 }
313 }
314 }
315 'Z' => {
316 for amp in amps.iter_mut().enumerate().filter(|(i, _)| i & bit != 0).map(|(_, a)| a) {
317 *amp = -*amp;
318 }
319 }
320 'Y' => {
321 for i in 0..amps.len() {
323 if i & bit == 0 {
324 let j = i | bit;
325 let a0 = amps[i];
326 let a1 = amps[j];
327 amps[i] = (-C::I) * a1;
328 amps[j] = C::I * a0;
329 }
330 }
331 }
332 other => return Err(other),
333 }
334 Ok(())
335 }
336
337 fn inner(a: &[C], b: &[C]) -> C {
339 a.iter()
340 .zip(b.iter())
341 .fold(C::ZERO, |acc, (x, y)| acc + x.conj() * *y)
342 }
343}
344
345impl QuantBackend for ReferenceSimulator {
346 fn capacity(&self) -> usize {
347 self.cap
348 }
349
350 fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError> {
351 if x.is_empty() {
352 return Err(QuantError::DimensionMismatch {
353 detail: "empty input vector".to_string(),
354 });
355 }
356 match scheme {
357 EncodingScheme::Amplitude => {
358 let n = Self::amplitude_qubits(x.len());
359 if n > self.cap {
360 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
361 }
362 let norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
364 if (norm - 1.0).abs() > NORM_TOL {
365 return Err(QuantError::NotNormalized { norm });
366 }
367 let mut amps = vec![C::ZERO; 1usize << n];
368 for (i, &v) in x.iter().enumerate() {
369 amps[i] = C::real(v);
370 }
371 Ok(StateVector { n, amps })
372 }
373 EncodingScheme::Angle => {
374 let n = x.len();
375 if n > self.cap {
376 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
377 }
378 let mut amps = vec![C::ZERO; 1usize << n];
380 for (idx, amp) in amps.iter_mut().enumerate() {
381 let mut coeff = 1.0f64;
382 for (q, &angle) in x.iter().enumerate() {
383 let bit = (idx >> q) & 1;
384 coeff *= if bit == 0 { (angle / 2.0).cos() } else { (angle / 2.0).sin() };
385 }
386 *amp = C::real(coeff);
387 }
388 Ok(StateVector { n, amps })
389 }
390 }
391 }
392
393 fn evolve(&self, mut state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError> {
394 let n = state.n;
395 for (li, layer) in circuit.layers.iter().enumerate() {
396 if layer.ry.len() != n || layer.rz.len() != n {
397 return Err(QuantError::DimensionMismatch {
398 detail: format!(
399 "layer {li} has {}/{} rotation angles but the register has {n} qubits",
400 layer.ry.len(),
401 layer.rz.len()
402 ),
403 });
404 }
405 for q in 0..n {
407 let ty = layer.ry[q];
408 let ry = [
409 [C::real((ty / 2.0).cos()), C::real(-(ty / 2.0).sin())],
410 [C::real((ty / 2.0).sin()), C::real((ty / 2.0).cos())],
411 ];
412 Self::apply_1q(&mut state.amps, q, ry);
413 let tz = layer.rz[q];
414 let rz = [
415 [C::new((tz / 2.0).cos(), -(tz / 2.0).sin()), C::ZERO],
416 [C::ZERO, C::new((tz / 2.0).cos(), (tz / 2.0).sin())],
417 ];
418 Self::apply_1q(&mut state.amps, q, rz);
419 }
420 for q in 0..n.saturating_sub(1) {
422 Self::apply_cnot(&mut state.amps, q, q + 1);
423 }
424 }
425 Ok(state)
426 }
427
428 fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError> {
429 let n = state.n;
430 let mut expectation = 0.0f64;
431 for (coeff, pauli) in &observable.terms {
432 if pauli.chars().count() != n {
433 return Err(QuantError::DimensionMismatch {
434 detail: format!(
435 "Pauli string '{pauli}' spans {} qubit(s) but the state has {n}",
436 pauli.chars().count()
437 ),
438 });
439 }
440 let mut phi = state.amps.clone();
442 for (q, p) in pauli.chars().enumerate() {
443 Self::apply_pauli(&mut phi, q, p)
444 .map_err(|bad| QuantError::BadPauli { pauli: pauli.clone(), bad })?;
445 }
446 expectation += coeff * Self::inner(&state.amps, &phi).re;
447 }
448 Ok(expectation)
449 }
450
451 fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError> {
452 if a.n != b.n {
453 return Err(QuantError::DimensionMismatch {
454 detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
455 });
456 }
457 Ok(Self::inner(&a.amps, &b.amps).norm_sqr())
458 }
459}
460
461impl ReferenceSimulator {
462 pub fn polynomial_kernel(a: &StateVector, b: &StateVector, degree: u32) -> Result<f64, QuantError> {
469 if a.n != b.n {
470 return Err(QuantError::DimensionMismatch {
471 detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
472 });
473 }
474 Ok(Self::inner(&a.amps, &b.amps).re.powi(degree as i32))
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use std::f64::consts::PI;
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}