# Copyright (C) 2024 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, Optional
from qbraid_core._import import LazyLoader
from qbraid.programs import QPROGRAM_ALIASES, ProgramTypeError, get_program_type_alias
from .exceptions import CircuitConversionError, ConversionPathNotFoundError, NodeNotFoundError
from .graph import ConversionGraph
if TYPE_CHECKING:
import qbraid.programs
logger = logging.getLogger(__name__)
def _warn_if_unsupported(program_type, program_direction):
if program_type not in QPROGRAM_ALIASES:
warnings.warn(
f"Converting {program_direction} unsupported program type '{program_type}'.",
UserWarning,
)
def _format_exception(err: Exception) -> str:
return f"{type(err).__name__}: {str(err)}\n"
[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,
**kwargs,
) -> "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(**kwargs)
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_alias(program)
if not graph.has_node(source):
raise NodeNotFoundError(graph_type, source, 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)
error_messages = []
for path in paths:
path_details = ConversionGraph._get_path_from_bound_methods(path)
temp_program = deepcopy(program)
try:
for convert_func in path:
try:
temp_program = convert_func(temp_program)
except Exception as err: # pylint: disable=broad-exception-caught
try:
alias = get_program_type_alias(temp_program)
except ProgramTypeError:
alias = None
if alias == "cirq":
cirq_qasm_import = LazyLoader(
"cirq_qasm_import", globals(), "cirq.contrib.qasm_import"
)
temp_program = cirq_qasm_import.circuit_from_qasm(temp_program.to_qasm())
temp_program = convert_func(temp_program) # Retry conversion
else:
error_detail = (
f"Conversion {path_details} failed due to "
f"exception raised while converting from '{alias}'."
)
error_messages.append(error_detail)
error_messages.append(_format_exception(err))
raise
logger.info("\nSuccessfully transpiled using conversions: %s", path_details)
return temp_program
except Exception as err: # pylint: disable=broad-exception-caught
logger.info("\nFailed to transpile using conversions: %s", path_details)
formatted_error = _format_exception(err)
if len(error_messages) == 0 or error_messages[-1] != formatted_error:
error_messages.append(formatted_error)
continue
raise CircuitConversionError(
f"Failed to convert '{source}' to '{target}'"
+ (
" due to the following error(s):\n\n" + "\n".join(error_messages)
if error_messages
else "."
)
)