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 d131b549c4b64b678fdcbe5e1beeed9c 0 ede67f4d7d8244b3879ac9d07800a120 RX(0.5) d131b549c4b64b678fdcbe5e1beeed9c--ede67f4d7d8244b3879ac9d07800a120 0a4eb131a71045c891aa2f7e2cb6a183 1 7e9d372db05b42d2acedf2b1122da1b7 ede67f4d7d8244b3879ac9d07800a120--7e9d372db05b42d2acedf2b1122da1b7 25ef92f591ce4441a802b10b4bc9b555 7e9d372db05b42d2acedf2b1122da1b7--25ef92f591ce4441a802b10b4bc9b555 3b2d5be5a8de4712a0b224d7961e91f7 ed630a82991349399db37c7123877bed 0a4eb131a71045c891aa2f7e2cb6a183--ed630a82991349399db37c7123877bed 0f4ff89e8d664bf19ef3ca62450ad21f X ed630a82991349399db37c7123877bed--0f4ff89e8d664bf19ef3ca62450ad21f 0f4ff89e8d664bf19ef3ca62450ad21f--7e9d372db05b42d2acedf2b1122da1b7 0f4ff89e8d664bf19ef3ca62450ad21f--3b2d5be5a8de4712a0b224d7961e91f7

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 00ac28518fa745918a582b5a4cf44ca7 0 6bfba367ece84a149d7a1f944c2f9afa X 00ac28518fa745918a582b5a4cf44ca7--6bfba367ece84a149d7a1f944c2f9afa 5caba5189da445449e9bb6660304a509 1 746bb757240d415a92340fc8937b3303 6bfba367ece84a149d7a1f944c2f9afa--746bb757240d415a92340fc8937b3303 60349623bd1b45c0af665a12aeb4ea18 0e17803069cf4e0fb4acf7709c63acc4 Y 5caba5189da445449e9bb6660304a509--0e17803069cf4e0fb4acf7709c63acc4 0e17803069cf4e0fb4acf7709c63acc4--60349623bd1b45c0af665a12aeb4ea18

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 9ac14c20a5b14ea3b16d570041940a14 0 f10d7e9b70d14c80aa71bff881232f1a X 9ac14c20a5b14ea3b16d570041940a14--f10d7e9b70d14c80aa71bff881232f1a b68719ee99d64314be4f88f0f037608b 1 dbc04a38d0564c2fbd54b8606b5bc0c9 Y f10d7e9b70d14c80aa71bff881232f1a--dbc04a38d0564c2fbd54b8606b5bc0c9 6020af4adac24d78afaebd13251530db dbc04a38d0564c2fbd54b8606b5bc0c9--6020af4adac24d78afaebd13251530db e0f2f6ec7e614caeb5d06e21343e3222 e40237ebe9324d9aaa5d78457a5ad13d X b68719ee99d64314be4f88f0f037608b--e40237ebe9324d9aaa5d78457a5ad13d 17c0fd048a2c4b24bdc1f494d0b88cd1 Y e40237ebe9324d9aaa5d78457a5ad13d--17c0fd048a2c4b24bdc1f494d0b88cd1 17c0fd048a2c4b24bdc1f494d0b88cd1--e0f2f6ec7e614caeb5d06e21343e3222

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 522f64f8d7a54612bfb40bf1f9349b6e 0 c0a5816bfaf04bc8a58a25e548adc680 H 522f64f8d7a54612bfb40bf1f9349b6e--c0a5816bfaf04bc8a58a25e548adc680 fb7282d8ce4340f08cd0996e8e629aca 1 640c0ec4d85a4faaa84dc1b1b1541879 PHASE(1.571) c0a5816bfaf04bc8a58a25e548adc680--640c0ec4d85a4faaa84dc1b1b1541879 f81e2dd3e0f14027b7edfedea51bddef PHASE(0.785) 640c0ec4d85a4faaa84dc1b1b1541879--f81e2dd3e0f14027b7edfedea51bddef 4f0aed86beff4ec5810db3649cca7444 640c0ec4d85a4faaa84dc1b1b1541879--4f0aed86beff4ec5810db3649cca7444 e5a315ee982644c195f6c94df2e41662 f81e2dd3e0f14027b7edfedea51bddef--e5a315ee982644c195f6c94df2e41662 eb774ea86db24ceeb732dc99bcf15cec f81e2dd3e0f14027b7edfedea51bddef--eb774ea86db24ceeb732dc99bcf15cec 825357ff1ae04b019d3c0c38598ceeec e5a315ee982644c195f6c94df2e41662--825357ff1ae04b019d3c0c38598ceeec 9bcd9e2bcb2e4176bbe7083b1b75c720 825357ff1ae04b019d3c0c38598ceeec--9bcd9e2bcb2e4176bbe7083b1b75c720 5ceda470ed764c8dae714b424ad9a68b 9bcd9e2bcb2e4176bbe7083b1b75c720--5ceda470ed764c8dae714b424ad9a68b e21351764de24095a23dbe39fc92a50b 1532dbedb5e9469f9ee697b03767fcda fb7282d8ce4340f08cd0996e8e629aca--1532dbedb5e9469f9ee697b03767fcda 572603cc468c4a079f6de413c1ade30a 2 1532dbedb5e9469f9ee697b03767fcda--4f0aed86beff4ec5810db3649cca7444 d3f7a3b20ea344a784516651ad6d4cd6 4f0aed86beff4ec5810db3649cca7444--d3f7a3b20ea344a784516651ad6d4cd6 848cd89967f8405881494fd66e899f04 H d3f7a3b20ea344a784516651ad6d4cd6--848cd89967f8405881494fd66e899f04 37c8541768264522b7f7f14716a004f6 PHASE(1.571) 848cd89967f8405881494fd66e899f04--37c8541768264522b7f7f14716a004f6 9d1db5dab7ec4587a659b19462b271b3 37c8541768264522b7f7f14716a004f6--9d1db5dab7ec4587a659b19462b271b3 cf7ededb220a4e7ca0ded450e8392a06 37c8541768264522b7f7f14716a004f6--cf7ededb220a4e7ca0ded450e8392a06 9d1db5dab7ec4587a659b19462b271b3--e21351764de24095a23dbe39fc92a50b 4c9b4cc370004b6b9f94a7a3b8234a4e d0d1301094564cbda8f92a728df5b30f 572603cc468c4a079f6de413c1ade30a--d0d1301094564cbda8f92a728df5b30f a1824d1085154d8e842a90426740ed41 d0d1301094564cbda8f92a728df5b30f--a1824d1085154d8e842a90426740ed41 a1824d1085154d8e842a90426740ed41--eb774ea86db24ceeb732dc99bcf15cec 8baf093e8e7c4ce688d2ecab66796cb7 eb774ea86db24ceeb732dc99bcf15cec--8baf093e8e7c4ce688d2ecab66796cb7 8baf093e8e7c4ce688d2ecab66796cb7--cf7ededb220a4e7ca0ded450e8392a06 f3aed21133474f4ba1ad5351061bf820 H cf7ededb220a4e7ca0ded450e8392a06--f3aed21133474f4ba1ad5351061bf820 f3aed21133474f4ba1ad5351061bf820--4c9b4cc370004b6b9f94a7a3b8234a4e

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 2dae613d7d444585bc80f7a722047552 0 eba4c2a1a06a406cb6cb1ad5eca8609f 2dae613d7d444585bc80f7a722047552--eba4c2a1a06a406cb6cb1ad5eca8609f a3d7bf3730be4d589439f3196126ee85 1 0531d5b14693450b84e3a9b7156c4c7d eba4c2a1a06a406cb6cb1ad5eca8609f--0531d5b14693450b84e3a9b7156c4c7d 30cd4c07ace44280917915524771276e 0531d5b14693450b84e3a9b7156c4c7d--30cd4c07ace44280917915524771276e 4ba686d5146f46c5b377847a59dda9f0 PHASE(-0.785) 30cd4c07ace44280917915524771276e--4ba686d5146f46c5b377847a59dda9f0 c9f44737ed2f46b096d5d1c97cd77264 PHASE(-1.571) 4ba686d5146f46c5b377847a59dda9f0--c9f44737ed2f46b096d5d1c97cd77264 ce6d55195ae0483ca3e3b8834ddc0e39 4ba686d5146f46c5b377847a59dda9f0--ce6d55195ae0483ca3e3b8834ddc0e39 b8ba87d6aa17447c8cf2966b0217af2b H c9f44737ed2f46b096d5d1c97cd77264--b8ba87d6aa17447c8cf2966b0217af2b 78a7900f3b3d4d87b5ca15fba3581b5f c9f44737ed2f46b096d5d1c97cd77264--78a7900f3b3d4d87b5ca15fba3581b5f 1114676e362e4a639ae1c2b112678155 b8ba87d6aa17447c8cf2966b0217af2b--1114676e362e4a639ae1c2b112678155 0a2514a9057a4503bcbd4f10f93d0126 9ff1f7f014774888b1a3bebd48321160 a3d7bf3730be4d589439f3196126ee85--9ff1f7f014774888b1a3bebd48321160 ee3d01a263464d18aef7967df8180c92 2 50d86c7a941a4368bdeb6ca3e1ed5186 PHASE(-1.571) 9ff1f7f014774888b1a3bebd48321160--50d86c7a941a4368bdeb6ca3e1ed5186 c468fedbd3fb4700b153a43b77cce925 H 50d86c7a941a4368bdeb6ca3e1ed5186--c468fedbd3fb4700b153a43b77cce925 a77288579f9c49728863ed98d1fd2b0c 50d86c7a941a4368bdeb6ca3e1ed5186--a77288579f9c49728863ed98d1fd2b0c fb1fcaf86d6643fc9ad4aaefdefc9231 c468fedbd3fb4700b153a43b77cce925--fb1fcaf86d6643fc9ad4aaefdefc9231 fb1fcaf86d6643fc9ad4aaefdefc9231--78a7900f3b3d4d87b5ca15fba3581b5f 68454db4bed64d6fa22db5d792cd5226 78a7900f3b3d4d87b5ca15fba3581b5f--68454db4bed64d6fa22db5d792cd5226 68454db4bed64d6fa22db5d792cd5226--0a2514a9057a4503bcbd4f10f93d0126 92c9a25d8c344921b90c8448e4c2d681 4e9d93b0aa63463c8afeac805c740722 H ee3d01a263464d18aef7967df8180c92--4e9d93b0aa63463c8afeac805c740722 4e9d93b0aa63463c8afeac805c740722--a77288579f9c49728863ed98d1fd2b0c e9580e32b2224172a71fc6751a345883 a77288579f9c49728863ed98d1fd2b0c--e9580e32b2224172a71fc6751a345883 e9580e32b2224172a71fc6751a345883--ce6d55195ae0483ca3e3b8834ddc0e39 8570baf9b3344a39bfd9fe623e8bfbf6 ce6d55195ae0483ca3e3b8834ddc0e39--8570baf9b3344a39bfd9fe623e8bfbf6 7e20661cd92b4c16a2d3c81923f3a4e0 8570baf9b3344a39bfd9fe623e8bfbf6--7e20661cd92b4c16a2d3c81923f3a4e0 7e20661cd92b4c16a2d3c81923f3a4e0--92c9a25d8c344921b90c8448e4c2d681

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_30606dc8aeb04562ba573659f8e3fad7 26c9e83a76e9499c84f2af50522258a0 0 d37ec369897d4bfb91301545cdc701cf RX(0.0) 26c9e83a76e9499c84f2af50522258a0--d37ec369897d4bfb91301545cdc701cf 5294e9b898984855b2bae1675598a319 1 250bdff3305a4cbf91880784f776b889 HamEvo d37ec369897d4bfb91301545cdc701cf--250bdff3305a4cbf91880784f776b889 0be4c861caad45558492d4c23baf38bb RX(0.0) 250bdff3305a4cbf91880784f776b889--0be4c861caad45558492d4c23baf38bb 415d6759e8c841e8800205d67305f7cd 0be4c861caad45558492d4c23baf38bb--415d6759e8c841e8800205d67305f7cd ebb79ca5c9474610baf3e4a5911a6722 0901123cb54a4bc2b6f956c9eb56b8db RX(1.571) 5294e9b898984855b2bae1675598a319--0901123cb54a4bc2b6f956c9eb56b8db f2d117e19a3e4609b858c01a53516a53 2 105555aa551344a38240e11ffd10961b t = 1.000 0901123cb54a4bc2b6f956c9eb56b8db--105555aa551344a38240e11ffd10961b acc82a1f6d434fa3965ba6576dffb994 RX(1.571) 105555aa551344a38240e11ffd10961b--acc82a1f6d434fa3965ba6576dffb994 acc82a1f6d434fa3965ba6576dffb994--ebb79ca5c9474610baf3e4a5911a6722 0d76d17167fc4df7a578eaf8d7cd5077 70601e401e3349fbac65b26dd5a0b0a4 RX(3.142) f2d117e19a3e4609b858c01a53516a53--70601e401e3349fbac65b26dd5a0b0a4 8167db3c7c764cc298138b287d148448 70601e401e3349fbac65b26dd5a0b0a4--8167db3c7c764cc298138b287d148448 6d63aad35e2047699af85911052089b9 RX(3.142) 8167db3c7c764cc298138b287d148448--6d63aad35e2047699af85911052089b9 6d63aad35e2047699af85911052089b9--0d76d17167fc4df7a578eaf8d7cd5077

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({'10': 267, '00': 256, '11': 255, '01': 222})]
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.