Skip to content

RFC: results and observables #229

@jon-wurtz

Description

@jon-wurtz

Once a kernel is executed using an interpreter, it must generate some data for analysis. There are only so many relevant data types, so we can start to lock in the basics. The big question is figuring out how to do observables-- returned quantum data lives in a basis hosted by qubits, which are possibly decontextualized from their definition within a kernel.

These results would come from execution of some job, aka

results = job.results()
results = context.run(kernel)
...

I propose thinking about extracting wavefunctions and other internal state as a "debug" mode. One can have a register, list of qubits, or wires as a return value, in which case an emulator can simply return the quantum state. But, the same piece of code should also be runnable on hardware, where you cannot return a state. So, I propose a debug dialect (like print statements one uses in python) that a simulator can use to log variables, such as classical or quantum variables.

@bloqade.kernel
def main():
    x = 1
    y = x + 1
    bloqade.debug.log(y,"message")
    reg = bloqade.circuit.qubits(2)
    bloqade.circuit.h(reg[0])
    bloqade.debug.log(reg,"register")

context = bloqade.runtime.debugger()
result = context.run(main) # {"message": 2, "register":reg:Wavefunction}

context = bloqade.runtime.statevector()
result = context.run(main) # wf:Wavefunction, the final wavefunction

There are two kinds of runtime values: ClassicalRuntimeValues, which are runtime values within the kernel, and QuantumRuntimeValues, which are wavefunctions, probability distributions or stochastic samples.

class ResultABC(ABC):
    """
    Base class for results to be fed into analysis.
    """

"""
Kernel Runtime Values are [classical] values that are instantiated
during execution of the kernel, and live in classical memory.
These can be returned when a kernel is run in a debug mode,
similar to how a print() statement is used, or when using a
debugger to step through a program to see the variables in context
"""
class ClassicalRuntimeValue(ResultABC):...   

"""
Kernel results are the things that are returned when running a kernel.
"""
@dataclass(frozen=True)
class KernelResult(ResultABC):
    _data: tuple[Any,...]
    @property
    def data(self)->tuple[Any,...]:
        return self._data
  
@dataclass(frozen=True) # Maybe just use qiskit / cirq etc.
class Qubit(ClassicalRuntimeValue):
    label: Any

@dataclass(frozen=True) # Maybe just use qiskit / cirq etc.
class Register(ClassicalRuntimeValue):
    qubits: list[Qubit]

"""
Quantum Runtime Values are quantum values that are instantiated
during execution of the kernel, and and live on the quantum processor.
These can be returned when a kernel is run in a debug mode,
similar to how a print() statement is used, or when using a
debugger to step through a program to see the variables in context.
"""
class QuantumRuntimeValue(ResultABC):...

@dataclass(frozen=True)
class ProbabilityFunction(QuantumRuntimeValue):
    _data: list[float]
    _basis:  list[Qubit]
    
    

@dataclass(frozen=True)
class BitstringResult(QuantumRuntimeValue):
    _data: list[list[int]]
    _basis:  list[list[Qubit]] | list[Qubit]
    
    
    def probability(self)->ProbabilityFunction:
        data = np.array(self._data)
        if len(data.shape)==2: # Number of qubits is consistent
            data = np.sum(data, axis=0)/data.shape[0]
            return ProbabilityFunction(_data = data, _basis = self._basis)
        else:
            raise ValueError("BitstringResult data is not a valid probability distribution")
            

@dataclass(frozen=True)
class Wavefunction(QuantumRuntimeValue):
    _data: np.ndarray
    _basis: list[Qubit]
    
    def sample(nsamples:int)->BitstringResult:
        """
        Sample from the wavefunction in the Z basis
        """
        raise NotImplementedError("Sampling has not been implemented")
    
    def apply(circuit:cirq.Circuit)->"Wavefunction":
        """
        Apply an abstract circuit to a wavefunction
        """
        raise NotImplementedError("Application of circuits has not been implemented")
    
    def expect(observable:Observable)->float:
        """
        Compute an expectation value with respect to some observable
        """
        
    def probability(self)->ProbabilityFunction:
        return ProbabilityFunction(_data = np.abs(self._data)**2, _basis = self._basis)
    
    def convert(self, target: qiskit.Wavefunction | cirq.Wavefunction | ...):
        """
        Convert the wavefunction to a different representation in another
        open source package.
        """



@dataclass(frozen=True)
class StochasticWavefunction(QuantumRuntimeValue):
    """
    A wavefunction sampled many times from different noise instantiations
    """
    _data: list[Wavefunction]
    
    def densitymatrix(self)->"DensityFunction":...


@dataclass(frozen=True)
class DensityFunction(QuantumRuntimeValue):
    _data:np.ndarray
    _basis:list[Qubit]

We can separate observables and expectation values, because they now are functions which QuantumRuntimeValue as input! This, of course, runs the risk of decoupling qubit labels with the statevector. There's no good way around this-- the statevector is not a true quantity of reality, so its on the user to manage matching qubit labels with the statevector. Nonetheless, the statevector should carry around its basis as an attribute; for 2^N statevector this can be as simple as a list of qubits, implicitly in the Z basis.

"""
Observables are tricky, as they require knowledge of the basis.
In principle this doesn't need any extra objects, by using functions
"""
 
# For example, observing X_2*Y_7 on qubit 3 could look like the following;
# observe that qubit3 and qubit7 have to be defined elsewhere
wf:Wavefunction = ...
qubit3:Qubit = ...
qubit7:Qubit = ...
wf.apply(cirq.X(qubit3)*cirq.Y(qubit7)) * wf # where * is a dot product

# This could have all sorts of happy helper functions, e.g.
def observable(observable:cirq.PauliString, wf:Wavefunction)->float:
    return wf.apply(observable) * wf
 
# Alternatively, observables could be their own object
class Observable:
    _paulis:str
    _qubits:list[Qubit]
    
    def expect(self,wavefunction:Wavefunction | StochasticWavefunction | DensityFunction)->float:
        return observable(self, wavefunction)


# A similar thing can apply to expectation values from data
def expectation_value(distribution:ProbabilityFunction, polynomial:list[tuple[float, list[Qubit]]])->float:
    """
    Compute the expectation value of a binary polynomial with respect to a probability distribution
    """
    raise NotImplementedError("Expectation value has not been implemented")

# Alternatively,
class BinaryPolynomial:
    _coefficients:list[float]
    _qubits:list[list[Qubit]]
    
    def expect(self, distribution:ProbabilityFunction)->float:
        return expectation_value(distribution, self._coefficients)
    
    def __plus__(self,other)->"BinaryPolynomial":...
    def __mul__(self,other)->"BinaryPolynomial":...
    def __sub__(self,other)->"BinaryPolynomial":...
    def __truediv__(self,other)->"BinaryPolynomial":...

This RFC can be seen as a MINIMAL working set for results. There can be much more here, such as helper functions that compute correlation functions, statistical distance, fidelity, and so forth. But they ultimately act on these basic classes. I am not sure if this fits the requirements of error correction-- maybe we can hash that out soon.

A key point to make here is that these data, when linked appropriately with an interpreter, can be part of the building blocks of much larger packages and code. These structures are the use case! I'm being intentionally vague as to how the interpreters generate these data; that's more of an implementation question. But, having a robust way to do emulations and simulation is critical... to be discussed...

Metadata

Metadata

Assignees

No one assigned

    Labels

    rfcRequest for Comments

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions