Skip to content
2 changes: 2 additions & 0 deletions bqskit/ft/cliffordrz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

from bqskit.ft.cliffordrz.cliffordrzgates import clifford_rz_gates
from bqskit.ft.cliffordrz.cliffordrzmodel import CliffordRZModel
from bqskit.ft.cliffordrz.defaultworkflow import build_cliffordrz_workflow

__all__ = [
'CliffordRZModel',
'clifford_rz_gates',
'build_cliffordrz_workflow',
]
2 changes: 1 addition & 1 deletion bqskit/ft/cliffordrz/defaultworkflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def clifford_replace() -> BasePass:
ReplacementRule(sdg_repl_rule, SdgGate()), # type: ignore
ReplacementRule(t_repl_rule, TGate()), # type: ignore
ReplacementRule(tdg_repl_rule, TdgGate()), # type: ignore
ReplacementRule(i_repl_rule, IdentityGate()), # type: ignore
ReplacementRule(i_repl_rule, None), # type: ignore
],
collection_filter=single_qudit_filter,
)
Expand Down
2 changes: 1 addition & 1 deletion bqskit/ft/cliffordt/defaultworkflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def clifford_replace() -> BasePass:
ReplacementRule(sdg_repl_rule, SdgGate()), # type: ignore
ReplacementRule(t_repl_rule, TGate()), # type: ignore
ReplacementRule(tdg_repl_rule, TdgGate()), # type: ignore
ReplacementRule(i_repl_rule, IdentityGate()), # type: ignore
ReplacementRule(i_repl_rule, None), # type: ignore
],
collection_filter=single_qudit_filter,
)
Expand Down
4 changes: 4 additions & 0 deletions bqskit/ft/ftpasses/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""Fault-tolerant synthesis passes for BQSKit."""
from __future__ import annotations

from bqskit.ft.ftpasses.convert_to_pkac import ConvertToPKAC
from bqskit.ft.ftpasses.gridsynth import GridSynthPass
from bqskit.ft.ftpasses.pkac_to_gates import PKACtoGatesPass
from bqskit.ft.ftpasses.rounding import RoundToDiscreteZPass

__all__ = [
'GridSynthPass',
'RoundToDiscreteZPass',
'ConvertToPKAC',
'PKACtoGatesPass',
]
125 changes: 125 additions & 0 deletions bqskit/ft/ftpasses/convert_to_pkac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from __future__ import annotations

from bqskit.compiler.basepass import BasePass
from bqskit.compiler.passdata import PassData
from bqskit.ft.gadgets.qft import QFTGadget
from bqskit.ft.gates.fractional_rz import FractionalRZGate
from bqskit.ft.gates.gidney_adder import GidneyAdder
from bqskit.ir.circuit import Circuit
from bqskit.ir.gates.constant.cx import CNOTGate
from bqskit.ir.gates.constant.x import XGate
from bqskit.ir.gates.measure import MeasurementPlaceholder


class ConvertToPKAC(BasePass):
'''
Pass that converts all FractionalRZGates to a circuit with a QFT
and GidneyAdders.

The new circuit will have 3k-1 more qubits than the original circuit.
Of these, 2k will be permanent data qubits, and k-1 will be ancilla
qubits that disappear after each adder. I'm not sure how to notate that
right now.
'''

def __init__(self, k: int = 3, add_measurements: bool = True) -> None:
'''

Args:
k (int): Will be the register size for the Adder used to perform
RZ gates. Can perform rotation multiples of 2*pi/(2 ** k). If k=3,
can perform T gates.

add_measurements (bool): Whether or not to add Measurements and
Resets to the circuit. This is useful for mappers, but not for
unitary-based subroutines.
'''
self.k = k
self.add_measurements = add_measurements

def calculate_cnot_circuit(self, numerator: int) -> Circuit:
'''
Calculate the circuit of CNOTs needed to apply the appropriate RZ
rotation. Starting with LSB of target, applying a CNOT applies an
RZ of 2*pi / 2^k on the control qubit. LSB + 1 applies an RZ of
2*pi / 2^(k-1), and so forth. If you want to apply RZ(-2*pi / 2^k),
you can apply an X on the LSB and then a CNOT. We make the circuit
to minimize the number of CNOTs.

Args:
numerator (int): The numerator of the angle to apply, where the
denominator is 2^k. For example, if k = 3 and you want to apply
RZ(pi/4), the numerator would be 1, since pi/4 = 2*pi / 2^3.

'''
circ = Circuit(self.k + 1)
# Let 0 be the control and 1...k be the target register for the adder

# Run NAF algorithm to find the optimal bitstring
bitstring = []
n = numerator
while n > 0:
if n % 2 == 0:
bitstring.append(0)
n = n // 2
else:
r = 2 - (n % 4)
bitstring.append(2 - (n % 4))
n = (n - r) // 2

# Now bitstring[i] tells us whether we need to apply a CNOT with target
for i, bit in enumerate(bitstring):
# LSB is the last bit of register
target_ind = self.k - i
if bit == 1:
circ.append_gate(CNOTGate(), [0, target_ind])
elif bit == -1:
circ.append_gate(XGate(), [target_ind])
circ.append_gate(CNOTGate(), [0, target_ind])

return circ

async def run(self, circuit: Circuit, data: PassData) -> None:
new_circ = Circuit(circuit.num_qudits + 3 * self.k - 1)

n = circuit.num_qudits

input_a_qubits = list(range(n, n + self.k))
input_b_qubits = list(range(n + self.k, n + 2 * self.k))
ancilla_qubits = list(range(n + 2 * self.k, n + 3 * self.k - 1))

# Initialize input B in QFT state
new_circ.append_gate(XGate(), [input_b_qubits[-1]])
new_circ.append_circuit(QFTGadget.generate(self.k), input_b_qubits)

# Initialize ancilla qubits with measurements
if self.add_measurements:
for q in ancilla_qubits:
new_circ.append_gate(
MeasurementPlaceholder(
[('a', 1)], {q: ('a', 0)},
), [q],
)

for op in circuit.operations():
if isinstance(op.gate, FractionalRZGate):
# Replace with adder circuit
q = op.location[0]
# Add a CNOT to LSB on input A
cnots = self.calculate_cnot_circuit(op.gate.numerator)
new_circ.append_circuit(cnots, [q] + input_a_qubits)

# Apply adder on A, B, and ancilla
new_circ.append_gate(
GidneyAdder(self.k, add_reset=self.add_measurements),
(
input_a_qubits + input_b_qubits
+ ancilla_qubits
),
)

new_circ.append_circuit(cnots, [q] + input_a_qubits)
else:
new_circ.append(op)

circuit.become(new_circ)
56 changes: 56 additions & 0 deletions bqskit/ft/ftpasses/pkac_to_gates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from __future__ import annotations

from numpy import random

from bqskit.compiler.basepass import BasePass
from bqskit.compiler.passdata import PassData
from bqskit.ft.gates.logical_and import LogicalAndDgGate
from bqskit.ir.circuit import Circuit
from bqskit.ir.gates import CircuitGate
from bqskit.ir.gates.constant.cz import CZGate
from bqskit.ir.gates.constant.h import HGate
from bqskit.ir.gates.measure import MeasurementPlaceholder
from bqskit.ir.operation import Operation
from bqskit.ir.point import CircuitPoint


class PKACtoGatesPass(BasePass):
'''
Pass that converts all GidneyAdders, QFTs, and LogicalAnds to gates
placeable in tilers. Importantly, this pass does *not* keep the unitary
of the circuit the same, since we replace LogicalAndInverses with an
H gate and control Z (which is applied 50% of the time).
This is because we want to be able to test the PKAC mapping.
'''
async def run(self, circuit: Circuit, data: PassData) -> None:
# First step, unfold all the gadgets in the circuit
# This will unfold all the GidneyAdders
circuit.unfold_all()

base_log_and_circ = Circuit(3)
base_log_and_circ.append_gate(HGate(), [2])
base_log_and_circ.append_gate(
MeasurementPlaceholder([('a', 1)], {2: ('a', 0)}), [2],
)

# Now, we should replace all LogicalAndDgs with the measure and fixup
pts = []
new_circuit_gates = []
for cycle, op in circuit.operations_with_cycles():
if isinstance(op.gate, LogicalAndDgGate):
# Replace with H, measurement and control Z
new_circ = base_log_and_circ.copy()
# Apply a CZ gate with 50% probability
if random.rand() < 0.5:
new_circ.append_gate(CZGate(), [0, 1])

pt = CircuitPoint(cycle, op.location[0])
new_circ_gate = CircuitGate(new_circ)
new_circ_op = Operation(new_circ_gate, op.location)
pts.append(pt)
new_circuit_gates.append(new_circ_op)

circuit.batch_replace(pts, new_circuit_gates)

# Unfold all LogicalAndDgs
circuit.unfold_all()
86 changes: 86 additions & 0 deletions bqskit/ft/gadgets/qft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Generate a quantum Fourier transform that can be applied to some qubits."""
from __future__ import annotations

