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 a546858ee7e54118834a1e22fdae572e 0 221727262da048d4a2db661be1b62670 RX(0.5) a546858ee7e54118834a1e22fdae572e--221727262da048d4a2db661be1b62670 9db20d16b92d4e7d9679e1fdf7410d51 1 5f6c5083c0b9438798f172f3db6e9f57 221727262da048d4a2db661be1b62670--5f6c5083c0b9438798f172f3db6e9f57 0c24a1d8674044bc8d33eba8f15b07d5 5f6c5083c0b9438798f172f3db6e9f57--0c24a1d8674044bc8d33eba8f15b07d5 b4c51070bcf04a92abed43b43113597f 7034c5ac6803459e9567d025ee1a3167 9db20d16b92d4e7d9679e1fdf7410d51--7034c5ac6803459e9567d025ee1a3167 740f9df9a246422396574605df20bc89 X 7034c5ac6803459e9567d025ee1a3167--740f9df9a246422396574605df20bc89 740f9df9a246422396574605df20bc89--5f6c5083c0b9438798f172f3db6e9f57 740f9df9a246422396574605df20bc89--b4c51070bcf04a92abed43b43113597f

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 557a1fbab4484c5d8880a213a473d607 0 baf92c8f4dd04cc19122c76a6cb5de20 X 557a1fbab4484c5d8880a213a473d607--baf92c8f4dd04cc19122c76a6cb5de20 2cfd57d462974787b9cc01a70fdcb861 1 00678b3546124213bcc50330af8e97f4 baf92c8f4dd04cc19122c76a6cb5de20--00678b3546124213bcc50330af8e97f4 35ac2b89ca1d4508a4a97fb3ce2891b3 f761a6dfcbf74a7cbbe1f9e2266b2088 Y 2cfd57d462974787b9cc01a70fdcb861--f761a6dfcbf74a7cbbe1f9e2266b2088 f761a6dfcbf74a7cbbe1f9e2266b2088--35ac2b89ca1d4508a4a97fb3ce2891b3

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 6e3480efd16c4f0fad54c7a208615602 0 7dcdf0c478f14271ae2005c07504f2d3 X 6e3480efd16c4f0fad54c7a208615602--7dcdf0c478f14271ae2005c07504f2d3 4f30ddbabbb94f689c76f5021dabce0a 1 2b8dfb2ce5734883a26bdcc58473ed11 Y 7dcdf0c478f14271ae2005c07504f2d3--2b8dfb2ce5734883a26bdcc58473ed11 88fa6eda2dbe477bb996215a0bfc119d 2b8dfb2ce5734883a26bdcc58473ed11--88fa6eda2dbe477bb996215a0bfc119d 6ec675f248984fd09cb62108a50adc01 b5c497a7e64249389e944185950e6f10 X 4f30ddbabbb94f689c76f5021dabce0a--b5c497a7e64249389e944185950e6f10 e050279367ce4650ae569f5b446cf105 Y b5c497a7e64249389e944185950e6f10--e050279367ce4650ae569f5b446cf105 e050279367ce4650ae569f5b446cf105--6ec675f248984fd09cb62108a50adc01

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 c5cbbf779b7941f3832e81c5e3b3d3fd 0 1dc15c868a5945f1aa820b2a23d34ba3 H c5cbbf779b7941f3832e81c5e3b3d3fd--1dc15c868a5945f1aa820b2a23d34ba3 dfe67084b6e94b4b90e8f0ca1ce11896 1 ca4e875f689848929656e7ea30aa0a16 PHASE(1.571) 1dc15c868a5945f1aa820b2a23d34ba3--ca4e875f689848929656e7ea30aa0a16 26c866a8ba7947a4aeccf126e5ae78d3 PHASE(0.785) ca4e875f689848929656e7ea30aa0a16--26c866a8ba7947a4aeccf126e5ae78d3 d1bcebd87766401b9c9f1c00a307900c ca4e875f689848929656e7ea30aa0a16--d1bcebd87766401b9c9f1c00a307900c afb9c0e6fdd941429074e91b77e7207a 26c866a8ba7947a4aeccf126e5ae78d3--afb9c0e6fdd941429074e91b77e7207a d3bf8a403c20401cbfec0bd808390a5f 26c866a8ba7947a4aeccf126e5ae78d3--d3bf8a403c20401cbfec0bd808390a5f 6fa6127e3b4c4dda80580e714cf6ec81 afb9c0e6fdd941429074e91b77e7207a--6fa6127e3b4c4dda80580e714cf6ec81 bf7dbae5e1884a2eb9ed3ede9808cb94 6fa6127e3b4c4dda80580e714cf6ec81--bf7dbae5e1884a2eb9ed3ede9808cb94 b0d00290d1f04d4aabb0ffaef211209b bf7dbae5e1884a2eb9ed3ede9808cb94--b0d00290d1f04d4aabb0ffaef211209b cca2edffe3d64537880104c50871e24c 63e05257f9d54ab69f9e9cc53f0f4ef1 dfe67084b6e94b4b90e8f0ca1ce11896--63e05257f9d54ab69f9e9cc53f0f4ef1 eded286ec6ed46ae81240debc9c665fa 2 63e05257f9d54ab69f9e9cc53f0f4ef1--d1bcebd87766401b9c9f1c00a307900c 4f5442a46d8a4832812ba7135ee7f09a d1bcebd87766401b9c9f1c00a307900c--4f5442a46d8a4832812ba7135ee7f09a 4789c21cf965405281be26112fca1b7a H 4f5442a46d8a4832812ba7135ee7f09a--4789c21cf965405281be26112fca1b7a d9ac5451d81a4abca985a16d0b24c1c1 PHASE(1.571) 4789c21cf965405281be26112fca1b7a--d9ac5451d81a4abca985a16d0b24c1c1 6835993e1d93433f98536e975124bb92 d9ac5451d81a4abca985a16d0b24c1c1--6835993e1d93433f98536e975124bb92 149224a12e55423da93d1a4941e7154c d9ac5451d81a4abca985a16d0b24c1c1--149224a12e55423da93d1a4941e7154c 6835993e1d93433f98536e975124bb92--cca2edffe3d64537880104c50871e24c f15931f459a14c54aa8d571e453bfcf8 2227cad8aed649f1bb637a20e4c67c5b eded286ec6ed46ae81240debc9c665fa--2227cad8aed649f1bb637a20e4c67c5b 81e83a7b85ed42e4b747ba6ee656b93c 2227cad8aed649f1bb637a20e4c67c5b--81e83a7b85ed42e4b747ba6ee656b93c 81e83a7b85ed42e4b747ba6ee656b93c--d3bf8a403c20401cbfec0bd808390a5f 686c0157c6d444a79bb88dfcc03e70b6 d3bf8a403c20401cbfec0bd808390a5f--686c0157c6d444a79bb88dfcc03e70b6 686c0157c6d444a79bb88dfcc03e70b6--149224a12e55423da93d1a4941e7154c a91d7269eefd44a1af245777b1c879c6 H 149224a12e55423da93d1a4941e7154c--a91d7269eefd44a1af245777b1c879c6 a91d7269eefd44a1af245777b1c879c6--f15931f459a14c54aa8d571e453bfcf8

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 e40374ef7e5445e695f58dc8c81e4295 0 1bfb995ee9b64672a9a957502ad9e5d3 e40374ef7e5445e695f58dc8c81e4295--1bfb995ee9b64672a9a957502ad9e5d3 1704c09709d54a67b56f8d1089516e33 1 8f29ff9477dc4a6ba674106f94b6f2ff 1bfb995ee9b64672a9a957502ad9e5d3--8f29ff9477dc4a6ba674106f94b6f2ff 7053ecc421754235bbda84d5ce4ed24d 8f29ff9477dc4a6ba674106f94b6f2ff--7053ecc421754235bbda84d5ce4ed24d 8acc7ee48f9941fe96387d020c6f49a9 PHASE(-0.785) 7053ecc421754235bbda84d5ce4ed24d--8acc7ee48f9941fe96387d020c6f49a9 69fadbe86ebb4d56bd0c93a0ca9aa9af PHASE(-1.571) 8acc7ee48f9941fe96387d020c6f49a9--69fadbe86ebb4d56bd0c93a0ca9aa9af b538bd342bcc411987414355ce735090 8acc7ee48f9941fe96387d020c6f49a9--b538bd342bcc411987414355ce735090 93fae2d28c5c43c4ba5fedc29f528311 H 69fadbe86ebb4d56bd0c93a0ca9aa9af--93fae2d28c5c43c4ba5fedc29f528311 2686e0b8cbfa430bb82762330aa47da7 69fadbe86ebb4d56bd0c93a0ca9aa9af--2686e0b8cbfa430bb82762330aa47da7 948f6583aca54a8dbec329209d8779ef 93fae2d28c5c43c4ba5fedc29f528311--948f6583aca54a8dbec329209d8779ef fcf8b44e0912442ca35a17adc65071fe 4ec6063cfa0d4e26b18a0dca6ba4fb16 1704c09709d54a67b56f8d1089516e33--4ec6063cfa0d4e26b18a0dca6ba4fb16 729983d1def74c3b8d435cea3d391310 2 fe435edbdee54ab482eaf92473fc460a PHASE(-1.571) 4ec6063cfa0d4e26b18a0dca6ba4fb16--fe435edbdee54ab482eaf92473fc460a c0bee84e28a64ce2b50833365ad4ba62 H fe435edbdee54ab482eaf92473fc460a--c0bee84e28a64ce2b50833365ad4ba62 00cb909f598f4c568c95d3b18025ec33 fe435edbdee54ab482eaf92473fc460a--00cb909f598f4c568c95d3b18025ec33 7c41c579fe594e1dbd091a69ae81876c c0bee84e28a64ce2b50833365ad4ba62--7c41c579fe594e1dbd091a69ae81876c 7c41c579fe594e1dbd091a69ae81876c--2686e0b8cbfa430bb82762330aa47da7 4991961c9fd54c059225f62b5fe8fc2b 2686e0b8cbfa430bb82762330aa47da7--4991961c9fd54c059225f62b5fe8fc2b 4991961c9fd54c059225f62b5fe8fc2b--fcf8b44e0912442ca35a17adc65071fe 6b955507cd694e4caa63611c29ee7049 95e049d31cc94b5da9e656583a9f2c63 H 729983d1def74c3b8d435cea3d391310--95e049d31cc94b5da9e656583a9f2c63 95e049d31cc94b5da9e656583a9f2c63--00cb909f598f4c568c95d3b18025ec33 a6ae5385b74648f3bac456e92e71ffc8 00cb909f598f4c568c95d3b18025ec33--a6ae5385b74648f3bac456e92e71ffc8 a6ae5385b74648f3bac456e92e71ffc8--b538bd342bcc411987414355ce735090 f5a98932813b4d70bf516ba03e4cbb34 b538bd342bcc411987414355ce735090--f5a98932813b4d70bf516ba03e4cbb34 d1bd818e69a64a00a1cf21a54bb51b9f f5a98932813b4d70bf516ba03e4cbb34--d1bd818e69a64a00a1cf21a54bb51b9f d1bd818e69a64a00a1cf21a54bb51b9f--6b955507cd694e4caa63611c29ee7049

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_fcd255cd099c4657b3d166d74486994c b66e4407199c4ab48dc082dff45b987c 0 9171139562d742a494085f92f10a0916 RX(0.0) b66e4407199c4ab48dc082dff45b987c--9171139562d742a494085f92f10a0916 10483def4dff4b7dadb5ae6a9cf7ae64 1 28693f8e1ee74b0db97d4ad58e0a6be4 HamEvo 9171139562d742a494085f92f10a0916--28693f8e1ee74b0db97d4ad58e0a6be4 e66696cd3f204f3e9bdc14ae737fb656 RX(0.0) 28693f8e1ee74b0db97d4ad58e0a6be4--e66696cd3f204f3e9bdc14ae737fb656 3cfae45639ea4a2c83f0422477dadc84 e66696cd3f204f3e9bdc14ae737fb656--3cfae45639ea4a2c83f0422477dadc84 888cc82dace042d8b958192d709298a2 0ccd1271eb3e461386ae13543d00b86c RX(1.571) 10483def4dff4b7dadb5ae6a9cf7ae64--0ccd1271eb3e461386ae13543d00b86c 9f5346d8250847f3a16c9db9c25584ca 2 b7fc28cb9f874ef188f22f7cc25533e0 t = 1.000 0ccd1271eb3e461386ae13543d00b86c--b7fc28cb9f874ef188f22f7cc25533e0 5b8ffd48d7124bdc9e75843a6edfbbf8 RX(1.571) b7fc28cb9f874ef188f22f7cc25533e0--5b8ffd48d7124bdc9e75843a6edfbbf8 5b8ffd48d7124bdc9e75843a6edfbbf8--888cc82dace042d8b958192d709298a2 36167c55a6eb4799a06464e15e420d6c db8d4bc5fd7c453b93a11dc28f9d2456 RX(3.142) 9f5346d8250847f3a16c9db9c25584ca--db8d4bc5fd7c453b93a11dc28f9d2456 aa9375ba47944ae0bdbd7bb7a912d736 db8d4bc5fd7c453b93a11dc28f9d2456--aa9375ba47944ae0bdbd7bb7a912d736 ce4e787290b04d9fbca42c1329dbf0b3 RX(3.142) aa9375ba47944ae0bdbd7bb7a912d736--ce4e787290b04d9fbca42c1329dbf0b3 ce4e787290b04d9fbca42c1329dbf0b3--36167c55a6eb4799a06464e15e420d6c

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({'11': 264, '10': 254, '01': 242, '00': 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.