use num_complex::Complex64;
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha8Rng;
use smallvec::SmallVec;
use crate::backend::simd;
use crate::backend::statevector::{StatevectorBackend, insert_zero_bit};
use crate::backend::{Backend, NORM_CLAMP_MIN};
use crate::circuit::{ClassicalCondition, Instruction};
use crate::error::Result;
use crate::gates::{Gate, McuData};
use crate::sim::i_pow;
use crate::sim::unified_pauli::PauliTerm;
fn block_superoperator(kraus: &[[[Complex64; 2]; 2]]) -> [[Complex64; 4]; 4] {
let mut s = [[Complex64::new(0.0, 0.0); 4]; 4];
for k in kraus {
for a in 0..2 {
for b in 0..2 {
for ap in 0..2 {
for bp in 0..2 {
s[2 * a + b][2 * ap + bp] += k[a][ap] * k[b][bp].conj();
}
}
}
}
}
s
}
fn project_tile(
tile: &mut [Complex64],
base: usize,
rmask: usize,
cmask: usize,
outcome: bool,
scale: Complex64,
) {
if ((base & rmask) != 0) != outcome {
simd::zero_slice(tile);
return;
}
let zero = Complex64::new(0.0, 0.0);
for (j, amp) in tile.iter_mut().enumerate() {
*amp = if (((base + j) & cmask) != 0) == outcome {
*amp * scale
} else {
zero
};
}
}
fn reset_fold_pair(r0: &mut [Complex64], r1: &mut [Complex64], cmask: usize) {
let zero = Complex64::new(0.0, 0.0);
for (j, amp) in r0.iter_mut().enumerate() {
*amp = if j & cmask == 0 {
*amp + r1[j | cmask]
} else {
zero
};
}
simd::zero_slice(r1);
}
#[cfg(feature = "parallel")]
fn row_aligned_tile(cmask: usize, rmask: usize) -> usize {
(cmask << 1).max(crate::backend::MIN_PAR_ELEMS).min(rmask)
}
fn conjugate_2x2(m: &[[Complex64; 2]; 2]) -> [[Complex64; 2]; 2] {
[
[m[0][0].conj(), m[0][1].conj()],
[m[1][0].conj(), m[1][1].conj()],
]
}
fn matrix_1q(gate: &Gate) -> Option<[[Complex64; 2]; 2]> {
match gate {
Gate::Id
| Gate::X
| Gate::Y
| Gate::Z
| Gate::H
| Gate::S
| Gate::Sdg
| Gate::T
| Gate::Tdg
| Gate::SX
| Gate::SXdg
| Gate::Rx(_)
| Gate::Ry(_)
| Gate::Rz(_)
| Gate::P(_)
| Gate::Fused(_) => Some(gate.matrix_2x2()),
_ => None,
}
}
fn conjugate_gate(gate: &Gate) -> Option<Gate> {
match gate {
Gate::Cx | Gate::Cz | Gate::Swap => Some(gate.clone()),
Gate::Rzz(theta) => Some(Gate::Rzz(-*theta)),
Gate::Cu(mat) => Some(Gate::Cu(Box::new(conjugate_2x2(mat)))),
Gate::Mcu(data) => Some(Gate::Mcu(Box::new(McuData {
mat: conjugate_2x2(&data.mat),
num_controls: data.num_controls,
}))),
_ => None,
}
}
pub struct DensityMatrixBackend {
num_qubits: usize,
classical_bits: Vec<bool>,
rng: ChaCha8Rng,
sv: StatevectorBackend,
}
impl DensityMatrixBackend {
pub fn new(seed: u64) -> Self {
Self {
num_qubits: 0,
classical_bits: Vec::new(),
rng: ChaCha8Rng::seed_from_u64(seed),
sv: StatevectorBackend::new(seed),
}
}
pub fn purity(&self) -> f64 {
crate::backend::state_norm_sqr(&self.sv.state)
}
#[inline]
fn dim(&self) -> usize {
1usize << self.num_qubits
}
fn conjugate_buffer(&mut self) {
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
if self.sv.state.len() >= (1 << crate::backend::PARALLEL_THRESHOLD_QUBITS) {
self.sv
.state
.par_iter_mut()
.for_each(|amp| *amp = amp.conj());
return;
}
}
for amp in self.sv.state.iter_mut() {
*amp = amp.conj();
}
}
fn apply_block_superoperator(&mut self, qubit: usize, s: &[[Complex64; 4]; 4]) {
let n = self.num_qubits;
self.sv.apply_fused_2q(qubit + n, qubit, s);
}
fn apply_unitary(&mut self, gate: &Gate, targets: &[usize]) -> Result<()> {
let n = self.num_qubits;
if let Some(mat) = matrix_1q(gate) {
if 2 * n >= crate::backend::PARALLEL_THRESHOLD_QUBITS {
let s = block_superoperator(&[mat]);
self.apply_block_superoperator(targets[0], &s);
return Ok(());
}
self.sv.apply_1q_matrix(targets[0] + n, &mat)?;
return self.sv.apply_1q_matrix(targets[0], &conjugate_2x2(&mat));
}
let ket_targets: SmallVec<[usize; 4]> = targets.iter().map(|&t| t + n).collect();
self.sv.apply(&Instruction::Gate {
gate: gate.clone(),
targets: ket_targets,
})?;
let bra_targets: SmallVec<[usize; 4]> = targets.iter().copied().collect();
if let Some(conjugate) = conjugate_gate(gate) {
return self.sv.apply(&Instruction::Gate {
gate: conjugate,
targets: bra_targets,
});
}
self.conjugate_buffer();
self.sv.apply(&Instruction::Gate {
gate: gate.clone(),
targets: bra_targets,
})?;
self.conjugate_buffer();
Ok(())
}
fn prob_one(&self, qubit: usize) -> f64 {
let d = self.dim();
let bit = 1usize << qubit;
let mut p1 = 0.0;
for r in 0..d {
if r & bit != 0 {
p1 += self.sv.state[r * d + r].re;
}
}
p1.clamp(0.0, 1.0)
}
fn apply_measure(&mut self, qubit: usize, classical_bit: usize) {
let p1 = self.prob_one(qubit);
let u: f64 = self.rng.random();
let outcome = u < p1;
self.classical_bits[classical_bit] = outcome;
let p = if outcome { p1 } else { 1.0 - p1 };
self.project(qubit, outcome, p);
}
fn apply_reset(&mut self, qubit: usize) {
let n = self.num_qubits;
let rmask = 1usize << (qubit + n);
let cmask = 1usize << qubit;
let block_size = rmask << 1;
#[cfg(feature = "parallel")]
if 2 * n >= crate::backend::PARALLEL_THRESHOLD_QUBITS {
use crate::backend::chunk_min_len;
use rayon::prelude::*;
if self.sv.state.len() / block_size >= 4 {
self.sv
.state
.par_chunks_mut(block_size)
.with_min_len(chunk_min_len(block_size))
.for_each(|block| {
let (r0, r1) = block.split_at_mut(rmask);
reset_fold_pair(r0, r1, cmask);
});
return;
}
let tile = row_aligned_tile(cmask, rmask);
for block in self.sv.state.chunks_mut(block_size) {
let (r0, r1) = block.split_at_mut(rmask);
r0.par_chunks_mut(tile)
.zip(r1.par_chunks_mut(tile))
.for_each(|(t0, t1)| reset_fold_pair(t0, t1, cmask));
}
return;
}
for block in self.sv.state.chunks_mut(block_size) {
let (r0, r1) = block.split_at_mut(rmask);
reset_fold_pair(r0, r1, cmask);
}
}
fn project(&mut self, qubit: usize, outcome: bool, p: f64) {
let n = self.num_qubits;
let rmask = 1usize << (qubit + n);
let cmask = 1usize << qubit;
let scale = Complex64::new(1.0 / p.clamp(NORM_CLAMP_MIN, 1.0), 0.0);
#[cfg(feature = "parallel")]
if 2 * n >= crate::backend::PARALLEL_THRESHOLD_QUBITS {
use crate::backend::chunk_min_len;
use rayon::prelude::*;
let tile = row_aligned_tile(cmask, rmask);
self.sv
.state
.par_chunks_mut(tile)
.with_min_len(chunk_min_len(tile))
.enumerate()
.for_each(|(t, chunk)| {
project_tile(chunk, t * tile, rmask, cmask, outcome, scale);
});
return;
}
for (t, chunk) in self.sv.state.chunks_mut(rmask).enumerate() {
project_tile(chunk, t * rmask, rmask, cmask, outcome, scale);
}
}
fn apply_conditional(
&mut self,
condition: &ClassicalCondition,
gate: &Gate,
targets: &[usize],
) -> Result<()> {
if condition.evaluate(&self.classical_bits) {
self.apply_unitary(gate, targets)?;
}
Ok(())
}
pub fn apply_1q_kraus(&mut self, qubit: usize, kraus: &[[[Complex64; 2]; 2]]) {
let s = block_superoperator(kraus);
self.apply_block_superoperator(qubit, &s);
}
pub fn apply_2q_depolarizing(&mut self, q0: usize, q1: usize, p: f64) {
let c1 = Complex64::new(1.0, 0.0);
let c0 = Complex64::new(0.0, 0.0);
let ci = Complex64::new(0.0, 1.0);
let paulis: [[[Complex64; 2]; 2]; 4] = [
[[c1, c0], [c0, c1]],
[[c0, c1], [c1, c0]],
[[c0, -ci], [ci, c0]],
[[c1, c0], [c0, -c1]],
];
let mut s = [[Complex64::new(0.0, 0.0); 16]; 16];
for a in 0..4 {
for b in 0..4 {
let w = if a == 0 && b == 0 {
(1.0 - p).sqrt()
} else {
(p / 15.0).sqrt()
};
let kentry = |t: usize, tp: usize| {
Complex64::new(w, 0.0) * paulis[b][t >> 1][tp >> 1] * paulis[a][t & 1][tp & 1]
};
for tr in 0..4 {
for trp in 0..4 {
let kr = kentry(tr, trp);
if kr == Complex64::new(0.0, 0.0) {
continue;
}
for tc in 0..4 {
for tcp in 0..4 {
s[4 * tr + tc][4 * trp + tcp] += kr * kentry(tc, tcp).conj();
}
}
}
}
}
}
let n = self.num_qubits;
let d = self.dim();
let mut positions = [q0, q1, q0 + n, q1 + n];
positions.sort_unstable();
let flat_offset = |tr: usize, tc: usize| {
(if tr & 1 != 0 { 1usize << (q0 + n) } else { 0 })
| (if tr & 2 != 0 { 1usize << (q1 + n) } else { 0 })
| (if tc & 1 != 0 { 1usize << q0 } else { 0 })
| (if tc & 2 != 0 { 1usize << q1 } else { 0 })
};
let mut flats = [0usize; 16];
for tr in 0..4 {
for tc in 0..4 {
flats[4 * tr + tc] = flat_offset(tr, tc);
}
}
let num_groups = (d * d) >> 4;
#[cfg(feature = "parallel")]
if 2 * n >= crate::backend::PARALLEL_THRESHOLD_QUBITS {
use crate::backend::MIN_PAR_ITERS;
use crate::backend::statevector::SendPtr;
use rayon::prelude::*;
let ptr = SendPtr(self.sv.state.as_mut_ptr());
(0..num_groups)
.into_par_iter()
.with_min_len(MIN_PAR_ITERS)
.for_each(move |m| {
let mut base = m;
for &pos in &positions {
base = insert_zero_bit(base, pos);
}
let mut v = [Complex64::new(0.0, 0.0); 16];
unsafe {
for (k, &off) in flats.iter().enumerate() {
v[k] = ptr.load(base | off);
}
for (i, &off) in flats.iter().enumerate() {
let mut acc = Complex64::new(0.0, 0.0);
for j in 0..16 {
acc += s[i][j] * v[j];
}
ptr.store(base | off, acc);
}
}
});
return;
}
for m in 0..num_groups {
let mut base = m;
for &pos in &positions {
base = insert_zero_bit(base, pos);
}
let mut v = [Complex64::new(0.0, 0.0); 16];
for (k, &off) in flats.iter().enumerate() {
v[k] = self.sv.state[base | off];
}
for (i, &off) in flats.iter().enumerate() {
let mut acc = Complex64::new(0.0, 0.0);
for j in 0..16 {
acc += s[i][j] * v[j];
}
self.sv.state[base | off] = acc;
}
}
}
pub fn expectation_pauli(&self, xmask: usize, zmask: usize, num_y: u32) -> f64 {
let d = self.dim();
let mut acc = Complex64::new(0.0, 0.0);
for j in 0..d {
let sign = if (j & zmask).count_ones() & 1 == 1 {
-1.0
} else {
1.0
};
acc += self.sv.state[j * d + (j ^ xmask)] * sign;
}
(acc * i_pow(num_y)).re
}
}
impl Backend for DensityMatrixBackend {
fn name(&self) -> &'static str {
"density_matrix"
}
fn init(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<()> {
crate::backend::check_state_allocation(
"density_matrix",
num_qubits,
crate::backend::max_density_matrix_qubits(),
crate::backend::DM_QUBIT_CAP_ENV,
)?;
self.num_qubits = num_qubits;
self.classical_bits = vec![false; num_classical_bits];
self.sv.init(2 * num_qubits, 0)
}
fn apply(&mut self, instruction: &Instruction) -> Result<()> {
match instruction {
Instruction::Gate { gate, targets } => self.apply_unitary(gate, targets),
Instruction::Barrier { .. } => Ok(()),
Instruction::Measure {
qubit,
classical_bit,
} => {
self.apply_measure(*qubit, *classical_bit);
Ok(())
}
Instruction::Reset { qubit } => {
self.apply_reset(*qubit);
Ok(())
}
Instruction::Conditional {
condition,
gate,
targets,
} => self.apply_conditional(condition, gate, targets),
}
}
fn classical_results(&self) -> &[bool] {
&self.classical_bits
}
fn probabilities(&self) -> Result<Vec<f64>> {
let d = self.dim();
let mut probs = vec![0.0f64; d];
for (k, p) in probs.iter_mut().enumerate() {
*p = self.sv.state[k * d + k].re.max(0.0);
}
Ok(probs)
}
fn num_qubits(&self) -> usize {
self.num_qubits
}
fn supports_fused_gates(&self) -> bool {
false
}
fn qubit_probability(&self, qubit: usize) -> Result<f64> {
Ok(self.prob_one(qubit))
}
fn reset(&mut self, qubit: usize) -> Result<()> {
self.apply_reset(qubit);
Ok(())
}
fn supports_pauli_expectation(&self) -> bool {
true
}
fn pauli_expectations(&self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
observables
.iter()
.map(|observable| {
let (xmask, zmask, num_y) = crate::sim::pauli_masks(observable, self.num_qubits)?;
Ok(self.expectation_pauli(xmask, zmask, num_y))
})
.collect()
}
fn reduced_density_matrix_1q(&self, qubit: usize) -> Result<[[Complex64; 2]; 2]> {
let n = self.num_qubits;
let d = self.dim();
let bit = 1usize << qubit;
let others = 1usize << (n - 1);
let mut r00 = Complex64::new(0.0, 0.0);
let mut r01 = Complex64::new(0.0, 0.0);
let mut r10 = Complex64::new(0.0, 0.0);
let mut r11 = Complex64::new(0.0, 0.0);
for m in 0..others {
let base = (m & (bit - 1)) | ((m >> qubit) << (qubit + 1));
let i1 = base | bit;
r00 += self.sv.state[base * d + base];
r01 += self.sv.state[base * d + i1];
r10 += self.sv.state[i1 * d + base];
r11 += self.sv.state[i1 * d + i1];
}
Ok([[r00, r01], [r10, r11]])
}
fn apply_1q_matrix(&mut self, qubit: usize, matrix: &[[Complex64; 2]; 2]) -> Result<()> {
let n = self.num_qubits;
if 2 * n >= crate::backend::PARALLEL_THRESHOLD_QUBITS {
let s = block_superoperator(&[*matrix]);
self.apply_block_superoperator(qubit, &s);
return Ok(());
}
self.sv.apply_1q_matrix(qubit + n, matrix)?;
self.sv.apply_1q_matrix(qubit, &conjugate_2x2(matrix))
}
}