Skip to content

Block system

Quantum programs in Qadence are constructed using a block-system, with an emphasis on composability of primitive blocks to obtain larger, composite blocks. This functional approach is different from other frameworks which follow a more object-oriented way to construct circuits and express programs.

Primitive blocks

A PrimitiveBlock represents a digital or an analog time-evolution quantum operation applied to a qubit support. Programs can always be decomposed down into a sequence of PrimitiveBlock elements.

Two canonical examples of digital primitive blocks are the parametrized RX and the CNOT gates:

from qadence import chain, RX, CNOT

rx = RX(0, 0.5)
cnot = CNOT(0, 1)

block = chain(rx, cnot)
%3 97e0360b829940d19df2cdb8b1be1687 0 433fad6e713d4ff0b4ce1a83ade1052c RX(0.5) 97e0360b829940d19df2cdb8b1be1687--433fad6e713d4ff0b4ce1a83ade1052c 395137b1f7494f68be9bc853359b224b 1 676cf749ea3f489897624701e5259f7b 433fad6e713d4ff0b4ce1a83ade1052c--676cf749ea3f489897624701e5259f7b b62d16408e5a4367a0c5ff19d377cfb2 676cf749ea3f489897624701e5259f7b--b62d16408e5a4367a0c5ff19d377cfb2 0c2837dc50ba4276a5dadf7a8ed0751a 827fdd0d214d42b28f398db46d93967c 395137b1f7494f68be9bc853359b224b--827fdd0d214d42b28f398db46d93967c c0237057288b4ecf8feb698617f4b7a2 X 827fdd0d214d42b28f398db46d93967c--c0237057288b4ecf8feb698617f4b7a2 c0237057288b4ecf8feb698617f4b7a2--676cf749ea3f489897624701e5259f7b c0237057288b4ecf8feb698617f4b7a2--0c2837dc50ba4276a5dadf7a8ed0751a

A list of all available primitive operations can be found here.

How to visualize blocks

There are two ways to display blocks in a Python interpreter: either as a tree in ASCII format using print:

from qadence import X, Y, kron

kron_block = kron(X(0), Y(1))
print(kron_block)
KronBlock(0,1)
├── X(0)
└── Y(1)

Or using the visualization package:

from qadence import X, Y, kron
from qadence.draw import display

kron_block = kron(X(0), Y(1))
# display(kron_block)
%3 a3eb2636768048d7bdbce74fd6f10107 0 8bbbf3cd31c6430daebae358072efacd X a3eb2636768048d7bdbce74fd6f10107--8bbbf3cd31c6430daebae358072efacd 59347cb8d0bd499891199f7b0ba0d92d 1 a38721bae6064576895efca9cc7f0322 8bbbf3cd31c6430daebae358072efacd--a38721bae6064576895efca9cc7f0322 18ea03729f3f4e15a0aca1559936c432 72e4c3d21c94469897af377345b92f1e Y 59347cb8d0bd499891199f7b0ba0d92d--72e4c3d21c94469897af377345b92f1e 72e4c3d21c94469897af377345b92f1e--18ea03729f3f4e15a0aca1559936c432

Composite Blocks

Programs can be expressed by composing blocks to result in a larger CompositeBlock using three fundamental operations: chain, kron, and add.

  • chain applies a set of blocks in sequence, which can have overlapping qubit supports, and results in a ChainBlock type. It is akin to applying a matrix product of the sub-blocks, and can also be used with the * operator.
  • kron applies a set of blocks in parallel, requiring disjoint qubit support, and results in a KronBlock type. This is akin to applying a tensor product of the sub-blocks, and can also be used with the @ operator.
  • add performs a direct sum of the operators, and results in an AddBlock type. Blocks constructed this way are typically non-unitary, as is the case for Hamiltonians which can be constructed through sums of Pauli strings. Addition can also be performed directly with the + operator.
from qadence import X, Y, chain, kron

chain_0 = chain(X(0), Y(0))
chain_1 = chain(X(1), Y(1))

