Source code for qiskit.qasm2.export

# This code is part of Qiskit.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Export tools for OpenQASM 2."""

from __future__ import annotations

__all__ = ["dump", "dumps"]

import collections.abc
import io
import itertools
import os
import re
import string

from qiskit.circuit import (
    QuantumCircuit,
    Instruction,
    QuantumRegister,
    ClassicalRegister,
    Qubit,
    Clbit,
    Parameter,
    library as lib,
)
from qiskit.circuit.tools import pi_check
from .exceptions import QASM2ExportError

_EXISTING_GATE_NAMES = frozenset(
    [
        "barrier",
        "measure",
        "reset",
        "u3",
        "u2",
        "u1",
        "cx",
        "id",
        "u0",
        "u",
        "p",
        "x",
        "y",
        "z",
        "h",
        "s",
        "sdg",
        "t",
        "tdg",
        "rx",
        "ry",
        "rz",
        "sx",
        "sxdg",
        "cz",
        "cy",
        "swap",
        "ch",
        "ccx",
        "cswap",
        "crx",
        "cry",
        "crz",
        "cu1",
        "cp",
        "cu3",
        "csx",
        "cu",
        "rxx",
        "rzz",
        "rccx",
        "rc3x",
        "c3x",
        "c3sx",  # This is the Qiskit gate name, but the qelib1.inc name is 'c3sqrtx'.
        "c4x",
    ]
)

_RESERVED = frozenset(
    [
        "OPENQASM",
        "qreg",
        "creg",
        "include",
        "gate",
        "opaque",
        "U",
        "CX",
        "measure",
        "reset",
        "if",
        "barrier",
    ]
)


