Quantum State Tomography

Quantum tomography is an experimental procedure to reconstruct a description of part of a quantum system from the measurement outcomes of a specific set of experiments. In particular, quantum state tomography reconstructs the density matrix of a quantum state by preparing the state many times and measuring them in a tomographically complete basis of measurement operators.

Note

This tutorial requires the qiskit-aer and qiskit-ibm-runtime packages to run simulations. You can install them with python -m pip install qiskit-aer qiskit-ibm-runtime.

We first initialize a simulator to run the experiments on.

from qiskit_aer import AerSimulator
from qiskit_ibm_runtime.fake_provider import FakePerth

backend = AerSimulator.from_backend(FakePerth())

To run a state tomography experiment, we initialize the experiment with a circuit to prepare the state to be measured. We can also pass in an Operator or a Statevector to describe the preparation circuit.

import qiskit
from qiskit_experiments.framework import ParallelExperiment
from qiskit_experiments.library import StateTomography

# GHZ State preparation circuit
nq = 2
qc_ghz = qiskit.QuantumCircuit(nq)
qc_ghz.h(0)
qc_ghz.s(0)
for i in range(1, nq):
    qc_ghz.cx(0, i)

# QST Experiment
qstexp1 = StateTomography(qc_ghz)
qstdata1 = qstexp1.run(backend, seed_simulation=100).block_for_results()

# Print results
for result in qstdata1.analysis_results():
    print(result)
AnalysisResult
- name: state
- value: DensityMatrix([[ 0.47475742+0.00000000e+00j,  0.0172249 -8.11458644e-03j,
                -0.01950902-1.15240387e-02j, -0.01991301-4.43469781e-01j],
               [ 0.0172249 +8.11458644e-03j,  0.02756347+0.00000000e+00j,
                -0.00629449+4.70004173e-03j,  0.01941185+1.18737176e-02j],
               [-0.01950902+1.15240387e-02j, -0.00629449-4.70004173e-03j,
                 0.02551699-4.33680869e-19j, -0.00617223+1.52182964e-02j],
               [-0.01991301+4.43469781e-01j,  0.01941185-1.18737176e-02j,
                -0.00617223-1.52182964e-02j,  0.47216212-3.46944695e-18j]],
              dims=(2, 2))
- quality: unknown
- extra: <9 items>
- device_components: ['Q0', 'Q1']
- verified: False
AnalysisResult
- name: state_fidelity
- value: 0.9169295522337089
- quality: unknown
- extra: <9 items>
- device_components: ['Q0', 'Q1']
- verified: False
AnalysisResult
- name: positive
- value: True
- quality: unknown
- extra: <9 items>
- device_components: ['Q0', 'Q1']
- verified: False

Tomography Results

The main result for tomography is the fitted state, which is stored as a DensityMatrix object:

state_result = qstdata1.analysis_results("state")
print(state_result.value)
DensityMatrix([[ 0.47475742+0.00000000e+00j,  0.0172249 -8.11458644e-03j,
                -0.01950902-1.15240387e-02j, -0.01991301-4.43469781e-01j],
               [ 0.0172249 +8.11458644e-03j,  0.02756347+0.00000000e+00j,
                -0.00629449+4.70004173e-03j,  0.01941185+1.18737176e-02j],
               [-0.01950902+1.15240387e-02j, -0.00629449-4.70004173e-03j,
                 0.02551699-4.33680869e-19j, -0.00617223+1.52182964e-02j],
               [-0.01991301+4.43469781e-01j,  0.01941185-1.18737176e-02j,
                -0.00617223-1.52182964e-02j,  0.47216212-3.46944695e-18j]],
              dims=(2, 2))

We can also visualize the density matrix:

from qiskit.visualization import plot_state_city
plot_state_city(qstdata1.analysis_results("state").value, title='Density Matrix')
../../_images/state_tomography_3_0.png

The state fidelity of the fitted state with the ideal state prepared by the input circuit is stored in the "state_fidelity" result field. Note that if the input circuit contained any measurements the ideal state cannot be automatically generated and this field will be set to None.

fid_result = qstdata1.analysis_results("state_fidelity")
print("State Fidelity = {:.5f}".format(fid_result.value))
State Fidelity = 0.91693

Additional state metadata

Additional data is stored in the tomography under the "state_metadata" field. This includes

  • eigvals: the eigenvalues of the fitted state

  • trace: the trace of the fitted state

  • positive: Whether the eigenvalues are all non-negative

  • positive_delta: the deviation from positivity given by 1-norm of negative eigenvalues.

