abp

This is the documentation for abp. It’s a work in progress.

abp is a Python port of Anders and Briegel’ s method for fast simulation of Clifford circuits. That means that you can make quantum states of thousands of qubits, perform any sequence of Clifford operations, and measure in any of \(\{\sigma_x, \sigma_y, \sigma_z\}\).

Installing

You can install from pip:

$ pip install --user abp==0.4.27

Alternatively, clone from the github repo and run setup.py:

$ git clone https://github.com/peteshadbolt/abp
$ cd abp
$ python setup.py install --user

If you want to modify and test abp without having to re-install, switch into develop mode:

$ python setup.py develop --user

Quickstart

Let’s make a new GraphState object with a register of three qubits:

>>> from abp import GraphState
>>> g = GraphState(3)

All the qubits are initialized by default in the \(|+\rangle\) state:

>>> print g.to_state_vector()
|000❭:       √1/8   + i √0
|100❭:       √1/8   + i √0
|010❭:       √1/8   + i √0
|110❭:       √1/8   + i √0
|001❭:       √1/8   + i √0
|101❭:       √1/8   + i √0
|011❭:       √1/8   + i √0
|111❭:       √1/8   + i √0

We can also check the stabilizer tableau:

>>> print g.to_stabilizer()
 0  1  2
---------
 X
    X
       X

Or look directly at the vertex operators and neighbour lists:

>>> print g
0:  IA      -
1:  IA      -
2:  IA      -

This representation might be unfamiliar. Each row shows the index of the qubit, then the vertex operator, then a list of neighbouring qubits. To understand vertex operators, read the original paper by Anders and Briegel.

Let’s act a Hadamard gate on the zeroth qubit – this will evolve qubit 0 to the \(H|+\rangle = |1\rangle\) state:

>>> g.act_hadamard(0)
>>> print g.to_state_vector()
|000❭:       √1/4   + i √0
|010❭:       √1/4   + i √0
|001❭:       √1/4   + i √0
|011❭:       √1/4   + i √0
>>> print g
0:  YC      -
1:  IA      -
2:  IA      -

And now run some CZ gates:

>>> g.act_cz(0,1)
>>> g.act_cz(1,2)
>>> print g
0:  YC      -
1:  IA      (2,)
2:  IA      (1,)
>>> print g.to_state_vector()
|000❭:       √1/4   + i √0
|010❭:       √1/4   + i √0
|001❭:       √1/4   + i √0
|011❭:      -√1/4   + i √0

Tidy up a bit:

>>> g.del_node(0)
>>> g.act_hadamard(0)
>>> print g.to_state_vector()
|00❭:        √1/2   + i √0
|11❭:        √1/2   + i √0

Cool, we made a Bell state. Incidentally, those those state vectors and stabilizers are genuine Python objects, not just stringy representations of the state:

>>> g = abp.GraphState(2)
>>> g.act_cz(0, 1)
>>> g.act_hadamard(0)
>>> psi = g.to_state_vector()
>>> print psi
|00❭:        √1/2   + i √0
|11❭:        √1/2   + i √0

psi is a state vector – i.e. it is an exponentially large vector of complex numbers. We can still run gates on it:

>>> psi.act_cnot(0, 1)
>>> psi.act_hadamard(0)
>>> print psi
|00❭:        √1     + i √0

But these operations will be very slow. Let’s have a look at the stabilizer tableau:

>>> tab = g.to_stabilizer()
>>> print tab
 0  1
 ------
 Z  Z
 X  X
>>> print tab.tableau
{0: {0: 3, 1: 3}, 1: {0: 1, 1: 1}}
>>> print tab[0, 0]
3

Quantum mechanics is nondeterminstic. However, it’s often useful to get determinstic behaviour for testing purposes. You can force abp to behave determinstically by setting:

>>> abp.DETERMINSTIC = True

Visualization

abp comes with a tool to visualize graph states in a WebGL compatible web browser (Chrome, Firefox, Safari etc). It uses a client-server architecture.

First, run abpserver -v in a terminal:

$ abpserver -v
Listening on port 5000 for clients..

