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 ab2b24005cf04e279041e15558b8185b 0 9bae4de8ab904443a534a57208349b83 RX(0.5) ab2b24005cf04e279041e15558b8185b--9bae4de8ab904443a534a57208349b83 276506dc5c5646d7859620c90daee0ac 1 c111a1297ecf4c6bbe67973a894360d0 9bae4de8ab904443a534a57208349b83--c111a1297ecf4c6bbe67973a894360d0 a0144b46181c4fb99b625b04f9e145d3 c111a1297ecf4c6bbe67973a894360d0--a0144b46181c4fb99b625b04f9e145d3 cdbee9cc8c704399bea2711649a9dd84 aa6cefdd015e454dab56ccd330893494 276506dc5c5646d7859620c90daee0ac--aa6cefdd015e454dab56ccd330893494 9178a58ab23646e0b3b795c2c8587924 X aa6cefdd015e454dab56ccd330893494--9178a58ab23646e0b3b795c2c8587924 9178a58ab23646e0b3b795c2c8587924--c111a1297ecf4c6bbe67973a894360d0 9178a58ab23646e0b3b795c2c8587924--cdbee9cc8c704399bea2711649a9dd84

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 ac511630159e4ec0b415e7eab9fb0574 0 0f599c4fc2134ea28564147f513e7b0a X ac511630159e4ec0b415e7eab9fb0574--0f599c4fc2134ea28564147f513e7b0a 203266dfe8f5430faab482ee3ce11d30 1 7c591ff8fc5649d5964d6e4949ade419 0f599c4fc2134ea28564147f513e7b0a--7c591ff8fc5649d5964d6e4949ade419 b19b3be966204f82a5621cb8f5677eed 72636fced37a4b6c8550d32530285e8c Y 203266dfe8f5430faab482ee3ce11d30--72636fced37a4b6c8550d32530285e8c 72636fced37a4b6c8550d32530285e8c--b19b3be966204f82a5621cb8f5677eed

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 844a295467fc4151a36ce880da5bfda7 0 61937154d4c840fcb231b14cd1a5f1fa X 844a295467fc4151a36ce880da5bfda7--61937154d4c840fcb231b14cd1a5f1fa da357a1c94784334979beeadf61c1868 1 e93d42bdfd3e4e6486dfff8312b9fb54 Y 61937154d4c840fcb231b14cd1a5f1fa--e93d42bdfd3e4e6486dfff8312b9fb54 d23d1180292a46f99d72bbd5073adb04 e93d42bdfd3e4e6486dfff8312b9fb54--d23d1180292a46f99d72bbd5073adb04 6b721210641640398e8d90e8ab57ee9d e6f83fc3a43542df9cd729d0c682df3e X da357a1c94784334979beeadf61c1868--e6f83fc3a43542df9cd729d0c682df3e 1e15d91e65274b6bb9d15d3c2c49598e Y e6f83fc3a43542df9cd729d0c682df3e--1e15d91e65274b6bb9d15d3c2c49598e 1e15d91e65274b6bb9d15d3c2c49598e--6b721210641640398e8d90e8ab57ee9d

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 d15aa2c4fe6846cba0fb736ea10dfb8a 0 a2c5ed199f8c4a2e998cef510841c824 H d15aa2c4fe6846cba0fb736ea10dfb8a--a2c5ed199f8c4a2e998cef510841c824 d80ad134adc64e7b93e11fd3738c894d 1 ad5c9cd677504f24b76e2ac286b9760e PHASE(1.571) a2c5ed199f8c4a2e998cef510841c824--ad5c9cd677504f24b76e2ac286b9760e fa486002ea9f4df4ab880ce168d62db3 PHASE(0.785) ad5c9cd677504f24b76e2ac286b9760e--fa486002ea9f4df4ab880ce168d62db3 b9a8234e26b94d3e9017c60cbe7366f2 ad5c9cd677504f24b76e2ac286b9760e--b9a8234e26b94d3e9017c60cbe7366f2 0a22c72ea4324f16bd245f72d313ac3b fa486002ea9f4df4ab880ce168d62db3--0a22c72ea4324f16bd245f72d313ac3b 52822e032b19407695295e349adc65fe fa486002ea9f4df4ab880ce168d62db3--52822e032b19407695295e349adc65fe 191a781a9bee4ca09607dcb5db98a4ea 0a22c72ea4324f16bd245f72d313ac3b--191a781a9bee4ca09607dcb5db98a4ea 98d31423d90d46a3bc9f0c58f2c7c399 191a781a9bee4ca09607dcb5db98a4ea--98d31423d90d46a3bc9f0c58f2c7c399 a3812bc41622404b9981a3b03416a358 98d31423d90d46a3bc9f0c58f2c7c399--a3812bc41622404b9981a3b03416a358 9cd581d52d8e4d59bc77608f50e681e1 f83f5f0f5c9e47ee92b31a788b28ca80 d80ad134adc64e7b93e11fd3738c894d--f83f5f0f5c9e47ee92b31a788b28ca80 0679ca0d7a5c409cb17363f43d22545d 2 f83f5f0f5c9e47ee92b31a788b28ca80--b9a8234e26b94d3e9017c60cbe7366f2 1d81949375ec403da4d1c454f5cfff92 b9a8234e26b94d3e9017c60cbe7366f2--1d81949375ec403da4d1c454f5cfff92 a5a36290e5764d319592345f0563d63f H 1d81949375ec403da4d1c454f5cfff92--a5a36290e5764d319592345f0563d63f a327c002a59d4b7b82e100621a230aae PHASE(1.571) a5a36290e5764d319592345f0563d63f--a327c002a59d4b7b82e100621a230aae 913c2550bbca4e5c8aa7045251740cf1 a327c002a59d4b7b82e100621a230aae--913c2550bbca4e5c8aa7045251740cf1 beb44a1ddb2b4479a38b84c30ce4beb6 a327c002a59d4b7b82e100621a230aae--beb44a1ddb2b4479a38b84c30ce4beb6 913c2550bbca4e5c8aa7045251740cf1--9cd581d52d8e4d59bc77608f50e681e1 f41ed33fe5b94e8aa9fcfda9666fd0ca bdfea7586f4c4480944882dee3793c9d 0679ca0d7a5c409cb17363f43d22545d--bdfea7586f4c4480944882dee3793c9d ff616c8ef1194bf19e71912b69c4c9bd bdfea7586f4c4480944882dee3793c9d--ff616c8ef1194bf19e71912b69c4c9bd ff616c8ef1194bf19e71912b69c4c9bd--52822e032b19407695295e349adc65fe 8aa98cfa7dbb4783917cef1413333873 52822e032b19407695295e349adc65fe--8aa98cfa7dbb4783917cef1413333873 8aa98cfa7dbb4783917cef1413333873--beb44a1ddb2b4479a38b84c30ce4beb6 fb52004f475c4bbda7d82bb8a5159775 H beb44a1ddb2b4479a38b84c30ce4beb6--fb52004f475c4bbda7d82bb8a5159775 fb52004f475c4bbda7d82bb8a5159775--f41ed33fe5b94e8aa9fcfda9666fd0ca

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 b69695d8998a4284ba53fa0d082e4305 0 dd92c2111a034e2895c87d28106e1754 b69695d8998a4284ba53fa0d082e4305--dd92c2111a034e2895c87d28106e1754 1dc819bbb7894dbaa4b495fe0b446eb4 1 07cd4a16360b490ab0d96063797d70c0 dd92c2111a034e2895c87d28106e1754--07cd4a16360b490ab0d96063797d70c0 1e3b052aba944d62abd59c4392240d95 07cd4a16360b490ab0d96063797d70c0--1e3b052aba944d62abd59c4392240d95 81dcd2000c2c4f858a56d09ee6596ad7 PHASE(-0.785) 1e3b052aba944d62abd59c4392240d95--81dcd2000c2c4f858a56d09ee6596ad7 9674ce0371d64b7cb5408924cffc4ec1 PHASE(-1.571) 81dcd2000c2c4f858a56d09ee6596ad7--9674ce0371d64b7cb5408924cffc4ec1 db5e1b06e5ec4aae9851ba5428280eac 81dcd2000c2c4f858a56d09ee6596ad7--db5e1b06e5ec4aae9851ba5428280eac f88460327bfc4421b0b09d0e2eb359c7 H 9674ce0371d64b7cb5408924cffc4ec1--f88460327bfc4421b0b09d0e2eb359c7 7537dac26fc6423cb7631805879cbcb6 9674ce0371d64b7cb5408924cffc4ec1--7537dac26fc6423cb7631805879cbcb6 3d9d3c8ab8254554b14a29c94d93baba f88460327bfc4421b0b09d0e2eb359c7--3d9d3c8ab8254554b14a29c94d93baba b849c8b78e674d1f82fa319042d69f57 a599ccf7d8284f1e8a16b68a52ede580 1dc819bbb7894dbaa4b495fe0b446eb4--a599ccf7d8284f1e8a16b68a52ede580 6c659606199548b2b0ad04882926a0f3 2 f264181712f14abb81cf595c040120ab PHASE(-1.571) a599ccf7d8284f1e8a16b68a52ede580--f264181712f14abb81cf595c040120ab 4c22a37c6fc44141ad5f1e9da25c032a H f264181712f14abb81cf595c040120ab--4c22a37c6fc44141ad5f1e9da25c032a ebde37efc7b140ecb057a78b54fd9903 f264181712f14abb81cf595c040120ab--ebde37efc7b140ecb057a78b54fd9903 43e23308103046228b184fbcac72c47a 4c22a37c6fc44141ad5f1e9da25c032a--43e23308103046228b184fbcac72c47a 43e23308103046228b184fbcac72c47a--7537dac26fc6423cb7631805879cbcb6 36b0462767e346469a1e0a7693764320 7537dac26fc6423cb7631805879cbcb6--36b0462767e346469a1e0a7693764320 36b0462767e346469a1e0a7693764320--b849c8b78e674d1f82fa319042d69f57 2953d51a66014a248154d514d591dece 1cf001e16d3b4ec6a0aaaac658f048c0 H 6c659606199548b2b0ad04882926a0f3--1cf001e16d3b4ec6a0aaaac658f048c0 1cf001e16d3b4ec6a0aaaac658f048c0--ebde37efc7b140ecb057a78b54fd9903 0f287bf9e37f49b68d65a6596379b7dc ebde37efc7b140ecb057a78b54fd9903--0f287bf9e37f49b68d65a6596379b7dc 0f287bf9e37f49b68d65a6596379b7dc--db5e1b06e5ec4aae9851ba5428280eac 10b6a1e5b96b42a383bfc770933303bc db5e1b06e5ec4aae9851ba5428280eac--10b6a1e5b96b42a383bfc770933303bc 6b7333aaabe245bc83c08ef836ddb58b 10b6a1e5b96b42a383bfc770933303bc--6b7333aaabe245bc83c08ef836ddb58b 6b7333aaabe245bc83c08ef836ddb58b--2953d51a66014a248154d514d591dece

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_a36093c44f52464a8dbb11af7f9ab5a8 70840e40dc3742628ba7333abda5529c 0 c1cf2ee907ea4b91a7cdf99cf9128289 RX(0.0) 70840e40dc3742628ba7333abda5529c--c1cf2ee907ea4b91a7cdf99cf9128289 747bae48cef34294a4c7c15ea7567b79 1 c14f91c27fa84e50a1f691b7ff84bc4e HamEvo c1cf2ee907ea4b91a7cdf99cf9128289--c14f91c27fa84e50a1f691b7ff84bc4e d3dedc3fa5034e9dbfa8bed16f2d36e5 RX(0.0) c14f91c27fa84e50a1f691b7ff84bc4e--d3dedc3fa5034e9dbfa8bed16f2d36e5 e6230d867ee44a5191eb69d8d9566337 d3dedc3fa5034e9dbfa8bed16f2d36e5--e6230d867ee44a5191eb69d8d9566337 c34084f8352b4b18a183279afdb967d4 7a2cd6a23b3e4539aff4de8ba752a301 RX(1.571) 747bae48cef34294a4c7c15ea7567b79--7a2cd6a23b3e4539aff4de8ba752a301 f49fa5c6da9e4befab90bd55c1365fbf 2 eb46ab58c3dd40178ecfeacf306a3059 t = 1.000 7a2cd6a23b3e4539aff4de8ba752a301--eb46ab58c3dd40178ecfeacf306a3059 9bc7a85d766f41b6a1eaf64731459cb4 RX(1.571) eb46ab58c3dd40178ecfeacf306a3059--9bc7a85d766f41b6a1eaf64731459cb4 9bc7a85d766f41b6a1eaf64731459cb4--c34084f8352b4b18a183279afdb967d4 3b66ba008cec46f18e3e338f0fd74c59 9d568f719a494a2c89af4a49c772521b RX(3.142) f49fa5c6da9e4befab90bd55c1365fbf--9d568f719a494a2c89af4a49c772521b 53373f008eef4060b1e912973971b05a 9d568f719a494a2c89af4a49c772521b--53373f008eef4060b1e912973971b05a c03ca6b491e1434b9dd88bac02c8dbde RX(3.142) 53373f008eef4060b1e912973971b05a--c03ca6b491e1434b9dd88bac02c8dbde c03ca6b491e1434b9dd88bac02c8dbde--3b66ba008cec46f18e3e338f0fd74c59

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({'10': 253, '11': 251, '01': 250, '00': 246})]
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.