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

8) Intermediate measurements#

In this section, we will demonstrate the use of intermediate measurements and simple classical control. Ava is capable of the same operations in this regard as Qiskit, with a few additions.

The interface is contained within the Qiskit circuits without any additional steps:

  1. Add ClassicalRegisters to the circuit

  2. Include one of more measurements that write to classical bits

  3. Optionally add classically-controlled gates

  4. Customize settings in the config as desired

  5. Submit a job with the defined circuit and config.

from qiskit.circuit import Parameter
from qiskit import QuantumCircuit, ClassicalRegister
from qiskit.quantum_info import SparsePauliOp
from fermioniq.config.defaults import standard_config

# 1: Build the circuit
circuit = QuantumCircuit(2)
circuit.h(0)


# 2: Add a ClassicalRegister
c_reg1 = ClassicalRegister(1, name="c_reg1") # 1 bit
circuit.add_register(c_reg1)


# 3: Add a measurement that writes to the classical bit
circuit.measure(0, c_reg1[0]) # Write to the first (and only) bit


# 4: Add a classically controlled gate that reads from the register
circuit.h(1).c_if(c_reg1, 1) # Apply Hadamard to qubit 1 if the register contains 1


# 5: Customize the settings if required
config = standard_config(circuit)
config["trajectory"] = {
    "n_shots": 2, # Total maximum number of shots
    "target_probability_tol": 1e-6, # Total probability convergence tolerance
    "search_method": "dfs", # Trajectory exploration method
}


# 6: Submit a job and wait for optimization
emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config, remote_config="develop-cpu-2")
result = client.schedule_and_wait(emulator_job)

Trajectory based emulation

Due to their inherently quantum nature, quantums states probabilistically collapse when measured. This means that any intermediate measurement in a quantum circuit will impact the future state of the system. For this reason, a quantum circuit can have multiple possible trajectories. One might need to emulate the circuit many times to account for the different trajectories, in order to get representative statistics of the results.

Note that this is not the case when emulating circuits which only have measurements at the end. In these scenarios, we can sample the same final state multiple times when gathering bitstring statistics.

Every trajectory is uniquely defined by the intermediate measurement outcomes. Not all trajectories are equally likely, each one has a specific probability associated to it (although it is possible to construct circuits where every trajectory is equally probable).


Results

The results show the final bitstrings, classical register values and probability of each trajectory. Note that the basis states belonging to the same register output are sampled directly from the final matrix product state, rather than from full individual simulations. In this example, the trajectory c_reg1=1 yields an equal superposition of 01 and 11. For the trajectory c_reg1=0 however, we end up with the state 00.

The raw output data follows Qiskit’s convention, with a dict in the format {"xxx yy zz ...": p}, with xxx the qubit basis state, yy zz etc. the classical register values (in reverse order of the circuit) and p the probability.

# Full results table
rich.print(result)

# Extract optimizer data
print("Trajectories:", result.trajectories(circuit_number=0, run_number=0))

Config Settings

Here we give an overview of the different settings which can be specified in the “trajectory” section of the config.

  • n_shots: Maximum number of shots.
    The default is 1, meaning only one trajectory. Each trajectory is an individual simulation, so the overall simulation time will grow when this parameter is increased.

  • target_probability_tol (default 1e-3): Target probability convergence tolerance.
    The overall simulation will stop when the cumulative probability (of all visited trajectories) surpasses the threshold p >= 1 - target_probability_tol.

  • search_method (default dfs): The method used for exploring trajectories.
    When set to dfs, a depth-first search is performed. At each measurement, the outcome of maximal probability is chosen. Then the simulation backtracks through the trajectory, exploring alternative outcomes in a depth-first manner. NOTE: this method is only preferrable when the full number of trajectories (or at least the total number of trajectories with appreciable probability) is small. The alternative value is random, which does not constrain the simulation at all, exploring trajectories by sampling measurement outcomes weighed by their probabilities. This method is preferrable when the number of trajectories is large, and allows for unbiased sampling of trajectory outcomes”.

Below we show a slightly more complex example, with multiple measurements writing to the same register as well as multiple controlled gates reading from it:

from qiskit.circuit import Parameter
from qiskit import QuantumCircuit, ClassicalRegister
from qiskit.quantum_info import SparsePauliOp
from fermioniq.config.defaults import standard_config

# Build the circuit
circuit = QuantumCircuit(6)

# Randomize the first three qubits
circuit.h(0)
circuit.h(1)
circuit.h(2)

# Add a 3-bit classical register
c_reg1 = ClassicalRegister(3, name="c_reg1")
circuit.add_register(c_reg1)

# Write the qubit measurements of qubits 0-2 to the register
circuit.measure(0, c_reg1[0])
circuit.measure(1, c_reg1[1])
circuit.measure(2, c_reg1[2])

# Control the other three qubits 3-5 by the outcomes
# Note: the "value" of the register is equal to the integer
# representation of its classical bits, in accordance to
# Qiskit's conventions
circuit.x(3).c_if(c_reg1, 1)
circuit.x(4).c_if(c_reg1, 2)
circuit.z(5).c_if(c_reg1, 3)

# A few more gates
circuit.reset(0)
circuit.x(0)
circuit.z(3)
circuit.h(5)


# Customize the settings
config = standard_config(circuit)
config["trajectory"] = {
    "n_shots": 16, # Equal to the expected total number of trajectories
    "target_probability_tol": 1e-6, # Total probability convergence tolerance
    "search_method": "dfs", # Trajectory exploration method
}


# Submit a job and wait for optimization
emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config, remote_config="develop-cpu-2")
result = client.schedule_and_wait(emulator_job)

# Full results table
rich.print(result)