kron_block = kron(chain_0, chain_1)
%3 fce60ef44d2c48ac899e0ddf4cf9e7b9 0 aa8512229f684eeb9c0370e05ed77fa9 X fce60ef44d2c48ac899e0ddf4cf9e7b9--aa8512229f684eeb9c0370e05ed77fa9 a6f5e650cf194c0ea2a61eef34981a3c 1 c7cfc01fcbfa4ee8a38fb8f9d78b4023 Y aa8512229f684eeb9c0370e05ed77fa9--c7cfc01fcbfa4ee8a38fb8f9d78b4023 eae226fb596e4a578c05c676e2d8f07c c7cfc01fcbfa4ee8a38fb8f9d78b4023--eae226fb596e4a578c05c676e2d8f07c 551b43fe00724cb69090818c73acca42 0cc12107708a4a0a9b046da659b65c21 X a6f5e650cf194c0ea2a61eef34981a3c--0cc12107708a4a0a9b046da659b65c21 71295fd7bbaf418e9fb5a27afff53059 Y 0cc12107708a4a0a9b046da659b65c21--71295fd7bbaf418e9fb5a27afff53059 71295fd7bbaf418e9fb5a27afff53059--551b43fe00724cb69090818c73acca42

All composition functions support list comprehension syntax. Below we exemplify the creation of an XY Hamiltonian for qubits laid out on a line.

from qadence import X, Y, add

def xy_int(i: int, j: int):
    return (1/2) * (X(i)@X(j) + Y(i)@Y(j))

n_qubits = 3

xy_ham = add(xy_int(i, i+1) for i in range(n_qubits-1))
AddBlock(0,1,2)
├── [mul: 0.500] 
   └── AddBlock(0,1)
       ├── KronBlock(0,1)
          ├── X(0)
          └── X(1)
       └── KronBlock(0,1)
           ├── Y(0)
           └── Y(1)
└── [mul: 0.500] 
    └── AddBlock(1,2)
        ├── KronBlock(1,2)
           ├── X(1)
           └── X(2)
        └── KronBlock(1,2)
            ├── Y(1)
            └── Y(2)

Qadence blocks can be directly translated to matrix form by calling block.tensor(). Note that first dimension is the batch dimension, following PyTorch conventions. This becomes relevant if the block are parameterized and batched input values are passed, as we will see later.

from qadence import X, Y

xy = (1/2) * (X(0)@X(1) + Y(0)@Y(1))

print(xy.tensor().real)
tensor([[[0., 0., 0., 0.],
         [0., 0., 1., 0.],
         [0., 1., 0., 0.],
         [0., 0., 0., 0.]]])

For a final example of the flexibility of functional block composition, below is an implementation of the Quantum Fourier Transform on an arbitrary qubit support.

from qadence import H, CPHASE, PI, chain, kron

def qft_layer(qs: tuple, l: int):
    cphases = chain(CPHASE(qs[j], qs[l], PI/2**(j-l)) for j in range(l+1, len(qs)))
    return H(qs[l]) * cphases

def qft(qs: tuple):
    return chain(qft_layer(qs, l) for l in range(len(qs)))
