I'm trying to build a new instruction for my circuit. This instruction needs both a controller qubit qctl and an arbitrary register qreg. When qctl is set then the Qiskit's initialize function is applied to qreg.
The original Initialize gate (version 10.5) can be found in the official documentation or locally at path: /anaconda3/envs/<environment name>/lib/python3.7/site-packages/qiskit/extensions/initializer.py. It follows a particular procedure which consists of applying a sequence of RY and RZ gates in order to match the desired state.
The idea is:
- copy the
Initializeinstruction, naming itControlledInitialize; - pass an additional single qubit register
qctltoControlledInitialize; - change all RZ, RY gates with CRZ, CRY gates (the first one is already available, the second one have to be made from scratch).
The problem
It seems that I have passed qctl register in the wrong way, in fact the below minimum example throws the error:
DAGCircuitError: '(qu)bit qctl[0] not found'
Minimum example
import numpy as np
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import BasicAer, execute
# copy here CRY and ControlledInitialize implementation
desired_vector = [ 1 / math.sqrt(2), 0, 0, 1 / math.sqrt(2) ]
qctl = QuantumRegister(1, "qctl")
qreg = QuantumRegister(2, "qreg")
creg = ClassicalRegister(2, "creg")
circuit = QuantumCircuit(qctl, qreg, creg)
circuit.x(qctl)
circuit.controlled_initialize(qctl, desired_vector, qreg)
circuit.measure(qreg, creg)
job = execute(circuit, BasicAer.get_backend('qasm_simulator'), shots=10000)
print('Counts: ', job.result().get_counts(circuit))
The implementation
The whole code can be seen here as .py and here as Jupyter notebook.
CRZ, CRY gates
CRZ is a standard gate, you can use it with
from qiskit.extensions.standard.crz import CrzGate
CRY is present in Aqua module as a function but not as a subclass of Gate class. You can easily derive the gate implementation:
from qiskit.circuit import CompositeGate
from qiskit.circuit import Gate
from qiskit.circuit import QuantumCircuit
from qiskit.circuit import QuantumRegister
from qiskit.circuit.decorators import _op_expand, _to_bits
from qiskit.extensions.standard.u3 import U3Gate
from qiskit.extensions.standard.cx import CnotGate
class CryGate(Gate):
"""controlled-rz gate."""
def __init__(self, theta):
"""Create new cry gate."""
super().__init__("cry", 2, [theta]) # 2 = number of qubits
def _define(self):
"""
self.u3(theta / 2, 0, 0, q_target)
self.cx(q_control, q_target)
self.u3(-theta / 2, 0, 0, q_target)
self.cx(q_control, q_target)
"""
definition = []
q = QuantumRegister(2, "q")
rule = [
(U3Gate(self.params[0] / 2, 0, 0), [q[1]], []),
(CnotGate(), [q[0], q[1]], []),
(U3Gate(-self.params[0] / 2, 0, 0), [q[1]], []),
(CnotGate(), [q[0], q[1]], [])
]
for inst in rule:
definition.append(inst)
self.definition = definition
def inverse(self):
"""Invert this gate."""
return CrzGate(-self.params[0])
@_to_bits(2)
@_op_expand(2)
def cry(self, theta, ctl, tgt):
"""Apply crz from ctl to tgt with angle theta."""
return self.append(CryGate(theta), [ctl, tgt], [])
QuantumCircuit.cry = cry
CompositeGate.cry = cry
ControlledInitialize instruction
Any modification of original Initialize instruction is denoted with WATCH ME comment. Here an overview:
- in
__init__I just save the single qubit control register; - in
_define,gates_to_uncompute,multiplexerthe temporary circuit built will have alsoqctlregister; - in
_define,gates_to_uncompute,multiplexerany append function call is enriched withqctlregister in the list of qubits taken as second parameter; in
gates_to_uncomputejust substituteRYGate/RZGatewithCryGate/CrzGate.class ControlledInitialize(Instruction): """Complex amplitude initialization. Class that implements the (complex amplitude) initialization of some flexible collection of qubit registers (assuming the qubits are in the zero state). """ def __init__(self, controlled_qubit, params): """Create new initialize composite. params (list): vector of complex amplitudes to initialize to """ # WATCH ME: save controlled qubit register self.controlled_qubit = controlled_qubit num_qubits = math.log2(len(params)) # Check if param is a power of 2 if num_qubits == 0 or not num_qubits.is_integer(): raise QiskitError("Desired statevector length not a positive power of 2.") # Check if probabilities (amplitudes squared) sum to 1 if not math.isclose(sum(np.absolute(params) ** 2), 1.0, abs_tol=_EPS): raise QiskitError("Sum of amplitudes-squared does not equal one.") num_qubits = int(num_qubits) super().__init__("controlledinitialize", num_qubits, 0, params) # +1 per il controllo def _define(self): """Calculate a subcircuit that implements this initialization Implements a recursive initialization algorithm, including optimizations, from "Synthesis of Quantum Logic Circuits" Shende, Bullock, Markov https://arxiv.org/abs/quant-ph/0406176v5 Additionally implements some extra optimizations: remove zero rotations and double cnots. """ # call to generate the circuit that takes the desired vector to zero disentangling_circuit = self.gates_to_uncompute() # invert the circuit to create the desired vector from zero (assuming # the qubits are in the zero state) initialize_instr = disentangling_circuit.to_instruction().inverse() q = QuantumRegister(self.num_qubits, 'q') initialize_circuit = QuantumCircuit(self.controlled_qubit, q, name='init_def') for qubit in q: initialize_circuit.append(Reset(), [qubit]) # WATCH ME: cambiati registri temp_qubitsreg = [ self.controlled_qubit[0] ] + q[:] # initialize_circuit.append(initialize_instr, q[:]) initialize_circuit.append(initialize_instr, temp_qubitsreg) self.definition = initialize_circuit.data def gates_to_uncompute(self): """ Call to create a circuit with gates that take the desired vector to zero. Returns: QuantumCircuit: circuit to take self.params vector to |00..0> """ q = QuantumRegister(self.num_qubits) # WATCH ME: aggiunto registro controlled_qubit circuit = QuantumCircuit(self.controlled_qubit, q, name='disentangler') # kick start the peeling loop, and disentangle one-by-one from LSB to MSB remaining_param = self.params for i in range(self.num_qubits): # work out which rotations must be done to disentangle the LSB # qubit (we peel away one qubit at a time) (remaining_param, thetas, phis) = ControlledInitialize._rotations_to_disentangle(remaining_param) # WATCH ME: Initialize._rotations_to_disentangle diventa ControlledInitialize._rotations_to_disentangle # perform the required rotations to decouple the LSB qubit (so that # it can be "factored" out, leaving a shorter amplitude vector to peel away) # WATCH ME: substitute RZ with CRZ # rz_mult = self._multiplex(RZGate, phis) rz_mult = self._multiplex(CrzGate, phis) # WATCH ME: substitute RY with CRY # ry_mult = self._multiplex(RYGate, thetas) ry_mult = self._multiplex(CryGate, thetas) # WATCH ME: cambiati registri temp_qubitsreg = [ self.controlled_qubit[0] ] + q[i:self.num_qubits] # circuit.append(rz_mult.to_instruction(), q[i:self.num_qubits]) # circuit.append(ry_mult.to_instruction(), q[i:self.num_qubits]) circuit.append(rz_mult.to_instruction(), temp_qubitsreg) circuit.append(ry_mult.to_instruction(), temp_qubitsreg) print("Z: ", phis, " | Y: ", thetas) return circuit @staticmethod def _rotations_to_disentangle(local_param): """ Static internal method to work out Ry and Rz rotation angles used to disentangle the LSB qubit. These rotations make up the block diagonal matrix U (i.e. multiplexor) that disentangles the LSB. [[Ry(theta_1).Rz(phi_1) 0 . . 0], [0 Ry(theta_2).Rz(phi_2) . 0], . . 0 0 Ry(theta_2^n).Rz(phi_2^n)]] """ remaining_vector = [] thetas = [] phis = [] param_len = len(local_param) for i in range(param_len // 2): # Ry and Rz rotations to move bloch vector from 0 to "imaginary" # qubit # (imagine a qubit state signified by the amplitudes at index 2*i # and 2*(i+1), corresponding to the select qubits of the # multiplexor being in state |i>) (remains, add_theta, add_phi) = ControlledInitialize._bloch_angles(local_param[2 * i: 2 * (i + 1)]) # WATCH ME: Initialize._bloch_angles diventa ControlledInitialize._bloch_angles remaining_vector.append(remains) # rotations for all imaginary qubits of the full vector # to move from where it is to zero, hence the negative sign thetas.append(-add_theta) phis.append(-add_phi) return remaining_vector, thetas, phis @staticmethod def _bloch_angles(pair_of_complex): """ Static internal method to work out rotation to create the passed in qubit from the zero vector. """ [a_complex, b_complex] = pair_of_complex # Force a and b to be complex, as otherwise numpy.angle might fail. a_complex = complex(a_complex) b_complex = complex(b_complex) mag_a = np.absolute(a_complex) final_r = float(np.sqrt(mag_a ** 2 + np.absolute(b_complex) ** 2)) if final_r < _EPS: theta = 0 phi = 0 final_r = 0 final_t = 0 else: theta = float(2 * np.arccos(mag_a / final_r)) a_arg = np.angle(a_complex) b_arg = np.angle(b_complex) final_t = a_arg + b_arg phi = b_arg - a_arg return final_r * np.exp(1.J * final_t / 2), theta, phi def _multiplex(self, target_gate, list_of_angles): """ Return a recursive implementation of a multiplexor circuit, where each instruction itself has a decomposition based on smaller multiplexors. The LSB is the multiplexor "data" and the other bits are multiplexor "select". Args: target_gate (Gate): Ry or Rz gate to apply to target qubit, multiplexed over all other "select" qubits list_of_angles (list[float]): list of rotation angles to apply Ry and Rz Returns: DAGCircuit: the circuit implementing the multiplexor's action """ list_len = len(list_of_angles) local_num_qubits = int(math.log2(list_len)) + 1 q = QuantumRegister(local_num_qubits) # WATCH ME: aggiunto registro controlled_qubit circuit = QuantumCircuit(self.controlled_qubit, q, name="multiplex" + local_num_qubits.__str__()) lsb = q[0] msb = q[local_num_qubits - 1] # case of no multiplexing: base case for recursion if local_num_qubits == 1: temp_qubitsreg = [ self.controlled_qubit[0], q[0] ] circuit.append(target_gate(list_of_angles[0]), temp_qubitsreg) return circuit # calc angle weights, assuming recursion (that is the lower-level # requested angles have been correctly implemented by recursion angle_weight = scipy.kron([[0.5, 0.5], [0.5, -0.5]], np.identity(2 ** (local_num_qubits - 2))) # calc the combo angles list_of_angles = angle_weight.dot(np.array(list_of_angles)).tolist() # recursive step on half the angles fulfilling the above assumption multiplex_1 = self._multiplex(target_gate, list_of_angles[0:(list_len // 2)]) temp_qubitsreg = [ self.controlled_qubit[0] ] + q[0:-1] circuit.append(multiplex_1.to_instruction(), temp_qubitsreg) # attach CNOT as follows, thereby flipping the LSB qubit circuit.append(CnotGate(), [msb, lsb]) # implement extra efficiency from the paper of cancelling adjacent # CNOTs (by leaving out last CNOT and reversing (NOT inverting) the # second lower-level multiplex) multiplex_2 = self._multiplex(target_gate, list_of_angles[(list_len // 2):]) temp_qubitsreg = [ self.controlled_qubit[0] ] + q[0:-1] if list_len > 1: circuit.append(multiplex_2.to_instruction().mirror(), temp_qubitsreg) else: circuit.append(multiplex_2.to_instruction(), temp_qubitsreg) # attach a final CNOT circuit.append(CnotGate(), [msb, lsb]) return circuit
Qiskit version
Latest version is used:
import qiskit
qiskit.__qiskit_version__
{'qiskit': '0.10.5',
'qiskit-terra': '0.8.2',
'qiskit-ignis': '0.1.1',
'qiskit-aer': '0.2.1',
'qiskit-ibmq-provider': '0.2.2',
'qiskit-aqua': '0.5.2'}