Skip to content

Commit ba4dd7a

Browse files
Add new API for inserting multiple operations into the DAGCircuit in Rust. (Qiskit#13335)
* Initial: Add `DAGCircuitConcat` struct and new insertion API. - Add struct that allows continuous addition of instructions into the back of the `DAGCircuit` by performing edge modifications in the output nodes at the end of the additions. - The new struct includes the following new methods: - `push_operation_back` which pushes a valid `PackedInstruction` into the back of the `DAGCircuit`. It behaves very similarly to `push_back`. - `apply_operation_back` which inserts a new operation into the back of the `DAGCircuit`. It has two variants which depend on the ownership of the bit indices (`Qubit`, `Clbit`). - `pack_instruction`: private method that basically does just that, it creates a valid instance of `PackedInstruction` based on other provided atributes. - Modified `DAGCircuit::extend` to use this new API, without any considerable losses. * Add: `OwnedOrSlice<T>` - Add struct that allows usage of both slice or owned collections of bit indices. - Leverage use of `as_concat()` in `circuit_to_dag`. * Add: Optional qargs/cargs in `apply_operation_back` - Leverage usage of new methods in `UnitarySynthesis` after Qiskit#13141 merged. * Fix: Leverage usage in `BasisTranslator` * Fix: Replace `OnceCell` with `OnceLock` * Fix: Remove `SliceOrOwned` in favor of `Cow` * Fix: Expose the interners via references within `DAGCircuitConcat`. * Refactor: `DAGCircuit::as_concat` to `DAGCircuit::into_concat`. - Allow `DAGCircuitConcat` to take ownership of the original `DAGCircuit` it is being called from, and allow `DAGCircuitConcat::end` to return the original `DAGCircuit`. - Bypass taking ownership of the `DAGCircuit` during `DAGCircuit::extend` calls by swapping with a temporary replacement. * Fix: Remove inefficient code from `UnitarySynthesis` - A previous commit added some inefficient code into the `apply_synth_dag` function in `UnitarySynthesis`. The newer code should be much simpler to use. - Rename interner views in `DAGCircuitConcat` to match their `DAGCircuit` counterparts. * Fix: Remove obsolete pytoken. * Update crates/circuit/src/dag_circuit.rs Co-authored-by: Kevin Hartman <[email protected]> * Fix: Address comments from code review - Create `_push_back` method to perform initial additions and parsing before pushing an instruction into the circuit. This will be used by both the `DAGCircuit` and the `DAGCircuitBuilder`. - Fix `DAGCircuitBuilder::end` docstring. - Remove `qarg/carg_interner_mut`, replaced by `insert_qargs/cargs`. * Apply suggestions from code review Co-authored-by: Kevin Hartman <[email protected]> * Fix: Address review comments - Rename `_push_back` to `get_classical_resources`. - Rename variables to keep the naming scheme. - Remove `increment_op` step from `get_classical_resources`. * Fix: Limit `get_classical_resources` to only `Clbits` and `Vars`. - Remove `DAGNodeInfo`. * Fix: Missing `py` token added after modification to `DAGCircuit::num_vars()` * Apply suggestions from code review Co-authored-by: Kevin Hartman <[email protected]> * Fix: Remove usage of `end`. - Remove mutably borrowing the `DAGCircuit` in `get_classical_resources`. --------- Co-authored-by: Kevin Hartman <[email protected]>
1 parent f71d201 commit ba4dd7a

File tree

3 files changed

+351
-243
lines changed

3 files changed

+351
-243
lines changed

crates/accelerate/src/basis/basis_translator/mod.rs

Lines changed: 34 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use pyo3::types::{IntoPyDict, PyComplex, PyDict, PyTuple};
2929
use pyo3::PyTypeInfo;
3030
use qiskit_circuit::circuit_instruction::OperationFromPython;
3131
use qiskit_circuit::converters::circuit_to_dag;
32+
use qiskit_circuit::dag_circuit::DAGCircuitBuilder;
3233
use qiskit_circuit::imports::DAG_TO_CIRCUIT;
3334
use qiskit_circuit::imports::PARAMETER_EXPRESSION;
3435
use qiskit_circuit::operations::Param;
@@ -460,7 +461,8 @@ fn apply_translation(
460461
>,
461462
) -> PyResult<(DAGCircuit, bool)> {
462463
let mut is_updated = false;
463-
let mut out_dag = dag.copy_empty_like(py, "alike")?;
464+
let out_dag = dag.copy_empty_like(py, "alike")?;
465+
let mut out_dag_builder = out_dag.into_builder(py);
464466
for node in dag.topological_op_nodes()? {
465467
let node_obj = dag[node].unwrap_operation();
466468
let node_qarg = dag.get_qargs(node_obj.qubits);
@@ -504,38 +506,28 @@ fn apply_translation(
504506
new_op = Some(replaced_blocks.extract()?);
505507
}
506508
if let Some(new_op) = new_op {
507-
out_dag.apply_operation_back(
509+
out_dag_builder.apply_operation_back(
508510
py,
509511
new_op.operation,
510-
node_qarg,
511-
node_carg,
512+
Some(node_qarg.into()),
513+
Some(node_carg.into()),
512514
if new_op.params.is_empty() {
513515
None
514516
} else {
515-
Some(new_op.params)
517+
Some(Box::new(new_op.params))
516518
},
517-
new_op.label.map(|x| *x),
519+
new_op.label.as_deref().cloned(),
518520
#[cfg(feature = "cache_pygates")]
519521
None,
520522
)?;
521523
} else {
522-
out_dag.apply_operation_back(
524+
out_dag_builder.apply_operation_back(
523525
py,
524526
node_obj.op.clone(),
525-
node_qarg,
526-
node_carg,
527-
if node_obj.params_view().is_empty() {
528-
None
529-
} else {
530-
Some(
531-
node_obj
532-
.params_view()
533-
.iter()
534-
.map(|param| param.clone_ref(py))
535-
.collect(),
536-
)
537-
},
538-
node_obj.label.as_ref().map(|x| x.as_ref().clone()),
527+
Some(node_qarg.into()),
528+
Some(node_carg.into()),
529+
node_obj.params.clone(),
530+
node_obj.label.as_deref().cloned(),
539531
#[cfg(feature = "cache_pygates")]
540532
None,
541533
)?;
@@ -547,23 +539,13 @@ fn apply_translation(
547539
if qargs_with_non_global_operation.contains_key(&node_qarg_as_physical)
548540
&& qargs_with_non_global_operation[&node_qarg_as_physical].contains(node_obj.op.name())
549541
{
550-
out_dag.apply_operation_back(
542+
out_dag_builder.apply_operation_back(
551543
py,
552544
node_obj.op.clone(),
553-
node_qarg,
554-
node_carg,
555-
if node_obj.params_view().is_empty() {
556-
None
557-
} else {
558-
Some(
559-
node_obj
560-
.params_view()
561-
.iter()
562-
.map(|param| param.clone_ref(py))
563-
.collect(),
564-
)
565-
},
566-
node_obj.label.as_ref().map(|x| x.as_ref().clone()),
545+
Some(node_qarg.into()),
546+
Some(node_carg.into()),
547+
node_obj.params.clone(),
548+
node_obj.label.as_deref().cloned(),
567549
#[cfg(feature = "cache_pygates")]
568550
None,
569551
)?;
@@ -578,14 +560,14 @@ fn apply_translation(
578560
if extra_inst_map.contains_key(&unique_qargs) {
579561
replace_node(
580562
py,
581-
&mut out_dag,
563+
&mut out_dag_builder,
582564
node_obj.clone(),
583565
&extra_inst_map[&unique_qargs],
584566
)?;
585567
} else if instr_map
586568
.contains_key(&(node_obj.op.name().to_string(), node_obj.op.num_qubits()))
587569
{
588-
replace_node(py, &mut out_dag, node_obj.clone(), instr_map)?;
570+
replace_node(py, &mut out_dag_builder, node_obj.clone(), instr_map)?;
589571
} else {
590572
return Err(TranspilerError::new_err(format!(
591573
"BasisTranslator did not map {}",
@@ -594,13 +576,12 @@ fn apply_translation(
594576
}
595577
is_updated = true;
596578
}
597-
598-
Ok((out_dag, is_updated))
579+
Ok((out_dag_builder.build(), is_updated))
599580
}
600581

601582
fn replace_node(
602583
py: Python,
603-
dag: &mut DAGCircuit,
584+
dag: &mut DAGCircuitBuilder,
604585
node: PackedInstruction,
605586
instr_map: &IndexMap<GateIdentifier, (SmallVec<[Param; 3]>, DAGCircuit), ahash::RandomState>,
606587
) -> PyResult<()> {
@@ -619,8 +600,8 @@ fn replace_node(
619600
if node.params_view().is_empty() {
620601
for inner_index in target_dag.topological_op_nodes()? {
621602
let inner_node = &target_dag[inner_index].unwrap_operation();
622-
let old_qargs = dag.get_qargs(node.qubits);
623-
let old_cargs = dag.get_cargs(node.clbits);
603+
let old_qargs = dag.qargs_interner().get(node.qubits);
604+
let old_cargs = dag.cargs_interner().get(node.clbits);
624605
let new_qubits: Vec<Qubit> = target_dag
625606
.get_qargs(inner_node.qubits)
626607
.iter()
@@ -641,18 +622,17 @@ fn replace_node(
641622
.iter()
642623
.map(|param| param.clone_ref(py))
643624
.collect();
644-
let new_extra_props = node.label.as_ref().map(|x| x.as_ref().clone());
645625
dag.apply_operation_back(
646626
py,
647627
new_op,
648-
&new_qubits,
649-
&new_clbits,
628+
Some(new_qubits.into()),
629+
Some(new_clbits.into()),
650630
if new_params.is_empty() {
651631
None
652632
} else {
653-
Some(new_params)
633+
Some(Box::new(new_params))
654634
},
655-
new_extra_props,
635+
node.label.as_deref().cloned(),
656636
#[cfg(feature = "cache_pygates")]
657637
None,
658638
)?;
@@ -665,8 +645,8 @@ fn replace_node(
665645
.into_py_dict(py)?;
666646
for inner_index in target_dag.topological_op_nodes()? {
667647
let inner_node = &target_dag[inner_index].unwrap_operation();
668-
let old_qargs = dag.get_qargs(node.qubits);
669-
let old_cargs = dag.get_cargs(node.clbits);
648+
let old_qargs = dag.qargs_interner().get(node.qubits);
649+
let old_cargs = dag.cargs_interner().get(node.clbits);
670650
let new_qubits: Vec<Qubit> = target_dag
671651
.get_qargs(inner_node.qubits)
672652
.iter()
@@ -749,14 +729,14 @@ fn replace_node(
749729
dag.apply_operation_back(
750730
py,
751731
new_op,
752-
&new_qubits,
753-
&new_clbits,
732+
Some(new_qubits.into()),
733+
Some(new_clbits.into()),
754734
if new_params.is_empty() {
755735
None
756736
} else {
757-
Some(new_params)
737+
Some(Box::new(new_params))
758738
},
759-
inner_node.label.as_ref().map(|x| x.as_ref().clone()),
739+
inner_node.label.as_deref().cloned(),
760740
#[cfg(feature = "cache_pygates")]
761741
None,
762742
)?;

crates/accelerate/src/unitary_synthesis.rs

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
#![allow(clippy::too_many_arguments)]
1313

1414
use std::f64::consts::PI;
15-
#[cfg(feature = "cache_pygates")]
16-
use std::sync::OnceLock;
1715

1816
use approx::relative_eq;
1917
use hashbrown::{HashMap, HashSet};
@@ -23,6 +21,7 @@ use ndarray::prelude::*;
2321
use num_complex::{Complex, Complex64};
2422
use numpy::IntoPyArray;
2523
use qiskit_circuit::circuit_instruction::OperationFromPython;
24+
use qiskit_circuit::dag_circuit::DAGCircuitBuilder;
2625
use smallvec::{smallvec, SmallVec};
2726

2827
use pyo3::intern;
@@ -134,7 +133,7 @@ fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet
134133
/// it should be applied.
135134
fn apply_synth_dag(
136135
py: Python<'_>,
137-
out_dag: &mut DAGCircuit,
136+
out_dag: &mut DAGCircuitBuilder,
138137
out_qargs: &[Qubit],
139138
synth_dag: &DAGCircuit,
140139
) -> PyResult<()> {
@@ -145,7 +144,7 @@ fn apply_synth_dag(
145144
.iter()
146145
.map(|qarg| out_qargs[qarg.0 as usize])
147146
.collect();
148-
out_packed_instr.qubits = out_dag.qargs_interner.insert(&mapped_qargs);
147+
out_packed_instr.qubits = out_dag.insert_qargs(mapped_qargs.into());
149148
out_dag.push_back(py, out_packed_instr)?;
150149
}
151150
out_dag.add_global_phase(&synth_dag.get_global_phase())?;
@@ -158,11 +157,10 @@ fn apply_synth_dag(
158157
/// so `out_qargs` is used to track the final qubit ids where they should be applied.
159158
fn apply_synth_sequence(
160159
py: Python<'_>,
161-
out_dag: &mut DAGCircuit,
160+
out_dag: &mut DAGCircuitBuilder,
162161
out_qargs: &[Qubit],
163162
sequence: &TwoQubitUnitarySequence,
164163
) -> PyResult<()> {
165-
let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len());
166164
for (gate, params, qubit_ids) in sequence.gate_sequence.gates() {
167165
let packed_op = match gate {
168166
None => &sequence.decomp_op,
@@ -212,18 +210,17 @@ fn apply_synth_sequence(
212210
}
213211
};
214212

215-
let instruction = PackedInstruction {
216-
op: new_op,
217-
qubits: out_dag.qargs_interner.insert(&mapped_qargs),
218-
clbits: out_dag.cargs_interner.get_default(),
219-
params: new_params,
220-
label: None,
213+
out_dag.apply_operation_back(
214+
py,
215+
new_op,
216+
Some(mapped_qargs.into()),
217+
None,
218+
new_params,
219+
None,
221220
#[cfg(feature = "cache_pygates")]
222-
py_op: OnceLock::new(),
223-
};
224-
instructions.push(instruction);
221+
None,
222+
)?;
225223
}
226-
out_dag.extend(py, instructions.into_iter())?;
227224
out_dag.add_global_phase(&Param::Float(sequence.gate_sequence.global_phase()))?;
228225
Ok(())
229226
}
@@ -374,10 +371,11 @@ fn py_run_main_loop(
374371
PhysicalQubit::new(qubit_indices[out_qargs[0].0 as usize] as u32),
375372
PhysicalQubit::new(qubit_indices[out_qargs[1].0 as usize] as u32),
376373
];
377-
let apply_original_op = |out_dag: &mut DAGCircuit| -> PyResult<()> {
374+
let apply_original_op = |out_dag: &mut DAGCircuitBuilder| -> PyResult<()> {
378375
out_dag.push_back(py, packed_instr.clone())?;
379376
Ok(())
380377
};
378+
let mut builder = out_dag.into_builder(py);
381379
run_2q_unitary_synthesis(
382380
py,
383381
unitary,
@@ -388,10 +386,11 @@ fn py_run_main_loop(
388386
approximation_degree,
389387
natural_direction,
390388
pulse_optimize,
391-
&mut out_dag,
389+
&mut builder,
392390
out_qargs,
393391
apply_original_op,
394392
)?;
393+
out_dag = builder.build()
395394
}
396395
// Run 3q+ synthesis
397396
_ => {
@@ -409,7 +408,9 @@ fn py_run_main_loop(
409408
None,
410409
)?;
411410
let out_qargs = dag.get_qargs(packed_instr.qubits);
412-
apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?;
411+
let mut dag_builder = out_dag.into_builder(py);
412+
apply_synth_dag(py, &mut dag_builder, out_qargs, &synth_dag)?;
413+
out_dag = dag_builder.build();
413414
}
414415
}
415416
}
@@ -1071,8 +1072,9 @@ fn reversed_synth_su4_dag(
10711072
unreachable!("reversed_synth_su4_dag should only be called for XXDecomposer")
10721073
};
10731074

1074-
let mut target_dag = synth_dag.copy_empty_like(py, "alike")?;
1075+
let target_dag = synth_dag.copy_empty_like(py, "alike")?;
10751076
let flip_bits: [Qubit; 2] = [Qubit(1), Qubit(0)];
1077+
let mut target_dag_builder = target_dag.into_builder(py);
10761078
for node in synth_dag.topological_op_nodes()? {
10771079
let mut inst = synth_dag[node].unwrap_operation().clone();
10781080
let qubits: Vec<Qubit> = synth_dag
@@ -1081,10 +1083,10 @@ fn reversed_synth_su4_dag(
10811083
.iter()
10821084
.map(|x| flip_bits[x.0 as usize])
10831085
.collect();
1084-
inst.qubits = target_dag.qargs_interner.insert_owned(qubits);
1085-
target_dag.push_back(py, inst)?;
1086+
inst.qubits = target_dag_builder.insert_qargs(qubits.into());
1087+
target_dag_builder.push_back(py, inst)?;
10861088
}
1087-
Ok(target_dag)
1089+
Ok(target_dag_builder.build())
10881090
}
10891091

10901092
/// Score the synthesis output (DAG or sequence) based on the expected gate fidelity/error score.
@@ -1162,9 +1164,9 @@ fn run_2q_unitary_synthesis(
11621164
approximation_degree: Option<f64>,
11631165
natural_direction: Option<bool>,
11641166
pulse_optimize: Option<bool>,
1165-
out_dag: &mut DAGCircuit,
1167+
out_dag: &mut DAGCircuitBuilder,
11661168
out_qargs: &[Qubit],
1167-
mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>,
1169+
mut apply_original_op: impl FnMut(&mut DAGCircuitBuilder) -> PyResult<()>,
11681170
) -> PyResult<()> {
11691171
// Find decomposer candidates
11701172
let decomposers = match target {

0 commit comments

Comments
 (0)