Example #1
0
def test_is_identity():
    pt1 = -1.5j * sI(2)
    pt2 = 1.5 * sX(1) * sZ(2)

    assert is_identity(pt1)
    assert is_identity(pt2 + (-1 * pt2) + sI(0))
    assert not is_identity(0 * pt1)
    assert not is_identity(pt2 + (-1 * pt2))
Example #2
0
def _commutes(p1, p2):
    # Identity commutes with anything
    if is_identity(p1) or is_identity(p2):
        return True

    # Operators acting on different qubits commute
    if len(set(p1.get_qubits()) & set(p2.get_qubits())) == 0:
        return True

    # Otherwise, they must be the same thing modulo coefficient
    return p1.id() == p2.id()
Example #3
0
def test_check_commutation_rigorous():
    # more rigorous test.  Get all operators in Pauli group
    p_n_group = ("I", "X", "Y", "Z")
    pauli_list = list(product(p_n_group, repeat=3))
    pauli_ops = [list(zip(x, range(3))) for x in pauli_list]
    pauli_ops_pq = [reduce(mul, (PauliTerm(*x) for x in op)) for op in pauli_ops]

    non_commuting_pairs = []
    commuting_pairs = []
    for x in range(len(pauli_ops_pq)):
        for y in range(x, len(pauli_ops_pq)):

            tmp_op = _commutator(pauli_ops_pq[x], pauli_ops_pq[y])
            assert len(tmp_op.terms) == 1
            if is_identity(tmp_op.terms[0]):
                commuting_pairs.append((pauli_ops_pq[x], pauli_ops_pq[y]))
            else:
                non_commuting_pairs.append((pauli_ops_pq[x], pauli_ops_pq[y]))

    # now that we have our sets let's check against our code.
    for t1, t2 in non_commuting_pairs:
        assert not check_commutation([t1], t2)

    for t1, t2 in commuting_pairs:
        assert check_commutation([t1], t2)
Example #4
0
    def apply_clifford_to_pauli(self, clifford, pauli_in):
        r"""
        Given a circuit that consists only of elements of the Clifford group,
        return its action on a PauliTerm.

        In particular, for Clifford C, and Pauli P, this returns the PauliTerm
        representing CPC^{\dagger}.

        :param Program clifford: A Program that consists only of Clifford operations.
        :param PauliTerm pauli_in: A PauliTerm to be acted on by clifford via conjugation.
        :return: A PauliTerm corresponding to clifford * pauli_in * clifford^{\dagger}
        """
        # do nothing if `pauli_in` is the identity
        if is_identity(pauli_in):
            return pauli_in

        indices_and_terms = list(zip(*list(pauli_in.operations_as_set())))

        payload = ConjugateByCliffordRequest(
            clifford=clifford.out(),
            pauli=rpcq.messages.PauliTerm(indices=list(indices_and_terms[0]),
                                          symbols=list(indices_and_terms[1])))
        response: ConjugateByCliffordResponse = self.client.call(
            'conjugate_pauli_by_clifford', payload)
        phase_factor, paulis = response.phase, response.pauli

        pauli_out = PauliTerm("I", 0, 1.j**phase_factor)
        clifford_qubits = clifford.get_qubits()
        pauli_qubits = pauli_in.get_qubits()
        all_qubits = sorted(set(pauli_qubits).union(set(clifford_qubits)))
        # The returned pauli will have specified its value on all_qubits, sorted by index.
        #  This is maximal set of qubits that can be affected by this conjugation.
        for i, pauli in enumerate(paulis):
            pauli_out *= PauliTerm(pauli, all_qubits[i])
        return pauli_out * pauli_in.coefficient
Example #5
0
def _pauli_to_product_state(in_state: PauliTerm) -> TensorProductState:
    """
    Convert a Pauli term to a TensorProductState.
    """
    if is_identity(in_state):
        return TensorProductState()
    else:
        return TensorProductState([
            _OneQState(label=pauli_label, index=0, qubit=cast(int, qubit))
            for qubit, pauli_label in in_state._ops.items()
        ])
Example #6
0
def remove_identity(psum):
    """
    Remove the identity term from a Pauli sum

    :param psum:
    :return:
    """
    new_psum = []
    identity_terms = []
    for term in psum:
        if not is_identity(term):
            new_psum.append(term)
        else:
            identity_terms.append(term)
    return sum(new_psum), sum(identity_terms)
