<jupyter_start><jupyter_text>BQSKit Intermediate Representation (IR) TutorialThis tutorial will introduce a user to the core objects in the BQSKit package, as well as, how to manipulate them.  1. Unitary MatricesIn BQSKit, most objects are part of the unitary class hierarchy. The base class, `Unitary`, represents a map from zero or more real numbers to a unitary matrix. All `Unitary` objects have a `get_unitary` method which takes in the unitaries parameters and returns the concrete `UnitaryMatrix` object.In the next section, we will learn more about gates, but for now we will use two well known gates to learn about `Unitary` objects.<jupyter_code>from bqskit.ir.gates import CNOTGate, RZGate

cnot = CNOTGate() # The constant Controlled-Not gate
rz = RZGate() # Z-Rotation gate that takes one angle as a parameter<jupyter_output><empty_output><jupyter_text>In this first exercise, let's ensure these gates are part of the unitary class hierarchy.<jupyter_code>from bqskit.qis.unitary import Unitary

assert isinstance(cnot, Unitary)
assert isinstance(rz, Unitary)<jupyter_output><empty_output><jupyter_text>All `Unitary` objects contain a `get_unitary` method that takes zero or more parameters and returns a `UnitaryMatrix`. This is because the BQSKit IR treats everything as a function from a vector of real numbers to a unitary matrix. **Exercise:** In the below example, we calculate the matrix for both gates. Try changing the parameters, what happens?<jupyter_code>cnot_utry = cnot.get_unitary()
rz_utry = rz.get_unitary([0])

print("CNOT's Unitary")
print(cnot_utry)
print()

print("RZ's Unitary")
print(rz_utry)<jupyter_output>CNOT's Unitary
[[1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j]]

RZ's Unitary
[[1.-0.j 0.+0.j]
 [0.+0.j 1.+0.j]]<jupyter_text>We can ask the `Unitary` for the number of parameters it takes in its `get_unitary` method.<jupyter_code>assert cnot.num_params == 0
assert rz.num_params == 1

# The cnot gate is constant since it takes zero parameters
assert cnot.is_constant()

# The rz gate is not constant and hence parameterized since it takes one or more parameters
assert rz.is_parameterized()<jupyter_output><empty_output><jupyter_text>`Unitary` objects in BQSKit are always described in the context of a quantum system. As a result, we associate a `radixes` list with each object. This is a list of integers which describes the number of orthogonal states for each qudit in the system. For example, a n-qubit unitary would have a n-length list containing only `2`s. A qutrit unitary would have a list containing only `3`s, and hybrid systems are supported.<jupyter_code>from bqskit.qis.unitary import UnitaryMatrix

# Generate a random unitary sampled from the Haar random distribituion
utry = UnitaryMatrix.random(4)
print(f"Generated a {utry.num_qudits}-qubit unitary, radixes is: {utry.radixes}.")

# The random generation also takes an optional parameter `radixes`.
utry_qutrit = UnitaryMatrix.random(4, [3, 3, 3, 3])
print(f"Generated a {utry_qutrit.num_qudits}-qutrit unitary, radixes is: {utry_qutrit.radixes}.")

utry_mixed = UnitaryMatrix.random(3, [2, 3, 4])
print(f"Generated a {utry_mixed.num_qudits}-hybrid-qudit unitary, radixes is: {utry_mixed.radixes}.")<jupyter_output>Generated a 4-qubit unitary, radixes is: (2, 2, 2, 2).
Generated a 4-qutrit unitary, radixes is: (3, 3, 3, 3).
Generated a 3-hybrid-qudit unitary, radixes is: (2, 3, 4).<jupyter_text>In the last example, the `utry_mixed` unitary is associated with a 3-qudit system, where the first qudit is a qubit, the second is a qutrit, and last is a qudit with 4 bases.**UnitaryMatrix**`UnitaryMatrix` objects are concrete matrices that are also constant `Unitary` objects. They support most of the numpy api and for the most part can be used like normal numpy arrays. If you perform an operation that is closed for unitaries (conjugate, transpose, matmul, ...) the resulting object will remain a `UnitaryMatrix` object, otherwise it will downgrade to a `np.ndarray`.<jupyter_code>assert isinstance(utry, Unitary)

import numpy as np
utry1 = UnitaryMatrix.random(2)
utry2 = UnitaryMatrix.random(2)
utry3 = utry1 @ utry2
utry4 = utry3.otimes(UnitaryMatrix.identity(1))
print(utry4)
assert isinstance(utry4, UnitaryMatrix)<jupyter_output>[[ 0.61910261-0.35289046j  0.01140578-0.28967516j -0.54741901+0.16298858j
  -0.23276731+0.16650834j]
 [ 0.06846238-0.29882812j  0.74571794-0.3920657j   0.39658085+0.00585362j
   0.11796827-0.15804035j]
 [ 0.00707347+0.27645586j -0.03019624-0.28174741j -0.34429933+0.26552496j
   0.77206912-0.24102375j]
 [-0.42939949-0.37058487j  0.23498563+0.26577394j -0.54297414-0.1805272j
  -0.08304087-0.46703547j]]<jupyter_text>2. Circuits, Gates, and OperationsCircuits are 2d arrays of operations. An operation encapsulates a gate, a set of qubits, and the gate's parameters if any. BQSKit supports many gates, you can find them [here](https://bqskit.readthedocs.io/en/latest/source/ir.htmlbqskit-gates-bqskit-ir-gates). You will also find out how to implement them below if you would like to support your own gates.<jupyter_code>from bqskit.ir import Circuit

