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

3) Emulation settings#

There are two emulation routines available: Statevector & DMRG. This tutorial explains the two routines, and how to configure them.

Statevector

The statevector mode provides exact emulation of a circuit. It uses the cuQuantum software packafe from Nvidia under the hood.

  • Usually quicker than DMRG for small circuits ( < 30 qubits)

  • Suffers from exponential scaling in the number of qubits, emulation of more than 33 qubits is not possible

  • Does not support the same advanced emulation features as DMRG (like noise models, VQE & intermediate measurements)

DMRG

Emulates the circuit by breaking it down into segments called ‘subcircuits’, and for each subcircuit finds an MPS (of a given bond dimension D) that best approximates the state obtained after applying that subcircuit to the MPS resulting from the previous subcircuit.

  • built to be very fast on a GPU.

  • Can emulate many more qubits than statevector, sometimes at the expense of accuracy.

We will start by explaining the Statevector configuration, which is much shorter, and then continue with the more advanced DMRG.


Circuit Setup

We’ll make a circuit that prepares the entangled state \(\frac{1}{\sqrt{2}}\left(|110\rangle + |011\rangle\right)\)

from qiskit import QuantumCircuit

circuit = QuantumCircuit(3)
circuit.h(0)
circuit.x(1)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(0, 1)

print(circuit)

Statevector simulation

It is also possible to do a full statevector simulation for up to 34 qubits (using Nvidia’s cuStateVec). To do this simply set the config mode to statevector. This mode does not have any more settings than this.

Note: Currently only pure-state emulations are supported for statevector. Mixed-state emulations coming soon.

from fermioniq.config.defaults import standard_config

config = standard_config(circuit)
config["mode"] = "statevector"

emulator_job = fermioniq.EmulatorJob(config=config, circuit=circuit)

rich.print(client.schedule_and_wait(emulator_job))

DMRG

The DMRG mode on the other hand has very many different configurations. In the rest of the tutorial, we will tweak these to demonstrate the effect they have on the emulation.

# Obtain a standard config for the circuit
config = standard_config(circuit)

# Give this config alongside the circuit to create an emulator job
emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config)

# Run the job and print the results
result = client.schedule_and_wait(emulator_job)
rich.print(result)

Bond dimension

First, we will investigate what effect changing the bond dimension of the emulation has. By default we run a ‘dmrg’-type emulation, and so we will change the maximum bond dimension allowed for dmrg, by modifying the "D" parameter of the "dmrg" config settings. We will set it to 1, so that only product states can be represented. This should change the output of the emulation for the circuit we created above.

config = standard_config(circuit)
dmrg_config = config["dmrg"]
dmrg_config["D"] = 1

# Run the circuit using these config settings
emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config)
rich.print(client.schedule_and_wait(emulator_job))

We obtain a final product state that is as close as we can get to the entangled state \(\frac{1}{\sqrt{2}}\left(|110\rangle + |011\rangle\right)\) with a product state. In the metadata output, you can see that the fidelity of the emulation is 0.5.

Increasing the bond dimension makes the emulation more accurate. In principle, for arbitrarily deep circuits, a bond dimension that is exponential in the number of qubits is needed to emulate the circuit exactly. However, especially for not too deep circuits, it often turns out that in practice a much smaller than exponentially-large bond dimension is enough to get a very good approximation of the final state of the circuit. This is the main strength of using tensor network states to emulate quantum circuits.


Grouping

The grouping determines which qubits are placed together in a single MPS tensor. Qubits that become strongly entangled should – ideally – not be too far from each other in the MPS; it is important to group qubits in a smart way. The standard_config function creates a (usually quite optimal) grouping by doing some simple analysis of the input circuit, creating groups of size 2 for pure state emulation, and groups of size 1 for mixed state emulation.

If we want the grouping algorithm to create groups of different sizes, then we can specify the group_size field of the config to specifiy how large we want the groups to be.

config = standard_config(circuit)

# Remove the automatically generated grouping from the config
del config["grouping"]

# Set the group size instead
config["group_size"] = 2

If both grouping and group_size are given, then an error will be raised upon validation.

It is also possible to input a manual grouping. Recall that if the input circuit is expected to entangle particular qubits, it makes sense to put them into the same group so that their entanglement structure can be preserved independently of the chosen bond dimension. Given that the circuit effectively generates a Bell-pair on qubits 0 and 2, it makes sense to group these together into a single group. Because qubit 1 is not entangled with qubits 0 and 2, we expect that a bond dimension of 1 should result in a fidelity 1 emulation. Let’s try it out.

