"""This module implements the SeedLayerGenerator class."""
from __future__ import annotations
import logging
from typing import cast
from typing import Sequence
from bqskit.compiler.passdata import PassData
from bqskit.ir.circuit import Circuit
from bqskit.ir.point import CircuitPoint
from bqskit.passes.search.generator import LayerGenerator
from bqskit.passes.search.generators.simple import SimpleLayerGenerator
from bqskit.qis.state.state import StateVector
from bqskit.qis.state.system import StateSystem
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.utils.typing import is_integer
from bqskit.utils.typing import is_sequence
_logger = logging.getLogger(__name__)
[docs]
class SeedLayerGenerator(LayerGenerator):
"""Layer Generator for search that starts from a seed."""
[docs]
def __init__(
self,
seed: Circuit | Sequence[Circuit],
forward_generator: LayerGenerator = SimpleLayerGenerator(),
num_removed: int = 1,
hash_on_1q_gate: bool = False,
) -> None:
"""
Construct a SeedLayerGenerator.
Args:
seed (Circuit | Sequence[Circuit]): The seed or seeds
to start synthesis from.
forward_generator (LayerGenerator): A generator used to grow
the circuit. (Default: SimpleLayerGenerator)
num_removed (int): The number of atomic gate units removed
from the circuit in each backwards branch. If 0, no
backwards traversal of the synthesis tree will be done.
(Default: 1)
hash_on_1q_gate (bool): If True, 1 qubit gates that appear
in circuit templates will be used in circuit hash
calculations. Otherwise only multi-qubit gates will be
used. Setting this value to False can help stability
when using more complex LayerGenerators for the
forward_generator. (Default: False)
Raises:
ValueError: If 'num_removed' is negative.
"""
if not isinstance(forward_generator, LayerGenerator):
raise TypeError(
'Expected LayerGenerator for forward_generator'
f', got {type(forward_generator)}.',
)
if not is_integer(num_removed):
raise TypeError(
'Expected integer for num_removed, '
f'got {type(num_removed)}.',
)
if not isinstance(hash_on_1q_gate, bool):
raise TypeError(
'Expected bool for hash_on_1q_gate, '
f'got {type(hash_on_1q_gate)}.',
)
if num_removed < 0:
raise ValueError(
'Expected non-negative value for num_removed, '
f'got {num_removed}.',
)
if isinstance(seed, Circuit):
seed = [seed]
if not is_sequence(seed):
raise TypeError(
'Expected a Circuit or Sequence of Circuits for seed, '
f'got {type(seed)}.',
)
if not all(isinstance(s, Circuit) for s in seed):
msg = 'Expected a Circuit or Sequence of Circuits for seed.'
for i, s in enumerate(seed):
if not isinstance(s, Circuit):
msg += f'\nGot Sequence with {type(s)} at index {i}.'
raise TypeError(msg)
self.seeds = list(cast(Sequence[Circuit], seed))
self.forward_generator = forward_generator
self.num_removed = num_removed
self.hash_on_1q_gate = hash_on_1q_gate
[docs]
def gen_initial_layer(
self,
target: UnitaryMatrix | StateVector | StateSystem,
data: PassData,
) -> Circuit:
"""
Generate the initial layer, see LayerGenerator for more.
Raises:
TypeError: If target is not a UnitaryMatrix, StateVector, or
StateSystem.
Notes:
- For seeded layer generation, the initial_layer is the
empty Circuit. The `gen_successors` method checks for this
before returning `self.seeds` as successors.
"""
if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
raise TypeError(f'Expected unitary or state, got {type(target)}.')
return Circuit(target.num_qudits, target.radixes)
[docs]
def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
"""
Generate the successors of a circuit node.
If `circuit` is the empty Circuit, seeds with radixes matching
`circuit` will be used. If no seeds match, the empty Circuit will
be returned as the only successor with a warning message.
"""
if not isinstance(circuit, Circuit):
raise TypeError(f'Expected Circuit, got {type(circuit)}.')
if 'seed_seen_before' not in data:
data['seed_seen_before'] = set()
# If circuit is empty Circuit, successors are the seeds
if circuit.is_empty:
circ_hash = self.hash_structure(circuit, self.hash_on_1q_gate)
# Do not return seeds if empty circuit already visited
if circ_hash in data['seed_seen_before']:
return []
data['seed_seen_before'] = {circ_hash}
# Check if any seeds match circuit, only use those seeds
usable_seeds = [
seed for seed in self.seeds
if circuit.radixes == seed.radixes
]
for seed in usable_seeds:
h = self.hash_structure(seed, self.hash_on_1q_gate)
data['seed_seen_before'].add(h)
if len(usable_seeds) == 0:
_logger.warning(
'No seeds matching the circuit\'s radixes found.'
'\nGenerating successors from empty circuit.'
'\nThis may cause a malformed search tree because the'
'generator never generated a proper initial layer.',
)
usable_seeds.append(circuit)
return usable_seeds
# Generate successors
successors = self.forward_generator.gen_successors(circuit, data)
# Search reverse direction
ancestor_circuits = self.remove_atomic_units(circuit)
successors = ancestor_circuits + successors
# Filter out successors that have already been visited
filtered_successors = []
for s in successors:
h = self.hash_structure(s, self.hash_on_1q_gate)
if h not in data['seed_seen_before']:
data['seed_seen_before'].add(h)
filtered_successors.append(s)
return filtered_successors
[docs]
@staticmethod
def hash_structure(circuit: Circuit, hash_on_1q_gate: bool = True) -> int:
"""
Compute a hash of the Circuit's structure.
Args:
circuit (Circuit): Circuit to be hashed.
hash_on_1q_gate (bool): Whether or not to include 1 qubit
gates in the hash computation. (Default: True)
"""
hashes = []
for cycle, op in circuit.operations_with_cycles():
if not hash_on_1q_gate and op.num_qudits <= 1:
continue
hashes.append(hash((cycle, str(op))))
if len(hashes) > 100:
hashes = [sum(hashes)]
return sum(hashes)
[docs]
def remove_atomic_units(self, circuit: Circuit) -> list[Circuit]:
"""
Return circuits after removing upto `self.num_removed` atomic units.
Atomic units defined as multi-qudit gates and the single-qudit
gates that are directly dependent on them. Except for single-qudit
circuits, where the single-qudit gate is the atomic unit.
For two qudit synthesis, these atomic units look like:
-- two_qudit_gate -- single_qudit_gate_1 --
|
-- two_qudit_gate -- single_qudit_gate_2 --
Generally, this will find the last `num_removed` multi-qudit
gates, and remove them and any single qudit gates that are
directly dependent on them.
"""
num_removed = 0
ancestor_circuits = []
circuit_copy = circuit.copy()
if circuit_copy.num_qudits == 1:
init_num_cycles = circuit_copy.num_cycles
for _ in range(min(self.num_removed, init_num_cycles)):
circuit_copy.pop_cycle(-1)
ancestor_circuits.append(circuit_copy.copy())
for cycle, op in circuit.operations_with_cycles(reverse=True):
if num_removed >= self.num_removed:
break
if op.num_qudits == 1:
continue
# Remove multi-qudit gate and single qudit dependents
point = CircuitPoint(cycle, op.location[0])
dependents = []
for next_point in circuit_copy.next(point):
if circuit_copy.get_operation(next_point).num_qudits == 1:
dependents.append(next_point)
to_remove = dependents + [point]
circuit_copy.batch_pop(to_remove)
ancestor_circuits.append(circuit_copy.copy())
num_removed += 1
return ancestor_circuits