def compile(self, seq, registers): """Try to arrange a quantum circuit into a form suitable for Gaussian boson sampling. This method checks whether the circuit can be implemented as a Gaussian boson sampling problem, i.e., if it is equivalent to a circuit A+B, where the sequence A only contains Gaussian operations, and B only contains Fock measurements. If the answer is yes, the circuit is arranged into the A+B order, and all the Fock measurements are combined into a single :class:`MeasureFock` operation. 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 GBS """ A, B, C = group_operations(seq, lambda x: isinstance(x, ops.MeasureFock)) # C should be empty if C: raise CircuitError("Operations following the Fock measurements.") # A should only contain Gaussian operations # (but this is already guaranteed by group_operations() and our primitive set) # without Fock measurements GBS is pointless if not B: raise CircuitError("GBS circuits must contain Fock measurements.") # there should be only Fock measurements in B measured = set() for cmd in B: if not isinstance(cmd.op, ops.MeasureFock): raise CircuitError( "The Fock measurements are not consecutive.") # combine the Fock measurements temp = set(cmd.reg) if measured & temp: raise CircuitError("Measuring the same mode more than once.") measured |= temp # replace B with a single Fock measurement B = [ Command(ops.MeasureFock(), sorted(list(measured), key=lambda x: x.ind)) ] return super().compile(A + B, registers)
def compile(self, seq, registers): """Try to arrange a quantum circuit into a form suitable for Chip0. 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 Chip0 """ # pylint: disable=too-many-statements,too-many-branches # First, check if provided sequence matches the circuit template. # This will avoid superfluous compilation if the user is using the # template directly. try: seq = super().compile(seq, registers) except CircuitError: # failed topology check. Continue to more general # compilation below. pass else: return seq # 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)) if A: raise CircuitError("Circuits must start with two S2gates.") # get circuit registers regrefs = {q for cmd in B for q in cmd.reg} if len(regrefs) != self.modes: raise CircuitError("S2gates do not appear on the correct modes.") # Compile the unitary: combine and then decompose all unitaries # ------------------------------------------------------------- A, B, C = group_operations( seq, lambda x: isinstance(x, (ops.Rgate, ops.BSgate))) # begin unitary lists for mode [0, 1] and modes [2, 3] with # two identity matrices. This is because multi_dot requires # at least two matrices in the list. U_list01 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2 U_list23 = [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 # Note: this is done separately on modes [0, 1] # and modes [2, 3] modes = [i.ind for i in cmd.reg] params = [i.x for i in cmd.op.p] U = np.identity(self.modes // 2, dtype=np.complex128) if isinstance(cmd.op, ops.Rgate): m = modes[0] U[m % 2, m % 2] = np.exp(1j * params[0]) 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 % 2, m % 2] = t U[m % 2, n % 2] = -np.conj(r) U[n % 2, m % 2] = r U[n % 2, n % 2] = t if set(modes).issubset({0, 1}): U_list01.insert(0, U) elif set(modes).issubset({2, 3}): U_list23.insert(0, U) else: raise CircuitError( "Unitary must be applied separately to modes [0, 1] and modes [2, 3]." ) # multiply all unitaries together, to get the final # unitary representation on modes [0, 1] and [2, 3]. U01 = multi_dot(U_list01) U23 = multi_dot(U_list23) # check unitaries are equal if not np.allclose(U01, U23): raise CircuitError( "Interferometer on modes [0, 1] must be identical to interferometer on modes [2, 3]." ) U = block_diag(U01, U23) # replace B with an interferometer B = [ Command(ops.Interferometer(U01), registers[:2]), Command(ops.Interferometer(U23), registers[2:]), ] # 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
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
def compile(self, seq, registers): # the number of modes in the provided program n_modes = len(registers) # Number of modes must be even if n_modes % 2 != 0: raise CircuitError( "The X series only supports programs with an even number of modes." ) half_n_modes = n_modes // 2 # Call the GBS compiler to do basic measurement validation. # The GBS compiler also merges multiple measurement commands # into a single MeasureFock command at the end of the circuit. seq = GBS().compile(seq, registers) # ensure that all modes are measured if len(seq[-1].reg) != n_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)) # If there are no two-mode squeezers add squeezers at the beginning with squeezing param equal to zero. if B == []: initS2 = [ Command(ops.S2gate(0, 0), [registers[i], registers[i + half_n_modes]]) for i in range(half_n_modes) ] seq = initS2 + seq A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate)) if A != []: raise CircuitError( "There can be no operations before the S2gates.") 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, half_n_modes), range(half_n_modes, n_modes))) if not regrefs.issubset(allowed_modes): raise CircuitError("S2gates do not appear on the correct modes.") # determine which modes do not have input S2gates specified missing = allowed_modes - regrefs for i, j in missing: # insert S2gates with 0 squeezing B.insert(0, Command(ops.S2gate(0, 0), [registers[i], registers[j]])) # get list of circuit registers as a tuple for each S2gate regrefs = [(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B] # merge S2gates if len(regrefs) > half_n_modes: for mode, indices in list_duplicates(regrefs): r = 0 phi = 0 for k, i in enumerate(sorted(indices, reverse=True)): removed_cmd = B.pop(i) r += removed_cmd.op.p[0] phi_new = removed_cmd.op.p[1] if k > 0 and phi_new != phi: raise CircuitError( "Cannot merge S2gates with different phase values." ) phi = phi_new i, j = mode B.insert( indices[0], Command(ops.S2gate(r, phi), [registers[i], registers[j]])) meas_seq = [C[-1]] seq = GaussianUnitary().compile(C[:-1], registers) # extract the compiled symplectic matrix if seq == []: S = np.identity(2 * n_modes) used_modes = list(range(n_modes)) else: S = seq[0].op.p[0] # determine the modes that are acted on by the symplectic transformation used_modes = [x.ind for x in seq[0].reg] if not np.allclose(S @ S.T, np.identity(len(S))): raise CircuitError( "The operations after squeezing do not correspond to an interferometer." ) if len(used_modes) != n_modes: # The symplectic transformation acts on a subset of # the programs registers. We must expand the symplectic # matrix to one that acts on all registers. # simply extract the computed symplectic matrix S = expand(seq[0].op.p[0], used_modes, n_modes) U = S[:n_modes, :n_modes] - 1j * S[:n_modes, n_modes:] U11 = U[:half_n_modes, :half_n_modes] U12 = U[:half_n_modes, half_n_modes:] U21 = U[half_n_modes:, :half_n_modes] U22 = U[half_n_modes:, half_n_modes:] if not np.allclose(U12, 0) or not np.allclose(U21, 0): # Not a bipartite graph raise CircuitError( "The applied unitary cannot mix between the modes {}-{} and modes {}-{}." .format(0, half_n_modes - 1, half_n_modes, n_modes - 1)) if not np.allclose(U11, U22): # Not a symmetric bipartite graph raise CircuitError( "The applied unitary on modes {}-{} must be identical to the applied unitary on modes {}-{}." .format(0, half_n_modes - 1, half_n_modes, n_modes - 1)) U1 = ops.Interferometer(U11, mesh="rectangular_symmetric", drop_identity=False)._decompose( registers[:half_n_modes]) U2 = copy.deepcopy(U1) for Ui in U2: Ui.reg = [registers[r.ind + half_n_modes] for r in Ui.reg] return B + U1 + U2 + meas_seq