# When creating a Circuit object, we just need to pass it how many qudits it will be for
circuit = Circuit(2) # Two-qubit circuit

# We can also pass in a `radixes` object similar to before
circuit = Circuit(4, [3, 3, 3, 3]) # Four-qutrit circuit

# You can also load a circuit from a qasm file:
# circuit = Circuit.from_file("some_program.qasm")
from bqskit.ir.gates import HGate, CXGate

# Let's construct a simple circuit that prepares a Bell state
circuit = Circuit(2)
circuit.append_gate(HGate(), 0) # Add a Hadamard gate on the 0-th qudit.
circuit.append_gate(CXGate(), (0, 1)) # Add a cnot on qudits 0 and 1 (Control 0).
print(circuit.get_unitary()) # Calculate and print the unitary for the circuit
# We can also simulate a statevector:
print(circuit.get_statevector([1, 0, 0, 0]))
# This will return the output state when (1, 0, 0, 0) is given as the input state.
# We can iterate through all the operations in the circuit
for op in circuit:
    # This will print the entire operation
    # which includes the gate as well as
    # the qudits the gate is applied to
    # and the parameters if there are any
    print(op)<jupyter_output>HGate@(0,)
CNOTGate@(0, 1)<jupyter_text>BQSKit gates are simple objects that are designed to be simple to create, but also support a rich variety of features. Any gate that is created will need to specify a way to calculate its unitary through `get_unitary`. We also need to associate a quantum system with the gate, this specifies the `radixes` that the gate can handle. There are a number of gate base classes that handle implementing a lot of this for you. For example, the hadamard gate implementation is below:<jupyter_code>import math

from bqskit.ir.gates.constantgate import ConstantGate
from bqskit.ir.gates.qubitgate import QubitGate
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix


class HGate(ConstantGate, QubitGate):
    _num_qudits = 1
    _qasm_name = 'h'
    _utry = UnitaryMatrix(
        [
            [math.sqrt(2) / 2, math.sqrt(2) / 2],
            [math.sqrt(2) / 2, -math.sqrt(2) / 2],
        ],
    )<jupyter_output><empty_output><jupyter_text>Note, the gate subclasses both `ConstantGate` and `QubitGate`. A `ConstantGate` requires a `_utry` class or instance attribute and then will handle unitary calculations for you. The `QubitGate` requires a `_num_qudits` class or instance attribute and then will handle `radixes` calculations for you. There is also the `QutritGate`. Parameterized gates, gates that take one or more parameters to calculate their unitary e.g. X-rotation gate, require a little more. Look at the below example to see how a X-rotation gate is implemented:<jupyter_code>from typing import Sequence

import numpy as np

from bqskit.ir.gates.qubitgate import QubitGate
from bqskit.qis.unitary.differentiable import DifferentiableUnitary
from bqskit.qis.unitary.optimizable import LocallyOptimizableUnitary
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.qis.unitary.unitary import RealVector
from bqskit.utils.cachedclass import CachedClass


class RXGate(
    QubitGate,
    DifferentiableUnitary,
    LocallyOptimizableUnitary,
    CachedClass,
):
    _num_qudits = 1
    _num_params = 1
    _qasm_name = 'rx'

    def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
        """Return the unitary for this gate, see :class:`Unitary` for more."""
        self.check_parameters(params)

        cos = np.cos(params[0] / 2)
        sin = -1j * np.sin(params[0] / 2)

        return UnitaryMatrix(
            [
                [cos, sin],
                [sin, cos],
            ],
        )

    def get_grad(self, params: RealVector = []) -> np.ndarray:
        """
        Return the gradient for this gate.

        See :class:`DifferentiableUnitary` for more info.
        """
        self.check_parameters(params)

        dcos = -np.sin(params[0] / 2) / 2
        dsin = -1j * np.cos(params[0] / 2) / 2

        return np.array(
            [
                [
                    [dcos, dsin],
                    [dsin, dcos],
                ],
            ], dtype=np.complex128,
        )

    def optimize(self, env_matrix: np.ndarray) -> list[float]:
        """
        Return the optimal parameters with respect to an environment matrix.

        See :class:`LocallyOptimizableUnitary` for more info.
        """
        self.check_env_matrix(env_matrix)
        a = np.real(env_matrix[0, 0] + env_matrix[1, 1])
        b = np.imag(env_matrix[0, 1] + env_matrix[1, 0])
        theta = 2 * np.arccos(a / np.sqrt(a ** 2 + b ** 2))
        theta *= -1 if b < 0 else 1
        return [theta]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0945712836123795, 'fraction_numerical': 0.0443429731100964, 'mean_word_length': 3.8623581647755305, 'pattern_counts': {'":': 0, '<': 34, '<?xml version=': 0, '>': 36, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '555505', 'license': 'BSD-3-Clause-LBNL', 'n_tokens_mistral': 3568, 'n_tokens_neox': 3235, 'n_words': 1131, 'path': 'examples/tutorials/Introduction to BQSKit IR.ipynb', 'repo_name': 'jkalloor3/bqskit', 'size': '9855'}