Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor OpenQASM 2 exporter #9953

Merged
merged 3 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 8 additions & 21 deletions qiskit/circuit/library/generalized_gates/permutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,6 @@ def __init__(
)
pattern = np.array(pattern)

# This is needed to support qasm()
self._qasm_name = "permutation__" + "_".join([str(n) for n in pattern]) + "_"
self._qasm_definition = None

super().__init__(name="permutation", num_qubits=num_qubits, params=[pattern])

def __array__(self, dtype=None):
Expand Down Expand Up @@ -183,21 +179,12 @@ def inverse(self):

return PermutationGate(pattern=_inverse_pattern(self.pattern))

def qasm(self):
"""The qasm for a permutation."""

if not self._qasm_definition:

# pylint: disable=cyclic-import
from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap

# This qasm should be identical to the one produced when permutation
# was a circuit rather than a gate.
swaps = _get_ordered_swap(self.pattern)
gates_def = "".join(["swap q" + str(i) + ",q" + str(j) + "; " for i, j in swaps])
qubit_list = ",".join(["q" + str(n) for n in range(len(self.pattern))])
self._qasm_definition = (
"gate " + self._qasm_name + " " + qubit_list + " { " + gates_def + "}"
)
def _qasm2_decomposition(self):
# pylint: disable=cyclic-import
from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap

return self._qasmif(self._qasm_name)
name = f"permutation__{'_'.join(str(n) for n in self.pattern)}_"
out = QuantumCircuit(self.num_qubits, name=name)
for i, j in _get_ordered_swap(self.pattern):
out.swap(i, j)
return out.to_gate()
275 changes: 129 additions & 146 deletions qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1620,7 +1620,7 @@ def qasm(
if self.num_parameters > 0:
raise QasmError("Cannot represent circuits with unbound parameters in OpenQASM 2.")

existing_gate_names = [
existing_gate_names = {
"barrier",
"measure",
"reset",
Expand Down Expand Up @@ -1666,12 +1666,11 @@ def qasm(
"c3x",
"c3sx", # This is the Qiskit gate name, but the qelib1.inc name is 'c3sqrtx'.
"c4x",
]

existing_composite_circuits: list[Instruction] = []
}

string_temp = self.header + "\n"
string_temp += self.extension_lib + "\n"
# 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: OrderedDict[str, tuple[Instruction, str]] = OrderedDict()

regless_qubits = [bit for bit in self.qubits if not self.find_bit(bit).registers]
regless_clbits = [bit for bit in self.clbits if not self.find_bit(bit).registers]
Expand All @@ -1691,72 +1690,51 @@ def qasm(
for name, register in register_escaped_names.items()
for (idx, bit) in enumerate(register)
}
for name, reg in register_escaped_names.items():
string_temp += (
f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];\n"
)

register_definitions_qasm = "".join(
f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];\n"
for name, reg in register_escaped_names.items()
)
instruction_calls = []
for instruction in self._data:
operation = instruction.operation
if operation.name == "measure":
qubit = instruction.qubits[0]
clbit = instruction.clbits[0]
string_temp += "{} {} -> {};\n".format(
operation.qasm(),
bit_labels[qubit],
bit_labels[clbit],
)
instruction_qasm = f"measure {bit_labels[qubit]} -> {bit_labels[clbit]};"
elif operation.name == "reset":
string_temp += f"reset {bit_labels[instruction.qubits[0]]};\n"
instruction_qasm = f"reset {bit_labels[instruction.qubits[0]]};"
elif operation.name == "barrier":
qargs = ",".join(bit_labels[q] for q in instruction.qubits)
string_temp += "barrier;\n" if not qargs else f"barrier {qargs};\n"
instruction_qasm = "barrier;" if not qargs else f"barrier {qargs};"
else:
# Check instructions names or label are valid
escaped = _qasm_escape_name(operation.name, "gate_")
if escaped != operation.name:
operation = operation.copy(name=escaped)

