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 f4200e97429c48bc82c0fe7896556598 0 16b5daa4d0f54f13acb711fdd4e1f69d RX(0.5) f4200e97429c48bc82c0fe7896556598--16b5daa4d0f54f13acb711fdd4e1f69d 59b6983dc2324d1682873215e2cac39c 1 9acedb75e50f438792ad55a4f7896e2a 16b5daa4d0f54f13acb711fdd4e1f69d--9acedb75e50f438792ad55a4f7896e2a 6caec729b67241da8a9f995ce34ba50c 9acedb75e50f438792ad55a4f7896e2a--6caec729b67241da8a9f995ce34ba50c cdc160bd294e4276ae30c7ebf57b5b56 8bcf9d845587406fb1d10b1108e10bc2 59b6983dc2324d1682873215e2cac39c--8bcf9d845587406fb1d10b1108e10bc2 e7468e5d7eca45c3ae78650383a25fa6 X 8bcf9d845587406fb1d10b1108e10bc2--e7468e5d7eca45c3ae78650383a25fa6 e7468e5d7eca45c3ae78650383a25fa6--9acedb75e50f438792ad55a4f7896e2a e7468e5d7eca45c3ae78650383a25fa6--cdc160bd294e4276ae30c7ebf57b5b56

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 af355b7421674eba8dd1ebc0d0c7342e 0 e5a348c1895e457bb7eaae61d554c5c1 X af355b7421674eba8dd1ebc0d0c7342e--e5a348c1895e457bb7eaae61d554c5c1 551dd656a68c4603bb03227d3c4b2b91 1 8f3b65c957d745a6a85ad5089487f288 e5a348c1895e457bb7eaae61d554c5c1--8f3b65c957d745a6a85ad5089487f288 ba8d1a5a3a804276928eed55bc9e00c6 e8f7e5c303dd41a8b2a8616133e8efd6 Y 551dd656a68c4603bb03227d3c4b2b91--e8f7e5c303dd41a8b2a8616133e8efd6 e8f7e5c303dd41a8b2a8616133e8efd6--ba8d1a5a3a804276928eed55bc9e00c6

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 b7e55bcefa5847ffb8b206a6922971bd 0 ca6f2a23e04b4db98fbc594e0283f75e X b7e55bcefa5847ffb8b206a6922971bd--ca6f2a23e04b4db98fbc594e0283f75e bdf44919e8fd429e8a61f2f66d16a552 1 b5ee21d3de8341388beeef79608ad347 Y ca6f2a23e04b4db98fbc594e0283f75e--b5ee21d3de8341388beeef79608ad347 63bb92e17aad42ef85acbe19020c74e9 b5ee21d3de8341388beeef79608ad347--63bb92e17aad42ef85acbe19020c74e9 001117446c444f5287e349c1ea75dc28 365491e6a65b4720b3792214fc439246 X bdf44919e8fd429e8a61f2f66d16a552--365491e6a65b4720b3792214fc439246 cec1117da1c54c9ebbe63e50a0bc63fb Y 365491e6a65b4720b3792214fc439246--cec1117da1c54c9ebbe63e50a0bc63fb cec1117da1c54c9ebbe63e50a0bc63fb--001117446c444f5287e349c1ea75dc28

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 2cdf5e2dce3b400e81e61ddc899027cb 0 15034f690e2149afb95bf5d415608804 H 2cdf5e2dce3b400e81e61ddc899027cb--15034f690e2149afb95bf5d415608804 d6e56e36977f44cdbf22c1482890b477 1 a08ed97defdd4496b0493e35eb2d70b0 PHASE(1.571) 15034f690e2149afb95bf5d415608804--a08ed97defdd4496b0493e35eb2d70b0 dcdfe921e2774f3ab08e03ccbde05800 PHASE(0.785) a08ed97defdd4496b0493e35eb2d70b0--dcdfe921e2774f3ab08e03ccbde05800 5cfbaf7fa61f459ba6234622b2a0ba44 a08ed97defdd4496b0493e35eb2d70b0--5cfbaf7fa61f459ba6234622b2a0ba44 71d85d4798b6431da8da2463cd168d7b dcdfe921e2774f3ab08e03ccbde05800--71d85d4798b6431da8da2463cd168d7b 39187da2f9eb4ba5945f2f8aa6d45740 dcdfe921e2774f3ab08e03ccbde05800--39187da2f9eb4ba5945f2f8aa6d45740 aedf5d0a71984603aae8b48c5a279a8d 71d85d4798b6431da8da2463cd168d7b--aedf5d0a71984603aae8b48c5a279a8d ce90892734104c35a6b39aff5019f0ea aedf5d0a71984603aae8b48c5a279a8d--ce90892734104c35a6b39aff5019f0ea baaaf9b5d9654b7a82d6d382a45c293c ce90892734104c35a6b39aff5019f0ea--baaaf9b5d9654b7a82d6d382a45c293c 0a09c2b6d5144328bb5640b85370f384 25ef6f983add4d7da842128dbd8fa2f4 d6e56e36977f44cdbf22c1482890b477--25ef6f983add4d7da842128dbd8fa2f4 e086441580244d9b800e88ddbc05807f 2 25ef6f983add4d7da842128dbd8fa2f4--5cfbaf7fa61f459ba6234622b2a0ba44 5c3c2f30c039427ebd4745a9f901fab5 5cfbaf7fa61f459ba6234622b2a0ba44--5c3c2f30c039427ebd4745a9f901fab5 c1a9d146a19f42f183c4e32814cb4e9d H 5c3c2f30c039427ebd4745a9f901fab5--c1a9d146a19f42f183c4e32814cb4e9d 79d004e7fd974bc8820d1f9e3a58cf0e PHASE(1.571) c1a9d146a19f42f183c4e32814cb4e9d--79d004e7fd974bc8820d1f9e3a58cf0e bcef05a4083848ac889eb40832248577 79d004e7fd974bc8820d1f9e3a58cf0e--bcef05a4083848ac889eb40832248577 1f2491fe6b92400688984cc77827f9f8 79d004e7fd974bc8820d1f9e3a58cf0e--1f2491fe6b92400688984cc77827f9f8 bcef05a4083848ac889eb40832248577--0a09c2b6d5144328bb5640b85370f384 9515a3412e7c41fea6f9853101be390b 8956f602c24c483c8aaa2e8a625d6649 e086441580244d9b800e88ddbc05807f--8956f602c24c483c8aaa2e8a625d6649 39ce98580bae4c00bdca28b5e1195a0b 8956f602c24c483c8aaa2e8a625d6649--39ce98580bae4c00bdca28b5e1195a0b 39ce98580bae4c00bdca28b5e1195a0b--39187da2f9eb4ba5945f2f8aa6d45740 45b751d4b5b74f87bd369e83932cd1fa 39187da2f9eb4ba5945f2f8aa6d45740--45b751d4b5b74f87bd369e83932cd1fa 45b751d4b5b74f87bd369e83932cd1fa--1f2491fe6b92400688984cc77827f9f8 7c11c7a83a124b28bc309364e701b6d9 H 1f2491fe6b92400688984cc77827f9f8--7c11c7a83a124b28bc309364e701b6d9 7c11c7a83a124b28bc309364e701b6d9--9515a3412e7c41fea6f9853101be390b

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 1682155a510c4035b0928988e9aa6f4f 0 25d1daf476ee4289b7cccf587dbb4798 1682155a510c4035b0928988e9aa6f4f--25d1daf476ee4289b7cccf587dbb4798 a3a860e7b81a428e847e61a80668f0ce 1 6552ccd494044a17a3a3d23af567a733 25d1daf476ee4289b7cccf587dbb4798--6552ccd494044a17a3a3d23af567a733 4e66efe1a8d540d390a4317b3dae0e18 6552ccd494044a17a3a3d23af567a733--4e66efe1a8d540d390a4317b3dae0e18 f869b7132fff4b87857d6fad05fb74c2 PHASE(-0.785) 4e66efe1a8d540d390a4317b3dae0e18--f869b7132fff4b87857d6fad05fb74c2 ba64a8442ca64486a8cafed5eb5cef92 PHASE(-1.571) f869b7132fff4b87857d6fad05fb74c2--ba64a8442ca64486a8cafed5eb5cef92 9571a4142ae8422cb5077693213707e3 f869b7132fff4b87857d6fad05fb74c2--9571a4142ae8422cb5077693213707e3 43eee2e11afe411f8fda67fbfaa44cdf H ba64a8442ca64486a8cafed5eb5cef92--43eee2e11afe411f8fda67fbfaa44cdf f3aab38ed661447bba5c7c037a7a7b7b ba64a8442ca64486a8cafed5eb5cef92--f3aab38ed661447bba5c7c037a7a7b7b 96f36dfd92184f89ad6b09c60eac5012 43eee2e11afe411f8fda67fbfaa44cdf--96f36dfd92184f89ad6b09c60eac5012 804b2edaec0f4ed3857d2389e9001dc6 a461a6da4c214c49809ff260539297c1 a3a860e7b81a428e847e61a80668f0ce--a461a6da4c214c49809ff260539297c1 0056f759fce643a39a5bd56ec8efe375 2 5bb417a06ea94926bbdf7cac9d0e4c5d PHASE(-1.571) a461a6da4c214c49809ff260539297c1--5bb417a06ea94926bbdf7cac9d0e4c5d 9cc1c9237d964481a2c6f555f6c46e6c H 5bb417a06ea94926bbdf7cac9d0e4c5d--9cc1c9237d964481a2c6f555f6c46e6c 8eb89145dd8e4d2c99dc896ee5715fc5 5bb417a06ea94926bbdf7cac9d0e4c5d--8eb89145dd8e4d2c99dc896ee5715fc5 2094f6c1d7914b4199c439da92b641fc 9cc1c9237d964481a2c6f555f6c46e6c--2094f6c1d7914b4199c439da92b641fc 2094f6c1d7914b4199c439da92b641fc--f3aab38ed661447bba5c7c037a7a7b7b d59e7428752b4e84a0460863f2637754 f3aab38ed661447bba5c7c037a7a7b7b--d59e7428752b4e84a0460863f2637754 d59e7428752b4e84a0460863f2637754--804b2edaec0f4ed3857d2389e9001dc6 5d0ec0a91c7d42dc99ad753ffb3a30f9 51907076602e46e98838bf7a36ae0a2c H 0056f759fce643a39a5bd56ec8efe375--51907076602e46e98838bf7a36ae0a2c 51907076602e46e98838bf7a36ae0a2c--8eb89145dd8e4d2c99dc896ee5715fc5 abcdaf5d3d7844869c75c0f13950287c 8eb89145dd8e4d2c99dc896ee5715fc5--abcdaf5d3d7844869c75c0f13950287c abcdaf5d3d7844869c75c0f13950287c--9571a4142ae8422cb5077693213707e3 2097a4187de64b45953b43667c5bc347 9571a4142ae8422cb5077693213707e3--2097a4187de64b45953b43667c5bc347 3202dedaec8a4111bf90fbc4a0f2b172 2097a4187de64b45953b43667c5bc347--3202dedaec8a4111bf90fbc4a0f2b172 3202dedaec8a4111bf90fbc4a0f2b172--5d0ec0a91c7d42dc99ad753ffb3a30f9

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_a2239b3da7274a209a7bbff055f7cd62 5cf1a3e664d543f8b10ff519697915f9 0 58eaf86f3cd8468d81e806741d8433e4 RX(0.0) 5cf1a3e664d543f8b10ff519697915f9--58eaf86f3cd8468d81e806741d8433e4 96af6ffcc7194d408e161ae28e0f0d3c 1 3ceb2840bfd247ea83e84a77b3e9fe72 HamEvo 58eaf86f3cd8468d81e806741d8433e4--3ceb2840bfd247ea83e84a77b3e9fe72 74cefb93d2564548894a9b64f18f510b RX(0.0) 3ceb2840bfd247ea83e84a77b3e9fe72--74cefb93d2564548894a9b64f18f510b 79153110bb49499799ae40149122dd8f 74cefb93d2564548894a9b64f18f510b--79153110bb49499799ae40149122dd8f 0a184eb47cec446a9f392baaed159770 8d6c23095d7b49a79e6f79045367a3ea RX(1.571) 96af6ffcc7194d408e161ae28e0f0d3c--8d6c23095d7b49a79e6f79045367a3ea ccc98c3b7d384a44a3be39ca539ee496 2 5cf26fbc41614381a47fbc8bf480c495 t = 1.000 8d6c23095d7b49a79e6f79045367a3ea--5cf26fbc41614381a47fbc8bf480c495 c30efd4497f54019bad421ad63e5447d RX(1.571) 5cf26fbc41614381a47fbc8bf480c495--c30efd4497f54019bad421ad63e5447d c30efd4497f54019bad421ad63e5447d--0a184eb47cec446a9f392baaed159770 76e3c46821084cbda3c1215b8ee565cd 672df9f05ef04aba89a6dd1e1a682598 RX(3.142) ccc98c3b7d384a44a3be39ca539ee496--672df9f05ef04aba89a6dd1e1a682598 7b4a626ef8074c6cb4ca865954110af9 672df9f05ef04aba89a6dd1e1a682598--7b4a626ef8074c6cb4ca865954110af9 d506801e94f7477b81f2997c77448216 RX(3.142) 7b4a626ef8074c6cb4ca865954110af9--d506801e94f7477b81f2997c77448216 d506801e94f7477b81f2997c77448216--76e3c46821084cbda3c1215b8ee565cd

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