This ought to pop open a browser window at http://localhost:5001/. You can run abpserver --help for help. Now use an instance of abp.VizClient to show the state in the browser:

>>> from abp import GraphState, VizClient
>>> g = GraphState(10)
>>> g.act_circuit([(i, "hadamard") for i in range(10)])
>>> g.act_circuit([((i, i+1), "cz") for i in range(9)])
>>> v = VizClient()
>>> v.update(g)

And you should see a 3D visualization of the state. You can call update() in a loop to see an animation.

By default, the graph is automatically laid out in 3D using the Fruchterman-Reingold force-directed algorithm (i.e. springs). If you want to specify geometry, give each node a position attribute:

>>> g.add_qubit(0, position={"x": 0, "y":0, "z":0}, vop="identity")
>>> g.add_qubit(0, position={"x": 1, "y":0, "z":0}, vop="identity")

There’s a utility function in abp.util to construct those dictionaries:

>>> from abp.util import xyz
>>> g.add_qubit(0, position=xyz(0, 0, 0), vop="identity")
>>> g.add_qubit(1, position=xyz(0, 0, 1), vop="identity")

Note that if any nodes are missing a position attribute, abp will revert to automatic layout for all qubits.

GraphState API

The abp.GraphState class is the main interface to abp.

class abp.GraphState(data=(), vop='identity')

This is the main class used to model stabilizer states. Internally it uses the same dictionary-of-dictionaries data structure as networkx.

__init__(data=(), vop='identity')

Construct a GraphState

Parameters:
  • data – An iterable of nodes used to construct the graph, or an integer – the number of nodes, or a nx.Graph.
  • vop – The default VOP for new qubits. Setting vop="identity" initializes qubits in \(|+\rangle\). Setting vop="hadamard" initializes qubits in \(|0\rangle\).
act_circuit(circuit)

Run many gates in one call.

Parameters:circuit – An iterable containing tuples of the form (node, operation). If operation is a name for a local operation (e.g. 6, hadamard) then that operation is performed on node. If operation is cz then a CZ is performed on the two nodes in node.

Example (makes a Bell pair):

>>> g.act_circuit([(0, "hadamard"), (1, "hadamard"), ((0, 1), "cz")])
act_cz(a, b)

Act a controlled-phase gate on two qubits

Parameters:
  • a – The first qubit
  • b – The second qubit
act_czs(*pairs)

Shorthand to act many CZs

act_hadamard(qubit)

Shorthand for self.act_local_rotation(qubit, "hadamard")

act_local_rotation(node, operation)

Act a local rotation on a qubit

Parameters:
  • node – The index of the node to act on
  • operation – The Clifford-group operation to perform. You can use any of the names in the Clifford group alias table.
add_node(*args, **kwargs)

Add a node

add_qubit(name, **kwargs)

Add a qubit to the state.

Parameters:
  • name (Any hashable type) – The name of the node, e.g. 9, start.
  • kwargs – Any extra node attributes

By default, qubits are initialized in the \(|0\rangle\) state. Provide the optional vop argument to set the initial state.

Example of using node attributes

>>> g._add_node(0, label="fred", position=(1,2,3))
>>> g.node[0]["label"]
fred
copy()

Make a copy of this graphstate

del_qubit(node)

Remove a qubit. TODO: this is a hack right now.

edgelist()

Describe a graph as an edgelist # TODO: inefficient

from_json(data)

Construct the graph from JSON data :param data: JSON data to be read.

has_edge(v1, v2)

Test existence of an edge between two vertices

local_complementation(v)

As defined in LISTING 1 of Anders & Briegel

measure(node, basis, force=None, detail=False, friend=None)

Measure in an arbitrary basis

Parameters:
  • node – The name of the qubit to measure.
  • basis (\(\in\) {"px", "py", "pz"}) – The basis in which to measure.
  • friend (Any neighbour of node.) – Specify a node to toggle about when performing an \(X\) measurement.
  • force (boolean) – Forces the measurement outcome.
  • detail (boolean) – Get detailed information.

Measurements in quantum mechanics are probabilistic. If you want to force a particular outcome \(\in\{0, 1\}\), use force.

