def test_rectangular_MZ(self, U, tol): """This test checks the function :func:`dec.rectangular_MZ` for various unitary matrices. A given unitary (identity or random draw from Haar measure) is decomposed using the function :func:`dec.rectangular_MZ` and the resulting beamsplitters are multiplied together. Test passes if the product matches the given unitary. """ nmax, mmax = U.shape assert nmax == mmax tilist, diags, tlist = dec.rectangular_MZ(U) qrec = np.identity(nmax) for i in tilist: qrec = dec.mach_zehnder(*i) @ qrec qrec = np.diag(diags) @ qrec for i in reversed(tlist): qrec = dec.mach_zehnder_inv(*i) @ qrec assert np.allclose(U, qrec, atol=tol, rtol=0)
def test_decomposition(self, U, tol): """This test checks the function :func:`dec.rectangular_symmetric` for various unitary matrices. A given unitary (identity or random draw from Haar measure) is decomposed using the function :func:`dec.rectangular_symmetric` and the resulting beamsplitters are multiplied together. Test passes if the product matches the given unitary. """ nmax, mmax = U.shape assert nmax == mmax tlist, diags, _ = dec.rectangular_symmetric(U) qrec = np.identity(nmax) for i in tlist: assert i[2] >= 0 and i[2] < 2 * np.pi # internal phase assert i[3] >= 0 and i[3] < 2 * np.pi # external phase qrec = dec.mach_zehnder(*i) @ qrec qrec = np.diag(diags) @ qrec assert np.allclose(U, qrec, atol=tol, rtol=0)
def compile(self, seq, registers): """Try to arrange a quantum circuit into a form suitable for X8. Args: seq (Sequence[Command]): quantum circuit to modify registers (Sequence[RegRefs]): quantum registers Returns: List[Command]: modified circuit Raises: CircuitError: the circuit does not correspond to X8 """ # pylint: disable=too-many-statements,too-many-branches # first do general GBS compilation to make sure # Fock measurements are correct # --------------------------------------------- seq = GBSSpecs().compile(seq, registers) A, B, C = group_operations(seq, lambda x: isinstance(x, ops.MeasureFock)) if len(B[0].reg) != self.modes: raise CircuitError("All modes must be measured.") # Check circuit begins with two mode squeezers # -------------------------------------------- A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate)) regrefs = set() if B: # get set of circuit registers as a tuple for each S2gate regrefs = {(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B} # the set of allowed mode-tuples the S2gates must have allowed_modes = set(zip(range(0, 4), range(4, 8))) if not regrefs.issubset(allowed_modes): raise CircuitError("S2gates do not appear on the correct modes.") # ensure provided S2gates all have the allowed squeezing values allowed_sq_value = {(0.0, 0.0), (self.sq_amplitude, 0.0)} sq_params = {(float(np.round(cmd.op.p[0], 3)), float(cmd.op.p[1])) for cmd in B} if not sq_params.issubset(allowed_sq_value): wrong_params = sq_params - allowed_sq_value raise CircuitError( "Incorrect squeezing value(s) (r, phi)={}. Allowed squeezing " "value(s) are (r, phi)={}.".format(wrong_params, allowed_sq_value)) # determine which modes do not have input S2gates specified missing = allowed_modes - regrefs for i, j in missing: # insert S2gates with 0 squeezing seq.insert(0, Command(ops.S2gate(0, 0), [registers[i], registers[j]])) # Check if matches the circuit template # -------------------------------------------- # This will avoid superfluous unitary compilation. try: seq = super().compile(seq, registers) except CircuitError: # failed topology check. Continue to more general # compilation below. pass else: return seq # Compile the unitary: combine and then decompose all unitaries # ------------------------------------------------------------- A, B, C = group_operations( seq, lambda x: isinstance(x, (ops.Rgate, ops.BSgate, ops.MZgate))) # begin unitary lists for mode [0, 1, 2, 3] and modes [4, 5, 6, 7] with # two identity matrices. This is because multi_dot requires # at least two matrices in the list. U_list0 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2 U_list4 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2 if not B: # no interferometer was applied A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate)) A = B # move the S2gates to A else: for cmd in B: # calculate the unitary matrix representing each # rotation gate and each beamsplitter modes = [i.ind for i in cmd.reg] params = par_evaluate(cmd.op.p) U = np.identity(self.modes // 2, dtype=np.complex128) if isinstance(cmd.op, ops.Rgate): m = modes[0] U[m % 4, m % 4] = np.exp(1j * params[0]) elif isinstance(cmd.op, ops.MZgate): m, n = modes U = mach_zehnder(m % 4, n % 4, params[0], params[1], self.modes // 2) elif isinstance(cmd.op, ops.BSgate): m, n = modes t = np.cos(params[0]) r = np.exp(1j * params[1]) * np.sin(params[0]) U[m % 4, m % 4] = t U[m % 4, n % 4] = -np.conj(r) U[n % 4, m % 4] = r U[n % 4, n % 4] = t if set(modes).issubset({0, 1, 2, 3}): U_list0.insert(0, U) elif set(modes).issubset({4, 5, 6, 7}): U_list4.insert(0, U) else: raise CircuitError( "Unitary must be applied separately to modes [0, 1, 2, 3] and modes [4, 5, 6, 7]." ) # multiply all unitaries together, to get the final # unitary representation on modes [0, 1] and [2, 3]. U0 = multi_dot(U_list0) U4 = multi_dot(U_list4) # check unitaries are equal if not np.allclose(U0, U4): raise CircuitError( "Interferometer on modes [0, 1, 2, 3] must be identical to interferometer on modes [4, 5, 6, 7]." ) U = block_diag(U0, U4) # replace B with an interferometer B = [ Command(ops.Interferometer(U0), registers[:4]), Command(ops.Interferometer(U4), registers[4:]), ] # decompose the interferometer, using Mach-Zehnder interferometers B = self.decompose(B) # Do a final circuit topology check # --------------------------------- seq = super().compile(A + B + C, registers) return seq