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 bed5c210335044298d8dc832ea761188 0 d60a1f473f1d499593a4bb2503093154 RX(0.5) bed5c210335044298d8dc832ea761188--d60a1f473f1d499593a4bb2503093154 318d7a1ee6b247c5a875c30f78f71eb1 1 f16deccd383846b5b2bba75ac249a9be d60a1f473f1d499593a4bb2503093154--f16deccd383846b5b2bba75ac249a9be 435443c7c2ed48269b4538fabea89b99 f16deccd383846b5b2bba75ac249a9be--435443c7c2ed48269b4538fabea89b99 669d212ccd92483ab61e8cac7594155b f01cf7d5c9b4471b9293e82cfa4c2605 318d7a1ee6b247c5a875c30f78f71eb1--f01cf7d5c9b4471b9293e82cfa4c2605 286ecb7a74144cf59de3b4a86f5a088d X f01cf7d5c9b4471b9293e82cfa4c2605--286ecb7a74144cf59de3b4a86f5a088d 286ecb7a74144cf59de3b4a86f5a088d--f16deccd383846b5b2bba75ac249a9be 286ecb7a74144cf59de3b4a86f5a088d--669d212ccd92483ab61e8cac7594155b

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 1f6861b6eb4248e1b47f7a59cd65de11 0 623385489c434e328468b8b9e4d003f4 X 1f6861b6eb4248e1b47f7a59cd65de11--623385489c434e328468b8b9e4d003f4 ddbbb51815ab4802bf3a0461ea8fd055 1 24a8da94131e4cd6ad1bf7a833d265a6 623385489c434e328468b8b9e4d003f4--24a8da94131e4cd6ad1bf7a833d265a6 00b142d9474e4663b9f49b731cb034c4 88afafdb061741d7ac0302eda6b29adc Y ddbbb51815ab4802bf3a0461ea8fd055--88afafdb061741d7ac0302eda6b29adc 88afafdb061741d7ac0302eda6b29adc--00b142d9474e4663b9f49b731cb034c4

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 8ed773b8a67e4c73a8c2d8b4068dc2a7 0 be2f49a62bd948c08f4307b4dba9ab9e X 8ed773b8a67e4c73a8c2d8b4068dc2a7--be2f49a62bd948c08f4307b4dba9ab9e 8990ca238049419daa25fa235b215689 1 80afa78f857b4e8fae4fc03d9fe7fc69 Y be2f49a62bd948c08f4307b4dba9ab9e--80afa78f857b4e8fae4fc03d9fe7fc69 816de3aced3e4abca2f78057f4a561e1 80afa78f857b4e8fae4fc03d9fe7fc69--816de3aced3e4abca2f78057f4a561e1 0d9f35c801d449999852b5662df8021a 437b1459ec044e988be66f7aafd53de5 X 8990ca238049419daa25fa235b215689--437b1459ec044e988be66f7aafd53de5 6dcf0b1be2884ef7a5d1202af33cc01c Y 437b1459ec044e988be66f7aafd53de5--6dcf0b1be2884ef7a5d1202af33cc01c 6dcf0b1be2884ef7a5d1202af33cc01c--0d9f35c801d449999852b5662df8021a

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 281c7c5b8362404bb8a662fe618928f7 0 1c931fa936824daca9bce570767afff9 H 281c7c5b8362404bb8a662fe618928f7--1c931fa936824daca9bce570767afff9 ed826d3ac81c421882dd24f7686c4ad5 1 0dc78c4d803644c283e790d1f2db2c04 PHASE(1.571) 1c931fa936824daca9bce570767afff9--0dc78c4d803644c283e790d1f2db2c04 1f5cf61ac75a41eea62e8f0b59ce52bc PHASE(0.785) 0dc78c4d803644c283e790d1f2db2c04--1f5cf61ac75a41eea62e8f0b59ce52bc 2d1f43a6525448e9a1958ed792f2c217 0dc78c4d803644c283e790d1f2db2c04--2d1f43a6525448e9a1958ed792f2c217 e18cd5301ef94fe3b937977d1557f758 1f5cf61ac75a41eea62e8f0b59ce52bc--e18cd5301ef94fe3b937977d1557f758 32f97940c2764ac5aca33adfdee38a4d 1f5cf61ac75a41eea62e8f0b59ce52bc--32f97940c2764ac5aca33adfdee38a4d 00fbf5bc5e7d4744a1fd889b3d0d28c9 e18cd5301ef94fe3b937977d1557f758--00fbf5bc5e7d4744a1fd889b3d0d28c9 33f5405ff94f481fbee67c922114c182 00fbf5bc5e7d4744a1fd889b3d0d28c9--33f5405ff94f481fbee67c922114c182 26e4d3da32e0442f9e6d576fae2e1076 33f5405ff94f481fbee67c922114c182--26e4d3da32e0442f9e6d576fae2e1076 9a1734fe5c7d42f68f82a8b106876313 cd362c6acc504062a8984b4f606c1300 ed826d3ac81c421882dd24f7686c4ad5--cd362c6acc504062a8984b4f606c1300 9ca511b3d7ce45da8463bb01c9316da5 2 cd362c6acc504062a8984b4f606c1300--2d1f43a6525448e9a1958ed792f2c217 7bf5e56b95f54a1eae99e639e62484b1 2d1f43a6525448e9a1958ed792f2c217--7bf5e56b95f54a1eae99e639e62484b1 c918960bbd48408c8106c5477ae9ce74 H 7bf5e56b95f54a1eae99e639e62484b1--c918960bbd48408c8106c5477ae9ce74 eb747c1c311740b4b7fd629690e2fc2d PHASE(1.571) c918960bbd48408c8106c5477ae9ce74--eb747c1c311740b4b7fd629690e2fc2d f44a52ab5acb4352b99de8885947d556 eb747c1c311740b4b7fd629690e2fc2d--f44a52ab5acb4352b99de8885947d556 0d760ebe5b944b3cb9d369a02006f6a6 eb747c1c311740b4b7fd629690e2fc2d--0d760ebe5b944b3cb9d369a02006f6a6 f44a52ab5acb4352b99de8885947d556--9a1734fe5c7d42f68f82a8b106876313 384f37c19b1243c488c056d93f1424bd 021a2e73f3314085b2ba483fc209a123 9ca511b3d7ce45da8463bb01c9316da5--021a2e73f3314085b2ba483fc209a123 cc64b82f419f4bfdb73b3ff61daf7d60 021a2e73f3314085b2ba483fc209a123--cc64b82f419f4bfdb73b3ff61daf7d60 cc64b82f419f4bfdb73b3ff61daf7d60--32f97940c2764ac5aca33adfdee38a4d 7e76f0374e134120ae9e99cb08b3e376 32f97940c2764ac5aca33adfdee38a4d--7e76f0374e134120ae9e99cb08b3e376 7e76f0374e134120ae9e99cb08b3e376--0d760ebe5b944b3cb9d369a02006f6a6 99c809e0fc5748149838801df9d1f35e H 0d760ebe5b944b3cb9d369a02006f6a6--99c809e0fc5748149838801df9d1f35e 99c809e0fc5748149838801df9d1f35e--384f37c19b1243c488c056d93f1424bd

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 9948c2c865f14ef6a8d29845b4d9babb 0 7e776e55c00b4a3398a8da488d95c09e 9948c2c865f14ef6a8d29845b4d9babb--7e776e55c00b4a3398a8da488d95c09e bf715d6a6acf4159a2d66a59d132152c 1 07834187b6c946a090d2d8597644da58 7e776e55c00b4a3398a8da488d95c09e--07834187b6c946a090d2d8597644da58 ff2acac035804dc88bbc8f99337d26ec 07834187b6c946a090d2d8597644da58--ff2acac035804dc88bbc8f99337d26ec ff6df76565174281b8407f7faef26697 PHASE(-0.785) ff2acac035804dc88bbc8f99337d26ec--ff6df76565174281b8407f7faef26697 0b2b6f8f30a34b2da59f36763fbf4cfd PHASE(-1.571) ff6df76565174281b8407f7faef26697--0b2b6f8f30a34b2da59f36763fbf4cfd 5fe3392e226a43f68ea748e893153ecf ff6df76565174281b8407f7faef26697--5fe3392e226a43f68ea748e893153ecf 2de3e507d9fc40a5806439682f10fee5 H 0b2b6f8f30a34b2da59f36763fbf4cfd--2de3e507d9fc40a5806439682f10fee5 9014b89e22294f469264a77ac652914e 0b2b6f8f30a34b2da59f36763fbf4cfd--9014b89e22294f469264a77ac652914e abd3badb359749bfb24db1c997a3af2c 2de3e507d9fc40a5806439682f10fee5--abd3badb359749bfb24db1c997a3af2c 6e2f484674754430a0bf6109fe77b2c0 4842692e19bc4bc98071730bef0d8184 bf715d6a6acf4159a2d66a59d132152c--4842692e19bc4bc98071730bef0d8184 7fbcef5cebe04567b87143a460709a0f 2 21018284e25c446e80718089f7bd0deb PHASE(-1.571) 4842692e19bc4bc98071730bef0d8184--21018284e25c446e80718089f7bd0deb c7c1af15e7504d20b3616160b08cb061 H 21018284e25c446e80718089f7bd0deb--c7c1af15e7504d20b3616160b08cb061 b0ae784342e044dbb126dc39f7cbd7af 21018284e25c446e80718089f7bd0deb--b0ae784342e044dbb126dc39f7cbd7af 0bebc72e3452409591c1ba9cfd80ae25 c7c1af15e7504d20b3616160b08cb061--0bebc72e3452409591c1ba9cfd80ae25 0bebc72e3452409591c1ba9cfd80ae25--9014b89e22294f469264a77ac652914e a8531e491dd54bccb33cfa35b5b34798 9014b89e22294f469264a77ac652914e--a8531e491dd54bccb33cfa35b5b34798 a8531e491dd54bccb33cfa35b5b34798--6e2f484674754430a0bf6109fe77b2c0 122e9351d8ed4f5ebb879837f230115f 4620f1416342443ca2b1388304dc989d H 7fbcef5cebe04567b87143a460709a0f--4620f1416342443ca2b1388304dc989d 4620f1416342443ca2b1388304dc989d--b0ae784342e044dbb126dc39f7cbd7af 9b451e047a7e486c850405f517b3c11b b0ae784342e044dbb126dc39f7cbd7af--9b451e047a7e486c850405f517b3c11b 9b451e047a7e486c850405f517b3c11b--5fe3392e226a43f68ea748e893153ecf f6cd819424004b8b81e8b81b25aa62ee 5fe3392e226a43f68ea748e893153ecf--f6cd819424004b8b81e8b81b25aa62ee ffb539c3209a46c381c2adc38ad80a47 f6cd819424004b8b81e8b81b25aa62ee--ffb539c3209a46c381c2adc38ad80a47 ffb539c3209a46c381c2adc38ad80a47--122e9351d8ed4f5ebb879837f230115f

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_bb4a211aca2241a6bc1edda4ab1ff760 fcfc91dc900d412dac5f5e8abc9b1784 0 b0a8e5a5c04d4c11ae8f934277333708 RX(0.0) fcfc91dc900d412dac5f5e8abc9b1784--b0a8e5a5c04d4c11ae8f934277333708 c96d9fdc89ec44af96562ea04e73d86c 1 fbfee0ab95d84bee82e28cca6962f463 HamEvo b0a8e5a5c04d4c11ae8f934277333708--fbfee0ab95d84bee82e28cca6962f463 40e3389e68334533b73962b010d770f9 RX(0.0) fbfee0ab95d84bee82e28cca6962f463--40e3389e68334533b73962b010d770f9 43e37350c4cf4dbfb4cd815db01cf150 40e3389e68334533b73962b010d770f9--43e37350c4cf4dbfb4cd815db01cf150 f39a6a05457c4b60b0996230b0633660 080c27ad59024f9f8ef6309f73c4fff4 RX(1.571) c96d9fdc89ec44af96562ea04e73d86c--080c27ad59024f9f8ef6309f73c4fff4 4ad66037939744ebbad5ff52eb82c1be 2 2041c9509b564db7925db0bc436f8662 t = 1.000 080c27ad59024f9f8ef6309f73c4fff4--2041c9509b564db7925db0bc436f8662 85cead3bcf574963802b69a9ac347545 RX(1.571) 2041c9509b564db7925db0bc436f8662--85cead3bcf574963802b69a9ac347545 85cead3bcf574963802b69a9ac347545--f39a6a05457c4b60b0996230b0633660 bebfeb79e029409c9f1075ca5b0659c9 ff68f36d3df34bbe978d125ce0eb19e0 RX(3.142) 4ad66037939744ebbad5ff52eb82c1be--ff68f36d3df34bbe978d125ce0eb19e0 3275becfed6b4d1c9e32d1fb9e146158 ff68f36d3df34bbe978d125ce0eb19e0--3275becfed6b4d1c9e32d1fb9e146158 89d95b14301440e7b1f20648749fc620 RX(3.142) 3275becfed6b4d1c9e32d1fb9e146158--89d95b14301440e7b1f20648749fc620 89d95b14301440e7b1f20648749fc620--bebfeb79e029409c9f1075ca5b0659c9

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': 258, '10': 256, '00': 253, '11': 233})]
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.

Adding noise to gates

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