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 5b1736b5bc8a4945b957331ba4e25341 0 ebc5ff4d90fe4e789a35a6d87bde1121 RX(0.5) 5b1736b5bc8a4945b957331ba4e25341--ebc5ff4d90fe4e789a35a6d87bde1121 12c8ef7b6a7d4fbba8daa10c360bd9a3 1 990a39daa04049e785e899a982960098 ebc5ff4d90fe4e789a35a6d87bde1121--990a39daa04049e785e899a982960098 54613140f6844c4fb758791049931135 990a39daa04049e785e899a982960098--54613140f6844c4fb758791049931135 91a11609dad04aba9fb4fd3be8370dd6 a58ff321d2fb4b2aba48ed6ebb1af075 12c8ef7b6a7d4fbba8daa10c360bd9a3--a58ff321d2fb4b2aba48ed6ebb1af075 93a3d2ad0b074b25ae5702076fb8087b X a58ff321d2fb4b2aba48ed6ebb1af075--93a3d2ad0b074b25ae5702076fb8087b 93a3d2ad0b074b25ae5702076fb8087b--990a39daa04049e785e899a982960098 93a3d2ad0b074b25ae5702076fb8087b--91a11609dad04aba9fb4fd3be8370dd6

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 80817dc09d7244c08e16d819322b7cf1 0 d53b38e3fda94c899a6f2c3cf955932e X 80817dc09d7244c08e16d819322b7cf1--d53b38e3fda94c899a6f2c3cf955932e 5bc6a6ba4d5d4ede9e76f564e1d20146 1 fc313ddfa83b4bd28a6e42ca64b9b89e d53b38e3fda94c899a6f2c3cf955932e--fc313ddfa83b4bd28a6e42ca64b9b89e f90becd2aaa746798ed4f425e49b2434 6956c9db0e3d40718deeb63a8e703fce Y 5bc6a6ba4d5d4ede9e76f564e1d20146--6956c9db0e3d40718deeb63a8e703fce 6956c9db0e3d40718deeb63a8e703fce--f90becd2aaa746798ed4f425e49b2434

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 2be990e6b48c4c50a3999d48bdf8b8e6 0 7a0179372a7a48e68cdb3c7ee3ed8d2f X 2be990e6b48c4c50a3999d48bdf8b8e6--7a0179372a7a48e68cdb3c7ee3ed8d2f c130849eadb44980acb12397f022978f 1 78ba4c4eb58c481c9d7879e4a62573bb Y 7a0179372a7a48e68cdb3c7ee3ed8d2f--78ba4c4eb58c481c9d7879e4a62573bb fc4a653fa175403d914f89feaf2d1cf5 78ba4c4eb58c481c9d7879e4a62573bb--fc4a653fa175403d914f89feaf2d1cf5 08e6071ac94c4b35b63822a61295e5ad a03f5adda5dd45bb8e2ec870924a45cf X c130849eadb44980acb12397f022978f--a03f5adda5dd45bb8e2ec870924a45cf fe68486641d04b008b650e9dc32ffe19 Y a03f5adda5dd45bb8e2ec870924a45cf--fe68486641d04b008b650e9dc32ffe19 fe68486641d04b008b650e9dc32ffe19--08e6071ac94c4b35b63822a61295e5ad

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 06b9e954881b435da9b0ebb39b3b3d4e 0 be8b95be8460446fbe076bc1e660c469 H 06b9e954881b435da9b0ebb39b3b3d4e--be8b95be8460446fbe076bc1e660c469 19ac6f585dfc4270adf78f2c143a81c6 1 95c9a3c85c784c7d9f63c3f32467e368 PHASE(1.571) be8b95be8460446fbe076bc1e660c469--95c9a3c85c784c7d9f63c3f32467e368 c6bd1190051c4f18a6775e57169e96ed PHASE(0.785) 95c9a3c85c784c7d9f63c3f32467e368--c6bd1190051c4f18a6775e57169e96ed e0b1675527104f869073e58420dd1fab 95c9a3c85c784c7d9f63c3f32467e368--e0b1675527104f869073e58420dd1fab 21e578d9b87048ca9bb3fa7d15093620 c6bd1190051c4f18a6775e57169e96ed--21e578d9b87048ca9bb3fa7d15093620 266eb36a692e4c1da18697c6bc28082b c6bd1190051c4f18a6775e57169e96ed--266eb36a692e4c1da18697c6bc28082b 8403b84f57154e8a9b7181c1e2bdf023 21e578d9b87048ca9bb3fa7d15093620--8403b84f57154e8a9b7181c1e2bdf023 9d4ec45b5a75498ba7ea2425397f5fae 8403b84f57154e8a9b7181c1e2bdf023--9d4ec45b5a75498ba7ea2425397f5fae cf6ac8b43020449aae8beec668912e3e 9d4ec45b5a75498ba7ea2425397f5fae--cf6ac8b43020449aae8beec668912e3e 23a3355cd16d47ac92dfcb8e9c4842bc 0fab32a07cfc471bb01166a58b317780 19ac6f585dfc4270adf78f2c143a81c6--0fab32a07cfc471bb01166a58b317780 76287017b9204c53a8e22251a46f648d 2 0fab32a07cfc471bb01166a58b317780--e0b1675527104f869073e58420dd1fab 4b6debec978f4ab6816ffb125e048da4 e0b1675527104f869073e58420dd1fab--4b6debec978f4ab6816ffb125e048da4 d47d997064264d769ee628fcf6eb8b37 H 4b6debec978f4ab6816ffb125e048da4--d47d997064264d769ee628fcf6eb8b37 0fd10ee781dd4b0f90e4b4bc9c6590c2 PHASE(1.571) d47d997064264d769ee628fcf6eb8b37--0fd10ee781dd4b0f90e4b4bc9c6590c2 e328b80c3d114c408aa42d479dfbf26f 0fd10ee781dd4b0f90e4b4bc9c6590c2--e328b80c3d114c408aa42d479dfbf26f 962aace09bcc496c944fc90f8e26cebd 0fd10ee781dd4b0f90e4b4bc9c6590c2--962aace09bcc496c944fc90f8e26cebd e328b80c3d114c408aa42d479dfbf26f--23a3355cd16d47ac92dfcb8e9c4842bc cadd97abd51b412ea575fbac7f95dc1b d8f4d38c5e5941288adda14f0c47d045 76287017b9204c53a8e22251a46f648d--d8f4d38c5e5941288adda14f0c47d045 d18f9faf697044d5a8f59bf6aee15410 d8f4d38c5e5941288adda14f0c47d045--d18f9faf697044d5a8f59bf6aee15410 d18f9faf697044d5a8f59bf6aee15410--266eb36a692e4c1da18697c6bc28082b b32c021b41b147a7b730d8ecd1a03240 266eb36a692e4c1da18697c6bc28082b--b32c021b41b147a7b730d8ecd1a03240 b32c021b41b147a7b730d8ecd1a03240--962aace09bcc496c944fc90f8e26cebd ad6071a54a554a8e9f7d31f543e29e34 H 962aace09bcc496c944fc90f8e26cebd--ad6071a54a554a8e9f7d31f543e29e34 ad6071a54a554a8e9f7d31f543e29e34--cadd97abd51b412ea575fbac7f95dc1b

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 9c6a7213e2f94bb99745f5d612b85d4e 0 bccf3b9127e9405fb44ff06d027c8413 9c6a7213e2f94bb99745f5d612b85d4e--bccf3b9127e9405fb44ff06d027c8413 226144c9aeac4e038b2e54cb40796cb6 1 1663a37821934b9385d6d8b5166f4773 bccf3b9127e9405fb44ff06d027c8413--1663a37821934b9385d6d8b5166f4773 3d422b6ae5d743d08c8f140c52e3e133 1663a37821934b9385d6d8b5166f4773--3d422b6ae5d743d08c8f140c52e3e133 76a2b60455354424b58599607027f4c6 PHASE(-0.785) 3d422b6ae5d743d08c8f140c52e3e133--76a2b60455354424b58599607027f4c6 605c21476f1e4569aabe09bb1afbaa31 PHASE(-1.571) 76a2b60455354424b58599607027f4c6--605c21476f1e4569aabe09bb1afbaa31 ddd2516dff694d2b8fe47507a72b4682 76a2b60455354424b58599607027f4c6--ddd2516dff694d2b8fe47507a72b4682 61e79dcaae544782a0f090d5bdd5111f H 605c21476f1e4569aabe09bb1afbaa31--61e79dcaae544782a0f090d5bdd5111f f35de9415b5c4b7c93546fda6bb6c3b4 605c21476f1e4569aabe09bb1afbaa31--f35de9415b5c4b7c93546fda6bb6c3b4 f359c51eba8b4d83956ac87fa52146bd 61e79dcaae544782a0f090d5bdd5111f--f359c51eba8b4d83956ac87fa52146bd 1c23f0eee64544f3a700a7e02e96dc42 e77828e71b454861b34a16036208d5ba 226144c9aeac4e038b2e54cb40796cb6--e77828e71b454861b34a16036208d5ba 3d094fcd3fba4f5e847c6f39c773bd75 2 59c06da6e0284633b29f28f2ebe60b5d PHASE(-1.571) e77828e71b454861b34a16036208d5ba--59c06da6e0284633b29f28f2ebe60b5d b59c6710a5bf4b50bfa763d84414675c H 59c06da6e0284633b29f28f2ebe60b5d--b59c6710a5bf4b50bfa763d84414675c 516b08e1e02e4bea902bd8ee85a41120 59c06da6e0284633b29f28f2ebe60b5d--516b08e1e02e4bea902bd8ee85a41120 b420ba3f0e81438191982bfa6a76a5d3 b59c6710a5bf4b50bfa763d84414675c--b420ba3f0e81438191982bfa6a76a5d3 b420ba3f0e81438191982bfa6a76a5d3--f35de9415b5c4b7c93546fda6bb6c3b4 7febd56f7d4c4ed79be530fc04e5a647 f35de9415b5c4b7c93546fda6bb6c3b4--7febd56f7d4c4ed79be530fc04e5a647 7febd56f7d4c4ed79be530fc04e5a647--1c23f0eee64544f3a700a7e02e96dc42 57e2ff2eb99c4badade98aa06fee848e 6e18c2c4951b48e7a1f86ed4da66deb9 H 3d094fcd3fba4f5e847c6f39c773bd75--6e18c2c4951b48e7a1f86ed4da66deb9 6e18c2c4951b48e7a1f86ed4da66deb9--516b08e1e02e4bea902bd8ee85a41120 a9757c5c62fc4918bd3983018cdf1f69 516b08e1e02e4bea902bd8ee85a41120--a9757c5c62fc4918bd3983018cdf1f69 a9757c5c62fc4918bd3983018cdf1f69--ddd2516dff694d2b8fe47507a72b4682 aff9ca6764154a46b3675588326fab46 ddd2516dff694d2b8fe47507a72b4682--aff9ca6764154a46b3675588326fab46 7a62cfb77d3a4b3899587455f260d733 aff9ca6764154a46b3675588326fab46--7a62cfb77d3a4b3899587455f260d733 7a62cfb77d3a4b3899587455f260d733--57e2ff2eb99c4badade98aa06fee848e

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_c1fb3d5f149543cc988ac429aa7cf133 8a16704cf51c45b1a0f97a7657bd500b 0 04c13fe7b2214971beceb2bedbaa28cf RX(0.0) 8a16704cf51c45b1a0f97a7657bd500b--04c13fe7b2214971beceb2bedbaa28cf 44ce05611ee24fbda070c2b199f2683c 1 eb1f352e497f4801b7899a880b40afd4 HamEvo 04c13fe7b2214971beceb2bedbaa28cf--eb1f352e497f4801b7899a880b40afd4 d18b4110e6984150858b663bc23b74fc RX(0.0) eb1f352e497f4801b7899a880b40afd4--d18b4110e6984150858b663bc23b74fc 9f0f4d3d9eeb474da01e284d8745272a d18b4110e6984150858b663bc23b74fc--9f0f4d3d9eeb474da01e284d8745272a ef37a5e7bae141cc829eccae5a53bb72 5aab262f9ea545938659525b95a88191 RX(1.571) 44ce05611ee24fbda070c2b199f2683c--5aab262f9ea545938659525b95a88191 047e8859d7e94f0ba6cda99b131e7743 2 081389595c7a4d41ab28dd031904b492 t = 1.000 5aab262f9ea545938659525b95a88191--081389595c7a4d41ab28dd031904b492 548458c121cf412bb20715ba35ce8c45 RX(1.571) 081389595c7a4d41ab28dd031904b492--548458c121cf412bb20715ba35ce8c45 548458c121cf412bb20715ba35ce8c45--ef37a5e7bae141cc829eccae5a53bb72 f3cd8c84b6c844d99b04f2ea9690c022 c237c6a924bb486c8ba14a44ab962ea9 RX(3.142) 047e8859d7e94f0ba6cda99b131e7743--c237c6a924bb486c8ba14a44ab962ea9 ca58063e120d452c830874379eea750b c237c6a924bb486c8ba14a44ab962ea9--ca58063e120d452c830874379eea750b de5349a1cf9c482197a00cf127778190 RX(3.142) ca58063e120d452c830874379eea750b--de5349a1cf9c482197a00cf127778190 de5349a1cf9c482197a00cf127778190--f3cd8c84b6c844d99b04f2ea9690c022

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({'00': 263, '01': 262, '11': 239, '10': 236})]
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.