Source code for mqt.qcec.compilation_flow_profiles

"""Module for generating compilation flow profiles for the equivalence checking process."""

from __future__ import annotations

from enum import Enum, unique
from pathlib import Path
from typing import TYPE_CHECKING, Any

import numpy as np

from ._compat.optional import HAS_QISKIT

if TYPE_CHECKING:
    from numpy.typing import NDArray
    from qiskit.circuit import QuantumCircuit

__all__ = [
    "AncillaMode",
    "generate_profile",
    "generate_profile_name",
]


def __dir__() -> list[str]:
    return __all__


[docs] @unique class AncillaMode(Enum): """Enum for the ancilla mode.""" NO_ANCILLA = "noancilla" """No ancilla qubits are used.""" RECURSION = "recursion" """A single ancilla is used in a recursive manner.""" V_CHAIN = "v-chain" """A chain of ancilla qubits is used.""" def __eq__(self, other: object) -> bool: """Check if two AncillaMode objects are equal. Supports string comparison.""" if isinstance(other, str): return self.value == other if isinstance(other, self.__class__): return self.value == other.value return False def __hash__(self) -> int: """Return the hash of the AncillaMode.""" return hash(self.value) def __str__(self) -> str: """Return the string representation of the AncillaMode.""" return self.value
single_qubit_gates_no_params = { "qubits": 1, "params": 0, "controls": 0, "gates": ["x", "y", "z", "h", "s", "sdg", "t", "tdg", "sx", "sxdg"], } single_qubit_gates_one_param = { "qubits": 1, "params": 1, "controls": 0, "gates": ["p", "rx", "ry", "rz"], } single_qubit_gates_three_params = { "qubits": 1, "params": 3, "controls": 0, "gates": ["u"], } two_qubit_gates_no_params = {"qubits": 2, "params": 0, "controls": 0, "gates": ["swap", "iswap", "dcx", "ecr"]} two_qubit_gates_one_param = { "qubits": 2, "params": 1, "controls": 0, "gates": ["rxx", "ryy", "rzz", "rzx"], } single_controlled_single_qubit_gates_no_params = { "qubits": 1, "params": 0, "controls": 1, "gates": ["x", "y", "z", "h", "sx"], } single_controlled_single_qubit_gates_one_param = { "qubits": 1, "params": 1, "controls": 1, "gates": ["p", "rx", "ry", "rz"], } single_controlled_single_qubit_gates_three_params = { "qubits": 1, "params": 4, "controls": 1, "gates": ["u"], } single_controlled_two_qubit_gates = { "qubits": 2, "params": 0, "controls": 1, "gates": ["swap"], } double_controlled_single_qubit_gates_no_params = { "qubits": 1, "params": 0, "controls": 2, "gates": ["x"], } max_controls = 11 control_range = range(2, max_controls + 1) mcx_no_ancilla = { "qubits": 1, "params": 0, "controls": control_range, "mode": AncillaMode.NO_ANCILLA, "ancilla_qubits": 0, "gates": ["x"], } mcx_recursion = { "qubits": 1, "params": 0, "controls": control_range, "mode": AncillaMode.RECURSION, "ancilla_qubits": 1, "gates": ["x"], } mcx_v_chain = { "qubits": 1, "params": 0, "controls": control_range, "mode": AncillaMode.V_CHAIN, "ancilla_qubits": None, "gates": ["x"], } mcphase = { "qubits": 1, "params": 1, "controls": control_range, "mode": None, "ancilla_qubits": 0, "gates": ["p"], } general_gates = [ single_qubit_gates_no_params, single_qubit_gates_one_param, single_qubit_gates_three_params, two_qubit_gates_no_params, two_qubit_gates_one_param, single_controlled_single_qubit_gates_no_params, single_controlled_single_qubit_gates_one_param, single_controlled_single_qubit_gates_three_params, single_controlled_two_qubit_gates, double_controlled_single_qubit_gates_no_params, ] multi_controlled_gates = [mcphase] multi_controlled_gates_no_ancilla = [mcx_no_ancilla] multi_controlled_gates_recursion = [mcx_recursion] multi_controlled_gates_v_chain = [mcx_v_chain] def __create_general_gate(qubits: int, params: int, controls: int, identifier: str) -> QuantumCircuit: """Create a ``QuantumCircuit`` containing a single gate ``identifier`` with the given number of ``qubits``, ``params``, and ``controls``.""" from qiskit.circuit import QuantumCircuit required_qubits = qubits + controls qc = QuantumCircuit(required_qubits) gate_identifier = "c" * controls + identifier rng = np.random.default_rng(seed=12345) parameter_list = [rng.uniform(-np.pi, np.pi) for _ in range(params)] getattr(qc, gate_identifier)(*parameter_list, *range(required_qubits)) return qc def __create_multi_controlled_gate( qubits: int, params: int, controls: int, mode: AncillaMode | None, ancilla_qubits: int | None, identifier: str, ) -> QuantumCircuit: """Create a ``QuantumCircuit`` containing a single multi-controlled gate ``identifier`` with the given number of ``qubits``, ``params``, and ``controls`` using ``ancilla_qubits`` ancilla qubits and the given ancilla ``mode``.""" from qiskit.circuit import QuantumCircuit required_qubits = qubits + controls # special handling for v-chain mode which is indicated by the ancilla_qubits being None if ancilla_qubits is None: ancilla_qubits = max(0, controls - 2) # special handling for recursion mode with less than 5 controls, # which does not require ancilla qubits no_ancilla_threshold = 5 if mode == "recursion" and controls < no_ancilla_threshold: ancilla_qubits = 0 required_qubits += ancilla_qubits qc = QuantumCircuit(required_qubits) gate_identifier = "mc" + identifier rng = np.random.default_rng(seed=12345) parameter_list = [rng.uniform(-np.pi, np.pi) for _ in range(params)] if mode is not None: getattr(qc, gate_identifier)( *parameter_list, control_qubits=list(range(controls)), target_qubit=controls, ancilla_qubits=list(range(controls + 1, controls + 1 + ancilla_qubits)), mode=mode, ) else: getattr(qc, gate_identifier)(*parameter_list, control_qubits=list(range(controls)), target_qubit=controls) return qc def __compute_cost( qc: QuantumCircuit, basis_gates: list[str], optimization_level: int = 1, ) -> int: """Compute the cost of a circuit by transpiling the circuit to a given ``basis_gates`` gate set and a certain ``optimization_level``.""" from qiskit import transpile transpiled_circuit = transpile( qc, basis_gates=basis_gates, optimization_level=optimization_level, seed_transpiler=12345 ) size: int = transpiled_circuit.size() return size class GateType(Enum): """Enum for gate types.""" GENERAL = 1 MULTI_CONTROLLED = 2 def __create_gate_profile_data( gate_collection: list[dict[str, Any]], gate_type: GateType, basis_gates: list[str] | None = None, optimization_level: int = 1, ) -> dict[tuple[str, int], int]: """Create a dictionary of gate profile data.""" if basis_gates is None: basis_gates = ["id", "rz", "sx", "x", "cx"] profile_data = {} for gate_set in gate_collection: gates = gate_set["gates"] qubits = gate_set["qubits"] params = gate_set["params"] controls = gate_set["controls"] # pack single control numbers into list if gate_type == GateType.GENERAL: controls = [controls] for gate in gates: for control in controls: qc = None # create the gate if gate_type == GateType.GENERAL: qc = __create_general_gate(qubits, params, control, gate) elif gate_type == GateType.MULTI_CONTROLLED: qc = __create_multi_controlled_gate( qubits, params, control, gate_set["mode"], gate_set["ancilla_qubits"], gate, ) # compute the cost cost = __compute_cost(qc, basis_gates, optimization_level) # add the cost to the profile data profile_data[gate, control] = cost return profile_data def __add_special_case_data( profile_data: dict[tuple[str, int], int], special_cases: dict[str, Any] | None = None, ) -> None: """Add special case data to the profile data. This is used to extrapolate the cost of specific rotation gates (e.g., S, T, ...) from the cost of the generic phase gate.""" if special_cases is None: special_cases = { "gates": ["z", "s", "sdg", "t", "tdg"], "underlying_gate": "p", } gates = special_cases["gates"] underlying_gate = special_cases["underlying_gate"] for (g, nc), cost in profile_data.copy().items(): if g is underlying_gate: for gate in gates: profile_data.setdefault((gate, nc), cost) def __write_profile_data_to_file(profile_data: dict[tuple[str, int], int], filename: Path) -> None: """Write the profile data to a file.""" HAS_QISKIT.require_now("generate compilation flow profiles") from qiskit import __version__ as qiskit_version with Path(filename).open("w+", encoding="utf-8") as f: f.write(f"# {filename}, Qiskit version: {qiskit_version}\n") f.writelines(f"{gate} {controls} {cost}\n" for (gate, controls), cost in profile_data.items()) def __check_recurrence(seq: list[int], order: int = 2) -> list[int] | None: """Determine a recurrence relation with a given ``order`` in ``sequence`` and return the corresponding coefficients or ``None`` if no relation was determined.""" if len(seq) < (2 * order + 1): return None mat = np.array([seq[i : i + order] for i in range(order)], dtype=int) f = np.array([seq[i + order] for i in range(order)], dtype=int) if np.linalg.det(mat) == 0: return None coefficients: NDArray[np.float64] = np.linalg.inv(mat).dot(f) for i in range(2 * order, len(seq)): predict: float = np.sum(coefficients * np.array(seq[i - order : i], dtype=float)) if abs(predict - seq[i]) > 10 ** (-2): return None return list(coefficients) def __find_continuation( profile_data: dict[tuple[str, int], int], gate: str, cutoff: int = 5, max_order: int = 3, max_control: int = 11, max_qubits: int = 128, prediction_cutoff: float = 1e6, ) -> None: """Extrapolate from the given profile data by finding recurrence relations.""" sequence = [cost for (g, _), cost in profile_data.items() if g == gate] # sort the sequence sequence = sorted(sequence) # cut off the sequence at the cutoff point sequence = sequence[cutoff:] coeffs = None for order in range(1, max_order + 1): coeffs = __check_recurrence(sequence, order) if coeffs is not None: break # if no recurrence sequence was found, assume the sequence is linear if coeffs is None: coeffs = [-1, 2] entries_to_compute = max_qubits - max_control - 1 order = len(coeffs) for i in range(entries_to_compute): next_term = int(np.round(np.sum(coeffs * np.array(sequence[-order:])))) if next_term >= prediction_cutoff: break sequence.append(next_term) profile_data[gate, max_control + i + 1] = next_term gate_collection_for_mode = { AncillaMode.NO_ANCILLA: multi_controlled_gates_no_ancilla, AncillaMode.RECURSION: multi_controlled_gates_recursion, AncillaMode.V_CHAIN: multi_controlled_gates_v_chain, } default_profile_path = Path(__file__).resolve().parent.joinpath("profiles")
[docs] def generate_profile_name(optimization_level: int = 1, mode: AncillaMode = AncillaMode.NO_ANCILLA) -> str: """Generate a profile name based on the given optimization level and ancilla mode.""" return "qiskit_O" + str(optimization_level) + "_" + str(mode) + ".profile"
[docs] def generate_profile( optimization_level: int = 1, mode: AncillaMode = AncillaMode.NO_ANCILLA, filepath: Path | None = None, ) -> None: """Generate a compilation flow profile for the given optimization level and ancilla mode. Args: optimization_level: The IBM Qiskit optimization level to use for the profile (0, 1, 2, or 3). Defaults to 1. mode: The :class:`ancilla mode <.AncillaMode>` used for realizing multi-controlled Toffoli gates, as available in Qiskit. Defaults to :attr:`.AncillaMode.NO_ANCILLA`. filepath: The path to the directory where the profile should be stored. Defaults to the ``profiles`` directory in the ``mqt.qcec`` package. """ if filepath is None: filepath = default_profile_path # generate general profile data profile = __create_gate_profile_data(general_gates, GateType.GENERAL, optimization_level=optimization_level) # add multi-controlled gates profile.update( __create_gate_profile_data( multi_controlled_gates, GateType.MULTI_CONTROLLED, optimization_level=optimization_level, ) ) __find_continuation(profile, gate="p", max_control=max_controls) gate_collection = gate_collection_for_mode[mode] # add multi-controlled gates with specific mode profile.update( __create_gate_profile_data( gate_collection, GateType.MULTI_CONTROLLED, optimization_level=optimization_level, ) ) __find_continuation(profile, gate="x", max_control=max_controls) # add special case data __add_special_case_data(profile) # write profile data to file filename = generate_profile_name(optimization_level=optimization_level, mode=mode) filepath = filepath.joinpath(filename) __write_profile_data_to_file(profile, filepath)