Exemplo n.º 1
0
def test_group_experiments_greedy():
    ungrouped_tomo_expt = Experiment(
        [
            [
                ExperimentSetting(
                    _pauli_to_product_state(
                        PauliTerm.from_compact_str("(1+0j)*Z7Y8Z1Y4Z2Y5Y0X6")),
                    PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"),
                )
            ],
            [ExperimentSetting(plusZ(7), sY(1))],
        ],
        program=Program(H(0), H(1), H(2)),
    )
    grouped_tomo_expt = group_settings(ungrouped_tomo_expt, method="greedy")
    expected_grouped_tomo_expt = Experiment(
        [[
            ExperimentSetting(
                TensorProductState.from_str(
                    "Z0_7 * Y0_8 * Z0_1 * Y0_4 * Z0_2 * Y0_5 * Y0_0 * X0_6"),
                PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"),
            ),
            ExperimentSetting(plusZ(7), sY(1)),
        ]],
        program=Program(H(0), H(1), H(2)),
    )
    assert grouped_tomo_expt == expected_grouped_tomo_expt
Exemplo n.º 2
0
def test_merge_disjoint_experiments():
    sett1 = ExperimentSetting(TensorProductState(), sX(0) * sY(1))
    sett2 = ExperimentSetting(plusZ(1), sY(1))
    sett3 = ExperimentSetting(plusZ(0), sX(0))
    sett4 = ExperimentSetting(minusX(1), sY(1))
    sett5 = ExperimentSetting(TensorProductState(), sZ(2))

    expt1 = Experiment(settings=[sett1, sett2], program=Program(X(1)))
    expt2 = Experiment(settings=[sett3, sett4], program=Program(Z(0)))
    expt3 = Experiment(settings=[sett5], program=Program())

    merged_expt = merge_disjoint_experiments([expt1, expt2, expt3])
    assert len(merged_expt) == 2
Exemplo n.º 3
0
def group_settings_clique_removal(experiments: Experiment) -> Experiment:
    """
    Group experiments that are diagonal in a shared tensor product basis (TPB) to minimize number
    of QPU runs, using a graph clique removal algorithm.

    :param experiments: a tomography experiment
    :return: a tomography experiment with all the same settings, just grouped according to shared
        TPBs.
    """
    g = construct_tpb_graph(experiments)
    _, cliqs = clique_removal(g)
    new_cliqs: List[List[ExperimentSetting]] = []
    for cliq in cliqs:
        new_cliq: List[ExperimentSetting] = []
        for expt in cliq:
            # duplicate `count` times
            new_cliq += [expt] * g.nodes[expt]["count"]

        new_cliqs += [new_cliq]

    return Experiment(
        new_cliqs,
        program=experiments.program,
        symmetrization=experiments.symmetrization,
    )
Exemplo n.º 4
0
def merge_disjoint_experiments(
    experiments: List[Experiment], group_merged_settings: bool = True
) -> Experiment:
    """
    Merges the list of experiments into a single experiment that runs the sum of the individual
    experiment programs and contains all of the combined experiment settings.

    A group of Experiments whose programs operate on disjoint sets of qubits can be
    'parallelized' so that the total number of runs can be reduced after grouping the settings.
    Settings which act on disjoint sets of qubits can be automatically estimated from the same
    run on the quantum computer.

    If any experiment programs act on a shared qubit they cannot be thoughtlessly composed since
    the order of operations on the shared qubit may have a significant impact on the program
    behaviour; therefore we do not recommend using this method if this is the case.

    Even when the individual experiments act on disjoint sets of qubits you must be
    careful not to associate 'parallel' with 'simultaneous' execution. Physically the gates
    specified in a pyquil Program occur as soon as resources are available; meanwhile, measurement
    happens only after all gates. There is no specification of the exact timing of gates beyond
    their causal relationships. Therefore, while grouping experiments into parallel operation can
    be quite beneficial for time savings, do not depend on any simultaneous execution of gates on
    different qubits, and be wary of the fact that measurement happens only after all gates have
    finished.

    Note that to get the time saving benefits the settings must be grouped on the merged
    experiment--by default this is done before returning the experiment.

    :param experiments: a group of experiments to combine into a single experiment
    :param group_merged_settings: By default group the settings of the merged experiment.
    :return: a single experiment that runs the summed program and all settings.
    """
    used_qubits: Set[int] = set()
    for expt in experiments:
        if expt.program.get_qubits().intersection(used_qubits):
            raise ValueError(
                "Experiment programs act on some shared set of qubits and cannot be "
                "merged unambiguously."
            )
        used_qubits = used_qubits.union(cast(Set[int], expt.program.get_qubits()))

    # get a flat list of all settings, to be regrouped later
    all_settings = [
        setting for expt in experiments for simult_settings in expt for setting in simult_settings
    ]
    merged_program = sum([expt.program for expt in experiments], Program())
    merged_program.wrap_in_numshots_loop(max([expt.program.num_shots for expt in experiments]))

    symm_levels = [expt.symmetrization for expt in experiments]
    symm_level = max(symm_levels)
    if SymmetrizationLevel.EXHAUSTIVE in symm_levels:
        symm_level = SymmetrizationLevel.EXHAUSTIVE
    merged_expt = Experiment(all_settings, merged_program, symmetrization=symm_level)

    if group_merged_settings:
        merged_expt = group_settings(merged_expt)

    return merged_expt
