HierarQcal Quickstart

The idea of this notebook is to quickly showcase different kinds of circuits that can be built with the package. More in depth tutorials are given in other notebooks, this only to functions as a quick view of some functionality.

Overview

HierarQcal is a quantum circuit builder that simplifies circuit design, composition, generation, scaling, and parameter management. It provides an intuitive and dynamic data structure for constructing computation graphs hierarchically. This enables the generation of complex quantum circuit architectures, which is particularly useful for Neural Architecture Search (NAS), where an algorithm can determine the most efficient circuit architecture for a specific task and hardware. HierarQcal also facilitates the creation of hierarchical quantum circuits, such as those resembling tensor tree networks or MERA, with a single line of code. The package is open-source and framework-agnostic, it includes tutorials for Qiskit, PennyLane, and Cirq. Built to address the unique challenges of applying NAS to Quantum Computing, HierarQcal offers a novel approach to explore and optimize quantum circuit architectures.








A robot building itself with artificial intelligence, pencil drawing - generated with Dall E 2

import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
from hierarqcal import (
    Qhierarchy,
    Qcycle,
    Qpermute,
    Qmask,
    Qunmask,
    Qpivot,
    Qinit,
    Qmotif,
    Qmotifs,
    plot_motif,
    plot_circuit,
    Qunitary,
)
backend = "qiskit"
if backend == "qiskit":
    import qiskit
    from hierarqcal.qiskit.qiskit_circuits import V2, U2, V4

    def get_circuit(hierq):
        return hierq(backend="qiskit")

    def draw_circuit(circuit, **kwargs):
        return circuit.draw(output="mpl", **kwargs)

elif backend == "cirq":
    import cirq
    from cirq.contrib.svg import SVGCircuit
    from hierarqcal.cirq.cirq_circuits import V2, U2, V4
    import logging
    logging.getLogger('matplotlib.font_manager').disabled = True
    def get_circuit(hierq):
        return hierq(backend="cirq")

    def draw_circuit(circuit, **kwargs):
        return SVGCircuit(circuit, **kwargs)

elif backend == "pennylane":
    import pennylane as qml
    from hierarqcal.pennylane.pennylane_circuits import V2, U2, V4

    def get_circuit(hierq):
        dev = qml.device("default.qubit", wires=hierq.tail.Q)

        @qml.qnode(dev)
        def circuit():
            if isinstance(next(hierq.get_symbols(), False), sp.Symbol):
                # Pennylane doesn't support symbolic parameters, so if no symbols were set (i.e. they are still symbolic), we initialize them randomly
                hierq.set_symbols(np.random.uniform(0, 2 * np.pi, hierq.n_symbols))
            hierq(
                backend="pennylane"
            )  # This executes the compute graph in order
            return [qml.expval(qml.PauliZ(wire)) for wire in hierq.tail.Q]

        return circuit

    def draw_circuit(circuit, **kwargs):
        fig, ax = qml.draw_mpl(circuit)(**kwargs)
u2 = Qunitary(U2, 1, 2)
v2 = Qunitary(V2, 0, 2)
v4 = Qunitary(V4, 0, 4)

Binary trees

Here are some quick examples of combining cycles with masking patterns that forms binary trees. Qmask takes a global_pattern argument which is a bitstring where 1 specifies mask (ignore) and 0 specifies don’t mask. Wildcard characters ! and * can be used where ! fills with ones and * with zeros.

nq = 3
hierq = Qinit(2**nq) + (Qcycle(mapping=u2) + Qmask("*!", mapping=v2))*nq
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/795d397fe947e2659495192d294c5c78b957193b15c9a291fca35d065dd29643.png
nq = 3
hierq = Qinit(2**nq) + (Qcycle(mapping=u2) + Qmask("!*", mapping=v2))*nq
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/d8069c8e95016f8c6a94eec226602ff32dd925652573fdb62de7b2eea362370c.png
nq = 3
hierq = Qinit(2**nq) + (Qcycle(mapping=u2) + Qmask("!*!", mapping=v2))*nq
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/b9114a7033e15c94db0890b7bc00ea28a8a400b0bbbb06c85cffccb77b28d0df.png
nq = 3
hierq = Qinit(2**nq) + (Qcycle(mapping=u2) + Qmask("*!*", mapping=v2))*nq
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/120e5ed94bf12775d2971ff9d24ccfd44b9254d33a5b9d654ec24ac8357f4d0a.png

MERA from Grant et al.

The Multi-scale Entanglement Renormalization Ansatz (MERA) architecture from Hierarchical quantum classifiers, Grant et al.. We generate a N-qubit MERA circuit, change N to 4,8,16,32 to see how simple scaling the same circuit design is. Note that v4 is a 4-qubit unitary, which illustrates how primitves can handle N-qubit unitaries.