If trace rescaling was performed this dictionary will also contain a raw_trace field containing the trace before rescaling. Futhermore, if the state was rescaled to be positive or trace 1 an additional field raw_eigvals will contain the state eigenvalues before rescaling was performed.

state_result.extra
{'trace': 1.0000000000000013,
 'eigvals': array([0.91846979, 0.05502092, 0.02650929, 0.        ]),
 'raw_eigvals': array([ 0.91951553,  0.05606665,  0.02755503, -0.0031372 ]),
 'rescaled_psd': True,
 'fitter_metadata': {'fitter': 'linear_inversion',
  'fitter_time': 0.0047760009765625},
 'conditional_probability': 1.0,
 'positive': True,
 'experiment': 'StateTomography',
 'run_time': None}

To see the effect of rescaling, we can perform a “bad” fit with very low counts:

# QST Experiment
bad_data = qstexp1.run(backend, shots=10, seed_simulation=100).block_for_results()
bad_state_result = bad_data.analysis_results("state")

# Print result
print(bad_state_result)

# Show extra data
bad_state_result.extra
AnalysisResult
- name: state
- value: DensityMatrix([[ 0.48242803+0.j        , -0.01222268-0.00990337j,
                 0.08081079-0.01036926j, -0.1246342 -0.38027643j],
               [-0.01222268+0.00990337j,  0.04048011+0.j        ,
                 0.01729528+0.00254237j, -0.03023101+0.06353364j],
               [ 0.08081079+0.01036926j,  0.01729528-0.00254237j,
                 0.02292528+0.j        , -0.03154436-0.0387158j ],
               [-0.1246342 +0.38027643j, -0.03023101-0.06353364j,
                -0.03154436+0.0387158j ,  0.45416657+0.j        ]],
              dims=(2, 2))
- quality: unknown
- extra: <9 items>
- device_components: ['Q0', 'Q1']
- verified: False
{'trace': 1.0000000000000064,
 'eigvals': array([0.88126547, 0.11873453, 0.        , 0.        ]),
 'raw_eigvals': array([ 1.03565885,  0.2731279 ,  0.13488114, -0.44366789]),
 'rescaled_psd': True,
 'fitter_metadata': {'fitter': 'linear_inversion',
  'fitter_time': 0.00388336181640625},
 'conditional_probability': 1.0,
 'positive': True,
 'experiment': 'StateTomography',
 'run_time': None}

Tomography Fitters

The default fitters is linear_inversion, which reconstructs the state using dual basis of the tomography basis. This will typically result in a non-positive reconstructed state. This state is rescaled to be positive-semidefinite (PSD) by computing its eigen-decomposition and rescaling its eigenvalues using the approach from Ref. [1].

There are several other fitters are included (See API documentation for details). For example, if cvxpy is installed we can use the cvxpy_gaussian_lstsq() fitter, which allows constraining the fit to be PSD without requiring rescaling.

try:
    import cvxpy

    # Set analysis option for cvxpy fitter
    qstexp1.analysis.set_options(fitter='cvxpy_gaussian_lstsq')

    # Re-run experiment
    qstdata2 = qstexp1.run(backend, seed_simulation=100).block_for_results()

    state_result2 = qstdata2.analysis_results("state")
    print(state_result2)
    print("\nextra:")
    for key, val in state_result2.extra.items():
        print(f"- {key}: {val}")

except ModuleNotFoundError:
    print("CVXPY is not installed")
AnalysisResult
- name: state
- value: DensityMatrix([[ 0.47384082+0.j        ,  0.00872602+0.00222989j,
                 0.00412199-0.00624817j,  0.01118597-0.43819152j],
               [ 0.00872602-0.00222989j,  0.02848006+0.j        ,
                -0.00572879+0.00398451j,  0.01239408-0.00873476j],
               [ 0.00412199+0.00624817j, -0.00572879-0.00398451j,
                 0.02427678+0.j        , -0.00386097-0.00058081j],
               [ 0.01118597+0.43819152j,  0.01239408+0.00873476j,
                -0.00386097+0.00058081j,  0.47340235+0.j        ]],
              dims=(2, 2))
- quality: unknown
- extra: <9 items>
- device_components: ['Q0', 'Q1']
- verified: False