Exemplo n.º 5
0
def test_max_tpb_overlap_2():
    expt_setting = ExperimentSetting(
        _pauli_to_product_state(
            PauliTerm.from_compact_str("(1+0j)*Z7Y8Z1Y4Z2Y5Y0X6")),
        PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"),
    )
    p = Program(H(0), H(1), H(2))
    tomo_expt = Experiment([expt_setting], p)
    expected_dict = {expt_setting: [expt_setting]}
    assert expected_dict == _max_tpb_overlap(tomo_expt)
Exemplo n.º 6
0
    def calibrate(self, experiment: Experiment) -> List[ExperimentResult]:
        """
        Perform readout calibration on the various multi-qubit observables involved in the provided
        ``Experiment``.

        :param experiment: The ``Experiment`` to calibrate readout error for.
        :return: A list of ``ExperimentResult`` objects that contain the expectation values that
            correspond to the scale factors resulting from symmetric readout error.
        """
        calibration_experiment = experiment.generate_calibration_experiment()
        return self.experiment(calibration_experiment)
Exemplo n.º 7
0
def test_max_tpb_overlap_3():
    # add another ExperimentSetting to the above
    expt_setting = ExperimentSetting(
        _pauli_to_product_state(
            PauliTerm.from_compact_str("(1+0j)*Z7Y8Z1Y4Z2Y5Y0X6")),
        PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"),
    )
    expt_setting2 = ExperimentSetting(plusZ(7), sY(1))
    p = Program(H(0), H(1), H(2))
    tomo_expt2 = Experiment([expt_setting, expt_setting2], p)
    expected_dict2 = {expt_setting: [expt_setting, expt_setting2]}
    assert expected_dict2 == _max_tpb_overlap(tomo_expt2)
Exemplo n.º 8
0
def group_settings_greedy(tomo_expt: Experiment) -> Experiment:
    """
    Greedy method to group ExperimentSettings in a given Experiment

    :param tomo_expt: Experiment to group ExperimentSettings within
    :return: Experiment, with grouped ExperimentSettings according to whether
        it consists of PauliTerms diagonal in the same tensor product basis
    """
    diag_sets = _max_tpb_overlap(tomo_expt)
    grouped_expt_settings_list = list(diag_sets.values())
    grouped_tomo_expt = Experiment(grouped_expt_settings_list,
                                   program=tomo_expt.program)
    return grouped_tomo_expt
Exemplo n.º 9
0
def test_group_experiments(grouping_method):
    expts = [  # cf above, I removed the inner nesting. Still grouped visually
        ExperimentSetting(TensorProductState(),
                          sX(0) * sI(1)),
        ExperimentSetting(TensorProductState(),
                          sI(0) * sX(1)),
        ExperimentSetting(TensorProductState(),
                          sZ(0) * sI(1)),
        ExperimentSetting(TensorProductState(),
                          sI(0) * sZ(1)),
    ]
    suite = Experiment(expts, Program())
    grouped_suite = group_settings(suite, method=grouping_method)
    assert len(suite) == 4
    assert len(grouped_suite) == 2
Exemplo n.º 10
0
def test_max_tpb_overlap_1():
    tomo_expt_settings = [
        ExperimentSetting(plusZ(1) * plusX(0),
                          sY(2) * sY(1)),
        ExperimentSetting(plusX(2) * plusZ(1),
                          sY(2) * sZ(0)),
    ]
    tomo_expt_program = Program(H(0), H(1), H(2))
    tomo_expt = Experiment(tomo_expt_settings, tomo_expt_program)
    expected_dict = {
        ExperimentSetting(
            plusX(0) * plusZ(1) * plusX(2),
            sZ(0) * sY(1) * sY(2)): [
            ExperimentSetting(plusZ(1) * plusX(0),
                              sY(2) * sY(1)),
            ExperimentSetting(plusX(2) * plusZ(1),
                              sY(2) * sZ(0)),
        ]
    }
    assert expected_dict == _max_tpb_overlap(tomo_expt)