# If you're using qiskit you can create Qunitaries with strings
# u2 = Qunitary("crx(x)^01")
# v4 = Qunitary("cnot()^01;cnot()^32")
N=8
c1 = Qcycle(1,2,1,mapping=u2, boundary="open")
c2 = Qcycle(1,2,0,mapping=u2, boundary="open")
m1 = Qmask("1001", merge_within="1001", steps=[2,2,1], boundaries=["open","open","open"] ,mapping=v4)
hierq = Qinit(N) + c1 + (m1+c2)*int(np.log2(N))
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/1307f66b4a3cdda1ce52dfb9a3c7485e4d20a7479e08ebf14fb9d6dc3df66da9.png

Sub motif as a mapping for a motif

A cycle of cycles

m1 = Qinit(4) + (Qcycle(mapping=u2) + Qmask("right", mapping=v2)) * 2
circuit = get_circuit(m1)
draw_circuit(circuit)
../_images/9f68f84ce691cd58bd36520caba1f3ae23f51ec8f6526d82e44db6b539dc9089.png
hierq = Qinit(10) + Qcycle(1, 3, mapping=m1, boundary="open")
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/459deb0b4970a3f6d45ae34db1f8606a05c3c06412aef3eca1b24a8a76499ca5.png

A cycle of cycles of permutations

subsub = Qinit(3) + Qpermute(mapping=u2, share_weights=False, combinations=True)
circuit = get_circuit(subsub)
draw_circuit(circuit)
../_images/ba41c9cd23c1c1fd33410c44b38ffdc370e6383307ae66cacbf9332f61804368.png
sub = Qinit(11) + Qcycle(
    1, 2, 0, mapping=subsub, share_weights=True, boundary="open"
)
circuit = get_circuit(sub)
draw_circuit(circuit)
../_images/629411bdca8f483261fafbf5b26a2be52a707a96dc6f899daf823e47faf48120.png
hierq = Qinit(22) + Qcycle(1, 11, 0, mapping=sub, share_weights=False, boundary="open")
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/4f96e4fe734d6a5a2698bc32c90210ad670b5afe20c3d565ab0ac3c13ebb9766.png

Symbol management

m1 = Qinit(4) + (Qcycle(mapping=u2, share_weights=False) + Qmask("right", mapping=v2)) * 2
hierq = Qinit(10) + Qcycle(1, 3, mapping=m1, boundary="open", share_weights=False)
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/053687b7c5dd43731675b9469bd58ed06c2dd6d6dd49e5b61f31b20d8394417e.png
m1 = Qinit(4) + (Qcycle(mapping=u2, share_weights=False) + Qmask("right", mapping=v2)) * 2
hierq = Qinit(10) + Qcycle(1, 3, mapping=m1, boundary="open", share_weights=True)
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/f789d5c6fa2b3693b0a04e48fd898db8e143836c033842a00793afe018c720ca.png
m1 = Qinit(4) + (Qcycle(mapping=u2, share_weights=True) + Qmask("right", mapping=v2)) * 2
hierq = Qinit(10) + Qcycle(1, 3, mapping=m1, boundary="open", share_weights=False)
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/21237e29d994e8885b8b638e530a1ec31f30506ba71bfff2c83d26e98c722f2e.png
m1 = Qinit(4) + (Qcycle(mapping=u2, share_weights=True) + Qmask("right", mapping=v2)) * 2
hierq = Qinit(10) + Qcycle(1, 3, mapping=m1, boundary="open", share_weights=True)
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/459deb0b4970a3f6d45ae34db1f8606a05c3c06412aef3eca1b24a8a76499ca5.png

Specifying rotational angles

m1 = Qinit(4) + (Qcycle(mapping=u2, share_weights=True) + Qmask("right", mapping=v2)) * 2
hierq = Qinit(10) + Qcycle(1, 3, mapping=m1, boundary="open", share_weights=True)
hierq.set_symbols(np.random.uniform(0, 2 * np.pi, hierq.n_symbols))
list(hierq.get_symbols())
[3.568522990845462, 0.8256215855592418]
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/5f1afe69429fce74837a17757c75b3d9600e2ce350d77a0c474faf9d11af48ae.png

Symbols as a function

nq = 10
motif = Qcycle
# ne is the edge number, i.e. the first edge corresponds to the first unitary in a layer
# ns is the symbol number which you'll use for unitaries with multiple symbols
symbol_fn = lambda x, ns, ne: np.pi / ne
hierq = Qinit(nq) + motif(mapping=u2, share_weights=False, symbol_fn=symbol_fn) * 2
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/f38bbdbc87b466a77f71b5486f5f6ce773ef661c67146cca465996082bb07a83.png

Masking

hierq = Qinit(8) + (Qcycle(1, 1, 0, mapping=u2) + Qmask("*!"))*3 + (Qunmask("previous") + Qcycle(1, 1, 0, mapping=u2))*3
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/fbc60dde09ed01fa86c1be47f745ee9ec28f6a84624c814f725e569c9854a767.png
# The global pattern specifies the outside gets masked. 
# The merge between specifies that the masked qubits connect to the outside ones
hierq = Qinit(8) + Qmask(global_pattern="1**1", merge_between="1*1", mapping=u2)*4
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/27dc2cf95f622bc2451dd34cfa57c43fc7bc19e864db06e9393d67a798cabad7.png
# The global pattern specifies the outside gets masked. 
# The merge between specifies that the masked qubits connect to the inside ones
hierq = Qinit(7) + Qmask(global_pattern="1**1", merge_between="*1*", mapping=u2)*4
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/522332171e4cbb4a513f1c4ef29c3dd215dee7b771d590096c0d402bfe7ba292.png

