Skip to content

matrix_expander

can1

can1(
    axis: AxisLike, angle: float, phase: float = 0
) -> NDArray[np.complex128]

Generates the unitary matrix for a canonical single-qubit gate.

Parameters:

Name Type Description Default
axis AxisLike

The rotation axis.

required
angle float

The rotation angle in radians.

required
phase float

The phase in radians.

0

Returns:

Type Description
NDArray[complex128]

The unitary \(2\times 2\) matrix.

Source code in opensquirrel/utils/matrix_expander.py
def can1(axis: AxisLike, angle: float, phase: float = 0) -> NDArray[np.complex128]:
    """Generates the unitary matrix for a canonical single-qubit gate.

    Args:
        axis (AxisLike): The rotation axis.
        angle (float): The rotation angle in radians.
        phase (float): The phase in radians.

    Returns:
        The unitary $2\\times 2$ matrix.

    """
    nx, ny, nz = Axis(axis)

    result = cmath.rect(1, phase) * (
        math.cos(angle / 2) * np.identity(2) - 1j * math.sin(angle / 2) * (nx * X + ny * Y + nz * Z)
    )
    return np.asarray(result, dtype=np.complex128)

can2

can2(canonical_axis: AxisLike) -> NDArray[np.complex128]

Generates the unitary matrix for a canonical two-qubit gate.

Parameters:

Name Type Description Default
canonical_axis AxisLike

The canonical axis.

required

Returns:

Type Description
NDArray[complex128]

The unitary \(4\times 4\) matrix.

Source code in opensquirrel/utils/matrix_expander.py
def can2(canonical_axis: AxisLike) -> NDArray[np.complex128]:
    """Generates the unitary matrix for a canonical two-qubit gate.

    Args:
        canonical_axis (AxisLike): The canonical axis.

    Returns:
        The unitary $4\\times 4$ matrix.

    """
    tx, ty, tz = CanonicalAxis(canonical_axis)

    return np.array(
        [
            [
                cmath.exp(-1j * (pi / 2) * tz) * math.cos((pi / 2) * (tx - ty)),
                0,
                0,
                -1j * cmath.exp(-1j * (pi / 2) * tz) * math.sin((pi / 2) * (tx - ty)),
            ],
            [
                0,
                cmath.exp(1j * (pi / 2) * tz) * math.cos((pi / 2) * (tx + ty)),
                -1j * cmath.exp(1j * (pi / 2) * tz) * math.sin((pi / 2) * (tx + ty)),
                0,
            ],
            [
                0,
                -1j * cmath.exp(1j * (pi / 2) * tz) * math.sin((pi / 2) * (tx + ty)),
                cmath.exp(1j * (pi / 2) * tz) * math.cos((pi / 2) * (tx + ty)),
                0,
            ],
            [
                -1j * cmath.exp(-1j * (pi / 2) * tz) * math.sin((pi / 2) * (tx - ty)),
                0,
                0,
                cmath.exp(-1j * (pi / 2) * tz) * math.cos((pi / 2) * (tx - ty)),
            ],
        ],
        dtype=np.complex128,
    )

expand_ket

expand_ket(
    base_ket: int,
    reduced_ket: int,
    qubits: Iterable[QubitLike],
) -> int

Given a base quantum ket on \(n\) qubits and a reduced ket on a subset of those qubits, this computes the expanded ket where the reduction qubits and the other qubits are set based on the reduced ket and the base ket, respectively.

Roughly equivalent to the PDEP assembly instruction (bits deposit).

Parameters:

Name Type Description Default
base_ket int

A quantum ket, represented by its corresponding non-negative integer. By convention, qubit #0 corresponds to the least significant bit.

required
reduced_ket int

A quantum ket, represented by its corresponding non-negative integer. By convention, qubit #0 corresponds to the least significant bit.

required
qubits Iterable[QubitLike]

The indices of the qubits to expand from the reduced ket. Order matters.

required

Returns:

Type Description
int

The non-negative integer corresponding to the expanded ket.

Example

