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 0d1cc22fcf1a4c6d95430ca2c667eec8 0 2dfed21c71c24d35b465a2c0af3d6e36 RX(0.5) 0d1cc22fcf1a4c6d95430ca2c667eec8--2dfed21c71c24d35b465a2c0af3d6e36 0a9dff8ba2bd4f97b7643b2558cf95e4 1 1ccfd5669c8546f0816b4274d2378ad5 2dfed21c71c24d35b465a2c0af3d6e36--1ccfd5669c8546f0816b4274d2378ad5 444c4509e3cb45a0baeaadae44417e1b 1ccfd5669c8546f0816b4274d2378ad5--444c4509e3cb45a0baeaadae44417e1b 38213e5365d54951a6b3071529338698 ee3a78d8cec8445089d1eff94a41c900 0a9dff8ba2bd4f97b7643b2558cf95e4--ee3a78d8cec8445089d1eff94a41c900 e651ca5b31e442579336a2cfc37e6622 X ee3a78d8cec8445089d1eff94a41c900--e651ca5b31e442579336a2cfc37e6622 e651ca5b31e442579336a2cfc37e6622--1ccfd5669c8546f0816b4274d2378ad5 e651ca5b31e442579336a2cfc37e6622--38213e5365d54951a6b3071529338698

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 56f557d004f54c6484b354cf41b61fcc 0 9687acf9b63a4ad4bac190799ea00831 X 56f557d004f54c6484b354cf41b61fcc--9687acf9b63a4ad4bac190799ea00831 19551a60102a418783ad69afb9c12858 1 464263df17e8426bab8790bf8a68aeb0 9687acf9b63a4ad4bac190799ea00831--464263df17e8426bab8790bf8a68aeb0 b2e85bfb119f4539b234f81c13dbc5e8 a58d223e7d4c4db2afacdaa85c5a4267 Y 19551a60102a418783ad69afb9c12858--a58d223e7d4c4db2afacdaa85c5a4267 a58d223e7d4c4db2afacdaa85c5a4267--b2e85bfb119f4539b234f81c13dbc5e8

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 ae78cd501f9440bebbdf55b844717916 0 916ebe6277664bcdb7dba58907c24825 X ae78cd501f9440bebbdf55b844717916--916ebe6277664bcdb7dba58907c24825 061fd26f7b854ff5bb34eb60cf7fe655 1 76a64464bb0a42b5bac1cf47e57b6fbb Y 916ebe6277664bcdb7dba58907c24825--76a64464bb0a42b5bac1cf47e57b6fbb 205ec7edda1d45b08232c04e441b4d28 76a64464bb0a42b5bac1cf47e57b6fbb--205ec7edda1d45b08232c04e441b4d28 487ad995eb2a48aea684438ac9015e7e 8b356d84fff740fcabcf30654f6c4612 X 061fd26f7b854ff5bb34eb60cf7fe655--8b356d84fff740fcabcf30654f6c4612 4f74f486b4d4411783cf53327b8f25e0 Y 8b356d84fff740fcabcf30654f6c4612--4f74f486b4d4411783cf53327b8f25e0 4f74f486b4d4411783cf53327b8f25e0--487ad995eb2a48aea684438ac9015e7e

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 43917c1cd0fc4a2bb6fbf698ffc424a3 0 6793ff542fe3496b83fc0c096786dec3 H 43917c1cd0fc4a2bb6fbf698ffc424a3--6793ff542fe3496b83fc0c096786dec3 e6677fdf51ed46e6b6f631862970594f 1 6a76c9f9187947b79e22cc3bc98169ab PHASE(1.571) 6793ff542fe3496b83fc0c096786dec3--6a76c9f9187947b79e22cc3bc98169ab 41cd3af88af14816a663040c2c234d7f PHASE(0.785) 6a76c9f9187947b79e22cc3bc98169ab--41cd3af88af14816a663040c2c234d7f 192d4ad195a948298f4fdb33548a8b7f 6a76c9f9187947b79e22cc3bc98169ab--192d4ad195a948298f4fdb33548a8b7f 67b53c3e667e443184fbc054e45460bb 41cd3af88af14816a663040c2c234d7f--67b53c3e667e443184fbc054e45460bb 3b09f0d5900d4e0d9c8daf719a52b8bf 41cd3af88af14816a663040c2c234d7f--3b09f0d5900d4e0d9c8daf719a52b8bf 1d38e062cb694578af473e3ab8c52804 67b53c3e667e443184fbc054e45460bb--1d38e062cb694578af473e3ab8c52804 682a740246fa488e9d5f6030c1c33e56 1d38e062cb694578af473e3ab8c52804--682a740246fa488e9d5f6030c1c33e56 8a4b73897fb64c5a8eacf85832cb141a 682a740246fa488e9d5f6030c1c33e56--8a4b73897fb64c5a8eacf85832cb141a d7ce977447634c9c8dfe5113292b7659 2fd37cbf71f54100a81c8f379cfdc2be e6677fdf51ed46e6b6f631862970594f--2fd37cbf71f54100a81c8f379cfdc2be e59ecb4971974dd5b51c2baefb4fc6c9 2 2fd37cbf71f54100a81c8f379cfdc2be--192d4ad195a948298f4fdb33548a8b7f a3d18d6c6a73473c9c9ddcec0bed4814 192d4ad195a948298f4fdb33548a8b7f--a3d18d6c6a73473c9c9ddcec0bed4814 f36b447ffb6c41f9af570ac01a8b34b5 H a3d18d6c6a73473c9c9ddcec0bed4814--f36b447ffb6c41f9af570ac01a8b34b5 0e84730d6f334722ac0b6ee588f1c51e PHASE(1.571) f36b447ffb6c41f9af570ac01a8b34b5--0e84730d6f334722ac0b6ee588f1c51e 5677e4198f2c4ff0af4e5b1f3b173956 0e84730d6f334722ac0b6ee588f1c51e--5677e4198f2c4ff0af4e5b1f3b173956 48516806d3f840788c05bff3843f0059 0e84730d6f334722ac0b6ee588f1c51e--48516806d3f840788c05bff3843f0059 5677e4198f2c4ff0af4e5b1f3b173956--d7ce977447634c9c8dfe5113292b7659 b324642081b444ce83fc870aa615b4bc 4f7fd012407b4d1a9d26d43aeda218a4 e59ecb4971974dd5b51c2baefb4fc6c9--4f7fd012407b4d1a9d26d43aeda218a4 f3f8cbf9a4fa4bf3bf56dfb7b3aebdd6 4f7fd012407b4d1a9d26d43aeda218a4--f3f8cbf9a4fa4bf3bf56dfb7b3aebdd6 f3f8cbf9a4fa4bf3bf56dfb7b3aebdd6--3b09f0d5900d4e0d9c8daf719a52b8bf 0a0b9b40e6144f30910aec6ccde9fcc8 3b09f0d5900d4e0d9c8daf719a52b8bf--0a0b9b40e6144f30910aec6ccde9fcc8 0a0b9b40e6144f30910aec6ccde9fcc8--48516806d3f840788c05bff3843f0059 8d1b1b6e6c0a4119bb14ddd60d5c7a68 H 48516806d3f840788c05bff3843f0059--8d1b1b6e6c0a4119bb14ddd60d5c7a68 8d1b1b6e6c0a4119bb14ddd60d5c7a68--b324642081b444ce83fc870aa615b4bc

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 c2d4ae1d938e40b4853196fe1eb2fcee 0 829f52d92651427ba1ba2e491c619d0a c2d4ae1d938e40b4853196fe1eb2fcee--829f52d92651427ba1ba2e491c619d0a 04f3747c946a45b38a8449ef23da8f0d 1 cb26c9d75ab24d79a48c2f49ff5ee5c9 829f52d92651427ba1ba2e491c619d0a--cb26c9d75ab24d79a48c2f49ff5ee5c9 fd39a6ba617d418dbb92356d738687e9 cb26c9d75ab24d79a48c2f49ff5ee5c9--fd39a6ba617d418dbb92356d738687e9 f0c0d7ca07144ab0945735fbf6f3e301 PHASE(-0.785) fd39a6ba617d418dbb92356d738687e9--f0c0d7ca07144ab0945735fbf6f3e301 ebf2b884ce4542dcb7ed918433b5da98 PHASE(-1.571) f0c0d7ca07144ab0945735fbf6f3e301--ebf2b884ce4542dcb7ed918433b5da98 2e753ca8985d48639c07ef2386e9386b f0c0d7ca07144ab0945735fbf6f3e301--2e753ca8985d48639c07ef2386e9386b 65d43e468e704a88aaa6eb1df47c5927 H ebf2b884ce4542dcb7ed918433b5da98--65d43e468e704a88aaa6eb1df47c5927 39e69bb6177e482d8db59574cadf2113 ebf2b884ce4542dcb7ed918433b5da98--39e69bb6177e482d8db59574cadf2113 968a3452ae2f423fb7d7249573c4f064 65d43e468e704a88aaa6eb1df47c5927--968a3452ae2f423fb7d7249573c4f064 f8d2cd77f7bb4cbbb68230e3f09998b2 4974bbab74c046b1a9096b9649a19702 04f3747c946a45b38a8449ef23da8f0d--4974bbab74c046b1a9096b9649a19702 bb1671920e8a46268fe1d0a42108d20a 2 43c0fb9f24bd450ab3970617d7f8413f PHASE(-1.571) 4974bbab74c046b1a9096b9649a19702--43c0fb9f24bd450ab3970617d7f8413f d4d5cae99eef448daf4d96fa9a7613ef H 43c0fb9f24bd450ab3970617d7f8413f--d4d5cae99eef448daf4d96fa9a7613ef 61a3cb4b947e4a28a8e3538ee3fd08bb 43c0fb9f24bd450ab3970617d7f8413f--61a3cb4b947e4a28a8e3538ee3fd08bb 95ee8dd49b8a4072a3ecefeac2e71b68 d4d5cae99eef448daf4d96fa9a7613ef--95ee8dd49b8a4072a3ecefeac2e71b68 95ee8dd49b8a4072a3ecefeac2e71b68--39e69bb6177e482d8db59574cadf2113 ebbb22778c63448a856e5ff67a111cd1 39e69bb6177e482d8db59574cadf2113--ebbb22778c63448a856e5ff67a111cd1 ebbb22778c63448a856e5ff67a111cd1--f8d2cd77f7bb4cbbb68230e3f09998b2 b8036e7b40774bef9318cda27b45cbf2 0ee4d240af564902877e64e11cb0010a H bb1671920e8a46268fe1d0a42108d20a--0ee4d240af564902877e64e11cb0010a 0ee4d240af564902877e64e11cb0010a--61a3cb4b947e4a28a8e3538ee3fd08bb ae217257d4904080a6e1f27946ecd666 61a3cb4b947e4a28a8e3538ee3fd08bb--ae217257d4904080a6e1f27946ecd666 ae217257d4904080a6e1f27946ecd666--2e753ca8985d48639c07ef2386e9386b 3d4ea78803f74558b5c3efd3ea6446a3 2e753ca8985d48639c07ef2386e9386b--3d4ea78803f74558b5c3efd3ea6446a3 29349802cde9470ebd40528d273ca68b 3d4ea78803f74558b5c3efd3ea6446a3--29349802cde9470ebd40528d273ca68b 29349802cde9470ebd40528d273ca68b--b8036e7b40774bef9318cda27b45cbf2

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_62b9c6df5edd471281a1959bd63aa949 69df7ada12df4ea8929f8ec56773386f 0 88065d6815194c6e8348f375365395d4 RX(0.0) 69df7ada12df4ea8929f8ec56773386f--88065d6815194c6e8348f375365395d4 2a9fd3e46f324c78b2c4a0e5b1731814 1 f776ae34e86b41bc9a568fa3f0c13210 HamEvo 88065d6815194c6e8348f375365395d4--f776ae34e86b41bc9a568fa3f0c13210 c3953a4cfdf84f6891ba9f1e1d5fcb50 RX(0.0) f776ae34e86b41bc9a568fa3f0c13210--c3953a4cfdf84f6891ba9f1e1d5fcb50 011245f25b63495394c153cd24b062f1 c3953a4cfdf84f6891ba9f1e1d5fcb50--011245f25b63495394c153cd24b062f1 ac75c85b52de46fc8627bf7e1eb32b63 88e7ba079476491fb8ca50f8d7f544c7 RX(1.571) 2a9fd3e46f324c78b2c4a0e5b1731814--88e7ba079476491fb8ca50f8d7f544c7 650698da06584db288a1e1f12c107dff 2 0c0cf065a89249729027c7d9bf8d90d3 t = 1.000 88e7ba079476491fb8ca50f8d7f544c7--0c0cf065a89249729027c7d9bf8d90d3 f83cdad38d8342e0ad4cbfe3f385082a RX(1.571) 0c0cf065a89249729027c7d9bf8d90d3--f83cdad38d8342e0ad4cbfe3f385082a f83cdad38d8342e0ad4cbfe3f385082a--ac75c85b52de46fc8627bf7e1eb32b63 94a1b888b09d48cc8e0fb788a8d5ba0e bd23b0c4f25e4171be432ce4fd06107f RX(3.142) 650698da06584db288a1e1f12c107dff--bd23b0c4f25e4171be432ce4fd06107f 9d3f86f95a1c4d00a54e3183074ee11f bd23b0c4f25e4171be432ce4fd06107f--9d3f86f95a1c4d00a54e3183074ee11f 24e9980938844a52ba90c525f6a8295c RX(3.142) 9d3f86f95a1c4d00a54e3183074ee11f--24e9980938844a52ba90c525f6a8295c 24e9980938844a52ba90c525f6a8295c--94a1b888b09d48cc8e0fb788a8d5ba0e

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': 259, '11': 257, '00': 245, '10': 239})]
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.

Adding noise to gates

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