# Copyright (C) 2023 qBraid
#
# This file is part of the qBraid-SDK
#
# The qBraid-SDK is free software released under the GNU General Public License v3
# or later. You can redistribute and/or modify it under the terms of the GPL v3.
# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
"""
Module for transpiling quantum programs between different quantum programming languages
"""
import logging
import warnings
from copy import deepcopy
from typing import TYPE_CHECKING, Callable, List, Optional
from qbraid.programs import QPROGRAM_LIBS, get_program_type
from .exceptions import CircuitConversionError, ConversionPathNotFoundError, NodeNotFoundError
from .graph import ConversionGraph
if TYPE_CHECKING:
import cirq
import qbraid.programs
logger = logging.getLogger(__name__)
def _warn_if_unsupported(program_type, program_direction):
if program_type not in QPROGRAM_LIBS:
warnings.warn(
f"Converting {program_direction} unsupported program type '{program_type}'.",
UserWarning,
)
def _flatten_cirq(circuit: "cirq.Circuit") -> "cirq.Circuit":
"""
Flatten a Cirq circuit.
Args:
circuit (cirq.Circuit): The Cirq circuit to flatten.
Returns:
cirq.Circuit: The flattened Cirq circuit.
"""
# TODO: potentially replace with native cirq.decompose
# https://quantumai.google/reference/python/cirq/decompose
# pylint: disable=import-outside-toplevel
from cirq.contrib.qasm_import import circuit_from_qasm
return circuit_from_qasm(circuit.to_qasm())
def _get_path_from_bound_methods(bound_methods: List[Callable]) -> str:
"""
Constructs a path string from a list of bound methods of Conversion instances.
This function takes a list of bound methods (specifically 'convert' methods bound to
Conversion instances) and constructs a path string representing the sequence of
conversions. Each conversion is defined by the 'source' and 'target' properties of the
Conversion instance to which each method is bound.
Args:
bound_methods: A list of bound methods from Conversion instances.
Returns:
A string representing the path of conversions, formatted as
'source1 -> source2 -> ... -> targetN'.
Raises:
AttributeError: If the bound methods do not have the expected 'source'
and 'target' attributes.
IndexError: If the list of bound methods is empty.
"""
if not bound_methods:
raise IndexError("The list of bound methods is empty.")
path = []
for method in bound_methods:
instance = method.__self__ # Get the instance to which the method is bound
if not hasattr(instance, "source") or not hasattr(instance, "target"):
raise AttributeError("Bound method instance lacks 'source' or 'target' attributes.")
path.append(instance.source) # Add the source node of the instance
# Add the target of the last method's instance to complete the path
path.append(bound_methods[-1].__self__.target)
return " -> ".join(path)
[docs]
def transpile(
program: "qbraid.programs.QPROGRAM",
target: str,
conversion_graph: Optional[ConversionGraph] = None,
max_path_attempts: int = 3,
max_path_depth: Optional[int] = None,
) -> "qbraid.programs.QPROGRAM":
"""
Transpile a quantum program to a target language using a conversion graph.
This function attempts to find a conversion path from the program's current
format to the target format. It can limit the search to a certain number of
attempts and path depths.
Args:
program (qbraid.programs.QPROGRAM): The quantum program to transpile.
target (str): The target language to transpile to.
conversion_graph (Optional[ConversionGraph]): The graph representing available conversions.
If None, a default graph is used. Defaults to None.
max_path_attempts (int): The maximum number of conversion paths to attempt before raising an
exception. This is useful to avoid excessive computations when multiple paths are
available. Defaults to 3.
max_path_depth (Optional[int]): The maximum depth of conversions within a given path to
allow. For example, a path with a depth of 2 would be ['cirq' -> 'qasm2' -> 'qiskit'],
whereas a depth of 1 would be a direct conversion ['cirq' -> 'braket']. Defaults
to None, i.e. no limit set on the path depth.
Returns:
qbraid.programs.QPROGRAM: The transpiled quantum program.
Raises:
NodeNotFoundError: If the target or source package is not in the ConversionGraph.
ConversionPathNotFoundError: If no path is available to conversion between the
source and target packages.
CircuitConversionError: If the conversion fails through all attempted paths.
"""
graph = conversion_graph or ConversionGraph()
graph_type = "Default" if conversion_graph is None else "Provided"
if not graph.has_node(target):
raise NodeNotFoundError(graph_type, target, graph.nodes)
source = get_program_type(program, require_supported=conversion_graph is None)
if not graph.has_node(source):
raise NodeNotFoundError(graph_type, target, graph.nodes)
if not graph.has_path(source, target):
raise ConversionPathNotFoundError(source, target)
if source == target:
return program
_warn_if_unsupported(source, "from")
_warn_if_unsupported(target, "to")
paths = graph.find_top_shortest_conversion_paths(source, target, top_n=max_path_attempts)
if max_path_depth is not None:
paths = [path for path in paths if len(path) <= max_path_depth]
if len(paths) == 0:
raise ConversionPathNotFoundError(source, target, max_path_depth)
for path in paths:
temp_program = deepcopy(program)
try:
for convert_func in path:
try:
temp_program = convert_func(temp_program)
except Exception: # pylint: disable=broad-exception-caught
if get_program_type(temp_program) == "cirq":
temp_program = _flatten_cirq(temp_program)
temp_program = convert_func(temp_program) # Retry conversion
else:
raise
logger.info(
"\nSuccessfully transpiled using conversions: %s",
_get_path_from_bound_methods(path),
)
return temp_program
except Exception: # pylint: disable=broad-exception-caught
logger.info(
"\nFailed to transpile using conversions: %s",
_get_path_from_bound_methods(path),
)
continue
raise CircuitConversionError(f"Failed to convert program from '{source}' to '{target}'.")