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 fn unsupported_observables(
216 backend: &dyn QuantBackend,
217 observables: &[crate::ir_nodes::IRObservable],
218) -> Vec<String> {
219 let cap = backend.capacity();
220 observables
221 .iter()
222 .filter_map(|o| {
223 let n = usize::try_from(o.qubits?).ok()?;
226 (n > cap).then(|| {
227 format!(
228 "observable `{}` declares {n} qubits, above the mounted simulator's \
229 capacity of {cap}. Refusing at DEPLOY rather than at the `yield`: a \
230 register that cannot be realised is not a late failure to discover. \
231 The OSS reference simulator is capped at {} qubits (D = 2^{} \
232 amplitudes); larger registers require the enterprise backend.",
233 o.name, OSS_QUBIT_CAP, OSS_QUBIT_CAP
234 )
235 })
236 })
237 .collect()
238}
239
240pub const OSS_QUBIT_CAP: usize = 10;
246
247const NORM_TOL: f64 = 1e-9;
249
250#[derive(Debug, Clone)]
253pub struct ReferenceSimulator {
254 cap: usize,
255}
256
257impl Default for ReferenceSimulator {
258 fn default() -> Self {
259 ReferenceSimulator { cap: OSS_QUBIT_CAP }
260 }
261}
262
263impl ReferenceSimulator {
264 pub fn new() -> Self {
265 Self::default()
266 }
267
268 fn amplitude_qubits(d: usize) -> usize {
270 let mut n = 0usize;
271 while (1usize << n) < d {
272 n += 1;
273 }
274 n
275 }
276
277 fn apply_1q(amps: &mut [C], q: usize, g: [[C; 2]; 2]) {
279 let bit = 1usize << q;
280 for i in 0..amps.len() {
281 if i & bit == 0 {
282 let j = i | bit;
283 let a0 = amps[i];
284 let a1 = amps[j];
285 amps[i] = g[0][0] * a0 + g[0][1] * a1;
286 amps[j] = g[1][0] * a0 + g[1][1] * a1;
287 }
288 }
289 }
290
291 pub fn reupload_encode(&self, x: &[f64], layers: usize) -> Result<StateVector, QuantError> {
301 let n = x.len();
302 if n == 0 {
303 return Err(QuantError::DimensionMismatch {
304 detail: "empty input vector".to_string(),
305 });
306 }
307 if n > self.cap {
308 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
309 }
310 let mut amps = vec![C::ZERO; 1usize << n];
312 amps[0] = C::real(1.0);
313 for _ in 0..layers.max(1) {
314 for (q, &angle) in x.iter().enumerate() {
316 let ry = [
317 [C::real((angle / 2.0).cos()), C::real(-(angle / 2.0).sin())],
318 [C::real((angle / 2.0).sin()), C::real((angle / 2.0).cos())],
319 ];
320 Self::apply_1q(&mut amps, q, ry);
321 }
322 for q in 0..n.saturating_sub(1) {
325 Self::apply_cnot(&mut amps, q, q + 1);
326 }
327 }
328 Ok(StateVector { n, amps })
329 }
330
331 fn apply_cnot(amps: &mut [C], c: usize, t: usize) {
333 let cb = 1usize << c;
334 let tb = 1usize << t;
335 for i in 0..amps.len() {
336 if i & cb != 0 && i & tb == 0 {
337 amps.swap(i, i | tb);
338 }
339 }
340 }
341
342 fn apply_pauli(amps: &mut [C], q: usize, p: char) -> Result<(), char> {
344 let bit = 1usize << q;
345 match p {
346 'I' => {}
347 'X' => {
348 for i in 0..amps.len() {
349 if i & bit == 0 {
350 amps.swap(i, i | bit);
351 }
352 }
353 }
354 'Z' => {
355 for amp in amps.iter_mut().enumerate().filter(|(i, _)| i & bit != 0).map(|(_, a)| a) {
356 *amp = -*amp;
357 }
358 }
359 'Y' => {
360 for i in 0..amps.len() {
362 if i & bit == 0 {
363 let j = i | bit;
364 let a0 = amps[i];
365 let a1 = amps[j];
366 amps[i] = (-C::I) * a1;
367 amps[j] = C::I * a0;
368 }
369 }
370 }
371 other => return Err(other),
372 }
373 Ok(())
374 }
375
376 fn inner(a: &[C], b: &[C]) -> C {
378 a.iter()
379 .zip(b.iter())
380 .fold(C::ZERO, |acc, (x, y)| acc + x.conj() * *y)
381 }
382}
383
384impl QuantBackend for ReferenceSimulator {
385 fn capacity(&self) -> usize {
386 self.cap
387 }
388
389 fn encode(&self, x: &[f64], scheme: EncodingScheme) -> Result<StateVector, QuantError> {
390 if x.is_empty() {
391 return Err(QuantError::DimensionMismatch {
392 detail: "empty input vector".to_string(),
393 });
394 }
395 match scheme {
396 EncodingScheme::Amplitude => {
397 let n = Self::amplitude_qubits(x.len());
398 if n > self.cap {
399 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
400 }
401 let norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
403 if (norm - 1.0).abs() > NORM_TOL {
404 return Err(QuantError::NotNormalized { norm });
405 }
406 let mut amps = vec![C::ZERO; 1usize << n];
407 for (i, &v) in x.iter().enumerate() {
408 amps[i] = C::real(v);
409 }
410 Ok(StateVector { n, amps })
411 }
412 EncodingScheme::Angle => {
413 let n = x.len();
414 if n > self.cap {
415 return Err(QuantError::CapacityExceeded { requested: n, cap: self.cap });
416 }
417 let mut amps = vec![C::ZERO; 1usize << n];
419 for (idx, amp) in amps.iter_mut().enumerate() {
420 let mut coeff = 1.0f64;
421 for (q, &angle) in x.iter().enumerate() {
422 let bit = (idx >> q) & 1;
423 coeff *= if bit == 0 { (angle / 2.0).cos() } else { (angle / 2.0).sin() };
424 }
425 *amp = C::real(coeff);
426 }
427 Ok(StateVector { n, amps })
428 }
429 }
430 }
431
432 fn evolve(&self, mut state: StateVector, circuit: &VariationalCircuit) -> Result<StateVector, QuantError> {
433 let n = state.n;
434 for (li, layer) in circuit.layers.iter().enumerate() {
435 if layer.ry.len() != n || layer.rz.len() != n {
436 return Err(QuantError::DimensionMismatch {
437 detail: format!(
438 "layer {li} has {}/{} rotation angles but the register has {n} qubits",
439 layer.ry.len(),
440 layer.rz.len()
441 ),
442 });
443 }
444 for q in 0..n {
446 let ty = layer.ry[q];
447 let ry = [
448 [C::real((ty / 2.0).cos()), C::real(-(ty / 2.0).sin())],
449 [C::real((ty / 2.0).sin()), C::real((ty / 2.0).cos())],
450 ];
451 Self::apply_1q(&mut state.amps, q, ry);
452 let tz = layer.rz[q];
453 let rz = [
454 [C::new((tz / 2.0).cos(), -(tz / 2.0).sin()), C::ZERO],
455 [C::ZERO, C::new((tz / 2.0).cos(), (tz / 2.0).sin())],
456 ];
457 Self::apply_1q(&mut state.amps, q, rz);
458 }
459 for q in 0..n.saturating_sub(1) {
461 Self::apply_cnot(&mut state.amps, q, q + 1);
462 }
463 }
464 Ok(state)
465 }
466
467 fn measure(&self, state: &StateVector, observable: &PauliSum) -> Result<f64, QuantError> {
468 let n = state.n;
469 let mut expectation = 0.0f64;
470 for (coeff, pauli) in &observable.terms {
471 if pauli.chars().count() != n {
472 return Err(QuantError::DimensionMismatch {
473 detail: format!(
474 "Pauli string '{pauli}' spans {} qubit(s) but the state has {n}",
475 pauli.chars().count()
476 ),
477 });
478 }
479 let mut phi = state.amps.clone();
481 for (q, p) in pauli.chars().enumerate() {
482 Self::apply_pauli(&mut phi, q, p)
483 .map_err(|bad| QuantError::BadPauli { pauli: pauli.clone(), bad })?;
484 }
485 expectation += coeff * Self::inner(&state.amps, &phi).re;
486 }
487 Ok(expectation)
488 }
489
490 fn kernel(&self, a: &StateVector, b: &StateVector) -> Result<f64, QuantError> {
491 if a.n != b.n {
492 return Err(QuantError::DimensionMismatch {
493 detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
494 });
495 }
496 Ok(Self::inner(&a.amps, &b.amps).norm_sqr())
497 }
498}
499
500impl ReferenceSimulator {
501 pub fn polynomial_kernel(a: &StateVector, b: &StateVector, degree: u32) -> Result<f64, QuantError> {
508 if a.n != b.n {
509 return Err(QuantError::DimensionMismatch {
510 detail: format!("kernel operands span {} vs {} qubits", a.n, b.n),
511 });
512 }
513 Ok(Self::inner(&a.amps, &b.amps).re.powi(degree as i32))
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use std::f64::consts::PI;
522
523 fn approx(a: f64, b: f64) -> bool {
524 (a - b).abs() < 1e-9
525 }
526
527 #[test]
528 fn amplitude_qubits_is_ceil_log2() {
529 assert_eq!(ReferenceSimulator::amplitude_qubits(1), 0);
530 assert_eq!(ReferenceSimulator::amplitude_qubits(2), 1);
531 assert_eq!(ReferenceSimulator::amplitude_qubits(3), 2);
532 assert_eq!(ReferenceSimulator::amplitude_qubits(4), 2);
533 assert_eq!(ReferenceSimulator::amplitude_qubits(1024), 10);
534 assert_eq!(ReferenceSimulator::amplitude_qubits(1025), 11);
535 }
536
537 #[test]
538 fn capacity_cap_is_enforced_with_e0783() {
539 let sim = ReferenceSimulator::new();
540 let x = vec![0.0; 1025];
542 let err = sim.encode(&x, EncodingScheme::Amplitude).unwrap_err();
543 assert!(matches!(err, QuantError::CapacityExceeded { requested: 11, cap: 10 }));
544 assert_eq!(err.code(), "axon-E0783");
545 }
546
547 #[test]
548 fn amplitude_encode_requires_unit_norm() {
549 let sim = ReferenceSimulator::new();
550 let ok = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
552 assert_eq!(ok.n, 1);
553 assert!(approx(ok.norm_sqr(), 1.0));
554 let err = sim.encode(&[1.0, 1.0], EncodingScheme::Amplitude).unwrap_err();
556 assert!(matches!(err, QuantError::NotNormalized { .. }));
557 assert_eq!(err.code(), "axon-E0788");
558 }
559
560 #[test]
561 fn angle_encode_is_unit_norm_product_state() {
562 let sim = ReferenceSimulator::new();
563 let s0 = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
565 assert!(approx(s0.amps[0].re, 1.0) && approx(s0.amps[1].re, 0.0));
566 let s1 = sim.encode(&[PI], EncodingScheme::Angle).unwrap();
568 assert!(approx(s1.amps[0].re, 0.0) && approx(s1.amps[1].re, 1.0));
569 assert!(approx(s1.norm_sqr(), 1.0));
570 }
571
572 #[test]
573 fn ry_pi_flips_zero_to_one() {
574 let sim = ReferenceSimulator::new();
575 let s = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
577 let circuit = VariationalCircuit {
578 layers: vec![RotationLayer { ry: vec![PI], rz: vec![0.0] }],
579 };
580 let out = sim.evolve(s, &circuit).unwrap();
581 assert!(approx(out.amps[1].norm_sqr(), 1.0));
583 assert!(approx(out.amps[0].norm_sqr(), 0.0));
584 }
585
586 #[test]
587 fn measure_pauli_z_eigenvalues() {
588 let sim = ReferenceSimulator::new();
589 let z = PauliSum { terms: vec![(1.0, "Z".to_string())] };
590 let s0 = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
592 assert!(approx(sim.measure(&s0, &z).unwrap(), 1.0));
593 let s1 = sim.encode(&[PI], EncodingScheme::Angle).unwrap();
595 assert!(approx(sim.measure(&s1, &z).unwrap(), -1.0));
596 }
597
598 #[test]
599 fn measure_zz_on_two_qubits() {
600 let sim = ReferenceSimulator::new();
601 let zz = PauliSum { terms: vec![(1.0, "ZZ".to_string())] };
602 let s00 = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap();
604 assert!(approx(sim.measure(&s00, &zz).unwrap(), 1.0));
605 let s01 = sim.encode(&[PI, 0.0], EncodingScheme::Angle).unwrap();
607 assert!(approx(sim.measure(&s01, &zz).unwrap(), -1.0));
608 }
609
610 #[test]
611 fn measure_rejects_wrong_length_pauli() {
612 let sim = ReferenceSimulator::new();
613 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 { .. })));
616 }
617
618 #[test]
619 fn measure_rejects_bad_pauli_alphabet() {
620 let sim = ReferenceSimulator::new();
621 let s = sim.encode(&[0.0], EncodingScheme::Angle).unwrap();
622 let bad = PauliSum { terms: vec![(1.0, "K".to_string())] };
623 let err = sim.measure(&s, &bad).unwrap_err();
624 assert!(matches!(err, QuantError::BadPauli { bad: 'K', .. }));
625 }
626
627 #[test]
628 fn kernel_fidelity_identical_and_orthogonal() {
629 let sim = ReferenceSimulator::new();
630 let a = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
631 assert!(approx(sim.kernel(&a, &a).unwrap(), 1.0));
633 let e0 = sim.encode(&[1.0, 0.0], EncodingScheme::Amplitude).unwrap();
635 let e1 = sim.encode(&[0.0, 1.0], EncodingScheme::Amplitude).unwrap();
636 assert!(approx(sim.kernel(&e0, &e1).unwrap(), 0.0));
637 }
638
639 #[test]
640 fn reupload_changes_the_feature_map_and_stays_a_valid_kernel() {
641 let sim = ReferenceSimulator::new();
645 let x = [0.5, 1.2, 0.3];
646 let y = [1.0, 0.2, 0.9];
647 let k1 = sim
648 .kernel(
649 &sim.reupload_encode(&x, 1).unwrap(),
650 &sim.reupload_encode(&y, 1).unwrap(),
651 )
652 .unwrap();
653 let k2 = sim
654 .kernel(
655 &sim.reupload_encode(&x, 2).unwrap(),
656 &sim.reupload_encode(&y, 2).unwrap(),
657 )
658 .unwrap();
659 assert!(
660 (k1 - k2).abs() > 1e-3,
661 "re-uploading must change the kernel (escape the single-layer bound): k1={k1}, k2={k2}"
662 );
663 let sx = sim.reupload_encode(&x, 2).unwrap();
665 assert!(approx(sim.kernel(&sx, &sx).unwrap(), 1.0));
666 assert!(k2 >= -1e-9 && k2 <= 1.0 + 1e-9);
667 }
668
669 #[test]
670 fn polynomial_kernel_is_dot_to_the_degree() {
671 let sim = ReferenceSimulator::new();
673 let a = sim.encode(&[0.6, 0.8], EncodingScheme::Amplitude).unwrap();
674 let b = sim.encode(&[1.0, 0.0], EncodingScheme::Amplitude).unwrap();
675 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 0).unwrap(), 1.0));
676 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 1).unwrap(), 0.6));
677 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 2).unwrap(), 0.36));
678 assert!(approx(ReferenceSimulator::polynomial_kernel(&a, &b, 3).unwrap(), 0.216));
679 }
680
681 #[test]
682 fn cnot_entangles_for_bell_correlation() {
683 let sim = ReferenceSimulator::new();
685 let s = sim.encode(&[0.0, 0.0], EncodingScheme::Angle).unwrap();
686 let circuit = VariationalCircuit {
687 layers: vec![RotationLayer { ry: vec![PI, 0.0], rz: vec![0.0, 0.0] }],
688 };
689 let out = sim.evolve(s, &circuit).unwrap();
690 let zz = PauliSum { terms: vec![(1.0, "ZZ".to_string())] };
691 assert!(approx(sim.measure(&out, &zz).unwrap(), 1.0), "post-CNOT |11⟩ ⇒ ⟨ZZ⟩ = +1");
692 }
693}