def test_four_modes(hbar): """Test that probabilities are correctly updates for a four modes system under loss""" # All this block is to generate the correct covariance matrix. # It correnponds to num_modes=4 modes that undergo two mode squeezing between modes i and i + (num_modes / 2). # Then they undergo displacement. # The signal and idlers see and interferometer with unitary matrix u2x2. # And then they see loss by amount etas[i]. num_modes = 4 theta = 0.45 phi = 0.7 u2x2 = np.array([ [np.cos(theta / 2), np.exp(1j * phi) * np.sin(theta / 2)], [-np.exp(-1j * phi) * np.sin(theta / 2), np.cos(theta / 2)], ]) u4x4 = block_diag(u2x2, u2x2) cov = np.identity(2 * num_modes) * hbar / 2 means = 0.5 * np.random.rand(2 * num_modes) * np.sqrt(hbar / 2) rs = [0.1, 0.9] n_half = num_modes // 2 for i, r_val in enumerate(rs): Sexpanded = expand(two_mode_squeezing(r_val, 0.0), [i, n_half + i], num_modes) cov = Sexpanded @ cov @ (Sexpanded.T) Su = expand(interferometer(u4x4), range(num_modes), num_modes) cov = Su @ cov @ (Su.T) cov_lossless = np.copy(cov) means_lossless = np.copy(means) etas = [0.9, 0.7, 0.9, 0.1] for i, eta in enumerate(etas): means, cov = loss(means, cov, eta, i, hbar=hbar) cutoff = 3 probs_lossless = probabilities(means_lossless, cov_lossless, 4 * cutoff, hbar=hbar) probs = probabilities(means, cov, cutoff, hbar=hbar) probs_updated = update_probabilities_with_loss(etas, probs_lossless) assert np.allclose(probs, probs_updated[:cutoff, :cutoff, :cutoff, :cutoff], atol=1e-6)
def test_displaced_loss_against_interferometer(self, hbar, tol): """Test that the loss channel on a displaced state corresponds to a beamsplitter acting on the mode with loss and an ancilla vacuum state""" T = 0.812 alpha = np.random.random(size=[2]) + np.random.random(size=[2]) * 1j mu = np.concatenate([alpha.real, alpha.imag]) # perform loss mu_res, _ = symplectic.loss(mu, np.identity(4), T, mode=0, hbar=hbar) # create a two mode beamsplitter acting on modes 0 and 2 B = np.array([[np.sqrt(T), -np.sqrt(1 - T), 0, 0], [np.sqrt(1 - T), np.sqrt(T), 0, 0], [0, 0, np.sqrt(T), -np.sqrt(1 - T)], [0, 0, np.sqrt(1 - T), np.sqrt(T)]]) B = symplectic.expand(B, modes=[0, 2], N=3) # apply the beamsplitter to modes 0 and 2 mu_expand = np.zeros([6]) mu_expand[np.array([0, 1, 3, 4])] = mu mu_expected, _ = symplectic.reduced_state(B @ mu_expand, np.identity(6), modes=[0, 1]) # compare loss function result to an interferometer mixing mode 0 with the vacuum assert np.allclose(mu_expected, mu_res, atol=tol, rtol=0)
def test_expand_two(self, m1, m2, tol): """Test expanding a two mode gate""" r = 0.1 phi = 0.423 N = 4 S = symplectic.two_mode_squeezing(r, phi) res = symplectic.expand(S, modes=[m1, m2], N=N) expected = np.identity(2 * N) # mode1 terms expected[m1, m1] = S[0, 0] expected[m1, m1 + N] = S[0, 2] expected[m1 + N, m1] = S[2, 0] expected[m1 + N, m1 + N] = S[2, 2] # mode2 terms expected[m2, m2] = S[1, 1] expected[m2, m2 + N] = S[1, 3] expected[m2 + N, m2] = S[3, 1] expected[m2 + N, m2 + N] = S[3, 3] # cross terms expected[m1, m2] = S[0, 1] expected[m1, m2 + N] = S[0, 3] expected[m1 + N, m2] = S[2, 1] expected[m1 + N, m2 + N] = S[2, 3] expected[m2, m1] = S[1, 0] expected[m2, m1 + N] = S[3, 0] expected[m2 + N, m1] = S[1, 2] expected[m2 + N, m1 + N] = S[3, 2] assert np.allclose(res, expected, atol=tol, rtol=0)
def expandS(self, modes, S): """Expands symplectic matrix on subset of modes to symplectic matrix for the whole system. Args: modes (list): list of modes on which S acts S (array): symplectic matrix """ return symp.expand(S, modes, self.nlen)
def expandXY(self, modes, X, Y): """Expands deterministic Gaussian CPTP matrices ``(X,Y)`` on subset of modes to transformations for the whole system. Args: modes (list): list of modes on which ``(X,Y)`` act X (array): matrix for mutltiplicative part of transformation Y (array): matrix for additive part of transformation """ X2 = symp.expand(X, modes, self.nlen) Y2 = symp.expand(Y, modes, self.nlen) for i in range(self.nlen): if i not in modes: Y2[i, i] = 0 Y2[i + self.nlen, i + self.nlen] = 0 return X2, Y2
def test_symplectic(self, tol): """Test that the two mode squeeze operator is symplectic""" r = 0.543 phi = 0.123 S = symplectic.expand(symplectic.two_mode_squeezing(r, phi), modes=[0, 2], N=4) # the symplectic matrix O = np.block([[np.zeros([4, 4]), np.identity(4)], [-np.identity(4), np.zeros([4, 4])]]) assert np.allclose(S @ O @ S.T, O, atol=tol, rtol=0)
def test_two_mode_gate_expand(M, tol): """test _apply_symp_two_mode_gate applies correctly""" S = np.random.random((2 * M, 2 * M)) r = np.random.random(2 * M) S_G = interferometer(0.5**0.5 * np.fft.fft(np.eye(2))) S1, r1 = _apply_symp_two_mode_gate(S_G, S.copy(), r.copy(), 1, 3) S_G_expand = expand(S_G, [1, 3], M) S2 = S_G_expand @ S r2 = S_G_expand @ r assert np.allclose(S1, S2, atol=tol, rtol=0) assert np.allclose(r1, r2, atol=tol, rtol=0)
def test_one_mode_gate_expand(M, tol): """test _apply_symp_one_mode_gate applies correctly on a larger matrices""" S = np.random.random((2 * M, 2 * M)) r = np.random.random(2 * M) S_G = interferometer(np.exp(1j * 0.3)) S1, r1 = _apply_symp_one_mode_gate(S_G, S.copy(), r.copy(), 1) S_G_expand = expand(S_G, [1], M) S2 = S_G_expand @ S r2 = S_G_expand @ r assert np.allclose(S1, S2, atol=tol, rtol=0) assert np.allclose(r1, r2, atol=tol, rtol=0)
def phase_shift(self, phi, k): r"""Implement a phase shift in mode k. Args: phi (float): phase k (int): mode to be phase shifted Raises: ValueError: if the mode is not in the list of active modes """ if self.active[k] is None: raise ValueError("Cannot phase shift mode, mode does not exist") rot = symp.expand(symp.rotation(phi), k, self.nlen) self.means = update_means(self.means, rot, self.from_xp) self.covs = update_covs(self.covs, rot, self.from_xp)
def squeeze(self, r, phi, k): r"""Squeeze mode ``k`` by the amount ``r*exp(1j*phi)``. Args: r (float): squeezing magnitude phi (float): squeezing phase k (int): mode to be squeezed Raises: ValueError: if the mode is not in the list of active modes """ if self.active[k] is None: raise ValueError("Cannot squeeze mode, mode does not exist") sq = symp.expand(symp.squeezing(r, phi), k, self.nlen) self.means = update_means(self.means, sq, self.from_xp) self.covs = update_covs(self.covs, sq, self.from_xp)
def test_expand_one(self, mode, tol): """Test expanding a one mode gate""" r = 0.1 phi = 0.423 N = 3 S = np.array([ [np.cosh(r) - np.cos(phi) * np.sinh(r), -np.sin(phi) * np.sinh(r)], [-np.sin(phi) * np.sinh(r), np.cosh(r) + np.cos(phi) * np.sinh(r)], ]) res = symplectic.expand(S, modes=mode, N=N) expected = np.identity(2 * N) expected[mode, mode] = S[0, 0] expected[mode, mode + N] = S[0, 1] expected[mode + N, mode] = S[1, 0] expected[mode + N, mode + N] = S[1, 1] assert np.allclose(res, expected, atol=tol, rtol=0)
def test_TMS_against_interferometer(self, hbar, tol): """Test that the loss channel on a TMS state corresponds to a beamsplitter acting on the mode with loss and an ancilla vacuum state""" r = 0.543 phi = 0.432 T = 0.812 S = symplectic.two_mode_squeezing(r, phi) cov = S @ S.T * (hbar / 2) # perform loss _, cov_res = symplectic.loss(np.zeros([4]), cov, T, mode=0, hbar=hbar) # create a two mode beamsplitter acting on modes 0 and 2 B = np.array([ [np.sqrt(T), -np.sqrt(1 - T), 0, 0], [np.sqrt(1 - T), np.sqrt(T), 0, 0], [0, 0, np.sqrt(T), -np.sqrt(1 - T)], [0, 0, np.sqrt(1 - T), np.sqrt(T)], ]) B = symplectic.expand(B, modes=[0, 2], N=3) # add an ancilla vacuum state in mode 2 cov_expand = np.identity(6) * hbar / 2 cov_expand[:2, :2] = cov[:2, :2] cov_expand[3:5, :2] = cov[2:, :2] cov_expand[:2, 3:5] = cov[:2, 2:] cov_expand[3:5, 3:5] = cov[2:, 2:] # apply the beamsplitter to modes 0 and 2 cov_expand = B @ cov_expand @ B.T # compare loss function result to an interferometer mixing mode 0 with the vacuum _, cov_expected = symplectic.reduced_state(np.zeros([6]), cov_expand, modes=[0, 1]) assert np.allclose(cov_expected, cov_res, atol=tol, rtol=0)
def beamsplitter(self, theta, phi, k, l): r"""Implement a beam splitter operation between modes k and l. Args: theta (float): real beamsplitter angle phi (float): complex beamsplitter angle k (int): first mode l (int): second mode Raises: ValueError: if any of the two modes is not in the list of active modes ValueError: if the first mode equals the second mode """ if self.active[k] is None or self.active[l] is None: raise ValueError( "Cannot perform beamsplitter, mode(s) do not exist") if k == l: raise ValueError( "Cannot use the same mode for beamsplitter inputs.") bs = symp.expand(symp.beam_splitter(theta, phi), [k, l], self.nlen) self.means = update_means(self.means, bs, self.from_xp) self.covs = update_covs(self.covs, bs, self.from_xp)
def test_inverse_ops_cancel(self, hbar, tol): """Test that applying squeezing and interferometers to a four mode circuit, followed by applying the inverse operations, return the state to the vacuum""" # the symplectic matrix O = np.block([[np.zeros([4, 4]), np.identity(4)], [-np.identity(4), np.zeros([4, 4])]]) # begin in the vacuum state mu_init, cov_init = symplectic.vacuum_state(4, hbar=hbar) # add displacement alpha = np.random.random(size=[4]) + np.random.random(size=[4]) * 1j D = np.concatenate([alpha.real, alpha.imag]) mu = mu_init + D cov = cov_init.copy() # random squeezing r = np.random.random() phi = np.random.random() S = symplectic.expand(symplectic.two_mode_squeezing(r, phi), modes=[0, 1], N=4) # check symplectic assert np.allclose(S @ O @ S.T, O, atol=tol, rtol=0) # random interferometer # fmt:off u = np.array([[ -0.06658906 - 0.36413058j, 0.07229868 + 0.65935896j, 0.59094625 - 0.17369183j, -0.18254686 - 0.10140904j ], [ 0.53854866 + 0.36529723j, 0.61152793 + 0.15022026j, 0.05073631 + 0.32624882j, -0.17482023 - 0.20103772j ], [ 0.34818923 + 0.51864844j, -0.24334624 + 0.0233729j, 0.3625974 - 0.4034224j, 0.10989667 + 0.49366039j ], [ 0.16548085 + 0.14792642j, -0.3012549 - 0.11387682j, -0.12731847 - 0.44851389j, -0.55816075 - 0.5639976j ]]) # fmt on U = symplectic.interferometer(u) # check unitary assert np.allclose(u @ u.conj().T, np.identity(4), atol=tol, rtol=0) # check symplectic assert np.allclose(U @ O @ U.T, O, atol=tol, rtol=0) # apply squeezing and interferometer cov = U @ S @ cov @ S.T @ U.T mu = U @ S @ mu # check we are no longer in the vacuum state assert not np.allclose(mu, mu_init, atol=tol, rtol=0) assert not np.allclose(cov, cov_init, atol=tol, rtol=0) # return the inverse operations Sinv = symplectic.expand(symplectic.two_mode_squeezing(-r, phi), modes=[0, 1], N=4) Uinv = symplectic.interferometer(u.conj().T) # check inverses assert np.allclose(Uinv, np.linalg.inv(U), atol=tol, rtol=0) assert np.allclose(Sinv, np.linalg.inv(S), atol=tol, rtol=0) # apply the inverse operations cov = Sinv @ Uinv @ cov @ Uinv.T @ Sinv.T mu = Sinv @ Uinv @ mu # inverse displacement mu -= D # check that we return to the vacuum state assert np.allclose(mu, mu_init, atol=tol, rtol=0) assert np.allclose(cov, cov_init, atol=tol, rtol=0)
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 fock_tensor(S, alpha, cutoff, choi_r=np.arcsinh(1.0), check_symplectic=True, sf_order=False): r""" Calculates the Fock representation of a Gaussian unitary parametrized by the symplectic matrix S and the displacements alpha up to cutoff in Fock space. Args: S (array): symplectic matrix alpha (array): complex vector of displacements cutoff (int): cutoff in Fock space choi_r (float): squeezing parameter used for the Choi expansion check_symplectic (boolean): checks whether the input matrix is symplectic sf_order (boolean): reshapes the tensor so that it follows the sf ordering of indices Return: (array): Tensor containing the Fock representation of the Gaussian unitary """ # Check the matrix is symplectic if check_symplectic: if not is_symplectic(S): raise ValueError("The matrix S is not symplectic") # And that S and alpha have compatible dimensions m, _ = S.shape if m // 2 != len(alpha): raise ValueError( "The matrix S and the vector alpha do not have compatible dimensions" ) # Construct the covariance matrix of l two-mode squeezed vacua pairing modes i and i+l l = m // 2 ch = np.cosh(choi_r) * np.identity(l) sh = np.sinh(choi_r) * np.identity(l) zh = np.zeros([l, l]) Schoi = np.block([[ch, sh, zh, zh], [sh, ch, zh, zh], [zh, zh, ch, -sh], [zh, zh, -sh, ch]]) # And then its Choi expanded symplectic S_exp = expand(S, list(range(l)), 2 * l) @ Schoi # And this is the corresponding covariance matrix cov = S_exp @ S_exp.T alphat = np.array(list(alpha) + ([0] * l)) x = 2 * alphat.real p = 2 * alphat.imag mu = np.concatenate([x, p]) tensor = state_vector(mu, cov, normalize=False, cutoff=cutoff, hbar=2, check_purity=False, choi_r=choi_r) if sf_order: sf_indexing = tuple(chain.from_iterable([[i, i + l] for i in range(l)])) return tensor.transpose(sf_indexing) return tensor
def test_no_unitary(self, tol): """Test compilation works with no unitary provided""" prog = sf.Program(8) with prog.context as q: ops.S2gate(SQ_AMPLITUDE) | (q[0], q[4]) ops.S2gate(SQ_AMPLITUDE) | (q[1], q[5]) ops.S2gate(SQ_AMPLITUDE) | (q[2], q[6]) ops.S2gate(SQ_AMPLITUDE) | (q[3], q[7]) ops.MeasureFock() | q res = prog.compile("Xunitary") expected = sf.Program(8) with expected.context as q: ops.S2gate(SQ_AMPLITUDE, 0) | (q[0], q[4]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[1], q[5]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[2], q[6]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[3], q[7]) # corresponds to an identity on modes [0, 1, 2, 3] # This can be easily seen from below by noting that: # MZ(pi, pi) = R(0) = I # MZ(pi, 0) @ MZ(pi, 0) = I # [R(pi) \otimes I] @ MZ(pi, 0) = I ops.MZgate(np.pi, 0) | (q[0], q[1]) ops.MZgate(np.pi, 0) | (q[2], q[3]) ops.MZgate(np.pi, np.pi) | (q[1], q[2]) ops.MZgate(np.pi, np.pi) | (q[0], q[1]) ops.MZgate(np.pi, 0) | (q[2], q[3]) ops.MZgate(np.pi, np.pi) | (q[1], q[2]) ops.Rgate(np.pi) | (q[0]) ops.Rgate(0) | (q[1]) ops.Rgate(0) | (q[2]) ops.Rgate(0) | (q[3]) # corresponds to an identity on modes [4, 5, 6, 7] ops.MZgate(np.pi, 0) | (q[4], q[5]) ops.MZgate(np.pi, 0) | (q[6], q[7]) ops.MZgate(np.pi, np.pi) | (q[5], q[6]) ops.MZgate(np.pi, np.pi) | (q[4], q[5]) ops.MZgate(np.pi, 0) | (q[6], q[7]) ops.MZgate(np.pi, np.pi) | (q[5], q[6]) ops.Rgate(np.pi) | (q[4]) ops.Rgate(0) | (q[5]) ops.Rgate(0) | (q[6]) ops.Rgate(0) | (q[7]) ops.MeasureFock() | q assert program_equivalence(res, expected, atol=tol, compare_params=False) # double check that the applied symplectic is correct # remove the Fock measurements res.circuit = res.circuit[:-1] # extract the Gaussian symplectic matrix O = res.compile("gaussian_unitary").circuit[0].op.p[0] # construct the expected symplectic matrix corresponding # to just the initial two mode squeeze gates S = two_mode_squeezing(SQ_AMPLITUDE, 0) num_modes = 8 expected = np.identity(2 * num_modes) for i in range(num_modes // 2): expected = expand(S, [i, i + num_modes // 2], num_modes) @ expected # Note that the comparison has to be made at the level of covariance matrices # Not at the level of symplectic matrices assert np.allclose(O @ O.T, expected @ expected.T, atol=tol)
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): # 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