# SETUP (common to all tutorials)
import fermioniq
client = fermioniq.Client() # Initialize the client (assumes environment variables are set)

import rich # Module for pretty printing
import nest_asyncio # Necessary only for Jupyter, can be omitted in .py files
nest_asyncio.apply()
# Install cirq
!pip install cirq

6) Observables#

The Fermioniq emulator supports the computation of expectation values of general (local) observables. Observables can be specified via strings of Pauli operators, via:

  • Cirq’s PauliSum or PauliString classes (if using Cirq).

  • Qiskit’s SparsePauliOp class (if using Qiskit).

    • Note that operators in this class must technically act on all qubits, hence:

    • The number of qubits in the operator must equal the number of qubits in the emulator config.

    • Qubits are assumed to be in the order specified in the emulator config.

    To easily specify operators that act on all qubits, it is convenient to use the from_sparse_list() constructor of the SparsePauliOp class.

The code snippet below shows how to specify observables when using Qiskit.

from fermioniq.config.defaults import standard_config
from qiskit.quantum_info import SparsePauliOp
from qiskit import QuantumCircuit

def qiskit_bell_pair() -> QuantumCircuit:
    circuit = QuantumCircuit(2)
    circuit.h(0)
    circuit.cx(0, 1)
    return circuit

# Make a qiskit circuit
circuit = qiskit_bell_pair()

# Make a qiskit pauli operator that is the sum of the following two observables:
# -1 * X(qubit[0]) Z(qubit[1])
# 2 * Y(qubit[0]) Y(qubit[1])
# In from_sparse_list, the Pauli labels are paired with the qubit indices from left to right,
# so ("ZX", [1, 0], -1) means -1 * Z(qubit[1]) X(qubit[0]).
qiskit_pauli = SparsePauliOp.from_sparse_list([("ZX", [1, 0], -1), ("YY", [0, 1], 2)], num_qubits=circuit.num_qubits)

# Make a standard config
config = standard_config(circuit)

# Make an output config with the observable in it and put it into the config (under 'expectation_values')
output_config = {
    "expectation_values": {
        "enabled": True,
        "observables": {"-XZ + 2YY": qiskit_pauli},  # dict of observables and their corresponding names, for each observable, the expectation value will be computed
    }
}
config["output"] = output_config

# Make an emulator job with that output config
emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config)

rich.print(client.schedule_and_wait(emulator_job))

The code snippet below shows how to specify observables when using Cirq.

Note that, after creating the EmulatorJob instance, both configs essentially look the same, up to qubit labelling.

import cirq

# Create a Bell-pair circuit
circuit = cirq.Circuit()
circuit.append(cirq.H(cirq.GridQubit(0, 0)))
circuit.append(cirq.CNOT(cirq.GridQubit(0, 0), cirq.GridQubit(0, 1)))

# The default config uses the default cirq qubit order, obtained as follows
a, b = cirq.QubitOrder.DEFAULT.order_for(circuit.all_qubits())
# Note that the circuit.all_qubits() might return the qubits in a different order!

# Make cirq pauli operator
cirq_pauli = cirq.PauliSum.from_pauli_strings(
    [cirq.PauliString(-1, cirq.Z(b), cirq.X(a)), cirq.PauliString(2, cirq.Y(a), cirq.Y(b))]
)

# Make a standard config
config = standard_config(circuit)

# Make an output config with the observable in it and put it into the config
output_config = {
    "expectation_values": {
        "enabled": True,
        "observables": {"-XZ + 2YY": cirq_pauli},  # dict of observables and their corresponding names, for each observable, the expectation value will be computed
    }
}
config["output"] = output_config

# Make an emulator job with that output config (which serializes the config)
emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config)

rich.print(client.schedule_and_wait(emulator_job))

Reading out the expectation values#

Besides pretty-printing the whole result, the expectation values can be obtained programmatically from the JobResult returned by schedule_and_wait, via result.expectation_values(circuit_number, run_number). This returns a list of dictionaries with a name and an expval key.

Because a Bell pair is symmetric under exchanging its two qubits, the observables above would give the same answer even if the qubit order were reversed. The example below therefore uses a state in which the two qubits are not in the same state, so that each expectation value pins down which qubit an operator acts on.

# |psi> = |+> on qubit 0, |0> on qubit 1
circuit = QuantumCircuit(2)
circuit.h(0)

# Single-qubit observables, with their analytic expectation values in |psi>
observables = {
    "X0": SparsePauliOp.from_sparse_list([("X", [0], 1)], num_qubits=2),
    "X1": SparsePauliOp.from_sparse_list([("X", [1], 1)], num_qubits=2),
    "Z0": SparsePauliOp.from_sparse_list([("Z", [0], 1)], num_qubits=2),
    "Z1": SparsePauliOp.from_sparse_list([("Z", [1], 1)], num_qubits=2),
}
analytic = {"X0": 1.0, "X1": 0.0, "Z0": 0.0, "Z1": 1.0}

config = standard_config(circuit)
config["output"] = {"expectation_values": {"enabled": True, "observables": observables}}

result = client.schedule_and_wait(fermioniq.EmulatorJob(circuit=circuit, config=config))

for exp_val in result.expectation_values(circuit_number=0, run_number=0):
    name, value = exp_val["name"], complex(exp_val["expval"])
    print(f"<{name}> = {value.real:+.6f} (analytic: {analytic[name]:+.6f})")