Skip to content

Commit 31f616f

Browse files
committed
Use the peephole pass heuristic for best synthesis selection
Previously there was a mismatch between the scoring of synthesis results and the peephole pass's comparison with the original block. The pass is documented as using the tuple (num_2q_gates, error, num_gates) and picking the min of all the choices. But, when we called the unitary synthesis function that selects the best synthesis outcome it was maximizing the estimated fidelity but not considering the gate counts like the pass is documented as doing. This corrects this mismatch by updating the function doing the synthesis to be generic on score type and taking a scorer callback. This lets the peephole pass control the heuristic used for selecting the best score.
1 parent 56ea5aa commit 31f616f

2 files changed

Lines changed: 60 additions & 16 deletions

File tree

crates/transpiler/src/passes/two_qubit_peephole.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use qiskit_circuit::operations::{
3434
use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation};
3535
use qiskit_circuit::{BlocksMode, Qubit, VarsMode};
3636

37+
use super::unitary_synthesis::Direction2q;
3738
use crate::passes::unitary_synthesis::{
3839
Approximation, QpuConstraint, TwoQSynthesisResult, fidelity_2q_sequence, synthesize_2q_matrix,
3940
};
@@ -42,10 +43,11 @@ use crate::target::Target;
4243
use qiskit_circuit::PhysicalQubit;
4344
use qiskit_synthesis::linalg::nalgebra_array_view;
4445
use qiskit_synthesis::matrix::two_qubit::blocks_to_matrix;
46+
use qiskit_synthesis::two_qubit_decompose::TwoQubitGateSequence;
4547
use qiskit_util::getenv_use_multiple_threads;
4648
use thread_local::ThreadLocal;
4749

48-
type MappingIterItem = Option<(TwoQSynthesisResult, [Qubit; 2])>;
50+
type MappingIterItem = Option<(TwoQSynthesisResult<f64>, [Qubit; 2])>;
4951

5052
// This is a separate function in case we need to handle any Python synchronization in the future
5153
// (such as releasing the GIL). For right now this doesn't seem to be necessary, but keeping it
@@ -59,6 +61,24 @@ pub fn py_two_qubit_unitary_peephole_optimize(
5961
two_qubit_unitary_peephole_optimize(dag, target, approximation_degree)
6062
}
6163