Pivots

hierq = Qinit(N) + Qpivot(
    global_pattern="1*",
    merge_within="*1",
    merge_between=None,
    strides=[1, 1, 0],
    steps=[1, 1, 1],
    offsets=[0, 0, 0],
    boundaries=["open", "open", "periodic"],
    mapping=u2)
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/898f7d2ac92e2be0d87056fe7419636772d796c5cbff25bd9769b4ac08059ea9.png
hierq = Qinit(10) + Qpivot(
    global_pattern="1*1*",
    merge_within="*1",
    merge_between=None,
    strides=[1, 1, 0],
    steps=[1, 1, 1],
    offsets=[0, 0, 0],
    boundaries=["open", "open", "periodic"],
    mapping=u2)
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/5d0dea8252329b5103d7057e6082a8ad385716ec9322f0e39d440866594c09cf.png

Specifying unitaries

if backend == "qiskit":
    from qiskit.circuit import QuantumRegister
    def anz1(bits, symbols=None, circuit=None, **kwargs):
        
        # Assume bits are strings and in the correct QASM format
        q0, q1 = QuantumRegister(1, bits[0]), QuantumRegister(1, bits[1])
        circuit.crz(symbols[0], q0, q1)
        circuit.x(q0)
        circuit.crx(symbols[1], q0, q1)

        return circuit

    def anz2(bits, symbols=None, circuit=None, **kwargs):
        # Assume bits are strings and in the correct QASM format
        q0, q1 = QuantumRegister(1, bits[0]), QuantumRegister(1, bits[1])
        circuit.rz(symbols[0], q1)
        circuit.cnot(q1, q0)
        circuit.rz(symbols[1], q0)
        circuit.ry(symbols[2], q1)
        circuit.cnot(q0, q1)
        circuit.ry(symbols[3], q1)
        circuit.cnot(q1, q0)
        circuit.rz(symbols[1], q0)
        return circuit

elif backend == "cirq":
    import cirq
    # Masking circuit
    def anz1(bits, symbols=None, circuit=None):
        q0, q1 = cirq.LineQubit(bits[0]), cirq.LineQubit(bits[1])
        circuit += cirq.rz(symbols[0]).on(q1).controlled_by(q0)
        circuit += cirq.X(q0)
        circuit += cirq.rx(symbols[1]).on(q1).controlled_by(q0)

        return circuit

    # Cycle circuit
    def anz2(bits, symbols=None, circuit=None):
        q0, q1 = cirq.LineQubit(bits[0]), cirq.LineQubit(bits[1])
        circuit += cirq.rz(symbols[0]).on(q1)
        circuit += cirq.CNOT(q1, q0)
        circuit += cirq.rz(symbols[1]).on(q0)
        circuit += cirq.ry(symbols[2]).on(q1)
        circuit += cirq.CNOT(q0, q1)
        circuit += cirq.ry(symbols[3]).on(q1)
        circuit += cirq.CNOT(q1, q0)
        circuit += cirq.rz(symbols[4]).on(q0)
        return circuit

elif backend == "pennylane":
    # Masking circuit
    def anz1(bits, symbols=None):
        qml.CRZ(symbols[0], wires=[bits[0], bits[1]])
        qml.PauliX(wires=bits[0])
        qml.CRX(symbols[1], wires=[bits[0], bits[1]])

    # Cycle circuit
    def anz2(bits, symbols=None):
        qml.RZ(symbols[0], wires=bits[1])
        qml.CNOT(wires=[bits[1], bits[0]])
        qml.RZ(symbols[1], wires=bits[0])
        qml.RY(symbols[2], wires=bits[1])
        qml.CNOT(wires=[bits[0], bits[1]])
        qml.RY(symbols[3], wires=bits[1])
        qml.CNOT(wires=[bits[1], bits[0]])
        qml.RZ(symbols[4], wires=bits[0])


u = Qunitary(anz2, 5, 2)
v = Qunitary(anz1, 2, 2)
ansatz_1 = Qinit(2) + Qcycle(mapping=u)
circuit = get_circuit(ansatz_1)
draw_circuit(circuit)
../_images/7a00513606de562bbe80f78fe619853ff73b36d8f32a06d7addc4d248c05055b.png
ansatz_2 = Qinit(2) + Qmask("right", mapping=v)
circuit = get_circuit(ansatz_2)
draw_circuit(circuit)
../_images/25a23377d9c3bab7de96bf251272959611cb92af2e7e799fe1e8f6835c6cd0b0.png
hierq = Qinit(8) + (Qcycle(1, mapping=u) + Qmask(global_pattern="*!", mapping=v)) * 3
circuit = get_circuit(hierq)
draw_circuit(circuit)
../_images/f20bfa7f6fbd6e2b2586ab2a936c1c4fd8e1120850522bfc767ff8387c9c1675.png