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 57de9f69c54942f4a27b2c5e9a7daa9e 0 36dee175323f49059c952f49cc92c0b6 RX(0.5) 57de9f69c54942f4a27b2c5e9a7daa9e--36dee175323f49059c952f49cc92c0b6 ee9705bd69ee4812ae90d8132e2a2202 1 7d7d04309ffd49708b3beb6ceb878cea 36dee175323f49059c952f49cc92c0b6--7d7d04309ffd49708b3beb6ceb878cea 4af60fc8d0034d48aaee48935d85f6bb 7d7d04309ffd49708b3beb6ceb878cea--4af60fc8d0034d48aaee48935d85f6bb ca17a4ed33784cadbda1dbf664540155 5b38923a61ff49788afb43abbe82fa61 ee9705bd69ee4812ae90d8132e2a2202--5b38923a61ff49788afb43abbe82fa61 42269c62c54c4e6083009566f65a2c0c X 5b38923a61ff49788afb43abbe82fa61--42269c62c54c4e6083009566f65a2c0c 42269c62c54c4e6083009566f65a2c0c--7d7d04309ffd49708b3beb6ceb878cea 42269c62c54c4e6083009566f65a2c0c--ca17a4ed33784cadbda1dbf664540155

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 c56c9a0ba2b948a0b34a4cba5197a8d3 0 683abf8191c54e9580e5f43368edcc4c X c56c9a0ba2b948a0b34a4cba5197a8d3--683abf8191c54e9580e5f43368edcc4c a2311b9c9df44aa28ff33cabcae3855a 1 436da690482b4ba2ab62e63cf38dfda0 683abf8191c54e9580e5f43368edcc4c--436da690482b4ba2ab62e63cf38dfda0 81a574856a794f369adc23fcd5897855 dfbce1d7699f4a81be11dc7c01e56bda Y a2311b9c9df44aa28ff33cabcae3855a--dfbce1d7699f4a81be11dc7c01e56bda dfbce1d7699f4a81be11dc7c01e56bda--81a574856a794f369adc23fcd5897855

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 8b6ab6e69c3744daa446c4b6bbb975c1 0 52ea7d419d524d17920576619e3e892b X 8b6ab6e69c3744daa446c4b6bbb975c1--52ea7d419d524d17920576619e3e892b 26b04e3e16b44886a6c8727ccccbc758 1 1db94bbd1cca4a2cb35198d44a9352a2 Y 52ea7d419d524d17920576619e3e892b--1db94bbd1cca4a2cb35198d44a9352a2 e8242d77feb24ab7bfc4a7367e7e5f41 1db94bbd1cca4a2cb35198d44a9352a2--e8242d77feb24ab7bfc4a7367e7e5f41 09eeb14ec0dc4ebc9f4da162c6d4e35e 0db81d440c874f74b996b58f68c131ad X 26b04e3e16b44886a6c8727ccccbc758--0db81d440c874f74b996b58f68c131ad b7571a7943fe4a31898d400482649e9b Y 0db81d440c874f74b996b58f68c131ad--b7571a7943fe4a31898d400482649e9b b7571a7943fe4a31898d400482649e9b--09eeb14ec0dc4ebc9f4da162c6d4e35e

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 4f1ee9ec4eb24a8e87629e4d1c51233b 0 b898b9c6dde04f678a2ca7e0fd218190 H 4f1ee9ec4eb24a8e87629e4d1c51233b--b898b9c6dde04f678a2ca7e0fd218190 6ec2614d26874b279ad265745f3e2cf2 1 639bdf6effb04fe8b2fd8e13058c6bdf PHASE(1.571) b898b9c6dde04f678a2ca7e0fd218190--639bdf6effb04fe8b2fd8e13058c6bdf 2d758e7f32cc4a62b0c765213cf12a9f PHASE(0.785) 639bdf6effb04fe8b2fd8e13058c6bdf--2d758e7f32cc4a62b0c765213cf12a9f 8887294a580a4dfaa683171991ea005a 639bdf6effb04fe8b2fd8e13058c6bdf--8887294a580a4dfaa683171991ea005a 7714432784424b55b7133181f2c42f80 2d758e7f32cc4a62b0c765213cf12a9f--7714432784424b55b7133181f2c42f80 0df19ecf4f12445d83018e19ffdb3dad 2d758e7f32cc4a62b0c765213cf12a9f--0df19ecf4f12445d83018e19ffdb3dad 76fd0bfddeea454bbdef926f61c04c0f 7714432784424b55b7133181f2c42f80--76fd0bfddeea454bbdef926f61c04c0f cc4337c0d8e44e90829a052b3f9a639d 76fd0bfddeea454bbdef926f61c04c0f--cc4337c0d8e44e90829a052b3f9a639d 4e07d1b7fb5c40f385383b827d73df93 cc4337c0d8e44e90829a052b3f9a639d--4e07d1b7fb5c40f385383b827d73df93 0d509f7b76464d94b9373a3fcafa333b d3a77167785140e8a34c8bcec7651bbc 6ec2614d26874b279ad265745f3e2cf2--d3a77167785140e8a34c8bcec7651bbc bc6efeea37ef4982aabbb2355646847c 2 d3a77167785140e8a34c8bcec7651bbc--8887294a580a4dfaa683171991ea005a 47446bdcf51143e98fbc821221ac8ef1 8887294a580a4dfaa683171991ea005a--47446bdcf51143e98fbc821221ac8ef1 573394821a9d482a9ca840c2d25ef460 H 47446bdcf51143e98fbc821221ac8ef1--573394821a9d482a9ca840c2d25ef460 a603bf4988fa46d2a7acf83091cbb021 PHASE(1.571) 573394821a9d482a9ca840c2d25ef460--a603bf4988fa46d2a7acf83091cbb021 c703b509e2354092937177737e5ddfce a603bf4988fa46d2a7acf83091cbb021--c703b509e2354092937177737e5ddfce 3b551ffe26d9430bb76704326a638262 a603bf4988fa46d2a7acf83091cbb021--3b551ffe26d9430bb76704326a638262 c703b509e2354092937177737e5ddfce--0d509f7b76464d94b9373a3fcafa333b 8c459b8f006c414db4608162fa660b06 02a18ac013664c4388153530b2ced274 bc6efeea37ef4982aabbb2355646847c--02a18ac013664c4388153530b2ced274 ac50a88858574a19878897aa79f66ee6 02a18ac013664c4388153530b2ced274--ac50a88858574a19878897aa79f66ee6 ac50a88858574a19878897aa79f66ee6--0df19ecf4f12445d83018e19ffdb3dad 3dac3da67a9d4ac69009da65c458c078 0df19ecf4f12445d83018e19ffdb3dad--3dac3da67a9d4ac69009da65c458c078 3dac3da67a9d4ac69009da65c458c078--3b551ffe26d9430bb76704326a638262 55d07432f5d14e69a0345121fa3d155d H 3b551ffe26d9430bb76704326a638262--55d07432f5d14e69a0345121fa3d155d 55d07432f5d14e69a0345121fa3d155d--8c459b8f006c414db4608162fa660b06

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 acc9ba9e37204b1786571e06ca5d6e81 0 0e30eb89f7ff451fbb1ac21ecf8dbe7c acc9ba9e37204b1786571e06ca5d6e81--0e30eb89f7ff451fbb1ac21ecf8dbe7c ad1a77721223404f87cafa7f66c5dc96 1 136412ca959a4616bc5665cbedc8ad41 0e30eb89f7ff451fbb1ac21ecf8dbe7c--136412ca959a4616bc5665cbedc8ad41 d56635122f184679b8e85ba48b07a9ef 136412ca959a4616bc5665cbedc8ad41--d56635122f184679b8e85ba48b07a9ef 20896232d1f54d1a8f1d260f575f614d PHASE(-0.785) d56635122f184679b8e85ba48b07a9ef--20896232d1f54d1a8f1d260f575f614d c3c584d8b8094da78206e30c2d4da094 PHASE(-1.571) 20896232d1f54d1a8f1d260f575f614d--c3c584d8b8094da78206e30c2d4da094 ca856c8951214cd39cd5118e3319ac15 20896232d1f54d1a8f1d260f575f614d--ca856c8951214cd39cd5118e3319ac15 8278cf9f88f147fa9bcefac0d4b33119 H c3c584d8b8094da78206e30c2d4da094--8278cf9f88f147fa9bcefac0d4b33119 294085ab5ab64dbaab345a999443c6a2 c3c584d8b8094da78206e30c2d4da094--294085ab5ab64dbaab345a999443c6a2 030f24e3f90a432dbdade94f66e33d1a 8278cf9f88f147fa9bcefac0d4b33119--030f24e3f90a432dbdade94f66e33d1a 2b252d3600e84cee864a8609efde1df3 e6ee715cfafd420bbba0c88c8dccf122 ad1a77721223404f87cafa7f66c5dc96--e6ee715cfafd420bbba0c88c8dccf122 0d116c8b79004c9d9969a48a45386ad0 2 c77ffaec4efb4a83b50431f2080daad6 PHASE(-1.571) e6ee715cfafd420bbba0c88c8dccf122--c77ffaec4efb4a83b50431f2080daad6 78c2f19c3ac24c118c37fa24346f9c34 H c77ffaec4efb4a83b50431f2080daad6--78c2f19c3ac24c118c37fa24346f9c34 764fae4f6fe94be9ad4ff57484e8b218 c77ffaec4efb4a83b50431f2080daad6--764fae4f6fe94be9ad4ff57484e8b218 6596618215924cec8642d23a4ac3c11e 78c2f19c3ac24c118c37fa24346f9c34--6596618215924cec8642d23a4ac3c11e 6596618215924cec8642d23a4ac3c11e--294085ab5ab64dbaab345a999443c6a2 6a0bb706082c4d92965036b925ac2fee 294085ab5ab64dbaab345a999443c6a2--6a0bb706082c4d92965036b925ac2fee 6a0bb706082c4d92965036b925ac2fee--2b252d3600e84cee864a8609efde1df3 7cc5d817b60f45a3933d32bed91ad05d d59a631ef0504b2682d18d70cd19e8e7 H 0d116c8b79004c9d9969a48a45386ad0--d59a631ef0504b2682d18d70cd19e8e7 d59a631ef0504b2682d18d70cd19e8e7--764fae4f6fe94be9ad4ff57484e8b218 80e962abdf7d4a81aa139842f956756c 764fae4f6fe94be9ad4ff57484e8b218--80e962abdf7d4a81aa139842f956756c 80e962abdf7d4a81aa139842f956756c--ca856c8951214cd39cd5118e3319ac15 16f32e3eea6d4965b637ffb2bee7c38e ca856c8951214cd39cd5118e3319ac15--16f32e3eea6d4965b637ffb2bee7c38e 3dffd1e4a6a242dbbfc715afd13122e0 16f32e3eea6d4965b637ffb2bee7c38e--3dffd1e4a6a242dbbfc715afd13122e0 3dffd1e4a6a242dbbfc715afd13122e0--7cc5d817b60f45a3933d32bed91ad05d

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_ec33d8adda7949db89aeeca56fa43e7f f718fab085a3489d9c5b6c8a7a5f7f25 0 59ac85cf414c441383bd66c4e12877bf RX(0.0) f718fab085a3489d9c5b6c8a7a5f7f25--59ac85cf414c441383bd66c4e12877bf 895e9a5a4b774ff79909f246bbc4557f 1 8e3452fee10b417c84a7743ca1a9f49b HamEvo 59ac85cf414c441383bd66c4e12877bf--8e3452fee10b417c84a7743ca1a9f49b 6ab25d6a3608453285abcf0a22891195 RX(0.0) 8e3452fee10b417c84a7743ca1a9f49b--6ab25d6a3608453285abcf0a22891195 5f4ae1b62ca746e4afe96a28a10a50d3 6ab25d6a3608453285abcf0a22891195--5f4ae1b62ca746e4afe96a28a10a50d3 f0f3d268661949d4810d302dd925f4a8 a32d42fa1cb74f10ae230e0742d10924 RX(1.571) 895e9a5a4b774ff79909f246bbc4557f--a32d42fa1cb74f10ae230e0742d10924 627a80bf4f0d476f8f4dd1f637923dc2 2 14be77932db241189dfa1da8185eafef t = 1.000 a32d42fa1cb74f10ae230e0742d10924--14be77932db241189dfa1da8185eafef 43157c875e584633b4b3ee1d8b395bce RX(1.571) 14be77932db241189dfa1da8185eafef--43157c875e584633b4b3ee1d8b395bce 43157c875e584633b4b3ee1d8b395bce--f0f3d268661949d4810d302dd925f4a8 5b43fb5bb08641c2a16b934a195e6a3b 423f19c51d334028b019c5e6f5890b41 RX(3.142) 627a80bf4f0d476f8f4dd1f637923dc2--423f19c51d334028b019c5e6f5890b41 34cac631bb98483699979ff2d43e4944 423f19c51d334028b019c5e6f5890b41--34cac631bb98483699979ff2d43e4944 58d41b2a675d4e23b2d13d3137a2e0d1 RX(3.142) 34cac631bb98483699979ff2d43e4944--58d41b2a675d4e23b2d13d3137a2e0d1 58d41b2a675d4e23b2d13d3137a2e0d1--5b43fb5bb08641c2a16b934a195e6a3b

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': 255, '00': 252, '11': 252, '10': 241})]
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.