Core functionality tutorial

This notebook will go over each primitive in the package and show their intended use.

import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
from hierarqcal import (
    Qhierarchy,
    Qcycle,
    Qpermute,
    Qpivot,
    Qmask,
    Qunmask,
    Qinit,
    Qmotif,
    Qmotifs,
    Qsplit,
    plot_motif,
    Qunitary,
    plot_circuit,
)

Specify backend

It is possible to use the package without a specific backend, but we choose one here for visualisation purposes. We’re mainly going to use the backends plotting functionality to display the circuits, so pick the one you have installed.

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)

Building blocks

Circuit architectures are created by stacking motifs hierarchacially, the lowest level motifs (primitives) are building blocks for higher level ones. On a high level you can regard a primitive as a layer capturing some design motif (cycle, mask, etc) and we’re creating a sequence of layers for the circuit. This view only captures hierarchical architectures of two levels (level 1 -> primitives, level 2 -> sequence of primitves). The framework is more general in that there is no limit to the number of levels used to represent an architecture. See the comparison below:

Layer view

  • architecture = (Cycle, Mask, Cycle, Mask, Cycle, Cycle, Cycle, Mask, Cycle, Mask)

Hierarchical view

  • m2_1 = (Cycle, Mask)

  • m2_2 = (Cycle, Cycle)

  • m3_1 = (m2_1, m2_1)

  • architecture = (m3_1, m2_2, m3_1)

Consider the figure above, to create the architecture on the right we want to **repeat the alternation between a cycle of stride 1 and masking from bottom to top three times**. The semantic description is simple and intuitive, the package is designed to allow building circuits this way. Notice a cycle of stride 1 and masking from bottom to top are both primitves. Alternating between them, is a level two motif (p1+p2), then repeating that 3 times is a level 3 motif. This gives a concise description of the circuit on the right. The circuit being a binary tree architecture (which is hierarchical).

There are two main classes to generate circuit architectures: Qhierarchy and Qmotif. We call our building blocks primitives, all are instances of Qmotif. A primitive is a directed graph with nodes representing qubits and edges unitaries applied between them. The edges are determined by its rules and the number of available qubits. Primitives are stacked with addition or mutliplication operations which act as append and extend, ex: Qcycle()+Qpermute() = (Qcycle,Qpermute) which is a Qmotifs object, a sequence of Qmotif. Simlarly Qcycle()*3 = (Qcycle(), Qcycle(), Qcycle) is again an instance of Qmotifs. Note it’s possible to add motifs together without specifying qubits, when you have a motif ready, it’s initialized with Qinit in the following way: Qinit(8)+motif. This creates a Qhierarchy object, which runs through all the motifs generates their edges.

The types of primitves are:

  • Qcycle: Create a cycle of unitaries, sometimes called ladders or convolutions. It’s hyperparamaters are used to generate cycles in directed graphs

  • Qpermute: unitaries permuted across qubits.

  • Qmask: Mask certain qubits based on a pattern, a mapping can be provided to save it’s information. Also reffered to as entanglers or pooling layers.

  • Qunmask: Unmask previously masked unitaries

  • Qinit: specify the qubit names, order and amount.

Qunitary

The edges of primitves encode the connectivity of the unitary, 1-qubit unitaries correspond to edges with arity 1 (1-ary tuples primitive.E=[(1,),(2),,…]). 2 qubit unitaries correspond to 2-ary tuples primitive.E=[(1,2),(3,4),…] and N-qubit unitaries to N-ary tuples ex: primitive.E=[(1,2,…,7),(3,4,…,8),…]. Qunitary tells a primitive the arity of it’s operation, each primitive has a mapping hyperparamater that expects a Qunitary object. The arguments for Qunitary are:

  • function: the function that get’s executed at every edge

  • n_symbols: the number of symbols the function expects

  • arity: the arity of the function (number of qubits it acts upon)

Notice that any function is permitted, there is no restriction to unitarity or even quantumness. In this sense the package is a dynamic datatrstructe to construct of hierarchichal compute graphs, but we work mainly in the context of quantum computing. This is also why the packages is framework agnostic, since no matter the framework, you will be able to create a function that does it’s circuit execution. HierarQcal manages the order in which these circuits get executed (well it generates the order, and number of executions.). For creating architectures, we only need to know the arity and number of symbols our function expects. This way, you can generate a circuit architecture without specifying the specific function corresponding to a unitary and framework. Once the architecture is ready, you can specify the functions and execute it on a backend by calling hierq(backend="qiskit") where hierq is the Qhierarchy object containing the architecture (or more generally, the compute graph).

