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 6c79205bd5e144a9b21b248c3fd6bebf 0 b0e765b12db54e579d7b6463d309eebe RX(0.5) 6c79205bd5e144a9b21b248c3fd6bebf--b0e765b12db54e579d7b6463d309eebe 8c292ab95d6e4c4cb62d70e1884123c9 1 a32834096ef34516b3924babf8b150b0 b0e765b12db54e579d7b6463d309eebe--a32834096ef34516b3924babf8b150b0 243647684b6840908ed08b9003d76e24 a32834096ef34516b3924babf8b150b0--243647684b6840908ed08b9003d76e24 9c0d97cfb4834b56adfda679280e5215 d945dd5db3d640ffbffd8f0e76ad1d46 8c292ab95d6e4c4cb62d70e1884123c9--d945dd5db3d640ffbffd8f0e76ad1d46 a711c7db429b4a67a7f711a05009185d X d945dd5db3d640ffbffd8f0e76ad1d46--a711c7db429b4a67a7f711a05009185d a711c7db429b4a67a7f711a05009185d--a32834096ef34516b3924babf8b150b0 a711c7db429b4a67a7f711a05009185d--9c0d97cfb4834b56adfda679280e5215

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 eb9540de84d0452eb462811f3a99bb95 0 3d191abd8e0f4f7da3a518cecd254762 X eb9540de84d0452eb462811f3a99bb95--3d191abd8e0f4f7da3a518cecd254762 0be7fa248b554cb186284b9b58e19bdc 1 f67461792d6749d5a53a87338f7954cb 3d191abd8e0f4f7da3a518cecd254762--f67461792d6749d5a53a87338f7954cb 5134527acd7a4244a5263ede6c8dbc60 315eee1f1225418dbc6d289e3ab6c559 Y 0be7fa248b554cb186284b9b58e19bdc--315eee1f1225418dbc6d289e3ab6c559 315eee1f1225418dbc6d289e3ab6c559--5134527acd7a4244a5263ede6c8dbc60

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 cb44cf41393340baa2c1e582d5d56360 0 70fff6b8e5be4ed09db80e3df02f65bc X cb44cf41393340baa2c1e582d5d56360--70fff6b8e5be4ed09db80e3df02f65bc 72fb0806d7b64fd29b32676a6f757994 1 82f2ff44f6414c638aa30e36e00ed25d Y 70fff6b8e5be4ed09db80e3df02f65bc--82f2ff44f6414c638aa30e36e00ed25d e2d314081cee471d9a7ce209c68c3dbc 82f2ff44f6414c638aa30e36e00ed25d--e2d314081cee471d9a7ce209c68c3dbc b52a94a691e747899715cd83a3a59ac8 c05bcc495f2c433bb3c56d4ad01be790 X 72fb0806d7b64fd29b32676a6f757994--c05bcc495f2c433bb3c56d4ad01be790 281b1a3dfa824475bcc8c246b54a7e2f Y c05bcc495f2c433bb3c56d4ad01be790--281b1a3dfa824475bcc8c246b54a7e2f 281b1a3dfa824475bcc8c246b54a7e2f--b52a94a691e747899715cd83a3a59ac8

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 8443092f370c40699989313f416b6265 0 de5121ea4063431ebd45a109147d102c H 8443092f370c40699989313f416b6265--de5121ea4063431ebd45a109147d102c d9d33b64b17c445ab9720be750945f36 1 322ee9032e744c38a982d78cf21c4d31 PHASE(1.571) de5121ea4063431ebd45a109147d102c--322ee9032e744c38a982d78cf21c4d31 1c1c96f8bfb94fc99aeb07e4e30ee467 PHASE(0.785) 322ee9032e744c38a982d78cf21c4d31--1c1c96f8bfb94fc99aeb07e4e30ee467 04e8a38d15cf45289cd3b3e37bf538c6 322ee9032e744c38a982d78cf21c4d31--04e8a38d15cf45289cd3b3e37bf538c6 3c1a1b6dde5c455092fa2129df27bd82 1c1c96f8bfb94fc99aeb07e4e30ee467--3c1a1b6dde5c455092fa2129df27bd82 5654b2505de24e39b86577ebfb77d9ee 1c1c96f8bfb94fc99aeb07e4e30ee467--5654b2505de24e39b86577ebfb77d9ee aa8e6786172949c0aa223555b8aa23ef 3c1a1b6dde5c455092fa2129df27bd82--aa8e6786172949c0aa223555b8aa23ef 0fdfe138f5934ff4b7fbbaae2752be87 aa8e6786172949c0aa223555b8aa23ef--0fdfe138f5934ff4b7fbbaae2752be87 c77a9913602f4c1f9bc8a91d825cee6b 0fdfe138f5934ff4b7fbbaae2752be87--c77a9913602f4c1f9bc8a91d825cee6b 6ba2c3751c204fb5839e987bd9f9dd1d 37c3878e510b4c27a40b692d8c2648c1 d9d33b64b17c445ab9720be750945f36--37c3878e510b4c27a40b692d8c2648c1 2acdbea0f74744a889c89d8870cd8ade 2 37c3878e510b4c27a40b692d8c2648c1--04e8a38d15cf45289cd3b3e37bf538c6 0f3e318b52974f03bd90197995325fbf 04e8a38d15cf45289cd3b3e37bf538c6--0f3e318b52974f03bd90197995325fbf 5c061ff96bd44fe8b62c567fb0251632 H 0f3e318b52974f03bd90197995325fbf--5c061ff96bd44fe8b62c567fb0251632 d9d291f9151542649d18d5ab2a77263a PHASE(1.571) 5c061ff96bd44fe8b62c567fb0251632--d9d291f9151542649d18d5ab2a77263a ff95e790108b402b809fd5cc260e68e3 d9d291f9151542649d18d5ab2a77263a--ff95e790108b402b809fd5cc260e68e3 a54b77f973f14fd7b935f5fce76b2cf5 d9d291f9151542649d18d5ab2a77263a--a54b77f973f14fd7b935f5fce76b2cf5 ff95e790108b402b809fd5cc260e68e3--6ba2c3751c204fb5839e987bd9f9dd1d 6dc75a409294458a87945ce145a7fb96 d60f5bae9ffc4bac8c1109a69951b3b8 2acdbea0f74744a889c89d8870cd8ade--d60f5bae9ffc4bac8c1109a69951b3b8 69dc3a37f3754d729dc38d223daf2fc3 d60f5bae9ffc4bac8c1109a69951b3b8--69dc3a37f3754d729dc38d223daf2fc3 69dc3a37f3754d729dc38d223daf2fc3--5654b2505de24e39b86577ebfb77d9ee ffaca2a6fc5245338b7d3d741a215f54 5654b2505de24e39b86577ebfb77d9ee--ffaca2a6fc5245338b7d3d741a215f54 ffaca2a6fc5245338b7d3d741a215f54--a54b77f973f14fd7b935f5fce76b2cf5 f8a8b124f531468295495a4799b6bbf0 H a54b77f973f14fd7b935f5fce76b2cf5--f8a8b124f531468295495a4799b6bbf0 f8a8b124f531468295495a4799b6bbf0--6dc75a409294458a87945ce145a7fb96

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 17bc8305444d47128ea7ad44ff531d4e 0 f347892604334b3a85b84104acf28df4 17bc8305444d47128ea7ad44ff531d4e--f347892604334b3a85b84104acf28df4 75e43f384b384bcc8dc32f0610693fac 1 73ea1eabebb2467a8860a964dfb72f80 f347892604334b3a85b84104acf28df4--73ea1eabebb2467a8860a964dfb72f80 df8e984f79294beabf7e1c307dfbf09f 73ea1eabebb2467a8860a964dfb72f80--df8e984f79294beabf7e1c307dfbf09f 79179a3c55e94cb99fcc8151506f029e PHASE(-0.785) df8e984f79294beabf7e1c307dfbf09f--79179a3c55e94cb99fcc8151506f029e 840e5c88146748359d82daab5f96e2ba PHASE(-1.571) 79179a3c55e94cb99fcc8151506f029e--840e5c88146748359d82daab5f96e2ba 2a2a800dc1f34c3d9554c140af24ec8e 79179a3c55e94cb99fcc8151506f029e--2a2a800dc1f34c3d9554c140af24ec8e 990942ca848f40aa8db9a624ee6bfb4a H 840e5c88146748359d82daab5f96e2ba--990942ca848f40aa8db9a624ee6bfb4a 1b0d1a1cd8ea4048a7a92c5948358215 840e5c88146748359d82daab5f96e2ba--1b0d1a1cd8ea4048a7a92c5948358215 90188be3db7948edbf2fc04521fb0303 990942ca848f40aa8db9a624ee6bfb4a--90188be3db7948edbf2fc04521fb0303 258ab2ff8ed240d682f35e1d71d548f3 5d525a1fb86d43b3861a19f37d9af0ec 75e43f384b384bcc8dc32f0610693fac--5d525a1fb86d43b3861a19f37d9af0ec 4b4302f4446042d6ba4cf29409cb44d3 2 75de9a5a36ea454f8c4efa741e8def8a PHASE(-1.571) 5d525a1fb86d43b3861a19f37d9af0ec--75de9a5a36ea454f8c4efa741e8def8a 8a5dbc54199d4e058cd3560fa1188c6d H 75de9a5a36ea454f8c4efa741e8def8a--8a5dbc54199d4e058cd3560fa1188c6d 6161c3dd3ebc482f910561d57d2eb154 75de9a5a36ea454f8c4efa741e8def8a--6161c3dd3ebc482f910561d57d2eb154 f1a5550f383d4ed7a92d61eee30bbc10 8a5dbc54199d4e058cd3560fa1188c6d--f1a5550f383d4ed7a92d61eee30bbc10 f1a5550f383d4ed7a92d61eee30bbc10--1b0d1a1cd8ea4048a7a92c5948358215 c573b660f5d745b598731b0dbfec35c3 1b0d1a1cd8ea4048a7a92c5948358215--c573b660f5d745b598731b0dbfec35c3 c573b660f5d745b598731b0dbfec35c3--258ab2ff8ed240d682f35e1d71d548f3 a2cb70704abd4b2686f9400dafdafa18 313a70bf0f9d4472948da6d632828a30 H 4b4302f4446042d6ba4cf29409cb44d3--313a70bf0f9d4472948da6d632828a30 313a70bf0f9d4472948da6d632828a30--6161c3dd3ebc482f910561d57d2eb154 465f8e7537e7488692b086d3ed8dd2af 6161c3dd3ebc482f910561d57d2eb154--465f8e7537e7488692b086d3ed8dd2af 465f8e7537e7488692b086d3ed8dd2af--2a2a800dc1f34c3d9554c140af24ec8e 1a13afd4cbb24c4eb0216ccba45c785c 2a2a800dc1f34c3d9554c140af24ec8e--1a13afd4cbb24c4eb0216ccba45c785c f7bffdb426984a139663138228d5c0d9 1a13afd4cbb24c4eb0216ccba45c785c--f7bffdb426984a139663138228d5c0d9 f7bffdb426984a139663138228d5c0d9--a2cb70704abd4b2686f9400dafdafa18

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_341c6ec6c5574cb48b796fee0854922c 7fb6c65756e24800b8e3754bb103d801 0 f5b29a8cb9c7483499dfbf8020d0ed8f RX(0.0) 7fb6c65756e24800b8e3754bb103d801--f5b29a8cb9c7483499dfbf8020d0ed8f eb6a5b9cc6ed4dad9d444aebfba87ca1 1 679282b9c495403b91f56bf2644aa0e5 HamEvo f5b29a8cb9c7483499dfbf8020d0ed8f--679282b9c495403b91f56bf2644aa0e5 dac25ac5e0744ded8d39ed95ba12f999 RX(0.0) 679282b9c495403b91f56bf2644aa0e5--dac25ac5e0744ded8d39ed95ba12f999 b04854ad0de9428393d6a143bf50626b dac25ac5e0744ded8d39ed95ba12f999--b04854ad0de9428393d6a143bf50626b b2b96a97551d40fea13c143d2b5a8279 4741b10f2a8343efbcc15306d55b0fa2 RX(1.571) eb6a5b9cc6ed4dad9d444aebfba87ca1--4741b10f2a8343efbcc15306d55b0fa2 065868a781f747438290b2463dbf3d34 2 107264b2c22145c8ab910c02aeef68d6 t = 1.000 4741b10f2a8343efbcc15306d55b0fa2--107264b2c22145c8ab910c02aeef68d6 2b7ecbc4ae9c41a8bda4c67da9eea87a RX(1.571) 107264b2c22145c8ab910c02aeef68d6--2b7ecbc4ae9c41a8bda4c67da9eea87a 2b7ecbc4ae9c41a8bda4c67da9eea87a--b2b96a97551d40fea13c143d2b5a8279 0b5074cb88bd4e63a513463573a6647c cd39fdebc9bf4ba4bbb8c3fb647a7336 RX(3.142) 065868a781f747438290b2463dbf3d34--cd39fdebc9bf4ba4bbb8c3fb647a7336 4a325cd20ccc4539926a524ff8973c12 cd39fdebc9bf4ba4bbb8c3fb647a7336--4a325cd20ccc4539926a524ff8973c12 9285fd7a2ff74f2fbc8da4ce809b08b1 RX(3.142) 4a325cd20ccc4539926a524ff8973c12--9285fd7a2ff74f2fbc8da4ce809b08b1 9285fd7a2ff74f2fbc8da4ce809b08b1--0b5074cb88bd4e63a513463573a6647c

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, '01': 252, '00': 249, '11': 232})]
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.