Exemplo n.º 11
0
    def experiment(
        self,
        experiment: Experiment,
        memory_map: Optional[Mapping[str, Sequence[Union[int, float]]]] = None,
    ) -> List[ExperimentResult]:
        """
        Run an ``Experiment`` on a QVM or QPU backend. An ``Experiment`` is composed of:

            - A main ``Program`` body (or ansatz).
            - A collection of ``ExperimentSetting`` objects, each of which encodes a particular
              state preparation and measurement.
            - A ``SymmetrizationLevel`` for enacting different readout symmetrization strategies.
            - A number of shots to collect for each (unsymmetrized) ``ExperimentSetting``.

        Because the main ``Program`` is static from run to run of an ``Experiment``, we can leverage
        our platform's Parametric Compilation feature. This means that the ``Program`` can be
        compiled only once, and the various alterations due to state preparation, measurement,
        and symmetrization can all be realized at runtime by providing a ``memory_map``. Thus, the
        steps in the ``experiment`` method are as follows:

            1. Generate a parameterized program corresponding to the ``Experiment``
                (see the ``Experiment.generate_experiment_program()`` method for more
                details on how it changes the main body program to support state preparation,
                measurement, and symmetrization).
            2. Compile the parameterized program into a parametric (binary) executable, which
                   contains declared variables that can be assigned at runtime.

            3. For each ``ExperimentSetting`` in the ``Experiment``, we repeat the following:

                a. Build a collection of memory maps that correspond to the various state
                   preparation, measurement, and symmetrization specifications.
                b. Run the parametric executable on the QVM or QPU backend, providing the memory map
                   to assign variables at runtime.
                c. Extract the desired statistics from the classified bitstrings that are produced
                   by the QVM or QPU backend, and package them in an ``ExperimentResult`` object.

            4. Return the list of ``ExperimentResult`` objects.

        This method is extremely useful shorthand for running near-term applications and algorithms,
        which often have this ansatz + settings structure.

        :param experiment: The ``Experiment`` to run.
        :param memory_map: A dictionary mapping declared variables / parameters to their values.
            The values are a list of floats or integers. Each float or integer corresponds to
            a particular classical memory register. The memory map provided to the ``experiment``
            method corresponds to variables in the main body program that we would like to change
            at runtime (e.g. the variational parameters provided to the ansatz of the variational
            quantum eigensolver).
        :return: A list of ``ExperimentResult`` objects containing the statistics gathered
            according to the specifications of the ``Experiment``.
        """
        experiment_program = experiment.generate_experiment_program()
        executable = self.compile(experiment_program)

        if memory_map is None:
            memory_map = {}

        results = []
        for settings in experiment:
            if len(settings) > 1:
                raise ValueError("settings must be of length 1")
            setting = settings[0]

            qubits = cast(List[int], setting.out_operator.get_qubits())
            experiment_setting_memory_map = experiment.build_setting_memory_map(
                setting)
            symmetrization_memory_maps = experiment.build_symmetrization_memory_maps(
                qubits)
            merged_memory_maps = merge_memory_map_lists(
                [experiment_setting_memory_map], symmetrization_memory_maps)

            all_bitstrings = []
            for merged_memory_map in merged_memory_maps:
                final_memory_map = {**memory_map, **merged_memory_map}
                self.qam.reset()
                bitstrings = self.run(executable, memory_map=final_memory_map)

                if "symmetrization" in final_memory_map:
                    bitmask = np.array(
                        np.array(final_memory_map["symmetrization"]) / np.pi,
                        dtype=int)
                    bitstrings = np.bitwise_xor(bitstrings, bitmask)
                all_bitstrings.append(bitstrings)
            symmetrized_bitstrings = np.concatenate(all_bitstrings)

            joint_expectations = [experiment.get_meas_registers(qubits)]
            if setting.additional_expectations:
                joint_expectations += setting.additional_expectations
            expectations = bitstrings_to_expectations(
                symmetrized_bitstrings, joint_expectations=joint_expectations)

            means = cast(np.ndarray, np.mean(expectations, axis=0))
            std_errs = np.std(expectations, axis=0, ddof=1) / np.sqrt(
                len(expectations))

            joint_results = []
            for qubit_subset, mean, std_err in zip(joint_expectations, means,
                                                   std_errs):
                out_operator = PauliTerm.from_list([
                    (setting.out_operator[i], i) for i in qubit_subset
                ])
                s = ExperimentSetting(
                    in_state=setting.in_state,
                    out_operator=out_operator,
                    additional_expectations=None,
                )
                r = ExperimentResult(setting=s,
                                     expectation=mean,
                                     std_err=std_err,
                                     total_counts=len(expectations))
                joint_results.append(r)

            result = ExperimentResult(
                setting=setting,
                expectation=joint_results[0].expectation,
                std_err=joint_results[0].std_err,
                total_counts=joint_results[0].total_counts,
                additional_results=joint_results[1:],
            )
            results.append(result)

        return results