For the examples below, we specify an arbitrary Qunitary object with no specific function connected to it:

u0_1 = Qunitary(function=None, n_symbols=0, arity=1)
u0_2 = Qunitary(function=None, n_symbols=0, arity=2)
u1_2 = Qunitary(function=None, n_symbols=1, arity=2)
u2_3 = Qunitary(function=None, n_symbols=2, arity=3)
u2_4 = Qunitary(function=None, n_symbols=2, arity=4)

Qinit primitive

Qinit is a special primitive with no operational effect. It initializes a set of qubit labels as reference for operations. Qinit is always provided at the start of a statement ex: Qinit(3)+motifs which causes a Qhierarchy object to be created. Qhierarchy manages the edge creation of motifs. Use Qinit is usually used in the final statement combining all motifs into an architecture or in statements that create sub hierarchies. Providing an integer $n$ labels the qubits $1, 2, \dots, n$, but providing a list with specific qubit order is also possible:

init_primitive = Qinit(10)
fig, ax = plot_motif(init_primitive)
../_images/356eafc6b56dc727a4fb973e364f839f4fc86216cd11f2113f7eae44df73e5f8.png
init_primitive = Qinit([2,6,4,10])
fig, ax = plot_motif(init_primitive)
../_images/f063506b85333c05fcda4582e14beefe73e6ba97e2dc913b0fbf02b464a5712a.png
init_primitive = Qinit(["q1", "q2"])
fig, ax = plot_motif(init_primitive)
../_images/0b97959a438bb9a8fd8b417856ad547ffc0cf2138f938281d3d964c17ec478a2.png

Cycle primitive

Qcycle is a primitve that creates a cycle or ladder or convolution of unitaries. It has four main hyperparameters:

  • stride: The number of nodes to skip, before connecting the edge

  • step: The number of nodes to step over after the edge was connected

  • offset: The node to start the procedure with

  • boundary: The boundary condition of the cycle, can be open or periodic The action is best seen through examples, but the general procedure for edge creation is staring at offset step in units of step at each step connect to stride units away.

# Convolution of stride 3, try out other stride values
cycle = Qcycle(stride=5, mapping=u1_2)
cycle_on_8_qubits = Qinit(8) + cycle

# Note the type of 'conv_on_8_qubits', it's a qcnn with two motifs: (Qfree, Qconv)
print(f"Type of object\t {type(cycle_on_8_qubits)}")
print(f"First motif\t {type(cycle_on_8_qubits.tail)}")
print(f"Second motif\t {type(cycle_on_8_qubits.tail.next)}")
Type of object	 <class 'hierarqcal.core.Qhierarchy'>
First motif	 <class 'hierarqcal.core.Qinit'>
Second motif	 <class 'hierarqcal.core.Qcycle'>
plot_motif(cycle_on_8_qubits[0])
plot_motif(cycle_on_8_qubits[1])
(<Figure size 400x400 with 1 Axes>, <AxesSubplot:>)
../_images/0355f53fb6af8443b97503a79f0cad9f319998d38b4bb8a5515e25c4335b7571.png ../_images/41694ce53ba8fffa4316f02a1f3f53caad9bb2809a022151cd19c7ce609b0973.png
# There is a minimal circuit plotter which converts the sequence of digraph into a two dimensional view.
# Notice no specific unitary needs to be specified, it's seen as a black box where we're only building the connectivity.
plot_circuit(cycle_on_8_qubits)
../_images/85fd9f146526cb6b2900e8b45eff7aa0b68a12dc364ac20a76428e7001d84239.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

Stride examples