from numpy import pi

from bqskit.ir.circuit import Circuit
from bqskit.ir.gates import CNOTGate
from bqskit.ir.gates import HGate
from bqskit.ir.gates import RZGate
from bqskit.ir.gates import SwapGate
from bqskit.ir.gates import TdgGate
from bqskit.ir.gates import TGate


class QFTGadget:

@staticmethod
def controlled_rz_layer(width: int) -> Circuit:
"""
A layer of controlled RZ gates decomposed in to CNOTs and RZs
"""
assert width >= 2, 'Width must be at least 2 for a controlled RZ layer.'
circuit = Circuit(width)
for i in range(1, width):
base = 2 ** (i + 1)
if base == 4:
gate_0, gate_i = TdgGate(), TGate()
params_0, params_i = [], []
else:
gate_0, gate_i = RZGate(), RZGate()
params_0, params_i = [-pi / base], [pi / base]
circuit.append_gate(CNOTGate(), [i, 0])
circuit.append_gate(gate_0, [0], params_0)
circuit.append_gate(CNOTGate(), [i, 0])
circuit.append_gate(gate_i, [i], params_i)
circuit.append_gate(gate_i, [0], params_i)
return circuit

@staticmethod
def generate(num_qudits: int) -> Circuit:
"""
Generate a QFT that can be applied to qubits.

Args:
num_qudits (int): The number of qubits the QFT should be applied to.

Returns:
(CircuitGate): A QFT block that can be applied to qubits.
"""
circuit = Circuit(num_qudits)
for i in range(num_qudits - 1):
circuit.append_gate(HGate(), [i])
width = num_qudits - i
subcirc = QFTGadget.controlled_rz_layer(width)
support = list(range(i, num_qudits))
circuit.append_circuit(subcirc, support)
circuit.append_gate(HGate(), [num_qudits - 1])

