Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auxiliary qubit tracking in HighLevelSynthesis #12911

Merged
merged 7 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Sasha's review comments
  • Loading branch information
Cryoris committed Aug 13, 2024
commit 40e7ede0ec11fcf06cf4b69418ad21c758265e2c
42 changes: 19 additions & 23 deletions qiskit/transpiler/passes/synthesis/high_level_synthesis.py
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@
from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper
from qiskit.utils.deprecation import deprecate_arg

from qiskit.circuit.annotated_operation import (
AnnotatedOperation,
Expand Down Expand Up @@ -367,12 +366,6 @@ class HighLevelSynthesis(TransformationPass):

"""

@deprecate_arg(
"use_qubit_indices",
since="1.3",
additional_msg="The qubit indices will then always be used.",
pending=True,
)
def __init__(
self,
hls_config: HLSConfig | None = None,
Expand Down Expand Up @@ -533,9 +526,12 @@ def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit:
qubit_map = {
qubit: index_to_qubit[index] for index, qubit in zip(qubits, op.qubits)
}
clbit_map = dict(zip(op.clbits, node.cargs))
for sub_node in op.op_nodes():
out.apply_operation_back(
sub_node.op, tuple(qubit_map[qarg] for qarg in sub_node.qargs)
sub_node.op,
tuple(qubit_map[qarg] for qarg in sub_node.qargs),
tuple(clbit_map[carg] for carg in sub_node.cargs),
)
out.global_phase += op.global_phase
else:
Expand Down Expand Up @@ -570,8 +566,7 @@ def _synthesize_operation(
mod.num_ctrl_qubits for mod in modifiers if isinstance(mod, ControlModifier)
)
baseop_qubits = qubits[num_ctrl:] # reminder: control qubits are the first ones
baseop_tracker = tracker.copy()
baseop_tracker.drop(qubits[:num_ctrl]) # no access to control qubits
baseop_tracker = tracker.copy(drop=qubits[:num_ctrl]) # no access to control qubits

# get qubits of base operation
synthesized_base_op, _ = self._synthesize_operation(
Expand All @@ -584,21 +579,22 @@ def _synthesize_operation(

synthesized = self._apply_annotations(synthesized_base_op, operation.modifiers)

# synthesize via HLS
elif len(hls_methods := self._methods_to_try(operation.name)) > 0:
# TODO once ``use_qubit_indices`` is removed from the initializer, just pass ``qubits``
# If it was no AnnotatedOperation, try synthesizing via HLS or by unrolling.
else:
# Try synthesis via HLS -- which will return ``None`` if unsuccessful.
indices = qubits if self._use_qubit_indices else None
synthesized = self._synthesize_op_using_plugins(
hls_methods,
operation,
indices,
tracker.num_clean(qubits),
tracker.num_dirty(qubits),
)
if len(hls_methods := self._methods_to_try(operation.name)) > 0:
synthesized = self._synthesize_op_using_plugins(
hls_methods,
operation,
indices,
tracker.num_clean(qubits),
tracker.num_dirty(qubits),
)

# try unrolling custom definitions
elif not self._top_level_only:
synthesized = self._unroll_custom_definition(operation, qubits)
# If HLS did not apply, or was unsuccessful, try unrolling custom definitions.
if synthesized is None and not self._top_level_only:
synthesized = self._unroll_custom_definition(operation, indices)

if synthesized is None:
# if we didn't synthesize, there was nothing to unroll, so just set the used qubits
Expand Down
21 changes: 17 additions & 4 deletions qiskit/transpiler/passes/synthesis/qubit_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@

@dataclass
class QubitTracker:
"""Track qubits (per index) and their state.
"""Track qubits (by global index) and their state.

The states are distinguished into clean (meaning in state :math:`|0\rangle`) or dirty (an
unkown state).
unknown state).
"""

# This could in future be extended to track different state types, if necessary.
Expand All @@ -49,7 +49,12 @@ def borrow(self, num_qubits: int, active_qubits: Iterable[int] | None = None) ->
if num_qubits > (available := len(available_qubits)):
raise RuntimeError(f"Cannot borrow {num_qubits} qubits, only {available} available.")

return available_qubits[:num_qubits]
# for now, prioritize returning clean qubits
available_clean = [qubit for qubit in available_qubits if qubit in self.clean]
available_dirty = [qubit for qubit in available_qubits if qubit in self.dirty]

borrowed = available_clean[:num_qubits]
return borrowed + available_dirty[: (num_qubits - len(borrowed))]

def used(self, qubits: Iterable[int], check: bool = True) -> None:
"""Set the state of ``qubits`` to used (i.e. False)."""
Expand Down Expand Up @@ -85,14 +90,22 @@ def drop(self, qubits: Iterable[int], check: bool = True) -> None:
self.clean -= qubits
self.dirty -= qubits

def copy(self, qubit_map: dict[int, int] | None = None) -> "QubitTracker":
def copy(
self, qubit_map: dict[int, int] | None = None, drop: Iterable[int] | None = None
) -> "QubitTracker":
"""Copy self.

Args:
qubit_map: If provided, apply the mapping ``{old_qubit: new_qubit}`` to
the qubits in the tracker. Only those old qubits in the mapping will be
part of the new one.
drop: If provided, drop these qubits in the copied tracker. This argument is ignored
if ``qubit_map`` is given, since the qubits can then just be dropped in the map.
"""
if qubit_map is None and drop is not None:
remaining_qubits = [qubit for qubit in self.qubits if qubit not in drop]
qubit_map = dict(zip(remaining_qubits, remaining_qubits))

if qubit_map is None:
clean = self.clean.copy()
dirty = self.dirty.copy()
Expand Down
50 changes: 45 additions & 5 deletions test/python/transpiler/test_high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def method(self, op_name, method_name):
return self.plugins[plugin_name]()


class MockHLS(HighLevelSynthesisPlugin):
class MockPlugin(HighLevelSynthesisPlugin):
"""A mock HLS using auxiliary qubits."""

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
Expand All @@ -246,6 +246,14 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **
return decomposition


class EmptyPlugin(HighLevelSynthesisPlugin):
"""A mock plugin returning None (i.e. a failed synthesis)."""

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Elaborate code to return None :)"""
return None


@ddt
class TestHighLevelSynthesisInterface(QiskitTestCase):
"""Tests for the synthesis plugin interface."""
Expand Down Expand Up @@ -544,7 +552,7 @@ def test_qubits_get_passed_to_plugins(self):
def test_ancilla_arguments(self):
"""Test ancillas are correctly labelled."""
gate = Gate(name="duckling", num_qubits=5, params=[])
hls_config = HLSConfig(duckling=[MockHLS()])
hls_config = HLSConfig(duckling=[MockPlugin()])

qc = QuantumCircuit(10)
qc.h([0, 8, 9]) # the two last H gates yield two dirty ancillas
Expand All @@ -563,7 +571,7 @@ def test_ancilla_arguments(self):
def test_ancilla_noop(self):
"""Test ancillas states are not affected by no-ops."""
gate = Gate(name="duckling", num_qubits=1, params=[])
hls_config = HLSConfig(duckling=[MockHLS()])
hls_config = HLSConfig(duckling=[MockPlugin()])
pm = PassManager([HighLevelSynthesis(hls_config)])

noops = [Delay(100), IGate()]
Expand All @@ -584,7 +592,7 @@ def test_ancilla_noop(self):
def test_ancilla_reset(self, reset):
"""Test ancillas are correctly freed after a reset operation."""
gate = Gate(name="duckling", num_qubits=1, params=[])
hls_config = HLSConfig(duckling=[MockHLS()])
hls_config = HLSConfig(duckling=[MockPlugin()])
pm = PassManager([HighLevelSynthesis(hls_config)])

qc = QuantumCircuit(2)
Expand All @@ -607,7 +615,7 @@ def test_ancilla_reset(self, reset):
def test_ancilla_state_maintained(self):
"""Test ancillas states are still dirty/clean after they've been used."""
gate = Gate(name="duckling", num_qubits=1, params=[])
hls_config = HLSConfig(duckling=[MockHLS()])
hls_config = HLSConfig(duckling=[MockPlugin()])
pm = PassManager([HighLevelSynthesis(hls_config)])

