Skip to content

Commit

Permalink
Merge pull request #125 from cgranade/cgranade/qir-export
Browse files Browse the repository at this point in the history
Add draft exporter from QubitCircuit to QIR.
  • Loading branch information
BoxiLi committed Oct 7, 2022
2 parents bf3f7c1 + 6e123e0 commit d32bd0f
Show file tree
Hide file tree
Showing 8 changed files with 547 additions and 4 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,36 @@ jobs:
- os: ubuntu-latest
qutip-version: '@dev.major'
qiskit-version: ''
pyqir-version: ''
python-version: '3.10'
- os: windows-latest
qutip-version: '==4.6.*'
qiskit-version: ''
pyqir-version: ''
python-version: '3.8'
- os: windows-latest
qutip-version: '==4.6.*'
qiskit-version: ''
pyqir-version: '==0.6.2'
python-version: '3.8'
- os: macOS-latest
qutip-version: '==4.7.*'
qiskit-version: ''
pyqir-version: ''
python-version: '3.9'
- os: macOS-latest
qutip-version: '==4.7.*'
qiskit-version: ''
pyqir-version: '==0.6.2'
python-version: '3.9'
- os: ubuntu-latest
qutip-version: ''
qiskit-version: '==0.36.*'
pyqir-version: ''
python-version: '3.7'
- os: ubuntu-latest
qutip-version: ''
pyqir-version: '==0.6.2'
python-version: '3.7'

steps:
Expand All @@ -49,6 +67,11 @@ jobs:
if: ${{ matrix.qiskit-version != '' }}
run: python -m pip install 'qiskit${{ matrix.qiskit-version }}'

- name: Install PyQIR from PyPI
if: ${{ matrix.pyqir-version != '' }}
# We use each subpackage explicitly here; see https://github.com/qir-alliance/pyqir/issues/167.
run: python -m pip install 'pyqir-generator${{ matrix.pyqir-version }}' 'pyqir-parser${{ matrix.pyqir-version }}'

- name: Install qutip-qip
# Installing in-place so that coveralls can locate the source code.
run: |
Expand Down
2 changes: 2 additions & 0 deletions doc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ numpydoc==1.4.0
matplotlib==3.5.2
docutils==0.17.1
sphinxcontrib-bibtex==2.4.2
pyqir-generator==0.6.2
pyqir-parser==0.6.2
1 change: 1 addition & 0 deletions doc/source/apidoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Simulation based on operator-state multiplication.
qutip_qip.qubits
qutip_qip.decompose
qutip_qip.qasm
qutip_qip.qir
qutip_qip.vqa

Pulse-level simulation
Expand Down
16 changes: 16 additions & 0 deletions doc/source/apidoc/qutip_qip.qir.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
:orphan:

qutip\_qip.qir
===============

.. automodule:: qutip_qip.qir
:members:
:show-inheritance:
:imported-members:

.. rubric:: Functions

.. autosummary::

QirFormat
circuit_to_qir
27 changes: 24 additions & 3 deletions doc/source/qip-simulator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,9 @@ We are left with a mixed state.
Import and export quantum circuits
==================================

QuTiP supports importation and exportation of quantum circuit in the `OpenQASM 2 W-state <https://github.com/Qiskit/openqasm/tree/OpenQASM2.x>`_ format
throught the function :func:`.read_qasm` and :func:`.save_qasm`.
We demonstrate this using the w-state generation circuit.
QuTiP supports importing and exporting quantum circuits in the `OpenQASM 2.0 <https://github.com/Qiskit/openqasm/tree/OpenQASM2.x>`_ format, as well as exporting circuits to `Quantum Intermediate Representation <https://www.qir-alliance.org/>`_.
To import from and export to OpenQASM 2.0, you can use the :func:`.read_qasm` and :func:`.save_qasm` functions, respectively.
We demonstrate this functionality by loading a circuit for preparing the :math:`\left|W\right\rangle`-state from an OpenQASM 2.0 file.
The following code is in OpenQASM format:

.. code-block::
Expand Down Expand Up @@ -326,3 +326,24 @@ One can save it in a ``.qasm`` file and import it using the following code:

from qutip_qip.qasm import read_qasm
qc = read_qasm("source/w-state.qasm")

QuTiP circuits can also be exported to QIR:

.. doctest::

>>> from qutip_qip.circuit import QubitCircuit
>>> from qutip_qip.qir import circuit_to_qir

>>> circuit = QubitCircuit(3, num_cbits=2)
>>> msg, here, there = range(3)
>>> circuit.add_gate("RZ", targets=[msg], arg_value=0.123)
>>> circuit.add_gate("SNOT", targets=[here])
>>> circuit.add_gate("CNOT", targets=[there], controls=[here])
>>> circuit.add_gate("CNOT", targets=[here], controls=[msg])
>>> circuit.add_gate("SNOT", targets=[msg])
>>> circuit.add_measurement("Z", targets=[msg], classical_store=0)
>>> circuit.add_measurement("Z", targets=[here], classical_store=1)
>>> circuit.add_gate("X", targets=[there], classical_controls=[0])
>>> circuit.add_gate("Z", targets=[there], classical_controls=[1])

>>> print(circuit_to_qir(circuit, "text")) # doctest: +SKIP
3 changes: 2 additions & 1 deletion src/qutip_qip/operations/gateclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from itertools import product, chain
from functools import partial, reduce
from operator import mul
from typing import Optional

import warnings
import inspect
Expand Down Expand Up @@ -148,7 +149,7 @@ def __init__(
arg_value=None,
control_value=None,
classical_controls=None,
classical_control_value=None,
classical_control_value: Optional[int] = None,
arg_label=None,
**kwargs,
):
Expand Down
235 changes: 235 additions & 0 deletions src/qutip_qip/qir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# 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"]


class QirFormat(Enum):
"""
Specifies the format used to serialize QIR.
"""

#: Specifies that QIR should be encoded as LLVM bitcode (typically, files
#: ending in `.bc`).
BITCODE = auto()
#: Specifies that QIR should be encoded as plain text (typicaly, files
#: ending in `.ll`).
TEXT = auto()
#: Specifies that QIR should be encoded as a PyQIR module object.
MODULE = auto()

@classmethod
def ensure(
cls, val: Union[Literal["bitcode", "text", "module"], QirFormat]
) -> QirFormat:
"""
Given a value, returns a value ensured to be of type `QirFormat`,
attempting to convert if needed.
"""
if isinstance(val, cls):
return val
elif isinstance(val, str):
return cls[val.upper()]

return cls(val)


# 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:
...


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."

0 comments on commit d32bd0f

Please sign in to comment.