def test_merge(self, hbar, tol): """Test that two symplectics merge: S = S2 @ S1""" n = 3 S1 = random_symplectic(n) S2 = random_symplectic(n) G1 = ops.GaussianTransform(S1) G1inv = ops.GaussianTransform(np.linalg.inv(S1)) G2 = ops.GaussianTransform(S2) # a symplectic merged with its inverse is identity assert G1.merge(G1inv) is None # two merged symplectics are the same as their product assert np.allclose(G1.merge(G2).p[0].x, S2 @ S1, atol=tol, rtol=0)
def test_decomposition_passive(self, tol): """Test that a passive symplectic is correctly decomposed into an interferometer""" n = 3 S = random_symplectic(n, passive=True) X1 = S[:n, :n] P1 = S[n:, :n] U1 = X1 + 1j * P1 prog = sf.Program(n) G = ops.GaussianTransform(S) cmds = G.decompose(prog.register) S = np.identity(2 * n) # command queue should have 1 interferometer assert len(cmds) == 1 # calculating the resulting decomposed symplectic for cmd in cmds: # all operations should be Interferometers assert isinstance(cmd.op, ops.Interferometer) # build up the symplectic transform #modes = [i.ind for i in cmd.reg] if isinstance(cmd.op, ops.Interferometer): U1 = cmd.op.p[0] S_U = np.vstack( [np.hstack([U1.real, -U1.imag]), np.hstack([U1.imag, U1.real])] ) S = S_U @ S # the resulting covariance state cov = S @ S.T assert np.allclose(cov, S @ S.T, atol=tol, rtol=0)
def test_setting_hbar(self, hbar): """Test that an exception is raised if hbar not provided""" prog = sf.Program(3, hbar=hbar) S1 = random_symplectic(3, passive=False) with pytest.raises(ValueError, match="specify the hbar keyword argument"): ops.GaussianTransform(S1) # hbar can be passed as a keyword arg G = ops.GaussianTransform(S1, hbar=hbar) assert G.hbar == hbar # or determined via the engine context with eng: G = ops.GaussianTransform(S1) assert G.hbar == hbar
def test_gaussian_transform(self, setup_eng, hbar, tol): """Test applying a Gaussian symplectic transform""" eng, prog = setup_eng(3) with prog.context as q: ops.GaussianTransform(S) | q state = eng.run(prog).state assert np.allclose(state.cov(), S @ S.T * hbar / 2, atol=tol)
def test_passive(self, tol): """Test that a passive decomposition is correctly flagged as requiring only a single interferometer""" G = ops.GaussianTransform(np.identity(6)) assert not G.active assert hasattr(G, "U1") assert not hasattr(G, "Sq") assert not hasattr(G, "U2")
def test_active_gaussian_transform_on_vacuum(self, setup_eng, hbar, tol): """Test applying a passive Gaussian symplectic transform, which is simply squeezing and ONE interferometer""" eng, prog = setup_eng(3) with prog.context as q: ops.GaussianTransform(S, vacuum=True) | q state = eng.run(prog).state assert np.allclose(state.cov(), S @ S.T * hbar / 2, atol=tol)
def test_active(self, tol): """Test that an active decomposition is correctly flagged as requiring two interferometers and squeezing""" S1 = random_symplectic(3, passive=False) G = ops.GaussianTransform(S1) assert G.active assert hasattr(G, "U1") assert hasattr(G, "Sq") assert hasattr(G, "U2")
def test_decomposition_active(self, hbar, tol): """Test that an active symplectic is correctly decomposed into two interferometers and squeezing""" n = 3 S = random_symplectic(n, passive=False) O1, Sq, O2 = dec.bloch_messiah(S) X1 = O1[:n, :n] P1 = O1[n:, :n] X2 = O2[:n, :n] P2 = O2[n:, :n] U1 = X1 + 1j * P1 U2 = X2 + 1j * P2 prog = sf.Program(n, hbar=hbar) with eng: G = ops.GaussianTransform(S) cmds = G.decompose(q) assert np.all(U1 == G.U1) assert np.all(U2 == G.U2) assert np.all(np.diag(Sq)[:n] == G.Sq) S = np.identity(2 * n) # command queue should have 2 interferometers, 3 squeezers assert len(cmds) == 5 # calculating the resulting decomposed symplectic for cmd in cmds: # all operations should be BSgates, Rgates, or Sgates assert isinstance(cmd.op, (ops.Interferometer, ops.Sgate)) # build up the symplectic transform modes = [i.ind for i in cmd.reg] if isinstance(cmd.op, ops.Sgate): S = _squeezing(cmd.op.p[0].x, cmd.op.p[1].x, modes, n) @ S if isinstance(cmd.op, ops.Interferometer): U1 = cmd.op.p[0].x S_U = np.vstack([ np.hstack([U1.real, -U1.imag]), np.hstack([U1.imag, U1.real]) ]) S = S_U @ S # the resulting covariance state cov = S @ S.T assert np.allclose(cov, S @ S.T * hbar / 2, atol=tol, rtol=0)
def test_symplectic_composition(depth, width): """Tests that symplectic operations are composed correctly""" eng = sf.LocalEngine(backend="gaussian") eng1 = sf.LocalEngine(backend="gaussian") circuit = sf.Program(width) Snet = np.identity(2 * width) with circuit.context as q: for _ in range(depth): S = random_symplectic(width, scale=0.2) Snet = S @ Snet ops.GaussianTransform(S) | q compiled_circuit = circuit.compile(compiler="gaussian_unitary") assert np.allclose(compiled_circuit.circuit[0].op.p[0], Snet)
def test_passive_gaussian_transform(self, setup_eng, tol): """Test applying a passive Gaussian symplectic transform, which is simply an interferometer""" eng, q = setup_eng(3) O = np.vstack( [np.hstack([u1.real, -u1.imag]), np.hstack([u1.imag, u1.real])]) with eng: ops.All(ops.Squeezed(0.5)) | q init = eng.run() ops.GaussianTransform(O) | q state = eng.run() assert np.allclose(state.cov(), O @ init.cov() @ O.T, atol=tol)
def test_active_on_vacuum(self, hbar, tol): """Test that an active symplectic applied to a vacuum is correctly decomposed into just squeezing and one interferometer""" n = 3 S = random_symplectic(n, passive=False) O1, Sq, O2 = dec.bloch_messiah(S) X1 = O1[:n, :n] P1 = O1[n:, :n] X2 = O2[:n, :n] P2 = O2[n:, :n] U1 = X1 + 1j * P1 U2 = X2 + 1j * P2 prog = sf.Program(n) G = ops.GaussianTransform(S, vacuum=True) cmds = G.decompose(prog.register) S = np.identity(2 * n) # command queue should have 3 Sgates, 1 interferometer assert len(cmds) == 4 # calculating the resulting decomposed symplectic for cmd in cmds: # all operations should be Interferometers or Sgates assert isinstance(cmd.op, (ops.Interferometer, ops.Sgate)) # build up the symplectic transform modes = [i.ind for i in cmd.reg] if isinstance(cmd.op, ops.Sgate): S = _squeezing(cmd.op.p[0].x, cmd.op.p[1].x, modes, n) @ S if isinstance(cmd.op, ops.Interferometer): U1 = cmd.op.p[0].x S_U = np.vstack([ np.hstack([U1.real, -U1.imag]), np.hstack([U1.imag, U1.real]) ]) S = S_U @ S # the resulting covariance state cov = S @ S.T assert np.allclose(cov, S @ S.T, atol=tol, rtol=0)
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