def build_circuit(self): params_counter = 0 sgates = [] dgates = [] kgates = [] vgates = [] for gate_structure in self.gates_structure: if gate_structure[0] is Sgate: sgates.append( ParametrizedGate(gate_structure[0], gate_structure[1], [ make_param(**gate_structure[2]), make_param(**gate_structure[3]) ])) if gate_structure[0] is Dgate: dgates.append( ParametrizedGate(gate_structure[0], gate_structure[1], [ make_param(**gate_structure[2]), make_param(**gate_structure[3]) ])) if gate_structure[0] is Kgate: kgates.append( ParametrizedGate(gate_structure[0], gate_structure[1], [make_param(**gate_structure[2])])) if gate_structure[0] is Vgate: vgates.append( ParametrizedGate(gate_structure[0], gate_structure[1], [make_param(**gate_structure[2])])) eng, q = sf.Engine(self.n_qumodes) rl, U = takagi(self.adj_matrix) initial_squeezings = np.arctanh(rl) with eng: Interferometer(U) | q for i, squeeze_value in enumerate(initial_squeezings): Sgate(squeeze_value) | i if len(sgates) != 0: Interferometer(self.interferometer_matrix) | q for gate in sgates: gate.gate(gate.params[0], gate.params[1]) | gate.qumodes if len(dgates) != 0: Interferometer(self.interferometer_matrix) | q for gate in dgates: gate.gate(gate.params[0], gate.params[1]) | gate.qumodes for gate in kgates: gate.gate(gate.params[0]) | gate.qumodes for gate in vgates: gate.gate(gate.params[0]) | gate.qumodes circuit = {} circuit['eng'] = eng circuit['q'] = q return circuit
def test_zeros(self): """Verify that the Takagi decomposition returns a zero vector and identity matrix when input a matrix of zeros""" dim = 4 a = np.zeros((dim, dim)) rl, U = dec.takagi(a) assert np.allclose(rl, np.zeros(dim)) assert np.allclose(U, np.eye(dim))
def test_random_symm(self, tol): """Verify that the Takagi decomposition, applied to a random symmetric matrix, produced a decomposition that can be used to reconstruct the matrix.""" A = np.random.random([6, 6]) + 1j * np.random.random([6, 6]) A += A.T rl, U = dec.takagi(A) res = U @ np.diag(rl) @ U.T assert np.allclose(res, A, atol=tol, rtol=0)
def test_real_degenerate(self): """Verify that the Takagi decomposition returns a matrix that is unitary and results in a correct decomposition when input a real but highly degenerate matrix. This test uses the adjacency matrix of a balanced tree graph.""" g = nx.balanced_tree(2, 4) a = nx.to_numpy_array(g) rl, U = dec.takagi(a) assert np.allclose(U @ U.conj().T, np.eye(len(a))) assert np.allclose(U @ np.diag(rl) @ U.T, a)
def test_takagi_random_symm(self): error = np.empty(nsamples) for i in range(nsamples): X = random_degenerate_symmetric() rl, U = dec.takagi(X) Xr = U @ np.diag(rl) @ np.transpose(U) diff = np.linalg.norm(Xr - X) error[i] = diff self.assertAlmostEqual(error.mean(), 0)
def jsa_from_m(m): """Given a phase sensitive moment m returns the joint spectral amplitude associated with it. Args: m (array): phase sentive moment Returns: (array): joint spectral amplitude """ ls, u = takagi(m) return u @ np.diag(0.5 * np.arcsinh(2 * ls)) @ u.T
def test_symmetric_validation(self): """Test that the takagi decomposition raises exception if not symmetric""" A = np.random.random([5, 5]) + 1j * np.random.random([5, 5]) with pytest.raises(ValueError, match="matrix is not symmetric"): dec.takagi(A)
def test_square_validation(self): """Test that the takagi decomposition raises exception if not square""" A = np.random.random([4, 5]) + 1j * np.random.random([4, 5]) with pytest.raises(ValueError, match="matrix must be square"): dec.takagi(A)
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 matelem(l, m, n, U, Up, ls, alpha): """ Calculates a Fock matrix element <m|W(alpha,U,ls,Up)|n> of the Gaussian unitary W specified by alpha, U, ls, Up. Args: l (integer): Number of modes m (list): List of integers specifying the input Fock states n (list): List of integers specifying the output Fock states U (array): Unitary matrix of size l Up (array): Unitary matrix of size l ls (array): Squeezing parameters alpha (array): Complex displacements Returns: (complex): Value of the required matrix element """ assert l == len(m) assert l == len(n) assert U.shape == (l, l) assert Up.shape == (l, l) assert len(ls) == l assert len(alpha) == l idl = np.identity(l) # Define extended unitaries that are identities in the second half of the mode Ue = np.block([[U, 0 * idl], [0 * idl, idl]]) Uep = np.block([[Up, 0 * idl], [0 * idl, idl]]) # Define the ts of the squeezing parameters # pylint: disable=assignment-from-no-return ts = np.arcsinh(np.sqrt(1.0 * np.array(n))) # Now we generate the circuit in Fig 4.(b) nmodes = 2 * l state = gc.GaussianModes(nmodes) for i, t in enumerate(ts): tmsq(state, i, i + l, -t) state.apply_u(Uep) for i, lval in enumerate(ls): state.squeeze(-lval, 0, i) state.apply_u(Ue) # Shortcircuited Bloch-Messiah using Takagi Mt = state.mmat lt, ut = takagi(Mt, 15) # Define the lambda tilde and the u tilde lt = -0.5 * np.arcsinh(2 * lt) ut = ut.conj() alphat = np.array(list(alpha) + list(np.zeros_like(alpha))) B = ut @ np.diag(np.tanh(lt)) @ ut.T zeta = alphat - B @ alphat.conj() pref = -0.5 * alphat.conj() @ zeta p = m + n # Calculating prefactors R = 1.0 / np.prod((np.tanh(ts)**n) / np.cosh(ts)) prefns = np.sqrt(np.prod(np.array([np.math.factorial(i) for i in p]))) T = np.exp(pref) / (prefns * np.sqrt(np.prod(np.cosh(lt)))) # Calculating the multiset S_p sp = [] for k, pval in enumerate(p): for i in range(pval): sp.append(k) # Generate Bp with possibly repeated rows and columns Bp = B[:, sp][sp, :] # Generate zetap with possibly repeated entries zetap = zeta[sp] # Calculate Bt np.fill_diagonal(Bp, zetap) if Bp.shape == (0, 0): amp = 1.0 else: amp = hafnian(Bp, loop=True) mu = R * T * amp return mu
def build_circuit(self, adj_matrix): params_counter = 0 number_of_layers = 2 all_sgates = [[]] * number_of_layers all_dgates = [[]] * number_of_layers all_kgates = [[]] * number_of_layers all_vgates = [[]] * number_of_layers for gate_structure in self.gates_structure: current_layer = int(gate_structure[2]['name'].split('_')[-1][0]) if gate_structure[0] is Sgate: current_gate = ParametrizedGate( gate_structure[0], gate_structure[1], [ make_param(**gate_structure[2]), make_param(**gate_structure[3]) ]) all_sgates[current_layer].append(current_gate) if gate_structure[0] is Dgate: current_gate = ParametrizedGate( gate_structure[0], gate_structure[1], [ make_param(**gate_structure[2]), make_param(**gate_structure[3]) ]) all_dgates[current_layer].append(current_gate) if gate_structure[0] is Kgate: current_gate = ParametrizedGate( gate_structure[0], gate_structure[1], [make_param(**gate_structure[2])]) all_kgates[current_layer].append(current_gate) if gate_structure[0] is Vgate: current_gate = ParametrizedGate( gate_structure[0], gate_structure[1], [make_param(**gate_structure[2])]) all_vgates[current_layer].append(current_gate) eng, q = sf.Engine(self.n_qumodes) rl, U = takagi(adj_matrix) initial_squeezings = np.arctanh(rl) with eng: for i, squeeze_value in enumerate(initial_squeezings): Sgate(squeeze_value) | i Interferometer(U) | q for layer in range(number_of_layers): sgates = all_sgates[layer] dgates = all_dgates[layer] kgates = all_kgates[layer] vgates = all_vgates[layer] if len(sgates) != 0: Interferometer(self.interferometer_matrix) | q for gate in sgates: gate.gate(gate.params[0], gate.params[1]) | gate.qumodes if len(dgates) != 0: Interferometer(self.interferometer_matrix) | q for gate in dgates: gate.gate(gate.params[0], gate.params[1]) | gate.qumodes for gate in kgates: gate.gate(gate.params[0]) | gate.qumodes for gate in vgates: gate.gate(gate.params[0]) | gate.qumodes circuit = {} circuit['eng'] = eng circuit['q'] = q return circuit