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:
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.
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.
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.
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()
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.
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.
To quickly run block operations and access wavefunctions, samples or expectation values of observables, one can use the convenience functions run, sample and expectation.
fromqadenceimportkron,add,H,Z,run,sample,expectationn_qubits=2# Prepares a uniform stateh_block=kron(H(i)foriinrange(n_qubits))wf=run(h_block)xs=sample(h_block,n_shots=1000)obs=add(Z(i)foriinrange(n_qubits))ex=expectation(h_block,obs)
More fine-grained control and better performance is provided via the high-level QuantumModel abstraction. Quantum programs in Qadence are constructed in two steps:
Build a QuantumCircuit which ties together a composite block and a register.
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.