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 88e6c674d3294b70bca5926407fd78b4 0 78872b2575c541b9b5bf483b3332159e RX(0.5) 88e6c674d3294b70bca5926407fd78b4--78872b2575c541b9b5bf483b3332159e f053ea1d81df42afbf531ee34ee5357f 1 fbf5daa5899f4bd48b38d9a00b25aee0 78872b2575c541b9b5bf483b3332159e--fbf5daa5899f4bd48b38d9a00b25aee0 e52b9f886e034135bf3e21d6ff1eeeee fbf5daa5899f4bd48b38d9a00b25aee0--e52b9f886e034135bf3e21d6ff1eeeee a71d20470f0f4224877a9d699b3c0e64 45a01ce453a6423196d3bde4ac3952ad f053ea1d81df42afbf531ee34ee5357f--45a01ce453a6423196d3bde4ac3952ad 81b7b491e7944b01b7861c4a12c1b876 X 45a01ce453a6423196d3bde4ac3952ad--81b7b491e7944b01b7861c4a12c1b876 81b7b491e7944b01b7861c4a12c1b876--fbf5daa5899f4bd48b38d9a00b25aee0 81b7b491e7944b01b7861c4a12c1b876--a71d20470f0f4224877a9d699b3c0e64

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 e5248da52cb24d738a348177bd0481f1 0 3fceb102722e4355accb5d834c4b9f17 X e5248da52cb24d738a348177bd0481f1--3fceb102722e4355accb5d834c4b9f17 b59f0963363b4bf8956a2013fae9c2cf 1 db776a6e50374708a85f20edd8101e25 3fceb102722e4355accb5d834c4b9f17--db776a6e50374708a85f20edd8101e25 a415f6eaa6084688a27dcccc41d5102c 4bac16a28ca54acd8bd8852269a76e18 Y b59f0963363b4bf8956a2013fae9c2cf--4bac16a28ca54acd8bd8852269a76e18 4bac16a28ca54acd8bd8852269a76e18--a415f6eaa6084688a27dcccc41d5102c

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 3f509a618a15417192e71f1f35bc1ec0 0 112a465096a442169fa7b8ca362c6c0a X 3f509a618a15417192e71f1f35bc1ec0--112a465096a442169fa7b8ca362c6c0a a2bc666c8ec44d26a6492f17a76ce584 1 a54748fe6341416da014b29763d1e91c Y 112a465096a442169fa7b8ca362c6c0a--a54748fe6341416da014b29763d1e91c f7b54e8467d0407795bc6bbb08fc8009 a54748fe6341416da014b29763d1e91c--f7b54e8467d0407795bc6bbb08fc8009 8f723997fc354e7c9707387885436d75 a76d03991bcb4c7db8eb98ef476392f1 X a2bc666c8ec44d26a6492f17a76ce584--a76d03991bcb4c7db8eb98ef476392f1 de94a227642645a398f3a800eebc52f8 Y a76d03991bcb4c7db8eb98ef476392f1--de94a227642645a398f3a800eebc52f8 de94a227642645a398f3a800eebc52f8--8f723997fc354e7c9707387885436d75

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 71d2511e003748dea76b4ad2f8828f60 0 f7bdb1c0b8784aa0b56044feafdd83e0 H 71d2511e003748dea76b4ad2f8828f60--f7bdb1c0b8784aa0b56044feafdd83e0 003b99ce887b4e14a9971c85fc9e1351 1 93d2e9b6345b46a989c4f1170801c9e3 PHASE(1.571) f7bdb1c0b8784aa0b56044feafdd83e0--93d2e9b6345b46a989c4f1170801c9e3 c155f51eae7d42568de0b6311bffe404 PHASE(0.785) 93d2e9b6345b46a989c4f1170801c9e3--c155f51eae7d42568de0b6311bffe404 ad0181f546fc4183b8879efc169e50dd 93d2e9b6345b46a989c4f1170801c9e3--ad0181f546fc4183b8879efc169e50dd 951cf5d58eb44e7fa28955581e6cff83 c155f51eae7d42568de0b6311bffe404--951cf5d58eb44e7fa28955581e6cff83 ecc93b54c41d4a8c96e6f280fd0ea31a c155f51eae7d42568de0b6311bffe404--ecc93b54c41d4a8c96e6f280fd0ea31a 5c8a55cb5e8c45e19a38a1d0cebe7c3b 951cf5d58eb44e7fa28955581e6cff83--5c8a55cb5e8c45e19a38a1d0cebe7c3b 9ffe6eb93ca7401bb0e68c3cffddb990 5c8a55cb5e8c45e19a38a1d0cebe7c3b--9ffe6eb93ca7401bb0e68c3cffddb990 fd8ae440628940ad9877ce3cbbded62b 9ffe6eb93ca7401bb0e68c3cffddb990--fd8ae440628940ad9877ce3cbbded62b 6d70c89331424aaf88859c0c2203570c 073424f946fd41bcb69a2e889c80b192 003b99ce887b4e14a9971c85fc9e1351--073424f946fd41bcb69a2e889c80b192 0642661ba8b34c2e91b691ab704cfc13 2 073424f946fd41bcb69a2e889c80b192--ad0181f546fc4183b8879efc169e50dd d49fdafd30d447a3bee645673865c302 ad0181f546fc4183b8879efc169e50dd--d49fdafd30d447a3bee645673865c302 1c7a9261829b4365948b7cd7fc5ba011 H d49fdafd30d447a3bee645673865c302--1c7a9261829b4365948b7cd7fc5ba011 92179678e128493998baa216bb734ac7 PHASE(1.571) 1c7a9261829b4365948b7cd7fc5ba011--92179678e128493998baa216bb734ac7 ffa2cbac0f4349afaeb3deb5f94ae7ca 92179678e128493998baa216bb734ac7--ffa2cbac0f4349afaeb3deb5f94ae7ca 61a783dfb04441b8ab99539eab31f970 92179678e128493998baa216bb734ac7--61a783dfb04441b8ab99539eab31f970 ffa2cbac0f4349afaeb3deb5f94ae7ca--6d70c89331424aaf88859c0c2203570c fb8a24421131425eb0025dad11b7f552 8e7838e029f44c1a8a75ed1b7d400694 0642661ba8b34c2e91b691ab704cfc13--8e7838e029f44c1a8a75ed1b7d400694 6bc7d5c822a0465dabe375ca4b6d2fcf 8e7838e029f44c1a8a75ed1b7d400694--6bc7d5c822a0465dabe375ca4b6d2fcf 6bc7d5c822a0465dabe375ca4b6d2fcf--ecc93b54c41d4a8c96e6f280fd0ea31a b8c0601b25d24dc3bd867bc334de6b7b ecc93b54c41d4a8c96e6f280fd0ea31a--b8c0601b25d24dc3bd867bc334de6b7b b8c0601b25d24dc3bd867bc334de6b7b--61a783dfb04441b8ab99539eab31f970 bf86ecf1f83349638381409a4b3e403f H 61a783dfb04441b8ab99539eab31f970--bf86ecf1f83349638381409a4b3e403f bf86ecf1f83349638381409a4b3e403f--fb8a24421131425eb0025dad11b7f552

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 5d42e9c0ad6b4e30b50b3cbe100e8857 0 08a5b8f30d424f5d89f5b2b054a80ba9 5d42e9c0ad6b4e30b50b3cbe100e8857--08a5b8f30d424f5d89f5b2b054a80ba9 0c7e7ed647314c60b6936e6a16f9b7b4 1 e8d91e823ebc49f5aab172d1e53b3fa3 08a5b8f30d424f5d89f5b2b054a80ba9--e8d91e823ebc49f5aab172d1e53b3fa3 90d330f533b34453afbe3299647c48ef e8d91e823ebc49f5aab172d1e53b3fa3--90d330f533b34453afbe3299647c48ef 9c5e9894bdf44de391071258c3b4dd5b PHASE(-0.785) 90d330f533b34453afbe3299647c48ef--9c5e9894bdf44de391071258c3b4dd5b 14001fd1d1684a66aa44725d2fc1e070 PHASE(-1.571) 9c5e9894bdf44de391071258c3b4dd5b--14001fd1d1684a66aa44725d2fc1e070 38dc77b5923a4a1e9e8c68fc2b7f66fe 9c5e9894bdf44de391071258c3b4dd5b--38dc77b5923a4a1e9e8c68fc2b7f66fe d06426a857af4d579375940838f191fe H 14001fd1d1684a66aa44725d2fc1e070--d06426a857af4d579375940838f191fe 0c5a2400248146cca471aa81eaf2d9fb 14001fd1d1684a66aa44725d2fc1e070--0c5a2400248146cca471aa81eaf2d9fb 8b5c04febf5740b2b36fecc41472848f d06426a857af4d579375940838f191fe--8b5c04febf5740b2b36fecc41472848f ed49b6384dbe40fb9e73a3633a33e71f d9709b3e0dc14e629d35589211743d6c 0c7e7ed647314c60b6936e6a16f9b7b4--d9709b3e0dc14e629d35589211743d6c e804f986f04d41fb8af4877a30225094 2 49d59f4118a8442ebd3bc8aea3e56bdd PHASE(-1.571) d9709b3e0dc14e629d35589211743d6c--49d59f4118a8442ebd3bc8aea3e56bdd 71268cccf9f64f86aef721f82435fcd2 H 49d59f4118a8442ebd3bc8aea3e56bdd--71268cccf9f64f86aef721f82435fcd2 22c96401bcae478fa8ea21aa7442043a 49d59f4118a8442ebd3bc8aea3e56bdd--22c96401bcae478fa8ea21aa7442043a 9aeceb03f230421fa192e826ca6422a1 71268cccf9f64f86aef721f82435fcd2--9aeceb03f230421fa192e826ca6422a1 9aeceb03f230421fa192e826ca6422a1--0c5a2400248146cca471aa81eaf2d9fb 782a668a3c30419783004b34e1ddb585 0c5a2400248146cca471aa81eaf2d9fb--782a668a3c30419783004b34e1ddb585 782a668a3c30419783004b34e1ddb585--ed49b6384dbe40fb9e73a3633a33e71f f7ad41c5bff6462ba56eb35ef5e37746 00c7007c5c384c2fb2a44c06bfb35608 H e804f986f04d41fb8af4877a30225094--00c7007c5c384c2fb2a44c06bfb35608 00c7007c5c384c2fb2a44c06bfb35608--22c96401bcae478fa8ea21aa7442043a 0c0b1555547c4f10948d6af06fb6928b 22c96401bcae478fa8ea21aa7442043a--0c0b1555547c4f10948d6af06fb6928b 0c0b1555547c4f10948d6af06fb6928b--38dc77b5923a4a1e9e8c68fc2b7f66fe 94ab301715c446658c0c15e07aa8b235 38dc77b5923a4a1e9e8c68fc2b7f66fe--94ab301715c446658c0c15e07aa8b235 3c9b9b2d588448e6b65826dbba65e80a 94ab301715c446658c0c15e07aa8b235--3c9b9b2d588448e6b65826dbba65e80a 3c9b9b2d588448e6b65826dbba65e80a--f7ad41c5bff6462ba56eb35ef5e37746

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_677749f24d514923afb2e7569a3e01ac b209d0fff6aa4fe5bd24b41cdfd7bdca 0 c12e09462ecc4902a9f132005354e464 RX(0.0) b209d0fff6aa4fe5bd24b41cdfd7bdca--c12e09462ecc4902a9f132005354e464 037b8356f5da462e984d60b087c762de 1 90f7ae86a30c4ad0bc6fe3b21e7c2ebd HamEvo c12e09462ecc4902a9f132005354e464--90f7ae86a30c4ad0bc6fe3b21e7c2ebd 6567f76a537d4414a29065b888293702 RX(0.0) 90f7ae86a30c4ad0bc6fe3b21e7c2ebd--6567f76a537d4414a29065b888293702 c5c8cf0aef544a57ac6b599e265b38a2 6567f76a537d4414a29065b888293702--c5c8cf0aef544a57ac6b599e265b38a2 6223ab0aae9d4e10959c0e898ac3fee1 1968fae3dabe40149c95103c25b72bba RX(1.571) 037b8356f5da462e984d60b087c762de--1968fae3dabe40149c95103c25b72bba d3606e8d10094988b8ae7fbcba5bd978 2 9d698de149ed417d9b7ab132131aaa9a t = 1.000 1968fae3dabe40149c95103c25b72bba--9d698de149ed417d9b7ab132131aaa9a 5580b6ead2684e64be5e3eb7eb3a3814 RX(1.571) 9d698de149ed417d9b7ab132131aaa9a--5580b6ead2684e64be5e3eb7eb3a3814 5580b6ead2684e64be5e3eb7eb3a3814--6223ab0aae9d4e10959c0e898ac3fee1 69b111293d9a49cd81ea97e5669d1264 0f6bf9e6800c4eda9ec38bd1e95c427c RX(3.142) d3606e8d10094988b8ae7fbcba5bd978--0f6bf9e6800c4eda9ec38bd1e95c427c 079b590e16544b559681d6afc9a9b76c 0f6bf9e6800c4eda9ec38bd1e95c427c--079b590e16544b559681d6afc9a9b76c 2bd24a8aa1214b3a970267476b145945 RX(3.142) 079b590e16544b559681d6afc9a9b76c--2bd24a8aa1214b3a970267476b145945 2bd24a8aa1214b3a970267476b145945--69b111293d9a49cd81ea97e5669d1264

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': 269, '10': 253, '00': 246, '01': 232})]
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.