def remove_identity(psum):
    """
    Remove the identity term from a Pauli sum

    :param PauliSum psum: PauliSum object to remove identity
    :return: The new pauli sum and the identity term.
    """
    new_psum = []
    identity_terms = []
    for term in psum:
        if not is_identity(term):
            new_psum.append(term)
        else:
            identity_terms.append(term)
    return sum(new_psum), sum(identity_terms)
    def __init__(self, in_state: TensorProductState, out_operator: PauliTerm):
        # For backwards compatibility, handle in_state specified by PauliTerm.
        if isinstance(in_state, PauliTerm):
            warnings.warn("Please specify in_state as a TensorProductState",
                          DeprecationWarning, stacklevel=2)

            if is_identity(in_state):
                in_state = TensorProductState()
            else:
                in_state = TensorProductState([
                    _OneQState(label=pauli_label, index=0, qubit=qubit)
                    for qubit, pauli_label in in_state._ops.items()
                ])

        object.__setattr__(self, 'in_state', in_state)
        object.__setattr__(self, 'out_operator', out_operator)
Example #9
0
def remove_identity(
    psum: PauliSum
) -> Tuple[Union[PauliSum, PauliTerm], Union[float, PauliSum, PauliTerm]]:
    """
    Remove the identity term from a Pauli sum.

    :param psum: The PauliSum to process.
    :return: The sums of the non-identity and identity PauliSums.
    """
    new_psum = []
    identity_terms = []
    for term in psum:
        if not is_identity(term):
            new_psum.append(term)
        else:
            identity_terms.append(term)
    return sum(new_psum), sum(identity_terms)
Example #10
0
def _sic_process_tomo_settings(qubits: Sequence[int]):
    """Yield settings over SIC basis cross I,X,Y,Z operators

    Used as a helper function for generate_process_tomography_experiment

    :param qubits: The qubits to tomographize.
    """
    for in_sics in itertools.product([SIC0, SIC1, SIC2, SIC3], repeat=len(qubits)):
        i_state = functools.reduce(mul, (state(q) for state, q in zip(in_sics, qubits)),
                                   TensorProductState())
        for o_ops in itertools.product([sI, sX, sY, sZ], repeat=len(qubits)):
            o_op = functools.reduce(mul, (op(q) for op, q in zip(o_ops, qubits)), sI())

            if is_identity(o_op):
                continue

            yield ExperimentSetting(
                in_state=i_state,
                out_operator=o_op,
            )
Example #11
0
def _pauli_process_tomo_settings(qubits):
    """Yield settings over +-XYZ basis cross I,X,Y,Z operators

    Used as a helper function for generate_process_tomography_experiment

    :param qubits: The qubits to tomographize.
    """
    for states in itertools.product([plusX, minusX, plusY, minusY, plusZ, minusZ],
                                    repeat=len(qubits)):
        i_state = functools.reduce(mul, (state(q) for state, q in zip(states, qubits)),
                                   TensorProductState())
        for o_ops in itertools.product([sI, sX, sY, sZ], repeat=len(qubits)):
            o_op = functools.reduce(mul, (op(q) for op, q in zip(o_ops, qubits)), sI())

            if is_identity(o_op):
                continue

            yield ExperimentSetting(
                in_state=i_state,
                out_operator=o_op,
            )