[docs]def dump(circuit: QuantumCircuit, filename_or_stream: os.PathLike | io.TextIOBase, /): """Dump a circuit as an OpenQASM 2 program to a file or stream. Args: circuit: the :class:`.QuantumCircuit` to be exported. filename_or_stream: either a path-like object (likely a :class:`str` or :class:`pathlib.Path`), or an already opened text-mode stream. Raises: QASM2ExportError: if the circuit cannot be represented by OpenQASM 2. """ if isinstance(filename_or_stream, io.TextIOBase): print(dumps(circuit), file=filename_or_stream) return with open(filename_or_stream, "w") as stream: print(dumps(circuit), file=stream)
[docs]def dumps(circuit: QuantumCircuit, /) -> str: """Export a circuit to an OpenQASM 2 program in a string. Args: circuit: the :class:`.QuantumCircuit` to be exported. Returns: An OpenQASM 2 string representing the circuit. Raises: QASM2ExportError: if the circuit cannot be represented by OpenQASM 2. """ if circuit.num_parameters > 0: raise QASM2ExportError("Cannot represent circuits with unbound parameters in OpenQASM 2.") # Mapping of instruction name to a pair of the source for a definition, and an OQ2 string # that includes the `gate` or `opaque` statement that defines the gate. gates_to_define: collections.OrderedDict[ str, tuple[Instruction, str] ] = collections.OrderedDict() regless_qubits = [bit for bit in circuit.qubits if not circuit.find_bit(bit).registers] regless_clbits = [bit for bit in circuit.clbits if not circuit.find_bit(bit).registers] dummy_registers: list[QuantumRegister | ClassicalRegister] = [] if regless_qubits: dummy_registers.append(QuantumRegister(name="qregless", bits=regless_qubits)) if regless_clbits: dummy_registers.append(ClassicalRegister(name="cregless", bits=regless_clbits)) register_escaped_names: dict[str, QuantumRegister | ClassicalRegister] = {} for regs in (circuit.qregs, circuit.cregs, dummy_registers): for reg in regs: register_escaped_names[ _make_unique(_escape_name(reg.name, "reg_"), register_escaped_names) ] = reg bit_labels: dict[Qubit | Clbit, str] = { bit: "%s[%d]" % (name, idx) for name, register in register_escaped_names.items() for (idx, bit) in enumerate(register) } register_definitions_qasm = "\n".join( f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];" for name, reg in register_escaped_names.items() ) instruction_calls = [] for instruction in circuit._data: operation = instruction.operation if operation.name == "measure": qubit = instruction.qubits[0] clbit = instruction.clbits[0] instruction_qasm = f"measure {bit_labels[qubit]} -> {bit_labels[clbit]};" elif operation.name == "reset": instruction_qasm = f"reset {bit_labels[instruction.qubits[0]]};" elif operation.name == "barrier": if not instruction.qubits: # Barriers with no operands are invalid in (strict) OQ2, and the statement # would have no meaning anyway. continue qargs = ",".join(bit_labels[q] for q in instruction.qubits) instruction_qasm = "barrier;" if not qargs else f"barrier {qargs};" else: instruction_qasm = _custom_operation_statement(instruction, gates_to_define, bit_labels) instruction_calls.append(instruction_qasm) instructions_qasm = "\n".join(f"{call}" for call in instruction_calls) gate_definitions_qasm = "\n".join(f"{qasm}" for _, qasm in gates_to_define.values()) return "\n".join( part for part in ( "OPENQASM 2.0;", 'include "qelib1.inc";', gate_definitions_qasm, register_definitions_qasm, instructions_qasm, ) if part )
def _escape_name(name: str, prefix: str) -> str: """Returns a valid OpenQASM 2.0 identifier, using `prefix` as a prefix if necessary. `prefix` must itself be a valid identifier.""" # Replace all non-ASCII-word characters (letters, digits, underscore) with the underscore. escaped_name = re.sub(r"\W", "_", name, flags=re.ASCII) if ( not escaped_name or escaped_name[0] not in string.ascii_lowercase or escaped_name in _RESERVED ): escaped_name = prefix + escaped_name return escaped_name def _make_unique(name: str, already_defined: collections.abc.Set[str]) -> str: """Generate a name by suffixing the given stem that is unique within the defined set.""" if name not in already_defined: return name used = {in_use[len(name) :] for in_use in already_defined if in_use.startswith(name)} characters = (string.digits + string.ascii_letters) if name else string.ascii_letters for parts in itertools.chain.from_iterable( itertools.product(characters, repeat=n) for n in itertools.count(1) ): suffix = "".join(parts) if suffix not in used: return name + suffix # This isn't actually reachable because the above loop is infinite. return name def _rename_operation(operation): """Returns the operation with a new name following this pattern: {operation name}_{operation id}""" new_name = f"{operation.name}_{id(operation)}" updated_operation = operation.copy(name=new_name) return updated_operation def _instruction_call_site(operation): """Return an OpenQASM 2 string for the instruction.""" if operation.name == "c3sx": qasm2_call = "c3sqrtx" else: qasm2_call = operation.name if operation.params: qasm2_call = "{}({})".format( qasm2_call, ",".join([pi_check(i, output="qasm", eps=1e-12) for i in operation.params]), ) if operation.condition is not None: if not isinstance(operation.condition[0], ClassicalRegister): raise QASM2ExportError( "OpenQASM 2 can only condition on registers, but got '{operation.condition[0]}'" ) qasm2_call = ( "if(%s==%d) " % (operation.condition[0].name, operation.condition[1]) + qasm2_call ) return qasm2_call # Just needs to have enough parameters to support the largest standard (non-controlled) gate in our # standard library. We have to use the same `Parameter` instances each time so the equality # comparisons will work. _FIXED_PARAMETERS = [Parameter("param0"), Parameter("param1"), Parameter("param2")] def _custom_operation_statement(instruction, gates_to_define, bit_labels): operation = _define_custom_operation(instruction.operation, gates_to_define) # Insert qasm representation of the original instruction if instruction.clbits: bits = itertools.chain(instruction.qubits, instruction.clbits) else: bits = instruction.qubits bits_qasm = ",".join(bit_labels[j] for j in bits) return f"{_instruction_call_site(operation)} {bits_qasm};" def _define_custom_operation(operation, gates_to_define): """Extract a custom definition from the given operation, and append any necessary additional subcomponents' definitions to the ``gates_to_define`` ordered dictionary. Returns a potentially new :class:`.Instruction`, which should be used for the :meth:`~.Instruction.qasm` call (it may have been renamed).""" if operation.name in _EXISTING_GATE_NAMES: return operation # Check instructions names or label are valid escaped = _escape_name(operation.name, "gate_") if escaped != operation.name: operation = operation.copy(name=escaped) # These are built-in gates that are known to be safe to construct by passing the correct number # of `Parameter` instances positionally, and have no other information. We can't guarantee that # if they've been subclassed, though. This is a total hack; ideally we'd be able to inspect the # "calling" signatures of Qiskit `Gate` objects to know whether they're safe to re-parameterise. known_good_parameterized = { lib.PhaseGate, lib.RGate, lib.RXGate, lib.RXXGate, lib.RYGate, lib.RYYGate, lib.RZGate, lib.RZXGate, lib.RZZGate, lib.XXMinusYYGate, lib.XXPlusYYGate, lib.UGate, lib.U1Gate, lib.U2Gate, lib.U3Gate, } # In known-good situations we want to use a manually parametrised object as the source of the # definition, but still continue to return the given object as the call-site object. if operation.base_class in known_good_parameterized: parameterized_operation = type(operation)(*_FIXED_PARAMETERS[: len(operation.params)]) elif hasattr(operation, "_qasm2_decomposition"): new_op = operation._qasm2_decomposition() parameterized_operation = operation = new_op.copy(name=_escape_name(new_op.name, "gate_")) else: parameterized_operation = operation # If there's an _equal_ operation in the existing circuits to be defined, then our job is done. previous_definition_source, _ = gates_to_define.get(operation.name, (None, None)) if parameterized_operation == previous_definition_source: return operation # Otherwise, if there's a naming clash, we need a unique name. if operation.name in gates_to_define: operation = _rename_operation(operation) new_name = operation.name if parameterized_operation.params: parameters_qasm = ( "(" + ",".join(f"param{i}" for i in range(len(parameterized_operation.params))) + ")" ) else: parameters_qasm = "" if operation.num_qubits == 0: raise QASM2ExportError( f"OpenQASM 2 cannot represent '{operation.name}, which acts on zero qubits." ) if operation.num_clbits != 0: raise QASM2ExportError( f"OpenQASM 2 cannot represent '{operation.name}', which acts on {operation.num_clbits}" " classical bits." ) qubits_qasm = ",".join(f"q{i}" for i in range(parameterized_operation.num_qubits)) parameterized_definition = getattr(parameterized_operation, "definition", None) if parameterized_definition is None: gates_to_define[new_name] = ( parameterized_operation, f"opaque {new_name}{parameters_qasm} {qubits_qasm};", ) else: qubit_labels = {bit: f"q{i}" for i, bit in enumerate(parameterized_definition.qubits)} body_qasm = " ".join( _custom_operation_statement(instruction, gates_to_define, qubit_labels) for instruction in parameterized_definition.data ) # if an inner operation has the same name as the actual operation, it needs to be renamed if operation.name in gates_to_define: operation = _rename_operation(operation) new_name = operation.name definition_qasm = f"gate {new_name}{parameters_qasm} {qubits_qasm} {{ {body_qasm} }}" gates_to_define[new_name] = (parameterized_operation, definition_qasm) return operation