def encode_zero(self, prog: Program, block: CodeBlock, ancilla: CodeBlock, scratch: MemoryChunk): if len(scratch) < self.error_correct_scratch_size: raise ValueError("scratch buffer is too small") flag = scratch[0] outcome = scratch[1] scratch = scratch[2:] # To do a somewhat fault tolerant preparation, we will do a noisy preparation of the target # block, then perform a Steane error correction using a noisy ancilla. Instead of actually # doing an error correction though, we will just do error detection and repeat until no # errors are detected. If no errors are detected, then either both the block and ancilla # were clean or they both has the exact same errors, which is unlikely. This idea comes from # section 4.6 of "An Introduction to Quantum Error Correction and Fault-Tolerant Quantum # Computation" by Daniel Gottesman. loop_prog = Program() loop_prog += gates.MOVE(flag, 0) block.reset(loop_prog) loop_prog += self.noisy_encode_zero(block.qubits) self._error_detect_x(loop_prog, block, ancilla, outcome, scratch, include_operators=True) loop_prog += gates.IOR(flag, outcome) self._error_detect_z(loop_prog, block, ancilla, outcome, scratch, include_operators=False) loop_prog += gates.IOR(flag, outcome) prog += gates.MOVE(flag, 1) prog.while_do(flag, loop_prog)
def reset(self, prog: Program): """ Reset the physical qubits to the |0>^{\otimes n} state and the errors to 0. This code block must not be entangled with any other qubits in the system. """ prog += (gates.MEASURE(self.qubits[i], self.x_errors[i]) for i in range(self.n)) for i in range(self.n): prog.if_then(self.x_errors[i], gates.X(self.qubits[i])) prog += gates.MOVE(self.x_errors[i], 0) prog += gates.MOVE(self.z_errors[i], 0)
def majority_vote(prog: Program, inputs: MemoryChunk, output: MemoryReference, scratch_int: MemoryChunk): if len(scratch_int) < 2: raise ValueError("scratch_int buffer too small") if len(inputs) % 2 == 0: raise ValueError("inputs length must be odd") prog += gates.MOVE(scratch_int[0], 0) for bit in inputs: prog += gates.CONVERT(scratch_int[1], bit) prog += gates.ADD(scratch_int[0], scratch_int[1]) threshold = (len(inputs) + 1) // 2 prog += gates.MOVE(scratch_int[1], threshold) prog += gates.GE(output, scratch_int[0], scratch_int[1])
def quil_classical_detect(prog: Program, codeword: MemoryChunk, errors: MemoryChunk, outcome: MemoryReference, scratch: MemoryChunk, parity_check): """ Extend a Quil program with instructions to detect errors in a noisy classical codeword. Sets the outcome bit if any errors are detected and unsets it otherwise. """ m, n = parity_check.shape if len(codeword) != n: raise ValueError("codeword is of incorrect size") if len(errors) != n: raise ValueError("errors is of incorrect size") if len(scratch) < m + 2: raise ValueError("scratch buffer is too small") # Add in existing known errors to the noisy codeword. prog += (gates.XOR(codeword[i], errors[i]) for i in range(n)) # Compute the syndrome by multiplying by the parity check matrix. syndrome = scratch[2:m+2] quil_classical.matmul(prog, parity_check, codeword, syndrome, scratch[:2]) # Revert codeword back to the state without error corrections. prog += (gates.XOR(codeword[i], errors[i]) for i in range(n)) # Set outcome only if syndrome is non-zero. prog += gates.MOVE(outcome, 0) prog += (gates.IOR(outcome, syndrome[i]) for i in range(m))
def test_string_match(self): test_cases = [ ([0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], True), ([0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 1], True), ([0, 0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 0, 0, 1, 1], True), ([0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], False), ([0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1], False), ([0, 0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 0, 0, 0, 1], False), ] for vec1, vec2, expected_match in test_cases: n = len(vec1) prog = Program() raw_mem = prog.declare('ro', 'BIT', n + 2) self.initialize_memory(prog, raw_mem) mem = MemoryChunk(raw_mem, 0, n + 2) vec = mem[0:n] output = mem[n:(n + 1)] scratch = mem[(n + 1):(n + 2)] # Copy data from vec2 into vec. for i in range(n): prog += gates.MOVE(vec[i], vec2[i]) match_vec = np.array(vec1, dtype='int') quil_classical.string_match(prog, vec, match_vec, output, scratch) results = self.qvm.run(prog)[0] self.assertEqual(results[n] == 1, expected_match)
def test_matmul(self): m, n = 20, 10 mat = np.random.randint(0, 2, size=(m, n), dtype='int') vec = np.random.randint(0, 2, size=n, dtype='int') prog = Program() raw_mem = prog.declare('ro', 'BIT', n + m + 1) self.initialize_memory(prog, raw_mem) mem = MemoryChunk(raw_mem, 0, n + m + 1) vec_in = mem[0:n] vec_out = mem[n:(n + m)] scratch = mem[(n + m):(n + m + 1)] # Copy data from vec into program memory. for i in range(n): prog += gates.MOVE(vec_in[i], int(vec[i])) quil_classical.matmul(prog, mat, vec_in, vec_out, scratch) results = self.qvm.run(prog)[0] actual = results[n:(n + m)] expected = np.mod(np.matmul(mat, vec), 2) for i in range(m): self.assertEqual(actual[i], int(expected[i]))
def string_match(prog: Program, mem: MemoryChunk, vec, output: MemoryChunk, scratch: MemoryChunk): """ Compares a bit string in Quil classical memory to a constant vector. If they are equal, the function assigns output to 1, otherwise 0. """ n = len(mem) if vec.size != n: raise ValueError("length of mem and vec do not match") if len(scratch) < 1: raise ValueError("scratch buffer is too small") prog += gates.MOVE(output[0], 0) for i in range(n): prog += gates.MOVE(scratch[0], mem[i]) prog += gates.XOR(scratch[0], int(vec[i])) prog += gates.IOR(output[0], scratch[0]) prog += gates.NOT(output[0])
def _initialize_memory(prog: Program, mem: MemoryReference, qubits: List[QubitPlaceholder]): """ The QVM has some weird behavior where classical memory registers have to be initialized with a MEASURE before any values can be MOVE'd to them. So for memory regions used internally, initialize memory by performing superfluous measurements. """ prog += (gates.MEASURE(qubits[i % len(qubits)], mem[i]) for i in range(mem.declared_size)) prog += (gates.MOVE(mem[i], 0) for i in range(mem.declared_size))
def matmul(prog: Program, mat, vec: MemoryChunk, result: MemoryChunk, scratch: MemoryChunk): """ Extend a Quil program with instructions to perform a classical matrix multiplication of a fixed binary matrix with a vector of bits stored in classical memory. """ m, n = mat.shape if len(vec) != n: raise ValueError("mat and vec are of incompatible sizes") if len(result) != m: raise ValueError("mat and result are of incompatible sizes") if len(scratch) < 1: raise ValueError("scratch buffer is too small") for i in range(m): prog += gates.MOVE(result[i], 0) for j in range(n): prog += gates.MOVE(scratch[0], vec[j]) prog += gates.AND(scratch[0], int(mat[i][j])) prog += gates.XOR(result[i], scratch[0])
def conditional_xor(prog: Program, mem: MemoryChunk, vec, flag: MemoryChunk, scratch: MemoryChunk): """ Conditionally bitwise XORs a constant vector to a bit string in Quil classical memory if a flag bit is set. If the flag bit is not set, this does not modify the memory. """ n = len(mem) if vec.size != n: raise ValueError("length of mem and vec do not match") for i in range(n): prog += gates.MOVE(scratch[0], flag[0]) prog += gates.AND(scratch[0], int(vec[i])) prog += gates.XOR(mem[i], scratch[0])
def qsolution(ansatz, opt_angles): """Returns the wavefunction of the ansatz at the optimal angles.""" prog = Program() memory_map = {"theta": opt_angles} for name, arr in memory_map.items(): for index, value in enumerate(arr): prog += gates.MOVE(gates.MemoryReference(name, offset=index), value) ansatz = prog + ansatz soln = pyquil.quil.percolate_declares(ansatz) wfsim = pyquil.api.WavefunctionSimulator() return wfsim.wavefunction(soln).amplitudes
def encode_plus(self, prog: Program, block: CodeBlock, ancilla: CodeBlock, scratch: MemoryChunk): if len(scratch) < self.error_correct_scratch_size: raise ValueError("scratch buffer is too small") flag = scratch[0] outcome = scratch[1] scratch = scratch[2:] # See encode_zero for more thorough comments. loop_prog = Program() loop_prog += gates.MOVE(flag, 0) block.reset(loop_prog) loop_prog += self.noisy_encode_plus(block.qubits) self._error_detect_x(loop_prog, block, ancilla, outcome, scratch, include_operators=False) loop_prog += gates.IOR(flag, outcome) self._error_detect_z(loop_prog, block, ancilla, outcome, scratch, include_operators=True) loop_prog += gates.IOR(flag, outcome) prog += gates.MOVE(flag, 1) prog.while_do(flag, loop_prog)
def measure(self, prog: Program, data: CodeBlock, index: int, outcome: MemoryReference, ancilla_1: CodeBlock, ancilla_2: CodeBlock, scratch: MemoryChunk, scratch_int: MemoryChunk): """ Extend a Quil program to measure the logical qubit in the Z basis. Ancilla must be in a logical |0> state. Index is the index of the logical qubit within the code block. Currently must be 0. This measurement is made fault tolerant by repeating a noisy measurement 2t + 1 times and returning a majority vote. This yields control after each fault tolerant operation so that a round of error correction may be performed globally if required. """ if index != 0: raise ValueError("only one logical qubit per code block") if data.n != self.n: raise ValueError("data code word is of incorrect size") if ancilla_1.n != self.n: raise ValueError("ancilla_1 code word is of incorrect size") if ancilla_2.n != self.n: raise ValueError("ancilla_2 code word is of incorrect size") if len(scratch) < self.measure_scratch_size: raise ValueError("scratch buffer is too small") if len(scratch_int) < 1: raise ValueError("scratch_int buffer is too small") trials = 2 * self.t + 1 # Split up the scratch buffer. noisy_outcomes = scratch[:trials] noisy_scratch = scratch[trials:] for i in range(trials): self.noisy_measure(prog, data, index, noisy_outcomes[i], ancilla_1, ancilla_2, noisy_scratch) yield outcome_bit = noisy_scratch[0] quil_classical.majority_vote(prog, noisy_outcomes, outcome_bit, scratch_int) # Because of the stupid thing where the QVM relies on MEASURE to initialize classical # registers, do a superfluous measure here of the already trashed ancilla. prog += gates.MEASURE(ancilla_1.qubits[0], outcome) # In case outcome is not a bit reference, do a CONVERT instead of a MOVE. prog += gates.MOVE(outcome, outcome_bit)
def test_majority_vote(self): test_cases = [ ([0, 0, 0], 0), ([0, 0, 1], 0), ([0, 1, 0], 0), ([1, 0, 0], 0), ([0, 1, 1], 1), ([1, 0, 1], 1), ([1, 1, 0], 1), ([1, 1, 1], 1), ([0, 1, 0, 1, 0], 0), ([1, 0, 1, 0, 1], 1), ] for inputs, expected_output in test_cases: prog = Program() raw_mem = prog.declare('ro', 'BIT', len(inputs) + 1) raw_scratch_int = prog.declare('scratch_int', 'INTEGER', 2) self.initialize_memory(prog, raw_mem) self.initialize_memory(prog, raw_scratch_int) mem = MemoryChunk(raw_mem, 0, raw_mem.declared_size) output_mem = mem[0] inputs_mem = mem[1:] scratch_int = MemoryChunk(raw_scratch_int, 0, raw_scratch_int.declared_size) # Copy data from inputs into mem. prog += (gates.MOVE(inputs_mem[i], inputs[i]) for i in range(len(inputs))) quil_classical.majority_vote(prog, inputs_mem, output_mem, scratch_int) results = self.qvm.run(prog)[0] self.assertEqual(results[0], expected_output)
def initialize_memory(self, prog, mem): # Need to measure a qubit to initialize memory for some reason. for i in range(mem.declared_size): prog += gates.MEASURE(0, mem[i]) prog += gates.MOVE(mem[i], 0)