def test_mz_gate_standard(self, tol): """Test that the Mach-Zehnder gate compiles to give the correct unitary for some specific standard parameters""" prog = sf.Program(8) with prog.context as q: ops.MZgate(np.pi / 2, np.pi) | (q[0], q[1]) ops.MZgate(np.pi, 0) | (q[2], q[3]) ops.MZgate(np.pi / 2, np.pi) | (q[4], q[5]) ops.MZgate(np.pi, 0) | (q[6], q[7]) ops.MeasureFock() | q # compile the program using the X8_01 spec res = prog.compile("X8_01") # remove the Fock measurements res.circuit = res.circuit[:-1] # extract the Gaussian symplectic matrix O = res.compile("gaussian_unitary").circuit[0].op.p[0] # By construction, we know that the symplectic matrix is # passive, and so represents a unitary matrix U = O[:8, :8] + 1j * O[8:, :8] # the constructed program should implement the following # unitary matrix expected = np.array([[0.5 - 0.5j, -0.5 + 0.5j, 0, 0], [0.5 - 0.5j, 0.5 - 0.5j, 0, 0], [0, 0, -1, -0], [0, 0, -0, 1]]) expected = block_diag(expected, expected) assert np.allclose(U, expected, atol=tol)
def runJob(self, eng): num_subsystem = 8 prog = sf.Program(num_subsystem, name="remote_job") U = random_interferometer(4) with prog.context as q: # Initial squeezed states # Allowed values are r=1.0 or r=0.0 ops.S2gate(1.0) | (q[0], q[4]) ops.S2gate(1.0) | (q[1], q[5]) ops.S2gate(1.0) | (q[3], q[7]) # Interferometer on the signal modes (0-3) ops.Interferometer(U) | (q[0], q[1], q[2], q[3]) ops.BSgate(0.543, 0.123) | (q[2], q[0]) ops.Rgate(0.453) | q[1] ops.MZgate(0.65, -0.54) | (q[2], q[3]) # *Same* interferometer on the idler modes (4-7) ops.Interferometer(U) | (q[4], q[5], q[6], q[7]) ops.BSgate(0.543, 0.123) | (q[6], q[4]) ops.Rgate(0.453) | q[5] ops.MZgate(0.65, -0.54) | (q[6], q[7]) ops.MeasureFock() | q eng = eng results =eng.run(prog, shots=10) # state = results.state # measurements = results.samples return results.samples
def test_all_passive_gates(hbar, tol): """test that all gates run and do not cause anything to crash""" eng = sf.LocalEngine(backend="gaussian") circuit = sf.Program(4) with circuit.context as q: for i in range(4): ops.Sgate(1, 0.3) | q[i] ops.Rgate(np.pi) | q[0] ops.PassiveChannel(np.ones((2, 2))) | (q[1], q[2]) ops.LossChannel(0.9) | q[1] ops.MZgate(0.25 * np.pi, 0) | (q[2], q[3]) ops.PassiveChannel(np.array([[0.83]])) | q[0] ops.sMZgate(0.11, -2.1) | (q[0], q[3]) ops.Interferometer(np.array([[np.exp(1j * 2)]])) | q[1] ops.BSgate(0.8, 0.4) | (q[1], q[3]) ops.Interferometer(0.5**0.5 * np.fft.fft(np.eye(2))) | (q[0], q[2]) ops.PassiveChannel(0.1 * np.ones((3, 3))) | (q[3], q[1], q[0]) cov = eng.run(circuit).state.cov() circuit = sf.Program(4) with circuit.context as q: ops.Rgate(np.pi) | q[0] ops.PassiveChannel(np.ones((2, 2))) | (q[1], q[2]) ops.LossChannel(0.9) | q[1] ops.MZgate(0.25 * np.pi, 0) | (q[2], q[3]) ops.PassiveChannel(np.array([[0.83]])) | q[0] ops.sMZgate(0.11, -2.1) | (q[0], q[3]) ops.Interferometer(np.array([[np.exp(1j * 2)]])) | q[1] ops.BSgate(0.8, 0.4) | (q[1], q[3]) ops.Interferometer(0.5**0.5 * np.fft.fft(np.eye(2))) | (q[0], q[2]) ops.PassiveChannel(0.1 * np.ones((3, 3))) | (q[3], q[1], q[0]) compiled_circuit = circuit.compile(compiler="passive") T = compiled_circuit.circuit[0].op.p[0] S_sq = np.eye(8, dtype=np.complex128) r = 1 phi = 0.3 for i in range(4): S_sq[i, i] = np.cosh(r) - np.sinh(r) * np.cos(phi) S_sq[i, i + 4] = -np.sinh(r) * np.sin(phi) S_sq[i + 4, i] = -np.sinh(r) * np.sin(phi) S_sq[i + 4, i + 4] = np.cosh(r) + np.sinh(r) * np.cos(phi) cov_sq = (hbar / 2) * S_sq @ S_sq.T mu = np.zeros(8) P = interferometer(T) L = (hbar / 2) * (np.eye(P.shape[0]) - P @ P.T) cov2 = P @ cov_sq @ P.T + L assert np.allclose(cov, cov2, atol=tol, rtol=0)
def test_mz_gate_non_standard(self, theta1, phi1, tol): """Test that the Mach-Zehnder gate compiles to give the correct unitary for a variety of non-standard angles""" prog = sf.Program(8) theta2 = np.pi / 13 phi2 = 3 * np.pi / 7 with prog.context as q: ops.MZgate(theta1, phi1) | (q[0], q[1]) ops.MZgate(theta2, phi2) | (q[2], q[3]) ops.MZgate(theta1, phi1) | (q[4], q[5]) ops.MZgate(theta2, phi2) | (q[6], q[7]) ops.MeasureFock() | q # compile the program using the X8_01 spec res = prog.compile("X8_01") # remove the Fock measurements res.circuit = res.circuit[:-1] # extract the Gaussian symplectic matrix O = res.compile("gaussian_unitary").circuit[0].op.p[0] # By construction, we know that the symplectic matrix is # passive, and so represents a unitary matrix U = O[:8, :8] + 1j * O[8:, :8] # the constructed program should implement the following # unitary matrix expected = np.array([ [(np.exp(1j * phi1) * (-1 + np.exp(1j * theta1))) / 2.0, 0.5j * (1 + np.exp(1j * theta1)), 0, 0], [ 0.5j * np.exp(1j * phi1) * (1 + np.exp(1j * theta1)), (1 - np.exp(1j * theta1)) / 2.0, 0, 0 ], [ 0, 0, (np.exp(1j * phi2) * (-1 + np.exp(1j * theta2))) / 2.0, 0.5j * (1 + np.exp(1j * theta2)) ], [ 0, 0, 0.5j * np.exp(1j * phi2) * (1 + np.exp(1j * theta2)), (1 - np.exp(1j * theta2)) / 2.0 ], ]) expected = block_diag(expected, expected) assert np.allclose(U, expected, atol=tol)
def test_non_primitive_gates(): """Tests that the compiler is able to compile a number of non-primitive Gaussian gates""" width = 6 eng = sf.LocalEngine(backend="gaussian") eng1 = sf.LocalEngine(backend="gaussian") circuit = sf.Program(width) A = np.random.rand(width, width) + 1j * np.random.rand(width, width) A = A + A.T valsA = np.linalg.svd(A, compute_uv=False) A = A / 2 * np.max(valsA) B = np.random.rand( width // 2, width // 2) + 1j * np.random.rand(width // 2, width // 2) valsB = np.linalg.svd(B, compute_uv=False) B = B / 2 * valsB B = np.block([[0 * B, B], [B.T, 0 * B]]) with circuit.context as q: ops.GraphEmbed(A) | q ops.BipartiteGraphEmbed(B) | q ops.Pgate(0.1) | q[1] ops.CXgate(0.2) | (q[0], q[1]) ops.MZgate(0.4, 0.5) | (q[2], q[3]) ops.Fourier | q[0] ops.Xgate(0.4) | q[1] ops.Zgate(0.5) | q[3] compiled_circuit = circuit.compile(compiler="gaussian_unitary") cv = eng.run(circuit).state.cov() mean = eng.run(circuit).state.means() cv1 = eng1.run(compiled_circuit).state.cov() mean1 = eng1.run(compiled_circuit).state.means() assert np.allclose(cv, cv1) assert np.allclose(mean, mean1)
def test_MZgate_decomposition(self): """Test that decompositions take place if the circuit spec requests it.""" class DummyCircuit(Compiler): modes = None remote = False local = True interactive = True primitives = {"S2gate", "Interferometer", "BSgate", "Sgate", "MZgate", "Rgate"} decompositions = {"MZgate": {}} prog = sf.Program(3) U = np.array([[0, 1], [1, 0]]) with prog.context as q: ops.MZgate(0.6, 0.7) | [q[0], q[1]] ops.Interferometer(U) | [q[0], q[1]] new_prog = prog.compile(compiler=DummyCircuit()) # check compiled program now has 5 gates # the MZgate should decompose into two BS and two Rgates assert len(new_prog) == 5 # test gates are correct circuit = new_prog.circuit assert circuit[0].op.__class__.__name__ == "Rgate" assert circuit[1].op.__class__.__name__ == "BSgate" assert circuit[2].op.__class__.__name__ == "Rgate" assert circuit[3].op.__class__.__name__ == "BSgate" assert circuit[4].op.__class__.__name__ == "Interferometer"
def test_no_decompositions(self): """Test that no decompositions take place if the circuit spec doesn't support it.""" class DummyCircuit(Compiler): """A circuit spec with no decompositions""" modes = None remote = False local = True interactive = True primitives = {"S2gate", "Interferometer", "MZgate"} decompositions = set() prog = sf.Program(3) U = np.array([[0, 1], [1, 0]]) with prog.context as q: ops.S2gate(0.6) | [q[0], q[1]] ops.Interferometer(U) | [q[0], q[1]] ops.MZgate(0.1, 0.6) | [q[0], q[1]] new_prog = prog.compile(compiler=DummyCircuit()) # check compiled program only has three gates assert len(new_prog) == 3 # test gates are correct circuit = new_prog.circuit assert circuit[0].op.__class__.__name__ == "S2gate" assert circuit[1].op.__class__.__name__ == "Interferometer" assert circuit[2].op.__class__.__name__ == "MZgate"
def test_mach_zehnder_interferometers(self, tol): """Test Mach-Zehnder gates correctly compile""" prog = sf.Program(4) phi = 0.543 theta = -1.654 with prog.context as q: ops.S2gate(0.5) | (q[0], q[2]) ops.S2gate(0.5) | (q[3], q[1]) ops.MZgate(phi, theta) | (q[0], q[1]) ops.MZgate(phi, theta) | (q[2], q[3]) ops.MeasureFock() | q res = prog.compile("chip0") expected = sf.Program(4) with expected.context as q: ops.S2gate(0.5, 0) | (q[0], q[2]) ops.S2gate(0.5, 0) | (q[1], q[3]) # corresponds to MZgate(phi, theta) on modes [0, 1] ops.Rgate(np.mod(phi, 2 * np.pi)) | q[0] ops.BSgate(np.pi / 4, np.pi / 2) | (q[0], q[1]) ops.Rgate(np.mod(theta, 2 * np.pi)) | q[0] ops.BSgate(np.pi / 4, np.pi / 2) | (q[0], q[1]) ops.Rgate(0) | q[0] ops.Rgate(0) | q[1] # corresponds to MZgate(phi, theta) on modes [2, 3] ops.Rgate(np.mod(phi, 2 * np.pi)) | q[2] ops.BSgate(np.pi / 4, np.pi / 2) | (q[2], q[3]) ops.Rgate(np.mod(theta, 2 * np.pi)) | q[2] ops.BSgate(np.pi / 4, np.pi / 2) | (q[2], q[3]) ops.Rgate(0) | q[2] ops.Rgate(0) | q[3] ops.MeasureFock() | (q[0], q[3], q[1], q[2]) assert program_equivalence(res, expected, atol=tol)
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 unitary(q): ops.MZgate(0.5, 0.1) | (q[0], q[1]) ops.BSgate(0.1, 0.2) | (q[1], q[2]) ops.Rgate(0.4) | q[0]
def test_no_unitary(self, chip, tol): """Test compilation works with no unitary provided""" prog = sf.Program(12) with prog.context as q: ops.S2gate(SQ_AMPLITUDE) | (q[0], q[6]) ops.S2gate(SQ_AMPLITUDE) | (q[1], q[7]) ops.S2gate(SQ_AMPLITUDE) | (q[2], q[8]) ops.S2gate(SQ_AMPLITUDE) | (q[3], q[9]) ops.S2gate(SQ_AMPLITUDE) | (q[4], q[10]) ops.S2gate(SQ_AMPLITUDE) | (q[5], q[11]) ops.MeasureFock() | q res = prog.compile(chip.short_name) expected = sf.Program(12) with expected.context as q: ops.S2gate(SQ_AMPLITUDE, 0) | (q[0], q[6]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[1], q[7]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[2], q[8]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[3], q[9]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[4], q[10]) ops.S2gate(SQ_AMPLITUDE, 0) | (q[5], q[11]) # corresponds to an identity on modes [0, 1, 2, 3, 4, 5] # 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, 0) | (q[4], q[5]) ops.MZgate(np.pi, np.pi) | (q[1], q[2]) ops.MZgate(np.pi, np.pi) | (q[3], q[4]) ops.MZgate(np.pi, 0) | (q[0], q[1]) ops.MZgate(np.pi, 0) | (q[2], q[3]) ops.MZgate(np.pi, 0) | (q[4], q[5]) ops.MZgate(np.pi, np.pi) | (q[1], q[2]) ops.MZgate(np.pi, np.pi) | (q[3], q[4]) ops.MZgate(np.pi, 0) | (q[0], q[1]) ops.MZgate(np.pi, np.pi) | (q[2], q[3]) ops.MZgate(np.pi, np.pi) | (q[4], q[5]) ops.MZgate(np.pi, np.pi) | (q[1], q[2]) ops.MZgate(np.pi, np.pi) | (q[3], q[4]) ops.Rgate(np.pi) | (q[0]) ops.Rgate(0) | (q[1]) ops.Rgate(0) | (q[2]) ops.Rgate(0) | (q[3]) ops.Rgate(0) | (q[4]) ops.Rgate(0) | (q[5]) # corresponds to an identity on modes [6, 7, 8, 9, 10, 11] ops.MZgate(np.pi, 0) | (q[6], q[7]) ops.MZgate(np.pi, 0) | (q[8], q[9]) ops.MZgate(np.pi, 0) | (q[10], q[11]) ops.MZgate(np.pi, np.pi) | (q[7], q[8]) ops.MZgate(np.pi, np.pi) | (q[9], q[10]) ops.MZgate(np.pi, 0) | (q[6], q[7]) ops.MZgate(np.pi, 0) | (q[8], q[9]) ops.MZgate(np.pi, 0) | (q[10], q[11]) ops.MZgate(np.pi, np.pi) | (q[7], q[8]) ops.MZgate(np.pi, np.pi) | (q[9], q[10]) ops.MZgate(np.pi, 0) | (q[6], q[7]) ops.MZgate(np.pi, np.pi) | (q[8], q[9]) ops.MZgate(np.pi, np.pi) | (q[10], q[11]) ops.MZgate(np.pi, np.pi) | (q[7], q[8]) ops.MZgate(np.pi, np.pi) | (q[9], q[10]) ops.Rgate(np.pi) | (q[6]) ops.Rgate(0) | (q[7]) ops.Rgate(0) | (q[8]) ops.Rgate(0) | (q[9]) ops.Rgate(0) | (q[10]) ops.Rgate(0) | (q[11]) ops.MeasureFock() | q # 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 = TMS(SQ_AMPLITUDE, 0) expected = np.zeros([2*12, 2*12]) l = 12 // 2 ch = np.cosh(SQ_AMPLITUDE) * np.identity(l) sh = np.sinh(SQ_AMPLITUDE) * np.identity(l) zh = np.zeros([l, l]) expected = np.block([[ch, sh, zh, zh], [sh, ch, zh, zh], [zh, zh, ch, -sh], [zh, zh, -sh, ch]]) assert np.allclose(O, expected, atol=tol)
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("X8_01") 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, 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.Rgate(0) | (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, 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.Rgate(0) | (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) # 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 = TMS(SQ_AMPLITUDE, 0) expected = np.zeros([2 * 8, 2 * 8]) idx = np.arange(2 * 8).reshape(4, 4).T for i in idx: expected[i.reshape(-1, 1), i.reshape(1, -1)] = S assert np.allclose(O, expected, atol=tol)