%3 8d304eaa2f3d4f53a23ae54611431059 0 90f269492b2a4710ba6de1993179ce87 H 8d304eaa2f3d4f53a23ae54611431059--90f269492b2a4710ba6de1993179ce87 c8de21e2131b485482ecc5862eb4e9ea 1 ab7e7881ec364048a80a15c718628b9f PHASE(1.571) 90f269492b2a4710ba6de1993179ce87--ab7e7881ec364048a80a15c718628b9f 5df7120737604921a004cd6f6f73bcec PHASE(0.785) ab7e7881ec364048a80a15c718628b9f--5df7120737604921a004cd6f6f73bcec fb3076c2ccb240a087c05a4a9ac62fc6 ab7e7881ec364048a80a15c718628b9f--fb3076c2ccb240a087c05a4a9ac62fc6 8d57dc02d4144eb799bfcedb6b8c8678 5df7120737604921a004cd6f6f73bcec--8d57dc02d4144eb799bfcedb6b8c8678 1ddeea608c674080b7c60097ea89fc71 5df7120737604921a004cd6f6f73bcec--1ddeea608c674080b7c60097ea89fc71 c01debbcc7d3413a89cac5482d42769a 8d57dc02d4144eb799bfcedb6b8c8678--c01debbcc7d3413a89cac5482d42769a 45be869b730b4e6e8eaf61e802b3d4a7 c01debbcc7d3413a89cac5482d42769a--45be869b730b4e6e8eaf61e802b3d4a7 4d1531512d284f7699a9a39dcadee869 45be869b730b4e6e8eaf61e802b3d4a7--4d1531512d284f7699a9a39dcadee869 2f96f7d9f0d949b8b00c0745f8ae3478 de605bf6ed9e4fb3a348d625e1a47198 c8de21e2131b485482ecc5862eb4e9ea--de605bf6ed9e4fb3a348d625e1a47198 05a4255074d14ea6a079e96dd0a535ab 2 de605bf6ed9e4fb3a348d625e1a47198--fb3076c2ccb240a087c05a4a9ac62fc6 3e5ce8f0b3114aa5a02075f37ba01a64 fb3076c2ccb240a087c05a4a9ac62fc6--3e5ce8f0b3114aa5a02075f37ba01a64 447cbd913d524344a7d24029b7617861 H 3e5ce8f0b3114aa5a02075f37ba01a64--447cbd913d524344a7d24029b7617861 88bc479f714d4b278c48423c3785a343 PHASE(1.571) 447cbd913d524344a7d24029b7617861--88bc479f714d4b278c48423c3785a343 026a1d3698f24ce8ae705d5ce46d850b 88bc479f714d4b278c48423c3785a343--026a1d3698f24ce8ae705d5ce46d850b 6d90ce0f6d734c9bb1ce0ac7950f8309 88bc479f714d4b278c48423c3785a343--6d90ce0f6d734c9bb1ce0ac7950f8309 026a1d3698f24ce8ae705d5ce46d850b--2f96f7d9f0d949b8b00c0745f8ae3478 ff174a14e0644846b2704e3bdaf76326 6f4d54e573de4245ba328a328bf3a82c 05a4255074d14ea6a079e96dd0a535ab--6f4d54e573de4245ba328a328bf3a82c 0a111b324fd54aa38aa4b27ddb67831d 6f4d54e573de4245ba328a328bf3a82c--0a111b324fd54aa38aa4b27ddb67831d 0a111b324fd54aa38aa4b27ddb67831d--1ddeea608c674080b7c60097ea89fc71 7a46691bee074c1fa5f3885c81a1e485 1ddeea608c674080b7c60097ea89fc71--7a46691bee074c1fa5f3885c81a1e485 7a46691bee074c1fa5f3885c81a1e485--6d90ce0f6d734c9bb1ce0ac7950f8309 0f8f2f372457494684fe782c51983286 H 6d90ce0f6d734c9bb1ce0ac7950f8309--0f8f2f372457494684fe782c51983286 0f8f2f372457494684fe782c51983286--ff174a14e0644846b2704e3bdaf76326

Other functionalities are directly built in the block system. For example, the inverse operation can be created with the dagger() method.

