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 37b6914c1cf64f728979dd86dbe2f389 0 22114b87716d47a18f204eaf9e93d37d RX(0.5) 37b6914c1cf64f728979dd86dbe2f389--22114b87716d47a18f204eaf9e93d37d a5496d49d0ac4d17a3ac0ccbe855d980 1 59e0d68cface4f08b94aca5e57420c82 22114b87716d47a18f204eaf9e93d37d--59e0d68cface4f08b94aca5e57420c82 7b5e814cb8514620b2496c91d4a437f0 59e0d68cface4f08b94aca5e57420c82--7b5e814cb8514620b2496c91d4a437f0 6a1d90fb8e084dd9b0bea7b77ca7026a 8bf5c53f42d54db1933be50d796cb6f5 a5496d49d0ac4d17a3ac0ccbe855d980--8bf5c53f42d54db1933be50d796cb6f5 3d11ef4415c3427aa455b2e4fbce0485 X 8bf5c53f42d54db1933be50d796cb6f5--3d11ef4415c3427aa455b2e4fbce0485 3d11ef4415c3427aa455b2e4fbce0485--59e0d68cface4f08b94aca5e57420c82 3d11ef4415c3427aa455b2e4fbce0485--6a1d90fb8e084dd9b0bea7b77ca7026a

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 e139b9128428421ba550a715aceb17e3 0 669880d4449c4a3d839d42d2219884f7 X e139b9128428421ba550a715aceb17e3--669880d4449c4a3d839d42d2219884f7 0ffad60802d141b7874695faa367d7eb 1 9e10c8568ee0464ea1a0bedd851c8054 669880d4449c4a3d839d42d2219884f7--9e10c8568ee0464ea1a0bedd851c8054 59603b2274754f589a85abac20e09f61 8b44aa9c972b4dba875054eb74ad0efa Y 0ffad60802d141b7874695faa367d7eb--8b44aa9c972b4dba875054eb74ad0efa 8b44aa9c972b4dba875054eb74ad0efa--59603b2274754f589a85abac20e09f61

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 63edce67e05a421a8c69bd78b8d2fd68 0 1f560c9403ed44ccadb6f594393f1909 X 63edce67e05a421a8c69bd78b8d2fd68--1f560c9403ed44ccadb6f594393f1909 5be332c144854b718deb703007d5aad2 1 f3142211bc294678b69f92fb6d1b9dbb Y 1f560c9403ed44ccadb6f594393f1909--f3142211bc294678b69f92fb6d1b9dbb 74be37e6ad0845d8b54bc92fc2977abd f3142211bc294678b69f92fb6d1b9dbb--74be37e6ad0845d8b54bc92fc2977abd 5625f078c50f420d99d08703871ba45c 051d1661a78143c4b1580e4f476dbf67 X 5be332c144854b718deb703007d5aad2--051d1661a78143c4b1580e4f476dbf67 236ca5cceca9458fbc24e27e58114357 Y 051d1661a78143c4b1580e4f476dbf67--236ca5cceca9458fbc24e27e58114357 236ca5cceca9458fbc24e27e58114357--5625f078c50f420d99d08703871ba45c

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 7b718d963c634dd58055f9a6d30561f2 0 935098b513c94040bd57007f055fe8ab H 7b718d963c634dd58055f9a6d30561f2--935098b513c94040bd57007f055fe8ab 12f30f0584154d29ba5a71654db182b6 1 95ac6c8b456744fe913a16814225774f PHASE(1.571) 935098b513c94040bd57007f055fe8ab--95ac6c8b456744fe913a16814225774f 9c7cbdad2d0f46f1acb34033a1f65aae PHASE(0.785) 95ac6c8b456744fe913a16814225774f--9c7cbdad2d0f46f1acb34033a1f65aae 0441fe587efb49378a74d5b7a52b385a 95ac6c8b456744fe913a16814225774f--0441fe587efb49378a74d5b7a52b385a cb624af3eb66457cb682c96a210daae1 9c7cbdad2d0f46f1acb34033a1f65aae--cb624af3eb66457cb682c96a210daae1 ee57a2a45f914b36a274c133cb13a72f 9c7cbdad2d0f46f1acb34033a1f65aae--ee57a2a45f914b36a274c133cb13a72f 336cdf291d7749dab4f9a4312b73af5d cb624af3eb66457cb682c96a210daae1--336cdf291d7749dab4f9a4312b73af5d 07e945bb27db4c1b8f5a521b55250985 336cdf291d7749dab4f9a4312b73af5d--07e945bb27db4c1b8f5a521b55250985 663edeb6d8774847ae272b1b976069b8 07e945bb27db4c1b8f5a521b55250985--663edeb6d8774847ae272b1b976069b8 9e52763e23c0472ab5ba903f986f72ed 8b36c00c1f0a4a80b60487353113b47c 12f30f0584154d29ba5a71654db182b6--8b36c00c1f0a4a80b60487353113b47c df19da3e9a6f4aa2996898344c753828 2 8b36c00c1f0a4a80b60487353113b47c--0441fe587efb49378a74d5b7a52b385a f1294bba421545049f13c636c4736ca0 0441fe587efb49378a74d5b7a52b385a--f1294bba421545049f13c636c4736ca0 c52f5d02428240f5b47a4484ae0a87b3 H f1294bba421545049f13c636c4736ca0--c52f5d02428240f5b47a4484ae0a87b3 abf3d8b9af92485396938ef6fb469111 PHASE(1.571) c52f5d02428240f5b47a4484ae0a87b3--abf3d8b9af92485396938ef6fb469111 fe4992a6c85b4991bded949c419e4e58 abf3d8b9af92485396938ef6fb469111--fe4992a6c85b4991bded949c419e4e58 f7d21723e2724c23a059cd20e044c7dd abf3d8b9af92485396938ef6fb469111--f7d21723e2724c23a059cd20e044c7dd fe4992a6c85b4991bded949c419e4e58--9e52763e23c0472ab5ba903f986f72ed 9fa94579a1274ee392885ce35fdd2ca0 66e30e5b9ce14f798400b74713d31021 df19da3e9a6f4aa2996898344c753828--66e30e5b9ce14f798400b74713d31021 b034e79f785a4506a4fb27d85757cfbc 66e30e5b9ce14f798400b74713d31021--b034e79f785a4506a4fb27d85757cfbc b034e79f785a4506a4fb27d85757cfbc--ee57a2a45f914b36a274c133cb13a72f c04def7d9a75464f9d0869adf18149ab ee57a2a45f914b36a274c133cb13a72f--c04def7d9a75464f9d0869adf18149ab c04def7d9a75464f9d0869adf18149ab--f7d21723e2724c23a059cd20e044c7dd 22d81b9a5a874c0187d3f19df2c0b785 H f7d21723e2724c23a059cd20e044c7dd--22d81b9a5a874c0187d3f19df2c0b785 22d81b9a5a874c0187d3f19df2c0b785--9fa94579a1274ee392885ce35fdd2ca0

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 d8fd79d7738045f7a3a57abed90db52c 0 0246b085aedf4c438e18854c3ed5ea41 d8fd79d7738045f7a3a57abed90db52c--0246b085aedf4c438e18854c3ed5ea41 0d677edc18884b04a93d194e0d8bca9b 1 d4b8594277204d57bb6671337e9f7c8f 0246b085aedf4c438e18854c3ed5ea41--d4b8594277204d57bb6671337e9f7c8f 24c69aca66154c98b755ce8e50b23b42 d4b8594277204d57bb6671337e9f7c8f--24c69aca66154c98b755ce8e50b23b42 6176b9e6af374959afc1fc1df573f87d PHASE(-0.785) 24c69aca66154c98b755ce8e50b23b42--6176b9e6af374959afc1fc1df573f87d 24ca3b9e43a147ab9b514f84dec3e606 PHASE(-1.571) 6176b9e6af374959afc1fc1df573f87d--24ca3b9e43a147ab9b514f84dec3e606 641051fa17be4da38282376eabb401c5 6176b9e6af374959afc1fc1df573f87d--641051fa17be4da38282376eabb401c5 c8c25c1655914754af8b72189be3b81d H 24ca3b9e43a147ab9b514f84dec3e606--c8c25c1655914754af8b72189be3b81d 7747f6b5f0aa4481a75ed658b614da04 24ca3b9e43a147ab9b514f84dec3e606--7747f6b5f0aa4481a75ed658b614da04 ff4b120e6c6d47bcaad21c43332510c7 c8c25c1655914754af8b72189be3b81d--ff4b120e6c6d47bcaad21c43332510c7 5b0ad2eef1d84e93bb8b4d5c78f284f8 50b9ed1be2284211838a414b03327414 0d677edc18884b04a93d194e0d8bca9b--50b9ed1be2284211838a414b03327414 653604630e5e46d790b56d06548c4681 2 5ba7fcfc40d44a1d8e0750a66565b182 PHASE(-1.571) 50b9ed1be2284211838a414b03327414--5ba7fcfc40d44a1d8e0750a66565b182 6ba445f4a93e436eb1322f72af055ff5 H 5ba7fcfc40d44a1d8e0750a66565b182--6ba445f4a93e436eb1322f72af055ff5 61202b0c421a4a82a4ba712d68faf49e 5ba7fcfc40d44a1d8e0750a66565b182--61202b0c421a4a82a4ba712d68faf49e 87f7c318e5aa45a2816b05bd74e9f190 6ba445f4a93e436eb1322f72af055ff5--87f7c318e5aa45a2816b05bd74e9f190 87f7c318e5aa45a2816b05bd74e9f190--7747f6b5f0aa4481a75ed658b614da04 0094051c4033417199b0fc7c74401f47 7747f6b5f0aa4481a75ed658b614da04--0094051c4033417199b0fc7c74401f47 0094051c4033417199b0fc7c74401f47--5b0ad2eef1d84e93bb8b4d5c78f284f8 40ec8fb6352d4609a8cefe8f2460b895 66251aa0cd9f48bdbeb7b253be8f851e H 653604630e5e46d790b56d06548c4681--66251aa0cd9f48bdbeb7b253be8f851e 66251aa0cd9f48bdbeb7b253be8f851e--61202b0c421a4a82a4ba712d68faf49e c1def275a70c4385934dfe7f642d1055 61202b0c421a4a82a4ba712d68faf49e--c1def275a70c4385934dfe7f642d1055 c1def275a70c4385934dfe7f642d1055--641051fa17be4da38282376eabb401c5 70570030ceeb4dc69afb456b4c47b35e 641051fa17be4da38282376eabb401c5--70570030ceeb4dc69afb456b4c47b35e f20193de98374edf8d769bba0e62f19b 70570030ceeb4dc69afb456b4c47b35e--f20193de98374edf8d769bba0e62f19b f20193de98374edf8d769bba0e62f19b--40ec8fb6352d4609a8cefe8f2460b895

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_2a128256e2024d7da9896ef82d9cf292 ee4929266c0b443e946cdec4cf359f12 0 91fccab4e64d4fbda370df265ba3ce60 RX(0.0) ee4929266c0b443e946cdec4cf359f12--91fccab4e64d4fbda370df265ba3ce60 2320ab2e2aff4a51803b5c23a2aefb25 1 db449a439d5d49938c28b16a8ac371c0 HamEvo 91fccab4e64d4fbda370df265ba3ce60--db449a439d5d49938c28b16a8ac371c0 a8267e6930d44514a0ae223acc62dac0 RX(0.0) db449a439d5d49938c28b16a8ac371c0--a8267e6930d44514a0ae223acc62dac0 7fa8f284280c413c8dce099877ca7de5 a8267e6930d44514a0ae223acc62dac0--7fa8f284280c413c8dce099877ca7de5 b314720fc68345ed8c765f00cf8da97e 853f265d0c974abf84b9df92f40bd2a7 RX(1.571) 2320ab2e2aff4a51803b5c23a2aefb25--853f265d0c974abf84b9df92f40bd2a7 ed672e4520fb4d06b9c09303908586f8 2 1c23668116f640f4ad483c2e3d73bd31 t = 1.000 853f265d0c974abf84b9df92f40bd2a7--1c23668116f640f4ad483c2e3d73bd31 dabee7ce029b4b19a3e0deda77ea7fbd RX(1.571) 1c23668116f640f4ad483c2e3d73bd31--dabee7ce029b4b19a3e0deda77ea7fbd dabee7ce029b4b19a3e0deda77ea7fbd--b314720fc68345ed8c765f00cf8da97e 53c05f1de87b4caf9afe9a82da6b89bd 78b8691f23724d10a4e7ce0c17b6beb3 RX(3.142) ed672e4520fb4d06b9c09303908586f8--78b8691f23724d10a4e7ce0c17b6beb3 1f76defa497d434f95bf8fd060c9e7f3 78b8691f23724d10a4e7ce0c17b6beb3--1f76defa497d434f95bf8fd060c9e7f3 27c583d0b9774bcb85cc43165ccb2939 RX(3.142) 1f76defa497d434f95bf8fd060c9e7f3--27c583d0b9774bcb85cc43165ccb2939 27c583d0b9774bcb85cc43165ccb2939--53c05f1de87b4caf9afe9a82da6b89bd

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({'01': 265, '11': 249, '00': 244, '10': 242})]
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.