def measure_observables(qc: QuantumComputer, tomo_experiment: TomographyExperiment,
                        n_shots: int = 10000, progress_callback=None, active_reset=False,
                        symmetrize_readout: str = 'exhaustive',
                        calibrate_readout: str = 'plus-eig'):
    """
    Measure all the observables in a TomographyExperiment.

    :param qc: A QuantumComputer which can run quantum programs
    :param tomo_experiment: A suite of tomographic observables to measure
    :param n_shots: The number of shots to take per ExperimentSetting
    :param progress_callback: If not None, this function is called each time a group of
        settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress
        is ``i / len(tomo_experiment)``.
    :param active_reset: Whether to actively reset qubits instead of waiting several
        times the coherence length for qubits to decay to |0> naturally. Setting this
        to True is much faster but there is a ~1% error per qubit in the reset operation.
        Thermal noise from "traditional" reset is not routinely characterized but is of the same
        order.
    :param symmetrize_readout: Method used to symmetrize the readout errors, i.e. set
        p(0|1) = p(1|0). For uncorrelated readout errors, this can be achieved by randomly
        selecting between the POVMs {X.D1.X, X.D0.X} and {D0, D1} (where both D0 and D1 are
        diagonal). However, here we currently support exhaustive symmetrization and loop through
        all possible 2^n POVMs {X/I . POVM . X/I}^n, and obtain symmetrization more generally,
        i.e. set p(00|00) = p(01|01) = .. = p(11|11), as well as p(00|01) = p(01|00) etc. If this
        is None, no symmetrization is performed. The exhaustive method can be specified by setting
        this variable to 'exhaustive' (default value). Set to `None` if no symmetrization is
        desired.
    :param calibrate_readout: Method used to calibrate the readout results. Currently, the only
        method supported is normalizing against the operator's expectation value in its +1
        eigenstate, which can be specified by setting this variable to 'plus-eig' (default value).
        The preceding symmetrization and this step together yield a more accurate estimation of the observable. Set to `None` if no calibration is desired.
    """
    # calibration readout only works with symmetrization turned on
    if calibrate_readout is not None and symmetrize_readout is None:
        raise ValueError("Readout calibration only works with readout symmetrization turned on")

    # Outer loop over a collection of grouped settings for which we can simultaneously
    # estimate.
    for i, settings in enumerate(tomo_experiment):

        log.info(f"Collecting bitstrings for the {len(settings)} settings: {settings}")

        # 1.1 Prepare a state according to the amalgam of all setting.in_state
        total_prog = Program()
        if active_reset:
            total_prog += RESET()
        max_weight_in_state = _max_weight_state(setting.in_state for setting in settings)
        for oneq_state in max_weight_in_state.states:
            total_prog += _one_q_state_prep(oneq_state)

        # 1.2 Add in the program
        total_prog += tomo_experiment.program

        # 1.3 Measure the state according to setting.out_operator
        max_weight_out_op = _max_weight_operator(setting.out_operator for setting in settings)
        for qubit, op_str in max_weight_out_op:
            total_prog += _local_pauli_eig_meas(op_str, qubit)

        # 2. Symmetrization
        qubits = max_weight_out_op.get_qubits()

        if symmetrize_readout == 'exhaustive' and len(qubits) > 0:
            bitstrings, d_qub_idx = _exhaustive_symmetrization(qc, qubits, n_shots, total_prog)

        elif symmetrize_readout is None and len(qubits) > 0:
            total_prog_no_symm = total_prog.copy()
            ro = total_prog_no_symm.declare('ro', 'BIT', len(qubits))
            d_qub_idx = {}
            for i, q in enumerate(qubits):
                total_prog_no_symm += MEASURE(q, ro[i])
                # Keep track of qubit-classical register mapping via dict
                d_qub_idx[q] = i
            total_prog_no_symm.wrap_in_numshots_loop(n_shots)
            total_prog_no_symm_native = qc.compiler.quil_to_native_quil(total_prog_no_symm)
            total_prog_no_symm_bin = qc.compiler.native_quil_to_executable(total_prog_no_symm_native)
            bitstrings = qc.run(total_prog_no_symm_bin)

        elif len(qubits) == 0:
            # looks like an identity operation
            pass

        else:
            raise ValueError("Readout symmetrization method must be either 'exhaustive' or None")

        if progress_callback is not None:
            progress_callback(i, len(tomo_experiment))

        # 3. Post-process
        # Inner loop over the grouped settings. They only differ in which qubits' measurements
        # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we
        # measure (n_shots, n_qubits=2) obs_strings then the full operator value involves selecting
        # either the first column, second column, or both and multiplying along the row.
        for setting in settings:
            # 3.1 Get the term's coefficient so we can multiply it in later.
            coeff = complex(setting.out_operator.coefficient)
            if not np.isclose(coeff.imag, 0):
                raise ValueError(f"{setting}'s out_operator has a complex coefficient.")
            coeff = coeff.real

            # 3.2 Special case for measuring the "identity" operator, which doesn't make much
            #     sense but should happen perfectly.
            if is_identity(setting.out_operator):
                yield ExperimentResult(
                    setting=setting,
                    expectation=coeff,
                    std_err=0.0,
                    total_counts=n_shots,
                )
                continue

            # 3.3 Obtain statistics from result of experiment
            obs_mean, obs_var = _stats_from_measurements(bitstrings, d_qub_idx, setting, n_shots, coeff)

            if calibrate_readout == 'plus-eig':
                # 4 Readout calibration
                # 4.1 Obtain calibration program
                calibr_prog = _calibration_program(qc, tomo_experiment, setting)
                # 4.2 Perform symmetrization on the calibration program
                if symmetrize_readout == 'exhaustive':
                    qubs_calibr = setting.out_operator.get_qubits()
                    calibr_shots = n_shots
                    calibr_results, d_calibr_qub_idx = _exhaustive_symmetrization(qc, qubs_calibr, calibr_shots, calibr_prog)

                else:
                    raise ValueError("Readout symmetrization method must be either 'exhaustive' or None")

                # 4.3 Obtain statistics from the measurement process
                obs_calibr_mean, obs_calibr_var = _stats_from_measurements(calibr_results, d_calibr_qub_idx, setting, calibr_shots)
                # 4.3 Calibrate the readout results
                corrected_mean = obs_mean / obs_calibr_mean
                corrected_var = ratio_variance(obs_mean, obs_var, obs_calibr_mean, obs_calibr_var)

                yield ExperimentResult(
                    setting=setting,
                    expectation=corrected_mean.item(),
                    std_err=np.sqrt(corrected_var).item(),
                    total_counts=n_shots,
                    raw_expectation=obs_mean.item(),
                    raw_std_err=np.sqrt(obs_var).item(),
                    calibration_expectation=obs_calibr_mean.item(),
                    calibration_std_err=np.sqrt(obs_calibr_var).item(),
                    calibration_counts=calibr_shots,
                )

            elif calibrate_readout is None:
                # No calibration
                yield ExperimentResult(
                    setting=setting,
                    expectation=obs_mean.item(),
                    std_err=np.sqrt(obs_var).item(),
                    total_counts=n_shots,
                )

            else:
                raise ValueError("Calibration readout method must be either 'plus-eig' or None")
