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 4bf2767cf95249258c0b0e65744212dd 0 f720e1333aeb43b3a07506ebea9ed0c2 RX(0.5) 4bf2767cf95249258c0b0e65744212dd--f720e1333aeb43b3a07506ebea9ed0c2 290b1c2d8e6b4f26af3de2d807a13a93 1 40d280f7b786424c967e4ff00c59e9e4 f720e1333aeb43b3a07506ebea9ed0c2--40d280f7b786424c967e4ff00c59e9e4 030eb7ecd0804cf0a5aa04c5f33f8b6d 40d280f7b786424c967e4ff00c59e9e4--030eb7ecd0804cf0a5aa04c5f33f8b6d 2184968ba7ee485abc78cf9b366790ab 6bdc9e3fb7b645ccb5c546ac72bbfded 290b1c2d8e6b4f26af3de2d807a13a93--6bdc9e3fb7b645ccb5c546ac72bbfded 3c763bb037b647c3a2084cdbe5c68b23 X 6bdc9e3fb7b645ccb5c546ac72bbfded--3c763bb037b647c3a2084cdbe5c68b23 3c763bb037b647c3a2084cdbe5c68b23--40d280f7b786424c967e4ff00c59e9e4 3c763bb037b647c3a2084cdbe5c68b23--2184968ba7ee485abc78cf9b366790ab

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 09659942d45944aea38169d6fbff3d5a 0 58f3c3c3fd7c498e8395897bfe1226ed X 09659942d45944aea38169d6fbff3d5a--58f3c3c3fd7c498e8395897bfe1226ed f6d96efe224947af88cc2d62d0c389b7 1 dc3bfaf8f52c4ababcd56506fe6ed9d3 58f3c3c3fd7c498e8395897bfe1226ed--dc3bfaf8f52c4ababcd56506fe6ed9d3 a832c1f7301e4b11aa4fc41ba07c7fd9 8277da3f13ea472bbcce3ec29e2aec76 Y f6d96efe224947af88cc2d62d0c389b7--8277da3f13ea472bbcce3ec29e2aec76 8277da3f13ea472bbcce3ec29e2aec76--a832c1f7301e4b11aa4fc41ba07c7fd9

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 305223effa6b4bb3b43d2b9ba675cf5c 0 9fc63cfed5c24d8eb8ec816a182fae42 X 305223effa6b4bb3b43d2b9ba675cf5c--9fc63cfed5c24d8eb8ec816a182fae42 937b2144f69e4e48bf63f2f679f8b9d5 1 7f7f7dd13e774f588a6a069110e56c20 Y 9fc63cfed5c24d8eb8ec816a182fae42--7f7f7dd13e774f588a6a069110e56c20 c70c9d0634cf48df84956d1f3e0b1233 7f7f7dd13e774f588a6a069110e56c20--c70c9d0634cf48df84956d1f3e0b1233 274f58055df747d49775b7335e5d898a 1fbbea0452b74ea9b6c148f635b1c257 X 937b2144f69e4e48bf63f2f679f8b9d5--1fbbea0452b74ea9b6c148f635b1c257 06b75ce83c2f4ab9b572f4705ae9a510 Y 1fbbea0452b74ea9b6c148f635b1c257--06b75ce83c2f4ab9b572f4705ae9a510 06b75ce83c2f4ab9b572f4705ae9a510--274f58055df747d49775b7335e5d898a

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 15b7ae935de14bf0889c93050dcddc35 0 3026948cfd984897b13c506e9d8ccce3 H 15b7ae935de14bf0889c93050dcddc35--3026948cfd984897b13c506e9d8ccce3 cf446b6a8ca54ccca022973e91db6d8d 1 8bb18d382dd94fa1a0add5cc13b1e8c8 PHASE(1.571) 3026948cfd984897b13c506e9d8ccce3--8bb18d382dd94fa1a0add5cc13b1e8c8 e1e1b2971c9c4879a073b2d3d7962ac5 PHASE(0.785) 8bb18d382dd94fa1a0add5cc13b1e8c8--e1e1b2971c9c4879a073b2d3d7962ac5 0496fc08f5714b4ca55399fd259ae3eb 8bb18d382dd94fa1a0add5cc13b1e8c8--0496fc08f5714b4ca55399fd259ae3eb 7baae3e0e0314517b33771a74ff04e90 e1e1b2971c9c4879a073b2d3d7962ac5--7baae3e0e0314517b33771a74ff04e90 6f8947d2750e429a9f1d968cd502460a e1e1b2971c9c4879a073b2d3d7962ac5--6f8947d2750e429a9f1d968cd502460a 3af7283eadf34758a934a370fda779a0 7baae3e0e0314517b33771a74ff04e90--3af7283eadf34758a934a370fda779a0 9b06205a6f024954bfa62f52dcd77677 3af7283eadf34758a934a370fda779a0--9b06205a6f024954bfa62f52dcd77677 20a50c44c7af422f955dc27dcc156bdf 9b06205a6f024954bfa62f52dcd77677--20a50c44c7af422f955dc27dcc156bdf 70532e726ae643c3a8bc380da27a13ae e060fcc7999949ca899c3e6177c3c3fb cf446b6a8ca54ccca022973e91db6d8d--e060fcc7999949ca899c3e6177c3c3fb 654d8cdbbe8a4a35ad58efc2cf656b61 2 e060fcc7999949ca899c3e6177c3c3fb--0496fc08f5714b4ca55399fd259ae3eb 25ebd3703c3c46dbb26b4603b1449015 0496fc08f5714b4ca55399fd259ae3eb--25ebd3703c3c46dbb26b4603b1449015 40ff44e8b71f45679fea1af79629c2df H 25ebd3703c3c46dbb26b4603b1449015--40ff44e8b71f45679fea1af79629c2df 4d6899d63c3c48f280157cb2cb69e0bc PHASE(1.571) 40ff44e8b71f45679fea1af79629c2df--4d6899d63c3c48f280157cb2cb69e0bc 3f7e7860006540f19a1c21f8b3bdac57 4d6899d63c3c48f280157cb2cb69e0bc--3f7e7860006540f19a1c21f8b3bdac57 11d2058f9d2a4670a01bc5c466bdf0b8 4d6899d63c3c48f280157cb2cb69e0bc--11d2058f9d2a4670a01bc5c466bdf0b8 3f7e7860006540f19a1c21f8b3bdac57--70532e726ae643c3a8bc380da27a13ae 34df684d03524230a8f7ea35de12daf5 7ea1c142eb6b40458772de3e140ba934 654d8cdbbe8a4a35ad58efc2cf656b61--7ea1c142eb6b40458772de3e140ba934 5f2d0bb4104c43a499ba0526fea17072 7ea1c142eb6b40458772de3e140ba934--5f2d0bb4104c43a499ba0526fea17072 5f2d0bb4104c43a499ba0526fea17072--6f8947d2750e429a9f1d968cd502460a 0f3932bbbcf04173a8502ac57c894e0b 6f8947d2750e429a9f1d968cd502460a--0f3932bbbcf04173a8502ac57c894e0b 0f3932bbbcf04173a8502ac57c894e0b--11d2058f9d2a4670a01bc5c466bdf0b8 ff8ad1706b704cee847e07a2e6bfeb6b H 11d2058f9d2a4670a01bc5c466bdf0b8--ff8ad1706b704cee847e07a2e6bfeb6b ff8ad1706b704cee847e07a2e6bfeb6b--34df684d03524230a8f7ea35de12daf5

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 a469f97fda7a4c0588570000dabcd239 0 876c8041da824089a64ae0f2df6b2528 a469f97fda7a4c0588570000dabcd239--876c8041da824089a64ae0f2df6b2528 ef9db54b03194593ac1095a0d5b4fd7e 1 84e74dfa3f2848ecaf94b094ff6965bd 876c8041da824089a64ae0f2df6b2528--84e74dfa3f2848ecaf94b094ff6965bd cace040df58346028b3d30d136e87a93 84e74dfa3f2848ecaf94b094ff6965bd--cace040df58346028b3d30d136e87a93 6b2179e7bc1a4a2ca28a66b7bbab9ecd PHASE(-0.785) cace040df58346028b3d30d136e87a93--6b2179e7bc1a4a2ca28a66b7bbab9ecd de7c8914ab7344099a38a9140255f896 PHASE(-1.571) 6b2179e7bc1a4a2ca28a66b7bbab9ecd--de7c8914ab7344099a38a9140255f896 e95cbff5a9f246eb988f877e1d7ba066 6b2179e7bc1a4a2ca28a66b7bbab9ecd--e95cbff5a9f246eb988f877e1d7ba066 cacb3470b9ce46249fd6ca3d6ea54bc0 H de7c8914ab7344099a38a9140255f896--cacb3470b9ce46249fd6ca3d6ea54bc0 e81397bb29da49a6b78575806804c911 de7c8914ab7344099a38a9140255f896--e81397bb29da49a6b78575806804c911 3914c84ccf2745e3ba67305809d3ba1a cacb3470b9ce46249fd6ca3d6ea54bc0--3914c84ccf2745e3ba67305809d3ba1a 9422b3cc9f8b4c5d9463c0578a515b03 090719e4e95d45e1a640995343a1c5b6 ef9db54b03194593ac1095a0d5b4fd7e--090719e4e95d45e1a640995343a1c5b6 13ae9216e5de4dd58b6430063190aaa6 2 980f4608092a450ba4b57785c0add415 PHASE(-1.571) 090719e4e95d45e1a640995343a1c5b6--980f4608092a450ba4b57785c0add415 6519f0f997154c7bb795a565d7ec8ddd H 980f4608092a450ba4b57785c0add415--6519f0f997154c7bb795a565d7ec8ddd 0a8317c1bdc94bd9b1558fea2c4d9cbc 980f4608092a450ba4b57785c0add415--0a8317c1bdc94bd9b1558fea2c4d9cbc 161b19c8dcf64ab586ad5e541354ac1f 6519f0f997154c7bb795a565d7ec8ddd--161b19c8dcf64ab586ad5e541354ac1f 161b19c8dcf64ab586ad5e541354ac1f--e81397bb29da49a6b78575806804c911 f2e8222e980643c7ab40e0ada18b8187 e81397bb29da49a6b78575806804c911--f2e8222e980643c7ab40e0ada18b8187 f2e8222e980643c7ab40e0ada18b8187--9422b3cc9f8b4c5d9463c0578a515b03 13b795fd6be1419c8863c80435f1b1c2 7d8bb7961d734da49422ef7159ba9985 H 13ae9216e5de4dd58b6430063190aaa6--7d8bb7961d734da49422ef7159ba9985 7d8bb7961d734da49422ef7159ba9985--0a8317c1bdc94bd9b1558fea2c4d9cbc 4ea054a224f84257ba4bfc7605479e05 0a8317c1bdc94bd9b1558fea2c4d9cbc--4ea054a224f84257ba4bfc7605479e05 4ea054a224f84257ba4bfc7605479e05--e95cbff5a9f246eb988f877e1d7ba066 41c126177fb141a581eab3e0d5fdf9d9 e95cbff5a9f246eb988f877e1d7ba066--41c126177fb141a581eab3e0d5fdf9d9 9e7ff8a73f314204a3799c64944bbc63 41c126177fb141a581eab3e0d5fdf9d9--9e7ff8a73f314204a3799c64944bbc63 9e7ff8a73f314204a3799c64944bbc63--13b795fd6be1419c8863c80435f1b1c2

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_518a5f0cc5c447f9b052dc1eea740a7d 65548fdc90cf46a49e3069dd3979526e 0 fdbc987b7a96442e8ae6923005c778f1 RX(0.0) 65548fdc90cf46a49e3069dd3979526e--fdbc987b7a96442e8ae6923005c778f1 6152fa65d6b7471b96a31a44aec4d4c9 1 b26421c08cc5446886547ff8cf3e7e70 HamEvo fdbc987b7a96442e8ae6923005c778f1--b26421c08cc5446886547ff8cf3e7e70 b8c81b5206aa48fc886e0dca2ffa7611 RX(0.0) b26421c08cc5446886547ff8cf3e7e70--b8c81b5206aa48fc886e0dca2ffa7611 bf7ec9fa32f74363a683d1a950f9ce66 b8c81b5206aa48fc886e0dca2ffa7611--bf7ec9fa32f74363a683d1a950f9ce66 caddbb66a60942c4b3a536e044a86446 b5f60499d5fb4442b72d3c8baf0cb217 RX(1.571) 6152fa65d6b7471b96a31a44aec4d4c9--b5f60499d5fb4442b72d3c8baf0cb217 db634841811e401aaa920f36045b82cf 2 3883febd25b34432855709efa9ed18a7 t = 1.000 b5f60499d5fb4442b72d3c8baf0cb217--3883febd25b34432855709efa9ed18a7 361d92f2bc0841a9992779c776ef0da6 RX(1.571) 3883febd25b34432855709efa9ed18a7--361d92f2bc0841a9992779c776ef0da6 361d92f2bc0841a9992779c776ef0da6--caddbb66a60942c4b3a536e044a86446 7b0cfd5d41944989b4237b58aa10a6dd 0e4a8a1c5ac1426da4ccdbbb04c78105 RX(3.142) db634841811e401aaa920f36045b82cf--0e4a8a1c5ac1426da4ccdbbb04c78105 8c87327633de49a096489b56da6a5222 0e4a8a1c5ac1426da4ccdbbb04c78105--8c87327633de49a096489b56da6a5222 f7238e3d089a41949b558725e92e6fd0 RX(3.142) 8c87327633de49a096489b56da6a5222--f7238e3d089a41949b558725e92e6fd0 f7238e3d089a41949b558725e92e6fd0--7b0cfd5d41944989b4237b58aa10a6dd

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 = [OrderedCounter({'01': 254, '11': 254, '10': 247, '00': 245})]
ex = tensor([[0.]])

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.

Adding noise to gates

It is possible to add noise to gates. Please refer to the noise tutorial here.