qc = QuantumCircuit(3)
Expand All @@ -628,6 +636,24 @@ def test_ancilla_state_maintained(self):

self.assertEqual(ref, pm.run(qc))

def test_synth_fails_definition_exists(self):
"""Test the case that a synthesis fails but the operation can be unrolled."""

circuit = QuantumCircuit(1)
circuit.ry(0.2, 0)

config = HLSConfig(ry=[EmptyPlugin()])
hls = HighLevelSynthesis(hls_config=config)

with self.subTest("nothing happened w/o basis gates"):
out = hls(circuit)
self.assertEqual(out, circuit)

hls = HighLevelSynthesis(hls_config=config, basis_gates=["u"])
with self.subTest("unrolled w/ basis gates"):
out = hls(circuit)
self.assertEqual(out.count_ops(), {"u": 1})


class TestPMHSynthesisLinearFunctionPlugin(QiskitTestCase):
"""Tests for the PMHSynthesisLinearFunction plugin for synthesizing linear functions."""
Expand Down Expand Up @@ -1857,6 +1883,20 @@ def test_unrolling_parameterized_composite_gates(self):

self.assertEqual(circuit_to_dag(expected), out_dag)

def test_unroll_with_clbit(self):
"""Test unrolling a custom definition that has qubits and clbits."""
block = QuantumCircuit(1, 1)
block.h(0)
block.measure(0, 0)

circuit = QuantumCircuit(1, 1)
circuit.append(block.to_instruction(), [0], [0])

hls = HighLevelSynthesis(basis_gates=["h", "measure"])
out = hls(circuit)

self.assertEqual(block, out)


class TestGate(Gate):
"""Mock one qubit zero param gate."""
Expand Down
Loading