extra:
- trace: 1.0000000011357344
- eigvals: [0.91220489 0.04652896 0.02493927 0.01632688]
- raw_eigvals: [0.91220489 0.04652896 0.02493927 0.01632688]
- rescaled_psd: False
- fitter_metadata: {'fitter': 'cvxpy_gaussian_lstsq', 'cvxpy_solver': 'SCS', 'cvxpy_status': ['optimal'], 'psd_constraint': True, 'trace_preserving': True, 'fitter_time': 0.020397424697875977}
- conditional_probability: 1.0
- positive: True
- experiment: StateTomography
- run_time: None

Parallel Tomography Experiment

We can also use the ParallelExperiment class to run subsystem tomography on multiple qubits in parallel.

For example if we want to perform 1-qubit QST on several qubits at once:

from math import pi
num_qubits = 5
gates = [qiskit.circuit.library.RXGate(i * pi / (num_qubits - 1))
         for i in range(num_qubits)]

subexps = [
    StateTomography(gate, physical_qubits=(i,))
    for i, gate in enumerate(gates)
]
parexp = ParallelExperiment(subexps)
pardata = parexp.run(backend, seed_simulation=100).block_for_results()

for result in pardata.analysis_results():
    print(result)
AnalysisResult
- name: state
- value: DensityMatrix([[ 0.97363281+0.j        , -0.00683594+0.00683594j],
               [-0.00683594-0.00683594j,  0.02636719+0.j        ]],
              dims=(2,))
- quality: unknown
- extra: <9 items>
- device_components: ['Q0']
- verified: False
AnalysisResult
- name: state_fidelity
- value: 0.9736328124999999
- quality: unknown
- extra: <9 items>
- device_components: ['Q0']
- verified: False
AnalysisResult
- name: positive
- value: True
- quality: unknown
- extra: <9 items>
- device_components: ['Q0']
- verified: False
AnalysisResult
- name: state
- value: DensityMatrix([[ 0.83691406+0.j        , -0.00683594+0.33984375j],
               [-0.00683594-0.33984375j,  0.16308594+0.j        ]],
              dims=(2,))
- quality: unknown
- extra: <9 items>
- device_components: ['Q1']
- verified: False
AnalysisResult
- name: state_fidelity
- value: 0.9785400384397243
- quality: unknown
- extra: <9 items>
- device_components: ['Q1']
- verified: False
AnalysisResult
- name: positive
- value: True
- quality: unknown
- extra: <9 items>
- device_components: ['Q1']
- verified: False
AnalysisResult
- name: state
- value: DensityMatrix([[0.50097656+0.j        , 0.0234375 +0.45898438j],
               [0.0234375 -0.45898438j, 0.49902344+0.j        ]],
              dims=(2,))
- quality: unknown
- extra: <9 items>
- device_components: ['Q2']
- verified: False
AnalysisResult
- name: state_fidelity
- value: 0.9589843750000002
- quality: unknown
- extra: <9 items>
- device_components: ['Q2']
- verified: False
AnalysisResult
- name: positive
- value: True
- quality: unknown
- extra: <9 items>
- device_components: ['Q2']
- verified: False
AnalysisResult
- name: state
- value: DensityMatrix([[ 0.1796875 +0.j        , -0.00488281+0.34472656j],
               [-0.00488281-0.34472656j,  0.8203125 +0.j        ]],
              dims=(2,))
- quality: unknown
- extra: <9 items>
- device_components: ['Q3']
- verified: False
AnalysisResult
- name: state_fidelity
- value: 0.9702536308476947
- quality: unknown
- extra: <9 items>
- device_components: ['Q3']
- verified: False
AnalysisResult
- name: positive
- value: True
- quality: unknown
- extra: <9 items>
- device_components: ['Q3']
- verified: False
AnalysisResult
- name: state
- value: DensityMatrix([[0.03027344+0.j        , 0.01953125+0.01269531j],
               [0.01953125-0.01269531j, 0.96972656+0.j        ]],
              dims=(2,))
- quality: unknown
- extra: <9 items>
- device_components: ['Q4']
- verified: False
AnalysisResult
- name: state_fidelity
- value: 0.9697265625
- quality: unknown
- extra: <9 items>
- device_components: ['Q4']
- verified: False
AnalysisResult
- name: positive
- value: True
- quality: unknown
- extra: <9 items>
- device_components: ['Q4']
- verified: False

View component experiment analysis results:

for i, expdata in enumerate(pardata.child_data()):
    state_result_i = expdata.analysis_results("state")
    fid_result_i = expdata.analysis_results("state_fidelity")

    print(f'\nPARALLEL EXP {i}')
    print("State Fidelity: {:.5f}".format(fid_result_i.value))
    print("State: {}".format(state_result_i.value))

References

See also