def get_qubit_registers_for_adder(qc: QuantumComputer, num_length: int, qubits: Optional[Sequence[int]] = None)\ -> Tuple[Sequence[int], Sequence[int], int, int]: """ Searches for a layout among the given qubits for the two n-bit registers and two additional ancilla that matches the simple layout given in figure 4 of [CDKM96]_. This method ignores any considerations of physical characteristics of the qc aside from the qubit layout. An error is thrown if the appropriate layout is not found. :param qc: the quantum resource on which an adder program will be executed. :param num_length: the length of the bitstring representation of one summand :param qubits: the available qubits on which to run the adder program. :return: the necessary registers and ancilla labels for implementing an adder program to add the numbers a and b. The output can be passed directly to :func:`adder` """ if qubits is None: unavailable = [] # assume this means all qubits in qc are available else: unavailable = [qubit for qubit in qc.qubits() if qubit not in qubits] graph = qc.qubit_topology().copy() for qubit in unavailable: graph.remove_node(qubit) # network x only provides subgraph isomorphism, but we want a subgraph monomorphism, i.e. we # specifically want to match the edges desired_layout with some subgraph of graph. To # accomplish this, we swap the nodes and edges of graph by making a line graph. line_graph = nx.line_graph(graph) # We want a path of n nodes, which has n-1 edges. Since we are matching edges of graph with # nodes of layout we make a layout of n-1 nodes. num_desired_nodes = 2 * num_length + 2 desired_layout = nx.path_graph(num_desired_nodes - 1) g_matcher = nx.algorithms.isomorphism.GraphMatcher(line_graph, desired_layout) try: # pick out a subgraph isomorphic to the desired_layout if one exists # this is an isomorphic mapping from edges in graph (equivalently nodes of line_graph) to # nodes in desired_layout (equivalently edges of a path graph with one more node) edge_iso = next(g_matcher.subgraph_isomorphisms_iter()) except IndexError: raise Exception( "An appropriate layout for the qubits could not be found among the " "provided qubits.") # pick out the edges of the isomorphism from the original graph subgraph = nx.Graph(graph.edge_subgraph(edge_iso.keys())) # pick out an endpoint of our path to start the assignment start_node = -1 for node in subgraph.nodes: if subgraph.degree(node) == 1: # found an endpoint start_node = node break return assign_registers_to_line_or_cycle(start_node, subgraph, num_length)
def process_characterisation(qc: QuantumComputer) -> dict: """Convert a :py:class:`pyquil.api.QuantumComputer` to a dictionary containing Rigetti device Characteristics :param qc: A quantum computer to be converted :type qc: QuantumComputer :return: A dictionary containing Rigetti device characteristics """ specs = qc.device.get_specs() coupling_map = [[n, ni] for n, neigh_dict in qc.qubit_topology().adjacency() for ni, _ in neigh_dict.items()] node_ers_dict = {} link_ers_dict = {} t1_times_dict = {} t2_times_dict = {} device_node_fidelities = specs.f1QRBs() # type: ignore device_link_fidelities = specs.fCZs() # type: ignore device_fROs = specs.fROs() # type: ignore device_t1s = specs.T1s() # type: ignore device_t2s = specs.T2s() # type: ignore for index in qc.qubits(): error_cont = QubitErrorContainer({OpType.Rx, OpType.Rz}) error_cont.add_readout(1 - device_fROs[index]) # type: ignore t1_times_dict[index] = device_t1s[index] t2_times_dict[index] = device_t2s[index] error_cont.add_error( (OpType.Rx, 1 - device_node_fidelities[index])) # type: ignore # Rigetti use frame changes for Rz, so they effectively have no error. node_ers_dict[Node(index)] = error_cont for (a, b), fid in device_link_fidelities.items(): error_cont = QubitErrorContainer({OpType.CZ}) error_cont.add_error((OpType.CZ, 1 - fid)) # type: ignore link_ers_dict[(Node(a), Node(b))] = error_cont link_ers_dict[(Node(b), Node(a))] = error_cont arc = Architecture(coupling_map) characterisation = dict() characterisation["NodeErrors"] = node_ers_dict characterisation["EdgeErrors"] = link_ers_dict characterisation["Architecture"] = arc characterisation["t1times"] = t1_times_dict characterisation["t2times"] = t2_times_dict return characterisation
def add_programs_to_dataframe( df: DataFrame, qc: QuantumComputer, qubits_at_depth: Dict[int, Sequence[int]] = None, program_generator: Callable[ [QuantumComputer, Sequence[int], np.ndarray, np.ndarray], Program] = _naive_program_generator ) -> DataFrame: """ Passes the abstract circuit description in each row of the dataframe df along to the supplied program_generator which yields a program that can be run on the available qubits_at_depth[depth] on the given qc resource. :param df: a dataframe populated with abstract descriptions of model circuits, i.e. a df returned by a call to generate_quantum_volume_experiments. :param qc: the quantum resource on which each output program will be run. :param qubits_at_depth: the qubits of the qc available for use at each depth, default all qubits in the qc for each depth. Any subset of these may actually be used by the program. :param program_generator: a method which uses the given qc, its available qubits, and an abstract description of the model circuit to produce a PyQuil program implementing the circuit using only native gates and the given qubits. This program must respect the topology of the qc induced by the given qubits. The default _naive_program_generator uses the qc's compiler to achieve this result. :return: a copy of df with a new "Program" column populated with native PyQuil programs that implement the circuit in "Abstract Ckt" on the qc using a subset of the qubits specified as available for the given depth. The used qubits are also recorded in a "Qubits" column. Note that although the abstract circuit has depth=width, for the program width >= depth. """ new_df = df.copy() depths = new_df["Depth"].values circuits = new_df["Abstract Ckt"].values if qubits_at_depth is None: all_qubits = qc.qubits( ) # by default the program can act on any qubit in the computer qubits = [all_qubits for _ in circuits] else: qubits = [qubits_at_depth[depth] for depth in depths] programs = [ program_generator(qc, qbits, *ckt) for qbits, ckt in zip(qubits, circuits) ] new_df["Program"] = Series(programs) # these are the qubits actually used in the program, a subset of qubits_at_depth[depth] new_df["Qubits"] = Series([program.get_qubits() for program in programs]) return new_df
def estimate_joint_reset_confusion(qc: QuantumComputer, qubits: Sequence[int] = None, num_trials: int = 10, joint_group_size: int = 1, use_active_reset: bool = True, show_progress_bar: bool = False) \ -> Dict[Tuple[int, ...], np.ndarray]: """ Measures a reset 'confusion matrix' for all groups of size joint_group_size among the qubits. Specifically, for each possible joint_group_sized group among the qubits we perform a measurement for each bitstring on that group. The measurement proceeds as follows: -Repeatedly try to prepare the bitstring state on the qubits until success. -If use_active_reset is true (default) actively reset qubits to ground state; otherwise, wait the preset amount of time for qubits to decay to ground state. -Measure the state after the chosen reset method. Since reset should result in the all zeros state this 'confusion matrix' should ideally have all ones down the left-most column of the matrix. The entry at (row_idx, 0) thus represents the success probability of reset given that the pre-reset state is the binary representation of the number row_idx. WARNING: this method can be very slow :param qc: a quantum computer whose reset error you wish to characterize :param qubits: a list of accessible qubits on the qc you wish to characterize. Defaults to all qubits in qc. :param num_trials: number of repeated trials of reset after preparation of each bitstring on each joint group of qubits. Note: num_trials does not correspond to num_shots; a new program must be run in each trial, so running a group of n trials takes longer than would collecting the same number of shots output from a single program. :param joint_group_size: the size of each group; a square matrix with 2^joint_group_size number of rows/columns will be estimated for each group of qubits of the given size among the provided qubits. :param use_active_reset: dictates whether to actively reset qubits or else wait the pre-allotted amount of time for the qubits to decay to the ground state. Using active reset will allow for faster data collection. :param show_progress_bar: displays a progress bar via tqdm if true. :return: a dictionary whose keys are all possible joint_group_sized tuples that can be formed from the qubits. Each value is an estimated 2^group_size square matrix for the corresponding tuple of qubits. Each key is listed in order of increasing qubit number. The corresponding matrix has rows and columns indexed in increasing bitstring order, with most significant (leftmost) bit labeling the smallest qubit number. Each matrix row corresponds to the bitstring that was prepared before the reset, and each column corresponds to the bitstring measured after the reset. """ # establish default as all operational qubits in qc if qubits is None: qubits = qc.qubits() qubits = sorted(qubits) groups = list(itertools.combinations(qubits, joint_group_size)) confusion_matrices = {} total_num_rounds = len(groups) * 2**joint_group_size with tqdm(total=total_num_rounds, disable=not show_progress_bar) as pbar: for group in groups: # program prepares a bit string (specified by parameterization) and immediately measures prep_program = _readout_group_parameterized_bitstring(group) prep_executable = qc.compiler.native_quil_to_executable( prep_program) matrix = np.zeros((2**joint_group_size, 2**joint_group_size)) for row, bitstring in enumerate( itertools.product([0, 1], repeat=joint_group_size)): for _ in range(num_trials): # try preparation at most 10 times. for _ in range(10): # prepare the given bitstring and measure result = qc.run(prep_executable, memory_map={ 'target': [bit * pi for bit in bitstring] }) # if the preparation is successful, move on to reset. if np.array_equal(result[0], bitstring): break # execute program that measures the post-reset state if use_active_reset: # program runs immediately after prep program and actively resets qubits reset_measure_program = Program(RESET()) else: # this program automatically waits pre-allotted time for qubits to decay reset_measure_program = Program() ro = reset_measure_program.declare('ro', memory_type='BIT', memory_size=len(qubits)) for idx, qubit in enumerate(group): reset_measure_program += MEASURE(qubit, ro[idx]) executable = qc.compiler.native_quil_to_executable( reset_measure_program) results = qc.run(executable) # update confusion matrix for result in results: base = np.array( [2**i for i in reversed(range(joint_group_size))]) observed = np.sum(base * result) matrix[row, observed] += 1 / num_trials # update the progress bar pbar.update(1) # store completed confusion matrix in dictionary confusion_matrices[group] = matrix return confusion_matrices
def estimate_joint_confusion_in_set(qc: QuantumComputer, qubits: Sequence[int] = None, num_shots: int = 1000, joint_group_size: int = 1, use_param_program: bool = True, use_active_reset=False, show_progress_bar: bool = False) \ -> Dict[Tuple[int, ...], np.ndarray]: """ Measures the joint readout confusion matrix for all groups of size group_size among the qubits. Simultaneous readout of multiple qubits may exhibit correlations in readout error. Measuring a single qubit confusion for each qubit of interest is therefore not sufficient to completely characterize readout error in general. This method estimates joint readout confusion matrices for groups of qubits, which involves preparing all 2^joint_group_size possible bitstrings on each of (len(qubits) choose joint_group_size) many groups. The joint_group_size specifies which order of correlation you wish to characterize, and a separate confusion matrix is estimated for every possible group of that size. For example, a joint_group_size of one will simply estimate a single-qubit 2x2 confusion matrix for each of the provided qubits, similar to len(qubits) calls to estimate_confusion_matrix. Meanwhile, a joint_group_size of two will yield estimates of the (len(qubits) choose 2) many 4x4 confusion matrices for each possible pair of qubits. When the joint_group_size is the same as len(qubits) a single confusion matrix is estimated, which requires 2^len(qubits) number of state preparations. Note that the maximum number of measurements occurs when 1 << joint_group_size << len(qubits), and this maximum is quite large. Because this method performs the measurement of exponentially many bitstrings on a particular group of qubits, use_param_program should result in an appreciable speed up for estimation of each matrix by looping over bitstrings at a lower level of the stack, rather than creating a new program for each bitstring. Use_active_reset should also speed up estimation, at the cost of introducing more error and potentially complicating interpretation of results. :param qc: a quantum computer whose readout error you wish to characterize :param qubits: a list of accessible qubits on the qc you wish to characterize. Defaults to all qubits in qc. :param num_shots: number of shots in measurement of each bit string on each joint group of qubits. :param joint_group_size: the size of each group; a joint confusion matrix with 2^joint_group_size number of rows/columns will be estimated for each group of qubits of the given size within the qubit set. :param use_param_program: dictates whether to use a parameterized program to measure out each bitstring. If set to default of True, this routine should execute faster on a QPU. Note that the parameterized option does not execute a no-op when measuring 0. :param use_active_reset: if true, all qubits in qc (not just provided qubits) will be actively reset to 0 state at the start of each bitstring measurement. This option is intended as a potential speed up, but may complicate interpretation of the estimated confusion matrices. The method estimate_joint_active_reset_confusion separately characterizes active reset. :param show_progress_bar: displays a progress bar via tqdm if true. :return: a dictionary whose keys are all possible joint_group_sized tuples that can be formed from the qubits. Each value is an estimated 2^group_size square confusion matrix for the corresponding tuple of qubits. Each key is listed in order of increasing qubit number. The corresponding matrix has rows and columns indexed in increasing bitstring order, with most significant (leftmost) bit labeling the smallest qubit number. """ # establish default as all operational qubits in qc if qubits is None: qubits = qc.qubits() qubits = sorted(qubits) groups = list(itertools.combinations(qubits, joint_group_size)) confusion_matrices = {} total_num_rounds = len(groups) * 2**joint_group_size with tqdm(total=total_num_rounds, disable=not show_progress_bar) as pbar: for group in groups: # initialize program to optionally actively reset. Active reset will provide a speed up # but may make the estimate harder to interpret due to errors in the reset. program_start = Program() if use_active_reset: program_start += RESET() executable = None # will be re-assigned to either compiled param or non-param program # generate one parameterized program now, or later create a program for each bitstring if use_param_program: # generate parameterized program for this group and append to start param_program = program_start + _readout_group_parameterized_bitstring( group) param_program.wrap_in_numshots_loop(shots=num_shots) executable = qc.compiler.native_quil_to_executable( param_program) matrix = np.zeros((2**joint_group_size, 2**joint_group_size)) for row, bitstring in enumerate( itertools.product([0, 1], repeat=joint_group_size)): if use_param_program: # specify bitstring in parameterization at run-time results = qc.run( executable, memory_map={'target': [bit * pi for bit in bitstring]}) else: # generate program that measures given bitstring on group, and append to start bitstring_program = program_start + _readout_group_bitstring( group, bitstring) bitstring_program.wrap_in_numshots_loop(shots=num_shots) executable = qc.compiler.native_quil_to_executable( bitstring_program) results = qc.run(executable) # update confusion matrix for result in results: base = np.array( [2**i for i in reversed(range(joint_group_size))]) observed = np.sum(base * result) matrix[row, observed] += 1 / num_shots # update the progress bar pbar.update(1) # store completed confusion matrix in dictionary confusion_matrices[group] = matrix return confusion_matrices
def measure_quantum_volume(qc: QuantumComputer, qubits: Sequence[int] = None, program_generator: Callable[[QuantumComputer, Sequence[int], Sequence[np.ndarray], np.ndarray], Program] = _naive_program_generator, num_circuits: int = 100, num_shots: int = 1000, depths: np.ndarray = None, achievable_threshold: float = 2 / 3, stop_when_fail: bool = True, show_progress_bar: bool = False) \ -> Dict[int, Tuple[float, float]]: """ Measures the quantum volume of a quantum resource, as described in [QVol]_. By default this method scans increasing depths from 2 to len(qubits) and tests whether the qc can adequately implement random model circuits on depth-many qubits such that the given depth is 'achieved'. A model circuit depth is achieved if the sample distribution for a sample of num_circuits many randomly generated model circuits of the given depth sufficiently matches the ideal distribution of that circuit (See Eq. 6 of [QVol]_). The frequency of sampling 'heavy-outputs' is used as a measure of closeness of the circuit distributions. This estimated frequency (across all sampled circuits) is reported for each depth along with a bool which indicates whether that depth was achieved. The logarithm of the quantum volume is by definition the largest achievable depth of the circuit; see :func:`extract_quantum_volume_from_results` for obtaining the quantum volume from the results returned by this method. .. [QVol] Validating quantum computers using randomized model circuits. Cross et al. arXiv:1811.12926v1 (2018). https://arxiv.org/abs/1811.12926 :param qc: the quantum resource whose volume you wish to measure :param qubits: available qubits on which to act during measurement. Default all qubits in qc. :param program_generator: a method which 1) takes in a quantum computer, the qubits on that computer available for use, a series of sequences representing the qubit permutations in a model circuit, an array of matrices representing the 2q gates in the model circuit 2) outputs a native quil program that implements the circuit and measures the appropriate qubits in the order implicitly dictated by the model circuit representation created in sample_rand_circuits_for_heavy_out. The default option simply picks the smallest qubit labels and lets the compiler do the rest. :param num_circuits: number of unique random circuits that will be sampled. :param num_shots: number of shots for each circuit sampled. :param depths: the circuit depths to scan over. Defaults to all depths from 2 to len(qubits) :param achievable_threshold: threshold at which a depth is considered 'achieved'. Eq. 6 of [QVol]_ defines this to be the default of 2/3. To be considered achievable, the estimated probability of sampling a heavy output at the given depth must be large enough such that the one-sided confidence interval of this estimate is greater than the given threshold. :param stop_when_fail: if true, the measurement will stop after the first un-achievable depth :param show_progress_bar: displays a progress bar for each depth if true. :return: dict with key depth: (prob_sample_heavy, ons_sided_conf_interval) gives both the estimated probability of sampling a heavy output at each depth and the 2-sigma lower bound on this estimate; a depth qualifies as being achievable only if this lower bound exceeds the threshold, defined in [QVol]_ to be 2/3 """ if num_circuits < 100: warnings.warn( "The number of random circuits ran ought to be greater than 100 for results " "to be valid.") if qubits is None: qubits = qc.qubits() if depths is None: depths = np.arange(2, len(qubits) + 1) results = {} for depth in depths: log.info("Starting depth {}".format(depth)) # Use the program generator to implement random model circuits for this depth and compare # the outputs to the ideal simulations; get the count of the total number of heavy outputs num_heavy = sample_rand_circuits_for_heavy_out(qc, qubits, depth, program_generator, num_circuits, num_shots, show_progress_bar) prob_sample_heavy, one_sided_conf_intrvl = calculate_prob_est_and_err( num_heavy, num_circuits, num_shots) # prob of sampling heavy output must be large enough such that the one-sided confidence # interval is larger than the threshold is_achievable = one_sided_conf_intrvl > achievable_threshold results[depth] = (prob_sample_heavy, one_sided_conf_intrvl) if stop_when_fail and not is_achievable: break return results