Skip to content

SAMPLED and EXACT coefficients in generate_qpd_weights: How they are both handled ? #757

@Cmurilochem

Description

@Cmurilochem

Dear colleagues.

Wanted to kindly request some clarification regarding the num_samples parameter and how the joint QPD is calculated/estimated within the code.

I understand that if num_samples >= 6^{Ms}, where Ms corresponds to, say, the number of space-like cuts, the resulting joint QPD and all its 6^{Ms} terms/subexperiments will be calculated. So, in principle, if the proper number of shots are considered per subexperiment, the final expectation value reconstructed from the QPD expansion is nearly exact.

However, in the case where num_samples < 6^{Ms}, the terms with probabilities < 1/ num_samples, according to the docstrings, are sampled by Monte Carlo, while the remaining terms are calculated with the "exact" coefficient.

I am interested in understanding this more in details, so I have created a simple example, see below:

import numpy as np

from qiskit import QuantumCircuit
from qiskit.providers import BackendV2 as Backend
from qiskit.quantum_info import SparsePauliOp
from qiskit.providers import BackendV2 as Backend
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_aer import AerSimulator

from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import Batch
from qiskit_ibm_runtime import Session

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_addon_cutting import (
    generate_cutting_experiments,
    partition_problem,
    reconstruct_expectation_values,
)
from qiskit_addon_cutting.qpd import generate_qpd_weights


def simple_quantum_circuit():
    """Fixture to create a simple Qiskit Circuit for testing."""
    qc = QuantumCircuit(2)
    qc.h(0)
    qc.cx(0, 1)
    qc.cx(0, 1)
    qc.h(0)
    return qc


def get_expectation_value_without_circuit_cutting(
    qc: QuantumCircuit,
    observable: SparsePauliOp,
    shots: int,
    backend: Backend,
) -> float:
    """Get the expectation value of a simple quantum circuit without cutting."""
    pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
    isa_circuit = pm.run(qc)
    isa_observable = observable.apply_layout(isa_circuit.layout)

    with Session(backend=backend) as session:
        estimator = Estimator(mode=session, options={"default_shots": shots})
        job = estimator.run([(isa_circuit, isa_observable)])

    results = job.result()
    return results[0].data.evs


if __name__ == "__main__":

    backend = AerSimulator(method="statevector")
    circuit = simple_quantum_circuit()
    observable = SparsePauliOp(["II", "IZ", "ZI", "ZZ"])
    shots = 2**14
    partition_labels = "AB"
    num_samples = 20


    # Partition the circuit into subcircuits based on the partition labels
    partitioned_problem = partition_problem(
        circuit=circuit,
        partition_labels=partition_labels,
        observables=observable.paulis,
    )

    # Extract subcircuits, observables, and QPD bases from the partitioned problem
    subcircuits = partitioned_problem.subcircuits
    subobservables = partitioned_problem.subobservables
    bases = partitioned_problem.bases


    # Prepare subexperiments and calculate joint QPD coefficients
    subexperiments, coefficients = generate_cutting_experiments(
        circuits=subcircuits, observables=subobservables, num_samples=num_samples
    )

    print("Joint QPD coefficients:")
    for i, coeff in enumerate(coefficients):
        print(f"  Coefficient {i}: {coeff}")

    #random_samples = generate_qpd_weights(bases, num_samples=num_samples)
    #print(random_samples)
    #print(sum([value[0] for value in random_samples.values()]))

    # Transpile the subexperiments to ISA circuits
    pass_manager = generate_preset_pass_manager(optimization_level=1, backend=backend)
    isa_subexperiments = {
        label: pass_manager.run(partition_subexpts)
        for label, partition_subexpts in subexperiments.items()
    }

    # Submit each partition's subexperiments to the Qiskit Runtime Sampler primitive, in a single batch so that the jobs will run back-to-back.
    with Batch(backend=backend) as batch:
        sampler = Sampler(
            mode=batch,
        )
        jobs = {
            label: sampler.run(subsystem_subexpts, shots=shots)
            for label, subsystem_subexpts in isa_subexperiments.items()
        }

    # Retrieve results
    results = {label: job.result() for label, job in jobs.items()}

    # Get expectation values for each observable term
    reconstructed_expvals = reconstruct_expectation_values(
        results,
        coefficients,
        subobservables,
    )

    # Reconstruct the total expectation value
    circuit_cut_expval = np.dot(reconstructed_expvals, observable.coeffs).real

    
    # Calculate expectation value without circuit cutting
    expected_expval = get_expectation_value_without_circuit_cutting(
        circuit, observable, shots, backend
    )
    
    print(f"Expectation value without circuit cutting: {expected_expval }")
    print(f"Expectation value with circuit cutting: {circuit_cut_expval }")

Note that, in my environment, I have installed, the following packages versions:

qiskit                                     2.0.1
qiskit-addon-cutting             0.10.0
qiskit-aer                                0.17.0
qiskit-ibm-runtime                 0.39.0

The output of the above run is:

Joint QPD coefficients:
  Coefficient 0: (np.float64(0.9), <WeightType.SAMPLED: 2>)
  Coefficient 1: (np.float64(0.9), <WeightType.SAMPLED: 2>)
  Coefficient 2: (np.float64(0.9), <WeightType.SAMPLED: 2>)
  Coefficient 3: (np.float64(-0.9), <WeightType.SAMPLED: 2>)
  Coefficient 4: (np.float64(0.9), <WeightType.SAMPLED: 2>)
  Coefficient 5: (np.float64(0.45), <WeightType.SAMPLED: 2>)
  Coefficient 6: (np.float64(-0.45), <WeightType.SAMPLED: 2>)
  Coefficient 7: (np.float64(-0.45), <WeightType.SAMPLED: 2>)
  Coefficient 8: (np.float64(0.45), <WeightType.SAMPLED: 2>)
  Coefficient 9: (np.float64(-0.45), <WeightType.SAMPLED: 2>)
  Coefficient 10: (np.float64(0.45), <WeightType.SAMPLED: 2>)
  Coefficient 11: (np.float64(-0.45), <WeightType.SAMPLED: 2>)
  Coefficient 12: (np.float64(0.45), <WeightType.SAMPLED: 2>)
  Coefficient 13: (np.float64(0.45), <WeightType.SAMPLED: 2>)
  Coefficient 14: (np.float64(-0.45), <WeightType.SAMPLED: 2>)

Expectation value without circuit cutting: 4.0
Expectation value with circuit cutting: 2.6943627610802654

As you can see, It looks like that 15-20 terms will be sampled from the joint QPD and calculated using the Sampler primitive. But:

  • How are the other <WeightType.EXACT: 1> coefficients are calculated ?
  • Where they are defined then ?
  • Are we just performing here a MC sampling out of the ~num_samples terms in the current version?

I would appreciate very much your guidance in this matter as this will certainly help other users as well.
Kind regards,
Carlos

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions