Module hybridq.extras.simulation.otoc

Author: Salvatore Mandra (salvatore.mandra@nasa.gov)

Copyright © 2021, United States Government, as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.

The HybridQ: A Hybrid Simulator for Quantum Circuits platform is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Expand source code
"""
Author: Salvatore Mandra (salvatore.mandra@nasa.gov)

Copyright © 2021, United States Government, as represented by the Administrator
of the National Aeronautics and Space Administration. All rights reserved.

The HybridQ: A Hybrid Simulator for Quantum Circuits platform is licensed under
the Apache License, Version 2.0 (the "License"); you may not use this file
except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""

from __future__ import annotations
from hybridq.gate import Gate
from hybridq.circuit.circuit import Circuit
from typing import List, Tuple, Callable, Dict
from hybridq.utils import sort, argsort

# Define Qubit type
Qubit = any

# Define Coupling
Coupling = Tuple[Qubit, Qubit]

# Define Layout
Layout = List[Qubit]


def generate_U(layout: dict[any, list[Coupling]],
               qubits_order: list[Qubit],
               depth: int,
               sequence: list[any],
               one_qb_gates: iter[Gate],
               two_qb_gates: iter[Gate],
               exclude_qubits: iter[Qubit] = None) -> Circuit:
    """
    Generate U at a given depth.
    """

    # Initialize circuit
    circ = Circuit()

    # Get qubits to exclude
    exclude_qubits = set() if exclude_qubits is None else set(exclude_qubits)

    # Remove qubits to exclude from qubits_order
    qubits_order = [q for q in qubits_order if q not in exclude_qubits]

    # Gate index for single-qubit gate
    index = 0

    for d in range(depth):

        # Get sequence
        seq = sequence[d % len(sequence)]

        # Get right layout
        layer = layout[seq]

        # Get tag
        tags = {'depth': d, 'sequence': seq}

        # Add single qubit gates
        circ += [
            next(one_qb_gates).on([q]).set_tags({
                **tags, 'index': index + i
            }) for i, q in enumerate(qubits_order)
        ]

        # Add two qubit gates
        circ += [
            next(two_qb_gates).on(q).set_tags(tags)
            for q in layer
            if not exclude_qubits.intersection(q)
        ]

        # Increment index
        index += len(qubits_order)

    return circ


def generate_OTOC(layout: dict[any, list[Coupling]],
                  depth: int,
                  sequence: list[any],
                  one_qb_gates: iter[Gate],
                  two_qb_gates: iter[Gate],
                  butterfly_op: str,
                  ancilla: Qubit,
                  targets: list[Qubit],
                  qubits_order: list[Qubit] = None) -> Circuit:

    # Get all qubits
    all_qubits = {
        q for s in sequence[:min(depth, len(sequence))] for gate in layout[s]
        for q in gate
    }

    # Get order of qubits
    qubits_order = sort(all_qubits) if qubits_order is None else qubits_order

    # Get list if single butterfly is provided
    butterfly_op = list(butterfly_op)

    # Check order of qubits
    if sort(all_qubits) != sort(qubits_order):
        raise ValueError(
            "'qubits_order' must be a valid permutation of all qubits.")

    # Check if butterfly op has valid strings
    if set(butterfly_op).difference(['I', 'X', 'Y', 'Z']):
        raise ValueError('Only {I, X, Y, Z} are valid butterfly operators')

    # Check if ancilla/targets are in layout
    if set(targets).union([ancilla]).difference(all_qubits):
        raise ValueError(f"Ancilla/Targets must be in layout.")

    # Check if targets are unique
    if len(set(targets)) != len(targets):
        raise ValueError('Targets must be unique.')

    # Check that ancilla is not in targets
    if ancilla in targets:
        raise ValueError('Ancilla must be different from targets')

    # Check if the number of targets corresponds to the number of butterfly ops
    if len(targets) != len(butterfly_op) + 1:
        raise ValueError(
            f"Number of butterfly operators does not match number "
            f"of targets (expected {len(targets)-1}, got {len(butterfly_op)}).")

    # Check that there is a coupling between the ancilla qubit and the measurement qubit
    if next((False for s in sequence[:min(depth, len(sequence))]
             for w in layout[s] if sort(w) == sort([ancilla, targets[0]])),
            True):
        raise ValueError(
            f"No available two-qubit gate between ancilla {ancilla} "
            f"and qubit {targets[0]}.")

    # Initialize Circuit
    circ = Circuit()

    # Add initial layer of single qubit gates
    circ.extend([
        Gate('SQRT_Y' if q != ancilla else 'SQRT_X',
             qubits=[q],
             tags={
                 'depth': 0,
                 'sequence': 'initial'
             }) for q in sort(all_qubits)
    ])

    # Add CZ between ancilla and first target qubit
    circ.append(
        Gate('CZ', [ancilla, targets[0]],
             tags={
                 'depth': 0,
                 'sequence': 'first_control'
             }))

    # Generate U
    U = generate_U(layout=layout,
                   qubits_order=qubits_order,
                   depth=depth,
                   sequence=sequence,
                   one_qb_gates=one_qb_gates,
                   two_qb_gates=two_qb_gates,
                   exclude_qubits=[ancilla]).update_all_tags({'U': True})

    # Add U to circuit
    circ += U

    # Add butterfly operator
    circ.extend([
        Gate(_b,
             qubits=[_t],
             tags={
                 'depth': depth - 1,
                 'sequence': 'butterfly'
             }) for _b, _t in zip(butterfly_op, targets[1:])
    ])

    # Add U* to circuit and update depth
    circ += Circuit(
        gate.update_tags({
            'depth': 2 * depth - gate.tags['depth'] - 1,
            'U^-1': True
        }) for gate in U.inv().remove_all_tags(['U']))

    # Add CZ between ancilla and first target qubit
    circ.append(
        Gate('CZ', [ancilla, targets[0]],
             tags={
                 'depth': 2 * depth - 1,
                 'sequence': 'second_control'
             }))

    return circ

Functions

def generate_OTOC(layout: dict[any, list[Coupling]], depth: int, sequence: list[any], one_qb_gates: iter[Gate], two_qb_gates: iter[Gate], butterfly_op: str, ancilla: Qubit, targets: list[Qubit], qubits_order: list[Qubit] = None) ‑> Circuit
Expand source code
def generate_OTOC(layout: dict[any, list[Coupling]],
                  depth: int,
                  sequence: list[any],
                  one_qb_gates: iter[Gate],
                  two_qb_gates: iter[Gate],
                  butterfly_op: str,
                  ancilla: Qubit,
                  targets: list[Qubit],
                  qubits_order: list[Qubit] = None) -> Circuit:

    # Get all qubits
    all_qubits = {
        q for s in sequence[:min(depth, len(sequence))] for gate in layout[s]
        for q in gate
    }

    # Get order of qubits
    qubits_order = sort(all_qubits) if qubits_order is None else qubits_order

    # Get list if single butterfly is provided
    butterfly_op = list(butterfly_op)

    # Check order of qubits
    if sort(all_qubits) != sort(qubits_order):
        raise ValueError(
            "'qubits_order' must be a valid permutation of all qubits.")

    # Check if butterfly op has valid strings
    if set(butterfly_op).difference(['I', 'X', 'Y', 'Z']):
        raise ValueError('Only {I, X, Y, Z} are valid butterfly operators')

    # Check if ancilla/targets are in layout
    if set(targets).union([ancilla]).difference(all_qubits):
        raise ValueError(f"Ancilla/Targets must be in layout.")

    # Check if targets are unique
    if len(set(targets)) != len(targets):
        raise ValueError('Targets must be unique.')

    # Check that ancilla is not in targets
    if ancilla in targets:
        raise ValueError('Ancilla must be different from targets')

    # Check if the number of targets corresponds to the number of butterfly ops
    if len(targets) != len(butterfly_op) + 1:
        raise ValueError(
            f"Number of butterfly operators does not match number "
            f"of targets (expected {len(targets)-1}, got {len(butterfly_op)}).")

    # Check that there is a coupling between the ancilla qubit and the measurement qubit
    if next((False for s in sequence[:min(depth, len(sequence))]
             for w in layout[s] if sort(w) == sort([ancilla, targets[0]])),
            True):
        raise ValueError(
            f"No available two-qubit gate between ancilla {ancilla} "
            f"and qubit {targets[0]}.")

    # Initialize Circuit
    circ = Circuit()

    # Add initial layer of single qubit gates
    circ.extend([
        Gate('SQRT_Y' if q != ancilla else 'SQRT_X',
             qubits=[q],
             tags={
                 'depth': 0,
                 'sequence': 'initial'
             }) for q in sort(all_qubits)
    ])

    # Add CZ between ancilla and first target qubit
    circ.append(
        Gate('CZ', [ancilla, targets[0]],
             tags={
                 'depth': 0,
                 'sequence': 'first_control'
             }))

    # Generate U
    U = generate_U(layout=layout,
                   qubits_order=qubits_order,
                   depth=depth,
                   sequence=sequence,
                   one_qb_gates=one_qb_gates,
                   two_qb_gates=two_qb_gates,
                   exclude_qubits=[ancilla]).update_all_tags({'U': True})

    # Add U to circuit
    circ += U

    # Add butterfly operator
    circ.extend([
        Gate(_b,
             qubits=[_t],
             tags={
                 'depth': depth - 1,
                 'sequence': 'butterfly'
             }) for _b, _t in zip(butterfly_op, targets[1:])
    ])

    # Add U* to circuit and update depth
    circ += Circuit(
        gate.update_tags({
            'depth': 2 * depth - gate.tags['depth'] - 1,
            'U^-1': True
        }) for gate in U.inv().remove_all_tags(['U']))

    # Add CZ between ancilla and first target qubit
    circ.append(
        Gate('CZ', [ancilla, targets[0]],
             tags={
                 'depth': 2 * depth - 1,
                 'sequence': 'second_control'
             }))

    return circ
def generate_U(layout: dict[any, list[Coupling]], qubits_order: list[Qubit], depth: int, sequence: list[any], one_qb_gates: iter[Gate], two_qb_gates: iter[Gate], exclude_qubits: iter[Qubit] = None) ‑> Circuit

Generate U at a given depth.

Expand source code
def generate_U(layout: dict[any, list[Coupling]],
               qubits_order: list[Qubit],
               depth: int,
               sequence: list[any],
               one_qb_gates: iter[Gate],
               two_qb_gates: iter[Gate],
               exclude_qubits: iter[Qubit] = None) -> Circuit:
    """
    Generate U at a given depth.
    """

    # Initialize circuit
    circ = Circuit()

    # Get qubits to exclude
    exclude_qubits = set() if exclude_qubits is None else set(exclude_qubits)

    # Remove qubits to exclude from qubits_order
    qubits_order = [q for q in qubits_order if q not in exclude_qubits]

    # Gate index for single-qubit gate
    index = 0

    for d in range(depth):

        # Get sequence
        seq = sequence[d % len(sequence)]

        # Get right layout
        layer = layout[seq]

        # Get tag
        tags = {'depth': d, 'sequence': seq}

        # Add single qubit gates
        circ += [
            next(one_qb_gates).on([q]).set_tags({
                **tags, 'index': index + i
            }) for i, q in enumerate(qubits_order)
        ]

        # Add two qubit gates
        circ += [
            next(two_qb_gates).on(q).set_tags(tags)
            for q in layer
            if not exclude_qubits.intersection(q)
        ]

        # Increment index
        index += len(qubits_order)

    return circ