expand_ket(0b00000, 0b0, [Qubit(5)]) # 0b000000 0 expand_ket(0b00000, 0b1, [Qubit(5)]) # 0b100000 32 expand_ket(0b00111, 0b0, [Qubit(5)]) # 0b000111 7 expand_ket(0b00111, 0b1, [Qubit(5)]) # 0b100111 39 expand_ket(0b0000, 0b000, [Qubit(1), Qubit(2), Qubit(3)]) # 0b0000 0 expand_ket(0b0000, 0b001, [Qubit(1), Qubit(2), Qubit(3)]) # 0b0010 2 expand_ket(0b0000, 0b011, [Qubit(1), Qubit(2), Qubit(3)]) # 0b0110 6 expand_ket(0b0000, 0b101, [Qubit(1), Qubit(2), Qubit(3)]) # 0b1010 10 expand_ket(0b0001, 0b101, [Qubit(1), Qubit(2), Qubit(3)]) # 0b1011 11

Source code in opensquirrel/utils/matrix_expander.py
def expand_ket(base_ket: int, reduced_ket: int, qubits: Iterable[QubitLike]) -> int:
    """Given a base quantum ket on $n$ qubits and a reduced ket on a subset of those qubits, this
    computes the expanded ket where the reduction qubits and the other qubits are set based on the
    reduced ket and the base ket, respectively.

    Roughly equivalent to the PDEP assembly instruction (bits deposit).

    Args:
        base_ket (int): A quantum ket, represented by its corresponding non-negative integer.
                  By convention, qubit #0 corresponds to the least significant bit.
        reduced_ket (int): A quantum ket, represented by its corresponding non-negative integer.
                     By convention, qubit #0 corresponds to the least significant bit.
        qubits (Iterable[QubitLike]): The indices of the qubits to expand from the reduced ket.
                                      Order matters.

    Returns:
        The non-negative integer corresponding to the expanded ket.

    Example:
        >>> expand_ket(0b00000, 0b0, [Qubit(5)])   # 0b000000
        0
        >>> expand_ket(0b00000, 0b1, [Qubit(5)])   # 0b100000
        32
        >>> expand_ket(0b00111, 0b0, [Qubit(5)])   # 0b000111
        7
        >>> expand_ket(0b00111, 0b1, [Qubit(5)])   # 0b100111
        39
        >>> expand_ket(0b0000, 0b000, [Qubit(1), Qubit(2), Qubit(3)])    # 0b0000
        0
        >>> expand_ket(0b0000, 0b001, [Qubit(1), Qubit(2), Qubit(3)])    # 0b0010
        2
        >>> expand_ket(0b0000, 0b011, [Qubit(1), Qubit(2), Qubit(3)])    # 0b0110
        6
        >>> expand_ket(0b0000, 0b101, [Qubit(1), Qubit(2), Qubit(3)])   # 0b1010
        10
        >>> expand_ket(0b0001, 0b101, [Qubit(1), Qubit(2), Qubit(3)])   # 0b1011
        11

    """
    expanded_ket = base_ket
    for i, qubit in enumerate(qubits):
        qubit = Qubit(qubit)
        expanded_ket &= ~(1 << qubit.index)  # erase bit
        expanded_ket |= ((reduced_ket & (1 << i)) >> i) << qubit.index  # set bit to value from reduced_ket

    return expanded_ket

get_matrix

get_matrix(
    gate: Gate, qubit_register_size: int
) -> NDArray[np.complex128]

Compute the unitary matrix corresponding to the gate applied to those qubit operands, taken among any number of qubits. This can be used for, e.g.,

  • testing,
  • permuting the operands of multi-qubit gates,
  • simulating a circuit (simulation in this way is inefficient for large numbers of qubits).

Parameters:

Name Type Description Default
gate Gate

The gate, including the qubits on which it is operated on.

required
qubit_register_size int

The size of the qubit register.

required

Returns:

Type Description
NDArray[complex128]

The unitary matrix corresponding to the gate applied to the qubit operands.

Example

X = lambda q: BlochSphereRotation(qubit=q, axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2) get_matrix(X(1), 2).astype(int) # X q[1] array([[0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 1, 0, 0]]) CNOT02 = ControlledGate(0, X(2)) get_matrix(CNOT02, 3).astype(int) # CNOT q[0], q[2] array([[1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 0, 0, 0, 0]]) get_matrix(ControlledGate(1, X(2)), 3).astype(int) # CNOT q[1], q[2] array([[1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0]])