Example #13
0
def measure_observables(
    qc: QuantumComputer,
    tomo_experiment: Experiment,
    n_shots: Optional[int] = None,
    progress_callback: Optional[Callable[[int, int], None]] = None,
    active_reset: Optional[bool] = None,
    symmetrize_readout: Optional[Union[int, str]] = "None",
    calibrate_readout: Optional[str] = "plus-eig",
    readout_symmetrize: Optional[str] = None,
) -> Generator[ExperimentResult, None, None]:
    """
    Measure all the observables in a TomographyExperiment.

    :param qc: A QuantumComputer which can run quantum programs
    :param tomo_experiment: A suite of tomographic observables to measure
    :param progress_callback: If not None, this function is called each time a group of
        settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress
        is ``i / len(tomo_experiment)``.
    :param calibrate_readout: Method used to calibrate the readout results. Currently, the only
        method supported is normalizing against the operator's expectation value in its +1
        eigenstate, which can be specified by setting this variable to 'plus-eig' (default value).
        The preceding symmetrization and this step together yield a more accurate estimation of
        the observable. Set to `None` if no calibration is desired.
    """
    shots = tomo_experiment.shots
    symmetrization = tomo_experiment.symmetrization
    reset = tomo_experiment.reset

    if n_shots is not None:
        warnings.warn(
            "'n_shots' has been deprecated; if you want to set the number of shots "
            "for this run of measure_observables please provide the number to "
            "Program.wrap_in_numshots_loop() for the Quil program that you provide "
            "when creating your TomographyExperiment object. For now, this value will "
            "override that in the TomographyExperiment, but eventually this keyword "
            "argument will be removed.",
            FutureWarning,
        )
        shots = n_shots
    else:
        if shots == 1:
            warnings.warn(
                "'n_shots' has been deprecated; if you want to set the number of shots "
                "for this run of measure_observables please provide the number to "
                "Program.wrap_in_numshots_loop() for the Quil program that you provide "
                "when creating your TomographyExperiment object. It looks like your "
                "TomographyExperiment object has shots = 1, so for now we will change "
                "that to 10000, which was the previous default value.",
                FutureWarning,
            )
            shots = 10000

    if active_reset is not None:
        warnings.warn(
            "'active_reset' has been deprecated; if you want to enable active qubit "
            "reset please provide a Quil program that has a RESET instruction in it when "
            "creating your TomographyExperiment object. For now, this value will "
            "override that in the TomographyExperiment, but eventually this keyword "
            "argument will be removed.",
            FutureWarning,
        )
        reset = active_reset

    if readout_symmetrize is not None and symmetrize_readout != "None":
        raise ValueError(
            "'readout_symmetrize' and 'symmetrize_readout' are conflicting keyword "
            "arguments -- please provide only one.")

    if readout_symmetrize is not None:
        warnings.warn(
            "'readout_symmetrize' has been deprecated; please provide the symmetrization "
            "level when creating your TomographyExperiment object. For now, this value "
            "will override that in the TomographyExperiment, but eventually this keyword "
            "argument will be removed.",
            FutureWarning,
        )
        symmetrization = SymmetrizationLevel(readout_symmetrize)

    if symmetrize_readout != "None":
        warnings.warn(
            "'symmetrize_readout' has been deprecated; please provide the symmetrization "
            "level when creating your TomographyExperiment object. For now, this value "
            "will override that in the TomographyExperiment, but eventually this keyword "
            "argument will be removed.",
            FutureWarning,
        )
        if symmetrize_readout is None:
            symmetrize_readout = SymmetrizationLevel.NONE
        elif symmetrize_readout == "exhaustive":
            symmetrize_readout = SymmetrizationLevel.EXHAUSTIVE
        symmetrization = SymmetrizationLevel(symmetrize_readout)

    # calibration readout only works with symmetrization turned on
    if calibrate_readout is not None and symmetrization != SymmetrizationLevel.EXHAUSTIVE:
        raise ValueError(
            "Readout calibration only currently works with exhaustive readout "
            "symmetrization turned on.")

    # generate programs for each group of simultaneous settings.
    programs, meas_qubits = _generate_experiment_programs(
        tomo_experiment, reset)

    for i, (prog, qubits,
            settings) in enumerate(zip(programs, meas_qubits,
                                       tomo_experiment)):
        log.info(
            f"Collecting bitstrings for the {len(settings)} settings: {settings}"
        )

        # we don't need to do any actual measurement if the combined operator is simply the
        # identity, i.e. weight=0. We handle this specially below.
        if len(qubits) > 0:
            # obtain (optionally symmetrized) bitstring results for all of the qubits
            bitstrings = qc.run_symmetrized_readout(prog, shots,
                                                    symmetrization, qubits)

        if progress_callback is not None:
            progress_callback(i, len(tomo_experiment))

        # Post-process
        # Inner loop over the grouped settings. They only differ in which qubits' measurements
        # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we
        # measure (shots, n_qubits=2) obs_strings then the full operator value involves selecting
        # either the first column, second column, or both and multiplying along the row.
        for setting in settings:
            # Get the term's coefficient so we can multiply it in later.
            coeff = setting.out_operator.coefficient
            assert isinstance(coeff, Complex)
            if not np.isclose(coeff.imag, 0):
                raise ValueError(
                    f"{setting}'s out_operator has a complex coefficient.")
            coeff = coeff.real

            # Special case for measuring the "identity" operator, which doesn't make much
            #     sense but should happen perfectly.
            if is_identity(setting.out_operator):
                yield ExperimentResult(setting=setting,
                                       expectation=coeff,
                                       std_err=0.0,
                                       total_counts=shots)
                continue

            # Obtain statistics from result of experiment
            obs_mean, obs_var = _stats_from_measurements(
                bitstrings, {q: idx
                             for idx, q in enumerate(qubits)}, setting, shots,
                coeff)

            if calibrate_readout == "plus-eig":
                # Readout calibration
                # Obtain calibration program
                calibr_prog = _calibration_program(qc, tomo_experiment,
                                                   setting)
                calibr_qubs = setting.out_operator.get_qubits()
                calibr_qub_dict = {
                    cast(int, q): idx
                    for idx, q in enumerate(calibr_qubs)
                }

                # Perform symmetrization on the calibration program
                calibr_results = qc.run_symmetrized_readout(
                    calibr_prog, shots, SymmetrizationLevel.EXHAUSTIVE,
                    calibr_qubs)

                # Obtain statistics from the measurement process
                obs_calibr_mean, obs_calibr_var = _stats_from_measurements(
                    calibr_results, calibr_qub_dict, setting, shots)
                # Calibrate the readout results
                corrected_mean = obs_mean / obs_calibr_mean
                corrected_var = ratio_variance(obs_mean, obs_var,
                                               obs_calibr_mean, obs_calibr_var)

                yield ExperimentResult(
                    setting=setting,
                    expectation=corrected_mean.item(),
                    std_err=np.sqrt(corrected_var).item(),
                    total_counts=len(bitstrings),
                    raw_expectation=obs_mean.item(),
                    raw_std_err=np.sqrt(obs_var).item(),
                    calibration_expectation=obs_calibr_mean.item(),
                    calibration_std_err=np.sqrt(obs_calibr_var).item(),
                    calibration_counts=len(calibr_results),
                )

            elif calibrate_readout is None:
                # No calibration
                yield ExperimentResult(
                    setting=setting,
                    expectation=obs_mean.item(),
                    std_err=np.sqrt(obs_var).item(),
                    total_counts=len(bitstrings),
                )

            else:
                raise ValueError(
                    "Calibration readout method must be either 'plus-eig' or None"
                )
