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
Dear colleagues.
Wanted to kindly request some clarification regarding the
num_samplesparameter and how the joint QPD is calculated/estimated within the code.I understand that if
num_samples >= 6^{Ms}, whereMscorresponds to, say, the number of space-like cuts, the resulting joint QPD and all its6^{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 withprobabilities < 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:
Note that, in my environment, I have installed, the following packages versions:
The output of the above run is:
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:
<WeightType.EXACT: 1>coefficients are calculated ?~num_samplesterms 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