64+
fn score_sequence(
65+
dir: &Direction2q,
66+
sequence: &TwoQubitGateSequence,
67+
constraint: &QpuConstraint,
68+
qargs: [PhysicalQubit; 2],
69+
) -> (i64, f64, i64) {
70+
let fidelity = fidelity_2q_sequence(dir, sequence, constraint, qargs);
71+
// Make the gate counts negative because synthesize_2q_matrix picks the largest value
72+
// we want to minimize the gate counts.
73+
let gate_count = -(sequence.gates.len() as i64);
74+
let twoq_gate_count = -(sequence
75+
.gates
76+
.iter()
77+
.filter(|x| x.0.num_qubits() == 2)
78+
.count() as i64);
79+
(twoq_gate_count, fidelity, gate_count)
80+
}
81+
6282
/// This function runs the two qubit unitary peephole optimization pass
6383
///
6484
/// It returns None if there is no modifications/optimiations made to the input dag and the pass
@@ -108,6 +128,7 @@ pub fn two_qubit_unitary_peephole_optimize(
108128
q_phys,
109129
&mut synthesis_state.borrow_mut(),
110130
QpuConstraint::Target(target),
131+
score_sequence,
111132
)?;
112133
if result.is_none() {
113134
return Ok(None);
@@ -152,15 +173,20 @@ pub fn two_qubit_unitary_peephole_optimize(
152173
original_total_count,
153174
);
154175
let new_2q_count = result
155-
.sequence
156-
.gates
157-
.iter()
158-
.filter(|x| x.0.num_qubits() == 2)
159-
.count();
176+
.score
177+
.map(|score| -score.0 as usize)
178+
.unwrap_or_else(|| {
179+
result
180+
.sequence
181+
.gates
182+
.iter()
183+
.filter(|x| x.0.num_qubits() == 2)
184+
.count()
185+
});
160186
let new_gate_count = result.sequence.gates.len();
161187
let new_score = (
162188
new_2q_count,
163-
1. - result.fidelity.unwrap_or_else(|| {
189+
1. - result.score.map(|score| score.1).unwrap_or_else(|| {
164190
fidelity_2q_sequence(
165191
&result.dir,
166192
&result.sequence,
@@ -183,6 +209,11 @@ pub fn two_qubit_unitary_peephole_optimize(
183209
node_mapping[node.index()] = run_index;
184210
}
185211
substitution_made.store(true, std::sync::atomic::Ordering::Relaxed);
212+
let result = TwoQSynthesisResult {
213+
sequence: result.sequence,
214+
dir: result.dir,
215+
score: result.score.map(|score| score.1),
216+
};
186217
Ok(Some((result, q_virt)))
187218
};
188219

crates/transpiler/src/passes/unitary_synthesis/mod.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ use numpy::PyReadonlyArray2;
2323
use pyo3::prelude::*;
2424
use pyo3::{intern, wrap_pyfunction};
2525

26-
use self::decomposers::{Decomposer2q, DecomposerCache, Direction2q, FlipDirection};
26+
pub(crate) use self::decomposers::Direction2q;
27+
28+
use self::decomposers::{Decomposer2q, DecomposerCache, FlipDirection};
2729
use crate::QiskitError;
2830
use crate::target::Target;
2931
use qiskit_circuit::bit::QuantumRegister;
@@ -487,10 +489,10 @@ fn synthesize_1q_matrix_onto(
487489
}
488490

489491
#[derive(Debug)]
490-
pub struct TwoQSynthesisResult {
492+
pub struct TwoQSynthesisResult<S> {
491493
pub sequence: TwoQubitGateSequence,
492494
pub dir: Direction2q,
493-
pub fidelity: Option<f64>,
495+
pub score: Option<S>,
494496
}
495497

496498
/// Estimate the fidelity of a synthesized two qubit unitary synthesis output
@@ -544,12 +546,21 @@ pub(crate) fn fidelity_2q_sequence(
544546
/// `qargs_phys` - The physical qubits the unitary is being applied to
545547
/// `state` - The internal state of the pass, this includes the configured synthesizers
546548
/// `constraint` - The qpu constraints used for the synthesis, typically just a Target
547-
pub(crate) fn synthesize_2q_matrix(
549+
/// `fidelity_calculation` - A callable that is called with a two qubit synthesis output sequence and
550+
/// the context around it. It is expected to return a fidelity score to compare that synthesis output
551+
/// against other decomposers. The synthesis output with the maximum score is selected and is
552+
/// what is returned by the `synthesize_2q_matrix`.
553+
pub(crate) fn synthesize_2q_matrix<F, S>(
548554
mut unitary: CowArray<Complex64, Ix2>,
549555
qargs_phys: [PhysicalQubit; 2],
550556
state: &mut UnitarySynthesisState,
551557
constraint: QpuConstraint,
552-
) -> PyResult<Option<TwoQSynthesisResult>> {
558+
mut fidelity_calculation: F,
559+
) -> PyResult<Option<TwoQSynthesisResult<S>>>
560+
where
561+
F: FnMut(&Direction2q, &TwoQubitGateSequence, &QpuConstraint, [PhysicalQubit; 2]) -> S,
562+
S: PartialOrd,
563+
{
553564
let decomposer_cache = &mut state.cache;
554565
let config = &state.config;
555566

@@ -611,9 +622,9 @@ pub(crate) fn synthesize_2q_matrix(
611622
for sequence in sequences {
612623
let sequence = sequence?;
613624
let prev_fidelity = best_fidelity.unwrap_or_else(|| {
614-
fidelity_2q_sequence(&best_pair.0, &best_pair.1, &constraint, qargs_phys)
625+
fidelity_calculation(&best_pair.0, &best_pair.1, &constraint, qargs_phys)
615626
});
616-
let this_fidelity = fidelity_2q_sequence(&sequence.0, &sequence.1, &constraint, qargs_phys);
627+
let this_fidelity = fidelity_calculation(&sequence.0, &sequence.1, &constraint, qargs_phys);
617628
if this_fidelity > prev_fidelity {
618629
best_fidelity = Some(this_fidelity);
619630
best_pair = sequence;
@@ -624,7 +635,7 @@ pub(crate) fn synthesize_2q_matrix(
624635
Ok(Some(TwoQSynthesisResult {
625636
sequence: best_pair.1,
626637
dir: best_pair.0,
627-
fidelity: best_fidelity,
638+
score: best_fidelity,
628639
}))
629640
}
630641

@@ -636,7 +647,9 @@ fn synthesize_2q_matrix_onto(
636647
state: &mut UnitarySynthesisState,
637648
constraint: QpuConstraint,
638649
) -> PyResult<bool> {
639-
let Some(result) = synthesize_2q_matrix(unitary, qargs_phys, state, constraint)? else {
650+
let Some(result) =
651+
synthesize_2q_matrix(unitary, qargs_phys, state, constraint, fidelity_2q_sequence)?
652+
else {
640653
return Ok(false);
641654
};
642655
// ... now apply the best sequence.

0 commit comments

Comments
 (0)