From 1745b2969e05655727469bdacafcf790a7834828 Mon Sep 17 00:00:00 2001 From: Victory Omole Date: Mon, 13 Jun 2022 17:22:16 -0500 Subject: [PATCH] Create a `quil_output.py` in `cirq-rigetti` that doesn't call the `quil` protocol (#5490) Part 1 of: https://github.com/quantumlib/Cirq/issues/4623 --- cirq-rigetti/cirq_rigetti/__init__.py | 2 +- cirq-rigetti/cirq_rigetti/_quil_output.py | 88 --- .../cirq_rigetti/circuit_transformers.py | 2 +- cirq-rigetti/cirq_rigetti/quil_output.py | 547 ++++++++++++++++++ cirq-rigetti/cirq_rigetti/quil_output_test.py | 489 ++++++++++++++++ 5 files changed, 1038 insertions(+), 90 deletions(-) delete mode 100644 cirq-rigetti/cirq_rigetti/_quil_output.py create mode 100644 cirq-rigetti/cirq_rigetti/quil_output.py create mode 100644 cirq-rigetti/cirq_rigetti/quil_output_test.py diff --git a/cirq-rigetti/cirq_rigetti/__init__.py b/cirq-rigetti/cirq_rigetti/__init__.py index ac81e36f5fe..a37325381a1 100644 --- a/cirq-rigetti/cirq_rigetti/__init__.py +++ b/cirq-rigetti/cirq_rigetti/__init__.py @@ -15,7 +15,7 @@ from cirq_rigetti._version import __version__ from cirq_rigetti.sampler import RigettiQCSSampler, get_rigetti_qcs_sampler from cirq_rigetti.service import RigettiQCSService, get_rigetti_qcs_service -from cirq_rigetti import circuit_sweep_executors +from cirq_rigetti import circuit_sweep_executors, quil_output from cirq_rigetti import circuit_transformers from cirq_rigetti.aspen_device import ( RigettiQCSAspenDevice, diff --git a/cirq-rigetti/cirq_rigetti/_quil_output.py b/cirq-rigetti/cirq_rigetti/_quil_output.py deleted file mode 100644 index 58aa2d00f9a..00000000000 --- a/cirq-rigetti/cirq_rigetti/_quil_output.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2021 The Cirq Developers -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Callable, Dict, Set, Tuple, Optional, List -import cirq - - -class RigettiQCSQuilOutput(cirq.QuilOutput): - """A sub-class of `cirq.circuits.quil_output.QuilOutput` that additionally accepts a - `qubit_id_map` for explicitly mapping logical qubits to physical qubits. - - Attributes: - qubit_id_map: A dictionary mapping `cirq.Qid` to strings that - address physical qubits in the outputted QUIL. - measurement_id_map: A dictionary mapping a Cirq measurement key to - the corresponding QUIL memory region. - formatter: A QUIL formatter that formats QUIL strings account for both - the `qubit_id_map` and `measurement_id_map`. - """ - - def __init__( - self, - *, - operations: cirq.OP_TREE, - qubits: Tuple[cirq.Qid, ...], - decompose_operation: Optional[Callable[[cirq.Operation], List[cirq.Operation]]] = None, - qubit_id_map: Optional[Dict[cirq.Qid, str]] = None, - ): - """Initializes an instance of `RigettiQCSQuilOutput`. - - Args: - operations: A list or tuple of `cirq.OP_TREE` arguments. - qubits: The qubits used in the operations. - decompose_operation: Optional; A callable that decomposes a circuit operation - into a list of equivalent operations. If None provided, this class - decomposes operations by invoking `QuilOutput._write_quil`. - qubit_id_map: Optional; A dictionary mapping `cirq.Qid` to strings that - address physical qubits in the outputted QUIL. - """ - super().__init__(operations, qubits) - self.qubit_id_map = qubit_id_map or self._generate_qubit_ids() - self.measurement_id_map = self._generate_measurement_ids() - - self.formatter = cirq.QuilFormatter( - qubit_id_map=self.qubit_id_map, measurement_id_map=self.measurement_id_map - ) - self._decompose_operation = decompose_operation - - def _write_quil(self, output_func: Callable[[str], None]) -> None: - """Calls `output_func` for successive lines of QUIL output. - - Args: - output_func: A function that accepts a string of QUIL. This will likely - write the QUIL to a file. - - Returns: - None. - """ - if self._decompose_operation is None: - return super()._write_quil(output_func) - - output_func("# Created using Cirq.\n\n") - - if len(self.measurements) > 0: - measurements_declared: Set[str] = set() - for m in self.measurements: - key = cirq.measurement_key_name(m) - if key in measurements_declared: - continue - measurements_declared.add(key) - output_func(f"DECLARE {self.measurement_id_map[key]} BIT[{len(m.qubits)}]\n") - output_func("\n") - - for main_op in self.operations: - decomposed = self._decompose_operation(main_op) - for decomposed_op in decomposed: - output_func(cirq.quil(decomposed_op, formatter=self.formatter)) diff --git a/cirq-rigetti/cirq_rigetti/circuit_transformers.py b/cirq-rigetti/cirq_rigetti/circuit_transformers.py index 8921454c550..60669e098e2 100644 --- a/cirq-rigetti/cirq_rigetti/circuit_transformers.py +++ b/cirq-rigetti/cirq_rigetti/circuit_transformers.py @@ -17,7 +17,7 @@ from typing import Dict, cast, Optional, Tuple, List, Callable from pyquil import Program import cirq -from cirq_rigetti._quil_output import RigettiQCSQuilOutput +from cirq_rigetti.quil_output import RigettiQCSQuilOutput from typing_extensions import Protocol diff --git a/cirq-rigetti/cirq_rigetti/quil_output.py b/cirq-rigetti/cirq_rigetti/quil_output.py new file mode 100644 index 00000000000..90151b4cdf1 --- /dev/null +++ b/cirq-rigetti/cirq_rigetti/quil_output.py @@ -0,0 +1,547 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import string +from typing import Callable, Dict, Set, Tuple, Union, Any, Optional, List, cast +import numpy as np +import cirq +import cirq_rigetti +from cirq import protocols, value, ops + + +def to_quil_complex_format(num) -> str: + """A function for outputting a number to a complex string in QUIL format.""" + cnum = complex(str(num)) + return f"{cnum.real}+{cnum.imag}i" + + +class QuilFormatter(string.Formatter): + """A unique formatter to correctly output values to QUIL.""" + + def __init__( + self, qubit_id_map: Dict['cirq.Qid', str], measurement_id_map: Dict[str, str] + ) -> None: + """Inits QuilFormatter. + + Args: + qubit_id_map: A dictionary {qubit, quil_output_string} for + the proper QUIL output for each qubit. + measurement_id_map: A dictionary {measurement_key, + quil_output_string} for the proper QUIL output for each + measurement key. + """ + self.qubit_id_map = {} if qubit_id_map is None else qubit_id_map + self.measurement_id_map = {} if measurement_id_map is None else measurement_id_map + + def format_field(self, value: Any, spec: str) -> str: + if isinstance(value, cirq.ops.Qid): + value = self.qubit_id_map[value] + if isinstance(value, str) and spec == 'meas': + value = self.measurement_id_map[value] + spec = '' + return super().format_field(value, spec) + + +@value.value_equality(approximate=True) +class QuilOneQubitGate(ops.Gate): + """A QUIL gate representing any single qubit unitary with a DEFGATE and + 2x2 matrix in QUIL. + """ + + def __init__(self, matrix: np.ndarray) -> None: + """Inits QuilOneQubitGate. + + Args: + matrix: The 2x2 unitary matrix for this gate. + """ + self.matrix = matrix + + def _num_qubits_(self) -> int: + return 1 + + def __repr__(self) -> str: + return f'cirq.circuits.quil_output.QuilOneQubitGate(matrix=\n{self.matrix}\n)' + + def _value_equality_values_(self): + return self.matrix + + +@value.value_equality(approximate=True) +class QuilTwoQubitGate(ops.Gate): + """A two qubit gate represented in QUIL with a DEFGATE and it's 4x4 + unitary matrix. + """ + + def __init__(self, matrix: np.ndarray) -> None: + """Inits QuilTwoQubitGate. + + Args: + matrix: The 4x4 unitary matrix for this gate. + """ + self.matrix = matrix + + def _num_qubits_(self) -> int: + return 2 + + def _value_equality_values_(self): + return self.matrix + + def __repr__(self) -> str: + return f'cirq.circuits.quil_output.QuilTwoQubitGate(matrix=\n{self.matrix}\n)' + + +def _ccnotpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> Optional[str]: + gate = cast(cirq.CCNotPowGate, op.gate) + if gate._exponent != 1: + return None + return formatter.format('CCNOT {0} {1} {2}\n', op.qubits[0], op.qubits[1], op.qubits[2]) + + +def _cczpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> Optional[str]: + gate = cast(cirq.CCZPowGate, op.gate) + if gate._exponent != 1: + return None + lines = [ + formatter.format('H {0}\n', op.qubits[2]), + formatter.format('CCNOT {0} {1} {2}\n', op.qubits[0], op.qubits[1], op.qubits[2]), + formatter.format('H {0}\n', op.qubits[2]), + ] + return ''.join(lines) + + +def _cnotpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> Optional[str]: + gate = cast(cirq.CNotPowGate, op.gate) + if gate._exponent == 1: + return formatter.format('CNOT {0} {1}\n', op.qubits[0], op.qubits[1]) + return None + + +def _cswap_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + return formatter.format('CSWAP {0} {1} {2}\n', op.qubits[0], op.qubits[1], op.qubits[2]) + + +def _czpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.CZPowGate, op.gate) + if gate._exponent == 1: + return formatter.format('CZ {0} {1}\n', op.qubits[0], op.qubits[1]) + return formatter.format( + 'CPHASE({0}) {1} {2}\n', gate._exponent * np.pi, op.qubits[0], op.qubits[1] + ) + + +def _hpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.HPowGate, op.gate) + if gate._exponent == 1: + return formatter.format('H {0}\n', op.qubits[0]) + return formatter.format( + 'RY({0}) {3}\nRX({1}) {3}\nRY({2}) {3}\n', + 0.25 * np.pi, + gate._exponent * np.pi, + -0.25 * np.pi, + op.qubits[0], + ) + + +def _identity_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + return ''.join(formatter.format('I {0}\n', qubit) for qubit in op.qubits) + + +def _iswappow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.ISwapPowGate, op.gate) + if gate._exponent == 1: + return formatter.format('ISWAP {0} {1}\n', op.qubits[0], op.qubits[1]) + return formatter.format('XY({0}) {1} {2}\n', gate._exponent * np.pi, op.qubits[0], op.qubits[1]) + + +def _measurement_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.MeasurementGate, op.gate) + invert_mask = gate.invert_mask + if len(invert_mask) < len(op.qubits): + invert_mask = invert_mask + (False,) * (len(op.qubits) - len(invert_mask)) + lines = [] + for i, (qubit, inv) in enumerate(zip(op.qubits, invert_mask)): + if inv: + lines.append(formatter.format('X {0} # Inverting for following measurement\n', qubit)) + lines.append(formatter.format('MEASURE {0} {1:meas}[{2}]\n', qubit, gate.key, i)) + return ''.join(lines) + + +def _quilonequbit_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(QuilOneQubitGate, op.gate) + return ( + f'DEFGATE USERGATE:\n ' + f'{to_quil_complex_format(gate.matrix[0, 0])}, ' + f'{to_quil_complex_format(gate.matrix[0, 1])}\n ' + f'{to_quil_complex_format(gate.matrix[1, 0])}, ' + f'{to_quil_complex_format(gate.matrix[1, 1])}\n' + f'{formatter.format("USERGATE {0}", op.qubits[0])}\n' + ) + + +def _quiltwoqubit_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(QuilOneQubitGate, op.gate) + return ( + f'DEFGATE USERGATE:\n ' + f'{to_quil_complex_format(gate.matrix[0, 0])}, ' + f'{to_quil_complex_format(gate.matrix[0, 1])}, ' + f'{to_quil_complex_format(gate.matrix[0, 2])}, ' + f'{to_quil_complex_format(gate.matrix[0, 3])}\n ' + f'{to_quil_complex_format(gate.matrix[1, 0])}, ' + f'{to_quil_complex_format(gate.matrix[1, 1])}, ' + f'{to_quil_complex_format(gate.matrix[1, 2])}, ' + f'{to_quil_complex_format(gate.matrix[1, 3])}\n ' + f'{to_quil_complex_format(gate.matrix[2, 0])}, ' + f'{to_quil_complex_format(gate.matrix[2, 1])}, ' + f'{to_quil_complex_format(gate.matrix[2, 2])}, ' + f'{to_quil_complex_format(gate.matrix[2, 3])}\n ' + f'{to_quil_complex_format(gate.matrix[3, 0])}, ' + f'{to_quil_complex_format(gate.matrix[3, 1])}, ' + f'{to_quil_complex_format(gate.matrix[3, 2])}, ' + f'{to_quil_complex_format(gate.matrix[3, 3])}\n' + f'{formatter.format("USERGATE {0} {1}", op.qubits[0], op.qubits[1])}\n' + ) + + +def _swappow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.SwapPowGate, op.gate) + if gate._exponent % 2 == 1: + return formatter.format('SWAP {0} {1}\n', op.qubits[0], op.qubits[1]) + return formatter.format( + 'PSWAP({0}) {1} {2}\n', gate._exponent * np.pi, op.qubits[0], op.qubits[1] + ) + + +def _twoqubitdiagonal_gate(op: cirq.Operation, formatter: QuilFormatter) -> Optional[str]: + gate = cast(cirq.TwoQubitDiagonalGate, op.gate) + if np.count_nonzero(gate._diag_angles_radians) == 1: + if gate._diag_angles_radians[0] != 0: + return formatter.format( + 'CPHASE00({0}) {1} {2}\n', gate._diag_angles_radians[0], op.qubits[0], op.qubits[1] + ) + elif gate._diag_angles_radians[1] != 0: + return formatter.format( + 'CPHASE01({0}) {1} {2}\n', gate._diag_angles_radians[1], op.qubits[0], op.qubits[1] + ) + elif gate._diag_angles_radians[2] != 0: + return formatter.format( + 'CPHASE10({0}) {1} {2}\n', gate._diag_angles_radians[2], op.qubits[0], op.qubits[1] + ) + elif gate._diag_angles_radians[3] != 0: + return formatter.format( + 'CPHASE({0}) {1} {2}\n', gate._diag_angles_radians[3], op.qubits[0], op.qubits[1] + ) + return None + + +def _wait_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + return 'WAIT\n' + + +def _xpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.XPowGate, op.gate) + if gate._exponent == 1 and gate._global_shift != -0.5: + return formatter.format('X {0}\n', op.qubits[0]) + return formatter.format('RX({0}) {1}\n', gate._exponent * np.pi, op.qubits[0]) + + +def _xxpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.XPowGate, op.gate) + if gate._exponent == 1: + return formatter.format('X {0}\nX {1}\n', op.qubits[0], op.qubits[1]) + return formatter.format( + 'RX({0}) {1}\nRX({2}) {3}\n', + gate._exponent * np.pi, + op.qubits[0], + gate._exponent * np.pi, + op.qubits[1], + ) + + +def _ypow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.YPowGate, op.gate) + if gate._exponent == 1 and gate.global_shift != -0.5: + return formatter.format('Y {0}\n', op.qubits[0]) + return formatter.format('RY({0}) {1}\n', gate._exponent * np.pi, op.qubits[0]) + + +def _yypow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.YYPowGate, op.gate) + if gate._exponent == 1: + return formatter.format('Y {0}\nY {1}\n', op.qubits[0], op.qubits[1]) + + return formatter.format( + 'RY({0}) {1}\nRY({2}) {3}\n', + gate._exponent * np.pi, + op.qubits[0], + gate._exponent * np.pi, + op.qubits[1], + ) + + +def _zpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.ZPowGate, op.gate) + if gate._exponent == 1 and gate.global_shift != -0.5: + return formatter.format('Z {0}\n', op.qubits[0]) + return formatter.format('RZ({0}) {1}\n', gate._exponent * np.pi, op.qubits[0]) + + +def _zzpow_gate(op: cirq.Operation, formatter: QuilFormatter) -> str: + gate = cast(cirq.ZZPowGate, op.gate) + if gate._exponent == 1: + return formatter.format('Z {0}\nZ {1}\n', op.qubits[0], op.qubits[1]) + + return formatter.format( + 'RZ({0}) {1}\nRZ({2}) {3}\n', + gate._exponent * np.pi, + op.qubits[0], + gate._exponent * np.pi, + op.qubits[1], + ) + + +SUPPORTED_GATES = { + ops.CCNotPowGate: _ccnotpow_gate, + ops.CCZPowGate: _cczpow_gate, + ops.CNotPowGate: _cnotpow_gate, + ops.CSwapGate: _cswap_gate, + ops.CZPowGate: _czpow_gate, + ops.HPowGate: _hpow_gate, + ops.IdentityGate: _identity_gate, + ops.ISwapPowGate: _iswappow_gate, + ops.MeasurementGate: _measurement_gate, + QuilOneQubitGate: _quilonequbit_gate, + QuilTwoQubitGate: _quiltwoqubit_gate, + ops.SwapPowGate: _swappow_gate, + ops.TwoQubitDiagonalGate: _twoqubitdiagonal_gate, + ops.WaitGate: _wait_gate, + ops.XPowGate: _xpow_gate, + ops.XXPowGate: _xxpow_gate, + ops.YPowGate: _ypow_gate, + ops.YYPowGate: _yypow_gate, + ops.ZPowGate: _zpow_gate, + ops.ZZPowGate: _zzpow_gate, +} + + +class QuilOutput: + """An object for passing operations and qubits then outputting them to + QUIL format. The string representation returns the QUIL output for the + circuit. + """ + + def __init__(self, operations: 'cirq.OP_TREE', qubits: Tuple['cirq.Qid', ...]) -> None: + """Inits QuilOutput. + + Args: + operations: A list or tuple of `cirq.OP_TREE` arguments. + qubits: The qubits used in the operations. + """ + self.qubits = qubits + self.operations = tuple(cirq.ops.flatten_to_ops(operations)) + self.measurements = tuple( + op for op in self.operations if isinstance(op.gate, ops.MeasurementGate) + ) + self.qubit_id_map = self._generate_qubit_ids() + self.measurement_id_map = self._generate_measurement_ids() + self.formatter = cirq_rigetti.quil_output.QuilFormatter( + qubit_id_map=self.qubit_id_map, measurement_id_map=self.measurement_id_map + ) + + def _generate_qubit_ids(self) -> Dict['cirq.Qid', str]: + return {qubit: str(i) for i, qubit in enumerate(self.qubits)} + + def _generate_measurement_ids(self) -> Dict[str, str]: + index = 0 + measurement_id_map: Dict[str, str] = {} + for op in self.operations: + if isinstance(op.gate, ops.MeasurementGate): + key = protocols.measurement_key_name(op) + if key in measurement_id_map: + continue + measurement_id_map[key] = f'm{index}' + index += 1 + return measurement_id_map + + def save_to_file(self, path: Union[str, bytes, int]) -> None: + """Write QUIL output to a file specified by path.""" + with open(path, 'w') as f: + f.write(str(self)) + + def __str__(self) -> str: + output = [] + self._write_quil(lambda s: output.append(s)) + return self.rename_defgates(''.join(output)) + + def _op_to_maybe_quil(self, op: cirq.Operation) -> Optional[str]: + for gate_type in SUPPORTED_GATES.keys(): + if isinstance(op.gate, gate_type): + quil: Callable[[cirq.Operation, QuilFormatter], Optional[str]] = SUPPORTED_GATES[ + gate_type + ] + return quil(op, self.formatter) + return None + + def _op_to_quil(self, op: cirq.Operation) -> str: + quil_str = self._op_to_maybe_quil(op) + if not quil_str: + raise ValueError("Can't convert Operation to string") + return quil_str + + def _write_quil(self, output_func: Callable[[str], None]) -> None: + output_func('# Created using Cirq.\n\n') + if len(self.measurements) > 0: + measurements_declared: Set[str] = set() + for m in self.measurements: + key = protocols.measurement_key_name(m) + if key in measurements_declared: + continue + measurements_declared.add(key) + output_func(f'DECLARE {self.measurement_id_map[key]} BIT[{len(m.qubits)}]\n') + output_func('\n') + + def keep(op: 'cirq.Operation') -> bool: + if isinstance(op.gate, tuple(SUPPORTED_GATES.keys())): + if not self._op_to_maybe_quil(op): + return False + return True + return False + + def fallback(op): + if len(op.qubits) not in [1, 2]: + return NotImplemented + + mat = protocols.unitary(op, None) + if mat is None: + return NotImplemented + + # Following code is a safety measure + # Could not find a gate that doesn't decompose into a gate + # with a _quil_ implementation + # coverage: ignore + if len(op.qubits) == 1: + return QuilOneQubitGate(mat).on(*op.qubits) + return QuilTwoQubitGate(mat).on(*op.qubits) + + def on_stuck(bad_op): + return ValueError(f'Cannot output operation as QUIL: {bad_op!r}') + + for main_op in self.operations: + decomposed = protocols.decompose( + main_op, keep=keep, fallback_decomposer=fallback, on_stuck_raise=on_stuck + ) + + for decomposed_op in decomposed: + output_func(self._op_to_quil(decomposed_op)) + + def rename_defgates(self, output: str) -> str: + """A function for renaming the DEFGATEs within the QUIL output. This + utilizes a second pass to find each DEFGATE and rename it based on + a counter. + """ + result = output + defString = "DEFGATE" + nameString = "USERGATE" + defIdx = 0 + nameIdx = 0 + gateNum = 0 + i = 0 + while i < len(output): + if result[i] == defString[defIdx]: + defIdx += 1 + else: + defIdx = 0 + if result[i] == nameString[nameIdx]: + nameIdx += 1 + else: + nameIdx = 0 + if defIdx == len(defString): + gateNum += 1 + defIdx = 0 + if nameIdx == len(nameString): + result = result[: i + 1] + str(gateNum) + result[i + 1 :] + nameIdx = 0 + i += 1 + i += 1 + return result + + +class RigettiQCSQuilOutput(QuilOutput): + """A sub-class of `cirq.circuits.quil_output.QuilOutput` that additionally accepts a + `qubit_id_map` for explicitly mapping logical qubits to physical qubits. + + Attributes: + qubit_id_map: A dictionary mapping `cirq.Qid` to strings that + address physical qubits in the outputted QUIL. + measurement_id_map: A dictionary mapping a Cirq measurement key to + the corresponding QUIL memory region. + formatter: A QUIL formatter that formats QUIL strings account for both + the `qubit_id_map` and `measurement_id_map`. + """ + + def __init__( + self, + *, + operations: cirq.OP_TREE, + qubits: Tuple[cirq.Qid, ...], + decompose_operation: Optional[Callable[[cirq.Operation], List[cirq.Operation]]] = None, + qubit_id_map: Optional[Dict[cirq.Qid, str]] = None, + ): + """Initializes an instance of `RigettiQCSQuilOutput`. + + Args: + operations: A list or tuple of `cirq.OP_TREE` arguments. + qubits: The qubits used in the operations. + decompose_operation: Optional; A callable that decomposes a circuit operation + into a list of equivalent operations. If None provided, this class + decomposes operations by invoking `QuilOutput._write_quil`. + qubit_id_map: Optional; A dictionary mapping `cirq.Qid` to strings that + address physical qubits in the outputted QUIL. + """ + super().__init__(operations, qubits) + self.qubit_id_map = qubit_id_map or self._generate_qubit_ids() + self.measurement_id_map = self._generate_measurement_ids() + + self.formatter = QuilFormatter( + qubit_id_map=self.qubit_id_map, measurement_id_map=self.measurement_id_map + ) + self._decompose_operation = decompose_operation + + def _write_quil(self, output_func: Callable[[str], None]) -> None: + """Calls `output_func` for successive lines of QUIL output. + + Args: + output_func: A function that accepts a string of QUIL. This will likely + write the QUIL to a file. + + Returns: + None. + """ + if self._decompose_operation is None: + return super()._write_quil(output_func) + + output_func("# Created using Cirq.\n\n") + + if len(self.measurements) > 0: + measurements_declared: Set[str] = set() + for m in self.measurements: + key = cirq.measurement_key_name(m) + if key in measurements_declared: + continue + measurements_declared.add(key) + output_func(f"DECLARE {self.measurement_id_map[key]} BIT[{len(m.qubits)}]\n") + output_func("\n") + + for main_op in self.operations: + decomposed = self._decompose_operation(main_op) + for decomposed_op in decomposed: + output_func(self._op_to_quil(decomposed_op)) diff --git a/cirq-rigetti/cirq_rigetti/quil_output_test.py b/cirq-rigetti/cirq_rigetti/quil_output_test.py new file mode 100644 index 00000000000..2a81de9e25d --- /dev/null +++ b/cirq-rigetti/cirq_rigetti/quil_output_test.py @@ -0,0 +1,489 @@ +# Copyright 2020 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import numpy as np +import pytest + +import cirq +from cirq.ops.pauli_interaction_gate import PauliInteractionGate + +import cirq_rigetti +from cirq_rigetti.quil_output import QuilOutput + + +def _make_qubits(n): + return [cirq.NamedQubit(f'q{i}') for i in range(n)] + + +def test_single_gate_no_parameter(): + (q0,) = _make_qubits(1) + output = cirq_rigetti.quil_output.QuilOutput((cirq.X(q0),), (q0,)) + assert ( + str(output) + == """# Created using Cirq. + +X 0\n""" + ) + + +def test_single_gate_with_parameter(): + (q0,) = _make_qubits(1) + output = cirq_rigetti.quil_output.QuilOutput((cirq.X(q0) ** 0.5,), (q0,)) + assert ( + str(output) + == f"""# Created using Cirq. + +RX({np.pi / 2}) 0\n""" + ) + + +def test_single_gate_named_qubit(): + q = cirq.NamedQubit('qTest') + output = cirq_rigetti.quil_output.QuilOutput((cirq.X(q),), (q,)) + + assert ( + str(output) + == """# Created using Cirq. + +X 0\n""" + ) + + +def test_h_gate_with_parameter(): + (q0,) = _make_qubits(1) + output = cirq_rigetti.quil_output.QuilOutput((cirq.H(q0) ** 0.25,), (q0,)) + assert ( + str(output) + == f"""# Created using Cirq. + +RY({np.pi / 4}) 0 +RX({np.pi / 4}) 0 +RY({-np.pi / 4}) 0\n""" + ) + + +def test_save_to_file(tmpdir): + file_path = os.path.join(tmpdir, 'test.quil') + (q0,) = _make_qubits(1) + output = cirq_rigetti.quil_output.QuilOutput((cirq.X(q0)), (q0,)) + output.save_to_file(file_path) + with open(file_path, 'r') as f: + file_content = f.read() + assert ( + file_content + == """# Created using Cirq. + +X 0\n""" + ) + + +def test_quil_one_qubit_gate_repr(): + gate = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 1]])) + assert repr(gate) == ( + """cirq.circuits.quil_output.QuilOneQubitGate(matrix= +[[1 0] + [0 1]] +)""" + ) + + +def test_quil_two_qubit_gate_repr(): + gate = cirq_rigetti.quil_output.QuilTwoQubitGate( + np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + assert repr(gate) == ( + """cirq.circuits.quil_output.QuilTwoQubitGate(matrix= +[[1 0 0 0] + [0 1 0 0] + [0 0 1 0] + [0 0 0 1]] +)""" + ) + + +def test_quil_one_qubit_gate_eq(): + gate = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 1]])) + gate2 = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 1]])) + assert cirq.approx_eq(gate, gate2, atol=1e-16) + gate3 = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 1]])) + gate4 = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 2]])) + assert not cirq.approx_eq(gate4, gate3, atol=1e-16) + + +def test_quil_two_qubit_gate_eq(): + gate = cirq_rigetti.quil_output.QuilTwoQubitGate( + np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + gate2 = cirq_rigetti.quil_output.QuilTwoQubitGate( + np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + assert cirq.approx_eq(gate, gate2, atol=1e-8) + gate3 = cirq_rigetti.quil_output.QuilTwoQubitGate( + np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + gate4 = cirq_rigetti.quil_output.QuilTwoQubitGate( + np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]]) + ) + assert not cirq.approx_eq(gate4, gate3, atol=1e-8) + + +def test_quil_one_qubit_gate_output(): + (q0,) = _make_qubits(1) + gate = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 1]])) + output = cirq_rigetti.quil_output.QuilOutput((gate.on(q0),), (q0,)) + assert ( + str(output) + == """# Created using Cirq. + +DEFGATE USERGATE1: + 1.0+0.0i, 0.0+0.0i + 0.0+0.0i, 1.0+0.0i +USERGATE1 0 +""" + ) + + +def test_two_quil_one_qubit_gate_output(): + (q0,) = _make_qubits(1) + gate = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 1]])) + gate1 = cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[2, 0], [0, 3]])) + output = cirq_rigetti.quil_output.QuilOutput((gate.on(q0), gate1.on(q0)), (q0,)) + assert ( + str(output) + == """# Created using Cirq. + +DEFGATE USERGATE1: + 1.0+0.0i, 0.0+0.0i + 0.0+0.0i, 1.0+0.0i +USERGATE1 0 +DEFGATE USERGATE2: + 2.0+0.0i, 0.0+0.0i + 0.0+0.0i, 3.0+0.0i +USERGATE2 0 +""" + ) + + +def test_quil_two_qubit_gate_output(): + (q0, q1) = _make_qubits(2) + gate = cirq_rigetti.quil_output.QuilTwoQubitGate( + np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + output = cirq_rigetti.quil_output.QuilOutput((gate.on(q0, q1),), (q0, q1)) + assert ( + str(output) + == """# Created using Cirq. + +DEFGATE USERGATE1: + 1.0+0.0i, 0.0+0.0i, 0.0+0.0i, 0.0+0.0i + 0.0+0.0i, 1.0+0.0i, 0.0+0.0i, 0.0+0.0i + 0.0+0.0i, 0.0+0.0i, 1.0+0.0i, 0.0+0.0i + 0.0+0.0i, 0.0+0.0i, 0.0+0.0i, 1.0+0.0i +USERGATE1 0 1 +""" + ) + + +def test_unsupported_operation(): + (q0,) = _make_qubits(1) + + class UnsupportedOperation(cirq.Operation): + qubits = (q0,) + with_qubits = NotImplemented + + output = cirq_rigetti.quil_output.QuilOutput((UnsupportedOperation(),), (q0,)) + with pytest.raises(ValueError): + _ = str(output) + + +def test_i_swap_with_power(): + q0, q1 = _make_qubits(2) + + output = QuilOutput((cirq.ISWAP(q0, q1) ** 0.25,), (q0, q1)) + assert ( + str(output) + == f"""# Created using Cirq. + +XY({np.pi / 4}) 0 1 +""" + ) + + +def test_all_operations(): + qubits = tuple(_make_qubits(5)) + operations = _all_operations(*qubits, include_measurements=False) + output = cirq_rigetti.quil_output.QuilOutput(operations, qubits) + + assert ( + str(output) + == f"""# Created using Cirq. + +DECLARE m0 BIT[1] +DECLARE m1 BIT[1] +DECLARE m2 BIT[1] +DECLARE m3 BIT[3] + +Z 0 +RZ({5 * np.pi / 8}) 0 +Y 0 +RY({3 * np.pi / 8}) 0 +X 0 +RX({7 * np.pi / 8}) 0 +H 1 +CZ 0 1 +CPHASE({np.pi / 4}) 0 1 +CNOT 0 1 +RY({-np.pi / 2}) 1 +CPHASE({np.pi / 2}) 0 1 +RY({np.pi / 2}) 1 +SWAP 0 1 +SWAP 1 0 +PSWAP({3 * np.pi / 4}) 0 1 +H 2 +CCNOT 0 1 2 +H 2 +CCNOT 0 1 2 +RZ({np.pi / 8}) 0 +RZ({np.pi / 8}) 1 +RZ({np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +RZ({-np.pi / 8}) 1 +RZ({np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +RZ({-np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +RZ({-np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +H 2 +RZ({np.pi / 8}) 0 +RZ({np.pi / 8}) 1 +RZ({np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +RZ({-np.pi / 8}) 1 +RZ({np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +RZ({-np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +RZ({-np.pi / 8}) 2 +CNOT 0 1 +CNOT 1 2 +H 2 +CSWAP 0 1 2 +X 0 +X 1 +RX({3 * np.pi / 4}) 0 +RX({3 * np.pi / 4}) 1 +Y 0 +Y 1 +RY({3 * np.pi / 4}) 0 +RY({3 * np.pi / 4}) 1 +Z 0 +Z 1 +RZ({3 * np.pi / 4}) 0 +RZ({3 * np.pi / 4}) 1 +I 0 +I 0 +I 1 +I 2 +ISWAP 2 0 +RZ({-0.111 * np.pi}) 1 +RX({np.pi / 4}) 1 +RZ({0.111 * np.pi}) 1 +RZ({-0.333 * np.pi}) 1 +RX({np.pi / 2}) 1 +RZ({0.333 * np.pi}) 1 +RZ({-0.777 * np.pi}) 1 +RX({-np.pi / 2}) 1 +RZ({0.777 * np.pi}) 1 +WAIT +MEASURE 0 m0[0] +MEASURE 2 m1[0] +MEASURE 3 m2[0] +MEASURE 2 m1[0] +MEASURE 1 m3[0] +X 2 # Inverting for following measurement +MEASURE 2 m3[1] +MEASURE 3 m3[2] +""" + ) + + +def _all_operations(q0, q1, q2, q3, q4, include_measurements=True): + return ( + cirq.Z(q0), + cirq.Z(q0) ** 0.625, + cirq.Y(q0), + cirq.Y(q0) ** 0.375, + cirq.X(q0), + cirq.X(q0) ** 0.875, + cirq.H(q1), + cirq.CZ(q0, q1), + cirq.CZ(q0, q1) ** 0.25, # Requires 2-qubit decomposition + cirq.CNOT(q0, q1), + cirq.CNOT(q0, q1) ** 0.5, # Requires 2-qubit decomposition + cirq.SWAP(q0, q1), + cirq.SWAP(q1, q0) ** -1, + cirq.SWAP(q0, q1) ** 0.75, # Requires 2-qubit decomposition + cirq.CCZ(q0, q1, q2), + cirq.CCX(q0, q1, q2), + cirq.CCZ(q0, q1, q2) ** 0.5, + cirq.CCX(q0, q1, q2) ** 0.5, + cirq.CSWAP(q0, q1, q2), + cirq.XX(q0, q1), + cirq.XX(q0, q1) ** 0.75, + cirq.YY(q0, q1), + cirq.YY(q0, q1) ** 0.75, + cirq.ZZ(q0, q1), + cirq.ZZ(q0, q1) ** 0.75, + cirq.IdentityGate(1).on(q0), + cirq.IdentityGate(3).on(q0, q1, q2), + cirq.ISWAP(q2, q0), # Requires 2-qubit decomposition + cirq.PhasedXPowGate(phase_exponent=0.111, exponent=0.25).on(q1), + cirq.PhasedXPowGate(phase_exponent=0.333, exponent=0.5).on(q1), + cirq.PhasedXPowGate(phase_exponent=0.777, exponent=-0.5).on(q1), + cirq.wait(q0, nanos=0), + cirq.measure(q0, key='xX'), + cirq.measure(q2, key='x_a'), + cirq.measure(q3, key='X'), + cirq.measure(q2, key='x_a'), + cirq.measure(q1, q2, q3, key='multi', invert_mask=(False, True)), + ) + + +def test_fails_on_big_unknowns(): + class UnrecognizedGate(cirq.testing.ThreeQubitGate): + pass + + q0, q1, q2 = _make_qubits(3) + res = cirq_rigetti.quil_output.QuilOutput(UnrecognizedGate().on(q0, q1, q2), (q0, q1, q2)) + with pytest.raises(ValueError, match='Cannot output operation as QUIL'): + _ = str(res) + + +def test_pauli_interaction_gate(): + (q0, q1) = _make_qubits(2) + output = cirq_rigetti.quil_output.QuilOutput(PauliInteractionGate.CZ.on(q0, q1), (q0, q1)) + assert ( + str(output) + == """# Created using Cirq. + +CZ 0 1 +""" + ) + + +def test_equivalent_unitaries(): + """This test covers the factor of pi change. However, it will be skipped + if pyquil is unavailable for import. + + References: + https://docs.pytest.org/en/latest/skipping.html#skipping-on-a-missing-import-dependency + """ + pyquil = pytest.importorskip("pyquil") + pyquil_simulation_tools = pytest.importorskip("pyquil.simulation.tools") + q0, q1 = _make_qubits(2) + operations = [ + cirq.XPowGate(exponent=0.5, global_shift=-0.5)(q0), + cirq.YPowGate(exponent=0.5, global_shift=-0.5)(q0), + cirq.ZPowGate(exponent=0.5, global_shift=-0.5)(q0), + cirq.CZPowGate(exponent=0.5)(q0, q1), + cirq.ISwapPowGate(exponent=0.5)(q0, q1), + ] + output = cirq_rigetti.quil_output.QuilOutput(operations, (q0, q1)) + program = pyquil.Program(str(output)) + pyquil_unitary = pyquil_simulation_tools.program_unitary(program, n_qubits=2) + # Qubit ordering differs between pyQuil and Cirq. + cirq_unitary = cirq.Circuit(cirq.SWAP(q0, q1), operations, cirq.SWAP(q0, q1)).unitary() + assert np.allclose(pyquil_unitary, cirq_unitary) + + +QUIL_CPHASES_PROGRAM = """ +CPHASE00(pi/2) 0 1 +CPHASE01(pi/2) 0 1 +CPHASE10(pi/2) 0 1 +CPHASE(pi/2) 0 1 +""" + +QUIL_DIAGONAL_DECOMPOSE_PROGRAM = """ +RZ(0) 0 +RZ(0) 1 +CPHASE(0) 0 1 +X 0 +X 1 +CPHASE(0) 0 1 +X 0 +X 1 +""" + + +def test_two_qubit_diagonal_gate_quil_output(): + pyquil = pytest.importorskip("pyquil") + pyquil_simulation_tools = pytest.importorskip("pyquil.simulation.tools") + q0, q1 = _make_qubits(2) + operations = [ + cirq.TwoQubitDiagonalGate([np.pi / 2, 0, 0, 0])(q0, q1), + cirq.TwoQubitDiagonalGate([0, np.pi / 2, 0, 0])(q0, q1), + cirq.TwoQubitDiagonalGate([0, 0, np.pi / 2, 0])(q0, q1), + cirq.TwoQubitDiagonalGate([0, 0, 0, np.pi / 2])(q0, q1), + ] + output = cirq_rigetti.quil_output.QuilOutput(operations, (q0, q1)) + program = pyquil.Program(str(output)) + assert f"\n{program.out()}" == QUIL_CPHASES_PROGRAM + + pyquil_unitary = pyquil_simulation_tools.program_unitary(program, n_qubits=2) + # Qubit ordering differs between pyQuil and Cirq. + cirq_unitary = cirq.Circuit(cirq.SWAP(q0, q1), operations, cirq.SWAP(q0, q1)).unitary() + assert np.allclose(pyquil_unitary, cirq_unitary) + # Also test non-CPHASE case, which decomposes into X/RZ/CPhase + operations = [cirq.TwoQubitDiagonalGate([0, 0, 0, 0])(q0, q1)] + output = cirq_rigetti.quil_output.QuilOutput(operations, (q0, q1)) + program = pyquil.Program(str(output)) + assert f"\n{program.out()}" == QUIL_DIAGONAL_DECOMPOSE_PROGRAM + + +def test_parseable_defgate_output(): + pyquil = pytest.importorskip("pyquil") + q0, q1 = _make_qubits(2) + operations = [ + cirq_rigetti.quil_output.QuilOneQubitGate(np.array([[1, 0], [0, 1]])).on(q0), + cirq_rigetti.quil_output.QuilTwoQubitGate( + np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ).on(q0, q1), + ] + output = cirq_rigetti.quil_output.QuilOutput(operations, (q0, q1)) + # Just checks that we can create a pyQuil Program without crashing. + pyquil.Program(str(output)) + + +def test_unconveritble_op(): + (q0,) = _make_qubits(1) + + class MyGate(cirq.Gate): + def num_qubits(self) -> int: + return 1 + + op = MyGate()(q0) + + # Documenting that this + # operation would crash if you call _op_to_quil_directly + with pytest.raises(ValueError, match="Can't convert"): + _ = cirq_rigetti.quil_output.QuilOutput(op, (q0,))._op_to_quil(op)