# Needed to defer evaluating type hints so that we don't need forward
# references and can hide type hint–only imports from runtime usage.
from __future__ import annotations
from base64 import b64decode
from enum import Enum, auto
from operator import mod
import os
from tempfile import NamedTemporaryFile
from typing import Union, overload, TYPE_CHECKING
if TYPE_CHECKING:
from typing_extensions import Literal
try:
import pyqir.generator as pqg
except ImportError as ex:
raise ImportError("qutip.qip.qir depends on PyQIR") from ex
try:
import pyqir.parser as pqp
except ImportError as ex:
raise ImportError("qutip.qip.qir depends on PyQIR") from ex
from qutip_qip.circuit import QubitCircuit
from qutip_qip.operations import Gate, Measurement
__all__ = ["circuit_to_qir", "QirFormat"]
# Specify return types for each different format, so that IDE tooling and type
# checkers can resolve the return type based on arguments.
@overload
def circuit_to_qir(
circuit: QubitCircuit,
format: Union[Literal[QirFormat.BITCODE], Literal["bitcode"]],
module_name: str,
) -> bytes: ...
@overload
def circuit_to_qir(
circuit: QubitCircuit,
format: Union[Literal[QirFormat.TEXT], Literal["text"]],
module_name: str,
) -> str: ...
@overload
def circuit_to_qir(
circuit: QubitCircuit,
format: Union[Literal[QirFormat.MODULE], Literal["module"]],
module_name: str,
) -> pqp.QirModule: ...
[docs]def circuit_to_qir(circuit, format, module_name="qutip_circuit"):
"""Converts a qubit circuit to its representation in QIR.
Given a circuit acting on qubits, generates a representation of that
circuit using Quantum Intermediate Representation (QIR).
Parameters
----------
circuit
The circuit to be translated to QIR.
format
The QIR serialization to be used. If `"text"`, returns a
plain-text representation using LLVM IR. If `"bitcode"`, returns a
dense binary representation ideal for use with other compilation tools.
If `"module"`, returns a PyQIR module object that can be manipulated
further before generating QIR.
module_name
The name of the module to be emitted.
Returns
-------
A QIR representation of `circuit`, encoded using the format specified by
`format`.
"""
# Define as an inner function to make it easier to call from conditional
# branches.
def append_operation(
module: pqg.SimpleModule, builder: pqg.BasicQisBuilder, op: Gate
):
if op.classical_controls:
result = op.classical_controls[0]
value = "zero" if op.classical_control_value == 0 else "one"
# Pull off the first control and recurse.
op_with_less_controls = Gate(**op.__dict__)
op_with_less_controls.classical_controls = (
op_with_less_controls.classical_controls[1:]
)
op_with_less_controls.classical_control_value = (
op_with_less_controls.classical_control_value
if isinstance(
op_with_less_controls.classical_control_value, int
)
else (
(op_with_less_controls.classical_control_value[1:])
if op_with_less_controls.classical_control_value
is not None
else None
)
)
branch_body = {
value: (
lambda: append_operation(
module, builder, op_with_less_controls
)
)
}
builder.if_result(module.results[result], **branch_body)
return
if op.controls:
if op.name not in ("CNOT", "CX", "CZ") or len(op.controls) != 1:
raise NotImplementedError(
"Arbitrary controlled quantum operations are not yet supported."
)
if op.name == "X":
builder.x(module.qubits[op.targets[0]])
elif op.name == "Y":
builder.y(module.qubits[op.targets[0]])
elif op.name == "Z":
builder.z(module.qubits[op.targets[0]])
elif op.name == "S":
builder.s(module.qubits[op.targets[0]])
elif op.name == "T":
builder.t(module.qubits[op.targets[0]])
elif op.name == "SNOT":
builder.h(module.qubits[op.targets[0]])
elif op.name in ("CNOT", "CX"):
builder.cx(
module.qubits[op.controls[0]], module.qubits[op.targets[0]]
)
elif op.name == "CZ":
builder.cz(
module.qubits[op.controls[0]], module.qubits[op.targets[0]]
)
elif op.name == "RX":
builder.rx(op.arg_value, module.qubits[op.targets[0]])
elif op.name == "RY":
builder.ry(op.arg_value, module.qubits[op.targets[0]])
elif op.name == "RZ":
builder.rz(op.arg_value, module.qubits[op.targets[0]])
elif op.name in ("CRZ", "TOFFOLI"):
raise NotImplementedError(
"Decomposition of CRZ and Toffoli gates into base "
+ "profile instructions is not yet implemented."
)
else:
raise ValueError(
f"Gate {op.name} not supported by the basic QIR builder, "
+ "and may require a custom declaration."
)
fmt = QirFormat.ensure(format)
module = pqg.SimpleModule(module_name, circuit.N, circuit.num_cbits or 0)
builder = pqg.BasicQisBuilder(module.builder)
for op in circuit.gates:
# If we have a QuTiP gate, then we need to convert it into one of
# the reserved operation names in the QIR base profile's quantum
# instruction set (QIS).
if isinstance(op, Gate):
# TODO: Validate indices.
append_operation(module, builder, op)
elif isinstance(op, Measurement):
builder.mz(
module.qubits[op.targets[0]],
module.results[op.classical_store],
)
else:
raise NotImplementedError(
f"Instruction {op} is not implemented in the QIR base "
+ "profile and may require a custom declaration."
)
if fmt == QirFormat.TEXT:
return module.ir()
elif fmt == QirFormat.BITCODE:
return module.bitcode()
elif fmt == QirFormat.MODULE:
bitcode = module.bitcode()
f = NamedTemporaryFile(suffix=".bc", delete=False)
try:
f.write(bitcode)
finally:
f.close()
module = pqp.QirModule(f.name)
try:
os.unlink(f.name)
except:
pass
return module
else:
assert (
False
), "Internal error; should have caught invalid format enum earlier."