pyqtorch - pyqtorch

Skip to content

# `pyqtorch`¶

A fast large scale emulator for quantum machine learning on a PyTorch backend.

## Installation¶

To install the library for development, you can go into any virtual environment of your choice and install it normally with `pip` (including extra dependencies for development):

    pip install pyqtorch

## Contribute¶

If you want to contribute to the package, make sure to execute tests and MyPy checks otherwise the automatic pipeline will not pass. To do so, the recommended way is to use `hatch` for managing the environments:

    hatch env create tests
    hatch --env tests run python -m pytest -vvv --cov pyqtorch tests
    hatch --env tests run python -m mypy pyqtorch tests

If you don't want to use `hatch`, you can use the environment manager of your choice (e.g. Conda) and execute the following:

    pip install -e .[dev]
    pytest -vvv --cov pyqtorch tests
    mypy pyqtorch tests

## Getting started with `pyqtorch`¶

### Gates¶

`pyqtorch` implements most of the commonly used gates like Pauli gates, rotation gates, and controlled gates. Every gate accepts a sequence of `qubits` on which it operates and a total number `n_qubits` of the state that it will operate on:

In [1]:
Copied!

    import torch
    import pyqtorch.modules as pyq

    gate = pyq.X(qubits=[0], n_qubits=1)
    z = pyq.zero_state(n_qubits=1)

    gate(z)

import torch import pyqtorch.modules as pyq gate = pyq.X(qubits=[0], n_qubits=1) z = pyq.zero_state(n_qubits=1) gate(z)

    /Users/niklas/Library/Application Support/hatch/env/virtual/qucint/6NOL9orC/qucint/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
      from .autonotebook import tqdm as notebook_tqdm

Out[1]:

    tensor([[0.+0.j],
            [1.+0.j]], dtype=torch.complex128)

In [2]:
Copied!

    gate = pyq.CNOT(qubits=[0,1], n_qubits=2)
    z = pyq.zero_state(n_qubits=2)
    gate(z)

gate = pyq.CNOT(qubits=[0,1], n_qubits=2) z = pyq.zero_state(n_qubits=2) gate(z)
Out[2]:

    tensor([[[1.+0.j],
             [0.+0.j]],

            [[0.+0.j],
             [0.+0.j]]], dtype=torch.complex128)

In [3]:
Copied!

    z.shape

z.shape
Out[3]:

    torch.Size([2, 2, 1])

In `pyqtorch` the state is a `n_qubit+1` dimensional `Tensor`, for example a state with 3 qubits has the shape `(2, 2, 2, 1)` (i.e. one dimension for each qubit, plus one dimension for the batch size).

NOTE: We always work with batched state in `pyqtorch`.

In [4]:
Copied!

    z = pyq.zero_state(n_qubits=3)
    print(z.shape)
    z = pyq.zero_state(n_qubits=3, batch_size=5)
    print(z.shape)

z = pyq.zero_state(n_qubits=3) print(z.shape) z = pyq.zero_state(n_qubits=3, batch_size=5) print(z.shape)

    torch.Size([2, 2, 2, 1])
    torch.Size([2, 2, 2, 5])

### `QuantumCircuit`s¶

To compose multiple gates we use a `QuantumCircuit` which is constructed from a list of operations.

In [5]:
Copied!

    circ = pyq.QuantumCircuit(
        n_qubits=2,
        operations=[
            pyq.X([0], 2),
            pyq.CNOT([0,1], 2)
        ]
    )

    z = pyq.zero_state(2)
    circ(z)

circ = pyq.QuantumCircuit( n_qubits=2, operations=[ pyq.X([0], 2), pyq.CNOT([0,1], 2) ] ) z = pyq.zero_state(2) circ(z)
Out[5]:

    tensor([[[0.+0.j],
             [0.+0.j]],

            [[0.+0.j],
             [1.+0.j]]], dtype=torch.complex128)

Every gate and circuit in `pyqtorch` accepts a state and an optional tensor of angles. If the gate/circuit does not depend on any angles, the second argument is ignored.

In [6]:
Copied!

    theta = torch.rand(1)
    circ(z, theta)  # theta is ignored

theta = torch.rand(1) circ(z, theta) # theta is ignored
Out[6]:

    tensor([[[0.+0.j],
             [0.+0.j]],

            [[0.+0.j],
             [1.+0.j]]], dtype=torch.complex128)

In [7]:
Copied!

    circ = pyq.QuantumCircuit(
        n_qubits=2,
        operations=[
            pyq.RX([0], 2), # rotation instead of X gate
            pyq.CNOT([0,1], 2)
        ]
    )

    circ(z, theta)  # theta is used!

circ = pyq.QuantumCircuit( n_qubits=2, operations=[ pyq.RX([0], 2), # rotation instead of X gate pyq.CNOT([0,1], 2) ] ) circ(z, theta) # theta is used!
Out[7]:

    tensor([[[0.9885+0.0000j],
             [0.0000+0.0000j]],

            [[0.0000+0.0000j],
             [0.0000-0.1511j]]], dtype=torch.complex128)

The vanilla `QuantumCircuit` is always passing the same `theta` tensor to its operations, meaning the `forward` method of the circuit is:

    class QuantumCircuit(torch.nn.Module):

        # ...

        def forward(self, state: torch.Tensor, thetas: torch.Tensor = None) -> torch.Tensor:
            for op in self.operations:
                state = op(state, thetas)
            return state

The `FeaturemapLayer` is a convenience constructor for a `QuantumCircuit` which accepts an operation to put on every qubit.

In [13]:
Copied!

    circ = pyq.FeaturemapLayer(n_qubits=3, Op=pyq.RX)
    print(circ)

    states = pyq.zero_state(n_qubits=3, batch_size=4)
    inputs = torch.rand(4)

    # the same batch of inputs are passed to the operations
    circ(states, inputs).shape

circ = pyq.FeaturemapLayer(n_qubits=3, Op=pyq.RX) print(circ) states = pyq.zero_state(n_qubits=3, batch_size=4) inputs = torch.rand(4) # the same batch of inputs are passed to the operations circ(states, inputs).shape

    QuantumCircuit(
      (operations): ModuleList(
        (0): RX(qubits=[0], n_qubits=3)
        (1): RX(qubits=[1], n_qubits=3)
        (2): RX(qubits=[2], n_qubits=3)
      )
    )

Out[13]:

    torch.Size([2, 2, 2, 4])

### Trainable `QuantumCircuit`s aka `VariationalLayer`s¶

If you want the angles of your circuit to be trainable you can use a `VariationalLayer`. The `VariationalLayer` ignores the second input (because it has trainable angle parameters).

In [15]:
Copied!

    circ = pyq.VariationalLayer(n_qubits=3, Op=pyq.RX)

    state = pyq.zero_state(3)
    this_argument_is_ignored = None
    circ(state, this_argument_is_ignored)

circ = pyq.VariationalLayer(n_qubits=3, Op=pyq.RX) state = pyq.zero_state(3) this_argument_is_ignored = None circ(state, this_argument_is_ignored)
Out[15]:

    tensor([[[[ 0.2419+0.0000j],
              [ 0.0000+0.2235j]],

             [[ 0.0000-0.0524j],
              [ 0.0484+0.0000j]]],

            [[[ 0.0000+0.6759j],
              [-0.6244+0.0000j]],

             [[ 0.1464+0.0000j],
              [ 0.0000+0.1352j]]]], dtype=torch.complex128,
           grad_fn=<ViewBackward0>)

### Composing `QuantumCircuit`s¶

As every gate and circuit in `pyqtorch` accept the same arguments we can easily compose them to larger circuits, i.e. to implement a hardware efficient ansatz:

In [18]:
Copied!

    def hea(n_qubits: int, n_layers: int) -> pyq.QuantumCircuit:
        ops = []
        for _ in range(n_layers):
            ops.append(pyq.VariationalLayer(n_qubits, pyq.RX))
            ops.append(pyq.VariationalLayer(n_qubits, pyq.RY))
            ops.append(pyq.VariationalLayer(n_qubits, pyq.RX))
            ops.append(pyq.EntanglingLayer(n_qubits))
        return pyq.QuantumCircuit(n_qubits, ops)

    circ = hea(3,2)
    print(circ)

    state = pyq.zero_state(3)
    circ(state)

def hea(n_qubits: int, n_layers: int) -> pyq.QuantumCircuit: ops = [] for _ in range(n_layers): ops.append(pyq.VariationalLayer(n_qubits, pyq.RX)) ops.append(pyq.VariationalLayer(n_qubits, pyq.RY)) ops.append(pyq.VariationalLayer(n_qubits, pyq.RX)) ops.append(pyq.EntanglingLayer(n_qubits)) return pyq.QuantumCircuit(n_qubits, ops) circ = hea(3,2) print(circ) state = pyq.zero_state(3) circ(state)

    QuantumCircuit(
      (operations): ModuleList(
        (0): VariationalLayer(
          (operations): ModuleList(
            (0): RX(qubits=[0], n_qubits=3)
            (1): RX(qubits=[1], n_qubits=3)
            (2): RX(qubits=[2], n_qubits=3)
          )
        )
        (1): VariationalLayer(
          (operations): ModuleList(
            (0): RY(qubits=[0], n_qubits=3)
            (1): RY(qubits=[1], n_qubits=3)
            (2): RY(qubits=[2], n_qubits=3)
          )
        )
        (2): VariationalLayer(
          (operations): ModuleList(
            (0): RX(qubits=[0], n_qubits=3)
            (1): RX(qubits=[1], n_qubits=3)
            (2): RX(qubits=[2], n_qubits=3)
          )
        )
        (3): EntanglingLayer(
          (operations): ModuleList(
            (0): CNOT(qubits=[0, 1], n_qubits=3)
            (1): CNOT(qubits=[1, 2], n_qubits=3)
            (2): CNOT(qubits=[2, 0], n_qubits=3)
          )
        )
        (4): VariationalLayer(
          (operations): ModuleList(
            (0): RX(qubits=[0], n_qubits=3)
            (1): RX(qubits=[1], n_qubits=3)
            (2): RX(qubits=[2], n_qubits=3)
          )
        )
        (5): VariationalLayer(
          (operations): ModuleList(
            (0): RY(qubits=[0], n_qubits=3)
            (1): RY(qubits=[1], n_qubits=3)
            (2): RY(qubits=[2], n_qubits=3)
          )
        )
        (6): VariationalLayer(
          (operations): ModuleList(
            (0): RX(qubits=[0], n_qubits=3)
            (1): RX(qubits=[1], n_qubits=3)
            (2): RX(qubits=[2], n_qubits=3)
          )
        )
        (7): EntanglingLayer(
          (operations): ModuleList(
            (0): CNOT(qubits=[0, 1], n_qubits=3)
            (1): CNOT(qubits=[1, 2], n_qubits=3)
            (2): CNOT(qubits=[2, 0], n_qubits=3)
          )
        )
      )
    )

Out[18]:

    tensor([[[[-0.5790-0.0103j],
              [ 0.1601+0.0274j]],

             [[-0.1465+0.3101j],
              [-0.0429+0.2236j]]],

            [[[-0.1688-0.3029j],
              [-0.2445-0.2112j]],

             [[-0.4516+0.0601j],
              [ 0.0749-0.1762j]]]], dtype=torch.complex128,
           grad_fn=<PermuteBackward0>)