use itertools::Itertools;
use ket::{
error::KetError,
execution::{
BatchExecution, DumpData, GradientStrategy, LiveExecution, NativeGate,
QuantumExecution as KetQuantumExecution, SampleData,
},
ir::{
gate::{GateInstruction, QuantumGate},
hamiltonian::{Hamiltonian, Pauli},
},
process::QPUConfig,
};
use num::Integer;
use rand::distr::weighted::WeightedIndex;
use rand::distr::Distribution;
use rand::{rngs::StdRng, Rng, SeedableRng};
use std::f64::consts::FRAC_PI_2;
use crate::convert::{from_dump_to_prob, from_prob_to_shots};
pub trait QuantumExecution {
fn new(num_qubits: usize) -> Result<Self, KetError>
where
Self: Sized;
fn pauli_x(&mut self, target: usize, control: &[usize]);
fn pauli_y(&mut self, target: usize, control: &[usize]);
fn pauli_z(&mut self, target: usize, control: &[usize]);
fn hadamard(&mut self, target: usize, control: &[usize]);
fn phase(&mut self, lambda: f64, target: usize, control: &[usize]);
fn rx(&mut self, theta: f64, target: usize, control: &[usize]);
fn ry(&mut self, theta: f64, target: usize, control: &[usize]);
fn rz(&mut self, theta: f64, target: usize, control: &[usize]);
fn measure_p1(&mut self, target: usize) -> f64;
fn measure_collapse(&mut self, target: usize, result: bool, p: f64);
fn dump(&mut self, qubits: &[usize]) -> DumpData;
fn sample<R: Rng>(&mut self, qubits: &[usize], shots: usize, rng: &mut R) -> SampleData {
let data = self.dump(qubits);
from_prob_to_shots(&from_dump_to_prob(data), shots, rng)
}
fn exp_value(&mut self, hamiltonian: &Hamiltonian) -> f64 {
hamiltonian
.pauli_strings
.iter()
.map(|pauli_terms| {
pauli_terms.iter().for_each(|term| match term.pauli {
Pauli::PauliX => self.hadamard(term.qubit, &[]),
Pauli::PauliY => {
self.phase(-FRAC_PI_2, term.qubit, &[]);
self.hadamard(term.qubit, &[]);
}
Pauli::PauliZ => {}
});
let dump_data = self.dump(&pauli_terms.iter().map(|term| term.qubit).collect_vec());
let probabilities = from_dump_to_prob(dump_data);
let result: f64 = probabilities
.basis_states
.iter()
.zip(probabilities.probabilities.iter())
.map(|(state, prob)| {
let parity = if state
.iter()
.fold(0, |acc, bit| acc + bit.count_ones())
.is_even()
{
1.0
} else {
-1.0
};
*prob * parity
})
.sum();
pauli_terms.iter().for_each(|term| match term.pauli {
Pauli::PauliX => self.hadamard(term.qubit, &[]),
Pauli::PauliY => {
self.hadamard(term.qubit, &[]);
self.phase(FRAC_PI_2, term.qubit, &[]);
}
Pauli::PauliZ => {}
});
result
})
.zip(&hamiltonian.coefficients)
.map(|(result, coefficient)| result * *coefficient)
.sum()
}
fn clear(&mut self);
}
pub struct QubitManager<S: QuantumExecution> {
simulator: S,
rng: std::sync::Mutex<StdRng>,
num_qubits: usize,
}
impl<S: QuantumExecution> QubitManager<S> {
pub fn new(num_qubits: usize) -> Result<Self, KetError> {
let seed = std::env::var("KBW_SEED")
.unwrap_or_default()
.parse::<u64>()
.unwrap_or_else(|_| rand::random());
Ok(Self {
simulator: S::new(num_qubits)?,
rng: std::sync::Mutex::new(StdRng::seed_from_u64(seed)),
num_qubits,
})
}
pub fn make_live_config(num_qubits: usize, decompose: bool) -> Result<QPUConfig, KetError>
where
S: 'static,
{
Ok(QPUConfig {
num_qubits,
quantum_execution: Some(KetQuantumExecution::Live {
qpu: Box::new(Self::new(num_qubits)?),
decompose,
native_gate_set: if decompose { Some(Box::new(())) } else { None },
}),
})
}
pub fn make_batch_config(
num_qubits: usize,
decompose: bool,
coupling_graph: Option<Vec<(usize, usize)>>,
gradient: bool,
) -> Result<QPUConfig, KetError>
where
S: 'static,
{
Ok(QPUConfig {
num_qubits,
quantum_execution: Some(KetQuantumExecution::Batch {
qpu: Box::new(Self::new(num_qubits)?),
native_gate_set: if coupling_graph.is_some() {
Some(Box::new(()))
} else {
None
},
gradient: if gradient {
GradientStrategy::ParameterShiftRule
} else {
GradientStrategy::None
},
exp_value: ket::execution::ExpValueStrategy::Native,
coupling_graph,
decompose,
}),
})
}
}
impl<S: QuantumExecution> QubitManager<S> {
fn apply_gate(&mut self, gate: &QuantumGate, target: usize, control: &[usize]) {
match gate {
QuantumGate::RotationX(theta) => {
self.simulator.rx(theta.value(), target, control);
}
QuantumGate::RotationY(theta) => {
self.simulator.ry(theta.value(), target, control);
}
QuantumGate::RotationZ(theta) => {
self.simulator.rz(theta.value(), target, control);
}
QuantumGate::Phase(lambda) => {
self.simulator.phase(lambda.value(), target, control);
}
QuantumGate::Hadamard => self.simulator.hadamard(target, control),
QuantumGate::PauliX => self.simulator.pauli_x(target, control),
QuantumGate::PauliY => self.simulator.pauli_y(target, control),
QuantumGate::PauliZ => self.simulator.pauli_z(target, control),
}
}
fn do_measure(&mut self, qubits: &[usize]) -> u64 {
qubits
.iter()
.rev()
.enumerate()
.map(|(index, qubit)| {
let p1 = self.simulator.measure_p1(*qubit);
let p0 = (1.0 - p1).max(0.0);
let result = WeightedIndex::new([p0, p1])
.unwrap()
.sample(&mut *self.rng.lock().unwrap());
let prob = if result == 1 { p1 } else { p0 };
let p = 1.0 / f64::sqrt(prob.max(f64::MIN_POSITIVE));
self.simulator.measure_collapse(*qubit, result == 1, p);
(result << index) as u64
})
.reduce(|a, b| a | b)
.unwrap_or(0)
}
fn do_sample(&mut self, qubits: &[usize], shots: usize) -> SampleData {
assert!(!qubits.is_empty());
self.simulator
.sample(qubits, shots, &mut *self.rng.lock().unwrap())
}
fn do_dump(&mut self, qubits: &[usize]) -> DumpData {
self.simulator.dump(qubits)
}
fn do_exp_value(&mut self, hamiltonian: &Hamiltonian) -> f64 {
self.simulator.exp_value(hamiltonian)
}
}
impl<S: QuantumExecution + 'static> LiveExecution for QubitManager<S> {
fn compute_gate(&mut self, gate: &GateInstruction) -> Result<(), ket::error::KetError> {
let control: Vec<usize> = gate.control.iter().copied().collect();
self.apply_gate(&gate.gate, gate.target, &control);
Ok(())
}
fn compute_native_gates(&mut self, gates: &[NativeGate]) -> Result<(), ket::error::KetError> {
self.compute_gates(gates)
}
fn measure(&mut self, qubits: &[usize]) -> Result<u64, ket::error::KetError> {
Ok(self.do_measure(qubits))
}
fn dump(&mut self, qubits: &[usize]) -> Result<DumpData, ket::error::KetError> {
Ok(self.do_dump(qubits))
}
fn sample(
&mut self,
qubits: &[usize],
shots: usize,
) -> Result<SampleData, ket::error::KetError> {
Ok(self.do_sample(qubits, shots))
}
fn exp_value(&mut self, hamiltonian: Hamiltonian) -> Result<f64, ket::error::KetError> {
Ok(self.do_exp_value(&hamiltonian))
}
}
impl<S: QuantumExecution + 'static> QubitManager<S> {
fn compute_gates(&mut self, gates: &[NativeGate]) -> Result<(), KetError> {
self.simulator.clear();
for (name, angles, qubits) in gates {
let control: Vec<usize> = qubits[..qubits.len().saturating_sub(1)].to_vec();
let target = *qubits.last().unwrap();
match name.as_str() {
"x" => self.simulator.pauli_x(target, &[]),
"y" => self.simulator.pauli_y(target, &[]),
"z" => self.simulator.pauli_z(target, &[]),
"h" => self.simulator.hadamard(target, &[]),
"rx" => self.simulator.rx(angles[0], target, &[]),
"ry" => self.simulator.ry(angles[0], target, &[]),
"rz" => self.simulator.rz(angles[0], target, &[]),
"p" => self.simulator.phase(angles[0], target, &[]),
"cnot" => self.simulator.pauli_x(target, &control),
"cz" => self.simulator.pauli_z(target, &control),
_ => {
return Err(KetError::NativeGateUnsupported);
}
}
}
Ok(())
}
}
impl<S: QuantumExecution + 'static> BatchExecution for QubitManager<S> {
fn sample(
&self,
gates: &[GateInstruction],
qubits_to_sample: &[usize],
shots: usize,
) -> Result<SampleData, ket::error::KetError> {
let mut sim = Self::new(self.num_qubits)?;
for gate in gates {
sim.compute_gate(gate)?;
}
Ok(sim.do_sample(qubits_to_sample, shots))
}
fn sample_native(
&self,
gates: &[NativeGate],
qubits_to_sample: &[usize],
shots: usize,
) -> Result<SampleData, ket::error::KetError> {
let mut sim = Self::new(self.num_qubits)?;
sim.compute_gates(gates)?;
Ok(sim.do_sample(qubits_to_sample, shots))
}
fn exp_value(
&self,
gates: &[GateInstruction],
hamiltonian_list: &[Hamiltonian],
) -> Result<Vec<f64>, ket::error::KetError> {
let mut sim = Self::new(self.num_qubits)?;
for gate in gates {
sim.compute_gate(gate)?;
}
Ok(hamiltonian_list
.iter()
.map(|h| sim.do_exp_value(h))
.collect())
}
fn exp_value_native(
&self,
gates: &[NativeGate],
hamiltonian_list: &[Hamiltonian],
) -> Result<Vec<f64>, ket::error::KetError> {
let mut sim = Self::new(self.num_qubits)?;
sim.compute_gates(gates)?;
Ok(hamiltonian_list
.iter()
.map(|h| sim.do_exp_value(h))
.collect())
}
fn gradient(
&self,
_gates: &[GateInstruction],
_hamiltonian: &Hamiltonian,
) -> Result<(f64, Vec<f64>), KetError> {
Err(KetError::NativeGateUnsupported) }
}