qft_inv = qft((0, 1, 2)).dagger()
%3 a87999d13a2b4b439649f03e0006fbd5 0 12bba8fb027b4fb19835616d684d8e6b a87999d13a2b4b439649f03e0006fbd5--12bba8fb027b4fb19835616d684d8e6b 35c0b31424314b19ac32cc5a87eea0bd 1 a832379f9c364806995e7c74971b194c 12bba8fb027b4fb19835616d684d8e6b--a832379f9c364806995e7c74971b194c 3420bc45ba3949b8bc64fcf30ccdf666 a832379f9c364806995e7c74971b194c--3420bc45ba3949b8bc64fcf30ccdf666 211b2389fd3e4e34900503d5cff66c6d PHASE(-0.785) 3420bc45ba3949b8bc64fcf30ccdf666--211b2389fd3e4e34900503d5cff66c6d edab1595de354aeba8bd547680432316 PHASE(-1.571) 211b2389fd3e4e34900503d5cff66c6d--edab1595de354aeba8bd547680432316 fc7c4d060dde45d79275673a284bce05 211b2389fd3e4e34900503d5cff66c6d--fc7c4d060dde45d79275673a284bce05 9dfb4979478040848390fa30f404c7b6 H edab1595de354aeba8bd547680432316--9dfb4979478040848390fa30f404c7b6 c12ae7e260594d25b60c8fd5de2922fc edab1595de354aeba8bd547680432316--c12ae7e260594d25b60c8fd5de2922fc ab6753a3b6a64e2b99d644a5ff0bdec5 9dfb4979478040848390fa30f404c7b6--ab6753a3b6a64e2b99d644a5ff0bdec5 82c36063e0374976898ec8aa3038170f 1a3a932faf6d4c94a8f2e47c93848ebf 35c0b31424314b19ac32cc5a87eea0bd--1a3a932faf6d4c94a8f2e47c93848ebf c6a6d2307135437c97ebc6622059118d 2 4512a0a7cd194f5d8b32c8ae8785ed73 PHASE(-1.571) 1a3a932faf6d4c94a8f2e47c93848ebf--4512a0a7cd194f5d8b32c8ae8785ed73 16ed38cdde304c5abd48ba86d75c525e H 4512a0a7cd194f5d8b32c8ae8785ed73--16ed38cdde304c5abd48ba86d75c525e 8cbbf6f10806455c8cf97ea26d9073bd 4512a0a7cd194f5d8b32c8ae8785ed73--8cbbf6f10806455c8cf97ea26d9073bd 3f7f6df5adde41268eecb59988e8beab 16ed38cdde304c5abd48ba86d75c525e--3f7f6df5adde41268eecb59988e8beab 3f7f6df5adde41268eecb59988e8beab--c12ae7e260594d25b60c8fd5de2922fc f964f1e3c5de4e5db26d80598456a828 c12ae7e260594d25b60c8fd5de2922fc--f964f1e3c5de4e5db26d80598456a828 f964f1e3c5de4e5db26d80598456a828--82c36063e0374976898ec8aa3038170f 6ca5d75912904cd1b3450b9b63f38fb5 c66a40d63b654e4891504c6281e39fc0 H c6a6d2307135437c97ebc6622059118d--c66a40d63b654e4891504c6281e39fc0 c66a40d63b654e4891504c6281e39fc0--8cbbf6f10806455c8cf97ea26d9073bd 0d2ef209a77b43c687482731d8233481 8cbbf6f10806455c8cf97ea26d9073bd--0d2ef209a77b43c687482731d8233481 0d2ef209a77b43c687482731d8233481--fc7c4d060dde45d79275673a284bce05 33c1a741556c426a89bab68317e7788b fc7c4d060dde45d79275673a284bce05--33c1a741556c426a89bab68317e7788b 12667497ae25441a84fcb16f2f7dbde4 33c1a741556c426a89bab68317e7788b--12667497ae25441a84fcb16f2f7dbde4 12667497ae25441a84fcb16f2f7dbde4--6ca5d75912904cd1b3450b9b63f38fb5

Digital-analog composition

In Qadence, analog operations are first-class citizens. An analog operation is one whose unitary is best described by the evolution of some hermitian generator, or Hamiltonian, acting on an arbitrary number of qubits. Qadence provides the HamEvo class to initialize analog operations. For a time-independent generator \(\mathcal{H}\) and some time variable \(t\), HamEvo(H, t) represents the evolution operator \(\exp(-i\mathcal{H}t)\).

Analog operations constitute a generalization of digital operations, and all digital operations can also be represented as the evolution of some hermitian generator. For example, the RX gate is the evolution of X.

from qadence import X, RX, HamEvo, PI
from torch import allclose

angle = PI/2

block_digital = RX(0, angle)

block_analog = HamEvo(0.5*X(0), angle)

print(allclose(block_digital.tensor(), block_analog.tensor()))
True

As seen in the previous section, arbitrary Hamiltonians can be constructed using Pauli operators. Their evolution can be combined with other arbitrary digital operations and incorporated into any quantum program.

from qadence import X, Y, RX, HamEvo
from qadence import add, kron, PI

def xy_int(i: int, j: int):
    return (1/2) * (X(i)@X(j) + Y(i)@Y(j))

n_qubits = 3

xy_ham = add(xy_int(i, i+1) for i in range(n_qubits-1))

analog_evo = HamEvo(xy_ham, 1.0)

digital_block = kron(RX(i, i*PI/2) for i in range(n_qubits))

program = digital_block * analog_evo * digital_block
%3 cluster_6daaba9c9f254a02845374bd3f8b4116 35a3b3c0be9749ad9b81369357eb5136 0 fa8023fd10e543d4a888f09eb078d31a RX(0.0) 35a3b3c0be9749ad9b81369357eb5136--fa8023fd10e543d4a888f09eb078d31a 3bfbcce1973b443199770bbe86b9424d 1 ad0b21464ecc413c96989e731020aeb6 HamEvo fa8023fd10e543d4a888f09eb078d31a--ad0b21464ecc413c96989e731020aeb6 4487fa01a93d4b6bb2c32877b614a232 RX(0.0) ad0b21464ecc413c96989e731020aeb6--4487fa01a93d4b6bb2c32877b614a232 b91af39173d646ff836a7981051fe4de 4487fa01a93d4b6bb2c32877b614a232--b91af39173d646ff836a7981051fe4de 813be535d67f47a59e9fa656be27653a bac424e0f8034a538577b7ef2602c6a4 RX(1.571) 3bfbcce1973b443199770bbe86b9424d--bac424e0f8034a538577b7ef2602c6a4 cb35097443e24d58a712014aea87b804 2 fad67f5ed42d4808aff6bc304d4ca2c5 t = 1.000 bac424e0f8034a538577b7ef2602c6a4--fad67f5ed42d4808aff6bc304d4ca2c5 117c0345a41e4e3b8c1317905b9a7910 RX(1.571) fad67f5ed42d4808aff6bc304d4ca2c5--117c0345a41e4e3b8c1317905b9a7910 117c0345a41e4e3b8c1317905b9a7910--813be535d67f47a59e9fa656be27653a dd1b776a66014413b6c9b2f7668a2a99 afac18f3c7c3420d880566b3ab4cf457 RX(3.142) cb35097443e24d58a712014aea87b804--afac18f3c7c3420d880566b3ab4cf457 bcf8b494ca5a4c3e8cfd5cc3feb6c610 afac18f3c7c3420d880566b3ab4cf457--bcf8b494ca5a4c3e8cfd5cc3feb6c610 390c22311ba44dafa7a920a592c0255e RX(3.142) bcf8b494ca5a4c3e8cfd5cc3feb6c610--390c22311ba44dafa7a920a592c0255e 390c22311ba44dafa7a920a592c0255e--dd1b776a66014413b6c9b2f7668a2a99

Block execution

To quickly run block operations and access wavefunctions, samples or expectation values of observables, one can use the convenience functions run, sample and expectation.

from qadence import kron, add, H, Z, run, sample, expectation

n_qubits = 2

# Prepares a uniform state
h_block = kron(H(i) for i in range(n_qubits))

wf = run(h_block)

xs = sample(h_block, n_shots=1000)

obs = add(Z(i) for i in range(n_qubits))
ex = expectation(h_block, obs)
wf = tensor([[0.5000+0.j, 0.5000+0.j, 0.5000+0.j, 0.5000+0.j]])
xs = [Counter({'11': 262, '01': 252, '10': 244, '00': 242})]
ex = tensor([[0.]], requires_grad=True)

Execution via QuantumCircuit and QuantumModel

More fine-grained control and better performance is provided via the high-level QuantumModel abstraction. Quantum programs in Qadence are constructed in two steps:

  1. Build a QuantumCircuit which ties together a composite block and a register.
  2. Define a QuantumModel which differentiates, compiles and executes the circuit.

Execution of more complex Qadence programs will be explored in the next tutorials.