def add_loss(self, program, device): """Adds realistic Borealis loss to circuit.""" eta_glob = device.certificate["common_efficiency"] etas_loop = device.certificate["loop_efficiencies"] etas_ch_rel = device.certificate["relative_channel_efficiencies"] prog_length = len(program.tdm_params[0]) reps = int(np.ceil(prog_length / 16)) etas_ch_rel = np.tile(etas_ch_rel, reps)[:prog_length] program.tdm_params.append(etas_ch_rel) program.locked = False seq = [] loop = 0 for i, s in enumerate(program.circuit): # apply loss before MeasureFock if isinstance(s.op, MeasureFock): program.loop_vars.append(program.params(f"p{len(program.tdm_params) - 1}")) seq.append(Command(LossChannel(program.loop_vars[-1]), program.circuit[i].reg)) seq.append(s) # apply loss after Sgate and BSgate if isinstance(s.op, Sgate): seq.append(Command(LossChannel(eta_glob), program.circuit[i].reg)) if isinstance(s.op, BSgate): seq.append(Command(LossChannel(etas_loop[loop]), program.circuit[i].reg[1])) loop += 1 program.circuit = seq program.lock()
def decompose(self, reg): """Return the decomposed commands""" cmds = [] cmds += [Command(GaussianTransform(self.S), reg)] if self.disp: cmds += [ Command(Xgate(x), reg) for x in self.d[:self.ns] if x != 0. ] cmds += [ Command(Zgate(z), reg) for z in self.d[self.ns:] if z != 0. ] return cmds
def _program_in_CJ_rep(prog, cutoff_dim: int): """Convert a Program object to Choi-Jamiolkowski representation. Doubles the number of modes of a Program object and prepends to its circuit the preparation of the maximally entangled ket state. The core idea is that when we apply any quantum channel (e.g. a unitary gate) to the density matrix of the maximally entangled state, we obtain the Choi matrix of the channel as the result. If the channel is unitary, applying it on the maximally entangled ket yields the corresponding unitary matrix, reshaped. Args: prog (Program): quantum program cutoff_dim (int): the Fock basis truncation Returns: Program: modified program """ prog = copy.deepcopy(prog) prog.locked = False # unlock the copy so we can modify it N = prog.init_num_subsystems prog._add_subsystems(N) # pylint: disable=protected-access prog.init_num_subsystems = 2 * N I = _interleaved_identities(N, cutoff_dim) # prepend the circuit with the I ket preparation prog.circuit.insert(0, Command(Ket(I), list(prog.reg_refs.values()))) return prog
def decompose(self, reg): # make BS gate theta = self.layer['BS'][0] phi = self.layer['BS'][1] BS = BSgate(theta, phi) # make cross-Kerr gate CK = None param = self.layer.get('CK', [0])[0] if param != 0: CK = CKgate(param) # make Kerr gate K = None param = self.layer.get('K', [0])[0] if param != 0: K = Kgate(param) # make rotation gate R = None param = self.layer.get('R', [0])[0] if param != 0: R = Rgate(param) cmds = [] for i in range(self.num_layers): #pylint: disable=unused-variable for q0, q1 in self.layer['BS'][2]: cmds.append(Command(BS, (reg[q0], reg[q1]))) if CK is not None: for q0, q1 in self.layer['CK'][1]: cmds.append(Command(CK, (reg[q0], reg[q1]))) if K is not None: for mode in self.layer['K'][1]: cmds.append(Command(K, reg[mode])) if R is not None: for mode in self.layer['R'][1]: cmds.append(Command(R, reg[mode])) return cmds
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 the canonical Symplectic form. This method checks whether the circuit can be implemented as a sequence of Gaussian operations. If the answer is yes it arranges them in the canonical order with displacement at the end. 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 a Gaussian unitary """ # Check which modes are actually being used used_modes = [] for operations in seq: modes = [modes_label.ind for modes_label in operations.reg] used_modes.append(modes) # pylint: disable=consider-using-set-comprehension used_modes = list( set([item for sublist in used_modes for item in sublist])) # dictionary mapping the used modes to consecutive non-negative integers dict_indices = {used_modes[i]: i for i in range(len(used_modes))} nmodes = len(used_modes) # This is the identity transformation in phase-space, multiply by the identity and add zero Snet = np.identity(2 * nmodes) rnet = np.zeros(2 * nmodes) # Now we will go through each operation in the sequence `seq` and apply it in quadrature space # We will keep track of the net transforation in the Symplectic matrix `Snet` and the quadrature # vector `rnet`. for operations in seq: name = operations.op.__class__.__name__ params = par_evaluate(operations.op.p) modes = [modes_label.ind for modes_label in operations.reg] if name == "Dgate": rnet = rnet + expand_vector( params[0] * (np.exp(1j * params[1])), dict_indices[modes[0]], nmodes) else: if name == "Rgate": S = expand(rotation(params[0]), dict_indices[modes[0]], nmodes) elif name == "Sgate": S = expand(squeezing(params[0], params[1]), dict_indices[modes[0]], nmodes) elif name == "S2gate": S = expand( two_mode_squeezing(params[0], params[1]), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) elif name == "Interferometer": S = expand(interferometer(params[0]), [dict_indices[mode] for mode in modes], nmodes) elif name == "GaussianTransform": S = expand(params[0], [dict_indices[mode] for mode in modes], nmodes) elif name == "BSgate": S = expand( beam_splitter(params[0], params[1]), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) elif name == "MZgate": v = np.exp(1j * params[0]) u = np.exp(1j * params[1]) U = 0.5 * np.array([[u * (v - 1), 1j * (1 + v)], [1j * u * (1 + v), 1 - v]]) S = expand( interferometer(U), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) Snet = S @ Snet rnet = S @ rnet # Having obtained the net displacement we simply convert it into complex notation alphas = 0.5 * (rnet[0:nmodes] + 1j * rnet[nmodes:2 * nmodes]) # And now we just pass the net transformation as a big Symplectic operation plus displacements ord_reg = [r for r in list(registers) if r.ind in used_modes] ord_reg = sorted(list(ord_reg), key=lambda x: x.ind) if np.allclose(Snet, np.identity(2 * nmodes)): A = [] else: A = [Command(ops.GaussianTransform(Snet), ord_reg)] B = [ Command(ops.Dgate(np.abs(alphas[i]), np.angle(alphas[i])), ord_reg[i]) for i in range(len(ord_reg)) if not np.allclose(alphas[i], 0.0) ] return A + B
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.") # 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 = GBSSpecs().compile(seq, registers) # ensure that all modes are measured if len(seq[-1].reg) != n_modes: raise CircuitError("All modes must be measured.") # Use the GaussianUnitary compiler to compute the symplectic # matrix representing the Gaussian operations. # Note that the Gaussian unitary compiler does not accept measurements, # so we append the measurement separately. meas_seq = [seq[-1]] seq = GaussianUnitary().compile(seq[:-1], registers) + meas_seq # determine the modes that are acted on by the symplectic transformation used_modes = [x.ind for x in seq[0].reg] # extract the compiled symplectic matrix S = seq[0].op.p[0] 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) half_n_modes = n_modes // 2 # Construct the covariance matrix of the state. # Note that hbar is a global variable that is set by the user cov = (sf.hbar / 2) * S @ S.T # Construct the A matrix A = Amat(cov, hbar=sf.hbar) # Construct the adjacency matrix represented by the A matrix. # This must be an weighted, undirected bipartite graph. That is, # B00 = B11 = 0 (no edges between the two vertex sets 0 and 1), # and B01 == B10.T (undirected edges between the two vertex sets). B = A[:n_modes, :n_modes] B00 = B[:half_n_modes, :half_n_modes] B01 = B[:half_n_modes, half_n_modes:] B10 = B[half_n_modes:, :half_n_modes] B11 = B[half_n_modes:, half_n_modes:] # Perform unitary validation to ensure that the # applied unitary is valid. if not np.allclose(B00, 0) or not np.allclose(B11, 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(B01, B10): # 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 ) ) # Now that the unitary has been validated, perform the Takagi decomposition # to determine the constituent two-mode squeezing and interferometer # parameters. sqs, U = takagi(B01) sqs = np.arctanh(sqs) # ensure provided S2gates all have the allowed squeezing values if not all(s in self.allowed_sq_ranges for s in sqs): wrong_sq_values = [np.round(s, 4) for s in sqs if s not in self.allowed_sq_ranges] raise CircuitError( "Incorrect squeezing value(s) r={}. Allowed squeezing " "value(s) are {}.".format(wrong_sq_values, self.allowed_sq_ranges) ) # Convert the squeezing values into a sequence of S2gate commands sq_seq = [ Command(ops.S2gate(sqs[i]), [registers[i], registers[i + half_n_modes]]) for i in range(half_n_modes) ] # NOTE: at some point, it might make sense to add a keyword argument to this method, # to allow the user to specify if they want the interferometers decomposed or not. # Convert the unitary into a sequence of MZgate and Rgate commands on the signal modes U1 = ops.Interferometer(U, 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 sq_seq + U1 + U2 + meas_seq
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 passive circuit into a single multimode passive operation This method checks whether the circuit can be implemented as a sequence of passive gates. If the answer is yes it arranges them into a single operation. Args: seq (Sequence[Command]): passive quantum circuit to modify registers (Sequence[RegRefs]): quantum registers Returns: List[Command]: compiled circuit Raises: CircuitError: the circuit does not correspond to a passive unitary """ # Check which modes are actually being used used_modes = [] for operations in seq: modes = [modes_label.ind for modes_label in operations.reg] used_modes.append(modes) used_modes = list( set(item for sublist in used_modes for item in sublist)) # dictionary mapping the used modes to consecutive non-negative integers dict_indices = {used_modes[i]: i for i in range(len(used_modes))} nmodes = len(used_modes) # We start with an identity then sequentially update with the gate transformations T = np.identity(nmodes, dtype=np.complex128) # Now we will go through each operation in the sequence `seq` and apply it to T for operations in seq: name = operations.op.__class__.__name__ params = par_evaluate(operations.op.p) modes = [modes_label.ind for modes_label in operations.reg] if name == "Rgate": G = np.exp(1j * params[0]) T = _apply_one_mode_gate(G, T, dict_indices[modes[0]]) elif name == "LossChannel": G = np.sqrt(params[0]) T = _apply_one_mode_gate(G, T, dict_indices[modes[0]]) elif name == "Interferometer": U = params[0] if U.shape == (1, 1): T = _apply_one_mode_gate(U[0, 0], T, dict_indices[modes[0]]) elif U.shape == (2, 2): T = _apply_two_mode_gate(U, T, dict_indices[modes[0]], dict_indices[modes[1]]) else: modes = [dict_indices[mode] for mode in modes] U_expand = np.eye(nmodes, dtype=np.complex128) U_expand[np.ix_(modes, modes)] = U T = U_expand @ T elif name == "PassiveChannel": T0 = params[0] if T0.shape == (1, 1): T = _apply_one_mode_gate(T0[0, 0], T, dict_indices[modes[0]]) elif T0.shape == (2, 2): T = _apply_two_mode_gate(T0, T, dict_indices[modes[0]], dict_indices[modes[1]]) else: modes = [dict_indices[mode] for mode in modes] T0_expand = np.eye(nmodes, dtype=np.complex128) T0_expand[np.ix_(modes, modes)] = T0 T = T0_expand @ T elif name == "BSgate": G = _beam_splitter_passive(params[0], params[1]) T = _apply_two_mode_gate(G, T, dict_indices[modes[0]], dict_indices[modes[1]]) elif name == "MZgate": v = np.exp(1j * params[0]) u = np.exp(1j * params[1]) U = 0.5 * np.array([[u * (v - 1), 1j * (1 + v)], [1j * u * (1 + v), 1 - v]]) T = _apply_two_mode_gate(U, T, dict_indices[modes[0]], dict_indices[modes[1]]) elif name == "sMZgate": exp_sigma = np.exp(1j * (params[0] + params[1]) / 2) delta = (params[0] - params[1]) / 2 U = exp_sigma * np.array([[np.sin(delta), np.cos(delta)], [np.cos(delta), -np.sin(delta)]]) T = _apply_two_mode_gate(U, T, dict_indices[modes[0]], dict_indices[modes[1]]) ord_reg = [r for r in list(registers) if r.ind in used_modes] ord_reg = sorted(list(ord_reg), key=lambda x: x.ind) return [Command(ops.PassiveChannel(T), ord_reg)]
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