"""This module implements the Circuit class."""
from __future__ import annotations
import copy
import logging
import warnings
from typing import Any
from typing import cast
from typing import Collection
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
import numpy as np
import numpy.typing as npt
from bqskit.ir.gate import Gate
from bqskit.ir.gates.circuitgate import CircuitGate
from bqskit.ir.gates.constant.unitary import ConstantUnitaryGate
from bqskit.ir.gates.measure import MeasurementPlaceholder
from bqskit.ir.iterator import CircuitIterator
from bqskit.ir.lang import get_language
from bqskit.ir.location import CircuitLocation
from bqskit.ir.location import CircuitLocationLike
from bqskit.ir.operation import Operation
from bqskit.ir.opt.cost.generator import CostFunctionGenerator
from bqskit.ir.opt.instantiater import Instantiater
from bqskit.ir.opt.instantiaters import instantiater_order
from bqskit.ir.opt.minimizers.ceres import CeresMinimizer
from bqskit.ir.opt.multistartgen import MultiStartGenerator
from bqskit.ir.point import CircuitPoint
from bqskit.ir.point import CircuitPointLike
from bqskit.ir.region import CircuitRegion
from bqskit.ir.region import CircuitRegionLike
from bqskit.qis.graph import CouplingGraph
from bqskit.qis.state.state import StateLike
from bqskit.qis.state.state import StateVector
from bqskit.qis.state.statemap import StateVectorMap
from bqskit.qis.state.system import StateSystem
from bqskit.qis.unitary.differentiable import DifferentiableUnitary
from bqskit.qis.unitary.unitary import RealVector
from bqskit.qis.unitary.unitarybuilder import UnitaryBuilder
from bqskit.qis.unitary.unitarymatrix import UnitaryLike
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.utils.random import seed_random_sources
from bqskit.utils.typing import is_bool
from bqskit.utils.typing import is_integer
from bqskit.utils.typing import is_iterable
from bqskit.utils.typing import is_sequence_of_int
from bqskit.utils.typing import is_valid_radixes
if TYPE_CHECKING:
from bqskit.ir.opt.cost.function import CostFunction
from bqskit.compiler.basepass import BasePass
from bqskit.compiler.gateset import GateSet
_logger = logging.getLogger(__name__)
[docs]
class Circuit(DifferentiableUnitary, StateVectorMap, Collection[Operation]):
"""
A Circuit is a quantum program composed of operation objects.
The operations are organized in 2-dimensions, and are indexed by
a CircuitPoint. The first index corresponds to an operation's cycle.
This describes when an operation will be executed. The second index
is the qudit index and describes where the operation will execute.
An operation can be multi-qudit meaning its location - list of qudit
indices describing which qudits the operation affects - contains
multiple indices. All operations exist in only one cycle at a time,
but if an operation is multi-qudit, then it is pointed to by multiple
qudit indices.
Invariants:
1. A circuit method will never complete with an idle cycle.
An idle cycle is one that contains no gates.
2. No one logical operation will ever be pointed to from more
than one cycle.
3. Iterating through the entire circuit always produces
operations in simulation order. This means that if those
operations were applied to a quantum state in the same
order, then the result is the same as simulating the circuit.
Notes:
While a guarantee is made that the circuit never has any idle
cycles, this means that cycles can be deleted or inserted
during a method call. Therefore, cycle indices may need to be
updated in between calls to circuit methods. There are several
"batch" variants of methods that can handle this for you.
"""
[docs]
def __init__(self, num_qudits: int, radixes: Sequence[int] = []) -> None:
"""
Build an empty circuit with the specified number of qudits.
By default, all qudits are qubits, but this can be changed
with radixes.
Args:
num_qudits (int): The number of qudits in this circuit.
radixes (Sequence[int]): A sequence with its length equal
to `num_qudits`. Each element specifies the base of a
qudit. Defaults to qubits.
Raises:
ValueError: if `num_qudits` is nonpositive.
Examples:
>>> circ = Circuit(4) # Creates four-qubit empty circuit.
>>> circ = Circuit(2, [2, 3]) # Creates one qubit and one qutrit.
>>> circ = Circuit(2)
>>> from bqskit.ir.gates import HGate, CXGate, HGate
>>> circ.append_gate(HGate(), 0)
>>> circ.append_gate(CXGate(), (0, 1))
>>> circ.append_gate(HGate(), 1)
>>> circ.get_unitary()
array([[ 0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j],
[ 0.5+0.j, -0.5+0.j, 0.5+0.j, -0.5+0.j],
[ 0.5+0.j, 0.5+0.j, -0.5+0.j, -0.5+0.j],
[-0.5+0.j, 0.5+0.j, 0.5+0.j, -0.5+0.j]])
>>> circ.get_statevector([1, 0, 0, 0])
array([ 0.5+0.j, 0.5+0.j, 0.5+0.j, -0.5+0.j])
"""
if not is_integer(num_qudits):
raise TypeError(
f'Expected integer num_qudits, got {type(num_qudits)}.',
)
if num_qudits <= 0:
raise ValueError(
f'Expected positive integer for num_qudits, got {num_qudits}.',
)
self._num_qudits = int(num_qudits)
self._radixes = tuple(
radixes if len(radixes) > 0 else [2] * self.num_qudits,
)
if not is_valid_radixes(self.radixes):
raise TypeError('Invalid qudit radixes.')
if len(self.radixes) != self.num_qudits:
raise ValueError(
'Expected length of radixes to be equal to num_qudits:'
' %d != %d' % (len(self.radixes), self.num_qudits),
)
self._circuit: list[list[Operation | None]] = []
self._gate_info: dict[Gate, int] = {}
self._graph_info: dict[tuple[int, int], int] = {}
_NodePtrs = Dict[int, Optional[CircuitPoint]]
self._front: _NodePtrs = {i: None for i in range(self.num_qudits)}
self._rear: _NodePtrs = {i: None for i in range(self.num_qudits)}
self._dag: dict[CircuitPoint, tuple[_NodePtrs, _NodePtrs]] = {}
# region Circuit Properties
@property
def num_params(self) -> int:
"""The total number of parameters in the circuit."""
num_params_acm = 0
for gate, count in self._gate_info.items():
num_params_acm += gate.num_params * count
return num_params_acm
@property
def num_operations(self) -> int:
"""The total number of operations in the circuit."""
num_gates_acm = 0
for _, count in self._gate_info.items():
num_gates_acm += count
return num_gates_acm
@property
def num_cycles(self) -> int:
"""The number of cycles in the circuit."""
return len(self._circuit)
@property
def params(self) -> npt.NDArray[np.float64]:
"""The stored parameters for the circuit."""
return np.array(sum((list(op.params) for op in self), []))
@property
def depth(self) -> int:
"""The length of the critical path in the circuit."""
qudit_depths = np.zeros(self.num_qudits, dtype=int)
for op in self:
new_depth = max(qudit_depths[list(op.location)]) + 1
qudit_depths[list(op.location)] = new_depth
return int(max(qudit_depths))
@property
def multi_qudit_depth(self) -> int:
"""The length of the critical path excluding single-qudit gates."""
qudit_depths = np.zeros(self.num_qudits, dtype=int)
for op in self:
if op.num_qudits == 1:
continue
new_depth = max(qudit_depths[list(op.location)]) + 1
qudit_depths[list(op.location)] = new_depth
return int(max(qudit_depths))
@property
def parallelism(self) -> float:
"""The amount of parallelism in the circuit."""
depth = self.depth
if depth == 0:
return 0
weighted_num_operations = np.sum([
gate.num_qudits * count
for gate, count in self._gate_info.items()
])
return float(weighted_num_operations / depth)
@property
def coupling_graph(self) -> CouplingGraph:
"""
The qudit connectivity in the circuit.
Returns:
(CouplingGraph): The coupling graph required by the circuit.
Notes:
- Logical multi-qudit gates require participating qudits to
have all-to-all connectivity.
"""
return CouplingGraph(self._graph_info.keys(), self.num_qudits)
@property
def gate_set(self) -> GateSet:
"""The set of gates in the circuit."""
from bqskit.compiler.gateset import GateSet
return GateSet(set(self._gate_info.keys()))
@property
def gate_set_no_blocks(self) -> GateSet:
"""The set of gates in the circuit, recurses into circuit gates."""
from bqskit.compiler.gateset import GateSet
gates: set[Gate] = set()
for g, _ in self._gate_info.items():
if isinstance(g, CircuitGate):
for other_g in g._circuit.gate_set_no_blocks:
gates.add(other_g)
else:
gates.add(g)
return GateSet(gates)
@property
def gate_counts(self) -> dict[Gate, int]:
"""The count of each type of gate in the circuit."""
return {gate: self.count(gate) for gate in self.gate_set}
@property
def active_qudits(self) -> list[int]:
"""The qudits involved in at least one operation."""
active_qudits = set()
for qudit, point in self._front.items():
if point is not None:
active_qudits.add(qudit)
return list(sorted(active_qudits))
@property
def is_empty(self) -> bool:
"""If there are no operations in the Circuit."""
return self.num_operations == 0
[docs]
def is_differentiable(self) -> bool:
"""Check if all gates are differentiable."""
return all(
isinstance(gate, DifferentiableUnitary)
for gate in self.gate_set
)
# endregion
# region Qudit Methods
[docs]
def append_qudit(self, radix: int = 2) -> None:
"""
Append a qudit to the circuit.
Args:
radix (int): The radix of the qudit. (Default: qubit)
Raises:
ValueError: If `radix` is < 2
"""
if not is_integer(radix):
raise TypeError('Expected integer for radix, got: %s', type(radix))
if radix < 2:
raise ValueError('Expected radix to be >= 2, got %d' % radix)
self._num_qudits += 1
self._radixes = self.radixes + (radix,)
for cycle in self._circuit:
cycle.append(None)
self._rear[self.num_qudits - 1] = None
self._front[self.num_qudits - 1] = None
[docs]
def extend_qudits(self, radixes: Iterable[int]) -> None:
"""
Append many qudits to the circuit.
Args:
radixes (Iterable[int]): The radix for each qudit to append.
Raises:
ValueError: If any radix in `radixes` is < 2.
"""
for radix in radixes:
self.append_qudit(radix)
[docs]
def insert_qudit(self, qudit_index: int, radix: int = 2) -> None:
"""
Insert a qudit into the circuit.
Args:
qudit_index (int): The index where to insert the qudit.
radix (int): The radix of the qudit. (Default: qubit)
Raises:
ValueError: If `radix` is < 2.
"""
if not is_integer(qudit_index):
raise TypeError(
f'Expected integer for qudit_index, got: {type(qudit_index)}',
)
if not is_integer(radix):
raise TypeError(f'Expected integer for radix, got: {type(radix)}')
if radix < 2:
raise ValueError(f'Expected radix to be >= 2, got {radix}')
if qudit_index >= self.num_qudits:
return self.append_qudit(radix)
if qudit_index <= -self.num_qudits:
qudit_index = 0
elif qudit_index < 0:
qudit_index = self.num_qudits + qudit_index
# Update circuit properties
self._num_qudits += 1
radix_list = list(self.radixes)
radix_list.insert(qudit_index, radix)
self._radixes = tuple(radix_list)
# Insert qudit
shift_index = lambda q: q if q < qudit_index else q + 1
inc_point = lambda p: CircuitPoint(p.cycle, p.qudit + 1)
shift_point = lambda p: p if p.qudit < qudit_index else inc_point(p)
shift_point_or_none = lambda p: None if p is None else shift_point(p)
shift_edge = lambda e: (shift_index(e[0]), shift_index(e[1]))
for cycle in self._circuit:
cycle.insert(qudit_index, None)
# Renumber gates with now-invalid locations
qudits_to_skip: list[int] = []
for i, op in enumerate(cycle[qudit_index:]):
if op is None or i + qudit_index in qudits_to_skip:
continue
shifted_location = [shift_index(i) for i in op.location]
op._location = CircuitLocation(shifted_location)
qudits_to_skip.extend(op.location)
# Shift _front, _rear, _graph_info, _dag
self._front = {
shift_index(q): shift_point_or_none(p)
for q, p in self._front.items()
}
self._front[qudit_index] = None
self._rear = {
shift_index(q): shift_point_or_none(p)
for q, p in self._rear.items()
}
self._rear[qudit_index] = None
self._graph_info = {
shift_edge(e): i
for e, i in self._graph_info.items()
}
self._dag = {
shift_point(p): (
# Prev
{
shift_index(q): shift_point_or_none(p2)
for q, p2 in ptr_map[0].items()
},
# Next
{
shift_index(q): shift_point_or_none(p2)
for q, p2 in ptr_map[1].items()
},
)
for p, ptr_map in self._dag.items()
}
[docs]
def pop_qudit(self, qudit_index: int) -> None:
"""
Pop a qudit from the circuit and all gates attached to it.
Args:
qudit_index (int): The index of the qudit to pop.
Raises:
IndexError: If `qudit_index` is out of range.
ValueError: If circuit only has one qudit.
"""
if not is_integer(qudit_index):
raise TypeError(
f'Expected integer for qudit_index, got: {type(qudit_index)}',
)
if not self.is_qudit_in_range(qudit_index):
raise IndexError(f'Qudit index ({qudit_index}) is out-of-range.')
if self.num_qudits == 1:
raise ValueError('Cannot pop only qudit in circuit.')
if qudit_index < 0:
qudit_index = self.num_qudits + qudit_index
# Remove gates attached to popped qudit
points = []
for cycle_index, cycle in enumerate(self._circuit):
if cycle[qudit_index] is not None:
points.append((cycle_index, qudit_index))
if len(points) > 0:
self.batch_pop(points)
# Update circuit properties
self._num_qudits -= 1
radix_list = list(self.radixes)
radix_list.pop(qudit_index)
self._radixes = tuple(radix_list)
# Remove qudit
shift_index = lambda q: q if q < qudit_index else q - 1
dec_point = lambda p: CircuitPoint(p.cycle, p.qudit - 1)
shift_point = lambda p: p if p.qudit < qudit_index else dec_point(p)
shift_point_or_none = lambda p: None if p is None else shift_point(p)
shift_edge = lambda e: (shift_index(e[0]), shift_index(e[1]))
for cycle_index, cycle in enumerate(self._circuit):
cycle.pop(qudit_index)
# Renumber gates with now-invalid locations
for cycle_index, cycle in enumerate(self._circuit):
qudits_to_skip: list[int] = []
for i, op in enumerate(cycle[qudit_index:]):
if op is None or i + qudit_index in qudits_to_skip:
continue
shifted_location = [shift_index(i) for i in op.location]
op._location = CircuitLocation(shifted_location)
qudits_to_skip.extend(op.location)
# Shift _front, _rear, _graph_info, _dag
self._front = {
shift_index(q): shift_point_or_none(p)
for q, p in self._front.items()
if q != qudit_index
}
self._rear = {
shift_index(q): shift_point_or_none(p)
for q, p in self._rear.items()
if q != qudit_index
}
self._graph_info = {
shift_edge(e): i
for e, i in self._graph_info.items()
}
self._dag = {
shift_point(p): (
# Prev
{
shift_index(q): shift_point_or_none(p2)
for q, p2 in ptr_map[0].items()
},
# Next
{
shift_index(q): shift_point_or_none(p2)
for q, p2 in ptr_map[1].items()
},
)
for p, ptr_map in self._dag.items()
}
[docs]
def is_qudit_in_range(self, qudit_index: int) -> bool:
"""Return true if `qudit_index` is in-range for the circuit."""
if not is_integer(qudit_index):
raise TypeError(
f'Expected integer for qudit_index, got: {type(qudit_index)}',
)
return (
qudit_index < self.num_qudits
and qudit_index >= -self.num_qudits
)
[docs]
def is_qudit_idle(self, qudit_index: int) -> bool:
"""Return true if the qudit is not involved in any operations."""
if not is_integer(qudit_index):
raise TypeError(
f'Expected integer for qudit_index, got: {type(qudit_index)}',
)
return self._front[qudit_index] is None
[docs]
def renumber_qudits(self, qudit_permutation: Sequence[int]) -> None:
"""
Permute the qudits in the circuit.
Args:
qudit_permutation (Sequence[int]): A map from qudit indices
to qudit indices.
Raises:
IndexError: If any of the indices are out of range.
ValueError: If the `qudit_permutation` is not the same size
as the circuit.
ValueError: If the `qudit_permutation` is not a valid permutation.
"""
if not is_sequence_of_int(qudit_permutation):
raise TypeError(
'Expected sequence of integers'
', got %s' % type(qudit_permutation),
)
if len(qudit_permutation) != self.num_qudits:
raise ValueError(
'Expected qudit_permutation length equal to circuit num_qudits:'
'%d, got %d' % (self.num_qudits, len(qudit_permutation)),
)
if len(qudit_permutation) != len(set(qudit_permutation)):
raise ValueError('Invalid permutation.')
perm = [int(q) for q in qudit_permutation]
perm_point = lambda p: CircuitPoint(p.cycle, perm[p.qudit])
perm_point_or_none = lambda p: perm_point(p) if p is not None else p
self._graph_info = {
(perm[e[0]], perm[e[1]]): i
for e, i in self._graph_info.items()
}
self._front = {
perm[i]: perm_point_or_none(p)
for i, p in self._front.items()
}
self._rear = {
perm[i]: perm_point_or_none(p)
for i, p in self._rear.items()
}
self._dag = {
perm_point(p): (
# Prev
{
perm[q]: perm_point_or_none(p2)
for q, p2 in ptr_map[0].items()
},
# Next
{
perm[q]: perm_point_or_none(p2)
for q, p2 in ptr_map[1].items()
},
)
for p, ptr_map in self._dag.items()
}
for i in range(self.num_cycles):
self._circuit[i] = [
self._circuit[i][perm.index(q)]
for q in range(self.num_qudits)
]
qudits_to_skip: list[int] = []
for j in range(self.num_qudits):
if j in qudits_to_skip:
continue
op = self._circuit[i][j]
if op is not None:
loc = CircuitLocation([perm[q] for q in op.location])
op._location = loc
qudits_to_skip.extend(loc)
# endregion
# region Cycle Methods
def _append_cycle(self) -> None:
"""Appends an idle cycle to the end of the circuit."""
self._circuit.append([None] * self.num_qudits)
def _insert_cycle(self, cycle_index: int) -> None:
"""Inserts an idle cycle in the circuit."""
self._circuit.insert(cycle_index, [None] * self.num_qudits)
inc_point = lambda p: CircuitPoint(p.cycle + 1, p.qudit)
shift_point = lambda p: p if p.cycle < cycle_index else inc_point(p)
shift_point_or_none = lambda p: None if p is None else shift_point(p)
self._front = {
q: shift_point_or_none(p)
for q, p in self._front.items()
}
self._rear = {
q: shift_point_or_none(p)
for q, p in self._rear.items()
}
self._dag = {
shift_point(p): (
# Prev
{
q: shift_point_or_none(p2)
for q, p2 in prevs.items()
},
# Next
{
q: shift_point_or_none(p2)
for q, p2 in nexts.items()
},
)
for p, (prevs, nexts) in self._dag.items()
}
[docs]
def pop_cycle(self, cycle_index: int) -> None:
"""
Pop a cycle from the circuit and all operations in it.
Args:
cycle_index (int): The index of the cycle to pop.
Raises:
IndexError: If `cycle_index` is out of range.
"""
if not is_integer(cycle_index):
raise TypeError(
f'Expected integer for cycle_index, got: {type(cycle_index)}',
)
if not self.is_cycle_in_range(cycle_index):
raise IndexError(f'Cycle index ({cycle_index}) is out-of-range.')
ops = {op for op in self._circuit[cycle_index] if op is not None}
points = [(cycle_index, op.location[0]) for op in ops]
if len(points) != 0:
for point in points:
self.pop(point)
return
self._circuit.pop(cycle_index)
if cycle_index == -1 or cycle_index == self.num_cycles:
return
dec_point = lambda p: CircuitPoint(p.cycle - 1, p.qudit)
shift_point = lambda p: p if p.cycle < cycle_index else dec_point(p)
shift_point_or_none = lambda p: None if p is None else shift_point(p)
self._front = {
q: shift_point_or_none(p)
for q, p in self._front.items()
}
self._rear = {
q: shift_point_or_none(p)
for q, p in self._rear.items()
}
self._dag = {
shift_point(p): (
# Prev
{
q: shift_point_or_none(p2)
for q, p2 in ptr_map[0].items()
},
# Next
{
q: shift_point_or_none(p2)
for q, p2 in ptr_map[1].items()
},
)
for p, ptr_map in self._dag.items()
}
def _is_cycle_idle(self, cycle_index: int) -> bool:
"""Return true if the cycle is idle, that is it contains no gates."""
return all(op is None for op in self._circuit[cycle_index])
[docs]
def is_cycle_in_range(self, cycle_index: int) -> bool:
"""Return true if cycle is a valid in-range index in the circuit."""
if not is_integer(cycle_index):
raise TypeError(
f'Expected integer for cycle_index, got: {type(cycle_index)}',
)
return (
cycle_index < self.num_cycles
and cycle_index >= -self.num_cycles
)
[docs]
def is_cycle_unoccupied(
self,
cycle_index: int,
location: CircuitLocationLike,
) -> bool:
"""
Check if a cycle is unoccupied for all qudits in `location`.
Args:
cycle_index (int): The cycle to check.
location (CircuitLocationLike): The set of qudits to check.
Returns:
bool: True if the `cycle_index` at `location` is unoccupied.
Raises:
IndexError: If `cycle_index` is out of range.
Examples:
>>> from bqskit.ir.gates import HGate, XGate, ZGate
>>> circuit = Circuit(2)
>>> circuit.append_gate(HGate(), [0])
>>> circuit.append_gate(XGate(), [0])
>>> circuit.append_gate(ZGate(), [1])
>>> circuit.is_cycle_unoccupied(0, [0])
False
>>> circuit.is_cycle_unoccupied(1, [1])
True
"""
if not is_integer(cycle_index):
raise TypeError(
'Expected integer for cycle_index, got: %s', type(cycle_index),
)
if not CircuitLocation.is_location(location):
raise TypeError('Invalid location.')
if not self.is_cycle_in_range(cycle_index):
raise IndexError('Out-of-range cycle index: %d.' % cycle_index)
location = CircuitLocation(location)
if max(location) > self.num_qudits:
raise ValueError('Location has an out-of-range qudit index.')
for qudit_index in location:
if self._circuit[cycle_index][qudit_index] is not None:
return False
return True
[docs]
def find_available_cycle(self, location: CircuitLocationLike) -> int:
"""
Finds the first available cycle for qudits in `location`.
An available cycle for `location` is one where it and all
cycles after it are unoccupied for `location`.
Args:
localtion (CircuitLocationLike): Find a cycle for this location.
Returns:
int: The first available cycle.
Raises:
ValueError: If no available cycle exists.
Examples:
>>> from bqskit.ir.gates import HGate, XGate, ZGate
>>> circuit = Circuit(2)
>>> circuit.append_gate(HGate(), [0])
>>> circuit.find_available_cycle([1])
0
>>> circuit.append_gate(XGate(), [0])
>>> circuit.append_gate(ZGate(), [1])
>>> circuit.find_available_cycle([1])
1
"""
location = CircuitLocation(location)
if max(location) > self.num_qudits:
raise ValueError('Location has an out-of-range qudit index.')
# No available cycle
if self.num_cycles == 0:
return 0
cycle = 0
for q in location:
rear = self._rear[q]
if rear is not None:
cycle = max(cycle, rear.cycle + 1)
return cycle
def _find_available_or_append_cycle(
self,
location: CircuitLocationLike,
) -> int:
"""Find the first available cycle, if none exists append one."""
available_cycle = self.find_available_cycle(location)
# If no available cycle
if available_cycle == self.num_cycles:
self._append_cycle()
return self.num_cycles - 1
return available_cycle
# endregion
# region Point Methods
[docs]
def is_point_in_range(self, point: CircuitPointLike) -> bool:
"""Return true if `point` is a valid in-range index in the circuit."""
if not CircuitPoint.is_point(point):
raise TypeError(f'Expected CircuitPoint, got: {type(point)}.')
return (
self.is_cycle_in_range(point[0])
and self.is_qudit_in_range(point[1])
)
[docs]
def is_point_idle(self, point: CircuitPointLike) -> bool:
"""Return true if an operation does not exist at `point`."""
if not CircuitPoint.is_point(point):
raise TypeError(f'Expected CircuitPoint, got {type(point)}.')
return self._circuit[point[0]][point[1]] is None
[docs]
def normalize_point(self, point: CircuitPointLike) -> CircuitPoint:
"""Flip negative indices to positive indices."""
if not self.is_point_in_range(point):
raise IndexError('Out-of-range point.')
cycle = point[0] if point[0] >= 0 else self.num_cycles + point[0]
qudit = point[1] if point[1] >= 0 else self.num_qudits + point[1]
return CircuitPoint(cycle, qudit)
# endregion
# region DAG Methods
@property
def front(self) -> set[CircuitPoint]:
"""The positions of operations with no dependencies."""
front_set = set()
for point in self._front.values():
if point is not None and len(self.prev(point)) == 0:
front_set.add(point)
return front_set
@property
def rear(self) -> set[CircuitPoint]:
"""The positions of operations with nothing after them."""
rear_set = set()
for point in self._rear.values():
if point is not None and len(self.next(point)) == 0:
rear_set.add(point)
return rear_set
[docs]
def last_on(self, qudit: int) -> CircuitPoint | None:
"""Report the point for the last operation on `qudit` if it exists."""
return self._rear[qudit]
[docs]
def first_on(self, qudit: int) -> CircuitPoint | None:
"""Report the point for the first operation on `qudit` if it exists."""
return self._front[qudit]
[docs]
def next(self, point: CircuitPoint) -> set[CircuitPoint]:
"""Return the points of operations dependent on the one at `point`."""
return {p for p in self._dag[point][1].values() if p is not None}
[docs]
def prev(self, point: CircuitPoint) -> set[CircuitPoint]:
"""Return the points of operations the one at `point` depends on."""
return {p for p in self._dag[point][0].values() if p is not None}
# endregion
# region Operation/Gate/Circuit Methods
[docs]
def check_valid_operation(self, op: Operation) -> None:
"""Check that `op` can be applied to the circuit."""
if not isinstance(op, Operation):
raise TypeError('Expected Operation got %s.' % type(op))
if not all([qudit < self.num_qudits for qudit in op.location]):
raise ValueError('Operation location mismatch with Circuit.')
for op_radix, circ_radix_idx in zip(op.radixes, op.location):
if op_radix != self.radixes[circ_radix_idx]:
raise ValueError('Operation radix mismatch with Circuit.')
[docs]
def get_operation(self, point: CircuitPointLike) -> Operation:
"""
Retrieve the operation at the `point`.
Args:
point (CircuitPointLike): The circuit point position of the
operation.
Raises:
IndexError: If no operation exists at the point specified.
Returns:
Operation: The operation at `point`.
Examples:
>>> from bqskit.ir.gates import HGate, CNOTGate
>>> circuit = Circuit(2)
>>> circuit.append_gate(HGate(), [0])
>>> circuit.append_gate(CNOTGate(), [0, 1])
>>> circuit.get_operation((1, 0))
CNOTGate@(0, 1)
"""
if not self.is_point_in_range(point):
raise IndexError('Out-of-range or invalid point.')
op = self._circuit[point[0]][point[1]]
if op is None:
raise IndexError(
'No operation exists at the specified point: %s.' % str(point),
)
return op
[docs]
def point(
self,
op: Operation | Gate,
start: CircuitPointLike = (0, 0),
end: CircuitPointLike | None = None,
) -> CircuitPoint:
"""
Return point of the first occurrence of `op`.
Args:
op (Operation | Gate): The operation or gate to find.
start (CircuitPointLike): Circuit point to start searching
from. Inclusive. (Default: Beginning of the circuit.)
end (CircuitPointLike | None): Cycle index to stop searching at.
Inclusive. (Default: End of the circuit.)
Returns:
CircuitPoint: The first point that contains `op`.
Raises:
ValueError: If `op` is not found.
ValueError: If `op` could not have been placed on the circuit
due to either an invalid location or gate radix mismatch.
Examples:
>>> from bqskit.ir.gates import HGate, XGate
>>> circuit = Circuit(1)
>>> opH = Operation(HGate(), [0])
>>> circuit.append(opH)
>>> circuit.point(opH)
(0, 0)
>>> opX = Operation(XGate(), [0])
>>> circuit.append(opX)
>>> circuit.point(opX)
(1, 0)
"""
if not isinstance(op, (Operation, Gate)):
raise TypeError(f'Expected gate or operation, got {type(op)}.')
end = end if end is not None else (-1, -1)
start = self.normalize_point(start)
end = self.normalize_point(end)
if isinstance(op, Operation):
self.check_valid_operation(op)
if op.gate not in self._gate_info:
raise ValueError('No such operation exists in the circuit.')
elif isinstance(op, Gate):
if op not in self._gate_info:
raise ValueError('No such operation exists in the circuit.')
if not self.is_point_in_range(start):
raise IndexError('Out-of-range or invalid start point.')
if not self.is_point_in_range(end):
raise IndexError('Out-of-range or invalid end point.')
if isinstance(op, Operation):
qudit_index = op.location[0]
for i, cycle in enumerate(self._circuit[start[0]:end[0] + 1]):
if cycle[qudit_index] is not None and cycle[qudit_index] == op:
return CircuitPoint(start[0] + i, qudit_index)
else:
for i, cycle in enumerate(self._circuit[start[0]:end[0] + 1]):
for q, _op in enumerate(cycle[start[1]:end[1] + 1]):
if _op is not None and _op.gate == op:
return CircuitPoint(start[0] + i, start[1] + q)
raise ValueError('No such operation exists in the circuit.')
[docs]
def append(self, op: Operation) -> int:
"""
Append `op` to the end of the circuit and return its cycle index.
Args:
op (Operation): The operation to append.
Returns:
int: The cycle index of the appended operation.
Raises:
ValueError: If `op` cannot be placed on the circuit due to
either an invalid location or gate radix mismatch.
Notes:
Due to the circuit being represented as a matrix,
`circuit.append(op)` does not imply `op` is last in simulation
order but it implies `op` is in the last cycle of circuit.
Examples:
>>> from bqskit.ir.gates import HGate
>>> circ = Circuit(1)
>>> op = Operation(HGate(), [0])
>>> circ.append(op) # Appends a Hadamard gate to qudit 0.
"""
self.check_valid_operation(op)
cycle_index = self._find_available_or_append_cycle(op.location)
point = CircuitPoint(cycle_index, op.location[0])
prevs: dict[int, CircuitPoint | None] = {i: None for i in op.location}
for qudit_index in op.location:
# Add op to the circuit structure
self._circuit[cycle_index][qudit_index] = op
# Update necessary gates already in _dag to point to this one
rear = self._rear[qudit_index]
if rear is not None:
prevs[qudit_index] = rear
self._dag[rear][1][qudit_index] = point
# Update rear pointers
self._rear[qudit_index] = point
# Update front pointers
if self._front[qudit_index] is None:
self._front[qudit_index] = point
# Add new op to the dag
self._dag[point] = (prevs, {i: None for i in op.location})
# Update _graph_info
for pair in op.location.pairs:
if pair not in self._graph_info:
self._graph_info[pair] = 0
self._graph_info[pair] += 1
# Update _gate_info
if op.gate not in self._gate_info:
self._gate_info[op.gate] = 0
self._gate_info[op.gate] += 1
return cycle_index
[docs]
def append_gate(
self,
gate: Gate,
location: CircuitLocationLike,
params: RealVector = [],
) -> int:
"""
Append the gate object to the circuit on the qudits in location.
Args:
gate (Gate): The gate to append.
location (CircuitLocationLike): Apply the gate to these qudits.
params (RealVector): The gate's parameters.
(Default: all zeros)
Returns:
int: The cycle index of the appended gate.
Examples:
>>> from bqskit.ir.gates import HGate
>>> circ = Circuit(1)
>>> # Append a Hadamard gate to qudit 0.
>>> circ.append_gate(HGate(), 0)
See Also:
:func:`append`
"""
return self.append(Operation(gate, location, params))
[docs]
def append_circuit(
self,
circuit: Circuit,
location: CircuitLocationLike,
as_circuit_gate: bool = False,
move: bool = False,
) -> int:
"""
Append `circuit` at the qudit location specified.
Args:
circuit (Circuit): The circuit to append.
location (CircuitLocationLike): Apply the circuit to these qudits.
as_circuit_gate (bool): If true, append `circuit` as a unit
block (CircuitGate) rather than each operation in `circuit`
individually. (Default: False)
move (bool): Move circuit into circuit gate rather than copy.
(Default: False)
Returns:
int: The starting cycle index of the appended circuit. If the
appended circuit is empty, then this will be -1.
Raises:
ValueError: If `circuit` is not the same size as `location`.
See Also:
:func:`append`
"""
if not isinstance(circuit, Circuit):
raise TypeError('Expected circuit, got %s.' % type(circuit))
if not CircuitLocation.is_location(location):
raise TypeError('Invalid location.')
location = CircuitLocation(location)
if not is_bool(as_circuit_gate):
raise TypeError(f'Expected bool, got: {type(as_circuit_gate)}.')
if circuit.num_qudits != len(location):
raise ValueError('Circuit and location size mismatch.')
if as_circuit_gate:
op = Operation(CircuitGate(circuit, move), location, circuit.params)
return self.append(op)
cycle_index = -1
for op in circuit:
mapped_location = [location[q] for q in op.location]
ci = self.append(Operation(op.gate, mapped_location, op.params))
if cycle_index is None:
cycle_index = ci
return cycle_index
[docs]
def extend(self, ops: Iterable[Operation]) -> None:
"""
Append all operations in `ops` to the circuit.
Args:
ops (Operation): The operations to append.
Examples:
>>> from bqskit.ir.gates import HGate, XGate
>>> circ = Circuit(1)
>>> opH = Operation(HGate(), [0])
>>> opX = Operation(XGate(), [0])
>>> circ.extend([opH, opX])
>>> circ.point(opH)
(0, 0)
>>> circ.point(opX)
(1, 0)
Notes:
See `append` for more info.
"""
for op in ops:
self.append(op)
[docs]
def insert(self, cycle_index: int, op: Operation) -> None:
"""
Insert `op` in the circuit at the specified cycle.
After this, if cycle was in range, you can expect:
`all([self[cycle_index, idx] == op for idx in op.location])`
Unless cycle_index was out-of-bounds, in which case, it will
be appended ASAP rather than inserted.
Args:
cycle_index (int): The cycle to insert the operation.
op (Operation): The operation to insert.
Raises:
ValueError: If `op` cannot be placed on the circuit due to
either an invalid location or gate radix mismatch.
Examples:
>>> from bqskit.ir.gates import HGate, XGate
>>> circ = Circuit(1)
>>> opX = Operation(XGate(), [0])
>>> opH = Operation(HGate(), [0])
>>> circ.append(opX)
>>> circ.insert(0, opH)
>>> circ.point(opH).cycle
0
Notes:
Clamps cycle to be in range.
"""
self.check_valid_operation(op)
if self.num_cycles == 0:
self.append(op)
return
if not self.is_cycle_in_range(cycle_index):
if cycle_index < -self.num_cycles:
cycle_index = 0
else:
self.append(op)
return
if cycle_index < 0:
cycle_index = self.num_cycles + cycle_index
if not self.is_cycle_unoccupied(cycle_index, op.location):
self._insert_cycle(cycle_index)
point = CircuitPoint(cycle_index, op.location[0])
prevs: dict[int, CircuitPoint | None] = {i: None for i in op.location}
nexts: dict[int, CircuitPoint | None] = {i: None for i in op.location}
for qudit_index in op.location:
# Update front pointers if necessary
if self._front[qudit_index] is None:
self._front[qudit_index] = point
# Update rear pointers if necessary
if self._rear[qudit_index] is None:
self._rear[qudit_index] = point
# Search to left of cycle_index on qudit_index
for c in reversed(range(cycle_index)):
prev_op = self._circuit[c][qudit_index]
# Find first op on qudit_index
if prev_op is not None:
prev_point = CircuitPoint(c, prev_op.location[0])
# Update its next to be this
self._dag[prev_point][1][qudit_index] = point
# Add it to this prev
prevs[qudit_index] = prev_point
# Stop searching to left
break
# Search to right of cycle_index on qudit_index
for c in range(cycle_index, self.num_cycles):
next_op = self._circuit[c][qudit_index]
# Find first op on qudit_index
if next_op is not None:
next_point = CircuitPoint(c, next_op.location[0])
# Update its prev to be this
self._dag[next_point][0][qudit_index] = point
# Add it to this next
nexts[qudit_index] = next_point
# Stop searching to right
break
# Update _front if this is new front
for qudit_index, _prev_point in prevs.items():
if _prev_point is None:
self._front[qudit_index] = point
# Update _rear if this is new rear
for qudit_index, _next_point in nexts.items():
if _next_point is None:
self._rear[qudit_index] = point
# Add this to _dag
self._dag[point] = (prevs, nexts)
# Add op to the circuit structure
for qudit_index in op.location:
self._circuit[cycle_index][qudit_index] = op
# Update _graph_info
for pair in op.location.pairs:
if pair not in self._graph_info:
self._graph_info[pair] = 0
self._graph_info[pair] += 1
# Update _gate_info
if op.gate not in self._gate_info:
self._gate_info[op.gate] = 0
self._gate_info[op.gate] += 1
[docs]
def insert_gate(
self,
cycle_index: int,
gate: Gate,
location: CircuitLocationLike,
params: RealVector = [],
) -> None:
"""
Insert the gate object in the circuit on the qudits in location.
After this, if cycle was in range, you can expect:
`all([self[cycle_index, idx].gate == gate for idx in location])`
Args:
cycle_index (int): The cycle to insert the gate.
gate (Gate): The gate to insert.
location (CircuitLocationLike): Apply the gate to this set of
qudits.
params (RealVector): The gate's parameters.
Raises:
IndexError: If the specified cycle doesn't exist.
ValueError: If `gate` cannot be placed on the circuit due to
either an invalid location or gate radix mismatch.
See Also:
:func:`insert`
"""
self.insert(cycle_index, Operation(gate, location, params))
[docs]
def insert_circuit(
self,
cycle_index: int,
circuit: Circuit,
location: CircuitLocationLike,
as_circuit_gate: bool = False,
move: bool = False,
) -> None:
"""
Insert `circuit` at the cycle and location specified.
Args:
cycle_index (int): The cycle to insert the circuit.
circuit (Circuit): The circuit to insert.
location (CircuitLocationLike): Apply the circuit to these
qudits.
as_circuit_gate (bool): If true, append `circuit` as a unit
block (CircuitGate) rather than each operation in `circuit`
individually. (Default: False)
move (bool): Move circuit into circuit gate rather than copy.
(Default: False)
Raises:
ValueError: If `circuit` is not the same size as `location`.
See Also:
:func:`insert`
"""
if not is_integer(cycle_index):
raise TypeError(
f'Expected integer cycle index, got: {cycle_index}.',
)
if not is_bool(as_circuit_gate):
raise TypeError(f'Expected bool, got: {type(as_circuit_gate)}.')
if not CircuitLocation.is_location(location):
raise TypeError('Invalid location.')
location = CircuitLocation(location)
if circuit.num_qudits != len(location):
raise ValueError('Circuit and location size mismatch.')
if as_circuit_gate:
op = Operation(CircuitGate(circuit, move), location, circuit.params)
self.insert(cycle_index, op)
return
for op in reversed(circuit):
mapped_location = [location[q] for q in op.location]
self.insert(
cycle_index,
Operation(op.gate, mapped_location, op.params),
)
[docs]
def remove(self, op: Operation | Gate) -> None:
"""
Removes the first occurrence of `op` in the circuit.
Args:
op (Operation | Gate): The Operation or Gate to remove.
Raises:
ValueError: If the `op` doesn't exist in the circuit.
ValueError: If `op` could not have been placed on the circuit
due to either an invalid location or gate radix mismatch.
Examples:
>>> from bqskit.ir.gates import HGate
>>> circ = Circuit(1)
>>> op = Operation(HGate(), [0])
>>> circ.append(op)
>>> circ.num_operations
1
>>> circ.remove(op)
>>> circ.num_operations
0
See Also:
:func:`remove_all`
"""
self.pop(self.point(op))
[docs]
def remove_all(self, op: Operation | Gate) -> None:
"""
Removes the all occurrences of `op` in the circuit.
Args:
op (Operation | Gate): The Operation or Gate to remove.
Raises:
ValueError: If the `op` doesn't exist in the circuit.
ValueError: If `op` could not have been placed on the circuit
due to either an invalid location or gate radix mismatch.
See Also:
:func:`remove`
"""
while True:
try:
self.pop(self.point(op))
except (ValueError, IndexError):
break
[docs]
def count(self, op: Operation | Gate) -> int:
"""
Count the number of times `op` occurs in the circuit.
Args:
op (Operation | Gate): The operation or gate to count.
Raises:
ValueError: If `op` could not have been placed on the circuit
due to either an invalid location or gate radix mismatch.
Examples:
>>> from bqskit.ir.gates import HGate
>>> circ = Circuit(1)
>>> op = Operation(HGate(), [0])
>>> circ.append(op)
>>> circ.count(op)
1
>>> circ.append(op)
>>> circ.count(op)
2
"""
count = 0
if isinstance(op, Operation):
self.check_valid_operation(op)
if op.gate not in self._gate_info:
return 0
qudit_index = op.location[0]
for _op in self.operations(qudits_or_region=[qudit_index]):
if _op == op:
count += 1
elif isinstance(op, Gate):
if op not in self._gate_info:
return 0
return self._gate_info[op]
else:
raise TypeError('Expected gate or operation, got %s.' % type(op))
return count
[docs]
def pop(self, point: CircuitPointLike | None = None) -> Operation:
"""
Pop the operation at `point`, defaults to last operation.
Args:
point (CircuitPointLike | None): The cycle and qudit index
to pop from.
Returns:
Operation: The popped operation is returned.
Raises:
IndexError: If the `point` is out-of-range, or if no operation
exists at `point`.
Examples:
>>> from bqskit.ir.gates import HGate
>>> circ = Circuit(1)
>>> circ.append_gate(HGate(), [0])
>>> circ.num_operations
1
>>> circ.pop((0, 0))
HGate@(0,)
>>> circ.num_operations
0
"""
# Use given point
if point is not None:
if not self.is_point_in_range(point):
raise IndexError('Out-of-range point: %s.' % str(point))
# Or find last gate in simulation order
else:
cycle_index = self.num_cycles - 1
for i, op in enumerate(reversed(self._circuit[cycle_index])):
if op is not None:
point = (cycle_index, self.num_qudits - 1 - i)
break
if point is None:
raise IndexError('Pop from empty circuit.')
op = self[point]
point = self.normalize_point(point)
point = CircuitPoint(point[0], op.location[0])
# Remove it from _dag
prevs, nexts = self._dag[point]
for q in op.location:
prev = prevs[q]
if prev is not None:
self._dag[prev][1][q] = nexts[q]
next = nexts[q]
if next is not None:
self._dag[next][0][q] = prevs[q]
if self._front[q] == point:
self._front[q] = nexts[q]
if self._rear[q] == point:
self._rear[q] = prevs[q]
self._dag.pop(point)
# Remove it from _circuit
for qudit_index in op.location:
self._circuit[point[0]][qudit_index] = None
if self._is_cycle_idle(point.cycle):
self.pop_cycle(point.cycle)
# Update gate and graph counts
self._gate_info[op.gate] -= 1
if self._gate_info[op.gate] <= 0:
self._gate_info.pop(op.gate)
for pair in op.location.pairs:
self._graph_info[pair] -= 1
if self._graph_info[pair] <= 0:
self._graph_info.pop(pair)
return op
[docs]
def batch_pop(self, points: Iterable[CircuitPointLike]) -> Circuit:
"""
Pop all operatons at `points` at once.
Args:
points (Iterable[CircuitPointLike]): Remove operations
at these points all at the same time.
Returns:
Circuit: The circuit formed from all the popped operations.
Raises:
IndexError: If any of `points` are out-of-range.
IndexError: If all of `points` are invalid.
"""
if not all(self.is_point_in_range(point) for point in points):
raise IndexError('Out-of-range point.')
# Sort points
points = [self.normalize_point(point) for point in points]
points = [p for p in points if not self.is_point_idle(p)]
ops_and_cycles = {(self[point], point[0]) for point in points}
ops_and_cycles = sorted(list(ops_and_cycles), key=lambda x: x[1])
if len(points) == 0:
raise IndexError('Must batch pop at least one point.')
# Pop from circuit
for op, cycle in reversed(ops_and_cycles):
self.pop((cycle, op.location[0]))
# Form new circuit and return
ops = [op for op, _ in ops_and_cycles]
qudits = list(set(sum((tuple(op.location) for op in ops), ())))
qudits = sorted(qudits)
radixes = [self.radixes[q] for q in qudits]
circuit = Circuit(len(radixes), radixes)
for op in ops:
location = [qudits.index(q) for q in op.location]
circuit.append(Operation(op.gate, location, op.params))
return circuit
[docs]
def replace(self, point: CircuitPointLike, op: Operation) -> None:
"""
Replace the operation at `point` with `op`.
Args:
point (CircuitPointLike): The index of the operation to
replace.
op (Operation): The to-be-inserted operation.
Raises:
IndexError: If there is no operation at `point`.
ValueError: If `op` cannot be placed on the circuit due to
either an invalid location or gate radix mismatch.
ValueError: If `point.qudit` is not in `op.location`
"""
if len(self[point].location.intersection(op.location)) == 0:
raise ValueError("Point's qudit is not in operation's location.")
old_op = self._circuit[point[0]][point[1]]
if old_op is not None and set(old_op.location) == set(op.location):
if old_op.location[0] != op.location[0]:
old_point = CircuitPoint(point[0], old_op.location[0])
new_point = CircuitPoint(point[0], op.location[0])
prevs, nexts = self._dag[old_point]
for q in old_op.location:
if self._front[q] == old_point:
self._front[q] = new_point
if self._rear[q] == old_point:
self._rear[q] = new_point
prev = prevs[q]
if prev is not None:
self._dag[prev][1][q] = new_point
next = nexts[q]
if next is not None:
self._dag[next][0][q] = new_point
self._dag[new_point] = self._dag[old_point]
self._dag.pop(old_point)
self._gate_info[old_op.gate] -= 1
if self._gate_info[old_op.gate] <= 0:
self._gate_info.pop(old_op.gate)
if op.gate not in self._gate_info:
self._gate_info[op.gate] = 0
self._gate_info[op.gate] += 1
for q in old_op.location:
self._circuit[point[0]][q] = op
return
self.pop(point)
self.insert(point[0], op)
[docs]
def batch_replace(
self,
points: Sequence[CircuitPointLike],
ops: Sequence[Operation],
) -> None:
"""
Replace the operations at `points` with `ops`.
Args:
points (Sequence[CircuitPointLike]): The indices of the
operations to replace.
ops (Sequence[Operation]): The to-be-inserted operations.
Raises:
IndexError: If there is no operation at some point.
ValueError: If any op cannot be placed on the circuit due to
either an invalid location or gate radix mismatch.
ValueError: If any `point.qudit` is not in `op.location`
ValueError: If `points` and `ops` have different lengths.
"""
if len(points) != len(ops):
raise ValueError('Points and Ops have different lengths.')
points_and_ops = sorted(zip(points, ops), key=lambda x: x[0][0])
num_cycles = self.num_cycles
for point, op in points_and_ops:
shrink_amount = num_cycles - self.num_cycles
shifted_point = (point[0] - shrink_amount, point[1])
self.replace(shifted_point, op)
[docs]
def replace_gate(
self,
point: CircuitPointLike,
gate: Gate,
location: CircuitLocationLike,
params: RealVector = [],
) -> None:
"""Replace the operation at 'point' with `gate`."""
self.replace(point, Operation(gate, location, params))
[docs]
def replace_with_circuit(
self,
point: CircuitPointLike,
circuit: Circuit,
as_circuit_gate: bool = False,
move: bool = False,
) -> None:
"""Replace the operation at 'point' with `circuit`."""
op = self.pop(point)
if circuit.num_qudits != op.num_qudits:
raise ValueError('Cannot replace operation with circuit.')
if circuit.radixes != tuple(self.radixes[x] for x in op.location):
raise ValueError('Cannot replace operation with circuit.')
self.insert_circuit(
point[0],
circuit,
op.location,
as_circuit_gate,
move,
)
[docs]
def copy(self) -> Circuit:
"""Return a deep copy of this circuit."""
circuit = Circuit(self.num_qudits, self.radixes)
circuit._circuit = copy.deepcopy(self._circuit)
circuit._gate_info = copy.deepcopy(self._gate_info)
circuit._graph_info = copy.deepcopy(self._graph_info)
circuit._front = copy.deepcopy(self._front)
circuit._rear = copy.deepcopy(self._rear)
circuit._dag = copy.deepcopy(self._dag)
return circuit
[docs]
def become(self, circuit: Circuit, deepcopy: bool = True) -> None:
"""Become a copy of `circuit`."""
if deepcopy:
self._num_qudits = circuit.num_qudits
self._radixes = circuit.radixes
self._circuit = copy.deepcopy(circuit._circuit)
self._gate_info = copy.deepcopy(circuit._gate_info)
self._graph_info = copy.deepcopy(circuit._graph_info)
self._front = copy.deepcopy(circuit._front)
self._rear = copy.deepcopy(circuit._rear)
self._dag = copy.deepcopy(circuit._dag)
else:
self._num_qudits = circuit.num_qudits
self._radixes = circuit.radixes
self._circuit = copy.copy(circuit._circuit)
self._gate_info = copy.copy(circuit._gate_info)
self._graph_info = copy.copy(circuit._graph_info)
self._front = copy.copy(circuit._front)
self._rear = copy.copy(circuit._rear)
self._dag = copy.copy(circuit._dag)
[docs]
def clear(self) -> None:
"""Clear the circuit."""
self._circuit = []
self._gate_info = {}
self._graph_info = {}
self._front = {i: None for i in range(self.num_qudits)}
self._rear = {i: None for i in range(self.num_qudits)}
self._dag = {}
[docs]
def compress(self) -> None:
"""Compress the circuit's cycles."""
compressed_circuit = Circuit(self.num_qudits, self.radixes)
for op in self:
compressed_circuit.append(op)
self.become(compressed_circuit)
# endregion
# region Region Methods
[docs]
def is_valid_region(
self,
region: CircuitRegionLike,
strict: bool = False,
) -> bool:
"""Return true if `region` is valid in the context of this circuit."""
try:
self.check_region(region, strict)
except ValueError:
return False
return True
[docs]
def check_region(
self,
region: CircuitRegionLike,
strict: bool = False,
) -> None:
"""
Check `region` to be a valid in the context of this circuit.
Args:
region (CircuitRegionLike): The region to check.
strict (bool): If True, fail on any disconnect, even if there
are no gates in the disconnect. (Default: False)
Raises:
ValueError: If `region` includes qudits not in the circuit,
or if the region is too large for the circuit.
ValueError: If the region cannot be folded due to a possible
change to simulation order. This can happen when the
region is disconnected between qudits and outside gates
occur in the disconnect.
"""
if not CircuitRegion.is_region(region):
raise TypeError(f'Expected a CircuitRegion, got: {type(region)}.')
region = CircuitRegion(region)
if not CircuitLocation.is_location(region.location, self.num_qudits):
raise ValueError('Region circuit location mismatch.')
if region.max_cycle >= self.num_cycles:
raise ValueError(
'Region goes off circuit; '
f'circuit only has {self.num_cycles} cycles, '
f"but region's maximum cycle is {region.max_cycle}.",
)
for qudit_index, cycle_intervals in region.items():
for other_qudit_index, other_cycle_intervals in region.items():
if cycle_intervals.overlaps(other_cycle_intervals):
continue
involved_qudits = {qudit_index}
min_index = min(
cycle_intervals.upper,
other_cycle_intervals.upper,
)
max_index = max(
cycle_intervals.lower,
other_cycle_intervals.lower,
)
for cycle_index in range(min_index + 1, max_index):
try:
ops = self[cycle_index, involved_qudits]
except IndexError:
continue
if strict:
raise ValueError('Disconnect detected in region.')
if any(other_qudit_index in op.location for op in ops):
raise ValueError(
'Disconnected region has excluded gate in middle.',
)
for op in ops:
involved_qudits.update(op.location)
[docs]
def straighten(
self,
region: CircuitRegionLike,
) -> tuple[CircuitRegion, int, CircuitRegion]:
"""
Push gates back so the region has a single starting cycle.
Args:
region (CircuitRegionLike): The region to straighten.
Returns:
(tuple[CircuitRegion, int, CircuitRegion]):
Statistics on how gates where moved to straighten this
region. The first return value is the straightened region.
The second integer return value is the net number of
new cycles inserted at `region.min_cycle`. The last
return value is the shadow region, or the portion of the
middle region which did not move. See the Notes for
more info.
Raises:
ValueError: If the region is invalid, see `circuit.check_region`.
Notes:
When isolating a region, the circuit is divided into three
parts by `region.min_cycle` and `region.max_min_cycle`. The
left region is left unchanged and the right region
potentially moves to the right. Gates in the middle can
either be moved to the right or left unchanged depending
on their location. The last return value describes the
cycle-qudits that did not move from the middle portion.
"""
if len(region) == 0:
return CircuitRegion({}), 0, CircuitRegion({})
self.check_region(region)
region = self.downsize_region(region)
# Add idle cycles to create space
shadow_length = region.max_min_cycle - region.min_cycle
shadow_start = region.min_cycle
for i in range(shadow_length):
self._insert_cycle(shadow_start)
region = region.shift_right(shadow_length)
# Track region shadow and move gates
idle_cycles: list[int] = []
shadow_qudits: set[int] = set(region.keys())
# shadow_map = {qudit: region[qudit].lower for qudit in shadow_qudits}
shadow_map = {
qudit: min(
region.min_cycle - 1,
region[qudit].upper - shadow_length,
)
for qudit in shadow_qudits
}
for i in range(shadow_length):
old_cycle_index = region.max_min_cycle - 1 - i
new_cycle_index = region.min_cycle - 1 - i
gate_moved = False
qudits_to_add_to_shadow: list[int] = []
for qudit_index in shadow_qudits:
if (
qudit_index not in region
or old_cycle_index < region[qudit_index][0]
):
op = self._circuit[old_cycle_index][qudit_index]
if op is not None:
gate_moved = True
s_point = CircuitPoint(old_cycle_index, op.location[0])
d_point = CircuitPoint(new_cycle_index, op.location[0])
prevs, nexts = self._dag[s_point]
for qudit in op.location:
self._circuit[new_cycle_index][qudit] = op
self._circuit[old_cycle_index][qudit] = None
prev = prevs[qudit]
if prev is not None:
self._dag[prev][1][qudit] = d_point
next = nexts[qudit]
if next is not None:
self._dag[next][0][qudit] = d_point
if self._front[qudit] == s_point:
self._front[qudit] = d_point
if self._rear[qudit] == s_point:
self._rear[qudit] = d_point
self._dag.pop(s_point)
self._dag[d_point] = (prevs, nexts)
qudits_to_add_to_shadow.extend(op.location)
shadow_qudits.update(qudits_to_add_to_shadow)
for qudit in shadow_qudits:
if qudit not in shadow_map:
shadow_map[qudit] = new_cycle_index
if not gate_moved:
idle_cycles.append(new_cycle_index)
for i, cycle_index in enumerate(sorted(idle_cycles)):
self.pop_cycle(cycle_index - i)
region = region.shift_left(len(idle_cycles))
# Prep output
region = CircuitRegion({
qudit_index: (region.min_cycle, region[qudit_index][1])
for qudit_index in region
})
net_new_cycles = shadow_length - len(idle_cycles)
shadow_region = CircuitRegion({
qudit_index: (shadow_start, shadow_map[qudit_index])
for qudit_index in shadow_qudits
if shadow_start <= shadow_map[qudit_index]
})
return region, net_new_cycles, shadow_region
[docs]
def fold(self, region: CircuitRegionLike) -> CircuitPoint:
"""
Fold the specified `region` into a CircuitGate.
Args:
region (CircuitRegionLike): The region to fold into a
CircuitGate.
Returns:
(CircuitPoint): The resulting CircuitGate's location.
Raises:
ValueError: If `region` is invalid or cannot be straightened.
"""
if len(region) == 0:
raise ValueError('Empty region cannot be folded.')
region = self.straighten(region)[0]
circuit = self.batch_pop(region.points)
# Insert popped circuit as a CircuitGate
self.insert_circuit(
region.min_cycle,
circuit,
sorted(list(region.keys())),
True,
)
return CircuitPoint(region.min_cycle, region.min_qudit)
[docs]
def unfold(self, point: CircuitPointLike) -> None:
"""Unfold the CircuitGate at `point` into the circuit."""
if not isinstance(self[point].gate, CircuitGate):
raise ValueError('Expected to unfold a CircuitGate.')
op = self[point]
circuit: Circuit = op.gate._circuit # type: ignore
circuit.set_params(op.params)
self.replace_with_circuit(point, circuit)
[docs]
def batch_unfold(self, points: Sequence[CircuitPointLike]) -> None:
"""Unfold the CircuitGates at `points` into the circuit."""
points = {(point[0], self[point].location[0]) for point in points}
for point in reversed(sorted(points)):
self.unfold(point)
[docs]
def unfold_all(self) -> None:
"""Unfold all CircuitGates in the circuit."""
while any(isinstance(gate, CircuitGate) for gate in self.gate_set):
unfolded_circuit = Circuit(self.num_qudits, self.radixes)
for op in self:
if isinstance(op.gate, CircuitGate):
circuit = op.gate._circuit
circuit.set_params(op.params)
unfolded_circuit.append_circuit(circuit, op.location)
else:
unfolded_circuit.append(op)
self.become(unfolded_circuit)
[docs]
def surround(
self,
point: CircuitPointLike,
num_qudits: int,
bounding_region: CircuitRegionLike | None = None,
fail_quickly: bool = False,
) -> CircuitRegion:
"""
Retrieve the maximal region in this circuit with `point` included.
Args:
point (CircuitPointLike): Find a surrounding region for this
point. This point will be in the final CircuitRegion.
num_qudits (int): The number of qudits to include in the region.
bounding_region (CircuitRegionLike | None): An optional
region that bounds the resulting region.
fail_quickly (bool): If set to true, will not branch on
an invalid region. This will lead to a much faster
result in some cases at the cost of only approximating
the maximal region.
Raises:
IndexError: If `point` is not a valid index.
ValueError: If `num_qudits` is nonpositive.
ValueError: If the operation at `point` is too large for
`num_qudits`.
ValueError: If `bounding_region` is invalid.
Notes:
This algorithm explores outward horizontally as much as possible.
When a gate is encountered that involves another qudit not
currently in the region, a decision needs to be made on whether
that gate will be included or not. These decisions form a tree;
an exhaustive search is employed to find the maximal region
from this decision tree.
"""
if not is_integer(num_qudits):
raise TypeError(
f'Expected an integer num_qudits, got {type(num_qudits)}.',
)
if num_qudits <= 0:
raise ValueError(
f'Expected a positive integer num_qudits, got {num_qudits}.',
)
if bounding_region is not None:
bounding_region = CircuitRegion(bounding_region)
point = self.normalize_point(point)
init_op: Operation = self[point] # Allow starting at an idle point
if init_op.num_qudits > num_qudits:
raise ValueError('Gate at point is too large for num_qudits.')
HalfWire = Tuple[CircuitPoint, str]
"""
A HalfWire is a point in the circuit and a direction.
This represents a point to start exploring from and a direction to
explore in.
"""
Node = Tuple[
List[HalfWire],
Set[Tuple[int, Operation]],
CircuitLocation,
Set[CircuitPoint],
]
"""
A Node in the search tree.
Each node represents a region that may grow further. The data structure
tracks all HalfWires in the region and the set of operations inside the
region. During node exploration each HalfWire is walked until we find a
multi-qudit gate. Multi- qudit gates form branches in the tree on
whether on the gate should be included. The node structure additionally
stores the set of qudit indices involved in the region currently. Also,
we track points that have already been explored to reduce repetition.
"""
# Initialize the frontier
init_node = (
[
(CircuitPoint(point[0], qudit_index), 'left')
for qudit_index in init_op.location
]
+ [
(CircuitPoint(point[0], qudit_index), 'right')
for qudit_index in init_op.location
],
{(point[0], init_op)},
init_op.location,
{CircuitPoint(point[0], q) for q in init_op.location},
)
frontier: list[Node] = [init_node]
# Track best so far
def score(node: Node) -> int:
return sum(op[1].num_qudits for op in node[1])
best_score = score(init_node)
best_region = self.get_region({(point[0], init_op.location[0])})
# Exhaustive Search
while len(frontier) > 0:
node = frontier.pop(0)
_logger.debug('popped node:')
_logger.debug(node[0])
_logger.debug(f'Items remaining in the frontier: {len(frontier)}')
# Evaluate node
if score(node) > best_score:
# Calculate region from best node and return
points = {(cycle, op.location[0]) for cycle, op in node[1]}
try:
best_region = self.get_region(points)
best_score = score(node)
_logger.debug(f'new best: {best_region}.')
# Need to reject bad regions
except ValueError:
if fail_quickly:
continue
# Expand node
absorbed_gates: set[tuple[int, Operation]] = set()
branches: set[tuple[int, int, Operation]] = set()
before_branch_half_wires: dict[int, HalfWire] = {}
for i, half_wire in enumerate(node[0]):
cycle_index, qudit_index = half_wire[0]
step = -1 if half_wire[1] == 'left' else 1
while True:
# Take a step
cycle_index += step
# Stop at edges
if cycle_index < 0 or cycle_index >= self.num_cycles:
break
# Stop when outside bounds
if bounding_region is not None:
if (cycle_index, qudit_index) not in bounding_region:
break
# Stop when exploring previously explored points
point = CircuitPoint(cycle_index, qudit_index)
if point in node[3]:
break
node[3].add(point)
# Continue until next operation
if self.is_point_idle(point):
continue
op: Operation = self[cycle_index, qudit_index]
# Gates already in region stop the half_wire
if (cycle_index, op) in node[1]:
break
# Gates already accounted for stop the half_wire
if (cycle_index, op) in absorbed_gates:
break
if (cycle_index, op) in [(c, o) for h, c, o in branches]:
break
# Absorb single-qudit gates
if len(op.location) == 1:
absorbed_gates.add((cycle_index, op))
continue
# Operations that are too large stop the half_wire
if len(op.location.union(node[2])) > num_qudits:
break
# Otherwise branch on the operation
branches.add((i, cycle_index, op))
# Track state of half wire right before branch
prev_point = CircuitPoint(cycle_index - step, qudit_index)
before_branch_half_wires[i] = (prev_point, half_wire[1])
break
# Compute children and extend frontier
for half_wire_index, cycle_index, op in branches:
child_half_wires = [
half_wire
for i, half_wire in before_branch_half_wires.items()
if half_wire_index != i
]
qudit = node[0][half_wire_index][0].qudit
direction = node[0][half_wire_index][1]
left_expansion = [
(CircuitPoint(cycle_index, qudit_index), 'left')
for qudit_index in op.location
if qudit != qudit_index or direction == 'left'
]
right_expansion = [
(CircuitPoint(cycle_index, qudit_index), 'right')
for qudit_index in op.location
if qudit != qudit_index or direction == 'right'
]
expansion = left_expansion + right_expansion
# Branch/Gate not taken
frontier.append((
child_half_wires,
node[1] | absorbed_gates,
node[2],
node[3],
))
# Branch/Gate taken
op_points = {CircuitPoint(cycle_index, q) for q in op.location}
frontier.append((
list(set(child_half_wires + expansion)),
node[1] | absorbed_gates | {(cycle_index, op)},
node[2].union(op.location),
node[3] | op_points,
))
# Append terminal node to handle absorbed gates with no branches
if len(node[1] | absorbed_gates) != len(node[1]):
frontier.append(([], node[1] | absorbed_gates, *node[2:]))
return best_region
[docs]
def get_region(self, points: Iterable[CircuitPointLike]) -> CircuitRegion:
"""
Calculate the minimal region from a sequence of points.
Args:
points (Iterable[CircuitPointLike]): The positions of operations
to group together.
Returns:
(CircuitRegion): The region given by `points`.
Raises:
ValueError: If `points` do not form a valid convex region
in the circuit. This happens when there exists an operation
within the bounds of `points`, but not contained in it.
IndexError: If any of `points` are out-of-range.
"""
if not is_iterable(points):
raise TypeError(
'Expected iterable of points, got %s.' % type(points),
)
points = list(points)
if not all(CircuitPoint.is_point(point) for point in points):
checks = [CircuitPoint.is_point(point) for point in points]
raise TypeError(
'Expected iterable of points'
f', got {points[checks.index(False)]} for at least one point.',
)
if len(points) == 0:
return CircuitRegion({})
# Collect operations
ops_and_cycles = list({
(point[0], self[point])
for point in points
if not self.is_point_idle(point)
})
# Calculate the bounding region to be folded
# The region is represented by the max and min cycle for each qudit.
region: dict[int, tuple[int, int]] = {}
for point in points:
# Flip negative indices
cycle_index = point[0]
if cycle_index < 0:
cycle_index = self.num_cycles + cycle_index
if self.is_point_idle(point):
continue
for qudit_index in self[point].location:
if qudit_index not in region:
region[qudit_index] = (self.num_cycles, -1)
if cycle_index < region[qudit_index][0]:
region[qudit_index] = (cycle_index, region[qudit_index][1])
if cycle_index > region[qudit_index][1]:
region[qudit_index] = (region[qudit_index][0], cycle_index)
# Count operations within region
ops_in_region = set()
for qudit_index, bounds in region.items():
for i, cycle in enumerate(self._circuit[bounds[0]:bounds[1] + 1]):
op = cycle[qudit_index]
if op is not None:
ops_in_region.add((bounds[0] + i, op))
# All operations in the region must be getting folded
if len(ops_and_cycles) != len(ops_in_region):
raise ValueError(
'Operations cannot be grouped in a region due to'
' another operation in the middle.',
)
self.check_region(region)
return CircuitRegion(region)
[docs]
def downsize_region(self, region: CircuitRegionLike) -> CircuitRegion:
"""Remove all idle qudits-cycles in `region` while keeping it valid."""
return self.get_region(CircuitRegion(region).points)
[docs]
def get_operations(
self,
points: Iterable[CircuitPointLike],
) -> list[Operation]:
"""Retrieve operations from `points` without throwing IndexError."""
if not is_iterable(points):
raise TypeError(
f'Expected a circuit point iterable, got {type(points)}.',
)
if not all(CircuitPoint.is_point(point) for point in points):
raise TypeError(
f'Expected a circuit point iterable, got {type(points)}.',
)
ops: list[tuple[int, Operation]] = list()
for point in points:
try:
ops.append((point[0], self.get_operation(point)))
except IndexError:
continue
return [op_and_cycle[1] for op_and_cycle in list(dict.fromkeys(ops))]
[docs]
def get_slice(self, points: Sequence[CircuitPointLike]) -> Circuit:
"""Return a copy of a slice of this circuit."""
# Sort points
points = sorted(points)
# Collect operations avoiding duplicates
ops_and_cycles: list[tuple[Operation, int]] = list({
(self[point], point[0])
for point in points
if not self.is_point_idle(point)
})
if len(ops_and_cycles) == 0:
raise IndexError('No operations exists at any of the points.')
ops_and_cycles.sort(key=lambda x: (x[1], *x[0].location))
ops = [op for op, _ in ops_and_cycles]
if len(ops_and_cycles) == 0:
raise IndexError('No operations exists at any of the points.')
# Form new circuit and return
qudits = list(set(sum((tuple(op.location) for op in ops), ())))
qudits = sorted(qudits)
radixes = [self.radixes[q] for q in qudits]
circuit = Circuit(len(radixes), radixes)
for op in ops:
location = [qudits.index(q) for q in op.location]
circuit.append(Operation(op.gate, location, op.params))
return circuit
# endregion
# region Parameter Methods
[docs]
def get_param(self, param_index: int) -> float:
"""Return the parameter at param_index."""
cycle, qudit, param = self.get_param_location(param_index)
return self[cycle, qudit].params[param]
[docs]
def set_param(self, param_index: int, value: float) -> None:
"""Set a circuit parameter."""
cycle, qudit, param = self.get_param_location(param_index)
self[cycle, qudit].params[param] = value
[docs]
def set_params(self, params: RealVector) -> None:
"""Set all parameters at once."""
self.check_parameters(params)
param_index = 0
for op in self:
op.params = list(
params[param_index: param_index + op.num_params],
)
param_index += op.num_params
[docs]
def freeze_param(self, param_index: int) -> None:
"""Freeze a circuit parameter to its current value."""
cycle, qudit, param = self.get_param_location(param_index)
op = self[cycle, qudit]
gate = op.gate.with_frozen_params({param: op.params[param]})
params = op.params.copy()
params.pop(param)
self.replace_gate((cycle, qudit), gate, op.location, params)
[docs]
def get_param_location(self, param_index: int) -> tuple[int, int, int]:
"""
Converts a param_index to a cycle, qudit, and operation-param index.
Args:
param_index (int): The parameter index to convert.
Returns:
(tuple[int, int, int]): A tuple of cycle_index, qudit_index,
and operation-param index. The operation the parameter
belongs to will be at circuit[cycle_index, qudit_index].
This parameter in that operation is indexed by
the operation-param index.
Raises:
IndexError: If the param_index is invalid.
Examples:
>>> from bqskit.ir.gates import U3Gate
>>> circ = Circuit(1)
>>> circ.append_gate(U3Gate(), [0])
>>> circ.append_gate(U3Gate(), [0])
>>> circ.num_params
6
>>> circ.get_param_location(4)
(1, 0, 1)
"""
if param_index < 0:
raise IndexError('Negative parameter index is not supported.')
count = 0
for cycle, op in self.operations_with_cycles():
count += len(op.params)
if count > param_index:
param = param_index - (count - len(op.params))
return (cycle, op.location[0], param)
raise IndexError('Out-of-range parameter index.')
# endregion
# region Circuit Logic Methods
[docs]
def get_inverse(self) -> Circuit:
"""Return the circuit's inverse circuit."""
circuit = Circuit(self.num_qudits, self.radixes)
for op in reversed(self):
circuit.append(op.get_inverse())
return circuit
[docs]
def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
"""
Return the unitary matrix of the circuit.
Args:
params (RealVector): Optionally specify parameters
overriding the ones stored in the circuit. (Default:
use parameters already in circuit.)
Returns:
The UnitaryMatrix object that the circuit implements.
Raises:
ValueError: If parameters are specified and invalid.
Examples:
>>> from bqskit.ir.gates import HGate
>>> circ = Circuit(1)
>>> op = Operation(HGate(), [0])
>>> circ.append(op)
>>> circ.get_unitary() == HGate().get_unitary()
True
"""
if len(params) != 0:
self.check_parameters(params)
param_index = 0
utry = UnitaryBuilder(self.num_qudits, self.radixes)
for op in self:
if len(params) != 0:
gparams = params[param_index:param_index + op.num_params]
utry.apply_right(op.get_unitary(gparams), op.location)
param_index += op.num_params
else:
utry.apply_right(op.get_unitary(), op.location)
return utry.get_unitary()
[docs]
def get_statevector(
self,
in_state: StateLike,
params: RealVector = [],
) -> StateVector:
"""
Return the result of applying `self` to `in_state`
Args:
params (RealVector): Optionally specify parameters
overriding the ones stored in the circuit. (Default:
use parameters already in circuit.)
Returns:
The StateVector object for the new state after the circuit
Raises:
ValueError: If parameters are specified and invalid.
Examples:
>>> from bqskit.ir.gates import HGate
>>> circ = Circuit(1)
>>> op = Operation(HGate(), [0])
>>> circ.append(op)
>>> V = StateVector([1,0])
>>> np.allclose(circ.get_statevector(V), np.array([1,1])/np.sqrt(2))
True
"""
if len(params) != 0:
self.check_parameters(params)
param_index = 0
new_state = StateVector(in_state)
for op in self:
if len(params) != 0:
gparams = params[param_index:param_index + op.num_params]
new_state.apply(op.get_unitary(gparams), op.location)
param_index += op.num_params
else:
new_state.apply(op.get_unitary(), op.location)
return new_state
[docs]
def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]:
"""Return the gradient of the circuit."""
return self.get_unitary_and_grad(params)[1]
[docs]
def get_unitary_and_grad(
self, params: RealVector = [],
) -> tuple[UnitaryMatrix, npt.NDArray[np.complex128]]:
"""Return the unitary and gradient of the circuit."""
if len(params) != 0:
self.check_parameters(params)
param_index = 0
# Collect matrices, gradients, and locations
matrices = []
grads = []
locations = []
for op in self:
if len(params) != 0:
gparams = params[param_index:param_index + op.num_params]
param_index += op.num_params
M, dM = op.get_unitary_and_grad(gparams)
matrices.append(M)
grads.append(dM)
locations.append(op.location)
else:
M, dM = op.get_unitary_and_grad()
matrices.append(M)
grads.append(dM)
locations.append(op.location)
# Calculate gradient
left = UnitaryBuilder(self.num_qudits, self.radixes)
right = UnitaryBuilder(self.num_qudits, self.radixes)
full_grads = []
for M, loc in zip(matrices, locations):
right.apply_right(M, loc)
for M, dM, loc in zip(matrices, grads, locations):
right.apply_left(M, loc, inverse=True)
right_utry = right.get_unitary()
for grad in dM:
full_grads.append(right_utry @ left.eval_apply_right(grad, loc))
left.apply_right(M, loc)
return left.get_unitary(), np.array(full_grads)
[docs]
def instantiate(
self,
target: StateLike | UnitaryLike | StateSystem,
method: str | Instantiater | None = None,
multistarts: int = 1,
seed: int | None = None,
multistart_gen: MultiStartGenerator | None = None,
score_fn_gen: CostFunctionGenerator | None = None,
**kwargs: Any,
) -> Circuit:
"""
Instantiate the circuit with respect to a target state or unitary.
Attempts to change the parameters of the circuit such that the
circuit either implements the target unitary or maps the zero
state to the target state.
Args:
target (StateLike | UnitaryLike | StateSystem): The target unitary
or state. If a unitary is specified, the method changes
the circuit's parameters in an effort to get closer to
implementing the target. If a state is specified, the
method changes the circuit's parameters in an effort to
get closer to producing the target state when starting
from the zero state.
method (str | Instantiater | None): The method with which to
instantiate the circuit. Currently, `"qfactor"` and
`"minimization"` are supported. If left None, attempts to
pick best method. You can also pass an :class:`Instantiater`
directly through this.
multistarts (int): The number of starting points to sample
instantiation with. (Default: 1)
seed (int | None): The seed for any pseudo-random number generators
to use. Note that this is not guaranteed to make this method
reproducible.
multistart_gen (MultiStartGenerator): (Deprecated)
score_fn_gen (CostFunctionGenerator): (Deprecated)
kwargs (dict[str, Any]): Method specific options, passed
directly to method constructor. For more info, see
`bqskit.ir.opt.instantiaters`.
Returns:
Circuit: A reference to `self` is returned
Raises:
ValueError: If `method` is invalid.
ValueError: If `circuit` is incompatible with any method.
ValueError: If `target` dimension doesn't match with circuit.
ValueError: If `multistarts` is not a positive integer.
ValueError: If `seed` is not an integer or `None`
"""
if multistart_gen is not None or score_fn_gen is not None:
warnings.warn(
'Multistart handling has moved from the `circuit.instantiate`'
' method to the Instantiater class. See'
' `Instantiater.multi_start_instantiate` for more info. If you'
' would like to override how starts are generated or'
' instantiation results are processed, you should subclass'
' an Instantiater. An Instantiatier object can be passed to'
' `circuit.instantiate` through the `method` parameter. It'
' can also be passed to most BQSKit compiler passes through'
" the `instantiate_options={'method':...} parameter. The use"
' of `multistart_gen` and `score_fn_gen` parameters in'
' `circuit.instantiate` is deprecated, and this warning'
' will turn into an error in the future.',
DeprecationWarning,
)
# Set seed if specified
if seed is not None:
if not isinstance(seed, int):
raise ValueError(
f'Expected seed to be an integer, got {type(seed)}.',
)
seed_random_sources(seed)
# Use given Instantiater if one is specified.
if isinstance(method, Instantiater):
instantiater = method
if not instantiater.is_capable(self):
raise ValueError(
'Circuit cannot be instantiated using the '
f'{method} method.'
f'\n{instantiater.get_violation_report(self)}',
)
# Find best Instantiater if none specified
elif method is None:
err = ''
instantiater = None
for inst in instantiater_order:
inst_t = cast(Instantiater, inst)
if inst_t.is_capable(self):
instantiater = inst_t(**kwargs) # type: ignore
break
err += inst_t.get_violation_report(self) + '\n'
if instantiater is None:
raise ValueError(f'No capable instantiater.\n{err}')
# If method is specified by name; match it
elif isinstance(method, str):
instantiater = None
for inst in instantiater_order:
inst_t = cast(Instantiater, inst)
if inst_t.get_method_name().lower() == method.lower():
if not inst_t.is_capable(self):
raise ValueError(
'Circuit cannot be instantiated using the '
f'{method} method.'
f'\n{inst_t.get_violation_report(self)}',
)
instantiater = inst_t(**kwargs) # type: ignore
break
if instantiater is None:
raise ValueError(f'No such instantiatation method {method}.')
else:
raise TypeError(
'Expected a instantiater or name for method,'
f' got {type(method)}.',
)
instantiater = cast(Instantiater, instantiater)
# Instantiate
instantiater.multi_start_instantiate_inplace(self, target, multistarts)
return self
[docs]
def minimize(self, cost: CostFunction, **kwargs: Any) -> None:
"""
Minimize the circuit's cost with respect to some CostFunction.
Attempts to change the parameters of the circuit such that the
circuit's cost according to `cost` is best minimized.
Args:
cost (CostFunction): The cost function to use when evaluting
the circuit's cost.
minimizer (str): The minimization method to use. If unspecified,
attempts to assign best method. (kwarg)
"""
minimizer = kwargs.get('minimizer', CeresMinimizer())
self.set_params(minimizer.minimize(cost, self.params))
# endregion
# region Measurement Methods
[docs]
def remove_all_measurements(self) -> None:
"""Remove all measurement placeholders from the circuit."""
while any(isinstance(g, MeasurementPlaceholder) for g in self.gate_set):
for g in self.gate_set:
if isinstance(g, MeasurementPlaceholder):
self.remove(g)
# endregion
# region Collection and Iteration Methods
@overload
def __getitem__(self, point: CircuitPointLike) -> Operation:
...
@overload
def __getitem__(
self, points: Iterable[CircuitPointLike],
) -> list[Operation]:
...
@overload
def __getitem__(self, region: CircuitRegionLike) -> list[Operation]:
...
@overload
def __getitem__(
self,
slice: int | slice,
) -> list[Operation]:
...
@overload
def __getitem__(
self,
slices: tuple[Iterable[int] | slice, Iterable[int] | slice]
| tuple[int, Iterable[int] | slice]
| tuple[Iterable[int] | slice, int],
) -> list[Operation]:
...
def __getitem__(
self,
indices: CircuitPointLike
| Iterable[CircuitPointLike]
| CircuitRegionLike
| int | Iterable[int] | slice
| tuple[Iterable[int] | slice, Iterable[int] | slice]
| tuple[int, Iterable[int] | slice]
| tuple[Iterable[int] | slice, int],
) -> Operation | list[Operation]:
"""
Retrieve operations from the circuit.
Args:
indices (
CircuitPointLike
| Iterable[CircuitPointLike]
| CircuitRegionLike
| int | slice
| tuple[Iterable[int] | slice, Iterable[int] | slice]
| tuple[int, Iterable[int] | slice]
| tuple[Iterable[int] | slice, int],
):
This parameter describes the area in the circuit to retrieve
operations from. If a point is given, then the operation
at that point will be returned. In all other cases,
a list of operations will be returned. You can specify
a sequence of points to directly get operations from all
those points. You can specify a cycle index, a sequence of
cycle indices, or a slice to retrieve operations from those
cycles. You can also specify a tuple that describes all
cycles and qudits to sample from. Lastly, you can specify
a circuit region to get all the operations there.
Returns:
(Operation | list[Operation]): Either a specific operation is
returned or a list of them depending on the type of `indices`.
Raises:
IndexError: If any specified point is invalid or out-of-range.
"""
if CircuitPoint.is_point(indices):
return self.get_operation(indices)
if is_iterable(indices):
if all(CircuitPoint.is_point(point) for point in indices):
return self.get_operations(indices)
if CircuitRegion.is_region(indices):
return self[CircuitRegion(indices).points]
if is_integer(indices):
return list({
op
for op in self._circuit[indices]
if op is not None
})
# if is_iterable(indices):
# if all(is_integer(cycle_index) for cycle_index in indices):
# return sum([self[cycle_index] for cycle_index in indices], [])
if isinstance(indices, slice):
start, stop, step = indices.indices(self.num_cycles)
acm: list[Operation] = []
return sum((self[index] for index in range(start, stop, step)), acm)
if isinstance(indices, tuple) and len(indices) == 2:
cycle_indices, qudit_indices = indices
cycles, qudits = None, None
if is_integer(cycle_indices):
cycles = [cycle_indices]
elif isinstance(cycle_indices, slice):
start, stop, step = cycle_indices.indices(self.num_cycles)
cycles = list(range(start, stop, step))
elif is_iterable(cycle_indices):
if all(is_integer(index) for index in cycle_indices):
cycles = list(cycle_indices)
if is_integer(qudit_indices):
qudits = [qudit_indices]
elif isinstance(qudit_indices, slice):
start, stop, step = qudit_indices.indices(self.num_qudits)
qudits = list(range(start, stop, step))
elif is_iterable(qudit_indices):
if all(is_integer(index) for index in qudit_indices):
qudits = list(qudit_indices)
if cycles is not None and qudits is not None:
return self[[
CircuitPoint(cycle, qudit)
for cycle in cycles
for qudit in qudits
]]
raise TypeError(
'Invalid index type. Expected point'
', sequence of points, slice, region, or pair of indices'
', got %s.' % type(indices),
)
def __iter__(self) -> Iterator[Operation]:
"""Return an iterator that iterates through all operations."""
return CircuitIterator(self) # type: ignore
def __reversed__(self) -> Iterator[Operation]:
"""Return a reverse iterator that iterates through all operations."""
return CircuitIterator(self, reverse=True) # type: ignore
def __contains__(self, op: object) -> bool:
"""Return true if `op` is in the circuit."""
if isinstance(op, Operation):
try:
self.check_valid_operation(op)
except ValueError:
return False
if op.gate not in self._gate_info:
return False
for _op in self.operations(qudits_or_region=[op.location[0]]):
if op == _op:
return True
return False
elif isinstance(op, Gate):
return op in self._gate_info
else:
return False
def __len__(self) -> int:
"""Return the number of operations in the circuit."""
return self.num_operations
[docs]
def operations(
self,
start: CircuitPointLike = CircuitPoint(0, 0),
end: CircuitPointLike | None = None,
qudits_or_region: CircuitRegionLike | Sequence[int] | None = None,
exclude: bool = False,
reverse: bool = False,
) -> Iterator[Operation]:
"""Create and return an iterator, for more info see CircuitIterator."""
return CircuitIterator(
self,
start,
end,
qudits_or_region,
exclude,
reverse,
) # type: ignore
[docs]
def operations_with_cycles(
self,
start: CircuitPointLike = CircuitPoint(0, 0),
end: CircuitPointLike | None = None,
qudits_or_region: CircuitRegionLike | Sequence[int] | None = None,
exclude: bool = False,
reverse: bool = False,
) -> Iterator[tuple[int, Operation]]:
"""Create and return an iterator, for more info see CircuitIterator."""
return CircuitIterator(
self,
start,
end,
qudits_or_region,
exclude,
reverse,
True,
) # type: ignore
# endregion
# region Operator Overloads
def __invert__(self) -> Circuit:
"""Invert the circuit."""
return self.get_inverse()
def __eq__(self, rhs: object) -> bool:
"""
Check for circuit equality.
Two circuits are equal if: 1) They have the same number of operations.
2) All qudit radixes are equal. 3) All operations in simulation order
are equal.
"""
if not isinstance(rhs, Circuit):
raise NotImplemented
if self is rhs:
return True
if self._gate_info != rhs._gate_info:
return False
for r1, r2 in zip(self.radixes, rhs.radixes):
if r1 != r2:
return False
return all(op1 == op2 for op1, op2 in zip(self, rhs))
def __ne__(self, rhs: object) -> bool:
"""Check for circuit inequality, see __eq__ for more info."""
return not self == rhs
def __add__(self, rhs: Circuit) -> Circuit:
"""Return a concatenated circuit copy."""
circuit = Circuit(self.num_qudits, self.radixes)
circuit.append_circuit(self, list(range(self.num_qudits)))
circuit.append_circuit(rhs, list(range(self.num_qudits)))
return circuit
def __mul__(self, rhs: int) -> Circuit:
"""Return a repeated circuit copy."""
circuit = Circuit(self.num_qudits, self.radixes)
for x in range(rhs):
circuit.append_circuit(self, list(range(self.num_qudits)))
return circuit
def __radd__(self, lhs: Circuit) -> Circuit:
"""Return a concatenated circuit copy."""
circuit = Circuit(self.num_qudits, self.radixes)
circuit.append_circuit(lhs, list(range(self.num_qudits)))
circuit.append_circuit(self, list(range(self.num_qudits)))
return circuit
def __iadd__(self, rhs: Circuit) -> None:
"""Return a concatenated circuit copy."""
self.append_circuit(rhs, list(range(self.num_qudits)))
def __imul__(self, rhs: int) -> None:
"""Return a repeated circuit copy."""
circuit = self.copy()
for x in range(rhs - 1):
self.append_circuit(circuit, list(range(self.num_qudits)))
# endregion
# region IO Methods
def __str__(self) -> str:
"""String representation of the circuit."""
op_string = '['
if self.num_operations == 1:
op_string += str(list(self.operations())[0])
elif self.num_operations == 2:
op_string += str(list(self.operations())[0])
op_string += ', '
op_string += str(list(self.operations())[1])
elif self.num_operations > 2:
op_string += str(list(self.operations())[0])
op_string += ' ... '
op_string += str(list(self.operations())[-1])
op_string += ']'
num_qudits = self.num_qudits
return f'Circuit({num_qudits}){op_string}'
def __repr__(self) -> str:
"""Repr representation of the circuit."""
string = f'Circuit({self.num_qudits})'
for cycle in self._circuit[:100]:
string += f'\n\t{cycle}'
if self.num_cycles > 100:
string += '...'
return string
[docs]
def save(self, filename: str) -> None:
"""Save the circuit to a file."""
language = get_language(filename.split('.')[-1])
with open(filename, 'w') as f:
f.write(language.encode(self))
[docs]
def to(self, type: str) -> str:
"""Convert circuit to language."""
return get_language(type).encode(self)
[docs]
@staticmethod
def from_file(filename: str) -> Circuit:
"""Restore a circuit from a file."""
language = get_language(filename.split('.')[-1])
with open(filename) as f:
return language.decode(f.read())
[docs]
@staticmethod
def from_unitary(utry: UnitaryLike) -> Circuit:
"""Construct a circuit from a single unitary."""
utry = UnitaryMatrix(utry)
circuit = Circuit(utry.num_qudits, utry.radixes)
circuit.append_gate(
ConstantUnitaryGate(utry), list(range(utry.num_qudits)),
)
return circuit
[docs]
@staticmethod
def from_operation(op: Operation) -> Circuit:
"""Construct a circuit from a single operation."""
circuit = Circuit(op.num_qudits, op.radixes)
circuit.append_gate(op.gate, list(range(circuit.num_qudits)), op.params)
return circuit
# endregion