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 08e49591612844df90843055ba4b0428 0 04c6404ec7e945f0bdb9a645d90a59a9 RX(0.5) 08e49591612844df90843055ba4b0428--04c6404ec7e945f0bdb9a645d90a59a9 82493a21fc374c65a9e3ae8d5ad940fb 1 e914eb9d499b418e8858c51dab74a2ee 04c6404ec7e945f0bdb9a645d90a59a9--e914eb9d499b418e8858c51dab74a2ee d90e3f9363be4e01a5c1b4f23c575be2 e914eb9d499b418e8858c51dab74a2ee--d90e3f9363be4e01a5c1b4f23c575be2 8cad096842ea4adbb3d8a1d92b918be7 05646dd98aab4455aabdc36023a5d018 82493a21fc374c65a9e3ae8d5ad940fb--05646dd98aab4455aabdc36023a5d018 a3d7556b15564a25846a5f3802954397 X 05646dd98aab4455aabdc36023a5d018--a3d7556b15564a25846a5f3802954397 a3d7556b15564a25846a5f3802954397--e914eb9d499b418e8858c51dab74a2ee a3d7556b15564a25846a5f3802954397--8cad096842ea4adbb3d8a1d92b918be7

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 a02f0a86757c45b180eafb46cab06f57 0 afad75b1566b4666883572d667a2ca75 X a02f0a86757c45b180eafb46cab06f57--afad75b1566b4666883572d667a2ca75 193f4bf05be743bea1cf99c683172024 1 9dcfb04212754dcf92672807ee1cadbb afad75b1566b4666883572d667a2ca75--9dcfb04212754dcf92672807ee1cadbb 11de88e7c2ad4c15b57abcf74ba5bd5c d1c546aecdb449fbb1c149e7e3281e87 Y 193f4bf05be743bea1cf99c683172024--d1c546aecdb449fbb1c149e7e3281e87 d1c546aecdb449fbb1c149e7e3281e87--11de88e7c2ad4c15b57abcf74ba5bd5c

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 28859aa65c46409d8864b7bd16590b79 0 618b8dd98fe449fe9501f7e66b23ac6a X 28859aa65c46409d8864b7bd16590b79--618b8dd98fe449fe9501f7e66b23ac6a 4565e2f683b049029e0910bfbc244ec6 1 10393ee4513e4b1e8c2b95a48aa350f0 Y 618b8dd98fe449fe9501f7e66b23ac6a--10393ee4513e4b1e8c2b95a48aa350f0 4affd1aaaba54de981680fe4cc87cdd8 10393ee4513e4b1e8c2b95a48aa350f0--4affd1aaaba54de981680fe4cc87cdd8 e0c2ce0399994a8fa39b9c41271eba24 fe2f3442f65a4343a4d7a6ac0663f8a0 X 4565e2f683b049029e0910bfbc244ec6--fe2f3442f65a4343a4d7a6ac0663f8a0 7260d227f6314a49a5439787874e4822 Y fe2f3442f65a4343a4d7a6ac0663f8a0--7260d227f6314a49a5439787874e4822 7260d227f6314a49a5439787874e4822--e0c2ce0399994a8fa39b9c41271eba24

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 4c7773353a4049b7ab822b199fd63f1f 0 8691c8803e2549f29a18357543e9747e H 4c7773353a4049b7ab822b199fd63f1f--8691c8803e2549f29a18357543e9747e 7a6958bb483a441cb4db97b9a27f030f 1 f79683677cff4767b54bc2e96b97e125 PHASE(1.571) 8691c8803e2549f29a18357543e9747e--f79683677cff4767b54bc2e96b97e125 976720ba3706411694b0155e39c021c1 PHASE(0.785) f79683677cff4767b54bc2e96b97e125--976720ba3706411694b0155e39c021c1 9d6c6e6a7c2246ad8797003008c9243e f79683677cff4767b54bc2e96b97e125--9d6c6e6a7c2246ad8797003008c9243e 81c02d52c6b2400b9df97b7244ac718e 976720ba3706411694b0155e39c021c1--81c02d52c6b2400b9df97b7244ac718e 9d3fb0d6538c406f852b1d239403b6de 976720ba3706411694b0155e39c021c1--9d3fb0d6538c406f852b1d239403b6de e390ab932de14336a0a89e6c154ddfc8 81c02d52c6b2400b9df97b7244ac718e--e390ab932de14336a0a89e6c154ddfc8 901e871ecb9f440081d37874e676c73a e390ab932de14336a0a89e6c154ddfc8--901e871ecb9f440081d37874e676c73a 8ef7e134957340e0b01649f95f7261c1 901e871ecb9f440081d37874e676c73a--8ef7e134957340e0b01649f95f7261c1 7fcb6aaa01ea47bba7287cc39077bd2e 2136b9e5a18f4089ad53843cbff49229 7a6958bb483a441cb4db97b9a27f030f--2136b9e5a18f4089ad53843cbff49229 9b42679a0f1b4b0a9ebf1a756a281f66 2 2136b9e5a18f4089ad53843cbff49229--9d6c6e6a7c2246ad8797003008c9243e f13d80dc23f84f928f20f1b5d99d645c 9d6c6e6a7c2246ad8797003008c9243e--f13d80dc23f84f928f20f1b5d99d645c 6b1bdae93ece45babc51ed22a1a6459a H f13d80dc23f84f928f20f1b5d99d645c--6b1bdae93ece45babc51ed22a1a6459a 9964ea5e635c476990731f36a5576376 PHASE(1.571) 6b1bdae93ece45babc51ed22a1a6459a--9964ea5e635c476990731f36a5576376 d618cd4b8e7b4fb0929352402d7609f7 9964ea5e635c476990731f36a5576376--d618cd4b8e7b4fb0929352402d7609f7 f2cc0a81dd864f3daf7a83eea274a0d7 9964ea5e635c476990731f36a5576376--f2cc0a81dd864f3daf7a83eea274a0d7 d618cd4b8e7b4fb0929352402d7609f7--7fcb6aaa01ea47bba7287cc39077bd2e 6712520748e7478fbddb0c5867d19135 c1e4adf9346849ecb16c7ef29c257cd2 9b42679a0f1b4b0a9ebf1a756a281f66--c1e4adf9346849ecb16c7ef29c257cd2 f4e44a5faabc4c5f81619bea64850cad c1e4adf9346849ecb16c7ef29c257cd2--f4e44a5faabc4c5f81619bea64850cad f4e44a5faabc4c5f81619bea64850cad--9d3fb0d6538c406f852b1d239403b6de b092ba1017f14fde98f6272c2ed484c4 9d3fb0d6538c406f852b1d239403b6de--b092ba1017f14fde98f6272c2ed484c4 b092ba1017f14fde98f6272c2ed484c4--f2cc0a81dd864f3daf7a83eea274a0d7 f2d60744ccb74cc5a71456309386bed3 H f2cc0a81dd864f3daf7a83eea274a0d7--f2d60744ccb74cc5a71456309386bed3 f2d60744ccb74cc5a71456309386bed3--6712520748e7478fbddb0c5867d19135

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 7abf8953ea1545969f4faaaabc7df8dd 0 0f511e30de7b4af780182e0834bd2174 7abf8953ea1545969f4faaaabc7df8dd--0f511e30de7b4af780182e0834bd2174 5204f2105acb40c390ea46a0a7b026ab 1 a195368b39ed431385d05bc7859115de 0f511e30de7b4af780182e0834bd2174--a195368b39ed431385d05bc7859115de 4b3ea13acf1445d6858ee774c3cae79c a195368b39ed431385d05bc7859115de--4b3ea13acf1445d6858ee774c3cae79c 5923f23e324a43d59f85071448583df4 PHASE(-0.785) 4b3ea13acf1445d6858ee774c3cae79c--5923f23e324a43d59f85071448583df4 008558a012e7458f8a94df26c4fae24b PHASE(-1.571) 5923f23e324a43d59f85071448583df4--008558a012e7458f8a94df26c4fae24b d2abf10365ec40688105187104f4194c 5923f23e324a43d59f85071448583df4--d2abf10365ec40688105187104f4194c 57a3b7fba9854b339dc2e399e8b66b62 H 008558a012e7458f8a94df26c4fae24b--57a3b7fba9854b339dc2e399e8b66b62 31fd4d2d6543446ea54d3d2cd4f5ed1c 008558a012e7458f8a94df26c4fae24b--31fd4d2d6543446ea54d3d2cd4f5ed1c 20bfabcb55e74acbba7bd35d9b8f5403 57a3b7fba9854b339dc2e399e8b66b62--20bfabcb55e74acbba7bd35d9b8f5403 b89984dd1dba4ebf923a9725b24a654d 24b3424d1f104dca81ac666d914a1958 5204f2105acb40c390ea46a0a7b026ab--24b3424d1f104dca81ac666d914a1958 c63874b88a474fc589f9c3e5fdc93002 2 d6398e44a42d4af1b23eb2c0600c4750 PHASE(-1.571) 24b3424d1f104dca81ac666d914a1958--d6398e44a42d4af1b23eb2c0600c4750 e28a147b66364e1fb110e7ad8e7b96f6 H d6398e44a42d4af1b23eb2c0600c4750--e28a147b66364e1fb110e7ad8e7b96f6 f447ab5ff913460493300fd9bd227aeb d6398e44a42d4af1b23eb2c0600c4750--f447ab5ff913460493300fd9bd227aeb ce2697af43e24cf8b087a9ed1baca3b7 e28a147b66364e1fb110e7ad8e7b96f6--ce2697af43e24cf8b087a9ed1baca3b7 ce2697af43e24cf8b087a9ed1baca3b7--31fd4d2d6543446ea54d3d2cd4f5ed1c 961273bd41e148fe8647993ebea9a112 31fd4d2d6543446ea54d3d2cd4f5ed1c--961273bd41e148fe8647993ebea9a112 961273bd41e148fe8647993ebea9a112--b89984dd1dba4ebf923a9725b24a654d ccb6307caf494b8496995adc104affd4 876d8bc721554a6d84539281e5affb2e H c63874b88a474fc589f9c3e5fdc93002--876d8bc721554a6d84539281e5affb2e 876d8bc721554a6d84539281e5affb2e--f447ab5ff913460493300fd9bd227aeb 2f35c2e0fc494e52b0159c13273bace4 f447ab5ff913460493300fd9bd227aeb--2f35c2e0fc494e52b0159c13273bace4 2f35c2e0fc494e52b0159c13273bace4--d2abf10365ec40688105187104f4194c 6d3221dc4185467aaefde08ead8619a6 d2abf10365ec40688105187104f4194c--6d3221dc4185467aaefde08ead8619a6 9a0967adf71e4bdabed6a1878a00956a 6d3221dc4185467aaefde08ead8619a6--9a0967adf71e4bdabed6a1878a00956a 9a0967adf71e4bdabed6a1878a00956a--ccb6307caf494b8496995adc104affd4

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_a3a02d8e336a4d318d862f9279920fc5 46e9e32755864d4991293b5f3de9b3dc 0 cb45f2947873412fa5812d30a30a8061 RX(0.0) 46e9e32755864d4991293b5f3de9b3dc--cb45f2947873412fa5812d30a30a8061 5ec518e6c00740d8bb99a8376beb5595 1 11f5b0d2875a40be8fa6748404cfd77e HamEvo cb45f2947873412fa5812d30a30a8061--11f5b0d2875a40be8fa6748404cfd77e a4f6c8571d0a4407bfb2956c8ba2ff17 RX(0.0) 11f5b0d2875a40be8fa6748404cfd77e--a4f6c8571d0a4407bfb2956c8ba2ff17 114e9fd8ad2b4403ae219bd8bc8bd554 a4f6c8571d0a4407bfb2956c8ba2ff17--114e9fd8ad2b4403ae219bd8bc8bd554 a6feac9650fa43fd86b6418a5385130f 1c0c2061f55d4ad59295d14a80b83b9f RX(1.571) 5ec518e6c00740d8bb99a8376beb5595--1c0c2061f55d4ad59295d14a80b83b9f a6d61dee3ae74cada7bd095a4180484d 2 71e4d35f1e974cf5accee438e1f7ce7d t = 1.000 1c0c2061f55d4ad59295d14a80b83b9f--71e4d35f1e974cf5accee438e1f7ce7d b6d3fb04cb9c427dafdb2d7a420d2943 RX(1.571) 71e4d35f1e974cf5accee438e1f7ce7d--b6d3fb04cb9c427dafdb2d7a420d2943 b6d3fb04cb9c427dafdb2d7a420d2943--a6feac9650fa43fd86b6418a5385130f e39d194921694a6fb9ad0553e28e9b0b 63c450ddd0804157a8175a1ab36cec13 RX(3.142) a6d61dee3ae74cada7bd095a4180484d--63c450ddd0804157a8175a1ab36cec13 00e7c194465d41e5861f837cdb6e850a 63c450ddd0804157a8175a1ab36cec13--00e7c194465d41e5861f837cdb6e850a 0a100476d80c463d8cb5cc5675abb28a RX(3.142) 00e7c194465d41e5861f837cdb6e850a--0a100476d80c463d8cb5cc5675abb28a 0a100476d80c463d8cb5cc5675abb28a--e39d194921694a6fb9ad0553e28e9b0b

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': 259, '10': 256, '01': 255, '00': 230})]
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.