You can get more information by setting detail=True, in which case measure() returns a dictionary with the following keys:

  • outcome: the measurement outcome.
  • determinate: indicates whether the outcome was determinate or random. For example, measuring \(|0\rangle\) in \(\sigma_x\) always gives a deterministic outcome. determinate is overridden by force – forced outcomes are always determinate.
  • conjugated_basis: The index of the measurement operator, rotated by the vertex operator of the measured node, i.e. \(U_\text{vop} \sigma_m U_\text{vop}^\dagger\).
  • phase: The phase of the cojugated basis, \(\pm 1\).
  • node: The name of the measured node.
  • force: The value of force.
measure_sequence(measurements, forces=None, detail=False)

Measures a sequence of Paulis

Parameters:
  • measurements – The sequence of measurements to be made, in the form [(node, basis), ...]
  • force (list) – Measurements in quantum mechanics are probabilistic. If you want to force a particular outcome, use the force. List outcome force values in same order as measurements
  • detail (boolean) – Provide detailed information
measure_x(node, force=None, detail=False, friend=None)

Measure in the X basis

measure_y(node, force=None, detail=False)

Measure in the Y basis

measure_z(node, force=None, detail=False)

Measure in the Z basis

order()

Get the number of qubits

remove_vop(node, avoid)

Attempts to remove the vertex operator on a particular qubit.

Parameters:
  • node – The node whose vertex operator should be reduced to the identity.
  • avoid – We will try to leave this node alone during the process (if possible).
to_json(stringify=False)

Convert the graph to JSON-like form.

Parameters:stringify – JSON keys must be strings, But sometimes it is useful to have a JSON-like object whose keys are tuples.

If you want to dump a graph to disk, do something like this:

>>> import json
>>> with open("graph.json") as f:
        json.dump(graph.to_json(True), f)
to_stabilizer()

Get the stabilizer representation of the state:

>>> print g.to_stabilizer()
   0    1    2    3    100  200
  ------------------------------
   X    Z    Z    X
   Z    X    Z
   Z    Z    X
 - Z              Z
                       X    Z
                       Z    X
to_state_vector()
Get the full state vector corresponding to this stabilizer state. Useful for debugging, interface with other simulators.
This method becomes very slow for more than about ten qubits!

The output state is represented as a abp.qi.CircuitModel:

>>> print g.to_state_vector()
|00000❭: 0.18+0.00j
|00001❭: 0.18+0.00j ...

The Clifford group

This module handles operations on the Clifford group. It makes extensive use of the lookup tables in abp.tables. The code to generate those tables is included in this distribution as abp/build_tables.py This package emumerates and labels the single-qubit Clifford group, and provides methods for matrix multiplication and conjugation. It also includes the look-up table for the CZ gate.

There are 24 members of the single-qubit Clifford group. You can refer to some of them by multiple names. The complete set of aliases for single-qubit Cliffords is as follows:

Index Aliases
0 IA, identity, identity_h
1 XA, px, px_h
2 YA, py, py_h
3 ZA, pz, pz_h
4 IB
5 XB, sqz, msqz_h, phase_h
6 YB, msqz, sqz_h, phase
7 ZB
8 IC
9 XC, msqy, sqy_h
10 YC, hadamard, hadamard_h
11 ZC, sqy, msqy_h
12 ID
13 XD
14 YD, sqx, msqx_h
15 ZD, msqx, sqx_h
16 IE
17 XE
18 YE
19 ZE
20 IF
21 XF
22 YF
23 ZF

The clifford module provides a few useful functions:

abp.clifford.use_old_cz()

Use the CZ lookup table from A&B’s code, rather than our own. Useful for testing.

Testing

abp has a bunch of tests. It tests against all sorts of things, including the circuit model, Anders & Briegels’ original code, Scott Aaronson’s chp, and common sense. You can run all the tests using nose:

$ nosetests
...
53 tests run in 39.5 seconds (53 tests passed)

Currently I use some reference implementations of chp and graphsim which you won’t have installed, so some tests will be skipped. That’s expected.

Reference

More detailed docs are available here: