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:
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
tensor([[0.+0.j],
[1.+0.j]], dtype=torch.complex128)
gate = pyq.CNOT(qubits=[0,1], n_qubits=2)
z = pyq.zero_state(n_qubits=2)
gate(z)
tensor([[[1.+0.j],
[0.+0.j]],
[[0.+0.j],
[0.+0.j]]], dtype=torch.complex128)
z.shape
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.
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])
QuantumCircuits¶
To compose multiple gates we use a QuantumCircuit which is constructed from
a list of operations.
circ = pyq.QuantumCircuit(
n_qubits=2,
operations=[
pyq.X([0], 2),
pyq.CNOT([0,1], 2)
]
)
z = pyq.zero_state(2)
circ(z)
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.
theta = torch.rand(1)
circ(z, theta) # theta is ignored
tensor([[[0.+0.j],
[0.+0.j]],
[[0.+0.j],
[1.+0.j]]], dtype=torch.complex128)
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!
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.
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)
)
)
torch.Size([2, 2, 2, 4])
Trainable QuantumCircuits aka VariationalLayers¶
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).
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)
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 QuantumCircuits¶
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:
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)
)
)
)
)
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>)