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 c9ee5603e37f4fabad88e8d2e483b498 0 46d86444dc8347b48accdd7fba57fc3e RX(0.5) c9ee5603e37f4fabad88e8d2e483b498--46d86444dc8347b48accdd7fba57fc3e 8d4bd190408c4e72815b7d1c9bc43cff 1 543ae056a3434d2182e95a09c39e841b 46d86444dc8347b48accdd7fba57fc3e--543ae056a3434d2182e95a09c39e841b 1796a6b6533c46bdb6e23f9043024b8a 543ae056a3434d2182e95a09c39e841b--1796a6b6533c46bdb6e23f9043024b8a 9bf83e52923b47df95e1af6a7defd0b5 8a36819c595345b3a85b1bcc8dc0b7c0 8d4bd190408c4e72815b7d1c9bc43cff--8a36819c595345b3a85b1bcc8dc0b7c0 31ec1ce97423482ea96bfa0d09d70b15 X 8a36819c595345b3a85b1bcc8dc0b7c0--31ec1ce97423482ea96bfa0d09d70b15 31ec1ce97423482ea96bfa0d09d70b15--543ae056a3434d2182e95a09c39e841b 31ec1ce97423482ea96bfa0d09d70b15--9bf83e52923b47df95e1af6a7defd0b5

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 09749ac1fb8a43129cb16073e2c4fd69 0 21c03627a12c49df8dd2afd664518475 X 09749ac1fb8a43129cb16073e2c4fd69--21c03627a12c49df8dd2afd664518475 9d263d3d1c6f47f9bb240698aeca5674 1 b176025d83a545de98034323bac60313 21c03627a12c49df8dd2afd664518475--b176025d83a545de98034323bac60313 ef3e491a36994ca58c18af44e46caec9 53d3c6d7d7a24b82a2cd3f26b83ed8e3 Y 9d263d3d1c6f47f9bb240698aeca5674--53d3c6d7d7a24b82a2cd3f26b83ed8e3 53d3c6d7d7a24b82a2cd3f26b83ed8e3--ef3e491a36994ca58c18af44e46caec9

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 4afc1f6d07ac46a1829e886c95ffb770 0 68dfd899e6e24160beedf978000378e5 X 4afc1f6d07ac46a1829e886c95ffb770--68dfd899e6e24160beedf978000378e5 b05e01ce44924096badc76b48a45885d 1 2dc144718e154449ba64c819624ed88d Y 68dfd899e6e24160beedf978000378e5--2dc144718e154449ba64c819624ed88d 44a76c3046ce435fbc3c3682e751b218 2dc144718e154449ba64c819624ed88d--44a76c3046ce435fbc3c3682e751b218 4a481a1a00c24e24ae4e243f5f393ed0 0a7015ebc07e4fb0b18c639bd31c9297 X b05e01ce44924096badc76b48a45885d--0a7015ebc07e4fb0b18c639bd31c9297 df7746c74c9f49328eaa89a33afecbf0 Y 0a7015ebc07e4fb0b18c639bd31c9297--df7746c74c9f49328eaa89a33afecbf0 df7746c74c9f49328eaa89a33afecbf0--4a481a1a00c24e24ae4e243f5f393ed0

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 8d2e8380556b4252a1158905fd4a92dc 0 ed2ba5a270424f6bb0ad2eb62a0fc642 H 8d2e8380556b4252a1158905fd4a92dc--ed2ba5a270424f6bb0ad2eb62a0fc642 ec492e4f7e5f488ba49aed5be62d1c90 1 61f14fcf10074c78ac67483a9bc6077c PHASE(1.571) ed2ba5a270424f6bb0ad2eb62a0fc642--61f14fcf10074c78ac67483a9bc6077c 8ee84a3918e54d238e14dc160f9aa1c6 PHASE(0.785) 61f14fcf10074c78ac67483a9bc6077c--8ee84a3918e54d238e14dc160f9aa1c6 e962b29f7d224c56a70df927ba5e756c 61f14fcf10074c78ac67483a9bc6077c--e962b29f7d224c56a70df927ba5e756c 14b2388ad325495689b97b9bef5cb2a6 8ee84a3918e54d238e14dc160f9aa1c6--14b2388ad325495689b97b9bef5cb2a6 70c7a6ff953e495aa01cea6b11ff4f38 8ee84a3918e54d238e14dc160f9aa1c6--70c7a6ff953e495aa01cea6b11ff4f38 e1ca3d681c4945f28f454ccc3b8acb6c 14b2388ad325495689b97b9bef5cb2a6--e1ca3d681c4945f28f454ccc3b8acb6c 87c0e7cd741643f890ccd801576e8f28 e1ca3d681c4945f28f454ccc3b8acb6c--87c0e7cd741643f890ccd801576e8f28 c5041a0e2030435880a70f4399b724ca 87c0e7cd741643f890ccd801576e8f28--c5041a0e2030435880a70f4399b724ca 80006089745c42609a7d268873ee5e11 649db7d0912c49a7b5b3e847cf6bacf6 ec492e4f7e5f488ba49aed5be62d1c90--649db7d0912c49a7b5b3e847cf6bacf6 f6350701afea471da1e844bd1ed21f0c 2 649db7d0912c49a7b5b3e847cf6bacf6--e962b29f7d224c56a70df927ba5e756c fa83bc6aa57e4e3e9de026898db5c356 e962b29f7d224c56a70df927ba5e756c--fa83bc6aa57e4e3e9de026898db5c356 eb3ea1282653492ab49d52f7f7e51c04 H fa83bc6aa57e4e3e9de026898db5c356--eb3ea1282653492ab49d52f7f7e51c04 e95457fed59b4bf89ad0ddb761ce7ba8 PHASE(1.571) eb3ea1282653492ab49d52f7f7e51c04--e95457fed59b4bf89ad0ddb761ce7ba8 932493b55d7d46738a46849eb55b1210 e95457fed59b4bf89ad0ddb761ce7ba8--932493b55d7d46738a46849eb55b1210 2fa7438d7aa6478db9634422eacb598f e95457fed59b4bf89ad0ddb761ce7ba8--2fa7438d7aa6478db9634422eacb598f 932493b55d7d46738a46849eb55b1210--80006089745c42609a7d268873ee5e11 1755a51cbaee4de9a5202971c6b38c9b eb896db422aa4e9095feb266a9863aab f6350701afea471da1e844bd1ed21f0c--eb896db422aa4e9095feb266a9863aab cdabf25b8bf04ae5b18d607bcea95644 eb896db422aa4e9095feb266a9863aab--cdabf25b8bf04ae5b18d607bcea95644 cdabf25b8bf04ae5b18d607bcea95644--70c7a6ff953e495aa01cea6b11ff4f38 bf7044164234451eb6dd1e252f9feb4f 70c7a6ff953e495aa01cea6b11ff4f38--bf7044164234451eb6dd1e252f9feb4f bf7044164234451eb6dd1e252f9feb4f--2fa7438d7aa6478db9634422eacb598f 7bfb358badef418f91b57fa9c5955e79 H 2fa7438d7aa6478db9634422eacb598f--7bfb358badef418f91b57fa9c5955e79 7bfb358badef418f91b57fa9c5955e79--1755a51cbaee4de9a5202971c6b38c9b

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 25bde6eaf44b4631a623028ee8f1f06b 0 d62af55827f1487d84149103f9818d4d 25bde6eaf44b4631a623028ee8f1f06b--d62af55827f1487d84149103f9818d4d 47fa230bc2204e42ac1058e62c08b2ad 1 606d78ea05d74f248c1e196d76612c05 d62af55827f1487d84149103f9818d4d--606d78ea05d74f248c1e196d76612c05 5c16c939a04841f095b252a2a186556f 606d78ea05d74f248c1e196d76612c05--5c16c939a04841f095b252a2a186556f 7c1b4892647b4a9fb9723fd47a7f99a0 PHASE(-0.785) 5c16c939a04841f095b252a2a186556f--7c1b4892647b4a9fb9723fd47a7f99a0 f269407e3fa8454998be3c107eaf83d5 PHASE(-1.571) 7c1b4892647b4a9fb9723fd47a7f99a0--f269407e3fa8454998be3c107eaf83d5 63cc298727614054979fbc26fb89b74a 7c1b4892647b4a9fb9723fd47a7f99a0--63cc298727614054979fbc26fb89b74a 954482b15fbf453da0f47e6a92a5b3f4 H f269407e3fa8454998be3c107eaf83d5--954482b15fbf453da0f47e6a92a5b3f4 e203a969d81d48ad980af4290d8fe1f7 f269407e3fa8454998be3c107eaf83d5--e203a969d81d48ad980af4290d8fe1f7 da9904265e2a4e64ab52fdbe4eefeedd 954482b15fbf453da0f47e6a92a5b3f4--da9904265e2a4e64ab52fdbe4eefeedd a5624d140e2b478693009a45570f4836 f955421d569342e0ba5cc38844351f29 47fa230bc2204e42ac1058e62c08b2ad--f955421d569342e0ba5cc38844351f29 7e5c971005de4294b585db8eb5d20c6d 2 b74797b66ab7411d99af5fff06eb370e PHASE(-1.571) f955421d569342e0ba5cc38844351f29--b74797b66ab7411d99af5fff06eb370e efa2c582bc734144955332ead3ea1bf8 H b74797b66ab7411d99af5fff06eb370e--efa2c582bc734144955332ead3ea1bf8 9bfb3c61bf3548f4acbec6b9b6947732 b74797b66ab7411d99af5fff06eb370e--9bfb3c61bf3548f4acbec6b9b6947732 f280f45195464cf8b82c45db6146a9ef efa2c582bc734144955332ead3ea1bf8--f280f45195464cf8b82c45db6146a9ef f280f45195464cf8b82c45db6146a9ef--e203a969d81d48ad980af4290d8fe1f7 6136c677324444fbb8596df9213fadab e203a969d81d48ad980af4290d8fe1f7--6136c677324444fbb8596df9213fadab 6136c677324444fbb8596df9213fadab--a5624d140e2b478693009a45570f4836 380857234cbb4f3aaf0471d320057e58 689b4dfab4ec4601a699278bc887d16c H 7e5c971005de4294b585db8eb5d20c6d--689b4dfab4ec4601a699278bc887d16c 689b4dfab4ec4601a699278bc887d16c--9bfb3c61bf3548f4acbec6b9b6947732 45c1658175254776869993ac437b047c 9bfb3c61bf3548f4acbec6b9b6947732--45c1658175254776869993ac437b047c 45c1658175254776869993ac437b047c--63cc298727614054979fbc26fb89b74a 8d568d96a7594823a6ec9745f02a5d31 63cc298727614054979fbc26fb89b74a--8d568d96a7594823a6ec9745f02a5d31 654f5071143f4c6b9e515de5ee6a37ea 8d568d96a7594823a6ec9745f02a5d31--654f5071143f4c6b9e515de5ee6a37ea 654f5071143f4c6b9e515de5ee6a37ea--380857234cbb4f3aaf0471d320057e58

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_3e2ca6b5868f4239aa4c931738b365ab c5b702bfd5ab452db74b64029073a760 0 01a479102f794912b3d90967e1b3c4bf RX(0.0) c5b702bfd5ab452db74b64029073a760--01a479102f794912b3d90967e1b3c4bf 7e03adff81d44d02ac39426d5337ea00 1 217f2701b3ef48198b4b3599783f49dd HamEvo 01a479102f794912b3d90967e1b3c4bf--217f2701b3ef48198b4b3599783f49dd d1df473da9db49d2b0cb526a031597b4 RX(0.0) 217f2701b3ef48198b4b3599783f49dd--d1df473da9db49d2b0cb526a031597b4 8282141f225647df98bc95fd69093a74 d1df473da9db49d2b0cb526a031597b4--8282141f225647df98bc95fd69093a74 18a7f126c9154b8ea54ba50ef77c4706 1d5c4d993dd64c1ab44d72bc91f8d67e RX(1.571) 7e03adff81d44d02ac39426d5337ea00--1d5c4d993dd64c1ab44d72bc91f8d67e 124f44d8ca304ec48ed69693bffc9674 2 990475e5f680405cbfd6fc1b42b56c6b t = 1.000 1d5c4d993dd64c1ab44d72bc91f8d67e--990475e5f680405cbfd6fc1b42b56c6b 2001fe7cdcf64b83bb57e74af0e21de2 RX(1.571) 990475e5f680405cbfd6fc1b42b56c6b--2001fe7cdcf64b83bb57e74af0e21de2 2001fe7cdcf64b83bb57e74af0e21de2--18a7f126c9154b8ea54ba50ef77c4706 54c34b1891ff4875ba8c4195efcf49ec a138618e024146dba515e62759b8993d RX(3.142) 124f44d8ca304ec48ed69693bffc9674--a138618e024146dba515e62759b8993d 64b73c78f0f344d8bc4a4b9b9f5be002 a138618e024146dba515e62759b8993d--64b73c78f0f344d8bc4a4b9b9f5be002 2d2602da78d742bc9adfd5cf84bde4a8 RX(3.142) 64b73c78f0f344d8bc4a4b9b9f5be002--2d2602da78d742bc9adfd5cf84bde4a8 2d2602da78d742bc9adfd5cf84bde4a8--54c34b1891ff4875ba8c4195efcf49ec

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': 267, '00': 255, '10': 241, '01': 237})]
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.