use num_complex::Complex64;
use rand::RngExt;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use crate::circuit::{Circuit, ClassicalCondition, Instruction, smallvec};
use crate::gates::Gate;
use crate::sim::unified_pauli::{PauliAxis, PauliTerm};
pub fn qft_circuit(n: usize) -> Circuit {
let mut c = Circuit::new(n, 0);
debug_assert!(n <= u8::MAX as usize, "QFT block size must fit in u8");
let targets: Vec<usize> = (0..n).collect();
c.add_gate(
Gate::QftBlock {
start: 0,
num: n as u8,
inverse: false,
swaps: true,
big_endian: false,
},
&targets,
);
c
}
pub fn random_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let singles = [Gate::H, Gate::X, Gate::Y, Gate::Z, Gate::S, Gate::T];
for layer in 0..depth {
for q in 0..n {
c.add_gate(singles[rng.random_range(0..singles.len())].clone(), &[q]);
}
let offset = layer % 2;
for q in (offset..n - 1).step_by(2) {
let keep = rng.random_bool(0.5);
if layer < 2 || keep {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
}
}
c
}
pub fn hardware_efficient_ansatz(n: usize, layers: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for _ in 0..layers {
for q in 0..n {
c.add_gate(Gate::Ry(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
c.add_gate(Gate::Rz(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
for q in 0..n - 1 {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
}
c
}
pub fn clifford_heavy_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let cliffords = [Gate::H, Gate::S, Gate::X, Gate::Y, Gate::Z];
for layer in 0..depth {
for q in 0..n {
c.add_gate(
cliffords[rng.random_range(0..cliffords.len())].clone(),
&[q],
);
}
let offset = layer % 2;
for q in (offset..n - 1).step_by(2) {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
}
c
}
pub fn clifford_random_pairs(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let cliffords = [Gate::H, Gate::S, Gate::X, Gate::Y, Gate::Z];
for _ in 0..depth {
for q in 0..n {
c.add_gate(
cliffords[rng.random_range(0..cliffords.len())].clone(),
&[q],
);
}
let num_pairs = n / 2;
let mut available: Vec<usize> = (0..n).collect();
for _ in 0..num_pairs {
if available.len() < 2 {
break;
}
let i = rng.random_range(0..available.len());
let q0 = available.swap_remove(i);
let j = rng.random_range(0..available.len());
let q1 = available.swap_remove(j);
c.add_gate(Gate::Cx, &[q0, q1]);
}
}
c
}
pub fn independent_bell_pairs(n_pairs: usize) -> Circuit {
let n = n_pairs * 2;
let mut c = Circuit::new(n, 0);
for i in 0..n_pairs {
c.add_gate(Gate::H, &[2 * i]);
c.add_gate(Gate::Cx, &[2 * i, 2 * i + 1]);
}
c
}
pub fn partially_independent_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
assert!(
n >= 4,
"partially_independent_circuit needs at least 4 qubits"
);
let big = n - 2;
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let singles = [Gate::H, Gate::X, Gate::Y, Gate::Z, Gate::S, Gate::T];
for layer in 0..depth {
for q in 0..big {
c.add_gate(singles[rng.random_range(0..singles.len())].clone(), &[q]);
}
for q in (layer % 2..big - 1).step_by(2) {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
}
for q in 0..big - 1 {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
for layer in 0..depth {
c.add_gate(singles[rng.random_range(0..singles.len())].clone(), &[big]);
c.add_gate(
singles[rng.random_range(0..singles.len())].clone(),
&[big + 1],
);
if layer == 0 {
c.add_gate(Gate::Cx, &[big, big + 1]);
}
}
c
}
pub fn measure_split_circuit(n: usize, layers: usize, seed: u64) -> Circuit {
assert!(n >= 8, "measure_split_circuit needs at least 8 qubits");
let big = n - 2;
let collapsed: Vec<usize> = (1..big).step_by(2).collect();
let kept: Vec<usize> = (0..big).step_by(2).collect();
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, collapsed.len().div_ceil(2));
for q in 0..big {
c.add_gate(Gate::Ry(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
for q in 0..big - 1 {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
for q in 0..big {
c.add_gate(Gate::Rz(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
for (index, &q) in collapsed.iter().enumerate() {
if index % 2 == 0 {
c.add_measure(q, index / 2);
} else {
c.add_reset(q);
}
}
for layer in 0..layers {
for &q in &kept {
c.add_gate(Gate::Ry(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
c.add_gate(Gate::Rz(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
for pair in kept[layer % 2..].as_chunks::<2>().0 {
c.add_gate(Gate::Cx, &[pair[0], pair[1]]);
}
}
c.add_gate(
Gate::Ry(rng.random::<f64>() * std::f64::consts::TAU),
&[big],
);
c.add_gate(Gate::Cx, &[big, big + 1]);
c
}
pub fn independent_random_blocks(
num_blocks: usize,
block_size: usize,
depth: usize,
seed: u64,
) -> Circuit {
let n = num_blocks * block_size;
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let singles = [Gate::H, Gate::X, Gate::Y, Gate::Z, Gate::S, Gate::T];
for block in 0..num_blocks {
let base = block * block_size;
for layer in 0..depth {
for q in 0..block_size {
c.add_gate(
singles[rng.random_range(0..singles.len())].clone(),
&[base + q],
);
}
if block_size >= 2 {
let offset = layer % 2;
for q in (offset..block_size - 1).step_by(2) {
if rng.random_bool(0.5) {
c.add_gate(Gate::Cx, &[base + q, base + q + 1]);
}
}
}
}
}
c
}
pub fn ghz_circuit(n: usize) -> Circuit {
let mut c = Circuit::new(n, 0);
c.add_gate(Gate::H, &[0]);
for q in 0..n - 1 {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
c
}
pub fn qaoa_circuit(n: usize, layers: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for _ in 0..layers {
for q in 0..n - 1 {
let gamma: f64 = rng.random::<f64>() * std::f64::consts::TAU;
c.add_gate(Gate::Rzz(gamma), &[q, q + 1]);
}
for q in 0..n {
let beta: f64 = rng.random::<f64>() * std::f64::consts::TAU;
c.add_gate(Gate::Rx(beta), &[q]);
}
}
c
}
pub fn disjoint_block_layers_circuit(n: usize, k: usize, layers: usize, seed: u64) -> Circuit {
assert!(
k >= 2 && n.is_multiple_of(k) && n / k >= 2,
"disjoint_block_layers_circuit needs at least two blocks of at least two qubits"
);
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for _ in 0..layers {
for q in 0..n {
c.add_gate(Gate::Rx(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
for block in 0..n / k {
let base = block * k;
for i in 0..k {
let theta = rng.random::<f64>() * std::f64::consts::TAU;
c.add_gate(Gate::Rzz(theta), &[base + i, base + (i + 1) % k]);
}
}
}
c
}
pub fn diagonal_mixed_circuit(n: usize, layers: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for q in 0..n {
c.add_gate(Gate::H, &[q]);
}
for layer in 0..layers {
for q in 0..n {
let theta = rng.random::<f64>() * std::f64::consts::TAU;
match rng.random_range(0..3) {
0 => c.add_gate(Gate::Rz(theta), &[q]),
1 => c.add_gate(Gate::T, &[q]),
_ => c.add_gate(Gate::P(theta), &[q]),
}
}
let stride = 1 + layer % (n / 2);
for q in 0..n / 2 {
let q0 = 2 * q;
let q1 = (q0 + stride) % n;
let theta = rng.random::<f64>() * std::f64::consts::TAU;
match rng.random_range(0..3) {
0 => c.add_gate(Gate::Cz, &[q0, q1]),
1 => c.add_gate(Gate::cphase(theta), &[q0, q1]),
_ => c.add_gate(Gate::Rzz(theta), &[q0, q1]),
}
}
}
c
}
pub fn single_qubit_rotation_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for _ in 0..depth {
for q in 0..n {
let choice: usize = rng.random_range(0..3);
let angle: f64 = rng.random::<f64>() * std::f64::consts::TAU;
match choice {
0 => c.add_gate(Gate::Rx(angle), &[q]),
1 => c.add_gate(Gate::Ry(angle), &[q]),
_ => c.add_gate(Gate::Rz(angle), &[q]),
}
}
}
c
}
pub fn clifford_t_circuit(n: usize, depth: usize, t_fraction: f64, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let cliffords = [Gate::H, Gate::S, Gate::X, Gate::Y, Gate::Z];
for layer in 0..depth {
for q in 0..n {
if rng.random::<f64>() < t_fraction {
if rng.random_bool(0.5) {
c.add_gate(Gate::T, &[q]);
} else {
c.add_gate(Gate::Tdg, &[q]);
}
} else {
c.add_gate(
cliffords[rng.random_range(0..cliffords.len())].clone(),
&[q],
);
}
}
let offset = layer % 2;
for q in (offset..n.saturating_sub(1)).step_by(2) {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
}
c
}
pub fn w_state_circuit(n: usize) -> Circuit {
let mut c = Circuit::new(n, 0);
c.add_gate(Gate::X, &[0]);
for i in 0..n - 1 {
let remaining = (n - i) as f64;
let theta = 2.0 * (1.0 / remaining).sqrt().acos();
c.add_gate(Gate::Ry(theta / 2.0), &[i + 1]);
c.add_gate(Gate::Cx, &[i, i + 1]);
c.add_gate(Gate::Ry(-theta / 2.0), &[i + 1]);
c.add_gate(Gate::Cx, &[i, i + 1]);
c.add_gate(Gate::Cx, &[i + 1, i]);
}
c
}
pub fn quantum_volume_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let n_pairs = n / 2;
for _ in 0..depth {
let mut perm: Vec<usize> = (0..n).collect();
for i in (1..n).rev() {
let j = rng.random_range(0..=i);
perm.swap(i, j);
}
for p in 0..n_pairs {
let q0 = perm[2 * p];
let q1 = perm[2 * p + 1];
let a0: f64 = rng.random::<f64>() * std::f64::consts::TAU;
let a1: f64 = rng.random::<f64>() * std::f64::consts::TAU;
let a2: f64 = rng.random::<f64>() * std::f64::consts::TAU;
c.add_gate(Gate::Ry(a0), &[q0]);
c.add_gate(Gate::Rz(a1), &[q0]);
c.add_gate(Gate::Ry(a2), &[q1]);
c.add_gate(Gate::Cx, &[q0, q1]);
let b0: f64 = rng.random::<f64>() * std::f64::consts::TAU;
let b1: f64 = rng.random::<f64>() * std::f64::consts::TAU;
let b2: f64 = rng.random::<f64>() * std::f64::consts::TAU;
c.add_gate(Gate::Ry(b0), &[q0]);
c.add_gate(Gate::Rz(b1), &[q1]);
c.add_gate(Gate::Cx, &[q0, q1]);
c.add_gate(Gate::Ry(b2), &[q0]);
}
}
c
}
pub fn local_clifford_blocks(
num_blocks: usize,
block_size: usize,
depth: usize,
seed: u64,
) -> Circuit {
let n = num_blocks * block_size;
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let cliffords = [Gate::H, Gate::S, Gate::X, Gate::Y, Gate::Z];
for block in 0..num_blocks {
let base = block * block_size;
for layer in 0..depth {
for q in 0..block_size {
c.add_gate(
cliffords[rng.random_range(0..cliffords.len())].clone(),
&[base + q],
);
}
if block_size >= 2 {
let offset = layer % 2;
for q in (offset..block_size - 1).step_by(2) {
c.add_gate(Gate::Cx, &[base + q, base + q + 1]);
}
}
}
}
c
}
pub fn clifford_prefix_measured_blocks(
num_blocks: usize,
block_size: usize,
depth: usize,
seed: u64,
) -> Circuit {
assert!(
block_size >= 2,
"clifford_prefix_measured_blocks needs at least 2 qubits per block"
);
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(num_blocks * block_size, num_blocks * (block_size + 1));
let cliffords = [Gate::H, Gate::S, Gate::X, Gate::Y, Gate::Z];
for block in 0..num_blocks {
let base = block * block_size;
for layer in 0..depth {
for q in 0..block_size {
c.add_gate(
cliffords[rng.random_range(0..cliffords.len())].clone(),
&[base + q],
);
}
for q in (layer % 2..block_size - 1).step_by(2) {
c.add_gate(Gate::Cx, &[base + q, base + q + 1]);
}
}
}
let mut rotations = |c: &mut Circuit, base: usize| {
for q in base..base + block_size {
c.add_gate(Gate::Ry(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
c.add_gate(Gate::Rz(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
for q in base..base + block_size - 1 {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
};
for block in 0..num_blocks {
let base = block * block_size;
let mid = block * (block_size + 1);
rotations(&mut c, base);
c.add_measure(base, mid);
c.instructions.push(Instruction::Conditional {
condition: ClassicalCondition::BitIsOne(mid),
gate: Gate::X,
targets: smallvec![base + 1],
});
c.add_reset(base);
rotations(&mut c, base);
for q in 0..block_size {
c.add_measure(base + q, mid + 1 + q);
}
}
c
}
pub fn cz_chain_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
let singles = [Gate::H, Gate::S, Gate::T, Gate::X];
for layer in 0..depth {
for q in 0..n {
c.add_gate(singles[rng.random_range(0..singles.len())].clone(), &[q]);
}
let offset = layer % 2;
for q in (offset..n.saturating_sub(1)).step_by(2) {
c.add_gate(Gate::Cz, &[q, q + 1]);
}
}
c
}
pub fn brickwork_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for layer in 0..depth {
for q in 0..n {
c.add_gate(Gate::Ry(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
c.add_gate(Gate::Rz(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
let offset = layer % 2;
for q in (offset..n.saturating_sub(1)).step_by(2) {
c.add_gate(Gate::Cz, &[q, q + 1]);
}
}
c
}
pub fn matched_brickwork_circuit(n: usize, depth: usize, seed: u64) -> Circuit {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for _ in 0..depth {
for q in 0..n {
c.add_gate(Gate::Ry(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
c.add_gate(Gate::Rz(rng.random::<f64>() * std::f64::consts::TAU), &[q]);
}
let mut order: Vec<usize> = (0..n).collect();
for i in (1..n).rev() {
let j = rng.random_range(0..=i);
order.swap(i, j);
}
for pair in order.as_chunks::<2>().0 {
c.add_gate(Gate::Cz, &[pair[0], pair[1]]);
}
}
c
}
pub fn sparse_walk_circuit(n: usize, k: usize, depth: usize, seed: u64) -> Circuit {
assert!(
k <= n && n >= 2,
"sparse_walk_circuit needs k <= n and n >= 2"
);
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut c = Circuit::new(n, 0);
for q in 0..k {
c.add_gate(Gate::H, &[q]);
}
for layer in 0..depth {
for q in 0..n {
let theta = rng.random::<f64>() * std::f64::consts::TAU;
match rng.random_range(0..4) {
0 => c.add_gate(Gate::Rz(theta), &[q]),
1 => c.add_gate(Gate::P(theta), &[q]),
2 => c.add_gate(Gate::T, &[q]),
_ => c.add_gate(Gate::X, &[q]),
}
}
let offset = layer % 2;
for q in (offset..n - 1).step_by(2) {
c.add_gate(Gate::Cx, &[q, q + 1]);
}
for q in (offset..n - 1).step_by(5) {
let theta = rng.random::<f64>() * std::f64::consts::TAU;
c.add_gate(Gate::Rzz(theta), &[q, q + 1]);
}
c.add_gate(Gate::Swap, &[layer % (n - 1), layer % (n - 1) + 1]);
}
c
}
pub(crate) fn apply_inverse_qft(c: &mut Circuit, start: usize, n: usize) {
for i in 0..n / 2 {
c.add_gate(Gate::Swap, &[start + i, start + n - 1 - i]);
}
for q in 0..n {
for k in (0..q).rev() {
let theta = std::f64::consts::TAU / (1u64 << (q - k + 1)) as f64;
c.add_gate(Gate::cphase(-theta), &[start + k, start + q]);
}
c.add_gate(Gate::H, &[start + q]);
}
}
pub fn phase_estimation_circuit(n: usize) -> Circuit {
let n_counting = n - 1;
let target = n_counting;
let mut c = Circuit::new(n, 0);
c.add_gate(Gate::X, &[target]);
for i in 0..n_counting {
c.add_gate(Gate::H, &[i]);
}
for k in 0..n_counting {
let theta = std::f64::consts::FRAC_PI_4 * (1u64 << k) as f64;
c.add_gate(Gate::cphase(theta), &[k, target]);
}
apply_inverse_qft(&mut c, 0, n_counting);
c
}
pub fn jordan_wigner_hamiltonian(
n: usize,
max_terms: usize,
seed: u64,
) -> Vec<(f64, Vec<PauliTerm>)> {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut accumulated: std::collections::HashMap<Vec<PauliTerm>, Complex64> =
std::collections::HashMap::new();
let mut absorb = |strings: Vec<PauliString>, weight: f64| {
for string in strings {
let mut factors: Vec<PauliTerm> = string
.axes
.iter()
.enumerate()
.filter_map(|(qubit, axis)| axis.map(|axis| PauliTerm::new(qubit, axis)))
.collect();
factors.sort_unstable_by_key(|term| term.qubit);
*accumulated
.entry(factors)
.or_insert(Complex64::new(0.0, 0.0)) += string.phase * weight;
}
};
for p in 0..n {
for q in p..n {
let t = rng.random::<f64>() * 2.0 - 1.0;
let hop = ladder_product(n, &[(p, true), (q, false)]);
if p == q {
absorb(hop, t);
} else {
absorb(hop, t);
absorb(ladder_product(n, &[(q, true), (p, false)]), t);
}
}
}
for _ in 0..3 * n * n {
let mut pair = || loop {
let a = rng.random_range(0..n);
let b = rng.random_range(0..n);
if a != b {
return (a.min(b), a.max(b));
}
};
let (p, q) = pair();
let (r, s) = pair();
let v = rng.random::<f64>() * 2.0 - 1.0;
absorb(
ladder_product(n, &[(p, true), (q, true), (r, false), (s, false)]),
v,
);
absorb(
ladder_product(n, &[(s, true), (r, true), (q, false), (p, false)]),
v,
);
}
let mut terms: Vec<(f64, Vec<PauliTerm>)> = accumulated
.into_iter()
.filter(|(_, coefficient)| coefficient.re.abs() > 1e-12)
.map(|(factors, coefficient)| {
debug_assert!(coefficient.im.abs() < 1e-9);
(coefficient.re, factors)
})
.collect();
terms.sort_by(|a, b| b.0.abs().total_cmp(&a.0.abs()).then_with(|| a.1.cmp(&b.1)));
terms.truncate(max_terms);
terms
}
struct PauliString {
phase: Complex64,
axes: Vec<Option<PauliAxis>>,
}
fn pauli_axis_mul(a: PauliAxis, b: PauliAxis) -> (Complex64, Option<PauliAxis>) {
use PauliAxis::{X, Y, Z};
let i = Complex64::new(0.0, 1.0);
match (a, b) {
(X, X) | (Y, Y) | (Z, Z) => (Complex64::new(1.0, 0.0), None),
(X, Y) => (i, Some(Z)),
(Y, X) => (-i, Some(Z)),
(Y, Z) => (i, Some(X)),
(Z, Y) => (-i, Some(X)),
(Z, X) => (i, Some(Y)),
(X, Z) => (-i, Some(Y)),
}
}
fn pauli_string_mul(a: &PauliString, b: &PauliString) -> PauliString {
let mut phase = a.phase * b.phase;
let axes = a
.axes
.iter()
.zip(&b.axes)
.map(|(&left, &right)| match (left, right) {
(None, axis) | (axis, None) => axis,
(Some(left), Some(right)) => {
let (factor, axis) = pauli_axis_mul(left, right);
phase *= factor;
axis
}
})
.collect();
PauliString { phase, axes }
}
fn ladder_product(n: usize, ops: &[(usize, bool)]) -> Vec<PauliString> {
let mut product = vec![PauliString {
phase: Complex64::new(1.0, 0.0),
axes: vec![None; n],
}];
for &(mode, is_creation) in ops {
let mut x_axes = vec![None; n];
for chain in x_axes.iter_mut().take(mode) {
*chain = Some(PauliAxis::Z);
}
let mut y_axes = x_axes.clone();
x_axes[mode] = Some(PauliAxis::X);
y_axes[mode] = Some(PauliAxis::Y);
let y_phase = Complex64::new(0.0, if is_creation { -0.5 } else { 0.5 });
let factor = [
PauliString {
phase: Complex64::new(0.5, 0.0),
axes: x_axes,
},
PauliString {
phase: y_phase,
axes: y_axes,
},
];
product = product
.iter()
.flat_map(|left| factor.iter().map(|right| pauli_string_mul(left, right)))
.collect();
}
product
}