def direct_fidelity_estimation(
    circuit: cirq.Circuit,
    qubits: list[cirq.Qid],
    sampler: cirq.Sampler,
    n_measured_operators: int | None,
    samples_per_term: int,
):
    """Perform a direct fidelity estimation.

    This implementation of direct fidelity estimation, is that of 'Direct Fidelity
    Estimation from Few Pauli Measurements' https://arxiv.org/abs/1104.4695 and
    'Practical characterization of quantum devices without tomography'
    https://arxiv.org/abs/1104.3835.

    Args:
        circuit: The circuit to run the simulation on.
        qubits: The list of qubits.
        sampler: Either a noisy simulator or an engine.
        n_measured_operators: The total number of Pauli measurements, or None to
            explore each Pauli state once.
        samples_per_term: if set to 0, we use the 'sampler' parameter above as
            a noise (must be of type cirq.DensityMatrixSimulator) and
            simulate noise in the circuit. If greater than 0, we instead use the
            'sampler' parameter directly to estimate the characteristic
            function.

    Returns:
        The estimated fidelity and a log of the run.

    Raises:
        TypeError: If the circuit is not made up entirely of Clifford gates.
    """
    # n_measured_operators is upper-case N in https://arxiv.org/abs/1104.3835

    # Number of qubits, lower-case n in https://arxiv.org/abs/1104.3835
    n_qubits = len(qubits)

    clifford_circuit = True
    clifford_tableau = cirq.CliffordTableau(n_qubits)
    try:
        for gate in circuit.all_operations():
            tableau_args = clifford.CliffordTableauSimulationState(
                tableau=clifford_tableau, qubits=qubits
            )
            cirq.act_on(gate, tableau_args)
    except TypeError:
        clifford_circuit = False

    # Computes for every \hat{P_i} of https://arxiv.org/abs/1104.3835
    # estimate rho_i and Pr(i). We then collect tuples (rho_i, Pr(i), \hat{Pi})
    # inside the variable 'pauli_traces'.
    if clifford_circuit:
        # The stabilizers_basis variable only contains basis vectors. For
        # example, if we have n=3 qubits, then we should have 2**n=8 Pauli
        # states that we can sample, but the basis will still have 3 entries. We
        # must flip a coin for each, whether or not to include them.
        stabilizer_basis: list[cirq.DensePauliString] = clifford_tableau.stabilizers()

        pauli_traces = _estimate_pauli_traces_clifford(
            n_qubits, stabilizer_basis, n_measured_operators
        )
    else:
        pauli_traces = _estimate_pauli_traces_general(qubits, circuit, n_measured_operators)

    p = np.asarray([x.Pr_i for x in pauli_traces])

    if n_measured_operators is None:
        # Since we enumerate all the possible traces, the probs should add to 1.
        assert np.isclose(np.sum(p), 1.0, atol=1e-6)
    p /= np.sum(p)

    fidelity = 0.0

    if samples_per_term == 0:
        # sigma in https://arxiv.org/abs/1104.3835
        if not isinstance(sampler, cirq.DensityMatrixSimulator):
            raise TypeError(
                'sampler is not a cirq.DensityMatrixSimulator but samples_per_term is zero.'
            )
        noisy_density_matrix = sampler.simulate(circuit).final_density_matrix

    if clifford_circuit and n_measured_operators is None:
        # In case the circuit is Clifford and we compute an exhaustive list of
        # Pauli traces, instead of sampling we can simply enumerate them because
        # they all have the same probability.
        measured_pauli_traces = pauli_traces
    else:
        # Otherwise, randomly sample as per probability.
        measured_pauli_traces = np.random.choice(
            pauli_traces, size=len(pauli_traces), p=p  # type: ignore[arg-type]
        ).tolist()

    trial_results: list[Result] = []
    for pauli_trace in measured_pauli_traces:
        measure_pauli_string: cirq.PauliString = pauli_trace.P_i
        rho_i = pauli_trace.rho_i

        if samples_per_term > 0:
            sigma_i = duet.run(
                estimate_characteristic_function,
                circuit,
                measure_pauli_string,
                sampler,
                samples_per_term,
            )
        else:
            sigma_i, _ = compute_characteristic_function(
                measure_pauli_string, qubits, noisy_density_matrix
            )

        trial_results.append(Result(pauli_trace=pauli_trace, sigma_i=sigma_i))

        fidelity += sigma_i / rho_i

    estimated_fidelity = fidelity / len(pauli_traces)

    std_dev_estimate: float | None
    std_dev_bound: float | None
    if clifford_circuit:
        std_dev_estimate, std_dev_bound = _estimate_std_devs_clifford(
            estimated_fidelity, len(measured_pauli_traces)
        )
    else:
        std_dev_estimate, std_dev_bound = None, None

    dfe_intermediate_result = DFEIntermediateResult(
        clifford_tableau=clifford_tableau if clifford_circuit else None,
        pauli_traces=pauli_traces,
        trial_results=trial_results,
        std_dev_estimate=std_dev_estimate,
        std_dev_bound=std_dev_bound,
    )

    return estimated_fidelity, dfe_intermediate_result
Perform a direct fidelity estimation.

    This implementation of direct fidelity estimation, is that of 'Direct Fidelity
    Estimation from Few Pauli Measurements' https://arxiv.org/abs/1104.4695 and
    'Practical characterization of quantum devices without tomography'
    https://arxiv.org/abs/1104.3835.

    Args:
        circuit: The circuit to run the simulation on.
        qubits: The list of qubits.
        sampler: Either a noisy simulator or an engine.
        n_measured_operators: The total number of Pauli measurements, or None to
            explore each Pauli state once.
        samples_per_term: if set to 0, we use the 'sampler' parameter above as
            a noise (must be of type cirq.DensityMatrixSimulator) and
            simulate noise in the circuit. If greater than 0, we instead use the
            'sampler' parameter directly to estimate the characteristic
            function.

    Returns:
        The estimated fidelity and a log of the run.

    Raises:
        TypeError: If the circuit is not made up entirely of Clifford gates.
    
examples/direct_fidelity_estimation.py