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 0a14a5a8b2244a88a7106ddf3ea02db5 0 1074e8e196a84a4d9367e3646b377be1 RX(0.5) 0a14a5a8b2244a88a7106ddf3ea02db5--1074e8e196a84a4d9367e3646b377be1 3261ac952dd94d95be0bfbfb96027183 1 0412147cc81742f8a2af38f85a315535 1074e8e196a84a4d9367e3646b377be1--0412147cc81742f8a2af38f85a315535 afd16e430f8b4e698ec809bd2245d647 0412147cc81742f8a2af38f85a315535--afd16e430f8b4e698ec809bd2245d647 0c150a8787aa4f01835dd3cdcfda2bdd c48003b21cdc4867950f162b93d3bed8 3261ac952dd94d95be0bfbfb96027183--c48003b21cdc4867950f162b93d3bed8 c51c7d5a2e9343acbb2b73a93602f21f X c48003b21cdc4867950f162b93d3bed8--c51c7d5a2e9343acbb2b73a93602f21f c51c7d5a2e9343acbb2b73a93602f21f--0412147cc81742f8a2af38f85a315535 c51c7d5a2e9343acbb2b73a93602f21f--0c150a8787aa4f01835dd3cdcfda2bdd

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 3afd1a2218334b9ba35c12ca15712f14 0 d97631e3c8c3428fb009f632c3853566 X 3afd1a2218334b9ba35c12ca15712f14--d97631e3c8c3428fb009f632c3853566 25e9e96049ad47ddabf41bef7d5a9a07 1 0d2ff80a718e4d03bf782681b4233d3e d97631e3c8c3428fb009f632c3853566--0d2ff80a718e4d03bf782681b4233d3e 5aa0be2a42bf43e6b06aa1c76595efe3 ca45b327ab424b10b0974117ffaa25b0 Y 25e9e96049ad47ddabf41bef7d5a9a07--ca45b327ab424b10b0974117ffaa25b0 ca45b327ab424b10b0974117ffaa25b0--5aa0be2a42bf43e6b06aa1c76595efe3

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 7804ab6099eb4e95b3c785d6a24f67ea 0 60905f2fea874d3e9054ab26db9772d4 X 7804ab6099eb4e95b3c785d6a24f67ea--60905f2fea874d3e9054ab26db9772d4 ead3a6c79a6840899f8d0435628f7a34 1 1e6164f18720475b90ecd01b1266d2de Y 60905f2fea874d3e9054ab26db9772d4--1e6164f18720475b90ecd01b1266d2de 602321844e994ee09c3b333e248d381a 1e6164f18720475b90ecd01b1266d2de--602321844e994ee09c3b333e248d381a 6eff07808cb64f9696bd1f2ae73d39f2 cea9f9fdf5ba4b5eb2318cc38f9b8d0e X ead3a6c79a6840899f8d0435628f7a34--cea9f9fdf5ba4b5eb2318cc38f9b8d0e fe62ec1533d04d12a2caee97639a65e5 Y cea9f9fdf5ba4b5eb2318cc38f9b8d0e--fe62ec1533d04d12a2caee97639a65e5 fe62ec1533d04d12a2caee97639a65e5--6eff07808cb64f9696bd1f2ae73d39f2

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 f9a3815b55954b14b643a7561e3d1678 0 0b003ff747724826a3328da919002457 H f9a3815b55954b14b643a7561e3d1678--0b003ff747724826a3328da919002457 383534123bab45f9ab4fdaafac8cf0eb 1 92b12af477254cd9ae61e02da95acfdb PHASE(1.571) 0b003ff747724826a3328da919002457--92b12af477254cd9ae61e02da95acfdb f55e28d7a1764ca98b4312941efa86ce PHASE(0.785) 92b12af477254cd9ae61e02da95acfdb--f55e28d7a1764ca98b4312941efa86ce eebc4589e52440ca96c08ff5c1282414 92b12af477254cd9ae61e02da95acfdb--eebc4589e52440ca96c08ff5c1282414 87e156d9d21f4e558a61e9f003e55476 f55e28d7a1764ca98b4312941efa86ce--87e156d9d21f4e558a61e9f003e55476 6ddec8f4894c47889831933c5f4d868d f55e28d7a1764ca98b4312941efa86ce--6ddec8f4894c47889831933c5f4d868d d46eb6b239224f3f8b55396fc0643d51 87e156d9d21f4e558a61e9f003e55476--d46eb6b239224f3f8b55396fc0643d51 bd4e9419ae114650a6fee54b5c620dd7 d46eb6b239224f3f8b55396fc0643d51--bd4e9419ae114650a6fee54b5c620dd7 4d40a0bc03674f629107fbccfc84c75d bd4e9419ae114650a6fee54b5c620dd7--4d40a0bc03674f629107fbccfc84c75d 3e100e1c19034355915a65d35f7bcc60 0d716512cd564039ae04117d4d5c9e60 383534123bab45f9ab4fdaafac8cf0eb--0d716512cd564039ae04117d4d5c9e60 9f5f1c15d57e4ae883600d7d41ea75a9 2 0d716512cd564039ae04117d4d5c9e60--eebc4589e52440ca96c08ff5c1282414 4ad36cd78dbb42c69e1887e511c5a1b9 eebc4589e52440ca96c08ff5c1282414--4ad36cd78dbb42c69e1887e511c5a1b9 30cc4c630bc7417eaa763e8072f1ce5a H 4ad36cd78dbb42c69e1887e511c5a1b9--30cc4c630bc7417eaa763e8072f1ce5a 5fc61b81b47643f3875b2eb705042323 PHASE(1.571) 30cc4c630bc7417eaa763e8072f1ce5a--5fc61b81b47643f3875b2eb705042323 9bb586b43cfc48f2b44675231a543985 5fc61b81b47643f3875b2eb705042323--9bb586b43cfc48f2b44675231a543985 871e17c6f4d24eb7a87feffccb4082e9 5fc61b81b47643f3875b2eb705042323--871e17c6f4d24eb7a87feffccb4082e9 9bb586b43cfc48f2b44675231a543985--3e100e1c19034355915a65d35f7bcc60 97f18a68cd45499bbf71435ce7456bc2 37940b43873c49a287257bc3a0eefcaa 9f5f1c15d57e4ae883600d7d41ea75a9--37940b43873c49a287257bc3a0eefcaa 41c60890258c494cbafa7bbaacd00a81 37940b43873c49a287257bc3a0eefcaa--41c60890258c494cbafa7bbaacd00a81 41c60890258c494cbafa7bbaacd00a81--6ddec8f4894c47889831933c5f4d868d ac88f14fcf2b48a8bbcf7d7147e465db 6ddec8f4894c47889831933c5f4d868d--ac88f14fcf2b48a8bbcf7d7147e465db ac88f14fcf2b48a8bbcf7d7147e465db--871e17c6f4d24eb7a87feffccb4082e9 3ea9530867a14bb5a943d534046a11e8 H 871e17c6f4d24eb7a87feffccb4082e9--3ea9530867a14bb5a943d534046a11e8 3ea9530867a14bb5a943d534046a11e8--97f18a68cd45499bbf71435ce7456bc2

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 8df766b2ddaa4bd6903bf74ec45433d0 0 583b610c39ce4b20ae99f42a12c5bf61 8df766b2ddaa4bd6903bf74ec45433d0--583b610c39ce4b20ae99f42a12c5bf61 8cf69c23acc641e4b0e355ad92a2dba4 1 c7660f868a2b4871bdfaab6252e123e4 583b610c39ce4b20ae99f42a12c5bf61--c7660f868a2b4871bdfaab6252e123e4 fb46f1bb362a4dd6a3c60aef68a67ac2 c7660f868a2b4871bdfaab6252e123e4--fb46f1bb362a4dd6a3c60aef68a67ac2 5dde26794fbd406f9064c44650747c0d PHASE(-0.785) fb46f1bb362a4dd6a3c60aef68a67ac2--5dde26794fbd406f9064c44650747c0d f122ce9eba4a4c82857fb39e04112987 PHASE(-1.571) 5dde26794fbd406f9064c44650747c0d--f122ce9eba4a4c82857fb39e04112987 a3bb48b50d3c4f8aa69a77bc4bac2ad9 5dde26794fbd406f9064c44650747c0d--a3bb48b50d3c4f8aa69a77bc4bac2ad9 f1a50cff690f4ad7955a4bc448bb84dd H f122ce9eba4a4c82857fb39e04112987--f1a50cff690f4ad7955a4bc448bb84dd 63a911ee769743588281fdad948e0576 f122ce9eba4a4c82857fb39e04112987--63a911ee769743588281fdad948e0576 7c8c778e2af945cbafb5e105dabdd07a f1a50cff690f4ad7955a4bc448bb84dd--7c8c778e2af945cbafb5e105dabdd07a 9fe07c18c58d4299b0b5f4173e0bef45 d61ee1a6f09f433699753f32a332701a 8cf69c23acc641e4b0e355ad92a2dba4--d61ee1a6f09f433699753f32a332701a 6cf11bd0f2844f828a5968dea7a80fb7 2 bf61dff2491a4ac6b0f1f03e727e50c2 PHASE(-1.571) d61ee1a6f09f433699753f32a332701a--bf61dff2491a4ac6b0f1f03e727e50c2 a7649d1178564255b38b20c84b7aabcd H bf61dff2491a4ac6b0f1f03e727e50c2--a7649d1178564255b38b20c84b7aabcd c95ec37fa0cb4c248c4a594756a0b5e7 bf61dff2491a4ac6b0f1f03e727e50c2--c95ec37fa0cb4c248c4a594756a0b5e7 19f587044a6b43cb809a06d3ff6d635e a7649d1178564255b38b20c84b7aabcd--19f587044a6b43cb809a06d3ff6d635e 19f587044a6b43cb809a06d3ff6d635e--63a911ee769743588281fdad948e0576 9518951e412744da9da555f60a46e929 63a911ee769743588281fdad948e0576--9518951e412744da9da555f60a46e929 9518951e412744da9da555f60a46e929--9fe07c18c58d4299b0b5f4173e0bef45 1142e4e441f546fd8e6a90da39c39d54 abf4b4496d2548bf97081b599b01845b H 6cf11bd0f2844f828a5968dea7a80fb7--abf4b4496d2548bf97081b599b01845b abf4b4496d2548bf97081b599b01845b--c95ec37fa0cb4c248c4a594756a0b5e7 0c0e7d021718487c8afa23a09a84eddd c95ec37fa0cb4c248c4a594756a0b5e7--0c0e7d021718487c8afa23a09a84eddd 0c0e7d021718487c8afa23a09a84eddd--a3bb48b50d3c4f8aa69a77bc4bac2ad9 d989d2e8abb94774ad02077fda6357db a3bb48b50d3c4f8aa69a77bc4bac2ad9--d989d2e8abb94774ad02077fda6357db 78940c0411e94fc6bd70ee8e77ff5879 d989d2e8abb94774ad02077fda6357db--78940c0411e94fc6bd70ee8e77ff5879 78940c0411e94fc6bd70ee8e77ff5879--1142e4e441f546fd8e6a90da39c39d54

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_b41fb0f2af60440a9bf016a1a03dc868 8975b87851a84529bd6628de54449b58 0 de2b77af4b314e4c95402f97c9e36c1a RX(0.0) 8975b87851a84529bd6628de54449b58--de2b77af4b314e4c95402f97c9e36c1a 6a0ea8ac68aa45988d9e38a0621305a7 1 98aa783a80f6444096b974656d3f0555 HamEvo de2b77af4b314e4c95402f97c9e36c1a--98aa783a80f6444096b974656d3f0555 8dd57f6ba751457693333aadecf179db RX(0.0) 98aa783a80f6444096b974656d3f0555--8dd57f6ba751457693333aadecf179db b2c41beea872467f84f3f69292be6994 8dd57f6ba751457693333aadecf179db--b2c41beea872467f84f3f69292be6994 bc24446f57a94c17a00d7133f3b84601 633ec59d0d7e4319a39a2b6ff3c16a7a RX(1.571) 6a0ea8ac68aa45988d9e38a0621305a7--633ec59d0d7e4319a39a2b6ff3c16a7a d9191ce6a12a45928b64b58b3d4909b2 2 83179c69987f4c639d55909d06c46297 t = 1.000 633ec59d0d7e4319a39a2b6ff3c16a7a--83179c69987f4c639d55909d06c46297 8674a31cb8184c4b99b361477ce6736b RX(1.571) 83179c69987f4c639d55909d06c46297--8674a31cb8184c4b99b361477ce6736b 8674a31cb8184c4b99b361477ce6736b--bc24446f57a94c17a00d7133f3b84601 b6967c7203924b508a91546bd7278fb2 db019e40265c4b9387036e7b2d1c4532 RX(3.142) d9191ce6a12a45928b64b58b3d4909b2--db019e40265c4b9387036e7b2d1c4532 65a709c7d1824cf7bf11591ebb84b7e5 db019e40265c4b9387036e7b2d1c4532--65a709c7d1824cf7bf11591ebb84b7e5 14604f3194034acbab5d0d9f372c29c4 RX(3.142) 65a709c7d1824cf7bf11591ebb84b7e5--14604f3194034acbab5d0d9f372c29c4 14604f3194034acbab5d0d9f372c29c4--b6967c7203924b508a91546bd7278fb2

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 = [Counter({'11': 264, '10': 260, '00': 238, '01': 238})]
ex = tensor([[0.]], requires_grad=True)

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.