cycle = Qcycle(stride=1, step=1,offset=0, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/72eacff884561cb138184655d0a329663a473e8c3a2e7f9b47dc7f703f4bf22e.png ../_images/a87f4bdf5aee954d0821d3245a9505497409c780ca7dea7b2d85a0838f10b056.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
cycle = Qcycle(stride=2, step=1,offset=0, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/0c7aab2ce478aab96057b02526f1728141872a276098da843ff0567f3e470010.png ../_images/06202980d8d5d8735bab19b22865734f4f1db6ea48d296f6956f0e87b6db3654.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
cycle = Qcycle(stride=3, step=1,offset=0, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/09dd8cc827052b6217d37eeffa481c57027c89d39457ed7eb969388cce856dfc.png ../_images/fd1cec4b67801e0a9486e8532a97ffc8e3b3f0c1aae0b129b52b5fa6c0d079ae.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

Step examples

cycle = Qcycle(stride=1, step=2,offset=0, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/dc1ce262ff543035e6c3414ab7635ca6aa513c3408686c5289d6d96a158905a2.png ../_images/4f0b0a2d9f1cae0698544e2217060c1983f6e1685a0ba55a35f1c2cee5fcbb79.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
cycle = Qcycle(stride=1, step=3,offset=0, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/b42a0a46c8f41762180d89081c8627e2cecb9f6e29a988c08d9fcc4f8dbfbcab.png ../_images/7109266347b645b331f042ed033d0be782bb7082bce38308993dc9377b75fba1.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
cycle = Qcycle(stride=1, step=4,offset=0, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/bdf82200084844f9a2b366bbe39dd52e32502f7bdeefbec33eec925402617b8a.png ../_images/03f441ce001b2f55baf02a1d40363a760f4f62a99bffc7efcaad7a49df21393b.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

Offset examples

cycle = Qcycle(stride=1, step=1,offset=3, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/6f6b55dcc363ed962939abb17fc4f4981a3f715122c1968b13d50823b140a75e.png ../_images/03b3f596f0ff66314896b445c29ef742afb9fbec9692c45fcb012b7b15e70c6e.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
cycle = Qcycle(stride=1, step=1,offset=7, mapping=u1_2)
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/9bc22c71d23d627464a04abd05cca1ecedc88e231842fca0f15b057ed91b802e.png ../_images/845203da8ea4eb77745832b3d88a86e5be8210f81c4520337c91f05bf255a799.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

Boundary examples

cycle = Qcycle(stride=1, step=1,offset=0, mapping=u1_2, boundary="open")
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/ab98576d4100ef02d6bf164fe7f2a0381f24f600d68da7b74d5583e6ef4542ca.png ../_images/d0e9437850998f7a8e0178ccfcf1af75f2a5f9804e58462d13ef9bd6bebafaae.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
cycle = Qcycle(stride=5, step=1,offset=0, mapping=u1_2, boundary="open")
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/76df75aef7c9ae02e69d30d373924680c0e207cabf9be4f9541d3572ffc35f43.png ../_images/410b7713f335c80c8f48711f8ba2eaa29ba1aa0c23dbf27b05330c90fa0aaa59.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
cycle = Qcycle(stride=5, step=1,offset=0, mapping=u1_2, boundary="periodic")
cycle = Qinit(8) + cycle
plot_motif(cycle[1])
plot_circuit(cycle)
../_images/41694ce53ba8fffa4316f02a1f3f53caad9bb2809a022151cd19c7ce609b0973.png ../_images/85fd9f146526cb6b2900e8b45eff7aa0b68a12dc364ac20a76428e7001d84239.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

N qubit unitaries

For N-qubit unitaries other than N=2, the cycle primitive becomes a hypergraph. stride, step and offset still has the same meaning but now the edges are N-ary tuples. For example a stride of 3 causes edges of the form (1,4,7) for a 3-qubit unitary. This means the function gets sent bits 1,4,7 in that order. Consider the same 3 qubit unitary defined with pennylane, qiskit or cirq

if backend == "qiskit":
    from qiskit.circuit import QuantumRegister
    def U3(bits, symbols=None, circuit=None, **kwargs):
        q0, q1, q2 = QuantumRegister(1, bits[0]), QuantumRegister(1, bits[1]), QuantumRegister(1, bits[2])
        circuit.crz(symbols[0], q0, q1)
        circuit.crx(symbols[1], q2, q1)
        return circuit

elif backend == "cirq":
    import cirq
    def U3(bits, symbols=None, circuit=None):
        q0, q1, q2 = cirq.LineQubit(bits[0]), cirq.LineQubit(bits[1]), cirq.LineQubit(bits[2])
        circuit += cirq.rz(symbols[0]).on(q1).controlled_by(q0)
        circuit += cirq.rx(symbols[1]).on(q1).controlled_by(q2)
        return circuit

elif backend == "pennylane":
    def U3(bits, symbols=None):
        qml.CRZ(symbols[0], wires=[bits[0], bits[1]])
        qml.CRX(symbols[1], wires=[bits[2], bits[1]])
u3 = Qunitary(U3, 2, 3)
cycle = Qcycle(stride=1, step=1,offset=0, mapping=u3, boundary="open")
cycle = Qinit(8) + cycle
plot_circuit(cycle)
circuit = get_circuit(cycle)
draw_circuit(circuit)
../_images/df402765a84fc18c0e0e04216ff02e1381e45194fc91ad7a387b55d6cfb2bcdf.png ../_images/5f59983b9c7042126858cf9370d601b71629d2d9ddf4bb27716a6ea0d0c75283.png
cycle = Qcycle(stride=2, step=1,offset=0, mapping=u3, boundary="open")
cycle = Qinit(8) + cycle
plot_circuit(cycle)
circuit = get_circuit(cycle)
draw_circuit(circuit)
../_images/0defb6d7f7fa7dd94c11581742550db79f8cd5fb81c29e248e585e6fad8a95d3.png ../_images/9effbdd155a40bc9b0af9c792ca849928fe1d67a23f611b86460e7d026b1c67b.png
cycle = Qcycle(stride=1, step=2,offset=1, mapping=u3, boundary="open")
cycle = Qinit(8) + cycle
plot_circuit(cycle)
circuit = get_circuit(cycle)
draw_circuit(circuit)
../_images/db827f52a278a050a9206bdd7e5c81b20ecec22d8ab8c0b890d29808fc00d2b6.png ../_images/ee8ff3652289eaf31ffc2fbbc3d59e884268ac89cd0b7f3975d8d2cc6bc2af3c.png

Qpermute primitive

Qpermute is a simple primitive that generates all combinations or permutations of edges based on the combinations hyperparameter and arity of corresponding function:

if self.combinations:
    E = list(it.combinations(Q, r=self.arity))
else:
    E = list(it.permutations(Q, r=self.arity))

Where E is the generated edges and Q the available qubits of the primitive

permute = Qpermute()
permute_5_qubits = Qinit(5) + permute
plot_motif(permute_5_qubits[1])
plot_circuit(permute_5_qubits)
../_images/ff790f6ae04042f2ad4c6563019f996ff430d6cf046e7c9bf72e381b01f2981c.png ../_images/cad802396ca493b1ac71c6e282bb62f2360f29f6ba75613e798c2d1d700ac999.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
permute = Qpermute(combinations=False)
permute_5_qubits = Qinit(5) + permute
plot_motif(permute_5_qubits[1])
plot_circuit(permute_5_qubits)
../_images/63aeae20a95a492364354fa725105b3323c7a24f6c7750b8c8aa37ab41932042.png ../_images/62aaed4d1e9a6848658651da3631b45917b29cf656a8b60cb5712bd270eb1dfa.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
permute = Qpermute(combinations=False, mapping=u2_3)
permute_5_qubits = Qinit(5) + permute
print(permute_5_qubits[1].E)
[(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 2), (1, 3, 4), (1, 3, 5), (1, 4, 2), (1, 4, 3), (1, 4, 5), (1, 5, 2), (1, 5, 3), (1, 5, 4), (2, 1, 3), (2, 1, 4), (2, 1, 5), (2, 3, 1), (2, 3, 4), (2, 3, 5), (2, 4, 1), (2, 4, 3), (2, 4, 5), (2, 5, 1), (2, 5, 3), (2, 5, 4), (3, 1, 2), (3, 1, 4), (3, 1, 5), (3, 2, 1), (3, 2, 4), (3, 2, 5), (3, 4, 1), (3, 4, 2), (3, 4, 5), (3, 5, 1), (3, 5, 2), (3, 5, 4), (4, 1, 2), (4, 1, 3), (4, 1, 5), (4, 2, 1), (4, 2, 3), (4, 2, 5), (4, 3, 1), (4, 3, 2), (4, 3, 5), (4, 5, 1), (4, 5, 2), (4, 5, 3), (5, 1, 2), (5, 1, 3), (5, 1, 4), (5, 2, 1), (5, 2, 3), (5, 2, 4), (5, 3, 1), (5, 3, 2), (5, 3, 4), (5, 4, 1), (5, 4, 2), (5, 4, 3)]

Qmask and Qunmask primitive

The Qmask primitive allows you to “mask” qubits according to a chosen pattern. Masking qubits in this context implies rendering certain qubits temporarily unavailable for subsequent operations, which facilitates specialized architectures and computations. You have the option to associate a unitary operation with the mask. The utility of this is that it “preserves” the information of the masked qubits, usually through a controlled unitary operation. This feature enables patterns of pooling in quantum neural networks, coarse graining, entanglers and “deferred measurement” where masked qubits are the controls of unmasked ones.

For 2-qubit unitaries there are predefined masking patterns such as “right”, “left”, “inside”, “outside”, “even”, “odd” which halves the circuit size. Alternatively, you can provide your own string such as “1*1” where ‘1’ signifies mask and “*” means to fill with 0’s based on the number of available qubits. ‘0’ signifies keeping the qubit active.

When specifying a unitary, there are three connection_type options (how to generate the edges):

  • “cycle”, simlar to the cycle primitive, generate edges with a stride, step and offset parameter

  • “nearest_circle” connect each masked qubit to the nearest available qubit in a circular topology

  • “nearest_tower” connect each masked qubit to the nearest available qubit in a tower topology

The Qunmask primitive allows you to “unmask” previously masked qubits, making them available for subsequent operations.

Examples will make this clearer:

Masking without a mapping

The small nodes are the masked ones

mask = Qinit(8) + Qmask("right")
plot_motif(mask[1])
plot_circuit(mask)
../_images/694097820f124d8b7492eeeee10a3e5fd92a8b70c9ba9106633468c5d0dbf969.png ../_images/c36a73306bf550f60759e941d480617b65405fd782f4e5b4cfc85ed81afd6865.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
mask = Qinit(8) + Qmask("01")
plot_motif(mask[1])
plot_circuit(mask)
../_images/28a93e085ae3eb2060857ce811425621a14be94e8f52a5dda8cee5f8df4a3ae4.png ../_images/54bdb88f2b687c083cddf7e7b507c604234eca16bec8e81c5e626b61628d8c98.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

This might be easier to see with a circuit and a mask between two cycles. We’ll use the following 2 qubit unitaries for the cycles:

mask = Qinit(8) + Qcycle(mapping=u0_2) + Qmask("01") + Qcycle(mapping=u0_2)
plot_circuit(mask)
../_images/78f201132e77436358dde78d02d7dea676d04eac1a6344b1d9ec0625f0dfe753.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
mask = Qinit(8) + Qcycle(mapping=u0_2) + Qmask("right") + Qcycle(mapping=u0_2)
plot_circuit(mask)
../_images/3e4fb11253c476fdbba895636caac8e8cf42bf1d6ef258d06bcf43bbcf50e304.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
mask = Qinit(8) + Qcycle(mapping=u0_2) + (Qmask("1*1") + Qcycle(mapping=u0_2))*3
plot_circuit(mask)
../_images/3ea6465308bd0645f3f62de0cfa9f5c12938c393d05f3429b626ead9888e28b2.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

Masking with a mapping

mask = Qinit(8) + Qmask("right", mapping=u0_2)
plot_motif(mask[1])
plot_circuit(mask)
../_images/87792a2305465a55ddba4690db07febc506421e12029e6fe9614da8dac716f07.png ../_images/35a401999f67475976e2be32c64a21f74b78d76d9a743e48d430ce4bf2d269c2.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
mask = Qinit(8) + Qmask("01", mapping=u0_2)
plot_motif(mask[1])
plot_circuit(mask)
../_images/ec5a82d6dbd4f96a22fe071efca56bb503bff5f131dd0a5cb8796d2d6c2bb6b9.png ../_images/f452bdb1aec5106a550af0c4573e9dd234a10a10f1d29b7120a9950898c2b3c0.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
mask = Qinit(8) + Qcycle(mapping=u0_2) + Qmask("*!", mapping=u0_2) + Qcycle(mapping=u0_2)
plot_circuit(mask)
../_images/b412a9aeb8846e78779de12f5fb5fe029799f396e919abb54c7305c39cf83a1d.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
mask = Qinit(8) + Qcycle(mapping=u0_2) + (Qmask("1*", mapping=u0_2) + Qcycle(mapping=u0_2))*6
plot_circuit(mask, depth=30)
../_images/c7a6a26f0218fe8b89986e12c92a5e9bcd6cffa3cc72ab2db4fbdcac33cd17a7.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

N-qubit unitaries

For N qubit unitaries, the pattern provided applies within the unitary. For example a pattern of “101” means for each edge of the form (a,b,c), a and c are going to be masked. Therefore the pattern string needs to be the same length as the arity for N>2 unitaries.

if backend == "qiskit":
    from qiskit.circuit import QuantumRegister
    def V3(bits, symbols=None, circuit=None, **kwargs):
        q0, q1, q2 = QuantumRegister(1, bits[0]), QuantumRegister(1, bits[1]), QuantumRegister(1, bits[2])
        circuit.cnot(q0, q1)
        circuit.cnot(q2, q1)
        return circuit

elif backend == "cirq":
    import cirq
    def V3(bits, symbols=None, circuit=None):
        q0, q1, q2 = cirq.LineQubit(bits[0]), cirq.LineQubit(bits[1]), cirq.LineQubit(bits[2])
        circuit += cirq.CNOT(q1, q0)
        circuit += cirq.CNOT(q1, q2)
        return circuit

elif backend == "pennylane":
    def V3(bits, symbols=None):
        qml.CNOT(wires=[bits[0], bits[1]])
        qml.CNOT(wires=[bits[2], bits[1]])
v3 = Qunitary(V3, 0, 3)
mask = Qinit(15) + Qmask(global_pattern="101",merge_within="101", strides=[1,0,0], steps=[2,1,1], offsets=[0,0,0], mapping=v3)
plot_circuit(mask)
circuit = get_circuit(mask)
draw_circuit(circuit)
../_images/132f24287b3e1feb31fcdbb566c60d3829913b499fef9d0f7060618b1bb3d030.png ../_images/a056c981b4d786d267af8e511fc7b189f716f22d858a2a35c89761fc282e4878.png

Qunmask example

hierq = Qinit(8) + (Qcycle(1, 1, 0, mapping=u0_2) + Qmask("*!"))*3 + (Qunmask("previous") + Qcycle(1, 1, 0, mapping=u0_2))*3
plot_circuit(hierq, depth=25)
../_images/fe6137c87ccc5d559c017d847303b28a0ded2cfbbb3f92d33e12c9fae3721790.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

Qpivot primitive

Qpivot is new, and this part of the tutorial will still get fleshed out. The pivot primitve connects the set of available qubits sequentially to a fixed set of qubits. The global pattern determines the pivot points while the merge pattern determines how the qubits are passed to the mapping. For detailed usage please see the grover and qft tutorials. Here are some quick examples:

hierq = Qinit(8) + 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=u1_2)
plot_motif(hierq[1])
plot_circuit(hierq)
../_images/b4b88e61c16af597285c82e1d071cc5b13f3e6f3d5492dd8c9d011a2f83e4418.png ../_images/6a21207af87539fb16dc2a85d279a8c0f4c66395a9aa4da65df389bbcccb0b88.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)

Classical and Quantum computation (Advanced usage + experimental features)

At the end of the day we are just building a compute graph in a hierarchichal manner with these primitves. For this reason we aren’t limited to quantum computation and can create classical algorithms also. This section will show how this can be achieved. We’ll also show how a naive quantum state vector simulation can be performed.

Classical computation (addition example)

Here is a completely classical computation for arbitrary integer addition. Notice we define two functions (half_adder and or_top) which changes a state variable directly (no matrix multiplication, just logic and overwriting, similar to classical circuits). The state is initialised with Qinit, then hierarqcal just executes these functions in the correct order. So you can create any function that changes the state as you desire.

def half_adder(bits, symbols=None, state=None):
    b1, b2 = state[bits[0]], state[bits[1]]
    xor = b1 ^ b2
    carry = b1 and b2
    state[bits[0]] = carry
    state[bits[1]] = xor
    return state


def or_top(bits, symbols=None, state=None):
    b1, b2 = state[bits[0]], state[bits[1]]
    state[bits[0]] = b1 or b2
    return state
x = [int(b) for b in bin(np.random.randint(0,512))[2:]]
y = [int(b) for b in bin(np.random.randint(0,512))[2:]]
bits = max(len(x),len(y))
n = 2 * bits
x = [0]*(bits - len(x)) + x
y = [0]*(bits - len(y)) + y
# program
full_adder = (
    Qinit(3)
    + Qcycle(mapping=Qunitary(half_adder, 0, 2), boundary="open")
    + Qpivot(global_pattern="1*", merge_within="11", mapping=Qunitary(or_top, 0, 2))
)
addition = (
    Qinit([i for i in range(n)], state=[elem for pair in zip(x, y) for elem in pair])
    + Qpivot("*1", "11", mapping=Qunitary(half_adder, 0, 2))
    + Qcycle(step=2, edge_order=[-1], mapping=full_adder, boundary="open")
)
plot_circuit(addition)
../_images/b61df78f4b01db8b3fbc924936f7416650d88dd47c65185a39044766de4bc9ed.png
(<Figure size 1000x600 with 1 Axes>, <AxesSubplot:>)
# Get the answer, we generate a bitstring and then turn it into a function that return only the array items with corresponding index 1
# Qsplit is a lower level primitve that Qmask and Qpivot inherhtis from, the bitstrings we generate for them also gets turned into functions via get_pattern_fn
# Don't run this twice! then you won't get the correct answer
b = addition()
pattern_fn = Qsplit.get_pattern_fn(
    None, pattern="1" + "01" * (int(n / 2) - 1) + "1", length=n
)
z = pattern_fn(b)
print(
    f"""
        {sum([x[i]*2**((len(x)-1)-i) for i in range(len(x))])}
        +
        {sum([y[i]*2**((len(y)-1)-i) for i in range(len(y))])}
        =
        {sum([z[i]*2**((len(z)-1)-i) for i in range(len(z))])}
    """
)
        350
        +
        82
        =
        432
    

Lower level control (create your own edge patterns) + custom Quantum simulation

Below we are show two things at the same time, first how you can specify individual edges to the Qmotif primitive. Depending on the arity, E is a list of arity-tuples which you can generate. For example E=[(0,2,4),(1,3,5)] means your Qunitary function takes 3 inputs (arity=3) and will apply the function to bits 0,2,4 and 1,3,5 seperatly, in the order that’s provided. This will works for 3rd party quantum packages (so you can do this with Qiskit, Cirq, or Pennylane). We’re combining this with a custom circuit simulation where we perform tensor contraction to calculate the final state vector. This way it’s possible to simulate qutrits, qudits or even more general tensor networks.

This is a quick demonstration, for details check out Qhierarchy.__call__ method in core.py and get_tensor_as_f in utils.py

The code below creates a Toffoli gate, we apply two bitflips on the first two qubits to see if the third will be flipped.

from hierarqcal import get_tensor_as_f
H_m = (1 / np.sqrt(2)) * np.array([[1, 1], [1, -1]])
X_m = np.array([[0, 1], [1, 0]])
CN_m = sp.Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
x0 = sp.symbols("x")
CP_m = sp.Matrix(
    [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, sp.exp(sp.I * x0)]]
)
H = Qunitary(get_tensor_as_f(H_m), 0, 1)
X = Qunitary(get_tensor_as_f(X_m), 0, 1)
CP = Qunitary(get_tensor_as_f(CP_m), 1, 2)
CN = Qunitary(get_tensor_as_f(CN_m), 0, 2)

tensors = [np.array([1, 0], dtype=np.complex256)] * 3
hierq = (
    Qinit([i for i in range(3)], tensors=tensors)
    + Qmotif(E=[(0,)], mapping=X)
    + Qmotif(E=[(1,)], mapping=X)
    + Qmotif(E=[(2,)], mapping=H)
    + Qmotif(E=[(0,2)], mapping=Qunitary(get_tensor_as_f(CP_m), symbols=[np.pi/2], arity=2))
    + Qmotif(E=[(1,2)], mapping=Qunitary(get_tensor_as_f(CP_m), symbols=[np.pi/2], arity=2))
    + Qmotif(E=[(0,1)], mapping=Qunitary(get_tensor_as_f(CN_m), arity=2))
    + Qmotif(E=[(1,2)], mapping=Qunitary(get_tensor_as_f(CP_m), symbols=[-np.pi/2], arity=2))
    + Qmotif(E=[(0,1)], mapping=Qunitary(get_tensor_as_f(CN_m), arity=2))
    + Qmotif(E=[(2,)], mapping=H)
)

plot_circuit(hierq)
print(hierq().flatten())
../_images/0e3c2278e06d6c8497b566ebdb4c73c181db62ec0523f95764a9b871b17563ae.png
[0.+0.000000e+00j 0.+0.000000e+00j 0.+0.000000e+00j 0.+0.000000e+00j
 0.+0.000000e+00j 0.+0.000000e+00j 0.+6.123234e-17j 1.-6.123234e-17j]