# 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 MPS emulation routines available: TEBD and DMRG. This tutorial explains how to configure both routines.

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.

  • works for any type of circuit (long- and short-range gates);

  • built to be very fast on a GPU.

TEBD

Emulates the circuit gate-by-gate, where each gate is applied to the MPS, and then the MPS is truncated back to a maximum bond dimension max_D.

  • only works for circuits with gates that are nearest-neighbor from the perspective of the MPS (for long-range gates, SWAP gates need to be added manually);

  • can be a bit more memory efficient than DMRG (allowing to push the bond dimension higher), but doesn’t benefit much from GPU acceleration.

  • often achieves a worse fidelity for a given bond dimension than DMRG.

Unless there is a specific reason to use TEBD, we recommend using DRMG, especially when using a GPU backend.

We will start by explaining the DMRG configuration settings. TEBD will be explained at the end of this tutorial. Note that TEBD can also be used to do a statevector emulation.


DMRG

We’ll make a circuit that prepares the entangled state \(\frac{1}{\sqrt{2}}\left(|110\rangle + |011\rangle\right)\), and see what effect various config settings have on the circuit and the state that the emulator obtains.

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)

Now we’ll emulate it using the emulator with the default config.

from fermioniq.config.defaults import standard_config

# 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

TEBD

It is also possible to perform a TEBD emulation. This requires that all gates in the circuit are nearest-neighbor with respect to the qubit grouping, i.e. every gate should act on qubits that are in neighboring groups.

In particular, this means that TEBD only works with a manual grouping.

There are two configuration settings for TEBD: the maximum bond dimension max_D, and the svd_cutoff. After absorbing a gate into the MPS, a singular value decomposition is performed to truncate the MPS back to a maximum bond dimension. The number of singular values kept is the minimum of (i) max_D and (ii) the number of singular values that are more than a factor svd_cutoff of the largest singular value.

config = standard_config(circuit)
config["mode"] = "tebd"
config["tebd"] = {"max_D": 2, 'svd_cutoff': 1e-6}
config["grouping"] = [[qubits[0]], [qubits[1], qubits[2]]]

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

rich.print(client.schedule_and_wait(emulator_job))

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.

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

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

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

rich.print(client.schedule_and_wait(emulator_job))

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, and one fidelity per gate for TEBD) – 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.