Source code in opensquirrel/utils/matrix_expander.py
def get_matrix(gate: Gate, qubit_register_size: int) -> NDArray[np.complex128]:
    """Compute the unitary matrix corresponding to the gate applied to those qubit operands, taken
    among any number of qubits. This can be used for, e.g.,

    - testing,
    - permuting the operands of multi-qubit gates,
    - simulating a circuit (simulation in this way is inefficient for large numbers of qubits).

    Args:
        gate (Gate): The gate, including the qubits on which it is operated on.
        qubit_register_size (int): The size of the qubit register.

    Returns:
        The unitary matrix corresponding to the gate applied to the qubit operands.

    Example:
        >>> X = lambda q: BlochSphereRotation(qubit=q, axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2)
        >>> get_matrix(X(1), 2).astype(int)           # X q[1]
        array([[0, 0, 1, 0],
               [0, 0, 0, 1],
               [1, 0, 0, 0],
               [0, 1, 0, 0]])
        >>> CNOT02 = ControlledGate(0, X(2))
        >>> get_matrix(CNOT02, 3).astype(int)     # CNOT q[0], q[2]
        array([[1, 0, 0, 0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0, 1, 0, 0],
               [0, 0, 1, 0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0, 0, 0, 1],
               [0, 0, 0, 0, 1, 0, 0, 0],
               [0, 1, 0, 0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0, 0, 1, 0],
               [0, 0, 0, 1, 0, 0, 0, 0]])
        >>> get_matrix(ControlledGate(1, X(2)), 3).astype(int)     # CNOT q[1], q[2]
        array([[1, 0, 0, 0, 0, 0, 0, 0],
               [0, 1, 0, 0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0, 0, 1, 0],
               [0, 0, 0, 0, 0, 0, 0, 1],
               [0, 0, 0, 0, 1, 0, 0, 0],
               [0, 0, 0, 0, 0, 1, 0, 0],
               [0, 0, 1, 0, 0, 0, 0, 0],
               [0, 0, 0, 1, 0, 0, 0, 0]])

    """
    expander = _MatrixExpander(qubit_register_size)
    return np.asarray(gate.accept(expander), dtype=np.complex128)

get_reduced_ket

get_reduced_ket(
    ket: int, qubits: Iterable[QubitLike]
) -> int

Given a quantum ket represented by its corresponding base-10 integer, this computes the reduced ket where only the given qubits appear, in order.

Roughly equivalent to the PEXT assembly instruction (bits extraction).

Parameters:

Name Type Description Default
ket int

A quantum ket, represented by its corresponding non-negative integer. By convention, qubit #0 corresponds to the least significant bit.

required
qubits Iterable[QubitLike]

The indices of the qubits to extract. Order matters.

required

Returns:

Type Description
int

The non-negative integer corresponding to the reduced ket.

Example

get_reduced_ket(1, [Qubit(0)]) # 0b01 1 get_reduced_ket(1111, [Qubit(2)]) # 0b01 1 get_reduced_ket(1111, [Qubit(5)]) # 0b0 0 get_reduced_ket(1111, [Qubit(2), Qubit(5)]) # 0b01 1 get_reduced_ket(101, [Qubit(1), Qubit(0)]) # 0b10 2 get_reduced_ket(101, [Qubit(0), Qubit(1)]) # 0b01 1

Source code in opensquirrel/utils/matrix_expander.py
def get_reduced_ket(ket: int, qubits: Iterable[QubitLike]) -> int:
    """Given a quantum ket represented by its corresponding base-10 integer, this computes the
    reduced ket where only the given qubits appear, in order.

    Roughly equivalent to the PEXT assembly instruction (bits extraction).

    Args:
        ket (int): A quantum ket, represented by its corresponding non-negative integer.
             By convention, qubit #0 corresponds to the least significant bit.
        qubits (Iterable[QubitLike]): The indices of the qubits to extract. Order matters.

    Returns:
        The non-negative integer corresponding to the reduced ket.

    Example:
        >>> get_reduced_ket(1, [Qubit(0)])         # 0b01
        1
        >>> get_reduced_ket(1111, [Qubit(2)])      # 0b01
        1
        >>> get_reduced_ket(1111, [Qubit(5)])      # 0b0
        0
        >>> get_reduced_ket(1111, [Qubit(2), Qubit(5)])   # 0b01
        1
        >>> get_reduced_ket(101, [Qubit(1), Qubit(0)])    # 0b10
        2
        >>> get_reduced_ket(101, [Qubit(0), Qubit(1)])    # 0b01
        1

    """
    reduced_ket = 0
    for i, qubit in enumerate(qubits):
        qubit = Qubit(qubit)
        reduced_ket |= ((ket & (1 << qubit.index)) >> qubit.index) << i

    return reduced_ket