PennyLaneAI / pennylane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
https://pennylane.ai
Apache License 2.0
2.17k stars 568 forks source link

[BUG][QUESTION] Cutting: Correctness of `qcut_processing_fn_sample`. #5897

Closed nathanieltornow closed 2 days ago

nathanieltornow commented 5 days ago

Expected behavior

Hi! I have a question regarding the two postprocessing functions of qcut:

Is qcut_processing_fn_mc with a postprocessing function fn the same as running qcut_processing_fn_sample and running np.mean([fn(s) for s in samples]) on the resulting samples? From the current documentation, their relationship is not entirely clear.

Further, the qcut_processing_fn_sample function seems to ignore the output of outgoing edges in the subcircuit. Is, therefore, qcut_processing_fn_sample producing "correct" samples?

Actual behavior

I expect that the following code should output the same expectation value for the ZZZ observable for the circuit, post-processed with the two mentioned postprocessing methods:

import pennylane as qml
from functools import partial
from pennylane import numpy as np

dev = qml.device("default.qubit", wires=2, shots=10000)

def fn(bitstring):
    # Sample Z expectation value
    return (-1) ** np.sum(bitstring)

@partial(qml.cut_circuit_mc, classical_processing_fn=fn)
@qml.qnode(dev)
def circuit(x):
    qml.RX(0.89, wires=0)
    qml.RY(0.5, wires=1)
    qml.RX(1.3, wires=2)

    qml.CNOT(wires=[0, 1])
    qml.WireCut(wires=1)
    qml.CNOT(wires=[1, 2])

    qml.RX(x, wires=0)
    qml.RY(0.7, wires=1)
    qml.RX(2.3, wires=2)
    return qml.sample(wires=[0, 1, 2])

x = 0.4
times = 5
results = [circuit(x) for _ in range(times)]
print(np.mean(results), np.std(results))

@qml.cut_circuit_mc
@qml.qnode(dev)
def circuit2(x):
    qml.RX(0.89, wires=0)
    qml.RY(0.5, wires=1)
    qml.RX(1.3, wires=2)

    qml.CNOT(wires=[0, 1])
    qml.WireCut(wires=1)
    qml.CNOT(wires=[1, 2])

    qml.RX(x, wires=0)
    qml.RY(0.7, wires=1)
    qml.RX(2.3, wires=2)
    return qml.sample(wires=[0, 1, 2])

results2 = []
for _ in range(times):
    samples = circuit2(x)
    results2.append(np.mean([fn(sample) for sample in samples]))

print(np.mean(results2), np.std(results2))

However, the output is

-0.26352 0.04056749437665579
-0.15024 0.010345549767895373

indicating that the sampling method fails to achieve the same (correct) result. (the correct result is -0.2981) Can we, therefore, say that qcut_processing_fn_sample is correct in reconstructing samples of the original circuit?

Additional information

No response

Source code

No response

Tracebacks

No response

System information

Name: PennyLane
Version: 0.36.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /home/nate/qvm/.venv/lib/python3.11/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info:           Linux-6.8.10-x86_64-with-glibc2.38
Python version:          3.11.8
Numpy version:           1.26.4
Scipy version:           1.12.0
Installed devices:
- default.clifford (PennyLane-0.36.0)
- default.gaussian (PennyLane-0.36.0)
- default.mixed (PennyLane-0.36.0)
- default.qubit (PennyLane-0.36.0)
- default.qubit.autograd (PennyLane-0.36.0)
- default.qubit.jax (PennyLane-0.36.0)
- default.qubit.legacy (PennyLane-0.36.0)
- default.qubit.tf (PennyLane-0.36.0)
- default.qubit.torch (PennyLane-0.36.0)
- default.qutrit (PennyLane-0.36.0)
- default.qutrit.mixed (PennyLane-0.36.0)
- null.qubit (PennyLane-0.36.0)
- lightning.qubit (PennyLane_Lightning-0.36.0)

Existing GitHub issues

nathanieltornow commented 4 days ago

To underline the issue with qcut_processing_fn_sample, here is another example, looking at how accurately the function reconstructs "perfect" samples:

import pennylane as qml
from functools import partial
from pennylane import numpy as np
from qiskit.quantum_info import hellinger_fidelity

shots = 10000
dev = qml.device("default.qubit", wires=2, shots=shots)

def samples_to_counts(samples):
    # Convert samples to a probability distribution
    distr = {}
    for sample in samples:
        sample = tuple(sample)
        if sample not in distr:
            distr[sample] = 0
        distr[sample] += 1
    return distr

@partial(qml.cut_circuit_mc)
@qml.qnode(dev)
def circuit(x):
    qml.RX(0.89, wires=0)
    qml.RY(0.5, wires=1)
    qml.RX(1.3, wires=2)

    qml.CNOT(wires=[0, 1])
    qml.WireCut(wires=1)
    qml.CNOT(wires=[1, 2])

    qml.RX(x, wires=0)
    qml.RY(0.7, wires=1)
    qml.RX(2.3, wires=2)
    return qml.sample(wires=[0, 1, 2])

x = 0.3

c1 = samples_to_counts(circuit(x))

dev2 = qml.device("default.qubit", wires=3, shots=shots)

@qml.qnode(dev2)
def circuit_base(x):
    qml.RX(0.89, wires=0)
    qml.RY(0.5, wires=1)
    qml.RX(1.3, wires=2)

    qml.CNOT(wires=[0, 1])
    qml.WireCut(wires=1)
    qml.CNOT(wires=[1, 2])

    qml.RX(x, wires=0)
    qml.RY(0.7, wires=1)
    qml.RX(2.3, wires=2)
    return qml.sample(wires=[0, 1, 2])

c2 = samples_to_counts(circuit_base(x))

print(hellinger_fidelity(c1, c2))
0.7794567323985822

This indicates that the sampling method is not completely off but still far away from a good (expected) fidelity of >0.99

trbromley commented 3 days ago

Hi @nathanieltornow, thanks for this question.

You're right, qcut_processing_fn_sample will not provide samples following the probability distribution of the original uncut circuit. Instead, it simply outputs samples that are stitched together from samples of the cut-up circuit fragments. The hope is that these samples might be some approximation to the distribution of the uncut circuit, but I don't recall any results that show this.

Instead, qcut_processing_fn_mc uses the approach described in Appendix IV of Peng et al. that combines the terminal and mid-circuit samples together to produce a faithful estimate of the expectation value from the uncut circuit. This is the postprocessing function we'd recommend to use, but we decided to provide both.

Note that both of these postprocessing functions can be used as part of cut_circuit_mc, so we recommend passing a classical_processing_fn here.