Example #14
0
def test_identity_no_qubit():
    assert is_identity(sI())
Example #15
0
def measure_observables(qc: QuantumComputer,
                        tomo_experiment: TomographyExperiment,
                        n_shots: int = 1000,
                        progress_callback=None,
                        active_reset=False,
                        readout_symmetrize: str = None,
                        calibrate_readout: str = None):
    """
    Measure all the observables in a TomographyExperiment.

    :param qc: A QuantumComputer which can run quantum programs
    :param tomo_experiment: A suite of tomographic observables to measure
    :param n_shots: The number of shots to take per ExperimentSetting
    :param progress_callback: If not None, this function is called each time a group of
        settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress
        is ``i / len(tomo_experiment)``.
    :param active_reset: Whether to actively reset qubits instead of waiting several
        times the coherence length for qubits to decay to |0> naturally. Setting this
        to True is much faster but there is a ~1% error per qubit in the reset operation.
        Thermal noise from "traditional" reset is not routinely characterized but is of the same
        order.
    :param readout_symmetrize: Method used to symmetrize the readout errors, i.e. set
        p(0|1) = p(1|0). For uncorrelated readout errors, this can be achieved by randomly
        selecting between the POVMs {X.D1.X, X.D0.X} and {D0, D1} (where both D0 and D1 are
        diagonal). However, here we currently support exhaustive symmetrization and loop through
        all possible 2^n POVMs {X/I . POVM . X/I}^n, and obtain symmetrization more generally,
        i.e. set p(00|00) = p(01|01) = .. = p(11|11), as well as p(00|01) = p(01|00) etc. If this
        is None, no symmetrization is performed. The exhaustive method can be specified by setting
        this variable to 'exhaustive'
    :param calibrate_readout: Method used to calibrate the readout results. Currently, the only
        method supported is normalizing against the operator's expectation value in its +1
        eigenstate, which can be specified by setting this variable to 'plus-eig'. The preceding
        symmetrization and this step together yield a more accurate estimation of the observable.
    """
    # calibration readout only works with symmetrization turned on
    if calibrate_readout is not None and readout_symmetrize is None:
        raise ValueError(
            "Readout calibration only works with readout symmetrization turned on"
        )

    # Outer loop over a collection of grouped settings for which we can simultaneously
    # estimate.
    for i, settings in enumerate(tomo_experiment):

        log.info(
            f"Collecting bitstrings for the {len(settings)} settings: {settings}"
        )

        # 1.1 Prepare a state according to the amalgam of all setting.in_state
        total_prog = Program()
        if active_reset:
            total_prog += RESET()
        max_weight_in_state = _max_weight_state(setting.in_state
                                                for setting in settings)
        for oneq_state in max_weight_in_state.states:
            total_prog += _one_q_state_prep(oneq_state)

        # 1.2 Add in the program
        total_prog += tomo_experiment.program

        # 1.3 Measure the state according to setting.out_operator
        max_weight_out_op = _max_weight_operator(setting.out_operator
                                                 for setting in settings)
        for qubit, op_str in max_weight_out_op:
            total_prog += _local_pauli_eig_meas(op_str, qubit)

        if readout_symmetrize == 'exhaustive':
            # 1.4 Symmetrize -- flip qubits pre-measurement
            qubits = max_weight_out_op.get_qubits()
            n_shots_symm = n_shots // 2**len(qubits)
            list_bitstrings_symm = []
            for ops_bool in itertools.product([0, 1], repeat=len(qubits)):
                total_prog_symm = total_prog.copy()
                prog_symm = _ops_bool_to_prog(ops_bool, qubits)
                total_prog_symm += prog_symm
                # 2. Run the experiment
                bitstrings_symm = qc.run_and_measure(total_prog_symm,
                                                     n_shots_symm)
                # 2.1 Flip the results post-measurement
                d_flips_symm = {
                    qubits[i]: op_bool
                    for i, op_bool in enumerate(ops_bool)
                }
                for qubit, bs_results in bitstrings_symm.items():
                    bitstrings_symm[qubit] = bs_results ^ d_flips_symm.get(
                        qubit, 0)
                # 2.2 Gather together the symmetrized results into list
                list_bitstrings_symm.append(bitstrings_symm)

            # 2.3 Gather together all the symmetrized results
            bitstrings = reduce(_stack_dicts, list_bitstrings_symm)

        elif readout_symmetrize is None:
            # 2. Run the experiment
            bitstrings = qc.run_and_measure(total_prog, n_shots)

        else:
            raise ValueError(
                "Readout symmetrization method must be either 'exhaustive' or None"
            )

        if progress_callback is not None:
            progress_callback(i, len(tomo_experiment))

        # 3. Post-process
        # Inner loop over the grouped settings. They only differ in which qubits' measurements
        # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we
        # measure (n_shots, n_qubits=2) obs_strings then the full operator value involves selecting
        # either the first column, second column, or both and multiplying along the row.
        for setting in settings:
            # 3.1 Get the term's coefficient so we can multiply it in later.
            coeff = complex(setting.out_operator.coefficient)
            if not np.isclose(coeff.imag, 0):
                raise ValueError(
                    f"{setting}'s out_operator has a complex coefficient.")
            coeff = coeff.real

            # 3.2 Special case for measuring the "identity" operator, which doesn't make much
            #     sense but should happen perfectly.
            if is_identity(setting.out_operator):
                yield ExperimentResult(
                    setting=setting,
                    expectation=coeff,
                    stddev=0.0,
                    total_counts=n_shots,
                )
                continue

            # 3.3 Obtain statistics from result of experiment
            obs_mean, obs_var = _stats_from_measurements(
                bitstrings, setting, n_shots, coeff)

            if calibrate_readout == 'plus-eig':
                # 4 Readout calibration
                # 4.1 Prepare the +1 eigenstate for the out operator
                calibr_prog = Program()
                for q, op in setting.out_operator.operations_as_set():
                    calibr_prog += _one_q_pauli_prep(label=op,
                                                     index=0,
                                                     qubit=q)
                # 4.2 Measure the out operator in this state
                for q, op in setting.out_operator.operations_as_set():
                    calibr_prog += _local_pauli_eig_meas(op, q)
                calibr_shots = n_shots
                calibr_results = qc.run_and_measure(calibr_prog, calibr_shots)
                # 4.3 Obtain statistics from the measurement process
                obs_calibr_mean, obs_calibr_var = _stats_from_measurements(
                    calibr_results, setting, calibr_shots)
                # 4.4 Calibrate the readout results
                corrected_mean = obs_mean / obs_calibr_mean
                corrected_var = ratio_variance(obs_mean, obs_var,
                                               obs_calibr_mean, obs_calibr_var)

                yield ExperimentResult(
                    setting=setting,
                    expectation=corrected_mean.item(),
                    stddev=np.sqrt(corrected_var).item(),
                    total_counts=n_shots,
                    raw_expectation=obs_mean.item(),
                    raw_stddev=np.sqrt(obs_var).item(),
                    calibration_expectation=obs_calibr_mean.item(),
                    calibration_stddev=np.sqrt(obs_calibr_var).item(),
                    calibration_counts=calibr_shots,
                )

            elif calibrate_readout is None:
                # No calibration
                yield ExperimentResult(
                    setting=setting,
                    expectation=obs_mean.item(),
                    stddev=np.sqrt(obs_var).item(),
                    total_counts=n_shots,
                )

            else:
                raise ValueError(
                    "Calibration readout method must be either 'plus-eig' or None"
                )