# Swap qubits at the end
for i in range(num_qudits // 2):
circuit.append_gate(SwapGate(), [i, num_qudits - i - 1])

return circuit


if __name__ == '__main__':
import numpy as np

num_qudits = 8
circuit = Circuit(num_qudits)
qft = QFTGadget.generate(num_qudits)
circuit.append_circuit(qft, [_ for _ in range(num_qudits)])
circuit.unfold_all()
for op in circuit:
print(op, op.params)

def qft_u(n):
# this is the qft unitary generator code from qsearch
root = np.e ** (2j * np.pi / n)
return np.fromfunction(lambda x, y: root**(x * y), (n, n)) / np.sqrt(n)

print('=' * 80)
one = np.zeros((2 ** num_qudits, 2 ** num_qudits), dtype=complex)
u_qft = qft_u(2 ** num_qudits)
dist = circuit.get_unitary().get_distance_from(u_qft)
print(dist)
44 changes: 44 additions & 0 deletions bqskit/ft/gates/fractional_rz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""This module implements the FractionalRZGate."""
from __future__ import annotations

import numpy as np

from bqskit.ir.gates.constantgate import ConstantGate
from bqskit.ir.gates.qubitgate import QubitGate
from bqskit.qis.unitary.unitary import RealVector
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix


class FractionalRZGate(ConstantGate, QubitGate):
"""
A gate representing an arbitrary rotation around the Z axis.

Takes 2 parameters, a numerator and k and implements the
gate RZ(2pi * numerator / 2 ** k). This is useful for implementing
the gates that can be implemented with PKAC.
"""

_num_qudits = 1
_num_params = 0
_qasm_name = 'fractional_rz'

def __init__(self, numerator: int = 0, k: int = 0) -> None:
self.numerator = numerator
self.k = k
assert int(numerator) == numerator, 'Numerator must be an integer'
assert int(k) == k, 'k must be an integer'
assert numerator < 2 ** k, 'Numerator must be less than 2^k'

def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
"""Return the unitary for this gate, see :class:`Unitary` for more."""
angle = 2 * np.pi * self.numerator / (2 ** self.k)

pexp = np.exp(1j * angle / 2)
nexp = np.exp(-1j * angle / 2)

return UnitaryMatrix(
[
[nexp, 0],
[0, pexp],
],
)
Loading