# decompose gate using definitions if they are not defined in OpenQASM2
if operation.name not in existing_gate_names:
op_qasm_name = None
if operation.name == "permutation":
op_qasm_name = getattr(operation, "_qasm_name", None)
if op_qasm_name:
operation = operation.copy(name=op_qasm_name)

if operation not in existing_composite_circuits:
if operation.name in [
operation.name for operation in existing_composite_circuits
]:
# append operation id to name of operation copy to make it unique
operation = operation.copy(name=f"{operation.name}_{id(operation)}")

existing_composite_circuits.append(operation)

# Strictly speaking, the code below does not work for operations that
# do not have the "definition" method but require a complex (recursive)
# "_qasm_definition". Fortunately, right now we do not have any such operations.
if getattr(operation, "definition", None) is not None:
_add_sub_instruction_to_existing_composite_circuits(
operation, existing_gate_names, existing_composite_circuits
)
operation = _qasm2_define_custom_operation(
operation, existing_gate_names, gates_to_define
)

# Insert qasm representation of the original instruction
string_temp += "{} {};\n".format(
operation.qasm(),
",".join([bit_labels[j] for j in instruction.qubits + instruction.clbits]),
bits_qasm = ",".join(
bit_labels[j] for j in itertools.chain(instruction.qubits, instruction.clbits)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not instruction.qubits + instruction.clbits as before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no particular reason - I probably just didn't notice I'd done it during the rewrite. itertools.chain is the normal idiom I personally use, so I just wouldn't have thought about it.

)

# insert gate definitions
string_temp = _insert_composite_gate_definition_qasm(
string_temp, existing_composite_circuits, self.extension_lib
instruction_qasm = f"{operation.qasm()} {bits_qasm};"
instruction_calls.append(instruction_qasm)
instructions_qasm = "".join(f"{call}\n" for call in instruction_calls)
gate_definitions_qasm = "".join(f"{qasm}\n" for _, qasm in gates_to_define.values())

out = "".join(
(
self.header,
"\n",
self.extension_lib,
"\n",
gate_definitions_qasm,
register_definitions_qasm,
instructions_qasm,
)
)

if filename:
with open(filename, "w+", encoding=encoding) as file:
file.write(string_temp)
file.close()
file.write(out)

if formatted:
if not HAS_PYGMENTS:
Expand All @@ -1766,12 +1744,11 @@ def qasm(
pip_install="pip install pygments",
)
code = pygments.highlight(
string_temp, OpenQASMLexer(), Terminal256Formatter(style=QasmTerminalStyle)
out, OpenQASMLexer(), Terminal256Formatter(style=QasmTerminalStyle)
)
print(code)
return None
else:
return string_temp
return out

def draw(
self,
Expand Down Expand Up @@ -4886,28 +4863,100 @@ def _compare_parameters(param1: Parameter, param2: Parameter) -> int:
return _standard_compare(param1.name, param2.name)


def _add_sub_instruction_to_existing_composite_circuits(
instruction: Instruction,
existing_gate_names: list[str],
existing_composite_circuits: list[Instruction],
) -> None:
"""Recursively add undefined sub-instructions in the definition of the given
instruction to existing_composite_circuit list.
"""
for sub_instruction in instruction.definition:
sub_operation = sub_instruction.operation
# Check instructions names are valid
escaped = _qasm_escape_name(sub_operation.name, "gate_")
if escaped != sub_operation.name:
sub_operation = sub_operation.copy(name=escaped)
if (
sub_operation.name not in existing_gate_names
and sub_operation not in existing_composite_circuits
):
existing_composite_circuits.insert(0, sub_operation)
_add_sub_instruction_to_existing_composite_circuits(
sub_operation, existing_gate_names, existing_composite_circuits
# Used by the OQ2 exporter. 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.
_QASM2_FIXED_PARAMETERS = [Parameter("param0"), Parameter("param1"), Parameter("param2")]


def _qasm2_define_custom_operation(operation, existing_gate_names, 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)."""
from qiskit.circuit import library as lib # pylint: disable=cyclic-import

if operation.name in existing_gate_names:
return operation

# Check instructions names or label are valid
escaped = _qasm_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 type(operation) in known_good_parameterized:
parameterized_operation = type(operation)(*_QASM2_FIXED_PARAMETERS[: len(operation.params)])
elif hasattr(operation, "_qasm2_decomposition"):
new_op = operation._qasm2_decomposition()
parameterized_operation = operation = new_op.copy(
name=_qasm_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:
new_name = f"{operation.name}_{id(operation)}"
operation = operation.copy(name=new_name)
else:
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 = ""
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:
statements = []
qubit_labels = {bit: f"q{i}" for i, bit in enumerate(parameterized_definition.qubits)}
for instruction in parameterized_definition.data:
new_operation = _qasm2_define_custom_operation(
instruction.operation, existing_gate_names, gates_to_define
)
bits_qasm = ",".join(qubit_labels[q] for q in instruction.qubits)
statements.append(f"{new_operation.qasm()} {bits_qasm};")
body_qasm = " ".join(statements)
1ucian0 marked this conversation as resolved.
Show resolved Hide resolved
definition_qasm = f"gate {new_name}{parameters_qasm} {qubits_qasm} {{ {body_qasm} }}"
gates_to_define[new_name] = (parameterized_operation, definition_qasm)
return operation


def _qasm_escape_name(name: str, prefix: str) -> str:
Expand Down Expand Up @@ -4940,72 +4989,6 @@ def _make_unique(name: str, already_defined: collections.abc.Set[str]) -> str:
return name


def _get_composite_circuit_qasm_from_instruction(instruction: Instruction) -> str:
"""Returns OpenQASM string composite circuit given an instruction.
The given instruction should be the result of composite_circuit.to_instruction()."""

if instruction.definition is None:
raise ValueError(f'Instruction "{instruction.name}" is not defined.')

gate_parameters = ",".join(["param%i" % num for num in range(len(instruction.params))])
qubit_parameters = ",".join(["q%i" % num for num in range(instruction.num_qubits)])
composite_circuit_gates = ""

definition = instruction.definition
definition_bit_labels = {
bit: idx for bits in (definition.qubits, definition.clbits) for idx, bit in enumerate(bits)
}
for sub_instruction in definition:
sub_operation = sub_instruction.operation
escaped = _qasm_escape_name(sub_operation.name, "gate_")
if escaped != sub_operation.name:
sub_operation = sub_operation.copy(name=escaped)

gate_qargs = ",".join(
"q%i" % index
for index in [definition_bit_labels[qubit] for qubit in sub_instruction.qubits]
)
composite_circuit_gates += f"{sub_operation.qasm()} {gate_qargs}; "

if composite_circuit_gates:
composite_circuit_gates = composite_circuit_gates.rstrip(" ")

if gate_parameters:
qasm_string = "gate {}({}) {} {{ {} }}".format(
instruction.name,
gate_parameters,
qubit_parameters,
composite_circuit_gates,
)
else:
qasm_string = "gate {} {} {{ {} }}".format(
instruction.name,
qubit_parameters,
composite_circuit_gates,
)

return qasm_string


def _insert_composite_gate_definition_qasm(
string_temp: str, existing_composite_circuits: list[Instruction], extension_lib: str
) -> str:
"""Insert composite gate definition QASM code right after extension library in the header"""

gate_definition_string = ""

# Generate gate definition string
for instruction in existing_composite_circuits:
if hasattr(instruction, "_qasm_definition"):
qasm_string = instruction._qasm_definition
else:
qasm_string = _get_composite_circuit_qasm_from_instruction(instruction)
gate_definition_string += "\n" + qasm_string

string_temp = string_temp.replace(extension_lib, f"{extension_lib}{gate_definition_string}")
return string_temp


def _bit_argument_conversion(specifier, bit_sequence, bit_set, type_) -> list[Bit]:
"""Get the list of bits referred to by the specifier ``specifier``.

Expand Down
Loading