Example #16
0
def measure_observables(qc: QuantumComputer,
                        tomo_experiment: TomographyExperiment,
                        n_shots=1000,
                        progress_callback=None,
                        active_reset=False):
    """
    Measure all the observables in a TomographyExperiment.

    :param qc: A QuantumComputer which can run quantum programs
    :param tomo_experiment: A suite of tomographic observables to measure
    :param n_shots: The number of shots to take per ExperimentSetting
    :param progress_callback: If not None, this function is called each time a group of
        settings is run with arguments ``f(i, len(tomo_experiment)`` such that the progress
        is ``i / len(tomo_experiment)``.
    :param active_reset: Whether to actively reset qubits instead of waiting several
        times the coherence length for qubits to decay to |0> naturally. Setting this
        to True is much faster but there is a ~1% error per qubit in the reset operation.
        Thermal noise from "traditional" reset is not routinely characterized but is of the same
        order.
    """
    for i, settings in enumerate(tomo_experiment):
        # Outer loop over a collection of grouped settings for which we can simultaneously
        # estimate.
        log.info(
            f"Collecting bitstrings for the {len(settings)} settings: {settings}"
        )

        # 1.1 Prepare a state according to the amalgam of all setting.in_state
        total_prog = Program()
        if active_reset:
            total_prog += RESET()
        max_weight_in_state = _max_weight_state(setting.in_state
                                                for setting in settings)
        for oneq_state in max_weight_in_state.states:
            total_prog += _one_q_state_prep(oneq_state)

        # 1.2 Add in the program
        total_prog += tomo_experiment.program

        # 1.3 Measure the state according to setting.out_operator
        max_weight_out_op = _max_weight_operator(setting.out_operator
                                                 for setting in settings)
        for qubit, op_str in max_weight_out_op:
            total_prog += _local_pauli_eig_meas(op_str, qubit)

        # 2. Run the experiment
        bitstrings = qc.run_and_measure(total_prog, n_shots)
        if progress_callback is not None:
            progress_callback(i, len(tomo_experiment))

        # 3. Post-process
        # 3.1 First transform bits to eigenvalues; ie (+1, -1)
        obs_strings = {q: 1 - 2 * bitstrings[q] for q in bitstrings}

        # Inner loop over the grouped settings. They only differ in which qubits' measurements
        # we include in the post-processing. For example, if `settings` is Z1, Z2, Z1Z2 and we
        # measure (n_shots, n_qubits=2) obs_strings then the full operator value involves selecting
        # either the first column, second column, or both and multiplying along the row.
        for setting in settings:
            # 3.2 Get the term's coefficient so we can multiply it in later.
            coeff = complex(setting.out_operator.coefficient)
            if not np.isclose(coeff.imag, 0):
                raise ValueError(
                    f"{setting}'s out_operator has a complex coefficient.")
            coeff = coeff.real

            # 3.3 Special case for measuring the "identity" operator, which doesn't make much
            #     sense but should happen perfectly.
            if is_identity(setting.out_operator):
                yield ExperimentResult(
                    setting=setting,
                    expectation=coeff,
                    stddev=0.0,
                    total_counts=n_shots,
                )
                continue

            # 3.4 Pick columns corresponding to qubits with a non-identity out_operation and stack
            #     into an array of shape (n_shots, n_measure_qubits)
            my_obs_strings = np.vstack(obs_strings[q]
                                       for q, op_str in setting.out_operator).T

            # 3.6 Multiply row-wise to get operator values. Do statistics. Yield result.
            obs_vals = coeff * np.prod(my_obs_strings, axis=1)
            obs_mean = np.mean(obs_vals)
            obs_var = np.var(obs_vals) / n_shots
            yield ExperimentResult(
                setting=setting,
                expectation=obs_mean.item(),
                stddev=np.sqrt(obs_var).item(),
                total_counts=n_shots,
            )