config = standard_config(circuit)

dmrg_config = config["dmrg"]
dmrg_config["D"] = 1

qubits = circuit.qubits
config["grouping"] = [[qubits[0], qubits[2]], [qubits[1]]]

emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config)
rich.print(client.schedule_and_wait(emulator_job))

This allows us to recover the entangled state \(\frac{1}{\sqrt{2}}\left(|110\rangle + |011\rangle\right)\) with a ‘product state’, with a fidelity of 1. Note that for complicated circuits this will not always be possible, even if the final state is unentangled between groups. If qubits in different groups become entangled at some point during the circuit the emulator will not be able to recover the final state with a product state.


Initial state routine (per subcircuit)

For each subcircuit, the emulator starts with an intial guess for the MPS. There are several options here:

  • random uses a randomly initialized MPS;

  • ket uses the MPS from the previous subcircuit (this is the default setting);

  • lowerD uses the MPS obtained from a (previous) lower bond dimension emulation of the same circuit.

We advise to use ket over random.

To use the lowerD option, a list of bond dimensions \([D_1, D_2, \dots, D_k]\) should be provided at the start of the emulation. The emulator will then emulate the same circuit for each value of the bond dimension \(D_i\) given, starting from the lowest. When emulating the \(j\)-th subcircuit at bond dimension \(D_i\), the \(D_{i-1}\) MPS obtained at the \(j\)-th subcircuit will be used as an intial state for the \(D_i\) emulation of the \(j\)-th subcircuit.

When running the emulator with a list of bond dimensions (whether the initial guess is set to lowerD or not), the output will have a run_number specifying which bond dimension was used. In the example below, the run_number will run from 0 (bond dimension 2) to 3 (bond dimension 16).

config = standard_config(circuit)

dmrg_config = config["dmrg"]
dmrg_config["D"] = [2, 4,  8, 16]
dmrg_config["init_mode"] = "lowerD"

emulator_job = fermioniq.EmulatorJob(circuit=circuit, config=config)
rich.print(client.schedule_and_wait(emulator_job))

Convergence criterion

Given a subcircuit, the emulator iteratively updates the tensors in the MPS one-by-one, and after each update the fidelity between the MPS and the state that it tries to approximate (given by applying the subcircuit to the MPS resulting from the previous subcircuit) gets computed. The emulator will stop when one of the following convergence criteria has been reached:

  • target_fidelity between the MPS and the state that it tries to approximate is reached (default is 1.0);

  • the standard deviation of the last convergence_window_size (default is 2 * number of tensors in the MPS) fidelities computed goes below the convergence_threshold (default is 1e-5);

  • max_sweeps (default is 1e4) is reached, in which case the emulator has not converged.

Each of these parameters can be adjusted by adjusting the numbers below.

config = standard_config(circuit)

dmrg_config = config["dmrg"]
dmrg_config["target_fidelity"] = 1.0
dmrg_config["convergence_window_size"] = len(config["qubits"]) // config.get("group_size", 1)
dmrg_config["convergence_threshold"] = 1e-5
dmrg_config["max_sweeps"] = 1e4

Emulation metadata (output)

Recall that the result of schedule_and_wait() (or schedule_async) for each individual emulation contains a dictionary called metadata (see Tutorial 2). metadata is a dictionary that contains the following keys:

  • runtime: the runtime of the emulation;

  • fidelity_product: product of all fidelities, one for each approximation made (one fidelity per subcircuit for DMRG) – printed as ‘estimated fidelity’;

  • extrapolated_2qubit_gate_fidelity: obtained as the 1/n-th power of fidelity_product, where n is the number of two-qubit gates in the circuit* – printed as ‘estimated 2-qubit gate fidelity’;

  • num_subcircuits: (DMRG only) the number of subcircuits the circuit was decomposed into;

  • circuit_depth: the depth of the circuit;

  • number_2qubit_gates: number of two-qubit gates;

  • output_metadata: metadata on the computation of the emulation output (amplitudes, sampling, mps or expectation_values).

*The extrapolated_2qubit_gate_fidelity can be thought of as a measure for how much truncation has happened per two-qubit gate due to the fact that we are approximating the circuit emulation by a finite-bond-dimension emulation.