# 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()

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])
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
from cirq.ops.linear_combinations import PauliSum
from cirq.ops.pauli_string import PauliString

# 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 = PauliSum.from_pauli_strings(
    [PauliString(-1, cirq.Z(b), cirq.X(a)), 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))