Basic Usage and Composability

To briefly demonstrate the usage of computables, we compute the variance of a Hamiltonian given a quantum state. First, we load the necessary modules and data to acquire a Hamiltonian and generate an ansatz.

from inquanto.ansatzes import TrotterAnsatz
from inquanto.core import SymbolDict
from inquanto.express import load_h5
from inquanto.operators import QubitOperatorList
from inquanto.states import QubitState

# Load H2 Hamiltonian and convert to qubit basis
h2 = load_h5("h2_sto3g.h5", as_tuple=True)
qubit_hamiltonian = h2.hamiltonian_operator.qubit_encode().hermitian_part()

exponents = QubitOperatorList.from_string("theta [(1j, Y0 X1 X2 X3)]")
reference = QubitState([1, 1, 0, 0])
ansatz = TrotterAnsatz(exponents, reference)
parameters = SymbolDict(theta=-0.41)

We want to compute the variance \(\text{Var} = \langle H^2 \rangle - \langle H \rangle^2\) where \(H\) is the Hamiltonian. We build this using the atomic computable ExpectationValue and the primitive ComputableFunction. At the construction of the variance computable, the actual value of the expectation value is not available. The ComputableFunction class allows us to build a new computable representing the variance.

from inquanto.computables import ExpectationValue
from inquanto.computables.primitive import ComputableFunction

c_variance = ComputableFunction(lambda x, y: x - y,
                ExpectationValue(ansatz, qubit_hamiltonian ** 2),
                ComputableFunction(lambda x: x ** 2, ExpectationValue(ansatz, qubit_hamiltonian))
            )

To evaluate the computable c_variance, an evaluator function is required. While this function typically entails quantum measurements, it is possible to define a simpler custom evaluator function for demonstration purposes. Furthermore, utilizing a custom evaluator can give you greater control over the evaluation process. Given that the variance expression is constructed on the top of the atomic ExpectationValue class, we need an evaluator function capable of calculating the expectation value:

def my_evaluator_function(computable):
    if isinstance(computable, ExpectationValue):
        v = computable.state.get_numeric_representation(parameters)
        return computable.kernel.state_expectation(v).real
    return computable

The ExpectationValue is a simple dataclass, which stores the state and kernel operator as attributes. If my_evaluator_function is called on a computable which is an atomic ExpectationValue, it computes the expectation value via a simple statevector method. With the evaluator function we can evaluate the variance expression as follows:

print(c_variance.evaluate(evaluator=my_evaluator_function))
0.23089952010568593

During the evaluation process of c_variance, the my_evaluator_function function will be invoked on all atomic computables to obtain their evaluated values. In this example, there are two expectation values to be computed. After the values for the atomic computables have been calculated, the final value of the expression is also computed and returned by the root level evaluate() method.

One might notice that certain portions of this calculation are redundant; fortunately, it is possible to make a my_evaluator_function that can further optimize the evaluation process, for instance, by caching the results of computable.state.get_numeric_representation(parameters). The advantage of using a computable expression is that the details of the optimization of the evaluation process are separated from the actual physical quantities calculated, in this case from the variance.